@blamejs/core 0.7.4 → 0.7.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +423 -395
- package/README.md +150 -149
- package/bin/blamejs.js +0 -0
- package/index.js +308 -284
- package/lib/api-key.js +660 -663
- package/lib/api-snapshot.js +338 -338
- package/lib/app-shutdown.js +385 -385
- package/lib/app.js +365 -365
- package/lib/archive.js +250 -250
- package/lib/atomic-file.js +544 -544
- package/lib/audit-chain.js +177 -177
- package/lib/audit-sign.js +344 -344
- package/lib/audit-tools.js +677 -677
- package/lib/audit.js +766 -766
- package/lib/auth/jwt.js +311 -311
- package/lib/auth/lockout.js +436 -436
- package/lib/auth/oauth.js +721 -721
- package/lib/auth/passkey.js +181 -181
- package/lib/auth/password.js +594 -594
- package/lib/backup/bundle.js +217 -217
- package/lib/backup/crypto.js +176 -176
- package/lib/backup/index.js +515 -515
- package/lib/backup/manifest.js +282 -282
- package/lib/break-glass.js +1338 -1338
- package/lib/bundler.js +441 -441
- package/lib/cache-redis.js +256 -256
- package/lib/cache.js +1206 -1206
- package/lib/canonical-json.js +115 -115
- package/lib/chain-writer.js +234 -234
- package/lib/cli-helpers.js +206 -206
- package/lib/cli.js +2334 -2334
- package/lib/cluster-provider-db.js +317 -317
- package/lib/cluster-storage.js +226 -226
- package/lib/cluster.js +703 -703
- package/lib/codepoint-class.js +196 -0
- package/lib/config-drift.js +301 -301
- package/lib/consent.js +222 -222
- package/lib/constants.js +191 -191
- package/lib/cookies.js +315 -315
- package/lib/credential-hash.js +322 -322
- package/lib/crypto.js +266 -266
- package/lib/csv.js +275 -286
- package/lib/db-declare-row-policy.js +267 -267
- package/lib/db-declare-view.js +420 -421
- package/lib/db-query.js +406 -406
- package/lib/db-schema.js +319 -319
- package/lib/db.js +1288 -1288
- package/lib/deprecate.js +222 -222
- package/lib/dev.js +335 -335
- package/lib/dual-control.js +473 -473
- package/lib/error-page.js +420 -420
- package/lib/external-db-migrate.js +441 -441
- package/lib/external-db.js +1061 -1061
- package/lib/file-type.js +273 -273
- package/lib/file-upload.js +213 -10
- package/lib/forms.js +422 -422
- package/lib/framework-error.js +293 -215
- package/lib/framework-schema.js +717 -717
- package/lib/gate-contract.js +971 -0
- package/lib/guard-all.js +405 -0
- package/lib/guard-archive.js +739 -0
- package/lib/guard-csv.js +816 -0
- package/lib/guard-email.js +744 -0
- package/lib/guard-filename.js +724 -0
- package/lib/guard-html.js +976 -0
- package/lib/guard-json.js +729 -0
- package/lib/guard-markdown.js +586 -0
- package/lib/guard-svg.js +976 -0
- package/lib/guard-xml.js +405 -0
- package/lib/guard-yaml.js +529 -0
- package/lib/handlers.js +350 -350
- package/lib/http-client-cookie-jar.js +508 -508
- package/lib/http-client.js +1195 -1195
- package/lib/i18n.js +878 -878
- package/lib/jobs.js +185 -185
- package/lib/log-stream-cloudwatch.js +369 -369
- package/lib/log-stream-local.js +146 -146
- package/lib/log-stream-otlp-grpc.js +410 -410
- package/lib/log-stream-otlp.js +286 -286
- package/lib/log-stream-syslog.js +302 -302
- package/lib/log-stream-webhook.js +199 -199
- package/lib/log-stream.js +330 -330
- package/lib/log.js +500 -500
- package/lib/mail-bounce.js +528 -528
- package/lib/mail-dkim.js +369 -362
- package/lib/mail.js +981 -962
- package/lib/metrics.js +683 -683
- package/lib/middleware/api-encrypt.js +936 -936
- package/lib/middleware/attach-user.js +157 -157
- package/lib/middleware/body-parser.js +1170 -1091
- package/lib/middleware/bot-guard.js +178 -178
- package/lib/middleware/compression.js +452 -452
- package/lib/middleware/cors.js +314 -314
- package/lib/middleware/csp-nonce.js +348 -348
- package/lib/middleware/csrf-protect.js +316 -316
- package/lib/middleware/db-role-for.js +264 -264
- package/lib/middleware/health.js +392 -392
- package/lib/middleware/index.js +79 -79
- package/lib/middleware/rate-limit.js +358 -358
- package/lib/middleware/request-id.js +61 -61
- package/lib/middleware/request-log.js +168 -168
- package/lib/middleware/require-auth.js +104 -104
- package/lib/middleware/security-headers.js +116 -116
- package/lib/middleware/sse.js +166 -166
- package/lib/migrations.js +383 -383
- package/lib/mtls-ca.js +518 -518
- package/lib/mtls-engine-default.js +481 -481
- package/lib/network-dns.js +632 -632
- package/lib/network-heartbeat.js +290 -290
- package/lib/network-nts.js +574 -574
- package/lib/network-proxy.js +265 -265
- package/lib/network-tls.js +328 -328
- package/lib/network.js +233 -233
- package/lib/notify.js +612 -612
- package/lib/ntp-check.js +229 -229
- package/lib/numeric-bounds.js +111 -91
- package/lib/object-store/azure-blob-bucket-ops.js +349 -349
- package/lib/object-store/azure-blob.js +488 -488
- package/lib/object-store/gcs-bucket-ops.js +351 -351
- package/lib/object-store/gcs.js +519 -519
- package/lib/object-store/http-put.js +153 -153
- package/lib/object-store/index.js +197 -197
- package/lib/object-store/sigv4-bucket-ops.js +1092 -1092
- package/lib/object-store/sigv4.js +903 -903
- package/lib/observability.js +151 -151
- package/lib/otel-export.js +269 -269
- package/lib/pagination.js +464 -464
- package/lib/parsers/index.js +80 -80
- package/lib/parsers/safe-env.js +642 -642
- package/lib/parsers/safe-ini.js +292 -292
- package/lib/parsers/safe-toml.js +784 -784
- package/lib/parsers/safe-xml.js +390 -390
- package/lib/parsers/safe-yaml.js +1015 -1015
- package/lib/permissions.js +708 -708
- package/lib/pqc-agent.js +87 -87
- package/lib/pqc-gate.js +279 -279
- package/lib/protobuf-encoder.js +190 -190
- package/lib/protocol-dispatcher.js +161 -161
- package/lib/pubsub-redis.js +167 -167
- package/lib/pubsub.js +429 -429
- package/lib/queue-local.js +476 -476
- package/lib/queue-redis.js +745 -745
- package/lib/queue-sqs.js +319 -319
- package/lib/queue.js +695 -695
- package/lib/redis-client.js +519 -519
- package/lib/request-helpers.js +340 -340
- package/lib/restore-bundle.js +237 -237
- package/lib/restore-rollback.js +259 -259
- package/lib/restore.js +409 -409
- package/lib/retry.js +376 -376
- package/lib/router.js +748 -748
- package/lib/safe-async.js +735 -735
- package/lib/safe-buffer.js +237 -237
- package/lib/safe-json.js +541 -541
- package/lib/safe-schema.js +1266 -1266
- package/lib/safe-url.js +159 -159
- package/lib/scheduler.js +706 -706
- package/lib/security-assert.js +373 -373
- package/lib/seeders.js +618 -618
- package/lib/session.js +478 -478
- package/lib/slug.js +269 -269
- package/lib/ssrf-guard.js +401 -401
- package/lib/static.js +184 -4
- package/lib/storage.js +471 -471
- package/lib/subject.js +281 -281
- package/lib/template.js +791 -791
- package/lib/testing.js +798 -798
- package/lib/time.js +310 -310
- package/lib/totp.js +302 -302
- package/lib/tracing.js +494 -494
- package/lib/uuid.js +132 -132
- package/lib/validate-opts.js +340 -319
- package/lib/vault/index.js +308 -308
- package/lib/vault/rotate.js +784 -784
- package/lib/vault/wrap.js +296 -296
- package/lib/vendor/noble-ciphers.cjs +9 -9
- package/lib/webhook.js +595 -595
- package/lib/websocket.js +1048 -1048
- package/package.json +77 -77
- package/sbom.cyclonedx.json +7 -7
package/lib/otel-export.js
CHANGED
|
@@ -1,269 +1,269 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* otel-export — OTLP/HTTP-JSON exporter for `b.observability` events.
|
|
4
|
-
*
|
|
5
|
-
* Bridges the framework's `observability.event(name, value, attrs)`
|
|
6
|
-
* surface to any OTel-compatible backend (Honeycomb, Datadog, Jaeger
|
|
7
|
-
* Collector, AWS Distro, Grafana, NewRelic — anything that speaks
|
|
8
|
-
* OTLP/HTTP). Spec: opentelemetry.io/docs/specs/otlp.
|
|
9
|
-
*
|
|
10
|
-
* var otel = b.otelExport.create({
|
|
11
|
-
* endpoint: "https://otel.honeycomb.io/v1/metrics",
|
|
12
|
-
* headers: { "X-Honeycomb-Team": env("HONEYCOMB_API_KEY") },
|
|
13
|
-
* serviceName: "wiki",
|
|
14
|
-
* intervalMs: b.constants.TIME.seconds(15), // auto-flush cadence
|
|
15
|
-
* httpClient: b.httpClient, // for testing
|
|
16
|
-
* });
|
|
17
|
-
*
|
|
18
|
-
* // Counter — accumulates per (name, attrs) tuple, flushed in batches.
|
|
19
|
-
* otel.recordCounter("http.requests", 1, { method: "GET", status: 200 });
|
|
20
|
-
*
|
|
21
|
-
* // Histogram — operator-bucketed observation. Less common; operators
|
|
22
|
-
* // wanting full histogram support build on top.
|
|
23
|
-
* otel.recordObservation("http.duration_ms", 142, { route: "/api/x" });
|
|
24
|
-
*
|
|
25
|
-
* await otel.flush(); // manual flush
|
|
26
|
-
* otel.close(); // cancels interval, final flush
|
|
27
|
-
*
|
|
28
|
-
* Wiring with `b.observability`:
|
|
29
|
-
*
|
|
30
|
-
* // Option A: install as an external tap on b.observability
|
|
31
|
-
* b.observability.setTap(otel.tapHandler);
|
|
32
|
-
*
|
|
33
|
-
* // Option B: alongside b.metrics — operators write their own
|
|
34
|
-
* // multi-tap fan-out (or pick one or the other for v1).
|
|
35
|
-
*
|
|
36
|
-
* Endpoint must accept OTLP/HTTP with Content-Type: application/json
|
|
37
|
-
* (the JSON variant of the OTLP protobuf — every modern collector
|
|
38
|
-
* supports it). The framework refuses to ship the binary protobuf
|
|
39
|
-
* encoding because it requires either a vendored proto runtime or
|
|
40
|
-
* hand-rolled wire format with no compelling benefit at this scope.
|
|
41
|
-
*/
|
|
42
|
-
var C = require("./constants");
|
|
43
|
-
var canonicalJson = require("./canonical-json");
|
|
44
|
-
var defaultHttpClient = require("./http-client");
|
|
45
|
-
var safeAsync = require("./safe-async");
|
|
46
|
-
var validateOpts = require("./validate-opts");
|
|
47
|
-
var { defineClass } = require("./framework-error");
|
|
48
|
-
|
|
49
|
-
var OtelExportError = defineClass("OtelExportError", { alwaysPermanent: false });
|
|
50
|
-
|
|
51
|
-
var DEFAULT_INTERVAL_MS = C.TIME.seconds(15);
|
|
52
|
-
|
|
53
|
-
// OTLP aggregation temporality:
|
|
54
|
-
// 1 = DELTA — counters report deltas since last export
|
|
55
|
-
// 2 = CUMULATIVE — counters report running totals
|
|
56
|
-
// DELTA is what most exporters do for short-lived processes; the
|
|
57
|
-
// receiving backend handles the running sum.
|
|
58
|
-
var TEMPORALITY_DELTA = 1;
|
|
59
|
-
|
|
60
|
-
// ---- attribute encoding ----
|
|
61
|
-
// OTLP attributes are KeyValue with typed `value` fields:
|
|
62
|
-
// { key, value: { stringValue | intValue | doubleValue | boolValue } }
|
|
63
|
-
function _attrsToOtlp(attrs) {
|
|
64
|
-
if (!attrs || typeof attrs !== "object") return [];
|
|
65
|
-
var out = [];
|
|
66
|
-
for (var k in attrs) {
|
|
67
|
-
if (!Object.prototype.hasOwnProperty.call(attrs, k)) continue;
|
|
68
|
-
var v = attrs[k];
|
|
69
|
-
var kv;
|
|
70
|
-
if (typeof v === "string") kv = { stringValue: v };
|
|
71
|
-
else if (typeof v === "number") {
|
|
72
|
-
kv = Number.isInteger(v) ? { intValue: String(v) } : { doubleValue: v };
|
|
73
|
-
}
|
|
74
|
-
else if (typeof v === "boolean") kv = { boolValue: v };
|
|
75
|
-
else if (v == null) continue;
|
|
76
|
-
else kv = { stringValue: String(v) };
|
|
77
|
-
out.push({ key: k, value: kv });
|
|
78
|
-
}
|
|
79
|
-
return out;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Stable key per (name, attrs) so tap calls aggregate. Routes the
|
|
83
|
-
// sort through canonical-json so the framework has one canonical-sort
|
|
84
|
-
// source of truth for hash-input-shaped keys (the OTLP attribute
|
|
85
|
-
// encoding _attrsToOtlp emits the wire-format separately).
|
|
86
|
-
function _bucketKey(name, attrs) {
|
|
87
|
-
if (!attrs) return name + "|";
|
|
88
|
-
// Coerce to strings so canonical-json doesn't reject odd value types.
|
|
89
|
-
var coerced = {};
|
|
90
|
-
var rawKeys = Object.keys(attrs);
|
|
91
|
-
for (var i = 0; i < rawKeys.length; i++) {
|
|
92
|
-
coerced[rawKeys[i]] = String(attrs[rawKeys[i]]);
|
|
93
|
-
}
|
|
94
|
-
return name + "|" + canonicalJson.stringify(coerced);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function create(opts) {
|
|
98
|
-
opts = opts || {};
|
|
99
|
-
validateOpts(opts, [
|
|
100
|
-
"endpoint", "headers", "serviceName", "intervalMs",
|
|
101
|
-
"httpClient", "resourceAttributes", "scope",
|
|
102
|
-
], "otelExport.create");
|
|
103
|
-
validateOpts.requireNonEmptyString(opts.endpoint, "create: endpoint", OtelExportError, "otel-export/bad-endpoint");
|
|
104
|
-
validateOpts.requireNonEmptyString(opts.serviceName, "create: serviceName", OtelExportError, "otel-export/bad-service-name");
|
|
105
|
-
var endpoint = opts.endpoint;
|
|
106
|
-
var serviceName = opts.serviceName;
|
|
107
|
-
var headers = opts.headers || {};
|
|
108
|
-
var intervalMs = opts.intervalMs != null ? opts.intervalMs : DEFAULT_INTERVAL_MS;
|
|
109
|
-
if (typeof intervalMs !== "number" || !isFinite(intervalMs) || intervalMs < 0) {
|
|
110
|
-
throw new OtelExportError("otel-export/bad-interval",
|
|
111
|
-
"create: intervalMs must be a non-negative finite number");
|
|
112
|
-
}
|
|
113
|
-
var httpClient = opts.httpClient || defaultHttpClient;
|
|
114
|
-
var scopeName = (opts.scope && opts.scope.name) || "blamejs";
|
|
115
|
-
var scopeVersion = (opts.scope && opts.scope.version) || "0.5.x";
|
|
116
|
-
var resourceAttrs = Object.assign({ "service.name": serviceName },
|
|
117
|
-
opts.resourceAttributes || {});
|
|
118
|
-
|
|
119
|
-
// Buckets: counters and observations keyed by (name, sorted-attrs).
|
|
120
|
-
var counters = new Map(); // bucketKey → { name, attrs, value, startUnixNano }
|
|
121
|
-
var observations = new Map(); // bucketKey → { name, attrs, sum, count, min, max, startUnixNano }
|
|
122
|
-
var startUnixNano = String(Date.now() * 1e6);
|
|
123
|
-
var loop = null;
|
|
124
|
-
var closed = false;
|
|
125
|
-
|
|
126
|
-
function recordCounter(name, value, attrs) {
|
|
127
|
-
if (closed) return;
|
|
128
|
-
if (typeof name !== "string" || name.length === 0) return;
|
|
129
|
-
var v = typeof value === "number" && isFinite(value) ? value : 1;
|
|
130
|
-
var key = _bucketKey(name, attrs);
|
|
131
|
-
var b = counters.get(key);
|
|
132
|
-
if (!b) {
|
|
133
|
-
b = { name: name, attrs: attrs || {}, value: 0, startUnixNano: startUnixNano };
|
|
134
|
-
counters.set(key, b);
|
|
135
|
-
}
|
|
136
|
-
b.value += v;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function recordObservation(name, value, attrs) {
|
|
140
|
-
if (closed) return;
|
|
141
|
-
if (typeof name !== "string" || name.length === 0) return;
|
|
142
|
-
if (typeof value !== "number" || !isFinite(value)) return;
|
|
143
|
-
var key = _bucketKey(name, attrs);
|
|
144
|
-
var b = observations.get(key);
|
|
145
|
-
if (!b) {
|
|
146
|
-
b = { name: name, attrs: attrs || {}, sum: 0, count: 0, min: value, max: value, startUnixNano: startUnixNano };
|
|
147
|
-
observations.set(key, b);
|
|
148
|
-
}
|
|
149
|
-
b.sum += value;
|
|
150
|
-
b.count += 1;
|
|
151
|
-
if (value < b.min) b.min = value;
|
|
152
|
-
if (value > b.max) b.max = value;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// Operators wire this as the observability tap. event(name, value, labels)
|
|
156
|
-
// → recordCounter for value=1 fire-and-forget shapes.
|
|
157
|
-
function tapHandler(name, value, labels) {
|
|
158
|
-
recordCounter(name, value, labels);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
function _drainAndEncode() {
|
|
162
|
-
var nowUnixNano = String(Date.now() * 1e6);
|
|
163
|
-
var metrics = [];
|
|
164
|
-
var c, o;
|
|
165
|
-
|
|
166
|
-
counters.forEach(function (entry) {
|
|
167
|
-
metrics.push({
|
|
168
|
-
name: entry.name,
|
|
169
|
-
sum: {
|
|
170
|
-
dataPoints: [{
|
|
171
|
-
attributes: _attrsToOtlp(entry.attrs),
|
|
172
|
-
startTimeUnixNano: entry.startUnixNano,
|
|
173
|
-
timeUnixNano: nowUnixNano,
|
|
174
|
-
asDouble: entry.value,
|
|
175
|
-
}],
|
|
176
|
-
aggregationTemporality: TEMPORALITY_DELTA,
|
|
177
|
-
isMonotonic: true,
|
|
178
|
-
},
|
|
179
|
-
});
|
|
180
|
-
});
|
|
181
|
-
void c;
|
|
182
|
-
observations.forEach(function (entry) {
|
|
183
|
-
metrics.push({
|
|
184
|
-
name: entry.name,
|
|
185
|
-
summary: {
|
|
186
|
-
dataPoints: [{
|
|
187
|
-
attributes: _attrsToOtlp(entry.attrs),
|
|
188
|
-
startTimeUnixNano: entry.startUnixNano,
|
|
189
|
-
timeUnixNano: nowUnixNano,
|
|
190
|
-
count: String(entry.count),
|
|
191
|
-
sum: entry.sum,
|
|
192
|
-
quantileValues: [
|
|
193
|
-
{ quantile: 0, value: entry.min },
|
|
194
|
-
{ quantile: 1, value: entry.max },
|
|
195
|
-
],
|
|
196
|
-
}],
|
|
197
|
-
},
|
|
198
|
-
});
|
|
199
|
-
});
|
|
200
|
-
void o;
|
|
201
|
-
|
|
202
|
-
// Reset buckets (DELTA temporality — each export is the delta).
|
|
203
|
-
counters.clear();
|
|
204
|
-
observations.clear();
|
|
205
|
-
startUnixNano = nowUnixNano;
|
|
206
|
-
if (metrics.length === 0) return null;
|
|
207
|
-
return {
|
|
208
|
-
resourceMetrics: [{
|
|
209
|
-
resource: { attributes: _attrsToOtlp(resourceAttrs) },
|
|
210
|
-
scopeMetrics: [{
|
|
211
|
-
scope: { name: scopeName, version: scopeVersion },
|
|
212
|
-
metrics: metrics,
|
|
213
|
-
}],
|
|
214
|
-
}],
|
|
215
|
-
};
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
async function flush() {
|
|
219
|
-
var payload = _drainAndEncode();
|
|
220
|
-
if (!payload) return { sent: false, reason: "no-data" };
|
|
221
|
-
var body = JSON.stringify(payload);
|
|
222
|
-
try {
|
|
223
|
-
var res = await httpClient.request({
|
|
224
|
-
method: "POST",
|
|
225
|
-
url: endpoint,
|
|
226
|
-
headers: Object.assign({ "Content-Type": "application/json" }, headers),
|
|
227
|
-
body: body,
|
|
228
|
-
});
|
|
229
|
-
if (res.statusCode < 200 || res.statusCode >= 300) {
|
|
230
|
-
throw new OtelExportError("otel-export/upstream-rejected",
|
|
231
|
-
"OTLP endpoint returned " + res.statusCode);
|
|
232
|
-
}
|
|
233
|
-
return { sent: true, statusCode: res.statusCode, bodyLength: body.length };
|
|
234
|
-
} catch (e) {
|
|
235
|
-
if (e && e.isOtelExportError) throw e;
|
|
236
|
-
throw new OtelExportError("otel-export/send-failed",
|
|
237
|
-
"OTLP send failed: " + ((e && e.message) || String(e)));
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
if (intervalMs > 0) {
|
|
242
|
-
loop = safeAsync.flushLoop(flush, intervalMs, { name: "otel-flush" });
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
function close() {
|
|
246
|
-
if (closed) return;
|
|
247
|
-
closed = true;
|
|
248
|
-
if (loop) { loop.stop(); loop = null; }
|
|
249
|
-
return flush().catch(function (_e) { /* close path swallows final-flush errors */ });
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
return {
|
|
253
|
-
recordCounter: recordCounter,
|
|
254
|
-
recordObservation: recordObservation,
|
|
255
|
-
tapHandler: tapHandler,
|
|
256
|
-
flush: flush,
|
|
257
|
-
close: close,
|
|
258
|
-
get bufferedCounters() { return counters.size; },
|
|
259
|
-
get bufferedObservations() { return observations.size; },
|
|
260
|
-
};
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
module.exports = {
|
|
264
|
-
create: create,
|
|
265
|
-
OtelExportError: OtelExportError,
|
|
266
|
-
// Test-only encoders for unit-testing the OTLP shape without an HTTP client.
|
|
267
|
-
_attrsToOtlpForTest: _attrsToOtlp,
|
|
268
|
-
_bucketKeyForTest: _bucketKey,
|
|
269
|
-
};
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* otel-export — OTLP/HTTP-JSON exporter for `b.observability` events.
|
|
4
|
+
*
|
|
5
|
+
* Bridges the framework's `observability.event(name, value, attrs)`
|
|
6
|
+
* surface to any OTel-compatible backend (Honeycomb, Datadog, Jaeger
|
|
7
|
+
* Collector, AWS Distro, Grafana, NewRelic — anything that speaks
|
|
8
|
+
* OTLP/HTTP). Spec: opentelemetry.io/docs/specs/otlp.
|
|
9
|
+
*
|
|
10
|
+
* var otel = b.otelExport.create({
|
|
11
|
+
* endpoint: "https://otel.honeycomb.io/v1/metrics",
|
|
12
|
+
* headers: { "X-Honeycomb-Team": env("HONEYCOMB_API_KEY") },
|
|
13
|
+
* serviceName: "wiki",
|
|
14
|
+
* intervalMs: b.constants.TIME.seconds(15), // auto-flush cadence
|
|
15
|
+
* httpClient: b.httpClient, // for testing
|
|
16
|
+
* });
|
|
17
|
+
*
|
|
18
|
+
* // Counter — accumulates per (name, attrs) tuple, flushed in batches.
|
|
19
|
+
* otel.recordCounter("http.requests", 1, { method: "GET", status: 200 });
|
|
20
|
+
*
|
|
21
|
+
* // Histogram — operator-bucketed observation. Less common; operators
|
|
22
|
+
* // wanting full histogram support build on top.
|
|
23
|
+
* otel.recordObservation("http.duration_ms", 142, { route: "/api/x" });
|
|
24
|
+
*
|
|
25
|
+
* await otel.flush(); // manual flush
|
|
26
|
+
* otel.close(); // cancels interval, final flush
|
|
27
|
+
*
|
|
28
|
+
* Wiring with `b.observability`:
|
|
29
|
+
*
|
|
30
|
+
* // Option A: install as an external tap on b.observability
|
|
31
|
+
* b.observability.setTap(otel.tapHandler);
|
|
32
|
+
*
|
|
33
|
+
* // Option B: alongside b.metrics — operators write their own
|
|
34
|
+
* // multi-tap fan-out (or pick one or the other for v1).
|
|
35
|
+
*
|
|
36
|
+
* Endpoint must accept OTLP/HTTP with Content-Type: application/json
|
|
37
|
+
* (the JSON variant of the OTLP protobuf — every modern collector
|
|
38
|
+
* supports it). The framework refuses to ship the binary protobuf
|
|
39
|
+
* encoding because it requires either a vendored proto runtime or
|
|
40
|
+
* hand-rolled wire format with no compelling benefit at this scope.
|
|
41
|
+
*/
|
|
42
|
+
var C = require("./constants");
|
|
43
|
+
var canonicalJson = require("./canonical-json");
|
|
44
|
+
var defaultHttpClient = require("./http-client");
|
|
45
|
+
var safeAsync = require("./safe-async");
|
|
46
|
+
var validateOpts = require("./validate-opts");
|
|
47
|
+
var { defineClass } = require("./framework-error");
|
|
48
|
+
|
|
49
|
+
var OtelExportError = defineClass("OtelExportError", { alwaysPermanent: false });
|
|
50
|
+
|
|
51
|
+
var DEFAULT_INTERVAL_MS = C.TIME.seconds(15);
|
|
52
|
+
|
|
53
|
+
// OTLP aggregation temporality:
|
|
54
|
+
// 1 = DELTA — counters report deltas since last export
|
|
55
|
+
// 2 = CUMULATIVE — counters report running totals
|
|
56
|
+
// DELTA is what most exporters do for short-lived processes; the
|
|
57
|
+
// receiving backend handles the running sum.
|
|
58
|
+
var TEMPORALITY_DELTA = 1;
|
|
59
|
+
|
|
60
|
+
// ---- attribute encoding ----
|
|
61
|
+
// OTLP attributes are KeyValue with typed `value` fields:
|
|
62
|
+
// { key, value: { stringValue | intValue | doubleValue | boolValue } }
|
|
63
|
+
function _attrsToOtlp(attrs) {
|
|
64
|
+
if (!attrs || typeof attrs !== "object") return [];
|
|
65
|
+
var out = [];
|
|
66
|
+
for (var k in attrs) {
|
|
67
|
+
if (!Object.prototype.hasOwnProperty.call(attrs, k)) continue;
|
|
68
|
+
var v = attrs[k];
|
|
69
|
+
var kv;
|
|
70
|
+
if (typeof v === "string") kv = { stringValue: v };
|
|
71
|
+
else if (typeof v === "number") {
|
|
72
|
+
kv = Number.isInteger(v) ? { intValue: String(v) } : { doubleValue: v };
|
|
73
|
+
}
|
|
74
|
+
else if (typeof v === "boolean") kv = { boolValue: v };
|
|
75
|
+
else if (v == null) continue;
|
|
76
|
+
else kv = { stringValue: String(v) };
|
|
77
|
+
out.push({ key: k, value: kv });
|
|
78
|
+
}
|
|
79
|
+
return out;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Stable key per (name, attrs) so tap calls aggregate. Routes the
|
|
83
|
+
// sort through canonical-json so the framework has one canonical-sort
|
|
84
|
+
// source of truth for hash-input-shaped keys (the OTLP attribute
|
|
85
|
+
// encoding _attrsToOtlp emits the wire-format separately).
|
|
86
|
+
function _bucketKey(name, attrs) {
|
|
87
|
+
if (!attrs) return name + "|";
|
|
88
|
+
// Coerce to strings so canonical-json doesn't reject odd value types.
|
|
89
|
+
var coerced = {};
|
|
90
|
+
var rawKeys = Object.keys(attrs);
|
|
91
|
+
for (var i = 0; i < rawKeys.length; i++) {
|
|
92
|
+
coerced[rawKeys[i]] = String(attrs[rawKeys[i]]);
|
|
93
|
+
}
|
|
94
|
+
return name + "|" + canonicalJson.stringify(coerced);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function create(opts) {
|
|
98
|
+
opts = opts || {};
|
|
99
|
+
validateOpts(opts, [
|
|
100
|
+
"endpoint", "headers", "serviceName", "intervalMs",
|
|
101
|
+
"httpClient", "resourceAttributes", "scope",
|
|
102
|
+
], "otelExport.create");
|
|
103
|
+
validateOpts.requireNonEmptyString(opts.endpoint, "create: endpoint", OtelExportError, "otel-export/bad-endpoint");
|
|
104
|
+
validateOpts.requireNonEmptyString(opts.serviceName, "create: serviceName", OtelExportError, "otel-export/bad-service-name");
|
|
105
|
+
var endpoint = opts.endpoint;
|
|
106
|
+
var serviceName = opts.serviceName;
|
|
107
|
+
var headers = opts.headers || {};
|
|
108
|
+
var intervalMs = opts.intervalMs != null ? opts.intervalMs : DEFAULT_INTERVAL_MS;
|
|
109
|
+
if (typeof intervalMs !== "number" || !isFinite(intervalMs) || intervalMs < 0) {
|
|
110
|
+
throw new OtelExportError("otel-export/bad-interval",
|
|
111
|
+
"create: intervalMs must be a non-negative finite number");
|
|
112
|
+
}
|
|
113
|
+
var httpClient = opts.httpClient || defaultHttpClient;
|
|
114
|
+
var scopeName = (opts.scope && opts.scope.name) || "blamejs";
|
|
115
|
+
var scopeVersion = (opts.scope && opts.scope.version) || "0.5.x";
|
|
116
|
+
var resourceAttrs = Object.assign({ "service.name": serviceName },
|
|
117
|
+
opts.resourceAttributes || {});
|
|
118
|
+
|
|
119
|
+
// Buckets: counters and observations keyed by (name, sorted-attrs).
|
|
120
|
+
var counters = new Map(); // bucketKey → { name, attrs, value, startUnixNano }
|
|
121
|
+
var observations = new Map(); // bucketKey → { name, attrs, sum, count, min, max, startUnixNano }
|
|
122
|
+
var startUnixNano = String(Date.now() * 1e6);
|
|
123
|
+
var loop = null;
|
|
124
|
+
var closed = false;
|
|
125
|
+
|
|
126
|
+
function recordCounter(name, value, attrs) {
|
|
127
|
+
if (closed) return;
|
|
128
|
+
if (typeof name !== "string" || name.length === 0) return;
|
|
129
|
+
var v = typeof value === "number" && isFinite(value) ? value : 1;
|
|
130
|
+
var key = _bucketKey(name, attrs);
|
|
131
|
+
var b = counters.get(key);
|
|
132
|
+
if (!b) {
|
|
133
|
+
b = { name: name, attrs: attrs || {}, value: 0, startUnixNano: startUnixNano };
|
|
134
|
+
counters.set(key, b);
|
|
135
|
+
}
|
|
136
|
+
b.value += v;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function recordObservation(name, value, attrs) {
|
|
140
|
+
if (closed) return;
|
|
141
|
+
if (typeof name !== "string" || name.length === 0) return;
|
|
142
|
+
if (typeof value !== "number" || !isFinite(value)) return;
|
|
143
|
+
var key = _bucketKey(name, attrs);
|
|
144
|
+
var b = observations.get(key);
|
|
145
|
+
if (!b) {
|
|
146
|
+
b = { name: name, attrs: attrs || {}, sum: 0, count: 0, min: value, max: value, startUnixNano: startUnixNano };
|
|
147
|
+
observations.set(key, b);
|
|
148
|
+
}
|
|
149
|
+
b.sum += value;
|
|
150
|
+
b.count += 1;
|
|
151
|
+
if (value < b.min) b.min = value;
|
|
152
|
+
if (value > b.max) b.max = value;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Operators wire this as the observability tap. event(name, value, labels)
|
|
156
|
+
// → recordCounter for value=1 fire-and-forget shapes.
|
|
157
|
+
function tapHandler(name, value, labels) {
|
|
158
|
+
recordCounter(name, value, labels);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function _drainAndEncode() {
|
|
162
|
+
var nowUnixNano = String(Date.now() * 1e6);
|
|
163
|
+
var metrics = [];
|
|
164
|
+
var c, o;
|
|
165
|
+
|
|
166
|
+
counters.forEach(function (entry) {
|
|
167
|
+
metrics.push({
|
|
168
|
+
name: entry.name,
|
|
169
|
+
sum: {
|
|
170
|
+
dataPoints: [{
|
|
171
|
+
attributes: _attrsToOtlp(entry.attrs),
|
|
172
|
+
startTimeUnixNano: entry.startUnixNano,
|
|
173
|
+
timeUnixNano: nowUnixNano,
|
|
174
|
+
asDouble: entry.value,
|
|
175
|
+
}],
|
|
176
|
+
aggregationTemporality: TEMPORALITY_DELTA,
|
|
177
|
+
isMonotonic: true,
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
void c;
|
|
182
|
+
observations.forEach(function (entry) {
|
|
183
|
+
metrics.push({
|
|
184
|
+
name: entry.name,
|
|
185
|
+
summary: {
|
|
186
|
+
dataPoints: [{
|
|
187
|
+
attributes: _attrsToOtlp(entry.attrs),
|
|
188
|
+
startTimeUnixNano: entry.startUnixNano,
|
|
189
|
+
timeUnixNano: nowUnixNano,
|
|
190
|
+
count: String(entry.count),
|
|
191
|
+
sum: entry.sum,
|
|
192
|
+
quantileValues: [
|
|
193
|
+
{ quantile: 0, value: entry.min },
|
|
194
|
+
{ quantile: 1, value: entry.max },
|
|
195
|
+
],
|
|
196
|
+
}],
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
void o;
|
|
201
|
+
|
|
202
|
+
// Reset buckets (DELTA temporality — each export is the delta).
|
|
203
|
+
counters.clear();
|
|
204
|
+
observations.clear();
|
|
205
|
+
startUnixNano = nowUnixNano;
|
|
206
|
+
if (metrics.length === 0) return null;
|
|
207
|
+
return {
|
|
208
|
+
resourceMetrics: [{
|
|
209
|
+
resource: { attributes: _attrsToOtlp(resourceAttrs) },
|
|
210
|
+
scopeMetrics: [{
|
|
211
|
+
scope: { name: scopeName, version: scopeVersion },
|
|
212
|
+
metrics: metrics,
|
|
213
|
+
}],
|
|
214
|
+
}],
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async function flush() {
|
|
219
|
+
var payload = _drainAndEncode();
|
|
220
|
+
if (!payload) return { sent: false, reason: "no-data" };
|
|
221
|
+
var body = JSON.stringify(payload);
|
|
222
|
+
try {
|
|
223
|
+
var res = await httpClient.request({
|
|
224
|
+
method: "POST",
|
|
225
|
+
url: endpoint,
|
|
226
|
+
headers: Object.assign({ "Content-Type": "application/json" }, headers),
|
|
227
|
+
body: body,
|
|
228
|
+
});
|
|
229
|
+
if (res.statusCode < 200 || res.statusCode >= 300) {
|
|
230
|
+
throw new OtelExportError("otel-export/upstream-rejected",
|
|
231
|
+
"OTLP endpoint returned " + res.statusCode);
|
|
232
|
+
}
|
|
233
|
+
return { sent: true, statusCode: res.statusCode, bodyLength: body.length };
|
|
234
|
+
} catch (e) {
|
|
235
|
+
if (e && e.isOtelExportError) throw e;
|
|
236
|
+
throw new OtelExportError("otel-export/send-failed",
|
|
237
|
+
"OTLP send failed: " + ((e && e.message) || String(e)));
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (intervalMs > 0) {
|
|
242
|
+
loop = safeAsync.flushLoop(flush, intervalMs, { name: "otel-flush" });
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function close() {
|
|
246
|
+
if (closed) return;
|
|
247
|
+
closed = true;
|
|
248
|
+
if (loop) { loop.stop(); loop = null; }
|
|
249
|
+
return flush().catch(function (_e) { /* close path swallows final-flush errors */ });
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return {
|
|
253
|
+
recordCounter: recordCounter,
|
|
254
|
+
recordObservation: recordObservation,
|
|
255
|
+
tapHandler: tapHandler,
|
|
256
|
+
flush: flush,
|
|
257
|
+
close: close,
|
|
258
|
+
get bufferedCounters() { return counters.size; },
|
|
259
|
+
get bufferedObservations() { return observations.size; },
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
module.exports = {
|
|
264
|
+
create: create,
|
|
265
|
+
OtelExportError: OtelExportError,
|
|
266
|
+
// Test-only encoders for unit-testing the OTLP shape without an HTTP client.
|
|
267
|
+
_attrsToOtlpForTest: _attrsToOtlp,
|
|
268
|
+
_bucketKeyForTest: _bucketKey,
|
|
269
|
+
};
|