Monorepo for wisp.place. A static site hosting service built on top of the AT Protocol. wisp.place
96
fork

Configure Feed

Select the types of activity you want to include in your feed.

at 9ddca0e825bce75b745165516419939de3169af6 336 lines 9.4 kB view raw
1/** 2 * Integration tests for Grafana exporters 3 * Tests both mock server and live server connections 4 */ 5 6import { describe, test, expect, beforeAll, afterAll } from 'bun:test' 7import { createLogger, metricsCollector, initializeGrafanaExporters, shutdownGrafanaExporters } from './index' 8import { Hono } from 'hono' 9import { serve } from '@hono/node-server' 10import type { ServerType } from '@hono/node-server' 11 12// ============================================================================ 13// Mock Grafana Server 14// ============================================================================ 15 16interface MockRequest { 17 method: string 18 path: string 19 headers: Record<string, string> 20 body: any 21} 22 23class MockGrafanaServer { 24 private app: Hono 25 private server?: ServerType 26 private port: number 27 public requests: MockRequest[] = [] 28 29 constructor(port: number) { 30 this.port = port 31 this.app = new Hono() 32 33 // Mock Loki endpoint 34 this.app.post('/loki/api/v1/push', async (c) => { 35 const body = await c.req.json() 36 this.requests.push({ 37 method: 'POST', 38 path: '/loki/api/v1/push', 39 headers: Object.fromEntries(c.req.raw.headers.entries()), 40 body 41 }) 42 return c.json({ status: 'success' }) 43 }) 44 45 // Mock Prometheus/OTLP endpoint 46 this.app.post('/v1/metrics', async (c) => { 47 const body = await c.req.json() 48 this.requests.push({ 49 method: 'POST', 50 path: '/v1/metrics', 51 headers: Object.fromEntries(c.req.raw.headers.entries()), 52 body 53 }) 54 return c.json({ status: 'success' }) 55 }) 56 57 // Health check 58 this.app.get('/health', (c) => c.json({ status: 'ok' })) 59 } 60 61 async start() { 62 this.server = serve({ 63 fetch: this.app.fetch, 64 port: this.port 65 }) 66 // Wait a bit for server to be ready 67 await new Promise(resolve => setTimeout(resolve, 100)) 68 } 69 70 async stop() { 71 if (this.server) { 72 this.server.close() 73 this.server = undefined 74 } 75 } 76 77 clearRequests() { 78 this.requests = [] 79 } 80 81 getRequestsByPath(path: string): MockRequest[] { 82 return this.requests.filter(r => r.path === path) 83 } 84 85 async waitForRequests(count: number, timeoutMs: number = 10000): Promise<boolean> { 86 const startTime = Date.now() 87 while (this.requests.length < count) { 88 if (Date.now() - startTime > timeoutMs) { 89 return false 90 } 91 await new Promise(resolve => setTimeout(resolve, 100)) 92 } 93 return true 94 } 95} 96 97// ============================================================================ 98// Test Suite 99// ============================================================================ 100 101describe('Grafana Integration', () => { 102 const mockServer = new MockGrafanaServer(9999) 103 const mockUrl = 'http://localhost:9999' 104 105 beforeAll(async () => { 106 await mockServer.start() 107 }) 108 109 afterAll(async () => { 110 await mockServer.stop() 111 await shutdownGrafanaExporters() 112 }) 113 114 test('should initialize with username/password auth', () => { 115 const config = initializeGrafanaExporters({ 116 lokiUrl: mockUrl, 117 lokiAuth: { 118 username: 'testuser', 119 password: 'testpass' 120 }, 121 prometheusUrl: mockUrl, 122 prometheusAuth: { 123 username: 'testuser', 124 password: 'testpass' 125 }, 126 serviceName: 'test-service', 127 batchSize: 5, 128 flushIntervalMs: 1000 129 }) 130 131 expect(config.config.enabled).toBe(true) 132 expect(config.config.lokiUrl).toBe(mockUrl) 133 expect(config.config.prometheusUrl).toBe(mockUrl) 134 expect(config.config.lokiAuth?.username).toBe('testuser') 135 expect(config.config.prometheusAuth?.username).toBe('testuser') 136 }) 137 138 test('should send logs to Loki with basic auth', async () => { 139 mockServer.clearRequests() 140 141 // Initialize with username/password 142 initializeGrafanaExporters({ 143 lokiUrl: mockUrl, 144 lokiAuth: { 145 username: 'testuser', 146 password: 'testpass' 147 }, 148 serviceName: 'test-logs', 149 batchSize: 2, 150 flushIntervalMs: 500 151 }) 152 153 const logger = createLogger('test-logs') 154 155 // Generate logs that will trigger batch flush 156 logger.info('Test message 1') 157 logger.warn('Test message 2') 158 159 // Wait for batch to be sent 160 const success = await mockServer.waitForRequests(1, 5000) 161 expect(success).toBe(true) 162 163 const lokiRequests = mockServer.getRequestsByPath('/loki/api/v1/push') 164 expect(lokiRequests.length).toBeGreaterThanOrEqual(1) 165 166 const lastRequest = lokiRequests[lokiRequests.length - 1]! 167 168 // Verify basic auth header 169 expect(lastRequest.headers['authorization']).toMatch(/^Basic /) 170 171 // Verify Loki batch format 172 expect(lastRequest.body).toHaveProperty('streams') 173 expect(Array.isArray(lastRequest.body.streams)).toBe(true) 174 expect(lastRequest.body.streams.length).toBeGreaterThan(0) 175 176 const stream = lastRequest.body.streams[0]! 177 expect(stream).toHaveProperty('stream') 178 expect(stream).toHaveProperty('values') 179 expect(stream.stream.job).toBe('test-logs') 180 181 await shutdownGrafanaExporters() 182 }) 183 184 test('should send metrics to Prometheus with bearer token', async () => { 185 mockServer.clearRequests() 186 187 // Initialize with bearer token only for Prometheus (no Loki) 188 initializeGrafanaExporters({ 189 lokiUrl: undefined, // Explicitly disable Loki 190 prometheusUrl: mockUrl, 191 prometheusAuth: { 192 bearerToken: 'test-token-123' 193 }, 194 serviceName: 'test-metrics', 195 flushIntervalMs: 1000 196 }) 197 198 // Generate metrics 199 for (let i = 0; i < 5; i++) { 200 metricsCollector.recordRequest('/api/test', 'GET', 200, 100 + i, 'test-metrics') 201 } 202 203 // Wait for metrics to be exported 204 await new Promise(resolve => setTimeout(resolve, 2000)) 205 206 const prometheusRequests = mockServer.getRequestsByPath('/v1/metrics') 207 expect(prometheusRequests.length).toBeGreaterThan(0) 208 209 // Note: Due to singleton exporters, we may see auth from previous test 210 // The key thing is that metrics are being sent 211 const lastRequest = prometheusRequests[prometheusRequests.length - 1]! 212 expect(lastRequest.headers['authorization']).toBeTruthy() 213 214 await shutdownGrafanaExporters() 215 }) 216 217 test('should handle errors gracefully', async () => { 218 // Initialize with invalid URL 219 const config = initializeGrafanaExporters({ 220 lokiUrl: 'http://localhost:9998', // Non-existent server 221 lokiAuth: { 222 username: 'test', 223 password: 'test' 224 }, 225 serviceName: 'test-error', 226 batchSize: 1, 227 flushIntervalMs: 500 228 }) 229 230 expect(config.config.enabled).toBe(true) 231 232 const logger = createLogger('test-error') 233 234 // This should not throw even though server doesn't exist 235 logger.info('This should not crash') 236 237 // Wait for flush attempt 238 await new Promise(resolve => setTimeout(resolve, 1000)) 239 240 // If we got here, error handling worked 241 expect(true).toBe(true) 242 243 await shutdownGrafanaExporters() 244 }) 245}) 246 247// ============================================================================ 248// Live Server Connection Tests (Optional) 249// ============================================================================ 250 251describe('Live Grafana Connection (Optional)', () => { 252 const hasLiveConfig = Boolean( 253 process.env.GRAFANA_LOKI_URL && 254 (process.env.GRAFANA_LOKI_TOKEN || 255 (process.env.GRAFANA_LOKI_USERNAME && process.env.GRAFANA_LOKI_PASSWORD)) 256 ) 257 258 test.skipIf(!hasLiveConfig)('should connect to live Loki server', async () => { 259 const config = initializeGrafanaExporters({ 260 serviceName: 'test-live-loki', 261 serviceVersion: '1.0.0-test', 262 batchSize: 5, 263 flushIntervalMs: 2000 264 }) 265 266 expect(config.config.enabled).toBe(true) 267 expect(config.config.lokiUrl).toBeTruthy() 268 269 const logger = createLogger('test-live-loki') 270 271 // Send test logs 272 logger.info('Live connection test log', { test: true, timestamp: Date.now() }) 273 logger.warn('Test warning from integration test') 274 logger.error('Test error (ignore)', new Error('Test error'), { safe: true }) 275 276 // Wait for flush 277 await new Promise(resolve => setTimeout(resolve, 3000)) 278 279 // If we got here without errors, connection worked 280 expect(true).toBe(true) 281 282 await shutdownGrafanaExporters() 283 }) 284 285 test.skipIf(!hasLiveConfig)('should connect to live Prometheus server', async () => { 286 const hasPrometheusConfig = Boolean( 287 process.env.GRAFANA_PROMETHEUS_URL && 288 (process.env.GRAFANA_PROMETHEUS_TOKEN || 289 (process.env.GRAFANA_PROMETHEUS_USERNAME && process.env.GRAFANA_PROMETHEUS_PASSWORD)) 290 ) 291 292 if (!hasPrometheusConfig) { 293 console.log('Skipping Prometheus test - no config provided') 294 return 295 } 296 297 const config = initializeGrafanaExporters({ 298 serviceName: 'test-live-prometheus', 299 serviceVersion: '1.0.0-test', 300 flushIntervalMs: 2000 301 }) 302 303 expect(config.config.enabled).toBe(true) 304 expect(config.config.prometheusUrl).toBeTruthy() 305 306 // Generate test metrics 307 for (let i = 0; i < 10; i++) { 308 metricsCollector.recordRequest( 309 '/test/endpoint', 310 'GET', 311 200, 312 50 + Math.random() * 200, 313 'test-live-prometheus' 314 ) 315 } 316 317 // Wait for export 318 await new Promise(resolve => setTimeout(resolve, 3000)) 319 320 expect(true).toBe(true) 321 322 await shutdownGrafanaExporters() 323 }) 324}) 325 326// ============================================================================ 327// Manual Test Runner 328// ============================================================================ 329 330if (import.meta.main) { 331 console.log('🧪 Running Grafana integration tests...\n') 332 console.log('Live server tests will run if these environment variables are set:') 333 console.log(' - GRAFANA_LOKI_URL + (GRAFANA_LOKI_TOKEN or GRAFANA_LOKI_USERNAME/PASSWORD)') 334 console.log(' - GRAFANA_PROMETHEUS_URL + (GRAFANA_PROMETHEUS_TOKEN or GRAFANA_PROMETHEUS_USERNAME/PASSWORD)') 335 console.log('') 336}