diff --git a/docs/advanced-usage/07-output-plugins.md b/docs/advanced-usage/07-output-plugins.md index ee2c513a..d11c7c02 100644 --- a/docs/advanced-usage/07-output-plugins.md +++ b/docs/advanced-usage/07-output-plugins.md @@ -31,7 +31,7 @@ It remains active unless explicitly replaced via `setOutputPlugins()`. > **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. +By default only message logs are forwarded; request logs can be enabled via [`setEmitRequestLogs()`](#emitting-request-logs). 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. @@ -73,6 +73,27 @@ log.addOutputPlugin(plugin); | `FieldInclusionMode.AllFields` | All log record fields are added as attributes | | `FieldInclusionMode.None` | No fields are added as attributes | +### Emitting Request Logs + +By default the plugin ignores request logs and only emits message logs. To emit request logs as OTel log records, enable it with `setEmitRequestLogs()`: + +```js +import log, { OpenTelemetryLogsOutputPlugin, FieldInclusionMode } from 'cf-nodejs-logging-support'; + +const plugin = new OpenTelemetryLogsOutputPlugin(); +plugin.setEmitRequestLogs(true); +// Optional: forward all request fields as attributes +plugin.setIncludeFieldsAsAttributes(FieldInclusionMode.AllFields); + +log.addOutputPlugin(plugin); +``` + +| Method | Description | +|---|---| +| `plugin.setEmitRequestLogs(enabled)` | Enables (`true`) or disables (`false`, default) emitting request logs as OTel log records. | + +Request logs have no message, so their OTel log body is a short summary of the `method`, `request` and `response_status` fields (e.g. `GET /hello 200`), falling back to `"request"`. All request fields are forwarded as attributes according to the configured [field inclusion mode](#including-fields-as-attributes). + ### Exception Attributes When logging an error, the plugin automatically maps error information to the standard OTel exception attributes: diff --git a/src/lib/plugins/otelOutput.ts b/src/lib/plugins/otelOutput.ts index fd4a42d9..a9dc9b66 100644 --- a/src/lib/plugins/otelOutput.ts +++ b/src/lib/plugins/otelOutput.ts @@ -29,6 +29,7 @@ export class OpenTelemetryLogsOutputPlugin implements OutputPlugin { private logger: Logger private includeFieldsAsAttributes: FieldInclusionMode private context?: OpenTelemetryLogContext + private emitRequestLogs: boolean /** * Constructs a new OpenTelemetryLogsOutputPlugin. @@ -43,6 +44,7 @@ export class OpenTelemetryLogsOutputPlugin implements OutputPlugin { } this.includeFieldsAsAttributes = FieldInclusionMode.CustomFieldsOnly this.context = context + this.emitRequestLogs = false } /** @@ -54,12 +56,21 @@ export class OpenTelemetryLogsOutputPlugin implements OutputPlugin { } /** - * Writes a log record to the output plugin. Request logs are ignored; only message logs are emitted. + * Controls whether request logs are emitted as OTel log records in addition to message logs. + * @param enabled Whether request logs should be emitted. Defaults to false. + */ + public setEmitRequestLogs(enabled: boolean) { + this.emitRequestLogs = enabled + } + + /** + * Writes a log record to the output plugin. Request logs are ignored unless enabled via + * {@link setEmitRequestLogs}; message logs are always emitted. * @param record The log record to write. */ public writeRecord(record: Record): void { - if (record.metadata.type == RecordType.Request) { - return // ignore request logs + if (record.metadata.type == RecordType.Request && !this.emitRequestLogs) { + return } const attributes = {} as LogAttributes @@ -72,12 +83,25 @@ export class OpenTelemetryLogsOutputPlugin implements OutputPlugin { this.logger.emit({ severityNumber: severityNumber, severityText: SeverityNumber[severityNumber], - body: record.metadata.message, + body: this.resolveBody(record), attributes: attributes, ...(context && { context }) }) } + /** + * Resolves the OTel log body. Message logs use their message; request logs, which have none, + * use a short summary of common request fields, falling back to "request". + */ + private resolveBody(record: Record): string | undefined { + if (record.metadata.type != RecordType.Request) { + return record.metadata.message + } + const { method, request, response_status } = record.payload + const summary = [method, request, response_status].filter(part => part !== undefined).join(" ") + return summary.length > 0 ? summary : "request" + } + private resolveContext(record: Record): Context | undefined { if (typeof this.context === 'function') { return this.context(record) diff --git a/src/test/unit-test/otel-output.test.js b/src/test/unit-test/otel-output.test.js index 23e4f134..24dfe3dc 100644 --- a/src/test/unit-test/otel-output.test.js +++ b/src/test/unit-test/otel-output.test.js @@ -107,6 +107,41 @@ describe('OpenTelemetryLogsOutputPlugin', function () { serverSpan.end(); }); + it('ignores request logs by default', function () { + const plugin = new OpenTelemetryLogsOutputPlugin(loggerProvider); + + plugin.writeRecord(createRequestRecord()); + + expect(exporter.getFinishedLogRecords()).to.have.lengthOf(0); + }); + + it('emits request logs with a summary body once enabled', function () { + const plugin = new OpenTelemetryLogsOutputPlugin(loggerProvider); + plugin.setEmitRequestLogs(true); + plugin.setIncludeFieldsAsAttributes(FieldInclusionMode.AllFields); + + plugin.writeRecord(createRequestRecord()); + + const [logRecord] = exporter.getFinishedLogRecords(); + expect(logRecord.body).to.equal('GET /hello 200'); + expect(logRecord.attributes).to.deep.equal({ + method: 'GET', + request: '/hello', + response_status: 200 + }); + }); + + it('falls back to a generic body when request fields are absent', function () { + const plugin = new OpenTelemetryLogsOutputPlugin(loggerProvider); + plugin.setEmitRequestLogs(true); + + const record = new Record(RecordType.Request, Level.Info); + plugin.writeRecord(record); + + const [logRecord] = exporter.getFinishedLogRecords(); + expect(logRecord.body).to.equal('request'); + }); + function createRecord() { const record = new Record(RecordType.Message, Level.Warn); record.metadata.message = 'context test'; @@ -115,4 +150,12 @@ describe('OpenTelemetryLogsOutputPlugin', function () { record.payload.internal = 'not included'; return record; } + + function createRequestRecord() { + const record = new Record(RecordType.Request, Level.Info); + record.payload.method = 'GET'; + record.payload.request = '/hello'; + record.payload.response_status = 200; + return record; + } });