export const api = (path, body) =>
send(path, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(body),
})
export const get = (path) => send(path, {})
export const REPLAY = typeof __REPLAY__ !== 'undefined' && __REPLAY__
async function send(path, init) {
if (REPLAY) {
const { replay } = await import('./replay.js')
return replay(path, init.body ? JSON.parse(init.body) : {})
}
let r
try {
r = await fetch(path, init)
} catch {
throw new Error(
`Cannot reach ${location.origin}. This page came from Mira, so the ` +
`likely cause is that the server has stopped — restart it and reload.`,
)
}
const text = await r.text()
let j
try {
j = JSON.parse(text)
} catch {
throw new Error(text || `HTTP ${r.status} ${r.statusText}`)
}
if (!r.ok) throw new Error(`HTTP ${r.status}: ${j.error || r.statusText}`)
return j
}
export const bounds = (range) =>
range === 'all' ? { from: 0, to: 'now' } : { from: range, to: 'now' }
export const FIELDS = {
logs: ['time_unix_nano', 'observed_time_unix_nano', 'severity_number',
'severity_text', 'event_name', 'body', 'trace_id', 'span_id', 'flags',
'dropped_attributes_count'],
traces: ['trace_id', 'span_id', 'parent_span_id', 'trace_state', 'flags',
'name', 'kind', 'start_time_unix_nano', 'duration_nano', 'status_code',
'status_message', 'dropped_attributes_count', 'dropped_events_count',
'dropped_links_count'],
metrics: [],
}
const OPS = [['>=', 'gte'], ['<=', 'lte'], ['!=', 'ne'], ['~', 'contains'],
['>', 'gt'], ['<', 'lt'], ['=', 'eq']]
export const FREE_TEXT = { logs: 'body', traces: 'name' }
const unquote = (raw) =>
raw.length > 1 && raw[0] === '"' && raw.endsWith('"') ? raw.slice(1, -1) : raw
const isId = (key) => key.endsWith('_id') || key.endsWith('.id')
function coerce(raw, key) {
if (raw !== unquote(raw)) return unquote(raw)
if (raw === 'true') return true
if (raw === 'false') return false
if (isId(key)) return raw
if (/^-?\d+$/.test(raw)) return Number(raw)
if (/^-?\d*\.\d+$/.test(raw)) return Number(raw)
return raw
}
export function parseFilter(text, signal) {
const terms = []
for (const tok of text.match(/(?:[^\s"]|"[^"]*")+/g) || []) {
const hit = OPS.map(([sym, op]) => [tok.indexOf(sym), sym, op])
.filter(([i]) => i > 0)
.sort((a, b) => a[0] - b[0] || b[1].length - a[1].length)[0]
if (!hit) {
const free = OPS.some(([sym]) => tok.includes(sym)) ? null : FREE_TEXT[signal]
if (!free) throw new Error(`\`${tok}\` needs an operator: = != ~ > >= < <=`)
terms.push({ field: free, contains: unquote(tok) })
continue
}
const [i, sym, op] = hit
let key = tok.slice(0, i)
let target = 'attr'
if (key.startsWith('field:') || key.startsWith('attr:')) {
;[target, key] = key.split(':')
} else if ((FIELDS[signal] || []).includes(key)) {
target = 'field'
}
terms.push({ [target]: key, [op]: coerce(tok.slice(i + sym.length), key) })
}
return terms
}
export function term(key, value) {
if (typeof value === 'number' || typeof value === 'boolean') return `${key}=${value}`
const s = String(value)
return s.includes('"') ? `${key}~"${s.split('"')[0]}"` : `${key}="${s}"`
}
export const addTerm = (q, t) => {
const cur = (q || '').trim()
return !cur ? t : cur.split(/\s+/).includes(t) ? cur : `${cur} ${t}`
}
export const serviceOf = (q) => ((q || '').match(/service\.name="?([^"\s]+)"?/) || [])[1] || ''
export function withService(q, name) {
const rest = (q || '')
.split(/\s+/)
.filter((t) => t && !t.startsWith('service.name='))
.join(' ')
return (name ? `${rest} service.name="${name}"` : rest).trim()
}
export function layer(nodes, edges) {
const depth = new Map(nodes.map((n) => [n.key, n.key === 'entry' ? 0 : 1]))
for (let i = 0; i < nodes.length; i++) {
let moved = false
for (const e of edges) {
if (!depth.has(e.from) || !depth.has(e.to)) continue
const d = depth.get(e.from) + 1
if (d > depth.get(e.to)) { depth.set(e.to, d); moved = true }
}
if (!moved) break
}
return depth
}
export const num = (v) => (v === null || v === undefined ? null : Number(v))
export function fmtTime(ns) {
const d = new Date(Number(ns) / 1e6)
const p = (n, w = 2) => String(n).padStart(w, '0')
return `${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:` +
`${p(d.getMinutes())}:${p(d.getSeconds())}.${p(d.getMilliseconds(), 3)}`
}
export function fmtDur(v) {
const ns = num(v)
if (ns === null || !Number.isFinite(ns)) return ''
if (ns < 1e3) return `${ns}ns`
if (ns < 1e6) return `${(ns / 1e3).toFixed(1)}µs`
if (ns < 1e9) return `${(ns / 1e6).toFixed(2)}ms`
return `${(ns / 1e9).toFixed(3)}s`
}
export function fmtWindow(ns) {
const s = Math.round(num(ns) / 1e9)
if (s >= 3600 && s % 3600 === 0) return `${s / 3600}h`
if (s >= 60 && s % 60 === 0) return `${s / 60}m`
return `${s}s`
}
export const fmtAlert = (metric, v) =>
metric === 'ratio' ? (num(v) * 100).toFixed(2) + '%' : String(num(v))
const SEV = ['trace', 'debug', 'info', 'warn', 'error', 'fatal']
export const sevName = (row) =>
(row.severity_text || SEV[Math.floor((row.severity_number - 1) / 4)] || '').toLowerCase()
export const fmtValue = (v) =>
v !== null && typeof v === 'object' ? JSON.stringify(v) : String(v ?? '')
export const svc = (row) => (row.attributes && row.attributes['service.name']) || ''
export const STATUS = ['', 'OK', 'ERROR']
export const COLORS = ['#58a6ff', '#3fb950', '#d29922', '#f85149', '#bc8cff',
'#39c5cf', '#ff7b72', '#a5d6ff']