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
15 changes: 15 additions & 0 deletions .github/ISSUE_TEMPLATE/bug_report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
name: Bug report
about: Something does not work as documented
labels: bug
---

**Version** (`monitor --version` or image tag):

**What happened:**

**What you expected:**

**Steps to reproduce** (API calls, check config; remove secrets):

**Logs / output:**
5 changes: 5 additions & 0 deletions .github/ISSUE_TEMPLATE/config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: Questions and ideas
url: https://github.com/plusclouds/monitoring.server/discussions
about: Ask questions and share setups in Discussions.
11 changes: 11 additions & 0 deletions .github/ISSUE_TEMPLATE/feature_request.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
name: Feature request
about: Suggest an improvement
labels: enhancement
---

**Problem you want to solve:**

**Proposed solution:**

**Alternatives considered:**
11 changes: 11 additions & 0 deletions .github/ISSUE_TEMPLATE/plugin_request.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
name: Plugin request
about: Ask for support for a protocol or device type
labels: plugin
---

**Device or protocol (vendor, model, MIB/API docs link):**

**What should be collected:**

**Can you test it against real hardware?**
6 changes: 6 additions & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
## What and why

## Checklist
- [ ] `make check` passes
- [ ] Tests added or updated
- [ ] Docs and CHANGELOG updated if user-visible
17 changes: 16 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ env:
# Docker stores credentials per host, so login, image tags and push must all use this same name.
REGISTRY: registry.plusclouds.com
IMAGE: monitoring
IMAGE_GRAFANA: monitoring-grafana

jobs:
lint:
Expand Down Expand Up @@ -97,6 +98,7 @@ jobs:
version="${GITHUB_REF_NAME#v}"
echo "version=$version" >> "$GITHUB_OUTPUT"
echo "tags=-t $REGISTRY/$IMAGE:$version -t $REGISTRY/$IMAGE:latest" >> "$GITHUB_OUTPUT"
echo "grafana_tags=-t $REGISTRY/$IMAGE_GRAFANA:$version -t $REGISTRY/$IMAGE_GRAFANA:latest" >> "$GITHUB_OUTPUT"

# Building needs no credentials (public base images), so it uses its own
# Docker config: ~/.docker/buildx on the shared runner is root-owned
Expand All @@ -115,16 +117,29 @@ jobs:
- name: Check the image runs
run: docker run --rm "$REGISTRY/$IMAGE:${{ steps.tags.outputs.version }}" version

# Grafana with the dashboards baked in (deploy/grafana/Dockerfile).
- name: Build Grafana image
env:
DOCKER_CONFIG: ${{ runner.temp }}/docker-build
run: |
docker build --pull \
--build-arg VERSION=${{ github.ref_name }} \
--label org.opencontainers.image.revision=${{ github.sha }} \
${{ steps.tags.outputs.grafana_tags }} deploy/grafana

# Uses the registry login stored on the self-hosted runner. If a push
# fails with "no basic auth credentials", another job logged the runner
# out (see plusclouds.api.v4 commit 8105c9dc): log it in again.
- name: Push
run: docker image push --all-tags "$REGISTRY/$IMAGE"
run: |
docker image push --all-tags "$REGISTRY/$IMAGE"
docker image push --all-tags "$REGISTRY/$IMAGE_GRAFANA"

# Remove only this build's images: a full prune would also delete
# other repositories' images and build cache on the shared runner.
- name: Cleanup
if: always()
run: |
docker image ls --format '{{.Repository}}:{{.Tag}}' "$REGISTRY/$IMAGE" | xargs -r docker image rm -f
docker image ls --format '{{.Repository}}:{{.Tag}}' "$REGISTRY/$IMAGE_GRAFANA" | xargs -r docker image rm -f
docker image prune -f
55 changes: 55 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Changelog

Notable changes per release. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions are git tags. The project is pre-1.0: the API can change between minor versions. Per-milestone detail is in [docs/progress.md](docs/progress.md).

