From b6f3ed314e1874175bfc4b9f4d96081e62c72d44 Mon Sep 17 00:00:00 2001 From: clawbot Date: Sun, 1 Mar 2026 07:51:35 -0800 Subject: [PATCH] Remove CLAUDE.md and CONVENTIONS.md Both files are superseded by REPO_POLICIES.md which is the authoritative standard from sneak/prompts. --- CLAUDE.md | 68 --- CONVENTIONS.md | 1225 ------------------------------------------------ 2 files changed, 1293 deletions(-) delete mode 100644 CLAUDE.md delete mode 100644 CONVENTIONS.md diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 9c9ebe5..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,68 +0,0 @@ -# Repository Rules - -Last Updated 2026-01-08 - -These rules MUST be followed at all times, it is very important. - -* Never use `git add -A` - add specific changes to a deliberate commit. A - commit should contain one change. After each change, make a commit with a - good one-line summary. - -* NEVER modify the linter config without asking first. - -* NEVER modify tests to exclude special cases or otherwise get them to pass - without asking first. In almost all cases, the code should be changed, - NOT the tests. If you think the test needs to be changed, make your case - for that and ask for permission to proceed, then stop. You need explicit - user approval to modify existing tests. (You do not need user approval - for writing NEW tests.) - -* When linting, assume the linter config is CORRECT, and that each item - output by the linter is something that legitimately needs fixing in the - code. - -* When running tests, use `make test`. - -* Before commits, run `make check`. This runs `make lint` and `make test` - and `make check-fmt`. Any issues discovered MUST be resolved before - committing unless explicitly told otherwise. - -* When fixing a bug, write a failing test for the bug FIRST. Add - appropriate logging to the test to ensure it is written correctly. Commit - that. Then go about fixing the bug until the test passes (without - modifying the test further). Then commit that. - -* When adding a new feature, do the same - implement a test first (TDD). It - doesn't have to be super complex. Commit the test, then commit the - feature. - -* When adding a new feature, use a feature branch. When the feature is - completely finished and the code is up to standards (passes `make check`) - then and only then can the feature branch be merged into `main` and the - branch deleted. - -* Write godoc documentation comments for all exported types and functions as - you go along. - -* ALWAYS be consistent in naming. If you name something one thing in one - place, name it the EXACT SAME THING in another place. - -* Be descriptive and specific in naming. `wl` is bad; - `SourceHostWhitelist` is good. `ConnsPerHost` is bad; - `MaxConnectionsPerHost` is good. - -* This is not prototype or teaching code - this is designed for production. - Any security issues (such as denial of service) or other web - vulnerabilities are P1 bugs and must be added to TODO.md at the top. - -* As this is production code, no stubbing of implementations unless - specifically instructed. We need working implementations. - -* Avoid vendoring deps unless specifically instructed to. NEVER commit - the vendor directory, NEVER commit compiled binaries. If these - directories or files exist, add them to .gitignore (and commit the - .gitignore) if they are not already in there. Keep the entire git - repository (with history) small - under 20MiB, unless you specifically - must commit larger files (e.g. test fixture example media files). Only - OUR source code and immediately supporting files (such as test examples) - goes into the repo/history. diff --git a/CONVENTIONS.md b/CONVENTIONS.md deleted file mode 100644 index 6f7c217..0000000 --- a/CONVENTIONS.md +++ /dev/null @@ -1,1225 +0,0 @@ -# Go HTTP Server Conventions - -This document defines the architectural patterns, design decisions, and conventions for building Go HTTP servers. All new projects must follow these standards. - -## Table of Contents - -1. [Required Libraries](#1-required-libraries) -2. [Project Structure](#2-project-structure) -3. [Dependency Injection (Uber fx)](#3-dependency-injection-uber-fx) -4. [Server Architecture](#4-server-architecture) -5. [Routing (go-chi)](#5-routing-go-chi) -6. [Handler Conventions](#6-handler-conventions) -7. [Middleware Conventions](#7-middleware-conventions) -8. [Configuration (Viper)](#8-configuration-viper) -9. [Logging (slog)](#9-logging-slog) -10. [Database Wrapper](#10-database-wrapper) -11. [Globals Package](#11-globals-package) -12. [Static Assets & Templates](#12-static-assets--templates) -13. [Health Check](#13-health-check) -14. [External Integrations](#14-external-integrations) - ---- - -## 1. Required Libraries - -These libraries are **mandatory** for all new projects: - -| Purpose | Library | Import Path | -|---------|---------|-------------| -| Dependency Injection | Uber fx | `go.uber.org/fx` | -| HTTP Router | go-chi | `github.com/go-chi/chi` | -| Logging | slog (stdlib) | `log/slog` | -| Configuration | Viper | `github.com/spf13/viper` | -| Environment Loading | godotenv | `github.com/joho/godotenv/autoload` | -| CORS | go-chi/cors | `github.com/go-chi/cors` | -| Error Reporting | Sentry | `github.com/getsentry/sentry-go` | -| Metrics | Prometheus | `github.com/prometheus/client_golang` | -| Metrics Middleware | go-http-metrics | `github.com/slok/go-http-metrics` | -| Basic Auth | basicauth-go | `github.com/99designs/basicauth-go` | - ---- - -## 2. Project Structure - -``` -project-root/ -├── cmd/ -│ └── {appname}/ -│ └── main.go # Entry point -├── internal/ -│ ├── config/ -│ │ └── config.go # Configuration loading -│ ├── database/ -│ │ └── database.go # Database wrapper -│ ├── globals/ -│ │ └── globals.go # Build-time variables -│ ├── handlers/ -│ │ ├── handlers.go # Base handler struct and helpers -│ │ ├── index.go # Individual handlers... -│ │ ├── healthcheck.go -│ │ └── {feature}.go -│ ├── healthcheck/ -│ │ └── healthcheck.go # Health check service -│ ├── logger/ -│ │ └── logger.go # Logger setup -│ ├── middleware/ -│ │ └── middleware.go # All middleware definitions -│ └── server/ -│ ├── server.go # Server struct and lifecycle -│ ├── http.go # HTTP server setup -│ └── routes.go # Route definitions -├── static/ -│ ├── static.go # Embed directive -│ ├── css/ -│ └── js/ -├── templates/ -│ ├── templates.go # Embed and parse -│ └── *.html -├── go.mod -├── go.sum -├── Makefile -└── Dockerfile -``` - -### Key Principles - -- **`cmd/{appname}/`**: Only the entry point. Minimal logic, just bootstrapping. -- **`internal/`**: All application packages. Not importable by external projects. -- **One package per concern**: config, database, handlers, middleware, etc. -- **Flat handler files**: One file per handler or logical group of handlers. - ---- - -## 3. Dependency Injection (Uber fx) - -### Entry Point Pattern - -```go -// cmd/httpd/main.go -package main - -import ( - "yourproject/internal/config" - "yourproject/internal/database" - "yourproject/internal/globals" - "yourproject/internal/handlers" - "yourproject/internal/healthcheck" - "yourproject/internal/logger" - "yourproject/internal/middleware" - "yourproject/internal/server" - "go.uber.org/fx" -) - -var ( - Appname string = "CHANGEME" - Version string - Buildarch string -) - -func main() { - globals.Appname = Appname - globals.Version = Version - globals.Buildarch = Buildarch - - fx.New( - fx.Provide( - config.New, - database.New, - globals.New, - handlers.New, - logger.New, - server.New, - middleware.New, - healthcheck.New, - ), - fx.Invoke(func(*server.Server) {}), - ).Run() -} -``` - -### Params Struct Pattern - -Every component that receives dependencies uses a params struct with `fx.In`: - -```go -type HandlersParams struct { - fx.In - Logger *logger.Logger - Globals *globals.Globals - Database *database.Database - Healthcheck *healthcheck.Healthcheck -} - -type Handlers struct { - params *HandlersParams - log *slog.Logger - hc *healthcheck.Healthcheck -} -``` - -### Factory Function Pattern - -All components expose a `New` function with this signature: - -```go -func New(lc fx.Lifecycle, params SomeParams) (*Something, error) { - s := new(Something) - s.params = ¶ms - s.log = params.Logger.Get() - - lc.Append(fx.Hook{ - OnStart: func(ctx context.Context) error { - // Initialize resources - return nil - }, - OnStop: func(ctx context.Context) error { - // Cleanup resources - return nil - }, - }) - return s, nil -} -``` - -### Dependency Order - -Providers are resolved automatically by fx, but conceptually follow this order: - -1. `globals.New` - Build-time variables (no dependencies) -2. `logger.New` - Logger (depends on Globals) -3. `config.New` - Configuration (depends on Globals, Logger) -4. `database.New` - Database (depends on Logger, Config) -5. `healthcheck.New` - Health check (depends on Globals, Config, Logger, Database) -6. `middleware.New` - Middleware (depends on Logger, Globals, Config) -7. `handlers.New` - Handlers (depends on Logger, Globals, Database, Healthcheck) -8. `server.New` - Server (depends on all above) - ---- - -## 4. Server Architecture - -### Server Struct - -The Server struct is the central orchestrator: - -```go -// internal/server/server.go -type ServerParams struct { - fx.In - Logger *logger.Logger - Globals *globals.Globals - Config *config.Config - Middleware *middleware.Middleware - Handlers *handlers.Handlers -} - -type Server struct { - startupTime time.Time - port int - exitCode int - sentryEnabled bool - log *slog.Logger - ctx context.Context - cancelFunc context.CancelFunc - httpServer *http.Server - router *chi.Mux - params ServerParams - mw *middleware.Middleware - h *handlers.Handlers -} -``` - -### Server Factory - -```go -func New(lc fx.Lifecycle, params ServerParams) (*Server, error) { - s := new(Server) - s.params = params - s.mw = params.Middleware - s.h = params.Handlers - s.log = params.Logger.Get() - - lc.Append(fx.Hook{ - OnStart: func(ctx context.Context) error { - s.startupTime = time.Now() - go s.Run() - return nil - }, - OnStop: func(ctx context.Context) error { - // Server shutdown logic - return nil - }, - }) - return s, nil -} -``` - -### HTTP Server Setup - -```go -// internal/server/http.go -func (s *Server) serveUntilShutdown() { - listenAddr := fmt.Sprintf(":%d", s.params.Config.Port) - s.httpServer = &http.Server{ - Addr: listenAddr, - ReadTimeout: 10 * time.Second, - WriteTimeout: 10 * time.Second, - MaxHeaderBytes: 1 << 20, - Handler: s, - } - - s.SetupRoutes() - - s.log.Info("http begin listen", "listenaddr", listenAddr) - if err := s.httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed { - s.log.Error("listen error", "error", err) - if s.cancelFunc != nil { - s.cancelFunc() - } - } -} - -func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { - s.router.ServeHTTP(w, r) -} -``` - -### Signal Handling and Graceful Shutdown - -```go -func (s *Server) serve() int { - s.ctx, s.cancelFunc = context.WithCancel(context.Background()) - - // Signal watcher - go func() { - c := make(chan os.Signal, 1) - signal.Ignore(syscall.SIGPIPE) - signal.Notify(c, os.Interrupt, syscall.SIGTERM) - sig := <-c - s.log.Info("signal received", "signal", sig) - if s.cancelFunc != nil { - s.cancelFunc() - } - }() - - go s.serveUntilShutdown() - - for range s.ctx.Done() { - } - s.cleanShutdown() - return s.exitCode -} - -func (s *Server) cleanShutdown() { - s.exitCode = 0 - ctxShutdown, shutdownCancel := context.WithTimeout(context.Background(), 5*time.Second) - if err := s.httpServer.Shutdown(ctxShutdown); err != nil { - s.log.Error("server clean shutdown failed", "error", err) - } - if shutdownCancel != nil { - shutdownCancel() - } - s.cleanupForExit() - if s.sentryEnabled { - sentry.Flush(2 * time.Second) - } -} -``` - ---- - -## 5. Routing (go-chi) - -### Route Setup Pattern - -```go -// internal/server/routes.go -func (s *Server) SetupRoutes() { - s.router = chi.NewRouter() - - // Global middleware (applied to all routes) - s.router.Use(middleware.Recoverer) - s.router.Use(middleware.RequestID) - s.router.Use(s.mw.Logging()) - - // Conditional middleware - if viper.GetString("METRICS_USERNAME") != "" { - s.router.Use(s.mw.Metrics()) - } - - s.router.Use(s.mw.CORS()) - s.router.Use(middleware.Timeout(60 * time.Second)) - - if s.sentryEnabled { - sentryHandler := sentryhttp.New(sentryhttp.Options{ - Repanic: true, - }) - s.router.Use(sentryHandler.Handle) - } - - // Routes - s.router.Get("/", s.h.HandleIndex()) - - // Static files - s.router.Mount("/s", http.StripPrefix("/s", http.FileServer(http.FS(static.Static)))) - - // API versioning - s.router.Route("/api/v1", func(r chi.Router) { - r.Get("/now", s.h.HandleNow()) - }) - - // Routes with specific middleware - auth := s.mw.Auth() - s.router.Get("/login", auth(s.h.HandleLoginGET()).ServeHTTP) - - // Health check (standard path) - s.router.Get("/.well-known/healthcheck.json", s.h.HandleHealthCheck()) - - // Protected route groups - if viper.GetString("METRICS_USERNAME") != "" { - s.router.Group(func(r chi.Router) { - r.Use(s.mw.MetricsAuth()) - r.Get("/metrics", http.HandlerFunc(promhttp.Handler().ServeHTTP)) - }) - } -} -``` - -### Middleware Ordering (Critical) - -1. `middleware.Recoverer` - Panic recovery (must be first) -2. `middleware.RequestID` - Generate request IDs -3. `s.mw.Logging()` - Request logging -4. `s.mw.Metrics()` - Prometheus metrics (if enabled) -5. `s.mw.CORS()` - CORS headers -6. `middleware.Timeout(60s)` - Request timeout -7. `sentryhttp.Handler` - Sentry error reporting (if enabled) - -### API Versioning - -Use route groups for API versioning: - -```go -s.router.Route("/api/v1", func(r chi.Router) { - r.Get("/resource", s.h.HandleResource()) -}) -``` - -### Static File Serving - -Static files are served at `/s/` prefix: - -```go -s.router.Mount("/s", http.StripPrefix("/s", http.FileServer(http.FS(static.Static)))) -``` - ---- - -## 6. Handler Conventions - -### Handler Base Struct - -```go -// internal/handlers/handlers.go -type HandlersParams struct { - fx.In - Logger *logger.Logger - Globals *globals.Globals - Database *database.Database - Healthcheck *healthcheck.Healthcheck -} - -type Handlers struct { - params *HandlersParams - log *slog.Logger - hc *healthcheck.Healthcheck -} - -func New(lc fx.Lifecycle, params HandlersParams) (*Handlers, error) { - s := new(Handlers) - s.params = ¶ms - s.log = params.Logger.Get() - s.hc = params.Healthcheck - lc.Append(fx.Hook{ - OnStart: func(ctx context.Context) error { - // Compile templates or other initialization - return nil - }, - }) - return s, nil -} -``` - -### Closure-Based Handler Pattern - -All handlers return `http.HandlerFunc` using the closure pattern. This allows initialization logic to run once when the handler is created: - -```go -// internal/handlers/index.go -func (s *Handlers) HandleIndex() http.HandlerFunc { - // Initialization runs once - t := templates.GetParsed() - - // Handler runs per-request - return func(w http.ResponseWriter, r *http.Request) { - err := t.ExecuteTemplate(w, "index.html", nil) - if err != nil { - s.log.Error("template execution failed", "error", err) - http.Error(w, http.StatusText(500), 500) - } - } -} -``` - -### JSON Handler Pattern - -```go -// internal/handlers/now.go -func (s *Handlers) HandleNow() http.HandlerFunc { - // Response struct defined in closure scope - type response struct { - Now time.Time `json:"now"` - } - return func(w http.ResponseWriter, r *http.Request) { - s.respondJSON(w, r, &response{Now: time.Now()}, 200) - } -} -``` - -### Response Helpers - -```go -// internal/handlers/handlers.go -func (s *Handlers) respondJSON(w http.ResponseWriter, r *http.Request, data interface{}, status int) { - w.WriteHeader(status) - w.Header().Set("Content-Type", "application/json") - if data != nil { - err := json.NewEncoder(w).Encode(data) - if err != nil { - s.log.Error("json encode error", "error", err) - } - } -} - -func (s *Handlers) decodeJSON(w http.ResponseWriter, r *http.Request, v interface{}) error { - return json.NewDecoder(r.Body).Decode(v) -} -``` - -### Handler Naming Convention - -- `HandleIndex()` - Main page -- `HandleLoginGET()` / `HandleLoginPOST()` - Form handlers with HTTP method suffix -- `HandleNow()` - API endpoints -- `HandleHealthCheck()` - System endpoints - ---- - -## 7. Middleware Conventions - -### Middleware Struct - -```go -// internal/middleware/middleware.go -type MiddlewareParams struct { - fx.In - Logger *logger.Logger - Globals *globals.Globals - Config *config.Config -} - -type Middleware struct { - log *slog.Logger - params *MiddlewareParams -} - -func New(lc fx.Lifecycle, params MiddlewareParams) (*Middleware, error) { - s := new(Middleware) - s.params = ¶ms - s.log = params.Logger.Get() - return s, nil -} -``` - -### Middleware Signature - -All custom middleware methods return `func(http.Handler) http.Handler`: - -```go -func (s *Middleware) Auth() func(http.Handler) http.Handler { - return func(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // Before request - s.log.Info("AUTH: before request") - - next.ServeHTTP(w, r) - - // After request (optional) - }) - } -} -``` - -### Logging Middleware with Status Capture - -```go -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 (s *Middleware) Logging() 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() - lrw := NewLoggingResponseWriter(w) - ctx := r.Context() - defer func() { - latency := time.Since(start) - s.log.InfoContext(ctx, "request", - "request_start", start, - "method", r.Method, - "url", r.URL.String(), - "useragent", r.UserAgent(), - "request_id", ctx.Value(middleware.RequestIDKey).(string), - "referer", r.Referer(), - "proto", r.Proto, - "remoteIP", ipFromHostPort(r.RemoteAddr), - "status", lrw.statusCode, - "latency_ms", latency.Milliseconds(), - ) - }() - next.ServeHTTP(lrw, r) - }) - } -} -``` - -### CORS Middleware - -```go -func (s *Middleware) CORS() func(http.Handler) http.Handler { - return cors.Handler(cors.Options{ - AllowedOrigins: []string{"*"}, - AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, - AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"}, - ExposedHeaders: []string{"Link"}, - AllowCredentials: false, - MaxAge: 300, - }) -} -``` - -### Metrics Middleware - -```go -func (s *Middleware) Metrics() func(http.Handler) http.Handler { - mdlw := ghmm.New(ghmm.Config{ - Recorder: metrics.NewRecorder(metrics.Config{}), - }) - return func(next http.Handler) http.Handler { - return std.Handler("", mdlw, next) - } -} - -func (s *Middleware) MetricsAuth() func(http.Handler) http.Handler { - return basicauth.New( - "metrics", - map[string][]string{ - viper.GetString("METRICS_USERNAME"): { - viper.GetString("METRICS_PASSWORD"), - }, - }, - ) -} -``` - ---- - -## 8. Configuration (Viper) - -### Config Struct - -```go -// internal/config/config.go -type ConfigParams struct { - fx.In - Globals *globals.Globals - Logger *logger.Logger -} - -type Config struct { - DBURL string - Debug bool - MaintenanceMode bool - MetricsPassword string - MetricsUsername string - Port int - SentryDSN string - params *ConfigParams - log *slog.Logger -} -``` - -### Configuration Loading - -```go -func New(lc fx.Lifecycle, params ConfigParams) (*Config, error) { - log := params.Logger.Get() - name := params.Globals.Appname - - // Config file settings - viper.SetConfigName(name) - viper.SetConfigType("yaml") - viper.AddConfigPath(fmt.Sprintf("/etc/%s", name)) - viper.AddConfigPath(fmt.Sprintf("$HOME/.config/%s", name)) - - // Environment variables override everything - viper.AutomaticEnv() - - // Defaults - viper.SetDefault("DEBUG", "false") - viper.SetDefault("MAINTENANCE_MODE", "false") - viper.SetDefault("PORT", "8080") - viper.SetDefault("DBURL", "") - viper.SetDefault("SENTRY_DSN", "") - viper.SetDefault("METRICS_USERNAME", "") - viper.SetDefault("METRICS_PASSWORD", "") - - // Read config file (optional) - if err := viper.ReadInConfig(); err != nil { - if _, ok := err.(viper.ConfigFileNotFoundError); ok { - // Config file not found is OK - } else { - log.Error("config file malformed", "error", err) - panic(err) - } - } - - // Build config struct - s := &Config{ - DBURL: viper.GetString("DBURL"), - Debug: viper.GetBool("debug"), - Port: viper.GetInt("PORT"), - SentryDSN: viper.GetString("SENTRY_DSN"), - MaintenanceMode: viper.GetBool("MAINTENANCE_MODE"), - MetricsUsername: viper.GetString("METRICS_USERNAME"), - MetricsPassword: viper.GetString("METRICS_PASSWORD"), - log: log, - params: ¶ms, - } - - // Enable debug logging if configured - if s.Debug { - params.Logger.EnableDebugLogging() - s.log = params.Logger.Get() - } - - return s, nil -} -``` - -### Configuration Precedence - -1. **Environment variables** (highest priority via `AutomaticEnv()`) -2. **`.env` file** (loaded via `godotenv/autoload` import) -3. **Config files**: `/etc/{appname}/{appname}.yaml`, `~/.config/{appname}/{appname}.yaml` -4. **Defaults** (lowest priority) - -### Environment Loading - -Import godotenv with autoload to automatically load `.env` files: - -```go -import ( - _ "github.com/joho/godotenv/autoload" -) -``` - ---- - -## 9. Logging (slog) - -### Logger Struct - -```go -// internal/logger/logger.go -type LoggerParams struct { - fx.In - Globals *globals.Globals -} - -type Logger struct { - log *slog.Logger - level *slog.LevelVar - params LoggerParams -} -``` - -### Logger Setup with TTY Detection - -```go -func New(lc fx.Lifecycle, params LoggerParams) (*Logger, error) { - l := new(Logger) - l.level = new(slog.LevelVar) - l.level.Set(slog.LevelInfo) - - // TTY detection for dev vs prod output - tty := false - if fileInfo, _ := os.Stdout.Stat(); (fileInfo.Mode() & os.ModeCharDevice) != 0 { - tty = true - } - - var handler slog.Handler - if tty { - // Text output for development - handler = slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ - Level: l.level, - AddSource: true, - }) - } else { - // JSON output for production - handler = slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ - Level: l.level, - AddSource: true, - }) - } - - l.log = slog.New(handler) - return l, nil -} -``` - -### Logger Methods - -```go -func (l *Logger) EnableDebugLogging() { - l.level.Set(slog.LevelDebug) - l.log.Debug("debug logging enabled", "debug", true) -} - -func (l *Logger) Get() *slog.Logger { - return l.log -} - -func (l *Logger) Identify() { - l.log.Info("starting", - "appname", l.params.Globals.Appname, - "version", l.params.Globals.Version, - "buildarch", l.params.Globals.Buildarch, - ) -} -``` - -### Logging Patterns - -```go -// Info with fields -s.log.Info("message", "key", "value") - -// Error with error object -s.log.Error("operation failed", "error", err) - -// With context -s.log.InfoContext(ctx, "processing request", "request_id", reqID) - -// Structured request logging -s.log.Info("request completed", - "request_start", start, - "method", r.Method, - "url", r.URL.String(), - "status", statusCode, - "latency_ms", latency.Milliseconds(), -) - -// Using slog.Group for nested attributes -s.log.Info("request", - slog.Group("http", - "method", r.Method, - "url", r.URL.String(), - ), - slog.Group("timing", - "start", start, - "latency_ms", latency.Milliseconds(), - ), -) -``` - ---- - -## 10. Database Wrapper - -### Database Struct - -```go -// internal/database/database.go -type DatabaseParams struct { - fx.In - Logger *logger.Logger - Config *config.Config -} - -type Database struct { - URL string - log *slog.Logger - params *DatabaseParams -} -``` - -### Database Factory with Lifecycle - -```go -func New(lc fx.Lifecycle, params DatabaseParams) (*Database, error) { - s := new(Database) - s.params = ¶ms - s.log = params.Logger.Get() - - s.log.Info("Database instantiated") - - lc.Append(fx.Hook{ - OnStart: func(ctx context.Context) error { - s.log.Info("Database OnStart Hook") - // Connect to database here - // Example: s.db, err = sql.Open("postgres", s.params.Config.DBURL) - return nil - }, - OnStop: func(ctx context.Context) error { - // Disconnect from database here - // Example: s.db.Close() - return nil - }, - }) - return s, nil -} -``` - -### Usage Pattern - -The Database struct is injected into handlers and other services: - -```go -type HandlersParams struct { - fx.In - Database *database.Database - // ... -} - -// Access in handler -func (s *Handlers) HandleSomething() http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - // Use s.params.Database - } -} -``` - ---- - -## 11. Globals Package - -### Package Variables and Struct - -```go -// internal/globals/globals.go -package globals - -import "go.uber.org/fx" - -// Package-level variables (set from main) -var ( - Appname string - Version string - Buildarch string -) - -// Struct for DI -type Globals struct { - Appname string - Version string - Buildarch string -} - -func New(lc fx.Lifecycle) (*Globals, error) { - n := &Globals{ - Appname: Appname, - Buildarch: Buildarch, - Version: Version, - } - return n, nil -} -``` - -### Setting Globals in Main - -```go -// cmd/httpd/main.go -var ( - Appname string = "CHANGEME" // Default, overridden by build - Version string // Set at build time - Buildarch string // Set at build time -) - -func main() { - globals.Appname = Appname - globals.Version = Version - globals.Buildarch = Buildarch - // ... -} -``` - -### Build-Time Variable Injection - -Use ldflags to inject version information at build time: - -```makefile -VERSION := $(shell git describe --tags --always) -BUILDARCH := $(shell go env GOARCH) - -build: - go build -ldflags "-X main.Version=$(VERSION) -X main.Buildarch=$(BUILDARCH)" ./cmd/httpd -``` - ---- - -## 12. Static Assets & Templates - -### Static File Embedding - -```go -// static/static.go -package static - -import "embed" - -//go:embed css js -var Static embed.FS -``` - -Directory structure: -``` -static/ -├── static.go -├── css/ -│ ├── bootstrap-4.5.3.min.css -│ └── style.css -└── js/ - ├── bootstrap-4.5.3.bundle.min.js - └── jquery-3.5.1.slim.min.js -``` - -### Template Embedding and Lazy Parsing - -```go -// templates/templates.go -package templates - -import ( - "embed" - "text/template" -) - -//go:embed *.html -var TemplatesRaw embed.FS -var TemplatesParsed *template.Template - -func GetParsed() *template.Template { - if TemplatesParsed == nil { - TemplatesParsed = template.Must(template.ParseFS(TemplatesRaw, "*")) - } - return TemplatesParsed -} -``` - -### Template Composition - -Templates use Go's template composition: - -```html - -{{ template "htmlheader.html" . }} -{{ template "navbar.html" . }} - -
- -
- -{{ template "pagefooter.html" . }} -{{ template "htmlfooter.html" . }} -``` - -### Static Asset References - -Reference static files with `/s/` prefix: - -```html - - - -``` - ---- - -## 13. Health Check - -### Health Check Service - -```go -// internal/healthcheck/healthcheck.go -type HealthcheckParams struct { - fx.In - Globals *globals.Globals - Config *config.Config - Logger *logger.Logger - Database *database.Database -} - -type Healthcheck struct { - StartupTime time.Time - log *slog.Logger - params *HealthcheckParams -} - -func New(lc fx.Lifecycle, params HealthcheckParams) (*Healthcheck, error) { - s := new(Healthcheck) - s.params = ¶ms - s.log = params.Logger.Get() - - lc.Append(fx.Hook{ - OnStart: func(ctx context.Context) error { - s.StartupTime = time.Now() - return nil - }, - OnStop: func(ctx context.Context) error { - return nil - }, - }) - return s, nil -} -``` - -### Health Check Response - -```go -type HealthcheckResponse struct { - Status string `json:"status"` - Now string `json:"now"` - UptimeSeconds int64 `json:"uptime_seconds"` - UptimeHuman string `json:"uptime_human"` - Version string `json:"version"` - Appname string `json:"appname"` - Maintenance bool `json:"maintenance_mode"` -} - -func (s *Healthcheck) uptime() time.Duration { - return time.Since(s.StartupTime) -} - -func (s *Healthcheck) Healthcheck() *HealthcheckResponse { - resp := &HealthcheckResponse{ - Status: "ok", - Now: time.Now().UTC().Format(time.RFC3339Nano), - UptimeSeconds: int64(s.uptime().Seconds()), - UptimeHuman: s.uptime().String(), - Appname: s.params.Globals.Appname, - Version: s.params.Globals.Version, - } - return resp -} -``` - -### Standard Endpoint - -Health check is served at the standard `.well-known` path: - -```go -s.router.Get("/.well-known/healthcheck.json", s.h.HandleHealthCheck()) -``` - ---- - -## 14. External Integrations - -### Sentry Error Reporting - -Sentry is conditionally enabled based on `SENTRY_DSN` environment variable: - -```go -func (s *Server) enableSentry() { - s.sentryEnabled = false - - if s.params.Config.SentryDSN == "" { - return - } - - err := sentry.Init(sentry.ClientOptions{ - Dsn: s.params.Config.SentryDSN, - Release: fmt.Sprintf("%s-%s", s.params.Globals.Appname, s.params.Globals.Version), - }) - if err != nil { - s.log.Error("sentry init failure", "error", err) - os.Exit(1) - return - } - s.log.Info("sentry error reporting activated") - s.sentryEnabled = true -} -``` - -Sentry middleware with repanic (bubbles panics to chi's Recoverer): - -```go -if s.sentryEnabled { - sentryHandler := sentryhttp.New(sentryhttp.Options{ - Repanic: true, - }) - s.router.Use(sentryHandler.Handle) -} -``` - -Flush Sentry on shutdown: - -```go -if s.sentryEnabled { - sentry.Flush(2 * time.Second) -} -``` - -### Prometheus Metrics - -Metrics are conditionally enabled and protected by basic auth: - -```go -// Only enable if credentials are configured -if viper.GetString("METRICS_USERNAME") != "" { - s.router.Use(s.mw.Metrics()) -} - -// Protected /metrics endpoint -if viper.GetString("METRICS_USERNAME") != "" { - s.router.Group(func(r chi.Router) { - r.Use(s.mw.MetricsAuth()) - r.Get("/metrics", http.HandlerFunc(promhttp.Handler().ServeHTTP)) - }) -} -``` - -### Environment Variables Summary - -| Variable | Description | Default | -|----------|-------------|---------| -| `PORT` | HTTP listen port | 8080 | -| `DEBUG` | Enable debug logging | false | -| `DBURL` | Database connection URL | "" | -| `SENTRY_DSN` | Sentry DSN for error reporting | "" | -| `MAINTENANCE_MODE` | Enable maintenance mode | false | -| `METRICS_USERNAME` | Basic auth username for /metrics | "" | -| `METRICS_PASSWORD` | Basic auth password for /metrics | "" |