logr_moonbit

https://github.com/moonbit-community/logr

moon add Yoorkin/logr_moonbit@0.1.0
Download zip
Author
Version
0.1.0
License
Apache-2.0
Last updated
11 months ago
Downloads
22
README

#MoonBit logr

A minimal structured logging API for MoonBit, inspired by the Go logr library.

#Overview

logr offers a structured logging API that allows MoonBit programs and libraries to do logging without becoming coupled to a particular logging implementation. This package provides the API interface, while the actual logging implementation is handled by "sinks".

The Logger type is intended for application and library authors. It provides a relatively small API which can be used everywhere you want to emit logs.

The LogSink enum is intended for logging library implementers. It provides different implementations of the actual logging functionality.

#Key Features

  • Structured Logging: Uses key-value pairs instead of printf-style format strings
  • Verbosity Levels: Support for different verbosity levels (V-levels)
  • Logger Names: Hierarchical logger naming for better organization
  • Context Support: Store and enrich loggers with contextual information
  • Discard Logger: Zero-cost logger that discards all messages
  • Funcr Implementation: Built-in formatted text output implementation

#Basic Usage

#Creating a Logger

test "basic logger creation" {
// Create a discard logger (for testing or when logging is disabled)
let discard_logger = discard()

// Create a funcr logger with default options
let logger = new_funcr(default_funcr_options())

// Create a funcr logger with custom options
let custom_opts = FuncrOptions::{
log_caller: message_class_none,
log_timestamp: true,
timestamp_format: "2006-01-02 15:04:05",
verbosity: 2
}
let custom_logger = new_funcr(custom_opts)
}

#Structured Logging

test "structured logging example" {
let logger = new_funcr(default_funcr_options())

// Info logging with key-value pairs
logger.info("user login successful", [
Value::from_string("user_id"), Value::from_int(12345),
Value::from_string("ip_address"), Value::from_string("192.168.1.100"),
Value::from_string("login_method"), Value::from_string("oauth")
])

// Error logging
logger.error(
Some("database connection timeout"),
"failed to authenticate user",
[
Value::from_string("user_id"), Value::from_int(12345),
Value::from_string("timeout_ms"), Value::from_int(5000)
]
)
}

#Verbosity Levels

Verbosity levels allow you to control how much detail your logs contain. Higher V-levels mean more verbose (and less important) logs.

test "verbosity levels example" {
let opts = FuncrOptions::{
log_caller: message_class_none,
log_timestamp: false,
timestamp_format: "",
verbosity: 2 // Enable V-levels 0, 1, and 2
}
let logger = new_funcr(opts)

// These will be logged (V-level <= 2)
logger.info("Important message", []) // V-level 0 (default)
logger.v(1).info("Less important message", []) // V-level 1
logger.v(2).info("Debug information", []) // V-level 2

// This will be filtered out (V-level > 2)
logger.v(3).info("Very verbose debug info", []) // V-level 3
}

#Logger Names

Logger names help organize logs hierarchically, making it easier to filter and understand log sources.

test "logger names example" {
let base_logger = new_funcr(default_funcr_options())

// Create service-specific loggers
let user_service_logger = base_logger.with_name("user-service")
let auth_logger = user_service_logger.with_name("auth")
let db_logger = user_service_logger.with_name("database")

user_service_logger.info("service starting", [])
auth_logger.info("user authenticated", [Value::from_string("user_id"), Value::from_int(123)])
db_logger.info("connection established", [Value::from_string("host"), Value::from_string("localhost")])
}

#Persistent Values

You can attach key-value pairs to a logger that will be included in all subsequent log messages.

test "persistent values example" {
let base_logger = new_funcr(default_funcr_options())

// Create a logger with persistent values
let request_logger = base_logger.with_values([
Value::from_string("request_id"), Value::from_string("req-12345"),
Value::from_string("user_id"), Value::from_int(67890),
Value::from_string("trace_id"), Value::from_string("trace-abcdef")
])

// All logs from this logger will include the persistent values
request_logger.info("processing request", [Value::from_string("action"), Value::from_string("create_user")])
request_logger.info("request completed", [Value::from_string("duration_ms"), Value::from_int(150)])
}

#Chaining Operations

Logger operations can be chained together for convenience:

test "chaining operations example" {
let logger = new_funcr(default_funcr_options())
.with_name("api-gateway")
.with_values([
Value::from_string("version"), Value::from_string("1.2.3"),
Value::from_string("instance"), Value::from_string("web-01")
])
.v(1) // Set verbosity level

logger.info("gateway started", [Value::from_string("port"), Value::from_int(8080)])
}

#Context Support

The library provides context support for storing loggers and enriching them with contextual information.

#Basic Context Usage

