@loadstrike/loadstrike-sdk 1.0.31601 → 1.0.33601
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -2
- package/dist/cjs/internal/prometheus-remote-write.js +37 -0
- package/dist/cjs/internal/reporting-sink-http-error.js +17 -0
- package/dist/cjs/internal/vendor-metric-payloads.js +390 -0
- package/dist/cjs/iteration-observations.js +24 -8
- package/dist/cjs/local.js +48 -63
- package/dist/cjs/reporting-containment.js +242 -0
- package/dist/cjs/runtime.js +83 -3
- package/dist/cjs/sinks.js +1337 -38
- package/dist/cjs/transports.js +1339 -151
- package/dist/esm/internal/prometheus-remote-write.js +31 -0
- package/dist/esm/internal/reporting-sink-http-error.js +13 -0
- package/dist/esm/internal/vendor-metric-payloads.js +382 -0
- package/dist/esm/iteration-observations.js +24 -8
- package/dist/esm/local.js +49 -64
- package/dist/esm/reporting-containment.js +238 -0
- package/dist/esm/runtime.js +85 -5
- package/dist/esm/sinks.js +1334 -35
- package/dist/esm/transports.js +1335 -151
- package/dist/types/contracts.d.ts +1 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/types/internal/prometheus-remote-write.d.ts +2 -0
- package/dist/types/internal/reporting-sink-http-error.d.ts +6 -0
- package/dist/types/internal/vendor-metric-payloads.d.ts +48 -0
- package/dist/types/local.d.ts +0 -6
- package/dist/types/reporting-containment.d.ts +2 -0
- package/dist/types/runtime.d.ts +1 -0
- package/dist/types/sinks.d.ts +134 -17
- package/dist/types/transports.d.ts +2 -0
- package/package.json +9 -3
- package/dist/cjs/internal-build.js +0 -4
- package/dist/esm/internal-build.js +0 -1
- 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
|
-
|
|
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,
|
|
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,
|
|
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
|
|
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.
|
|
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
|
|
1971
|
+
export class DynatraceReportingSink extends DirectVendorMetricReportingSink {
|
|
1684
1972
|
constructor(options = {}) {
|
|
1685
|
-
super("
|
|
1686
|
-
this.
|
|
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
|
|
2001
|
+
export class NewRelicReportingSink extends DirectVendorMetricReportingSink {
|
|
1690
2002
|
constructor(options = {}) {
|
|
1691
|
-
super("
|
|
1692
|
-
this.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
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") {
|