···11// Admin API routes
2233-import { errorTracker, logCollector, metricsCollector } from '@wispplace/observability'
43import { Elysia, t } from 'elysia'
54import { adminAuth, requireAdmin } from '../lib/admin-auth'
65import { addSupporter, db, getAllSupporters, removeSupporter } from '../lib/db'
···126125 },
127126 )
128127129129- // Get logs (protected)
130130- /**
131131- * GET /api/admin/logs
132132- * Success: { logs }
133133- * Unauthorized (401): { error: 'Unauthorized' }
134134- */
135135- .get(
136136- '/logs',
137137- async ({ query, cookie, set }) => {
138138- const check = requireAdmin({ cookie, set })
139139- if (check) return check
140140-141141- const filter: any = {}
142142-143143- if (query.level) filter.level = query.level
144144- if (query.service) filter.service = query.service
145145- if (query.search) filter.search = query.search
146146- if (query.eventType) filter.eventType = query.eventType
147147- if (query.limit) filter.limit = parseInt(query.limit as string, 10)
148148-149149- // Get logs from main app
150150- const mainLogs = logCollector.getLogs(filter)
151151-152152- // Get logs from hosting service
153153- let hostingLogs: any[] = []
154154- try {
155155- const hostingServiceUrl =
156156- process.env.HOSTING_SERVICE_URL || `http://localhost:${process.env.HOSTING_PORT || '3001'}`
157157- const params = new URLSearchParams()
158158- if (query.level) params.append('level', query.level as string)
159159- if (query.service) params.append('service', query.service as string)
160160- if (query.search) params.append('search', query.search as string)
161161- if (query.eventType) params.append('eventType', query.eventType as string)
162162- params.append('limit', String(filter.limit || 100))
163163-164164- const response = await fetch(`${hostingServiceUrl}/__internal__/observability/logs?${params}`)
165165- if (response.ok) {
166166- const data = await response.json()
167167- hostingLogs = data.logs
168168- }
169169- } catch (_err) {
170170- // Hosting service might not be running
171171- }
172172-173173- // Merge and sort by timestamp
174174- const allLogs = [...mainLogs, ...hostingLogs].sort(
175175- (a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime(),
176176- )
177177-178178- return { logs: allLogs.slice(0, filter.limit || 100) }
179179- },
180180- {
181181- cookie: t.Cookie(
182182- {
183183- admin_session: t.Optional(t.String()),
184184- },
185185- {
186186- secrets: cookieSecret,
187187- sign: ['admin_session'],
188188- },
189189- ),
190190- },
191191- )
192192-193193- // Get errors (protected)
194194- /**
195195- * GET /api/admin/errors
196196- * Success: { errors }
197197- * Unauthorized (401): { error: 'Unauthorized' }
198198- */
199199- .get(
200200- '/errors',
201201- async ({ query, cookie, set }) => {
202202- const check = requireAdmin({ cookie, set })
203203- if (check) return check
204204-205205- const filter: any = {}
206206-207207- if (query.service) filter.service = query.service
208208- if (query.limit) filter.limit = parseInt(query.limit as string, 10)
209209-210210- // Get errors from main app
211211- const mainErrors = errorTracker.getErrors(filter)
212212-213213- // Get errors from hosting service
214214- let hostingErrors: any[] = []
215215- try {
216216- const hostingServiceUrl =
217217- process.env.HOSTING_SERVICE_URL || `http://localhost:${process.env.HOSTING_PORT || '3001'}`
218218- const params = new URLSearchParams()
219219- if (query.service) params.append('service', query.service as string)
220220- params.append('limit', String(filter.limit || 100))
221221-222222- const response = await fetch(`${hostingServiceUrl}/__internal__/observability/errors?${params}`)
223223- if (response.ok) {
224224- const data = await response.json()
225225- hostingErrors = data.errors
226226- }
227227- } catch (_err) {
228228- // Hosting service might not be running
229229- }
230230-231231- // Merge and sort by last seen
232232- const allErrors = [...mainErrors, ...hostingErrors].sort(
233233- (a, b) => new Date(b.lastSeen).getTime() - new Date(a.lastSeen).getTime(),
234234- )
235235-236236- return { errors: allErrors.slice(0, filter.limit || 100) }
237237- },
238238- {
239239- cookie: t.Cookie(
240240- {
241241- admin_session: t.Optional(t.String()),
242242- },
243243- {
244244- secrets: cookieSecret,
245245- sign: ['admin_session'],
246246- },
247247- ),
248248- },
249249- )
250250-251251- // Get metrics (protected)
252252- /**
253253- * GET /api/admin/metrics
254254- * Success: { overall, mainApp, hostingService, timeWindow }
255255- * Unauthorized (401): { error: 'Unauthorized' }
256256- */
257257- .get(
258258- '/metrics',
259259- async ({ query, cookie, set }) => {
260260- const check = requireAdmin({ cookie, set })
261261- if (check) return check
262262-263263- const timeWindow = query.timeWindow ? parseInt(query.timeWindow as string, 10) : 3600000 // 1 hour default
264264-265265- const mainAppStats = metricsCollector.getStats('main-app', timeWindow)
266266- const overallStats = metricsCollector.getStats(undefined, timeWindow)
267267-268268- // Get hosting service stats from its own endpoint
269269- let hostingServiceStats = {
270270- totalRequests: 0,
271271- avgDuration: 0,
272272- p50Duration: 0,
273273- p95Duration: 0,
274274- p99Duration: 0,
275275- errorRate: 0,
276276- requestsPerMinute: 0,
277277- }
278278-279279- try {
280280- const hostingServiceUrl =
281281- process.env.HOSTING_SERVICE_URL || `http://localhost:${process.env.HOSTING_PORT || '3001'}`
282282- const response = await fetch(
283283- `${hostingServiceUrl}/__internal__/observability/metrics?timeWindow=${timeWindow}`,
284284- )
285285- if (response.ok) {
286286- const data = await response.json()
287287- hostingServiceStats = data.stats
288288- }
289289- } catch (_err) {
290290- // Hosting service might not be running
291291- }
292292-293293- return {
294294- overall: overallStats,
295295- mainApp: mainAppStats,
296296- hostingService: hostingServiceStats,
297297- timeWindow,
298298- }
299299- },
300300- {
301301- cookie: t.Cookie(
302302- {
303303- admin_session: t.Optional(t.String()),
304304- },
305305- {
306306- secrets: cookieSecret,
307307- sign: ['admin_session'],
308308- },
309309- ),
310310- },
311311- )
312312-313128 // Get database stats (protected)
314129 /**
315130 * GET /api/admin/database
···365180 set.status = 500
366181 return {
367182 error: 'Failed to fetch database stats',
368368- message: error instanceof Error ? error.message : String(error),
369369- }
370370- }
371371- },
372372- {
373373- cookie: t.Cookie(
374374- {
375375- admin_session: t.Optional(t.String()),
376376- },
377377- {
378378- secrets: cookieSecret,
379379- sign: ['admin_session'],
380380- },
381381- ),
382382- },
383383- )
384384-385385- // Get cache stats (protected)
386386- /**
387387- * GET /api/admin/cache
388388- * Success: hosting service cache stats payload.
389389- * Failure (503|500): { error, message }
390390- */
391391- .get(
392392- '/cache',
393393- async ({ cookie, set }) => {
394394- const check = requireAdmin({ cookie, set })
395395- if (check) return check
396396-397397- try {
398398- const hostingServiceUrl =
399399- process.env.HOSTING_SERVICE_URL || `http://localhost:${process.env.HOSTING_PORT || '3001'}`
400400- const response = await fetch(`${hostingServiceUrl}/__internal__/observability/cache`)
401401-402402- if (response.ok) {
403403- const data = await response.json()
404404- return data
405405- } else {
406406- set.status = 503
407407- return {
408408- error: 'Failed to fetch cache stats from hosting service',
409409- message: 'Hosting service unavailable',
410410- }
411411- }
412412- } catch (error) {
413413- set.status = 500
414414- return {
415415- error: 'Failed to fetch cache stats',
416183 message: error instanceof Error ? error.message : String(error),
417184 }
418185 }
+1-6
docs/src/content/docs/architecture.md
···130130131131## Observability
132132133133-Both services expose internal endpoints:
134134-135135-- `/__internal__/observability/logs`
136136-- `/__internal__/observability/errors`
137137-- `/__internal__/observability/metrics`
138138-- `/__internal__/observability/cache` (hosting service only)
133133+Operational observability is provided through service logs and configured Grafana exporters. The old in-memory internal endpoints were removed.
139134140135See [Monitoring & Metrics](/monitoring).
+1-5
docs/src/content/docs/monitoring.md
···73737474## Without Grafana
75757676-Metrics and logs are always stored in-memory. Access them directly:
7777-7878-- `http://localhost:8000/api/observability/logs`
7979-- `http://localhost:8000/api/observability/metrics`
8080-- `http://localhost:8000/api/observability/errors`
7676+The old in-memory observability endpoints were removed. Without Grafana, use service logs and the remaining health endpoints for operational checks.
81778278## Programmatic Setup
8379