File
Blob: src/workerd/api/tracing.c++
| 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 | |
| 11 | namespace workerd::api::user_tracing { |
| 12 | |
| 13 | namespace { |
| 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. |
| 18 | constexpr size_t MAX_SPAN_BYTES = 64 * 1024; |
| 19 | |
| 20 | size_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 | |
| 42 | SpanImpl::SpanImpl(kj::Own<workerd::SpanObserver> observer, kj::ConstString operationName) |
| 43 | : builder(kj::mv(observer), kj::mv(operationName)) {} |
| 44 | |
| 45 | SpanImpl::SpanImpl(decltype(nullptr)): builder(nullptr) {} |
| 46 | |
| 47 | SpanImpl::~SpanImpl() noexcept(false) { |
| 48 | end(); |
| 49 | } |
| 50 | |
| 51 | void 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 | |
| 57 | bool SpanImpl::getIsTraced() { |
| 58 | return builder.isObserved(); |
| 59 | } |
| 60 | |
| 61 | workerd::SpanParent SpanImpl::makeSpanParent() { |
| 62 | return workerd::SpanParent(builder); |
| 63 | } |
| 64 | |
| 65 | void 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 | |
| 94 | void 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 | |
| 112 | Span::Span(kj::OneOf<kj::Own<SpanImpl>, IoOwn<SpanImpl>> impl): impl(kj::mv(impl)) {} |
| 113 | |
| 114 | bool 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 | |
| 126 | void 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 | |
| 141 | void 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 | |
| 157 | namespace workerd::api { |
| 158 | |
| 159 | v8::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 |