Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: migrate to slog.Logger #31

Merged
merged 8 commits into from
Mar 11, 2024
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
18 changes: 10 additions & 8 deletions .github/actions/setup-go/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ description: |
Setup Go

inputs:
go-version:
description: Used Go version
default: '1.20'
cache:
description: Cache
required: false
default: "true"

runs:
using: "composite"
Expand All @@ -15,10 +16,11 @@ runs:
echo "Go version is set to ${{ inputs.go-version }}"
echo "GO_VERSION=${{ inputs.go-version }}" >> $GITHUB_ENV
shell: bash
name: Setup Go
- id: go-setup
uses: actions/setup-go@v3
uses: actions/setup-go@v4
with:
go-version: ${{ env.GO_VERSION }}
- run: |
go mod download
shell: bash
go-version-file: go.mod
check-latest: true
cache: ${{ inputs.cache }}

2 changes: 1 addition & 1 deletion .github/workflows/lint.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,4 @@ jobs:
- name: Run golangci-lint
uses: golangci/golangci-lint-action@v3
with:
version: v1.51.1
version: v1.56.1
28 changes: 23 additions & 5 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,28 @@ How to release a new version:

## [Unreleased]

## [0.6.2] - 2022-06-27
## [0.7.0] - 2024-03-11
### Changed
- Logging interface changes to `log/slog`.

NOTE: This is version presents a BREAKING CHANGE in the server's logger interface. The server now accepts a `slog.Logger` instead of a custom `ServerLogger`.
- Change in the logging middleware. The middleware now nests request specific data under the "request" group.
- Updated from Go 1.20 to Go 1.22.
- Updated packages:
```diff
- github.com/go-chi/chi/v5 v5.0.8
- github.com/google/uuid v1.3.0
- github.com/stretchr/testify v1.8.0
+ github.com/go-chi/chi/v5 v5.0.12
+ github.com/google/uuid v1.6.0
+ github.com/stretchr/testify v1.9.0
```

## [0.6.2] - 2023-06-27
### Fixed
- Error logging when terminating HTTP server.

## [0.6.1] - 2022-03-28
## [0.6.1] - 2023-03-28
### Changed
- package `http/param` does not zero the field if not tagged with any relevant tags

Expand All @@ -18,15 +35,15 @@ How to release a new version:
- package `http/signature` to simplify defining http handler functions
- package `http/param` to simplify parsing http path and query parameters

## [0.5.0] - 2022-01-20
## [0.5.0] - 2023-01-20
### Added
- `ErrorResponseOptions` contains public error message.
- `ErrorResponseOptions` contains request ID.
- Error response options:
- `WithErrorMessage`
- `WithRequestID`

## [0.4.0] - 2022-01-12
## [0.4.0] - 2023-01-12
### Changed
- JSON tags in `ErrorResponseOptions`.

Expand All @@ -53,7 +70,8 @@ How to release a new version:
### Added
- Added Changelog.

[Unreleased]: https://github.com/strvcom/strv-backend-go-net/compare/v0.6.2...HEAD
[Unreleased]: https://github.com/strvcom/strv-backend-go-net/compare/v0.7.0...HEAD
[0.7.0]: https://github.com/strvcom/strv-backend-go-net/compare/v0.6.2...v0.7.0
[0.6.2]: https://github.com/strvcom/strv-backend-go-net/compare/v0.6.1...v0.6.2
[0.6.1]: https://github.com/strvcom/strv-backend-go-net/compare/v0.6.0...v0.6.1
[0.6.0]: https://github.com/strvcom/strv-backend-go-net/compare/v0.5.0...v0.6.0
Expand Down
20 changes: 13 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,13 @@ Go package facilitating writing API applications in a fast and easy manner.
### errors
Definition of common errors.

### logger
Interface `ServerLogger` implements common logging methods.

### net
Common functionality that comes in handy regardless of the used API architecture. `net` currently supports generating request IDs with some helper methods.

