From b6452a3164ac26d0421d71292c1477a73143d94e Mon Sep 17 00:00:00 2001 From: Felix Hanau Date: Tue, 21 Oct 2025 16:25:39 -0400 Subject: [PATCH] [o11y] Report SpanOpen event earlier --- .../test/d1/d1-api-instrumentation-test.js | 62 +++++++++---------- src/workerd/api/actor-kv-test-tail.js | 2 +- src/workerd/api/r2-instrumentation-test.js | 26 ++++---- src/workerd/api/tail-worker-test.js | 2 +- src/workerd/io/trace.c++ | 34 +++++----- src/workerd/io/trace.capnp | 11 ++++ src/workerd/io/trace.h | 29 +++++++-- src/workerd/io/tracer.c++ | 40 +++++++----- src/workerd/io/tracer.h | 8 +++ src/workerd/server/server.c++ | 20 ++++-- 10 files changed, 145 insertions(+), 89 deletions(-) diff --git a/src/cloudflare/internal/test/d1/d1-api-instrumentation-test.js b/src/cloudflare/internal/test/d1/d1-api-instrumentation-test.js index fb9187d5799..d8c399e349c 100644 --- a/src/cloudflare/internal/test/d1/d1-api-instrumentation-test.js +++ b/src/cloudflare/internal/test/d1/d1-api-instrumentation-test.js @@ -31,18 +31,6 @@ export const test = { }; const expectedSpans = [ - { - name: 'fetch', - 'network.protocol.name': 'http', - 'network.protocol.version': 'HTTP/1.1', - 'http.request.method': 'POST', - 'url.full': 'http://d1/execute?resultsFormat=NONE', - 'http.request.header.content-type': 'application/json', - 'http.request.body.size': 231n, - 'http.response.status_code': 200n, - 'http.response.body.size': 0n, - closed: true, - }, { name: 'd1_run', 'db.system.name': 'cloudflare-d1', @@ -70,9 +58,9 @@ const expectedSpans = [ 'network.protocol.name': 'http', 'network.protocol.version': 'HTTP/1.1', 'http.request.method': 'POST', - 'url.full': 'http://d1/query?resultsFormat=ROWS_AND_COLUMNS', + 'url.full': 'http://d1/execute?resultsFormat=NONE', 'http.request.header.content-type': 'application/json', - 'http.request.body.size': 42n, + 'http.request.body.size': 231n, 'http.response.status_code': 200n, 'http.response.body.size': 0n, closed: true, @@ -96,7 +84,7 @@ const expectedSpans = [ 'network.protocol.name': 'http', 'network.protocol.version': 'HTTP/1.1', 'http.request.method': 'POST', - 'url.full': 'http://d1/execute?resultsFormat=NONE', + 'url.full': 'http://d1/query?resultsFormat=ROWS_AND_COLUMNS', 'http.request.header.content-type': 'application/json', 'http.request.body.size': 42n, 'http.response.status_code': 200n, @@ -124,7 +112,7 @@ const expectedSpans = [ 'http.request.method': 'POST', 'url.full': 'http://d1/execute?resultsFormat=NONE', 'http.request.header.content-type': 'application/json', - 'http.request.body.size': 40n, + 'http.request.body.size': 42n, 'http.response.status_code': 200n, 'http.response.body.size': 0n, closed: true, @@ -148,9 +136,9 @@ const expectedSpans = [ 'network.protocol.name': 'http', 'network.protocol.version': 'HTTP/1.1', 'http.request.method': 'POST', - 'url.full': 'http://d1/query?resultsFormat=ROWS_AND_COLUMNS', + 'url.full': 'http://d1/execute?resultsFormat=NONE', 'http.request.header.content-type': 'application/json', - 'http.request.body.size': 223n, + 'http.request.body.size': 40n, 'http.response.status_code': 200n, 'http.response.body.size': 0n, closed: true, @@ -180,9 +168,9 @@ const expectedSpans = [ 'network.protocol.name': 'http', 'network.protocol.version': 'HTTP/1.1', 'http.request.method': 'POST', - 'url.full': 'http://d1/execute?resultsFormat=NONE', + 'url.full': 'http://d1/query?resultsFormat=ROWS_AND_COLUMNS', 'http.request.header.content-type': 'application/json', - 'http.request.body.size': 40n, + 'http.request.body.size': 223n, 'http.response.status_code': 200n, 'http.response.body.size': 0n, closed: true, @@ -208,7 +196,7 @@ const expectedSpans = [ 'http.request.method': 'POST', 'url.full': 'http://d1/execute?resultsFormat=NONE', 'http.request.header.content-type': 'application/json', - 'http.request.body.size': 223n, + 'http.request.body.size': 40n, 'http.response.status_code': 200n, 'http.response.body.size': 0n, closed: true, @@ -238,9 +226,9 @@ const expectedSpans = [ 'network.protocol.name': 'http', 'network.protocol.version': 'HTTP/1.1', 'http.request.method': 'POST', - 'url.full': 'http://d1/query?resultsFormat=ROWS_AND_COLUMNS', + 'url.full': 'http://d1/execute?resultsFormat=NONE', 'http.request.header.content-type': 'application/json', - 'http.request.body.size': 31n, + 'http.request.body.size': 223n, 'http.response.status_code': 200n, 'http.response.body.size': 0n, closed: true, @@ -344,7 +332,7 @@ const expectedSpans = [ 'http.request.method': 'POST', 'url.full': 'http://d1/query?resultsFormat=ROWS_AND_COLUMNS', 'http.request.header.content-type': 'application/json', - 'http.request.body.size': 42n, + 'http.request.body.size': 31n, 'http.response.status_code': 200n, 'http.response.body.size': 0n, closed: true, @@ -448,7 +436,7 @@ const expectedSpans = [ 'http.request.method': 'POST', 'url.full': 'http://d1/query?resultsFormat=ROWS_AND_COLUMNS', 'http.request.header.content-type': 'application/json', - 'http.request.body.size': 61n, + 'http.request.body.size': 42n, 'http.response.status_code': 200n, 'http.response.body.size': 0n, closed: true, @@ -656,7 +644,7 @@ const expectedSpans = [ 'http.request.method': 'POST', 'url.full': 'http://d1/query?resultsFormat=ROWS_AND_COLUMNS', 'http.request.header.content-type': 'application/json', - 'http.request.body.size': 125n, + 'http.request.body.size': 61n, 'http.response.status_code': 200n, 'http.response.body.size': 0n, closed: true, @@ -685,7 +673,7 @@ const expectedSpans = [ 'http.request.method': 'POST', 'url.full': 'http://d1/query?resultsFormat=ROWS_AND_COLUMNS', 'http.request.header.content-type': 'application/json', - 'http.request.body.size': 79n, + 'http.request.body.size': 125n, 'http.response.status_code': 200n, 'http.response.body.size': 0n, closed: true, @@ -815,7 +803,7 @@ const expectedSpans = [ 'http.request.method': 'POST', 'url.full': 'http://d1/query?resultsFormat=ROWS_AND_COLUMNS', 'http.request.header.content-type': 'application/json', - 'http.request.body.size': 298n, + 'http.request.body.size': 79n, 'http.response.status_code': 200n, 'http.response.body.size': 0n, closed: true, @@ -846,7 +834,7 @@ const expectedSpans = [ 'http.request.method': 'POST', 'url.full': 'http://d1/query?resultsFormat=ROWS_AND_COLUMNS', 'http.request.header.content-type': 'application/json', - 'http.request.body.size': 45n, + 'http.request.body.size': 298n, 'http.response.status_code': 200n, 'http.response.body.size': 0n, closed: true, @@ -950,7 +938,7 @@ const expectedSpans = [ 'http.request.method': 'POST', 'url.full': 'http://d1/query?resultsFormat=ROWS_AND_COLUMNS', 'http.request.header.content-type': 'application/json', - 'http.request.body.size': 76n, + 'http.request.body.size': 45n, 'http.response.status_code': 200n, 'http.response.body.size': 0n, closed: true, @@ -976,7 +964,7 @@ const expectedSpans = [ 'http.request.method': 'POST', 'url.full': 'http://d1/query?resultsFormat=ROWS_AND_COLUMNS', 'http.request.header.content-type': 'application/json', - 'http.request.body.size': 117n, + 'http.request.body.size': 76n, 'http.response.status_code': 200n, 'http.response.body.size': 0n, closed: true, @@ -996,4 +984,16 @@ const expectedSpans = [ 'cloudflare.d1.response.changes': 0, closed: true, }, + { + name: 'fetch', + 'network.protocol.name': 'http', + 'network.protocol.version': 'HTTP/1.1', + 'http.request.method': 'POST', + 'url.full': 'http://d1/query?resultsFormat=ROWS_AND_COLUMNS', + 'http.request.header.content-type': 'application/json', + 'http.request.body.size': 117n, + 'http.response.status_code': 200n, + 'http.response.body.size': 0n, + closed: true, + }, ]; diff --git a/src/workerd/api/actor-kv-test-tail.js b/src/workerd/api/actor-kv-test-tail.js index b257078edd4..100f96af088 100644 --- a/src/workerd/api/actor-kv-test-tail.js +++ b/src/workerd/api/actor-kv-test-tail.js @@ -11,6 +11,7 @@ export default testTailHandler; export const test = { async test(ctrl, env, ctx) { const expected = [ + { name: 'durable_object_subrequest', closed: true }, { name: 'durable_object_storage_put', closed: true }, { name: 'durable_object_storage_put', closed: true }, { name: 'durable_object_storage_get', closed: true }, @@ -23,7 +24,6 @@ export const test = { { name: 'durable_object_storage_deleteAlarm', closed: true }, { name: 'durable_object_storage_transaction', closed: true }, { name: 'durable_object_storage_sync', closed: true }, - { name: 'durable_object_subrequest', closed: true }, ]; await Promise.allSettled(invocationPromises); diff --git a/src/workerd/api/r2-instrumentation-test.js b/src/workerd/api/r2-instrumentation-test.js index f86013dc686..4dceadca42d 100644 --- a/src/workerd/api/r2-instrumentation-test.js +++ b/src/workerd/api/r2-instrumentation-test.js @@ -192,12 +192,19 @@ export const test = { closed: true, }, { - name: 'r2_put', + name: 'r2_get', 'cloudflare.binding.type': 'r2', 'cloudflare.binding.name': 'BUCKET', - 'cloudflare.r2.operation': 'PutObject', + 'cloudflare.r2.operation': 'GetObject', 'cloudflare.r2.bucket': 'r2-test', - 'cloudflare.r2.request.key': 'throwOnInvalidEtag', + 'cloudflare.r2.request.key': 'rangeSuff', + 'cloudflare.r2.request.range.suffix': 2n, + 'cloudflare.r2.response.success': true, + 'cloudflare.r2.response.etag': 'objectEtag', + 'cloudflare.r2.response.size': 123, + 'cloudflare.r2.response.uploaded': '2024-08-27T14:00:57.918Z', + 'cloudflare.r2.response.storage_class': 'Standard', + 'cloudflare.r2.response.custom_metadata': true, closed: true, }, { @@ -210,19 +217,12 @@ export const test = { closed: true, }, { - name: 'r2_get', + name: 'r2_put', 'cloudflare.binding.type': 'r2', 'cloudflare.binding.name': 'BUCKET', - 'cloudflare.r2.operation': 'GetObject', + 'cloudflare.r2.operation': 'PutObject', 'cloudflare.r2.bucket': 'r2-test', - 'cloudflare.r2.request.key': 'rangeSuff', - 'cloudflare.r2.request.range.suffix': 2n, - 'cloudflare.r2.response.success': true, - 'cloudflare.r2.response.etag': 'objectEtag', - 'cloudflare.r2.response.size': 123, - 'cloudflare.r2.response.uploaded': '2024-08-27T14:00:57.918Z', - 'cloudflare.r2.response.storage_class': 'Standard', - 'cloudflare.r2.response.custom_metadata': true, + 'cloudflare.r2.request.key': 'throwOnInvalidEtag', closed: true, }, { diff --git a/src/workerd/api/tail-worker-test.js b/src/workerd/api/tail-worker-test.js index c2a8290a743..3f35a3a5097 100644 --- a/src/workerd/api/tail-worker-test.js +++ b/src/workerd/api/tail-worker-test.js @@ -125,7 +125,7 @@ export const test = { // tail-worker-test-jsrpc: Regression test for EW-9282 (missing onset event with // JsRpcSessionCustomEventImpl). This is derived from tests/js-rpc-test.js. - '{"type":"onset","executionModel":"stateless","spanId":"0000000000000000","scriptTags":[],"info":{"type":"custom"}}{"type":"spanOpen","name":"durable_object_subrequest","spanId":"0000000000000002"}{"type":"spanClose","outcome":"ok"}{"type":"spanOpen","name":"jsRpcSession","spanId":"0000000000000003"}{"type":"spanClose","outcome":"ok"}{"type":"spanOpen","name":"jsRpcSession","spanId":"0000000000000001"}{"type":"spanClose","outcome":"ok"}{"type":"outcome","outcome":"ok","cpuTime":0,"wallTime":0}', + '{"type":"onset","executionModel":"stateless","spanId":"0000000000000000","scriptTags":[],"info":{"type":"custom"}}{"type":"spanOpen","name":"jsRpcSession","spanId":"0000000000000001"}{"type":"spanOpen","name":"durable_object_subrequest","spanId":"0000000000000002"}{"type":"spanOpen","name":"jsRpcSession","spanId":"0000000000000003"}{"type":"spanClose","outcome":"ok"}{"type":"spanClose","outcome":"ok"}{"type":"spanClose","outcome":"ok"}{"type":"outcome","outcome":"ok","cpuTime":0,"wallTime":0}', '{"type":"onset","executionModel":"stateless","spanId":"0000000000000000","entrypoint":"MyService","scriptTags":[],"info":{"type":"jsrpc"}}{"type":"attributes","info":[{"name":"jsrpc.method","value":"nonFunctionProperty"}]}{"type":"log","level":"log","message":["bar"]}{"type":"log","level":"log","message":["foo"]}{"type":"outcome","outcome":"ok","cpuTime":0,"wallTime":0}', '{"type":"onset","executionModel":"durableObject","spanId":"0000000000000000","entrypoint":"MyActor","scriptTags":[],"info":{"type":"jsrpc"}}{"type":"log","level":"log","message":["baz"]}{"type":"attributes","info":[{"name":"jsrpc.method","value":"functionProperty"}]}{"type":"return"}{"type":"outcome","outcome":"ok","cpuTime":0,"wallTime":0}', // Test for transient objects - getCounter returns an object with methods diff --git a/src/workerd/io/trace.c++ b/src/workerd/io/trace.c++ index e12467aae64..294c953f063 100644 --- a/src/workerd/io/trace.c++ +++ b/src/workerd/io/trace.c++ @@ -1469,7 +1469,10 @@ SpanBuilder::SpanBuilder(kj::Maybe> observer, KJ_IF_SOME(obs, observer) { // TODO(o11y): Once we report the user tracing spanOpen event as soon as a span is created, we // should be able to fold this virtual call and just get the timestamp directly. - span.emplace(kj::mv(operationName), startTime.orDefault(obs->getTime())); + kj::Date time = startTime.orDefault([&]() { return obs->getTime(); }); + // Report spanOpen event for user tracing spans + obs->reportStart(operationName, time); + span.emplace(kj::mv(operationName), time); this->observer = kj::mv(obs); } } @@ -1612,7 +1615,6 @@ void CompleteSpan::copyTo(rpc::UserSpanData::Builder builder) const { builder.setStartTimeNs((startTime - kj::UNIX_EPOCH) / kj::NANOSECONDS); builder.setEndTimeNs((endTime - kj::UNIX_EPOCH) / kj::NANOSECONDS); builder.setSpanId(spanId); - builder.setParentSpanId(parentSpanId); auto tagsParam = builder.initTags(tags.size()); auto i = 0; @@ -1622,10 +1624,15 @@ void CompleteSpan::copyTo(rpc::UserSpanData::Builder builder) const { serializeTagValue(tagParam.initValue(), tag.value); } } +void SpanOpenData::copyTo(rpc::SpanOpenData::Builder builder) const { + builder.setOperationName(operationName.asPtr()); + builder.setStartTimeNs((startTime - kj::UNIX_EPOCH) / kj::NANOSECONDS); + builder.setSpanId(spanId); + builder.setParentSpanId(parentSpanId); +} CompleteSpan::CompleteSpan(rpc::UserSpanData::Reader reader) : spanId(reader.getSpanId()), - parentSpanId(reader.getParentSpanId()), operationName(kj::str(reader.getOperationName())), startTime(kj::UNIX_EPOCH + reader.getStartTimeNs() * kj::NANOSECONDS), endTime(kj::UNIX_EPOCH + reader.getEndTimeNs() * kj::NANOSECONDS) { @@ -1636,22 +1643,11 @@ CompleteSpan::CompleteSpan(rpc::UserSpanData::Reader reader) deserializeTagValue(tagParam.getValue())); } } - -CompleteSpan CompleteSpan::clone() const { - CompleteSpan copy( - spanId, parentSpanId, kj::ConstString(kj::str(operationName)), startTime, endTime); - copy.tags.reserve(tags.size()); - for (auto& tag: tags) { - copy.tags.insert(kj::ConstString(kj::str(tag.key)), spanTagClone(tag.value)); - } - return copy; -} - -kj::String CompleteSpan::toString() const { - return kj::str("CompleteSpan: ", operationName, - kj::strArray( - KJ_MAP(tag, tags) { return kj::str("(", tag.key, ", ", tag.value, ")"); }, ", ")); -} +SpanOpenData::SpanOpenData(rpc::SpanOpenData::Reader reader) + : spanId(reader.getSpanId()), + parentSpanId(reader.getParentSpanId()), + operationName(kj::str(reader.getOperationName())), + startTime(kj::UNIX_EPOCH + reader.getStartTimeNs() * kj::NANOSECONDS) {} ScopedDurationTagger::ScopedDurationTagger( SpanBuilder& span, kj::ConstString key, const kj::MonotonicClock& timer) diff --git a/src/workerd/io/trace.capnp b/src/workerd/io/trace.capnp index d27e2cfe9fc..19d15dccc9a 100644 --- a/src/workerd/io/trace.capnp +++ b/src/workerd/io/trace.capnp @@ -42,3 +42,14 @@ struct UserSpanData { parentSpanId @5 :UInt64; } +struct SpanOpenData { + # Representation of a SpanOpen event + operationName @0 :Text; + + startTimeNs @1 :Int64; + # Nanoseconds since Unix epoch + + spanId @2 :UInt64; + parentSpanId @3 :UInt64; +} + diff --git a/src/workerd/io/trace.h b/src/workerd/io/trace.h index cc9873fa82a..03a892e0bfd 100644 --- a/src/workerd/io/trace.h +++ b/src/workerd/io/trace.h @@ -611,8 +611,10 @@ kj::String KJ_STRINGIFY(const CustomInfo& customInfo); struct CompleteSpan { // Represents a completed span within user tracing. tracing::SpanId spanId; - tracing::SpanId parentSpanId; + // TODO(cleanup): operationName and startTime are not used in the spanClose event, but still used + // for diagnostics and a fallback timestamp in the implementation. Get rid of them once that is no + // longer needed. kj::ConstString operationName; kj::Date startTime; kj::Date endTime; @@ -621,21 +623,37 @@ struct CompleteSpan { CompleteSpan(rpc::UserSpanData::Reader reader); void copyTo(rpc::UserSpanData::Builder builder) const; - CompleteSpan clone() const; explicit CompleteSpan(tracing::SpanId spanId, - tracing::SpanId parentSpanId, kj::ConstString operationName, kj::Date startTime, kj::Date endTime, kj::HashMap tags = kj::HashMap()) : spanId(spanId), - parentSpanId(parentSpanId), operationName(kj::mv(operationName)), startTime(startTime), endTime(endTime), tags(kj::mv(tags)) {} - kj::String toString() const; +}; + +struct SpanOpenData { + // Represents the data needed for a SpanOpen event + tracing::SpanId spanId; + tracing::SpanId parentSpanId; + + kj::ConstString operationName; + kj::Date startTime; + + SpanOpenData(rpc::SpanOpenData::Reader reader); + void copyTo(rpc::SpanOpenData::Builder builder) const; + explicit SpanOpenData(tracing::SpanId spanId, + tracing::SpanId parentSpanId, + kj::ConstString operationName, + kj::Date startTime) + : spanId(spanId), + parentSpanId(parentSpanId), + operationName(kj::mv(operationName)), + startTime(startTime) {} }; namespace tracing { @@ -1099,6 +1117,7 @@ class SpanObserver: public kj::Refcounted { // // This should always be called exactly once per observer. virtual void report(const Span& span) = 0; + virtual void reportStart(kj::ConstString& operationName, kj::Date startTime) = 0; // The current time to be provided for the span. For user tracing, we will override this to // provide I/O time. This *requires* that spans are only created when an IOContext is available diff --git a/src/workerd/io/tracer.c++ b/src/workerd/io/tracer.c++ index d04df7473eb..1236a682d01 100644 --- a/src/workerd/io/tracer.c++ +++ b/src/workerd/io/tracer.c++ @@ -220,34 +220,44 @@ void WorkerTracer::addSpan(CompleteSpan&& span) { } } - // Span events are transmitted together for now. + // Compose span events – attributes and spanClose are transmitted together for now. auto& topLevelContext = KJ_ASSERT_NONNULL(topLevelInvocationSpanContext); - // Compose span events. For SpanOpen, an all-zero spanId is interpreted as having no spans above - // this one, thus we use the Onset spanId instead (taken from topLevelContext). We go to great - // lengths to rule out getting an all-zero spanId by chance (see SpanId::fromEntropy()), so this - // should be safe. - tracing::SpanId parentSpanId = span.parentSpanId; - if (parentSpanId == tracing::SpanId::nullId) { - parentSpanId = topLevelContext.getSpanId(); - } - // TODO(o11y): Actually report the spanOpen event at span creation time - auto spanOpenContext = tracing::InvocationSpanContext( - topLevelContext.getTraceId(), topLevelContext.getInvocationId(), parentSpanId); auto spanComponentContext = tracing::InvocationSpanContext( topLevelContext.getTraceId(), topLevelContext.getInvocationId(), span.spanId); - tailStreamWriter->report( - spanOpenContext, tracing::SpanOpen(span.spanId, kj::str(span.operationName)), span.startTime); // If a span manages to exceed the size limit, truncate it by not providing span attributes. if (span.tags.size() && messageSize <= MAX_TRACE_BYTES) { tracing::CustomInfo attr = KJ_MAP(tag, span.tags) { - return tracing::Attribute(kj::ConstString(kj::str(tag.key)), spanTagClone(tag.value)); + return tracing::Attribute(kj::mv(tag.key), kj::mv(tag.value)); }; tailStreamWriter->report(spanComponentContext, kj::mv(attr), span.startTime); } tailStreamWriter->report(spanComponentContext, tracing::SpanClose(), span.endTime); } +void WorkerTracer::addSpanOpen(tracing::SpanId spanId, + tracing::SpanId parentSpanId, + kj::ConstString& operationName, + kj::Date startTime) { + // This is where we'll actually encode the span. + if (pipelineLogLevel == PipelineLogLevel::NONE) { + return; + } + + auto& tailStreamWriter = KJ_UNWRAP_OR_RETURN(maybeTailStreamWriter); + auto& topLevelContext = KJ_ASSERT_NONNULL(topLevelInvocationSpanContext); + // Compose SpanOpen. An all-zero spanId is interpreted as having no spans above this one, thus we + // use the Onset spanId instead (taken from topLevelContext). We go to great lengths to rule out + // getting an all-zero spanId by chance (see SpanId::fromEntropy()), so this should be safe. + if (parentSpanId == tracing::SpanId::nullId) { + parentSpanId = topLevelContext.getSpanId(); + } + auto spanOpenContext = tracing::InvocationSpanContext( + topLevelContext.getTraceId(), topLevelContext.getInvocationId(), parentSpanId); + tailStreamWriter->report( + spanOpenContext, tracing::SpanOpen(spanId, kj::str(operationName)), startTime); +} + void WorkerTracer::addException(const tracing::InvocationSpanContext& context, kj::Date timestamp, kj::String name, diff --git a/src/workerd/io/tracer.h b/src/workerd/io/tracer.h index 1b038cdc7c2..0e886694350 100644 --- a/src/workerd/io/tracer.h +++ b/src/workerd/io/tracer.h @@ -104,6 +104,10 @@ class BaseTracer: public kj::Refcounted { kj::String message) = 0; // Add a span. virtual void addSpan(CompleteSpan&& span) = 0; + virtual void addSpanOpen(tracing::SpanId spanId, + tracing::SpanId parentSpanId, + kj::ConstString& operationName, + kj::Date startTime) = 0; virtual void addException(const tracing::InvocationSpanContext& context, kj::Date timestamp, @@ -182,6 +186,10 @@ class WorkerTracer final: public BaseTracer { LogLevel logLevel, kj::String message) override; void addSpan(CompleteSpan&& span) override; + void addSpanOpen(tracing::SpanId spanId, + tracing::SpanId parentSpanId, + kj::ConstString& operationName, + kj::Date startTime) override; void addException(const tracing::InvocationSpanContext& context, kj::Date timestamp, kj::String name, diff --git a/src/workerd/server/server.c++ b/src/workerd/server/server.c++ index ac222f197f0..6a48a9ae2b3 100644 --- a/src/workerd/server/server.c++ +++ b/src/workerd/server/server.c++ @@ -1702,11 +1702,11 @@ class SpanSubmitter final: public kj::Refcounted { SpanSubmitter(kj::Own workerTracer) : predictableSpanId(0), workerTracer(kj::mv(workerTracer)) {} - void submitSpan(tracing::SpanId spanId, tracing::SpanId parentSpanId, const Span& span) { + void submitSpan(tracing::SpanId spanId, const Span& span) { // We largely recreate the span here which feels inefficient, but is hard to avoid given the // mismatch between the Span type and the full span information required for OTel. - CompleteSpan span2(spanId, parentSpanId, kj::ConstString(kj::str(span.operationName)), - span.startTime, span.endTime); + CompleteSpan span2( + spanId, kj::ConstString(kj::str(span.operationName)), span.startTime, span.endTime); span2.tags.reserve(span.tags.size()); for (auto& tag: span.tags) { span2.tags.insert(kj::ConstString(kj::str(tag.key)), spanTagClone(tag.value)); @@ -1717,6 +1717,15 @@ class SpanSubmitter final: public kj::Refcounted { workerTracer->addSpan(kj::mv(span2)); } + void submitSpanStart(tracing::SpanId spanId, + tracing::SpanId parentSpanId, + kj::ConstString& operationName, + kj::Date startTime) { + if (isPredictableModeForTest()) { + startTime = kj::UNIX_EPOCH; + } + workerTracer->addSpanOpen(spanId, parentSpanId, operationName, startTime); + } tracing::SpanId makeSpanId() { return tracing::SpanId(predictableSpanId++); @@ -1743,7 +1752,10 @@ class WorkerTracerSpanObserver: public SpanObserver { } void report(const Span& span) override { - spanSubmitter->submitSpan(spanId, parentSpanId, span); + spanSubmitter->submitSpan(spanId, span); + } + void reportStart(kj::ConstString& operationName, kj::Date startTime) override { + spanSubmitter->submitSpanStart(spanId, parentSpanId, operationName, startTime); } // Provide I/O time to the tracing system for user spans.