一个 Go 二进制文件,一个 YAML 配置文件,一个 SQLite 数据库:我编写了自己的监控工具
One Go binary, one YAML file, one SQLite database: I wrote my monitoring tool

原始链接: https://rvier.fr/posts/why-i-wrote-my-own-monitoring-tool-EN

作者开发了 **Gjallar**,这是一款轻量级、自包含的监控工具,旨在避免 Prometheus 或 Grafana 等臃肿平台带来的复杂性。面对在无需管理复杂依赖或分布式系统的情况下监控异构服务(Postgres、Oracle、Redis 等)的需求,作者选择了“KISS”(保持简单,傻瓜)的设计原则。 Gjallar 是一个无需 CGO 的单一 Go 二进制文件,利用纯 Go 驱动程序来处理数据库连接,最显著的优势是消除了对 Oracle 客户端库的依赖。其核心设计特点包括: * **无锁架构:** 使用通道(channel)的流水线机制确保了线程安全,避免了常见的“互斥锁丛林”,并对 SQLite 写入进行了串行化处理。 * **运维弹性:** 状态持久化存储在 SQLite 中,支持无缝重启,且不会遗漏正在进行的事件通知。 * **可靠性:** 该工具使用简单的 YAML 配置,支持热重载,并异步处理通知延迟以防止背压。 通过刻意摒弃集群、代理插件和复杂的仪表板,Gjallar 保持为一个易于理解且低维护的实用工具。它优先考虑简洁性,以确保即使在紧急情况下,监控系统依然是技术栈中最可靠的组件。

最近的一场 Hacker News 讨论强调了“单个 Go 二进制文件 + SQLite”架构在监控工具中的简洁与高效。原作者介绍了他们的项目 *rvier.fr*,该项目利用这种精简的架构来最大限度地降低部署复杂度。 社区对此方案给予了高度评价,用户指出它在安全性、扩展性和维护方面具有显著优势。一位评论者将其称为“HUGS”架构(Hypermedia、Unix、Go、SQLite),并表示他们多年来已在单租户虚拟机上成功应用了这一模式。其他人也分享了使用类似设置的经验(包括简单的定时脚本),并指出这种架构同样易于移植到 Rust 等其他语言。 讨论帖还涉及了一些技术考量,例如 macOS 上为避免文件系统冲突而采用的最佳命名实践。总体而言,社区共识反映出一种日益增长的趋势:在单节点服务管理中,人们正逐渐偏好“无聊”且自包含的基础设施,而非更为复杂的分布式方案。
相关文章

原文

I needed to watch a fleet of heterogeneous services: HTTP endpoints, PostgreSQL databases, a few Oracle instances, Redis, Elasticsearch indexes that must stay fresh, machines that should answer ping, and some Prometheus metrics. And I needed to be told, on Telegram, by SMS, on Signal, when something goes down, and when it comes back.

The classic answer is a monitoring platform: Prometheus plus Alertmanager plus Grafana plus a handful of exporters, or a container running a Node.js app with a database. All great tools. But for a few dozen checks, I did not want to operate a second distributed system just to know whether the first one is up. And none of the lightweight options could query Oracle without me installing the Oracle client libraries somewhere.

So I wrote Gjallar: a KISS monitoring service. One static binary, one YAML config file, one SQLite file. A black-and-red status page with history, HTMX-refreshed. About 3,400 lines of Go. MIT licensed.

Zero CGO, on purpose

The whole tool builds with CGO_ENABLED=0:

CGO_ENABLED=0 go build -trimpath -ldflags "-s -w"

That is only possible because every dependency that would traditionally bind to a C library has a pure-Go replacement nowadays, and they are excellent:

  • pgx for PostgreSQL: no libpq;
  • go-ora for Oracle: no Oracle Instant Client, which alone justified the project. If you have ever deployed the Oracle client on a minimal box, you know;
  • pro-bing for ICMP echo, privileged or unprivileged;
  • modernc.org/sqlite for storage: SQLite transpiled to pure Go, no libsqlite3;
  • Redis needs no driver at all: the check speaks the protocol directly: TCP connect, optional AUTH, PING, expect +PONG.

