/
logger.go
169 lines (139 loc) · 4.97 KB
/
logger.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
package middleware
import (
"context"
"net/http"
"os"
"time"
"github.com/danielgtaylor/huma"
"github.com/go-chi/chi"
"github.com/mattn/go-isatty"
"github.com/opentracing/opentracing-go"
"github.com/spf13/viper"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
type contextKey string
var logConfig zap.Config
// logContextKey allows you to get/set a logger from a context.
const logContextKey contextKey = "huma-middleware-logger"
// LogLevel sets the current Zap root logger's level when using the logging
// middleware. This can be changed dynamically at runtime.
var LogLevel *zap.AtomicLevel
// LogTracePrefix is used to prefix OpenTracing trace and span ID key names in
// emitted log message tag names. Use this to integrate with DataDog and other
// tracing service providers.
var LogTracePrefix = "dd."
// NewDefaultLogger returns a new low-level `*zap.Logger` instance. If the
// current terminal is a TTY, it will try ot use colored output automatically.
func NewDefaultLogger() (*zap.Logger, error) {
if LogLevel != nil {
// Only set up the config once. The level will control all loggers.
return logConfig.Build()
}
if isatty.IsTerminal(os.Stdout.Fd()) || isatty.IsCygwinTerminal(os.Stdout.Fd()) {
logConfig = zap.NewDevelopmentConfig()
logConfig.EncoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder
} else {
logConfig = zap.NewProductionConfig()
}
logConfig.EncoderConfig.EncodeTime = iso8601UTCTimeEncoder
LogLevel = &logConfig.Level
return logConfig.Build()
}
// NewLogger is a function that returns a new logger instance to use with
// the logger middleware.
var NewLogger func() (*zap.Logger, error) = NewDefaultLogger
// A UTC variation of ZapCore.ISO8601TimeEncoder with millisecond precision
func iso8601UTCTimeEncoder(t time.Time, enc zapcore.PrimitiveArrayEncoder) {
enc.AppendString(t.UTC().Format("2006-01-02T15:04:05.000Z"))
}
type statusRecorder struct {
http.ResponseWriter
status int
}
func (r *statusRecorder) WriteHeader(statusCode int) {
r.status = statusCode
r.ResponseWriter.WriteHeader(statusCode)
}
// `statusRecorder` implements the Flusher interface if the
// underlying `ResponseWriter` does
func (r *statusRecorder) Flush() {
if f, ok := r.ResponseWriter.(http.Flusher); ok {
f.Flush()
}
}
// Logger creates a new middleware to set a tagged `*zap.SugarLogger` in the
// request context. It debug logs request info. If the current terminal is a
// TTY, it will try to use colored output automatically.
func Logger(next http.Handler) http.Handler {
var err error
var l *zap.Logger
if l, err = NewLogger(); err != nil {
panic(err)
}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
chiCtx := chi.RouteContext(r.Context())
contextLog := l.With(
zap.String("http.version", r.Proto),
zap.String("http.method", r.Method),
// The route pattern isn't filled out until *after* the handler runs...
// zap.String("http.template", chiCtx.RoutePattern()),
zap.String("http.url", r.URL.String()),
zap.String("network.client.ip", r.RemoteAddr),
)
if span := opentracing.SpanFromContext(r.Context()); span != nil {
// We have a span context, so log its info to help with correlation.
if sc, ok := span.Context().(spanContext); ok {
contextLog = contextLog.With(
zap.Uint64(LogTracePrefix+"trace_id", sc.TraceID()),
zap.Uint64(LogTracePrefix+"span_id", sc.SpanID()),
)
}
}
r = r.WithContext(context.WithValue(r.Context(), logContextKey, contextLog.Sugar()))
nw := &statusRecorder{ResponseWriter: w}
next.ServeHTTP(nw, r)
contextLog = contextLog.With(
zap.String("http.template", chiCtx.RoutePattern()),
zap.Int("http.status_code", nw.status),
zap.Duration("duration", time.Since(start)),
)
if nw.status < 500 {
contextLog.Debug("Request")
} else {
contextLog.Error("Request")
}
})
}
// AddLoggerOptions adds command line options for enabling debug logging.
func AddLoggerOptions(app Flagger) {
// Add the debug flag to enable more logging
app.Flag("debug", "d", "Enable debug logs", false)
// Add pre-start handler
app.PreStart(func() {
if viper.GetBool("debug") {
if LogLevel != nil {
LogLevel.SetLevel(zapcore.DebugLevel)
}
}
})
}
// GetLogger returns the contextual logger for the current request. If no
// logger is present, it returns a no-op logger so no nil check is required.
func GetLogger(ctx context.Context) *zap.SugaredLogger {
log := ctx.Value(logContextKey)
if log != nil {
return log.(*zap.SugaredLogger)
}
return zap.NewNop().Sugar()
}
// SetLogger sets the contextual logger for the current request.
func SetLogger(r *http.Request, logger *zap.SugaredLogger) *http.Request {
return r.WithContext(context.WithValue(r.Context(), logContextKey, logger))
}
// SetLoggerInContext allows you to override the logger in the current request
// context. This is useful for modifying the logger in input resolvers.
func SetLoggerInContext(ctx huma.Context, logger *zap.SugaredLogger) {
ctx.SetValue(logContextKey, logger)
}