Skip to content

Commit 911aada

Browse files
Merge pull request #7348 from cloudflare/jmorrell/add-span-status
[WO-1581] Enable span status and renaming
2 parents 84338b9 + 1f6faa7 commit 911aada

20 files changed

Lines changed: 717 additions & 9 deletions

‎src/cloudflare/internal/test/instrumentation-test-helper.js‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export function createInstrumentationState() {
2323
invocationPromises: [],
2424
invocations: new Map(),
2525
spans: new Map(),
26+
spanUpdates: [],
2627
};
2728
}
2829

@@ -85,6 +86,10 @@ export function createTailStreamHandler(state) {
8586
state.spans.set(spanKey, span);
8687
break;
8788
}
89+
case 'spanUpdate': {
90+
state.spanUpdates.push(event);
91+
break;
92+
}
8893
case 'outcome':
8994
invocation.outcome = event.event;
9095
resolveFn();
@@ -259,6 +264,7 @@ export function createTailStreamCollector() {
259264
const tailStream = createTailStreamHandler(state);
260265

261266
const spans = state.spans;
267+
const spanUpdates = state.spanUpdates;
262268
const invocations = state.invocations;
263269
const invocationPromises = state.invocationPromises;
264270
const waitForCompletion = () => {
@@ -270,6 +276,7 @@ export function createTailStreamCollector() {
270276
waitForCompletion,
271277
invocations,
272278
spans,
279+
spanUpdates,
273280
};
274281
}
275282

‎src/cloudflare/internal/test/tracing/tracing-helpers-instrumentation-test.js‎

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,14 @@ export const validateSpans = {
2222
const allSpans = collector.spans.values();
2323
const spansByTest = groupSpansBy(allSpans, 'test');
2424
const invocations = [...collector.invocations.values()];
25+
const truncatedUpdatedName = `updated-${'x'.repeat(56)}`;
26+
const spanUpdatesFor = (spanKey) =>
27+
collector.spanUpdates
28+
.filter(
29+
(update) =>
30+
`${update.invocationId}#${update.spanContext.spanId}` === spanKey
31+
)
32+
.map((update) => update.event.info);
2533
const rootAttributes = invocations.flatMap(
2634
(invocation) => invocation.attributes
2735
);
@@ -39,6 +47,8 @@ export const validateSpans = {
3947
expectedSpan: 'undefined-attr-op',
4048
},
4149
{ test: 'setAttributes', expectedSpan: 'set-attributes-op' },
50+
{ test: 'setStatus', expectedSpan: 'status-error-op' },
51+
{ test: 'updateName', expectedSpan: 'update-name-original' },
4252
{ test: 'publicImportTracing', expectedSpan: 'public-import-op' },
4353
{
4454
test: 'publicImportStartActiveSpan',
@@ -66,6 +76,66 @@ export const validateSpans = {
6676
assert(span.closed, `${test}: Span '${expectedSpan}' should be closed`);
6777
}
6878

79+
{
80+
const statusSpans = spansByTest.get('setStatus') || [];
81+
const errorSpan = statusSpans.find((s) => s.name === 'status-error-op');
82+
const okSpan = statusSpans.find((s) => s.name === 'status-ok-op');
83+
84+
const entries = [...collector.spans.entries()];
85+
const errorKey = entries.find(([, span]) => span === errorSpan)[0];
86+
const okKey = entries.find(([, span]) => span === okSpan)[0];
87+
assert.deepStrictEqual(spanUpdatesFor(errorKey), [
88+
{
89+
type: 'status',
90+
status: { code: 'error', message: 'first error' },
91+
},
92+
{
93+
type: 'status',
94+
status: { code: 'error', message: 'second error' },
95+
},
96+
{ type: 'status', status: { code: 'unset' } },
97+
]);
98+
assert.deepStrictEqual(spanUpdatesFor(okKey), [
99+
{
100+
type: 'status',
101+
status: { code: 'error', message: 'temporary error' },
102+
},
103+
{ type: 'status', status: { code: 'ok' } },
104+
{
105+
type: 'status',
106+
status: { code: 'error', message: 'error after ok' },
107+
},
108+
]);
109+
}
110+
111+
{
112+
const [[spanKey]] = [...collector.spans.entries()].filter(
113+
([, span]) => span.test === 'updateName'
114+
);
115+
assert.deepStrictEqual(spanUpdatesFor(spanKey), [
116+
{ type: 'name', name: 'update-name-intermediate' },
117+
{ type: 'name', name: truncatedUpdatedName },
118+
]);
119+
}
120+
121+
{
122+
const invocation = invocations.find((candidate) =>
123+
candidate.attributes.some(
124+
({ name, value }) =>
125+
name === 'test' && value === 'updateInvocationSpan'
126+
)
127+
);
128+
assert(invocation, 'updateInvocationSpan: invocation present');
129+
const spanKey = `${invocation.invocationId}#${invocation.rootSpanId}`;
130+
assert.deepStrictEqual(spanUpdatesFor(spanKey), [
131+
{ type: 'name', name: 'updated-invocation' },
132+
{
133+
type: 'status',
134+
status: { code: 'error', message: 'invocation error' },
135+
},
136+
]);
137+
}
138+
69139
// setAttributeUndefined should NOT have a 'skipped' attribute recorded.
70140
{
71141
const span = (spansByTest.get('setAttributeUndefined') || []).find(

‎src/cloudflare/internal/test/tracing/tracing-helpers-test.js‎

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,53 @@ export const setAttributes = {
224224
},
225225
};
226226

227+
export const setStatus = {
228+
async test(ctrl, env, ctx) {
229+
const errorSpan = publicTracing.startSpan('status-error-op');
230+
errorSpan.setAttribute('test', 'setStatus');
231+
assert.strictEqual(
232+
errorSpan.setStatus({ code: 'error', message: 'first error' }),
233+
errorSpan
234+
);
235+
errorSpan.setStatus({ code: 'error', message: 'second error' });
236+
errorSpan.setStatus({ code: 'unset' });
237+
errorSpan.end();
238+
// All span mutations are no-ops after end().
239+
errorSpan.setStatus({ code: 'ok' });
240+
241+
const okSpan = publicTracing.startSpan('status-ok-op');
242+
okSpan.setAttribute('test', 'setStatus');
243+
okSpan.setStatus({ code: 'error', message: 'temporary error' });
244+
okSpan.setStatus({ code: 'ok', message: 'not retained' });
245+
okSpan.setStatus({ code: 'error', message: 'error after ok' });
246+
okSpan.end();
247+
},
248+
};
249+
250+
export const updateName = {
251+
async test() {
252+
const span = publicTracing.startSpan('update-name-original');
253+
span.setAttribute('test', 'updateName');
254+
assert.strictEqual(span.updateName('update-name-intermediate'), span);
255+
span.updateName(`updated-${'x'.repeat(100)}`);
256+
span.end();
257+
span.updateName('update-name-after-end');
258+
},
259+
};
260+
261+
export const updateInvocationSpan = {
262+
async test() {
263+
const span = publicTracing.getActiveSpan();
264+
assert(span);
265+
span.setAttribute('test', 'updateInvocationSpan');
266+
assert.strictEqual(span.updateName('updated-invocation'), span);
267+
assert.strictEqual(
268+
span.setStatus({ code: 'error', message: 'invocation error' }),
269+
span
270+
);
271+
},
272+
};
273+
227274
// Verify that nested withSpan calls produce correctly nested spans. This exercises the
228275
// AsyncContextFrame push path in enterSpan: the inner span should be parented on the
229276
// outer span.

‎src/cloudflare/internal/tracing.d.ts‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,14 @@ interface ExceptionWithName {
3333
type Exception =
3434
ExceptionWithCode | ExceptionWithMessage | ExceptionWithName | string;
3535

36+
type TracingSpanStatusCode = 'unset' | 'ok' | 'error';
37+
38+
interface TracingSpanStatus {
39+
code: TracingSpanStatusCode;
40+
/** A developer-facing error message. Ignored unless code is "error". */
41+
message?: string;
42+
}
43+
3644
declare class Span {
3745
// Returns true if this span will be recorded to the tracing system. False when the
3846
// current async context is not being traced, or when the span has already been submitted.
@@ -48,6 +56,13 @@ declare class Span {
4856
// Records an exception event on the span. Calls after the span has ended are ignored.
4957
recordException(exception: Exception): void;
5058

59+
// Changes the span name. Calls after the span has ended are ignored.
60+
updateName(name: string): this;
61+
62+
// Sets the span status. Calls after the span has ended are ignored. Messages are retained only
63+
// for errors.
64+
setStatus(status: TracingSpanStatus): this;
65+
5166
// Ends the span and submits its attributes to the tracing system. Idempotent.
5267
end(): void;
5368
}

‎src/workerd/api/tracing.c++‎

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,14 @@ class UserSpanState final: public SpanState {
125125
return workerd::SpanParent(builder);
126126
}
127127

128+
void updateName(kj::ConstString operationName) override {
129+
builder.setOperationName(kj::mv(operationName));
130+
}
131+
132+
void setStatus(tracing::SpanStatus status) override {
133+
builder.setStatus(kj::mv(status));
134+
}
135+
128136
protected:
129137
bool canRecordAttributes() override {
130138
return builder.isObserved();
@@ -199,6 +207,27 @@ class InvocationSpanState final: public SpanState {
199207
return parent.addRef();
200208
}
201209

210+
void updateName(kj::ConstString operationName) override {
211+
KJ_IF_SOME(valueContext, context) {
212+
KJ_IF_SOME(valueTracer, tracer) {
213+
valueTracer->runIfAlive([&](BaseTracer& tracer) {
214+
tracer.addSpanUpdate(
215+
valueContext.getSpanId(), tracing::SpanUpdate(kj::mv(operationName)));
216+
});
217+
}
218+
}
219+
}
220+
221+
void setStatus(tracing::SpanStatus status) override {
222+
KJ_IF_SOME(valueContext, context) {
223+
KJ_IF_SOME(valueTracer, tracer) {
224+
valueTracer->runIfAlive([&](BaseTracer& tracer) {
225+
tracer.addSpanUpdate(valueContext.getSpanId(), tracing::SpanUpdate(kj::mv(status)));
226+
});
227+
}
228+
}
229+
}
230+
202231
protected:
203232
bool canRecordAttributes() override {
204233
return getIsTraced();
@@ -260,6 +289,10 @@ class NoopSpanState final: public SpanState {
260289
return workerd::SpanParent(nullptr);
261290
}
262291

292+
void updateName(kj::ConstString) override {}
293+
294+
void setStatus(tracing::SpanStatus) override {}
295+
263296
protected:
264297
bool canRecordAttributes() override {
265298
return false;
@@ -365,6 +398,55 @@ void Span::recordException(
365398
}
366399
}
367400

401+
jsg::Ref<Span> Span::updateName(jsg::Lock& js, kj::String operationName) {
402+
if (operationName.size() > MAX_USER_OPERATION_NAME_BYTES) {
403+
operationName = kj::str(operationName.first(MAX_USER_OPERATION_NAME_BYTES));
404+
}
405+
auto name = kj::ConstString(kj::mv(operationName));
406+
KJ_SWITCH_ONEOF(state) {
407+
KJ_CASE_ONEOF(s, kj::Own<SpanState>) {
408+
s->updateName(kj::mv(name));
409+
}
410+
KJ_CASE_ONEOF(s, IoOwn<SpanState>) {
411+
s->updateName(kj::mv(name));
412+
}
413+
}
414+
return JSG_THIS;
415+
}
416+
417+
jsg::Ref<Span> Span::setStatus(jsg::Lock& js, TracingSpanStatus status) {
418+
tracing::SpanStatusCode code;
419+
if (status.code == "unset") {
420+
code = tracing::SpanStatusCode::UNSET;
421+
} else if (status.code == "ok") {
422+
code = tracing::SpanStatusCode::OK;
423+
} else if (status.code == "error") {
424+
code = tracing::SpanStatusCode::ERROR;
425+
} else {
426+
JSG_FAIL_REQUIRE(TypeError, "Span status code must be 'unset', 'ok', or 'error'.");
427+
}
428+
429+
kj::Maybe<kj::ConstString> message;
430+
if (code == tracing::SpanStatusCode::ERROR) {
431+
KJ_IF_SOME(value, status.message) {
432+
if (value.size() > MAX_SPAN_BYTES) {
433+
value = kj::str(value.first(MAX_SPAN_BYTES));
434+
}
435+
message = kj::ConstString(kj::mv(value));
436+
}
437+
}
438+
tracing::SpanStatus internalStatus(code, kj::mv(message));
439+
KJ_SWITCH_ONEOF(state) {
440+
KJ_CASE_ONEOF(s, kj::Own<SpanState>) {
441+
s->setStatus(kj::mv(internalStatus));
442+
}
443+
KJ_CASE_ONEOF(s, IoOwn<SpanState>) {
444+
s->setStatus(kj::mv(internalStatus));
445+
}
446+
}
447+
return JSG_THIS;
448+
}
449+
368450
void Span::end() {
369451
KJ_SWITCH_ONEOF(state) {
370452
KJ_CASE_ONEOF(s, kj::Own<SpanState>) {

‎src/workerd/api/tracing.h‎

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ class Tracing; // Forward decl; defined further down after user_tracing::Span.
1919
// the surrounding workerd::api namespace.
2020
namespace workerd::api::user_tracing {
2121

22-
// Max length of a user-supplied operation name in `ctx.tracing.enterSpan(name, ...)`.
22+
// Max length of a user-supplied span operation name.
2323
// Longer names are truncated at the API surface so the limit holds for every downstream
2424
// SpanSubmitter. Span names identify operations, not carry data; the bound is tight on
2525
// purpose.
@@ -39,6 +39,19 @@ struct ExceptionData {
3939
JSG_STRUCT(code, name, message, stack);
4040
};
4141

42+
struct TracingSpanStatus {
43+
kj::String code;
44+
jsg::Optional<kj::String> message;
45+
46+
JSG_STRUCT(code, message);
47+
JSG_STRUCT_TS_DEFINE(type TracingSpanStatusCode = "unset" | "ok" | "error");
48+
JSG_STRUCT_TS_OVERRIDE({
49+
code: TracingSpanStatusCode;
50+
/** A developer-facing error message. Ignored unless code is "error". */
51+
message?: string;
52+
});
53+
};
54+
4255
// Polymorphic state behind the JS Span wrapper. Concrete states represent recording user spans and
4356
// no-op spans, while sharing JS-side attribute byte-limit enforcement.
4457
class SpanState: public kj::Refcounted {
@@ -56,6 +69,9 @@ class SpanState: public kj::Refcounted {
5669
// ended or has no observer. Used by Tracing methods to push onto the AsyncContextFrame.
5770
virtual workerd::SpanParent makeSpanParent() = 0;
5871

72+
virtual void updateName(kj::ConstString operationName) = 0;
73+
virtual void setStatus(tracing::SpanStatus status) = 0;
74+
5975
// Sets a single attribute on the span. If value is kj::none, the attribute is not set.
6076
void setAttribute(kj::String key, kj::Maybe<TagValue> maybeValue);
6177

@@ -106,6 +122,13 @@ class Span: public jsg::Object {
106122
void recordException(
107123
jsg::Lock& js, jsg::Value exception, const jsg::TypeHandler<ExceptionData>& exceptionHandler);
108124

125+
// Changes the span name. Calls after the span has ended are ignored.
126+
jsg::Ref<Span> updateName(jsg::Lock& js, kj::String operationName);
127+
128+
// Sets the span status. Calls after the span has ended are ignored. Messages are retained only
129+
// for errors.
130+
jsg::Ref<Span> setStatus(jsg::Lock& js, TracingSpanStatus status);
131+
109132
// Ends the span and submits its content to the tracing system. Idempotent.
110133
void end();
111134

@@ -115,6 +138,8 @@ class Span: public jsg::Object {
115138
JSG_METHOD(setAttribute);
116139
JSG_METHOD(setAttributes);
117140
JSG_METHOD(recordException);
141+
JSG_METHOD(updateName);
142+
JSG_METHOD(setStatus);
118143
JSG_METHOD(end);
119144

120145
JSG_TS_OVERRIDE({
@@ -126,6 +151,8 @@ class Span: public jsg::Object {
126151
| { code: string | number; name?: string; message?: string; stack?: string }
127152
| { code?: string | number; name: string; message?: string; stack?: string }
128153
| { code?: string | number; name?: string; message: string; stack?: string }): void;
154+
updateName(name: string): this;
155+
setStatus(status: TracingSpanStatus): this;
129156
});
130157
}
131158

@@ -242,4 +269,5 @@ kj::Own<jsg::modules::ModuleBundle> getInternalTracingModuleBundle(auto featureF
242269
} // namespace workerd::api
243270

244271
#define EW_TRACING_ISOLATE_TYPES \
245-
api::Tracing, api::user_tracing::Span, api::user_tracing::ExceptionData
272+
api::Tracing, api::user_tracing::Span, api::user_tracing::ExceptionData, \
273+
api::user_tracing::TracingSpanStatus

0 commit comments

Comments
 (0)