## [0.14.1] - 2026-10-12
### Fixed
- Lint failure (unnecessary conversion) in device notifications.

## [0.14.0] - 2026-10-12
### Added
- Notification catalog and per-device notifications.

## [0.13.x] - 2026-10-10 to 2026-10-11
### Added
- `box.agent` heartbeats (0.13.0), every 5 s in every tenant (0.13.1, 0.13.3).
### Fixed
- Whoopsy relearn behavior (0.13.2).

## [0.12.x] - 2026-10-07
### Added
- PlusClouds VM agent telemetry (`vm.agent`) and its weight in usage metering.

## [0.11.0] - 2026-10-07
### Added
- Self-monitoring (`monitor.self`) and heartbeat.

## [0.10.0] - 2026-10-07
### Added
- Embedded MQTT broker and `push.mqtt` ([ADR-0013](docs/adr/0013-embedded-mqtt-broker.md)).

## [0.9.0] - 2026-10-07
### Added
- HTTP push ingest (`push.http`).

## [0.4.0 to 0.8.4] - 2026-10-04 to 2026-10-07
### Added
- Alert noise control: grouping, repeat notifications, escalation steps, dependency suppression; webhook completeness (replay, test send, limits, retention).
- Plugins: `snmp.*` (system, get, interfaces, ups, pdu, sensor), `redfish.health`, `xapi.pool` (XCP-ng with discovered devices), `camera.snapshot`, `rtsp.stream`.
- Whoopsy adaptive alerts.
- Grafana provisioning and dashboards (devices, websites, network, servers, power, cameras, XCP-ng).
- `admin rotate-platform-key`, `admin revoke-platform-keys`.

## [0.3.x] - 2026-10-04
### Added
- Metrics storage and Grafana data source (F07); usage metering for billing (`GET /v1/usage/tenants`, 0.3.5).

## [0.2.x] - 2026-10-04
### Added
- Scheduler/runner, state machine, incidents, signed Standard Webhooks delivery (M2).
- Device `status`, JSON Merge Patch on devices and checks, lookup by external ID (0.2.1).

## [0.1.x] - 2026-09-29 to 2026-10-04
First release (`v0.1.0-m1`, 2026-09-29).
### Added
- `monitor` CLI, config, migrations, tenants, users, API keys, hash-chained audit log, plugin SDK, `icmp` and `http` plugins, OpenAPI at `/docs`, Docker image and Compose deployment.
36 changes: 36 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Contributing

Thanks for helping. Bug reports, plugin ideas, docs fixes and code are all welcome.

## Ways to contribute

