| 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: [{ name: "exception", timeUnixNano: "1760000000100000000", attributes: [{ key: "exception.type", value: { stringValue: "UpstreamError" } }] }], |
| 31 | status: { code: 2, message: "upstream refused" }, |
| 32 | }, |
| 33 | // Ids of zeros are no ids: the span is dropped. |
| 34 | { traceId: "00000000000000000000000000000000", spanId: "0000000000000000", startTimeUnixNano: "1" }, |
| 35 | ], |
| 36 | }, |
| 37 | ], |
| 38 | }, |
| 39 | ], |
| 40 | }, |
| 41 | OTLP_DOOR, |
| 42 | 100, |
| 43 | ); |
| 44 | assert.equal(batch.spans.length, 1); |
| 45 | const span = batch.spans[0]; |
| 46 | assert.equal(span.service, "billing"); |
| 47 | assert.equal(span.environment, "production"); |
| 48 | assert.equal(span.commit_sha, "abc123"); |
| 49 | assert.equal(span.start_us, 1_760_000_000_000_000); |
| 50 | assert.equal(span.duration_us, 120_000); |
| 51 | assert.equal(span.status, "error"); |
| 52 | assert.equal(batch.requests.length, 1); |
| 53 | assert.equal(batch.requests[0].route, "/invoices/:id"); |
| 54 | assert.equal(batch.requests[0].status, 502); |
| 55 | assert.equal(batch.requests[0].outcome, "error"); |
| 56 | assert.equal(batch.errors[0].name, "UpstreamError"); |
| 57 | }); |
| 58 | |
| 59 | test("an app on g1t.page has its requests counted from its tail events, not its SDK's spans", () => { |
| 60 | const batch = readTraces( |
| 61 | { |
| 62 | resourceSpans: [ |
| 63 | { |
| 64 | resource: resource({ "service.name": "web", "g1t.deployment.id": "dep_1" }), |
| 65 | scopeSpans: [{ spans: [{ traceId: "4bf92f3577b34da6a3ce929d0e0e4736", spanId: "00f067aa0ba902b7", kind: "SPAN_KIND_SERVER" as never, startTimeUnixNano: "1760000000000000000" }] }], |
| 66 | }, |
| 67 | ], |
| 68 | }, |
| 69 | OTLP_DOOR, |
| 70 | 0, |
| 71 | ); |
| 72 | assert.equal(batch.spans[0].kind, "server"); |
| 73 | assert.equal(batch.spans[0].deployment_id, "dep_1"); |
| 74 | assert.equal(batch.requests.length, 0); |
| 75 | }); |
| 76 | |
| 77 | test("log records keep their severity, body and trace", () => { |
| 78 | const batch = readLogs( |
| 79 | { |
| 80 | resourceLogs: [ |
| 81 | { |
| 82 | resource: resource({ "service.name": "worker" }), |
| 83 | scopeLogs: [ |
| 84 | { |
| 85 | logRecords: [ |
| 86 | { timeUnixNano: "1760000000000000000", severityText: "warning", body: { stringValue: "slow" } }, |
| 87 | { observedTimeUnixNano: "1760000000000000000", severityNumber: 17, body: { kvlistValue: { values: [{ key: "a", value: { intValue: "1" } }] } }, attributes: [{ key: "exception.type", value: { stringValue: "Boom" } }] }, |
| 88 | ], |
| 89 | }, |
| 90 | ], |
| 91 | }, |
| 92 | ], |
| 93 | }, |
| 94 | OTLP_DOOR, |
| 95 | 0, |
| 96 | ); |
| 97 | assert.equal(batch.logs[0].severity, "warn"); |
| 98 | assert.equal(batch.logs[0].body, "slow"); |
| 99 | assert.equal(batch.logs[1].severity, "error"); |
| 100 | assert.equal(batch.logs[1].body, '{"a":1}'); |
| 101 | assert.equal(batch.errors.length, 1); |
| 102 | }); |
| 103 | |
| 104 | test("metrics of each kind become points", () => { |
| 105 | const batch = readMetrics( |
| 106 | { |
| 107 | resourceMetrics: [ |
| 108 | { |
| 109 | resource: resource({ "service.name": "api" }), |
| 110 | scopeMetrics: [ |
| 111 | { |
| 112 | metrics: [ |
| 113 | { name: "queue.depth", gauge: { dataPoints: [{ timeUnixNano: "1760000000000000000", asInt: "7" }] } }, |
| 114 | { name: "jobs", sum: { aggregationTemporality: 2, isMonotonic: true, dataPoints: [{ timeUnixNano: "1760000000000000000", asDouble: 3 }] } }, |
| 115 | { name: "latency", unit: "ms", histogram: { dataPoints: [{ timeUnixNano: "1760000000000000000", count: "4", sum: 40, explicitBounds: [10], bucketCounts: ["2", "2"] }] } }, |
| 116 | ], |
| 117 | }, |
| 118 | ], |
| 119 | }, |
| 120 | ], |
| 121 | }, |
| 122 | OTLP_DOOR, |
| 123 | 0, |
| 124 | ); |
| 125 | assert.deepEqual(batch.metrics.map((m) => [m.name, m.kind, m.value]), [["queue.depth", "gauge", 7], ["jobs", "sum", 3], ["latency", "histogram", 10]]); |
| 126 | assert.equal(batch.metrics[1].monotonic, true); |
| 127 | assert.deepEqual(batch.metrics[2].counts, [2, 2]); |
| 128 | }); |
| 129 | |
| 130 | test("a token names its project", () => { |
| 131 | assert.equal(projectOfToken("g1to_prj_01J9ZK3_abcdef0123"), "prj_01J9ZK3"); |
| 132 | assert.equal(projectOfToken("g1t_prj_x_y"), null); |
| 133 | assert.equal(projectOfToken("g1to_nounderscore"), null); |
| 134 | }); |