@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/src/types.ts ADDED
@@ -0,0 +1,119 @@
1
+ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ /**
5
+ * Shared types for the Metrics building block.
6
+ * This file has zero runtime dependencies — types only.
7
+ */
8
+ import type { ChildLogger } from '@aws-blocks/bb-logger';
9
+
10
+ /**
11
+ * Units supported by CloudWatch. Using a unit enables automatic conversions
12
+ * in dashboards (e.g., Bytes → Megabytes) and clearer Y-axis labels.
13
+ *
14
+ * Covers the most common use cases. For unlisted units, use 'None'.
15
+ */
16
+ export type MetricUnit =
17
+ | 'Count'
18
+ | 'Seconds'
19
+ | 'Milliseconds'
20
+ | 'Microseconds'
21
+ | 'Bytes'
22
+ | 'Kilobytes'
23
+ | 'Megabytes'
24
+ | 'Gigabytes'
25
+ | 'Percent'
26
+ | 'Bits/Second'
27
+ | 'None';
28
+
29
+ /**
30
+ * Storage resolution for a metric data point.
31
+ * - 'standard' (60s) — default, lower cost, 15-day retention at full resolution
32
+ * - 'high' (1s) — higher cost, 3-hour retention at full resolution then aggregated
33
+ */
34
+ export type MetricResolution = 'standard' | 'high';
35
+
36
+ /**
37
+ * Configuration for the Metrics building block.
38
+ */
39
+ export interface MetricsOptions {
40
+ /**
41
+ * CloudWatch namespace for all metrics emitted by this instance.
42
+ * Namespaces group related metrics in CloudWatch dashboards and alarms.
43
+ * Defaults to the scope's `fullId` (e.g., 'myapp-appMetrics').
44
+ */
45
+ namespace?: string;
46
+
47
+ /**
48
+ * Dimensions applied to every metric emitted by this instance.
49
+ * Useful for shared context like service name or environment.
50
+ * Per-emit dimensions are merged on top of these (per-emit wins on conflict).
51
+ */
52
+ defaultDimensions?: Record<string, string>;
53
+
54
+ /**
55
+ * Wrap an existing CloudWatch namespace instead of creating one.
56
+ * When set, `namespace` is ignored.
57
+ */
58
+ metrics?: ExternalMetricsRef;
59
+ /** Optional logger for internal operations. When omitted, a default Logger at error level is created. */
60
+ logger?: ChildLogger;
61
+ }
62
+
63
+ /**
64
+ * Options for a single metric emission.
65
+ */
66
+ export interface EmitOptions {
67
+ /** Unit of the metric value. Defaults to 'None'. */
68
+ unit?: MetricUnit;
69
+ /**
70
+ * Dimensions to attach to this data point (max 30 total including defaults).
71
+ * Merged with `defaultDimensions` — per-emit dimensions take precedence.
72
+ */
73
+ dimensions?: Record<string, string>;
74
+ /** Timestamp for the data point. Defaults to now. */
75
+ timestamp?: Date;
76
+ /**
77
+ * Storage resolution. 'standard' = 60-second aggregation (default).
78
+ * 'high' = 1-second aggregation (higher cost, useful for spike detection).
79
+ */
80
+ resolution?: MetricResolution;
81
+ }
82
+
83
+ /**
84
+ * A single metric data point for batch emission.
85
+ */
86
+ export interface MetricDatum {
87
+ /** Metric name (non-empty, max 1024 characters). */
88
+ name: string;
89
+ /** Numeric value. */
90
+ value: number;
91
+ /** Unit of the metric value. Defaults to 'None'. */
92
+ unit?: MetricUnit;
93
+ /** Dimensions to attach (max 30 total including defaults). */
94
+ dimensions?: Record<string, string>;
95
+ /** Timestamp for the data point. Defaults to now. */
96
+ timestamp?: Date;
97
+ /** Storage resolution. Defaults to 'standard'. */
98
+ resolution?: MetricResolution;
99
+ }
100
+
101
+ /**
102
+ * Reference to an existing CloudWatch namespace not managed by this BB.
103
+ * Created via `Metrics.fromExisting()`.
104
+ */
105
+ export interface ExternalMetricsRef {
106
+ readonly __brand: 'ExternalMetricsRef';
107
+ readonly namespace: string;
108
+ }
109
+
110
+ /**
111
+ * A child metrics instance with inherited dimensions and namespace.
112
+ * Provides the same metric emission methods but is not a Scope node.
113
+ */
114
+ export interface MetricsEmitter {
115
+ emit(name: string, value: number, options?: EmitOptions): void;
116
+ emitBatch(metrics: MetricDatum[]): void;
117
+ flush(): void;
118
+ child(dimensions: Record<string, string>): MetricsEmitter;
119
+ }
@@ -0,0 +1,97 @@
1
+ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ import { MetricsErrors } from './errors.js';
5
+
6
+ function blocksError(name: string, message: string): Error {
7
+ const err = new Error(`${name}: ${message}`);
8
+ err.name = name;
9
+ return err;
10
+ }
11
+
12
+ /**
13
+ * Validate a metric name against CloudWatch constraints.
14
+ * - Must be non-empty
15
+ * - Max 1024 characters
16
+ */
17
+ export function validateMetricName(name: string): void {
18
+ if (!name || name.length === 0) {
19
+ throw blocksError(MetricsErrors.InvalidMetricName, 'Metric name must not be empty');
20
+ }
21
+ if (name.length > 1024) {
22
+ throw blocksError(MetricsErrors.InvalidMetricName, `Metric name exceeds 1024 characters (got ${name.length})`);
23
+ }
24
+ }
25
+
26
+ /**
27
+ * Validate dimensions against CloudWatch constraints.
28
+ * - Max 30 dimension key-value pairs
29
+ * - Keys and values must be non-empty
30
+ * - Keys and values max 1024 characters each
31
+ */
32
+ export function validateDimensions(dimensions: Record<string, string>): void {
33
+ const entries = Object.entries(dimensions);
34
+ if (entries.length > 30) {
35
+ throw blocksError(MetricsErrors.InvalidDimensions, `Dimensions exceed 30 entries (got ${entries.length})`);
36
+ }
37
+ for (const [key, value] of entries) {
38
+ if (!key || key.length === 0) {
39
+ throw blocksError(MetricsErrors.InvalidDimensions, 'Dimension key must not be empty');
40
+ }
41
+ if (key.length > 1024) {
42
+ throw blocksError(MetricsErrors.InvalidDimensions, `Dimension key exceeds 1024 characters: '${key.substring(0, 50)}...'`);
43
+ }
44
+ if (value === undefined || value === null || value.length === 0) {
45
+ throw blocksError(MetricsErrors.InvalidDimensions, `Dimension value for key '${key}' must not be empty`);
46
+ }
47
+ if (value.length > 1024) {
48
+ throw blocksError(MetricsErrors.InvalidDimensions, `Dimension value for key '${key}' exceeds 1024 characters`);
49
+ }
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Validate batch size (max 100 metrics per EMF document).
55
+ */
56
+ export function validateBatchSize(count: number): void {
57
+ if (count > 100) {
58
+ throw blocksError(MetricsErrors.BatchTooLarge, `Batch exceeds 100 metrics (got ${count})`);
59
+ }
60
+ }
61
+
62
+ /** Valid characters for a CloudWatch namespace: alphanumeric, dot, underscore, hash, colon, slash, hyphen, space. */
63
+ const NAMESPACE_PATTERN = /^[a-zA-Z0-9._#:/ -]+$/;
64
+
65
+ /**
66
+ * Validate a namespace against CloudWatch constraints.
67
+ * - Must be non-empty (at least one non-whitespace character)
68
+ * - Max 256 characters
69
+ * - Only valid chars: [a-zA-Z0-9._#:/ -]
70
+ * - Must not start with "AWS/" (reserved for AWS services)
71
+ */
72
+ export function validateNamespace(namespace: string): void {
73
+ if (!namespace || namespace.trim().length === 0) {
74
+ throw blocksError(MetricsErrors.InvalidNamespace, 'Namespace must not be empty');
75
+ }
76
+ if (namespace.length > 256) {
77
+ throw blocksError(MetricsErrors.InvalidNamespace, `Namespace exceeds 256 characters (got ${namespace.length})`);
78
+ }
79
+ if (!NAMESPACE_PATTERN.test(namespace)) {
80
+ throw blocksError(MetricsErrors.InvalidNamespace, `Namespace contains invalid characters: '${namespace.substring(0, 50)}'. Valid: [a-zA-Z0-9._#:/ -]`);
81
+ }
82
+ if (namespace.startsWith('AWS/')) {
83
+ throw blocksError(MetricsErrors.InvalidNamespace, `Namespace must not start with 'AWS/' (reserved for AWS services): '${namespace}'`);
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Merge default dimensions with per-emit dimensions.
89
+ * Per-emit dimensions take precedence on key conflict.
90
+ */
91
+ export function mergeDimensions(
92
+ defaults: Record<string, string>,
93
+ overrides?: Record<string, string>,
94
+ ): Record<string, string> {
95
+ if (!overrides || Object.keys(overrides).length === 0) return { ...defaults };
96
+ return { ...defaults, ...overrides };
97
+ }