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

Configure Feed

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

remove memory o11y

+7 -637
+1 -34
apps/firehose-service/src/index.ts
··· 8 8 */ 9 9 10 10 import { serve } from '@hono/node-server' 11 - import { 12 - createLogger, 13 - errorTracker, 14 - initializeGrafanaExporters, 15 - logCollector, 16 - metricsCollector, 17 - } from '@wispplace/observability' 11 + import { createLogger, initializeGrafanaExporters } from '@wispplace/observability' 18 12 import { observabilityErrorHandler, observabilityMiddleware } from '@wispplace/observability/middleware/hono' 19 13 import { Hono } from 'hono' 20 14 import { config } from './config' ··· 241 235 242 236 logger.info(`Complete: ${processed} processed, ${skipped} skipped, ${failed} failed (${elapsedLabel} elapsed)`) 243 237 } 244 - 245 - // Internal observability endpoints (for admin panel) 246 - app.get('/__internal__/observability/logs', (c) => { 247 - const query = c.req.query() 248 - const filter: any = {} 249 - if (query.level) filter.level = query.level 250 - if (query.service) filter.service = query.service 251 - if (query.search) filter.search = query.search 252 - if (query.eventType) filter.eventType = query.eventType 253 - if (query.limit) filter.limit = parseInt(query.limit as string, 10) 254 - return c.json({ logs: logCollector.getLogs(filter) }) 255 - }) 256 - 257 - app.get('/__internal__/observability/errors', (c) => { 258 - const query = c.req.query() 259 - const filter: any = {} 260 - if (query.service) filter.service = query.service 261 - if (query.limit) filter.limit = parseInt(query.limit as string, 10) 262 - return c.json({ errors: errorTracker.getErrors(filter) }) 263 - }) 264 - 265 - app.get('/__internal__/observability/metrics', (c) => { 266 - const query = c.req.query() 267 - const timeWindow = query.timeWindow ? parseInt(query.timeWindow as string, 10) : 3600000 268 - const stats = metricsCollector.getStats('firehose-service', timeWindow) 269 - return c.json({ stats, timeWindow }) 270 - }) 271 238 272 239 // Main entry point 273 240 async function main() {
+1 -35
apps/hosting-service/src/server.ts
··· 4 4 */ 5 5 6 6 import { sanitizePath } from '@wispplace/fs-utils' 7 - import { createLogger, errorTracker, logCollector, metricsCollector } from '@wispplace/observability' 7 + import { createLogger } from '@wispplace/observability' 8 8 import { observabilityErrorHandler, observabilityMiddleware } from '@wispplace/observability/middleware/hono' 9 9 import { Hono } from 'hono' 10 10 import { cors } from 'hono/cors' ··· 12 12 import { getCustomDomain, getCustomDomainByHash, getWispDomain } from './lib/db' 13 13 import { serveFromCache, serveFromCacheWithRewrite } from './lib/file-serving' 14 14 import { extractHeaders, isValidRkey } from './lib/request-utils' 15 - import { getRevalidateMetrics } from './lib/revalidate-metrics' 16 15 import { resolveDid } from './lib/utils' 17 16 18 17 const logger = createLogger('hosting-service') ··· 178 177 179 178 const headers = extractHeaders(c.req.raw.headers) 180 179 return serveFromCache(customDomain.did, rkey, path, c.req.url, headers) 181 - }) 182 - 183 - // Internal observability endpoints (for admin panel) 184 - app.get('/__internal__/observability/logs', (c) => { 185 - const query = c.req.query() 186 - const filter: any = {} 187 - if (query.level) filter.level = query.level 188 - if (query.service) filter.service = query.service 189 - if (query.search) filter.search = query.search 190 - if (query.eventType) filter.eventType = query.eventType 191 - if (query.limit) filter.limit = parseInt(query.limit as string, 10) 192 - return c.json({ logs: logCollector.getLogs(filter) }) 193 - }) 194 - 195 - app.get('/__internal__/observability/errors', (c) => { 196 - const query = c.req.query() 197 - const filter: any = {} 198 - if (query.service) filter.service = query.service 199 - if (query.limit) filter.limit = parseInt(query.limit as string, 10) 200 - return c.json({ errors: errorTracker.getErrors(filter) }) 201 - }) 202 - 203 - app.get('/__internal__/observability/metrics', (c) => { 204 - const query = c.req.query() 205 - const timeWindow = query.timeWindow ? parseInt(query.timeWindow as string, 10) : 3600000 206 - const stats = metricsCollector.getStats('hosting-service', timeWindow) 207 - return c.json({ stats, revalidate: getRevalidateMetrics(), timeWindow }) 208 - }) 209 - 210 - app.get('/__internal__/observability/cache', async (c) => { 211 - const { getCacheStats } = await import('./lib/cache') 212 - const stats = await getCacheStats() 213 - return c.json({ cache: stats }) 214 180 }) 215 181 216 182 export default app
+3 -303
apps/main-app/public/admin/admin.tsx
··· 2 2 import { createRoot } from 'react-dom/client' 3 3 import './styles.css' 4 4 5 - // Types 6 - interface LogEntry { 7 - id: string 8 - timestamp: string 9 - level: 'info' | 'warn' | 'error' | 'debug' 10 - message: string 11 - service: string 12 - context?: Record<string, any> 13 - eventType?: string 14 - } 15 - 16 - interface ErrorEntry { 17 - id: string 18 - timestamp: string 19 - message: string 20 - stack?: string 21 - service: string 22 - count: number 23 - lastSeen: string 24 - } 25 - 26 5 // Helper function to format Unix timestamp from database 27 6 function formatDbDate(timestamp: number | string): Date { 28 7 const num = typeof timestamp === 'string' ? parseFloat(timestamp) : timestamp ··· 105 84 // Dashboard Component 106 85 function Dashboard() { 107 86 const [tab, setTab] = useState('overview') 108 - const [logs, setLogs] = useState<LogEntry[]>([]) 109 - const [errors, setErrors] = useState<ErrorEntry[]>([]) 110 - const [metrics, setMetrics] = useState<any>(null) 111 87 const [database, setDatabase] = useState<any>(null) 112 88 const [sites, setSites] = useState<any>(null) 113 89 const [health, setHealth] = useState<any>(null) 114 90 const [firehose, setFirehose] = useState<any>(null) 115 91 const [supporters, setSupporters] = useState<any[]>([]) 116 92 const [autoRefresh, setAutoRefresh] = useState(true) 117 - 118 - // Filters 119 - const [logLevel, setLogLevel] = useState('') 120 - const [logService, setLogService] = useState('') 121 - const [logSearch, setLogSearch] = useState('') 122 - const [logEventType, setLogEventType] = useState('') 123 93 124 94 // Supporter management 125 95 const [newSupporterIdentifier, setNewSupporterIdentifier] = useState('') ··· 130 100 const [showActorDropdown, setShowActorDropdown] = useState(false) 131 101 const [searchLoading, setSearchLoading] = useState(false) 132 102 133 - const fetchLogs = async () => { 134 - const params = new URLSearchParams() 135 - if (logLevel) params.append('level', logLevel) 136 - if (logService) params.append('service', logService) 137 - if (logSearch) params.append('search', logSearch) 138 - if (logEventType) params.append('eventType', logEventType) 139 - params.append('limit', '100') 140 - 141 - const res = await fetch(`/api/admin/logs?${params}`, { credentials: 'include' }) 142 - if (res.ok) { 143 - const data = await res.json() 144 - setLogs(data.logs) 145 - } 146 - } 147 - 148 - const fetchErrors = async () => { 149 - const res = await fetch('/api/admin/errors', { credentials: 'include' }) 150 - if (res.ok) { 151 - const data = await res.json() 152 - setErrors(data.errors) 153 - } 154 - } 155 - 156 - const fetchMetrics = async () => { 157 - const res = await fetch('/api/admin/metrics', { credentials: 'include' }) 158 - if (res.ok) { 159 - const data = await res.json() 160 - setMetrics(data) 161 - } 162 - } 163 - 164 103 const fetchDatabase = async () => { 165 104 const res = await fetch('/api/admin/database', { credentials: 'include' }) 166 105 if (res.ok) { ··· 327 266 } 328 267 329 268 useEffect(() => { 330 - fetchMetrics() 331 269 fetchDatabase() 332 270 fetchHealth() 333 271 fetchFirehose() 334 - fetchLogs() 335 - fetchErrors() 336 272 fetchSites() 337 273 fetchSupporters() 338 - }, [fetchDatabase, fetchErrors, fetchFirehose, fetchHealth, fetchLogs, fetchMetrics, fetchSites, fetchSupporters]) 339 - 340 - useEffect(() => { 341 - fetchLogs() 342 - }, [fetchLogs]) 274 + }, [fetchDatabase, fetchFirehose, fetchHealth, fetchSites, fetchSupporters]) 343 275 344 276 useEffect(() => { 345 277 if (!autoRefresh) return 346 278 347 279 const interval = setInterval(() => { 348 280 if (tab === 'overview') { 349 - fetchMetrics() 350 281 fetchHealth() 351 282 fetchFirehose() 352 - } else if (tab === 'logs') { 353 - fetchLogs() 354 - } else if (tab === 'errors') { 355 - fetchErrors() 356 283 } else if (tab === 'database') { 357 284 fetchDatabase() 358 285 } else if (tab === 'sites') { ··· 363 290 }, 5000) 364 291 365 292 return () => clearInterval(interval) 366 - }, [ 367 - tab, 368 - autoRefresh, 369 - fetchDatabase, 370 - fetchErrors, 371 - fetchFirehose, 372 - fetchHealth, 373 - fetchLogs, 374 - fetchMetrics, 375 - fetchSites, 376 - fetchSupporters, 377 - ]) 378 - 379 - const _formatDuration = (ms: number) => { 380 - if (ms < 1000) return `${ms}ms` 381 - return `${(ms / 1000).toFixed(2)}s` 382 - } 293 + }, [tab, autoRefresh, fetchDatabase, fetchFirehose, fetchHealth, fetchSites, fetchSupporters]) 383 294 384 295 const formatUptime = (seconds: number) => { 385 296 const hours = Math.floor(seconds / 3600) ··· 413 324 {/* Tabs */} 414 325 <div className="bg-gray-900 border-b border-gray-800 px-6"> 415 326 <div className="flex gap-1"> 416 - {['overview', 'logs', 'errors', 'database', 'sites', 'supporters'].map((t) => ( 327 + {['overview', 'database', 'sites', 'supporters'].map((t) => ( 417 328 <button 418 329 key={t} 419 330 onClick={() => setTab(t)} ··· 488 399 </div> 489 400 </div> 490 401 )} 491 - 492 - {/* Metrics */} 493 - {metrics && ( 494 - <div> 495 - <h2 className="text-xl font-bold mb-4">Performance Metrics</h2> 496 - <div className="space-y-4"> 497 - {/* Overall */} 498 - <div className="bg-gray-900 border border-gray-800 rounded-lg p-4"> 499 - <h3 className="text-lg font-semibold mb-3">Overall (Last Hour)</h3> 500 - <div className="grid grid-cols-2 md:grid-cols-4 gap-4"> 501 - <div> 502 - <div className="text-sm text-gray-400">Total Requests</div> 503 - <div className="text-xl font-bold">{metrics.overall.totalRequests}</div> 504 - </div> 505 - <div> 506 - <div className="text-sm text-gray-400">Avg Duration</div> 507 - <div className="text-xl font-bold">{metrics.overall.avgDuration}ms</div> 508 - </div> 509 - <div> 510 - <div className="text-sm text-gray-400">P95 Duration</div> 511 - <div className="text-xl font-bold">{metrics.overall.p95Duration}ms</div> 512 - </div> 513 - <div> 514 - <div className="text-sm text-gray-400">Error Rate</div> 515 - <div className="text-xl font-bold">{metrics.overall.errorRate.toFixed(2)}%</div> 516 - </div> 517 - </div> 518 - </div> 519 - 520 - {/* Main App */} 521 - <div className="bg-gray-900 border border-gray-800 rounded-lg p-4"> 522 - <h3 className="text-lg font-semibold mb-3">Main App</h3> 523 - <div className="grid grid-cols-2 md:grid-cols-4 gap-4"> 524 - <div> 525 - <div className="text-sm text-gray-400">Requests</div> 526 - <div className="text-xl font-bold">{metrics.mainApp.totalRequests}</div> 527 - </div> 528 - <div> 529 - <div className="text-sm text-gray-400">Avg</div> 530 - <div className="text-xl font-bold">{metrics.mainApp.avgDuration}ms</div> 531 - </div> 532 - <div> 533 - <div className="text-sm text-gray-400">P95</div> 534 - <div className="text-xl font-bold">{metrics.mainApp.p95Duration}ms</div> 535 - </div> 536 - <div> 537 - <div className="text-sm text-gray-400">Req/min</div> 538 - <div className="text-xl font-bold">{metrics.mainApp.requestsPerMinute}</div> 539 - </div> 540 - </div> 541 - </div> 542 - 543 - {/* Hosting Service */} 544 - <div className="bg-gray-900 border border-gray-800 rounded-lg p-4"> 545 - <h3 className="text-lg font-semibold mb-3">Hosting Service</h3> 546 - <div className="grid grid-cols-2 md:grid-cols-4 gap-4"> 547 - <div> 548 - <div className="text-sm text-gray-400">Requests</div> 549 - <div className="text-xl font-bold">{metrics.hostingService.totalRequests}</div> 550 - </div> 551 - <div> 552 - <div className="text-sm text-gray-400">Avg</div> 553 - <div className="text-xl font-bold">{metrics.hostingService.avgDuration}ms</div> 554 - </div> 555 - <div> 556 - <div className="text-sm text-gray-400">P95</div> 557 - <div className="text-xl font-bold">{metrics.hostingService.p95Duration}ms</div> 558 - </div> 559 - <div> 560 - <div className="text-sm text-gray-400">Req/min</div> 561 - <div className="text-xl font-bold">{metrics.hostingService.requestsPerMinute}</div> 562 - </div> 563 - </div> 564 - </div> 565 - </div> 566 - </div> 567 - )} 568 - </div> 569 - )} 570 - 571 - {tab === 'logs' && ( 572 - <div className="space-y-4"> 573 - <div className="flex gap-4"> 574 - <select 575 - value={logLevel} 576 - onChange={(e) => setLogLevel(e.target.value)} 577 - className="px-3 py-2 bg-gray-900 border border-gray-800 rounded text-white" 578 - > 579 - <option value="">All Levels</option> 580 - <option value="info">Info</option> 581 - <option value="warn">Warn</option> 582 - <option value="error">Error</option> 583 - <option value="debug">Debug</option> 584 - </select> 585 - <select 586 - value={logService} 587 - onChange={(e) => setLogService(e.target.value)} 588 - className="px-3 py-2 bg-gray-900 border border-gray-800 rounded text-white" 589 - > 590 - <option value="">All Services</option> 591 - <option value="main-app">Main App</option> 592 - <option value="hosting-service">Hosting Service</option> 593 - </select> 594 - <select 595 - value={logEventType} 596 - onChange={(e) => setLogEventType(e.target.value)} 597 - className="px-3 py-2 bg-gray-900 border border-gray-800 rounded text-white" 598 - > 599 - <option value="">All Event Types</option> 600 - <option value="DNS Verifier">DNS Verifier</option> 601 - <option value="Auth">Auth</option> 602 - <option value="User">User</option> 603 - <option value="Domain">Domain</option> 604 - <option value="Site">Site</option> 605 - <option value="File Upload">File Upload</option> 606 - <option value="Sync">Sync</option> 607 - <option value="Maintenance">Maintenance</option> 608 - <option value="KeyRotation">Key Rotation</option> 609 - <option value="Cleanup">Cleanup</option> 610 - <option value="Cache">Cache</option> 611 - <option value="FirehoseWorker">Firehose Worker</option> 612 - </select> 613 - <input 614 - type="text" 615 - value={logSearch} 616 - onChange={(e) => setLogSearch(e.target.value)} 617 - placeholder="Search logs..." 618 - className="flex-1 px-3 py-2 bg-gray-900 border border-gray-800 rounded text-white" 619 - /> 620 - </div> 621 - 622 - <div className="bg-gray-900 border border-gray-800 rounded-lg overflow-hidden"> 623 - <div className="max-h-[600px] overflow-y-auto"> 624 - <table className="w-full text-sm"> 625 - <thead className="bg-gray-800 sticky top-0"> 626 - <tr> 627 - <th className="px-4 py-2 text-left">Time</th> 628 - <th className="px-4 py-2 text-left">Level</th> 629 - <th className="px-4 py-2 text-left">Service</th> 630 - <th className="px-4 py-2 text-left">Event Type</th> 631 - <th className="px-4 py-2 text-left">Message</th> 632 - </tr> 633 - </thead> 634 - <tbody> 635 - {logs.map((log) => ( 636 - <tr key={log.id} className="border-t border-gray-800 hover:bg-gray-800"> 637 - <td className="px-4 py-2 text-gray-400 whitespace-nowrap"> 638 - {new Date(log.timestamp).toLocaleTimeString()} 639 - </td> 640 - <td className="px-4 py-2"> 641 - <span 642 - className={`px-2 py-1 rounded text-xs font-medium ${ 643 - log.level === 'error' 644 - ? 'bg-red-900 text-red-200' 645 - : log.level === 'warn' 646 - ? 'bg-yellow-900 text-yellow-200' 647 - : log.level === 'info' 648 - ? 'bg-blue-900 text-blue-200' 649 - : 'bg-gray-700 text-gray-300' 650 - }`} 651 - > 652 - {log.level} 653 - </span> 654 - </td> 655 - <td className="px-4 py-2 text-gray-400">{log.service}</td> 656 - <td className="px-4 py-2"> 657 - {log.eventType && ( 658 - <span className="px-2 py-1 bg-purple-900 text-purple-200 rounded text-xs font-medium"> 659 - {log.eventType} 660 - </span> 661 - )} 662 - </td> 663 - <td className="px-4 py-2"> 664 - <div>{log.message}</div> 665 - {log.context && Object.keys(log.context).length > 0 && ( 666 - <div className="text-xs text-gray-500 mt-1">{JSON.stringify(log.context)}</div> 667 - )} 668 - </td> 669 - </tr> 670 - ))} 671 - </tbody> 672 - </table> 673 - </div> 674 - </div> 675 - </div> 676 - )} 677 - 678 - {tab === 'errors' && ( 679 - <div className="space-y-4"> 680 - <h2 className="text-xl font-bold">Recent Errors</h2> 681 - <div className="space-y-3"> 682 - {errors.map((error) => ( 683 - <div key={error.id} className="bg-gray-900 border border-red-900 rounded-lg p-4"> 684 - <div className="flex items-start justify-between mb-2"> 685 - <div className="flex-1"> 686 - <div className="font-semibold text-red-400">{error.message}</div> 687 - <div className="text-sm text-gray-400 mt-1"> 688 - Service: {error.service} • Count: {error.count} • Last seen:{' '} 689 - {new Date(error.lastSeen).toLocaleString()} 690 - </div> 691 - </div> 692 - </div> 693 - {error.stack && ( 694 - <pre className="text-xs text-gray-500 bg-gray-950 p-2 rounded mt-2 overflow-x-auto"> 695 - {error.stack} 696 - </pre> 697 - )} 698 - </div> 699 - ))} 700 - {errors.length === 0 && <div className="text-center text-gray-500 py-8">No errors found</div>} 701 - </div> 702 402 </div> 703 403 )} 704 404
-233
apps/main-app/src/routes/admin.ts
··· 1 1 // Admin API routes 2 2 3 - import { errorTracker, logCollector, metricsCollector } from '@wispplace/observability' 4 3 import { Elysia, t } from 'elysia' 5 4 import { adminAuth, requireAdmin } from '../lib/admin-auth' 6 5 import { addSupporter, db, getAllSupporters, removeSupporter } from '../lib/db' ··· 126 125 }, 127 126 ) 128 127 129 - // Get logs (protected) 130 - /** 131 - * GET /api/admin/logs 132 - * Success: { logs } 133 - * Unauthorized (401): { error: 'Unauthorized' } 134 - */ 135 - .get( 136 - '/logs', 137 - async ({ query, cookie, set }) => { 138 - const check = requireAdmin({ cookie, set }) 139 - if (check) return check 140 - 141 - const filter: any = {} 142 - 143 - if (query.level) filter.level = query.level 144 - if (query.service) filter.service = query.service 145 - if (query.search) filter.search = query.search 146 - if (query.eventType) filter.eventType = query.eventType 147 - if (query.limit) filter.limit = parseInt(query.limit as string, 10) 148 - 149 - // Get logs from main app 150 - const mainLogs = logCollector.getLogs(filter) 151 - 152 - // Get logs from hosting service 153 - let hostingLogs: any[] = [] 154 - try { 155 - const hostingServiceUrl = 156 - process.env.HOSTING_SERVICE_URL || `http://localhost:${process.env.HOSTING_PORT || '3001'}` 157 - const params = new URLSearchParams() 158 - if (query.level) params.append('level', query.level as string) 159 - if (query.service) params.append('service', query.service as string) 160 - if (query.search) params.append('search', query.search as string) 161 - if (query.eventType) params.append('eventType', query.eventType as string) 162 - params.append('limit', String(filter.limit || 100)) 163 - 164 - const response = await fetch(`${hostingServiceUrl}/__internal__/observability/logs?${params}`) 165 - if (response.ok) { 166 - const data = await response.json() 167 - hostingLogs = data.logs 168 - } 169 - } catch (_err) { 170 - // Hosting service might not be running 171 - } 172 - 173 - // Merge and sort by timestamp 174 - const allLogs = [...mainLogs, ...hostingLogs].sort( 175 - (a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime(), 176 - ) 177 - 178 - return { logs: allLogs.slice(0, filter.limit || 100) } 179 - }, 180 - { 181 - cookie: t.Cookie( 182 - { 183 - admin_session: t.Optional(t.String()), 184 - }, 185 - { 186 - secrets: cookieSecret, 187 - sign: ['admin_session'], 188 - }, 189 - ), 190 - }, 191 - ) 192 - 193 - // Get errors (protected) 194 - /** 195 - * GET /api/admin/errors 196 - * Success: { errors } 197 - * Unauthorized (401): { error: 'Unauthorized' } 198 - */ 199 - .get( 200 - '/errors', 201 - async ({ query, cookie, set }) => { 202 - const check = requireAdmin({ cookie, set }) 203 - if (check) return check 204 - 205 - const filter: any = {} 206 - 207 - if (query.service) filter.service = query.service 208 - if (query.limit) filter.limit = parseInt(query.limit as string, 10) 209 - 210 - // Get errors from main app 211 - const mainErrors = errorTracker.getErrors(filter) 212 - 213 - // Get errors from hosting service 214 - let hostingErrors: any[] = [] 215 - try { 216 - const hostingServiceUrl = 217 - process.env.HOSTING_SERVICE_URL || `http://localhost:${process.env.HOSTING_PORT || '3001'}` 218 - const params = new URLSearchParams() 219 - if (query.service) params.append('service', query.service as string) 220 - params.append('limit', String(filter.limit || 100)) 221 - 222 - const response = await fetch(`${hostingServiceUrl}/__internal__/observability/errors?${params}`) 223 - if (response.ok) { 224 - const data = await response.json() 225 - hostingErrors = data.errors 226 - } 227 - } catch (_err) { 228 - // Hosting service might not be running 229 - } 230 - 231 - // Merge and sort by last seen 232 - const allErrors = [...mainErrors, ...hostingErrors].sort( 233 - (a, b) => new Date(b.lastSeen).getTime() - new Date(a.lastSeen).getTime(), 234 - ) 235 - 236 - return { errors: allErrors.slice(0, filter.limit || 100) } 237 - }, 238 - { 239 - cookie: t.Cookie( 240 - { 241 - admin_session: t.Optional(t.String()), 242 - }, 243 - { 244 - secrets: cookieSecret, 245 - sign: ['admin_session'], 246 - }, 247 - ), 248 - }, 249 - ) 250 - 251 - // Get metrics (protected) 252 - /** 253 - * GET /api/admin/metrics 254 - * Success: { overall, mainApp, hostingService, timeWindow } 255 - * Unauthorized (401): { error: 'Unauthorized' } 256 - */ 257 - .get( 258 - '/metrics', 259 - async ({ query, cookie, set }) => { 260 - const check = requireAdmin({ cookie, set }) 261 - if (check) return check 262 - 263 - const timeWindow = query.timeWindow ? parseInt(query.timeWindow as string, 10) : 3600000 // 1 hour default 264 - 265 - const mainAppStats = metricsCollector.getStats('main-app', timeWindow) 266 - const overallStats = metricsCollector.getStats(undefined, timeWindow) 267 - 268 - // Get hosting service stats from its own endpoint 269 - let hostingServiceStats = { 270 - totalRequests: 0, 271 - avgDuration: 0, 272 - p50Duration: 0, 273 - p95Duration: 0, 274 - p99Duration: 0, 275 - errorRate: 0, 276 - requestsPerMinute: 0, 277 - } 278 - 279 - try { 280 - const hostingServiceUrl = 281 - process.env.HOSTING_SERVICE_URL || `http://localhost:${process.env.HOSTING_PORT || '3001'}` 282 - const response = await fetch( 283 - `${hostingServiceUrl}/__internal__/observability/metrics?timeWindow=${timeWindow}`, 284 - ) 285 - if (response.ok) { 286 - const data = await response.json() 287 - hostingServiceStats = data.stats 288 - } 289 - } catch (_err) { 290 - // Hosting service might not be running 291 - } 292 - 293 - return { 294 - overall: overallStats, 295 - mainApp: mainAppStats, 296 - hostingService: hostingServiceStats, 297 - timeWindow, 298 - } 299 - }, 300 - { 301 - cookie: t.Cookie( 302 - { 303 - admin_session: t.Optional(t.String()), 304 - }, 305 - { 306 - secrets: cookieSecret, 307 - sign: ['admin_session'], 308 - }, 309 - ), 310 - }, 311 - ) 312 - 313 128 // Get database stats (protected) 314 129 /** 315 130 * GET /api/admin/database ··· 365 180 set.status = 500 366 181 return { 367 182 error: 'Failed to fetch database stats', 368 - message: error instanceof Error ? error.message : String(error), 369 - } 370 - } 371 - }, 372 - { 373 - cookie: t.Cookie( 374 - { 375 - admin_session: t.Optional(t.String()), 376 - }, 377 - { 378 - secrets: cookieSecret, 379 - sign: ['admin_session'], 380 - }, 381 - ), 382 - }, 383 - ) 384 - 385 - // Get cache stats (protected) 386 - /** 387 - * GET /api/admin/cache 388 - * Success: hosting service cache stats payload. 389 - * Failure (503|500): { error, message } 390 - */ 391 - .get( 392 - '/cache', 393 - async ({ cookie, set }) => { 394 - const check = requireAdmin({ cookie, set }) 395 - if (check) return check 396 - 397 - try { 398 - const hostingServiceUrl = 399 - process.env.HOSTING_SERVICE_URL || `http://localhost:${process.env.HOSTING_PORT || '3001'}` 400 - const response = await fetch(`${hostingServiceUrl}/__internal__/observability/cache`) 401 - 402 - if (response.ok) { 403 - const data = await response.json() 404 - return data 405 - } else { 406 - set.status = 503 407 - return { 408 - error: 'Failed to fetch cache stats from hosting service', 409 - message: 'Hosting service unavailable', 410 - } 411 - } 412 - } catch (error) { 413 - set.status = 500 414 - return { 415 - error: 'Failed to fetch cache stats', 416 183 message: error instanceof Error ? error.message : String(error), 417 184 } 418 185 }
+1 -6
docs/src/content/docs/architecture.md
··· 130 130 131 131 ## Observability 132 132 133 - Both services expose internal endpoints: 134 - 135 - - `/__internal__/observability/logs` 136 - - `/__internal__/observability/errors` 137 - - `/__internal__/observability/metrics` 138 - - `/__internal__/observability/cache` (hosting service only) 133 + Operational observability is provided through service logs and configured Grafana exporters. The old in-memory internal endpoints were removed. 139 134 140 135 See [Monitoring & Metrics](/monitoring).
+1 -5
docs/src/content/docs/monitoring.md
··· 73 73 74 74 ## Without Grafana 75 75 76 - Metrics and logs are always stored in-memory. Access them directly: 77 - 78 - - `http://localhost:8000/api/observability/logs` 79 - - `http://localhost:8000/api/observability/metrics` 80 - - `http://localhost:8000/api/observability/errors` 76 + The old in-memory observability endpoints were removed. Without Grafana, use service logs and the remaining health endpoints for operational checks. 81 77 82 78 ## Programmatic Setup 83 79
-21
docs/src/content/docs/reference/main-app-api.md
··· 200 200 { "authenticated": false } 201 201 ``` 202 202 203 - ### `GET /api/admin/logs` 204 - ```json 205 - { "logs": [ /* combined log entries */ ] } 206 - ``` 207 - 208 - ### `GET /api/admin/errors` 209 - ```json 210 - { "errors": [ /* combined error entries */ ] } 211 - ``` 212 - 213 - ### `GET /api/admin/metrics` 214 - ```json 215 - { "overall": {}, "mainApp": {}, "hostingService": {}, "timeWindow": 3600000 } 216 - ``` 217 - 218 203 ### `GET /api/admin/database` 219 204 ```json 220 205 { "stats": {}, "recentSites": [], "recentDomains": [] } 221 - ``` 222 - 223 - ### `GET /api/admin/cache` 224 - Returns hosting service cache stats, or: 225 - ```json 226 - { "error": "Failed to fetch cache stats from hosting service", "message": "Hosting service unavailable" } 227 206 ``` 228 207 229 208 ### `GET /api/admin/sites`