mirror of
https://github.com/n8n-io/n8n.git
synced 2026-09-01 15:47:41 +08:00
refactor(engine): Log through an injected logger instead of the console (#37111)
This commit is contained in:
@@ -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' });
|
||||
};
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user