xlog

Native structured logging with levels and category-based filtering.

log
logging
structured-logging
moon add tonyfettes/xlog@0.4.1
Download zip
Version
0.4.1
License
Apache-2.0
Last updated
25 days ago
Downloads
18K

Dependencies

README

#xlog

tonyfettes/xlog is a native-only structured logger with top-level helpers, logger instances, categories, structured fields, stdout/file/multi outputs, and MOON_XLOG filtering.

Logger calls are best-effort: output errors are ignored by Logger, while handlers can still report errors when called directly or wrapped.

#Quick Start

Use top-level helpers when passing a logger around would be noisy:

if @xlog.info(category="app") is Some(event) {
event.write_object_begin()
event.write_object_field("message", "server started")
event.write_object_field("port", 8080)
event.write_object_end()
}

Use a Logger when you want a specific output:

let logger = @xlog.Logger(@xlog.Stdout())

if logger.debug(category="app.db") is Some(event) {
event.write_object_begin()
event.write_object_field("message", "query plan")
event.write_object_end()
}

This API is shaped for a future conditional streaming form where a disabled event skips evaluating structured fields.

#Levels And Categories

The default level is Info. Level order, from least to most verbose:

Fatal Error Warn Info Debug Trace

A record is emitted when its level is less than or equal to the effective level. Category overrides are hierarchical, so app.db.query falls back to app.db, then app, then the logger's level.

Logger methods and top-level helpers return None when their level/category is disabled, or Some(event) when fields should be written:

let logger = @xlog.Logger(@xlog.Stdout())

if logger.info(category="app") is Some(event) {
event.write_object_begin()
event.write_object_field("message", "server started")
event.write_object_end()
}

Create separate logger instances when components need independent levels while sharing the same output handler.

All fields written through an event are user fields. A field named "message" is stored under fields like any other field; it is not promoted to top-level metadata.

#Configuration

Config::from_env() reads MOON_XLOG by default:

MOON_XLOG=debug MOON_XLOG='warn,app.db=debug,app.db.query=trace'

You can also build config directly:

let config = @xlog.Config(level=@xlog.Warn)
config.set_category_level("app.db", @xlog.Debug)

let logger = @xlog.Logger(@xlog.Stdout(), config~)
if logger.debug(category="app.db") is Some(event) {
event.write_object_begin()
event.write_object_field("message", "query plan")
event.write_object_end()
}

Invalid levels, invalid directives, and invalid categories raise ConfigError. The package global logger catches invalid MOON_XLOG and falls back to Info.

#Outputs

Stdout defaults to one JSON object per line. Use Text for human-readable key-value output:

@xlog.global().set_handler(@xlog.Stdout())
@xlog.global().set_handler(@xlog.Stdout(format=@xlog.Text))

File opens an append-mode file and defaults to Jsonl:

let file = @xlog.File("_build/app.log")
@xlog.global().set_handler(file)
if @xlog.info(category="app") is Some(event) {
event.write_object_begin()
event.write_object_field("message", "written to file")
event.write_object_end()
}

File is native-only and uses a small ISO C FILE * stub. The native handle closes automatically when the handler is no longer reachable; call close() only after removing it from long-lived loggers. It does not provide multi-process atomic logging guarantees.

Multi fans out to several handlers:

let file = @xlog.File("_build/app.log")
let handlers : Array[&@xlog.Handler] = [@xlog.Stdout(), file]
@xlog.global().set_handler(@xlog.Multi(handlers))

Multi attempts every child handler. Direct Multi.handle(entry) calls can raise MultiError(Array[Error]); ordinary logger calls swallow that error.

#Global Logger

Top-level helpers use @xlog.global(), a process-wide root logger:

let config = @xlog.Config::from_env() catch { _ => @xlog.Config() }

@xlog.global().set_config(config)
@xlog.global().set_level(@xlog.Debug)
if @xlog.info(category="app") is Some(event) {
event.write_object_begin()
event.write_object_field("message", "top-level record")
event.write_object_end()
}

global() is a root logger, not a replaceable default logger. Configure it with set_handler, set_config, and set_level.

#Example

The examples/lorem package demonstrates category filtering:

moon run examples/lorem MOON_XLOG=debug moon run examples/lorem MOON_XLOG='warn,lorem.db=trace,lorem.worker=debug,lorem.parser.lexer=trace' moon run examples/lorem

#
Handler

pub(open) trait Handler {
fn handle(Self, Entry) -> Unit raise
}

A sink that receives structured log entries.

Direct handler calls may raise errors. Logger methods catch and ignore those errors so logging failures do not interrupt application code.

#
ConfigError

pub(all) suberror ConfigError {
InvalidCategory(String)
InvalidDirective(String)
InvalidLevel(String)
}

Errors raised when parsing or updating logger configuration.

#
MultiError

pub(all) suberror MultiError {
MultiError(Array[Error])
}

Error raised by Multi when one or more child handlers fail.

#
Config

type Config

Logging configuration with a root level and category-specific overrides.

Category overrides are hierarchical: app.db.query falls back to app.db, then app, then the logger's root level.

#
Config::Config

fn Config::Config(level? : Level) -> Config

Create a configuration with the given root level.

#
Config::from_env

fn Config::from_env(env? : String, level? : Level) -> Config raise ConfigError

Read configuration from an environment variable.

env defaults to MOON_XLOG. The value is a comma-separated list of directives: either a root level such as debug, or a category override such as app.db=trace.

#
Config::set_category_level

fn Config::set_category_level(self : Config, category : String, level : Level) -> Unit raise ConfigError

Set a level override for a category.

Category names must be non-empty dot-separated segments, with no leading dot, trailing dot, or empty segment.

