diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 568bca1..da0c3b6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - module: ["."] + module: [".", "x/xlog"] steps: - uses: actions/setup-go@v5 diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 5b88a22..626cd96 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - module: ["."] + module: [".", "x/xlog"] steps: diff --git a/x/xlog/go.mod b/x/xlog/go.mod new file mode 100644 index 0000000..6bc8029 --- /dev/null +++ b/x/xlog/go.mod @@ -0,0 +1,5 @@ +module github.com/fogfish/logger/x/xlog + +go 1.23.1 + +require github.com/fogfish/logger/v3 v3.2.0 diff --git a/x/xlog/go.sum b/x/xlog/go.sum new file mode 100644 index 0000000..7fd16d7 --- /dev/null +++ b/x/xlog/go.sum @@ -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= diff --git a/x/xlog/types.go b/x/xlog/types.go new file mode 100644 index 0000000..d49b192 --- /dev/null +++ b/x/xlog/types.go @@ -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} } diff --git a/x/xlog/types_test.go b/x/xlog/types_test.go new file mode 100644 index 0000000..e66789f --- /dev/null +++ b/x/xlog/types_test.go @@ -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) + } +} diff --git a/x/xlog/version.go b/x/xlog/version.go new file mode 100644 index 0000000..4c94a99 --- /dev/null +++ b/x/xlog/version.go @@ -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" diff --git a/x/xlog/xlog.go b/x/xlog/xlog.go new file mode 100644 index 0000000..52d401d --- /dev/null +++ b/x/xlog/xlog.go @@ -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...) +} diff --git a/x/xlog/xlog_test.go b/x/xlog/xlog_test.go new file mode 100644 index 0000000..1146b69 --- /dev/null +++ b/x/xlog/xlog_test.go @@ -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) + } + }) +}