refactor(engine): Log through an injected logger instead of the console (#37111)

This commit is contained in:
Iván Ovejero
2026-08-28 10:04:56 +00:00
committed by GitHub
parent ab13ace6ac
commit 344ef416bf
10 changed files with 74 additions and 11 deletions
@@ -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 <token>` 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' });
};
+2
View File
@@ -1,6 +1,8 @@
export { createEngineRuntime } from './runtime';
export type { EngineRuntime, EngineRuntimeOptions } from './runtime';
export type { EngineLogger } from './logging';
export {
ACTION_TOKEN,
IDENTITY_TOKEN,
@@ -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<string, unknown>,
): 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),
};
}
@@ -0,0 +1,2 @@
export type { EngineLogger } from './logger.types';
export { createConsoleLogger } from './console-logger';
@@ -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<string, unknown>): void;
warn(message: string, metadata?: Record<string, unknown>): void;
info(message: string, metadata?: Record<string, unknown>): void;
debug(message: string, metadata?: Record<string, unknown>): void;
}
@@ -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<TMessage> implements WorkQueue<TMessage> {
constructor(private readonly logger: EngineLogger = createConsoleLogger()) {}
private pending: TMessage[] = [];
private handler: ((message: TMessage) => Promise<void>) | undefined;
@@ -40,7 +43,7 @@ export class InMemoryWorkQueue<TMessage> implements WorkQueue<TMessage> {
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<TMessage> implements WorkQueue<TMessage> {
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 {
@@ -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<OrchestrationMessage>();
const stepQueue = new InMemoryWorkQueue<StepMessage>();
const orchestrationQueue = new InMemoryWorkQueue<OrchestrationMessage>(logger);
const stepQueue = new InMemoryWorkQueue<StepMessage>(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 {
+8 -4
View File
@@ -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<void> {
const config = Container.get(EngineConfig);
@@ -27,18 +30,19 @@ async function main(): Promise<void> {
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<void> => {
if (shuttingDown) return;
shuttingDown = true;
console.log(`engine: received ${signal}, shutting down`);
logger.info(`received ${signal}, shutting down`);
await new Promise<void>((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
});
@@ -49,7 +53,7 @@ async function main(): Promise<void> {
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<void> {
}
main().catch((error: unknown) => {
console.error('engine: failed to start', error);
logger.error('failed to start', { error });
process.exit(1);
});
@@ -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));
@@ -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),