mirror of
https://github.com/ferdzo/fs.git
synced 2026-04-04 20:56:25 +00:00
208 lines
4.6 KiB
Go
208 lines
4.6 KiB
Go
package logging
|
|
|
|
import (
|
|
"fs/metrics"
|
|
"log/slog"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
"github.com/go-chi/chi/v5/middleware"
|
|
)
|
|
|
|
type Config struct {
|
|
Level slog.Level
|
|
LevelName string
|
|
Format string
|
|
Audit bool
|
|
AddSource bool
|
|
DebugMode bool
|
|
}
|
|
|
|
func ConfigFromEnv() Config {
|
|
levelName := strings.ToLower(strings.TrimSpace(os.Getenv("LOG_LEVEL")))
|
|
format := strings.ToLower(strings.TrimSpace(os.Getenv("LOG_FORMAT")))
|
|
return ConfigFromValues(levelName, format, envBool("AUDIT_LOG", true))
|
|
}
|
|
|
|
func ConfigFromValues(levelName, format string, audit bool) Config {
|
|
levelName = strings.ToLower(strings.TrimSpace(levelName))
|
|
if levelName == "" {
|
|
levelName = "info"
|
|
}
|
|
level := parseLevel(levelName)
|
|
levelName = strings.ToUpper(level.String())
|
|
|
|
format = strings.ToLower(strings.TrimSpace(format))
|
|
if format == "" {
|
|
format = "text"
|
|
}
|
|
if format != "json" && format != "text" {
|
|
format = "text"
|
|
}
|
|
|
|
debugMode := level <= slog.LevelDebug
|
|
return Config{
|
|
Level: level,
|
|
LevelName: levelName,
|
|
Format: format,
|
|
Audit: audit,
|
|
AddSource: debugMode,
|
|
DebugMode: debugMode,
|
|
}
|
|
}
|
|
|
|
func NewLogger(cfg Config) *slog.Logger {
|
|
opts := &slog.HandlerOptions{
|
|
Level: cfg.Level,
|
|
AddSource: cfg.AddSource,
|
|
}
|
|
opts.ReplaceAttr = func(_ []string, attr slog.Attr) slog.Attr {
|
|
if attr.Key == slog.SourceKey {
|
|
if src, ok := attr.Value.Any().(*slog.Source); ok && src != nil {
|
|
attr.Key = "src"
|
|
attr.Value = slog.StringValue(filepath.Base(src.File) + ":" + strconv.Itoa(src.Line))
|
|
}
|
|
}
|
|
return attr
|
|
}
|
|
|
|
var handler slog.Handler
|
|
if cfg.Format == "json" {
|
|
handler = slog.NewJSONHandler(os.Stdout, opts)
|
|
} else {
|
|
handler = slog.NewTextHandler(os.Stdout, opts)
|
|
}
|
|
|
|
logger := slog.New(handler)
|
|
slog.SetDefault(logger)
|
|
return logger
|
|
}
|
|
|
|
func HTTPMiddleware(logger *slog.Logger, cfg Config) func(http.Handler) http.Handler {
|
|
return func(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
start := time.Now()
|
|
ww := middleware.NewWrapResponseWriter(w, r.ProtoMajor)
|
|
op := metricOperationLabel(r)
|
|
metrics.Default.IncHTTPInFlightOp(op)
|
|
defer func() {
|
|
metrics.Default.DecHTTPInFlightOp(op)
|
|
}()
|
|
requestID := middleware.GetReqID(r.Context())
|
|
if requestID != "" {
|
|
ww.Header().Set("x-amz-request-id", requestID)
|
|
}
|
|
|
|
next.ServeHTTP(ww, r)
|
|
|
|
elapsed := time.Since(start)
|
|
status := ww.Status()
|
|
if status == 0 {
|
|
status = http.StatusOK
|
|
}
|
|
route := metricRouteLabel(r)
|
|
metrics.Default.ObserveHTTPRequestDetailed(r.Method, route, op, status, elapsed, ww.BytesWritten())
|
|
|
|
if !cfg.Audit && !cfg.DebugMode {
|
|
return
|
|
}
|
|
|
|
attrs := []any{
|
|
"method", r.Method,
|
|
"path", r.URL.Path,
|
|
"status", status,
|
|
"bytes", ww.BytesWritten(),
|
|
"duration_ms", float64(elapsed.Nanoseconds()) / 1_000_000.0,
|
|
"remote_addr", r.RemoteAddr,
|
|
}
|
|
if requestID != "" {
|
|
attrs = append(attrs, "request_id", requestID)
|
|
}
|
|
|
|
if cfg.DebugMode {
|
|
attrs = append(attrs,
|
|
"query", r.URL.RawQuery,
|
|
"user_agent", r.UserAgent(),
|
|
"content_length", r.ContentLength,
|
|
"content_type", r.Header.Get("Content-Type"),
|
|
"x_amz_sha256", r.Header.Get("x-amz-content-sha256"),
|
|
)
|
|
logger.Debug("http_request", attrs...)
|
|
return
|
|
}
|
|
|
|
logger.Info("http_request", attrs...)
|
|
})
|
|
}
|
|
}
|
|
|
|
func metricRouteLabel(r *http.Request) string {
|
|
if r == nil || r.URL == nil {
|
|
return "/unknown"
|
|
}
|
|
|
|
if routeCtx := chi.RouteContext(r.Context()); routeCtx != nil {
|
|
if pattern := strings.TrimSpace(routeCtx.RoutePattern()); pattern != "" {
|
|
return pattern
|
|
}
|
|
}
|
|
|
|
path := strings.TrimSpace(r.URL.Path)
|
|
if path == "" || path == "/" {
|
|
return "/"
|
|
}
|
|
if path == "/healthz" || path == "/metrics" {
|
|
return path
|
|
}
|
|
|
|
trimmed := strings.Trim(path, "/")
|
|
if trimmed == "" {
|
|
return "/"
|
|
}
|
|
if !strings.Contains(trimmed, "/") {
|
|
return "/{bucket}"
|
|
}
|
|
return "/{bucket}/*"
|
|
}
|
|
|
|
func metricOperationLabel(r *http.Request) string {
|
|
if r == nil {
|
|
return "other"
|
|
}
|
|
isDeletePost := false
|
|
if r.Method == http.MethodPost && r.URL != nil {
|
|
_, isDeletePost = r.URL.Query()["delete"]
|
|
}
|
|
return metrics.NormalizeHTTPOperation(r.Method, isDeletePost)
|
|
}
|
|
|
|
func envBool(key string, defaultValue bool) bool {
|
|
raw := os.Getenv(key)
|
|
if raw == "" {
|
|
return defaultValue
|
|
}
|
|
value, err := strconv.ParseBool(raw)
|
|
if err != nil {
|
|
return defaultValue
|
|
}
|
|
return value
|
|
}
|
|
|
|
func parseLevel(levelName string) slog.Level {
|
|
switch levelName {
|
|
case "debug":
|
|
return slog.LevelDebug
|
|
case "warn", "warning":
|
|
return slog.LevelWarn
|
|
case "error":
|
|
return slog.LevelError
|
|
default:
|
|
return slog.LevelInfo
|
|
}
|
|
}
|