This commit is contained in:
ge
2026-01-03 16:38:28 +03:00
commit 1fa21a9c74
12 changed files with 623 additions and 0 deletions

435
structlog.v Normal file
View File

@@ -0,0 +1,435 @@
module structlog
import io
import os
import strings
import time
import term
import x.json2 as json
pub interface RecordHandler {
mut:
// handle method must prepare the Record for writing and write it.
handle(rec Record) !
}
pub enum Level {
none // disables all logs.
fatal // disables error, warn, info, debug and trace.
error // disables warn, info, debug and trace.
warn // disables info, debug and trace.
info // disables debug and trace.
debug // disables trace.
trace
}
pub type Value = i8
| i16
| i32
| i64
| int
| isize
| u8
| u16
| u32
| u64
| usize
| f32
| f64
| string
| bool
| []Value
| map[string]Value
// str returns a string representation of Value.
pub fn (v Value) str() string {
return match v {
i8 { v.str() }
i16 { v.str() }
i32 { v.str() }
i64 { v.str() }
int { v.str() }
isize { v.str() }
u8 { v.str() }
u16 { v.str() }
u32 { v.str() }
u64 { v.str() }
usize { v.str() }
f32 { v.str() }
f64 { v.str() }
string { v.str() }
bool { v.str() }
[]Value { v.str() }
map[string]Value { v.str() }
}
}
// Field represents a named field of log record.
pub struct Field {
pub:
name string
value Value
}
// as_map converts array of fields into map.
pub fn (f []Field) as_map() map[string]Value {
mut mapping := map[string]Value{}
for field in f {
mapping[field.name] = field.value
}
return mapping
}
@[noinit]
pub struct Record {
channel chan Record
pub:
level Level
fields []Field
}
// append adds new fields to a record and returns the modified record.
pub fn (r Record) append(field ...Field) Record {
if field.len == 0 {
return r
}
mut fields_orig := unsafe { r.fields }
fields_orig << field
return Record{
channel: r.channel
level: r.level
fields: &fields_orig
}
}
// prepend adds new fields to the beginning of the record and returns the modified record.
pub fn (r Record) prepend(field ...Field) Record {
if field.len == 0 {
return r
}
mut new_fields := unsafe { field }
new_fields << r.fields
return Record{
channel: r.channel
level: r.level
fields: new_fields
}
}
// field adds new field with given name and value to a record and returns the modified record.
pub fn (r Record) field(name string, value Value) Record {
return r.append(Field{ name: name, value: value })
}
// field adds new message field to a record and returns the modified record.
// This is a shothand for `field('message', 'message text')`.
pub fn (r Record) message(s string) Record {
return r.field('message', s)
}
// error adds an error as new field to a record and returns the modified record.
// The IError .msg() and .code() methods output will be logged.
pub fn (r Record) error(err IError) Record {
return r.append(Field{
name: 'error'
value: {
'msg': Value(err.msg())
'code': Value(err.code())
}
})
}
// send sends a record to the record handler for the futher processing and writing.
pub fn (r Record) send() {
r.channel <- r
}
pub enum TimestampFormat {
default
rfc3339
rfc3339_micro
rfc3339_nano
ss
ss_micro
ss_milli
ss_nano
unix
unix_micro
unix_milli
unix_nano
custom
}
@[params]
pub struct LogConfig {
pub:
// level holds a logging level for the logger.
// This value cannot be changed after logger initialization.
level Level = .info
add_level bool = true // if true add `level` field to all log records.
add_timestamp bool = true // if true add `timestamp` field to all log records.
// timestamp_format sets the format of datettime for logs.
// TimestampFormat values map 1-to-1 to the date formats provided by `time.Time`.
// If .custom format is selected the timestamp_custom field must be set.
timestamp_format TimestampFormat = .rfc3339
// timestamp_custom sets the custom datetime string format if timestapm_format is
// set to .custom. See docs for Time.format_custom() fn from stadnard `time` module.
timestamp_custom string
// If timestamp_local is true the local time will be used instead of UTC.
timestamp_local bool
// handler holds a log record handler object which is used to process logs.
handler RecordHandler = TextHandler{
writer: os.stdout()
}
}
fn timestamp(format TimestampFormat, custom string, local bool) Value {
mut t := time.utc()
if local {
t = t.local()
}
return match format {
.default { t.format() }
.rfc3339 { t.format_rfc3339() }
.rfc3339_micro { t.format_rfc3339_micro() }
.rfc3339_nano { t.format_rfc3339_nano() }
.ss { t.format_ss() }
.ss_micro { t.format_ss_micro() }
.ss_milli { t.format_ss_milli() }
.ss_nano { t.format_ss_nano() }
.unix { t.unix() }
.unix_micro { t.unix_micro() }
.unix_milli { t.unix_milli() }
.unix_nano { t.unix_nano() }
.custom { t.custom_format(custom) }
}
}
// new creates new logger with given config. See LogConfig for defaults.
// This function starts a separate thread for processing and writing logs.
// The calling code MUST wait for this thread to complete to ensure all logs
// are written correctly. To do this, close the logger as shown in the examples.
// Example:
// ```v ignore
// log := structlog.new()
// defer {
// log.close()
// }
// ```
pub fn new(config LogConfig) StructuredLog {
ch := chan Record{cap: 4096}
mut logger := StructuredLog{
LogConfig: config
channel: ch
}
handler_thread := go fn [mut logger] () {
loop: for {
mut rec := <-logger.channel or { break }
if int(rec.level) > int(logger.level) {
continue loop
}
mut extra_fields := []Field{}
if logger.add_timestamp {
extra_fields << Field{
name: 'timestamp'
value: timestamp(logger.timestamp_format, logger.timestamp_custom,
logger.timestamp_local)
}
}
if logger.add_level {
extra_fields << Field{
name: 'level'
value: rec.level.str()
}
}
rec = rec.prepend(...extra_fields)
mut handler := logger.handler
handler.handle(rec) or { eprintln('error when handling log record!') }
if rec.level == .fatal {
exit(1)
}
}
}()
logger.handler_thread = handler_thread
return logger
}
@[heap; noinit]
pub struct StructuredLog {
LogConfig
mut:
channel chan Record
handler_thread thread
}
fn (s StructuredLog) record(level Level) Record {
return Record{
channel: s.channel
level: level
}
}
// trace creates new log record with trace level.
pub fn (s StructuredLog) trace() Record {
return s.record(.trace)
}
// debug creates new log record with debug level.
pub fn (s StructuredLog) debug() Record {
return s.record(.debug)
}
// info creates new log record with info level.
pub fn (s StructuredLog) info() Record {
return s.record(.info)
}
// warn creates new log record wth warning level.
pub fn (s StructuredLog) warn() Record {
return s.record(.warn)
}
// error creates new log record with error level.
pub fn (s StructuredLog) error() Record {
return s.record(.error)
}
// fatal creates new log record with fatal level.
// Note: After calling `send()` on record with fatal level the program will
// immediately exit with exit code 1.
pub fn (s StructuredLog) fatal() Record {
return s.record(.fatal)
}
// close closes the internal communication channell (which is used for transfer
// log messages) and waits for record handler thread. It MUST be called for
// normal log processing.
pub fn (s StructuredLog) close() {
s.channel.close()
s.handler_thread.wait()
}
// DefaultHandler is a default empty implementation of RecordHandler interface.
// Its only purpose for existence is to be embedded in a concrete implementation
// of the interface for common struct fields.
pub struct DefaultHandler {
pub mut:
writer io.Writer
}
// handle is the default implementation of handle method of RecordHandler. It does nothing.
pub fn (mut h DefaultHandler) handle(rec Record) ! {}
pub struct JSONHandler {
DefaultHandler
}
// handle converts the log record into json string and writes it into underlying writer.
pub fn (mut h JSONHandler) handle(rec Record) ! {
str := json.encode[map[string]Value](rec.fields.as_map()) + '\n'
h.writer.write(str.bytes())!
}
pub struct TextHandler {
DefaultHandler
pub:
// If true use colors in log messages. Otherwise disable colors at all.
// Turning on/off color here does not affect any colors that may be contained
// within the log itself i.e. in-string ANSI escape sequences are not processed.
color bool = true
}
// handle builds a log string from given record and writes it into underlying writer.
pub fn (mut h TextHandler) handle(rec Record) ! {
mut buf := strings.new_builder(512)
for i, field in rec.fields {
match field.name {
'timestamp' {
if field.value is string {
buf.write_string(field.value)
} else {
buf.write_string((field.value as i64).str())
}
}
'level' {
mut lvl := ''
if h.color {
lvl = match rec.level {
.trace { term.magenta('TRACE') }
.debug { term.cyan('DEBUG') }
.info { term.white('INFO ') }
.warn { term.yellow('WARN ') }
.error { term.red('ERROR') }
.fatal { term.bg_red('FATAL') }
.none { '' }
}
} else {
lvl = match rec.level {
.trace { 'TRACE' }
.debug { 'DEBUG' }
.info { 'INFO ' }
.warn { 'WARN ' }
.error { 'ERROR' }
.fatal { 'FATAL' }
.none { '' }
}
}
buf.write_byte(`[`)
buf.write_string(lvl)
buf.write_byte(`]`)
}
else {
if field.value is map[string]Value {
mut j := 0
for k, v in field.value {
j++
buf.write_string('${field.name}.${k}')
buf.write_byte(`:`)
buf.write_byte(` `)
buf.write_string(quote(v.str()))
if j != field.value.len {
buf.write_byte(` `)
}
}
} else {
buf.write_string(field.name)
buf.write_byte(`:`)
buf.write_byte(` `)
buf.write_string(quote(field.value.str()))
}
}
}
if i != rec.fields.len {
buf.write_byte(` `)
}
}
buf.write_byte(`\n`)
h.writer.write(buf)!
}
@[inline]
fn quote(input string) string {
if !input.contains(' ') {
return input
}
if input.contains("'") {
return '"' + input + '"'
}
return "'" + input + "'"
}