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. Horizon ships bundled templates for the common layers, and every layer your OAP reports — with or without a bundled template — is editable in the Layer Dashboards admin page (under Dashboard setup): a visual editor that saves a local draft and publishes to OAP with Check diff & push. You don’t hand-edit JSON on the page; the shape documented below is the stored format the editor reads and writes, useful for understanding what each control maps to and for authoring templates as files.

Template shape (reference)

{
  "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/",
  "aliases": { ... },
  "components": { ... },
  "header": { ... },
  "dashboards": {
    "service":   [ ... widgets ... ],
    "instance":  [ ... widgets ... ],
    "endpoint":  [ ... widgets ... ],
    "dependency":[ ... 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.
splitByServiceGroup boolean false Split this layer into one sidebar entry per OAP service group (the <group>:: prefix), each scoped to that group. Off keeps a single combined entry. Toggled in the admin right after Alias.
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.
aliases object OAP defaults Per-layer entity term overrides (see below). slots is an accepted legacy alias for this key.
components object Which sub-tabs are enabled (see below). Every tab except the service dashboard is off unless explicitly set true.
header object Service-list picker columns + default sort.
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).

aliases

Layer-specific term overrides used in UI labels. (slots is an accepted legacy alias for this key; every bundled template uses aliases.)

"aliases": {
  "services":         "services",
  "instances":        "instances",
  "endpoints":        "endpoints",
  "endpointDependency": "API dependency",
  "topology":         "Topology",
  "instanceTopology": "Instance map"
}

A Kubernetes layer might use Pods instead of Instances. The page titles, sidebar tabs, and pickers pick up the override automatically. topology renames the Topology sidebar tab; instanceTopology renames the Instance map drill-down. Edit these in the admin under Menu labels (the alias fields render in sidebar/menu order, showing only the entries the layer’s enabled components expose).

components

Per-tab feature toggles. A false value hides the tab.

"components": {
  "service":            true,
  "instances":          true,
  "endpoints":          true,
  "endpointDependency": true,
  "topology":           true,
  "traces":             true,
  "logs":               true,
  "traceProfiling":     true,
  "ebpfProfiling":      false,
  "asyncProfiling":     false,
  "pprofProfiling":     false,
  "deployment":         false
}

The keys are the per-layer sub-tabs. networkProfiling and podLogs are also available. Only the service dashboard is on when its key is omitted; every other tab is off unless explicitly set true — the bundled templates enable each tab they want (general.json sets every flag true for exactly this reason). The landing tab when a layer is clicked is the first enabled in the priority order service → instances → endpoints → endpointDependency → topology → traces → logs → traceProfiling.

deployment is the exception: it is off by default and only appears when the layer also carries a deployment config block — see Deployment below.

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.

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 Structured visibility predicate (object form). An MQE gate { "kind": "mqe", "expression": "<mqe>", "op": "exists" | "gt" | "lt", "value"?: <n> } shows the widget only when the expression returns data (exists) or crosses a threshold; an entity gate { "kind": "entity", "attribute": "<attr>", "op": "exists" | "eq", "value"?: "<v>" } shows it only when the selected entity has that attribute — entity gates are Instance-scope only.
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

Config for the Topology map (the service-map view): which MQE metrics decorate each service node and each service-to-service call edge — and, optionally, the instance map drill-down. Edited in the admin under the layer’s Topology scope (node-metric / server-edge / client-edge editors). Without a block, a sensible default metric set is used.

"topology": {
  "nodeMetrics": [
    { "id": "cpm",      "label": "RPM",     "mqe": "service_cpm",       "unit": "rpm", "role": "center",    "aggregation": "avg" },
    { "id": "sla",      "label": "SLA",     "mqe": "service_sla/100",   "unit": "%",   "role": "ring",      "aggregation": "avg",
      "thresholds": { "invertHealth": true, "ok": 0.1, "warn": 1, "danger": 5 } },
    { "id": "respTime", "label": "Latency", "mqe": "service_resp_time", "unit": "ms",  "role": "secondary", "aggregation": "avg" }
  ],
  "linkServerMetrics": [
    { "id": "cpm", "label": "RPM", "mqe": "service_relation_server_cpm", "unit": "rpm", "role": "lineServer", "aggregation": "avg" }
  ],
  "linkClientMetrics": [
    { "id": "cpm", "label": "RPM", "mqe": "service_relation_client_cpm", "unit": "rpm", "role": "lineClient", "aggregation": "avg" }
  ],
  "instanceTopology": { "nodeMetrics": [ ... ], "linkServerMetrics": [ ... ], "linkClientMetrics": [ ... ] }
}
Field Notes
nodeMetrics[] Per-service-node metrics. role: center (the number inside the node), ring (the health colour band on the node), secondary (surfaced in the node detail).
linkServerMetrics[] / linkClientMetrics[] Per-call-edge metrics — server side (service_relation_server_*) and client side (service_relation_client_*). Ids that match across the two render aligned in the edge detail panel.
*.id / *.label / *.mqe / *.unit Stable id, display name, MQE expression, optional unit. Everything on screen — names, values, legend — comes from these, nothing is hardcoded.
*.role Visual binding (above). Edge metrics use lineServer / lineClient.
*.aggregation sum or avg across the window.
*.thresholds Four-band colour for a ring metric: ok / warn / danger boundaries, plus invertHealth: true for higher-is-better metrics (SLA, apdex, success rate) and an optional invertBase (default 100).
instanceTopology Optional. Enables the instance map (see below). Same nodeMetrics / linkServerMetrics / linkClientMetrics shape, but the MQE is evaluated at instance scope (service_instance_* and service_instance_relation_server/client_*). Absent ⇒ the layer offers no instance map.

Instance map

When topology.instanceTopology is set, the Topology map gains an instance-to-instance drill-down. On the service map, select a call between two services and click Instance map →: it opens the instances of each service as two columns (left = client, right = server) with the instance-level calls between them — the same node health-ring (with a colour legend reading the ring metric’s thresholds), per-service grouping boxes, per-call client/server metric panel, and pan/zoom as the service map. A toolbar pair-picker swaps the two services; a back button returns to the service map. Each grouping box is named with its service (the <group>:: prefix handled by the same naming rule as the service map), and labels follow the layer’s instance term (the instances / instanceTopology slots — e.g. Pods, Sidecars).

Enable and configure it in the admin: open the layer’s Topology scope and turn on Enable instance topology, which reveals its own node / server-edge / client-edge metric editors (kept separate from the service-topology metrics). Horizon ships it pre-enabled for GENERAL, MESH, K8S_SERVICE, and CILIUM_SERVICE; it rides the topology block, so it travels with template export/import.

deployment

Config for the Deployment tab — the deployment topology of all of a service’s instances. Where the instance map drills into the instances between two services, Deployment shows how one service’s own instances are deployed and call each other (for example a clustered store whose nodes call one another). Pick a service from the layer’s Service header and the tab draws its instances as health-ring nodes with the intra-service calls between them — pan/zoom, animated edge flow, a per-call client/server metric panel, and a node popover with Open instance dashboard. The boxes lay out left → right along the calls between them, so an upstream → downstream chain reads in order.

It is opt-in: off for every layer until you enable the deployment component and add a deployment block — the Deployment sub-tab appears only when both are set. When the backend exposes no intra-service instance relations for the selected service, the tab simply shows an empty state — it is a pure consumer of what OAP reports.

Node grouping. Instances can be grouped into labelled boxes by one of three rules:

  • by one instance attribute — e.g. group by node_role;
  • by several attributes (composite) — combine attribute values into one key (e.g. node_role + node_type); an attribute that is absent on a node drops out of its key, so nodes carrying only the first attribute stay in one box while those carrying both split further;
  • by a name regex — a named-capture pattern run on the instance name (same mechanism as the service-map grouping).

A second rule can bundle a pod: instances sharing a value (e.g. the same pod_name) render as one pod — a main hexagon with its sidecar containers attached as smaller hexes. A third rule picks each container’s role, which sets the main container and lets each role carry its own metrics.

Configure it in the admin. Open the layer in Dashboard setup → Layer dashboards, enable Deployment under Components, then open the Deployment scope. It has its own node / server-edge / client-edge metric editors (evaluated at instance scope — service_instance_* for nodes, instance-relation metrics for edges) plus the Node clustering picker (off / by attribute / by attributes / by name regex). The block is self-contained on the layer template, independent of the service-map topology config, so it travels with template export/import.

Stored format (reference)

"deployment": {
  "clusterBy": { "kind": "attributes", "attributes": ["node_role", "node_type"], "separator": " / ", "alias": "role" },
  "siblingBy": { "kind": "attribute", "attribute": "pod_name", "alias": "pod" },
  "roleBy":    { "kind": "attribute", "attribute": "container_name", "alias": "container" },
  "roles": [
    { "key": "data", "label": "Data", "main": true, "nodeMetrics": [ { "id": "write", "label": "Write/s", "mqe": "service_instance_cpm", "unit": "w/s", "role": "center", "aggregation": "avg" } ] }
  ],
  "linkServerMetrics": [ ... ],
  "linkClientMetrics": [ ... ]
}
Field Purpose
clusterBy Which dashed box an instance lands in. kind: "attribute" (one attribute), kind: "attributes" (several, joined by separator, default /), or kind: "nameRegex" (named-capture regex on the instance name).
siblingBy Bundles instances that share this value into one pod (main + sidecar hexes). Same kind choices. Omit for one hex per instance.
roleBy Resolves each instance’s role (e.g. by container_name); the role decides the main container and which roles[] metrics apply.
roles[] Per-role display: key (matches the roleBy value), label, main (true for the pod’s primary hex), and nodeMetrics[] (same metric-def shape as the topology node metrics).
nodeMetrics[] Fallback per-instance metrics for instances with no matching role. Optional when roles[] cover every instance.
linkServerMetrics[] / linkClientMetrics[] Per-call metrics on the server and client side of each intra-service edge.

endpointDependency

Config for the API dependency view — the endpoint-to-endpoint dependency map: which MQE metrics decorate each endpoint node and each endpoint-to-endpoint call edge. Same metric-def shape as topology, but the MQE is evaluated at endpoint scope (endpoint_*) for nodes and endpoint-relation scope (endpoint_relation_*) for edges. Without a block, a sensible default metric set is used.

"endpointDependency": {
  "nodeMetrics": [
    { "id": "cpm",      "label": "RPM",     "mqe": "endpoint_cpm",       "unit": "rpm", "role": "center",    "aggregation": "avg" },
    { "id": "sla",      "label": "SLA",     "mqe": "endpoint_sla/100",   "unit": "%",   "role": "ring",      "aggregation": "avg" },
    { "id": "respTime", "label": "Latency", "mqe": "endpoint_resp_time", "unit": "ms",  "role": "secondary", "aggregation": "avg" }
  ],
  "linkMetrics": [
    { "id": "cpm",      "label": "RPM",               "mqe": "endpoint_relation_cpm",        "unit": "rpm", "role": "lineServer", "aggregation": "avg" },
    { "id": "respTime", "label": "Avg response time", "mqe": "endpoint_relation_resp_time",  "unit": "ms",  "aggregation": "avg" }
  ]
}
Field Notes
nodeMetrics[] Per-endpoint-node metrics. Same id / label / mqe / unit / role / aggregation / thresholds fields as the topology node metrics.
linkMetrics[] Per-call-edge metrics. Server-side only — OAP exposes no endpoint_relation_client_* family, so (unlike the service map) there’s a single edge metric list; use role: lineServer.
showGroup Group endpoints by their naming rule in the node panel, same semantics as the topology showGroup.

Edited in the admin under the layer’s API dependency scope.

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 picker lists every layer your OAP reports, not just the ones with a shipped template — a layer with no template yet opens on a blank default you can configure and publish on first save. Pick a layer from the filterable dropdown (alias + key + sync status), then edit its service / instance / endpoint / topology / trace / log / profiling views. A live menu preview sits beside the Alias / Components / Menu-labels editor; clicking a menu item jumps to that component’s config.

How edits flow: draft → preview → publish

Your work-in-progress lives in your browser, never on the server until you publish. The live page everyone sees stays on the published OAP version throughout.

  1. Save (local). Stores your edit as a draft in this browser only. Nobody else sees it, and your own normal browsing still shows the published version. The picker tags a layer with a local draft as local.
  2. Reset to ▾. Loads the Bundled (shipped default) or Remote (OAP live) version into the editor as a fresh starting point.
  3. Preview ▾. Opens the real layer page in a new tab rendering your Local draft, the Bundled default, or Remote — using sample data, so you can check layout, enabled components, and menu labels without touching the server. Preview works even for layers OAP currently reports no services for.
  4. Check diff & push. Shows a side-by-side remote → local diff and publishes to OAP (the runtime source of truth). Enabled only when your draft actually differs from remote. After publishing, the draft is cleared and everyone sees the change.

A top banner summarizes page state — Synced from OAP — N diverged, Y local plus how many layers are not configured yet — and Diverged / Local / Not configured filters narrow the picker. Each row shows a status chip: synced (bundled == OAP), diverged (OAP differs from bundled — OAP wins at render), remote-only (on OAP, no bundled default), disabled (deleted — see below), or bundled (OAP has no copy right now).

Bundled defaults vs. your OAP-published templates

Each layer template has two copies: the bundled default shipped with Horizon, and the remote copy stored on OAP (what end users actually render — OAP wins at render time). On boot, Horizon seeds OAP only with templates that are absent there — a brand-new layer with no remote copy yet is pushed automatically so it works out of the box.

It does not overwrite a template that already exists on OAP. So when you upgrade Horizon and a bundled default changes — a new metric, a new capability such as the instance map, a tweaked widget — layers you’ve already published show as diverged: OAP keeps winning at render and your published edits are preserved. The new bundled default is offered, not forced.

To adopt a new bundled default on an existing layer, publish it from the admin:

  • the Diverged filter narrows the picker to the affected layers;
  • Reset to ▾ → Bundled loads the shipped default into the editor, then Check diff & push publishes it to OAP; or
  • review the remote → bundled diff first and keep any of your own changes before pushing.

This is why a freshly shipped capability can read diverged / off until you push it: the new config is bundled, but your OAP copy stays the source of truth and only changes when you publish. (New layers absent from OAP are the one case that goes live automatically, via the boot-time seed above.)

Import / Export

Export downloads the layer’s in-use version — what end users render now (the OAP-live copy, or the bundled default when OAP has none) — as a JSON file, for backup, sharing, or moving the dashboard to another OAP.

Import reads a layer-template JSON file and loads it as a local draft in this browser — it never writes OAP directly. Preview it, then Check diff & push to publish. Because layer keys are a fixed set, import targets the layer the file names (e.g. MESH), and that layer must already be present on this deployment; a file for a layer not loaded here, or one that isn’t a valid layer template, is rejected with a message.

Import/export covers the source layer template (the English authoring layer) only. Per-locale translations are stored separately in OAP and managed on the Translations page — they’re not part of this file. A layer exported to a different OAP arrives with its English source only; move its translations across on the Translations page if you need them there.

Disabling / reactivating a layer

OAP has no hard delete, so the Disable button next to the layer title soft-disables the layer on OAP. A disabled layer is dropped from the sidebar and renders nowhere, for everyone.

A disabled layer still appears in this admin page (struck-through, status disabled) and offers a Reactivate button that re-enables it from the bundled default. A layer that exists only as an unpublished local draft is simply removed from your browser. Both actions are confirmed in a dialog first.

Note: re-enabling depends on the OAP UI-template API clearing the disabled flag. On OAP versions that don’t support this, a disabled layer must be re-enabled from the OAP side. Treat disabling a built-in layer as a heavyweight action.

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.