test "context basic usage" {
let logger = new_funcr(default_funcr_options())

// Create a context with a logger
let ctx = new_context_with_logger(logger)
.with_value("service", Value::from_string("user-api"))
.with_value("version", Value::from_string("2.1.0"))
.with_value("request_id", Value::from_string("req-789"))

// Log using the context (includes context values automatically)
ctx.info("processing user request", [
Value::from_string("action"), Value::from_string("get_profile")
])

ctx.error(Some("user not found"), "failed to retrieve profile", [
Value::from_string("user_id"), Value::from_int(404)
])
}

#Context Logger Modifications

test "context logger modifications" {
let base_logger = new_funcr(default_funcr_options())
let ctx = new_context_with_logger(base_logger)

// Create child contexts with modified loggers
let service_ctx = ctx.with_logger_name("payment-service")
let component_ctx = service_ctx.with_logger_values([
Value::from_string("component"), Value::from_string("stripe-integration")
])
let debug_ctx = component_ctx.with_logger_v(2)

debug_ctx.info("processing payment", [
Value::from_string("amount"), Value::from_int(2999),
Value::from_string("currency"), Value::from_string("USD")
])
}

#Value Types

The library uses the Value enum to represent all loggable values in a type-safe manner:

test "value types example" {
// Create different types of values
let string_val = Value::from_string("hello world")
let int_val = Value::from_int(42)
let bool_val = Value::from_bool(true)
let double_val = Value::from_double(3.14159)

let logger = new_funcr(default_funcr_options())
logger.info("demonstrating value types", [
Value::from_string("string_example"), string_val,
Value::from_string("int_example"), int_val,
Value::from_string("bool_example"), bool_val,
Value::from_string("double_example"), double_val
])
}

#Helper Functions

#Key-Value Helpers

test "helper functions example" {
// Create a single key-value pair
let single_kv = kv("user_name", Value::from_string("alice"))

// Create multiple key-value pairs
let multiple_kvs = kvs([
("user_id", Value::from_int(123)),
("is_active", Value::from_bool(true)),
("last_login", Value::from_string("2024-01-15T10:30:00Z"))
])

let logger = new_funcr(default_funcr_options())
logger.info("user information", single_kv)
logger.info("user details", multiple_kvs)
}

#Custom Value Formatting

For custom types, you can create helper functions to format them for logging:

struct User {
id: Int
name: String
email: String
}

fn User::to_log_value(self : User) -> Value {
// Only log id and name, not email for privacy
Value::from_string("User{id=\{self.id}, name=\{self.name}}")
}

test "custom formatting example" {
let user = User::{ id: 123, name: "Alice", email: "alice@example.com" }
let logger = new_funcr(default_funcr_options())

// Use the custom formatting function
logger.info("user action", [
Value::from_string("user"), user.to_log_value(),
Value::from_string("action"), Value::from_string("login")
])
}

#Best Practices

#1. Use Structured Logging

Prefer key-value pairs over concatenated strings:

test "structured logging best practice" {
let logger = new_funcr(default_funcr_options())

// Good
logger.info("user login failed", [
Value::from_string("user_id"), Value::from_int(123),
Value::from_string("reason"), Value::from_string("invalid_password"),
Value::from_string("attempt_count"), Value::from_int(3)
])

// Avoid - harder to parse and query
logger.info("User 123 login failed: invalid_password (attempt 3)", [])
}

#2. Use Consistent Key Names

Establish conventions for key names across your application:

test "consistent key names" {
let logger = new_funcr(default_funcr_options())

// Use consistent keys like "user_id", "request_id", "duration_ms"
logger.info("operation completed", [
Value::from_string("user_id"), Value::from_int(123),
Value::from_string("request_id"), Value::from_string("req-456"),
Value::from_string("duration_ms"), Value::from_int(250)
])
}

#3. Use Appropriate Verbosity Levels

  • V(0): Important operational information
  • V(1): General debugging information
  • V(2): Detailed debugging information
  • V(3): Very verbose trace information

#4. Use Logger Names for Organization

Create hierarchical logger names that reflect your application structure:

test "logger name organization" {
let base_logger = new_funcr(default_funcr_options())
let app_logger = base_logger.with_name("myapp")
let service_logger = app_logger.with_name("user-service")
let component_logger = service_logger.with_name("authentication")

component_logger.info("component initialized", [])
}

#5. Use Context for Request-Scoped Information

Store request-scoped information in contexts rather than passing it through every function:

test "context for request scope" {
let base_ctx = new_context()
let request_ctx = base_ctx
.with_value("request_id", Value::from_string("req-789"))
.with_value("user_id", Value::from_int(123))
.with_value("trace_id", Value::from_string("trace-abc"))

// Context can be passed through your call chain
// process_user_request(request_ctx, user_data)
}

