autotel-aws 0.12.16 → 0.12.18
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 +144 -0
- package/dist/cloudwatch.cjs +396 -0
- package/dist/cloudwatch.cjs.map +1 -0
- package/dist/cloudwatch.d.cts +213 -0
- package/dist/cloudwatch.d.ts +213 -0
- package/dist/cloudwatch.js +387 -0
- package/dist/cloudwatch.js.map +1 -0
- package/package.json +49 -4
package/README.md
CHANGED
|
@@ -347,6 +347,150 @@ export const publishEvent = traceKinesis({
|
|
|
347
347
|
});
|
|
348
348
|
```
|
|
349
349
|
|
|
350
|
+
## CloudWatch native OTLP endpoints
|
|
351
|
+
|
|
352
|
+
As of late 2025 / early 2026, CloudWatch accepts OTLP/HTTP directly — no collector required. Three SigV4-authed endpoints:
|
|
353
|
+
|
|
354
|
+
| Signal | Endpoint | Lands in |
|
|
355
|
+
|---|---|---|
|
|
356
|
+
| Traces | `https://xray.<region>.amazonaws.com/v1/traces` | X-Ray + Application Signals + Transaction Search |
|
|
357
|
+
| Logs | `https://logs.<region>.amazonaws.com/v1/logs` | CloudWatch Logs (Logs Insights / LiveTail) |
|
|
358
|
+
| Metrics | `https://monitoring.<region>.amazonaws.com/v1/metrics` | CloudWatch Metrics (PromQL-queryable) |
|
|
359
|
+
|
|
360
|
+
`autotel-aws/cloudwatch` ships SpanExporter / LogRecordExporter / PushMetricExporter implementations that serialize OTLP/JSON, sign with SigV4, and POST via `globalThis.fetch` — usable directly from a Lambda or any Node 18+ runtime, no sidecar.
|
|
361
|
+
|
|
362
|
+
### Install the optional peers
|
|
363
|
+
|
|
364
|
+
```bash
|
|
365
|
+
pnpm add autotel-aws autotel \
|
|
366
|
+
@smithy/signature-v4 @aws-crypto/sha256-js \
|
|
367
|
+
@aws-sdk/credential-providers \
|
|
368
|
+
@opentelemetry/sdk-trace-base \
|
|
369
|
+
@opentelemetry/sdk-logs \
|
|
370
|
+
@opentelemetry/sdk-metrics \
|
|
371
|
+
@opentelemetry/otlp-transformer
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
Only the peers you actually use are needed (e.g. drop `sdk-logs` / `sdk-metrics` if you only export traces).
|
|
375
|
+
|
|
376
|
+
### Traces
|
|
377
|
+
|
|
378
|
+
```typescript
|
|
379
|
+
import { init } from 'autotel';
|
|
380
|
+
import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base';
|
|
381
|
+
import { CloudWatchTraceExporter } from 'autotel-aws/cloudwatch';
|
|
382
|
+
|
|
383
|
+
init({
|
|
384
|
+
service: 'my-service',
|
|
385
|
+
spanProcessors: [
|
|
386
|
+
new BatchSpanProcessor(
|
|
387
|
+
new CloudWatchTraceExporter({ region: 'eu-west-1' }),
|
|
388
|
+
),
|
|
389
|
+
],
|
|
390
|
+
});
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
In Lambda, call `provider.forceFlush()` before the handler returns, or use `SimpleSpanProcessor` for synchronous export — otherwise the runtime freezes the process before the batch ships.
|
|
394
|
+
|
|
395
|
+
### Logs
|
|
396
|
+
|
|
397
|
+
The logs exporter needs an existing CloudWatch log group + stream. In Lambda the runtime provisions both and exposes them as `AWS_LAMBDA_LOG_GROUP_NAME` / `AWS_LAMBDA_LOG_STREAM_NAME` (read by default).
|
|
398
|
+
|
|
399
|
+
```typescript
|
|
400
|
+
import { BatchLogRecordProcessor, LoggerProvider } from '@opentelemetry/sdk-logs';
|
|
401
|
+
import { CloudWatchLogExporter } from 'autotel-aws/cloudwatch';
|
|
402
|
+
|
|
403
|
+
const provider = new LoggerProvider({
|
|
404
|
+
processors: [new BatchLogRecordProcessor(new CloudWatchLogExporter())],
|
|
405
|
+
});
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
### Metrics
|
|
409
|
+
|
|
410
|
+
```typescript
|
|
411
|
+
import { MeterProvider, PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics';
|
|
412
|
+
import { CloudWatchMetricExporter } from 'autotel-aws/cloudwatch';
|
|
413
|
+
|
|
414
|
+
const provider = new MeterProvider({
|
|
415
|
+
readers: [
|
|
416
|
+
new PeriodicExportingMetricReader({
|
|
417
|
+
exporter: new CloudWatchMetricExporter({ region: 'eu-west-1' }),
|
|
418
|
+
exportIntervalMillis: 60_000,
|
|
419
|
+
}),
|
|
420
|
+
],
|
|
421
|
+
});
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
### Direct vs ADOT collector layer
|
|
425
|
+
|
|
426
|
+
You have two integration paths:
|
|
427
|
+
|
|
428
|
+
| | In-process (`autotel-aws/cloudwatch`) | ADOT Lambda extension layer |
|
|
429
|
+
|---|---|---|
|
|
430
|
+
| Sidecar | None | Yes (extension layer) |
|
|
431
|
+
| Export latency | In the billed handler time | Outside billed time |
|
|
432
|
+
| Cold start cost | Bundle size + SigV4 init | Layer init |
|
|
433
|
+
| Best for | Low-volume functions, custom transports | Higher-throughput functions |
|
|
434
|
+
|
|
435
|
+
Pick one — running both ships duplicate spans.
|
|
436
|
+
|
|
437
|
+
### ADOT collector config (SigV4)
|
|
438
|
+
|
|
439
|
+
If you prefer the ADOT Lambda extension layer, configure the collector with
|
|
440
|
+
`sigv4auth` and signal-specific endpoints:
|
|
441
|
+
|
|
442
|
+
```yaml
|
|
443
|
+
extensions:
|
|
444
|
+
sigv4auth:
|
|
445
|
+
region: us-west-2
|
|
446
|
+
service: xray
|
|
447
|
+
|
|
448
|
+
receivers:
|
|
449
|
+
otlp:
|
|
450
|
+
protocols:
|
|
451
|
+
http:
|
|
452
|
+
|
|
453
|
+
exporters:
|
|
454
|
+
otlphttp/traces:
|
|
455
|
+
endpoint: https://xray.us-west-2.amazonaws.com/v1/traces
|
|
456
|
+
auth:
|
|
457
|
+
authenticator: sigv4auth
|
|
458
|
+
|
|
459
|
+
service:
|
|
460
|
+
extensions: [sigv4auth]
|
|
461
|
+
pipelines:
|
|
462
|
+
traces:
|
|
463
|
+
receivers: [otlp]
|
|
464
|
+
exporters: [otlphttp/traces]
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
For logs/metrics use the same pattern with:
|
|
468
|
+
- logs endpoint `https://logs.<region>.amazonaws.com/v1/logs`, service `logs`
|
|
469
|
+
- metrics endpoint `https://monitoring.<region>.amazonaws.com/v1/metrics`, service `monitoring`
|
|
470
|
+
|
|
471
|
+
### Production checklist
|
|
472
|
+
|
|
473
|
+
- Set `AWS_REGION` explicitly and keep endpoint region + signer region aligned.
|
|
474
|
+
- Grant IAM permissions for every signal you export (X-Ray, Logs, Metrics).
|
|
475
|
+
- For logs endpoint, ensure log group + stream exist (`CloudWatchLogExporter` can read Lambda defaults).
|
|
476
|
+
- Flush before Lambda return (or use ADOT mode) so buffered spans/logs/metrics are not dropped on freeze.
|
|
477
|
+
- Monitor 4xx/429 responses and tune batch size / export interval to stay within quotas.
|
|
478
|
+
- Choose one export path per signal (direct exporters or ADOT collector), not both.
|
|
479
|
+
- Keep payload sizes within endpoint limits and use gzip where appropriate.
|
|
480
|
+
- Keep runtime clocks in sync (timestamp windows are enforced).
|
|
481
|
+
|
|
482
|
+
### Limits to remember
|
|
483
|
+
|
|
484
|
+
- **Traces**: 5 MB / request, 10 000 spans / batch, 200 KB / span, timestamps within `[-14d, +2h]`.
|
|
485
|
+
- **Logs**: 1 MB / request (20 MB in some regions for LLO-backed events). 1 MB / log event before truncation.
|
|
486
|
+
- **Metrics**: 500 TPS / account, 150 labels / datapoint, 1 MB / request, 1 000 datapoints / request.
|
|
487
|
+
|
|
488
|
+
### Payload format note
|
|
489
|
+
|
|
490
|
+
`autotel-aws/cloudwatch` currently sends OTLP over HTTP using JSON payloads. CloudWatch endpoints also accept protobuf payloads, but protobuf transport is not currently wired in these exporters.
|
|
491
|
+
|
|
492
|
+
Full details: https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch-OTLPEndpoint.html
|
|
493
|
+
|
|
350
494
|
## X-Ray Annotations
|
|
351
495
|
|
|
352
496
|
For users sending traces to AWS X-Ray, annotations are indexed for filtering:
|
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
require('./chunk-JEQ2X3Z6.cjs');
|
|
4
|
+
var core = require('@opentelemetry/core');
|
|
5
|
+
var otlpTransformer = require('@opentelemetry/otlp-transformer');
|
|
6
|
+
var sdkMetrics = require('@opentelemetry/sdk-metrics');
|
|
7
|
+
|
|
8
|
+
// src/cloudwatch/endpoints.ts
|
|
9
|
+
function cloudWatchTracesEndpoint({ region }) {
|
|
10
|
+
return `https://xray.${region}.amazonaws.com/v1/traces`;
|
|
11
|
+
}
|
|
12
|
+
function cloudWatchLogsEndpoint({ region }) {
|
|
13
|
+
return `https://logs.${region}.amazonaws.com/v1/logs`;
|
|
14
|
+
}
|
|
15
|
+
function cloudWatchMetricsEndpoint({ region }) {
|
|
16
|
+
return `https://monitoring.${region}.amazonaws.com/v1/metrics`;
|
|
17
|
+
}
|
|
18
|
+
var SIGV4_SERVICE = {
|
|
19
|
+
traces: "xray",
|
|
20
|
+
logs: "logs",
|
|
21
|
+
metrics: "monitoring"
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// src/cloudwatch/sigv4.ts
|
|
25
|
+
async function signCloudWatchOtlpRequest(input) {
|
|
26
|
+
const { SignatureV4 } = await loadSigV4();
|
|
27
|
+
const { Sha256 } = await loadSha256();
|
|
28
|
+
const credentials = await resolveCredentials(input.credentials);
|
|
29
|
+
const parsed = new URL(input.url);
|
|
30
|
+
const contentType = input.contentType ?? "application/json";
|
|
31
|
+
const headers = {
|
|
32
|
+
host: parsed.host,
|
|
33
|
+
"content-type": contentType,
|
|
34
|
+
...input.contentEncoding && { "content-encoding": input.contentEncoding },
|
|
35
|
+
...input.additionalHeaders
|
|
36
|
+
};
|
|
37
|
+
const signer = new SignatureV4({
|
|
38
|
+
service: SIGV4_SERVICE[input.signal],
|
|
39
|
+
region: input.region,
|
|
40
|
+
credentials,
|
|
41
|
+
sha256: Sha256
|
|
42
|
+
});
|
|
43
|
+
const signed = await signer.sign({
|
|
44
|
+
method: "POST",
|
|
45
|
+
protocol: parsed.protocol,
|
|
46
|
+
hostname: parsed.hostname,
|
|
47
|
+
port: parsed.port ? Number(parsed.port) : void 0,
|
|
48
|
+
path: parsed.pathname + parsed.search,
|
|
49
|
+
headers,
|
|
50
|
+
body: input.body
|
|
51
|
+
});
|
|
52
|
+
return signed.headers;
|
|
53
|
+
}
|
|
54
|
+
async function resolveCredentials(source) {
|
|
55
|
+
if (typeof source === "function") return source();
|
|
56
|
+
if (source) return source;
|
|
57
|
+
return loadDefaultCredentials();
|
|
58
|
+
}
|
|
59
|
+
async function loadDefaultCredentials() {
|
|
60
|
+
try {
|
|
61
|
+
const mod = await import('@aws-sdk/credential-providers');
|
|
62
|
+
return await mod.fromNodeProviderChain()();
|
|
63
|
+
} catch (error) {
|
|
64
|
+
throw new Error(
|
|
65
|
+
"autotel-aws/cloudwatch: no credentials supplied and `@aws-sdk/credential-providers` is not installed. Either pass `credentials` explicitly or install the package.",
|
|
66
|
+
{ cause: error }
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
async function loadSigV4() {
|
|
71
|
+
try {
|
|
72
|
+
return await import('@smithy/signature-v4');
|
|
73
|
+
} catch (error) {
|
|
74
|
+
throw new Error(
|
|
75
|
+
"autotel-aws/cloudwatch: `@smithy/signature-v4` is required to sign OTLP requests. Install it (and `@aws-crypto/sha256-js`) alongside autotel-aws.",
|
|
76
|
+
{ cause: error }
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
async function loadSha256() {
|
|
81
|
+
try {
|
|
82
|
+
return await import('@aws-crypto/sha256-js');
|
|
83
|
+
} catch (error) {
|
|
84
|
+
throw new Error(
|
|
85
|
+
"autotel-aws/cloudwatch: `@aws-crypto/sha256-js` is required to sign OTLP requests. Install it alongside `@smithy/signature-v4`.",
|
|
86
|
+
{ cause: error }
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
var CloudWatchTraceExporter = class {
|
|
91
|
+
region;
|
|
92
|
+
endpoint;
|
|
93
|
+
credentials;
|
|
94
|
+
timeoutMs;
|
|
95
|
+
fetchImpl;
|
|
96
|
+
shutdownOnce = false;
|
|
97
|
+
constructor(config = {}) {
|
|
98
|
+
const region = config.region ?? process.env.AWS_REGION;
|
|
99
|
+
if (!region) {
|
|
100
|
+
throw new Error(
|
|
101
|
+
"CloudWatchTraceExporter: `region` is required (pass it explicitly or set AWS_REGION)."
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
this.region = region;
|
|
105
|
+
this.endpoint = config.endpoint ?? cloudWatchTracesEndpoint({ region });
|
|
106
|
+
this.credentials = config.credentials;
|
|
107
|
+
this.timeoutMs = config.timeoutMs ?? 1e4;
|
|
108
|
+
this.fetchImpl = config.fetchImpl ?? globalThis.fetch;
|
|
109
|
+
if (typeof this.fetchImpl !== "function") {
|
|
110
|
+
throw new TypeError(
|
|
111
|
+
"CloudWatchTraceExporter: global `fetch` is not available \u2014 pass `fetchImpl` explicitly (Node 18+ has it natively)."
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
export(spans, resultCallback) {
|
|
116
|
+
if (this.shutdownOnce) {
|
|
117
|
+
resultCallback({
|
|
118
|
+
code: core.ExportResultCode.FAILED,
|
|
119
|
+
error: new Error("CloudWatchTraceExporter: already shut down")
|
|
120
|
+
});
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (spans.length === 0) {
|
|
124
|
+
resultCallback({ code: core.ExportResultCode.SUCCESS });
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
this.sendBatch(spans).then(
|
|
128
|
+
() => resultCallback({ code: core.ExportResultCode.SUCCESS }),
|
|
129
|
+
(error) => resultCallback({
|
|
130
|
+
code: core.ExportResultCode.FAILED,
|
|
131
|
+
error: error instanceof Error ? error : new Error(String(error))
|
|
132
|
+
})
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
async shutdown() {
|
|
136
|
+
this.shutdownOnce = true;
|
|
137
|
+
}
|
|
138
|
+
async forceFlush() {
|
|
139
|
+
}
|
|
140
|
+
async sendBatch(spans) {
|
|
141
|
+
const body = otlpTransformer.JsonTraceSerializer.serializeRequest(spans);
|
|
142
|
+
if (!body) {
|
|
143
|
+
throw new Error("CloudWatchTraceExporter: serializer produced no body");
|
|
144
|
+
}
|
|
145
|
+
const headers = await signCloudWatchOtlpRequest({
|
|
146
|
+
url: this.endpoint,
|
|
147
|
+
body,
|
|
148
|
+
region: this.region,
|
|
149
|
+
signal: "traces",
|
|
150
|
+
credentials: this.credentials,
|
|
151
|
+
contentType: "application/json"
|
|
152
|
+
});
|
|
153
|
+
const controller = new AbortController();
|
|
154
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
155
|
+
let response;
|
|
156
|
+
try {
|
|
157
|
+
response = await this.fetchImpl(this.endpoint, {
|
|
158
|
+
method: "POST",
|
|
159
|
+
headers,
|
|
160
|
+
body,
|
|
161
|
+
signal: controller.signal
|
|
162
|
+
});
|
|
163
|
+
} finally {
|
|
164
|
+
clearTimeout(timer);
|
|
165
|
+
}
|
|
166
|
+
if (!response.ok) {
|
|
167
|
+
const text = await safeReadText(response);
|
|
168
|
+
throw new Error(
|
|
169
|
+
`CloudWatchTraceExporter: HTTP ${response.status} ${response.statusText} ${text}`.trim()
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
async function safeReadText(response) {
|
|
175
|
+
try {
|
|
176
|
+
return await response.text();
|
|
177
|
+
} catch {
|
|
178
|
+
return "";
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
var CloudWatchLogExporter = class {
|
|
182
|
+
region;
|
|
183
|
+
endpoint;
|
|
184
|
+
credentials;
|
|
185
|
+
logGroup;
|
|
186
|
+
logStream;
|
|
187
|
+
truncatableFields;
|
|
188
|
+
timeoutMs;
|
|
189
|
+
fetchImpl;
|
|
190
|
+
shutdownOnce = false;
|
|
191
|
+
constructor(config = {}) {
|
|
192
|
+
const region = config.region ?? process.env.AWS_REGION;
|
|
193
|
+
if (!region) {
|
|
194
|
+
throw new Error(
|
|
195
|
+
"CloudWatchLogExporter: `region` is required (pass it explicitly or set AWS_REGION)."
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
const logGroup = config.logGroup ?? process.env.AWS_LAMBDA_LOG_GROUP_NAME;
|
|
199
|
+
const logStream = config.logStream ?? process.env.AWS_LAMBDA_LOG_STREAM_NAME;
|
|
200
|
+
if (!logGroup || !logStream) {
|
|
201
|
+
throw new Error(
|
|
202
|
+
"CloudWatchLogExporter: `logGroup` and `logStream` are required (in Lambda they come from AWS_LAMBDA_LOG_GROUP_NAME / AWS_LAMBDA_LOG_STREAM_NAME)."
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
this.region = region;
|
|
206
|
+
this.endpoint = config.endpoint ?? cloudWatchLogsEndpoint({ region });
|
|
207
|
+
this.credentials = config.credentials;
|
|
208
|
+
this.logGroup = logGroup;
|
|
209
|
+
this.logStream = logStream;
|
|
210
|
+
this.truncatableFields = config.truncatableFields;
|
|
211
|
+
this.timeoutMs = config.timeoutMs ?? 1e4;
|
|
212
|
+
this.fetchImpl = config.fetchImpl ?? globalThis.fetch;
|
|
213
|
+
if (typeof this.fetchImpl !== "function") {
|
|
214
|
+
throw new TypeError(
|
|
215
|
+
"CloudWatchLogExporter: global `fetch` is not available \u2014 pass `fetchImpl` explicitly."
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
export(logs, resultCallback) {
|
|
220
|
+
if (this.shutdownOnce) {
|
|
221
|
+
resultCallback({
|
|
222
|
+
code: core.ExportResultCode.FAILED,
|
|
223
|
+
error: new Error("CloudWatchLogExporter: already shut down")
|
|
224
|
+
});
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
if (logs.length === 0) {
|
|
228
|
+
resultCallback({ code: core.ExportResultCode.SUCCESS });
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
this.sendBatch(logs).then(
|
|
232
|
+
() => resultCallback({ code: core.ExportResultCode.SUCCESS }),
|
|
233
|
+
(error) => resultCallback({
|
|
234
|
+
code: core.ExportResultCode.FAILED,
|
|
235
|
+
error: error instanceof Error ? error : new Error(String(error))
|
|
236
|
+
})
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
async shutdown() {
|
|
240
|
+
this.shutdownOnce = true;
|
|
241
|
+
}
|
|
242
|
+
async forceFlush() {
|
|
243
|
+
}
|
|
244
|
+
async sendBatch(logs) {
|
|
245
|
+
const body = otlpTransformer.JsonLogsSerializer.serializeRequest(logs);
|
|
246
|
+
if (!body) {
|
|
247
|
+
throw new Error("CloudWatchLogExporter: serializer produced no body");
|
|
248
|
+
}
|
|
249
|
+
const additionalHeaders = {
|
|
250
|
+
"x-aws-log-group": this.logGroup,
|
|
251
|
+
"x-aws-log-stream": this.logStream
|
|
252
|
+
};
|
|
253
|
+
if (this.truncatableFields) {
|
|
254
|
+
additionalHeaders["x-aws-truncatable-fields"] = this.truncatableFields;
|
|
255
|
+
}
|
|
256
|
+
const headers = await signCloudWatchOtlpRequest({
|
|
257
|
+
url: this.endpoint,
|
|
258
|
+
body,
|
|
259
|
+
region: this.region,
|
|
260
|
+
signal: "logs",
|
|
261
|
+
credentials: this.credentials,
|
|
262
|
+
contentType: "application/json",
|
|
263
|
+
additionalHeaders
|
|
264
|
+
});
|
|
265
|
+
const controller = new AbortController();
|
|
266
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
267
|
+
let response;
|
|
268
|
+
try {
|
|
269
|
+
response = await this.fetchImpl(this.endpoint, {
|
|
270
|
+
method: "POST",
|
|
271
|
+
headers,
|
|
272
|
+
body,
|
|
273
|
+
signal: controller.signal
|
|
274
|
+
});
|
|
275
|
+
} finally {
|
|
276
|
+
clearTimeout(timer);
|
|
277
|
+
}
|
|
278
|
+
if (!response.ok) {
|
|
279
|
+
const text = await safeReadText2(response);
|
|
280
|
+
throw new Error(
|
|
281
|
+
`CloudWatchLogExporter: HTTP ${response.status} ${response.statusText} ${text}`.trim()
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
async function safeReadText2(response) {
|
|
287
|
+
try {
|
|
288
|
+
return await response.text();
|
|
289
|
+
} catch {
|
|
290
|
+
return "";
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
var DEFAULT_TEMPORALITY = (_instrumentType) => sdkMetrics.AggregationTemporality.DELTA;
|
|
294
|
+
var CloudWatchMetricExporter = class {
|
|
295
|
+
region;
|
|
296
|
+
endpoint;
|
|
297
|
+
credentials;
|
|
298
|
+
timeoutMs;
|
|
299
|
+
fetchImpl;
|
|
300
|
+
temporalitySelector;
|
|
301
|
+
shutdownOnce = false;
|
|
302
|
+
constructor(config = {}) {
|
|
303
|
+
const region = config.region ?? process.env.AWS_REGION;
|
|
304
|
+
if (!region) {
|
|
305
|
+
throw new Error(
|
|
306
|
+
"CloudWatchMetricExporter: `region` is required (pass it explicitly or set AWS_REGION)."
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
this.region = region;
|
|
310
|
+
this.endpoint = config.endpoint ?? cloudWatchMetricsEndpoint({ region });
|
|
311
|
+
this.credentials = config.credentials;
|
|
312
|
+
this.timeoutMs = config.timeoutMs ?? 1e4;
|
|
313
|
+
this.fetchImpl = config.fetchImpl ?? globalThis.fetch;
|
|
314
|
+
this.temporalitySelector = config.temporalitySelector ?? DEFAULT_TEMPORALITY;
|
|
315
|
+
if (typeof this.fetchImpl !== "function") {
|
|
316
|
+
throw new TypeError(
|
|
317
|
+
"CloudWatchMetricExporter: global `fetch` is not available \u2014 pass `fetchImpl` explicitly."
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
export(metrics, resultCallback) {
|
|
322
|
+
if (this.shutdownOnce) {
|
|
323
|
+
resultCallback({
|
|
324
|
+
code: core.ExportResultCode.FAILED,
|
|
325
|
+
error: new Error("CloudWatchMetricExporter: already shut down")
|
|
326
|
+
});
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
this.sendBatch(metrics).then(
|
|
330
|
+
() => resultCallback({ code: core.ExportResultCode.SUCCESS }),
|
|
331
|
+
(error) => resultCallback({
|
|
332
|
+
code: core.ExportResultCode.FAILED,
|
|
333
|
+
error: error instanceof Error ? error : new Error(String(error))
|
|
334
|
+
})
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
async shutdown() {
|
|
338
|
+
this.shutdownOnce = true;
|
|
339
|
+
}
|
|
340
|
+
async forceFlush() {
|
|
341
|
+
}
|
|
342
|
+
selectAggregationTemporality(instrumentType) {
|
|
343
|
+
return this.temporalitySelector(instrumentType);
|
|
344
|
+
}
|
|
345
|
+
async sendBatch(metrics) {
|
|
346
|
+
const body = otlpTransformer.JsonMetricsSerializer.serializeRequest(metrics);
|
|
347
|
+
if (!body) {
|
|
348
|
+
throw new Error("CloudWatchMetricExporter: serializer produced no body");
|
|
349
|
+
}
|
|
350
|
+
const headers = await signCloudWatchOtlpRequest({
|
|
351
|
+
url: this.endpoint,
|
|
352
|
+
body,
|
|
353
|
+
region: this.region,
|
|
354
|
+
signal: "metrics",
|
|
355
|
+
credentials: this.credentials,
|
|
356
|
+
contentType: "application/json"
|
|
357
|
+
});
|
|
358
|
+
const controller = new AbortController();
|
|
359
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
360
|
+
let response;
|
|
361
|
+
try {
|
|
362
|
+
response = await this.fetchImpl(this.endpoint, {
|
|
363
|
+
method: "POST",
|
|
364
|
+
headers,
|
|
365
|
+
body,
|
|
366
|
+
signal: controller.signal
|
|
367
|
+
});
|
|
368
|
+
} finally {
|
|
369
|
+
clearTimeout(timer);
|
|
370
|
+
}
|
|
371
|
+
if (!response.ok) {
|
|
372
|
+
const text = await safeReadText3(response);
|
|
373
|
+
throw new Error(
|
|
374
|
+
`CloudWatchMetricExporter: HTTP ${response.status} ${response.statusText} ${text}`.trim()
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
};
|
|
379
|
+
async function safeReadText3(response) {
|
|
380
|
+
try {
|
|
381
|
+
return await response.text();
|
|
382
|
+
} catch {
|
|
383
|
+
return "";
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
exports.CloudWatchLogExporter = CloudWatchLogExporter;
|
|
388
|
+
exports.CloudWatchMetricExporter = CloudWatchMetricExporter;
|
|
389
|
+
exports.CloudWatchTraceExporter = CloudWatchTraceExporter;
|
|
390
|
+
exports.SIGV4_SERVICE = SIGV4_SERVICE;
|
|
391
|
+
exports.cloudWatchLogsEndpoint = cloudWatchLogsEndpoint;
|
|
392
|
+
exports.cloudWatchMetricsEndpoint = cloudWatchMetricsEndpoint;
|
|
393
|
+
exports.cloudWatchTracesEndpoint = cloudWatchTracesEndpoint;
|
|
394
|
+
exports.signCloudWatchOtlpRequest = signCloudWatchOtlpRequest;
|
|
395
|
+
//# sourceMappingURL=cloudwatch.cjs.map
|
|
396
|
+
//# sourceMappingURL=cloudwatch.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/cloudwatch/endpoints.ts","../src/cloudwatch/sigv4.ts","../src/cloudwatch/trace-exporter.ts","../src/cloudwatch/log-exporter.ts","../src/cloudwatch/metric-exporter.ts"],"names":["ExportResultCode","JsonTraceSerializer","JsonLogsSerializer","safeReadText","AggregationTemporality","JsonMetricsSerializer"],"mappings":"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","file":"cloudwatch.cjs","sourcesContent":["/**\n * CloudWatch native OTLP HTTP endpoints.\n *\n * Reference: https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch-OTLPEndpoint.html\n *\n * All three endpoints accept OTLP/HTTP (JSON or protobuf, optional gzip)\n * and require AWS Signature V4 authentication. They land data in:\n *\n * - traces → X-Ray + Application Signals + Transaction Search\n * - logs → CloudWatch Logs (needs x-aws-log-group + x-aws-log-stream headers)\n * - metrics → CloudWatch Metrics, queryable via PromQL\n */\n\nexport interface CloudWatchEndpointInput {\n readonly region: string;\n}\n\n/** Traces — signed against the `xray` service. */\nexport function cloudWatchTracesEndpoint({ region }: CloudWatchEndpointInput): string {\n return `https://xray.${region}.amazonaws.com/v1/traces`;\n}\n\n/** Logs — signed against the `logs` service. Requires log group/stream headers. */\nexport function cloudWatchLogsEndpoint({ region }: CloudWatchEndpointInput): string {\n return `https://logs.${region}.amazonaws.com/v1/logs`;\n}\n\n/** Metrics — signed against the `monitoring` service. */\nexport function cloudWatchMetricsEndpoint({ region }: CloudWatchEndpointInput): string {\n return `https://monitoring.${region}.amazonaws.com/v1/metrics`;\n}\n\n/**\n * The SigV4 service name used when signing requests for each endpoint.\n * These do NOT always match the host prefix — they're the canonical\n * AWS service identifiers used by the SigV4 signer.\n */\nexport const SIGV4_SERVICE = {\n traces: 'xray',\n logs: 'logs',\n metrics: 'monitoring',\n} as const;\n\nexport type CloudWatchSignal = keyof typeof SIGV4_SERVICE;\n","/**\n * SigV4 signing helper for CloudWatch OTLP HTTP endpoints.\n *\n * Uses `@smithy/signature-v4` + `@aws-crypto/sha256-js` (optional peer deps).\n * Returns the headers required to POST a signed OTLP request.\n */\n\nimport type { CloudWatchSignal } from './endpoints';\nimport { SIGV4_SERVICE } from './endpoints';\n\nexport interface AwsCredentialsLike {\n readonly accessKeyId: string;\n readonly secretAccessKey: string;\n readonly sessionToken?: string;\n}\n\nexport type AwsCredentialsProvider =\n | AwsCredentialsLike\n | (() => AwsCredentialsLike | Promise<AwsCredentialsLike>);\n\nexport interface SignOtlpRequestInput {\n /** The full endpoint URL (e.g. https://xray.eu-west-1.amazonaws.com/v1/traces). */\n url: string;\n /** OTLP payload — already serialized + (optionally) gzipped. */\n body: Uint8Array;\n /** AWS region (e.g. \"eu-west-1\"). Must match the host. */\n region: string;\n /** Which CloudWatch endpoint we're hitting (selects the SigV4 service name). */\n signal: CloudWatchSignal;\n /** Static credentials or a provider. Falls back to the AWS SDK default chain when omitted. */\n credentials?: AwsCredentialsProvider;\n /** Extra headers to include in the canonical request (signed). */\n additionalHeaders?: Record<string, string>;\n /** Content-Type — defaults to OTLP/JSON. Use `application/x-protobuf` for proto. */\n contentType?: 'application/json' | 'application/x-protobuf';\n /** Content-Encoding (e.g. \"gzip\"). Omit for identity. */\n contentEncoding?: 'gzip';\n}\n\n/**\n * Compute SigV4-signed headers for an OTLP HTTP POST to a CloudWatch endpoint.\n *\n * Throws if the optional `@smithy/signature-v4` / `@aws-crypto/sha256-js`\n * peer dependencies aren't installed — install them alongside autotel-aws\n * when you want to ship telemetry directly from app code (no collector).\n */\nexport async function signCloudWatchOtlpRequest(\n input: SignOtlpRequestInput,\n): Promise<Record<string, string>> {\n const { SignatureV4 } = await loadSigV4();\n const { Sha256 } = await loadSha256();\n\n const credentials = await resolveCredentials(input.credentials);\n const parsed = new URL(input.url);\n const contentType = input.contentType ?? 'application/json';\n\n const headers: Record<string, string> = {\n host: parsed.host,\n 'content-type': contentType,\n ...(input.contentEncoding && { 'content-encoding': input.contentEncoding }),\n ...input.additionalHeaders,\n };\n\n const signer = new SignatureV4({\n service: SIGV4_SERVICE[input.signal],\n region: input.region,\n credentials,\n sha256: Sha256,\n });\n\n const signed = await signer.sign({\n method: 'POST',\n protocol: parsed.protocol,\n hostname: parsed.hostname,\n port: parsed.port ? Number(parsed.port) : undefined,\n path: parsed.pathname + parsed.search,\n headers,\n body: input.body,\n });\n\n return signed.headers as Record<string, string>;\n}\n\nasync function resolveCredentials(\n source: AwsCredentialsProvider | undefined,\n): Promise<AwsCredentialsLike> {\n if (typeof source === 'function') return source();\n if (source) return source;\n return loadDefaultCredentials();\n}\n\nasync function loadDefaultCredentials(): Promise<AwsCredentialsLike> {\n try {\n // Optional peer dep — only required when caller doesn't pass credentials.\n const mod = await import('@aws-sdk/credential-providers');\n return await mod.fromNodeProviderChain()();\n } catch (error) {\n throw new Error(\n 'autotel-aws/cloudwatch: no credentials supplied and `@aws-sdk/credential-providers` is not installed. ' +\n 'Either pass `credentials` explicitly or install the package.',\n { cause: error },\n );\n }\n}\n\nasync function loadSigV4(): Promise<typeof import('@smithy/signature-v4')> {\n try {\n return await import('@smithy/signature-v4');\n } catch (error) {\n throw new Error(\n 'autotel-aws/cloudwatch: `@smithy/signature-v4` is required to sign OTLP requests. ' +\n 'Install it (and `@aws-crypto/sha256-js`) alongside autotel-aws.',\n { cause: error },\n );\n }\n}\n\nasync function loadSha256(): Promise<typeof import('@aws-crypto/sha256-js')> {\n try {\n return await import('@aws-crypto/sha256-js');\n } catch (error) {\n throw new Error(\n 'autotel-aws/cloudwatch: `@aws-crypto/sha256-js` is required to sign OTLP requests. ' +\n 'Install it alongside `@smithy/signature-v4`.',\n { cause: error },\n );\n }\n}\n","/**\n * SpanExporter that ships OTLP/JSON straight to the CloudWatch traces\n * endpoint (https://xray.<region>.amazonaws.com/v1/traces) using SigV4.\n *\n * No collector required. Useful in Lambda when you don't want to attach\n * the ADOT extension layer, or any other env where you'd rather not run\n * a sidecar.\n *\n * Lands in: X-Ray + Application Signals + Transaction Search.\n */\n\nimport type { ExportResult } from '@opentelemetry/core';\nimport { ExportResultCode } from '@opentelemetry/core';\nimport { JsonTraceSerializer } from '@opentelemetry/otlp-transformer';\nimport type {\n ReadableSpan,\n SpanExporter,\n} from '@opentelemetry/sdk-trace-base';\n\nimport { cloudWatchTracesEndpoint } from './endpoints';\nimport {\n signCloudWatchOtlpRequest,\n type AwsCredentialsProvider,\n} from './sigv4';\n\nexport interface CloudWatchTraceExporterConfig {\n /** AWS region — defaults to `process.env.AWS_REGION`. */\n region?: string;\n /** Override the endpoint (mainly for tests / VPC endpoints). */\n endpoint?: string;\n /** Static credentials or a provider. Defaults to the AWS SDK default chain. */\n credentials?: AwsCredentialsProvider;\n /** Per-export timeout in ms. Defaults to 10s (CloudWatch's documented soft cap). */\n timeoutMs?: number;\n /** Optional `fetch` override (Node 18+ has it globally; useful in tests). */\n fetchImpl?: typeof fetch;\n}\n\n/**\n * OTLP/JSON exporter for CloudWatch's traces endpoint.\n *\n * Use with `BatchSpanProcessor` or `SimpleSpanProcessor` from\n * `@opentelemetry/sdk-trace-base`. For Lambda, `SimpleSpanProcessor`\n * (synchronous export per span) is fine for small workloads;\n * `BatchSpanProcessor` is better for higher-throughput functions but\n * needs `forceFlush()` before the handler returns.\n */\nexport class CloudWatchTraceExporter implements SpanExporter {\n private readonly region: string;\n private readonly endpoint: string;\n private readonly credentials?: AwsCredentialsProvider;\n private readonly timeoutMs: number;\n private readonly fetchImpl: typeof fetch;\n private shutdownOnce = false;\n\n constructor(config: CloudWatchTraceExporterConfig = {}) {\n const region = config.region ?? process.env.AWS_REGION;\n if (!region) {\n throw new Error(\n 'CloudWatchTraceExporter: `region` is required (pass it explicitly or set AWS_REGION).',\n );\n }\n this.region = region;\n this.endpoint = config.endpoint ?? cloudWatchTracesEndpoint({ region });\n this.credentials = config.credentials;\n this.timeoutMs = config.timeoutMs ?? 10_000;\n this.fetchImpl = config.fetchImpl ?? globalThis.fetch;\n if (typeof this.fetchImpl !== 'function') {\n throw new TypeError(\n 'CloudWatchTraceExporter: global `fetch` is not available — pass `fetchImpl` explicitly (Node 18+ has it natively).',\n );\n }\n }\n\n export(\n spans: ReadableSpan[],\n resultCallback: (result: ExportResult) => void,\n ): void {\n if (this.shutdownOnce) {\n resultCallback({\n code: ExportResultCode.FAILED,\n error: new Error('CloudWatchTraceExporter: already shut down'),\n });\n return;\n }\n if (spans.length === 0) {\n resultCallback({ code: ExportResultCode.SUCCESS });\n return;\n }\n\n this.sendBatch(spans).then(\n () => resultCallback({ code: ExportResultCode.SUCCESS }),\n (error: unknown) =>\n resultCallback({\n code: ExportResultCode.FAILED,\n error: error instanceof Error ? error : new Error(String(error)),\n }),\n );\n }\n\n async shutdown(): Promise<void> {\n this.shutdownOnce = true;\n }\n\n async forceFlush(): Promise<void> {\n // Batching is the responsibility of the span processor. Nothing to flush here.\n }\n\n private async sendBatch(spans: ReadableSpan[]): Promise<void> {\n const body = JsonTraceSerializer.serializeRequest(spans);\n if (!body) {\n throw new Error('CloudWatchTraceExporter: serializer produced no body');\n }\n\n const headers = await signCloudWatchOtlpRequest({\n url: this.endpoint,\n body,\n region: this.region,\n signal: 'traces',\n credentials: this.credentials,\n contentType: 'application/json',\n });\n\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), this.timeoutMs);\n\n let response: Response;\n try {\n response = await this.fetchImpl(this.endpoint, {\n method: 'POST',\n headers,\n body,\n signal: controller.signal,\n });\n } finally {\n clearTimeout(timer);\n }\n\n if (!response.ok) {\n const text = await safeReadText(response);\n throw new Error(\n `CloudWatchTraceExporter: HTTP ${response.status} ${response.statusText} ${text}`.trim(),\n );\n }\n }\n}\n\nasync function safeReadText(response: Response): Promise<string> {\n try {\n return await response.text();\n } catch {\n return '';\n }\n}\n","/**\n * LogRecordExporter that ships OTLP/JSON logs to the CloudWatch logs\n * endpoint (https://logs.<region>.amazonaws.com/v1/logs) using SigV4.\n *\n * CloudWatch Logs requires `x-aws-log-group` and `x-aws-log-stream`\n * headers — both must already exist (this exporter does NOT create them).\n *\n * In Lambda, the runtime auto-provisions a log group + stream per function;\n * read them from `AWS_LAMBDA_LOG_GROUP_NAME` / `AWS_LAMBDA_LOG_STREAM_NAME`\n * (set automatically when the function runs).\n */\n\nimport type { ExportResult } from '@opentelemetry/core';\nimport { ExportResultCode } from '@opentelemetry/core';\nimport { JsonLogsSerializer } from '@opentelemetry/otlp-transformer';\nimport type {\n LogRecordExporter,\n ReadableLogRecord,\n} from '@opentelemetry/sdk-logs';\n\nimport { cloudWatchLogsEndpoint } from './endpoints';\nimport {\n signCloudWatchOtlpRequest,\n type AwsCredentialsProvider,\n} from './sigv4';\n\nexport interface CloudWatchLogExporterConfig {\n region?: string;\n endpoint?: string;\n credentials?: AwsCredentialsProvider;\n /**\n * Existing CloudWatch log group to write into. Defaults to\n * `AWS_LAMBDA_LOG_GROUP_NAME` (set automatically by the Lambda runtime).\n */\n logGroup?: string;\n /**\n * Existing CloudWatch log stream. Defaults to\n * `AWS_LAMBDA_LOG_STREAM_NAME`.\n */\n logStream?: string;\n /**\n * Optional comma-separated list of field paths CloudWatch may truncate\n * if the event exceeds 1 MB (sent as `x-aws-truncatable-fields`).\n */\n truncatableFields?: string;\n timeoutMs?: number;\n fetchImpl?: typeof fetch;\n}\n\nexport class CloudWatchLogExporter implements LogRecordExporter {\n private readonly region: string;\n private readonly endpoint: string;\n private readonly credentials?: AwsCredentialsProvider;\n private readonly logGroup: string;\n private readonly logStream: string;\n private readonly truncatableFields?: string;\n private readonly timeoutMs: number;\n private readonly fetchImpl: typeof fetch;\n private shutdownOnce = false;\n\n constructor(config: CloudWatchLogExporterConfig = {}) {\n const region = config.region ?? process.env.AWS_REGION;\n if (!region) {\n throw new Error(\n 'CloudWatchLogExporter: `region` is required (pass it explicitly or set AWS_REGION).',\n );\n }\n const logGroup = config.logGroup ?? process.env.AWS_LAMBDA_LOG_GROUP_NAME;\n const logStream = config.logStream ?? process.env.AWS_LAMBDA_LOG_STREAM_NAME;\n if (!logGroup || !logStream) {\n throw new Error(\n 'CloudWatchLogExporter: `logGroup` and `logStream` are required ' +\n '(in Lambda they come from AWS_LAMBDA_LOG_GROUP_NAME / AWS_LAMBDA_LOG_STREAM_NAME).',\n );\n }\n this.region = region;\n this.endpoint = config.endpoint ?? cloudWatchLogsEndpoint({ region });\n this.credentials = config.credentials;\n this.logGroup = logGroup;\n this.logStream = logStream;\n this.truncatableFields = config.truncatableFields;\n this.timeoutMs = config.timeoutMs ?? 10_000;\n this.fetchImpl = config.fetchImpl ?? globalThis.fetch;\n if (typeof this.fetchImpl !== 'function') {\n throw new TypeError(\n 'CloudWatchLogExporter: global `fetch` is not available — pass `fetchImpl` explicitly.',\n );\n }\n }\n\n export(\n logs: ReadableLogRecord[],\n resultCallback: (result: ExportResult) => void,\n ): void {\n if (this.shutdownOnce) {\n resultCallback({\n code: ExportResultCode.FAILED,\n error: new Error('CloudWatchLogExporter: already shut down'),\n });\n return;\n }\n if (logs.length === 0) {\n resultCallback({ code: ExportResultCode.SUCCESS });\n return;\n }\n\n this.sendBatch(logs).then(\n () => resultCallback({ code: ExportResultCode.SUCCESS }),\n (error: unknown) =>\n resultCallback({\n code: ExportResultCode.FAILED,\n error: error instanceof Error ? error : new Error(String(error)),\n }),\n );\n }\n\n async shutdown(): Promise<void> {\n this.shutdownOnce = true;\n }\n\n async forceFlush(): Promise<void> {}\n\n private async sendBatch(logs: ReadableLogRecord[]): Promise<void> {\n const body = JsonLogsSerializer.serializeRequest(logs);\n if (!body) {\n throw new Error('CloudWatchLogExporter: serializer produced no body');\n }\n\n const additionalHeaders: Record<string, string> = {\n 'x-aws-log-group': this.logGroup,\n 'x-aws-log-stream': this.logStream,\n };\n if (this.truncatableFields) {\n additionalHeaders['x-aws-truncatable-fields'] = this.truncatableFields;\n }\n\n const headers = await signCloudWatchOtlpRequest({\n url: this.endpoint,\n body,\n region: this.region,\n signal: 'logs',\n credentials: this.credentials,\n contentType: 'application/json',\n additionalHeaders,\n });\n\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), this.timeoutMs);\n\n let response: Response;\n try {\n response = await this.fetchImpl(this.endpoint, {\n method: 'POST',\n headers,\n body,\n signal: controller.signal,\n });\n } finally {\n clearTimeout(timer);\n }\n\n if (!response.ok) {\n const text = await safeReadText(response);\n throw new Error(\n `CloudWatchLogExporter: HTTP ${response.status} ${response.statusText} ${text}`.trim(),\n );\n }\n }\n}\n\nasync function safeReadText(response: Response): Promise<string> {\n try {\n return await response.text();\n } catch {\n return '';\n }\n}\n","/**\n * PushMetricExporter that ships OTLP/JSON metrics to the CloudWatch\n * metrics endpoint (https://monitoring.<region>.amazonaws.com/v1/metrics)\n * using SigV4.\n *\n * Lands in: CloudWatch Metrics — queryable via PromQL alongside the AWS\n * vended namespaces.\n */\n\nimport type { ExportResult } from '@opentelemetry/core';\nimport { ExportResultCode } from '@opentelemetry/core';\nimport { JsonMetricsSerializer } from '@opentelemetry/otlp-transformer';\nimport {\n AggregationTemporality,\n type AggregationTemporalitySelector,\n type InstrumentType,\n type PushMetricExporter,\n type ResourceMetrics,\n} from '@opentelemetry/sdk-metrics';\n\nimport { cloudWatchMetricsEndpoint } from './endpoints';\nimport {\n signCloudWatchOtlpRequest,\n type AwsCredentialsProvider,\n} from './sigv4';\n\nexport interface CloudWatchMetricExporterConfig {\n region?: string;\n endpoint?: string;\n credentials?: AwsCredentialsProvider;\n timeoutMs?: number;\n fetchImpl?: typeof fetch;\n /**\n * Aggregation temporality. CloudWatch's OTLP metrics endpoint accepts\n * delta and cumulative; delta is generally cheaper for counters.\n */\n temporalitySelector?: AggregationTemporalitySelector;\n}\n\nconst DEFAULT_TEMPORALITY: AggregationTemporalitySelector = (\n _instrumentType: InstrumentType,\n) => AggregationTemporality.DELTA;\n\nexport class CloudWatchMetricExporter implements PushMetricExporter {\n private readonly region: string;\n private readonly endpoint: string;\n private readonly credentials?: AwsCredentialsProvider;\n private readonly timeoutMs: number;\n private readonly fetchImpl: typeof fetch;\n private readonly temporalitySelector: AggregationTemporalitySelector;\n private shutdownOnce = false;\n\n constructor(config: CloudWatchMetricExporterConfig = {}) {\n const region = config.region ?? process.env.AWS_REGION;\n if (!region) {\n throw new Error(\n 'CloudWatchMetricExporter: `region` is required (pass it explicitly or set AWS_REGION).',\n );\n }\n this.region = region;\n this.endpoint = config.endpoint ?? cloudWatchMetricsEndpoint({ region });\n this.credentials = config.credentials;\n this.timeoutMs = config.timeoutMs ?? 10_000;\n this.fetchImpl = config.fetchImpl ?? globalThis.fetch;\n this.temporalitySelector =\n config.temporalitySelector ?? DEFAULT_TEMPORALITY;\n if (typeof this.fetchImpl !== 'function') {\n throw new TypeError(\n 'CloudWatchMetricExporter: global `fetch` is not available — pass `fetchImpl` explicitly.',\n );\n }\n }\n\n export(\n metrics: ResourceMetrics,\n resultCallback: (result: ExportResult) => void,\n ): void {\n if (this.shutdownOnce) {\n resultCallback({\n code: ExportResultCode.FAILED,\n error: new Error('CloudWatchMetricExporter: already shut down'),\n });\n return;\n }\n\n this.sendBatch(metrics).then(\n () => resultCallback({ code: ExportResultCode.SUCCESS }),\n (error: unknown) =>\n resultCallback({\n code: ExportResultCode.FAILED,\n error: error instanceof Error ? error : new Error(String(error)),\n }),\n );\n }\n\n async shutdown(): Promise<void> {\n this.shutdownOnce = true;\n }\n\n async forceFlush(): Promise<void> {}\n\n selectAggregationTemporality(\n instrumentType: InstrumentType,\n ): AggregationTemporality {\n return this.temporalitySelector(instrumentType);\n }\n\n private async sendBatch(metrics: ResourceMetrics): Promise<void> {\n const body = JsonMetricsSerializer.serializeRequest(metrics);\n if (!body) {\n throw new Error('CloudWatchMetricExporter: serializer produced no body');\n }\n\n const headers = await signCloudWatchOtlpRequest({\n url: this.endpoint,\n body,\n region: this.region,\n signal: 'metrics',\n credentials: this.credentials,\n contentType: 'application/json',\n });\n\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), this.timeoutMs);\n\n let response: Response;\n try {\n response = await this.fetchImpl(this.endpoint, {\n method: 'POST',\n headers,\n body,\n signal: controller.signal,\n });\n } finally {\n clearTimeout(timer);\n }\n\n if (!response.ok) {\n const text = await safeReadText(response);\n throw new Error(\n `CloudWatchMetricExporter: HTTP ${response.status} ${response.statusText} ${text}`.trim(),\n );\n }\n }\n}\n\nasync function safeReadText(response: Response): Promise<string> {\n try {\n return await response.text();\n } catch {\n return '';\n }\n}\n"]}
|