diff --git a/.changeset/quiet-spans-rest.md b/.changeset/quiet-spans-rest.md new file mode 100644 index 00000000000..9e574309a3d --- /dev/null +++ b/.changeset/quiet-spans-rest.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Skip HTTP server span attribute collection when the span is not sampled. diff --git a/packages/effect/src/unstable/http/HttpMiddleware.ts b/packages/effect/src/unstable/http/HttpMiddleware.ts index 4d09d91a296..c5ddb3508c0 100644 --- a/packages/effect/src/unstable/http/HttpMiddleware.ts +++ b/packages/effect/src/unstable/http/HttpMiddleware.ts @@ -193,32 +193,6 @@ export const tracer: ( fiber.setContext(prevServices) const endTime = fiber.getRef(Clock).currentTimeNanosUnsafe() fiber.currentDispatcher.scheduleTask(() => { - const url = Request.toURL(request) - if (Option.isSome(url) && (url.value.username !== "" || url.value.password !== "")) { - url.value.username = "REDACTED" - url.value.password = "REDACTED" - } - const redactedHeaderNames = fiber.getRef(Headers.CurrentRedactedNames) - const requestHeaders = Headers.redact(request.headers, redactedHeaderNames) - span.attribute("http.request.method", request.method) - if (Option.isSome(url)) { - span.attribute("url.full", url.value.toString()) - span.attribute("url.path", url.value.pathname) - const query = url.value.search.slice(1) - if (query !== "") { - span.attribute("url.query", url.value.search.slice(1)) - } - span.attribute("url.scheme", url.value.protocol.slice(0, -1)) - } - if (request.headers["user-agent"] !== undefined) { - span.attribute("user_agent.original", request.headers["user-agent"]) - } - for (const name in requestHeaders) { - span.attribute(`http.request.header.${name}`, String(requestHeaders[name])) - } - if (Option.isSome(request.remoteAddress)) { - span.attribute("client.address", request.remoteAddress.value) - } let response: HttpServerResponse let spanExit = exit if (Exit.isFailure(exit)) { @@ -228,10 +202,38 @@ export const tracer: ( } else { response = exit.value } - span.attribute("http.response.status_code", response.status) - const responseHeaders = Headers.redact(response.headers, redactedHeaderNames) - for (const name in responseHeaders) { - span.attribute(`http.response.header.${name}`, String(responseHeaders[name])) + if (span.sampled) { + const url = Request.toURL(request) + if (Option.isSome(url) && (url.value.username !== "" || url.value.password !== "")) { + url.value.username = "REDACTED" + url.value.password = "REDACTED" + } + const redactedHeaderNames = fiber.getRef(Headers.CurrentRedactedNames) + const requestHeaders = Headers.redact(request.headers, redactedHeaderNames) + span.attribute("http.request.method", request.method) + if (Option.isSome(url)) { + span.attribute("url.full", url.value.toString()) + span.attribute("url.path", url.value.pathname) + const query = url.value.search.slice(1) + if (query !== "") { + span.attribute("url.query", url.value.search.slice(1)) + } + span.attribute("url.scheme", url.value.protocol.slice(0, -1)) + } + if (request.headers["user-agent"] !== undefined) { + span.attribute("user_agent.original", request.headers["user-agent"]) + } + for (const name in requestHeaders) { + span.attribute(`http.request.header.${name}`, String(requestHeaders[name])) + } + if (Option.isSome(request.remoteAddress)) { + span.attribute("client.address", request.remoteAddress.value) + } + span.attribute("http.response.status_code", response.status) + const responseHeaders = Headers.redact(response.headers, redactedHeaderNames) + for (const name in responseHeaders) { + span.attribute(`http.response.header.${name}`, String(responseHeaders[name])) + } } span.end(endTime, spanExit) }, 0) diff --git a/packages/effect/test/unstable/http/HttpMiddleware.test.ts b/packages/effect/test/unstable/http/HttpMiddleware.test.ts index 8de2eeaf050..83bfda7f620 100644 --- a/packages/effect/test/unstable/http/HttpMiddleware.test.ts +++ b/packages/effect/test/unstable/http/HttpMiddleware.test.ts @@ -59,6 +59,70 @@ describe("HttpMiddleware", () => { }) describe("tracer", () => { + it.effect("records attributes for sampled spans", () => + Effect.gen(function*() { + let serverSpan: Tracer.NativeSpan | undefined + const tracer = Tracer.make({ + span(options) { + serverSpan = new Tracer.NativeSpan(options) + return serverSpan + } + }) + const request = HttpServerRequest.fromWeb( + new Request("https://localhost:3000/todos/1?foo=bar", { + method: "POST", + headers: { + "user-agent": "test-agent", + "x-request": "request" + } + }) + ) + const response = HttpServerResponse.empty({ + status: 201, + headers: { "x-response": "response" } + }) + + yield* HttpMiddleware.tracer(Effect.succeed(response)).pipe( + Effect.provideService(HttpServerRequest.HttpServerRequest, request), + Effect.provideService(Tracer.Tracer, tracer) + ) + yield* Effect.yieldNow + + assert(serverSpan !== undefined) + assert.strictEqual(serverSpan.sampled, true) + assert.strictEqual(serverSpan.attributes.get("http.request.method"), "POST") + assert.strictEqual(serverSpan.attributes.get("url.path"), "/todos/1") + assert.strictEqual(serverSpan.attributes.get("url.query"), "foo=bar") + assert.strictEqual(serverSpan.attributes.get("user_agent.original"), "test-agent") + assert.strictEqual(serverSpan.attributes.get("http.request.header.x-request"), "request") + assert.strictEqual(serverSpan.attributes.get("http.response.status_code"), 201) + assert.strictEqual(serverSpan.attributes.get("http.response.header.x-response"), "response") + })) + + it.effect("skips attributes for unsampled spans", () => + Effect.gen(function*() { + let serverSpan: Tracer.NativeSpan | undefined + const tracer = Tracer.make({ + span(options) { + serverSpan = new Tracer.NativeSpan(options) + return serverSpan + } + }) + const request = HttpServerRequest.fromWeb(new Request("http://localhost:3000/unsampled")) + + yield* HttpMiddleware.tracer(Effect.succeed(HttpServerResponse.empty({ status: 204 }))).pipe( + Effect.provideService(HttpServerRequest.HttpServerRequest, request), + Effect.provideService(Tracer.MinimumTraceLevel, "Fatal"), + Effect.provideService(Tracer.Tracer, tracer) + ) + yield* Effect.yieldNow + + assert(serverSpan !== undefined) + assert.strictEqual(serverSpan.sampled, false) + assert.strictEqual(serverSpan.attributes.size, 0) + assert.strictEqual(serverSpan.status._tag, "Ended") + })) + it.effect("excludes the sent response from a failed stream span", () => Effect.gen(function*() { let serverSpan: Tracer.NativeSpan | undefined