Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -156,4 +156,4 @@ packages/**/version.ts
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
*.code-workspace
*.code-workspace
4 changes: 2 additions & 2 deletions packages/durabletask-js/src/tracing/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,11 +78,11 @@ export function createSpanName(type: string, name: string, version?: string): st
/**
* Creates a timer span name following the Durable Task naming convention.
*
* Format: "timer:{orchName}"
* Format: "orchestration:{orchName}:timer"
*
* @param orchestrationName - The name of the parent orchestration.
* @returns The formatted timer span name.
*/
export function createTimerSpanName(orchestrationName: string): string {
return `${TaskType.TIMER}:${orchestrationName}`;
return `${TaskType.ORCHESTRATION}:${orchestrationName}:${TaskType.TIMER}`;
}
1 change: 1 addition & 0 deletions packages/durabletask-js/src/tracing/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export {
createPbTraceContext,
extractTraceparentFromSpan,
createParentContextFromPb,
createPbTraceContextFromSpan,
} from "./trace-context-utils";

// Internal-only exports (not re-exported from package index.ts):
Expand Down
30 changes: 30 additions & 0 deletions packages/durabletask-js/src/tracing/trace-context-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,36 @@ export function extractTraceparentFromSpan(span: Span | undefined | null): { tra
return { traceparent, tracestate };
}

/**
* Creates a protobuf TraceContext directly from a Span, avoiding the
* format→parse roundtrip of extractTraceparentFromSpan + createPbTraceContext.
* Returns undefined if the span context is not valid.
*/
export function createPbTraceContextFromSpan(span: Span | undefined | null): pb.TraceContext | undefined {
const otel = getOtelApi();
if (!otel || !span) return undefined;

const spanContext = span.spanContext();
if (!otel.isSpanContextValid(spanContext)) {
return undefined;
}

const traceparent = createTraceparent(spanContext.traceId, spanContext.spanId, spanContext.traceFlags);

const ctx = new pb.TraceContext();
ctx.setTraceparent(traceparent);
ctx.setSpanid(spanContext.spanId);

const tracestate = spanContext.traceState?.serialize();
if (tracestate) {
const sv = new StringValue();
sv.setValue(tracestate);
ctx.setTracestate(sv);
}

return ctx;
}

/**
* Creates an OTEL Context with a remote parent span from a protobuf TraceContext.
* Returns undefined if OTEL is not available or the pbTraceContext is not provided.
Expand Down
134 changes: 71 additions & 63 deletions packages/durabletask-js/src/tracing/trace-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,39 @@ import { Timestamp } from "google-protobuf/google/protobuf/timestamp_pb";
import { TRACER_NAME, DurableTaskAttributes, TaskType, createSpanName, createTimerSpanName } from "./constants";
import {
getOtelApi,
extractTraceparentFromSpan,
createPbTraceContext,
createPbTraceContextFromSpan,
createParentContextFromPb,
} from "./trace-context-utils";
import type { Span, Tracer } from "@opentelemetry/api";

// Cached tracer instance to avoid repeated lookups. The tracer is created once
// and reused for the lifetime of the process. This is safe because the OTEL JS
// SDK returns a proxy tracer from `trace.getTracer()` that dynamically delegates
// to the current global tracer provider, so provider swaps (e.g., in tests via
// `setGlobalTracerProvider`) are handled transparently.
let _cachedTracer: Tracer | undefined;

/**
* Returns the OTEL API and tracer, or undefined if OTEL is not available.
* Caches the tracer instance for efficiency.
*/
function getTracingContext(): { otel: typeof import("@opentelemetry/api"); tracer: Tracer } | undefined {
const otel = getOtelApi();
if (!otel) return undefined;

if (!_cachedTracer) {
_cachedTracer = otel.trace.getTracer(TRACER_NAME);
}

return { otel, tracer: _cachedTracer };
}

/**
* Gets the Durable Task tracer from the OpenTelemetry API.
* Returns undefined if OpenTelemetry is not installed.
*/
export function getTracer(): Tracer | undefined {
const otel = getOtelApi();
if (!otel) return undefined;
return otel.trace.getTracer(TRACER_NAME);
return getTracingContext()?.tracer;
}

