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

Update logging example to use preview version of new log/slog #771

Merged
merged 3 commits into from
Dec 22, 2022
Merged
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
97 changes: 50 additions & 47 deletions _examples/logging/main.go
Original file line number Diff line number Diff line change
@@ -1,41 +1,42 @@
//
// Custom Structured Logger
// ========================
// This example demonstrates how to use middleware.RequestLogger,
// middleware.LogFormatter and middleware.LogEntry to build a structured
// logger using the amazing sirupsen/logrus package as the logging
// logger using the preview version of the new log/slog package as the logging
// backend.
//
// Also: check out https://github.com/goware/httplog for an improved context
// logger with support for HTTP request logging, based on the example below.
//
package main

import (
"fmt"
"net/http"
"os"
"time"

"golang.org/x/exp/slog"

"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/sirupsen/logrus"
)

func main() {

// Setup the logger backend using sirupsen/logrus and configure
// it to use a custom JSONFormatter. See the logrus docs for how to
// configure the backend at github.com/sirupsen/logrus
logger := logrus.New()
logger.Formatter = &logrus.JSONFormatter{
// disable, as we set our own
DisableTimestamp: true,
}
// Setup a JSON handler for the new log/slog library
slogJSONHandler := slog.HandlerOptions{
// Remove default time slog.Attr, we create our own later
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
if a.Key == slog.TimeKey {
return slog.Attr{}
}
return a
},
}.NewJSONHandler(os.Stdout)

// Routes
r := chi.NewRouter()
r.Use(middleware.RequestID)
r.Use(NewStructuredLogger(logger))
r.Use(NewStructuredLogger(slogJSONHandler))
r.Use(middleware.Recoverer)

r.Get("/", func(w http.ResponseWriter, r *http.Request) {
Expand All @@ -49,70 +50,70 @@ func main() {
r.Get("/panic", func(w http.ResponseWriter, r *http.Request) {
panic("oops")
})
r.Get("/add_fields", func(w http.ResponseWriter, r *http.Request) {
LogEntrySetFields(r, map[string]interface{}{"foo": "bar", "bar": "foo"})
})
http.ListenAndServe(":3333", r)
}

// StructuredLogger is a simple, but powerful implementation of a custom structured
// logger backed on logrus. I encourage users to copy it, adapt it and make it their
// logger backed on log/slog. I encourage users to copy it, adapt it and make it their
// own. Also take a look at https://github.com/go-chi/httplog for a dedicated pkg based
// on this work, designed for context-based http routers.

func NewStructuredLogger(logger *logrus.Logger) func(next http.Handler) http.Handler {
return middleware.RequestLogger(&StructuredLogger{logger})
func NewStructuredLogger(handler slog.Handler) func(next http.Handler) http.Handler {
return middleware.RequestLogger(&StructuredLogger{Logger: handler})
}

type StructuredLogger struct {
Logger *logrus.Logger
Logger slog.Handler
}

func (l *StructuredLogger) NewLogEntry(r *http.Request) middleware.LogEntry {
entry := &StructuredLoggerEntry{Logger: logrus.NewEntry(l.Logger)}
logFields := logrus.Fields{}

logFields["ts"] = time.Now().UTC().Format(time.RFC1123)
var logFields []slog.Attr
logFields = append(logFields, slog.String("ts", time.Now().UTC().Format(time.RFC1123)))

if reqID := middleware.GetReqID(r.Context()); reqID != "" {
logFields["req_id"] = reqID
logFields = append(logFields, slog.String("req_id", reqID))
}

scheme := "http"
if r.TLS != nil {
scheme = "https"
}
logFields["http_scheme"] = scheme
logFields["http_proto"] = r.Proto
logFields["http_method"] = r.Method

logFields["remote_addr"] = r.RemoteAddr
logFields["user_agent"] = r.UserAgent()

logFields["uri"] = fmt.Sprintf("%s://%s%s", scheme, r.Host, r.RequestURI)
handler := l.Logger.WithAttrs(append(logFields,
slog.String("http_scheme", scheme),
slog.String("http_proto", r.Proto),
slog.String("http_method", r.Method),
slog.String("remote_addr", r.RemoteAddr),
slog.String("user_agent", r.UserAgent()),
slog.String("uri", fmt.Sprintf("%s://%s%s", scheme, r.Host, r.RequestURI))))

entry.Logger = entry.Logger.WithFields(logFields)
entry := StructuredLoggerEntry{Logger: slog.New(handler)}

entry.Logger.Infoln("request started")
entry.Logger.LogAttrs(slog.LevelInfo, "request started", logFields...)

return entry
return &entry
}

type StructuredLoggerEntry struct {
Logger logrus.FieldLogger
Logger *slog.Logger
}

func (l *StructuredLoggerEntry) Write(status, bytes int, header http.Header, elapsed time.Duration, extra interface{}) {
l.Logger = l.Logger.WithFields(logrus.Fields{
"resp_status": status, "resp_bytes_length": bytes,
"resp_elapsed_ms": float64(elapsed.Nanoseconds()) / 1000000.0,
})

l.Logger.Infoln("request complete")
l.Logger.LogAttrs(slog.LevelInfo, "request complete",
slog.Int("resp_status", status),
slog.Int("resp_byte_length", bytes),
slog.Float64("resp_elapsed_ms", float64(elapsed.Nanoseconds())/1000000.0),
)
}

func (l *StructuredLoggerEntry) Panic(v interface{}, stack []byte) {
l.Logger = l.Logger.WithFields(logrus.Fields{
"stack": string(stack),
"panic": fmt.Sprintf("%+v", v),
})
l.Logger.LogAttrs(slog.LevelInfo, "",
slog.String("stack", string(stack)),
slog.String("panic", fmt.Sprintf("%+v", v)),
)
}

// Helper methods used by the application to get the request-scoped
Expand All @@ -122,19 +123,21 @@ func (l *StructuredLoggerEntry) Panic(v interface{}, stack []byte) {
// passes through the handler chain, which at any point can be logged
// with a call to .Print(), .Info(), etc.

func GetLogEntry(r *http.Request) logrus.FieldLogger {
func GetLogEntry(r *http.Request) *slog.Logger {
entry := middleware.GetLogEntry(r).(*StructuredLoggerEntry)
return entry.Logger
}

func LogEntrySetField(r *http.Request, key string, value interface{}) {
if entry, ok := r.Context().Value(middleware.LogEntryCtxKey).(*StructuredLoggerEntry); ok {
entry.Logger = entry.Logger.WithField(key, value)
entry.Logger = entry.Logger.With(key, value)
}
}

func LogEntrySetFields(r *http.Request, fields map[string]interface{}) {
if entry, ok := r.Context().Value(middleware.LogEntryCtxKey).(*StructuredLoggerEntry); ok {
entry.Logger = entry.Logger.WithFields(fields)
for k, v := range fields {
entry.Logger = entry.Logger.With(k, v)
}
}
}