Skip to content

Commit c82b885

Browse files
authored
ENG-2239: Respect suppressed option telemetry (#20)
* ENG-2239: respect suppressed option telemetry * docs: use Photon Project identity * test: translate Developer telemetry fixture
1 parent f837922 commit c82b885

4 files changed

Lines changed: 113 additions & 28 deletions

File tree

docs/guides/option-runtime.mdx

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,16 @@ Calling it before `setupOtel()` is a configuration error.
3535
```ts
3636
const logger = option.createLogger("@photon-ai/developer-logs");
3737

38-
await option.withSpan("project.generate", { "project.id": projectId }, async () => {
39-
logger.emit({
40-
body: "starting project generation",
41-
eventName: "developer.message",
42-
});
43-
});
38+
await option.withSpan(
39+
"project.generate",
40+
{ "photon.project.id": projectId },
41+
async () => {
42+
logger.emit({
43+
body: "starting project generation",
44+
eventName: "developer.message",
45+
});
46+
}
47+
);
4448
```
4549

4650
Success leaves the Span status `UNSET`. If the callback throws, the runtime

docs/reference/api.mdx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,11 @@ interface OptionOtelHandle {
119119
inject(headers: Headers, captured?: Context): void;
120120
run<T>(captured: Context, fn: () => T): T;
121121
};
122+
withActiveSpan<T>(
123+
name: string,
124+
options: SpanOptions & { parentContext?: Context },
125+
fn: (span: Span) => Promise<T> | T,
126+
): Promise<T>;
122127
withSpan<T>(name: string, fn: () => Promise<T> | T): Promise<T>;
123128
withSpan<T>(
124129
name: string,
@@ -129,6 +134,11 @@ interface OptionOtelHandle {
129134
}
130135
```
131136

137+
`withActiveSpan()` is the instrumentation-level form. It accepts standard
138+
OpenTelemetry `SpanOptions`, an optional explicit parent Context, and exposes
139+
the active Span to the callback. A suppressed parent produces no recording
140+
Span and does not make `hasActiveSpan()` true; the business callback still runs.
141+
132142
The propagation helper uses the fixed internal
133143
`photon-developer-traceparent` header and never changes the standard
134144
`traceparent`.

src/option-runtime.ts

Lines changed: 39 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@ import {
55
defaultTextMapGetter,
66
defaultTextMapSetter,
77
diag,
8+
INVALID_SPAN_CONTEXT,
89
ROOT_CONTEXT,
910
type Span,
11+
type SpanOptions,
1012
SpanStatusCode,
1113
trace,
1214
} from "@opentelemetry/api";
@@ -52,7 +54,7 @@ export interface OptionOtelHandle {
5254
): {
5355
emit: (record: Omit<LogRecord, "context">) => void;
5456
};
55-
/** True only for a local Span created by this runtime, not a remote parent. */
57+
/** True only for a recording local Span, not a remote or suppressed parent. */
5658
hasActiveSpan(): boolean;
5759
readonly propagation: {
5860
/** Capture the current context so delayed iterators can re-enter it. */
@@ -65,6 +67,12 @@ export interface OptionOtelHandle {
6567
run: <T>(captured: Context, fn: () => T) => T;
6668
};
6769
shutdown(): Promise<void>;
70+
/** Run a callback with a Span active only in this runtime's private Context. */
71+
withActiveSpan<T>(
72+
name: string,
73+
options: SpanOptions & { parentContext?: Context },
74+
fn: (span: Span) => Promise<T> | T
75+
): Promise<T>;
6876
withSpan<T>(name: string, fn: () => Promise<T> | T): Promise<T>;
6977
withSpan<T>(
7078
name: string,
@@ -186,30 +194,28 @@ export const createOptionOtelRuntime = (
186194
run: (captured, fn) => contextManager.with(captured, fn),
187195
};
188196

189-
const withSpan = <T>(
190-
name: string,
191-
attributesOrFn: Attributes | (() => Promise<T> | T),
192-
maybeFn?: () => Promise<T> | T
193-
): Promise<T> => {
194-
const fn = typeof attributesOrFn === "function" ? attributesOrFn : maybeFn;
195-
if (!fn) {
196-
throw new Error("withSpan: function argument is required");
197-
}
198-
const attributes =
199-
typeof attributesOrFn === "function" ? undefined : attributesOrFn;
200-
const parent = contextManager.active();
197+
const withActiveSpan: OptionOtelHandle["withActiveSpan"] = async (
198+
name,
199+
options,
200+
fn
201+
) => {
202+
const { parentContext, ...spanOptions } = options;
203+
const parent = parentContext ?? contextManager.active();
201204
let span: Span;
202205
try {
203-
span = tracer.startSpan(name, { attributes }, parent);
206+
span = tracer.startSpan(name, spanOptions, parent);
204207
} catch (error) {
205208
reportDiagnostic("failed to start Span", error);
206-
return Promise.resolve().then(fn);
209+
return await fn(trace.wrapSpanContext(INVALID_SPAN_CONTEXT));
207210
}
208211

209-
const active = trace.setSpan(parent, span).setValue(localSpanKey, span);
210-
return contextManager.with(active, async () => {
212+
const spanContext = trace.setSpan(parent, span);
213+
const active = span.isRecording()
214+
? spanContext.setValue(localSpanKey, span)
215+
: spanContext;
216+
return await contextManager.with(active, async () => {
211217
try {
212-
return await fn();
218+
return await fn(span);
213219
} catch (error) {
214220
try {
215221
span.recordException(error instanceof Error ? error : String(error));
@@ -232,6 +238,20 @@ export const createOptionOtelRuntime = (
232238
});
233239
};
234240

241+
const withSpan = <T>(
242+
name: string,
243+
attributesOrFn: Attributes | (() => Promise<T> | T),
244+
maybeFn?: () => Promise<T> | T
245+
): Promise<T> => {
246+
const fn = typeof attributesOrFn === "function" ? attributesOrFn : maybeFn;
247+
if (!fn) {
248+
throw new Error("withSpan: function argument is required");
249+
}
250+
const attributes =
251+
typeof attributesOrFn === "function" ? undefined : attributesOrFn;
252+
return withActiveSpan(name, { attributes }, () => fn());
253+
};
254+
235255
return {
236256
createLogger(name, version) {
237257
const logger: Logger = loggerProvider.getLogger(name, version);
@@ -260,6 +280,7 @@ export const createOptionOtelRuntime = (
260280
}
261281
return shutdownPromise;
262282
},
283+
withActiveSpan,
263284
withSpan: withSpan as OptionOtelHandle["withSpan"],
264285
};
265286
};

tests/option-runtime.test.ts

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
1-
import { SpanStatusCode, trace } from "@opentelemetry/api";
1+
import {
2+
ROOT_CONTEXT,
3+
SpanKind,
4+
SpanStatusCode,
5+
trace,
6+
} from "@opentelemetry/api";
27
import { SeverityNumber } from "@opentelemetry/api-logs";
8+
import { suppressTracing } from "@opentelemetry/core";
39
import { resourceFromAttributes } from "@opentelemetry/resources";
410
import {
511
InMemoryLogRecordExporter,
@@ -155,22 +161,66 @@ describe("option runtime", () => {
155161
await runtime.shutdown();
156162
});
157163

164+
it("supports a private active SERVER root with full Span options", async () => {
165+
const { runtime, spanExporter } = createRuntime();
166+
let callbackSpanId = "";
167+
168+
await runtime.withActiveSpan(
169+
"developer.http",
170+
{
171+
attributes: { "photon.api_key.id": "pho_sk_test" },
172+
kind: SpanKind.SERVER,
173+
parentContext: ROOT_CONTEXT,
174+
},
175+
(span) => {
176+
callbackSpanId = span.spanContext().spanId;
177+
expect(runtime.hasActiveSpan()).toBe(true);
178+
}
179+
);
180+
181+
const [server] = spanExporter.getFinishedSpans();
182+
expect(server?.kind).toBe(SpanKind.SERVER);
183+
expect(server?.parentSpanContext).toBeUndefined();
184+
expect(server?.attributes["photon.api_key.id"]).toBe("pho_sk_test");
185+
expect(callbackSpanId).toBe(server?.spanContext().spanId);
186+
await runtime.shutdown();
187+
});
188+
189+
it("does not activate or export spans when tracing is suppressed", async () => {
190+
const { runtime, spanExporter } = createRuntime();
191+
let callbackRan = false;
192+
const headers = new Headers();
193+
194+
await runtime.propagation.run(suppressTracing(ROOT_CONTEXT), () =>
195+
runtime.withSpan("suppressed", () => {
196+
callbackRan = true;
197+
expect(runtime.hasActiveSpan()).toBe(false);
198+
runtime.propagation.inject(headers);
199+
})
200+
);
201+
202+
expect(callbackRan).toBe(true);
203+
expect(headers.has(TRACEPARENT_HEADER)).toBe(false);
204+
expect(spanExporter.getFinishedSpans()).toEqual([]);
205+
await runtime.shutdown();
206+
});
207+
158208
it("associates logs with the active local span and inherited Resource", async () => {
159209
const { logExporter, runtime } = createRuntime("projects-service");
160210
const logger = runtime.createLogger("@photon-ai/developer-logs");
161211

162212
await runtime.withSpan("project.generate", () => {
163213
logger.emit({
164-
attributes: { "project.id": "proj_123" },
165-
body: "开始生成项目",
214+
attributes: { "photon.project.id": "pho_prj_123" },
215+
body: "Starting project generation",
166216
eventName: "developer.message",
167217
severityNumber: SeverityNumber.INFO,
168218
severityText: "INFO",
169219
});
170220
});
171221

172222
const [record] = logExporter.getFinishedLogRecords();
173-
expect(record?.body).toBe("开始生成项目");
223+
expect(record?.body).toBe("Starting project generation");
174224
expect(record?.instrumentationScope.name).toBe("@photon-ai/developer-logs");
175225
expect(record?.spanContext?.traceId).toMatch(TRACE_ID_PATTERN);
176226
expect(record?.spanContext?.spanId).toMatch(SPAN_ID_PATTERN);

0 commit comments

Comments
 (0)