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 (
"database/sql"
"log"
"log/slog"
"net/http"
"os"
"gitea.michaelthomson.dev/mthomson/habits/internal/logging"
"gitea.michaelthomson.dev/mthomson/habits/internal/middleware"
"gitea.michaelthomson.dev/mthomson/habits/internal/migrate"
todohandler "gitea.michaelthomson.dev/mthomson/habits/internal/todo/handler"
todorepository "gitea.michaelthomson.dev/mthomson/habits/internal/todo/repository/postgres"
todoservice "gitea.michaelthomson.dev/mthomson/habits/internal/todo/service"
_ "github.com/jackc/pgx/v5/stdlib"
_ "github.com/mattn/go-sqlite3"
)
func main() {
// create logger
httpLogger := slog.New(slog.NewTextHandler(os.Stdout, nil))
logger := logging.NewLogger()
// create middlewares
loggingMiddleware := middleware.LoggingMiddleware(httpLogger)
contextMiddleware := middleware.ContextMiddleware(logger)
loggingMiddleware := middleware.LoggingMiddleware(logger)
// create db pool
postgresUrl := "postgres://todo:password@localhost:5432/todo"
@ -35,19 +34,19 @@ func main() {
migrate.Migrate(db)
// create repos
todoRepository := todorepository.NewPostgresTodoRepository(db)
todoRepository := todorepository.NewPostgresTodoRepository(logger, db)
// create services
todoService := todoservice.NewTodoService(todoRepository)
todoService := todoservice.NewTodoService(logger, todoRepository)
// create mux
mux := http.NewServeMux()
// register handlers
mux.Handle("GET /todo/{id}", loggingMiddleware(todohandler.HandleTodoGet(todoService)))
mux.Handle("POST /todo", loggingMiddleware(todohandler.HandleTodoCreate(todoService)))
mux.Handle("DELETE /todo/{id}", loggingMiddleware(todohandler.HandleTodoDelete(todoService)))
mux.Handle("PUT /todo/{id}", loggingMiddleware(todohandler.HandleTodoUpdate(todoService)))
mux.Handle("GET /todo/{id}", contextMiddleware(loggingMiddleware(todohandler.HandleTodoGet(logger, todoService))))
mux.Handle("POST /todo", contextMiddleware(loggingMiddleware(todohandler.HandleTodoCreate(logger, todoService))))
mux.Handle("DELETE /todo/{id}", contextMiddleware(loggingMiddleware(todohandler.HandleTodoDelete(logger, todoService))))
mux.Handle("PUT /todo/{id}", contextMiddleware(loggingMiddleware(todohandler.HandleTodoUpdate(logger, todoService))))
// create 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
import (
"context"
"log/slog"
"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 {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
logger.LogAttrs(
context.Background(),
slog.LevelInfo,
"Incoming request",
logger.InfoContext(r.Context(), "Incoming request",
slog.String("method", r.Method),
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 (
"encoding/json"
"fmt"
"log/slog"
"net/http"
"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}
}
func HandleTodoCreate(todoService TodoCreater) http.HandlerFunc {
func HandleTodoCreate(logger *slog.Logger, todoService TodoCreater) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
createTodoRequest := CreateTodoRequest{}
decoder := json.NewDecoder(r.Body)
decoder.DisallowUnknownFields()
err := decoder.Decode(&createTodoRequest)
if err != nil {
logger.ErrorContext(ctx, err.Error())
http.Error(w, "", http.StatusBadRequest)
return
}
todo, err := todoService.CreateTodo(TodoFromCreateTodoRequest(createTodoRequest))
todo, err := todoService.CreateTodo(ctx, TodoFromCreateTodoRequest(createTodoRequest))
if err != nil {
if err == service.ErrNotFound {
@ -47,6 +50,7 @@ func HandleTodoCreate(todoService TodoCreater) http.HandlerFunc {
return
}
logger.ErrorContext(ctx, err.Error())
http.Error(w, "", http.StatusInternalServerError)
return
}
@ -60,6 +64,7 @@ func HandleTodoCreate(todoService TodoCreater) http.HandlerFunc {
err = json.NewEncoder(w).Encode(response)
if err != nil {
logger.ErrorContext(ctx, err.Error())
http.Error(w, "", http.StatusInternalServerError)
return
}

View File

@ -1,24 +1,27 @@
package handler
import (
"log/slog"
"net/http"
"strconv"
"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) {
ctx := r.Context()
idString := r.PathValue("id")
id, err := strconv.ParseInt(idString, 10, 64)
if err != nil {
slog.ErrorContext(ctx, err.Error())
http.Error(w, "", http.StatusBadRequest)
return
}
err = todoService.DeleteTodo(id)
err = todoService.DeleteTodo(ctx, id)
if err != nil {
if err == service.ErrNotFound {
@ -26,6 +29,7 @@ func HandleTodoDelete(todoService TodoDeleter) http.HandlerFunc {
return
}
slog.ErrorContext(ctx, err.Error())
http.Error(w, "", http.StatusInternalServerError)
return
}

View File

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

View File

@ -1,21 +1,23 @@
package handler
import (
"context"
"gitea.michaelthomson.dev/mthomson/habits/internal/todo/service"
)
type TodoGetter interface {
GetTodo(id int64) (service.Todo, error)
GetTodo(ctx context.Context, id int64) (service.Todo, error)
}
type TodoCreater interface {
CreateTodo(todo service.Todo) (service.Todo, error)
CreateTodo(ctx context.Context, todo service.Todo) (service.Todo, error)
}
type TodoDeleter interface {
DeleteTodo(id int64) error
DeleteTodo(ctx context.Context, id int64) error
}
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 (
"encoding/json"
"log/slog"
"net/http"
"strconv"
@ -17,14 +18,16 @@ func TodoFromUpdateTodoRequest(todo UpdateTodoRequest, id int64) service.Todo {
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) {
ctx := r.Context()
updateTodoRequest := UpdateTodoRequest{}
decoder := json.NewDecoder(r.Body)
decoder.DisallowUnknownFields()
err := decoder.Decode(&updateTodoRequest)
if err != nil {
logger.ErrorContext(ctx, err.Error())
http.Error(w, "", http.StatusBadRequest)
return
}
@ -34,11 +37,12 @@ func HandleTodoUpdate(todoService TodoUpdater) http.HandlerFunc {
id, err := strconv.ParseInt(idString, 10, 64)
if err != nil {
logger.ErrorContext(ctx, err.Error())
http.Error(w, "", http.StatusBadRequest)
return
}
err = todoService.UpdateTodo(TodoFromUpdateTodoRequest(updateTodoRequest, id))
err = todoService.UpdateTodo(ctx, TodoFromUpdateTodoRequest(updateTodoRequest, id))
if err != nil {
if err == service.ErrNotFound {
@ -46,6 +50,7 @@ func HandleTodoUpdate(todoService TodoUpdater) http.HandlerFunc {
return
}
logger.ErrorContext(ctx, err.Error())
http.Error(w, "", http.StatusInternalServerError)
return
}

View File

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

View File

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

View File

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