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