Skip to content

Commit

Permalink
new: initial version
Browse files Browse the repository at this point in the history
  • Loading branch information
Pavel Ivanov committed Feb 11, 2024
1 parent 10f2b6b commit c63b992
Show file tree
Hide file tree
Showing 30 changed files with 28,857 additions and 2 deletions.
57 changes: 57 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
name: Continuous Integration

on:
push:
tags:
- v*
branches:
- master
- main
pull_request:

permissions:
contents: read

jobs:
linters:
name: Run linters
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.21'
cache-dependency-path: |
go.sum
test/go.sum
- name: Lint .
uses: golangci/golangci-lint-action@v3
with:
version: v1.56
# Disable caching as a workaround for https://github.com/golangci/golangci-lint-action/issues/135.
# The line can be removed once the golangci-lint issue is resolved.
skip-pkg-cache: true
- name: Lint other modules
run: go list -m -f '{{.Dir}}/...' | golangci-lint run

unit-tests:
name: Run unit tests
runs-on: ubuntu-latest
strategy:
matrix:
go: ['1.21']
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go }}
- name: Test
run: go list -m -f '{{.Dir}}/...' | xargs go test -race -coverprofile=cover.out -coverpkg=./...

- name: Collect coverage
run: go tool cover -html=cover.out -o cover.html

- name: Upload coverage to codecov.io
uses: codecov/codecov-action@v4
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
79 changes: 79 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
linters-settings:
dupl:
threshold: 150
exhaustive:
default-signifies-exhaustive: false
funlen:
lines: 100
statements: 50
goconst:
min-len: 2
min-occurrences: 2
gocyclo:
min-complexity: 32
goimports:
local-prefixes: github.com/pamburus
govet:
check-shadowing: false
lll:
line-length: 140
maligned:
suggest-new: true
nolintlint:
allow-unused: false # report any unused nolint directives
require-explanation: true # require an explanation for nolint directives
require-specific: false # don't require nolint directives to be specific about which linter is being skipped

linters:
# Please, do not use `enable-all`: it's deprecated and will be removed soon.
# Inverted configuration with `enable-all` and `disable` is not scalable during updates of golangci-lint.
disable-all: true
enable:
- asciicheck
- dupl
- errcheck
- exportloopref
- gocritic
- gocyclo
- godot
- gofmt
- goimports
- gosec
- gosimple
- govet
- ineffassign
- misspell
- nakedret
- nlreturn
- prealloc
- revive
- staticcheck
- stylecheck
- typecheck
- unconvert
- unparam
- unused
- vet
- vetshadow
- whitespace

issues:
# Disable this option because it causes golint to hide almost all issues.
exclude-use-default: false
# Excluding configuration per-path, per-linter, per-text and per-source.
exclude-rules:
# The dot-imports linter is disabled for the test files because it is convenient to dot-import third-party testing frameworks.
- linters: [revive]
text: '^dot-imports: '
paths:
- '*_test.go'
# The staticcheck linter reports that `nil` context is passed, but it is intentionally done for the sake of the tests.
- linters: [staticcheck]
text: '^SA1012:'
paths:
- '*_test.go'
# The duplicate lines in the benchmark_test.go file are expected because any attempt to deduplicate them would affect the performance of the benchmark.
- linters: [dupl]
text: 'lines are duplicate'
paths:
- 'benchmark_test.go'
23 changes: 23 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Go: Benchmark LogAfterWith",
"type": "go",
"request": "launch",
"mode": "test",
"program": "${workspaceFolder}",
"args": [
"-test.run=^$",
"-test.bench=^BenchmarkLogger/slogx/JSON/Enabled/WithSource/3xWrapped/LogAfterWith/TwoAndThreeAttrs$",
"-test.benchtime=1x"
],
"env": {
"GOFLAGS": "-count=1"
}
}
]
}
25 changes: 23 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,23 @@
# slogx
Extensions and helpers for log/slog package.
# slogx [![GoDoc][doc-img]][doc] [![Build Status][ci-img]][ci] [![Coverage Status][cov-img]][cov]

