@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/log-stream-otlp.js
CHANGED
|
@@ -1,286 +1,286 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* OTLP/HTTP-JSON log sink — OpenTelemetry Protocol logs over HTTP/JSON.
|
|
4
|
-
*
|
|
5
|
-
* Per the OTLP spec (v1.3+, Logs Data Model), log records are wrapped in
|
|
6
|
-
* a ResourceLogs envelope and POSTed to the operator's collector
|
|
7
|
-
* endpoint. Marshalling is OTel-native: timeUnixNano (string), severity
|
|
8
|
-
* mapping per OTel spec, attribute typing.
|
|
9
|
-
*
|
|
10
|
-
* Operator config:
|
|
11
|
-
*
|
|
12
|
-
* {
|
|
13
|
-
* url: "https://otel-collector.example.com:4318/v1/logs"
|
|
14
|
-
* // OR the collector root — "/v1/logs" auto-appended
|
|
15
|
-
* headers: { ... } // additional headers (api keys, tenant ids)
|
|
16
|
-
* auth: 'none' | 'bearer' | 'basic' | 'header'
|
|
17
|
-
* token / username+password
|
|
18
|
-
* serviceName: "my-service" // emitted as resource service.name
|
|
19
|
-
* serviceVersion: "1.2.3" // resource service.version
|
|
20
|
-
* resourceAttributes: { env: "prod", region: "us-east-1" }
|
|
21
|
-
* // additional resource attributes
|
|
22
|
-
* scopeName: "blamejs" // instrumentation scope name
|
|
23
|
-
* batchSize: 100
|
|
24
|
-
* maxBatchAgeMs: C.TIME.seconds(5)
|
|
25
|
-
* timeoutMs: C.TIME.seconds(30)
|
|
26
|
-
* retry: { maxAttempts, baseDelayMs, ... }
|
|
27
|
-
* bufferLimit: 10000 // ring-buffer cap; drops oldest on overflow
|
|
28
|
-
* onDrop: function ({ reason, batch, error }) { ... }
|
|
29
|
-
* }
|
|
30
|
-
*
|
|
31
|
-
* Severity mapping (per OTel Logs Data Model):
|
|
32
|
-
* debug → 5 (DEBUG)
|
|
33
|
-
* info → 9 (INFO)
|
|
34
|
-
* warn → 13 (WARN)
|
|
35
|
-
* error → 17 (ERROR)
|
|
36
|
-
*
|
|
37
|
-
* Endpoint convention: most OTel collectors expose:
|
|
38
|
-
* <base>:4318/v1/logs — HTTP/JSON path (per OTLP HTTP §4.5)
|
|
39
|
-
* <base>:4317 — gRPC path (NOT supported here)
|
|
40
|
-
*
|
|
41
|
-
* If `url` ends in `/v1/logs` it's used as-is. If not, `/v1/logs` is
|
|
42
|
-
* appended (collector root → logs endpoint). Operators with a custom
|
|
43
|
-
* routing prefix pass the full URL.
|
|
44
|
-
*
|
|
45
|
-
* Why JSON not protobuf: protobuf needs a schema parser (vendored dep
|
|
46
|
-
* or hand-rolled). The OTLP/HTTP spec accepts both Content-Type:
|
|
47
|
-
* application/json and application/x-protobuf; the JSON variant is the
|
|
48
|
-
* "no extra deps" path consistent with the framework's vendoring stance.
|
|
49
|
-
* Operators benchmarking >100K logs/s ship the OTel Collector locally
|
|
50
|
-
* (collector → upstream gRPC) — this sink hands JSON to the local
|
|
51
|
-
* collector which forwards.
|
|
52
|
-
*/
|
|
53
|
-
var C = require("./constants");
|
|
54
|
-
var pkg = require("../package.json");
|
|
55
|
-
var retryHelper = require("./retry");
|
|
56
|
-
var { LogStreamError } = require("./framework-error");
|
|
57
|
-
var httpClient = require("./http-client");
|
|
58
|
-
var safeAsync = require("./safe-async");
|
|
59
|
-
var safeUrl = require("./safe-url");
|
|
60
|
-
var authHeader = require("./auth-header");
|
|
61
|
-
|
|
62
|
-
var MAX_RESPONSE_BYTES = C.BYTES.mib(1);
|
|
63
|
-
var FRAMEWORK_VERSION = (pkg && pkg.version) || "unknown";
|
|
64
|
-
|
|
65
|
-
// OTel Logs Data Model severity numbers.
|
|
66
|
-
// https://opentelemetry.io/docs/specs/otel/logs/data-model/#field-severitynumber
|
|
67
|
-
var SEVERITY = {
|
|
68
|
-
debug: { number: 5, text: "DEBUG" },
|
|
69
|
-
info: { number: 9, text: "INFO" },
|
|
70
|
-
warn: { number: 13, text: "WARN" },
|
|
71
|
-
error: { number: 17, text: "ERROR" },
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
var DEFAULTS = {
|
|
75
|
-
scopeName: "blamejs",
|
|
76
|
-
batchSize: 100,
|
|
77
|
-
maxBatchAgeMs: C.TIME.seconds(5),
|
|
78
|
-
timeoutMs: C.TIME.seconds(30),
|
|
79
|
-
// Ring-buffer cap (event count, not bytes); routed through C.BYTES
|
|
80
|
-
// identity passthrough so the file's literal arithmetic has a single
|
|
81
|
-
// source of truth.
|
|
82
|
-
bufferLimit: C.BYTES.bytes(10000),
|
|
83
|
-
};
|
|
84
|
-
|
|
85
|
-
var _err = LogStreamError.factory;
|
|
86
|
-
|
|
87
|
-
function _resolveUrl(url) {
|
|
88
|
-
// If the URL already ends in "/v1/logs" (with or without trailing slash),
|
|
89
|
-
// use as-is. Otherwise treat as the collector root and append.
|
|
90
|
-
var trimmed = url.replace(/\/+$/, "");
|
|
91
|
-
if (/\/v1\/logs$/.test(trimmed)) return trimmed;
|
|
92
|
-
return trimmed + "/v1/logs";
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function _authHeaders(config) {
|
|
96
|
-
if (config.auth === "header") return Object.assign({}, config.headers || {});
|
|
97
|
-
return authHeader.fromConfig(config);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// OTel attribute encoding — JSON form per OTLP spec. Each attribute is
|
|
101
|
-
// { key: <name>, value: { <type>Value: <v> } } where the type field
|
|
102
|
-
// matches the JS type at the boundary.
|
|
103
|
-
function _encodeAttrValue(v) {
|
|
104
|
-
if (v === null || v === undefined) return null;
|
|
105
|
-
var t = typeof v;
|
|
106
|
-
if (t === "string") return { stringValue: v };
|
|
107
|
-
if (t === "boolean") return { boolValue: v };
|
|
108
|
-
if (t === "number") {
|
|
109
|
-
if (Number.isInteger(v)) return { intValue: String(v) }; // OTLP int is string-encoded
|
|
110
|
-
return { doubleValue: v };
|
|
111
|
-
}
|
|
112
|
-
if (Array.isArray(v)) {
|
|
113
|
-
return { arrayValue: { values: v.map(function (e) { return _encodeAttrValue(e); }).filter(Boolean) } };
|
|
114
|
-
}
|
|
115
|
-
if (t === "object") {
|
|
116
|
-
return { kvlistValue: { values: _encodeAttrs(v) } };
|
|
117
|
-
}
|
|
118
|
-
return { stringValue: String(v) };
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function _encodeAttrs(obj) {
|
|
122
|
-
if (!obj || typeof obj !== "object") return [];
|
|
123
|
-
return Object.keys(obj).map(function (k) {
|
|
124
|
-
var encoded = _encodeAttrValue(obj[k]);
|
|
125
|
-
if (!encoded) return null;
|
|
126
|
-
return { key: k, value: encoded };
|
|
127
|
-
}).filter(Boolean);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function _resourceAttrs(cfg) {
|
|
131
|
-
var attrs = {};
|
|
132
|
-
if (cfg.serviceName) attrs["service.name"] = cfg.serviceName;
|
|
133
|
-
if (cfg.serviceVersion) attrs["service.version"] = cfg.serviceVersion;
|
|
134
|
-
if (cfg.resourceAttributes && typeof cfg.resourceAttributes === "object") {
|
|
135
|
-
Object.assign(attrs, cfg.resourceAttributes);
|
|
136
|
-
}
|
|
137
|
-
return attrs;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function _toLogRecord(record) {
|
|
141
|
-
var sev = SEVERITY[record.level] || SEVERITY.info;
|
|
142
|
-
// OTel timeUnixNano is a string (JSON can't safely represent 64-bit ints).
|
|
143
|
-
var nanos = String(BigInt(record.ts) * 1000000n);
|
|
144
|
-
var attrs = record.meta ? _encodeAttrs(record.meta) : [];
|
|
145
|
-
return {
|
|
146
|
-
timeUnixNano: nanos,
|
|
147
|
-
observedTimeUnixNano: nanos,
|
|
148
|
-
severityNumber: sev.number,
|
|
149
|
-
severityText: sev.text,
|
|
150
|
-
body: { stringValue: record.message == null ? "" : String(record.message) },
|
|
151
|
-
attributes: attrs,
|
|
152
|
-
};
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
function _serializeBatch(records, cfg, scopeVersion) {
|
|
156
|
-
var resourceAttrs = _resourceAttrs(cfg);
|
|
157
|
-
return Buffer.from(JSON.stringify({
|
|
158
|
-
resourceLogs: [
|
|
159
|
-
{
|
|
160
|
-
resource: {
|
|
161
|
-
attributes: _encodeAttrs(resourceAttrs),
|
|
162
|
-
},
|
|
163
|
-
scopeLogs: [
|
|
164
|
-
{
|
|
165
|
-
scope: {
|
|
166
|
-
name: cfg.scopeName,
|
|
167
|
-
version: scopeVersion,
|
|
168
|
-
},
|
|
169
|
-
logRecords: records.map(_toLogRecord),
|
|
170
|
-
},
|
|
171
|
-
],
|
|
172
|
-
},
|
|
173
|
-
],
|
|
174
|
-
}), "utf8");
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
function _post(url, body, headers, timeoutMs, allowedProtocols, allowInternal) {
|
|
178
|
-
return httpClient.request({
|
|
179
|
-
method: "POST",
|
|
180
|
-
url: url,
|
|
181
|
-
headers: headers,
|
|
182
|
-
body: body,
|
|
183
|
-
idleTimeoutMs: timeoutMs,
|
|
184
|
-
maxResponseBytes: MAX_RESPONSE_BYTES,
|
|
185
|
-
errorClass: LogStreamError,
|
|
186
|
-
allowedProtocols: allowedProtocols,
|
|
187
|
-
allowInternal: allowInternal,
|
|
188
|
-
});
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
function create(config) {
|
|
192
|
-
if (!config || !config.url) throw _err("BAD_OPT", "log-stream otlp requires { url }");
|
|
193
|
-
var cfg = Object.assign({}, DEFAULTS, config);
|
|
194
|
-
var resolvedUrl = _resolveUrl(cfg.url);
|
|
195
|
-
// Validate URL shape + scheme at create time. Default HTTPS-only;
|
|
196
|
-
// operators with an internal cleartext OTel collector pass
|
|
197
|
-
// cfg.allowedProtocols (safeUrl.ALLOW_HTTP_ALL).
|
|
198
|
-
safeUrl.parse(resolvedUrl, {
|
|
199
|
-
allowedProtocols: cfg.allowedProtocols || safeUrl.ALLOW_HTTP_TLS,
|
|
200
|
-
errorClass: LogStreamError,
|
|
201
|
-
});
|
|
202
|
-
var scopeVersion = cfg.scopeVersion || FRAMEWORK_VERSION;
|
|
203
|
-
var headers = Object.assign({
|
|
204
|
-
"Content-Type": "application/json",
|
|
205
|
-
"Accept": "application/json",
|
|
206
|
-
}, _authHeaders(cfg));
|
|
207
|
-
var onDrop = typeof cfg.onDrop === "function" ? cfg.onDrop : null;
|
|
208
|
-
var _emitDrop = safeAsync.makeDropCallback(onDrop);
|
|
209
|
-
var buffer = [];
|
|
210
|
-
var dropCount = 0;
|
|
211
|
-
var inFlight = false;
|
|
212
|
-
var closed = false;
|
|
213
|
-
var flushScheduler = safeAsync.makeScheduledFlush(cfg.maxBatchAgeMs, function () { return _flush(); });
|
|
214
|
-
|
|
215
|
-
async function _flush() {
|
|
216
|
-
if (inFlight) return;
|
|
217
|
-
if (buffer.length === 0) return;
|
|
218
|
-
inFlight = true;
|
|
219
|
-
try {
|
|
220
|
-
while (buffer.length > 0 && !closed) {
|
|
221
|
-
var batch = buffer.splice(0, cfg.batchSize);
|
|
222
|
-
var body = _serializeBatch(batch, cfg, scopeVersion);
|
|
223
|
-
try {
|
|
224
|
-
await retryHelper.withRetry(function () {
|
|
225
|
-
return _post(resolvedUrl, body, headers, cfg.timeoutMs, cfg.allowedProtocols, cfg.allowInternal);
|
|
226
|
-
}, cfg.retry);
|
|
227
|
-
} catch (e) {
|
|
228
|
-
dropCount += batch.length;
|
|
229
|
-
_emitDrop("retry-exhausted", batch, e);
|
|
230
|
-
break;
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
} finally {
|
|
234
|
-
inFlight = false;
|
|
235
|
-
if (buffer.length > 0) flushScheduler.schedule();
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
function emit(record) {
|
|
240
|
-
if (closed) return Promise.resolve({ accepted: false, reason: "sink closed" });
|
|
241
|
-
if (buffer.length >= cfg.bufferLimit) {
|
|
242
|
-
var dropped = buffer.shift();
|
|
243
|
-
dropCount += 1;
|
|
244
|
-
_emitDrop("overflow", [dropped], null);
|
|
245
|
-
}
|
|
246
|
-
buffer.push(record);
|
|
247
|
-
if (buffer.length >= cfg.batchSize) {
|
|
248
|
-
_flush().catch(function () {});
|
|
249
|
-
} else {
|
|
250
|
-
flushScheduler.schedule();
|
|
251
|
-
}
|
|
252
|
-
return Promise.resolve({ accepted: true, queued: buffer.length });
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
async function close() {
|
|
256
|
-
closed = true;
|
|
257
|
-
flushScheduler.cancel();
|
|
258
|
-
await _flush();
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
function stats() {
|
|
262
|
-
return {
|
|
263
|
-
queued: buffer.length,
|
|
264
|
-
dropped: dropCount,
|
|
265
|
-
inFlight: inFlight,
|
|
266
|
-
url: resolvedUrl,
|
|
267
|
-
};
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
return {
|
|
271
|
-
protocol: "otlp",
|
|
272
|
-
emit: emit,
|
|
273
|
-
close: close,
|
|
274
|
-
stats: stats,
|
|
275
|
-
flush: _flush,
|
|
276
|
-
};
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
module.exports = {
|
|
280
|
-
create: create,
|
|
281
|
-
// Exposed for tests + advanced operator wiring.
|
|
282
|
-
_resolveUrl: _resolveUrl,
|
|
283
|
-
_encodeAttrs: _encodeAttrs,
|
|
284
|
-
_toLogRecord: _toLogRecord,
|
|
285
|
-
SEVERITY: SEVERITY,
|
|
286
|
-
};
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* OTLP/HTTP-JSON log sink — OpenTelemetry Protocol logs over HTTP/JSON.
|
|
4
|
+
*
|
|
5
|
+
* Per the OTLP spec (v1.3+, Logs Data Model), log records are wrapped in
|
|
6
|
+
* a ResourceLogs envelope and POSTed to the operator's collector
|
|
7
|
+
* endpoint. Marshalling is OTel-native: timeUnixNano (string), severity
|
|
8
|
+
* mapping per OTel spec, attribute typing.
|
|
9
|
+
*
|
|
10
|
+
* Operator config:
|
|
11
|
+
*
|
|
12
|
+
* {
|
|
13
|
+
* url: "https://otel-collector.example.com:4318/v1/logs"
|
|
14
|
+
* // OR the collector root — "/v1/logs" auto-appended
|
|
15
|
+
* headers: { ... } // additional headers (api keys, tenant ids)
|
|
16
|
+
* auth: 'none' | 'bearer' | 'basic' | 'header'
|
|
17
|
+
* token / username+password
|
|
18
|
+
* serviceName: "my-service" // emitted as resource service.name
|
|
19
|
+
* serviceVersion: "1.2.3" // resource service.version
|
|
20
|
+
* resourceAttributes: { env: "prod", region: "us-east-1" }
|
|
21
|
+
* // additional resource attributes
|
|
22
|
+
* scopeName: "blamejs" // instrumentation scope name
|
|
23
|
+
* batchSize: 100
|
|
24
|
+
* maxBatchAgeMs: C.TIME.seconds(5)
|
|
25
|
+
* timeoutMs: C.TIME.seconds(30)
|
|
26
|
+
* retry: { maxAttempts, baseDelayMs, ... }
|
|
27
|
+
* bufferLimit: 10000 // ring-buffer cap; drops oldest on overflow
|
|
28
|
+
* onDrop: function ({ reason, batch, error }) { ... }
|
|
29
|
+
* }
|
|
30
|
+
*
|
|
31
|
+
* Severity mapping (per OTel Logs Data Model):
|
|
32
|
+
* debug → 5 (DEBUG)
|
|
33
|
+
* info → 9 (INFO)
|
|
34
|
+
* warn → 13 (WARN)
|
|
35
|
+
* error → 17 (ERROR)
|
|
36
|
+
*
|
|
37
|
+
* Endpoint convention: most OTel collectors expose:
|
|
38
|
+
* <base>:4318/v1/logs — HTTP/JSON path (per OTLP HTTP §4.5)
|
|
39
|
+
* <base>:4317 — gRPC path (NOT supported here)
|
|
40
|
+
*
|
|
41
|
+
* If `url` ends in `/v1/logs` it's used as-is. If not, `/v1/logs` is
|
|
42
|
+
* appended (collector root → logs endpoint). Operators with a custom
|
|
43
|
+
* routing prefix pass the full URL.
|
|
44
|
+
*
|
|
45
|
+
* Why JSON not protobuf: protobuf needs a schema parser (vendored dep
|
|
46
|
+
* or hand-rolled). The OTLP/HTTP spec accepts both Content-Type:
|
|
47
|
+
* application/json and application/x-protobuf; the JSON variant is the
|
|
48
|
+
* "no extra deps" path consistent with the framework's vendoring stance.
|
|
49
|
+
* Operators benchmarking >100K logs/s ship the OTel Collector locally
|
|
50
|
+
* (collector → upstream gRPC) — this sink hands JSON to the local
|
|
51
|
+
* collector which forwards.
|
|
52
|
+
*/
|
|
53
|
+
var C = require("./constants");
|
|
54
|
+
var pkg = require("../package.json");
|
|
55
|
+
var retryHelper = require("./retry");
|
|
56
|
+
var { LogStreamError } = require("./framework-error");
|
|
57
|
+
var httpClient = require("./http-client");
|
|
58
|
+
var safeAsync = require("./safe-async");
|
|
59
|
+
var safeUrl = require("./safe-url");
|
|
60
|
+
var authHeader = require("./auth-header");
|
|
61
|
+
|
|
62
|
+
var MAX_RESPONSE_BYTES = C.BYTES.mib(1);
|
|
63
|
+
var FRAMEWORK_VERSION = (pkg && pkg.version) || "unknown";
|
|
64
|
+
|
|
65
|
+
// OTel Logs Data Model severity numbers.
|
|
66
|
+
// https://opentelemetry.io/docs/specs/otel/logs/data-model/#field-severitynumber
|
|
67
|
+
var SEVERITY = {
|
|
68
|
+
debug: { number: 5, text: "DEBUG" },
|
|
69
|
+
info: { number: 9, text: "INFO" },
|
|
70
|
+
warn: { number: 13, text: "WARN" },
|
|
71
|
+
error: { number: 17, text: "ERROR" },
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
var DEFAULTS = {
|
|
75
|
+
scopeName: "blamejs",
|
|
76
|
+
batchSize: 100,
|
|
77
|
+
maxBatchAgeMs: C.TIME.seconds(5),
|
|
78
|
+
timeoutMs: C.TIME.seconds(30),
|
|
79
|
+
// Ring-buffer cap (event count, not bytes); routed through C.BYTES
|
|
80
|
+
// identity passthrough so the file's literal arithmetic has a single
|
|
81
|
+
// source of truth.
|
|
82
|
+
bufferLimit: C.BYTES.bytes(10000),
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
var _err = LogStreamError.factory;
|
|
86
|
+
|
|
87
|
+
function _resolveUrl(url) {
|
|
88
|
+
// If the URL already ends in "/v1/logs" (with or without trailing slash),
|
|
89
|
+
// use as-is. Otherwise treat as the collector root and append.
|
|
90
|
+
var trimmed = url.replace(/\/+$/, "");
|
|
91
|
+
if (/\/v1\/logs$/.test(trimmed)) return trimmed;
|
|
92
|
+
return trimmed + "/v1/logs";
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function _authHeaders(config) {
|
|
96
|
+
if (config.auth === "header") return Object.assign({}, config.headers || {});
|
|
97
|
+
return authHeader.fromConfig(config);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// OTel attribute encoding — JSON form per OTLP spec. Each attribute is
|
|
101
|
+
// { key: <name>, value: { <type>Value: <v> } } where the type field
|
|
102
|
+
// matches the JS type at the boundary.
|
|
103
|
+
function _encodeAttrValue(v) {
|
|
104
|
+
if (v === null || v === undefined) return null;
|
|
105
|
+
var t = typeof v;
|
|
106
|
+
if (t === "string") return { stringValue: v };
|
|
107
|
+
if (t === "boolean") return { boolValue: v };
|
|
108
|
+
if (t === "number") {
|
|
109
|
+
if (Number.isInteger(v)) return { intValue: String(v) }; // OTLP int is string-encoded
|
|
110
|
+
return { doubleValue: v };
|
|
111
|
+
}
|
|
112
|
+
if (Array.isArray(v)) {
|
|
113
|
+
return { arrayValue: { values: v.map(function (e) { return _encodeAttrValue(e); }).filter(Boolean) } };
|
|
114
|
+
}
|
|
115
|
+
if (t === "object") {
|
|
116
|
+
return { kvlistValue: { values: _encodeAttrs(v) } };
|
|
117
|
+
}
|
|
118
|
+
return { stringValue: String(v) };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function _encodeAttrs(obj) {
|
|
122
|
+
if (!obj || typeof obj !== "object") return [];
|
|
123
|
+
return Object.keys(obj).map(function (k) {
|
|
124
|
+
var encoded = _encodeAttrValue(obj[k]);
|
|
125
|
+
if (!encoded) return null;
|
|
126
|
+
return { key: k, value: encoded };
|
|
127
|
+
}).filter(Boolean);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function _resourceAttrs(cfg) {
|
|
131
|
+
var attrs = {};
|
|
132
|
+
if (cfg.serviceName) attrs["service.name"] = cfg.serviceName;
|
|
133
|
+
if (cfg.serviceVersion) attrs["service.version"] = cfg.serviceVersion;
|
|
134
|
+
if (cfg.resourceAttributes && typeof cfg.resourceAttributes === "object") {
|
|
135
|
+
Object.assign(attrs, cfg.resourceAttributes);
|
|
136
|
+
}
|
|
137
|
+
return attrs;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function _toLogRecord(record) {
|
|
141
|
+
var sev = SEVERITY[record.level] || SEVERITY.info;
|
|
142
|
+
// OTel timeUnixNano is a string (JSON can't safely represent 64-bit ints).
|
|
143
|
+
var nanos = String(BigInt(record.ts) * 1000000n);
|
|
144
|
+
var attrs = record.meta ? _encodeAttrs(record.meta) : [];
|
|
145
|
+
return {
|
|
146
|
+
timeUnixNano: nanos,
|
|
147
|
+
observedTimeUnixNano: nanos,
|
|
148
|
+
severityNumber: sev.number,
|
|
149
|
+
severityText: sev.text,
|
|
150
|
+
body: { stringValue: record.message == null ? "" : String(record.message) },
|
|
151
|
+
attributes: attrs,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function _serializeBatch(records, cfg, scopeVersion) {
|
|
156
|
+
var resourceAttrs = _resourceAttrs(cfg);
|
|
157
|
+
return Buffer.from(JSON.stringify({
|
|
158
|
+
resourceLogs: [
|
|
159
|
+
{
|
|
160
|
+
resource: {
|
|
161
|
+
attributes: _encodeAttrs(resourceAttrs),
|
|
162
|
+
},
|
|
163
|
+
scopeLogs: [
|
|
164
|
+
{
|
|
165
|
+
scope: {
|
|
166
|
+
name: cfg.scopeName,
|
|
167
|
+
version: scopeVersion,
|
|
168
|
+
},
|
|
169
|
+
logRecords: records.map(_toLogRecord),
|
|
170
|
+
},
|
|
171
|
+
],
|
|
172
|
+
},
|
|
173
|
+
],
|
|
174
|
+
}), "utf8");
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function _post(url, body, headers, timeoutMs, allowedProtocols, allowInternal) {
|
|
178
|
+
return httpClient.request({
|
|
179
|
+
method: "POST",
|
|
180
|
+
url: url,
|
|
181
|
+
headers: headers,
|
|
182
|
+
body: body,
|
|
183
|
+
idleTimeoutMs: timeoutMs,
|
|
184
|
+
maxResponseBytes: MAX_RESPONSE_BYTES,
|
|
185
|
+
errorClass: LogStreamError,
|
|
186
|
+
allowedProtocols: allowedProtocols,
|
|
187
|
+
allowInternal: allowInternal,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function create(config) {
|
|
192
|
+
if (!config || !config.url) throw _err("BAD_OPT", "log-stream otlp requires { url }");
|
|
193
|
+
var cfg = Object.assign({}, DEFAULTS, config);
|
|
194
|
+
var resolvedUrl = _resolveUrl(cfg.url);
|
|
195
|
+
// Validate URL shape + scheme at create time. Default HTTPS-only;
|
|
196
|
+
// operators with an internal cleartext OTel collector pass
|
|
197
|
+
// cfg.allowedProtocols (safeUrl.ALLOW_HTTP_ALL).
|
|
198
|
+
safeUrl.parse(resolvedUrl, {
|
|
199
|
+
allowedProtocols: cfg.allowedProtocols || safeUrl.ALLOW_HTTP_TLS,
|
|
200
|
+
errorClass: LogStreamError,
|
|
201
|
+
});
|
|
202
|
+
var scopeVersion = cfg.scopeVersion || FRAMEWORK_VERSION;
|
|
203
|
+
var headers = Object.assign({
|
|
204
|
+
"Content-Type": "application/json",
|
|
205
|
+
"Accept": "application/json",
|
|
206
|
+
}, _authHeaders(cfg));
|
|
207
|
+
var onDrop = typeof cfg.onDrop === "function" ? cfg.onDrop : null;
|
|
208
|
+
var _emitDrop = safeAsync.makeDropCallback(onDrop);
|
|
209
|
+
var buffer = [];
|
|
210
|
+
var dropCount = 0;
|
|
211
|
+
var inFlight = false;
|
|
212
|
+
var closed = false;
|
|
213
|
+
var flushScheduler = safeAsync.makeScheduledFlush(cfg.maxBatchAgeMs, function () { return _flush(); });
|
|
214
|
+
|
|
215
|
+
async function _flush() {
|
|
216
|
+
if (inFlight) return;
|
|
217
|
+
if (buffer.length === 0) return;
|
|
218
|
+
inFlight = true;
|
|
219
|
+
try {
|
|
220
|
+
while (buffer.length > 0 && !closed) {
|
|
221
|
+
var batch = buffer.splice(0, cfg.batchSize);
|
|
222
|
+
var body = _serializeBatch(batch, cfg, scopeVersion);
|
|
223
|
+
try {
|
|
224
|
+
await retryHelper.withRetry(function () {
|
|
225
|
+
return _post(resolvedUrl, body, headers, cfg.timeoutMs, cfg.allowedProtocols, cfg.allowInternal);
|
|
226
|
+
}, cfg.retry);
|
|
227
|
+
} catch (e) {
|
|
228
|
+
dropCount += batch.length;
|
|
229
|
+
_emitDrop("retry-exhausted", batch, e);
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
} finally {
|
|
234
|
+
inFlight = false;
|
|
235
|
+
if (buffer.length > 0) flushScheduler.schedule();
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function emit(record) {
|
|
240
|
+
if (closed) return Promise.resolve({ accepted: false, reason: "sink closed" });
|
|
241
|
+
if (buffer.length >= cfg.bufferLimit) {
|
|
242
|
+
var dropped = buffer.shift();
|
|
243
|
+
dropCount += 1;
|
|
244
|
+
_emitDrop("overflow", [dropped], null);
|
|
245
|
+
}
|
|
246
|
+
buffer.push(record);
|
|
247
|
+
if (buffer.length >= cfg.batchSize) {
|
|
248
|
+
_flush().catch(function () {});
|
|
249
|
+
} else {
|
|
250
|
+
flushScheduler.schedule();
|
|
251
|
+
}
|
|
252
|
+
return Promise.resolve({ accepted: true, queued: buffer.length });
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async function close() {
|
|
256
|
+
closed = true;
|
|
257
|
+
flushScheduler.cancel();
|
|
258
|
+
await _flush();
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function stats() {
|
|
262
|
+
return {
|
|
263
|
+
queued: buffer.length,
|
|
264
|
+
dropped: dropCount,
|
|
265
|
+
inFlight: inFlight,
|
|
266
|
+
url: resolvedUrl,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return {
|
|
271
|
+
protocol: "otlp",
|
|
272
|
+
emit: emit,
|
|
273
|
+
close: close,
|
|
274
|
+
stats: stats,
|
|
275
|
+
flush: _flush,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
module.exports = {
|
|
280
|
+
create: create,
|
|
281
|
+
// Exposed for tests + advanced operator wiring.
|
|
282
|
+
_resolveUrl: _resolveUrl,
|
|
283
|
+
_encodeAttrs: _encodeAttrs,
|
|
284
|
+
_toLogRecord: _toLogRecord,
|
|
285
|
+
SEVERITY: SEVERITY,
|
|
286
|
+
};
|