Monorepo for wisp.place. A static site hosting service built on top of the AT Protocol. wisp.place
at main 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}