@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.
@@ -0,0 +1,270 @@
1
+ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ import { Scope } from '@aws-blocks/core';
4
+ import { validateMetricName, validateDimensions, validateBatchSize, validateNamespace, mergeDimensions, } from './validation.js';
5
+ import { Logger } from '@aws-blocks/bb-logger';
6
+ export { MetricsErrors } from './errors.js';
7
+ // ── Metrics (AWS runtime via EMF) ───────────────────────────────────────────
8
+ /**
9
+ * Custom application metrics backed by Amazon CloudWatch (via EMF).
10
+ *
11
+ * Metrics emitted from this Building Block appear in CloudWatch under the
12
+ * configured namespace. Use for dashboards, alarms, and operational visibility.
13
+ *
14
+ * Uses CloudWatch Embedded Metric Format (EMF) — writes metric data as
15
+ * structured JSON to stdout. Lambda captures these and CloudWatch extracts
16
+ * metrics automatically.
17
+ *
18
+ * **When to use:** You need to track numeric measurements over time — request
19
+ * counts, error rates, latency, queue depths, business KPIs.
20
+ *
21
+ * **When NOT to use:** If you need structured log output only, use `Logging`.
22
+ * If you need distributed request tracing, use `Tracing`. If you need to store
23
+ * time-series data for querying, use `Database` or `DistributedTable`.
24
+ *
25
+ * **Best practices:**
26
+ * - Keep dimension cardinality low (avoid user IDs or request IDs as dimensions)
27
+ * - Use consistent metric names across your application
28
+ * - Use `defaultDimensions` for shared context (service name, environment)
29
+ * - Prefer `emitBatch` when recording multiple metrics in a single request
30
+ * - Use units to enable automatic conversions in CloudWatch dashboards
31
+ *
32
+ * **Scaling:** CloudWatch accepts unlimited metrics via EMF. Standard resolution
33
+ * metrics (60s) are retained for 15 days; high-resolution (1s) for 3 hours,
34
+ * then aggregated. Costs scale with unique metric name + dimension combinations.
35
+ *
36
+ * **Local development:** Metrics are written as EMF JSON to stdout (same as AWS).
37
+ * No disk persistence — metrics are ephemeral in local dev.
38
+ *
39
+ * **⚠️ Synchronous:** All metric methods are synchronous (void, not Promise).
40
+ * EMF writes to stdout which Lambda captures asynchronously. Returning a Promise
41
+ * would add overhead for zero benefit.
42
+ *
43
+ * @example
44
+ * ```typescript
45
+ * import { Metrics } from '@aws-blocks/bb-metrics';
46
+ *
47
+ * const metrics = new Metrics(scope, 'appMetrics', {
48
+ * namespace: 'MyApp/Orders',
49
+ * defaultDimensions: { service: 'orders' },
50
+ * });
51
+ *
52
+ * metrics.emit('RequestCount', 1, { unit: 'Count' });
53
+ * metrics.emit('Latency', 42, { unit: 'Milliseconds' });
54
+ * ```
55
+ */
56
+ export class Metrics extends Scope {
57
+ /** The resolved CloudWatch namespace for metrics emitted by this instance. */
58
+ namespace;
59
+ /** Dimensions applied to every metric emitted by this instance. */
60
+ defaultDimensions;
61
+ /** @internal Logger for internal operations. Defaults to error-level when not provided. */
62
+ log;
63
+ constructor(scope, id, options) {
64
+ super(id, { parent: scope });
65
+ this.log = options?.logger ?? new Logger(this, 'logger', { level: 'error' });
66
+ this.namespace = options?.metrics?.namespace
67
+ ?? options?.namespace
68
+ ?? this.fullId;
69
+ this.defaultDimensions = options?.defaultDimensions ?? {};
70
+ validateNamespace(this.namespace);
71
+ }
72
+ /**
73
+ * Record a single metric data point via EMF.
74
+ *
75
+ * @param name - Metric name (e.g., 'RequestCount', 'Latency'). Non-empty, max 1024 chars.
76
+ * @param value - Numeric value to record.
77
+ * @param options - Unit, dimensions, timestamp, and resolution.
78
+ * @throws {MetricsErrors.InvalidMetricName} If the metric name is empty or exceeds 1024 characters.
79
+ * @throws {MetricsErrors.InvalidDimensions} If dimensions exceed 30 entries or contain empty keys/values.
80
+ *
81
+ * @example
82
+ * ```typescript
83
+ * metrics.emit('RequestCount', 1);
84
+ * metrics.emit('Latency', 42, { unit: 'Milliseconds' });
85
+ * metrics.emit('ErrorRate', 0.05, {
86
+ * unit: 'Percent',
87
+ * dimensions: { endpoint: '/api/orders' },
88
+ * });
89
+ * ```
90
+ */
91
+ emit(name, value, options) {
92
+ validateMetricName(name);
93
+ const dims = mergeDimensions(this.defaultDimensions, options?.dimensions);
94
+ validateDimensions(dims);
95
+ writeEmf(this.namespace, [{
96
+ name,
97
+ value,
98
+ unit: options?.unit ?? 'None',
99
+ dimensions: dims,
100
+ timestamp: options?.timestamp,
101
+ resolution: options?.resolution ?? 'standard',
102
+ }]);
103
+ }
104
+ /**
105
+ * Record multiple metric data points in a single EMF document.
106
+ * Metrics with the same dimension set are grouped into one EMF entry.
107
+ *
108
+ * @param metrics - Array of metric data points (max 100 per call).
109
+ * @throws {MetricsErrors.InvalidMetricName} If any metric name is invalid.
110
+ * @throws {MetricsErrors.InvalidDimensions} If any metric's dimensions are invalid.
111
+ * @throws {MetricsErrors.BatchTooLarge} If the batch exceeds 100 metrics.
112
+ *
113
+ * @example
114
+ * ```typescript
115
+ * metrics.emitBatch([
116
+ * { name: 'RequestCount', value: 1, unit: 'Count' },
117
+ * { name: 'Latency', value: 42, unit: 'Milliseconds' },
118
+ * { name: 'ErrorCount', value: 0, unit: 'Count' },
119
+ * ]);
120
+ * ```
121
+ */
122
+ emitBatch(metrics) {
123
+ validateBatchSize(metrics.length);
124
+ for (const m of metrics) {
125
+ validateMetricName(m.name);
126
+ const dims = mergeDimensions(this.defaultDimensions, m.dimensions);
127
+ validateDimensions(dims);
128
+ }
129
+ const resolved = metrics.map(m => ({
130
+ name: m.name,
131
+ value: m.value,
132
+ unit: (m.unit ?? 'None'),
133
+ dimensions: mergeDimensions(this.defaultDimensions, m.dimensions),
134
+ timestamp: m.timestamp,
135
+ resolution: (m.resolution ?? 'standard'),
136
+ }));
137
+ writeEmf(this.namespace, resolved);
138
+ }
139
+ /**
140
+ * No-op. EMF writes are synchronous stdout writes — no buffering to flush.
141
+ */
142
+ flush() { }
143
+ /**
144
+ * Create a child Metrics emitter with inherited namespace and dimensions.
145
+ * The child merges the provided dimensions on top of the parent's defaults.
146
+ *
147
+ * @param dimensions - Additional dimensions for the child instance.
148
+ * @returns A MetricsEmitter with merged dimensions.
149
+ *
150
+ * @example
151
+ * ```typescript
152
+ * const requestMetrics = metrics.child({ endpoint: '/api/users', method: 'GET' });
153
+ * requestMetrics.emit('RequestCount', 1);
154
+ * ```
155
+ */
156
+ child(dimensions) {
157
+ return new ChildMetrics(this.namespace, { ...this.defaultDimensions, ...dimensions });
158
+ }
159
+ /**
160
+ * Reference an existing CloudWatch namespace not managed by this Building Block.
161
+ *
162
+ * @param namespace - The CloudWatch namespace string.
163
+ *
164
+ * @example
165
+ * ```typescript
166
+ * const metrics = new Metrics(scope, 'legacy', {
167
+ * metrics: Metrics.fromExisting('MyOrg/SharedMetrics'),
168
+ * });
169
+ * ```
170
+ */
171
+ static fromExisting(namespace) {
172
+ return { __brand: 'ExternalMetricsRef', namespace };
173
+ }
174
+ }
175
+ // ── ChildMetrics ────────────────────────────────────────────────────────────
176
+ class ChildMetrics {
177
+ namespace;
178
+ defaultDimensions;
179
+ constructor(namespace, defaultDimensions) {
180
+ this.namespace = namespace;
181
+ this.defaultDimensions = defaultDimensions;
182
+ }
183
+ emit(name, value, options) {
184
+ validateMetricName(name);
185
+ const dims = mergeDimensions(this.defaultDimensions, options?.dimensions);
186
+ validateDimensions(dims);
187
+ writeEmf(this.namespace, [{
188
+ name,
189
+ value,
190
+ unit: options?.unit ?? 'None',
191
+ dimensions: dims,
192
+ timestamp: options?.timestamp,
193
+ resolution: options?.resolution ?? 'standard',
194
+ }]);
195
+ }
196
+ emitBatch(metrics) {
197
+ validateBatchSize(metrics.length);
198
+ for (const m of metrics) {
199
+ validateMetricName(m.name);
200
+ const dims = mergeDimensions(this.defaultDimensions, m.dimensions);
201
+ validateDimensions(dims);
202
+ }
203
+ const resolved = metrics.map(m => ({
204
+ name: m.name,
205
+ value: m.value,
206
+ unit: (m.unit ?? 'None'),
207
+ dimensions: mergeDimensions(this.defaultDimensions, m.dimensions),
208
+ timestamp: m.timestamp,
209
+ resolution: (m.resolution ?? 'standard'),
210
+ }));
211
+ writeEmf(this.namespace, resolved);
212
+ }
213
+ flush() { }
214
+ child(dimensions) {
215
+ return new ChildMetrics(this.namespace, { ...this.defaultDimensions, ...dimensions });
216
+ }
217
+ }
218
+ /**
219
+ * Write one or more metrics as EMF-formatted JSON lines to stdout.
220
+ * Groups metrics by dimension set (EMF requires same dimensions per entry).
221
+ */
222
+ function writeEmf(namespace, metrics) {
223
+ if (metrics.length === 0)
224
+ return;
225
+ const groups = groupByDimensions(metrics);
226
+ for (const group of groups) {
227
+ const dimKeys = Object.keys(group.dimensions);
228
+ const timestamp = group.metrics[0].timestamp?.getTime() ?? Date.now();
229
+ const emfPayload = {
230
+ _aws: {
231
+ Timestamp: timestamp,
232
+ CloudWatchMetrics: [{
233
+ Namespace: namespace,
234
+ Dimensions: dimKeys.length > 0 ? [dimKeys] : [[]],
235
+ Metrics: group.metrics.map(m => ({
236
+ Name: m.name,
237
+ Unit: m.unit,
238
+ StorageResolution: m.resolution === 'high' ? 1 : 60,
239
+ })),
240
+ }],
241
+ },
242
+ ...group.dimensions,
243
+ };
244
+ for (const m of group.metrics) {
245
+ emfPayload[m.name] = m.value;
246
+ }
247
+ process.stdout.write(JSON.stringify(emfPayload) + '\n');
248
+ }
249
+ }
250
+ /**
251
+ * Group metrics by their dimension set. EMF requires metrics in the same
252
+ * CloudWatchMetrics entry to share the same dimension keys and values.
253
+ */
254
+ function groupByDimensions(metrics) {
255
+ const groups = new Map();
256
+ for (const m of metrics) {
257
+ const key = dimensionKey(m.dimensions);
258
+ let group = groups.get(key);
259
+ if (!group) {
260
+ group = { dimensions: m.dimensions, metrics: [] };
261
+ groups.set(key, group);
262
+ }
263
+ group.metrics.push(m);
264
+ }
265
+ return Array.from(groups.values());
266
+ }
267
+ function dimensionKey(dims) {
268
+ const sorted = Object.entries(dims).sort(([a], [b]) => a.localeCompare(b));
269
+ return sorted.map(([k, v]) => `${k}=${v}`).join('&');
270
+ }
@@ -0,0 +1,12 @@
1
+ import type { EmitOptions, MetricDatum, ExternalMetricsRef, MetricsEmitter } from './types.js';
2
+ export { MetricsErrors } from './errors.js';
3
+ export type { MetricsOptions, EmitOptions, MetricDatum, MetricUnit, MetricResolution, ExternalMetricsRef, MetricsEmitter, } from './types.js';
4
+ export declare class Metrics implements MetricsEmitter {
5
+ constructor(..._args: any[]);
6
+ emit(_name: string, _value: number, _options?: EmitOptions): void;
7
+ emitBatch(_metrics: MetricDatum[]): void;
8
+ flush(): void;
9
+ child(_dimensions: Record<string, string>): MetricsEmitter;
10
+ static fromExisting(namespace: string): ExternalMetricsRef;
11
+ }
12
+ //# sourceMappingURL=index.browser.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.browser.d.ts","sourceRoot":"","sources":["../src/index.browser.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,kBAAkB,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAE/F,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,YAAY,EACX,cAAc,EACd,WAAW,EACX,WAAW,EACX,UAAU,EACV,gBAAgB,EAChB,kBAAkB,EAClB,cAAc,GACd,MAAM,YAAY,CAAC;AAEpB,qBAAa,OAAQ,YAAW,cAAc;gBACjC,GAAG,KAAK,EAAE,GAAG,EAAE;IAC3B,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,WAAW,GAAG,IAAI;IACjE,SAAS,CAAC,QAAQ,EAAE,WAAW,EAAE,GAAG,IAAI;IACxC,KAAK,IAAI,IAAI;IACb,KAAK,CAAC,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,cAAc;IAC1D,MAAM,CAAC,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,kBAAkB;CAG1D"}
@@ -0,0 +1,13 @@
1
+ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ export { MetricsErrors } from './errors.js';
4
+ export class Metrics {
5
+ constructor(..._args) { }
6
+ emit(_name, _value, _options) { }
7
+ emitBatch(_metrics) { }
8
+ flush() { }
9
+ child(_dimensions) { return new Metrics(); }
10
+ static fromExisting(namespace) {
11
+ return { __brand: 'ExternalMetricsRef', namespace };
12
+ }
13
+ }
@@ -0,0 +1,20 @@
1
+ import { Scope } from '@aws-blocks/core/cdk';
2
+ import type { ScopeParent } from '@aws-blocks/core';
3
+ import type { MetricsOptions } from './types.js';
4
+ export { MetricsErrors } from './errors.js';
5
+ export type { MetricsOptions, EmitOptions, MetricDatum, MetricUnit, MetricResolution, ExternalMetricsRef, MetricsEmitter, } from './types.js';
6
+ /**
7
+ * CDK construct for Metrics.
8
+ *
9
+ * EMF writes structured JSON to stdout which Lambda captures in CloudWatch Logs.
10
+ * CloudWatch automatically extracts metrics — no additional IAM permissions or
11
+ * environment variables are needed.
12
+ */
13
+ export declare class Metrics extends Scope {
14
+ /** The resolved CloudWatch namespace for metrics emitted by this instance. */
15
+ readonly namespace: string;
16
+ /** Default dimensions applied to every metric emitted by this instance. */
17
+ readonly defaultDimensions: Readonly<Record<string, string>>;
18
+ constructor(scope: ScopeParent, id: string, options?: MetricsOptions);
19
+ }
20
+ //# sourceMappingURL=index.cdk.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cdk.d.ts","sourceRoot":"","sources":["../src/index.cdk.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,EAAE,MAAM,sBAAsB,CAAC;AAC7C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAEjD,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,YAAY,EACX,cAAc,EACd,WAAW,EACX,WAAW,EACX,UAAU,EACV,gBAAgB,EAChB,kBAAkB,EAClB,cAAc,GACd,MAAM,YAAY,CAAC;AAEpB;;;;;;GAMG;AACH,qBAAa,OAAQ,SAAQ,KAAK;IACjC,8EAA8E;IAC9E,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAE3B,2EAA2E;IAC3E,QAAQ,CAAC,iBAAiB,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;gBAEjD,KAAK,EAAE,WAAW,EAAE,EAAE,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,cAAc;CAOpE"}
@@ -0,0 +1,24 @@
1
+ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ import { Scope } from '@aws-blocks/core/cdk';
4
+ export { MetricsErrors } from './errors.js';
5
+ /**
6
+ * CDK construct for Metrics.
7
+ *
8
+ * EMF writes structured JSON to stdout which Lambda captures in CloudWatch Logs.
9
+ * CloudWatch automatically extracts metrics — no additional IAM permissions or
10
+ * environment variables are needed.
11
+ */
12
+ export class Metrics extends Scope {
13
+ /** The resolved CloudWatch namespace for metrics emitted by this instance. */
14
+ namespace;
15
+ /** Default dimensions applied to every metric emitted by this instance. */
16
+ defaultDimensions;
17
+ constructor(scope, id, options) {
18
+ super(id, { parent: scope });
19
+ this.namespace = options?.metrics?.namespace
20
+ ?? options?.namespace
21
+ ?? this.fullId;
22
+ this.defaultDimensions = options?.defaultDimensions ?? {};
23
+ }
24
+ }
@@ -0,0 +1,3 @@
1
+ export { Metrics, MetricsErrors } from './index.aws.js';
2
+ export type { MetricsOptions, EmitOptions, MetricDatum, MetricUnit, MetricResolution, ExternalMetricsRef, MetricsEmitter, } from './types.js';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AACxD,YAAY,EACX,cAAc,EACd,WAAW,EACX,WAAW,EACX,UAAU,EACV,gBAAgB,EAChB,kBAAkB,EAClB,cAAc,GACd,MAAM,YAAY,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ export { Metrics, MetricsErrors } from './index.aws.js';
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Mock (local development) runtime for Metrics.
3
+ *
4
+ * Behavior is identical to the AWS runtime — both write EMF-formatted JSON to
5
+ * stdout. In local dev the output is visible in the terminal; in Lambda it is
6
+ * captured by CloudWatch Logs. No separate mock implementation is needed.
7
+ */
8
+ export { Metrics, MetricsErrors } from './index.aws.js';
9
+ export type { MetricsOptions, EmitOptions, MetricDatum, MetricUnit, MetricResolution, ExternalMetricsRef, MetricsEmitter, } from './types.js';
10
+ //# sourceMappingURL=index.mock.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mock.d.ts","sourceRoot":"","sources":["../src/index.mock.ts"],"names":[],"mappings":"AAGA;;;;;;GAMG;AACH,OAAO,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AACxD,YAAY,EACX,cAAc,EACd,WAAW,EACX,WAAW,EACX,UAAU,EACV,gBAAgB,EAChB,kBAAkB,EAClB,cAAc,GACd,MAAM,YAAY,CAAC"}
@@ -0,0 +1,10 @@
1
+ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ /**
4
+ * Mock (local development) runtime for Metrics.
5
+ *
6
+ * Behavior is identical to the AWS runtime — both write EMF-formatted JSON to
7
+ * stdout. In local dev the output is visible in the terminal; in Lambda it is
8
+ * captured by CloudWatch Logs. No separate mock implementation is needed.
9
+ */
10
+ export { Metrics, MetricsErrors } from './index.aws.js';
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=index.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.test.d.ts","sourceRoot":"","sources":["../src/index.test.ts"],"names":[],"mappings":""}