src/go/plugin/go.d/collector/cloudwatch/ARCHITECTURE.md
Maintainer-oriented map of cloudwatch, written to be read top to bottom as a
journey: the plain model and the big picture first, then the lifecycle and the
collection cycle, then each stage in detail, and finally the profile schema,
invariants, and where to change things. Per-service metric and chart details live
in the profile YAMLs (config/go.d/cloudwatch.profiles/) and the focused tests,
not here.
The one rule that shapes most of what follows:
GetMetricDatais billed by requested metrics, so the collector normally queries each series on the first sharedupdate_everytick that observes a newer aligned effective-period window, and re-emits cached values in between. Transient failures use bounded per-query backoff. Whenperiod < update_every, collection ticks also quantize successful execution and intermediate eligible windows can be skipped.
cloudwatch is a framework-V2 go.d collector that pulls AWS CloudWatch metrics
for a curated set of AWS services and renders them as dynamic Netdata charts. It
is profile-driven: each service is a YAML profile (see Profiles) declaring its
CloudWatch namespace, the dimension set that identifies one instance, the
metrics/statistics to query, and a chart template — so adding or adjusting a
service is usually a YAML edit, not Go.
It does not generate per-account or per-resource nodes: AWS resources are chart
instances keyed by by_labels. All output follows the job's configured
vnode, when present, otherwise the Agent host; the collector creates no host
scopes of its own.
Config picks the services; the collector discovers what actually exists when a
profile has identifying dimensions, queries each resulting instance on a
per-period schedule, and renders the results as charts under the cloudwatch.
namespace. A profile whose dimensions are all constants already describes one
complete instance, so compilation creates that static instance directly.
flowchart LR
cfg("Config
credentials · targets · ordered rules")
static("Static instance
all dimensions known")
disc("Discover dynamic instances
ListMetrics
cheap / free tier")
tags("Resolve tags
membership + labels")
plan("Plan + state
stable query + policy")
query("Query
GetMetricData
billed — the cost driver")
activity("Collector activity
SDK invocations · calculated units · profile estimates · query items")
store("metrix store
gauges + labels")
charts("Dynamic charts
cloudwatch.*")
cfg --> static --> plan
cfg --> disc --> tags --> plan --> query --> store --> charts
disc --> activity --> store
query --> activity
classDef input fill:#eef1f4,stroke:#8b949e,color:#24292f;
classDef sched fill:#ece2ff,stroke:#8250df,color:#3b1f6b;
classDef effect fill:#ffe6cc,stroke:#e36209,color:#5a2a00;
classDef commit fill:#d8f3e0,stroke:#2da44e,color:#0b3d1f;
class cfg input
class static,disc,tags,plan sched
class query effect
class store,charts commit
Each collection cycle (collect.go), in order:
plan.go, identity.go);ListMetrics scan per (target, region, namespace) and
match each participating profile against that stream (discover.go);tags.go);query_plan.go);query_executor.go);observe.go);metrix, stamped with
{account_id, region, <dimension labels>}, and re-emit not-due series (query_emit.go);activity.go);chart.go).Framework V2: Init → Check → repeated Collect; there is no background
Run (the collector does not implement CollectorV2Runner). collector.go
owns the Collector struct and lifecycle.
flowchart TD
Init("Init
applyDefaults + validate
no AWS calls")
Check("Check")
Runtime("ensurePlan
load catalog · compile rules
build + cache chart template")
Targets("ensureTargets
concurrent STS GetCallerIdentity
fail-soft · retry pending")
Post("framework postCheck
validate ChartTemplateYAML via chartengine")
Collect("Collect
every update_every")
Cycle("collect cycle
discover → tags → plan → schedule → execute → observe")
Init --> Check
Check --> Runtime --> Targets --> Post
Post --> Collect --> Cycle --> Collect
classDef input fill:#eef1f4,stroke:#8b949e,color:#24292f;
classDef sched fill:#ece2ff,stroke:#8250df,color:#3b1f6b;
classDef commit fill:#d8f3e0,stroke:#2da44e,color:#0b3d1f;
classDef core fill:#cfe4ff,stroke:#1f6feb,stroke-width:3px,color:#0b2948;
class Init,Check input
class Runtime,Targets sched
class Post commit
class Collect,Cycle core
applyDefaults, validate) — no network.postCheck
validates ChartTemplateYAML() through chartengine.LoadYAML before the
first Collect; an unbuilt template would fail the job.ensurePlan short-circuits once
compiled; ensureTargets retries only unresolved targets.metrix store is created
once in New and persists — it is not recreated.collect.go ties the stages together, in order:
flowchart TD
A("ensure collection plan + target account ids")
B("refreshDiscovery
shared ListMetrics scans per compatible target scope")
C("refreshTags
resolve filter membership + non-identity labels")
D("currentQueryPlan
cached until target/discovery/tag inputs change")
E("dueQueries
keep stable queries with a newer eligible window")
F("executeQueries
batched · concurrent GetMetricData")
G("apply outcomes + emit
write gauges · re-emit retained")
H("publish collector activity
absolute interval gauges")
A --> B --> C --> D --> E --> F --> G --> H
classDef input fill:#eef1f4,stroke:#8b949e,color:#24292f;
classDef sched fill:#ece2ff,stroke:#8250df,color:#3b1f6b;
classDef effect fill:#ffe6cc,stroke:#e36209,color:#5a2a00;
classDef commit fill:#d8f3e0,stroke:#2da44e,color:#0b3d1f;
class A input
class B,C,D,E sched
class F effect
class G,H commit
config.go, config_validate.go, plan.go, plan_compiler.go, and
runtime.go separate the raw operator contract,
compiler state, and installed execution plan:
Config.validate
decides whether the resulting configuration is valid during Init / Check.metrics selects each profile's
default-enabled series. Profiles without a metric group keep those defaults. A
group includes defaults unless defaults: false, then adds its exact included
MetricNames; statistics resolve from the metric entry, group, or profile in that
order. Group query overrides attach only to explicit include[] expansions;
item query overrides attach only to that item's expanded metric/statistic series.rule_defaults.filters.resource_tags is inherited when a rule omits
filters.resource_tags; a present list replaces the default and [] disables it.
Predicates are canonicalized once: exact case-sensitive keys are ANDed and the
exact values for one key are ORed.compileConfig coordinates a private staged compiler that resolves every reference, rejects unused credential/target
definitions, applies profile defaults/include/exclude semantics, rejects duplicate
profile groups and normalized statistics, pre-indexes profile MetricNames/statistics,
resolves inherited and replaced statistics into canonical exact exported-series
descriptors, rejects overlap between repeated explicit-item expansions, intersects
intrinsic supported regions, enforces target/role partition consistency, and
emits immutable ordered scopes.limits.max_instances and limits.max_discovery_groups are operator safeguards for final selected instances and
intended discovery breadth.Which profiles to query is decided by config, not by discovery (see Profiles):
the selected profiles are the CloudWatch namespaces ListMetrics runs against.
Discovery then finds which instances of those profiles exist per target and region.
discover.go. refreshDiscovery re-runs only when the snapshot TTL
(discovery.refresh_every, default 300s) has expired.
All-constant scopes are omitted from discovery groups. A static-only plan makes
refreshDiscovery a true no-op: no ListMetrics, snapshot installation, TTL
advance, or dependent tag/query invalidation.
discoveryGroups coalesces compiled scopes by target, region, and namespace.
Each target/profile/region matcher first evaluates the full horizons of its selected series;
the shared namespace scan then takes the least restrictive result, so
one long-horizon participant disables PT3H instead of creating a redundant
filtered ListMetrics stream beside the unfiltered superset.
discoverAll fans out over those groups concurrently
(bounded by apiConcurrency), with one CloudWatch client per (target, region).discoverAll uses two phases: every non-skipped group that resolves a client
attempts its first admitted ListMetrics operation before any group may request
a continuation page. Skipped groups and client-resolution failures consume no
operation budget. Continuations share the remaining job-level budget. Explicit
authorization denial cancels queued namespace work only in the same
(target, region) lane.discoveryGroupScanner pages one shared namespace and applies an index
compiled from each profile's canonical exact dimension-name set.
Each returned metric's dimensions are canonicalized once; profiles with a different
shape are rejected by the index lookup, while same-shape profiles evaluate only
their pinned constants. This collapses CloudWatch's multi-granularity fan-out to
the chosen instance grain and dedups shared instances. Constant mismatches fail
closed, so a constant dimension can never merge distinct instances onto one
unlabeled series.ListMetrics RecentlyActive=PT3H
filter is applied only when every selected series participating in the shared
target/region/namespace scan has publication_delay + lookback + period ≤ 3h. PT3H is the only value CloudWatch
accepts, so applying it to a daily profile (S3) would hide the metric most of
the day. Configurable (discovery.recently_active_only, default true).limits.max_discovery_groups bounds unique (target, region, namespace) groups
(default 64, valid 1..100). Compatible rules and profiles share one group. The
default is an accidental-expansion safeguard; larger jobs may raise it to 100,
while larger collection must be split across jobs. Each group is additionally bounded to 100 pages, 50,000 scanned metrics,
1,000,000 residual same-shape profile matches, and 20,000 candidate profile instances;
repeated pagination tokens fail the whole group rather than truncating it. The
residual bound was measured after indexed routing and prevents adversarial
metric-by-profile CPU expansion without limiting profile count directly.timeout is shared by the whole discovery stage. Admission happens
immediately before every actual first-page or continuation call; the AWS SDK may
still retry an admitted operation up to five wire attempts.buildDiscoverySnapshot stores instances for
successful target/region/namespace groups and carries forward the previous
instances for errored groups, so a transient group failure never drops series.
The merged effective snapshot is rechecked against the aggregate candidate-count
and weighted-memory bounds before installation, preventing rotating partial
failures from accumulating retained groups beyond the refresh envelope. Only a
first-ever pass where every group errors is fatal only when no executable static
scope exists. With static work, the empty prior dynamic state is retained and a
retry is scheduled after discovery.refresh_every.discovery.refresh_every. A first-ever aggregate failure
is returned to the job runner only when no executable static scope exists.
Otherwise static queries continue while dynamic discovery waits for its retry.
Parent cancellation changes neither snapshot state nor retry scheduling and is
always returned.tags.go, tag_fetch.go, tagjoin.go, and tagresolve.go form the stage
between discovery and query planning. Resource-tag predicates decide which
discovered instances a rule may query. Separately, labels.resource_tags
copies selected AWS tags to non-identity chart labels.
{target, region}. Policy scopes sharing target, region, and canonical predicate
share a fetch group. Each request supplies native TagFilters and the union of
compatible ResourceTypeFilters; pages are streamed directly into membership and
label indexes, locally rechecked, and intersected with discovered candidates.
Fetch topology is rebuilt only when discovery changes, and predicate groups share
one candidate index per target/region/profile. Cached results retain membership,
labels, shared membership ids, and freshness—not fetch-time candidate maps.unknown. On the first failure,
its selected series are withheld for every candidate and reserved from lower-priority
rules; disjoint series remain eligible. After a success, last-known matched members
continue to query those series while the same series remain reserved for every other
candidate. Freshness and retry state are fetch-group-local: failed groups retry next
collect while successful groups keep their result until its discovery TTL expires.
Optional label enrichment carries last-known labels and never controls identity.tagresolve.go). resolveTagPlan turns
labels.resource_tags into a per-profile awsKey → label plan once. Global config
validation rejects malformed or duplicate entries; profile-specific collisions with
account_id, region, or dimension labels are skipped with one warning. The optional
label resolves collisions; the default is the sanitized key (Name → name).tagjoin.go). RGTA returns ARN + tags; the cache is keyed by
the profile's ARN-projectable joinKey. A namespace-bound per-profile mapper (a
default single-component resource-id extractor plus overrides for ALB/NLB/target-group/ECS/OpenSearch/
Step-Functions) derives the joinKey from the ARN, and the instance side projects its
dimension values onto the same key. The compiler accepts a registered mapper only
when an override retains its expected CloudWatch namespace and every join dimension
remains identifying (not constant). Parent-resource profiles (S3,
DynamoDB-operation, ALB-target, and PrivateLink endpoint-by-subnet) key on the
parent dimension so children inherit its tags.
A default-selected profile without a safe association is skipped when filtering is
effective; explicitly including one is a config error unless that rule disables or
replaces the filter. Labels-only use remains best-effort for unsupported profiles.labels ({account_id, region, <dims>}) and tagLabels
travel separately through plannedQuery. writeSample emits labels + tagLabels
in a fresh slice; the stable query key uses identity labels but excludes tag labels,
so a tag change never churns query state. chartengine auto_intersection then
promotes the tag labels as non-identity chart labels—no template change. A changed
effective label set invalidates the plan once; the current plan supplies the new
labels and the normal numeric snapshot causes chartengine to emit a label-only
chart-definition update for an existing chart.The query subsystem is easiest to understand as one series moving through the same state machine on every collection cycle:
resolved policy + discovered instance + selected exported series
→ stable query identity
→ aligned eligible window
→ due or retry decision
→ cost-aware GetMetricData batch
→ per-query outcome
→ completion, retry, and observation state
→ retained value, synthetic zero, or gap
The rolling window is end = align_down(now - publication_delay, period) and
start = end - lookback. For example, with period=5m, lookback=15m,
publication_delay=10m, and now=12:17, the eligible window is
[11:50, 12:05). The next window becomes eligible at 12:20; intervening
collection cycles after terminal completion re-emit the retained presentation
without another AWS query.
This deliberately differs from YACE's align-then-subtract ordering. With a six-hour period, five-minute delay, and
now=12:02, this collector computes align_down(11:57, 6h) = 06:00; an align-then-subtract implementation computes
align_down(12:02, 6h) - 5m = 11:55. The collector waits for the first shared collection tick at or after 12:05
before the 12:00 boundary is eligible. Increasing lookback searches more older buckets for sparse or late data; it
does not change this end boundary or repair alignment.
One batch can contain queries that finish differently. Outcomes update each stable query independently:
| Outcome | Completion state | Retry state | Retained observation | Current emission |
|---|---|---|---|---|
Complete with a datapoint | Advance to this window | Clear | Keep the newest eligible point | Value |
Complete without a datapoint | Advance to this window | Clear | Keep only while still inside lookback; otherwise clear | Retained value while present; after expiry, synthetic zero for nil_as_zero, otherwise gap |
Forbidden | Advance to this window | Clear | Clear | Gap |
| Transient failure | Do not advance | Exponential backoff | Preserve, even beyond lookback | Retained value or zero when available; otherwise gap |
| Parent cancellation | Do not advance | Do not change | Do not change | Abort the frame |
query_plan.go builds the immutable per-series blueprint.
currentQueryPlan reuses an immutable blueprint until a target resolves or a
discovery/tag snapshot changes. buildQueryPlan emits one plannedQuery per
instance × selected exported series when that blueprint is invalidated.
Identity labels are {account_id, region} plus one label per identifying
instance dimension (a constant dimension is sent in the query but not
labeled). The exported series name is <profile>.<metric_id>_<statistic>.{final instance, exported series} ownership enforce rule precedence:
the first matching rule/target owns each overlapping series. Unknown tag membership
reserves only the scope's selected series, so disjoint lower-rule selections remain
eligible while failures stay fail-closed. This also catches dynamic overlap when
distinct targets resolve to the same account and see the same resource.limits.max_instances counts final instances that emit at least one selected series,
not planned metric queries. Overflow rejects the candidate plan atomically; the
current collection cycle returns an error and does not execute or emit the
previous plan. The previous immutable plan and its state remain intact, and the
dirty flag remains set so a later cycle can rebuild from corrected inputs. There
is no first-N truncation.plannedQuery contains a fully resolved {period, lookback, publication_delay} policy. Resolution is field-by-field, from highest to lowest
precedence: job metric item, job metric group, rule, rule defaults, profile metric,
profile defaults, then the built-in lookback/publication-delay fallbacks. A group
source applies only to explicit include[] expansions; an item source applies
only to that item's expanded series. Profile query.period is required; the
lookback fallback follows the resolved period.
Publication delay is collector scheduling policy, not an AWS SLA. In particular,
the stock S3 storage profile's 1d is a conservative choice; AWS documents that
storage metrics are reported once per day but does not guarantee delivery within
one day.observe.go owns due-window selection, completion, retry, and retained
observation state per stable query.
dueQueries runs only inside the shared
Netdata collection loop. If multiple period boundaries become eligible between
two update_every ticks, the next tick queries only the newest rolling window.update_every; each
later delay doubles and is capped at the effective period. A new eligible window
is immediately due and resets the sequence. Complete and Forbidden clear it;
parent-context cancellation advances neither retry nor completion state.query_batch.go + query_executor.go + query_response.go:
GetMetricData requests.Complete; only unresolved
queries inherit the request failure.PartialData may contribute a candidate but remains transient unless
a later result for that query is Complete.Complete is terminal success. Forbidden is terminal authorization failure.
Client/request failure, InternalError, missing/unknown status, unresolved
PartialData, and pagination overflow are transient for only the affected
queries. Bounded warnings identify those unresolved results; successful siblings
remain complete and are not reissued.observe.go keeps per-query completion and retained raw CloudWatch values;
query_emit.go writes one snapshot frame per cycle:
meter := store.Write().SnapshotMeter(""); each sample becomes
meter.WithLabels(labels...).Gauge(series, metrix.WithFloat(true)).Observe(v).algorithm: absolute (enforced by profile validation);
there is no counter/delta tracking.metrix.WithFloat(true) marks the metric
family float-native; chartengine inherits that onto the chart dimension, so
fractional values render at full precision without a per-dimension
options.float.update_every; metrix writes are free). Netdata expects a value per dimension
per collect cycle, so a stable query that is not due, or whose current attempt
is transient, is re-written from its last cached value—a long-period metric
(e.g. daily S3) renders as a continuous step line instead of gaps, at no extra
AWS cost. Queries sharing one request still retain and emit independently. A
successful rolling-window query keeps the newest eligible
datapoint while it remains inside lookback; once expired, nil_as_zero records
zero and other metrics gap.
Synthetic zero is presentation state, not a timestamped source observation, so
it cannot outrank a real older bucket that appears in a later rolling query.
Without an explicit override, only rate: true metrics at sum or
sample_count default to zero; every other statistic gaps. The cache otherwise
persists until a terminal success expires/replaces it or reconcilePlan drops
the stable key. Transient failures intentionally replay retained values beyond
lookback and follow the per-query retry backoff. Forbidden clears the value and completes
only the attempted window.These details bound cost and memory, but do not change the query state machine:
max_instances cannot allocate an unbounded query plan
before rejection.min(500, 30000 / bucket_count). Queries sharing one structural
CloudWatch metric identity are divided into billing units of at most five statistics;
deterministic packing keeps each unit whole while respecting width. Work preflight
and execution use the same packer. Every request sets exact MaxDatapoints = queries_in_batch × bucket_count; concurrency remains bounded by apiConcurrency,
with one configured timeout shared by the batch's pages.q0, q1, …) exist only inside one batch. Pagination is bounded
to two calls.chart.go, built once by ensurePlan and cached in chartTemplateYAML.
for each selected profile:
group := profile.Template.Clone() # typed deep copy; never mutate the catalog
group.Metrics = sorted(declared series) # includes default and opt-in metrics
assemble charttpl.Spec{Version, ContextNamespace: "cloudwatch", Groups}
return spec.MarshalTemplate() # Validate + yaml.v2 marshal, then cached
charttpl primitives: Group.Clone() (typed deep copy, so
the shared profile catalog is never mutated) and Spec.MarshalTemplate().cwprofiles/)profile.go defines the schema; catalog.go loads and resolves; stock profiles
live under config/go.d/cloudwatch.profiles/default/ (one YAML per service; a
user file with the same basename overrides its stock counterpart). The public
authoring contract lives in profile-format.md; keep it in
lockstep with every profile-schema change.
A Profile declares: namespace (e.g. AWS/EC2), optional
supported_regions (an intrinsic restriction for services such as CloudFront),
required nested query.period plus optional query.lookback and
query.publication_delay, instance
dimensions (the CloudWatch dimension names that identify one instance; each is
either mapped to a Netdata label, or pinned to a constant value — a
match-and-query-only dimension that is matched and queried but not emitted as a
label, for a constant CloudWatch dimension such as CloudFront's Region=Global),
metrics (with statistics, optional metric-level disabled, optional rate,
optional per-metric nested query field overrides, and optional nil_as_zero —
record 0 vs gap on a no-datapoint result; when unset, rate-normalized sum and
sample_count series default to 0 while other statistics default to a gap), and a
charttpl.Group template.
Load and resolution (catalog.go):
rules[].profiles includes every default-enabled profile. defaults: false
with include selects only named profiles; defaults: true with include
adds disabled opt-in profiles; exclude removes names from the union. Explicit
include/exclude overlap is invalid. The compiler intersects each rule's regions
with supported_regions: an incompatible defaults-selected profile is skipped
with a startup diagnostic, while an explicitly included incompatible profile
fails configuration.disabled: true are
the per-profile defaults. A disabled metric remains a fully declared series
with active chart definitions but contributes no selected-series horizon,
query, or billing work until named in that profile's rules[].metrics group.
Namespace discovery remains one metric-name-agnostic dimension-shape scan, so
the declaration adds no ListMetrics call or discovery group. Omitting a metric
group leaves a selected profile on its defaults; an omitted/true group defaults
value adds exact included MetricNames, while defaults: false creates an
exact-only selection for that profile.Profile validation invariants (profile.go) — these are load-bearing:
instances.by_labels must include account_id, region, and
every identifying instance-dimension label (a constant match-and-query-only
dimension has no label and is excluded). Chart-instance identity is built solely
from by_labels; a missing label would silently merge distinct AWS resources
onto one chart instance.algorithm: absolute. CloudWatch aggregates are
absolute per-period values, and the explicit declaration makes that profile
invariant reviewable and enforced independently of runtime metric metadata.template.metrics must be empty — the collector owns the visible-series list
and fills it at build time.rate: true requires a sum or sample_count statistic (the per-second value
divides a per-period total — the summed value or the observation count — by the
period).AWS/PrivateLinkEndpoints demonstrates why one namespace can need multiple
profiles. AWS publishes the same metric names at two exact dimension sets:
privatelink_endpoint identifies the endpoint by endpoint_type,
service_name, vpc_endpoint_id, and vpc_id. It is default-enabled.privatelink_endpoint_subnet adds subnet_id. It is opt-in because one
endpoint can fan out into several chart instances.The profiles share one namespace discovery scan. Both join RGTA
ec2:vpc-endpoint resources on VPC Endpoint Id; subnet instances therefore
inherit their parent endpoint's filter membership and mutable tag labels.
Each instance exports seven series across five CloudWatch MetricNames. Mixed
Average and Sum entries are intentional: Average remains the raw gauge,
while rate: true normalizes only the Sum sibling to a per-second value. Stock
timing is period=5m, lookback=5m, and publication_delay=5m. Exact
metric/statistic items in one profile group can assign one-minute Average and
six-hour Sum policies. Repeating a MetricName is valid only when the expanded
statistic sets are disjoint.
AWS/PrivateLinkServices is the provider-side companion namespace. AWS
publishes traffic metrics at five exact dimension sets, so the stock catalog
uses five profiles rather than merging incompatible identities:
privatelink_service identifies the endpoint service by service_id. It is
default-enabled and is the only grain with EndpointsCount.privatelink_service_az adds availability_zone.privatelink_service_load_balancer adds load_balancer_arn.privatelink_service_az_load_balancer adds both detail dimensions.privatelink_service_vpc_endpoint adds the consumer vpc_endpoint_id.The four detailed profiles are opt-in because one service can fan out across
Availability Zones, load balancers, and consumer endpoints. All five share one
namespace discovery scan per target/region. They also share one RGTA association:
ec2:vpc-endpoint-service joined by Service Id. Detailed instances therefore
inherit the parent service's filter membership and mutable tag labels; endpoint
or load-balancer tags are intentionally not consulted.
Traffic profiles export raw Average gauges and per-second Sum siblings for
bytes, new connections, and sent reset packets. Stock timing is 5m/5m/5m.
EndpointsCount is a five-minute service-only gauge that records zero when AWS
returns no datapoint; absent traffic gauges remain gaps. Exact rules can select
one-minute traffic Average, five-minute endpoint count, and six-hour byte Sum as
independent per-selection query policies in one rule.
internal/awsauth is collector-local. A named credential source is either the
AWS SDK default chain (environment, shared config, EC2 instance profile, EKS
IRSA) or explicit static/session credentials. A target uses one source directly
or layers one AssumeRole provider over it, so static credentials can bootstrap
cross-account roles without environment indirection. The collector builds an
aws.Config per (target, region).
Credential sources are a list of named entries. Each entry uses a required
type selector. type: default has no branch-specific configuration;
type: static requires a type_static object containing access_key_id,
secret_access_key, and an optional session_token. Keeping branch-specific
fields nested makes the runtime model match the conditional DynCfg form. After
validation, the plan compiler builds a private name index for target reference
resolution; list order has no credential precedence semantics.
Each target's AWS account id is resolved through STS GetCallerIdentity
(identity.go, ensureTargets) and stamped on its series. Resolution is
fail-soft and retried: an unresolved target stays pending while healthy targets
continue; only a cycle with no resolved target fails. Named targets are never
deduplicated by account id because their permissions and visible resources may
differ. Dynamic final-instance overlap is deduplicated later by ordered
rule/target precedence. AWS does not require an explicit IAM permission grant for
GetCallerIdentity.
Partition consistency is enforced per target. A target cannot span AWS partitions, and an assumed-role ARN partition must match the selected regions (aws / aws-us-gov / aws-cn / aws-iso / aws-iso-b / aws-iso-e / aws-iso-f / aws-eusc).
apiConcurrency (5) bounds ListMetrics discovery, RGTA fetch groups, and
GetMetricData chunk execution via conc/pool. maxQueriesPerRequest (500) is
the AWS request ceiling; the datapoint budget may reduce actual batch width.
Both are internal constants, not config.clientCache builds at most one CloudWatch client per (target, region), under
a mutex, caching only successes (a transient credential error is retried next
call).collect stages run sequentially, and the per-cycle store
write is single-threaded. Discovery, resource-tag lookup, and query execution
fan out within their respective stages.The two CloudWatch APIs bill differently, and the design leans on that:
ListMetrics (dynamic discovery) is cheap — it falls under CloudWatch's free API
tier (~1M requests/month), then ~$0.01 per 1,000 requests, and returns only
metric metadata (no per-datapoint charge). So discovery.refresh_every
(default 300s) barely moves the bill; the recently-active filter further trims
result pages. All-constant profiles make no ListMetrics call. Raise
discovery.refresh_every only to cut API load on very large dynamic catalogs.GetMetricData (query) is the cost driver — it is always billed (~$0.01
per 1,000 metrics requested) and scales with metric count × query frequency.
Point-aware batching keeps each AWS billing unit of up to five statistics for
one structural metric in a single request, avoiding duplicate billing at a
batch boundary.update_every <= period, successful steady-state frequency follows the period;
when update_every > period, execution is limited by update_every and skipped
intermediate windows are not backfilled. Between queries, values are re-emitted
from cache at zero AWS cost (see Emission And Sample-And-Hold).Narrow the bill with focused rule target/profile/region selections or longer effective periods configured through rule/default query timing; nested profile query defaults are the fallback. Discovery frequency is a minor lever. (Rates are AWS's published model — verify current per-region prices on the CloudWatch pricing page.)
activity.go keeps four bounded activity accumulators. Four chart templates
under the public cloudwatch.collector_* contexts render interval counts as
absolute gauges for the interval since the preceding successfully committed
collector frame:
ListMetrics
and GetMetricData SDK method invocations. It materializes one chart per
(account_id, region, operation) with a fixed invocations dimension. Failed
attempts and every requested continuation page count; SDK-internal retries do not.(account_id, region) with a
fixed calculated_metric_requests dimension.(account_id, region, profile) with a fixed estimated_metric_requests
dimension. This is a non-additive estimate for relative cost ranking: shared
structural metrics can contribute to several profiles, profile values need not
sum to the calculated total, and no _shared profile is emitted.MetricDataQuery items by source
profile. It materializes per (account_id, region, profile) with a fixed
query_items dimension.For one account and region over the same interval, after summing all profile
instances and considering only the GetMetricData operation, the counts satisfy:
query items ≥ summed profile metric-request estimates ≥ calculated metric
requests ≥ GetMetricData SDK invocations. The profile sum can exceed the
calculated total because cross-profile overlap is counted independently.
The calculated total is attributed only to account and region because one query batch can serve multiple profiles. Profile estimates and query items keep the source profile because every planned series has exactly one. Different effective policies produce separate batches and separate estimates. Targets that resolve to the same account aggregate.
buildQueryBatches computes one immutable activity summary per physical request
after packing. Initial and continuation pages replay that same summary, so each
submitted page counts the complete request footprint. Recording scans and hashes no
queries. Once the bounded scope keys exist, steady-state page recording allocates
nothing; first insertion or map growth may allocate while holding the activity mutex.
Per-page work is proportional only to the number of profiles represented in the batch.
netdata.go.plugin.collector.cloudwatch.* names the internal metrix selectors;
it is not a chart-context prefix. The chart spec's context_namespace: cloudwatch
keeps all four activity contexts beside the service-profile contexts in the UI.
Pending activity lives outside the staged metrix frame. At the start of the next
cycle, activity.go checks CollectMeta.LastSuccessSeq and clears the prior interval
only when that sequence proves its frame committed. A failed collection or failed
metric-store commit therefore carries its work into the next successful frame. Once
a series is known, a successful cached interval with no real AWS work publishes zero.
Cleanup, job replacement, and process restart reset the pending activity and known
keys. SDK invocation counts cover CloudWatch ListMetrics and GetMetricData, not
Resource Groups Tagging API, STS, or credential-provider calls. These metrics are
cost inputs, not an AWS invoice: AWS owns the billing rules, may change them, and does
not separately document pagination billing semantics.
end = align_down(now - publication_delay, period) and start = end - lookback; end is exclusive and the newest fully
eligible finite bucket wins. The policy is evaluated on shared collection ticks.nil_as_zero, defaulting on only for rate: true sum/sample_count) or
gaps after a terminal successful window expires the retained datapoint.
Transient client/request/result failures retain and replay state; Forbidden
clears state and completes the attempted window.by_labels.
They use the configured job vnode or the Agent host; no per-target/resource
HostScope is generated.labels.resource_tags can update labels on an
existing chart but cannot change its identity or overwrite an identity label.limits.max_instances, limits.max_discovery_groups, timeout, vnode, and the framework's
common fields remain job-level settings.
Concurrency and per-request/per-group AWS-work bounds are internal constants.
Discovery breadth uses the explicit operator safeguard; compatible rules/profiles
share a group, and larger valid installations can raise it or split across jobs.config/go.d/cloudwatch.profiles/default/ (namespace, instance dimensions,
metrics, chart template). A new service that fits the profile model needs no
Go change. Add a matching metadata.yaml monitored-instance entry and
regenerate the integration docs.discover.go; terminal diagnostics and install/retry/fail application
are in discovery_disposition.go; aggregate admission, stage timeout, and
authorization lanes are in discovery_budget.go.structural_id.go and
query_plan.go.internal/cwquery/config.go.query_policy.go.query_batch.go and
limits.go.query_response.go.query_executor.go.observe.go.query_emit.go.chart.go, plus
the profile template.internal/awsauth (collector-local; extract to a shared package only when a second AWS consumer appears).config.go + config_schema.json +
metadata.yaml + stock cloudwatch.conf + regenerated integrations/ docs
(collector consistency). Prefer internal constants over new options unless the
option names a real operator decision.cd src/go
go test -count=1 ./plugin/go.d/collector/cloudwatch/...
go vet ./plugin/go.d/collector/cloudwatch/...
gofmt -l plugin/go.d/collector/cloudwatch/
If metadata.yaml changed, regenerate and commit the integration docs
(collector consistency; see the integrations-lifecycle skill):
python3 integrations/gen_integrations.py
python3 integrations/gen_docs_integrations.py -c go.d.plugin/cloudwatch