Skip to content

Commit

Permalink
Merge pull request #31 from strvcom/feat/migrate-to-slog
Browse files Browse the repository at this point in the history
feat: migrate to slog.Logger
  • Loading branch information
CermakM committed Mar 11, 2024
2 parents da2103d + 13501ff commit 6e8c0ef
Show file tree
Hide file tree
Showing 13 changed files with 161 additions and 142 deletions.
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
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 (
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...)
}
}()
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

0 comments on commit 6e8c0ef

Please sign in to comment.