Skip to content

Commit 3e16462

Browse files
committed
fix: external traces now respect parent sampling, and prevent broken traces when there is no external trace context
1 parent 944b187 commit 3e16462

File tree

9 files changed

+131
-17
lines changed

9 files changed

+131
-17
lines changed

apps/webapp/app/runEngine/services/triggerTask.server.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -385,7 +385,8 @@ export class RunEngineTriggerTaskService {
385385

386386
const newExternalTraceparent = serializeTraceparent(
387387
parsedTraceparent.traceId,
388-
parentSpanId ?? parsedTraceparent.spanId
388+
parentSpanId ?? parsedTraceparent.spanId,
389+
parsedTraceparent.traceFlags
389390
);
390391

391392
return {
Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
export function parseTraceparent(
22
traceparent?: string
3-
): { traceId: string; spanId: string } | undefined {
3+
): { traceId: string; spanId: string; traceFlags?: string } | undefined {
44
if (!traceparent) {
55
return undefined;
66
}
@@ -11,7 +11,7 @@ export function parseTraceparent(
1111
return undefined;
1212
}
1313

14-
const [version, traceId, spanId] = parts;
14+
const [version, traceId, spanId, traceFlags] = parts;
1515

1616
if (version !== "00") {
1717
return undefined;
@@ -21,9 +21,9 @@ export function parseTraceparent(
2121
return undefined;
2222
}
2323

24-
return { traceId, spanId };
24+
return { traceId, spanId, traceFlags };
2525
}
2626

27-
export function serializeTraceparent(traceId: string, spanId: string) {
28-
return `00-${traceId}-${spanId}-01`;
27+
export function serializeTraceparent(traceId: string, spanId: string, traceFlags?: string) {
28+
return `00-${traceId}-${spanId}-${traceFlags ?? "01"}`;
2929
}

packages/core/src/v3/otel/tracingSDK.ts

Lines changed: 75 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import {
2121
SimpleSpanProcessor,
2222
SpanExporter,
2323
} from "@opentelemetry/sdk-trace-node";
24-
import { SemanticResourceAttributes } from "@opentelemetry/semantic-conventions";
24+
import { SemanticResourceAttributes, SEMATTRS_HTTP_URL } from "@opentelemetry/semantic-conventions";
2525
import { VERSION } from "../../version.js";
2626
import {
2727
OTEL_ATTRIBUTE_PER_EVENT_COUNT_LIMIT,
@@ -287,17 +287,27 @@ function setLogLevel(level: TracingDiagnosticLogLevel) {
287287
}
288288

289289
class ExternalSpanExporterWrapper {
290+
private readonly _isExternallySampled: boolean;
291+
290292
constructor(
291293
private underlyingExporter: SpanExporter,
292294
private externalTraceId: string,
293295
private externalTraceContext:
294-
| { traceId: string; spanId: string; tracestate?: string }
296+
| { traceId: string; spanId: string; tracestate?: string; traceFlags?: string }
295297
| undefined
296-
) {}
298+
) {
299+
this._isExternallySampled =
300+
typeof externalTraceContext?.traceFlags === "string"
301+
? externalTraceContext.traceFlags === "01"
302+
: true;
303+
}
297304

298305
private transformSpan(span: ReadableSpan): ReadableSpan | undefined {
299-
if (span.attributes[SemanticInternalAttributes.SPAN_PARTIAL]) {
300-
// Skip partial spans
306+
if (!this._isExternallySampled) {
307+
return;
308+
}
309+
310+
if (isSpanInternalOnly(span)) {
301311
return;
302312
}
303313

@@ -325,8 +335,15 @@ class ExternalSpanExporterWrapper {
325335
traceState: this.externalTraceContext.tracestate
326336
? new TraceState(this.externalTraceContext.tracestate)
327337
: undefined,
328-
traceFlags: parentSpanContext?.traceFlags ?? 0,
338+
traceFlags:
339+
typeof this.externalTraceContext.traceFlags === "string"
340+
? this.externalTraceContext.traceFlags === "01"
341+
? 1
342+
: 0
343+
: parentSpanContext?.traceFlags ?? 0,
329344
};
345+
} else if (isAttemptSpan) {
346+
parentSpanContext = undefined;
330347
}
331348

332349
return {
@@ -360,15 +377,28 @@ class ExternalSpanExporterWrapper {
360377
}
361378

362379
class ExternalLogRecordExporterWrapper {
380+
private readonly _isExternallySampled: boolean;
381+
363382
constructor(
364383
private underlyingExporter: LogRecordExporter,
365384
private externalTraceId: string,
366385
private externalTraceContext:
367-
| { traceId: string; spanId: string; tracestate?: string }
386+
| { traceId: string; spanId: string; tracestate?: string; traceFlags?: string }
368387
| undefined
369-
) {}
388+
) {
389+
this._isExternallySampled =
390+
typeof externalTraceContext?.traceFlags === "string"
391+
? externalTraceContext.traceFlags === "01"
392+
: true;
393+
}
370394

371395
export(logs: any[], resultCallback: (result: any) => void): void {
396+
if (!this._isExternallySampled) {
397+
this.underlyingExporter.export([], resultCallback);
398+
399+
return;
400+
}
401+
372402
const modifiedLogs = logs.map(this.transformLogRecord.bind(this));
373403

374404
this.underlyingExporter.export(modifiedLogs, resultCallback);
@@ -410,3 +440,40 @@ class ExternalLogRecordExporterWrapper {
410440
});
411441
}
412442
}
443+
444+
function isSpanInternalOnly(span: ReadableSpan): boolean {
445+
if (span.attributes[SemanticInternalAttributes.SPAN_PARTIAL]) {
446+
// Skip partial spans
447+
return true;
448+
}
449+
450+
const urlPath = span.attributes["url.path"];
451+
452+
if (typeof urlPath === "string" && urlPath === "/api/v1/usage/ingest") {
453+
return true;
454+
}
455+
456+
const httpUrl = span.attributes[SEMATTRS_HTTP_URL] ?? span.attributes["url.full"];
457+
458+
if (typeof httpUrl === "string" && httpUrl.includes("https://api.trigger.dev")) {
459+
return true;
460+
}
461+
462+
if (typeof httpUrl === "string" && httpUrl.includes("https://billing.trigger.dev")) {
463+
return true;
464+
}
465+
466+
if (typeof httpUrl === "string" && httpUrl.includes("https://cloud.trigger.dev")) {
467+
return true;
468+
}
469+
470+
if (typeof httpUrl === "string" && httpUrl.includes("https://engine.trigger.dev")) {
471+
return true;
472+
}
473+
474+
if (typeof httpUrl === "string" && httpUrl.includes("/api/v1/usage/ingest")) {
475+
return true;
476+
}
477+
478+
return false;
479+
}

packages/core/src/v3/traceContext/manager.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,12 @@ export class StandardTraceContextManager implements TraceContextManager {
3939
const spanContext = {
4040
traceId: externalTraceContext.traceId,
4141
spanId: currentSpanContext.spanId,
42-
traceFlags: TraceFlags.SAMPLED,
42+
traceFlags:
43+
typeof externalTraceContext.traceFlags === "string"
44+
? externalTraceContext.traceFlags === "01"
45+
? TraceFlags.SAMPLED
46+
: TraceFlags.NONE
47+
: TraceFlags.SAMPLED,
4348
isRemote: true,
4449
};
4550

@@ -60,7 +65,7 @@ function extractExternalTraceContext(traceContext: unknown) {
6065
: undefined;
6166

6267
if ("traceparent" in traceContext && typeof traceContext.traceparent === "string") {
63-
const [version, traceId, spanId] = traceContext.traceparent.split("-");
68+
const [version, traceId, spanId, traceFlags] = traceContext.traceparent.split("-");
6469

6570
if (!traceId || !spanId) {
6671
return undefined;
@@ -69,6 +74,7 @@ function extractExternalTraceContext(traceContext: unknown) {
6974
return {
7075
traceId,
7176
spanId,
77+
traceFlags,
7278
tracestate: tracestate,
7379
};
7480
}

packages/core/src/v3/traceContext/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export interface TraceContextManager {
88
| {
99
traceId: string;
1010
spanId: string;
11+
traceFlags?: string;
1112
tracestate?: string;
1213
}
1314
| undefined;

pnpm-lock.yaml

Lines changed: 34 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

references/d3-chat/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626
"@opentelemetry/api-logs": "^0.203.0",
2727
"@opentelemetry/exporter-logs-otlp-http": "0.203.0",
2828
"@opentelemetry/exporter-trace-otlp-http": "0.203.0",
29+
"@opentelemetry/instrumentation-http": "0.203.0",
30+
"@opentelemetry/instrumentation-undici": "0.14.0",
2931
"@opentelemetry/instrumentation": "^0.203.0",
3032
"@opentelemetry/sdk-logs": "^0.203.0",
3133
"@radix-ui/react-avatar": "^1.1.3",
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { registerOTel } from "@vercel/otel";
22

33
export function register() {
4-
registerOTel({ serviceName: "d3-chat" });
4+
registerOTel({ serviceName: "d3-chat", traceSampler: "traceidratio" });
55
}

references/d3-chat/trigger.config.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,14 @@ import { pythonExtension } from "@trigger.dev/python/extension";
33
import { installPlaywrightChromium } from "./src/extensions/playwright";
44
import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-http";
55
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
6+
import { HttpInstrumentation } from "@opentelemetry/instrumentation-http";
7+
import { UndiciInstrumentation } from "@opentelemetry/instrumentation-undici";
68

79
export default defineConfig({
810
project: process.env.TRIGGER_PROJECT_REF!,
911
dirs: ["./src/trigger"],
1012
telemetry: {
13+
instrumentations: [new HttpInstrumentation(), new UndiciInstrumentation()],
1114
logExporters: [
1215
new OTLPLogExporter({
1316
url: "https://api.axiom.co/v1/logs",

0 commit comments

Comments
 (0)