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
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:
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
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 {
// 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,
"buildarch", l.params.Globals.Buildarch,
)
}
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
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
// 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:
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
// 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 | "" |