Put the caller, site and route on every request's span and log line
CI / full-gate (push) Successful in 3m23s

This commit is contained in:
2026-11-08 14:59:55 +01:00
parent 996eef2dc5
commit a69593eb72
14 changed files with 275 additions and 68 deletions
+65
View File
@@ -0,0 +1,65 @@
import type { IncomingMessage } from "node:http";
import { BlockList, isIP } from "node:net";
export type TrustedProxies = BlockList;
// Each entry is an IP address or a CIDR range (`10.0.0.0/8`). Fails loud: a typo here would
// silently log a proxy's address for every caller, or trust a stranger's header.
export function parseTrustedProxies(entries: readonly string[] = []): TrustedProxies {
const list = new BlockList();
for (const entry of entries) {
const [address = "", prefix, ...rest] = entry.split("/");
const family = isIP(address);
const length = prefix === undefined ? undefined : Number(prefix);
const maxLength = family === 4 ? 32 : 128;
if (family === 0 || rest.length > 0 || (length !== undefined && !(Number.isInteger(length) && length >= 0 && length <= maxLength))) {
throw new Error(`config: TRUSTED_PROXIES entry "${entry}" is not an IP address or CIDR range`);
}
const kind = family === 4 ? "ipv4" : "ipv6";
if (length === undefined) list.addAddress(address, kind);
else list.addSubnet(address, length, kind);
}
return list;
}
// A dual-stack socket reports an IPv4 peer as `::ffff:a.b.c.d`; log the plain form.
const plain = (address: string): string => (address.startsWith("::ffff:") && isIP(address.slice(7)) === 4 ? address.slice(7) : address);
// The socket's peer, unless the peer is a listed proxy: then the nearest address in X-Forwarded-For
// that is not itself a listed proxy. Reading from the right is what makes a forged leftmost entry
// harmless — only a hop a trusted proxy appended is believed.
export function clientAddress(peer: string | undefined, forwardedFor: string | undefined, trusted: TrustedProxies): string | undefined {
if (peer === undefined) return undefined;
const isTrusted = (address: string): boolean => trusted.check(address, isIP(address) === 4 ? "ipv4" : "ipv6");
const direct = plain(peer);
if (!isTrusted(direct)) return direct;
const hops = (forwardedFor ?? "").split(",").map((hop) => plain(hop.trim())).filter(Boolean);
let client = direct;
for (let i = hops.length - 1; i >= 0; i--) {
const hop = hops[i]!;
if (isIP(hop) === 0) return direct;
client = hop;
if (!isTrusted(hop)) break;
}
return client;
}
// OpenTelemetry HTTP semantic-convention attributes every request carries on its span and `request`
// line. The path only: the query may carry tokens.
export function requestFields(req: IncomingMessage, trusted: TrustedProxies): Record<string, string> {
const fields: Record<string, string> = {
"http.request.method": req.method ?? "GET",
"url.path": (req.url ?? "/").split("?", 1)[0] ?? "/",
};
const client = clientAddress(req.socket.remoteAddress, headerValue(req.headers["x-forwarded-for"]), trusted);
if (client !== undefined) fields["client.address"] = client;
const host = headerValue(req.headers.host);
if (host !== undefined) {
try { fields["server.address"] = new URL(`http://${host}`).hostname; } catch { /* malformed Host: leave it off */ }
}
const agent = headerValue(req.headers["user-agent"]);
if (agent !== undefined) fields["user_agent.original"] = agent;
return fields;
}
const headerValue = (value: string | string[] | undefined): string | undefined => (Array.isArray(value) ? value.join(", ") : value);