### http
CermakM marked this conversation as resolved.
Show resolved Hide resolved
Wrapper around the Go native http server. `http` defines the `Server` that can be configured by the `ServerConfig`. Implemented features:
- Started http server can be easily stopped by cancelling the context that is passed by the `Run` method.
- The `Server` can be configured with a logger for logging important information during starting/ending of the server.
- The `Server` can be configured with a slog.Logger for logging important information during starting/ending of the server.
- The `Server` listens for `SIGINT` and `SIGTERM` signals so it can be stopped by firing the signal.
- By the `ServerConfig` can be configured functions to be called before the `Server` ends.

Expand All @@ -46,7 +43,16 @@ import (

func main() {
...

h := slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{
Level: level,
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
if a.Key == slog.TimeKey {
a.Value = slog.StringValue(a.Value.Time().Format("2006-01-02T15:04:05.000Z"))
}
return a
},
})
l := slog.New(h)
serverConfig := httpx.ServerConfig{
Addr: ":8080",
Handler: handler(), // define your http handler
Expand All @@ -58,11 +64,11 @@ func main() {
},
},
Limits: nil,
Logger: util.NewServerLogger("httpx.Server"), // wrapper around zap logger to implement httpx logging interface
Logger: l.WithGroup("httpx.Server"), // the server expects *slog.Logger
}
server := httpx.NewServer(&serverConfig)
if err = server.Start(ctx); err != nil {
logger.Fatal("HTTP server unexpectedly ended", zap.Error(err))
l.Error("HTTP server unexpectedly ended", slog.Any("error", err))
}
}
```
Expand Down
12 changes: 6 additions & 6 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
module go.strv.io/net

go 1.20
go 1.22

require (
CermakM marked this conversation as resolved.
Show resolved Hide resolved
github.com/go-chi/chi/v5 v5.0.8
github.com/google/uuid v1.3.0
github.com/stretchr/testify v1.8.0
github.com/go-chi/chi/v5 v5.0.12
github.com/google/uuid v1.6.0
github.com/stretchr/testify v1.9.0
go.strv.io/time v0.2.0
)

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/kr/pretty v0.3.0 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rogpeppe/go-internal v1.8.0 // indirect
github.com/rogpeppe/go-internal v1.12.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
30 changes: 11 additions & 19 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,37 +1,29 @@
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-chi/chi/v5 v5.0.8 h1:lD+NLqFcAi1ovnVZpsnObHGW4xb4J8lNmoYVfECH1Y0=
github.com/go-chi/chi/v5 v5.0.8/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s=
github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
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=
go.strv.io/time v0.2.0 h1:RgCpABq+temfp8+DLM2zqsdimnKpktOSPduUghM8ZIk=
go.strv.io/time v0.2.0/go.mod h1:B/lByAO3oACN3uLOXQaB64cKhkVIMoZjnZBhADFNbFY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
5 changes: 2 additions & 3 deletions http/config.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
package http

import (
"log/slog"
"net/http"

"go.strv.io/net/logger"

"go.strv.io/time"
)

Expand All @@ -23,7 +22,7 @@ type ServerConfig struct {
Limits *Limits `json:"limits,omitempty"`

// Logger is server logger.
Logger logger.ServerLogger
Logger *slog.Logger
}

// Limits define timeouts and header restrictions.
Expand Down
83 changes: 53 additions & 30 deletions http/middleware.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
package http

import (
"log/slog"
"net/http"
"runtime/debug"
"time"

"go.strv.io/net"
"go.strv.io/net/internal"
"go.strv.io/net/logger"
)

const (
Expand Down Expand Up @@ -40,10 +41,26 @@ func RequestIDMiddleware(f RequestIDFunc) func(http.Handler) http.Handler {
}
}

type RecoverMiddlewareOptions struct {
enableStackTrace bool
}

type RecoverMiddlewareOption func(*RecoverMiddlewareOptions)

func WithStackTrace() RecoverMiddlewareOption {
return func(opts *RecoverMiddlewareOptions) {
opts.enableStackTrace = true
}
}

// RecoverMiddleware calls next handler and recovers from a panic.
// If a panic occurs, log this event, set http.StatusInternalServerError as a status code
// and save a panic object into the response writer.
func RecoverMiddleware(l logger.ServerLogger) func(http.Handler) http.Handler {
func RecoverMiddleware(l *slog.Logger, opts ...RecoverMiddlewareOption) func(http.Handler) http.Handler {
options := RecoverMiddlewareOptions{}
for _, o := range opts {
o(&options)
}
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
Expand All @@ -56,10 +73,14 @@ func RecoverMiddleware(l logger.ServerLogger) func(http.Handler) http.Handler {
rw.SetPanicObject(re)
rw.WriteHeader(http.StatusInternalServerError)

l.With(
logger.Any("err", re),
logger.Any(requestIDLogFieldName, net.RequestIDFromCtx(r.Context())),
).Error("panic recover", nil)
logAttributes := []slog.Attr{
slog.String(requestIDLogFieldName, net.RequestIDFromCtx(r.Context())),
slog.Any("error", re),
}
if options.enableStackTrace {
logAttributes = append(logAttributes, slog.String("stack_trace", string(debug.Stack())))
}
l.LogAttrs(r.Context(), slog.LevelError, "panic recover", logAttributes...)
CermakM marked this conversation as resolved.
Show resolved Hide resolved
}
}()
next.ServeHTTP(w, r)
Expand All @@ -77,7 +98,7 @@ func RecoverMiddleware(l logger.ServerLogger) func(http.Handler) http.Handler {
// - Panic object if exists
//
// If the status code >= http.StatusInternalServerError, logs with error level, info otherwise.
func LoggingMiddleware(l logger.ServerLogger) func(http.Handler) http.Handler {
func LoggingMiddleware(l *slog.Logger) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
rw, ok := w.(*internal.ResponseWriter)
Expand All @@ -90,57 +111,59 @@ func LoggingMiddleware(l logger.ServerLogger) func(http.Handler) http.Handler {
statusCode := rw.StatusCode()
requestID := net.RequestIDFromCtx(r.Context())

ld := LogData{
ld := RequestData{
Path: r.URL.EscapedPath(),
Method: r.Method,
RequestID: requestID,
Duration: time.Since(requestStart),
ResponseStatusCode: statusCode,
Err: rw.ErrorObject(),
Panic: rw.PanicObject(),
}

if statusCode >= http.StatusInternalServerError {
WithData(l, ld).Error("request processed", nil)
withRequestData(l, rw, ld).Error("request processed")
} else {
WithData(l, ld).Info("request processed")
withRequestData(l, rw, ld).Info("request processed")
}
})
}
}

// LogData contains processed request data for logging purposes.
// RequestData contains processed request data for logging purposes.
// Path is path from URL of the request.
// Method is HTTP request method.
// Duration is how long it took to process whole request.
// ResponseStatusCode is HTTP status code which was returned.
// RequestID is unique identifier of request.
// Err is error object containing error message.
// Panic is panic object containing error message.
type LogData struct {
type RequestData struct {
Path string
Method string
Duration time.Duration
ResponseStatusCode int
RequestID string
Err error
Panic any
}

// WithData returns logger with filled fields.
func WithData(l logger.ServerLogger, ld LogData) logger.ServerLogger {
l = l.With(
logger.Any("method", ld.Method),
logger.Any("path", ld.Path),
logger.Any("status_code", ld.ResponseStatusCode),
logger.Any("request_id", ld.RequestID),
logger.Any("duration_ms", ld.Duration.Milliseconds()),
)
if ld.Err != nil {
l = l.With(logger.Any("err", ld.Err.Error()))
func (r RequestData) LogValue() slog.Value {
attr := []slog.Attr{
slog.String("id", r.RequestID),
slog.String("method", r.Method),
slog.String("path", r.Path),
slog.Int("status_code", r.ResponseStatusCode),
slog.Duration("duration_ms", r.Duration),
}
return slog.GroupValue(attr...)
}

// withRequestData returns slog with filled fields.
func withRequestData(l *slog.Logger, rw *internal.ResponseWriter, rd RequestData) *slog.Logger {
errorObject := rw.ErrorObject()
panicObject := rw.PanicObject()
if errorObject != nil {
l = l.With("error", errorObject)
}
if ld.Panic != nil {
l = l.With(logger.Any("panic", ld.Panic))
if panicObject != nil {
l = l.With("panic", panicObject)
}
return l
return l.With("request", rd)
}
Loading
Loading