Files
electric-cherry/src/routes/api/telemetry/+server.ts
T

89 lines
4.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { json } from '@sveltejs/kit';
import { getServer, listServers } from '$lib/server/fleet/servers.js';
import { saveTelemetry } from '$lib/server/fleet/telemetry.js';
import { emit } from '$lib/server/pipeline/bus.js';
import { db } from '$lib/server/db/index.js';
import { acceptTestSignal, parseTestSignal } from '$lib/server/fleet/beta-tests.js';
// Reporters fetch this once at process startup and keep the token for that boot.
export async function GET({ url }: { url: URL }) {
const server = url.searchParams.get('server');
if (!server || !getServer(server)) return json({ error: 'Unknown server' }, { status: 404 });
const runs = db().prepare(`SELECT r.id AS runId, r.beta_id AS betaId FROM beta_test_runs r
JOIN betas b ON b.id = r.beta_id JOIN beta_test_plans p ON p.beta_id = b.id
WHERE r.server = ? AND r.retired_at IS NULL AND b.state IN ('running','ready-promote')
AND p.outcome IN ('active','passed')`).all(server);
return json({ runs }, { headers: { 'Cache-Control': 'no-store' } });
}
export async function POST({ request }: { request: Request }) {
console.log('[telemetry] received POST /api/telemetry');
// Encode payloads as JSON so script-supplied newlines cannot forge log lines.
// Bound the preview to keep malformed requests from flooding container logs.
const raw = await request.text();
const context = {
contentType: request.headers.get('content-type'),
payloadPreview: raw.slice(0, 4096),
payloadTruncated: raw.length > 4096
};
function reject(status: number, error: string, details: Record<string, unknown> = {}) {
console.log(`[telemetry] rejected ${status}: ${JSON.stringify({ error, ...context, ...details })}`);
return json({ ok: false, error, details }, { status });
}
if (request.headers.get('content-type')?.split(';')[0].trim().toLowerCase() !== 'application/json') {
return reject(415, 'Content-Type must be application/json', { receivedContentType: context.contentType });
}
let body;
try {
body = JSON.parse(raw);
} catch (error) {
return reject(400, 'Malformed JSON', { parseError: (error as Error).message });
}
if (!body || typeof body !== 'object' || Array.isArray(body)) {
return reject(400, 'Payload must be a JSON object');
}
const issues: string[] = [];
if (body.runId === undefined && body.event !== 'round_change') issues.push('Session telemetry requires runId');
if (typeof body.server !== 'string' || !body.server.length || body.server.length > 255) {
issues.push('server must be a registered server name (1–255 characters)');
}
for (const field of ['round', 'players']) {
if (!Number.isSafeInteger(body[field]) || body[field] < 0) {
issues.push(`${field} must be a nonnegative safe integer (JSON number, not a string)`);
}
}
if (issues.length) {
return reject(400, 'Invalid telemetry payload', { issues });
}
const server = getServer(body.server);
if (!server) {
const registeredServers = listServers().filter((entry) => !entry.destroyed_at).map((entry) => entry.name);
return reject(404, 'Server is not registered', {
receivedServer: body.server,
hint: 'Send the exact Electric Cherry server name in the server field; matching is case-sensitive.',
registeredServers
});
}
if (server.destroyed_at) {
return reject(404, 'Server has been destroyed', {
receivedServer: body.server, destroyedAt: server.destroyed_at
});
}
let test: { accepted: boolean; reason?: string } | undefined;
if (body.runId !== undefined) {
try {
const signal = parseTestSignal(body);
test = acceptTestSignal(body.server, body.runId, body.eventId, signal);
} catch (error) { return reject(400, (error as Error).message); }
if (body.event !== 'round_change') return json({ ok: true, test });
}
const telemetry = {
server: body.server as string, round: body.round as number,
players: body.players as number, receivedAt: new Date().toISOString()
};
saveTelemetry(telemetry);
console.log(`[telemetry] accepted round_change ${JSON.stringify(telemetry)}`);
emit({ type: 'server-telemetry', entries: [telemetry] });
return json({ ok: true, telemetry, test });
}