Monorepo for wisp.place. A static site hosting service built on top of the AT Protocol. wisp.place
1# @wisp/observability 2 3Framework-agnostic observability package with Grafana integration for logs and metrics persistence. 4 5## Features 6 7- **In-memory storage** for local development 8- **Grafana Loki** integration for log persistence 9- **Prometheus/OTLP** integration for metrics 10- Framework middleware for Elysia and Hono 11- Automatic batching and buffering for efficient data transmission 12 13## Installation 14 15```bash 16bun add @wisp/observability 17``` 18 19## Basic Usage 20 21### Without Grafana (In-Memory Only) 22 23```typescript 24import { createLogger, metricsCollector } from '@wisp/observability' 25 26const logger = createLogger('my-service') 27 28// Log messages 29logger.info('Server started') 30logger.error('Failed to connect', new Error('Connection refused')) 31 32// Record metrics 33metricsCollector.recordRequest('/api/users', 'GET', 200, 45, 'my-service') 34``` 35 36### With Grafana Integration 37 38```typescript 39import { initializeGrafanaExporters, createLogger } from '@wisp/observability' 40 41// Initialize at application startup 42initializeGrafanaExporters({ 43 lokiUrl: 'https://logs-prod.grafana.net', 44 lokiAuth: { 45 bearerToken: 'your-loki-api-key' 46 }, 47 prometheusUrl: 'https://prometheus-prod.grafana.net', 48 prometheusAuth: { 49 bearerToken: 'your-prometheus-api-key' 50 }, 51 serviceName: 'wisp-app', 52 serviceVersion: '1.0.0', 53 batchSize: 100, 54 flushIntervalMs: 5000 55}) 56 57// Now all logs and metrics will be sent to Grafana automatically 58const logger = createLogger('my-service') 59logger.info('This will be sent to Grafana Loki') 60``` 61 62## Configuration 63 64### Environment Variables 65 66You can configure Grafana integration using environment variables: 67 68```bash 69# Loki configuration 70GRAFANA_LOKI_URL=https://logs-prod.grafana.net 71 72# Authentication Option 1: Bearer Token (Grafana Cloud) 73GRAFANA_LOKI_TOKEN=your-loki-api-key 74 75# Authentication Option 2: Username/Password (Self-hosted or some Grafana setups) 76GRAFANA_LOKI_USERNAME=your-username 77GRAFANA_LOKI_PASSWORD=your-password 78 79# Prometheus configuration 80GRAFANA_PROMETHEUS_URL=https://prometheus-prod.grafana.net/api/prom 81 82# Authentication Option 1: Bearer Token (Grafana Cloud) 83GRAFANA_PROMETHEUS_TOKEN=your-prometheus-api-key 84 85# Authentication Option 2: Username/Password (Self-hosted or some Grafana setups) 86GRAFANA_PROMETHEUS_USERNAME=your-username 87GRAFANA_PROMETHEUS_PASSWORD=your-password 88``` 89 90### Programmatic Configuration 91 92```typescript 93import { initializeGrafanaExporters } from '@wisp/observability' 94 95initializeGrafanaExporters({ 96 // Loki configuration for logs 97 lokiUrl: 'https://logs-prod.grafana.net', 98 lokiAuth: { 99 // Option 1: Bearer token (recommended for Grafana Cloud) 100 bearerToken: 'your-api-key', 101 102 // Option 2: Basic auth 103 username: 'your-username', 104 password: 'your-password' 105 }, 106 107 // Prometheus/OTLP configuration for metrics 108 prometheusUrl: 'https://prometheus-prod.grafana.net', 109 prometheusAuth: { 110 bearerToken: 'your-api-key' 111 }, 112 113 // Service metadata 114 serviceName: 'wisp-app', 115 serviceVersion: '1.0.0', 116 117 // Batching configuration 118 batchSize: 100, // Flush after this many entries 119 flushIntervalMs: 5000, // Flush every 5 seconds 120 121 // Enable/disable exporters 122 enabled: true 123}) 124``` 125 126## Middleware Integration 127 128### Elysia 129 130```typescript 131import { Elysia } from 'elysia' 132import { observabilityMiddleware } from '@wisp/observability/middleware/elysia' 133import { initializeGrafanaExporters } from '@wisp/observability' 134 135// Initialize Grafana exporters 136initializeGrafanaExporters({ 137 lokiUrl: process.env.GRAFANA_LOKI_URL, 138 lokiAuth: { bearerToken: process.env.GRAFANA_LOKI_TOKEN } 139}) 140 141const app = new Elysia() 142 .use(observabilityMiddleware({ service: 'main-app' })) 143 .get('/', () => 'Hello World') 144 .listen(3000) 145``` 146 147### Hono 148 149```typescript 150import { Hono } from 'hono' 151import { observabilityMiddleware, observabilityErrorHandler } from '@wisp/observability/middleware/hono' 152import { initializeGrafanaExporters } from '@wisp/observability' 153 154// Initialize Grafana exporters 155initializeGrafanaExporters({ 156 lokiUrl: process.env.GRAFANA_LOKI_URL, 157 lokiAuth: { bearerToken: process.env.GRAFANA_LOKI_TOKEN } 158}) 159 160const app = new Hono() 161app.use('*', observabilityMiddleware({ service: 'hosting-service' })) 162app.onError(observabilityErrorHandler({ service: 'hosting-service' })) 163``` 164 165## Grafana Cloud Setup 166 1671. **Create a Grafana Cloud account** at https://grafana.com/ 168 1692. **Get your Loki credentials:** 170 - Go to your Grafana Cloud portal 171 - Navigate to "Loki" → "Details" 172 - Copy the Push endpoint URL and create an API key 173 1743. **Get your Prometheus credentials:** 175 - Navigate to "Prometheus" → "Details" 176 - Copy the Remote Write endpoint and create an API key 177 1784. **Configure your application:** 179 ```typescript 180 initializeGrafanaExporters({ 181 lokiUrl: 'https://logs-prod-xxx.grafana.net', 182 lokiAuth: { bearerToken: 'glc_xxx' }, 183 prometheusUrl: 'https://prometheus-prod-xxx.grafana.net/api/prom', 184 prometheusAuth: { bearerToken: 'glc_xxx' } 185 }) 186 ``` 187 188## Data Flow 189 1901. **Logs** → Buffered → Batched → Sent to Grafana Loki 1912. **Metrics** → Aggregated → Exported via OTLP → Sent to Prometheus 1923. **Errors** → Deduplicated → Sent to Loki with error tag 193 194## Performance Considerations 195 196- Logs and metrics are batched to reduce network overhead 197- Default batch size: 100 entries 198- Default flush interval: 5 seconds 199- Failed exports are logged but don't block application 200- In-memory buffers are capped to prevent memory leaks 201 202## Graceful Shutdown 203 204The exporters automatically register shutdown handlers: 205 206```typescript 207import { shutdownGrafanaExporters } from '@wisp/observability' 208 209// Manual shutdown if needed 210process.on('beforeExit', async () => { 211 await shutdownGrafanaExporters() 212}) 213``` 214 215## License 216 217MIT