From 40d2338c0f2d3bdd54e7891f41d22196056aa68f Mon Sep 17 00:00:00 2001 From: Ruidy Date: Fri, 12 Sep 2025 12:17:20 -0400 Subject: [PATCH] feat(logging): add slog-based structured logging Introduce slog-based structured logging throughout the booking service and server handlers. Add configurable log level via LOG_LEVEL environment variable. Replace legacy log usage with slog and propagate logger to booking service for improved observability. --- .golangci.yml | 10 ++++++++++ internal/config/config.go | 2 ++ internal/cron/job_report.go | 2 +- internal/driver/logger/log.go | 30 +++++++++++++++++++++++++++++ internal/server/handle_bookings.go | 3 +++ internal/service/booking/service.go | 13 ++++++++----- main.go | 7 ++++++- 7 files changed, 60 insertions(+), 7 deletions(-) create mode 100644 .golangci.yml create mode 100644 internal/driver/logger/log.go diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..62cf7ec --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,10 @@ +version: "2" +linters: + default: none + enable: + - sloglint + settings: + sloglint: + # Enforce using attributes only. + # This will raise an error for any key-value pair arguments. + attr-only: true diff --git a/internal/config/config.go b/internal/config/config.go index 07d9786..e63b3a7 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -17,6 +17,8 @@ type Config struct { DatabaseUrl string `env:"DATABASE_URL, required"` // Debug enables debug mode when true Debug bool `env:"DEBUG, default=false"` + // LogLevel is the logging level (e.g., debug, info, warn, error) + LogLevel string `env:"LOG_LEVEL, default=info"` // Origins is the list of allowed origins Origins []string `env:"ORIGINS, required"` // Port is the HTTP server port number diff --git a/internal/cron/job_report.go b/internal/cron/job_report.go index d45b7e3..96aa146 100644 --- a/internal/cron/job_report.go +++ b/internal/cron/job_report.go @@ -29,7 +29,7 @@ func JobMonthlyBookingReport() error { } store := booking.NewPgStore(db) - service, err := bookingService.NewService(store, nil, ps) + service, err := bookingService.NewService(nil, store, nil, ps) if err != nil { return fmt.Errorf("error creating booking service: %w", err) } diff --git a/internal/driver/logger/log.go b/internal/driver/logger/log.go new file mode 100644 index 0000000..80a0bd7 --- /dev/null +++ b/internal/driver/logger/log.go @@ -0,0 +1,30 @@ +package logger + +import ( + "log/slog" + "os" + "strings" +) + +var logLevel slog.LevelVar + +func getLevel(levelStr string) slog.Level { + switch strings.ToLower(levelStr) { + case "debug": + return slog.LevelDebug + case "warn": + return slog.LevelWarn + case "error": + return slog.LevelError + default: + return slog.LevelInfo + } +} + +func New(levelStr string) *slog.Logger { + logLevel.Set(getLevel(levelStr)) + + return slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ + Level: &logLevel, + })) +} diff --git a/internal/server/handle_bookings.go b/internal/server/handle_bookings.go index 27a1f07..1d4b07f 100644 --- a/internal/server/handle_bookings.go +++ b/internal/server/handle_bookings.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "io" + "log/slog" "net/http" "strconv" "time" @@ -31,6 +32,8 @@ func handleBookingListPage(bs *booking.Service, hc *config.Host) echo.HandlerFun bookings = bs.All() } + slog.Info("serving bookings", slog.Int("bookings_length", len(bookings))) + bvm := u.Map(bookings, func(b *booking.Line) *view.ListBookingsViewModel { return &view.ListBookingsViewModel{ Id: b.InvoiceNumber(hc), diff --git a/internal/service/booking/service.go b/internal/service/booking/service.go index e5031eb..e8e2892 100644 --- a/internal/service/booking/service.go +++ b/internal/service/booking/service.go @@ -1,7 +1,7 @@ package booking import ( - "log" + "log/slog" "time" "github.com/rjNemo/rentease/internal/config" @@ -46,10 +46,12 @@ type Service struct { store Store parser parserClient pdf PdfClient + logger *slog.Logger } -func NewService(store Store, parser parserClient, pdf PdfClient) (*Service, error) { +func NewService(logger *slog.Logger, store Store, parser parserClient, pdf PdfClient) (*Service, error) { return &Service{ + logger: logger.With(slog.String("component", "booking_service")), store: store, parser: parser, pdf: pdf, @@ -57,6 +59,7 @@ func NewService(store Store, parser parserClient, pdf PdfClient) (*Service, erro } func (bs Service) All() []*Line { + bs.logger.Info("fetching all bookings") return bs.store.All() } @@ -71,7 +74,7 @@ func (bs Service) Create(From time.Time, To time.Time, Name, PhoneNumber, Email, b := NewBooking(From, To, Name, PhoneNumber, Email, Platform, CustomerNumber, PlatformFees, externalId) err := bs.store.Create(b) if err != nil { - log.Println(err) + bs.logger.Info("failed to create booking", slog.Any("err", err)) } return b } @@ -86,7 +89,7 @@ func (bs Service) Update(id int, From time.Time, To time.Time, Name string, Phon ) *Booking { b := NewBooking(From, To, Name, PhoneNumber, Email, Platform, CustomerNumber, PlatformFees, externalId).WithId(id) if err := bs.store.Update(b); err != nil { - log.Println(err) + bs.logger.Info("failed to create booking", slog.Any("err", err)) } return b } @@ -94,7 +97,7 @@ func (bs Service) Update(id int, From time.Time, To time.Time, Name string, Phon func (bs Service) Cancel(id int) { err := bs.store.Cancel(id) if err != nil { - log.Println(err) + bs.logger.Info("failed to create booking", slog.Any("err", err)) } } diff --git a/main.go b/main.go index 126fc95..21547ff 100644 --- a/main.go +++ b/main.go @@ -3,6 +3,7 @@ package main import ( "context" "fmt" + "log/slog" "os" "os/signal" @@ -11,6 +12,7 @@ import ( "github.com/rjNemo/rentease/assets" "github.com/rjNemo/rentease/internal/config" "github.com/rjNemo/rentease/internal/driver/database" + "github.com/rjNemo/rentease/internal/driver/logger" "github.com/rjNemo/rentease/internal/driver/parser" "github.com/rjNemo/rentease/internal/driver/pdf" bookingRepo "github.com/rjNemo/rentease/internal/repository/booking" @@ -37,6 +39,9 @@ func run(c context.Context) error { return err } + appLogger := logger.New(appConfig.LogLevel) + slog.SetDefault(appLogger) + // init sentry if err := sentry.Init(sentry.ClientOptions{ Dsn: appConfig.SentryDsn, @@ -66,7 +71,7 @@ func run(c context.Context) error { parsingClient := parser.NewBookingAgentParser() - bookingService, err := booking.NewService(bookingStore, parsingClient, pc) + bookingService, err := booking.NewService(appLogger, bookingStore, parsingClient, pc) if err != nil { return fmt.Errorf("error creating booking service: %w", err) }