Once upon a time, I wrote this logging library for Go called grip and used it a lot at a couple of jobs, where it provided a pretty great API and way to think about running logging and metrics infrastructure for an application. For the last year and a half, I've mostly ignored it. I didn't want to be that guy about loggers, so I mostly let it drop, but I was looking at some other logging systems and I felt inspired to unwrap it a bit and see if I could improve things and if the time away would inspire me to rethink any of the basic assumptions.
This post is about a lot of things (and maybe it will spill over,) but generally:
- the practice and use of logging, and how that is changing,
- adoption of libraries out side of the standard library for common infrastructure,
- the process of changing grip, and also digging into some more specific changes.
Grip came about sort of at the height of the structured logging craze, and I think still focuses more on being a way to provide not just the ability to do slightly cooler than a collection of print statements but also be the primary way your software transmits events. Because all of the parts of grip are pretty plugable, we used this for everything from normal application logging to our entire event notification system, and kind of everything in between, including a sort of frightful amount of managing the output of a distributed build farm. The output of these messages went to all of the usual notification targets (e.g. email, slack, github, jira,) but also directly to log aggregation services (e.g. splunk) and the system log without being written to a file, which just made the services much easier to operate.
Like me, grip is somewhat opinionated. A lot of loggers compete based on "speed" (typically of writing messages to standard output or to any file) or on features (automatic contextual data collection and message annotation, formatting tools, hierarchical filtering, etc.) but I think grip's selling point is really:
- great interfaces for doing all of the things that you normally do with a logger, and all the things you want to do.
- provide ways of logging data that doesn't involve building/formatting strings, and lets the normal data.
- make logging work just as well for the small CLI process as the large distributed application, and with similar amounts of ease. Pursuant to this I really think that any logging system or architecture that requires lots of extra stuff (agents, forwarders, rotation tools, etc.) to actually be able to use the logs is overkill. There's nothing particularly exciting about sending logs over the network (or to a file, or a local socket,) and it should be possible to reduce the amount of complexity in this part of the deployment, which is good for just about everyone.
- provide good interfaces and paradigms for replacing or custimizing core
logging behavior. It should be possible for applications to write their own
tools for sending log messages to places, or functions to filter and format
log messages. This is a logging tool kit that can grow and develop with your
- in the x hierarchy include implementations for many messaging formats, and tools: use grip implementations to send alerts to telegram, email, slack, jira, etc. Also, support for sending logs _directly_ to wherever they need to be (splunk, syslog, etc.). Whever the logs need to be, grip should be able to get you there.
- The core grip package provides logging multiplexers, buffers, and batching tools to help control and manage the flow of logging from your application code to whatever logging backend you're targeting.'
- Implementations and bridges between grip's interfaces and other logging framework and tools. Grip attempts to make it possible to become the logging system for existing applications without needing to replace all logging call sites immediately (or ever!)
- Tools to support using grip interfaces and backends inside of other tools: you can use a grip Sender as a writer, or access a standard-library logger implementation that writes to the underlying grip logger.
- logging should be fast, but speed of logging only really matters when data volume is really high, which is usually a problem for any logger: when data volume is lower even a really slow logger is still faster than the network or the file system. Picking a logger that's fast, given this, is typically a poor optimization. Grip should be fast enough for any real world application and contains enough flexibility to provide an optimized path for distributing log messages as needed.
- provide features to support some great logging paradigms:
- lazy construction of messages: while the speed of writing messages to their output handler typically doesn't matter, sometimes building log messages can be intensive. Recently I ran into a case where calling Sprintf at the call site of the logger for messages that were not logged (e.g. debug messages,) had material effects on the memory usage of the program. While string formating is a common case (and grip has "Logf" style handlers that are lazy by default,) we found a bunch of metrics collection workloads that had similar sorts of patterns and lazy execution of these metrics tended to help a lot.
- conditional logging, rather than wrapping a log statement in an if block, I found that you could have logging handlers that
- randomized logging or sampled logging: In some cases, I want to have log messages that only get logged half the time or something similar. In some high-volume code paths logging all the time is too much, and never is also too much, but it's possible to devise a happy medium. Grip, for years now, has implemented this in terms of the conditional logging functionality.
- structured logging, rather than logging strings, it's sometimes nice to just pass data, in the form of a special structure or just a map and let the logging system package deal with the output formating and transmission of messages. In general, line-oriented JSON makes for a good logging format, assuming most or enough of your messages have related structures and your log viewing and processing tools support this kind of data, although alternate human-readable string formats should also be available.
None of these things changed in the rewrite: by default, grip mostly just looks and works like the Go standard library logger (and even uses the logger under the hood for a lot of things,) but it was definitely fun to look at more contemporary logging practices and tools and think about what makes a logging library compelling (or not).
Infrastructure libraries are an interresting beast: ideally every dependency carries some kind of maintenance cost, so you want to minimize the number of dependencies you require. At the same time, not using libraries is also bad because it means you end up writing more code and that has even more maintenance costs. It's also the case that software engineers love writing infrastructure code and are often quite opinionated about the ergonomics of their infrastructure libraries.
On top of that, you make infrastructure software decisions once and then are sort of stuck with them for a really long time. I've changed loggers in projects and it's rough, and in general you want to choose libraries: as an application developer you have the great feeling that no one's differentiating feature is going to have anything to do with the logger  and you want something that's battle tested and familiar to everyone. Sometimes--as in the logging package in Python--the standard library has a library just works and everyone just uses that; other times, there are one or two options that most project uses (e.g. log4j in java, etc.).
Even if grip is great, it seems unlikely that everyone (or anyone?) will switch to using grip over some of the other more popular options. I'm OK with that, I'm not sure that beyond writing a few blog posts I'm really that excited about doing the kind of marketing and promotion that it might take to promote a logging library like this, and at the same time the moment for a new logging library might have already passed.
|||Arguably, in a CI platform, most of the hard problems have something to do with logging, so this is an exception, but perhaps one of the only exceptions|
The "new grip" is a pretty substantial change. A lot of implementation details changed and I deleted big parts of the interface that didn't quite make sense, or that I thought were a bit fussy to use. Basically just sanding off a lot of awkward edges. The big changes:
- greatly simplified dependencies, with more packages and an x hierarchy for extensions. The main grip package, and it's primary sub packages' no longer has any external dependencies (beyond github.com/tychoish/fun.) Any logging backend or message type that has additional dependencies are in x.
- I deleted a lot of code. There were a lot of things that just weren't needed, there was an extra interface, a bunch of constructors for various objects that weren't useful. I also simplified the concept of levels/priority within the logging system.
- simpler high level logging interface. I used to have an extra interface and package to hold all of the Info, Error, (and so forth), and I cut a lot of that out and just made a Logger type in the main package which just wraps an underlying interface and doesn't need to be mutable, and doing this made it possible to simplify a lot of the code.
- added some straight forward handlers to attach loggers to contexts. I think previously, I was split on the opinion that loggers should either be (functionally) global or passed explicitly around as objects, and I think I've come around to the idea that loggers maybe ought to hang off the context object, but contextual loggers, global loggers, and logging objects are all available.
- the high level logging interface is much smaller, with handlers for all the levels and formatting, line, and conditional (e.g. f, ln, When) logging. I'm not 100% sold on having ln, but I'm pretty pleased with having things be pretty simple and streamlined. The logger interface, as with the standard logger is mirrored in the grip package, with a shim.
- new message.Builder interface and methods that provides a chainable interface for building messages without needing to mess with the internals of the message package which might be ungainly at logging call sites.
- new KV message type: this makes it possible to have structured logging payloads without using map types, which might prove easier easier to integrate with the new zerolog and zap backends.
- I have begun exploring in the series package, what it'd mean to have a metrics collection and publication system that is integrated into a logger. I wrote probably too much code, but I am excited to do some more work to do some more work in this area.