http/server/contextlogger/context_logging.go (90 lines of code) (raw):
package contextlogger
import (
"context"
"log/slog"
"net/http"
opreq "github.com/Azure/aks-middleware/http/server/operationrequest"
"github.com/gorilla/mux"
"google.golang.org/grpc/metadata"
)
type loggerKeyType string
type ExtractFunction func(ctx context.Context, r *http.Request) map[string]interface{}
const (
ctxLogSource = "CtxLog"
ctxLoggerKey loggerKeyType = "CtxLogKey"
)
// DefaultExtractor extracts operation request fields from the context.
// It returns the filtered map containing only the specified keys.
func DefaultExtractor(ctx context.Context, r *http.Request) map[string]interface{} {
op := opreq.OperationRequestFromContext(ctx)
if op == nil {
return nil
}
return opreq.FilteredOperationRequestMap(op, []string{
"TargetURI", "HttpMethod", "AcceptedLanguage", "APIVersion", "Region",
"SubscriptionID", "ResourceGroup", "ResourceName", "CorrelationID", "OperationID",
})
}
// New creates a context logging middleware.
// Parameters
//
// logger: A slog.Logger instance used for logging. Any static attributes added to this logger before passing it in will be preserved
// extractFunction: ExtractFunction extracts information from the ctx and/or the request and put it in the logger
func New(logger slog.Logger, extractFunction ExtractFunction) mux.MiddlewareFunc {
if extractFunction == nil {
extractFunction = DefaultExtractor
}
return func(next http.Handler) http.Handler {
return &contextLogMiddleware{
next: next,
logger: logger,
extractFunction: extractFunction,
}
}
}
// Enforce that contextLogMiddleware implements http.Handler.
var _ http.Handler = &contextLogMiddleware{}
type contextLogMiddleware struct {
next http.Handler
logger slog.Logger
extractFunction ExtractFunction
}
func (m *contextLogMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
attributes := BuildAttributes(ctx, r, m.extractFunction)
contextLogger := m.logger.With(attributes...)
ctx = context.WithValue(ctx, ctxLoggerKey, contextLogger)
r = r.WithContext(ctx)
m.next.ServeHTTP(w, r)
}
func BuildAttributes(ctx context.Context, r *http.Request, extractFunc func(ctx context.Context, r *http.Request) map[string]interface{}) []interface{} {
md, ok := metadata.FromIncomingContext(ctx)
headers := make(map[string]string)
if ok {
for key, values := range md {
if len(values) > 0 {
headers[key] = values[0]
}
}
}
attributes := defaultCtxLogAttributes(r)
logAttrs := make(map[string]interface{})
// Use the extract function to get additional attributes.
if extractFunc != nil {
extractedAttrs := extractFunc(ctx, r)
for k, v := range extractedAttrs {
logAttrs[k] = v
}
}
// Include metadata headers as part of the attributes.
attributes = append(attributes, "log", logAttrs)
// grab desired headers from the request (based on extraction function passed to request ID middleware)
attributes = append(attributes, "headers", headers)
return attributes
}
func defaultCtxLogAttributes(r *http.Request) []interface{} {
return []interface{}{
"request", r.URL.Path,
"method", r.Method,
"source", ctxLogSource,
}
}
func GetLogger(ctx context.Context) *slog.Logger {
logger := slog.Default().With("src", "self gen, not available in ctx")
if ctx == nil {
return logger
}
if ctxlogger, ok := ctx.Value(ctxLoggerKey).(*slog.Logger); ok {
return ctxlogger
}
return logger
}