ℹ️ v1.5 adds instanced logger context,
inspect,tap_time,level_from_string,set_level_from_env,get_level,append_context, and soft deprecation warnings for legacy field helpers. No breaking changes.ℹ️ v1.4 adds 4 new OTP levels (
Notice,Critical,Alert,Emergency),beam_event_sink, multi-sink dispatch, anddev()/prod()presets.⚠️ v1.3 breaking change: fields changed from
List(#(String, String))toList(#(String, FieldValue)). See docs/migration_v1_3.md.
woof
A straightforward logging library for Gleam.
Dedicated to Echo, my dog.
woof gets out of your way: import it, call info(...), and you're done.
Structured fields, namespaces, scoped context, typed events - all there
when you need them, invisible when you don't.
Install
gleam add woofQuick start
import woof
pub fn main() {
woof.info("Server started", [woof.str("host", "0.0.0.0"), woof.int("port", 3000)])
woof.warning("Cache almost full", [woof.int("usage_pct", 92)])
woof.error("Connection lost", [woof.str("host", "db-primary")])
}[INFO] 10:30:45 Server started
host: 0.0.0.0
port: 3000
[WARN] 10:30:46 Cache almost full
usage_pct: 92
[ERROR] 10:30:47 Connection lost
host: db-primaryNo setup, no builder chains, no ceremony.
Typed fields
Fields carry their original Gleam types through the entire pipeline. Pattern-match on them in event sinks, assert on them in tests.
woof.info("Payment processed", [
woof.str("order_id", "ORD-42"),
woof.int("amount_cents", 4999),
woof.float("tax_rate", 8.5),
woof.bool("express", True),
])Testing capture typed events
let #(sink, get) = woof.test_sink()
woof.set_sink(woof.silent_sink)
woof.set_event_sink(sink)
process_payment(order_id: "ORD-99", amount: 0)
let assert [event] = get()
event.level |> should.equal(woof.Error)
event.message |> should.equal("Payment rejected")
event.fields |> should.equal([
#("order_id", woof.FString("ORD-99")),
#("reason", woof.FString("zero amount")),
])One-call setup
pub fn main() {
woof.dev() // Debug level, Text format, colors Auto, stdout
// - or -
woof.prod() // Info level, Json format, OTP logger
}Or wire up sinks explicitly:
woof.set_sinks([woof.beam_logger_sink, my_metrics_sink])
woof.set_event_sink(woof.beam_event_sink) // structured typed fields to OTPInstanced loggers with context
Pass a fixed set of fields through a logger instance - no global state needed:
let db = woof.new("database") |> woof.set_context([woof.str("component", "db")])
db |> woof.log(woof.Info, "Connected", [woof.str("host", "localhost")])
// → namespace: "database", fields: component="db", host="localhost"Ideal for JS async code where global context is unreliable.
Debugging helpers
fetch_user(id)
|> woof.inspect("user") // logs string repr at Debug, passes value through
|> woof.tap_time("after_fetch") // logs monotonic_ms as Int field at Debug
|> transform()Level from environment variable
pub fn main() {
let _ = woof.set_level_from_env("LOG_LEVEL") // reads LOG_LEVEL, falls back silently
// ...
}LOG_LEVEL=warning ./my_app # sets Warning level at startupParse or inspect the level anywhere:
woof.level_from_string("critical") // Ok(Critical)
woof.get_level() // current LevelDocumentation
| Document | Contents |
|---|---|
| docs/guide.md | Full reference: levels, formats, sinks, context, BEAM integration, API table |
| docs/migration_v1_3.md | Upgrading from v1.2 - what changed and how to fix it |
| CHANGELOG.md | Release history |
| hexdocs.pm/woof | Generated module reference |
Requirements
- Gleam 1.14 or newer
- OTP 22+ on the BEAM (CI uses OTP 28)
gleam_stdlibthe only dependency
Made with Gleam 💜