Skip to content

Commit

Permalink
Merge pull request #108 from bmf-san/feature/disable-debug-endpoints
Browse files Browse the repository at this point in the history
Implement contextual logging and disable debug endpoints
  • Loading branch information
bmf-san authored Oct 8, 2023
2 parents d3fbbe1 + 3f53732 commit 0df3eeb
Show file tree
Hide file tree
Showing 9 changed files with 234 additions and 168 deletions.
12 changes: 12 additions & 0 deletions app/domain/logger.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package domain

import "context"

// A Logger is a logger interface.
type Logger interface {
WithTraceID(context.Context) context.Context
Error(string, ...any)
ErrorContext(context.Context, string, ...any)
Info(string, ...any)
InfoContext(context.Context, string, ...any)
}
58 changes: 54 additions & 4 deletions app/infrastructure/logger.go
Original file line number Diff line number Diff line change
@@ -1,16 +1,66 @@
package infrastructure

import (
"context"
"os"

"log/slog"

"github.com/google/uuid"
)

// Logger represents the singular of logger.
type Logger struct {
*slog.Logger
}

// NewLogger creates a logger.
func NewLogger(level int) *slog.Logger {
handler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
func NewLogger(level int) *Logger {
handler := TraceIDHandler{slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.Level(level),
})
})}
logger := slog.New(handler)
return logger
return &Logger{
logger,
}
}

// WithTraceID returns a context with a trace id.
func (l *Logger) WithTraceID(ctx context.Context) context.Context {
uuid, _ := uuid.NewRandom()
return context.WithValue(ctx, ctxTraceIDKey, uuid.String())
}

func (l *Logger) Error(msg string, args ...any) {
l.Logger.Error(msg, args...)
}

func (l *Logger) ErrorContext(ctx context.Context, msg string, args ...any) {
l.Logger.ErrorContext(ctx, msg, args...)
}

func (l *Logger) Info(msg string, args ...any) {
l.Logger.Info(msg, args...)
}

func (l *Logger) InfoContext(ctx context.Context, msg string, args ...any) {
l.Logger.InfoContext(ctx, msg, args...)
}

// TraceIDHandler represents the singular of trace id handler.
type TraceIDHandler struct {
slog.Handler
}

type ctxTraceID struct{}

var ctxTraceIDKey = ctxTraceID{}

// Handle implements slog.Handler.
func (t TraceIDHandler) Handle(ctx context.Context, r slog.Record) error {
tid, ok := ctx.Value(ctxTraceIDKey).(string)
if ok {
r.AddAttrs(slog.String("trace_id", tid))
}
return t.Handler.Handle(ctx, r)
}
40 changes: 25 additions & 15 deletions app/infrastructure/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,39 +8,49 @@ import (

"github.com/bmf-san/gobel-api/app/domain"
"github.com/bmf-san/gobel-api/app/interfaces/controller"
"github.com/bmf-san/gobel-api/app/interfaces/repository"
"github.com/bmf-san/gobel-api/app/usecase/repository"
)

// Middleware represents the plural of middelware.
type Middleware struct {
logger *slog.Logger
logger domain.Logger
adminRepository repository.AdminRepository
JWT repository.JWT
}

// NewLogger creates a Middleware.
func NewMiddleware(l *slog.Logger, ar repository.AdminRepository, jr repository.JWT) *Middleware {
func NewMiddleware(l *Logger, ar repository.AdminRepository, jr repository.JWT) *Middleware {
return &Middleware{
logger: l,
adminRepository: ar,
JWT: jr,
}
}

// Log is a middleware for logging. It logs the access log. It also adds a trace id to the context.
func (mw *Middleware) Log(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := mw.logger.WithTraceID(r.Context())

mw.logger.InfoContext(ctx, "access log", slog.String("http_method", r.Method), slog.String("path", r.URL.Path), slog.String("remote_addr", r.RemoteAddr), slog.String("user_agent", r.UserAgent()))
next.ServeHTTP(w, r.WithContext(ctx))
})
}

