Problem Description
After upgrading the tf-serving Helm chart, Prometheus stopped collecting latency and throughput metrics from TensorFlow Serving pods. The Prometheus server logs contain entries such as:
scrape target creation failed: connection refused (target: http://10.1.2.3:8501/metrics)
no endpoints found for service "tf-serving" in namespace "ml" – check ServiceMonitor selector
error reading body: EOF while scraping TensorFlow Serving metrics
Consequences observed in the monitoring dashboards:
- Missing
tf_serving_request_latency_secondsandtf_serving_inference_requests_totalseries. - Alert rules that depend on these metrics fire continuously.
- Prometheus UI shows “0 active targets” for the
tf-servingjob.
Root Cause Analysis
The failure originates from the service discovery layer of the Prometheus Operator. Two intertwined mis‑configurations are typical after a Helm upgrade:
- ServiceMonitor selector mismatch – The
ServiceMonitorCRD created by the chart uses amatchLabelsselector (e.g.,app: tf-serving). During the upgrade the chart changed the label key toapp.kubernetes.io/name: tf-serving(see GitHub issue #2314). Prometheus therefore cannot associate any pods with the existing ServiceMonitor, resulting in “no active targets”. - Metrics port name change – The chart version bump renamed the container port from
metricstohttp-metrics. The default relabel rule in the Operator’sscrape_configfilters endpoints by__meta_kubernetes_pod_container_port_name=metrics(Prometheus docs – Kubernetes Service Discovery). Because the new name does not match, the endpoint is dropped, leading to “connection refused” or “EOF” errors when Prometheus attempts to scrape the old address.
Both issues are documented in the official Prometheus Operator CRD reference (ServiceMonitor CRD) and the TensorFlow Serving metrics guide (TensorFlow Serving Monitoring).
Investigation and Debugging
Step‑by‑step diagnostics that reproduced the failure:
- Inspect Prometheus logs for target errors:
kubectl -n monitoring logs prometheus-0 | grep "scrape target creation failed" - Verify ServiceMonitor selector against pod labels:
kubectl -n ml get servicemonitor tf-serving -o yaml kubectl -n ml get pods -l app=tf-serving -o jsonpath="{.items[*].metadata.labels}"Expected output shows a mismatch:
serviceMonitor.spec.selector.matchLabels: app: tf-serving pod.labels: app.kubernetes.io/name: tf-serving - Check the metrics port name on the pods:
kubectl -n ml get pod tf-serving-abc123 -o jsonpath="{.spec.containers[0].ports}"Output after upgrade:
[ { "containerPort": 8501, "name": "http-metrics" } ] - Confirm ServiceMonitor endpoint definition:
kubectl -n ml get servicemonitor tf-serving -o yaml | grep portShows
port: metrics, which no longer exists. - Run a manual curl against a pod IP to see if the endpoint is reachable:
POD_IP=$(kubectl -n ml get pod tf-serving-abc123 -o jsonpath="{.status.podIP}") curl -s http://$POD_IP:8501/metrics | head -n 5Result:
curl: (7) Failed to connect– confirming the port name mismatch.
Resolution
Two corrective actions are required: align ServiceMonitor selectors with pod labels and ensure the metrics port name matches the relabel rule.
1. Update ServiceMonitor selector
Before:
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: tf-serving
spec:
selector:
matchLabels:
app: tf-serving
namespaceSelector:
matchNames:
- ml
endpoints:
- port: metrics
path: /metrics
After (matching the chart’s label schema):
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: tf-serving
spec:
selector:
matchLabels:
app.kubernetes.io/name: tf-serving
namespaceSelector:
matchNames:
- ml
endpoints:
- port: http-metrics
path: /metrics
2. Adjust the port name in the Helm values (or chart)
If you prefer to keep the original ServiceMonitor, rename the container port back to metrics via Helm values:
# values.yaml
serving:
containerPort: 8501
metricsPortName: metrics # ← add or override
Or patch the existing deployment:
kubectl -n ml patch deployment tf-serving \
-p '{"spec":{"template":{"spec":{"containers":[{"name":"tf-serving","ports":[{"containerPort":8501,"name":"metrics"}]}]}}}}'
3. Re‑apply the ServiceMonitor
Because Helm upgrade hooks may delete and recreate CRDs, ensure the ServiceMonitor is recreated after the deployment:
helm upgrade tf-serving ./tf-serving-chart \
--namespace ml \
--set prometheusOperator.enabled=true \
--set prometheusOperator.serviceMonitor.enabled=true \
--reuse-values
Adding the annotation helm.sh/hook: post-install,post-upgrade to the ServiceMonitor manifest (see Helm best practices) guarantees it persists across upgrades.
Verification
After applying the fixes, perform the following checks:
- Prometheus target status:
kubectl -n monitoring port-forward svc/prometheus-operated 9090:9090 & curl -s http://localhost:9090/api/v1/targets | jq '.data.activeTargets[] | select(.labels.job=="tf-serving")'Output should show
health":"up"and a validscrapeUrl. - Metric presence:
curl -s http://localhost:9090/api/v1/query?query=tf_serving_inference_requests_total | jq '.data.result | length'Non‑zero result indicates successful collection.
- Manual scrape test:
curl -s http://$(kubectl -n ml get svc tf-serving -o jsonpath="{.spec.clusterIP}"):8501/metrics | grep tf_servingShould return metric lines without errors.
Prevention and Best Practices
| Area | Recommendation |
|---|---|
| Helm chart versioning | Pin chart versions and review values.yaml diffs for label or port name changes before upgrade. |
| ServiceMonitor stability | Declare ServiceMonitor as a helm.sh/hook with post-upgrade to ensure recreation after CRD upgrades. |
| Label consistency | Adopt a single label convention (e.g., app.kubernetes.io/name) across all workloads and ServiceMonitors. |
| Port naming | Keep the metrics port name constant (default metrics) or update relabel rules in scrape_config accordingly. |
| Observability testing | Include a CI step that runs promtool test rules and a smoke‑test curl against /metrics after each chart release. |
Related Topic Hub: Observability Troubleshooting Hub
FAQ
- Why does Prometheus show “no active targets” after a Helm upgrade?
Because the ServiceMonitor selector no longer matches any pod labels or the endpoint port name changed, causing the discovery process to drop all endpoints. - Can I rely solely on pod annotations (
prometheus.io/scrape) instead of ServiceMonitors?
Annotations work with the default Prometheus configuration, but the Prometheus Operator disables them by default in favor of ServiceMonitors. Mixing both can lead to duplicate or missing targets after upgrades. - How do I debug a “connection refused” error when scraping TensorFlow Serving?
Verify the pod’s IP and port withkubectl execorcurl, ensure the container exposes the correct port name, and confirm the ServiceMonitor endpoint references that name. - What if the chart upgrades the namespace selector?
Update the ServiceMonitor’snamespaceSelectorto include the new namespace or useany: trueif you want to monitor across all namespaces. - Is there a way to automatically detect mismatched ServiceMonitor selectors?
Enable the Prometheus Operator’s--log-level=debugflag; it emits warnings when a ServiceMonitor has no matching endpoints, which can be routed to an alerting rule.