Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.
| Projects have Observability: their requests, logs, traces, metrics and errors, from apps on g1t.page with nothing to set up, and from software anywhere else over OpenTelemetry. | 1 | /** |
| 2 | * Small readings shared by both doors: routes with their ids taken out, | |
| 3 | * severities, ids, and text kept to a size. | |
| 4 | */ | |
| 5 | import type { LogSeverity } from "@g1t/contracts/telemetry"; | |
| 6 | ||
| 7 | /** A log body or a message is kept to this many characters. */ | |
| 8 | export const MAX_TEXT = 16_384; | |
| 9 | /** An attribute's string value, to this many. */ | |
| 10 | export const MAX_VALUE = 4_096; | |
| 11 | ||
| 12 | export function clip(text: string, max = MAX_TEXT): string { | |
| 13 | return text.length <= max ? text : `${text.slice(0, max)}… (${text.length - max} more characters)`; | |
| 14 | } | |
| 15 | ||
| 16 | const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; | |
| 17 | const NUMBER = /^\d+$/; | |
| 18 | const HEXISH = /^[0-9a-f]{12,}$/i; | |
| 19 | /** Long tokens that mix letters and digits: slugs with ids, base64, ulids. */ | |
| 20 | const TOKENISH = /^(?=.*\d)(?=.*[a-z])[a-z0-9_-]{16,}$/i; | |
| 21 | ||
| 22 | /** | |
| 23 | * The path with its ids taken out, so `/users/42` and `/users/43` count | |
| 24 | * as one route: `/users/:id`. Files keep their names; the query goes. | |
| 25 | */ | |
| 26 | export function route(path: string): string { | |
| 27 | const bare = path.split(/[?#]/)[0] || "/"; | |
| 28 | const parts = bare.split("/").map((part) => { | |
| 29 | if (part === "") return part; | |
| 30 | let decoded = part; | |
| 31 | try { | |
| 32 | decoded = decodeURIComponent(part); | |
| 33 | } catch {} | |
| 34 | if (NUMBER.test(decoded) || UUID.test(decoded) || HEXISH.test(decoded) || TOKENISH.test(decoded)) return ":id"; | |
| 35 | return part; | |
| 36 | }); | |
| 37 | const joined = parts.join("/"); | |
| 38 | return joined.length > 200 ? `${joined.slice(0, 200)}…` : joined; | |
| 39 | } | |
| 40 | ||
| 41 | /** Host and path, without the query or the scheme. */ | |
| 42 | export function hostPath(url: string): { url: string; path: string } | null { | |
| 43 | try { | |
| 44 | const u = new URL(url); | |
| 45 | return { url: `${u.host}${u.pathname}`, path: u.pathname }; | |
| 46 | } catch { | |
| 47 | return null; | |
| 48 | } | |
| 49 | } | |
| 50 | ||
| 51 | /** OpenTelemetry's severity number (1–24) for a console level or a severity text. */ | |
| 52 | export function severityNumber(level: string | undefined | null): number { | |
| 53 | switch ((level ?? "").toLowerCase()) { | |
| 54 | case "trace": | |
| 55 | return 1; | |
| 56 | case "debug": | |
| 57 | return 5; | |
| 58 | case "log": | |
| 59 | case "info": | |
| 60 | case "information": | |
| 61 | case "notice": | |
| 62 | return 9; | |
| 63 | case "warn": | |
| 64 | case "warning": | |
| 65 | return 13; | |
| 66 | case "error": | |
| 67 | case "err": | |
| 68 | return 17; | |
| 69 | case "fatal": | |
| 70 | case "critical": | |
| 71 | case "crit": | |
| 72 | case "emergency": | |
| 73 | case "alert": | |
| 74 | return 21; | |
| 75 | default: | |
| 76 | return 9; | |
| 77 | } | |
| 78 | } | |
| 79 | ||
| 80 | /** The level a severity number falls in. 0 (unset) reads as info. */ | |
| 81 | export function severityOf(n: number): LogSeverity { | |
| 82 | if (n >= 21) return "fatal"; | |
| 83 | if (n >= 17) return "error"; | |
| 84 | if (n >= 13) return "warn"; | |
| 85 | if (n >= 9 || n <= 0) return "info"; | |
| 86 | if (n >= 5) return "debug"; | |
| 87 | return "trace"; | |
| 88 | } | |
| 89 | ||
| 90 | /** Lowest severity number of each level, to filter by. */ | |
| 91 | export const SEVERITY_FLOOR: Record<LogSeverity, [number, number]> = { | |
| 92 | trace: [1, 4], | |
| 93 | debug: [5, 8], | |
| 94 | info: [9, 12], | |
| 95 | warn: [13, 16], | |
| 96 | error: [17, 20], | |
| 97 | fatal: [21, 24], | |
| 98 | }; | |
| 99 | ||
| 100 | /** A random id of `bytes` bytes, as hex: trace ids are 16, span and request ids 8. */ | |
| 101 | export function randomId(bytes = 8): string { | |
| 102 | return [...crypto.getRandomValues(new Uint8Array(bytes))].map((b) => b.toString(16).padStart(2, "0")).join(""); | |
| 103 | } | |
| 104 | ||
| 105 | /** A lowercase hex id, or null for one that is empty, all zeros, or not hex. */ | |
| 106 | export function hexId(id: string | undefined | null, length: number): string | null { | |
| 107 | if (!id) return null; | |
| 108 | let hex = id.toLowerCase(); | |
| 109 | // OTLP/JSON writes ids as hex, but some exporters send base64. | |
| 110 | if (!/^[0-9a-f]+$/.test(hex)) { | |
| 111 | try { | |
| 112 | hex = [...atob(id)].map((c) => c.charCodeAt(0).toString(16).padStart(2, "0")).join(""); | |
| 113 | } catch { | |
| 114 | return null; | |
| 115 | } | |
| 116 | } | |
| 117 | if (hex.length !== length || /^0+$/.test(hex)) return null; | |
| 118 | return hex; | |
| 119 | } | |
| 120 | ||
| 121 | /** Nanoseconds (as OTLP writes them, a decimal string or a number) to microseconds. */ | |
| 122 | export function nanosToMicros(nanos: string | number | undefined | null): number | null { | |
| 123 | if (nanos === undefined || nanos === null || nanos === "" || nanos === "0" || nanos === 0) return null; | |
| 124 | try { | |
| 125 | const n = typeof nanos === "number" ? BigInt(Math.trunc(nanos)) : BigInt(nanos); | |
| 126 | return Number(n / 1000n); | |
| 127 | } catch { | |
| 128 | return null; | |
| 129 | } | |
| 130 | } | |
| 131 | ||
| 132 | /** Anything a console call was given, as one line of text. */ | |
| 133 | export function text(value: unknown): string { | |
| 134 | if (typeof value === "string") return value; | |
| 135 | if (Array.isArray(value)) return value.map((v) => (typeof v === "string" ? v : safeJson(v))).join(" "); | |
| 136 | return safeJson(value); | |
| 137 | } | |
| 138 | ||
| 139 | function safeJson(value: unknown): string { | |
| 140 | try { | |
| 141 | return JSON.stringify(value) ?? String(value); | |
| 142 | } catch { | |
| 143 | return String(value); | |
| 144 | } | |
| 145 | } |