@loadstrike/loadstrike-sdk 1.0.31001 → 1.0.32601
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -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-report-input.js +21 -0
- package/dist/cjs/local.js +48 -63
- package/dist/cjs/report-history.js +421 -0
- package/dist/cjs/reporting-containment.js +242 -0
- package/dist/cjs/reporting-svg.js +116 -0
- package/dist/cjs/reporting.js +413 -136
- package/dist/cjs/runtime.js +237 -8
- 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-report-input.js +17 -0
- package/dist/esm/local.js +49 -64
- package/dist/esm/report-history.js +413 -0
- package/dist/esm/reporting-containment.js +238 -0
- package/dist/esm/reporting-svg.js +113 -0
- package/dist/esm/reporting.js +413 -136
- package/dist/esm/runtime.js +239 -10
- 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-report-input.d.ts +6 -0
- package/dist/types/local.d.ts +0 -6
- package/dist/types/report-history.d.ts +124 -0
- package/dist/types/reporting-containment.d.ts +2 -0
- package/dist/types/reporting-svg.d.ts +2 -0
- package/dist/types/reporting.d.ts +6 -3
- package/dist/types/runtime.d.ts +24 -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
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import protobuf from "protobufjs";
|
|
2
|
+
import { prepareProtocolMetricPoints } from "./vendor-metric-payloads.js";
|
|
3
|
+
const PrometheusLabel = new protobuf.Type("Label")
|
|
4
|
+
.add(new protobuf.Field("name", 1, "string"))
|
|
5
|
+
.add(new protobuf.Field("value", 2, "string"));
|
|
6
|
+
const PrometheusSample = new protobuf.Type("Sample")
|
|
7
|
+
.add(new protobuf.Field("value", 1, "double"))
|
|
8
|
+
.add(new protobuf.Field("timestamp", 2, "int64"));
|
|
9
|
+
const PrometheusTimeSeries = new protobuf.Type("TimeSeries")
|
|
10
|
+
.add(new protobuf.Field("labels", 1, "Label", "repeated"))
|
|
11
|
+
.add(new protobuf.Field("samples", 2, "Sample", "repeated"))
|
|
12
|
+
.add(PrometheusLabel)
|
|
13
|
+
.add(PrometheusSample);
|
|
14
|
+
const PrometheusWriteRequest = new protobuf.Type("WriteRequest")
|
|
15
|
+
.add(new protobuf.Field("timeseries", 1, "TimeSeries", "repeated"))
|
|
16
|
+
.add(PrometheusTimeSeries);
|
|
17
|
+
export function encodePrometheusRemoteWrite(points, staticLabels = {}) {
|
|
18
|
+
const prepared = prepareProtocolMetricPoints("prometheus", points, staticLabels);
|
|
19
|
+
const timeseries = prepared.map((point) => {
|
|
20
|
+
const labels = [["__name__", point.metricName], ...point.attributes];
|
|
21
|
+
labels.sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0);
|
|
22
|
+
return {
|
|
23
|
+
labels: labels.map(([name, value]) => ({ name, value })),
|
|
24
|
+
samples: [{ value: point.value, timestamp: point.occurredUtc.getTime() }]
|
|
25
|
+
};
|
|
26
|
+
});
|
|
27
|
+
return {
|
|
28
|
+
contentType: "application/x-protobuf",
|
|
29
|
+
body: PrometheusWriteRequest.encode({ timeseries }).finish()
|
|
30
|
+
};
|
|
31
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export class ReportingSinkHttpError extends Error {
|
|
2
|
+
constructor(sinkName, status, statusText, requestId) {
|
|
3
|
+
const normalizedStatus = Number.isFinite(status) ? Math.max(Math.trunc(status), 0) : 0;
|
|
4
|
+
super(`${sinkName} write failed with HTTP status ${normalizedStatus}`
|
|
5
|
+
+ (statusText ? ` ${statusText}` : "")
|
|
6
|
+
+ (requestId ? ` (requestId=${requestId})` : "")
|
|
7
|
+
+ ".");
|
|
8
|
+
this.name = "ReportingSinkHttpError";
|
|
9
|
+
this.status = normalizedStatus;
|
|
10
|
+
this.statusText = statusText;
|
|
11
|
+
this.requestId = requestId;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
const MAXIMUM_METRIC_NAME_BYTES = 200;
|
|
2
|
+
const MAXIMUM_ATTRIBUTE_KEY_BYTES = 64;
|
|
3
|
+
const MAXIMUM_ATTRIBUTE_VALUE_BYTES = 256;
|
|
4
|
+
const MAXIMUM_DYNATRACE_ATTRIBUTE_VALUE_BYTES = 255;
|
|
5
|
+
const MAXIMUM_ATTRIBUTES = 20;
|
|
6
|
+
const MAXIMUM_ABSOLUTE_VALUE = 1e100;
|
|
7
|
+
const MAXIMUM_SIGNED_INT64 = 9223372036854775807n;
|
|
8
|
+
const RESERVED_ATTRIBUTES = new Set(["scenario", "status", "run_id"]);
|
|
9
|
+
export function createCloudWatchMetricData(namespace, points, staticDimensions = {}) {
|
|
10
|
+
const normalizedNamespace = normalizeCloudWatchNamespace(namespace);
|
|
11
|
+
const prepared = prepareProtocolMetricPoints("cloudwatch", points, staticDimensions);
|
|
12
|
+
return {
|
|
13
|
+
Namespace: normalizedNamespace,
|
|
14
|
+
MetricData: prepared.map((point) => {
|
|
15
|
+
const unit = cloudWatchUnit(point.metricKind, point.unitOfMeasure);
|
|
16
|
+
return {
|
|
17
|
+
MetricName: point.metricName,
|
|
18
|
+
Dimensions: point.attributes.map(([Name, Value]) => ({ Name, Value })),
|
|
19
|
+
Value: point.value,
|
|
20
|
+
...(unit ? { Unit: unit } : {}),
|
|
21
|
+
Timestamp: new Date(point.occurredUtc.getTime())
|
|
22
|
+
};
|
|
23
|
+
})
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
export function encodeDynatraceMetrics(points, staticDimensions = {}) {
|
|
27
|
+
const prepared = prepareProtocolMetricPoints("dynatrace", points, staticDimensions);
|
|
28
|
+
return encodeDynatracePreparedMetrics(prepared);
|
|
29
|
+
}
|
|
30
|
+
export function encodeDynatraceMetricBatches(points, staticDimensions = {}, maximumBodyBytesExclusive = 1000000) {
|
|
31
|
+
const prepared = prepareProtocolMetricPoints("dynatrace", points, staticDimensions);
|
|
32
|
+
return encodeTextBatches("dynatrace", "text/plain; charset=utf-8", prepared.map((point) => dynatraceMetricLine(point)), "", "", "\n", maximumBodyBytesExclusive);
|
|
33
|
+
}
|
|
34
|
+
export function encodeNewRelicMetricBatch(points, staticAttributes = {}) {
|
|
35
|
+
const prepared = prepareProtocolMetricPoints("newrelic", points, staticAttributes);
|
|
36
|
+
return encodeNewRelicPreparedMetrics(prepared);
|
|
37
|
+
}
|
|
38
|
+
export function encodeNewRelicMetricBatches(points, staticAttributes = {}, maximumBodyBytesExclusive = 1000000) {
|
|
39
|
+
const prepared = prepareProtocolMetricPoints("newrelic", points, staticAttributes);
|
|
40
|
+
return encodeTextBatches("newrelic", "application/json", prepared.map((point) => newRelicMetricJson(point)), '[{"metrics":[', "]}]", ",", maximumBodyBytesExclusive);
|
|
41
|
+
}
|
|
42
|
+
export function prepareProtocolMetricPoints(sink, points, staticAttributes = {}) {
|
|
43
|
+
if (!Array.isArray(points) || points.length === 0) {
|
|
44
|
+
throw protocolError(sink, "metric batch");
|
|
45
|
+
}
|
|
46
|
+
if (!isPlainRecord(staticAttributes)) {
|
|
47
|
+
throw protocolError(sink, "attribute collection");
|
|
48
|
+
}
|
|
49
|
+
// Validate and copy every point before any protocol bytes or DTOs are emitted.
|
|
50
|
+
const prepared = [];
|
|
51
|
+
for (let index = 0; index < points.length; index += 1) {
|
|
52
|
+
prepared.push(prepareProtocolMetricPoint(sink, points[index], staticAttributes));
|
|
53
|
+
}
|
|
54
|
+
return prepared;
|
|
55
|
+
}
|
|
56
|
+
function prepareProtocolMetricPoint(sink, point, staticAttributes) {
|
|
57
|
+
if (!point || typeof point !== "object") {
|
|
58
|
+
throw protocolError(sink, "metric point");
|
|
59
|
+
}
|
|
60
|
+
const metricKind = normalizeMetricKind(sink, point.metricKind);
|
|
61
|
+
if (!(point.occurredUtc instanceof Date) || !Number.isFinite(point.occurredUtc.getTime())) {
|
|
62
|
+
throw protocolError(sink, "timestamp");
|
|
63
|
+
}
|
|
64
|
+
if (!Number.isFinite(point.value) || point.value < -MAXIMUM_ABSOLUTE_VALUE || point.value > MAXIMUM_ABSOLUTE_VALUE) {
|
|
65
|
+
throw protocolError(sink, "value");
|
|
66
|
+
}
|
|
67
|
+
const intervalMs = metricKind === "count"
|
|
68
|
+
? normalizePositiveInt64(sink, point.intervalMs)
|
|
69
|
+
: undefined;
|
|
70
|
+
if (metricKind === "gauge" && point.intervalMs !== undefined) {
|
|
71
|
+
throw protocolError(sink, "interval");
|
|
72
|
+
}
|
|
73
|
+
const rawUnitOfMeasure = point.unitOfMeasure;
|
|
74
|
+
let unitOfMeasure;
|
|
75
|
+
if (rawUnitOfMeasure !== undefined && rawUnitOfMeasure !== null) {
|
|
76
|
+
if (typeof rawUnitOfMeasure !== "string" || !isWellFormedUnicode(rawUnitOfMeasure)) {
|
|
77
|
+
throw protocolError(sink, "unit of measure");
|
|
78
|
+
}
|
|
79
|
+
unitOfMeasure = rawUnitOfMeasure;
|
|
80
|
+
}
|
|
81
|
+
const metricName = normalizeMetricName(sink, point.metricName);
|
|
82
|
+
const authoritative = [
|
|
83
|
+
["scenario", validateAuthoritativeValue(sink, "scenario", point.scenario)],
|
|
84
|
+
["status", validateAuthoritativeValue(sink, "status", point.status)]
|
|
85
|
+
];
|
|
86
|
+
if (point.runId !== undefined) {
|
|
87
|
+
authoritative.push(["run_id", validateAuthoritativeValue(sink, "run_id", point.runId)]);
|
|
88
|
+
}
|
|
89
|
+
const emptyValuesAllowed = sink === "prometheus" || sink === "newrelic";
|
|
90
|
+
const normalizedStatic = new Map();
|
|
91
|
+
for (const [rawKey, rawValue] of Object.entries(staticAttributes)) {
|
|
92
|
+
const key = normalizeAttributeKey(sink, rawKey);
|
|
93
|
+
if (RESERVED_ATTRIBUTES.has(key)) {
|
|
94
|
+
throw protocolError(sink, key);
|
|
95
|
+
}
|
|
96
|
+
if (sink === "prometheus" && key === "__name__") {
|
|
97
|
+
throw protocolError(sink, "attribute key");
|
|
98
|
+
}
|
|
99
|
+
if (normalizedStatic.has(key)) {
|
|
100
|
+
throw protocolError(sink, "attribute key collision");
|
|
101
|
+
}
|
|
102
|
+
if (typeof rawValue !== "string" || !isWellFormedUnicode(rawValue) ||
|
|
103
|
+
(!emptyValuesAllowed && rawValue.length === 0) ||
|
|
104
|
+
utf8Length(rawValue) > maximumAttributeValueBytes(sink) ||
|
|
105
|
+
(sink === "dynatrace" && /[\r\n]/.test(rawValue)) ||
|
|
106
|
+
(sink === "cloudwatch" && !isCloudWatchDimensionValue(rawValue))) {
|
|
107
|
+
throw protocolError(sink, "attribute value");
|
|
108
|
+
}
|
|
109
|
+
normalizedStatic.set(key, rawValue);
|
|
110
|
+
}
|
|
111
|
+
if (authoritative.length + normalizedStatic.size > MAXIMUM_ATTRIBUTES) {
|
|
112
|
+
throw protocolError(sink, "attribute count");
|
|
113
|
+
}
|
|
114
|
+
const attributes = [...authoritative, ...normalizedStatic.entries()]
|
|
115
|
+
.sort(([left], [right]) => compareOrdinal(left, right));
|
|
116
|
+
return {
|
|
117
|
+
metricName,
|
|
118
|
+
metricKind,
|
|
119
|
+
occurredUtc: new Date(point.occurredUtc.getTime()),
|
|
120
|
+
value: point.value,
|
|
121
|
+
unitOfMeasure,
|
|
122
|
+
intervalMs,
|
|
123
|
+
attributes
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
function encodeDynatracePreparedMetrics(prepared) {
|
|
127
|
+
return {
|
|
128
|
+
contentType: "text/plain; charset=utf-8",
|
|
129
|
+
body: new TextEncoder().encode(prepared.map((point) => dynatraceMetricLine(point)).join("\n"))
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
function dynatraceMetricLine(point) {
|
|
133
|
+
const dimensions = point.attributes
|
|
134
|
+
.map(([key, value]) => `${key}="${escapeDynatraceDimensionValue(value)}"`)
|
|
135
|
+
.join(",");
|
|
136
|
+
const identity = dimensions.length > 0 ? `${point.metricName},${dimensions}` : point.metricName;
|
|
137
|
+
const payload = point.metricKind === "count"
|
|
138
|
+
? `count,delta=${formatFiniteNumber(point.value)}`
|
|
139
|
+
: `gauge,${formatFiniteNumber(point.value)}`;
|
|
140
|
+
return `${identity} ${payload} ${point.occurredUtc.getTime()}`;
|
|
141
|
+
}
|
|
142
|
+
function encodeNewRelicPreparedMetrics(prepared) {
|
|
143
|
+
return {
|
|
144
|
+
contentType: "application/json",
|
|
145
|
+
body: new TextEncoder().encode(`[{"metrics":[${prepared.map((point) => newRelicMetricJson(point)).join(",")}]}]`)
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
function newRelicMetricJson(point) {
|
|
149
|
+
const properties = [
|
|
150
|
+
`"name":${JSON.stringify(point.metricName)}`,
|
|
151
|
+
`"type":${JSON.stringify(point.metricKind)}`,
|
|
152
|
+
`"value":${JSON.stringify(point.value)}`,
|
|
153
|
+
`"timestamp":${point.occurredUtc.getTime()}`
|
|
154
|
+
];
|
|
155
|
+
if (point.metricKind === "count") {
|
|
156
|
+
if (point.intervalMs === undefined) {
|
|
157
|
+
throw protocolError("newrelic", "interval");
|
|
158
|
+
}
|
|
159
|
+
properties.push(`"interval.ms":${point.intervalMs.toString()}`);
|
|
160
|
+
}
|
|
161
|
+
properties.push(`"attributes":${JSON.stringify(Object.fromEntries(point.attributes))}`);
|
|
162
|
+
return `{${properties.join(",")}}`;
|
|
163
|
+
}
|
|
164
|
+
function encodeTextBatches(sink, contentType, items, prefix, suffix, separator, maximumBodyBytesExclusive) {
|
|
165
|
+
if (!Number.isSafeInteger(maximumBodyBytesExclusive) || maximumBodyBytesExclusive <= 0) {
|
|
166
|
+
throw protocolError(sink, "payload size limit");
|
|
167
|
+
}
|
|
168
|
+
const prefixBytes = utf8Length(prefix);
|
|
169
|
+
const suffixBytes = utf8Length(suffix);
|
|
170
|
+
const separatorBytes = utf8Length(separator);
|
|
171
|
+
const batches = [];
|
|
172
|
+
let current = [];
|
|
173
|
+
let currentBytes = prefixBytes + suffixBytes;
|
|
174
|
+
for (const item of items) {
|
|
175
|
+
const itemBytes = utf8Length(item);
|
|
176
|
+
if (prefixBytes + itemBytes + suffixBytes >= maximumBodyBytesExclusive) {
|
|
177
|
+
throw protocolError(sink, "payload size");
|
|
178
|
+
}
|
|
179
|
+
const candidateBytes = currentBytes + (current.length > 0 ? separatorBytes : 0) + itemBytes;
|
|
180
|
+
if (candidateBytes >= maximumBodyBytesExclusive) {
|
|
181
|
+
batches.push(current);
|
|
182
|
+
current = [item];
|
|
183
|
+
currentBytes = prefixBytes + itemBytes + suffixBytes;
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
current.push(item);
|
|
187
|
+
currentBytes = candidateBytes;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (current.length > 0) {
|
|
191
|
+
batches.push(current);
|
|
192
|
+
}
|
|
193
|
+
const encoder = new TextEncoder();
|
|
194
|
+
return batches.map((batch) => ({
|
|
195
|
+
contentType,
|
|
196
|
+
body: encoder.encode(`${prefix}${batch.join(separator)}${suffix}`)
|
|
197
|
+
}));
|
|
198
|
+
}
|
|
199
|
+
function normalizePositiveInt64(sink, value) {
|
|
200
|
+
let normalized;
|
|
201
|
+
if (typeof value === "bigint") {
|
|
202
|
+
normalized = value;
|
|
203
|
+
}
|
|
204
|
+
else if (typeof value === "number" && Number.isSafeInteger(value)) {
|
|
205
|
+
normalized = BigInt(value);
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
throw protocolError(sink, "interval");
|
|
209
|
+
}
|
|
210
|
+
if (normalized <= 0n || normalized > MAXIMUM_SIGNED_INT64) {
|
|
211
|
+
throw protocolError(sink, "interval");
|
|
212
|
+
}
|
|
213
|
+
return normalized;
|
|
214
|
+
}
|
|
215
|
+
function normalizeMetricName(sink, value) {
|
|
216
|
+
if (typeof value !== "string" || !isWellFormedUnicode(value) || value.trim().length === 0) {
|
|
217
|
+
throw protocolError(sink, "metric name");
|
|
218
|
+
}
|
|
219
|
+
let normalized = sink === "prometheus"
|
|
220
|
+
? normalizeAsciiIdentifier(value.trim(), true)
|
|
221
|
+
: sink === "dynatrace"
|
|
222
|
+
? normalizeAsciiIdentifier(value.trim(), false)
|
|
223
|
+
: value.trim();
|
|
224
|
+
if (sink === "dynatrace" && utf8Length(normalized) < 3) {
|
|
225
|
+
normalized = `ls_${normalized}`;
|
|
226
|
+
}
|
|
227
|
+
if (normalized.length === 0 || utf8Length(normalized) > MAXIMUM_METRIC_NAME_BYTES) {
|
|
228
|
+
throw protocolError(sink, "metric name");
|
|
229
|
+
}
|
|
230
|
+
return normalized;
|
|
231
|
+
}
|
|
232
|
+
function normalizeMetricKind(sink, value) {
|
|
233
|
+
if (typeof value !== "string" || !isWellFormedUnicode(value)) {
|
|
234
|
+
throw protocolError(sink, "metric kind");
|
|
235
|
+
}
|
|
236
|
+
let normalized = "";
|
|
237
|
+
for (const character of value.trim()) {
|
|
238
|
+
normalized += character >= "A" && character <= "Z"
|
|
239
|
+
? String.fromCharCode(character.charCodeAt(0) + 0x20)
|
|
240
|
+
: character;
|
|
241
|
+
}
|
|
242
|
+
if (normalized !== "count" && normalized !== "gauge") {
|
|
243
|
+
throw protocolError(sink, "metric kind");
|
|
244
|
+
}
|
|
245
|
+
return normalized;
|
|
246
|
+
}
|
|
247
|
+
function normalizeAttributeKey(sink, value) {
|
|
248
|
+
if (typeof value !== "string" || !isWellFormedUnicode(value) || value.trim().length === 0) {
|
|
249
|
+
throw protocolError(sink, "attribute key");
|
|
250
|
+
}
|
|
251
|
+
if (sink === "cloudwatch" && !isPrintableASCII(value)) {
|
|
252
|
+
throw protocolError(sink, "attribute key");
|
|
253
|
+
}
|
|
254
|
+
let normalized;
|
|
255
|
+
if (sink === "prometheus")
|
|
256
|
+
normalized = normalizeAsciiIdentifier(value.trim(), false);
|
|
257
|
+
else if (sink === "dynatrace")
|
|
258
|
+
normalized = normalizeAsciiIdentifier(value.trim(), false).toLowerCase();
|
|
259
|
+
else if (sink === "newrelic")
|
|
260
|
+
normalized = normalizeNewRelicAttributeKey(value.trim());
|
|
261
|
+
else
|
|
262
|
+
normalized = value.trim();
|
|
263
|
+
if (normalized.length === 0 || utf8Length(normalized) > MAXIMUM_ATTRIBUTE_KEY_BYTES) {
|
|
264
|
+
throw protocolError(sink, "attribute key");
|
|
265
|
+
}
|
|
266
|
+
if (sink === "cloudwatch" && !isCloudWatchDimensionName(normalized)) {
|
|
267
|
+
throw protocolError(sink, "attribute key");
|
|
268
|
+
}
|
|
269
|
+
return normalized;
|
|
270
|
+
}
|
|
271
|
+
function normalizeAsciiIdentifier(value, allowColon) {
|
|
272
|
+
let normalized = "";
|
|
273
|
+
for (const character of value) {
|
|
274
|
+
normalized += /[A-Za-z0-9_]/.test(character) || (allowColon && character === ":")
|
|
275
|
+
? character
|
|
276
|
+
: "_";
|
|
277
|
+
}
|
|
278
|
+
if (!/^[A-Za-z_]/.test(normalized) && !(allowColon && normalized.startsWith(":"))) {
|
|
279
|
+
normalized = `_${normalized}`;
|
|
280
|
+
}
|
|
281
|
+
return normalized;
|
|
282
|
+
}
|
|
283
|
+
function validateAuthoritativeValue(sink, field, value) {
|
|
284
|
+
if (typeof value !== "string" || !isWellFormedUnicode(value) ||
|
|
285
|
+
value.trim().length === 0 || utf8Length(value) > maximumAttributeValueBytes(sink) ||
|
|
286
|
+
(sink === "dynatrace" && /[\r\n]/.test(value)) ||
|
|
287
|
+
(sink === "cloudwatch" && !isCloudWatchDimensionValue(value))) {
|
|
288
|
+
throw protocolError(sink, field);
|
|
289
|
+
}
|
|
290
|
+
return value;
|
|
291
|
+
}
|
|
292
|
+
function normalizeCloudWatchNamespace(value) {
|
|
293
|
+
if (typeof value !== "string" || !isWellFormedUnicode(value)) {
|
|
294
|
+
throw protocolError("cloudwatch", "namespace");
|
|
295
|
+
}
|
|
296
|
+
const normalized = value.trim();
|
|
297
|
+
if (utf8Length(normalized) < 1 || utf8Length(normalized) > 100 || normalized.startsWith(":")) {
|
|
298
|
+
throw protocolError("cloudwatch", "namespace");
|
|
299
|
+
}
|
|
300
|
+
return normalized;
|
|
301
|
+
}
|
|
302
|
+
function cloudWatchUnit(metricKind, unit) {
|
|
303
|
+
if (metricKind === "count")
|
|
304
|
+
return "Count";
|
|
305
|
+
switch (unit?.trim().toLowerCase()) {
|
|
306
|
+
case "count":
|
|
307
|
+
case "counts": return "Count";
|
|
308
|
+
case "ms":
|
|
309
|
+
case "millisecond":
|
|
310
|
+
case "milliseconds": return "Milliseconds";
|
|
311
|
+
case "byte":
|
|
312
|
+
case "bytes": return "Bytes";
|
|
313
|
+
case "s":
|
|
314
|
+
case "sec":
|
|
315
|
+
case "seconds":
|
|
316
|
+
case "second": return "Seconds";
|
|
317
|
+
default: return "None";
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
function escapeDynatraceDimensionValue(value) {
|
|
321
|
+
return value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
|
|
322
|
+
}
|
|
323
|
+
function formatFiniteNumber(value) {
|
|
324
|
+
return Object.is(value, -0) ? "0" : String(value);
|
|
325
|
+
}
|
|
326
|
+
function utf8Length(value) {
|
|
327
|
+
return new TextEncoder().encode(value).byteLength;
|
|
328
|
+
}
|
|
329
|
+
function isWellFormedUnicode(value) {
|
|
330
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
331
|
+
const codeUnit = value.charCodeAt(index);
|
|
332
|
+
if (codeUnit >= 0xd800 && codeUnit <= 0xdbff) {
|
|
333
|
+
if (index + 1 >= value.length)
|
|
334
|
+
return false;
|
|
335
|
+
const next = value.charCodeAt(index + 1);
|
|
336
|
+
if (next < 0xdc00 || next > 0xdfff)
|
|
337
|
+
return false;
|
|
338
|
+
index += 1;
|
|
339
|
+
}
|
|
340
|
+
else if (codeUnit >= 0xdc00 && codeUnit <= 0xdfff) {
|
|
341
|
+
return false;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
return true;
|
|
345
|
+
}
|
|
346
|
+
function isPlainRecord(value) {
|
|
347
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
348
|
+
return false;
|
|
349
|
+
const prototype = Object.getPrototypeOf(value);
|
|
350
|
+
return prototype === Object.prototype || prototype === null;
|
|
351
|
+
}
|
|
352
|
+
function normalizeNewRelicAttributeKey(value) {
|
|
353
|
+
let normalized = "";
|
|
354
|
+
for (const character of value) {
|
|
355
|
+
normalized += /[A-Za-z0-9:._]/.test(character) ? character : "_";
|
|
356
|
+
}
|
|
357
|
+
return normalized;
|
|
358
|
+
}
|
|
359
|
+
function maximumAttributeValueBytes(sink) {
|
|
360
|
+
return sink === "dynatrace" ? MAXIMUM_DYNATRACE_ATTRIBUTE_VALUE_BYTES : MAXIMUM_ATTRIBUTE_VALUE_BYTES;
|
|
361
|
+
}
|
|
362
|
+
function isCloudWatchDimensionName(value) {
|
|
363
|
+
return !value.startsWith(":") && isPrintableASCII(value) && value.trim().length > 0;
|
|
364
|
+
}
|
|
365
|
+
function isCloudWatchDimensionValue(value) {
|
|
366
|
+
return isPrintableASCII(value) && value.trim().length > 0;
|
|
367
|
+
}
|
|
368
|
+
function isPrintableASCII(value) {
|
|
369
|
+
for (const character of value) {
|
|
370
|
+
const code = character.charCodeAt(0);
|
|
371
|
+
if (code < 0x20 || code > 0x7e)
|
|
372
|
+
return false;
|
|
373
|
+
}
|
|
374
|
+
return true;
|
|
375
|
+
}
|
|
376
|
+
function protocolError(sink, field) {
|
|
377
|
+
const displayName = sink === "newrelic" ? "New Relic" : sink === "cloudwatch" ? "CloudWatch" : sink;
|
|
378
|
+
return new Error(`${displayName} reporting metric has an invalid ${field}.`);
|
|
379
|
+
}
|
|
380
|
+
function compareOrdinal(left, right) {
|
|
381
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
382
|
+
}
|
|
@@ -144,7 +144,7 @@ export class ProcessIterationObservationBuffer {
|
|
|
144
144
|
}
|
|
145
145
|
const processIterationObservationBuffer = new ProcessIterationObservationBuffer();
|
|
146
146
|
class SinkDispatcher {
|
|
147
|
-
constructor(target, depth, parallelism, retryCount, retryBackoffMs, logger, runId, resultOwnerId, onDropped) {
|
|
147
|
+
constructor(target, depth, parallelism, retryCount, retryBackoffMs, logger, runId, resultOwnerId, retryableErrorClassifier, onDropped) {
|
|
148
148
|
this.target = target;
|
|
149
149
|
this.depth = depth;
|
|
150
150
|
this.parallelism = parallelism;
|
|
@@ -153,6 +153,7 @@ class SinkDispatcher {
|
|
|
153
153
|
this.logger = logger;
|
|
154
154
|
this.runId = runId;
|
|
155
155
|
this.resultOwnerId = resultOwnerId;
|
|
156
|
+
this.retryableErrorClassifier = retryableErrorClassifier;
|
|
156
157
|
this.onDropped = onDropped;
|
|
157
158
|
this.queue = [];
|
|
158
159
|
this.active = new Set();
|
|
@@ -265,12 +266,14 @@ class SinkDispatcher {
|
|
|
265
266
|
return "success";
|
|
266
267
|
}
|
|
267
268
|
catch (error) {
|
|
268
|
-
const
|
|
269
|
+
const retryable = this.isRetryableError(error);
|
|
270
|
+
const exhausted = attempt >= maximumAttempts || !retryable;
|
|
271
|
+
const reportedMaximumAttempts = retryable ? maximumAttempts : attempt;
|
|
269
272
|
const nextDelayMs = exhausted
|
|
270
273
|
? 0
|
|
271
274
|
: sinkRetryDelayMs(this.retryBackoffMs, attempt);
|
|
272
275
|
logIterationObservationFailure(this.logger, exhausted ? "error" : "warn", {
|
|
273
|
-
...this.diagnosticContext(item, attempt,
|
|
276
|
+
...this.diagnosticContext(item, attempt, reportedMaximumAttempts),
|
|
274
277
|
nextDelayMs
|
|
275
278
|
}, error);
|
|
276
279
|
if (exhausted) {
|
|
@@ -284,6 +287,17 @@ class SinkDispatcher {
|
|
|
284
287
|
}
|
|
285
288
|
return "exhausted";
|
|
286
289
|
}
|
|
290
|
+
isRetryableError(error) {
|
|
291
|
+
if (!this.retryableErrorClassifier) {
|
|
292
|
+
return true;
|
|
293
|
+
}
|
|
294
|
+
try {
|
|
295
|
+
return this.retryableErrorClassifier(error) !== false;
|
|
296
|
+
}
|
|
297
|
+
catch {
|
|
298
|
+
return true;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
287
301
|
diagnosticContext(item, attempt, maximumAttempts) {
|
|
288
302
|
return {
|
|
289
303
|
sinkName: this.target.name,
|
|
@@ -408,7 +422,7 @@ export class IterationObservationReporter {
|
|
|
408
422
|
this.recordWarning("sink_iteration_batches_unsupported", sink.name, "", -1, 1n, "The reporting sink does not support raw iteration batches.");
|
|
409
423
|
return false;
|
|
410
424
|
})
|
|
411
|
-
.map((target) => new SinkDispatcher(target, this.settings.sinkQueueDepth, this.settings.sinkParallelism, normalizeSinkRetryCount(options.sinkRetryCount), normalizeSinkRetryBackoffMs(options.sinkRetryBackoffMs), options.logger, options.runId, options.resultOwnerId, (code, sinkName, observations) => this.recordSinkDrop(code, sinkName, observations)));
|
|
425
|
+
.map((target) => new SinkDispatcher(target, this.settings.sinkQueueDepth, this.settings.sinkParallelism, normalizeSinkRetryCount(options.sinkRetryCount), normalizeSinkRetryBackoffMs(options.sinkRetryBackoffMs), options.logger, options.runId, options.resultOwnerId, target.retryableErrorClassifier, (code, sinkName, observations) => this.recordSinkDrop(code, sinkName, observations)));
|
|
412
426
|
if (this.dispatchers.length > 0) {
|
|
413
427
|
this.buffer.register(this.streamKey, this.settings.maxBufferBytes);
|
|
414
428
|
}
|
|
@@ -486,7 +500,7 @@ export class IterationObservationReporter {
|
|
|
486
500
|
&& dispatcher.droppedCount === 0n
|
|
487
501
|
&& dispatcher.deliveredCount === this.captured;
|
|
488
502
|
const completion = this.createCompletion(dispatcher, sinkReportingComplete);
|
|
489
|
-
const outcome = await this.completeWithRetries(complete, completion, deadline, dispatcher.target.name);
|
|
503
|
+
const outcome = await this.completeWithRetries(dispatcher, complete, completion, deadline, dispatcher.target.name);
|
|
490
504
|
if (outcome === "success") {
|
|
491
505
|
return true;
|
|
492
506
|
}
|
|
@@ -522,7 +536,7 @@ export class IterationObservationReporter {
|
|
|
522
536
|
};
|
|
523
537
|
return { ...this.finalResult };
|
|
524
538
|
}
|
|
525
|
-
async completeWithRetries(complete, completion, deadline, sinkName) {
|
|
539
|
+
async completeWithRetries(dispatcher, complete, completion, deadline, sinkName) {
|
|
526
540
|
const retryCount = normalizeSinkRetryCount(this.options.sinkRetryCount);
|
|
527
541
|
const backoffMs = normalizeSinkRetryBackoffMs(this.options.sinkRetryBackoffMs);
|
|
528
542
|
const maximumAttempts = retryCount + 1;
|
|
@@ -561,7 +575,9 @@ export class IterationObservationReporter {
|
|
|
561
575
|
if (outcome.kind === "timeout") {
|
|
562
576
|
return "timeout";
|
|
563
577
|
}
|
|
564
|
-
const
|
|
578
|
+
const retryable = dispatcher.isRetryableError(outcome.error);
|
|
579
|
+
const exhausted = attempt >= maximumAttempts || !retryable;
|
|
580
|
+
const reportedMaximumAttempts = retryable ? maximumAttempts : attempt;
|
|
565
581
|
const nextDelayMs = exhausted ? 0 : sinkRetryDelayMs(backoffMs, attempt);
|
|
566
582
|
logIterationObservationFailure(this.options.logger, exhausted ? "error" : "warn", {
|
|
567
583
|
sinkName,
|
|
@@ -570,7 +586,7 @@ export class IterationObservationReporter {
|
|
|
570
586
|
resultOwnerId: this.options.resultOwnerId,
|
|
571
587
|
observationCount: 0,
|
|
572
588
|
attempt,
|
|
573
|
-
maximumAttempts,
|
|
589
|
+
maximumAttempts: reportedMaximumAttempts,
|
|
574
590
|
nextDelayMs
|
|
575
591
|
}, outcome.error);
|
|
576
592
|
if (exhausted) {
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export function emptyLocalReportInput() {
|
|
2
|
+
return {
|
|
3
|
+
history: {
|
|
4
|
+
status: "available",
|
|
5
|
+
points: []
|
|
6
|
+
}
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
export function distributedLocalReportInput() {
|
|
10
|
+
return {
|
|
11
|
+
history: {
|
|
12
|
+
status: "unavailable",
|
|
13
|
+
reasonCategory: "distributed_temporal_aggregation_unavailable",
|
|
14
|
+
points: []
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
}
|