logging update with context

This commit is contained in:
Michael Thomson 2025-03-10 16:35:22 -04:00
parent 180e0a96e7
commit 96975c7bd2
12 changed files with 141 additions and 38 deletions

View File

@ -3,25 +3,24 @@ package main
import ( import (
"database/sql" "database/sql"
"log" "log"
"log/slog"
"net/http" "net/http"
"os"
"gitea.michaelthomson.dev/mthomson/habits/internal/logging"
"gitea.michaelthomson.dev/mthomson/habits/internal/middleware" "gitea.michaelthomson.dev/mthomson/habits/internal/middleware"
"gitea.michaelthomson.dev/mthomson/habits/internal/migrate" "gitea.michaelthomson.dev/mthomson/habits/internal/migrate"
todohandler "gitea.michaelthomson.dev/mthomson/habits/internal/todo/handler" todohandler "gitea.michaelthomson.dev/mthomson/habits/internal/todo/handler"
todorepository "gitea.michaelthomson.dev/mthomson/habits/internal/todo/repository/postgres" todorepository "gitea.michaelthomson.dev/mthomson/habits/internal/todo/repository/postgres"
todoservice "gitea.michaelthomson.dev/mthomson/habits/internal/todo/service" todoservice "gitea.michaelthomson.dev/mthomson/habits/internal/todo/service"
_ "github.com/jackc/pgx/v5/stdlib" _ "github.com/jackc/pgx/v5/stdlib"
_ "github.com/mattn/go-sqlite3"
) )
func main() { func main() {
// create logger // create logger
httpLogger := slog.New(slog.NewTextHandler(os.Stdout, nil)) logger := logging.NewLogger()
// create middlewares // create middlewares
loggingMiddleware := middleware.LoggingMiddleware(httpLogger) contextMiddleware := middleware.ContextMiddleware(logger)
loggingMiddleware := middleware.LoggingMiddleware(logger)
// create db pool // create db pool
postgresUrl := "postgres://todo:password@localhost:5432/todo" postgresUrl := "postgres://todo:password@localhost:5432/todo"
@ -35,19 +34,19 @@ func main() {
migrate.Migrate(db) migrate.Migrate(db)
// create repos // create repos
todoRepository := todorepository.NewPostgresTodoRepository(db) todoRepository := todorepository.NewPostgresTodoRepository(logger, db)
// create services // create services
todoService := todoservice.NewTodoService(todoRepository) todoService := todoservice.NewTodoService(logger, todoRepository)
// create mux // create mux
mux := http.NewServeMux() mux := http.NewServeMux()
// register handlers // register handlers
mux.Handle("GET /todo/{id}", loggingMiddleware(todohandler.HandleTodoGet(todoService))) mux.Handle("GET /todo/{id}", contextMiddleware(loggingMiddleware(todohandler.HandleTodoGet(logger, todoService))))
mux.Handle("POST /todo", loggingMiddleware(todohandler.HandleTodoCreate(todoService))) mux.Handle("POST /todo", contextMiddleware(loggingMiddleware(todohandler.HandleTodoCreate(logger, todoService))))
mux.Handle("DELETE /todo/{id}", loggingMiddleware(todohandler.HandleTodoDelete(todoService))) mux.Handle("DELETE /todo/{id}", contextMiddleware(loggingMiddleware(todohandler.HandleTodoDelete(logger, todoService))))
mux.Handle("PUT /todo/{id}", loggingMiddleware(todohandler.HandleTodoUpdate(todoService))) mux.Handle("PUT /todo/{id}", contextMiddleware(loggingMiddleware(todohandler.HandleTodoUpdate(logger, todoService))))
// create server // create server
server := &http.Server{ server := &http.Server{

View File

@ -0,0 +1,26 @@
package logging
import (
"context"
"log/slog"
"os"
)
type ContextHandler struct {
slog.Handler
}
func (h *ContextHandler) Handle(ctx context.Context, r slog.Record) error {
if requestID, ok := ctx.Value("trace_id").(string); ok {
r.AddAttrs(slog.String("trace_id", requestID))
}
return h.Handler.Handle(ctx, r)
}
func NewLogger() *slog.Logger {
baseHandler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{AddSource: false})
customHandler := &ContextHandler{Handler: baseHandler}
logger := slog.New(customHandler)
return logger
}

View File

@ -0,0 +1,21 @@
package middleware
import (
"context"
"log/slog"
"net/http"
"github.com/google/uuid"
)
func ContextMiddleware(logger *slog.Logger) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
traceid := uuid.NewString()
ctx := context.WithValue(r.Context(), "trace_id", traceid)
newReq := r.WithContext(ctx)
next.ServeHTTP(w, newReq)
})
}
}

View File

