@loadstrike/loadstrike-sdk 1.0.31601 → 1.0.32601

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/README.md +14 -2
  2. package/dist/cjs/internal/prometheus-remote-write.js +37 -0
  3. package/dist/cjs/internal/reporting-sink-http-error.js +17 -0
  4. package/dist/cjs/internal/vendor-metric-payloads.js +390 -0
  5. package/dist/cjs/iteration-observations.js +24 -8
  6. package/dist/cjs/local.js +48 -63
  7. package/dist/cjs/reporting-containment.js +242 -0
  8. package/dist/cjs/runtime.js +83 -3
  9. package/dist/cjs/sinks.js +1337 -38
  10. package/dist/cjs/transports.js +1339 -151
  11. package/dist/esm/internal/prometheus-remote-write.js +31 -0
  12. package/dist/esm/internal/reporting-sink-http-error.js +13 -0
  13. package/dist/esm/internal/vendor-metric-payloads.js +382 -0
  14. package/dist/esm/iteration-observations.js +24 -8
  15. package/dist/esm/local.js +49 -64
  16. package/dist/esm/reporting-containment.js +238 -0
  17. package/dist/esm/runtime.js +85 -5
  18. package/dist/esm/sinks.js +1334 -35
  19. package/dist/esm/transports.js +1335 -151
  20. package/dist/types/contracts.d.ts +1 -0
  21. package/dist/types/index.d.ts +1 -1
  22. package/dist/types/internal/prometheus-remote-write.d.ts +2 -0
  23. package/dist/types/internal/reporting-sink-http-error.d.ts +6 -0
  24. package/dist/types/internal/vendor-metric-payloads.d.ts +48 -0
  25. package/dist/types/local.d.ts +0 -6
  26. package/dist/types/reporting-containment.d.ts +2 -0
  27. package/dist/types/runtime.d.ts +1 -0
  28. package/dist/types/sinks.d.ts +134 -17
  29. package/dist/types/transports.d.ts +2 -0
  30. package/package.json +9 -3
  31. package/dist/cjs/internal-build.js +0 -4
  32. package/dist/esm/internal-build.js +0 -1
  33. package/dist/types/internal-build.d.ts +0 -1
package/dist/cjs/sinks.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.__loadstrikeTestExports = exports.JsonlFileReportingSink = exports.NetdataStatsDReportingSink = exports.DogStatsDReportingSink = exports.StatsDReportingSink = exports.KafkaReportingSink = exports.GenericWebhookReportingSink = exports.NewRelicReportingSink = exports.OpenSearchReportingSink = exports.ElasticsearchReportingSink = exports.DynatraceReportingSink = exports.CloudWatchReportingSink = exports.PrometheusRemoteWriteReportingSink = exports.OtelCollectorReportingSink = exports.SplunkReportingSink = exports.DatadogReportingSink = exports.TimescaleDbReportingSink = exports.GrafanaLokiReportingSink = exports.InfluxDbReportingSink = exports.PortalReportingSink = exports.CompositeReportingSink = exports.ConsoleReportingSink = exports.MemoryReportingSink = exports.OtelCollectorReportingSinkOptions = exports.SplunkReportingSinkOptions = exports.DatadogReportingSinkOptions = exports.TimescaleDbReportingSinkOptions = exports.GrafanaLokiReportingSinkOptions = exports.InfluxDbReportingSinkOptions = void 0;
3
+ exports.__loadstrikeTestExports = exports.JsonlFileReportingSink = exports.NetdataStatsDReportingSink = exports.DogStatsDReportingSink = exports.StatsDReportingSink = exports.KafkaReportingSink = exports.GenericWebhookReportingSink = exports.OpenSearchReportingSink = exports.ElasticsearchReportingSink = exports.NewRelicReportingSink = exports.DynatraceReportingSink = exports.CloudWatchReportingSink = exports.PrometheusRemoteWriteReportingSink = exports.OtelCollectorReportingSink = exports.SplunkReportingSink = exports.DatadogReportingSink = exports.TimescaleDbReportingSink = exports.GrafanaLokiReportingSink = exports.InfluxDbReportingSink = exports.PortalReportingSink = exports.CompositeReportingSink = exports.ConsoleReportingSink = exports.MemoryReportingSink = exports.OtelCollectorReportingSinkOptions = exports.SplunkReportingSinkOptions = exports.DatadogReportingSinkOptions = exports.TimescaleDbReportingSinkOptions = exports.GrafanaLokiReportingSinkOptions = exports.InfluxDbReportingSinkOptions = void 0;
4
4
  exports.cloneReportingSinkForRun = cloneReportingSinkForRun;
5
5
  const runtime_js_1 = require("./runtime.js");
6
6
  const node_crypto_1 = require("node:crypto");
@@ -8,28 +8,27 @@ const promises_1 = require("node:fs/promises");
8
8
  const node_path_1 = require("node:path");
9
9
  const node_dgram_1 = require("node:dgram");
10
10
  const node_zlib_1 = require("node:zlib");
11
+ const client_cloudwatch_1 = require("@aws-sdk/client-cloudwatch");
11
12
  const pg_1 = require("pg");
13
+ const snappyjs_1 = require("snappyjs");
12
14
  const iteration_observations_js_1 = require("./iteration-observations.js");
13
15
  const iteration_observation_diagnostics_js_1 = require("./iteration-observation-diagnostics.js");
16
+ const prometheus_remote_write_js_1 = require("./internal/prometheus-remote-write.js");
17
+ const reporting_sink_http_error_js_1 = require("./internal/reporting-sink-http-error.js");
18
+ const vendor_metric_payloads_js_1 = require("./internal/vendor-metric-payloads.js");
14
19
  const PORTAL_RUN_TOKEN_PROVIDER = Symbol.for("loadstrike.internal.portal-run-token-provider");
20
+ const REPORTING_INTERVAL_SECONDS = Symbol.for("loadstrike.internal.reporting-interval-seconds");
21
+ const SEND_DIRECT_VENDOR_PROTOCOL_POINTS = Symbol("loadstrike.internal.send-direct-vendor-protocol-points");
22
+ const CLONE_DIRECT_VENDOR_FOR_RUN = Symbol("loadstrike.internal.clone-direct-vendor-for-run");
23
+ const CLOUDWATCH_MAXIMUM_DATUMS_PER_REQUEST = 1000;
24
+ const CLOUDWATCH_SAFE_REQUEST_BODY_BYTES = 900 * 1024;
25
+ const DIRECT_VENDOR_MAXIMUM_BODY_BYTES_EXCLUSIVE = 1000000;
26
+ const MAXIMUM_NON_NEGATIVE_SIGNED_INT64 = 9223372036854775807n;
27
+ const LOADSTRIKE_TYPESCRIPT_SDK_USER_AGENT = "loadstrike-typescript-sdk/1.0.16101";
28
+ const EXPANDED_REPORTING_SINK_BINDINGS = new WeakMap();
15
29
  const MAXIMUM_HTTP_RESPONSE_BODY_BYTES = 256 * 1024;
16
30
  const MAXIMUM_HTTP_METADATA_CHARACTERS = 256;
17
31
  const HTTP_METADATA_TRUNCATION_SUFFIX = " [truncated]";
