fix(cli): isolate server request traces (#37395)
This commit is contained in:
parent
309860558d
commit
331533deff
5 changed files with 65 additions and 6 deletions
|
|
@ -5,7 +5,7 @@ import { ServerProcess } from "../../server-process"
|
||||||
|
|
||||||
export default Runtime.handler(
|
export default Runtime.handler(
|
||||||
Commands.commands.serve,
|
Commands.commands.serve,
|
||||||
Effect.fn("cli.serve")(function* (input) {
|
Effect.fnUntraced(function* (input) {
|
||||||
if (input.service && input.stdio) return yield* Effect.fail(new Error("--service and --stdio cannot be combined"))
|
if (input.service && input.stdio) return yield* Effect.fail(new Error("--service and --stdio cannot be combined"))
|
||||||
return yield* ServerProcess.run({
|
return yield* ServerProcess.run({
|
||||||
mode: input.service ? "service" : input.stdio ? "stdio" : "default",
|
mode: input.service ? "service" : input.stdio ? "stdio" : "default",
|
||||||
|
|
|
||||||
|
|
@ -24,13 +24,14 @@ export type Options = {
|
||||||
readonly port?: number
|
readonly port?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export const run = Effect.fn("cli.server-process.run")((options: Options) =>
|
// The process effect lives until server shutdown; tracing it would parent every request to one process-lifetime trace.
|
||||||
processEffect(options).pipe(
|
export const run = Effect.fnUntraced(function* (options: Options) {
|
||||||
|
return yield* processEffect(options).pipe(
|
||||||
Effect.provide(Updater.layer),
|
Effect.provide(Updater.layer),
|
||||||
Effect.provide(AppNodeBuilder.build(LayerNode.group([Global.node, AppProcess.node]))),
|
Effect.provide(AppNodeBuilder.build(LayerNode.group([Global.node, AppProcess.node]))),
|
||||||
Effect.provide(NodeServices.layer),
|
Effect.provide(NodeServices.layer),
|
||||||
),
|
)
|
||||||
)
|
})
|
||||||
|
|
||||||
const processEffect = Effect.fnUntraced(function* (options: Options) {
|
const processEffect = Effect.fnUntraced(function* (options: Options) {
|
||||||
if (options.mode === "service") yield* Effect.sync(() => process.chdir(Global.Path.home))
|
if (options.mode === "service") yield* Effect.sync(() => process.chdir(Global.Path.home))
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import { HttpMiddleware, HttpRouter, HttpServer, HttpServerRequest, HttpServerRe
|
||||||
import { createServer } from "node:http"
|
import { createServer } from "node:http"
|
||||||
import { ServerAuth } from "./auth"
|
import { ServerAuth } from "./auth"
|
||||||
import { authorizedRequest } from "./middleware/authorization"
|
import { authorizedRequest } from "./middleware/authorization"
|
||||||
|
import { withoutParentSpan } from "./request-tracing"
|
||||||
import { createRoutes } from "./routes"
|
import { createRoutes } from "./routes"
|
||||||
import { ServerInfo } from "./server-info"
|
import { ServerInfo } from "./server-info"
|
||||||
import { Status } from "./service-status"
|
import { Status } from "./service-status"
|
||||||
|
|
@ -39,7 +40,10 @@ export const start = Effect.fn("ServerProcess.start")(function* <E, R>(options:
|
||||||
})
|
})
|
||||||
const bound = yield* listen(options)
|
const bound = yield* listen(options)
|
||||||
const application = yield* Ref.make(Option.none<App>())
|
const application = yield* Ref.make(Option.none<App>())
|
||||||
yield* bound.http.serve(dispatch(options.password, status, application, shutdown), HttpMiddleware.logger)
|
// Request fibers may continue inbound trace context, but must not inherit the server startup parent.
|
||||||
|
yield* bound.http
|
||||||
|
.serve(dispatch(options.password, status, application, shutdown), HttpMiddleware.logger)
|
||||||
|
.pipe(withoutParentSpan)
|
||||||
if (options.service) yield* options.service.onListen(bound.http.address)
|
if (options.service) yield* options.service.onListen(bound.http.address)
|
||||||
|
|
||||||
const parentScope = yield* Scope.Scope
|
const parentScope = yield* Scope.Scope
|
||||||
|
|
|
||||||
6
packages/server/src/request-tracing.ts
Normal file
6
packages/server/src/request-tracing.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
import { Context, Effect, Scope, Tracer } from "effect"
|
||||||
|
|
||||||
|
export const withoutParentSpan = <A, E>(effect: Effect.Effect<A, E, Scope.Scope>) =>
|
||||||
|
effect.pipe(
|
||||||
|
Effect.updateContext((context: Context.Context<Scope.Scope>) => Context.omit(Tracer.ParentSpan)(context)),
|
||||||
|
)
|
||||||
48
packages/server/test/request-tracing.test.ts
Normal file
48
packages/server/test/request-tracing.test.ts
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
import { expect, test } from "bun:test"
|
||||||
|
import { Effect, Option, Scope, Tracer } from "effect"
|
||||||
|
import { HttpMiddleware, HttpServerRequest, HttpServerResponse } from "effect/unstable/http"
|
||||||
|
import { withoutParentSpan } from "../src/request-tracing"
|
||||||
|
|
||||||
|
test("requests ignore ambient parents and continue inbound trace context", async () => {
|
||||||
|
const spans: Tracer.NativeSpan[] = []
|
||||||
|
const tracer = Tracer.make({
|
||||||
|
span(options) {
|
||||||
|
const span = new Tracer.NativeSpan(options)
|
||||||
|
spans.push(span)
|
||||||
|
return span
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const app = Effect.gen(function* () {
|
||||||
|
yield* Scope.Scope
|
||||||
|
return yield* HttpMiddleware.tracer(Effect.succeed(HttpServerResponse.empty()))
|
||||||
|
})
|
||||||
|
const request = (traceparent?: string) =>
|
||||||
|
app.pipe(
|
||||||
|
Effect.provideService(
|
||||||
|
HttpServerRequest.HttpServerRequest,
|
||||||
|
HttpServerRequest.fromWeb(
|
||||||
|
new Request("http://localhost/trace", {
|
||||||
|
headers: traceparent === undefined ? undefined : { traceparent },
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
withoutParentSpan,
|
||||||
|
)
|
||||||
|
|
||||||
|
await Effect.runPromise(
|
||||||
|
Effect.gen(function* () {
|
||||||
|
yield* request()
|
||||||
|
yield* request()
|
||||||
|
yield* request("00-11111111111111111111111111111111-2222222222222222-01")
|
||||||
|
yield* Effect.yieldNow
|
||||||
|
}).pipe(Effect.withSpan("fixture.lifecycle"), Effect.provideService(Tracer.Tracer, tracer), Effect.scoped),
|
||||||
|
)
|
||||||
|
|
||||||
|
const requests = spans.filter((span) => span.kind === "server")
|
||||||
|
expect(requests).toHaveLength(3)
|
||||||
|
expect(requests[0]?.traceId).not.toBe(requests[1]?.traceId)
|
||||||
|
expect(Option.getOrUndefined(requests[0]?.parent ?? Option.none())).toBeUndefined()
|
||||||
|
expect(Option.getOrUndefined(requests[1]?.parent ?? Option.none())).toBeUndefined()
|
||||||
|
expect(requests[2]?.traceId).toBe("11111111111111111111111111111111")
|
||||||
|
expect(Option.getOrUndefined(requests[2]?.parent ?? Option.none())?.spanId).toBe("2222222222222222")
|
||||||
|
})
|
||||||
Loading…
Add table
Add a link
Reference in a new issue