woof
Types
Controls whether ANSI colors are used in Text output.
pub type ColorMode {
Auto
Always
Never
}
Constructors
-
AutoAuto-detect: colors are enabled when stdout is a TTY and the
NO_COLORenvironment variable is not set. -
AlwaysAlways emit ANSI color codes, even when piped to a file.
-
NeverNever emit ANSI color codes.
A fully resolved log entry, ready to be formatted.
Field values have been serialised to strings at this point.
This type is public so that Custom formatters can pattern-match on it
and arrange the data however they like.
pub type Entry {
Entry(
level: Level,
message: String,
fields: List(#(String, String)),
namespace: option.Option(String),
timestamp: String,
)
}
Constructors
-
Entry( level: Level, message: String, fields: List(#(String, String)), namespace: option.Option(String), timestamp: String, )
A typed sink receives a LogEvent with the original FieldValue types
intact. Register one with set_event_sink. Use test_sink to build a
capture sink for assertions in tests.
pub type EventSink =
fn(LogEvent) -> Nil
A typed log field value.
Use the helper constructors (woof.str, woof.int, woof.float,
woof.bool) to build values rather than constructing these directly.
The type is public so that sinks and tests can pattern-match on it.
pub type FieldValue {
FString(String)
FInt(Int)
FFloat(Float)
FBool(Bool)
}
Constructors
-
FString(String) -
FInt(Int) -
FFloat(Float) -
FBool(Bool)
Controls how log output is formatted.
Text- human-readable lines, great for development.Json- one JSON object per line, great for production and log aggregation tools.Compact- single-line, key=value pairs. A middle ground betweenTextreadability andJsonparsability.Custom- bring your own formatter. The function receives a fully assembledEntryand must return the string to print. This is the escape hatch for integrating with other formatting or output libraries.
pub type Format {
Text
Json
Compact
Custom(formatter: fn(Entry) -> String)
}
Constructors
-
Text -
Json -
Compact -
Custom(formatter: fn(Entry) -> String)
Log severity levels, ordered from least to most severe.
Only messages at or above the configured minimum level are emitted.
The default level is Debug (everything is printed).
The eight levels mirror the OTP logger / syslog severity scale:
| Level | Use for |
|---|---|
Debug | Development traces |
Info | Normal application flow |
Notice | Significant business events (not errors) |
Warning | Anomalous but recoverable situations |
Error | Failures requiring attention |
Critical | System degradation, partial failure |
Alert | Immediate action required |
Emergency | System is unusable |
pub type Level {
Debug
Info
Notice
Warning
Error
Critical
Alert
Emergency
}
Constructors
-
Debug -
Info -
Notice -
Warning -
Error -
Critical -
Alert -
Emergency
A fully typed log event, delivered to every registered EventSink.
Unlike Entry, fields here carry their original Gleam types - no
information is lost before the sink decides how to format or route
the event. Use test_sink to capture LogEvents in tests.
pub type LogEvent {
LogEvent(
level: Level,
message: String,
fields: List(#(String, FieldValue)),
timestamp: String,
namespace: option.Option(String),
)
}
Constructors
-
LogEvent( level: Level, message: String, fields: List(#(String, FieldValue)), timestamp: String, namespace: option.Option(String), )
A namespaced logger that optionally carries its own per-instance context.
Create with woof.new("name"). Attach context with woof.set_context.
Context fields appear after global and scoped context, before inline fields.
let db = woof.new("database") |> woof.set_context([woof.str("pool", "ro")])
db |> woof.log(woof.Info, "query ok", [woof.int("ms", 12)])
pub opaque type Logger
A legacy sink receives both the resolved Entry and the pre-formatted
string produced by the active Format. Field values are serialised to
strings before this point, so the types are not available here.
For full type fidelity use an EventSink via set_event_sink.
pub type Sink =
fn(Entry, String) -> Nil
Values
pub fn alert(
message: String,
fields: List(#(String, FieldValue)),
) -> Nil
Log at Alert level. Use when automatic action is insufficient and a human must intervene right away.
pub fn alert_lazy(
build: fn() -> String,
fields: List(#(String, FieldValue)),
) -> Nil
Log at Alert level, evaluating the message only if Alert is enabled.
pub fn append_context(
logger: Logger,
fields: List(#(String, FieldValue)),
) -> Logger
Append fields to a logger’s instance context without replacing it.
Unlike set_context, which replaces the entire context, append_context
adds fields at the end of the existing list. Returns a new Logger.
let base = woof.new("api") |> woof.set_context([woof.str("service", "api")])
let req = base |> woof.append_context([woof.str("request_id", id)])
pub fn append_global_context(
fields: List(#(String, FieldValue)),
) -> Nil
Append fields to the global context without replacing the existing ones.
pub fn beam_event_sink(event: LogEvent) -> Nil
A typed event sink that routes log events through the OTP logger with
fully structured metadata. Unlike beam_logger_sink, field values
are passed as native Erlang terms (integer(), float(), boolean(),
binary()) rather than pre-formatted strings.
Register alongside or instead of the legacy beam_logger_sink:
woof.set_event_sink(woof.beam_event_sink)
On the JavaScript target the event is routed to the level-appropriate
console method, same as beam_logger_sink.
pub fn beam_logger_sink(entry: Entry, formatted: String) -> Nil
A sink that routes log events through the official OTP logging pipeline.
On the BEAM target each event is delivered to OTP’s logger module
(available since OTP 21), so the entire BEAM ecosystem can observe,
filter, and re-route woof messages:
- Applications that use woof no longer need a second logging system.
- Libraries that depend on woof can be silenced by the host application.
- BEAM logger handlers (Loki, Datadog, etc.) receive woof events.
- OTP performance features apply: async dispatch, load-shedding, etc.
Each event is tagged with domain => [woof] so handlers and filters
can target woof output specifically:
%% Silence all woof output in a specific environment:
logger:add_primary_filter(no_woof,
{fun logger_filters:domain/2, {stop, sub, [woof]}}).
On the JavaScript target the event is passed to the level-appropriate
console method (console.debug, console.info, console.warn, or
console.error) - the JS equivalent of routing by severity.
Usage
Call once at application startup, before any logging:
pub fn main() {
woof.set_sink(woof.beam_logger_sink)
// ... rest of startup
}
pub fn bool(key: String, value: Bool) -> #(String, FieldValue)
Create a boolean field. Renders as "true" / "false" (lowercase) in
the legacy string path.
woof.info("auth", [woof.bool("cached", True)])
pub fn bool_field(
key: String,
value: Bool,
) -> #(String, FieldValue)
Deprecated: Use woof.bool instead
Create a field from a Bool.
Prefer woof.bool — this alias is kept for backwards compatibility.
pub fn configure(config: Config) -> Nil
Replace the current configuration.
This sets level, format, and color mode at once. Global context is
left untouched - use set_global_context if you need to change it.
pub fn critical(
message: String,
fields: List(#(String, FieldValue)),
) -> Nil
Log at Critical level. Use when the system is partially degraded and immediate investigation is required.
pub fn critical_lazy(
build: fn() -> String,
fields: List(#(String, FieldValue)),
) -> Nil
Log at Critical level, evaluating the message only if Critical is enabled.
pub fn debug(
message: String,
fields: List(#(String, FieldValue)),
) -> Nil
Log at Debug level.
pub fn debug_lazy(
build: fn() -> String,
fields: List(#(String, FieldValue)),
) -> Nil
Log at Debug level, evaluating the message only if Debug is enabled.
Use this when building the message string is expensive.
pub fn default_sink(entry: Entry, formatted: String) -> Nil
The default sink - prints the formatted log line to standard output.
This is the out-of-the-box behaviour: zero configuration, beautiful output on any terminal. Useful when building a custom sink that still wants to write to stdout.
See beam_logger_sink for the OTP-integrated alternative.
pub fn dev() -> Nil
Configure woof for development: Debug level, Text format, Auto
colors, stdout output. Clears any previously registered sinks.
pub fn emergency(
message: String,
fields: List(#(String, FieldValue)),
) -> Nil
Log at Emergency level. Use when the system is completely unusable.
pub fn emergency_lazy(
build: fn() -> String,
fields: List(#(String, FieldValue)),
) -> Nil
Log at Emergency level, evaluating the message only if Emergency is enabled.
pub fn error(
message: String,
fields: List(#(String, FieldValue)),
) -> Nil
Log at Error level.
pub fn error_lazy(
build: fn() -> String,
fields: List(#(String, FieldValue)),
) -> Nil
Log at Error level, evaluating the message only if Error is enabled.
pub fn field(key: String, value: String) -> #(String, FieldValue)
Deprecated: Use woof.str instead
Create a string field.
Prefer woof.str — this alias is kept for backwards compatibility.
pub fn float(key: String, value: Float) -> #(String, FieldValue)
Create a float field.
woof.info("timing", [woof.float("ms", 12.4)])
pub fn float_field(
key: String,
value: Float,
) -> #(String, FieldValue)
Deprecated: Use woof.float instead
Create a field from a Float.
Prefer woof.float — this alias is kept for backwards compatibility.
pub fn format(entry: Entry, output_format: Format) -> String
Format an Entry without emitting it.
Handy for testing formatter output or building custom sink wrappers.
Directly constructs Entry values with string fields for full control.
pub fn get_global_context() -> List(#(String, FieldValue))
Get the current global context fields.
pub fn info(
message: String,
fields: List(#(String, FieldValue)),
) -> Nil
Log at Info level.
pub fn info_lazy(
build: fn() -> String,
fields: List(#(String, FieldValue)),
) -> Nil
Log at Info level, evaluating the message only if Info is enabled.
pub fn inspect(value: a, label: String) -> a
Log the string representation of a value at Debug level and pass it through.
Useful for inspecting intermediate values in pipelines without breaking
the chain. The value is rendered via string.inspect.
compute()
|> woof.inspect("result before filter")
|> list.filter(fn(x) { x > 0 })
pub fn int(key: String, value: Int) -> #(String, FieldValue)
Create an integer field. The value is preserved as FInt through the
entire pipeline and only serialised to a string by the legacy sink.
woof.info("request", [woof.int("status", 200)])
pub fn int_field(
key: String,
value: Int,
) -> #(String, FieldValue)
Deprecated: Use woof.int instead
Create a field from an Int.
Prefer woof.int — this alias is kept for backwards compatibility.
pub fn is_enabled(level: Level) -> Bool
Check if a specific log level is currently enabled.
Useful if you need to perform expensive work before emitting several log messages, and want to skip that work if the level is silenced.
pub fn level_from_string(s: String) -> Result(Level, Nil)
Parse a level name string (case-insensitive) into a Level.
Accepts the eight OTP level names: "debug", "info", "notice",
"warning", "error", "critical", "alert", "emergency".
Returns Error(Nil) for any unrecognised string.
woof.level_from_string("warning") // Ok(Warning)
woof.level_from_string("WARN") // Error(Nil)
pub fn level_name(level: Level) -> String
Return the lowercase name of a level.
Useful inside Custom formatters.
pub fn log(
logger: Logger,
level: Level,
message: String,
fields: List(#(String, FieldValue)),
) -> Nil
Log a message through a namespaced logger.
Logger context fields (set via set_context) are prepended to inline fields.
pub fn log_error(
res: Result(a, b),
message: String,
fields: List(#(String, FieldValue)),
) -> Result(a, b)
If the Result is Error, log the message at Error level and pass
the original value through - useful in result pipelines.
pub fn new(namespace: String) -> Logger
Create a namespaced logger.
The namespace is prepended to every message formatted with Text and
included as a "ns" field in Json output.
pub fn notice(
message: String,
fields: List(#(String, FieldValue)),
) -> Nil
Log at Notice level. Use for significant business events that are not errors - successful deployments, config reloads, scheduled task completions.
pub fn notice_lazy(
build: fn() -> String,
fields: List(#(String, FieldValue)),
) -> Nil
Log at Notice level, evaluating the message only if Notice is enabled.
pub fn prod() -> Nil
Configure woof for production: Info level, Json format, no
colors, OTP logger output via beam_logger_sink. Clears any previously
registered sinks.
pub fn set_colors(mode: ColorMode) -> Nil
Change whether text logs use ANSI colors. (Json/Compact formats ignore this setting.)
pub fn set_context(
logger: Logger,
fields: List(#(String, FieldValue)),
) -> Logger
Attach per-instance context fields to a logger.
Context fields appear after global and scoped context, before inline fields.
Each call to set_context replaces the previous context.
let db = woof.new("database") |> woof.set_context([woof.str("pool", "ro")])
pub fn set_event_sink(sink: fn(LogEvent) -> Nil) -> Nil
Register a typed event sink.
The sink receives a LogEvent with fields as FieldValue - types are
preserved through the entire pipeline. The legacy Sink (if any) is
called independently; both are active simultaneously.
Use test_sink to get a capture sink for tests.
pub fn set_global_context(
fields: List(#(String, FieldValue)),
) -> Nil
Set fields that appear on every log message globally.
Typically called once at application start.
pub fn set_level(level: Level) -> Nil
Set the minimum log level.
Messages below this level are silently dropped with near-zero overhead.
pub fn set_level_from_env(var: String) -> Result(Nil, Nil)
Read a log level from an environment variable and apply it.
Returns Ok(Nil) if the variable is set and its value is a recognised
level name (case-insensitive). Returns Error(Nil) if the variable is
absent or its value is not a valid level name; in that case the current
level is unchanged.
// In your application startup:
let _ = woof.set_level_from_env("LOG_LEVEL")
pub fn set_sink(sink: fn(Entry, String) -> Nil) -> Nil
Set the legacy sink function used to emit formatted logs.
Replaces all registered sinks with the single given sink. The legacy sink
receives an Entry (with string-serialised fields) and the pre-formatted
string. For the original FieldValue types use set_event_sink instead.
Equivalent to set_sinks([sink]).
pub fn set_sinks(sinks: List(fn(Entry, String) -> Nil)) -> Nil
Register multiple legacy sinks. Every registered sink is called in order for each emitted log event. Replaces any previously registered sinks.
woof.set_sinks([woof.default_sink, my_datadog_sink])
pub fn silent_sink(entry: Entry, formatted: String) -> Nil
A sink that does nothing and discards all log events.
Useful for muting logs entirely, for example during test runs:
woof.set_sink(woof.silent_sink)
pub fn str(key: String, value: String) -> #(String, FieldValue)
Create a string field.
woof.info("user login", [woof.str("method", "oauth")])
pub fn tap_alert(
value: a,
message: String,
fields: List(#(String, FieldValue)),
) -> a
Log the value at Alert level and pass it through.
pub fn tap_critical(
value: a,
message: String,
fields: List(#(String, FieldValue)),
) -> a
Log the value at Critical level and pass it through.
pub fn tap_debug(
value: a,
message: String,
fields: List(#(String, FieldValue)),
) -> a
Log the value at Debug level and pass it through.
pub fn tap_emergency(
value: a,
message: String,
fields: List(#(String, FieldValue)),
) -> a
Log the value at Emergency level and pass it through.
pub fn tap_error(
value: a,
message: String,
fields: List(#(String, FieldValue)),
) -> a
Log the value at Error level and pass it through.
pub fn tap_info(
value: a,
message: String,
fields: List(#(String, FieldValue)),
) -> a
Log the value at Info level and pass it through. Fits naturally in pipelines.
pub fn tap_notice(
value: a,
message: String,
fields: List(#(String, FieldValue)),
) -> a
Log the value at Notice level and pass it through.
pub fn tap_time(value: a, label: String) -> a
Log the current monotonic timestamp at Debug level and pass the value through.
Insert at multiple points in a pipeline to measure elapsed time between steps.
Each call emits a monotonic_ms field — diff adjacent values for duration.
fetch_data()
|> woof.tap_time("after fetch")
|> transform()
|> woof.tap_time("after transform")
pub fn tap_warning(
value: a,
message: String,
fields: List(#(String, FieldValue)),
) -> a
Log the value at Warning level and pass it through.
pub fn test_sink() -> #(
fn(LogEvent) -> Nil,
fn() -> List(LogEvent),
)
Build a capture sink for use in tests.
Returns a pair of #(sink, get):
sinkis anEventSinkto register withset_event_sink.getreads and clears the capturedLogEventlist.
let #(sink, get) = woof.test_sink()
woof.set_event_sink(sink)
woof.error("boom", [woof.int("code", 500)])
let assert [event] = get()
event.level |> should.equal(woof.Error)
event.message |> should.equal("boom")
event.fields |> should.equal([#("code", woof.FInt(500))])
pub fn time(label: String, body: fn() -> a) -> a
Measure how long body takes and log it at Info level.
Returns whatever body returns - the timing log is a side effect.
pub fn warning(
message: String,
fields: List(#(String, FieldValue)),
) -> Nil
Log at Warning level.
pub fn warning_lazy(
build: fn() -> String,
fields: List(#(String, FieldValue)),
) -> Nil
Log at Warning level, evaluating the message only if Warning is enabled.
pub fn with_context(
fields: List(#(String, FieldValue)),
body: fn() -> a,
) -> a
Run body with extra fields attached to every log call inside it.
Fields from the context are merged with inline fields. If a key appears in both, the inline value wins (it comes last in the list).
Contexts can be nested - inner fields accumulate on top of outer ones.
On the BEAM each process gets its own context (process dictionary), so concurrent request handlers never interfere with each other.
Notice for JavaScript async users: On the JavaScript target, because
JS uses cooperative concurrency and is single-threaded, with_context
modifies a global state. If your callback enters an async sleep/promise,
the context might be overwritten by other concurrent tasks. Use with
caution in highly concurrent async Node/Deno servers.