feat(server): auto-tag route spans with route params (session.id, message.id, …) (#23189)

This commit is contained in:
Kit Langton
2026-04-17 18:43:10 -04:00
committed by opencode
parent a35b8a95c2
commit d5c4c26b4b
2 changed files with 76 additions and 11 deletions

View File

@@ -4,18 +4,31 @@ import { AppRuntime } from "@/effect/app-runtime"
type AppEnv = Parameters<typeof AppRuntime.runPromise>[0] extends Effect.Effect<any, any, infer R> ? R : never
// Build the base span attributes for an HTTP handler: method, path, and every
// matched route param (sessionID, messageID, partID, providerID, ptyID, …)
// prefixed with `opencode.`. This makes each request's root span searchable
// by ID in motel without having to parse the path string.
export interface RequestLike {
readonly req: {
readonly method: string
readonly url: string
param(): Record<string, string>
}
}
export function requestAttributes(c: RequestLike): Record<string, string> {
const attributes: Record<string, string> = {
"http.method": c.req.method,
"http.path": new URL(c.req.url).pathname,
}
for (const [key, value] of Object.entries(c.req.param())) {
attributes[`opencode.${key}`] = value
}
return attributes
}
export function runRequest<A, E>(name: string, c: Context, effect: Effect.Effect<A, E, AppEnv>) {
const url = new URL(c.req.url)
return AppRuntime.runPromise(
effect.pipe(
Effect.withSpan(name, {
attributes: {
"http.method": c.req.method,
"http.path": url.pathname,
},
}),
),
)
return AppRuntime.runPromise(effect.pipe(Effect.withSpan(name, { attributes: requestAttributes(c) })))
}
export async function jsonRequest<C extends Context, A, E>(

View File

@@ -0,0 +1,52 @@
import { describe, expect, test } from "bun:test"
import { requestAttributes } from "../../src/server/routes/instance/trace"
function fakeContext(method: string, url: string, params: Record<string, string>) {
return {
req: {
method,
url,
param: () => params,
},
}
}
describe("requestAttributes", () => {
test("includes http method and path", () => {
const attrs = requestAttributes(fakeContext("GET", "http://localhost/session", {}))
expect(attrs["http.method"]).toBe("GET")
expect(attrs["http.path"]).toBe("/session")
})
test("strips query string from path", () => {
const attrs = requestAttributes(fakeContext("GET", "http://localhost/file/search?query=foo&limit=10", {}))
expect(attrs["http.path"]).toBe("/file/search")
})
test("tags route params with opencode.<param> prefix", () => {
const attrs = requestAttributes(
fakeContext("GET", "http://localhost/session/ses_abc/message/msg_def/part/prt_ghi", {
sessionID: "ses_abc",
messageID: "msg_def",
partID: "prt_ghi",
}),
)
expect(attrs["opencode.sessionID"]).toBe("ses_abc")
expect(attrs["opencode.messageID"]).toBe("msg_def")
expect(attrs["opencode.partID"]).toBe("prt_ghi")
})
test("produces no param attributes when no params are matched", () => {
const attrs = requestAttributes(fakeContext("POST", "http://localhost/config", {}))
expect(Object.keys(attrs).filter((k) => k.startsWith("opencode."))).toEqual([])
})
test("handles non-ID params (e.g. mcp :name) without mangling", () => {
const attrs = requestAttributes(
fakeContext("POST", "http://localhost/mcp/exa/connect", {
name: "exa",
}),
)
expect(attrs["opencode.name"]).toBe("exa")
})
})