@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/esm/sinks.js CHANGED
@@ -4,28 +4,27 @@ import { appendFile, mkdir } from "node:fs/promises";
4
4
  import { dirname } from "node:path";
5
5
  import { createSocket } from "node:dgram";
6
6
  import { gzip } from "node:zlib";
7
+ import { CloudWatchClient, PutMetricDataCommand } from "@aws-sdk/client-cloudwatch";
7
8
  import { Pool } from "pg";
9
+ import { compress as compressSnappy } from "snappyjs";
8
10
  import { serializeIterationObservationBatchGzipJson } from "./iteration-observations.js";
9
11
  import { redactIterationObservationSecrets } from "./iteration-observation-diagnostics.js";
12
+ import { encodePrometheusRemoteWrite } from "./internal/prometheus-remote-write.js";
13
+ import { ReportingSinkHttpError } from "./internal/reporting-sink-http-error.js";
14
+ import { createCloudWatchMetricData, encodeDynatraceMetricBatches, encodeNewRelicMetricBatches } from "./internal/vendor-metric-payloads.js";
10
15
  const PORTAL_RUN_TOKEN_PROVIDER = Symbol.for("loadstrike.internal.portal-run-token-provider");
16
+ const REPORTING_INTERVAL_SECONDS = Symbol.for("loadstrike.internal.reporting-interval-seconds");
17
+ const SEND_DIRECT_VENDOR_PROTOCOL_POINTS = Symbol("loadstrike.internal.send-direct-vendor-protocol-points");
18
+ const CLONE_DIRECT_VENDOR_FOR_RUN = Symbol("loadstrike.internal.clone-direct-vendor-for-run");
19
+ const CLOUDWATCH_MAXIMUM_DATUMS_PER_REQUEST = 1000;
20
+ const CLOUDWATCH_SAFE_REQUEST_BODY_BYTES = 900 * 1024;
21
+ const DIRECT_VENDOR_MAXIMUM_BODY_BYTES_EXCLUSIVE = 1000000;
22
+ const MAXIMUM_NON_NEGATIVE_SIGNED_INT64 = 9223372036854775807n;
23
+ const LOADSTRIKE_TYPESCRIPT_SDK_USER_AGENT = "loadstrike-typescript-sdk/1.0.16101";
24
+ const EXPANDED_REPORTING_SINK_BINDINGS = new WeakMap();
11
25
  const MAXIMUM_HTTP_RESPONSE_BODY_BYTES = 256 * 1024;
12
26
  const MAXIMUM_HTTP_METADATA_CHARACTERS = 256;
13
27
  const HTTP_METADATA_TRUNCATION_SUFFIX = " [truncated]";
