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
apps/bff/src/logic/layers/loader.ts:widgetsForScope() resolves 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
interface DashboardWidget {
id: string;
title: string;
tip?: string;
type: 'card' | 'line' | 'top' | 'record';
expressions: string[];
expressionLabels?: string[]; // tab labels for 'top'
expressionUnits?: string[]; // per-expression unit override
expressionAxes?: number[]; // 0 = left, 1 = right (dual y-axis)
unit?: string; // widget-level unit suffix
format?: 'int' | 'decimal' | 'compact';
span?: number; // 12-col span; default 4
rowSpan?: number; // row count; default 1
visibleWhen?: string; // visibility predicate
layerScope?: boolean; // evaluate against layer rather than selected service
// legacy 24-col coordinates (back-compat with old templates):
x?: number; y?: number; w?: number; h?: number;
}
| Field | Notes |
|---|---|
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 /admin/layer-templates (verb dashboard:write). The editor shows the JSON tree with per-field type-aware controls. Changes are validated against the same schema as the bundled files, then written through POST /api/admin/layer-templates/:key.
Bundled templates remain in-place; admin edits override them per-instance and persist in the configured location.
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 (bundled or admin-edited) take effect on the next /api/menu or /api/layer/:key/dashboard/config request. Browsers see the new shape on the next page navigation. No BFF restart needed.
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.