@loadstrike/loadstrike-sdk 1.0.30201 → 1.0.31001
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/README.md +28 -0
- package/dist/cjs/cluster.js +2417 -7
- package/dist/cjs/index.js +13 -2
- package/dist/cjs/iteration-observation-diagnostics.js +513 -0
- package/dist/cjs/iteration-observations.js +884 -0
- package/dist/cjs/load-engine-v2.js +966 -0
- package/dist/cjs/local.js +128 -30
- package/dist/cjs/reporting.js +148 -19
- package/dist/cjs/runtime.js +2514 -196
- package/dist/cjs/sink-retry-policy.js +52 -0
- package/dist/cjs/sinks.js +580 -23
- package/dist/cjs/transports.js +154 -163
- package/dist/esm/cluster.js +2386 -7
- package/dist/esm/index.js +1 -0
- package/dist/esm/iteration-observation-diagnostics.js +508 -0
- package/dist/esm/iteration-observations.js +871 -0
- package/dist/esm/load-engine-v2.js +942 -0
- package/dist/esm/local.js +128 -30
- package/dist/esm/reporting.js +148 -19
- package/dist/esm/runtime.js +2515 -197
- package/dist/esm/sink-retry-policy.js +44 -0
- package/dist/esm/sinks.js +580 -23
- package/dist/esm/transports.js +154 -163
- package/dist/types/cluster.d.ts +379 -1
- package/dist/types/index.d.ts +3 -1
- package/dist/types/iteration-observation-diagnostics.d.ts +21 -0
- package/dist/types/iteration-observations.d.ts +230 -0
- package/dist/types/load-engine-v2.d.ts +147 -0
- package/dist/types/runtime.d.ts +216 -8
- package/dist/types/sink-retry-policy.d.ts +9 -0
- package/dist/types/sinks.d.ts +73 -0
- package/dist/types/transports.d.ts +6 -8
- package/package.json +3 -4
package/dist/esm/sinks.js
CHANGED
|
@@ -1,9 +1,43 @@
|
|
|
1
1
|
import { LoadStrikePluginData as LoadStrikePluginDataModel, LoadStrikePluginDataTable as LoadStrikePluginDataTableModel } from "./runtime.js";
|
|
2
2
|
import { createHash, randomBytes } from "node:crypto";
|
|
3
|
-
import {
|
|
3
|
+
import { appendFile, mkdir } from "node:fs/promises";
|
|
4
4
|
import { dirname } from "node:path";
|
|
5
5
|
import { createSocket } from "node:dgram";
|
|
6
|
+
import { gzip } from "node:zlib";
|
|
6
7
|
import { Pool } from "pg";
|
|
8
|
+
import { serializeIterationObservationBatchGzipJson } from "./iteration-observations.js";
|
|
9
|
+
import { redactIterationObservationSecrets } from "./iteration-observation-diagnostics.js";
|
|
10
|
+
const PORTAL_RUN_TOKEN_PROVIDER = Symbol.for("loadstrike.internal.portal-run-token-provider");
|
|
11
|
+
const MAXIMUM_HTTP_RESPONSE_BODY_BYTES = 256 * 1024;
|
|
12
|
+
const MAXIMUM_HTTP_METADATA_CHARACTERS = 256;
|
|
13
|
+
const HTTP_METADATA_TRUNCATION_SUFFIX = " [truncated]";
|
|
14
|
+
class ReportingSinkHttpError extends Error {
|
|
15
|
+
constructor(sinkName, status, statusText, requestId) {
|
|
16
|
+
const normalizedStatus = Number.isFinite(status) ? Math.max(Math.trunc(status), 0) : 0;
|
|
17
|
+
const normalizedStatusText = normalizeHttpMetadata(statusText);
|
|
18
|
+
const normalizedRequestId = normalizeHttpMetadata(requestId);
|
|
19
|
+
super(`${sinkName} write failed with HTTP status ${normalizedStatus}`
|
|
20
|
+
+ (normalizedStatusText ? ` ${normalizedStatusText}` : "")
|
|
21
|
+
+ (normalizedRequestId ? ` (requestId=${normalizedRequestId})` : "")
|
|
22
|
+
+ ".");
|
|
23
|
+
this.name = "ReportingSinkHttpError";
|
|
24
|
+
this.status = normalizedStatus;
|
|
25
|
+
this.statusText = normalizedStatusText;
|
|
26
|
+
this.requestId = normalizedRequestId;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function gzipJsonAsync(value) {
|
|
30
|
+
const json = Buffer.from(JSON.stringify(value), "utf8");
|
|
31
|
+
return new Promise((resolve, reject) => {
|
|
32
|
+
gzip(json, (error, compressed) => {
|
|
33
|
+
if (error) {
|
|
34
|
+
reject(error);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
resolve(compressed);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
}
|
|
7
41
|
const DEFAULT_INFLUX_CONFIGURATION_SECTION_PATH = "LoadStrike:ReportingSinks:InfluxDb";
|
|
8
42
|
const DEFAULT_GRAFANA_LOKI_CONFIGURATION_SECTION_PATH = "LoadStrike:ReportingSinks:GrafanaLoki";
|
|
9
43
|
const DEFAULT_TIMESCALEDB_CONFIGURATION_SECTION_PATH = "LoadStrike:ReportingSinks:TimescaleDb";
|
|
@@ -364,6 +398,28 @@ export class CompositeReportingSink {
|
|
|
364
398
|
async SaveRunResult(result) {
|
|
365
399
|
await this.saveRunResult(result);
|
|
366
400
|
}
|
|
401
|
+
async saveIterationBatch(batch) {
|
|
402
|
+
for (const sink of this.sinks) {
|
|
403
|
+
const saveIterationBatch = sink.saveIterationBatch ?? sink.SaveIterationBatch;
|
|
404
|
+
if (saveIterationBatch) {
|
|
405
|
+
await saveIterationBatch.call(sink, batch);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
async SaveIterationBatch(batch) {
|
|
410
|
+
await this.saveIterationBatch(batch);
|
|
411
|
+
}
|
|
412
|
+
async completeIterationObservationStream(completion) {
|
|
413
|
+
for (const sink of this.sinks) {
|
|
414
|
+
const completeIterationObservationStream = sink.completeIterationObservationStream ?? sink.CompleteIterationObservationStream;
|
|
415
|
+
if (completeIterationObservationStream) {
|
|
416
|
+
await completeIterationObservationStream.call(sink, completion);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
async CompleteIterationObservationStream(completion) {
|
|
421
|
+
await this.completeIterationObservationStream(completion);
|
|
422
|
+
}
|
|
367
423
|
async stop() {
|
|
368
424
|
for (const sink of this.sinks) {
|
|
369
425
|
const stop = sink.stop ?? sink.Stop;
|
|
@@ -378,6 +434,7 @@ export class CompositeReportingSink {
|
|
|
378
434
|
}
|
|
379
435
|
export class PortalReportingSink {
|
|
380
436
|
constructor(options = {}) {
|
|
437
|
+
this.iterationObservationPortalSink = true;
|
|
381
438
|
this.sinkName = "portal";
|
|
382
439
|
this.SinkName = "portal";
|
|
383
440
|
this.licenseFeature = "extensions.reporting_sinks.portal";
|
|
@@ -385,6 +442,7 @@ export class PortalReportingSink {
|
|
|
385
442
|
this.baseContext = null;
|
|
386
443
|
this.session = null;
|
|
387
444
|
this.runToken = "";
|
|
445
|
+
this.runTokenProvider = null;
|
|
388
446
|
this.ingestUrl = "";
|
|
389
447
|
this.optionsInput = { ...options };
|
|
390
448
|
const source = asRecord(options);
|
|
@@ -401,7 +459,11 @@ export class PortalReportingSink {
|
|
|
401
459
|
this.init(context, infraConfig);
|
|
402
460
|
}
|
|
403
461
|
start(session) {
|
|
404
|
-
|
|
462
|
+
const internalProvider = session[PORTAL_RUN_TOKEN_PROVIDER];
|
|
463
|
+
this.runTokenProvider = typeof internalProvider === "function"
|
|
464
|
+
? internalProvider
|
|
465
|
+
: () => String(session.runToken ?? session.RunToken ?? "").trim();
|
|
466
|
+
this.runToken = this.runTokenProvider();
|
|
405
467
|
this.ingestUrl = String(session.portalReportingIngestUrl ?? session.PortalReportingIngestUrl ?? "").trim();
|
|
406
468
|
if (!this.runToken || !this.ingestUrl) {
|
|
407
469
|
throw new Error("PortalReportingSink requires a managed portal reporting session.");
|
|
@@ -431,8 +493,27 @@ export class PortalReportingSink {
|
|
|
431
493
|
async SaveRunResult(result) {
|
|
432
494
|
await this.saveRunResult(result);
|
|
433
495
|
}
|
|
496
|
+
async saveIterationBatch(batch) {
|
|
497
|
+
const compressed = await gzipJsonAsync(batch);
|
|
498
|
+
await this.persistObservationPayload({
|
|
499
|
+
compressedObservationBatches: [{
|
|
500
|
+
compression: "gzip-json",
|
|
501
|
+
payloadBase64: compressed.toString("base64")
|
|
502
|
+
}]
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
async SaveIterationBatch(batch) {
|
|
506
|
+
await this.saveIterationBatch(batch);
|
|
507
|
+
}
|
|
508
|
+
async completeIterationObservationStream(completion) {
|
|
509
|
+
await this.persistObservationPayload({ observationStreamCompletions: [completion] });
|
|
510
|
+
}
|
|
511
|
+
async CompleteIterationObservationStream(completion) {
|
|
512
|
+
await this.completeIterationObservationStream(completion);
|
|
513
|
+
}
|
|
434
514
|
stop() {
|
|
435
515
|
this.session = null;
|
|
516
|
+
this.runTokenProvider = null;
|
|
436
517
|
}
|
|
437
518
|
Stop() {
|
|
438
519
|
this.stop();
|
|
@@ -461,12 +542,27 @@ export class PortalReportingSink {
|
|
|
461
542
|
method: "POST",
|
|
462
543
|
headers: { "Content-Type": "application/json" },
|
|
463
544
|
body: JSON.stringify({
|
|
464
|
-
runToken: this.
|
|
465
|
-
events: events.map((event, index) => portalEventPayload(event, index))
|
|
545
|
+
runToken: this.currentRunToken(),
|
|
546
|
+
events: events.map((event, index) => portalEventPayload(removeSdkPercentileFields(event), index))
|
|
547
|
+
})
|
|
548
|
+
}, this.timeoutMs, "PortalReportingSink");
|
|
549
|
+
validatePortalIngestResponse(responseBody);
|
|
550
|
+
}
|
|
551
|
+
async persistObservationPayload(payload) {
|
|
552
|
+
const responseBody = await postWithTimeout(this.fetchImpl, this.ingestUrl, {
|
|
553
|
+
method: "POST",
|
|
554
|
+
headers: { "Content-Type": "application/json" },
|
|
555
|
+
body: JSON.stringify({
|
|
556
|
+
runToken: this.currentRunToken(),
|
|
557
|
+
...payload
|
|
466
558
|
})
|
|
467
559
|
}, this.timeoutMs, "PortalReportingSink");
|
|
468
560
|
validatePortalIngestResponse(responseBody);
|
|
469
561
|
}
|
|
562
|
+
currentRunToken() {
|
|
563
|
+
const current = this.runTokenProvider?.() ?? "";
|
|
564
|
+
return current || this.runToken;
|
|
565
|
+
}
|
|
470
566
|
}
|
|
471
567
|
export function cloneReportingSinkForRun(sink) {
|
|
472
568
|
if (sink instanceof PortalReportingSink) {
|
|
@@ -542,6 +638,18 @@ export class InfluxDbReportingSink {
|
|
|
542
638
|
async SaveRunResult(result) {
|
|
543
639
|
await this.saveRunResult(result);
|
|
544
640
|
}
|
|
641
|
+
async saveIterationBatch(batch) {
|
|
642
|
+
await this.persistEvents(createIterationObservationEvents(this.getSession(), batch));
|
|
643
|
+
}
|
|
644
|
+
async SaveIterationBatch(batch) {
|
|
645
|
+
await this.saveIterationBatch(batch);
|
|
646
|
+
}
|
|
647
|
+
async completeIterationObservationStream(completion) {
|
|
648
|
+
await this.persistEvents(createIterationCompletionEvents(this.getSession(), completion));
|
|
649
|
+
}
|
|
650
|
+
async CompleteIterationObservationStream(completion) {
|
|
651
|
+
await this.completeIterationObservationStream(completion);
|
|
652
|
+
}
|
|
545
653
|
stop() {
|
|
546
654
|
this.session = null;
|
|
547
655
|
}
|
|
@@ -651,6 +759,18 @@ export class GrafanaLokiReportingSink {
|
|
|
651
759
|
async SaveRunResult(result) {
|
|
652
760
|
await this.saveRunResult(result);
|
|
653
761
|
}
|
|
762
|
+
async saveIterationBatch(batch) {
|
|
763
|
+
await this.persistEvents(createIterationObservationEvents(this.getSession(), batch));
|
|
764
|
+
}
|
|
765
|
+
async SaveIterationBatch(batch) {
|
|
766
|
+
await this.saveIterationBatch(batch);
|
|
767
|
+
}
|
|
768
|
+
async completeIterationObservationStream(completion) {
|
|
769
|
+
await this.persistEvents(createIterationCompletionEvents(this.getSession(), completion));
|
|
770
|
+
}
|
|
771
|
+
async CompleteIterationObservationStream(completion) {
|
|
772
|
+
await this.completeIterationObservationStream(completion);
|
|
773
|
+
}
|
|
654
774
|
stop() {
|
|
655
775
|
this.session = null;
|
|
656
776
|
}
|
|
@@ -796,6 +916,18 @@ export class TimescaleDbReportingSink {
|
|
|
796
916
|
async SaveRunResult(result) {
|
|
797
917
|
await this.saveRunResult(result);
|
|
798
918
|
}
|
|
919
|
+
async saveIterationBatch(batch) {
|
|
920
|
+
await this.persistEvents(createIterationObservationEvents(this.getSession(), batch));
|
|
921
|
+
}
|
|
922
|
+
async SaveIterationBatch(batch) {
|
|
923
|
+
await this.saveIterationBatch(batch);
|
|
924
|
+
}
|
|
925
|
+
async completeIterationObservationStream(completion) {
|
|
926
|
+
await this.persistEvents(createIterationCompletionEvents(this.getSession(), completion));
|
|
927
|
+
}
|
|
928
|
+
async CompleteIterationObservationStream(completion) {
|
|
929
|
+
await this.completeIterationObservationStream(completion);
|
|
930
|
+
}
|
|
799
931
|
async stop() {
|
|
800
932
|
this.session = null;
|
|
801
933
|
if (this.pool) {
|
|
@@ -1071,6 +1203,18 @@ export class DatadogReportingSink {
|
|
|
1071
1203
|
async SaveRunResult(result) {
|
|
1072
1204
|
await this.saveRunResult(result);
|
|
1073
1205
|
}
|
|
1206
|
+
async saveIterationBatch(batch) {
|
|
1207
|
+
await this.persistEvents(createIterationObservationEvents(this.getSession(), batch));
|
|
1208
|
+
}
|
|
1209
|
+
async SaveIterationBatch(batch) {
|
|
1210
|
+
await this.saveIterationBatch(batch);
|
|
1211
|
+
}
|
|
1212
|
+
async completeIterationObservationStream(completion) {
|
|
1213
|
+
await this.persistEvents(createIterationCompletionEvents(this.getSession(), completion));
|
|
1214
|
+
}
|
|
1215
|
+
async CompleteIterationObservationStream(completion) {
|
|
1216
|
+
await this.completeIterationObservationStream(completion);
|
|
1217
|
+
}
|
|
1074
1218
|
stop() {
|
|
1075
1219
|
this.session = null;
|
|
1076
1220
|
}
|
|
@@ -1193,6 +1337,18 @@ export class SplunkReportingSink {
|
|
|
1193
1337
|
async SaveRunResult(result) {
|
|
1194
1338
|
await this.saveRunResult(result);
|
|
1195
1339
|
}
|
|
1340
|
+
async saveIterationBatch(batch) {
|
|
1341
|
+
await this.persistEvents(createIterationObservationEvents(this.getSession(), batch));
|
|
1342
|
+
}
|
|
1343
|
+
async SaveIterationBatch(batch) {
|
|
1344
|
+
await this.saveIterationBatch(batch);
|
|
1345
|
+
}
|
|
1346
|
+
async completeIterationObservationStream(completion) {
|
|
1347
|
+
await this.persistEvents(createIterationCompletionEvents(this.getSession(), completion));
|
|
1348
|
+
}
|
|
1349
|
+
async CompleteIterationObservationStream(completion) {
|
|
1350
|
+
await this.completeIterationObservationStream(completion);
|
|
1351
|
+
}
|
|
1196
1352
|
stop() {
|
|
1197
1353
|
this.session = null;
|
|
1198
1354
|
}
|
|
@@ -1295,6 +1451,18 @@ export class OtelCollectorReportingSink {
|
|
|
1295
1451
|
async SaveRunResult(result) {
|
|
1296
1452
|
await this.saveRunResult(result);
|
|
1297
1453
|
}
|
|
1454
|
+
async saveIterationBatch(batch) {
|
|
1455
|
+
await this.persistEvents(createIterationObservationEvents(this.getSession(), batch));
|
|
1456
|
+
}
|
|
1457
|
+
async SaveIterationBatch(batch) {
|
|
1458
|
+
await this.saveIterationBatch(batch);
|
|
1459
|
+
}
|
|
1460
|
+
async completeIterationObservationStream(completion) {
|
|
1461
|
+
await this.persistEvents(createIterationCompletionEvents(this.getSession(), completion));
|
|
1462
|
+
}
|
|
1463
|
+
async CompleteIterationObservationStream(completion) {
|
|
1464
|
+
await this.completeIterationObservationStream(completion);
|
|
1465
|
+
}
|
|
1298
1466
|
stop() {
|
|
1299
1467
|
this.session = null;
|
|
1300
1468
|
}
|
|
@@ -1371,6 +1539,7 @@ class ExpandedEventReportingSink {
|
|
|
1371
1539
|
constructor(sinkName, licenseFeature) {
|
|
1372
1540
|
this.baseContext = null;
|
|
1373
1541
|
this.session = null;
|
|
1542
|
+
this.preservesRawIterationObservations = true;
|
|
1374
1543
|
this.sinkName = sinkName;
|
|
1375
1544
|
this.SinkName = sinkName;
|
|
1376
1545
|
this.licenseFeature = licenseFeature;
|
|
@@ -1406,6 +1575,18 @@ class ExpandedEventReportingSink {
|
|
|
1406
1575
|
async SaveRunResult(result) {
|
|
1407
1576
|
await this.saveRunResult(result);
|
|
1408
1577
|
}
|
|
1578
|
+
async saveIterationBatch(batch) {
|
|
1579
|
+
await this.persistEvents(createIterationObservationEvents(this.getSession(), batch));
|
|
1580
|
+
}
|
|
1581
|
+
async SaveIterationBatch(batch) {
|
|
1582
|
+
await this.saveIterationBatch(batch);
|
|
1583
|
+
}
|
|
1584
|
+
async completeIterationObservationStream(completion) {
|
|
1585
|
+
await this.persistEvents(createIterationCompletionEvents(this.getSession(), completion));
|
|
1586
|
+
}
|
|
1587
|
+
async CompleteIterationObservationStream(completion) {
|
|
1588
|
+
await this.completeIterationObservationStream(completion);
|
|
1589
|
+
}
|
|
1409
1590
|
stop() {
|
|
1410
1591
|
this.session = null;
|
|
1411
1592
|
}
|
|
@@ -1429,15 +1610,18 @@ class ExpandedEventReportingSink {
|
|
|
1429
1610
|
return this.session;
|
|
1430
1611
|
}
|
|
1431
1612
|
buildPayload(events, staticTags = {}) {
|
|
1613
|
+
const projectedEvents = this.preservesRawIterationObservations
|
|
1614
|
+
? events.map(removeSdkPercentileFields)
|
|
1615
|
+
: events;
|
|
1432
1616
|
return {
|
|
1433
1617
|
sinkName: this.sinkName,
|
|
1434
1618
|
runId: this.getSession().runId,
|
|
1435
1619
|
sessionId: this.getSession().sessionId,
|
|
1436
|
-
events:
|
|
1620
|
+
events: projectedEvents.map((event) => ({
|
|
1437
1621
|
...event,
|
|
1438
1622
|
tags: { ...staticTags, ...event.tags }
|
|
1439
1623
|
})),
|
|
1440
|
-
metrics: createReportingSinkMetricPoints(this.sinkName,
|
|
1624
|
+
metrics: createReportingSinkMetricPoints(this.sinkName, projectedEvents, staticTags)
|
|
1441
1625
|
};
|
|
1442
1626
|
}
|
|
1443
1627
|
}
|
|
@@ -1471,20 +1655,41 @@ class ExpandedHttpJsonReportingSink extends ExpandedEventReportingSink {
|
|
|
1471
1655
|
body: JSON.stringify(this.buildPayload(events, this.staticTags))
|
|
1472
1656
|
}, this.timeoutMs, this.constructor.name);
|
|
1473
1657
|
}
|
|
1658
|
+
async saveIterationBatch(batch) {
|
|
1659
|
+
await this.persistCanonicalGzipPayload(batch);
|
|
1660
|
+
}
|
|
1661
|
+
async completeIterationObservationStream(completion) {
|
|
1662
|
+
await this.persistCanonicalGzipPayload(completion);
|
|
1663
|
+
}
|
|
1664
|
+
async persistCanonicalGzipPayload(payload) {
|
|
1665
|
+
const compressed = await gzipJsonAsync(payload);
|
|
1666
|
+
await postWithTimeout(this.fetchImpl, `${trimTrailingSlashes(this.baseUrl)}${normalizePath(this.endpointPath)}`, {
|
|
1667
|
+
method: "POST",
|
|
1668
|
+
headers: {
|
|
1669
|
+
"Content-Type": "application/json",
|
|
1670
|
+
"Content-Encoding": "gzip",
|
|
1671
|
+
...this.headers
|
|
1672
|
+
},
|
|
1673
|
+
body: Uint8Array.from(compressed).buffer
|
|
1674
|
+
}, this.timeoutMs, this.constructor.name);
|
|
1675
|
+
}
|
|
1474
1676
|
}
|
|
1475
1677
|
export class PrometheusRemoteWriteReportingSink extends ExpandedHttpJsonReportingSink {
|
|
1476
1678
|
constructor(options = {}) {
|
|
1477
1679
|
super("prometheus-remote-write", "extensions.reporting_sinks.prometheus_remote_write", "/api/v1/write", options);
|
|
1680
|
+
this.iterationObservationShapeLimited = true;
|
|
1478
1681
|
}
|
|
1479
1682
|
}
|
|
1480
1683
|
export class CloudWatchReportingSink extends ExpandedHttpJsonReportingSink {
|
|
1481
1684
|
constructor(options = {}) {
|
|
1482
1685
|
super("cloudwatch", "extensions.reporting_sinks.cloudwatch", "/loadstrike/events", options);
|
|
1686
|
+
this.iterationObservationShapeLimited = true;
|
|
1483
1687
|
}
|
|
1484
1688
|
}
|
|
1485
1689
|
export class DynatraceReportingSink extends ExpandedHttpJsonReportingSink {
|
|
1486
1690
|
constructor(options = {}) {
|
|
1487
1691
|
super("dynatrace", "extensions.reporting_sinks.dynatrace", "/api/v2/logs/ingest", options);
|
|
1692
|
+
this.iterationObservationShapeLimited = true;
|
|
1488
1693
|
}
|
|
1489
1694
|
}
|
|
1490
1695
|
export class ElasticsearchReportingSink extends ExpandedHttpJsonReportingSink {
|
|
@@ -1500,11 +1705,25 @@ export class OpenSearchReportingSink extends ExpandedHttpJsonReportingSink {
|
|
|
1500
1705
|
export class NewRelicReportingSink extends ExpandedHttpJsonReportingSink {
|
|
1501
1706
|
constructor(options = {}) {
|
|
1502
1707
|
super("new-relic", "extensions.reporting_sinks.new_relic", "/log/v1", options);
|
|
1708
|
+
this.iterationObservationShapeLimited = true;
|
|
1503
1709
|
}
|
|
1504
1710
|
}
|
|
1505
1711
|
export class GenericWebhookReportingSink extends ExpandedHttpJsonReportingSink {
|
|
1506
1712
|
constructor(options = {}) {
|
|
1507
1713
|
super("webhook", "extensions.reporting_sinks.webhook", "/loadstrike", options);
|
|
1714
|
+
this.preservesRawIterationObservations = true;
|
|
1715
|
+
}
|
|
1716
|
+
async saveIterationBatch(batch) {
|
|
1717
|
+
await this.persistCanonicalGzipPayload(batch);
|
|
1718
|
+
}
|
|
1719
|
+
async SaveIterationBatch(batch) {
|
|
1720
|
+
await this.saveIterationBatch(batch);
|
|
1721
|
+
}
|
|
1722
|
+
async completeIterationObservationStream(completion) {
|
|
1723
|
+
await this.persistCanonicalGzipPayload(completion);
|
|
1724
|
+
}
|
|
1725
|
+
async CompleteIterationObservationStream(completion) {
|
|
1726
|
+
await this.completeIterationObservationStream(completion);
|
|
1508
1727
|
}
|
|
1509
1728
|
}
|
|
1510
1729
|
export class KafkaReportingSink extends ExpandedEventReportingSink {
|
|
@@ -1515,6 +1734,12 @@ export class KafkaReportingSink extends ExpandedEventReportingSink {
|
|
|
1515
1734
|
this.staticTags = normalizeStringMap(optionRecord(source, "staticTags", "StaticTags"));
|
|
1516
1735
|
this.publishAsync = pickRecordValue(source, "publishAsync", "PublishAsync");
|
|
1517
1736
|
}
|
|
1737
|
+
async saveIterationBatch(batch) {
|
|
1738
|
+
await this.publishCanonicalValue(batch);
|
|
1739
|
+
}
|
|
1740
|
+
async completeIterationObservationStream(completion) {
|
|
1741
|
+
await this.publishCanonicalValue(completion);
|
|
1742
|
+
}
|
|
1518
1743
|
async persistEvents(events) {
|
|
1519
1744
|
if (!events.length) {
|
|
1520
1745
|
return;
|
|
@@ -1524,10 +1749,17 @@ export class KafkaReportingSink extends ExpandedEventReportingSink {
|
|
|
1524
1749
|
}
|
|
1525
1750
|
await this.publishAsync(this.topic, JSON.stringify(this.buildPayload(events, this.staticTags)));
|
|
1526
1751
|
}
|
|
1752
|
+
async publishCanonicalValue(value) {
|
|
1753
|
+
if (!this.publishAsync) {
|
|
1754
|
+
throw new Error("KafkaReportingSink requires PublishAsync when a Kafka producer is not configured by the host application.");
|
|
1755
|
+
}
|
|
1756
|
+
await this.publishAsync(this.topic, JSON.stringify(value));
|
|
1757
|
+
}
|
|
1527
1758
|
}
|
|
1528
1759
|
class StatsDReportingSinkBase extends ExpandedEventReportingSink {
|
|
1529
1760
|
constructor(sinkName, licenseFeature, options = {}, dogStatsD = false) {
|
|
1530
1761
|
super(sinkName, licenseFeature);
|
|
1762
|
+
this.iterationObservationShapeLimited = true;
|
|
1531
1763
|
const source = asRecord(options);
|
|
1532
1764
|
this.prefix = optionString(source, "prefix", "Prefix").trim() || "loadstrike";
|
|
1533
1765
|
this.host = optionString(source, "host", "Host").trim() || "127.0.0.1";
|
|
@@ -1536,6 +1768,47 @@ class StatsDReportingSinkBase extends ExpandedEventReportingSink {
|
|
|
1536
1768
|
this.sendLineAsync = pickRecordValue(source, "sendLineAsync", "SendLineAsync");
|
|
1537
1769
|
this.dogStatsD = dogStatsD;
|
|
1538
1770
|
}
|
|
1771
|
+
async saveIterationBatch(batch) {
|
|
1772
|
+
for (const observation of batch.observations) {
|
|
1773
|
+
const tags = {
|
|
1774
|
+
run_id: batch.runId,
|
|
1775
|
+
result_owner_id: batch.resultOwnerId,
|
|
1776
|
+
scenario_name: observation.scenarioName,
|
|
1777
|
+
phase: observation.phase,
|
|
1778
|
+
outcome: observation.isSuccess ? "success" : "failure"
|
|
1779
|
+
};
|
|
1780
|
+
const occurredUtc = new Date();
|
|
1781
|
+
await this.sendLine(this.formatPoint({
|
|
1782
|
+
metricName: "iteration.reported_latency_us",
|
|
1783
|
+
metricKind: "gauge",
|
|
1784
|
+
occurredUtc,
|
|
1785
|
+
value: Number(observation.reportedLatencyUs64),
|
|
1786
|
+
unitOfMeasure: "us",
|
|
1787
|
+
tags
|
|
1788
|
+
}));
|
|
1789
|
+
await this.sendLine(this.formatPoint({
|
|
1790
|
+
metricName: "iteration.attempt",
|
|
1791
|
+
metricKind: "count",
|
|
1792
|
+
occurredUtc,
|
|
1793
|
+
value: 1,
|
|
1794
|
+
unitOfMeasure: "count",
|
|
1795
|
+
tags
|
|
1796
|
+
}));
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
async completeIterationObservationStream(completion) {
|
|
1800
|
+
await this.sendLine(this.formatPoint({
|
|
1801
|
+
metricName: "observation.reporting_complete",
|
|
1802
|
+
metricKind: "gauge",
|
|
1803
|
+
occurredUtc: new Date(),
|
|
1804
|
+
value: completion.reportingComplete ? 1 : 0,
|
|
1805
|
+
unitOfMeasure: "boolean",
|
|
1806
|
+
tags: {
|
|
1807
|
+
run_id: completion.runId,
|
|
1808
|
+
result_owner_id: completion.resultOwnerId
|
|
1809
|
+
}
|
|
1810
|
+
}));
|
|
1811
|
+
}
|
|
1539
1812
|
async persistEvents(events) {
|
|
1540
1813
|
const points = createReportingSinkMetricPoints(this.sinkName, events, this.tags);
|
|
1541
1814
|
for (const point of points) {
|
|
@@ -1575,6 +1848,8 @@ export class NetdataStatsDReportingSink extends StatsDReportingSinkBase {
|
|
|
1575
1848
|
export class JsonlFileReportingSink extends ExpandedEventReportingSink {
|
|
1576
1849
|
constructor(options = {}) {
|
|
1577
1850
|
super("jsonl", "extensions.reporting_sinks.jsonl");
|
|
1851
|
+
this.preservesRawIterationObservations = true;
|
|
1852
|
+
this.writeTail = Promise.resolve();
|
|
1578
1853
|
const source = asRecord(options);
|
|
1579
1854
|
this.filePath = optionString(source, "filePath", "FilePath").trim() || "loadstrike-reporting.jsonl";
|
|
1580
1855
|
this.staticTags = normalizeStringMap(optionRecord(source, "staticTags", "StaticTags"));
|
|
@@ -1583,8 +1858,28 @@ export class JsonlFileReportingSink extends ExpandedEventReportingSink {
|
|
|
1583
1858
|
if (!events.length) {
|
|
1584
1859
|
return;
|
|
1585
1860
|
}
|
|
1586
|
-
|
|
1587
|
-
|
|
1861
|
+
await this.persistCanonicalPayload(this.buildPayload(events, this.staticTags));
|
|
1862
|
+
}
|
|
1863
|
+
async saveIterationBatch(batch) {
|
|
1864
|
+
await this.persistCanonicalPayload(batch);
|
|
1865
|
+
}
|
|
1866
|
+
async SaveIterationBatch(batch) {
|
|
1867
|
+
await this.saveIterationBatch(batch);
|
|
1868
|
+
}
|
|
1869
|
+
async completeIterationObservationStream(completion) {
|
|
1870
|
+
await this.persistCanonicalPayload(completion);
|
|
1871
|
+
}
|
|
1872
|
+
async CompleteIterationObservationStream(completion) {
|
|
1873
|
+
await this.completeIterationObservationStream(completion);
|
|
1874
|
+
}
|
|
1875
|
+
persistCanonicalPayload(payload) {
|
|
1876
|
+
const line = `${JSON.stringify(payload)}\n`;
|
|
1877
|
+
const write = this.writeTail.then(async () => {
|
|
1878
|
+
await mkdir(dirname(this.filePath), { recursive: true });
|
|
1879
|
+
await appendFile(this.filePath, line, "utf8");
|
|
1880
|
+
});
|
|
1881
|
+
this.writeTail = write.catch(() => undefined);
|
|
1882
|
+
return write;
|
|
1588
1883
|
}
|
|
1589
1884
|
}
|
|
1590
1885
|
function createRealtimeStatsEvents(session, scenarios) {
|
|
@@ -1631,9 +1926,18 @@ function createRunResultEvents(session, result) {
|
|
|
1631
1926
|
completed_utc: result.completedUtc,
|
|
1632
1927
|
report_file_count: result.reportFiles.length,
|
|
1633
1928
|
disabled_sink_count: result.disabledSinks.length,
|
|
1634
|
-
sink_error_count: result.sinkErrors.length
|
|
1929
|
+
sink_error_count: result.sinkErrors.length,
|
|
1930
|
+
reporting_complete: result.reportingComplete ?? true,
|
|
1931
|
+
observation_last_batch_sequence_64: result.observationDeliveryStats?.lastBatchSequence64 ?? "-1",
|
|
1932
|
+
observation_captured_count_64: result.observationDeliveryStats?.capturedCount64 ?? "0",
|
|
1933
|
+
observation_delivered_count_64: result.observationDeliveryStats?.deliveredCount64 ?? "0",
|
|
1934
|
+
observation_dropped_buffer_count_64: result.observationDeliveryStats?.droppedBufferCount64 ?? "0",
|
|
1935
|
+
observation_dropped_sink_count_64: result.observationDeliveryStats?.droppedSinkCount64 ?? "0"
|
|
1635
1936
|
})
|
|
1636
1937
|
];
|
|
1938
|
+
for (const row of result.correlationRows ?? []) {
|
|
1939
|
+
events.push(createCorrelationOutcomeEvent(session, occurredUtc, row));
|
|
1940
|
+
}
|
|
1637
1941
|
for (const reportFile of result.reportFiles) {
|
|
1638
1942
|
events.push(createReportingEvent(session, occurredUtc, "run.report.final", null, null, {
|
|
1639
1943
|
phase: "final",
|
|
@@ -1664,8 +1968,65 @@ function createRunResultEvents(session, result) {
|
|
|
1664
1968
|
attempts: sinkError.attempts
|
|
1665
1969
|
}));
|
|
1666
1970
|
}
|
|
1971
|
+
for (const warning of result.generatorWarnings ?? []) {
|
|
1972
|
+
events.push(createReportingEvent(session, occurredUtc, "run.generator-warning.final", warning.scenarioName || null, null, {
|
|
1973
|
+
phase: "final",
|
|
1974
|
+
entity: "generator-warning",
|
|
1975
|
+
warning_code: warning.code,
|
|
1976
|
+
...(warning.sinkName ? { sink_name: warning.sinkName } : {})
|
|
1977
|
+
}, {
|
|
1978
|
+
warning_code: warning.code,
|
|
1979
|
+
sink_name: warning.sinkName ?? "",
|
|
1980
|
+
scenario_name: warning.scenarioName,
|
|
1981
|
+
scenario_index: warning.scenarioIndex ?? -1,
|
|
1982
|
+
simulation_index: warning.simulationIndex,
|
|
1983
|
+
simulation_kind: warning.simulationKind ?? "",
|
|
1984
|
+
count_64: warning.count64,
|
|
1985
|
+
message: warning.message,
|
|
1986
|
+
first_observed_utc_ns: warning.firstObservedUtcNs,
|
|
1987
|
+
last_observed_utc_ns: warning.lastObservedUtcNs
|
|
1988
|
+
}));
|
|
1989
|
+
}
|
|
1667
1990
|
return events;
|
|
1668
1991
|
}
|
|
1992
|
+
function createCorrelationOutcomeEvent(session, fallbackOccurredUtc, row) {
|
|
1993
|
+
const occurredToken = optionString(row, "occurredUtc", "OccurredUtc");
|
|
1994
|
+
const parsedOccurredUtc = occurredToken ? new Date(occurredToken) : fallbackOccurredUtc;
|
|
1995
|
+
const occurredUtc = Number.isFinite(parsedOccurredUtc.getTime())
|
|
1996
|
+
? parsedOccurredUtc
|
|
1997
|
+
: fallbackOccurredUtc;
|
|
1998
|
+
const scenarioName = optionString(row, "scenario", "Scenario");
|
|
1999
|
+
const source = optionString(row, "source", "Source");
|
|
2000
|
+
const destination = optionString(row, "destination", "Destination");
|
|
2001
|
+
const runMode = optionString(row, "runMode", "RunMode");
|
|
2002
|
+
const statusCode = optionString(row, "statusCode", "StatusCode");
|
|
2003
|
+
const gatherByField = optionString(row, "gatherByField", "GatherByField");
|
|
2004
|
+
const gatherByValue = optionString(row, "gatherByValue", "GatherByValue");
|
|
2005
|
+
return createReportingEvent(session, occurredUtc, "correlation.outcome.final", scenarioName || null, null, {
|
|
2006
|
+
phase: "final",
|
|
2007
|
+
entity: "correlation-outcome",
|
|
2008
|
+
source,
|
|
2009
|
+
destination,
|
|
2010
|
+
run_mode: runMode,
|
|
2011
|
+
status_code: statusCode,
|
|
2012
|
+
gather_by_field: gatherByField,
|
|
2013
|
+
gather_by_value: gatherByValue
|
|
2014
|
+
}, {
|
|
2015
|
+
occurred_utc: occurredToken || occurredUtc.toISOString(),
|
|
2016
|
+
source,
|
|
2017
|
+
destination,
|
|
2018
|
+
run_mode: runMode,
|
|
2019
|
+
status_code: statusCode,
|
|
2020
|
+
is_success: pickBooleanValue(row, false, "isSuccess", "IsSuccess"),
|
|
2021
|
+
is_failure: pickBooleanValue(row, false, "isFailure", "IsFailure"),
|
|
2022
|
+
gather_by_field: gatherByField,
|
|
2023
|
+
gather_by_value: gatherByValue,
|
|
2024
|
+
tracking_id: optionString(row, "trackingId", "TrackingId"),
|
|
2025
|
+
event_id: optionString(row, "eventId", "EventId"),
|
|
2026
|
+
latency_ms: optionNumber(row, "latencyMs", "LatencyMs") ?? 0,
|
|
2027
|
+
message: optionString(row, "message", "Message")
|
|
2028
|
+
});
|
|
2029
|
+
}
|
|
1669
2030
|
function createNodeSummaryEvent(session, occurredUtc, stats) {
|
|
1670
2031
|
return createReportingEvent(session, occurredUtc, "test.final", null, null, {
|
|
1671
2032
|
phase: "final",
|
|
@@ -1851,6 +2212,125 @@ function createReportingEvent(session, occurredUtc, eventType, scenarioName, ste
|
|
|
1851
2212
|
fields: eventFields
|
|
1852
2213
|
};
|
|
1853
2214
|
}
|
|
2215
|
+
function removeSdkPercentileFields(event) {
|
|
2216
|
+
const fields = Object.fromEntries(Object.entries(event.fields).filter(([name]) => !/_(?:latency|bytes)_p(?:50|75|95|99)(?:_|$)/iu.test(name)));
|
|
2217
|
+
return {
|
|
2218
|
+
...event,
|
|
2219
|
+
fields
|
|
2220
|
+
};
|
|
2221
|
+
}
|
|
2222
|
+
function createIterationObservationEvents(session, batch) {
|
|
2223
|
+
const events = [];
|
|
2224
|
+
const occurredUtc = dateFromUtcNanoseconds(batch.createdUtcNs);
|
|
2225
|
+
for (const observation of batch.observations) {
|
|
2226
|
+
const tags = {
|
|
2227
|
+
run_id: batch.runId,
|
|
2228
|
+
result_owner_id: batch.resultOwnerId,
|
|
2229
|
+
phase: observation.phase,
|
|
2230
|
+
scenario_name: observation.scenarioName,
|
|
2231
|
+
simulation_kind: observation.simulationKind,
|
|
2232
|
+
is_final_attempt: observation.isFinalAttempt ? "true" : "false"
|
|
2233
|
+
};
|
|
2234
|
+
events.push({
|
|
2235
|
+
runId: batch.runId,
|
|
2236
|
+
eventType: "iteration.observation",
|
|
2237
|
+
occurredUtc,
|
|
2238
|
+
sessionId: batch.sessionId,
|
|
2239
|
+
testSuite: session.testSuite,
|
|
2240
|
+
testName: session.testName,
|
|
2241
|
+
clusterId: session.clusterId,
|
|
2242
|
+
nodeType: session.nodeType,
|
|
2243
|
+
machineName: session.machineName,
|
|
2244
|
+
scenarioName: observation.scenarioName,
|
|
2245
|
+
stepName: null,
|
|
2246
|
+
tags,
|
|
2247
|
+
fields: {
|
|
2248
|
+
schema_version: observation.schemaVersion,
|
|
2249
|
+
batch_id: batch.batchId,
|
|
2250
|
+
observation_id: observation.observationId,
|
|
2251
|
+
iteration_id: observation.iterationId,
|
|
2252
|
+
process_group: observation.processGroup,
|
|
2253
|
+
scenario_index: observation.scenarioIndex,
|
|
2254
|
+
simulation_index: observation.simulationIndex,
|
|
2255
|
+
global_ordinal_64: observation.globalOrdinal64,
|
|
2256
|
+
shard_index: observation.shardIndex,
|
|
2257
|
+
shard_count: observation.shardCount,
|
|
2258
|
+
attempt_index: observation.attemptIndex,
|
|
2259
|
+
started_utc_ns: observation.startedUtcNs,
|
|
2260
|
+
completed_utc_ns: observation.completedUtcNs,
|
|
2261
|
+
observed_latency_us_64: observation.observedLatencyUs64,
|
|
2262
|
+
reported_latency_us_64: observation.reportedLatencyUs64,
|
|
2263
|
+
is_success: observation.isSuccess,
|
|
2264
|
+
status_code: observation.statusCode,
|
|
2265
|
+
size_bytes_64: observation.sizeBytes64
|
|
2266
|
+
}
|
|
2267
|
+
});
|
|
2268
|
+
for (const step of observation.steps) {
|
|
2269
|
+
events.push({
|
|
2270
|
+
runId: batch.runId,
|
|
2271
|
+
eventType: "iteration.step",
|
|
2272
|
+
occurredUtc,
|
|
2273
|
+
sessionId: batch.sessionId,
|
|
2274
|
+
testSuite: session.testSuite,
|
|
2275
|
+
testName: session.testName,
|
|
2276
|
+
clusterId: session.clusterId,
|
|
2277
|
+
nodeType: session.nodeType,
|
|
2278
|
+
machineName: session.machineName,
|
|
2279
|
+
scenarioName: observation.scenarioName,
|
|
2280
|
+
stepName: step.stepName,
|
|
2281
|
+
tags: { ...tags },
|
|
2282
|
+
fields: {
|
|
2283
|
+
schema_version: observation.schemaVersion,
|
|
2284
|
+
batch_id: batch.batchId,
|
|
2285
|
+
observation_id: observation.observationId,
|
|
2286
|
+
iteration_id: observation.iterationId,
|
|
2287
|
+
attempt_index: observation.attemptIndex,
|
|
2288
|
+
sort_index: step.sortIndex,
|
|
2289
|
+
started_utc_ns: step.startedUtcNs,
|
|
2290
|
+
completed_utc_ns: step.completedUtcNs,
|
|
2291
|
+
observed_latency_us_64: step.observedLatencyUs64,
|
|
2292
|
+
reported_latency_us_64: step.reportedLatencyUs64,
|
|
2293
|
+
is_success: step.isSuccess,
|
|
2294
|
+
status_code: step.statusCode,
|
|
2295
|
+
size_bytes_64: step.sizeBytes64
|
|
2296
|
+
}
|
|
2297
|
+
});
|
|
2298
|
+
}
|
|
2299
|
+
}
|
|
2300
|
+
return events;
|
|
2301
|
+
}
|
|
2302
|
+
function createIterationCompletionEvents(session, completion) {
|
|
2303
|
+
const occurredUtc = dateFromUtcNanoseconds(completion.completedUtcNs);
|
|
2304
|
+
return [{
|
|
2305
|
+
runId: completion.runId,
|
|
2306
|
+
eventType: "observation.stream.completed",
|
|
2307
|
+
occurredUtc,
|
|
2308
|
+
sessionId: completion.sessionId,
|
|
2309
|
+
testSuite: session.testSuite,
|
|
2310
|
+
testName: session.testName,
|
|
2311
|
+
clusterId: session.clusterId,
|
|
2312
|
+
nodeType: session.nodeType,
|
|
2313
|
+
machineName: session.machineName,
|
|
2314
|
+
scenarioName: null,
|
|
2315
|
+
stepName: null,
|
|
2316
|
+
tags: {
|
|
2317
|
+
run_id: completion.runId,
|
|
2318
|
+
result_owner_id: completion.resultOwnerId,
|
|
2319
|
+
phase: "final"
|
|
2320
|
+
},
|
|
2321
|
+
fields: {
|
|
2322
|
+
schema_version: completion.schemaVersion,
|
|
2323
|
+
process_group: completion.processGroup,
|
|
2324
|
+
last_batch_sequence_64: completion.lastBatchSequence64,
|
|
2325
|
+
captured_count_64: completion.capturedCount64,
|
|
2326
|
+
delivered_count_64: completion.deliveredCount64,
|
|
2327
|
+
dropped_buffer_count_64: completion.droppedBufferCount64,
|
|
2328
|
+
dropped_sink_count_64: completion.droppedSinkCount64,
|
|
2329
|
+
reporting_complete: completion.reportingComplete,
|
|
2330
|
+
completed_utc_ns: completion.completedUtcNs
|
|
2331
|
+
}
|
|
2332
|
+
}];
|
|
2333
|
+
}
|
|
1854
2334
|
function portalEventPayload(event, index) {
|
|
1855
2335
|
return {
|
|
1856
2336
|
eventId: portalEventId(event, index),
|
|
@@ -1883,6 +2363,22 @@ function portalEventId(event, index) {
|
|
|
1883
2363
|
});
|
|
1884
2364
|
return `lsr_${createHash("sha256").update(material).digest("hex").slice(0, 40)}`;
|
|
1885
2365
|
}
|
|
2366
|
+
function dateFromUtcNanoseconds(value) {
|
|
2367
|
+
try {
|
|
2368
|
+
const nanoseconds = BigInt(value);
|
|
2369
|
+
if (nanoseconds < 0n) {
|
|
2370
|
+
return new Date(0);
|
|
2371
|
+
}
|
|
2372
|
+
const milliseconds = nanoseconds / 1000000n;
|
|
2373
|
+
const numeric = Number(milliseconds);
|
|
2374
|
+
return Number.isFinite(numeric) && numeric <= 8640000000000000
|
|
2375
|
+
? new Date(numeric)
|
|
2376
|
+
: new Date(0);
|
|
2377
|
+
}
|
|
2378
|
+
catch {
|
|
2379
|
+
return new Date(0);
|
|
2380
|
+
}
|
|
2381
|
+
}
|
|
1886
2382
|
function addMeasurementFields(fields, prefix, measurement) {
|
|
1887
2383
|
const request = measurement?.request ?? { count: 0, percent: 0, rps: 0 };
|
|
1888
2384
|
const latency = measurement?.latency ?? {
|
|
@@ -1923,10 +2419,6 @@ function addMeasurementFields(fields, prefix, measurement) {
|
|
|
1923
2419
|
fields[`${prefix}_latency_min_ms`] = latency.minMs ?? 0;
|
|
1924
2420
|
fields[`${prefix}_latency_mean_ms`] = latency.meanMs ?? 0;
|
|
1925
2421
|
fields[`${prefix}_latency_max_ms`] = latency.maxMs ?? 0;
|
|
1926
|
-
fields[`${prefix}_latency_p50_ms`] = latency.percent50 ?? 0;
|
|
1927
|
-
fields[`${prefix}_latency_p75_ms`] = latency.percent75 ?? 0;
|
|
1928
|
-
fields[`${prefix}_latency_p95_ms`] = latency.percent95 ?? 0;
|
|
1929
|
-
fields[`${prefix}_latency_p99_ms`] = latency.percent99 ?? 0;
|
|
1930
2422
|
fields[`${prefix}_latency_std_dev`] = latency.stdDev ?? 0;
|
|
1931
2423
|
fields[`${prefix}_latency_le_800_count`] = latencyCount.lessOrEq800 ?? 0;
|
|
1932
2424
|
fields[`${prefix}_latency_gt_800_lt_1200_count`] = latencyCount.more800Less1200 ?? 0;
|
|
@@ -1935,10 +2427,6 @@ function addMeasurementFields(fields, prefix, measurement) {
|
|
|
1935
2427
|
fields[`${prefix}_bytes_min`] = dataTransfer.minBytes ?? 0;
|
|
1936
2428
|
fields[`${prefix}_bytes_mean`] = dataTransfer.meanBytes ?? 0;
|
|
1937
2429
|
fields[`${prefix}_bytes_max`] = dataTransfer.maxBytes ?? 0;
|
|
1938
|
-
fields[`${prefix}_bytes_p50`] = dataTransfer.percent50 ?? 0;
|
|
1939
|
-
fields[`${prefix}_bytes_p75`] = dataTransfer.percent75 ?? 0;
|
|
1940
|
-
fields[`${prefix}_bytes_p95`] = dataTransfer.percent95 ?? 0;
|
|
1941
|
-
fields[`${prefix}_bytes_p99`] = dataTransfer.percent99 ?? 0;
|
|
1942
2430
|
fields[`${prefix}_bytes_std_dev`] = dataTransfer.stdDev ?? 0;
|
|
1943
2431
|
fields[`${prefix}_status_code_count`] = statusCodes.length;
|
|
1944
2432
|
}
|
|
@@ -3074,27 +3562,93 @@ function deepCloneValue(value) {
|
|
|
3074
3562
|
}
|
|
3075
3563
|
return value;
|
|
3076
3564
|
}
|
|
3565
|
+
function gzipJsonRequestBody(value) {
|
|
3566
|
+
return Uint8Array.from(serializeIterationObservationBatchGzipJson(value)).buffer;
|
|
3567
|
+
}
|
|
3077
3568
|
async function postWithTimeout(fetchImpl, url, init, timeoutMs, sinkName) {
|
|
3078
3569
|
const controller = new AbortController();
|
|
3079
3570
|
const timer = setTimeout(() => controller.abort(), Math.max(timeoutMs, 1));
|
|
3080
3571
|
try {
|
|
3081
3572
|
const response = await fetchImpl(url, { ...init, signal: controller.signal });
|
|
3082
|
-
const body = await readResponseBodyText(response);
|
|
3083
3573
|
if (!response.ok) {
|
|
3084
|
-
throw new
|
|
3574
|
+
throw new ReportingSinkHttpError(sinkName, response.status, readHttpResponseStatusText(response), readHttpResponseRequestId(response));
|
|
3085
3575
|
}
|
|
3086
|
-
return
|
|
3576
|
+
return await readResponseBodyText(response);
|
|
3087
3577
|
}
|
|
3088
3578
|
finally {
|
|
3089
3579
|
clearTimeout(timer);
|
|
3090
3580
|
}
|
|
3091
3581
|
}
|
|
3092
3582
|
async function readResponseBodyText(response) {
|
|
3093
|
-
const
|
|
3094
|
-
if (typeof
|
|
3583
|
+
const body = response.body;
|
|
3584
|
+
if (body && typeof body.getReader === "function") {
|
|
3585
|
+
const reader = body.getReader();
|
|
3586
|
+
const chunks = [];
|
|
3587
|
+
let bytes = 0;
|
|
3588
|
+
try {
|
|
3589
|
+
while (true) {
|
|
3590
|
+
const next = await reader.read();
|
|
3591
|
+
if (next.done)
|
|
3592
|
+
break;
|
|
3593
|
+
if (!next.value)
|
|
3594
|
+
continue;
|
|
3595
|
+
if (bytes + next.value.byteLength > MAXIMUM_HTTP_RESPONSE_BODY_BYTES) {
|
|
3596
|
+
await reader.cancel();
|
|
3597
|
+
throw new Error("Reporting sink response exceeded the bounded response limit.");
|
|
3598
|
+
}
|
|
3599
|
+
chunks.push(next.value);
|
|
3600
|
+
bytes += next.value.byteLength;
|
|
3601
|
+
}
|
|
3602
|
+
}
|
|
3603
|
+
finally {
|
|
3604
|
+
reader.releaseLock();
|
|
3605
|
+
}
|
|
3606
|
+
return Buffer.concat(chunks.map((chunk) => Buffer.from(chunk))).toString("utf8");
|
|
3607
|
+
}
|
|
3608
|
+
return "";
|
|
3609
|
+
}
|
|
3610
|
+
function readHttpResponseStatusText(response) {
|
|
3611
|
+
try {
|
|
3612
|
+
return normalizeHttpMetadata(response.statusText);
|
|
3613
|
+
}
|
|
3614
|
+
catch {
|
|
3615
|
+
return "";
|
|
3616
|
+
}
|
|
3617
|
+
}
|
|
3618
|
+
function readHttpResponseRequestId(response) {
|
|
3619
|
+
for (const name of [
|
|
3620
|
+
"x-request-id",
|
|
3621
|
+
"x-correlation-id",
|
|
3622
|
+
"request-id",
|
|
3623
|
+
"traceparent"
|
|
3624
|
+
]) {
|
|
3625
|
+
try {
|
|
3626
|
+
const value = response.headers?.get(name);
|
|
3627
|
+
if (value) {
|
|
3628
|
+
return normalizeHttpMetadata(value);
|
|
3629
|
+
}
|
|
3630
|
+
}
|
|
3631
|
+
catch {
|
|
3632
|
+
return "";
|
|
3633
|
+
}
|
|
3634
|
+
}
|
|
3635
|
+
return "";
|
|
3636
|
+
}
|
|
3637
|
+
function normalizeHttpMetadata(value) {
|
|
3638
|
+
let text;
|
|
3639
|
+
try {
|
|
3640
|
+
const source = String(value ?? "");
|
|
3641
|
+
text = source.length <= MAXIMUM_HTTP_METADATA_CHARACTERS
|
|
3642
|
+
? source
|
|
3643
|
+
: source.slice(0, MAXIMUM_HTTP_METADATA_CHARACTERS - HTTP_METADATA_TRUNCATION_SUFFIX.length) + HTTP_METADATA_TRUNCATION_SUFFIX;
|
|
3644
|
+
}
|
|
3645
|
+
catch {
|
|
3095
3646
|
return "";
|
|
3096
3647
|
}
|
|
3097
|
-
return
|
|
3648
|
+
return redactIterationObservationSecrets(Buffer.from(text, "utf8").toString("utf8")
|
|
3649
|
+
.replace(/[\u0000-\u001F\u007F-\u009F]/gu, " ")
|
|
3650
|
+
.replace(/\s{2,}/gu, " ")
|
|
3651
|
+
.trim());
|
|
3098
3652
|
}
|
|
3099
3653
|
function validatePortalIngestResponse(responseBody) {
|
|
3100
3654
|
const text = String(responseBody ?? "").trim();
|
|
@@ -3147,6 +3701,8 @@ export const __loadstrikeTestExports = {
|
|
|
3147
3701
|
cloneScenarioStats,
|
|
3148
3702
|
cloneSessionStartInfo,
|
|
3149
3703
|
createFinalStatsEvents,
|
|
3704
|
+
createIterationCompletionEvents,
|
|
3705
|
+
createIterationObservationEvents,
|
|
3150
3706
|
createRealtimeStatsEvents,
|
|
3151
3707
|
createRunResultEvents,
|
|
3152
3708
|
createReportingEvent,
|
|
@@ -3163,6 +3719,7 @@ export const __loadstrikeTestExports = {
|
|
|
3163
3719
|
normalizeStringMap,
|
|
3164
3720
|
optionNumber,
|
|
3165
3721
|
optionString,
|
|
3722
|
+
portalEventPayload,
|
|
3166
3723
|
pickBooleanValue,
|
|
3167
3724
|
pickRecordValue,
|
|
3168
3725
|
postWithTimeout,
|