refactor: adopt portigo infrastructure patterns
Add config package with build-tag-switched dev/prod environments, structured logging via zerolog, Taskfile for dev workflow, golangci-lint config, testutil package, and improved DB setup with proper SQLite pragmas and cleanup. Rename sqlc output package from gen to repository. Switch to allowlist .gitignore, Alpine+UPX+scratch Dockerfile, and CI pipeline with test/lint gates before deploy.
This commit is contained in:
41
logging/log.go
Normal file
41
logging/log.go
Normal file
@@ -0,0 +1,41 @@
|
||||
// Package logging configures zerolog and provides HTTP request logging middleware.
|
||||
package logging
|
||||
|
||||
import (
|
||||
"io"
|
||||
stdlog "log"
|
||||
"os"
|
||||
|
||||
"github.com/ryanhamamura/c4/config"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/rs/zerolog/pkgerrors"
|
||||
)
|
||||
|
||||
func SetupLogger(env config.Environment, level zerolog.Level) *zerolog.Logger {
|
||||
zerolog.ErrorStackFieldName = "stack_trace"
|
||||
zerolog.ErrorStackMarshaler = pkgerrors.MarshalStack
|
||||
|
||||
zerolog.SetGlobalLevel(level)
|
||||
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
|
||||
|
||||
var output io.Writer
|
||||
switch env {
|
||||
case config.Dev:
|
||||
output = zerolog.ConsoleWriter{
|
||||
Out: os.Stderr,
|
||||
TimeFormat: "2006/01/02 15:04:05",
|
||||
}
|
||||
case config.Prod:
|
||||
output = os.Stderr
|
||||
}
|
||||
|
||||
logger := zerolog.New(output).With().Timestamp().Stack().Logger()
|
||||
zerolog.DefaultContextLogger = &logger
|
||||
log.Logger = logger
|
||||
|
||||
stdlog.SetFlags(0)
|
||||
stdlog.SetOutput(logger)
|
||||
return &logger
|
||||
}
|
||||
126
logging/middleware.go
Normal file
126
logging/middleware.go
Normal file
@@ -0,0 +1,126 @@
|
||||
package logging
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/ryanhamamura/c4/config"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
const (
|
||||
ansiReset = "\033[0m"
|
||||
ansiBrightRed = "\033[31;1m"
|
||||
ansiBrightGreen = "\033[32;1m"
|
||||
ansiBrightYellow = "\033[33;1m"
|
||||
ansiBrightMagenta = "\033[35;1m"
|
||||
ansiBrightCyan = "\033[36;1m"
|
||||
ansiGreen = "\033[32m"
|
||||
ansiYellow = "\033[33m"
|
||||
ansiRed = "\033[31m"
|
||||
)
|
||||
|
||||
func colorStatus(status int, useColor bool) string {
|
||||
s := fmt.Sprintf("%d", status)
|
||||
if !useColor {
|
||||
return s
|
||||
}
|
||||
switch {
|
||||
case status < 200:
|
||||
return ansiBrightGreen + s + ansiReset
|
||||
case status < 300:
|
||||
return ansiBrightGreen + s + ansiReset
|
||||
case status < 400:
|
||||
return ansiBrightCyan + s + ansiReset
|
||||
case status < 500:
|
||||
return ansiBrightYellow + s + ansiReset
|
||||
default:
|
||||
return ansiBrightRed + s + ansiReset
|
||||
}
|
||||
}
|
||||
|
||||
func colorMethod(method string, useColor bool) string {
|
||||
if !useColor {
|
||||
return method
|
||||
}
|
||||
return ansiBrightMagenta + method + ansiReset
|
||||
}
|
||||
|
||||
func colorLatency(d time.Duration, useColor bool) string {
|
||||
s := d.String()
|
||||
if !useColor {
|
||||
return s
|
||||
}
|
||||
switch {
|
||||
case d < 500*time.Millisecond:
|
||||
return ansiGreen + s + ansiReset
|
||||
case d < 5*time.Second:
|
||||
return ansiYellow + s + ansiReset
|
||||
default:
|
||||
return ansiRed + s + ansiReset
|
||||
}
|
||||
}
|
||||
|
||||
type responseWriter struct {
|
||||
http.ResponseWriter
|
||||
status int
|
||||
}
|
||||
|
||||
func (rw *responseWriter) WriteHeader(code int) {
|
||||
rw.status = code
|
||||
rw.ResponseWriter.WriteHeader(code)
|
||||
}
|
||||
|
||||
func RequestLogger(logger *zerolog.Logger, env config.Environment) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
start := time.Now()
|
||||
rw := &responseWriter{ResponseWriter: w}
|
||||
|
||||
next.ServeHTTP(rw, r)
|
||||
|
||||
status := rw.status
|
||||
if status == 0 {
|
||||
status = http.StatusOK
|
||||
}
|
||||
l := log.Ctx(r.Context())
|
||||
if l.GetLevel() == zerolog.Disabled {
|
||||
l = logger
|
||||
}
|
||||
|
||||
var evt *zerolog.Event
|
||||
switch {
|
||||
case status < 400:
|
||||
evt = l.Info()
|
||||
case status < 500:
|
||||
evt = l.Warn()
|
||||
case status < 600:
|
||||
evt = l.Error()
|
||||
default:
|
||||
evt = l.Info()
|
||||
}
|
||||
|
||||
latency := time.Since(start)
|
||||
switch env {
|
||||
case config.Dev:
|
||||
useColor := true
|
||||
evt.Msg(fmt.Sprintf("%s %s %s [%s]",
|
||||
colorStatus(status, useColor),
|
||||
colorMethod(r.Method, useColor),
|
||||
r.URL.Path,
|
||||
colorLatency(latency, useColor),
|
||||
))
|
||||
default:
|
||||
evt.
|
||||
Int("status", status).
|
||||
Str("method", r.Method).
|
||||
Str("path", r.URL.Path).
|
||||
Dur("latency", latency).
|
||||
Msg("request")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user