Skip to content

Commit 1535869

Browse files
steebchenclaude
andcommitted
Add request/response body size tracking to workerd trace events
Add requestSize and bodySize fields to FetchEventInfo and FetchResponseInfo so tail workers can access body size information. Request size is captured from the Content-Length header, and response size from expectedBodySize in ResponseSentTracker. Both are exposed as optional properties in JavaScript trace events. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
1 parent 7d0bd27 commit 1535869

6 files changed

Lines changed: 83 additions & 19 deletions

File tree

src/workerd/api/trace.c++

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -127,8 +127,8 @@ kj::Own<TraceItem::FetchEventInfo::Request::Detail> getFetchRequestDetail(
127127
};
128128
};
129129

130-
return kj::refcounted<TraceItem::FetchEventInfo::Request::Detail>(
131-
getCf(), getHeaders(), kj::str(eventInfo.method), kj::str(eventInfo.url));
130+
return kj::refcounted<TraceItem::FetchEventInfo::Request::Detail>(getCf(), getHeaders(),
131+
kj::str(eventInfo.method), kj::str(eventInfo.url), eventInfo.requestSize);
132132
}
133133

134134
kj::Maybe<TraceItem::EventInfo> getTraceEvent(jsg::Lock& js, const Trace& trace) {
@@ -304,11 +304,13 @@ TraceItem::FetchEventInfo::FetchEventInfo(jsg::Lock& js,
304304
TraceItem::FetchEventInfo::Request::Detail::Detail(jsg::Optional<jsg::V8Ref<v8::Object>> cf,
305305
kj::Array<tracing::FetchEventInfo::Header> headers,
306306
kj::String method,
307-
kj::String url)
307+
kj::String url,
308+
uint64_t requestSize)
308309
: cf(kj::mv(cf)),
309310
headers(kj::mv(headers)),
310311
method(kj::mv(method)),
311-
url(kj::mv(url)) {}
312+
url(kj::mv(url)),
313+
requestSize(requestSize) {}
312314

313315
jsg::Ref<TraceItem::FetchEventInfo::Request> TraceItem::FetchEventInfo::getRequest() {
314316
return request.addRef();
@@ -361,19 +363,36 @@ kj::String TraceItem::FetchEventInfo::Request::getUrl() {
361363
return (redacted ? redactUrl(detail->url) : kj::str(detail->url));
362364
}
363365

366+
jsg::Optional<double> TraceItem::FetchEventInfo::Request::getBodySize() {
367+
// Return null if requestSize is 0 (unknown/no body), otherwise return the size
368+
if (detail->requestSize == 0) {
369+
return kj::none;
370+
}
371+
return static_cast<double>(detail->requestSize);
372+
}
373+
364374
jsg::Ref<TraceItem::FetchEventInfo::Request> TraceItem::FetchEventInfo::Request::getUnredacted(
365375
jsg::Lock& js) {
366376
return js.alloc<Request>(*detail, false /* details are not redacted */);
367377
}
368378

369379
TraceItem::FetchEventInfo::Response::Response(
370380
const Trace& trace, const tracing::FetchResponseInfo& responseInfo)
371-
: status(responseInfo.statusCode) {}
381+
: status(responseInfo.statusCode),
382+
bodySize(responseInfo.bodySize) {}
372383

373384
uint16_t TraceItem::FetchEventInfo::Response::getStatus() {
374385
return status;
375386
}
376387

388+
jsg::Optional<double> TraceItem::FetchEventInfo::Response::getBodySize() {
389+
// Return null if bodySize is 0 (unknown/no body), otherwise return the size
390+
if (bodySize == 0) {
391+
return kj::none;
392+
}
393+
return static_cast<double>(bodySize);
394+
}
395+
377396
TraceItem::JsRpcEventInfo::JsRpcEventInfo(
378397
const Trace& trace, const tracing::JsRpcEventInfo& eventInfo)
379398
: rpcMethod(kj::str(eventInfo.methodName)) {}

src/workerd/api/trace.h

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,11 +199,13 @@ class TraceItem::FetchEventInfo::Request final: public jsg::Object {
199199
kj::Array<tracing::FetchEventInfo::Header> headers;
200200
kj::String method;
201201
kj::String url;
202+
uint64_t requestSize;
202203

203204
Detail(jsg::Optional<jsg::V8Ref<v8::Object>> cf,
204205
kj::Array<tracing::FetchEventInfo::Header> headers,
205206
kj::String method,
206-
kj::String url);
207+
kj::String url,
208+
uint64_t requestSize);
207209

208210
JSG_MEMORY_INFO(Detail) {
209211
tracker.trackField("cf", cf);
@@ -224,6 +226,7 @@ class TraceItem::FetchEventInfo::Request final: public jsg::Object {
224226
jsg::Dict<kj::String, kj::String> getHeaders(jsg::Lock& js);
225227
kj::StringPtr getMethod();
226228
kj::String getUrl();
229+
jsg::Optional<double> getBodySize();
227230

228231
jsg::Ref<Request> getUnredacted(jsg::Lock& js);
229232

@@ -232,6 +235,7 @@ class TraceItem::FetchEventInfo::Request final: public jsg::Object {
232235
JSG_LAZY_READONLY_INSTANCE_PROPERTY(headers, getHeaders);
233236
JSG_LAZY_READONLY_INSTANCE_PROPERTY(method, getMethod);
234237
JSG_LAZY_READONLY_INSTANCE_PROPERTY(url, getUrl);
238+
JSG_LAZY_READONLY_INSTANCE_PROPERTY(bodySize, getBodySize);
235239

236240
JSG_METHOD(getUnredacted);
237241
}
@@ -250,13 +254,16 @@ class TraceItem::FetchEventInfo::Response final: public jsg::Object {
250254
explicit Response(const Trace& trace, const tracing::FetchResponseInfo& responseInfo);
251255

252256
uint16_t getStatus();
257+
jsg::Optional<double> getBodySize();
253258

254259
JSG_RESOURCE_TYPE(Response) {
255260
JSG_LAZY_READONLY_INSTANCE_PROPERTY(status, getStatus);
261+
JSG_LAZY_READONLY_INSTANCE_PROPERTY(bodySize, getBodySize);
256262
}
257263

258264
private:
259265
uint16_t status;
266+
uint64_t bodySize;
260267
};
261268

262269
class TraceItem::JsRpcEventInfo final: public jsg::Object {

src/workerd/io/trace.c++

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -323,17 +323,22 @@ static kj::HttpMethod validateMethod(capnp::HttpMethod method) {
323323

324324
} // namespace
325325

326-
FetchEventInfo::FetchEventInfo(
327-
kj::HttpMethod method, kj::String url, kj::String cfJson, kj::Array<Header> headers)
326+
FetchEventInfo::FetchEventInfo(kj::HttpMethod method,
327+
kj::String url,
328+
kj::String cfJson,
329+
kj::Array<Header> headers,
330+
uint64_t requestSize)
328331
: method(method),
329332
url(kj::mv(url)),
330333
cfJson(kj::mv(cfJson)),
331-
headers(kj::mv(headers)) {}
334+
headers(kj::mv(headers)),
335+
requestSize(requestSize) {}
332336

333337
FetchEventInfo::FetchEventInfo(rpc::Trace::FetchEventInfo::Reader reader)
334338
: method(validateMethod(reader.getMethod())),
335339
url(kj::str(reader.getUrl())),
336-
cfJson(kj::str(reader.getCfJson())) {
340+
cfJson(kj::str(reader.getCfJson())),
341+
requestSize(reader.getRequestSize()) {
337342
kj::Vector<Header> v;
338343
v.addAll(reader.getHeaders());
339344
headers = v.releaseAsArray();
@@ -343,6 +348,7 @@ void FetchEventInfo::copyTo(rpc::Trace::FetchEventInfo::Builder builder) const {
343348
builder.setMethod(static_cast<capnp::HttpMethod>(method));
344349
builder.setUrl(url);
345350
builder.setCfJson(cfJson);
351+
builder.setRequestSize(requestSize);
346352

347353
auto list = builder.initHeaders(headers.size());
348354
for (auto i: kj::indices(headers)) {
@@ -352,7 +358,7 @@ void FetchEventInfo::copyTo(rpc::Trace::FetchEventInfo::Builder builder) const {
352358

353359
FetchEventInfo FetchEventInfo::clone() const {
354360
return FetchEventInfo(
355-
method, kj::str(url), kj::str(cfJson), KJ_MAP(h, headers) { return h.clone(); });
361+
method, kj::str(url), kj::str(cfJson), KJ_MAP(h, headers) { return h.clone(); }, requestSize);
356362
}
357363

358364
kj::String FetchEventInfo::toString() const {
@@ -595,17 +601,21 @@ HibernatableWebSocketEventInfo::Type HibernatableWebSocketEventInfo::readFrom(
595601
}
596602
}
597603

598-
FetchResponseInfo::FetchResponseInfo(uint16_t statusCode): statusCode(statusCode) {}
604+
FetchResponseInfo::FetchResponseInfo(uint16_t statusCode, uint64_t bodySize)
605+
: statusCode(statusCode),
606+
bodySize(bodySize) {}
599607

600608
FetchResponseInfo::FetchResponseInfo(rpc::Trace::FetchResponseInfo::Reader reader)
601-
: statusCode(reader.getStatusCode()) {}
609+
: statusCode(reader.getStatusCode()),
610+
bodySize(reader.getBodySize()) {}
602611

603612
void FetchResponseInfo::copyTo(rpc::Trace::FetchResponseInfo::Builder builder) const {
604613
builder.setStatusCode(statusCode);
614+
builder.setBodySize(bodySize);
605615
}
606616

607617
FetchResponseInfo FetchResponseInfo::clone() const {
608-
return FetchResponseInfo(statusCode);
618+
return FetchResponseInfo(statusCode, bodySize);
609619
}
610620

611621
Log::Log(kj::Date timestamp, LogLevel logLevel, kj::String message)

src/workerd/io/trace.h

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -309,8 +309,11 @@ kj::String KJ_STRINGIFY(const SpanContext& context);
309309
struct FetchEventInfo final {
310310
struct Header;
311311

312-
explicit FetchEventInfo(
313-
kj::HttpMethod method, kj::String url, kj::String cfJson, kj::Array<Header> headers);
312+
explicit FetchEventInfo(kj::HttpMethod method,
313+
kj::String url,
314+
kj::String cfJson,
315+
kj::Array<Header> headers,
316+
uint64_t requestSize = 0);
314317
FetchEventInfo(rpc::Trace::FetchEventInfo::Reader reader);
315318
FetchEventInfo(FetchEventInfo&&) = default;
316319
FetchEventInfo& operator=(FetchEventInfo&&) = default;
@@ -341,6 +344,7 @@ struct FetchEventInfo final {
341344
// TODO(perf): It might be more efficient to store some sort of parsed JSON result instead?
342345
kj::String cfJson;
343346
kj::Array<Header> headers;
347+
uint64_t requestSize = 0; // Request body size in bytes. 0 if unknown or no body.
344348

345349
void copyTo(rpc::Trace::FetchEventInfo::Builder builder) const;
346350
FetchEventInfo clone() const;
@@ -484,13 +488,14 @@ struct CustomEventInfo final {
484488

485489
// Describes a fetch response
486490
struct FetchResponseInfo final {
487-
explicit FetchResponseInfo(uint16_t statusCode);
491+
explicit FetchResponseInfo(uint16_t statusCode, uint64_t bodySize = 0);
488492
FetchResponseInfo(rpc::Trace::FetchResponseInfo::Reader reader);
489493
FetchResponseInfo(FetchResponseInfo&&) = default;
490494
FetchResponseInfo& operator=(FetchResponseInfo&&) = default;
491495
KJ_DISALLOW_COPY(FetchResponseInfo);
492496

493497
uint16_t statusCode;
498+
uint64_t bodySize = 0; // Response body size in bytes. 0 if unknown or no body.
494499

495500
void copyTo(rpc::Trace::FetchResponseInfo::Builder builder) const;
496501
FetchResponseInfo clone() const;

src/workerd/io/worker-entrypoint.c++

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,9 @@ class WorkerEntrypoint::ResponseSentTracker final: public kj::HttpService::Respo
138138
uint getHttpResponseStatus() const {
139139
return httpResponseStatus;
140140
}
141+
kj::Maybe<uint64_t> getExpectedBodySize() const {
142+
return expectedBodySize;
143+
}
141144

142145
kj::Own<kj::AsyncOutputStream> send(uint statusCode,
143146
kj::StringPtr statusText,
@@ -147,6 +150,7 @@ class WorkerEntrypoint::ResponseSentTracker final: public kj::HttpService::Respo
147150
"workerd", "WorkerEntrypoint::ResponseSentTracker::send()", "statusCode", statusCode);
148151
sent = true;
149152
httpResponseStatus = statusCode;
153+
this->expectedBodySize = expectedBodySize;
150154
return inner.send(statusCode, statusText, headers, expectedBodySize);
151155
}
152156

@@ -158,6 +162,7 @@ class WorkerEntrypoint::ResponseSentTracker final: public kj::HttpService::Respo
158162

159163
private:
160164
uint httpResponseStatus = 0;
165+
kj::Maybe<uint64_t> expectedBodySize;
161166
kj::HttpService::Response& inner;
162167
bool sent = false;
163168
};
@@ -286,8 +291,20 @@ kj::Promise<void> WorkerEntrypoint::request(kj::HttpMethod method,
286291
return tracing::FetchEventInfo::Header(kj::mv(entry.key), kj::strArray(entry.value, ", "));
287292
};
288293

294+
// Extract request body size from Content-Length header if present
295+
uint64_t requestSize = 0;
296+
KJ_IF_SOME(contentLength, headers.get(kj::HttpHeaderId::CONTENT_LENGTH)) {
297+
// Parse the Content-Length value. If parsing fails, we leave requestSize as 0.
298+
char* end;
299+
auto parsed = strtoull(contentLength.cStr(), &end, 10);
300+
if (end != contentLength.cStr() && *end == '\0') {
301+
requestSize = parsed;
302+
}
303+
}
304+
289305
t.setEventInfo(*incomingRequest,
290-
tracing::FetchEventInfo(method, kj::str(url), kj::mv(cfJson), kj::mv(traceHeadersArray)));
306+
tracing::FetchEventInfo(
307+
method, kj::str(url), kj::mv(cfJson), kj::mv(traceHeadersArray), requestSize));
291308
workerTracer = t;
292309
}
293310

@@ -331,7 +348,9 @@ kj::Promise<void> WorkerEntrypoint::request(kj::HttpMethod method,
331348
KJ_IF_SOME(t, workerTracer) {
332349
auto httpResponseStatus = wrappedResponse.getHttpResponseStatus();
333350
if (httpResponseStatus != 0) {
334-
t.setReturn(context.now(), tracing::FetchResponseInfo(httpResponseStatus));
351+
uint64_t responseBodySize = wrappedResponse.getExpectedBodySize().orDefault(0);
352+
t.setReturn(
353+
context.now(), tracing::FetchResponseInfo(httpResponseStatus, responseBodySize));
335354
} else {
336355
t.setReturn(context.now());
337356
}

src/workerd/io/worker-interface.capnp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,8 @@ struct Trace @0x8e8d911203762d34 {
108108
name @0 :Text;
109109
value @1 :Text;
110110
}
111+
requestSize @4 :UInt64;
112+
# Request body size in bytes. 0 if unknown or no body.
111113
}
112114

113115
struct JsRpcEventInfo {
@@ -158,6 +160,8 @@ struct Trace @0x8e8d911203762d34 {
158160
response @8 :FetchResponseInfo;
159161
struct FetchResponseInfo {
160162
statusCode @0 :UInt16;
163+
bodySize @1 :UInt64;
164+
# Response body size in bytes. 0 if unknown or no body.
161165
}
162166

163167
cpuTime @10 :UInt64;

0 commit comments

Comments
 (0)