Amazon CloudWatch Integration
The Amazon CloudWatch integration exports approved nats-sinks metrics as
CloudWatch custom metrics. It is intended for AWS deployments where CloudWatch
is the operational dashboarding and alerting surface.
The connector is part of the observability plane, not the delivery plane. It reads a local metrics snapshot, applies the shared observability policy, and uses the AWS SDK only when a live export is requested. It does not connect to NATS, does not read sink payloads, does not inspect Oracle Database, Oracle MySQL, file-sink output, or other destination records, and never affects ACK, NAK, DLQ, retry, or sink-write decisions.
Security Model
Amazon CloudWatch export is disabled by default. Enable it only after reviewing which aggregate metric names may leave the host and which AWS identity will be used by the separate observability service.
The connector deliberately does not export:
- message payloads,
- NATS subjects,
- message IDs,
- file paths,
- table names,
- Oracle connection details,
- classification values,
- message labels,
- mission metadata,
- AWS account IDs,
- AWS access keys,
- AWS secret keys,
- session tokens,
- CloudWatch endpoints,
- exception messages from the AWS SDK.
Prepared subject-family labels can be exported as CloudWatch dimensions only
when subject_metrics.enabled and cloudwatch.include_metric_labels_as_dimensions
are both explicitly enabled. The default is to suppress prepared labels so a
CloudWatch metric does not accidentally become high-cardinality or reveal
operational routing structure.
AWS API Bounds
The connector uses the PutMetricData API for custom metrics:
AWS currently documents these important limits:
- each
PutMetricDatarequest is limited to 1 MB, - each request can include no more than 1000 metrics,
- metric names are limited to 255 characters,
- AWS permits up to 30 dimensions per metric,
- special numeric values such as
NaNand infinity are rejected.
nats-sinks uses a stricter default posture:
cloudwatch.max_metrics_per_requestdefaults to20,cloudwatch.max_request_bytesis capped at1048576,- static dimensions are capped at
10, - metric labels are not turned into dimensions unless explicitly enabled.
That keeps API cost, request size, and dimension cardinality visible before a deployment goes live.
Installation
The base package does not install boto3. Install the optional CloudWatch extra only on hosts that will perform live CloudWatch export:
python -m pip install "nats-sinks[cloudwatch]"
Dry-run rendering and unit tests do not require boto3. This is intentional so developers can inspect the request shape without AWS credentials.
Configuration
CloudWatch configuration lives inside the same observability policy JSON as the other connectors:
{
"schema": "nats_sinks.observability.policy.v1",
"enabled": true,
"namespace": "mission_ops",
"allowed_metrics": [
"messages_fetched_total",
"messages_acked_total"
],
"allowed_metric_patterns": [],
"denied_metrics": [],
"denied_metric_patterns": [],
"include_observations": false,
"include_legacy": false,
"subjects": [],
"cloudwatch": {
"enabled": true,
"metric_namespace": "nats-sinks/metrics",
"region": "eu-west-1",
"unit": "None",
"storage_resolution": 60,
"dimensions": {
"deployment": "edge",
"environment": "prod"
},
"include_metric_labels_as_dimensions": false,
"timeout_seconds": 5,
"max_retries": 0,
"retry_backoff_seconds": 0.25,
"stale_after_seconds": 60,
"max_metrics_per_request": 20,
"max_request_bytes": 1048576
}
}
Options
| Option | Default | Description |
|---|---|---|
cloudwatch.enabled |
false |
Enables Amazon CloudWatch export when the top-level observability policy is also enabled. |
cloudwatch.metric_namespace |
nats-sinks/metrics |
CloudWatch custom metric namespace. It must not start with AWS/, must not contain colons, and is limited to safe ASCII characters. |
cloudwatch.region |
null |
AWS region used by the SDK client. Required when CloudWatch export is enabled. The dry-run output does not include this value. |
cloudwatch.unit |
None |
CloudWatch metric unit. Valid values are the units accepted by MetricDatum, such as Count, Seconds, Milliseconds, Bytes, Percent, and None. |
cloudwatch.storage_resolution |
60 |
CloudWatch storage resolution. Use 60 for standard resolution or 1 for high-resolution custom metrics. |
cloudwatch.dimensions |
{} |
Static low-cardinality dimensions added to every metric. Dimension names that look sensitive or high-cardinality, such as subject, classification, label, message, table, file, host, user, token, secret, or key, are rejected. |
cloudwatch.include_metric_labels_as_dimensions |
false |
When true, prepared labeled_metrics rows can export their bounded labels as dimensions. Keep this disabled unless subject-family sharing has been reviewed. |
cloudwatch.timeout_seconds |
5 |
Connection and read timeout used when the boto3 client is created. |
cloudwatch.max_retries |
0 |
Bounded connector-level retries after the first failed PutMetricData attempt. |
cloudwatch.retry_backoff_seconds |
0.25 |
Delay between connector-level retry attempts. |
cloudwatch.stale_after_seconds |
null |
Optional maximum metrics snapshot age. When set, stale snapshots fail closed unless --allow-stale is used. |
cloudwatch.max_metrics_per_request |
20 |
Maximum metric datum objects per PutMetricData request. The local cap cannot exceed the AWS request limit of 1000. |
cloudwatch.max_request_bytes |
1048576 |
Maximum rendered request size. Oversized requests fail closed before the AWS SDK is called. |
Dry Run
Dry-run mode renders the PutMetricData request list without importing boto3,
loading credentials, or calling AWS:
nats-sink-observe cloudwatch-export \
/var/lib/nats-sink/metrics.json \
/etc/nats-sinks/observability.prometheus.json \
--dry-run
Example output:
[
{
"MetricData": [
{
"Dimensions": [
{
"Name": "deployment",
"Value": "edge"
},
{
"Name": "environment",
"Value": "prod"
}
],
"MetricName": "mission_ops_messages_fetched_total",
"StorageResolution": 60,
"Unit": "None",
"Value": 256.0
}
],
"Namespace": "nats-sinks/metrics"
}
]
Notice that the request body does not contain the AWS region, AWS account, AWS credentials, NATS subject, payload, classification, labels, file path, table name, or destination address.
Live Export
Live export uses the optional boto3 path:
nats-sink-observe cloudwatch-export \
/var/lib/nats-sink/metrics.json \
/etc/nats-sinks/observability.prometheus.json
Successful output is intentionally short:
Amazon CloudWatch export: attempted=true delivered=true attempts=1 requests=1 metrics=2 message=Amazon CloudWatch export delivered
If the connector exhausts its bounded retries, the CLI exits with status 3
and prints a sanitized category:
Amazon CloudWatch export: attempted=true delivered=false attempts=3 requests=1 metrics=2 message=Amazon CloudWatch export failed with TimeoutError
The message does not include AWS account IDs, endpoints, regions, access keys, request IDs, exception messages, or metric payload details.
AWS Identity And IAM
Prefer short-lived or platform-managed AWS identity:
- EC2 instance profiles,
- ECS task roles,
- EKS workload identity or IRSA,
- AWS SSO profiles for local operator testing,
- environment-backed credentials only for short-lived local checks.
Do not store AWS access keys in the observability policy. Do not pass access
keys through command-line arguments. The observability service should have only
the permissions required to call cloudwatch:PutMetricData for the approved
namespace and should not receive broad CloudWatch, IAM, or account-management
permissions.
Subject-Family Dimensions
Prepared subject-family metrics are disabled by default. If an approved policy
has already generated safe labeled_metrics rows, CloudWatch can turn those
prepared labels into dimensions:
{
"subject_metrics": {
"enabled": true,
"rules": [
{
"subject": "sensor.>",
"action": "allow",
"label": "sensor_track"
}
]
},
"cloudwatch": {
"enabled": true,
"metric_namespace": "nats-sinks/metrics",
"region": "eu-west-1",
"include_metric_labels_as_dimensions": true
}
}
With the flag enabled, a prepared row can render like this:
{
"MetricName": "mission_ops_messages_fetched_total",
"Value": 4.0,
"Unit": "None",
"StorageResolution": 60,
"Dimensions": [
{
"Name": "subject_family",
"Value": "sensor_track"
}
]
}
Do not enable this for raw subject names. Use stable family labels, redacted labels, or hashed labels from the subject-aware observability runbook.
Service Separation
Run CloudWatch export separately from the sink worker. The sink worker writes the local metrics snapshot. The CloudWatch service reads that snapshot and the observability policy.
Example oneshot unit:
[Unit]
Description=nats-sinks Amazon CloudWatch export
After=network-online.target
[Service]
Type=oneshot
User=nats-sinks-observe
Group=nats-sinks
ExecStart=/usr/local/bin/nats-sink-observe cloudwatch-export /var/lib/nats-sink/metrics.json /etc/nats-sinks/observability.prometheus.json
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/lib/nats-sink
For periodic export, use a systemd timer. Keep AWS credentials scoped to the observability service identity, not the delivery worker, whenever practical.
Testing
The default test suite uses fake CloudWatch clients and dry-run request rendering. It does not require AWS credentials:
python -m pytest tests/unit/test_cloudwatch_observability.py tests/unit/test_observability_cli.py -q
The focused tests cover:
- disabled-by-default behavior,
- shared allow-list and deny-list filtering,
- observation inclusion controls,
- static dimension validation,
- prepared label suppression by default,
- explicit prepared labels as dimensions,
- request chunking,
- local request-size failure,
- bounded retry behavior,
- sanitized failure summaries,
- CLI dry-run output,
- stale snapshot rejection.
Live AWS integration testing should use a disposable AWS account or controlled lab account, a short-lived role, an approved namespace, and a narrow metric allow list. Do not run live CloudWatch tests from the default unit suite.
Limitations
The first CloudWatch connector intentionally does not:
- publish logs,
- create dashboards,
- create alarms,
- manage CloudWatch Metric Streams,
- create or manage IAM roles,
- export raw NATS subjects,
- export raw message labels,
- export classification or mission metadata values,
- guarantee CloudWatch ingestion, retention, or alert evaluation.
It provides a bounded custom-metric export path for approved aggregate
nats-sinks metrics only.