Merge pull request #15688 from backstage/rugvip/conflogport

backend-app-api: move over logger and config implementations from backend-common
This commit is contained in:
Patrik Oldsberg
2023-01-16 14:30:31 +01:00
committed by GitHub
31 changed files with 1003 additions and 640 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/backend-common': patch
---
Internal refactor of the logger and configuration loading implementations.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/backend-app-api': patch
---
Moved over logging and configuration loading implementations from `@backstage/backend-common`. There is a now `WinstonLogger` which implements the `RootLoggerService` through Winston with accompanying utilities. For configuration the `loadBackendConfig` function has been moved over, but it now instead returns an object with a `config` property.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/backend-plugin-api': patch
---
Updated the `RootLoggerService` to also have an `addRedactions` method.
+58
View File
@@ -12,13 +12,16 @@ import { CorsOptions } from 'cors';
import { ErrorRequestHandler } from 'express';
import { Express as Express_2 } from 'express';
import { ExtensionPoint } from '@backstage/backend-plugin-api';
import { Format } from 'logform';
import { Handler } from 'express';
import { HelmetOptions } from 'helmet';
import * as http from 'http';
import { HttpRouterService } from '@backstage/backend-plugin-api';
import { IdentityService } from '@backstage/backend-plugin-api';
import { LifecycleService } from '@backstage/backend-plugin-api';
import { LoadConfigOptionsRemote } from '@backstage/config-loader';
import { LoggerService } from '@backstage/backend-plugin-api';
import { LogMeta } from '@backstage/backend-plugin-api';
import { PermissionsService } from '@backstage/backend-plugin-api';
import { PluginCacheManager } from '@backstage/backend-common';
import { PluginDatabaseManager } from '@backstage/backend-common';
@@ -33,6 +36,7 @@ import { ServiceFactory } from '@backstage/backend-plugin-api';
import { ServiceFactoryOrFunction } from '@backstage/backend-plugin-api';
import { ServiceRef } from '@backstage/backend-plugin-api';
import { TokenManagerService } from '@backstage/backend-plugin-api';
import { transport } from 'winston';
import { UrlReader } from '@backstage/backend-common';
// @public (undocumented)
@@ -51,6 +55,18 @@ export const cacheFactory: () => ServiceFactory<PluginCacheManager>;
// @public (undocumented)
export const configFactory: () => ServiceFactory<ConfigService>;
// @public (undocumented)
export interface ConfigFactoryOptions {
argv?: string[];
remote?: LoadConfigOptionsRemote;
}
// @public (undocumented)
export function createConfigSecretEnumerator(options: {
logger: LoggerService;
dir?: string;
}): Promise<(config: Config) => Iterable<string>>;
// @public
export function createHttpServer(
listener: RequestListener,
@@ -147,6 +163,14 @@ export type IdentityFactoryOptions = {
// @public
export const lifecycleFactory: () => ServiceFactory<LifecycleService>;
// @public
export function loadBackendConfig(options: {
remote?: LoadConfigOptionsRemote;
argv: string[];
}): Promise<{
config: Config;
}>;
// @public (undocumented)
export const loggerFactory: () => ServiceFactory<LoggerService>;
@@ -231,4 +255,38 @@ export const tokenManagerFactory: () => ServiceFactory<TokenManagerService>;
// @public (undocumented)
export const urlReaderFactory: () => ServiceFactory<UrlReader>;
// @public
export class WinstonLogger implements RootLoggerService {
// (undocumented)
addRedactions(redactions: Iterable<string>): void;
// (undocumented)
child(meta: LogMeta): LoggerService;
static colorFormat(): Format;
static create(options: WinstonLoggerOptions): WinstonLogger;
// (undocumented)
debug(message: string, meta?: LogMeta): void;
// (undocumented)
error(message: string, meta?: LogMeta): void;
// (undocumented)
info(message: string, meta?: LogMeta): void;
static redacter(): {
format: Format;
add: (redactions: Iterable<string>) => void;
};
// (undocumented)
warn(message: string, meta?: LogMeta): void;
}
// @public (undocumented)
export interface WinstonLoggerOptions {
// (undocumented)
format: Format;
// (undocumented)
level: string;
// (undocumented)
meta?: LogMeta;
// (undocumented)
transports: transport[];
}
```
+9 -1
View File
@@ -36,10 +36,14 @@
"@backstage/backend-common": "workspace:^",
"@backstage/backend-plugin-api": "workspace:^",
"@backstage/backend-tasks": "workspace:^",
"@backstage/cli-common": "workspace:^",
"@backstage/config": "workspace:^",
"@backstage/config-loader": "workspace:^",
"@backstage/errors": "workspace:^",
"@backstage/plugin-auth-node": "workspace:^",
"@backstage/plugin-permission-node": "workspace:^",
"@backstage/types": "workspace:^",
"@manypkg/get-packages": "^1.1.3",
"@types/cors": "^2.8.6",
"@types/express": "^4.17.6",
"compression": "^1.7.4",
@@ -49,18 +53,22 @@
"fs-extra": "10.1.0",
"helmet": "^6.0.0",
"lodash": "^4.17.21",
"logform": "^2.3.2",
"minimatch": "^5.0.0",
"minimist": "^1.2.5",
"morgan": "^1.10.0",
"node-forge": "^1.3.1",
"selfsigned": "^2.0.0",
"stoppable": "^1.1.0",
"winston": "^3.2.1"
"winston": "^3.2.1",
"winston-transport": "^4.5.0"
},
"devDependencies": {
"@backstage/cli": "workspace:^",
"@types/compression": "^1.7.0",
"@types/fs-extra": "^9.0.3",
"@types/http-errors": "^2.0.0",
"@types/minimist": "^1.2.0",
"@types/morgan": "^1.9.0",
"@types/node-forge": "^1.3.0",
"@types/stoppable": "^1.1.0",
@@ -14,19 +14,12 @@
* limitations under the License.
*/
import { LoggerService } from '@backstage/backend-plugin-api';
import { ConfigReader } from '@backstage/config';
import { ObservableConfigProxy } from './config';
import { ObservableConfigProxy } from './ObservableConfigProxy';
describe('ObservableConfigProxy', () => {
const errLogger = {
error: (message: string) => {
throw new Error(message);
},
} as unknown as LoggerService;
it('should notify subscribers', () => {
const config = new ObservableConfigProxy(errLogger);
const config = new ObservableConfigProxy();
const fn = jest.fn();
const sub = config.subscribe(fn);
@@ -51,7 +44,7 @@ describe('ObservableConfigProxy', () => {
});
it('should forward subscriptions', () => {
const config1 = new ObservableConfigProxy(errLogger);
const config1 = new ObservableConfigProxy();
const fn1 = jest.fn();
const fn2 = jest.fn();
@@ -119,7 +112,7 @@ describe('ObservableConfigProxy', () => {
});
it('should make sub configs available as expected', () => {
const config = new ObservableConfigProxy(errLogger);
const config = new ObservableConfigProxy();
config.setConfig(new ConfigReader({ a: { x: 1 } }));
@@ -0,0 +1,129 @@
/*
* Copyright 2023 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 { ConfigService } from '@backstage/backend-plugin-api';
import { ConfigReader } from '@backstage/config';
import { JsonValue } from '@backstage/types';
export class ObservableConfigProxy implements ConfigService {
private config: ConfigService = new ConfigReader({});
private readonly subscribers: (() => void)[] = [];
constructor(
private readonly parent?: ObservableConfigProxy,
private parentKey?: string,
) {
if (parent && !parentKey) {
throw new Error('parentKey is required if parent is set');
}
}
setConfig(config: ConfigService) {
if (this.parent) {
throw new Error('immutable');
}
this.config = config;
for (const subscriber of this.subscribers) {
try {
subscriber();
} catch (error) {
console.error(`Config subscriber threw error, ${error}`);
}
}
}
subscribe(onChange: () => void): { unsubscribe: () => void } {
if (this.parent) {
return this.parent.subscribe(onChange);
}
this.subscribers.push(onChange);
return {
unsubscribe: () => {
const index = this.subscribers.indexOf(onChange);
if (index >= 0) {
this.subscribers.splice(index, 1);
}
},
};
}
private select(required: true): ConfigService;
private select(required: false): ConfigService | undefined;
private select(required: boolean): ConfigService | undefined {
if (this.parent && this.parentKey) {
if (required) {
return this.parent.select(true).getConfig(this.parentKey);
}
return this.parent.select(false)?.getOptionalConfig(this.parentKey);
}
return this.config;
}
has(key: string): boolean {
return this.select(false)?.has(key) ?? false;
}
keys(): string[] {
return this.select(false)?.keys() ?? [];
}
get<T = JsonValue>(key?: string): T {
return this.select(true).get(key);
}
getOptional<T = JsonValue>(key?: string): T | undefined {
return this.select(false)?.getOptional(key);
}
getConfig(key: string): ConfigService {
return new ObservableConfigProxy(this, key);
}
getOptionalConfig(key: string): ConfigService | undefined {
if (this.select(false)?.has(key)) {
return new ObservableConfigProxy(this, key);
}
return undefined;
}
getConfigArray(key: string): ConfigService[] {
return this.select(true).getConfigArray(key);
}
getOptionalConfigArray(key: string): ConfigService[] | undefined {
return this.select(false)?.getOptionalConfigArray(key);
}
getNumber(key: string): number {
return this.select(true).getNumber(key);
}
getOptionalNumber(key: string): number | undefined {
return this.select(false)?.getOptionalNumber(key);
}
getBoolean(key: string): boolean {
return this.select(true).getBoolean(key);
}
getOptionalBoolean(key: string): boolean | undefined {
return this.select(false)?.getOptionalBoolean(key);
}
getString(key: string): string {
return this.select(true).getString(key);
}
getOptionalString(key: string): string | undefined {
return this.select(false)?.getOptionalString(key);
}
getStringArray(key: string): string[] {
return this.select(true).getStringArray(key);
}
getOptionalStringArray(key: string): string[] | undefined {
return this.select(false)?.getOptionalStringArray(key);
}
}
@@ -0,0 +1,120 @@
/*
* 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 { resolve as resolvePath } from 'path';
import parseArgs from 'minimist';
import { LoggerService } from '@backstage/backend-plugin-api';
import { findPaths } from '@backstage/cli-common';
import {
loadConfigSchema,
loadConfig,
ConfigTarget,
LoadConfigOptionsRemote,
} from '@backstage/config-loader';
import { Config, ConfigReader } from '@backstage/config';
import { getPackages } from '@manypkg/get-packages';
import { ObservableConfigProxy } from './ObservableConfigProxy';
import { isValidUrl } from '../lib/urls';
/** @public */
export async function createConfigSecretEnumerator(options: {
logger: LoggerService;
dir?: string;
}): Promise<(config: Config) => Iterable<string>> {
const { logger, dir = process.cwd() } = options;
const { packages } = await getPackages(dir);
const schema = await loadConfigSchema({
dependencies: packages.map(p => p.packageJson.name),
});
return (config: Config) => {
const [secretsData] = schema.process(
[{ data: config.getOptional() ?? {}, context: 'schema-enumerator' }],
{
visibility: ['secret'],
ignoreSchemaErrors: true,
},
);
const secrets = new Set<string>();
JSON.parse(
JSON.stringify(secretsData),
(_, v) => typeof v === 'string' && secrets.add(v),
);
logger.info(
`Found ${secrets.size} new secrets in config that will be redacted`,
);
return secrets;
};
}
/**
* Load configuration for a Backend.
*
* This function should only be called once, during the initialization of the backend.
*
* @public
*/
export async function loadBackendConfig(options: {
remote?: LoadConfigOptionsRemote;
argv: string[];
}): Promise<{ config: Config }> {
const args = parseArgs(options.argv);
const configTargets: ConfigTarget[] = [args.config ?? []]
.flat()
.map(arg => (isValidUrl(arg) ? { url: arg } : { path: resolvePath(arg) }));
/* eslint-disable-next-line no-restricted-syntax */
const paths = findPaths(__dirname);
let currentCancelFunc: (() => void) | undefined = undefined;
const config = new ObservableConfigProxy();
const { appConfigs } = await loadConfig({
configRoot: paths.targetRoot,
configTargets: configTargets,
remote: options.remote,
watch: {
onChange(newConfigs) {
console.info(
`Reloaded config from ${newConfigs.map(c => c.context).join(', ')}`,
);
config.setConfig(ConfigReader.fromConfigs(newConfigs));
},
stopSignal: new Promise(resolve => {
if (currentCancelFunc) {
currentCancelFunc();
}
currentCancelFunc = resolve;
// TODO(Rugvip): We keep this here for now to avoid breaking the old system
// since this is re-used in backend-common
if (module.hot) {
module.hot.addDisposeHandler(resolve);
}
}),
},
});
console.info(
`Loaded config from ${appConfigs.map(c => c.context).join(', ')}`,
);
config.setConfig(ConfigReader.fromConfigs(appConfigs));
return { config };
}
@@ -0,0 +1,17 @@
/*
* Copyright 2023 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 { loadBackendConfig, createConfigSecretEnumerator } from './config';
+2
View File
@@ -20,6 +20,8 @@
* @packageDocumentation
*/
export * from './config';
export * from './http';
export * from './logging';
export * from './wiring';
export * from './services/implementations';
@@ -0,0 +1,84 @@
/*
* Copyright 2021 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 { escapeRegExp } from './escapeRegExp';
describe('escapeRegExp', () => {
test('does not escape non-regex characters', () => {
expect(escapeRegExp('Backstage Backstage')).toBe('Backstage Backstage');
});
test('all the characters', () => {
expect(escapeRegExp('^$\\.*+?()[]{}|')).toBe(
'\\^\\$\\\\\\.\\*\\+\\?\\(\\)\\[\\]\\{\\}\\|',
);
});
test('character: ^', () => {
expect(escapeRegExp('^')).toBe('\\^');
});
test('character: $', () => {
expect(escapeRegExp('$')).toBe('\\$');
});
test('character: \\', () => {
expect(escapeRegExp('\\')).toBe('\\\\');
});
test('character: .', () => {
expect(escapeRegExp('.')).toBe('\\.');
});
test('character: *', () => {
expect(escapeRegExp('*')).toBe('\\*');
});
test('character: +', () => {
expect(escapeRegExp('+')).toBe('\\+');
});
test('character: ?', () => {
expect(escapeRegExp('?')).toBe('\\?');
});
test('character: (', () => {
expect(escapeRegExp('(')).toBe('\\(');
});
test('character: )', () => {
expect(escapeRegExp(')')).toBe('\\)');
});
test('character: [', () => {
expect(escapeRegExp('[')).toBe('\\[');
});
test('character: ]', () => {
expect(escapeRegExp(']')).toBe('\\]');
});
test('character: {', () => {
expect(escapeRegExp('{')).toBe('\\{');
});
test('character: }', () => {
expect(escapeRegExp('}')).toBe('\\}');
});
test('character: |', () => {
expect(escapeRegExp('|')).toBe('\\|');
});
});
@@ -1,5 +1,5 @@
/*
* Copyright 2020 The Backstage Authors
* Copyright 2021 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.
@@ -14,15 +14,11 @@
* limitations under the License.
*/
import * as winston from 'winston';
/**
* A logger that just throws away all messages.
* Escapes a given string to be used inside a RegExp.
*
* @public
* Taken from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions
*/
export function getVoidLogger(): winston.Logger {
return winston.createLogger({
transports: [new winston.transports.Console({ silent: true })],
});
}
export const escapeRegExp = (text: string) => {
return text.replace(/[.*+?^${}(\)|[\]\\]/g, '\\$&');
};
@@ -0,0 +1,34 @@
/*
* Copyright 2021 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 { isValidUrl } from './urls';
describe('isValidUrl', () => {
it('should return true for url', () => {
const validUrl = isValidUrl('http://some.valid.url');
expect(validUrl).toBe(true);
});
it('should return false for absolute path', () => {
const validUrl = isValidUrl('/some/absolute/path');
expect(validUrl).toBe(false);
});
it('should return false for relative path', () => {
const validUrl = isValidUrl('../some/relative/path');
expect(validUrl).toBe(false);
});
});
+25
View File
@@ -0,0 +1,25 @@
/*
* Copyright 2021 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 function isValidUrl(url: string): boolean {
try {
// eslint-disable-next-line no-new
new URL(url);
return true;
} catch {
return false;
}
}
@@ -0,0 +1,175 @@
/*
* Copyright 2023 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,
LogMeta,
RootLoggerService,
} from '@backstage/backend-plugin-api';
import { Format, TransformableInfo } from 'logform';
import {
Logger,
format,
createLogger,
transports,
transport as Transport,
} from 'winston';
/**
* @public
*/
export interface WinstonLoggerOptions {
meta?: LogMeta;
level: string;
format: Format;
transports: Transport[];
}
/**
* A {@link @backstage/backend-plugin-api#LoggerService} implementation based on winston.
*
* @public
*/
export class WinstonLogger implements RootLoggerService {
#winston: Logger;
#addRedactions?: (redactions: Iterable<string>) => void;
/**
* Creates a {@link WinstonLogger} instance.
*/
static create(options: WinstonLoggerOptions): WinstonLogger {
const redacter = WinstonLogger.redacter();
let logger = createLogger({
level: options.level,
format: format.combine(redacter.format, options.format),
transports: options.transports ?? new transports.Console(),
});
if (options.meta) {
logger = logger.child(options.meta);
}
return new WinstonLogger(logger, redacter.add);
}
/**
* Creates a winston log formatter for redacting secrets.
*/
static redacter(): {
format: Format;
add: (redactions: Iterable<string>) => void;
} {
const redactionSet = new Set<string>();
let redactionPattern: RegExp | undefined = undefined;
return {
format: format(info => {
if (redactionPattern && typeof info.message === 'string') {
info.message = info.message.replace(redactionPattern, '[REDACTED]');
}
return info;
})(),
add(newRedactions) {
let added = 0;
for (const redaction of newRedactions) {
// 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'.
if (redaction.length <= 1) {
continue;
}
if (!redactionSet.has(redaction)) {
redactionSet.add(redaction);
added += 1;
}
}
if (added > 0) {
redactionPattern = new RegExp(
`(${Array.from(redactionSet).join('|')})`,
'g',
);
}
},
};
}
/**
* Creates a pretty printed winston log formatter.
*/
static colorFormat(): Format {
const colorizer = format.colorize();
return format.combine(
format.timestamp(),
format.colorize({
colors: {
timestamp: 'dim',
prefix: 'blue',
field: 'cyan',
debug: 'grey',
},
}),
format.printf((info: TransformableInfo) => {
const { timestamp, level, message, plugin, service, ...fields } = info;
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}`;
}),
);
}
private constructor(
winston: Logger,
addRedactions?: (redactions: Iterable<string>) => void,
) {
this.#winston = winston;
this.#addRedactions = addRedactions;
}
error(message: string, meta?: LogMeta): void {
this.#winston.error(message, meta);
}
warn(message: string, meta?: LogMeta): void {
this.#winston.warn(message, meta);
}
info(message: string, meta?: LogMeta): void {
this.#winston.info(message, meta);
}
debug(message: string, meta?: LogMeta): void {
this.#winston.debug(message, meta);
}
child(meta: LogMeta): LoggerService {
return new WinstonLogger(this.#winston.child(meta));
}
addRedactions(redactions: Iterable<string>) {
this.#addRedactions?.(redactions);
}
}
@@ -0,0 +1,18 @@
/*
* Copyright 2023 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 { WinstonLogger } from './WinstonLogger';
export type { WinstonLoggerOptions } from './WinstonLogger';
@@ -14,26 +14,34 @@
* limitations under the License.
*/
import {
loadBackendConfig,
loggerToWinstonLogger,
} from '@backstage/backend-common';
import {
coreServices,
createServiceFactory,
} from '@backstage/backend-plugin-api';
import { LoadConfigOptionsRemote } from '@backstage/config-loader';
import { loadBackendConfig } from '../../../config';
/** @public */
export interface ConfigFactoryOptions {
/**
* Process arguments to use instead of the default `process.argv()`.
*/
argv?: string[];
/**
* Enables and sets options for remote configuration loading.
*/
remote?: LoadConfigOptionsRemote;
}
/** @public */
export const configFactory = createServiceFactory({
service: coreServices.config,
deps: {
logger: coreServices.rootLogger,
},
async factory({ logger }) {
const config = await loadBackendConfig({
argv: process.argv,
logger: loggerToWinstonLogger(logger),
});
deps: {},
async factory({}, options?: ConfigFactoryOptions) {
const { argv = process.argv, remote } = options ?? {};
const { config } = await loadBackendConfig({ argv, remote });
return config;
},
});
@@ -15,3 +15,4 @@
*/
export { configFactory } from './configFactory';
export type { ConfigFactoryOptions } from './configFactory';
@@ -14,48 +14,37 @@
* limitations under the License.
*/
import { createRootLogger } from '@backstage/backend-common';
import {
createServiceFactory,
LoggerService,
coreServices,
} from '@backstage/backend-plugin-api';
import { LogMeta } from '@backstage/backend-plugin-api';
import { Logger as WinstonLogger } from 'winston';
class BackstageLogger implements LoggerService {
static fromWinston(logger: WinstonLogger): BackstageLogger {
return new BackstageLogger(logger);
}
private constructor(private readonly winston: WinstonLogger) {}
error(message: string, meta?: LogMeta): void {
this.winston.error(message, meta);
}
warn(message: string, meta?: LogMeta): void {
this.winston.warn(message, meta);
}
info(message: string, meta?: LogMeta): void {
this.winston.info(message, meta);
}
debug(message: string, meta?: LogMeta): void {
this.winston.debug(message, meta);
}
child(meta: LogMeta): LoggerService {
return new BackstageLogger(this.winston.child(meta));
}
}
import { WinstonLogger } from '../../../logging';
import { transports, format } from 'winston';
import { createConfigSecretEnumerator } from '../../../config';
/** @public */
export const rootLoggerFactory = createServiceFactory({
service: coreServices.rootLogger,
deps: {},
async factory() {
return BackstageLogger.fromWinston(createRootLogger());
deps: {
config: coreServices.config,
},
async factory({ config }) {
const logger = WinstonLogger.create({
meta: {
service: 'backstage',
},
level: process.env.LOG_LEVEL || 'info',
format:
process.env.NODE_ENV === 'production'
? format.json()
: WinstonLogger.colorFormat(),
transports: [new transports.Console()],
});
const secretEnumerator = await createConfigSecretEnumerator({ logger });
logger.addRedactions(secretEnumerator(config));
config.subscribe?.(() => logger.addRedactions(secretEnumerator(config)));
return logger;
},
});
+13 -211
View File
@@ -14,166 +14,14 @@
* limitations under the License.
*/
import { resolve as resolvePath } from 'path';
import parseArgs from 'minimist';
import { LoggerService } from '@backstage/backend-plugin-api';
import { findPaths } from '@backstage/cli-common';
import {
loadConfigSchema,
loadConfig,
ConfigSchema,
ConfigTarget,
LoadConfigOptionsRemote,
} from '@backstage/config-loader';
import { AppConfig, Config, ConfigReader } from '@backstage/config';
import { JsonValue } from '@backstage/types';
import { getPackages } from '@manypkg/get-packages';
import { isValidUrl } from './urls';
import { setRootLoggerRedactionList } from './logging/rootLogger';
// Fetch the schema and get all the secrets to pass to the rootLogger for redaction
const updateRedactionList = (
schema: ConfigSchema,
configs: AppConfig[],
logger: LoggerService,
) => {
const secretAppConfigs = schema.process(configs, {
visibility: ['secret'],
ignoreSchemaErrors: true,
});
const secretConfig = ConfigReader.fromConfigs(secretAppConfigs);
const values = new Set<string>();
const data = secretConfig.get();
JSON.parse(
JSON.stringify(data),
(_, v) => typeof v === 'string' && values.add(v),
);
logger.info(
`${values.size} secret${
values.size > 1 ? 's' : ''
} found in the config which will be redacted`,
);
setRootLoggerRedactionList(Array.from(values));
};
export class ObservableConfigProxy implements Config {
private config: Config = new ConfigReader({});
private readonly subscribers: (() => void)[] = [];
constructor(
private readonly logger: LoggerService,
private readonly parent?: ObservableConfigProxy,
private parentKey?: string,
) {
if (parent && !parentKey) {
throw new Error('parentKey is required if parent is set');
}
}
setConfig(config: Config) {
if (this.parent) {
throw new Error('immutable');
}
this.config = config;
for (const subscriber of this.subscribers) {
try {
subscriber();
} catch (error) {
this.logger.error(`Config subscriber threw error, ${error}`);
}
}
}
subscribe(onChange: () => void): { unsubscribe: () => void } {
if (this.parent) {
return this.parent.subscribe(onChange);
}
this.subscribers.push(onChange);
return {
unsubscribe: () => {
const index = this.subscribers.indexOf(onChange);
if (index >= 0) {
this.subscribers.splice(index, 1);
}
},
};
}
private select(required: true): Config;
private select(required: false): Config | undefined;
private select(required: boolean): Config | undefined {
if (this.parent && this.parentKey) {
if (required) {
return this.parent.select(true).getConfig(this.parentKey);
}
return this.parent.select(false)?.getOptionalConfig(this.parentKey);
}
return this.config;
}
has(key: string): boolean {
return this.select(false)?.has(key) ?? false;
}
keys(): string[] {
return this.select(false)?.keys() ?? [];
}
get<T = JsonValue>(key?: string): T {
return this.select(true).get(key);
}
getOptional<T = JsonValue>(key?: string): T | undefined {
return this.select(false)?.getOptional(key);
}
getConfig(key: string): Config {
return new ObservableConfigProxy(this.logger, this, key);
}
getOptionalConfig(key: string): Config | undefined {
if (this.select(false)?.has(key)) {
return new ObservableConfigProxy(this.logger, this, key);
}
return undefined;
}
getConfigArray(key: string): Config[] {
return this.select(true).getConfigArray(key);
}
getOptionalConfigArray(key: string): Config[] | undefined {
return this.select(false)?.getOptionalConfigArray(key);
}
getNumber(key: string): number {
return this.select(true).getNumber(key);
}
getOptionalNumber(key: string): number | undefined {
return this.select(false)?.getOptionalNumber(key);
}
getBoolean(key: string): boolean {
return this.select(true).getBoolean(key);
}
getOptionalBoolean(key: string): boolean | undefined {
return this.select(false)?.getOptionalBoolean(key);
}
getString(key: string): string {
return this.select(true).getString(key);
}
getOptionalString(key: string): string | undefined {
return this.select(false)?.getOptionalString(key);
}
getStringArray(key: string): string[] {
return this.select(true).getStringArray(key);
}
getOptionalStringArray(key: string): string[] | undefined {
return this.select(false)?.getOptionalStringArray(key);
}
}
// A global used to ensure that only a single file watcher is active at a time.
let currentCancelFunc: () => void;
createConfigSecretEnumerator,
loadBackendConfig as newLoadBackendConfig,
} from '@backstage/backend-app-api';
import { LoggerService } from '@backstage/backend-plugin-api';
import { Config } from '@backstage/config';
import { LoadConfigOptionsRemote } from '@backstage/config-loader';
import { setRootLoggerRedactionList } from './logging/createRootLogger';
/**
* Load configuration for a Backend.
@@ -188,60 +36,14 @@ export async function loadBackendConfig(options: {
remote?: LoadConfigOptionsRemote;
argv: string[];
}): Promise<Config> {
const args = parseArgs(options.argv);
const configTargets: ConfigTarget[] = [args.config ?? []]
.flat()
.map(arg => (isValidUrl(arg) ? { url: arg } : { path: resolvePath(arg) }));
/* eslint-disable-next-line no-restricted-syntax */
const paths = findPaths(__dirname);
// TODO(hhogg): This is fetching _all_ of the packages of the monorepo
// in order to find the secrets for redactions, however we only care about
// the backend ones, we need to find a way to exclude the frontend packages.
const { packages } = await getPackages(paths.targetDir);
const schema = await loadConfigSchema({
dependencies: packages.map(p => p.packageJson.name),
const secretEnumerator = await createConfigSecretEnumerator({
logger: options.logger,
});
const { config } = await newLoadBackendConfig(options);
const config = new ObservableConfigProxy(options.logger);
const { appConfigs } = await loadConfig({
configRoot: paths.targetRoot,
configTargets: configTargets,
remote: options.remote,
watch: {
onChange(newConfigs) {
options.logger.info(
`Reloaded config from ${newConfigs.map(c => c.context).join(', ')}`,
);
config.setConfig(ConfigReader.fromConfigs(newConfigs));
},
stopSignal: new Promise(resolve => {
if (currentCancelFunc) {
currentCancelFunc();
}
currentCancelFunc = resolve;
// For reloads of this module we need to use a dispose handler rather than the global.
if (module.hot) {
module.hot.addDisposeHandler(resolve);
}
}),
},
});
options.logger.info(
`Loaded config from ${appConfigs.map(c => c.context).join(', ')}`,
);
config.setConfig(ConfigReader.fromConfigs(appConfigs));
// Subscribe to config changes and update the redaction list for logging
updateRedactionList(schema, appConfigs, options.logger);
config.subscribe(() =>
updateRedactionList(schema, appConfigs, options.logger),
setRootLoggerRedactionList(secretEnumerator(config));
config.subscribe?.(() =>
setRootLoggerRedactionList(secretEnumerator(config)),
);
return config;
@@ -0,0 +1,88 @@
/*
* 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 { WinstonLogger } from '@backstage/backend-app-api';
import { merge } from 'lodash';
import * as winston from 'winston';
import { LoggerOptions } from 'winston';
import { setRootLogger } from './globalLoggers';
const redacter = WinstonLogger.redacter();
export const setRootLoggerRedactionList = redacter.add;
/**
* A winston formatting function that finds occurrences of filteredKeys
* and replaces them with the corresponding identifier.
*
* @public
*/
export function redactWinstonLogLine(
info: winston.Logform.TransformableInfo,
): winston.Logform.TransformableInfo {
return redacter.format.transform(info) as winston.Logform.TransformableInfo;
}
/**
* Creates a pretty printed winston log formatter.
*
* @public
*/
export const coloredFormat = WinstonLogger.colorFormat();
/**
* 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(
redacter.format,
env.NODE_ENV === 'production'
? winston.format.json()
: WinstonLogger.colorFormat(),
),
transports: [
new winston.transports.Console({
silent: env.JEST_WORKER_ID !== undefined && !env.LOG_LEVEL,
}),
],
},
options,
),
)
.child({ service: 'backstage' });
setRootLogger(logger);
return logger;
}
setRootLogger(createRootLogger());
@@ -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),
);
@@ -0,0 +1,57 @@
/*
* 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 })],
});
}
let rootLogger: winston.Logger;
/**
* 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;
}
+3 -5
View File
@@ -14,12 +14,10 @@
* limitations under the License.
*/
export * from './formats';
export { getRootLogger, getVoidLogger, setRootLogger } from './globalLoggers';
export {
createRootLogger,
getRootLogger,
setRootLogger,
redactWinstonLogLine,
} from './rootLogger';
export * from './voidLogger';
coloredFormat,
} from './createRootLogger';
export { loggerToWinstonLogger } from './loggerToWinstonLogger';
@@ -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();
@@ -16,7 +16,7 @@
import { ConfigReader } from '@backstage/config';
import * as jose from 'jose';
import { getVoidLogger } from '../logging/voidLogger';
import { getVoidLogger } from '../logging';
import { ServerTokenManager } from './ServerTokenManager';
import { TokenManager } from './types';
@@ -0,0 +1,90 @@
/*
* Copyright 2023 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 {
coreServices,
createServiceFactory,
LoggerService,
LogMeta,
RootLoggerService,
} from '@backstage/backend-plugin-api';
interface MockLoggerOptions {
levels:
| boolean
| { error: boolean; warn: boolean; info: boolean; debug: boolean };
}
class MockLogger implements RootLoggerService {
#levels: Exclude<MockLoggerOptions['levels'], boolean>;
#meta: LogMeta;
error(message: string, meta?: LogMeta | Error | undefined): void {
this.#log('error', message, meta);
}
warn(message: string, meta?: LogMeta | Error | undefined): void {
this.#log('warn', message, meta);
}
info(message: string, meta?: LogMeta | Error | undefined): void {
this.#log('info', message, meta);
}
debug(message: string, meta?: LogMeta | Error | undefined): void {
this.#log('debug', message, meta);
}
child(meta: LogMeta): LoggerService {
return new MockLogger(this.#levels, { ...this.#meta, ...meta });
}
constructor(levels: MockLoggerOptions['levels'], meta: LogMeta) {
if (typeof levels === 'boolean') {
this.#levels = {
error: levels,
debug: levels,
info: levels,
warn: levels,
};
} else {
this.#levels = levels;
}
this.#meta = meta;
}
#log(
level: 'error' | 'warn' | 'info' | 'debug',
message: string,
meta?: LogMeta | Error | undefined,
) {
if (this.#levels[level]) {
const labels = Object.entries(this.#meta)
.map(([key, value]) => `${key}=${value}`)
.join(',');
console[level](`${labels} ${message}`, meta);
}
}
}
/** @public */
export const mockRootLoggerService = createServiceFactory({
service: coreServices.rootLogger,
deps: {},
async factory(_deps) {
return new MockLogger(false, {});
},
});
@@ -20,7 +20,6 @@ import {
lifecycleFactory,
rootLifecycleFactory,
loggerFactory,
rootLoggerFactory,
cacheFactory,
permissionsFactory,
schedulerFactory,
@@ -43,6 +42,7 @@ import {
} from '@backstage/backend-plugin-api';
import { mockConfigFactory } from '../implementations/mockConfigService';
import { mockRootLoggerService } from '../implementations/mockRootLoggerService';
import { mockTokenManagerFactory } from '../implementations/mockTokenManagerService';
import { ConfigReader } from '@backstage/config';
import express from 'express';
@@ -89,10 +89,10 @@ const defaultServiceFactories = [
lifecycleFactory(),
loggerFactory(),
mockConfigFactory(),
mockRootLoggerService(),
mockTokenManagerFactory(),
permissionsFactory(),
rootLifecycleFactory(),
rootLoggerFactory(),
schedulerFactory(),
urlReaderFactory(),
];
@@ -19,12 +19,6 @@ import { resolve as resolvePath } from 'path';
import fetch from 'node-fetch';
import { startTestBackend } from '@backstage/backend-test-utils';
import { appPlugin } from './appPlugin';
import {
databaseFactory,
httpRouterFactory,
loggerFactory,
rootLoggerFactory,
} from '@backstage/backend-app-api';
describe('appPlugin', () => {
beforeEach(() => {
@@ -45,12 +39,6 @@ describe('appPlugin', () => {
it('boots', async () => {
const { server } = await startTestBackend({
services: [
loggerFactory(),
rootLoggerFactory(),
databaseFactory(),
httpRouterFactory(),
],
features: [
appPlugin({
appPackageName: 'app',
+8
View File
@@ -3381,15 +3381,20 @@ __metadata:
"@backstage/backend-plugin-api": "workspace:^"
"@backstage/backend-tasks": "workspace:^"
"@backstage/cli": "workspace:^"
"@backstage/cli-common": "workspace:^"
"@backstage/config": "workspace:^"
"@backstage/config-loader": "workspace:^"
"@backstage/errors": "workspace:^"
"@backstage/plugin-auth-node": "workspace:^"
"@backstage/plugin-permission-node": "workspace:^"
"@backstage/types": "workspace:^"
"@manypkg/get-packages": ^1.1.3
"@types/compression": ^1.7.0
"@types/cors": ^2.8.6
"@types/express": ^4.17.6
"@types/fs-extra": ^9.0.3
"@types/http-errors": ^2.0.0
"@types/minimist": ^1.2.0
"@types/morgan": ^1.9.0
"@types/node-forge": ^1.3.0
"@types/stoppable": ^1.1.0
@@ -3401,13 +3406,16 @@ __metadata:
helmet: ^6.0.0
http-errors: ^2.0.0
lodash: ^4.17.21
logform: ^2.3.2
minimatch: ^5.0.0
minimist: ^1.2.5
morgan: ^1.10.0
node-forge: ^1.3.1
selfsigned: ^2.0.0
stoppable: ^1.1.0
supertest: ^6.1.3
winston: ^3.2.1
winston-transport: ^4.5.0
languageName: unknown
linkType: soft