Go
github.com/infinitetoken/lumber-go is Lumber's Go client. It encrypts every log with AES-256-GCM before it leaves your process, has no dependencies beyond the standard library, and plugs into log/slog, so code that already logs with slog can send to Lumber.
Install
go get github.com/infinitetoken/lumber-go
Go 1.22 or later.
Quick start
import lumber "github.com/infinitetoken/lumber-go"
client, err := lumber.New(os.Getenv("LUMBER_API_KEY"))
if err != nil {
log.Fatal(err)
}
defer client.Close(context.Background()) // sends what's still queued
client.Info("Server started", "port", 8080)
client.Warn("Rate limit approaching", "request_id", "req-123", "user_id", 456)
client.Error("Payment failed", "trace_id", "trace-abc", "order_id", 789, "err", err)
The arguments after the message are slog's: alternating keys and values, or slog.Attr. Logs are sent from a background goroutine, so logging never waits on the network. Call Close before your program exits, or the logs still queued are lost; give its context a deadline if exiting must not wait long.
The API key comes from the Lumber app (see Getting Started). Keep it in an environment variable or your secrets manager, never in source control, and never in an app or game you ship to people (why).
With log/slog
client.Handler is a slog.Handler:
slog.SetDefault(slog.New(client.Handler(&lumber.HandlerOptions{Level: slog.LevelInfo, AddSource: true})))
slog.Info("Report ready", "request_id", "req-123", "rows", 1500, "took", 2400*time.Millisecond)
logger := slog.Default().With("service", "billing").WithGroup("job")
logger.Error("Job failed", "id", 42, "err", err)
logger.Log(ctx, lumber.LevelFatal, "Database unreachable")
Each record becomes a log at the matching level: from slog.LevelError up is ERROR, and from lumber.LevelFatal (12) up is FATAL.
- Attributes go into the log's metadata, with groups as nested objects.
- Three are special at the top level:
request_idandtrace_id(orrequestIdandtraceId) are the log's own fields, and acontextgroup or map goes into its context. - An error is sent as its message.
AddSourcesends the file, line and function that logged in the context.Leveldefaults toslog.LevelInfo.
Log methods
| Method | Level |
|---|---|
Debug(msg, args...) | DEBUG |
Info(msg, args...) | INFO |
Warn(msg, args...) | WARN |
Error(msg, args...) | ERROR |
Fatal(msg, args...) | FATAL. Unlike log.Fatal, it doesn't exit |
Flush(ctx) | Waits for queued logs to send, or for ctx |
Close(ctx) | Sends what's queued, waiting for ctx at most, and stops |
Options
client, err := lumber.New(os.Getenv("LUMBER_API_KEY"),
lumber.WithEnvironment("production"), // sent as `env` in every log's context
lumber.WithCaller(0), // send the file, line and function of the client's own calls; 1 skips a wrapper
lumber.WithoutAutoContext(), // don't send the process id and hostname
lumber.WithTimeout(10*time.Second), // how long to wait for Lumber to answer one log
lumber.WithMaxQueue(10_000), // logs waiting to send before new ones are dropped
lumber.WithHTTPClient(httpClient), // your own http.Client, for a proxy or custom TLS
lumber.WithWarnings(func(msg string) { /* your logger */ }),
)
Delivery
Logs are sent in order, one POST /logs per log, over a connection kept open between logs.
- A network error or a 5xx counts as a failed attempt. Each log gets 5 attempts, waiting 1, 2, 4, then 8 seconds between them, and is then dropped with a warning.
- A log Lumber refuses (a 4xx, such as an invalid API key) is dropped at once with a warning, since sending it again won't help.
- When your team has sent its plan's logs for the month, Lumber answers 429: the client warns once and drops logs until the month turns.
- While Lumber can't be reached, up to
WithMaxQueuelogs wait; past that, new ones are dropped.
Warnings go to standard error unless you pass WithWarnings.
Source
The client is open source: lumber-go on GitHub, with its reference on pkg.go.dev. It's tested against the same known answers for encryption and the wire format as Lumber's other clients.