@aws-blocks/bb-metrics 0.1.1 → 0.1.2

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.
Files changed (3) hide show
  1. package/DESIGN.md +130 -0
  2. package/README.md +2 -0
  3. package/package.json +3 -2
package/DESIGN.md ADDED
@@ -0,0 +1,130 @@
1
+ # Metrics — Design
2
+
3
+ Design document for Metrics. For usage, see [README.md](./README.md).
4
+
5
+ **Package:** `@aws-blocks/bb-metrics`
6
+ **Type:** Primitive (no new infrastructure created)
7
+ **AWS Service:** Amazon CloudWatch Metrics (via Embedded Metric Format)
8
+
9
+ ## Key Design Decision: EMF over PutMetricData
10
+
11
+ Metrics are emitted using **CloudWatch Embedded Metric Format (EMF)** rather than calling the `PutMetricData` API directly.
12
+
13
+ ### Why EMF?
14
+
15
+ | Concern | PutMetricData | EMF |
16
+ |---------|---------------|-----|
17
+ | **Latency impact** | HTTP call to CloudWatch (~5-50ms) | Synchronous stdout write (~0ms) |
18
+ | **IAM permissions** | Requires `cloudwatch:PutMetricData` | Uses CloudWatch Logs (Lambda has this by default) |
19
+ | **Batching** | Developer must buffer and flush | CloudWatch extracts metrics from log lines automatically |
20
+ | **Error handling** | Network failures require retries | Stdout never fails (kernel-buffered) |
21
+ | **API surface** | Async (`Promise<void>`) | Sync (`void`) — simpler DX |
22
+ | **Cost** | ~$0.01 per 1000 PutMetricData API calls + metric storage | Log ingestion (already paid) + metric storage only |
23
+ | **Cold start** | AWS SDK init adds ~100ms | Zero additional cold start |
24
+
25
+ ### Trade-offs
26
+
27
+ - **No aggregation control:** EMF extracts metrics at the data point level. If you emit `Latency: 42ms` 1000 times in one invocation, CloudWatch stores 1000 data points (or groups them into a single EMF document with multiple values). With PutMetricData you could pre-aggregate into a StatisticSet.
28
+ - **Log volume:** Each emit writes a JSON line to CloudWatch Logs. High-frequency metrics increase log volume (and log storage cost). Mitigated by CloudWatch Logs Infrequent Access class or retention policies.
29
+ - **Maximum metrics per document:** EMF supports up to 100 metrics per JSON document. The `emitBatch` limit of 100 matches this constraint (vs PutMetricData's 1000 per call).
30
+
31
+ ### Why the trade-offs are acceptable
32
+
33
+ 1. Most BB users emit <100 metrics per invocation — log volume is negligible.
34
+ 2. The simplicity of a synchronous, zero-config API far outweighs the rare need for client-side aggregation.
35
+ 3. Lambda log costs are typically dominated by application logs, not metric EMF lines.
36
+
37
+ ## Architecture
38
+
39
+ ### Runtime Flow (AWS + Local)
40
+
41
+ ```
42
+ emit('Count', 1, { dimensions: { endpoint: '/api' } })
43
+
44
+ ├─ validateMetricName('Count')
45
+ ├─ mergeDimensions(defaults, { endpoint: '/api' })
46
+ ├─ validateDimensions(merged)
47
+
48
+ └─ writeEmf(namespace, [{ name, value, unit, dimensions, timestamp, resolution }])
49
+
50
+ ├─ groupByDimensions(metrics) // EMF requires same dims per entry
51
+
52
+ └─ for each group:
53
+ process.stdout.write(JSON.stringify(emfPayload) + '\n')
54
+ ```
55
+
56
+ ### EMF Document Structure
57
+
58
+ ```json
59
+ {
60
+ "_aws": {
61
+ "Timestamp": 1718450000000,
62
+ "CloudWatchMetrics": [{
63
+ "Namespace": "MyApp/Orders",
64
+ "Dimensions": [["service", "endpoint"]],
65
+ "Metrics": [
66
+ { "Name": "RequestCount", "Unit": "Count", "StorageResolution": 60 },
67
+ { "Name": "Latency", "Unit": "Milliseconds", "StorageResolution": 60 }
68
+ ]
69
+ }]
70
+ },
71
+ "service": "orders",
72
+ "endpoint": "/api",
73
+ "RequestCount": 1,
74
+ "Latency": 42
75
+ }
76
+ ```
77
+
78
+ ### Dimension Grouping
79
+
80
+ EMF requires all metrics in a single `CloudWatchMetrics` entry to share the same dimension keys and values. When `emitBatch` receives metrics with different dimension sets, they are grouped and written as separate JSON lines.
81
+
82
+ ### Child Emitters
83
+
84
+ `child(dimensions)` returns a lightweight `ChildMetrics` object (not a Scope node) that:
85
+ - Inherits the parent's namespace
86
+ - Merges the provided dimensions on top of the parent's `defaultDimensions`
87
+ - Supports further nesting via `child()` on the child itself
88
+
89
+ This enables per-request or per-endpoint metric scoping without creating new Scope nodes.
90
+
91
+ ## Infrastructure (CDK)
92
+
93
+ Unlike most Building Blocks, Metrics does **not** create any AWS resources. CloudWatch namespaces are created implicitly on first metric data point arrival.
94
+
95
+ The CDK construct creates no AWS resources and adds no environment variables or IAM grants. EMF uses CloudWatch Logs (which Lambda already has), so no `cloudwatch:PutMetricData` grant is needed. The construct only:
96
+ 1. **Resolves the namespace:** Computes the resolved `namespace` and exposes it as a readonly property.
97
+ 2. **Exposes `defaultDimensions`:** Exposes `defaultDimensions` as a readonly property so that other CDK-time consumers (like the Dashboard BB) can read them and build matching CloudWatch widget queries.
98
+
99
+ ### Namespace Resolution Order
100
+
101
+ ```
102
+ ExternalMetricsRef.namespace (fromExisting)
103
+ → options.namespace (constructor arg)
104
+ → scope.fullId (default)
105
+ ```
106
+
107
+ ## Mock Implementation
108
+
109
+ There is no separate mock — the AWS runtime (`index.aws.ts`) and the mock (`index.mock.ts`) are identical. Both write EMF JSON to stdout. In local dev, the output is visible in the terminal; in Lambda, it is captured by CloudWatch Logs.
110
+
111
+ This is unique among Building Blocks. Most BBs need a mock because their AWS runtime makes network calls (DynamoDB, S3, SQS, etc.). Metrics via EMF only writes to stdout, which works identically in all environments.
112
+
113
+ ### Mock vs AWS Behavior Differences
114
+
115
+ | Behavior Difference | Impact | Mitigation |
116
+ |------------|--------|------------|
117
+ | No CloudWatch extraction | Metrics are written but not extracted into CloudWatch locally | Expected — local dev is for correctness testing, not dashboards |
118
+ | No alarms | Threshold breaches are not detected locally | Document the gap — alarms require CloudWatch infrastructure |
119
+ | No dashboards | Cannot preview dashboard visualizations locally | Document the gap — dashboards are a CloudWatch console feature |
120
+ | No namespace-scoped IAM | Permission errors only surface in AWS | IAM is handled by CDK grants automatically |
121
+ | No log retention limits | Stdout grows unbounded in local dev | Terminal scrollback is the natural limit |
122
+
123
+ ## Validation
124
+
125
+ All validation matches CloudWatch constraints:
126
+ - **Metric name:** Non-empty, max 1024 characters
127
+ - **Dimensions:** Max 30 key-value pairs, non-empty keys and values, max 1024 chars each
128
+ - **Batch size:** Max 100 metrics per `emitBatch` call (EMF document limit)
129
+
130
+ Validation runs synchronously before the stdout write. Failures throw typed errors (`MetricsErrors.*`) that can be caught with `isBlocksError()`.
package/README.md CHANGED
@@ -6,6 +6,8 @@ Custom application metrics backed by Amazon CloudWatch (via Embedded Metric Form
6
6
 
7
7
  **When NOT to use:** If you need structured log output, use `Logging`. If you need distributed request tracing, use `Tracing`. If you need to store time-series data for querying, use `Database` or `DistributedTable`.
8
8
 
9
+ > Design & mock parity details: [DESIGN.md](./DESIGN.md)
10
+
9
11
  ## API
10
12
 
11
13
  ```typescript
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "@aws-blocks/bb-metrics",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "author": "Amazon Web Services",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
7
7
  "files": [
8
8
  "dist",
9
9
  "README.md",
10
+ "DESIGN.md",
10
11
  "src",
11
12
  "LICENSE"
12
13
  ],
@@ -28,7 +29,7 @@
28
29
  },
29
30
  "dependencies": {
30
31
  "@aws-blocks/core": "^0.1.1",
31
- "@aws-blocks/bb-logger": "^0.1.1"
32
+ "@aws-blocks/bb-logger": "^0.1.2"
32
33
  },
33
34
  "devDependencies": {
34
35
  "@types/node": "^20.0.0",