Layer Dashboard Templates
A layer template is a single JSON file that describes everything Horizon needs to know about one OAP layer: its display name, color, sidebar grouping, which sub-tabs to expose, the service-list picker columns, the per-scope widget grids, the trace/log/topology routing, and the service-name parsing rule.
There is one template per layer, stored under apps/bff/src/bundled_templates/layers/<key>.json (lowercase filename matches the OAP layer enum, e.g. general.json for the GENERAL layer).
Top-level shape
{
"key": "GENERAL",
"alias": "General Service",
"group": "Application",
"visibility": "public",
"color": "var(--sw-accent)",
"documentLink": "https://skywalking.apache.org/docs/main/next/en/concepts-and-designs/scopes/",
"slots": { ... },
"components": { ... },
"header": { ... },
"overview": { ... },
"dashboards": {
"service": [ ... widgets ... ],
"instance": [ ... widgets ... ],
"endpoint": [ ... widgets ... ],
"topology": [ ... widgets ... ],
"trace": [ ... widgets ... ],
"logs": [ ... widgets ... ],
"traceProfiling": [ ... widgets ... ],
"ebpfProfiling": [ ... widgets ... ],
"asyncProfiling": [ ... widgets ... ]
},
"topology": { ... },
"endpointDependency": { ... },
"traces": { "source": "native" },
"log": { ... },
"naming": { ... }
}
Every field is optional except key. Defaults are baked in for the rest.
Top-level fields
| Field | Type | Default | Notes |
|---|---|---|---|
key |
string (UPPER_SNAKE) | required | Matches the OAP layer enum. The filename is the lowercased key. |
alias |
string | OAP-reported name | Display name in the sidebar and page headers. |
group |
string | — | Sidebar grouping label. Layers sharing a group collapse together. |
visibility |
public | operate |
public |
Section placement. operate puts the layer under the Operate group. |
color |
string | var(--sw-accent) |
Hex or CSS variable for the layer’s accent. |
documentLink |
string (URL) | — | External docs URL; renders as a small chip on the layer page. |
slots |
object | OAP defaults | Per-layer entity term overrides (see below). |
components |
object | all-true |
Which sub-tabs are enabled (see below). |
header |
object | — | Service-list picker columns + default sort. |
overview |
object | — | Overview tile config (groups of self-contained metrics) shown above the dashboard. |
dashboards |
object | — | Per-scope widget arrays (the bulk of the template). |
topology |
object | — | Topology MQE override for the service-map view. |
endpointDependency |
object | — | API-dependency dashboard MQE override. |
traces |
{ source?: 'native' | 'zipkin' | 'both' } |
native |
Trace backend selection for this layer. |
log |
object | — | Logs tab scope (service / instance / endpoint). |
naming |
object | — | Service-name parsing rule (extracts cluster or other tokens from the OAP-reported name). |
slots
Layer-specific term overrides used in UI labels.
"slots": {
"service": "service",
"services": "services",
"instance": "instance",
"instances": "instances",
"endpoint": "endpoint",
"endpoints": "endpoints"
}
A Kubernetes layer might use pod / pods instead of instance / instances. The page titles and pickers pick up the override automatically.
components
Per-tab feature toggles. A false value hides the tab.
"components": {
"service": true,
"instance": true,
"endpoint": true,
"topology": true,
"trace": true,
"logs": false,
"profiling": true
}
The landing tab when a layer is clicked is the first enabled in the priority order service → instance → endpoint → topology → trace → logs → profiling.
header
The service-list picker on the layer landing page. Columns sortable, with one designated default sort.
"header": {
"orderBy": "cpm",
"columns": [
{
"metric": "cpm",
"label": "RPM",
"mqe": "service_cpm",
"aggregation": "sum"
},
{
"metric": "apdex",
"label": "Apdex",
"mqe": "service_apdex/10000",
"aggregation": "avg"
},
{
"metric": "p95",
"label": "P95",
"mqe": "service_percentile{p='95'}",
"unit": "ms",
"aggregation": "avg"
}
]
}
| Field | Type | Notes |
|---|---|---|
orderBy |
string | metric value of the column that should sort by default. |
columns[].metric |
string | Unique id for the column (referenced by orderBy). |
columns[].label |
string | Column header label. |
columns[].mqe |
string | MQE expression evaluated per service. |
columns[].unit |
string | Optional unit suffix. |
columns[].aggregation |
sum | avg |
Aggregation across the time window. |
overview
Header summary tiles on the layer page (above the dashboard grid). Renders self-contained, sub-layout-aware groups of metrics.
"overview": {
"groups": [
{
"title": "Latency & errors",
"size": "auto",
"metrics": [
{
"id": "p95",
"label": "P95",
"mqe": "service_percentile{p='95'}",
"unit": "ms",
"aggregation": "avg"
},
{
"id": "errors",
"label": "Errors",
"mqe": "service_resp_time_percent_99",
"unit": "%",
"aggregation": "avg"
}
]
}
]
}
| Field | Type | Notes |
|---|---|---|
groups[].title |
string | Group header. |
groups[].size |
auto | wide |
Layout hint. wide doubles the group’s column allocation. |
groups[].metrics[].id |
string | Unique id within the group. |
groups[].metrics[].label |
string | Tile label. |
groups[].metrics[].mqe |
string | MQE expression evaluated layer-wide (or per-service if a service is selected). |
groups[].metrics[].unit |
string | Unit suffix. |
groups[].metrics[].aggregation |
sum | avg |
Aggregation across the time window. |
dashboards
The bulk of the template. A map from scope to an ordered widget array.
"dashboards": {
"service": [
{ "id": "rpm", "type": "line", "title": "RPM", ... },
{ "id": "p95", "type": "line", "title": "P95 latency", ... },
{ "id": "errors", "type": "card", "title": "Error rate", ... },
{ "id": "top_apis", "type": "top", "title": "Top 20 APIs", ... }
],
"instance": [ ... ],
"endpoint": [ ... ]
}
Scope enum
| Scope | Page |
|---|---|
service |
Service drill-down (primary). Used as fallback when other scopes are unset. |
instance |
Single service instance. |
endpoint |
Single endpoint. |
dependency |
Endpoint-to-endpoint relationships. |
topology |
Service-map visualization. |
trace |
Trace explorer. |
logs |
Log viewer. |
traceProfiling |
SkyWalking trace-driven profiling. |
ebpfProfiling |
eBPF profiling. |
asyncProfiling |
JVM async-profiler. |
Scope resolution
Widgets for a scope resolve in this order:
dashboards[scope] → dashboards.service → template.widgets (legacy)
A layer without an explicit instance widget set will reuse service widgets on the instance page. The fallback keeps minimal templates short.
Dashboard widget fields
| Field | Notes |
|---|---|
id |
Unique widget id within the dashboard. |
title |
Widget title shown in the card header. |
tip |
Optional hover hint. |
type |
Widget kind, usually card, line, top, record, or table. |
expressions[] |
MQE expressions to run. |
expressionLabels[] |
Tab labels for top, legend labels for line. |
expressionUnits[] |
Per-expression unit override. |
expressionAxes[] |
0 for left axis, 1 for right axis on dual-axis line charts. |
unit |
Widget-level unit suffix. |
format |
int, decimal, or compact. |
span |
12-column width. Default 4. |
rowSpan |
Row count. Default 1. |
visibleWhen |
Visibility predicate. |
layerScope |
Evaluate against the whole layer rather than the selected service. |
x, y, w, h |
Legacy coordinates kept for old templates. Prefer span and rowSpan. |
type |
card for single scalar (MQE collapses to one number); line for time-series; top for sorted list; record for tabular records (slow SQL, slow statements). |
expressions[] |
Array of MQE expressions. card typically uses one; line uses one per series; top may use multiple (each becomes a tab). |
expressionLabels[] |
Used by top to label each tab. |
expressionUnits[] |
Per-expression unit when expressions have heterogeneous units (e.g. ms + count). |
expressionAxes[] |
Two-axis charting. 0 = left y-axis (default), 1 = right. |
unit |
Widget-level unit (used when all expressions share the same unit). |
format |
Numeric formatting: int, decimal, compact (K / M suffixes). |
span |
Column span in the 12-col grid. Default 4 = three widgets per row. |
rowSpan |
Vertical span. Default 1 (one 120 px row). |
visibleWhen |
Predicate. Two supported shapes: #entity.<key> (truthy if the named entity key is set; e.g. #entity.serviceInstance to show only when an instance is selected) and <metric> has value (only show if the metric returns data). |
layerScope |
If true, MQE evaluates against the whole layer rather than the selected service. Used for layer-level summaries on the service page. |
Choosing type
The widget type must match the MQE shape:
- Outermost call
latest(...),max(...),min(...),avg(<plain-metric>),sum(<plain-metric>)→ collapses to one scalar →type: card. - Outermost call
relabels(...),top_n(...),histogram*(...),rate(...),increase(...),aggregate_labels(...)without scalar collapse → series →type: line. - Outermost call
top_n(...)returning a labeled list →type: top. - Database-shaped record returns →
type: record.
A line widget with a scalar-collapsed MQE renders a one-point chart and confuses operators. The widget editor warns; the schema does not enforce.
topology
Per-layer override for the service-map view’s MQE.
"topology": {
"metric": "service_resp_time"
}
Without an override, topology uses a default metric appropriate to the layer.
endpointDependency
Per-layer override for the API-dependency dashboard.
"endpointDependency": {
"metric": "endpoint_avg"
}
traces
"traces": { "source": "native" }
| Source | Behavior |
|---|---|
native (default) |
Traces queried via OAP’s native trace query. |
zipkin |
Traces queried via the Zipkin v2 endpoint at oap.zipkinUrl. |
both |
Both sources, with a UI toggle. |
log
"log": { "scope": "service" }
| Scope | Behavior |
|---|---|
service |
Logs are queried per service. |
instance |
Logs are queried per service instance. |
endpoint |
Logs are queried per endpoint. |
naming
Service-name parsing rule. Extracts a cluster (or other token) from the OAP-reported service name so the UI can show a grouped picker.
"naming": {
"pattern": "^([^|]+)\\|(.+)$",
"groups": { "cluster": 1, "name": 2 }
}
When set, the layer’s service list groups by cluster. Without it, services are listed flat.
Admin Editor
Layer templates are editable at runtime via Dashboard setup → Layer dashboards (/admin/layer-dashboards, verb dashboard:write). The editor shows layer-specific controls for service, instance, endpoint, topology, trace, log, and profiling views.
The save/publish model has two steps:
- Save locally. “Save locally” writes your edit to the local bundled copy and renders it immediately for preview — it does not touch OAP. The template now shows as diverged (local differs from what OAP serves), the row carries a Synced from OAP — N diverged banner, and the affected layers show a yellow warning icon in the sidebar. Save works even when OAP is unreachable.
- Publish. Sync all to OAP pushes the diverged templates to OAP (the runtime source of truth) — only the ones that differ — behind a confirmation that lists exactly what will be written. After publishing, the template is synced and everyone sees it.
A Diverged only filter and a Showing: Local / Remote display toggle sit at the top of the page; Show diff opens a side-by-side bundled-vs-OAP comparison.
Local vs. remote conflicts
OAP is the source of truth at runtime, so by default the app renders the OAP-stored version. When your local edits diverge, a per-session prompt (listing the affected items by menu name) asks which to render:
- Keep my local edits — render your local copy for preview; publish later with Sync all.
- Use live — overwrite your local copy with the remote (OAP) version. This discards your local edits and is confirmed first; use it when OAP holds the newer version.
Bundled examples
| File | Layer | Notes |
|---|---|---|
general.json |
GENERAL |
Reference shape — service/instance/endpoint dashboards, top_apis, header columns. |
mesh.json |
MESH |
Istio data-plane. Uses mesh_ metric family. |
k8s.json |
K8S |
Kubernetes cluster. Slots use pod instead of instance. |
mesh_cp.json |
MESH_CP |
Istio control-plane (Pilot). |
| … | various | One per OAP layer. |
Read the bundled JSON for the closest layer to yours before authoring a new template — most of the work is renaming MQE expressions to match your layer’s metric prefix.
Hot reload
Template changes made in the admin editor take effect on the next menu or dashboard refresh. Bundled file changes made outside Horizon require a BFF restart.
Common patterns
Borrow from another layer
Templates are not inheritance-aware. To “inherit” from general.json, copy it and rename MQE expressions. There is no extends: keyword.
Hide a tab entirely
"components": { "logs": false }
The Logs tab disappears from the layer page nav. Existing direct-URL navigation to /layer/<key>/logs redirects to the first enabled tab.
Add a layer-wide summary widget on the service page
{
"id": "layer_total_rpm",
"type": "card",
"title": "Layer-wide RPM",
"expressions": ["sum(service_cpm)"],
"layerScope": true,
"span": 3
}
layerScope: true evaluates the MQE against the entire layer rather than the selected service.