| 1 | import assert from "node:assert/strict"; |
| 2 | import test from "node:test"; |
| 3 | |
| 4 | import { OTLP_DOOR, readLogs, readMetrics, readTraces } from "./normalize.ts"; |
| 5 | import { projectOfToken } from "./ingest.ts"; |
| 6 | |
| 7 | const resource = (attrs: Record<string, string>) => ({ attributes: Object.entries(attrs).map(([key, v]) => ({ key, value: { stringValue: v } })) }); |
| 8 | |
| 9 | test("a server span is a request, filed under its service and environment", () => { |
| 10 | const batch = readTraces( |
| 11 | { |
| 12 | resourceSpans: [ |
| 13 | { |
| 14 | resource: resource({ "service.name": "billing", "deployment.environment.name": "production", "vcs.ref.head.revision": "abc123" }), |
| 15 | scopeSpans: [ |
| 16 | { |
| 17 | spans: [ |
| 18 | { |
| 19 | traceId: "4bf92f3577b34da6a3ce929d0e0e4736", |
| 20 | spanId: "00f067aa0ba902b7", |
| 21 | name: "POST", |
| 22 | kind: 2, |
| 23 | startTimeUnixNano: "1760000000000000000", |
| 24 | endTimeUnixNano: "1760000000120000000", |
| 25 | attributes: [ |
| 26 | { key: "http.request.method", value: { stringValue: "POST" } }, |
| 27 | { key: "url.path", value: { stringValue: "/invoices/12345" } }, |
| 28 | { key: "http.response.status_code", value: { intValue: "502" } }, |
| 29 | ], |
| 30 | events: [ |
| 31 | { |
| 32 | name: "exception", |
| 33 | timeUnixNano: "1760000000100000000", |
| 34 | attributes: [{ key: "exception.type", value: { stringValue: "UpstreamError" } }], |
| 35 | }, |
| 36 | ], |
| 37 | status: { code: 2, message: "upstream refused" }, |
| 38 | }, |
| 39 | // Ids of zeros are no ids: the span is dropped. |
| 40 | { traceId: "00000000000000000000000000000000", spanId: "0000000000000000", startTimeUnixNano: "1" }, |
| 41 | ], |
| 42 | }, |
| 43 | ], |
| 44 | }, |
| 45 | ], |
| 46 | }, |
| 47 | OTLP_DOOR, |
| 48 | 100, |
| 49 | ); |
| 50 | assert.equal(batch.spans.length, 1); |
| 51 | const span = batch.spans[0]; |
| 52 | assert.equal(span.service, "billing"); |
| 53 | assert.equal(span.environment, "production"); |
| 54 | assert.equal(span.commit_sha, "abc123"); |
| 55 | assert.equal(span.start_us, 1_760_000_000_000_000); |
| 56 | assert.equal(span.duration_us, 120_000); |
| 57 | assert.equal(span.status, "error"); |
| 58 | assert.equal(batch.requests.length, 1); |
| 59 | assert.equal(batch.requests[0].route, "/invoices/:id"); |
| 60 | assert.equal(batch.requests[0].status, 502); |
| 61 | assert.equal(batch.requests[0].outcome, "error"); |
| 62 | assert.equal(batch.errors[0].name, "UpstreamError"); |
| 63 | }); |
| 64 | |
| 65 | test("an app on g1t.page has its requests counted from its tail events, not its SDK's spans", () => { |
| 66 | const batch = readTraces( |
| 67 | { |
| 68 | resourceSpans: [ |
| 69 | { |
| 70 | resource: resource({ "service.name": "web", "g1t.deployment.id": "dep_1" }), |
| 71 | scopeSpans: [ |
| 72 | { |
| 73 | spans: [ |
| 74 | { |
| 75 | traceId: "4bf92f3577b34da6a3ce929d0e0e4736", |
| 76 | spanId: "00f067aa0ba902b7", |
| 77 | kind: "SPAN_KIND_SERVER" as never, |
| 78 | startTimeUnixNano: "1760000000000000000", |
| 79 | }, |
| 80 | ], |
| 81 | }, |
| 82 | ], |
| 83 | }, |
| 84 | ], |
| 85 | }, |
| 86 | OTLP_DOOR, |
| 87 | 0, |
| 88 | ); |
| 89 | assert.equal(batch.spans[0].kind, "server"); |
| 90 | assert.equal(batch.spans[0].deployment_id, "dep_1"); |
| 91 | assert.equal(batch.requests.length, 0); |
| 92 | }); |
| 93 | |
| 94 | test("log records keep their severity, body and trace", () => { |
| 95 | const batch = readLogs( |
| 96 | { |
| 97 | resourceLogs: [ |
| 98 | { |
| 99 | resource: resource({ "service.name": "worker" }), |
| 100 | scopeLogs: [ |
| 101 | { |
| 102 | logRecords: [ |
| 103 | { timeUnixNano: "1760000000000000000", severityText: "warning", body: { stringValue: "slow" } }, |
| 104 | { |
| 105 | observedTimeUnixNano: "1760000000000000000", |
| 106 | severityNumber: 17, |
| 107 | body: { kvlistValue: { values: [{ key: "a", value: { intValue: "1" } }] } }, |
| 108 | attributes: [{ key: "exception.type", value: { stringValue: "Boom" } }], |
| 109 | }, |
| 110 | ], |
| 111 | }, |
| 112 | ], |
| 113 | }, |
| 114 | ], |
| 115 | }, |
| 116 | OTLP_DOOR, |
| 117 | 0, |
| 118 | ); |
| 119 | assert.equal(batch.logs[0].severity, "warn"); |
| 120 | assert.equal(batch.logs[0].body, "slow"); |
| 121 | assert.equal(batch.logs[1].severity, "error"); |
| 122 | assert.equal(batch.logs[1].body, '{"a":1}'); |
| 123 | assert.equal(batch.errors.length, 1); |
| 124 | }); |
| 125 | |
| 126 | test("metrics of each kind become points", () => { |
| 127 | const batch = readMetrics( |
| 128 | { |
| 129 | resourceMetrics: [ |
| 130 | { |
| 131 | resource: resource({ "service.name": "api" }), |
| 132 | scopeMetrics: [ |
| 133 | { |
| 134 | metrics: [ |
| 135 | { name: "queue.depth", gauge: { dataPoints: [{ timeUnixNano: "1760000000000000000", asInt: "7" }] } }, |
| 136 | { name: "jobs", sum: { aggregationTemporality: 2, isMonotonic: true, dataPoints: [{ timeUnixNano: "1760000000000000000", asDouble: 3 }] } }, |
| 137 | { |
| 138 | name: "latency", |
| 139 | unit: "ms", |
| 140 | histogram: { dataPoints: [{ timeUnixNano: "1760000000000000000", count: "4", sum: 40, explicitBounds: [10], bucketCounts: ["2", "2"] }] }, |
| 141 | }, |
| 142 | ], |
| 143 | }, |
| 144 | ], |
| 145 | }, |
| 146 | ], |
| 147 | }, |
| 148 | OTLP_DOOR, |
| 149 | 0, |
| 150 | ); |
| 151 | assert.deepEqual( |
| 152 | batch.metrics.map((m) => [m.name, m.kind, m.value]), |
| 153 | [ |
| 154 | ["queue.depth", "gauge", 7], |
| 155 | ["jobs", "sum", 3], |
| 156 | ["latency", "histogram", 10], |
| 157 | ], |
| 158 | ); |
| 159 | assert.equal(batch.metrics[1].monotonic, true); |
| 160 | assert.deepEqual(batch.metrics[2].counts, [2, 2]); |
| 161 | }); |
| 162 | |
| 163 | test("a token names its project", () => { |
| 164 | assert.equal(projectOfToken("g1to_prj_01jb2k7x9hfq0b3zj0f5s2m8ra_abcdef0123"), "prj_01jb2k7x9hfq0b3zj0f5s2m8ra"); |
| 165 | // Only a project id as g1t makes them: anything else never reaches a store. |
| 166 | assert.equal(projectOfToken("g1to_prj_01J9ZK3_abcdef0123"), null); |
| 167 | assert.equal(projectOfToken("g1to_anything-at-all_abcdef0123"), null); |
| 168 | assert.equal(projectOfToken("g1to_prj_01jb2k7x9hfq0b3zj0f5s2m8rax_abcdef0123"), null); |
| 169 | assert.equal(projectOfToken("g1t_prj_x_y"), null); |
| 170 | assert.equal(projectOfToken("g1to_nounderscore"), null); |
| 171 | }); |