Integrate docs from PR #5119

This commit is contained in:
gingerBill
2026-06-23 12:35:58 +01:00
parent a1fe7222bb
commit 6698ebacb1
5 changed files with 458 additions and 33 deletions

68
core/log/doc.odin Normal file
View File

@@ -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

View File

@@ -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

View File

@@ -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 {

View File

@@ -6,6 +6,7 @@ import "base:runtime"
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 {
// 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, // Wrapped allocator
level: Level,
prefix: string,
lock: sync.Mutex,
level: Level, // Log Level used for allocations
prefix: string, // Prefix to use in log messages
lock: sync.Mutex,
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
la.level = level
@@ -31,7 +42,15 @@ log_allocator_init :: proc(la: ^Log_Allocator, level: Level, size_fmt := Log_All
}
/*
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,
data = la,
@@ -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) {
la := (^Log_Allocator)(allocator_data)

View File

@@ -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