14
- class ReportingSinkHttpError extends Error {
15
- constructor(sinkName, status, statusText, requestId) {
16
- const normalizedStatus = Number.isFinite(status) ? Math.max(Math.trunc(status), 0) : 0;
17
- const normalizedStatusText = normalizeHttpMetadata(statusText);
18
- const normalizedRequestId = normalizeHttpMetadata(requestId);
19
- super(`${sinkName} write failed with HTTP status ${normalizedStatus}`
20
- + (normalizedStatusText ? ` ${normalizedStatusText}` : "")
21
- + (normalizedRequestId ? ` (requestId=${normalizedRequestId})` : "")
22
- + ".");
23
- this.name = "ReportingSinkHttpError";
24
- this.status = normalizedStatus;
25
- this.statusText = normalizedStatusText;
26
- this.requestId = normalizedRequestId;
27
- }
28
- }
29
28
  function gzipJsonAsync(value) {
30
29
  const json = Buffer.from(JSON.stringify(value), "utf8");
31
30
  return new Promise((resolve, reject) => {
@@ -564,12 +563,72 @@ export class PortalReportingSink {
564
563
  return current || this.runToken;
565
564
  }
566
565
  }
567
- export function cloneReportingSinkForRun(sink) {
566
+ export function cloneReportingSinkForRun(sink, infraConfig = {}, environment = process.env) {
568
567
  if (sink instanceof PortalReportingSink) {
569
568
  return sink.cloneForRun();
570
569
  }
570
+ if (sink instanceof CompositeReportingSink) {
571
+ const children = sink.sinks;
572
+ if (sink.constructor !== CompositeReportingSink &&
573
+ !hasSelectedExpandedConfiguration(children, infraConfig, environment)) {
574
+ return sink;
575
+ }
576
+ const boundChildren = children.map((child) => cloneReportingSinkForRun(child, infraConfig, environment));
577
+ if (boundChildren.every((child, index) => child === children[index])) {
578
+ return sink;
579
+ }
580
+ if (sink.constructor !== CompositeReportingSink) {
581
+ throw new Error(`${sink.constructor.name || "Custom composite reporting sink"} cannot bind child configuration `
582
+ + "without an explicit clone implementation. Configure its children in code instead.");
583
+ }
584
+ return new CompositeReportingSink(...boundChildren);
585
+ }
586
+ const expandedBinding = EXPANDED_REPORTING_SINK_BINDINGS.get(sink);
587
+ if (expandedBinding) {
588
+ const configuredSection = readExpandedConfigSection(infraConfig, environment, expandedBinding.configurationSectionPath);
589
+ const expectedConstructor = expandedReportingSinkConstructor(expandedBinding.kind);
590
+ if (!configuredSection.present && sink.constructor !== expectedConstructor) {
591
+ return sink;
592
+ }
593
+ if (configuredSection.present) {
594
+ assertExpandedSinkSupportsConfigurationBinding(sink, expandedBinding.kind, configuredSection.path);
595
+ }
596
+ assertExpandedSinkExecutableOptions(expandedBinding);
597
+ if (!configuredSection.present) {
598
+ if (!(sink instanceof DirectVendorMetricReportingSink)) {
599
+ return sink;
600
+ }
601
+ return sink.constructor === expectedConstructor
602
+ ? sink[CLONE_DIRECT_VENDOR_FOR_RUN]()
603
+ : sink;
604
+ }
605
+ return createBoundExpandedReportingSink(sink, expandedBinding, infraConfig, environment);
606
+ }
607
+ if (sink instanceof DirectVendorMetricReportingSink) {
608
+ return sink[CLONE_DIRECT_VENDOR_FOR_RUN]();
609
+ }
571
610
  return sink;
572
611
  }
612
+ function hasSelectedExpandedConfiguration(sinks, infraConfig, environment, visited = new Set()) {
613
+ for (const sink of sinks) {
614
+ if (visited.has(sink)) {
615
+ continue;
616
+ }
617
+ visited.add(sink);
618
+ if (sink instanceof CompositeReportingSink) {
619
+ const children = sink.sinks;
620
+ if (hasSelectedExpandedConfiguration(children, infraConfig, environment, visited)) {
621
+ return true;
622
+ }
623
+ continue;
624
+ }
625
+ const binding = EXPANDED_REPORTING_SINK_BINDINGS.get(sink);
626
+ if (binding && readExpandedConfigSection(infraConfig, environment, binding.configurationSectionPath).present) {
627
+ return true;
628
+ }
629
+ }
630
+ return false;
631
+ }
573
632
  export class InfluxDbReportingSink {
574
633
  constructor(options = {}) {
575
634
  this.sinkName = "influxdb";
@@ -1626,27 +1685,34 @@ class ExpandedEventReportingSink {
1626
1685
  }
1627
1686
  }
1628
1687
  class ExpandedHttpJsonReportingSink extends ExpandedEventReportingSink {
1629
- constructor(sinkName, licenseFeature, defaultEndpointPath, options = {}) {
1688
+ constructor(sinkName, licenseFeature, defaultEndpointPath, options = {}, bindingOptions = options) {
1630
1689
  super(sinkName, licenseFeature);
1631
1690
  const source = asRecord(options);
1632
1691
  this.baseUrl = optionString(source, "baseUrl", "BaseUrl").trim();
1692
+ this.exactUrl = optionString(source, "__loadstrikeExactUrl").trim();
1633
1693
  this.endpointPath = optionString(source, "endpointPath", "EndpointPath").trim() || defaultEndpointPath;
1634
1694
  this.headers = normalizeStringMap(optionRecord(source, "headers", "Headers"));
1635
1695
  this.staticTags = normalizeStringMap(optionRecord(source, "staticTags", "StaticTags"));
1636
1696
  this.timeoutMs = resolveTimeoutMs(optionNumber(source, "timeoutSeconds", "TimeoutSeconds"), optionNumber(source, "timeoutMs", "TimeoutMs"));
1637
1697
  this.fetchImpl = pickRecordValue(source, "fetchImpl", "FetchImpl") ?? fetch;
1698
+ const expandedKind = expandedReportingSinkKindFromName(sinkName);
1699
+ if (expandedKind) {
1700
+ rememberExpandedReportingSink(this, expandedKind, asRecord(bindingOptions));
1701
+ }
1638
1702
  }
1639
1703
  init(context, infraConfig) {
1640
1704
  super.init(context, infraConfig);
1641
- if (!this.baseUrl.trim()) {
1705
+ const configuredUrl = this.exactUrl || this.baseUrl;
1706
+ if (!configuredUrl) {
1642
1707
  throw new Error(`${this.constructor.name} requires BaseUrl.`);
1643
1708
  }
1709
+ requireHttpReportingUrl(configuredUrl, `${this.constructor.name} BaseUrl`);
1644
1710
  }
1645
1711
  async persistEvents(events) {
1646
1712
  if (!events.length) {
1647
1713
  return;
1648
1714
  }
1649
- await postWithTimeout(this.fetchImpl, `${trimTrailingSlashes(this.baseUrl)}${normalizePath(this.endpointPath)}`, {
1715
+ await postWithTimeout(this.fetchImpl, this.requestUrl(), {
1650
1716
  method: "POST",
1651
1717
  headers: {
1652
1718
  "Content-Type": "application/json",
@@ -1663,7 +1729,7 @@ class ExpandedHttpJsonReportingSink extends ExpandedEventReportingSink {
1663
1729
  }
1664
1730
  async persistCanonicalGzipPayload(payload) {
1665
1731
  const compressed = await gzipJsonAsync(payload);
1666
- await postWithTimeout(this.fetchImpl, `${trimTrailingSlashes(this.baseUrl)}${normalizePath(this.endpointPath)}`, {
1732
+ await postWithTimeout(this.fetchImpl, this.requestUrl(), {
1667
1733
  method: "POST",
1668
1734
  headers: {
1669
1735
  "Content-Type": "application/json",
@@ -1673,40 +1739,408 @@ class ExpandedHttpJsonReportingSink extends ExpandedEventReportingSink {
1673
1739
  body: Uint8Array.from(compressed).buffer
1674
1740
  }, this.timeoutMs, this.constructor.name);
1675
1741
  }
1742
+ requestUrl() {
1743
+ return this.exactUrl
1744
+ || `${trimTrailingSlashes(this.baseUrl)}${normalizePath(this.endpointPath)}`;
1745
+ }
1746
+ }
1747
+ function cloneDirectVendorSinkOptions(options) {
1748
+ const source = (options ?? {});
1749
+ return {
1750
+ ...source,
1751
+ ...(source.Headers ? { Headers: { ...source.Headers } } : {}),
1752
+ ...(source.headers ? { headers: { ...source.headers } } : {}),
1753
+ ...(source.StaticTags ? { StaticTags: { ...source.StaticTags } } : {}),
1754
+ ...(source.staticTags ? { staticTags: { ...source.staticTags } } : {})
1755
+ };
1756
+ }
1757
+ class DirectVendorMetricReportingSink extends ExpandedEventReportingSink {
1758
+ constructor(sinkName, licenseFeature, defaultEndpointPath, options, requireBaseUrl = true) {
1759
+ super(sinkName, licenseFeature);
1760
+ this.iterationObservationShapeLimited = true;
1761
+ this.previousCounts = new Map();
1762
+ this.previousCountTimes = new Map();
1763
+ this.stateGeneration = 0;
1764
+ this.stateQueue = Promise.resolve();
1765
+ const source = asRecord(options);
1766
+ this.baseUrl = optionString(source, "endpointUrl", "EndpointUrl", "baseUrl", "BaseUrl").trim();
1767
+ this.endpointPath = optionString(source, "endpointPath", "EndpointPath").trim() || defaultEndpointPath;
1768
+ this.headers = normalizeProtocolHeaders(normalizeStringMap(optionRecord(source, "headers", "Headers")));
1769
+ this.staticTags = normalizeStringMap(optionRecord(source, "staticTags", "StaticTags"));
1770
+ this.timeoutMs = resolveTimeoutMs(optionNumber(source, "timeoutSeconds", "TimeoutSeconds"), optionNumber(source, "timeoutMs", "TimeoutMs"));
1771
+ this.fetchImpl = pickRecordValue(source, "fetchImpl", "FetchImpl") ?? fetch;
1772
+ const expandedKind = expandedReportingSinkKindFromName(sinkName);
1773
+ if (expandedKind) {
1774
+ rememberExpandedReportingSink(this, expandedKind, source);
1775
+ }
1776
+ this.configuredRunId = optionString(source, "runId", "RunId").trim();
1777
+ this.requireBaseUrl = requireBaseUrl;
1778
+ this.configuredFirstIntervalMs = reportingIntervalMilliseconds(optionNumber(source, "reportingIntervalSeconds", "ReportingIntervalSeconds"));
1779
+ }
1780
+ init(context, infraConfig) {
1781
+ super.init(context, infraConfig);
1782
+ const runtimeInterval = context[REPORTING_INTERVAL_SECONDS];
1783
+ if (runtimeInterval !== undefined) {
1784
+ this.configuredFirstIntervalMs = reportingIntervalMilliseconds(Number(runtimeInterval));
1785
+ }
1786
+ if (this.requireBaseUrl && !this.baseUrl) {
1787
+ throw new Error(`${this.constructor.name} requires BaseUrl.`);
1788
+ }
1789
+ if (this.baseUrl) {
1790
+ requireHttpReportingUrl(this.baseUrl, `${this.constructor.name} BaseUrl`);
1791
+ }
1792
+ }
1793
+ start(session) {
1794
+ super.start(session);
1795
+ this.stateGeneration += 1;
1796
+ this.previousCounts.clear();
1797
+ this.previousCountTimes.clear();
1798
+ const parsedStartedUtc = new Date(this.getSession().startedUtc);
1799
+ this.runStartedUtc = Number.isFinite(parsedStartedUtc.getTime()) ? parsedStartedUtc : undefined;
1800
+ }
1801
+ stop() {
1802
+ this.stateGeneration += 1;
1803
+ this.previousCounts.clear();
1804
+ this.previousCountTimes.clear();
1805
+ this.runStartedUtc = undefined;
1806
+ super.stop();
1807
+ }
1808
+ async persistEvents(events) {
1809
+ if (!events.length) {
1810
+ return;
1811
+ }
1812
+ const generation = this.stateGeneration;
1813
+ const eventRunId = String(events[0]?.sessionId ?? "").trim();
1814
+ const operation = this.stateQueue.then(async () => {
1815
+ const occurredUtc = new Date();
1816
+ const points = createDirectVendorMetricPoints(events, occurredUtc, this.configuredRunId || eventRunId, this.sinkName);
1817
+ if (!points.length) {
1818
+ return;
1819
+ }
1820
+ const prepared = this.prepareStatefulPoints(points);
1821
+ await this[SEND_DIRECT_VENDOR_PROTOCOL_POINTS](prepared.points);
1822
+ if (generation === this.stateGeneration) {
1823
+ this.previousCounts = prepared.counts;
1824
+ this.previousCountTimes = prepared.countTimes;
1825
+ }
1826
+ });
1827
+ this.stateQueue = operation.then(() => undefined, () => undefined);
1828
+ await operation;
1829
+ }
1830
+ prepareStatefulPoints(points) {
1831
+ const counts = new Map(this.previousCounts);
1832
+ const countTimes = new Map(Array.from(this.previousCountTimes, ([key, value]) => [key, new Date(value.getTime())]));
1833
+ const prepared = [];
1834
+ for (const point of points) {
1835
+ if (point.metricKind !== "count") {
1836
+ prepared.push(point);
1837
+ continue;
1838
+ }
1839
+ const key = directVendorMetricSeriesKey(point);
1840
+ const previousValue = counts.get(key);
1841
+ const previousTime = countTimes.get(key);
1842
+ const intervalMs = previousValue === undefined && this.configuredFirstIntervalMs !== undefined
1843
+ ? this.configuredFirstIntervalMs
1844
+ : positiveIntervalMilliseconds(previousTime ?? this.runStartedUtc, point.occurredUtc);
1845
+ const value = this.sinkName === "prometheus-remote-write" || previousValue === undefined
1846
+ ? point.value
1847
+ : point.value >= previousValue
1848
+ ? point.value - previousValue
1849
+ : point.value;
1850
+ prepared.push({ ...point, value, intervalMs });
1851
+ counts.set(key, point.value);
1852
+ countTimes.set(key, new Date(point.occurredUtc.getTime()));
1853
+ }
1854
+ return { points: prepared, counts, countTimes };
1855
+ }
1676
1856
  }
1677
- export class PrometheusRemoteWriteReportingSink extends ExpandedHttpJsonReportingSink {
1857
+ export class PrometheusRemoteWriteReportingSink extends DirectVendorMetricReportingSink {
1678
1858
  constructor(options = {}) {
1679
1859
  super("prometheus-remote-write", "extensions.reporting_sinks.prometheus_remote_write", "/api/v1/write", options);
1680
- this.iterationObservationShapeLimited = true;
1860
+ this.bearerToken = optionString(asRecord(options), "bearerToken", "BearerToken").trim();
1861
+ this.cloneOptions = cloneDirectVendorSinkOptions(options);
1862
+ rememberExpandedReportingSink(this, "PrometheusRemoteWrite", asRecord(options));
1863
+ }
1864
+ [CLONE_DIRECT_VENDOR_FOR_RUN]() {
1865
+ return new PrometheusRemoteWriteReportingSink(cloneDirectVendorSinkOptions(this.cloneOptions));
1866
+ }
1867
+ async [SEND_DIRECT_VENDOR_PROTOCOL_POINTS](points) {
1868
+ const encoded = encodePrometheusRemoteWrite(points, this.staticTags);
1869
+ const body = Uint8Array.from(compressSnappy(encoded.body));
1870
+ await postWithTimeout(this.fetchImpl, `${trimTrailingSlashes(this.baseUrl)}${normalizePath(this.endpointPath)}`, {
1871
+ method: "POST",
1872
+ headers: mergeProtocolHeaders(this.headers, {
1873
+ ...(this.bearerToken ? { Authorization: `Bearer ${this.bearerToken}` } : {}),
1874
+ "Content-Type": encoded.contentType,
1875
+ "Content-Encoding": "snappy",
1876
+ "X-Prometheus-Remote-Write-Version": "0.1.0",
1877
+ "User-Agent": LOADSTRIKE_TYPESCRIPT_SDK_USER_AGENT
1878
+ }),
1879
+ body: body.buffer
1880
+ }, this.timeoutMs, this.constructor.name, true);
1881
+ }
1882
+ }
1883
+ export class CloudWatchReportingSink extends DirectVendorMetricReportingSink {
1884
+ constructor(options = {}) {
1885
+ super("cloudwatch", "extensions.reporting_sinks.cloudwatch", "/", options, false);
1886
+ const source = asRecord(options);
1887
+ this.namespace = optionString(source, "namespace", "Namespace").trim() || "LoadStrike";
1888
+ this.region = optionString(source, "region", "Region").trim();
1889
+ this.accessKeyId = optionString(source, "accessKeyId", "AccessKeyId").trim();
1890
+ this.secretAccessKey = optionString(source, "secretAccessKey", "SecretAccessKey").trim();
1891
+ this.sessionToken = optionString(source, "sessionToken", "SessionToken").trim();
1892
+ this.cloneOptions = cloneDirectVendorSinkOptions(options);
1893
+ rememberExpandedReportingSink(this, "CloudWatch", source);
1894
+ }
1895
+ [CLONE_DIRECT_VENDOR_FOR_RUN]() {
1896
+ return new CloudWatchReportingSink(cloneDirectVendorSinkOptions(this.cloneOptions));
1897
+ }
1898
+ init(context, infraConfig) {
1899
+ if (normalizePath(this.endpointPath) !== "/") {
1900
+ throw new Error("CloudWatchReportingSink does not support EndpointPath; include any required path in EndpointUrl.");
1901
+ }
1902
+ if (!this.namespace) {
1903
+ throw new Error("CloudWatchReportingSink requires Namespace.");
1904
+ }
1905
+ if (!this.region) {
1906
+ throw new Error("CloudWatchReportingSink requires Region.");
1907
+ }
1908
+ if (!this.accessKeyId) {
1909
+ throw new Error("CloudWatchReportingSink requires AccessKeyId.");
1910
+ }
1911
+ if (!this.secretAccessKey) {
1912
+ throw new Error("CloudWatchReportingSink requires SecretAccessKey.");
1913
+ }
1914
+ super.init(context, infraConfig);
1915
+ this.cloudWatchClient?.destroy();
1916
+ this.cloudWatchClient = new CloudWatchClient({
1917
+ region: this.region,
1918
+ credentials: {
1919
+ accessKeyId: this.accessKeyId,
1920
+ secretAccessKey: this.secretAccessKey,
1921
+ ...(this.sessionToken ? { sessionToken: this.sessionToken } : {})
1922
+ },
1923
+ ...(this.baseUrl ? { endpoint: this.baseUrl } : {}),
1924
+ maxAttempts: 1
1925
+ });
1926
+ const customHeaders = Object.fromEntries(Object.entries(this.headers).filter(([name]) => !isCloudWatchProtocolHeader(name)));
1927
+ if (Object.keys(customHeaders).length > 0) {
1928
+ this.cloudWatchClient.middlewareStack.add((next) => async (args) => {
1929
+ const request = args.request;
1930
+ if (request?.headers) {
1931
+ const merged = mergeProtocolHeaders(request.headers, customHeaders);
1932
+ for (const name of Object.keys(request.headers)) {
1933
+ delete request.headers[name];
1934
+ }
1935
+ Object.assign(request.headers, merged);
1936
+ }
1937
+ return next(args);
1938
+ }, { step: "build", name: "loadStrikeCloudWatchHeaders", priority: "low" });
1939
+ }
1940
+ }
1941
+ Dispose() {
1942
+ this.cloudWatchClient?.destroy();
1943
+ this.cloudWatchClient = undefined;
1944
+ super.Dispose();
1945
+ }
1946
+ async [SEND_DIRECT_VENDOR_PROTOCOL_POINTS](points) {
1947
+ const client = this.cloudWatchClient;
1948
+ if (!client) {
1949
+ throw new Error("CloudWatchReportingSink has not been initialized.");
1950
+ }
1951
+ const payload = createCloudWatchMetricData(this.namespace, points, this.staticTags);
1952
+ const controller = new AbortController();
1953
+ const timer = setTimeout(() => controller.abort(), Math.max(this.timeoutMs, 1));
1954
+ try {
1955
+ for (const batch of createCloudWatchMetricDataBatches(payload.Namespace, payload.MetricData)) {
1956
+ await client.send(new PutMetricDataCommand(batch), { abortSignal: controller.signal });
1957
+ }
1958
+ }
1959
+ catch (error) {
1960
+ if (controller.signal.aborted) {
1961
+ throw new DOMException("CloudWatchReportingSink request aborted.", "AbortError");
1962
+ }
1963
+ void error;
1964
+ throw new Error("CloudWatchReportingSink write failed.");
1965
+ }
1966
+ finally {
1967
+ clearTimeout(timer);
1968
+ }
1681
1969
  }
1682
1970
  }
1683
- export class CloudWatchReportingSink extends ExpandedHttpJsonReportingSink {
1971
+ export class DynatraceReportingSink extends DirectVendorMetricReportingSink {
1684
1972
  constructor(options = {}) {
1685
- super("cloudwatch", "extensions.reporting_sinks.cloudwatch", "/loadstrike/events", options);
1686
- this.iterationObservationShapeLimited = true;
1973
+ super("dynatrace", "extensions.reporting_sinks.dynatrace", "/api/v2/metrics/ingest", options);
1974
+ this.apiToken = optionString(asRecord(options), "apiToken", "ApiToken").trim();
1975
+ this.cloneOptions = cloneDirectVendorSinkOptions(options);
1976
+ rememberExpandedReportingSink(this, "Dynatrace", asRecord(options));
1977
+ }
1978
+ [CLONE_DIRECT_VENDOR_FOR_RUN]() {
1979
+ return new DynatraceReportingSink(cloneDirectVendorSinkOptions(this.cloneOptions));
1980
+ }
1981
+ init(context, infraConfig) {
1982
+ if (!this.apiToken && !hasCaseInsensitiveHeader(this.headers, "Authorization")) {
1983
+ throw new Error("DynatraceReportingSink requires ApiToken.");
1984
+ }
1985
+ super.init(context, infraConfig);
1986
+ }
1987
+ async [SEND_DIRECT_VENDOR_PROTOCOL_POINTS](points) {
1988
+ const batches = encodeDynatraceMetricBatches(points, this.staticTags, DIRECT_VENDOR_MAXIMUM_BODY_BYTES_EXCLUSIVE);
1989
+ for (const encoded of batches) {
1990
+ await postWithTimeout(this.fetchImpl, `${trimTrailingSlashes(this.baseUrl)}${normalizePath(this.endpointPath)}`, {
1991
+ method: "POST",
1992
+ headers: mergeProtocolHeaders(this.headers, {
1993
+ ...(this.apiToken ? { Authorization: `Api-Token ${this.apiToken}` } : {}),
1994
+ "Content-Type": encoded.contentType
1995
+ }),
1996
+ body: Uint8Array.from(encoded.body).buffer
1997
+ }, this.timeoutMs, this.constructor.name);
1998
+ }
1687
1999
  }
1688
2000
  }
1689
- export class DynatraceReportingSink extends ExpandedHttpJsonReportingSink {
2001
+ export class NewRelicReportingSink extends DirectVendorMetricReportingSink {
1690
2002
  constructor(options = {}) {
1691
- super("dynatrace", "extensions.reporting_sinks.dynatrace", "/api/v2/logs/ingest", options);
1692
- this.iterationObservationShapeLimited = true;
2003
+ super("new-relic", "extensions.reporting_sinks.new_relic", "/metric/v1", options);
2004
+ this.licenseKey = optionString(asRecord(options), "licenseKey", "LicenseKey", "apiKey", "ApiKey").trim();
2005
+ this.cloneOptions = cloneDirectVendorSinkOptions(options);
2006
+ rememberExpandedReportingSink(this, "NewRelic", asRecord(options));
2007
+ }
2008
+ [CLONE_DIRECT_VENDOR_FOR_RUN]() {
2009
+ return new NewRelicReportingSink(cloneDirectVendorSinkOptions(this.cloneOptions));
2010
+ }
2011
+ init(context, infraConfig) {
2012
+ if (!this.licenseKey && !hasCaseInsensitiveHeader(this.headers, "Api-Key")) {
2013
+ throw new Error("NewRelicReportingSink requires LicenseKey or ApiKey.");
2014
+ }
2015
+ super.init(context, infraConfig);
2016
+ }
2017
+ async [SEND_DIRECT_VENDOR_PROTOCOL_POINTS](points) {
2018
+ const batches = encodeNewRelicMetricBatches(points, this.staticTags, DIRECT_VENDOR_MAXIMUM_BODY_BYTES_EXCLUSIVE);
2019
+ for (const encoded of batches) {
2020
+ await postWithTimeout(this.fetchImpl, `${trimTrailingSlashes(this.baseUrl)}${normalizePath(this.endpointPath)}`, {
2021
+ method: "POST",
2022
+ headers: mergeProtocolHeaders(this.headers, {
2023
+ ...(this.licenseKey ? { "Api-Key": this.licenseKey } : {}),
2024
+ "Content-Type": encoded.contentType
2025
+ }),
2026
+ body: Uint8Array.from(encoded.body).buffer
2027
+ }, this.timeoutMs, this.constructor.name);
2028
+ }
1693
2029
  }
1694
2030
  }
1695
2031
  export class ElasticsearchReportingSink extends ExpandedHttpJsonReportingSink {
1696
2032
  constructor(options = {}) {
1697
- super("elasticsearch", "extensions.reporting_sinks.elasticsearch", "/loadstrike-events/_doc", options);
2033
+ const prepared = prepareSearchSinkOptions("Elasticsearch", options);
2034
+ const defaultEndpointPath = prepared.usesIndexName
2035
+ ? searchDocumentEndpointPath(prepared.indexName, "ElasticsearchReportingSink IndexName")
2036
+ : "/loadstrike-events/_doc";
2037
+ super("elasticsearch", "extensions.reporting_sinks.elasticsearch", defaultEndpointPath, prepared.options, options);
2038
+ this.indexName = prepared.indexName;
2039
+ this.usesIndexName = prepared.usesIndexName;
2040
+ }
2041
+ init(context, infraConfig) {
2042
+ if (this.usesIndexName) {
2043
+ requireValidSearchIndexName(this.indexName, `${this.constructor.name} IndexName`);
2044
+ }
2045
+ super.init(context, infraConfig);
1698
2046
  }
1699
2047
  }
1700
2048
  export class OpenSearchReportingSink extends ExpandedHttpJsonReportingSink {
1701
2049
  constructor(options = {}) {
1702
- super("opensearch", "extensions.reporting_sinks.opensearch", "/loadstrike-events/_doc", options);
2050
+ const prepared = prepareSearchSinkOptions("OpenSearch", options);
2051
+ const defaultEndpointPath = prepared.usesIndexName
2052
+ ? searchDocumentEndpointPath(prepared.indexName, "OpenSearchReportingSink IndexName")
2053
+ : "/loadstrike-events/_doc";
2054
+ super("opensearch", "extensions.reporting_sinks.opensearch", defaultEndpointPath, prepared.options, options);
2055
+ this.indexName = prepared.indexName;
2056
+ this.usesIndexName = prepared.usesIndexName;
2057
+ this.username = prepared.username;
2058
+ this.password = prepared.password;
2059
+ }
2060
+ init(context, infraConfig) {
2061
+ if (this.usesIndexName) {
2062
+ requireValidSearchIndexName(this.indexName, `${this.constructor.name} IndexName`);
2063
+ }
2064
+ if (this.username && !this.password) {
2065
+ throw new Error(`${this.constructor.name} requires Password when Username is configured.`);
2066
+ }
2067
+ if (this.password && !this.username) {
2068
+ throw new Error(`${this.constructor.name} requires Username when Password is configured.`);
2069
+ }
2070
+ super.init(context, infraConfig);
1703
2071
  }
1704
2072
  }
1705
- export class NewRelicReportingSink extends ExpandedHttpJsonReportingSink {
1706
- constructor(options = {}) {
1707
- super("new-relic", "extensions.reporting_sinks.new_relic", "/log/v1", options);
1708
- this.iterationObservationShapeLimited = true;
2073
+ function prepareSearchSinkOptions(kind, options) {
2074
+ const source = asRecord(options);
2075
+ const rawIndexName = pickRecordValue(source, "indexName", "IndexName");
2076
+ const endpointPath = optionString(source, "endpointPath", "EndpointPath").trim();
2077
+ const indexName = rawIndexName === undefined
2078
+ ? "loadstrike-events"
2079
+ : typeof rawIndexName === "string"
2080
+ ? rawIndexName.trim()
2081
+ : "";
2082
+ let headers = normalizeStringMap(optionRecord(source, "headers", "Headers"));
2083
+ let username = "";
2084
+ let password = "";
2085
+ if (kind === "Elasticsearch") {
2086
+ const apiKey = optionString(source, "apiKey", "ApiKey").trim();
2087
+ if (apiKey && !hasCaseInsensitiveHeader(headers, "Authorization")) {
2088
+ headers = mergeProtocolHeaders(headers, { Authorization: `ApiKey ${apiKey}` });
2089
+ }
2090
+ }
2091
+ else {
2092
+ username = optionString(source, "username", "Username").trim();
2093
+ password = optionString(source, "password", "Password");
2094
+ if ((username || password) && !hasCaseInsensitiveHeader(headers, "Authorization")) {
2095
+ headers = mergeProtocolHeaders(headers, {
2096
+ Authorization: `Basic ${Buffer.from(`${username}:${password}`, "utf8").toString("base64")}`
2097
+ });
2098
+ }
1709
2099
  }
2100
+ return {
2101
+ indexName,
2102
+ usesIndexName: !endpointPath,
2103
+ username,
2104
+ password,
2105
+ options: { ...source, headers }
2106
+ };
2107
+ }
2108
+ function searchDocumentEndpointPath(indexName, path) {
2109
+ if (hasUnpairedUnicodeSurrogate(indexName)) {
2110
+ throw new Error(`${path} must be a valid search index name.`);
2111
+ }
2112
+ return `/${encodeURIComponent(indexName)}/_doc`;
2113
+ }
2114
+ function requireValidSearchIndexName(indexName, path) {
2115
+ const byteLength = Buffer.byteLength(indexName, "utf8");
2116
+ const invalid = !indexName
2117
+ || indexName !== indexName.toLowerCase()
2118
+ || byteLength > 255
2119
+ || indexName === "."
2120
+ || indexName === ".."
2121
+ || /^[\-_+]/u.test(indexName)
2122
+ || /[?*\u002f\u005c\x22<>| ,#:]/u.test(indexName)
2123
+ || /[\u0000-\u001f\u007f]/u.test(indexName)
2124
+ || hasUnpairedUnicodeSurrogate(indexName);
2125
+ if (invalid) {
2126
+ throw new Error(`${path} must be a valid search index name.`);
2127
+ }
2128
+ }
2129
+ function hasUnpairedUnicodeSurrogate(value) {
2130
+ for (let index = 0; index < value.length; index += 1) {
2131
+ const codeUnit = value.charCodeAt(index);
2132
+ if (codeUnit >= 0xd800 && codeUnit <= 0xdbff) {
2133
+ const next = index + 1 < value.length ? value.charCodeAt(index + 1) : -1;
2134
+ if (next < 0xdc00 || next > 0xdfff) {
2135
+ return true;
2136
+ }
2137
+ index += 1;
2138
+ }
2139
+ else if (codeUnit >= 0xdc00 && codeUnit <= 0xdfff) {
2140
+ return true;
2141
+ }
2142
+ }
2143
+ return false;
1710
2144
  }
1711
2145
  export class GenericWebhookReportingSink extends ExpandedHttpJsonReportingSink {
1712
2146
  constructor(options = {}) {
@@ -1731,8 +2165,17 @@ export class KafkaReportingSink extends ExpandedEventReportingSink {
1731
2165
  super("kafka", "extensions.reporting_sinks.kafka");
1732
2166
  const source = asRecord(options);
1733
2167
  this.topic = optionString(source, "topic", "Topic").trim() || "loadstrike-events";
2168
+ this.bootstrapServers = optionString(source, "bootstrapServers", "BootstrapServers").trim();
1734
2169
  this.staticTags = normalizeStringMap(optionRecord(source, "staticTags", "StaticTags"));
1735
2170
  this.publishAsync = pickRecordValue(source, "publishAsync", "PublishAsync");
2171
+ rememberExpandedReportingSink(this, "Kafka", source);
2172
+ }
2173
+ init(context, infraConfig) {
2174
+ if (!this.publishAsync) {
2175
+ throw new Error("KafkaReportingSink requires PublishAsync because TypeScript Kafka reporting is callback-backed; "
2176
+ + "BootstrapServers does not enable native broker delivery.");
2177
+ }
2178
+ super.init(context, infraConfig);
1736
2179
  }
1737
2180
  async saveIterationBatch(batch) {
1738
2181
  await this.publishCanonicalValue(batch);
@@ -1767,6 +2210,10 @@ class StatsDReportingSinkBase extends ExpandedEventReportingSink {
1767
2210
  this.tags = normalizeStringMap(optionRecord(source, "tags", "Tags"));
1768
2211
  this.sendLineAsync = pickRecordValue(source, "sendLineAsync", "SendLineAsync");
1769
2212
  this.dogStatsD = dogStatsD;
2213
+ const expandedKind = expandedReportingSinkKindFromName(sinkName);
2214
+ if (expandedKind) {
2215
+ rememberExpandedReportingSink(this, expandedKind, source);
2216
+ }
1770
2217
  }
1771
2218
  async saveIterationBatch(batch) {
1772
2219
  for (const observation of batch.observations) {
@@ -1853,6 +2300,7 @@ export class JsonlFileReportingSink extends ExpandedEventReportingSink {
1853
2300
  const source = asRecord(options);
1854
2301
  this.filePath = optionString(source, "filePath", "FilePath").trim() || "loadstrike-reporting.jsonl";
1855
2302
  this.staticTags = normalizeStringMap(optionRecord(source, "staticTags", "StaticTags"));
2303
+ rememberExpandedReportingSink(this, "Jsonl", source);
1856
2304
  }
1857
2305
  async persistEvents(events) {
1858
2306
  if (!events.length) {
@@ -2046,6 +2494,8 @@ function createScenarioEvent(session, occurredUtc, phase, scenario) {
2046
2494
  all_request_count: scenario.allRequestCount,
2047
2495
  all_ok_count: scenario.allOkCount,
2048
2496
  all_fail_count: scenario.allFailCount,
2497
+ all_ok_count_64: scenario.ok?.count64 ?? scenario.ok?.Count64,
2498
+ all_fail_count_64: scenario.fail?.count64 ?? scenario.fail?.Count64,
2049
2499
  all_bytes: scenario.allBytes,
2050
2500
  duration_ms: scenario.durationMs,
2051
2501
  sort_index: scenario.sortIndex,
@@ -2575,6 +3025,120 @@ function toReportingSinkEventJson(event) {
2575
3025
  Fields: event.fields
2576
3026
  });
2577
3027
  }
3028
+ function createDirectVendorMetricPoints(events, occurredUtc, runId, sinkName) {
3029
+ const points = [];
3030
+ for (const event of events) {
3031
+ const eventType = String(event.eventType ?? "").trim().toLowerCase();
3032
+ const scenario = String(event.scenarioName ?? "").trim() || "default";
3033
+ const status = String(event.tags?.status ?? "").trim() || "ok";
3034
+ if (eventType === "scenario.realtime" || eventType === "scenario.final") {
3035
+ let statusCountPresent = false;
3036
+ for (const scenarioStatus of ["ok", "fail"]) {
3037
+ const count = protocolCountValue(sinkName, event.fields[`all_${scenarioStatus}_count_64`], event.fields[`all_${scenarioStatus}_count`], event.fields[`${scenarioStatus}_request_count`]);
3038
+ statusCountPresent || (statusCountPresent = count.present);
3039
+ if (!count.present || count.value === 0) {
3040
+ continue;
3041
+ }
3042
+ points.push({
3043
+ metricName: "loadstrike_requests_total",
3044
+ metricKind: "count",
3045
+ occurredUtc: new Date(occurredUtc.getTime()),
3046
+ value: count.value,
3047
+ unitOfMeasure: "count",
3048
+ scenario,
3049
+ status: scenarioStatus,
3050
+ ...(runId ? { runId } : {}),
3051
+ intervalMs: 1
3052
+ });
3053
+ const latency = event.fields[`${scenarioStatus}_latency_mean_ms`];
3054
+ if (isFiniteNumberLike(latency)) {
3055
+ points.push({
3056
+ metricName: "loadstrike_request_duration_ms",
3057
+ metricKind: "gauge",
3058
+ occurredUtc: new Date(occurredUtc.getTime()),
3059
+ value: toNumericValue(latency),
3060
+ unitOfMeasure: "ms",
3061
+ scenario,
3062
+ status: scenarioStatus,
3063
+ ...(runId ? { runId } : {})
3064
+ });
3065
+ }
3066
+ }
3067
+ if (!statusCountPresent && isFiniteNumberLike(event.fields.all_request_count)) {
3068
+ points.push({
3069
+ metricName: "loadstrike_requests_total",
3070
+ metricKind: "count",
3071
+ occurredUtc: new Date(occurredUtc.getTime()),
3072
+ value: toNumericValue(event.fields.all_request_count),
3073
+ unitOfMeasure: "count",
3074
+ scenario,
3075
+ status,
3076
+ ...(runId ? { runId } : {}),
3077
+ intervalMs: 1
3078
+ });
3079
+ }
3080
+ continue;
3081
+ }
3082
+ const isCounter = eventType.startsWith("metric.counter");
3083
+ const isGauge = eventType.startsWith("metric.gauge");
3084
+ if ((!isCounter && !isGauge) || !isFiniteNumberLike(event.fields.value)) {
3085
+ continue;
3086
+ }
3087
+ const unit = event.fields.unit_of_measure;
3088
+ points.push({
3089
+ metricName: buildCustomMetricName(String(event.tags?.metric_name ?? "")),
3090
+ metricKind: isCounter ? "count" : "gauge",
3091
+ occurredUtc: new Date(occurredUtc.getTime()),
3092
+ value: toNumericValue(event.fields.value),
3093
+ ...(typeof unit === "string" ? { unitOfMeasure: unit } : {}),
3094
+ scenario,
3095
+ status,
3096
+ ...(runId ? { runId } : {}),
3097
+ ...(isCounter ? { intervalMs: 1 } : {})
3098
+ });
3099
+ }
3100
+ return points;
3101
+ }
3102
+ function directVendorMetricSeriesKey(point) {
3103
+ return JSON.stringify([
3104
+ point.metricName,
3105
+ point.scenario,
3106
+ point.status,
3107
+ point.runId ?? ""
3108
+ ]);
3109
+ }
3110
+ function createCloudWatchMetricDataBatches(namespace, metricData) {
3111
+ const emptyBodyBytes = Buffer.byteLength(JSON.stringify({ Namespace: namespace, MetricData: [] }), "utf8");
3112
+ const batches = [];
3113
+ let current = [];
3114
+ let currentBodyBytes = emptyBodyBytes;
3115
+ for (const datum of metricData) {
3116
+ const datumBodyBytes = Buffer.byteLength(JSON.stringify(datum), "utf8");
3117
+ const separatorBytes = current.length > 0 ? 1 : 0;
3118
+ const exceedsDatumLimit = current.length >= CLOUDWATCH_MAXIMUM_DATUMS_PER_REQUEST;
3119
+ const exceedsBodyLimit = currentBodyBytes + separatorBytes + datumBodyBytes > CLOUDWATCH_SAFE_REQUEST_BODY_BYTES;
3120
+ if (current.length > 0 && (exceedsDatumLimit || exceedsBodyLimit)) {
3121
+ batches.push({
3122
+ Namespace: namespace,
3123
+ MetricData: current
3124
+ });
3125
+ current = [];
3126
+ currentBodyBytes = emptyBodyBytes;
3127
+ }
3128
+ if (currentBodyBytes + (current.length > 0 ? 1 : 0) + datumBodyBytes > CLOUDWATCH_SAFE_REQUEST_BODY_BYTES) {
3129
+ throw new Error("CloudWatchReportingSink metric datum exceeds the safe request size.");
3130
+ }
3131
+ currentBodyBytes += (current.length > 0 ? 1 : 0) + datumBodyBytes;
3132
+ current.push(datum);
3133
+ }
3134
+ if (current.length > 0) {
3135
+ batches.push({
3136
+ Namespace: namespace,
3137
+ MetricData: current
3138
+ });
3139
+ }
3140
+ return batches;
3141
+ }
2578
3142
  function createReportingSinkMetricPoints(sinkName, events, staticTags) {
2579
3143
  const points = [];
2580
3144
  for (const event of events) {
@@ -2686,6 +3250,45 @@ function toNumericValue(value) {
2686
3250
  }
2687
3251
  return Number(value);
2688
3252
  }
3253
+ function protocolCountValue(sinkName, ...values) {
3254
+ let zeroPresent = false;
3255
+ for (const value of values) {
3256
+ if (value === undefined || value === null) {
3257
+ continue;
3258
+ }
3259
+ let parsed;
3260
+ if (typeof value === "string") {
3261
+ const text = value.trim();
3262
+ if (!text) {
3263
+ continue;
3264
+ }
3265
+ if (!/^\d+$/.test(text)) {
3266
+ throw invalidProtocolRequestCount(sinkName);
3267
+ }
3268
+ parsed = BigInt(text);
3269
+ }
3270
+ else if (typeof value === "bigint") {
3271
+ parsed = value;
3272
+ }
3273
+ else if (typeof value === "number" && Number.isSafeInteger(value)) {
3274
+ parsed = BigInt(value);
3275
+ }
3276
+ else {
3277
+ throw invalidProtocolRequestCount(sinkName);
3278
+ }
3279
+ if (parsed < 0n || parsed > MAXIMUM_NON_NEGATIVE_SIGNED_INT64) {
3280
+ throw invalidProtocolRequestCount(sinkName);
3281
+ }
3282
+ if (parsed !== 0n) {
3283
+ return { value: Number(parsed), present: true };
3284
+ }
3285
+ zeroPresent = true;
3286
+ }
3287
+ return { value: 0, present: zeroPresent };
3288
+ }
3289
+ function invalidProtocolRequestCount(sinkName) {
3290
+ return new Error(`${sinkName} request count must be a non-negative signed 64-bit integer.`);
3291
+ }
2689
3292
  function inferUnitOfMeasure(fieldName) {
2690
3293
  const normalized = fieldName.toLowerCase();
2691
3294
  if (normalized.endsWith("_ms")) {
@@ -3199,6 +3802,620 @@ function mergeOtelCollectorOptions(target, source) {
3199
3802
  target.staticResourceAttributes = normalizeStringMap(source.staticResourceAttributes);
3200
3803
  }
3201
3804
  }
3805
+ const DEFAULT_EXPANDED_REPORTING_SECTION_PATHS = {
3806
+ PrometheusRemoteWrite: "LoadStrike:ReportingSinks:PrometheusRemoteWrite",
3807
+ CloudWatch: "LoadStrike:ReportingSinks:CloudWatch",
3808
+ Dynatrace: "LoadStrike:ReportingSinks:Dynatrace",
3809
+ Elasticsearch: "LoadStrike:ReportingSinks:Elasticsearch",
3810
+ OpenSearch: "LoadStrike:ReportingSinks:OpenSearch",
3811
+ NewRelic: "LoadStrike:ReportingSinks:NewRelic",
3812
+ Webhook: "LoadStrike:ReportingSinks:Webhook",
3813
+ Kafka: "LoadStrike:ReportingSinks:Kafka",
3814
+ StatsD: "LoadStrike:ReportingSinks:StatsD",
3815
+ Netdata: "LoadStrike:ReportingSinks:Netdata",
3816
+ DogStatsD: "LoadStrike:ReportingSinks:DogStatsD",
3817
+ Jsonl: "LoadStrike:ReportingSinks:Jsonl"
3818
+ };
3819
+ const EXPANDED_HTTP_CONFIG_FIELDS = [
3820
+ { canonicalName: "BaseUrl", valueKind: "string" },
3821
+ { canonicalName: "EndpointPath", valueKind: "string" },
3822
+ { canonicalName: "Headers", valueKind: "stringMap" },
3823
+ { canonicalName: "StaticTags", valueKind: "stringMap" },
3824
+ { canonicalName: "TimeoutSeconds", valueKind: "number" },
3825
+ { canonicalName: "TimeoutMs", valueKind: "number" }
3826
+ ];
3827
+ const DIRECT_VENDOR_CONFIG_FIELDS = [
3828
+ ...EXPANDED_HTTP_CONFIG_FIELDS,
3829
+ { canonicalName: "RunId", valueKind: "string" },
3830
+ { canonicalName: "ReportingIntervalSeconds", valueKind: "number" }
3831
+ ];
3832
+ const EXPANDED_CONFIG_FIELDS = {
3833
+ PrometheusRemoteWrite: [
3834
+ ...DIRECT_VENDOR_CONFIG_FIELDS,
3835
+ { canonicalName: "BearerToken", valueKind: "string" }
3836
+ ],
3837
+ CloudWatch: [
3838
+ ...DIRECT_VENDOR_CONFIG_FIELDS,
3839
+ { canonicalName: "EndpointUrl", valueKind: "string" },
3840
+ { canonicalName: "Namespace", valueKind: "string" },
3841
+ { canonicalName: "Region", valueKind: "string" },
3842
+ { canonicalName: "AccessKeyId", valueKind: "string" },
3843
+ { canonicalName: "SecretAccessKey", valueKind: "string" },
3844
+ { canonicalName: "SessionToken", valueKind: "string" }
3845
+ ],
3846
+ Dynatrace: [
3847
+ ...DIRECT_VENDOR_CONFIG_FIELDS,
3848
+ { canonicalName: "ApiToken", valueKind: "string" }
3849
+ ],
3850
+ Elasticsearch: [
3851
+ ...EXPANDED_HTTP_CONFIG_FIELDS,
3852
+ { canonicalName: "IndexName", valueKind: "string" },
3853
+ { canonicalName: "ApiKey", valueKind: "string" }
3854
+ ],
3855
+ OpenSearch: [
3856
+ ...EXPANDED_HTTP_CONFIG_FIELDS,
3857
+ { canonicalName: "IndexName", valueKind: "string" },
3858
+ { canonicalName: "Username", valueKind: "string" },
3859
+ { canonicalName: "Password", valueKind: "string", preserveWhitespace: true }
3860
+ ],
3861
+ NewRelic: [
3862
+ ...DIRECT_VENDOR_CONFIG_FIELDS,
3863
+ { canonicalName: "LicenseKey", valueKind: "string" },
3864
+ { canonicalName: "ApiKey", valueKind: "string" }
3865
+ ],
3866
+ Webhook: [
3867
+ ...EXPANDED_HTTP_CONFIG_FIELDS,
3868
+ { canonicalName: "Url", valueKind: "string" },
3869
+ { canonicalName: "Secret", valueKind: "string" }
3870
+ ],
3871
+ Kafka: [
3872
+ { canonicalName: "BootstrapServers", valueKind: "string" },
3873
+ { canonicalName: "Topic", valueKind: "string" },
3874
+ { canonicalName: "StaticTags", valueKind: "stringMap" }
3875
+ ],
3876
+ StatsD: statsDExpandedConfigFields(),
3877
+ Netdata: statsDExpandedConfigFields(),
3878
+ DogStatsD: statsDExpandedConfigFields(),
3879
+ Jsonl: [
3880
+ { canonicalName: "FilePath", valueKind: "string" },
3881
+ { canonicalName: "StaticTags", valueKind: "stringMap" }
3882
+ ]
3883
+ };
3884
+ function statsDExpandedConfigFields() {
3885
+ return [
3886
+ { canonicalName: "Prefix", valueKind: "string" },
3887
+ { canonicalName: "Host", valueKind: "string" },
3888
+ { canonicalName: "Port", valueKind: "number" },
3889
+ { canonicalName: "Tags", valueKind: "stringMap", aliases: ["StaticTags"] }
3890
+ ];
3891
+ }
3892
+ function rememberExpandedReportingSink(sink, kind, options) {
3893
+ const configuredPath = optionString(options, "configurationSectionPath", "ConfigurationSectionPath").trim();
3894
+ EXPANDED_REPORTING_SINK_BINDINGS.set(sink, {
3895
+ kind,
3896
+ configurationSectionPath: configuredPath || DEFAULT_EXPANDED_REPORTING_SECTION_PATHS[kind],
3897
+ options: cloneExpandedSinkOptionRecord(options)
3898
+ });
3899
+ }
3900
+ function expandedReportingSinkConstructor(kind) {
3901
+ switch (kind) {
3902
+ case "PrometheusRemoteWrite": return PrometheusRemoteWriteReportingSink;
3903
+ case "CloudWatch": return CloudWatchReportingSink;
3904
+ case "Dynatrace": return DynatraceReportingSink;
3905
+ case "Elasticsearch": return ElasticsearchReportingSink;
3906
+ case "OpenSearch": return OpenSearchReportingSink;
3907
+ case "NewRelic": return NewRelicReportingSink;
3908
+ case "Webhook": return GenericWebhookReportingSink;
3909
+ case "Kafka": return KafkaReportingSink;
3910
+ case "StatsD": return StatsDReportingSink;
3911
+ case "Netdata": return NetdataStatsDReportingSink;
3912
+ case "DogStatsD": return DogStatsDReportingSink;
3913
+ case "Jsonl": return JsonlFileReportingSink;
3914
+ }
3915
+ }
3916
+ function assertExpandedSinkSupportsConfigurationBinding(sink, kind, sectionPath) {
3917
+ if (sink.constructor === expandedReportingSinkConstructor(kind)) {
3918
+ return;
3919
+ }
3920
+ throw new Error(`${sectionPath} cannot be applied to ${sink.constructor.name || "a custom reporting sink"}; `
3921
+ + "configure the custom sink explicitly in code instead.");
3922
+ }
3923
+ function assertExpandedSinkExecutableOptions(binding) {
3924
+ if (binding.kind !== "Kafka") {
3925
+ return;
3926
+ }
3927
+ const publishAsync = pickRecordValue(binding.options, "publishAsync", "PublishAsync");
3928
+ if (typeof publishAsync === "function") {
3929
+ return;
3930
+ }
3931
+ throw new Error(`${binding.configurationSectionPath}:PublishAsync is required because TypeScript Kafka reporting is `
3932
+ + "callback-backed; BootstrapServers does not enable native broker delivery.");
3933
+ }
3934
+ function expandedReportingSinkKindFromName(sinkName) {
3935
+ switch (normalizeConfigKey(sinkName)) {
3936
+ case "prometheusremotewrite": return "PrometheusRemoteWrite";
3937
+ case "cloudwatch": return "CloudWatch";
3938
+ case "dynatrace": return "Dynatrace";
3939
+ case "elasticsearch": return "Elasticsearch";
3940
+ case "opensearch": return "OpenSearch";
3941
+ case "newrelic": return "NewRelic";
3942
+ case "webhook": return "Webhook";
3943
+ case "kafka": return "Kafka";
3944
+ case "statsd": return "StatsD";
3945
+ case "netdatastatsd": return "Netdata";
3946
+ case "dogstatsd": return "DogStatsD";
3947
+ case "jsonl": return "Jsonl";
3948
+ default: return undefined;
3949
+ }
3950
+ }
3951
+ function cloneExpandedSinkOptionRecord(options) {
3952
+ const clone = { ...options };
3953
+ for (const [key, value] of Object.entries(clone)) {
3954
+ if (isRecord(value)) {
3955
+ clone[key] = { ...value };
3956
+ }
3957
+ else if (Array.isArray(value)) {
3958
+ clone[key] = [...value];
3959
+ }
3960
+ }
3961
+ return clone;
3962
+ }
3963
+ function createBoundExpandedReportingSink(sourceSink, binding, infraConfig, environment) {
3964
+ const section = readExpandedConfigSection(infraConfig, environment, binding.configurationSectionPath);
3965
+ const options = cloneExpandedSinkOptionRecord(binding.options);
3966
+ if (section.present) {
3967
+ assertExpandedSinkSupportsConfigurationBinding(sourceSink, binding.kind, section.path);
3968
+ const configured = normalizeExpandedConfigValues(binding.kind, section.path, section.configurationValues);
3969
+ const environmentConfigured = normalizeExpandedConfigValues(binding.kind, section.path, section.environmentValues);
3970
+ const normalized = mergeExpandedConfigSources(binding.kind, configured, environmentConfigured);
3971
+ applyMissingExpandedConfigOptions(binding.kind, options, binding.options, normalized);
3972
+ prepareExpandedCredentialOptions(binding.kind, options, {
3973
+ explicit: binding.options,
3974
+ environment: environmentConfigured,
3975
+ configuration: configured
3976
+ });
3977
+ validateBoundExpandedConfig(binding.kind, options, section.path);
3978
+ }
3979
+ switch (binding.kind) {
3980
+ case "PrometheusRemoteWrite":
3981
+ return new PrometheusRemoteWriteReportingSink(options);
3982
+ case "CloudWatch":
3983
+ return new CloudWatchReportingSink(options);
3984
+ case "Dynatrace":
3985
+ return new DynatraceReportingSink(options);
3986
+ case "Elasticsearch":
3987
+ return new ElasticsearchReportingSink(options);
3988
+ case "OpenSearch":
3989
+ return new OpenSearchReportingSink(options);
3990
+ case "NewRelic":
3991
+ return new NewRelicReportingSink(options);
3992
+ case "Webhook":
3993
+ return new GenericWebhookReportingSink(options);
3994
+ case "Kafka":
3995
+ return new KafkaReportingSink(options);
3996
+ case "StatsD":
3997
+ return new StatsDReportingSink(options);
3998
+ case "Netdata":
3999
+ return new NetdataStatsDReportingSink(options);
4000
+ case "DogStatsD":
4001
+ return new DogStatsDReportingSink(options);
4002
+ case "Jsonl":
4003
+ return new JsonlFileReportingSink(options);
4004
+ }
4005
+ }
4006
+ function readExpandedConfigSection(infraConfig, environment, configuredPath) {
4007
+ const pathSegments = splitExpandedConfigPath(configuredPath);
4008
+ const diagnosticPath = pathSegments.join(":");
4009
+ const configurationValues = {};
4010
+ const environmentValues = {};
4011
+ let present = false;
4012
+ const visitConfig = (record, consumed) => {
4013
+ for (const [rawKey, rawValue] of Object.entries(record)) {
4014
+ const candidate = [...consumed, ...splitExpandedConfigPath(rawKey)];
4015
+ if (!expandedConfigPathStartsWith(candidate, pathSegments) &&
4016
+ !expandedConfigPathStartsWith(pathSegments, candidate)) {
4017
+ continue;
4018
+ }
4019
+ if (expandedConfigPathEquals(candidate, pathSegments)) {
4020
+ present = true;
4021
+ if (isRecord(rawValue)) {
4022
+ mergeExpandedConfigRecords(configurationValues, rawValue);
4023
+ }
4024
+ continue;
4025
+ }
4026
+ if (candidate.length < pathSegments.length) {
4027
+ if (isRecord(rawValue)) {
4028
+ visitConfig(rawValue, candidate);
4029
+ }
4030
+ continue;
4031
+ }
4032
+ if (expandedConfigPathStartsWith(candidate, pathSegments)) {
4033
+ present = true;
4034
+ setExpandedConfigTail(configurationValues, candidate.slice(pathSegments.length), rawValue);
4035
+ }
4036
+ }
4037
+ };
4038
+ if (isRecord(infraConfig)) {
4039
+ visitConfig(infraConfig, []);
4040
+ }
4041
+ for (const [rawKey, rawValue] of Object.entries(environment)) {
4042
+ if (rawValue === undefined) {
4043
+ continue;
4044
+ }
4045
+ const candidate = splitExpandedConfigPath(rawKey);
4046
+ if (candidate.length <= pathSegments.length ||
4047
+ !expandedConfigPathStartsWith(candidate, pathSegments)) {
4048
+ continue;
4049
+ }
4050
+ present = true;
4051
+ setExpandedConfigTail(environmentValues, candidate.slice(pathSegments.length), rawValue);
4052
+ }
4053
+ return { path: diagnosticPath, present, configurationValues, environmentValues };
4054
+ }
4055
+ function splitExpandedConfigPath(value) {
4056
+ return String(value ?? "")
4057
+ .split(/__|[:.]/g)
4058
+ .map((segment) => segment.trim())
4059
+ .filter(Boolean);
4060
+ }
4061
+ function expandedConfigPathStartsWith(candidate, expectedPrefix) {
4062
+ return candidate.length >= expectedPrefix.length && expectedPrefix.every((segment, index) => normalizeConfigKey(candidate[index]) === normalizeConfigKey(segment));
4063
+ }
4064
+ function expandedConfigPathEquals(left, right) {
4065
+ return left.length === right.length && expandedConfigPathStartsWith(left, right);
4066
+ }
4067
+ function setExpandedConfigTail(target, tail, value) {
4068
+ if (!tail.length) {
4069
+ return;
4070
+ }
4071
+ let current = target;
4072
+ for (let index = 0; index < tail.length - 1; index += 1) {
4073
+ const segment = tail[index];
4074
+ const existingKey = Object.keys(current).find((key) => normalizeConfigKey(key) === normalizeConfigKey(segment));
4075
+ const key = existingKey ?? segment;
4076
+ if (!isRecord(current[key])) {
4077
+ current[key] = {};
4078
+ }
4079
+ current = current[key];
4080
+ }
4081
+ const leaf = tail[tail.length - 1];
4082
+ const existingLeaf = Object.keys(current).find((key) => normalizeConfigKey(key) === normalizeConfigKey(leaf));
4083
+ current[existingLeaf ?? leaf] = value;
4084
+ }
4085
+ function mergeExpandedConfigRecords(target, source) {
4086
+ for (const [rawKey, rawValue] of Object.entries(source)) {
4087
+ const existingKey = Object.keys(target).find((key) => normalizeConfigKey(key) === normalizeConfigKey(rawKey));
4088
+ const key = existingKey ?? rawKey;
4089
+ if (isRecord(target[key]) && isRecord(rawValue)) {
4090
+ mergeExpandedConfigRecords(target[key], rawValue);
4091
+ }
4092
+ else {
4093
+ target[key] = isRecord(rawValue) ? cloneExpandedSinkOptionRecord(rawValue) : rawValue;
4094
+ }
4095
+ }
4096
+ }
4097
+ function normalizeExpandedConfigValues(kind, sectionPath, values) {
4098
+ const fields = EXPANDED_CONFIG_FIELDS[kind];
4099
+ const normalized = {};
4100
+ const entries = Object.entries(values);
4101
+ for (const [rawKey] of entries) {
4102
+ const field = fields.find((candidate) => [candidate.canonicalName, ...(candidate.aliases ?? [])]
4103
+ .some((name) => normalizeConfigKey(name) === normalizeConfigKey(rawKey)));
4104
+ if (!field) {
4105
+ throw new Error(`${sectionPath}:${rawKey} is not supported.`);
4106
+ }
4107
+ }
4108
+ for (const field of fields) {
4109
+ const namesByPrecedence = [...(field.aliases ?? []), field.canonicalName];
4110
+ let selectedValue;
4111
+ let selected = false;
4112
+ for (const name of namesByPrecedence) {
4113
+ const matchingEntries = entries.filter(([rawKey]) => normalizeConfigKey(rawKey) === normalizeConfigKey(name));
4114
+ if (matchingEntries.length > 1) {
4115
+ throw new Error(`${sectionPath}:${name} is configured more than once.`);
4116
+ }
4117
+ if (matchingEntries.length === 0) {
4118
+ continue;
4119
+ }
4120
+ const [rawKey, rawValue] = matchingEntries[0];
4121
+ const value = normalizeExpandedConfigValue(rawValue, field, `${sectionPath}:${rawKey}`);
4122
+ selectedValue = field.valueKind === "stringMap" && selected &&
4123
+ isRecord(selectedValue) && isRecord(value)
4124
+ ? { ...selectedValue, ...value }
4125
+ : value;
4126
+ selected = true;
4127
+ }
4128
+ if (selected) {
4129
+ normalized[field.canonicalName] = selectedValue;
4130
+ }
4131
+ }
4132
+ return normalized;
4133
+ }
4134
+ function normalizeExpandedConfigValue(value, field, path) {
4135
+ if (field.valueKind === "string") {
4136
+ if (typeof value !== "string") {
4137
+ throw new Error(`${path} must be a string.`);
4138
+ }
4139
+ return field.preserveWhitespace ? value : value.trim();
4140
+ }
4141
+ if (field.valueKind === "number") {
4142
+ if (typeof value === "boolean" || value == null ||
4143
+ (typeof value !== "number" && typeof value !== "string")) {
4144
+ throw new Error(`${path} must be a finite number.`);
4145
+ }
4146
+ const parsed = typeof value === "number" ? value : Number(value.trim());
4147
+ if (!Number.isFinite(parsed)) {
4148
+ throw new Error(`${path} must be a finite number.`);
4149
+ }
4150
+ return parsed;
4151
+ }
4152
+ if (!isRecord(value)) {
4153
+ throw new Error(`${path} must be an object of string values.`);
4154
+ }
4155
+ const normalizedMap = {};
4156
+ for (const [mapKey, mapValue] of Object.entries(value)) {
4157
+ if (typeof mapValue !== "string") {
4158
+ throw new Error(`${path}:${mapKey} must be a string value.`);
4159
+ }
4160
+ normalizedMap[mapKey] = mapValue;
4161
+ }
4162
+ return normalizedMap;
4163
+ }
4164
+ function mergeExpandedConfigSources(kind, configured, environment) {
4165
+ const merged = cloneExpandedSinkOptionRecord(configured);
4166
+ const clearedSemanticGroups = new Set();
4167
+ for (const [canonicalName, value] of Object.entries(environment)) {
4168
+ const precedenceGroup = expandedOptionPrecedenceGroup(kind, canonicalName);
4169
+ const groupIdentity = precedenceGroup
4170
+ .map((name) => normalizeConfigKey(name))
4171
+ .sort()
4172
+ .join("|");
4173
+ const lowerPriorityMap = precedenceGroup.length === 1 && isRecord(merged[canonicalName])
4174
+ ? merged[canonicalName]
4175
+ : undefined;
4176
+ if (precedenceGroup.length > 1 && !clearedSemanticGroups.has(groupIdentity)) {
4177
+ for (const existingName of Object.keys(merged)) {
4178
+ if (precedenceGroup.some((candidate) => normalizeConfigKey(candidate) === normalizeConfigKey(existingName))) {
4179
+ delete merged[existingName];
4180
+ }
4181
+ }
4182
+ clearedSemanticGroups.add(groupIdentity);
4183
+ }
4184
+ merged[canonicalName] = lowerPriorityMap && isRecord(value)
4185
+ ? { ...lowerPriorityMap, ...value }
4186
+ : isRecord(value)
4187
+ ? { ...value }
4188
+ : value;
4189
+ }
4190
+ return merged;
4191
+ }
4192
+ function applyMissingExpandedConfigOptions(kind, target, explicitOptions, configured) {
4193
+ for (const [canonicalName, value] of Object.entries(configured)) {
4194
+ if (hasExplicitExpandedOption(kind, explicitOptions, canonicalName)) {
4195
+ continue;
4196
+ }
4197
+ target[canonicalName] = isRecord(value) ? { ...value } : value;
4198
+ }
4199
+ }
4200
+ function hasExplicitExpandedOption(kind, options, canonicalName) {
4201
+ const aliases = expandedOptionPrecedenceGroup(kind, canonicalName);
4202
+ return Object.entries(options).some(([key, value]) => value !== undefined && aliases.some((alias) => normalizeConfigKey(alias) === normalizeConfigKey(key)));
4203
+ }
4204
+ function expandedOptionPrecedenceGroup(kind, canonicalName) {
4205
+ if (canonicalName === "TimeoutSeconds" || canonicalName === "TimeoutMs") {
4206
+ return ["TimeoutSeconds", "TimeoutMs"];
4207
+ }
4208
+ if ((canonicalName === "BaseUrl" || canonicalName === "EndpointUrl") && kind === "CloudWatch") {
4209
+ return ["BaseUrl", "EndpointUrl"];
4210
+ }
4211
+ if ((canonicalName === "BaseUrl" || canonicalName === "Url") && kind === "Webhook") {
4212
+ return ["BaseUrl", "Url"];
4213
+ }
4214
+ if ((canonicalName === "LicenseKey" || canonicalName === "ApiKey") && kind === "NewRelic") {
4215
+ return ["LicenseKey", "ApiKey"];
4216
+ }
4217
+ if ((canonicalName === "EndpointPath" || canonicalName === "IndexName") &&
4218
+ (kind === "Elasticsearch" || kind === "OpenSearch")) {
4219
+ return ["EndpointPath", "IndexName"];
4220
+ }
4221
+ return [canonicalName];
4222
+ }
4223
+ function prepareExpandedCredentialOptions(kind, options, sources) {
4224
+ let headers = normalizeStringMap(optionRecord(options, "headers", "Headers"));
4225
+ const headerName = expandedCredentialHeaderName(kind);
4226
+ const headerSourceRank = headerName
4227
+ ? expandedCredentialHeaderSourceRank(sources, headerName)
4228
+ : 0;
4229
+ const credentialSourceRank = expandedCredentialSourceRank(kind, sources);
4230
+ if (headerSourceRank > credentialSourceRank) {
4231
+ deleteExpandedCredentialOptions(kind, options);
4232
+ }
4233
+ const credentialMustReplaceHeader = credentialSourceRank > headerSourceRank;
4234
+ if (kind === "Elasticsearch") {
4235
+ const apiKey = optionString(options, "apiKey", "ApiKey").trim();
4236
+ if (apiKey && (credentialMustReplaceHeader || !hasCaseInsensitiveHeader(headers, "Authorization"))) {
4237
+ headers = mergeProtocolHeaders(headers, { Authorization: `ApiKey ${apiKey}` });
4238
+ }
4239
+ }
4240
+ else if (kind === "OpenSearch") {
4241
+ const username = optionString(options, "username", "Username").trim();
4242
+ const password = optionString(options, "password", "Password");
4243
+ if ((username || password) &&
4244
+ (credentialMustReplaceHeader || !hasCaseInsensitiveHeader(headers, "Authorization"))) {
4245
+ headers = mergeProtocolHeaders(headers, {
4246
+ Authorization: `Basic ${Buffer.from(`${username}:${password}`, "utf8").toString("base64")}`
4247
+ });
4248
+ }
4249
+ }
4250
+ else if (kind === "Webhook") {
4251
+ const secret = optionString(options, "secret", "Secret").trim();
4252
+ const url = optionString(options, "url", "Url").trim();
4253
+ if (url) {
4254
+ options.__loadstrikeExactUrl = url;
4255
+ }
4256
+ if (secret && (credentialMustReplaceHeader || !hasCaseInsensitiveHeader(headers, "Authorization"))) {
4257
+ headers = mergeProtocolHeaders(headers, { Authorization: `LoadStrike ${secret}` });
4258
+ }
4259
+ }
4260
+ if (Object.keys(headers).length > 0) {
4261
+ options.Headers = headers;
4262
+ delete options.headers;
4263
+ }
4264
+ }
4265
+ function expandedCredentialHeaderName(kind) {
4266
+ if (kind === "PrometheusRemoteWrite" || kind === "Dynatrace" ||
4267
+ kind === "Elasticsearch" || kind === "OpenSearch" || kind === "Webhook") {
4268
+ return "Authorization";
4269
+ }
4270
+ return kind === "NewRelic" ? "Api-Key" : undefined;
4271
+ }
4272
+ function expandedCredentialHeaderSourceRank(sources, headerName) {
4273
+ return expandedSourceRank(sources, (source) => {
4274
+ const headers = normalizeStringMap(optionRecord(source, "headers", "Headers"));
4275
+ return Object.entries(headers).some(([name, value]) => name.toLowerCase() === headerName.toLowerCase() && value.trim().length > 0);
4276
+ });
4277
+ }
4278
+ function expandedCredentialSourceRank(kind, sources) {
4279
+ return expandedSourceRank(sources, (source) => expandedSourceHasCredential(kind, source));
4280
+ }
4281
+ function expandedSourceRank(sources, predicate) {
4282
+ if (predicate(sources.explicit))
4283
+ return 3;
4284
+ if (predicate(sources.environment))
4285
+ return 2;
4286
+ if (predicate(sources.configuration))
4287
+ return 1;
4288
+ return 0;
4289
+ }
4290
+ function expandedSourceHasCredential(kind, source) {
4291
+ switch (kind) {
4292
+ case "PrometheusRemoteWrite":
4293
+ return optionString(source, "bearerToken", "BearerToken").trim().length > 0;
4294
+ case "Dynatrace":
4295
+ return optionString(source, "apiToken", "ApiToken").trim().length > 0;
4296
+ case "Elasticsearch":
4297
+ return optionString(source, "apiKey", "ApiKey").trim().length > 0;
4298
+ case "OpenSearch":
4299
+ return optionString(source, "username", "Username").trim().length > 0
4300
+ || optionString(source, "password", "Password").length > 0;
4301
+ case "NewRelic":
4302
+ return optionString(source, "licenseKey", "LicenseKey", "apiKey", "ApiKey").trim().length > 0;
4303
+ case "Webhook":
4304
+ return optionString(source, "secret", "Secret").trim().length > 0;
4305
+ default:
4306
+ return false;
4307
+ }
4308
+ }
4309
+ function deleteExpandedCredentialOptions(kind, options) {
4310
+ const names = (() => {
4311
+ switch (kind) {
4312
+ case "PrometheusRemoteWrite": return ["BearerToken"];
4313
+ case "Dynatrace": return ["ApiToken"];
4314
+ case "Elasticsearch": return ["ApiKey"];
4315
+ case "OpenSearch": return ["Username", "Password"];
4316
+ case "NewRelic": return ["LicenseKey", "ApiKey"];
4317
+ case "Webhook": return ["Secret"];
4318
+ default: return [];
4319
+ }
4320
+ })();
4321
+ for (const key of Object.keys(options)) {
4322
+ if (names.some((name) => normalizeConfigKey(name) === normalizeConfigKey(key))) {
4323
+ delete options[key];
4324
+ }
4325
+ }
4326
+ }
4327
+ function validateBoundExpandedConfig(kind, options, sectionPath) {
4328
+ const requireString = (name, ...aliases) => {
4329
+ const value = optionString(options, name, ...aliases).trim();
4330
+ if (!value) {
4331
+ throw new Error(`${sectionPath}:${name} is required.`);
4332
+ }
4333
+ return value;
4334
+ };
4335
+ const validateUrl = (name, value) => {
4336
+ try {
4337
+ const parsed = new URL(value);
4338
+ if ((parsed.protocol !== "http:" && parsed.protocol !== "https:") ||
4339
+ !parsed.hostname || parsed.username || parsed.password) {
4340
+ throw new Error("invalid");
4341
+ }
4342
+ }
4343
+ catch {
4344
+ throw new Error(`${sectionPath}:${name} must be an absolute HTTP or HTTPS URL.`);
4345
+ }
4346
+ };
4347
+ if (kind === "PrometheusRemoteWrite" || kind === "Dynatrace" ||
4348
+ kind === "Elasticsearch" || kind === "OpenSearch" || kind === "NewRelic") {
4349
+ validateUrl("BaseUrl", requireString("BaseUrl"));
4350
+ }
4351
+ if (kind === "CloudWatch") {
4352
+ const endpointPath = optionString(options, "endpointPath", "EndpointPath").trim();
4353
+ if (endpointPath && normalizePath(endpointPath) !== "/") {
4354
+ throw new Error(`${sectionPath}:EndpointPath is not supported for CloudWatch; `
4355
+ + "include any required path in EndpointUrl.");
4356
+ }
4357
+ const endpoint = optionString(options, "endpointUrl", "EndpointUrl", "baseUrl", "BaseUrl").trim();
4358
+ if (endpoint) {
4359
+ const endpointName = optionString(options, "endpointUrl", "EndpointUrl").trim()
4360
+ ? "EndpointUrl"
4361
+ : "BaseUrl";
4362
+ validateUrl(endpointName, endpoint);
4363
+ }
4364
+ requireString("Region");
4365
+ requireString("AccessKeyId");
4366
+ requireString("SecretAccessKey");
4367
+ }
4368
+ if (kind === "Dynatrace" &&
4369
+ !optionString(options, "apiToken", "ApiToken").trim() &&
4370
+ !hasCaseInsensitiveHeader(normalizeStringMap(optionRecord(options, "headers", "Headers")), "Authorization")) {
4371
+ throw new Error(`${sectionPath}:ApiToken is required.`);
4372
+ }
4373
+ if (kind === "NewRelic" &&
4374
+ !optionString(options, "licenseKey", "LicenseKey", "apiKey", "ApiKey").trim() &&
4375
+ !hasCaseInsensitiveHeader(normalizeStringMap(optionRecord(options, "headers", "Headers")), "Api-Key")) {
4376
+ throw new Error(`${sectionPath}:LicenseKey is required.`);
4377
+ }
4378
+ if (kind === "Elasticsearch" || kind === "OpenSearch") {
4379
+ const endpointPath = optionString(options, "endpointPath", "EndpointPath").trim();
4380
+ if (!endpointPath) {
4381
+ const rawIndexName = pickRecordValue(options, "indexName", "IndexName");
4382
+ const indexName = rawIndexName === undefined
4383
+ ? "loadstrike-events"
4384
+ : typeof rawIndexName === "string"
4385
+ ? rawIndexName.trim()
4386
+ : "";
4387
+ requireValidSearchIndexName(indexName, `${sectionPath}:IndexName`);
4388
+ }
4389
+ }
4390
+ if (kind === "OpenSearch") {
4391
+ const username = optionString(options, "username", "Username").trim();
4392
+ const password = optionString(options, "password", "Password");
4393
+ if (username && !password) {
4394
+ throw new Error(`${sectionPath}:Password is required when Username is configured.`);
4395
+ }
4396
+ if (password && !username) {
4397
+ throw new Error(`${sectionPath}:Username is required when Password is configured.`);
4398
+ }
4399
+ }
4400
+ if (kind === "Webhook") {
4401
+ const url = optionString(options, "url", "Url").trim();
4402
+ if (url) {
4403
+ validateUrl("Url", url);
4404
+ }
4405
+ else {
4406
+ validateUrl("BaseUrl", requireString("BaseUrl"));
4407
+ }
4408
+ }
4409
+ if (kind === "StatsD" || kind === "Netdata" || kind === "DogStatsD") {
4410
+ const configuredPort = pickRecordValue(options, "port", "Port");
4411
+ if (configuredPort !== undefined) {
4412
+ const port = Number(configuredPort);
4413
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
4414
+ throw new Error(`${sectionPath}:Port must be an integer between 1 and 65535.`);
4415
+ }
4416
+ }
4417
+ }
4418
+ }
3202
4419
  function resolveConfigSection(source, path) {
3203
4420
  const trimmedPath = String(path ?? "").trim();
3204
4421
  if (!trimmedPath) {
@@ -3323,6 +4540,73 @@ function resolveTimeoutMs(timeoutSeconds, timeoutMs) {
3323
4540
  }
3324
4541
  return 30000;
3325
4542
  }
4543
+ function reportingIntervalMilliseconds(seconds) {
4544
+ if (seconds === undefined) {
4545
+ return undefined;
4546
+ }
4547
+ if (!Number.isFinite(seconds) || seconds <= 0) {
4548
+ throw new Error("ReportingIntervalSeconds must be a positive finite number.");
4549
+ }
4550
+ const milliseconds = Math.round(seconds * 1000);
4551
+ if (!Number.isSafeInteger(milliseconds) || milliseconds <= 0) {
4552
+ throw new Error("ReportingIntervalSeconds is outside the supported range.");
4553
+ }
4554
+ return milliseconds;
4555
+ }
4556
+ function positiveIntervalMilliseconds(previous, current) {
4557
+ if (!previous || !Number.isFinite(previous.getTime()) || !Number.isFinite(current.getTime())) {
4558
+ return 1;
4559
+ }
4560
+ return Math.max(Math.round(current.getTime() - previous.getTime()), 1);
4561
+ }
4562
+ function requireHttpReportingUrl(value, fieldName) {
4563
+ let parsed;
4564
+ try {
4565
+ parsed = new URL(value);
4566
+ }
4567
+ catch {
4568
+ throw new Error(`${fieldName} must be an absolute HTTP or HTTPS URL.`);
4569
+ }
4570
+ if ((parsed.protocol !== "http:" && parsed.protocol !== "https:") || !parsed.hostname) {
4571
+ throw new Error(`${fieldName} must be an absolute HTTP or HTTPS URL.`);
4572
+ }
4573
+ if (parsed.username || parsed.password) {
4574
+ throw new Error(`${fieldName} must not contain credentials.`);
4575
+ }
4576
+ }
4577
+ function hasCaseInsensitiveHeader(headers, expectedName) {
4578
+ const expected = expectedName.toLowerCase();
4579
+ return Object.entries(headers).some(([name, value]) => name.toLowerCase() === expected && value.trim().length > 0);
4580
+ }
4581
+ function normalizeProtocolHeaders(headers) {
4582
+ return mergeProtocolHeaders({}, headers);
4583
+ }
4584
+ function mergeProtocolHeaders(headers, requiredHeaders) {
4585
+ const merged = {};
4586
+ const namesByLowerCase = new Map();
4587
+ for (const source of [headers, requiredHeaders]) {
4588
+ for (const [name, value] of Object.entries(source)) {
4589
+ const normalized = name.toLowerCase();
4590
+ const previousName = namesByLowerCase.get(normalized);
4591
+ if (previousName !== undefined) {
4592
+ delete merged[previousName];
4593
+ }
4594
+ merged[name] = String(value);
4595
+ namesByLowerCase.set(normalized, name);
4596
+ }
4597
+ }
4598
+ return merged;
4599
+ }
4600
+ function isCloudWatchProtocolHeader(name) {
4601
+ const normalized = name.toLowerCase();
4602
+ return normalized === "authorization"
4603
+ || normalized === "host"
4604
+ || normalized === "content-type"
4605
+ || normalized === "content-length"
4606
+ || normalized === "user-agent"
4607
+ || normalized.startsWith("x-amz-")
4608
+ || normalized.startsWith("amz-sdk-");
4609
+ }
3326
4610
  function toUnixNanoseconds(value) {
3327
4611
  return (BigInt(value.getTime()) * 1000000n).toString();
3328
4612
  }
@@ -3565,7 +4849,7 @@ function deepCloneValue(value) {
3565
4849
  function gzipJsonRequestBody(value) {
3566
4850
  return Uint8Array.from(serializeIterationObservationBatchGzipJson(value)).buffer;
3567
4851
  }
3568
- async function postWithTimeout(fetchImpl, url, init, timeoutMs, sinkName) {
4852
+ async function postWithTimeout(fetchImpl, url, init, timeoutMs, sinkName, ignoreSuccessfulResponseBody = false) {
3569
4853
  const controller = new AbortController();
3570
4854
  const timer = setTimeout(() => controller.abort(), Math.max(timeoutMs, 1));
3571
4855
  try {
@@ -3573,12 +4857,27 @@ async function postWithTimeout(fetchImpl, url, init, timeoutMs, sinkName) {
3573
4857
  if (!response.ok) {
3574
4858
  throw new ReportingSinkHttpError(sinkName, response.status, readHttpResponseStatusText(response), readHttpResponseRequestId(response));
3575
4859
  }
4860
+ if (ignoreSuccessfulResponseBody) {
4861
+ disposeResponseBody(response);
4862
+ return "";
4863
+ }
3576
4864
  return await readResponseBodyText(response);
3577
4865
  }
3578
4866
  finally {
3579
4867
  clearTimeout(timer);
3580
4868
  }
3581
4869
  }
4870
+ function disposeResponseBody(response) {
4871
+ try {
4872
+ const cancellation = response.body?.cancel();
4873
+ if (cancellation && typeof cancellation.catch === "function") {
4874
+ void cancellation.catch(() => undefined);
4875
+ }
4876
+ }
4877
+ catch {
4878
+ // A successful write must not be reclassified as failed because its response body cannot be consumed.
4879
+ }
4880
+ }
3582
4881
  async function readResponseBodyText(response) {
3583
4882
  const body = response.body;
3584
4883
  if (body && typeof body.getReader === "function") {