@loadstrike/loadstrike-sdk 1.0.26901 → 1.0.27301

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/esm/sinks.js CHANGED
@@ -1,5 +1,8 @@
1
1
  import { LoadStrikePluginData as LoadStrikePluginDataModel, LoadStrikePluginDataTable as LoadStrikePluginDataTableModel } from "./runtime.js";
2
2
  import { createHash, randomBytes } from "node:crypto";
3
+ import { appendFileSync, mkdirSync } from "node:fs";
4
+ import { dirname } from "node:path";
5
+ import { createSocket } from "node:dgram";
3
6
  import { Pool } from "pg";
4
7
  const DEFAULT_INFLUX_CONFIGURATION_SECTION_PATH = "LoadStrike:ReportingSinks:InfluxDb";
5
8
  const DEFAULT_GRAFANA_LOKI_CONFIGURATION_SECTION_PATH = "LoadStrike:ReportingSinks:GrafanaLoki";
@@ -1364,6 +1367,226 @@ export class OtelCollectorReportingSink {
1364
1367
  }, resolveTimeoutMs(this.options.timeoutSeconds, this.options.timeoutMs), "OtelCollectorReportingSink metrics");
1365
1368
  }
1366
1369
  }
1370
+ class ExpandedEventReportingSink {
1371
+ constructor(sinkName, licenseFeature) {
1372
+ this.baseContext = null;
1373
+ this.session = null;
1374
+ this.sinkName = sinkName;
1375
+ this.SinkName = sinkName;
1376
+ this.licenseFeature = licenseFeature;
1377
+ this.LicenseFeature = licenseFeature;
1378
+ }
1379
+ init(context, _infraConfig) {
1380
+ this.baseContext = cloneBaseContext(context);
1381
+ }
1382
+ Init(context, infraConfig) {
1383
+ this.init(context, infraConfig);
1384
+ }
1385
+ start(session) {
1386
+ this.session = sinkSessionMetadataFromContext(this.getBaseContext(), session);
1387
+ }
1388
+ Start(session) {
1389
+ this.start(session);
1390
+ }
1391
+ async saveRealtimeStats(scenarioStats) {
1392
+ await this.persistEvents(createRealtimeStatsEvents(this.getSession(), scenarioStats));
1393
+ }
1394
+ async SaveRealtimeStats(scenarioStats) {
1395
+ await this.saveRealtimeStats(scenarioStats);
1396
+ }
1397
+ async saveRealtimeMetrics(metrics) {
1398
+ await this.persistEvents(createRealtimeMetricEvents(this.getSession(), metrics));
1399
+ }
1400
+ async SaveRealtimeMetrics(metrics) {
1401
+ await this.saveRealtimeMetrics(metrics);
1402
+ }
1403
+ async saveRunResult(result) {
1404
+ await this.persistEvents(createRunResultEvents(this.getSession(), result));
1405
+ }
1406
+ async SaveRunResult(result) {
1407
+ await this.saveRunResult(result);
1408
+ }
1409
+ stop() {
1410
+ this.session = null;
1411
+ }
1412
+ Stop() {
1413
+ this.stop();
1414
+ }
1415
+ Dispose() {
1416
+ this.baseContext = null;
1417
+ this.stop();
1418
+ }
1419
+ getBaseContext() {
1420
+ if (!this.baseContext) {
1421
+ throw new Error(`${this.sinkName} has not been initialized.`);
1422
+ }
1423
+ return this.baseContext;
1424
+ }
1425
+ getSession() {
1426
+ if (!this.session) {
1427
+ throw new Error(`${this.sinkName} has not been started.`);
1428
+ }
1429
+ return this.session;
1430
+ }
1431
+ buildPayload(events, staticTags = {}) {
1432
+ return {
1433
+ sinkName: this.sinkName,
1434
+ runId: this.getSession().runId,
1435
+ sessionId: this.getSession().sessionId,
1436
+ events: events.map((event) => ({
1437
+ ...event,
1438
+ tags: { ...staticTags, ...event.tags }
1439
+ })),
1440
+ metrics: createReportingSinkMetricPoints(this.sinkName, events, staticTags)
1441
+ };
1442
+ }
1443
+ }
1444
+ class ExpandedHttpJsonReportingSink extends ExpandedEventReportingSink {
1445
+ constructor(sinkName, licenseFeature, defaultEndpointPath, options = {}) {
1446
+ super(sinkName, licenseFeature);
1447
+ const source = asRecord(options);
1448
+ this.baseUrl = optionString(source, "baseUrl", "BaseUrl").trim();
1449
+ this.endpointPath = optionString(source, "endpointPath", "EndpointPath").trim() || defaultEndpointPath;
1450
+ this.headers = normalizeStringMap(optionRecord(source, "headers", "Headers"));
1451
+ this.staticTags = normalizeStringMap(optionRecord(source, "staticTags", "StaticTags"));
1452
+ this.timeoutMs = resolveTimeoutMs(optionNumber(source, "timeoutSeconds", "TimeoutSeconds"), optionNumber(source, "timeoutMs", "TimeoutMs"));
1453
+ this.fetchImpl = pickRecordValue(source, "fetchImpl", "FetchImpl") ?? fetch;
1454
+ }
1455
+ init(context, infraConfig) {
1456
+ super.init(context, infraConfig);
1457
+ if (!this.baseUrl.trim()) {
1458
+ throw new Error(`${this.constructor.name} requires BaseUrl.`);
1459
+ }
1460
+ }
1461
+ async persistEvents(events) {
1462
+ if (!events.length) {
1463
+ return;
1464
+ }
1465
+ await postWithTimeout(this.fetchImpl, `${trimTrailingSlashes(this.baseUrl)}${normalizePath(this.endpointPath)}`, {
1466
+ method: "POST",
1467
+ headers: {
1468
+ "Content-Type": "application/json",
1469
+ ...this.headers
1470
+ },
1471
+ body: JSON.stringify(this.buildPayload(events, this.staticTags))
1472
+ }, this.timeoutMs, this.constructor.name);
1473
+ }
1474
+ }
1475
+ export class PrometheusRemoteWriteReportingSink extends ExpandedHttpJsonReportingSink {
1476
+ constructor(options = {}) {
1477
+ super("prometheus-remote-write", "extensions.reporting_sinks.prometheus_remote_write", "/api/v1/write", options);
1478
+ }
1479
+ }
1480
+ export class CloudWatchReportingSink extends ExpandedHttpJsonReportingSink {
1481
+ constructor(options = {}) {
1482
+ super("cloudwatch", "extensions.reporting_sinks.cloudwatch", "/loadstrike/events", options);
1483
+ }
1484
+ }
1485
+ export class DynatraceReportingSink extends ExpandedHttpJsonReportingSink {
1486
+ constructor(options = {}) {
1487
+ super("dynatrace", "extensions.reporting_sinks.dynatrace", "/api/v2/logs/ingest", options);
1488
+ }
1489
+ }
1490
+ export class ElasticsearchReportingSink extends ExpandedHttpJsonReportingSink {
1491
+ constructor(options = {}) {
1492
+ super("elasticsearch", "extensions.reporting_sinks.elasticsearch", "/loadstrike-events/_doc", options);
1493
+ }
1494
+ }
1495
+ export class OpenSearchReportingSink extends ExpandedHttpJsonReportingSink {
1496
+ constructor(options = {}) {
1497
+ super("opensearch", "extensions.reporting_sinks.opensearch", "/loadstrike-events/_doc", options);
1498
+ }
1499
+ }
1500
+ export class NewRelicReportingSink extends ExpandedHttpJsonReportingSink {
1501
+ constructor(options = {}) {
1502
+ super("new-relic", "extensions.reporting_sinks.new_relic", "/log/v1", options);
1503
+ }
1504
+ }
1505
+ export class GenericWebhookReportingSink extends ExpandedHttpJsonReportingSink {
1506
+ constructor(options = {}) {
1507
+ super("webhook", "extensions.reporting_sinks.webhook", "/loadstrike", options);
1508
+ }
1509
+ }
1510
+ export class KafkaReportingSink extends ExpandedEventReportingSink {
1511
+ constructor(options = {}) {
1512
+ super("kafka", "extensions.reporting_sinks.kafka");
1513
+ const source = asRecord(options);
1514
+ this.topic = optionString(source, "topic", "Topic").trim() || "loadstrike-events";
1515
+ this.staticTags = normalizeStringMap(optionRecord(source, "staticTags", "StaticTags"));
1516
+ this.publishAsync = pickRecordValue(source, "publishAsync", "PublishAsync");
1517
+ }
1518
+ async persistEvents(events) {
1519
+ if (!events.length) {
1520
+ return;
1521
+ }
1522
+ if (!this.publishAsync) {
1523
+ throw new Error("KafkaReportingSink requires PublishAsync when a Kafka producer is not configured by the host application.");
1524
+ }
1525
+ await this.publishAsync(this.topic, JSON.stringify(this.buildPayload(events, this.staticTags)));
1526
+ }
1527
+ }
1528
+ class StatsDReportingSinkBase extends ExpandedEventReportingSink {
1529
+ constructor(sinkName, licenseFeature, options = {}, dogStatsD = false) {
1530
+ super(sinkName, licenseFeature);
1531
+ const source = asRecord(options);
1532
+ this.prefix = optionString(source, "prefix", "Prefix").trim() || "loadstrike";
1533
+ this.host = optionString(source, "host", "Host").trim() || "127.0.0.1";
1534
+ this.port = optionNumber(source, "port", "Port") ?? 8125;
1535
+ this.tags = normalizeStringMap(optionRecord(source, "tags", "Tags"));
1536
+ this.sendLineAsync = pickRecordValue(source, "sendLineAsync", "SendLineAsync");
1537
+ this.dogStatsD = dogStatsD;
1538
+ }
1539
+ async persistEvents(events) {
1540
+ const points = createReportingSinkMetricPoints(this.sinkName, events, this.tags);
1541
+ for (const point of points) {
1542
+ await this.sendLine(this.formatPoint(point));
1543
+ }
1544
+ }
1545
+ formatPoint(point) {
1546
+ const metricType = point.metricKind === "count" ? "c" : "g";
1547
+ const tagSuffix = this.dogStatsD
1548
+ ? dogStatsDTagSuffix({ ...this.tags, ...point.tags })
1549
+ : "";
1550
+ return `${this.prefix}.${sanitizeMetricToken(point.metricName)}:${point.value}|${metricType}${tagSuffix}`;
1551
+ }
1552
+ async sendLine(line) {
1553
+ if (this.sendLineAsync) {
1554
+ await this.sendLineAsync(line);
1555
+ return;
1556
+ }
1557
+ await sendStatsDUdpLine(this.host, this.port, line);
1558
+ }
1559
+ }
1560
+ export class StatsDReportingSink extends StatsDReportingSinkBase {
1561
+ constructor(options = {}) {
1562
+ super("statsd", "extensions.reporting_sinks.statsd", options, false);
1563
+ }
1564
+ }
1565
+ export class DogStatsDReportingSink extends StatsDReportingSinkBase {
1566
+ constructor(options = {}) {
1567
+ super("dogstatsd", "extensions.reporting_sinks.dogstatsd", options, true);
1568
+ }
1569
+ }
1570
+ export class NetdataStatsDReportingSink extends StatsDReportingSinkBase {
1571
+ constructor(options = {}) {
1572
+ super("netdata-statsd", "extensions.reporting_sinks.netdata", options, false);
1573
+ }
1574
+ }
1575
+ export class JsonlFileReportingSink extends ExpandedEventReportingSink {
1576
+ constructor(options = {}) {
1577
+ super("jsonl", "extensions.reporting_sinks.jsonl");
1578
+ const source = asRecord(options);
1579
+ this.filePath = optionString(source, "filePath", "FilePath").trim() || "loadstrike-reporting.jsonl";
1580
+ this.staticTags = normalizeStringMap(optionRecord(source, "staticTags", "StaticTags"));
1581
+ }
1582
+ async persistEvents(events) {
1583
+ if (!events.length) {
1584
+ return;
1585
+ }
1586
+ mkdirSync(dirname(this.filePath), { recursive: true });
1587
+ appendFileSync(this.filePath, `${JSON.stringify(this.buildPayload(events, this.staticTags))}\n`, "utf8");
1588
+ }
1589
+ }
1367
1590
  function createRealtimeStatsEvents(session, scenarios) {
1368
1591
  const occurredUtc = new Date();
1369
1592
  const events = [];
@@ -2577,6 +2800,32 @@ function normalizeStringMap(value) {
2577
2800
  }
2578
2801
  return rows;
2579
2802
  }