/**
Expand Down Expand Up @@ -50,29 +69,27 @@ export interface OrchestrationSpanInfo {
* @returns The span (or undefined if OTEL is not available). Caller must end it.
*/
export function startSpanForNewOrchestration(req: pb.CreateInstanceRequest): Span | undefined {
const otel = getOtelApi();
const tracer = getTracer();
if (!otel || !tracer) return undefined;
const ctx = getTracingContext();
if (!ctx) return undefined;

const name = req.getName();
const version = req.getVersion()?.getValue();
const instanceId = req.getInstanceid();
const spanName = createSpanName(TaskType.CREATE_ORCHESTRATION, name, version);

const span = tracer.startSpan(spanName, {
kind: otel.SpanKind.PRODUCER,
const span = ctx.tracer.startSpan(spanName, {
kind: ctx.otel.SpanKind.PRODUCER,
attributes: {
[DurableTaskAttributes.TYPE]: TaskType.CREATE_ORCHESTRATION,
[DurableTaskAttributes.TYPE]: TaskType.ORCHESTRATION,
[DurableTaskAttributes.TASK_NAME]: name,
Comment thread
torosent marked this conversation as resolved.
[DurableTaskAttributes.TASK_INSTANCE_ID]: instanceId,
...(version ? { [DurableTaskAttributes.TASK_VERSION]: version } : {}),
},
});

// Inject trace context into the proto request
const traceInfo = extractTraceparentFromSpan(span);
if (traceInfo) {
const pbCtx = createPbTraceContext(traceInfo.traceparent, traceInfo.tracestate);
const pbCtx = createPbTraceContextFromSpan(span);
if (pbCtx) {
req.setParenttracecontext(pbCtx);
}

Expand All @@ -93,9 +110,8 @@ export function startSpanForOrchestrationExecution(
orchestrationTraceContext: pb.OrchestrationTraceContext | undefined,
instanceId: string,
): { span: Span; spanInfo: OrchestrationSpanInfo } | undefined {
const otel = getOtelApi();
const tracer = getTracer();
if (!otel || !tracer) return undefined;
const ctx = getTracingContext();
if (!ctx) return undefined;

const name = executionStartedEvent.getName();
const version = executionStartedEvent.getVersion()?.getValue();
Expand All @@ -118,10 +134,10 @@ export function startSpanForOrchestrationExecution(
// first-execution time for storage in OrchestrationTraceContext.
const persistedStartTime = isReplay ? existingStartTime! : spanStartTime;

const span = tracer.startSpan(
const span = ctx.tracer.startSpan(
spanName,
{
kind: otel.SpanKind.SERVER,
kind: ctx.otel.SpanKind.SERVER,
startTime: spanStartTime,
attributes: {
[DurableTaskAttributes.TYPE]: TaskType.ORCHESTRATION,
Expand Down Expand Up @@ -168,21 +184,20 @@ export function startSpanForSchedulingTask(
action: pb.ScheduleTaskAction,
taskId: number,
): void {
const otel = getOtelApi();
const tracer = getTracer();
if (!otel || !tracer) return;
const ctx = getTracingContext();
if (!ctx) return;

const name = action.getName();
const version = action.getVersion()?.getValue();
const spanName = createSpanName(TaskType.ACTIVITY, name, version);

// Create a context with the orchestration span as parent
const parentContext = otel.trace.setSpan(otel.context.active(), orchestrationSpan);
const parentContext = ctx.otel.trace.setSpan(ctx.otel.context.active(), orchestrationSpan);

const span = tracer.startSpan(
const span = ctx.tracer.startSpan(
spanName,
{
kind: otel.SpanKind.CLIENT,
kind: ctx.otel.SpanKind.CLIENT,
attributes: {
[DurableTaskAttributes.TYPE]: TaskType.ACTIVITY,
[DurableTaskAttributes.TASK_NAME]: name,
Expand All @@ -194,9 +209,8 @@ export function startSpanForSchedulingTask(
);

// Inject trace context into the action
const traceInfo = extractTraceparentFromSpan(span);
if (traceInfo) {
const pbCtx = createPbTraceContext(traceInfo.traceparent, traceInfo.tracestate);
const pbCtx = createPbTraceContextFromSpan(span);
if (pbCtx) {
action.setParenttracecontext(pbCtx);
}

Expand All @@ -211,9 +225,8 @@ export function startSpanForSchedulingTask(
* @returns The span (or undefined if OTEL is not available). Caller must end it.
*/
export function startSpanForTaskExecution(req: pb.ActivityRequest): Span | undefined {
const otel = getOtelApi();
const tracer = getTracer();
if (!otel || !tracer) return undefined;
const ctx = getTracingContext();
if (!ctx) return undefined;

const name = req.getName();
const spanName = createSpanName(TaskType.ACTIVITY, name);
Expand All @@ -223,10 +236,10 @@ export function startSpanForTaskExecution(req: pb.ActivityRequest): Span | undef

const instanceId = req.getOrchestrationinstance()?.getInstanceid() ?? "";

const span = tracer.startSpan(
const span = ctx.tracer.startSpan(
spanName,
{
kind: otel.SpanKind.SERVER,
kind: ctx.otel.SpanKind.SERVER,
attributes: {
[DurableTaskAttributes.TYPE]: TaskType.ACTIVITY,
[DurableTaskAttributes.TASK_NAME]: name,
Expand All @@ -253,23 +266,22 @@ export function startSpanForSchedulingSubOrchestration(
action: pb.CreateSubOrchestrationAction,
taskId: number,
): void {
const otel = getOtelApi();
const tracer = getTracer();
if (!otel || !tracer) return;
const ctx = getTracingContext();
if (!ctx) return;

const name = action.getName();
const version = action.getVersion()?.getValue();
const instanceId = action.getInstanceid();
const spanName = createSpanName(TaskType.CREATE_ORCHESTRATION, name, version);
const spanName = createSpanName(TaskType.ORCHESTRATION, name, version);

const parentContext = otel.trace.setSpan(otel.context.active(), orchestrationSpan);
const parentContext = ctx.otel.trace.setSpan(ctx.otel.context.active(), orchestrationSpan);

const span = tracer.startSpan(
const span = ctx.tracer.startSpan(
spanName,
{
kind: otel.SpanKind.CLIENT,
kind: ctx.otel.SpanKind.CLIENT,
attributes: {
[DurableTaskAttributes.TYPE]: TaskType.CREATE_ORCHESTRATION,
[DurableTaskAttributes.TYPE]: TaskType.ORCHESTRATION,
[DurableTaskAttributes.TASK_NAME]: name,
[DurableTaskAttributes.TASK_INSTANCE_ID]: instanceId,
[DurableTaskAttributes.TASK_TASK_ID]: taskId,
Expand All @@ -280,9 +292,8 @@ export function startSpanForSchedulingSubOrchestration(
);

// Inject trace context into the action
const traceInfo = extractTraceparentFromSpan(span);
if (traceInfo) {
const pbCtx = createPbTraceContext(traceInfo.traceparent, traceInfo.tracestate);
const pbCtx = createPbTraceContextFromSpan(span);
if (pbCtx) {
action.setParenttracecontext(pbCtx);
}

Expand All @@ -304,17 +315,16 @@ export function emitSpanForTimer(
fireAt: Date,
timerId: number,
): void {
const otel = getOtelApi();
const tracer = getTracer();
if (!otel || !tracer) return;
const ctx = getTracingContext();
if (!ctx) return;

const spanName = createTimerSpanName(orchestrationName);
const parentContext = otel.trace.setSpan(otel.context.active(), orchestrationSpan);
const parentContext = ctx.otel.trace.setSpan(ctx.otel.context.active(), orchestrationSpan);

const span = tracer.startSpan(
const span = ctx.tracer.startSpan(
spanName,
{
kind: otel.SpanKind.INTERNAL,
kind: ctx.otel.SpanKind.INTERNAL,
attributes: {
[DurableTaskAttributes.TYPE]: TaskType.TIMER,
[DurableTaskAttributes.TASK_TASK_ID]: timerId,
Expand All @@ -339,19 +349,18 @@ export function emitSpanForEventSent(
eventName: string,
targetInstanceId?: string,
): void {
const otel = getOtelApi();
const tracer = getTracer();
if (!otel || !tracer) return;
const ctx = getTracingContext();
if (!ctx) return;

const spanName = createSpanName(TaskType.ORCHESTRATION_EVENT, eventName);
const parentContext = otel.trace.setSpan(otel.context.active(), orchestrationSpan);
const parentContext = ctx.otel.trace.setSpan(ctx.otel.context.active(), orchestrationSpan);

const span = tracer.startSpan(
const span = ctx.tracer.startSpan(
spanName,
{
kind: otel.SpanKind.PRODUCER,
kind: ctx.otel.SpanKind.PRODUCER,
attributes: {
[DurableTaskAttributes.TYPE]: TaskType.ORCHESTRATION_EVENT,
[DurableTaskAttributes.TYPE]: TaskType.EVENT,
[DurableTaskAttributes.TASK_NAME]: eventName,
...(targetInstanceId ? { [DurableTaskAttributes.EVENT_TARGET_INSTANCE_ID]: targetInstanceId } : {}),
},
Expand All @@ -370,16 +379,15 @@ export function emitSpanForEventSent(
* @returns The span (or undefined if OTEL is not available). Caller must end it.
*/
export function startSpanForEventRaisedFromClient(eventName: string, instanceId: string): Span | undefined {
const otel = getOtelApi();
const tracer = getTracer();
if (!otel || !tracer) return undefined;
const ctx = getTracingContext();
if (!ctx) return undefined;

const spanName = createSpanName(TaskType.ORCHESTRATION_EVENT, eventName);

const span = tracer.startSpan(spanName, {
kind: otel.SpanKind.PRODUCER,
const span = ctx.tracer.startSpan(spanName, {
kind: ctx.otel.SpanKind.PRODUCER,
attributes: {
[DurableTaskAttributes.TYPE]: TaskType.ORCHESTRATION_EVENT,
[DurableTaskAttributes.TYPE]: TaskType.EVENT,
[DurableTaskAttributes.TASK_NAME]: eventName,
[DurableTaskAttributes.EVENT_TARGET_INSTANCE_ID]: instanceId,
},
Expand Down
Loading
Loading