diff --git a/NOTES.md b/NOTES.md index 4b0a5c0..44de7b8 100644 --- a/NOTES.md +++ b/NOTES.md @@ -62,4 +62,12 @@ curl -w '\nTime: %{time_total}s \n' localhost:5002/v1/movies/4 } Time: 8.009385s +``` + +## Timeout a Curl Request + +We can timeout our curl request like so + +```bash +curl --max-time 2 localhost:5002/v1/movies/4 ``` \ No newline at end of file diff --git a/README.md b/README.md index 6a1cb87..9278b8f 100644 --- a/README.md +++ b/README.md @@ -260,4 +260,9 @@ If we want to perform some kind of custom decoding for our type, this can be don type Unmarshaler interface { UnmarshalJSON([]byte) error } -``` \ No newline at end of file +``` + +## Additional Resources + +- https://betterstack.com/community/guides/scaling-go/postgresql-pgx-golang/ +- https://betterstack.com/community/guides/scaling-go/golang-testify/ \ No newline at end of file diff --git a/cmd/api/handlers.go b/cmd/api/handlers.go index 0db03db..9c2eb84 100644 --- a/cmd/api/handlers.go +++ b/cmd/api/handlers.go @@ -243,6 +243,11 @@ func (app *application) deleteMovieHandler(w http.ResponseWriter, r *http.Reques } func (app *application) listMoviesHandler(w http.ResponseWriter, r *http.Request) { + qparams := r.URL.Query() + if len(qparams) != 0 { + app.logger.Debug("query params", "qp", qparams) + } + movies, err := app.models.Movies.List(r.Context()) if err != nil { app.serverErrorResponse(w, r, err) diff --git a/cmd/api/middleware.go b/cmd/api/middleware.go index b3475a5..9c8088b 100644 --- a/cmd/api/middleware.go +++ b/cmd/api/middleware.go @@ -28,3 +28,17 @@ func (app *application) recoverPanic(next http.Handler) http.Handler { next.ServeHTTP(w, r) }) } + +func (app *application) RquestLoggingMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + + var ( + ip = r.RemoteAddr + proto = r.Proto + method = r.Method + uri = r.URL.RequestURI() + ) + app.logger.Info("recieved request", "ip", ip, "proto", proto, "method", method, "uri", uri) + next.ServeHTTP(w, r) + }) +} diff --git a/cmd/api/routes.go b/cmd/api/routes.go index 9d77a77..0ec0b46 100644 --- a/cmd/api/routes.go +++ b/cmd/api/routes.go @@ -30,5 +30,5 @@ func (app *application) routes() http.Handler { router.HandlerFunc(http.MethodDelete, "/v1/movies/:id", app.deleteMovieHandler) // middleware - return app.recoverPanic(router) + return app.recoverPanic(app.RquestLoggingMiddleware(router)) } diff --git a/internal/config/config.go b/internal/config/config.go index 17e5b33..44c40e0 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -1,6 +1,10 @@ package config -import "github.com/kelseyhightower/envconfig" +import ( + "flag" + + "github.com/kelseyhightower/envconfig" +) type ServiceConfig struct { LogLevel string `default:"INFO"` @@ -12,5 +16,13 @@ type ServiceConfig struct { func GetServiceConfig() ServiceConfig { var sc ServiceConfig envconfig.MustProcess("PULLEY", &sc) + + // Flag overrides + port := flag.Int("port", sc.Port, "Service port. Default: '5002'") + logLevel := flag.String("log-level", sc.LogLevel, "Logging level Default:'INFO'") + flag.Parse() + sc.Port = *port + sc.LogLevel = *logLevel + return sc } diff --git a/internal/logging/logging.go b/internal/logging/logging.go index db07338..e832309 100644 --- a/internal/logging/logging.go +++ b/internal/logging/logging.go @@ -1,11 +1,20 @@ package logging import ( + "context" "io" "log/slog" "strings" ) +type CtxKey int + +const ( + _ CtxKey = iota + CtxKeyLogger + CtxKeyTraceID +) + func parseLogLevel(levelStr string) slog.Level { switch strings.ToUpper(levelStr) { case "DEBUG": @@ -34,3 +43,13 @@ func InitLogging(level string, w io.Writer, addSource bool) *slog.Logger { slog.SetDefault(logger) return logger } + +func LoggerFromCtx(ctx context.Context) *slog.Logger { + // context doesn't enforce type when context.WithValue(ctx, ctxKeyLogger, logger) is used + // it is stored as empty interface. So use type assertion .(*slog.Logger) to retrieve the + // value + if l, ok := ctx.Value(CtxKeyLogger).(*slog.Logger); ok && l != nil { + return l + } + return slog.Default() +} diff --git a/internal/logging/logging_test.go b/internal/logging/logging_test.go index dd9af15..7b6f0db 100644 --- a/internal/logging/logging_test.go +++ b/internal/logging/logging_test.go @@ -2,6 +2,7 @@ package logging import ( "bytes" + "context" "log/slog" "testing" @@ -63,3 +64,24 @@ func TestInitLogging(t *testing.T) { assert.StringContains(t, content, msg) } + +func TestLoggerFromCtx(t *testing.T) { + defaultLogger := slog.Default() + subLogger := defaultLogger.With("testLogger", "this is a test") + + t.Run("test logger returned is default", func(t *testing.T) { + assert.Equal(t, LoggerFromCtx(context.TODO()), defaultLogger) + }) + + t.Run("test logger returns saved logger", func(t *testing.T) { + assert.Equal(t, LoggerFromCtx(context.WithValue(context.TODO(), CtxKeyLogger, subLogger)), subLogger) + }) + + t.Run("test default logger when nil", func(t *testing.T) { + assert.Equal(t, LoggerFromCtx(context.WithValue(context.TODO(), CtxKeyLogger, nil)), defaultLogger) + }) + + t.Run("test default logger when wrong type", func(t *testing.T) { + assert.Equal(t, LoggerFromCtx(context.WithValue(context.TODO(), CtxKeyLogger, "bad type")), defaultLogger) + }) +}