#
Entry

pub struct Entry {
level : Level
category : String?
timestamp :
ZonedDateTime

source : SourceLoc?
fields : Map[String, Json]
}

A structured log record passed to handlers.
impl Show for Entry
impl ToJson for Entry

#
Event

type Event

A structured log event that is written field-by-field.

Event values are returned by logger level methods only when the log site is enabled. Call write_object_begin, write each field, then call write_object_end to emit the entry.

#
Event::write_object_begin

fn Event::write_object_begin(self : Event) -> Unit

Begin writing the structured object for this event.

#
Event::write_object_end

fn Event::write_object_end(self : Event) -> Unit

Finish and emit the event.

#
Event::write_object_field

fn[T : ToJson] Event::write_object_field(self : Event, key : String, value : T) -> Unit

Write one structured field.

#
File

type File

A handler that appends log entries to a file.

File is native-only. It keeps the file open until close is called or the handler becomes unreachable.
impl Handler for File

#
File::File

fn File::File(path : String, format? : Format) -> File raise
IOError

Open a file handler in append mode.

The default format is Jsonl.

#
File::close

fn File::close(self : File) -> Unit

Close the file handle.

Calling close more than once is safe.

#
Format

pub(all) enum Format {
Text
Jsonl
}

Output format used by built-in handlers.

#
Level

pub(all) enum Level {
Fatal
Error
Warn
Info
Debug
Trace
} derive(Compare, Eq)

Log levels ordered from most severe to most verbose.

A record is emitted when its level is less than or equal to the effective logger level.
impl Show for Level

#
Logger

type Logger

A structured logger that emits entries through a Handler.

Logger calls are best-effort: errors raised by the handler are ignored.

#
Logger::Logger

fn[H : Handler] Logger::Logger(handler : H, level? : Level, config? : Config) -> Logger

Create a logger that writes to handler.

The default level is Info. When config is provided, its root level is used as the logger level and category-specific overrides are consulted for each entry.

#
Logger::debug

#callsite(autofill(loc))
fn Logger::debug(self : Logger, category? : String, loc~ : SourceLoc) -> Event?

Start a Debug event if it is enabled.

#
Logger::error

#callsite(autofill(loc))
fn Logger::error(self : Logger, category? : String, loc~ : SourceLoc) -> Event?

Start an Error event if it is enabled.

#
Logger::event

#callsite(autofill(loc))
fn Logger::event(self : Logger, level : Level, category? : String, loc~ : SourceLoc) -> Event?

Start a log event at level if it is enabled.

#
Logger::fatal

#callsite(autofill(loc))
fn Logger::fatal(self : Logger, category? : String, loc~ : SourceLoc) -> Event?

Start a Fatal event if it is enabled.

#
Logger::info

#callsite(autofill(loc))
fn Logger::info(self : Logger, category? : String, loc~ : SourceLoc) -> Event?

Start an Info event if it is enabled.

#
Logger::set_config

fn Logger::set_config(self : Logger, config : Config) -> Unit

Install a configuration object and reset the logger's root level from it.

#
Logger::set_handler

fn[H : Handler] Logger::set_handler(self : Logger, handler : H) -> Unit

Replace the handler used by this logger.

#
Logger::set_level

fn Logger::set_level(self : Logger, level : Level) -> Unit

Set the logger's root level.

Category-specific overrides from an installed Config still take precedence for matching categories.

#
Logger::trace

#callsite(autofill(loc))
fn Logger::trace(self : Logger, category? : String, loc~ : SourceLoc) -> Event?

Start a Trace event if it is enabled.

#
Logger::warn

#callsite(autofill(loc))
fn Logger::warn(self : Logger, category? : String, loc~ : SourceLoc) -> Event?

Start a Warn event if it is enabled.

#
Multi

pub(all) struct Multi(Array[&Handler])

A handler that fans out each entry to multiple child handlers.

Every child handler is attempted. If any fail, MultiError is raised after all handlers have been called.
impl Handler for Multi

#
Stdout

type Stdout

A handler that writes log entries to standard output.
impl Handler for Stdout

#
Stdout::Stdout

fn Stdout::Stdout(format? : Format) -> Stdout

Create a standard-output handler.

The default format is Jsonl.

#
debug

#callsite(autofill(loc))
fn debug(category? : String, loc~ : SourceLoc) -> Event?

Start a Debug event with the global logger if it is enabled.

#
error

#callsite(autofill(loc))
fn error(category? : String, loc~ : SourceLoc) -> Event?

Start an Error event with the global logger if it is enabled.

#
event

#callsite(autofill(loc))
fn event(level : Level, category? : String, loc~ : SourceLoc) -> Event?

Start a log event at level with the global logger if it is enabled.

#
fatal

#callsite(autofill(loc))
fn fatal(category? : String, loc~ : SourceLoc) -> Event?

Start a Fatal event with the global logger if it is enabled.

#
global

fn global() -> Logger

Return the process-wide root logger used by top-level logging helpers.

The returned logger can be configured with set_handler, set_config, and set_level.

#
info

#callsite(autofill(loc))
fn info(category? : String, loc~ : SourceLoc) -> Event?

Start an Info event with the global logger if it is enabled.

#
trace

#callsite(autofill(loc))
fn trace(category? : String, loc~ : SourceLoc) -> Event?

Start a Trace event with the global logger if it is enabled.

#
warn

#callsite(autofill(loc))
fn warn(category? : String, loc~ : SourceLoc) -> Event?

Start a Warn event with the global logger if it is enabled.

Powered by MoonBit

Site sourceReport issuePackagesBuild queueSkillsStatistics

© 2026 mooncakes.io