Module [slogx](https://pkg.go.dev/github.com/pamburus/slogx) provides extensions and helpers for the [log/slog](https://pkg.go.dev/log/slog) package.

## Packages
* [slogx](./README.md)
* [slogc](slogc/README.md)

Package [slogx](https://pkg.go.dev/github.com/pamburus/slogx) provides [Logger](https://pkg.go.dev/github.com/pamburus/slogx#Logger) as an alternative to [slog.Logger](https://pkg.go.dev/log/slog#Logger), which focuses on performance and makes several design changes to improve it:
* It does not provide convenience methods for attributes that may affect performace. All methods accept attributes only as [slog.Attr](https://pkg.go.dev/log/slog#Attr).
* It provides an [option](https://pkg.go.dev/github.com/pamburus/slogx#Logger.WithSource) to disable the inclusion of [source](https://pkg.go.dev/log/slog#Source) information into the log [Record](https://pkg.go.dev/log/slog#Record). This can improve performance up to 100% in cases where the source information is not included by the [Handler](https://pkg.go.dev/log/slog#Handler) anyway.
* Its [With](https://pkg.go.dev/github.com/pamburus/slogx#Logger.With) method does not immediately call the [WithAttrs](https://pkg.go.dev/log/slog#Handler.WithAttrs) method of the handler, instead it buffers up to 4 attributes which are then will be added to each log [Record](https://pkg.go.dev/log/slog#Record). This improves performance if you need to define a temporary set of attributes in a function and log a few messages with those attributes a few times. It also significantly improves performance when the logger is disabled. It is because calling [WithAttrs](https://pkg.go.dev/log/slog#Handler.WithAttrs) is quite an expensive operation, especially if the [Handler](https://pkg.go.dev/log/slog#Handler) is wrapped several times. That is, each layer will call the underlying handler's [WithAttrs](https://pkg.go.dev/log/slog#Handler.WithAttrs) method and cause a lot of allocations. But what if the message is discarded because the logger is disabled? Yes, it will be a waste of CPU time. So for temporary [With](https://pkg.go.dev/github.com/pamburus/slogx#Logger.With) attribute sets, it is usually more efficient to hold them at the [Logger](https://pkg.go.dev/github.com/pamburus/slogx#Logger). If any of the [WithGroup](https://pkg.go.dev/github.com/pamburus/slogx#Logger.WithGroup), [Handler](https://pkg.go.dev/github.com/pamburus/slogx#Logger.Handler), or [LongTerm](https://pkg.go.dev/github.com/pamburus/slogx#Logger.LongTerm) methods are called later, the temporary attributes will be flushed using the [WithAttrs](https://pkg.go.dev/log/slog#Handler.WithAttrs) method.
* It provides the [WithLongTerm](https://pkg.go.dev/github.com/pamburus/slogx#Logger.WithLongTerm) method, which acts as a sequence of [With](https://pkg.go.dev/github.com/pamburus/slogx#Logger.With) and [LongTerm](https://pkg.go.dev/github.com/pamburus/slogx#Logger.LongTerm) method calls and is needed for cases where the resulting logger is intended to be reused multiple times and may reside in a rather long-lived context.

## Performance
* See [benchmark results](doc/benchmark/README.md) for details.

[doc-img]: https://pkg.go.dev/badge/github.com/pamburus/slogx
[doc]: https://pkg.go.dev/github.com/pamburus/slogx
[ci-img]: https://github.com/pamburus/slogx/actions/workflows/ci.yml/badge.svg
[ci]: https://github.com/pamburus/slogx/actions/workflows/ci.yml
[cov-img]: https://codecov.io/gh/pamburus/slogx/graph/badge.svg?token=0TF6JD4KDU
[cov]: https://codecov.io/gh/pamburus/slogx
108 changes: 108 additions & 0 deletions attr.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package slogx

import (
"log/slog"
"slices"
)

// ErrorAttr returns an attribute with the error.
func ErrorAttr(err error) slog.Attr {
return slog.Any(ErrorKey, err)
}

const (
// ErrorKey is the key used for the error attribute.
ErrorKey = "error"
)

// AttrPack is a an optimized pack of attributes.
type AttrPack struct {
front [4]slog.Attr
nFront int
back []slog.Attr
}

// Clone returns a copy of the AttrPack without a shared state.
func (r AttrPack) Clone() AttrPack {
r.back = slices.Clip(r.back)

return r
}

// Len returns the number of attributes in the AttrPack.
func (r AttrPack) Len() int {
return r.nFront + len(r.back)
}

// Enumerate calls f on each Attr in the AttrPack.
func (r AttrPack) Enumerate(f func(slog.Attr) bool) {
for i := 0; i < r.nFront; i++ {
if !f(r.front[i]) {
return
}
}
for _, a := range r.back {
if !f(a) {
return
}
}
}

// Add appends the given Attrs to the AttrPack's list of Attrs.
func (r *AttrPack) Add(attrs ...slog.Attr) {
var i int
for i = 0; i < len(attrs) && r.nFront < len(r.front); i++ {
a := attrs[i]
if isEmptyGroup(a.Value) {
continue
}
r.front[r.nFront] = a
r.nFront++
}
if cap(r.back) > len(r.back) {
end := r.back[:len(r.back)+1][len(r.back)]
if !end.Equal(slog.Attr{}) {
panic("multiple copies of a slogx.AttrPack modified the shared state simultaneously, use Clone to avoid this")
}
}
ne := countEmptyGroups(attrs[i:])
r.back = slices.Grow(r.back, len(attrs[i:])-ne)
for _, a := range attrs[i:] {
if !isEmptyGroup(a.Value) {
r.back = append(r.back, a)
}
}
}

// Collect returns all the attributes in the AttrPack as a slice.
func (r *AttrPack) Collect() []slog.Attr {
attrs := make([]slog.Attr, 0, r.Len())
r.Enumerate(func(a slog.Attr) bool {
attrs = append(attrs, a)

return true
})

return attrs
}

// ---

func countEmptyGroups(as []slog.Attr) int {
n := 0
for _, a := range as {
if isEmptyGroup(a.Value) {
n++
}
}

return n
}

func isEmptyGroup(v slog.Value) bool {
if v.Kind() != slog.KindGroup {
return false
}

return len(v.Group()) == 0
}
Loading

0 comments on commit c63b992

Please sign in to comment.