grafana-api
Low-level REST client for the Grafana HTTP API,
100% generated by openapi-generator (-g ruby-nextgen)
from the instance's own OpenAPI document. A generic gem with no business logic: multi-instance,
Faraday, Zeitwerk,
per-resource namespaced sub-clients.
- Verified target: Grafana 13.0.1+security-01 (OpenAPI 3.0.3, 212 paths, 475 schemas, 15 tags) → 486 files, 406 models, 30 top-level sub-clients, 40 nested ones, 391 operations.
- Pinned and committed spec:
versions/grafana-rest.v13.0.1.json, served by the instance itself as a static asset on/public/openapi3.json(Swagger UI on/openapi3; the Swagger 2.0 flavour of the same document is on/public/api-merged.json). - Scope: the classic
/api/*surface only. The Kubernetes-style apiserver (/apis, 13 groups) publishes 39 separate OpenAPI documents and is deliberately out of scope.
⚠️
lib/is entirely generated — never edit it by hand. Every change goes through the spec +mise run generate. The only hand-maintained files are listed in.openapi-generator-ignore(README, CLAUDE.md,mise.toml,.rubocop.yml,versions/,.github/,.gitignore,spec/e2e/,docker-compose.e2e.yml).
Installation
# Gemfile
gem 'grafana-api', git: 'https://github.com/jbox-web/grafana-api.git'
bundle install
Configuration (environment variables)
Secrets are never hard-coded or committed — only supplied through the environment:
| Variable | Purpose |
|---|---|
GRAFANA_BASE_URL |
e.g. https://grafana.example.org/api |
GRAFANA_TOKEN |
service account token (preferred) |
GRAFANA_USER |
account login, for HTTP Basic |
GRAFANA_PASSWORD |
account password, for HTTP Basic |
Usage
require 'grafana-api'
client = Grafana::Api::Client.new(
base_url: ENV.fetch('GRAFANA_BASE_URL'),
username: ENV.fetch('GRAFANA_USER'),
password: ENV.fetch('GRAFANA_PASSWORD')
)
response = client.folders.list
response.status # => 200
response.data # => Array<Grafana::Api::Models::FolderSearchHit>
The /api prefix belongs to base_url
The document declares servers: [{ url: "/api" }], so every generated path is bare
(/folders, not /api/folders). The transport resolves request paths against base_url, so
that prefix has to be part of it:
Grafana::Api::Client.new(base_url: 'https://grafana.example.org/api') # ✅
Grafana::Api::Client.new(base_url: 'https://grafana.example.org') # ❌ every call 404s
A base_url without /api reaches Grafana's frontend router, which answers 404 on everything —
a failure mode that looks like a missing resource rather than a misconfiguration. Smoothing this
over is the job of the higher-level grafana-client gem.
Authentication — both schemes work
Unlike OpenObserve, every one of Grafana's authenticated operations declares both security
schemes, basic and api_key. Since the transport applies exactly the schemes the called
operation declares, either credential works with no hand-built header:
# service account token (preferred): sent as a raw `Authorization` header value
Grafana::Api::Client.new(base_url: url, api_key: "Bearer #{ENV.fetch('GRAFANA_TOKEN')}")
# HTTP Basic: applied natively by the transport
Grafana::Api::Client.new(base_url: url, username: user, password: password)
Sub-client shape
Sub-clients follow the URL segments, not the OpenAPI tags — and since the /api prefix lives
in servers, there is no extra hop to absorb (contrast with openobserve-api, where everything
lands on client.api):
client.folders.list
client.dashboards.db(dashboard: ...) # POST /api/dashboards/db
client.datasources.list
client.annotations.list(from: ..., to: ...)
client.ds.query(...) # POST /api/ds/query
client.search.list(query: 'cpu')
client.v1.provisioning_alert_rules # GET /api/v1/provisioning/alert-rules
client.health.list # GET /api/health
Reaching the nested resources
Deeper URL segments generate their own classes (40 of them: Dashboards::Uid,
Datasources::Uid, Datasources::Name, Teams::Members, Org::Users, Reports::Images, …)
but the generator emits no accessor for them on their parent — the same is true of
openobserve-api and dolibarr-api. Instantiate them with the client's connection:
dashboards = Grafana::Api::Dashboards::Uid.new(client.connection)
dashboards.get(uid: 'abc123') # GET /api/dashboards/uid/{uid}
datasources = Grafana::Api::Datasources::Uid.new(client.connection)
datasources.health(uid: 'xyz789') # GET /api/datasources/uid/{uid}/health
Four accessors that raise — use the nested class instead
client.cloudmigration, client.dashboard, client.groupsync and client.public raise
NoMethodError: undefined method 'new' for module …. Those four URL segments carry no operation
of their own (there is no GET /api/dashboard, only /api/dashboard/snapshots), so the segment
exists solely as a Zeitwerk namespace — yet the generator still emits an accessor that calls
.new on it. It is a generator bug this spec happens to expose, not a Grafana quirk.
Every operation underneath remains reachable through its nested class:
Grafana::Api::Dashboard::Snapshots.new(client.connection).list
Grafana::Api::Cloudmigration::Migration.new(client.connection).list
Grafana::Api::Groupsync::Groups.new(client.connection).list
Grafana::Api::Public::Dashboards.new(client.connection).get(...)
Alerting is provisioning-only
The document covers /api/v1/provisioning/* — alert rules, contact points, notification
policies, mute timings and templates — and nothing else. Silences and the alerts' runtime state
(/api/alertmanager/…, /api/ruler/…, /api/prometheus/…) exist in Grafana but are absent
from its OpenAPI document, so they are absent here too. That is a limit of the source, not a
choice made by this gem.
Regeneration
mise run generate # patch a throwaway copy of the pinned spec + purge lib/ + generate
mise run build # generate + format
mise run dev:spec # spec suite
mise run spec:fetch # re-fetch the pinned spec from a disposable Grafana container
Generator.
ruby-nextgenis upstream in openapi-generator since 7.24.0. The version is pinned byOPENAPI_GENERATOR_VERSIONinmise.tomland cross-checked by thegeneratetask. Install it locally withbrew install openapi-generator; CI downloads the released jar — see.github/workflows/regenerate.yml.
As in openobserve-api, mise run format is not a no-op: the generator emits one
autocorrectable Style/IfUnlessModifier offense in lib/grafana-api/configuration.rb, so
dependencies must be installed before mise run build. bin/rubocop then reports zero offenses
across the whole project.
One transformation, applied to a throwaway copy
versions/grafana-rest.v13.0.1.json is the byte-for-byte document the server serves and is
never edited. But it cannot be fed to the generator untouched: Grafana publishes one
malformed path, /reports/images/:image, carrying a Go-style route parameter that leaked out of
its go-swagger annotation instead of OpenAPI's {image}. The generator derives method names from
path segments, so it emits def images_:image — not valid Ruby, which breaks eager loading,
RuboCop and YARD across the entire gem. The same defect is present in the Swagger 2.0 document,
so it comes from Grafana, not from a format conversion.
The generate task therefore rewrites that one path — and declares the path parameter, which
Grafana omits entirely — on a temporary copy, leaving the committed file verbatim. The weekly
regenerate workflow stays meaningful: a diff there still means the server changed. The step
becomes a no-op the day Grafana fixes its annotation.
To bump the pinned Grafana version, edit GRAFANA_VERSION / GRAFANA_IMAGE_TAG in mise.toml,
then:
mise run spec:fetch # fetch the spec from the newly pinned image
mise run build
Tests
- Generated network-free unit specs (model round-trips, sub-client resolution): 1288 examples.
- An e2e canary (
mise run e2e) that spins up a disposable Grafana, drives a folder through create / read / list / delete against the live REST API and asserts the delete really took effect, then tears everything down. Opt-in: the spec self-gates onGRAFANA_E2E.
Known noise
Loading lib/grafana-api/models/provisioned_alert_rule.rb prints one Ruby warning:
warning: character class has '-' without escape: /^[a-zA-Z0-9-_]+$/
The regex is copied verbatim from the document's pattern keyword. It is Grafana's to fix; this
gem does not rewrite spec-supplied patterns.
License
MIT — see LICENSE.