18
- class ReportingSinkHttpError extends Error {
19
- constructor(sinkName, status, statusText, requestId) {
20
- const normalizedStatus = Number.isFinite(status) ? Math.max(Math.trunc(status), 0) : 0;
21
- const normalizedStatusText = normalizeHttpMetadata(statusText);
22
- const normalizedRequestId = normalizeHttpMetadata(requestId);
23
- super(`${sinkName} write failed with HTTP status ${normalizedStatus}`
24
- + (normalizedStatusText ? ` ${normalizedStatusText}` : "")
25
- + (normalizedRequestId ? ` (requestId=${normalizedRequestId})` : "")
26
- + ".");
27
- this.name = "ReportingSinkHttpError";
28
- this.status = normalizedStatus;
29
- this.statusText = normalizedStatusText;
30
- this.requestId = normalizedRequestId;
31
- }
32
- }
33
32
  function gzipJsonAsync(value) {
34
33
  const json = Buffer.from(JSON.stringify(value), "utf8");
35
34
  return new Promise((resolve, reject) => {
@@ -578,12 +577,72 @@ class PortalReportingSink {
578
577
  }
579
578
  }
580
579
  exports.PortalReportingSink = PortalReportingSink;
581
- function cloneReportingSinkForRun(sink) {
580
+ function cloneReportingSinkForRun(sink, infraConfig = {}, environment = process.env) {
582
581
  if (sink instanceof PortalReportingSink) {
583
582
  return sink.cloneForRun();
584
583
  }
584
+ if (sink instanceof CompositeReportingSink) {
585
+ const children = sink.sinks;
586
+ if (sink.constructor !== CompositeReportingSink &&
587
+ !hasSelectedExpandedConfiguration(children, infraConfig, environment)) {
588
+ return sink;
589
+ }
590
+ const boundChildren = children.map((child) => cloneReportingSinkForRun(child, infraConfig, environment));
591
+ if (boundChildren.every((child, index) => child === children[index])) {
592
+ return sink;
593
+ }
594
+ if (sink.constructor !== CompositeReportingSink) {
595
+ throw new Error(`${sink.constructor.name || "Custom composite reporting sink"} cannot bind child configuration `
596
+ + "without an explicit clone implementation. Configure its children in code instead.");
597
+ }
598
+ return new CompositeReportingSink(...boundChildren);
599
+ }
600
+ const expandedBinding = EXPANDED_REPORTING_SINK_BINDINGS.get(sink);
601
+ if (expandedBinding) {
602
+ const configuredSection = readExpandedConfigSection(infraConfig, environment, expandedBinding.configurationSectionPath);
603
+ const expectedConstructor = expandedReportingSinkConstructor(expandedBinding.kind);
604
+ if (!configuredSection.present && sink.constructor !== expectedConstructor) {
605
+ return sink;
606
+ }
607
+ if (configuredSection.present) {
608
+ assertExpandedSinkSupportsConfigurationBinding(sink, expandedBinding.kind, configuredSection.path);
609
+ }
610
+ assertExpandedSinkExecutableOptions(expandedBinding);
611
+ if (!configuredSection.present) {
612
+ if (!(sink instanceof DirectVendorMetricReportingSink)) {
613
+ return sink;
614
+ }
615
+ return sink.constructor === expectedConstructor
616
+ ? sink[CLONE_DIRECT_VENDOR_FOR_RUN]()
617
+ : sink;
618
+ }
619
+ return createBoundExpandedReportingSink(sink, expandedBinding, infraConfig, environment);
620
+ }
621
+ if (sink instanceof DirectVendorMetricReportingSink) {
622
+ return sink[CLONE_DIRECT_VENDOR_FOR_RUN]();
623
+ }
585
624
  return sink;
586
625
  }
626
+ function hasSelectedExpandedConfiguration(sinks, infraConfig, environment, visited = new Set()) {
627
+ for (const sink of sinks) {
628
+ if (visited.has(sink)) {
629
+ continue;
630
+ }
631
+ visited.add(sink);
632
+ if (sink instanceof CompositeReportingSink) {
633
+ const children = sink.sinks;
634
+ if (hasSelectedExpandedConfiguration(children, infraConfig, environment, visited)) {
635
+ return true;
636
+ }
637
+ continue;
638
+ }
639
+ const binding = EXPANDED_REPORTING_SINK_BINDINGS.get(sink);
640
+ if (binding && readExpandedConfigSection(infraConfig, environment, binding.configurationSectionPath).present) {
641
+ return true;
642
+ }
643
+ }
644
+ return false;
645
+ }
587
646
  class InfluxDbReportingSink {
588
647
  constructor(options = {}) {
589
648
  this.sinkName = "influxdb";
@@ -1646,27 +1705,34 @@ class ExpandedEventReportingSink {
1646
1705
  }
1647
1706
  }
1648
1707
  class ExpandedHttpJsonReportingSink extends ExpandedEventReportingSink {
1649
- constructor(sinkName, licenseFeature, defaultEndpointPath, options = {}) {
1708
+ constructor(sinkName, licenseFeature, defaultEndpointPath, options = {}, bindingOptions = options) {
1650
1709
  super(sinkName, licenseFeature);
1651
1710
  const source = asRecord(options);
1652
1711
  this.baseUrl = optionString(source, "baseUrl", "BaseUrl").trim();
1712
+ this.exactUrl = optionString(source, "__loadstrikeExactUrl").trim();
1653
1713
  this.endpointPath = optionString(source, "endpointPath", "EndpointPath").trim() || defaultEndpointPath;
1654
1714
  this.headers = normalizeStringMap(optionRecord(source, "headers", "Headers"));
1655
1715
  this.staticTags = normalizeStringMap(optionRecord(source, "staticTags", "StaticTags"));
1656
1716
  this.timeoutMs = resolveTimeoutMs(optionNumber(source, "timeoutSeconds", "TimeoutSeconds"), optionNumber(source, "timeoutMs", "TimeoutMs"));
1657
1717
  this.fetchImpl = pickRecordValue(source, "fetchImpl", "FetchImpl") ?? fetch;
1718
+ const expandedKind = expandedReportingSinkKindFromName(sinkName);
1719
+ if (expandedKind) {
1720
+ rememberExpandedReportingSink(this, expandedKind, asRecord(bindingOptions));
1721
+ }
1658
1722
  }
1659
1723
  init(context, infraConfig) {
1660
1724
  super.init(context, infraConfig);
1661
- if (!this.baseUrl.trim()) {
1725
+ const configuredUrl = this.exactUrl || this.baseUrl;
1726
+ if (!configuredUrl) {
1662
1727
  throw new Error(`${this.constructor.name} requires BaseUrl.`);
1663
1728
  }
1729
+ requireHttpReportingUrl(configuredUrl, `${this.constructor.name} BaseUrl`);
1664
1730
  }
1665
1731
  async persistEvents(events) {
1666
1732
  if (!events.length) {
1667
1733
  return;
1668
1734
  }
1669
- await postWithTimeout(this.fetchImpl, `${trimTrailingSlashes(this.baseUrl)}${normalizePath(this.endpointPath)}`, {
1735
+ await postWithTimeout(this.fetchImpl, this.requestUrl(), {
1670
1736
  method: "POST",
1671
1737
  headers: {
1672
1738
  "Content-Type": "application/json",
@@ -1683,7 +1749,7 @@ class ExpandedHttpJsonReportingSink extends ExpandedEventReportingSink {
1683
1749
  }
1684
1750
  async persistCanonicalGzipPayload(payload) {
1685
1751
  const compressed = await gzipJsonAsync(payload);
1686
- await postWithTimeout(this.fetchImpl, `${trimTrailingSlashes(this.baseUrl)}${normalizePath(this.endpointPath)}`, {
1752
+ await postWithTimeout(this.fetchImpl, this.requestUrl(), {
1687
1753
  method: "POST",
1688
1754
  headers: {
1689
1755
  "Content-Type": "application/json",
@@ -1693,47 +1759,415 @@ class ExpandedHttpJsonReportingSink extends ExpandedEventReportingSink {
1693
1759
  body: Uint8Array.from(compressed).buffer
1694
1760
  }, this.timeoutMs, this.constructor.name);
1695
1761
  }
1762
+ requestUrl() {
1763
+ return this.exactUrl
1764
+ || `${trimTrailingSlashes(this.baseUrl)}${normalizePath(this.endpointPath)}`;
1765
+ }
1766
+ }
1767
+ function cloneDirectVendorSinkOptions(options) {
1768
+ const source = (options ?? {});
1769
+ return {
1770
+ ...source,
1771
+ ...(source.Headers ? { Headers: { ...source.Headers } } : {}),
1772
+ ...(source.headers ? { headers: { ...source.headers } } : {}),
1773
+ ...(source.StaticTags ? { StaticTags: { ...source.StaticTags } } : {}),
1774
+ ...(source.staticTags ? { staticTags: { ...source.staticTags } } : {})
1775
+ };
1696
1776
  }
1697
- class PrometheusRemoteWriteReportingSink extends ExpandedHttpJsonReportingSink {
1777
+ class DirectVendorMetricReportingSink extends ExpandedEventReportingSink {
1778
+ constructor(sinkName, licenseFeature, defaultEndpointPath, options, requireBaseUrl = true) {
1779
+ super(sinkName, licenseFeature);
1780
+ this.iterationObservationShapeLimited = true;
1781
+ this.previousCounts = new Map();
1782
+ this.previousCountTimes = new Map();
1783
+ this.stateGeneration = 0;
1784
+ this.stateQueue = Promise.resolve();
1785
+ const source = asRecord(options);
1786
+ this.baseUrl = optionString(source, "endpointUrl", "EndpointUrl", "baseUrl", "BaseUrl").trim();
1787
+ this.endpointPath = optionString(source, "endpointPath", "EndpointPath").trim() || defaultEndpointPath;
1788
+ this.headers = normalizeProtocolHeaders(normalizeStringMap(optionRecord(source, "headers", "Headers")));
1789
+ this.staticTags = normalizeStringMap(optionRecord(source, "staticTags", "StaticTags"));
1790
+ this.timeoutMs = resolveTimeoutMs(optionNumber(source, "timeoutSeconds", "TimeoutSeconds"), optionNumber(source, "timeoutMs", "TimeoutMs"));
1791
+ this.fetchImpl = pickRecordValue(source, "fetchImpl", "FetchImpl") ?? fetch;
1792
+ const expandedKind = expandedReportingSinkKindFromName(sinkName);
1793
+ if (expandedKind) {
1794
+ rememberExpandedReportingSink(this, expandedKind, source);
1795
+ }
1796
+ this.configuredRunId = optionString(source, "runId", "RunId").trim();
1797
+ this.requireBaseUrl = requireBaseUrl;
1798
+ this.configuredFirstIntervalMs = reportingIntervalMilliseconds(optionNumber(source, "reportingIntervalSeconds", "ReportingIntervalSeconds"));
1799
+ }
1800
+ init(context, infraConfig) {
1801
+ super.init(context, infraConfig);
1802
+ const runtimeInterval = context[REPORTING_INTERVAL_SECONDS];
1803
+ if (runtimeInterval !== undefined) {
1804
+ this.configuredFirstIntervalMs = reportingIntervalMilliseconds(Number(runtimeInterval));
1805
+ }
1806
+ if (this.requireBaseUrl && !this.baseUrl) {
1807
+ throw new Error(`${this.constructor.name} requires BaseUrl.`);
1808
+ }
1809
+ if (this.baseUrl) {
1810
+ requireHttpReportingUrl(this.baseUrl, `${this.constructor.name} BaseUrl`);
1811
+ }
1812
+ }
1813
+ start(session) {
1814
+ super.start(session);
1815
+ this.stateGeneration += 1;
1816
+ this.previousCounts.clear();
1817
+ this.previousCountTimes.clear();
1818
+ const parsedStartedUtc = new Date(this.getSession().startedUtc);
1819
+ this.runStartedUtc = Number.isFinite(parsedStartedUtc.getTime()) ? parsedStartedUtc : undefined;
1820
+ }
1821
+ stop() {
1822
+ this.stateGeneration += 1;
1823
+ this.previousCounts.clear();
1824
+ this.previousCountTimes.clear();
1825
+ this.runStartedUtc = undefined;
1826
+ super.stop();
1827
+ }
1828
+ async persistEvents(events) {
1829
+ if (!events.length) {
1830
+ return;
1831
+ }
1832
+ const generation = this.stateGeneration;
1833
+ const eventRunId = String(events[0]?.sessionId ?? "").trim();
1834
+ const operation = this.stateQueue.then(async () => {
1835
+ const occurredUtc = new Date();
1836
+ const points = createDirectVendorMetricPoints(events, occurredUtc, this.configuredRunId || eventRunId, this.sinkName);
1837
+ if (!points.length) {
1838
+ return;
1839
+ }
1840
+ const prepared = this.prepareStatefulPoints(points);
1841
+ await this[SEND_DIRECT_VENDOR_PROTOCOL_POINTS](prepared.points);
1842
+ if (generation === this.stateGeneration) {
1843
+ this.previousCounts = prepared.counts;
1844
+ this.previousCountTimes = prepared.countTimes;
1845
+ }
1846
+ });
1847
+ this.stateQueue = operation.then(() => undefined, () => undefined);
1848
+ await operation;
1849
+ }
1850
+ prepareStatefulPoints(points) {
1851
+ const counts = new Map(this.previousCounts);
1852
+ const countTimes = new Map(Array.from(this.previousCountTimes, ([key, value]) => [key, new Date(value.getTime())]));
1853
+ const prepared = [];
1854
+ for (const point of points) {
1855
+ if (point.metricKind !== "count") {
1856
+ prepared.push(point);
1857
+ continue;
1858
+ }
1859
+ const key = directVendorMetricSeriesKey(point);
1860
+ const previousValue = counts.get(key);
1861
+ const previousTime = countTimes.get(key);
1862
+ const intervalMs = previousValue === undefined && this.configuredFirstIntervalMs !== undefined
1863
+ ? this.configuredFirstIntervalMs
1864
+ : positiveIntervalMilliseconds(previousTime ?? this.runStartedUtc, point.occurredUtc);
1865
+ const value = this.sinkName === "prometheus-remote-write" || previousValue === undefined
1866
+ ? point.value
1867
+ : point.value >= previousValue
1868
+ ? point.value - previousValue
1869
+ : point.value;
1870
+ prepared.push({ ...point, value, intervalMs });
1871
+ counts.set(key, point.value);
1872
+ countTimes.set(key, new Date(point.occurredUtc.getTime()));
1873
+ }
1874
+ return { points: prepared, counts, countTimes };
1875
+ }
1876
+ }
1877
+ class PrometheusRemoteWriteReportingSink extends DirectVendorMetricReportingSink {
1698
1878
  constructor(options = {}) {
1699
1879
  super("prometheus-remote-write", "extensions.reporting_sinks.prometheus_remote_write", "/api/v1/write", options);
1700
- this.iterationObservationShapeLimited = true;
1880
+ this.bearerToken = optionString(asRecord(options), "bearerToken", "BearerToken").trim();
1881
+ this.cloneOptions = cloneDirectVendorSinkOptions(options);
1882
+ rememberExpandedReportingSink(this, "PrometheusRemoteWrite", asRecord(options));
1883
+ }
1884
+ [CLONE_DIRECT_VENDOR_FOR_RUN]() {
1885
+ return new PrometheusRemoteWriteReportingSink(cloneDirectVendorSinkOptions(this.cloneOptions));
1886
+ }
1887
+ async [SEND_DIRECT_VENDOR_PROTOCOL_POINTS](points) {
1888
+ const encoded = (0, prometheus_remote_write_js_1.encodePrometheusRemoteWrite)(points, this.staticTags);
1889
+ const body = Uint8Array.from((0, snappyjs_1.compress)(encoded.body));
1890
+ await postWithTimeout(this.fetchImpl, `${trimTrailingSlashes(this.baseUrl)}${normalizePath(this.endpointPath)}`, {
1891
+ method: "POST",
1892
+ headers: mergeProtocolHeaders(this.headers, {
1893
+ ...(this.bearerToken ? { Authorization: `Bearer ${this.bearerToken}` } : {}),
1894
+ "Content-Type": encoded.contentType,
1895
+ "Content-Encoding": "snappy",
1896
+ "X-Prometheus-Remote-Write-Version": "0.1.0",
1897
+ "User-Agent": LOADSTRIKE_TYPESCRIPT_SDK_USER_AGENT
1898
+ }),
1899
+ body: body.buffer
1900
+ }, this.timeoutMs, this.constructor.name, true);
1701
1901
  }
1702
1902
  }
1703
1903
  exports.PrometheusRemoteWriteReportingSink = PrometheusRemoteWriteReportingSink;
1704
- class CloudWatchReportingSink extends ExpandedHttpJsonReportingSink {
1904
+ class CloudWatchReportingSink extends DirectVendorMetricReportingSink {
1705
1905
  constructor(options = {}) {
1706
- super("cloudwatch", "extensions.reporting_sinks.cloudwatch", "/loadstrike/events", options);
1707
- this.iterationObservationShapeLimited = true;
1906
+ super("cloudwatch", "extensions.reporting_sinks.cloudwatch", "/", options, false);
1907
+ const source = asRecord(options);
1908
+ this.namespace = optionString(source, "namespace", "Namespace").trim() || "LoadStrike";
1909
+ this.region = optionString(source, "region", "Region").trim();
1910
+ this.accessKeyId = optionString(source, "accessKeyId", "AccessKeyId").trim();
1911
+ this.secretAccessKey = optionString(source, "secretAccessKey", "SecretAccessKey").trim();
1912
+ this.sessionToken = optionString(source, "sessionToken", "SessionToken").trim();
1913
+ this.cloneOptions = cloneDirectVendorSinkOptions(options);
1914
+ rememberExpandedReportingSink(this, "CloudWatch", source);
1915
+ }
1916
+ [CLONE_DIRECT_VENDOR_FOR_RUN]() {
1917
+ return new CloudWatchReportingSink(cloneDirectVendorSinkOptions(this.cloneOptions));
1918
+ }
1919
+ init(context, infraConfig) {
1920
+ if (normalizePath(this.endpointPath) !== "/") {
1921
+ throw new Error("CloudWatchReportingSink does not support EndpointPath; include any required path in EndpointUrl.");
1922
+ }
1923
+ if (!this.namespace) {
1924
+ throw new Error("CloudWatchReportingSink requires Namespace.");
1925
+ }
1926
+ if (!this.region) {
1927
+ throw new Error("CloudWatchReportingSink requires Region.");
1928
+ }
1929
+ if (!this.accessKeyId) {
1930
+ throw new Error("CloudWatchReportingSink requires AccessKeyId.");
1931
+ }
1932
+ if (!this.secretAccessKey) {
1933
+ throw new Error("CloudWatchReportingSink requires SecretAccessKey.");
1934
+ }
1935
+ super.init(context, infraConfig);
1936
+ this.cloudWatchClient?.destroy();
1937
+ this.cloudWatchClient = new client_cloudwatch_1.CloudWatchClient({
1938
+ region: this.region,
1939
+ credentials: {
1940
+ accessKeyId: this.accessKeyId,
1941
+ secretAccessKey: this.secretAccessKey,
1942
+ ...(this.sessionToken ? { sessionToken: this.sessionToken } : {})
1943
+ },
1944
+ ...(this.baseUrl ? { endpoint: this.baseUrl } : {}),
1945
+ maxAttempts: 1
1946
+ });
1947
+ const customHeaders = Object.fromEntries(Object.entries(this.headers).filter(([name]) => !isCloudWatchProtocolHeader(name)));
1948
+ if (Object.keys(customHeaders).length > 0) {
1949
+ this.cloudWatchClient.middlewareStack.add((next) => async (args) => {
1950
+ const request = args.request;
1951
+ if (request?.headers) {
1952
+ const merged = mergeProtocolHeaders(request.headers, customHeaders);
1953
+ for (const name of Object.keys(request.headers)) {
1954
+ delete request.headers[name];
1955
+ }
1956
+ Object.assign(request.headers, merged);
1957
+ }
1958
+ return next(args);
1959
+ }, { step: "build", name: "loadStrikeCloudWatchHeaders", priority: "low" });
1960
+ }
1961
+ }
1962
+ Dispose() {
1963
+ this.cloudWatchClient?.destroy();
1964
+ this.cloudWatchClient = undefined;
1965
+ super.Dispose();
1966
+ }
1967
+ async [SEND_DIRECT_VENDOR_PROTOCOL_POINTS](points) {
1968
+ const client = this.cloudWatchClient;
1969
+ if (!client) {
1970
+ throw new Error("CloudWatchReportingSink has not been initialized.");
1971
+ }
1972
+ const payload = (0, vendor_metric_payloads_js_1.createCloudWatchMetricData)(this.namespace, points, this.staticTags);
1973
+ const controller = new AbortController();
1974
+ const timer = setTimeout(() => controller.abort(), Math.max(this.timeoutMs, 1));
1975
+ try {
1976
+ for (const batch of createCloudWatchMetricDataBatches(payload.Namespace, payload.MetricData)) {
1977
+ await client.send(new client_cloudwatch_1.PutMetricDataCommand(batch), { abortSignal: controller.signal });
1978
+ }
1979
+ }
1980
+ catch (error) {
1981
+ if (controller.signal.aborted) {
1982
+ throw new DOMException("CloudWatchReportingSink request aborted.", "AbortError");
1983
+ }
1984
+ void error;
1985
+ throw new Error("CloudWatchReportingSink write failed.");
1986
+ }
1987
+ finally {
1988
+ clearTimeout(timer);
1989
+ }
1708
1990
  }
1709
1991
  }
1710
1992
  exports.CloudWatchReportingSink = CloudWatchReportingSink;
1711
- class DynatraceReportingSink extends ExpandedHttpJsonReportingSink {
1993
+ class DynatraceReportingSink extends DirectVendorMetricReportingSink {
1712
1994
  constructor(options = {}) {
1713
- super("dynatrace", "extensions.reporting_sinks.dynatrace", "/api/v2/logs/ingest", options);
1714
- this.iterationObservationShapeLimited = true;
1995
+ super("dynatrace", "extensions.reporting_sinks.dynatrace", "/api/v2/metrics/ingest", options);
1996
+ this.apiToken = optionString(asRecord(options), "apiToken", "ApiToken").trim();
1997
+ this.cloneOptions = cloneDirectVendorSinkOptions(options);
1998
+ rememberExpandedReportingSink(this, "Dynatrace", asRecord(options));
1999
+ }
2000
+ [CLONE_DIRECT_VENDOR_FOR_RUN]() {
2001
+ return new DynatraceReportingSink(cloneDirectVendorSinkOptions(this.cloneOptions));
2002
+ }
2003
+ init(context, infraConfig) {
2004
+ if (!this.apiToken && !hasCaseInsensitiveHeader(this.headers, "Authorization")) {
2005
+ throw new Error("DynatraceReportingSink requires ApiToken.");
2006
+ }
2007
+ super.init(context, infraConfig);
2008
+ }
2009
+ async [SEND_DIRECT_VENDOR_PROTOCOL_POINTS](points) {
2010
+ const batches = (0, vendor_metric_payloads_js_1.encodeDynatraceMetricBatches)(points, this.staticTags, DIRECT_VENDOR_MAXIMUM_BODY_BYTES_EXCLUSIVE);
2011
+ for (const encoded of batches) {
2012
+ await postWithTimeout(this.fetchImpl, `${trimTrailingSlashes(this.baseUrl)}${normalizePath(this.endpointPath)}`, {
2013
+ method: "POST",
2014
+ headers: mergeProtocolHeaders(this.headers, {
2015
+ ...(this.apiToken ? { Authorization: `Api-Token ${this.apiToken}` } : {}),
2016
+ "Content-Type": encoded.contentType
2017
+ }),
2018
+ body: Uint8Array.from(encoded.body).buffer
2019
+ }, this.timeoutMs, this.constructor.name);
2020
+ }
1715
2021
  }
1716
2022
  }
1717
2023
  exports.DynatraceReportingSink = DynatraceReportingSink;
2024
+ class NewRelicReportingSink extends DirectVendorMetricReportingSink {
2025
+ constructor(options = {}) {
2026
+ super("new-relic", "extensions.reporting_sinks.new_relic", "/metric/v1", options);
2027
+ this.licenseKey = optionString(asRecord(options), "licenseKey", "LicenseKey", "apiKey", "ApiKey").trim();
2028
+ this.cloneOptions = cloneDirectVendorSinkOptions(options);
2029
+ rememberExpandedReportingSink(this, "NewRelic", asRecord(options));
2030
+ }
2031
+ [CLONE_DIRECT_VENDOR_FOR_RUN]() {
2032
+ return new NewRelicReportingSink(cloneDirectVendorSinkOptions(this.cloneOptions));
2033
+ }
2034
+ init(context, infraConfig) {
2035
+ if (!this.licenseKey && !hasCaseInsensitiveHeader(this.headers, "Api-Key")) {
2036
+ throw new Error("NewRelicReportingSink requires LicenseKey or ApiKey.");
2037
+ }
2038
+ super.init(context, infraConfig);
2039
+ }
2040
+ async [SEND_DIRECT_VENDOR_PROTOCOL_POINTS](points) {
2041
+ const batches = (0, vendor_metric_payloads_js_1.encodeNewRelicMetricBatches)(points, this.staticTags, DIRECT_VENDOR_MAXIMUM_BODY_BYTES_EXCLUSIVE);
2042
+ for (const encoded of batches) {
2043
+ await postWithTimeout(this.fetchImpl, `${trimTrailingSlashes(this.baseUrl)}${normalizePath(this.endpointPath)}`, {
2044
+ method: "POST",
2045
+ headers: mergeProtocolHeaders(this.headers, {
2046
+ ...(this.licenseKey ? { "Api-Key": this.licenseKey } : {}),
2047
+ "Content-Type": encoded.contentType
2048
+ }),
2049
+ body: Uint8Array.from(encoded.body).buffer
2050
+ }, this.timeoutMs, this.constructor.name);
2051
+ }
2052
+ }
2053
+ }
2054
+ exports.NewRelicReportingSink = NewRelicReportingSink;
1718
2055
  class ElasticsearchReportingSink extends ExpandedHttpJsonReportingSink {
1719
2056
  constructor(options = {}) {
1720
- super("elasticsearch", "extensions.reporting_sinks.elasticsearch", "/loadstrike-events/_doc", options);
2057
+ const prepared = prepareSearchSinkOptions("Elasticsearch", options);
2058
+ const defaultEndpointPath = prepared.usesIndexName
2059
+ ? searchDocumentEndpointPath(prepared.indexName, "ElasticsearchReportingSink IndexName")
2060
+ : "/loadstrike-events/_doc";
2061
+ super("elasticsearch", "extensions.reporting_sinks.elasticsearch", defaultEndpointPath, prepared.options, options);
2062
+ this.indexName = prepared.indexName;
2063
+ this.usesIndexName = prepared.usesIndexName;
2064
+ }
2065
+ init(context, infraConfig) {
2066
+ if (this.usesIndexName) {
2067
+ requireValidSearchIndexName(this.indexName, `${this.constructor.name} IndexName`);
2068
+ }
2069
+ super.init(context, infraConfig);
1721
2070
  }
1722
2071
  }
1723
2072
  exports.ElasticsearchReportingSink = ElasticsearchReportingSink;
1724
2073
  class OpenSearchReportingSink extends ExpandedHttpJsonReportingSink {
1725
2074
  constructor(options = {}) {
1726
- super("opensearch", "extensions.reporting_sinks.opensearch", "/loadstrike-events/_doc", options);
2075
+ const prepared = prepareSearchSinkOptions("OpenSearch", options);
2076
+ const defaultEndpointPath = prepared.usesIndexName
2077
+ ? searchDocumentEndpointPath(prepared.indexName, "OpenSearchReportingSink IndexName")
2078
+ : "/loadstrike-events/_doc";
2079
+ super("opensearch", "extensions.reporting_sinks.opensearch", defaultEndpointPath, prepared.options, options);
2080
+ this.indexName = prepared.indexName;
2081
+ this.usesIndexName = prepared.usesIndexName;
2082
+ this.username = prepared.username;
2083
+ this.password = prepared.password;
2084
+ }
2085
+ init(context, infraConfig) {
2086
+ if (this.usesIndexName) {
2087
+ requireValidSearchIndexName(this.indexName, `${this.constructor.name} IndexName`);
2088
+ }
2089
+ if (this.username && !this.password) {
2090
+ throw new Error(`${this.constructor.name} requires Password when Username is configured.`);
2091
+ }
2092
+ if (this.password && !this.username) {
2093
+ throw new Error(`${this.constructor.name} requires Username when Password is configured.`);
2094
+ }
2095
+ super.init(context, infraConfig);
1727
2096
  }
1728
2097
  }
1729
2098
  exports.OpenSearchReportingSink = OpenSearchReportingSink;
1730
- class NewRelicReportingSink extends ExpandedHttpJsonReportingSink {
1731
- constructor(options = {}) {
1732
- super("new-relic", "extensions.reporting_sinks.new_relic", "/log/v1", options);
1733
- this.iterationObservationShapeLimited = true;
2099
+ function prepareSearchSinkOptions(kind, options) {
2100
+ const source = asRecord(options);
2101
+ const rawIndexName = pickRecordValue(source, "indexName", "IndexName");
2102
+ const endpointPath = optionString(source, "endpointPath", "EndpointPath").trim();
2103
+ const indexName = rawIndexName === undefined
2104
+ ? "loadstrike-events"
2105
+ : typeof rawIndexName === "string"
2106
+ ? rawIndexName.trim()
2107
+ : "";
2108
+ let headers = normalizeStringMap(optionRecord(source, "headers", "Headers"));
2109
+ let username = "";
2110
+ let password = "";
2111
+ if (kind === "Elasticsearch") {
2112
+ const apiKey = optionString(source, "apiKey", "ApiKey").trim();
2113
+ if (apiKey && !hasCaseInsensitiveHeader(headers, "Authorization")) {
2114
+ headers = mergeProtocolHeaders(headers, { Authorization: `ApiKey ${apiKey}` });
2115
+ }
1734
2116
  }
2117
+ else {
2118
+ username = optionString(source, "username", "Username").trim();
2119
+ password = optionString(source, "password", "Password");
2120
+ if ((username || password) && !hasCaseInsensitiveHeader(headers, "Authorization")) {
2121
+ headers = mergeProtocolHeaders(headers, {
2122
+ Authorization: `Basic ${Buffer.from(`${username}:${password}`, "utf8").toString("base64")}`
2123
+ });
2124
+ }
2125
+ }
2126
+ return {
2127
+ indexName,
2128
+ usesIndexName: !endpointPath,
2129
+ username,
2130
+ password,
2131
+ options: { ...source, headers }
2132
+ };
2133
+ }
2134
+ function searchDocumentEndpointPath(indexName, path) {
2135
+ if (hasUnpairedUnicodeSurrogate(indexName)) {
2136
+ throw new Error(`${path} must be a valid search index name.`);
2137
+ }
2138
+ return `/${encodeURIComponent(indexName)}/_doc`;
2139
+ }
2140
+ function requireValidSearchIndexName(indexName, path) {
2141
+ const byteLength = Buffer.byteLength(indexName, "utf8");
2142
+ const invalid = !indexName
2143
+ || indexName !== indexName.toLowerCase()
2144
+ || byteLength > 255
2145
+ || indexName === "."
2146
+ || indexName === ".."
2147
+ || /^[\-_+]/u.test(indexName)
2148
+ || /[?*\u002f\u005c\x22<>| ,#:]/u.test(indexName)
2149
+ || /[\u0000-\u001f\u007f]/u.test(indexName)
2150
+ || hasUnpairedUnicodeSurrogate(indexName);
2151
+ if (invalid) {
2152
+ throw new Error(`${path} must be a valid search index name.`);
2153
+ }
2154
+ }
2155
+ function hasUnpairedUnicodeSurrogate(value) {
2156
+ for (let index = 0; index < value.length; index += 1) {
2157
+ const codeUnit = value.charCodeAt(index);
2158
+ if (codeUnit >= 0xd800 && codeUnit <= 0xdbff) {
2159
+ const next = index + 1 < value.length ? value.charCodeAt(index + 1) : -1;
2160
+ if (next < 0xdc00 || next > 0xdfff) {
2161
+ return true;
2162
+ }
2163
+ index += 1;
2164
+ }
2165
+ else if (codeUnit >= 0xdc00 && codeUnit <= 0xdfff) {
2166
+ return true;
2167
+ }
2168
+ }
2169
+ return false;
1735
2170
  }
1736
- exports.NewRelicReportingSink = NewRelicReportingSink;
1737
2171
  class GenericWebhookReportingSink extends ExpandedHttpJsonReportingSink {
1738
2172
  constructor(options = {}) {
1739
2173
  super("webhook", "extensions.reporting_sinks.webhook", "/loadstrike", options);
@@ -1758,8 +2192,17 @@ class KafkaReportingSink extends ExpandedEventReportingSink {
1758
2192
  super("kafka", "extensions.reporting_sinks.kafka");
1759
2193
  const source = asRecord(options);
1760
2194
  this.topic = optionString(source, "topic", "Topic").trim() || "loadstrike-events";
2195
+ this.bootstrapServers = optionString(source, "bootstrapServers", "BootstrapServers").trim();
1761
2196
  this.staticTags = normalizeStringMap(optionRecord(source, "staticTags", "StaticTags"));
1762
2197
  this.publishAsync = pickRecordValue(source, "publishAsync", "PublishAsync");
2198
+ rememberExpandedReportingSink(this, "Kafka", source);
2199
+ }
2200
+ init(context, infraConfig) {
2201
+ if (!this.publishAsync) {
2202
+ throw new Error("KafkaReportingSink requires PublishAsync because TypeScript Kafka reporting is callback-backed; "
2203
+ + "BootstrapServers does not enable native broker delivery.");
2204
+ }
2205
+ super.init(context, infraConfig);
1763
2206
  }
1764
2207
  async saveIterationBatch(batch) {
1765
2208
  await this.publishCanonicalValue(batch);
@@ -1795,6 +2238,10 @@ class StatsDReportingSinkBase extends ExpandedEventReportingSink {
1795
2238
  this.tags = normalizeStringMap(optionRecord(source, "tags", "Tags"));
1796
2239
  this.sendLineAsync = pickRecordValue(source, "sendLineAsync", "SendLineAsync");
1797
2240
  this.dogStatsD = dogStatsD;
2241
+ const expandedKind = expandedReportingSinkKindFromName(sinkName);
2242
+ if (expandedKind) {
2243
+ rememberExpandedReportingSink(this, expandedKind, source);
2244
+ }
1798
2245
  }
1799
2246
  async saveIterationBatch(batch) {
1800
2247
  for (const observation of batch.observations) {
@@ -1884,6 +2331,7 @@ class JsonlFileReportingSink extends ExpandedEventReportingSink {
1884
2331
  const source = asRecord(options);
1885
2332
  this.filePath = optionString(source, "filePath", "FilePath").trim() || "loadstrike-reporting.jsonl";
1886
2333
  this.staticTags = normalizeStringMap(optionRecord(source, "staticTags", "StaticTags"));
2334
+ rememberExpandedReportingSink(this, "Jsonl", source);
1887
2335
  }
1888
2336
  async persistEvents(events) {
1889
2337
  if (!events.length) {
@@ -2078,6 +2526,8 @@ function createScenarioEvent(session, occurredUtc, phase, scenario) {
2078
2526
  all_request_count: scenario.allRequestCount,
2079
2527
  all_ok_count: scenario.allOkCount,
2080
2528
  all_fail_count: scenario.allFailCount,
2529
+ all_ok_count_64: scenario.ok?.count64 ?? scenario.ok?.Count64,
2530
+ all_fail_count_64: scenario.fail?.count64 ?? scenario.fail?.Count64,
2081
2531
  all_bytes: scenario.allBytes,
2082
2532
  duration_ms: scenario.durationMs,
2083
2533
  sort_index: scenario.sortIndex,
@@ -2607,6 +3057,120 @@ function toReportingSinkEventJson(event) {
2607
3057
  Fields: event.fields
2608
3058
  });
2609
3059
  }
3060
+ function createDirectVendorMetricPoints(events, occurredUtc, runId, sinkName) {
3061
+ const points = [];
3062
+ for (const event of events) {
3063
+ const eventType = String(event.eventType ?? "").trim().toLowerCase();
3064
+ const scenario = String(event.scenarioName ?? "").trim() || "default";
3065
+ const status = String(event.tags?.status ?? "").trim() || "ok";
3066
+ if (eventType === "scenario.realtime" || eventType === "scenario.final") {
3067
+ let statusCountPresent = false;
3068
+ for (const scenarioStatus of ["ok", "fail"]) {
3069
+ const count = protocolCountValue(sinkName, event.fields[`all_${scenarioStatus}_count_64`], event.fields[`all_${scenarioStatus}_count`], event.fields[`${scenarioStatus}_request_count`]);
3070
+ statusCountPresent || (statusCountPresent = count.present);
3071
+ if (!count.present || count.value === 0) {
3072
+ continue;
3073
+ }
3074
+ points.push({
3075
+ metricName: "loadstrike_requests_total",
3076
+ metricKind: "count",
3077
+ occurredUtc: new Date(occurredUtc.getTime()),
3078
+ value: count.value,
3079
+ unitOfMeasure: "count",
3080
+ scenario,
3081
+ status: scenarioStatus,
3082
+ ...(runId ? { runId } : {}),
3083
+ intervalMs: 1
3084
+ });
3085
+ const latency = event.fields[`${scenarioStatus}_latency_mean_ms`];
3086
+ if (isFiniteNumberLike(latency)) {
3087
+ points.push({
3088
+ metricName: "loadstrike_request_duration_ms",
3089
+ metricKind: "gauge",
3090
+ occurredUtc: new Date(occurredUtc.getTime()),
3091
+ value: toNumericValue(latency),
3092
+ unitOfMeasure: "ms",
3093
+ scenario,
3094
+ status: scenarioStatus,
3095
+ ...(runId ? { runId } : {})
3096
+ });
3097
+ }
3098
+ }
3099
+ if (!statusCountPresent && isFiniteNumberLike(event.fields.all_request_count)) {
3100
+ points.push({
3101
+ metricName: "loadstrike_requests_total",
3102
+ metricKind: "count",
3103
+ occurredUtc: new Date(occurredUtc.getTime()),
3104
+ value: toNumericValue(event.fields.all_request_count),
3105
+ unitOfMeasure: "count",
3106
+ scenario,
3107
+ status,
3108
+ ...(runId ? { runId } : {}),
3109
+ intervalMs: 1
3110
+ });
3111
+ }
3112
+ continue;
3113
+ }
3114
+ const isCounter = eventType.startsWith("metric.counter");
3115
+ const isGauge = eventType.startsWith("metric.gauge");
3116
+ if ((!isCounter && !isGauge) || !isFiniteNumberLike(event.fields.value)) {
3117
+ continue;
3118
+ }
3119
+ const unit = event.fields.unit_of_measure;
3120
+ points.push({
3121
+ metricName: buildCustomMetricName(String(event.tags?.metric_name ?? "")),
3122
+ metricKind: isCounter ? "count" : "gauge",
3123
+ occurredUtc: new Date(occurredUtc.getTime()),
3124
+ value: toNumericValue(event.fields.value),
3125
+ ...(typeof unit === "string" ? { unitOfMeasure: unit } : {}),
3126
+ scenario,
3127
+ status,
3128
+ ...(runId ? { runId } : {}),
3129
+ ...(isCounter ? { intervalMs: 1 } : {})
3130
+ });
3131
+ }
3132
+ return points;
3133
+ }
3134
+ function directVendorMetricSeriesKey(point) {
3135
+ return JSON.stringify([
3136
+ point.metricName,
3137
+ point.scenario,
3138
+ point.status,
3139
+ point.runId ?? ""
3140
+ ]);
3141
+ }
3142
+ function createCloudWatchMetricDataBatches(namespace, metricData) {
3143
+ const emptyBodyBytes = Buffer.byteLength(JSON.stringify({ Namespace: namespace, MetricData: [] }), "utf8");
3144
+ const batches = [];
3145
+ let current = [];
3146
+ let currentBodyBytes = emptyBodyBytes;
3147
+ for (const datum of metricData) {
3148
+ const datumBodyBytes = Buffer.byteLength(JSON.stringify(datum), "utf8");
3149
+ const separatorBytes = current.length > 0 ? 1 : 0;
3150
+ const exceedsDatumLimit = current.length >= CLOUDWATCH_MAXIMUM_DATUMS_PER_REQUEST;
3151
+ const exceedsBodyLimit = currentBodyBytes + separatorBytes + datumBodyBytes > CLOUDWATCH_SAFE_REQUEST_BODY_BYTES;
3152
+ if (current.length > 0 && (exceedsDatumLimit || exceedsBodyLimit)) {
3153
+ batches.push({
3154
+ Namespace: namespace,
3155
+ MetricData: current
3156
+ });
3157
+ current = [];
3158
+ currentBodyBytes = emptyBodyBytes;
3159
+ }
3160
+ if (currentBodyBytes + (current.length > 0 ? 1 : 0) + datumBodyBytes > CLOUDWATCH_SAFE_REQUEST_BODY_BYTES) {
3161
+ throw new Error("CloudWatchReportingSink metric datum exceeds the safe request size.");
3162
+ }
3163
+ currentBodyBytes += (current.length > 0 ? 1 : 0) + datumBodyBytes;
3164
+ current.push(datum);
3165
+ }
3166
+ if (current.length > 0) {
3167
+ batches.push({
3168
+ Namespace: namespace,
3169
+ MetricData: current
3170
+ });
3171
+ }
3172
+ return batches;
3173
+ }
2610
3174
  function createReportingSinkMetricPoints(sinkName, events, staticTags) {
2611
3175
  const points = [];
2612
3176
  for (const event of events) {
@@ -2718,6 +3282,45 @@ function toNumericValue(value) {
2718
3282
  }
2719
3283
  return Number(value);
2720
3284
  }
3285
+ function protocolCountValue(sinkName, ...values) {
3286
+ let zeroPresent = false;
3287
+ for (const value of values) {
3288
+ if (value === undefined || value === null) {
3289
+ continue;
3290
+ }
3291
+ let parsed;
3292
+ if (typeof value === "string") {
3293
+ const text = value.trim();
3294
+ if (!text) {
3295
+ continue;
3296
+ }
3297
+ if (!/^\d+$/.test(text)) {
3298
+ throw invalidProtocolRequestCount(sinkName);
3299
+ }
3300
+ parsed = BigInt(text);
3301
+ }
3302
+ else if (typeof value === "bigint") {
3303
+ parsed = value;
3304
+ }
3305
+ else if (typeof value === "number" && Number.isSafeInteger(value)) {
3306
+ parsed = BigInt(value);
3307
+ }
3308
+ else {
3309
+ throw invalidProtocolRequestCount(sinkName);
3310
+ }
3311
+ if (parsed < 0n || parsed > MAXIMUM_NON_NEGATIVE_SIGNED_INT64) {
3312
+ throw invalidProtocolRequestCount(sinkName);
3313
+ }
3314
+ if (parsed !== 0n) {
3315
+ return { value: Number(parsed), present: true };
3316
+ }
3317
+ zeroPresent = true;
3318
+ }
3319
+ return { value: 0, present: zeroPresent };
3320
+ }
3321
+ function invalidProtocolRequestCount(sinkName) {
3322
+ return new Error(`${sinkName} request count must be a non-negative signed 64-bit integer.`);
3323
+ }
2721
3324
  function inferUnitOfMeasure(fieldName) {
2722
3325
  const normalized = fieldName.toLowerCase();
2723
3326
  if (normalized.endsWith("_ms")) {
@@ -3231,6 +3834,620 @@ function mergeOtelCollectorOptions(target, source) {
3231
3834
  target.staticResourceAttributes = normalizeStringMap(source.staticResourceAttributes);
3232
3835
  }
3233
3836
  }
3837
+ const DEFAULT_EXPANDED_REPORTING_SECTION_PATHS = {
3838
+ PrometheusRemoteWrite: "LoadStrike:ReportingSinks:PrometheusRemoteWrite",
3839
+ CloudWatch: "LoadStrike:ReportingSinks:CloudWatch",
3840
+ Dynatrace: "LoadStrike:ReportingSinks:Dynatrace",
3841
+ Elasticsearch: "LoadStrike:ReportingSinks:Elasticsearch",
3842
+ OpenSearch: "LoadStrike:ReportingSinks:OpenSearch",
3843
+ NewRelic: "LoadStrike:ReportingSinks:NewRelic",
3844
+ Webhook: "LoadStrike:ReportingSinks:Webhook",
3845
+ Kafka: "LoadStrike:ReportingSinks:Kafka",
3846
+ StatsD: "LoadStrike:ReportingSinks:StatsD",
3847
+ Netdata: "LoadStrike:ReportingSinks:Netdata",
3848
+ DogStatsD: "LoadStrike:ReportingSinks:DogStatsD",
3849
+ Jsonl: "LoadStrike:ReportingSinks:Jsonl"
3850
+ };
3851
+ const EXPANDED_HTTP_CONFIG_FIELDS = [
3852
+ { canonicalName: "BaseUrl", valueKind: "string" },
3853
+ { canonicalName: "EndpointPath", valueKind: "string" },
3854
+ { canonicalName: "Headers", valueKind: "stringMap" },
3855
+ { canonicalName: "StaticTags", valueKind: "stringMap" },
3856
+ { canonicalName: "TimeoutSeconds", valueKind: "number" },
3857
+ { canonicalName: "TimeoutMs", valueKind: "number" }
3858
+ ];
3859
+ const DIRECT_VENDOR_CONFIG_FIELDS = [
3860
+ ...EXPANDED_HTTP_CONFIG_FIELDS,
3861
+ { canonicalName: "RunId", valueKind: "string" },
3862
+ { canonicalName: "ReportingIntervalSeconds", valueKind: "number" }
3863
+ ];
3864
+ const EXPANDED_CONFIG_FIELDS = {
3865
+ PrometheusRemoteWrite: [
3866
+ ...DIRECT_VENDOR_CONFIG_FIELDS,
3867
+ { canonicalName: "BearerToken", valueKind: "string" }
3868
+ ],
3869
+ CloudWatch: [
3870
+ ...DIRECT_VENDOR_CONFIG_FIELDS,
3871
+ { canonicalName: "EndpointUrl", valueKind: "string" },
3872
+ { canonicalName: "Namespace", valueKind: "string" },
3873
+ { canonicalName: "Region", valueKind: "string" },
3874
+ { canonicalName: "AccessKeyId", valueKind: "string" },
3875
+ { canonicalName: "SecretAccessKey", valueKind: "string" },
3876
+ { canonicalName: "SessionToken", valueKind: "string" }
3877
+ ],
3878
+ Dynatrace: [
3879
+ ...DIRECT_VENDOR_CONFIG_FIELDS,
3880
+ { canonicalName: "ApiToken", valueKind: "string" }
3881
+ ],
3882
+ Elasticsearch: [
3883
+ ...EXPANDED_HTTP_CONFIG_FIELDS,
3884
+ { canonicalName: "IndexName", valueKind: "string" },
3885
+ { canonicalName: "ApiKey", valueKind: "string" }
3886
+ ],
3887
+ OpenSearch: [
3888
+ ...EXPANDED_HTTP_CONFIG_FIELDS,
3889
+ { canonicalName: "IndexName", valueKind: "string" },
3890
+ { canonicalName: "Username", valueKind: "string" },
3891
+ { canonicalName: "Password", valueKind: "string", preserveWhitespace: true }
3892
+ ],
3893
+ NewRelic: [
3894
+ ...DIRECT_VENDOR_CONFIG_FIELDS,
3895
+ { canonicalName: "LicenseKey", valueKind: "string" },
3896
+ { canonicalName: "ApiKey", valueKind: "string" }
3897
+ ],
3898
+ Webhook: [
3899
+ ...EXPANDED_HTTP_CONFIG_FIELDS,
3900
+ { canonicalName: "Url", valueKind: "string" },
3901
+ { canonicalName: "Secret", valueKind: "string" }
3902
+ ],
3903
+ Kafka: [
3904
+ { canonicalName: "BootstrapServers", valueKind: "string" },
3905
+ { canonicalName: "Topic", valueKind: "string" },
3906
+ { canonicalName: "StaticTags", valueKind: "stringMap" }
3907
+ ],
3908
+ StatsD: statsDExpandedConfigFields(),
3909
+ Netdata: statsDExpandedConfigFields(),
3910
+ DogStatsD: statsDExpandedConfigFields(),
3911
+ Jsonl: [
3912
+ { canonicalName: "FilePath", valueKind: "string" },
3913
+ { canonicalName: "StaticTags", valueKind: "stringMap" }
3914
+ ]
3915
+ };
3916
+ function statsDExpandedConfigFields() {
3917
+ return [
3918
+ { canonicalName: "Prefix", valueKind: "string" },
3919
+ { canonicalName: "Host", valueKind: "string" },
3920
+ { canonicalName: "Port", valueKind: "number" },
3921
+ { canonicalName: "Tags", valueKind: "stringMap", aliases: ["StaticTags"] }
3922
+ ];
3923
+ }
3924
+ function rememberExpandedReportingSink(sink, kind, options) {
3925
+ const configuredPath = optionString(options, "configurationSectionPath", "ConfigurationSectionPath").trim();
3926
+ EXPANDED_REPORTING_SINK_BINDINGS.set(sink, {
3927
+ kind,
3928
+ configurationSectionPath: configuredPath || DEFAULT_EXPANDED_REPORTING_SECTION_PATHS[kind],
3929
+ options: cloneExpandedSinkOptionRecord(options)
3930
+ });
3931
+ }
3932
+ function expandedReportingSinkConstructor(kind) {
3933
+ switch (kind) {
3934
+ case "PrometheusRemoteWrite": return PrometheusRemoteWriteReportingSink;
3935
+ case "CloudWatch": return CloudWatchReportingSink;
3936
+ case "Dynatrace": return DynatraceReportingSink;
3937
+ case "Elasticsearch": return ElasticsearchReportingSink;
3938
+ case "OpenSearch": return OpenSearchReportingSink;
3939
+ case "NewRelic": return NewRelicReportingSink;
3940
+ case "Webhook": return GenericWebhookReportingSink;
3941
+ case "Kafka": return KafkaReportingSink;
3942
+ case "StatsD": return StatsDReportingSink;
3943
+ case "Netdata": return NetdataStatsDReportingSink;
3944
+ case "DogStatsD": return DogStatsDReportingSink;
3945
+ case "Jsonl": return JsonlFileReportingSink;
3946
+ }
3947
+ }
3948
+ function assertExpandedSinkSupportsConfigurationBinding(sink, kind, sectionPath) {
3949
+ if (sink.constructor === expandedReportingSinkConstructor(kind)) {
3950
+ return;
3951
+ }
3952
+ throw new Error(`${sectionPath} cannot be applied to ${sink.constructor.name || "a custom reporting sink"}; `
3953
+ + "configure the custom sink explicitly in code instead.");
3954
+ }
3955
+ function assertExpandedSinkExecutableOptions(binding) {
3956
+ if (binding.kind !== "Kafka") {
3957
+ return;
3958
+ }
3959
+ const publishAsync = pickRecordValue(binding.options, "publishAsync", "PublishAsync");
3960
+ if (typeof publishAsync === "function") {
3961
+ return;
3962
+ }
3963
+ throw new Error(`${binding.configurationSectionPath}:PublishAsync is required because TypeScript Kafka reporting is `
3964
+ + "callback-backed; BootstrapServers does not enable native broker delivery.");
3965
+ }
3966
+ function expandedReportingSinkKindFromName(sinkName) {
3967
+ switch (normalizeConfigKey(sinkName)) {
3968
+ case "prometheusremotewrite": return "PrometheusRemoteWrite";
3969
+ case "cloudwatch": return "CloudWatch";
3970
+ case "dynatrace": return "Dynatrace";
3971
+ case "elasticsearch": return "Elasticsearch";
3972
+ case "opensearch": return "OpenSearch";
3973
+ case "newrelic": return "NewRelic";
3974
+ case "webhook": return "Webhook";
3975
+ case "kafka": return "Kafka";
3976
+ case "statsd": return "StatsD";
3977
+ case "netdatastatsd": return "Netdata";
3978
+ case "dogstatsd": return "DogStatsD";
3979
+ case "jsonl": return "Jsonl";
3980
+ default: return undefined;
3981
+ }
3982
+ }
3983
+ function cloneExpandedSinkOptionRecord(options) {
3984
+ const clone = { ...options };
3985
+ for (const [key, value] of Object.entries(clone)) {
3986
+ if (isRecord(value)) {
3987
+ clone[key] = { ...value };
3988
+ }
3989
+ else if (Array.isArray(value)) {
3990
+ clone[key] = [...value];
3991
+ }
3992
+ }
3993
+ return clone;
3994
+ }
3995
+ function createBoundExpandedReportingSink(sourceSink, binding, infraConfig, environment) {
3996
+ const section = readExpandedConfigSection(infraConfig, environment, binding.configurationSectionPath);
3997
+ const options = cloneExpandedSinkOptionRecord(binding.options);
3998
+ if (section.present) {
3999
+ assertExpandedSinkSupportsConfigurationBinding(sourceSink, binding.kind, section.path);
4000
+ const configured = normalizeExpandedConfigValues(binding.kind, section.path, section.configurationValues);
4001
+ const environmentConfigured = normalizeExpandedConfigValues(binding.kind, section.path, section.environmentValues);
4002
+ const normalized = mergeExpandedConfigSources(binding.kind, configured, environmentConfigured);
4003
+ applyMissingExpandedConfigOptions(binding.kind, options, binding.options, normalized);
4004
+ prepareExpandedCredentialOptions(binding.kind, options, {
4005
+ explicit: binding.options,
4006
+ environment: environmentConfigured,
4007
+ configuration: configured
4008
+ });
4009
+ validateBoundExpandedConfig(binding.kind, options, section.path);
4010
+ }
4011
+ switch (binding.kind) {
4012
+ case "PrometheusRemoteWrite":
4013
+ return new PrometheusRemoteWriteReportingSink(options);
4014
+ case "CloudWatch":
4015
+ return new CloudWatchReportingSink(options);
4016
+ case "Dynatrace":
4017
+ return new DynatraceReportingSink(options);
4018
+ case "Elasticsearch":
4019
+ return new ElasticsearchReportingSink(options);
4020
+ case "OpenSearch":
4021
+ return new OpenSearchReportingSink(options);
4022
+ case "NewRelic":
4023
+ return new NewRelicReportingSink(options);
4024
+ case "Webhook":
4025
+ return new GenericWebhookReportingSink(options);
4026
+ case "Kafka":
4027
+ return new KafkaReportingSink(options);
4028
+ case "StatsD":
4029
+ return new StatsDReportingSink(options);
4030
+ case "Netdata":
4031
+ return new NetdataStatsDReportingSink(options);
4032
+ case "DogStatsD":
4033
+ return new DogStatsDReportingSink(options);
4034
+ case "Jsonl":
4035
+ return new JsonlFileReportingSink(options);
4036
+ }
4037
+ }
4038
+ function readExpandedConfigSection(infraConfig, environment, configuredPath) {
4039
+ const pathSegments = splitExpandedConfigPath(configuredPath);
4040
+ const diagnosticPath = pathSegments.join(":");
4041
+ const configurationValues = {};
4042
+ const environmentValues = {};
4043
+ let present = false;
4044
+ const visitConfig = (record, consumed) => {
4045
+ for (const [rawKey, rawValue] of Object.entries(record)) {
4046
+ const candidate = [...consumed, ...splitExpandedConfigPath(rawKey)];
4047
+ if (!expandedConfigPathStartsWith(candidate, pathSegments) &&
4048
+ !expandedConfigPathStartsWith(pathSegments, candidate)) {
4049
+ continue;
4050
+ }
4051
+ if (expandedConfigPathEquals(candidate, pathSegments)) {
4052
+ present = true;
4053
+ if (isRecord(rawValue)) {
4054
+ mergeExpandedConfigRecords(configurationValues, rawValue);
4055
+ }
4056
+ continue;
4057
+ }
4058
+ if (candidate.length < pathSegments.length) {
4059
+ if (isRecord(rawValue)) {
4060
+ visitConfig(rawValue, candidate);
4061
+ }
4062
+ continue;
4063
+ }
4064
+ if (expandedConfigPathStartsWith(candidate, pathSegments)) {
4065
+ present = true;
4066
+ setExpandedConfigTail(configurationValues, candidate.slice(pathSegments.length), rawValue);
4067
+ }
4068
+ }
4069
+ };
4070
+ if (isRecord(infraConfig)) {
4071
+ visitConfig(infraConfig, []);
4072
+ }
4073
+ for (const [rawKey, rawValue] of Object.entries(environment)) {
4074
+ if (rawValue === undefined) {
4075
+ continue;
4076
+ }
4077
+ const candidate = splitExpandedConfigPath(rawKey);
4078
+ if (candidate.length <= pathSegments.length ||
4079
+ !expandedConfigPathStartsWith(candidate, pathSegments)) {
4080
+ continue;
4081
+ }
4082
+ present = true;
4083
+ setExpandedConfigTail(environmentValues, candidate.slice(pathSegments.length), rawValue);
4084
+ }
4085
+ return { path: diagnosticPath, present, configurationValues, environmentValues };
4086
+ }
4087
+ function splitExpandedConfigPath(value) {
4088
+ return String(value ?? "")
4089
+ .split(/__|[:.]/g)
4090
+ .map((segment) => segment.trim())
4091
+ .filter(Boolean);
4092
+ }
4093
+ function expandedConfigPathStartsWith(candidate, expectedPrefix) {
4094
+ return candidate.length >= expectedPrefix.length && expectedPrefix.every((segment, index) => normalizeConfigKey(candidate[index]) === normalizeConfigKey(segment));
4095
+ }
4096
+ function expandedConfigPathEquals(left, right) {
4097
+ return left.length === right.length && expandedConfigPathStartsWith(left, right);
4098
+ }
4099
+ function setExpandedConfigTail(target, tail, value) {
4100
+ if (!tail.length) {
4101
+ return;
4102
+ }
4103
+ let current = target;
4104
+ for (let index = 0; index < tail.length - 1; index += 1) {
4105
+ const segment = tail[index];
4106
+ const existingKey = Object.keys(current).find((key) => normalizeConfigKey(key) === normalizeConfigKey(segment));
4107
+ const key = existingKey ?? segment;
4108
+ if (!isRecord(current[key])) {
4109
+ current[key] = {};
4110
+ }
4111
+ current = current[key];
4112
+ }
4113
+ const leaf = tail[tail.length - 1];
4114
+ const existingLeaf = Object.keys(current).find((key) => normalizeConfigKey(key) === normalizeConfigKey(leaf));
4115
+ current[existingLeaf ?? leaf] = value;
4116
+ }
4117
+ function mergeExpandedConfigRecords(target, source) {
4118
+ for (const [rawKey, rawValue] of Object.entries(source)) {
4119
+ const existingKey = Object.keys(target).find((key) => normalizeConfigKey(key) === normalizeConfigKey(rawKey));
4120
+ const key = existingKey ?? rawKey;
4121
+ if (isRecord(target[key]) && isRecord(rawValue)) {
4122
+ mergeExpandedConfigRecords(target[key], rawValue);
4123
+ }
4124
+ else {
4125
+ target[key] = isRecord(rawValue) ? cloneExpandedSinkOptionRecord(rawValue) : rawValue;
4126
+ }
4127
+ }
4128
+ }
4129
+ function normalizeExpandedConfigValues(kind, sectionPath, values) {
4130
+ const fields = EXPANDED_CONFIG_FIELDS[kind];
4131
+ const normalized = {};
4132
+ const entries = Object.entries(values);
4133
+ for (const [rawKey] of entries) {
4134
+ const field = fields.find((candidate) => [candidate.canonicalName, ...(candidate.aliases ?? [])]
4135
+ .some((name) => normalizeConfigKey(name) === normalizeConfigKey(rawKey)));
4136
+ if (!field) {
4137
+ throw new Error(`${sectionPath}:${rawKey} is not supported.`);
4138
+ }
4139
+ }
4140
+ for (const field of fields) {
4141
+ const namesByPrecedence = [...(field.aliases ?? []), field.canonicalName];
4142
+ let selectedValue;
4143
+ let selected = false;
4144
+ for (const name of namesByPrecedence) {
4145
+ const matchingEntries = entries.filter(([rawKey]) => normalizeConfigKey(rawKey) === normalizeConfigKey(name));
4146
+ if (matchingEntries.length > 1) {
4147
+ throw new Error(`${sectionPath}:${name} is configured more than once.`);
4148
+ }
4149
+ if (matchingEntries.length === 0) {
4150
+ continue;
4151
+ }
4152
+ const [rawKey, rawValue] = matchingEntries[0];
4153
+ const value = normalizeExpandedConfigValue(rawValue, field, `${sectionPath}:${rawKey}`);
4154
+ selectedValue = field.valueKind === "stringMap" && selected &&
4155
+ isRecord(selectedValue) && isRecord(value)
4156
+ ? { ...selectedValue, ...value }
4157
+ : value;
4158
+ selected = true;
4159
+ }
4160
+ if (selected) {
4161
+ normalized[field.canonicalName] = selectedValue;
4162
+ }
4163
+ }
4164
+ return normalized;
4165
+ }
4166
+ function normalizeExpandedConfigValue(value, field, path) {
4167
+ if (field.valueKind === "string") {
4168
+ if (typeof value !== "string") {
4169
+ throw new Error(`${path} must be a string.`);
4170
+ }
4171
+ return field.preserveWhitespace ? value : value.trim();
4172
+ }
4173
+ if (field.valueKind === "number") {
4174
+ if (typeof value === "boolean" || value == null ||
4175
+ (typeof value !== "number" && typeof value !== "string")) {
4176
+ throw new Error(`${path} must be a finite number.`);
4177
+ }
4178
+ const parsed = typeof value === "number" ? value : Number(value.trim());
4179
+ if (!Number.isFinite(parsed)) {
4180
+ throw new Error(`${path} must be a finite number.`);
4181
+ }
4182
+ return parsed;
4183
+ }
4184
+ if (!isRecord(value)) {
4185
+ throw new Error(`${path} must be an object of string values.`);
4186
+ }
4187
+ const normalizedMap = {};
4188
+ for (const [mapKey, mapValue] of Object.entries(value)) {
4189
+ if (typeof mapValue !== "string") {
4190
+ throw new Error(`${path}:${mapKey} must be a string value.`);
4191
+ }
4192
+ normalizedMap[mapKey] = mapValue;
4193
+ }
4194
+ return normalizedMap;
4195
+ }
4196
+ function mergeExpandedConfigSources(kind, configured, environment) {
4197
+ const merged = cloneExpandedSinkOptionRecord(configured);
4198
+ const clearedSemanticGroups = new Set();
4199
+ for (const [canonicalName, value] of Object.entries(environment)) {
4200
+ const precedenceGroup = expandedOptionPrecedenceGroup(kind, canonicalName);
4201
+ const groupIdentity = precedenceGroup
4202
+ .map((name) => normalizeConfigKey(name))
4203
+ .sort()
4204
+ .join("|");
4205
+ const lowerPriorityMap = precedenceGroup.length === 1 && isRecord(merged[canonicalName])
4206
+ ? merged[canonicalName]
4207
+ : undefined;
4208
+ if (precedenceGroup.length > 1 && !clearedSemanticGroups.has(groupIdentity)) {
4209
+ for (const existingName of Object.keys(merged)) {
4210
+ if (precedenceGroup.some((candidate) => normalizeConfigKey(candidate) === normalizeConfigKey(existingName))) {
4211
+ delete merged[existingName];
4212
+ }
4213
+ }
4214
+ clearedSemanticGroups.add(groupIdentity);
4215
+ }
4216
+ merged[canonicalName] = lowerPriorityMap && isRecord(value)
4217
+ ? { ...lowerPriorityMap, ...value }
4218
+ : isRecord(value)
4219
+ ? { ...value }
4220
+ : value;
4221
+ }
4222
+ return merged;
4223
+ }
4224
+ function applyMissingExpandedConfigOptions(kind, target, explicitOptions, configured) {
4225
+ for (const [canonicalName, value] of Object.entries(configured)) {
4226
+ if (hasExplicitExpandedOption(kind, explicitOptions, canonicalName)) {
4227
+ continue;
4228
+ }
4229
+ target[canonicalName] = isRecord(value) ? { ...value } : value;
4230
+ }
4231
+ }
4232
+ function hasExplicitExpandedOption(kind, options, canonicalName) {
4233
+ const aliases = expandedOptionPrecedenceGroup(kind, canonicalName);
4234
+ return Object.entries(options).some(([key, value]) => value !== undefined && aliases.some((alias) => normalizeConfigKey(alias) === normalizeConfigKey(key)));
4235
+ }
4236
+ function expandedOptionPrecedenceGroup(kind, canonicalName) {
4237
+ if (canonicalName === "TimeoutSeconds" || canonicalName === "TimeoutMs") {
4238
+ return ["TimeoutSeconds", "TimeoutMs"];
4239
+ }
4240
+ if ((canonicalName === "BaseUrl" || canonicalName === "EndpointUrl") && kind === "CloudWatch") {
4241
+ return ["BaseUrl", "EndpointUrl"];
4242
+ }
4243
+ if ((canonicalName === "BaseUrl" || canonicalName === "Url") && kind === "Webhook") {
4244
+ return ["BaseUrl", "Url"];
4245
+ }
4246
+ if ((canonicalName === "LicenseKey" || canonicalName === "ApiKey") && kind === "NewRelic") {
4247
+ return ["LicenseKey", "ApiKey"];
4248
+ }
4249
+ if ((canonicalName === "EndpointPath" || canonicalName === "IndexName") &&
4250
+ (kind === "Elasticsearch" || kind === "OpenSearch")) {
4251
+ return ["EndpointPath", "IndexName"];
4252
+ }
4253
+ return [canonicalName];
4254
+ }
4255
+ function prepareExpandedCredentialOptions(kind, options, sources) {
4256
+ let headers = normalizeStringMap(optionRecord(options, "headers", "Headers"));
4257
+ const headerName = expandedCredentialHeaderName(kind);
4258
+ const headerSourceRank = headerName
4259
+ ? expandedCredentialHeaderSourceRank(sources, headerName)
4260
+ : 0;
4261
+ const credentialSourceRank = expandedCredentialSourceRank(kind, sources);
4262
+ if (headerSourceRank > credentialSourceRank) {
4263
+ deleteExpandedCredentialOptions(kind, options);
4264
+ }
4265
+ const credentialMustReplaceHeader = credentialSourceRank > headerSourceRank;
4266
+ if (kind === "Elasticsearch") {
4267
+ const apiKey = optionString(options, "apiKey", "ApiKey").trim();
4268
+ if (apiKey && (credentialMustReplaceHeader || !hasCaseInsensitiveHeader(headers, "Authorization"))) {
4269
+ headers = mergeProtocolHeaders(headers, { Authorization: `ApiKey ${apiKey}` });
4270
+ }
4271
+ }
4272
+ else if (kind === "OpenSearch") {
4273
+ const username = optionString(options, "username", "Username").trim();
4274
+ const password = optionString(options, "password", "Password");
4275
+ if ((username || password) &&
4276
+ (credentialMustReplaceHeader || !hasCaseInsensitiveHeader(headers, "Authorization"))) {
4277
+ headers = mergeProtocolHeaders(headers, {
4278
+ Authorization: `Basic ${Buffer.from(`${username}:${password}`, "utf8").toString("base64")}`
4279
+ });
4280
+ }
4281
+ }
4282
+ else if (kind === "Webhook") {
4283
+ const secret = optionString(options, "secret", "Secret").trim();
4284
+ const url = optionString(options, "url", "Url").trim();
4285
+ if (url) {
4286
+ options.__loadstrikeExactUrl = url;
4287
+ }
4288
+ if (secret && (credentialMustReplaceHeader || !hasCaseInsensitiveHeader(headers, "Authorization"))) {
4289
+ headers = mergeProtocolHeaders(headers, { Authorization: `LoadStrike ${secret}` });
4290
+ }
4291
+ }
4292
+ if (Object.keys(headers).length > 0) {
4293
+ options.Headers = headers;
4294
+ delete options.headers;
4295
+ }
4296
+ }
4297
+ function expandedCredentialHeaderName(kind) {
4298
+ if (kind === "PrometheusRemoteWrite" || kind === "Dynatrace" ||
4299
+ kind === "Elasticsearch" || kind === "OpenSearch" || kind === "Webhook") {
4300
+ return "Authorization";
4301
+ }
4302
+ return kind === "NewRelic" ? "Api-Key" : undefined;
4303
+ }
4304
+ function expandedCredentialHeaderSourceRank(sources, headerName) {
4305
+ return expandedSourceRank(sources, (source) => {
4306
+ const headers = normalizeStringMap(optionRecord(source, "headers", "Headers"));
4307
+ return Object.entries(headers).some(([name, value]) => name.toLowerCase() === headerName.toLowerCase() && value.trim().length > 0);
4308
+ });
4309
+ }
4310
+ function expandedCredentialSourceRank(kind, sources) {
4311
+ return expandedSourceRank(sources, (source) => expandedSourceHasCredential(kind, source));
4312
+ }
4313
+ function expandedSourceRank(sources, predicate) {
4314
+ if (predicate(sources.explicit))
4315
+ return 3;
4316
+ if (predicate(sources.environment))
4317
+ return 2;
4318
+ if (predicate(sources.configuration))
4319
+ return 1;
4320
+ return 0;
4321
+ }
4322
+ function expandedSourceHasCredential(kind, source) {
4323
+ switch (kind) {
4324
+ case "PrometheusRemoteWrite":
4325
+ return optionString(source, "bearerToken", "BearerToken").trim().length > 0;
4326
+ case "Dynatrace":
4327
+ return optionString(source, "apiToken", "ApiToken").trim().length > 0;
4328
+ case "Elasticsearch":
4329
+ return optionString(source, "apiKey", "ApiKey").trim().length > 0;
4330
+ case "OpenSearch":
4331
+ return optionString(source, "username", "Username").trim().length > 0
4332
+ || optionString(source, "password", "Password").length > 0;
4333
+ case "NewRelic":
4334
+ return optionString(source, "licenseKey", "LicenseKey", "apiKey", "ApiKey").trim().length > 0;
4335
+ case "Webhook":
4336
+ return optionString(source, "secret", "Secret").trim().length > 0;
4337
+ default:
4338
+ return false;
4339
+ }
4340
+ }
4341
+ function deleteExpandedCredentialOptions(kind, options) {
4342
+ const names = (() => {
4343
+ switch (kind) {
4344
+ case "PrometheusRemoteWrite": return ["BearerToken"];
4345
+ case "Dynatrace": return ["ApiToken"];
4346
+ case "Elasticsearch": return ["ApiKey"];
4347
+ case "OpenSearch": return ["Username", "Password"];
4348
+ case "NewRelic": return ["LicenseKey", "ApiKey"];
4349
+ case "Webhook": return ["Secret"];
4350
+ default: return [];
4351
+ }
4352
+ })();
4353
+ for (const key of Object.keys(options)) {
4354
+ if (names.some((name) => normalizeConfigKey(name) === normalizeConfigKey(key))) {
4355
+ delete options[key];
4356
+ }
4357
+ }
4358
+ }
4359
+ function validateBoundExpandedConfig(kind, options, sectionPath) {
4360
+ const requireString = (name, ...aliases) => {
4361
+ const value = optionString(options, name, ...aliases).trim();
4362
+ if (!value) {
4363
+ throw new Error(`${sectionPath}:${name} is required.`);
4364
+ }
4365
+ return value;
4366
+ };
4367
+ const validateUrl = (name, value) => {
4368
+ try {
4369
+ const parsed = new URL(value);
4370
+ if ((parsed.protocol !== "http:" && parsed.protocol !== "https:") ||
4371
+ !parsed.hostname || parsed.username || parsed.password) {
4372
+ throw new Error("invalid");
4373
+ }
4374
+ }
4375
+ catch {
4376
+ throw new Error(`${sectionPath}:${name} must be an absolute HTTP or HTTPS URL.`);
4377
+ }
4378
+ };
4379
+ if (kind === "PrometheusRemoteWrite" || kind === "Dynatrace" ||
4380
+ kind === "Elasticsearch" || kind === "OpenSearch" || kind === "NewRelic") {
4381
+ validateUrl("BaseUrl", requireString("BaseUrl"));
4382
+ }
4383
+ if (kind === "CloudWatch") {
4384
+ const endpointPath = optionString(options, "endpointPath", "EndpointPath").trim();
4385
+ if (endpointPath && normalizePath(endpointPath) !== "/") {
4386
+ throw new Error(`${sectionPath}:EndpointPath is not supported for CloudWatch; `
4387
+ + "include any required path in EndpointUrl.");
4388
+ }
4389
+ const endpoint = optionString(options, "endpointUrl", "EndpointUrl", "baseUrl", "BaseUrl").trim();
4390
+ if (endpoint) {
4391
+ const endpointName = optionString(options, "endpointUrl", "EndpointUrl").trim()
4392
+ ? "EndpointUrl"
4393
+ : "BaseUrl";
4394
+ validateUrl(endpointName, endpoint);
4395
+ }
4396
+ requireString("Region");
4397
+ requireString("AccessKeyId");
4398
+ requireString("SecretAccessKey");
4399
+ }
4400
+ if (kind === "Dynatrace" &&
4401
+ !optionString(options, "apiToken", "ApiToken").trim() &&
4402
+ !hasCaseInsensitiveHeader(normalizeStringMap(optionRecord(options, "headers", "Headers")), "Authorization")) {
4403
+ throw new Error(`${sectionPath}:ApiToken is required.`);
4404
+ }
4405
+ if (kind === "NewRelic" &&
4406
+ !optionString(options, "licenseKey", "LicenseKey", "apiKey", "ApiKey").trim() &&
4407
+ !hasCaseInsensitiveHeader(normalizeStringMap(optionRecord(options, "headers", "Headers")), "Api-Key")) {
4408
+ throw new Error(`${sectionPath}:LicenseKey is required.`);
4409
+ }
4410
+ if (kind === "Elasticsearch" || kind === "OpenSearch") {
4411
+ const endpointPath = optionString(options, "endpointPath", "EndpointPath").trim();
4412
+ if (!endpointPath) {
4413
+ const rawIndexName = pickRecordValue(options, "indexName", "IndexName");
4414
+ const indexName = rawIndexName === undefined
4415
+ ? "loadstrike-events"
4416
+ : typeof rawIndexName === "string"
4417
+ ? rawIndexName.trim()
4418
+ : "";
4419
+ requireValidSearchIndexName(indexName, `${sectionPath}:IndexName`);
4420
+ }
4421
+ }
4422
+ if (kind === "OpenSearch") {
4423
+ const username = optionString(options, "username", "Username").trim();
4424
+ const password = optionString(options, "password", "Password");
4425
+ if (username && !password) {
4426
+ throw new Error(`${sectionPath}:Password is required when Username is configured.`);
4427
+ }
4428
+ if (password && !username) {
4429
+ throw new Error(`${sectionPath}:Username is required when Password is configured.`);
4430
+ }
4431
+ }
4432
+ if (kind === "Webhook") {
4433
+ const url = optionString(options, "url", "Url").trim();
4434
+ if (url) {
4435
+ validateUrl("Url", url);
4436
+ }
4437
+ else {
4438
+ validateUrl("BaseUrl", requireString("BaseUrl"));
4439
+ }
4440
+ }
4441
+ if (kind === "StatsD" || kind === "Netdata" || kind === "DogStatsD") {
4442
+ const configuredPort = pickRecordValue(options, "port", "Port");
4443
+ if (configuredPort !== undefined) {
4444
+ const port = Number(configuredPort);
4445
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
4446
+ throw new Error(`${sectionPath}:Port must be an integer between 1 and 65535.`);
4447
+ }
4448
+ }
4449
+ }
4450
+ }
3234
4451
  function resolveConfigSection(source, path) {
3235
4452
  const trimmedPath = String(path ?? "").trim();
3236
4453
  if (!trimmedPath) {
@@ -3355,6 +4572,73 @@ function resolveTimeoutMs(timeoutSeconds, timeoutMs) {
3355
4572
  }
3356
4573
  return 30000;
3357
4574
  }
4575
+ function reportingIntervalMilliseconds(seconds) {
4576
+ if (seconds === undefined) {
4577
+ return undefined;
4578
+ }
4579
+ if (!Number.isFinite(seconds) || seconds <= 0) {
4580
+ throw new Error("ReportingIntervalSeconds must be a positive finite number.");
4581
+ }
4582
+ const milliseconds = Math.round(seconds * 1000);
4583
+ if (!Number.isSafeInteger(milliseconds) || milliseconds <= 0) {
4584
+ throw new Error("ReportingIntervalSeconds is outside the supported range.");
4585
+ }
4586
+ return milliseconds;
4587
+ }
4588
+ function positiveIntervalMilliseconds(previous, current) {
4589
+ if (!previous || !Number.isFinite(previous.getTime()) || !Number.isFinite(current.getTime())) {
4590
+ return 1;
4591
+ }
4592
+ return Math.max(Math.round(current.getTime() - previous.getTime()), 1);
4593
+ }
4594
+ function requireHttpReportingUrl(value, fieldName) {
4595
+ let parsed;
4596
+ try {
4597
+ parsed = new URL(value);
4598
+ }
4599
+ catch {
4600
+ throw new Error(`${fieldName} must be an absolute HTTP or HTTPS URL.`);
4601
+ }
4602
+ if ((parsed.protocol !== "http:" && parsed.protocol !== "https:") || !parsed.hostname) {
4603
+ throw new Error(`${fieldName} must be an absolute HTTP or HTTPS URL.`);
4604
+ }
4605
+ if (parsed.username || parsed.password) {
4606
+ throw new Error(`${fieldName} must not contain credentials.`);
4607
+ }
4608
+ }
4609
+ function hasCaseInsensitiveHeader(headers, expectedName) {
4610
+ const expected = expectedName.toLowerCase();
4611
+ return Object.entries(headers).some(([name, value]) => name.toLowerCase() === expected && value.trim().length > 0);
4612
+ }
4613
+ function normalizeProtocolHeaders(headers) {
4614
+ return mergeProtocolHeaders({}, headers);
4615
+ }
4616
+ function mergeProtocolHeaders(headers, requiredHeaders) {
4617
+ const merged = {};
4618
+ const namesByLowerCase = new Map();
4619
+ for (const source of [headers, requiredHeaders]) {
4620
+ for (const [name, value] of Object.entries(source)) {
4621
+ const normalized = name.toLowerCase();
4622
+ const previousName = namesByLowerCase.get(normalized);
4623
+ if (previousName !== undefined) {
4624
+ delete merged[previousName];
4625
+ }
4626
+ merged[name] = String(value);
4627
+ namesByLowerCase.set(normalized, name);
4628
+ }
4629
+ }
4630
+ return merged;
4631
+ }
4632
+ function isCloudWatchProtocolHeader(name) {
4633
+ const normalized = name.toLowerCase();
4634
+ return normalized === "authorization"
4635
+ || normalized === "host"
4636
+ || normalized === "content-type"
4637
+ || normalized === "content-length"
4638
+ || normalized === "user-agent"
4639
+ || normalized.startsWith("x-amz-")
4640
+ || normalized.startsWith("amz-sdk-");
4641
+ }
3358
4642
  function toUnixNanoseconds(value) {
3359
4643
  return (BigInt(value.getTime()) * 1000000n).toString();
3360
4644
  }
@@ -3597,13 +4881,17 @@ function deepCloneValue(value) {
3597
4881
  function gzipJsonRequestBody(value) {
3598
4882
  return Uint8Array.from((0, iteration_observations_js_1.serializeIterationObservationBatchGzipJson)(value)).buffer;
3599
4883
  }
3600
- async function postWithTimeout(fetchImpl, url, init, timeoutMs, sinkName) {
4884
+ async function postWithTimeout(fetchImpl, url, init, timeoutMs, sinkName, ignoreSuccessfulResponseBody = false) {
3601
4885
  const controller = new AbortController();
3602
4886
  const timer = setTimeout(() => controller.abort(), Math.max(timeoutMs, 1));
3603
4887
  try {
3604
4888
  const response = await fetchImpl(url, { ...init, signal: controller.signal });
3605
4889
  if (!response.ok) {
3606
- throw new ReportingSinkHttpError(sinkName, response.status, readHttpResponseStatusText(response), readHttpResponseRequestId(response));
4890
+ throw new reporting_sink_http_error_js_1.ReportingSinkHttpError(sinkName, response.status, readHttpResponseStatusText(response), readHttpResponseRequestId(response));
4891
+ }
4892
+ if (ignoreSuccessfulResponseBody) {
4893
+ disposeResponseBody(response);
4894
+ return "";
3607
4895
  }
3608
4896
  return await readResponseBodyText(response);
3609
4897
  }
@@ -3611,6 +4899,17 @@ async function postWithTimeout(fetchImpl, url, init, timeoutMs, sinkName) {
3611
4899
  clearTimeout(timer);
3612
4900
  }
3613
4901
  }
4902
+ function disposeResponseBody(response) {
4903
+ try {
4904
+ const cancellation = response.body?.cancel();
4905
+ if (cancellation && typeof cancellation.catch === "function") {
4906
+ void cancellation.catch(() => undefined);
4907
+ }
4908
+ }
4909
+ catch {
4910
+ // A successful write must not be reclassified as failed because its response body cannot be consumed.
4911
+ }
4912
+ }
3614
4913
  async function readResponseBodyText(response) {
3615
4914
  const body = response.body;
3616
4915
  if (body && typeof body.getReader === "function") {