30 KiB
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
- Required Libraries
- Project Structure
- Dependency Injection (Uber fx)
- Server Architecture
- Routing (go-chi)
- Handler Conventions
- Middleware Conventions
- Configuration (Viper)
- Logging (slog)
- Database Wrapper
- Globals Package
- Static Assets & Templates
- Health Check
- 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
// 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
)
func main() {
globals.Appname = Appname
globals.Version = Version
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:
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:
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:
globals.New- Build-time variables (no dependencies)logger.New- Logger (depends on Globals)config.New- Configuration (depends on Globals, Logger)database.New- Database (depends on Logger, Config)healthcheck.New- Health check (depends on Globals, Config, Logger, Database)middleware.New- Middleware (depends on Logger, Globals, Config)handlers.New- Handlers (depends on Logger, Globals, Database, Healthcheck)server.New- Server (depends on all above)
4. Server Architecture
Server Struct
The Server struct is the central orchestrator:
// 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
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
// 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
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
// 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)
middleware.Recoverer- Panic recovery (must be first)middleware.RequestID- Generate request IDss.mw.Logging()- Request loggings.mw.Metrics()- Prometheus metrics (if enabled)s.mw.CORS()- CORS headersmiddleware.Timeout(60s)- Request timeoutsentryhttp.Handler- Sentry error reporting (if enabled)
API Versioning
Use route groups for API versioning:
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:
s.router.Mount("/s", http.StripPrefix("/s", http.FileServer(http.FS(static.Static))))
6. Handler Conventions
Handler Base Struct
// 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
For JSON route handlers, both the request and the response structures are
defined in the scope of the method that returns the HandlerFunc. They can
be called simply Request and Response or slightly more descriptive
names.
All handlers return http.HandlerFunc using the closure pattern. This allows initialization logic to run once when the handler is created:
// 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
// internal/handlers/now.go
func (s *Handlers) HandleNow() http.HandlerFunc {
type request struct {
// request format
}
// 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
// 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 pageHandleLoginGET()/HandleLoginPOST()- Form handlers with HTTP method suffixHandleNow()- API endpointsHandleHealthCheck()- System endpoints
7. Middleware Conventions
Middleware Struct
// 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:
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
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
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
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
// 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
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
- Environment variables (highest priority via
AutomaticEnv()) .envfile (loaded viagodotenv/autoloadimport)- Config files:
/etc/{appname}/{appname}.yaml,~/.config/{appname}/{appname}.yaml - Defaults (lowest priority)
Environment Loading
Import godotenv with autoload to automatically load .env files:
import (
_ "github.com/joho/godotenv/autoload"
)
9. Logging (slog)
Logger Struct
// 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
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
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,
)
}
Logging Patterns
// 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
// 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
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:
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
// internal/globals/globals.go
package globals
import "go.uber.org/fx"
// Package-level variables (set from main)
var (
Appname string
Version string
)
// Struct for DI
type Globals struct {
Appname string
Version string
}
func New(lc fx.Lifecycle) (*Globals, error) {
n := &Globals{
Appname: Appname,
Version: Version,
}
return n, nil
}
Setting Globals in Main
// cmd/httpd/main.go
var (
Appname string = "CHANGEME" // Default, overridden by build
Version string // Set at build time
)
func main() {
globals.Appname = Appname
globals.Version = Version
// ...
}
Build-Time Variable Injection
Use ldflags to inject version information at build time:
VERSION := $(shell git describe --tags --always)
build:
go build -ldflags "-X main.Version=$(VERSION)" ./cmd/httpd
12. Static Assets & Templates
Static File Embedding
// 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
// 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:
<!-- index.html -->
{{ template "htmlheader.html" . }} {{ template "navbar.html" . }}
<main>
<!-- Page content -->
</main>
{{ template "pagefooter.html" . }} {{ template "htmlfooter.html" . }}
Static Asset References
Reference static files with /s/ prefix:
<link rel="stylesheet" href="/s/css/bootstrap-4.5.3.min.css" />
<link rel="stylesheet" href="/s/css/style.css" />
<script src="/s/js/jquery-3.5.1.slim.min.js"></script>
13. Health Check
Health Check Service
// 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
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:
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:
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):
if s.sentryEnabled {
sentryHandler := sentryhttp.New(sentryhttp.Options{
Repanic: true,
})
s.router.Use(sentryHandler.Handle)
}
Flush Sentry on shutdown:
if s.sentryEnabled {
sentry.Flush(2 * time.Second)
}
Prometheus Metrics
Metrics are conditionally enabled and protected by basic auth:
// 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 | "" |