mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
rolling file appender: implement retention policy (#182346)
## Summary Fix https://github.com/elastic/kibana/issues/165191 Support more complex retention policies via the introduction of the `retention` configuration section of the `rolling-file` appender. 3 different retention policies are supported: - File count based (`maxFiles`) - Total file size based (`maxAccumulatedFileSize`) - Last modification date based (`removeOlderThan`) Backward-compatibility is assured by using the `strategy.max` (the only way to define the only retention before this PR) as a default for `retention.maxFiles` ## Examples #### maxFiles ```yaml logging: appenders: rolling-file: type: rolling-file fileName: /var/logs/kibana.log layout: type: pattern policy: type: time-interval interval: 1d modulate: true retention: maxFiles: 5 ``` #### maxAccumulatedFileSize ```yaml logging: appenders: rolling-file: type: rolling-file fileName: /var/logs/kibana.log layout: type: pattern policy: type: time-interval interval: 1d modulate: true retention: maxAccumulatedFileSize: "5GB" ``` #### removeOlderThan ```yaml logging: appenders: rolling-file: type: rolling-file fileName: /var/logs/kibana.log layout: type: pattern policy: type: time-interval interval: 1d modulate: true retention: removeOlderThan: "30d" ``` ## Release note Kibana's `rolling-file` appender now supports more advanced retention policies. Please refer to the Kibana logging documentation for more details
This commit is contained in:
parent
3b4aa1ebbc
commit
6ca7cbcffc
26 changed files with 862 additions and 109 deletions
|
@ -12,13 +12,16 @@ import type { RollingFileContext } from './rolling_file_context';
|
|||
import type { RollingFileManager } from './rolling_file_manager';
|
||||
import type { TriggeringPolicy } from './policies/policy';
|
||||
import type { RollingStrategy } from './strategies/strategy';
|
||||
import type { RetentionPolicy } from './retention/retention_policy';
|
||||
|
||||
const createContextMock = (filePath: string) => {
|
||||
const createContextMock = (filePath: string = 'kibana.log') => {
|
||||
const mock: jest.Mocked<RollingFileContext> = {
|
||||
currentFileSize: 0,
|
||||
currentFileTime: 0,
|
||||
filePath,
|
||||
refreshFileInfo: jest.fn(),
|
||||
getOrderedRolledFiles: jest.fn(),
|
||||
setOrderedRolledFileFn: jest.fn(),
|
||||
};
|
||||
return mock;
|
||||
};
|
||||
|
@ -52,10 +55,18 @@ const createFileManagerMock = () => {
|
|||
return mock;
|
||||
};
|
||||
|
||||
const createRetentionPolicyMock = () => {
|
||||
const mock: jest.Mocked<RetentionPolicy> = {
|
||||
apply: jest.fn(),
|
||||
};
|
||||
return mock;
|
||||
};
|
||||
|
||||
export const rollingFileAppenderMocks = {
|
||||
createContext: createContextMock,
|
||||
createStrategy: createStrategyMock,
|
||||
createPolicy: createPolicyMock,
|
||||
createLayout: createLayoutMock,
|
||||
createFileManager: createFileManagerMock,
|
||||
createRetentionPolicy: createRetentionPolicyMock,
|
||||
};
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import type { RollingStrategyConfig } from '@kbn/core-logging-server';
|
||||
import { mergeRetentionPolicyConfig } from './create_retention_policy';
|
||||
|
||||
describe('mergeRetentionPolicyConfig', () => {
|
||||
const createRollingStrategyConfig = (max: number): RollingStrategyConfig => {
|
||||
return {
|
||||
type: 'numeric',
|
||||
pattern: '-%i',
|
||||
max,
|
||||
};
|
||||
};
|
||||
|
||||
it('uses the value from the retention strategy config if defined', () => {
|
||||
const merged = mergeRetentionPolicyConfig({ maxFiles: 42 }, createRollingStrategyConfig(10));
|
||||
expect(merged).toEqual({
|
||||
maxFiles: 42,
|
||||
});
|
||||
});
|
||||
|
||||
it('uses the value from the rolling strategy config not defined on the retention config', () => {
|
||||
const merged = mergeRetentionPolicyConfig({}, createRollingStrategyConfig(10));
|
||||
expect(merged).toEqual({
|
||||
maxFiles: 10,
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import type { RetentionPolicyConfig, RollingStrategyConfig } from '@kbn/core-logging-server';
|
||||
import type { RollingFileContext } from '../rolling_file_context';
|
||||
import { GenericRetentionPolicy, type RetentionPolicy } from './retention_policy';
|
||||
|
||||
export const createRetentionPolicy = (
|
||||
config: RetentionPolicyConfig,
|
||||
context: RollingFileContext
|
||||
): RetentionPolicy => {
|
||||
return new GenericRetentionPolicy(config, context);
|
||||
};
|
||||
|
||||
export const mergeRetentionPolicyConfig = (
|
||||
config: RetentionPolicyConfig | undefined,
|
||||
strategyConfig: RollingStrategyConfig
|
||||
): RetentionPolicyConfig => {
|
||||
return {
|
||||
...config,
|
||||
maxFiles: config?.maxFiles || strategyConfig.max,
|
||||
};
|
||||
};
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { unlink, stat } from 'fs/promises';
|
||||
|
||||
export const deleteFiles = async ({ filesToDelete }: { filesToDelete: string[] }) => {
|
||||
await Promise.all(filesToDelete.map((fileToDelete) => unlink(fileToDelete)));
|
||||
};
|
||||
|
||||
export type GetFileInfoResult = { exist: false } | { exist: true; size: number; mtime: Date };
|
||||
|
||||
export const getFileInfo = async (absFilePath: string): Promise<GetFileInfoResult> => {
|
||||
try {
|
||||
const { size, mtime } = await stat(absFilePath);
|
||||
return { exist: true, size, mtime };
|
||||
} catch (e) {
|
||||
if (e.code === 'ENOENT') {
|
||||
return { exist: false };
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
};
|
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
export { retentionPolicyConfigSchema, type RetentionPolicy } from './retention_policy';
|
||||
export { createRetentionPolicy, mergeRetentionPolicyConfig } from './create_retention_policy';
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import type { listFilesOlderThan, listFilesExceedingSize } from './utils';
|
||||
import type { deleteFiles } from './fs';
|
||||
|
||||
export const listFilesExceedingSizeMock: jest.MockedFn<typeof listFilesExceedingSize> = jest.fn();
|
||||
export const listFilesOlderThanMock: jest.MockedFn<typeof listFilesOlderThan> = jest.fn();
|
||||
|
||||
jest.doMock('./utils', () => {
|
||||
const actual = jest.requireActual('./utils');
|
||||
return {
|
||||
...actual,
|
||||
listFilesExceedingSize: listFilesExceedingSizeMock,
|
||||
listFilesOlderThan: listFilesOlderThanMock,
|
||||
};
|
||||
});
|
||||
|
||||
export const deleteFilesMock: jest.MockedFn<typeof deleteFiles> = jest.fn();
|
||||
|
||||
jest.doMock('./fs', () => {
|
||||
const actual = jest.requireActual('./fs');
|
||||
return {
|
||||
...actual,
|
||||
deleteFiles: deleteFilesMock,
|
||||
};
|
||||
});
|
|
@ -0,0 +1,141 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import {
|
||||
listFilesExceedingSizeMock,
|
||||
listFilesOlderThanMock,
|
||||
deleteFilesMock,
|
||||
} from './retention_policy.test.mocks';
|
||||
import type { RetentionPolicyConfig } from '@kbn/core-logging-server';
|
||||
import { rollingFileAppenderMocks } from '../mocks';
|
||||
import { GenericRetentionPolicy, retentionPolicyConfigSchema } from './retention_policy';
|
||||
|
||||
describe('GenericRetentionPolicy', () => {
|
||||
let context: ReturnType<typeof rollingFileAppenderMocks.createContext>;
|
||||
let config: RetentionPolicyConfig;
|
||||
|
||||
beforeEach(() => {
|
||||
deleteFilesMock.mockReset();
|
||||
listFilesExceedingSizeMock.mockReset();
|
||||
listFilesOlderThanMock.mockReset();
|
||||
|
||||
context = rollingFileAppenderMocks.createContext();
|
||||
});
|
||||
|
||||
it('supports the maxFile directive', async () => {
|
||||
config = retentionPolicyConfigSchema.validate({
|
||||
maxFiles: 2,
|
||||
});
|
||||
context.getOrderedRolledFiles.mockResolvedValue(['file-1', 'file-2', 'file-3', 'file-4']);
|
||||
|
||||
const policy = new GenericRetentionPolicy(config, context);
|
||||
|
||||
await policy.apply();
|
||||
|
||||
expect(deleteFilesMock).toHaveBeenCalledTimes(1);
|
||||
expect(deleteFilesMock).toHaveBeenCalledWith({
|
||||
filesToDelete: ['file-3', 'file-4'],
|
||||
});
|
||||
});
|
||||
|
||||
it('supports the maxAccumulatedFileSize directive', async () => {
|
||||
config = retentionPolicyConfigSchema.validate({
|
||||
maxAccumulatedFileSize: '50b',
|
||||
});
|
||||
context.getOrderedRolledFiles.mockResolvedValue(['file-1', 'file-2', 'file-3', 'file-4']);
|
||||
|
||||
listFilesExceedingSizeMock.mockResolvedValue(['file-4']);
|
||||
|
||||
const policy = new GenericRetentionPolicy(config, context);
|
||||
|
||||
await policy.apply();
|
||||
|
||||
expect(listFilesOlderThanMock).not.toHaveBeenCalled();
|
||||
expect(listFilesExceedingSizeMock).toHaveBeenCalledTimes(1);
|
||||
expect(listFilesExceedingSizeMock).toHaveBeenCalledWith({
|
||||
orderedFiles: ['file-1', 'file-2', 'file-3', 'file-4'],
|
||||
maxSizeInBytes: config.maxAccumulatedFileSize!.getValueInBytes(),
|
||||
});
|
||||
|
||||
expect(deleteFilesMock).toHaveBeenCalledTimes(1);
|
||||
expect(deleteFilesMock).toHaveBeenCalledWith({
|
||||
filesToDelete: ['file-4'],
|
||||
});
|
||||
});
|
||||
|
||||
it('supports the removeOlderThan directive', async () => {
|
||||
config = retentionPolicyConfigSchema.validate({
|
||||
removeOlderThan: '30d',
|
||||
});
|
||||
context.getOrderedRolledFiles.mockResolvedValue(['file-1', 'file-2', 'file-3', 'file-4']);
|
||||
|
||||
listFilesOlderThanMock.mockResolvedValue(['file-2', 'file-3', 'file-4']);
|
||||
|
||||
const policy = new GenericRetentionPolicy(config, context);
|
||||
|
||||
await policy.apply();
|
||||
|
||||
expect(listFilesExceedingSizeMock).not.toHaveBeenCalled();
|
||||
expect(listFilesOlderThanMock).toHaveBeenCalledTimes(1);
|
||||
expect(listFilesOlderThanMock).toHaveBeenCalledWith({
|
||||
orderedFiles: ['file-1', 'file-2', 'file-3', 'file-4'],
|
||||
duration: config.removeOlderThan!,
|
||||
});
|
||||
|
||||
expect(deleteFilesMock).toHaveBeenCalledTimes(1);
|
||||
expect(deleteFilesMock).toHaveBeenCalledWith({
|
||||
filesToDelete: ['file-2', 'file-3', 'file-4'],
|
||||
});
|
||||
});
|
||||
|
||||
it('supports all directives at the same time', async () => {
|
||||
config = retentionPolicyConfigSchema.validate({
|
||||
maxFiles: 3,
|
||||
removeOlderThan: '30d',
|
||||
maxAccumulatedFileSize: '50b',
|
||||
});
|
||||
context.getOrderedRolledFiles.mockResolvedValue(['file-1', 'file-2', 'file-3', 'file-4']);
|
||||
|
||||
listFilesOlderThanMock.mockResolvedValue(['file-2']);
|
||||
listFilesExceedingSizeMock.mockResolvedValue(['file-3']);
|
||||
|
||||
const policy = new GenericRetentionPolicy(config, context);
|
||||
|
||||
await policy.apply();
|
||||
|
||||
expect(listFilesExceedingSizeMock).toHaveBeenCalledTimes(1);
|
||||
expect(listFilesExceedingSizeMock).toHaveBeenCalledWith({
|
||||
orderedFiles: ['file-1', 'file-2', 'file-3', 'file-4'],
|
||||
maxSizeInBytes: config.maxAccumulatedFileSize!.getValueInBytes(),
|
||||
});
|
||||
|
||||
expect(listFilesOlderThanMock).toHaveBeenCalledTimes(1);
|
||||
expect(listFilesOlderThanMock).toHaveBeenCalledWith({
|
||||
orderedFiles: ['file-1', 'file-2', 'file-3', 'file-4'],
|
||||
duration: config.removeOlderThan!,
|
||||
});
|
||||
|
||||
expect(deleteFilesMock).toHaveBeenCalledTimes(1);
|
||||
expect(deleteFilesMock).toHaveBeenCalledWith({
|
||||
filesToDelete: ['file-4', 'file-3', 'file-2'],
|
||||
});
|
||||
});
|
||||
|
||||
it('do not call deleteFiles if no file should be deleted', async () => {
|
||||
config = retentionPolicyConfigSchema.validate({
|
||||
maxFiles: 5,
|
||||
});
|
||||
context.getOrderedRolledFiles.mockResolvedValue(['file-1', 'file-2', 'file-3', 'file-4']);
|
||||
|
||||
const policy = new GenericRetentionPolicy(config, context);
|
||||
|
||||
await policy.apply();
|
||||
|
||||
expect(deleteFilesMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,85 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { dirname, join } from 'path';
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import type { RetentionPolicyConfig } from '@kbn/core-logging-server';
|
||||
import type { RollingFileContext } from '../rolling_file_context';
|
||||
import { deleteFiles } from './fs';
|
||||
import { listFilesExceedingSize, listFilesOlderThan } from './utils';
|
||||
|
||||
export const retentionPolicyConfigSchema = schema.object(
|
||||
{
|
||||
maxFiles: schema.maybe(schema.number({ min: 1, max: 365 })),
|
||||
maxAccumulatedFileSize: schema.maybe(schema.byteSize()),
|
||||
removeOlderThan: schema.maybe(schema.duration({ max: '365d' })),
|
||||
},
|
||||
{
|
||||
validate: (config) => {
|
||||
if (!config.maxFiles && !config.maxAccumulatedFileSize && !config.removeOlderThan) {
|
||||
return 'Retention policy must define at least one directive: maxFiles, maxAccumulatedFileSize or removeOlderThan';
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
export interface RetentionPolicy {
|
||||
/**
|
||||
* Apply the configured policy, checking the existing log files bound to the appender
|
||||
* and disposing of those that should.
|
||||
*/
|
||||
apply(): Promise<void>;
|
||||
}
|
||||
|
||||
export class GenericRetentionPolicy implements RetentionPolicy {
|
||||
private readonly logFileFolder;
|
||||
|
||||
constructor(
|
||||
private readonly config: RetentionPolicyConfig,
|
||||
private readonly context: RollingFileContext
|
||||
) {
|
||||
this.logFileFolder = dirname(this.context.filePath);
|
||||
}
|
||||
|
||||
async apply() {
|
||||
const { maxFiles, maxAccumulatedFileSize, removeOlderThan } = this.config;
|
||||
const orderedFilesBaseNames = await this.context.getOrderedRolledFiles();
|
||||
const absOrderedFiles = orderedFilesBaseNames.map((filepath) =>
|
||||
join(this.logFileFolder, filepath)
|
||||
);
|
||||
|
||||
const filesToDelete: Set<string> = new Set();
|
||||
|
||||
if (maxFiles) {
|
||||
const exceedingFiles = absOrderedFiles.slice(maxFiles, absOrderedFiles.length);
|
||||
exceedingFiles.forEach((file) => filesToDelete.add(file));
|
||||
}
|
||||
|
||||
if (maxAccumulatedFileSize) {
|
||||
const toRemoveByFileSize = await listFilesExceedingSize({
|
||||
orderedFiles: absOrderedFiles,
|
||||
maxSizeInBytes: maxAccumulatedFileSize.getValueInBytes(),
|
||||
});
|
||||
toRemoveByFileSize.forEach((file) => filesToDelete.add(file));
|
||||
}
|
||||
|
||||
if (removeOlderThan) {
|
||||
const toRemoveByAge = await listFilesOlderThan({
|
||||
orderedFiles: absOrderedFiles,
|
||||
duration: removeOlderThan,
|
||||
});
|
||||
toRemoveByAge.forEach((file) => filesToDelete.add(file));
|
||||
}
|
||||
|
||||
if (filesToDelete.size > 0) {
|
||||
await deleteFiles({
|
||||
filesToDelete: [...filesToDelete],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import type { getFileInfo } from './fs';
|
||||
|
||||
export const getFileInfoMock: jest.MockedFn<typeof getFileInfo> = jest.fn();
|
||||
|
||||
jest.doMock('./fs', () => {
|
||||
const actual = jest.requireActual('./fs');
|
||||
return {
|
||||
...actual,
|
||||
getFileInfo: getFileInfoMock,
|
||||
};
|
||||
});
|
|
@ -0,0 +1,104 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import moment from 'moment-timezone';
|
||||
import { getFileInfoMock } from './utils.test.mocks';
|
||||
import { listFilesOlderThan, listFilesExceedingSize } from './utils';
|
||||
|
||||
describe('listFilesExceedingSize', () => {
|
||||
beforeEach(() => {
|
||||
getFileInfoMock.mockReset();
|
||||
});
|
||||
|
||||
const mockWithSizeMap = (fileSizeMap: Record<string, number | undefined>) => {
|
||||
getFileInfoMock.mockImplementation(async (fileName) => {
|
||||
const fileSize = fileSizeMap[fileName];
|
||||
if (fileSize !== undefined) {
|
||||
return { exist: true, size: fileSize, mtime: new Date() };
|
||||
}
|
||||
return { exist: false };
|
||||
});
|
||||
};
|
||||
|
||||
it('returns an empty list if total accumulated size is lower than the limit', async () => {
|
||||
mockWithSizeMap({
|
||||
'file-1': 50,
|
||||
'file-2': 50,
|
||||
'file-3': 50,
|
||||
});
|
||||
|
||||
const result = await listFilesExceedingSize({
|
||||
orderedFiles: ['file-1', 'file-2', 'file-3'],
|
||||
maxSizeInBytes: 200,
|
||||
});
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('returns the list of files over the limit, including the one that breached the limit', async () => {
|
||||
mockWithSizeMap({
|
||||
'file-1': 100,
|
||||
'file-2': 100,
|
||||
'file-3': 100,
|
||||
'file-4': 100,
|
||||
});
|
||||
|
||||
const result = await listFilesExceedingSize({
|
||||
orderedFiles: ['file-1', 'file-2', 'file-3', 'file-4'],
|
||||
maxSizeInBytes: 250,
|
||||
});
|
||||
|
||||
expect(result).toEqual(['file-3', 'file-4']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('listFilesOlderThan', () => {
|
||||
beforeEach(() => {
|
||||
getFileInfoMock.mockReset();
|
||||
});
|
||||
|
||||
const mockWithMtime = (fileMtimeMap: Record<string, Date | undefined>) => {
|
||||
getFileInfoMock.mockImplementation(async (fileName) => {
|
||||
const fileDate = fileMtimeMap[fileName];
|
||||
if (fileDate !== undefined) {
|
||||
return { exist: true, size: 0, mtime: fileDate };
|
||||
}
|
||||
return { exist: false };
|
||||
});
|
||||
};
|
||||
|
||||
it('returns an empty list if total accumulated size is lower than the limit', async () => {
|
||||
mockWithMtime({
|
||||
'file-1': moment().add(-1, 'day').toDate(),
|
||||
'file-2': moment().add(-10, 'day').toDate(),
|
||||
'file-3': moment().add(-20, 'day').toDate(),
|
||||
});
|
||||
|
||||
const result = await listFilesOlderThan({
|
||||
orderedFiles: ['file-1', 'file-2', 'file-3'],
|
||||
duration: moment.duration(15, 'day'),
|
||||
});
|
||||
|
||||
expect(result).toEqual(['file-3']);
|
||||
});
|
||||
|
||||
it('ignores files that were not found', async () => {
|
||||
mockWithMtime({
|
||||
'file-1': moment().add(-1, 'day').toDate(),
|
||||
'file-2': moment().add(-20, 'day').toDate(),
|
||||
'file-3': undefined,
|
||||
});
|
||||
|
||||
const result = await listFilesOlderThan({
|
||||
orderedFiles: ['file-1', 'file-2', 'file-3'],
|
||||
duration: moment.duration(15, 'day'),
|
||||
});
|
||||
|
||||
expect(result).toEqual(['file-2']);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import moment, { type Duration } from 'moment-timezone';
|
||||
import { getFileInfo } from './fs';
|
||||
|
||||
export const listFilesExceedingSize = async ({
|
||||
orderedFiles,
|
||||
maxSizeInBytes,
|
||||
}: {
|
||||
orderedFiles: string[];
|
||||
maxSizeInBytes: number;
|
||||
}): Promise<string[]> => {
|
||||
let currentSize = 0;
|
||||
|
||||
for (let i = 0; i < orderedFiles.length; i++) {
|
||||
const filePath = orderedFiles[i];
|
||||
const stats = await getFileInfo(filePath);
|
||||
if (stats.exist) {
|
||||
currentSize += stats.size;
|
||||
if (currentSize > maxSizeInBytes) {
|
||||
return orderedFiles.slice(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
export const listFilesOlderThan = async ({
|
||||
orderedFiles,
|
||||
duration,
|
||||
}: {
|
||||
orderedFiles: string[];
|
||||
duration: Duration;
|
||||
}): Promise<string[]> => {
|
||||
const filesOlderThanLimit = [];
|
||||
const timeLimit = moment().subtract(duration).toDate().getTime();
|
||||
|
||||
for (let i = 0; i < orderedFiles.length; i++) {
|
||||
const filePath = orderedFiles[i];
|
||||
const stats = await getFileInfo(filePath);
|
||||
if (stats.exist) {
|
||||
if (stats.mtime.getTime() < timeLimit) {
|
||||
filesOlderThanLimit.push(filePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return filesOlderThanLimit;
|
||||
};
|
|
@ -38,10 +38,20 @@ jest.doMock('./rolling_file_context', () => ({
|
|||
RollingFileContext: RollingFileContextMock,
|
||||
}));
|
||||
|
||||
export const createRetentionPolicyMock = jest.fn();
|
||||
jest.doMock('./retention', () => {
|
||||
const actual = jest.requireActual('./retention');
|
||||
return {
|
||||
...actual,
|
||||
createRetentionPolicy: createRetentionPolicyMock,
|
||||
};
|
||||
});
|
||||
|
||||
export const resetAllMocks = () => {
|
||||
LayoutsMock.create.mockReset();
|
||||
createTriggeringPolicyMock.mockReset();
|
||||
createRollingStrategyMock.mockReset();
|
||||
createRetentionPolicyMock.mockReset();
|
||||
RollingFileManagerMock.mockReset();
|
||||
RollingFileContextMock.mockReset();
|
||||
};
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
import {
|
||||
createRollingStrategyMock,
|
||||
createTriggeringPolicyMock,
|
||||
createRetentionPolicyMock,
|
||||
LayoutsMock,
|
||||
resetAllMocks,
|
||||
RollingFileContextMock,
|
||||
|
@ -38,6 +39,9 @@ const config: RollingFileAppenderConfig = {
|
|||
max: 5,
|
||||
pattern: '-%i',
|
||||
},
|
||||
retention: {
|
||||
maxFiles: 7,
|
||||
},
|
||||
};
|
||||
|
||||
const createLogRecord = (parts: Partial<LogRecord> = {}): LogRecord => ({
|
||||
|
@ -72,6 +76,7 @@ describe('RollingFileAppender', () => {
|
|||
let layout: ReturnType<typeof rollingFileAppenderMocks.createLayout>;
|
||||
let strategy: ReturnType<typeof rollingFileAppenderMocks.createStrategy>;
|
||||
let policy: ReturnType<typeof rollingFileAppenderMocks.createPolicy>;
|
||||
let retention: ReturnType<typeof rollingFileAppenderMocks.createRetentionPolicy>;
|
||||
let context: ReturnType<typeof rollingFileAppenderMocks.createContext>;
|
||||
let fileManager: ReturnType<typeof rollingFileAppenderMocks.createFileManager>;
|
||||
|
||||
|
@ -85,6 +90,9 @@ describe('RollingFileAppender', () => {
|
|||
strategy = rollingFileAppenderMocks.createStrategy();
|
||||
createRollingStrategyMock.mockReturnValue(strategy);
|
||||
|
||||
retention = rollingFileAppenderMocks.createRetentionPolicy();
|
||||
createRetentionPolicyMock.mockReturnValue(retention);
|
||||
|
||||
context = rollingFileAppenderMocks.createContext('file-path');
|
||||
RollingFileContextMock.mockImplementation(() => context);
|
||||
|
||||
|
@ -111,6 +119,9 @@ describe('RollingFileAppender', () => {
|
|||
expect(createTriggeringPolicyMock).toHaveBeenCalledTimes(1);
|
||||
expect(createTriggeringPolicyMock).toHaveBeenCalledWith(config.policy, context);
|
||||
|
||||
expect(createRetentionPolicyMock).toHaveBeenCalledTimes(1);
|
||||
expect(createRetentionPolicyMock).toHaveBeenCalledWith(config.retention, context);
|
||||
|
||||
expect(createRollingStrategyMock).toHaveBeenCalledTimes(1);
|
||||
expect(createRollingStrategyMock).toHaveBeenCalledWith(config.strategy, context);
|
||||
});
|
||||
|
@ -189,6 +200,21 @@ describe('RollingFileAppender', () => {
|
|||
expect(fileManager.closeStream).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('triggers the retention policy once the rollout is complete', async () => {
|
||||
const { promise, resolve } = createPromiseResolver();
|
||||
strategy.rollout.mockReturnValue(promise);
|
||||
|
||||
const log = createLogRecord({ message: '1' });
|
||||
appender.append(log);
|
||||
|
||||
expect(retention.apply).not.toHaveBeenCalled();
|
||||
|
||||
resolve();
|
||||
await nextTick();
|
||||
|
||||
expect(retention.apply).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('logs the event once the rollout is complete', async () => {
|
||||
const { promise, resolve } = createPromiseResolver();
|
||||
strategy.rollout.mockReturnValue(promise);
|
||||
|
|
|
@ -7,12 +7,26 @@
|
|||
*/
|
||||
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import { LogRecord, Layout, DisposableAppender } from '@kbn/logging';
|
||||
import type { LogRecord, Layout, DisposableAppender } from '@kbn/logging';
|
||||
import type { RollingFileAppenderConfig } from '@kbn/core-logging-server';
|
||||
import { Layouts } from '../../layouts/layouts';
|
||||
import { BufferAppender } from '../buffer/buffer_appender';
|
||||
import { createTriggeringPolicy, triggeringPolicyConfigSchema, TriggeringPolicy } from './policies';
|
||||
import { RollingStrategy, createRollingStrategy, rollingStrategyConfigSchema } from './strategies';
|
||||
import {
|
||||
createTriggeringPolicy,
|
||||
triggeringPolicyConfigSchema,
|
||||
type TriggeringPolicy,
|
||||
} from './policies';
|
||||
import {
|
||||
createRollingStrategy,
|
||||
rollingStrategyConfigSchema,
|
||||
type RollingStrategy,
|
||||
} from './strategies';
|
||||
import {
|
||||
createRetentionPolicy,
|
||||
mergeRetentionPolicyConfig,
|
||||
retentionPolicyConfigSchema,
|
||||
type RetentionPolicy,
|
||||
} from './retention';
|
||||
import { RollingFileManager } from './rolling_file_manager';
|
||||
import { RollingFileContext } from './rolling_file_context';
|
||||
|
||||
|
@ -27,6 +41,7 @@ export class RollingFileAppender implements DisposableAppender {
|
|||
fileName: schema.string(),
|
||||
policy: triggeringPolicyConfigSchema,
|
||||
strategy: rollingStrategyConfigSchema,
|
||||
retention: schema.maybe(retentionPolicyConfigSchema),
|
||||
});
|
||||
|
||||
private isRolling = false;
|
||||
|
@ -36,8 +51,9 @@ export class RollingFileAppender implements DisposableAppender {
|
|||
private readonly layout: Layout;
|
||||
private readonly context: RollingFileContext;
|
||||
private readonly fileManager: RollingFileManager;
|
||||
private readonly policy: TriggeringPolicy;
|
||||
private readonly strategy: RollingStrategy;
|
||||
private readonly triggeringPolicy: TriggeringPolicy;
|
||||
private readonly rollingStrategy: RollingStrategy;
|
||||
private readonly retentionPolicy: RetentionPolicy;
|
||||
private readonly buffer: BufferAppender;
|
||||
|
||||
constructor(config: RollingFileAppenderConfig) {
|
||||
|
@ -45,8 +61,12 @@ export class RollingFileAppender implements DisposableAppender {
|
|||
this.context.refreshFileInfo();
|
||||
this.fileManager = new RollingFileManager(this.context);
|
||||
this.layout = Layouts.create(config.layout);
|
||||
this.policy = createTriggeringPolicy(config.policy, this.context);
|
||||
this.strategy = createRollingStrategy(config.strategy, this.context);
|
||||
this.triggeringPolicy = createTriggeringPolicy(config.policy, this.context);
|
||||
this.rollingStrategy = createRollingStrategy(config.strategy, this.context);
|
||||
this.retentionPolicy = createRetentionPolicy(
|
||||
mergeRetentionPolicyConfig(config.retention, config.strategy),
|
||||
this.context
|
||||
);
|
||||
this.buffer = new BufferAppender();
|
||||
}
|
||||
|
||||
|
@ -96,8 +116,9 @@ export class RollingFileAppender implements DisposableAppender {
|
|||
}
|
||||
this.isRolling = true;
|
||||
try {
|
||||
await this.strategy.rollout();
|
||||
await this.rollingStrategy.rollout();
|
||||
await this.fileManager.closeStream();
|
||||
await this.retentionPolicy.apply();
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('[RollingFileAppender]: error while rolling file: ', e);
|
||||
|
@ -129,6 +150,6 @@ export class RollingFileAppender implements DisposableAppender {
|
|||
* Checks if the current event should trigger a rollout
|
||||
*/
|
||||
private needRollout(record: LogRecord) {
|
||||
return this.policy.isTriggeringEvent(record);
|
||||
return this.triggeringPolicy.isTriggeringEvent(record);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,8 @@
|
|||
|
||||
import { statSync } from 'fs';
|
||||
|
||||
export type GetOrderedRolledFileFn = () => Promise<string[]>;
|
||||
|
||||
/**
|
||||
* Context shared between the rolling file manager, policy and strategy.
|
||||
*/
|
||||
|
@ -36,4 +38,17 @@ export class RollingFileContext {
|
|||
this.currentFileSize = 0;
|
||||
}
|
||||
}
|
||||
|
||||
#orderedRolledFileFn?: GetOrderedRolledFileFn;
|
||||
|
||||
public async getOrderedRolledFiles(): Promise<string[]> {
|
||||
if (this.#orderedRolledFileFn) {
|
||||
return this.#orderedRolledFileFn();
|
||||
}
|
||||
throw new Error('orderedRolledFileFn not registered on the context');
|
||||
}
|
||||
|
||||
public setOrderedRolledFileFn(fn: GetOrderedRolledFileFn) {
|
||||
this.#orderedRolledFileFn = fn;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,14 +7,12 @@
|
|||
*/
|
||||
|
||||
export const getOrderedRolledFilesMock = jest.fn();
|
||||
export const deleteFilesMock = jest.fn();
|
||||
export const rollPreviousFilesInOrderMock = jest.fn();
|
||||
export const rollCurrentFileMock = jest.fn();
|
||||
export const shouldSkipRolloutMock = jest.fn();
|
||||
|
||||
jest.doMock('./rolling_tasks', () => ({
|
||||
getOrderedRolledFiles: getOrderedRolledFilesMock,
|
||||
deleteFiles: deleteFilesMock,
|
||||
rollPreviousFilesInOrder: rollPreviousFilesInOrderMock,
|
||||
rollCurrentFile: rollCurrentFileMock,
|
||||
shouldSkipRollout: shouldSkipRolloutMock,
|
||||
|
@ -23,7 +21,6 @@ jest.doMock('./rolling_tasks', () => ({
|
|||
export const resetAllMock = () => {
|
||||
shouldSkipRolloutMock.mockReset();
|
||||
getOrderedRolledFilesMock.mockReset();
|
||||
deleteFilesMock.mockReset();
|
||||
rollPreviousFilesInOrderMock.mockReset();
|
||||
rollCurrentFileMock.mockReset();
|
||||
};
|
||||
|
|
|
@ -10,7 +10,6 @@ import { join } from 'path';
|
|||
import {
|
||||
resetAllMock,
|
||||
shouldSkipRolloutMock,
|
||||
deleteFilesMock,
|
||||
getOrderedRolledFilesMock,
|
||||
rollCurrentFileMock,
|
||||
rollPreviousFilesInOrderMock,
|
||||
|
@ -42,6 +41,11 @@ describe('NumericRollingStrategy', () => {
|
|||
resetAllMock();
|
||||
});
|
||||
|
||||
it('calls `context.setOrderedRolledFileFn` in constructor', () => {
|
||||
expect(context.setOrderedRolledFileFn).toHaveBeenCalledTimes(1);
|
||||
expect(context.setOrderedRolledFileFn).toHaveBeenCalledWith(expect.any(Function));
|
||||
});
|
||||
|
||||
it('calls `getOrderedRolledFiles` with the correct parameters', async () => {
|
||||
await strategy.rollout();
|
||||
|
||||
|
@ -53,23 +57,6 @@ describe('NumericRollingStrategy', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('calls `deleteFiles` with the correct files', async () => {
|
||||
getOrderedRolledFilesMock.mockResolvedValue([
|
||||
'kibana.1.log',
|
||||
'kibana.2.log',
|
||||
'kibana.3.log',
|
||||
'kibana.4.log',
|
||||
]);
|
||||
|
||||
await strategy.rollout();
|
||||
|
||||
expect(deleteFilesMock).toHaveBeenCalledTimes(1);
|
||||
expect(deleteFilesMock).toHaveBeenCalledWith({
|
||||
filesToDelete: ['kibana.3.log', 'kibana.4.log'],
|
||||
logFileFolder,
|
||||
});
|
||||
});
|
||||
|
||||
it('calls `rollPreviousFilesInOrder` with the correct files', async () => {
|
||||
getOrderedRolledFilesMock.mockResolvedValue([
|
||||
'kibana.1.log',
|
||||
|
@ -82,7 +69,7 @@ describe('NumericRollingStrategy', () => {
|
|||
|
||||
expect(rollPreviousFilesInOrderMock).toHaveBeenCalledTimes(1);
|
||||
expect(rollPreviousFilesInOrderMock).toHaveBeenCalledWith({
|
||||
filesToRoll: ['kibana.1.log', 'kibana.2.log'],
|
||||
filesToRoll: ['kibana.1.log', 'kibana.2.log', 'kibana.3.log', 'kibana.4.log'],
|
||||
logFileFolder,
|
||||
logFileBaseName,
|
||||
pattern,
|
||||
|
@ -116,24 +103,14 @@ describe('NumericRollingStrategy', () => {
|
|||
|
||||
await strategy.rollout();
|
||||
|
||||
const deleteFilesCall = deleteFilesMock.mock.invocationCallOrder[0];
|
||||
const rollPreviousFilesInOrderCall = rollPreviousFilesInOrderMock.mock.invocationCallOrder[0];
|
||||
const rollCurrentFileCall = rollCurrentFileMock.mock.invocationCallOrder[0];
|
||||
const refreshFileInfoCall = context.refreshFileInfo.mock.invocationCallOrder[0];
|
||||
|
||||
expect(deleteFilesCall).toBeLessThan(rollPreviousFilesInOrderCall);
|
||||
expect(rollPreviousFilesInOrderCall).toBeLessThan(rollCurrentFileCall);
|
||||
expect(rollCurrentFileCall).toBeLessThan(refreshFileInfoCall);
|
||||
});
|
||||
|
||||
it('do not calls `deleteFiles` if no file should be deleted', async () => {
|
||||
getOrderedRolledFilesMock.mockResolvedValue(['kibana.1.log', 'kibana.2.log']);
|
||||
|
||||
await strategy.rollout();
|
||||
|
||||
expect(deleteFilesMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('do not calls `rollPreviousFilesInOrder` if no file should be rolled', async () => {
|
||||
getOrderedRolledFilesMock.mockResolvedValue([]);
|
||||
|
||||
|
@ -154,7 +131,6 @@ describe('NumericRollingStrategy', () => {
|
|||
await strategy.rollout();
|
||||
|
||||
expect(getOrderedRolledFilesMock).not.toHaveBeenCalled();
|
||||
expect(deleteFilesMock).not.toHaveBeenCalled();
|
||||
expect(rollPreviousFilesInOrderMock).not.toHaveBeenCalled();
|
||||
expect(rollCurrentFileMock).not.toHaveBeenCalled();
|
||||
expect(context.refreshFileInfo).not.toHaveBeenCalled();
|
||||
|
|
|
@ -14,7 +14,6 @@ import { RollingFileContext } from '../../rolling_file_context';
|
|||
import {
|
||||
shouldSkipRollout,
|
||||
getOrderedRolledFiles,
|
||||
deleteFiles,
|
||||
rollCurrentFile,
|
||||
rollPreviousFilesInOrder,
|
||||
} from './rolling_tasks';
|
||||
|
@ -46,7 +45,8 @@ export const numericRollingStrategyConfigSchema = schema.object({
|
|||
* strategy:
|
||||
* type: numeric
|
||||
* pattern: "-%i"
|
||||
* max: 2
|
||||
* retention:
|
||||
* maxFiles: 2
|
||||
* ```
|
||||
* - During the first rollover kibana.log is renamed to kibana-1.log. A new kibana.log file is created and starts
|
||||
* being written to.
|
||||
|
@ -69,6 +69,15 @@ export class NumericRollingStrategy implements RollingStrategy {
|
|||
this.logFilePath = this.context.filePath;
|
||||
this.logFileBaseName = basename(this.context.filePath);
|
||||
this.logFileFolder = dirname(this.context.filePath);
|
||||
context.setOrderedRolledFileFn(this.getOrderedRolledFiles.bind(this));
|
||||
}
|
||||
|
||||
private async getOrderedRolledFiles() {
|
||||
return await getOrderedRolledFiles({
|
||||
logFileFolder: this.logFileFolder,
|
||||
logFileBaseName: this.logFileBaseName,
|
||||
pattern: this.config.pattern,
|
||||
});
|
||||
}
|
||||
|
||||
async rollout() {
|
||||
|
@ -82,20 +91,15 @@ export class NumericRollingStrategy implements RollingStrategy {
|
|||
}
|
||||
|
||||
// get the files matching the pattern in the folder, and sort them by `%i` value
|
||||
const orderedFiles = await getOrderedRolledFiles({
|
||||
logFileFolder,
|
||||
logFileBaseName,
|
||||
pattern,
|
||||
});
|
||||
const filesToRoll = orderedFiles.slice(0, this.config.max - 1);
|
||||
const filesToDelete = orderedFiles.slice(filesToRoll.length, orderedFiles.length);
|
||||
const orderedFilesToRoll = await this.getOrderedRolledFiles();
|
||||
|
||||
if (filesToDelete.length > 0) {
|
||||
await deleteFiles({ logFileFolder, filesToDelete });
|
||||
}
|
||||
|
||||
if (filesToRoll.length > 0) {
|
||||
await rollPreviousFilesInOrder({ filesToRoll, logFileFolder, logFileBaseName, pattern });
|
||||
if (orderedFilesToRoll.length > 0) {
|
||||
await rollPreviousFilesInOrder({
|
||||
filesToRoll: orderedFilesToRoll,
|
||||
logFileFolder,
|
||||
logFileBaseName,
|
||||
pattern,
|
||||
});
|
||||
}
|
||||
|
||||
await rollCurrentFile({ pattern, logFileBaseName, logFileFolder });
|
||||
|
|
|
@ -7,20 +7,17 @@
|
|||
*/
|
||||
|
||||
export const readdirMock = jest.fn();
|
||||
export const unlinkMock = jest.fn();
|
||||
export const renameMock = jest.fn();
|
||||
export const accessMock = jest.fn();
|
||||
|
||||
jest.doMock('fs/promises', () => ({
|
||||
readdir: readdirMock,
|
||||
unlink: unlinkMock,
|
||||
rename: renameMock,
|
||||
access: accessMock,
|
||||
}));
|
||||
|
||||
export const clearAllMocks = () => {
|
||||
readdirMock.mockClear();
|
||||
unlinkMock.mockClear();
|
||||
renameMock.mockClear();
|
||||
accessMock.mockClear();
|
||||
};
|
||||
|
|
|
@ -7,18 +7,11 @@
|
|||
*/
|
||||
|
||||
import { join } from 'path';
|
||||
import {
|
||||
accessMock,
|
||||
readdirMock,
|
||||
renameMock,
|
||||
unlinkMock,
|
||||
clearAllMocks,
|
||||
} from './rolling_tasks.test.mocks';
|
||||
import { accessMock, readdirMock, renameMock, clearAllMocks } from './rolling_tasks.test.mocks';
|
||||
import {
|
||||
shouldSkipRollout,
|
||||
rollCurrentFile,
|
||||
rollPreviousFilesInOrder,
|
||||
deleteFiles,
|
||||
getOrderedRolledFiles,
|
||||
} from './rolling_tasks';
|
||||
|
||||
|
@ -97,26 +90,6 @@ describe('NumericRollingStrategy tasks', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('deleteFiles', () => {
|
||||
it('calls `unlink` once for each file', async () => {
|
||||
await deleteFiles({
|
||||
logFileFolder: 'log-folder',
|
||||
filesToDelete: ['file-a', 'file-b', 'file-c'],
|
||||
});
|
||||
|
||||
expect(unlinkMock).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
it('calls `unlink` with the correct parameters', async () => {
|
||||
await deleteFiles({
|
||||
logFileFolder: 'log-folder',
|
||||
filesToDelete: ['file-a', 'file-b'],
|
||||
});
|
||||
|
||||
expect(unlinkMock).toHaveBeenNthCalledWith(1, join('log-folder', 'file-a'));
|
||||
expect(unlinkMock).toHaveBeenNthCalledWith(2, join('log-folder', 'file-b'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('getOrderedRolledFiles', () => {
|
||||
it('returns the rolled files matching the pattern in order', async () => {
|
||||
readdirMock.mockResolvedValue([
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
*/
|
||||
|
||||
import { join } from 'path';
|
||||
import { readdir, unlink, access } from 'fs/promises';
|
||||
import { readdir, access } from 'fs/promises';
|
||||
import { getFileNameMatcher, getRollingFileName } from './pattern_matcher';
|
||||
import { moveFile } from './utils';
|
||||
|
||||
|
@ -47,16 +47,6 @@ export const getOrderedRolledFiles = async ({
|
|||
.map(({ fileName }) => fileName);
|
||||
};
|
||||
|
||||
export const deleteFiles = async ({
|
||||
logFileFolder,
|
||||
filesToDelete,
|
||||
}: {
|
||||
logFileFolder: string;
|
||||
filesToDelete: string[];
|
||||
}) => {
|
||||
await Promise.all(filesToDelete.map((fileToDelete) => unlink(join(logFileFolder, fileToDelete))));
|
||||
};
|
||||
|
||||
export const rollPreviousFilesInOrder = async ({
|
||||
filesToRoll,
|
||||
logFileFolder,
|
||||
|
|
|
@ -23,4 +23,5 @@ export type {
|
|||
RewritePolicyConfig,
|
||||
MetaRewritePolicyConfigProperty,
|
||||
MetaRewritePolicyConfig,
|
||||
RetentionPolicyConfig,
|
||||
} from './src/appenders';
|
||||
|
|
|
@ -26,6 +26,7 @@ export type {
|
|||
TimeIntervalTriggeringPolicyConfig,
|
||||
NumericRollingStrategyConfig,
|
||||
RollingStrategyConfig,
|
||||
RetentionPolicyConfig,
|
||||
} from './rolling_file';
|
||||
|
||||
/** @public */
|
||||
|
|
|
@ -32,6 +32,10 @@ export interface RollingFileAppenderConfig {
|
|||
* The {@link RollingStrategy | rollout strategy} to use for rolling.
|
||||
*/
|
||||
strategy: RollingStrategyConfig;
|
||||
/**
|
||||
* The {@link RetentionPolicy | retention strategy} to use to know which files to keep.
|
||||
*/
|
||||
retention?: RetentionPolicyConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -106,6 +110,14 @@ export interface NumericRollingStrategyConfig {
|
|||
/**
|
||||
* The maximum number of files to keep. Once this number is reached, oldest
|
||||
* files will be deleted. Defaults to `7`
|
||||
*
|
||||
* @deprecated use retention policy instead
|
||||
*/
|
||||
max: number;
|
||||
}
|
||||
|
||||
export interface RetentionPolicyConfig {
|
||||
maxFiles?: number;
|
||||
maxAccumulatedFileSize?: ByteSizeValue;
|
||||
removeOlderThan?: Duration;
|
||||
}
|
||||
|
|
|
@ -18,7 +18,8 @@ tags: ['kibana','dev', 'contributor', 'api docs']
|
|||
- [Appenders](#appenders)
|
||||
- [Rolling File Appender](#rolling-file-appender)
|
||||
- [Triggering Policies](#triggering-policies)
|
||||
- [Rolling strategies](#rolling-strategies)
|
||||
- [Rolling Strategies](#rolling-strategies)
|
||||
- [Retention Policies](#retention-policies)
|
||||
- [Configuration](#configuration)
|
||||
- [Usage](#usage)
|
||||
- [Logging config migration](#logging-config-migration)
|
||||
|
@ -270,15 +271,13 @@ The default value is `true`.
|
|||
|
||||
#### Rolling strategies
|
||||
|
||||
The rolling strategy determines how the rollover should occur: both the naming of the rolled files,
|
||||
and their retention policy.
|
||||
The rolling strategy determines how the naming of the rolled file should be performed.
|
||||
|
||||
There is currently one strategy supported: `numeric`.
|
||||
|
||||
##### NumericRollingStrategy
|
||||
|
||||
This strategy will suffix the file with a given pattern when rolling,
|
||||
and will retains a fixed amount of rolled files.
|
||||
This strategy will suffix the file with a given pattern when rolling.
|
||||
|
||||
```yaml
|
||||
logging:
|
||||
|
@ -291,7 +290,8 @@ logging:
|
|||
strategy:
|
||||
type: numeric
|
||||
pattern: '-%i'
|
||||
max: 2
|
||||
retention:
|
||||
maxFiles: 2
|
||||
layout:
|
||||
type: pattern
|
||||
```
|
||||
|
@ -323,6 +323,86 @@ The maximum number of files to keep. Once this number is reached, oldest files w
|
|||
|
||||
The default value is `7`
|
||||
|
||||
**deprecated**: Please use a retention policy instead.
|
||||
|
||||
#### Retention Policies
|
||||
|
||||
The retention policy defines when the rolled files must be deleted.
|
||||
|
||||
##### maxFiles
|
||||
|
||||
Retention policy that will keep, at max, the defined number of files (ordered from the most recent to the oldest one)
|
||||
and delete the others.
|
||||
|
||||
This is the same behavior as what was previously achieved via the deprecated `strategy.max` field.
|
||||
|
||||
**Example**: Appender configured to keep `5` rolled files:
|
||||
```yaml
|
||||
logging:
|
||||
appenders:
|
||||
rolling-file:
|
||||
type: rolling-file
|
||||
fileName: /var/logs/kibana.log
|
||||
layout:
|
||||
type: pattern
|
||||
policy:
|
||||
// ...
|
||||
strategy:
|
||||
// ...
|
||||
retention:
|
||||
maxFiles: 5
|
||||
```
|
||||
|
||||
##### maxAccumulatedFileSize
|
||||
|
||||
Retention policy based on the size on disk used by the rolled files. Size count will be
|
||||
performed from the most recent file to the oldest one, and will delete all files
|
||||
(including the one breaching the threshold) over the limit. Only the size from the rolled files is taken into
|
||||
account (the active file is ignored).
|
||||
|
||||
*Note:* The field supports the following optional suffixes: `b`, `kb`, `mb`, `gb` and `tb` (case-insensitive). The default suffix is `b`
|
||||
|
||||
**Example**: Appender configured to keep 5GB of rolled logs at maximum:
|
||||
```yaml
|
||||
logging:
|
||||
appenders:
|
||||
rolling-file:
|
||||
type: rolling-file
|
||||
fileName: /var/logs/kibana.log
|
||||
layout:
|
||||
type: pattern
|
||||
policy:
|
||||
// ...
|
||||
strategy:
|
||||
// ...
|
||||
retention:
|
||||
maxAccumulatedFileSize: "5GB"
|
||||
```
|
||||
|
||||
##### removeOlderThan
|
||||
|
||||
Retention policy based on the time (modification date - `mtime`) the files were rolled, and that will delete all files
|
||||
that are older than the specified duration.
|
||||
|
||||
*Note:* The field supports the following optional suffixes: `ms`, `s`, `m`, `h`, `d`, `w`, `M` and `Y`. The default suffix is `ms`.
|
||||
|
||||
**Example**: Appender configured to delete rolled files after 30 days:
|
||||
```yaml
|
||||
logging:
|
||||
appenders:
|
||||
rolling-file:
|
||||
type: rolling-file
|
||||
fileName: /var/logs/kibana.log
|
||||
layout:
|
||||
type: pattern
|
||||
policy:
|
||||
// ...
|
||||
strategy:
|
||||
// ...
|
||||
retention:
|
||||
removeOlderThan: "30d"
|
||||
```
|
||||
|
||||
### Rewrite Appender
|
||||
|
||||
*This appender is currently considered experimental and is not intended
|
||||
|
|
|
@ -60,7 +60,7 @@ describe('RollingFileAppender', () => {
|
|||
const expectedFileContent = (indices: number[]) => indices.map(message).join('\n') + '\n';
|
||||
|
||||
describe('`size-limit` policy with `numeric` strategy', () => {
|
||||
it('rolls the log file in the correct order', async () => {
|
||||
it('supports the deprecated `strategy.max` field', async () => {
|
||||
root = createRoot({
|
||||
type: 'rolling-file',
|
||||
fileName: logFile,
|
||||
|
@ -106,7 +106,7 @@ describe('RollingFileAppender', () => {
|
|||
expect(await getFileContent('kibana.2.log')).toEqual(expectedFileContent([1, 2, 3]));
|
||||
});
|
||||
|
||||
it('only keep the correct number of files', async () => {
|
||||
it('maxFile retention: only keep the correct number of files', async () => {
|
||||
root = createRoot({
|
||||
type: 'rolling-file',
|
||||
fileName: logFile,
|
||||
|
@ -120,9 +120,11 @@ describe('RollingFileAppender', () => {
|
|||
},
|
||||
strategy: {
|
||||
type: 'numeric',
|
||||
max: 2,
|
||||
pattern: '-%i',
|
||||
},
|
||||
retention: {
|
||||
maxFiles: 2,
|
||||
},
|
||||
});
|
||||
await root.preboot();
|
||||
await root.setup();
|
||||
|
@ -153,6 +155,107 @@ describe('RollingFileAppender', () => {
|
|||
expect(await getFileContent('kibana-1.log')).toEqual(expectedFileContent([5, 6]));
|
||||
expect(await getFileContent('kibana-2.log')).toEqual(expectedFileContent([3, 4]));
|
||||
});
|
||||
|
||||
it('maxAccumulatedFileSize retention: only keep the correct number of files', async () => {
|
||||
root = createRoot({
|
||||
type: 'rolling-file',
|
||||
fileName: logFile,
|
||||
layout: {
|
||||
type: 'pattern',
|
||||
pattern: '%message',
|
||||
},
|
||||
policy: {
|
||||
type: 'size-limit',
|
||||
size: '60b',
|
||||
},
|
||||
strategy: {
|
||||
type: 'numeric',
|
||||
pattern: '-%i',
|
||||
},
|
||||
retention: {
|
||||
maxAccumulatedFileSize: '100b',
|
||||
},
|
||||
});
|
||||
await root.preboot();
|
||||
await root.setup();
|
||||
|
||||
const logger = root.logger.get('test.rolling.file');
|
||||
|
||||
// size = 60b, message.length ~= 40b, should roll every 2 message
|
||||
|
||||
// last file - 'kibana-3.log' (which will be removed during rolling)
|
||||
logger.info(message(1));
|
||||
logger.info(message(2));
|
||||
// roll - 'kibana-2.log' (which will be removed during rolling)
|
||||
logger.info(message(3));
|
||||
logger.info(message(4));
|
||||
// roll - 'kibana-1.log'
|
||||
logger.info(message(5));
|
||||
logger.info(message(6));
|
||||
// roll - 'kibana.log'
|
||||
logger.info(message(7));
|
||||
logger.info(message(8));
|
||||
|
||||
await flush();
|
||||
|
||||
const files = await readdir(testDir);
|
||||
|
||||
expect(files.sort()).toEqual(['kibana-1.log', 'kibana.log']);
|
||||
expect(await getFileContent('kibana.log')).toEqual(expectedFileContent([7, 8]));
|
||||
expect(await getFileContent('kibana-1.log')).toEqual(expectedFileContent([5, 6]));
|
||||
});
|
||||
|
||||
it('removeOlderThan retention: only keep the correct files', async () => {
|
||||
root = createRoot({
|
||||
type: 'rolling-file',
|
||||
fileName: logFile,
|
||||
layout: {
|
||||
type: 'pattern',
|
||||
pattern: '%message',
|
||||
},
|
||||
policy: {
|
||||
type: 'size-limit',
|
||||
size: '60b',
|
||||
},
|
||||
strategy: {
|
||||
type: 'numeric',
|
||||
pattern: '-%i',
|
||||
},
|
||||
retention: {
|
||||
removeOlderThan: '2s',
|
||||
},
|
||||
});
|
||||
await root.preboot();
|
||||
await root.setup();
|
||||
|
||||
const logger = root.logger.get('test.rolling.file');
|
||||
|
||||
// size = 60b, message.length ~= 40b, should roll every 2 message
|
||||
|
||||
// last file - 'kibana-3.log' (which will be removed during rolling)
|
||||
logger.info(message(1));
|
||||
logger.info(message(2));
|
||||
// roll - 'kibana-2.log' (which will be removed during rolling)
|
||||
logger.info(message(3));
|
||||
logger.info(message(4));
|
||||
|
||||
await delay(2500);
|
||||
|
||||
// roll - 'kibana-1.log'
|
||||
logger.info(message(5));
|
||||
logger.info(message(6));
|
||||
// roll - 'kibana.log'
|
||||
logger.info(message(7));
|
||||
logger.info(message(8));
|
||||
|
||||
await flush();
|
||||
|
||||
const files = await readdir(testDir);
|
||||
|
||||
expect(files.sort()).toEqual(['kibana-1.log', 'kibana.log']);
|
||||
expect(await getFileContent('kibana.log')).toEqual(expectedFileContent([7, 8]));
|
||||
expect(await getFileContent('kibana-1.log')).toEqual(expectedFileContent([5, 6]));
|
||||
});
|
||||
});
|
||||
|
||||
describe('`time-interval` policy with `numeric` strategy', () => {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue