Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
5 changes: 5 additions & 0 deletions .changeset/violet-llamas-roll.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"trigger.dev": patch
---

fix: external traces now respect parent sampling, and prevent broken traces when there is no external trace context
3 changes: 2 additions & 1 deletion apps/webapp/app/runEngine/services/triggerTask.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -385,7 +385,8 @@ export class RunEngineTriggerTaskService {

const newExternalTraceparent = serializeTraceparent(
parsedTraceparent.traceId,
parentSpanId ?? parsedTraceparent.spanId
parentSpanId ?? parsedTraceparent.spanId,
parsedTraceparent.traceFlags
);

return {
Expand Down
10 changes: 5 additions & 5 deletions packages/core/src/v3/isomorphic/traceContext.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export function parseTraceparent(
traceparent?: string
): { traceId: string; spanId: string } | undefined {
): { traceId: string; spanId: string; traceFlags?: string } | undefined {
if (!traceparent) {
return undefined;
}
Expand All @@ -11,7 +11,7 @@ export function parseTraceparent(
return undefined;
}

const [version, traceId, spanId] = parts;
const [version, traceId, spanId, traceFlags] = parts;

if (version !== "00") {
return undefined;
Expand All @@ -21,9 +21,9 @@ export function parseTraceparent(
return undefined;
}

return { traceId, spanId };
return { traceId, spanId, traceFlags };
}

export function serializeTraceparent(traceId: string, spanId: string) {
return `00-${traceId}-${spanId}-01`;
export function serializeTraceparent(traceId: string, spanId: string, traceFlags?: string) {
return `00-${traceId}-${spanId}-${traceFlags ?? "01"}`;
}
83 changes: 75 additions & 8 deletions packages/core/src/v3/otel/tracingSDK.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
SimpleSpanProcessor,
SpanExporter,
} from "@opentelemetry/sdk-trace-node";
import { SemanticResourceAttributes } from "@opentelemetry/semantic-conventions";
import { SemanticResourceAttributes, SEMATTRS_HTTP_URL } from "@opentelemetry/semantic-conventions";
import { VERSION } from "../../version.js";
import {
OTEL_ATTRIBUTE_PER_EVENT_COUNT_LIMIT,
Expand Down Expand Up @@ -287,17 +287,27 @@
}

class ExternalSpanExporterWrapper {
private readonly _isExternallySampled: boolean;

constructor(
private underlyingExporter: SpanExporter,
private externalTraceId: string,
private externalTraceContext:
| { traceId: string; spanId: string; tracestate?: string }
| { traceId: string; spanId: string; tracestate?: string; traceFlags?: string }
| undefined
) {}
) {
this._isExternallySampled =
typeof externalTraceContext?.traceFlags === "string"
? externalTraceContext.traceFlags === "01"
: true;
}

private transformSpan(span: ReadableSpan): ReadableSpan | undefined {
if (span.attributes[SemanticInternalAttributes.SPAN_PARTIAL]) {
// Skip partial spans
if (!this._isExternallySampled) {
return;
}

if (isSpanInternalOnly(span)) {
return;
}

Expand Down Expand Up @@ -325,8 +335,15 @@
traceState: this.externalTraceContext.tracestate
? new TraceState(this.externalTraceContext.tracestate)
: undefined,
traceFlags: parentSpanContext?.traceFlags ?? 0,
traceFlags:
typeof this.externalTraceContext.traceFlags === "string"
? this.externalTraceContext.traceFlags === "01"
? 1
: 0
: parentSpanContext?.traceFlags ?? 0,
};
} else if (isAttemptSpan) {
parentSpanContext = undefined;
}

return {
Expand Down Expand Up @@ -360,15 +377,28 @@
}

class ExternalLogRecordExporterWrapper {
private readonly _isExternallySampled: boolean;

constructor(
private underlyingExporter: LogRecordExporter,
private externalTraceId: string,
private externalTraceContext:
| { traceId: string; spanId: string; tracestate?: string }
| { traceId: string; spanId: string; tracestate?: string; traceFlags?: string }
| undefined
) {}
) {
this._isExternallySampled =
typeof externalTraceContext?.traceFlags === "string"
? externalTraceContext.traceFlags === "01"
: true;
}

export(logs: any[], resultCallback: (result: any) => void): void {
if (!this._isExternallySampled) {
this.underlyingExporter.export([], resultCallback);

return;
}

const modifiedLogs = logs.map(this.transformLogRecord.bind(this));

this.underlyingExporter.export(modifiedLogs, resultCallback);
Expand Down Expand Up @@ -410,3 +440,40 @@
});
}
}

