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

     1  # Developer's guide to the Synthetic Monitoring agent
     2  
     3  This file provides some guidance as to how to work with the codebase.
     4  
     5  ## Project Overview
     6  
     7  This is the Grafana Synthetic Monitoring agent: a distributed probe that
     8  executes network checks (HTTP, DNS, TCP, ICMP, gRPC, Traceroute, k6-based
     9  scripted/browser checks) from various locations and pushes metrics/logs to
    10  Grafana Cloud (Prometheus/Loki).
    11  
    12  ## Common Commands
    13  
    14  The build system is organized around make. Make is ubiquitous and the chances
    15  are that if you have `go` installed, you also have `make` installed. If not,
    16  it's straightforward to install (possibly using your distribution's preferred
    17  package manager), with very little dependencies.
    18  
    19  You can get up-to-date documentation by running `make help`, but some common
    20  targets are mentioned below.
    21  
    22  ### Building
    23  ```bash
    24  make build              # Build all Go binaries for all platforms
    25  make build-native       # Build only for current OS/arch
    26  make deps               # Download and verify Go dependencies
    27  ```
    28  
    29  To run scripted, browser, and multihttp checks the agent uses the `sm-k6`
    30  binary (a k6 build with the Synthetic Monitoring extensions). Use the
    31  following commands to download it:
    32  
    33  ```bash
    34  make sm-k6              # Download sm-k6 for all supported platforms (iterates over XK6_PLATFORMS)
    35  make sm-k6-native       # Download sm-k6 only for the current host OS/arch
    36  ```
    37  
    38  Binaries are placed under `dist/<os>-<arch>/k6-v1`.
    39  
    40  ### Testing
    41  ```bash
    42  make test               # Run all tests with coverage and race detection
    43  make test-fast          # Run only fast tests (with -short flag)
    44  
    45  # Run specific package tests
    46  make test-go GO_TEST_ARGS=./internal/prober/...
    47  
    48  # Run single test
    49  make test-go GO_TEST_ARGS="-v -run TestSpecificTest ./internal/package/..."
    50  ```
    51  
    52  Note that tests are run inside a docker container, to ensure some level of
    53  reproducibility and isolation from the local environment. If you want, you can
    54  use `go test` directly, but keep in mind that this might end up using a
    55  different version of the Go compiler, and it might be affected by other things
    56  in your local environment.
    57  
    58  ### Linting
    59  ```bash
    60  make lint               # Run all linters (golangci-lint + go vet + shellcheck)
    61  make lint-go            # Run only Go linters
    62  ```
    63  
    64  Again, the linting tools are executed in a docker container, to ensure that
    65  versions match. You can use `golangci-lint` directly, but keep in mind that the
    66  configuration file targets v2.
    67  
    68  ### Code generation
    69  
    70  Some of the code and support files are generated, specifically:
    71  
    72  - internal/scraper/testdata/*.txt
    73  - pkg/accounting/data.go
    74  - pkg/pb/synthetic_monitoring/checks.pb.go
    75  
    76  In order to update `internal/scraper/testdata/*.txt` you can use the
    77  `testdata` target in the Makefile.
    78  
    79  `pkg/accounting/data.go` and `pkg/pb/synthetic_monitoring/checks.pb.go`
    80  are updated by the `generate` target in the Makefile.
    81  
    82  Code generation is *not* run automatically, but there are some tests
    83  that try to detect discrepancies.
    84  
    85  ### Development
    86  
    87  ```bash
    88  # Run the agent locally (requires API token and sm-k6 binary)
    89  ./dist/synthetic-monitoring-agent \
    90    -api-server-address=synthetic-monitoring-grpc.grafana.net:443 \
    91    -api-token=<your-token> \
    92    -k6-repository=./dist/<os>-<arch> \
    93    -verbose
    94  ```
    95  
    96  The `-k6-repository` flag points the agent at a directory containing the
    97  `sm-k6` binary. It defaults to `/usr/libexec/sm-k6`.
    98  
    99  Other k6 flags:
   100  
   101  - **`-k6-uri=<path>`** — direct path to a k6 binary; overrides the
   102    repository directory.
   103  - **`-disable-k6`** — run without the `sm-k6` binary. The agent
   104    will not be able to execute k6 checks.
   105  
   106  Build Docker images:
   107  
   108  ```bash
   109  docker build --target release .              # Without Chromium (smaller)
   110  docker build --target with-browser .         # With Chromium (for browser checks)
   111  ```
   112  
   113  ### Livereload development
   114  
   115  Install [air](https://github.com/air-verse/air?tab=readme-ov-file#installation)
   116  and ensure `SM_AGENT_API_TOKEN` is set prior to running.
   117  
   118  ```bash
   119  air
   120  ```
   121  
   122  Any code changes will now trigger air to rebuild the agent and run the
   123  generated executable.
   124  
   125  ## Architecture Overview
   126  
   127  ### Core Flow
   128  ```
   129  API Server (gRPC) → Updater → Scrapers → Probers → Publisher → Prometheus/Loki
   130                           ↓
   131                      Adhoc Handler (on-demand checks)
   132  ```
   133  
   134  ### Key Components
   135  
   136  **Updater** (`internal/checks/checks.go`)
   137  - Maintains long-lived gRPC stream to API server via `GetChanges()`
   138  - Receives check ADD/UPDATE/DELETE operations
   139  - Manages life cycle of all scrapers (one per check)
   140  - Handles reconnection with back off on failures
   141  - Responds to SIGUSR1 for graceful disconnect (enables zero-downtime upgrades)
   142  
   143  **Scraper** (`internal/scraper/scraper.go`)
   144  - One instance per active check, runs on scheduled interval
   145  - Executes probe via prober interface
   146  - Adds user-defined labels and check metadata to all telemetry
   147  - Implements "republishing" - resends metrics with updated timestamps when check interval > 2min
   148  - Sends stale markers (NaN) on shutdown for proper Prometheus metric life cycle
   149  - Tracks check state machine (passing/failing thresholds)
   150  
   151  **Prober** (`internal/prober/prober.go`)
   152  - Factory pattern for check-type-specific implementations
   153  - Types: HTTP, DNS, TCP, ICMP, gRPC, Traceroute, Scripted (k6), Browser, MultiHTTP
   154  - Interface: `Probe(ctx, target, registry, logger) (success bool, duration time.Duration)`
   155  - Each prober registers metrics into Prometheus registry
   156  
   157  **k6Runner** (`internal/k6runner/`)
   158  - Handles scripted, browser, and multihttp checks via k6
   159  - Two modes: Local (executes k6 binary) or HTTP (remote k6 runner service)
   160  - HTTP runner includes retry logic with back off for transient failures
   161  - Grace time: adds 20s to timeout for runner communication overhead
   162  - Processor parses k6 output and registers metrics in Prometheus format
   163  
   164  **Publisher** (`internal/pusher/v2/publisher.go`)
   165  - V2 is current (V1 exists for compatibility)
   166  - Per-tenant push handlers with automatic life cycle management
   167  - Batches and sends remote write to Prometheus/Loki
   168  - Publishers created via registry/factory pattern
   169  
   170  **Adhoc Handler** (`internal/adhoc/adhoc.go`)
   171  - Separate gRPC stream for on-demand checks (instant testing from UI)
   172  - Creates ephemeral runners for single-shot execution
   173  - Results published as logs with special labels
   174  - No retry or scheduling logic
   175  
   176  **Tenant Manager** (`internal/tenants/manager.go`)
   177  - Caches tenant metadata with TTL (15min default with jitter)
   178  - Provides tenant info to publisher for authentication
   179  - Thread-safe with per-tenant locks
   180  
   181  ### Check Types
   182  
   183  **Simple probers**: DNS, HTTP, TCP, ICMP, gRPC, Traceroute
   184  - Direct implementation using
   185    [blackbox_exporter](https://github.com/prometheus/blackbox_exporter) as backend
   186  - ICMP and Traceroute implement the same interface, but are custom
   187    implementations
   188  - Return metrics via Prometheus registry
   189  
   190  **k6-Based**: Scripted, Browser, MultiHTTP
   191  - Convert check settings to k6 script
   192  - Execute via k6runner (local process or remote HTTP service)
   193  - MultiHTTP generates k6 script from assertion-based configuration
   194  - Parse k6's Prometheus output format provided by a custom extension
   195  
   196  ### Data Flow
   197  
   198  ![agent process][process]
   199  
   200  **Metrics Path:**
   201  ```
   202  Prober → Prometheus Registry → Scraper (extract + label) → Publisher → Remote Write → Prometheus
   203  ```
   204  
   205  **Logs Path:**
   206  ```
   207  Prober (logfmt logger) → Scraper (parse + label) → Publisher → Loki Push API
   208  ```
   209  
   210  **Special Metric:**
   211  - `sm_check_info`: Contains check metadata (geohash, frequency, alert sensitivity)
   212  - Used for joining check data in queries
   213  
   214  ## Important Patterns
   215  
   216  ### Error Handling
   217  - Custom error types: `FatalError` (stop scraper), `TransientError` (retry with back off)
   218  - Use `pkg/errors.Wrap()` to add context
   219  - k6 runner retries on retriable errors (timeout, connection refused, etc.)
   220  
   221  ### Metrics Philosophy
   222  - **Derived metrics**: Creates summaries/histograms from gauge metrics for flexibility
   223  - **Stale markers**: Sends NaN values on scraper shutdown so Prometheus knows metric is gone
   224  - **Republishing**: Resends metrics with updated timestamps to handle long intervals (keeps data fresh)
   225  - **Check info metric**: Special `sm_check_info` with labels for metadata joins
   226  
   227  ### Concurrency
   228  - Heavy use of `errgroup.Group` for coordinated goroutine management
   229  - Per-tenant locks in publisher to avoid contention
   230  - Each scraper has independent context for life cycle control
   231  - Context-aware sleep functions for clean shutdown
   232  
   233  ### Configuration
   234  - Check configuration comes from API (not local files)
   235  - Probe capabilities negotiated during registration
   236  - Feature flags control which check types are enabled
   237  - Environment-based configuration via command-line flags
   238  
   239  ### Tenant Isolation
   240  - Every payload tagged with tenant ID
   241  - Publisher maintains separate handlers per tenant
   242  - Tenant metadata cached with expiration
   243  - Secrets stored in remote secret store (not in agent)
   244  
   245  ## Code Organization
   246  
   247  ```
   248  cmd/
   249    synthetic-monitoring-agent/    # Main agent binary
   250    test-api/                      # Test API server (outdated)
   251    synthetic-monitoring-proto/    # Proto manipulation
   252  
   253  internal/
   254    adhoc/          # On-demand check execution
   255    checks/         # Check lifecycle management (Updater)
   256    k6runner/       # K6 script execution (local/remote)
   257    prober/         # Check type implementations
   258    pusher/         # Metrics/logs publishing (v1 and v2)
   259    scraper/        # Scheduled check execution
   260    tenants/        # Tenant metadata caching
   261    usage/          # Usage reporting
   262    secrets/        # Secret store integration
   263    limits/         # Rate limiting
   264    telemetry/      # Internal metrics
   265    version/        # Version information
   266  
   267  pkg/pb/synthetic_monitoring/     # Generated protobuf code
   268  ```
   269  
   270  ## Development Notes
   271  
   272  ### Testing
   273  - Tests use CGO for race detection (`CGO_ENABLED=1` in Makefile)
   274  - Use `-short` flag for fast tests that skip integration tests
   275  - Testhelper package (`internal/testhelper/`) provides common test utilities
   276  
   277  ### Linting
   278  - Configuration in `.golangci.yaml`
   279  - Custom rules via ruleguard in `internal/rules/rules.go`
   280  - Import aliases enforced: `sm` for synthetic_monitoring, `dto` for Prometheus client model
   281  - Maximum cyclomatic complexity: 20
   282  
   283  ### Docker Images
   284  - Two variants: with/without Chromium (tagged `*-browser`)
   285  - Use `--target release` for non-browser builds to keep images small
   286  - Multi-stage Dockerfile defaults to browser variant
   287  
   288  ### Signals
   289  - `SIGTERM`: Graceful shutdown
   290  - `SIGUSR1`: Disconnect from API but keep running checks (allows another agent to connect)
   291    - After 1 minute, attempts to reconnect if no other agent has connected
   292    - Also available via `/disconnect` HTTP endpoint
   293    - Use case: Zero-downtime agent upgrades
   294  
   295  ### HTTP Endpoints
   296  - `/ready`: Returns 200 when connected to API, 503 otherwise
   297  - `/metrics`: Prometheus metrics for agent internals
   298  - `/disconnect`: Sends SIGUSR1 to process
   299  - `/debug/pprof`: Performance profiling (if enabled)
   300  
   301  ### Build System
   302  - Uses make with modular includes from `scripts/make/`
   303  - Builds for multiple platforms: `linux/amd64`, `linux/arm64`
   304  - Version info injected at build time via ldflags
   305  - Output in `dist/` directory
   306  
   307  ### API Integration
   308  - gRPC with streaming: `GetChanges()` for continuous check updates
   309  - Registration includes probe info (version, capabilities, location)
   310  - Health checks via `Ping()` RPC with 30s interval
   311  - Tenant metadata fetched via `GetTenant()` RPC and cached
   312  - Adhoc checks via separate `GetAdHocChecks()` stream
   313  
   314  ### Performance Considerations
   315  - Auto-memory limit based on cgroup/system memory (90% by default)
   316  - Random offset per check within frequency to spread load
   317  - Republishing keeps metrics fresh without overloading backends
   318  
   319  [process]: https://www.planttext.com/api/plantuml/svg/dLHDRy8m3BtdLqGz3wHTEKo88KsxJVi78VLA14swn47ityzE0ZHLcIQua8zdl-Tdf-k0ocFiZqAq2jLE1P3DTjE8WOwDDeEoA9lmOt4Fj5_qpXfqtjXkeGRJI1Ka_Sl_m3kmc0DuLKUGY0xoRLxMruDtFL3A61BajgrXHtV8adWXHEAHYvUaS2MrinOqIdS2Bzy-FrwdW02svTmxaCP-ETyhDCuAfT6S54BHpLWAsMuemeDsliG83nYzBGdUjwA5IUIKpyDtX81Ixq4VP40Fgh-apz2YAGEUnNAv_EFUYiwxE53nRf0alnoVXQJVbJhRotQaMpY3ZgdCXBe8BatWir8M6UwDfkOHtz5r8TsDIXn5P1dEQaZRYhwk_DP8EkeTPkDlKJErpkBci_CKF9oNpchZHbfNSeXXVx6aXYNI0hZwn8shXHPgD3suY8BPKdkdCzAQKERsxk2DCRCv7XxyUuIygJHLJbuVWB3iQ69DWAVyBjc8e7fWe8OG-C7kW6Y1RP0S9CIQblHL-WK0
   320  [PlantUML]: https://www.planttext.com/?text=dLHDRy8m3BtdLqGz3wHTEKo88KsxJVi78VLA14swn47ityzE0ZHLcIQua8zdl-Tdf-k0ocFiZqAq2jLE1P3DTjE8WOwDDeEoA9lmOt4Fj5_qpXfqtjXkeGRJI1Ka_Sl_m3kmc0DuLKUGY0xoRLxMruDtFL3A61BajgrXHtV8adWXHEAHYvUaS2MrinOqIdS2Bzy-FrwdW02svTmxaCP-ETyhDCuAfT6S54BHpLWAsMuemeDsliG83nYzBGdUjwA5IUIKpyDtX81Ixq4VP40Fgh-apz2YAGEUnNAv_EFUYiwxE53nRf0alnoVXQJVbJhRotQaMpY3ZgdCXBe8BatWir8M6UwDfkOHtz5r8TsDIXn5P1dEQaZRYhwk_DP8EkeTPkDlKJErpkBci_CKF9oNpchZHbfNSeXXVx6aXYNI0hZwn8shXHPgD3suY8BPKdkdCzAQKERsxk2DCRCv7XxyUuIygJHLJbuVWB3iQ69DWAVyBjc8e7fWe8OG-C7kW6Y1RP0S9CIQblHL-WK0