2803
+ function sanitizeMetricToken(value) {
2804
+ return String(value ?? "")
2805
+ .trim()
2806
+ .replace(/[^A-Za-z0-9_.-]+/g, "_")
2807
+ .replace(/^_+|_+$/g, "") || "metric";
2808
+ }
2809
+ function dogStatsDTagSuffix(tags) {
2810
+ const values = Object.entries(tags)
2811
+ .filter(([key]) => key.trim().length > 0)
2812
+ .map(([key, value]) => `${sanitizeMetricToken(key)}:${String(value ?? "").replace(/[,|]+/g, "_")}`);
2813
+ return values.length ? `|#${values.join(",")}` : "";
2814
+ }
2815
+ function sendStatsDUdpLine(host, port, line) {
2816
+ return new Promise((resolve, reject) => {
2817
+ const socket = createSocket("udp4");
2818
+ const payload = Buffer.from(line, "utf8");
2819
+ socket.send(payload, port, host, (error) => {
2820
+ socket.close();
2821
+ if (error) {
2822
+ reject(error);
2823
+ return;
2824
+ }
2825
+ resolve();
2826
+ });
2827
+ });
2828
+ }
2580
2829
  function resolveTimeoutMs(timeoutSeconds, timeoutMs) {
2581
2830
  if (Number.isFinite(timeoutMs) && Number(timeoutMs) > 0) {
2582
2831
  return Math.max(Math.trunc(Number(timeoutMs)), 1);