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/`.