minhyung / openobserve
A library for OpenObserve
Requires
- php: ^8.3
- php-http/discovery: ^1.17
- psr/http-client-implementation: *
- psr/http-factory-implementation: *
Requires (Dev)
- guzzlehttp/guzzle: ^7.10
- monolog/monolog: ^3.0 || ^4.0
- pestphp/pest: ^4.0
Suggests
- monolog/monolog: Send logs to OpenObserve via the Monolog handler (^3.0 || ^4.0)
README
A PHP client for OpenObserve. Ships logs/metrics/traces, runs SQL searches, manages streams, and exposes the administrative APIs through a small, typed surface built on PSR-18 / PSR-17.
Installation
composer require minhyung/openobserve
The library uses PSR-18 / PSR-17 HTTP abstractions and discovers an implementation at
runtime via php-http/discovery. If your project does not already ship one, install
Guzzle (or any other implementation) alongside:
composer require guzzlehttp/guzzle
Quick start
Email + password
use Minhyung\OpenObserve\Client; $client = new Client( baseUrl: 'http://localhost:5080', email: 'root@example.com', password: 'Complexpass#123', organization: 'default', ); $client->logs()->json('app-logs', [ ['level' => 'info', 'message' => 'hello, world'], ]);
Pre-encoded token
OpenObserve exposes a "Basic auth token" in the UI — a base64 blob of email:password
you can copy without re-encoding. Use Client::withToken():
$client = Client::withToken( baseUrl: 'http://localhost:5080', token: 'YWRtaW46Q29tcGxleHBhc3MjMTIz', organization: 'default', );
The bound organization is the default for every resource accessor; pass an override
per call (e.g. $client->logs('other-org')) when you need to target a different one.
Log ingestion
Three endpoints are exposed via the Logs resource:
// _json: array of records into one stream $client->logs()->json('app-logs', [ ['_timestamp' => 1_700_000_000_000_000, 'level' => 'info', 'message' => 'one'], ['_timestamp' => 1_700_000_001_000_000, 'level' => 'warn', 'message' => 'two'], ]); // _multi: same shape but sent as NDJSON $client->logs()->multi('app-logs', $records); // _bulk: Elasticsearch-compatible, target many streams at once $client->logs()->bulk([ 'app-logs' => [['level' => 'info', 'message' => 'hi']], 'audit' => [['action' => 'login', 'user' => 'alice']], ]);
OpenTelemetry (OTLP/JSON)
$client->otlp()->logs([ 'resourceLogs' => [/* OTLP/JSON */], ], streamName: 'app-logs'); $client->otlp()->metrics(['resourceMetrics' => [/* ... */]]); $client->otlp()->traces(['resourceSpans' => [/* ... */]]);
Search
$result = $client->search()->sql( sql: 'SELECT * FROM "app-logs" WHERE level = \'error\'', startTime: 1_700_000_000_000_000, // unix microseconds endTime: 1_700_000_060_000_000, size: 100, ); foreach ($result['hits'] ?? [] as $row) { // ... }
Streams management
use Minhyung\OpenObserve\Resource\Streams; $client->streams()->list(Streams::TYPE_LOGS, fetchSchema: true); $client->streams()->get('app-logs'); $client->streams()->updateSettings('app-logs', [ 'data_retention' => 30, 'full_text_search_keys' => ['set' => ['body']], ]); $client->streams()->delete('app-logs', Streams::TYPE_LOGS, deleteAll: true);
Administrative resources
All follow the same CRUD shape: list, get, create, update, delete.
$client->functions()->create([ 'function' => 'function(row) return row end', 'order' => 1, ]); $client->users()->create([ 'email' => 'admin@example.com', 'first_name' => 'ming', 'last_name' => 'xing', 'password' => 'complex#pass', 'role' => 'admin', ]); $client->alerts()->setEnabled('high-error-rate', false); $client->alerts()->trigger('high-error-rate'); $client->dashboards()->list(folder: 'production'); $client->organizations()->list(); // server-global $client->organizations()->summary('acme'); // org-scoped stats
Monolog integration
use Minhyung\OpenObserve\Monolog\Handler; use Monolog\Level; use Monolog\Logger; $logger = new Logger('app'); $logger->pushHandler(new Handler($client, stream: 'app-logs', level: Level::Info)); $logger->info('hello world', ['user_id' => 42]);
handleBatch() is implemented so Monolog's buffering handlers (e.g.
BufferHandler, FingersCrossedHandler) send one HTTP request per flush.
Escape hatch
For endpoints not yet covered by a typed resource — or OpenObserve deployments whose URL shapes diverge from this library's defaults — drop down to the raw HTTP layer:
$client->request('POST', '/api/default/_settings', ['retention' => 30]); $client->requestRaw('POST', '/api/upload', "row1\nrow2\n", 'text/csv');
Both apply the configured base URL and Authorization header, decode JSON responses, and surface non-2xx responses as exceptions.
Errors
All library exceptions implement Minhyung\OpenObserve\Exception\OpenObserveException,
so you can catch them uniformly:
| Class | Trigger |
|---|---|
AuthenticationException |
HTTP 401 / 403 |
ApiException |
Any other 4xx / 5xx, with getStatusCode() and getResponseBody() |
TransportException |
PSR-18 client failure (network error, DNS, etc.) |
use Minhyung\OpenObserve\Exception\OpenObserveException; try { $client->logs()->json('app-logs', $records); } catch (OpenObserveException $e) { // logging, retry, etc. }
Requirements
- PHP 8.3+
- A PSR-18 HTTP client and PSR-17 message factories (autodiscovered via
php-http/discovery)
Development
composer install
composer test
Integration tests (optional)
A handful of integration tests live in tests/Integration/. They skip by default and
only run when you point them at a real OpenObserve instance through environment
variables:
OPENOBSERVE_URL=http://localhost:5080 \
OPENOBSERVE_EMAIL=root@example.com \
OPENOBSERVE_PASSWORD='Complexpass#123' \
composer test:integration
Alternatively, copy phpunit.xml.dist to phpunit.xml (gitignored) and set the
<env> values there.