Skip to content
14 changes: 14 additions & 0 deletions charts/amazon-cloudwatch-observability/templates/_helpers.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,20 @@ Logic:
{{- end -}}
{{- end -}}

{{/*
Returns "true" when otelContainerInsights-driven ServiceMonitor/PodMonitor scraping
applies to the given agent. True when otelContainerInsights is enabled, the agent is
the configured targetAgent, and at least one of serviceMonitor/podMonitor is enabled.
Accepts a dict with "agentName" (string) and "context" (root context $).
*/}}
{{- define "cloudwatch-agent.otelCIScrapeEnabled" -}}
{{- $ctx := .context -}}
{{- $agentName := .agentName -}}
{{- if and $ctx.Values.otelContainerInsights.enabled (eq $agentName $ctx.Values.otelContainerInsights.targetAgent) (or $ctx.Values.otelContainerInsights.serviceMonitor.enabled $ctx.Values.otelContainerInsights.podMonitor.enabled) -}}

@musa-asad musa-asad Jul 21, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This helper reads otelContainerInsights.serviceMonitor.enabled directly. Fine today since the PR ships those keys as defaults, but it goes nil if a user replaces the whole map, so a dig with a default fallback might be safer.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the subkeys ship as chart defaults and Helm deep-merges them, they're present under any partial override, so this is safe as-is I'll leave the direct access

true
{{- end -}}
{{- end -}}

