diff --git a/packages/console/function/src/log-processor.ts b/packages/console/function/src/log-processor.ts index 25e1838ed77..65d6832c3ee 100644 --- a/packages/console/function/src/log-processor.ts +++ b/packages/console/function/src/log-processor.ts @@ -21,6 +21,7 @@ export default { ) continue + const ip = event.event.request.headers["x-real-ip"] let data: Record = { "cf.continent": event.event.request.cf?.continent, "cf.country": event.event.request.cf?.country, @@ -32,7 +33,8 @@ export default { duration: event.wallTime, request_length: parseInt(event.event.request.headers["content-length"] ?? "0"), status: event.event.response?.status ?? 0, - ip: event.event.request.headers["x-real-ip"], + ip, + "ip.prefix": ipPrefix(ip), } const time = new Date(event.eventTimestamp ?? Date.now()).toISOString() const events = [ @@ -110,6 +112,7 @@ function toLakeEvent(time: string, data: Record) { request_length: integer(data, "request_length"), status: integer(data, "status"), ip: string(data, "ip"), + ip_prefix: string(data, "ip.prefix"), is_stream: boolean(data, "is_stream"), session: string(data, "session"), request: string(data, "request"), @@ -146,15 +149,32 @@ function toLakeEvent(time: string, data: Record) { cost_cache_read_microcents: integer(data, "cost.cache_read.microcents"), cost_cache_write_microcents: integer(data, "cost.cache_write.microcents"), cost_total_microcents: integer(data, "cost.total.microcents"), - cost_input: integer(data, "cost.input"), - cost_output: integer(data, "cost.output"), - cost_cache_read: integer(data, "cost.cache_read"), - cost_cache_write_5m: integer(data, "cost.cache_write_5m"), - cost_cache_write_1h: integer(data, "cost.cache_write_1h"), - cost_total: integer(data, "cost.total"), } } +// Returns a stable lookup key for an IP address. +// IPv4: full address as /32 (e.g. "203.0.113.45/32"). +// IPv6: the /64 network prefix (e.g. "2001:db8:abcd:1234::/64"). ISPs commonly +// rotate the lower 64 host bits via SLAAC privacy extensions (RFC 8981), so +// grouping by /64 collapses those rotations into one key. +function ipPrefix(ip: string | undefined) { + if (!ip) return undefined + if (ip.includes(".") && !ip.includes(":")) return `${ip}/32` + if (!ip.includes(":")) return undefined + + // Expand "::" to its full form, then keep the first 4 hextets. + const [head, tail] = ip.split("::") as [string, string | undefined] + const headParts = head ? head.split(":") : [] + const tailParts = tail !== undefined ? tail.split(":") : [] + const missing = 8 - headParts.length - tailParts.length + if (missing < 0) return undefined + const full = [...headParts, ...new Array(missing).fill("0"), ...tailParts] + if (full.length !== 8) return undefined + + const prefix = full.slice(0, 4).map((part) => part.toLowerCase().replace(/^0+(?=.)/, "")).join(":") + return `${prefix}::/64` +} + function string(data: Record, key: string) { const value = data[key] if (typeof value === "string") return value