Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .air.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ testdata_dir = "testdata"
tmp_dir = "tmp"

[build]
entrypoint = ["out/wasgehtd", "-log-level=debug"]
entrypoint = ["out/wasgehtd"]
cmd = "make build"
delay = 1000
exclude_dir = ["data"]
Expand Down
50 changes: 46 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@
- **Extensible Check System**: Modular check types via a Registry/Factory pattern. Each check type implements a common `Check` interface and declares its own metrics through a `Descriptor`.
- **Built-in Check Types**:
- **ping**: ICMP echo requests for host availability and latency.
- **http**: HTTP/HTTPS endpoint reachability and per-URL response time.
- **wifi_stations**: Scrapes a Prometheus metrics endpoint for connected WiFi client counts per radio interface.
- **Multi-Metric Checks**: Checks can produce multiple metrics stored as separate data sources in a single RRD file. Multi-metric checks render as stacked area graphs.
- **Multi-Metric Checks**: Checks can produce multiple metrics stored as separate data sources in a single RRD file. Multi-metric checks render as stacked area graphs or colored line graphs depending on the check type.
- **Host Status Aggregation**: Each host has an aggregate status (`up`, `down`, `degraded`, `unknown`) computed from all its checks. A check must be alive and have reported within the last 5 minutes to count as healthy.
- **RRD Storage**: Uses Round Robin Databases for time-series data, with configurable archives from 1-minute resolution (1 week) to 8-hour resolution (5 years).
- **Graph Generation**: Generates historical graphs at multiple time scales (15 minutes through 5 years) for each check type on each host.
Expand Down Expand Up @@ -115,7 +116,10 @@ Hosts are defined in a JSON file. Each host can specify an address and a set of
"google": {
"address": "8.8.8.8",
"checks": {
"ping": { "timeout": "5s" }
"ping": { "timeout": "5s" },
"http": {
"urls": ["https://www.google.com"]
}
}
},
"ap1": {
Expand All @@ -126,6 +130,19 @@ Hosts are defined in a JSON file. Each host can specify an address and a set of
}
}
},
"qube": {
"checks": {
"ping": {},
"http": {
"urls": [
"http://qube.example.com:2018/sign.json",
"https://whatsup.example.com",
"http://mrtg.example.com"
],
"timeout": "15s"
}
}
},
"disabled-example": {
"checks": {
"ping": { "enabled": false }
Expand All @@ -146,6 +163,19 @@ Sends ICMP echo requests to check host availability and measure latency.
| `count` | number | `1` | Number of ping packets to send |
| `enabled` | bool | `true` | Set to `false` to disable |

#### http

Performs HTTP GET requests to a list of URLs and reports per-URL response time. Each URL becomes a separate data source in the RRD, rendered as colored lines on the graph. The check succeeds only if all configured URLs return a response (any HTTP status code counts as reachable). Redirects are not followed.

TLS certificate verification is skipped by default to support locally signed certificates.

| Option | Type | Default | Description |
| ------------- | -------- | ------------ | ---------------------------------- |
| `urls` | []string | _(required)_ | List of full URLs to check |
| `timeout` | string | `"10s"` | HTTP request timeout (Go duration) |
| `skip_verify` | bool | `true` | Skip TLS certificate verification |
| `enabled` | bool | `true` | Set to `false` to disable |

#### wifi_stations

Scrapes a Prometheus metrics endpoint for `wifi_stations{ifname="..."}` gauge values, reporting connected client counts per radio interface. Each configured radio becomes a separate data source in the RRD, rendered as a stacked area graph.
Expand Down Expand Up @@ -195,6 +225,13 @@ Returns JSON with the status of all hosts:
"latency_us": 12345
},
"lastupdate": 1700000000
},
"http": {
"alive": true,
"metrics": {
"https://www.google.com": 45230
},
"lastupdate": 1700000000
}
}
},
Expand Down Expand Up @@ -248,7 +285,8 @@ data/
│ ├── router/
│ │ └── ping.rrd
│ ├── google/
│ │ └── ping.rrd
│ │ ├── ping.rrd
│ │ └── http.rrd
│ ├── ap1/
│ │ ├── ping.rrd
│ │ └── wifi_stations.rrd
Expand All @@ -259,6 +297,10 @@ data/
│ ├── router_ping_15m.png
│ ├── router_ping_1h.png
│ └── ...
├── google/
│ ├── google_ping_15m.png
│ ├── google_http_15m.png
│ └── ...
├── ap1/
│ ├── ap1_ping_15m.png
│ ├── ap1_ping_1h.png
Expand All @@ -268,7 +310,7 @@ data/
└── ...
```

Each check type gets its own RRD file (e.g., `ping.rrd`, `wifi_stations.rrd`). Multi-metric checks store all their data sources in a single RRD file.
Each check type gets its own RRD file (e.g., `ping.rrd`, `http.rrd`, `wifi_stations.rrd`). Multi-metric checks store all their data sources in a single RRD file.

## Makefile Targets

Expand Down
25 changes: 25 additions & 0 deletions pkg/check/descriptor.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
package check

// Graph style constants for Descriptor.GraphStyle.
const (
// GraphStyleStack renders multi-metric graphs as stacked areas
// (AREA for first metric, STACK for subsequent). This is the default
// when GraphStyle is empty.
GraphStyleStack = "stack"

// GraphStyleLine renders multi-metric graphs as colored lines
// (LINE2 for each metric). Useful for check types where metrics
// are independent measurements (e.g. per-URL response times)
// rather than parts of a whole.
GraphStyleLine = "line"
)

// MetricDef describes a single metric produced by a check type.
type MetricDef struct {
// ResultKey is the key used in Result.Metrics (e.g. "latency_us").
Expand All @@ -26,6 +40,17 @@ type MetricDef struct {
// via Check.Describe(), allowing config-dependent metric shapes (e.g.
// a wifi_stations check with a variable number of radios).
type Descriptor struct {
// GraphStyle controls how multi-metric graphs are rendered.
// Empty or "stack" means stacked areas (default). "line" means
// colored lines. Single-metric checks ignore this field.
GraphStyle string

// Label is a human-readable label used for graph titles and the
// vertical axis. If empty, the first metric's Label is used.
// Useful when individual metric labels are long or not suitable
// for titles (e.g. full URLs).
Label string

// Metrics lists the metrics this check instance produces.
Metrics []MetricDef
}
42 changes: 42 additions & 0 deletions pkg/check/descriptor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ func TestDescriptor_ZeroValue(t *testing.T) {
if d.Metrics != nil {
t.Error("zero Descriptor should have nil Metrics")
}
if d.GraphStyle != "" {
t.Error("zero Descriptor should have empty GraphStyle")
}
}

func TestDescriptor_WithMetrics(t *testing.T) {
Expand Down Expand Up @@ -45,3 +48,42 @@ func TestDescriptor_ScaleOneMeansNoScaling(t *testing.T) {
t.Errorf("expected Scale 1, got %d", d.Scale)
}
}

func TestDescriptor_GraphStyleStack(t *testing.T) {
d := Descriptor{
GraphStyle: GraphStyleStack,
Metrics: []MetricDef{{ResultKey: "a", DSName: "a", Label: "a", Unit: "x"}},
}
if d.GraphStyle != "stack" {
t.Errorf("expected GraphStyle 'stack', got %q", d.GraphStyle)
}
}

func TestDescriptor_GraphStyleLine(t *testing.T) {
d := Descriptor{
GraphStyle: GraphStyleLine,
Metrics: []MetricDef{{ResultKey: "a", DSName: "a", Label: "a", Unit: "x"}},
}
if d.GraphStyle != "line" {
t.Errorf("expected GraphStyle 'line', got %q", d.GraphStyle)
}
}

func TestDescriptor_EmptyGraphStyleDefaultsToStack(t *testing.T) {
// Empty string should be treated as stack by the graph layer
d := Descriptor{
Metrics: []MetricDef{{ResultKey: "a", DSName: "a", Label: "a", Unit: "x"}},
}
if d.GraphStyle != "" {
t.Errorf("expected empty GraphStyle, got %q", d.GraphStyle)
}
}

func TestGraphStyleConstants(t *testing.T) {
if GraphStyleStack != "stack" {
t.Errorf("expected GraphStyleStack='stack', got %q", GraphStyleStack)
}
if GraphStyleLine != "line" {
t.Errorf("expected GraphStyleLine='line', got %q", GraphStyleLine)
}
}
Loading