// Recovery is a middleware for recovering from panic.
func (mw *Middleware) Recovery(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
switch e := err.(type) {
case string:
mw.logger.Error("[panic] " + e)
mw.logger.ErrorContext(r.Context(), "[panic] "+e)
case runtime.Error:
mw.logger.Error("[panic] " + e.Error())
mw.logger.ErrorContext(r.Context(), "[panic] "+e.Error())
case error:
mw.logger.Error("[panic] " + e.Error())
mw.logger.ErrorContext(r.Context(), "[panic] "+e.Error())
default:
mw.logger.Error("[panic] " + e.(string))
mw.logger.ErrorContext(r.Context(), "[panic] "+e.(string))
}
controller.JSONResponse(w, http.StatusInternalServerError, nil)
}
Expand All @@ -56,28 +66,28 @@ func (mw *Middleware) Auth(next http.Handler) http.Handler {

verifiedToken, err := j.GetVerifiedAccessToken(r.Header.Get("Authorization"))
if err != nil {
mw.logger.Error(err.Error())
mw.logger.ErrorContext(r.Context(), err.Error())
controller.JSONResponse(w, http.StatusUnauthorized, nil)
return
}

accessUUID, err := j.GetAccessUUID(verifiedToken)
if err != nil {
mw.logger.Error(err.Error())
mw.logger.ErrorContext(r.Context(), err.Error())
controller.JSONResponse(w, http.StatusUnauthorized, nil)
return
}

adminID, err := mw.JWT.FindIDByAccessUUID(accessUUID)
if err != nil {
mw.logger.Error(err.Error())
mw.logger.ErrorContext(r.Context(), err.Error())
controller.JSONResponse(w, http.StatusUnauthorized, nil)
return
}

_, err = mw.adminRepository.FindByID(adminID)
if err != nil {
mw.logger.Error(err.Error())
mw.logger.ErrorContext(r.Context(), err.Error())
controller.JSONResponse(w, http.StatusUnauthorized, nil)
return
}
Expand All @@ -94,28 +104,28 @@ func (mw *Middleware) Refresh(next http.Handler) http.Handler {
verifiedToken, err := j.GetVerifiedRefreshToken(r.Header.Get("Authorization"))

if err != nil {
mw.logger.Error(err.Error())
mw.logger.ErrorContext(r.Context(), err.Error())
controller.JSONResponse(w, http.StatusUnauthorized, nil)
return
}

refreshUUID, err := j.GetRefreshUUID(verifiedToken)
if err != nil {
mw.logger.Error(err.Error())
mw.logger.ErrorContext(r.Context(), err.Error())
controller.JSONResponse(w, http.StatusUnauthorized, nil)
return
}

adminID, err := mw.JWT.FindIDByRefreshUUID(refreshUUID)
if err != nil {
mw.logger.Error(err.Error())
mw.logger.ErrorContext(r.Context(), err.Error())
controller.JSONResponse(w, http.StatusUnauthorized, nil)
return
}

_, err = mw.adminRepository.FindByID(adminID)
if err != nil {
mw.logger.Error(err.Error())
mw.logger.ErrorContext(r.Context(), err.Error())
controller.JSONResponse(w, http.StatusUnauthorized, nil)
return
}
Expand Down
73 changes: 36 additions & 37 deletions app/infrastructure/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package infrastructure

import (
"database/sql"
"log/slog"
"net/http"
"os"

Expand All @@ -13,12 +12,12 @@ import (
)

// Route sets the routing.
func Route(connm *sql.DB, connr *redis.Client, l *slog.Logger) *goblin.Router {
ar := repository.AdminRepository{
func Route(connm *sql.DB, connr *redis.Client, l *Logger) *goblin.Router {
ar := &repository.AdminRepository{
ConnMySQL: connm,
ConnRedis: connr,
}
jr := repository.JWT{
jr := &repository.JWT{
ConnRedis: connr,
}

Expand All @@ -41,7 +40,7 @@ func Route(connm *sql.DB, connr *redis.Client, l *slog.Logger) *goblin.Router {

r.DefaultOPTIONSHandler = defaultOPTIONSHandler

r.Methods(http.MethodGet).Use(mw.Recovery).Handler(`/healthcheck`, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
r.Methods(http.MethodGet).Use(mw.Log, mw.Recovery).Handler(`/healthcheck`, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("ok"))
}))
Expand All @@ -60,38 +59,38 @@ func Route(connm *sql.DB, connr *redis.Client, l *slog.Logger) *goblin.Router {
// r.Methods(http.MethodGet).Use(mw.Recovery).Handler("/debug/pprof/threadcreate", pprof.Handler("threadcreate"))
// r.Methods(http.MethodGet).Use(mw.Recovery).Handler("/debug/pprof/block", pprof.Handler("block"))

r.Methods(http.MethodGet).Use(mw.Recovery).Handler(`/posts`, postController.Index())
r.Methods(http.MethodGet).Use(mw.Recovery).Handler(`/posts/search`, postController.IndexByKeyword())
r.Methods(http.MethodGet).Use(mw.Recovery).Handler(`/posts/categories/:name`, postController.IndexByCategory())
r.Methods(http.MethodGet).Use(mw.Recovery).Handler(`/posts/tags/:name`, postController.IndexByTag())
r.Methods(http.MethodGet).Use(mw.Recovery).Handler(`/posts/:title`, postController.Show())
r.Methods(http.MethodPost).Use(mw.Recovery).Handler(`/posts/:title/comments`, commentController.Store())
r.Methods(http.MethodGet).Use(mw.Recovery).Handler(`/categories`, categoryController.Index())
r.Methods(http.MethodGet).Use(mw.Recovery).Handler(`/categories/:name`, categoryController.Show())
r.Methods(http.MethodGet).Use(mw.Recovery).Handler(`/tags`, tagController.Index())
r.Methods(http.MethodGet).Use(mw.Recovery).Handler(`/tags/:name`, tagController.Show())
r.Methods(http.MethodPost).Use(mw.Recovery).Handler(`/signin`, authController.SignIn())
r.Methods(http.MethodPost).Use(mw.Recovery, mw.Auth).Handler(`/private/signout`, authController.SignOut())
r.Methods(http.MethodPost).Use(mw.Recovery, mw.Refresh).Handler(`/private/refresh`, authController.Refresh())
r.Methods(http.MethodGet).Use(mw.Recovery, mw.Auth).Handler(`/private/me`, authController.ShowUserInfo())
r.Methods(http.MethodGet).Use(mw.Recovery, mw.Auth).Handler(`/private/posts`, postController.IndexPrivate())
r.Methods(http.MethodGet).Use(mw.Recovery, mw.Auth).Handler(`/private/posts/:id`, postController.ShowPrivate())
r.Methods(http.MethodPost).Use(mw.Recovery, mw.Auth).Handler(`/private/posts`, postController.StorePrivate())
r.Methods(http.MethodPatch).Use(mw.Recovery, mw.Auth).Handler(`/private/posts/:id`, postController.UpdatePrivate())
r.Methods(http.MethodDelete).Use(mw.Recovery, mw.Auth).Handler(`/private/posts/:id`, postController.DestroyPrivate())
r.Methods(http.MethodGet).Use(mw.Recovery, mw.Auth).Handler(`/private/comments`, commentController.IndexPrivate())
r.Methods(http.MethodGet).Use(mw.Recovery, mw.Auth).Handler(`/private/comments/:id`, commentController.ShowPrivate())
r.Methods(http.MethodPatch).Use(mw.Recovery, mw.Auth).Handler(`/private/comments/:id/status`, commentController.UpdateStatusPrivate())
r.Methods(http.MethodGet).Use(mw.Recovery, mw.Auth).Handler(`/private/categories`, categoryController.IndexPrivate())
r.Methods(http.MethodGet).Use(mw.Recovery, mw.Auth).Handler(`/private/categories/:id`, categoryController.ShowPrivate())
r.Methods(http.MethodPost).Use(mw.Recovery, mw.Auth).Handler(`/private/categories`, categoryController.StorePrivate())
r.Methods(http.MethodPatch).Use(mw.Recovery, mw.Auth).Handler(`/private/categories/:id`, categoryController.UpdatePrivate())
r.Methods(http.MethodDelete).Use(mw.Recovery, mw.Auth).Handler(`/private/categories/:id`, categoryController.DestroyPrivate())
r.Methods(http.MethodGet).Use(mw.Recovery, mw.Auth).Handler(`/private/tags`, tagController.IndexPrivate())
r.Methods(http.MethodGet).Use(mw.Recovery, mw.Auth).Handler(`/private/tags/:id`, tagController.ShowPrivate())
r.Methods(http.MethodPost).Use(mw.Recovery, mw.Auth).Handler(`/private/tags`, tagController.StorePrivate())
r.Methods(http.MethodPatch).Use(mw.Recovery, mw.Auth).Handler(`/private/tags/:id`, tagController.UpdatePrivate())
r.Methods(http.MethodDelete).Use(mw.Recovery, mw.Auth).Handler(`/private/tags/:id`, tagController.DestroyPrivate())
r.Methods(http.MethodGet).Use(mw.Log, mw.Recovery).Handler(`/posts`, postController.Index())
r.Methods(http.MethodGet).Use(mw.Log, mw.Recovery).Handler(`/posts/search`, postController.IndexByKeyword())
r.Methods(http.MethodGet).Use(mw.Log, mw.Recovery).Handler(`/posts/categories/:name`, postController.IndexByCategory())
r.Methods(http.MethodGet).Use(mw.Log, mw.Recovery).Handler(`/posts/tags/:name`, postController.IndexByTag())
r.Methods(http.MethodGet).Use(mw.Log, mw.Recovery).Handler(`/posts/:title`, postController.Show())
r.Methods(http.MethodPost).Use(mw.Log, mw.Recovery).Handler(`/posts/:title/comments`, commentController.Store())
r.Methods(http.MethodGet).Use(mw.Log, mw.Recovery).Handler(`/categories`, categoryController.Index())
r.Methods(http.MethodGet).Use(mw.Log, mw.Recovery).Handler(`/categories/:name`, categoryController.Show())
r.Methods(http.MethodGet).Use(mw.Log, mw.Recovery).Handler(`/tags`, tagController.Index())
r.Methods(http.MethodGet).Use(mw.Log, mw.Recovery).Handler(`/tags/:name`, tagController.Show())
r.Methods(http.MethodPost).Use(mw.Log, mw.Recovery).Handler(`/signin`, authController.SignIn())
r.Methods(http.MethodPost).Use(mw.Log, mw.Recovery, mw.Auth).Handler(`/private/signout`, authController.SignOut())
r.Methods(http.MethodPost).Use(mw.Log, mw.Recovery, mw.Refresh).Handler(`/private/refresh`, authController.Refresh())
r.Methods(http.MethodGet).Use(mw.Log, mw.Recovery, mw.Auth).Handler(`/private/me`, authController.ShowUserInfo())
r.Methods(http.MethodGet).Use(mw.Log, mw.Recovery, mw.Auth).Handler(`/private/posts`, postController.IndexPrivate())
r.Methods(http.MethodGet).Use(mw.Log, mw.Recovery, mw.Auth).Handler(`/private/posts/:id`, postController.ShowPrivate())
r.Methods(http.MethodPost).Use(mw.Log, mw.Recovery, mw.Auth).Handler(`/private/posts`, postController.StorePrivate())
r.Methods(http.MethodPatch).Use(mw.Log, mw.Recovery, mw.Auth).Handler(`/private/posts/:id`, postController.UpdatePrivate())
r.Methods(http.MethodDelete).Use(mw.Log, mw.Recovery, mw.Auth).Handler(`/private/posts/:id`, postController.DestroyPrivate())
r.Methods(http.MethodGet).Use(mw.Log, mw.Recovery, mw.Auth).Handler(`/private/comments`, commentController.IndexPrivate())
r.Methods(http.MethodGet).Use(mw.Log, mw.Recovery, mw.Auth).Handler(`/private/comments/:id`, commentController.ShowPrivate())
r.Methods(http.MethodPatch).Use(mw.Log, mw.Recovery, mw.Auth).Handler(`/private/comments/:id/status`, commentController.UpdateStatusPrivate())
r.Methods(http.MethodGet).Use(mw.Log, mw.Recovery, mw.Auth).Handler(`/private/categories`, categoryController.IndexPrivate())
r.Methods(http.MethodGet).Use(mw.Log, mw.Recovery, mw.Auth).Handler(`/private/categories/:id`, categoryController.ShowPrivate())
r.Methods(http.MethodPost).Use(mw.Log, mw.Recovery, mw.Auth).Handler(`/private/categories`, categoryController.StorePrivate())
r.Methods(http.MethodPatch).Use(mw.Log, mw.Recovery, mw.Auth).Handler(`/private/categories/:id`, categoryController.UpdatePrivate())
r.Methods(http.MethodDelete).Use(mw.Log, mw.Recovery, mw.Auth).Handler(`/private/categories/:id`, categoryController.DestroyPrivate())
r.Methods(http.MethodGet).Use(mw.Log, mw.Recovery, mw.Auth).Handler(`/private/tags`, tagController.IndexPrivate())
r.Methods(http.MethodGet).Use(mw.Log, mw.Recovery, mw.Auth).Handler(`/private/tags/:id`, tagController.ShowPrivate())
r.Methods(http.MethodPost).Use(mw.Log, mw.Recovery, mw.Auth).Handler(`/private/tags`, tagController.StorePrivate())
r.Methods(http.MethodPatch).Use(mw.Log, mw.Recovery, mw.Auth).Handler(`/private/tags/:id`, tagController.UpdatePrivate())
r.Methods(http.MethodDelete).Use(mw.Log, mw.Recovery, mw.Auth).Handler(`/private/tags/:id`, tagController.DestroyPrivate())

return r
}
Loading

0 comments on commit 0df3eeb

Please sign in to comment.