Ramblings of an aging IT geek
← Ramblings of an aging IT geek
golang

the smallest useful daemon i've written in go

A small Go daemon that watches a directory and posts a webhook, built so it could run for months and be forgotten about.

Code on a terminal

There's a particular pleasure in a program that does one thing and then gets out of the way. I wrote one this week: a daemon that watches a directory, and when a file lands in it, posts a small JSON payload to a webhook. That's the whole brief. No queue, no database, no config server. About two hundred lines, and most of those are error handling.

The reason it's in Go rather than a shell script with inotifywait is boring and correct: I wanted a single static binary I could drop on a box with no runtime to install, and I wanted it to keep running. A shell loop that dies at 3am because the network blipped is not a daemon, it's a liability.

the shape of it

The core is fsnotify for the watch, a buffered channel to decouple the watcher from the sender, and a worker that drains the channel. Decoupling matters more than it looks: filesystem events arrive in bursts, and if you do the HTTP POST inline you'll miss events while you're blocked on a slow endpoint.

events := make(chan string, 256)

go func() {
	for {
		select {
		case ev := <-watcher.Events:
			if ev.Op&fsnotify.Create == fsnotify.Create {
				events <- ev.Name
			}
		case err := <-watcher.Errors:
			log.Printf("watch error: %v", err)
		}
	}
}()

The worker side does the actual work, with a retry and a sane timeout on the client. The default http.Client has no timeout at all, which is the kind of thing that bites you six weeks later when a downstream hangs and your daemon quietly wedges. Always set one.

client := &http.Client{Timeout: 10 * time.Second}

A small workshop bench

the bits that aren't glamorous

Graceful shutdown took longer than the feature. I want it to finish the file it's on, drain what it can, and then exit when systemd sends SIGTERM. So signal.Notify for SIGINT and SIGTERM, a context that gets cancelled, and the worker checks it between sends. Nothing clever, just the discipline to actually wire it up.

The other unglamorous bit is the unit file. Run it under systemd with Restart=on-failure, a dedicated user, and ReadWritePaths scoped to exactly the directory it watches. If the daemon is going to be forgotten about, and it is, then it should be forgotten about safely.

I built it with CGO_ENABLED=0 so the binary is genuinely static, scp'd it across, dropped in the unit file, and that was that. It has been running for four days and posted a few hundred events without complaint. The best compliment a daemon can get is that you stop thinking about it.