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:
Ryan Hamamura
2026-03-02 11:48:47 -10:00
parent 6d4f3eb821
commit 2df20c2840
27 changed files with 694 additions and 143 deletions

41
logging/log.go Normal file
View 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
View 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")
}
})
}
}