Skip to content

Commit

Permalink
init
Browse files Browse the repository at this point in the history
  • Loading branch information
syahidfrd committed Feb 19, 2023
0 parents commit acbe4e6
Show file tree
Hide file tree
Showing 5 changed files with 243 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.idea
.vscode
49 changes: 49 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Chilog
The [Chi](https://github.com/go-chi/chi) middleware that contains functionality of requestid, recover and logs HTTP request/response details for traceability.

## Installation
Install chilog with:

```sh
go get -u github.com/Qiscus-Integration/chilog
```

## Usage
```go
package main

import (
"net/http"

"github.com/Qiscus-Integration/chilog"
"github.com/go-chi/chi/v5"
"github.com/rs/zerolog/log"
)

func filter(w http.ResponseWriter, r *http.Request) bool {
return r.URL.Path == "/healthcheck"
}

func main() {
r := chi.NewRouter()
r.Use(chilog.Middleware(filter))
// Output: {"level":"info","request_id":"9627a4a0-9d94-4ab6-844c-9599c0a15cd0","remote_ip":"[::1]:62542","host":"localhost:8080","method":"GET","path":"/","body":"","status_code":200,"latency":0,"tag":"request","time":"2023-02-19T14:07:37+07:00","message":"success"}

// Or without filter
// r.Use(chilog.Middleware(nil))

r.Get("/", func(w http.ResponseWriter, r *http.Request) {
// Log with request context to get request_id
ctx := r.Context()
log.Ctx(ctx).Info().Msg("hello world")
// Output: {"level":"info","request_id":"9627a4a0-9d94-4ab6-844c-9599c0a15cd0","time":"2023-02-19T15:06:39+07:00","message":"hello world"}

w.WriteHeader(http.StatusOK)
})

srv := http.Server{Addr: ":8080", Handler: r}
if err := srv.ListenAndServe(); err != nil {
log.Fatal().Msgf("listen:%+s\n", err)
}
}
```
162 changes: 162 additions & 0 deletions chilog.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
package chilog

import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"runtime/debug"
"time"

"github.com/go-chi/chi/v5/middleware"
"github.com/google/uuid"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
)

// RequestIDHeader is the name of the HTTP Header which contains the request id.
// Exported so that it can be changed by developers
var RequestIDHeader = "X-Request-Id"

type logFields struct {
RemoteIP string
Host string
Method string
Path string
Body string
StatusCode int
Latency float64
Error error
Stack []byte
}

func (l *logFields) MarshalZerologObject(e *zerolog.Event) {
e.
Str("remote_ip", l.RemoteIP).
Str("host", l.Host).
Str("method", l.Method).
Str("path", l.Path).
Str("body", l.Body).
Int("status_code", l.StatusCode).
Float64("latency", l.Latency).
Str("tag", "request")

if l.Error != nil {
e.Err(l.Error)
}

if l.Stack != nil {
e.Bytes("stack", l.Stack)
}
}

// Middleware contains functionality of request_id, logger and recover for request traceability
func Middleware(filter func(w http.ResponseWriter, r *http.Request) bool) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Check filter
if filter != nil && filter(w, r) {
next.ServeHTTP(w, r)
return
}

// Start timer
start := time.Now()

// Generate request ID
// will search for a request ID header and set into the log context
if r.Header.Get(RequestIDHeader) == "" {
r.Header.Set(RequestIDHeader, uuid.New().String())
}

ctx := log.With().
Str("request_id", r.Header.Get(RequestIDHeader)).
Logger().
WithContext(r.Context())

// Wraps an http.ResponseWriter, returning a proxy that allows you to
// hook into various parts of the response process.
ww := middleware.NewWrapResponseWriter(w, r.ProtoMajor)

// Read request body
var buf []byte
if r.Body != nil {
buf, _ = io.ReadAll(r.Body)

// Restore the io.ReadCloser to its original state
r.Body = io.NopCloser(bytes.NewBuffer(buf))
}

// Create log fields
fields := &logFields{
RemoteIP: r.RemoteAddr,
Method: r.Method,
Host: r.Host,
Path: r.URL.Path,
Body: formatReqBody(buf),
}

defer func() {
rvr := recover()

if rvr != nil {
if rvr == http.ErrAbortHandler {
// We don't recover http.ErrAbortHandler so the response
// to the client is aborted, this should not be logged
panic(rvr)
}

err, ok := rvr.(error)
if !ok {
err = fmt.Errorf("%v", rvr)
}

fields.Error = err
fields.Stack = debug.Stack()

w.WriteHeader(http.StatusInternalServerError)
}

fields.StatusCode = ww.Status()
fields.Latency = float64(time.Since(start).Nanoseconds()/1e4) / 100.0

switch {
case rvr != nil:
log.Ctx(ctx).Error().EmbedObject(fields).Msg("panic recover")
case fields.StatusCode >= 500:
log.Ctx(ctx).Error().EmbedObject(fields).Msg("server error")
case fields.StatusCode >= 400:
log.Ctx(ctx).Error().EmbedObject(fields).Msg("client error")
case fields.StatusCode >= 300:
log.Ctx(ctx).Warn().EmbedObject(fields).Msg("redirect")
case fields.StatusCode >= 200:
log.Ctx(ctx).Info().EmbedObject(fields).Msg("success")
case fields.StatusCode >= 100:
log.Ctx(ctx).Info().EmbedObject(fields).Msg("informative")
default:
log.Ctx(ctx).Warn().EmbedObject(fields).Msg("unknown status")
}

}()

next.ServeHTTP(ww, r.WithContext(ctx))

})
}
}

func formatReqBody(data []byte) string {
var js map[string]interface{}
if json.Unmarshal(data, &js) != nil {
return string(data)
}

result := new(bytes.Buffer)
if err := json.Compact(result, data); err != nil {
log.Error().Err(err).Msg("error compacting body request json")
return ""
}

return result.String()
}
13 changes: 13 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
module github.com/Qiscus-Integration/chilog

go 1.19

require github.com/rs/zerolog v1.29.0

require (
github.com/go-chi/chi/v5 v5.0.8
github.com/google/uuid v1.3.0
github.com/mattn/go-colorable v0.1.12 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6 // indirect
)
17 changes: 17 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
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/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
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/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.29.0 h1:Zes4hju04hjbvkVkOhdl2HpZa+0PmVwigmo8XoORE5w=
github.com/rs/zerolog v1.29.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6 h1:foEbQz/B0Oz6YIqu/69kfXPYeFQAuuMYFkjaqXzl5Wo=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=

0 comments on commit acbe4e6

Please sign in to comment.