From e31923e62a9521a3b13e1091fb61caee42179513 Mon Sep 17 00:00:00 2001 From: Christian Dinse Date: Thu, 30 Apr 2026 17:10:10 +0200 Subject: [PATCH 1/7] Add OpenTelemetry output plugin --- .gitignore | 2 +- package-lock.json | 33 ++++++++ package.json | 1 + src/index.ts | 2 + src/lib/config/default/config-schema.json | 3 + src/lib/config/interfaces.ts | 3 + src/lib/helper/levelUtils.ts | 24 ------ src/lib/helper/pluginProvider.ts | 31 ++++++++ src/lib/logger/level.ts | 26 ++++++ src/lib/logger/logger.ts | 32 ++------ src/lib/logger/record.ts | 39 +++++++-- src/lib/logger/recordFactory.ts | 27 ++++--- src/lib/logger/recordWriter.ts | 33 -------- src/lib/logger/rootLogger.ts | 23 +++++- src/lib/logger/sourceUtils.ts | 34 +++++--- src/lib/middleware/middleware.ts | 10 +-- src/lib/plugins/interfaces.ts | 5 ++ src/lib/plugins/otelOutput.ts | 97 +++++++++++++++++++++++ src/lib/plugins/stdoutOutput.ts | 23 ++++++ src/test/unit-test/helper.test.js | 3 +- 20 files changed, 330 insertions(+), 121 deletions(-) delete mode 100644 src/lib/helper/levelUtils.ts create mode 100644 src/lib/helper/pluginProvider.ts delete mode 100644 src/lib/logger/recordWriter.ts create mode 100644 src/lib/plugins/interfaces.ts create mode 100644 src/lib/plugins/otelOutput.ts create mode 100644 src/lib/plugins/stdoutOutput.ts diff --git a/.gitignore b/.gitignore index 7c9c9604..db186aa1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ -build/ node_modules/ coverage/ +build/ .vscode/ .nyc_output/ diff --git a/package-lock.json b/package-lock.json index de87e5fe..12ca191b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "8.0.0", "license": "Apache-2.0", "dependencies": { + "@opentelemetry/api-logs": "^0.218.0", "ajv": "^8.18.0", "json-stringify-safe": "^5.0.1", "jsonwebtoken": "^9.0.3", @@ -975,6 +976,27 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@opentelemetry/api": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/api-logs": { + "version": "0.218.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.218.0.tgz", + "integrity": "sha512-fmEWp5kXlGEc3i/lR698Hz41DfGyN4Tbe4g7L1AxSc7fF8Xeh/FQ9Quqpa9dVA413Q1Ad43QOLzU4JoXgbFPWw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/@paralleldrive/cuid2": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz", @@ -3785,6 +3807,17 @@ "node": "20 || >=22" } }, + "node_modules/istanbul-lib-processinfo/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", + "dev": true, + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/istanbul-lib-report": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", diff --git a/package.json b/package.json index 468deecf..d21b2c52 100644 --- a/package.json +++ b/package.json @@ -75,6 +75,7 @@ "README.md" ], "dependencies": { + "@opentelemetry/api-logs": "^0.218.0", "ajv": "^8.18.0", "json-stringify-safe": "^5.0.1", "jsonwebtoken": "^9.0.3", diff --git a/src/index.ts b/src/index.ts index f2edbb2e..b5ccaebc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,3 +5,5 @@ export default rootLogger; export * from "./lib/config/interfaces.js"; export * from "./lib/logger/level.js"; export * from "./lib/logger/logger.js"; +export * from "./lib/plugins/stdoutOutput.js"; +export * from "./lib/plugins/otelOutput.js"; diff --git a/src/lib/config/default/config-schema.json b/src/lib/config/default/config-schema.json index a2d65f01..aa002bdd 100644 --- a/src/lib/config/default/config-schema.json +++ b/src/lib/config/default/config-schema.json @@ -121,6 +121,9 @@ "writtenTs", "message", "stacktrace", + "rawStacktrace", + "errorName", + "errorMessage", "level" ], "type": "string" diff --git a/src/lib/config/interfaces.ts b/src/lib/config/interfaces.ts index 39e9c6b6..6db5ede6 100644 --- a/src/lib/config/interfaces.ts +++ b/src/lib/config/interfaces.ts @@ -85,6 +85,9 @@ export enum DetailName { WrittenTs = "writtenTs", Message = "message", Stacktrace = "stacktrace", + RawStacktrace = "rawStacktrace", + ErrorName = "errorName", + ErrorMessage = "errorMessage", Level = "level" } diff --git a/src/lib/helper/levelUtils.ts b/src/lib/helper/levelUtils.ts deleted file mode 100644 index 75d6c72c..00000000 --- a/src/lib/helper/levelUtils.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Level } from '../logger/level.js'; - -export default class LevelUtils { - - private static readonly defaultLevel: Level = Level.Info - - static getLevel(name: string): Level { - const key = name.charAt(0).toUpperCase() + name.slice(1).toLowerCase(); - const level: Level = Level[key as keyof typeof Level] - if (level === undefined) { - return LevelUtils.defaultLevel; - } - return level - } - - static getName(level: Level): string { - return Level[level].toLowerCase() - } - - static isLevelEnabled(threshold: Level, level: Level) { - if (level <= Level.Off) return false; - return level <= threshold - } -} diff --git a/src/lib/helper/pluginProvider.ts b/src/lib/helper/pluginProvider.ts new file mode 100644 index 00000000..8535ffd2 --- /dev/null +++ b/src/lib/helper/pluginProvider.ts @@ -0,0 +1,31 @@ +import { OutputPlugin } from "../plugins/interfaces"; + +export default class PluginProvider { + private static instance: PluginProvider; + private outputPlugins: OutputPlugin[]; + + private constructor() { + this.outputPlugins = []; + } + + static getInstance(): PluginProvider { + if (!PluginProvider.instance) { + PluginProvider.instance = new PluginProvider(); + } + return PluginProvider.instance; + } + + addOutputPlugin(outputPlugin: OutputPlugin) { + this.outputPlugins.push(outputPlugin); + } + + setOutputPlugins(outputPlugins: OutputPlugin[]) { + this.outputPlugins = outputPlugins; + } + + getOutputPlugins(): OutputPlugin[] { + return this.outputPlugins; + } +} + + diff --git a/src/lib/logger/level.ts b/src/lib/logger/level.ts index d417c874..4bdac7fe 100644 --- a/src/lib/logger/level.ts +++ b/src/lib/logger/level.ts @@ -8,3 +8,29 @@ export enum Level { Debug = 4, Silly = 5 } + +export class LevelUtils { + private static readonly defaultLevel: Level = Level.Info + + static getLevel(level: string | Level): Level { + if (typeof level === 'string') { + const key = level.charAt(0).toUpperCase() + level.slice(1).toLowerCase(); + const lvl: Level = Level[key as keyof typeof Level] + if (lvl !== undefined) { + return lvl; + } + } else { + return level as Level + } + return LevelUtils.defaultLevel; + } + + static getName(level: Level): string { + return Level[level].toLowerCase() + } + + static isLevelEnabled(threshold: Level, level: Level) { + if (level <= Level.Off) return false; + return level <= threshold + } +} diff --git a/src/lib/logger/logger.ts b/src/lib/logger/logger.ts index c39dfa61..e3acc00e 100644 --- a/src/lib/logger/logger.ts +++ b/src/lib/logger/logger.ts @@ -1,9 +1,8 @@ -import LevelUtils from '../helper/levelUtils.js'; import { isValidObject } from '../middleware/utils.js'; -import { Level } from './level.js'; +import { Level, LevelUtils } from './level.js'; import RecordFactory from './recordFactory.js'; -import RecordWriter from './recordWriter.js'; import Context from './context.js'; +import PluginProvider from '../helper/pluginProvider.js'; export class Logger { private parent?: Logger = undefined @@ -11,7 +10,7 @@ export class Logger { private registeredCustomFields: Array = []; private customFields: Map = new Map() private recordFactory: RecordFactory; - private recordWriter: RecordWriter; + protected loggingLevelThreshold: Level = Level.Inherit constructor(parent?: Logger, context?: Context) { @@ -23,7 +22,6 @@ export class Logger { this.context = context; } this.recordFactory = RecordFactory.getInstance(); - this.recordWriter = RecordWriter.getInstance(); } createLogger(customFields?: Map | object, createNewContext?: boolean): Logger { @@ -37,11 +35,7 @@ export class Logger { } setLoggingLevel(level: string | Level) { - if (typeof level === 'string') { - this.loggingLevelThreshold = LevelUtils.getLevel(level) - } else { - this.loggingLevelThreshold = level - } + this.loggingLevelThreshold = LevelUtils.getLevel(level) } getLoggingLevel(): string { @@ -55,26 +49,14 @@ export class Logger { if (this.loggingLevelThreshold == Level.Inherit) { return this.parent!.isLoggingLevel(level) } - if (typeof level === 'string') { - return LevelUtils.isLevelEnabled(this.loggingLevelThreshold, LevelUtils.getLevel(level)) - } else { - return LevelUtils.isLevelEnabled(this.loggingLevelThreshold, level) - } + return LevelUtils.isLevelEnabled(this.loggingLevelThreshold, LevelUtils.getLevel(level)) } logMessage(level: string | Level, ...args: any) { if (!this.isLoggingLevel(level)) return; const loggerCustomFields = this.getCustomFieldsFromLogger(this); - - let levelName: string; - if (typeof level === 'string') { - levelName = level; - } else { - levelName = LevelUtils.getName(level); - } - - const record = this.recordFactory.buildMsgRecord(this.registeredCustomFields, loggerCustomFields, levelName, args, this.context); - this.recordWriter.writeLog(record); + const record = this.recordFactory.buildMsgRecord(this.registeredCustomFields, loggerCustomFields, LevelUtils.getLevel(level), args, this.context); + PluginProvider.getInstance().getOutputPlugins().forEach(output => { output.writeRecord(record) }) } error(...args: any) { diff --git a/src/lib/logger/record.ts b/src/lib/logger/record.ts index 1d9fd579..a4d0a75e 100644 --- a/src/lib/logger/record.ts +++ b/src/lib/logger/record.ts @@ -1,11 +1,34 @@ -export default class Record { - payload: any - metadata: any +import { Level } from "./level" - constructor(level: string) { +export class Record { + payload: { [key: string]: any } + metadata: RecordMetadata + + constructor(type: RecordType, level: Level) { this.payload = {} - this.metadata = { - level: level - } + this.metadata = new RecordMetadata(type, level) + } +} + +export class RecordMetadata { + type: RecordType + level: Level + message?: string + rawStacktrace?: string + stacktrace?: string[] + errorName?: string + errorMessage?: string + customFieldNames: string[] + + constructor(type: RecordType, level: Level) { + this.type = type + this.level = level + this.customFieldNames = new Array() } -} \ No newline at end of file +} + +export type RecordFieldValue = string | string[] | number | boolean + +export enum RecordType { + Request, Message +} diff --git a/src/lib/logger/recordFactory.ts b/src/lib/logger/recordFactory.ts index e1529ddd..b00be29d 100644 --- a/src/lib/logger/recordFactory.ts +++ b/src/lib/logger/recordFactory.ts @@ -6,9 +6,10 @@ import { CustomFieldsFormat, CustomFieldsTypeConversion, Output } from '../confi import StacktraceUtils from '../helper/stacktraceUtils.js'; import { isValidObject } from '../middleware/utils.js'; import Cache from './cache.js'; -import Record from './record.js'; +import { Record, RecordType } from './record.js'; import Context from './context.js'; import SourceUtils from './sourceUtils.js'; +import { Level } from './level.js'; export default class RecordFactory { @@ -34,19 +35,25 @@ export default class RecordFactory { } // init a new record and assign fields with output "msg-log" - buildMsgRecord(registeredCustomFields: Array, loggerCustomFields: Map, levelName: string, args: Array, context?: Context): Record { + buildMsgRecord(registeredCustomFields: Array, loggerCustomFields: Map, level: Level, args: Array, context?: Context): Record { const lastArg = args[args.length - 1]; let customFieldsFromArgs = new Map(); - const record = new Record(levelName) - - + const record = new Record(RecordType.Message, level) + + if (typeof lastArg === "object") { if (this.stacktraceUtils.isErrorWithStacktrace(lastArg)) { record.metadata.stacktrace = this.stacktraceUtils.prepareStacktrace(lastArg.stack); + record.metadata.rawStacktrace = lastArg.stack + record.metadata.errorMessage = lastArg.message + record.metadata.errorName = lastArg.name } else if (isValidObject(lastArg)) { customFieldsFromArgs = new Map(Object.entries(lastArg)); if (this.stacktraceUtils.isErrorWithStacktrace(lastArg._error)) { record.metadata.stacktrace = this.stacktraceUtils.prepareStacktrace(lastArg._error.stack); + record.metadata.rawStacktrace = lastArg._error.stack + record.metadata.errorMessage = lastArg._error.message + record.metadata.errorName = lastArg._error.name customFieldsFromArgs.delete("_error"); } } else if (lastArg instanceof Map) { @@ -77,8 +84,8 @@ export default class RecordFactory { } // init a new record and assign fields with output "req-log" - buildReqRecord(levelName: string, req: any, res: any, context: Context): Record { - const record = new Record(levelName) + buildReqRecord(level: Level, req: any, res: any, context: Context): Record { + const record = new Record(RecordType.Request, level) // assign static fields from cache const cacheFields = this.config.getCacheReqFields(); @@ -88,7 +95,7 @@ export default class RecordFactory { // assign dynamic fields this.addDynamicFields(record, Output.ReqLog, req, res); - // assign values request context + // assign values request context this.addContext(record, context); // assign custom fields @@ -98,7 +105,7 @@ export default class RecordFactory { return record; } - private addCustomFields(record: Record, registeredCustomFields: Array, loggerCustomFields: Map, + private addCustomFields(record: Record, registeredCustomFields: Array, loggerCustomFields: Map, customFieldsFromArgs: Map = new Map()) { const providedFields = new Map([...loggerCustomFields, ...customFieldsFromArgs]); const customFieldsFormat = this.config.getConfig().customFieldsFormat!; @@ -129,6 +136,8 @@ export default class RecordFactory { indexedCustomFields[key] = value; } } + + record.metadata.customFieldNames.push(key) }); // Write custom fields in the correct order and correlates i to the place in registeredCustomFields diff --git a/src/lib/logger/recordWriter.ts b/src/lib/logger/recordWriter.ts deleted file mode 100644 index a7746ad7..00000000 --- a/src/lib/logger/recordWriter.ts +++ /dev/null @@ -1,33 +0,0 @@ -import os from 'os'; - -import Record from './record.js'; - -export default class RecordWriter { - - private static instance: RecordWriter; - private customSinkFunction: ((level: string, payload: string) => any) | undefined; - - private constructor() {} - - static getInstance(): RecordWriter { - if (!RecordWriter.instance) { - RecordWriter.instance = new RecordWriter(); - } - - return RecordWriter.instance; - } - - writeLog(record: Record): void { - const level = record.metadata.level; - if (this.customSinkFunction) { - this.customSinkFunction(level, JSON.stringify(record.payload)); - } else { - // default to stdout - process.stdout.write(JSON.stringify(record.payload) + os.EOL); - } - } - - setSinkFunction(f: (level: string, payload: string) => any) { - this.customSinkFunction = f; - } -} diff --git a/src/lib/logger/rootLogger.ts b/src/lib/logger/rootLogger.ts index cc21e170..3185e4d7 100644 --- a/src/lib/logger/rootLogger.ts +++ b/src/lib/logger/rootLogger.ts @@ -6,18 +6,23 @@ import EnvService from '../helper/envService.js'; import Middleware from '../middleware/middleware.js'; import RequestAccessor from '../middleware/requestAccessor.js'; import ResponseAccessor from '../middleware/responseAccessor.js'; +import { StdoutOutputPlugin } from '../plugins/stdoutOutput.js'; +import { OutputPlugin } from '../plugins/interfaces.js'; +import PluginProvider from '../helper/pluginProvider.js'; import createTransport from '../winston/winstonTransport.js'; import { Level } from './level.js'; import { Logger } from './logger.js'; -import RecordWriter from './recordWriter.js'; export default class RootLogger extends Logger { private static instance: RootLogger; + private stdoutOutput: StdoutOutputPlugin; private config = Config.getInstance(); private constructor() { super() this.loggingLevelThreshold = Level.Info + this.stdoutOutput = new StdoutOutputPlugin(); + PluginProvider.getInstance().setOutputPlugins([this.stdoutOutput]); } static getInstance(): RootLogger { @@ -56,8 +61,20 @@ export default class RootLogger extends Logger { return this.config.setStartupMessageEnabled(enabled); } - setSinkFunction(func: (level: string, payload: string) => void) { - RecordWriter.getInstance().setSinkFunction(func); + setSinkFunction(func: (level: string, payload: string) => any) { + this.stdoutOutput.setSinkFunction(func); + } + + addOutputPlugin(outputPlugin: OutputPlugin) { + PluginProvider.getInstance().addOutputPlugin(outputPlugin); + } + + setOutputPlugins(...outputPlugin: OutputPlugin[]) { + PluginProvider.getInstance().setOutputPlugins(outputPlugin); + } + + getOutputPlugins(): OutputPlugin[] { + return PluginProvider.getInstance().getOutputPlugins(); } enableTracing(input: string | string[]) { diff --git a/src/lib/logger/sourceUtils.ts b/src/lib/logger/sourceUtils.ts index 034e4293..9486bc90 100644 --- a/src/lib/logger/sourceUtils.ts +++ b/src/lib/logger/sourceUtils.ts @@ -5,7 +5,8 @@ import { ConfigField, Conversion, DetailName, Output, Source, SourceType } from import EnvVarHelper from '../helper/envVarHelper.js'; import RequestAccessor from '../middleware/requestAccessor.js'; import ResponseAccessor from '../middleware/responseAccessor.js'; -import Record from './record.js'; +import { Record, RecordFieldValue } from './record.js'; +import { LevelUtils } from '../logger/level.js'; const NS_PER_MS = 1e6; const REDACTED_PLACEHOLDER = "redacted"; @@ -31,11 +32,11 @@ export default class SourceUtils { return SourceUtils.instance; } - getValue(field: ConfigField, record: Record, output: Output, req?: any, res?: any): string | number | boolean | undefined { + getValue(field: ConfigField, record: Record, output: Output, req?: any, res?: any): RecordFieldValue | undefined { if (!field.source) return undefined const sources = Array.isArray(field.source) ? field.source : [field.source] - let value: string | number | boolean | undefined; + let value: RecordFieldValue | undefined; let sourceIndex = 0; @@ -81,8 +82,8 @@ export default class SourceUtils { return value } - private getValueFromSource(source: Source, record: Record, output: Output, req?: any, res?: any): string | number | boolean | undefined { - let value: string | number | boolean | undefined; + private getValueFromSource(source: Source, record: Record, output: Output, req?: any, res?: any): RecordFieldValue | undefined { + let value: RecordFieldValue | undefined; switch (source.type) { case SourceType.ReqHeader: value = req ? this.requestAccessor.getHeaderField(req, source.fieldName!) : undefined; @@ -122,8 +123,8 @@ export default class SourceUtils { return value } - private getDetail(detailName: DetailName, record: Record, req?: any, res?: any): string | number | undefined { - let value: string | number | undefined; + private getDetail(detailName: DetailName, record: Record, req?: any, res?: any): RecordFieldValue | undefined { + let value: RecordFieldValue | undefined; switch (detailName as DetailName) { case DetailName.RequestReceivedAt: value = req ? new Date(req._receivedAt).toJSON() : undefined; @@ -157,8 +158,17 @@ export default class SourceUtils { case DetailName.Stacktrace: value = record.metadata.stacktrace break; + case DetailName.RawStacktrace: + value = record.metadata.rawStacktrace + break + case DetailName.ErrorMessage: + value = record.metadata.errorMessage + break; + case DetailName.ErrorName: + value = record.metadata.errorName + break; case DetailName.Level: - value = record.metadata.level + value = LevelUtils.getName(record.metadata.level) break; } return value; @@ -196,7 +206,7 @@ export default class SourceUtils { return undefined; } - private parseIntValue(value: string | number | boolean): number { + private parseIntValue(value: RecordFieldValue): number { switch (typeof value) { case 'string': return parseInt(value, 0) @@ -205,9 +215,10 @@ export default class SourceUtils { case 'boolean': return value ? 1 : 0 } + return 0 } - private parseFloatValue(value: string | number | boolean): number { + private parseFloatValue(value: RecordFieldValue): number { switch (typeof value) { case 'string': return parseFloat(value) @@ -216,9 +227,10 @@ export default class SourceUtils { case 'boolean': return value ? 1 : 0 } + return 0 } - private parseBooleanValue(value: string | number | boolean): boolean { + private parseBooleanValue(value: RecordFieldValue): boolean { return value === 'true' || value === 'TRUE' || value === 'True' || value === 1 || value === true } } diff --git a/src/lib/middleware/middleware.ts b/src/lib/middleware/middleware.ts index 6b84a54f..aa0e941b 100644 --- a/src/lib/middleware/middleware.ts +++ b/src/lib/middleware/middleware.ts @@ -1,13 +1,13 @@ import JWTService from '../helper/jwtService.js'; -import LevelUtils from '../helper/levelUtils.js'; +import { LevelUtils } from '../logger/level.js'; import { Logger } from '../logger/logger.js'; import RecordFactory from '../logger/recordFactory.js'; -import RecordWriter from '../logger/recordWriter.js'; import Context from '../logger/context.js'; import RootLogger from '../logger/rootLogger.js'; import RequestAccessor from './requestAccessor.js'; import Config from '../config/config.js'; import ResponseAccessor from './responseAccessor.js'; +import PluginRegistry from '../helper/pluginProvider.js'; export default class Middleware { static logNetwork(req: any, res: any, next?: any) { @@ -20,7 +20,7 @@ export default class Middleware { // initialize Logger with parent to set registered fields const networkLogger = new Logger(parentLogger, context); const jwtService = JWTService.getInstance(); - + const dynLogLevelHeader = jwtService.getDynLogLevelHeaderName(); const token = RequestAccessor.getInstance().getHeaderField(req, dynLogLevelHeader); if (token) { @@ -39,8 +39,8 @@ export default class Middleware { const level = LevelUtils.getLevel(levelName); const threshold = LevelUtils.getLevel(req.logger.getLoggingLevel()); if (LevelUtils.isLevelEnabled(threshold, level)) { - const record = RecordFactory.getInstance().buildReqRecord(levelName, req, res, context); - RecordWriter.getInstance().writeLog(record); + const record = RecordFactory.getInstance().buildReqRecord(level, req, res, context); + PluginRegistry.getInstance().getOutputPlugins().forEach(output => { output.writeRecord(record) }) } logSent = true; } diff --git a/src/lib/plugins/interfaces.ts b/src/lib/plugins/interfaces.ts new file mode 100644 index 00000000..ac098a3b --- /dev/null +++ b/src/lib/plugins/interfaces.ts @@ -0,0 +1,5 @@ +import { Record } from "../logger/record" + +export interface OutputPlugin { + writeRecord(record: Record): void +} diff --git a/src/lib/plugins/otelOutput.ts b/src/lib/plugins/otelOutput.ts new file mode 100644 index 00000000..fc1d1f43 --- /dev/null +++ b/src/lib/plugins/otelOutput.ts @@ -0,0 +1,97 @@ + +import { logs as logsAPI, Logger, LoggerProvider, SeverityNumber, LogAttributes} from '@opentelemetry/api-logs' +import { OutputPlugin } from './interfaces.js' +import { Record, RecordType } from '../logger/record.js' +import { Level } from '../logger/level.js' + +export class OpenTelemetryLogsOutputPlugin implements OutputPlugin { + private logger: Logger + private includeFieldsAsAttributes: FieldInclusionMode + + public constructor(loggerProvider?: LoggerProvider) { + if (loggerProvider) { + this.logger = loggerProvider.getLogger('default') + } else { + this.logger = logsAPI.getLoggerProvider().getLogger("default") + } + this.includeFieldsAsAttributes = FieldInclusionMode.CustomFieldsOnly + } + + public setIncludeFieldsAsAttributes(includeFieldsAsAttributes: FieldInclusionMode) { + this.includeFieldsAsAttributes = includeFieldsAsAttributes + } + + public writeRecord(record: Record): void { + if (record.metadata.type == RecordType.Request) { + return // ignore request logs + } + + const attributes = {} as LogAttributes + this.populateExceptionAttributes(record, attributes) + this.populateAdditionalAttributes(record, attributes) + + const severityNumber = this.mapLevelToSeverityNumber(record.metadata.level) + + this.logger.emit({ + severityNumber: severityNumber, + severityText: SeverityNumber[severityNumber], + body: record.metadata.message, + attributes: attributes + }) + } + + private mapLevelToSeverityNumber(level: Level): SeverityNumber { + switch (level) { + case Level.Error: + return SeverityNumber.ERROR + case Level.Warn: + return SeverityNumber.WARN + case Level.Info: + return SeverityNumber.INFO + case Level.Verbose: + case Level.Debug: + return SeverityNumber.DEBUG + case Level.Silly: + return SeverityNumber.TRACE + } + return SeverityNumber.UNSPECIFIED + } + + private populateExceptionAttributes(record: Record, attributes: LogAttributes) { + if (record.metadata.errorName) { + attributes["exception.type"] = record.metadata.errorName + } + if (record.metadata.errorMessage) { + attributes["exception.message"] = record.metadata.errorMessage + } + if (record.metadata.rawStacktrace) { + attributes["exception.stacktrace"] = record.metadata.rawStacktrace + } + } + + private populateAdditionalAttributes(record: Record, attributes: LogAttributes) { + switch(this.includeFieldsAsAttributes) { + case FieldInclusionMode.AllFields: + for (const key in record.payload) { + attributes[key] = record.payload[key] + } + break; + case FieldInclusionMode.CustomFieldsOnly: + for (const key of record.metadata.customFieldNames) { + if (record.payload[key] !== undefined) { + attributes[key] = record.payload[key] + } + } + break; + case FieldInclusionMode.None: + default: + return; + } + } +} + +export enum FieldInclusionMode { + AllFields = "all", + CustomFieldsOnly = "custom-fields", + None = "none" +} diff --git a/src/lib/plugins/stdoutOutput.ts b/src/lib/plugins/stdoutOutput.ts new file mode 100644 index 00000000..a642a63f --- /dev/null +++ b/src/lib/plugins/stdoutOutput.ts @@ -0,0 +1,23 @@ +import os from 'os'; +import { Record } from "../logger/record.js"; +import { OutputPlugin } from "./interfaces.js"; +import { LevelUtils } from '../logger/level.js'; + +export class StdoutOutputPlugin implements OutputPlugin { + + private sinkFunction: ((level: string, payload: string) => any) | undefined; + + writeRecord(record: Record): void { + const jsonStr: string = JSON.stringify(record.payload); + if (this.sinkFunction) { + const level: string = LevelUtils.getName(record.metadata.level); + this.sinkFunction(level, jsonStr); + } else { + process.stdout.write(jsonStr + os.EOL); + } + } + + setSinkFunction(callback: (level: string, payload: string) => any | undefined) { + this.sinkFunction = callback; + } +} diff --git a/src/test/unit-test/helper.test.js b/src/test/unit-test/helper.test.js index 98c473b3..f3ad3d1e 100644 --- a/src/test/unit-test/helper.test.js +++ b/src/test/unit-test/helper.test.js @@ -1,8 +1,7 @@ const { BUILD_CJS_LIB } = require('../paths'); const expect = require('chai').expect; const JSONHelper = require(`${BUILD_CJS_LIB}/helper/jsonHelper.js`).default; -const LevelUtils = require(`${BUILD_CJS_LIB}/helper/levelUtils.js`).default; -const { Level } = require(`${BUILD_CJS_LIB}/logger/level.js`); +const { Level, LevelUtils } = require(`${BUILD_CJS_LIB}/logger/level.js`); const EnvVarHelper = require(`${BUILD_CJS_LIB}/helper/envVarHelper.js`).default; const StacktraceUtils = require(`${BUILD_CJS_LIB}/helper/stacktraceUtils.js`).default; From 4979977f02d62caea25f3598590e1d156f3ff686 Mon Sep 17 00:00:00 2001 From: Florian Rhinow Date: Wed, 9 Sep 2026 11:44:38 +0200 Subject: [PATCH 2/7] Allow explicit context for OpenTelemetry logs --- package-lock.json | 103 ++++++++++++++++++++- package.json | 4 + src/lib/plugins/otelOutput.ts | 20 ++++- src/test/unit-test/otel-output.test.js | 118 +++++++++++++++++++++++++ 4 files changed, 239 insertions(+), 6 deletions(-) create mode 100644 src/test/unit-test/otel-output.test.js diff --git a/package-lock.json b/package-lock.json index 12ca191b..33047ff7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "8.0.0", "license": "Apache-2.0", "dependencies": { + "@opentelemetry/api": "^1.9.1", "@opentelemetry/api-logs": "^0.218.0", "ajv": "^8.18.0", "json-stringify-safe": "^5.0.1", @@ -18,6 +19,9 @@ }, "devDependencies": { "@eslint/js": "^10.0.1", + "@opentelemetry/context-async-hooks": "^2.7.1", + "@opentelemetry/sdk-logs": "^0.218.0", + "@opentelemetry/sdk-trace-base": "^2.7.1", "@types/json-stringify-safe": "^5.0.3", "@types/jsonwebtoken": "^9.0.10", "@types/node": "^25.6.0", @@ -977,9 +981,9 @@ } }, "node_modules/@opentelemetry/api": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", - "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.1.tgz", + "integrity": "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==", "license": "Apache-2.0", "engines": { "node": ">=8.0.0" @@ -997,6 +1001,99 @@ "node": ">=8.0.0" } }, + "node_modules/@opentelemetry/context-async-hooks": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-2.7.1.tgz", + "integrity": "sha512-OPFBYuXEn1E4ja3Y6eeA7O+ZnLBNcXTV5Cgsn1VaqBZ6hC5FnpZPLBNme1LJY8ZtF4aOujPKFoeWN4ik487KuQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/core": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.7.1.tgz", + "integrity": "sha512-QAqIj32AtK6+pEVNG7EOVxHdE06RP+FM5qpiEJ4RtDcFIqKUZHYhl7/7UY5efhwmwNAg7j8QbJVBLxMerc0+gw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/resources": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.7.1.tgz", + "integrity": "sha512-DeT6KKolmC4e/dRQvMQ/RwlnzhaqeiFOXY5ngoOPJ07GgVVKxZOg9EcrNZb5aTzUn+iCrJldAgOfQm1O/QfPAQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.7.1", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-logs": { + "version": "0.218.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.218.0.tgz", + "integrity": "sha512-QvnNdugatFTVCJXH0Mcu7GOOJSylA9j127kIezOE4YwTI4YbowRons2K4WZTv5FMS8T4q9P0NdaRHdkSmeAIag==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.218.0", + "@opentelemetry/core": "2.7.1", + "@opentelemetry/resources": "2.7.1", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.4.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-base": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.7.1.tgz", + "integrity": "sha512-NAYIlsF8MPUsKqJMiDQJTMPOmlbawC1Iz/omMLygZ1C9am8fTKYjTaI+OZM+WTY3t3Glo0wnOg/6/pac6RGPPw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.7.1", + "@opentelemetry/resources": "2.7.1", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/semantic-conventions": { + "version": "1.43.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.43.0.tgz", + "integrity": "sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, "node_modules/@paralleldrive/cuid2": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz", diff --git a/package.json b/package.json index d21b2c52..02b842f8 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,9 @@ }, "devDependencies": { "@eslint/js": "^10.0.1", + "@opentelemetry/context-async-hooks": "^2.7.1", + "@opentelemetry/sdk-logs": "^0.218.0", + "@opentelemetry/sdk-trace-base": "^2.7.1", "@types/json-stringify-safe": "^5.0.3", "@types/jsonwebtoken": "^9.0.10", "@types/node": "^25.6.0", @@ -75,6 +78,7 @@ "README.md" ], "dependencies": { + "@opentelemetry/api": "^1.9.1", "@opentelemetry/api-logs": "^0.218.0", "ajv": "^8.18.0", "json-stringify-safe": "^5.0.1", diff --git a/src/lib/plugins/otelOutput.ts b/src/lib/plugins/otelOutput.ts index fc1d1f43..685ddc4c 100644 --- a/src/lib/plugins/otelOutput.ts +++ b/src/lib/plugins/otelOutput.ts @@ -1,20 +1,25 @@ - +import type { Context } from '@opentelemetry/api' import { logs as logsAPI, Logger, LoggerProvider, SeverityNumber, LogAttributes} from '@opentelemetry/api-logs' import { OutputPlugin } from './interfaces.js' import { Record, RecordType } from '../logger/record.js' import { Level } from '../logger/level.js' +export type OpenTelemetryLogContextResolver = (record: Record) => Context | undefined +export type OpenTelemetryLogContext = Context | OpenTelemetryLogContextResolver + export class OpenTelemetryLogsOutputPlugin implements OutputPlugin { private logger: Logger private includeFieldsAsAttributes: FieldInclusionMode + private context?: OpenTelemetryLogContext - public constructor(loggerProvider?: LoggerProvider) { + public constructor(loggerProvider?: LoggerProvider, context?: OpenTelemetryLogContext) { if (loggerProvider) { this.logger = loggerProvider.getLogger('default') } else { this.logger = logsAPI.getLoggerProvider().getLogger("default") } this.includeFieldsAsAttributes = FieldInclusionMode.CustomFieldsOnly + this.context = context } public setIncludeFieldsAsAttributes(includeFieldsAsAttributes: FieldInclusionMode) { @@ -31,15 +36,24 @@ export class OpenTelemetryLogsOutputPlugin implements OutputPlugin { this.populateAdditionalAttributes(record, attributes) const severityNumber = this.mapLevelToSeverityNumber(record.metadata.level) + const context = this.resolveContext(record) this.logger.emit({ severityNumber: severityNumber, severityText: SeverityNumber[severityNumber], body: record.metadata.message, - attributes: attributes + attributes: attributes, + ...(context && { context }) }) } + private resolveContext(record: Record): Context | undefined { + if (typeof this.context === 'function') { + return this.context(record) + } + return this.context + } + private mapLevelToSeverityNumber(level: Level): SeverityNumber { switch (level) { case Level.Error: diff --git a/src/test/unit-test/otel-output.test.js b/src/test/unit-test/otel-output.test.js new file mode 100644 index 00000000..555dde7f --- /dev/null +++ b/src/test/unit-test/otel-output.test.js @@ -0,0 +1,118 @@ +const { context, SpanKind, trace, TraceFlags } = require('@opentelemetry/api'); +const { SeverityNumber } = require('@opentelemetry/api-logs'); +const { AsyncLocalStorageContextManager } = require('@opentelemetry/context-async-hooks'); +const { + InMemoryLogRecordExporter, + LoggerProvider, + SimpleLogRecordProcessor +} = require('@opentelemetry/sdk-logs'); +const { BasicTracerProvider } = require('@opentelemetry/sdk-trace-base'); +const { expect } = require('chai'); + +const { BUILD_CJS_LIB } = require('../paths'); +const { Level } = require(`${BUILD_CJS_LIB}/logger/level.js`); +const { Record, RecordType } = require(`${BUILD_CJS_LIB}/logger/record.js`); +const { + FieldInclusionMode, + OpenTelemetryLogsOutputPlugin +} = require(`${BUILD_CJS_LIB}/plugins/otelOutput.js`); + +describe('OpenTelemetryLogsOutputPlugin', function () { + let contextManager; + let exporter; + let loggerProvider; + let tracerProvider; + + beforeEach(function () { + contextManager = new AsyncLocalStorageContextManager().enable(); + context.setGlobalContextManager(contextManager); + exporter = new InMemoryLogRecordExporter(); + loggerProvider = new LoggerProvider({ + processors: [new SimpleLogRecordProcessor(exporter)] + }); + }); + + afterEach(async function () { + await loggerProvider.shutdown(); + if (tracerProvider) { + await tracerProvider.shutdown(); + } + context.disable(); + contextManager.disable(); + }); + + it('uses the active context when no context is configured', function () { + const plugin = new OpenTelemetryLogsOutputPlugin(loggerProvider); + const nonRecordingSpanContext = { + traceId: '11111111111111111111111111111111', + spanId: '2222222222222222', + traceFlags: TraceFlags.NONE + }; + const activeContext = trace.setSpanContext(context.active(), nonRecordingSpanContext); + + context.with(activeContext, () => plugin.writeRecord(createRecord())); + + const [logRecord] = exporter.getFinishedLogRecords(); + expect(logRecord.spanContext).to.deep.equal(nonRecordingSpanContext); + expect(trace.getSpan(activeContext).isRecording()).to.equal(false); + }); + + it('uses an explicitly provided context', function () { + const explicitSpanContext = { + traceId: '33333333333333333333333333333333', + spanId: '4444444444444444', + traceFlags: TraceFlags.SAMPLED + }; + const explicitContext = trace.setSpanContext(context.active(), explicitSpanContext); + const plugin = new OpenTelemetryLogsOutputPlugin(loggerProvider, explicitContext); + + plugin.writeRecord(createRecord()); + + const [logRecord] = exporter.getFinishedLogRecords(); + expect(logRecord.spanContext).to.deep.equal(explicitSpanContext); + }); + + it('uses a resolved recording server context while preserving log fields', function () { + tracerProvider = new BasicTracerProvider(); + const serverSpan = tracerProvider.getTracer('test').startSpan('request', { + kind: SpanKind.SERVER + }); + const serverContext = trace.setSpan(context.active(), serverSpan); + const droppedSpanContext = { + traceId: '55555555555555555555555555555555', + spanId: '6666666666666666', + traceFlags: TraceFlags.NONE + }; + const droppedContext = trace.setSpanContext(context.active(), droppedSpanContext); + const record = createRecord(); + let resolvedRecord; + const plugin = new OpenTelemetryLogsOutputPlugin(loggerProvider, currentRecord => { + resolvedRecord = currentRecord; + return serverContext; + }); + plugin.setIncludeFieldsAsAttributes(FieldInclusionMode.CustomFieldsOnly); + + context.with(droppedContext, () => plugin.writeRecord(record)); + + const [logRecord] = exporter.getFinishedLogRecords(); + expect(resolvedRecord).to.equal(record); + expect(serverSpan.isRecording()).to.equal(true); + expect(logRecord.spanContext).to.deep.equal(serverSpan.spanContext()); + expect(logRecord.spanContext.spanId).not.to.equal(droppedSpanContext.spanId); + expect(logRecord.severityNumber).to.equal(SeverityNumber.WARN); + expect(logRecord.severityText).to.equal('WARN'); + expect(logRecord.body).to.equal('context test'); + expect(logRecord.attributes).to.deep.equal({ custom: 'value' }); + + serverSpan.end(); + }); + + function createRecord() { + const record = new Record(RecordType.Message, Level.Warn); + record.metadata.message = 'context test'; + record.metadata.customFieldNames.push('custom'); + record.payload.custom = 'value'; + record.payload.internal = 'not included'; + return record; + } +}); From c656ea0eb1db0eaffbcdd128aa62244d6e724c74 Mon Sep 17 00:00:00 2001 From: Christian Dinse Date: Wed, 9 Sep 2026 22:05:14 +0200 Subject: [PATCH 3/7] Add docs for output plugins (incl. OTel) --- docs/advanced-usage/07-output-plugins.md | 97 +++++++++++++++++++ ...n-transport.md => 08-winston-transport.md} | 2 +- docs/index.markdown | 6 +- 3 files changed, 101 insertions(+), 4 deletions(-) create mode 100644 docs/advanced-usage/07-output-plugins.md rename docs/advanced-usage/{07-winston-transport.md => 08-winston-transport.md} (98%) diff --git a/docs/advanced-usage/07-output-plugins.md b/docs/advanced-usage/07-output-plugins.md new file mode 100644 index 00000000..b7018c9c --- /dev/null +++ b/docs/advanced-usage/07-output-plugins.md @@ -0,0 +1,97 @@ +--- +layout: default +title: Output Plugins +parent: Advanced Usage +nav_order: 7 +permalink: /advanced-usage/output-plugins +--- + +# Output Plugins + +Output plugins define where and how log records are emitted. The library supports multiple active plugins simultaneously and ships with two built-in implementations. + +## Plugin Management + +Output plugins can be managed at runtime using the following methods: + +| Method | Description | +|---|---| +| `log.addOutputPlugin(plugin)` | Adds a plugin alongside any existing ones. | +| `log.setOutputPlugins(...plugins)` | Replaces all existing plugins with the given one(s). | +| `log.getOutputPlugins()` | Returns the list of currently registered plugins. | + +## StdoutOutputPlugin + +The default plugin, registered automatically on startup. +It writes log records to stdout or a configured [sink function](/cf-nodejs-logging-support/advanced-usage/custom-sink-function). +It remains active unless explicitly replaced via `setOutputPlugins()`. + +## OpenTelemetryLogsOutputPlugin Experimental + +> **Note:** This plugin relies on [`@opentelemetry/api-logs`](https://www.npmjs.com/package/@opentelemetry/api-logs), which is marked as experimental by the OpenTelemetry project. Therefore consider this plugin experimental as well and be prepared for potential breaking changes in future releases. + +Available since version 8.1.0. Emits log records via the [OpenTelemetry Logs API](https://opentelemetry.io/docs/specs/otel/logs/). +Only message logs are forwarded, whereas request logs are not emitted. +It requires a configured OTel SDK with a `LoggerProvider` and appropriate exporters, either via the global OTel SDK (e.g. `@opentelemetry/sdk-node`) or passed explicitly to the constructor. +The plugin itself does not initialize any OTel SDK components. + +### Registering the Plugin + +```js +import log, { OpenTelemetryLogsOutputPlugin } from 'cf-nodejs-logging-support'; + +// Keep stdout output and also emit via OTel: +log.addOutputPlugin(new OpenTelemetryLogsOutputPlugin()); + +// Emit via OTel only: +log.setOutputPlugins(new OpenTelemetryLogsOutputPlugin()); +``` + +The constructor accepts the following optional parameters: + +| Parameter | Type | Description | +|---|---|---| +| `loggerProvider` | `LoggerProvider` | OTel `LoggerProvider` to use. Defaults to the global provider. | +| `context` | `Context` or `(record) => Context | undefined` | OTel context to attach to emitted log records. Can be a static `Context` or a resolver function that receives the log record and returns a context. | + +### Including Fields as Attributes + +By default, only custom fields are forwarded as OTel log attributes. This can be changed with `setIncludeFieldsAsAttributes()`: + +```js +import log, { OpenTelemetryLogsOutputPlugin, FieldInclusionMode } from 'cf-nodejs-logging-support'; + +const plugin = new OpenTelemetryLogsOutputPlugin(); +plugin.setIncludeFieldsAsAttributes(FieldInclusionMode.AllFields); + +log.addOutputPlugin(plugin); +``` + +| Mode | Description | +|---|---| +| `FieldInclusionMode.CustomFieldsOnly` | Only custom fields are added as attributes (default) | +| `FieldInclusionMode.AllFields` | All log record fields are added as attributes | +| `FieldInclusionMode.None` | No fields are added as attributes | + +### Exception Attributes + +When logging an error, the plugin automatically maps error information to the standard OTel exception attributes: + +| OTel Attribute | Source | +|---|---| +| `exception.type` | Error name | +| `exception.message` | Error message | +| `exception.stacktrace` | Stack trace | + +### Logging Level Mapping + +The plugin maps the library's log levels to OTel severity levels as follows: + +| Logging Level | OTel Severity Text | OTel Severity Number | +|---|---|---| +| `error` | `ERROR` | 17 | +| `warn` | `WARN` | 13 | +| `info` | `INFO` | 9 | +| `verbose` | `DEBUG` | 5 | +| `debug` | `DEBUG` | 5 | +| `silly` | `TRACE` | 1 | diff --git a/docs/advanced-usage/07-winston-transport.md b/docs/advanced-usage/08-winston-transport.md similarity index 98% rename from docs/advanced-usage/07-winston-transport.md rename to docs/advanced-usage/08-winston-transport.md index 47857cef..eca74006 100644 --- a/docs/advanced-usage/07-winston-transport.md +++ b/docs/advanced-usage/08-winston-transport.md @@ -2,7 +2,7 @@ layout: default title: Winston Transport parent: Advanced Usage -nav_order: 7 +nav_order: 8 permalink: /advanced-usage/winston-transport --- diff --git a/docs/index.markdown b/docs/index.markdown index 9f75d947..1319231f 100644 --- a/docs/index.markdown +++ b/docs/index.markdown @@ -47,9 +47,9 @@ Create child loggers that inherit and extend a logging context, ideal for tracing a single request or transaction across multiple modules. -
-

TypeScript ready

-Full TypeScript typings are included. +
+

OpenTelemetryExperimental

+Emit logs via the OpenTelemetry Logs API using the built-in output plugin.
From a9f26d35a6600e531516c1374bfb51d020e307ea Mon Sep 17 00:00:00 2001 From: Christian Dinse Date: Thu, 10 Sep 2026 10:21:05 +0200 Subject: [PATCH 4/7] Update dependencies --- package-lock.json | 323 ++++++++++++++++++---------------------------- package.json | 4 +- 2 files changed, 126 insertions(+), 201 deletions(-) diff --git a/package-lock.json b/package-lock.json index 33047ff7..b34adb6f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "Apache-2.0", "dependencies": { "@opentelemetry/api": "^1.9.1", - "@opentelemetry/api-logs": "^0.218.0", + "@opentelemetry/api-logs": "^0.222.0", "ajv": "^8.18.0", "json-stringify-safe": "^5.0.1", "jsonwebtoken": "^9.0.3", @@ -20,7 +20,7 @@ "devDependencies": { "@eslint/js": "^10.0.1", "@opentelemetry/context-async-hooks": "^2.7.1", - "@opentelemetry/sdk-logs": "^0.218.0", + "@opentelemetry/sdk-logs": "^0.222.0", "@opentelemetry/sdk-trace-base": "^2.7.1", "@types/json-stringify-safe": "^5.0.3", "@types/jsonwebtoken": "^9.0.10", @@ -412,9 +412,9 @@ } }, "node_modules/@eslint/config-array/node_modules/brace-expansion": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", - "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", "dev": true, "license": "MIT", "peer": true, @@ -422,7 +422,7 @@ "balanced-match": "^4.0.2" }, "engines": { - "node": "18 || 20 || >=22" + "node": "20 || >=22" } }, "node_modules/@eslint/config-array/node_modules/minimatch": { @@ -990,9 +990,9 @@ } }, "node_modules/@opentelemetry/api-logs": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.218.0.tgz", - "integrity": "sha512-fmEWp5kXlGEc3i/lR698Hz41DfGyN4Tbe4g7L1AxSc7fF8Xeh/FQ9Quqpa9dVA413Q1Ad43QOLzU4JoXgbFPWw==", + "version": "0.222.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.222.0.tgz", + "integrity": "sha512-9mb1If+IF6u0ZVXkHQ6ogEae5HwA6ajIVUgpSDQyRASxft6BSXHvBvPooRle3yFN/fKnCdSOnuu0OC3PLcF6+g==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/api": "^1.3.0" @@ -1015,9 +1015,9 @@ } }, "node_modules/@opentelemetry/core": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.7.1.tgz", - "integrity": "sha512-QAqIj32AtK6+pEVNG7EOVxHdE06RP+FM5qpiEJ4RtDcFIqKUZHYhl7/7UY5efhwmwNAg7j8QbJVBLxMerc0+gw==", + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.11.0.tgz", + "integrity": "sha512-7YP44XH0tV6+Mb54x2YGf84i7yi+31MBZlE8JwvozkxyTvXbSp10X7cI7YE49ChJ3shMJoBmCJF3+1QFBJctGA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -1031,13 +1031,13 @@ } }, "node_modules/@opentelemetry/resources": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.7.1.tgz", - "integrity": "sha512-DeT6KKolmC4e/dRQvMQ/RwlnzhaqeiFOXY5ngoOPJ07GgVVKxZOg9EcrNZb5aTzUn+iCrJldAgOfQm1O/QfPAQ==", + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.11.0.tgz", + "integrity": "sha512-Ie7+8q8MDF4FAEQCKVMTx3ReUvxiIAgIiiW3c9JdmP8+HMcDy20puT+AHjexnExgnbvBxjQ9fjkFDWrikJ2jQA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.7.1", + "@opentelemetry/core": "2.11.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { @@ -1048,15 +1048,15 @@ } }, "node_modules/@opentelemetry/sdk-logs": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.218.0.tgz", - "integrity": "sha512-QvnNdugatFTVCJXH0Mcu7GOOJSylA9j127kIezOE4YwTI4YbowRons2K4WZTv5FMS8T4q9P0NdaRHdkSmeAIag==", + "version": "0.222.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.222.0.tgz", + "integrity": "sha512-+19YHODIjaUCArxleaJtuufFZVpz/xvvK+VllQqE+W8hHolxdoRwHfK/s667zezwh1hkx6FFF+oYzetYgqK+Bg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "0.218.0", - "@opentelemetry/core": "2.7.1", - "@opentelemetry/resources": "2.7.1", + "@opentelemetry/api-logs": "0.222.0", + "@opentelemetry/core": "2.11.0", + "@opentelemetry/resources": "2.11.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { @@ -1066,15 +1066,34 @@ "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, + "node_modules/@opentelemetry/sdk-trace": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace/-/sdk-trace-2.11.0.tgz", + "integrity": "sha512-fFnTqGm8/G73GQVnxYi7LXa1ZVYEUvgL6XI1LpvV0bPC7WQ/ZGgKxCSl8FnlZBKto9JHHEFTO6s6CUpvvtwFrA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.11.0", + "@opentelemetry/resources": "2.11.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, "node_modules/@opentelemetry/sdk-trace-base": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.7.1.tgz", - "integrity": "sha512-NAYIlsF8MPUsKqJMiDQJTMPOmlbawC1Iz/omMLygZ1C9am8fTKYjTaI+OZM+WTY3t3Glo0wnOg/6/pac6RGPPw==", + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.11.0.tgz", + "integrity": "sha512-H19x/TX/LZdqiYOjM7fqtSxwlplC5pgelavqbQdHbhdq0q/AI/TGkM2dfGuuynTXmJPeF2HoZVoPDu+TGoW78A==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.7.1", - "@opentelemetry/resources": "2.7.1", + "@opentelemetry/core": "2.11.0", + "@opentelemetry/resources": "2.11.0", + "@opentelemetry/sdk-trace": "2.11.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { @@ -1394,16 +1413,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", - "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" }, "engines": { - "node": "18 || 20 || >=22" + "node": "20 || >=22" } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { @@ -1700,21 +1719,21 @@ } }, "node_modules/body-parser": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", - "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", + "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", "dev": true, "license": "MIT", "dependencies": { "bytes": "^3.1.2", - "content-type": "^1.0.5", + "content-type": "^2.0.0", "debug": "^4.4.3", - "http-errors": "^2.0.0", - "iconv-lite": "^0.7.0", + "http-errors": "^2.0.1", + "iconv-lite": "^0.7.2", "on-finished": "^2.4.1", - "qs": "^6.14.1", - "raw-body": "^3.0.1", - "type-is": "^2.0.1" + "qs": "^6.15.2", + "raw-body": "^3.0.2", + "type-is": "^2.1.0" }, "engines": { "node": ">=18" @@ -1724,6 +1743,20 @@ "url": "https://opencollective.com/express" } }, + "node_modules/body-parser/node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/body-parser/node_modules/media-typer": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", @@ -1780,24 +1813,10 @@ "url": "https://opencollective.com/express" } }, - "node_modules/body-parser/node_modules/type-is/node_modules/content-type": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", - "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, "node_modules/brace-expansion": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", - "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", "dev": true, "license": "MIT", "dependencies": { @@ -2524,9 +2543,9 @@ } }, "node_modules/eslint/node_modules/brace-expansion": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", - "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", "dev": true, "license": "MIT", "peer": true, @@ -2534,7 +2553,7 @@ "balanced-match": "^4.0.2" }, "engines": { - "node": "18 || 20 || >=22" + "node": "20 || >=22" } }, "node_modules/eslint/node_modules/eslint-visitor-keys": { @@ -2791,27 +2810,6 @@ "node": ">= 0.8" } }, - "node_modules/express/node_modules/http-errors": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", - "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "depd": "~2.0.0", - "inherits": "~2.0.4", - "setprototypeof": "~1.2.0", - "statuses": "~2.0.2", - "toidentifier": "~1.0.1" - }, - "engines": { - "node": ">= 0.8" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, "node_modules/express/node_modules/media-typer": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", @@ -2899,16 +2897,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/express/node_modules/statuses": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/express/node_modules/type-is": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", @@ -3651,19 +3639,24 @@ "license": "MIT" }, "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", "dev": true, + "license": "MIT", "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" }, "engines": { "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/iconv-lite": { @@ -3904,17 +3897,6 @@ "node": "20 || >=22" } }, - "node_modules/istanbul-lib-processinfo/node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", - "dev": true, - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/istanbul-lib-report": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", @@ -4697,16 +4679,16 @@ } }, "node_modules/nyc/node_modules/brace-expansion": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", - "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" }, "engines": { - "node": "18 || 20 || >=22" + "node": "20 || >=22" } }, "node_modules/nyc/node_modules/cliui": { @@ -5270,12 +5252,14 @@ } }, "node_modules/qs": { - "version": "6.15.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", - "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.16.0.tgz", + "integrity": "sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { - "side-channel": "^1.1.0" + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" }, "engines": { "node": ">=0.6" @@ -5314,35 +5298,6 @@ "node": ">= 0.10" } }, - "node_modules/raw-body/node_modules/http-errors": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", - "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", - "dev": true, - "dependencies": { - "depd": "~2.0.0", - "inherits": "~2.0.4", - "setprototypeof": "~1.2.0", - "statuses": "~2.0.2", - "toidentifier": "~1.0.1" - }, - "engines": { - "node": ">= 0.8" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/raw-body/node_modules/statuses": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", - "dev": true, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/readdirp": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.1.1.tgz", @@ -5682,16 +5637,16 @@ } }, "node_modules/rimraf/node_modules/brace-expansion": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", - "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" }, "engines": { - "node": "18 || 20 || >=22" + "node": "20 || >=22" } }, "node_modules/rimraf/node_modules/glob": { @@ -5857,27 +5812,6 @@ "node": ">= 0.8" } }, - "node_modules/serve-static/node_modules/http-errors": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", - "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "depd": "~2.0.0", - "inherits": "~2.0.4", - "setprototypeof": "~1.2.0", - "statuses": "~2.0.2", - "toidentifier": "~1.0.1" - }, - "engines": { - "node": ">= 0.8" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, "node_modules/serve-static/node_modules/mime-db": { "version": "1.54.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", @@ -5932,16 +5866,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/serve-static/node_modules/statuses": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", @@ -5984,15 +5908,15 @@ } }, "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" }, @@ -6004,14 +5928,14 @@ } }, "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" + "object-inspect": "^1.13.4" }, "engines": { "node": ">= 0.4" @@ -6126,10 +6050,11 @@ "license": "BSD-3-Clause" }, "node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.8" } @@ -6295,16 +6220,16 @@ } }, "node_modules/test-exclude/node_modules/brace-expansion": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", - "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" }, "engines": { - "node": "18 || 20 || >=22" + "node": "20 || >=22" } }, "node_modules/test-exclude/node_modules/glob": { diff --git a/package.json b/package.json index 02b842f8..8e953642 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "devDependencies": { "@eslint/js": "^10.0.1", "@opentelemetry/context-async-hooks": "^2.7.1", - "@opentelemetry/sdk-logs": "^0.218.0", + "@opentelemetry/sdk-logs": "^0.222.0", "@opentelemetry/sdk-trace-base": "^2.7.1", "@types/json-stringify-safe": "^5.0.3", "@types/jsonwebtoken": "^9.0.10", @@ -79,7 +79,7 @@ ], "dependencies": { "@opentelemetry/api": "^1.9.1", - "@opentelemetry/api-logs": "^0.218.0", + "@opentelemetry/api-logs": "^0.222.0", "ajv": "^8.18.0", "json-stringify-safe": "^5.0.1", "jsonwebtoken": "^9.0.3", From 3c512621aa8132e329a797161bb6a9e2857055b1 Mon Sep 17 00:00:00 2001 From: Christian Dinse Date: Thu, 10 Sep 2026 10:21:19 +0200 Subject: [PATCH 5/7] Fix test setup --- src/test/unit-test/otel-output.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/unit-test/otel-output.test.js b/src/test/unit-test/otel-output.test.js index 555dde7f..23e4f134 100644 --- a/src/test/unit-test/otel-output.test.js +++ b/src/test/unit-test/otel-output.test.js @@ -28,7 +28,7 @@ describe('OpenTelemetryLogsOutputPlugin', function () { context.setGlobalContextManager(contextManager); exporter = new InMemoryLogRecordExporter(); loggerProvider = new LoggerProvider({ - processors: [new SimpleLogRecordProcessor(exporter)] + processors: [new SimpleLogRecordProcessor({ exporter })] }); }); From 4cebdcab6d4720f340b6e0322063f1b92b6cdca6 Mon Sep 17 00:00:00 2001 From: Christian Dinse Date: Thu, 10 Sep 2026 10:21:51 +0200 Subject: [PATCH 6/7] Add code docs --- src/lib/plugins/interfaces.ts | 2 ++ src/lib/plugins/otelOutput.ts | 36 ++++++++++++++++++++++++++++++----- 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/src/lib/plugins/interfaces.ts b/src/lib/plugins/interfaces.ts index ac098a3b..0b357f50 100644 --- a/src/lib/plugins/interfaces.ts +++ b/src/lib/plugins/interfaces.ts @@ -1,5 +1,7 @@ import { Record } from "../logger/record" +// Interface for output plugins that handle log records. export interface OutputPlugin { + // Writes a log record to the output plugin. Implementations should handle the record appropriately. writeRecord(record: Record): void } diff --git a/src/lib/plugins/otelOutput.ts b/src/lib/plugins/otelOutput.ts index 685ddc4c..d5bee1cf 100644 --- a/src/lib/plugins/otelOutput.ts +++ b/src/lib/plugins/otelOutput.ts @@ -4,14 +4,37 @@ import { OutputPlugin } from './interfaces.js' import { Record, RecordType } from '../logger/record.js' import { Level } from '../logger/level.js' +/** @experimental Requires `@opentelemetry/api-logs`, which is itself experimental and subject to breaking changes. */ export type OpenTelemetryLogContextResolver = (record: Record) => Context | undefined + +/** @experimental Requires `@opentelemetry/api-logs`, which is itself experimental and subject to breaking changes. */ export type OpenTelemetryLogContext = Context | OpenTelemetryLogContextResolver +/** + * @experimental Used with {@link OpenTelemetryLogsOutputPlugin}, which is experimental. + */ +export enum FieldInclusionMode { + AllFields = "all", + CustomFieldsOnly = "custom-fields", + None = "none" +} + +/** + * Output plugin that emits log records via the OpenTelemetry Logs API. + * + * @experimental This plugin depends on `@opentelemetry/api-logs`, which is marked as experimental + * by the OpenTelemetry project and may introduce breaking changes in future releases. + */ export class OpenTelemetryLogsOutputPlugin implements OutputPlugin { private logger: Logger private includeFieldsAsAttributes: FieldInclusionMode private context?: OpenTelemetryLogContext + /** + * Constructs a new OpenTelemetryLogsOutputPlugin. + * @param loggerProvider Optional OTel LoggerProvider to use. Defaults to the global provider. + * @param context Optional OTel context or context resolver function to attach to emitted log records. + */ public constructor(loggerProvider?: LoggerProvider, context?: OpenTelemetryLogContext) { if (loggerProvider) { this.logger = loggerProvider.getLogger('default') @@ -22,10 +45,18 @@ export class OpenTelemetryLogsOutputPlugin implements OutputPlugin { this.context = context } + /** + * Sets the mode for including fields as attributes in emitted OTel log records. + * @param includeFieldsAsAttributes The field inclusion mode to use. + */ public setIncludeFieldsAsAttributes(includeFieldsAsAttributes: FieldInclusionMode) { this.includeFieldsAsAttributes = includeFieldsAsAttributes } + /** + * Writes a log record to the output plugin. Request logs are ignored; only message logs are emitted. + * @param record The log record to write. + */ public writeRecord(record: Record): void { if (record.metadata.type == RecordType.Request) { return // ignore request logs @@ -104,8 +135,3 @@ export class OpenTelemetryLogsOutputPlugin implements OutputPlugin { } } -export enum FieldInclusionMode { - AllFields = "all", - CustomFieldsOnly = "custom-fields", - None = "none" -} From 8c1b20abe998885ff21c5acfd3672744768c13be Mon Sep 17 00:00:00 2001 From: Christian Dinse Date: Mon, 14 Sep 2026 13:33:07 +0200 Subject: [PATCH 7/7] Fix level-to-severity mapping --- docs/advanced-usage/07-output-plugins.md | 2 +- src/lib/plugins/otelOutput.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/advanced-usage/07-output-plugins.md b/docs/advanced-usage/07-output-plugins.md index b7018c9c..ee2c513a 100644 --- a/docs/advanced-usage/07-output-plugins.md +++ b/docs/advanced-usage/07-output-plugins.md @@ -92,6 +92,6 @@ The plugin maps the library's log levels to OTel severity levels as follows: | `error` | `ERROR` | 17 | | `warn` | `WARN` | 13 | | `info` | `INFO` | 9 | -| `verbose` | `DEBUG` | 5 | +| `verbose` | `DEBUG2` | 6 | | `debug` | `DEBUG` | 5 | | `silly` | `TRACE` | 1 | diff --git a/src/lib/plugins/otelOutput.ts b/src/lib/plugins/otelOutput.ts index d5bee1cf..fd4a42d9 100644 --- a/src/lib/plugins/otelOutput.ts +++ b/src/lib/plugins/otelOutput.ts @@ -94,6 +94,7 @@ export class OpenTelemetryLogsOutputPlugin implements OutputPlugin { case Level.Info: return SeverityNumber.INFO case Level.Verbose: + return SeverityNumber.DEBUG2 case Level.Debug: return SeverityNumber.DEBUG case Level.Silly: