logger / openobserve
PSR-3 logging for OpenObserve with memory and durable file buffers
Requires
- php: >=8.1
- ext-json: *
- psr/http-client: ^1.0
- psr/http-factory: ^1.0
- psr/http-message: ^1.0 || ^2.0
- psr/log: ^1.1 || ^2.0 || ^3.0
Requires (Dev)
- guzzlehttp/guzzle: ^7.10
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^10.5
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
PHP-Bibliothek unter MIT-Lizenz für strukturiertes Logging gegen OpenObserve Cloud und selbst gehostete Instanzen. Sie implementiert PSR-3 und lässt sich mit LoggerEssentials kombinieren.
Varianten
| Klasse | Verwendung |
|---|---|
OpenObserveLogger |
Sendet jeden Logaufruf sofort per HTTP. |
OpenObserveBufferedLogger |
Sammelt im Speicher und sendet ausschließlich bei flush(). |
OpenObserveRotatingFileLogger |
Schreibt ein JSON-Dokument pro Zeile in lokale Dateien. |
OpenObserveFileTransferService |
Überträgt neue Zeilen aus diesen Dateien, speichert seinen Fortschritt und löscht vollständig abgearbeitete, abgeschlossene Dateien. |
Alle Klassen liegen im Namespace Logger\OpenObserve und tragen das Präfix OpenObserve.
Voraussetzungen und Installation
- PHP ab 8.1, JSON-Erweiterung.
- PSR-18-HTTP-Client und PSR-17-Request-/Stream-Factories, beispielsweise Guzzle.
- Für Dateipuffer: ein lokales POSIX-Dateisystem (Linux/macOS) mit
flock(), atomaremrename()undfsync(). Kein gemeinsames NFS-Verzeichnis.
Das Paket heißt logger/openobserve. Eine Veröffentlichung in einem Composer-Repository ist nicht Bestandteil dieses Projekts. Für die lokale Einbindung diese Einträge mit der bestehenden composer.json der Anwendung zusammenführen:
{
"repositories": [
{"type": "path", "url": "/absoluter/pfad/logger-openobserve", "options": {"symlink": true}}
],
"require": {
"logger/openobserve": "@dev",
"guzzlehttp/guzzle": "^7.10"
}
}
Anschließend im Anwendungsprojekt composer update logger/openobserve guzzlehttp/guzzle ausführen. Bei Einbindung über ein internes Composer-Repository dessen Repository-Konfiguration verwenden. logger/essentials ist optional und wird von der Anwendung eingebunden.
HTTP-Anbindung für Cloud und Self-Hosting
use GuzzleHttp\Client; use GuzzleHttp\Psr7\HttpFactory; use Logger\OpenObserve\OpenObserveClient; use Logger\OpenObserve\OpenObserveConfiguration; $factory = new HttpFactory(); $client = new OpenObserveClient( client: new Client([ 'connect_timeout' => 2.0, 'timeout' => 5.0, 'allow_redirects' => false, ]), requestFactory: $factory, streamFactory: $factory, config: new OpenObserveConfiguration( serverUrl: 'https://api.openobserve.ai', organization: 'your_organization', stream: 'application', username: 'your-ingestion-user', password: 'your-ingestion-password', ), );
Für Cloud die tatsächliche Serveradresse, Organisation und Zugangsdaten aus der Ingestion-Oberfläche übernehmen; die Serveradresse kann von diesem Beispiel abweichen. Für Self-Hosting beispielsweise http://localhost:5080 und Organisation default verwenden. Ein Reverse-Proxy-Präfix wie https://logs.example.com/openobserve bleibt erhalten. Die Serveradresse enthält noch nicht /api/{organization}/_bulk.
Alternativ zu Benutzername/Passwort kann authorization: 'Basic …' mit dem vollständigen Header aus der Ingestion-Konfiguration übergeben werden. Beide Verfahren schließen sich aus. Geheimnisse aus der Anwendungskonfiguration oder Umgebung laden. Timeouts, TLS und Proxy-Einstellungen gehören zum HTTP-Client; die Bibliothek setzt keinen bestimmten Client voraus.
Die Bibliothek verwendet die Bulk-API und wertet jede Einzelantwort aus. Ein HTTP-Erfolg allein reicht nicht. Der Server muss das dokumentierte Antwortformat mit errors und items liefern; unbekannte oder widersprüchliche Antworten bleiben unbestätigt. Den tatsächlichen Streamnamen verwenden, vorzugsweise Kleinbuchstaben und Unterstriche, damit eine serverseitige Namensnormalisierung nicht zu einem abweichenden Ziel führt.
Direkt loggen
use Logger\OpenObserve\OpenObserveLogger; $logger = new OpenObserveLogger($client); $logger->info('Bestellung {order_id} importiert', ['order_id' => 123]); $logger->error('Import fehlgeschlagen', ['exception' => $exception]);
Ein Aufruf wartet auf die HTTP-Antwort. Bei Übertragungsfehlern erfolgt standardmäßig eine Diagnose über PHPs error_log(); die Anwendung läuft weiter. Der direkte Logger hält keinen dauerhaften Wiederholungspuffer vor.
Mit LoggerEssentials verwenden
Jede der drei Logger-Varianten implementiert Psr\Log\LoggerInterface. Vorhandene Filter, Formatter und Logger-Sammlungen können sie verwenden. Erweiterte Kontexte und Unterlogger lassen sich über den vorhandenen Wrapper ergänzen:
use Logger\Common\ExtendedLoggerImpl; use Logger\OpenObserve\OpenObserveLogger; $logger = new ExtendedLoggerImpl(new OpenObserveLogger($client)); $import = $logger->createSubLogger('Import', ['job_id' => 42]); $import->info('Artikel {sku} verarbeitet', ['sku' => 'ABC-123']);
Der vorhandene Logger\Wrappers\BufferedLogger aus LoggerEssentials ruft beim Leeren einzelne Logaufrufe auf. Für gemeinsame HTTP-Übertragungen den OpenObserveBufferedLogger verwenden.
Im Speicher sammeln
use Logger\OpenObserve\OpenObserveBufferedLogger; $logger = new OpenObserveBufferedLogger($client, batchSize: 100); $logger->info('Job gestartet'); $logger->info('Job abgeschlossen', ['processed' => 123]); $successful = $logger->flush();
batchSize begrenzt nur die Anzahl je HTTP-Request während flush(). Es gibt keinerlei automatischen Versand nach Anzahl, beim Skriptende oder im Destruktor. Die Anwendung muss flush() selbst einplanen.
Bei einem Fehler liefert flush() standardmäßig false. Bereits bestätigte Einträge am Anfang werden entfernt; unbestätigte Einträge bleiben mit unveränderter ID und Entstehungszeit im Speicher. getPendingCount() und getPendingRecords() machen den verbleibenden Puffer zugänglich. Auch inhaltlich abgelehnte Einträge bleiben im Speicher; die dauerhafte Aussonderung in Fehlerdateien gehört zum Datei-Service. Ein erneutes flush() ist ein neuer Übertragungsversuch. Ohne erfolgreichen Abschluss gehen verbleibende Einträge beim Prozessende verloren. Der Puffer wächst bis zur Leerung im Arbeitsspeicher.
Dateien unter ./var/log schreiben
use Logger\OpenObserve\OpenObserveFileStore; use Logger\OpenObserve\OpenObserveRotatingFileLogger; $projectRoot = '/srv/my-project'; $store = new OpenObserveFileStore( directory: $projectRoot.'/var/log', maxFileBytes: 10 * 1024 * 1024, ); $logger = new OpenObserveRotatingFileLogger($store); $logger->info('Für Übertragung vorgemerkt', ['job_id' => 42]);
Jedes Projekt auf jedem Server verwendet seinen eigenen lokalen Puffer. Den Pfad aus dem absoluten Projektverzeichnis ableiten, damit Webprozess und Hintergrundjob unabhängig von ihrem Arbeitsverzeichnis dasselbe Verzeichnis verwenden. Das Verzeichnis wird bei Bedarf angelegt. Mehrere PHP-Prozesse dürfen gleichzeitig hineinschreiben.
Rotation erfolgt beim UTC-Tageswechsel und zusätzlich nach Größe, standardmäßig 10 MiB. Eine JSON-Zeile bleibt immer vollständig; ein einzelner größerer Datensatz erhält eine eigene Datei, die größer als die Grenze sein darf. Auch der Service schließt Dateien vergangener Tage, wenn keine weiteren Einträge geschrieben werden.
Dateien mit Endung .active.jsonl werden noch beschrieben, .ready.jsonl sind abgeschlossen. Die Identität bleibt bei der Rotation gleich. Fortschritt und Sperren liegen als versteckte .openobserve-*-Dateien im selben Verzeichnis. Andere Dateien in var/log werden nicht verwaltet. Verzeichnis, Dateien und Fortschritt gemeinsam erhalten; sie sind kein Cache, der vor einer erfolgreichen Übertragung gelöscht werden darf.
Hintergrundjob pro Projekt
use Logger\OpenObserve\OpenObserveFileTransferService; $service = new OpenObserveFileTransferService( store: $store, client: $client, batchSize: 100, maxBatchBytes: 1024 * 1024, ); $result = $service->run(); // Beispiel für einen projektspezifischen CLI-Job: exit($result->error === null ? 0 : 1);
Das Projekt startet den Service selbst in der gewünschten Frequenz. Die Bibliothek richtet keinen Scheduler ein. Für jeden Aufruf werden die vorhandenen Dateien und ihre aktuellen Längen erfasst. Der Service überträgt neue vollständige Zeilen bis zu diesen Grenzen, auch aus aktiven Dateien. Später hinzugekommene Zeilen folgen beim nächsten Aufruf. Der Byte-Grenzwert wird an Datensatzgrenzen geprüft und darf um einen einzelnen Datensatz überschritten werden.
OpenObserveTransferResult enthält transferred, rejected, deletedFiles, busy und error. isSuccessful() ist wahr, wenn der Lauf nicht wegen einer Sperre ausgelassen wurde und keinen Betriebsfehler hatte; ausgesonderte Datensätze bleiben separat über rejected sichtbar. Bei einem bereits laufenden Service kehrt ein überlappender Aufruf mit busy = true zurück.
Bei Verbindungsproblemen, Serverfehlern, Authentifizierungsfehlern oder unklaren Antworten endet der Durchlauf ohne interne Wiederholungen oder Wartezeiten. Unbestätigte Einträge bleiben bis zum nächsten projektgesteuerten Aufruf erhalten. Nur vollständig abgearbeitete abgeschlossene Dateien werden gelöscht. Fortschritt wird atomar gespeichert; Dateiinhalte und Verzeichnisänderungen werden synchronisiert. Fehler beim Speichern des Fortschritts gelten als Fehler, nicht als erfolgreiche Verarbeitung.
Ablehnungen und Fehlerdiagnosen
Ein eindeutig inhaltlich abgelehnter Datensatz wird samt Serverbegründung in openobserve-rejected-<Datei-ID>-<Position>.jsonl gesichert. Erst danach darf der Service die Position überspringen und mit weiteren Einträgen fortfahren. Diese Fehlerdateien werden nicht automatisch gelöscht. Eine Datei darf entfernt werden, wenn alle Einträge bestätigt oder auf diese Weise separat gesichert wurden; ausgesonderte Einträge zählen nicht als erfolgreich übertragen.
Als inhaltliche Ablehnung gelten explizite Einzelantworten mit HTTP-Status 422 oder bekannte Parsing-Fehlertypen bei Einzelstatus 400. Ein Fehlerstatus für den gesamten Request oder eine unklare Einzelantwort genügt nicht. Beschädigte lokale vollständige Zeilen werden ebenfalls gesichert; nicht interpretierbare Originalbytes bleiben Base64-kodiert erhalten. Ein unvollständiges Ende einer aktiven Datei wird zunächst zurückgestellt. Beim nächsten Schreibvorgang wird eine solche Datei abgeschlossen, damit der beschädigte Rest separat gesichert werden kann.
Zusätzlich meldet der Service eine kompakte Diagnose: Eine Nachricht konnte wegen ihres Inhalts nicht übernommen werden. Sie enthält Grund, event_id und lokale Fundstelle. Standard ist das allgemeine PHP-Fehlerlog. Ein vorhandener Projektlogger oder separater OpenObserve-Logger wird über den Fehlerhandler angebunden:
use Logger\OpenObserve\OpenObserveErrorHandler; use Logger\OpenObserve\OpenObserveFileTransferService; $errors = new OpenObserveErrorHandler( onError: static function(Throwable $error, array $context) use ($projectLogger): void { $projectLogger->error($error->getMessage(), $context); }, ); $service = new OpenObserveFileTransferService($store, $client, $errors);
Mit throwOnError: true werden Betriebsfehler nach ihrer Meldung weitergeworfen. Konfigurationsfehler und unbekannte PSR-3-Log-Level sind Programmierfehler und werfen immer Exceptions. Erfolgreich ausgesonderte Datensätze werden gemeldet, ohne den weiteren Lauf abzubrechen.
Diagnosen sind bestmöglich zugestellt, nicht garantiert. Die lokale Fehlerdatei bleibt bei Diagnosefehlern erhalten. Rekursive Meldungen im Fehlerhandler werden unterdrückt; über diese Bibliothek erzeugte Diagnoseereignisse tragen openobserve_diagnostic = true, damit ihre spätere Ablehnung keine weiteren Ablehnungsdiagnosen erzeugt. Den abgelehnten Originalinhalt nicht erneut als Diagnosekontext übergeben.
Ereignisse, Kontext und Laufzeitdaten
Jeder ursprüngliche Logaufruf erhält eine UUIDv4 als event_id und seinen UTC-Entstehungszeitpunkt als _timestamp. Beide bleiben bei Puffern und Wiederholungen unverändert. Daneben stehen level, interpolierte message und strukturierter context.
Übergebene Exceptions enthalten Klasse, Nachricht, Code, Datei, Zeile, Stacktrace und vorherige Exceptions. Stackframes enthalten keine Funktionsargumente oder Objektinhalte. Kontext unterstützt unter anderem Arrays, öffentliche Objekteigenschaften, Stringable, JsonSerializable und Datumsobjekte. Fehlerhafte Serialisierer und zu tiefe beziehungsweise zyklische Strukturen erhalten erkennbare Platzhalter; die Normalisierungstiefe ist auf 12 begrenzt. Ungültiges UTF-8 wird ersetzt.
use Logger\OpenObserve\OpenObserveRecordFactory; $records = new OpenObserveRecordFactory( runtimeContext: true, includeQueryParameters: false, includeCommandArguments: false, defaultContext: ['application' => 'my-project', 'environment' => 'production'], );
Diese Factory kann jeder Logger-Variante als recordFactory übergeben werden. Laufzeitdaten werden beim ursprünglichen Logaufruf ermittelt: Hostname, Prozess-ID, PHP-SAPI und bei Webaufrufen HTTP-Methode und URL-Pfad, bei CLI-Aufrufen Skriptname. Queryparameter und Kommandozeilenargumente sind ausdrücklich zuschaltbar. Mit runtimeContext: false entfällt die automatische Erfassung. Pro Aufruf übergebene Kontextwerte überschreiben gleichnamige Standardwerte.
OpenObserve flacht verschachtelte Felder ab. Stabile Kontextschlüssel verwenden, beispielsweise order_id statt einer wechselnden Bestellnummer als Feldname. Feldanzahl, Schema und zulässiges Alter der Einträge hängen vom Zielserver ab. Bei lange verzögerter Übertragung muss dessen Zeitfenster zur gewünschten Aufbewahrung passen; die Bibliothek verändert den ursprünglichen Ereigniszeitpunkt nicht.
Zustellverhalten
Unbestätigte Datei-Einträge werden erneut versucht. Ist die Serverantwort verloren gegangen oder stirbt der Prozess zwischen Annahme und Fortschrittsspeicherung, können Duplikate entstehen. Bei einem Teilerfolg wird der bestätigte Anfang gespeichert; bereits angenommene Einträge hinter einem unbestätigten Eintrag können beim nächsten Lauf erneut gesendet werden.
Die stabile event_id ermöglicht das Erkennen von Duplikaten, garantiert aber keine einmalige Speicherung. Die Bibliothek behauptet keine serverseitige Idempotenz. Der Dateipuffer schützt vor Prozessneustarts und vorübergehenden Übertragungsfehlern, nicht vor dem Verlust seines Datenträgers. Unbestätigte Daten werden nicht nach Alter oder Gesamtgröße gelöscht.
Ausführbare Beispiele und Entwicklung
Im Bibliotheksverzeichnis:
composer install
composer test
composer phpstan
Die Beispiele lesen OPENOBSERVE_URL, OPENOBSERVE_ORGANIZATION, OPENOBSERVE_STREAM sowie OPENOBSERVE_USERNAME und OPENOBSERVE_PASSWORD. Alternativ OPENOBSERVE_AUTHORIZATION verwenden. Für Dateibeispiele zusätzlich OPENOBSERVE_PROJECT_DIR auf das absolute Anwendungsprojekt setzen. Die Bibliothek selbst liest keine Umgebungsvariablen; das übernehmen die Beispiele.
- Direkter Versand:
php examples/direct.php - Speicherpuffer:
php examples/buffered.php - Datei schreiben:
php examples/file.php - Projektjob für Dateiübertragung:
php examples/transfer.php - HTTP-Client konfigurieren
Tests verwenden simulierte PSR-18-Antworten und echte lokale Dateien. Sie prüfen auch parallele Prozesse, sofern pcntl vorhanden ist. Ein erfolgreicher Testlauf allein ist kein Nachweis der Anbindung an eine konkrete Cloud- oder Self-Hosting-Instanz.