Skip to content
File

Blob: src/workerd/api/tracing.h

cpp185 lines
1// Copyright (c) 2017-2025 Cloudflare, Inc.
2// Licensed under the Apache 2.0 license found in the LICENSE file or at:
3// https://opensource.org/licenses/Apache-2.0
4 
5#pragma once
6 
7#include <workerd/io/io-context.h>
8#include <workerd/io/trace.h>
9#include <workerd/jsg/jsg.h>
10 
11namespace workerd::api {
12class Tracing; // Forward decl; defined further down after user_tracing::Span.
13} // namespace workerd::api
14 
15// The `Span` class exposed to user JavaScript lives in this sub-namespace purely to avoid
16// a name collision with the workerd::Span struct that is used internally by the runtime
17// tracing implementation. Callers outside this file generally don't need to reference this
18// namespace directly - the Tracing class (which is what gets wired into JS) lives in
19// the surrounding workerd::api namespace.
20namespace workerd::api::user_tracing {
21 
22// Max length of a user-supplied operation name in `ctx.tracing.enterSpan(name, ...)`.
23// Longer names are truncated at the API surface so the limit holds for every downstream
24// SpanSubmitter. Span names identify operations, not carry data; the bound is tight on
25// purpose.
26constexpr size_t MAX_USER_OPERATION_NAME_BYTES = 64;
27 
28// The types allowed for tag and log values from JavaScript.
29using TagValue = kj::OneOf<bool, double, kj::String>;
30 
31// Refcounted wrapper around workerd::SpanBuilder, exposing the JS Span surface: bytes-used
32// limit enforcement and JS-side TagValue forwarding. Span lifecycle (onOpen/onClose) is
33// delegated to SpanBuilder.
34class SpanImpl final: public kj::Refcounted {
35 public:
36 // Construct an observed span. The builder drives the observer's onOpen immediately.
37 SpanImpl(kj::Own<workerd::SpanObserver> observer, kj::ConstString operationName);
38 
39 // Construct a no-op span (not recording). Used when there is no current user trace span
40 // (e.g., running outside a traced request) or when we are in a context where we cannot
41 // safely observe spans.
42 explicit SpanImpl(decltype(nullptr));
43 
44 KJ_DISALLOW_COPY_AND_MOVE(SpanImpl);
45 
46 ~SpanImpl() noexcept(false);
47 
48 // Submits the span and marks it as no longer traced. Idempotent; the destructor calls
49 // end() as well.
50 void end();
51 
52 bool getIsTraced();
53 
54 // Returns a SpanParent wrapping this span's observer, or a null SpanParent if the span has
55 // ended or has no observer. Used by Tracing::enterSpan() to push onto the AsyncContextFrame.
56 workerd::SpanParent makeSpanParent();
57 
58 // Sets a single attribute on the span. If value is kj::none, the attribute is not set.
59 void setAttribute(kj::String key, kj::Maybe<TagValue> maybeValue);
60 
61 private:
62 workerd::SpanBuilder builder;
63 
64 size_t bytesUsed = 0;
65 
66 void setSpanDataLimitError(kj::StringPtr itemKind, kj::StringPtr name, size_t valueSize);
67};
68 
69// JavaScript-accessible tracing span (exposed as `Span`). From the user's perspective this
70// is the only kind of span there is; internal C++ plumbing lives on SpanImpl. Kept in the
71// workerd::api::user_tracing namespace (not workerd::api) to avoid collision with the
72// runtime's own workerd::Span type.
73//
74// The impl is wrapped in IoOwn when an IoContext exists, so that destruction is funneled
75// through the IoContext's delete queue and cannot cross threads. When no IoContext is
76// available (unusual for user tracing - typically startup paths), a plain kj::Own is used.
77class Span: public jsg::Object {
78 public:
79 explicit Span(kj::OneOf<kj::Own<SpanImpl>, IoOwn<SpanImpl>> impl);
80 
81 // Returns true if this span will be recorded. False when the current async context is not
82 // being traced, or when the span has already been submitted (which happens automatically
83 // when the enterSpan callback returns). Callers can gate expensive attribute-computation
84 // code on this.
85 bool getIsTraced();
86 
87 // Sets a single attribute. If `value` is undefined, the attribute is not set - useful for
88 // optional fields.
89 void setAttribute(jsg::Lock& js, kj::String key, jsg::Optional<TagValue> value);
90 
91 // Ends the span and submits its content to the tracing system. Not exposed to JS - only
92 // called by Tracing::enterSpan when the user callback returns / throws / its promise
93 // settles. Callers outside this file should not need it.
94 void end();
95 
96 JSG_RESOURCE_TYPE(Span) {
97 JSG_READONLY_PROTOTYPE_PROPERTY(isTraced, getIsTraced);
98 
99 JSG_METHOD(setAttribute);
100 }
101 
102 private:
103 kj::OneOf<kj::Own<SpanImpl>, IoOwn<SpanImpl>> impl;
104 
105 friend class ::workerd::api::Tracing;
106};
107 
108} // namespace workerd::api::user_tracing
109 
110namespace workerd::api {
111 
112// User-tracing module. Exposed to JS as the `Tracing` class, reachable both via
113// `import { tracing } from 'cloudflare:workers'` and as `ctx.tracing`, and registered as
114// the builtin module `cloudflare-internal:tracing`. The class name (not "TracingModule")
115// is what shows up in `.d.ts` output and in `typeof ctx.tracing` — historically this was
116// called `TracingModule` back when the API was only reachable via an ES module import;
117// that suffix is vestigial now that it's also a property on `ctx`.
118class Tracing: public jsg::Object {
119 public:
120 Tracing() = default;
121 Tracing(jsg::Lock&, const jsg::Url&) {}
122 
123 // Creates a new child span of the current user trace span, pushes it onto the
124 // AsyncContextFrame as the active user span, invokes callback(span, ...args), and
125 // automatically ends the span on completion. If the callback returns a Promise, the
126 // span is ended when the promise settles (whether fulfilled or rejected). If the
127 // callback returns synchronously (or throws synchronously), the span is ended
128 // before the return (or rethrow).
129 //
130 // The span is constructed as a child of whatever span is currently active on the
131 // AsyncContextFrame (or, if none, the root user request span on the current
132 // IncomingRequest, via IoContext::getCurrentUserTraceSpan()).
133 //
134 // If no IoContext is available (e.g., during worker startup), the callback runs with
135 // a no-op span and no AsyncContextFrame push.
136 v8::Local<v8::Value> enterSpan(jsg::Lock& js,
137 kj::String operationName,
138 v8::Local<v8::Function> callback,
139 jsg::Arguments<jsg::Value> args,
140 const jsg::TypeHandler<jsg::Ref<user_tracing::Span>>& spanHandler,
141 const jsg::TypeHandler<jsg::Promise<jsg::Value>>& valuePromiseHandler);
142 
143 JSG_RESOURCE_TYPE(Tracing) {
144 JSG_METHOD(enterSpan);
145 
146 // Use the _NAMED variant so the property ends up as `tracing.Span` rather than
147 // `tracing["user_tracing::Span"]`.
148 JSG_NESTED_TYPE_NAMED(user_tracing::Span, Span);
149 
150 // Override the auto-generated `enterSpan(name: string, callback: Function, ...args:
151 // any[]): any` with a properly-typed generic form: the callback's first argument is
152 // typed as `Span`, the callback's trailing args flow through to the varargs, and the
153 // return value is preserved. Matches the shape documented in the user-tracing RFC.
154 JSG_TS_OVERRIDE({
155 enterSpan<T, A extends unknown[]>(
156 name: string,
157 callback: (span: Span, ...args: A) => T,
158 ...args: A
159 ): T;
160 });
161 }
162};
163 
164// Registers `cloudflare-internal:tracing` as a builtin module. The `Module` suffix on the
165// helper is describing what it registers (a JS module), not the name of the class — the
166// class itself is `Tracing` because that's what users see.
167template <class Registry>
168void registerTracingModule(Registry& registry, CompatibilityFlags::Reader flags) {
169 registry.template addBuiltinModule<Tracing>(
170 "cloudflare-internal:tracing", workerd::jsg::ModuleRegistry::Type::INTERNAL);
171}
172 
173template <typename TypeWrapper>
174kj::Own<jsg::modules::ModuleBundle> getInternalTracingModuleBundle(auto featureFlags) {
175 jsg::modules::ModuleBundle::BuiltinBuilder builder(
176 jsg::modules::ModuleBundle::BuiltinBuilder::Type::BUILTIN_ONLY);
177 static const auto kSpecifier = "cloudflare-internal:tracing"_url;
178 builder.addObject<Tracing, TypeWrapper>(kSpecifier);
179 return builder.finish();
180}
181 
182} // namespace workerd::api
183 
184#define EW_TRACING_ISOLATE_TYPES api::Tracing, api::user_tracing::Span