Add per-server telemetry request diagnostics
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import { getServer } from '$lib/server/fleet/servers.js';
|
||||
import { listTelemetryRequests } from '$lib/server/fleet/telemetry-requests.js';
|
||||
import { TELEMETRY_REQUEST_RETENTION } from '$lib/telemetry-diagnostics.js';
|
||||
|
||||
export async function GET({ params }: { params: { name: string } }) {
|
||||
if (!getServer(params.name)) return json({ ok: false, error: 'no such server' }, { status: 404 });
|
||||
return json({ ok: true, requests: listTelemetryRequests(params.name), retention: TELEMETRY_REQUEST_RETENTION }, {
|
||||
headers: { 'Cache-Control': 'no-store' }
|
||||
});
|
||||
}
|
||||
@@ -2,26 +2,51 @@ import { json } from '@sveltejs/kit';
|
||||
import { parseGameEvent, type GameTelemetry } from '$lib/telemetry.js';
|
||||
import { getServer } from '$lib/server/fleet/servers.js';
|
||||
import { saveTelemetry } from '$lib/server/fleet/telemetry.js';
|
||||
import { recordTelemetryRequest } from '$lib/server/fleet/telemetry-requests.js';
|
||||
import { recordGameEvent } from '$lib/server/fleet/beta-tests.js';
|
||||
import { db } from '$lib/server/db/index.js';
|
||||
import { emit } from '$lib/server/pipeline/bus.js';
|
||||
|
||||
export async function POST({ request }: { request: Request }) {
|
||||
if (request.headers.get('content-type')?.split(';')[0].trim().toLowerCase() !== 'application/json') {
|
||||
return json({ ok: false, error: 'Content-Type must be application/json' }, { status: 415 });
|
||||
}
|
||||
const receivedAt = new Date().toISOString();
|
||||
const contentType = request.headers.get('content-type');
|
||||
let value: unknown;
|
||||
let malformed = false;
|
||||
try { value = await request.json(); }
|
||||
catch { return json({ ok: false, error: 'Malformed JSON' }, { status: 400 }); }
|
||||
catch { malformed = true; }
|
||||
|
||||
function log(status: number, error: string | null): void {
|
||||
try { recordTelemetryRequest({ value, receivedAt, status, error, contentType }); }
|
||||
catch (error) {
|
||||
// Diagnostics must not turn a saved report into a failed request.
|
||||
console.error('Could not record telemetry request diagnostics', error);
|
||||
}
|
||||
}
|
||||
function reject(status: number, error: string): Response {
|
||||
log(status, error);
|
||||
return json({ ok: false, error }, { status });
|
||||
}
|
||||
// Even a wrong content type may carry readable JSON with a server name.
|
||||
if (contentType?.split(';')[0].trim().toLowerCase() !== 'application/json') {
|
||||
return reject(415, 'Content-Type must be application/json');
|
||||
}
|
||||
if (malformed) return reject(400, 'Malformed JSON');
|
||||
let telemetry: GameTelemetry;
|
||||
try { telemetry = { ...parseGameEvent(value), receivedAt: new Date().toISOString() }; }
|
||||
catch (error) { return json({ ok: false, error: (error as Error).message }, { status: 400 }); }
|
||||
const server = getServer(telemetry.server);
|
||||
if (!server || server.destroyed_at) return json({ ok: false, error: 'Server is not registered or has been destroyed' }, { status: 404 });
|
||||
db().transaction(() => {
|
||||
saveTelemetry(telemetry);
|
||||
recordGameEvent(telemetry);
|
||||
})();
|
||||
emit({ type: 'server-telemetry', entries: [telemetry] });
|
||||
try { telemetry = { ...parseGameEvent(value), receivedAt }; }
|
||||
catch (error) { return reject(400, (error as Error).message); }
|
||||
try {
|
||||
const server = getServer(telemetry.server);
|
||||
if (!server || server.destroyed_at) return reject(404, 'Server is not registered or has been destroyed');
|
||||
db().transaction(() => {
|
||||
saveTelemetry(telemetry);
|
||||
recordGameEvent(telemetry);
|
||||
})();
|
||||
emit({ type: 'server-telemetry', entries: [telemetry] });
|
||||
} catch (error) {
|
||||
console.error('Could not process telemetry', error);
|
||||
log(500, `Could not process telemetry: ${error instanceof Error ? error.message : String(error)}`);
|
||||
return json({ ok: false, error: 'Could not process telemetry' }, { status: 500 });
|
||||
}
|
||||
log(200, null);
|
||||
return json({ ok: true, telemetry });
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import RoundTelemetry from '$ui/components/RoundTelemetry.svelte';
|
||||
import TelemetryDiagnostics from '$ui/components/TelemetryDiagnostics.svelte';
|
||||
import { pollVisible } from '$ui/poll.js';
|
||||
import { fleet, fleetServers, fleetError, fleetIntegrations, refreshFleet } from '$ui/stores/fleet.js';
|
||||
import { freshServer, fleetIdentity } from '$lib/fleet-overview.js';
|
||||
@@ -284,7 +285,7 @@
|
||||
<Tabs {tabs} bind:value={activeTab} ariaLabel="Server sections" idPrefix="server-tabs" panelId="server-panel" />
|
||||
</div>
|
||||
|
||||
<div class="tab-panel" role="tabpanel" id="server-panel" aria-labelledby={"server-tabs-" + activeTab}>
|
||||
<div class="tab-panel" class:diagnostics={activeTab === 'diagnostics'} role="tabpanel" id="server-panel" aria-labelledby={"server-tabs-" + activeTab}>
|
||||
{#if tabError}<div class="ui-error" role="alert">{tabError}<Button size="sm" onclick={() => loadTab(name, activeTab)}>Retry</Button></div>{/if}
|
||||
{#if activeTab === 'scoreboard'}
|
||||
<Card padding="lg">
|
||||
@@ -328,6 +329,9 @@
|
||||
</Card>
|
||||
<ListFooter total={inventory.length} bind:limit={inventoryLimit} label="files" />
|
||||
{:else if activeTab === 'diagnostics'}
|
||||
{#key name}
|
||||
<TelemetryDiagnostics server={name} />
|
||||
{/key}
|
||||
<Card padding="lg">
|
||||
<div class="panel-head">
|
||||
<h2>RCON diagnostic</h2>
|
||||
@@ -575,6 +579,10 @@
|
||||
}
|
||||
|
||||
/* Tabs row */
|
||||
.tab-panel.diagnostics {
|
||||
display: grid;
|
||||
gap: var(--space-6);
|
||||
}
|
||||
.tabs-row {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
Reference in New Issue
Block a user