Library / SDK
inconshreveable/log15 avatar
inconshreveable/log15

log15 gives Go a composable structured logger, but slog has caught up

Structured, composable logging for Go

1,104 stars144 forksGoNOASSERTION

At a glance

What is it?
log15 is a Go logging package built around a small Handler interface, with child loggers, lazy evaluation, multi handler forking and built-in formats. The v3 rewrite lives in a v3 subdirectory, and the README itself suggests the standard library log/slog package may now be a better fit.
Who is it for?
log15 suits a Go codebase that already depends on it, or that needs a handler interface small enough to implement in a few lines, and the v3 branch plus the compat package make it a reasonable choice for an existing project that cannot rewrite its logging in one go. It is the wrong choice for a new project, because the standard library log/slog package exists and the README says so itself.
Can I use it commercially?
Check first. The repository uses a licence we do not classify automatically, so read its LICENSE file before any commercial use.
Is it still maintained?
Yes. The repository last received commits 165 days 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 22, 2026, and from our analysis. They are not legal advice.

Editorial analysis

A logger built around a small Handler interface

log15 provides an opinionated, simple toolkit for best-practice logging in Go that is both human and machine readable. The README says it is modeled after the standard library's io and net/http packages and is an alternative to the standard library's log package.

Those two models are the design. The io model gives you small composable pieces, and the net/http model gives you a handler that decides what happens to a record, which you can swap at runtime. The Logger type holds a handler, and the handler is where the interesting choices live: a StreamHandler for stderr, a FileHandler for a file, a LvlFilterHandler to select a minimum level, a MultiHandler to fork records to several destinations.

The example in the README shows the whole shape in a few lines:

go
srvlog.SetHandler(log.MultiHandler(
    log.StreamHandler(os.Stderr, log.LogfmtFormat()),
    log.LvlFilterHandler(
        log.LvlError,
        log.Must.FileHandler("errors.json", log.JsonFormat()))))

One logger, writing human readable logfmt to the terminal and JSON errors to a file, with a level filter in between. Because the interface is small, the README lists a simple Handler interface as a feature, described as allowing construction of flexible custom logging configurations with a tiny API.

The output is logfmt, which is worth a look because it explains the design:

code
WARN[06-17|21:58:10] abnormal conn rate                       module=app/server rate=0.500 low=0.100 high=0.800
INFO[06-17|21:58:10] connection open                          module=app/server raddr=10.0.0.1

Human readable at a glance, and parseable by a machine because the key/value pairs are structured rather than interpolated into a string.

Child loggers carry context forward

The feature list names child loggers which inherit and add their own private context, and the example shows how it is used.

go
srvlog := log.New("module", "app/server")

srvlog.Warn("abnormal conn rate", "rate", curRate, "low", lowRate, "high", highRate)

connlog := srvlog.New("raddr", c.RemoteAddr())
connlog.Info("connection open")

The parent logger carries module=app/server. The child inherits that and adds raddr, so every message from the child carries both. The result is that a single request handler can create a child logger with the connection address in it and every subsequent line in that scope is tagged, without each call site repeating the module.

This is the part of the API that most directly encourages structured logging, which the feature list describes as promoting structured logging by encouraging use of key/value pairs. Because the context is carried in the logger rather than passed at each call, a developer who creates a child logger has already made the call site structured.

The second thing in the example is lazy evaluation, which is the other feature worth calling out. The logger offers a Lazy wrapper:

go
connlog.Debug("ping remote", "latency", log.Lazy{pingRemote})

The wrapped function is only called when the record is actually going to be written. A debug line that formats an expensive value therefore costs nothing when the level filter would have discarded it, which is a real saving in a hot path and one of the reasons log15 was written the way it was.

Version 3 removed things, and the README says to consider slog

The v3 upgrade is described as a version that compiles with Go modules and provides some basic simplifications, and the removals are specific.

The Call property was removed from a log15.Record for performance, and with it went the CallerStackHandler, CallerFuncHandler and CallerFileHandler, since all three relied on Call. The term subpackage was removed, with the reason given as there being multiple better replacements available now. The migration for most code is a one-line import path change:

patch
- import log "github.com/inconshreveable/log15"
+ import log "github.com/inconshreveable/log15/v3"

Then there is the honest part. The README says a lot has happened since log15 was released and that you may find the log/slog package in the standard library to be a better fit for your use case. That sentence is the most important line in the document for anyone deciding whether to start a new project on this library.

Development of the v3 branch happens in the v3 subdirectory, and the repository carries both trees at once, with compat/ providing a compatibility layer so existing code can use the v3 library and migrate incrementally. compat.CompatHandler takes a v3 Handler and returns a legacy log15 Handler, and compat.CompatLogger does the same with the Logger type. That is the difference between a v2 to v3 upgrade being a weekend and being a quarter.

The versioning policy is blunt: the API of the master branch should always be considered unstable, and if you want to rely on a stable API you must vendor the library. That is an unusual requirement for a Go package, and it is a direct consequence of the project predating Go modules.

The varargs style is brittle, and Ctx is the answer

The FAQ at the end of the README is unusually candid about a flaw in the API. The heading is a complaint from a user: the varargs style is brittle and error prone, can I have type safety please. The answer is yes, use log.Ctx.

