1- import { metrics , trace , SpanStatusCode } from "@opentelemetry/api" ;
2- import type { SeverityNumber } from "@opentelemetry/api-logs" ;
1+ import {
2+ context as otelContextApi ,
3+ metrics ,
4+ trace ,
5+ SpanStatusCode ,
6+ TraceFlags ,
7+ } from "@opentelemetry/api" ;
8+ import type { LogRecord , SeverityNumber } from "@opentelemetry/api-logs" ;
39import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-proto" ;
410import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-proto" ;
511import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto" ;
@@ -9,8 +15,19 @@ import { PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics";
915import { NodeSDK } from "@opentelemetry/sdk-node" ;
1016import { ParentBasedSampler , TraceIdRatioBasedSampler } from "@opentelemetry/sdk-trace-base" ;
1117import { ATTR_SERVICE_NAME } from "@opentelemetry/semantic-conventions" ;
12- import type { DiagnosticEventPayload , OpenClawPluginService } from "../api.js" ;
13- import { onDiagnosticEvent , redactSensitiveText , registerLogTransport } from "../api.js" ;
18+ import type {
19+ DiagnosticEventPayload ,
20+ DiagnosticTraceContext ,
21+ OpenClawPluginService ,
22+ } from "../api.js" ;
23+ import {
24+ isValidDiagnosticSpanId ,
25+ isValidDiagnosticTraceFlags ,
26+ isValidDiagnosticTraceId ,
27+ onDiagnosticEvent ,
28+ redactSensitiveText ,
29+ registerLogTransport ,
30+ } from "../api.js" ;
1431
1532const DEFAULT_SERVICE_NAME = "openclaw" ;
1633
@@ -62,6 +79,83 @@ function redactOtelAttributes(attributes: Record<string, string | number | boole
6279 return redactedAttributes ;
6380}
6481
82+ function normalizeTraceContext ( value : unknown ) : DiagnosticTraceContext | undefined {
83+ if ( ! value || typeof value !== "object" || Array . isArray ( value ) ) {
84+ return undefined ;
85+ }
86+ const candidate = value as Partial < DiagnosticTraceContext > ;
87+ if ( ! isValidDiagnosticTraceId ( candidate . traceId ) ) {
88+ return undefined ;
89+ }
90+ if ( candidate . spanId !== undefined && ! isValidDiagnosticSpanId ( candidate . spanId ) ) {
91+ return undefined ;
92+ }
93+ if ( candidate . parentSpanId !== undefined && ! isValidDiagnosticSpanId ( candidate . parentSpanId ) ) {
94+ return undefined ;
95+ }
96+ if ( candidate . traceFlags !== undefined && ! isValidDiagnosticTraceFlags ( candidate . traceFlags ) ) {
97+ return undefined ;
98+ }
99+ return {
100+ traceId : candidate . traceId ,
101+ ...( candidate . spanId ? { spanId : candidate . spanId } : { } ) ,
102+ ...( candidate . parentSpanId ? { parentSpanId : candidate . parentSpanId } : { } ) ,
103+ ...( candidate . traceFlags ? { traceFlags : candidate . traceFlags } : { } ) ,
104+ } ;
105+ }
106+
107+ function extractTraceContext ( value : unknown ) : DiagnosticTraceContext | undefined {
108+ const direct = normalizeTraceContext ( value ) ;
109+ if ( direct ) {
110+ return direct ;
111+ }
112+ if ( ! value || typeof value !== "object" || Array . isArray ( value ) ) {
113+ return undefined ;
114+ }
115+ return normalizeTraceContext ( ( value as { trace ?: unknown } ) . trace ) ;
116+ }
117+
118+ function findLogTraceContext (
119+ bindings : Record < string , unknown > | undefined ,
120+ numericArgs : unknown [ ] ,
121+ ) : DiagnosticTraceContext | undefined {
122+ const fromBindings = extractTraceContext ( bindings ) ;
123+ if ( fromBindings ) {
124+ return fromBindings ;
125+ }
126+ for ( const arg of numericArgs ) {
127+ const fromArg = extractTraceContext ( arg ) ;
128+ if ( fromArg ) {
129+ return fromArg ;
130+ }
131+ }
132+ return undefined ;
133+ }
134+
135+ function traceFlagsToOtel ( traceFlags : string | undefined ) : TraceFlags {
136+ const parsed = Number . parseInt ( traceFlags ?? "00" , 16 ) ;
137+ return ( parsed & TraceFlags . SAMPLED ) !== 0 ? TraceFlags . SAMPLED : TraceFlags . NONE ;
138+ }
139+
140+ function addTraceAttributes (
141+ attributes : Record < string , string | number | boolean > ,
142+ traceContext : DiagnosticTraceContext | undefined ,
143+ ) : void {
144+ if ( ! traceContext ) {
145+ return ;
146+ }
147+ attributes [ "openclaw.traceId" ] = traceContext . traceId ;
148+ if ( traceContext . spanId ) {
149+ attributes [ "openclaw.spanId" ] = traceContext . spanId ;
150+ }
151+ if ( traceContext . parentSpanId ) {
152+ attributes [ "openclaw.parentSpanId" ] = traceContext . parentSpanId ;
153+ }
154+ if ( traceContext . traceFlags ) {
155+ attributes [ "openclaw.traceFlags" ] = traceContext . traceFlags ;
156+ }
157+ }
158+
65159export function createDiagnosticsOtelService ( ) : OpenClawPluginService {
66160 let sdk : NodeSDK | null = null ;
67161 let logProvider : LoggerProvider | null = null ;
@@ -294,6 +388,7 @@ export function createDiagnosticsOtelService(): OpenClawPluginService {
294388 // ignore malformed json bindings
295389 }
296390 }
391+ const traceContext = findLogTraceContext ( bindings , numericArgs ) ;
297392
298393 let message = "" ;
299394 if ( numericArgs . length > 0 && typeof numericArgs [ numericArgs . length - 1 ] === "string" ) {
@@ -343,15 +438,25 @@ export function createDiagnosticsOtelService(): OpenClawPluginService {
343438 if ( meta ?. path ?. filePathWithLine ) {
344439 attributes [ "openclaw.code.location" ] = meta . path . filePathWithLine ;
345440 }
441+ addTraceAttributes ( attributes , traceContext ) ;
346442
347443 // OTLP can leave the host boundary, so redact string fields before export.
348- otelLogger . emit ( {
444+ const logRecord : LogRecord = {
349445 body : redactSensitiveText ( message ) ,
350446 severityText : logLevelName ,
351447 severityNumber,
352448 attributes : redactOtelAttributes ( attributes ) ,
353449 timestamp : meta ?. date ?? new Date ( ) ,
354- } ) ;
450+ } ;
451+ if ( traceContext ?. spanId ) {
452+ logRecord . context = trace . setSpanContext ( otelContextApi . active ( ) , {
453+ traceId : traceContext . traceId ,
454+ spanId : traceContext . spanId ,
455+ traceFlags : traceFlagsToOtel ( traceContext . traceFlags ) ,
456+ isRemote : true ,
457+ } ) ;
458+ }
459+ otelLogger . emit ( logRecord ) ;
355460 } catch ( err ) {
356461 ctx . logger . error ( `diagnostics-otel: log transport failed: ${ formatError ( err ) } ` ) ;
357462 }
0 commit comments