@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
|
@@ -1,369 +1,369 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* AWS CloudWatch Logs sink — PutLogEvents over HTTPS with SigV4.
|
|
4
|
-
*
|
|
5
|
-
* Operator config:
|
|
6
|
-
*
|
|
7
|
-
* {
|
|
8
|
-
* region: "us-east-1"
|
|
9
|
-
* accessKeyId: env("AWS_ACCESS_KEY_ID")
|
|
10
|
-
* secretAccessKey: env("AWS_SECRET_ACCESS_KEY")
|
|
11
|
-
* sessionToken: env("AWS_SESSION_TOKEN") // optional, STS creds
|
|
12
|
-
* logGroupName: "my-app-logs" // operator pre-creates
|
|
13
|
-
* logStreamName: "instance-1" // operator pre-creates
|
|
14
|
-
* endpoint: "https://logs.us-east-1.amazonaws.com" // optional
|
|
15
|
-
* batchSize: 100 // CW caps at 10K events / 1 MiB per call
|
|
16
|
-
* maxBatchAgeMs: C.TIME.seconds(5)
|
|
17
|
-
* timeoutMs: C.TIME.seconds(30)
|
|
18
|
-
* retry: { maxAttempts, baseDelayMs, ... }
|
|
19
|
-
* bufferLimit: 10000
|
|
20
|
-
* onDrop: function ({ reason, batch, error }) { ... }
|
|
21
|
-
* }
|
|
22
|
-
*
|
|
23
|
-
* Wire format (Logs_20140328 PutLogEvents — JSON-1.1 over HTTPS):
|
|
24
|
-
*
|
|
25
|
-
* POST /
|
|
26
|
-
* X-Amz-Target: Logs_20140328.PutLogEvents
|
|
27
|
-
* Content-Type: application/x-amz-json-1.1
|
|
28
|
-
* Authorization: AWS4-HMAC-SHA256 Credential=... SignedHeaders=... Signature=...
|
|
29
|
-
* Body: { logGroupName, logStreamName, logEvents: [{ timestamp, message }, ...] }
|
|
30
|
-
*
|
|
31
|
-
* AWS quirks the framework handles:
|
|
32
|
-
* - Events MUST be sorted by timestamp ascending — sink sorts before send.
|
|
33
|
-
* - Per-batch caps: 10,000 events AND <= 1 MiB total payload. Operator
|
|
34
|
-
* batchSize is enforced; the framework also splits batches when the
|
|
35
|
-
* 1 MiB ceiling is reached mid-build.
|
|
36
|
-
* - Per-event 256 KiB hard cap. Oversized events are dropped at emit-time
|
|
37
|
-
* with onDrop fired.
|
|
38
|
-
* - sequenceToken is optional in modern CloudWatch (post-2023). If a
|
|
39
|
-
* legacy account requires it, CloudWatch returns
|
|
40
|
-
* InvalidSequenceTokenException with the expected token; the
|
|
41
|
-
* framework retries with that token transparently.
|
|
42
|
-
* - ResourceNotFoundException -> permanent error (operator forgot to
|
|
43
|
-
* create the log group or stream); surfaced via onDrop with a
|
|
44
|
-
* clear error.
|
|
45
|
-
*
|
|
46
|
-
* SigV4 signing reuses lib/object-store/sigv4.js with service: "logs".
|
|
47
|
-
*/
|
|
48
|
-
var C = require("./constants");
|
|
49
|
-
var nodeCrypto = require("node:crypto");
|
|
50
|
-
var safeAsync = require("./safe-async");
|
|
51
|
-
var safeJson = require("./safe-json");
|
|
52
|
-
var sigv4 = require("./object-store/sigv4");
|
|
53
|
-
var retryHelper = require("./retry");
|
|
54
|
-
var { LogStreamError } = require("./framework-error");
|
|
55
|
-
var httpClient = require("./http-client");
|
|
56
|
-
|
|
57
|
-
var MAX_RESPONSE_BYTES = C.BYTES.mib(1);
|
|
58
|
-
// AWS PutLogEvents hard limit: 10K events per batch (Logs_20140328 docs).
|
|
59
|
-
var CW_MAX_EVENTS_PER_BATCH = C.BYTES.bytes(10000);
|
|
60
|
-
var CW_MAX_BATCH_BYTES = C.BYTES.mib(1);
|
|
61
|
-
var CW_MAX_EVENT_BYTES = C.BYTES.kib(256);
|
|
62
|
-
var CW_EVENT_OVERHEAD_BYTES = C.BYTES.bytes(26);
|
|
63
|
-
// Truncated-message preview embedded in onDrop callbacks for oversize
|
|
64
|
-
// events — short enough to fit in a single log line so operators can
|
|
65
|
-
// spot the producer that emitted the over-cap record.
|
|
66
|
-
var DROP_PREVIEW_BYTES = C.BYTES.bytes(200);
|
|
67
|
-
|
|
68
|
-
var DEFAULTS = {
|
|
69
|
-
batchSize: 100,
|
|
70
|
-
maxBatchAgeMs: C.TIME.seconds(5),
|
|
71
|
-
timeoutMs: C.TIME.seconds(30),
|
|
72
|
-
// Ring-buffer cap (event count, not bytes); routed through C.BYTES
|
|
73
|
-
// identity passthrough so the file's literal arithmetic has a single
|
|
74
|
-
// source of truth.
|
|
75
|
-
bufferLimit: C.BYTES.bytes(10000),
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
var _err = LogStreamError.factory;
|
|
79
|
-
|
|
80
|
-
function _resolveEndpoint(cfg) {
|
|
81
|
-
if (cfg.endpoint) return cfg.endpoint.replace(/\/+$/, "") + "/";
|
|
82
|
-
return "https://logs." + cfg.region + ".amazonaws.com/";
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function _eventByteSize(message) {
|
|
86
|
-
return Buffer.byteLength(message, "utf8") + CW_EVENT_OVERHEAD_BYTES;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function _serializeBatch(events, cfg, sequenceToken) {
|
|
90
|
-
events.sort(function (a, b) { return a.timestamp - b.timestamp; });
|
|
91
|
-
var body = {
|
|
92
|
-
logGroupName: cfg.logGroupName,
|
|
93
|
-
logStreamName: cfg.logStreamName,
|
|
94
|
-
logEvents: events,
|
|
95
|
-
};
|
|
96
|
-
if (sequenceToken) body.sequenceToken = sequenceToken;
|
|
97
|
-
return Buffer.from(JSON.stringify(body), "utf8");
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function _signedHeaders(cfg, body, target) {
|
|
101
|
-
target = target || "Logs_20140328.PutLogEvents";
|
|
102
|
-
var url = _resolveEndpoint(cfg);
|
|
103
|
-
var payloadHash = nodeCrypto.createHash("sha256").update(body).digest("hex");
|
|
104
|
-
var unsigned = {
|
|
105
|
-
"Content-Type": "application/x-amz-json-1.1",
|
|
106
|
-
"X-Amz-Target": target,
|
|
107
|
-
};
|
|
108
|
-
var signed = sigv4.signRequest({
|
|
109
|
-
method: "POST",
|
|
110
|
-
url: url,
|
|
111
|
-
headers: unsigned,
|
|
112
|
-
payloadHash: payloadHash,
|
|
113
|
-
region: cfg.region,
|
|
114
|
-
service: "logs",
|
|
115
|
-
accessKeyId: cfg.accessKeyId,
|
|
116
|
-
secretAccessKey: cfg.secretAccessKey,
|
|
117
|
-
sessionToken: cfg.sessionToken || null,
|
|
118
|
-
allowedProtocols: cfg.allowedProtocols,
|
|
119
|
-
});
|
|
120
|
-
return signed.headers;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// CloudWatch CreateLogGroup / CreateLogStream wrappers. Operator opts in
|
|
124
|
-
// via cfg.autoCreate = true; default stays "operator pre-creates via
|
|
125
|
-
// Terraform / CDK / aws cli" so the framework doesn't paper over an
|
|
126
|
-
// IAM-misconfigured deployment with surprising side effects.
|
|
127
|
-
async function _ensureLogGroupAndStream(cfg) {
|
|
128
|
-
// CreateLogGroup — idempotent on the wire (we treat
|
|
129
|
-
// ResourceAlreadyExistsException as success).
|
|
130
|
-
var groupBody = Buffer.from(JSON.stringify({ logGroupName: cfg.logGroupName }), "utf8");
|
|
131
|
-
var groupHeaders = _signedHeaders(cfg, groupBody, "Logs_20140328.CreateLogGroup");
|
|
132
|
-
try {
|
|
133
|
-
await _post(cfg, groupBody, groupHeaders);
|
|
134
|
-
} catch (e) {
|
|
135
|
-
var msg = (e && e.message) || "";
|
|
136
|
-
if (!/ResourceAlreadyExistsException/.test(msg)) {
|
|
137
|
-
throw _err("AUTOCREATE_FAILED",
|
|
138
|
-
"log-stream cloudwatch autoCreate: CreateLogGroup failed: " + msg);
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
// CreateLogStream — same idempotency treatment.
|
|
142
|
-
var streamBody = Buffer.from(JSON.stringify({
|
|
143
|
-
logGroupName: cfg.logGroupName,
|
|
144
|
-
logStreamName: cfg.logStreamName,
|
|
145
|
-
}), "utf8");
|
|
146
|
-
var streamHeaders = _signedHeaders(cfg, streamBody, "Logs_20140328.CreateLogStream");
|
|
147
|
-
try {
|
|
148
|
-
await _post(cfg, streamBody, streamHeaders);
|
|
149
|
-
} catch (e) {
|
|
150
|
-
var msg2 = (e && e.message) || "";
|
|
151
|
-
if (!/ResourceAlreadyExistsException/.test(msg2)) {
|
|
152
|
-
throw _err("AUTOCREATE_FAILED",
|
|
153
|
-
"log-stream cloudwatch autoCreate: CreateLogStream failed: " + msg2);
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
function _post(cfg, body, headers) {
|
|
159
|
-
return httpClient.request({
|
|
160
|
-
method: "POST",
|
|
161
|
-
url: _resolveEndpoint(cfg),
|
|
162
|
-
headers: headers,
|
|
163
|
-
body: body,
|
|
164
|
-
idleTimeoutMs: cfg.timeoutMs,
|
|
165
|
-
maxResponseBytes: MAX_RESPONSE_BYTES,
|
|
166
|
-
errorClass: LogStreamError,
|
|
167
|
-
allowedProtocols: cfg.allowedProtocols,
|
|
168
|
-
allowInternal: cfg.allowInternal,
|
|
169
|
-
});
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
function _isPermanentAwsError(err) {
|
|
173
|
-
if (!err) return false;
|
|
174
|
-
var msg = err.message || "";
|
|
175
|
-
if (/ResourceNotFoundException/.test(msg)) return true;
|
|
176
|
-
if (/InvalidParameterException/.test(msg)) return true;
|
|
177
|
-
if (/UnrecognizedClientException/.test(msg)) return true;
|
|
178
|
-
if (/AccessDeniedException/.test(msg)) return true;
|
|
179
|
-
if (/SerializationException/.test(msg)) return true;
|
|
180
|
-
return false;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
function create(config) {
|
|
184
|
-
if (!config || !config.region) {
|
|
185
|
-
throw _err("BAD_OPT", "log-stream cloudwatch requires { region }");
|
|
186
|
-
}
|
|
187
|
-
if (!config.accessKeyId || !config.secretAccessKey) {
|
|
188
|
-
throw _err("BAD_OPT",
|
|
189
|
-
"log-stream cloudwatch requires { accessKeyId, secretAccessKey } " +
|
|
190
|
-
"(IAM role or env-supplied STS credentials)");
|
|
191
|
-
}
|
|
192
|
-
if (!config.logGroupName || !config.logStreamName) {
|
|
193
|
-
throw _err("BAD_OPT",
|
|
194
|
-
"log-stream cloudwatch requires { logGroupName, logStreamName }. " +
|
|
195
|
-
"Operator pre-creates both via aws / CDK / Terraform by default; " +
|
|
196
|
-
"pass { autoCreate: true } to have the framework issue " +
|
|
197
|
-
"CreateLogGroup + CreateLogStream on first emit (idempotent — " +
|
|
198
|
-
"ResourceAlreadyExistsException treated as success).");
|
|
199
|
-
}
|
|
200
|
-
var cfg = Object.assign({}, DEFAULTS, config);
|
|
201
|
-
var onDrop = typeof cfg.onDrop === "function" ? cfg.onDrop : null;
|
|
202
|
-
var _emitDrop = safeAsync.makeDropCallback(onDrop);
|
|
203
|
-
var buffer = [];
|
|
204
|
-
var dropCount = 0;
|
|
205
|
-
var inFlight = false;
|
|
206
|
-
var closed = false;
|
|
207
|
-
var sequenceToken = null;
|
|
208
|
-
var flushScheduler = safeAsync.makeScheduledFlush(cfg.maxBatchAgeMs, function () { return _flush(); });
|
|
209
|
-
|
|
210
|
-
function _takeBatch() {
|
|
211
|
-
var batch = [];
|
|
212
|
-
var totalBytes = 0;
|
|
213
|
-
while (buffer.length > 0) {
|
|
214
|
-
var nextEvent = buffer[0];
|
|
215
|
-
var size = _eventByteSize(nextEvent.message);
|
|
216
|
-
if (batch.length > 0 &&
|
|
217
|
-
(batch.length >= cfg.batchSize ||
|
|
218
|
-
batch.length >= CW_MAX_EVENTS_PER_BATCH ||
|
|
219
|
-
totalBytes + size > CW_MAX_BATCH_BYTES)) {
|
|
220
|
-
break;
|
|
221
|
-
}
|
|
222
|
-
batch.push(buffer.shift());
|
|
223
|
-
totalBytes += size;
|
|
224
|
-
}
|
|
225
|
-
return batch;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
// autoCreate handshake — runs once per process before the first
|
|
229
|
-
// PutLogEvents. Promise cached so concurrent emits don't fire
|
|
230
|
-
// duplicate CreateLogGroup / CreateLogStream calls.
|
|
231
|
-
var autoCreatePromise = null;
|
|
232
|
-
function _ensureAutoCreated() {
|
|
233
|
-
if (!cfg.autoCreate) return Promise.resolve();
|
|
234
|
-
if (!autoCreatePromise) autoCreatePromise = _ensureLogGroupAndStream(cfg);
|
|
235
|
-
return autoCreatePromise;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
async function _flush() {
|
|
239
|
-
if (inFlight) return;
|
|
240
|
-
if (buffer.length === 0) return;
|
|
241
|
-
inFlight = true;
|
|
242
|
-
try {
|
|
243
|
-
try { await _ensureAutoCreated(); }
|
|
244
|
-
catch (acErr) {
|
|
245
|
-
// autoCreate failure is permanent — every subsequent batch
|
|
246
|
-
// would hit the same error. Drop the queue with the
|
|
247
|
-
// operator-supplied onDrop callback so they see exactly which
|
|
248
|
-
// events were lost AND why, then bail.
|
|
249
|
-
var allBuffered = buffer.splice(0, buffer.length);
|
|
250
|
-
dropCount += allBuffered.length;
|
|
251
|
-
_emitDrop("autocreate-failed", allBuffered, acErr);
|
|
252
|
-
return;
|
|
253
|
-
}
|
|
254
|
-
while (buffer.length > 0 && !closed) {
|
|
255
|
-
var batch = _takeBatch();
|
|
256
|
-
if (batch.length === 0) break;
|
|
257
|
-
try {
|
|
258
|
-
await retryHelper.withRetry(function () {
|
|
259
|
-
return _send(batch);
|
|
260
|
-
}, Object.assign({
|
|
261
|
-
isPermanent: _isPermanentAwsError,
|
|
262
|
-
}, cfg.retry || {}));
|
|
263
|
-
} catch (e) {
|
|
264
|
-
dropCount += batch.length;
|
|
265
|
-
_emitDrop("retry-exhausted", batch, e);
|
|
266
|
-
break;
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
} finally {
|
|
270
|
-
inFlight = false;
|
|
271
|
-
if (buffer.length > 0) flushScheduler.schedule();
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
async function _send(batch) {
|
|
276
|
-
var body = _serializeBatch(batch, cfg, sequenceToken);
|
|
277
|
-
var headers = _signedHeaders(cfg, body);
|
|
278
|
-
var res;
|
|
279
|
-
try {
|
|
280
|
-
res = await _post(cfg, body, headers);
|
|
281
|
-
} catch (e) {
|
|
282
|
-
var match = /expected sequenceToken is:\s*(\S+)/.exec(e.message || "");
|
|
283
|
-
if (match) {
|
|
284
|
-
sequenceToken = match[1];
|
|
285
|
-
var retryBody = _serializeBatch(batch, cfg, sequenceToken);
|
|
286
|
-
var retryHeaders = _signedHeaders(cfg, retryBody);
|
|
287
|
-
res = await _post(cfg, retryBody, retryHeaders);
|
|
288
|
-
} else {
|
|
289
|
-
throw e;
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
if (res && res.body) {
|
|
293
|
-
try {
|
|
294
|
-
var parsed = safeJson.parse(res.body.toString("utf8"), { maxBytes: MAX_RESPONSE_BYTES });
|
|
295
|
-
if (parsed && parsed.nextSequenceToken) sequenceToken = parsed.nextSequenceToken;
|
|
296
|
-
} catch (_e) { /* response body not JSON; modern CW returns empty */ }
|
|
297
|
-
}
|
|
298
|
-
return res;
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
function emit(record) {
|
|
302
|
-
if (closed) return Promise.resolve({ accepted: false, reason: "sink closed" });
|
|
303
|
-
var message;
|
|
304
|
-
if (typeof record.message === "string") {
|
|
305
|
-
message = record.message;
|
|
306
|
-
} else {
|
|
307
|
-
message = JSON.stringify(record);
|
|
308
|
-
}
|
|
309
|
-
var size = _eventByteSize(message);
|
|
310
|
-
if (size > CW_MAX_EVENT_BYTES) {
|
|
311
|
-
_emitDrop("event-too-large", [{
|
|
312
|
-
timestamp: record.ts || Date.now(),
|
|
313
|
-
message: message.slice(0, DROP_PREVIEW_BYTES) + "...[truncated for drop event]",
|
|
314
|
-
}], new Error("event exceeds 256 KiB CloudWatch hard cap (was " + size + " bytes)"));
|
|
315
|
-
dropCount += 1;
|
|
316
|
-
return Promise.resolve({ accepted: false, reason: "event too large" });
|
|
317
|
-
}
|
|
318
|
-
if (buffer.length >= cfg.bufferLimit) {
|
|
319
|
-
var dropped = buffer.shift();
|
|
320
|
-
dropCount += 1;
|
|
321
|
-
_emitDrop("overflow", [dropped], null);
|
|
322
|
-
}
|
|
323
|
-
buffer.push({
|
|
324
|
-
timestamp: record.ts || Date.now(),
|
|
325
|
-
message: message,
|
|
326
|
-
});
|
|
327
|
-
if (buffer.length >= cfg.batchSize) {
|
|
328
|
-
_flush().catch(function () {});
|
|
329
|
-
} else {
|
|
330
|
-
flushScheduler.schedule();
|
|
331
|
-
}
|
|
332
|
-
return Promise.resolve({ accepted: true, queued: buffer.length });
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
async function close() {
|
|
336
|
-
closed = true;
|
|
337
|
-
flushScheduler.cancel();
|
|
338
|
-
await _flush();
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
function stats() {
|
|
342
|
-
return {
|
|
343
|
-
queued: buffer.length,
|
|
344
|
-
dropped: dropCount,
|
|
345
|
-
inFlight: inFlight,
|
|
346
|
-
sequenceToken: sequenceToken,
|
|
347
|
-
endpoint: _resolveEndpoint(cfg),
|
|
348
|
-
};
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
return {
|
|
352
|
-
protocol: "cloudwatch",
|
|
353
|
-
emit: emit,
|
|
354
|
-
close: close,
|
|
355
|
-
stats: stats,
|
|
356
|
-
flush: _flush,
|
|
357
|
-
};
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
module.exports = {
|
|
361
|
-
create: create,
|
|
362
|
-
_resolveEndpoint: _resolveEndpoint,
|
|
363
|
-
_eventByteSize: _eventByteSize,
|
|
364
|
-
_serializeBatch: _serializeBatch,
|
|
365
|
-
_isPermanentAwsError: _isPermanentAwsError,
|
|
366
|
-
CW_MAX_EVENTS_PER_BATCH: CW_MAX_EVENTS_PER_BATCH,
|
|
367
|
-
CW_MAX_BATCH_BYTES: CW_MAX_BATCH_BYTES,
|
|
368
|
-
CW_MAX_EVENT_BYTES: CW_MAX_EVENT_BYTES,
|
|
369
|
-
};
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* AWS CloudWatch Logs sink — PutLogEvents over HTTPS with SigV4.
|
|
4
|
+
*
|
|
5
|
+
* Operator config:
|
|
6
|
+
*
|
|
7
|
+
* {
|
|
8
|
+
* region: "us-east-1"
|
|
9
|
+
* accessKeyId: env("AWS_ACCESS_KEY_ID")
|
|
10
|
+
* secretAccessKey: env("AWS_SECRET_ACCESS_KEY")
|
|
11
|
+
* sessionToken: env("AWS_SESSION_TOKEN") // optional, STS creds
|
|
12
|
+
* logGroupName: "my-app-logs" // operator pre-creates
|
|
13
|
+
* logStreamName: "instance-1" // operator pre-creates
|
|
14
|
+
* endpoint: "https://logs.us-east-1.amazonaws.com" // optional
|
|
15
|
+
* batchSize: 100 // CW caps at 10K events / 1 MiB per call
|
|
16
|
+
* maxBatchAgeMs: C.TIME.seconds(5)
|
|
17
|
+
* timeoutMs: C.TIME.seconds(30)
|
|
18
|
+
* retry: { maxAttempts, baseDelayMs, ... }
|
|
19
|
+
* bufferLimit: 10000
|
|
20
|
+
* onDrop: function ({ reason, batch, error }) { ... }
|
|
21
|
+
* }
|
|
22
|
+
*
|
|
23
|
+
* Wire format (Logs_20140328 PutLogEvents — JSON-1.1 over HTTPS):
|
|
24
|
+
*
|
|
25
|
+
* POST /
|
|
26
|
+
* X-Amz-Target: Logs_20140328.PutLogEvents
|
|
27
|
+
* Content-Type: application/x-amz-json-1.1
|
|
28
|
+
* Authorization: AWS4-HMAC-SHA256 Credential=... SignedHeaders=... Signature=...
|
|
29
|
+
* Body: { logGroupName, logStreamName, logEvents: [{ timestamp, message }, ...] }
|
|
30
|
+
*
|
|
31
|
+
* AWS quirks the framework handles:
|
|
32
|
+
* - Events MUST be sorted by timestamp ascending — sink sorts before send.
|
|
33
|
+
* - Per-batch caps: 10,000 events AND <= 1 MiB total payload. Operator
|
|
34
|
+
* batchSize is enforced; the framework also splits batches when the
|
|
35
|
+
* 1 MiB ceiling is reached mid-build.
|
|
36
|
+
* - Per-event 256 KiB hard cap. Oversized events are dropped at emit-time
|
|
37
|
+
* with onDrop fired.
|
|
38
|
+
* - sequenceToken is optional in modern CloudWatch (post-2023). If a
|
|
39
|
+
* legacy account requires it, CloudWatch returns
|
|
40
|
+
* InvalidSequenceTokenException with the expected token; the
|
|
41
|
+
* framework retries with that token transparently.
|
|
42
|
+
* - ResourceNotFoundException -> permanent error (operator forgot to
|
|
43
|
+
* create the log group or stream); surfaced via onDrop with a
|
|
44
|
+
* clear error.
|
|
45
|
+
*
|
|
46
|
+
* SigV4 signing reuses lib/object-store/sigv4.js with service: "logs".
|
|
47
|
+
*/
|
|
48
|
+
var C = require("./constants");
|
|
49
|
+
var nodeCrypto = require("node:crypto");
|
|
50
|
+
var safeAsync = require("./safe-async");
|
|
51
|
+
var safeJson = require("./safe-json");
|
|
52
|
+
var sigv4 = require("./object-store/sigv4");
|
|
53
|
+
var retryHelper = require("./retry");
|
|
54
|
+
var { LogStreamError } = require("./framework-error");
|
|
55
|
+
var httpClient = require("./http-client");
|
|
56
|
+
|
|
57
|
+
var MAX_RESPONSE_BYTES = C.BYTES.mib(1);
|
|
58
|
+
// AWS PutLogEvents hard limit: 10K events per batch (Logs_20140328 docs).
|
|
59
|
+
var CW_MAX_EVENTS_PER_BATCH = C.BYTES.bytes(10000);
|
|
60
|
+
var CW_MAX_BATCH_BYTES = C.BYTES.mib(1);
|
|
61
|
+
var CW_MAX_EVENT_BYTES = C.BYTES.kib(256);
|
|
62
|
+
var CW_EVENT_OVERHEAD_BYTES = C.BYTES.bytes(26);
|
|
63
|
+
// Truncated-message preview embedded in onDrop callbacks for oversize
|
|
64
|
+
// events — short enough to fit in a single log line so operators can
|
|
65
|
+
// spot the producer that emitted the over-cap record.
|
|
66
|
+
var DROP_PREVIEW_BYTES = C.BYTES.bytes(200);
|
|
67
|
+
|
|
68
|
+
var DEFAULTS = {
|
|
69
|
+
batchSize: 100,
|
|
70
|
+
maxBatchAgeMs: C.TIME.seconds(5),
|
|
71
|
+
timeoutMs: C.TIME.seconds(30),
|
|
72
|
+
// Ring-buffer cap (event count, not bytes); routed through C.BYTES
|
|
73
|
+
// identity passthrough so the file's literal arithmetic has a single
|
|
74
|
+
// source of truth.
|
|
75
|
+
bufferLimit: C.BYTES.bytes(10000),
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
var _err = LogStreamError.factory;
|
|
79
|
+
|
|
80
|
+
function _resolveEndpoint(cfg) {
|
|
81
|
+
if (cfg.endpoint) return cfg.endpoint.replace(/\/+$/, "") + "/";
|
|
82
|
+
return "https://logs." + cfg.region + ".amazonaws.com/";
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function _eventByteSize(message) {
|
|
86
|
+
return Buffer.byteLength(message, "utf8") + CW_EVENT_OVERHEAD_BYTES;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function _serializeBatch(events, cfg, sequenceToken) {
|
|
90
|
+
events.sort(function (a, b) { return a.timestamp - b.timestamp; });
|
|
91
|
+
var body = {
|
|
92
|
+
logGroupName: cfg.logGroupName,
|
|
93
|
+
logStreamName: cfg.logStreamName,
|
|
94
|
+
logEvents: events,
|
|
95
|
+
};
|
|
96
|
+
if (sequenceToken) body.sequenceToken = sequenceToken;
|
|
97
|
+
return Buffer.from(JSON.stringify(body), "utf8");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function _signedHeaders(cfg, body, target) {
|
|
101
|
+
target = target || "Logs_20140328.PutLogEvents";
|
|
102
|
+
var url = _resolveEndpoint(cfg);
|
|
103
|
+
var payloadHash = nodeCrypto.createHash("sha256").update(body).digest("hex");
|
|
104
|
+
var unsigned = {
|
|
105
|
+
"Content-Type": "application/x-amz-json-1.1",
|
|
106
|
+
"X-Amz-Target": target,
|
|
107
|
+
};
|
|
108
|
+
var signed = sigv4.signRequest({
|
|
109
|
+
method: "POST",
|
|
110
|
+
url: url,
|
|
111
|
+
headers: unsigned,
|
|
112
|
+
payloadHash: payloadHash,
|
|
113
|
+
region: cfg.region,
|
|
114
|
+
service: "logs",
|
|
115
|
+
accessKeyId: cfg.accessKeyId,
|
|
116
|
+
secretAccessKey: cfg.secretAccessKey,
|
|
117
|
+
sessionToken: cfg.sessionToken || null,
|
|
118
|
+
allowedProtocols: cfg.allowedProtocols,
|
|
119
|
+
});
|
|
120
|
+
return signed.headers;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// CloudWatch CreateLogGroup / CreateLogStream wrappers. Operator opts in
|
|
124
|
+
// via cfg.autoCreate = true; default stays "operator pre-creates via
|
|
125
|
+
// Terraform / CDK / aws cli" so the framework doesn't paper over an
|
|
126
|
+
// IAM-misconfigured deployment with surprising side effects.
|
|
127
|
+
async function _ensureLogGroupAndStream(cfg) {
|
|
128
|
+
// CreateLogGroup — idempotent on the wire (we treat
|
|
129
|
+
// ResourceAlreadyExistsException as success).
|
|
130
|
+
var groupBody = Buffer.from(JSON.stringify({ logGroupName: cfg.logGroupName }), "utf8");
|
|
131
|
+
var groupHeaders = _signedHeaders(cfg, groupBody, "Logs_20140328.CreateLogGroup");
|
|
132
|
+
try {
|
|
133
|
+
await _post(cfg, groupBody, groupHeaders);
|
|
134
|
+
} catch (e) {
|
|
135
|
+
var msg = (e && e.message) || "";
|
|
136
|
+
if (!/ResourceAlreadyExistsException/.test(msg)) {
|
|
137
|
+
throw _err("AUTOCREATE_FAILED",
|
|
138
|
+
"log-stream cloudwatch autoCreate: CreateLogGroup failed: " + msg);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
// CreateLogStream — same idempotency treatment.
|
|
142
|
+
var streamBody = Buffer.from(JSON.stringify({
|
|
143
|
+
logGroupName: cfg.logGroupName,
|
|
144
|
+
logStreamName: cfg.logStreamName,
|
|
145
|
+
}), "utf8");
|
|
146
|
+
var streamHeaders = _signedHeaders(cfg, streamBody, "Logs_20140328.CreateLogStream");
|
|
147
|
+
try {
|
|
148
|
+
await _post(cfg, streamBody, streamHeaders);
|
|
149
|
+
} catch (e) {
|
|
150
|
+
var msg2 = (e && e.message) || "";
|
|
151
|
+
if (!/ResourceAlreadyExistsException/.test(msg2)) {
|
|
152
|
+
throw _err("AUTOCREATE_FAILED",
|
|
153
|
+
"log-stream cloudwatch autoCreate: CreateLogStream failed: " + msg2);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function _post(cfg, body, headers) {
|
|
159
|
+
return httpClient.request({
|
|
160
|
+
method: "POST",
|
|
161
|
+
url: _resolveEndpoint(cfg),
|
|
162
|
+
headers: headers,
|
|
163
|
+
body: body,
|
|
164
|
+
idleTimeoutMs: cfg.timeoutMs,
|
|
165
|
+
maxResponseBytes: MAX_RESPONSE_BYTES,
|
|
166
|
+
errorClass: LogStreamError,
|
|
167
|
+
allowedProtocols: cfg.allowedProtocols,
|
|
168
|
+
allowInternal: cfg.allowInternal,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function _isPermanentAwsError(err) {
|
|
173
|
+
if (!err) return false;
|
|
174
|
+
var msg = err.message || "";
|
|
175
|
+
if (/ResourceNotFoundException/.test(msg)) return true;
|
|
176
|
+
if (/InvalidParameterException/.test(msg)) return true;
|
|
177
|
+
if (/UnrecognizedClientException/.test(msg)) return true;
|
|
178
|
+
if (/AccessDeniedException/.test(msg)) return true;
|
|
179
|
+
if (/SerializationException/.test(msg)) return true;
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function create(config) {
|
|
184
|
+
if (!config || !config.region) {
|
|
185
|
+
throw _err("BAD_OPT", "log-stream cloudwatch requires { region }");
|
|
186
|
+
}
|
|
187
|
+
if (!config.accessKeyId || !config.secretAccessKey) {
|
|
188
|
+
throw _err("BAD_OPT",
|
|
189
|
+
"log-stream cloudwatch requires { accessKeyId, secretAccessKey } " +
|
|
190
|
+
"(IAM role or env-supplied STS credentials)");
|
|
191
|
+
}
|
|
192
|
+
if (!config.logGroupName || !config.logStreamName) {
|
|
193
|
+
throw _err("BAD_OPT",
|
|
194
|
+
"log-stream cloudwatch requires { logGroupName, logStreamName }. " +
|
|
195
|
+
"Operator pre-creates both via aws / CDK / Terraform by default; " +
|
|
196
|
+
"pass { autoCreate: true } to have the framework issue " +
|
|
197
|
+
"CreateLogGroup + CreateLogStream on first emit (idempotent — " +
|
|
198
|
+
"ResourceAlreadyExistsException treated as success).");
|
|
199
|
+
}
|
|
200
|
+
var cfg = Object.assign({}, DEFAULTS, config);
|
|
201
|
+
var onDrop = typeof cfg.onDrop === "function" ? cfg.onDrop : null;
|
|
202
|
+
var _emitDrop = safeAsync.makeDropCallback(onDrop);
|
|
203
|
+
var buffer = [];
|
|
204
|
+
var dropCount = 0;
|
|
205
|
+
var inFlight = false;
|
|
206
|
+
var closed = false;
|
|
207
|
+
var sequenceToken = null;
|
|
208
|
+
var flushScheduler = safeAsync.makeScheduledFlush(cfg.maxBatchAgeMs, function () { return _flush(); });
|
|
209
|
+
|
|
210
|
+
function _takeBatch() {
|
|
211
|
+
var batch = [];
|
|
212
|
+
var totalBytes = 0;
|
|
213
|
+
while (buffer.length > 0) {
|
|
214
|
+
var nextEvent = buffer[0];
|
|
215
|
+
var size = _eventByteSize(nextEvent.message);
|
|
216
|
+
if (batch.length > 0 &&
|
|
217
|
+
(batch.length >= cfg.batchSize ||
|
|
218
|
+
batch.length >= CW_MAX_EVENTS_PER_BATCH ||
|
|
219
|
+
totalBytes + size > CW_MAX_BATCH_BYTES)) {
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
222
|
+
batch.push(buffer.shift());
|
|
223
|
+
totalBytes += size;
|
|
224
|
+
}
|
|
225
|
+
return batch;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// autoCreate handshake — runs once per process before the first
|
|
229
|
+
// PutLogEvents. Promise cached so concurrent emits don't fire
|
|
230
|
+
// duplicate CreateLogGroup / CreateLogStream calls.
|
|
231
|
+
var autoCreatePromise = null;
|
|
232
|
+
function _ensureAutoCreated() {
|
|
233
|
+
if (!cfg.autoCreate) return Promise.resolve();
|
|
234
|
+
if (!autoCreatePromise) autoCreatePromise = _ensureLogGroupAndStream(cfg);
|
|
235
|
+
return autoCreatePromise;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async function _flush() {
|
|
239
|
+
if (inFlight) return;
|
|
240
|
+
if (buffer.length === 0) return;
|
|
241
|
+
inFlight = true;
|
|
242
|
+
try {
|
|
243
|
+
try { await _ensureAutoCreated(); }
|
|
244
|
+
catch (acErr) {
|
|
245
|
+
// autoCreate failure is permanent — every subsequent batch
|
|
246
|
+
// would hit the same error. Drop the queue with the
|
|
247
|
+
// operator-supplied onDrop callback so they see exactly which
|
|
248
|
+
// events were lost AND why, then bail.
|
|
249
|
+
var allBuffered = buffer.splice(0, buffer.length);
|
|
250
|
+
dropCount += allBuffered.length;
|
|
251
|
+
_emitDrop("autocreate-failed", allBuffered, acErr);
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
while (buffer.length > 0 && !closed) {
|
|
255
|
+
var batch = _takeBatch();
|
|
256
|
+
if (batch.length === 0) break;
|
|
257
|
+
try {
|
|
258
|
+
await retryHelper.withRetry(function () {
|
|
259
|
+
return _send(batch);
|
|
260
|
+
}, Object.assign({
|
|
261
|
+
isPermanent: _isPermanentAwsError,
|
|
262
|
+
}, cfg.retry || {}));
|
|
263
|
+
} catch (e) {
|
|
264
|
+
dropCount += batch.length;
|
|
265
|
+
_emitDrop("retry-exhausted", batch, e);
|
|
266
|
+
break;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
} finally {
|
|
270
|
+
inFlight = false;
|
|
271
|
+
if (buffer.length > 0) flushScheduler.schedule();
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
async function _send(batch) {
|
|
276
|
+
var body = _serializeBatch(batch, cfg, sequenceToken);
|
|
277
|
+
var headers = _signedHeaders(cfg, body);
|
|
278
|
+
var res;
|
|
279
|
+
try {
|
|
280
|
+
res = await _post(cfg, body, headers);
|
|
281
|
+
} catch (e) {
|
|
282
|
+
var match = /expected sequenceToken is:\s*(\S+)/.exec(e.message || "");
|
|
283
|
+
if (match) {
|
|
284
|
+
sequenceToken = match[1];
|
|
285
|
+
var retryBody = _serializeBatch(batch, cfg, sequenceToken);
|
|
286
|
+
var retryHeaders = _signedHeaders(cfg, retryBody);
|
|
287
|
+
res = await _post(cfg, retryBody, retryHeaders);
|
|
288
|
+
} else {
|
|
289
|
+
throw e;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
if (res && res.body) {
|
|
293
|
+
try {
|
|
294
|
+
var parsed = safeJson.parse(res.body.toString("utf8"), { maxBytes: MAX_RESPONSE_BYTES });
|
|
295
|
+
if (parsed && parsed.nextSequenceToken) sequenceToken = parsed.nextSequenceToken;
|
|
296
|
+
} catch (_e) { /* response body not JSON; modern CW returns empty */ }
|
|
297
|
+
}
|
|
298
|
+
return res;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function emit(record) {
|
|
302
|
+
if (closed) return Promise.resolve({ accepted: false, reason: "sink closed" });
|
|
303
|
+
var message;
|
|
304
|
+
if (typeof record.message === "string") {
|
|
305
|
+
message = record.message;
|
|
306
|
+
} else {
|
|
307
|
+
message = JSON.stringify(record);
|
|
308
|
+
}
|
|
309
|
+
var size = _eventByteSize(message);
|
|
310
|
+
if (size > CW_MAX_EVENT_BYTES) {
|
|
311
|
+
_emitDrop("event-too-large", [{
|
|
312
|
+
timestamp: record.ts || Date.now(),
|
|
313
|
+
message: message.slice(0, DROP_PREVIEW_BYTES) + "...[truncated for drop event]",
|
|
314
|
+
}], new Error("event exceeds 256 KiB CloudWatch hard cap (was " + size + " bytes)"));
|
|
315
|
+
dropCount += 1;
|
|
316
|
+
return Promise.resolve({ accepted: false, reason: "event too large" });
|
|
317
|
+
}
|
|
318
|
+
if (buffer.length >= cfg.bufferLimit) {
|
|
319
|
+
var dropped = buffer.shift();
|
|
320
|
+
dropCount += 1;
|
|
321
|
+
_emitDrop("overflow", [dropped], null);
|
|
322
|
+
}
|
|
323
|
+
buffer.push({
|
|
324
|
+
timestamp: record.ts || Date.now(),
|
|
325
|
+
message: message,
|
|
326
|
+
});
|
|
327
|
+
if (buffer.length >= cfg.batchSize) {
|
|
328
|
+
_flush().catch(function () {});
|
|
329
|
+
} else {
|
|
330
|
+
flushScheduler.schedule();
|
|
331
|
+
}
|
|
332
|
+
return Promise.resolve({ accepted: true, queued: buffer.length });
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
async function close() {
|
|
336
|
+
closed = true;
|
|
337
|
+
flushScheduler.cancel();
|
|
338
|
+
await _flush();
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function stats() {
|
|
342
|
+
return {
|
|
343
|
+
queued: buffer.length,
|
|
344
|
+
dropped: dropCount,
|
|
345
|
+
inFlight: inFlight,
|
|
346
|
+
sequenceToken: sequenceToken,
|
|
347
|
+
endpoint: _resolveEndpoint(cfg),
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return {
|
|
352
|
+
protocol: "cloudwatch",
|
|
353
|
+
emit: emit,
|
|
354
|
+
close: close,
|
|
355
|
+
stats: stats,
|
|
356
|
+
flush: _flush,
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
module.exports = {
|
|
361
|
+
create: create,
|
|
362
|
+
_resolveEndpoint: _resolveEndpoint,
|
|
363
|
+
_eventByteSize: _eventByteSize,
|
|
364
|
+
_serializeBatch: _serializeBatch,
|
|
365
|
+
_isPermanentAwsError: _isPermanentAwsError,
|
|
366
|
+
CW_MAX_EVENTS_PER_BATCH: CW_MAX_EVENTS_PER_BATCH,
|
|
367
|
+
CW_MAX_BATCH_BYTES: CW_MAX_BATCH_BYTES,
|
|
368
|
+
CW_MAX_EVENT_BYTES: CW_MAX_EVENT_BYTES,
|
|
369
|
+
};
|