Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion docs/advanced-usage/07-output-plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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:
Expand Down
32 changes: 28 additions & 4 deletions src/lib/plugins/otelOutput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -43,6 +44,7 @@ export class OpenTelemetryLogsOutputPlugin implements OutputPlugin {
}
this.includeFieldsAsAttributes = FieldInclusionMode.CustomFieldsOnly
this.context = context
this.emitRequestLogs = false
}

/**
Expand All @@ -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
Expand All @@ -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)
Expand Down
43 changes: 43 additions & 0 deletions src/test/unit-test/otel-output.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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;
}
});
Loading