103
return LogLevel.Trace;
104
}
106
>
/**
107
>
* Scalar value types accepted for a structured log attribute. Mirrors the
108
>
* subset of OTLP `AnyValue` we serialise on the wire
109
>
* (`stringValue`/`intValue`/`doubleValue`/`boolValue`).
110
>
*/
111
>
export type OtelAttributeValue = string | number | boolean;
112
>
113
>
/**
114
>
* Structured metadata a log call can carry alongside its human-readable
115
>
* message. Pass an instance as the final argument to any `ILogger` method
116
>
* (e.g. `logService.info('MCP server started', new OtelData({ server: 'github' }))`).
117
>
*
118
>
* - For the regular file logger the value is rendered via {@link toJSON}
119
>
* using the usual log formatting (`JSON.stringify`), so it appears as a
120
>
* compact object after the message.
121
>
* - For the {@link OtlpEmitterLogger} the attributes are lifted out of the
122
>
* message and emitted as spec-conformant OTLP `LogRecord.attributes`,
123
>
* keeping the body free of serialised JSON.
124
>
*/
125
>
export class OtelData {
126
>
constructor(readonly attributes: Readonly<Record<string, OtelAttributeValue>>) { }
127
>
128
>
toJSON(): Readonly<Record<string, OtelAttributeValue>> {
129
return this.attributes;
130
}
132
>
133
>
/**
134
>
* A single log record produced by an {@link OtlpEmitterLogger}. The shape
135
>
* mirrors the relevant fields from the OTLP/JSON `LogRecord` spec but is
136
>
* kept intentionally small — only what we need to populate a
137
>
* spec-conformant `ExportLogsServiceRequest` envelope.
138
>
*/
139
>
export interface IOtlpLogRecord {
140
>
/**
141
>
* Time the record was produced, in nanoseconds since the Unix epoch.
142
>
* Encoded as a string because JS numbers cannot losslessly represent
143
>
* 64-bit nanosecond timestamps (this matches the OTLP/JSON wire format).
144
>
*/
145
>
readonly timeUnixNano: string;
146
>
/** OTLP `SeverityNumber` in the range 1..24 (see {@link levelToSeverityNumber}). */
147
>
readonly severityNumber: number;
148
>
/** Short severity name (`trace`/`debug`/...) matching the protocol's `{level}` vocabulary. */
149
>
readonly severityText: OtlpLogLevelName;
150
>
/**
151
>
* Pre-formatted log body. We send the same string the existing
152
>
* `ILogger` printed to the file logger — the OTLP spec models this as
153
>
* `body: { stringValue }`.
154
>
*/
155
>
readonly body: string;
156
>
/**
157
>
* Optional structured metadata carried by an {@link OtelData} argument
158
>
* on the originating log call. Serialised to OTLP `LogRecord.attributes`
159
>
* and absent when the call had no {@link OtelData}.
160
>
*/
161
>
readonly attributes?: Readonly<Record<string, OtelAttributeValue>>;
162
>
}
163
>
164
>
/**
165
>
* Connection-process-wide hub that {@link OtlpEmitterLogger} writes to and
166
>
* the protocol server reads from. Decouples log production (which happens
167
>
* via {@link ILogger}) from protocol broadcast (which needs awareness of
168
>
* connected clients and their subscribed severity).
169
>
*/
170
>
export class OtlpLogEmitter extends Disposable {
171
172
private readonly _onDidLog = this._register(new Emitter<IOtlpLogRecord>());
173
readonly onDidLog: Event<IOtlpLogRecord> = this._onDidLog.event;
175
>
emit(record: IOtlpLogRecord): void {
176
this._onDidLog.fire(record);
177
}
179
>
180
>
/**
181
>
* `AbstractMessageLogger` that converts each `log(level, message)` call
182
>
* into an {@link IOtlpLogRecord} and emits it on the shared
183
>
* {@link OtlpLogEmitter}. Designed to be installed alongside the regular
184
>
* file logger via `new LogService(primary, [otlpLogger])` so every log
185
>
* call is mirrored to OTLP subscribers without duplicating call sites.
186
>
*/
187
>
export class OtlpEmitterLogger extends AbstractMessageLogger {
188
>
189
>
constructor(
190
private readonly _emitter: OtlpLogEmitter,
191
initialLevel: LogLevel = LogLevel.Trace,