You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
94 lines
2.8 KiB
94 lines
2.8 KiB
// Package logging configures the application's structured logger (log/slog) |
|
// based on the environment: human-readable text in development, JSON in production. |
|
package logging |
|
|
|
import ( |
|
"io" |
|
"log/slog" |
|
"os" |
|
"strings" |
|
"time" |
|
|
|
"github.com/gin-gonic/gin" |
|
|
|
"git/palnet/slack-notifier/internal/config" |
|
) |
|
|
|
// Setup builds the slog logger from config and installs it as the default |
|
// (so package-level slog.Info/Warn/Error calls everywhere use this config). |
|
// |
|
// poster가 nil이 아니고 cfg.MonitorChannel이 설정돼 있으면, Error 이상 레벨의 로그를 |
|
// 해당 Slack 채널로도 알린다. (poster는 보통 *slack.Client) |
|
func Setup(cfg config.Config, poster AlertPoster) *slog.Logger { |
|
w := resolveWriter(cfg) |
|
|
|
opts := &slog.HandlerOptions{Level: parseLevel(cfg.LogLevel)} |
|
|
|
var h slog.Handler |
|
if strings.EqualFold(cfg.LogFormat, "json") { |
|
h = slog.NewJSONHandler(w, opts) |
|
} else { |
|
h = slog.NewTextHandler(w, opts) |
|
} |
|
|
|
if poster != nil && cfg.MonitorChannel != "" { |
|
h = newSlackAlertHandler(h, poster, cfg.MonitorChannel) |
|
} |
|
|
|
logger := slog.New(h).With("service", "slack-notifier", "env", cfg.Env) |
|
slog.SetDefault(logger) |
|
return logger |
|
} |
|
|
|
// resolveWriter returns stdout (development), or stdout+날짜별 파일 when LOG_FILE is set |
|
// (production defaults to logs/app.log → logs/app-YYYY-MM-DD.log). 날짜가 바뀌면 새 파일로 |
|
// 회전하고 LOG_RETENTION_DAYS일치만 보존한다. stdout도 유지해 Docker/journald가 함께 캡처. |
|
func resolveWriter(cfg config.Config) io.Writer { |
|
if cfg.LogFile == "" { |
|
return os.Stdout |
|
} |
|
dw, err := newDailyWriter(cfg.LogFile, cfg.LogRetentionDays) |
|
if err != nil { |
|
// 파일 준비 실패 — stdout으로 폴백(기본 로거로 경고). |
|
slog.Warn("cannot open log file, using stdout only", "file", cfg.LogFile, "err", err) |
|
return os.Stdout |
|
} |
|
return io.MultiWriter(os.Stdout, dw) |
|
} |
|
|
|
func parseLevel(s string) slog.Level { |
|
switch strings.ToLower(s) { |
|
case "debug": |
|
return slog.LevelDebug |
|
case "warn", "warning": |
|
return slog.LevelWarn |
|
case "error": |
|
return slog.LevelError |
|
default: |
|
return slog.LevelInfo |
|
} |
|
} |
|
|
|
// GinMiddleware logs each HTTP request through slog, replacing gin.Logger() |
|
// so access logs share the same env-based format. |
|
func GinMiddleware() gin.HandlerFunc { |
|
return func(c *gin.Context) { |
|
start := time.Now() |
|
c.Next() |
|
|
|
level := slog.LevelInfo |
|
if c.Writer.Status() >= 500 { |
|
level = slog.LevelError |
|
} else if c.Writer.Status() >= 400 { |
|
level = slog.LevelWarn |
|
} |
|
|
|
slog.LogAttrs(c.Request.Context(), level, "http_request", |
|
slog.String("method", c.Request.Method), |
|
slog.String("path", c.Request.URL.Path), |
|
slog.Int("status", c.Writer.Status()), |
|
slog.Int64("latency_ms", time.Since(start).Milliseconds()), |
|
slog.String("client_ip", c.ClientIP()), |
|
) |
|
} |
|
}
|
|
|