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