Ecosystem integrations — Grafana, Prometheus, CMDB, cloud metrics
What this is
probectl is built to slot into the observability stack you already run, not to demand you rip it out and start over. Three integrations make that real:
- Optional Grafana compatibility lets an operator who already runs Grafana query probectl as if it were a Prometheus. probectl does not bundle, require, or pin a Grafana runtime; native Dashboards and Explorer remain the complete product surfaces.
- Prometheus (the de-facto open-source metrics database, which collects by scraping — periodically fetching metrics over HTTP) either scrapes metrics out of probectl (federation — one metrics system serving selected series for another to scrape) or pushes metrics into it (remote-write — Prometheus's standard push protocol).
- ServiceNow or NetBox CMDB correlation links probectl incidents and assets to your existing configuration items (a CMDB is a configuration management database — the organization's asset inventory; a CI, configuration item, is one tracked asset in it).
- Cloud metric import turns local/exported AWS CloudWatch, Azure Monitor, and Google Cloud Monitoring rows into tenant-scoped probectl metrics without polling cloud APIs.
The metrics surfaces live in internal/promapi; the CMDB clients live in
internal/cmdb; the local cloud metric importer lives in
cmd/probectl-cloud-metrics and internal/cloudmetrics. The native Terraform
provider lives in cmd/terraform-provider-probectl and
internal/terraformprovider.
%%{init: {'theme':'base','themeVariables':{'background':'#0d1117','primaryColor':'#161b22','primaryTextColor':'#e6edf3','primaryBorderColor':'#3b82f6','lineColor':'#8b949e','secondaryColor':'#21262d','tertiaryColor':'#0d1117','clusterBkg':'#161b22','clusterBorder':'#30363d','fontFamily':'ui-monospace, SFMono-Regular, Menlo, monospace'},'flowchart':{'curve':'basis','nodeSpacing':55,'rankSpacing':55,'padding':12}}}%%
flowchart LR
G[Grafana] -- "Prometheus datasource API\n/v1/grafana/api/v1/*" --> P[probectl control plane]
Prom[Prometheus] -- "scrape /v1/prometheus/federate" --> P
Ext[external Prometheus / agents] -- "remote-write /v1/prometheus/write" --> P
Cloud[local cloud metric exports] -- "probectl-cloud-metrics\nremote-write" --> P
P -- "read-only CMDB lookups (TLS)" --> SN[ServiceNow / NetBox CMDB]
P --- T[(TSDB: probectl_* series)]
The tenant boundary (read this first)
The dangerous part of exposing a metrics query API is that a query language is powerful enough to ask for anyone's data. probectl closes that hole by enforcing tenant first, then RBAC (role-based access control — the caller's permission set) on every surface here (the tenant-isolation rule in the Non-negotiables):
- Only plain series selectors are accepted —
metric{label="value",...}, a metric name plus label filters. The rest of PromQL (Prometheus's full query language) — functions and operators — is rejected outright, because a query probectl cannot fully parse is a query it cannot tenant-scope. (The parser ininternal/promapi/selector.goreturns an explicit error for anything beyond a selector.) - The tenant is forced, not trusted. Whatever
tenant_idmatcher the caller wrote is removed, and a singletenant_id="<caller's tenant>"equality is injected (ForceTenant) — the bank teller ignores whatever account number you wrote on the slip and uses the one on your ID. InPROBECTL_TSDB_MODE=prometheusmode, only the canonical reconstructed selector is forwarded upstream — never the caller's raw text. - Remote-write payloads are untrusted: size/series/sample/label caps apply,
and every incoming sample's
tenant_idlabel is forced to the caller's tenant. - RBAC sits on top: reads need
metrics.read, remote-write needsmetrics.write, CMDB lookups needcmdb.read(permissions added in migration0022_metrics_cmdb_permissions.sql).
Grafana datasource
probectl exposes a Prometheus-compatible API subset at /v1/grafana, so you add
it to an existing, operator-owned Grafana as a Prometheus datasource — no
plugin to install. This is optional interoperability. It is not part of the
probectl deployment, release artifact, or commercial license:
- Connections → Data sources → Add → Prometheus.
- URL:
https://<probectl>/v1/grafana. Set the HTTP method to POST. - Attach credentials for a probectl principal holding
metrics.read(in dev mode, none needed). - "Save & test" — probectl answers Grafana's
buildinfoand1+1health probes.
Provisioning-as-code lives at
deploy/grafana/provisioning/datasources/probectl.yml.
The available endpoints, all under /v1/grafana/api/v1/: query, query_range
(GET and form-POST, the way Grafana actually sends them), series, labels,
label/{name}/values, status/buildinfo, metadata. Range queries return the
raw stored samples in the window (no step interpolation) — use Grafana
transformations for any client-side math. The metric catalog is the
probectl_* namespace (results, devices, flows, BGP, threat — whatever the
pipelines land in the TSDB).
Two modes: with the in-memory TSDB (lightweight mode) queries evaluate
in-process; with PROBECTL_TSDB_MODE=prometheus the canonical selector is
forwarded to the backing Prometheus/VictoriaMetrics and the response passes
through.
Prometheus federation (probectl → Prometheus)
GET /v1/prometheus/federate?match[]=<selector> serves the latest sample
of every matching series in the Prometheus text exposition format (the
plain-text name{labels} value lines a Prometheus scrape expects) — drop it
into a Prometheus scrape config:
scrape_configs:
- job_name: probectl
honor_labels: true
metrics_path: /v1/prometheus/federate
params:
"match[]": ["{__name__=~\"probectl_.*\"}"]
scheme: https
static_configs: [{ targets: ["probectl.example.com"] }]
Cardinality guard: cardinality is the number of distinct series — every
unique label combination is one more. A scrape matching more than the series cap
(DefaultMaxSeries, 5000) fails closed with an explicit error rather than
melting the scraper — narrow the selector. This is the thing to watch for when
federating: an over-broad match[] is rejected on purpose, not silently
truncated.
Prometheus remote-write (external → probectl)
POST /v1/prometheus/write accepts the standard snappy-compressed protobuf
WriteRequest (snappy — a fast compression format; protobuf — a compact binary
encoding; together, the remote-write wire format), so an existing Prometheus
(or vmagent / Grafana Alloy) can push metrics into probectl:
remote_write:
- url: https://probectl.example.com/v1/prometheus/write
# credentials for a principal holding metrics.write
Ingested samples land in probectl's TSDB tenant-tagged (the tenant_id is forced
to the caller's tenant on decode) and immediately become queryable and alertable
just like native series.
Cloud metric import
probectl-cloud-metrics is the local connector for cloud metric exports. It
accepts newline-delimited JSON records from the operator's own export pipeline
and posts snappy-compressed Prometheus remote-write to the self-hosted control
plane:
export PROBECTL_API_URL=https://probectl.example.com
export PROBECTL_TENANT=00000000-0000-0000-0000-000000000001
export PROBECTL_API_TOKEN="$TOKEN"
probectl-cloud-metrics \
-provider aws_cloudwatch_export \
-file /var/lib/probectl/imports/cloudwatch.jsonl
Provider values are aws_cloudwatch_export, azure_monitor_export, and
gcp_cloud_monitoring_export. The importer never fetches AWS, Azure, or Google
APIs by itself. Any tenant_id field in the input file is ignored; the emitted
remote-write request is authenticated and tenant-bound by the API header, and
the control plane forces tenant_id again when decoding remote-write.
For aws_cloudwatch_export the importer accepts both the native CloudWatch
Metric Streams JSON that Kinesis Firehose delivers — a numeric
epoch-millisecond timestamp and a statistic-set value object
({"max","min","sum","count"}) — and the simpler hand-shaped form (a quoted
RFC3339 or numeric-string timestamp and a scalar value). A statistic set is
reduced to a single sample: by default the average (sum / count), the
representative central value over the aggregation window and the same
first-choice aggregation the Azure decoder uses; it falls back to sum, then
maximum, minimum, and count for a partial set. The statistic actually used
is recorded on the aggregation label of the emitted series.
CMDB correlation
This links probectl's view of the network to your system of record for assets. It is read-only: probectl looks up CIs and never writes to the CMDB. Configure either provider via environment variables:
export PROBECTL_CMDB_PROVIDER=servicenow
export PROBECTL_CMDB_URL=https://acme.service-now.com
export PROBECTL_CMDB_SECRET='integration-user:password' # env only, never logged
# optional: PROBECTL_CMDB_TABLE=cmdb_ci PROBECTL_CMDB_CACHE_TTL=10m
export PROBECTL_CMDB_PROVIDER=netbox
export PROBECTL_CMDB_URL=https://netbox.example.com
export PROBECTL_CMDB_SECRET="$NETBOX_READ_TOKEN" # env only, never logged
Surfaces:
GET /v1/cmdb/lookup?key=<ip|hostname>— direct lookup.GET /v1/incidents/{id}/cis— the incident's target plus its signal targets, resolved tenant-scoped and correlated to CIs with deep links.GET /v1/agents/{id}/ci— asset correlation by agent hostname.
Behavior: ServiceNow lookups hit the Table API with an encoded disjunction
query (ip_address=<k>^ORfqdn=<k>^ORname=<k> — "match the key as an IP, or a
fully-qualified domain name, or a name"). NetBox lookups hit the IPAM,
device, and VM API endpoints for the same canonical key. Both providers are
capped at 10 CIs per lookup (maxCIsPerLookup) over verified TLS (the
PROBECTL_CMDB_URL must be HTTPS; plain http is allowed only for loopback test
instances).
Results — including misses — are TTL-cached (each cache entry expires after its
time-to-live), so a down CMDB serves stale cache
and never breaks core function — the same read-only, cached, degrade-gracefully
discipline probectl applies to every external source. Keys are canonicalized
(case, ports, schemes), and non-keys (CIDR prefixes, free text) are dropped
before a query is ever made.
Multi-tenant note: the CMDB endpoint and credential are deployment-level — one CMDB connection for the install. Correlation requests, however, are tenant-scoped: a caller can only correlate its own tenant's incidents and agents. Per-tenant CMDB configurations would ride the per-tenant secrets work and are not part of this integration today.
Testing
go test ./internal/promapi ./internal/cmdb ./internal/cloudmetrics ./internal/terraformprovider ./internal/control ./cmd/probectl-cloud-metrics ./cmd/terraform-provider-probectl covers the
strict selector grammar (including injection attempts), tenant forcing,
instant/range/labels/series evaluation, cardinality caps, federation exposition,
remote-write decode limits plus tenant forcing, the full Grafana request
sequence against a seeded TSDB (renders plus cross-tenant leak canaries), the
RBAC route declarations and their 401s, the ServiceNow and NetBox
client/resolver paths against httptest doubles (cache, stale-serve, negative
cache, correlation), the cloud metric importer through the same remote-write
decoder served by /v1/prometheus/write, and the Terraform provider's
tenant-scoped CRUD/header behavior against httptest doubles.