import test from 'node:test'
import assert from 'node:assert/strict'
import {
addTerm, api, fmtAlert, fmtDur, fmtTime, fmtValue, fmtWindow, get, layer, num, parseFilter,
serviceOf,
term, withService,
} from './api.js'
test('operators split longest-first and route to field or attr', () => {
assert.deepEqual(
parseFilter('service.name=checkout severity_number>=17 http.route~/api', 'logs'),
[
{ attr: 'service.name', eq: 'checkout' },
{ field: 'severity_number', gte: 17 },
{ attr: 'http.route', contains: '/api' },
],
)
assert.deepEqual(parseFilter('k!=v', 'logs'), [{ attr: 'k', ne: 'v' }])
assert.deepEqual(parseFilter('name=checkout', 'traces'), [{ field: 'name', eq: 'checkout' }])
assert.deepEqual(parseFilter('name=checkout', 'logs'), [{ attr: 'name', eq: 'checkout' }])
assert.deepEqual(parseFilter('field:name=x', 'logs'), [{ field: 'name', eq: 'x' }])
})
test('a word with no operator searches the signal message column', () => {
assert.deepEqual(parseFilter('refused', 'logs'), [{ field: 'body', contains: 'refused' }])
assert.deepEqual(parseFilter('checkout', 'traces'), [{ field: 'name', contains: 'checkout' }])
assert.deepEqual(
parseFilter('"connection refused" a=b', 'logs'),
[{ field: 'body', contains: 'connection refused' }, { attr: 'a', eq: 'b' }],
)
assert.deepEqual(parseFilter('500', 'logs'), [{ field: 'body', contains: '500' }])
})
test('ids stay strings however numeric they look', () => {
assert.deepEqual(
parseFilter('span_id=0000000000001234', 'logs'),
[{ field: 'span_id', eq: '0000000000001234' }],
)
assert.deepEqual(
parseFilter('trace_id=00000000000002705555555555555725', 'traces'),
[{ field: 'trace_id', eq: '00000000000002705555555555555725' }],
)
assert.deepEqual(parseFilter('peer.id=42', 'logs'), [{ attr: 'peer.id', eq: '42' }])
assert.deepEqual(parseFilter('attr:span_id=42', 'traces'), [{ attr: 'span_id', eq: '42' }])
assert.deepEqual(parseFilter('http.status_code=500', 'logs'), [
{ attr: 'http.status_code', eq: 500 },
])
})
test('event_name and the dropped counts are columns, not attributes', () => {
assert.deepEqual(parseFilter('event_name=user.login', 'logs'), [
{ field: 'event_name', eq: 'user.login' },
])
assert.deepEqual(parseFilter('dropped_events_count>0', 'traces'), [
{ field: 'dropped_events_count', gt: 0 },
])
})
test('a nested AnyValue survives rendering', () => {
assert.equal(fmtValue({ role: 'user', content: 'hello' }), '{"role":"user","content":"hello"}')
assert.equal(fmtValue(['mira', '--data-dir']), '["mira","--data-dir"]')
assert.equal(fmtValue('connection refused'), 'connection refused')
assert.equal(fmtValue(0), '0')
assert.equal(fmtValue(false), 'false')
assert.equal(fmtValue(undefined), '')
assert.equal(fmtValue(null), '')
})
test('what cannot be placed is an error, never a dropped term', () => {
assert.throws(() => parseFilter('refused', 'metrics'), /needs an operator/)
assert.throws(() => parseFilter('=v', 'logs'), /needs an operator/)
})
test('64-bit integers arrive as strings and still render', () => {
const ns = '1757462096123456789'
assert.equal(fmtTime(ns), fmtTime(Number(ns)))
assert.match(fmtTime(ns), /^\d\d-\d\d \d\d:\d\d:\d\d\.\d\d\d$/)
assert.equal(fmtDur('500'), '500ns')
assert.equal(fmtDur('1500'), '1.5µs')
assert.equal(fmtDur('2500000'), '2.50ms')
assert.equal(fmtDur('3000000000'), '3.000s')
assert.equal(fmtDur(undefined), '')
assert.equal(fmtDur(null), '')
assert.equal(num('42'), 42)
assert.equal(num(0), 0)
assert.equal(num(null), null)
assert.equal(num(undefined), null)
})
test('a value clicked out of a detail pane round-trips into the filter', () => {
const round = (k, v, signal) => parseFilter(term(k, v), signal)
assert.deepEqual(round('service.name', 'checkout', 'logs'), [
{ attr: 'service.name', eq: 'checkout' },
])
assert.deepEqual(round('severity_number', 17, 'logs'), [{ field: 'severity_number', eq: 17 }])
assert.deepEqual(round('flags', 0, 'logs'), [{ field: 'flags', eq: 0 }])
assert.deepEqual(round('feature.on', true, 'logs'), [{ attr: 'feature.on', eq: true }])
assert.deepEqual(round('body', 'connection refused', 'logs'), [
{ field: 'body', eq: 'connection refused' },
])
assert.deepEqual(round('span_id', '0000000000001234', 'traces'), [
{ field: 'span_id', eq: '0000000000001234' },
])
assert.deepEqual(round('body', 'said "no" twice', 'logs'), [{ field: 'body', contains: 'said ' }])
})
test('adding a term never rewrites what is already in the box', () => {
assert.equal(addTerm('', 'a="b"'), 'a="b"')
assert.equal(addTerm(undefined, 'a="b"'), 'a="b"')
assert.equal(addTerm('x=1', 'a="b"'), 'x=1 a="b"')
assert.equal(addTerm('x=1 a="b"', 'a="b"'), 'x=1 a="b"')
})
test('the service picker replaces its own term rather than anding a second', () => {
assert.equal(withService('service.name="a"', 'b'), 'service.name="b"')
assert.equal(withService('body~timeout service.name="a"', 'b'), 'body~timeout service.name="b"')
assert.equal(withService('body~timeout', 'b'), 'body~timeout service.name="b"')
assert.equal(withService('', 'b'), 'service.name="b"')
assert.equal(withService('body~timeout service.name="a"', ''), 'body~timeout')
assert.equal(serviceOf('body~x service.name="checkout"'), 'checkout')
assert.equal(serviceOf('service.name=checkout body~x'), 'checkout')
assert.equal(serviceOf('body~x'), '')
assert.equal(serviceOf(withService('a=1', 'gw')), 'gw')
})
test('the service map lays out cycles instead of hanging on them', () => {
const n = (...keys) => keys.map((key) => ({ key }))
const e = (...pairs) => pairs.map(([from, to]) => ({ from, to }))
const chain = layer(n('entry', 'a', 'b', 'c'), e(['entry', 'a'], ['a', 'b'], ['b', 'c']))
assert.deepEqual([...chain.values()], [0, 1, 2, 3])
const diamond = layer(
n('entry', 'a', 'b', 'c'),
e(['entry', 'a'], ['a', 'b'], ['a', 'c'], ['b', 'c']),
)
assert.equal(diamond.get('c'), 3)
const cyc = layer(n('entry', 'a', 'b'), e(['entry', 'a'], ['a', 'b'], ['b', 'a']))
assert.equal(cyc.size, 3)
for (const d of cyc.values()) assert.ok(Number.isFinite(d))
const partial = layer(n('entry', 'a'), e(['entry', 'a'], ['a', 'ghost'], ['ghost', 'a']))
assert.deepEqual([...partial.keys()], ['entry', 'a'])
})
test('a failed request says which kind of failure it was', async () => {
const origin = 'http://127.0.0.1:4318'
globalThis.location = { origin }
const stub = (fn) => { globalThis.fetch = fn }
stub(() => Promise.reject(new TypeError('Failed to fetch')))
await assert.rejects(api('/api/v1/query', {}), (e) => {
assert.match(e.message, /Cannot reach http:\/\/127\.0\.0\.1:4318/)
assert.match(e.message, /server has stopped/)
return true
})
stub(() => Promise.resolve(new Response('{"error":"unknown field \\"svc\\""}', { status: 400 })))
await assert.rejects(api('/api/v1/query', {}), /^Error: HTTP 400: unknown field "svc"$/)
stub(() => Promise.resolve(new Response('query task panicked', { status: 500 })))
await assert.rejects(api('/api/v1/query', {}), /query task panicked/)
stub(() => Promise.resolve(new Response('{"rows":[]}', { status: 200 })))
assert.deepEqual(await api('/api/v1/query', {}), { rows: [] })
})
test('a rule window reads back as the duration the rules file spelled', () => {
assert.equal(fmtWindow('60000000000'), '1m')
assert.equal(fmtWindow('300000000000'), '5m')
assert.equal(fmtWindow('3600000000000'), '1h')
assert.equal(fmtWindow('7200000000000'), '2h')
assert.equal(fmtWindow('30000000000'), '30s')
assert.equal(fmtWindow('90000000000'), '90s')
assert.equal(fmtWindow('5400000000000'), '90m')
assert.equal(fmtWindow('0'), '0s')
})
test('a GET carries no body and fails the same way a query does', async () => {
globalThis.location = { origin: 'http://127.0.0.1:4318' }
let seen = null
globalThis.fetch = (path, init) => {
seen = { path, init }
return Promise.resolve(new Response('{"alerts":[]}', { status: 200 }))
}
assert.deepEqual(await get('/api/v1/alerts'), { alerts: [] })
assert.equal(seen.path, '/api/v1/alerts')
assert.deepEqual(seen.init, {})
globalThis.fetch = () => Promise.reject(new TypeError('Load failed'))
await assert.rejects(get('/api/v1/alerts'), /server has stopped/)
})
test('the filter an alert reports parses back into the terms it counted', () => {
assert.deepEqual(parseFilter('field:status_code=2', 'traces'), [
{ field: 'status_code', eq: 2 },
])
assert.deepEqual(
parseFilter('attr:service.name=checkout field:duration_nano>250000000', 'traces'),
[{ attr: 'service.name', eq: 'checkout' }, { field: 'duration_nano', gt: 250000000 }],
)
assert.deepEqual(parseFilter('attr:exception.type=payments.CardDeclined', 'traces'), [
{ attr: 'exception.type', eq: 'payments.CardDeclined' },
])
assert.deepEqual(
parseFilter('attr:service.name=inventory field:severity_number>=21', 'logs'),
[{ attr: 'service.name', eq: 'inventory' }, { field: 'severity_number', gte: 21 }],
)
assert.deepEqual(parseFilter('field:name="GET /health"', 'traces'), [
{ field: 'name', eq: 'GET /health' },
])
})
test('a ratio rule reads as a percentage and a count rule does not', () => {
assert.equal(fmtAlert('ratio', 0.0481), '4.81%')
assert.equal(fmtAlert('ratio', 0.02), '2.00%')
assert.equal(fmtAlert('ratio', 0), '0.00%')
assert.equal(fmtAlert('ratio', 1), '100.00%')
assert.equal(fmtAlert('count', 20), '20')
assert.equal(fmtAlert('count', 0), '0')
})