89 lines
3.8 KiB
TypeScript
89 lines
3.8 KiB
TypeScript
import { fleetSettings } from '../settings.js';
|
|
import { db } from '../db/index.js';
|
|
import { type TelemetryRequestLogEntry } from '../../telemetry-diagnostics.js';
|
|
|
|
const MAX_PAYLOAD_LENGTH = 16 * 1024;
|
|
// Bound storage even when requests claim many different, unregistered names.
|
|
export function telemetryRetention() {
|
|
const settings = fleetSettings();
|
|
return {
|
|
maxAgeDays: settings.telemetryKeepDays,
|
|
maxPerServer: settings.telemetryMaxPerServer,
|
|
maxTotal: settings.telemetryMaxTotal
|
|
};
|
|
}
|
|
export const telemetryCutoff = () =>
|
|
telemetryRetention().maxAgeDays
|
|
? new Date(Date.now() - telemetryRetention().maxAgeDays * 86_400_000).toISOString()
|
|
: '';
|
|
const cutoff = telemetryCutoff;
|
|
const textField = (value: unknown) => (typeof value === 'string' ? value.slice(0, 255) : null);
|
|
|
|
export function recordTelemetryRequest(input: {
|
|
value: unknown;
|
|
receivedAt: string;
|
|
status: number;
|
|
error: string | null;
|
|
contentType: string | null;
|
|
}): void {
|
|
// Use only the top-level JSON field, never a player's name or a substring.
|
|
// Malformed JSON cannot be reliably attributed to an individual server.
|
|
if (!input.value || typeof input.value !== 'object' || Array.isArray(input.value)) return;
|
|
const body = input.value as Record<string, unknown>;
|
|
if (typeof body.server !== 'string' || !body.server.trim() || body.server.length > 255) return;
|
|
const payload = JSON.stringify(body);
|
|
const handle = db();
|
|
handle.transaction(() => {
|
|
handle
|
|
.prepare(
|
|
`INSERT INTO telemetry_requests
|
|
(server, received_at, status, error, event, map, round, player_count, content_type, payload, payload_truncated)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
)
|
|
.run(
|
|
body.server,
|
|
input.receivedAt,
|
|
input.status,
|
|
input.error?.slice(0, 2000) ?? null,
|
|
textField(body.event),
|
|
textField(body.map),
|
|
Number.isSafeInteger(body.round) ? (body.round as number) : null,
|
|
Array.isArray(body.players) ? body.players.length : null,
|
|
textField(input.contentType),
|
|
payload.slice(0, MAX_PAYLOAD_LENGTH),
|
|
Number(payload.length > MAX_PAYLOAD_LENGTH)
|
|
);
|
|
if (telemetryRetention().maxAgeDays)
|
|
handle.prepare('DELETE FROM telemetry_requests WHERE received_at < ?').run(cutoff());
|
|
if (telemetryRetention().maxPerServer)
|
|
handle
|
|
.prepare(
|
|
`DELETE FROM telemetry_requests WHERE server = ? AND id <= (
|
|
SELECT id FROM telemetry_requests WHERE server = ? ORDER BY id DESC LIMIT 1 OFFSET ?
|
|
)`
|
|
)
|
|
.run(body.server, body.server, telemetryRetention().maxPerServer);
|
|
if (telemetryRetention().maxTotal)
|
|
handle
|
|
.prepare(
|
|
`DELETE FROM telemetry_requests WHERE id <= (
|
|
SELECT id FROM telemetry_requests ORDER BY id DESC LIMIT 1 OFFSET ?
|
|
)`
|
|
)
|
|
.run(telemetryRetention().maxTotal);
|
|
})();
|
|
}
|
|
|
|
export function listTelemetryRequests(server: string): TelemetryRequestLogEntry[] {
|
|
const rows = db()
|
|
.prepare(
|
|
`SELECT id, server, received_at AS receivedAt, status, error, event, map, round,
|
|
player_count AS playerCount, content_type AS contentType, payload, payload_truncated AS payloadTruncated
|
|
FROM telemetry_requests WHERE server = ? AND received_at >= ? ORDER BY id DESC LIMIT ?`
|
|
)
|
|
.all(server, cutoff(), telemetryRetention().maxPerServer || 10000) as Array<
|
|
Omit<TelemetryRequestLogEntry, 'payloadTruncated'> & { payloadTruncated: number }
|
|
>;
|
|
return rows.map((row) => ({ ...row, payloadTruncated: !!row.payloadTruncated }));
|
|
}
|