Log export (Deutsch)
1. Funktionsweise
Abschnitt betitelt „1. Funktionsweise“call_logs (SQLite) → callLogExportSource.getCallLogsForExport(cursor, batchSize) → LogExportRecord[] (der Feldsatz der Registerkarte „Logs“) → Ziel-Client.send(batch) → advanceLogExportCursor(id, lastRowId, count)- Zeitplan — ein
JobRegistry-Cronjob namenslog_export, standardmäßig mit0 * * * *(stündlich, UTC). Registriert insrc/lib/initCloudSync.ts; kann mitOMNIROUTE_LOG_EXPORT_CRONüberschrieben werden. Bei jeder Ausführung werden alle aktivierten Ziele nacheinander vollständig abgearbeitet. - Cursor — das implizite
call_logs.rowidvon SQLite, das pro Ziel inlog_export_destinations.cursor_row_idpersistiert wird.timestampwird bewusst nicht als Cursor verwendet: Aufrufer können einen eigenen Wert angeben, sodass eine langsame Anfrage nach einer schnelleren Anfrage geschrieben werden kann, die später gestartet wurde; ein Zeitstempel-Cursor würde sie überspringen. - Batchverarbeitung —
batch_sizeZeilen pro Anfrage (Standardwert 500),max_rows_per_runZeilen pro Ausführung (Standardwert 10000), damit ein großer Rückstand über mehrere Ausführungen hinweg abgearbeitet wird, anstatt eine einzelne zu blockieren. - Zustellung — der Cursor wird erst weiterbewegt, nachdem
send()erfolgreich abgeschlossen wurde. Bei einem fehlgeschlagenen Batch bleibt der Cursor unverändert, sodass dieselben Zeilen bei der nächsten Ausführung erneut versucht werden. Die Garantie besteht aus mindestens einmaliger Zustellung plus zielseitiger Deduplizierung, nicht aus echter Genau-einmal-Zustellung: BigQuery versieht jede Zeile anhand der Aufrufprotokoll-ID mit einem Schlüssel, den es innerhalb seines eigenen Deduplizierungsfensters nach dem Best-Effort-Prinzip berücksichtigt. - Schutz vor Überschneidungen — die Cronausführung und
POST .../runkönnen gleichzeitig ausgelöst werden. Ein Ziel, das bereits abgearbeitet wird, wird übersprungen, anstatt zweimal abgearbeitet zu werden (skipped: trueim Ausführungsergebnis). Dadurch kann eine parallele Ausführung weder einen Batch erneut senden noch den Cursor zurücksetzen. - Wiederherstellung nach Bereinigung — wenn
cursor_row_idüberMAX(rowid)liegt, weil die gesamte Tabelle bereinigt wurde und die Zeilen-IDs neu begonnen haben, setzt der Runner den Cursor auf 0 zurück, anstatt dauerhaft keine neuen Daten mehr zu erkennen.
Nutzdaten (Prompts und Vervollständigungen)
Abschnitt betitelt „Nutzdaten (Prompts und Vervollständigungen)“Standardmäßig enthält der Export nur die Zusammenfassungsfelder, die in der Liste der Registerkarte „Logs“ angezeigt werden. Wenn Prompts und Antworten exportieren (includeBodies) aktiviert wird, werden zusätzlich die Inhalte übertragen, die der Detailbereich der Registerkarte „Logs“ für jeden Aufruf anzeigt:
| Feld | Inhalt |
|---|---|
request_body / response_body |
Die Aufrufnutzdaten in der Darstellung des Dashboards |
pipeline_route_decision |
Das vom Router ausgewählte Ziel und Modell |
pipeline_client_request |
Die unveränderte Anfrage, wie sie vom Client gesendet wurde |
pipeline_openai_request |
Nach der Übersetzung in das interne OpenAI-Format |
pipeline_provider_request |
Wie tatsächlich an den Upstream-Dienst gesendet, im Dialekt des Anbieters |
pipeline_provider_response |
Die unveränderte Upstream-Antwort |
pipeline_client_response |
Was an den Aufrufer zurückgegeben wurde |
pipeline_error |
Fehlerdetails auf Pipeline-Ebene für einen fehlgeschlagenen Aufruf |
bodies_truncated |
Wahr, wenn eines der obigen Felder maxBodyBytes erreicht hat |
Hierbei handelt es sich um Prompt-Inhalte. Daher ist diese Option standardmäßig deaktiviert und bewusst für jedes Ziel einzeln konfigurierbar. Exportiert wird, was das Dashboard anzeigt, da beide über getCallLogById lesen: Nutzdaten werden bereits beim Schreiben von personenbezogenen Daten bereinigt und von Geheimnissen befreit. Bei einem Aufruf mit einem noLog-API-Schlüssel werden überhaupt keine Nutzdaten gespeichert, sodass nichts exportiert werden kann.
Die Nutzdaten werden für jede Zeile aus dem Dateisystemartefakt gelesen. Daher erfolgt die Anreicherung nur für Ziele, die sie angefordert haben. Wenn das Artefakt einer Zeile fehlt oder beschädigt ist, wird deren Zusammenfassung mit Null-Nutzdaten exportiert, anstatt den Batch fehlschlagen zu lassen und den Cursor zu blockieren.
maxBodyBytes (Standardwert 262144) begrenzt jedes Feld. Längere Nutzdaten werden gekürzt und nicht verworfen — ein abgeschnittener Prompt beantwortet weiterhin die Frage „Was wurde gefragt?“ — und die Zeile wird mit bodies_truncated gekennzeichnet. Chunkweise gestreamte Deltas werden nicht exportiert; die zusammengesetzte Antwort ist bereits in pipeline_provider_response und pipeline_client_response enthalten.
2. Dateien
Abschnitt betitelt „2. Dateien“| Komponente | Speicherort |
|---|---|
| Zielvertrag | src/lib/logExport/types.ts |
| Registry | src/lib/logExport/registry.ts |
| Verwaltung von Secrets | src/lib/logExport/secrets.ts |
| Runner (Cursor-Schleife) | src/lib/logExport/runner.ts |
| API-Projektion | src/lib/logExport/presenter.ts |
| BigQuery-Ziel | src/lib/logExport/destinations/bigquery.ts |
| Google-SA-Authentifizierung | src/lib/logExport/googleServiceAccount.ts |
| Anrufprotokollquelle | src/lib/usage/callLogExportSource.ts |
| Persistenz | src/lib/db/logExportDestinations.ts |
| Cronjob | src/lib/jobs/logExportJob.ts |
| REST-Schicht | src/app/api/log-export/ |
| Dashboard-Seite | src/app/(dashboard)/dashboard/log-export/ |
Schema: src/lib/db/migrations/170_log_export_destinations.sql.
3. REST-API
Abschnitt betitelt „3. REST-API“Alle Routen sind durch die Verwaltungs-Authentifizierung geschützt (requireManagementAuth). Secrets werden niemals zurückgegeben:
Ein gespeichertes Secret wird als Literal __stored__ zurückgegeben. Wird dieser Wert bei einer Aktualisierung zurückgesendet,
bleiben die gespeicherten Anmeldedaten erhalten.
Das Erstellen oder Aktualisieren eines Ziels, dessen Typ ein Secret deklariert, erfordert
STORAGE_ENCRYPTION_KEY. Ohne diesen Schlüssel führt encrypt() stillschweigend keine Verschlüsselung durch. Daher wird der Schreibvorgang
mit Status 400 abgelehnt, statt Anmeldedaten im Klartext in SQLite abzulegen (dieselbe Schutzmaßnahme
verwendet auch der Telegram-Webhook).
| Methode | Pfad | Zweck |
|---|---|---|
GET |
/api/log-export/types |
Zieltypen und Liste ihrer Konfigurationsfelder |
GET |
/api/log-export/destinations |
Ziele auflisten (Secrets unkenntlich gemacht) |
POST |
/api/log-export/destinations |
Ein Ziel erstellen |
GET |
/api/log-export/destinations/{id} |
Ein Ziel abrufen |
PUT |
/api/log-export/destinations/{id} |
Name/Aktivierungsstatus/Konfiguration/Batchverarbeitung aktualisieren |
DELETE |
/api/log-export/destinations/{id} |
Löschen |
POST |
/api/log-export/destinations/{id}/test |
Anmeldedaten prüfen, ohne Daten zu schreiben |
POST |
/api/log-export/destinations/{id}/run |
Jetzt abarbeiten, über denselben Pfad wie der geplante Lauf |
GET |
/api/log-export/status |
Cron-Status, letzte Läufe und Rückstand pro Ziel |
GET /api/log-export/types ermöglicht die generische Benutzeroberfläche: Das Dashboard-Formular wird anhand
der zurückgegebenen Feldbeschreibungen gerendert, sodass für ein neues Ziel keine Änderung an der Benutzeroberfläche erforderlich ist.
4. BigQuery-Ziel
Abschnitt betitelt „4. BigQuery-Ziel“Konfigurationsschlüssel (type: "bigquery"):
| Schlüssel | Hinweise |
|---|---|
projectId |
GCP-Projekt, das den Datensatz enthält |
datasetId |
[A-Za-z0-9_]+ |
tableId |
[A-Za-z0-9_]+ |
location |
Wird nur verwendet, wenn der Datensatz erstellt werden muss (Standardwert EU) |
serviceAccountJson |
Dienstkontoschlüssel. Secret: im Ruhezustand verschlüsselt, wird niemals zurückgegeben |
autoCreate |
Datensatz und Tabelle beim ersten Export erstellen (Standardwert true) |
Das Dienstkonto benötigt bigquery.tables.updateData für die Zieltabelle sowie
bigquery.datasets.create / bigquery.tables.create, wenn autoCreate aktiviert ist.
Ein konfigurierter Batch ist eine Cursor-Einheit, keine HTTP-Einheit: send() unterteilt ihn in insertAll-Aufrufe
mit jeweils höchstens 500 Zeilen, sodass ein großer batch_size das BigQuery-Anforderungslimit von 10 MB nicht überschreiten kann.
Bei vorübergehenden Statuscodes (408/429/500/502/503/504) werden bis zu drei Wiederholungsversuche mit exponentiellem
Backoff durchgeführt, wobei dieselben insertIds wiederverwendet werden. Authentifizierungs- und Schemafehler lösen bereits beim ersten Versuch eine Ausnahme aus,
statt den Lauf mit weiteren Versuchen zu belasten.
Eine erst vor wenigen Augenblicken erstellte Tabelle ist für den Streaming-Endpunkt noch nicht sichtbar, der für einige Sekunden mit 404 antwortet. Dieser 404-Fehler wird erneut versucht, jedoch nur, wenn die Tabelle in diesem Lauf erstellt wurde — eine tatsächlich fehlende Tabelle führt weiterhin sofort zu einem Fehler. Beachten Sie, dass BigQuery Streaming-Einfügungen für mehrere Minuten verweigert, wenn eine Tabelle unter einem kürzlich gelöschten Namen neu erstellt wird. Dies ist eine Eigenschaft des Löschens und anschließenden Neuerstellens; verwenden Sie daher vorzugsweise einen neuen Tabellennamen, statt eine Tabelle zu löschen und erneut hinzuzufügen.
Ein partieller Fehler wird als HTTP 200 mit einem nicht leeren insertErrors[]-Array zurückgegeben. Dies wird als
Fehler behandelt und löst eine Ausnahme aus. Dadurch wird verhindert, dass der Cursor über Zeilen hinaus fortschreitet, die BigQuery nie
akzeptiert hat; tests/unit/log-export-bigquery.test.ts schreibt dieses Verhalten fest.
Für die Übertragung wird reines REST verwendet: Eine selbstsignierte RS256-Assertion wird unter
https://oauth2.googleapis.com/token gegen ein Zugriffstoken ausgetauscht; anschließend werden die Zeilen an tabledata.insertAll gesendet. Es wird kein Google SDK
eingebunden. Zugriffstoken werden pro (Dienstkonto, Scope) prozessintern zwischengespeichert.
Die erstellte Tabelle enthält eine Spalte für jedes Feld der Registerkarte „Logs“ sowie exported_at und ist darauf ausgelegt,
wie Anrufprotokolle tatsächlich abgefragt werden:
- Tagespartitionierung nach
timestamp, sodass eine auf ein Datum begrenzte Abfrage nur die betreffenden Tage scannt. - Clustering nach
api_key_name,provider,model,status(in dieser Reihenfolge), sodass das Filtern danach, wer den Aufruf ausgeführt hat, wohin er ging oder ob er fehlgeschlagen ist, Blöcke innerhalb jeder Partition ausschließt. BigQuery erlaubt höchstens vier Clustering-Spalten, und die Reihenfolge ist relevant: Ein Filter allein nachapi_key_nameschließt Blöcke aus, ein Filter allein nachstatushingegen nicht. - Optionale Aufbewahrungsdauer für Partitionen über
partitionExpirationDays(0behält alles), die beim Erstellen der Tabelle angewendet wird.
Beide Einstellungen werden zum Erstellungszeitpunkt angewendet. Eine vorhandene Tabelle behält ihr bestehendes Layout bei. Verweisen Sie daher auf eine neue Tabellen-ID als Ziel, wenn Sie diese Einstellungen übernehmen möchten.
tests/unit/log-export-bigquery.test.ts stellt sicher, dass der Mapper und das Tabellenschema synchron bleiben, sodass eine neue Spalte im Aufrufprotokoll beim Export nicht unbemerkt verworfen werden kann.
Batches werden sowohl nach Zeilenanzahl als auch nach serialisierter Größe in Byte aufgeteilt. Die Zeilenanzahl allein reicht nicht aus, sobald Nutzdaten exportiert werden: 500 Zeilen mit Prompts können mehrere zehn Megabyte groß sein, und insertAll lehnt Anfragen über 10 MB ab. Chunks werden bei 500 Zeilen oder 9 MB abgeschlossen, je nachdem, welcher Grenzwert zuerst erreicht wird.
5. Hinzufügen eines Ziels
Abschnitt betitelt „5. Hinzufügen eines Ziels“- Erstellen Sie
src/lib/logExport/destinations/<name>.ts, das einenLogExportDestinationTypeexportiert: ein Zod-configSchema, einfields-Deskriptor-Array für die Benutzeroberfläche,secretFieldssowie einecreateClient(config)-Funktion, dietest()/prepare()/send(records)zurückgibt. - Fügen Sie es dem
DESTINATIONS-Array insrc/lib/logExport/registry.tshinzu. - Schreiben Sie Tests unter
tests/unit/.
Das ist die gesamte Änderung: Persistenz, Cronjob, REST-Schicht, Verschlüsselung geheimer Daten und das Dashboard-Formular lesen alle die Registry aus.
Für ein neues Ziel gelten zwei Regeln:
send()muss bei einem teilweisen Fehlschlag eine Ausnahme auslösen. Eine erfolgreiche Rückgabe bedeutet: „Das Ziel enthält diese Zeilen“, und der Cursor wird dauerhaft über sie hinaus verschoben.- Ein Ziel, das eine vom Benutzer bereitgestellte URL akzeptiert, muss diese vor dem Abruf mit
parseAndValidateWebhookUrl(src/shared/network/outboundUrlGuardPolicy.ts) validieren, genauso wie Webhooks. Für BigQuery ist dies nicht erforderlich: Dessen Hosts sind Konstanten.
6. Betrieb
Abschnitt betitelt „6. Betrieb“- Dashboard: Integrationen → Logexport. Fügen Sie ein Ziel hinzu, führen Sie Testen aus, um die Anmeldedaten zu prüfen, ohne Zeilen zu schreiben, und aktivieren Sie es anschließend.
- Rückstand: Jede Zielkarte zeigt die ausstehenden Zeilen und den Cursor an;
GET /api/log-export/statusgibt dieselben Werte sowie die letzten 20 Jobausführungen zurück. - Ein fehlschlagendes Ziel führt nicht zum Fehlschlag der anderen — die Ausführungszusammenfassung zeichnet den Status jedes Ziels
in
last_status/last_errorauf, und der Verlauf der Jobausführungen enthält das Gesamtergebnis. - Beim Löschen eines Ziels wird auch dessen Cursor gelöscht. Wird es erneut hinzugefügt, beginnt es beim ältesten noch vorgehaltenen
Aufrufprotokoll, wodurch Zeilen erneut gesendet werden, die das Ziel möglicherweise bereits enthält. Bei BigQuery verhindert die zeilenbezogene
insertIddies nur innerhalb des BigQuery-eigenen Deduplizierungszeitfensters. Daher sollten Sie ein Ziel vorzugsweise deaktivieren, statt es zu löschen.
HagiCode
HagiCode ist ein agentischer Coding-Arbeitsplatz mit strukturierten Workflows, Multi-Agent-Ausführung und Hero-Dungeon-Ansichten.
Mit einem intelligenteren, schnelleren und unterhaltsameren agentischen Workflow wird aus Ideen nutzbare Software.

- SmartStrukturierte Workflows machen aus Absichten einen umsetzbaren Weg von der Idee bis zur Auslieferung.
- EfficientMulti-Agent-Workflows führen Recherche, Umsetzung und Prüfung parallel aus.
- FunHero Dungeon macht lange Coding-Sitzungen anschaulich und gemeinschaftlich.