Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
module: ["."]
module: [".", "x/xlog"]

steps:
- uses: actions/setup-go@v5
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
module: ["."]
module: [".", "x/xlog"]

steps:

Expand Down
5 changes: 5 additions & 0 deletions x/xlog/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module github.com/fogfish/logger/x/xlog

go 1.23.1

require github.com/fogfish/logger/v3 v3.2.0
2 changes: 2 additions & 0 deletions x/xlog/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
github.com/fogfish/logger/v3 v3.2.0 h1:YjCyV+KvmacVvRy37RWH5431UjTGtPE1CSj4N9XS+1E=
github.com/fogfish/logger/v3 v3.2.0/go.mod h1:hsucoJz/3OX90UdYrXykcKvjjteBnPcYSTr4Rie0ZqU=
60 changes: 60 additions & 0 deletions x/xlog/types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
//
// Copyright (C) 2021 - 2024 Dmitry Kolesnikov
//
// This file may be modified and distributed under the terms
// of the MIT license. See the LICENSE file for details.
// https://github.com/fogfish/logger
//

package xlog

import (
"encoding/json"
"strconv"
"time"
)

// Since logs duration passed since given time
type Since time.Time

func (s Since) String() string { return time.Since(time.Time(s)).String() }
func (s Since) MarshalJSON() ([]byte, error) { return json.Marshal(s.String()) }

// SinceNow return instance of [Since] type initialized with [time.Now]
func SinceNow() Since { return Since(time.Now()) }

//------------------------------------------------------------------------------

// PerSecond logs rates per second since given time
type PerSecond struct {
T time.Time
Acc int
}

func (r PerSecond) String() string {
v := float64(r.Acc) / time.Since(r.T).Seconds()
return strconv.FormatFloat(v, 'f', 4, 64)
}

func (r PerSecond) MarshalJSON() ([]byte, error) { return json.Marshal(r.String()) }

// PerSecondNow return instance of [PerSecond] type initialized with [time.Now]
func PerSecondNow() *PerSecond { return &PerSecond{T: time.Now(), Acc: 0} }

//------------------------------------------------------------------------------

// MillisecondOp logs millisecond required for operation
type MillisecondOp struct {
T time.Time
Acc int
}

func (r MillisecondOp) String() string {
v := time.Since(r.T).Nanoseconds() / int64(r.Acc)
return time.Duration(v).String()
}

func (r MillisecondOp) MarshalJSON() ([]byte, error) { return json.Marshal(r.String()) }

// MillisecondOpNow return instance of [MillisecondOp] type initialized with [time.Now]
func MillisecondOpNow() *MillisecondOp { return &MillisecondOp{T: time.Now(), Acc: 0} }
77 changes: 77 additions & 0 deletions x/xlog/types_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
//
// Copyright (C) 2021 - 2024 Dmitry Kolesnikov
//
// This file may be modified and distributed under the terms
// of the MIT license. See the LICENSE file for details.
// https://github.com/fogfish/logger
//

package xlog_test

import (
"bytes"
"encoding/json"
"log/slog"
"regexp"
"testing"
"time"

"github.com/fogfish/logger/x/xlog"
)

func TestSince(t *testing.T) {
itShouldBe(t, "since", "10[0-9].[0-9]+ms",
func(log *slog.Logger) {
v := xlog.SinceNow()
time.Sleep(100 * time.Millisecond)
log.Info("", "since", v)
},
)
}

func TestPerSecond(t *testing.T) {
itShouldBe(t, "rate", "1[0-9][0-9][0-9][0-9].[0-9]+",
func(log *slog.Logger) {
v := xlog.PerSecondNow()
v.Acc += 1000
time.Sleep(90 * time.Millisecond)
log.Info("", "rate", v)
},
)
}

func TestMillisecondOp(t *testing.T) {
itShouldBe(t, "ms/op", "1[0-9][0-9]",
func(log *slog.Logger) {
v := xlog.MillisecondOpNow()
v.Acc += 1
time.Sleep(100 * time.Millisecond)
log.Info("", "ms/op", v)
},
)
}

//------------------------------------------------------------------------------

func itShouldBe(t *testing.T, key string, expected string, f func(log *slog.Logger)) {
t.Helper()

buf := &bytes.Buffer{}
log := slog.New(slog.NewJSONHandler(buf, nil))

f(log)

var val map[string]any
if err := json.Unmarshal(buf.Bytes(), &val); err != nil {
t.Errorf("invalid format, json expected %v", buf.String())
}

v, has := val[key]
if !has {
t.Errorf("no `%s` found in %v", key, val)
}

if matched, err := regexp.MatchString(expected, v.(string)); err != nil || !matched {
t.Errorf("invalid `%s` value %v, expected %s", key, v, expected)
}
}
11 changes: 11 additions & 0 deletions x/xlog/version.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
//
// Copyright (C) 2021 - 2025 Dmitry Kolesnikov
//
// This file may be modified and distributed under the terms
// of the MIT license. See the LICENSE file for details.
// https://github.com/fogfish/logger
//

