Skip to content
File

Blob: src/workerd/api/tracing.c++

9.4 KB
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#include "tracing.h"
6 
7#include <workerd/io/trace.h>
8#include <workerd/io/tracer.h>
9#include <workerd/util/thread-scopes.h>
10 
11namespace workerd::api::user_tracing {
12 
13namespace {
14 
15// Approximately how much data we allow to be added to a user span before we start ignoring
16// modification requests. This is a soft cap to prevent accidental misuse from unbounded
17// memory growth; downstream tail-stream submission may apply additional limits.
18constexpr size_t MAX_SPAN_BYTES = 64 * 1024;
19 
20size_t estimateTagValueSize(TagValue& value) {
21 // Approximate size; different encodings will produce different sizes. The goal is to bound
22 // accidental overuse, not to be byte-accurate.
23 KJ_SWITCH_ONEOF(value) {
24 KJ_CASE_ONEOF(b, bool) {
25 return 8;
26 }
27 KJ_CASE_ONEOF(d, double) {
28 return 8;
29 }
30 KJ_CASE_ONEOF(s, kj::String) {
31 return s.size();
32 }
33 }
34 KJ_UNREACHABLE;
35}
36 
37} // namespace
38 
39// ======================================================================================
40// SpanImpl
41 
42SpanImpl::SpanImpl(kj::Own<workerd::SpanObserver> observer, kj::ConstString operationName)
43 : builder(kj::mv(observer), kj::mv(operationName)) {}
44 
45SpanImpl::SpanImpl(decltype(nullptr)): builder(nullptr) {}
46 
47SpanImpl::~SpanImpl() noexcept(false) {
48 end();
49}
50 
51void SpanImpl::end() {
52 // Move-assigning a null builder ends the old one (submitting via onClose) and drops the
53 // observer reference so subsequent setTag/isObserved calls no-op.
54 builder = workerd::SpanBuilder(nullptr);
55}
56 
57bool SpanImpl::getIsTraced() {
58 return builder.isObserved();
59}
60 
61workerd::SpanParent SpanImpl::makeSpanParent() {
62 return workerd::SpanParent(builder);
63}
64 
65void SpanImpl::setAttribute(kj::String key, kj::Maybe<TagValue> maybeValue) {
66 if (!builder.isObserved()) {
67 return;
68 }
69 KJ_IF_SOME(value, maybeValue) {
70 if (bytesUsed > MAX_SPAN_BYTES) {
71 return;
72 }
73 size_t valueSize = estimateTagValueSize(value);
74 bytesUsed += key.size() + valueSize;
75 if (bytesUsed > MAX_SPAN_BYTES) {
76 setSpanDataLimitError("attribute", key, valueSize);
77 return;
78 }
79 KJ_SWITCH_ONEOF(value) {
80 KJ_CASE_ONEOF(b, bool) {
81 builder.setTag(kj::ConstString(kj::mv(key)), b);
82 }
83 KJ_CASE_ONEOF(d, double) {
84 builder.setTag(kj::ConstString(kj::mv(key)), d);
85 }
86 KJ_CASE_ONEOF(s, kj::String) {
87 builder.setTag(kj::ConstString(kj::mv(key)), kj::mv(s));
88 }
89 }
90 }
91 // If value is kj::none the attribute is left unset (undefined on the JS side).
92}
93 
94void SpanImpl::setSpanDataLimitError(kj::StringPtr itemKind, kj::StringPtr name, size_t valueSize) {
95 if (!builder.isObserved()) {
96 return;
97 }
98 kj::String shortName;
99 if (name.size() > 64) {
100 shortName = kj::str("\"", name.slice(0, 64), "...\" (key length ", name.size(), ")");
101 } else {
102 shortName = kj::str("\"", name, "\"");
103 }
104 auto message = kj::ConstString(kj::str("exceeded span data limit while trying to record ",
105 itemKind, " ", shortName, " of size ", valueSize));
106 builder.setTag("span_error"_kjc, kj::mv(message));
107}
108 
109// ======================================================================================
110// Span
111 
112Span::Span(kj::OneOf<kj::Own<SpanImpl>, IoOwn<SpanImpl>> impl): impl(kj::mv(impl)) {}
113 
114bool Span::getIsTraced() {
115 KJ_SWITCH_ONEOF(impl) {
116 KJ_CASE_ONEOF(s, kj::Own<SpanImpl>) {
117 return s->getIsTraced();
118 }
119 KJ_CASE_ONEOF(s, IoOwn<SpanImpl>) {
120 return s->getIsTraced();
121 }
122 }
123 KJ_UNREACHABLE;
124}
125 
126void Span::setAttribute(jsg::Lock& js, kj::String key, jsg::Optional<TagValue> value) {
127 kj::Maybe<TagValue> maybeValue;
128 KJ_IF_SOME(v, value) {
129 maybeValue = kj::mv(v);
130 }
131 KJ_SWITCH_ONEOF(impl) {
132 KJ_CASE_ONEOF(s, kj::Own<SpanImpl>) {
133 s->setAttribute(kj::mv(key), kj::mv(maybeValue));
134 }
135 KJ_CASE_ONEOF(s, IoOwn<SpanImpl>) {
136 s->setAttribute(kj::mv(key), kj::mv(maybeValue));
137 }
138 }
139}
140 
141void Span::end() {
142 KJ_SWITCH_ONEOF(impl) {
143 KJ_CASE_ONEOF(s, kj::Own<SpanImpl>) {
144 s->end();
145 }
146 KJ_CASE_ONEOF(s, IoOwn<SpanImpl>) {
147 s->end();
148 }
149 }
150}
151 
152} // namespace workerd::api::user_tracing
153 
154// ======================================================================================
155// Tracing
156 
157namespace workerd::api {
158 
159v8::Local<v8::Value> Tracing::enterSpan(jsg::Lock& js,
160 kj::String operationName,
161 v8::Local<v8::Function> callback,
162 jsg::Arguments<jsg::Value> args,
163 const jsg::TypeHandler<jsg::Ref<user_tracing::Span>>& spanHandler,
164 const jsg::TypeHandler<jsg::Promise<jsg::Value>>& valuePromiseHandler) {
165 // We use qualified `user_tracing::Span` / `user_tracing::SpanImpl` throughout because an
166 // unqualified `Span` in this namespace resolves to workerd::Span (the runtime span struct),
167 // which is a different type.
168 
169 // Cap operation name length at the API boundary so every downstream submitter sees the
170 // truncated value.
171 if (operationName.size() > user_tracing::MAX_USER_OPERATION_NAME_BYTES) {
172 operationName = kj::str(operationName.first(user_tracing::MAX_USER_OPERATION_NAME_BYTES));
173 }
174 
175 kj::Own<user_tracing::SpanImpl> impl;
176 kj::Maybe<SpanParent> childSpanForAsyncContext;
177 
178 if (IoContext::hasCurrent()) {
179 auto& context = IoContext::current();
180 SpanParent parent = context.getCurrentUserTraceSpan();
181 
182 if (parent.isObserved()) {
183 KJ_IF_SOME(observer, parent.getObserver()) {
184 // newChildFromUserCode (vs newChild) signals user-origin to the submitter so it can
185 // skip the operation-name allowlist that gates runtime spans.
186 auto childObserver = observer.newChildFromUserCode();
187 impl = kj::refcounted<user_tracing::SpanImpl>(
188 kj::mv(childObserver), kj::ConstString(kj::heapString(operationName)));
189 // Capture a SpanParent for the child so we can push it onto the AsyncContextFrame
190 // below. Safe to carry across the request boundary thanks to BaseTracer::WeakRef in
191 // the submitter - stale parents cannot pin the tracer.
192 childSpanForAsyncContext = impl->makeSpanParent();
193 } else {
194 impl = kj::refcounted<user_tracing::SpanImpl>(nullptr);
195 }
196 } else {
197 impl = kj::refcounted<user_tracing::SpanImpl>(nullptr);
198 }
199 } else {
200 // No IoContext: callback still runs, but with a no-op span and no async-context push.
201 impl = kj::refcounted<user_tracing::SpanImpl>(nullptr);
202 }
203 
204 // Wrap impl in IoOwn (when inside an IoContext) so destruction funnels through the
205 // IoContext's delete queue and cannot cross threads. Outside an IoContext, fall back to
206 // kj::Own; enterSpan without an IoContext is a no-op tracing-wise but still runs the
207 // callback.
208 jsg::Ref<user_tracing::Span> jsSpan = [&]() -> jsg::Ref<user_tracing::Span> {
209 if (IoContext::hasCurrent()) {
210 auto ownedImpl = IoContext::current().addObject(kj::mv(impl));
211 return js.alloc<user_tracing::Span>(kj::mv(ownedImpl));
212 }
213 return js.alloc<user_tracing::Span>(kj::mv(impl));
214 }();
215 
216 // Build argv for the callback: (span, ...args).
217 v8::LocalVector<v8::Value> argv(js.v8Isolate);
218 argv.push_back(spanHandler.wrap(js, jsSpan.addRef()));
219 for (auto& arg: args) {
220 argv.push_back(arg.getHandle(js));
221 }
222 
223 auto executeCallback = [&]() -> v8::Local<v8::Value> {
224 auto v8Context = js.v8Context();
225 return js.tryCatch([&]() -> v8::Local<v8::Value> {
226 auto result =
227 jsg::check(callback->Call(v8Context, v8Context->Global(), argv.size(), argv.data()));
228 // If the callback returned a promise, defer end() until settlement.
229 if (result->IsPromise()) {
230 auto promise = KJ_ASSERT_NONNULL(valuePromiseHandler.tryUnwrap(js, result))
231 .then(js,
232 [jsSpan = jsSpan.addRef()](
233 jsg::Lock& js, jsg::Value value) mutable -> jsg::Value {
234 jsSpan->end();
235 return kj::mv(value);
236 },
237 [jsSpan = jsSpan.addRef()](
238 jsg::Lock& js, jsg::Value exception) mutable -> jsg::Value {
239 jsSpan->end();
240 js.throwException(kj::mv(exception));
241 });
242 // If the promise never settles, the span will still be submitted when the IoOwn is
243 // destroyed (via ~SpanImpl calling end()), though this is a corner case and should
244 // generally be avoided by users.
245 return valuePromiseHandler.wrap(js, kj::mv(promise));
246 } else {
247 // Synchronous success: end immediately.
248 jsSpan->end();
249 return result;
250 }
251 }, [&](jsg::Value exception) -> v8::Local<v8::Value> {
252 // Synchronous exception: end then rethrow.
253 jsSpan->end();
254 js.throwException(kj::mv(exception));
255 });
256 };
257 
258 // If we have an IoContext and an observed child span, push it onto the AsyncContextFrame
259 // for the duration of the callback. The StorageScope RAII object restores the prior
260 // async-context storage on scope exit; any async continuations captured during the
261 // callback will already have snapshotted the new frame and will see our child span as
262 // "current".
263 KJ_IF_SOME(span, kj::mv(childSpanForAsyncContext)) {
264 auto& context = IoContext::current();
265 jsg::AsyncContextFrame::StorageScope traceScope =
266 context.makeUserAsyncTraceScope(context.getCurrentLock(), kj::mv(span));
267 return executeCallback();
268 } else {
269 return executeCallback();
270 }
271}
272 
273} // namespace workerd::api