diff --git a/src/workerd/api/tracing.c++ b/src/workerd/api/tracing.c++ index d30079c9968..d58a5e514cc 100644 --- a/src/workerd/api/tracing.c++ +++ b/src/workerd/api/tracing.c++ @@ -51,33 +51,10 @@ kj::LiteralStringConst spanWarningTypeName(SpanWarningType type) { } // namespace // ====================================================================================== -// SpanImpl +// SpanState -SpanImpl::SpanImpl(kj::Own observer, kj::ConstString operationName) - : builder(kj::mv(observer), kj::mv(operationName)) {} - -SpanImpl::SpanImpl(decltype(nullptr)): builder(nullptr) {} - -SpanImpl::~SpanImpl() noexcept(false) { - end(); -} - -void SpanImpl::end() { - // Move-assigning a null builder ends the old one (submitting via onClose) and drops the - // observer reference so subsequent setTag/isObserved calls no-op. - builder = workerd::SpanBuilder(nullptr); -} - -bool SpanImpl::getIsTraced() { - return builder.isObserved(); -} - -workerd::SpanParent SpanImpl::makeSpanParent() { - return workerd::SpanParent(builder); -} - -void SpanImpl::setAttribute(kj::String key, kj::Maybe maybeValue) { - if (!builder.isObserved()) { +void SpanState::setAttribute(kj::String key, kj::Maybe maybeValue) { + if (!canRecordAttributes()) { return; } KJ_IF_SOME(value, maybeValue) { @@ -87,9 +64,43 @@ void SpanImpl::setAttribute(kj::String key, kj::Maybe maybeValue) { size_t valueSize = estimateTagValueSize(value); bytesUsed += key.size() + valueSize; if (bytesUsed > MAX_SPAN_BYTES) { - setSpanDataLimitError("attribute", key, valueSize); + recordSpanDataLimitError("attribute", key, valueSize); return; } + recordAttribute(kj::mv(key), kj::mv(value)); + } + // If value is kj::none the attribute is left unset (undefined on the JS side). +} + +class UserSpanState final: public SpanState { + public: + UserSpanState(kj::Own observer, kj::ConstString operationName) + : builder(kj::mv(observer), kj::mv(operationName)) {} + + ~UserSpanState() noexcept(false) override { + end(); + } + + void end() override { + // Move-assigning a null builder ends the old one (submitting via onClose) and drops the + // observer reference so subsequent setTag/isObserved calls no-op. + builder = workerd::SpanBuilder(nullptr); + } + + bool getIsTraced() override { + return builder.isObserved(); + } + + workerd::SpanParent makeSpanParent() override { + return workerd::SpanParent(builder); + } + + protected: + bool canRecordAttributes() override { + return builder.isObserved(); + } + + void recordAttribute(kj::String key, TagValue value) override { KJ_SWITCH_ONEOF(value) { KJ_CASE_ONEOF(b, bool) { builder.setTag(kj::ConstString(kj::mv(key)), b, IsCustomTag::YES); @@ -102,37 +113,60 @@ void SpanImpl::setAttribute(kj::String key, kj::Maybe maybeValue) { } } } - // If value is kj::none the attribute is left unset (undefined on the JS side). -} -void SpanImpl::setSpanDataLimitError(kj::StringPtr itemKind, kj::StringPtr name, size_t valueSize) { - if (!builder.isObserved()) { - return; + void recordSpanDataLimitError( + kj::StringPtr itemKind, kj::StringPtr name, size_t valueSize) override { + if (!builder.isObserved()) { + return; + } + kj::String shortName; + if (name.size() > 64) { + shortName = kj::str("\"", name.slice(0, 64), "...\" (key length ", name.size(), ")"); + } else { + shortName = kj::str("\"", name, "\""); + } + auto message = kj::ConstString(kj::str("exceeded span data limit while trying to record ", + itemKind, " ", shortName, " of size ", valueSize)); + builder.setTag("cloudflare.warning.type"_kjc, + spanWarningTypeName(SpanWarningType::SPAN_DATA_LIMIT_EXCEEDED)); + builder.setTag("cloudflare.warning.message"_kjc, kj::mv(message)); } - kj::String shortName; - if (name.size() > 64) { - shortName = kj::str("\"", name.slice(0, 64), "...\" (key length ", name.size(), ")"); - } else { - shortName = kj::str("\"", name, "\""); + + private: + workerd::SpanBuilder builder; +}; + +class NoopSpanState final: public SpanState { + public: + void end() override {} + + bool getIsTraced() override { + return false; } - auto message = kj::ConstString(kj::str("exceeded span data limit while trying to record ", - itemKind, " ", shortName, " of size ", valueSize)); - builder.setTag("cloudflare.warning.type"_kjc, - spanWarningTypeName(SpanWarningType::SPAN_DATA_LIMIT_EXCEEDED)); - builder.setTag("cloudflare.warning.message"_kjc, kj::mv(message)); -} + + workerd::SpanParent makeSpanParent() override { + return workerd::SpanParent(nullptr); + } + + protected: + bool canRecordAttributes() override { + return false; + } + + void recordAttribute(kj::String, TagValue) override {} +}; // ====================================================================================== // Span -Span::Span(kj::OneOf, IoOwn> impl): impl(kj::mv(impl)) {} +Span::Span(kj::OneOf, IoOwn> state): state(kj::mv(state)) {} bool Span::getIsTraced() { - KJ_SWITCH_ONEOF(impl) { - KJ_CASE_ONEOF(s, kj::Own) { + KJ_SWITCH_ONEOF(state) { + KJ_CASE_ONEOF(s, kj::Own) { return s->getIsTraced(); } - KJ_CASE_ONEOF(s, IoOwn) { + KJ_CASE_ONEOF(s, IoOwn) { return s->getIsTraced(); } } @@ -144,11 +178,11 @@ jsg::Ref Span::setAttribute(jsg::Lock& js, kj::String key, jsg::Optional) { + KJ_SWITCH_ONEOF(state) { + KJ_CASE_ONEOF(s, kj::Own) { s->setAttribute(kj::mv(key), kj::mv(maybeValue)); } - KJ_CASE_ONEOF(s, IoOwn) { + KJ_CASE_ONEOF(s, IoOwn) { s->setAttribute(kj::mv(key), kj::mv(maybeValue)); } } @@ -163,11 +197,11 @@ jsg::Ref Span::setAttributes(jsg::Lock& js, jsg::Dict) { + KJ_SWITCH_ONEOF(state) { + KJ_CASE_ONEOF(s, kj::Own) { s->end(); } - KJ_CASE_ONEOF(s, IoOwn) { + KJ_CASE_ONEOF(s, IoOwn) { s->end(); } } @@ -190,7 +224,7 @@ struct CreatedSpan { }; CreatedSpan createSpan(jsg::Lock& js, kj::String operationName) { - // We use qualified `user_tracing::Span` / `user_tracing::SpanImpl` throughout because an + // We use qualified `user_tracing::Span` / `user_tracing::SpanState` throughout because an // unqualified `Span` in this namespace resolves to workerd::Span (the runtime span struct), // which is a different type. @@ -200,7 +234,7 @@ CreatedSpan createSpan(jsg::Lock& js, kj::String operationName) { operationName = kj::str(operationName.first(user_tracing::MAX_USER_OPERATION_NAME_BYTES)); } - kj::Own impl; + kj::Own state; kj::Maybe childSpanForAsyncContext; bool hasIoContext = IoContext::hasCurrent(); @@ -213,32 +247,32 @@ CreatedSpan createSpan(jsg::Lock& js, kj::String operationName) { // newChildFromUserCode (vs newChild) signals user-origin to the submitter so it can // skip the operation-name allowlist that gates runtime spans. auto childObserver = observer.newChildFromUserCode(); - impl = kj::refcounted( + state = kj::refcounted( kj::mv(childObserver), kj::ConstString(kj::heapString(operationName))); // Capture a SpanParent for the child so startActiveSpan() / enterSpan() can push it onto // the AsyncContextFrame. Safe to carry across the request boundary thanks to // BaseTracer::WeakRef in the submitter - stale parents cannot pin the tracer. - childSpanForAsyncContext = impl->makeSpanParent(); + childSpanForAsyncContext = state->makeSpanParent(); } else { - impl = kj::refcounted(nullptr); + state = kj::refcounted(); } } else { - impl = kj::refcounted(nullptr); + state = kj::refcounted(); } } else { // No IoContext: create a no-op span. - impl = kj::refcounted(nullptr); + state = kj::refcounted(); } - // Wrap impl in IoOwn (when inside an IoContext) so destruction funnels through the + // Wrap state in IoOwn (when inside an IoContext) so destruction funnels through the // IoContext's delete queue and cannot cross threads. Outside an IoContext, fall back to // kj::Own; tracing without an IoContext is a no-op tracing-wise. auto span = [&]() -> jsg::Ref { if (hasIoContext) { - auto ownedImpl = IoContext::current().addObject(kj::mv(impl)); - return js.alloc(kj::mv(ownedImpl)); + auto ownedState = IoContext::current().addObject(kj::mv(state)); + return js.alloc(kj::mv(ownedState)); } - return js.alloc(kj::mv(impl)); + return js.alloc(kj::mv(state)); }(); return CreatedSpan{ @@ -288,7 +322,7 @@ v8::Local runSpan(jsg::Lock& js, js.throwException(kj::mv(exception)); }); // If the promise never settles, the span will still be submitted when the IoOwn is - // destroyed (via ~SpanImpl calling end()), though this is a corner case and should + // destroyed (via ~SpanState calling end()), though this is a corner case and should // generally be avoided by users. return valuePromiseHandler->wrap(js, kj::mv(promise)); } else { diff --git a/src/workerd/api/tracing.h b/src/workerd/api/tracing.h index d2928277673..f085c85e8ef 100644 --- a/src/workerd/api/tracing.h +++ b/src/workerd/api/tracing.h @@ -28,55 +28,48 @@ constexpr size_t MAX_USER_OPERATION_NAME_BYTES = 64; // The types allowed for tag and log values from JavaScript. using TagValue = kj::OneOf; -// Refcounted wrapper around workerd::SpanBuilder, exposing the JS Span surface: bytes-used -// limit enforcement and JS-side TagValue forwarding. Span lifecycle (onOpen/onClose) is -// delegated to SpanBuilder. -class SpanImpl final: public kj::Refcounted { +// Polymorphic state behind the JS Span wrapper. Concrete states represent recording user spans and +// no-op spans, while sharing JS-side attribute byte-limit enforcement. +class SpanState: public kj::Refcounted { public: - // Construct an observed span. The builder drives the observer's onOpen immediately. - SpanImpl(kj::Own observer, kj::ConstString operationName); - - // Construct a no-op span (not recording). Used when there is no current user trace span - // (e.g., running outside a traced request) or when we are in a context where we cannot - // safely observe spans. - explicit SpanImpl(decltype(nullptr)); - - KJ_DISALLOW_COPY_AND_MOVE(SpanImpl); - - ~SpanImpl() noexcept(false); + virtual ~SpanState() noexcept(false) = default; + KJ_DISALLOW_COPY_AND_MOVE(SpanState); // Submits the span and marks it as no longer traced. Idempotent; the destructor calls // end() as well. - void end(); + virtual void end() = 0; - bool getIsTraced(); + virtual bool getIsTraced() = 0; // Returns a SpanParent wrapping this span's observer, or a null SpanParent if the span has // ended or has no observer. Used by Tracing methods to push onto the AsyncContextFrame. - workerd::SpanParent makeSpanParent(); + virtual workerd::SpanParent makeSpanParent() = 0; // Sets a single attribute on the span. If value is kj::none, the attribute is not set. void setAttribute(kj::String key, kj::Maybe maybeValue); - private: - workerd::SpanBuilder builder; + protected: + SpanState() = default; + virtual bool canRecordAttributes() = 0; + virtual void recordAttribute(kj::String key, TagValue value) = 0; + virtual void recordSpanDataLimitError( + kj::StringPtr itemKind, kj::StringPtr name, size_t valueSize) {} + private: size_t bytesUsed = 0; - - void setSpanDataLimitError(kj::StringPtr itemKind, kj::StringPtr name, size_t valueSize); }; // JavaScript-accessible tracing span (exposed as `Span`). From the user's perspective this -// is the only kind of span there is; internal C++ plumbing lives on SpanImpl. Kept in the +// is the only kind of span there is; internal C++ plumbing lives on SpanState. Kept in the // workerd::api::user_tracing namespace (not workerd::api) to avoid collision with the // runtime's own workerd::Span type. // -// The impl is wrapped in IoOwn when an IoContext exists, so that destruction is funneled +// The state is wrapped in IoOwn when an IoContext exists, so that destruction is funneled // through the IoContext's delete queue and cannot cross threads. When no IoContext is // available (unusual for user tracing - typically startup paths), a plain kj::Own is used. class Span: public jsg::Object { public: - explicit Span(kj::OneOf, IoOwn> impl); + explicit Span(kj::OneOf, IoOwn> state); // Returns true if this span will be recorded. False when the current async context is not // being traced, or when the span has already been submitted (which happens automatically @@ -109,7 +102,7 @@ class Span: public jsg::Object { } private: - kj::OneOf, IoOwn> impl; + kj::OneOf, IoOwn> state; friend class ::workerd::api::Tracing; }; @@ -152,7 +145,7 @@ class Tracing: public jsg::Object { // Creates a new child span, pushes it onto the AsyncContextFrame while invoking // callback(span, ...args), and returns the callback result without ending the span. // The caller must call span.end() explicitly; forgotten spans are still ended by - // SpanImpl's destructor when the request-owned span object is destroyed. + // SpanState's destructor when the request-owned span object is destroyed. v8::Local startActiveSpan(jsg::Lock& js, kj::String operationName, v8::Local callback,