Skip to content

Logs that gleam. ✨πŸͺ΅βœ¨ A comprehensive logging library in Gleam that targets both Erlang and JavaScript.

License

Notifications You must be signed in to change notification settings

tylerbutler/birch

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

65 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

birch - logs that gleam ✨πŸͺ΅βœ¨

A logging library for Gleam with cross-platform support.

The name "birch" comes from birch trees, whose white bark gleams in the light.

Package Version Hex Docs

Features

  • Cross-platform: Works on both Erlang and JavaScript targets
  • Zero-configuration startup: Just import and start logging
  • Structured logging: Key-value metadata on every log message
  • Multiple handlers: Console, file, JSON, async, or custom handlers
  • Log rotation: Size-based and time-based rotation for file handlers
  • Color support: Colored output for TTY terminals
  • Lazy evaluation: Avoid expensive string formatting when logs are filtered
  • Scoped context: Request-scoped metadata that propagates automatically
  • Sampling: Probabilistic sampling and rate limiting for high-volume scenarios

Quick Start

import birch as log

pub fn main() {
  // Simple logging
  log.info("Application starting")
  log.debug("Debug message")
  log.error("Something went wrong")

  // With metadata
  log.info_m("User logged in", [#("user_id", "123"), #("ip", "192.168.1.1")])
}

Installation

Add birch to your gleam.toml:

[dependencies]
birch = ">= 0.1.0"

Global Configuration

Configure the default logger with custom settings:

import birch as log
import birch/level
import birch/handler/console
import birch/handler/json

pub fn main() {
  // Configure with multiple options
  log.configure([
    log.config_level(level.Debug),
    log.config_handlers([console.handler(), json.handler()]),
    log.config_context([#("app", "myapp"), #("env", "production")]),
  ])

  // All logs now include the context and go to both handlers
  log.info("Server starting")
}

Runtime Level Changes

Change the log level at runtime without reconfiguring everything:

import birch as log
import birch/level

// Enable debug logging for troubleshooting
log.set_level(level.Debug)

// Later, reduce verbosity
log.set_level(level.Warn)

// Check current level
let current = log.get_level()

Named Loggers

Create named loggers for different components:

import birch as log

pub fn main() {
  let db_logger = log.new("myapp.database")
  let http_logger = log.new("myapp.http")

  db_logger |> log.logger_info("Connected to database", [])
  http_logger |> log.logger_info("Server started on port 8080", [])
}

Logger Context

Add persistent context to a logger:

import birch as log

pub fn handle_request(request_id: String) {
  let logger = log.new("myapp.http")
    |> log.with_context([
      #("request_id", request_id),
      #("service", "api"),
    ])

  // All logs from this logger include the context
  logger |> log.logger_info("Processing request", [])
  logger |> log.logger_info("Request complete", [#("status", "200")])
}

Scoped Context

Automatically attach metadata to all logs within a scope:

import birch as log

pub fn handle_request(request_id: String) {
  log.with_scope([#("request_id", request_id)], fn() {
    // All logs in this block include request_id automatically
    log.info("Processing request")
    do_work()  // Logs in nested functions also include request_id
    log.info("Request complete")
  })
}

Scopes can be nested, with inner scopes adding to outer scope context:

log.with_scope([#("request_id", "123")], fn() {
  log.info("Start")  // request_id=123

  log.with_scope([#("step", "validation")], fn() {
    log.info("Validating")  // request_id=123 step=validation
  })

  log.info("Done")  // request_id=123
})

Platform Support

  • Erlang: Uses process dictionary. Each process has isolated context.
  • Node.js: Uses AsyncLocalStorage. Context propagates across async operations.
  • Other JS runtimes: Falls back to stack-based storage.

Check availability with log.is_scoped_context_available().

Log Levels

Six log levels are supported, from least to most severe:

Level Use Case
Trace Very detailed diagnostic information
Debug Debugging information during development
Info Normal operational messages (default)
Warn Warning conditions that might need attention
Error Error conditions that should be addressed
Fatal Critical errors preventing continuation

Set the minimum level for a logger:

import birch as log
import birch/level

let logger = log.new("myapp")
  |> log.with_level(level.Debug)  // Log Debug and above

Handlers

Console Handler

The default handler outputs to stdout with colors:

import birch/handler/console

let handler = console.handler()
// or with configuration
let handler = console.handler_with_config(console.ConsoleConfig(
  color: True,
  target: handler.Stdout,
))

JSON Handler

For log aggregation systems:

import birch/handler/json

let handler = json.handler()

Output:

{"timestamp":"2024-12-26T10:30:45.123Z","level":"info","logger":"myapp","message":"Request complete","method":"POST","path":"/api/users"}

Custom JSON Format

Use the builder pattern to customize JSON output:

import birch/handler/json
import gleam/json as j

let custom_handler =
  json.standard_builder()
  |> json.add_custom(fn(_record) {
    [
      #("service", j.string("my-app")),
      #("version", j.string("1.0.0")),
    ]
  })
  |> json.build()
  |> json.handler_with_formatter()

File Handler

Write to files with optional rotation:

import birch/handler/file

// Size-based rotation
let handler = file.handler(file.FileConfig(
  path: "/var/log/myapp.log",
  rotation: file.SizeRotation(max_bytes: 10_000_000, max_files: 5),
))

// Time-based rotation (daily)
let handler = file.handler(file.FileConfig(
  path: "/var/log/myapp.log",
  rotation: file.TimeRotation(interval: file.Daily, max_files: 7),
))

// Combined rotation (size OR time)
let handler = file.handler(file.FileConfig(
  path: "/var/log/myapp.log",
  rotation: file.CombinedRotation(
    max_bytes: 50_000_000,
    interval: file.Daily,
    max_files: 10,
  ),
))

Async Handler

Wrap any handler for non-blocking logging:

import birch/handler/async
import birch/handler/console

// Make console logging async
let async_console =
  console.handler()
  |> async.make_async(async.default_config())

// With custom configuration
let config =
  async.config()
  |> async.with_queue_size(5000)
  |> async.with_flush_interval(50)
  |> async.with_overflow(async.DropOldest)

let handler = async.make_async(console.handler(), config)

// Before shutdown, ensure all logs are written
async.flush()

Null Handler

For testing or disabling logging:

import birch/handler

let handler = handler.null()

Custom Handlers

Create custom handlers with the handler interface:

import birch/handler
import birch/formatter

let my_handler = handler.new(
  name: "custom",
  write: fn(message) {
    // Send to external service, etc.
  },
  format: formatter.human_readable,
)

Error Callbacks

Handle errors from handlers without crashing:

import birch/handler
import birch/handler/file

let handler =
  file.handler(config)
  |> handler.with_error_callback(fn(err) {
    io.println("Handler " <> err.handler_name <> " failed: " <> err.error)
  })

Lazy Evaluation

Avoid expensive operations when logs are filtered:

import birch as log

// The closure is only called if debug level is enabled
log.debug_lazy(fn() {
  "Expensive debug info: " <> compute_debug_info()
})

Error Result Helpers

Log errors with automatic metadata extraction:

import birch as log

case file.read("config.json") {
  Ok(content) -> parse_config(content)
  Error(_) as result -> {
    // Automatically includes error value in metadata
    log.error_result("Failed to read config file", result)
    use_defaults()
  }
}

// With additional metadata
log.error_result_m("Database query failed", result, [
  #("query", "SELECT * FROM users"),
  #("table", "users"),
])

Sampling

For high-volume logging, sample messages probabilistically:

import birch as log
import birch/level
import birch/sampling

// Log only 10% of debug messages
log.configure([
  log.config_sampling(sampling.config(level.Debug, 0.1)),
])

// Debug messages above the threshold are always logged
// Messages at or below Debug level are sampled at 10%

Testing Support

Custom Time Providers

Use deterministic timestamps in tests:

import birch as log

let test_logger =
  log.new("test")
  |> log.with_time_provider(fn() { "2024-01-01T00:00:00.000Z" })

Caller ID Capture

Track which process/thread created each log:

import birch as log

let logger =
  log.new("myapp.worker")
  |> log.with_caller_id_capture()

// Log records will include:
// - Erlang: PID like "<0.123.0>"
// - JavaScript: "main", "pid-N", or "worker-N"

Output Formats

Human-Readable (default)

2024-12-26T10:30:45.123Z | INFO  | myapp.http | Request complete | method=POST path=/api/users

JSON

{"timestamp":"2024-12-26T10:30:45.123Z","level":"info","logger":"myapp.http","message":"Request complete","method":"POST","path":"/api/users"}

Library Authors

For library code, create silent loggers that consumers can configure:

// In your library
import birch as log

const logger = log.silent("mylib.internal")

pub fn do_something() {
  logger |> log.logger_debug("Starting operation", [])
  // ...
}

Consumers control logging by adding handlers to the logger.

Comparison with Other Logging Libraries

Several logging libraries exist in the Gleam ecosystem. Here's how they compare:

Feature birch glight glogg palabres
Erlang target βœ… βœ… βœ… βœ…
JavaScript target βœ… ❌ βœ… βœ…
Console output βœ… βœ… ❌ βœ…
File output βœ… βœ… ❌ ❌
JSON output βœ… βœ… βœ… βœ…
File rotation βœ… ❌ ❌ ❌
Colored output βœ… βœ… ❌ βœ…
Structured metadata βœ… βœ… βœ… βœ…
Typed metadata values ❌ ❌ βœ… βœ…
Named loggers βœ… ❌ ❌ ❌
Logger context βœ… βœ… βœ… ❌
Scoped context βœ… ❌ ❌ ❌
Lazy evaluation βœ… ❌ ❌ ❌
Custom handlers βœ… ❌ ❌ ❌
Sampling βœ… ❌ ❌ ❌
Stacktrace capture ❌ ❌ βœ… ❌
Erlang logger integration βœ… βœ… ❌ ❌
Wisp integration ❌ ❌ ❌ βœ…
Zero-config startup βœ… ❌ ❌ βœ…

When to Choose Each Library

  • birch: Applications needing file rotation, scoped context propagation, lazy evaluation, custom handlers, or Erlang logger integration with cross-platform support.
  • glight: Erlang-only applications that want a minimal wrapper around Erlang's standard logger module.
  • glogg: Applications requiring typed metadata fields (Int, Float, Bool, Duration) or stacktrace capture.
  • palabres: Wisp web applications that benefit from built-in middleware integration.

Development

See DEV.md for development setup, testing, and contribution guidelines.

License

MIT License - see LICENSE for details.

About

Logs that gleam. ✨πŸͺ΅βœ¨ A comprehensive logging library in Gleam that targets both Erlang and JavaScript.

Topics

Resources

License

Stars

Watchers

Forks

Contributors 15