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