@loadstrike/loadstrike-sdk 1.0.23401 → 1.0.26701
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/dist/cjs/runtime.js +45 -16
- package/dist/cjs/sinks.js +80 -7
- package/dist/esm/runtime.js +46 -17
- package/dist/esm/sinks.js +80 -8
- package/dist/types/sinks.d.ts +7 -2
- package/package.json +1 -1
package/dist/cjs/runtime.js
CHANGED
|
@@ -2487,7 +2487,7 @@ class LoadStrikeRunner {
|
|
|
2487
2487
|
const scenarioAccumulators = new Map();
|
|
2488
2488
|
const scenarioDurationsMs = new Map();
|
|
2489
2489
|
const stepStats = new Map();
|
|
2490
|
-
const sinks = this.options.reportingSinks ?? [];
|
|
2490
|
+
const sinks = (this.options.reportingSinks ?? []).map((sink) => (0, sinks_js_1.cloneReportingSinkForRun)(sink));
|
|
2491
2491
|
const sinkStates = sinks.map((sink, index) => ({
|
|
2492
2492
|
sink,
|
|
2493
2493
|
disabled: false,
|
|
@@ -2521,6 +2521,7 @@ class LoadStrikeRunner {
|
|
|
2521
2521
|
let sinksStopped = false;
|
|
2522
2522
|
let realtimeTimer = null;
|
|
2523
2523
|
let realtimeInFlight = false;
|
|
2524
|
+
let realtimeCurrent = null;
|
|
2524
2525
|
let licenseClient = null;
|
|
2525
2526
|
let licensePayload = null;
|
|
2526
2527
|
let licenseSession = null;
|
|
@@ -2616,9 +2617,38 @@ class LoadStrikeRunner {
|
|
|
2616
2617
|
}
|
|
2617
2618
|
};
|
|
2618
2619
|
const reportingIntervalMs = Math.max(Math.trunc((this.options.reportingIntervalSeconds ?? 5) * 1000), 1);
|
|
2620
|
+
const triggerRealtimeSnapshot = () => {
|
|
2621
|
+
if (realtimeCurrent) {
|
|
2622
|
+
return;
|
|
2623
|
+
}
|
|
2624
|
+
const current = emitRealtimeSnapshot()
|
|
2625
|
+
.catch((error) => {
|
|
2626
|
+
sinkErrors.push({
|
|
2627
|
+
sinkName: "realtime",
|
|
2628
|
+
phase: "realtime",
|
|
2629
|
+
message: String(error ?? "realtime reporting failed"),
|
|
2630
|
+
attempts: 1
|
|
2631
|
+
});
|
|
2632
|
+
})
|
|
2633
|
+
.finally(() => {
|
|
2634
|
+
if (realtimeCurrent === current) {
|
|
2635
|
+
realtimeCurrent = null;
|
|
2636
|
+
}
|
|
2637
|
+
});
|
|
2638
|
+
realtimeCurrent = current;
|
|
2639
|
+
};
|
|
2640
|
+
const stopRealtimeReporting = async () => {
|
|
2641
|
+
if (realtimeTimer) {
|
|
2642
|
+
clearInterval(realtimeTimer);
|
|
2643
|
+
realtimeTimer = null;
|
|
2644
|
+
}
|
|
2645
|
+
if (realtimeCurrent) {
|
|
2646
|
+
await realtimeCurrent;
|
|
2647
|
+
}
|
|
2648
|
+
};
|
|
2619
2649
|
if (sinkStates.length > 0 || toBoolean(this.options.displayConsoleMetrics, true)) {
|
|
2620
2650
|
realtimeTimer = setInterval(() => {
|
|
2621
|
-
|
|
2651
|
+
triggerRealtimeSnapshot();
|
|
2622
2652
|
}, reportingIntervalMs);
|
|
2623
2653
|
if (typeof realtimeTimer.unref === "function") {
|
|
2624
2654
|
realtimeTimer.unref();
|
|
@@ -2706,6 +2736,7 @@ class LoadStrikeRunner {
|
|
|
2706
2736
|
failedCorrelationRows: buildDetailedFailedCorrelationRows()
|
|
2707
2737
|
};
|
|
2708
2738
|
}
|
|
2739
|
+
await stopRealtimeReporting();
|
|
2709
2740
|
result.pluginsData = mergePluginData(result.pluginsData, await this.collectPluginData(plugins, attachRunResultAliases(result), pluginLifecycleErrors));
|
|
2710
2741
|
const finalizedResult = attachRunResultAliases(result);
|
|
2711
2742
|
finalizedResult.logFiles = mergeStringArrays(finalizedResult.logFiles, loggerSetup.logFiles);
|
|
@@ -2720,9 +2751,7 @@ class LoadStrikeRunner {
|
|
|
2720
2751
|
return finalizedResult;
|
|
2721
2752
|
}
|
|
2722
2753
|
finally {
|
|
2723
|
-
|
|
2724
|
-
clearInterval(realtimeTimer);
|
|
2725
|
-
}
|
|
2754
|
+
await stopRealtimeReporting();
|
|
2726
2755
|
if (!sinksStopped) {
|
|
2727
2756
|
await this.stopSinks(sinkStates, sinkRetryCount, sinkRetryBackoffMs, sinkErrors);
|
|
2728
2757
|
}
|
|
@@ -3043,14 +3072,14 @@ class LoadStrikeRunner {
|
|
|
3043
3072
|
}
|
|
3044
3073
|
catch (error) {
|
|
3045
3074
|
if (attempts > retryCount) {
|
|
3075
|
+
sinkErrors.push({
|
|
3076
|
+
sinkName: state.name,
|
|
3077
|
+
phase,
|
|
3078
|
+
message: String(error ?? "sink action failed"),
|
|
3079
|
+
attempts
|
|
3080
|
+
});
|
|
3046
3081
|
if (disableOnFailure) {
|
|
3047
3082
|
state.disabled = true;
|
|
3048
|
-
sinkErrors.push({
|
|
3049
|
-
sinkName: state.name,
|
|
3050
|
-
phase,
|
|
3051
|
-
message: String(error ?? "sink action failed"),
|
|
3052
|
-
attempts
|
|
3053
|
-
});
|
|
3054
3083
|
}
|
|
3055
3084
|
return;
|
|
3056
3085
|
}
|
|
@@ -3651,7 +3680,7 @@ function normalizeReply(value) {
|
|
|
3651
3680
|
statusCode: normalizeStatusCode(value?.statusCode, Boolean(value?.isSuccess)),
|
|
3652
3681
|
message: value?.message ? String(value.message) : "",
|
|
3653
3682
|
sizeBytes: Number.isFinite(value?.sizeBytes) ? Number(value.sizeBytes) : 0,
|
|
3654
|
-
customLatencyMs: Number.isFinite(value?.customLatencyMs) ? Number(value.customLatencyMs) :
|
|
3683
|
+
customLatencyMs: Number.isFinite(value?.customLatencyMs) ? Number(value.customLatencyMs) : -1,
|
|
3655
3684
|
payload: value?.payload
|
|
3656
3685
|
};
|
|
3657
3686
|
}
|
|
@@ -3663,7 +3692,7 @@ function attachReplyProjection(reply) {
|
|
|
3663
3692
|
SizeBytes: "sizeBytes",
|
|
3664
3693
|
Payload: "payload"
|
|
3665
3694
|
});
|
|
3666
|
-
defineAliasProperty(reply, "CustomLatencyMs", () => Number.isFinite(reply.customLatencyMs) ? Number(reply.customLatencyMs) :
|
|
3695
|
+
defineAliasProperty(reply, "CustomLatencyMs", () => Number.isFinite(reply.customLatencyMs) ? Number(reply.customLatencyMs) : -1);
|
|
3667
3696
|
const asReply = () => attachReplyProjection({
|
|
3668
3697
|
isSuccess: reply.isSuccess,
|
|
3669
3698
|
statusCode: reply.statusCode,
|
|
@@ -4561,15 +4590,15 @@ function attachRunResultAliases(result) {
|
|
|
4561
4590
|
defineAliasProperty(projected, "Duration", () => projected.durationMs);
|
|
4562
4591
|
return projected;
|
|
4563
4592
|
}
|
|
4564
|
-
function resolveResponseCustomLatency(customLatencyMs,
|
|
4593
|
+
function resolveResponseCustomLatency(customLatencyMs, _usesOverloadDefaults) {
|
|
4565
4594
|
if (!Number.isFinite(customLatencyMs)) {
|
|
4566
|
-
return
|
|
4595
|
+
return -1;
|
|
4567
4596
|
}
|
|
4568
4597
|
return Number(customLatencyMs);
|
|
4569
4598
|
}
|
|
4570
4599
|
function resolveRecordedLatency(customLatencyMs, observedLatencyMs) {
|
|
4571
4600
|
if (!Number.isFinite(customLatencyMs)) {
|
|
4572
|
-
return 0;
|
|
4601
|
+
return Math.max(observedLatencyMs, 0);
|
|
4573
4602
|
}
|
|
4574
4603
|
const resolved = Number(customLatencyMs);
|
|
4575
4604
|
if (resolved >= 0) {
|
package/dist/cjs/sinks.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.__loadstrikeTestExports = exports.OtelCollectorReportingSink = exports.SplunkReportingSink = exports.DatadogReportingSink = exports.TimescaleDbReportingSink = exports.GrafanaLokiReportingSink = exports.InfluxDbReportingSink = exports.PortalReportingSink = exports.CompositeReportingSink = exports.ConsoleReportingSink = exports.MemoryReportingSink = exports.OtelCollectorReportingSinkOptions = exports.SplunkReportingSinkOptions = exports.DatadogReportingSinkOptions = exports.TimescaleDbReportingSinkOptions = exports.GrafanaLokiReportingSinkOptions = exports.InfluxDbReportingSinkOptions = void 0;
|
|
4
|
+
exports.cloneReportingSinkForRun = cloneReportingSinkForRun;
|
|
4
5
|
const runtime_js_1 = require("./runtime.js");
|
|
5
6
|
const node_crypto_1 = require("node:crypto");
|
|
6
7
|
const pg_1 = require("pg");
|
|
@@ -395,10 +396,14 @@ class PortalReportingSink {
|
|
|
395
396
|
this.session = null;
|
|
396
397
|
this.runToken = "";
|
|
397
398
|
this.ingestUrl = "";
|
|
399
|
+
this.optionsInput = { ...options };
|
|
398
400
|
const source = asRecord(options);
|
|
399
401
|
this.timeoutMs = resolveTimeoutMs(optionNumber(source, "timeoutSeconds", "TimeoutSeconds"), optionNumber(source, "timeoutMs", "TimeoutMs"));
|
|
400
402
|
this.fetchImpl = pickRecordValue(source, "fetchImpl", "FetchImpl") ?? fetch;
|
|
401
403
|
}
|
|
404
|
+
cloneForRun() {
|
|
405
|
+
return new PortalReportingSink(this.optionsInput);
|
|
406
|
+
}
|
|
402
407
|
init(context, _infraConfig) {
|
|
403
408
|
this.baseContext = cloneBaseContext(context);
|
|
404
409
|
}
|
|
@@ -411,7 +416,9 @@ class PortalReportingSink {
|
|
|
411
416
|
if (!this.runToken || !this.ingestUrl) {
|
|
412
417
|
throw new Error("PortalReportingSink requires a managed portal reporting session.");
|
|
413
418
|
}
|
|
414
|
-
this.session = sinkSessionMetadataFromContext(this.getBaseContext(), session
|
|
419
|
+
this.session = sinkSessionMetadataFromContext(this.getBaseContext(), session, {
|
|
420
|
+
distinctRunIdFallback: true
|
|
421
|
+
});
|
|
415
422
|
}
|
|
416
423
|
Start(session) {
|
|
417
424
|
this.start(session);
|
|
@@ -460,7 +467,7 @@ class PortalReportingSink {
|
|
|
460
467
|
if (!events.length) {
|
|
461
468
|
return;
|
|
462
469
|
}
|
|
463
|
-
await postWithTimeout(this.fetchImpl, this.ingestUrl, {
|
|
470
|
+
const responseBody = await postWithTimeout(this.fetchImpl, this.ingestUrl, {
|
|
464
471
|
method: "POST",
|
|
465
472
|
headers: { "Content-Type": "application/json" },
|
|
466
473
|
body: JSON.stringify({
|
|
@@ -468,9 +475,16 @@ class PortalReportingSink {
|
|
|
468
475
|
events: events.map((event, index) => portalEventPayload(event, index))
|
|
469
476
|
})
|
|
470
477
|
}, this.timeoutMs, "PortalReportingSink");
|
|
478
|
+
validatePortalIngestResponse(responseBody);
|
|
471
479
|
}
|
|
472
480
|
}
|
|
473
481
|
exports.PortalReportingSink = PortalReportingSink;
|
|
482
|
+
function cloneReportingSinkForRun(sink) {
|
|
483
|
+
if (sink instanceof PortalReportingSink) {
|
|
484
|
+
return sink.cloneForRun();
|
|
485
|
+
}
|
|
486
|
+
return sink;
|
|
487
|
+
}
|
|
474
488
|
class InfluxDbReportingSink {
|
|
475
489
|
constructor(options = {}) {
|
|
476
490
|
this.sinkName = "influxdb";
|
|
@@ -2208,15 +2222,20 @@ function toOtelAnyValue(value) {
|
|
|
2208
2222
|
function toUnixSeconds(value) {
|
|
2209
2223
|
return value.getTime() / 1000;
|
|
2210
2224
|
}
|
|
2211
|
-
function sinkSessionMetadataFromContext(context, session) {
|
|
2225
|
+
function sinkSessionMetadataFromContext(context, session, options = {}) {
|
|
2212
2226
|
const nodeInfo = context.getNodeInfo?.() ?? context.nodeInfo;
|
|
2213
2227
|
const testInfo = context.testInfo;
|
|
2214
2228
|
const sessionStartedUtc = String(session?.startedUtc ?? session?.StartedUtc ?? "");
|
|
2215
2229
|
const contextStartedUtc = String(testInfo.createdUtc ?? testInfo.CreatedUtc ?? "");
|
|
2216
|
-
const
|
|
2230
|
+
const explicitRunId = String(session?.portalReportingRunId ?? session?.PortalReportingRunId ?? "").trim();
|
|
2231
|
+
const sessionId = String(testInfo.sessionId ?? "");
|
|
2232
|
+
const fallbackRunId = options.distinctRunIdFallback
|
|
2233
|
+
? buildDistinctSinkRunId(sessionId)
|
|
2234
|
+
: sessionId;
|
|
2235
|
+
const runId = explicitRunId || fallbackRunId;
|
|
2217
2236
|
return {
|
|
2218
|
-
runId: runId ||
|
|
2219
|
-
sessionId
|
|
2237
|
+
runId: runId || sessionId,
|
|
2238
|
+
sessionId,
|
|
2220
2239
|
testSuite: String(testInfo.testSuite ?? ""),
|
|
2221
2240
|
testName: String(testInfo.testName ?? ""),
|
|
2222
2241
|
clusterId: String(testInfo.clusterId ?? ""),
|
|
@@ -2225,6 +2244,14 @@ function sinkSessionMetadataFromContext(context, session) {
|
|
|
2225
2244
|
startedUtc: sessionStartedUtc || contextStartedUtc || new Date().toISOString()
|
|
2226
2245
|
};
|
|
2227
2246
|
}
|
|
2247
|
+
function buildDistinctSinkRunId(sessionId) {
|
|
2248
|
+
const sessionPart = String(sessionId ?? "")
|
|
2249
|
+
.replace(/[^A-Za-z0-9._-]+/g, "-")
|
|
2250
|
+
.replace(/^-+|-+$/g, "")
|
|
2251
|
+
.slice(0, 80);
|
|
2252
|
+
const uniquePart = (0, node_crypto_1.randomBytes)(8).toString("hex");
|
|
2253
|
+
return sessionPart ? `${sessionPart}-${uniquePart}` : uniquePart;
|
|
2254
|
+
}
|
|
2228
2255
|
function readInfluxOptionsFromConfig(infraConfig, configurationSectionPath) {
|
|
2229
2256
|
const section = resolveConfigSection(infraConfig, configurationSectionPath);
|
|
2230
2257
|
return {
|
|
@@ -2823,15 +2850,61 @@ async function postWithTimeout(fetchImpl, url, init, timeoutMs, sinkName) {
|
|
|
2823
2850
|
const timer = setTimeout(() => controller.abort(), Math.max(timeoutMs, 1));
|
|
2824
2851
|
try {
|
|
2825
2852
|
const response = await fetchImpl(url, { ...init, signal: controller.signal });
|
|
2853
|
+
const body = await readResponseBodyText(response);
|
|
2826
2854
|
if (!response.ok) {
|
|
2827
|
-
const body = await response.text();
|
|
2828
2855
|
throw new Error(`${sinkName} write failed with status ${response.status}: ${body}`);
|
|
2829
2856
|
}
|
|
2857
|
+
return body;
|
|
2830
2858
|
}
|
|
2831
2859
|
finally {
|
|
2832
2860
|
clearTimeout(timer);
|
|
2833
2861
|
}
|
|
2834
2862
|
}
|
|
2863
|
+
async function readResponseBodyText(response) {
|
|
2864
|
+
const candidate = response;
|
|
2865
|
+
if (typeof candidate.text !== "function") {
|
|
2866
|
+
return "";
|
|
2867
|
+
}
|
|
2868
|
+
return String(await candidate.text());
|
|
2869
|
+
}
|
|
2870
|
+
function validatePortalIngestResponse(responseBody) {
|
|
2871
|
+
const text = String(responseBody ?? "").trim();
|
|
2872
|
+
if (!text) {
|
|
2873
|
+
return;
|
|
2874
|
+
}
|
|
2875
|
+
let parsed;
|
|
2876
|
+
try {
|
|
2877
|
+
parsed = JSON.parse(text);
|
|
2878
|
+
}
|
|
2879
|
+
catch {
|
|
2880
|
+
return;
|
|
2881
|
+
}
|
|
2882
|
+
if (!isRecord(parsed)) {
|
|
2883
|
+
return;
|
|
2884
|
+
}
|
|
2885
|
+
const rejected = readPortalIngestCount(parsed, "rejectedCount", "RejectedCount", "rejected");
|
|
2886
|
+
if (rejected <= 0) {
|
|
2887
|
+
return;
|
|
2888
|
+
}
|
|
2889
|
+
const accepted = readPortalIngestCount(parsed, "acceptedCount", "AcceptedCount", "accepted");
|
|
2890
|
+
const duplicate = readPortalIngestCount(parsed, "duplicateCount", "DuplicateCount", "duplicate");
|
|
2891
|
+
throw new Error(`PortalReportingSink ingest rejected ${rejected} reporting event(s). Accepted ${accepted}, duplicate ${duplicate}.`);
|
|
2892
|
+
}
|
|
2893
|
+
function readPortalIngestCount(source, ...keys) {
|
|
2894
|
+
for (const key of keys) {
|
|
2895
|
+
const value = source[key];
|
|
2896
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
2897
|
+
return Math.max(Math.trunc(value), 0);
|
|
2898
|
+
}
|
|
2899
|
+
if (typeof value === "string") {
|
|
2900
|
+
const parsed = Number.parseInt(value, 10);
|
|
2901
|
+
if (Number.isFinite(parsed)) {
|
|
2902
|
+
return Math.max(parsed, 0);
|
|
2903
|
+
}
|
|
2904
|
+
}
|
|
2905
|
+
}
|
|
2906
|
+
return 0;
|
|
2907
|
+
}
|
|
2835
2908
|
exports.__loadstrikeTestExports = {
|
|
2836
2909
|
GrafanaLokiReportingSink,
|
|
2837
2910
|
InfluxDbReportingSink,
|
package/dist/esm/runtime.js
CHANGED
|
@@ -7,7 +7,7 @@ import { DistributedClusterAgent, DistributedClusterCoordinator } from "./cluste
|
|
|
7
7
|
import { CorrelationStoreConfiguration, CrossPlatformTrackingRuntime, RedisCorrelationStore, RedisCorrelationStoreOptions, TrackingFieldSelector } from "./correlation.js";
|
|
8
8
|
import { EndpointAdapterFactory, LOADSTRIKE_TRACE_ID_TRACKING_FIELD } from "./transports.js";
|
|
9
9
|
import { buildDotnetCsvReport, buildDotnetHtmlReport, buildDotnetMarkdownReport, buildDotnetTxtReport } from "./reporting.js";
|
|
10
|
-
import { PortalReportingSink } from "./sinks.js";
|
|
10
|
+
import { PortalReportingSink, cloneReportingSinkForRun } from "./sinks.js";
|
|
11
11
|
export const LoadStrikeNodeType = {
|
|
12
12
|
SingleNode: "SingleNode",
|
|
13
13
|
Coordinator: "Coordinator",
|
|
@@ -2468,7 +2468,7 @@ export class LoadStrikeRunner {
|
|
|
2468
2468
|
const scenarioAccumulators = new Map();
|
|
2469
2469
|
const scenarioDurationsMs = new Map();
|
|
2470
2470
|
const stepStats = new Map();
|
|
2471
|
-
const sinks = this.options.reportingSinks ?? [];
|
|
2471
|
+
const sinks = (this.options.reportingSinks ?? []).map((sink) => cloneReportingSinkForRun(sink));
|
|
2472
2472
|
const sinkStates = sinks.map((sink, index) => ({
|
|
2473
2473
|
sink,
|
|
2474
2474
|
disabled: false,
|
|
@@ -2502,6 +2502,7 @@ export class LoadStrikeRunner {
|
|
|
2502
2502
|
let sinksStopped = false;
|
|
2503
2503
|
let realtimeTimer = null;
|
|
2504
2504
|
let realtimeInFlight = false;
|
|
2505
|
+
let realtimeCurrent = null;
|
|
2505
2506
|
let licenseClient = null;
|
|
2506
2507
|
let licensePayload = null;
|
|
2507
2508
|
let licenseSession = null;
|
|
@@ -2597,9 +2598,38 @@ export class LoadStrikeRunner {
|
|
|
2597
2598
|
}
|
|
2598
2599
|
};
|
|
2599
2600
|
const reportingIntervalMs = Math.max(Math.trunc((this.options.reportingIntervalSeconds ?? 5) * 1000), 1);
|
|
2601
|
+
const triggerRealtimeSnapshot = () => {
|
|
2602
|
+
if (realtimeCurrent) {
|
|
2603
|
+
return;
|
|
2604
|
+
}
|
|
2605
|
+
const current = emitRealtimeSnapshot()
|
|
2606
|
+
.catch((error) => {
|
|
2607
|
+
sinkErrors.push({
|
|
2608
|
+
sinkName: "realtime",
|
|
2609
|
+
phase: "realtime",
|
|
2610
|
+
message: String(error ?? "realtime reporting failed"),
|
|
2611
|
+
attempts: 1
|
|
2612
|
+
});
|
|
2613
|
+
})
|
|
2614
|
+
.finally(() => {
|
|
2615
|
+
if (realtimeCurrent === current) {
|
|
2616
|
+
realtimeCurrent = null;
|
|
2617
|
+
}
|
|
2618
|
+
});
|
|
2619
|
+
realtimeCurrent = current;
|
|
2620
|
+
};
|
|
2621
|
+
const stopRealtimeReporting = async () => {
|
|
2622
|
+
if (realtimeTimer) {
|
|
2623
|
+
clearInterval(realtimeTimer);
|
|
2624
|
+
realtimeTimer = null;
|
|
2625
|
+
}
|
|
2626
|
+
if (realtimeCurrent) {
|
|
2627
|
+
await realtimeCurrent;
|
|
2628
|
+
}
|
|
2629
|
+
};
|
|
2600
2630
|
if (sinkStates.length > 0 || toBoolean(this.options.displayConsoleMetrics, true)) {
|
|
2601
2631
|
realtimeTimer = setInterval(() => {
|
|
2602
|
-
|
|
2632
|
+
triggerRealtimeSnapshot();
|
|
2603
2633
|
}, reportingIntervalMs);
|
|
2604
2634
|
if (typeof realtimeTimer.unref === "function") {
|
|
2605
2635
|
realtimeTimer.unref();
|
|
@@ -2687,6 +2717,7 @@ export class LoadStrikeRunner {
|
|
|
2687
2717
|
failedCorrelationRows: buildDetailedFailedCorrelationRows()
|
|
2688
2718
|
};
|
|
2689
2719
|
}
|
|
2720
|
+
await stopRealtimeReporting();
|
|
2690
2721
|
result.pluginsData = mergePluginData(result.pluginsData, await this.collectPluginData(plugins, attachRunResultAliases(result), pluginLifecycleErrors));
|
|
2691
2722
|
const finalizedResult = attachRunResultAliases(result);
|
|
2692
2723
|
finalizedResult.logFiles = mergeStringArrays(finalizedResult.logFiles, loggerSetup.logFiles);
|
|
@@ -2701,9 +2732,7 @@ export class LoadStrikeRunner {
|
|
|
2701
2732
|
return finalizedResult;
|
|
2702
2733
|
}
|
|
2703
2734
|
finally {
|
|
2704
|
-
|
|
2705
|
-
clearInterval(realtimeTimer);
|
|
2706
|
-
}
|
|
2735
|
+
await stopRealtimeReporting();
|
|
2707
2736
|
if (!sinksStopped) {
|
|
2708
2737
|
await this.stopSinks(sinkStates, sinkRetryCount, sinkRetryBackoffMs, sinkErrors);
|
|
2709
2738
|
}
|
|
@@ -3024,14 +3053,14 @@ export class LoadStrikeRunner {
|
|
|
3024
3053
|
}
|
|
3025
3054
|
catch (error) {
|
|
3026
3055
|
if (attempts > retryCount) {
|
|
3056
|
+
sinkErrors.push({
|
|
3057
|
+
sinkName: state.name,
|
|
3058
|
+
phase,
|
|
3059
|
+
message: String(error ?? "sink action failed"),
|
|
3060
|
+
attempts
|
|
3061
|
+
});
|
|
3027
3062
|
if (disableOnFailure) {
|
|
3028
3063
|
state.disabled = true;
|
|
3029
|
-
sinkErrors.push({
|
|
3030
|
-
sinkName: state.name,
|
|
3031
|
-
phase,
|
|
3032
|
-
message: String(error ?? "sink action failed"),
|
|
3033
|
-
attempts
|
|
3034
|
-
});
|
|
3035
3064
|
}
|
|
3036
3065
|
return;
|
|
3037
3066
|
}
|
|
@@ -3631,7 +3660,7 @@ function normalizeReply(value) {
|
|
|
3631
3660
|
statusCode: normalizeStatusCode(value?.statusCode, Boolean(value?.isSuccess)),
|
|
3632
3661
|
message: value?.message ? String(value.message) : "",
|
|
3633
3662
|
sizeBytes: Number.isFinite(value?.sizeBytes) ? Number(value.sizeBytes) : 0,
|
|
3634
|
-
customLatencyMs: Number.isFinite(value?.customLatencyMs) ? Number(value.customLatencyMs) :
|
|
3663
|
+
customLatencyMs: Number.isFinite(value?.customLatencyMs) ? Number(value.customLatencyMs) : -1,
|
|
3635
3664
|
payload: value?.payload
|
|
3636
3665
|
};
|
|
3637
3666
|
}
|
|
@@ -3643,7 +3672,7 @@ function attachReplyProjection(reply) {
|
|
|
3643
3672
|
SizeBytes: "sizeBytes",
|
|
3644
3673
|
Payload: "payload"
|
|
3645
3674
|
});
|
|
3646
|
-
defineAliasProperty(reply, "CustomLatencyMs", () => Number.isFinite(reply.customLatencyMs) ? Number(reply.customLatencyMs) :
|
|
3675
|
+
defineAliasProperty(reply, "CustomLatencyMs", () => Number.isFinite(reply.customLatencyMs) ? Number(reply.customLatencyMs) : -1);
|
|
3647
3676
|
const asReply = () => attachReplyProjection({
|
|
3648
3677
|
isSuccess: reply.isSuccess,
|
|
3649
3678
|
statusCode: reply.statusCode,
|
|
@@ -4541,15 +4570,15 @@ function attachRunResultAliases(result) {
|
|
|
4541
4570
|
defineAliasProperty(projected, "Duration", () => projected.durationMs);
|
|
4542
4571
|
return projected;
|
|
4543
4572
|
}
|
|
4544
|
-
function resolveResponseCustomLatency(customLatencyMs,
|
|
4573
|
+
function resolveResponseCustomLatency(customLatencyMs, _usesOverloadDefaults) {
|
|
4545
4574
|
if (!Number.isFinite(customLatencyMs)) {
|
|
4546
|
-
return
|
|
4575
|
+
return -1;
|
|
4547
4576
|
}
|
|
4548
4577
|
return Number(customLatencyMs);
|
|
4549
4578
|
}
|
|
4550
4579
|
function resolveRecordedLatency(customLatencyMs, observedLatencyMs) {
|
|
4551
4580
|
if (!Number.isFinite(customLatencyMs)) {
|
|
4552
|
-
return 0;
|
|
4581
|
+
return Math.max(observedLatencyMs, 0);
|
|
4553
4582
|
}
|
|
4554
4583
|
const resolved = Number(customLatencyMs);
|
|
4555
4584
|
if (resolved >= 0) {
|
package/dist/esm/sinks.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { LoadStrikePluginData as LoadStrikePluginDataModel, LoadStrikePluginDataTable as LoadStrikePluginDataTableModel } from "./runtime.js";
|
|
2
|
-
import { createHash } from "node:crypto";
|
|
2
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
3
3
|
import { Pool } from "pg";
|
|
4
4
|
const DEFAULT_INFLUX_CONFIGURATION_SECTION_PATH = "LoadStrike:ReportingSinks:InfluxDb";
|
|
5
5
|
const DEFAULT_GRAFANA_LOKI_CONFIGURATION_SECTION_PATH = "LoadStrike:ReportingSinks:GrafanaLoki";
|
|
@@ -383,10 +383,14 @@ export class PortalReportingSink {
|
|
|
383
383
|
this.session = null;
|
|
384
384
|
this.runToken = "";
|
|
385
385
|
this.ingestUrl = "";
|
|
386
|
+
this.optionsInput = { ...options };
|
|
386
387
|
const source = asRecord(options);
|
|
387
388
|
this.timeoutMs = resolveTimeoutMs(optionNumber(source, "timeoutSeconds", "TimeoutSeconds"), optionNumber(source, "timeoutMs", "TimeoutMs"));
|
|
388
389
|
this.fetchImpl = pickRecordValue(source, "fetchImpl", "FetchImpl") ?? fetch;
|
|
389
390
|
}
|
|
391
|
+
cloneForRun() {
|
|
392
|
+
return new PortalReportingSink(this.optionsInput);
|
|
393
|
+
}
|
|
390
394
|
init(context, _infraConfig) {
|
|
391
395
|
this.baseContext = cloneBaseContext(context);
|
|
392
396
|
}
|
|
@@ -399,7 +403,9 @@ export class PortalReportingSink {
|
|
|
399
403
|
if (!this.runToken || !this.ingestUrl) {
|
|
400
404
|
throw new Error("PortalReportingSink requires a managed portal reporting session.");
|
|
401
405
|
}
|
|
402
|
-
this.session = sinkSessionMetadataFromContext(this.getBaseContext(), session
|
|
406
|
+
this.session = sinkSessionMetadataFromContext(this.getBaseContext(), session, {
|
|
407
|
+
distinctRunIdFallback: true
|
|
408
|
+
});
|
|
403
409
|
}
|
|
404
410
|
Start(session) {
|
|
405
411
|
this.start(session);
|
|
@@ -448,7 +454,7 @@ export class PortalReportingSink {
|
|
|
448
454
|
if (!events.length) {
|
|
449
455
|
return;
|
|
450
456
|
}
|
|
451
|
-
await postWithTimeout(this.fetchImpl, this.ingestUrl, {
|
|
457
|
+
const responseBody = await postWithTimeout(this.fetchImpl, this.ingestUrl, {
|
|
452
458
|
method: "POST",
|
|
453
459
|
headers: { "Content-Type": "application/json" },
|
|
454
460
|
body: JSON.stringify({
|
|
@@ -456,8 +462,15 @@ export class PortalReportingSink {
|
|
|
456
462
|
events: events.map((event, index) => portalEventPayload(event, index))
|
|
457
463
|
})
|
|
458
464
|
}, this.timeoutMs, "PortalReportingSink");
|
|
465
|
+
validatePortalIngestResponse(responseBody);
|
|
459
466
|
}
|
|
460
467
|
}
|
|
468
|
+
export function cloneReportingSinkForRun(sink) {
|
|
469
|
+
if (sink instanceof PortalReportingSink) {
|
|
470
|
+
return sink.cloneForRun();
|
|
471
|
+
}
|
|
472
|
+
return sink;
|
|
473
|
+
}
|
|
461
474
|
export class InfluxDbReportingSink {
|
|
462
475
|
constructor(options = {}) {
|
|
463
476
|
this.sinkName = "influxdb";
|
|
@@ -2189,15 +2202,20 @@ function toOtelAnyValue(value) {
|
|
|
2189
2202
|
function toUnixSeconds(value) {
|
|
2190
2203
|
return value.getTime() / 1000;
|
|
2191
2204
|
}
|
|
2192
|
-
function sinkSessionMetadataFromContext(context, session) {
|
|
2205
|
+
function sinkSessionMetadataFromContext(context, session, options = {}) {
|
|
2193
2206
|
const nodeInfo = context.getNodeInfo?.() ?? context.nodeInfo;
|
|
2194
2207
|
const testInfo = context.testInfo;
|
|
2195
2208
|
const sessionStartedUtc = String(session?.startedUtc ?? session?.StartedUtc ?? "");
|
|
2196
2209
|
const contextStartedUtc = String(testInfo.createdUtc ?? testInfo.CreatedUtc ?? "");
|
|
2197
|
-
const
|
|
2210
|
+
const explicitRunId = String(session?.portalReportingRunId ?? session?.PortalReportingRunId ?? "").trim();
|
|
2211
|
+
const sessionId = String(testInfo.sessionId ?? "");
|
|
2212
|
+
const fallbackRunId = options.distinctRunIdFallback
|
|
2213
|
+
? buildDistinctSinkRunId(sessionId)
|
|
2214
|
+
: sessionId;
|
|
2215
|
+
const runId = explicitRunId || fallbackRunId;
|
|
2198
2216
|
return {
|
|
2199
|
-
runId: runId ||
|
|
2200
|
-
sessionId
|
|
2217
|
+
runId: runId || sessionId,
|
|
2218
|
+
sessionId,
|
|
2201
2219
|
testSuite: String(testInfo.testSuite ?? ""),
|
|
2202
2220
|
testName: String(testInfo.testName ?? ""),
|
|
2203
2221
|
clusterId: String(testInfo.clusterId ?? ""),
|
|
@@ -2206,6 +2224,14 @@ function sinkSessionMetadataFromContext(context, session) {
|
|
|
2206
2224
|
startedUtc: sessionStartedUtc || contextStartedUtc || new Date().toISOString()
|
|
2207
2225
|
};
|
|
2208
2226
|
}
|
|
2227
|
+
function buildDistinctSinkRunId(sessionId) {
|
|
2228
|
+
const sessionPart = String(sessionId ?? "")
|
|
2229
|
+
.replace(/[^A-Za-z0-9._-]+/g, "-")
|
|
2230
|
+
.replace(/^-+|-+$/g, "")
|
|
2231
|
+
.slice(0, 80);
|
|
2232
|
+
const uniquePart = randomBytes(8).toString("hex");
|
|
2233
|
+
return sessionPart ? `${sessionPart}-${uniquePart}` : uniquePart;
|
|
2234
|
+
}
|
|
2209
2235
|
function readInfluxOptionsFromConfig(infraConfig, configurationSectionPath) {
|
|
2210
2236
|
const section = resolveConfigSection(infraConfig, configurationSectionPath);
|
|
2211
2237
|
return {
|
|
@@ -2804,15 +2830,61 @@ async function postWithTimeout(fetchImpl, url, init, timeoutMs, sinkName) {
|
|
|
2804
2830
|
const timer = setTimeout(() => controller.abort(), Math.max(timeoutMs, 1));
|
|
2805
2831
|
try {
|
|
2806
2832
|
const response = await fetchImpl(url, { ...init, signal: controller.signal });
|
|
2833
|
+
const body = await readResponseBodyText(response);
|
|
2807
2834
|
if (!response.ok) {
|
|
2808
|
-
const body = await response.text();
|
|
2809
2835
|
throw new Error(`${sinkName} write failed with status ${response.status}: ${body}`);
|
|
2810
2836
|
}
|
|
2837
|
+
return body;
|
|
2811
2838
|
}
|
|
2812
2839
|
finally {
|
|
2813
2840
|
clearTimeout(timer);
|
|
2814
2841
|
}
|
|
2815
2842
|
}
|
|
2843
|
+
async function readResponseBodyText(response) {
|
|
2844
|
+
const candidate = response;
|
|
2845
|
+
if (typeof candidate.text !== "function") {
|
|
2846
|
+
return "";
|
|
2847
|
+
}
|
|
2848
|
+
return String(await candidate.text());
|
|
2849
|
+
}
|
|
2850
|
+
function validatePortalIngestResponse(responseBody) {
|
|
2851
|
+
const text = String(responseBody ?? "").trim();
|
|
2852
|
+
if (!text) {
|
|
2853
|
+
return;
|
|
2854
|
+
}
|
|
2855
|
+
let parsed;
|
|
2856
|
+
try {
|
|
2857
|
+
parsed = JSON.parse(text);
|
|
2858
|
+
}
|
|
2859
|
+
catch {
|
|
2860
|
+
return;
|
|
2861
|
+
}
|
|
2862
|
+
if (!isRecord(parsed)) {
|
|
2863
|
+
return;
|
|
2864
|
+
}
|
|
2865
|
+
const rejected = readPortalIngestCount(parsed, "rejectedCount", "RejectedCount", "rejected");
|
|
2866
|
+
if (rejected <= 0) {
|
|
2867
|
+
return;
|
|
2868
|
+
}
|
|
2869
|
+
const accepted = readPortalIngestCount(parsed, "acceptedCount", "AcceptedCount", "accepted");
|
|
2870
|
+
const duplicate = readPortalIngestCount(parsed, "duplicateCount", "DuplicateCount", "duplicate");
|
|
2871
|
+
throw new Error(`PortalReportingSink ingest rejected ${rejected} reporting event(s). Accepted ${accepted}, duplicate ${duplicate}.`);
|
|
2872
|
+
}
|
|
2873
|
+
function readPortalIngestCount(source, ...keys) {
|
|
2874
|
+
for (const key of keys) {
|
|
2875
|
+
const value = source[key];
|
|
2876
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
2877
|
+
return Math.max(Math.trunc(value), 0);
|
|
2878
|
+
}
|
|
2879
|
+
if (typeof value === "string") {
|
|
2880
|
+
const parsed = Number.parseInt(value, 10);
|
|
2881
|
+
if (Number.isFinite(parsed)) {
|
|
2882
|
+
return Math.max(parsed, 0);
|
|
2883
|
+
}
|
|
2884
|
+
}
|
|
2885
|
+
}
|
|
2886
|
+
return 0;
|
|
2887
|
+
}
|
|
2816
2888
|
export const __loadstrikeTestExports = {
|
|
2817
2889
|
GrafanaLokiReportingSink,
|
|
2818
2890
|
InfluxDbReportingSink,
|
package/dist/types/sinks.d.ts
CHANGED
|
@@ -450,6 +450,7 @@ export declare class PortalReportingSink implements LoadStrikeReportingSink {
|
|
|
450
450
|
readonly SinkName = "portal";
|
|
451
451
|
readonly licenseFeature = "extensions.reporting_sinks.portal";
|
|
452
452
|
readonly LicenseFeature = "extensions.reporting_sinks.portal";
|
|
453
|
+
private readonly optionsInput;
|
|
453
454
|
private readonly fetchImpl;
|
|
454
455
|
private readonly timeoutMs;
|
|
455
456
|
private baseContext;
|
|
@@ -457,6 +458,7 @@ export declare class PortalReportingSink implements LoadStrikeReportingSink {
|
|
|
457
458
|
private runToken;
|
|
458
459
|
private ingestUrl;
|
|
459
460
|
constructor(options?: PortalReportingSinkOptionsInput);
|
|
461
|
+
cloneForRun(): PortalReportingSink;
|
|
460
462
|
init(context: LoadStrikeBaseContext, _infraConfig: Record<string, unknown>): void;
|
|
461
463
|
Init(context: LoadStrikeBaseContext, infraConfig: Record<string, unknown>): void;
|
|
462
464
|
start(session: LoadStrikeSessionStartInfo): void;
|
|
@@ -474,6 +476,7 @@ export declare class PortalReportingSink implements LoadStrikeReportingSink {
|
|
|
474
476
|
private getSession;
|
|
475
477
|
private persistEvents;
|
|
476
478
|
}
|
|
479
|
+
export declare function cloneReportingSinkForRun(sink: LoadStrikeReportingSink): LoadStrikeReportingSink;
|
|
477
480
|
export declare class InfluxDbReportingSink implements LoadStrikeReportingSink {
|
|
478
481
|
readonly sinkName = "influxdb";
|
|
479
482
|
readonly SinkName = "influxdb";
|
|
@@ -644,7 +647,9 @@ declare function createFinalStatsEvents(session: SinkSessionMetadata, stats: Loa
|
|
|
644
647
|
declare function createRunResultEvents(session: SinkSessionMetadata, result: LoadStrikeRunResult): ReportingSinkEvent[];
|
|
645
648
|
declare function createReportingEvent(session: SinkSessionMetadata, occurredUtc: Date, eventType: string, scenarioName: string | null, stepName: string | null, tags: Record<string, string>, fields: Record<string, unknown>): ReportingSinkEvent;
|
|
646
649
|
declare function toOtelAnyValue(value: unknown): Record<string, unknown>;
|
|
647
|
-
declare function sinkSessionMetadataFromContext(context: LoadStrikeBaseContext, session?: LoadStrikeSessionStartInfo
|
|
650
|
+
declare function sinkSessionMetadataFromContext(context: LoadStrikeBaseContext, session?: LoadStrikeSessionStartInfo, options?: {
|
|
651
|
+
distinctRunIdFallback?: boolean;
|
|
652
|
+
}): SinkSessionMetadata;
|
|
648
653
|
declare function mergeInfluxOptions(target: InfluxDbResolvedOptions, source: Partial<InfluxDbResolvedOptions>): void;
|
|
649
654
|
declare function mergeGrafanaLokiOptions(target: GrafanaLokiResolvedOptions, source: Partial<GrafanaLokiResolvedOptions>): void;
|
|
650
655
|
declare function mergeTimescaleDbOptions(target: TimescaleDbResolvedOptions, source: Partial<TimescaleDbResolvedOptions>): void;
|
|
@@ -675,7 +680,7 @@ declare function cloneSessionStartInfo(session: LoadStrikeSessionStartInfo): Loa
|
|
|
675
680
|
declare function cloneMetricStats(metrics: LoadStrikeMetricStats): LoadStrikeMetricStats;
|
|
676
681
|
declare function cloneScenarioStats(value: LoadStrikeScenarioStats): LoadStrikeScenarioStats;
|
|
677
682
|
declare function cloneNodeStats(result: LoadStrikeNodeStats): LoadStrikeNodeStats;
|
|
678
|
-
declare function postWithTimeout(fetchImpl: SinkFetch, url: string, init: RequestInit, timeoutMs: number, sinkName: string): Promise<
|
|
683
|
+
declare function postWithTimeout(fetchImpl: SinkFetch, url: string, init: RequestInit, timeoutMs: number, sinkName: string): Promise<string>;
|
|
679
684
|
export declare const __loadstrikeTestExports: {
|
|
680
685
|
GrafanaLokiReportingSink: typeof GrafanaLokiReportingSink;
|
|
681
686
|
InfluxDbReportingSink: typeof InfluxDbReportingSink;
|
package/package.json
CHANGED