@rawdash/connector-aws-cloudwatch 0.15.0 → 0.17.0
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/LICENSE +202 -0
- package/README.md +59 -81
- package/dist/index.d.ts +103 -43
- package/dist/index.js +290 -197
- package/dist/index.js.map +1 -1
- package/package.json +15 -13
package/dist/index.js
CHANGED
|
@@ -1,61 +1,6 @@
|
|
|
1
|
-
//
|
|
2
|
-
|
|
3
|
-
response;
|
|
4
|
-
constructor(message, response) {
|
|
5
|
-
super(message);
|
|
6
|
-
this.name = new.target.name;
|
|
7
|
-
this.response = response;
|
|
8
|
-
}
|
|
9
|
-
};
|
|
10
|
-
var TransientError = class extends HttpClientError {
|
|
11
|
-
kind = "transient";
|
|
12
|
-
};
|
|
13
|
-
var RateLimitError = class extends HttpClientError {
|
|
14
|
-
kind = "rate_limit";
|
|
15
|
-
retryAfter;
|
|
16
|
-
constructor(message, response, retryAfter) {
|
|
17
|
-
super(message, response);
|
|
18
|
-
this.retryAfter = retryAfter;
|
|
19
|
-
}
|
|
20
|
-
};
|
|
21
|
-
var AuthError = class extends HttpClientError {
|
|
22
|
-
kind = "auth";
|
|
23
|
-
};
|
|
24
|
-
var HTTP_CLIENT_VERSION = "0.0.0";
|
|
25
|
-
var DEFAULT_USER_AGENT = `rawdash-connector/${HTTP_CLIENT_VERSION} (+https://rawdash.dev)`;
|
|
26
|
-
function connectorUserAgent(connectorId) {
|
|
27
|
-
return `rawdash-connector-${connectorId}/${HTTP_CLIENT_VERSION} (+https://rawdash.dev)`;
|
|
28
|
-
}
|
|
29
|
-
function parseEpoch(value, unit) {
|
|
30
|
-
if (value === null || value === void 0) {
|
|
31
|
-
return null;
|
|
32
|
-
}
|
|
33
|
-
if (unit === "iso") {
|
|
34
|
-
if (typeof value !== "string") {
|
|
35
|
-
return null;
|
|
36
|
-
}
|
|
37
|
-
const ms = new Date(value).getTime();
|
|
38
|
-
return Number.isFinite(ms) ? ms : null;
|
|
39
|
-
}
|
|
40
|
-
if (typeof value === "string" && value.trim() === "") {
|
|
41
|
-
return null;
|
|
42
|
-
}
|
|
43
|
-
const n = typeof value === "number" ? value : Number(value);
|
|
44
|
-
if (!Number.isFinite(n)) {
|
|
45
|
-
return null;
|
|
46
|
-
}
|
|
47
|
-
const result = unit === "s" ? n * 1e3 : n;
|
|
48
|
-
return Number.isFinite(result) ? result : null;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// src/aws-cloudwatch.ts
|
|
52
|
-
import {
|
|
53
|
-
BaseConnector,
|
|
54
|
-
defineConfigFields
|
|
55
|
-
} from "@rawdash/core";
|
|
1
|
+
// ../aws-shared/dist/index.js
|
|
2
|
+
import { BaseConnector } from "@rawdash/core";
|
|
56
3
|
import { z } from "zod";
|
|
57
|
-
|
|
58
|
-
// src/sigv4.ts
|
|
59
4
|
var encoder = new TextEncoder();
|
|
60
5
|
var ALGORITHM = "AWS4-HMAC-SHA256";
|
|
61
6
|
function u8(data) {
|
|
@@ -126,8 +71,6 @@ async function createAuthorizationHeader(params) {
|
|
|
126
71
|
const signature = toHex(await hmac(signingKey, stringToSign));
|
|
127
72
|
return `${ALGORITHM} Credential=${params.accessKeyId}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;
|
|
128
73
|
}
|
|
129
|
-
|
|
130
|
-
// src/xml.ts
|
|
131
74
|
function decodeEntities(value) {
|
|
132
75
|
return value.replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/'/g, "'").replace(/&/g, "&");
|
|
133
76
|
}
|
|
@@ -208,74 +151,55 @@ function parseAssumeRole(xml) {
|
|
|
208
151
|
function parseErrorCode(xml) {
|
|
209
152
|
return firstText(xml, "Code");
|
|
210
153
|
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
154
|
+
var HttpClientError = class extends Error {
|
|
155
|
+
response;
|
|
156
|
+
constructor(message, response) {
|
|
157
|
+
super(message);
|
|
158
|
+
this.name = new.target.name;
|
|
159
|
+
this.response = response;
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
var TransientError = class extends HttpClientError {
|
|
163
|
+
kind = "transient";
|
|
164
|
+
};
|
|
165
|
+
var RateLimitError = class extends HttpClientError {
|
|
166
|
+
kind = "rate_limit";
|
|
167
|
+
retryAfter;
|
|
168
|
+
constructor(message, response, retryAfter) {
|
|
169
|
+
super(message, response);
|
|
170
|
+
this.retryAfter = retryAfter;
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
var AuthError = class extends HttpClientError {
|
|
174
|
+
kind = "auth";
|
|
175
|
+
};
|
|
176
|
+
var HTTP_CLIENT_VERSION = "0.0.0";
|
|
177
|
+
var DEFAULT_USER_AGENT = `rawdash-connector/${HTTP_CLIENT_VERSION} (+https://rawdash.dev)`;
|
|
178
|
+
function connectorUserAgent(connectorId) {
|
|
179
|
+
return `rawdash-connector-${connectorId}/${HTTP_CLIENT_VERSION} (+https://rawdash.dev)`;
|
|
216
180
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
)
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
stat: z.string().min(1),
|
|
225
|
-
periodSeconds: z.number().int().min(60).refine((n) => n % 60 === 0, {
|
|
226
|
-
message: "periodSeconds must be a multiple of 60 (1 minute)"
|
|
227
|
-
}),
|
|
228
|
-
dimensions: z.record(z.string(), z.string()).optional()
|
|
229
|
-
});
|
|
230
|
-
var configFields = defineConfigFields(
|
|
231
|
-
z.object({
|
|
232
|
-
region: z.string().regex(
|
|
233
|
-
/^[a-z0-9-]+$/,
|
|
234
|
-
"region must look like an AWS region, e.g. us-east-1"
|
|
235
|
-
).meta({
|
|
236
|
-
label: "AWS Region",
|
|
237
|
-
description: "The AWS region whose CloudWatch metrics you want to read, e.g. us-east-1.",
|
|
238
|
-
placeholder: "us-east-1"
|
|
239
|
-
}),
|
|
240
|
-
accessKeyId: z.object({ $secret: z.string() }).optional().meta({
|
|
241
|
-
label: "Access Key ID",
|
|
242
|
-
description: "AWS access key ID for an IAM principal with cloudwatch:GetMetricData. Use this together with the secret access key for static-credential auth.",
|
|
243
|
-
secret: true
|
|
244
|
-
}),
|
|
245
|
-
secretAccessKey: z.object({ $secret: z.string() }).optional().meta({
|
|
246
|
-
label: "Secret Access Key",
|
|
247
|
-
description: "AWS secret access key paired with the access key ID above.",
|
|
248
|
-
secret: true
|
|
249
|
-
}),
|
|
250
|
-
roleArn: z.string().regex(
|
|
251
|
-
/^arn:aws:iam::\d{12}:role\/.+/,
|
|
252
|
-
"roleArn must be a full IAM role ARN, e.g. arn:aws:iam::123456789012:role/rawdash"
|
|
253
|
-
).optional().meta({
|
|
254
|
-
label: "Role ARN",
|
|
255
|
-
description: "IAM role to assume via STS instead of using static keys. The base credentials (the access key above, or the ambient AWS environment) must be allowed to sts:AssumeRole this role.",
|
|
256
|
-
placeholder: "arn:aws:iam::123456789012:role/rawdash-cloudwatch"
|
|
257
|
-
}),
|
|
258
|
-
externalId: z.string().min(1).optional().meta({
|
|
259
|
-
label: "External ID",
|
|
260
|
-
description: "External ID required by the trust policy of the role being assumed. Only used with Role ARN."
|
|
261
|
-
}),
|
|
262
|
-
metricQueries: z.array(metricQuerySchema).nonempty().meta({
|
|
263
|
-
label: "Metric queries",
|
|
264
|
-
description: "CloudWatch is too broad to mirror wholesale \u2014 declare the specific metrics to pull. Each query needs an id, namespace, metric name, statistic, and period (seconds, multiple of 60), with optional dimensions."
|
|
265
|
-
}),
|
|
266
|
-
lookbackMinutes: z.number().int().positive().max(40320).optional().meta({
|
|
267
|
-
label: "Lookback (minutes)",
|
|
268
|
-
description: "How far back to pull data points on a full sync when the host does not supply a since bound. Defaults to 180.",
|
|
269
|
-
placeholder: "180"
|
|
270
|
-
})
|
|
271
|
-
}).refine(
|
|
272
|
-
(val) => val.roleArn !== void 0 || val.accessKeyId !== void 0 && val.secretAccessKey !== void 0,
|
|
273
|
-
{
|
|
274
|
-
message: "Provide either accessKeyId + secretAccessKey (static credentials) or roleArn (role assumption)"
|
|
181
|
+
function parseEpoch(value, unit) {
|
|
182
|
+
if (value === null || value === void 0) {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
if (unit === "iso") {
|
|
186
|
+
if (typeof value !== "string") {
|
|
187
|
+
return null;
|
|
275
188
|
}
|
|
276
|
-
|
|
277
|
-
);
|
|
278
|
-
|
|
189
|
+
const ms = new Date(value).getTime();
|
|
190
|
+
return Number.isFinite(ms) ? ms : null;
|
|
191
|
+
}
|
|
192
|
+
if (typeof value === "string" && value.trim() === "") {
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
const n = typeof value === "number" ? value : Number(value);
|
|
196
|
+
if (!Number.isFinite(n)) {
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
const result = unit === "s" ? n * 1e3 : n;
|
|
200
|
+
return Number.isFinite(result) ? result : null;
|
|
201
|
+
}
|
|
202
|
+
var awsCredentialsSchema = {
|
|
279
203
|
accessKeyId: {
|
|
280
204
|
description: "AWS access key ID",
|
|
281
205
|
auth: "optional"
|
|
@@ -285,61 +209,18 @@ var cloudWatchCredentials = {
|
|
|
285
209
|
auth: "optional"
|
|
286
210
|
}
|
|
287
211
|
};
|
|
288
|
-
var metricDataResponseSchema = z.object({
|
|
289
|
-
MetricDataResults: z.array(
|
|
290
|
-
z.object({
|
|
291
|
-
Id: z.string(),
|
|
292
|
-
Label: z.string(),
|
|
293
|
-
Timestamps: z.array(z.iso.datetime()),
|
|
294
|
-
Values: z.array(z.number()),
|
|
295
|
-
StatusCode: z.enum([
|
|
296
|
-
"Complete",
|
|
297
|
-
"InternalError",
|
|
298
|
-
"PartialData",
|
|
299
|
-
"Forbidden"
|
|
300
|
-
])
|
|
301
|
-
})
|
|
302
|
-
),
|
|
303
|
-
NextToken: z.string().optional()
|
|
304
|
-
});
|
|
305
|
-
var CLOUDWATCH_SERVICE = "monitoring";
|
|
306
|
-
var CLOUDWATCH_API_VERSION = "2010-08-01";
|
|
307
212
|
var STS_SERVICE = "sts";
|
|
308
213
|
var STS_API_VERSION = "2011-06-15";
|
|
309
|
-
var MAX_QUERIES_PER_CALL = 500;
|
|
310
|
-
var DEFAULT_LOOKBACK_MINUTES = 180;
|
|
311
214
|
var ASSUMED_ROLE_TTL_BUFFER_MS = 6e4;
|
|
312
215
|
var ASSUME_ROLE_DURATION_SECONDS = 3600;
|
|
313
|
-
var MS_PER_MINUTE = 6e4;
|
|
314
216
|
var FORM_CONTENT_TYPE = "application/x-www-form-urlencoded; charset=utf-8";
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
const parsed = configFields.parse(input);
|
|
322
|
-
return new _CloudWatchConnector(
|
|
323
|
-
{
|
|
324
|
-
region: parsed.region,
|
|
325
|
-
roleArn: parsed.roleArn,
|
|
326
|
-
externalId: parsed.externalId,
|
|
327
|
-
metricQueries: parsed.metricQueries,
|
|
328
|
-
lookbackMinutes: parsed.lookbackMinutes
|
|
329
|
-
},
|
|
330
|
-
{
|
|
331
|
-
accessKeyId: parsed.accessKeyId,
|
|
332
|
-
secretAccessKey: parsed.secretAccessKey
|
|
333
|
-
},
|
|
334
|
-
ctx
|
|
335
|
-
);
|
|
336
|
-
}
|
|
337
|
-
id = "aws-cloudwatch";
|
|
338
|
-
credentials = cloudWatchCredentials;
|
|
217
|
+
function readEnv(name) {
|
|
218
|
+
const env = globalThis.process?.env;
|
|
219
|
+
return env?.[name];
|
|
220
|
+
}
|
|
221
|
+
var BaseAWSConnector = class extends BaseConnector {
|
|
222
|
+
credentials = awsCredentialsSchema;
|
|
339
223
|
assumedCreds = null;
|
|
340
|
-
// -------------------------------------------------------------------------
|
|
341
|
-
// Credential resolution
|
|
342
|
-
// -------------------------------------------------------------------------
|
|
343
224
|
baseCredentials() {
|
|
344
225
|
const { accessKeyId, secretAccessKey } = this.creds;
|
|
345
226
|
if (accessKeyId && secretAccessKey) {
|
|
@@ -355,7 +236,7 @@ var CloudWatchConnector = class _CloudWatchConnector extends BaseConnector {
|
|
|
355
236
|
};
|
|
356
237
|
}
|
|
357
238
|
throw new AuthError(
|
|
358
|
-
|
|
239
|
+
`${this.id}: no AWS credentials available \u2014 provide accessKeyId + secretAccessKey, or set them in the environment for role assumption`
|
|
359
240
|
);
|
|
360
241
|
}
|
|
361
242
|
async resolveSigningCredentials(signal) {
|
|
@@ -363,7 +244,7 @@ var CloudWatchConnector = class _CloudWatchConnector extends BaseConnector {
|
|
|
363
244
|
const { accessKeyId, secretAccessKey } = this.creds;
|
|
364
245
|
if (!accessKeyId || !secretAccessKey) {
|
|
365
246
|
throw new AuthError(
|
|
366
|
-
|
|
247
|
+
`${this.id}: static-credential auth requires both accessKeyId and secretAccessKey`
|
|
367
248
|
);
|
|
368
249
|
}
|
|
369
250
|
return { accessKeyId, secretAccessKey };
|
|
@@ -371,15 +252,14 @@ var CloudWatchConnector = class _CloudWatchConnector extends BaseConnector {
|
|
|
371
252
|
if (this.assumedCreds && Date.now() < this.assumedCreds.expiresAt) {
|
|
372
253
|
return this.assumedCreds.value;
|
|
373
254
|
}
|
|
374
|
-
|
|
375
|
-
return assumed;
|
|
255
|
+
return this.assumeRole(this.settings.roleArn, signal);
|
|
376
256
|
}
|
|
377
257
|
async assumeRole(roleArn, signal) {
|
|
378
258
|
const params = new URLSearchParams();
|
|
379
259
|
params.set("Action", "AssumeRole");
|
|
380
260
|
params.set("Version", STS_API_VERSION);
|
|
381
261
|
params.set("RoleArn", roleArn);
|
|
382
|
-
params.set("RoleSessionName",
|
|
262
|
+
params.set("RoleSessionName", `rawdash-${this.id}`);
|
|
383
263
|
params.set("DurationSeconds", String(ASSUME_ROLE_DURATION_SECONDS));
|
|
384
264
|
if (this.settings.externalId !== void 0) {
|
|
385
265
|
params.set("ExternalId", this.settings.externalId);
|
|
@@ -396,7 +276,7 @@ var CloudWatchConnector = class _CloudWatchConnector extends BaseConnector {
|
|
|
396
276
|
const parsed = parseAssumeRole(xml);
|
|
397
277
|
if (parsed === null) {
|
|
398
278
|
throw new AuthError(
|
|
399
|
-
|
|
279
|
+
`${this.id}: STS AssumeRole returned no usable credentials`
|
|
400
280
|
);
|
|
401
281
|
}
|
|
402
282
|
this.cacheAssumedCredentials(parsed);
|
|
@@ -418,9 +298,6 @@ var CloudWatchConnector = class _CloudWatchConnector extends BaseConnector {
|
|
|
418
298
|
expiresAt
|
|
419
299
|
};
|
|
420
300
|
}
|
|
421
|
-
// -------------------------------------------------------------------------
|
|
422
|
-
// Signed transport
|
|
423
|
-
// -------------------------------------------------------------------------
|
|
424
301
|
async signedPost(args) {
|
|
425
302
|
const { amzDate, dateStamp } = formatAmzDate(/* @__PURE__ */ new Date());
|
|
426
303
|
const payloadHash = await sha256Hex(args.body);
|
|
@@ -451,7 +328,7 @@ var CloudWatchConnector = class _CloudWatchConnector extends BaseConnector {
|
|
|
451
328
|
"content-type": FORM_CONTENT_TYPE,
|
|
452
329
|
"x-amz-content-sha256": payloadHash,
|
|
453
330
|
"x-amz-date": amzDate,
|
|
454
|
-
"user-agent": connectorUserAgent(
|
|
331
|
+
"user-agent": connectorUserAgent(this.id),
|
|
455
332
|
Authorization: authorization
|
|
456
333
|
};
|
|
457
334
|
if (args.signingCredentials.sessionToken !== void 0) {
|
|
@@ -474,9 +351,6 @@ var CloudWatchConnector = class _CloudWatchConnector extends BaseConnector {
|
|
|
474
351
|
throw this.classifyAwsError(err);
|
|
475
352
|
}
|
|
476
353
|
}
|
|
477
|
-
// CloudWatch and STS return AWS error codes inside the (XML) body even on a
|
|
478
|
-
// 400 — map the documented ones to the shared error taxonomy so the host
|
|
479
|
-
// backs off / pauses / retries correctly.
|
|
480
354
|
classifyAwsError(err) {
|
|
481
355
|
if (!(err instanceof Error) || !("kind" in err)) {
|
|
482
356
|
return err;
|
|
@@ -498,13 +372,231 @@ var CloudWatchConnector = class _CloudWatchConnector extends BaseConnector {
|
|
|
498
372
|
}
|
|
499
373
|
return err;
|
|
500
374
|
}
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
375
|
+
};
|
|
376
|
+
var awsAuthConfigShape = {
|
|
377
|
+
region: z.string().regex(
|
|
378
|
+
/^[a-z0-9-]+$/,
|
|
379
|
+
"region must look like an AWS region, e.g. us-east-1"
|
|
380
|
+
).meta({
|
|
381
|
+
label: "AWS Region",
|
|
382
|
+
description: "The AWS region whose service endpoint you want to call, e.g. us-east-1.",
|
|
383
|
+
placeholder: "us-east-1"
|
|
384
|
+
}),
|
|
385
|
+
accessKeyId: z.object({ $secret: z.string() }).optional().meta({
|
|
386
|
+
label: "Access Key ID",
|
|
387
|
+
description: "AWS access key ID for an IAM principal with permission to call the relevant service. Use together with the secret access key for static-credential auth.",
|
|
388
|
+
secret: true
|
|
389
|
+
}),
|
|
390
|
+
secretAccessKey: z.object({ $secret: z.string() }).optional().meta({
|
|
391
|
+
label: "Secret Access Key",
|
|
392
|
+
description: "AWS secret access key paired with the access key ID above.",
|
|
393
|
+
secret: true
|
|
394
|
+
}),
|
|
395
|
+
roleArn: z.string().regex(
|
|
396
|
+
/^arn:aws:iam::\d{12}:role\/.+/,
|
|
397
|
+
"roleArn must be a full IAM role ARN, e.g. arn:aws:iam::123456789012:role/rawdash"
|
|
398
|
+
).optional().meta({
|
|
399
|
+
label: "Role ARN",
|
|
400
|
+
description: "IAM role to assume via STS instead of using static keys. The base credentials (the access key above, or the ambient AWS environment) must be allowed to sts:AssumeRole this role.",
|
|
401
|
+
placeholder: "arn:aws:iam::123456789012:role/rawdash"
|
|
402
|
+
}),
|
|
403
|
+
externalId: z.string().min(1).optional().meta({
|
|
404
|
+
label: "External ID",
|
|
405
|
+
description: "External ID required by the trust policy of the role being assumed. Only used with Role ARN."
|
|
406
|
+
})
|
|
407
|
+
};
|
|
408
|
+
var awsAuthRefine = {
|
|
409
|
+
predicate: (val) => {
|
|
410
|
+
const hasRole = val.roleArn !== void 0;
|
|
411
|
+
const hasStatic = val.accessKeyId !== void 0 && val.secretAccessKey !== void 0;
|
|
412
|
+
if (val.externalId !== void 0 && !hasRole) {
|
|
413
|
+
return false;
|
|
414
|
+
}
|
|
415
|
+
return hasRole || hasStatic;
|
|
416
|
+
},
|
|
417
|
+
message: "Provide either accessKeyId + secretAccessKey (static credentials) or roleArn (role assumption). externalId requires roleArn."
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
// ../../connector-shared/dist/index.js
|
|
421
|
+
var HTTP_CLIENT_VERSION2 = "0.0.0";
|
|
422
|
+
var DEFAULT_USER_AGENT2 = `rawdash-connector/${HTTP_CLIENT_VERSION2} (+https://rawdash.dev)`;
|
|
423
|
+
function parseEpoch2(value, unit) {
|
|
424
|
+
if (value === null || value === void 0) {
|
|
425
|
+
return null;
|
|
426
|
+
}
|
|
427
|
+
if (unit === "iso") {
|
|
428
|
+
if (typeof value !== "string") {
|
|
429
|
+
return null;
|
|
430
|
+
}
|
|
431
|
+
const ms = new Date(value).getTime();
|
|
432
|
+
return Number.isFinite(ms) ? ms : null;
|
|
433
|
+
}
|
|
434
|
+
if (typeof value === "string" && value.trim() === "") {
|
|
435
|
+
return null;
|
|
436
|
+
}
|
|
437
|
+
const n = typeof value === "number" ? value : Number(value);
|
|
438
|
+
if (!Number.isFinite(n)) {
|
|
439
|
+
return null;
|
|
440
|
+
}
|
|
441
|
+
const result = unit === "s" ? n * 1e3 : n;
|
|
442
|
+
return Number.isFinite(result) ? result : null;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// src/aws-cloudwatch.ts
|
|
446
|
+
import {
|
|
447
|
+
defineConfigFields,
|
|
448
|
+
defineConnectorDoc,
|
|
449
|
+
defineResources,
|
|
450
|
+
schemasFromResources
|
|
451
|
+
} from "@rawdash/core";
|
|
452
|
+
import { z as z2 } from "zod";
|
|
453
|
+
var metricQuerySchema = z2.object({
|
|
454
|
+
id: z2.string().regex(
|
|
455
|
+
/^[a-z][a-zA-Z0-9_]*$/,
|
|
456
|
+
"CloudWatch query id must start with a lowercase letter and contain only letters, digits, and underscores"
|
|
457
|
+
),
|
|
458
|
+
namespace: z2.string().min(1),
|
|
459
|
+
metric: z2.string().min(1),
|
|
460
|
+
stat: z2.string().min(1),
|
|
461
|
+
periodSeconds: z2.number().int().min(60).refine((n) => n % 60 === 0, {
|
|
462
|
+
message: "periodSeconds must be a multiple of 60 (1 minute)"
|
|
463
|
+
}),
|
|
464
|
+
dimensions: z2.record(z2.string(), z2.string()).optional()
|
|
465
|
+
});
|
|
466
|
+
var configFields = defineConfigFields(
|
|
467
|
+
z2.object({
|
|
468
|
+
...awsAuthConfigShape,
|
|
469
|
+
metricQueries: z2.array(metricQuerySchema).nonempty().meta({
|
|
470
|
+
label: "Metric queries",
|
|
471
|
+
description: "CloudWatch is too broad to mirror wholesale; declare the specific metrics to pull. Each query needs an id, namespace, metric name, statistic, and period (seconds, multiple of 60), with optional dimensions."
|
|
472
|
+
}),
|
|
473
|
+
lookbackMinutes: z2.number().int().positive().max(40320).optional().meta({
|
|
474
|
+
label: "Lookback (minutes)",
|
|
475
|
+
description: "How far back to pull data points on a full sync when the host does not supply a since bound. Defaults to 180.",
|
|
476
|
+
placeholder: "180"
|
|
477
|
+
})
|
|
478
|
+
}).refine(awsAuthRefine.predicate, { message: awsAuthRefine.message }).refine(
|
|
479
|
+
(cfg) => new Set(cfg.metricQueries.map((q) => q.id)).size === cfg.metricQueries.length,
|
|
480
|
+
{
|
|
481
|
+
path: ["metricQueries"],
|
|
482
|
+
message: "Each metric query id must be unique"
|
|
483
|
+
}
|
|
484
|
+
)
|
|
485
|
+
);
|
|
486
|
+
var doc = defineConnectorDoc({
|
|
487
|
+
displayName: "AWS CloudWatch",
|
|
488
|
+
category: "infrastructure",
|
|
489
|
+
brandColor: "#FF4F8B",
|
|
490
|
+
tagline: "Pull declared CloudWatch metric time series (any namespace, statistic, and period) into a single metric series per query.",
|
|
491
|
+
rateLimit: "GetMetricData is batched at most 500 metrics per call with NextToken pagination; throttling (Throttling / RequestLimitExceeded / TooManyRequests) is retried with backoff.",
|
|
492
|
+
vendor: {
|
|
493
|
+
name: "Amazon Web Services",
|
|
494
|
+
apiDocs: "https://docs.aws.amazon.com/AmazonCloudWatch/latest/APIReference/API_GetMetricData.html",
|
|
495
|
+
website: "https://aws.amazon.com/cloudwatch/"
|
|
496
|
+
},
|
|
497
|
+
auth: {
|
|
498
|
+
summary: "Authenticate with either static IAM access keys or an assumed IAM role (STS). The principal needs cloudwatch:GetMetricData on the target region.",
|
|
499
|
+
setup: [
|
|
500
|
+
"Create an IAM user or role with a policy granting `cloudwatch:GetMetricData`.",
|
|
501
|
+
"For static credentials, generate an access key ID and secret access key for that IAM user and store them as secrets.",
|
|
502
|
+
"For role assumption, set `roleArn` to the role to assume (and `externalId` if its trust policy requires one); the base credentials must be allowed to `sts:AssumeRole` it.",
|
|
503
|
+
"Set `region` to the AWS region whose CloudWatch endpoint holds the metrics, e.g. `us-east-1`.",
|
|
504
|
+
'Reference the keys from config, e.g. `accessKeyId: secret("AWS_ACCESS_KEY_ID")` and `secretAccessKey: secret("AWS_SECRET_ACCESS_KEY")`.'
|
|
505
|
+
]
|
|
506
|
+
},
|
|
507
|
+
limitations: [
|
|
508
|
+
"CloudWatch is too broad to mirror wholesale; only the metrics declared in `metricQueries` are synced; there is no automatic metric discovery.",
|
|
509
|
+
"The series name is derived from the query namespace/metric, so two queries against the same metric with different statistics or dimensions share one series name and are distinguished only by sample attributes.",
|
|
510
|
+
"Each query period must be a multiple of 60 seconds; sub-minute resolution is not supported.",
|
|
511
|
+
"A full sync uses lookbackMinutes; a latest sync uses a short window covering the last few periods."
|
|
512
|
+
]
|
|
513
|
+
});
|
|
514
|
+
var metricDataResponseSchema = z2.object({
|
|
515
|
+
MetricDataResults: z2.array(
|
|
516
|
+
z2.object({
|
|
517
|
+
Id: z2.string(),
|
|
518
|
+
Label: z2.string(),
|
|
519
|
+
Timestamps: z2.array(z2.iso.datetime()),
|
|
520
|
+
Values: z2.array(z2.number()),
|
|
521
|
+
StatusCode: z2.enum([
|
|
522
|
+
"Complete",
|
|
523
|
+
"InternalError",
|
|
524
|
+
"PartialData",
|
|
525
|
+
"Forbidden"
|
|
526
|
+
])
|
|
527
|
+
})
|
|
528
|
+
),
|
|
529
|
+
NextToken: z2.string().optional()
|
|
530
|
+
});
|
|
531
|
+
var awsCloudwatchResources = defineResources({
|
|
532
|
+
"<namespace>/<metric>": {
|
|
533
|
+
shape: "metric",
|
|
534
|
+
dynamic: true,
|
|
535
|
+
description: "One metric series per declared metric query. The series name is the query namespace/metric (e.g. `AWS/EC2/CPUUtilization`), so the actual keys depend on the configured `metricQueries`. Each sample carries the query statistic, period, query id, the upstream status code, and label as attributes.",
|
|
536
|
+
endpoint: "POST / (GetMetricData)",
|
|
537
|
+
granularity: "Per query period (periodSeconds, a multiple of 60)",
|
|
538
|
+
notes: "Each sync replaces the full set of samples for the metric names it owns (idempotent).",
|
|
539
|
+
dimensions: [
|
|
540
|
+
{
|
|
541
|
+
name: "stat",
|
|
542
|
+
description: "The CloudWatch statistic requested for the query, e.g. Average, Sum, or p99."
|
|
543
|
+
},
|
|
544
|
+
{
|
|
545
|
+
name: "period",
|
|
546
|
+
description: "The aggregation period in seconds for the data points."
|
|
547
|
+
},
|
|
548
|
+
{
|
|
549
|
+
name: "queryId",
|
|
550
|
+
description: "The configured id of the metric query that produced the sample."
|
|
551
|
+
},
|
|
552
|
+
{
|
|
553
|
+
name: "statusCode",
|
|
554
|
+
description: "GetMetricData result status for the series (Complete, PartialData, InternalError, or Forbidden)."
|
|
555
|
+
},
|
|
556
|
+
{
|
|
557
|
+
name: "label",
|
|
558
|
+
description: "The human-readable label CloudWatch returned for the series."
|
|
559
|
+
}
|
|
560
|
+
],
|
|
561
|
+
responses: { metric_data: metricDataResponseSchema }
|
|
562
|
+
}
|
|
563
|
+
});
|
|
564
|
+
var CLOUDWATCH_SERVICE = "monitoring";
|
|
565
|
+
var CLOUDWATCH_API_VERSION = "2010-08-01";
|
|
566
|
+
var MAX_QUERIES_PER_CALL = 500;
|
|
567
|
+
var DEFAULT_LOOKBACK_MINUTES = 180;
|
|
568
|
+
var MS_PER_MINUTE = 6e4;
|
|
569
|
+
var id = "aws-cloudwatch";
|
|
570
|
+
var cost = {
|
|
571
|
+
warning: "CloudWatch GetMetricData is billed per metric requested on the paid tier; high-frequency syncs over many metrics add up."
|
|
572
|
+
};
|
|
573
|
+
var CloudWatchConnector = class _CloudWatchConnector extends BaseAWSConnector {
|
|
574
|
+
static id = id;
|
|
575
|
+
static resources = awsCloudwatchResources;
|
|
576
|
+
static schemas = schemasFromResources(awsCloudwatchResources);
|
|
577
|
+
static cost = cost;
|
|
578
|
+
static create(input, ctx) {
|
|
579
|
+
const parsed = configFields.parse(input);
|
|
580
|
+
return new _CloudWatchConnector(
|
|
581
|
+
{
|
|
582
|
+
region: parsed.region,
|
|
583
|
+
roleArn: parsed.roleArn,
|
|
584
|
+
externalId: parsed.externalId,
|
|
585
|
+
metricQueries: parsed.metricQueries,
|
|
586
|
+
lookbackMinutes: parsed.lookbackMinutes
|
|
587
|
+
},
|
|
588
|
+
{
|
|
589
|
+
accessKeyId: parsed.accessKeyId,
|
|
590
|
+
secretAccessKey: parsed.secretAccessKey
|
|
591
|
+
},
|
|
592
|
+
ctx
|
|
593
|
+
);
|
|
594
|
+
}
|
|
595
|
+
id = id;
|
|
504
596
|
computeWindow(options) {
|
|
505
597
|
const endMs = Date.now();
|
|
506
598
|
if (options.since) {
|
|
507
|
-
const sinceMs =
|
|
599
|
+
const sinceMs = parseEpoch2(options.since, "iso");
|
|
508
600
|
if (sinceMs !== null) {
|
|
509
601
|
return { startMs: Math.min(sinceMs, endMs), endMs };
|
|
510
602
|
}
|
|
@@ -546,9 +638,6 @@ var CloudWatchConnector = class _CloudWatchConnector extends BaseConnector {
|
|
|
546
638
|
});
|
|
547
639
|
return params.toString();
|
|
548
640
|
}
|
|
549
|
-
// -------------------------------------------------------------------------
|
|
550
|
-
// sync
|
|
551
|
-
// -------------------------------------------------------------------------
|
|
552
641
|
async sync(options, storage, signal) {
|
|
553
642
|
const queries = this.settings.metricQueries;
|
|
554
643
|
const names = new Set(queries.map((q) => `${q.namespace}/${q.metric}`));
|
|
@@ -557,7 +646,6 @@ var CloudWatchConnector = class _CloudWatchConnector extends BaseConnector {
|
|
|
557
646
|
}
|
|
558
647
|
const queriesById = new Map(queries.map((q) => [q.id, q]));
|
|
559
648
|
const { startMs, endMs } = this.computeWindow(options);
|
|
560
|
-
const signingCredentials = await this.resolveSigningCredentials(signal);
|
|
561
649
|
const samples = [];
|
|
562
650
|
const host = `${CLOUDWATCH_SERVICE}.${this.settings.region}.amazonaws.com`;
|
|
563
651
|
for (let i = 0; i < queries.length; i += MAX_QUERIES_PER_CALL) {
|
|
@@ -574,6 +662,7 @@ var CloudWatchConnector = class _CloudWatchConnector extends BaseConnector {
|
|
|
574
662
|
endMs,
|
|
575
663
|
nextToken
|
|
576
664
|
);
|
|
665
|
+
const signingCredentials = await this.resolveSigningCredentials(signal);
|
|
577
666
|
const xml = await this.signedPost({
|
|
578
667
|
host,
|
|
579
668
|
service: CLOUDWATCH_SERVICE,
|
|
@@ -619,7 +708,7 @@ var CloudWatchConnector = class _CloudWatchConnector extends BaseConnector {
|
|
|
619
708
|
};
|
|
620
709
|
const count = Math.min(result.timestamps.length, result.values.length);
|
|
621
710
|
for (let i = 0; i < count; i++) {
|
|
622
|
-
const ts =
|
|
711
|
+
const ts = parseEpoch2(result.timestamps[i], "iso");
|
|
623
712
|
const value = result.values[i];
|
|
624
713
|
if (ts === null || !Number.isFinite(value)) {
|
|
625
714
|
continue;
|
|
@@ -634,6 +723,10 @@ var index_default = CloudWatchConnector;
|
|
|
634
723
|
export {
|
|
635
724
|
CloudWatchConnector,
|
|
636
725
|
configFields,
|
|
637
|
-
|
|
726
|
+
cost,
|
|
727
|
+
index_default as default,
|
|
728
|
+
doc,
|
|
729
|
+
id,
|
|
730
|
+
awsCloudwatchResources as resources
|
|
638
731
|
};
|
|
639
732
|
//# sourceMappingURL=index.js.map
|