backend-common: forklift logging implementation to backend-app-api
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
@@ -1,45 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as winston from 'winston';
|
||||
import { TransformableInfo } from 'logform';
|
||||
|
||||
const coloredTemplate = (info: TransformableInfo) => {
|
||||
const { timestamp, level, message, plugin, service, ...fields } = info;
|
||||
const colorizer = winston.format.colorize();
|
||||
const prefix = plugin || service;
|
||||
const timestampColor = colorizer.colorize('timestamp', timestamp);
|
||||
const prefixColor = colorizer.colorize('prefix', prefix);
|
||||
|
||||
const extraFields = Object.entries(fields)
|
||||
.map(([key, value]) => `${colorizer.colorize('field', `${key}`)}=${value}`)
|
||||
.join(' ');
|
||||
|
||||
return `${timestampColor} ${prefixColor} ${level} ${message} ${extraFields}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* A logging format that adds coloring to console output.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const coloredFormat = winston.format.combine(
|
||||
winston.format.timestamp(),
|
||||
winston.format.colorize({
|
||||
colors: { timestamp: 'dim', prefix: 'blue', field: 'cyan', debug: 'grey' },
|
||||
}),
|
||||
winston.format.printf(coloredTemplate),
|
||||
);
|
||||
@@ -1,25 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export * from './formats';
|
||||
export {
|
||||
createRootLogger,
|
||||
getRootLogger,
|
||||
setRootLogger,
|
||||
redactWinstonLogLine,
|
||||
} from './rootLogger';
|
||||
export * from './voidLogger';
|
||||
export { loggerToWinstonLogger } from './loggerToWinstonLogger';
|
||||
@@ -1,63 +0,0 @@
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { LoggerService } from '@backstage/backend-plugin-api';
|
||||
import { Logger as WinstonLogger, createLogger } from 'winston';
|
||||
import Transport, { TransportStreamOptions } from 'winston-transport';
|
||||
|
||||
class BackstageLoggerTransport extends Transport {
|
||||
constructor(
|
||||
private readonly backstageLogger: LoggerService,
|
||||
opts?: TransportStreamOptions,
|
||||
) {
|
||||
super(opts);
|
||||
}
|
||||
|
||||
log(info: unknown, callback: VoidFunction) {
|
||||
if (typeof info !== 'object' || info === null) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
const { level, message, ...meta } = info as { [name: string]: unknown };
|
||||
switch (level) {
|
||||
case 'error':
|
||||
this.backstageLogger.error(String(message), meta);
|
||||
break;
|
||||
case 'warn':
|
||||
this.backstageLogger.warn(String(message), meta);
|
||||
break;
|
||||
case 'info':
|
||||
this.backstageLogger.info(String(message), meta);
|
||||
break;
|
||||
case 'debug':
|
||||
this.backstageLogger.debug(String(message), meta);
|
||||
break;
|
||||
default:
|
||||
this.backstageLogger.info(String(message), meta);
|
||||
}
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export function loggerToWinstonLogger(
|
||||
logger: LoggerService,
|
||||
opts?: TransportStreamOptions,
|
||||
): WinstonLogger {
|
||||
return createLogger({
|
||||
transports: [new BackstageLoggerTransport(logger, opts)],
|
||||
});
|
||||
}
|
||||
@@ -1,164 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as winston from 'winston';
|
||||
import {
|
||||
createRootLogger,
|
||||
getRootLogger,
|
||||
setRootLogger,
|
||||
setRootLoggerRedactionList,
|
||||
} from './rootLogger';
|
||||
|
||||
describe('rootLogger', () => {
|
||||
it('can replace the default logger', () => {
|
||||
const logger = winston.createLogger();
|
||||
jest.spyOn(logger, 'info').mockReturnValue(logger);
|
||||
|
||||
setRootLogger(logger);
|
||||
getRootLogger().info('testing');
|
||||
|
||||
expect(logger.info).toHaveBeenCalledWith(
|
||||
expect.stringContaining('testing'),
|
||||
);
|
||||
});
|
||||
|
||||
it('redacts given secrets', () => {
|
||||
const transport = new winston.transports.Console();
|
||||
const logger = createRootLogger({ transports: [transport] });
|
||||
jest.spyOn(transport, 'write');
|
||||
setRootLoggerRedactionList(['SECRET-1', 'SECRET_2', 'SECRET.3']);
|
||||
logger.info('Logging SECRET-1 and SECRET_2 and SECRET.3');
|
||||
|
||||
expect(transport.write).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
message: 'Logging [REDACTED] and [REDACTED] and [REDACTED]',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('redacts but ignores empty and one-character secrets', () => {
|
||||
const transport = new winston.transports.Console();
|
||||
const logger = createRootLogger({ transports: [transport] });
|
||||
jest.spyOn(transport, 'write');
|
||||
setRootLoggerRedactionList(['SECRET-1', 'SECRET_2', 'Q', '']);
|
||||
logger.info('Logging SECRET-1 and SECRET_2 and Q');
|
||||
|
||||
expect(transport.write).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
message: 'Logging [REDACTED] and [REDACTED] and Q',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
describe('createRootLogger', () => {
|
||||
it('creates a new logger', () => {
|
||||
const oldLogger = getRootLogger();
|
||||
const newLogger = createRootLogger();
|
||||
|
||||
expect(oldLogger).not.toBe(newLogger);
|
||||
});
|
||||
|
||||
it('replaces the existing root logger', () => {
|
||||
const oldLogger = getRootLogger();
|
||||
createRootLogger();
|
||||
const newLogger = getRootLogger();
|
||||
expect(oldLogger).not.toBe(newLogger);
|
||||
});
|
||||
|
||||
it('can append additional default metadata', () => {
|
||||
const format = winston.format.json();
|
||||
const logger = createRootLogger({
|
||||
format,
|
||||
defaultMeta: {
|
||||
appName: 'backstage',
|
||||
appEnv: 'prod',
|
||||
containerId: 'abc',
|
||||
},
|
||||
});
|
||||
jest.spyOn(format, 'transform');
|
||||
|
||||
logger.info('testing');
|
||||
|
||||
expect(format.transform).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
message: 'testing',
|
||||
service: 'backstage',
|
||||
appName: 'backstage',
|
||||
appEnv: 'prod',
|
||||
containerId: 'abc',
|
||||
}),
|
||||
{},
|
||||
);
|
||||
});
|
||||
|
||||
it('can add override existing transports', () => {
|
||||
const transport = new winston.transports.Console({ level: 'debug' });
|
||||
const logger = createRootLogger({ transports: [transport] });
|
||||
expect(logger.transports.length).toBe(1);
|
||||
expect(logger.transports[0]).toBe(transport);
|
||||
});
|
||||
|
||||
it('can append an additional transport', () => {
|
||||
const logger = createRootLogger();
|
||||
const transport = new winston.transports.Console({ level: 'debug' });
|
||||
logger.add(transport);
|
||||
expect(logger.transports.length).toBe(2);
|
||||
expect(logger.transports[1]).toBe(transport);
|
||||
expect(logger.transports[1].level).toBe('debug');
|
||||
});
|
||||
|
||||
it('can override default format', () => {
|
||||
const format = winston.format(() => false)();
|
||||
const logger = createRootLogger({ format });
|
||||
expect(
|
||||
logger.format.transform({ message: 'hello', level: 'info' }),
|
||||
).toBeFalsy();
|
||||
});
|
||||
|
||||
it('can override the service label', () => {
|
||||
const transport = new winston.transports.Console();
|
||||
const logger = createRootLogger({ transports: [transport] });
|
||||
const writeSpy = jest
|
||||
.spyOn(transport, 'write')
|
||||
.mockImplementation((_c, _e) => true);
|
||||
|
||||
logger.info('msg-a');
|
||||
logger.child({ service: 'b' }).info('msg-b');
|
||||
logger.info('msg-c', { service: 'c' });
|
||||
|
||||
expect(writeSpy.mock.calls).toEqual([
|
||||
[
|
||||
expect.objectContaining({
|
||||
message: 'msg-a',
|
||||
service: 'backstage',
|
||||
}),
|
||||
],
|
||||
[
|
||||
expect.objectContaining({
|
||||
message: 'msg-b',
|
||||
service: 'b',
|
||||
}),
|
||||
],
|
||||
[
|
||||
expect.objectContaining({
|
||||
message: 'msg-c',
|
||||
service: 'c',
|
||||
}),
|
||||
],
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,131 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { merge } from 'lodash';
|
||||
import * as winston from 'winston';
|
||||
import { LoggerOptions } from 'winston';
|
||||
import { coloredFormat } from './formats';
|
||||
import { escapeRegExp } from '../util/escapeRegExp';
|
||||
|
||||
let rootLogger: winston.Logger;
|
||||
let redactionRegExp: RegExp | undefined;
|
||||
|
||||
/**
|
||||
* Gets the current root logger.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export function getRootLogger(): winston.Logger {
|
||||
return rootLogger;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a completely custom default "root" logger.
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* This is the logger instance that will be the foundation for all other logger
|
||||
* instances passed to plugins etc, in a given backend.
|
||||
*
|
||||
* Only use this if you absolutely need to make a completely custom logger.
|
||||
* Normally if you want to make light adaptations to the default logger
|
||||
* behavior, you would instead call {@link createRootLogger}.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export function setRootLogger(newLogger: winston.Logger) {
|
||||
rootLogger = newLogger;
|
||||
}
|
||||
|
||||
export function setRootLoggerRedactionList(redactionList: string[]) {
|
||||
// Exclude secrets that are empty or just one character in length. These
|
||||
// typically mean that you are running local dev or tests, or using the
|
||||
// --lax flag which sets things to just 'x'. So exclude those.
|
||||
const filtered = redactionList.filter(r => r.length > 1);
|
||||
|
||||
if (filtered.length) {
|
||||
redactionRegExp = new RegExp(
|
||||
`(${filtered.map(escapeRegExp).join('|')})`,
|
||||
'g',
|
||||
);
|
||||
} else {
|
||||
redactionRegExp = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A winston formatting function that finds occurrences of filteredKeys
|
||||
* and replaces them with the corresponding identifier.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export function redactWinstonLogLine(info: winston.Logform.TransformableInfo) {
|
||||
// TODO(hhogg): The logger is created before the config is loaded, because the
|
||||
// logger is needed in the config loader. There is a risk of a secret being
|
||||
// logged out during the config loading stage.
|
||||
// TODO(freben): Added a check that info.message actually was a string,
|
||||
// because it turned out that this was not necessarily guaranteed.
|
||||
// https://github.com/backstage/backstage/issues/8306
|
||||
if (redactionRegExp && typeof info.message === 'string') {
|
||||
info.message = info.message.replace(redactionRegExp, '[REDACTED]');
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a default "root" logger. This also calls {@link setRootLogger} under
|
||||
* the hood.
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* This is the logger instance that will be the foundation for all other logger
|
||||
* instances passed to plugins etc, in a given backend.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export function createRootLogger(
|
||||
options: winston.LoggerOptions = {},
|
||||
env = process.env,
|
||||
): winston.Logger {
|
||||
const logger = winston
|
||||
.createLogger(
|
||||
merge<LoggerOptions, LoggerOptions>(
|
||||
{
|
||||
level: env.LOG_LEVEL || 'info',
|
||||
format: winston.format.combine(
|
||||
winston.format(redactWinstonLogLine)(),
|
||||
env.NODE_ENV === 'production'
|
||||
? winston.format.json()
|
||||
: coloredFormat,
|
||||
),
|
||||
transports: [
|
||||
new winston.transports.Console({
|
||||
silent: env.JEST_WORKER_ID !== undefined && !env.LOG_LEVEL,
|
||||
}),
|
||||
],
|
||||
},
|
||||
options,
|
||||
),
|
||||
)
|
||||
.child({ service: 'backstage' });
|
||||
|
||||
setRootLogger(logger);
|
||||
|
||||
return logger;
|
||||
}
|
||||
|
||||
rootLogger = createRootLogger();
|
||||
@@ -1,28 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as winston from 'winston';
|
||||
|
||||
/**
|
||||
* A logger that just throws away all messages.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export function getVoidLogger(): winston.Logger {
|
||||
return winston.createLogger({
|
||||
transports: [new winston.transports.Console({ silent: true })],
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user