{"id":29013115,"url":"https://github.com/kylerisse/wasgeht","last_synced_at":"2026-03-01T01:05:59.915Z","repository":{"id":265002799,"uuid":"892946433","full_name":"kylerisse/wasgeht","owner":"kylerisse","description":"Simple host based monitoring","archived":false,"fork":false,"pushed_at":"2026-02-22T20:55:51.000Z","size":210,"stargazers_count":2,"open_issues_count":0,"forks_count":2,"subscribers_count":1,"default_branch":"master","last_synced_at":"2026-02-23T01:07:06.464Z","etag":null,"topics":["golang","monitoring","nix-flake","rrdtool"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":false,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/kylerisse.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2024-11-23T05:43:23.000Z","updated_at":"2026-02-22T20:55:54.000Z","dependencies_parsed_at":"2025-02-27T20:08:35.871Z","dependency_job_id":"e358eb06-4d1e-49d9-9e22-315022739406","html_url":"https://github.com/kylerisse/wasgeht","commit_stats":null,"previous_names":["kylerisse/wasgeht"],"tags_count":5,"template":false,"template_full_name":null,"purl":"pkg:github/kylerisse/wasgeht","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kylerisse%2Fwasgeht","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kylerisse%2Fwasgeht/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kylerisse%2Fwasgeht/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kylerisse%2Fwasgeht/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/kylerisse","download_url":"https://codeload.github.com/kylerisse/wasgeht/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kylerisse%2Fwasgeht/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29957128,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-28T22:53:01.873Z","status":"ssl_error","status_checked_at":"2026-02-28T22:52:50.699Z","response_time":90,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["golang","monitoring","nix-flake","rrdtool"],"created_at":"2025-06-25T19:08:20.908Z","updated_at":"2026-03-01T01:05:59.889Z","avatar_url":"https://github.com/kylerisse.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Was Geht\n\n[![CI](https://github.com/kylerisse/wasgeht/actions/workflows/ci.yml/badge.svg)](https://github.com/kylerisse/wasgeht/actions/workflows/ci.yml)\n\n## Overview\n\n**Was Geht** is a small Go application that monitors a list of hosts at regular intervals, tracks their availability and metrics, and records the data in Round Robin Databases (RRD). A lightweight web interface serves host status information and interactive graphs of the recorded metrics.\n\n## Features\n\n- **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`.\n- **Built-in Check Types**:\n  - **ping**: ICMP echo requests for host availability and latency.\n  - **http**: HTTP/HTTPS endpoint reachability and per-URL response time.\n  - **dns**: DNS query validation against a specific server with expected-answer verification. Supports A, AAAA, and PTR records.\n  - **wifi_stations**: Scrapes a Prometheus metrics endpoint for connected WiFi client counts per radio interface.\n- **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.\n- **Host Status Aggregation**: Each host has an aggregate status (`up`, `down`, `degraded`, `stale`, `pending`, `unconfigured`) computed from all its checks. A check must be alive and have reported within the last 5 minutes to count as healthy.\n- **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).\n- **Graph Generation**: Generates historical graphs at multiple time scales (15 minutes through 5 years) for each check type on each host.\n- **Simple Web Interface**: Serves an HTML/JS front-end to display host status and dynamically loaded graphs. Available in table and flame graph formats.\n- **REST API**: Exposes JSON endpoints for all hosts (`GET /api`), individual hosts (`GET /api/hosts/{hostname}`), and status summaries (`GET /api/summary`). Supports hostname, tag, and status filtering.\n- **Prometheus Support**: Exposes metrics in Prometheus format at `GET /metrics`.\n\n## Requirements\n\n### Using Nix (Recommended)\n\nIf you have **direnv** installed, follow the instructions when entering this directory.\n\nIf you have **Nix** installed, you can simply enter a development shell with all required dependencies using:\n\n```bash\nnix develop\n```\n\nYou will need experimental features `flakes` and `nix-command`.\n\nThis loads the environment specified in `flake.nix`:\n\n- Go (for building),\n- gnumake (for Makefile),\n- air (for live reload during development),\n- rrdtool (for handling RRD databases),\n- unixtools.ping (ping utility).\n\nOnce inside the shell, you can run the usual make commands\n\n### Without Nix\n\nEnsure the following are installed:\n\n- **Go** (1.25+ recommended)\n- **air** (for live reload during development, optional)\n- **rrdtool** and **unixtools ping** must be installed and available on the system path.\n- Basic Unix tools for building and running (`make`, etc.).\n\n## Quick Start\n\n1. **Clone** the repository:\n\n   ```bash\n   git clone https://github.com/kylerisse/wasgeht.git\n   cd wasgeht\n   ```\n\n2. **Install dependencies**:\n\n   ```bash\n   make deps\n   ```\n\n3. **Build** the binary:\n\n   ```bash\n   make build\n   ```\n\n   This will compile the Go code and produce a `wasgehtd` binary in the `out/` directory.\n\n4. **Prepare data directories**:\n\n   By default, Was Geht expects two subdirectories under `./data`:\n   - `rrds` for storing RRD files\n   - `graphs` for storing generated graph images\n\n   These directories will be created automatically if they do not exist, but make sure that `./data` itself exists.\n\n5. **Configure hosts**:\n\n   Update or create your own JSON file listing your hosts (see `sample-hosts.json` for reference).\n\n6. **Run** the application:\n\n   ```bash\n   ./out/wasgehtd --host-file=sample-hosts.json --data-dir=./data --port=1982 --log-level=info\n   ```\n\n7. **Access the web interface**:\n\n   Open your browser to [http://localhost:1982](http://localhost:1982). The main table shows all hosts and their current status (UP or DOWN). Hover over the status to see a latency graph.\n\n## Configuration\n\n- **Host File** (`--host-file`): Path to the JSON file specifying host definitions.\n- **Data Directory** (`--data-dir`): Root directory that contains `rrds/` and `graphs/`.\n- **Port** (`--port`): Port on which the API and front-end are served.\n- **Logging Level** (`--log-level`): Set the verbosity of logs (e.g., `debug`, `info`, `warn`, `error`, `fatal`, `panic`).\n\n### Host Configuration\n\nHosts are defined in a JSON file. Each host can specify optional `tags` for metadata and a `checks` block defining which check types to run. Hosts without a `checks` block will have `unconfigured` status.\n\n```json\n{\n\t\"router\": {\n\t\t\"tags\": { \"category\": \"router\" },\n\t\t\"checks\": {\n\t\t\t\"ping\": {\n\t\t\t\t\"addresses\": [\"router.example.com\"]\n\t\t\t}\n\t\t}\n\t},\n\t\"google\": {\n\t\t\"checks\": {\n\t\t\t\"ping\": {\n\t\t\t\t\"addresses\": [\"8.8.8.8\", \"8.8.4.4\"],\n\t\t\t\t\"timeout\": \"5s\"\n\t\t\t},\n\t\t\t\"http\": {\n\t\t\t\t\"urls\": [\"https://www.google.com\"]\n\t\t\t}\n\t\t}\n\t},\n\t\"ap1\": {\n\t\t\"tags\": { \"category\": \"ap\", \"building\": \"expo\" },\n\t\t\"checks\": {\n\t\t\t\"ping\": {\n\t\t\t\t\"addresses\": [\"ap1.example.com\"]\n\t\t\t},\n\t\t\t\"wifi_stations\": {\n\t\t\t\t\"address\": \"ap1.example.com\",\n\t\t\t\t\"radios\": [\"phy0-ap0\", \"phy1-ap0\"]\n\t\t\t}\n\t\t}\n\t},\n\t\"qube\": {\n\t\t\"tags\": { \"category\": \"server\" },\n\t\t\"checks\": {\n\t\t\t\"ping\": {\n\t\t\t\t\"addresses\": [\"qube.example.com\"]\n\t\t\t},\n\t\t\t\"http\": {\n\t\t\t\t\"urls\": [\n\t\t\t\t\t\"http://qube.example.com:2018/sign.json\",\n\t\t\t\t\t\"https://whatsup.example.com\",\n\t\t\t\t\t\"http://mrtg.example.com\"\n\t\t\t\t],\n\t\t\t\t\"timeout\": \"15s\",\n\t\t\t\t\"skip_verify\": true\n\t\t\t}\n\t\t}\n\t},\n\t\"unconfigured-host\": {},\n\t\"disabled-example\": {\n\t\t\"checks\": {\n\t\t\t\"ping\": { \"enabled\": false }\n\t\t}\n\t}\n}\n```\n\n### Check Types\n\n#### ping\n\nSends ICMP echo requests to check host availability and measure latency.\n\n| Option      | Type     | Default      | Description                         |\n| ----------- | -------- | ------------ | ----------------------------------- |\n| `addresses` | []string | _(required)_ | List of IPs or hostnames to ping    |\n| `timeout`   | string   | `\"3s\"`       | Ping timeout (Go duration)          |\n| `count`     | number   | `1`          | Number of ping packets to send      |\n| `enabled`   | bool     | `true`       | Set to `false` to disable           |\n\n#### http\n\nPerforms 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.\n\nSet `skip_verify` to `true` to support locally signed certificates.\n\n| Option        | Type     | Default      | Description                        |\n| ------------- | -------- | ------------ | ---------------------------------- |\n| `urls`        | []string | _(required)_ | List of full URLs to check         |\n| `timeout`     | string   | `\"10s\"`      | HTTP request timeout (Go duration) |\n| `skip_verify` | bool     | `false`      | Skip TLS certificate verification  |\n| `enabled`     | bool     | `true`       | Set to `false` to disable          |\n\n#### dns\n\nSends DNS queries to a specific server and validates each answer against an expected value. Supports A, AAAA, and PTR record types. Each query produces a separate data source in the RRD, rendered as colored lines on the graph. The check succeeds only if all configured queries resolve and every answer matches its expected value.\n\nPTR query names must be provided in reverse notation (e.g. `1.168.168.192.in-addr.arpa`). Expected PTR values may include or omit the trailing dot — both forms are accepted.\n\n| Option    | Type           | Default      | Description                                          |\n| --------- | -------------- | ------------ | ---------------------------------------------------- |\n| `server`  | string         | _(required)_ | DNS server to query, as `host:port`                  |\n| `queries` | list of objects | _(required)_ | One or more query definitions (see below)            |\n| `timeout` | string         | `\"3s\"`       | Per-query timeout (Go duration)                      |\n\nEach entry in `queries` requires:\n\n| Field    | Type   | Description                                                                 |\n| -------- | ------ | --------------------------------------------------------------------------- |\n| `name`   | string | DNS name to query (e.g. `router.example.com`, `1.0.168.192.in-addr.arpa`)  |\n| `type`   | string | Record type: `A`, `AAAA`, or `PTR` (case-insensitive)                       |\n| `expect` | string | Expected value in the answer (IP address for A/AAAA, hostname for PTR)      |\n\nExample — testing an internal resolver with forward and reverse lookups:\n\n```json\n\"dns\": {\n    \"server\": \"router.example.com:53\",\n    \"timeout\": \"5s\",\n    \"queries\": [\n        { \"name\": \"router.example.com\",         \"type\": \"A\",   \"expect\": \"192.168.1.1\" },\n        { \"name\": \"1.1.168.192.in-addr.arpa\",   \"type\": \"PTR\", \"expect\": \"router.example.com.\" },\n        { \"name\": \"k.root-servers.net\",         \"type\": \"A\",   \"expect\": \"193.0.14.129\" },\n        { \"name\": \"129.14.0.193.in-addr.arpa\",  \"type\": \"PTR\", \"expect\": \"k.root-servers.net.\" }\n    ]\n}\n```\n\n#### wifi_stations\n\nScrapes 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.\n\n| Option    | Type     | Default      | Description                                              |\n| --------- | -------- | ------------ | -------------------------------------------------------- |\n| `address` | string   | _(required)_ | Hostname or IP of the target (scraped at port 9100)      |\n| `radios`  | []string | _(required)_ | List of `ifname` label values to monitor                 |\n| `timeout` | string   | `\"5s\"`       | HTTP scrape timeout (Go duration)                        |\n| `enabled` | bool     | `true`       | Set to `false` to disable                                |\n\nThe target host expects a Prometheus node exporter (or compatible) exposing metrics like:\n\n```\nwifi_stations{ifname=\"phy0-ap0\"} 3\nwifi_stations{ifname=\"phy1-ap0\"} 7\n```\n\n## Host Status\n\nEach host has an aggregate status derived from all its enabled checks:\n\n| Status           | Color  | Meaning                                                                |\n| ---------------- | ------ | ---------------------------------------------------------------------- |\n| **up**           | Green  | All checks are alive and reported within the last 5 minutes.           |\n| **degraded**     | Yellow | Some checks are healthy, others are down, stale, or pending.           |\n| **down**         | Red    | All checks have fresh results and all are down.                        |\n| **stale**        | Gray   | All checks have run before but all results are older than 5 minutes.   |\n| **pending**      | Gray   | Checks are defined but none have run yet.                              |\n| **unconfigured** | Gray   | No checks defined for the host.                                        |\n\nA check result is considered **stale** if its last successful RRD update is older than 5 minutes.\n\n## API\n\nAll API endpoints return JSON with `Content-Type: application/json`.\n\n### Filtering\n\nThe `/api` and `/api/summary` endpoints support query parameter filters:\n\n- **`?hostname=value`** — Filter to specific hostnames. Multiple `hostname` params are ORed together. Non-matching hostnames return an empty result (no 404).\n- **`?tag=key:value`** — Filter hosts by tag. Multiple `tag` params are ANDed together.\n- **`?status=value`** — Filter hosts by status. Multiple `status` params are ORed together. Valid values: `up`, `down`, `degraded`, `stale`, `pending`, `unconfigured`.\n\n### `GET /api`\n\nReturns all hosts wrapped in an envelope:\n\n```json\n{\n\t\"generated_at\": 1700000000,\n\t\"hosts\": {\n\t\t\"google\": {\n\t\t\t\"status\": \"up\",\n\t\t\t\"checks\": {\n\t\t\t\t\"ping\": {\n\t\t\t\t\t\"alive\": true,\n\t\t\t\t\t\"metrics\": {\n\t\t\t\t\t\t\"8.8.8.8\": 12345,\n\t\t\t\t\t\t\"8.8.4.4\": 11200\n\t\t\t\t\t},\n\t\t\t\t\t\"lastupdate\": 1700000000\n\t\t\t\t},\n\t\t\t\t\"http\": {\n\t\t\t\t\t\"alive\": true,\n\t\t\t\t\t\"metrics\": {\n\t\t\t\t\t\t\"https://www.google.com\": 45230\n\t\t\t\t\t},\n\t\t\t\t\t\"lastupdate\": 1700000000\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t\t\"ap1\": {\n\t\t\t\"status\": \"up\",\n\t\t\t\"tags\": { \"category\": \"ap\", \"building\": \"expo\" },\n\t\t\t\"checks\": {\n\t\t\t\t\"ping\": {\n\t\t\t\t\t\"alive\": true,\n\t\t\t\t\t\"metrics\": {\n\t\t\t\t\t\t\"ap1.example.com\": 237\n\t\t\t\t\t},\n\t\t\t\t\t\"lastupdate\": 1700000000\n\t\t\t\t},\n\t\t\t\t\"wifi_stations\": {\n\t\t\t\t\t\"alive\": true,\n\t\t\t\t\t\"metrics\": {\n\t\t\t\t\t\t\"phy0-ap0\": 3,\n\t\t\t\t\t\t\"phy1-ap0\": 7,\n\t\t\t\t\t\t\"total\": 10\n\t\t\t\t\t},\n\t\t\t\t\t\"lastupdate\": 1700000000\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t\t\"unconfigured-host\": {\n\t\t\t\"status\": \"unconfigured\",\n\t\t\t\"checks\": {}\n\t\t}\n\t}\n}\n```\n\nThe `status` field is one of `up`, `down`, `degraded`, `stale`, `pending`, or `unconfigured` (see [Host Status](#host-status) above). The `tags` field is omitted when empty.\n\n### `GET /api/hosts/{hostname}`\n\nReturns a single host (bare response, no envelope). Returns 404 if the hostname is not found.\n\n```json\n{\n\t\"status\": \"up\",\n\t\"tags\": { \"category\": \"ap\", \"building\": \"expo\" },\n\t\"checks\": {\n\t\t\"ping\": {\n\t\t\t\"alive\": true,\n\t\t\t\"metrics\": {\n\t\t\t\t\"ap1.example.com\": 237\n\t\t\t},\n\t\t\t\"lastupdate\": 1700000000\n\t\t}\n\t}\n}\n```\n\n### `GET /api/summary`\n\nReturns host counts grouped by status. Supports the same `?hostname=`, `?tag=`, and `?status=` filters.\n\n```json\n{\n\t\"generated_at\": 1700000000,\n\t\"total\": 10,\n\t\"by_status\": {\n\t\t\"up\": 6,\n\t\t\"down\": 1,\n\t\t\"degraded\": 1,\n\t\t\"stale\": 0,\n\t\t\"pending\": 1,\n\t\t\"unconfigured\": 1\n\t}\n}\n```\n\n### `GET /metrics`\n\nExposes Prometheus-formatted metrics:\n\n```\ncheck_alive{host=\"google\", check=\"ping\"} 1\ncheck_metric{host=\"google\", check=\"ping\", metric=\"8.8.8.8\"} 12345\ncheck_alive{host=\"ap1\", check=\"ping\"} 1\ncheck_metric{host=\"ap1\", check=\"ping\", metric=\"ap1.example.com\"} 237\n```\n\n## Data Directory Layout\n\nRRD files and graph images are organized into per-host subdirectories:\n\n```\ndata/\n├── rrds/\n│   ├── router/\n│   │   ├── ping.rrd\n│   │   └── dns.rrd\n│   ├── google/\n│   │   ├── ping.rrd\n│   │   └── http.rrd\n│   ├── ap1/\n│   │   ├── ping.rrd\n│   │   └── wifi_stations.rrd\n│   └── ...\n└── graphs/\n    └── imgs/\n        ├── router/\n        │   ├── router_ping_15m.png\n        │   ├── router_ping_1h.png\n        │   ├── router_dns_15m.png\n        │   ├── router_dns_1h.png\n        │   └── ...\n        ├── google/\n        │   ├── google_ping_15m.png\n        │   ├── google_http_15m.png\n        │   └── ...\n        ├── ap1/\n        │   ├── ap1_ping_15m.png\n        │   ├── ap1_ping_1h.png\n        │   ├── ap1_wifi_stations_15m.png\n        │   ├── ap1_wifi_stations_1h.png\n        │   └── ...\n        └── ...\n```\n\nEach 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.\n\n## Makefile Targets\n\n- **test**: Runs staticcheck and `go test` with race detection.\n- **build**: Compiles the Go code and produces `out/wasgehtd`.\n- **deps**: Verifies module dependencies and updates `go.mod` and `go.sum`.\n- **clean**: Removes build output and generated graphs.\n- **mrproper**: Removes all data including RRD files and generated graphs.\n\n## Contributing\n\n1. Fork the repository.\n2. Create a new feature or bugfix branch.\n3. Send a pull request (PR).\n\n## License\n\nMIT License\n\nCopyright (c) 2026 Kyle Risse\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkylerisse%2Fwasgeht","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fkylerisse%2Fwasgeht","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkylerisse%2Fwasgeht/lists"}