Initial project import
This commit is contained in:
commit
23f2d29e32
11957 files changed
+1754577
No files matched your search
@@ -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.
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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')
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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') }
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
Reference in new issue
Block a user