@ -1,23 +1,39 @@
package middleware package middleware
import ( import (
"context"
"log/slog" "log/slog"
"net/http" "net/http"
) )
type LoggingResponseWriter struct {
http.ResponseWriter
statusCode int
}
func NewLoggingResponseWriter(w http.ResponseWriter) *LoggingResponseWriter {
return &LoggingResponseWriter{w, http.StatusOK}
}
func (lrw *LoggingResponseWriter) WriteHeader(code int) {
lrw.statusCode = code
lrw.ResponseWriter.WriteHeader(code)
}
func LoggingMiddleware(logger *slog.Logger) func(http.Handler) http.Handler { func LoggingMiddleware(logger *slog.Logger) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
logger.LogAttrs( logger.InfoContext(r.Context(), "Incoming request",
context.Background(),
slog.LevelInfo,
"Incoming request",
slog.String("method", r.Method), slog.String("method", r.Method),
slog.String("path", r.URL.String()), slog.String("path", r.URL.String()),
) )
next.ServeHTTP(w, r) lrw := NewLoggingResponseWriter(w)
next.ServeHTTP(lrw, r)
logger.InfoContext(r.Context(), "Sent response",
slog.Int("code", lrw.statusCode),
slog.String("message", http.StatusText(lrw.statusCode)),
)
}) })
} }
} }

View File

