github.com/grafana/synthetic-monitoring-agent@v0.64.2/docs/architecture/scraper.md (about)

     1  ---
     2  component: Scraper
     3  source_paths:
     4    - internal/scraper/
     5    - pkg/collector/
     6  last_reviewed_commit: f77bec5e
     7  generated: 2026-05-23
     8  ---
     9  
    10  # Scraper — `internal/scraper`
    11  
    12  ## Purpose
    13  
    14  One Scraper exists per active check. It runs the prober on the check's
    15  schedule, decorates the result with check, probe, and user labels, then
    16  hands the payload to the publisher. It also manages the Prometheus
    17  metric lifecycle for the check: emitting **stale markers** on shutdown
    18  and **republishing** metrics for low-frequency checks so the series
    19  doesn't go stale between probes.
    20  
    21  ## Where it lives
    22  
    23  `internal/scraper/`
    24  
    25  | File              | Responsibility                                          |
    26  | ----------------- | ------------------------------------------------------- |
    27  | `scraper.go`      | The `Scraper` struct, factory, scheduling loop (`tickWithOffset`), payload assembly, label handling, state machine. |
    28  | `metrics.go`      | The `Metrics` / `Incrementer` / `IncrementerVec` interfaces and the per-scraper metric wrapper. |
    29  | `scraper_test.go` | Golden-file tests against local servers (see below).    |
    30  | `testdata/*.txt`  | Golden Prometheus output for every check type.          |
    31  
    32  `pkg/collector/` is the public, single-execution adapter. It accepts an
    33  API-level check, probe identity, and caller-supplied prober, then returns the
    34  same transformed metrics and logs without scheduling or publishing them.
    35  
    36  ## How it fits in
    37  
    38  ```mermaid
    39  flowchart LR
    40      Updater -->|Factory call| Scraper
    41      Collector[pkg/collector] -->|one execution| Scraper
    42      Caller -->|supplied Probe| Collector
    43      Scraper -->|Probe<br/>ctx, target, registry, logger| Prober
    44      Prober -.->|scripted / browser / multihttp| K6Runner
    45      Scraper -->|payload<br/>(metrics + logs)| Publisher
    46      Scraper -->|AddExecution| Telemeter[internal/telemetry]
    47      Scraper -->|MetricLabels / LogLabels| Limits[internal/limits]
    48      Scraper -->|CostAttributionLabels| Cals[internal/cals]
    49  ```
    50  
    51  ## Lifecycle
    52  
    53  ### Construction
    54  
    55  The package-level factory `scraper.New(...)` is the one wired into the
    56  Updater via `scraper.Factory`. It builds the `prober.ProberFactory` from
    57  the k6 runner, probe ID, feature collection, and secret provider, then
    58  calls `NewWithOpts`.
    59  
    60  `NewWithOpts` derives a logger with `region_id`, `tenantId`, `check_id`,
    61  `probe`, `target`, `job`, and `check` (the type) labels, creates the
    62  per-scraper context via `context.WithCancel`, and asks the prober
    63  factory for the right `Prober` implementation. The Prober is constructed
    64  *once* per scraper — restarts come from the Updater destroying and
    65  recreating the scraper, not from re-probing.
    66  
    67  External callers that need the transformation pipeline without the agent run
    68  loop use `collector.New(...)` and `(*collector.Collector).Collect(ctx, t)`.
    69  The supplied `collector.Probe` registers metrics and emits logs through a
    70  small public interface; execution IDs remain owned and generated by the
    71  scraper. Callers must close the collector when it is no longer needed to
    72  release its internal scraper context. User labels default to the agent's
    73  prefixed mode; callers can select a different tenant-compatible shape with
    74  `collector.WithLabelMode(...)`.
    75  
    76  `t` controls the emitted event timestamp only. The collector does not install
    77  a virtual clock: its context deadline is still wall-clock time, and a supplied
    78  probe that returns immediately will not trigger that deadline merely because
    79  it reports a larger logical duration. Historical generators must resolve
    80  logical timeout outcomes before calling the collector. This timestamp policy
    81  is isolated to the unscheduled collector; the normal scheduled scraper keeps
    82  its existing wall-clock wrapper-log behavior.
    83  
    84  ### Run loop
    85  
    86  `Scraper.Run(ctx)`:
    87  
    88  ```mermaid
    89  sequenceDiagram
    90      participant Run as Scraper.Run
    91      participant Tick as tickWithOffset
    92      participant Handler as scrapeHandler
    93  
    94      Run->>Tick: with frequency, offset
    95      Tick->>Tick: wait initial offset
    96      loop work tick (every frequency)
    97          Tick->>Handler: scrape(ctx, t)
    98          Handler->>Prober: Probe
    99          Handler->>Publisher: Publish(payload)
   100          Handler->>Telemeter: AddExecution
   101      end
   102      loop inactivity tick (only if frequency > 2min)
   103          Tick->>Handler: republish(ctx, t)
   104          Handler->>Publisher: Publish (same data, new timestamps)
   105      end
   106      Note over Tick: on stop channel
   107      Tick->>Handler: cleanup(ctx, lastTick)
   108      Handler->>Publisher: Publish (stale markers)
   109  ```
   110  
   111  Three actions, all defined on `scrapeHandler`:
   112  
   113  - **`scrape`** — full probe run. Builds the payload via `collectData`, feeds the check state machine, records a `telemetry.Execution`, and publishes.
   114  - **`republish`** — same payload, timestamps advanced to *now*. Logs are dropped (`streams = nil`) so they aren't duplicated. Triggered by the inactivity ticker.
   115  - **`cleanup`** — runs when the stop channel closes (i.e. the Updater is tearing the scraper down). Replaces every sample value with a stale-marker NaN (`0x7ff0000000000002`) and publishes once more so Prometheus knows the series has ended.
   116  
   117  ### Shutdown
   118  
   119  The Updater calls `Scraper.Stop()` which closes `s.stop`. `tickWithOffset`
   120  sees the close, stops both tickers, calls `cleanup`, and returns. Then
   121  `Run` calls `s.cancel()` to release any prober-bound resources tied to
   122  the per-scraper context.
   123  
   124  If the parent context is cancelled instead (e.g. on SIGTERM), `cleanup`
   125  does *not* run — the agent is shutting down and we deliberately skip
   126  the extra publish.
   127  
   128  ## Scheduling: `tickWithOffset`
   129  
   130  The scheduler is a single function. It takes a `period`, `offset`,
   131  `maxIdle` (the republishing threshold — currently `maxPublishInterval`
   132  = 2 minutes), and `minGap` (don't republish within 10s of a real run).
   133  
   134  - The initial offset spreads load across scrapers that boot together. If the check has no explicit offset, one is chosen at random in `[0, min(frequency, 2 minutes))`.
   135  - A `workTicker` fires every `period` and calls `work`.
   136  - An `inactivityTicker` only exists if `period > maxIdle`. It fires every `maxIdle` and calls `idle` (= republish), but skips if it's within `minGap` of either side of a real run.
   137  - The stop channel triggers `cleanup`.
   138  - Context cancellation triggers immediate return without `cleanup`.
   139  
   140  This is the only place that knows about scheduling timings. Tune
   141  constants (`maxPublishInterval`, `minPublishGap`) in `scraper.go` if
   142  you need to change republishing behaviour.
   143  
   144  ## Payload assembly: `collectData`
   145  
   146  For each probe run:
   147  
   148  1. Build **user labels** (from check configuration) and **check-info labels** (probe, region — emitted as `sm_region` for tenants in label mode DUAL_WRITE/UNPREFIXED — instance, job, check_name, plus user labels in their mode-appropriate form) used by the `sm_check_info` metric.
   149  2. Pull the per-tenant `MetricLabels` and `LogLabels` limits from `LabelsLimiter`. Fail closed if the limiter errors.
   150  3. Capture all check logs into an in-memory `bytes.Buffer` via a `kitlog` logfmt logger. The logger is decorated with every log label.
   151  4. Choose a `timeout`:
   152     - k6-backed checks (`Scripted`, `Browser`, `MultiHttp`) use `frequency` as the timeout — the k6 runner has its own retry logic and the hard wall is "until the next scheduled run".
   153     - Everything else uses `Timeout` from the check.
   154  5. Run the prober via `getProbeMetrics`, which creates a fresh `prometheus.Registry`, calls `runProber`, gathers, and adds `sm_check_info` and derived summaries/histograms.
   155  6. Convert the gathered metric families into `prompb.TimeSeries` via `extractTimeseries`.
   156  7. Parse the captured logs into `logproto.Stream`s via `extractLogs`. Loki doesn't support joins, so every stream carries the full label set.
   157  8. Append `probe_success="0"|"1"` to log labels so failed-run lines are easy to filter.
   158  9. Return a `probeData` containing time series, streams, and the tenant's global ID.
   159  
   160  `patchDuration` is a workaround: for k6-backed checks the
   161  `probe_duration_seconds` value reported by the prober is "wait +
   162  script" time, while `probe_script_duration_seconds` is the value
   163  operators actually care about. The function rewrites the former to
   164  match the latter when both are present.
   165  
   166  ## The `sm_check_info` metric
   167  
   168  Special metric carrying check metadata for join queries. Defined by:
   169  
   170  - name = `sm_check_info`
   171  - value = `1`
   172  - labels = the `checkInfoLabels` built in `buildCheckInfoLabels` (probe, region — `sm_region` for tenants in label mode DUAL_WRITE/UNPREFIXED — instance, job, check_name, frequency, geohash, alert sensitivity, plus user labels in their mode-appropriate form).
   173  
   174  If you add a new piece of check metadata that operators need to join
   175  on, this is the metric to extend.
   176  
   177  ## Stale markers
   178  
   179  Stale markers are NaN values with a specific bit pattern Prometheus
   180  treats as "series ended". Defined as:
   181  
   182  ```go
   183  var (
   184      staleNaN    uint64  = 0x7ff0000000000002
   185      staleMarker float64 = math.Float64frombits(staleNaN)
   186  )
   187  ```
   188  
   189  `cleanup` replaces every sample value in the last payload with this
   190  marker and publishes one final time. The timestamp is bumped by 1 ms
   191  past the last real sample so the marker is strictly after the last
   192  real point.
   193  
   194  ## Check state machine
   195  
   196  `checkStateMachine` (`scraper.go`) holds a `passes` / `failures` /
   197  `threshold` triple. `pass()` and `fail()` call back when the *state*
   198  changes (not on every probe), which is how the agent logs "check
   199  entered PASS/FAIL state" without spamming.
   200  
   201  Today the threshold is effectively zero (transitions on the first
   202  flip). If you add hysteresis, this is the type.
   203  
   204  ## Key types and entry points
   205  
   206  | Type / function                     | File           | Notes                                                       |
   207  | ----------------------------------- | -------------- | ----------------------------------------------------------- |
   208  | `Scraper`                           | `scraper.go`   | One per active check.                                       |
   209  | `New(...)` / `NewWithOpts(...)`     | `scraper.go`   | Factory; `New` matches `scraper.Factory` consumed by Updater. |
   210  | `(*Scraper).Run(ctx)`               | `scraper.go`   | Scheduled execution.                                        |
   211  | `(*Scraper).Stop()`                 | `scraper.go`   | Closes the stop channel.                                    |
   212  | `tickWithOffset(...)`               | `scraper.go`   | Scheduling primitive (work + idle + cleanup).               |
   213  | `scrapeHandler.{scrape,republish,cleanup}` | `scraper.go` | The three tick actions.                                     |
   214  | `collectData(...)`                  | `scraper.go`   | Probe → payload.                                            |
   215  | `patchDuration(...)`                | `scraper.go`  | Aligns `probe_duration_seconds` with the script duration.    |
   216  | `NewMetrics(...)`, `Metrics`        | `metrics.go`   | Per-scraper scrape / error counters.                        |
   217  | `collector.New(...)` / `Collector.Collect(...)` | `pkg/collector/collector.go` | Public, unscheduled single-execution adapter. |
   218  
   219  ## Testing strategy
   220  
   221  `internal/scraper/scraper_test.go` runs **golden-file** tests against
   222  local servers:
   223  
   224  - For each check type the test brings up a local HTTP / DNS / TCP / gRPC server, constructs a scraper, runs one probe, and compares the gathered metric output against `testdata/<type>.txt` (or `<type>_basic.txt` for `BasicMetricsOnly`).
   225  - Update goldens with `-update-golden` (see the `testdata` Makefile target).
   226  - k6-backed types (`scripted.dat`, `browser.dat`, `multihttp.dat`, `k6.dat`) use pre-captured k6 output rather than launching the real binary, which keeps the tests fast.
   227  - Slow tests (real network) are guarded by `testing.Short()` so `make test-fast` skips them.
   228  
   229  Run just this package:
   230  
   231  ```bash
   232  make test-go GO_TEST_ARGS=./internal/scraper/...
   233  ```
   234  
   235  ## When to update this doc
   236  
   237  Update this document when you:
   238  
   239  - Change the scheduling primitives in `tickWithOffset` (`maxPublishInterval`, `minPublishGap`, offset randomisation).
   240  - Change the republish or stale-marker behaviour in `scrapeHandler`.
   241  - Add a check-info label or change how `sm_check_info` is assembled.
   242  - Add a new piece of label decoration in `collectData`.
   243  - Change the per-check-type timeout selection in `collectData`.
   244  - Add or remove a collaborator interface (`LabelsLimiter`, `Telemeter`, `TenantCals`).
   245  - Change the contract of `scraper.Factory` (consumed by `internal/checks`).
   246  - Add a new check type whose payload assembly differs from the existing pattern.
   247  - Touch the golden-file harness in `scraper_test.go` or the layout of `testdata/`.