@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-syslog.js
CHANGED
|
@@ -1,302 +1,302 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* Syslog log-stream sink — RFC 5424 framing over UDP / TCP / TLS.
|
|
4
|
-
*
|
|
5
|
-
* Wire format (RFC 5424 §6):
|
|
6
|
-
* <PRI>VERSION SP TIMESTAMP SP HOSTNAME SP APP-NAME SP PROCID SP MSGID SP STRUCTURED-DATA SP MSG
|
|
7
|
-
*
|
|
8
|
-
* PRI = (facility * 8) + severity
|
|
9
|
-
* facility (default 16 = local0; 1 = user — operators commonly pick
|
|
10
|
-
* local0..local7 for app-emitted records)
|
|
11
|
-
* severity is mapped from the framework's level field:
|
|
12
|
-
* debug → 7, info → 6, warn → 4, error → 3
|
|
13
|
-
*
|
|
14
|
-
* Transport:
|
|
15
|
-
* udp — single datagram per record (no framing)
|
|
16
|
-
* tcp — octet-counting framing (RFC 6587 §3.4.1):
|
|
17
|
-
* "<length> <message>" with a SPACE between
|
|
18
|
-
* length and the rfc5424 message bytes
|
|
19
|
-
* tls — same octet-counting framing on a TLS socket
|
|
20
|
-
* (RFC 5425). Standard port 6514.
|
|
21
|
-
*
|
|
22
|
-
* Defaults match RFC 3164/5424 conventions: appName = "blamejs",
|
|
23
|
-
* facility = local0 (16), hostname = os.hostname(), structuredData = "-".
|
|
24
|
-
*
|
|
25
|
-
* Flow control:
|
|
26
|
-
* The TCP / TLS variants buffer pending writes during socket
|
|
27
|
-
* reconnect and replay them on the new connection. UDP is best-effort
|
|
28
|
-
* (datagrams that race a closed socket are dropped to onDrop).
|
|
29
|
-
*/
|
|
30
|
-
var dgram = require("dgram");
|
|
31
|
-
var net = require("net");
|
|
32
|
-
var os = require("os");
|
|
33
|
-
var tls = require("tls");
|
|
34
|
-
var C = require("./constants");
|
|
35
|
-
var { boot } = require("./log");
|
|
36
|
-
var safeAsync = require("./safe-async");
|
|
37
|
-
var safeUrl = require("./safe-url");
|
|
38
|
-
var { LogStreamError } = require("./framework-error");
|
|
39
|
-
|
|
40
|
-
var _err = LogStreamError.factory;
|
|
41
|
-
var log = boot("log-stream-syslog");
|
|
42
|
-
|
|
43
|
-
// RFC 5424 facility codes (0-23). 0x10 = 16 = local0 — the
|
|
44
|
-
// conventional facility for app-emitted records.
|
|
45
|
-
var DEFAULT_FACILITY = 0x10;
|
|
46
|
-
var DEFAULT_APP_NAME = "blamejs";
|
|
47
|
-
var DEFAULT_PROC_ID = String(process.pid);
|
|
48
|
-
var DEFAULT_MSG_ID = "-";
|
|
49
|
-
var DEFAULT_STRUCT_DATA = "-";
|
|
50
|
-
var TCP_DEFAULT_PORT = 514;
|
|
51
|
-
var TLS_DEFAULT_PORT = 6514;
|
|
52
|
-
var UDP_DEFAULT_PORT = 514;
|
|
53
|
-
var DEFAULT_TIMEOUT_MS = C.TIME.seconds(10);
|
|
54
|
-
var DEFAULT_RECONNECT_BASE_MS = 250;
|
|
55
|
-
var DEFAULT_RECONNECT_MAX_MS = C.TIME.seconds(30);
|
|
56
|
-
var DEFAULT_BUFFER_LIMIT = C.BYTES.bytes(10000);
|
|
57
|
-
|
|
58
|
-
// RFC 5424 severity codes from the framework's level names.
|
|
59
|
-
var LEVEL_TO_SEVERITY = {
|
|
60
|
-
debug: 7,
|
|
61
|
-
info: 6,
|
|
62
|
-
warn: 4,
|
|
63
|
-
error: 3,
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
function _toRfc3339(tsMs) {
|
|
67
|
-
return new Date(tsMs).toISOString();
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function _formatRfc5424(record, cfg) {
|
|
71
|
-
var severity = LEVEL_TO_SEVERITY[record.level] != null
|
|
72
|
-
? LEVEL_TO_SEVERITY[record.level] : 6;
|
|
73
|
-
// RFC 5424 §6.2.1: PRI = facility << 3 | severity (severity is 3 bits).
|
|
74
|
-
var pri = (cfg.facility << 3) + severity;
|
|
75
|
-
var ts = _toRfc3339(record.ts || Date.now());
|
|
76
|
-
// Body — JSON-encode meta + message together so the structured
|
|
77
|
-
// payload survives the wire as a single MSG token. Operators with
|
|
78
|
-
// their own RFC 5424 STRUCTURED-DATA producers pass cfg.structuredData
|
|
79
|
-
// (string) to override the default "-".
|
|
80
|
-
var body = record.message || "";
|
|
81
|
-
if (record.meta && Object.keys(record.meta).length > 0) {
|
|
82
|
-
try { body += " " + JSON.stringify(record.meta); }
|
|
83
|
-
catch (_e) { /* best-effort */ }
|
|
84
|
-
}
|
|
85
|
-
return "<" + pri + ">1 " + ts + " " + cfg.hostname + " " +
|
|
86
|
-
cfg.appName + " " + cfg.procId + " " + cfg.msgId + " " +
|
|
87
|
-
(cfg.structuredData || "-") + " " + body;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function create(config) {
|
|
91
|
-
if (!config) throw _err("BAD_OPT", "log-stream syslog requires { url } (e.g. udp://host:514, tcp://host:514, tls://host:6514)");
|
|
92
|
-
var url = config.url;
|
|
93
|
-
if (typeof url !== "string" || url.length === 0) {
|
|
94
|
-
throw _err("BAD_OPT", "log-stream syslog requires { url } (string)");
|
|
95
|
-
}
|
|
96
|
-
// Parse URL — accept udp://, tcp://, tls:// only.
|
|
97
|
-
var parsed;
|
|
98
|
-
try {
|
|
99
|
-
parsed = safeUrl.parse(url, {
|
|
100
|
-
allowedProtocols: ["udp:", "tcp:", "tls:"],
|
|
101
|
-
errorClass: LogStreamError,
|
|
102
|
-
});
|
|
103
|
-
} catch (e) {
|
|
104
|
-
throw _err("BAD_URL", "log-stream syslog: bad url '" + url + "': " +
|
|
105
|
-
((e && e.message) || String(e)));
|
|
106
|
-
}
|
|
107
|
-
var transport = parsed.protocol.replace(/:$/, "").toLowerCase();
|
|
108
|
-
var defaultPort = transport === "tls" ? TLS_DEFAULT_PORT
|
|
109
|
-
: transport === "tcp" ? TCP_DEFAULT_PORT
|
|
110
|
-
: UDP_DEFAULT_PORT;
|
|
111
|
-
var host = parsed.hostname;
|
|
112
|
-
var port = parsed.port ? parseInt(parsed.port, 10) : defaultPort;
|
|
113
|
-
|
|
114
|
-
var cfg = {
|
|
115
|
-
transport: transport,
|
|
116
|
-
host: host,
|
|
117
|
-
port: port,
|
|
118
|
-
facility: (typeof config.facility === "number" && config.facility >= 0 && config.facility <= 23)
|
|
119
|
-
? Math.floor(config.facility) : DEFAULT_FACILITY,
|
|
120
|
-
appName: config.appName || DEFAULT_APP_NAME,
|
|
121
|
-
procId: config.procId || DEFAULT_PROC_ID,
|
|
122
|
-
msgId: config.msgId || DEFAULT_MSG_ID,
|
|
123
|
-
hostname: config.hostname || os.hostname(),
|
|
124
|
-
structuredData: config.structuredData || DEFAULT_STRUCT_DATA,
|
|
125
|
-
timeoutMs: config.timeoutMs || DEFAULT_TIMEOUT_MS,
|
|
126
|
-
bufferLimit: config.bufferLimit || DEFAULT_BUFFER_LIMIT,
|
|
127
|
-
reconnectBaseMs: config.reconnectBaseMs || DEFAULT_RECONNECT_BASE_MS,
|
|
128
|
-
reconnectMaxMs: config.reconnectMaxMs || DEFAULT_RECONNECT_MAX_MS,
|
|
129
|
-
ca: config.ca || null,
|
|
130
|
-
rejectUnauthorized: config.rejectUnauthorized !== false,
|
|
131
|
-
servername: config.servername || null,
|
|
132
|
-
};
|
|
133
|
-
// safeUrl-style guard for the URL — reject userinfo (no auth in
|
|
134
|
-
// syslog wire) so a stray "syslog://user:pw@host" doesn't silently
|
|
135
|
-
// get through.
|
|
136
|
-
if (parsed.username || parsed.password) {
|
|
137
|
-
throw _err("BAD_URL",
|
|
138
|
-
"log-stream syslog: url must not contain userinfo");
|
|
139
|
-
}
|
|
140
|
-
// Track the operator's onDrop so dropped events surface.
|
|
141
|
-
var onDrop = typeof config.onDrop === "function" ? config.onDrop : null;
|
|
142
|
-
var _emitDrop = safeAsync.makeDropCallback(onDrop,
|
|
143
|
-
function (e) { log.warn("onDrop-callback-failed: " + e.message); });
|
|
144
|
-
|
|
145
|
-
// ---- UDP transport ----
|
|
146
|
-
if (transport === "udp") {
|
|
147
|
-
var udpFamily = host.indexOf(":") !== -1 ? "udp6" : "udp4";
|
|
148
|
-
var udpSock = dgram.createSocket(udpFamily);
|
|
149
|
-
udpSock.unref && udpSock.unref();
|
|
150
|
-
var udpClosed = false;
|
|
151
|
-
udpSock.on("error", function () { /* non-fatal — datagrams race */ });
|
|
152
|
-
|
|
153
|
-
return {
|
|
154
|
-
protocol: "syslog-udp",
|
|
155
|
-
emit: function (record) {
|
|
156
|
-
if (udpClosed) {
|
|
157
|
-
_emitDrop("sink-closed", [record], null);
|
|
158
|
-
return Promise.resolve({ accepted: false, reason: "closed" });
|
|
159
|
-
}
|
|
160
|
-
var msg = _formatRfc5424(record, cfg);
|
|
161
|
-
var buf = Buffer.from(msg, "utf8");
|
|
162
|
-
return new Promise(function (resolve) {
|
|
163
|
-
udpSock.send(buf, 0, buf.length, cfg.port, cfg.host, function (err) {
|
|
164
|
-
if (err) _emitDrop("udp-send-error", [record], err);
|
|
165
|
-
resolve({ accepted: !err, queued: 0 });
|
|
166
|
-
});
|
|
167
|
-
});
|
|
168
|
-
},
|
|
169
|
-
close: function () {
|
|
170
|
-
udpClosed = true;
|
|
171
|
-
try { udpSock.close(); }
|
|
172
|
-
catch (e) { log.warn("udp-close-failed: " + e.message); }
|
|
173
|
-
return Promise.resolve();
|
|
174
|
-
},
|
|
175
|
-
};
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// ---- TCP / TLS transport — octet-counting framing (RFC 6587) ----
|
|
179
|
-
// Buffer outgoing records during socket-down windows; replay on
|
|
180
|
-
// reconnect. Operator opts: bufferLimit caps the queue; oldest
|
|
181
|
-
// dropped first with the onDrop "overflow" reason.
|
|
182
|
-
var sock = null;
|
|
183
|
-
var sockReady = false;
|
|
184
|
-
var connecting = false;
|
|
185
|
-
var queue = [];
|
|
186
|
-
var closed = false;
|
|
187
|
-
var reconnectAttempt = 0;
|
|
188
|
-
|
|
189
|
-
function _writeFramed(record) {
|
|
190
|
-
var msg = _formatRfc5424(record, cfg);
|
|
191
|
-
var msgBuf = Buffer.from(msg, "utf8");
|
|
192
|
-
var prefix = Buffer.from(msgBuf.length + " ", "utf8");
|
|
193
|
-
sock.write(Buffer.concat([prefix, msgBuf]));
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
function _connect() {
|
|
197
|
-
if (closed || connecting) return;
|
|
198
|
-
connecting = true;
|
|
199
|
-
sockReady = false;
|
|
200
|
-
var connectOpts = { host: cfg.host, port: cfg.port };
|
|
201
|
-
var onConnect = function () {
|
|
202
|
-
connecting = false;
|
|
203
|
-
sockReady = true;
|
|
204
|
-
reconnectAttempt = 0;
|
|
205
|
-
// Drain queue in arrival order on (re)connect.
|
|
206
|
-
while (queue.length > 0 && sockReady) {
|
|
207
|
-
try { _writeFramed(queue.shift()); }
|
|
208
|
-
catch (e) { _emitDrop("write-error", [/* drained */], e); break; }
|
|
209
|
-
}
|
|
210
|
-
};
|
|
211
|
-
if (transport === "tls") {
|
|
212
|
-
var tlsOpts = Object.assign({}, connectOpts, {
|
|
213
|
-
rejectUnauthorized: cfg.rejectUnauthorized,
|
|
214
|
-
minVersion: "TLSv1.3",
|
|
215
|
-
});
|
|
216
|
-
if (cfg.ca) tlsOpts.ca = cfg.ca;
|
|
217
|
-
if (cfg.servername) tlsOpts.servername = cfg.servername;
|
|
218
|
-
sock = tls.connect(tlsOpts, onConnect);
|
|
219
|
-
} else {
|
|
220
|
-
sock = net.connect(connectOpts, onConnect);
|
|
221
|
-
}
|
|
222
|
-
sock.unref && sock.unref();
|
|
223
|
-
sock.on("error", function () { /* reconnect handled in the close listener */ });
|
|
224
|
-
sock.on("close", function () {
|
|
225
|
-
sockReady = false;
|
|
226
|
-
connecting = false;
|
|
227
|
-
try { sock.destroy(); }
|
|
228
|
-
catch (e) { log.warn("sock-destroy-failed: " + e.message); }
|
|
229
|
-
sock = null;
|
|
230
|
-
if (closed) return;
|
|
231
|
-
reconnectAttempt += 1;
|
|
232
|
-
var delay = Math.min(cfg.reconnectMaxMs,
|
|
233
|
-
cfg.reconnectBaseMs * Math.pow(2, reconnectAttempt - 1));
|
|
234
|
-
var t = setTimeout(_connect, delay);
|
|
235
|
-
t.unref && t.unref();
|
|
236
|
-
});
|
|
237
|
-
}
|
|
238
|
-
_connect();
|
|
239
|
-
|
|
240
|
-
return {
|
|
241
|
-
protocol: "syslog-" + transport,
|
|
242
|
-
emit: function (record) {
|
|
243
|
-
if (closed) {
|
|
244
|
-
_emitDrop("sink-closed", [record], null);
|
|
245
|
-
return Promise.resolve({ accepted: false, reason: "closed" });
|
|
246
|
-
}
|
|
247
|
-
if (sockReady) {
|
|
248
|
-
try { _writeFramed(record); }
|
|
249
|
-
catch (e) {
|
|
250
|
-
_emitDrop("write-error", [record], e);
|
|
251
|
-
return Promise.resolve({ accepted: false, reason: "write-error" });
|
|
252
|
-
}
|
|
253
|
-
return Promise.resolve({ accepted: true, queued: 0 });
|
|
254
|
-
}
|
|
255
|
-
// Socket not yet up — buffer with overflow-by-oldest semantics.
|
|
256
|
-
if (queue.length >= cfg.bufferLimit) {
|
|
257
|
-
var dropped = queue.shift();
|
|
258
|
-
_emitDrop("overflow", [dropped], null);
|
|
259
|
-
}
|
|
260
|
-
queue.push(record);
|
|
261
|
-
return Promise.resolve({ accepted: true, queued: queue.length });
|
|
262
|
-
},
|
|
263
|
-
close: function () {
|
|
264
|
-
// Give an in-flight (re)connect a brief window to complete and
|
|
265
|
-
// drain the buffer. Without this, records emitted just before
|
|
266
|
-
// shutdown race the slower TLS handshake and surface as
|
|
267
|
-
// "sink-closed" drops even though the framework had a viable
|
|
268
|
-
// connection in progress.
|
|
269
|
-
var DRAIN_TIMEOUT_MS = C.TIME.seconds(3);
|
|
270
|
-
var started = Date.now();
|
|
271
|
-
return new Promise(function (resolve) {
|
|
272
|
-
function _finish() {
|
|
273
|
-
closed = true;
|
|
274
|
-
var pending = queue.splice(0, queue.length);
|
|
275
|
-
if (pending.length > 0) _emitDrop("sink-closed", pending, null);
|
|
276
|
-
try { if (sock) sock.end(); }
|
|
277
|
-
catch (e) { log.warn("sock-end-failed: " + e.message); }
|
|
278
|
-
try { if (sock) sock.destroy(); }
|
|
279
|
-
catch (e) { log.warn("sock-destroy-failed: " + e.message); }
|
|
280
|
-
sock = null;
|
|
281
|
-
resolve();
|
|
282
|
-
}
|
|
283
|
-
function _tick() {
|
|
284
|
-
if (queue.length === 0 || Date.now() - started > DRAIN_TIMEOUT_MS) {
|
|
285
|
-
return _finish();
|
|
286
|
-
}
|
|
287
|
-
// Keep the timer ref'd — close() is being awaited; if we
|
|
288
|
-
// unref the drain-tick timer the event loop can exit between
|
|
289
|
-
// ticks (UDP/TCP socket are also unref'd) and the close
|
|
290
|
-
// promise pends forever silently.
|
|
291
|
-
setTimeout(_tick, 25);
|
|
292
|
-
}
|
|
293
|
-
_tick();
|
|
294
|
-
});
|
|
295
|
-
},
|
|
296
|
-
// Test-only: returns the in-flight queue size for assertions.
|
|
297
|
-
_queueSizeForTest: function () { return queue.length; },
|
|
298
|
-
_formatRfc5424ForTest: function (rec) { return _formatRfc5424(rec, cfg); },
|
|
299
|
-
};
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
module.exports = { create: create };
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Syslog log-stream sink — RFC 5424 framing over UDP / TCP / TLS.
|
|
4
|
+
*
|
|
5
|
+
* Wire format (RFC 5424 §6):
|
|
6
|
+
* <PRI>VERSION SP TIMESTAMP SP HOSTNAME SP APP-NAME SP PROCID SP MSGID SP STRUCTURED-DATA SP MSG
|
|
7
|
+
*
|
|
8
|
+
* PRI = (facility * 8) + severity
|
|
9
|
+
* facility (default 16 = local0; 1 = user — operators commonly pick
|
|
10
|
+
* local0..local7 for app-emitted records)
|
|
11
|
+
* severity is mapped from the framework's level field:
|
|
12
|
+
* debug → 7, info → 6, warn → 4, error → 3
|
|
13
|
+
*
|
|
14
|
+
* Transport:
|
|
15
|
+
* udp — single datagram per record (no framing)
|
|
16
|
+
* tcp — octet-counting framing (RFC 6587 §3.4.1):
|
|
17
|
+
* "<length> <message>" with a SPACE between
|
|
18
|
+
* length and the rfc5424 message bytes
|
|
19
|
+
* tls — same octet-counting framing on a TLS socket
|
|
20
|
+
* (RFC 5425). Standard port 6514.
|
|
21
|
+
*
|
|
22
|
+
* Defaults match RFC 3164/5424 conventions: appName = "blamejs",
|
|
23
|
+
* facility = local0 (16), hostname = os.hostname(), structuredData = "-".
|
|
24
|
+
*
|
|
25
|
+
* Flow control:
|
|
26
|
+
* The TCP / TLS variants buffer pending writes during socket
|
|
27
|
+
* reconnect and replay them on the new connection. UDP is best-effort
|
|
28
|
+
* (datagrams that race a closed socket are dropped to onDrop).
|
|
29
|
+
*/
|
|
30
|
+
var dgram = require("dgram");
|
|
31
|
+
var net = require("net");
|
|
32
|
+
var os = require("os");
|
|
33
|
+
var tls = require("tls");
|
|
34
|
+
var C = require("./constants");
|
|
35
|
+
var { boot } = require("./log");
|
|
36
|
+
var safeAsync = require("./safe-async");
|
|
37
|
+
var safeUrl = require("./safe-url");
|
|
38
|
+
var { LogStreamError } = require("./framework-error");
|
|
39
|
+
|
|
40
|
+
var _err = LogStreamError.factory;
|
|
41
|
+
var log = boot("log-stream-syslog");
|
|
42
|
+
|
|
43
|
+
// RFC 5424 facility codes (0-23). 0x10 = 16 = local0 — the
|
|
44
|
+
// conventional facility for app-emitted records.
|
|
45
|
+
var DEFAULT_FACILITY = 0x10;
|
|
46
|
+
var DEFAULT_APP_NAME = "blamejs";
|
|
47
|
+
var DEFAULT_PROC_ID = String(process.pid);
|
|
48
|
+
var DEFAULT_MSG_ID = "-";
|
|
49
|
+
var DEFAULT_STRUCT_DATA = "-";
|
|
50
|
+
var TCP_DEFAULT_PORT = 514;
|
|
51
|
+
var TLS_DEFAULT_PORT = 6514;
|
|
52
|
+
var UDP_DEFAULT_PORT = 514;
|
|
53
|
+
var DEFAULT_TIMEOUT_MS = C.TIME.seconds(10);
|
|
54
|
+
var DEFAULT_RECONNECT_BASE_MS = 250;
|
|
55
|
+
var DEFAULT_RECONNECT_MAX_MS = C.TIME.seconds(30);
|
|
56
|
+
var DEFAULT_BUFFER_LIMIT = C.BYTES.bytes(10000);
|
|
57
|
+
|
|
58
|
+
// RFC 5424 severity codes from the framework's level names.
|
|
59
|
+
var LEVEL_TO_SEVERITY = {
|
|
60
|
+
debug: 7,
|
|
61
|
+
info: 6,
|
|
62
|
+
warn: 4,
|
|
63
|
+
error: 3,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
function _toRfc3339(tsMs) {
|
|
67
|
+
return new Date(tsMs).toISOString();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function _formatRfc5424(record, cfg) {
|
|
71
|
+
var severity = LEVEL_TO_SEVERITY[record.level] != null
|
|
72
|
+
? LEVEL_TO_SEVERITY[record.level] : 6;
|
|
73
|
+
// RFC 5424 §6.2.1: PRI = facility << 3 | severity (severity is 3 bits).
|
|
74
|
+
var pri = (cfg.facility << 3) + severity;
|
|
75
|
+
var ts = _toRfc3339(record.ts || Date.now());
|
|
76
|
+
// Body — JSON-encode meta + message together so the structured
|
|
77
|
+
// payload survives the wire as a single MSG token. Operators with
|
|
78
|
+
// their own RFC 5424 STRUCTURED-DATA producers pass cfg.structuredData
|
|
79
|
+
// (string) to override the default "-".
|
|
80
|
+
var body = record.message || "";
|
|
81
|
+
if (record.meta && Object.keys(record.meta).length > 0) {
|
|
82
|
+
try { body += " " + JSON.stringify(record.meta); }
|
|
83
|
+
catch (_e) { /* best-effort */ }
|
|
84
|
+
}
|
|
85
|
+
return "<" + pri + ">1 " + ts + " " + cfg.hostname + " " +
|
|
86
|
+
cfg.appName + " " + cfg.procId + " " + cfg.msgId + " " +
|
|
87
|
+
(cfg.structuredData || "-") + " " + body;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function create(config) {
|
|
91
|
+
if (!config) throw _err("BAD_OPT", "log-stream syslog requires { url } (e.g. udp://host:514, tcp://host:514, tls://host:6514)");
|
|
92
|
+
var url = config.url;
|
|
93
|
+
if (typeof url !== "string" || url.length === 0) {
|
|
94
|
+
throw _err("BAD_OPT", "log-stream syslog requires { url } (string)");
|
|
95
|
+
}
|
|
96
|
+
// Parse URL — accept udp://, tcp://, tls:// only.
|
|
97
|
+
var parsed;
|
|
98
|
+
try {
|
|
99
|
+
parsed = safeUrl.parse(url, {
|
|
100
|
+
allowedProtocols: ["udp:", "tcp:", "tls:"],
|
|
101
|
+
errorClass: LogStreamError,
|
|
102
|
+
});
|
|
103
|
+
} catch (e) {
|
|
104
|
+
throw _err("BAD_URL", "log-stream syslog: bad url '" + url + "': " +
|
|
105
|
+
((e && e.message) || String(e)));
|
|
106
|
+
}
|
|
107
|
+
var transport = parsed.protocol.replace(/:$/, "").toLowerCase();
|
|
108
|
+
var defaultPort = transport === "tls" ? TLS_DEFAULT_PORT
|
|
109
|
+
: transport === "tcp" ? TCP_DEFAULT_PORT
|
|
110
|
+
: UDP_DEFAULT_PORT;
|
|
111
|
+
var host = parsed.hostname;
|
|
112
|
+
var port = parsed.port ? parseInt(parsed.port, 10) : defaultPort;
|
|
113
|
+
|
|
114
|
+
var cfg = {
|
|
115
|
+
transport: transport,
|
|
116
|
+
host: host,
|
|
117
|
+
port: port,
|
|
118
|
+
facility: (typeof config.facility === "number" && config.facility >= 0 && config.facility <= 23)
|
|
119
|
+
? Math.floor(config.facility) : DEFAULT_FACILITY,
|
|
120
|
+
appName: config.appName || DEFAULT_APP_NAME,
|
|
121
|
+
procId: config.procId || DEFAULT_PROC_ID,
|
|
122
|
+
msgId: config.msgId || DEFAULT_MSG_ID,
|
|
123
|
+
hostname: config.hostname || os.hostname(),
|
|
124
|
+
structuredData: config.structuredData || DEFAULT_STRUCT_DATA,
|
|
125
|
+
timeoutMs: config.timeoutMs || DEFAULT_TIMEOUT_MS,
|
|
126
|
+
bufferLimit: config.bufferLimit || DEFAULT_BUFFER_LIMIT,
|
|
127
|
+
reconnectBaseMs: config.reconnectBaseMs || DEFAULT_RECONNECT_BASE_MS,
|
|
128
|
+
reconnectMaxMs: config.reconnectMaxMs || DEFAULT_RECONNECT_MAX_MS,
|
|
129
|
+
ca: config.ca || null,
|
|
130
|
+
rejectUnauthorized: config.rejectUnauthorized !== false,
|
|
131
|
+
servername: config.servername || null,
|
|
132
|
+
};
|
|
133
|
+
// safeUrl-style guard for the URL — reject userinfo (no auth in
|
|
134
|
+
// syslog wire) so a stray "syslog://user:pw@host" doesn't silently
|
|
135
|
+
// get through.
|
|
136
|
+
if (parsed.username || parsed.password) {
|
|
137
|
+
throw _err("BAD_URL",
|
|
138
|
+
"log-stream syslog: url must not contain userinfo");
|
|
139
|
+
}
|
|
140
|
+
// Track the operator's onDrop so dropped events surface.
|
|
141
|
+
var onDrop = typeof config.onDrop === "function" ? config.onDrop : null;
|
|
142
|
+
var _emitDrop = safeAsync.makeDropCallback(onDrop,
|
|
143
|
+
function (e) { log.warn("onDrop-callback-failed: " + e.message); });
|
|
144
|
+
|
|
145
|
+
// ---- UDP transport ----
|
|
146
|
+
if (transport === "udp") {
|
|
147
|
+
var udpFamily = host.indexOf(":") !== -1 ? "udp6" : "udp4";
|
|
148
|
+
var udpSock = dgram.createSocket(udpFamily);
|
|
149
|
+
udpSock.unref && udpSock.unref();
|
|
150
|
+
var udpClosed = false;
|
|
151
|
+
udpSock.on("error", function () { /* non-fatal — datagrams race */ });
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
protocol: "syslog-udp",
|
|
155
|
+
emit: function (record) {
|
|
156
|
+
if (udpClosed) {
|
|
157
|
+
_emitDrop("sink-closed", [record], null);
|
|
158
|
+
return Promise.resolve({ accepted: false, reason: "closed" });
|
|
159
|
+
}
|
|
160
|
+
var msg = _formatRfc5424(record, cfg);
|
|
161
|
+
var buf = Buffer.from(msg, "utf8");
|
|
162
|
+
return new Promise(function (resolve) {
|
|
163
|
+
udpSock.send(buf, 0, buf.length, cfg.port, cfg.host, function (err) {
|
|
164
|
+
if (err) _emitDrop("udp-send-error", [record], err);
|
|
165
|
+
resolve({ accepted: !err, queued: 0 });
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
},
|
|
169
|
+
close: function () {
|
|
170
|
+
udpClosed = true;
|
|
171
|
+
try { udpSock.close(); }
|
|
172
|
+
catch (e) { log.warn("udp-close-failed: " + e.message); }
|
|
173
|
+
return Promise.resolve();
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ---- TCP / TLS transport — octet-counting framing (RFC 6587) ----
|
|
179
|
+
// Buffer outgoing records during socket-down windows; replay on
|
|
180
|
+
// reconnect. Operator opts: bufferLimit caps the queue; oldest
|
|
181
|
+
// dropped first with the onDrop "overflow" reason.
|
|
182
|
+
var sock = null;
|
|
183
|
+
var sockReady = false;
|
|
184
|
+
var connecting = false;
|
|
185
|
+
var queue = [];
|
|
186
|
+
var closed = false;
|
|
187
|
+
var reconnectAttempt = 0;
|
|
188
|
+
|
|
189
|
+
function _writeFramed(record) {
|
|
190
|
+
var msg = _formatRfc5424(record, cfg);
|
|
191
|
+
var msgBuf = Buffer.from(msg, "utf8");
|
|
192
|
+
var prefix = Buffer.from(msgBuf.length + " ", "utf8");
|
|
193
|
+
sock.write(Buffer.concat([prefix, msgBuf]));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function _connect() {
|
|
197
|
+
if (closed || connecting) return;
|
|
198
|
+
connecting = true;
|
|
199
|
+
sockReady = false;
|
|
200
|
+
var connectOpts = { host: cfg.host, port: cfg.port };
|
|
201
|
+
var onConnect = function () {
|
|
202
|
+
connecting = false;
|
|
203
|
+
sockReady = true;
|
|
204
|
+
reconnectAttempt = 0;
|
|
205
|
+
// Drain queue in arrival order on (re)connect.
|
|
206
|
+
while (queue.length > 0 && sockReady) {
|
|
207
|
+
try { _writeFramed(queue.shift()); }
|
|
208
|
+
catch (e) { _emitDrop("write-error", [/* drained */], e); break; }
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
if (transport === "tls") {
|
|
212
|
+
var tlsOpts = Object.assign({}, connectOpts, {
|
|
213
|
+
rejectUnauthorized: cfg.rejectUnauthorized,
|
|
214
|
+
minVersion: "TLSv1.3",
|
|
215
|
+
});
|
|
216
|
+
if (cfg.ca) tlsOpts.ca = cfg.ca;
|
|
217
|
+
if (cfg.servername) tlsOpts.servername = cfg.servername;
|
|
218
|
+
sock = tls.connect(tlsOpts, onConnect);
|
|
219
|
+
} else {
|
|
220
|
+
sock = net.connect(connectOpts, onConnect);
|
|
221
|
+
}
|
|
222
|
+
sock.unref && sock.unref();
|
|
223
|
+
sock.on("error", function () { /* reconnect handled in the close listener */ });
|
|
224
|
+
sock.on("close", function () {
|
|
225
|
+
sockReady = false;
|
|
226
|
+
connecting = false;
|
|
227
|
+
try { sock.destroy(); }
|
|
228
|
+
catch (e) { log.warn("sock-destroy-failed: " + e.message); }
|
|
229
|
+
sock = null;
|
|
230
|
+
if (closed) return;
|
|
231
|
+
reconnectAttempt += 1;
|
|
232
|
+
var delay = Math.min(cfg.reconnectMaxMs,
|
|
233
|
+
cfg.reconnectBaseMs * Math.pow(2, reconnectAttempt - 1));
|
|
234
|
+
var t = setTimeout(_connect, delay);
|
|
235
|
+
t.unref && t.unref();
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
_connect();
|
|
239
|
+
|
|
240
|
+
return {
|
|
241
|
+
protocol: "syslog-" + transport,
|
|
242
|
+
emit: function (record) {
|
|
243
|
+
if (closed) {
|
|
244
|
+
_emitDrop("sink-closed", [record], null);
|
|
245
|
+
return Promise.resolve({ accepted: false, reason: "closed" });
|
|
246
|
+
}
|
|
247
|
+
if (sockReady) {
|
|
248
|
+
try { _writeFramed(record); }
|
|
249
|
+
catch (e) {
|
|
250
|
+
_emitDrop("write-error", [record], e);
|
|
251
|
+
return Promise.resolve({ accepted: false, reason: "write-error" });
|
|
252
|
+
}
|
|
253
|
+
return Promise.resolve({ accepted: true, queued: 0 });
|
|
254
|
+
}
|
|
255
|
+
// Socket not yet up — buffer with overflow-by-oldest semantics.
|
|
256
|
+
if (queue.length >= cfg.bufferLimit) {
|
|
257
|
+
var dropped = queue.shift();
|
|
258
|
+
_emitDrop("overflow", [dropped], null);
|
|
259
|
+
}
|
|
260
|
+
queue.push(record);
|
|
261
|
+
return Promise.resolve({ accepted: true, queued: queue.length });
|
|
262
|
+
},
|
|
263
|
+
close: function () {
|
|
264
|
+
// Give an in-flight (re)connect a brief window to complete and
|
|
265
|
+
// drain the buffer. Without this, records emitted just before
|
|
266
|
+
// shutdown race the slower TLS handshake and surface as
|
|
267
|
+
// "sink-closed" drops even though the framework had a viable
|
|
268
|
+
// connection in progress.
|
|
269
|
+
var DRAIN_TIMEOUT_MS = C.TIME.seconds(3);
|
|
270
|
+
var started = Date.now();
|
|
271
|
+
return new Promise(function (resolve) {
|
|
272
|
+
function _finish() {
|
|
273
|
+
closed = true;
|
|
274
|
+
var pending = queue.splice(0, queue.length);
|
|
275
|
+
if (pending.length > 0) _emitDrop("sink-closed", pending, null);
|
|
276
|
+
try { if (sock) sock.end(); }
|
|
277
|
+
catch (e) { log.warn("sock-end-failed: " + e.message); }
|
|
278
|
+
try { if (sock) sock.destroy(); }
|
|
279
|
+
catch (e) { log.warn("sock-destroy-failed: " + e.message); }
|
|
280
|
+
sock = null;
|
|
281
|
+
resolve();
|
|
282
|
+
}
|
|
283
|
+
function _tick() {
|
|
284
|
+
if (queue.length === 0 || Date.now() - started > DRAIN_TIMEOUT_MS) {
|
|
285
|
+
return _finish();
|
|
286
|
+
}
|
|
287
|
+
// Keep the timer ref'd — close() is being awaited; if we
|
|
288
|
+
// unref the drain-tick timer the event loop can exit between
|
|
289
|
+
// ticks (UDP/TCP socket are also unref'd) and the close
|
|
290
|
+
// promise pends forever silently.
|
|
291
|
+
setTimeout(_tick, 25);
|
|
292
|
+
}
|
|
293
|
+
_tick();
|
|
294
|
+
});
|
|
295
|
+
},
|
|
296
|
+
// Test-only: returns the in-flight queue size for assertions.
|
|
297
|
+
_queueSizeForTest: function () { return queue.length; },
|
|
298
|
+
_formatRfc5424ForTest: function (rec) { return _formatRfc5424(rec, cfg); },
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
module.exports = { create: create };
|