7 Commits

Author SHA1 Message Date
ge
b423c161af all: various improvements
* Add os.File.flush() call to fix writing log into files.
* Add comma as TextHandler fields separator.
* Add struct_adapter()
* Add field() generic function to creating Field instances.
* Add new write_to_file.v example.
* Rename Record.field() to Record.add()
2026-03-28 18:23:57 +03:00
ge
c6d53758b3 mod: bump version 2026-02-05 01:07:29 +03:00
ge
a9a0e22d3a breaking: remove deprecations, fix timestamp settings 2026-02-05 01:07:02 +03:00
ge
f688b3cad5 cleanup, use struct update syntax 2026-01-11 08:57:56 +03:00
ge
8b13a596f7 LogConfig: deprecate timestamp_*, add Timestamp 2026-01-11 06:04:13 +03:00
ge
98738e773d doc: pass v vet in CI 2026-01-03 16:44:31 +03:00
ge
791b376ff3 ci: Add CI 2026-01-03 16:41:00 +03:00
9 changed files with 236 additions and 33 deletions

48
.github/workflows/docs.yaml vendored Normal file
View File

@@ -0,0 +1,48 @@
name: Docs
on:
push:
branches: [ "master" ]
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup V
run: |
wget -qO /tmp/v.zip https://github.com/vlang/v/releases/latest/download/v_linux.zip
unzip -q /tmp/v.zip -d /tmp
echo /tmp/v >> "$GITHUB_PATH"
- name: Build docs
run: |
v doc -f html -m .
pushd _docs
ln -vs ${{ github.event.repository.name }}.html index.html
ls -alFh
popd
- name: Upload static files as artifact
id: deployment
uses: actions/upload-pages-artifact@v3
with:
path: _docs/
deploy:
needs: build
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
permissions:
contents: read
pages: write
id-token: write

27
.github/workflows/test.yaml vendored Normal file
View File

@@ -0,0 +1,27 @@
name: Lint and test
on:
push:
branches: [ "master" ]
pull_request:
branches: [ "master" ]
workflow_dispatch:
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup V
run: |
wget -qO /tmp/v.zip https://github.com/vlang/v/releases/latest/download/v_linux.zip
unzip -q /tmp/v.zip -d /tmp
echo /tmp/v >> "$GITHUB_PATH"
- name: Run tests
run: |
v fmt -verify .
v vet -v -W -I -F -r .
v missdoc -r --verify .
v -stats test .

2
.gitignore vendored
View File

@@ -22,3 +22,5 @@ bin/
# vweb and database
*.db
*.js
*.bak

View File

@@ -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()

View File

@@ -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()
}

View File

@@ -4,8 +4,10 @@ import structlog
fn main() {
// Initialize logger with edited timestamp.
log := structlog.new(
timestamp_format: .unix
handler: structlog.JSONHandler{
timestamp: structlog.Timestamp{
format: .unix
}
handler: structlog.JSONHandler{
writer: os.stdout()
}
)

26
examples/write_to_file.v Normal file
View File

@@ -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()
}

View File

@@ -96,9 +96,8 @@ pub fn (r Record) append(field ...Field) Record {
mut fields_orig := unsafe { r.fields }
fields_orig << field
return Record{
channel: r.channel
level: r.level
fields: &fields_orig
...r
fields: &fields_orig
}
}
@@ -110,21 +109,26 @@ pub fn (r Record) prepend(field ...Field) Record {
mut new_fields := unsafe { field }
new_fields << r.fields
return Record{
channel: r.channel
level: r.level
fields: new_fields
...r
fields: new_fields
}
}
// 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 })
}
// field adds new message field to a record and returns the modified record.
// This is a shothand for `field('message', 'message text')`.
// message adds new message field to a record and returns the modified record.
// 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.
@@ -144,6 +148,25 @@ pub fn (r Record) send() {
r.channel <- r
}
pub struct Timestamp {
pub mut:
// format sets the format of datetime in logs. TimestampFormat values
// map 1-to-1 to the date formats provided by `time.Time`.
// If .custom format is selected the `custom` field must be set.
format TimestampFormat = .rfc3339
// custom sets the custom datetime string format if format is set to .custom.
// See docs for Time.format_custom() fn from stadnard `time` module.
custom string
// If local is true the local time will be used instead of UTC.
local bool
}
fn (t Timestamp) as_value() Value {
return timestamp(t.format, t.custom, t.local)
}
pub enum TimestampFormat {
default
rfc3339
@@ -167,21 +190,12 @@ pub:
// This value cannot be changed after logger initialization.
level Level = .info
// timestamp holds the timestamp settings.
timestamp Timestamp
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()
@@ -242,8 +256,7 @@ pub fn new(config LogConfig) StructuredLog {
if logger.add_timestamp {
extra_fields << Field{
name: 'timestamp'
value: timestamp(logger.timestamp_format, logger.timestamp_custom,
logger.timestamp_local)
value: logger.timestamp.as_value()
}
}
@@ -344,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 {
@@ -404,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 {
@@ -415,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]
@@ -433,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}
}
}

2
v.mod
View File

@@ -1,7 +1,7 @@
Module {
name: 'structlog'
description: 'Structured logs'
version: '0.1.0'
version: '0.4.0'
license: 'MIT'
dependencies: []
}