ℹ️ v1.9 adds production-hardening sink wrappers (
redact_event_sink,sample_event_sink,consistent_sample_event_sink,rate_limit_event_sink,batch_event_sink), pluserror_withandlog_at_most.ℹ️ v1.8 adds trace correlation (
with_trace,set_trace,current_trace), theOtlpJsonformat withformat_event_otlp, and OpenTelemetry resource attributes (set_resource,get_resource).ℹ️ v1.7 adds
FList/FMap/FNullvariants,list/map/nullconstructors,vstr/vint/vfloat/vbool/vnullraw helpers, native typed JSON output, and publicformat_event_*helpers.ℹ️ v1.6 adds
childloggers,filter_event_sink, publicemit(LogEvent), and publiclevel_to_int. No breaking changes.ℹ️ 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.ℹ️ 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 fromList(#(String, String))toList(#(String, FieldValue)). See docs/migration_v1_3.md.
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.
gleam add woofimportwoofpubfnmain(){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-primary
No setup, no builder chains, no ceremony.
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),])Lists, nested objects, and explicit null pass through as typed values:
woof.info("order",[woof.str("id","ORD-42"),woof.list("items",[woof.vstr("widget"),woof.vstr("gadget")]),woof.map("address",[#("city",woof.vstr("Bologna")),#("zip",woof.vstr("40121")),]),woof.null("coupon"),])JSON output emits real types: "items":["widget","gadget"], "address":{...},
"coupon":null. Numbers are numbers, booleans are booleans.
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)letassert[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")),])pubfnmain(){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 OTPPass a fixed set of fields through a logger instance - no global state needed:
letdb=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"Build hierarchies with child (inherits parent context, dot-joined namespace):
lethttp=woof.new("http")letrouter=woof.child(http,"router")// namespace: "http.router"Ideal for JS async code where global context is unreliable.
Wrap a sink with a predicate to send only matching events somewhere:
woof.set_event_sink(woof.filter_event_sink(fn(e){woof.level_to_int(e.level)>=woof.level_to_int(woof.Error)},pagerduty_sink,))Redact secrets, cap volume, and batch deliveries by composing sink wrappers - redact first, batch last:
woof.set_event_sink(otlp_http_sink|>woof.redact_event_sink(["password","authorization"])|>woof.rate_limit_event_sink(2000)|>woof.consistent_sample_event_sink(0.1,"trace_id",woof.Error)|>woof.batch_event_sink(200,5000),)error_with and log_at_most cover two more common cases: a structured
error field, and capping a noisy log line without an if at every call
site.
woof.error_with("payment failed","timeout",[woof.str("order_id","O1")])woof.log_at_most(5,"db_retry_failed",woof.Warning,"retry failed",[])See docs/production_setup.md for the reasoning behind the composition order and docs/guide.md for the full reference.
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()pubfnmain(){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 LevelTie logs to a distributed trace. Inside with_trace, every log carries
trace_id and span_id:
woof.with_trace(trace_id,span_id,fn(){woof.info("payment captured",[woof.int("amount_cents",4200)])})Set OpenTelemetry resource attributes once at startup, then switch the format
to OtlpJson for an OpenTelemetry-shaped object per line:
woof.set_resource([woof.str("service.name","checkout"),woof.str("service.version","1.8.0"),])woof.configure(woof.Config(woof.Info,woof.OtlpJson,woof.Never)){"timestamp_unix_nano":1779000000000000000,"severity_number":9,"severity_text":"INFO","body":"payment captured","trace_id":"...","span_id":"...","resource":{"service.name":"checkout"},"attributes":{"amount_cents":4200}}| Document | Contents |
|---|---|
| docs/guide.md | Full reference: levels, formats, sinks, context, BEAM integration, API table |
| docs/production_setup.md | Composing v1.9 sink wrappers for production |
| docs/sink_composition.md | Why sink wrapper order matters |
| docs/benchmarks.md | Measured overhead of each sink wrapper |
| docs/semantic_conventions.md | Standard field names for queryable logs |
| docs/log_levels.md | Choosing the right log level |
| docs/migration_v1_3.md | Upgrading from v1.2 - what changed and how to fix it |
| ROADMAP.md | Future releases v1.9 to v2.0 |
| CHANGELOG.md | Release history |
| hexdocs.pm/woof | Generated module reference |
- Gleam 1.14 or newer
- OTP 22+ on the BEAM (CI uses OTP 28)
gleam_stdlibthe only dependency
Made with Gleam 💜
