Go Monitoring & Observability Best Practices¶
Objective: Master senior-level Go monitoring and observability patterns for production systems. When you need to build comprehensive monitoring solutions, when you want to implement distributed tracing, when you need enterprise-grade observability patternsโthese best practices become your weapon of choice.
Core Principles¶
- Three Pillars: Metrics, Logs, and Traces
- Structured Logging: Use structured, machine-readable logs
- Context Propagation: Maintain context across service boundaries
- Performance Monitoring: Track application performance and health
- Alerting: Implement intelligent alerting based on meaningful metrics
Structured Logging¶
Logging Framework¶
// internal/logging/logger.go
package logging
import (
"context"
"encoding/json"
"fmt"
"os"
"runtime"
"time"
)
// LogLevel represents log levels
type LogLevel int
const (
DebugLevel LogLevel = iota
InfoLevel
WarnLevel
ErrorLevel
FatalLevel
)
// String returns the string representation of the log level
func (ll LogLevel) String() string {
switch ll {
case DebugLevel:
return "DEBUG"
case InfoLevel:
return "INFO"
case WarnLevel:
return "WARN"
case ErrorLevel:
return "ERROR"
case FatalLevel:
return "FATAL"
default:
return "UNKNOWN"
}
}
// LogEntry represents a log entry
type LogEntry struct {
Timestamp time.Time `json:"timestamp"`
Level LogLevel `json:"level"`
Message string `json:"message"`
Fields map[string]interface{} `json:"fields,omitempty"`
Caller string `json:"caller,omitempty"`
TraceID string `json:"trace_id,omitempty"`
SpanID string `json:"span_id,omitempty"`
}
// Logger represents a structured logger
type Logger struct {
level LogLevel
fields map[string]interface{}
output *os.File
}
// NewLogger creates a new logger
func NewLogger(level LogLevel) *Logger {
return &Logger{
level: level,
fields: make(map[string]interface{}),
output: os.Stdout,
}
}
// WithFields adds fields to the logger
func (l *Logger) WithFields(fields map[string]interface{}) *Logger {
newFields := make(map[string]interface{})
for k, v := range l.fields {
newFields[k] = v
}
for k, v := range fields {
newFields[k] = v
}
return &Logger{
level: l.level,
fields: newFields,
output: l.output,
}
}
// WithField adds a single field to the logger
func (l *Logger) WithField(key string, value interface{}) *Logger {
return l.WithFields(map[string]interface{}{key: value})
}
// WithContext adds context fields to the logger
func (l *Logger) WithContext(ctx context.Context) *Logger {
fields := make(map[string]interface{})
if traceID := ctx.Value("trace_id"); traceID != nil {
fields["trace_id"] = traceID
}
if spanID := ctx.Value("span_id"); spanID != nil {
fields["span_id"] = spanID
}
if userID := ctx.Value("user_id"); userID != nil {
fields["user_id"] = userID
}
return l.WithFields(fields)
}
// log logs a message with the specified level
func (l *Logger) log(level LogLevel, msg string, fields map[string]interface{}) {
if level < l.level {
return
}
entry := LogEntry{
Timestamp: time.Now().UTC(),
Level: level,
Message: msg,
Fields: l.mergeFields(fields),
}
// Add caller information
if _, file, line, ok := runtime.Caller(3); ok {
entry.Caller = fmt.Sprintf("%s:%d", file, line)
}
// Add context fields
if traceID, ok := entry.Fields["trace_id"].(string); ok {
entry.TraceID = traceID
}
if spanID, ok := entry.Fields["span_id"].(string); ok {
entry.SpanID = spanID
}
// Write to output
json.NewEncoder(l.output).Encode(entry)
}
// mergeFields merges logger fields with entry fields
func (l *Logger) mergeFields(fields map[string]interface{}) map[string]interface{} {
merged := make(map[string]interface{})
for k, v := range l.fields {
merged[k] = v
}
for k, v := range fields {
merged[k] = v
}
return merged
}
// Debug logs a debug message
func (l *Logger) Debug(msg string, fields ...map[string]interface{}) {
l.log(DebugLevel, msg, l.mergeFieldsList(fields))
}
// Info logs an info message
func (l *Logger) Info(msg string, fields ...map[string]interface{}) {
l.log(InfoLevel, msg, l.mergeFieldsList(fields))
}
// Warn logs a warning message
func (l *Logger) Warn(msg string, fields ...map[string]interface{}) {
l.log(WarnLevel, msg, l.mergeFieldsList(fields))
}
// Error logs an error message
func (l *Logger) Error(msg string, fields ...map[string]interface{}) {
l.log(ErrorLevel, msg, l.mergeFieldsList(fields))
}
// Fatal logs a fatal message and exits
func (l *Logger) Fatal(msg string, fields ...map[string]interface{}) {
l.log(FatalLevel, msg, l.mergeFieldsList(fields))
os.Exit(1)
}
// mergeFieldsList merges a list of field maps
func (l *Logger) mergeFieldsList(fields []map[string]interface{}) map[string]interface{} {
merged := make(map[string]interface{})
for _, fieldMap := range fields {
for k, v := range fieldMap {
merged[k] = v
}
}
return merged
}
Context-Aware Logging¶
// internal/logging/context_logger.go
package logging
import (
"context"
"fmt"
"time"
)
// ContextLogger provides context-aware logging
type ContextLogger struct {
logger *Logger
ctx context.Context
}
// NewContextLogger creates a new context logger
func NewContextLogger(logger *Logger, ctx context.Context) *ContextLogger {
return &ContextLogger{
logger: logger.WithContext(ctx),
ctx: ctx,
}
}
// WithFields adds fields to the context logger
func (cl *ContextLogger) WithFields(fields map[string]interface{}) *ContextLogger {
return &ContextLogger{
logger: cl.logger.WithFields(fields),
ctx: cl.ctx,
}
}
// WithField adds a single field to the context logger
func (cl *ContextLogger) WithField(key string, value interface{}) *ContextLogger {
return cl.WithFields(map[string]interface{}{key: value})
}
// Debug logs a debug message
func (cl *ContextLogger) Debug(msg string, fields ...map[string]interface{}) {
cl.logger.Debug(msg, fields...)
}
// Info logs an info message
func (cl *ContextLogger) Info(msg string, fields ...map[string]interface{}) {
cl.logger.Info(msg, fields...)
}
// Warn logs a warning message
func (cl *ContextLogger) Warn(msg string, fields ...map[string]interface{}) {
cl.logger.Warn(msg, fields...)
}
// Error logs an error message
func (cl *ContextLogger) Error(msg string, fields ...map[string]interface{}) {
cl.logger.Error(msg, fields...)
}
// Fatal logs a fatal message and exits
func (cl *ContextLogger) Fatal(msg string, fields ...map[string]interface{}) {
cl.logger.Fatal(msg, fields...)
}
// LogRequest logs an HTTP request
func (cl *ContextLogger) LogRequest(method, path string, statusCode int, duration time.Duration) {
cl.Info("HTTP request", map[string]interface{}{
"method": method,
"path": path,
"status_code": statusCode,
"duration_ms": duration.Milliseconds(),
})
}
// LogError logs an error with context
func (cl *ContextLogger) LogError(err error, msg string, fields ...map[string]interface{}) {
errorFields := map[string]interface{}{
"error": err.Error(),
}
for _, fieldMap := range fields {
for k, v := range fieldMap {
errorFields[k] = v
}
}
cl.Error(msg, errorFields)
}
Metrics Collection¶
Prometheus Metrics¶
// internal/metrics/prometheus.go
package metrics
import (
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
)
// Metrics represents application metrics
type Metrics struct {
// HTTP metrics
HTTPRequestsTotal *prometheus.CounterVec
HTTPRequestDuration *prometheus.HistogramVec
HTTPRequestsInFlight prometheus.Gauge
// Business metrics
UsersTotal prometheus.Gauge
OrdersTotal prometheus.Counter
OrderValue prometheus.Histogram
// System metrics
GoRoutines prometheus.Gauge
MemoryUsage prometheus.Gauge
CPUUsage prometheus.Gauge
}
// NewMetrics creates new metrics
func NewMetrics() *Metrics {
return &Metrics{
HTTPRequestsTotal: promauto.NewCounterVec(
prometheus.CounterOpts{
Name: "http_requests_total",
Help: "Total number of HTTP requests",
},
[]string{"method", "path", "status_code"},
),
HTTPRequestDuration: promauto.NewHistogramVec(
prometheus.HistogramOpts{
Name: "http_request_duration_seconds",
Help: "HTTP request duration in seconds",
Buckets: prometheus.DefBuckets,
},
[]string{"method", "path"},
),
HTTPRequestsInFlight: promauto.NewGauge(
prometheus.GaugeOpts{
Name: "http_requests_in_flight",
Help: "Number of HTTP requests currently being processed",
},
),
UsersTotal: promauto.NewGauge(
prometheus.GaugeOpts{
Name: "users_total",
Help: "Total number of users",
},
),
OrdersTotal: promauto.NewCounter(
prometheus.CounterOpts{
Name: "orders_total",
Help: "Total number of orders",
},
),
OrderValue: promauto.NewHistogram(
prometheus.HistogramOpts{
Name: "order_value",
Help: "Order value distribution",
Buckets: prometheus.ExponentialBuckets(10, 2, 10),
},
),
GoRoutines: promauto.NewGauge(
prometheus.GaugeOpts{
Name: "go_goroutines",
Help: "Number of goroutines",
},
),
MemoryUsage: promauto.NewGauge(
prometheus.GaugeOpts{
Name: "memory_usage_bytes",
Help: "Memory usage in bytes",
},
),
CPUUsage: promauto.NewGauge(
prometheus.GaugeOpts{
Name: "cpu_usage_percent",
Help: "CPU usage percentage",
},
),
}
}
// RecordHTTPRequest records an HTTP request
func (m *Metrics) RecordHTTPRequest(method, path string, statusCode int, duration float64) {
m.HTTPRequestsTotal.WithLabelValues(method, path, fmt.Sprintf("%d", statusCode)).Inc()
m.HTTPRequestDuration.WithLabelValues(method, path).Observe(duration)
}
// RecordOrder records an order
func (m *Metrics) RecordOrder(value float64) {
m.OrdersTotal.Inc()
m.OrderValue.Observe(value)
}
// UpdateSystemMetrics updates system metrics
func (m *Metrics) UpdateSystemMetrics() {
m.GoRoutines.Set(float64(runtime.NumGoroutine()))
var m runtime.MemStats
runtime.ReadMemStats(&m)
m.MemoryUsage.Set(float64(m.Alloc))
}
Custom Metrics¶
// internal/metrics/custom_metrics.go
package metrics
import (
"sync"
"time"
)
// CustomMetrics represents custom application metrics
type CustomMetrics struct {
mutex sync.RWMutex
// Business metrics
activeUsers int64
totalSessions int64
averageSession time.Duration
// Performance metrics
cacheHits int64
cacheMisses int64
databaseQueries int64
databaseErrors int64
// Error metrics
errorCounts map[string]int64
lastErrorTime time.Time
}
// NewCustomMetrics creates new custom metrics
func NewCustomMetrics() *CustomMetrics {
return &CustomMetrics{
errorCounts: make(map[string]int64),
}
}
// IncrementActiveUsers increments active users
func (cm *CustomMetrics) IncrementActiveUsers() {
cm.mutex.Lock()
defer cm.mutex.Unlock()
cm.activeUsers++
}
// DecrementActiveUsers decrements active users
func (cm *CustomMetrics) DecrementActiveUsers() {
cm.mutex.Lock()
defer cm.mutex.Unlock()
if cm.activeUsers > 0 {
cm.activeUsers--
}
}
// RecordSession records a session
func (cm *CustomMetrics) RecordSession(duration time.Duration) {
cm.mutex.Lock()
defer cm.mutex.Unlock()
cm.totalSessions++
cm.averageSession = time.Duration(
(int64(cm.averageSession)*int64(cm.totalSessions-1) + int64(duration)) / int64(cm.totalSessions),
)
}
// RecordCacheHit records a cache hit
func (cm *CustomMetrics) RecordCacheHit() {
cm.mutex.Lock()
defer cm.mutex.Unlock()
cm.cacheHits++
}
// RecordCacheMiss records a cache miss
func (cm *CustomMetrics) RecordCacheMiss() {
cm.mutex.Lock()
defer cm.mutex.Unlock()
cm.cacheMisses++
}
// RecordDatabaseQuery records a database query
func (cm *CustomMetrics) RecordDatabaseQuery() {
cm.mutex.Lock()
defer cm.mutex.Unlock()
cm.databaseQueries++
}
// RecordDatabaseError records a database error
func (cm *CustomMetrics) RecordDatabaseError() {
cm.mutex.Lock()
defer cm.mutex.Unlock()
cm.databaseErrors++
}
// RecordError records an error
func (cm *CustomMetrics) RecordError(errorType string) {
cm.mutex.Lock()
defer cm.mutex.Unlock()
cm.errorCounts[errorType]++
cm.lastErrorTime = time.Now()
}
// GetMetrics returns current metrics
func (cm *CustomMetrics) GetMetrics() map[string]interface{} {
cm.mutex.RLock()
defer cm.mutex.RUnlock()
cacheHitRate := float64(0)
if cm.cacheHits+cm.cacheMisses > 0 {
cacheHitRate = float64(cm.cacheHits) / float64(cm.cacheHits+cm.cacheMisses)
}
databaseErrorRate := float64(0)
if cm.databaseQueries > 0 {
databaseErrorRate = float64(cm.databaseErrors) / float64(cm.databaseQueries)
}
return map[string]interface{}{
"active_users": cm.activeUsers,
"total_sessions": cm.totalSessions,
"average_session_ms": cm.averageSession.Milliseconds(),
"cache_hits": cm.cacheHits,
"cache_misses": cm.cacheMisses,
"cache_hit_rate": cacheHitRate,
"database_queries": cm.databaseQueries,
"database_errors": cm.databaseErrors,
"database_error_rate": databaseErrorRate,
"error_counts": cm.errorCounts,
"last_error_time": cm.lastErrorTime,
}
}
Distributed Tracing¶
OpenTelemetry Integration¶
// internal/tracing/opentelemetry.go
package tracing
import (
"context"
"fmt"
"time"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/exporters/jaeger"
"go.opentelemetry.io/otel/propagation"
"go.opentelemetry.io/otel/sdk/resource"
"go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.4.0"
"go.opentelemetry.io/otel/trace"
)
// Tracer represents a distributed tracer
type Tracer struct {
tracer trace.Tracer
}
// NewTracer creates a new tracer
func NewTracer(serviceName, serviceVersion string) (*Tracer, error) {
// Create Jaeger exporter
exp, err := jaeger.New(jaeger.WithCollectorEndpoint())
if err != nil {
return nil, fmt.Errorf("failed to create Jaeger exporter: %w", err)
}
// Create resource
res, err := resource.New(context.Background(),
resource.WithAttributes(
semconv.ServiceNameKey.String(serviceName),
semconv.ServiceVersionKey.String(serviceVersion),
),
)
if err != nil {
return nil, fmt.Errorf("failed to create resource: %w", err)
}
// Create tracer provider
tp := trace.NewTracerProvider(
trace.WithBatcher(exp),
trace.WithResource(res),
)
// Set global tracer provider
otel.SetTracerProvider(tp)
// Set global propagator
otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(
propagation.TraceContext{},
propagation.Baggage{},
))
return &Tracer{
tracer: tp.Tracer(serviceName),
}, nil
}
// StartSpan starts a new span
func (t *Tracer) StartSpan(ctx context.Context, name string, opts ...trace.SpanStartOption) (context.Context, trace.Span) {
return t.tracer.Start(ctx, name, opts...)
}
// StartSpanWithAttributes starts a new span with attributes
func (t *Tracer) StartSpanWithAttributes(ctx context.Context, name string, attrs map[string]interface{}) (context.Context, trace.Span) {
spanOpts := []trace.SpanStartOption{
trace.WithAttributes(t.convertAttributes(attrs)...),
}
return t.tracer.Start(ctx, name, spanOpts...)
}
// convertAttributes converts map to attributes
func (t *Tracer) convertAttributes(attrs map[string]interface{}) []attribute.KeyValue {
var attributes []attribute.KeyValue
for k, v := range attrs {
switch val := v.(type) {
case string:
attributes = append(attributes, attribute.String(k, val))
case int:
attributes = append(attributes, attribute.Int64(k, int64(val)))
case int64:
attributes = append(attributes, attribute.Int64(k, val))
case float64:
attributes = append(attributes, attribute.Float64(k, val))
case bool:
attributes = append(attributes, attribute.Bool(k, val))
default:
attributes = append(attributes, attribute.String(k, fmt.Sprintf("%v", val)))
}
}
return attributes
}
// FinishSpan finishes a span with success
func (t *Tracer) FinishSpan(span trace.Span) {
span.SetStatus(codes.Ok, "")
span.End()
}
// FinishSpanWithError finishes a span with error
func (t *Tracer) FinishSpanWithError(span trace.Span, err error) {
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
span.End()
}
// AddSpanAttributes adds attributes to a span
func (t *Tracer) AddSpanAttributes(span trace.Span, attrs map[string]interface{}) {
span.SetAttributes(t.convertAttributes(attrs)...)
}
// AddSpanEvent adds an event to a span
func (t *Tracer) AddSpanEvent(span trace.Span, name string, attrs map[string]interface{}) {
span.AddEvent(name, trace.WithAttributes(t.convertAttributes(attrs)...))
}
HTTP Tracing Middleware¶
// internal/tracing/http_middleware.go
package tracing
import (
"net/http"
"time"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/propagation"
"go.opentelemetry.io/otel/trace"
)
// TracingMiddleware provides HTTP tracing middleware
type TracingMiddleware struct {
tracer trace.Tracer
}
// NewTracingMiddleware creates a new tracing middleware
func NewTracingMiddleware() *TracingMiddleware {
return &TracingMiddleware{
tracer: otel.Tracer("http-server"),
}
}
// Middleware returns the HTTP middleware function
func (tm *TracingMiddleware) Middleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Extract trace context from headers
ctx := otel.GetTextMapPropagator().Extract(r.Context(), propagation.HeaderCarrier(r.Header))
// Start span
ctx, span := tm.tracer.Start(ctx, fmt.Sprintf("%s %s", r.Method, r.URL.Path))
defer span.End()
// Add span attributes
span.SetAttributes(
attribute.String("http.method", r.Method),
attribute.String("http.url", r.URL.String()),
attribute.String("http.user_agent", r.UserAgent()),
attribute.String("http.remote_addr", r.RemoteAddr),
)
// Create response writer wrapper
ww := &responseWriter{ResponseWriter: w, statusCode: http.StatusOK}
// Set trace context in response headers
otel.GetTextMapPropagator().Inject(ctx, propagation.HeaderCarrier(ww.Header()))
// Process request
start := time.Now()
next.ServeHTTP(ww, r.WithContext(ctx))
duration := time.Since(start)
// Add response attributes
span.SetAttributes(
attribute.Int("http.status_code", ww.statusCode),
attribute.Int64("http.response_size", ww.size),
attribute.Float64("http.duration_ms", float64(duration.Nanoseconds())/1e6),
)
// Set span status
if ww.statusCode >= 400 {
span.SetStatus(codes.Error, fmt.Sprintf("HTTP %d", ww.statusCode))
}
})
}
// responseWriter wraps http.ResponseWriter to capture status code
type responseWriter struct {
http.ResponseWriter
statusCode int
size int64
}
func (rw *responseWriter) WriteHeader(code int) {
rw.statusCode = code
rw.ResponseWriter.WriteHeader(code)
}
func (rw *responseWriter) Write(b []byte) (int, error) {
n, err := rw.ResponseWriter.Write(b)
rw.size += int64(n)
return n, err
}
Health Checks¶
Health Check System¶
// internal/health/health_check.go
package health
import (
"context"
"fmt"
"sync"
"time"
)
// HealthStatus represents health status
type HealthStatus string
const (
StatusHealthy HealthStatus = "healthy"
StatusUnhealthy HealthStatus = "unhealthy"
StatusDegraded HealthStatus = "degraded"
)
// HealthCheck represents a health check
type HealthCheck interface {
Name() string
Check(ctx context.Context) HealthResult
}
// HealthResult represents the result of a health check
type HealthResult struct {
Name string `json:"name"`
Status HealthStatus `json:"status"`
Message string `json:"message,omitempty"`
Duration time.Duration `json:"duration"`
Timestamp time.Time `json:"timestamp"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
}
// HealthChecker represents a health checker
type HealthChecker struct {
checks []HealthCheck
mutex sync.RWMutex
}
// NewHealthChecker creates a new health checker
func NewHealthChecker() *HealthChecker {
return &HealthChecker{
checks: make([]HealthCheck, 0),
}
}
// AddCheck adds a health check
func (hc *HealthChecker) AddCheck(check HealthCheck) {
hc.mutex.Lock()
defer hc.mutex.Unlock()
hc.checks = append(hc.checks, check)
}
// CheckAll runs all health checks
func (hc *HealthChecker) CheckAll(ctx context.Context) map[string]HealthResult {
hc.mutex.RLock()
checks := make([]HealthCheck, len(hc.checks))
copy(checks, hc.checks)
hc.mutex.RUnlock()
results := make(map[string]HealthResult)
for _, check := range checks {
result := check.Check(ctx)
results[check.Name()] = result
}
return results
}
// GetOverallStatus returns the overall health status
func (hc *HealthChecker) GetOverallStatus(ctx context.Context) HealthStatus {
results := hc.CheckAll(ctx)
hasUnhealthy := false
hasDegraded := false
for _, result := range results {
switch result.Status {
case StatusUnhealthy:
hasUnhealthy = true
case StatusDegraded:
hasDegraded = true
}
}
if hasUnhealthy {
return StatusUnhealthy
}
if hasDegraded {
return StatusDegraded
}
return StatusHealthy
}
// DatabaseHealthCheck represents a database health check
type DatabaseHealthCheck struct {
name string
pingFunc func(context.Context) error
}
// NewDatabaseHealthCheck creates a new database health check
func NewDatabaseHealthCheck(name string, pingFunc func(context.Context) error) *DatabaseHealthCheck {
return &DatabaseHealthCheck{
name: name,
pingFunc: pingFunc,
}
}
// Name returns the health check name
func (dhc *DatabaseHealthCheck) Name() string {
return dhc.name
}
// Check performs the health check
func (dhc *DatabaseHealthCheck) Check(ctx context.Context) HealthResult {
start := time.Now()
err := dhc.pingFunc(ctx)
duration := time.Since(start)
result := HealthResult{
Name: dhc.name,
Duration: duration,
Timestamp: time.Now(),
}
if err != nil {
result.Status = StatusUnhealthy
result.Message = err.Error()
} else {
result.Status = StatusHealthy
result.Message = "Database connection successful"
}
return result
}
// HTTPHealthCheck represents an HTTP health check
type HTTPHealthCheck struct {
name string
url string
client *http.Client
timeout time.Duration
}
// NewHTTPHealthCheck creates a new HTTP health check
func NewHTTPHealthCheck(name, url string, timeout time.Duration) *HTTPHealthCheck {
return &HTTPHealthCheck{
name: name,
url: url,
client: &http.Client{Timeout: timeout},
timeout: timeout,
}
}
// Name returns the health check name
func (hhc *HTTPHealthCheck) Name() string {
return hhc.name
}
// Check performs the health check
func (hhc *HTTPHealthCheck) Check(ctx context.Context) HealthResult {
start := time.Now()
req, err := http.NewRequestWithContext(ctx, "GET", hhc.url, nil)
if err != nil {
return HealthResult{
Name: hhc.name,
Status: StatusUnhealthy,
Message: fmt.Sprintf("Failed to create request: %v", err),
Duration: time.Since(start),
Timestamp: time.Now(),
}
}
resp, err := hhc.client.Do(req)
duration := time.Since(start)
result := HealthResult{
Name: hhc.name,
Duration: duration,
Timestamp: time.Now(),
}
if err != nil {
result.Status = StatusUnhealthy
result.Message = err.Error()
} else {
resp.Body.Close()
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
result.Status = StatusHealthy
result.Message = "HTTP health check successful"
} else {
result.Status = StatusUnhealthy
result.Message = fmt.Sprintf("HTTP status %d", resp.StatusCode)
}
}
return result
}
Alerting¶
Alert Manager¶
// internal/alerting/alert_manager.go
package alerting
import (
"context"
"fmt"
"sync"
"time"
)
// AlertLevel represents alert levels
type AlertLevel int
const (
InfoLevel AlertLevel = iota
WarningLevel
CriticalLevel
)
// String returns the string representation of the alert level
func (al AlertLevel) String() string {
switch al {
case InfoLevel:
return "INFO"
case WarningLevel:
return "WARNING"
case CriticalLevel:
return "CRITICAL"
default:
return "UNKNOWN"
}
}
// Alert represents an alert
type Alert struct {
ID string `json:"id"`
Level AlertLevel `json:"level"`
Title string `json:"title"`
Message string `json:"message"`
Source string `json:"source"`
Timestamp time.Time `json:"timestamp"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
Resolved bool `json:"resolved"`
ResolvedAt *time.Time `json:"resolved_at,omitempty"`
}
// AlertRule represents an alert rule
type AlertRule struct {
ID string
Name string
Condition func(metrics map[string]interface{}) bool
Level AlertLevel
Title string
Message string
Cooldown time.Duration
lastTriggered time.Time
}
// AlertManager represents an alert manager
type AlertManager struct {
rules []AlertRule
alerts map[string]*Alert
mutex sync.RWMutex
notifiers []Notifier
}
// NewAlertManager creates a new alert manager
func NewAlertManager() *AlertManager {
return &AlertManager{
rules: make([]AlertRule, 0),
alerts: make(map[string]*Alert),
notifiers: make([]Notifier, 0),
}
}
// AddRule adds an alert rule
func (am *AlertManager) AddRule(rule AlertRule) {
am.mutex.Lock()
defer am.mutex.Unlock()
am.rules = append(am.rules, rule)
}
// AddNotifier adds a notifier
func (am *AlertManager) AddNotifier(notifier Notifier) {
am.mutex.Lock()
defer am.mutex.Unlock()
am.notifiers = append(am.notifiers, notifier)
}
// EvaluateRules evaluates all alert rules
func (am *AlertManager) EvaluateRules(ctx context.Context, metrics map[string]interface{}) {
am.mutex.RLock()
rules := make([]AlertRule, len(am.rules))
copy(rules, am.rules)
am.mutex.RUnlock()
for _, rule := range rules {
if rule.Condition(metrics) {
am.triggerAlert(rule, metrics)
}
}
}
// triggerAlert triggers an alert
func (am *AlertManager) triggerAlert(rule AlertRule, metrics map[string]interface{}) {
// Check cooldown
if time.Since(rule.lastTriggered) < rule.Cooldown {
return
}
alert := &Alert{
ID: fmt.Sprintf("%s-%d", rule.ID, time.Now().Unix()),
Level: rule.Level,
Title: rule.Title,
Message: rule.Message,
Source: "alert-manager",
Timestamp: time.Now(),
Metadata: metrics,
Resolved: false,
}
am.mutex.Lock()
am.alerts[alert.ID] = alert
am.mutex.Unlock()
// Notify
for _, notifier := range am.notifiers {
go notifier.Notify(ctx, alert)
}
// Update last triggered time
rule.lastTriggered = time.Now()
}
// ResolveAlert resolves an alert
func (am *AlertManager) ResolveAlert(alertID string) {
am.mutex.Lock()
defer am.mutex.Unlock()
if alert, exists := am.alerts[alertID]; exists {
alert.Resolved = true
now := time.Now()
alert.ResolvedAt = &now
}
}
// GetActiveAlerts returns active alerts
func (am *AlertManager) GetActiveAlerts() []Alert {
am.mutex.RLock()
defer am.mutex.RUnlock()
var activeAlerts []Alert
for _, alert := range am.alerts {
if !alert.Resolved {
activeAlerts = append(activeAlerts, *alert)
}
}
return activeAlerts
}
// Notifier represents a notification interface
type Notifier interface {
Notify(ctx context.Context, alert *Alert) error
}
// SlackNotifier represents a Slack notifier
type SlackNotifier struct {
webhookURL string
channel string
}
// NewSlackNotifier creates a new Slack notifier
func NewSlackNotifier(webhookURL, channel string) *SlackNotifier {
return &SlackNotifier{
webhookURL: webhookURL,
channel: channel,
}
}
// Notify sends a notification to Slack
func (sn *SlackNotifier) Notify(ctx context.Context, alert *Alert) error {
// Implement Slack notification
return nil
}
// EmailNotifier represents an email notifier
type EmailNotifier struct {
smtpHost string
smtpPort int
username string
password string
from string
to []string
}
// NewEmailNotifier creates a new email notifier
func NewEmailNotifier(smtpHost string, smtpPort int, username, password, from string, to []string) *EmailNotifier {
return &EmailNotifier{
smtpHost: smtpHost,
smtpPort: smtpPort,
username: username,
password: password,
from: from,
to: to,
}
}
// Notify sends an email notification
func (en *EmailNotifier) Notify(ctx context.Context, alert *Alert) error {
// Implement email notification
return nil
}
TL;DR Runbook¶
Quick Start¶
// 1. Structured logging
logger := NewLogger(InfoLevel)
logger.Info("Application started", map[string]interface{}{
"version": "1.0.0",
"port": 8080,
})
// 2. Metrics collection
metrics := NewMetrics()
metrics.RecordHTTPRequest("GET", "/api/users", 200, 0.1)
// 3. Distributed tracing
tracer, err := NewTracer("myapp", "1.0.0")
ctx, span := tracer.StartSpan(ctx, "process-request")
defer tracer.FinishSpan(span)
// 4. Health checks
healthChecker := NewHealthChecker()
healthChecker.AddCheck(NewDatabaseHealthCheck("database", db.Ping))
Essential Patterns¶
// Context-aware logging
ctxLogger := NewContextLogger(logger, ctx)
ctxLogger.Info("Processing request", map[string]interface{}{
"user_id": userID,
"action": "create",
})
// Custom metrics
customMetrics := NewCustomMetrics()
customMetrics.RecordSession(5 * time.Minute)
// Alert rules
alertManager.AddRule(AlertRule{
ID: "high-error-rate",
Condition: func(metrics map[string]interface{}) bool {
return metrics["error_rate"].(float64) > 0.1
},
Level: CriticalLevel,
Title: "High Error Rate",
Message: "Error rate is above 10%",
})
This guide provides the complete machinery for implementing comprehensive monitoring and observability in Go applications. Each pattern includes implementation examples, integration strategies, and real-world usage patterns for enterprise deployment.