From 6698ebacb1e0ef7cfd0b1988204bb83dc1512aca Mon Sep 17 00:00:00 2001 From: gingerBill Date: Tue, 23 Jun 2026 12:35:58 +0100 Subject: [PATCH] Integrate docs from PR #5119 --- core/log/doc.odin | 68 +++++++ core/log/file_console_logger.odin | 78 +++++++- core/log/log.odin | 292 +++++++++++++++++++++++++++--- core/log/log_allocator.odin | 28 ++- core/log/multi_logger.odin | 25 +++ 5 files changed, 458 insertions(+), 33 deletions(-) create mode 100644 core/log/doc.odin diff --git a/core/log/doc.odin b/core/log/doc.odin new file mode 100644 index 000000000..d0e45bd93 --- /dev/null +++ b/core/log/doc.odin @@ -0,0 +1,68 @@ +/* +Implementation of logging facilities. + +Odin has builtin support for logging using procedure `context`. After a logger is created it can then be assigned to +`context.logger` and used implicitly in future log calls. + +While it is ok for simple apps to use the `core:fmt` package, libraries and complex apps should prefer the `core:log` +package. By using the implicit logger library and application authors allow the caller to decide how to process log +messages. + +When starting out you can easily just init the logger with a single line. +Example: + + package main + + import "core:log" + + main :: proc() { + context.logger = log.create_console_logger() + log.info("Hello World!") + } + +However when the application gets more involved you might want to try a more complex setup. +Example: + + package main + + import "core:log" + import "core:os" + + main :: proc() { + handle, err := os.open("logs.txt", os.O_RDWR | os.O_APPEND | os.O_CREATE, 0o666) + assert(err == nil, "Cannot open log file") + + file_logger := log.create_file_logger(handle) + // This closes the file handle + defer log.destroy_file_logger(file_logger) + + console_logger := log.create_console_logger() + defer log.destroy_console_logger(console_logger) + + multi_logger := log.create_multi_logger(console_logger, file_logger) + defer log.destroy_multi_logger(multi_logger) + + context.logger = multi_logger + + log.info("Application started!") + } + +It is also possible to create an allocator that logs all allocations. +Example: + + package main + + import "core:log" + + main :: proc() { + context.logger = log.create_console_logger() + + alloc: log.Log_Allocator + log.log_allocator_init(&alloc, .Debug) + context.allocator = log.log_allocator(&alloc) + + a := new(i32) + free(a) + } +*/ +package log \ No newline at end of file diff --git a/core/log/file_console_logger.odin b/core/log/file_console_logger.odin index 47174719f..b7220ad30 100644 --- a/core/log/file_console_logger.odin +++ b/core/log/file_console_logger.odin @@ -11,6 +11,7 @@ import "core:terminal" import "core:terminal/ansi" import "core:time" +// Strings to output when `.Level` is included in the logger options. Level_Headers := [?]string{ 0..<10 = "[DEBUG] --- ", 10..<20 = "[INFO ] --- ", @@ -19,22 +20,49 @@ Level_Headers := [?]string{ 40..<50 = "[FATAL] --- ", } +/* +The default option set for a console logger. + +It is similar to the file logger default option set, but the output includes colors. + +When you use this set of options you can expect the following output: + + [LEVEL] --- [YYYY-MM-DD HH:MM:SS] [file.odin:L:proc()] Message + +For example: + + [INFO ] --- [2025-01-02 12:34:56] [main.odin:8:main()] Hello World! +*/ Default_Console_Logger_Opts :: Options{ .Level, .Terminal_Color, .Short_File_Path, .Line, .Procedure, -} | Full_Timestamp_Opts +} + Full_Timestamp_Opts +/* +The default option set for a file logger. + +It is similar to the console logger default option set, but the output is not colored. + +When you use this set of options you can expect the following output: + + [LEVEL] --- [YYYY-MM-DD HH:MM:SS] [file.odin:L:proc()] Message + +For example: + + [INFO ] --- [2025-01-02 12:34:56] [main.odin:8:main()] Hello World! +*/ Default_File_Logger_Opts :: Options{ .Level, .Short_File_Path, .Line, .Procedure, -} | Full_Timestamp_Opts +} + Full_Timestamp_Opts +//Data backing a file or console logger. File_Console_Logger_Data :: struct { file_handle: ^os.File, ident: string, @@ -67,6 +95,21 @@ init_standard_stream_status :: proc "contextless" () { } } + +/* +Create a logger that outputs to a file. + +*Allocates Using Provided Allocator* + +When no longer needed can be destroyed with `destroy_file_logger`. + +Inputs: +- `h`: A handle to the output file +- `lowest`: Log level to use (default is `.Debug`) +- `opt`: Specifies additional data present in the log output (default is `log.Default_File_Logger_Opts`) +- `ident`: Identifier to include in the output (default is `""`) +- `allocator`: Allocator to use for data backing the logger (default is `context.allocator`) +*/ create_file_logger :: proc(f: ^os.File, lowest := Level.Debug, opt := Default_File_Logger_Opts, ident := "", allocator := context.allocator) -> Logger { data := new(File_Console_Logger_Data, allocator) data.file_handle = f @@ -74,6 +117,13 @@ create_file_logger :: proc(f: ^os.File, lowest := Level.Debug, opt := Default_Fi return Logger{file_logger_proc, data, lowest, opt} } +/* +Free the state allocated with `create_file_logger` and close the file handle. + +Inputs: +- `log`: Logger created with `create_file_logger` +- `allocator`: Allocator passed to `create_file_logger` (default is `context.allocator`) +*/ destroy_file_logger :: proc(log: Logger, allocator := context.allocator) { data := cast(^File_Console_Logger_Data)log.data if data.file_handle != nil { @@ -82,6 +132,19 @@ destroy_file_logger :: proc(log: Logger, allocator := context.allocator) { free(data, allocator) } +/* +Create a logger that outputs to the terminal. + +*Allocates Using Provided Allocator* + +When no longer needed can be destroyed with `destroy_console_logger`. + +Inputs: +- `lowest`: Log level to use (default is `.Debug`) +- `opt`: Specifies additional data present in the log output (default is `log.Default_Console_Logger_Opts`) +- `ident`: Identifier to include in the output (default is `""`) +- `allocator`: Allocator to use for data backing the logger (default is `context.allocator`) +*/ create_console_logger :: proc(lowest := Level.Debug, opt := Default_Console_Logger_Opts, ident := "", allocator := context.allocator) -> Logger { data := new(File_Console_Logger_Data, allocator) data.file_handle = nil @@ -89,6 +152,13 @@ create_console_logger :: proc(lowest := Level.Debug, opt := Default_Console_Logg return Logger{console_logger_proc, data, lowest, opt} } +/* +Free the state allocated with `create_console_logger`. + +Inputs: +- `log`: Logger created with `create_console_logger` +- `allocator`: Allocator passed to `create_console_logger` (default is `context.allocator`) +*/ destroy_console_logger :: proc(log: Logger, allocator := context.allocator) { free(log.data, allocator) } @@ -117,6 +187,7 @@ _file_console_logger_proc :: proc(h: ^os.File, ident: string, level: Level, text fmt.fprintf(h, "%s%s\n", strings.to_string(buf), text) } + file_logger_proc :: proc(logger_data: rawptr, level: Level, text: string, options: Options, location := #caller_location) { data := cast(^File_Console_Logger_Data)logger_data _file_console_logger_proc(data.file_handle, data.ident, level, text, options, location) @@ -136,6 +207,7 @@ console_logger_proc :: proc(logger_data: rawptr, level: Level, text: string, opt _file_console_logger_proc(h, data.ident, level, text, options, location) } +// Helper used to build the part of the message including the log level. do_level_header :: proc(opts: Options, str: ^strings.Builder, level: Level) { RESET :: ansi.CSI + ansi.RESET + ansi.SGR @@ -162,6 +234,7 @@ do_level_header :: proc(opts: Options, str: ^strings.Builder, level: Level) { } } +// Helper used to build the part of the message including the data and time. do_time_header :: proc(opts: Options, buf: ^strings.Builder, t: time.Time) { when time.IS_SUPPORTED { if Full_Timestamp_Opts & opts != nil { @@ -180,6 +253,7 @@ do_time_header :: proc(opts: Options, buf: ^strings.Builder, t: time.Time) { } } +// Helper used to build the part of the message including the file location. do_location_header :: proc(opts: Options, buf: ^strings.Builder, location := #caller_location) { if Location_Header_Opts & opts == nil { return diff --git a/core/log/log.odin b/core/log/log.odin index 4209a2850..ce0f49124 100644 --- a/core/log/log.odin +++ b/core/log/log.odin @@ -1,52 +1,102 @@ -// Implementations of the `context.Logger` interface. package log import "base:runtime" import "core:fmt" -// NOTE(bill, 2019-12-31): These are defined in `package runtime` as they are used in the `context`. This is to prevent an import definition cycle. +//These are defined in package `base:runtime` as they are used in the `context`. This is to prevent an import definition cycle. /* -Logger_Level :: enum { - Debug = 0, - Info = 10, - Warning = 20, - Error = 30, - Fatal = 40, -} + Logger_Level :: enum { + Debug = 0, + Info = 10, + Warning = 20, + Error = 30, + Fatal = 40, + } */ Level :: runtime.Logger_Level /* -Option :: enum { - Level, - Date, - Time, - Short_File_Path, - Long_File_Path, - Line, - Procedure, - Terminal_Color -} +Specifies additional data present in the log output. + +Defined in `package runtime` as it is used in the `context`. This is to prevent an import definition cycle. + + Option :: enum { + // The log level, e.g. "[DEBUG] ---" + Level, + // The date, e.g. [2025-01-02] + Date, + // The time, e.g. [12:34:56] + Time, + // Just the filename, e.g. [main.odin] + Short_File_Path, + // Full file path, e.g. [/tmp/project/main.odin] + Long_File_Path, + // File line of the log statement, e.g. [8] + Line, + // Calling procedure, e.g. [main()] + Procedure, + // Enables colored output + Terminal_Color + } */ Option :: runtime.Logger_Option /* -Options :: bit_set[Option]; +Specifies additional data present in the log output. + +Defined in `package runtime` as it is used in the `context`. This is to prevent an import definition cycle. + + Options :: bit_set[Option]; */ Options :: runtime.Logger_Options +/* +A preset option set for a logger. + +When you use this set of options you can expect the following output: + + [YYYY-MM-DD HH:MM:SS] Message + +For example: + + [2025-01-02 12:34:56] Hello World! +*/ Full_Timestamp_Opts :: Options{ .Date, .Time, } + +/* +A preset option set for a logger. + +When you use this set of options you can expect the following output: + + [file.odin:L:proc()] Message + +For example: + + [main.odin:8:main()] Hello World! +*/ Location_Header_Opts :: Options{ .Short_File_Path, .Long_File_Path, .Line, .Procedure, } + +/* +A preset option set for a logger. + +When you use this set of options you can expect the following output: + + [file.odin] Message + +For example: + + [main.odin] Hello World! +*/ Location_File_Opts :: Options{ .Short_File_Path, .Long_File_Path, @@ -54,67 +104,206 @@ Location_File_Opts :: Options{ /* -Logger_Proc :: #type proc(data: rawptr, level: Level, text: string, options: Options, location := #caller_location); +Implementation of the logger. + +Defined in `package runtime` as it is used in the `context`. This is to prevent an import definition cycle. + + Logger_Proc :: #type proc(data: rawptr, level: Level, text: string, options: Options, location := #caller_location); + */ Logger_Proc :: runtime.Logger_Proc /* -Logger :: struct { - procedure: Logger_Proc, - data: rawptr, - lowest_level: Level, - options: Logger_Options, -} +Data backing the logger. + +Defined in `package runtime` as it is used in the `context`. This is to prevent an import definition cycle. + + Logger :: struct { + // Implementation + procedure: Logger_Proc, + // Configuration data passed to the implementation + data: rawptr, + // Minimum level for messages passed to the implementation + lowest_level: Level, + // Additional data present in the log output + options: Logger_Options, + } */ Logger :: runtime.Logger +/* +Do nothing. + +Defined in `package runtime` as it is used in the `context`. This is to prevent an import definition cycle. +*/ nil_logger_proc :: runtime.default_logger_proc +/* +Create a logger that does nothing. + +Returns: +- A logger that does nothing +*/ nil_logger :: proc() -> Logger { return Logger{nil_logger_proc, nil, Level.Debug, nil} } + +/* +Log a formatted message at the `Debug` level. + +Inputs: +- `fmt_str`: A format string, e.g. `"a: %v, b: %v" +- `args`: Arguments for the format string +- `location`: Location of the caller (default is #caller_location) +*/ debugf :: proc(fmt_str: string, args: ..any, location := #caller_location) { logf(.Debug, fmt_str, ..args, location=location) } + +/* +Log a formatted message at the `Info` level. + +Inputs: +- `fmt_str`: A format string, e.g. `"a: %v, b: %v" +- `args`: Arguments for the format string +- `location`: Location of the caller (default is #caller_location) +*/ infof :: proc(fmt_str: string, args: ..any, location := #caller_location) { logf(.Info, fmt_str, ..args, location=location) } + +/* +Log a formatted message at the `Warn` level. + +Inputs: +- `fmt_str`: A format string, e.g. `"a: %v, b: %v" +- `args`: Arguments for the format string +- `location`: Location of the caller (default is #caller_location) +*/ warnf :: proc(fmt_str: string, args: ..any, location := #caller_location) { logf(.Warning, fmt_str, ..args, location=location) } + +/* +Log a formatted message at the `Error` level. + +Inputs: +- `fmt_str`: A format string, e.g. `"a: %v, b: %v" +- `args`: Arguments for the format string +- `location`: Location of the caller (default is #caller_location) +*/ errorf :: proc(fmt_str: string, args: ..any, location := #caller_location) { logf(.Error, fmt_str, ..args, location=location) } + +/* +Log a formatted message at the `Fatal` level. + +Inputs: +- `fmt_str`: A format string, e.g. `"a: %v, b: %v" +- `args`: Arguments for the format string +- `location`: Location of the caller (default is #caller_location) +*/ fatalf :: proc(fmt_str: string, args: ..any, location := #caller_location) { logf(.Fatal, fmt_str, ..args, location=location) } +/* +Log a message at the `Debug` level. + +Inputs: +- `args`: values to be concatenated into the output +- `sep`: separator to use when concatenating (default is `" "`) +- `location`: Location of the caller (default is #caller_location) +*/ debug :: proc(args: ..any, sep := " ", location := #caller_location) { log(.Debug, ..args, sep=sep, location=location) } + +/* +Log a message at the `Info` level. + +Inputs: +- `args`: values to be concatenated into the output +- `sep`: separator to use when concatenating (default is `" "`) +- `location`: Location of the caller (default is #caller_location) +*/ info :: proc(args: ..any, sep := " ", location := #caller_location) { log(.Info, ..args, sep=sep, location=location) } + +/* +Log a message at the `Warn` level. + +Inputs: +- `args`: values to be concatenated into the output +- `sep`: separator to use when concatenating (default is `" "`) +- `location`: Location of the caller (default is #caller_location) +*/ warn :: proc(args: ..any, sep := " ", location := #caller_location) { log(.Warning, ..args, sep=sep, location=location) } + +/* +Log a message at the `Error` level. + +Inputs: +- `args`: values to be concatenated into the output +- `sep`: separator to use when concatenating (default is `" "`) +- `location`: Location of the caller (default is #caller_location) +*/ error :: proc(args: ..any, sep := " ", location := #caller_location) { log(.Error, ..args, sep=sep, location=location) } + +/* +Log a message at the `Fatal` level. + +Inputs: +- `args`: values to be concatenated into the output +- `sep`: separator to use when concatenating (default is `" "`) +- `location`: Location of the caller (default is #caller_location) +*/ fatal :: proc(args: ..any, sep := " ", location := #caller_location) { log(.Fatal, ..args, sep=sep, location=location) } +/* +Log a message at the `Fatal` level and abort the program. + +Inputs: +- `args`: values to be concatenated into the output +- `location`: Location of the caller (default is #caller_location) +*/ panic :: proc(args: ..any, location := #caller_location) -> ! { log(.Fatal, ..args, location=location) runtime.panic("log.panic", location) } + +/* +Log a formatted message at the `Fatal` level and abort the program. + +Inputs: +- `fmt_str`: A format string, e.g. `"a: %v, b: %v" +- `args`: Arguments for the format string +- `location`: Location of the caller (default is #caller_location) +*/ panicf :: proc(fmt_str: string, args: ..any, location := #caller_location) -> ! { logf(.Fatal, fmt_str, ..args, location=location) runtime.panic("log.panicf", location) } +/* +When condition is `false` log a message at the `Fatal` level and abort the program. + +Can be disabled using `ODIN_DISABLE_ASSERT`. + +Inputs: +- `condition`: A boolean to check +- `message`: Message to log when condition is false (a default is provided) +- `loc`: Location of the caller (default is #caller_location) +*/ @(disabled=ODIN_DISABLE_ASSERT) assert :: proc(condition: bool, message := #caller_expression(condition), loc := #caller_location) { if !condition { @@ -131,6 +320,17 @@ assert :: proc(condition: bool, message := #caller_expression(condition), loc := } } +/* +When condition is `false` log a formatted message at the `Fatal` level and abort the program. + +Can be disabled using `ODIN_DISABLE_ASSERT`. + +Inputs: +- `condition`: A boolean to check +- `fmt_str`: A format string to use when condition is false, e.g. `"a: %v, b: %v" +- `args`: Arguments for the format string +- `loc`: Location of the caller (default is #caller_location) +*/ @(disabled=ODIN_DISABLE_ASSERT) assertf :: proc(condition: bool, fmt_str: string, args: ..any, loc := #caller_location) { if !condition { @@ -152,6 +352,16 @@ assertf :: proc(condition: bool, fmt_str: string, args: ..any, loc := #caller_lo } } +/* +When condition is `false` log a message at the `Fatal` level and abort the program. + +Unlike `assert` this procedure cannot be disabled with `ODIN_DISABLE_ASSERT` and will always execute. + +Inputs: +- `condition`: A boolean to check +- `message`: Message to log when condition is false (a default is provided) +- `loc`: Location of the caller (default is #caller_location) +*/ ensure :: proc(condition: bool, message := #caller_expression(condition), loc := #caller_location) { if !condition { @(cold) @@ -167,6 +377,17 @@ ensure :: proc(condition: bool, message := #caller_expression(condition), loc := } } +/* +When condition is `false` log a formatted message at the `Fatal` level and abort the program. + +Unlike `assertf` this procedure cannot be disabled with `ODIN_DISABLE_ASSERT` and will always execute. + +Inputs: +- `condition`: A boolean to check +- `fmt_str`: A format string to use when condition is false, e.g. `"a: %v, b: %v" +- `args`: Arguments for the format string +- `loc`: Location of the caller (default is #caller_location) +*/ ensuref :: proc(condition: bool, fmt_str: string, args: ..any, loc := #caller_location) { if !condition { @(cold) @@ -184,7 +405,15 @@ ensuref :: proc(condition: bool, fmt_str: string, args: ..any, loc := #caller_lo } +/* +Log a message at the desired level. +Inputs: +- `level`: The level of the message +- `args`: values to be concatenated into the output +- `sep`: separator to use when concatenating (default is `" "`) +- `location`: Location of the caller (default is #caller_location) +*/ log :: proc(level: Level, args: ..any, sep := " ", location := #caller_location) { logger := context.logger if logger.procedure == nil || logger.procedure == nil_logger_proc { @@ -198,6 +427,15 @@ log :: proc(level: Level, args: ..any, sep := " ", location := #caller_location) logger.procedure(logger.data, level, str, logger.options, location) } +/* +Log a formatted message at the desired level. + +Inputs: +- `level`: The level of the message +- `fmt_str`: A format string, e.g. `"a: %v, b: %v" +- `args`: Arguments for the format string +- `location`: Location of the caller (default is #caller_location) +*/ logf :: proc(level: Level, fmt_str: string, args: ..any, location := #caller_location) { logger := context.logger if logger.procedure == nil || logger.procedure == nil_logger_proc { diff --git a/core/log/log_allocator.odin b/core/log/log_allocator.odin index c4c54f38f..a6784133b 100644 --- a/core/log/log_allocator.odin +++ b/core/log/log_allocator.odin @@ -6,6 +6,7 @@ import "base:runtime" import "core:fmt" import "core:sync" +// Format to use when logging allocations. Log_Allocator_Format :: enum { Bytes, // Actual number of bytes. Human, // Bytes in human units like bytes, kibibytes, etc. as appropriate. @@ -15,13 +16,23 @@ Log_Allocator_Format :: enum { // Log_Allocator is an allocator which calls `context.logger` on each of its allocations operations. // The format can be changed by setting the `size_fmt: Log_Allocator_Format` field to either `Bytes` or `Human`. Log_Allocator :: struct { - allocator: runtime.Allocator, - level: Level, - prefix: string, + allocator: runtime.Allocator, // Wrapped allocator + level: Level, // Log Level used for allocations + prefix: string, // Prefix to use in log messages lock: sync.Mutex, - size_fmt: Log_Allocator_Format, + size_fmt: Log_Allocator_Format, // Format to use when logging allocations } +/* +Initialize the backing data for the allocator that logs all allocations. + +Inputs: +- `la`: Pointer to the data structure to initialize +- `level`: Log level to use for allocations +- `size_fmt`: Format to use when logging allocations (default is `.Bytes`) +- `allocator`: Wrapped allocator (default is `context.allocator`) +- `prefix`: Prefix to use in log messages (default is `""`) +*/ log_allocator_init :: proc(la: ^Log_Allocator, level: Level, size_fmt := Log_Allocator_Format.Bytes, allocator := context.allocator, prefix := "") { la.allocator = allocator @@ -31,7 +42,15 @@ log_allocator_init :: proc(la: ^Log_Allocator, level: Level, size_fmt := Log_All la.size_fmt = size_fmt } +/* +Create an allocator that logs all allocations. +Inputs: +- `la`: Pointer to the data structure backing the allocator + +Returns: +- An allocator that logs all allocations +*/ log_allocator :: proc(la: ^Log_Allocator) -> runtime.Allocator { return runtime.Allocator{ procedure = log_allocator_proc, @@ -39,6 +58,7 @@ log_allocator :: proc(la: ^Log_Allocator) -> runtime.Allocator { } } +// Backing procedure for allocator that logs all allocations. log_allocator_proc :: proc(allocator_data: rawptr, mode: runtime.Allocator_Mode, size, alignment: int, old_memory: rawptr, old_size: int, location := #caller_location) -> ([]byte, runtime.Allocator_Error) { diff --git a/core/log/multi_logger.odin b/core/log/multi_logger.odin index 6122554bb..99d7377db 100644 --- a/core/log/multi_logger.odin +++ b/core/log/multi_logger.odin @@ -1,10 +1,27 @@ package log +// A container backing for multiple loggers. Multi_Logger_Data :: struct { loggers: []Logger, } +/* +Create a logger that logs to all backing loggers. + +*Allocates Using Provided Allocator* + +When no longer needed can be destroyed with `destroy_multi_logger`. + +Note: Logs using a multi logger take both the multi logger and the backing loggers' log levels into account. + +Inputs: +- `logs` - Backing loggers passed as multiple arguments +- `allocator` - An allocator used to allocate data to store backing loggers (default is `context.allocator`) + +Returns: +- A multi logger +*/ create_multi_logger :: proc(logs: ..Logger, allocator := context.allocator) -> Logger { data := new(Multi_Logger_Data, allocator) data.loggers = make([]Logger, len(logs), allocator) @@ -12,12 +29,20 @@ create_multi_logger :: proc(logs: ..Logger, allocator := context.allocator) -> L return Logger{multi_logger_proc, data, Level.Debug, nil} } +/* +Free the state allocated with `create_multi_logger`. + +Inputs: +- `log`: Logger created with `create_multi_logger` +- `allocator`: Allocator passed to `create_multi_logger` (default is `context.allocator`) +*/ destroy_multi_logger :: proc(log: Logger, allocator := context.allocator) { data := (^Multi_Logger_Data)(log.data) delete(data.loggers, allocator) free(data, allocator) } +// Backing procedure for the multi logger. multi_logger_proc :: proc(logger_data: rawptr, level: Level, text: string, options: Options, location := #caller_location) { data := cast(^Multi_Logger_Data)logger_data