forked from ericc-ch/copilot-api
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathrequest-logger.ts
More file actions
89 lines (73 loc) · 2.65 KB
/
request-logger.ts
File metadata and controls
89 lines (73 loc) · 2.65 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
import type { MiddlewareHandler } from "hono"
import consola from "consola"
import { randomUUID } from "node:crypto"
import { state } from "./state"
const VERBOSE_BODY_TAIL = 2000
function formatHeaders(headers: Headers): string {
const entries: Array<string> = []
for (const [key, value] of headers.entries()) {
// Mask authorization tokens, show only last 8 chars
const safeValue =
key.toLowerCase() === "authorization" && value.length > 12 ?
`***${value.slice(-8)}`
: value
entries.push(` ${key}: ${safeValue}`)
}
return entries.join("\n")
}
function tailOfBody(body: string): string {
if (body.length <= VERBOSE_BODY_TAIL) return body
return `...[truncated ${body.length - VERBOSE_BODY_TAIL} chars]...\n${body.slice(-VERBOSE_BODY_TAIL)}`
}
function resolveModelLabel(modelId: string | undefined): string {
if (!modelId) return ""
const model = state.models?.data.find((m) => m.id === modelId)
if (model?.name && model.name !== modelId)
return ` | model: ${modelId} (${model.name})`
return ` | model: ${modelId}`
}
export const requestLogger: MiddlewareHandler = async (c, next) => {
const requestId = c.req.header("x-request-id") ?? randomUUID()
const userAgent = c.req.header("user-agent") ?? "unknown"
const method = c.req.method
const path = c.req.path
const clientIp =
c.req.header("x-forwarded-for")?.split(",")[0]?.trim()
?? c.req.header("x-real-ip")
?? "unknown"
// Set request ID on response for traceability
c.header("x-request-id", requestId)
const tag = requestId.slice(0, 8)
const start = performance.now()
consola.info(
`→ [${tag}] ${method} ${path} | agent: ${userAgent} | ip: ${clientIp}`,
)
// Extract model from request body for log enrichment (non-destructive)
let modelLabel = ""
if (method === "POST") {
try {
const cloned = c.req.raw.clone()
const body = (await cloned.json()) as { model?: string }
modelLabel = resolveModelLabel(body.model)
} catch {
// body not JSON or unreadable — skip
}
}
// Verbose mode: log headers and body tail (only at debug level / --verbose)
if (consola.level >= 5) {
consola.debug(`→ [${tag}] Headers:\n${formatHeaders(c.req.raw.headers)}`)
if (method !== "GET" && method !== "HEAD") {
try {
const body = await c.req.text()
consola.debug(`→ [${tag}] Body (tail):\n${tailOfBody(body)}`)
} catch {
consola.debug(`→ [${tag}] Body: <unable to read>`)
}
}
}
await next()
const duration = (performance.now() - start).toFixed(1)
consola.info(
`← [${tag}] ${method} ${path} | ${c.res.status} | ${duration}ms${modelLabel}`,
)
}