fix(otel): exported logs and spans will now have matching trace IDs (#2724)

When using custom OTLP exporters via `telemetry.exporters` and 

This occurred when tasks were triggered **without** a parent trace 
context (e.g., via API or dashboard). In this scenario: - Spans were 
correctly rewritten to use the generated `externalTraceId` - Logs kept 
their original internal trace ID due to a bug in the early return logic

### Root Cause

In `ExternalLogRecordExporterWrapper.transformLogRecord()`, the early 
return condition incorrectly included `!this.externalTraceContext`:

```typescript
if (!logRecord.spanContext || !this.externalTraceId || 
!this.externalTraceContext) {   return logRecord;  // Bug: Returns early
when externalTraceContext is undefined }

// This fallback logic was never reached:
const externalTraceId = this.externalTraceContext
  ? this.externalTraceContext.traceId
  : this.externalTraceId;
```

### Fix

1. **Reordered logic in `transformLogRecord()`**: Move the 
1. `externalTraceId` calculation before the early return, and check the 
1. culated value instead of `this.externalTraceContext`:

```typescript
const externalTraceId = this.externalTraceContext
  ? this.externalTraceContext.traceId
  : this.externalTraceId;

if (!logRecord.spanContext || !externalTraceId) {
  return logRecord;
}
```

2. **Clarified `_isExternallySampled` logic**: Updated both 
2. `ExternalSpanExporterWrapper` and `ExternalLogRecordExporterWrapper` 
2. explicitly handle the case where there's no external trace context 
2. a generated `externalTraceId` exists:

```typescript
this._isExternallySampled = externalTraceContext
  ? isTraceFlagSampled(externalTraceContext.traceFlags)
  : !!externalTraceId;
```

### Impact

Logs and spans from the same task run will now have matching trace IDs 
when exported to external observability tools, enabling proper trace correlation regardless of whether the task was triggered with or without a parent trace context. 
`telemetry.logExporters` in `trigger.config.ts`, logs and spans were 
exported with **different trace IDs**, breaking trace correlation in
external observability tools like Datadog.
This commit is contained in:
Eric Allam
2025-12-02 14:16:06 +00:00
committed by GitHub
parent 2f1a72b109
commit 9f27422472
2 changed files with 17 additions and 7 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"trigger.dev": patch
---
fix(otel): exported logs and spans will now have matching trace IDs
+12 -7
View File
@@ -314,7 +314,9 @@ class ExternalSpanExporterWrapper {
| { traceId: string; spanId: string; traceFlags: number; tracestate?: string }
| undefined
) {
this._isExternallySampled = isTraceFlagSampled(externalTraceContext?.traceFlags);
this._isExternallySampled = externalTraceContext
? isTraceFlagSampled(externalTraceContext.traceFlags)
: !!externalTraceId;
}
private transformSpan(span: ReadableSpan): ReadableSpan | undefined {
@@ -396,7 +398,9 @@ class ExternalLogRecordExporterWrapper {
| { traceId: string; spanId: string; tracestate?: string; traceFlags: number }
| undefined
) {
this._isExternallySampled = isTraceFlagSampled(externalTraceContext?.traceFlags);
this._isExternallySampled = externalTraceContext
? isTraceFlagSampled(externalTraceContext.traceFlags)
: !!externalTraceId;
}
export(logs: any[], resultCallback: (result: any) => void): void {
@@ -416,16 +420,17 @@ class ExternalLogRecordExporterWrapper {
}
transformLogRecord(logRecord: ReadableLogRecord): ReadableLogRecord {
// If there's no spanContext, or if the externalTraceId is not set, return the original logRecord.
if (!logRecord.spanContext || !this.externalTraceId || !this.externalTraceContext) {
return logRecord;
}
// Capture externalTraceId for use within the proxy's scope.
// Use externalTraceContext.traceId if available, otherwise fall back to generated externalTraceId
const externalTraceId = this.externalTraceContext
? this.externalTraceContext.traceId
: this.externalTraceId;
// If there's no spanContext, or if the externalTraceId is not set, return the original logRecord.
if (!logRecord.spanContext || !externalTraceId) {
return logRecord;
}
return new Proxy(logRecord, {
get(target, prop, receiver) {
if (prop === "spanContext") {