#Performance Considerations

  • The discard logger has zero cost - it's safe to leave debug loggers in production code
  • Verbosity level checks are performed before any log formatting
  • String concatenation and formatting only occurs when logs will actually be emitted
  • Context enrichment is lazy - values are only processed when logging occurs

#Extending the Library

To implement a custom LogSink, you would add a new variant to the LogSink enum and implement the required methods. For example:

// Example of how you could extend LogSink
// (Note: This would require modifying the library source)

// struct CustomSink {
// // Your custom sink fields
// }

// Add variant to LogSink enum:
// CustomSink(CustomSink)

// Then implement the required methods in the LogSink module functions

This pattern allows for type-safe extension while maintaining the performance benefits of enum dispatch.

#
Marshaler

pub trait Marshaler {
marshal_log(Self) -> Value
}

Marshaler is an optional trait that logged values may choose to implement. Loggers with structured output should log the object returned by marshal_log instead of the original value.

#
Context

pub struct Context {
values : Map[String, Value]
logger : Logger?
}

Context holds key-value pairs and can carry a Logger
impl Eq for Context
impl Show for Context

#
Context::enrich_logger

fn Context::enrich_logger(self : Context, logger : Logger) -> Logger

Add all context values as key-value pairs to a logger

#
Context::error

fn Context::error(self : Context, err : String?, msg : String, keys_and_values : Array[Value]) -> Unit

Log an error message using the context's logger

#
Context::get_logger

fn Context::get_logger(self : Context) -> Logger?

Get the logger from the context

#
Context::get_value

fn Context::get_value(self : Context, key : String) -> Value?

Get a value from the context

#
Context::info

fn Context::info(self : Context, msg : String, keys_and_values : Array[Value]) -> Unit

Log an info message using the context's logger

#
Context::logger_or_discard

fn Context::logger_or_discard(self : Context) -> Logger

Get the logger from context, or return a discard logger if none exists

#
Context::with_logger

fn Context::with_logger(self : Context, logger : Logger) -> Context

Set a logger in the context

#
Context::with_logger_name

fn Context::with_logger_name(self : Context, name : String) -> Context

Create a child context with a modified logger (e.g., with additional values or name)

#
Context::with_logger_v

fn Context::with_logger_v(self : Context, level : Int) -> Context

Create a child context with a specific verbosity level

#
Context::with_logger_values

fn Context::with_logger_values(self : Context, keys_and_values : Array[Value]) -> Context

Create a child context with additional logger values

#
Context::with_value

fn Context::with_value(self : Context, key : String, value : Value) -> Context

Add a value to the context

#
FuncrOptions

pub struct FuncrOptions {
log_caller : MessageClass
log_timestamp : Bool
timestamp_format : String
verbosity : Int
}

Options for funcr formatting
impl Eq for FuncrOptions

#
FuncrSink

pub struct FuncrSink {
prefix : String
values : Array[Value]
enabled_level : Int
options : FuncrOptions
}

FuncrSink is a concrete implementation that formats logs and calls a function Note: We can't derive Eq for this because function types don't implement Eq

#
LogSink

pub enum LogSink {
Discard
Funcr(FuncrSink)
}

Abstract LogSink type that can hold different implementations
impl Eq for LogSink

#
LogSink::enabled

fn LogSink::enabled(self : LogSink, level : Int) -> Bool

Check if this LogSink is enabled at the specified V-level

#
LogSink::error

fn LogSink::error(self : LogSink, err : String?, msg : String, keys_and_values : Array[Value]) -> Unit

Log an error message

#
LogSink::info

fn LogSink::info(self : LogSink, level : Int, msg : String, keys_and_values : Array[Value]) -> Unit

Log an info message

#
LogSink::init

fn LogSink::init(self : LogSink, info : RuntimeInfo) -> Unit

Initialize the LogSink with runtime info

#
LogSink::with_name

fn LogSink::with_name(self : LogSink, name : String) -> LogSink

Return a new LogSink with the specified name appended

#
LogSink::with_values

fn LogSink::with_values(self : LogSink, keys_and_values : Array[Value]) -> LogSink

Return a new LogSink with additional key/value pairs

#
Logger

pub struct Logger {
sink : LogSink?
level : Int
}

Logger is a concrete type for performance reasons, but all the real work is passed on to a LogSink implementation.
impl Eq for Logger

#
Logger::enabled

fn Logger::enabled(self : Logger) -> Bool

enabled tests whether this Logger is enabled

#
Logger::error

fn Logger::error(self : Logger, err : String?, msg : String, keys_and_values : Array[Value]) -> Unit

error logs an error, with the given message and key/value pairs as context. The log message will always be emitted, regardless of verbosity level. The err parameter is optional and None may be passed instead of an error.

#
Logger::get_sink

fn Logger::get_sink(self : Logger) -> LogSink?

get_sink returns the stored sink