function isSpanInternalOnly(span: ReadableSpan): boolean {
if (span.attributes[SemanticInternalAttributes.SPAN_PARTIAL]) {
// Skip partial spans
return true;
}

const urlPath = span.attributes["url.path"];

if (typeof urlPath === "string" && urlPath === "/api/v1/usage/ingest") {
return true;
}

const httpUrl = span.attributes[SEMATTRS_HTTP_URL] ?? span.attributes["url.full"];

if (typeof httpUrl === "string" && httpUrl.includes("https://api.trigger.dev")) {
return true;
}

if (typeof httpUrl === "string" && httpUrl.includes("https://billing.trigger.dev")) {
return true;
}

if (typeof httpUrl === "string" && httpUrl.includes("https://cloud.trigger.dev")) {
return true;
}

if (typeof httpUrl === "string" && httpUrl.includes("https://engine.trigger.dev")) {
return true;
}

if (typeof httpUrl === "string" && httpUrl.includes("/api/v1/usage/ingest")) {
return true;
}

return false;
}
10 changes: 8 additions & 2 deletions packages/core/src/v3/traceContext/manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,12 @@ export class StandardTraceContextManager implements TraceContextManager {
const spanContext = {
traceId: externalTraceContext.traceId,
spanId: currentSpanContext.spanId,
traceFlags: TraceFlags.SAMPLED,
traceFlags:
typeof externalTraceContext.traceFlags === "string"
? externalTraceContext.traceFlags === "01"
? TraceFlags.SAMPLED
: TraceFlags.NONE
: TraceFlags.SAMPLED,
isRemote: true,
};

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

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

if (!traceId || !spanId) {
return undefined;
Expand All @@ -69,6 +74,7 @@ function extractExternalTraceContext(traceContext: unknown) {
return {
traceId,
spanId,
traceFlags,
tracestate: tracestate,
};
}
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/v3/traceContext/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export interface TraceContextManager {
| {
traceId: string;
spanId: string;
traceFlags?: string;
tracestate?: string;
}
| undefined;
Expand Down
34 changes: 34 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions references/d3-chat/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
"@opentelemetry/api-logs": "^0.203.0",
"@opentelemetry/exporter-logs-otlp-http": "0.203.0",
"@opentelemetry/exporter-trace-otlp-http": "0.203.0",
"@opentelemetry/instrumentation-http": "0.203.0",
"@opentelemetry/instrumentation-undici": "0.14.0",
"@opentelemetry/instrumentation": "^0.203.0",
"@opentelemetry/sdk-logs": "^0.203.0",
"@radix-ui/react-avatar": "^1.1.3",
Expand Down
2 changes: 1 addition & 1 deletion references/d3-chat/src/instrumentation.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { registerOTel } from "@vercel/otel";

export function register() {
registerOTel({ serviceName: "d3-chat" });
registerOTel({ serviceName: "d3-chat", traceSampler: "traceidratio" });
}
3 changes: 3 additions & 0 deletions references/d3-chat/trigger.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@ import { pythonExtension } from "@trigger.dev/python/extension";
import { installPlaywrightChromium } from "./src/extensions/playwright";
import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-http";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
import { HttpInstrumentation } from "@opentelemetry/instrumentation-http";
import { UndiciInstrumentation } from "@opentelemetry/instrumentation-undici";

export default defineConfig({
project: process.env.TRIGGER_PROJECT_REF!,
dirs: ["./src/trigger"],
telemetry: {
instrumentations: [new HttpInstrumentation(), new UndiciInstrumentation()],
logExporters: [
new OTLPLogExporter({
url: "https://api.axiom.co/v1/logs",
Expand Down
Loading