From 344ef416bfbb65a039ae956d8743eac0efcb0c38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Fri, 28 Aug 2026 10:04:56 +0000 Subject: [PATCH] refactor(engine): Log through an injected logger instead of the console (#37111) --- packages/@n8n/engine/src/auth/authenticate.ts | 8 ++++-- packages/@n8n/engine/src/index.ts | 2 ++ .../@n8n/engine/src/logging/console-logger.ts | 26 +++++++++++++++++++ packages/@n8n/engine/src/logging/index.ts | 2 ++ .../@n8n/engine/src/logging/logger.types.ts | 13 ++++++++++ .../engine/src/queue/in-memory-work-queue.ts | 7 +++-- .../src/runtime/create-engine-runtime.ts | 9 +++++-- packages/@n8n/engine/src/serve.ts | 12 ++++++--- .../engine/src/server/create-engine-server.ts | 5 +++- .../modules/engine-v2/engine-v2.runtime.ts | 1 + 10 files changed, 74 insertions(+), 11 deletions(-) create mode 100644 packages/@n8n/engine/src/logging/console-logger.ts create mode 100644 packages/@n8n/engine/src/logging/index.ts create mode 100644 packages/@n8n/engine/src/logging/logger.types.ts diff --git a/packages/@n8n/engine/src/auth/authenticate.ts b/packages/@n8n/engine/src/auth/authenticate.ts index 858a86b7fcd..5372e0d8e4b 100644 --- a/packages/@n8n/engine/src/auth/authenticate.ts +++ b/packages/@n8n/engine/src/auth/authenticate.ts @@ -1,18 +1,22 @@ import type { RequestHandler } from 'express'; import type { IdentityVerifier } from './identity.types'; +import { createConsoleLogger, type EngineLogger } from '../logging'; import { fail } from '../server/error-response'; const BEARER_PREFIX = /^bearer /i; /** Verifies `Authorization: Bearer ` on every request it guards, or 401s with no reason. */ -export function createAuthenticationMiddleware(verifier: IdentityVerifier): RequestHandler { +export function createAuthenticationMiddleware( + verifier: IdentityVerifier, + logger: EngineLogger = createConsoleLogger(), +): RequestHandler { return (req, res, next) => { // The reason is logged, never returned: an operator needs it, a caller must not have it. const reject = (reason: string): void => { // Query string dropped: it can carry values that must not reach a log. const path = req.originalUrl.split('?')[0]; - console.warn(`engine: rejected ${req.method} ${path} - ${reason}`); + logger.warn(`rejected ${req.method} ${path} - ${reason}`); fail(res, 401, { error: 'unauthenticated' }); }; diff --git a/packages/@n8n/engine/src/index.ts b/packages/@n8n/engine/src/index.ts index b5ef120df25..f4c21dd22e9 100644 --- a/packages/@n8n/engine/src/index.ts +++ b/packages/@n8n/engine/src/index.ts @@ -1,6 +1,8 @@ export { createEngineRuntime } from './runtime'; export type { EngineRuntime, EngineRuntimeOptions } from './runtime'; +export type { EngineLogger } from './logging'; + export { ACTION_TOKEN, IDENTITY_TOKEN, diff --git a/packages/@n8n/engine/src/logging/console-logger.ts b/packages/@n8n/engine/src/logging/console-logger.ts new file mode 100644 index 00000000000..e3ae36056b5 --- /dev/null +++ b/packages/@n8n/engine/src/logging/console-logger.ts @@ -0,0 +1,26 @@ +import type { EngineLogger } from './logger.types'; + +/** + * An `EngineLogger` writing to the console, for standalone mode and as the + * fallback wherever a host supplied none. + * + * The scope is prefixed here because an integrated host gets it from its own + * logger instead, so the messages themselves carry none. + */ +export function createConsoleLogger(scope = 'engine'): EngineLogger { + const write = ( + to: (message: string, ...rest: unknown[]) => void, + message: string, + metadata?: Record, + ): void => { + if (metadata === undefined) to(`${scope}: ${message}`); + else to(`${scope}: ${message}`, metadata); + }; + + return { + error: (message, metadata) => write(console.error, message, metadata), + warn: (message, metadata) => write(console.warn, message, metadata), + info: (message, metadata) => write(console.info, message, metadata), + debug: (message, metadata) => write(console.debug, message, metadata), + }; +} diff --git a/packages/@n8n/engine/src/logging/index.ts b/packages/@n8n/engine/src/logging/index.ts new file mode 100644 index 00000000000..c6e4be1107e --- /dev/null +++ b/packages/@n8n/engine/src/logging/index.ts @@ -0,0 +1,2 @@ +export type { EngineLogger } from './logger.types'; +export { createConsoleLogger } from './console-logger'; diff --git a/packages/@n8n/engine/src/logging/logger.types.ts b/packages/@n8n/engine/src/logging/logger.types.ts new file mode 100644 index 00000000000..d5db69d0ee1 --- /dev/null +++ b/packages/@n8n/engine/src/logging/logger.types.ts @@ -0,0 +1,13 @@ +/** + * What the engine needs from a logger, and no more. + * + * Declared here rather than imported so the engine keeps no runtime dependency + * on the rest of n8n. `Logger` from `@n8n/backend-common` satisfies this shape + * already, so an integrated host passes its own scoped logger straight in. + */ +export interface EngineLogger { + error(message: string, metadata?: Record): void; + warn(message: string, metadata?: Record): void; + info(message: string, metadata?: Record): void; + debug(message: string, metadata?: Record): void; +} diff --git a/packages/@n8n/engine/src/queue/in-memory-work-queue.ts b/packages/@n8n/engine/src/queue/in-memory-work-queue.ts index 4d3d33291b2..0eb047c354e 100644 --- a/packages/@n8n/engine/src/queue/in-memory-work-queue.ts +++ b/packages/@n8n/engine/src/queue/in-memory-work-queue.ts @@ -1,3 +1,4 @@ +import { createConsoleLogger, type EngineLogger } from '../logging'; import type { WorkQueue } from './work-queue.types'; /** @@ -5,6 +6,8 @@ import type { WorkQueue } from './work-queue.types'; * sequentially on a microtask. */ export class InMemoryWorkQueue implements WorkQueue { + constructor(private readonly logger: EngineLogger = createConsoleLogger()) {} + private pending: TMessage[] = []; private handler: ((message: TMessage) => Promise) | undefined; @@ -40,7 +43,7 @@ export class InMemoryWorkQueue implements WorkQueue { if (this.dispatching || !this.handler || this.pending.length === 0) return; this.dispatching = true; this.dispatchPending().catch((error: unknown) => { - console.error('engine: work queue dispatch failed', error); + this.logger.error('work queue dispatch failed', { error }); }); } @@ -52,7 +55,7 @@ export class InMemoryWorkQueue implements WorkQueue { await this.handler(message); } catch (error) { // Contained per message: a failed one must not strand those behind it. - console.error('engine: work queue handler failed', error); + this.logger.error('work queue handler failed', { error }); } } } finally { diff --git a/packages/@n8n/engine/src/runtime/create-engine-runtime.ts b/packages/@n8n/engine/src/runtime/create-engine-runtime.ts index 90b1b2582b0..792b60bdc68 100644 --- a/packages/@n8n/engine/src/runtime/create-engine-runtime.ts +++ b/packages/@n8n/engine/src/runtime/create-engine-runtime.ts @@ -17,6 +17,7 @@ import { } from '../execution'; import { BatchingLifecycleEventPublisher, noopLifecycleEventPublisher } from '../lifecycle-events'; import type { LifecycleEventPublisher } from '../lifecycle-events'; +import { createConsoleLogger, type EngineLogger } from '../logging'; import { InMemoryWorkQueue } from '../queue'; import type { OrchestrationMessage, StepMessage } from '../queue'; import { createEngineServer } from '../server'; @@ -27,6 +28,8 @@ export interface EngineRuntimeOptions { admittance: AdmittanceService; /** Verifies the identity token on every `/api` request. No default: an unauthenticated engine must never boot by omission. */ identityVerifier: IdentityVerifier; + /** Where the engine writes its own messages. Defaults to the console. */ + logger?: EngineLogger; /** * Builds the capabilities the engine does not own. It receives the engine's * stores, because a `v1-node` executor reads step data through them and the @@ -61,10 +64,11 @@ export function createEngineRuntime({ dataSource, admittance, identityVerifier, + logger = createConsoleLogger(), externalDependencies, }: EngineRuntimeOptions): EngineRuntime { - const orchestrationQueue = new InMemoryWorkQueue(); - const stepQueue = new InMemoryWorkQueue(); + const orchestrationQueue = new InMemoryWorkQueue(logger); + const stepQueue = new InMemoryWorkQueue(logger); const { executionStore, stepStore, executionViewStore } = createStores(dataSource); // Built once, not per handler: the factory must not run twice. const dependencies = @@ -104,6 +108,7 @@ export function createEngineRuntime({ startExecution: new StartExecutionService(admittance, executionStore, orchestrationQueue), executionQuery: new ExecutionQueryService(executionViewStore), identityVerifier, + logger, }); return { diff --git a/packages/@n8n/engine/src/serve.ts b/packages/@n8n/engine/src/serve.ts index 4328b02ee62..f90dc623e66 100644 --- a/packages/@n8n/engine/src/serve.ts +++ b/packages/@n8n/engine/src/serve.ts @@ -4,8 +4,11 @@ import { Container } from '@n8n/di'; import { AllowAllAdmittance } from './admittance'; import { SharedSecretIdentityVerifier } from './auth'; import { createDataSource } from './database'; +import { createConsoleLogger } from './logging'; import { createEngineRuntime } from './runtime'; +const logger = createConsoleLogger(); + async function main(): Promise { const config = Container.get(EngineConfig); @@ -27,18 +30,19 @@ async function main(): Promise { dataSource, admittance: new AllowAllAdmittance(), identityVerifier: new SharedSecretIdentityVerifier(config.authSecret), + logger, }); runtime.start(); const server = runtime.app.listen(config.port, config.host, () => { - console.log(`engine: listening on http://${config.host}:${config.port}`); + logger.info(`listening on http://${config.host}:${config.port}`); }); let shuttingDown = false; const shutdown = async (signal: string): Promise => { if (shuttingDown) return; shuttingDown = true; - console.log(`engine: received ${signal}, shutting down`); + logger.info(`received ${signal}, shutting down`); await new Promise((resolve, reject) => { server.close((error) => (error ? reject(error) : resolve())); }); @@ -49,7 +53,7 @@ async function main(): Promise { const onSignal = (signal: string): void => { shutdown(signal).catch((error: unknown) => { - console.error('engine: error during shutdown', error); + logger.error('error during shutdown', { error }); process.exit(1); }); }; @@ -59,6 +63,6 @@ async function main(): Promise { } main().catch((error: unknown) => { - console.error('engine: failed to start', error); + logger.error('failed to start', { error }); process.exit(1); }); diff --git a/packages/@n8n/engine/src/server/create-engine-server.ts b/packages/@n8n/engine/src/server/create-engine-server.ts index 1677e41f171..9ac97cd9681 100644 --- a/packages/@n8n/engine/src/server/create-engine-server.ts +++ b/packages/@n8n/engine/src/server/create-engine-server.ts @@ -4,6 +4,7 @@ import { createAuthenticationMiddleware } from '../auth/authenticate'; import type { IdentityVerifier } from '../auth/identity.types'; import type { ExecutionQueryService } from '../execution'; import type { StartExecutionService } from '../execution/start-execution.service'; +import type { EngineLogger } from '../logging'; import { createWorkflowExecutionsRouter } from './routes/workflow-executions'; /** Services the engine API is built on, handed in at construction. */ @@ -11,6 +12,8 @@ export interface EngineServerDeps { startExecution: StartExecutionService; executionQuery: ExecutionQueryService; identityVerifier: IdentityVerifier; + /** Where the engine writes its own messages. Defaults to the console. */ + logger?: EngineLogger; } /** Builds the engine HTTP app: `/healthz` plus the authenticated execution API. */ @@ -23,7 +26,7 @@ export function createEngineServer(deps: EngineServerDeps): { app: Application } }); // Mounted on the prefix, not on each router, so a future router cannot forget it. - app.use('/api', createAuthenticationMiddleware(deps.identityVerifier)); + app.use('/api', createAuthenticationMiddleware(deps.identityVerifier, deps.logger)); app.use('/api', express.json()); app.use('/api/workflow-executions', createWorkflowExecutionsRouter(deps)); diff --git a/packages/cli/src/modules/engine-v2/engine-v2.runtime.ts b/packages/cli/src/modules/engine-v2/engine-v2.runtime.ts index fb0771eceaf..6d606df667c 100644 --- a/packages/cli/src/modules/engine-v2/engine-v2.runtime.ts +++ b/packages/cli/src/modules/engine-v2/engine-v2.runtime.ts @@ -84,6 +84,7 @@ export class EngineV2Runtime { // limits are applied. admittance: new AllowAllAdmittance(), identityVerifier: new SharedSecretIdentityVerifier(this.engineConfig.authSecret), + logger: this.logger, externalDependencies: ({ executionStore, stepStore }) => ({ lifecycleEventCallback: async (events, signal) => await this.controlPlaneClient.sendLifecycleEvents(events, signal),