â–ķ🗚 Pipeline Flow
  SD Providers (K8s, Consul, File, DNS, AWS, â€Ķ)
        │
        │ []*targetgroup.Group  (goroutine per provider)
        ▾
  ┌──────────────────────────────────────────┐
  │       discovery.Manager                  │  discovery/manager.go:170
  │  â€Ē syncCh  chan map[string][]*Group       │
  │  â€Ē ticker-based reconciliation           │
  └────────────────────┮─────────────────────┘
                       │ map[jobName][]*targetgroup.Group
                       ▾
  ┌──────────────────────────────────────────┐
  │        scrape.Manager                    │  scrape/manager.go:186
  │  â€Ē one scrapePool per job                │
  │  â€Ē applies target relabeling             │
  │  â€Ē starts/stops scrapeLoop goroutines    │
  └────────────────────┮─────────────────────┘
                       │
              (per target goroutine)
                       │
                       ▾
  ┌──────────────────────────────────────────┐
  │           scrapeLoop                     │  scrape/scrape.go:1167
  │  â€Ē ticker: every scrape_interval         │
  │  â€Ē HTTP GET target.URL                   │
  │  â€Ē parse response (text or proto)        │
  │  â€Ē apply sample/label limits             │
  │  â€Ē call appender.Append() per sample     │
  │  â€Ē appender.Commit()                     │
  └────────────────────┮─────────────────────┘
                       │ samples, exemplars, histograms
                       ▾
              storage.Appender
          (fanoutStorage → TSDB + Remote)
â–ķ🔭 Discovery Manager async

Each service discovery mechanism runs as a pluggable Discoverer. The manager aggregates their output and pushes batched updates downstream over a channel.

discovery/manager.go — Manager struct L170
type Manager struct {
    logger         *slog.Logger
    name           string
    httpClientOpts []config.HTTPClientOption

    // Guards metrics and targetSets.
    mtx sync.RWMutex

    // Sync channel to the consumer (scrape/notify manager).
    syncCh chan map[string][]*targetgroup.Group

    // Providers holds all active SD providers.
    providers []*Provider

    // Target groups keyed by providerRef → jobName.
    targetSets map[string]map[string]*targetgroup.Group
    ...
}

Each SD provider type has its own Discoverer implementation. When target groups change, the provider sends updates into the manager's internal channel, which the manager batches and forwards to syncCh after a configurable updatert delay (default 5 s).

The syncCh is read by the Scrape Manager in a dedicated goroutine. The Notify Manager has its own separate Discovery Manager instance for Alertmanager endpoint discovery.

Supported SD Mechanisms

NamePackageNotes
kubernetesdiscovery/kubernetesPods, services, endpoints, nodes
filediscovery/fileJSON/YAML; inotify watched
ec2discovery/awsAWS EC2 instances via API
consuldiscovery/consulConsul catalog long-poll
dnsdiscovery/dnsSRV/A/AAAA record lookup
httpdiscovery/httpHTTP endpoint returning JSON target groups
â–ķ🏊 Scrape Manager & scrapePool
scrape/manager.go — Manager struct L186
type Manager struct {
    opts         *Options
    logger       *slog.Logger
    append       storage.Appendable
    graceShut    chan struct{}

    offsetSeed    uint64              // used to jitter scrape offsets
    mtxScrape     sync.Mutex         // guards the maps below
    scrapeConfigs map[string]*config.ScrapeConfig
    scrapePools   map[string]*scrapePool  // one pool per job_name
    ...
}

When the Discovery Manager pushes a new set of target groups, Manager.reload() diffs the current and incoming pools:

  1. New jobs → create scrapePool, call pool.Sync(tgroups)
  2. Removed jobs → call pool.stop() to drain loops
  3. Changed targets within a job → pool.Sync() starts/stops individual scrapeLoop goroutines

Target Relabeling

Before a target is scraped, the scrape pool applies relabel.Process() using the job's relabel_configs. Labels prefixed with __ are internal and stripped before storage. Key labels:

LabelSourcePurpose
__address__SDhost:port to scrape
__scheme__confighttp or https
__metrics_path__configdefaults to /metrics
__scrape_interval__configper-target override
instancederivedset to __address__ after relabeling
jobconfigjob_name from scrape config
â–ķ🔁 scrapeLoop — Per-Target Goroutine
scrape/scrape.go — newScrapeLoop constructor L1167
func newScrapeLoop(opts scrapeLoopOptions) *scrapeLoop {
    sl := &scrapeLoop{
        target:             opts.target,
        scraper:            opts.scraper,       // HTTP client
        buffers:            pool.New(1e3, 1e6, 3, func(n int) interface{} {
                                return make([]byte, 0, n)
                            }),
        sampleMutator:      opts.sampleMutator,  // relabeling
        reportSampleMutator: opts.reportSampleMutator,
        appender:           opts.appender,        // storage.Appendable
        ...
    }
    return sl
}
scrape/scrape.go — main run loop L1263
func (sl *scrapeLoop) run(errc chan<- error) {
    // Align scrape to a fixed offset within the interval to spread load.
    select {
    case <-time.After(sl.scrapeOffset()):
    case <-sl.ctx.Done():
        return
    }

    ticker := time.NewTicker(sl.interval)
    defer ticker.Stop()

    for {
        select {
        case <-ticker.C:
            // 1. HTTP GET target URL
            // 2. Parse response into samples
            // 3. Append to storage
            // 4. Report scrape metadata (__scrape_duration_seconds__, etc.)
            sl.scrapeAndReport(...)
        case <-sl.ctx.Done():
            return
        }
    }
}

Parse & Append Sequence

  1. HTTP GET target.URL() with Accept: text/plain; version=0.0.4, application/openmetrics-text
  2. Response body piped through textparse.New() or openmetricsparse.New()
  3. Each metric family iterated; relabeling applied per sample
  4. app.Append(ref, lset, ts, val) called for each float sample
  5. app.AppendHistogram() for native histograms
  6. app.AppendExemplar() for OpenMetrics exemplars
  7. Sample/label limits checked; excess → errSampleLimit
  8. app.Commit() — atomically writes all samples of this scrape
  9. Stale markers written for series that disappeared since last scrape
If Commit() fails the entire scrape is rolled back via app.Rollback() — scrape metrics are stored atomically or not at all.
â–ķ⚙ Scrape Config Parameters
ParameterDefaultEffect
scrape_interval1mHow often to scrape
scrape_timeout10sHTTP request deadline
sample_limit0 (off)Max samples per scrape before error
label_limit0 (off)Max labels per series
target_limit0 (off)Max targets per job
honor_labelsfalsePrefer target labels over server labels on conflict
honor_timestampstrueUse timestamps from exposition if present
metrics_path/metricsEndpoint to scrape
schemehttphttp or https

Config struct: config/config.go — ScrapeConfig

â–ķðŸŠĶ Stale Markers

When a series disappears (target down or metric removed), Prometheus injects a special stale NaN value at the next scrape. This tells the query engine not to extrapolate across the gap.

scrape/scrape.go — stale marker injection scrape.go
// For each series seen in the previous scrape but absent now:
app.Append(ref, lset, scrapeTimestamp, math.Float64frombits(value.StaleNaN))
value.StaleNaN is a special IEEE 754 NaN with the stale marker bit pattern (0x7ff0000000000002). The query engine treats it as end-of-series rather than a data point.