Skip to content

Commit

Permalink
Initial logging package (#1)
Browse files Browse the repository at this point in the history
  • Loading branch information
anjmao authored Dec 12, 2024
1 parent 1abb988 commit 037e30d
Show file tree
Hide file tree
Showing 14 changed files with 703 additions and 0 deletions.
38 changes: 38 additions & 0 deletions .github/workflows/build.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
name: Build

on:
pull_request:
branches:
- main

jobs:
build:
name: Build
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}

- name: Secret Scanning
uses: trufflesecurity/trufflehog@main
with:
extra_args: --only-verified

- name: Setup Go 1.23
uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5
with:
go-version-file: "go.mod"

- name: Run golangci-lint
if: ${{ github.event_name == 'pull_request' && !contains(env.head_commit_message, '#skip-lint') }}
uses: golangci/[email protected]
with:
args: -v --timeout=5m
version: v1.60.3

- name: Test
if: ${{ github.event_name == 'pull_request' && !contains(env.head_commit_message, '#skip-test') }}
run: go test -race -short ./...

1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.idea
19 changes: 19 additions & 0 deletions .golangci.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
linters:
disable:
- wrapcheck
- err113
- contextcheck
- exhaustive
- protogetter
presets:
- bugs
- error
- unused

run:
tests: false

issues:
exclude-dirs:
- .github

11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Slog based logging for Go

## Install

```
go get github.com/castai/logging
```

## Example

See logging_test.go example test.
84 changes: 84 additions & 0 deletions export_handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package logging

import (
"context"
"log/slog"
)

type ExportHandlerConfig struct {
MinLevel slog.Level // Only export logs for this min log level.
BufferSize int // Logs channel size.
}

func NewExportHandler(cfg ExportHandlerConfig) *ExportHandler {
if cfg.BufferSize == 0 {
cfg.BufferSize = 1000
}

handler := &ExportHandler{
cfg: cfg,
ch: make(chan slog.Record, cfg.BufferSize),
}
return handler
}

// ExportHandler exports logs to separate channel available via Records()
type ExportHandler struct {
next slog.Handler
cfg ExportHandlerConfig

ch chan slog.Record
}

func (h *ExportHandler) Register(next slog.Handler) slog.Handler {
h.next = next
return h
}

func (h *ExportHandler) Records() <-chan slog.Record {
return h.ch
}

func (h *ExportHandler) Enabled(ctx context.Context, level slog.Level) bool {
if h.next == nil {
return true
}
return h.next.Enabled(ctx, level)
}

func (h *ExportHandler) Handle(ctx context.Context, record slog.Record) error {
if record.Level >= h.cfg.MinLevel {
select {
case <-ctx.Done():
return ctx.Err()
case h.ch <- record:
default:
}
}
if h.next == nil {
return nil
}
return h.next.Handle(ctx, record)
}

func (h *ExportHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
clone := &ExportHandler{
cfg: h.cfg,
ch: h.ch,
}
if h.next != nil {
clone.next = h.next.WithAttrs(attrs)
}
return clone
}

func (h *ExportHandler) WithGroup(name string) slog.Handler {
clone := &ExportHandler{
cfg: h.cfg,
ch: h.ch,
}
if h.next != nil {
clone.next = h.next.WithGroup(name)
}
return clone
}
34 changes: 34 additions & 0 deletions export_handler_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package logging_test

import (
"log/slog"
"testing"

"github.com/castai/logging"
"github.com/stretchr/testify/require"
)

func TestExportHandler(t *testing.T) {
r := require.New(t)

exportHandler := logging.NewExportHandler(logging.ExportHandlerConfig{
MinLevel: slog.LevelWarn,
BufferSize: 2,
})
log := logging.New(exportHandler)

log.Debug("msg1")
log.Info("msg2")
log.Warn("msg3")
log.WithField("k", "v").Error("msg4")
log.WithGroup("g").Error("msg5")

// Only warn and error should be inside the export channel.
msg1 := <-exportHandler.Records()
r.Equal("msg3", msg1.Message)
msg2 := <-exportHandler.Records()
r.Equal("msg4", msg2.Message)

// Ensure logs are not blocked.
log.Debug("msg5")
}
14 changes: 14 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
module github.com/castai/logging

