Initial project import

This commit is contained in:
Agila Forge Importer committed 2026-07-13 15:20:32 +00:00
commit 23f2d29e32
11957 files changed
+1754577

No files matched your search

+74
View File
@@ -0,0 +1,74 @@
## Description
`log` provides your application logging services.
You can log to file or to the console and use different
logging levels, so that you are not overwhelmed by the logs.
## Basic usage
The log module creates a default Log instance by default, and
provides utility functions, that you can use to access it.
Note: the default Log instance is thread safe.
That makes it very convenient to use in subsystems, without having
to thread a log instance everywhere:
```v
import log
fn abc() {
log.info('some information')
log.warn('a warning')
}
// this will not be visible, the default log level is .info:
log.debug('a debug message')
log.set_level(.debug)
// this will be now visible, the log level was changed to .debug:
log.debug('a debug message')
abc()
```
## Advanced usage
You can also create your own log instances, with different options
applied to them:
```v
import log
fn main() {
mut l := log.Log{}
l.set_level(.info)
l.set_full_logpath('./info.log')
l.log_to_console_too()
l.info('info')
l.warn('warn')
l.error('error')
l.fatal('fatal') // panic, marked as [noreturn]
}
```
## Backwards compatibility
After 2025/01/21, the `log` module outputs to `stderr` by default.
Before that, it used `stdout` by default.
If you want to restore the previous behaviour, you have to explicitly call `log.use_stdout()` :
```v
import os
import log
fn main() {
// log.info('this will be printed to stderr after 2025/01/21 by default')
log.use_stdout()
log.info('this will be printed to stdout')
}
```
If you want to just silence the note about the stdout -> stderr, during the transition period,
call `l.set_output_stream(os.stderr())` explicitly.
+95
View File
@@ -0,0 +1,95 @@
module log
import term
// Level defines the possible log levels, used by Log.set_level()
pub enum Level {
disabled = 0 // lowest level, disables everything else
fatal // disables error, warn, info and debug
error // disables warn, info and debug
warn // disables info and debug
info // disables debug
debug
}
// LogTarget defines the possible log targets, that Log supports
pub enum LogTarget {
console
file
both
}
// tag_to_console returns the tag for log level `l` as a colored string.
@[noinline]
fn tag_to_console(l Level, short_tag bool) string {
if short_tag {
return match l {
.disabled { ' ' }
.fatal { term.red('F') }
.error { term.red('E') }
.warn { term.yellow('W') }
.info { term.white('I') }
.debug { term.magenta('D') }
}
} else {
return match l {
.disabled { '' }
.fatal { term.red('FATAL') }
.error { term.red('ERROR') }
.warn { term.yellow('WARN ') }
.info { term.white('INFO ') }
.debug { term.magenta('DEBUG') }
}
}
}
// tag_to_file returns the tag for log level `l` as a string.
@[noinline]
fn tag_to_file(l Level, short_tag bool) string {
if short_tag {
return match l {
.disabled { ' ' }
.fatal { 'F' }
.error { 'E' }
.warn { 'W' }
.info { 'I' }
.debug { 'D' }
}
} else {
return match l {
.disabled { ' ' }
.fatal { 'FATAL' }
.error { 'ERROR' }
.warn { 'WARN ' }
.info { 'INFO ' }
.debug { 'DEBUG' }
}
}
}
// level_from_tag returns the log level from the given string.
// It returns `none` when it does not find a match.
@[noinline]
pub fn level_from_tag(tag string) ?Level {
return match tag {
'DISABLED', ' ' { Level.disabled }
'FATAL', 'F' { Level.fatal }
'ERROR', 'E' { Level.error }
'WARN', 'W' { Level.warn }
'INFO', 'I' { Level.info }
'DEBUG', 'D' { Level.debug }
else { none }
}
}
// target_from_label returns the log target from the given string.
// It returns `none` when it does not find a match.
@[noinline]
pub fn target_from_label(label string) ?LogTarget {
return match label {
'console' { LogTarget.console }
'file' { LogTarget.file }
'both' { LogTarget.both }
else { none }
}
}
+42
View File
@@ -0,0 +1,42 @@
// Copyright (c) 2019-2024 Alexander Medvednikov. All rights reserved.
// Use of this source code is governed by an MIT license
// that can be found in the LICENSE file.
@[has_globals]
module log
__global default_logger &Logger
// TODO: remove this hack, when the language has a way to access the raw pointer to an interface value directly:
@[typedef]
pub struct C.log__Logger {
mut:
_object voidptr
}
// init will be called before the user's main program starts, to initialize the default logger
fn init() {
default_logger = new_thread_safe_log()
at_exit(deinit) or {}
}
// deinit will be called on exit of the program and will free the memory allocated for the default logger
fn deinit() {
free_logger(default_logger)
}
@[manualfree]
fn free_logger(logger &Logger) {
if voidptr(logger) == unsafe { nil } {
return
}
unsafe {
// C.printf(c'free_logger logger: %p\n', logger)
logger.free()
//
pobject := &C.log__Logger(logger)._object
// C.printf(c'free_logger pobject: %p\n', pobject)
free(pobject)
//
free(logger)
}
}
+64
View File
@@ -0,0 +1,64 @@
// Copyright (c) 2019-2024 Alexander Medvednikov. All rights reserved.
// Use of this source code is governed by an MIT license
// that can be found in the LICENSE file.
module log
// set_logger changes the default logger instance to the one provided by the user.
// The existing logger will be freed, *after* the change is done.
@[manualfree]
pub fn set_logger(logger &Logger) {
// C.printf(c"set_logger logger: %p | old logger: %p\n", logger, default_logger)
old_logger := unsafe { default_logger }
default_logger = unsafe { logger }
free_logger(old_logger)
}
// get_logger returns a pointer to the current default logger instance
@[unsafe]
pub fn get_logger() &Logger {
return default_logger
}
// get_level returns the log level of the default Logger instance
pub fn get_level() Level {
return default_logger.get_level()
}
// set_level changes the log level of the default Logger instance
pub fn set_level(level Level) {
default_logger.set_level(level)
}
// fatal logs a `fatal` message, using the default Logger instance
@[noreturn]
pub fn fatal(s string) {
default_logger.fatal(s)
// the compiler currently has no way to mark functions in an interface
// as @[noreturn], so we need to make sure this is never returning ourselves
exit(1)
}
// error logs an `error` message, using the default Logger instance
pub fn error(s string) {
default_logger.error(s)
}
// error logs a `warning` message, using the default Logger instance
pub fn warn(s string) {
default_logger.warn(s)
}
// info logs an `info` message, using the default Logger instance
pub fn info(s string) {
default_logger.info(s)
}
// debug logs a `debug` message, using the default Logger instance
pub fn debug(s string) {
default_logger.debug(s)
}
// set_always_flush called with true, will make the log flush after every single .fatal(), .error(), .warn(), .info(), .debug() call.
pub fn set_always_flush(should_flush_on_every_message bool) {
default_logger.set_always_flush(should_flush_on_every_message)
}
+38
View File
@@ -0,0 +1,38 @@
import log
import time
fn test_default_log_instance() {
println(@FN + ' start')
log.info('info')
log.warn('warn')
log.error('error')
log.debug('no output for debug')
println('^^^ there should be no `no output for debug` shown above')
log.set_level(.debug)
log.debug('debug now')
println('^^^ there should be `debug now` shown above')
log.set_level(log.level_from_tag('INFO') or { log.Level.disabled })
log.info('info again')
log.debug('no output for debug')
log.set_level(log.level_from_tag('') or { log.Level.disabled })
log.error('no output anymore')
println('^^^ there should be no `no output anymore` shown above')
println(@FN + ' end')
}
fn log_messages(tidx int) {
time.sleep(2 * time.millisecond)
for i in 0 .. 3 {
log.debug('hi from thread ${tidx}')
}
}
fn test_default_log_instance_used_in_multiple_threads() {
eprintln('\n${@METHOD} start')
log.set_level(.debug)
mut threads := []thread{}
for tidx in 0 .. 3 {
threads << spawn log_messages(tidx)
}
threads.wait()
}
+67
View File
@@ -0,0 +1,67 @@
import os
import log
import rand
fn test_reopen() {
if os.user_os() == 'windows' && os.getenv('SKIP_TEST_REOPEN') == '' {
eprintln('skip renaming and reopening a log file on windows')
exit(0)
}
lfolder := os.join_path(os.vtmp_dir(), rand.ulid())
lpath1 := os.join_path(lfolder, 'current.log')
lpath2 := os.join_path(lfolder, 'current.log.2')
os.mkdir_all(lfolder)!
dump(lfolder)
mut l := log.new_thread_safe_log()
l.set_level(.debug)
l.set_full_logpath(lpath1)
l.warn('one warning')
l.error('one error')
// simulate a log rotation, by moving the log file
os.rename(lpath1, lpath2)!
l.warn('another warning')
// call reopen, note that the message from above, should be in the new file lpath2:
l.reopen()!
l.warn('third warning')
l.flush()
l.close()
// os.system('ls -la ${lpath1} ${lpath2}')
lcontent1 := os.read_file(lpath1)!
lcontent2 := os.read_file(lpath2)!
assert lcontent1.len > 0
assert lcontent2.len > 0
// the rotated log should have all messages before the l.reopen() call:
assert lcontent2.contains('one warning')
assert lcontent2.contains('one error')
assert lcontent2.contains('another warning')
// the log file that was reopened, should have only the new message:
assert lcontent1.contains('third warning')
assert !lcontent1.contains('one warning')
os.rmdir_all(lfolder) or {}
}
fn test_set_always_flush() {
log.set_level(.debug)
lfolder := os.join_path(os.vtmp_dir(), rand.ulid())
lpath1 := os.join_path(lfolder, 'current.log')
os.mkdir_all(lfolder)!
defer {
os.rmdir_all(lfolder) or {}
}
dump(lfolder)
mut l := log.Log{}
l.set_level(.info)
l.set_full_logpath(lpath1)
l.set_always_flush(true)
l.warn('one warning')
l.info('one info message')
l.error('one error')
l.close()
lcontent1 := os.read_file(lpath1)!
assert lcontent1.len > 0
assert lcontent1.contains('one warning')
assert lcontent1.contains('one error')
assert lcontent1.contains('one info message')
}
+333
View File
@@ -0,0 +1,333 @@
// Copyright (c) 2019-2024 Alexander Medvednikov. All rights reserved.
// Use of this source code is governed by an MIT license
// that can be found in the LICENSE file.
module log
import os
import time
import io
// TimeFormat define the log time string format, come from time/format.v
pub enum TimeFormat {
tf_ss_micro // YYYY-MM-DD HH:mm:ss.123456 (24h)
tf_default // YYYY-MM-DD HH:mm (24h)
tf_ss // YYYY-MM-DD HH:mm:ss (24h)
tf_ss_milli // YYYY-MM-DD HH:mm:ss.123 (24h)
tf_ss_nano // YYYY-MM-DD HH:mm:ss.123456789 (24h)
tf_rfc3339 // YYYY-MM-DDTHH:mm:ss.123Z (24 hours, see https://www.rfc-editor.org/rfc/rfc3339.html)
tf_rfc3339_micro // default, YYYY-MM-DDTHH:mm:ss.123456Z (24 hours, see https://www.rfc-editor.org/rfc/rfc3339.html)
tf_rfc3339_nano // YYYY-MM-DDTHH:mm:ss.123456789Z (24 hours, see https://www.rfc-editor.org/rfc/rfc3339.html)
tf_hhmm // HH:mm (24h)
tf_hhmmss // HH:mm:ss (24h)
tf_hhmm12 // hh:mm (12h)
tf_ymmdd // YYYY-MM-DD
tf_ddmmy // DD.MM.YYYY
tf_md // MMM D
tf_custom_format // 'MMMM Do YY N kk:mm:ss A' output like: January 1st 22 AD 13:45:33 PM
}
const stderr = os.stderr()
// Log represents a logging object
pub struct Log {
mut:
level Level = .debug
output_label string
ofile os.File
output_target LogTarget // output to console (stdout/stderr) or file or both.
time_format TimeFormat = .tf_rfc3339_micro
custom_time_format string = 'MMMM Do YY N kk:mm:ss A' // timestamp with custom format
short_tag bool
local_time bool // use local time or utc time in log
always_flush bool // flush after every single .fatal(), .error(), .warn(), .info(), .debug() call
output_stream io.Writer = stderr
pub mut:
output_file_name string // log output to this file
}
// get_level gets the internal logging level.
pub fn (l &Log) get_level() Level {
return l.level
}
// set_level sets the logging level to `level`. Messages for levels above it will skipped.
// For example, after calling log.set_level(.info), log.debug('message') will produce nothing.
// Call log.set_level(.disabled) to turn off the logging of all messages.
pub fn (mut l Log) set_level(level Level) {
l.level = level
}
// set_full_logpath sets the output label and output path from `full_log_path`.
pub fn (mut l Log) set_full_logpath(full_log_path string) {
rlog_file := os.real_path(full_log_path)
l.set_output_label(os.file_name(rlog_file))
l.set_output_path(os.dir(rlog_file))
}
// set_output_label sets the `label` for the output.
pub fn (mut l Log) set_output_label(label string) {
l.output_label = label
}
// set_output_path sets the file to which output is logged to.
pub fn (mut l Log) set_output_path(output_file_path string) {
if l.ofile.is_opened {
l.ofile.close()
}
l.output_target = .file
l.output_file_name = os.join_path(os.real_path(output_file_path), l.output_label)
ofile := os.open_append(l.output_file_name) or {
panic('error while opening log file ' + l.output_file_name + ' for appending')
}
l.ofile = ofile
}
// set_output_stream sets the output stream to write log e.g. stderr, stdout, etc.
pub fn (mut l Log) set_output_stream(stream io.Writer) {
l.output_stream = stream
}
// log_to_console_too turns on logging to the console too, in addition to logging to a file.
// You have to call it *after* calling .set_output_path(output_file_path).
pub fn (mut l Log) log_to_console_too() {
if l.output_target != .file {
panic('log_to_console_too should be called *after* .set_output_path')
}
l.output_target = .both
}
// flush writes the log file content to disk.
pub fn (mut l Log) flush() {
l.ofile.flush()
}
// close closes the log file.
pub fn (mut l Log) close() {
l.ofile.close()
}
// reopen reopens the log file. Useful for log rotation.
// This does nothing if you are only writing to the console.
pub fn (mut l Log) reopen() ! {
l.flush()
if l.output_target == .file || l.output_target == .both {
l.ofile.reopen(l.output_file_name, 'ab') or {
return error_with_code('re-opening log file `${l.output_file_name}` for appending failed',
1)
}
}
}
// log_file writes log line `s` with `level` to the log file.
fn (mut l Log) log_file(s string, level Level) {
timestamp := l.time_format(if l.local_time { time.now() } else { time.utc() })
e := tag_to_file(level, l.short_tag)
unsafe {
l.ofile.write_ptr(timestamp.str, timestamp.len)
l.ofile.write_ptr(c' ', 1)
l.ofile.write_ptr(c'[', 1)
l.ofile.write_ptr(e.str, e.len)
l.ofile.write_ptr(c']', 1)
l.ofile.write_ptr(c' ', 1)
l.ofile.write_ptr(s.str, s.len)
l.ofile.write_ptr(c'\n', 1)
}
if l.always_flush {
l.flush()
}
}
// log_stream writes log line `s` with `level` to stderr or stderr depending on set output stream.
fn (mut l Log) log_stream(s string, level Level) {
timestamp := l.time_format(if l.local_time { time.now() } else { time.utc() })
tag := tag_to_console(level, l.short_tag)
msg := '${timestamp} [${tag}] ${s}\n'
arr := msg.bytes()
l.output_stream.write(arr) or {}
if l.always_flush {
if mut l.output_stream is os.File {
match l.output_stream.fd {
1 { flush_stdout() }
2 { flush_stderr() }
else {}
}
}
}
}
// send_output writes log line `s` with `level` to either the log file or the console
// according to the value of the `.output_target` field.
pub fn (mut l Log) send_output(s &string, level Level) {
if l.output_target == .file || l.output_target == .both {
l.log_file(s, level)
}
if l.output_target == .console || l.output_target == .both {
l.log_stream(s, level)
}
}
// fatal logs line `s` via `send_output` if `Log.level` is greater than or equal to the `Level.fatal` category.
// Note that this method performs a panic at the end, even if log level is not enabled.
@[noreturn]
pub fn (mut l Log) fatal(s string) {
if int(l.level) >= int(Level.fatal) {
l.send_output(s, .fatal)
l.ofile.close()
}
panic(l.output_label + ': ' + s)
}
// error logs line `s` via `send_output` if `Log.level` is greater than or equal to the `Level.error` category.
pub fn (mut l Log) error(s string) {
if int(l.level) < int(Level.error) {
return
}
l.send_output(s, .error)
}
// warn logs line `s` via `send_output` if `Log.level` is greater than or equal to the `Level.warn` category.
pub fn (mut l Log) warn(s string) {
if int(l.level) < int(Level.warn) {
return
}
l.send_output(s, .warn)
}
// info logs line `s` via `send_output` if `Log.level` is greater than or equal to the `Level.info` category.
pub fn (mut l Log) info(s string) {
if int(l.level) < int(Level.info) {
return
}
l.send_output(s, .info)
}
// debug logs line `s` via `send_output` if `Log.level` is greater than or equal to the `Level.debug` category.
pub fn (mut l Log) debug(s string) {
if int(l.level) < int(Level.debug) {
return
}
l.send_output(s, .debug)
}
// free frees the given Log instance
@[unsafe]
pub fn (mut f Log) free() {
unsafe {
f.output_label.free()
f.ofile.close()
f.output_file_name.free()
}
}
// time_format return a timestamp string in the pre-defined format
fn (l Log) time_format(t time.Time) string {
match l.time_format {
.tf_rfc3339_micro { // YYYY-MM-DDTHH:mm:ss.123456Z (24 hours, see https://www.rfc-editor.org/rfc/rfc3339.html)
return t.format_rfc3339_micro()
}
.tf_ss_micro { // YYYY-MM-DD HH:mm:ss.123456 (24h) default
return t.format_ss_micro()
}
.tf_default { // YYYY-MM-DD HH:mm (24h)
return t.format()
}
.tf_ss { // YYYY-MM-DD HH:mm:ss (24h)
return t.format_ss()
}
.tf_ss_milli { // YYYY-MM-DD HH:mm:ss.123 (24h)
return t.format_ss_milli()
}
.tf_ss_nano { // YYYY-MM-DD HH:mm:ss.123456789 (24h)
return t.format_ss_nano()
}
.tf_rfc3339 { // YYYY-MM-DDTHH:mm:ss.123Z (24 hours, see https://www.rfc-editor.org/rfc/rfc3339.html)
return t.format_rfc3339()
}
.tf_rfc3339_nano { // YYYY-MM-DDTHH:mm:ss.123456789Z (24 hours, see https://www.rfc-editor.org/rfc/rfc3339.html)
return t.format_rfc3339_nano()
}
.tf_hhmm { // HH:mm (24h)
return t.hhmm()
}
.tf_hhmmss { // HH:mm:ss (24h)
return t.hhmmss()
}
.tf_hhmm12 { // hh:mm (12h)
return t.hhmm12()
}
.tf_ymmdd { // YYYY-MM-DD
return t.ymmdd()
}
.tf_ddmmy { // DD.MM.YYYY
return t.ddmmy()
}
.tf_md { // MMM D
return t.md()
}
.tf_custom_format { // 'MMMM Do YY N kk:mm:ss A' output like: January 1st 22 AD 13:45:33 PM
return t.custom_format(l.custom_time_format)
}
}
}
// set_time_format will set the log time format to a pre-defined format
pub fn (mut l Log) set_time_format(f TimeFormat) {
l.time_format = f
}
// set_always_flush called with true, will make the log flush after every single .fatal(), .error(), .warn(), .info(), .debug() call.
// That can be much slower, if you plan to do lots of frequent calls, but if your program exits early or crashes, your logs will be more complete.
pub fn (mut l Log) set_always_flush(should_flush_every_time bool) {
l.always_flush = should_flush_every_time
}
// get_time_format will get the log time format
pub fn (l Log) get_time_format() TimeFormat {
return l.time_format
}
// set_custom_time_format will set the log custom time format
// refer to time/custom_format() for more information
// eg. 'MMMM Do YY N kk:mm:ss A' output like: January 1st 22 AD 13:45:33 PM
pub fn (mut l Log) set_custom_time_format(f string) {
l.time_format = .tf_custom_format
l.custom_time_format = f
}
// get_custom_time_format will get the log custom time format
pub fn (l Log) get_custom_time_format() string {
return l.custom_time_format
}
// set_short_tag will set the log tag to it's short version
// eg. '[FATAL]'=>'[F]', '[ERROR]'=>'[E]','[WARN ]'=>'[W]','[INFO ]'=>'[I]','[DEBUG]'=>'[D]'
pub fn (mut l Log) set_short_tag(enabled bool) {
l.short_tag = enabled
}
// get_short_tag will get the log short tag enable state
pub fn (l Log) get_short_tag() bool {
return l.short_tag
}
// set_local_time will set the log using local time
pub fn (mut l Log) set_local_time(enabled bool) {
l.local_time = enabled
}
// get_local_time will get the log using local time state
pub fn (l Log) get_local_time() bool {
return l.local_time
}
// use_stdout will restore the old behaviour of logging to stdout, instead of stderr.
// It will also silence the deprecation note in the transition period.
pub fn use_stdout() {
mut l := ThreadSafeLog{}
l.set_output_stream(os.stdout())
set_logger(l)
}
+153
View File
@@ -0,0 +1,153 @@
module log
fn log_mutable_statements(mut l Log) {
println(@FN + ' start')
l.info('info')
l.warn('warn')
l.error('error')
l.debug('no output for debug')
l.set_level(.debug)
l.debug('debug now')
l.set_level(level_from_tag('INFO') or { Level.disabled })
l.info('info again')
l.set_level(level_from_tag('') or { Level.disabled })
l.error('no output anymore')
println(@FN + ' end')
}
fn logger_mutable_statements(mut l Logger) {
println(@FN + ' start')
// the given logger instance could have a level to filter some levels used here
l.info('info')
l.warn('warn')
l.error('error')
l.debug('no output for debug')
l.set_level(.debug) // change logging level, now part of the Logger interface
l.debug('output for debug now')
l.info('output for info now')
println(@FN + ' end')
}
// Note that Log and Logger methods requires a mutable instance
// new_log create and return a new Log reference
pub fn new_log() &Log {
return &Log{
level: .info
} // reference needed for its parallel usage
}
// new_log_as_logger create and return a new Log reference as a generic Logger
pub fn new_log_as_logger() &Logger {
return &Log{
level: .info
} // reference needed for its parallel usage
}
fn test_log_mutable() {
println(@FN + ' start')
mut l := Log{}
l.set_level(.info)
log_mutable_statements(mut l)
assert true
println(@FN + ' end')
}
fn test_log_mutable_reference() {
println(@FN + ' start')
mut l := new_log()
assert typeof(l).name == '&log.Log'
t := spawn log_mutable_statements(mut l)
t.wait()
assert true
println(@FN + ' end')
}
fn test_logger_mutable_reference() {
println(@FN + ' start')
// get log as Logger and use it
mut logger := new_log_as_logger()
logger.set_level(.warn)
assert typeof(logger).name == '&log.Logger'
t := spawn logger_mutable_statements(mut logger)
t.wait()
assert true
println(@FN + ' end')
}
fn test_log_mutable_short_tag() {
println(@FN + ' start')
mut l := Log{}
l.set_level(.info)
l.set_short_tag(true)
log_mutable_statements(mut l)
assert true
println(@FN + ' end')
}
fn test_level_from_tag() ? {
assert level_from_tag('INFO')? == .info
assert level_from_tag('FATAL')? == .fatal
assert level_from_tag('WARN')? == .warn
assert level_from_tag('ERROR')? == .error
assert level_from_tag('DEBUG')? == .debug
assert level_from_tag('I')? == .info
assert level_from_tag('F')? == .fatal
assert level_from_tag('W')? == .warn
assert level_from_tag('E')? == .error
assert level_from_tag('D')? == .debug
invalid := ['', 'FOO', 'nope']
for value in invalid {
mut passed := false
level_from_tag(value) or { passed = true }
assert passed
}
}
fn test_target_from_label() ? {
assert target_from_label('console')? == .console
assert target_from_label('file')? == .file
assert target_from_label('both')? == .both
invalid := ['', 'FOO', 'nope']
for value in invalid {
mut passed := false
target_from_label(value) or { passed = true }
assert passed
}
}
fn test_log_time_format() {
println(@FN + ' start')
mut l := Log{}
l.set_level(.info)
l.set_time_format(.tf_rfc3339_nano)
assert TimeFormat.tf_rfc3339_nano == l.get_time_format()
l.info('${@FN} rfc3339_nano == YYYY-MM-DDTHH:mm:ss.123456789Z')
l.set_time_format(.tf_ymmdd)
assert TimeFormat.tf_ymmdd == l.get_time_format()
l.info('${@FN} ymmdd == YYYY-MM-DD')
l.set_custom_time_format('MMMM Do YY N kk:mm:ss A')
assert TimeFormat.tf_custom_format == l.get_time_format()
l.info('${@FN} custom like January 1st 22 AD 13:45:33 PM')
assert true
l.set_time_format(.tf_default)
l.set_local_time(true)
l.info('${@FN} time log in local time')
assert l.get_local_time()
l.set_local_time(false)
l.info('${@FN} time log in utc time')
assert !l.get_local_time()
println(@FN + ' end')
}
fn make_error() ?string {
return 'ok'
}
fn test_log_default_fatal_has_noreturn() {
_ := make_error() or { fatal('error') }
}
+16
View File
@@ -0,0 +1,16 @@
module log
// Logger is an interface that describes a generic Logger
pub interface Logger {
get_level() Level
mut:
fatal(s string)
error(s string)
warn(s string)
info(s string)
debug(s string)
// utility methods:
set_level(level Level)
set_always_flush(should_flush bool)
free()
}
+114
View File
@@ -0,0 +1,114 @@
module log
import sync
// ThreadSafeLog embeds Log, and adds a mutex field.
// It uses the mutex to synchronise accesses to the embedded Log.
pub struct ThreadSafeLog {
Log
pub mut:
mu &sync.Mutex = sync.new_mutex()
}
// new_thread_safe_log returns a new log structure, whose methods are safe
// to call by multiple threads.
pub fn new_thread_safe_log() &ThreadSafeLog {
slevel := $d('log_default_level', 'info')
level := level_from_tag(slevel.to_upper()) or { panic('invalid log_default_level: ${slevel}') }
mut x := &ThreadSafeLog{
level: level
}
return x
}
// free frees the given ThreadSafeLog instance.
@[unsafe]
pub fn (mut x ThreadSafeLog) free() {
unsafe {
// make sure other threads are not in the blocks protected by the mutex:
if x.mu.try_lock() {
x.mu.unlock()
}
x.mu.destroy()
free(x.mu)
x.mu = nil
x.Log.free()
}
}
// set_level changes the log level
pub fn (mut x ThreadSafeLog) set_level(level Level) {
if unsafe { x.mu == 0 } {
return
}
x.mu.lock()
x.Log.set_level(level)
x.mu.unlock()
}
// set_always_flush called with true, will make the log flush after every single .fatal(), .error(), .warn(), .info(), .debug() call.
// That can be much slower, if you plan to do lots of frequent calls, but if your program exits early or crashes, your logs will be more complete.
pub fn (mut x ThreadSafeLog) set_always_flush(should_flush bool) {
if unsafe { x.mu == 0 } {
return
}
x.mu.lock()
x.Log.set_always_flush(should_flush)
x.mu.unlock()
}
// debug logs a debug message
pub fn (mut x ThreadSafeLog) debug(s string) {
if unsafe { x.mu == 0 } {
return
}
x.mu.lock()
x.Log.debug(s)
x.mu.unlock()
}
// info logs an info messagep
pub fn (mut x ThreadSafeLog) info(s string) {
if unsafe { x.mu == 0 } {
return
}
x.mu.lock()
x.Log.info(s)
x.mu.unlock()
}
// warn logs a warning message
pub fn (mut x ThreadSafeLog) warn(s string) {
if unsafe { x.mu == 0 } {
return
}
x.mu.lock()
x.Log.warn(s)
x.mu.unlock()
}
// error logs an error message
pub fn (mut x ThreadSafeLog) error(s string) {
if unsafe { x.mu == 0 } {
return
}
x.mu.lock()
x.Log.error(s)
x.mu.unlock()
}
// fatal logs a fatal message, and panics
@[noreturn]
pub fn (mut x ThreadSafeLog) fatal(s string) {
if unsafe { x.mu == 0 } {
panic(s)
}
x.mu.lock()
defer {
// TODO: Log.fatal() is marked as noreturn, but this defer is allowed.
// Think whether it should be, or if it should be a compiler notice at least,
// since it would not be reached at all (.fatal() panics).
x.mu.unlock()
}
x.Log.fatal(s)
}