From 3a81ac1fb4ede87dbdf597bd6ea7529ca11afb6d Mon Sep 17 00:00:00 2001 From: igoramf Date: Tue, 16 Jun 2026 18:03:24 -0300 Subject: [PATCH 1/3] fix(observability): OTel semantic attrs on access log + status on root span Two instrumentation gaps closed: 1. logRequest() was emitting legacy attribute names (method, path, status, duration_ms). Switch to OTel semantic convention names so tail-worker and direct-POST logs share the same attribute keys and dashboards can query either source without aliasing. 2. deco.http.request root span never received http.status_code, leaving it permanently Unset even for 5xx responses. Stamp it after the handler returns so the setAttribute promotion in otelHttpTracer triggers and 5xx spans surface as StatusCode=Error in ClickHouse. Co-Authored-By: Claude Sonnet 4.6 --- src/middleware/observability.ts | 8 ++++---- src/sdk/workerEntry.ts | 12 ++++++++---- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/middleware/observability.ts b/src/middleware/observability.ts index c3892f1f..ebfe75b4 100644 --- a/src/middleware/observability.ts +++ b/src/middleware/observability.ts @@ -602,10 +602,10 @@ export function logRequest( // logger floor automatically (no need to attach manually here). const level = status >= 500 ? "error" : "info"; logger[level]("request handled", { - method: request.method, - path: url.pathname, - status, - duration_ms: Math.round(durationMs), + "http.request.method": request.method, + "url.path": url.pathname, + "http.response.status_code": status, + "duration_ms": Math.round(durationMs), ...extra, }); } diff --git a/src/sdk/workerEntry.ts b/src/sdk/workerEntry.ts index 2f49af02..0cbb2064 100644 --- a/src/sdk/workerEntry.ts +++ b/src/sdk/workerEntry.ts @@ -1136,10 +1136,14 @@ export function createDecoWorkerEntry( // Run app middleware (injects app state into RequestContext.bag, // runs registered middleware like VTEX cookie forwarding). const appMw = getAppMiddleware(); - if (appMw) { - return appMw(request, () => handleRequest(request, env, ctx)); - } - return handleRequest(request, env, ctx); + const response = await (appMw + ? appMw(request, () => handleRequest(request, env, ctx)) + : handleRequest(request, env, ctx)); + // Stamp the response status on the root span so the span status + // promotion in otelHttpTracer/otel.ts fires correctly — without + // this, deco.http.request spans for 5xx responses are always Unset. + setSpanAttribute("http.status_code", response.status); + return response; }, { "http.method": method, From 40de218137c99a23d2f0a0c65c536352c377316c Mon Sep 17 00:00:00 2001 From: igoramf Date: Thu, 18 Jun 2026 21:12:22 -0300 Subject: [PATCH 2/3] refactor(observability): use OTel semconv constants in logRequest Co-Authored-By: Claude Sonnet 4.6 --- src/middleware/observability.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/middleware/observability.ts b/src/middleware/observability.ts index ebfe75b4..f5b1ecc9 100644 --- a/src/middleware/observability.ts +++ b/src/middleware/observability.ts @@ -34,6 +34,9 @@ import * as asyncHooks from "node:async_hooks"; import { + ATTR_HTTP_REQUEST_METHOD, + ATTR_HTTP_RESPONSE_STATUS_CODE, + ATTR_URL_PATH, METRIC_HTTP_CLIENT_REQUEST_DURATION, METRIC_HTTP_SERVER_REQUEST_DURATION, } from "@opentelemetry/semantic-conventions"; @@ -602,9 +605,9 @@ export function logRequest( // logger floor automatically (no need to attach manually here). const level = status >= 500 ? "error" : "info"; logger[level]("request handled", { - "http.request.method": request.method, - "url.path": url.pathname, - "http.response.status_code": status, + [ATTR_HTTP_REQUEST_METHOD]: request.method, + [ATTR_URL_PATH]: url.pathname, + [ATTR_HTTP_RESPONSE_STATUS_CODE]: status, "duration_ms": Math.round(durationMs), ...extra, }); From 9473ccf3a286c6ac22587487c5a80042a7abe236 Mon Sep 17 00:00:00 2001 From: igoramf Date: Thu, 18 Jun 2026 21:15:26 -0300 Subject: [PATCH 3/3] refactor(observability): remove duration_ms from access log MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Duration belongs on the span (startTime/endTime), not the log. The tail worker already exposes cloudflare.worker.wall_time_ms for the same purpose — having duration_ms in the direct-POST log creates a second inconsistent attribute measuring a slightly different thing. Co-Authored-By: Claude Sonnet 4.6 --- src/middleware/observability.ts | 2 -- src/sdk/workerEntry.ts | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/middleware/observability.ts b/src/middleware/observability.ts index f5b1ecc9..e303f8dc 100644 --- a/src/middleware/observability.ts +++ b/src/middleware/observability.ts @@ -593,7 +593,6 @@ const isDev = export function logRequest( request: Request, status: number, - durationMs: number, extra?: Record, ) { const url = new URL(request.url); @@ -608,7 +607,6 @@ export function logRequest( [ATTR_HTTP_REQUEST_METHOD]: request.method, [ATTR_URL_PATH]: url.pathname, [ATTR_HTTP_RESPONSE_STATUS_CODE]: status, - "duration_ms": Math.round(durationMs), ...extra, }); } diff --git a/src/sdk/workerEntry.ts b/src/sdk/workerEntry.ts index 0cbb2064..e5fcc277 100644 --- a/src/sdk/workerEntry.ts +++ b/src/sdk/workerEntry.ts @@ -1226,7 +1226,7 @@ export function createDecoWorkerEntry( /* swallow — observability must never fail the request */ } try { - logRequest(request, finalResponse.status, durationMs, { + logRequest(request, finalResponse.status, { ...(identity.requestId ? { "request.id": identity.requestId } : {}), ...(identity.traceId ? { "trace.id": identity.traceId } : {}), });