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