Developer integrations
Developer integrations
Telemetry UI isn't just a finished dashboard — it's a toolkit. A panel is a plain PHP class with a query engine, chart helpers and scope/tenancy already wired in, so a new panel is a few lines, not a project. This page is the map; the linked pages go deep.
You can:
- Add panels to any page (or a whole new page / "module").
- Replace or remove built-in panels and pages (white-label the dashboard).
- Declare dimensions so your own attributes become facets, chips and entity pages.
- Attach detail panels to an entity page.
- Read the JSON API from your own code or pages.
- Add backends (custom drivers).
- Add MCP tools for agents.
- Hook auth and multi-tenant scope.
Build a panel
Every panel extends Cbox\TelemetryUi\Panels\Panel and returns a payload from
data(). The terse path — a whole metric chart in one call — is promChart():
use Cbox\TelemetryUi\Panels\Panel;
final class QueueDepth extends Panel
{
public function data(): array
{
// Queries the range, converts the series, catches backend errors,
// returns a chart payload — with the current scope already applied.
return $this->promChart('Queue depth', $this->metric('queue_size'), unit: 'number', stat: 'Now');
}
}
Register it and it inherits scope, brush-to-zoom, deploy annotations, auto-refresh and the gate:
TelemetryUi::panel(QueueDepth::class, page: 'jobs');
Need more control? Build the series yourself and call chartCard(), or return
a table, stats row, bars or any other kind with the Ui builders. The kinds
are listed in pages & panels;
the toolkit and conventions are in custom panels.
Declare a panel instead of coding it
A chart over a metric needs no class — useful when the metric comes from a sidecar in another language (Go, Rust, anything exporting OTLP), where there is no PHP to hang a panel on:
TelemetryUi::page('indexer', 'Indexer', group: 'Infrastructure');
TelemetryUi::setPanels('indexer', []); // no built-ins on this page
TelemetryUi::metricPanel('indexer-queue', page: 'indexer',
title: 'Queue depth', metric: 'indexer_queue_depth', type: 'area', stat: 'Now');
TelemetryUi::metricPanel('indexer-docs', page: 'indexer', span: 2,
title: 'Documents indexed', metric: 'indexer_docs_total', rate: true, by: 'status');
TelemetryUi::metricPanel('indexer-latency', page: 'indexer',
title: 'Latency p95', metric: 'indexer_duration_seconds_bucket', quantile: 0.95, unit: 'ms');
A gauge reads as itself, rate: true turns a counter into per-minute
throughput, quantile: reads a histogram, by: splits the series by a label
and where: adds label matchers. Declared panels get the same scope, gate,
auto-refresh, deploy markers and brush-to-zoom as coded ones — they are just
configuration instead of code. Reach for a panel class when the payload isn't a chart
(tables, composites, stories).
A routing layer as its own area
Some layers name their requests rather than emitting their own attribute:
Livewire writes livewire:{component}, and a host's own layer might write
portal:{screen}. One call makes that a first-class part of the dashboard:
TelemetryUi::routeFamily('portal', label: 'Screens',
pattern: 'portal:{value}', dimension: 'portal.screen');
That registers a page (the family's throughput plus a per-value table with the
prefix stripped, each row opening that value's page) and declares
portal.screen as a derived dimension,
so the same values become facets, chips, filters and entity pages everywhere
else.
Register: add, replace, remove
use Cbox\TelemetryUi\Facades\TelemetryUi;
TelemetryUi::page('autoscale', 'Autoscale', group: 'Queues'); // a page (group = a "module")
TelemetryUi::panel(MyPanel::class, page: 'autoscale'); // add
TelemetryUi::setPanels('dashboard', [MyHeadline::class]); // replace a page's panels
TelemetryUi::removePanel(JobsOverview::class, 'dashboard'); // remove one
TelemetryUi::removePage('users'); // remove a section
Declare dimensions
TelemetryUi::dimension('billing.customer_id', label: 'Customer', group: 'Billing',
link: fn ($id) => route('customers.show', $id));
The attribute becomes a facet, a group-by option, a filter chip and a
clickable chip on every trace, and gets an entity page at
/entity/billing.customer_id?value=…. See
dimensions & Explore.
Detail panels scoped to one entity, hidden pages, entityPage() and the
ScopesTo* traits are in custom detail pages.
The rest of the surface
- JSON API — every screen's data under
{path}/api/v2, same gate and scope lock. Embedding cards as Livewire widgets was removed in v2; link to the SPA or read the API instead. - Custom drivers —
ConnectionManager::extend('victoriametrics', fn ($config) => new MyDriver(...))to add a backend; panels depend only on the contracts. - Issue trackers — add a tracker (or a list of repos) implementing
IssuesSource. - Navigation —
TelemetryUi::navLink()puts links out of the dashboard (your settings, your home) at the foot of the rail, for hosts that mount it as the whole UI. - View state —
TelemetryUi::viewState()to read (and move) the reader's time window, auto-refresh interval and scope, plus theViewStateChangedevent; it survives reload and links that carry no query string. - Connection switcher —
TelemetryUi::connection()puts your backend profiles in the dashboard header, so switching doesn't mean leaving. - MCP server —
TelemetryUi::mcpTool(MyTool::class)exposes a read tool to agents. - Authorization & tenancy — the
viewTelemetryUi/manageTelemetryUigates,TelemetryUi::restrictScopeUsing()to lock a viewer to services/environments, andTelemetryUi::resolveConnectionsUsing()for per-tenant backends. - Events — listen to
Cbox\TelemetryUi\Events\DashboardViewed(audit / usage metering: who opened which page in which scope — fired when the SPA shell is served, so client-side navigation inside the app does not fire it again),Cbox\TelemetryUi\Events\BackendQueried(backend load metering: url, method, duration, ok — one per real backend hit, cached reads excluded) andCbox\TelemetryUi\Events\ViewStateChanged(the reader moved the time window, refresh interval or scope). - Branding —
telemetry-ui.brandconfig sets thename/logoandaccentcolour to white-label the dashboard. There are no views to override in v2; the SPA is prebuilt.
Conventions
- Query through
$this->metrics()/traces()/logs()so named connections, custom drivers and tenancy keep working. - Never throw from
data(): catchSourceExceptionand return the payload with anerrorkey (the chart helpers do this for you) — a broken backend must never take the page down. - Respect
$this->range(); don't hardcode time windows. - Return links as
Ui::*arrays, not URLs. - Boot stays cheap: register class-strings, never instantiate connectors in a service provider.