@aws-blocks/bb-metrics 0.1.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 +174 -0
- package/README.md +186 -0
- package/dist/errors.d.ts +28 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +29 -0
- package/dist/index.aws.d.ts +134 -0
- package/dist/index.aws.d.ts.map +1 -0
- package/dist/index.aws.js +270 -0
- package/dist/index.browser.d.ts +12 -0
- package/dist/index.browser.d.ts.map +1 -0
- package/dist/index.browser.js +13 -0
- package/dist/index.cdk.d.ts +20 -0
- package/dist/index.cdk.d.ts.map +1 -0
- package/dist/index.cdk.js +24 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.mock.d.ts +10 -0
- package/dist/index.mock.d.ts.map +1 -0
- package/dist/index.mock.js +10 -0
- package/dist/index.test.d.ts +2 -0
- package/dist/index.test.d.ts.map +1 -0
- package/dist/index.test.js +495 -0
- package/dist/types.d.ts +97 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/validation.d.ts +31 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +87 -0
- package/package.json +41 -0
- package/src/errors.ts +30 -0
- package/src/index.aws.ts +351 -0
- package/src/index.browser.ts +29 -0
- package/src/index.cdk.ts +40 -0
- package/src/index.mock.ts +20 -0
- package/src/index.test.ts +632 -0
- package/src/index.ts +13 -0
- package/src/types.ts +119 -0
- package/src/validation.ts +97 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
import { MetricsErrors } from './errors.js';
|
|
4
|
+
function blocksError(name, message) {
|
|
5
|
+
const err = new Error(`${name}: ${message}`);
|
|
6
|
+
err.name = name;
|
|
7
|
+
return err;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Validate a metric name against CloudWatch constraints.
|
|
11
|
+
* - Must be non-empty
|
|
12
|
+
* - Max 1024 characters
|
|
13
|
+
*/
|
|
14
|
+
export function validateMetricName(name) {
|
|
15
|
+
if (!name || name.length === 0) {
|
|
16
|
+
throw blocksError(MetricsErrors.InvalidMetricName, 'Metric name must not be empty');
|
|
17
|
+
}
|
|
18
|
+
if (name.length > 1024) {
|
|
19
|
+
throw blocksError(MetricsErrors.InvalidMetricName, `Metric name exceeds 1024 characters (got ${name.length})`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Validate dimensions against CloudWatch constraints.
|
|
24
|
+
* - Max 30 dimension key-value pairs
|
|
25
|
+
* - Keys and values must be non-empty
|
|
26
|
+
* - Keys and values max 1024 characters each
|
|
27
|
+
*/
|
|
28
|
+
export function validateDimensions(dimensions) {
|
|
29
|
+
const entries = Object.entries(dimensions);
|
|
30
|
+
if (entries.length > 30) {
|
|
31
|
+
throw blocksError(MetricsErrors.InvalidDimensions, `Dimensions exceed 30 entries (got ${entries.length})`);
|
|
32
|
+
}
|
|
33
|
+
for (const [key, value] of entries) {
|
|
34
|
+
if (!key || key.length === 0) {
|
|
35
|
+
throw blocksError(MetricsErrors.InvalidDimensions, 'Dimension key must not be empty');
|
|
36
|
+
}
|
|
37
|
+
if (key.length > 1024) {
|
|
38
|
+
throw blocksError(MetricsErrors.InvalidDimensions, `Dimension key exceeds 1024 characters: '${key.substring(0, 50)}...'`);
|
|
39
|
+
}
|
|
40
|
+
if (value === undefined || value === null || value.length === 0) {
|
|
41
|
+
throw blocksError(MetricsErrors.InvalidDimensions, `Dimension value for key '${key}' must not be empty`);
|
|
42
|
+
}
|
|
43
|
+
if (value.length > 1024) {
|
|
44
|
+
throw blocksError(MetricsErrors.InvalidDimensions, `Dimension value for key '${key}' exceeds 1024 characters`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Validate batch size (max 100 metrics per EMF document).
|
|
50
|
+
*/
|
|
51
|
+
export function validateBatchSize(count) {
|
|
52
|
+
if (count > 100) {
|
|
53
|
+
throw blocksError(MetricsErrors.BatchTooLarge, `Batch exceeds 100 metrics (got ${count})`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/** Valid characters for a CloudWatch namespace: alphanumeric, dot, underscore, hash, colon, slash, hyphen, space. */
|
|
57
|
+
const NAMESPACE_PATTERN = /^[a-zA-Z0-9._#:/ -]+$/;
|
|
58
|
+
/**
|
|
59
|
+
* Validate a namespace against CloudWatch constraints.
|
|
60
|
+
* - Must be non-empty (at least one non-whitespace character)
|
|
61
|
+
* - Max 256 characters
|
|
62
|
+
* - Only valid chars: [a-zA-Z0-9._#:/ -]
|
|
63
|
+
* - Must not start with "AWS/" (reserved for AWS services)
|
|
64
|
+
*/
|
|
65
|
+
export function validateNamespace(namespace) {
|
|
66
|
+
if (!namespace || namespace.trim().length === 0) {
|
|
67
|
+
throw blocksError(MetricsErrors.InvalidNamespace, 'Namespace must not be empty');
|
|
68
|
+
}
|
|
69
|
+
if (namespace.length > 256) {
|
|
70
|
+
throw blocksError(MetricsErrors.InvalidNamespace, `Namespace exceeds 256 characters (got ${namespace.length})`);
|
|
71
|
+
}
|
|
72
|
+
if (!NAMESPACE_PATTERN.test(namespace)) {
|
|
73
|
+
throw blocksError(MetricsErrors.InvalidNamespace, `Namespace contains invalid characters: '${namespace.substring(0, 50)}'. Valid: [a-zA-Z0-9._#:/ -]`);
|
|
74
|
+
}
|
|
75
|
+
if (namespace.startsWith('AWS/')) {
|
|
76
|
+
throw blocksError(MetricsErrors.InvalidNamespace, `Namespace must not start with 'AWS/' (reserved for AWS services): '${namespace}'`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Merge default dimensions with per-emit dimensions.
|
|
81
|
+
* Per-emit dimensions take precedence on key conflict.
|
|
82
|
+
*/
|
|
83
|
+
export function mergeDimensions(defaults, overrides) {
|
|
84
|
+
if (!overrides || Object.keys(overrides).length === 0)
|
|
85
|
+
return { ...defaults };
|
|
86
|
+
return { ...defaults, ...overrides };
|
|
87
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@aws-blocks/bb-metrics",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"author": "Amazon Web Services",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist",
|
|
9
|
+
"README.md",
|
|
10
|
+
"src",
|
|
11
|
+
"LICENSE"
|
|
12
|
+
],
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"browser": "./dist/index.browser.js",
|
|
16
|
+
"cdk": {
|
|
17
|
+
"types": "./dist/index.cdk.d.ts",
|
|
18
|
+
"default": "./dist/index.cdk.js"
|
|
19
|
+
},
|
|
20
|
+
"aws-runtime": "./dist/index.aws.js",
|
|
21
|
+
"types": "./dist/index.mock.d.ts",
|
|
22
|
+
"default": "./dist/index.mock.js"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "tsc --build",
|
|
27
|
+
"test": "node --test dist/**/*.test.js"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@aws-blocks/core": "^0.1.0",
|
|
31
|
+
"@aws-blocks/bb-logger": "^0.1.0"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@types/node": "^20.0.0",
|
|
35
|
+
"typescript": "^5.3.0"
|
|
36
|
+
},
|
|
37
|
+
"peerDependencies": {
|
|
38
|
+
"aws-cdk-lib": "^2.257.0",
|
|
39
|
+
"constructs": "^10.6.0"
|
|
40
|
+
}
|
|
41
|
+
}
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Typed error constants for Metrics. Use with `isBlocksError()` in catch blocks.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* import { isBlocksError } from '@aws-blocks/core';
|
|
10
|
+
* import { MetricsErrors } from '@aws-blocks/bb-metrics';
|
|
11
|
+
*
|
|
12
|
+
* try {
|
|
13
|
+
* metrics.emit('', 1);
|
|
14
|
+
* } catch (e) {
|
|
15
|
+
* if (isBlocksError(e, MetricsErrors.InvalidMetricName)) {
|
|
16
|
+
* // handle invalid metric name
|
|
17
|
+
* }
|
|
18
|
+
* }
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export const MetricsErrors = {
|
|
22
|
+
/** Metric name is empty or exceeds 1024 characters. */
|
|
23
|
+
InvalidMetricName: 'InvalidMetricNameException',
|
|
24
|
+
/** Dimensions exceed 30 entries, or contain empty keys/values, or key/value exceeds 1024 chars. */
|
|
25
|
+
InvalidDimensions: 'InvalidDimensionsException',
|
|
26
|
+
/** Batch contains more than 100 metrics. */
|
|
27
|
+
BatchTooLarge: 'BatchTooLargeException',
|
|
28
|
+
/** Namespace is empty, too long, contains invalid characters, or uses reserved AWS/ prefix. */
|
|
29
|
+
InvalidNamespace: 'InvalidNamespaceException',
|
|
30
|
+
} as const;
|
package/src/index.aws.ts
ADDED
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import { Scope } from '@aws-blocks/core';
|
|
5
|
+
import type { ScopeParent } from '@aws-blocks/core';
|
|
6
|
+
import type {
|
|
7
|
+
MetricsOptions,
|
|
8
|
+
EmitOptions,
|
|
9
|
+
MetricDatum,
|
|
10
|
+
MetricUnit,
|
|
11
|
+
MetricResolution,
|
|
12
|
+
ExternalMetricsRef,
|
|
13
|
+
MetricsEmitter,
|
|
14
|
+
} from './types.js';
|
|
15
|
+
import { MetricsErrors } from './errors.js';
|
|
16
|
+
import {
|
|
17
|
+
validateMetricName,
|
|
18
|
+
validateDimensions,
|
|
19
|
+
validateBatchSize,
|
|
20
|
+
validateNamespace,
|
|
21
|
+
mergeDimensions,
|
|
22
|
+
} from './validation.js';
|
|
23
|
+
import { Logger } from '@aws-blocks/bb-logger';
|
|
24
|
+
import type { ChildLogger } from '@aws-blocks/bb-logger';
|
|
25
|
+
|
|
26
|
+
export { MetricsErrors } from './errors.js';
|
|
27
|
+
export type {
|
|
28
|
+
MetricsOptions,
|
|
29
|
+
EmitOptions,
|
|
30
|
+
MetricDatum,
|
|
31
|
+
MetricUnit,
|
|
32
|
+
MetricResolution,
|
|
33
|
+
ExternalMetricsRef,
|
|
34
|
+
MetricsEmitter,
|
|
35
|
+
} from './types.js';
|
|
36
|
+
|
|
37
|
+
// ── Metrics (AWS runtime via EMF) ───────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Custom application metrics backed by Amazon CloudWatch (via EMF).
|
|
41
|
+
*
|
|
42
|
+
* Metrics emitted from this Building Block appear in CloudWatch under the
|
|
43
|
+
* configured namespace. Use for dashboards, alarms, and operational visibility.
|
|
44
|
+
*
|
|
45
|
+
* Uses CloudWatch Embedded Metric Format (EMF) — writes metric data as
|
|
46
|
+
* structured JSON to stdout. Lambda captures these and CloudWatch extracts
|
|
47
|
+
* metrics automatically.
|
|
48
|
+
*
|
|
49
|
+
* **When to use:** You need to track numeric measurements over time — request
|
|
50
|
+
* counts, error rates, latency, queue depths, business KPIs.
|
|
51
|
+
*
|
|
52
|
+
* **When NOT to use:** If you need structured log output only, use `Logging`.
|
|
53
|
+
* If you need distributed request tracing, use `Tracing`. If you need to store
|
|
54
|
+
* time-series data for querying, use `Database` or `DistributedTable`.
|
|
55
|
+
*
|
|
56
|
+
* **Best practices:**
|
|
57
|
+
* - Keep dimension cardinality low (avoid user IDs or request IDs as dimensions)
|
|
58
|
+
* - Use consistent metric names across your application
|
|
59
|
+
* - Use `defaultDimensions` for shared context (service name, environment)
|
|
60
|
+
* - Prefer `emitBatch` when recording multiple metrics in a single request
|
|
61
|
+
* - Use units to enable automatic conversions in CloudWatch dashboards
|
|
62
|
+
*
|
|
63
|
+
* **Scaling:** CloudWatch accepts unlimited metrics via EMF. Standard resolution
|
|
64
|
+
* metrics (60s) are retained for 15 days; high-resolution (1s) for 3 hours,
|
|
65
|
+
* then aggregated. Costs scale with unique metric name + dimension combinations.
|
|
66
|
+
*
|
|
67
|
+
* **Local development:** Metrics are written as EMF JSON to stdout (same as AWS).
|
|
68
|
+
* No disk persistence — metrics are ephemeral in local dev.
|
|
69
|
+
*
|
|
70
|
+
* **⚠️ Synchronous:** All metric methods are synchronous (void, not Promise).
|
|
71
|
+
* EMF writes to stdout which Lambda captures asynchronously. Returning a Promise
|
|
72
|
+
* would add overhead for zero benefit.
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* ```typescript
|
|
76
|
+
* import { Metrics } from '@aws-blocks/bb-metrics';
|
|
77
|
+
*
|
|
78
|
+
* const metrics = new Metrics(scope, 'appMetrics', {
|
|
79
|
+
* namespace: 'MyApp/Orders',
|
|
80
|
+
* defaultDimensions: { service: 'orders' },
|
|
81
|
+
* });
|
|
82
|
+
*
|
|
83
|
+
* metrics.emit('RequestCount', 1, { unit: 'Count' });
|
|
84
|
+
* metrics.emit('Latency', 42, { unit: 'Milliseconds' });
|
|
85
|
+
* ```
|
|
86
|
+
*/
|
|
87
|
+
export class Metrics extends Scope implements MetricsEmitter {
|
|
88
|
+
/** The resolved CloudWatch namespace for metrics emitted by this instance. */
|
|
89
|
+
readonly namespace: string;
|
|
90
|
+
/** Dimensions applied to every metric emitted by this instance. */
|
|
91
|
+
readonly defaultDimensions: Readonly<Record<string, string>>;
|
|
92
|
+
|
|
93
|
+
/** @internal Logger for internal operations. Defaults to error-level when not provided. */
|
|
94
|
+
protected log: ChildLogger;
|
|
95
|
+
|
|
96
|
+
constructor(scope: ScopeParent, id: string, options?: MetricsOptions) {
|
|
97
|
+
super(id, { parent: scope });
|
|
98
|
+
this.log = options?.logger ?? new Logger(this, 'logger', { level: 'error' });
|
|
99
|
+
this.namespace = options?.metrics?.namespace
|
|
100
|
+
?? options?.namespace
|
|
101
|
+
?? this.fullId;
|
|
102
|
+
this.defaultDimensions = options?.defaultDimensions ?? {};
|
|
103
|
+
validateNamespace(this.namespace);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Record a single metric data point via EMF.
|
|
108
|
+
*
|
|
109
|
+
* @param name - Metric name (e.g., 'RequestCount', 'Latency'). Non-empty, max 1024 chars.
|
|
110
|
+
* @param value - Numeric value to record.
|
|
111
|
+
* @param options - Unit, dimensions, timestamp, and resolution.
|
|
112
|
+
* @throws {MetricsErrors.InvalidMetricName} If the metric name is empty or exceeds 1024 characters.
|
|
113
|
+
* @throws {MetricsErrors.InvalidDimensions} If dimensions exceed 30 entries or contain empty keys/values.
|
|
114
|
+
*
|
|
115
|
+
* @example
|
|
116
|
+
* ```typescript
|
|
117
|
+
* metrics.emit('RequestCount', 1);
|
|
118
|
+
* metrics.emit('Latency', 42, { unit: 'Milliseconds' });
|
|
119
|
+
* metrics.emit('ErrorRate', 0.05, {
|
|
120
|
+
* unit: 'Percent',
|
|
121
|
+
* dimensions: { endpoint: '/api/orders' },
|
|
122
|
+
* });
|
|
123
|
+
* ```
|
|
124
|
+
*/
|
|
125
|
+
emit(name: string, value: number, options?: EmitOptions): void {
|
|
126
|
+
validateMetricName(name);
|
|
127
|
+
const dims = mergeDimensions(this.defaultDimensions, options?.dimensions);
|
|
128
|
+
validateDimensions(dims);
|
|
129
|
+
|
|
130
|
+
writeEmf(this.namespace, [{
|
|
131
|
+
name,
|
|
132
|
+
value,
|
|
133
|
+
unit: options?.unit ?? 'None',
|
|
134
|
+
dimensions: dims,
|
|
135
|
+
timestamp: options?.timestamp,
|
|
136
|
+
resolution: options?.resolution ?? 'standard',
|
|
137
|
+
}]);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Record multiple metric data points in a single EMF document.
|
|
142
|
+
* Metrics with the same dimension set are grouped into one EMF entry.
|
|
143
|
+
*
|
|
144
|
+
* @param metrics - Array of metric data points (max 100 per call).
|
|
145
|
+
* @throws {MetricsErrors.InvalidMetricName} If any metric name is invalid.
|
|
146
|
+
* @throws {MetricsErrors.InvalidDimensions} If any metric's dimensions are invalid.
|
|
147
|
+
* @throws {MetricsErrors.BatchTooLarge} If the batch exceeds 100 metrics.
|
|
148
|
+
*
|
|
149
|
+
* @example
|
|
150
|
+
* ```typescript
|
|
151
|
+
* metrics.emitBatch([
|
|
152
|
+
* { name: 'RequestCount', value: 1, unit: 'Count' },
|
|
153
|
+
* { name: 'Latency', value: 42, unit: 'Milliseconds' },
|
|
154
|
+
* { name: 'ErrorCount', value: 0, unit: 'Count' },
|
|
155
|
+
* ]);
|
|
156
|
+
* ```
|
|
157
|
+
*/
|
|
158
|
+
emitBatch(metrics: MetricDatum[]): void {
|
|
159
|
+
validateBatchSize(metrics.length);
|
|
160
|
+
|
|
161
|
+
for (const m of metrics) {
|
|
162
|
+
validateMetricName(m.name);
|
|
163
|
+
const dims = mergeDimensions(this.defaultDimensions, m.dimensions);
|
|
164
|
+
validateDimensions(dims);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const resolved = metrics.map(m => ({
|
|
168
|
+
name: m.name,
|
|
169
|
+
value: m.value,
|
|
170
|
+
unit: (m.unit ?? 'None') as MetricUnit,
|
|
171
|
+
dimensions: mergeDimensions(this.defaultDimensions, m.dimensions),
|
|
172
|
+
timestamp: m.timestamp,
|
|
173
|
+
resolution: (m.resolution ?? 'standard') as MetricResolution,
|
|
174
|
+
}));
|
|
175
|
+
|
|
176
|
+
writeEmf(this.namespace, resolved);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* No-op. EMF writes are synchronous stdout writes — no buffering to flush.
|
|
181
|
+
*/
|
|
182
|
+
flush(): void {}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Create a child Metrics emitter with inherited namespace and dimensions.
|
|
186
|
+
* The child merges the provided dimensions on top of the parent's defaults.
|
|
187
|
+
*
|
|
188
|
+
* @param dimensions - Additional dimensions for the child instance.
|
|
189
|
+
* @returns A MetricsEmitter with merged dimensions.
|
|
190
|
+
*
|
|
191
|
+
* @example
|
|
192
|
+
* ```typescript
|
|
193
|
+
* const requestMetrics = metrics.child({ endpoint: '/api/users', method: 'GET' });
|
|
194
|
+
* requestMetrics.emit('RequestCount', 1);
|
|
195
|
+
* ```
|
|
196
|
+
*/
|
|
197
|
+
child(dimensions: Record<string, string>): MetricsEmitter {
|
|
198
|
+
return new ChildMetrics(
|
|
199
|
+
this.namespace,
|
|
200
|
+
{ ...this.defaultDimensions, ...dimensions },
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Reference an existing CloudWatch namespace not managed by this Building Block.
|
|
206
|
+
*
|
|
207
|
+
* @param namespace - The CloudWatch namespace string.
|
|
208
|
+
*
|
|
209
|
+
* @example
|
|
210
|
+
* ```typescript
|
|
211
|
+
* const metrics = new Metrics(scope, 'legacy', {
|
|
212
|
+
* metrics: Metrics.fromExisting('MyOrg/SharedMetrics'),
|
|
213
|
+
* });
|
|
214
|
+
* ```
|
|
215
|
+
*/
|
|
216
|
+
static fromExisting(namespace: string): ExternalMetricsRef {
|
|
217
|
+
return { __brand: 'ExternalMetricsRef' as const, namespace };
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ── ChildMetrics ────────────────────────────────────────────────────────────
|
|
222
|
+
|
|
223
|
+
class ChildMetrics implements MetricsEmitter {
|
|
224
|
+
constructor(
|
|
225
|
+
private namespace: string,
|
|
226
|
+
readonly defaultDimensions: Readonly<Record<string, string>>,
|
|
227
|
+
) {}
|
|
228
|
+
|
|
229
|
+
emit(name: string, value: number, options?: EmitOptions): void {
|
|
230
|
+
validateMetricName(name);
|
|
231
|
+
const dims = mergeDimensions(this.defaultDimensions, options?.dimensions);
|
|
232
|
+
validateDimensions(dims);
|
|
233
|
+
|
|
234
|
+
writeEmf(this.namespace, [{
|
|
235
|
+
name,
|
|
236
|
+
value,
|
|
237
|
+
unit: options?.unit ?? 'None',
|
|
238
|
+
dimensions: dims,
|
|
239
|
+
timestamp: options?.timestamp,
|
|
240
|
+
resolution: options?.resolution ?? 'standard',
|
|
241
|
+
}]);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
emitBatch(metrics: MetricDatum[]): void {
|
|
245
|
+
validateBatchSize(metrics.length);
|
|
246
|
+
|
|
247
|
+
for (const m of metrics) {
|
|
248
|
+
validateMetricName(m.name);
|
|
249
|
+
const dims = mergeDimensions(this.defaultDimensions, m.dimensions);
|
|
250
|
+
validateDimensions(dims);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const resolved = metrics.map(m => ({
|
|
254
|
+
name: m.name,
|
|
255
|
+
value: m.value,
|
|
256
|
+
unit: (m.unit ?? 'None') as MetricUnit,
|
|
257
|
+
dimensions: mergeDimensions(this.defaultDimensions, m.dimensions),
|
|
258
|
+
timestamp: m.timestamp,
|
|
259
|
+
resolution: (m.resolution ?? 'standard') as MetricResolution,
|
|
260
|
+
}));
|
|
261
|
+
|
|
262
|
+
writeEmf(this.namespace, resolved);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
flush(): void {}
|
|
266
|
+
|
|
267
|
+
child(dimensions: Record<string, string>): MetricsEmitter {
|
|
268
|
+
return new ChildMetrics(
|
|
269
|
+
this.namespace,
|
|
270
|
+
{ ...this.defaultDimensions, ...dimensions },
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// ── EMF Writer ──────────────────────────────────────────────────────────────
|
|
276
|
+
|
|
277
|
+
interface ResolvedMetric {
|
|
278
|
+
name: string;
|
|
279
|
+
value: number;
|
|
280
|
+
unit: string;
|
|
281
|
+
dimensions: Record<string, string>;
|
|
282
|
+
timestamp?: Date;
|
|
283
|
+
resolution: MetricResolution;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
interface MetricGroup {
|
|
287
|
+
dimensions: Record<string, string>;
|
|
288
|
+
metrics: ResolvedMetric[];
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Write one or more metrics as EMF-formatted JSON lines to stdout.
|
|
293
|
+
* Groups metrics by dimension set (EMF requires same dimensions per entry).
|
|
294
|
+
*/
|
|
295
|
+
function writeEmf(namespace: string, metrics: ResolvedMetric[]): void {
|
|
296
|
+
if (metrics.length === 0) return;
|
|
297
|
+
|
|
298
|
+
const groups = groupByDimensions(metrics);
|
|
299
|
+
|
|
300
|
+
for (const group of groups) {
|
|
301
|
+
const dimKeys = Object.keys(group.dimensions);
|
|
302
|
+
const timestamp = group.metrics[0].timestamp?.getTime() ?? Date.now();
|
|
303
|
+
|
|
304
|
+
const emfPayload: Record<string, unknown> = {
|
|
305
|
+
_aws: {
|
|
306
|
+
Timestamp: timestamp,
|
|
307
|
+
CloudWatchMetrics: [{
|
|
308
|
+
Namespace: namespace,
|
|
309
|
+
Dimensions: dimKeys.length > 0 ? [dimKeys] : [[]],
|
|
310
|
+
Metrics: group.metrics.map(m => ({
|
|
311
|
+
Name: m.name,
|
|
312
|
+
Unit: m.unit,
|
|
313
|
+
StorageResolution: m.resolution === 'high' ? 1 : 60,
|
|
314
|
+
})),
|
|
315
|
+
}],
|
|
316
|
+
},
|
|
317
|
+
...group.dimensions,
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
for (const m of group.metrics) {
|
|
321
|
+
emfPayload[m.name] = m.value;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
process.stdout.write(JSON.stringify(emfPayload) + '\n');
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Group metrics by their dimension set. EMF requires metrics in the same
|
|
330
|
+
* CloudWatchMetrics entry to share the same dimension keys and values.
|
|
331
|
+
*/
|
|
332
|
+
function groupByDimensions(metrics: ResolvedMetric[]): MetricGroup[] {
|
|
333
|
+
const groups = new Map<string, MetricGroup>();
|
|
334
|
+
|
|
335
|
+
for (const m of metrics) {
|
|
336
|
+
const key = dimensionKey(m.dimensions);
|
|
337
|
+
let group = groups.get(key);
|
|
338
|
+
if (!group) {
|
|
339
|
+
group = { dimensions: m.dimensions, metrics: [] };
|
|
340
|
+
groups.set(key, group);
|
|
341
|
+
}
|
|
342
|
+
group.metrics.push(m);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return Array.from(groups.values());
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function dimensionKey(dims: Record<string, string>): string {
|
|
349
|
+
const sorted = Object.entries(dims).sort(([a], [b]) => a.localeCompare(b));
|
|
350
|
+
return sorted.map(([k, v]) => `${k}=${v}`).join('&');
|
|
351
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
// Browser stub — Metrics is server-side only.
|
|
5
|
+
// Provides a no-op implementation that silently drops all metric emissions.
|
|
6
|
+
|
|
7
|
+
import type { EmitOptions, MetricDatum, ExternalMetricsRef, MetricsEmitter } from './types.js';
|
|
8
|
+
|
|
9
|
+
export { MetricsErrors } from './errors.js';
|
|
10
|
+
export type {
|
|
11
|
+
MetricsOptions,
|
|
12
|
+
EmitOptions,
|
|
13
|
+
MetricDatum,
|
|
14
|
+
MetricUnit,
|
|
15
|
+
MetricResolution,
|
|
16
|
+
ExternalMetricsRef,
|
|
17
|
+
MetricsEmitter,
|
|
18
|
+
} from './types.js';
|
|
19
|
+
|
|
20
|
+
export class Metrics implements MetricsEmitter {
|
|
21
|
+
constructor(..._args: any[]) {}
|
|
22
|
+
emit(_name: string, _value: number, _options?: EmitOptions): void {}
|
|
23
|
+
emitBatch(_metrics: MetricDatum[]): void {}
|
|
24
|
+
flush(): void {}
|
|
25
|
+
child(_dimensions: Record<string, string>): MetricsEmitter { return new Metrics(); }
|
|
26
|
+
static fromExisting(namespace: string): ExternalMetricsRef {
|
|
27
|
+
return { __brand: 'ExternalMetricsRef' as const, namespace };
|
|
28
|
+
}
|
|
29
|
+
}
|
package/src/index.cdk.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import { Scope } from '@aws-blocks/core/cdk';
|
|
5
|
+
import type { ScopeParent } from '@aws-blocks/core';
|
|
6
|
+
import type { MetricsOptions } from './types.js';
|
|
7
|
+
|
|
8
|
+
export { MetricsErrors } from './errors.js';
|
|
9
|
+
export type {
|
|
10
|
+
MetricsOptions,
|
|
11
|
+
EmitOptions,
|
|
12
|
+
MetricDatum,
|
|
13
|
+
MetricUnit,
|
|
14
|
+
MetricResolution,
|
|
15
|
+
ExternalMetricsRef,
|
|
16
|
+
MetricsEmitter,
|
|
17
|
+
} from './types.js';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* CDK construct for Metrics.
|
|
21
|
+
*
|
|
22
|
+
* EMF writes structured JSON to stdout which Lambda captures in CloudWatch Logs.
|
|
23
|
+
* CloudWatch automatically extracts metrics — no additional IAM permissions or
|
|
24
|
+
* environment variables are needed.
|
|
25
|
+
*/
|
|
26
|
+
export class Metrics extends Scope {
|
|
27
|
+
/** The resolved CloudWatch namespace for metrics emitted by this instance. */
|
|
28
|
+
readonly namespace: string;
|
|
29
|
+
|
|
30
|
+
/** Default dimensions applied to every metric emitted by this instance. */
|
|
31
|
+
readonly defaultDimensions: Readonly<Record<string, string>>;
|
|
32
|
+
|
|
33
|
+
constructor(scope: ScopeParent, id: string, options?: MetricsOptions) {
|
|
34
|
+
super(id, { parent: scope });
|
|
35
|
+
this.namespace = options?.metrics?.namespace
|
|
36
|
+
?? options?.namespace
|
|
37
|
+
?? this.fullId;
|
|
38
|
+
this.defaultDimensions = options?.defaultDimensions ?? {};
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Mock (local development) runtime for Metrics.
|
|
6
|
+
*
|
|
7
|
+
* Behavior is identical to the AWS runtime — both write EMF-formatted JSON to
|
|
8
|
+
* stdout. In local dev the output is visible in the terminal; in Lambda it is
|
|
9
|
+
* captured by CloudWatch Logs. No separate mock implementation is needed.
|
|
10
|
+
*/
|
|
11
|
+
export { Metrics, MetricsErrors } from './index.aws.js';
|
|
12
|
+
export type {
|
|
13
|
+
MetricsOptions,
|
|
14
|
+
EmitOptions,
|
|
15
|
+
MetricDatum,
|
|
16
|
+
MetricUnit,
|
|
17
|
+
MetricResolution,
|
|
18
|
+
ExternalMetricsRef,
|
|
19
|
+
MetricsEmitter,
|
|
20
|
+
} from './types.js';
|