/// MoonBit implementation of go-logr
///
/// logr offers an opinion on how MoonBit programs and libraries can do logging
/// without becoming coupled to a particular logging implementation. This is not
/// an implementation of logging - it is an API.
///
/// 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.
///| Value type for structured logging
///|
/// Value represents any value that can be logged
pub enum Value {
VString(String)
VInt(Int)
VBool(Bool)
VDouble(Double)
VArray(Array[Value])
VMap(Map[String, Value])
VNone
} derive(Eq, Show)
///|
/// Convert common types to Value
pub fn Value::from_string(s : String) -> Value {
VString(s)
}
///|
pub fn Value::from_int(i : Int) -> Value {
VInt(i)
}
///|
pub fn Value::from_bool(b : Bool) -> Value {
VBool(b)
}
///|
pub fn Value::from_double(d : Double) -> Value {
VDouble(d)
}
///| RuntimeInfo and basic types
///|
/// RuntimeInfo holds information that the logr library knows
pub struct RuntimeInfo {
/// CallDepth is the number of call frames the logr library adds
call_depth : Int
} derive(Eq, Show, Default)
///|
/// Message classes for caller logging
pub enum MessageClass {
None
All
Info
Error
} derive(Eq, Show)
///|
impl Default for MessageClass with default() {
None
}
///| LogSink implementations
///|
/// Abstract LogSink type that can hold different implementations
pub enum LogSink {
Discard
Funcr(FuncrSink)
// We can add more implementations here
} derive(Eq)
///|
/// 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
pub struct FuncrSink {
prefix : String
values : Array[Value]
enabled_level : Int
options : FuncrOptions
}
///|
impl Eq for FuncrSink with op_equal(self, other) {
self.prefix == other.prefix &&
self.values == other.values &&
self.enabled_level == other.enabled_level &&
self.options == other.options
}
///|
/// Options for funcr formatting
pub struct FuncrOptions {
log_caller : MessageClass
log_timestamp : Bool
timestamp_format : String
verbosity : Int
} derive(Eq, Show, Default)
///|
impl Show for LogSink with output(self, logger) {
match self {
Discard => logger.write_string("DiscardSink")
Funcr(sink) => {
logger.write_string("FuncrSink{prefix=")
logger.write_string(sink.prefix)
logger.write_string("}")
}
}
}
///|
impl Show for FuncrSink with output(self, logger) {
logger.write_string("FuncrSink{prefix=")
logger.write_string(self.prefix)
logger.write_string(", level=")
logger.write_string(self.enabled_level.to_string())
logger.write_string("}")
}
///| LogSink methods
///|
/// Initialize the LogSink with runtime info
pub fn LogSink::init(self : LogSink, info : RuntimeInfo) -> Unit {
match self {
Discard => ()
Funcr(_) => () // FuncrSink doesn't need special initialization
}
}
///|
/// Check if this LogSink is enabled at the specified V-level
pub fn LogSink::enabled(self : LogSink, level : Int) -> Bool {
match self {
Discard => false
Funcr(sink) => level <= sink.options.verbosity
}
}
///|
/// Log an info message
pub fn LogSink::info(
self : LogSink,
level : Int,
msg : String,
keys_and_values : Array[Value],
) -> Unit {
match self {
Discard => ()
Funcr(sink) =>
if level <= sink.options.verbosity {
let formatted = format_info_message(sink, level, msg, keys_and_values)
// For now, just print to stdout - in real implementation this would call write_fn
println(sink.prefix + " " + formatted)
}
}
}
///|
/// Log an error message
pub fn LogSink::error(
self : LogSink,
err : String?,
msg : String,
keys_and_values : Array[Value],
) -> Unit {
match self {
Discard => ()
Funcr(sink) => {
let formatted = format_error_message(sink, err, msg, keys_and_values)
// For now, just print to stdout - in real implementation this would call write_fn
println(sink.prefix + " " + formatted)
}
}
}
///|
/// Return a new LogSink with additional key/value pairs
pub fn LogSink::with_values(
self : LogSink,
keys_and_values : Array[Value],
) -> LogSink {
match self {
Discard => Discard
Funcr(sink) => {
let new_values = []
for v in sink.values {
new_values.push(v)
}
for v in keys_and_values {
new_values.push(v)
}
Funcr(FuncrSink::{
prefix: sink.prefix,
values: new_values,
enabled_level: sink.enabled_level,
options: sink.options,
})
}
}
}
///|
/// Return a new LogSink with the specified name appended
pub fn LogSink::with_name(self : LogSink, name : String) -> LogSink {
match self {
Discard => Discard
Funcr(sink) => {
let new_prefix = if sink.prefix.length() > 0 {
sink.prefix + "/" + name
} else {
name
}
Funcr(FuncrSink::{
prefix: new_prefix,
values: sink.values,
enabled_level: sink.enabled_level,
options: sink.options,
})
}
}
}
///| Logger type
///|
/// Logger is a concrete type for performance reasons, but all the real work
/// is passed on to a LogSink implementation.
pub struct Logger {
sink : LogSink?
level : Int
} derive(Eq)
///|
impl Show for Logger with output(self, logger) {
logger.write_string("Logger{level=")
logger.write_string(self.level.to_string())
match self.sink {
Some(sink) => {
logger.write_string(", sink=")
sink.output(logger)
}
None => logger.write_string(", sink=None")
}
logger.write_string("}")
}
///| Logger construction and basic operations
///|
/// 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.
pub fn new(sink : LogSink?) -> Logger {
let logger = Logger::{ sink, level: 0 }
match sink {
Some(s) => s.init(RuntimeInfo::{ call_depth: 1 })
None => ()
}
logger
}
///|
/// discard returns a Logger that discards all messages logged to it
pub fn discard() -> Logger {
new(None)
}
///|
/// get_sink returns the stored sink
pub fn Logger::get_sink(self : Logger) -> LogSink? {
self.sink
}
///|
/// with_sink returns a copy of the logger with the new sink
pub fn Logger::with_sink(self : Logger, sink : LogSink?) -> Logger {
Logger::{ sink, level: self.level }
}
///| Logger methods
///|
/// enabled tests whether this Logger is enabled
pub fn Logger::enabled(self : Logger) -> Bool {
match self.sink {
Some(s) => s.enabled(self.level)
None => false
}
}
///|
/// 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.
pub fn Logger::info(
self : Logger,
msg : String,
keys_and_values : Array[Value],
) -> Unit {
match self.sink {
Some(s) =>
if s.enabled(self.level) {
s.info(self.level, msg, keys_and_values)
}
None => ()
}
}
///|
/// 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.
pub fn Logger::error(
self : Logger,
err : String?,
msg : String,
keys_and_values : Array[Value],
) -> Unit {
match self.sink {
Some(s) => s.error(err, msg, keys_and_values)
None => ()
}
}
///|
/// 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.
pub fn Logger::v(self : Logger, level : Int) -> Logger {
match self.sink {
Some(_) => {
let adjusted_level = if level < 0 { 0 } else { level }
Logger::{ sink: self.sink, level: self.level + adjusted_level }
}
None => self
}
}
///|
/// get_v returns the verbosity level of the logger
pub fn Logger::get_v(self : Logger) -> Int {
self.level
}
///|
/// with_values returns a new Logger instance with additional key/value pairs
pub fn Logger::with_values(
self : Logger,
keys_and_values : Array[Value],
) -> Logger {
match self.sink {
Some(s) =>
Logger::{ sink: Some(s.with_values(keys_and_values)), level: self.level }
None => self
}
}
///|
/// 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.
pub fn Logger::with_name(self : Logger, name : String) -> Logger {
match self.sink {
Some(s) => Logger::{ sink: Some(s.with_name(name)), level: self.level }
None => self
}
}
///|
/// is_zero returns true if this logger is an uninitialized zero value
pub fn Logger::is_zero(self : Logger) -> Bool {
match self.sink {
None => true
Some(_) => false
}
}
///| Funcr implementation
///|
/// new_funcr creates a new logger with funcr-style formatting
pub fn new_funcr(options : FuncrOptions) -> Logger {
let sink = Funcr(FuncrSink::{
prefix: "",
values: [],
enabled_level: 0,
options,
})
new(Some(sink))
}
///|
/// Default funcr options
pub fn default_funcr_options() -> FuncrOptions {
FuncrOptions::{
log_caller: message_class_none,
log_timestamp: false,
timestamp_format: "2006-01-02 15:04:05.000000",
verbosity: 0,
}
}
///| Formatting helpers
///|
/// Format an info message for funcr
fn format_info_message(
sink : FuncrSink,
level : Int,
msg : String,
keys_and_values : Array[Value],
) -> String {
let mut result = ""
// Add level if > 0
if level > 0 {
result = result + "level=" + level.to_string() + " "
}
// Add message
result = result + "msg=\"" + msg + "\""
// Add pre-stored values
if sink.values.length() > 0 {
result = result + " " + format_kv_pairs(sink.values)
}
// Add current key-value pairs
if keys_and_values.length() > 0 {
result = result + " " + format_kv_pairs(keys_and_values)
}
result
}
///|
/// Format an error message for funcr
fn format_error_message(
sink : FuncrSink,
err : String?,
msg : String,
keys_and_values : Array[Value],
) -> String {
let mut result = ""
// Add message
result = result + "msg=\"" + msg + "\""
// Add error if present
match err {
Some(e) => result = result + " error=\"" + e + "\""
None => ()
}
// Add pre-stored values
if sink.values.length() > 0 {
result = result + " " + format_kv_pairs(sink.values)
}
// Add current key-value pairs
if keys_and_values.length() > 0 {
result = result + " " + format_kv_pairs(keys_and_values)
}
result
}
///| Key-Value pair utilities
///|
/// sanitize_kv ensures that a list of key-value pairs has a value for every key.
/// It expects alternating string keys and Value values.
pub fn sanitize_kv(kv_list : Array[Value]) -> Array[Value] {
let result = []
let mut i = 0
while i < kv_list.length() {
// Add the key (should be a string, but we'll accept any Value)
result.push(kv_list[i])
// Add the value, or a placeholder if missing
if i + 1 < kv_list.length() {
result.push(kv_list[i + 1])
} else {
result.push(VString(""))
}
i += 2
}
result
}
///|
/// format_kv_pairs converts key-value pairs to a formatted string representation
pub fn format_kv_pairs(kv_pairs : Array[Value]) -> String {
let mut result = ""
let sanitized = sanitize_kv(kv_pairs)
let mut i = 0
while i + 1 < sanitized.length() {
if i > 0 {
result = result + " "
}
// Format as key=value
let key_str = match sanitized[i] {
VString(s) => s
other => other.to_string()
}
result = result + key_str + "=" + sanitized[i + 1].to_string()
i += 2
}
result
}
///| Convenience functions for creating Value from common types
///|
/// Helper function to create key-value pairs easily
pub fn kv(key : String, value : Value) -> Array[Value] {
[VString(key), value]
}
///|
/// Helper to create multiple key-value pairs
pub fn kvs(pairs : Array[(String, Value)]) -> Array[Value] {
let result = []
for pair in pairs {
let (key, value) = pair
result.push(VString(key))
result.push(value)
}
result
}
///| Marshaler trait for custom log formatting
///|
/// 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.
pub trait Marshaler {
/// marshal_log can be used to ensure structs are logged appropriately
marshal_log(Self) -> Value
}
///| Global constants for convenience
///|
/// Default message class constants
pub let message_class_none : MessageClass = MessageClass::None
///|
pub let message_class_all : MessageClass = MessageClass::All
///|
pub let message_class_info : MessageClass = MessageClass::Info
///|
pub let message_class_error : MessageClass = MessageClass::Error
///| Context support for storing and retrieving loggers
///|
/// Context holds key-value pairs and can carry a Logger
pub struct Context {
values : Map[String, Value]
logger : Logger?
} derive(Eq, Show)
///|
/// Create a new empty context
pub fn new_context() -> Context {
Context::{ values: Map::new(), logger: None }
}
///|
/// Create a context with a logger
pub fn new_context_with_logger(logger : Logger) -> Context {
Context::{ values: Map::new(), logger: Some(logger) }
}
///|
/// Add a value to the context
pub fn Context::with_value(
self : Context,
key : String,
value : Value,
) -> Context {
let new_values = Map::new()
for k, v in self.values {
new_values[k] = v
}
new_values[key] = value
Context::{ values: new_values, logger: self.logger }
}
///|
/// Get a value from the context
pub fn Context::get_value(self : Context, key : String) -> Value? {
self.values.get(key)
}
///|
/// Set a logger in the context
pub fn Context::with_logger(self : Context, logger : Logger) -> Context {
Context::{ values: self.values, logger: Some(logger) }
}
///|
/// Get the logger from the context
pub fn Context::get_logger(self : Context) -> Logger? {
self.logger
}
///|
/// Get the logger from context, or return a discard logger if none exists
pub fn Context::logger_or_discard(self : Context) -> Logger {
match self.logger {
Some(logger) => logger
None => discard()
}
}
///|
/// Add all context values as key-value pairs to a logger
pub fn Context::enrich_logger(self : Context, logger : Logger) -> Logger {
let kv_pairs = []
for key, value in self.values {
kv_pairs.push(Value::from_string(key))
kv_pairs.push(value)
}
logger.with_values(kv_pairs)
}
///| Context-aware logging functions
///|
/// Log an info message using the context's logger
pub fn Context::info(
self : Context,
msg : String,
keys_and_values : Array[Value],
) -> Unit {
match self.logger {
Some(logger) => {
let enriched_logger = self.enrich_logger(logger)
enriched_logger.info(msg, keys_and_values)
}
None => () // No logger in context, do nothing
}
}
///|
/// Log an error message using the context's logger
pub fn Context::error(
self : Context,
err : String?,
msg : String,
keys_and_values : Array[Value],
) -> Unit {
match self.logger {
Some(logger) => {
let enriched_logger = self.enrich_logger(logger)
enriched_logger.error(err, msg, keys_and_values)
}
None => () // No logger in context, do nothing
}
}
///|
/// Create a child context with a modified logger (e.g., with additional values or name)
pub fn Context::with_logger_name(self : Context, name : String) -> Context {
match self.logger {
Some(logger) =>
Context::{ values: self.values, logger: Some(logger.with_name(name)) }
None => self
}
}
///|
/// Create a child context with additional logger values
pub fn Context::with_logger_values(
self : Context,
keys_and_values : Array[Value],
) -> Context {
match self.logger {
Some(logger) =>
Context::{
values: self.values,
logger: Some(logger.with_values(keys_and_values)),
}
None => self
}
}
///|
/// Create a child context with a specific verbosity level
pub fn Context::with_logger_v(self : Context, level : Int) -> Context {
match self.logger {
Some(logger) =>
Context::{ values: self.values, logger: Some(logger.v(level)) }
None => self
}
}
///| Public constructors for structs
///|
/// Create a new RuntimeInfo
pub fn make_runtime_info(call_depth : Int) -> RuntimeInfo {
RuntimeInfo::{ call_depth, }
}
///|
/// Create a new FuncrOptions
pub fn make_funcr_options(
log_caller : MessageClass,
log_timestamp : Bool,
timestamp_format : String,
verbosity : Int,
) -> FuncrOptions {
FuncrOptions::{ log_caller, log_timestamp, timestamp_format, verbosity }
}
///|
/// Create a new FuncrSink
pub fn make_funcr_sink(
prefix : String,
values : Array[Value],
enabled_level : Int,
options : FuncrOptions,
) -> FuncrSink {
FuncrSink::{ prefix, values, enabled_level, options }
}
///|
/// Create a new Context
pub fn make_context(values : Map[String, Value], logger : Logger?) -> Context {
Context::{ values, logger }
}
///|
/// Create a new LogSink::Discard
pub fn make_discard_sink() -> LogSink {
LogSink::Discard
}
///|
/// Create a new LogSink::Funcr
pub fn make_funcr_sink_logsink(funcr_sink : FuncrSink) -> LogSink {
LogSink::Funcr(funcr_sink)
}