Jsonnet libraries for generating base14 Scout dashboards and alerts as code.
Scout is a Grafana-derived observability platform that stores OpenTelemetry telemetry in ClickHouse. Its dashboards, folders and alert rules are declarative resources, so they can be generated, reviewed in a pull request, and applied by CI rather than edited by hand in a UI.
This library is the reusable part of doing that: query builders that know the OpenTelemetry schema, panel scaffolding that matches what Scout stores after it normalises a dashboard on save, deterministic resource naming, and a small model for scoping assets to an environment and — optionally — a tenant.
Pre-release (0.x). The API will change without deprecation cycles.
Not yet published to a tag; consume it from a local checkout or a commit SHA
until v0.1.0 lands. The mixins/ layer is scaffolded but empty.
Writing a Grafana dashboard by hand is easy. Writing four hundred of them, for several environments, so that every query filters the right environment and none of them silently return nothing, is not. The failure mode that motivates this library is that a wrong observability query does not raise an error — it renders an empty panel, or a plausible wrong number, and an alert built on it simply never fires.
So the library is opinionated in three places where mistakes are silent:
- Counters. Selecting a raw cumulative value from a counter table produces a smooth rising line that looks like data. The builders compute per-interval deltas per series instead, dropping each series' first bucket and clamping counter resets.
- Attribute placement. OpenTelemetry splits metadata across resource attributes and datapoint attributes, and the same key can live in either depending on the metric. Reading the wrong one returns an empty result with no warning, so dimension access goes through helpers rather than string concatenation.
- Scoping. Every query must filter its environment. A missing predicate on a multi-tenant deployment shows one tenant another's data.
- go-jsonnet 0.20.0 — the implementation
matters, not just the version:
0.6renders as0.59999999999999998under one jsonnet and0.6under another, so two contributors on different builds produce different bytes. - jsonnet-bundler (
jb) - Python 3.12 and uv, to run the tests
jb install github.com/base-14/scout-jsonnet-libs@mainThen put vendor/ on the jsonnet search path:
jsonnet -J vendor your-render.jsonnetA dashboard is a plain object with a build function. It receives a scope —
which supplies the environment predicate and any template variables — and
returns a Grafana dashboard.
local p = import 'github.com/base-14/scout-jsonnet-libs/core/panels.libsonnet';
local ch = import 'github.com/base-14/scout-jsonnet-libs/core/sql.libsonnet';
{
name: 'go-runtime',
kind: 'dashboard',
title: 'Go Runtime',
modes: ['browse'],
build(ctx)::
local s = ctx.scope;
p.dashboard(
title=$.title,
variables=s.variables,
panels=[
p.timeseries(1, 'Goroutines', { h: 8, w: 12, x: 0, y: 0 }, [
p.target(
s.datasourceUid,
s.database,
ch.tables.gauge,
ch.timeSeriesQuery(
s.database,
ch.tables.gauge,
'go_goroutines',
'max(Value)',
'goroutines',
ch.metricPredicates('go_goroutines') + [s.envPredicate],
),
),
]),
],
),
}s.envPredicate is the part that matters: the scope decides how an environment
is identified, so the same dashboard renders correctly whether the deployment
has a tenant dimension or not.
| Layer | Contents |
|---|---|
core/ |
Query builders, panel scaffolding, the resource envelope, naming. No opinion on how environments are identified. |
identity/ |
Scope modes and identity profiles. |
mixins/ |
Parameterised asset bundles (empty today). |
Dependencies run strictly downward. Mixins never import one another, and a mixin reaches the environment predicate only through the identity contract — so a mixin written against one deployment model works under another.
Every deployment has environments. Only some have tenants. A profile is a small object that answers, for a given scope: which predicate filters this environment, and which expression distinguishes one series from another.
| Profile | Shape |
|---|---|
environmentOnly |
One axis: an environment resource attribute. |
tenantAttribute |
environment holds the bare tier; a separate tenant attribute holds the customer. |
labelSuffix |
The tenant is folded into the environment label, e.g. staging-acme. |
A deployment whose scheme matches none of these can supply its own object
implementing the same contract. tests/test_identity_conformance.py is exported
for exactly that purpose — run it against your profile.
scoped— one asset per instance, everything pinned, no dropdowns.browse— one asset with dropdowns over the available environments.global— no environment dimension at all, for platform-level telemetry such as collector health.
global is the only mode exempt from the environment-filter requirement, which
is why it must be declared explicitly rather than inferred from a query that
happens to lack a predicate.
Scout releases are built on a specific Grafana version, which fixes the
dashboard schemaVersion, the plugin version, and the resource API versions a
render must emit. Getting them wrong does not error — Scout normalises on save,
so a mismatched render produces a permanent diff against the server on every
asset.
Declare one version and let the rest follow. The supported set lives in
core/compat.libsonnet; an unsupported version fails the render by name rather
than emitting the wrong constants.
Read this before writing a custom identity profile.
A profile decides the environment predicate. A profile that omits it produces a query which is valid, returns rows, and shows data from environments or tenants the viewer should not see. Nothing in this library can detect that — by the time the profile has run, the predicate is simply absent.
Your rendering pipeline must validate the generated SQL, not the profile:
fail any scoped or browse query that does not filter its environment. The
safety net has to sit downstream of the extension point. If you adopt custom
profiles without that check, you have removed the only thing standing between a
one-line mistake and a cross-tenant data leak.
To report a vulnerability, please use GitHub's private vulnerability reporting on this repository rather than opening a public issue.
Contributions are welcome. See CONTRIBUTING.md for the development setup, the test layout, and the rules for fixtures.
uv run --frozen pytestMIT — see LICENSE.