@loadstrike/loadstrike-sdk 1.0.31601 → 1.0.33601

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.
Files changed (33) hide show
  1. package/README.md +14 -2
  2. package/dist/cjs/internal/prometheus-remote-write.js +37 -0
  3. package/dist/cjs/internal/reporting-sink-http-error.js +17 -0
  4. package/dist/cjs/internal/vendor-metric-payloads.js +390 -0
  5. package/dist/cjs/iteration-observations.js +24 -8
  6. package/dist/cjs/local.js +48 -63
  7. package/dist/cjs/reporting-containment.js +242 -0
  8. package/dist/cjs/runtime.js +83 -3
  9. package/dist/cjs/sinks.js +1337 -38
  10. package/dist/cjs/transports.js +1339 -151
  11. package/dist/esm/internal/prometheus-remote-write.js +31 -0
  12. package/dist/esm/internal/reporting-sink-http-error.js +13 -0
  13. package/dist/esm/internal/vendor-metric-payloads.js +382 -0
  14. package/dist/esm/iteration-observations.js +24 -8
  15. package/dist/esm/local.js +49 -64
  16. package/dist/esm/reporting-containment.js +238 -0
  17. package/dist/esm/runtime.js +85 -5
  18. package/dist/esm/sinks.js +1334 -35
  19. package/dist/esm/transports.js +1335 -151
  20. package/dist/types/contracts.d.ts +1 -0
  21. package/dist/types/index.d.ts +1 -1
  22. package/dist/types/internal/prometheus-remote-write.d.ts +2 -0
  23. package/dist/types/internal/reporting-sink-http-error.d.ts +6 -0
  24. package/dist/types/internal/vendor-metric-payloads.d.ts +48 -0
  25. package/dist/types/local.d.ts +0 -6
  26. package/dist/types/reporting-containment.d.ts +2 -0
  27. package/dist/types/runtime.d.ts +1 -0
  28. package/dist/types/sinks.d.ts +134 -17
  29. package/dist/types/transports.d.ts +2 -0
  30. package/package.json +9 -3
  31. package/dist/cjs/internal-build.js +0 -4
  32. package/dist/esm/internal-build.js +0 -1
  33. package/dist/types/internal-build.d.ts +0 -1
package/dist/esm/local.js CHANGED
@@ -15,10 +15,9 @@ import * as fs from "node:fs";
15
15
  import * as childProcess from "node:child_process";
16
16
  import { createHash, createVerify, randomUUID } from "node:crypto";
17
17
  import { CorrelationStoreConfiguration, CrossPlatformTrackingRuntime, RedisCorrelationStoreOptions, RedisCorrelationStore, TrackingFieldSelector } from "./correlation.js";
18
- import { EndpointAdapterFactory, LOADSTRIKE_TRACE_ID_TRACKING_FIELD } from "./transports.js";
19
- import { BLACKBOX_LICENSING_API_BASE_URL_TOKEN } from "./internal-build.js";
18
+ import { EndpointAdapterFactory, LOADSTRIKE_TRACE_ID_TRACKING_FIELD, validateNativeEndpointExecutionSupport } from "./transports.js";
19
+ import { assertNoUnsupportedReportingInfraConfig, assertNoUnsupportedReportingSinkGraph } from "./reporting-containment.js";
20
20
  const DEFAULT_LICENSING_API_BASE_URL = "https://licensing.loadstrike.com";