go 1.23.2

require (
github.com/stretchr/testify v1.9.0
golang.org/x/time v0.6.0
)

require (
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
12 changes: 12 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
128 changes: 128 additions & 0 deletions logging.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package logging

import (
"context"
"fmt"
"log/slog"
"os"
"runtime"
"time"
)

type HandlerFunc func(next slog.Handler) slog.Handler

func (h HandlerFunc) Register(next slog.Handler) slog.Handler {
return h(next)
}

// Handler allows to chain multiple handlers.
// Order of execution is reverse to order of registration meaning first handler is executed last.
type Handler interface {
Register(next slog.Handler) slog.Handler
}

func MustParseLevel(lvlStr string) slog.Level {
var lvl slog.Level
err := lvl.UnmarshalText([]byte(lvlStr))
if err != nil {
panic("parsing log level from level string " + lvlStr)
}
return lvl
}

func New(handlers ...Handler) *Logger {
if len(handlers) == 0 {
handlers = []Handler{
NewTextHandler(TextHandlerConfig{
Level: slog.LevelInfo,
Output: os.Stdout,
AddSource: false,
}),
}
}

// Chain handlers. Execution is in reverse order.
var slogHandler slog.Handler
for _, handler := range handlers {
slogHandler = handler.Register(slogHandler)
}

log := slog.New(slogHandler)
return &Logger{Log: log}
}

// Logger is a small wrapper around slog with some extra methods
// for easier migration from logrus.
type Logger struct {
Log *slog.Logger
}

func (l *Logger) Error(msg string) {
l.doLog(slog.LevelError, msg) //nolint:govet
}

func (l *Logger) Errorf(format string, a ...any) {
l.doLog(slog.LevelError, format, a...)
}

func (l *Logger) Infof(format string, a ...any) {
l.doLog(slog.LevelInfo, format, a...)
}

func (l *Logger) Info(msg string) {
l.doLog(slog.LevelInfo, msg) //nolint:govet
}

func (l *Logger) Debug(msg string) {
l.doLog(slog.LevelDebug, msg) //nolint:govet
}

func (l *Logger) Debugf(format string, a ...any) {
l.doLog(slog.LevelDebug, format, a...)
}

func (l *Logger) Warn(msg string) {
l.doLog(slog.LevelWarn, msg) //nolint:govet
}

func (l *Logger) Warnf(format string, a ...any) {
l.doLog(slog.LevelWarn, format, a...)
}

func (l *Logger) Fatal(msg string) {
l.doLog(slog.LevelError, msg) //nolint:govet
os.Exit(1)
}

func (l *Logger) IsEnabled(lvl slog.Level) bool {
ctx := context.Background()
return l.Log.Handler().Enabled(ctx, lvl)
}

func (l *Logger) doLog(lvl slog.Level, msg string, args ...any) {
ctx := context.Background()
if !l.Log.Handler().Enabled(ctx, lvl) {
return
}
var pcs [1]uintptr
runtime.Callers(3, pcs[:])
if len(args) > 0 {
r := slog.NewRecord(time.Now(), lvl, fmt.Sprintf(msg, args...), pcs[0])
_ = l.Log.Handler().Handle(ctx, r) //nolint:contextcheck
} else {
r := slog.NewRecord(time.Now(), lvl, msg, pcs[0])
_ = l.Log.Handler().Handle(ctx, r) //nolint:contextcheck
}
}

func (l *Logger) With(args ...any) *Logger {
return &Logger{Log: l.Log.With(args...)}
}

func (l *Logger) WithField(k, v string) *Logger {
return &Logger{Log: l.Log.With(slog.String(k, v))}
}

func (l *Logger) WithGroup(name string) *Logger {
return &Logger{Log: l.Log.WithGroup(name)}
}
Loading

0 comments on commit 037e30d

Please sign in to comment.