mirror of
https://github.com/complexcaresolutions/cms.c2sgmbh.git
synced 2026-03-17 20:54:11 +00:00
feat(monitoring): add REST API endpoints for monitoring dashboard
Add 7 API route handlers for the monitoring system: - GET /api/monitoring/health - system health overview - GET /api/monitoring/services - individual service status checks - GET /api/monitoring/performance - performance metrics with period filter - GET /api/monitoring/alerts - paginated alert history with severity filter - POST /api/monitoring/alerts/acknowledge - acknowledge alerts - GET /api/monitoring/logs - paginated logs with level/source/date filters - GET /api/monitoring/snapshots - time-series data for charts All endpoints require super-admin authentication. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
0615b22188
commit
5f38136b95
8 changed files with 528 additions and 0 deletions
42
src/app/(payload)/api/monitoring/alerts/acknowledge/route.ts
Normal file
42
src/app/(payload)/api/monitoring/alerts/acknowledge/route.ts
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
|
import { getPayload } from 'payload'
|
||||||
|
import config from '@payload-config'
|
||||||
|
|
||||||
|
export async function POST(req: NextRequest): Promise<NextResponse> {
|
||||||
|
try {
|
||||||
|
const payload = await getPayload({ config })
|
||||||
|
const { user } = await payload.auth({ headers: req.headers })
|
||||||
|
|
||||||
|
if (!user || !(user as any).isSuperAdmin) {
|
||||||
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = await req.json()
|
||||||
|
const { alertId } = body
|
||||||
|
|
||||||
|
if (!alertId) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'alertId is required' },
|
||||||
|
{ status: 400 },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const updated = await payload.update({
|
||||||
|
collection: 'monitoring-alert-history',
|
||||||
|
id: alertId,
|
||||||
|
data: {
|
||||||
|
acknowledgedBy: user.id,
|
||||||
|
resolvedAt: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return NextResponse.json({ data: updated })
|
||||||
|
} catch (error: unknown) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: error instanceof Error ? error.message : 'Unknown error' },
|
||||||
|
{ status: 500 },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const dynamic = 'force-dynamic'
|
||||||
45
src/app/(payload)/api/monitoring/alerts/route.ts
Normal file
45
src/app/(payload)/api/monitoring/alerts/route.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
|
import { getPayload } from 'payload'
|
||||||
|
import config from '@payload-config'
|
||||||
|
|
||||||
|
export async function GET(req: NextRequest): Promise<NextResponse> {
|
||||||
|
try {
|
||||||
|
const payload = await getPayload({ config })
|
||||||
|
const { user } = await payload.auth({ headers: req.headers })
|
||||||
|
|
||||||
|
if (!user || !(user as any).isSuperAdmin) {
|
||||||
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const page = parseInt(req.nextUrl.searchParams.get('page') || '1', 10)
|
||||||
|
const limit = parseInt(req.nextUrl.searchParams.get('limit') || '20', 10)
|
||||||
|
const severity = req.nextUrl.searchParams.get('severity')
|
||||||
|
|
||||||
|
const where: Record<string, unknown> = {}
|
||||||
|
if (severity) {
|
||||||
|
where.severity = { equals: severity }
|
||||||
|
}
|
||||||
|
|
||||||
|
const alerts = await payload.find({
|
||||||
|
collection: 'monitoring-alert-history',
|
||||||
|
where,
|
||||||
|
page,
|
||||||
|
limit: Math.min(limit, 100),
|
||||||
|
sort: '-createdAt',
|
||||||
|
})
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
data: alerts.docs,
|
||||||
|
totalDocs: alerts.totalDocs,
|
||||||
|
page: alerts.page,
|
||||||
|
totalPages: alerts.totalPages,
|
||||||
|
})
|
||||||
|
} catch (error: unknown) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: error instanceof Error ? error.message : 'Unknown error' },
|
||||||
|
{ status: 500 },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const dynamic = 'force-dynamic'
|
||||||
29
src/app/(payload)/api/monitoring/health/route.ts
Normal file
29
src/app/(payload)/api/monitoring/health/route.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
|
import { getPayload } from 'payload'
|
||||||
|
import config from '@payload-config'
|
||||||
|
import { checkSystemHealth } from '@/lib/monitoring/monitoring-service'
|
||||||
|
|
||||||
|
export async function GET(req: NextRequest): Promise<NextResponse> {
|
||||||
|
try {
|
||||||
|
const payload = await getPayload({ config })
|
||||||
|
const { user } = await payload.auth({ headers: req.headers })
|
||||||
|
|
||||||
|
if (!user || !(user as any).isSuperAdmin) {
|
||||||
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const health = await checkSystemHealth()
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
data: health,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
})
|
||||||
|
} catch (error: unknown) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: error instanceof Error ? error.message : 'Unknown error' },
|
||||||
|
{ status: 500 },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const dynamic = 'force-dynamic'
|
||||||
64
src/app/(payload)/api/monitoring/logs/route.ts
Normal file
64
src/app/(payload)/api/monitoring/logs/route.ts
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
|
import { getPayload } from 'payload'
|
||||||
|
import config from '@payload-config'
|
||||||
|
|
||||||
|
export async function GET(req: NextRequest): Promise<NextResponse> {
|
||||||
|
try {
|
||||||
|
const payload = await getPayload({ config })
|
||||||
|
const { user } = await payload.auth({ headers: req.headers })
|
||||||
|
|
||||||
|
if (!user || !(user as any).isSuperAdmin) {
|
||||||
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const page = parseInt(req.nextUrl.searchParams.get('page') || '1', 10)
|
||||||
|
const limit = parseInt(req.nextUrl.searchParams.get('limit') || '50', 10)
|
||||||
|
const level = req.nextUrl.searchParams.get('level')
|
||||||
|
const source = req.nextUrl.searchParams.get('source')
|
||||||
|
const search = req.nextUrl.searchParams.get('search')
|
||||||
|
const from = req.nextUrl.searchParams.get('from')
|
||||||
|
const to = req.nextUrl.searchParams.get('to')
|
||||||
|
|
||||||
|
const conditions: Record<string, unknown>[] = []
|
||||||
|
|
||||||
|
if (level) {
|
||||||
|
conditions.push({ level: { equals: level } })
|
||||||
|
}
|
||||||
|
if (source) {
|
||||||
|
conditions.push({ source: { equals: source } })
|
||||||
|
}
|
||||||
|
if (search) {
|
||||||
|
conditions.push({ message: { contains: search } })
|
||||||
|
}
|
||||||
|
if (from) {
|
||||||
|
conditions.push({ createdAt: { greater_than_equal: from } })
|
||||||
|
}
|
||||||
|
if (to) {
|
||||||
|
conditions.push({ createdAt: { less_than_equal: to } })
|
||||||
|
}
|
||||||
|
|
||||||
|
const where = conditions.length > 0 ? { and: conditions } : {}
|
||||||
|
|
||||||
|
const logs = await payload.find({
|
||||||
|
collection: 'monitoring-logs',
|
||||||
|
where,
|
||||||
|
page,
|
||||||
|
limit: Math.min(limit, 100),
|
||||||
|
sort: '-createdAt',
|
||||||
|
})
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
data: logs.docs,
|
||||||
|
totalDocs: logs.totalDocs,
|
||||||
|
page: logs.page,
|
||||||
|
totalPages: logs.totalPages,
|
||||||
|
})
|
||||||
|
} catch (error: unknown) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: error instanceof Error ? error.message : 'Unknown error' },
|
||||||
|
{ status: 500 },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const dynamic = 'force-dynamic'
|
||||||
46
src/app/(payload)/api/monitoring/performance/route.ts
Normal file
46
src/app/(payload)/api/monitoring/performance/route.ts
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
|
import { getPayload } from 'payload'
|
||||||
|
import config from '@payload-config'
|
||||||
|
import { performanceTracker } from '@/lib/monitoring/performance-tracker'
|
||||||
|
|
||||||
|
const VALID_PERIODS = ['1h', '6h', '24h', '7d'] as const
|
||||||
|
type Period = (typeof VALID_PERIODS)[number]
|
||||||
|
|
||||||
|
function isValidPeriod(value: string): value is Period {
|
||||||
|
return (VALID_PERIODS as readonly string[]).includes(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function GET(req: NextRequest): Promise<NextResponse> {
|
||||||
|
try {
|
||||||
|
const payload = await getPayload({ config })
|
||||||
|
const { user } = await payload.auth({ headers: req.headers })
|
||||||
|
|
||||||
|
if (!user || !(user as any).isSuperAdmin) {
|
||||||
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const period = req.nextUrl.searchParams.get('period') || '1h'
|
||||||
|
|
||||||
|
if (!isValidPeriod(period)) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: `Invalid period. Valid: ${VALID_PERIODS.join(', ')}` },
|
||||||
|
{ status: 400 },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const metrics = performanceTracker.getMetrics(period)
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
data: metrics,
|
||||||
|
period,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
})
|
||||||
|
} catch (error: unknown) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: error instanceof Error ? error.message : 'Unknown error' },
|
||||||
|
{ status: 500 },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const dynamic = 'force-dynamic'
|
||||||
64
src/app/(payload)/api/monitoring/services/route.ts
Normal file
64
src/app/(payload)/api/monitoring/services/route.ts
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
|
import { getPayload } from 'payload'
|
||||||
|
import config from '@payload-config'
|
||||||
|
import {
|
||||||
|
checkRedis,
|
||||||
|
checkPostgresql,
|
||||||
|
checkPgBouncer,
|
||||||
|
checkSmtp,
|
||||||
|
checkOAuthTokens,
|
||||||
|
checkCronJobs,
|
||||||
|
checkQueues,
|
||||||
|
} from '@/lib/monitoring/monitoring-service'
|
||||||
|
|
||||||
|
function resolveSettled<T>(result: PromiseSettledResult<T>, fallback: T): T {
|
||||||
|
if (result.status === 'fulfilled') {
|
||||||
|
return result.value
|
||||||
|
}
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function GET(req: NextRequest): Promise<NextResponse> {
|
||||||
|
try {
|
||||||
|
const payload = await getPayload({ config })
|
||||||
|
const { user } = await payload.auth({ headers: req.headers })
|
||||||
|
|
||||||
|
if (!user || !(user as any).isSuperAdmin) {
|
||||||
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const [redis, postgresql, pgbouncer, smtp, oauth, cronJobs, queues] =
|
||||||
|
await Promise.allSettled([
|
||||||
|
checkRedis(),
|
||||||
|
checkPostgresql(),
|
||||||
|
checkPgBouncer(),
|
||||||
|
checkSmtp(),
|
||||||
|
checkOAuthTokens(),
|
||||||
|
checkCronJobs(),
|
||||||
|
checkQueues(),
|
||||||
|
])
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
data: {
|
||||||
|
redis: resolveSettled(redis, { status: 'offline' }),
|
||||||
|
postgresql: resolveSettled(postgresql, { status: 'offline' }),
|
||||||
|
pgbouncer: resolveSettled(pgbouncer, { status: 'offline' }),
|
||||||
|
smtp: resolveSettled(smtp, { status: 'offline' }),
|
||||||
|
oauth: resolveSettled(oauth, {
|
||||||
|
metaOAuth: { status: 'error' },
|
||||||
|
youtubeOAuth: { status: 'error' },
|
||||||
|
}),
|
||||||
|
cronJobs: resolveSettled(cronJobs, {}),
|
||||||
|
queues: resolveSettled(queues, {}),
|
||||||
|
},
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
})
|
||||||
|
} catch (error: unknown) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: error instanceof Error ? error.message : 'Unknown error' },
|
||||||
|
{ status: 500 },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const dynamic = 'force-dynamic'
|
||||||
57
src/app/(payload)/api/monitoring/snapshots/route.ts
Normal file
57
src/app/(payload)/api/monitoring/snapshots/route.ts
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
|
import { getPayload } from 'payload'
|
||||||
|
import config from '@payload-config'
|
||||||
|
|
||||||
|
const PERIOD_MS: Record<string, number> = {
|
||||||
|
'1h': 3_600_000,
|
||||||
|
'6h': 21_600_000,
|
||||||
|
'24h': 86_400_000,
|
||||||
|
'7d': 604_800_000,
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function GET(req: NextRequest): Promise<NextResponse> {
|
||||||
|
try {
|
||||||
|
const payload = await getPayload({ config })
|
||||||
|
const { user } = await payload.auth({ headers: req.headers })
|
||||||
|
|
||||||
|
if (!user || !(user as any).isSuperAdmin) {
|
||||||
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const period = req.nextUrl.searchParams.get('period') || '24h'
|
||||||
|
const ms = PERIOD_MS[period]
|
||||||
|
|
||||||
|
if (!ms) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
error: `Invalid period. Valid: ${Object.keys(PERIOD_MS).join(', ')}`,
|
||||||
|
},
|
||||||
|
{ status: 400 },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const since = new Date(Date.now() - ms).toISOString()
|
||||||
|
|
||||||
|
const snapshots = await payload.find({
|
||||||
|
collection: 'monitoring-snapshots',
|
||||||
|
where: {
|
||||||
|
timestamp: { greater_than: since },
|
||||||
|
},
|
||||||
|
limit: 1000,
|
||||||
|
sort: 'timestamp',
|
||||||
|
})
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
data: snapshots.docs,
|
||||||
|
totalDocs: snapshots.totalDocs,
|
||||||
|
period,
|
||||||
|
})
|
||||||
|
} catch (error: unknown) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: error instanceof Error ? error.message : 'Unknown error' },
|
||||||
|
{ status: 500 },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const dynamic = 'force-dynamic'
|
||||||
181
src/app/(payload)/api/monitoring/stream/route.ts
Normal file
181
src/app/(payload)/api/monitoring/stream/route.ts
Normal file
|
|
@ -0,0 +1,181 @@
|
||||||
|
import { NextRequest } from 'next/server'
|
||||||
|
import { getPayload } from 'payload'
|
||||||
|
import config from '@payload-config'
|
||||||
|
import { checkSystemHealth } from '@/lib/monitoring/monitoring-service'
|
||||||
|
import { performanceTracker } from '@/lib/monitoring/performance-tracker'
|
||||||
|
|
||||||
|
const MAX_DURATION_MS = 25_000
|
||||||
|
const CYCLE_INTERVAL_MS = 5_000
|
||||||
|
const HEALTH_INTERVAL_MS = 10_000
|
||||||
|
const PERFORMANCE_INTERVAL_MS = 30_000
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats a named SSE event. Named events allow clients to listen selectively
|
||||||
|
* via `eventSource.addEventListener('health', ...)` rather than processing
|
||||||
|
* everything through the generic `onmessage` handler.
|
||||||
|
*/
|
||||||
|
function formatSSE(event: string, data: unknown): string {
|
||||||
|
return `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/monitoring/stream
|
||||||
|
*
|
||||||
|
* Server-Sent Events endpoint that pushes monitoring data to the admin
|
||||||
|
* dashboard in real time. Emits named events: connected, health,
|
||||||
|
* performance, alert, log, error, and reconnect.
|
||||||
|
*
|
||||||
|
* Requires super-admin authentication. The stream runs for up to 25 seconds
|
||||||
|
* before asking the client to reconnect (Vercel 30-second limit).
|
||||||
|
*/
|
||||||
|
export async function GET(request: NextRequest): Promise<Response> {
|
||||||
|
const payload = await getPayload({ config })
|
||||||
|
const { user } = await payload.auth({ headers: request.headers })
|
||||||
|
|
||||||
|
if (!user || !(user as Record<string, unknown>).isSuperAdmin) {
|
||||||
|
return new Response('Unauthorized', { status: 401 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const encoder = new TextEncoder()
|
||||||
|
let lastAlertCheck = new Date()
|
||||||
|
let lastLogCheck = new Date()
|
||||||
|
let lastHealthSend = 0
|
||||||
|
let lastPerfSend = 0
|
||||||
|
|
||||||
|
const stream = new ReadableStream({
|
||||||
|
async start(controller) {
|
||||||
|
const startTime = Date.now()
|
||||||
|
|
||||||
|
controller.enqueue(
|
||||||
|
encoder.encode(formatSSE('connected', { timestamp: new Date().toISOString() })),
|
||||||
|
)
|
||||||
|
|
||||||
|
while (Date.now() - startTime < MAX_DURATION_MS) {
|
||||||
|
try {
|
||||||
|
const now = Date.now()
|
||||||
|
|
||||||
|
if (now - lastHealthSend >= HEALTH_INTERVAL_MS) {
|
||||||
|
const health = await checkSystemHealth()
|
||||||
|
controller.enqueue(encoder.encode(formatSSE('health', health)))
|
||||||
|
lastHealthSend = now
|
||||||
|
}
|
||||||
|
|
||||||
|
if (now - lastPerfSend >= PERFORMANCE_INTERVAL_MS) {
|
||||||
|
const perf = performanceTracker.getMetrics('1h')
|
||||||
|
controller.enqueue(encoder.encode(formatSSE('performance', perf)))
|
||||||
|
lastPerfSend = now
|
||||||
|
}
|
||||||
|
|
||||||
|
lastAlertCheck = await emitNewAlerts(payload, controller, encoder, lastAlertCheck)
|
||||||
|
lastLogCheck = await emitNewLogs(payload, controller, encoder, lastLogCheck)
|
||||||
|
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, CYCLE_INTERVAL_MS))
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[MonitoringSSE] Error:', error)
|
||||||
|
controller.enqueue(
|
||||||
|
encoder.encode(
|
||||||
|
formatSSE('error', {
|
||||||
|
message: 'Internal error',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
controller.enqueue(
|
||||||
|
encoder.encode(formatSSE('reconnect', { timestamp: new Date().toISOString() })),
|
||||||
|
)
|
||||||
|
controller.close()
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return new Response(stream, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'text/event-stream',
|
||||||
|
'Cache-Control': 'no-cache, no-transform',
|
||||||
|
Connection: 'keep-alive',
|
||||||
|
'X-Accel-Buffering': 'no',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Queries recently created alerts and pushes each one as a named "alert" event.
|
||||||
|
* Returns the updated checkpoint timestamp.
|
||||||
|
*/
|
||||||
|
async function emitNewAlerts(
|
||||||
|
payload: Awaited<ReturnType<typeof getPayload>>,
|
||||||
|
controller: ReadableStreamDefaultController,
|
||||||
|
encoder: TextEncoder,
|
||||||
|
since: Date,
|
||||||
|
): Promise<Date> {
|
||||||
|
try {
|
||||||
|
const result = await payload.find({
|
||||||
|
collection: 'monitoring-alert-history',
|
||||||
|
where: { createdAt: { greater_than: since.toISOString() } },
|
||||||
|
limit: 10,
|
||||||
|
sort: '-createdAt',
|
||||||
|
})
|
||||||
|
|
||||||
|
if (result.docs.length > 0) {
|
||||||
|
for (const alert of result.docs) {
|
||||||
|
controller.enqueue(encoder.encode(formatSSE('alert', alert)))
|
||||||
|
}
|
||||||
|
return new Date()
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Alert collection query failed; skip this cycle
|
||||||
|
}
|
||||||
|
|
||||||
|
return since
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Queries recent warn/error/fatal log entries and pushes each one as a
|
||||||
|
* named "log" event. Returns the updated checkpoint timestamp.
|
||||||
|
*/
|
||||||
|
async function emitNewLogs(
|
||||||
|
payload: Awaited<ReturnType<typeof getPayload>>,
|
||||||
|
controller: ReadableStreamDefaultController,
|
||||||
|
encoder: TextEncoder,
|
||||||
|
since: Date,
|
||||||
|
): Promise<Date> {
|
||||||
|
try {
|
||||||
|
const result = await payload.find({
|
||||||
|
collection: 'monitoring-logs',
|
||||||
|
where: {
|
||||||
|
and: [
|
||||||
|
{ createdAt: { greater_than: since.toISOString() } },
|
||||||
|
{
|
||||||
|
or: [
|
||||||
|
{ level: { equals: 'warn' } },
|
||||||
|
{ level: { equals: 'error' } },
|
||||||
|
{ level: { equals: 'fatal' } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
limit: 20,
|
||||||
|
sort: '-createdAt',
|
||||||
|
})
|
||||||
|
|
||||||
|
if (result.docs.length > 0) {
|
||||||
|
for (const log of result.docs) {
|
||||||
|
controller.enqueue(encoder.encode(formatSSE('log', log)))
|
||||||
|
}
|
||||||
|
return new Date()
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Log collection query failed; skip this cycle
|
||||||
|
}
|
||||||
|
|
||||||
|
return since
|
||||||
|
}
|
||||||
|
|
||||||
|
export const dynamic = 'force-dynamic'
|
||||||
|
export const maxDuration = 30
|
||||||
Loading…
Reference in a new issue