21
- let developmentLicensingApiBaseUrlOverride;
22
21
  const BUILT_IN_WORKER_PLUGIN_NAMES = new Set([
23
22
  "loadstrike failed responses",
24
23
  "loadstrike correlation"
@@ -72,7 +71,7 @@ export class LoadStrikeLocalClient {
72
71
  _LoadStrikeLocalClient_signingKeyCache.set(this, new Map());
73
72
  _LoadStrikeLocalClient_heartbeatDrains.set(this, new WeakMap());
74
73
  assertNoDisableLicenseEnforcementOption(options, "LoadStrikeLocalClient");
75
- __classPrivateFieldSet(this, _LoadStrikeLocalClient_licensingApiBaseUrl, resolveLicensingApiBaseUrl(), "f");
74
+ __classPrivateFieldSet(this, _LoadStrikeLocalClient_licensingApiBaseUrl, DEFAULT_LICENSING_API_BASE_URL, "f");
76
75
  this.licenseValidationTimeoutMs = normalizeTimeoutMs(options.licenseValidationTimeoutMs);
77
76
  }
78
77
  portalReportingIngestUrl() {
@@ -80,6 +79,7 @@ export class LoadStrikeLocalClient {
80
79
  }
81
80
  async run(request) {
82
81
  const sanitized = sanitizeRequest(request);
82
+ validateContainmentRequest(sanitized);
83
83
  const licenseSession = await this.acquireLicenseLease(sanitized);
84
84
  try {
85
85
  const createdUtc = new Date();
@@ -534,66 +534,27 @@ function sanitizeRequest(request) {
534
534
  RunArgs: runArgs
535
535
  };
536
536
  }
537
- function normalizeLicensingApiBaseUrl(value) {
538
- const normalized = (value ?? "").trim();
539
- return normalized || DEFAULT_LICENSING_API_BASE_URL;
540
- }
541
- function resolveLicensingApiBaseUrl() {
542
- return normalizeLicensingApiBaseUrl(developmentLicensingApiBaseUrlOverride ?? resolveInternalBlackBoxLicensingApiBaseUrl());
543
- }
544
- function setDevelopmentLicensingApiBaseUrlOverride(value) {
545
- if (value != null && value.trim()) {
546
- assertInternalTestHarnessLicensingOverride();
547
- const normalized = normalizeLicensingApiBaseUrl(value);
548
- if (!isLoopbackHttpBaseUrl(normalized)) {
549
- throw new TypeError("Internal development licensing API overrides must use a loopback http URL.");
537
+ function validateContainmentRequest(request) {
538
+ const scenarios = Array.isArray(request.Scenarios) ? request.Scenarios : [];
539
+ for (const scenarioValue of scenarios) {
540
+ const scenario = asRecord(scenarioValue);
541
+ const tracking = asRecord(pickValue(scenario, "Tracking", "tracking"));
542
+ for (const field of ["Source", "Destination"]) {
543
+ const endpoint = asRecord(pickValue(tracking, field, field.toLowerCase()));
544
+ if (Object.keys(endpoint).length > 0) {
545
+ validateNativeEndpointExecutionSupport(endpoint);
546
+ }
550
547
  }
551
- developmentLicensingApiBaseUrlOverride = normalized;
552
- return;
553
- }
554
- if (value != null) {
555
- assertInternalTestHarnessLicensingOverride();
556
- }
557
- developmentLicensingApiBaseUrlOverride = undefined;
558
- }
559
- function assertInternalTestHarnessLicensingOverride() {
560
- if (isInternalTestHarnessCall()) {
561
- return;
562
- }
563
- throw new TypeError("Internal development licensing API override is available only to LoadStrike SDK tests.");
564
- }
565
- function isInternalTestHarnessCall() {
566
- const stack = String(new Error().stack ?? "").replace(/\\/g, "/").toLowerCase();
567
- return stack.includes("/sdk/ts/tests/");
568
- }
569
- function isLoopbackHttpBaseUrl(value) {
570
- let parsed;
571
- try {
572
- parsed = new URL(value);
573
- }
574
- catch {
575
- return false;
576
- }
577
- if (parsed.protocol !== "http:") {
578
- return false;
579
548
  }
580
- const host = parsed.hostname.trim().toLowerCase();
581
- return host === "localhost" || host === "127.0.0.1" || host === "[::1]" || host === "::1" || host.endsWith(".localhost");
582
- }
583
- function resolveInternalBlackBoxLicensingApiBaseUrl() {
584
- const compiledToken = String(BLACKBOX_LICENSING_API_BASE_URL_TOKEN ?? "").trim();
585
- if (!compiledToken) {
586
- return undefined;
587
- }
588
- const envToken = String(process.env.LOADSTRIKE_INTERNAL_BLACKBOX_API_BASE_URL_TOKEN ?? "").trim();
589
- if (!envToken || envToken !== compiledToken) {
590
- return undefined;
591
- }
592
- const candidate = normalizeLicensingApiBaseUrl(process.env.LOADSTRIKE_INTERNAL_BLACKBOX_API_BASE_URL);
593
- if (!isLoopbackHttpBaseUrl(candidate)) {
594
- return undefined;
549
+ const context = asRecord(request.Context);
550
+ const reportingSinks = asList(pickValue(context, "ReportingSinks", "reportingSinks"));
551
+ for (const [key, value] of Object.entries(context)) {
552
+ const normalizedKey = key.trim().toLowerCase().replace(/[^a-z0-9]+/g, "");
553
+ if (normalizedKey === "infraconfig") {
554
+ assertNoUnsupportedReportingInfraConfig(value, reportingSinks);
555
+ }
595
556
  }
596
- return candidate;
557
+ assertNoUnsupportedReportingSinkGraph(reportingSinks);
597
558
  }
598
559
  function normalizeTimeoutMs(value) {
599
560
  if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
@@ -1301,6 +1262,20 @@ function mapEndpointSpec(spec, useLoadStrikeTraceIdHeader = false) {
1301
1262
  redisStreams: asRecord(pickValue(spec, "RedisStreams", "redisStreams")),
1302
1263
  azureEventHubs: asRecord(pickValue(spec, "AzureEventHubs", "azureEventHubs")),
1303
1264
  pushDiffusion: asRecord(pickValue(spec, "PushDiffusion", "pushDiffusion")),
1265
+ grpc: mapEndpointProtocolOptions(spec, ["Grpc", "grpc"], [
1266
+ "Target", "target", "ServiceName", "serviceName", "MethodName", "methodName",
1267
+ "MethodType", "methodType", "Deadline", "deadline", "DeadlineSeconds", "deadlineSeconds",
1268
+ "DeadlineMs", "deadlineMs", "Metadata", "metadata", "Produce", "produce", "Consume", "consume",
1269
+ "ProduceAsync", "produceAsync", "ConsumeAsync", "consumeAsync", "ConnectionMetadata", "connectionMetadata",
1270
+ "NativeClient", "nativeClient"
1271
+ ]),
1272
+ webSocket: mapEndpointProtocolOptions(spec, ["WebSocket", "webSocket"], [
1273
+ "Url", "url", "Subprotocols", "subprotocols", "ConnectTimeout", "connectTimeout",
1274
+ "ConnectTimeoutSeconds", "connectTimeoutSeconds", "ConnectTimeoutMs", "connectTimeoutMs",
1275
+ "CloseTimeout", "closeTimeout", "CloseTimeoutSeconds", "closeTimeoutSeconds", "CloseTimeoutMs", "closeTimeoutMs",
1276
+ "Produce", "produce", "Consume", "consume", "ProduceAsync", "produceAsync", "ConsumeAsync", "consumeAsync",
1277
+ "ConnectionMetadata", "connectionMetadata", "NativeClient", "nativeClient"
1278
+ ]),
1304
1279
  delegate: (typeof delegateProduce === "function"
1305
1280
  || typeof delegateConsume === "function"
1306
1281
  || typeof delegateProduceAsync === "function"
@@ -1323,6 +1298,19 @@ function mapEndpointSpec(spec, useLoadStrikeTraceIdHeader = false) {
1323
1298
  : undefined
1324
1299
  };
1325
1300
  }
1301
+ function mapEndpointProtocolOptions(spec, nestedKeys, flatKeys) {
1302
+ const nested = asRecord(pickValue(spec, ...nestedKeys));
1303
+ if (Object.keys(nested).length > 0) {
1304
+ return nested;
1305
+ }
1306
+ const options = {};
1307
+ for (const key of flatKeys) {
1308
+ if (Object.prototype.hasOwnProperty.call(spec, key)) {
1309
+ options[key] = spec[key];
1310
+ }
1311
+ }
1312
+ return options;
1313
+ }
1326
1314
  function normalizePayload(payload, endpoint, index) {
1327
1315
  const resolvedBody = payload?.body ?? endpoint.messagePayload;
1328
1316
  const normalized = {
@@ -2279,8 +2267,6 @@ export const __loadstrikeTestExports = {
2279
2267
  mapEnvironmentClassification,
2280
2268
  mapEndpointSpec,
2281
2269
  normalizeEndpointPayloadValue,
2282
- normalizeLicensingApiBaseUrl,
2283
- resolveLicensingApiBaseUrl,
2284
2270
  normalizeNodeType,
2285
2271
  normalizePayload,
2286
2272
  normalizeStringArray,
@@ -2299,7 +2285,6 @@ export const __loadstrikeTestExports = {
2299
2285
  sanitizeLooseJson,
2300
2286
  sanitizeRequest,
2301
2287
  setJsonPathValue,
2302
- setDevelopmentLicensingApiBaseUrlOverride,
2303
2288
  sleep,
2304
2289
  stringOrDefault,
2305
2290
  toBoolean,
@@ -0,0 +1,238 @@
1
+ const UNBOUND_EXPANDED_SECTIONS = [
2
+ ["PrometheusRemoteWrite", "PrometheusRemoteWriteReportingSink"],
3
+ ["CloudWatch", "CloudWatchReportingSink"],
4
+ ["Dynatrace", "DynatraceReportingSink"],
5
+ ["NewRelic", "NewRelicReportingSink"],
6
+ ["Elasticsearch", "ElasticsearchReportingSink"],
7
+ ["OpenSearch", "OpenSearchReportingSink"],
8
+ ["Kafka", "KafkaReportingSink"],
9
+ ["StatsD", "StatsDReportingSink"],
10
+ ["DogStatsD", "DogStatsDReportingSink"],
11
+ ["Netdata", "NetdataStatsDReportingSink"],
12
+ ["Jsonl", "JsonlFileReportingSink"],
13
+ ["Webhook", "GenericWebhookReportingSink"]
14
+ ];
15
+ export function assertNoUnsupportedReportingInfraConfig(infraConfig, sinks = []) {
16
+ if (!isRecord(infraConfig)) {
17
+ return;
18
+ }
19
+ const selectedSections = selectedExpandedReportingSections(sinks);
20
+ for (const [sectionName, sinkType] of UNBOUND_EXPANDED_SECTIONS) {
21
+ if (selectedSections.has(sectionName)
22
+ && hasNonEmptyReportingSectionAlias(infraConfig, sectionName)) {
23
+ throw new Error(`Configuration section 'LoadStrike:ReportingSinks:${sectionName}' is not bound automatically by the TypeScript SDK. `
24
+ + `Construct ${sinkType} explicitly and pass its options directly.`);
25
+ }
26
+ }
27
+ }
28
+ function selectedExpandedReportingSections(sinks) {
29
+ const selected = new Set();
30
+ const visited = new Set();
31
+ const inspect = (sink) => {
32
+ if (!isRecord(sink) || visited.has(sink)) {
33
+ return;
34
+ }
35
+ visited.add(sink);
36
+ const identities = [
37
+ sink.licenseFeature,
38
+ sink.LicenseFeature,
39
+ sink.feature,
40
+ sink.Feature
41
+ ];
42
+ if (!hasReportingSinkLifecycle(sink)) {
43
+ identities.push(sink.kind, sink.Kind);
44
+ }
45
+ const normalizedIdentities = identities
46
+ .map(normalizeExpandedSinkIdentity)
47
+ .filter(Boolean);
48
+ for (const identity of normalizedIdentities) {
49
+ const sectionName = EXPANDED_SECTION_BY_IDENTITY.get(identity);
50
+ if (sectionName) {
51
+ selected.add(sectionName);
52
+ }
53
+ }
54
+ const children = Array.isArray(sink.sinks)
55
+ ? sink.sinks
56
+ : Array.isArray(sink.Sinks)
57
+ ? sink.Sinks
58
+ : [];
59
+ for (const child of children) {
60
+ inspect(child);
61
+ }
62
+ };
63
+ for (const sink of sinks) {
64
+ inspect(sink);
65
+ }
66
+ return selected;
67
+ }
68
+ const EXPANDED_SECTION_BY_IDENTITY = new Map([
69
+ ["prometheusremotewrite", "PrometheusRemoteWrite"],
70
+ ["cloudwatch", "CloudWatch"],
71
+ ["dynatrace", "Dynatrace"],
72
+ ["elasticsearch", "Elasticsearch"],
73
+ ["opensearch", "OpenSearch"],
74
+ ["newrelic", "NewRelic"],
75
+ ["webhook", "Webhook"],
76
+ ["genericwebhook", "Webhook"],
77
+ ["kafka", "Kafka"],
78
+ ["statsd", "StatsD"],
79
+ ["dogstatsd", "DogStatsD"],
80
+ ["netdata", "Netdata"],
81
+ ["netdatastatsd", "Netdata"],
82
+ ["jsonl", "Jsonl"],
83
+ ["jsonlfile", "Jsonl"]
84
+ ]);
85
+ function normalizeExpandedSinkIdentity(value) {
86
+ let normalized = normalizeIdentityToken(value);
87
+ const featurePrefix = "extensionsreportingsinks";
88
+ if (normalized.startsWith(featurePrefix)) {
89
+ normalized = normalized.slice(featurePrefix.length);
90
+ }
91
+ const classSuffix = "reportingsink";
92
+ if (normalized.endsWith(classSuffix)) {
93
+ normalized = normalized.slice(0, -classSuffix.length);
94
+ }
95
+ return normalized;
96
+ }
97
+ export function assertNoUnsupportedReportingSinkGraph(sinks) {
98
+ const visited = new Set();
99
+ const active = new Set();
100
+ const inspect = (sink) => {
101
+ if (!isRecord(sink)) {
102
+ return;
103
+ }
104
+ if (active.has(sink)) {
105
+ throw new Error("Composite reporting sink graph cannot contain a cycle.");
106
+ }
107
+ if (visited.has(sink)) {
108
+ return;
109
+ }
110
+ active.add(sink);
111
+ const identityTokens = [sink.sinkName, sink.SinkName, sink.kind, sink.Kind]
112
+ .map(normalizeIdentityToken)
113
+ .filter(Boolean);
114
+ const featureTokens = [sink.licenseFeature, sink.LicenseFeature, sink.feature, sink.Feature]
115
+ .map((value) => String(value ?? "").trim().toLowerCase())
116
+ .filter(Boolean);
117
+ const hasLifecycle = hasReportingSinkLifecycle(sink);
118
+ const displayName = directVendorDisplayName(hasLifecycle ? [] : identityTokens, hasLifecycle ? [] : featureTokens);
119
+ if (displayName) {
120
+ throw unsupportedDirectVendorReportingError(displayName);
121
+ }
122
+ if (identityTokens.includes("composite") || identityTokens.includes("compositereportingsink")) {
123
+ const children = Array.isArray(sink.sinks)
124
+ ? sink.sinks
125
+ : Array.isArray(sink.Sinks)
126
+ ? sink.Sinks
127
+ : [];
128
+ for (const child of children) {
129
+ inspect(child);
130
+ }
131
+ }
132
+ active.delete(sink);
133
+ visited.add(sink);
134
+ };
135
+ for (const sink of sinks) {
136
+ inspect(sink);
137
+ }
138
+ }
139
+ function hasNonEmptyReportingSectionAlias(infraConfig, sectionName) {
140
+ const segments = ["LoadStrike", "ReportingSinks", sectionName];
141
+ const values = [
142
+ ...readDirectValuesCaseInsensitive(infraConfig, segments.join(":")),
143
+ ...readDirectValuesCaseInsensitive(infraConfig, segments.join(".")),
144
+ ...readNestedValuesCaseInsensitive(infraConfig, segments)
145
+ ];
146
+ return values.some(isNonEmptySection);
147
+ }
148
+ function readDirectValuesCaseInsensitive(record, key) {
149
+ const expected = key.toLowerCase();
150
+ return Object.entries(record)
151
+ .filter(([candidate]) => candidate.toLowerCase() === expected)
152
+ .map(([, value]) => value);
153
+ }
154
+ function readNestedValuesCaseInsensitive(record, segments) {
155
+ let current = [record];
156
+ for (const segment of segments) {
157
+ const next = [];
158
+ for (const candidate of current) {
159
+ if (isRecord(candidate)) {
160
+ next.push(...readDirectValuesCaseInsensitive(candidate, segment));
161
+ }
162
+ }
163
+ if (next.length === 0) {
164
+ return [];
165
+ }
166
+ current = next;
167
+ }
168
+ return current;
169
+ }
170
+ function isNonEmptySection(value) {
171
+ if (value == null) {
172
+ return false;
173
+ }
174
+ if (typeof value === "string") {
175
+ return value.trim().length > 0;
176
+ }
177
+ if (Array.isArray(value)) {
178
+ return value.length > 0;
179
+ }
180
+ if (isRecord(value)) {
181
+ return Object.keys(value).length > 0;
182
+ }
183
+ return true;
184
+ }
185
+ function directVendorDisplayName(identityTokens, featureTokens) {
186
+ if (identityTokens.includes("prometheusremotewrite")
187
+ || identityTokens.includes("prometheusremotewritereportingsink")
188
+ || featureTokens.includes("extensions.reporting_sinks.prometheus_remote_write")) {
189
+ return "Prometheus Remote Write";
190
+ }
191
+ if (identityTokens.includes("cloudwatch")
192
+ || identityTokens.includes("cloudwatchreportingsink")
193
+ || featureTokens.includes("extensions.reporting_sinks.cloudwatch")) {
194
+ return "CloudWatch";
195
+ }
196
+ if (identityTokens.includes("dynatrace")
197
+ || identityTokens.includes("dynatracereportingsink")
198
+ || featureTokens.includes("extensions.reporting_sinks.dynatrace")) {
199
+ return "Dynatrace";
200
+ }
201
+ if (identityTokens.includes("newrelic")
202
+ || identityTokens.includes("newrelicreportingsink")
203
+ || featureTokens.includes("extensions.reporting_sinks.new_relic")) {
204
+ return "New Relic";
205
+ }
206
+ return undefined;
207
+ }
208
+ function unsupportedDirectVendorReportingError(displayName) {
209
+ return new Error(`Direct ${displayName} reporting is not supported by the TypeScript SDK. `
210
+ + "Use GenericWebhookReportingSink through a converting gateway or a custom LoadStrikeReportingSink.");
211
+ }
212
+ function normalizeIdentityToken(value) {
213
+ return String(value ?? "")
214
+ .trim()
215
+ .toLowerCase()
216
+ .replace(/[^a-z0-9]+/g, "");
217
+ }
218
+ function hasReportingSinkLifecycle(sink) {
219
+ return [
220
+ "init",
221
+ "Init",
222
+ "start",
223
+ "Start",
224
+ "saveRealtimeStats",
225
+ "SaveRealtimeStats",
226
+ "saveRealtimeMetrics",
227
+ "SaveRealtimeMetrics",
228
+ "saveRunResult",
229
+ "SaveRunResult",
230
+ "saveIterationBatch",
231
+ "SaveIterationBatch",
232
+ "stop",
233
+ "Stop"
234
+ ].some((member) => typeof sink[member] === "function");
235
+ }
236
+ function isRecord(value) {
237
+ return value !== null && typeof value === "object" && !Array.isArray(value);
238
+ }
@@ -5,17 +5,20 @@ import { resolve } from "node:path";
5
5
  import { LoadStrikeLocalClient } from "./local.js";
6
6
  import { DistributedClusterAgent, DistributedClusterCoordinator, buildLoadEngineV2ReservedStepOtherIdentityKey, buildLoadEngineV2GlobalInvocationId, buildLoadEngineV2ScenarioIdentityKey, buildLoadEngineV2SchedulerIdentityKey, buildLoadEngineV2StatusIdentityKey, buildLoadEngineV2StepIdentityKey, resolveLoadEngineV2StepIdentityKey, buildLoadEngineV2Plan } from "./cluster.js";
7
7
  import { CorrelationStoreConfiguration, CrossPlatformTrackingRuntime, RedisCorrelationStore, RedisCorrelationStoreOptions, TrackingFieldSelector } from "./correlation.js";
8
- import { EndpointAdapterFactory, LOADSTRIKE_TRACE_ID_TRACKING_FIELD } from "./transports.js";
8
+ import { EndpointAdapterFactory, LOADSTRIKE_TRACE_ID_TRACKING_FIELD, validateNativeEndpointExecutionSupport } from "./transports.js";
9
9
  import { buildDotnetCsvReport, buildDotnetHtmlReport, buildDotnetMarkdownReport, buildDotnetTxtReport } from "./reporting.js";
10
10
  import { ReportHistoryCollector, ReportHistoryLifecycleCoordinator, ReportHistoryWorker, sanitizedExceptionClassChain } from "./report-history.js";
11
11
  import { distributedLocalReportInput, emptyLocalReportInput } from "./local-report-input.js";
12
- import { PortalReportingSink, cloneReportingSinkForRun } from "./sinks.js";
12
+ import { PortalReportingSink, PrometheusRemoteWriteReportingSink, cloneReportingSinkForRun } from "./sinks.js";
13
+ import { ReportingSinkHttpError } from "./internal/reporting-sink-http-error.js";
14
+ import { assertNoUnsupportedReportingSinkGraph } from "./reporting-containment.js";
13
15
  import { LoadEngineV2ExecutionBudget, buildLoadEngineV2TrafficMixSeedId, LoadStrikeHistogramV1, parseLoadEngineV2HistogramArtifact, serializeLoadEngineV2HistogramArtifact, classifyLoadEngineV2Arrival, loadEngineV2FixedArrivalCount, loadEngineV2FixedDeadlineNs, loadEngineV2LatenessToleranceNs, loadEngineV2TrafficMixLaneUnitCount, loadEngineV2TrafficMixOwnedUnits, planRampingInjectionDeadlines, planRampingConstantDeadlines, planRandomInjectionDeadlines, fnv1a32 } from "./load-engine-v2.js";
14
16
  import { DEFAULT_ITERATION_OBSERVATION_SETTINGS, IterationObservationReporter, createIterationObservation, createIterationStepObservation, utcNowNs, validateIterationObservationSettings } from "./iteration-observations.js";
15
17
  import { logIterationObservationFailure, logIterationObservationRecovery } from "./iteration-observation-diagnostics.js";
16
18
  import { normalizeSinkRetryBackoffMs, normalizeSinkRetryCount, sinkRetryDelayMs, waitForSinkRetryDelay } from "./sink-retry-policy.js";
17
19
  const LOAD_ENGINE_V2_SCHEDULER_DISTRIBUTIONS = Symbol("loadstrike.load-engine-v2.scheduler-distributions");
18
20
  const PORTAL_RUN_TOKEN_PROVIDER = Symbol.for("loadstrike.internal.portal-run-token-provider");
21
+ const REPORTING_INTERVAL_SECONDS = Symbol.for("loadstrike.internal.reporting-interval-seconds");
19
22
  export const LoadStrikeNodeType = {
20
23
  SingleNode: "SingleNode",
21
24
  Coordinator: "Coordinator",
@@ -739,6 +742,15 @@ class ScenarioStatsAccumulator {
739
742
  return attachScenarioStatsAliases(scenario);
740
743
  }
741
744
  }
745
+ function isRetryableReportingSinkError(sink, error) {
746
+ if (!(sink instanceof PrometheusRemoteWriteReportingSink)) {
747
+ return true;
748
+ }
749
+ if (!(error instanceof ReportingSinkHttpError)) {
750
+ return true;
751
+ }
752
+ return error.status === 429 || error.status < 400 || error.status >= 500;
753
+ }
742
754
  export class LoadStrikeResponse {
743
755
  /**
744
756
  * Creates a successful reply.
@@ -3467,6 +3479,8 @@ export class LoadStrikeRunner {
3467
3479
  return this.buildContext();
3468
3480
  }
3469
3481
  async run(args = []) {
3482
+ assertNoUnsupportedReportingSinkGraph(this.options.reportingSinks ?? []);
3483
+ assertNoUnsupportedNativeTrackingScenarios(this.scenarios);
3470
3484
  if (this.contextConfigurators.length) {
3471
3485
  return new LoadStrikeRunner(this.scenarios, this.buildContext().toRunnerOptions(), [], this.internalOptions).run(args);
3472
3486
  }
@@ -3478,7 +3492,7 @@ export class LoadStrikeRunner {
3478
3492
  const scenarioAccumulators = new Map();
3479
3493
  const scenarioDurationsMs = new Map();
3480
3494
  const stepStats = new Map();
3481
- const sinks = (this.options.reportingSinks ?? []).map((sink) => cloneReportingSinkForRun(sink));
3495
+ const sinks = (this.options.reportingSinks ?? []).map((sink) => cloneReportingSinkForRun(sink, this.options.infraConfig ?? {}));
3482
3496
  const sinkStates = sinks.map((sink, index) => ({
3483
3497
  sink,
3484
3498
  disabled: false,
@@ -3603,6 +3617,12 @@ export class LoadStrikeRunner {
3603
3617
  testInfo,
3604
3618
  getNodeInfo: () => attachNodeInfoAliases({ ...nodeInfo })
3605
3619
  };
3620
+ Object.defineProperty(baseContext, REPORTING_INTERVAL_SECONDS, {
3621
+ value: this.options.reportingIntervalSeconds ?? 5,
3622
+ enumerable: false,
3623
+ configurable: false,
3624
+ writable: false
3625
+ });
3606
3626
  attachBaseContextAliases(baseContext);
3607
3627
  const sinkSession = {
3608
3628
  startedUtc: createdUtc,
@@ -3657,6 +3677,7 @@ export class LoadStrikeRunner {
3657
3677
  name: state.name,
3658
3678
  iterationObservationPortalSink: Boolean(state.sink.iterationObservationPortalSink),
3659
3679
  iterationObservationShapeLimited: Boolean(state.sink.iterationObservationShapeLimited),
3680
+ retryableErrorClassifier: (error) => isRetryableReportingSinkError(state.sink, error),
3660
3681
  saveIterationBatch: resolveSinkSaveIterationBatch(state.sink),
3661
3682
  completeIterationObservationStream: resolveSinkCompleteIterationObservationStream(state.sink)
3662
3683
  }));
@@ -4291,7 +4312,9 @@ export class LoadStrikeRunner {
4291
4312
  return;
4292
4313
  }
4293
4314
  catch (error) {
4294
- const exhausted = attempts >= maximumAttempts;
4315
+ const retryable = isRetryableReportingSinkError(state.sink, error);
4316
+ const exhausted = attempts >= maximumAttempts || !retryable;
4317
+ const reportedMaximumAttempts = retryable ? maximumAttempts : attempts;
4295
4318
  const nextDelayMs = exhausted ? 0 : sinkRetryDelayMs(backoffMs, attempts);
4296
4319
  logIterationObservationFailure(logger, exhausted ? "error" : "warn", {
4297
4320
  sinkName: state.name,
@@ -4301,7 +4324,7 @@ export class LoadStrikeRunner {
4301
4324
  resultOwnerId: "",
4302
4325
  observationCount: 0,
4303
4326
  attempt: attempts,
4304
- maximumAttempts,
4327
+ maximumAttempts: reportedMaximumAttempts,
4305
4328
  nextDelayMs
4306
4329
  }, error);
4307
4330
  if (exhausted) {
@@ -8867,6 +8890,7 @@ class ManagedScenarioTrackingRuntime {
8867
8890
  }
8868
8891
  async dispose() {
8869
8892
  this.shutdown = true;
8893
+ this.interruptEndpointAdapters();
8870
8894
  this.rejectOutstandingWaiters();
8871
8895
  await Promise.all([
8872
8896
  this.sourceLoop,
@@ -8932,6 +8956,7 @@ class ManagedScenarioTrackingRuntime {
8932
8956
  failureSeen = failureSeen || this.isFailedObservationOutcome(outcome);
8933
8957
  }
8934
8958
  this.shutdown = true;
8959
+ this.interruptEndpointAdapters();
8935
8960
  await Promise.all([
8936
8961
  this.sourceLoop,
8937
8962
  this.destinationLoop,
@@ -8948,6 +8973,20 @@ class ManagedScenarioTrackingRuntime {
8948
8973
  ? LoadStrikeResponse.fail("tracking_failures", "One or more observed source or destination events did not correlate successfully.", 0)
8949
8974
  : LoadStrikeResponse.ok("observed");
8950
8975
  }
8976
+ interruptEndpointAdapters() {
8977
+ try {
8978
+ this.sourceAdapter.interrupt?.();
8979
+ }
8980
+ catch {
8981
+ // Shutdown remains best-effort and still disposes every adapter below.
8982
+ }
8983
+ try {
8984
+ this.destinationAdapter?.interrupt?.();
8985
+ }
8986
+ catch {
8987
+ // Shutdown remains best-effort and still disposes every adapter below.
8988
+ }
8989
+ }
8951
8990
  async consumeSourceLoop() {
8952
8991
  while (!this.shutdown) {
8953
8992
  try {
@@ -9539,6 +9578,20 @@ function mapRuntimeTrackingEndpointSpec(spec, useLoadStrikeTraceIdHeader = false
9539
9578
  azureEventHubs: asTrackingRecord(pickTrackingValue(spec, "AzureEventHubs", "azureEventHubs")),
9540
9579
  sqs: asTrackingRecord(pickTrackingValue(spec, "Sqs", "sqs")),
9541
9580
  pushDiffusion: asTrackingRecord(pickTrackingValue(spec, "PushDiffusion", "pushDiffusion")),
9581
+ grpc: mapRuntimeEndpointProtocolOptions(spec, ["Grpc", "grpc"], [
9582
+ "Target", "target", "ServiceName", "serviceName", "MethodName", "methodName",
9583
+ "MethodType", "methodType", "Deadline", "deadline", "DeadlineSeconds", "deadlineSeconds",
9584
+ "DeadlineMs", "deadlineMs", "Metadata", "metadata", "Produce", "produce", "Consume", "consume",
9585
+ "ProduceAsync", "produceAsync", "ConsumeAsync", "consumeAsync", "ConnectionMetadata", "connectionMetadata",
9586
+ "NativeClient", "nativeClient"
9587
+ ]),
9588
+ webSocket: mapRuntimeEndpointProtocolOptions(spec, ["WebSocket", "webSocket"], [
9589
+ "Url", "url", "Subprotocols", "subprotocols", "ConnectTimeout", "connectTimeout",
9590
+ "ConnectTimeoutSeconds", "connectTimeoutSeconds", "ConnectTimeoutMs", "connectTimeoutMs",
9591
+ "CloseTimeout", "closeTimeout", "CloseTimeoutSeconds", "closeTimeoutSeconds", "CloseTimeoutMs", "closeTimeoutMs",
9592
+ "Produce", "produce", "Consume", "consume", "ProduceAsync", "produceAsync", "ConsumeAsync", "consumeAsync",
9593
+ "ConnectionMetadata", "connectionMetadata", "NativeClient", "nativeClient"
9594
+ ]),
9542
9595
  delegate: typeof delegateProduce === "function"
9543
9596
  || typeof delegateConsume === "function"
9544
9597
  || typeof delegateProduceAsync === "function"
@@ -9561,6 +9614,19 @@ function mapRuntimeTrackingEndpointSpec(spec, useLoadStrikeTraceIdHeader = false
9561
9614
  : undefined
9562
9615
  };
9563
9616
  }
9617
+ function mapRuntimeEndpointProtocolOptions(spec, nestedKeys, flatKeys) {
9618
+ const nested = asTrackingRecord(pickTrackingValue(spec, ...nestedKeys));
9619
+ if (Object.keys(nested).length > 0) {
9620
+ return nested;
9621
+ }
9622
+ const options = {};
9623
+ for (const key of flatKeys) {
9624
+ if (Object.prototype.hasOwnProperty.call(spec, key)) {
9625
+ options[key] = pickTrackingValue(spec, key);
9626
+ }
9627
+ }
9628
+ return options;
9629
+ }
9564
9630
  function normalizeRuntimeTrackingPayload(payload, endpoint, index) {
9565
9631
  const normalized = {
9566
9632
  headers: {
@@ -10324,6 +10390,20 @@ function assertNoDisableLicenseEnforcementOption(value, source) {
10324
10390
  }
10325
10391
  }
10326
10392
  }
10393
+ function assertNoUnsupportedNativeTrackingScenarios(scenarios) {
10394
+ for (const scenario of scenarios) {
10395
+ const tracking = scenario.getTrackingConfiguration();
10396
+ if (!tracking) {
10397
+ continue;
10398
+ }
10399
+ for (const field of ["Source", "Destination"]) {
10400
+ const endpoint = asTrackingRecord(pickTrackingValue(tracking, field, field.toLowerCase()));
10401
+ if (Object.keys(endpoint).length > 0) {
10402
+ validateNativeEndpointExecutionSupport(endpoint);
10403
+ }
10404
+ }
10405
+ }
10406
+ }
10327
10407
  function normalizeRunContextCollectionShapes(values) {
10328
10408
  assertNoDisableLicenseEnforcementOption(values, "LoadStrikeContext");
10329
10409
  const normalized = {