better otel i think
Build and Publish / Metadata (push) Successful in 2s
Build and Publish / BuildAndDeployAmd64 (push) Successful in 1m26s

This commit is contained in:
mats committed 2026-09-19 19:14:51 +02:00
1 parent 6edbdc340d
commit 8f670dfa2f
16 files changed
+1129 -675

No files matched your search

+70
View File
@@ -0,0 +1,70 @@
/**
* Helpers for making log bodies/attributes readable: expanding JSON that was
* encoded inside strings, and reducing a message to a template so similar
* records can be grouped.
*/
const looksLikeJson = (s: string) => {
const t = s.trim()
return (t.startsWith("{") && t.endsWith("}")) || (t.startsWith("[") && t.endsWith("]"))
}
/** Recursively parses strings that contain JSON ("JSON in JSON"), at any depth. */
export const deepParse = (value: unknown, depth = 0): unknown => {
if (depth > 8) return value
if (typeof value === "string") {
if (!looksLikeJson(value)) return value
try {
return deepParse(JSON.parse(value), depth + 1)
} catch {
return value
}
}
if (Array.isArray(value)) return value.map((v) => deepParse(v, depth + 1))
if (value && typeof value === "object") {
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, deepParse(v, depth + 1)]))
}
return value
}
/** Parsed structure for a string when it is (nested) JSON, otherwise undefined. */
export const parseStructured = (s: string): object | undefined => {
if (!looksLikeJson(s)) return undefined
try {
const parsed = deepParse(JSON.parse(s))
return parsed && typeof parsed === "object" ? parsed : undefined
} catch {
return undefined
}
}
const MESSAGE_KEYS = ["message", "msg", "event", "error", "err"]
/** A one-line summary for list rows: the message field of a JSON body, or the raw text. */
export const bodyHeadline = (body: string): string => {
const parsed = parseStructured(body)
if (parsed && !Array.isArray(parsed)) {
const obj = parsed as Record<string, unknown>
for (const k of MESSAGE_KEYS) if (typeof obj[k] === "string" && obj[k]) return obj[k] as string
}
return body
}
/** Message with volatile parts (ids, numbers, quoted values) replaced by placeholders. */
export const messageTemplate = (body: string): string => {
const parsed = parseStructured(body)
let text = body
if (parsed) {
const headline = bodyHeadline(body)
text = headline !== body ? headline : Array.isArray(parsed) ? "[array]" : `{${Object.keys(parsed).sort().join(",")}}`
}
return text
.replace(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, "<uuid>")
.replace(/\b0x[0-9a-f]+\b|\b[0-9a-f]{16,}\b/gi, "<hex>")
.replace(/https?:\/\/\S+/g, "<url>")
.replace(/"[^"]*"|'[^']*'/g, "<str>")
.replace(/\d+(\.\d+)?/g, "<n>")
.replace(/\s+/g, " ")
.trim()
.slice(0, 200)
}
+21 -10
View File
@@ -106,8 +106,8 @@ interface RawFrontendRecord {
release?: string
environment?: string
path?: string
vitalName?: string
vitalValue?: number
/** Metric values from a `page_vitals` beacon record (one per page view snapshot). */
vitals?: Record<string, number>
isError: boolean
isHttpFailure: boolean
exceptionType?: string
@@ -121,6 +121,20 @@ interface RawFrontendRecord {
breadcrumbs: ParsedBreadcrumb[]
}
/** Reads `{metrics: {LCP: {value}}}` from a page_vitals record body into name → value. */
function parseVitals(body?: string): Record<string, number> {
const out: Record<string, number> = {}
try {
const metrics = (JSON.parse(body ?? "") as { metrics?: Record<string, { value?: number }> }).metrics ?? {}
for (const [name, m] of Object.entries(metrics)) {
if (typeof m?.value === "number") out[name.toUpperCase()] = m.value
}
} catch {
/* not a vitals body */
}
return out
}
function extractFrontendRecords(logs: LogEntry[]): RawFrontendRecord[] {
const out: RawFrontendRecord[] = []
@@ -134,13 +148,13 @@ function extractFrontendRecords(logs: LogEntry[]): RawFrontendRecord[] {
;(sl.logRecords ?? []).forEach((r) => {
const attrs = r.attributes ?? []
const source = getAttributeValue(attrs, "source")
const vitalName = getAttributeValue(attrs, "rum.vital")
const isVitals = getAttributeValue(attrs, "rum.event") === "page_vitals"
const browserUrl = getAttributeValue(attrs, "browser.url")
const sessionId = getAttributeValue(attrs, "session.id")
const isRumScope = scopeName === "otel-rum"
const isFrontend =
source === "frontend" || isRumScope || Boolean(vitalName) || Boolean(browserUrl) || Boolean(sessionId)
source === "frontend" || isRumScope || isVitals || Boolean(browserUrl) || Boolean(sessionId)
if (!isFrontend) return
let path: string | undefined
@@ -157,7 +171,6 @@ function extractFrontendRecords(logs: LogEntry[]): RawFrontendRecord[] {
const sevNum = r.severityNumber ?? 0
const isErr = (sevNum >= 17 || excType !== "") && !isHttp
const vitalValStr = getAttributeValue(attrs, "rum.value")
const httpStatusStr = getAttributeValue(attrs, "http.response.status_code")
out.push({
@@ -168,8 +181,7 @@ function extractFrontendRecords(logs: LogEntry[]): RawFrontendRecord[] {
release: getAttributeValue(attrs, "service.release") || undefined,
environment: getAttributeValue(attrs, "service.environment") || undefined,
path,
vitalName: vitalName || undefined,
vitalValue: vitalValStr ? parseFloat(vitalValStr) : undefined,
vitals: isVitals ? parseVitals(r.body?.stringValue) : undefined,
isError: isErr,
isHttpFailure: isHttp,
exceptionType: excType || undefined,
@@ -227,9 +239,8 @@ export function buildRumSessions(logs: LogEntry[]): RumSession[] {
seenUrls.add(r.path)
urls.push(r.path)
}
if (r.vitalName && r.vitalValue !== undefined) {
vitals[r.vitalName.toUpperCase()] = r.vitalValue
}
// Each beacon is a full snapshot of one page view; later snapshots win.
if (r.vitals) Object.assign(vitals, r.vitals)
if (r.isError) {
errorCount += 1
errors.push({