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=pulsePULSE_STORAGE=memory # "memory" (default) | "sqlite"PULSE_STORAGE_DSN=pulse.db # path used when PULSE_STORAGE=sqlite
Pulse v1.0 is mounted with functional options and a context.Context (the context drives clean shutdown of the dashboard WebSocket and background samplers). The scaffolded mount call:
// Mount Pulse observabilityif cfg.PulseEnabled {pulseOpts := []pulse.Option{pulse.WithAppName(cfg.AppName),pulse.WithCredentials(cfg.PulseUsername, cfg.PulsePassword),pulse.WithExcludePaths("/studio/*", "/sentinel/*", "/docs/*", "/pulse/*"),pulse.WithPrometheus(),// Pulse's error middleware snapshots a request-body snippet for context,// but restores ONLY the first 4096 bytes — silently truncating larger// uploads/JSON. Disable capture so the full body reaches the handler.pulse.WithRequestBodyCaptureDisabled(),}if cfg.IsDevelopment() {pulseOpts = append(pulseOpts, pulse.WithDevMode())}if cfg.PulseStorage == "sqlite" && cfg.PulseStorageDSN != "" {pulseOpts = append(pulseOpts, pulse.WithSQLite(cfg.PulseStorageDSN))}p := pulse.Mount(context.Background(), r, db, pulseOpts...)// 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()},})}}
| Option | Effect |
|---|---|
| WithAppName(name) | Labels the dashboard |
| WithCredentials(user, pass) | Dashboard basic-auth login |
| WithExcludePaths(globs…) | Skip tracing for these routes |
| WithPrometheus() | Expose /pulse/metrics |
| WithDevMode() | Verbose, dev-only behaviour |
| WithSQLite(dsn) | Persist data across restarts |
| WithRequestBodyCaptureDisabled() | Stop the 4096-byte body truncation |
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(context.Background(), r, db, pulseOpts...)// 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) that run out of the box — no configuration needed. They fire against the metrics Pulse already collects and surface in the dashboard.
Custom rules & channels. The scaffold enables the built-in rules and doesn't wire any notification channel by default. To add your own rules or a Slack/Discord/webhook channel, pass the matching Pulse option when you build pulseOpts in routes.go — see the Pulse package docs for the current option set, since these evolve with the library.
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 — fastest, but data resets on restart. For persistence across restarts, set PULSE_STORAGE=sqlite in your .env; the mount call then adds the pulse.WithSQLite() option automatically:
PULSE_STORAGE=sqlite # switch from in-memory to persistentPULSE_STORAGE_DSN=pulse.db # SQLite file (or ":memory:")
if cfg.PulseStorage == "sqlite" && cfg.PulseStorageDSN != "" {pulseOpts = append(pulseOpts, pulse.WithSQLite(cfg.PulseStorageDSN))}
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.
