log.go 3.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115
  1. // Package log предоставляет структурированное логирование на основе slog.
  2. // Поддерживает JSON-формат для production и текстовый для development.
  3. // Позволяет получать/сохранять логгер в контексте (FromContext/WithContext),
  4. // добавлять request_id, user_id, error и произвольные поля к записям.
  5. package log
  6. import (
  7. "context"
  8. "log/slog"
  9. "os"
  10. "runtime"
  11. "strconv"
  12. )
  13. type ctxKey string
  14. const loggerKey ctxKey = "logger"
  15. var defaultLogger *slog.Logger
  16. func Init(env string) *slog.Logger {
  17. var handler slog.Handler
  18. if env == "production" {
  19. handler = slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
  20. Level: slog.LevelInfo,
  21. AddSource: true,
  22. ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
  23. if a.Key == slog.TimeKey {
  24. return slog.Attr{Key: "timestamp", Value: a.Value}
  25. }
  26. if a.Key == slog.LevelKey {
  27. return slog.Attr{Key: "level", Value: a.Value}
  28. }
  29. if a.Key == slog.MessageKey {
  30. return slog.Attr{Key: "message", Value: a.Value}
  31. }
  32. return a
  33. },
  34. })
  35. } else {
  36. handler = slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
  37. Level: slog.LevelDebug,
  38. AddSource: true,
  39. })
  40. }
  41. defaultLogger = slog.New(handler)
  42. slog.SetDefault(defaultLogger)
  43. return defaultLogger
  44. }
  45. func Default() *slog.Logger {
  46. if defaultLogger == nil {
  47. return slog.Default()
  48. }
  49. return defaultLogger
  50. }
  51. func FromContext(ctx context.Context) *slog.Logger {
  52. if logger, ok := ctx.Value(loggerKey).(*slog.Logger); ok {
  53. return logger
  54. }
  55. return Default()
  56. }
  57. func WithContext(ctx context.Context, logger *slog.Logger) context.Context {
  58. return context.WithValue(ctx, loggerKey, logger)
  59. }
  60. func WithRequestID(ctx context.Context, requestID string) *slog.Logger {
  61. return FromContext(ctx).With("request_id", requestID)
  62. }
  63. func WithUserID(ctx context.Context, userID string) *slog.Logger {
  64. return FromContext(ctx).With("user_id", userID)
  65. }
  66. func WithError(err error) slog.Attr {
  67. return slog.String("error", err.Error())
  68. }
  69. func WithFields(fields map[string]any) []slog.Attr {
  70. attrs := make([]slog.Attr, 0, len(fields))
  71. for k, v := range fields {
  72. attrs = append(attrs, slog.Any(k, v))
  73. }
  74. return attrs
  75. }
  76. func LogError(ctx context.Context, msg string, err error, args ...any) {
  77. logger := FromContext(ctx)
  78. allArgs := append([]any{"error", err}, args...)
  79. logger.ErrorContext(ctx, msg, allArgs...)
  80. }
  81. func LogWarn(ctx context.Context, msg string, args ...any) {
  82. FromContext(ctx).WarnContext(ctx, msg, args...)
  83. }
  84. func LogInfo(ctx context.Context, msg string, args ...any) {
  85. FromContext(ctx).InfoContext(ctx, msg, args...)
  86. }
  87. func LogDebug(ctx context.Context, msg string, args ...any) {
  88. FromContext(ctx).DebugContext(ctx, msg, args...)
  89. }
  90. func callerInfo(skip int) slog.Attr {
  91. _, file, line, ok := runtime.Caller(skip + 1)
  92. if !ok {
  93. return slog.String("caller", "unknown")
  94. }
  95. return slog.String("caller", file+":"+strconv.Itoa(line))
  96. }