backend-common: forklift logging implementation to backend-app-api

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2023-01-10 16:56:03 +01:00
parent 240514363f
commit 3d5b5f89da
8 changed files with 5 additions and 1 deletions
@@ -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 })],
});
}