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.
This commit is contained in:
Ruidy 2025-09-12 12:17:20 -04:00
parent 2aadb421ef
commit 40d2338c0f
No known key found for this signature in database
GPG key ID: 705C24D202990805
7 changed files with 60 additions and 7 deletions

10
.golangci.yml Normal file
View file

@ -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

View file

@ -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

View file

@ -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)
}

View file

@ -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,
}))
}

View file

@ -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),

View file

@ -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))
}
}

View file

@ -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)
}