package xlog

const Version = "x/xlog/v0.0.1"
91 changes: 91 additions & 0 deletions x/xlog/xlog.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
//
// Copyright (C) 2021 - 2025 Dmitry Kolesnikov
//
// This file may be modified and distributed under the terms
// of the MIT license. See the LICENSE file for details.
// https://github.com/fogfish/logger
//

package xlog

import (
"context"
"log/slog"
"runtime"
"time"

"github.com/fogfish/logger/v3"
)

func log(
ctx context.Context,
logger *slog.Logger,
level slog.Level,
msg string,
args ...any,
) {
if !logger.Enabled(ctx, level) {
return
}
var pc uintptr
var pcs [1]uintptr
// skip [runtime.Callers, this function, this function's caller]
runtime.Callers(3, pcs[:])
pc = pcs[0]
r := slog.NewRecord(time.Now(), level, msg, pc)
r.Add(args...)
if ctx == nil {
ctx = context.Background()
}
_ = logger.Handler().Handle(ctx, r)
}

// EMERGENCY
// system is unusable, panic execution of current routine/application,
// it is notpossible to gracefully terminate it.
func Emergency(msg string, err error, args ...any) {
if err != nil {
args = append(args, slog.Any("err", err))
}
log(context.Background(), slog.Default(), logger.EMERGENCY, msg, args...)
panic(err)
}

// CRITICAL
// system is failed, response actions must be taken immediately,
// the application is not able to execute correctly but still
// able to gracefully exit.
func Critical(msg string, err error, args ...any) {
if err != nil {
args = append(args, slog.Any("err", err))
}
log(context.Background(), slog.Default(), logger.CRITICAL, msg, args...)
}

// ERROR
// system is failed, unable to recover from error.
// The failure do not have global catastrophic impacts but
// local functionality is impaired, incorrect result is returned.
func Error(msg string, err error, args ...any) {
if err != nil {
args = append(args, slog.Any("err", err))
}
log(context.Background(), slog.Default(), logger.ERROR, msg, args...)
}

// WARN
// system is failed, unable to recover, degraded functionality.
// The failure is ignored and application still capable to deliver
// incomplete but correct results.
func Warn(msg string, err error, args ...any) {
if err != nil {
args = append(args, slog.Any("err", err))
}
log(context.Background(), slog.Default(), logger.WARN, msg, args...)
}

// NOTICE
// system is failed, error is recovered, no impact
func Notice(msg string, args ...any) {
log(context.Background(), slog.Default(), logger.NOTICE, msg, args...)
}
80 changes: 80 additions & 0 deletions x/xlog/xlog_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
//
// Copyright (C) 2021 - 2025 Dmitry Kolesnikov
//
// This file may be modified and distributed under the terms
// of the MIT license. See the LICENSE file for details.
// https://github.com/fogfish/logger
//

package xlog_test

import (
"bytes"
"io"
"log/slog"
"strings"
"testing"

"github.com/fogfish/logger/v3"
"github.com/fogfish/logger/x/xlog"
)

func TestStdioLogger(t *testing.T) {
b := &bytes.Buffer{}
slog.SetDefault(slog.New(logger.NewStdioHandler(logger.WithWriter(b))))

t.Run("Notice", func(t *testing.T) {
defer b.Reset()

xlog.Notice("test")
txt := b.String()
if !strings.Contains(txt, "NTC") ||
!strings.Contains(txt, "test") ||
!strings.Contains(txt, "source") ||
!strings.Contains(txt, "lggr") {
t.Errorf("unexpected log line %s", txt)
}
})

t.Run("Info", func(t *testing.T) {
defer b.Reset()

xlog.Warn("test", io.EOF)
txt := b.String()
if !strings.Contains(txt, "WRN") ||
!strings.Contains(txt, "EOF") ||
!strings.Contains(txt, "test") ||
!strings.Contains(txt, "source") ||
!strings.Contains(txt, "lggr") {
t.Errorf("unexpected log line %s", txt)
}
})

t.Run("Error", func(t *testing.T) {
defer b.Reset()

xlog.Error("test", io.EOF)
txt := b.String()
if !strings.Contains(txt, "ERR") ||
!strings.Contains(txt, "EOF") ||
!strings.Contains(txt, "test") ||
!strings.Contains(txt, "source") ||
!strings.Contains(txt, "lggr") {
t.Errorf("unexpected log line %s", txt)
}
})

t.Run("Critical", func(t *testing.T) {
defer b.Reset()

xlog.Critical("test", io.EOF)
txt := b.String()
if !strings.Contains(txt, "CRT") ||
!strings.Contains(txt, "EOF") ||
!strings.Contains(txt, "test") ||
!strings.Contains(txt, "source") ||
!strings.Contains(txt, "lggr") {
t.Errorf("unexpected log line %s", txt)
}
})
}
Loading