I want to add logging of an Request ID on every log message in every service. That way I can know which logs were coming from which request easier.
For my project I am using zerolog as the package of choice for logging.
Middleware function
func RequestIDMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
reqID := r.Header.Get("X-Request-ID")
if reqID == "" {
reqID = uuid.New().String()
}
ctx := context.WithValue(r.Context(), service.RequestIDKey, reqID)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
handler.go
func (h *Handler) Message(w http.ResponseWriter, r *http.Request) {
responseMsg, err := h.Service.GetMessage(r.Context())
reqId, ok := r.Context().Value(service.RequestIDKey).(string)
if !ok {
log.Info().Msg("Some log message from handler")
} else {
log.Info().Str("request_id", reqId).Msg("Some log message from handler")
}
if err != nil {
reqId, ok = r.Context().Value(service.RequestIDKey).(string)
if !ok {
log.Info().Msg("Some log message from handler")
} else {
log.Info().Str("request_id", reqId).Msg("Some log message from handler")
}
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.WriteHeader(http.StatusOK)
if _, err = w.Write([]byte(responseMsg)); err != nil {
http.Error(w, "Failed to encode response", http.StatusInternalServerError)
}
return
}
Service object
func (service *Service) GetMessage(ctx context.Context) (string, error) {
reqId, ok := ctx.Value(RequestIDKey).(string)
if !ok {
log.Info().Msg("Some log message from service")
} else {
log.Info().Str("request_id", reqId).Msg("Some log message from service")
}
return "Message from service", nil
}
Initiation function for the logger
package logger
func NewLogger() zerolog.Logger {
once.Do(func() {
consoleWriter := zerolog.ConsoleWriter{
Out: os.Stderr,
TimeFormat: time.RFC3339,
}
zerolog.ErrorStackMarshaler = pkgerrors.MarshalStack
var writers zerolog.LevelWriter
writers = zerolog.MultiLevelWriter(consoleWriter)
zerolog.CallerMarshalFunc = func(pc uintptr, file string, line int) string {
parts := strings.Split(file, "/")
if len(parts) > 3 {
file = strings.Join(parts[len(parts)-3:], "/")
}
return file + ":" + strconv.Itoa(line)
}
logger = zerolog.New(writers).With().Timestamp().Caller().Logger()
})
return logger
}
main.go
func main() {
// Set the global zerolog logger
log.Logger = logger.NewLogger()
s := service.NewService()
h := handler.NewHandler(s)
router := mux.NewRouter()
router.Use(RequestIDMiddleware)
router.HandleFunc("/message", h.Message).Methods("GET")
if err := http.ListenAndServe(":8080", router); err != nil {
log.Fatal().Err(err).Msg("Server failed")
}
log.Info().Msg("Server started")
}
I want to somehow abstract this repetitive code, that checks for requstId in the context and then adding it to the logger.
reqId, ok := r.Context().Value(service.RequestIDKey).(string)
if !ok {
log.Info().Msg("Some log message from handler")
} else {
log.Info().Str("request_id", reqId).Msg("Some log message from handler")
}
Ideally I would best like to have something like:
log.Info().RequestId(ctx).Msg("Some log message")
From what I've read online, I can add new logger into the request context, with populated RequestID, but I don't know how good of an idea is to do that.
That way I will have a line of code, that extract a logger from the context, in every method of every handler and service.