@loadstrike/loadstrike-sdk 0.1.0

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.
@@ -0,0 +1,2675 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.OtelCollectorReportingSink = exports.SplunkReportingSink = exports.DatadogReportingSink = exports.TimescaleDbReportingSink = exports.GrafanaLokiReportingSink = exports.InfluxDbReportingSink = exports.CompositeReportingSink = exports.ConsoleReportingSink = exports.MemoryReportingSink = exports.OtelCollectorReportingSinkOptions = exports.SplunkReportingSinkOptions = exports.DatadogReportingSinkOptions = exports.TimescaleDbReportingSinkOptions = exports.GrafanaLokiReportingSinkOptions = exports.InfluxDbReportingSinkOptions = void 0;
4
+ const runtime_js_1 = require("./runtime.js");
5
+ const pg_1 = require("pg");
6
+ const DEFAULT_INFLUX_CONFIGURATION_SECTION_PATH = "LoadStrike:ReportingSinks:InfluxDb";
7
+ const DEFAULT_GRAFANA_LOKI_CONFIGURATION_SECTION_PATH = "LoadStrike:ReportingSinks:GrafanaLoki";
8
+ const DEFAULT_TIMESCALEDB_CONFIGURATION_SECTION_PATH = "LoadStrike:ReportingSinks:TimescaleDb";
9
+ const DEFAULT_DATADOG_CONFIGURATION_SECTION_PATH = "LoadStrike:ReportingSinks:Datadog";
10
+ const DEFAULT_SPLUNK_CONFIGURATION_SECTION_PATH = "LoadStrike:ReportingSinks:Splunk";
11
+ const DEFAULT_OTEL_COLLECTOR_CONFIGURATION_SECTION_PATH = "LoadStrike:ReportingSinks:OtelCollector";
12
+ const IDENTIFIER_REGEX = /^[A-Za-z_][A-Za-z0-9_]*$/;
13
+ class InfluxDbReportingSinkOptions {
14
+ constructor(initial = {}) {
15
+ this.ConfigurationSectionPath = DEFAULT_INFLUX_CONFIGURATION_SECTION_PATH;
16
+ this.BaseUrl = "";
17
+ this.WriteEndpointPath = "/api/v2/write";
18
+ this.Organization = "";
19
+ this.Bucket = "";
20
+ this.Token = "";
21
+ this.MeasurementName = "loadstrike";
22
+ this.MetricsMeasurementName = "loadstrike_metrics";
23
+ this.TimeoutSeconds = 30;
24
+ this.StaticTags = {};
25
+ const source = asRecord(initial);
26
+ this.ConfigurationSectionPath =
27
+ optionString(source, "configurationSectionPath", "ConfigurationSectionPath").trim()
28
+ || DEFAULT_INFLUX_CONFIGURATION_SECTION_PATH;
29
+ this.BaseUrl = optionString(source, "baseUrl", "BaseUrl").trim() || "";
30
+ this.WriteEndpointPath = optionString(source, "writeEndpointPath", "WriteEndpointPath").trim() || "/api/v2/write";
31
+ this.Organization = optionString(source, "organization", "Organization").trim() || "";
32
+ this.Bucket = optionString(source, "bucket", "Bucket").trim() || "";
33
+ this.Token = optionString(source, "token", "Token").trim() || "";
34
+ this.MeasurementName = optionString(source, "measurementName", "MeasurementName").trim() || "loadstrike";
35
+ this.MetricsMeasurementName = optionString(source, "metricsMeasurementName", "MetricsMeasurementName").trim() || "loadstrike_metrics";
36
+ this.TimeoutSeconds = Number.isFinite(optionNumber(source, "timeoutSeconds", "TimeoutSeconds"))
37
+ ? Number(optionNumber(source, "timeoutSeconds", "TimeoutSeconds"))
38
+ : 30;
39
+ this.TimeoutMs = Number.isFinite(optionNumber(source, "timeoutMs", "TimeoutMs"))
40
+ ? Number(optionNumber(source, "timeoutMs", "TimeoutMs"))
41
+ : undefined;
42
+ this.StaticTags = normalizeStringMap(optionRecord(source, "staticTags", "StaticTags"));
43
+ this.FetchImpl = pickRecordValue(source, "fetchImpl", "FetchImpl");
44
+ }
45
+ }
46
+ exports.InfluxDbReportingSinkOptions = InfluxDbReportingSinkOptions;
47
+ class GrafanaLokiReportingSinkOptions {
48
+ constructor(initial = {}) {
49
+ this.ConfigurationSectionPath = DEFAULT_GRAFANA_LOKI_CONFIGURATION_SECTION_PATH;
50
+ this.BaseUrl = "";
51
+ this.PushEndpointPath = "/loki/api/v1/push";
52
+ this.MetricsBaseUrl = "";
53
+ this.MetricsEndpointPath = "/v1/metrics";
54
+ this.BearerToken = "";
55
+ this.Username = "";
56
+ this.Password = "";
57
+ this.TenantId = "";
58
+ this.TimeoutSeconds = 30;
59
+ this.StaticLabels = {};
60
+ this.MetricsHeaders = {};
61
+ const source = asRecord(initial);
62
+ this.ConfigurationSectionPath =
63
+ optionString(source, "configurationSectionPath", "ConfigurationSectionPath").trim()
64
+ || DEFAULT_GRAFANA_LOKI_CONFIGURATION_SECTION_PATH;
65
+ this.BaseUrl = optionString(source, "baseUrl", "BaseUrl").trim() || "";
66
+ this.PushEndpointPath = optionString(source, "pushEndpointPath", "PushEndpointPath").trim() || "/loki/api/v1/push";
67
+ this.MetricsBaseUrl = optionString(source, "metricsBaseUrl", "MetricsBaseUrl").trim() || "";
68
+ this.MetricsEndpointPath = optionString(source, "metricsEndpointPath", "MetricsEndpointPath").trim() || "/v1/metrics";
69
+ this.BearerToken = optionString(source, "bearerToken", "BearerToken").trim() || "";
70
+ this.Username = optionString(source, "username", "Username").trim() || "";
71
+ this.Password = String(pickRecordValue(source, "password", "Password") ?? "");
72
+ this.TenantId = optionString(source, "tenantId", "TenantId").trim() || "";
73
+ this.TimeoutSeconds = Number.isFinite(optionNumber(source, "timeoutSeconds", "TimeoutSeconds"))
74
+ ? Number(optionNumber(source, "timeoutSeconds", "TimeoutSeconds"))
75
+ : 30;
76
+ this.TimeoutMs = Number.isFinite(optionNumber(source, "timeoutMs", "TimeoutMs"))
77
+ ? Number(optionNumber(source, "timeoutMs", "TimeoutMs"))
78
+ : undefined;
79
+ this.StaticLabels = normalizeStringMap(optionRecord(source, "staticLabels", "StaticLabels"));
80
+ this.MetricsHeaders = normalizeStringMap(optionRecord(source, "metricsHeaders", "MetricsHeaders"));
81
+ this.FetchImpl = pickRecordValue(source, "fetchImpl", "FetchImpl");
82
+ }
83
+ }
84
+ exports.GrafanaLokiReportingSinkOptions = GrafanaLokiReportingSinkOptions;
85
+ class TimescaleDbReportingSinkOptions {
86
+ constructor(initial = {}) {
87
+ this.ConfigurationSectionPath = DEFAULT_TIMESCALEDB_CONFIGURATION_SECTION_PATH;
88
+ this.ConnectionString = "";
89
+ this.Schema = "public";
90
+ this.TableName = "loadstrike_reporting_events";
91
+ this.MetricsTableName = "loadstrike_reporting_metrics";
92
+ this.CreateSchemaIfMissing = true;
93
+ this.EnableHypertableIfAvailable = true;
94
+ this.StaticTags = {};
95
+ const source = asRecord(initial);
96
+ this.ConfigurationSectionPath =
97
+ optionString(source, "configurationSectionPath", "ConfigurationSectionPath").trim()
98
+ || DEFAULT_TIMESCALEDB_CONFIGURATION_SECTION_PATH;
99
+ this.ConnectionString = optionString(source, "connectionString", "ConnectionString").trim() || "";
100
+ this.Schema = optionString(source, "schema", "Schema").trim() || "public";
101
+ this.TableName = optionString(source, "tableName", "TableName").trim() || "loadstrike_reporting_events";
102
+ this.MetricsTableName = optionString(source, "metricsTableName", "MetricsTableName").trim() || "loadstrike_reporting_metrics";
103
+ this.CreateSchemaIfMissing = pickBooleanValue(source, true, "createSchemaIfMissing", "CreateSchemaIfMissing");
104
+ this.EnableHypertableIfAvailable = pickBooleanValue(source, true, "enableHypertableIfAvailable", "EnableHypertableIfAvailable");
105
+ this.StaticTags = normalizeStringMap(optionRecord(source, "staticTags", "StaticTags"));
106
+ this.Insert = pickRecordValue(source, "insert", "Insert");
107
+ this.InsertMetrics = pickRecordValue(source, "insertMetrics", "InsertMetrics");
108
+ }
109
+ }
110
+ exports.TimescaleDbReportingSinkOptions = TimescaleDbReportingSinkOptions;
111
+ class DatadogReportingSinkOptions {
112
+ constructor(initial = {}) {
113
+ this.ConfigurationSectionPath = DEFAULT_DATADOG_CONFIGURATION_SECTION_PATH;
114
+ this.BaseUrl = "";
115
+ this.LogsEndpointPath = "/api/v2/logs";
116
+ this.MetricsEndpointPath = "/api/v2/series";
117
+ this.ApiKey = "";
118
+ this.ApplicationKey = "";
119
+ this.Source = "loadstrike";
120
+ this.Service = "loadstrike";
121
+ this.Host = "";
122
+ this.TimeoutSeconds = 30;
123
+ this.StaticTags = {};
124
+ this.StaticAttributes = {};
125
+ const source = asRecord(initial);
126
+ this.ConfigurationSectionPath =
127
+ optionString(source, "configurationSectionPath", "ConfigurationSectionPath").trim()
128
+ || DEFAULT_DATADOG_CONFIGURATION_SECTION_PATH;
129
+ this.BaseUrl = optionString(source, "baseUrl", "BaseUrl").trim() || "";
130
+ this.LogsEndpointPath = optionString(source, "logsEndpointPath", "LogsEndpointPath").trim() || "/api/v2/logs";
131
+ this.MetricsEndpointPath = optionString(source, "metricsEndpointPath", "MetricsEndpointPath").trim() || "/api/v2/series";
132
+ this.ApiKey = optionString(source, "apiKey", "ApiKey").trim() || "";
133
+ this.ApplicationKey = optionString(source, "applicationKey", "ApplicationKey").trim() || "";
134
+ this.Source = optionString(source, "source", "Source").trim() || "loadstrike";
135
+ this.Service = optionString(source, "service", "Service").trim() || "loadstrike";
136
+ this.Host = optionString(source, "host", "Host").trim() || "";
137
+ this.TimeoutSeconds = Number.isFinite(optionNumber(source, "timeoutSeconds", "TimeoutSeconds"))
138
+ ? Number(optionNumber(source, "timeoutSeconds", "TimeoutSeconds"))
139
+ : 30;
140
+ this.TimeoutMs = Number.isFinite(optionNumber(source, "timeoutMs", "TimeoutMs"))
141
+ ? Number(optionNumber(source, "timeoutMs", "TimeoutMs"))
142
+ : undefined;
143
+ this.StaticTags = normalizeStringMap(optionRecord(source, "staticTags", "StaticTags"));
144
+ this.StaticAttributes = normalizeStringMap(optionRecord(source, "staticAttributes", "StaticAttributes"));
145
+ this.FetchImpl = pickRecordValue(source, "fetchImpl", "FetchImpl");
146
+ }
147
+ }
148
+ exports.DatadogReportingSinkOptions = DatadogReportingSinkOptions;
149
+ class SplunkReportingSinkOptions {
150
+ constructor(initial = {}) {
151
+ this.ConfigurationSectionPath = DEFAULT_SPLUNK_CONFIGURATION_SECTION_PATH;
152
+ this.BaseUrl = "";
153
+ this.EventEndpointPath = "/services/collector/event";
154
+ this.Token = "";
155
+ this.Source = "loadstrike";
156
+ this.Sourcetype = "_json";
157
+ this.Index = "";
158
+ this.Host = "";
159
+ this.TimeoutSeconds = 30;
160
+ this.StaticFields = {};
161
+ const source = asRecord(initial);
162
+ this.ConfigurationSectionPath =
163
+ optionString(source, "configurationSectionPath", "ConfigurationSectionPath").trim()
164
+ || DEFAULT_SPLUNK_CONFIGURATION_SECTION_PATH;
165
+ this.BaseUrl = optionString(source, "baseUrl", "BaseUrl").trim() || "";
166
+ this.EventEndpointPath = optionString(source, "eventEndpointPath", "EventEndpointPath").trim() || "/services/collector/event";
167
+ this.Token = optionString(source, "token", "Token").trim() || "";
168
+ this.Source = optionString(source, "source", "Source").trim() || "loadstrike";
169
+ this.Sourcetype = optionString(source, "sourcetype", "Sourcetype").trim() || "_json";
170
+ this.Index = optionString(source, "index", "Index").trim() || "";
171
+ this.Host = optionString(source, "host", "Host").trim() || "";
172
+ this.TimeoutSeconds = Number.isFinite(optionNumber(source, "timeoutSeconds", "TimeoutSeconds"))
173
+ ? Number(optionNumber(source, "timeoutSeconds", "TimeoutSeconds"))
174
+ : 30;
175
+ this.TimeoutMs = Number.isFinite(optionNumber(source, "timeoutMs", "TimeoutMs"))
176
+ ? Number(optionNumber(source, "timeoutMs", "TimeoutMs"))
177
+ : undefined;
178
+ this.StaticFields = normalizeStringMap(optionRecord(source, "staticFields", "StaticFields"));
179
+ this.FetchImpl = pickRecordValue(source, "fetchImpl", "FetchImpl");
180
+ }
181
+ }
182
+ exports.SplunkReportingSinkOptions = SplunkReportingSinkOptions;
183
+ class OtelCollectorReportingSinkOptions {
184
+ constructor(initial = {}) {
185
+ this.ConfigurationSectionPath = DEFAULT_OTEL_COLLECTOR_CONFIGURATION_SECTION_PATH;
186
+ this.BaseUrl = "";
187
+ this.LogsEndpointPath = "/v1/logs";
188
+ this.MetricsEndpointPath = "/v1/metrics";
189
+ this.TimeoutSeconds = 30;
190
+ this.Headers = {};
191
+ this.StaticResourceAttributes = {};
192
+ const source = asRecord(initial);
193
+ this.ConfigurationSectionPath =
194
+ optionString(source, "configurationSectionPath", "ConfigurationSectionPath").trim()
195
+ || DEFAULT_OTEL_COLLECTOR_CONFIGURATION_SECTION_PATH;
196
+ this.BaseUrl = optionString(source, "baseUrl", "BaseUrl").trim() || "";
197
+ this.LogsEndpointPath = optionString(source, "logsEndpointPath", "LogsEndpointPath").trim() || "/v1/logs";
198
+ this.MetricsEndpointPath = optionString(source, "metricsEndpointPath", "MetricsEndpointPath").trim() || "/v1/metrics";
199
+ this.TimeoutSeconds = Number.isFinite(optionNumber(source, "timeoutSeconds", "TimeoutSeconds"))
200
+ ? Number(optionNumber(source, "timeoutSeconds", "TimeoutSeconds"))
201
+ : 30;
202
+ this.TimeoutMs = Number.isFinite(optionNumber(source, "timeoutMs", "TimeoutMs"))
203
+ ? Number(optionNumber(source, "timeoutMs", "TimeoutMs"))
204
+ : undefined;
205
+ this.Headers = normalizeStringMap(optionRecord(source, "headers", "Headers"));
206
+ this.StaticResourceAttributes = normalizeStringMap(optionRecord(source, "staticResourceAttributes", "StaticResourceAttributes"));
207
+ this.FetchImpl = pickRecordValue(source, "fetchImpl", "FetchImpl");
208
+ }
209
+ }
210
+ exports.OtelCollectorReportingSinkOptions = OtelCollectorReportingSinkOptions;
211
+ class MemoryReportingSink {
212
+ constructor() {
213
+ this.sinkName = "memory";
214
+ this.SinkName = "memory";
215
+ this.initContexts = [];
216
+ this.realtimeSnapshots = [];
217
+ this.realtimeMetrics = [];
218
+ this.finalResults = [];
219
+ this.runResults = [];
220
+ this.sessions = [];
221
+ this.stopCount = 0;
222
+ }
223
+ init(context, infraConfig) {
224
+ this.initContexts.push({ context: cloneBaseContext(context), infraConfig: deepCloneRecord(infraConfig) });
225
+ }
226
+ Init(context, infraConfig) {
227
+ this.init(context, infraConfig);
228
+ }
229
+ start(session) {
230
+ this.sessions.push(cloneSessionStartInfo(session));
231
+ }
232
+ Start(session) {
233
+ this.start(session);
234
+ }
235
+ saveRealtimeStats(scenarioStats) {
236
+ this.realtimeSnapshots.push(scenarioStats.map((value) => cloneScenarioStats(value)));
237
+ }
238
+ SaveRealtimeStats(scenarioStats) {
239
+ this.saveRealtimeStats(scenarioStats);
240
+ }
241
+ saveRealtimeMetrics(metrics) {
242
+ this.realtimeMetrics.push(cloneMetricStats(metrics));
243
+ }
244
+ SaveRealtimeMetrics(metrics) {
245
+ this.saveRealtimeMetrics(metrics);
246
+ }
247
+ saveFinalStats(result) {
248
+ this.finalResults.push(cloneNodeStats(result));
249
+ }
250
+ SaveFinalStats(result) {
251
+ this.saveFinalStats(result);
252
+ }
253
+ saveRunResult(result) {
254
+ this.runResults.push(deepCloneRecord(result));
255
+ }
256
+ SaveRunResult(result) {
257
+ this.saveRunResult(result);
258
+ }
259
+ stop() {
260
+ this.stopCount += 1;
261
+ }
262
+ Stop() {
263
+ this.stop();
264
+ }
265
+ }
266
+ exports.MemoryReportingSink = MemoryReportingSink;
267
+ class ConsoleReportingSink {
268
+ constructor(writeLine) {
269
+ this.sinkName = "console";
270
+ this.SinkName = "console";
271
+ this.writeLine = writeLine ?? ((line) => { console.log(line); });
272
+ }
273
+ init() { }
274
+ Init() {
275
+ this.init();
276
+ }
277
+ start() { }
278
+ Start() {
279
+ this.start();
280
+ }
281
+ saveRealtimeStats(scenarioStats) {
282
+ const requestCount = scenarioStats.reduce((sum, value) => sum + value.allRequestCount, 0);
283
+ const okCount = scenarioStats.reduce((sum, value) => sum + value.allOkCount, 0);
284
+ const failCount = scenarioStats.reduce((sum, value) => sum + value.allFailCount, 0);
285
+ this.writeLine(`realtime requests=${requestCount} ok=${okCount} fail=${failCount}`);
286
+ }
287
+ SaveRealtimeStats(scenarioStats) {
288
+ this.saveRealtimeStats(scenarioStats);
289
+ }
290
+ saveRealtimeMetrics(metrics) {
291
+ this.writeLine(`metrics counters=${metrics.counters.length} gauges=${metrics.gauges.length}`);
292
+ }
293
+ SaveRealtimeMetrics(metrics) {
294
+ this.saveRealtimeMetrics(metrics);
295
+ }
296
+ saveFinalStats(result) {
297
+ this.writeLine(`final requests=${result.allRequestCount} ok=${result.allOkCount} fail=${result.allFailCount} failedThresholds=${result.failedThresholds}`);
298
+ }
299
+ SaveFinalStats(result) {
300
+ this.saveFinalStats(result);
301
+ }
302
+ saveRunResult(_result) { }
303
+ SaveRunResult(result) {
304
+ this.saveRunResult(result);
305
+ }
306
+ stop() { }
307
+ Stop() {
308
+ this.stop();
309
+ }
310
+ }
311
+ exports.ConsoleReportingSink = ConsoleReportingSink;
312
+ class CompositeReportingSink {
313
+ constructor(...sinks) {
314
+ this.sinkName = "composite";
315
+ this.SinkName = "composite";
316
+ for (let index = 0; index < sinks.length; index += 1) {
317
+ const sink = sinks[index];
318
+ if (sink == null) {
319
+ throw new Error("Composite reporting sink cannot contain null sink values.");
320
+ }
321
+ const configuredName = String(sink.sinkName ?? sink.SinkName ?? "").trim();
322
+ if (!configuredName) {
323
+ throw new Error(`Composite reporting sink entries must provide sinkName or SinkName. Index=${index}.`);
324
+ }
325
+ }
326
+ this.sinks = sinks;
327
+ }
328
+ async init(context, infraConfig) {
329
+ for (const sink of this.sinks) {
330
+ const init = sink.init ?? sink.Init;
331
+ if (init) {
332
+ await init.call(sink, context, infraConfig);
333
+ }
334
+ }
335
+ }
336
+ async Init(context, infraConfig) {
337
+ await this.init(context, infraConfig);
338
+ }
339
+ async start(session) {
340
+ for (const sink of this.sinks) {
341
+ const start = sink.start ?? sink.Start;
342
+ if (start) {
343
+ await start.call(sink, session);
344
+ }
345
+ }
346
+ }
347
+ async Start(session) {
348
+ await this.start(session);
349
+ }
350
+ async saveRealtimeStats(scenarioStats) {
351
+ for (const sink of this.sinks) {
352
+ const saveRealtimeStats = sink.saveRealtimeStats ?? sink.SaveRealtimeStats;
353
+ if (saveRealtimeStats) {
354
+ await saveRealtimeStats.call(sink, scenarioStats);
355
+ }
356
+ }
357
+ }
358
+ async SaveRealtimeStats(scenarioStats) {
359
+ await this.saveRealtimeStats(scenarioStats);
360
+ }
361
+ async saveRealtimeMetrics(metrics) {
362
+ for (const sink of this.sinks) {
363
+ const saveRealtimeMetrics = sink.saveRealtimeMetrics ?? sink.SaveRealtimeMetrics;
364
+ if (saveRealtimeMetrics) {
365
+ await saveRealtimeMetrics.call(sink, metrics);
366
+ }
367
+ }
368
+ }
369
+ async SaveRealtimeMetrics(metrics) {
370
+ await this.saveRealtimeMetrics(metrics);
371
+ }
372
+ async saveFinalStats(result) {
373
+ for (const sink of this.sinks) {
374
+ const saveFinalStats = sink.saveFinalStats ?? sink.SaveFinalStats;
375
+ if (saveFinalStats) {
376
+ await saveFinalStats.call(sink, result);
377
+ }
378
+ }
379
+ }
380
+ async SaveFinalStats(result) {
381
+ await this.saveFinalStats(result);
382
+ }
383
+ async saveRunResult(result) {
384
+ for (const sink of this.sinks) {
385
+ const saveRunResult = sink.saveRunResult ?? sink.SaveRunResult;
386
+ if (saveRunResult) {
387
+ await saveRunResult.call(sink, result);
388
+ }
389
+ }
390
+ }
391
+ async SaveRunResult(result) {
392
+ await this.saveRunResult(result);
393
+ }
394
+ async stop() {
395
+ for (const sink of this.sinks) {
396
+ const stop = sink.stop ?? sink.Stop;
397
+ if (stop) {
398
+ await stop.call(sink);
399
+ }
400
+ }
401
+ }
402
+ async Stop() {
403
+ await this.stop();
404
+ }
405
+ }
406
+ exports.CompositeReportingSink = CompositeReportingSink;
407
+ class InfluxDbReportingSink {
408
+ constructor(options = {}) {
409
+ this.sinkName = "influxdb";
410
+ this.SinkName = "influxdb";
411
+ this.licenseFeature = "extensions.reporting_sinks.influxdb";
412
+ this.LicenseFeature = "extensions.reporting_sinks.influxdb";
413
+ this.baseContext = null;
414
+ this.session = null;
415
+ const source = asRecord(options);
416
+ this.options = {
417
+ configurationSectionPath: optionString(source, "configurationSectionPath", "ConfigurationSectionPath").trim() || DEFAULT_INFLUX_CONFIGURATION_SECTION_PATH,
418
+ baseUrl: optionString(source, "baseUrl", "BaseUrl").trim() || "",
419
+ writeEndpointPath: optionString(source, "writeEndpointPath", "WriteEndpointPath").trim() || "/api/v2/write",
420
+ organization: optionString(source, "organization", "Organization").trim() || "",
421
+ bucket: optionString(source, "bucket", "Bucket").trim() || "",
422
+ token: optionString(source, "token", "Token").trim() || "",
423
+ measurementName: optionString(source, "measurementName", "MeasurementName").trim() || "loadstrike",
424
+ metricsMeasurementName: optionString(source, "metricsMeasurementName", "MetricsMeasurementName").trim() || "loadstrike_metrics",
425
+ timeoutSeconds: Number.isFinite(optionNumber(source, "timeoutSeconds", "TimeoutSeconds"))
426
+ ? Number(optionNumber(source, "timeoutSeconds", "TimeoutSeconds"))
427
+ : 30,
428
+ timeoutMs: Number.isFinite(optionNumber(source, "timeoutMs", "TimeoutMs"))
429
+ ? Number(optionNumber(source, "timeoutMs", "TimeoutMs"))
430
+ : undefined,
431
+ staticTags: normalizeStringMap(optionRecord(source, "staticTags", "StaticTags"))
432
+ };
433
+ this.fetchImpl = pickRecordValue(source, "fetchImpl", "FetchImpl") ?? fetch;
434
+ }
435
+ init(context, infraConfig) {
436
+ this.baseContext = cloneBaseContext(context);
437
+ mergeInfluxOptions(this.options, readInfluxOptionsFromConfig(infraConfig, this.options.configurationSectionPath));
438
+ if (!this.options.baseUrl.trim()) {
439
+ throw new Error("InfluxDbReportingSink requires BaseUrl.");
440
+ }
441
+ if (!this.options.organization.trim()) {
442
+ throw new Error("InfluxDbReportingSink requires Organization.");
443
+ }
444
+ if (!this.options.bucket.trim()) {
445
+ throw new Error("InfluxDbReportingSink requires Bucket.");
446
+ }
447
+ }
448
+ Init(context, infraConfig) {
449
+ this.init(context, infraConfig);
450
+ }
451
+ start(session) {
452
+ this.session = sinkSessionMetadataFromContext(this.getBaseContext(), session);
453
+ }
454
+ Start(session) {
455
+ this.start(session);
456
+ }
457
+ async saveRealtimeStats(scenarioStats) {
458
+ await this.persistEvents(createRealtimeStatsEvents(this.getSession(), scenarioStats));
459
+ }
460
+ async SaveRealtimeStats(scenarioStats) {
461
+ await this.saveRealtimeStats(scenarioStats);
462
+ }
463
+ async saveRealtimeMetrics(metrics) {
464
+ await this.persistEvents(createRealtimeMetricEvents(this.getSession(), metrics));
465
+ }
466
+ async SaveRealtimeMetrics(metrics) {
467
+ await this.saveRealtimeMetrics(metrics);
468
+ }
469
+ async saveFinalStats(result) {
470
+ await this.persistEvents(createFinalStatsEvents(this.getSession(), result));
471
+ }
472
+ async SaveFinalStats(result) {
473
+ await this.saveFinalStats(result);
474
+ }
475
+ async saveRunResult(result) {
476
+ await this.persistEvents(createRunResultEvents(this.getSession(), result));
477
+ }
478
+ async SaveRunResult(result) {
479
+ await this.saveRunResult(result);
480
+ }
481
+ stop() {
482
+ this.session = null;
483
+ }
484
+ Stop() {
485
+ this.stop();
486
+ }
487
+ Dispose() {
488
+ this.baseContext = null;
489
+ this.stop();
490
+ }
491
+ getBaseContext() {
492
+ if (!this.baseContext) {
493
+ throw new Error(`${this.sinkName} has not been initialized.`);
494
+ }
495
+ return this.baseContext;
496
+ }
497
+ getSession() {
498
+ if (!this.session) {
499
+ throw new Error(`${this.sinkName} has not been started.`);
500
+ }
501
+ return this.session;
502
+ }
503
+ async persistEvents(events) {
504
+ if (!events.length) {
505
+ return;
506
+ }
507
+ const payload = [
508
+ buildInfluxLineProtocol(this.options.measurementName, this.options.staticTags, events.filter((event) => !isMetricEventType(event.eventType))),
509
+ buildInfluxLineProtocol(this.options.metricsMeasurementName, this.options.staticTags, events.filter((event) => isMetricEventType(event.eventType)))
510
+ ]
511
+ .filter((value) => value.trim().length > 0)
512
+ .join("\n");
513
+ if (!payload) {
514
+ return;
515
+ }
516
+ await postWithTimeout(this.fetchImpl, buildInfluxWriteUri(this.options), {
517
+ method: "POST",
518
+ headers: {
519
+ "Content-Type": "text/plain; charset=utf-8",
520
+ ...(this.options.token ? { Authorization: `Token ${this.options.token}` } : {})
521
+ },
522
+ body: `${payload}\n`
523
+ }, resolveTimeoutMs(this.options.timeoutSeconds, this.options.timeoutMs), "InfluxDbReportingSink");
524
+ }
525
+ }
526
+ exports.InfluxDbReportingSink = InfluxDbReportingSink;
527
+ class GrafanaLokiReportingSink {
528
+ constructor(options = {}) {
529
+ this.sinkName = "grafana-loki";
530
+ this.SinkName = "grafana-loki";
531
+ this.licenseFeature = "extensions.reporting_sinks.grafana_loki";
532
+ this.LicenseFeature = "extensions.reporting_sinks.grafana_loki";
533
+ this.baseContext = null;
534
+ this.session = null;
535
+ const source = asRecord(options);
536
+ this.options = {
537
+ configurationSectionPath: optionString(source, "configurationSectionPath", "ConfigurationSectionPath").trim() || DEFAULT_GRAFANA_LOKI_CONFIGURATION_SECTION_PATH,
538
+ baseUrl: optionString(source, "baseUrl", "BaseUrl").trim() || "",
539
+ pushEndpointPath: optionString(source, "pushEndpointPath", "PushEndpointPath").trim() || "/loki/api/v1/push",
540
+ metricsBaseUrl: optionString(source, "metricsBaseUrl", "MetricsBaseUrl").trim() || "",
541
+ metricsEndpointPath: optionString(source, "metricsEndpointPath", "MetricsEndpointPath").trim() || "/v1/metrics",
542
+ bearerToken: optionString(source, "bearerToken", "BearerToken").trim() || "",
543
+ username: optionString(source, "username", "Username").trim() || "",
544
+ password: String(pickRecordValue(source, "password", "Password") ?? ""),
545
+ tenantId: optionString(source, "tenantId", "TenantId").trim() || "",
546
+ timeoutSeconds: Number.isFinite(optionNumber(source, "timeoutSeconds", "TimeoutSeconds"))
547
+ ? Number(optionNumber(source, "timeoutSeconds", "TimeoutSeconds"))
548
+ : 30,
549
+ timeoutMs: Number.isFinite(optionNumber(source, "timeoutMs", "TimeoutMs"))
550
+ ? Number(optionNumber(source, "timeoutMs", "TimeoutMs"))
551
+ : undefined,
552
+ staticLabels: normalizeStringMap(optionRecord(source, "staticLabels", "StaticLabels")),
553
+ metricsHeaders: normalizeStringMap(optionRecord(source, "metricsHeaders", "MetricsHeaders"))
554
+ };
555
+ this.fetchImpl = pickRecordValue(source, "fetchImpl", "FetchImpl") ?? fetch;
556
+ }
557
+ init(context, infraConfig) {
558
+ this.baseContext = cloneBaseContext(context);
559
+ mergeGrafanaLokiOptions(this.options, readGrafanaLokiOptionsFromConfig(infraConfig, this.options.configurationSectionPath));
560
+ if (!this.options.baseUrl.trim()) {
561
+ throw new Error("GrafanaLokiReportingSink requires BaseUrl.");
562
+ }
563
+ }
564
+ Init(context, infraConfig) {
565
+ this.init(context, infraConfig);
566
+ }
567
+ start(session) {
568
+ this.session = sinkSessionMetadataFromContext(this.getBaseContext(), session);
569
+ }
570
+ Start(session) {
571
+ this.start(session);
572
+ }
573
+ async saveRealtimeStats(scenarioStats) {
574
+ await this.persistEvents(createRealtimeStatsEvents(this.getSession(), scenarioStats));
575
+ }
576
+ async SaveRealtimeStats(scenarioStats) {
577
+ await this.saveRealtimeStats(scenarioStats);
578
+ }
579
+ async saveRealtimeMetrics(metrics) {
580
+ await this.persistEvents(createRealtimeMetricEvents(this.getSession(), metrics));
581
+ }
582
+ async SaveRealtimeMetrics(metrics) {
583
+ await this.saveRealtimeMetrics(metrics);
584
+ }
585
+ async saveFinalStats(result) {
586
+ await this.persistEvents(createFinalStatsEvents(this.getSession(), result));
587
+ }
588
+ async SaveFinalStats(result) {
589
+ await this.saveFinalStats(result);
590
+ }
591
+ async saveRunResult(result) {
592
+ await this.persistEvents(createRunResultEvents(this.getSession(), result));
593
+ }
594
+ async SaveRunResult(result) {
595
+ await this.saveRunResult(result);
596
+ }
597
+ stop() {
598
+ this.session = null;
599
+ }
600
+ Stop() {
601
+ this.stop();
602
+ }
603
+ Dispose() {
604
+ this.baseContext = null;
605
+ this.stop();
606
+ }
607
+ getBaseContext() {
608
+ if (!this.baseContext) {
609
+ throw new Error(`${this.sinkName} has not been initialized.`);
610
+ }
611
+ return this.baseContext;
612
+ }
613
+ getSession() {
614
+ if (!this.session) {
615
+ throw new Error(`${this.sinkName} has not been started.`);
616
+ }
617
+ return this.session;
618
+ }
619
+ async persistEvents(events) {
620
+ if (!events.length) {
621
+ return;
622
+ }
623
+ const streams = groupGrafanaLokiStreams(this.options.staticLabels, events);
624
+ if (!streams.length) {
625
+ return;
626
+ }
627
+ const headers = {
628
+ "Content-Type": "application/json"
629
+ };
630
+ if (this.options.bearerToken) {
631
+ headers.Authorization = `Bearer ${this.options.bearerToken}`;
632
+ }
633
+ else if (this.options.username) {
634
+ const raw = Buffer.from(`${this.options.username}:${this.options.password}`).toString("base64");
635
+ headers.Authorization = `Basic ${raw}`;
636
+ }
637
+ if (this.options.tenantId) {
638
+ headers["X-Scope-OrgID"] = this.options.tenantId;
639
+ }
640
+ await postWithTimeout(this.fetchImpl, `${trimTrailingSlashes(this.options.baseUrl)}${normalizePath(this.options.pushEndpointPath)}`, {
641
+ method: "POST",
642
+ headers,
643
+ body: JSON.stringify({ streams })
644
+ }, resolveTimeoutMs(this.options.timeoutSeconds, this.options.timeoutMs), "GrafanaLokiReportingSink");
645
+ const metricPoints = createReportingSinkMetricPoints(this.sinkName, events, this.options.staticLabels);
646
+ if (!metricPoints.length) {
647
+ return;
648
+ }
649
+ const grouped = new Map();
650
+ for (const point of metricPoints) {
651
+ const key = `${point.metricName}|${point.metricKind}|${point.unitOfMeasure ?? ""}`;
652
+ const bucket = grouped.get(key) ?? [];
653
+ bucket.push(point);
654
+ grouped.set(key, bucket);
655
+ }
656
+ const metricsPayload = {
657
+ resourceMetrics: [{
658
+ resource: { attributes: buildOtelResourceAttributes(this.sinkName, this.options.staticLabels) },
659
+ scopeMetrics: [{
660
+ scope: { name: "loadstrike.reporting", version: "1.0.0" },
661
+ metrics: Array.from(grouped.values()).map((points) => buildOtelMetric(points[0], points))
662
+ }]
663
+ }]
664
+ };
665
+ await postWithTimeout(this.fetchImpl, `${trimTrailingSlashes(this.options.metricsBaseUrl.trim() || this.options.baseUrl)}${normalizePath(this.options.metricsEndpointPath)}`, {
666
+ method: "POST",
667
+ headers: {
668
+ "Content-Type": "application/json",
669
+ ...this.options.metricsHeaders
670
+ },
671
+ body: JSON.stringify(metricsPayload)
672
+ }, resolveTimeoutMs(this.options.timeoutSeconds, this.options.timeoutMs), "GrafanaLokiReportingSink metrics");
673
+ }
674
+ }
675
+ exports.GrafanaLokiReportingSink = GrafanaLokiReportingSink;
676
+ class TimescaleDbReportingSink {
677
+ constructor(options = {}) {
678
+ this.sinkName = "timescaledb";
679
+ this.SinkName = "timescaledb";
680
+ this.licenseFeature = "extensions.reporting_sinks.timescaledb";
681
+ this.LicenseFeature = "extensions.reporting_sinks.timescaledb";
682
+ this.rows = [];
683
+ this.baseContext = null;
684
+ this.session = null;
685
+ this.pool = null;
686
+ const source = asRecord(options);
687
+ this.options = {
688
+ configurationSectionPath: optionString(source, "configurationSectionPath", "ConfigurationSectionPath").trim() || DEFAULT_TIMESCALEDB_CONFIGURATION_SECTION_PATH,
689
+ connectionString: optionString(source, "connectionString", "ConnectionString").trim() || "",
690
+ schema: optionString(source, "schema", "Schema").trim() || "public",
691
+ tableName: optionString(source, "tableName", "TableName").trim() || "loadstrike_reporting_events",
692
+ metricsTableName: optionString(source, "metricsTableName", "MetricsTableName").trim() || "loadstrike_reporting_metrics",
693
+ createSchemaIfMissing: pickBooleanValue(source, true, "createSchemaIfMissing", "CreateSchemaIfMissing"),
694
+ enableHypertableIfAvailable: pickBooleanValue(source, true, "enableHypertableIfAvailable", "EnableHypertableIfAvailable"),
695
+ staticTags: normalizeStringMap(optionRecord(source, "staticTags", "StaticTags")),
696
+ insert: pickRecordValue(source, "insert", "Insert"),
697
+ insertMetrics: pickRecordValue(source, "insertMetrics", "InsertMetrics")
698
+ };
699
+ }
700
+ async init(context, infraConfig) {
701
+ this.baseContext = cloneBaseContext(context);
702
+ mergeTimescaleDbOptions(this.options, readTimescaleDbOptionsFromConfig(infraConfig, this.options.configurationSectionPath));
703
+ if (!this.options.insert && !this.options.connectionString.trim()) {
704
+ throw new Error("TimescaleDbReportingSink requires ConnectionString.");
705
+ }
706
+ validateIdentifier(this.options.schema, "Schema");
707
+ validateIdentifier(this.options.tableName, "TableName");
708
+ validateIdentifier(this.options.metricsTableName, "MetricsTableName");
709
+ if (!this.options.insert) {
710
+ this.pool = new pg_1.Pool({
711
+ connectionString: this.options.connectionString
712
+ });
713
+ await this.ensureStorage(context.logger);
714
+ }
715
+ }
716
+ async Init(context, infraConfig) {
717
+ await this.init(context, infraConfig);
718
+ }
719
+ start(session) {
720
+ this.session = sinkSessionMetadataFromContext(this.getBaseContext(), session);
721
+ }
722
+ Start(session) {
723
+ this.start(session);
724
+ }
725
+ async saveRealtimeStats(scenarioStats) {
726
+ await this.persistEvents(createRealtimeStatsEvents(this.getSession(), scenarioStats));
727
+ }
728
+ async SaveRealtimeStats(scenarioStats) {
729
+ await this.saveRealtimeStats(scenarioStats);
730
+ }
731
+ async saveRealtimeMetrics(metrics) {
732
+ await this.persistEvents(createRealtimeMetricEvents(this.getSession(), metrics));
733
+ }
734
+ async SaveRealtimeMetrics(metrics) {
735
+ await this.saveRealtimeMetrics(metrics);
736
+ }
737
+ async saveFinalStats(result) {
738
+ await this.persistEvents(createFinalStatsEvents(this.getSession(), result));
739
+ }
740
+ async SaveFinalStats(result) {
741
+ await this.saveFinalStats(result);
742
+ }
743
+ async saveRunResult(result) {
744
+ await this.persistEvents(createRunResultEvents(this.getSession(), result));
745
+ }
746
+ async SaveRunResult(result) {
747
+ await this.saveRunResult(result);
748
+ }
749
+ async stop() {
750
+ this.session = null;
751
+ if (this.pool) {
752
+ const pool = this.pool;
753
+ this.pool = null;
754
+ await pool.end();
755
+ }
756
+ }
757
+ async Stop() {
758
+ await this.stop();
759
+ }
760
+ async Dispose() {
761
+ this.baseContext = null;
762
+ await this.stop();
763
+ }
764
+ getBufferedRows() {
765
+ return this.rows.map((value) => deepCloneRecord(value));
766
+ }
767
+ getBaseContext() {
768
+ if (!this.baseContext) {
769
+ throw new Error(`${this.sinkName} has not been initialized.`);
770
+ }
771
+ return this.baseContext;
772
+ }
773
+ getSession() {
774
+ if (!this.session) {
775
+ throw new Error(`${this.sinkName} has not been started.`);
776
+ }
777
+ return this.session;
778
+ }
779
+ async persistEvents(events) {
780
+ if (!events.length) {
781
+ return;
782
+ }
783
+ const rows = events.map((event) => ({
784
+ occurred_utc: event.occurredUtc.toISOString(),
785
+ session_id: event.sessionId,
786
+ test_suite: event.testSuite || null,
787
+ test_name: event.testName || null,
788
+ cluster_id: event.clusterId || null,
789
+ node_type: event.nodeType,
790
+ machine_name: event.machineName,
791
+ scenario_name: event.scenarioName,
792
+ step_name: event.stepName,
793
+ event_type: event.eventType,
794
+ tags: mergeTimescaleTags(event.tags, this.options.staticTags),
795
+ fields: deepCloneRecord(event.fields)
796
+ }));
797
+ const metricRows = createReportingSinkMetricPoints(this.sinkName, events, this.options.staticTags).map((point) => ({
798
+ occurred_utc: point.occurredUtc.toISOString(),
799
+ metric_name: point.metricName,
800
+ metric_kind: point.metricKind,
801
+ value: point.value,
802
+ unit_of_measure: point.unitOfMeasure ?? null,
803
+ tags: deepCloneRecord(point.tags)
804
+ }));
805
+ if (this.options.insert) {
806
+ await this.options.insert(quoteIdentifier(this.options.schema, this.options.tableName), rows.map((value) => deepCloneRecord(value)));
807
+ if (metricRows.length && this.options.insertMetrics) {
808
+ await this.options.insertMetrics(quoteIdentifier(this.options.schema, this.options.metricsTableName), metricRows.map((value) => deepCloneRecord(value)));
809
+ }
810
+ return;
811
+ }
812
+ if (this.pool) {
813
+ const client = await this.pool.connect();
814
+ try {
815
+ await client.query("BEGIN");
816
+ for (const row of rows) {
817
+ await client.query(`INSERT INTO ${quoteIdentifier(this.options.schema, this.options.tableName)}
818
+ (
819
+ occurred_utc,
820
+ session_id,
821
+ test_suite,
822
+ test_name,
823
+ cluster_id,
824
+ node_type,
825
+ machine_name,
826
+ scenario_name,
827
+ step_name,
828
+ event_type,
829
+ tags,
830
+ fields
831
+ )
832
+ VALUES
833
+ (
834
+ $1,
835
+ $2,
836
+ $3,
837
+ $4,
838
+ $5,
839
+ $6,
840
+ $7,
841
+ $8,
842
+ $9,
843
+ $10,
844
+ $11::jsonb,
845
+ $12::jsonb
846
+ );`, [
847
+ row.occurred_utc,
848
+ row.session_id,
849
+ row.test_suite,
850
+ row.test_name,
851
+ row.cluster_id,
852
+ row.node_type,
853
+ row.machine_name,
854
+ row.scenario_name,
855
+ row.step_name,
856
+ row.event_type,
857
+ JSON.stringify(row.tags),
858
+ JSON.stringify(row.fields)
859
+ ]);
860
+ }
861
+ for (const row of metricRows) {
862
+ await client.query(`INSERT INTO ${quoteIdentifier(this.options.schema, this.options.metricsTableName)}
863
+ (
864
+ occurred_utc,
865
+ metric_name,
866
+ metric_kind,
867
+ value,
868
+ unit_of_measure,
869
+ tags
870
+ )
871
+ VALUES
872
+ (
873
+ $1,
874
+ $2,
875
+ $3,
876
+ $4,
877
+ $5,
878
+ $6::jsonb
879
+ );`, [
880
+ row.occurred_utc,
881
+ row.metric_name,
882
+ row.metric_kind,
883
+ row.value,
884
+ row.unit_of_measure,
885
+ JSON.stringify(row.tags)
886
+ ]);
887
+ }
888
+ await client.query("COMMIT");
889
+ }
890
+ catch (error) {
891
+ await client.query("ROLLBACK");
892
+ throw error;
893
+ }
894
+ finally {
895
+ client.release();
896
+ }
897
+ return;
898
+ }
899
+ this.rows.push(...rows.map((value) => deepCloneRecord(value)));
900
+ this.rows.push(...metricRows.map((value) => deepCloneRecord(value)));
901
+ }
902
+ async ensureStorage(logger) {
903
+ if (!this.pool) {
904
+ return;
905
+ }
906
+ const client = await this.pool.connect();
907
+ try {
908
+ if (this.options.createSchemaIfMissing) {
909
+ await client.query(`CREATE SCHEMA IF NOT EXISTS ${quoteIdentifierPart(this.options.schema)};`);
910
+ }
911
+ await client.query(`CREATE TABLE IF NOT EXISTS ${quoteIdentifier(this.options.schema, this.options.tableName)}
912
+ (
913
+ id BIGSERIAL PRIMARY KEY,
914
+ occurred_utc TIMESTAMPTZ NOT NULL,
915
+ session_id TEXT NOT NULL,
916
+ test_suite TEXT NULL,
917
+ test_name TEXT NULL,
918
+ cluster_id TEXT NULL,
919
+ node_type TEXT NOT NULL,
920
+ machine_name TEXT NOT NULL,
921
+ scenario_name TEXT NULL,
922
+ step_name TEXT NULL,
923
+ event_type TEXT NOT NULL,
924
+ tags JSONB NOT NULL,
925
+ fields JSONB NOT NULL
926
+ );`);
927
+ await client.query(`CREATE TABLE IF NOT EXISTS ${quoteIdentifier(this.options.schema, this.options.metricsTableName)}
928
+ (
929
+ id BIGSERIAL PRIMARY KEY,
930
+ occurred_utc TIMESTAMPTZ NOT NULL,
931
+ metric_name TEXT NOT NULL,
932
+ metric_kind TEXT NOT NULL,
933
+ value DOUBLE PRECISION NOT NULL,
934
+ unit_of_measure TEXT NULL,
935
+ tags JSONB NOT NULL
936
+ );`);
937
+ await client.query(`CREATE INDEX IF NOT EXISTS ix_${this.options.tableName}_occurred_utc ON ${quoteIdentifier(this.options.schema, this.options.tableName)} (occurred_utc DESC);`);
938
+ await client.query(`CREATE INDEX IF NOT EXISTS ix_${this.options.tableName}_session_id ON ${quoteIdentifier(this.options.schema, this.options.tableName)} (session_id);`);
939
+ await client.query(`CREATE INDEX IF NOT EXISTS ix_${this.options.metricsTableName}_occurred_utc ON ${quoteIdentifier(this.options.schema, this.options.metricsTableName)} (occurred_utc DESC);`);
940
+ await client.query(`CREATE INDEX IF NOT EXISTS ix_${this.options.metricsTableName}_metric_name ON ${quoteIdentifier(this.options.schema, this.options.metricsTableName)} (metric_name);`);
941
+ if (this.options.enableHypertableIfAvailable) {
942
+ try {
943
+ await client.query("CREATE EXTENSION IF NOT EXISTS timescaledb;");
944
+ await client.query(`SELECT create_hypertable('${this.options.schema}.${this.options.tableName}', 'occurred_utc', if_not_exists => TRUE);`);
945
+ await client.query(`SELECT create_hypertable('${this.options.schema}.${this.options.metricsTableName}', 'occurred_utc', if_not_exists => TRUE);`);
946
+ }
947
+ catch (_error) {
948
+ logger.warn("TimescaleDbReportingSink could not enable hypertable mode. Continuing with standard PostgreSQL table.");
949
+ }
950
+ }
951
+ }
952
+ finally {
953
+ client.release();
954
+ }
955
+ }
956
+ }
957
+ exports.TimescaleDbReportingSink = TimescaleDbReportingSink;
958
+ class DatadogReportingSink {
959
+ constructor(options = {}) {
960
+ this.sinkName = "datadog";
961
+ this.SinkName = "datadog";
962
+ this.licenseFeature = "extensions.reporting_sinks.datadog";
963
+ this.LicenseFeature = "extensions.reporting_sinks.datadog";
964
+ this.baseContext = null;
965
+ this.session = null;
966
+ const source = asRecord(options);
967
+ this.options = {
968
+ configurationSectionPath: optionString(source, "configurationSectionPath", "ConfigurationSectionPath").trim() || DEFAULT_DATADOG_CONFIGURATION_SECTION_PATH,
969
+ baseUrl: optionString(source, "baseUrl", "BaseUrl").trim() || "",
970
+ logsEndpointPath: optionString(source, "logsEndpointPath", "LogsEndpointPath").trim() || "/api/v2/logs",
971
+ metricsEndpointPath: optionString(source, "metricsEndpointPath", "MetricsEndpointPath").trim() || "/api/v2/series",
972
+ apiKey: optionString(source, "apiKey", "ApiKey").trim() || "",
973
+ applicationKey: optionString(source, "applicationKey", "ApplicationKey").trim() || "",
974
+ source: optionString(source, "source", "Source").trim() || "loadstrike",
975
+ service: optionString(source, "service", "Service").trim() || "loadstrike",
976
+ host: optionString(source, "host", "Host").trim() || "",
977
+ timeoutSeconds: Number.isFinite(optionNumber(source, "timeoutSeconds", "TimeoutSeconds"))
978
+ ? Number(optionNumber(source, "timeoutSeconds", "TimeoutSeconds"))
979
+ : 30,
980
+ timeoutMs: Number.isFinite(optionNumber(source, "timeoutMs", "TimeoutMs"))
981
+ ? Number(optionNumber(source, "timeoutMs", "TimeoutMs"))
982
+ : undefined,
983
+ staticTags: normalizeStringMap(optionRecord(source, "staticTags", "StaticTags")),
984
+ staticAttributes: normalizeStringMap(optionRecord(source, "staticAttributes", "StaticAttributes"))
985
+ };
986
+ this.fetchImpl = pickRecordValue(source, "fetchImpl", "FetchImpl") ?? fetch;
987
+ }
988
+ init(context, infraConfig) {
989
+ this.baseContext = cloneBaseContext(context);
990
+ mergeDatadogOptions(this.options, readDatadogOptionsFromConfig(infraConfig, this.options.configurationSectionPath));
991
+ if (!this.options.baseUrl.trim()) {
992
+ throw new Error("DatadogReportingSink requires BaseUrl.");
993
+ }
994
+ if (!this.options.apiKey.trim()) {
995
+ throw new Error("DatadogReportingSink requires ApiKey.");
996
+ }
997
+ }
998
+ Init(context, infraConfig) {
999
+ this.init(context, infraConfig);
1000
+ }
1001
+ start(session) {
1002
+ this.session = sinkSessionMetadataFromContext(this.getBaseContext(), session);
1003
+ }
1004
+ Start(session) {
1005
+ this.start(session);
1006
+ }
1007
+ async saveRealtimeStats(scenarioStats) {
1008
+ await this.persistEvents(createRealtimeStatsEvents(this.getSession(), scenarioStats));
1009
+ }
1010
+ async SaveRealtimeStats(scenarioStats) {
1011
+ await this.saveRealtimeStats(scenarioStats);
1012
+ }
1013
+ async saveRealtimeMetrics(metrics) {
1014
+ await this.persistEvents(createRealtimeMetricEvents(this.getSession(), metrics));
1015
+ }
1016
+ async SaveRealtimeMetrics(metrics) {
1017
+ await this.saveRealtimeMetrics(metrics);
1018
+ }
1019
+ async saveFinalStats(result) {
1020
+ await this.persistEvents(createFinalStatsEvents(this.getSession(), result));
1021
+ }
1022
+ async SaveFinalStats(result) {
1023
+ await this.saveFinalStats(result);
1024
+ }
1025
+ async saveRunResult(result) {
1026
+ await this.persistEvents(createRunResultEvents(this.getSession(), result));
1027
+ }
1028
+ async SaveRunResult(result) {
1029
+ await this.saveRunResult(result);
1030
+ }
1031
+ stop() {
1032
+ this.session = null;
1033
+ }
1034
+ Stop() {
1035
+ this.stop();
1036
+ }
1037
+ Dispose() {
1038
+ this.baseContext = null;
1039
+ this.stop();
1040
+ }
1041
+ getBaseContext() {
1042
+ if (!this.baseContext) {
1043
+ throw new Error(`${this.sinkName} has not been initialized.`);
1044
+ }
1045
+ return this.baseContext;
1046
+ }
1047
+ getSession() {
1048
+ if (!this.session) {
1049
+ throw new Error(`${this.sinkName} has not been started.`);
1050
+ }
1051
+ return this.session;
1052
+ }
1053
+ async persistEvents(events) {
1054
+ if (!events.length) {
1055
+ return;
1056
+ }
1057
+ const headers = {
1058
+ "Content-Type": "application/json",
1059
+ "DD-API-KEY": this.options.apiKey
1060
+ };
1061
+ if (this.options.applicationKey) {
1062
+ headers["DD-APPLICATION-KEY"] = this.options.applicationKey;
1063
+ }
1064
+ const logEntries = events.map((event) => buildDatadogLogEntry(this.options, event));
1065
+ await postWithTimeout(this.fetchImpl, `${trimTrailingSlashes(this.options.baseUrl)}${normalizePath(this.options.logsEndpointPath)}`, {
1066
+ method: "POST",
1067
+ headers,
1068
+ body: JSON.stringify(logEntries)
1069
+ }, resolveTimeoutMs(this.options.timeoutSeconds, this.options.timeoutMs), "DatadogReportingSink logs");
1070
+ const series = createReportingSinkMetricPoints(this.sinkName, events, this.options.staticTags).map((point) => ({
1071
+ metric: point.metricName,
1072
+ type: point.metricKind,
1073
+ points: [[toUnixSeconds(point.occurredUtc), point.value]],
1074
+ tags: Object.entries(point.tags)
1075
+ .sort(([left], [right]) => left.localeCompare(right))
1076
+ .map(([key, value]) => `${sanitizeDatadogTagComponent(key)}:${sanitizeDatadogTagComponent(value)}`)
1077
+ }));
1078
+ if (!series.length) {
1079
+ return;
1080
+ }
1081
+ await postWithTimeout(this.fetchImpl, `${trimTrailingSlashes(this.options.baseUrl)}${normalizePath(this.options.metricsEndpointPath)}`, {
1082
+ method: "POST",
1083
+ headers,
1084
+ body: JSON.stringify({ series })
1085
+ }, resolveTimeoutMs(this.options.timeoutSeconds, this.options.timeoutMs), "DatadogReportingSink metrics");
1086
+ }
1087
+ }
1088
+ exports.DatadogReportingSink = DatadogReportingSink;
1089
+ class SplunkReportingSink {
1090
+ constructor(options = {}) {
1091
+ this.sinkName = "splunk";
1092
+ this.SinkName = "splunk";
1093
+ this.licenseFeature = "extensions.reporting_sinks.splunk";
1094
+ this.LicenseFeature = "extensions.reporting_sinks.splunk";
1095
+ this.baseContext = null;
1096
+ this.session = null;
1097
+ const source = asRecord(options);
1098
+ this.options = {
1099
+ configurationSectionPath: optionString(source, "configurationSectionPath", "ConfigurationSectionPath").trim() || DEFAULT_SPLUNK_CONFIGURATION_SECTION_PATH,
1100
+ baseUrl: optionString(source, "baseUrl", "BaseUrl").trim() || "",
1101
+ eventEndpointPath: optionString(source, "eventEndpointPath", "EventEndpointPath").trim() || "/services/collector/event",
1102
+ token: optionString(source, "token", "Token").trim() || "",
1103
+ source: optionString(source, "source", "Source").trim() || "loadstrike",
1104
+ sourcetype: optionString(source, "sourcetype", "Sourcetype").trim() || "_json",
1105
+ index: optionString(source, "index", "Index").trim() || "",
1106
+ host: optionString(source, "host", "Host").trim() || "",
1107
+ timeoutSeconds: Number.isFinite(optionNumber(source, "timeoutSeconds", "TimeoutSeconds"))
1108
+ ? Number(optionNumber(source, "timeoutSeconds", "TimeoutSeconds"))
1109
+ : 30,
1110
+ timeoutMs: Number.isFinite(optionNumber(source, "timeoutMs", "TimeoutMs"))
1111
+ ? Number(optionNumber(source, "timeoutMs", "TimeoutMs"))
1112
+ : undefined,
1113
+ staticFields: normalizeStringMap(optionRecord(source, "staticFields", "StaticFields"))
1114
+ };
1115
+ this.fetchImpl = pickRecordValue(source, "fetchImpl", "FetchImpl") ?? fetch;
1116
+ }
1117
+ init(context, infraConfig) {
1118
+ this.baseContext = cloneBaseContext(context);
1119
+ mergeSplunkOptions(this.options, readSplunkOptionsFromConfig(infraConfig, this.options.configurationSectionPath));
1120
+ if (!this.options.baseUrl.trim()) {
1121
+ throw new Error("SplunkReportingSink requires BaseUrl.");
1122
+ }
1123
+ if (!this.options.token.trim()) {
1124
+ throw new Error("SplunkReportingSink requires Token.");
1125
+ }
1126
+ }
1127
+ Init(context, infraConfig) {
1128
+ this.init(context, infraConfig);
1129
+ }
1130
+ start(session) {
1131
+ this.session = sinkSessionMetadataFromContext(this.getBaseContext(), session);
1132
+ }
1133
+ Start(session) {
1134
+ this.start(session);
1135
+ }
1136
+ async saveRealtimeStats(scenarioStats) {
1137
+ await this.persistEvents(createRealtimeStatsEvents(this.getSession(), scenarioStats));
1138
+ }
1139
+ async SaveRealtimeStats(scenarioStats) {
1140
+ await this.saveRealtimeStats(scenarioStats);
1141
+ }
1142
+ async saveRealtimeMetrics(metrics) {
1143
+ await this.persistEvents(createRealtimeMetricEvents(this.getSession(), metrics));
1144
+ }
1145
+ async SaveRealtimeMetrics(metrics) {
1146
+ await this.saveRealtimeMetrics(metrics);
1147
+ }
1148
+ async saveFinalStats(result) {
1149
+ await this.persistEvents(createFinalStatsEvents(this.getSession(), result));
1150
+ }
1151
+ async SaveFinalStats(result) {
1152
+ await this.saveFinalStats(result);
1153
+ }
1154
+ async saveRunResult(result) {
1155
+ await this.persistEvents(createRunResultEvents(this.getSession(), result));
1156
+ }
1157
+ async SaveRunResult(result) {
1158
+ await this.saveRunResult(result);
1159
+ }
1160
+ stop() {
1161
+ this.session = null;
1162
+ }
1163
+ Stop() {
1164
+ this.stop();
1165
+ }
1166
+ Dispose() {
1167
+ this.baseContext = null;
1168
+ this.stop();
1169
+ }
1170
+ getBaseContext() {
1171
+ if (!this.baseContext) {
1172
+ throw new Error(`${this.sinkName} has not been initialized.`);
1173
+ }
1174
+ return this.baseContext;
1175
+ }
1176
+ getSession() {
1177
+ if (!this.session) {
1178
+ throw new Error(`${this.sinkName} has not been started.`);
1179
+ }
1180
+ return this.session;
1181
+ }
1182
+ async persistEvents(events) {
1183
+ if (!events.length) {
1184
+ return;
1185
+ }
1186
+ const envelopes = [
1187
+ ...events.map((event) => JSON.stringify(buildSplunkLogEnvelope(this.options, event))),
1188
+ ...createReportingSinkMetricPoints(this.sinkName, events, {}).map((point) => JSON.stringify(buildSplunkMetricEnvelope(this.options, point)))
1189
+ ];
1190
+ if (!envelopes.length) {
1191
+ return;
1192
+ }
1193
+ await postWithTimeout(this.fetchImpl, `${trimTrailingSlashes(this.options.baseUrl)}${normalizePath(this.options.eventEndpointPath)}`, {
1194
+ method: "POST",
1195
+ headers: {
1196
+ "Content-Type": "application/json",
1197
+ Authorization: `Splunk ${this.options.token}`
1198
+ },
1199
+ body: envelopes.join("\n")
1200
+ }, resolveTimeoutMs(this.options.timeoutSeconds, this.options.timeoutMs), "SplunkReportingSink");
1201
+ }
1202
+ }
1203
+ exports.SplunkReportingSink = SplunkReportingSink;
1204
+ class OtelCollectorReportingSink {
1205
+ constructor(options = {}) {
1206
+ this.sinkName = "otel-collector";
1207
+ this.SinkName = "otel-collector";
1208
+ this.licenseFeature = "extensions.reporting_sinks.otel_collector";
1209
+ this.LicenseFeature = "extensions.reporting_sinks.otel_collector";
1210
+ this.baseContext = null;
1211
+ this.session = null;
1212
+ const source = asRecord(options);
1213
+ this.options = {
1214
+ configurationSectionPath: optionString(source, "configurationSectionPath", "ConfigurationSectionPath").trim() || DEFAULT_OTEL_COLLECTOR_CONFIGURATION_SECTION_PATH,
1215
+ baseUrl: optionString(source, "baseUrl", "BaseUrl").trim() || "",
1216
+ logsEndpointPath: optionString(source, "logsEndpointPath", "LogsEndpointPath").trim() || "/v1/logs",
1217
+ metricsEndpointPath: optionString(source, "metricsEndpointPath", "MetricsEndpointPath").trim() || "/v1/metrics",
1218
+ timeoutSeconds: Number.isFinite(optionNumber(source, "timeoutSeconds", "TimeoutSeconds"))
1219
+ ? Number(optionNumber(source, "timeoutSeconds", "TimeoutSeconds"))
1220
+ : 30,
1221
+ timeoutMs: Number.isFinite(optionNumber(source, "timeoutMs", "TimeoutMs"))
1222
+ ? Number(optionNumber(source, "timeoutMs", "TimeoutMs"))
1223
+ : undefined,
1224
+ headers: normalizeStringMap(optionRecord(source, "headers", "Headers")),
1225
+ staticResourceAttributes: normalizeStringMap(optionRecord(source, "staticResourceAttributes", "StaticResourceAttributes"))
1226
+ };
1227
+ this.fetchImpl = pickRecordValue(source, "fetchImpl", "FetchImpl") ?? fetch;
1228
+ }
1229
+ init(context, infraConfig) {
1230
+ this.baseContext = cloneBaseContext(context);
1231
+ mergeOtelCollectorOptions(this.options, readOtelCollectorOptionsFromConfig(infraConfig, this.options.configurationSectionPath));
1232
+ if (!this.options.baseUrl.trim()) {
1233
+ throw new Error("OtelCollectorReportingSink requires BaseUrl.");
1234
+ }
1235
+ }
1236
+ Init(context, infraConfig) {
1237
+ this.init(context, infraConfig);
1238
+ }
1239
+ start(session) {
1240
+ this.session = sinkSessionMetadataFromContext(this.getBaseContext(), session);
1241
+ }
1242
+ Start(session) {
1243
+ this.start(session);
1244
+ }
1245
+ async saveRealtimeStats(scenarioStats) {
1246
+ await this.persistEvents(createRealtimeStatsEvents(this.getSession(), scenarioStats));
1247
+ }
1248
+ async SaveRealtimeStats(scenarioStats) {
1249
+ await this.saveRealtimeStats(scenarioStats);
1250
+ }
1251
+ async saveRealtimeMetrics(metrics) {
1252
+ await this.persistEvents(createRealtimeMetricEvents(this.getSession(), metrics));
1253
+ }
1254
+ async SaveRealtimeMetrics(metrics) {
1255
+ await this.saveRealtimeMetrics(metrics);
1256
+ }
1257
+ async saveFinalStats(result) {
1258
+ await this.persistEvents(createFinalStatsEvents(this.getSession(), result));
1259
+ }
1260
+ async SaveFinalStats(result) {
1261
+ await this.saveFinalStats(result);
1262
+ }
1263
+ async saveRunResult(result) {
1264
+ await this.persistEvents(createRunResultEvents(this.getSession(), result));
1265
+ }
1266
+ async SaveRunResult(result) {
1267
+ await this.saveRunResult(result);
1268
+ }
1269
+ stop() {
1270
+ this.session = null;
1271
+ }
1272
+ Stop() {
1273
+ this.stop();
1274
+ }
1275
+ Dispose() {
1276
+ this.baseContext = null;
1277
+ this.stop();
1278
+ }
1279
+ getBaseContext() {
1280
+ if (!this.baseContext) {
1281
+ throw new Error(`${this.sinkName} has not been initialized.`);
1282
+ }
1283
+ return this.baseContext;
1284
+ }
1285
+ getSession() {
1286
+ if (!this.session) {
1287
+ throw new Error(`${this.sinkName} has not been started.`);
1288
+ }
1289
+ return this.session;
1290
+ }
1291
+ async persistEvents(events) {
1292
+ if (!events.length) {
1293
+ return;
1294
+ }
1295
+ const headers = {
1296
+ "Content-Type": "application/json",
1297
+ ...this.options.headers
1298
+ };
1299
+ const resourceAttributes = buildOtelResourceAttributes(this.sinkName, this.options.staticResourceAttributes);
1300
+ const logPayload = {
1301
+ resourceLogs: [{
1302
+ resource: { attributes: resourceAttributes },
1303
+ scopeLogs: [{
1304
+ scope: { name: "loadstrike.reporting", version: "1.0.0" },
1305
+ logRecords: events.map((event) => buildOtelLogRecord(event))
1306
+ }]
1307
+ }]
1308
+ };
1309
+ await postWithTimeout(this.fetchImpl, `${trimTrailingSlashes(this.options.baseUrl)}${normalizePath(this.options.logsEndpointPath)}`, {
1310
+ method: "POST",
1311
+ headers,
1312
+ body: JSON.stringify(logPayload)
1313
+ }, resolveTimeoutMs(this.options.timeoutSeconds, this.options.timeoutMs), "OtelCollectorReportingSink logs");
1314
+ const metricPoints = createReportingSinkMetricPoints(this.sinkName, events, {});
1315
+ if (!metricPoints.length) {
1316
+ return;
1317
+ }
1318
+ const grouped = new Map();
1319
+ for (const point of metricPoints) {
1320
+ const key = `${point.metricName}|${point.metricKind}|${point.unitOfMeasure ?? ""}`;
1321
+ const values = grouped.get(key) ?? [];
1322
+ values.push(point);
1323
+ grouped.set(key, values);
1324
+ }
1325
+ const metricPayload = {
1326
+ resourceMetrics: [{
1327
+ resource: { attributes: resourceAttributes },
1328
+ scopeMetrics: [{
1329
+ scope: { name: "loadstrike.reporting", version: "1.0.0" },
1330
+ metrics: Array.from(grouped.values()).map((points) => buildOtelMetric(points[0], points))
1331
+ }]
1332
+ }]
1333
+ };
1334
+ await postWithTimeout(this.fetchImpl, `${trimTrailingSlashes(this.options.baseUrl)}${normalizePath(this.options.metricsEndpointPath)}`, {
1335
+ method: "POST",
1336
+ headers,
1337
+ body: JSON.stringify(metricPayload)
1338
+ }, resolveTimeoutMs(this.options.timeoutSeconds, this.options.timeoutMs), "OtelCollectorReportingSink metrics");
1339
+ }
1340
+ }
1341
+ exports.OtelCollectorReportingSink = OtelCollectorReportingSink;
1342
+ function createRealtimeStatsEvents(session, scenarios) {
1343
+ const occurredUtc = new Date();
1344
+ const events = [];
1345
+ for (const scenario of scenarios) {
1346
+ events.push(createScenarioEvent(session, occurredUtc, "realtime", scenario));
1347
+ events.push(...createStepEvents(session, occurredUtc, "realtime", scenario));
1348
+ events.push(...createStatusCodeEvents(session, occurredUtc, "realtime", scenario.scenarioName, null, "ok", scenario.ok.statusCodes));
1349
+ events.push(...createStatusCodeEvents(session, occurredUtc, "realtime", scenario.scenarioName, null, "fail", scenario.fail.statusCodes));
1350
+ }
1351
+ return events;
1352
+ }
1353
+ function createRealtimeMetricEvents(session, metrics) {
1354
+ return createMetricEvents(session, metrics, "realtime");
1355
+ }
1356
+ function createFinalStatsEvents(session, stats) {
1357
+ const occurredUtc = new Date();
1358
+ const events = [createNodeSummaryEvent(session, occurredUtc, stats)];
1359
+ for (const scenario of stats.scenarioStats) {
1360
+ events.push(createScenarioEvent(session, occurredUtc, "final", scenario));
1361
+ events.push(...createStepEvents(session, occurredUtc, "final", scenario));
1362
+ events.push(...createStatusCodeEvents(session, occurredUtc, "final", scenario.scenarioName, null, "ok", scenario.ok.statusCodes));
1363
+ events.push(...createStatusCodeEvents(session, occurredUtc, "final", scenario.scenarioName, null, "fail", scenario.fail.statusCodes));
1364
+ }
1365
+ events.push(...createMetricEvents(session, stats.metrics, "final", occurredUtc));
1366
+ for (const threshold of stats.thresholds) {
1367
+ events.push(createThresholdEvent(session, occurredUtc, threshold));
1368
+ }
1369
+ for (const plugin of stats.pluginsData) {
1370
+ events.push(...createPluginEvents(session, occurredUtc, plugin));
1371
+ }
1372
+ return events;
1373
+ }
1374
+ function createRunResultEvents(session, result) {
1375
+ const occurredUtc = new Date();
1376
+ const events = [
1377
+ createReportingEvent(session, occurredUtc, "run.result.final", null, null, {
1378
+ phase: "final",
1379
+ entity: "run-result"
1380
+ }, {
1381
+ started_utc: result.startedUtc,
1382
+ completed_utc: result.completedUtc,
1383
+ report_file_count: result.reportFiles.length,
1384
+ disabled_sink_count: result.disabledSinks.length,
1385
+ sink_error_count: result.sinkErrors.length
1386
+ })
1387
+ ];
1388
+ for (const reportFile of result.reportFiles) {
1389
+ events.push(createReportingEvent(session, occurredUtc, "run.report.final", null, null, {
1390
+ phase: "final",
1391
+ entity: "run-report"
1392
+ }, {
1393
+ path: reportFile
1394
+ }));
1395
+ }
1396
+ for (const disabledSink of result.disabledSinks) {
1397
+ events.push(createReportingEvent(session, occurredUtc, "run.disabled-sink.final", null, null, {
1398
+ phase: "final",
1399
+ entity: "disabled-sink",
1400
+ sink_name: disabledSink
1401
+ }, {
1402
+ sink_name: disabledSink
1403
+ }));
1404
+ }
1405
+ for (const sinkError of result.sinkErrors) {
1406
+ events.push(createReportingEvent(session, occurredUtc, "run.sink-error.final", null, null, {
1407
+ phase: "final",
1408
+ entity: "sink-error",
1409
+ sink_name: sinkError.sinkName,
1410
+ sink_phase: sinkError.phase
1411
+ }, {
1412
+ sink_name: sinkError.sinkName,
1413
+ phase: sinkError.phase,
1414
+ message: sinkError.message,
1415
+ attempts: sinkError.attempts
1416
+ }));
1417
+ }
1418
+ return events;
1419
+ }
1420
+ function createNodeSummaryEvent(session, occurredUtc, stats) {
1421
+ return createReportingEvent(session, occurredUtc, "test.final", null, null, {
1422
+ phase: "final",
1423
+ entity: "test"
1424
+ }, {
1425
+ all_request_count: stats.allRequestCount,
1426
+ all_ok_count: stats.allOkCount,
1427
+ all_fail_count: stats.allFailCount,
1428
+ all_bytes: stats.allBytes,
1429
+ duration_ms: stats.durationMs,
1430
+ scenario_count: stats.scenarioStats.length,
1431
+ threshold_count: stats.thresholds.length
1432
+ });
1433
+ }
1434
+ function createScenarioEvent(session, occurredUtc, phase, scenario) {
1435
+ const fields = {
1436
+ all_request_count: scenario.allRequestCount,
1437
+ all_ok_count: scenario.allOkCount,
1438
+ all_fail_count: scenario.allFailCount,
1439
+ all_bytes: scenario.allBytes,
1440
+ duration_ms: scenario.durationMs,
1441
+ sort_index: scenario.sortIndex,
1442
+ operation: scenario.currentOperation,
1443
+ load_simulation_name: scenario.loadSimulationStats.simulationName,
1444
+ load_simulation_value: scenario.loadSimulationStats.value
1445
+ };
1446
+ addMeasurementFields(fields, "ok", scenario.ok);
1447
+ addMeasurementFields(fields, "fail", scenario.fail);
1448
+ return createReportingEvent(session, occurredUtc, `scenario.${phase}`, scenario.scenarioName, null, {
1449
+ phase,
1450
+ entity: "scenario"
1451
+ }, fields);
1452
+ }
1453
+ function createStepEvents(session, occurredUtc, phase, scenario) {
1454
+ const events = [];
1455
+ for (const step of scenario.stepStats) {
1456
+ const fields = {
1457
+ sort_index: step.sortIndex
1458
+ };
1459
+ addMeasurementFields(fields, "ok", step.ok);
1460
+ addMeasurementFields(fields, "fail", step.fail);
1461
+ events.push(createReportingEvent(session, occurredUtc, `step.${phase}`, scenario.scenarioName, step.stepName, {
1462
+ phase,
1463
+ entity: "step"
1464
+ }, fields));
1465
+ events.push(...createStatusCodeEvents(session, occurredUtc, phase, scenario.scenarioName, step.stepName, "ok", step.ok.statusCodes));
1466
+ events.push(...createStatusCodeEvents(session, occurredUtc, phase, scenario.scenarioName, step.stepName, "fail", step.fail.statusCodes));
1467
+ }
1468
+ return events;
1469
+ }
1470
+ function createStatusCodeEvents(session, occurredUtc, phase, scenarioName, stepName, resultKind, statusCodes) {
1471
+ return statusCodes.map((statusCode) => createReportingEvent(session, occurredUtc, `status-code.${phase}`, scenarioName, stepName, {
1472
+ phase,
1473
+ entity: stepName ? "step-status-code" : "scenario-status-code",
1474
+ result_kind: resultKind,
1475
+ status_code: statusCode.statusCode
1476
+ }, {
1477
+ count: statusCode.count,
1478
+ percent: statusCode.percent,
1479
+ is_error: statusCode.isError,
1480
+ message: statusCode.message
1481
+ }));
1482
+ }
1483
+ function createMetricEvent(session, occurredUtc, eventType, scenarioName, tags, fields) {
1484
+ return createReportingEvent(session, occurredUtc, eventType, scenarioName, null, tags, fields);
1485
+ }
1486
+ function createMetricEvents(session, metrics, phase, occurredUtc = new Date()) {
1487
+ const events = [];
1488
+ for (const counter of metrics?.counters ?? []) {
1489
+ events.push(createMetricEvent(session, occurredUtc, phase === "final" ? "metric.counter.final" : "metric.counter", counter.scenarioName, {
1490
+ phase,
1491
+ metric_name: counter.metricName,
1492
+ metric_type: "counter"
1493
+ }, {
1494
+ value: counter.value,
1495
+ unit_of_measure: counter.unitOfMeasure,
1496
+ duration_ms: metrics?.durationMs
1497
+ }));
1498
+ }
1499
+ for (const gauge of metrics?.gauges ?? []) {
1500
+ events.push(createMetricEvent(session, occurredUtc, phase === "final" ? "metric.gauge.final" : "metric.gauge", gauge.scenarioName, {
1501
+ phase,
1502
+ metric_name: gauge.metricName,
1503
+ metric_type: "gauge"
1504
+ }, {
1505
+ value: gauge.value,
1506
+ unit_of_measure: gauge.unitOfMeasure,
1507
+ duration_ms: metrics?.durationMs
1508
+ }));
1509
+ }
1510
+ return events;
1511
+ }
1512
+ function createThresholdEvent(session, occurredUtc, threshold) {
1513
+ return createReportingEvent(session, occurredUtc, "threshold.final", threshold.scenarioName, cleanNullableText(threshold.stepName), {
1514
+ phase: "final",
1515
+ entity: "threshold"
1516
+ }, {
1517
+ check_expression: threshold.checkExpression,
1518
+ is_failed: threshold.isFailed,
1519
+ error_count: threshold.errorCount,
1520
+ exception_message: threshold.exceptionMessage ?? null
1521
+ });
1522
+ }
1523
+ function createPluginEvents(session, occurredUtc, plugin) {
1524
+ const events = [];
1525
+ for (const hint of plugin.hints) {
1526
+ if (!String(hint ?? "").trim()) {
1527
+ continue;
1528
+ }
1529
+ events.push(createReportingEvent(session, occurredUtc, "plugin.hint.final", null, null, {
1530
+ phase: "final",
1531
+ entity: "plugin-hint",
1532
+ plugin_name: plugin.pluginName
1533
+ }, {
1534
+ hint
1535
+ }));
1536
+ }
1537
+ for (const table of plugin.tables) {
1538
+ for (let index = 0; index < table.rows.length; index += 1) {
1539
+ const row = table.rows[index] ?? {};
1540
+ const fields = {
1541
+ row_index: index
1542
+ };
1543
+ for (const [key, value] of Object.entries(row)) {
1544
+ fields[normalizePluginFieldName(key)] = normalizePluginFieldValue(value);
1545
+ }
1546
+ events.push(createReportingEvent(session, occurredUtc, "plugin.table.final", tryReadText(row, "Scenario"), tryReadText(row, "Step"), {
1547
+ phase: "final",
1548
+ entity: "plugin-table-row",
1549
+ plugin_name: plugin.pluginName,
1550
+ table_name: table.tableName
1551
+ }, fields));
1552
+ }
1553
+ }
1554
+ return events;
1555
+ }
1556
+ function createReportingEvent(session, occurredUtc, eventType, scenarioName, stepName, tags, fields) {
1557
+ const mergedTags = {
1558
+ event_type: eventType,
1559
+ session_id: session.sessionId,
1560
+ test_suite: session.testSuite.trim() ? session.testSuite : "default",
1561
+ test_name: session.testName.trim() ? session.testName : "loadstrike",
1562
+ cluster_id: session.clusterId.trim() ? session.clusterId : "default",
1563
+ node_type: session.nodeType,
1564
+ machine_name: session.machineName,
1565
+ started_utc: session.startedUtc
1566
+ };
1567
+ if (scenarioName) {
1568
+ mergedTags.scenario_name = scenarioName;
1569
+ }
1570
+ if (stepName) {
1571
+ mergedTags.step_name = stepName;
1572
+ }
1573
+ for (const [key, value] of Object.entries(tags)) {
1574
+ if (String(value ?? "").trim()) {
1575
+ mergedTags[key] = String(value);
1576
+ }
1577
+ }
1578
+ if (mergedTags.phase?.toLowerCase() === "final") {
1579
+ mergedTags.completed_utc = occurredUtc.toISOString();
1580
+ }
1581
+ const eventFields = {
1582
+ started_utc: session.startedUtc,
1583
+ ...deepCloneRecord(fields)
1584
+ };
1585
+ if (mergedTags.phase?.toLowerCase() === "final") {
1586
+ eventFields.completed_utc = occurredUtc.toISOString();
1587
+ }
1588
+ return {
1589
+ eventType,
1590
+ occurredUtc,
1591
+ sessionId: session.sessionId,
1592
+ testSuite: session.testSuite,
1593
+ testName: session.testName,
1594
+ clusterId: session.clusterId,
1595
+ nodeType: session.nodeType,
1596
+ machineName: session.machineName,
1597
+ scenarioName,
1598
+ stepName,
1599
+ tags: mergedTags,
1600
+ fields: eventFields
1601
+ };
1602
+ }
1603
+ function addMeasurementFields(fields, prefix, measurement) {
1604
+ fields[`${prefix}_request_count`] = measurement.request.count;
1605
+ fields[`${prefix}_request_percent`] = measurement.request.percent;
1606
+ fields[`${prefix}_request_rps`] = measurement.request.rps;
1607
+ fields[`${prefix}_latency_min_ms`] = measurement.latency.minMs;
1608
+ fields[`${prefix}_latency_mean_ms`] = measurement.latency.meanMs;
1609
+ fields[`${prefix}_latency_max_ms`] = measurement.latency.maxMs;
1610
+ fields[`${prefix}_latency_p50_ms`] = measurement.latency.percent50;
1611
+ fields[`${prefix}_latency_p75_ms`] = measurement.latency.percent75;
1612
+ fields[`${prefix}_latency_p95_ms`] = measurement.latency.percent95;
1613
+ fields[`${prefix}_latency_p99_ms`] = measurement.latency.percent99;
1614
+ fields[`${prefix}_latency_std_dev`] = measurement.latency.stdDev;
1615
+ fields[`${prefix}_latency_le_800_count`] = measurement.latency.latencyCount.lessOrEq800;
1616
+ fields[`${prefix}_latency_gt_800_lt_1200_count`] = measurement.latency.latencyCount.more800Less1200;
1617
+ fields[`${prefix}_latency_ge_1200_count`] = measurement.latency.latencyCount.moreOrEq1200;
1618
+ fields[`${prefix}_bytes_all`] = measurement.dataTransfer.allBytes;
1619
+ fields[`${prefix}_bytes_min`] = measurement.dataTransfer.minBytes;
1620
+ fields[`${prefix}_bytes_mean`] = measurement.dataTransfer.meanBytes;
1621
+ fields[`${prefix}_bytes_max`] = measurement.dataTransfer.maxBytes;
1622
+ fields[`${prefix}_bytes_p50`] = measurement.dataTransfer.percent50;
1623
+ fields[`${prefix}_bytes_p75`] = measurement.dataTransfer.percent75;
1624
+ fields[`${prefix}_bytes_p95`] = measurement.dataTransfer.percent95;
1625
+ fields[`${prefix}_bytes_p99`] = measurement.dataTransfer.percent99;
1626
+ fields[`${prefix}_bytes_std_dev`] = measurement.dataTransfer.stdDev;
1627
+ fields[`${prefix}_status_code_count`] = measurement.statusCodes.length;
1628
+ }
1629
+ function buildInfluxWriteUri(options) {
1630
+ const query = new URLSearchParams({
1631
+ org: options.organization,
1632
+ bucket: options.bucket,
1633
+ precision: "ns"
1634
+ });
1635
+ return `${trimTrailingSlashes(options.baseUrl)}${normalizePath(options.writeEndpointPath)}?${query.toString()}`;
1636
+ }
1637
+ function buildInfluxLineProtocol(measurementName, staticTags, events) {
1638
+ const lines = [];
1639
+ for (const event of events) {
1640
+ const fields = formatInfluxFields(event.fields);
1641
+ if (!fields) {
1642
+ continue;
1643
+ }
1644
+ const mergedTags = mergeInfluxTags(event.tags, staticTags);
1645
+ const tagPart = Object.entries(mergedTags)
1646
+ .sort(([left], [right]) => left.localeCompare(right))
1647
+ .map(([key, value]) => `${escapeInfluxToken(key)}=${escapeInfluxToken(value)}`)
1648
+ .join(",");
1649
+ lines.push(`${escapeInfluxToken(measurementName)}${tagPart ? `,${tagPart}` : ""} ${fields} ${toUnixNanoseconds(event.occurredUtc)}`);
1650
+ }
1651
+ return lines.join("\n");
1652
+ }
1653
+ function isMetricEventType(eventType) {
1654
+ const normalized = String(eventType ?? "").toLowerCase();
1655
+ return normalized.startsWith("metric.counter") || normalized.startsWith("metric.gauge");
1656
+ }
1657
+ function formatInfluxFields(fields) {
1658
+ const parts = [];
1659
+ for (const [key, value] of Object.entries(fields).sort(([left], [right]) => left.localeCompare(right))) {
1660
+ const formatted = formatInfluxFieldValue(value);
1661
+ if (formatted == null) {
1662
+ continue;
1663
+ }
1664
+ parts.push(`${escapeInfluxToken(key)}=${formatted}`);
1665
+ }
1666
+ return parts.join(",");
1667
+ }
1668
+ function formatInfluxFieldValue(value) {
1669
+ if (value == null) {
1670
+ return null;
1671
+ }
1672
+ if (typeof value === "string") {
1673
+ return `"${replaceLiteral(value, "\\", "\\\\").split("\"").join("\\\"")}"`;
1674
+ }
1675
+ if (typeof value === "boolean") {
1676
+ return value ? "true" : "false";
1677
+ }
1678
+ if (typeof value === "number") {
1679
+ if (!Number.isFinite(value)) {
1680
+ return null;
1681
+ }
1682
+ return Number.isInteger(value) ? `${value}i` : String(value);
1683
+ }
1684
+ if (typeof value === "bigint") {
1685
+ return `${value}i`;
1686
+ }
1687
+ return `"${replaceLiteral(String(value), "\\", "\\\\").split("\"").join("\\\"")}"`;
1688
+ }
1689
+ function mergeInfluxTags(eventTags, staticTags) {
1690
+ const merged = {
1691
+ source: "loadstrike",
1692
+ sink: "influxdb"
1693
+ };
1694
+ for (const [key, value] of Object.entries(staticTags)) {
1695
+ if (String(value ?? "").trim()) {
1696
+ merged[key] = String(value);
1697
+ }
1698
+ }
1699
+ for (const [key, value] of Object.entries(eventTags)) {
1700
+ if (String(value ?? "").trim()) {
1701
+ merged[key] = String(value);
1702
+ }
1703
+ }
1704
+ return merged;
1705
+ }
1706
+ function groupGrafanaLokiStreams(staticLabels, events) {
1707
+ const streams = new Map();
1708
+ for (const event of events) {
1709
+ const labels = buildGrafanaLokiLabels(staticLabels, event);
1710
+ const key = Object.entries(labels)
1711
+ .sort(([left], [right]) => left.localeCompare(right))
1712
+ .map(([labelKey, labelValue]) => `${labelKey}=${labelValue}`)
1713
+ .join("|");
1714
+ const entry = streams.get(key) ?? { stream: labels, values: [] };
1715
+ entry.values.push([
1716
+ toUnixNanoseconds(event.occurredUtc),
1717
+ toReportingSinkEventJson(event)
1718
+ ]);
1719
+ streams.set(key, entry);
1720
+ }
1721
+ return Array.from(streams.values()).map((entry) => ({
1722
+ stream: entry.stream,
1723
+ values: entry.values.sort((left, right) => Number(left[0]) - Number(right[0]))
1724
+ }));
1725
+ }
1726
+ function buildGrafanaLokiLabels(staticLabels, event) {
1727
+ const labels = {
1728
+ source: "loadstrike",
1729
+ sink: "grafana_loki",
1730
+ event_type: sanitizeGrafanaLabelValue(event.eventType),
1731
+ test_suite: sanitizeGrafanaLabelValue(event.testSuite.trim() ? event.testSuite : "default"),
1732
+ test_name: sanitizeGrafanaLabelValue(event.testName.trim() ? event.testName : "loadstrike")
1733
+ };
1734
+ for (const [key, value] of Object.entries(staticLabels)) {
1735
+ if (String(value ?? "").trim()) {
1736
+ labels[sanitizeGrafanaLabelKey(key)] = sanitizeGrafanaLabelValue(String(value));
1737
+ }
1738
+ }
1739
+ return labels;
1740
+ }
1741
+ function mergeTimescaleTags(eventTags, staticTags) {
1742
+ const merged = {
1743
+ source: "loadstrike",
1744
+ sink: "timescaledb"
1745
+ };
1746
+ for (const [key, value] of Object.entries(staticTags)) {
1747
+ if (String(value ?? "").trim()) {
1748
+ merged[key] = String(value);
1749
+ }
1750
+ }
1751
+ for (const [key, value] of Object.entries(eventTags)) {
1752
+ if (String(value ?? "").trim()) {
1753
+ merged[key] = String(value);
1754
+ }
1755
+ }
1756
+ return merged;
1757
+ }
1758
+ function toReportingSinkEventJson(event) {
1759
+ return JSON.stringify({
1760
+ EventType: event.eventType,
1761
+ occurredUtc: event.occurredUtc.toISOString(),
1762
+ SessionId: event.sessionId,
1763
+ TestSuite: event.testSuite,
1764
+ TestName: event.testName,
1765
+ ClusterId: event.clusterId,
1766
+ NodeType: event.nodeType,
1767
+ MachineName: event.machineName,
1768
+ ScenarioName: event.scenarioName,
1769
+ StepName: event.stepName,
1770
+ Tags: event.tags,
1771
+ Fields: event.fields
1772
+ });
1773
+ }
1774
+ function createReportingSinkMetricPoints(sinkName, events, staticTags) {
1775
+ const points = [];
1776
+ for (const event of events) {
1777
+ const metricTags = buildMetricProjectionTags(sinkName, event, staticTags);
1778
+ const normalizedEventType = event.eventType.toLowerCase();
1779
+ const isCounterEvent = normalizedEventType.startsWith("metric.counter");
1780
+ const isGaugeEvent = normalizedEventType.startsWith("metric.gauge");
1781
+ const metricNameTag = event.tags.metric_name;
1782
+ const unitOfMeasure = typeof event.fields.unit_of_measure === "string" && event.fields.unit_of_measure.trim()
1783
+ ? event.fields.unit_of_measure.trim()
1784
+ : undefined;
1785
+ if ((isCounterEvent || isGaugeEvent) && isFiniteNumberLike(event.fields.value)) {
1786
+ points.push({
1787
+ metricName: buildCustomMetricName(metricNameTag),
1788
+ metricKind: isCounterEvent ? "count" : "gauge",
1789
+ occurredUtc: event.occurredUtc,
1790
+ value: toNumericValue(event.fields.value),
1791
+ unitOfMeasure,
1792
+ tags: metricTags
1793
+ });
1794
+ }
1795
+ for (const [fieldName, fieldValue] of Object.entries(event.fields)) {
1796
+ if (fieldName.toLowerCase() === "unit_of_measure") {
1797
+ continue;
1798
+ }
1799
+ if ((isCounterEvent || isGaugeEvent) && fieldName.toLowerCase() === "value") {
1800
+ continue;
1801
+ }
1802
+ if (!isFiniteNumberLike(fieldValue)) {
1803
+ continue;
1804
+ }
1805
+ points.push({
1806
+ metricName: buildEventMetricName(event.eventType, fieldName),
1807
+ metricKind: "gauge",
1808
+ occurredUtc: event.occurredUtc,
1809
+ value: toNumericValue(fieldValue),
1810
+ unitOfMeasure: inferUnitOfMeasure(fieldName),
1811
+ tags: metricTags
1812
+ });
1813
+ }
1814
+ }
1815
+ return points;
1816
+ }
1817
+ function buildMetricProjectionTags(sinkName, event, staticTags) {
1818
+ const merged = {
1819
+ source: "loadstrike",
1820
+ sink: sinkName,
1821
+ event_type: event.eventType,
1822
+ session_id: event.sessionId,
1823
+ test_suite: event.testSuite.trim() ? event.testSuite : "default",
1824
+ test_name: event.testName.trim() ? event.testName : "loadstrike",
1825
+ cluster_id: event.clusterId.trim() ? event.clusterId : "default",
1826
+ node_type: event.nodeType.trim() ? event.nodeType : "SingleNode",
1827
+ machine_name: event.machineName.trim() ? event.machineName : "local-machine"
1828
+ };
1829
+ if (event.scenarioName) {
1830
+ merged.scenario_name = event.scenarioName;
1831
+ }
1832
+ if (event.stepName) {
1833
+ merged.step_name = event.stepName;
1834
+ }
1835
+ for (const [key, value] of Object.entries(staticTags)) {
1836
+ if (String(value ?? "").trim()) {
1837
+ merged[key] = String(value);
1838
+ }
1839
+ }
1840
+ for (const [key, value] of Object.entries(event.tags)) {
1841
+ if (String(value ?? "").trim()) {
1842
+ merged[key] = String(value);
1843
+ }
1844
+ }
1845
+ return merged;
1846
+ }
1847
+ function buildCustomMetricName(metricName) {
1848
+ const normalized = normalizeMetricPath(metricName);
1849
+ return normalized ? `loadstrike.metric.${normalized}` : "loadstrike.metric.value";
1850
+ }
1851
+ function buildEventMetricName(eventType, fieldName) {
1852
+ return `loadstrike.${normalizeMetricPath(eventType)}.${normalizeMetricPath(fieldName)}`;
1853
+ }
1854
+ function normalizeMetricPath(value) {
1855
+ const segments = String(value ?? "")
1856
+ .split(".")
1857
+ .map((entry) => normalizeMetricSegment(entry))
1858
+ .filter((entry) => entry.length > 0);
1859
+ return segments.length ? segments.join(".") : "value";
1860
+ }
1861
+ function normalizeMetricSegment(value) {
1862
+ let normalized = "";
1863
+ let previousUnderscore = false;
1864
+ for (const char of String(value ?? "")) {
1865
+ if (/[A-Za-z0-9]/.test(char)) {
1866
+ normalized += char.toLowerCase();
1867
+ previousUnderscore = false;
1868
+ }
1869
+ else if (!previousUnderscore) {
1870
+ normalized += "_";
1871
+ previousUnderscore = true;
1872
+ }
1873
+ }
1874
+ return normalized.replace(/^_+|_+$/g, "");
1875
+ }
1876
+ function isFiniteNumberLike(value) {
1877
+ return (typeof value === "number" && Number.isFinite(value)) || typeof value === "bigint";
1878
+ }
1879
+ function toNumericValue(value) {
1880
+ if (typeof value === "bigint") {
1881
+ return Number(value);
1882
+ }
1883
+ return Number(value);
1884
+ }
1885
+ function inferUnitOfMeasure(fieldName) {
1886
+ const normalized = fieldName.toLowerCase();
1887
+ if (normalized.endsWith("_ms")) {
1888
+ return "ms";
1889
+ }
1890
+ if (normalized.endsWith("_bytes") || normalized.endsWith("_byte")) {
1891
+ return "bytes";
1892
+ }
1893
+ return undefined;
1894
+ }
1895
+ function buildDatadogLogEntry(options, event) {
1896
+ const attributes = {
1897
+ intake_type: "log",
1898
+ event_type: event.eventType,
1899
+ occurred_utc: event.occurredUtc.toISOString(),
1900
+ session_id: event.sessionId,
1901
+ test_suite: event.testSuite,
1902
+ test_name: event.testName,
1903
+ cluster_id: event.clusterId,
1904
+ node_type: event.nodeType,
1905
+ machine_name: event.machineName,
1906
+ scenario_name: event.scenarioName,
1907
+ step_name: event.stepName,
1908
+ tags: event.tags,
1909
+ fields: event.fields
1910
+ };
1911
+ for (const [key, value] of Object.entries(options.staticAttributes)) {
1912
+ if (String(value ?? "").trim()) {
1913
+ attributes[key] = value;
1914
+ }
1915
+ }
1916
+ return {
1917
+ message: toReportingSinkEventJson(event),
1918
+ ddsource: options.source.trim() || "loadstrike",
1919
+ service: options.service.trim() || "loadstrike",
1920
+ hostname: options.host.trim() || event.machineName,
1921
+ status: "info",
1922
+ ddtags: buildDatadogTagString(event.tags, options.staticTags),
1923
+ attributes
1924
+ };
1925
+ }
1926
+ function buildDatadogTagString(eventTags, staticTags) {
1927
+ const tags = {
1928
+ source: "loadstrike"
1929
+ };
1930
+ for (const [key, value] of Object.entries(staticTags)) {
1931
+ if (String(value ?? "").trim()) {
1932
+ tags[key] = String(value);
1933
+ }
1934
+ }
1935
+ for (const [key, value] of Object.entries(eventTags)) {
1936
+ if (String(value ?? "").trim()) {
1937
+ tags[key] = String(value);
1938
+ }
1939
+ }
1940
+ return Object.entries(tags)
1941
+ .sort(([left], [right]) => left.localeCompare(right))
1942
+ .map(([key, value]) => `${sanitizeDatadogTagComponent(key)}:${sanitizeDatadogTagComponent(value)}`)
1943
+ .join(",");
1944
+ }
1945
+ function sanitizeDatadogTagComponent(value) {
1946
+ return String(value ?? "")
1947
+ .trim()
1948
+ .split(":").join("_")
1949
+ .split(",").join("_")
1950
+ .split("|").join("_");
1951
+ }
1952
+ function buildSplunkBaseFields(options, intakeType) {
1953
+ const fields = {
1954
+ intake_type: intakeType
1955
+ };
1956
+ for (const [key, value] of Object.entries(options.staticFields)) {
1957
+ if (String(value ?? "").trim()) {
1958
+ fields[key] = String(value);
1959
+ }
1960
+ }
1961
+ return fields;
1962
+ }
1963
+ function buildSplunkLogEnvelope(options, event) {
1964
+ const fields = buildSplunkBaseFields(options, "log");
1965
+ fields.event_type = event.eventType;
1966
+ return {
1967
+ time: toUnixSeconds(event.occurredUtc),
1968
+ host: options.host.trim() || event.machineName,
1969
+ source: options.source.trim() || "loadstrike",
1970
+ sourcetype: options.sourcetype.trim() || "_json",
1971
+ index: options.index.trim() || null,
1972
+ event: {
1973
+ intake_type: "log",
1974
+ event_type: event.eventType,
1975
+ occurred_utc: event.occurredUtc.toISOString(),
1976
+ session_id: event.sessionId,
1977
+ test_suite: event.testSuite,
1978
+ test_name: event.testName,
1979
+ cluster_id: event.clusterId,
1980
+ node_type: event.nodeType,
1981
+ machine_name: event.machineName,
1982
+ scenario_name: event.scenarioName,
1983
+ step_name: event.stepName,
1984
+ tags: event.tags,
1985
+ fields: event.fields
1986
+ },
1987
+ fields
1988
+ };
1989
+ }
1990
+ function buildSplunkMetricEnvelope(options, point) {
1991
+ const fields = buildSplunkBaseFields(options, "metric");
1992
+ fields.metric_name = point.metricName;
1993
+ fields.metric_kind = point.metricKind;
1994
+ return {
1995
+ time: toUnixSeconds(point.occurredUtc),
1996
+ host: options.host.trim() || point.tags.machine_name || "local-machine",
1997
+ source: options.source.trim() || "loadstrike",
1998
+ sourcetype: options.sourcetype.trim() || "_json",
1999
+ index: options.index.trim() || null,
2000
+ event: {
2001
+ intake_type: "metric",
2002
+ metric_name: point.metricName,
2003
+ metric_kind: point.metricKind,
2004
+ occurred_utc: point.occurredUtc.toISOString(),
2005
+ value: point.value,
2006
+ unit_of_measure: point.unitOfMeasure ?? null,
2007
+ tags: point.tags
2008
+ },
2009
+ fields
2010
+ };
2011
+ }
2012
+ function buildOtelResourceAttributes(sinkName, staticAttributes) {
2013
+ const attributes = {
2014
+ "service.name": "loadstrike",
2015
+ "service.namespace": "loadstrike",
2016
+ "loadstrike.sink": sinkName
2017
+ };
2018
+ for (const [key, value] of Object.entries(staticAttributes)) {
2019
+ if (String(value ?? "").trim()) {
2020
+ attributes[key] = String(value);
2021
+ }
2022
+ }
2023
+ return Object.entries(attributes)
2024
+ .sort(([left], [right]) => left.localeCompare(right))
2025
+ .map(([key, value]) => ({
2026
+ key,
2027
+ value: { stringValue: value }
2028
+ }));
2029
+ }
2030
+ function buildOtelLogRecord(event) {
2031
+ return {
2032
+ timeUnixNano: toUnixNanoseconds(event.occurredUtc),
2033
+ observedTimeUnixNano: toUnixNanoseconds(event.occurredUtc),
2034
+ severityText: "INFO",
2035
+ body: { stringValue: toReportingSinkEventJson(event) },
2036
+ attributes: buildOtelAttributes({
2037
+ intake_type: "log",
2038
+ event_type: event.eventType,
2039
+ session_id: event.sessionId,
2040
+ test_suite: event.testSuite,
2041
+ test_name: event.testName,
2042
+ cluster_id: event.clusterId,
2043
+ node_type: event.nodeType,
2044
+ machine_name: event.machineName,
2045
+ scenario_name: event.scenarioName,
2046
+ step_name: event.stepName,
2047
+ tags: event.tags,
2048
+ fields: event.fields
2049
+ })
2050
+ };
2051
+ }
2052
+ function buildOtelMetric(point, points) {
2053
+ const dataPoints = points.map((entry) => buildOtelMetricDataPoint(entry));
2054
+ return {
2055
+ name: point.metricName,
2056
+ description: "LoadStrike reporting sink metric projection",
2057
+ unit: point.unitOfMeasure ?? null,
2058
+ gauge: point.metricKind === "gauge" ? { dataPoints } : undefined,
2059
+ sum: point.metricKind === "count" ? {
2060
+ aggregationTemporality: 2,
2061
+ isMonotonic: true,
2062
+ dataPoints
2063
+ } : undefined
2064
+ };
2065
+ }
2066
+ function buildOtelMetricDataPoint(point) {
2067
+ return {
2068
+ timeUnixNano: toUnixNanoseconds(point.occurredUtc),
2069
+ asDouble: point.value,
2070
+ attributes: buildOtelAttributes({
2071
+ ...point.tags,
2072
+ intake_type: "metric",
2073
+ metric_kind: point.metricKind
2074
+ })
2075
+ };
2076
+ }
2077
+ function buildOtelAttributes(values) {
2078
+ return Object.entries(values)
2079
+ .filter(([, value]) => value != null)
2080
+ .sort(([left], [right]) => left.localeCompare(right))
2081
+ .map(([key, value]) => ({
2082
+ key,
2083
+ value: toOtelAnyValue(value)
2084
+ }));
2085
+ }
2086
+ function toOtelAnyValue(value) {
2087
+ if (value == null) {
2088
+ return { stringValue: "" };
2089
+ }
2090
+ if (typeof value === "boolean") {
2091
+ return { boolValue: value };
2092
+ }
2093
+ if (typeof value === "bigint") {
2094
+ return { intValue: value.toString() };
2095
+ }
2096
+ if (typeof value === "number") {
2097
+ return Number.isInteger(value)
2098
+ ? { intValue: String(value) }
2099
+ : { doubleValue: value };
2100
+ }
2101
+ if (value instanceof Date) {
2102
+ return { stringValue: value.toISOString() };
2103
+ }
2104
+ if (typeof value === "object") {
2105
+ return { stringValue: JSON.stringify(value) };
2106
+ }
2107
+ return { stringValue: String(value) };
2108
+ }
2109
+ function toUnixSeconds(value) {
2110
+ return value.getTime() / 1000;
2111
+ }
2112
+ function sinkSessionMetadataFromContext(context, session) {
2113
+ const nodeInfo = context.getNodeInfo?.() ?? context.nodeInfo;
2114
+ const testInfo = context.testInfo;
2115
+ const sessionStartedUtc = String(session?.startedUtc ?? session?.StartedUtc ?? "");
2116
+ const contextStartedUtc = String(testInfo.createdUtc ?? testInfo.CreatedUtc ?? "");
2117
+ return {
2118
+ sessionId: String(testInfo.sessionId ?? ""),
2119
+ testSuite: String(testInfo.testSuite ?? ""),
2120
+ testName: String(testInfo.testName ?? ""),
2121
+ clusterId: String(testInfo.clusterId ?? ""),
2122
+ nodeType: String(nodeInfo.nodeType ?? "SingleNode"),
2123
+ machineName: String(nodeInfo.machineName ?? ""),
2124
+ startedUtc: sessionStartedUtc || contextStartedUtc || new Date().toISOString()
2125
+ };
2126
+ }
2127
+ function readInfluxOptionsFromConfig(infraConfig, configurationSectionPath) {
2128
+ const section = resolveConfigSection(infraConfig, configurationSectionPath);
2129
+ return {
2130
+ baseUrl: optionString(section, "BaseUrl", "baseUrl"),
2131
+ writeEndpointPath: optionString(section, "WriteEndpointPath", "writeEndpointPath"),
2132
+ organization: optionString(section, "Organization", "organization"),
2133
+ bucket: optionString(section, "Bucket", "bucket"),
2134
+ token: optionString(section, "Token", "token"),
2135
+ measurementName: optionString(section, "MeasurementName", "measurementName"),
2136
+ metricsMeasurementName: optionString(section, "MetricsMeasurementName", "metricsMeasurementName"),
2137
+ timeoutSeconds: optionNumber(section, "TimeoutSeconds", "timeoutSeconds"),
2138
+ timeoutMs: optionNumber(section, "TimeoutMs", "timeoutMs"),
2139
+ staticTags: normalizeStringMap(optionRecord(section, "StaticTags", "staticTags"))
2140
+ };
2141
+ }
2142
+ function readGrafanaLokiOptionsFromConfig(infraConfig, configurationSectionPath) {
2143
+ const section = resolveConfigSection(infraConfig, configurationSectionPath);
2144
+ return {
2145
+ baseUrl: optionString(section, "BaseUrl", "baseUrl"),
2146
+ pushEndpointPath: optionString(section, "PushEndpointPath", "pushEndpointPath"),
2147
+ metricsBaseUrl: optionString(section, "MetricsBaseUrl", "metricsBaseUrl"),
2148
+ metricsEndpointPath: optionString(section, "MetricsEndpointPath", "metricsEndpointPath"),
2149
+ bearerToken: optionString(section, "BearerToken", "bearerToken"),
2150
+ username: optionString(section, "Username", "username"),
2151
+ password: optionString(section, "Password", "password"),
2152
+ tenantId: optionString(section, "TenantId", "tenantId"),
2153
+ timeoutSeconds: optionNumber(section, "TimeoutSeconds", "timeoutSeconds"),
2154
+ timeoutMs: optionNumber(section, "TimeoutMs", "timeoutMs"),
2155
+ staticLabels: normalizeStringMap(optionRecord(section, "StaticLabels", "staticLabels")),
2156
+ metricsHeaders: normalizeStringMap(optionRecord(section, "MetricsHeaders", "metricsHeaders"))
2157
+ };
2158
+ }
2159
+ function readTimescaleDbOptionsFromConfig(infraConfig, configurationSectionPath) {
2160
+ const section = resolveConfigSection(infraConfig, configurationSectionPath);
2161
+ return {
2162
+ connectionString: optionString(section, "ConnectionString", "connectionString"),
2163
+ schema: optionString(section, "Schema", "schema"),
2164
+ tableName: optionString(section, "TableName", "tableName"),
2165
+ metricsTableName: optionString(section, "MetricsTableName", "metricsTableName"),
2166
+ staticTags: normalizeStringMap(optionRecord(section, "StaticTags", "staticTags"))
2167
+ };
2168
+ }
2169
+ function readDatadogOptionsFromConfig(infraConfig, configurationSectionPath) {
2170
+ const section = resolveConfigSection(infraConfig, configurationSectionPath);
2171
+ return {
2172
+ baseUrl: optionString(section, "BaseUrl", "baseUrl"),
2173
+ logsEndpointPath: optionString(section, "LogsEndpointPath", "logsEndpointPath"),
2174
+ metricsEndpointPath: optionString(section, "MetricsEndpointPath", "metricsEndpointPath"),
2175
+ apiKey: optionString(section, "ApiKey", "apiKey"),
2176
+ applicationKey: optionString(section, "ApplicationKey", "applicationKey"),
2177
+ source: optionString(section, "Source", "source"),
2178
+ service: optionString(section, "Service", "service"),
2179
+ host: optionString(section, "Host", "host"),
2180
+ timeoutSeconds: optionNumber(section, "TimeoutSeconds", "timeoutSeconds"),
2181
+ timeoutMs: optionNumber(section, "TimeoutMs", "timeoutMs"),
2182
+ staticTags: normalizeStringMap(optionRecord(section, "StaticTags", "staticTags")),
2183
+ staticAttributes: normalizeStringMap(optionRecord(section, "StaticAttributes", "staticAttributes"))
2184
+ };
2185
+ }
2186
+ function readSplunkOptionsFromConfig(infraConfig, configurationSectionPath) {
2187
+ const section = resolveConfigSection(infraConfig, configurationSectionPath);
2188
+ return {
2189
+ baseUrl: optionString(section, "BaseUrl", "baseUrl"),
2190
+ eventEndpointPath: optionString(section, "EventEndpointPath", "eventEndpointPath"),
2191
+ token: optionString(section, "Token", "token"),
2192
+ source: optionString(section, "Source", "source"),
2193
+ sourcetype: optionString(section, "Sourcetype", "sourcetype"),
2194
+ index: optionString(section, "Index", "index"),
2195
+ host: optionString(section, "Host", "host"),
2196
+ timeoutSeconds: optionNumber(section, "TimeoutSeconds", "timeoutSeconds"),
2197
+ timeoutMs: optionNumber(section, "TimeoutMs", "timeoutMs"),
2198
+ staticFields: normalizeStringMap(optionRecord(section, "StaticFields", "staticFields"))
2199
+ };
2200
+ }
2201
+ function readOtelCollectorOptionsFromConfig(infraConfig, configurationSectionPath) {
2202
+ const section = resolveConfigSection(infraConfig, configurationSectionPath);
2203
+ return {
2204
+ baseUrl: optionString(section, "BaseUrl", "baseUrl"),
2205
+ logsEndpointPath: optionString(section, "LogsEndpointPath", "logsEndpointPath"),
2206
+ metricsEndpointPath: optionString(section, "MetricsEndpointPath", "metricsEndpointPath"),
2207
+ timeoutSeconds: optionNumber(section, "TimeoutSeconds", "timeoutSeconds"),
2208
+ timeoutMs: optionNumber(section, "TimeoutMs", "timeoutMs"),
2209
+ headers: normalizeStringMap(optionRecord(section, "Headers", "headers")),
2210
+ staticResourceAttributes: normalizeStringMap(optionRecord(section, "StaticResourceAttributes", "staticResourceAttributes"))
2211
+ };
2212
+ }
2213
+ function mergeInfluxOptions(target, source) {
2214
+ if (!target.baseUrl.trim()) {
2215
+ target.baseUrl = source.baseUrl?.trim() || "";
2216
+ }
2217
+ if (!target.writeEndpointPath.trim()) {
2218
+ target.writeEndpointPath = source.writeEndpointPath?.trim() || "";
2219
+ }
2220
+ if (!target.organization.trim()) {
2221
+ target.organization = source.organization?.trim() || "";
2222
+ }
2223
+ if (!target.bucket.trim()) {
2224
+ target.bucket = source.bucket?.trim() || "";
2225
+ }
2226
+ if (!target.token.trim()) {
2227
+ target.token = source.token?.trim() || "";
2228
+ }
2229
+ if (!target.measurementName.trim()) {
2230
+ target.measurementName = source.measurementName?.trim() || "";
2231
+ }
2232
+ if (!target.metricsMeasurementName.trim()) {
2233
+ target.metricsMeasurementName = source.metricsMeasurementName?.trim() || "";
2234
+ }
2235
+ if (resolveTimeoutMs(target.timeoutSeconds, target.timeoutMs) <= 0) {
2236
+ target.timeoutSeconds = Number.isFinite(source.timeoutSeconds) ? Number(source.timeoutSeconds) : target.timeoutSeconds;
2237
+ target.timeoutMs = Number.isFinite(source.timeoutMs) ? Number(source.timeoutMs) : target.timeoutMs;
2238
+ }
2239
+ if (Object.keys(target.staticTags).length === 0 && Object.keys(source.staticTags ?? {}).length > 0) {
2240
+ target.staticTags = normalizeStringMap(source.staticTags);
2241
+ }
2242
+ }
2243
+ function mergeGrafanaLokiOptions(target, source) {
2244
+ if (!target.baseUrl.trim()) {
2245
+ target.baseUrl = source.baseUrl?.trim() || "";
2246
+ }
2247
+ if (!target.pushEndpointPath.trim()) {
2248
+ target.pushEndpointPath = source.pushEndpointPath?.trim() || "";
2249
+ }
2250
+ if (!target.metricsBaseUrl.trim()) {
2251
+ target.metricsBaseUrl = source.metricsBaseUrl?.trim() || "";
2252
+ }
2253
+ if (!target.metricsEndpointPath.trim()) {
2254
+ target.metricsEndpointPath = source.metricsEndpointPath?.trim() || "";
2255
+ }
2256
+ if (!target.bearerToken.trim()) {
2257
+ target.bearerToken = source.bearerToken?.trim() || "";
2258
+ }
2259
+ if (!target.username.trim()) {
2260
+ target.username = source.username?.trim() || "";
2261
+ }
2262
+ if (!target.password.trim()) {
2263
+ target.password = source.password ?? "";
2264
+ }
2265
+ if (!target.tenantId.trim()) {
2266
+ target.tenantId = source.tenantId?.trim() || "";
2267
+ }
2268
+ if (resolveTimeoutMs(target.timeoutSeconds, target.timeoutMs) <= 0) {
2269
+ target.timeoutSeconds = Number.isFinite(source.timeoutSeconds) ? Number(source.timeoutSeconds) : target.timeoutSeconds;
2270
+ target.timeoutMs = Number.isFinite(source.timeoutMs) ? Number(source.timeoutMs) : target.timeoutMs;
2271
+ }
2272
+ if (Object.keys(target.staticLabels).length === 0 && Object.keys(source.staticLabels ?? {}).length > 0) {
2273
+ target.staticLabels = normalizeStringMap(source.staticLabels);
2274
+ }
2275
+ if (Object.keys(target.metricsHeaders).length === 0 && Object.keys(source.metricsHeaders ?? {}).length > 0) {
2276
+ target.metricsHeaders = normalizeStringMap(source.metricsHeaders);
2277
+ }
2278
+ }
2279
+ function mergeTimescaleDbOptions(target, source) {
2280
+ if (!target.connectionString.trim()) {
2281
+ target.connectionString = source.connectionString?.trim() || "";
2282
+ }
2283
+ if (!target.schema.trim()) {
2284
+ target.schema = source.schema?.trim() || "";
2285
+ }
2286
+ if (!target.tableName.trim()) {
2287
+ target.tableName = source.tableName?.trim() || "";
2288
+ }
2289
+ if (!target.metricsTableName.trim()) {
2290
+ target.metricsTableName = source.metricsTableName?.trim() || "";
2291
+ }
2292
+ if (Object.keys(target.staticTags).length === 0 && Object.keys(source.staticTags ?? {}).length > 0) {
2293
+ target.staticTags = normalizeStringMap(source.staticTags);
2294
+ }
2295
+ }
2296
+ function mergeDatadogOptions(target, source) {
2297
+ if (!target.baseUrl.trim()) {
2298
+ target.baseUrl = source.baseUrl?.trim() || "";
2299
+ }
2300
+ if (!target.logsEndpointPath.trim()) {
2301
+ target.logsEndpointPath = source.logsEndpointPath?.trim() || "";
2302
+ }
2303
+ if (!target.metricsEndpointPath.trim()) {
2304
+ target.metricsEndpointPath = source.metricsEndpointPath?.trim() || "";
2305
+ }
2306
+ if (!target.apiKey.trim()) {
2307
+ target.apiKey = source.apiKey?.trim() || "";
2308
+ }
2309
+ if (!target.applicationKey.trim()) {
2310
+ target.applicationKey = source.applicationKey?.trim() || "";
2311
+ }
2312
+ if (!target.source.trim()) {
2313
+ target.source = source.source?.trim() || "";
2314
+ }
2315
+ if (!target.service.trim()) {
2316
+ target.service = source.service?.trim() || "";
2317
+ }
2318
+ if (!target.host.trim()) {
2319
+ target.host = source.host?.trim() || "";
2320
+ }
2321
+ if (resolveTimeoutMs(target.timeoutSeconds, target.timeoutMs) <= 0) {
2322
+ target.timeoutSeconds = Number.isFinite(source.timeoutSeconds) ? Number(source.timeoutSeconds) : target.timeoutSeconds;
2323
+ target.timeoutMs = Number.isFinite(source.timeoutMs) ? Number(source.timeoutMs) : target.timeoutMs;
2324
+ }
2325
+ if (Object.keys(target.staticTags).length === 0 && Object.keys(source.staticTags ?? {}).length > 0) {
2326
+ target.staticTags = normalizeStringMap(source.staticTags);
2327
+ }
2328
+ if (Object.keys(target.staticAttributes).length === 0 && Object.keys(source.staticAttributes ?? {}).length > 0) {
2329
+ target.staticAttributes = normalizeStringMap(source.staticAttributes);
2330
+ }
2331
+ }
2332
+ function mergeSplunkOptions(target, source) {
2333
+ if (!target.baseUrl.trim()) {
2334
+ target.baseUrl = source.baseUrl?.trim() || "";
2335
+ }
2336
+ if (!target.eventEndpointPath.trim()) {
2337
+ target.eventEndpointPath = source.eventEndpointPath?.trim() || "";
2338
+ }
2339
+ if (!target.token.trim()) {
2340
+ target.token = source.token?.trim() || "";
2341
+ }
2342
+ if (!target.source.trim()) {
2343
+ target.source = source.source?.trim() || "";
2344
+ }
2345
+ if (!target.sourcetype.trim()) {
2346
+ target.sourcetype = source.sourcetype?.trim() || "";
2347
+ }
2348
+ if (!target.index.trim()) {
2349
+ target.index = source.index?.trim() || "";
2350
+ }
2351
+ if (!target.host.trim()) {
2352
+ target.host = source.host?.trim() || "";
2353
+ }
2354
+ if (resolveTimeoutMs(target.timeoutSeconds, target.timeoutMs) <= 0) {
2355
+ target.timeoutSeconds = Number.isFinite(source.timeoutSeconds) ? Number(source.timeoutSeconds) : target.timeoutSeconds;
2356
+ target.timeoutMs = Number.isFinite(source.timeoutMs) ? Number(source.timeoutMs) : target.timeoutMs;
2357
+ }
2358
+ if (Object.keys(target.staticFields).length === 0 && Object.keys(source.staticFields ?? {}).length > 0) {
2359
+ target.staticFields = normalizeStringMap(source.staticFields);
2360
+ }
2361
+ }
2362
+ function mergeOtelCollectorOptions(target, source) {
2363
+ if (!target.baseUrl.trim()) {
2364
+ target.baseUrl = source.baseUrl?.trim() || "";
2365
+ }
2366
+ if (!target.logsEndpointPath.trim()) {
2367
+ target.logsEndpointPath = source.logsEndpointPath?.trim() || "";
2368
+ }
2369
+ if (!target.metricsEndpointPath.trim()) {
2370
+ target.metricsEndpointPath = source.metricsEndpointPath?.trim() || "";
2371
+ }
2372
+ if (resolveTimeoutMs(target.timeoutSeconds, target.timeoutMs) <= 0) {
2373
+ target.timeoutSeconds = Number.isFinite(source.timeoutSeconds) ? Number(source.timeoutSeconds) : target.timeoutSeconds;
2374
+ target.timeoutMs = Number.isFinite(source.timeoutMs) ? Number(source.timeoutMs) : target.timeoutMs;
2375
+ }
2376
+ if (Object.keys(target.headers).length === 0 && Object.keys(source.headers ?? {}).length > 0) {
2377
+ target.headers = normalizeStringMap(source.headers);
2378
+ }
2379
+ if (Object.keys(target.staticResourceAttributes).length === 0 && Object.keys(source.staticResourceAttributes ?? {}).length > 0) {
2380
+ target.staticResourceAttributes = normalizeStringMap(source.staticResourceAttributes);
2381
+ }
2382
+ }
2383
+ function resolveConfigSection(source, path) {
2384
+ const trimmedPath = String(path ?? "").trim();
2385
+ if (!trimmedPath) {
2386
+ return {};
2387
+ }
2388
+ const direct = pickRecordValue(source, trimmedPath);
2389
+ if (isRecord(direct)) {
2390
+ return direct;
2391
+ }
2392
+ const segments = trimmedPath.split(":").filter((value) => value.length > 0);
2393
+ let current = source;
2394
+ for (let index = 0; index < segments.length; index += 1) {
2395
+ const currentRecord = asRecord(current);
2396
+ const remainingPath = segments.slice(index).join(":");
2397
+ const remaining = pickRecordValue(currentRecord, remainingPath);
2398
+ if (isRecord(remaining)) {
2399
+ return remaining;
2400
+ }
2401
+ const next = pickRecordValue(currentRecord, segments[index]);
2402
+ if (!isRecord(next)) {
2403
+ return {};
2404
+ }
2405
+ current = next;
2406
+ }
2407
+ return asRecord(current);
2408
+ }
2409
+ function pickRecordValue(record, ...keys) {
2410
+ for (const key of keys) {
2411
+ if (key in record) {
2412
+ return record[key];
2413
+ }
2414
+ const normalizedKey = normalizeConfigKey(key);
2415
+ const match = Object.entries(record).find(([entryKey]) => normalizeConfigKey(entryKey) === normalizedKey);
2416
+ if (match) {
2417
+ return match[1];
2418
+ }
2419
+ }
2420
+ return undefined;
2421
+ }
2422
+ function optionString(record, ...keys) {
2423
+ const value = pickRecordValue(record, ...keys);
2424
+ return typeof value === "string" ? value : "";
2425
+ }
2426
+ function pickBooleanValue(record, fallback, ...keys) {
2427
+ const value = pickRecordValue(record, ...keys);
2428
+ if (typeof value === "boolean") {
2429
+ return value;
2430
+ }
2431
+ if (typeof value === "number") {
2432
+ return value !== 0;
2433
+ }
2434
+ if (typeof value === "string") {
2435
+ const normalized = value.trim().toLowerCase();
2436
+ if (normalized === "true" || normalized === "1" || normalized === "yes") {
2437
+ return true;
2438
+ }
2439
+ if (normalized === "false" || normalized === "0" || normalized === "no") {
2440
+ return false;
2441
+ }
2442
+ }
2443
+ return fallback;
2444
+ }
2445
+ function optionNumber(record, ...keys) {
2446
+ const value = pickRecordValue(record, ...keys);
2447
+ if (typeof value === "number" && Number.isFinite(value)) {
2448
+ return value;
2449
+ }
2450
+ if (typeof value === "string" && value.trim()) {
2451
+ const parsed = Number(value);
2452
+ if (Number.isFinite(parsed)) {
2453
+ return parsed;
2454
+ }
2455
+ }
2456
+ return undefined;
2457
+ }
2458
+ function optionRecord(record, ...keys) {
2459
+ return asRecord(pickRecordValue(record, ...keys));
2460
+ }
2461
+ function normalizeStringMap(value) {
2462
+ const resolved = asRecord(value);
2463
+ const rows = {};
2464
+ for (const [key, entryValue] of Object.entries(resolved)) {
2465
+ if (entryValue == null) {
2466
+ continue;
2467
+ }
2468
+ rows[key] = String(entryValue);
2469
+ }
2470
+ return rows;
2471
+ }
2472
+ function resolveTimeoutMs(timeoutSeconds, timeoutMs) {
2473
+ if (Number.isFinite(timeoutMs) && Number(timeoutMs) > 0) {
2474
+ return Math.max(Math.trunc(Number(timeoutMs)), 1);
2475
+ }
2476
+ if (Number.isFinite(timeoutSeconds) && Number(timeoutSeconds) > 0) {
2477
+ return Math.max(Math.trunc(Number(timeoutSeconds) * 1000), 1);
2478
+ }
2479
+ return 30000;
2480
+ }
2481
+ function toUnixNanoseconds(value) {
2482
+ return (BigInt(value.getTime()) * 1000000n).toString();
2483
+ }
2484
+ function escapeInfluxToken(value) {
2485
+ return String(value ?? "")
2486
+ .split("\\").join("\\\\")
2487
+ .split(",").join("\\,")
2488
+ .split(" ").join("\\ ")
2489
+ .split("=").join("\\=");
2490
+ }
2491
+ function normalizePath(path) {
2492
+ const trimmed = String(path ?? "").trim();
2493
+ return trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
2494
+ }
2495
+ function trimTrailingSlashes(value) {
2496
+ return value.replace(/\/+$/g, "");
2497
+ }
2498
+ function sanitizeGrafanaLabelKey(value) {
2499
+ const sanitized = String(value ?? "")
2500
+ .split("")
2501
+ .map((char) => (/[A-Za-z0-9_]/.test(char) ? char : "_"))
2502
+ .join("");
2503
+ if (!sanitized || !/[A-Za-z_]/.test(sanitized[0])) {
2504
+ return `_${sanitized}`;
2505
+ }
2506
+ return sanitized;
2507
+ }
2508
+ function sanitizeGrafanaLabelValue(value) {
2509
+ return String(value ?? "").split("\r").join(" ").split("\n").join(" ").trim();
2510
+ }
2511
+ function replaceLiteral(value, search, replacement) {
2512
+ return value.split(search).join(replacement);
2513
+ }
2514
+ function normalizePluginFieldName(value) {
2515
+ const text = String(value ?? "").trim();
2516
+ if (!text) {
2517
+ return "value";
2518
+ }
2519
+ let normalized = "";
2520
+ let lastWasUnderscore = false;
2521
+ for (const char of text) {
2522
+ if (/[A-Za-z0-9]/.test(char)) {
2523
+ if (/[A-Z]/.test(char) && normalized && !lastWasUnderscore) {
2524
+ normalized += "_";
2525
+ }
2526
+ normalized += char.toLowerCase();
2527
+ lastWasUnderscore = false;
2528
+ }
2529
+ else if (!lastWasUnderscore) {
2530
+ normalized += "_";
2531
+ lastWasUnderscore = true;
2532
+ }
2533
+ }
2534
+ return normalized.replace(/^_+|_+$/g, "") || "value";
2535
+ }
2536
+ function normalizePluginFieldValue(value) {
2537
+ if (value == null || typeof value === "string" || typeof value === "boolean" || typeof value === "number") {
2538
+ return value;
2539
+ }
2540
+ if (typeof value === "bigint") {
2541
+ return Number(value);
2542
+ }
2543
+ if (value instanceof Date) {
2544
+ return value.toISOString();
2545
+ }
2546
+ return JSON.stringify(value);
2547
+ }
2548
+ function tryReadText(row, key) {
2549
+ const value = pickRecordValue(row, key, key.toLowerCase(), key.toUpperCase());
2550
+ if (value == null) {
2551
+ return null;
2552
+ }
2553
+ const text = String(value).trim();
2554
+ return text || null;
2555
+ }
2556
+ function cleanNullableText(value) {
2557
+ const text = String(value ?? "").trim();
2558
+ return text || null;
2559
+ }
2560
+ function validateIdentifier(value, parameterName) {
2561
+ if (!IDENTIFIER_REGEX.test(value)) {
2562
+ throw new Error(`TimescaleDbReportingSink ${parameterName} '${value}' contains unsupported characters.`);
2563
+ }
2564
+ }
2565
+ function quoteIdentifier(schema, tableName) {
2566
+ return `${quoteIdentifierPart(schema)}.${quoteIdentifierPart(tableName)}`;
2567
+ }
2568
+ function quoteIdentifierPart(value) {
2569
+ return `"${value}"`;
2570
+ }
2571
+ function normalizeConfigKey(value) {
2572
+ return String(value ?? "").replace(/[^A-Za-z0-9]+/g, "").toLowerCase();
2573
+ }
2574
+ function isRecord(value) {
2575
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
2576
+ }
2577
+ function asRecord(value) {
2578
+ return isRecord(value) ? value : {};
2579
+ }
2580
+ function cloneBaseContext(context) {
2581
+ const nodeInfo = cloneNodeInfo(context.nodeInfo);
2582
+ const testInfo = cloneTestInfo(context.testInfo);
2583
+ return {
2584
+ logger: context.logger,
2585
+ nodeInfo,
2586
+ testInfo,
2587
+ getNodeInfo: () => cloneNodeInfo(context.getNodeInfo?.() ?? nodeInfo)
2588
+ };
2589
+ }
2590
+ function cloneSessionStartInfo(session) {
2591
+ return {
2592
+ ...cloneBaseContext(session),
2593
+ startedUtc: session.startedUtc,
2594
+ scenarioNames: [...session.scenarioNames],
2595
+ scenarios: session.scenarios.map((value) => ({ ...value }))
2596
+ };
2597
+ }
2598
+ function cloneNodeInfo(nodeInfo) {
2599
+ return { ...nodeInfo };
2600
+ }
2601
+ function cloneTestInfo(testInfo) {
2602
+ return { ...testInfo };
2603
+ }
2604
+ function cloneMetricStats(metrics) {
2605
+ return {
2606
+ counters: metrics.counters.map((value) => ({ ...value })),
2607
+ gauges: metrics.gauges.map((value) => ({ ...value })),
2608
+ durationMs: metrics.durationMs
2609
+ };
2610
+ }
2611
+ function cloneScenarioStats(value) {
2612
+ return {
2613
+ ...value,
2614
+ ok: deepCloneRecord(value.ok),
2615
+ fail: deepCloneRecord(value.fail),
2616
+ loadSimulationStats: { ...value.loadSimulationStats },
2617
+ stepStats: value.stepStats.map((step) => ({
2618
+ ...step,
2619
+ ok: deepCloneRecord(step.ok),
2620
+ fail: deepCloneRecord(step.fail)
2621
+ }))
2622
+ };
2623
+ }
2624
+ function cloneNodeStats(result) {
2625
+ return {
2626
+ ...result,
2627
+ nodeInfo: cloneNodeInfo(result.nodeInfo),
2628
+ testInfo: cloneTestInfo(result.testInfo),
2629
+ thresholds: result.thresholds.map((value) => ({ ...value })),
2630
+ thresholdResults: result.thresholdResults.map((value) => ({ ...value })),
2631
+ metrics: cloneMetricStats(result.metrics),
2632
+ metricValues: result.metricValues.map((value) => ({ ...value })),
2633
+ scenarioStats: result.scenarioStats.map((value) => cloneScenarioStats(value)),
2634
+ stepStats: result.stepStats.map((step) => ({
2635
+ ...step,
2636
+ ok: deepCloneRecord(step.ok),
2637
+ fail: deepCloneRecord(step.fail)
2638
+ })),
2639
+ pluginsData: result.pluginsData.map((plugin) => new runtime_js_1.LoadStrikePluginData(plugin.pluginName, plugin.tables.map((table) => new runtime_js_1.LoadStrikePluginDataTable(table.tableName, [...table.headers], table.rows.map((row) => deepCloneRecord(row)))), [...plugin.hints])),
2640
+ disabledSinks: [...result.disabledSinks],
2641
+ sinkErrors: result.sinkErrors.map((value) => ({ ...value })),
2642
+ reportFiles: [...result.reportFiles]
2643
+ };
2644
+ }
2645
+ function deepCloneRecord(value) {
2646
+ const source = asRecord(value);
2647
+ const result = {};
2648
+ for (const [key, entryValue] of Object.entries(source)) {
2649
+ result[key] = deepCloneValue(entryValue);
2650
+ }
2651
+ return result;
2652
+ }
2653
+ function deepCloneValue(value) {
2654
+ if (Array.isArray(value)) {
2655
+ return value.map((entry) => deepCloneValue(entry));
2656
+ }
2657
+ if (isRecord(value)) {
2658
+ return deepCloneRecord(value);
2659
+ }
2660
+ return value;
2661
+ }
2662
+ async function postWithTimeout(fetchImpl, url, init, timeoutMs, sinkName) {
2663
+ const controller = new AbortController();
2664
+ const timer = setTimeout(() => controller.abort(), Math.max(timeoutMs, 1));
2665
+ try {
2666
+ const response = await fetchImpl(url, { ...init, signal: controller.signal });
2667
+ if (!response.ok) {
2668
+ const body = await response.text();
2669
+ throw new Error(`${sinkName} write failed with status ${response.status}: ${body}`);
2670
+ }
2671
+ }
2672
+ finally {
2673
+ clearTimeout(timer);
2674
+ }
2675
+ }