Skip to content

Commit b234390

Browse files
committed
use slog
1 parent 7e788a7 commit b234390

File tree

5 files changed

+140
-15
lines changed

5 files changed

+140
-15
lines changed

config.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
package main
22

33
import (
4-
"log"
54
"net/http"
65
"os"
6+
7+
"golang.org/x/exp/slog"
78
)
89

910
type Config struct {
@@ -27,7 +28,7 @@ type Response struct {
2728
IsJSON bool `json:"is_json,omitempty" yaml:"is_json,omitempty"`
2829
}
2930

30-
func responsesWriter(responses []Response) http.HandlerFunc {
31+
func responsesWriter(responses []Response, log *slog.Logger) http.HandlerFunc {
3132
var i int
3233
return func(writer http.ResponseWriter, request *http.Request) {
3334
for {
@@ -70,7 +71,7 @@ func responsesWriter(responses []Response) http.HandlerFunc {
7071

7172
if len(data) > 0 {
7273
if _, err := writer.Write(data); err != nil {
73-
log.Printf("sending response failed: %+v", err)
74+
log.Error("sending response failed", err)
7475
}
7576
}
7677
return

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,6 @@ go 1.20
55
require (
66
github.com/go-chi/chi/v5 v5.0.8
77
github.com/spf13/pflag v1.0.5
8+
golang.org/x/exp v0.0.0-20230203172020-98cc5a0785f9
89
gopkg.in/yaml.v3 v3.0.1
910
)

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ github.com/go-chi/chi/v5 v5.0.8 h1:lD+NLqFcAi1ovnVZpsnObHGW4xb4J8lNmoYVfECH1Y0=
22
github.com/go-chi/chi/v5 v5.0.8/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
33
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
44
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
5+
golang.org/x/exp v0.0.0-20230203172020-98cc5a0785f9 h1:frX3nT9RkKybPnjyI+yvZh6ZucTZatCCEm9D47sZ2zo=
6+
golang.org/x/exp v0.0.0-20230203172020-98cc5a0785f9/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
57
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
68
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
79
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

log.go

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"net/http"
6+
"os"
7+
"time"
8+
9+
"github.com/go-chi/chi/v5/middleware"
10+
"golang.org/x/exp/slog"
11+
)
12+
13+
func LogHandler() *slog.JSONHandler {
14+
// Setup a JSON handler for the new log/slog library
15+
return slog.HandlerOptions{
16+
// Remove default time slog.Attr, we create our own later
17+
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
18+
if a.Key == slog.TimeKey {
19+
return slog.Attr{}
20+
}
21+
return a
22+
},
23+
}.NewJSONHandler(os.Stdout)
24+
}
25+
26+
// StructuredLogger is a simple, but powerful implementation of a custom structured
27+
// logger backed on log/slog. I encourage users to copy it, adapt it and make it their
28+
// own. Also take a look at https://github.com/go-chi/httplog for a dedicated pkg based
29+
// on this work, designed for context-based http routers.
30+
31+
func NewStructuredLogger(handler slog.Handler) func(next http.Handler) http.Handler {
32+
return middleware.RequestLogger(&StructuredLogger{Logger: handler})
33+
}
34+
35+
type StructuredLogger struct {
36+
Logger slog.Handler
37+
}
38+
39+
func (l *StructuredLogger) NewLogEntry(r *http.Request) middleware.LogEntry {
40+
var logFields []slog.Attr
41+
logFields = append(logFields, slog.String("ts", time.Now().UTC().Format(time.RFC1123)))
42+
43+
if reqID := middleware.GetReqID(r.Context()); reqID != "" {
44+
logFields = append(logFields, slog.String("req_id", reqID))
45+
}
46+
47+
scheme := "http"
48+
if r.TLS != nil {
49+
scheme = "https"
50+
}
51+
52+
handler := l.Logger.WithAttrs(append(logFields,
53+
slog.String("http_scheme", scheme),
54+
slog.String("http_proto", r.Proto),
55+
slog.String("http_method", r.Method),
56+
slog.String("remote_addr", r.RemoteAddr),
57+
slog.String("user_agent", r.UserAgent()),
58+
slog.String("uri", fmt.Sprintf("%s://%s%s", scheme, r.Host, r.RequestURI)),
59+
))
60+
61+
entry := StructuredLoggerEntry{Logger: slog.New(handler)}
62+
63+
entry.Logger.LogAttrs(slog.LevelInfo, "request started")
64+
65+
return &entry
66+
}
67+
68+
type StructuredLoggerEntry struct {
69+
Logger *slog.Logger
70+
}
71+
72+
func (l *StructuredLoggerEntry) Write(status, bytes int, header http.Header, elapsed time.Duration, extra interface{}) {
73+
l.Logger.LogAttrs(slog.LevelInfo, "request complete",
74+
slog.Int("resp_status", status),
75+
slog.Int("resp_byte_length", bytes),
76+
slog.Float64("resp_elapsed_ms", float64(elapsed.Nanoseconds())/1000000.0),
77+
)
78+
}
79+
80+
func (l *StructuredLoggerEntry) Panic(v interface{}, stack []byte) {
81+
l.Logger.LogAttrs(slog.LevelInfo, "",
82+
slog.String("stack", string(stack)),
83+
slog.String("panic", fmt.Sprintf("%+v", v)),
84+
)
85+
}
86+
87+
// Helper methods used by the application to get the request-scoped
88+
// logger entry and set additional fields between handlers.
89+
//
90+
// This is a useful pattern to use to set state on the entry as it
91+
// passes through the handler chain, which at any point can be logged
92+
// with a call to .Print(), .Info(), etc.
93+
94+
func GetLogEntry(r *http.Request) *slog.Logger {
95+
entry := middleware.GetLogEntry(r).(*StructuredLoggerEntry)
96+
return entry.Logger
97+
}
98+
99+
func LogEntrySetField(r *http.Request, key string, value interface{}) {
100+
if entry, ok := r.Context().Value(middleware.LogEntryCtxKey).(*StructuredLoggerEntry); ok {
101+
entry.Logger = entry.Logger.With(key, value)
102+
}
103+
}
104+
105+
func LogEntrySetFields(r *http.Request, fields map[string]interface{}) {
106+
if entry, ok := r.Context().Value(middleware.LogEntryCtxKey).(*StructuredLoggerEntry); ok {
107+
for k, v := range fields {
108+
entry.Logger = entry.Logger.With(k, v)
109+
}
110+
}
111+
}

main.go

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import (
44
"context"
55
"errors"
66
"fmt"
7-
"log"
87
"net/http"
98
"os"
109
"os/signal"
@@ -15,10 +14,13 @@ import (
1514
"github.com/go-chi/chi/v5"
1615
"github.com/go-chi/chi/v5/middleware"
1716
flag "github.com/spf13/pflag"
17+
"golang.org/x/exp/slog"
1818
"gopkg.in/yaml.v3"
1919
)
2020

2121
func main() {
22+
logHandler := LogHandler()
23+
log := slog.New(logHandler)
2224
conf := flag.StringP("config", "c", "config.yaml", "config file")
2325
port := flag.IntP("port", "p", 8080, "http port")
2426
flag.Parse()
@@ -29,21 +31,24 @@ func main() {
2931
if v := os.Getenv("PORT"); v != "" && !flag.Lookup("port").Changed {
3032
p, err := strconv.Atoi(v)
3133
if err != nil {
32-
log.Fatal(err)
34+
log.Error(fmt.Sprintf("wrong value %q of env variable PORT", v), err)
35+
os.Exit(1)
3336
}
3437
port = &p
3538
}
3639

3740
f, err := os.Open(*conf)
3841
if err != nil {
39-
log.Fatal(err)
42+
log.Error(fmt.Sprintf("wrong config file %q", *conf), err)
43+
os.Exit(1)
4044
}
4145

4246
var config Config
4347
d := yaml.NewDecoder(f)
4448
d.KnownFields(true)
4549
if err := d.Decode(&config); err != nil {
46-
panic(err)
50+
log.Error("decoding config failed", err)
51+
os.Exit(1)
4752
}
4853
if config.Port != 0 {
4954
port = &config.Port
@@ -52,7 +57,11 @@ func main() {
5257
if config.RequestIDHeader != "" {
5358
middleware.RequestIDHeader = config.RequestIDHeader
5459
}
55-
r := chi.NewRouter().With(middleware.Logger, middleware.RequestID)
60+
61+
r := chi.NewMux().With(middleware.RequestID, NewStructuredLogger(LogHandler()))
62+
r.NotFound(func(writer http.ResponseWriter, request *http.Request) {
63+
http.NotFound(writer, request)
64+
})
5665
for _, route := range config.Routes {
5766
if route.Method == "" {
5867
route.Method = "GET"
@@ -62,7 +71,7 @@ func main() {
6271
if route.Pattern == "" {
6372
route.Pattern = "/"
6473
}
65-
r.MethodFunc(route.Method, route.Pattern, responsesWriter(route.Responses))
74+
r.MethodFunc(route.Method, route.Pattern, responsesWriter(route.Responses, log))
6675
}
6776

6877
server := &http.Server{
@@ -83,22 +92,23 @@ func main() {
8392
go func() {
8493
<-shutdownCtx.Done()
8594
if errors.Is(shutdownCtx.Err(), context.DeadlineExceeded) {
86-
log.Fatal("graceful shutdown timed out.. forcing exit.")
95+
log.Error("graceful shutdown timed out.. forcing exit.", nil)
96+
os.Exit(1)
8797
}
8898
}()
8999

90100
// Trigger graceful shutdown
91-
err := server.Shutdown(shutdownCtx)
92-
if err != nil {
93-
log.Fatal(err)
101+
if err := server.Shutdown(shutdownCtx); err != nil {
102+
log.Error("graceful shutdown failed", err)
94103
}
95104
serverStopCtx()
96105
}()
97106

98107
// Run the server
99-
log.Printf("Listen on http://localhost:%d\n", *port)
108+
log.Info(fmt.Sprintf("Listen on http://localhost:%d", *port))
100109
if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
101-
log.Fatal(err)
110+
log.Error("starting failed", err)
111+
os.Exit(1)
102112
}
103113

104114
// Wait for server context to be stopped

0 commit comments

Comments
 (0)