Skip to content
Draft
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
164 changes: 99 additions & 65 deletions src/workerd/api/tracing.c++
Original file line number Diff line number Diff line change
Expand Up @@ -51,33 +51,10 @@ kj::LiteralStringConst spanWarningTypeName(SpanWarningType type) {
} // namespace

// ======================================================================================
// SpanImpl
// SpanState

SpanImpl::SpanImpl(kj::Own<workerd::SpanObserver> 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<TagValue> maybeValue) {
if (!builder.isObserved()) {
void SpanState::setAttribute(kj::String key, kj::Maybe<TagValue> maybeValue) {
if (!canRecordAttributes()) {
return;
}
KJ_IF_SOME(value, maybeValue) {
Expand All @@ -87,9 +64,43 @@ void SpanImpl::setAttribute(kj::String key, kj::Maybe<TagValue> 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<workerd::SpanObserver> 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);
Expand All @@ -102,37 +113,60 @@ void SpanImpl::setAttribute(kj::String key, kj::Maybe<TagValue> 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<kj::Own<SpanImpl>, IoOwn<SpanImpl>> impl): impl(kj::mv(impl)) {}
Span::Span(kj::OneOf<kj::Own<SpanState>, IoOwn<SpanState>> state): state(kj::mv(state)) {}

bool Span::getIsTraced() {
KJ_SWITCH_ONEOF(impl) {
KJ_CASE_ONEOF(s, kj::Own<SpanImpl>) {
KJ_SWITCH_ONEOF(state) {
KJ_CASE_ONEOF(s, kj::Own<SpanState>) {
return s->getIsTraced();
}
KJ_CASE_ONEOF(s, IoOwn<SpanImpl>) {
KJ_CASE_ONEOF(s, IoOwn<SpanState>) {
return s->getIsTraced();
}
}
Expand All @@ -144,11 +178,11 @@ jsg::Ref<Span> Span::setAttribute(jsg::Lock& js, kj::String key, jsg::Optional<T
KJ_IF_SOME(v, value) {
maybeValue = kj::mv(v);
}
KJ_SWITCH_ONEOF(impl) {
KJ_CASE_ONEOF(s, kj::Own<SpanImpl>) {
KJ_SWITCH_ONEOF(state) {
KJ_CASE_ONEOF(s, kj::Own<SpanState>) {
s->setAttribute(kj::mv(key), kj::mv(maybeValue));
}
KJ_CASE_ONEOF(s, IoOwn<SpanImpl>) {
KJ_CASE_ONEOF(s, IoOwn<SpanState>) {
s->setAttribute(kj::mv(key), kj::mv(maybeValue));
}
}
Expand All @@ -163,11 +197,11 @@ jsg::Ref<Span> Span::setAttributes(jsg::Lock& js, jsg::Dict<jsg::Optional<TagVal
}

void Span::end() {
KJ_SWITCH_ONEOF(impl) {
KJ_CASE_ONEOF(s, kj::Own<SpanImpl>) {
KJ_SWITCH_ONEOF(state) {
KJ_CASE_ONEOF(s, kj::Own<SpanState>) {
s->end();
}
KJ_CASE_ONEOF(s, IoOwn<SpanImpl>) {
KJ_CASE_ONEOF(s, IoOwn<SpanState>) {
s->end();
}
}
Expand All @@ -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.

Expand All @@ -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<user_tracing::SpanImpl> impl;
kj::Own<user_tracing::SpanState> state;
kj::Maybe<SpanParent> childSpanForAsyncContext;
bool hasIoContext = IoContext::hasCurrent();

Expand All @@ -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<user_tracing::SpanImpl>(
state = kj::refcounted<user_tracing::UserSpanState>(
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<user_tracing::SpanImpl>(nullptr);
state = kj::refcounted<user_tracing::NoopSpanState>();
}
} else {
impl = kj::refcounted<user_tracing::SpanImpl>(nullptr);
state = kj::refcounted<user_tracing::NoopSpanState>();
}
} else {
// No IoContext: create a no-op span.
impl = kj::refcounted<user_tracing::SpanImpl>(nullptr);
state = kj::refcounted<user_tracing::NoopSpanState>();
}

// 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<user_tracing::Span> {
if (hasIoContext) {
auto ownedImpl = IoContext::current().addObject(kj::mv(impl));
return js.alloc<user_tracing::Span>(kj::mv(ownedImpl));
auto ownedState = IoContext::current().addObject(kj::mv(state));
return js.alloc<user_tracing::Span>(kj::mv(ownedState));
}
return js.alloc<user_tracing::Span>(kj::mv(impl));
return js.alloc<user_tracing::Span>(kj::mv(state));
}();

return CreatedSpan{
Expand Down Expand Up @@ -288,7 +322,7 @@ v8::Local<v8::Value> 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 {
Expand Down
47 changes: 20 additions & 27 deletions src/workerd/api/tracing.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<bool, double, kj::String>;

// 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<workerd::SpanObserver> 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<TagValue> 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<kj::Own<SpanImpl>, IoOwn<SpanImpl>> impl);
explicit Span(kj::OneOf<kj::Own<SpanState>, IoOwn<SpanState>> 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
Expand Down Expand Up @@ -109,7 +102,7 @@ class Span: public jsg::Object {
}

private:
kj::OneOf<kj::Own<SpanImpl>, IoOwn<SpanImpl>> impl;
kj::OneOf<kj::Own<SpanState>, IoOwn<SpanState>> state;

friend class ::workerd::api::Tracing;
};
Expand Down Expand Up @@ -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<v8::Value> startActiveSpan(jsg::Lock& js,
kj::String operationName,
v8::Local<v8::Function> callback,
Expand Down
Loading