Skip to content

Commit 35c2429

Browse files
sarg3ntclaude
andcommitted
feat(#95): agent — phase 3 source detection (nginx, Apache, Caddy, Traefik, Docker, host)
Completes the remaining detection work for issue #95 on top of #96's primary-source selection. Six new probe-only gears land their verdicts in the capability manifest so the dashboard sees a complete picture of which HTTP / container sources exist on each host. No metrics collection yet — that lands in Phase 4+ per-source. The four web-server detectors declare CategoryHTTPRequests via MetricSourceGear, so the resolver from #96 now actually picks between alternatives — set GEARBOX_AGENT_HTTP_SOURCE=nginx on a host with both HAProxy and nginx and the manifest flips. Per-source env overrides for non-default surfaces: NGINX_STATUS_URL / NGINX_CONFIG_FILE APACHE_STATUS_URL / APACHE_CONFIG_FILE CADDY_ADMIN_URL TRAEFIK_METRICS_URL DOCKER_SOCKET Also drops the dead internal/framework/discovery/ package (superseded by the ProbeableGear interface from #93; nothing imported it). The discovery/docker.go detection logic was modest — binary lookup + os.Stat + systemctl is-active — and lives on in the new docker gear with the new manifest plumbing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 2797bfb commit 35c2429

23 files changed

Lines changed: 2643 additions & 249 deletions

gearbox-agent/README.md

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -215,8 +215,8 @@ Auto-detection is the default. The override env vars below exist for the rare ed
215215
```bash
216216
# Force a specific gear as the primary for HTTP-request metrics
217217
# (request volume, response codes, response times, vhost breakdowns).
218-
# Valid values match gear identifiers in /api/v1/system/capabilities
219-
# today only "haproxy" is implemented; more land with issue #95.
218+
# Valid values match gear identifiers in /api/v1/system/capabilities:
219+
# haproxy, nginx, apache, caddy, traefik.
220220
GEARBOX_AGENT_HTTP_SOURCE=nginx
221221
```
222222

@@ -226,6 +226,26 @@ Override behaviour:
226226
- An override pointing at a gear that didn't probe Available, or doesn't produce data for the category, logs a warning at startup and **falls back to auto-detect** — locking out HTTP metrics because the override's target isn't installed on this box would be worse than serving auto-picked data.
227227
- The selected primary plus the chosen reason and the alternatives that were also available appear in `/api/v1/system/capabilities` under `primary_sources` so dashboards and humans can confirm the resolution.
228228

229+
### Source detection
230+
231+
The agent probes the host at startup for every supported source — nginx, Apache, Caddy, Traefik, Docker, plus the always-present `host` entry — and reports each one's status in the capability manifest as `available`, `not_installed`, or `inaccessible`. **Auto-detection is the default; no configuration required for the common case.**
232+
233+
The override env vars below exist for the rare cases where auto-detection misses or the operator wants to point the agent at a non-default surface:
234+
235+
| Env var | Purpose |
236+
|-----------------------|------------------------------------------------------------------|
237+
| `NGINX_STATUS_URL` | Force a specific `stub_status` URL (skips the default probe). |
238+
| `NGINX_CONFIG_FILE` | Force a specific `nginx.conf` path. |
239+
| `APACHE_STATUS_URL` | Force a specific `mod_status` URL (e.g. `?auto` variant). |
240+
| `APACHE_CONFIG_FILE` | Force a specific `httpd.conf` / `apache2.conf` path. |
241+
| `CADDY_ADMIN_URL` | Force the admin / Prometheus URL (default `:2019/metrics`). |
242+
| `TRAEFIK_METRICS_URL` | Force the Prometheus endpoint URL. |
243+
| `DOCKER_SOCKET` | Force a specific Docker socket path (e.g. rootless installs). |
244+
245+
When an override is set the agent trusts the operator and skips the synchronous detection probe for that source — a misconfigured value surfaces later when the metrics gear (Phase 4+) tries to read from it, not at startup.
246+
247+
See [docs/source-detection.md](docs/source-detection.md) for the full probe precedence flow, per-source troubleshooting recipes, and the "multiple instances of one source" limitation.
248+
229249
## Authentication
230250

231251
### API Key

gearbox-agent/cmd/gearbox-agent/main.go

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,16 +36,22 @@ import (
3636
"github.com/sarg3nt/gearbox-agent/internal/framework/config"
3737
"github.com/sarg3nt/gearbox-agent/internal/framework/crypto"
3838
"github.com/sarg3nt/gearbox-agent/internal/framework/events"
39-
"github.com/sarg3nt/gearbox-agent/internal/framework/middleware"
4039
"github.com/sarg3nt/gearbox-agent/internal/framework/gear"
40+
"github.com/sarg3nt/gearbox-agent/internal/framework/middleware"
4141
"github.com/sarg3nt/gearbox-agent/internal/framework/services/sync"
4242

4343
// Import plugins - blank identifier triggers init() registration
44+
_ "github.com/sarg3nt/gearbox-agent/internal/gears/apache"
45+
_ "github.com/sarg3nt/gearbox-agent/internal/gears/caddy"
4446
_ "github.com/sarg3nt/gearbox-agent/internal/gears/certs"
47+
_ "github.com/sarg3nt/gearbox-agent/internal/gears/docker"
4548
_ "github.com/sarg3nt/gearbox-agent/internal/gears/haproxy"
49+
_ "github.com/sarg3nt/gearbox-agent/internal/gears/host"
4650
_ "github.com/sarg3nt/gearbox-agent/internal/gears/logs"
4751
_ "github.com/sarg3nt/gearbox-agent/internal/gears/metrics"
52+
_ "github.com/sarg3nt/gearbox-agent/internal/gears/nginx"
4853
_ "github.com/sarg3nt/gearbox-agent/internal/gears/security"
54+
_ "github.com/sarg3nt/gearbox-agent/internal/gears/traefik"
4955
_ "github.com/sarg3nt/gearbox-agent/internal/gears/traffic"
5056
_ "github.com/sarg3nt/gearbox-agent/internal/gears/updates"
5157
)
@@ -384,6 +390,13 @@ func main() {
384390
HAProxyConfigPath: cfg.HAProxyConfigFile,
385391
CertbotTimer: cfg.CertbotTimer,
386392
SourceOverrides: buildSourceOverrides(cfg),
393+
NginxStatusURL: cfg.NginxStatusURL,
394+
NginxConfigFile: cfg.NginxConfigFile,
395+
ApacheStatusURL: cfg.ApacheStatusURL,
396+
ApacheConfigFile: cfg.ApacheConfigFile,
397+
CaddyAdminURL: cfg.CaddyAdminURL,
398+
TraefikMetricsURL: cfg.TraefikMetricsURL,
399+
DockerSocket: cfg.DockerSocket,
387400
}
388401

389402
// Create plugin manager
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
# Source detection
2+
3+
The gearbox-agent probes every supported "source" (HAProxy, nginx, Apache, Caddy, Traefik, Docker, plus the always-present `host` entry) at startup and reports each one's status to the dashboard via `GET /api/v1/system/capabilities`. The dashboard uses that manifest to decide which source cards to render, which gears to hide, and — once multiple HTTP producers exist on the same host — which one's data to display by default.
4+
5+
Auto-detection covers the common case with no configuration. The override env vars below exist for the rare edge cases. This doc walks the probe precedence model, the per-source troubleshooting recipes, and the limitations to be aware of.
6+
7+
## Probe precedence (per source)
8+
9+
Each detector runs the same six-step decision tree at startup:
10+
11+
```text
12+
1. Explicit override URL configured (e.g. NGINX_STATUS_URL)?
13+
├── Yes → Available, trust operator, no synchronous probe.
14+
└── No → 2
15+
16+
2. Binary on PATH (e.g. exec.LookPath("nginx"))?
17+
├── No → not_installed (the source isn't here at all)
18+
└── Yes → 3
19+
20+
3. Default status / metrics endpoint reachable?
21+
├── 200 + sentinel body match → Available
22+
├── 403 → Inaccessible (perms)
23+
├── 404 → Inaccessible (surface not configured)
24+
├── 200 without sentinel → Inaccessible (catch-all vhost)
25+
└── connection refused / timeout → Inaccessible (no listener)
26+
27+
4. Operator override via GEARBOX_AGENT_HTTP_SOURCE selects this gear?
28+
└── Same Available/fall-back logic as above (see "Metric-source
29+
overrides" in the agent README).
30+
```
31+
32+
The detector's verdict lands in the capability manifest with a free-text `reason` field aimed at operators — not just the four-state enum. If the status is `inaccessible`, the reason names the surface that was probed and the snippet that would fix it (e.g. for nginx 403, "add `allow 127.0.0.1; deny all;` to the stub_status location").
33+
34+
`Probe()` is cheap and bounded. The HTTP probe used by the web-server detectors has a 1-second timeout; binary lookups are pure `exec.LookPath`; config-path heuristics are `os.Stat` only. A misbehaving local service can't stall agent startup.
35+
36+
## Per-source troubleshooting
37+
38+
### nginx — "installed but capability shows inaccessible"
39+
40+
The probe expects `http://127.0.0.1/nginx_status` to return 200 with a body that starts with `Active connections:`. Add a stub_status location to one of your server blocks:
41+
42+
```nginx
43+
server {
44+
listen 127.0.0.1:80;
45+
server_name localhost;
46+
47+
location /nginx_status {
48+
stub_status;
49+
allow 127.0.0.1;
50+
deny all;
51+
}
52+
}
53+
```
54+
55+
If the probe gets 403, the location is there but the `allow` rule blocked us — usually because nginx is fronted by Cloudflare's `set_real_ip_from` and `127.0.0.1` is rewritten before the `allow` check runs. Either add `real_ip_header X-Real-IP;` before the `allow`, or set `NGINX_STATUS_URL` to whatever surface the agent can actually reach.
56+
57+
If you run nginx Plus or open-source 1.19+ with `--with-http_api_module`, the detector records `api_module=true` in the manifest. Phase 4's metrics gear will prefer the JSON API over `stub_status` automatically; no action needed.
58+
59+
### Apache — "installed but capability shows inaccessible"
60+
61+
The probe expects `http://127.0.0.1/server-status?auto` to return 200 with a body that starts with `Total Accesses:`. Two changes are typical:
62+
63+
1. Load `mod_status`. Debian/Ubuntu: `sudo a2enmod status`. RHEL/Fedora: it's loaded by default, check `httpd -M | grep status_module`.
64+
65+
2. Add a Location block (Debian/Ubuntu defaults already include one; RHEL doesn't):
66+
67+
```apache
68+
<Location "/server-status">
69+
SetHandler server-status
70+
Require local
71+
</Location>
72+
```
73+
74+
If the probe gets 403, the location is there but the `Require local` directive (or equivalent `Require ip 127.0.0.1`) is missing or refers to a different IP than the loopback the agent is probing from.
75+
76+
The detector tries `apache2` (Debian/Ubuntu) first, then falls back to `httpd` (RHEL/Fedora). The `binary` capability key records which one was found.
77+
78+
### Caddy — "installed but capability shows inaccessible"
79+
80+
Caddy's admin endpoint is on by default at `:2019` and exposes Prometheus at `:2019/metrics`. The probe expects 200 with a body containing `caddy_http_requests_total`.
81+
82+
The most common reason this fails: someone added `admin off` to their Caddyfile. To re-enable just the admin endpoint without the dashboard:
83+
84+
```caddyfile
85+
{
86+
admin :2019 {
87+
origins 127.0.0.1
88+
}
89+
}
90+
```
91+
92+
If you can't or won't expose the admin endpoint, set `CADDY_ADMIN_URL` to wherever your Prometheus exporter actually lives — the agent will trust the operator and skip the synchronous probe.
93+
94+
### Traefik — "installed but capability shows inaccessible"
95+
96+
Traefik's Prometheus surface is opt-in. The detector tries `:8082/metrics` first (the conventional metrics entrypoint), then falls back to `:8080/metrics` (the dashboard API entrypoint). Both have to be unreachable for the verdict to be `inaccessible`.
97+
98+
Enable Prometheus in your static config:
99+
100+
```yaml
101+
# traefik.yml
102+
metrics:
103+
prometheus:
104+
entryPoint: metrics
105+
106+
entryPoints:
107+
metrics:
108+
address: ":8082"
109+
```
110+
111+
If your metrics live somewhere non-default (a different port, an `/internal/metrics` path, behind a basic-auth middleware), set `TRAEFIK_METRICS_URL` to whatever the agent can reach.
112+
113+
### Docker — "installed but capability shows inaccessible"
114+
115+
The detector finds the docker binary on PATH but can't `os.Stat` the socket. Common causes:
116+
117+
- `dockerd` isn't running. `sudo systemctl start docker` — the `service_active` field in the manifest will go from `false` to `true` after agent restart.
118+
- Container-mode agent and the socket isn't bind-mounted. Add `-v /var/run/docker.sock:/var/run/docker.sock` to the agent's docker-compose definition.
119+
- Rootless docker. The socket is in the user's home (`~/.docker/run/docker.sock`). Set `DOCKER_SOCKET=/home/<user>/.docker/run/docker.sock`.
120+
121+
## Capability map keys (per source)
122+
123+
The detector populates the `ProbeResult.Capabilities` map with the facts it discovered. Stable keys across sources where applicable:
124+
125+
| Key | Example | Notes |
126+
|---------------------|--------------------------------------|------------------------------------------------------------------------|
127+
| `version` | `1.27.0` | Parsed from `--version` / `-v` output. |
128+
| `binary_path` | `/usr/sbin/nginx` | Result of `exec.LookPath`. |
129+
| `config_path` | `/etc/nginx/nginx.conf` | Either auto-detected or from env var. |
130+
| `status_url` | `http://127.0.0.1/nginx_status` | URL the agent successfully probed (or the configured one). |
131+
| `status_source` | `stub_status` / `mod_status` / `prometheus` | Which mechanism the agent will use to read metrics in Phase 4+. |
132+
| `override_source` | `env` | Set when an env var influenced the verdict — easier to spot at a glance. |
133+
134+
Source-specific keys exist too — `api_module` for nginx, `status_module` for Apache, `dashboard_api` for Traefik, `socket_path` and `service_active` for Docker.
135+
136+
## Conflict-resolution semantics
137+
138+
The "multiple potentially-conflicting resources on one box" scenario splits into three flavours, only one of which needs operator action:
139+
140+
1. **Multiple installed, only one running.** A box has both nginx and Apache installed but only nginx listens on port 80. nginx returns `available`, Apache returns `inaccessible` (no listener), the manifest shows both honestly. The dashboard surfaces nginx's metrics; the Apache card is greyed out. **No operator action needed.**
141+
142+
2. **Multiple actively running.** A box has nginx on `:80` and Apache on `:8080`, both happily serving. Both probe `available`, both surface in the manifest, the agent's primary-source resolver picks one as primary for HTTP-request metrics using its built-in preference order (HAProxy > nginx > Apache > Caddy > Traefik). The dashboard renders the primary's data with a "switch source" affordance for the alternatives. **No operator action needed** unless the auto-pick is wrong for this host.
143+
144+
3. **Auto-pick wrong; operator wants to force the choice.** This is what `GEARBOX_AGENT_HTTP_SOURCE` is for. Set it to the gear name (e.g. `nginx`) and the resolver hands the primary slot to that gear instead. Manifest reports `primary_sources.http_requests.reason: "operator override via GEARBOX_AGENT_HTTP_SOURCE"` so dashboards can confirm the override took effect at a glance. If the named gear isn't actually available on this host, the agent logs a warning and falls back to auto-detect — losing HTTP metrics because an override target isn't installed would be worse than serving auto-picked data.
145+
146+
> [!NOTE]
147+
> The override is **per metric category**, not per source. Today only `CategoryHTTPRequests` exists; more land as future metric categories are added (e.g. `container_metrics` when Docker and Podman both gain metrics support). See the agent's `MetricCategory` enum for the current set.
148+
149+
## Out-of-scope limitations
150+
151+
- **Multiple instances of the same source.** Two nginx instances on one box, each with its own config and listen address, can't both be represented in the manifest — the agent treats each source as singular. If you genuinely need this, run two agents (one per instance, each pointed at its own `NGINX_STATUS_URL`). Tracked as a deferred design question on issue #95.
152+
- **TLS verification on non-loopback probes.** The default probe URLs are all loopback (`127.0.0.1`), where self-signed certs are normal. The probe helper disables TLS verification for `127.0.0.1` / `[::1]` / `localhost` URLs only; setting an override URL that points at a public hostname will use full verification, which is correct but means you need a valid cert there.
153+
154+
## Related docs
155+
156+
- [`gear-probes.md`](gear-probes.md) — the probe lifecycle and `ProbeableGear` interface this detection layer builds on.
157+
- Project root [`CLAUDE.md`](../../gearbox/CLAUDE.md) — broader gearbox architecture and the dashboard side.
158+
- Issue [#95](https://github.com/sarg3nt/gearbox/issues/95) — phase-3 design discussion and PR breakdown.
159+
- Issue [#91](https://github.com/sarg3nt/gearbox/issues/91) — parent roadmap for the source-agnostic Metrics gear (Phases 0–8).

gearbox-agent/internal/framework/config/config.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,26 @@ type Config struct {
8181
// preference picks wrong. See [docs/source-detection.md] /
8282
// issue #95.
8383
HTTPSource string // GEARBOX_AGENT_HTTP_SOURCE — primary for CategoryHTTPRequests
84+
85+
// Per-source detection overrides. Each one short-circuits the
86+
// detector's default well-known-paths/loopback-URL probe and trusts
87+
// the operator-supplied surface instead. The agent does not probe
88+
// these synchronously at startup — a misconfigured value will
89+
// surface later when the metrics phase tries to read from it.
90+
//
91+
// Empty (the default) means "auto-detect" — the corresponding
92+
// gear's Probe() walks its well-known paths and default loopback
93+
// URL. Operators on hosts where the binary lives in a non-standard
94+
// place, or whose status endpoint lives on a non-default address,
95+
// reach for these as the escape hatch. See [docs/source-detection.md]
96+
// / issue #95.
97+
NginxStatusURL string // NGINX_STATUS_URL — force a specific stub_status URL
98+
NginxConfigFile string // NGINX_CONFIG_FILE — force a specific nginx.conf path
99+
ApacheStatusURL string // APACHE_STATUS_URL — force a specific mod_status URL
100+
ApacheConfigFile string // APACHE_CONFIG_FILE — force a specific httpd.conf path
101+
CaddyAdminURL string // CADDY_ADMIN_URL — force the admin/Prometheus URL
102+
TraefikMetricsURL string // TRAEFIK_METRICS_URL — force the Prometheus endpoint URL
103+
DockerSocket string // DOCKER_SOCKET — force a specific docker socket path
84104
}
85105

86106
// DefaultConfig returns the default configuration.
@@ -180,6 +200,18 @@ func Load() (*Config, error) {
180200
// 'haproxy' both match the gear's Info().Name. Empty = auto-detect.
181201
cfg.HTTPSource = normaliseSourceOverride(os.Getenv("GEARBOX_AGENT_HTTP_SOURCE"))
182202

203+
// Per-source detection overrides. Unprefixed env vars to match the
204+
// existing HAPROXY_STATS_URL style — these belong to the subject,
205+
// not the agent. Trimmed but not lowercased (URLs and paths are
206+
// case-sensitive on most filesystems).
207+
cfg.NginxStatusURL = strings.TrimSpace(os.Getenv("NGINX_STATUS_URL"))
208+
cfg.NginxConfigFile = strings.TrimSpace(os.Getenv("NGINX_CONFIG_FILE"))
209+
cfg.ApacheStatusURL = strings.TrimSpace(os.Getenv("APACHE_STATUS_URL"))
210+
cfg.ApacheConfigFile = strings.TrimSpace(os.Getenv("APACHE_CONFIG_FILE"))
211+
cfg.CaddyAdminURL = strings.TrimSpace(os.Getenv("CADDY_ADMIN_URL"))
212+
cfg.TraefikMetricsURL = strings.TrimSpace(os.Getenv("TRAEFIK_METRICS_URL"))
213+
cfg.DockerSocket = strings.TrimSpace(os.Getenv("DOCKER_SOCKET"))
214+
183215
return cfg, nil
184216
}
185217

0 commit comments

Comments
 (0)