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

     1  ---
     2  component: Glue / bootstrap
     3  source_paths:
     4    - cmd/synthetic-monitoring-agent/
     5  last_reviewed_commit: f77bec5e
     6  generated: 2026-05-23
     7  ---
     8  
     9  # Glue / bootstrap — `cmd/synthetic-monitoring-agent`
    10  
    11  ## Purpose
    12  
    13  The `synthetic-monitoring-agent` binary is the **glue**: it parses flags,
    14  constructs all top-level components in the right order, wires them
    15  together via plain field injection on option structs, and runs them as
    16  goroutines under a single `errgroup.Group`. There is no other place that
    17  knows how the agent is assembled — every component takes its
    18  collaborators as inputs.
    19  
    20  If you are looking for "where does X come from?" or "what gets started at
    21  boot?", this is the doc.
    22  
    23  ## Where it lives
    24  
    25  `cmd/synthetic-monitoring-agent/`
    26  
    27  | File         | Responsibility                                                                  |
    28  | ------------ | ------------------------------------------------------------------------------- |
    29  | `main.go`    | Flag parsing, bootstrap sequence (`run()`), `signalHandler()` for SIGTERM, cache setup, GOMEMLIMIT auto-tuning. |
    30  | `grpc.go`    | `dialAPIServer()` — bearer-token credentials, TLS, gRPC keep-alive parameters.  |
    31  | `http.go`    | HTTP `Mux`, readiness handler, `/disconnect` (sends SIGUSR1), `/logger` runtime log-level toggle, optional `/debug/pprof/*`. |
    32  | `flags.go`   | `StringList` custom flag type (comma-separated values).                         |
    33  | `metrics.go` | `registerMetrics()` — build-info, Go runtime, process collectors.               |
    34  | `secret.go`  | `Secret` string type that renders as `"<redacted>"` when logged.                |
    35  
    36  ## How it fits in
    37  
    38  ```mermaid
    39  flowchart TD
    40      cmd["cmd/synthetic-monitoring-agent<br/>(main.go run)"]
    41      cmd --> Updater["Updater<br/>internal/checks"]
    42      cmd --> Adhoc["Adhoc handler<br/>internal/adhoc"]
    43      cmd --> Publisher["Publisher<br/>internal/pusher"]
    44      cmd --> K6Runner["k6 runner<br/>internal/k6runner"]
    45      cmd --> Tenants["Tenant manager<br/>internal/tenants"]
    46      cmd --> Secrets["Secret provider<br/>internal/secrets"]
    47      cmd --> Limits["Limits<br/>internal/limits"]
    48      cmd --> Telemetry["Telemeter<br/>internal/telemetry"]
    49      cmd --> Meta["Metamonitoring<br/>internal/metamonitoring"]
    50      cmd --> HTTP["HTTP server<br/>/ready /metrics /disconnect /logger /debug/pprof"]
    51      cmd --> gRPC["gRPC client<br/>to API server"]
    52  ```
    53  
    54  ## Bootstrap sequence
    55  
    56  The single entry point is `run()` in `main.go`. The sequence below mirrors
    57  the actual order in that function — keep it in sync if you reorder
    58  anything.
    59  
    60  1. **Parse flags** into a single `config` struct (`main.go` ~lines 60–138). `-dev` enables several debug toggles at once. `-features` accepts a comma-separated feature-flag list.
    61  2. **Resolve the API token**: command line → `SM_AGENT_API_TOKEN` → `API_TOKEN`.
    62  3. **Set GOMEMLIMIT** based on cgroup/system memory if `-enable-auto-memlimit` is on (default). Implemented via `setupGoMemLimit()`.
    63  4. **Build the root `errgroup.Group`** from a cancellable context. Every long-running task is registered with `g.Go(...)`; `g.Wait()` at the end of `run()` is the agent's lifetime.
    64  5. **Initialise logging** (`zerolog`) — debug/verbose/default levels route to global `zerolog.SetGlobalLevel(...)`.
    65  6. **Install SIGTERM handler** (`signalHandler()`). Note this only handles `SIGINT` and `SIGTERM`; SIGUSR1 is installed *inside* the Updater (see [updater.md](updater.md)).
    66  7. **Build the usage reporter** (`internal/usage`) — HTTP to `stats.grafana.com` unless `-disable-usage-reports`.
    67  8. **Register standard Prometheus collectors** via `registerMetrics()`.
    68  9. **Set up the cache** (`setupCache()`): auto mode picks memcached if `-memcached-servers` is non-empty, otherwise local; can be forced to `noop`. Falls back gracefully on errors.
    69  10. **Create the readiness handler** (`NewReadynessHandler()`). The Updater calls `Set(true)` once it has registered with the API; the handler is wired into `/ready`.
    70  11. **Build the HTTP mux** (`NewMux()`) and start the HTTP server. The server is shut down via a separate `g.Go` that waits on `ctx.Done()` and calls `Shutdown` with a 5-second timeout.
    71  12. **Dial the API server** (`dialAPIServer()` in `grpc.go`). Uses bearer-token credentials and gRPC keep-alive set to `synthetic_monitoring.HealthCheckInterval` / `HealthCheckTimeout`.
    72  13. **Build the k6 runner** if the `k6` feature is set (it is, by default, unless `-disable-k6`). Validates `-blocked-nets` as a CIDR.
    73  14. **Build the tenant manager**, **publisher** (selected by `-publisher`; v2 is the default), **limits**, **secret provider**, **cost attribution labels**, and **telemeter**.
    74  15. **Spawn the Updater**: `checks.NewUpdater(...)` + `g.Go(updater.Run)`.
    75  16. **Spawn the Adhoc handler**: `adhoc.NewHandler(...)` + `g.Go(handler.Run)`.
    76  17. **Spawn metamonitoring** if `-experimental-push-telemetry` is set.
    77  18. **Spawn the k6 versions handler** if a k6 runner exists.
    78  19. **Block on `g.Wait()`**.
    79  
    80  ### Connection back-off
    81  
    82  `newConnectionBackoff()` (`main.go`) returns the `*backoff.Backoff` shared
    83  by both the Updater and the Adhoc handler: 2s → 30s in ~8 steps, with
    84  jitter. If you tune reconnect behaviour, this is the knob.
    85  
    86  ## HTTP endpoints
    87  
    88  All registered in `NewMux()` (`http.go`):
    89  
    90  | Path              | Purpose                                                     | Gated by                        |
    91  | ----------------- | ----------------------------------------------------------- | ------------------------------- |
    92  | `/`               | Returns "hello, world!". 404 for any other path.            | always on                       |
    93  | `/metrics`        | Prometheus scrape endpoint, instrumented.                   | always on                       |
    94  | `/ready`          | 200 once the Updater has connected at least once; otherwise 503. | always on                       |
    95  | `/logger`         | `POST debug` / `POST default` to change the log level at runtime. | `-enable-change-log-level` |
    96  | `/disconnect`     | Sends `SIGUSR1` to the agent process (see `disconnectHandler` in `http.go`). | `-enable-disconnect`        |
    97  | `/debug/pprof/*`  | Standard `net/http/pprof` profiling handlers.               | `-enable-pprof`                 |
    98  
    99  `-dev` flips on all four optional toggles at once.
   100  
   101  The mux also publishes HTTP metrics (`http_requests_duration_seconds`,
   102  `http_requests_written_bytes`, `http_requests_in_flight`) via
   103  `httpsnoop.CaptureMetrics`.
   104  
   105  ## Signal handling
   106  
   107  Two signals matter to the agent, and they are wired up in two different
   108  places:
   109  
   110  - **`SIGINT` / `SIGTERM` → graceful shutdown.** Installed by `signalHandler()` in `main.go`. On signal, it returns an error from its `errgroup` goroutine, which cancels the shared context and unwinds every other goroutine.
   111  - **`SIGUSR1` → temporary API disconnect ("handover").** Installed *inside* the Updater's connect loop by `installSignalHandler()` in `internal/checks/checks.go`. The handler closes the gRPC stream context but leaves scrapers running. The Updater then waits the back-off before reconnecting; this gives a replacement agent a window to connect and pick up the probe. See [updater.md](updater.md) for the lifecycle.
   112  
   113  `POST /disconnect` is a thin wrapper around `os.FindProcess(os.Getpid())
   114  .Signal(syscall.SIGUSR1)` (`disconnectHandler` in `http.go`).
   115  
   116  ## Dependency injection conventions
   117  
   118  Top-level components do **not** import each other for construction.
   119  Everything is built in `run()` and passed in as fields of an `Opts`
   120  struct. The Updater's `UpdaterOptions` (`internal/checks/checks.go`) is
   121  the canonical example: every collaborator (`Conn`, `Logger`, `Publisher`,
   122  `TenantCh`, `ProbeCh`, `IsConnected` callback, `K6Runner`,
   123  `ScraperFactory`, ...) is a field. The Adhoc handler follows the same
   124  pattern with `adhoc.HandlerOpts`.
   125  
   126  Two channels are created in `run()` and shared:
   127  
   128  - `tenantCh chan synthetic_monitoring.Tenant` — both the Updater and the Adhoc handler push tenant changes into this channel; the tenant manager consumes it.
   129  - `probeCh chan *synthetic_monitoring.Probe` (buffer 1) — the Updater sends the probe identity into this channel exactly once (`notifyProbeTenant`); metamonitoring consumes it to learn which tenant owns this probe.
   130  
   131  If you add a new top-level component, follow the same pattern: define an
   132  `Opts` struct, do all wiring in `run()`, and register the goroutine with
   133  `g.Go(...)`.
   134  
   135  ## Design details
   136  
   137  - **Lifetime ordering.** The HTTP server is started before the gRPC connection so `/ready` and `/metrics` are reachable during initial registration. Components that depend on the gRPC connection (Updater, Adhoc, telemeter, k6versions) are constructed after `dialAPIServer`.
   138  - **Readiness.** `readynessHandler` is a named type `int32`; used as `*readynessHandler`. The Updater calls `Set(true)` once it has registered with the API (see `Updater.loop` in `internal/checks/checks.go`). It never goes back to unready — the agent is either freshly started or it is ready.
   139  - **`Secret` for the API token.** `Secret.MarshalText` returns `"<redacted>"` so the config struct can be safely logged (`zl.Info().Interface("config", config)`).
   140  - **gRPC keep-alive.** Tuned via `HealthCheckInterval` / `HealthCheckTimeout` defined in the protobuf package. Required to detect network failures absent client writes — without it, the agent hangs when the server disappears mid-call.
   141  - **Cache fallbacks.** `setupCache` and `setupLocalCache` log and fall back rather than failing — the agent will boot with a noop cache if everything else fails. This is intentional: caching is a load-shedding optimisation, not a correctness requirement.
   142  - **The `k6` and `traceroute` feature flags are deprecated.** They are permanently enabled. `notifyAboutDeprecatedFeatureFlags` logs a hint if someone still passes them on the command line.
   143  - **The `protocol-secrets` feature flag.** When set (`-features protocol-secrets`), `config.EnableProtocolSecrets` is turned on, which makes the agent advertise `ProbeInfo.SupportsProtocolSecrets=true` at registration and enables `${secrets.<name>}` resolution for checks that opt in. It is opt-in for now, intended to become the default once released.
   144  
   145  ## Testing strategy
   146  
   147  `cmd/synthetic-monitoring-agent` carries only unit tests — there is no
   148  end-to-end test of `run()` itself. The tests cover the small leaf pieces:
   149  
   150  - `flags_test.go` — `StringList.Set` parsing and trimming.
   151  - `http_test.go` — the `readynessHandler` state machine and the `loggerHandler` request validation.
   152  - `secret_test.go` — `Secret.String` and `Secret.MarshalText` redaction.
   153  
   154  Bootstrap behaviour is exercised indirectly: each top-level component has
   155  its own tests under `internal/...` that drive it with mock collaborators
   156  matching the shape of the `Opts` struct.
   157  
   158  If you add a new branch in `run()` (a flag, a feature toggle, a new
   159  goroutine), prefer adding a focused unit test next to the new helper
   160  rather than trying to test `run()` itself.
   161  
   162  ## Other binaries
   163  
   164  `cmd/` contains two additional binaries that are *not* part of the
   165  running agent:
   166  
   167  - `cmd/synthetic-monitoring-proto` — small utility for manipulating serialised check protobufs.
   168  - `cmd/test-api` — mock gRPC API server used during local development.
   169  
   170  Document them separately if they grow.
   171  
   172  ## When to update this doc
   173  
   174  Update this document when you:
   175  
   176  - Add, remove, or reorder steps in `run()` in `main.go`.
   177  - Add or remove a top-level component constructed in `run()`.
   178  - Add or remove a command-line flag or environment variable consumed by `run()`.
   179  - Add or remove an HTTP route in `NewMux()` (`http.go`).
   180  - Change the connection back-off parameters in `newConnectionBackoff()`.
   181  - Change `SIGTERM` or `SIGUSR1` handling in `main.go` or `http.go`.
   182  - Change the readiness contract (`readynessHandler.Set` / `ServeHTTP`).
   183  - Add or remove an inter-component channel shared at boot.
   184  - Modify the cache setup or fallback logic in `setupCache` / `setupLocalCache`.