- **Report a bug or ask for a feature:** open an [issue](https://github.com/plusclouds/monitoring.server/issues/new/choose). Include the version (`monitor --version`), what you expected and what happened.
- **Ask a question or share a setup:** use GitHub Discussions.
- **Add a plugin:** protocols and device types are plugins, never core changes. Start from `pkg/plugin` and an existing plugin such as `plugins/http`, and read the [feature specs](docs/features/README.md).
- **Change the design:** edit the relevant feature spec or add an ADR from the [template](docs/adr/0000-template.md) in the same pull request.
- **Security issues:** do not open a public issue. Email security@plusclouds.com.

Look for issues labelled `good first issue`.

## Development

Requirements: Docker; Go 1.27 to build and test from source. See [Development in the README](README.md#development) to run the stack.

```sh
make build # bin/monitor
make test # unit and integration tests with -race (needs Docker)
make lint # golangci-lint, including gosec
make check # everything CI runs
```

If you change `api/openapi.yaml`, run `make generate` and commit the result.

## Pull requests

- Branch from `master`, one topic per pull request.
- Use conventional commit prefixes (`feat:`, `fix:`, `docs:`, `ci:`).
- Add or update tests; `make check` must pass.
- Update the docs and [CHANGELOG.md](CHANGELOG.md) for user-visible changes.
- Dependencies must use a license on the allowlist (see `ALLOWED_LICENSES` in the Makefile).

By contributing you agree your work is released under the [MIT license](LICENSE).
33 changes: 31 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,39 @@

An API-first, extensible monitoring engine written in Go. One engine watches network devices, server hardware (BMCs), IP cameras, web endpoints and ports, hypervisors and their VMs, and IoT/facility devices, and turns what it sees into incidents and signed webhook events.

> **Status: milestone M1 (skeleton).** The binary, process config, database schema, tenancy and API keys, audit log, plugin SDK and the `icmp` and `http` plugins exist. Scheduling, state, incidents and notifications (M2) do not yet. The rest of this page describes the intended system; see [Development](#development) for what runs today.
> **Status: v0.14, pre-1.0.** Scheduling, state, incidents, signed webhooks, metrics, Grafana dashboards and plugins for HTTP, ICMP, SNMP, Redfish, XCP-ng, cameras, HTTP/MQTT push and UPS/PDU all run today. Remote probes, VMware/Proxmox and the Phase 2 and 3 items in the [roadmap](#roadmap) do not exist yet. The API can still change before 1.0; see the [changelog](CHANGELOG.md) and [implementation progress](docs/progress.md).

**No built-in UI, by design.** The engine is an API; dashboards are [Grafana](deploy/grafana/README.md), reading PostgreSQL directly, with seven ready-made dashboards (devices and incidents, websites, network interfaces, server hardware, power, cameras, XCP-ng) provisioned for you. If you want a point-and-click admin UI instead, this is not that tool yet.

<!-- TODO: add Grafana screenshots here (docs/images/) -->

Developed by [PlusClouds](https://plusclouds.com) under the MIT license. It runs standalone or embedded in the PlusClouds panel, and it is multi-tenant from day one because customers run it too.

## Quickstart (5 minutes)

Needs Docker with Compose, `curl` and `jq`. This builds from source and uses plain HTTP: for trying it out, not for production (see [Deploy on a new VM](docs/deployment/standalone-vm.md)).

```sh
# 1. Start PostgreSQL, the engine and Grafana with the dashboards
git clone https://github.com/plusclouds/monitoring.server && cd monitoring.server
docker compose -f deploy/docker-compose.yml --profile grafana up -d --build --wait

# 2. Create your tenant and admin key (shown once)
docker compose -f deploy/docker-compose.yml run --rm monitor admin bootstrap --standalone --tenant "Demo"
export MON_URL=http://127.0.0.1:8443 MON_KEY=mon_... # paste the key
mon() { curl -sS -H "Authorization: Bearer $MON_KEY" -H "Content-Type: application/json" "$@"; }

# 3. Add a device with an HTTP check (a bad status makes it fail on purpose)
DEVICE=$(mon -X POST $MON_URL/v1/devices -d '{"name":"Demo site","type":"web","address":"https://httpstat.us/500"}' | jq -r .id)
mon -X POST $MON_URL/v1/devices/$DEVICE/checks -d '{"name":"Homepage","plugin":"http","interval_seconds":30,"is_host_check":true}'

# 4. Point a webhook at a receiver (open https://webhook.site, copy your URL) and route everything to it
WH=$(mon -X POST $MON_URL/v1/webhooks -d '{"name":"demo","url":"https://webhook.site/<your-id>"}' | jq -r .id)
mon -X POST $MON_URL/v1/alert-routes -d "{\"name\":\"all\",\"endpoint_id\":\"$WH\"}"
```

After a few failed runs a signed `monitoring.incident.opened` event arrives at your receiver. Open http://127.0.0.1:3000 (login `admin` / `admin`) to see the device, its check and the incident in the **Device overview** dashboard. Next: the [usage guide](docs/usage-guide.md) and the [Grafana notes](deploy/grafana/README.md).

---

## What it does
Expand Down Expand Up @@ -153,7 +182,7 @@ npx newman run api/postman/monitoring.postman_collection.json \

## Contributing

The project is in the design phase. Proposals and reviews happen on the feature specs and ADRs: open a pull request that edits the relevant document, or add a new ADR using the [template](docs/adr/0000-template.md).
See [CONTRIBUTING.md](CONTRIBUTING.md). Bugs and ideas go in [issues](https://github.com/plusclouds/monitoring.server/issues); design changes go through the feature specs and ADRs.

## License

Expand Down
9 changes: 8 additions & 1 deletion deploy/compose/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,18 @@ MONITOR_TRUSTED_PROXIES=172.16.0.0/12,192.168.0.0/16,10.0.0.0/8
# Image to run. The registry needs `docker login registry.plusclouds.com`
# unless the host is already logged in.
MONITOR_IMAGE=registry.plusclouds.com/monitoring
MONITOR_VERSION=0.1.2-m1
MONITOR_VERSION=0.15.0

# external_source of tenants and users provisioned by the platform key.
MONITOR_IDENTITY_SOURCE=plusclouds

# Optional: password of the read-only monitor_grafana database role (F07).
# Leave empty to keep the role unable to log in. See deploy/grafana.
MONITOR_GRAFANA_PASSWORD=

# Optional Grafana image (docker compose --profile grafana up -d). It needs
# MONITOR_GRAFANA_PASSWORD above and an admin password for the Grafana login.
# GRAFANA_ADMIN_PASSWORD=
# GRAFANA_ROOT_URL=http://localhost:3000
# GRAFANA_BIND=127.0.0.1
# GRAFANA_PORT=3000
32 changes: 30 additions & 2 deletions deploy/compose/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ services:
networks: [backend]

monitor:
image: ${MONITOR_IMAGE:-registry.plusclouds.com/monitoring}:${MONITOR_VERSION:-0.1.2-m1}
image: ${MONITOR_IMAGE:-registry.plusclouds.com/monitoring}:${MONITOR_VERSION:-0.15.0}
restart: unless-stopped
depends_on:
db-init:
Expand Down Expand Up @@ -108,7 +108,7 @@ services:
# Runs once per `up`: creates the installation ID, the platform tenant and
# the platform key on the first start, and does nothing afterwards.
bootstrap:
image: ${MONITOR_IMAGE:-registry.plusclouds.com/monitoring}:${MONITOR_VERSION:-0.1.2-m1}
image: ${MONITOR_IMAGE:-registry.plusclouds.com/monitoring}:${MONITOR_VERSION:-0.15.0}
command: ["admin", "bootstrap", "--if-needed"]
restart: "no"
depends_on:
Expand All @@ -120,6 +120,33 @@ services:
target: /etc/monitor/config.yaml
networks: [backend]

# Optional Grafana with the dashboards baked in: docker compose --profile grafana up -d
# Needs MONITOR_GRAFANA_PASSWORD in .env (db-init then lets monitor_grafana
# log in). Published on 127.0.0.1 only: put a TLS proxy in front.
grafana:
image: ${GRAFANA_IMAGE:-registry.plusclouds.com/monitoring-grafana}:${MONITOR_VERSION:-0.15.0}
profiles: [grafana]
restart: unless-stopped
depends_on:
db-init:
condition: service_completed_successfully
environment:
MONITOR_GRAFANA_DB_HOST: postgres:5432
MONITOR_GRAFANA_DB_SSLMODE: disable
MONITOR_GRAFANA_PASSWORD: ${MONITOR_GRAFANA_PASSWORD:?set MONITOR_GRAFANA_PASSWORD in .env to use Grafana}
GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_ADMIN_PASSWORD:?set GRAFANA_ADMIN_PASSWORD in .env to use Grafana}
GF_SERVER_ROOT_URL: ${GRAFANA_ROOT_URL:-http://localhost:3000}
volumes:
- grafana-data:/var/lib/grafana
networks: [backend, frontend]
ports:
- "${GRAFANA_BIND:-127.0.0.1}:${GRAFANA_PORT:-3000}:3000"
healthcheck:
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:3000/api/health"]
interval: 5s
timeout: 3s
retries: 30

configs:
# Engine settings. Secrets are not here: the environment above supplies the
# database DSNs, the key-encryption key and the public URL from .env.
Expand Down Expand Up @@ -247,3 +274,4 @@ networks:

volumes:
pgdata:
grafana-data:
21 changes: 21 additions & 0 deletions deploy/grafana/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Grafana with the starter dashboards and the PostgreSQL data source baked in
# (F07). Build from this directory:
# docker build --build-arg VERSION=$(git describe --tags --always) -t monitoring-grafana deploy/grafana
# Settings at run time (see README.md): MONITOR_GRAFANA_DB_HOST,
# MONITOR_GRAFANA_DB_SSLMODE, MONITOR_GRAFANA_PASSWORD.
ARG GRAFANA_VERSION=13.0.2
FROM grafana/grafana-oss:${GRAFANA_VERSION}
ARG VERSION=dev
LABEL org.opencontainers.image.title="monitoring.server Grafana" \
org.opencontainers.image.description="Grafana with the monitoring.server dashboards and data source" \
org.opencontainers.image.source="https://github.com/plusclouds/monitoring.server" \
org.opencontainers.image.licenses="MIT" \
org.opencontainers.image.version="${VERSION}"
ENV MONITOR_GRAFANA_DB_HOST=postgres:5432 \
MONITOR_GRAFANA_DB_SSLMODE=disable \
GF_AUTH_ANONYMOUS_ENABLED=false \
GF_ANALYTICS_REPORTING_ENABLED=false \
GF_ANALYTICS_CHECK_FOR_UPDATES=false \
GF_DASHBOARDS_DEFAULT_HOME_DASHBOARD_PATH=/var/lib/grafana/dashboards/monitor/device-overview.json
COPY --chown=472:0 provisioning /etc/grafana/provisioning
COPY --chown=472:0 dashboards /var/lib/grafana/dashboards/monitor
16 changes: 16 additions & 0 deletions deploy/grafana/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,22 @@ docker compose -f deploy/docker-compose.yml --profile grafana up -d --wait
open http://127.0.0.1:3000 # admin / admin
```

## Grafana image

`Dockerfile` builds Grafana with the provisioning and dashboards baked in, so nothing needs mounting. CI publishes it with every version tag as `registry.plusclouds.com/monitoring-grafana`, next to the engine image, with the same version.

```sh
docker build -t monitoring-grafana deploy/grafana
```

In `deploy/compose`, set `MONITOR_GRAFANA_PASSWORD` and `GRAFANA_ADMIN_PASSWORD` in `.env`, then:

```sh
docker compose --profile grafana up -d
```

Grafana listens on `127.0.0.1:3000`; put a TLS proxy in front before exposing it. The image reads the same three variables as below. Dashboards are read-only (`allowUiUpdates: false`); change them in `dashboards/` and rebuild.

## Production

1. Set `MONITOR_GRAFANA_PASSWORD` in `deploy/compose/.env`. On the next `docker compose up`, the `db-init` service lets `monitor_grafana` log in with that password and grants it the views.
Expand Down
9 changes: 9 additions & 0 deletions deploy/nginx/mqtt.plusclouds.com.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# mqtt.plusclouds.com: only the Let's Encrypt challenge for the MQTT broker's
# certificate (webroot renewal, see deploy/compose/mqtt-cert-hook.sh). MQTT
# itself (8883, 1883) goes straight to the container, not through nginx.
server {
listen 80;
server_name mqtt.plusclouds.com;
location /.well-known/acme-challenge/ { root /var/www/letsencrypt; }
location / { return 404; }
}
Loading