{{/*
Helper function to modify cloudwatch-agent config
*/}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,8 @@ processors:
- k8s.job.name
- k8s.cronjob.name
labels:
# $$$1 is Helm escaping: $$$ → $$ (Helm) → $ (OTel env resolver) → literal $1 backreference
# $$$1 -> literal $1 backreference (group 1 = label key). The agent's OTel confmap
# resolves it twice (expandconverter + resolver), each collapsing $$->$; Helm leaves it as-is.
- tag_name: "k8s.pod.label.$$$1"
key_regex: "(.*)"
from: pod
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,23 @@ receivers:
- targets:
- ${env:HOST_IP}:10250

{{- if or .Values.otelContainerInsights.serviceMonitor.enabled .Values.otelContainerInsights.podMonitor.enabled }}
# ServiceMonitor/PodMonitor scraping via the Target Allocator (prometheusCR discovery).
# The Target Allocator deployed for the targetAgent serves the scrape jobs derived from
# ServiceMonitor/PodMonitor CRs; this receiver pulls this collector's assigned shard and
# routes the series into the v2 OTLP pipeline (-> CloudWatch/Zeus). Requires the Target
# Allocator + prometheusCR to be enabled for the targetAgent and the POD_NAME env (set below).
prometheus/cw_k8s_ci_v0_prometheuscr:
target_allocator:
endpoint: https://{{ .Values.otelContainerInsights.targetAgent }}-target-allocator-service:80
interval: {{ .Values.otelContainerInsights.metricResolution }}

@musa-asad musa-asad Jul 21, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

interval here is the collector to Target Allocator refresh cadence, but it reuses otelContainerInsights.metricResolution, which is really the scrape cadence (prometheusCR.scrapeInterval). Worth a comment or a separate field so the two can diverge later?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed they're distinct knobs. I'd rather not add a second field preemptively, but the shared value being non-obvious is fair

collector_id: ${env:POD_NAME}
tls:
ca_file: /etc/amazon-cloudwatch-observability-agent-cert/tls-ca.crt
cert_file: /etc/amazon-cloudwatch-observability-agent-ta-client-cert/client.crt
key_file: /etc/amazon-cloudwatch-observability-agent-ta-client-cert/client.key
{{- end }}

{{- if .Values.dcgmExporter.enabled }}
prometheus/cw_k8s_ci_v0_dcgm:
config:
Expand Down Expand Up @@ -329,6 +346,18 @@ processors:
- set(attributes["cloudwatch.solution"], "k8s-otel-container-insights")
- set(attributes["cloudwatch.pipeline"], "efa")

{{- if or .Values.otelContainerInsights.serviceMonitor.enabled .Values.otelContainerInsights.podMonitor.enabled }}
transform/cw_k8s_ci_v0_set_scope_prometheuscr:

@musa-asad musa-asad Jul 21, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The scraping receiver and pipeline are guarded on serviceMonitor.enabled/podMonitor.enabled, but the transform/cw_k8s_ci_v0_set_scope_prometheuscr processor isn't, so turning both monitors off leaves it orphaned and the collector warns. Can we guard it the same way via otelCIScrapeEnabled?

error_mode: ignore
metric_statements:
- context: scope
statements:
- set(scope.schema_url, "")
- set(attributes["cloudwatch.source"], "cloudwatch-agent")
- set(attributes["cloudwatch.solution"], "k8s-otel-container-insights")
- set(attributes["cloudwatch.pipeline"], "prometheus-cr")
{{- end }}

transform/cw_k8s_ci_v0_set_scope_ebs_csi:
error_mode: ignore
metric_statements:
Expand Down Expand Up @@ -427,6 +456,8 @@ processors:
- k8s.job.name
- k8s.cronjob.name
labels:
# $$$1 -> literal $1 backreference (group 1 = label key). The agent's OTel confmap
# resolves it twice (expandconverter + resolver), each collapsing $$->$; Helm leaves it as-is.
- tag_name: "k8s.pod.label.$$$1"
key_regex: "(.*)"
from: pod
Expand Down Expand Up @@ -829,6 +860,23 @@ service:
exporters:
- otlphttp/cw_k8s_ci_v0_metrics_dest

{{- if or .Values.otelContainerInsights.serviceMonitor.enabled .Values.otelContainerInsights.podMonitor.enabled }}
metrics/cw_k8s_ci_v0_prometheuscr:
receivers: [prometheus/cw_k8s_ci_v0_prometheuscr]
# Phase 1: minimal chain to get ServiceMonitor/PodMonitor series flowing to v2.
# Richer OTel label/metadata enrichment (k8sattributes pod/node, workload, etc.)
# is intentionally deferred to the Phase 3 enrichment work.
processors:
- filter/cw_k8s_ci_v0_scrape_metadata
- metricstarttime/cw_k8s_ci_v0
- transform/cw_k8s_ci_v0_set_cluster_name
- transform/cw_k8s_ci_v0_set_scope_prometheuscr
- resourcedetection/cw_k8s_ci_v0
- batch/cw_k8s_ci_v0_metrics_dest
exporters:
- otlphttp/cw_k8s_ci_v0_metrics_dest
{{- end }}

{{- if .Values.dcgmExporter.enabled }}
metrics/cw_k8s_ci_v0_dcgm:
receivers: [prometheus/cw_k8s_ci_v0_dcgm]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,21 +110,46 @@ spec:
{{- else if ne (trimAll " \n\t" $generatedOtelConfig) "{}" }}
otelConfig: {{ include "cloudwatch-agent.modify-otel-config" (merge (dict "OtelConfig" $generatedOtelConfig) $) }}
{{- end }}
{{- $otelCIScrape := eq (include "cloudwatch-agent.otelCIScrapeEnabled" (dict "agentName" $agent.name "context" $)) "true" }}
{{- $taEnabled := or $agent.prometheus.targetAllocator.enabled $otelCIScrape }}
{{- if $agent.prometheus.config }}
prometheus:
{{- with $agent.prometheus.config }}
config:
{{- toYaml . | nindent 6 }}
{{- end }}
{{- else if $taEnabled }}
# The operator builds the Target Allocator config from spec.prometheus and requires
# a prometheus.config containing a scrape_configs key (see operator
# targetallocator/adapters GetPromConfig). With prometheusCR (ServiceMonitor/
# PodMonitor) discovery the static scrape list is empty -- the Target Allocator
# discovers targets from ServiceMonitor/PodMonitor CRs.
prometheus:
config:
scrape_configs: []
{{- end }}
{{- if $agent.prometheus.targetAllocator.enabled }}
{{- if $taEnabled }}
targetAllocator:
enabled: {{ $agent.prometheus.targetAllocator.enabled | default false }}
enabled: true
image: {{ template "target-allocator.image" (merge $agent.prometheus.targetAllocator.image (dict "region" $.Values.region)) }}
allocationStrategy: "consistent-hashing"
{{- if $agent.prometheus.targetAllocator.prometheusCR.enabled }}
{{- if or $agent.prometheus.targetAllocator.prometheusCR.enabled $otelCIScrape }}
prometheusCR:
enabled: {{ $agent.prometheus.targetAllocator.prometheusCR.enabled | default false }}
enabled: true
{{- if $otelCIScrape }}
scrapeInterval: {{ $.Values.otelContainerInsights.metricResolution | quote }}
{{- if not $.Values.otelContainerInsights.serviceMonitor.enabled }}
# serviceMonitor.enabled=false: select a label no real ServiceMonitor carries,
# so the Target Allocator discovers no ServiceMonitors.
serviceMonitorSelector:
amazon-cloudwatch-observability.aws/otel-ci-scrape: disabled
{{- end }}
{{- if not $.Values.otelContainerInsights.podMonitor.enabled }}
# podMonitor.enabled=false: select a label no real PodMonitor carries.
podMonitorSelector:
amazon-cloudwatch-observability.aws/otel-ci-scrape: disabled
{{- end }}
{{- end }}
{{- end }}
{{- end }}
{{- with $agent.resources }}
Expand Down Expand Up @@ -281,6 +306,12 @@ spec:
{{- if $.Values.otelContainerInsights.enabled }}
- name: OTEL_CI_VERSION
value: "1.0.0"
# POD_NAME is used as the Target Allocator collector_id by the prometheusCR
# receiver (otel-container-insights.config) so each agent pulls its own shard.
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
{{- end }}
{{- with $agent.env }}
{{- . | toYaml | nindent 2 }}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,22 @@
{{- if .Values.agent.enabled }}
{{- /*
All Target Allocators (targetAgent and clusterScraperAgent) default to the same
ServiceAccount (target-allocator-service-acct; see operator serviceaccount.go), so a single
ClusterRole covers every TA. Pre-scan the agents to decide whether any TA (and any
prometheusCR discovery) is enabled, then render exactly once to avoid duplicate-name objects.
*/}}
{{- $needsTA := false }}
{{- $needsCR := false }}
{{- range $i, $customAgent := .Values.agents }}
{{- if and (and (hasKey ($customAgent.prometheus) "targetAllocator") (hasKey ($customAgent.prometheus.targetAllocator) "enabled")) $customAgent.prometheus.targetAllocator.enabled }}
{{- $otelCIScrape := eq (include "cloudwatch-agent.otelCIScrapeEnabled" (dict "agentName" $customAgent.name "context" $)) "true" }}

@musa-asad musa-asad Jul 21, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This renders the ClusterRole inside range .Values.agents with a static name, so two matching agents produce duplicate ClusterRoles and Kubernetes just keeps the last write. Could we namespace the name per agent (same for the ClusterRoleBinding), or document that only one agent is supported?

{{- if or (and (hasKey ($customAgent.prometheus) "targetAllocator") (hasKey ($customAgent.prometheus.targetAllocator) "enabled") $customAgent.prometheus.targetAllocator.enabled) $otelCIScrape }}
{{- $needsTA = true }}
{{- end }}
{{- if or (and (hasKey ($customAgent.prometheus) "targetAllocator") (hasKey ($customAgent.prometheus.targetAllocator) "prometheusCR") $customAgent.prometheus.targetAllocator.prometheusCR.enabled) $otelCIScrape }}
{{- $needsCR = true }}
{{- end }}
{{- end }}
{{- if $needsTA }}
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
Expand All @@ -22,12 +38,14 @@ rules:
verbs: ["get", "list", "watch"]
- nonResourceURLs: ["/metrics"]
verbs: ["get"]
{{- if and (hasKey ($customAgent.prometheus.targetAllocator) "prometheusCR") $customAgent.prometheus.targetAllocator.prometheusCR.enabled }}
{{- if $needsCR }}
- apiGroups: [ "monitoring.coreos.com"]
resources: ["podmonitors", "servicemonitors"]
verbs: ["get", "list", "watch"]
# TA watches the SM/PM CRDs to start/stop informers as they appear or disappear (read-only).
- apiGroups: [ "apiextensions.k8s.io" ]
resources: ["customresourcedefinitions"]
verbs: ["get", "list", "watch"]
{{- end }}
{{- end }}
---
{{- end }}
{{- end }}
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
{{- if .Values.agent.enabled }}
{{- /*
Render a single ClusterRoleBinding for the shared Target Allocator ServiceAccount
(target-allocator-service-acct), used by every TA (targetAgent and clusterScraperAgent).
Pre-scan the agents to decide whether any TA is enabled, then render exactly once.
*/}}
{{- $needsTA := false }}
{{- range $i, $customAgent := .Values.agents }}
{{- if and (and (hasKey ($customAgent.prometheus) "targetAllocator") (hasKey ($customAgent.prometheus.targetAllocator) "enabled")) $customAgent.prometheus.targetAllocator.enabled }}
{{- $otelCIScrape := eq (include "cloudwatch-agent.otelCIScrapeEnabled" (dict "agentName" $customAgent.name "context" $)) "true" }}
{{- if or (and (hasKey ($customAgent.prometheus) "targetAllocator") (hasKey ($customAgent.prometheus.targetAllocator) "enabled") $customAgent.prometheus.targetAllocator.enabled) $otelCIScrape }}
{{- $needsTA = true }}
{{- end }}
{{- end }}
{{- if $needsTA }}
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
Expand All @@ -16,6 +27,4 @@ subjects:
name: "target-allocator-service-acct"
namespace: {{ $.Release.Namespace }}
{{- end }}
---
{{- end }}
{{- end }}
11 changes: 11 additions & 0 deletions charts/amazon-cloudwatch-observability/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1161,6 +1161,17 @@ otelContainerInsights:
## Only takes effect when otelContainerInsights.enabled is true.
logs:
enabled: true
## Prometheus ServiceMonitor scraping (monitoring.coreos.com/v1) via the Target
## Allocator's prometheusCR discovery. Defaults to true: when
## otelContainerInsights is enabled, ServiceMonitor scraping is on unless
## explicitly disabled here. Only takes effect when otelContainerInsights.enabled is true.
serviceMonitor:

@musa-asad musa-asad Jul 21, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

serviceMonitor.enabled and podMonitor.enabled default to true, so the moment someone turns on otelContainerInsights they silently get prometheusCR discovery, whose CRD and RBAC this PR doesn't set up yet. Could we default them to false, or gate the path on the CRDs being present?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The default-on behavior is intentional, the product goal is that enabling otelContainerInsights gives you working SM/PM scraping out of the box, and the surrounding work is built to make that safe: the operator's CRD-resilience change(#394) makes missing CRDs non-fatal (no crash/loop), and CRD bundling (#331) puts the CRDs in-cluster. So I'd like to keep SM/PM default-on rather than invert to opt-in.

enabled: true
## Prometheus PodMonitor scraping (monitoring.coreos.com/v1) via the Target
## Allocator's prometheusCR discovery. Defaults to true (see serviceMonitor note).
## Only takes effect when otelContainerInsights.enabled is true.
podMonitor:
enabled: true
## The agent in the agents array that receives node-level OTEL Container Insights config.
targetAgent: "cloudwatch-agent"
## The agent in the agents array that receives cluster-level OTEL Container Insights config (apiserver, kube-state-metrics scraping).
Expand Down
Loading