| 1 | import assert from "node:assert/strict"; |
| 2 | import test from "node:test"; |
| 3 | |
| 4 | import type { TelemetrySpan } from "@g1t/contracts"; |
| 5 | |
| 6 | import { change, count, duration, errorItem, filterOf, matchesLog, niceTicks, parseView, percent, viewHref } from "./observability.ts"; |
| 7 | import { spanTree } from "./observability-trace.ts"; |
| 8 | |
| 9 | test("a view reads from the address and writes back to it", () => { |
| 10 | const view = parseView(new URLSearchParams("range=1h&env=production&q=timeout")); |
| 11 | assert.deepEqual(view, { range: "1h", environment: "production", service: null, deployment: null, q: "timeout" }); |
| 12 | assert.equal(viewHref("/a/b/observability/logs", view), "/a/b/observability/logs?range=1h&env=production&q=timeout"); |
| 13 | assert.equal(viewHref("/x", view, { env: null, q: null, range: "24h" }), "/x?range=24h"); |
| 14 | // The default period is left out of the address. |
| 15 | assert.equal(viewHref("/x", parseView(new URLSearchParams("range=nonsense"))), "/x"); |
| 16 | const filter = filterOf(view, 3_600_000); |
| 17 | assert.equal(filter.from_us, 0); |
| 18 | assert.equal(filter.to_us, 3_600_000_000); |
| 19 | assert.deepEqual(filter.environments, ["production"]); |
| 20 | }); |
| 21 | |
| 22 | test("numbers read as people read them", () => { |
| 23 | assert.equal(duration(0.42), "420 µs"); |
| 24 | assert.equal(duration(4.25), "4.3 ms"); |
| 25 | assert.equal(duration(87), "87 ms"); |
| 26 | assert.equal(duration(1500), "1.5 s"); |
| 27 | assert.equal(duration(125_000), "2m 5s"); |
| 28 | assert.equal(duration(null), "—"); |
| 29 | assert.equal(duration(0), "0"); |
| 30 | assert.equal(count(12_400), "12.4K"); |
| 31 | assert.equal(percent(0.0004), "0.04%"); |
| 32 | assert.equal(percent(0.25), "25%"); |
| 33 | assert.deepEqual(change(110, 100, true), { text: "+10%", good: false }); |
| 34 | assert.deepEqual(change(90, 100, true), { text: "−10%", good: true }); |
| 35 | assert.equal(change(5, 0, true), null); |
| 36 | assert.deepEqual(niceTicks(0.37), [0, 0.1, 0.2, 0.3, 0.4]); |
| 37 | assert.deepEqual(niceTicks(870), [0, 250, 500, 750, 1000]); |
| 38 | }); |
| 39 | |
| 40 | test("a live line is shown only when the page's search would have found it", () => { |
| 41 | const log = { |
| 42 | severity: "error" as const, |
| 43 | body: "Payment timeout for order 7", |
| 44 | environment: "production", |
| 45 | service: "api", |
| 46 | deployment_id: null, |
| 47 | attributes: { "user.id": "42" }, |
| 48 | }; |
| 49 | const view = parseView(new URLSearchParams("env=production")); |
| 50 | assert.equal(matchesLog(log, view, []), true); |
| 51 | assert.equal(matchesLog(log, { ...view, q: "timeout" }, []), true); |
| 52 | assert.equal(matchesLog(log, { ...view, q: "refund" }, []), false); |
| 53 | assert.equal(matchesLog(log, { ...view, q: "user.id=42" }, []), true); |
| 54 | assert.equal(matchesLog(log, { ...view, q: "user.id=7" }, []), false); |
| 55 | assert.equal(matchesLog(log, { ...view, q: "level:warn" }, []), false); |
| 56 | assert.equal(matchesLog(log, view, ["error", "fatal"]), true); |
| 57 | assert.equal(matchesLog(log, { ...view, environment: "preview" }, []), false); |
| 58 | }); |
| 59 | |
| 60 | const span = (id: string, parent: string | null, start: number): TelemetrySpan => ({ |
| 61 | trace_id: "t", |
| 62 | span_id: id, |
| 63 | parent_span_id: parent, |
| 64 | name: id, |
| 65 | kind: "internal", |
| 66 | start_us: start, |
| 67 | duration_us: 1, |
| 68 | status: "unset", |
| 69 | status_message: null, |
| 70 | attributes: {}, |
| 71 | resource: {}, |
| 72 | events: [], |
| 73 | links: [], |
| 74 | service: "s", |
| 75 | environment: null, |
| 76 | deployment_id: null, |
| 77 | commit_sha: null, |
| 78 | source: "otlp", |
| 79 | }); |
| 80 | |
| 81 | test("spans nest under their parents, and orphans are still shown", () => { |
| 82 | const rows = spanTree([ |
| 83 | span("c", "a", 3), |
| 84 | span("a", null, 1), |
| 85 | span("b", "a", 2), |
| 86 | span("x", "missing", 5), |
| 87 | span("loop1", "loop2", 9), |
| 88 | span("loop2", "loop1", 9), |
| 89 | ]); |
| 90 | assert.deepEqual( |
| 91 | rows.map((r) => [r.span.span_id, r.depth]), |
| 92 | [ |
| 93 | ["a", 0], |
| 94 | ["b", 1], |
| 95 | ["c", 1], |
| 96 | ["x", 0], |
| 97 | ["loop1", 0], |
| 98 | ["loop2", 0], |
| 99 | ], |
| 100 | ); |
| 101 | }); |
| 102 | |
| 103 | test("an error's work item says what, how often and where to look", () => { |
| 104 | const item = errorItem( |
| 105 | { |
| 106 | name: "TypeError", |
| 107 | message: "x is undefined", |
| 108 | stack: "TypeError\n at f (a.ts:1)", |
| 109 | count: 3, |
| 110 | first_us: 0, |
| 111 | last_us: 60_000_000, |
| 112 | service: "web", |
| 113 | environment: "production", |
| 114 | first_deployment_id: "dep_1", |
| 115 | last_request_id: "r1", |
| 116 | last_trace_id: null, |
| 117 | }, |
| 118 | "https://g1t.sh/acme/web/observability", |
| 119 | ); |
| 120 | assert.equal(item.title, "TypeError: x is undefined"); |
| 121 | assert.match(item.body, /3 times/); |
| 122 | assert.match(item.body, /requests\/r1/); |
| 123 | assert.match(item.body, /dep_1/); |
| 124 | assert.doesNotMatch(item.body, /traces\//); |
| 125 | }); |