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