go
srvlog := log.New(log.Ctx{"module": "app/server"})
srvlog.Warn("abnormal conn rate", log.Ctx{"rate": curRate, "low": lowRate, "high": highRate})

The difference is that a map is passed as a single argument rather than as a sequence of key, value pairs. With varargs, a call that puts a value where a key belongs compiles and produces a log line with an odd field, and a key without a value shifts every subsequent pair. With Ctx, the type of the argument is a map and the mistake is a compile error rather than a corrupted log.

The trade is that the map form allocates on every call, and the varargs form exists partly because it is cheaper. The README treats Ctx as the answer to the type safety question without discussing the allocation cost, so if you are in a hot path the decision is yours to make.

The repository also tracks the API breaks explicitly, which is helpful when you are reading an old dependency. The Breaking API Changes section lists the specific commits that broke stability: one that added a Get method to the Logger interface to retrieve the current handler, one that changed the Record field Call to stack.Call with a switch to the go-stack/stack package, and one that restored the syslog.Priority argument to the SyslogXxx handler constructors.

Handlers, formats and the output destinations

The feature list covers the output side in one line, and it is a long list: built-in support for logging to files, streams, syslog and the network, plus support for forking records to multiple handlers, buffering records for output and failing over from failed handler writes.

The formats are the LogfmtFormat and JsonFormat used in the example, and the two are chosen at handler construction time rather than globally, which is what lets the same logger emit logfmt to a terminal and JSON to a file. A JsonFormat handler is what you want behind a log shipper, and LogfmtFormat is what you want when a human is reading the console.

The failover behaviour is the feature that most affects reliability. When a handler write fails, a failover handler can take over, so a full disk on one destination does not silently lose the records that were meant for it. Combined with buffering, that is the difference between a logger that drops lines under load and one that does not.

The repository structure reflects the design. handler.go, handler_go13.go and handler_go14.go hold the handler implementations, format.go the formats, logger.go the Logger type, root.go the root logger, syslog.go the syslog support, and there are ext/ and term/ directories, with the term subpackage being the one removed in v3. The go.mod declares Go 1.24.0 and depends on go-stack/stack 1.8.1 and golang.org/x/term 0.40.0, with golang.org/x/sys as an indirect dependency.

Licence, maintenance and what to check before adopting

The licence question has a wrinkle worth naming. The README's licence section says Apache, and there is a LICENSE file at the top of the repository, but GitHub's own classification of the repository licence is a custom one it cannot categorise. So the terms are Apache in the project's own statement, and the file is there to confirm it.

Maintenance is thin. The last push was on 2026-05-13, the repository is not archived, and there are no GitHub releases, so there is no tagged version to depend on and no changelog of releases to read. The repository does have a CHANGELOG.md, a CONTRIBUTORS file generated from a mailmap, and a Makefile whose only target regenerates that file with the write_mailmap tool.

For a new project, the calculation is straightforward. The standard library now has log/slog, the README of the library you are considering says that slog may be a better fit, and adopting a pre-slog structured logger means taking on a migration later rather than now. The argument for log15 is mostly about existing code.

For existing code, the situation is different and better. The v3 branch is a drop-in import path change for most code, the compat package lets old and new coexist while you migrate, and the handler interface is small enough that the pieces you already rely on, StreamHandler, FileHandler, LvlFilterHandler and MultiHandler, exist in both trees. If your project already depends on log15, the question is not whether to move but how much churn to accept now, and the removal of the term subpackage and the caller handlers is the part to plan around.

Editorial conclusion

log15 suits a Go codebase that already depends on it, or that needs a handler interface small enough to implement in a few lines, and the v3 branch plus the compat package make it a reasonable choice for an existing project that cannot rewrite its logging in one go. It is the wrong choice for a new project, because the standard library log/slog package exists and the README says so itself. Before starting a new logger, read the v3 upgrade notes, since the Call property, the caller handlers and the term subpackage were all removed, and if you are migrating check the compat package that lets v2 and v3 code coexist.

Frequently asked questions

What is log15?

log15 is a Go package providing an opinionated, simple toolkit for best-practice logging that is both human and machine readable, modeled after the standard library's io and net/http packages and positioned as an alternative to the log package.

What changed in log15 v3?

The Call property was removed from a log15.Record for performance, and the CallerStackHandler, CallerFuncHandler and CallerFileHandler went with it. The term subpackage was also removed. For most code the migration is only an import path change to github.com/inconshreveable/log15/v3.

How do I use log15 with type safe context?

The README's FAQ recommends log.Ctx, which takes a map instead of a sequence of key and value pairs, so a mismatched argument becomes a compile error. You use it both for the logger context, log.New(log.Ctx{"module": "app/server"}), and for the message context.

Can I migrate from log15 v2 to v3 incrementally?

Yes. A compat package exists for this. compat.CompatHandler takes a v3 Handler and returns a legacy log15 Handler, and compat.CompatLogger does the same for the Logger type, so both versions can coexist during a migration.

What licence does log15 use?

The README's licence section says Apache, and a LICENSE file is present at the top of the repository, although GitHub classifies the repository licence as a custom one it cannot categorise. The last push was on 2026-05-13 and the project has no GitHub releases.

Official sources

  1. inconshreveable/log15 on GitHub
  2. Issues
  3. Project website
  4. README
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.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/inconshreveable-log15.svg)](https://hysenlabs.com/projects/inconshreveable-log15)

Was this useful?