Files
Odin/core/log/log.odin
2026-06-23 12:35:58 +01:00

451 lines
12 KiB
Odin

package log
import "base:runtime"
import "core:fmt"
//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,
}
*/
Level :: runtime.Logger_Level
/*
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
/*
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,
}
/*
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
/*
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 {
@(cold)
internal :: proc(message: string, loc: runtime.Source_Code_Location) {
p := context.assertion_failure_proc
if p == nil {
p = runtime.default_assertion_failure_proc
}
log(.Fatal, message, location=loc)
p("runtime assertion", message, loc)
}
internal(message, 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 {
// NOTE(dragos): We are using the same trick as in builtin.assert
// to improve performance to make the CPU not
// execute speculatively, making it about an order of
// magnitude faster
@(cold)
internal :: proc(loc: runtime.Source_Code_Location, fmt_str: string, args: ..any) {
p := context.assertion_failure_proc
if p == nil {
p = runtime.default_assertion_failure_proc
}
message := fmt.tprintf(fmt_str, ..args)
log(.Fatal, message, location=loc)
p("runtime assertion", message, loc)
}
internal(loc, fmt_str, ..args)
}
}
/*
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)
internal :: proc(message: string, loc: runtime.Source_Code_Location) {
p := context.assertion_failure_proc
if p == nil {
p = runtime.default_assertion_failure_proc
}
log(.Fatal, message, location=loc)
p("unsatisfied ensure", message, loc)
}
internal(message, 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)
internal :: proc(loc: runtime.Source_Code_Location, fmt_str: string, args: ..any) {
p := context.assertion_failure_proc
if p == nil {
p = runtime.default_assertion_failure_proc
}
message := fmt.tprintf(fmt_str, ..args)
log(.Fatal, message, location=loc)
p("unsatisfied ensure", message, loc)
}
internal(loc, fmt_str, ..args)
}
}
/*
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 {
return
}
if level < logger.lowest_level {
return
}
runtime.DEFAULT_TEMP_ALLOCATOR_TEMP_GUARD()
str := fmt.tprint(..args, sep=sep)
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 {
return
}
if level < logger.lowest_level {
return
}
runtime.DEFAULT_TEMP_ALLOCATOR_TEMP_GUARD()
str := fmt.tprintf(fmt_str, ..args)
logger.procedure(logger.data, level, str, logger.options, location)
}