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).
pub type Level {
Debug
Info
Warning
Error
}
Constructors
-
Debug -
Info -
Warning -
Error
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 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 append_global_context(
fields: List(#(String, FieldValue)),
) -> Nil
Append fields to the global context without replacing the existing ones.
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)
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 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 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)
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)
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 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)
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_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.
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 set_colors(mode: ColorMode) -> Nil
Change whether text logs use ANSI colors. (Json/Compact formats ignore this setting.)
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_sink(sink: fn(Entry, String) -> Nil) -> Nil
Set the legacy sink function used to emit formatted logs.
The legacy sink receives an Entry (with string-serialised fields) and
the pre-formatted string. If you need the original FieldValue types
use set_event_sink instead. Both sinks can be active at the same time.
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_debug(
value: a,
message: String,
fields: List(#(String, FieldValue)),
) -> a
Log the value at Debug 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_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.