Pulse (Observability)
Every Grit project ships with Pulse, a self-hosted observability and performance monitoring SDK. It provides request tracing, database monitoring, runtime metrics, error tracking, health checks, alerting, and a real-time dashboard — all with a single Mount() call.
What You Get
After starting your API, the Pulse dashboard is available at /pulse/ui/ (default credentials: admin / pulse).
| Feature | Description |
|---|---|
| Request Tracing | Automatic trace IDs, slow request detection, latency tracking |
| Database Monitoring | GORM query capture, N+1 detection, connection pool stats |
| Runtime Metrics | Heap memory, goroutines, GC pauses, leak detection |
| Error Tracking | Panic recovery, stack traces, request body capture, error fingerprinting |
| Health Checks | Kubernetes-compatible /live and /ready endpoints |
| Alerting | Threshold-based rules with Slack, Discord, email, and webhook notifications |
| Prometheus | 18+ metrics at /pulse/metrics |
| Dashboard | React 19 UI with 8 pages: Overview, Routes, Database, Errors, Runtime, Health, Alerts, Settings |
Configuration
Pulse is mounted in routes.go and configured via environment variables:
# Observability — PulsePULSE_ENABLED=truePULSE_USERNAME=adminPULSE_PASSWORD=pulse
The mount call in routes.go:
// Mount Pulse observabilityif cfg.PulseEnabled {p := pulse.Mount(r, db, pulse.Config{AppName: cfg.AppName,DevMode: cfg.IsDevelopment(),Dashboard: pulse.DashboardConfig{Username: cfg.PulseUsername,Password: cfg.PulsePassword,},Tracing: pulse.TracingConfig{ExcludePaths: []string{"/studio/*", "/sentinel/*", "/docs/*", "/pulse/*"},},Prometheus: pulse.PrometheusConfig{Enabled: true,},})// Register health checks for connected servicesif svc.Cache != nil {p.AddHealthCheck(pulse.HealthCheck{Name: "redis",Type: "redis",Critical: false,CheckFunc: func(ctx context.Context) error {return svc.Cache.Client().Ping(ctx).Err()},})}}
Endpoints
| Endpoint | Description |
|---|---|
GET /pulse/ui/ | Dashboard UI |
GET /pulse/health | Full health status (public) |
GET /pulse/health/live | Kubernetes liveness probe |
GET /pulse/health/ready | Kubernetes readiness probe |
GET /pulse/metrics | Prometheus metrics |
POST /pulse/api/auth/login | Dashboard authentication |
GET /pulse/api/overview | Dashboard summary (auth required) |
WS /pulse/ws/live | WebSocket live updates |
Custom Health Checks
The Mount() function returns a Pulse instance. Use it to register health checks for any dependency:
p := pulse.Mount(r, db, cfg)// Redis health checkp.AddHealthCheck(pulse.HealthCheck{Name: "redis",Type: "redis",Critical: false, // non-critical = won't fail /readyCheckFunc: func(ctx context.Context) error {return redisClient.Ping(ctx).Err()},})// External API health checkp.AddHealthCheck(pulse.HealthCheck{Name: "payment-api",Type: "http",Critical: true, // critical = will fail /ready if downCheckFunc: func(ctx context.Context) error {resp, err := http.Get("https://api.stripe.com/v1/health")if err != nil {return err}defer resp.Body.Close()if resp.StatusCode != 200 {return fmt.Errorf("status %d", resp.StatusCode)}return nil},})
The database health check is registered automatically when you pass a *gorm.DB to Mount().
Alerting
Pulse ships with 5 default alert rules (high latency, high error rate, high memory, goroutine leak, health check failure). You can add custom rules and notification channels:
pulse.Config{Alerts: pulse.AlertConfig{Rules: []pulse.AlertRule{{Name: "api_slow",Metric: "p95_latency",Operator: ">",Threshold: 1000, // msDuration: 3 * time.Minute,Severity: "warning",},},// Notification channelsSlack: &pulse.SlackConfig{WebhookURL: "https://hooks.slack.com/services/...",Channel: "#alerts",},Discord: &pulse.DiscordConfig{WebhookURL: "https://discord.com/api/webhooks/...",},Webhooks: []pulse.WebhookConfig{{URL: "https://api.example.com/webhook",Secret: "hmac-secret", // HMAC-SHA256 signature},},},}
Built-in Default Rules
| Rule | Metric | Condition | Severity |
|---|---|---|---|
| high_latency | P95 latency | > 2000ms for 5min | critical |
| high_error_rate | Error rate | > 10% for 3min | critical |
| high_memory | Heap alloc | > 500MB for 5min | warning |
| goroutine_leak | Goroutine growth | > 100/hr for 10min | warning |
| health_check_failure | Health status | unhealthy for 2min | critical |
Data Storage
By default, Pulse uses in-memory ring buffers (~100K request capacity). For persistence across restarts, switch to SQLite:
pulse.Config{Storage: pulse.StorageConfig{Driver: pulse.SQLite,DSN: "pulse.db",RetentionHours: 24,},}
Prometheus Integration
Pulse exposes 18+ metrics in Prometheus exposition format at /pulse/metrics. Connect it to Grafana or any Prometheus-compatible tool.
Key metrics include:
pulse_http_requests_total— Total HTTP requests by method, path, statuspulse_http_request_duration_seconds— Request duration histogrampulse_http_error_rate— Current error rate percentagepulse_runtime_goroutines— Active goroutine countpulse_runtime_heap_bytes— Heap allocation in bytespulse_db_query_duration_seconds— Database query durationpulse_health_check_status— Health check status (1=healthy, 0=unhealthy)
Disabling Pulse
Set PULSE_ENABLED=false in your .env to disable Pulse entirely. This skips the mount and adds zero overhead to your application.