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

     1  ---
     2  component: Probers
     3  source_paths:
     4    - internal/prober/
     5  last_reviewed_commit: f77bec5e
     6  generated: 2026-05-23
     7  ---
     8  
     9  # Probers — `internal/prober`
    10  
    11  ## Purpose
    12  
    13  A Prober executes a single check of a given type. The package exposes a
    14  single `Prober` interface and a factory that picks the right
    15  implementation per check type. Each implementation either wraps a
    16  `blackbox_exporter` module, runs a custom probe (ICMP, Traceroute), or
    17  delegates to the [k6 runner](k6runner.md) for
    18  scripted/browser/multihttp checks.
    19  
    20  ## Where it lives
    21  
    22  `internal/prober/` plus one subpackage per check type:
    23  
    24  | Path                                | Check type / role                                            |
    25  | ----------------------------------- | ------------------------------------------------------------ |
    26  | `prober.go`                         | `Prober` interface, `ProberFactory`, type dispatch.          |
    27  | `http/`                             | HTTP — wraps `blackbox_exporter/prober`.                     |
    28  | `dns/`                              | DNS — uses an in-tree fork (`dns/internal/bbe`) of the upstream blackbox-exporter DNS prober due to an unmerged upstream PR. Has an experimental implementation gated on `feature.ExperimentalDnsProber`. |
    29  | `tcp/`                              | TCP — wraps `blackbox_exporter/prober`.                      |
    30  | `grpc/`                             | gRPC — wraps `blackbox_exporter/prober`.                     |
    31  | `icmp/`                             | ICMP — custom implementation (`icmp_impl.go` + `utils.go`).  |
    32  | `traceroute/`                       | Traceroute — custom implementation.                          |
    33  | `scripted/`                         | k6-backed scripted check.                                    |
    34  | `browser/`                          | k6-backed browser check.                                     |
    35  | `multihttp/`                        | k6-backed MultiHTTP; generates a k6 script from assertions (`script.go`, `script.tmpl`). |
    36  | `interpolation/`                    | Helpers for substituting check-config values into request payloads (used by MultiHTTP). |
    37  | `logger/`                           | The `logger.Logger` interface the prober contract uses to emit log lines. |
    38  
    39  ## How it fits in
    40  
    41  ```mermaid
    42  flowchart LR
    43      Scraper -->|Probe ctx, target, registry, logger| Prober
    44      Prober -.->|scripted / browser / multihttp| K6Runner["k6 runner<br/>(see separate doc)"]
    45      subgraph "Blackbox-based"
    46          HTTP
    47          DNS
    48          TCP
    49          gRPC
    50      end
    51      subgraph "Custom"
    52          ICMP
    53          Traceroute
    54      end
    55      subgraph "k6-backed"
    56          Scripted
    57          Browser
    58          MultiHTTP
    59      end
    60  ```
    61  
    62  ## The `Prober` interface
    63  
    64  ```go
    65  type Prober interface {
    66      Name() string
    67      Probe(ctx context.Context, target string, registry *prometheus.Registry, logger logger.Logger) (bool, float64)
    68  }
    69  ```
    70  
    71  Contract:
    72  
    73  - `Name()` returns the check-type string (used in metric labels and logs).
    74  - `Probe(...)` runs *one* probe. It registers all of its metrics in the caller-supplied `prometheus.Registry`, emits log lines via the caller-supplied `logger.Logger`, and returns `(success, duration)`.
    75  - The registry is fresh per-probe (created by `scraper.getProbeMetrics`) — implementations don't have to worry about deduplication or cleanup.
    76  - The function `prober.Run(...)` is a convenience that just calls `p.Probe(...)`. Use it if you don't already have a Prober reference.
    77  
    78  ## The `ProberFactory`
    79  
    80  ```go
    81  type ProberFactory interface {
    82      New(ctx context.Context, logger zerolog.Logger, check model.Check) (Prober, string, error)
    83  }
    84  ```
    85  
    86  Returns `(prober, target, error)`. The `target` is the address the
    87  scraper logs and exposes as the `instance` label — for most check types
    88  that's `check.Target`, but for DNS it's the DNS server address
    89  (`check.Settings.Dns.Server`), since the check.Target itself is the
    90  *name being looked up*.
    91  
    92  `NewProberFactory` (in `prober.go`) is constructed with the k6 runner,
    93  the probe ID (used to inject the `x-sm-id` request header for HTTP /
    94  MultiHTTP — see `getReservedHeaders`), the feature collection, and the
    95  secret provider.
    96  
    97  ### Type dispatch
    98  
    99  Defined in `proberFactory.New` (`prober.go`):
   100  
   101  | `sm.CheckType_*`     | Implementation                                                |
   102  | -------------------- | ------------------------------------------------------------- |
   103  | `CheckTypePing`      | `icmp.NewProber(check)`                                       |
   104  | `CheckTypeHttp`      | `httpProber.NewProber(ctx, check, logger, reservedHeaders, secretStore)` |
   105  | `CheckTypeDns`       | `dns.NewProber(check)` (or `dns.NewExperimentalProber` if `feature.ExperimentalDnsProber` is set) |
   106  | `CheckTypeTcp`       | `tcp.NewProber(ctx, check, logger)`                           |
   107  | `CheckTypeTraceroute`| `traceroute.NewProber(check, logger)`                         |
   108  | `CheckTypeScripted`  | `scripted.NewProber(ctx, check, logger, runner, secretStore)` — requires k6 runner. |
   109  | `CheckTypeBrowser`   | `browser.NewProber(ctx, check, logger, runner, secretStore)` — requires k6 runner. |
   110  | `CheckTypeMultiHttp` | `multihttp.NewProber(ctx, check, logger, runner, reservedHeaders, secretStore)` — requires k6 runner. |
   111  | `CheckTypeGrpc`      | `grpc.NewProber(ctx, check, logger)`                          |
   112  | (anything else)      | `errUnsupportedCheckType`                                     |
   113  
   114  If you add a new check type, this is the *only* place the agent learns
   115  about it — but you also need to wire it through `internal/checks`
   116  (feature gating) and `internal/scraper` (timeout selection,
   117  `sm_check_info` labels if relevant).
   118  
   119  ### Reserved headers
   120  
   121  `getReservedHeaders` injects the `x-sm-id` request header for HTTP and
   122  MultiHTTP checks. The header carries `"<globalCheckID>-<probeId>"` so
   123  upstream services can correlate requests with check executions. If
   124  `probeId == 0` (the test path, or before registration), nothing is
   125  injected.
   126  
   127  ## Implementation families
   128  
   129  ### Blackbox-based (`http`, `dns`, `tcp`, `grpc`)
   130  
   131  These wrap `github.com/prometheus/blackbox_exporter/prober`. The
   132  per-type file translates the SM `Check` configuration into a
   133  blackbox-exporter `config.Module` and calls the upstream
   134  `ProbeHTTP` / `ProbeDNS` / `ProbeTCP` / `ProbeGRPC` function.
   135  
   136  DNS is special: the project keeps an in-tree fork at
   137  `prober/dns/internal/bbe` because the upstream DNS prober is missing a
   138  behaviour change (see the PR link in `dns/internal/bbe/prober/dns.go`).
   139  The fork is intentionally narrow — keep it in sync with upstream when
   140  practical.
   141  
   142  ### Custom (`icmp`, `traceroute`)
   143  
   144  These implement the same interface but do not call into
   145  blackbox-exporter. They emit the same general shape of metrics
   146  (`probe_success`, `probe_duration_seconds`, plus type-specific gauges)
   147  so the rest of the pipeline doesn't need to know they are custom.
   148  
   149  ICMP splits across `icmp.go` and `icmp_impl.go` so the noisy raw-socket
   150  work stays in one place.
   151  
   152  ### k6-backed (`scripted`, `browser`, `multihttp`)
   153  
   154  These three are thin shells around the k6 runner. See
   155  [k6runner.md](k6runner.md) for everything
   156  about k6 execution and output parsing. The relevant per-prober
   157  responsibility is *script preparation*:
   158  
   159  - **scripted**: the user supplies the script verbatim.
   160  - **browser**: a small wrapper script around the user's browser-test code.
   161  - **multihttp**: the user supplies a *list of HTTP requests* with assertions; `multihttp/script.go` renders a k6 script from `script.tmpl` plus optional `interpolation/` substitutions. If you change the rendered script, update the golden tests in `multihttp/script_test.go`.
   162  
   163  ## Design details
   164  
   165  - **Per-probe Prometheus registry**: the scraper hands a *fresh* `prometheus.Registry` to every probe. This is what lets all probers register metrics with stable names (`probe_*`) without colliding across check runs.
   166  - **Logging via `logger.Logger`**: probers don't see Loki directly. They get a `logger.Logger` (a thin adapter around `go-kit/kit/log`) that the scraper has decorated with all the necessary labels. Anything written to it becomes a log line on the check's stream.
   167  - **Capabilities**: the API can disable scripted or browser support per probe (`Probe_Capabilities`). The Updater enforces capability validation against the available k6 runner; the prober factory itself just refuses to construct k6-backed probers when no runner is configured.
   168  - **Context lifetime**: the context handed to `New(...)` for k6-backed probers is the *scraper's* context (cancelled when the scraper is destroyed). The context handed to `Probe(...)` is the *per-probe* context with the timeout already applied.
   169  
   170  ## Testing strategy
   171  
   172  - **Table-driven** unit tests sit next to each implementation (`<type>_test.go`).
   173  - HTTP uses `internal/prober/http/testserver` to spin up a real local HTTP server with a fixed handler set.
   174  - DNS uses `github.com/miekg/dns` for an in-process DNS server.
   175  - MultiHTTP has both behavioural tests (`multihttp_test.go`) and script-generation tests (`script_test.go`) — the latter verify the rendered k6 script byte-for-byte.
   176  - k6-backed prober tests use the fake k6 binary at `internal/k6runner/testdata/k6-fake`, so no real k6 process is needed.
   177  - The factory itself has `prober_test.go` covering each `case` in the dispatch.
   178  
   179  End-to-end metric expectations are pinned by the **scraper** golden
   180  files (`internal/scraper/testdata/*.txt`), not by the prober tests
   181  themselves. If you change emitted metrics, regenerate those goldens.
   182  
   183  Run a single prober's tests:
   184  
   185  ```bash
   186  make test-go GO_TEST_ARGS=./internal/prober/http/...
   187  ```
   188  
   189  ## When to update this doc
   190  
   191  Update this document when you:
   192  
   193  - Add a new `case` in `proberFactory.New` (i.e. a new check type).
   194  - Change the `Prober` interface signature.
   195  - Change what `target` is returned from the factory for any check type.
   196  - Add or remove a reserved header in `getReservedHeaders`.
   197  - Add or remove a feature flag gating an alternative prober implementation (e.g. `ExperimentalDnsProber`).
   198  - Re-sync the `dns/internal/bbe` fork with upstream `blackbox_exporter`, or move it back to the upstream module.
   199  - Add a new prober subpackage, or change which family a check type belongs to (blackbox-based / custom / k6-backed).
   200  - Change the contract of `logger.Logger` consumed by probers.