openobserve-client

CI docs License: MIT

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.

Installation

# Gemfile
gem 'openobserve-client', git: 'https://github.com/jbox-web/openobserve-client.git'

Configuration (environment variables)

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

Usage

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.

Streams, alerting, dashboards

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 synchronisation

Source maps

Uploaded 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')

Errors

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.

What this gem absorbs

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

Development

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.

Tests

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.

License

MIT — see LICENSE.