ObservabilityVIII · Service DiscoveryServiceDiscovery
Static Discovery
What you'll learn
- Write a static_configs block that groups targets by exporter role with per-group labels
- Apply a prometheus.yml change with a reload and prove the new targets are being scraped
- Diagnose the classic static_configs failures: typos, stale entries, duplicates, label mistakes, missed reloads
- Decide when static discovery is the right tool and when a fleet has outgrown it
Prerequisites
Verified against Prometheus 2.55.x · Alertmanager 0.28.x · node_exporter 1.8.x · blackbox_exporter 0.26.x · Grafana 11.x · Loki 3.x · Tempo current · OpenTelemetry Collector 0.110.x · Grafana Alloy current · Docker Engine 28.x · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · RHEL / Rocky / AlmaLinux 9.x · 2026-08-13
Two new authoritative DNS servers go into the rack on Tuesday. On
Wednesday their metrics are already in Grafana, because a sysadmin added
four lines to prometheus.yml, opened a merge request, and reloaded
Prometheus when it merged. No discovery service, no API integration, no
agent: the config file said what to scrape, and Prometheus scraped it.
That mechanism is static_configs, and it is the right tool far more
often than its reputation suggests.
What static_configs is
static_configs is a list of target groups written directly into a
scrape job. Each target group has two keys:
targets— a list ofhost:portaddresses to scrape.labels— a label set attached to every target in the group.
Prometheus performs no lookup and talks to no external system. The
config file is the inventory. Every other discovery mechanism in this
part of the course — file_sd_configs, dns_sd_configs,
docker_sd_configs — exists to answer one question: “what should the
target list be right now?” Static discovery answers it with “exactly
what the file says,” which is either a strength or a liability depending
on how often the truthful answer changes.
Why a sysadmin cares
Three properties make static discovery operationally attractive for small, stable fleets:
- Zero external dependencies. Discovery cannot break because DNS is down, the Docker socket hung, or an API token expired. The monitoring stack itself, the DNS resolvers, the core switches polled through the SNMP exporter, the blackbox probe endpoints — these are precisely the things you want monitored while everything else is on fire, and static discovery has no moving parts that can fail with them.
- Reviewable inventory. A
prometheus.ymlin git gives you diff, blame, and merge request review on “what are we monitoring.” That audit trail matters during incident review. - Nothing to learn beyond YAML. The on-call engineer who has never touched Prometheus can still add a host correctly.
The trade-off arrives with churn. When hosts are provisioned weekly and the file is edited by hand, the file starts to lie: decommissioned hosts stay listed, new hosts get forgotten, and the monitoring system quietly diverges from reality. That is the signal to move to file-based discovery (lesson 02), not a reason to avoid static configs today.
How it works
A scrape job produces one target group per static_configs entry.
Every address in targets becomes one target carrying the group labels.
The target then flows through relabeling (lesson 05) and into its own
scrape loop:
prometheus.yml
|
v
- job_name: node one job per exporter role
static_configs:
- targets: [a:9100, b:9100] target group 1
labels: {env: production}
- targets: [c:9100] target group 2
labels: {env: staging}
|
v
one target per address
__address__ = host:port
labels = group labels
|
v
relabel_configs (lesson 05)
|
v
final labels: job, instance, env, ...
(instance defaults to __address__)
|
v
one scrape loop per target
Settings that must be identical for a set of targets —
scrape_interval, scrape_timeout, scheme, metrics_path,
authentication — live at the job level. That is why jobs are
grouped by exporter role (node, blackbox-http, snmp-core) and
never by host.
Configuring it
A realistic job for a small estate: node_exporter on every host, split into two environments with per-group labels:
scrape_configs:
- job_name: node
# Per-job settings apply to every target in every group below.
scrape_interval: 30s
scrape_timeout: 10s
static_configs:
- targets:
- 10.20.0.11:9100
- 10.20.0.12:9100
labels:
# Attached to every series from every target in THIS group.
env: production
site: lon1
- targets:
- 10.30.0.11:9100
labels:
env: staging
site: lon2
The second canonical use: blackbox probing of fixed URLs, where the “targets” are the URLs to probe and relabeling redirects the scrape to the blackbox exporter (the relabeling mechanics are lesson 05):
- job_name: blackbox-http
metrics_path: /probe
params:
module: [http_2xx]
static_configs:
- targets:
- https://status.example.com
- https://intranet.example.com
relabel_configs:
- source_labels: [__address__]
target_label: __param_target
- source_labels: [__param_target]
target_label: instance
- target_label: __address__
replacement: blackbox.example.com:9115
Validating it
# READ-ONLY: syntax and schema check before anything goes live
promtool check config /etc/prometheus/prometheus.yml
Checking /etc/prometheus/prometheus.yml
SUCCESS: /etc/prometheus/prometheus.yml is valid prometheus config file syntax
# CONFIGURATION: apply the change without a restart
sudo systemctl reload prometheus
# or, when --web.enable-lifecycle is set:
curl -X POST http://localhost:9090/-/reload
Then prove the reload landed and the targets exist:
# READ-ONLY: reload succeeded, and how long ago?
curl -s 'http://localhost:9090/api/v1/query' \
--data-urlencode 'query=prometheus_config_last_reload_successful' \
| jq '.data.result[].value'
# READ-ONLY: health per target in the node job
curl -s 'http://localhost:9090/api/v1/targets?state=active' \
| jq -r '.data.activeTargets[]
| select(.scrapePool=="node")
| [.labels.instance, .health, .lastError] | @tsv'
["1.7561507e+09", "1"]
10.20.0.11:9100 up
10.20.0.12:9100 down dial tcp 10.20.0.12:9100: connect: connection refused
10.30.0.11:9100 up
Two UI pages complete the picture: /targets shows health and the last
scrape error per target; /service-discovery shows, per target, the
discovered labels next to the final labels after relabeling — the page
you will live on from lesson 05 onwards.
How it fails
- The typo that scrapes forever.
10.20.0.12:9100typed as10.20.0.21:9100is valid YAML and a valid address. Prometheus creates the target and the scrape fails with connection refused or timeout. Symptom:upis 0,/targetsshows the dial error. Prometheus cannot catch this at config time — reachability is not part of validation. - The decommissioned host that was never removed. The target
stays down permanently and pages whoever owns the down alert — or
worse, the address gets reused by a different machine and Prometheus
happily scrapes that. Symptom of the second shape:
upis 1, and the metrics describe a host you have never heard of. - The duplicate entry. The same address appears in two target groups with different labels. Prometheus deduplicates only when the final label set and scrape URL are identical; different group labels mean two scrape loops and two copies of every series. Symptom: doubled series counts, alerts that fire twice with different label values.
- The label typo.
env: prdoctionpasses every check and silently excludes the series from every dashboard variable and alert route that filters onenv="production". Symptom: the host is “missing” from views, butupsays it is scraped. - The edit without the reload. The file on disk is correct and
the running config is stale. Symptom:
prometheus_config_last_reload_success_timestamp_secondsis older than the file’s mtime. This is the first thing to check when “my change did nothing.” - Inventory rot. The meta-failure: over a year, the file drifts from reality one small edit at a time until nobody trusts it. No metric fires; trust just evaporates. The fix is process (git review) or a different discovery mechanism.
Troubleshooting it
Work from the Prometheus process outward to the target:
- Is the intended config actually loaded? Check the reload
metrics above, then diff
/api/v1/status/configagainst the file on disk. - Does discovery produce the target?
/service-discoverylists every active target with its labels. If the target is absent here, the problem is the config, not the network. - What does the scrape say?
/targets(or the API above) showshealthandlastError.connection refused,timeout, andserver returned HTTP status 404point at three different layers. - Can the Prometheus host reach the endpoint at all?
curl -sv http://10.20.0.12:9100/metricsfrom the Prometheus host tests DNS, routing, firewall, and exporter in one step. - If curl works but the scrape fails, the delta is in job
settings: scheme (http vs https),
metrics_path, TLS verification, credentials, or ascrape_timeoutshorter than the exporter’s response time.
Security implications
prometheus.yml is a labelled map of the estate: hostnames, roles,
environments, and which ports answer HTTP. It is served back by the UI
and /api/v1/status/config, so both need access control — an attacker
with read access to the status endpoint has a curated inventory.
Scrape credentials (basic_auth, bearer_token_file,
tls_config key files) are referenced from the same file. Keep secrets
in referenced files with tight permissions rather than inline, so the
config can live in git without the credentials joining it. Where scrape
traffic crosses a segment you do not fully trust, use scheme: https
with a proper tls_config; the platform security part of the course
covers the full pattern.
Performance implications
Static discovery itself costs nothing: no polling loop, no API client, no refresh metric. The cost is the scrape fleet it defines — targets multiplied by series per target, divided by interval. A few hundred node_exporter targets at 30s is comfortably within a single Prometheus on modest hardware; the toil of keeping the file honest becomes the binding constraint long before CPU does.
One subtle cost: a target’s identity is its final label set. Editing a group label does not update the target — it creates a new one and retires the old, writing staleness markers and starting fresh series. Batch label changes deliberately rather than dribbling them in.
Production guidance
- Keep
prometheus.ymlin git. Every change via merge request, every merge request runspromtool check configin CI. - One job per exporter role. Group labels minimal and conventional:
env,site, maybeteam. Resist encoding the entire CMDB into labels. - After every change: reload, check the reload metric, check
/targets. Three steps, thirty seconds. - Prune ruthlessly. A down target for a host that no longer exists is alert noise today and a wrong-machine mystery after the next IP reuse.
- When the fleet changes faster than the merge requests can keep up —
roughly, when hosts appear or disappear weekly — graduate to
file_sd_configs(lesson 02). The static groups become a generated file; the job settings stay put.
Rollback. A static-config change is rolled back with
git revert plus a reload. Verify with the reload metric and
/api/v1/targets. No data is lost by removing a target: old samples
stay in the TSDB until retention, so a mistaken removal fixed within
the hour leaves only a small gap.
Verification
You should now be able to answer:
- What does a
static_configstarget group contain, and which labels end up on every series it produces? - How do you apply a
prometheus.ymlchange without a restart, and which two metrics prove the reload worked? - What happens to a target, its
upseries, and its alerts when you delete it from the config? - Why does the same address in two target groups with different labels double your series, and when would Prometheus deduplicate it?
- At what point should a fleet move from
static_configstofile_sd_configs?
Quiz
Knowledge check · 8 questions
Q1. What is a static_configs entry, precisely?
Q2. prometheus.yml has been edited. How does the change take effect without dropping in-flight scrapes?
Q3. A target removed from static_configs stops being scraped, so its up series goes stale within a couple of intervals and the alert resolves.
Q4. A static target has no instance label set by any rule. What does instance become?
Q5. Which are sound reasons to keep a fleet on static_configs?
Q6. The same address appears in two target groups of one job, with different group labels. What happens?
Q7. Name the command (binary and subcommand) that validates prometheus.yml before you reload.
Q8. A static target still points at an address that was reassigned to a different machine after decommissioning. What does Prometheus do?
Passing score: 75%. Answers are checked in this browser.