refactor(core): Support multiple log scopes (#11318)

This commit is contained in:
Iván Ovejero 2024-10-22 17:20:14 +02:00 committed by GitHub
parent c863abd083
commit 43f31b86aa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 55 additions and 37 deletions

View file

@ -1,14 +1,16 @@
import { Config, Env, Nested } from '../decorators'; import { Config, Env, Nested } from '../decorators';
import { StringArray } from '../utils'; import { StringArray } from '../utils';
/** /** Scopes (areas of functionality) to filter logs by. */
* Scopes (areas of functionality) to filter logs by. export const LOG_SCOPES = [
* 'concurrency',
* `executions` -> execution lifecycle 'license',
* `license` -> license SDK 'multi-main-setup',
* `scaling` -> scaling mode 'pubsub',
*/ 'redis',
export const LOG_SCOPES = ['executions', 'license', 'scaling'] as const; 'scaling',
'waiting-executions',
] as const;
export type LogScope = (typeof LOG_SCOPES)[number]; export type LogScope = (typeof LOG_SCOPES)[number];
@ -59,14 +61,19 @@ export class LoggingConfig {
/** /**
* Scopes to filter logs by. Nothing is filtered by default. * Scopes to filter logs by. Nothing is filtered by default.
* *
* Currently supported log scopes: * Supported log scopes:
* - `executions` *
* - `concurrency`
* - `license` * - `license`
* - `multi-main-setup`
* - `pubsub`
* - `redis`
* - `scaling` * - `scaling`
* - `waiting-executions`
* *
* @example * @example
* `N8N_LOG_SCOPES=license` * `N8N_LOG_SCOPES=license`
* `N8N_LOG_SCOPES=license,executions` * `N8N_LOG_SCOPES=license,waiting-executions`
*/ */
@Env('N8N_LOG_SCOPES') @Env('N8N_LOG_SCOPES')
scopes: StringArray<LogScope> = []; scopes: StringArray<LogScope> = [];

View file

@ -170,7 +170,7 @@ export class Start extends BaseCommand {
this.logger.info('Initializing n8n process'); this.logger.info('Initializing n8n process');
if (config.getEnv('executions.mode') === 'queue') { if (config.getEnv('executions.mode') === 'queue') {
const scopedLogger = this.logger.withScope('scaling'); const scopedLogger = this.logger.scoped('scaling');
scopedLogger.debug('Starting main instance in scaling mode'); scopedLogger.debug('Starting main instance in scaling mode');
scopedLogger.debug(`Host ID: ${this.instanceSettings.hostId}`); scopedLogger.debug(`Host ID: ${this.instanceSettings.hostId}`);
} }
@ -263,7 +263,7 @@ export class Start extends BaseCommand {
await subscriber.subscribe('n8n.commands'); await subscriber.subscribe('n8n.commands');
await subscriber.subscribe('n8n.worker-response'); await subscriber.subscribe('n8n.worker-response');
this.logger.withScope('scaling').debug('Pubsub setup completed'); this.logger.scoped(['scaling', 'pubsub']).debug('Pubsub setup completed');
if (!orchestrationService.isMultiMainSetupEnabled) return; if (!orchestrationService.isMultiMainSetupEnabled) return;

View file

@ -69,7 +69,7 @@ export class Worker extends BaseCommand {
super(argv, cmdConfig); super(argv, cmdConfig);
this.logger = Container.get(Logger).withScope('scaling'); this.logger = Container.get(Logger).scoped('scaling');
} }
async init() { async init() {
@ -151,7 +151,7 @@ export class Worker extends BaseCommand {
Container.get(PubSubHandler).init(); Container.get(PubSubHandler).init();
await Container.get(Subscriber).subscribe('n8n.commands'); await Container.get(Subscriber).subscribe('n8n.commands');
this.logger.withScope('scaling').debug('Pubsub setup ready'); this.logger.scoped(['scaling', 'pubsub']).debug('Pubsub setup completed');
} }
async setConcurrency() { async setConcurrency() {

View file

@ -33,7 +33,7 @@ export class ConcurrencyControlService {
private readonly telemetry: Telemetry, private readonly telemetry: Telemetry,
private readonly eventService: EventService, private readonly eventService: EventService,
) { ) {
this.logger = this.logger.withScope('executions'); this.logger = this.logger.scoped('concurrency');
this.productionLimit = config.getEnv('executions.concurrency.productionLimit'); this.productionLimit = config.getEnv('executions.concurrency.productionLimit');

View file

@ -40,7 +40,7 @@ export class License {
private readonly licenseMetricsService: LicenseMetricsService, private readonly licenseMetricsService: LicenseMetricsService,
private readonly globalConfig: GlobalConfig, private readonly globalConfig: GlobalConfig,
) { ) {
this.logger = this.logger.withScope('license'); this.logger = this.logger.scoped('license');
} }
/** /**

View file

@ -58,9 +58,11 @@ export class Logger {
this.internalLogger = internalLogger; this.internalLogger = internalLogger;
} }
withScope(scope: LogScope) { /** Create a logger that injects the given scopes into its log metadata. */
scoped(scopes: LogScope | LogScope[]) {
scopes = Array.isArray(scopes) ? scopes : [scopes];
const scopedLogger = new Logger(this.globalConfig, this.instanceSettings); const scopedLogger = new Logger(this.globalConfig, this.instanceSettings);
const childLogger = this.internalLogger.child({ scope }); const childLogger = this.internalLogger.child({ scopes });
scopedLogger.setInternalLogger(childLogger); scopedLogger.setInternalLogger(childLogger);
@ -106,11 +108,14 @@ export class Logger {
private scopeFilter() { private scopeFilter() {
return winston.format((info: TransformableInfo & { metadata: LogMetadata }) => { return winston.format((info: TransformableInfo & { metadata: LogMetadata }) => {
const shouldIncludeScope = info.metadata.scope && this.scopes.has(info.metadata.scope); if (!this.isScopingEnabled) return info;
if (this.isScopingEnabled && !shouldIncludeScope) return false; const { scopes } = info.metadata;
return info; const shouldIncludeScope =
scopes && scopes?.length > 0 && scopes.some((s) => this.scopes.has(s));
return shouldIncludeScope ? info : false;
})(); })();
} }

View file

@ -6,7 +6,7 @@ export type LogLevel = (typeof LOG_LEVELS)[number];
export type LogMetadata = { export type LogMetadata = {
[key: string]: unknown; [key: string]: unknown;
scope?: LogScope; scopes?: LogScope[];
file?: string; file?: string;
function?: string; function?: string;
}; };

View file

@ -40,7 +40,7 @@ export class JobProcessor {
private readonly nodeTypes: NodeTypes, private readonly nodeTypes: NodeTypes,
private readonly instanceSettings: InstanceSettings, private readonly instanceSettings: InstanceSettings,
) { ) {
this.logger = this.logger.withScope('scaling'); this.logger = this.logger.scoped('scaling');
} }
async processJob(job: Job): Promise<JobResult> { async processJob(job: Job): Promise<JobResult> {

View file

@ -36,7 +36,7 @@ export class MultiMainSetup extends TypedEmitter<MultiMainEvents> {
private readonly globalConfig: GlobalConfig, private readonly globalConfig: GlobalConfig,
) { ) {
super(); super();
this.logger = this.logger.withScope('scaling'); this.logger = this.logger.scoped(['scaling', 'multi-main-setup']);
} }
private leaderKey: string; private leaderKey: string;

View file

@ -26,7 +26,7 @@ export class Publisher {
// @TODO: Once this class is only ever initialized in scaling mode, throw in the next line instead. // @TODO: Once this class is only ever initialized in scaling mode, throw in the next line instead.
if (config.getEnv('executions.mode') !== 'queue') return; if (config.getEnv('executions.mode') !== 'queue') return;
this.logger = this.logger.withScope('scaling'); this.logger = this.logger.scoped(['scaling', 'pubsub']);
this.client = this.redisClientService.createClient({ type: 'publisher(n8n)' }); this.client = this.redisClientService.createClient({ type: 'publisher(n8n)' });
} }

View file

@ -27,7 +27,7 @@ export class Subscriber {
// @TODO: Once this class is only ever initialized in scaling mode, throw in the next line instead. // @TODO: Once this class is only ever initialized in scaling mode, throw in the next line instead.
if (config.getEnv('executions.mode') !== 'queue') return; if (config.getEnv('executions.mode') !== 'queue') return;
this.logger = this.logger.withScope('scaling'); this.logger = this.logger.scoped(['scaling', 'pubsub']);
this.client = this.redisClientService.createClient({ type: 'subscriber(n8n)' }); this.client = this.redisClientService.createClient({ type: 'subscriber(n8n)' });

View file

@ -51,7 +51,7 @@ export class ScalingService {
private readonly orchestrationService: OrchestrationService, private readonly orchestrationService: OrchestrationService,
private readonly eventService: EventService, private readonly eventService: EventService,
) { ) {
this.logger = this.logger.withScope('scaling'); this.logger = this.logger.scoped('scaling');
} }
// #region Lifecycle // #region Lifecycle

View file

@ -58,7 +58,7 @@ export class WorkerServer {
) { ) {
assert(this.instanceSettings.instanceType === 'worker'); assert(this.instanceSettings.instanceType === 'worker');
this.logger = this.logger.withScope('scaling'); this.logger = this.logger.scoped('scaling');
this.app = express(); this.app = express();

View file

@ -37,6 +37,9 @@ export class RedisClientService extends TypedEmitter<RedisEventMap> {
private readonly globalConfig: GlobalConfig, private readonly globalConfig: GlobalConfig,
) { ) {
super(); super();
this.logger = this.logger.scoped(['redis', 'scaling']);
this.registerListeners(); this.registerListeners();
} }
@ -99,9 +102,11 @@ export class RedisClientService extends TypedEmitter<RedisEventMap> {
options.host = host; options.host = host;
options.port = port; options.port = port;
this.logger.debug('[Redis] Initializing regular client', { type, host, port }); const client = new ioRedis(options);
return new ioRedis(options); this.logger.debug(`Started Redis client ${type}`, { type, host, port });
return client;
} }
private createClusterClient({ private createClusterClient({
@ -115,12 +120,14 @@ export class RedisClientService extends TypedEmitter<RedisEventMap> {
const clusterNodes = this.clusterNodes(); const clusterNodes = this.clusterNodes();
this.logger.debug('[Redis] Initializing cluster client', { type, clusterNodes }); const clusterClient = new ioRedis.Cluster(clusterNodes, {
return new ioRedis.Cluster(clusterNodes, {
redisOptions: options, redisOptions: options,
clusterRetryStrategy: this.retryStrategy(), clusterRetryStrategy: this.retryStrategy(),
}); });
this.logger.debug(`Started Redis cluster client ${type}`, { type, clusterNodes });
return clusterClient;
} }
private getOptions({ extraOptions }: { extraOptions?: RedisOptions }) { private getOptions({ extraOptions }: { extraOptions?: RedisOptions }) {

View file

@ -31,7 +31,7 @@ export class WaitTracker {
private readonly orchestrationService: OrchestrationService, private readonly orchestrationService: OrchestrationService,
private readonly instanceSettings: InstanceSettings, private readonly instanceSettings: InstanceSettings,
) { ) {
this.logger = this.logger.withScope('executions'); this.logger = this.logger.scoped('waiting-executions');
} }
has(executionId: string) { has(executionId: string) {

View file

@ -25,5 +25,4 @@ export const mockEntityManager = (entityClass: Class) => {
return entityManager; return entityManager;
}; };
export const mockLogger = () => export const mockLogger = () => mock<Logger>({ scoped: jest.fn().mockReturnValue(mock<Logger>()) });
mock<Logger>({ withScope: jest.fn().mockReturnValue(mock<Logger>()) });