@ -3,6 +3,7 @@ package handler
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"log/slog"
"net/http" "net/http"
"gitea.michaelthomson.dev/mthomson/habits/internal/todo/service" "gitea.michaelthomson.dev/mthomson/habits/internal/todo/service"
@ -27,19 +28,21 @@ func CreateTodoResponseFromTodo(todo service.Todo) CreateTodoResponse {
return CreateTodoResponse{Id: todo.Id, Name: todo.Name, Done: todo.Done} return CreateTodoResponse{Id: todo.Id, Name: todo.Name, Done: todo.Done}
} }
func HandleTodoCreate(todoService TodoCreater) http.HandlerFunc { func HandleTodoCreate(logger *slog.Logger, todoService TodoCreater) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
createTodoRequest := CreateTodoRequest{} createTodoRequest := CreateTodoRequest{}
decoder := json.NewDecoder(r.Body) decoder := json.NewDecoder(r.Body)
decoder.DisallowUnknownFields() decoder.DisallowUnknownFields()
err := decoder.Decode(&createTodoRequest) err := decoder.Decode(&createTodoRequest)
if err != nil { if err != nil {
logger.ErrorContext(ctx, err.Error())
http.Error(w, "", http.StatusBadRequest) http.Error(w, "", http.StatusBadRequest)
return return
} }
todo, err := todoService.CreateTodo(TodoFromCreateTodoRequest(createTodoRequest)) todo, err := todoService.CreateTodo(ctx, TodoFromCreateTodoRequest(createTodoRequest))
if err != nil { if err != nil {
if err == service.ErrNotFound { if err == service.ErrNotFound {
@ -47,6 +50,7 @@ func HandleTodoCreate(todoService TodoCreater) http.HandlerFunc {
return return
} }
logger.ErrorContext(ctx, err.Error())
http.Error(w, "", http.StatusInternalServerError) http.Error(w, "", http.StatusInternalServerError)
return return
} }
@ -60,6 +64,7 @@ func HandleTodoCreate(todoService TodoCreater) http.HandlerFunc {
err = json.NewEncoder(w).Encode(response) err = json.NewEncoder(w).Encode(response)
if err != nil { if err != nil {
logger.ErrorContext(ctx, err.Error())
http.Error(w, "", http.StatusInternalServerError) http.Error(w, "", http.StatusInternalServerError)
return return
} }

View File

@ -1,24 +1,27 @@
package handler package handler
import ( import (
"log/slog"
"net/http" "net/http"
"strconv" "strconv"
"gitea.michaelthomson.dev/mthomson/habits/internal/todo/service" "gitea.michaelthomson.dev/mthomson/habits/internal/todo/service"
) )
func HandleTodoDelete(todoService TodoDeleter) http.HandlerFunc { func HandleTodoDelete(logger *slog.Logger, todoService TodoDeleter) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
idString := r.PathValue("id") idString := r.PathValue("id")
id, err := strconv.ParseInt(idString, 10, 64) id, err := strconv.ParseInt(idString, 10, 64)
if err != nil { if err != nil {
slog.ErrorContext(ctx, err.Error())
http.Error(w, "", http.StatusBadRequest) http.Error(w, "", http.StatusBadRequest)
return return
} }
err = todoService.DeleteTodo(id) err = todoService.DeleteTodo(ctx, id)
if err != nil { if err != nil {
if err == service.ErrNotFound { if err == service.ErrNotFound {
@ -26,6 +29,7 @@ func HandleTodoDelete(todoService TodoDeleter) http.HandlerFunc {
return return
} }
slog.ErrorContext(ctx, err.Error())
http.Error(w, "", http.StatusInternalServerError) http.Error(w, "", http.StatusInternalServerError)
return return
} }

View File

@ -2,6 +2,7 @@ package handler
import ( import (
"encoding/json" "encoding/json"
"log/slog"
"net/http" "net/http"
"strconv" "strconv"
@ -18,18 +19,20 @@ func GetTodoResponseFromTodo(todo service.Todo) GetTodoResponse {
return GetTodoResponse{Id: todo.Id, Name: todo.Name, Done: todo.Done} return GetTodoResponse{Id: todo.Id, Name: todo.Name, Done: todo.Done}
} }
func HandleTodoGet(todoService TodoGetter) http.HandlerFunc { func HandleTodoGet(logger *slog.Logger, todoService TodoGetter) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
idString := r.PathValue("id") idString := r.PathValue("id")
id, err := strconv.ParseInt(idString, 10, 64) id, err := strconv.ParseInt(idString, 10, 64)
if err != nil { if err != nil {
logger.ErrorContext(ctx, err.Error())
http.Error(w, "", http.StatusBadRequest) http.Error(w, "", http.StatusBadRequest)
return return
} }
todo, err := todoService.GetTodo(id) todo, err := todoService.GetTodo(ctx, id)
if err != nil { if err != nil {
if err == service.ErrNotFound { if err == service.ErrNotFound {
@ -37,6 +40,7 @@ func HandleTodoGet(todoService TodoGetter) http.HandlerFunc {
return return
} }
logger.ErrorContext(ctx, err.Error())
http.Error(w, "", http.StatusInternalServerError) http.Error(w, "", http.StatusInternalServerError)
return return
} }
@ -49,6 +53,7 @@ func HandleTodoGet(todoService TodoGetter) http.HandlerFunc {
err = json.NewEncoder(w).Encode(response) err = json.NewEncoder(w).Encode(response)
if err != nil { if err != nil {
logger.ErrorContext(ctx, err.Error())
http.Error(w, "", http.StatusInternalServerError) http.Error(w, "", http.StatusInternalServerError)
return return
} }

View File

@ -1,21 +1,23 @@
package handler package handler
import ( import (
"context"
"gitea.michaelthomson.dev/mthomson/habits/internal/todo/service" "gitea.michaelthomson.dev/mthomson/habits/internal/todo/service"
) )
type TodoGetter interface { type TodoGetter interface {
GetTodo(id int64) (service.Todo, error) GetTodo(ctx context.Context, id int64) (service.Todo, error)
} }
type TodoCreater interface { type TodoCreater interface {
CreateTodo(todo service.Todo) (service.Todo, error) CreateTodo(ctx context.Context, todo service.Todo) (service.Todo, error)
} }
type TodoDeleter interface { type TodoDeleter interface {
DeleteTodo(id int64) error DeleteTodo(ctx context.Context, id int64) error
} }
type TodoUpdater interface { type TodoUpdater interface {
UpdateTodo(todo service.Todo) error UpdateTodo(ctx context.Context, todo service.Todo) error
} }

View File

@ -2,6 +2,7 @@ package handler
import ( import (
"encoding/json" "encoding/json"
"log/slog"
"net/http" "net/http"
"strconv" "strconv"
@ -17,14 +18,16 @@ func TodoFromUpdateTodoRequest(todo UpdateTodoRequest, id int64) service.Todo {
return service.Todo{Id: id, Name: todo.Name, Done: todo.Done} return service.Todo{Id: id, Name: todo.Name, Done: todo.Done}
} }
func HandleTodoUpdate(todoService TodoUpdater) http.HandlerFunc { func HandleTodoUpdate(logger *slog.Logger, todoService TodoUpdater) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
updateTodoRequest := UpdateTodoRequest{} updateTodoRequest := UpdateTodoRequest{}
decoder := json.NewDecoder(r.Body) decoder := json.NewDecoder(r.Body)
decoder.DisallowUnknownFields() decoder.DisallowUnknownFields()
err := decoder.Decode(&updateTodoRequest) err := decoder.Decode(&updateTodoRequest)
if err != nil { if err != nil {
logger.ErrorContext(ctx, err.Error())
http.Error(w, "", http.StatusBadRequest) http.Error(w, "", http.StatusBadRequest)
return return
} }
@ -34,11 +37,12 @@ func HandleTodoUpdate(todoService TodoUpdater) http.HandlerFunc {
id, err := strconv.ParseInt(idString, 10, 64) id, err := strconv.ParseInt(idString, 10, 64)
if err != nil { if err != nil {
logger.ErrorContext(ctx, err.Error())
http.Error(w, "", http.StatusBadRequest) http.Error(w, "", http.StatusBadRequest)
return return
} }
err = todoService.UpdateTodo(TodoFromUpdateTodoRequest(updateTodoRequest, id)) err = todoService.UpdateTodo(ctx, TodoFromUpdateTodoRequest(updateTodoRequest, id))
if err != nil { if err != nil {
if err == service.ErrNotFound { if err == service.ErrNotFound {
@ -46,6 +50,7 @@ func HandleTodoUpdate(todoService TodoUpdater) http.HandlerFunc {
return return
} }
logger.ErrorContext(ctx, err.Error())
http.Error(w, "", http.StatusInternalServerError) http.Error(w, "", http.StatusInternalServerError)
return return
} }

View File

@ -2,16 +2,19 @@ package postgres
import ( import (
"database/sql" "database/sql"
"log/slog"
"gitea.michaelthomson.dev/mthomson/habits/internal/todo/repository" "gitea.michaelthomson.dev/mthomson/habits/internal/todo/repository"
) )
type PostgresTodoRepository struct { type PostgresTodoRepository struct {
logger *slog.Logger
db *sql.DB db *sql.DB
} }
func NewPostgresTodoRepository(db *sql.DB) *PostgresTodoRepository { func NewPostgresTodoRepository(logger *slog.Logger, db *sql.DB) *PostgresTodoRepository {
return &PostgresTodoRepository{ return &PostgresTodoRepository{
logger: logger,
db: db, db: db,
} }
} }

View File

@ -1,8 +1,9 @@
package service package service
import ( import (
"context"
"errors" "errors"
"log" "log/slog"
"gitea.michaelthomson.dev/mthomson/habits/internal/todo/repository" "gitea.michaelthomson.dev/mthomson/habits/internal/todo/repository"
) )
@ -41,14 +42,18 @@ type TodoRepository interface {
} }
type TodoService struct { type TodoService struct {
logger *slog.Logger
repo TodoRepository repo TodoRepository
} }
func NewTodoService(todoRepo TodoRepository) *TodoService { func NewTodoService(logger *slog.Logger, todoRepo TodoRepository) *TodoService {
return &TodoService{todoRepo} return &TodoService{
logger: logger,
repo: todoRepo,
}
} }
func (s *TodoService) GetTodo(id int64) (Todo, error) { func (s *TodoService) GetTodo(ctx context.Context, id int64) (Todo, error) {
todo, err := s.repo.GetById(id) todo, err := s.repo.GetById(id)
if err != nil { if err != nil {
@ -56,36 +61,41 @@ func (s *TodoService) GetTodo(id int64) (Todo, error) {
return Todo{}, ErrNotFound return Todo{}, ErrNotFound
} }
s.logger.ErrorContext(ctx, err.Error())
return Todo{}, err return Todo{}, err
} }
return TodoFromTodoRow(todo), err return TodoFromTodoRow(todo), err
} }
func (s *TodoService) CreateTodo(todo Todo) (Todo, error) { func (s *TodoService) CreateTodo(ctx context.Context, todo Todo) (Todo, error) {
todoRow := TodoRowFromTodo(todo) todoRow := TodoRowFromTodo(todo)
newTodoRow, err := s.repo.Create(todoRow) newTodoRow, err := s.repo.Create(todoRow)
if err != nil { if err != nil {
log.Print(err) s.logger.ErrorContext(ctx, err.Error())
return Todo{}, err return Todo{}, err
} }
return TodoFromTodoRow(newTodoRow), err return TodoFromTodoRow(newTodoRow), err
} }
func (s *TodoService) DeleteTodo(id int64) error { func (s *TodoService) DeleteTodo(ctx context.Context, id int64) error {
err := s.repo.Delete(id) err := s.repo.Delete(id)
if err == repository.ErrNotFound { if err == repository.ErrNotFound {
return ErrNotFound return ErrNotFound
} }
if (err != nil) {
s.logger.ErrorContext(ctx, err.Error())
}
return err return err
} }
func (s *TodoService) UpdateTodo(todo Todo) error { func (s *TodoService) UpdateTodo(ctx context.Context, todo Todo) error {
todoRow := TodoRowFromTodo(todo) todoRow := TodoRowFromTodo(todo)
err := s.repo.Update(todoRow) err := s.repo.Update(todoRow)
@ -94,5 +104,9 @@ func (s *TodoService) UpdateTodo(todo Todo) error {
return ErrNotFound return ErrNotFound
} }
if (err != nil) {
s.logger.ErrorContext(ctx, err.Error())
}
return err return err
} }

View File

@ -7,5 +7,8 @@ build:
test: test:
go test ./... go test ./...
format:
go fmt ./...
clean: clean:
rm tmp/main habits.db rm tmp/main habits.db