diff --git a/.gitignore b/.gitignore index 02e493c..116e4a0 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,5 @@ bin/ # vweb and database *.db *.js + +*.bak diff --git a/examples/basic.v b/examples/basic.v index 12113c9..fe75cdb 100644 --- a/examples/basic.v +++ b/examples/basic.v @@ -19,8 +19,8 @@ fn main() { log.info().message('Hello, World!').send() // You can set your own named fields. - log.info().field('random_string', rand.string(5)).send() - log.info().field('answer', 42).field('computed_by', 'Deep Thought').send() + log.info().add('random_string', rand.string(100)).send() + log.info().add('answer', 42).add('computed_by', 'Deep Thought').send() // Errors can be passed to logger as is. log.error().message('this line contains error').error(error('oops')).send() diff --git a/examples/json_log.v b/examples/json_log.v index 5ed8072..ad27d0d 100644 --- a/examples/json_log.v +++ b/examples/json_log.v @@ -15,7 +15,7 @@ fn main() { } log.info().message('Hello, World!').send() - log.info().field('random_string', rand.string(5)).send() - log.info().field('answer', 42).field('computed_by', 'Deep Thought').send() + log.info().add('random_string', rand.string(100)).send() + log.info().add('answer', 42).add('computed_by', 'Deep Thought').send() log.error().message('this line contains error').error(error('oops')).send() } diff --git a/examples/write_to_file.v b/examples/write_to_file.v new file mode 100644 index 0000000..d613274 --- /dev/null +++ b/examples/write_to_file.v @@ -0,0 +1,26 @@ +import os +import structlog + +fn main() { + // Open a file in append mode. If file does not exists it will be created. + log_path := os.join_path_single(os.temp_dir(), 'example_log') + log_file := os.open_file(log_path, 'a+') or { + eprintln('Error: cound not open log file ${log_path}: ${err}') + exit(1) + } + + eprintln('Log file location: ${log_path}') + + // Initialize logger with os.File as writer. + log := structlog.new( + handler: structlog.TextHandler{ + color: false + writer: log_file + } + ) + defer { + log.close() + } + + log.info().message('Hello, World!').send() +} diff --git a/structlog.v b/structlog.v index ba705e9..a6d7226 100644 --- a/structlog.v +++ b/structlog.v @@ -114,15 +114,21 @@ pub fn (r Record) prepend(field ...Field) Record { } } +// add adds new field with given name and value to a record and returns the modified record. +pub fn (r Record) add(name string, value Value) Record { + return r.append(field(name, value)) +} + // field adds new field with given name and value to a record and returns the modified record. +@[deprecated: 'use add() instead'] pub fn (r Record) field(name string, value Value) Record { return r.append(Field{ name: name, value: value }) } // message adds new message field to a record and returns the modified record. -// This is a shothand for `field('message', 'message text')`. +// This is a shothand for `add('message', 'message text')`. pub fn (r Record) message(s string) Record { - return r.field('message', s) + return r.add('message', s) } // error adds an error as new field to a record and returns the modified record. @@ -351,6 +357,9 @@ pub struct JSONHandler { pub fn (mut h JSONHandler) handle(rec Record) ! { str := json.encode[map[string]Value](rec.fields.as_map()) + '\n' h.writer.write(str.bytes())! + if h.writer is os.File { + h.writer.flush() + } } pub struct TextHandler { @@ -411,7 +420,7 @@ pub fn (mut h TextHandler) handle(rec Record) ! { buf.write_byte(` `) buf.write_string(quote(v.str())) if j != field.value.len { - buf.write_byte(` `) + buf.write_string(', ') } } } else { @@ -422,12 +431,19 @@ pub fn (mut h TextHandler) handle(rec Record) ! { } } } - if i != rec.fields.len { - buf.write_byte(` `) + if i + 1 != rec.fields.len { + if i in [0, 1] { + buf.write_byte(` `) + } else { + buf.write_string(', ') + } } } buf.write_byte(`\n`) h.writer.write(buf)! + if h.writer is os.File { + h.writer.flush() + } } @[inline] @@ -440,3 +456,78 @@ fn quote(input string) string { } return "'" + input + "'" } + +// struct_adapter generates the log fields list form a flat struct. +// Supported struct field attrubutes: +// +// | Attribute | Meaning | +// | ------------------- | ------------------------------------------ | +// | `@[skip]` | Do not process field at all | +// | `@[structlog: '-']` | Do not process field at all | +// | `@[omitempty]` | Do not process field if it has empty value | +// +// Note: Nested struct fields are not supported. +pub fn struct_adapter[T](s T) []Field { + $if T !is $struct { + $compile_error('structlog.struct_adapted: only struct types is accepted') + } + mut fields := []Field{} + mut skip := false + mut omitempty := false + $for f in s.fields { + skip = false + for attr in f.attrs { + if attr == 'skip' || (attr.starts_with('structlog: ') + && attr.all_after('structlog: ').trim('"\'') == '-') { + skip = true + } + if attr == 'omitempty' { + omitempty = true + } + } + value := s.$(f.name) + if omitempty { + skip = check_is_empty(value) or { false } + } + if !skip { + fields << field(f.name, value) + } + } + return fields +} + +fn check_is_empty[T](val T) ?bool { + $if val is string { + if val == '' { + return false + } + } $else $if val is $int || val is $float { + if val == 0 { + return false + } + } $else $if val is ?string { + return val ? != '' + } $else $if val is ?int { + return val ? != 0 + } $else $if val is ?f64 || val is ?f32 { + return val ? != 0.0 + } + return true +} + +// field creates new `Field` with given name and value. +// Map values will be transformed to `map[string]Value`. +pub fn field[T](name string, value T) Field { + $if value is $struct { + $compile_error('structlog.field: cannot pass struct as field value') + } + $if value is $map { + mut value_map := map[string]Value{} + for k, v in value { + value_map[k.str()] = Value(v) + } + return Field{name, value_map} + } $else { + return Field{name, value} + } +} diff --git a/v.mod b/v.mod index febe852..a7c9148 100644 --- a/v.mod +++ b/v.mod @@ -1,7 +1,7 @@ Module { name: 'structlog' description: 'Structured logs' - version: '0.3.0' + version: '0.4.0' license: 'MIT' dependencies: [] }