An idiomatic, business-oriented Ruby wrapper over the OpenObserve REST API. It is a thin,
hand-written layer on top of the openobserve-api
transport gem, exposing a search-and-alerting working set and absorbing the raw API's
rough edges so they never reach the caller.
# Gemfile
gem 'openobserve-client', git: 'https://github.com/jbox-web/openobserve-client.git'Secrets are never hard-coded or committed — only supplied through the environment:
| Variable | Purpose |
|---|---|
OPENOBSERVE_BASE_URL |
e.g. https://oo.example.org |
OPENOBSERVE_USER |
account e-mail |
OPENOBSERVE_PASSWORD |
account password |
OPENOBSERVE_ORG |
organization; optional, defaults to default |
One instance targets one OpenObserve instance and one organization; there is no global singleton, so several may coexist.
require 'openobserve-client'
oo = OpenObserve.new # from ENV
oo = OpenObserve::Client.new( # explicit
base_url: 'https://oo.example.org',
user: 'root@example.com',
password: 'xxxxxxxx',
organization: 'acme'
)An OpenObserve served by a private authority — step-ca in a container stack, say — needs its
CA declared, the default trust store holding public ones only. ssl reaches Faraday verbatim:
oo = OpenObserve::Client.new(
base_url: 'https://openobserve.internal:8443',
user: 'root@example.com',
password: 'xxxxxxxx',
ssl: { ca_file: '/path/to/root.crt' }
)Without it every request dies on certificate verify failed (unable to get local issuer certificate).
SQL plus a window. No microseconds, no request models, no hits envelope to dig through.
oo.search.all(sql: "SELECT * FROM canary WHERE level = 'error'", last: 3600)
# => [#<OpenObserve::Hit>, ...] — auto-paginated across pages
result = oo.search.run(sql: 'SELECT * FROM canary', from: Time.now - 86_400)
result.hits # => Array<OpenObserve::Hit>
result.total # => server-side match count, not the page size
result.took # => milliseconds
oo.search.each(sql: 'SELECT * FROM canary', last: 60).lazy.first(10)
oo.search.around(stream: 'canary', key: '1700000000000000', size: 10)
oo.search.values(stream: 'canary', fields: %w[level], last: 3600)The window accepts Time, Date, a parseable String or epoch seconds, on either
from:/to: or a trailing last: in seconds. A search with no lower bound is refused
rather than left to scan everything, and an inverted window raises instead of quietly
returning nothing.
oo.streams.names # => ["canary", ...]
oo.streams.schema('canary')
oo.alerts.list(stream: 'canary', enabled: true)
oo.alerts.enable(id) / oo.alerts.disable(id) / oo.alerts.trigger(id)
oo.templates.list / oo.templates.prebuilt
oo.destinations.create(name: 'hook', url: '...', method: 'post', type: 'http', template: 'Default')
oo.incidents.list(status: 'firing')
oo.incidents.total
oo.dashboards.find_by_title('SLO')
oo.dashboards.sync('config/dashboards') # idempotent directory synchronisationUploaded so RUM turns a minified browser stack trace back into readable file names. OpenObserve
applies an archive only to events whose service/env/version triplet matches the one
declared here, and nothing validates the match: a mismatched upload answers 200 and symbolicates
nothing, so the caller owns the correspondence with what the browser SDK reports.
Enterprise only. POST /api/{org}/sourcemaps is absent from the OpenAPI document this gem
is written against — the pinned spec publishes the open source surface, and source map upload is
not part of it. The route is documented separately, and OpenObserve serves it on the Enterprise
build alone, to a token carrying the sourcemaps RBAC permission. On an open source build
("build_type": "opensource" in GET /config), the calls below raise rather than upload; the
usual answer without a licence is to keep the function names readable at build time (esbuild's
keepNames, Terser's keep_fnames) instead of symbolicating after the fact.
oo.sourcemaps.upload('maps.zip', service: 'web', env: 'production', version: '9.2.0')
# Same archive once per service — several instances commonly serve the same compiled assets
# while the SDK reports a distinct service name for each.
oo.sourcemaps.upload_all('maps.zip', services: %w[web-blue web-green], env: 'production', version: '9.2.0')Every failure — configuration, business validation, or a wrapped transport error — is
raised as (a subclass of) OpenObserve::Client::Error, so callers never rescue transport
classes directly. The original transport error is preserved as #cause.
| Class | Raised on |
|---|---|
Unauthorized |
401 — the credential is wrong or missing |
Forbidden |
403 — authenticated, but the role does not allow this operation |
NotFound |
404 on a single object by id or name |
Conflict |
409 — the write carried a stale revision; re-read and retry |
Error |
anything else |
An empty list is a 200 with an empty array, never an error: unlike the Dolibarr
stack, OpenObserve carries no empty-list-is-404 quirk, so a 404 on a listing stays loud
because it means a wrong path or a missing organization.
| Rough edge in the raw API | What the wrapper does |
|---|---|
org_id required on 114 of 150 paths |
carried by the client, never repeated by the caller |
Basic auth to build by hand and pass as api_key: |
built internally from user/password |
_search needs SearchSQLRequest wrapping SearchQuery |
one call taking SQL and a window |
| Timestamps in microseconds | Time, Date, String or epoch seconds |
hits envelope, from/size paging |
SearchResult, plus auto-paginating each/all |
| Alert CRUD on v2, templates and destinations on v1 | one domain per notion, version invisible |
folder: trailing on nearly every v2 alert operation |
a client-level default |
Write parameters named after response schemas (list_destinations200_response_inner:) |
plain keyword arguments |
Streams, Alerts, V2 unreachable from the transport Client |
resolved by the Resource base |
Use the committed binstubs in bin/ (no bundle exec). Regenerate them after
bundle install with bundle binstubs rspec-core rubocop rake yard.
bin/rspec # unit suite — network-free and write-free
bin/rubocop # lint (must pass in CI)
bin/rake # default task == spec
bin/rake spec:e2e # opt-in end-to-end suite (needs Docker)
bin/yard # YARD docs into doc/ (reads .yardopts)Mise tasks mirror these for CI and scripts: mise dev:deps, dev:spec, dev:e2e,
dev:docs.
Unit specs run without network: the transport connection is stubbed. An opt-in
end-to-end suite (spec/e2e, armed by OPENOBSERVE_E2E, which rake spec:e2e sets)
boots a disposable dockerized OpenObserve and drives the real thing: ingest, search it
back, list streams, and create then delete a template and a destination. It writes only to
that throwaway instance and owns its own compose lifecycle.
MIT — see LICENSE.