The result is a single self-contained binary (about 36 MB, most of it the SQLite and Oracle drivers) that cross-compiles from my laptop to any target with GOOS/GOARCH, and deploys with scp. No Docker, no package manager, no shared libraries, no "works on my machine".

A lock-free alert pipeline

Monitoring tools are naturally concurrent, every monitor waits on the network most of the time, and concurrency is where side projects usually grow their first mutex jungle. Gjallar has no locks around its state at all, because of how the pipeline is shaped:

one goroutine per monitor ──▶ results channel ──▶ single consumer
                                                  (state machine + SQLite writes)

Each monitor runs its check loop in its own goroutine and sends check.Result values into a shared channel. A single consumer goroutine owns everything downstream: the up/down state machine, incident rows, and history writes. Since only one goroutine ever touches the state map and the database connection, there is nothing to lock, and SQLite, which dislikes concurrent writers, gets exactly one.

The per-monitor state is small and explicit:

type monitorState struct {
    down         bool
    consecFails  int
    downSince    time.Time
    lastNotified time.Time
    threshold    int           // consecutive failures before DOWN fires
    realert      time.Duration // reminder interval while down; 0 = disabled
    notifiers    []string
}

Two design points earned their keep in production:

  • State survives restarts. At startup, each monitor's state is seeded from any open incident in SQLite. A restart while something is down neither re-fires the DOWN alert nor misses the recovery notification. Deploying a new version during an outage is a non-event.
  • Notifications are dispatched asynchronously. The consumer must never block: a slow SMTP server or a rate-limited Telegram API cannot back-pressure the whole pipeline. Sends go out in their own goroutines with a 15-second timeout.
  • Alerts fire after N consecutive failures, not on the first blip, no flapping noise, and an optional realert interval reminds you while an incident stays open.

Configuration that respects operations

Everything lives in one YAML file, with defaults, named notifiers, and monitor groups:

defaults:
  interval: 60s
  timeout: 10s
  failure_threshold: 3
  alerts: [ops-telegram]

alerts:
  ops-telegram:
    url: "telegram://TOKEN@telegram?chats=123456789"

monitors:
  - name: app-db
    type: postgres
    dsn: "postgres://monitor:${PG_PASSWORD}@db1:5432/app"
    query: "SELECT count(*) FROM jobs WHERE status = 'stuck'"
    rule: "== 0"

Three small features make it pleasant to operate:

  • Hot reload on SIGHUP: systemctl reload gjallar applies the new config, but only after it has been fully validated. A broken YAML keeps the running configuration alive and logs the error, instead of taking the monitoring down with it. Your watcher should be the last thing that dies from a typo.
  • ${VAR} environment expansion for secrets, with a clear startup failure if a referenced variable is undefined, and a bare $ (say, in a ~ ^OPEN$ regex rule) left untouched.
  • A -check flag for dry-run validation, so CI can lint the config before it ever reaches the server.

What it deliberately does not do

No clustering, no agents, no plugin system, no time-series dashboards, no user accounts. History is pruned after a configurable retention (30 days by default) so the SQLite file stays small forever. If a need is served well by an existing simple mechanism, systemd for the service lifecycle, shoutrrr URLs for the twenty notification services I will never use, Gjallar delegates instead of reimplementing.

This is the part I would defend the hardest. Every monitoring tool I have abandoned over the years died of the same disease: it slowly became a platform, and one day the monitoring needed monitoring. A tool whose whole state fits in one SQLite file and whose whole behaviour fits in one YAML file is a tool you still understand at 3 a.m., eighteen months after you wrote it.

Code and documentation: github.com/brvier/Gjallar.

联系我们 contact @ memedata.com