#
Logger::get_v

fn Logger::get_v(self : Logger) -> Int

get_v returns the verbosity level of the logger

#
Logger::info

fn Logger::info(self : Logger, msg : String, keys_and_values : Array[Value]) -> Unit

info logs a non-error message with the given key/value pairs as context. The msg argument should be used to add some constant description to the log line. The key/value pairs can then be used to add additional variable information.

#
Logger::is_zero

fn Logger::is_zero(self : Logger) -> Bool

is_zero returns true if this logger is an uninitialized zero value

#
Logger::v

fn Logger::v(self : Logger, level : Int) -> Logger

v returns a new Logger instance for a specific verbosity level, relative to this Logger. In other words, V-levels are additive. A higher verbosity level means a log message is less important. Negative V-levels are treated as 0.

#
Logger::with_name

fn Logger::with_name(self : Logger, name : String) -> Logger

with_name returns a new Logger instance with the specified name element added to the Logger's name. Successive calls with with_name append additional suffixes to the Logger's name.

#
Logger::with_sink

fn Logger::with_sink(self : Logger, sink : LogSink?) -> Logger

with_sink returns a copy of the logger with the new sink

#
Logger::with_values

fn Logger::with_values(self : Logger, keys_and_values : Array[Value]) -> Logger

with_values returns a new Logger instance with additional key/value pairs

#
MessageClass

pub enum MessageClass {
None
All
Info
Error
}

Message classes for caller logging
impl Eq for MessageClass

#
RuntimeInfo

pub struct RuntimeInfo {
call_depth : Int
}

RuntimeInfo holds information that the logr library knows
impl Eq for RuntimeInfo
impl Show for RuntimeInfo

#
Value

pub enum Value {
VString(String)
VInt(Int)
VBool(Bool)
VDouble(Double)
VArray(Array[Value])
VMap(Map[String, Value])
VNone
}

Value represents any value that can be logged
impl Eq for Value
impl Show for Value

#
Value::from_bool

fn Value::from_bool(b : Bool) -> Value

#
Value::from_double

fn Value::from_double(d : Double) -> Value

#
Value::from_int

fn Value::from_int(i : Int) -> Value

#
Value::from_string

fn Value::from_string(s : String) -> Value

Convert common types to Value

#
default_funcr_options

fn default_funcr_options() -> FuncrOptions

Default funcr options

#
discard

fn discard() -> Logger

discard returns a Logger that discards all messages logged to it

#
format_kv_pairs

fn format_kv_pairs(kv_pairs : Array[Value]) -> String

format_kv_pairs converts key-value pairs to a formatted string representation
fn kv(key : String, value : Value) -> Array[Value]

Helper function to create key-value pairs easily

#
kvs

fn kvs(pairs : Array[(String, Value)]) -> Array[Value]

Helper to create multiple key-value pairs

#
make_context

fn make_context(values : Map[String, Value], logger : Logger?) -> Context

Create a new Context

#
make_discard_sink

fn make_discard_sink() -> LogSink

Create a new LogSink::Discard

#
make_funcr_options

fn make_funcr_options(log_caller : MessageClass, log_timestamp : Bool, timestamp_format : String, verbosity : Int) -> FuncrOptions

Create a new FuncrOptions

#
make_funcr_sink

fn make_funcr_sink(prefix : String, values : Array[Value], enabled_level : Int, options : FuncrOptions) -> FuncrSink

Create a new FuncrSink

#
make_funcr_sink_logsink

fn make_funcr_sink_logsink(funcr_sink : FuncrSink) -> LogSink

Create a new LogSink::Funcr

#
make_runtime_info

fn make_runtime_info(call_depth : Int) -> RuntimeInfo

Create a new RuntimeInfo

#
message_class_all

let message_class_all : MessageClass

#
message_class_error

let message_class_error : MessageClass

#
message_class_info

let message_class_info : MessageClass

#
message_class_none

let message_class_none : MessageClass

Default message class constants

#
new

fn new(sink : LogSink?) -> Logger

new returns a new Logger instance. This is primarily used by libraries implementing LogSink, rather than end users. Passing None will create a Logger which discards all log lines.

#
new_context

fn new_context() -> Context

Create a new empty context

#
new_context_with_logger

fn new_context_with_logger(logger : Logger) -> Context

Create a context with a logger

#
new_funcr

fn new_funcr(options : FuncrOptions) -> Logger

new_funcr creates a new logger with funcr-style formatting

#
sanitize_kv

fn sanitize_kv(kv_list : Array[Value]) -> Array[Value]

sanitize_kv ensures that a list of key-value pairs has a value for every key. It expects alternating string keys and Value values.

Source Files

Powered by MoonBit

Site sourceReport issuePackagesBuild queueSkillsStatistics

© 2026 mooncakes.io