better otel i think
This commit is contained in:
1 parent
6edbdc340d
commit
8f670dfa2f
16 files changed
+1129
-675
No files matched your search
@@ -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)
|
||||
}
|
||||
@@ -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({
|
||||
|
||||
Reference in new issue
Block a user