gocraft/health: instrumenting a Go service with jobs, sinks and an aggregator
Instrument your web apps with logging and metrics
At a glance
- What is it?
- gocraft/health is a Go library for instrumenting a service with logging and metrics, sending that instrumentation to log files, StatsD, Bugsnag or a JSON polling API, and it ships healthd, an aggregator in the style of New Relic that surfaces your slowest endpoints and top error producers. The MIT licensed project has been quiet since 2026-04-23.
- Who is it for?
- gocraft/health suits a Go team that wants one instrumentation vocabulary covering both logs and metrics across several backends without adopting a vendor SDK, since the job model is the same idea as a trace span applied to a service's own semantics. It is the wrong tool for a new service in 2026, because it predates OpenTelemetry and Go's standard slog, both of which cover similar ground with better ecosystem support.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Activity is slowing. The repository last received commits 6 months ago.
- What is it written in?
- Mainly Go, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
One vocabulary for logs and metrics
gocraft/health allows you to instrument your service for logging and metrics, and then send that instrumentation to log files, StatsD, Bugsnag, or to be polled and aggregated via a JSON API.
The interesting part of that sentence is that it treats logs and metrics as the same stream rather than as two subsystems. A single Stream carries everything, and sinks decide where it goes. That is a meaningful simplification for a service that today probably logs to stdout in one place and reports metrics to StatsD in another, because the two are usually instrumented with different libraries, different naming conventions and different context.
The design is anchored on jobs. The README says gocraft/health excels at instrumenting services that perform jobs, with examples of serving an HTTP request, serving an RPC request, or processing a message from a work queue. Jobs are encoded semantically into the library in order to provide out-of-the-box answers to questions like what is my slowest endpoint.
A job serves three functions. It records a timing, for example that it took 21ms to complete the job. It records a status, whether the job completed successfully or there was an error. And it groups instrumentation inside that job together so you can analyse it later. The third is what makes the aggregator's questions answerable, since without the grouping you have a scatter of timings with no idea which request they belonged to.
The project also ships healthd, described as a New Relic-like aggregator that shows you your slowest endpoints, top error producers, top throughput endpoints and so on. The licence is MIT and the primary language is Go.
Wiring sinks onto a stream
The setup is a Stream and a set of sinks, and the README's example is worth reading in full because each sink type has its own constructor and its own error handling.
// Expose instrumentation in this app on a JSON endpoint that healthd can poll!
sink := health.NewJsonPollingSink(time.Minute, time.Minute*5)
stream.AddSink(sink)
sink.StartServer(addr)
// Send errors to bugsnag!
stream.AddSink(bugsnag.NewSink(&bugsnag.Config{APIKey: "myApiKey"}))
// Now that your stream is setup, start a web server or something...
}The JsonPollingSink takes two durations, a minute and five minutes in the example, and the sink is added to the stream and then started with a server address, which means the polling endpoint is a separate HTTP server the aggregator connects to. The Bugsnag sink takes a config struct with an API key and is added the same way.
The StatsD sink is constructed differently because it can fail, and the example shows what to do about that:
// Log to StatsD!
statsdSink, err = health.NewStatsDSink("127.0.0.1:8125", "myapp")
if err != nil {
stream.EventErr("new_statsd_sink", err)
return
}
stream.AddSink(statsdSink)A StatsD sink is a UDP client, so it resolves a host and port and can fail on a bad address, and the handling here is to report the failure as an event on the stream itself rather than to crash. The stream is normally kept in a package-level variable, and the sinks are attached in main.
The WriterSink takes a writer, so os.Stdout in the example, and the README notes it can also be used to write to a log file or Syslog. That is the sink that makes the library useful with no infrastructure at all, which matters for a service that has not yet been wired into a metrics pipeline.
Jobs, completion statuses, and the grouping that makes them useful
The pattern is to create a job at the start of the work and complete it at the end. Here is the handler from the README:
// All logging and instrumentation should be within the context of a job!
job := stream.NewJob("get_users")
err := fetchUsersFromDatabase(r)
if err != nil {
// When in your job's context, you can log errors, events, timings, etc.
job.EventErr("fetch_user_from_database", err)
}
// When done with the job, call job.Complete with a completion status.
if err == nil {
job.Complete(health.Success)
} else {
job.Complete(health.Error)
}There are five completion statuses, and the list is worth reading because it shows how carefully the author thought about distinguishing failure modes. Success is the obvious one. Error is when some library call resulted in an error that prevented the job completing successfully. Panic is when some code panicked. ValidationError is when your code was fine but the user passed in bad inputs. And Junk is when the job did not complete successfully but not really because of an error or a validation error, for instance a 404 or a 401 request, with the note that this status might not apply to all apps.
Separating ValidationError from Error is what lets a dashboard show that your service is healthy while a particular input was rejected, which is a distinction most instrumentation libraries collapse.
One caveat the README raises itself: the example is for illustration, and in practice you will probably want to use middleware to create the job if you have more than a few endpoints. Manual job creation at every handler is the version that drifts, and middleware is the answer.
The repository layout shows the pieces: health.go for the core types, aggregator.go for the aggregation logic, interval_aggregation.go and its merge and clone variants, json_polling_sink.go and its HTTP counterpart, writer_sink.go, statsd_sink.go, error.go, identifier.go, plus sinks/, stack/, healthd/, runtime_metrics/ and cmd/ directories.
Events, timings and gauges, and the naming convention
Within a job you emit events, timings, gauges and errors, and the first argument of each is a key. The README advises camel case with dots, because it works with other metrics stores like StatsD. Each method also has a variant that accepts key and value pairs.
Events are the simplest:
// Events. Notice the camel case with dots.
// (This is helpful when you want to use StatsD sinks)
job.Event("starting_server")
job.Event("proccess_user.by_email.gmail")
// Event with keys and values:
job.EventKv("failover.started", health.Kvs{"from_ip": fmt.Sprint(currentIP)})The dotted convention is what makes StatsD work, since a dot in a metric name is how StatsD signals a path segment.
Timings are always in nanoseconds, and the README says so twice, with a NOTE on the call site itself:
// Timings:
startTime := time.Now()
// Do something...
job.Timing("fetch_user", time.Since(startTime).Nanoseconds()) // NOTE: Nanoseconds!
// Timings also support keys/values:
job.TimingKv("fetch_user", time.Since(startTime).Nanoseconds(),
health.Kvs{"user_email": userEmail})For the WriterSink a timing looks like a log line with a time field, and for the StatsD sink it is sent as a timing. The JSON polling sink computes a summary of your timings, giving min, max, avg, stddev, count and sum.
Gauges follow the same shape, with a gauge reporting a current value rather than a duration:
// Gauges:
job.Gauge("num_goroutines", numRunningGoroutines())
// Timings also support keys/values:
job.GaugeKv("num_goroutines", numRunningGoroutines(),
health.Kvs{"dispatcher": dispatcherStatus()})One rough edge is worth noting. The README describes a Gauge in the timing section and says for the WriterSink, a timing is just like logging to a file, but the example line it shows is a gauge with a gauge field, so the prose and the sample are crossed. The code is the authority here.
Errors, stack traces and muting
Errors are emitted through the job, and the README notes that errors will capture a stacktrace by default so that you can diagnose them in things like Bugsnag. If an error is common or not worth sending to something like Bugsnag, you can mute it, and the effect is that health will not capture a stack trace or send it to Bugsnag.
// Errors:
err := someFunc(user.Email)
if err != nil {
return job.EventErr("some_func", err)
}
// And with keys/Values:
job.EventErrKv("some_func", err, health.Kvs{"email": user.Email})The sink behaviour differs by destination, which is the point of the sink abstraction. For the WriterSink an error is just logged to the file with the error. For metrics sinks, errors are just like events. The JSON polling sink and healthd let you see which errors are trending, which is a different question from which errors occurred. And for the Bugsnag sink each error is pushed to Bugsnag.
So one call site produces a stack trace in one destination, a counter increment in another and a notification in a third. That is the practical value of the sink design, and it is also the reason the library is easier to adopt incrementally: you can start with only a WriterSink to stdout and get structured logs with no infrastructure, then add sinks as your monitoring grows.
The example error line for the WriterSink shows the format with the error name and the key and value pairs on the same line, so a log line carries both the message and the context that produced it.
Where gocraft/health is the wrong choice today
Three limits are structural. The first is that it predates OpenTelemetry, and the gap matters more than it might seem. OpenTelemetry traces and metrics now cover the job-like grouping this library does, with spans carrying events, timings and attributes, and an ecosystem of exporters rather than a fixed set of sinks. The difference in approach is that gocraft/health models instrumentation semantically around jobs and offers a small set of destinations, while OpenTelemetry models it as generic signals with a plugin architecture. A team that has already committed to OpenTelemetry should not add a second model, and a team starting fresh in 2026 should ask why they are not using OpenTelemetry in the first place.
The second is that Go's standard library now has slog, which gives structured logging with handlers, and the ecosystem around it is broad. gocraft/health's advantage over slog is the job grouping and the built-in aggregator, and the disadvantages are a smaller community and a much older idea.
The third is the maintenance state, and here the facts are stark. The last push to the repository was on 2026-04-23, which is more than six months before today, and there are no GitHub releases. There is a TODO file at the root, which suggests the author intends to return to it, but an unversioned, unreleased dependency that has been quiet for months is a risk you take on knowingly. The MIT licence at least means you can vendor it and own the maintenance yourself, and the code is small enough that reading health.go and the sinks is a realistic way to decide.
A practical note for anyone who does adopt it: the library assumes a single global stream, which is fine for the common case of one service per process, and the JSON polling sink starts its own HTTP server, so you need to be aware of that in a deployment where you already have a listener on the port.
Editorial conclusion
gocraft/health suits a Go team that wants one instrumentation vocabulary covering both logs and metrics across several backends without adopting a vendor SDK, since the job model is the same idea as a trace span applied to a service's own semantics. It is the wrong tool for a new service in 2026, because it predates OpenTelemetry and Go's standard slog, both of which cover similar ground with better ecosystem support. Verify first whether your team already has an OpenTelemetry pipeline, since adding a second instrumentation model alongside it is usually not worth it, and note that the repository has no GitHub releases with the last push on 2026-04-23.
Frequently asked questions
What does gocraft/health do?
It lets you instrument a Go service for logging and metrics and send that instrumentation to log files, StatsD, Bugsnag, or to be polled and aggregated via a JSON API. It also ships healthd, an aggregator in the style of New Relic that shows your slowest endpoints and top error producers.
What is a job in gocraft/health?
A job is the unit of instrumented work, such as serving an HTTP request or processing a message from a work queue. It records a timing, records a completion status, and groups the instrumentation inside it so you can analyse it later.
Which completion statuses does gocraft/health support?
Five: Success, Error, Panic, ValidationError for when the user passed bad inputs, and Junk for a job that did not complete successfully for other reasons such as a 404 or a 401. The README notes Junk might not apply to all apps.
In what units are timings reported by gocraft/health?
Nanoseconds. The README states it explicitly and puts a NOTE on the call site, job.Timing("fetch_user", time.Since(startTime).Nanoseconds()). The JSON polling sink summarises timings as min, max, avg, stddev, count and sum.
Which sinks does gocraft/health provide?
A WriterSink that writes to any writer including os.Stdout, a StatsD sink created with health.NewStatsDSink, a Bugsnag sink created with bugsnag.NewSink, and a JsonPollingSink created with health.NewJsonPollingSink that exposes a JSON endpoint for healthd to poll.
Why should event keys use camel case with dots in gocraft/health?
The README says camel case with dots is good because it works with other metrics stores like StatsD, where a dot in a metric name signals a path segment. So a key such as proccess_user.by_email.gmail groups correctly in StatsD.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/gocraft-health)Was this useful?