You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@skywalking.apache.org by wu...@apache.org on 2026/05/14 09:38:03 UTC
(skywalking-horizon-ui) 03/04: layers: migrate 43 dashboards from booster + ship image pipeline
This is an automated email from the ASF dual-hosted git repository.
wu-sheng pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/skywalking-horizon-ui.git
commit f6137b6ceb800413c0a107168f0ce074003073b6
Author: Wu Sheng <wu...@foxmail.com>
AuthorDate: Thu May 14 16:38:01 2026 +0800
layers: migrate 43 dashboards from booster + ship image pipeline
- Migrate all 43 OAP layers' dashboards from booster-ui templates:
general / mesh / k8s / k8s_service / virtual_{database,cache,mq}
+ 37 new (browser / ios / mini-programs, gateways, databases,
queues, so11y_*, AWS, OS, banyandb / envoy_ai_gateway / cilium /
mesh_cp / mesh_dp / flink / virtual_genai).
- Move layer + overview config out of feature dirs and into one root
bundled_templates/{layers,overviews}/ — both loaders updated.
- Add .claude/skills/migrate-layer SKILL.md: booster → horizon widget
mapping, scope rules (SERVICE_INSTANCE-in-service dashboard must
use top_n; LABELED vs REGULAR aggregation), P95-over-P99 rule,
OAL/MAL fallback path when demo has no services for a layer.
- Validator helper /tmp/validate-layer.mjs (auto-picks a service with
data on demo OAP and reports OK / EMPTY / UNKNOWN / ERROR per
expression). Caught real bugs in mesh / k8s_service / so11y_oap /
virtual_mq during this migration.
- Sidebar: service-count chip moved to left-aligned 34px fixed-width
prefix so every layer row's name starts at the same column even
when counts swing wildly (1 ↔ 1000+).
- Dockerfile + .github/workflows/publish-image.yaml: multi-stage
build, multi-arch (amd64+arm64), publishes to GHCR on push to main
and v* tags. Canonical tag is the full 40-char commit SHA; moving
tags (:main, :vX.Y.Z, :latest on release) resolve to the same image.
- BFF: serve the SPA from HORIZON_STATIC_DIR (Docker layout puts the
Vite dist at /app/static); no-op in local dev where Vite owns the
SPA on :9091.
- Also folds in trace / log / overview routes + the topbar
auto-refresh cluster + trace popout work that had accumulated.
---
.claude/skills/migrate-layer/SKILL.md | 263 ++
.github/workflows/publish-image.yaml | 105 +
Dockerfile | 69 +
.../bff/src/bundled_templates/layers/activemq.json | 59 +
.../layers/alipay_mini_program.json | 179 ++
apps/bff/src/bundled_templates/layers/apisix.json | 67 +
.../src/bundled_templates/layers/aws_dynamodb.json | 57 +
apps/bff/src/bundled_templates/layers/aws_eks.json | 46 +
.../src/bundled_templates/layers/aws_gateway.json | 69 +
apps/bff/src/bundled_templates/layers/aws_s3.json | 40 +
.../bff/src/bundled_templates/layers/banyandb.json | 69 +
.../src/bundled_templates/layers/bookkeeper.json | 45 +
apps/bff/src/bundled_templates/layers/browser.json | 244 ++
.../bundled_templates/layers/cilium_service.json | 73 +
.../src/bundled_templates/layers/clickhouse.json | 60 +
.../bundled_templates/layers/elasticsearch.json | 59 +
.../bundled_templates/layers/envoy_ai_gateway.json | 61 +
apps/bff/src/bundled_templates/layers/flink.json | 67 +
.../layers}/general.json | 77 +-
apps/bff/src/bundled_templates/layers/ios.json | 254 ++
apps/bff/src/bundled_templates/layers/k8s.json | 266 ++
.../src/bundled_templates/layers/k8s_service.json | 404 +++
apps/bff/src/bundled_templates/layers/kafka.json | 54 +
apps/bff/src/bundled_templates/layers/kong.json | 81 +
apps/bff/src/bundled_templates/layers/mesh.json | 455 ++++
apps/bff/src/bundled_templates/layers/mesh_cp.json | 40 +
apps/bff/src/bundled_templates/layers/mesh_dp.json | 38 +
apps/bff/src/bundled_templates/layers/mongodb.json | 62 +
apps/bff/src/bundled_templates/layers/mysql.json | 228 ++
apps/bff/src/bundled_templates/layers/nginx.json | 76 +
.../bff/src/bundled_templates/layers/os_linux.json | 38 +
.../src/bundled_templates/layers/os_windows.json | 33 +
.../src/bundled_templates/layers/postgresql.json | 274 ++
apps/bff/src/bundled_templates/layers/pulsar.json | 50 +
.../bff/src/bundled_templates/layers/rabbitmq.json | 52 +
apps/bff/src/bundled_templates/layers/redis.json | 55 +
.../bff/src/bundled_templates/layers/rocketmq.json | 54 +
.../bundled_templates/layers/so11y_go_agent.json | 34 +
.../bundled_templates/layers/so11y_java_agent.json | 34 +
.../src/bundled_templates/layers/so11y_oap.json | 55 +
.../bundled_templates/layers/so11y_satellite.json | 35 +
.../bundled_templates/layers/virtual_cache.json | 184 ++
.../bundled_templates/layers/virtual_database.json | 101 +
.../bundled_templates/layers/virtual_genai.json | 49 +
.../src/bundled_templates/layers/virtual_mq.json | 168 ++
.../layers/wechat_mini_program.json | 234 ++
.../src/bundled_templates/overviews/general.json | 82 +
apps/bff/src/bundled_templates/overviews/mesh.json | 82 +
apps/bff/src/config/schema.ts | 11 +
apps/bff/src/dashboard/defaults.ts | 2 +-
apps/bff/src/dashboard/routes.ts | 8 +-
apps/bff/src/layers/config/k8s_service.json | 109 -
apps/bff/src/layers/config/mesh.json | 125 -
apps/bff/src/layers/config/virtual_cache.json | 75 -
apps/bff/src/layers/config/virtual_database.json | 102 -
apps/bff/src/layers/config/virtual_mq.json | 74 -
apps/bff/src/layers/loader.ts | 28 +-
apps/bff/src/oap/clients.ts | 30 +-
apps/bff/src/oap/endpoint-dependency-routes.ts | 48 +-
apps/bff/src/oap/endpoint-routes.ts | 8 +-
apps/bff/src/oap/graphql-client.ts | 38 +-
apps/bff/src/oap/info-routes.ts | 7 +-
apps/bff/src/oap/instance-routes.ts | 8 +-
apps/bff/src/oap/landing-routes.ts | 8 +-
apps/bff/src/oap/log-routes.ts | 313 +++
apps/bff/src/oap/menu-routes.ts | 6 +-
apps/bff/src/oap/server-time.ts | 11 +-
apps/bff/src/oap/topology-routes.ts | 8 +-
apps/bff/src/oap/trace-protocol-cache.ts | 89 +
apps/bff/src/oap/trace-routes.ts | 472 ++++
apps/bff/src/oap/trace-tag-routes.ts | 116 +
apps/bff/src/oap/zipkin-client.ts | 166 ++
apps/bff/src/overview/loader.ts | 120 +
apps/bff/src/overview/routes.ts | 88 +
apps/bff/src/server.ts | 31 +
apps/ui/src/api/client.ts | 126 +
apps/ui/src/components/charts/Sparkline.vue | 126 +-
apps/ui/src/components/shell/AppShell.vue | 4 +
apps/ui/src/components/shell/AppSidebar.vue | 18 +-
apps/ui/src/components/shell/AppTopbar.vue | 182 +-
apps/ui/src/components/trace/TracePopout.vue | 828 ++++++
apps/ui/src/composables/useAutoRefreshSubscribe.ts | 36 +
apps/ui/src/composables/useLayerDashboard.ts | 3 +
.../src/composables/useLayerEndpointDependency.ts | 3 +
apps/ui/src/composables/useLayerLanding.ts | 3 +
apps/ui/src/composables/useLayerLogs.ts | 118 +
apps/ui/src/composables/useLayerTopology.ts | 3 +
apps/ui/src/composables/useLayerTraces.ts | 131 +
apps/ui/src/composables/useOapInfo.ts | 4 +
apps/ui/src/composables/useTopologyIcons.ts | 10 +
apps/ui/src/composables/useTracePopout.ts | 66 +
apps/ui/src/router/index.ts | 7 +-
apps/ui/src/stores/autoRefresh.ts | 139 +
apps/ui/src/stores/setup.ts | 2 +-
apps/ui/src/views/admin/LayerDashboardsAdmin.vue | 540 +++-
apps/ui/src/views/layer/LayerDashboardsView.vue | 2 +-
.../views/layer/LayerEndpointDependencyView.vue | 516 +++-
apps/ui/src/views/layer/LayerLogsView.vue | 702 ++++++
apps/ui/src/views/layer/LayerServiceMapView.vue | 368 ++-
apps/ui/src/views/layer/LayerTracesView.vue | 2665 ++++++++++++++++++++
apps/ui/src/views/setup/LayerSetupCard.vue | 307 ++-
apps/ui/src/views/setup/SetupView.vue | 4 +-
horizon.example.yaml | 7 +
packages/api-client/src/index.ts | 40 +
packages/api-client/src/logs.ts | 100 +
packages/api-client/src/overview.ts | 107 +
packages/api-client/src/setup.ts | 16 +
packages/api-client/src/topology.ts | 25 +
packages/api-client/src/trace.ts | 221 ++
109 files changed, 13934 insertions(+), 877 deletions(-)
diff --git a/.claude/skills/migrate-layer/SKILL.md b/.claude/skills/migrate-layer/SKILL.md
new file mode 100644
index 0000000..3fdefdb
--- /dev/null
+++ b/.claude/skills/migrate-layer/SKILL.md
@@ -0,0 +1,263 @@
+---
+name: migrate-layer
+description: Migrate one OAP layer's UI templates from booster-ui (apache/skywalking ui-initialized-templates) into the Horizon UI bundled config — preserves intent, adapts to Horizon's flatter widget grid + extracts metric semantics from upstream docs.
+user-invocable: true
+---
+
+# Migrate one OAP layer into Horizon
+
+Source of truth for the upstream side:
+- Templates: `/Users/wusheng/github/skywalking/oap-server/server-starter/src/main/resources/ui-initialized-templates/<layer>/`
+- Docs index: `/Users/wusheng/github/skywalking/docs/menu.yml` (paths are relative to `/Users/wusheng/github/skywalking/docs/`)
+
+Output goes to: `apps/bff/src/bundled_templates/layers/<layer>.json`
+
+The Horizon JSON shape is defined by `apps/bff/src/layers/loader.ts` (`LayerTemplate` interface). Read it once before each new layer so any new fields are picked up.
+
+---
+
+## When this skill applies
+
+You're staring at a booster-ui template folder (e.g. `k8s/`, `mesh/`, `virtual_database/`) with 1–7 nested `*.json` files and you need to produce **one** Horizon JSON that lights up the corresponding layer page (Services / Instances / Endpoints / Topology / Traces / Logs / Profiling). Some layers have only `*-root.json` + `*-service.json` (service-only); others add `*-instance.json`, `*-endpoint.json`, and `*-*-relation.json` (full mesh).
+
+---
+
+## Booster → Horizon mapping reference
+
+### Top-level shape
+
+| Booster | Horizon | Notes |
+|---|---|---|
+| One file per scope, each wrapping a `Tab` tree | One JSON, scopes flattened into `dashboards.{service,instance,endpoint,dependency,topology}` arrays | The `Tab` containers in booster are dropped — Horizon's layer page already provides the tab strip via the route hierarchy. |
+| `configuration.layer` | top-level `key` (also matches the file basename uppercased) | The loader asserts `file.json` ↔ `"key": "FILE"` (UPPER_SNAKE). |
+| `configuration.entity` (`Service` / `ServiceInstance` / `Endpoint` / …) | implied by which `dashboards.*` bucket the widget lives in | Drop the entity field — placement decides. |
+| `configuration.name` (e.g. `General-Service`) | not needed | Horizon derives titles from `alias` + route. |
+| `Event` widget at the bottom | not yet supported | Skip; revisit when Horizon adds an events strip. |
+
+### Widget renderer mapping
+
+| Booster `graph.type` | Horizon `type` | Comment |
+|---|---|---|
+| `Card` | `card` | Single scalar, avg across window. Card MQE usually wraps with `avg(...)`. Horizon evaluates the average itself; **strip the `avg()` wrapper** unless it changes shape (e.g. `avg(x/100)`). |
+| `Line` (single expression) | `line` | One labeled series. Carry `unit`, `expressions[0]`, `expressionLabels?`. |
+| `Line` (multiple expressions) | `line` | Multi-series. Use `expressionLabels` (mandatory for legend). When the booster widget has a dual-axis trick (e.g. count + latency), set `expressionAxes: [0, 1]` and supply `expressionUnits`. |
+| `TopList` | `top` | Sorted list — usually `top_n(...,N,des)`. When booster ships several `TopList`s on the same layer that are conceptually the same ranking with different sort dimensions (e.g. Top APIs by Traffic / by Slow / by SR), **fold them into a single `top` widget with multiple `expressions` + `expressionLabels` + `expressionUnits`** (the SPA renders these as in-widget tabs). |
+| `TopList` with RECORD-typed MQE (`top_n_service_database_statement`, etc.) | `record` | Use `record`, not `top`. Pair with `relatedTrace.refIdType: traceId` upstream — Horizon's record widget surfaces trace links automatically. |
+| `Topology` | **not a dashboard widget** — pull node/link MQE up to top-level `topology` block | The `linkServerExpressions`, `linkClientExpressions`, `nodeExpressions` become `topology.{nodeMetrics, linkServerMetrics, linkClientMetrics}` arrays. Add `role` (`center`/`ring`/`secondary` for nodes, `lineServer`/`lineClient` for edges). |
+| `InstanceList`, `EndpointList` | **dropped** — Horizon ships built-in pickers | The picker columns come from `header.columns` (or `overview.*`) instead. Don't reproduce these as dashboard widgets. |
+| `Trace`, `Log` | **flip the component flag** in `components.{traces,logs}` | No widget needed — Horizon owns the Traces / Logs views. |
+| `Profile` (trace profiling) | `components.traceProfiling: true` | Horizon's profiling views are component-level, not widget-level. |
+| `Ebpf` | `components.ebpfProfiling: true` | Same. |
+| `AsyncProfiling` | `components.asyncProfiling: true` | Same. |
+| `Text` | dropped | Layer header in Horizon already documents the page; if you need an external link, set `documentLink` at the layer root. |
+| `Event` | dropped (no Horizon equivalent yet) | Note in the PR if a layer leans on this heavily. |
+
+### Field-level mapping
+
+| Booster | Horizon | Notes |
+|---|---|---|
+| `widget.title` | `title` | Drop unit suffixes from the title — Horizon shows the unit separately. e.g. `"Service Avg Response Time (ms)"` → `"Avg Response Time"` + `"unit": "ms"`. |
+| `widget.tips` | `tip` | Keep verbatim where useful; otherwise tighten to one sentence. |
+| `expressions[0]` | `expressions[0]` | MQE stays. For cards, unwrap a single outer `avg(...)`. |
+| `graph.fontSize`, `textAlign`, `showSymbol`, `step`, `smooth`, `color`, `showXAxis`/`Y` | dropped | Horizon's chart wrappers own the rendering style. |
+| `metricConfig[*].unit` | `unit` (or `expressionUnits`) | Use widget-level `unit` for single-series widgets, `expressionUnits` per index for multi-series. |
+| `metricConfig[*].label` | `expressionLabels[i]` | When booster sets a label to give the legend a friendly name, copy it; otherwise rely on the MQE relabel (e.g. `relabels(...,percentile='99')` is already the legend label). |
+| `associate` (cross-widget cursor sync) | dropped | Horizon syncs crosshairs via `useCrosshair()` automatically. |
+| `relatedTrace.enableRelate`, `latency`, `status`, `queryOrder` | dropped (for widget) | The dashboard's trace-list links are auto-derived. For `record` widgets, keep `relatedTrace.refIdType` semantics by ensuring the underlying MQE is RECORD-typed. |
+| `valueRelatedDashboard` | dropped | Horizon routing — clicking a top item drills into the instance/endpoint page automatically. |
+| `x/y/w/h` (24-col grid) | `span` / `rowSpan` (12-col flow) | **Halve the booster width** to convert to Horizon's 12-col grid (`w: 8` → `span: 4`). Use `rowSpan` only when the widget is taller than the default 2 rows (default ~14px × 8 = chart height). Top-lists and percentile charts typically `rowSpan: 3` or `4`. |
+| `subExpressions` (per-row sparkline MQE under `InstanceList`/`EndpointList`) | dropped (UI computes the row-sparklines itself) | These are the picker's per-row spark sources. Horizon's pickers fetch them through `landing` routes. |
+| `legendMQE` + `description.healthy/unhealthy` on Topology | Horizon doesn't yet have a legend-rule UI — capture the rule in a comment for now | Pending UI work. |
+
+### Picker column derivation
+
+The layer page header (services list) needs `header.columns`. Source order of preference:
+1. The booster `*-service.json` Overview tab's top three single-card metrics — these are conceptually the same as the picker columns.
+2. The picker's `subExpressions` array — first 1–3 entries map well.
+3. Default to `cpm` / `sla/100` / `resp_time` if nothing is obvious.
+
+Always set `orderBy` to the column you want as the default sort (usually `cpm`).
+
+### Overview tile derivation
+
+`overview.groups` drives the tall hero card above the picker. Pick 2–4 metrics that tell the layer's health story at a glance — almost always:
+- **one** latency percentile (**P95** — prefer P95 over P99; do not include both, they're redundant in a hero strip)
+- an error rate (`100 - service_sla/100` or layer equivalent)
+- a health score (apdex) where available **or** a throughput indicator — pick the one that's most informative for that layer
+
+Group as `{ "title": "Latency & errors", "size": "auto", "metrics": [...] }` and (optionally) `{ "title": "Health", "size": "square", "metrics": [<single>] }` or `{ "title": "Throughput", "size": "square", "metrics": [<RPM>] }` when the layer has no apdex equivalent.
+
+---
+
+## 5-step procedure for migrating one layer
+
+Run these in order **for each layer**. The order matters: 1–4 build the mental model; 5 produces the JSON. Don't skip ahead to writing JSON.
+
+### Step 1 — Learn the old layout
+
+Read every file under `/Users/wusheng/github/skywalking/oap-server/server-starter/src/main/resources/ui-initialized-templates/<layer>/`. List:
+- Each `*-root.json` Tab name + child widgets — defines the layer landing strip
+- Each `*-service.json` / `*-instance.json` / `*-endpoint.json` Overview tab widget set — these become the `dashboards.<scope>` arrays
+- The `*-relation.json` files — the relation widget sets feed into Horizon's edge/dependency drill panels but **most metrics belong to topology.linkServerMetrics / linkClientMetrics**, not as flat widgets. If the relation file has 4+ widgets, mention them in the migration notes; if it's just RPM/respTime/SLA/percentile, fold into topology.
+- Note which widgets the layer uses (`InstanceList`, `EndpointList`, `Topology`, `Trace`, `Log`, `Profile`, etc.) so you know which `components.*` flags to flip.
+
+Optional `Bash` helper to summarize:
+```bash
+for f in /Users/wusheng/github/skywalking/oap-server/server-starter/src/main/resources/ui-initialized-templates/<LAYER>/*.json; do
+ echo "=== $(basename $f) ==="
+ python3 -c "
+import json
+d = json.load(open('$f'))
+def walk(n, depth=0):
+ if isinstance(n, list):
+ for i in n: walk(i, depth); return
+ if not isinstance(n, dict): return
+ t = n.get('type'); name = n.get('name')
+ title = (n.get('widget') or {}).get('title') if isinstance(n.get('widget'), dict) else None
+ if name or (t and t != 'Tab') or title:
+ print(' '*depth + (t or 'Tab') + ((': ' + (name or title)) if (name or title) else ''))
+ for c in n.get('children') or []: walk(c, depth+1)
+ if n.get('configuration'): walk(n['configuration'], depth)
+walk(d)"
+done
+```
+
+### Step 2 — Learn the Horizon capabilities
+
+Reread `apps/bff/src/layers/loader.ts` (the `LayerTemplate` interface + comments) and `packages/api-client/src/dashboard.ts` (the `DashboardWidget` interface). Decide which Horizon features the migration will use:
+- `dashboards.service` (always)
+- `dashboards.instance` (if booster has instance template **and** the layer has per-instance metrics worth showing)
+- `dashboards.endpoint` (same logic for endpoints)
+- `dashboards.dependency` (only if the relation file has metrics distinct from topology edges)
+- `topology` block (any layer with a non-trivial service map — almost all do)
+- `endpointDependency` block (only if the layer has per-endpoint relations, e.g. general, mesh, k8s_service)
+- `components.{traces, logs, traceProfiling, ebpfProfiling, asyncProfiling}` flags
+- `visibleWhen` for any widget whose MQE only fires conditionally (`instance_jvm_cpu has value` etc.) — common on instance-scope widgets that span multiple runtimes.
+
+Also reread the existing `general.json` (and `mesh.json` if migrating a service-mesh-adjacent layer) as the reference template. Copy its shape verbatim and replace metric names — don't reinvent the structure.
+
+### Step 3 — Read metrics meaning from booster's `widget.tips` + metric names
+
+For each widget you're going to migrate, write a one-line summary of what the metric represents. Sources:
+- `widget.tips` in the booster JSON (often the most direct)
+- The metric name itself (e.g. `instance_jvm_gc_pause_duration` → JVM GC pause time per minute)
+- The OAP query-protocol schemas under `apache/skywalking/oap-server/.../query-protocol/*.graphqls` if the metric is exposed there
+
+If you can't write a one-liner from these alone, move to Step 4.
+
+### Step 4 — Web search the target component + popular key metrics
+
+For domain-specific layers (Kafka, Redis, MongoDB, Envoy, Cilium, etc.), web search a small set:
+- `"<component> top metrics for monitoring"` — usually finds the canonical 5–10 SREs care about
+- `"<component> golden signals"` — RED / USE method articles
+- Vendor docs (Confluent for Kafka, Redis Labs for Redis, etc.) for the metric definitions
+
+The goal is to **prioritize** the booster widgets — if booster ships 30 metrics but only 8 of them are commonly-watched, surface those 8 in the layer landing and stash the rest in a "More" group (or just keep them in the JSON with smaller `span` so they live below the fold).
+
+### Step 5 — Read upstream docs from `docs/menu.yml`
+
+`/Users/wusheng/github/skywalking/docs/menu.yml` has a `catalog:` tree. Find the entry whose name matches the layer (e.g. "MySQL/MariaDB monitoring", "Kafka monitoring", "Redis monitoring") and follow its `path:` to `/Users/wusheng/github/skywalking/docs/<path-without-leading-slash>.md`. These docs:
+- List every metric the OAP module emits, with definitions
+- Often categorize metrics ("RED metrics", "Capacity metrics", "Reliability metrics")
+- Sometimes include the suggested dashboard layout
+
+This is the most authoritative source for metric semantics. **If a booster widget tip is vague but the upstream doc explains the metric, use the doc's wording for the Horizon `tip`.**
+
+### Step 6 — Write the Horizon JSON
+
+Now produce `apps/bff/src/bundled_templates/layers/<layer>.json`. Skeleton:
+
+```jsonc
+{
+ "key": "<UPPER_SNAKE>",
+ "alias": "<Display Name>",
+ "color": "var(--sw-accent)", // most layers use the orange accent
+ "documentLink": "https://skywalking.apache.org/docs/main/next/en/<path-from-menu.yml>",
+ "aliases": { // optional — only override slot names when the layer uses non-standard entity terms
+ "services": "Brokers",
+ "instances": "Nodes",
+ "endpoints": "Topics"
+ },
+ "components": {
+ "service": true,
+ "instances": <has *-instance.json>,
+ "endpoints": <has *-endpoint.json>,
+ "endpointDependency": <has endpoint-relation.json>,
+ "topology": <has Topology widget anywhere>,
+ "traces": <has Trace widget>,
+ "logs": <has Log widget>,
+ "traceProfiling": <has Profile widget>,
+ "ebpfProfiling": <has Ebpf widget>,
+ "asyncProfiling": <has AsyncProfiling widget>
+ },
+ "layer-header": {
+ "orderBy": "<column metric key>",
+ "columns": [ /* 2-4 picker columns */ ]
+ },
+ "overview": {
+ "groups": [
+ { "title": "Latency & errors", "size": "auto", "metrics": [/* p95 + err (no p99 — redundant with p95 in a hero strip) */] },
+ { "title": "Health", "size": "square", "metrics": [/* single apdex-like */] }
+ ]
+ },
+ "dashboards": {
+ "service": [ /* the *-service.json Overview tab widgets, mapped 1:1 */ ],
+ "instance": [ /* *-instance.json Overview + JVM/CLR/etc tabs flattened */ ],
+ "endpoint": [ /* *-endpoint.json Overview tab */ ]
+ },
+ "topology": {
+ "nodeMetrics": [ /* center, ring, secondary */ ],
+ "linkServerMetrics": [ /* RPM, respTime, p95, SLA — order matters for fallback */ ],
+ "linkClientMetrics": [ /* same shape; omit on layers w/o client-side relations */ ]
+ },
+ "endpointDependency": { // only when components.endpointDependency
+ "nodeMetrics": [ /* center, ring, secondary */ ],
+ "linkMetrics": [ /* server-only — endpoint relations have no client side in OAP */ ]
+ }
+}
+```
+
+### Step 7 — Validate
+
+From `apps/bff`:
+```bash
+pnpm exec tsc --noEmit # schema still typechecks
+node --import tsx -e "import('./src/layers/loader.ts').then(m => { const t = m.getLayerTemplate('<KEY>'); console.log(JSON.stringify({key:t.key, components:t.components, headerCols:t.header.columns.length, dashScopes:Object.keys(t.dashboards||{}), topo:!!t.topology, epDep:!!t.endpointDependency}, null, 2)); })"
+```
+
+Sanity-check the JSON loads and surfaces all expected fields. If the BFF is running, hit `/api/menu` and `/api/layer/<key>/landing` against the demo OAP to confirm the picker columns resolve.
+
+---
+
+## Common pitfalls
+
+1. **Scope mismatch — SERVICE_INSTANCE metrics in service dashboards.** A bare per-instance metric (e.g. `meter_oap_instance_cpu_percentage`, catalog `SERVICE_INSTANCE`) **does not work** in a service-scope picker / overview / line widget. You have three options:
+ - **Picker column / overview / card** — wrap with `avg(...)` or `sum(...)` to coerce to a single scalar (e.g. `avg(meter_oap_instance_cpu_percentage)`).
+ - **Service-dashboard line widget showing per-instance trends** — must be a **`top_n(...)` `top` widget**, never a bare `line`. The line widget at service scope can only render SERVICE-scope metrics.
+ - **Per-instance metric on the instance scope** — put it under `dashboards.instance` and reference the bare metric; it works fine at instance entity.
+ The same rule applies one level deeper for ENDPOINT-scope metrics in service dashboards — use `top_n` or aggregate.
+2. **Labeled vs regular OAL/MAL output.** `aggregate_labels(metric, sum)` is only valid for LABELED_VALUE metrics. For REGULAR_VALUE metrics use plain `sum(metric)` / `avg(metric)`. Mixing them produces "result is not a labeled result" errors. Check `listMetrics(regex:^<name>$)` on the demo OAP to confirm catalog + type before composing.
+3. **Card MQE includes `avg(...)`** — booster wraps card values in `avg(...)` because their card renderer expects a scalar. Horizon's card path *also* averages, so leaving the `avg(...)` works but is redundant. **For consistency, unwrap a single outer `avg()` from card expressions** unless removing it changes the shape (e.g. `avg(x)/100` becomes `x/100`, but `avg(x{p='99'})/100` stays as-is because the inner `{p='99'}` is the dimension we're averaging across).
+2. **24-col → 12-col grid** — `w: 8` (booster) becomes `span: 4` (Horizon). Don't paste 24-col widths or widgets overflow the grid.
+3. **InstanceList / EndpointList** — these are *not* widgets in Horizon. They're the layer page's built-in pickers. The booster widget's `subExpressions` array (per-row MQE) is the closest analogue; drop it (Horizon's landing route picks per-row metrics from `header.columns`).
+4. **Relation files** — most of a `*-service-relation.json` collapses into the top-level `topology` block (server + client edge metrics). Don't replicate the relation widgets as flat `dashboards.dependency` entries unless they're metrics the topology renderer can't surface (e.g. relation-specific record widgets).
+5. **`visibleWhen`** — booster instance dashboards bundle JVM / CLR / Golang / Python / Ruby / Spring widgets in the same scope, and the SPA hides ones with no data. Carry this over with `"visibleWhen": "<first_metric> has value"` so the Horizon Instance view stays clean for a single-runtime instance.
+6. **No `Co-Authored-By` footers on commits** — per project `CLAUDE.md` and stored memory.
+
+---
+
+## Reference: General layer migration
+
+The General layer (already migrated) is the canonical reference. Compare:
+
+- Upstream files: `general-root.json`, `general-service.json`, `general-instance.json`, `general-endpoint.json`, `general-service-relation.json`, `general-instance-relation.json`, `general-endpoint-relation.json` (4565 lines total across 7 files)
+- Horizon output: `apps/bff/src/bundled_templates/layers/general.json` (989 lines, one file)
+
+Key transformations applied (use as a recipe):
+- 7 nested `Tab` files → flat `dashboards.{service,instance,endpoint}` arrays (dependency + topology folded into top-level blocks)
+- Card MQE unwrapped (`avg(service_apdex)/10000` → in card context Horizon does the avg, but kept as-is when the math is inside)
+- Picker columns derived from the booster Overview tab's three card metrics: cpm / apdex / error rate
+- Topology block merged `general-service-relation.json`'s `linkServerExpressions` / `linkClientExpressions` into a single block with four-metric server + client families (RPM / respTime / p95 / SLA)
+- Slow-statements widget converted from booster's `TopList` with RECORD-typed MQE to Horizon's `record` type
+- Multi-runtime instance widgets (JVM / CLR / Sleuth / Go / Python / Ruby) all carry `visibleWhen: "<first_metric> has value"` so instances only render the relevant rows
+- `components.{traceProfiling, ebpfProfiling, asyncProfiling}` set from the three booster profiling tabs
diff --git a/.github/workflows/publish-image.yaml b/.github/workflows/publish-image.yaml
new file mode 100644
index 0000000..37c0fc3
--- /dev/null
+++ b/.github/workflows/publish-image.yaml
@@ -0,0 +1,105 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# Publish a Horizon UI container image to GHCR on:
+# - push to `main` (tagged with `main` + the full commit SHA)
+# - any `v*` tag (tagged with the version + the full commit SHA)
+#
+# The full commit SHA is the canonical, immutable identifier — moving tags
+# like `main` and `vX.Y.Z` are conveniences that *also* point at the same
+# SHA-pinned image. Operators should pull by SHA in production.
+name: Publish image
+
+on:
+ push:
+ branches:
+ - main
+ tags:
+ - 'v*'
+
+permissions:
+ contents: read
+ packages: write
+
+concurrency:
+ group: publish-image-${{ github.ref }}
+ cancel-in-progress: false
+
+env:
+ REGISTRY: ghcr.io
+ IMAGE_NAME: ${{ github.repository }}
+
+jobs:
+ build-and-push:
+ name: Build + push (${{ github.ref_type }} ${{ github.ref_name }})
+ runs-on: ubuntu-latest
+ timeout-minutes: 30
+ steps:
+ - uses: actions/checkout@v6
+ with:
+ persist-credentials: false
+
+ - name: Set up QEMU
+ uses: docker/setup-qemu-action@v3
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3
+
+ - name: Log in to GHCR
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ env.REGISTRY }}
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
+ # The canonical tag is the full 40-char commit SHA. We also publish
+ # moving tags so common workflows ("pull :main", "pull :v1.2.3")
+ # work — they all resolve back to the same SHA-tagged image.
+ - name: Compute image tags
+ id: meta
+ uses: docker/metadata-action@v5
+ with:
+ images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
+ tags: |
+ type=sha,format=long,prefix=
+ type=ref,event=branch,enable=${{ github.ref == 'refs/heads/main' }}
+ type=semver,pattern={{version}},enable=${{ github.ref_type == 'tag' }}
+ type=semver,pattern={{major}}.{{minor}},enable=${{ github.ref_type == 'tag' }}
+ type=raw,value=latest,enable=${{ github.ref_type == 'tag' }}
+ labels: |
+ org.opencontainers.image.source=https://github.com/${{ github.repository }}
+ org.opencontainers.image.revision=${{ github.sha }}
+ org.opencontainers.image.title=Apache SkyWalking Horizon UI
+ org.opencontainers.image.description=Next-generation web UI for Apache SkyWalking.
+ org.opencontainers.image.licenses=Apache-2.0
+
+ - name: Build + push
+ uses: docker/build-push-action@v6
+ with:
+ context: .
+ file: Dockerfile
+ push: true
+ platforms: linux/amd64,linux/arm64
+ tags: ${{ steps.meta.outputs.tags }}
+ labels: ${{ steps.meta.outputs.labels }}
+ cache-from: type=gha
+ cache-to: type=gha,mode=max
+
+ - name: Image SHA tag (canonical)
+ run: |
+ echo "::notice::Published ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@sha=${{ github.sha }}"
+ echo "All published tags:"
+ echo '${{ steps.meta.outputs.tags }}'
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..6069c06
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,69 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# ---- builder ----------------------------------------------------------------
+# Builds the BFF bundle + the Vite SPA in a single workspace install. Final
+# image only carries the BFF dist + UI dist + the production dependency tree
+# (no source, no devDependencies, no pnpm store).
+FROM node:20-alpine AS builder
+WORKDIR /workspace
+
+# argon2 (password hashing) builds a native module; alpine needs python + a
+# C toolchain at build time. The deps are dropped from the runtime stage.
+RUN apk add --no-cache python3 make g++ libc6-compat
+RUN corepack enable && corepack prepare pnpm@9.12.3 --activate
+
+COPY pnpm-workspace.yaml pnpm-lock.yaml package.json ./
+COPY apps/bff/package.json apps/bff/
+COPY apps/ui/package.json apps/ui/
+COPY packages/api-client/package.json packages/api-client/
+COPY packages/design-tokens/package.json packages/design-tokens/
+COPY packages/templates/package.json packages/templates/
+RUN pnpm install --frozen-lockfile
+
+COPY . .
+RUN pnpm --filter @skywalking-horizon-ui/bff build \
+ && pnpm --filter @skywalking-horizon-ui/ui build
+
+# Deploy a focused production install for the BFF — pnpm copies only the
+# packages it actually needs (workspace deps + runtime npm deps). This is
+# what ships in the final image.
+RUN pnpm deploy --filter @skywalking-horizon-ui/bff --prod /deploy/bff
+
+# ---- runtime ---------------------------------------------------------------
+FROM node:20-alpine
+WORKDIR /app
+
+# Run as a non-root user — the BFF doesn't need any privileged access.
+RUN addgroup -S horizon && adduser -S -G horizon horizon
+
+COPY --from=builder /deploy/bff/dist ./dist
+COPY --from=builder /deploy/bff/node_modules ./node_modules
+COPY --from=builder /deploy/bff/package.json ./package.json
+# The bundled layer + overview JSONs must sit one level up from the
+# compiled server (`/app/dist/server.js`). The loader resolves them via
+# `path.join(__dirname, '..', 'bundled_templates', ...)` — keeping this
+# layout in sync with the source tree means no path remapping at boot.
+COPY --from=builder /workspace/apps/bff/src/bundled_templates ./bundled_templates
+COPY --from=builder /workspace/apps/ui/dist ./static
+COPY --from=builder /workspace/horizon.example.yaml ./horizon.example.yaml
+
+ENV NODE_ENV=production \
+ HORIZON_STATIC_DIR=/app/static \
+ HORIZON_CONFIG=/app/horizon.yaml
+
+USER horizon
+EXPOSE 8081
+CMD ["node", "dist/server.js"]
diff --git a/apps/bff/src/bundled_templates/layers/activemq.json b/apps/bff/src/bundled_templates/layers/activemq.json
new file mode 100644
index 0000000..be8f133
--- /dev/null
+++ b/apps/bff/src/bundled_templates/layers/activemq.json
@@ -0,0 +1,59 @@
+{
+ "key": "ACTIVEMQ",
+ "alias": "ActiveMQ",
+ "color": "var(--sw-purple)",
+ "documentLink": "https://skywalking.apache.org/docs/main/next/en/setup/backend/backend-activemq-monitoring/",
+ "aliases": { "services": "ActiveMQ clusters", "instances": "Brokers", "endpoints": "Destinations" },
+ "components": { "service": true, "instances": true, "endpoints": true, "topology": false, "traces": false, "logs": false },
+ "layer-header": {
+ "orderBy": "enqueue",
+ "columns": [
+ { "metric": "enqueue", "label": "Enqueue/s", "mqe": "meter_activemq_cluster_enqueue_rate", "aggregation": "avg" },
+ { "metric": "dequeue", "label": "Dequeue/s", "mqe": "meter_activemq_cluster_dequeue_rate", "aggregation": "avg" },
+ { "metric": "load", "label": "System Load", "mqe": "meter_activemq_cluster_system_load_average/10000", "aggregation": "avg" },
+ { "metric": "threads", "label": "Threads", "mqe": "meter_activemq_cluster_thread_count", "aggregation": "avg" }
+ ]
+ },
+ "overview": {
+ "groups": [
+ { "title": "Throughput", "size": "auto", "metrics": [
+ { "id": "enqueue", "label": "Enqueue/s", "tip": "Messages enqueued per second.", "mqe": "meter_activemq_cluster_enqueue_rate", "aggregation": "avg" },
+ { "id": "dequeue", "label": "Dequeue/s", "tip": "Messages dequeued per second.", "mqe": "meter_activemq_cluster_dequeue_rate", "aggregation": "avg" },
+ { "id": "expired", "label": "Expired/s", "tip": "Messages that expired before consumption.", "mqe": "meter_activemq_cluster_expired_rate", "aggregation": "avg" }
+ ]}
+ ]
+ },
+ "dashboards": {
+ "service": [
+ { "id": "load", "title": "System Load Average", "type": "line", "expressions": ["meter_activemq_cluster_system_load_average/10000"], "span": 3, "rowSpan": 2 },
+ { "id": "threads", "title": "Thread Count", "type": "line", "expressions": ["meter_activemq_cluster_thread_count"], "span": 3, "rowSpan": 2 },
+ { "id": "heap_used", "title": "Heap Used (MB)", "type": "line", "unit": "MB", "expressions": ["meter_activemq_cluster_heap_memory_usage_used/1024/1024"], "span": 3, "rowSpan": 2 },
+ { "id": "heap_max", "title": "Heap Max (MB)", "type": "line", "unit": "MB", "expressions": ["latest(aggregate_labels(meter_activemq_cluster_heap_memory_usage_max,sum))/1024/1024"], "span": 3, "rowSpan": 2 },
+ { "id": "enqueue_dequeue", "title": "Enqueue / Dequeue / Dispatch /s", "type": "line", "expressions": ["meter_activemq_cluster_enqueue_rate", "meter_activemq_cluster_dequeue_rate", "meter_activemq_cluster_dispatch_rate"], "expressionLabels": ["enqueue", "dequeue", "dispatch"], "span": 6, "rowSpan": 2 },
+ { "id": "expired_rate", "title": "Expired /s", "type": "line", "expressions": ["meter_activemq_cluster_expired_rate"], "span": 6, "rowSpan": 2 },
+ { "id": "enqueue_time", "title": "Enqueue Time", "type": "line", "unit": "s", "expressions": ["meter_activemq_cluster_average_enqueue_time/1000", "meter_activemq_cluster_max_enqueue_time/1000"], "expressionLabels": ["avg", "max"], "span": 6, "rowSpan": 2 },
+ { "id": "gc_counts", "title": "GC Counts (G1+Parallel)", "type": "line", "expressions": ["view_as_seq(meter_activemq_cluster_gc_g1_old_collection_count,meter_activemq_cluster_gc_parallel_old_collection_count)", "view_as_seq(meter_activemq_cluster_gc_g1_young_collection_count,meter_activemq_cluster_gc_parallel_young_collection_count)"], "expressionLabels": ["old", "young"], "span": 6, "rowSpan": 2 },
+ { "id": "gc_time", "title": "GC Time (ms)", "type": "line", "unit": "ms", "expressions": ["view_as_seq(meter_activemq_cluster_gc_g1_old_collection_time,meter_activemq_cluster_gc_parallel_old_collection_time)", "view_as_seq(meter_activemq_cluster_gc_g1_young_collection_time,meter_activemq_cluster_gc_parallel_young_collection_time)"], "expressionLabels": ["old", "young"], "span": 12, "rowSpan": 2 }
+ ],
+ "instance": [
+ { "id": "slave", "title": "Slave Broker", "type": "line", "expressions": ["latest(meter_activemq_broker_state)"], "span": 3, "rowSpan": 2 },
+ { "id": "uptime", "title": "Uptime (hours)", "type": "line", "expressions": ["latest(meter_activemq_broker_uptime)/1000/60/60"], "span": 3, "rowSpan": 2 },
+ { "id": "conns", "title": "Connections", "type": "line", "expressions": ["meter_activemq_broker_current_connections"], "span": 3, "rowSpan": 2 },
+ { "id": "msg_size", "title": "Message Size (B)", "type": "line", "expressions": ["aggregate_labels(meter_activemq_broker_average_message_size,avg)", "aggregate_labels(meter_activemq_broker_max_message_size,max)"], "expressionLabels": ["avg", "max"], "span": 3, "rowSpan": 2 },
+ { "id": "producer_count", "title": "Producer Count", "type": "line", "expressions": ["aggregate_labels(meter_activemq_broker_producer_count,sum)", "latest(aggregate_labels(meter_activemq_broker_current_producer_count,sum))"], "expressionLabels": ["increased", "current"], "span": 4, "rowSpan": 2 },
+ { "id": "consumer_count", "title": "Consumer Count", "type": "line", "expressions": ["aggregate_labels(meter_activemq_broker_consumer_count,sum)", "latest(aggregate_labels(meter_activemq_broker_current_consumer_count,sum))"], "expressionLabels": ["increased", "current"], "span": 4, "rowSpan": 2 },
+ { "id": "eq_deq", "title": "Enqueue/Dequeue Count", "type": "line", "expressions": ["aggregate_labels(meter_activemq_broker_enqueue_count,sum)", "aggregate_labels(meter_activemq_broker_dequeue_count,sum)"], "expressionLabels": ["enqueue", "dequeue"], "span": 4, "rowSpan": 2 },
+ { "id": "eq_deq_rate", "title": "Enqueue/Dequeue Rate", "type": "line", "expressions": ["aggregate_labels(meter_activemq_broker_enqueue_rate,avg)", "aggregate_labels(meter_activemq_broker_dequeue_rate,avg)"], "expressionLabels": ["enqueue", "dequeue"], "span": 4, "rowSpan": 2 },
+ { "id": "memory_usage", "title": "Memory Usage (MB)", "type": "line", "unit": "MB", "expressions": ["aggregate_labels(meter_activemq_broker_memory_usage,sum)/1024/1024"], "span": 4, "rowSpan": 2 },
+ { "id": "limits", "title": "Usage Limits (GB)", "type": "line", "unit": "GB", "expressions": ["aggregate_labels(meter_activemq_broker_memory_limit,sum)/1024/1024/1024", "aggregate_labels(meter_activemq_broker_store_limit,sum)/1024/1024/1024", "aggregate_labels(meter_activemq_broker_temp_limit,sum)/1024/1024/1024"], "expressionLabels": ["memory", "store", "temp"], "span": 4, "rowSpan": 2 }
+ ],
+ "endpoint": [
+ { "id": "producer", "title": "Producer Count", "type": "line", "expressions": ["meter_activemq_destination_producer_count"], "span": 4, "rowSpan": 2 },
+ { "id": "consumer", "title": "Consumer Count", "type": "line", "expressions": ["meter_activemq_destination_consumer_count"], "span": 4, "rowSpan": 2 },
+ { "id": "queue_size", "title": "Queue Size", "type": "line", "expressions": ["meter_activemq_destination_queue_size"], "span": 4, "rowSpan": 2 },
+ { "id": "memory", "title": "Memory Usage (MB)", "type": "line", "unit": "MB", "expressions": ["meter_activemq_destination_memory_usage/1024/1024"], "span": 4, "rowSpan": 2 },
+ { "id": "messages", "title": "Message Counts", "type": "line", "expressions": ["meter_activemq_destination_enqueue_count", "meter_activemq_destination_dequeue_count", "meter_activemq_destination_dispatch_count", "meter_activemq_destination_expired_count", "meter_activemq_destination_inflight_count"], "expressionLabels": ["enqueue", "dequeue", "dispatch", "expired", "inflight"], "span": 8, "rowSpan": 2 },
+ { "id": "enqueue_time", "title": "Enqueue Time (s)", "type": "line", "unit": "s", "expressions": ["meter_activemq_destination_average_enqueue_time/1000", "meter_activemq_destination_max_enqueue_time/1000"], "expressionLabels": ["avg", "max"], "span": 12, "rowSpan": 2 }
+ ]
+ }
+}
diff --git a/apps/bff/src/bundled_templates/layers/alipay_mini_program.json b/apps/bff/src/bundled_templates/layers/alipay_mini_program.json
new file mode 100644
index 0000000..10e7b1e
--- /dev/null
+++ b/apps/bff/src/bundled_templates/layers/alipay_mini_program.json
@@ -0,0 +1,179 @@
+{
+ "key": "ALIPAY_MINI_PROGRAM",
+ "alias": "Alipay Mini Program",
+ "color": "var(--sw-cyan)",
+ "documentLink": "https://skywalking.apache.org/docs/main/next/en/setup/backend/backend-alipay-mini-program-monitoring/",
+ "aliases": {
+ "services": "Mini-programs",
+ "instances": "Versions",
+ "endpoints": "Pages"
+ },
+ "components": {
+ "service": true,
+ "instances": true,
+ "endpoints": true,
+ "topology": false,
+ "traces": true,
+ "logs": true
+ },
+ "layer-header": {
+ "orderBy": "cpm",
+ "columns": [
+ { "metric": "cpm", "label": "Request RPM", "unit": "rpm", "mqe": "meter_alipay_mp_request_cpm", "aggregation": "sum" },
+ { "metric": "launch", "label": "Launch", "unit": "ms", "mqe": "meter_alipay_mp_app_launch_duration", "aggregation": "avg" },
+ { "metric": "firstRender", "label": "First Render", "unit": "ms", "mqe": "meter_alipay_mp_first_render_duration", "aggregation": "avg" },
+ { "metric": "errors", "label": "Errors", "mqe": "meter_alipay_mp_error_count", "aggregation": "sum" }
+ ]
+ },
+ "overview": {
+ "groups": [
+ {
+ "title": "Latency & errors",
+ "size": "auto",
+ "metrics": [
+ { "id": "request_p95", "label": "Request P95", "tip": "95th-percentile request duration from the mini-program network bridge.", "mqe": "meter_alipay_mp_request_duration_percentile{p='95'}", "unit": "ms", "aggregation": "avg" },
+ { "id": "launch", "label": "Launch", "tip": "Approximate mini-program launch duration.", "mqe": "meter_alipay_mp_app_launch_duration", "unit": "ms", "aggregation": "avg" },
+ { "id": "errors", "label": "Errors", "tip": "Reported error count.", "mqe": "meter_alipay_mp_error_count", "aggregation": "sum" }
+ ]
+ },
+ {
+ "title": "Throughput",
+ "size": "square",
+ "metrics": [
+ { "id": "cpm", "label": "Request RPM", "tip": "Bridge requests per minute.", "mqe": "meter_alipay_mp_request_cpm", "unit": "rpm", "aggregation": "sum" }
+ ]
+ }
+ ]
+ },
+ "dashboards": {
+ "service": [
+ {
+ "id": "launch_duration",
+ "title": "App Launch Duration",
+ "tip": "Approximate cold-launch duration from the Alipay container.",
+ "type": "line",
+ "unit": "ms",
+ "expressions": ["meter_alipay_mp_app_launch_duration"],
+ "span": 4,
+ "rowSpan": 2
+ },
+ {
+ "id": "first_render",
+ "title": "First Render Duration",
+ "type": "line",
+ "unit": "ms",
+ "expressions": ["meter_alipay_mp_first_render_duration"],
+ "span": 4,
+ "rowSpan": 2
+ },
+ {
+ "id": "error_count",
+ "title": "Error Count",
+ "type": "line",
+ "expressions": ["meter_alipay_mp_error_count"],
+ "span": 4,
+ "rowSpan": 2
+ },
+ {
+ "id": "request_cpm",
+ "title": "Request Load",
+ "type": "line",
+ "unit": "rpm",
+ "expressions": ["meter_alipay_mp_request_cpm"],
+ "span": 6,
+ "rowSpan": 2
+ },
+ {
+ "id": "request_percentile",
+ "title": "Request Duration Percentile",
+ "type": "line",
+ "unit": "ms",
+ "expressions": [
+ "relabels(meter_alipay_mp_request_duration_percentile{p='50,75,90,95,99'},p='50,75,90,95,99',percentile='50,75,90,95,99')"
+ ],
+ "span": 6,
+ "rowSpan": 2
+ }
+ ],
+ "instance": [
+ {
+ "id": "v_launch",
+ "title": "Launch Duration",
+ "type": "line",
+ "unit": "ms",
+ "expressions": ["meter_alipay_mp_instance_app_launch_duration"],
+ "span": 4,
+ "rowSpan": 2
+ },
+ {
+ "id": "v_first_render",
+ "title": "First Render Duration",
+ "type": "line",
+ "unit": "ms",
+ "expressions": ["meter_alipay_mp_instance_first_render_duration"],
+ "span": 4,
+ "rowSpan": 2
+ },
+ {
+ "id": "v_request_cpm",
+ "title": "Request Load",
+ "type": "line",
+ "unit": "rpm",
+ "expressions": ["meter_alipay_mp_instance_request_cpm"],
+ "span": 4,
+ "rowSpan": 2
+ },
+ {
+ "id": "v_request_percentile",
+ "title": "Request Duration Percentile",
+ "type": "line",
+ "unit": "ms",
+ "expressions": [
+ "relabels(meter_alipay_mp_instance_request_duration_percentile{p='50,75,90,95,99'},p='50,75,90,95,99',percentile='50,75,90,95,99')"
+ ],
+ "span": 12,
+ "rowSpan": 2
+ }
+ ],
+ "endpoint": [
+ {
+ "id": "ep_launch",
+ "title": "Launch Duration on Page",
+ "type": "line",
+ "unit": "ms",
+ "expressions": ["meter_alipay_mp_endpoint_app_launch_duration"],
+ "span": 4,
+ "rowSpan": 2
+ },
+ {
+ "id": "ep_first_render",
+ "title": "First Render Duration",
+ "type": "line",
+ "unit": "ms",
+ "expressions": ["meter_alipay_mp_endpoint_first_render_duration"],
+ "span": 4,
+ "rowSpan": 2
+ },
+ {
+ "id": "ep_request_cpm",
+ "title": "Request Load",
+ "type": "line",
+ "unit": "rpm",
+ "expressions": ["meter_alipay_mp_endpoint_request_cpm"],
+ "span": 4,
+ "rowSpan": 2
+ },
+ {
+ "id": "ep_request_percentile",
+ "title": "Request Duration Percentile",
+ "type": "line",
+ "unit": "ms",
+ "expressions": [
+ "relabels(meter_alipay_mp_endpoint_request_duration_percentile{p='50,75,90,95,99'},p='50,75,90,95,99',percentile='50,75,90,95,99')"
+ ],
+ "span": 12,
+ "rowSpan": 2
+ }
+ ]
+ }
+}
diff --git a/apps/bff/src/bundled_templates/layers/apisix.json b/apps/bff/src/bundled_templates/layers/apisix.json
new file mode 100644
index 0000000..0e97e4b
--- /dev/null
+++ b/apps/bff/src/bundled_templates/layers/apisix.json
@@ -0,0 +1,67 @@
+{
+ "key": "APISIX",
+ "alias": "APISIX",
+ "color": "var(--sw-ok)",
+ "documentLink": "https://skywalking.apache.org/docs/main/next/en/setup/backend/backend-apisix-monitoring/",
+ "aliases": {
+ "services": "APISIX services",
+ "instances": "Nodes",
+ "endpoints": "Routes"
+ },
+ "components": {
+ "service": true,
+ "instances": true,
+ "endpoints": true,
+ "topology": false,
+ "traces": false,
+ "logs": false
+ },
+ "layer-header": {
+ "orderBy": "rps",
+ "columns": [
+ { "metric": "rps", "label": "RPS", "mqe": "aggregate_labels(meter_apisix_sv_http_requests,sum(service_instance_id))", "aggregation": "sum" },
+ { "metric": "status200", "label": "200/s", "mqe": "aggregate_labels(meter_apisix_sv_http_status_matched{code='200'},sum(code))", "aggregation": "sum" },
+ { "metric": "status404", "label": "404/s", "mqe": "aggregate_labels(meter_apisix_sv_http_status_matched{code='404'},sum(code))", "aggregation": "sum" },
+ { "metric": "status503", "label": "503/s", "mqe": "aggregate_labels(meter_apisix_sv_http_status_matched{code='503'},sum(code))", "aggregation": "sum" }
+ ]
+ },
+ "overview": {
+ "groups": [
+ {
+ "title": "Throughput",
+ "size": "auto",
+ "metrics": [
+ { "id": "rps", "label": "RPS", "tip": "Requests per second matched by APISIX routes.", "mqe": "aggregate_labels(meter_apisix_sv_http_requests,sum(service_instance_id))", "aggregation": "sum" },
+ { "id": "status5xx", "label": "5xx", "tip": "Upstream / gateway error class.", "mqe": "aggregate_labels(meter_apisix_sv_http_status_matched{code='503'},sum(code))", "aggregation": "sum" }
+ ]
+ }
+ ]
+ },
+ "dashboards": {
+ "service": [
+ { "id": "rps", "title": "HTTP Request Trend", "type": "line", "expressions": ["aggregate_labels(meter_apisix_sv_http_requests,sum(service_instance_id))"], "span": 6, "rowSpan": 2 },
+ { "id": "status", "title": "HTTP Status Trend", "type": "line", "expressions": ["aggregate_labels(meter_apisix_sv_http_status_matched,sum(code))"], "span": 6, "rowSpan": 2 },
+ { "id": "latency", "title": "HTTP Latency", "type": "line", "unit": "ms", "expressions": ["aggregate_labels(meter_apisix_sv_http_latency_matched,avg(type,p))"], "span": 6, "rowSpan": 2 },
+ { "id": "bandwidth", "title": "HTTP Bandwidth", "type": "line", "unit": "KB", "expressions": ["aggregate_labels(meter_apisix_sv_bandwidth_matched/1024,sum(type))"], "span": 6, "rowSpan": 2 },
+ { "id": "conns", "title": "HTTP Connections", "type": "line", "expressions": ["aggregate_labels(meter_apisix_sv_http_connections,sum(state))"], "span": 6, "rowSpan": 2 },
+ { "id": "status_unmatched", "title": "Non-matched Status Trend", "tip": "Requests with no matching APISIX route.", "type": "line", "expressions": ["aggregate_labels(meter_apisix_sv_http_status_unmatched,sum(code))"], "span": 6, "rowSpan": 2 },
+ { "id": "latency_unmatched", "title": "Non-matched Latency", "type": "line", "unit": "ms", "expressions": ["aggregate_labels(meter_apisix_sv_http_latency_unmatched,avg(type,p))"], "span": 6, "rowSpan": 2 },
+ { "id": "bandwidth_unmatched", "title": "Non-matched Bandwidth", "type": "line", "unit": "KB", "expressions": ["aggregate_labels(meter_apisix_sv_bandwidth_unmatched/1024,sum(type))"], "span": 6, "rowSpan": 2 }
+ ],
+ "instance": [
+ { "id": "rps", "title": "HTTP Request Trend", "type": "line", "expressions": ["meter_apisix_instance_http_requests"], "span": 4, "rowSpan": 2 },
+ { "id": "status", "title": "HTTP Status Trend", "type": "line", "expressions": ["meter_apisix_instance_http_status_matched"], "span": 4, "rowSpan": 2 },
+ { "id": "latency", "title": "HTTP Latency", "type": "line", "unit": "ms", "expressions": ["meter_apisix_instance_http_latency_matched"], "span": 4, "rowSpan": 2 },
+ { "id": "bandwidth", "title": "HTTP Bandwidth", "type": "line", "unit": "KB", "expressions": ["meter_apisix_instance_bandwidth_matched/1024"], "span": 4, "rowSpan": 2 },
+ { "id": "conns", "title": "HTTP Connections", "type": "line", "expressions": ["meter_apisix_instance_http_connections"], "span": 4, "rowSpan": 2 },
+ { "id": "shared_dict", "title": "Shared Dict", "tip": "Capacity vs free space.", "type": "line", "unit": "MB", "expressions": ["meter_apisix_instance_shared_dict_capacity_bytes/1024/1024", "meter_apisix_instance_shared_dict_free_space_bytes/1024/1024"], "expressionLabels": ["capacity", "free"], "span": 4, "rowSpan": 2 },
+ { "id": "etcd", "title": "etcd", "type": "line", "expressions": ["meter_apisix_instance_etcd_indexes", "latest(meter_apisix_instance_etcd_reachable)"], "expressionLabels": ["indexes", "reachable"], "span": 6, "rowSpan": 2 },
+ { "id": "unmatched", "title": "Non-matched Traffic", "tip": "Status + latency + bandwidth for requests with no matching route.", "type": "line", "expressions": ["meter_apisix_instance_http_status_unmatched", "meter_apisix_instance_http_latency_unmatched", "meter_apisix_instance_bandwidth_unmatched/1024"], "expressionLabels": ["status", "latency (ms)", "bandwidth (KB)"], "span": 6, "rowSpan": 2 }
+ ],
+ "endpoint": [
+ { "id": "status", "title": "HTTP Status Trend", "type": "line", "expressions": ["aggregate_labels(meter_apisix_endpoint_http_status,sum(code))"], "span": 4, "rowSpan": 2 },
+ { "id": "latency", "title": "HTTP Latency", "type": "line", "unit": "ms", "expressions": ["aggregate_labels(meter_apisix_endpoint_http_latency,avg(type,p))"], "span": 4, "rowSpan": 2 },
+ { "id": "bandwidth", "title": "HTTP Bandwidth", "type": "line", "unit": "KB", "expressions": ["aggregate_labels(meter_apisix_endpoint_bandwidth/1024,sum(type))"], "span": 4, "rowSpan": 2 }
+ ]
+ }
+}
diff --git a/apps/bff/src/bundled_templates/layers/aws_dynamodb.json b/apps/bff/src/bundled_templates/layers/aws_dynamodb.json
new file mode 100644
index 0000000..7961ddd
--- /dev/null
+++ b/apps/bff/src/bundled_templates/layers/aws_dynamodb.json
@@ -0,0 +1,57 @@
+{
+ "key": "AWS_DYNAMODB",
+ "alias": "AWS DynamoDB",
+ "color": "var(--sw-warn)",
+ "documentLink": "https://skywalking.apache.org/docs/main/next/en/setup/backend/backend-aws-dynamodb-monitoring/",
+ "aliases": { "services": "DynamoDB accounts", "endpoints": "Tables" },
+ "components": { "service": true, "instances": false, "endpoints": true, "topology": false, "traces": false, "logs": false },
+ "layer-header": {
+ "orderBy": "throttled",
+ "columns": [
+ { "metric": "throttledR", "label": "Read Throttled", "mqe": "aws_dynamodb_read_throttled_requests", "aggregation": "sum" },
+ { "metric": "throttledW", "label": "Write Throttled", "mqe": "aws_dynamodb_write_throttled_requests", "aggregation": "sum" },
+ { "metric": "errR", "label": "Read Sys Err", "mqe": "aws_dynamodb_read_system_errors", "aggregation": "sum" },
+ { "metric": "errW", "label": "Write Sys Err", "mqe": "aws_dynamodb_write_system_errors", "aggregation": "sum" }
+ ]
+ },
+ "overview": {
+ "groups": [
+ { "title": "Throttling", "size": "auto", "metrics": [
+ { "id": "throttledR", "label": "Read Throttled", "tip": "Read requests throttled — capacity exhaustion.", "mqe": "aws_dynamodb_read_throttled_requests", "aggregation": "sum" },
+ { "id": "throttledW", "label": "Write Throttled", "tip": "Write requests throttled.", "mqe": "aws_dynamodb_write_throttled_requests", "aggregation": "sum" },
+ { "id": "userErr", "label": "User Errors", "tip": "Bad-request style user errors.", "mqe": "aws_dynamodb_user_errors", "aggregation": "sum" }
+ ]}
+ ]
+ },
+ "dashboards": {
+ "service": [
+ { "id": "throttled", "title": "Throttled Requests", "type": "line", "expressions": ["aws_dynamodb_read_throttled_requests", "aws_dynamodb_write_throttled_requests"], "expressionLabels": ["read", "write"], "span": 4, "rowSpan": 2 },
+ { "id": "throttle_events", "title": "Throttle Events", "type": "line", "expressions": ["aws_dynamodb_read_throttle_events", "aws_dynamodb_write_throttle_events"], "expressionLabels": ["read", "write"], "span": 4, "rowSpan": 2 },
+ { "id": "sys_errors", "title": "System Errors", "type": "line", "expressions": ["aws_dynamodb_read_system_errors", "aws_dynamodb_write_system_errors"], "expressionLabels": ["read", "write"], "span": 4, "rowSpan": 2 },
+ { "id": "user_errors", "title": "User Errors", "type": "line", "expressions": ["aws_dynamodb_user_errors"], "span": 4, "rowSpan": 2 },
+ { "id": "cond_check_fail", "title": "Conditional Check Failed", "type": "line", "expressions": ["aws_dynamodb_conditional_check_failed_requests"], "span": 4, "rowSpan": 2 },
+ { "id": "txn_conflict", "title": "Transaction Conflict", "type": "line", "expressions": ["aws_dynamodb_transaction_conflict"], "span": 4, "rowSpan": 2 },
+ { "id": "read_usage", "title": "Read Capacity (unit/s)", "type": "line", "expressions": ["aws_dynamodb_provisioned_read_capacity_units", "aws_dynamodb_consumed_write_capacity_units"], "expressionLabels": ["provisioned", "consumed"], "span": 6, "rowSpan": 2 },
+ { "id": "write_usage", "title": "Write Capacity (unit/s)", "type": "line", "expressions": ["aws_dynamodb_provisioned_write_capacity_units", "aws_dynamodb_consumed_write_capacity_units"], "expressionLabels": ["provisioned", "consumed"], "span": 6, "rowSpan": 2 },
+ { "id": "latency", "title": "Successful Request Latency (ms)", "type": "line", "unit": "ms", "expressions": ["aws_dynamodb_get_successful_request_latency", "aws_dynamodb_put_successful_request_latency", "aws_dynamodb_query_successful_request_latency", "aws_dynamodb_scan_successful_request_latency"], "expressionLabels": ["get", "put", "query", "scan"], "span": 12, "rowSpan": 2 },
+ { "id": "ttl_deleted", "title": "TTL Deleted Items", "type": "line", "expressions": ["aws_dynamodb_time_to_live_deleted_item_count"], "span": 4, "rowSpan": 2 },
+ { "id": "scan_items", "title": "Scan Returned Items", "type": "line", "expressions": ["aws_dynamodb_scan_returned_item_count"], "span": 4, "rowSpan": 2 },
+ { "id": "query_items", "title": "Query Returned Items", "type": "line", "expressions": ["aws_dynamodb_query_returned_item_count"], "span": 4, "rowSpan": 2 },
+ { "id": "account_max", "title": "Account Max Reads / Writes", "type": "line", "expressions": ["aws_dynamodb_account_max_table_level_reads", "aws_dynamodb_account_max_table_level_writes", "aws_dynamodb_account_max_reads", "aws_dynamodb_account_max_writes"], "expressionLabels": ["table-level reads", "table-level writes", "reads", "writes"], "span": 6, "rowSpan": 2 },
+ { "id": "account_util", "title": "Account Capacity Utilization", "type": "line", "unit": "%", "expressions": ["aws_dynamodb_account_provisioned_read_capacity_utilization", "aws_dynamodb_account_provisioned_write_capacity_utilization"], "expressionLabels": ["read", "write"], "span": 6, "rowSpan": 2 }
+ ],
+ "endpoint": [
+ { "id": "throttled", "title": "Throttled Requests", "type": "line", "expressions": ["aws_dynamodb_endpoint_read_throttled_requests", "aws_dynamodb_endpoint_write_throttled_requests"], "expressionLabels": ["read", "write"], "span": 4, "rowSpan": 2 },
+ { "id": "throttle_events", "title": "Throttle Events", "type": "line", "expressions": ["aws_dynamodb_endpoint_read_throttle_events", "aws_dynamodb_endpoint_write_throttle_events"], "expressionLabels": ["read", "write"], "span": 4, "rowSpan": 2 },
+ { "id": "sys_errors", "title": "System Errors", "type": "line", "expressions": ["aws_dynamodb_endpoint_read_system_errors", "aws_dynamodb_endpoint_write_system_errors"], "expressionLabels": ["read", "write"], "span": 4, "rowSpan": 2 },
+ { "id": "cond_check", "title": "Conditional Check Failed", "type": "line", "expressions": ["aws_dynamodb_endpoint_conditional_check_failed_requests"], "span": 4, "rowSpan": 2 },
+ { "id": "txn_conflict", "title": "Transaction Conflict", "type": "line", "expressions": ["aws_dynamodb_endpoint_transaction_conflict"], "span": 4, "rowSpan": 2 },
+ { "id": "ttl_deleted", "title": "TTL Deleted Items", "type": "line", "expressions": ["aws_dynamodb_endpoint_time_to_live_deleted_item_count"], "span": 4, "rowSpan": 2 },
+ { "id": "read_usage", "title": "Read Capacity", "type": "line", "expressions": ["aws_dynamodb_endpoint_provisioned_read_capacity_units", "aws_dynamodb_endpoint_consumed_read_capacity_units"], "expressionLabels": ["provisioned", "consumed"], "span": 6, "rowSpan": 2 },
+ { "id": "write_usage", "title": "Write Capacity", "type": "line", "expressions": ["aws_dynamodb_endpoint_provisioned_write_capacity_units", "aws_dynamodb_endpoint_consumed_write_capacity_units"], "expressionLabels": ["provisioned", "consumed"], "span": 6, "rowSpan": 2 },
+ { "id": "latency", "title": "Successful Request Latency (ms)", "type": "line", "unit": "ms", "expressions": ["aws_dynamodb_endpoint_get_successful_request_latency", "aws_dynamodb_endpoint_put_successful_request_latency", "aws_dynamodb_endpoint_query_successful_request_latency", "aws_dynamodb_endpoint_scan_successful_request_latency"], "expressionLabels": ["get", "put", "query", "scan"], "span": 12, "rowSpan": 2 },
+ { "id": "scan_items", "title": "Scan Returned Items", "type": "line", "expressions": ["aws_dynamodb_endpoint_scan_returned_item_count"], "span": 6, "rowSpan": 2 },
+ { "id": "query_items", "title": "Query Returned Items", "type": "line", "expressions": ["aws_dynamodb_endpoint_query_returned_item_count"], "span": 6, "rowSpan": 2 }
+ ]
+ }
+}
diff --git a/apps/bff/src/bundled_templates/layers/aws_eks.json b/apps/bff/src/bundled_templates/layers/aws_eks.json
new file mode 100644
index 0000000..d4af024
--- /dev/null
+++ b/apps/bff/src/bundled_templates/layers/aws_eks.json
@@ -0,0 +1,46 @@
+{
+ "key": "AWS_EKS",
+ "alias": "AWS EKS",
+ "color": "var(--sw-warn)",
+ "documentLink": "https://skywalking.apache.org/docs/main/next/en/setup/backend/backend-aws-eks-monitoring/",
+ "aliases": { "services": "EKS services", "instances": "Nodes" },
+ "components": { "service": true, "instances": true, "endpoints": false, "topology": false, "traces": false, "logs": false },
+ "layer-header": {
+ "orderBy": "podCpu",
+ "columns": [
+ { "metric": "podCpu", "label": "Pod CPU %", "unit": "%", "mqe": "avg(eks_cluster_service_pod_cpu_utilization)", "aggregation": "avg" },
+ { "metric": "podMem", "label": "Pod Mem %", "unit": "%", "mqe": "avg(eks_cluster_service_pod_memory_utilization)", "aggregation": "avg" },
+ { "metric": "txErr", "label": "TX Err", "mqe": "avg(eks_cluster_service_pod_net_tx_error)", "aggregation": "avg" },
+ { "metric": "rxErr", "label": "RX Err", "mqe": "avg(eks_cluster_service_pod_net_rx_error)", "aggregation": "avg" }
+ ]
+ },
+ "overview": {
+ "groups": [
+ { "title": "Resource", "size": "auto", "metrics": [
+ { "id": "podCpu", "label": "Pod CPU %", "tip": "Service pods CPU utilization.", "mqe": "avg(eks_cluster_service_pod_cpu_utilization)", "unit": "%", "aggregation": "avg" },
+ { "id": "podMem", "label": "Pod Mem %", "tip": "Service pods memory utilization.", "mqe": "avg(eks_cluster_service_pod_memory_utilization)", "unit": "%", "aggregation": "avg" }
+ ]}
+ ]
+ },
+ "dashboards": {
+ "service": [
+ { "id": "pod_cpu", "title": "Pod CPU Utilization (%)", "type": "line", "unit": "%", "expressions": ["eks_cluster_service_pod_cpu_utilization"], "span": 4, "rowSpan": 2 },
+ { "id": "pod_mem", "title": "Pod Memory Utilization (%)", "type": "line", "unit": "%", "expressions": ["eks_cluster_service_pod_memory_utilization"], "span": 4, "rowSpan": 2 },
+ { "id": "pod_net_rx", "title": "Pod Network RX (KB/s)", "type": "line", "unit": "KB/s", "expressions": ["eks_cluster_service_pod_net_rx_bytes/1024"], "span": 4, "rowSpan": 2 },
+ { "id": "pod_net_rx_err", "title": "Pod RX Errors / s", "type": "line", "expressions": ["eks_cluster_service_pod_net_rx_error"], "span": 4, "rowSpan": 2 },
+ { "id": "pod_net_tx", "title": "Pod Network TX (KB/s)", "type": "line", "unit": "KB/s", "expressions": ["eks_cluster_service_pod_net_tx_bytes/1024"], "span": 4, "rowSpan": 2 },
+ { "id": "pod_net_tx_err", "title": "Pod TX Errors / s", "type": "line", "expressions": ["eks_cluster_service_pod_net_tx_error"], "span": 4, "rowSpan": 2 }
+ ],
+ "instance": [
+ { "id": "pods", "title": "Pod Count", "type": "line", "expressions": ["eks_cluster_node_pod_number"], "span": 3, "rowSpan": 2 },
+ { "id": "node_cpu", "title": "CPU Utilization (%)", "type": "line", "unit": "%", "expressions": ["eks_cluster_node_cpu_utilization"], "span": 3, "rowSpan": 2 },
+ { "id": "node_mem", "title": "Memory Utilization (%)", "type": "line", "unit": "%", "expressions": ["eks_cluster_node_memory_utilization"], "span": 3, "rowSpan": 2 },
+ { "id": "node_fs", "title": "FS Utilization (%)", "type": "line", "unit": "%", "expressions": ["eks_cluster_node_fs_utilization"], "span": 3, "rowSpan": 2 },
+ { "id": "node_net_rx", "title": "Network RX (KB/s)", "type": "line", "unit": "KB/s", "expressions": ["eks_cluster_node_net_rx_bytes/1024", "eks_cluster_node_net_rx_error"], "expressionLabels": ["bytes", "errors"], "expressionAxes": [0, 1], "span": 6, "rowSpan": 2 },
+ { "id": "node_net_tx", "title": "Network TX (KB/s)", "type": "line", "unit": "KB/s", "expressions": ["eks_cluster_node_net_tx_bytes/1024", "eks_cluster_node_net_tx_error"], "expressionLabels": ["bytes", "errors"], "expressionAxes": [0, 1], "span": 6, "rowSpan": 2 },
+ { "id": "disk", "title": "Disk IO (B/s)", "type": "line", "expressions": ["eks_cluster_node_disk_io_read", "eks_cluster_node_disk_io_write"], "expressionLabels": ["read", "write"], "span": 6, "rowSpan": 2 },
+ { "id": "pod_cpu", "title": "Pod CPU on Node", "type": "line", "unit": "%", "expressions": ["eks_cluster_node_pod_cpu_utilization"], "span": 6, "rowSpan": 2 },
+ { "id": "pod_mem", "title": "Pod Memory on Node", "type": "line", "unit": "%", "expressions": ["eks_cluster_node_pod_memory_utilization"], "span": 6, "rowSpan": 2 }
+ ]
+ }
+}
diff --git a/apps/bff/src/bundled_templates/layers/aws_gateway.json b/apps/bff/src/bundled_templates/layers/aws_gateway.json
new file mode 100644
index 0000000..2571ed5
--- /dev/null
+++ b/apps/bff/src/bundled_templates/layers/aws_gateway.json
@@ -0,0 +1,69 @@
+{
+ "key": "AWS_GATEWAY",
+ "alias": "AWS API Gateway",
+ "color": "var(--sw-warn)",
+ "documentLink": "https://skywalking.apache.org/docs/main/next/en/setup/backend/backend-aws-api-gateway-monitoring/",
+ "aliases": {
+ "services": "AWS Gateways",
+ "endpoints": "Routes"
+ },
+ "components": {
+ "service": true,
+ "instances": false,
+ "endpoints": true,
+ "topology": false,
+ "traces": false,
+ "logs": false
+ },
+ "layer-header": {
+ "orderBy": "count",
+ "columns": [
+ { "metric": "count", "label": "Requests", "mqe": "aws_gateway_service_count", "aggregation": "sum" },
+ { "metric": "latency", "label": "Latency", "unit": "ms", "mqe": "aws_gateway_service_latency", "aggregation": "avg" },
+ { "metric": "err4xx", "label": "4xx", "mqe": "aws_gateway_service_4xx", "aggregation": "sum" },
+ { "metric": "err5xx", "label": "5xx", "mqe": "aws_gateway_service_5xx", "aggregation": "sum" }
+ ]
+ },
+ "overview": {
+ "groups": [
+ {
+ "title": "Errors",
+ "size": "auto",
+ "metrics": [
+ { "id": "err4xx", "label": "4xx", "tip": "Client-error count from CloudWatch.", "mqe": "aws_gateway_service_4xx", "aggregation": "sum" },
+ { "id": "err5xx", "label": "5xx", "tip": "Server-error count from CloudWatch.", "mqe": "aws_gateway_service_5xx", "aggregation": "sum" },
+ { "id": "latency", "label": "Avg Latency", "tip": "Average end-to-end latency.", "mqe": "aws_gateway_service_latency", "unit": "ms", "aggregation": "avg" }
+ ]
+ },
+ {
+ "title": "Throughput",
+ "size": "square",
+ "metrics": [
+ { "id": "count", "label": "Requests", "tip": "Total request count in the window.", "mqe": "aws_gateway_service_count", "aggregation": "sum" }
+ ]
+ }
+ ]
+ },
+ "dashboards": {
+ "service": [
+ { "id": "count", "title": "Request Count", "type": "line", "expressions": ["aws_gateway_service_count"], "span": 4, "rowSpan": 2 },
+ { "id": "err4xx", "title": "4xx Count", "type": "line", "expressions": ["aws_gateway_service_4xx"], "span": 4, "rowSpan": 2 },
+ { "id": "err5xx", "title": "5xx Count", "type": "line", "expressions": ["aws_gateway_service_5xx"], "span": 4, "rowSpan": 2 },
+ { "id": "latency", "title": "Request Avg Latency", "type": "line", "unit": "ms", "expressions": ["aws_gateway_service_latency"], "span": 6, "rowSpan": 2 },
+ { "id": "integration_latency", "title": "Integration Avg Latency", "tip": "Latency between the gateway and the backend integration.", "type": "line", "unit": "ms", "expressions": ["aws_gateway_service_integration_latency"], "span": 6, "rowSpan": 2 },
+ { "id": "data", "title": "Data Processed (HTTP API only)", "type": "line", "unit": "KB", "expressions": ["aws_gateway_service_data_processed/1024"], "span": 4, "rowSpan": 2 },
+ { "id": "cache_hit", "title": "Cache Hit Rate (REST API only)", "type": "line", "unit": "%", "expressions": ["aws_gateway_service_cache_hit_rate"], "span": 4, "rowSpan": 2 },
+ { "id": "cache_miss", "title": "Cache Miss Rate (REST API only)", "type": "line", "unit": "%", "expressions": ["aws_gateway_service_cache_miss_rate"], "span": 4, "rowSpan": 2 }
+ ],
+ "endpoint": [
+ { "id": "count", "title": "Request Count", "type": "line", "expressions": ["aws_gateway_endpoint_count"], "span": 4, "rowSpan": 2 },
+ { "id": "err4xx", "title": "4xx Count", "type": "line", "expressions": ["aws_gateway_endpoint_4xx"], "span": 4, "rowSpan": 2 },
+ { "id": "err5xx", "title": "5xx Count", "type": "line", "expressions": ["aws_gateway_endpoint_5xx"], "span": 4, "rowSpan": 2 },
+ { "id": "latency", "title": "Request Avg Latency", "type": "line", "unit": "ms", "expressions": ["aws_gateway_endpoint_latency"], "span": 6, "rowSpan": 2 },
+ { "id": "integration_latency", "title": "Integration Avg Latency", "type": "line", "unit": "ms", "expressions": ["aws_gateway_endpoint_integration_latency"], "span": 6, "rowSpan": 2 },
+ { "id": "data", "title": "Data Processed", "type": "line", "unit": "KB", "expressions": ["aws_gateway_endpoint_data_processed/1024"], "span": 4, "rowSpan": 2 },
+ { "id": "cache_hit", "title": "Cache Hit Rate", "type": "line", "unit": "%", "expressions": ["aws_gateway_endpoint_cache_hit_rate"], "span": 4, "rowSpan": 2 },
+ { "id": "cache_miss", "title": "Cache Miss Rate", "type": "line", "unit": "%", "expressions": ["aws_gateway_endpoint_cache_miss_rate"], "span": 4, "rowSpan": 2 }
+ ]
+ }
+}
diff --git a/apps/bff/src/bundled_templates/layers/aws_s3.json b/apps/bff/src/bundled_templates/layers/aws_s3.json
new file mode 100644
index 0000000..22a0ca6
--- /dev/null
+++ b/apps/bff/src/bundled_templates/layers/aws_s3.json
@@ -0,0 +1,40 @@
+{
+ "key": "AWS_S3",
+ "alias": "AWS S3",
+ "color": "var(--sw-warn)",
+ "documentLink": "https://skywalking.apache.org/docs/main/next/en/setup/backend/backend-aws-s3-monitoring/",
+ "aliases": { "services": "S3 buckets" },
+ "components": { "service": true, "instances": false, "endpoints": false, "topology": false, "traces": false, "logs": false },
+ "layer-header": {
+ "orderBy": "all",
+ "columns": [
+ { "metric": "all", "label": "Requests", "mqe": "latest(aws_s3_all_requests)", "aggregation": "sum" },
+ { "metric": "latency", "label": "Avg Latency", "unit": "ms", "mqe": "avg(aws_s3_request_latency)", "aggregation": "avg" },
+ { "metric": "err4xx", "label": "4xx", "mqe": "latest(aws_s3_4xx)", "aggregation": "sum" },
+ { "metric": "err5xx", "label": "5xx", "mqe": "latest(aws_s3_5xx)", "aggregation": "sum" }
+ ]
+ },
+ "overview": {
+ "groups": [
+ { "title": "Errors", "size": "auto", "metrics": [
+ { "id": "err4xx", "label": "4xx Count", "tip": "Client errors observed by CloudWatch.", "mqe": "latest(aws_s3_4xx)", "aggregation": "sum" },
+ { "id": "err5xx", "label": "5xx Count", "tip": "Server errors observed by CloudWatch.", "mqe": "latest(aws_s3_5xx)", "aggregation": "sum" },
+ { "id": "latency", "label": "Avg Latency", "tip": "Average request latency.", "mqe": "avg(aws_s3_request_latency)", "unit": "ms", "aggregation": "avg" }
+ ]}
+ ]
+ },
+ "dashboards": {
+ "service": [
+ { "id": "all_req", "title": "All Request Count", "type": "line", "expressions": ["aws_s3_all_requests"], "span": 3, "rowSpan": 2 },
+ { "id": "get_req", "title": "GET Request Count", "type": "line", "expressions": ["aws_s3_get_requests"], "span": 3, "rowSpan": 2 },
+ { "id": "put_req", "title": "PUT Request Count", "type": "line", "expressions": ["aws_s3_put_requests"], "span": 3, "rowSpan": 2 },
+ { "id": "delete_req", "title": "DELETE Request Count", "type": "line", "expressions": ["aws_s3_delete_requests"], "span": 3, "rowSpan": 2 },
+ { "id": "err4xx", "title": "4xx Count", "type": "line", "expressions": ["aws_s3_4xx"], "span": 3, "rowSpan": 2 },
+ { "id": "err5xx", "title": "5xx Count", "type": "line", "expressions": ["aws_s3_5xx"], "span": 3, "rowSpan": 2 },
+ { "id": "latency", "title": "Request Avg Latency", "type": "line", "unit": "ms", "expressions": ["aws_s3_request_latency"], "span": 3, "rowSpan": 2 },
+ { "id": "first_byte_latency", "title": "First Byte Avg Latency", "type": "line", "unit": "ms", "expressions": ["aws_s3_first_latency_bytes"], "span": 3, "rowSpan": 2 },
+ { "id": "downloaded", "title": "Downloaded (KB)", "type": "line", "unit": "KB", "expressions": ["aws_s3_downloaded_bytes"], "span": 6, "rowSpan": 2 },
+ { "id": "uploaded", "title": "Uploaded (KB)", "type": "line", "unit": "KB", "expressions": ["aws_s3_uploaded_bytes"], "span": 6, "rowSpan": 2 }
+ ]
+ }
+}
diff --git a/apps/bff/src/bundled_templates/layers/banyandb.json b/apps/bff/src/bundled_templates/layers/banyandb.json
new file mode 100644
index 0000000..3e11ae8
--- /dev/null
+++ b/apps/bff/src/bundled_templates/layers/banyandb.json
@@ -0,0 +1,69 @@
+{
+ "key": "BANYANDB",
+ "alias": "BanyanDB",
+ "color": "var(--sw-cyan)",
+ "documentLink": "https://skywalking.apache.org/docs/main/next/en/banyandb/banyandb-introduction/",
+ "aliases": { "services": "BanyanDB clusters", "instances": "Nodes" },
+ "components": { "service": true, "instances": true, "endpoints": false, "topology": false, "traces": false, "logs": false },
+ "layer-header": {
+ "orderBy": "writeRate",
+ "columns": [
+ { "metric": "writeRate", "label": "Write/s", "mqe": "latest(meter_banyandb_write_rate)", "aggregation": "sum" },
+ { "metric": "queryRate", "label": "Query/s", "mqe": "latest(meter_banyandb_query_rate{method='query'})", "aggregation": "sum" },
+ { "metric": "errRate", "label": "Err/s", "mqe": "latest(meter_banyandb_write_and_query_errors_rate)", "aggregation": "sum" },
+ { "metric": "active", "label": "Active", "mqe": "meter_banyandb_active_instance", "aggregation": "sum" }
+ ]
+ },
+ "overview": {
+ "groups": [
+ { "title": "Throughput", "size": "auto", "metrics": [
+ { "id": "writeRate", "label": "Write/s", "tip": "Writes per second across the cluster.", "mqe": "latest(meter_banyandb_write_rate)", "aggregation": "sum" },
+ { "id": "queryRate", "label": "Query/s", "tip": "Queries per second.", "mqe": "latest(meter_banyandb_query_rate{method='query'})", "aggregation": "sum" },
+ { "id": "errRate", "label": "Errors/s", "tip": "Combined write + query error rate.", "mqe": "latest(meter_banyandb_write_and_query_errors_rate)", "aggregation": "sum" }
+ ]}
+ ]
+ },
+ "dashboards": {
+ "service": [
+ { "id": "write_rate", "title": "Write Rate", "type": "line", "expressions": ["latest(meter_banyandb_write_rate)"], "span": 4, "rowSpan": 2 },
+ { "id": "query_rate", "title": "Query Rate", "type": "line", "expressions": ["latest(meter_banyandb_query_rate{method='query'})"], "span": 4, "rowSpan": 2 },
+ { "id": "err_rate", "title": "Write/Query Error Rate", "type": "line", "expressions": ["latest(meter_banyandb_write_and_query_errors_rate)"], "span": 4, "rowSpan": 2 },
+ { "id": "memory", "title": "Total Memory (GB)", "type": "line", "unit": "GB", "expressions": ["latest(meter_banyandb_total_memory/1024/1024/1024)"], "span": 4, "rowSpan": 2 },
+ { "id": "disk", "title": "Disk Usage (GB)", "type": "line", "unit": "GB", "expressions": ["latest(meter_banyandb_disk_usage/1024/1024/1024)"], "span": 4, "rowSpan": 2 },
+ { "id": "cpu", "title": "Total CPU", "type": "line", "expressions": ["latest(meter_banyandb_total_cpu)"], "span": 4, "rowSpan": 2 },
+ { "id": "active", "title": "Active Instances", "type": "line", "expressions": ["meter_banyandb_active_instance"], "span": 4, "rowSpan": 2 },
+ { "id": "etcd", "title": "Etcd Operation Rate", "type": "line", "expressions": ["latest(meter_banyandb_etcd_operation_rate)"], "span": 4, "rowSpan": 2 },
+ { "id": "cpu_usage", "title": "CPU Usage", "type": "line", "expressions": ["meter_banyandb_cpu_usage/1000"], "span": 4, "rowSpan": 2 },
+ { "id": "rss", "title": "RSS Memory", "type": "line", "expressions": ["meter_banyandb_rss_memory_usage/1000"], "span": 4, "rowSpan": 2 },
+ { "id": "disk_all", "title": "Disk Usage (all)", "type": "line", "expressions": ["meter_banyandb_disk_usage_all/1000"], "span": 4, "rowSpan": 2 },
+ { "id": "network", "title": "Network (KB/s)", "type": "line", "unit": "KB/s", "expressions": ["meter_banyandb_network_usage_recv/1024", "meter_banyandb_network_usage_sent/1024"], "expressionLabels": ["recv", "sent"], "span": 4, "rowSpan": 2 },
+ { "id": "merge", "title": "Merge File Latency", "type": "line", "expressions": ["meter_banyandb_merge_file_latency/1000"], "span": 4, "rowSpan": 2 },
+ { "id": "merge_data", "title": "Merge File Data", "type": "line", "expressions": ["meter_banyandb_merge_file_data"], "span": 4, "rowSpan": 2 },
+ { "id": "merge_parts", "title": "Merge File Partitions", "type": "line", "expressions": ["meter_banyandb_merge_file_partitions"], "span": 4, "rowSpan": 2 },
+ { "id": "stream_write", "title": "Stream Write Rate", "type": "line", "expressions": ["meter_banyandb_stream_write_rate"], "span": 4, "rowSpan": 2 },
+ { "id": "group_write", "title": "Group Write Rate", "type": "line", "expressions": ["meter_banyandb_storage_write_rate/1000"], "span": 4, "rowSpan": 2 },
+ { "id": "series_write", "title": "Series Write Rate", "type": "line", "expressions": ["meter_banyandb_series_write_rate/1000"], "span": 4, "rowSpan": 2 },
+ { "id": "query_latency", "title": "Query Latency", "type": "line", "expressions": ["meter_banyandb_query_latency/1000"], "span": 4, "rowSpan": 2 },
+ { "id": "totals", "title": "Total Data + Series + Documents", "type": "line", "expressions": ["meter_banyandb_total_data", "meter_banyandb_total_series", "meter_banyandb_total_document"], "expressionLabels": ["data", "series", "documents"], "span": 8, "rowSpan": 2 },
+ { "id": "term_search", "title": "Term Search Rate", "type": "line", "expressions": ["meter_banyandb_term_search_rate/1000"], "span": 4, "rowSpan": 2 }
+ ],
+ "instance": [
+ { "id": "write_rate", "title": "Write Rate", "type": "line", "expressions": ["latest(meter_banyandb_instance_write_rate)"], "span": 4, "rowSpan": 2 },
+ { "id": "query_rate", "title": "Query Rate", "type": "line", "expressions": ["latest(meter_banyandb_instance_query_rate{method='query'})"], "span": 4, "rowSpan": 2 },
+ { "id": "err_rate", "title": "Error Rate", "type": "line", "expressions": ["latest(meter_banyandb_instance_write_and_query_errors_rate)"], "span": 4, "rowSpan": 2 },
+ { "id": "memory", "title": "Total Memory (GB)", "type": "line", "unit": "GB", "expressions": ["latest(meter_banyandb_instance_total_memory/1024/1024/1024)"], "span": 4, "rowSpan": 2 },
+ { "id": "disk", "title": "Disk Usage (GB)", "type": "line", "unit": "GB", "expressions": ["latest(meter_banyandb_instance_disk_usage/1024/1024/1024)"], "span": 4, "rowSpan": 2 },
+ { "id": "cpu", "title": "CPU", "type": "line", "expressions": ["meter_banyandb_instance_cpu_usage/1000"], "span": 4, "rowSpan": 2 },
+ { "id": "etcd", "title": "Etcd Operation Rate", "type": "line", "expressions": ["latest(meter_banyandb_instance_etcd_operation_rate)"], "span": 4, "rowSpan": 2 },
+ { "id": "rss", "title": "RSS Memory", "type": "line", "expressions": ["meter_banyandb_instance_rss_memory_usage/1000"], "span": 4, "rowSpan": 2 },
+ { "id": "network", "title": "Network (KB/s)", "type": "line", "unit": "KB/s", "expressions": ["meter_banyandb_instance_network_usage_recv/1024", "meter_banyandb_instance_network_usage_sent/1024"], "expressionLabels": ["recv", "sent"], "span": 4, "rowSpan": 2 },
+ { "id": "totals", "title": "Total Data + Series", "type": "line", "expressions": ["meter_banyandb_instance_total_data", "meter_banyandb_instance_total_series"], "expressionLabels": ["data", "series"], "span": 6, "rowSpan": 2 },
+ { "id": "merge", "title": "Merge File Latency / Data / Partitions", "type": "line", "expressions": ["meter_banyandb_instance_merge_file_latency/1000", "meter_banyandb_instance_merge_file_data", "meter_banyandb_instance_merge_file_partitions"], "expressionLabels": ["latency", "data", "partitions"], "span": 6, "rowSpan": 2 },
+ { "id": "stream_write", "title": "Stream Write Rate", "type": "line", "expressions": ["meter_banyandb_instance_stream_write_rate"], "span": 4, "rowSpan": 2 },
+ { "id": "group_write", "title": "Group Write Rate", "type": "line", "expressions": ["meter_banyandb_instance_storage_write_rate/1000"], "span": 4, "rowSpan": 2 },
+ { "id": "series_write", "title": "Series Write Rate", "type": "line", "expressions": ["meter_banyandb_instance_series_write_rate/1000"], "span": 4, "rowSpan": 2 },
+ { "id": "query_latency", "title": "Query Latency", "type": "line", "expressions": ["meter_banyandb_instance_query_latency/1000"], "span": 6, "rowSpan": 2 },
+ { "id": "term_search", "title": "Term Search Rate", "type": "line", "expressions": ["meter_banyandb_instance_term_search_rate/1000"], "span": 6, "rowSpan": 2 }
+ ]
+ }
+}
diff --git a/apps/bff/src/bundled_templates/layers/bookkeeper.json b/apps/bff/src/bundled_templates/layers/bookkeeper.json
new file mode 100644
index 0000000..e5ce3bb
--- /dev/null
+++ b/apps/bff/src/bundled_templates/layers/bookkeeper.json
@@ -0,0 +1,45 @@
+{
+ "key": "BOOKKEEPER",
+ "alias": "BookKeeper",
+ "color": "var(--sw-purple)",
+ "documentLink": "https://skywalking.apache.org/docs/main/next/en/setup/backend/backend-bookkeeper-monitoring/",
+ "aliases": { "services": "BookKeeper clusters", "instances": "Bookies" },
+ "components": { "service": true, "instances": true, "endpoints": false, "topology": false, "traces": false, "logs": false },
+ "layer-header": {
+ "orderBy": "ledgers",
+ "columns": [
+ { "metric": "ledgers", "label": "Ledgers", "mqe": "aggregate_labels(meter_bookkeeper_bookie_ledgers_count,sum)", "aggregation": "sum" },
+ { "metric": "entries", "label": "Entries", "mqe": "aggregate_labels(meter_bookkeeper_bookie_entries_count,sum)", "aggregation": "sum" },
+ { "metric": "writableDirs", "label": "Writable Dirs", "mqe": "aggregate_labels(meter_bookkeeper_bookie_ledger_writable_dirs,sum)", "aggregation": "sum" },
+ { "metric": "dirUsage", "label": "Dir Usage", "mqe": "aggregate_labels(meter_bookkeeper_bookie_ledger_dir_data_bookkeeper_ledgers_usage,sum)", "aggregation": "avg" }
+ ]
+ },
+ "overview": {
+ "groups": [
+ { "title": "Throughput", "size": "auto", "metrics": [
+ { "id": "writeRate", "label": "Write B/s", "tip": "Bookie write throughput across the cluster.", "mqe": "aggregate_labels(meter_bookkeeper_bookie_write_rate,sum)", "aggregation": "sum" },
+ { "id": "readRate", "label": "Read B/s", "tip": "Bookie read throughput across the cluster.", "mqe": "aggregate_labels(meter_bookkeeper_bookie_read_rate,sum)", "aggregation": "sum" }
+ ]}
+ ]
+ },
+ "dashboards": {
+ "service": [
+ { "id": "ledgers", "title": "Bookie Ledgers", "type": "line", "expressions": ["aggregate_labels(meter_bookkeeper_bookie_ledgers_count,sum)"], "span": 4, "rowSpan": 2 },
+ { "id": "entries", "title": "Bookie Entries", "type": "line", "expressions": ["aggregate_labels(meter_bookkeeper_bookie_entries_count,sum)"], "span": 4, "rowSpan": 2 },
+ { "id": "writable_dirs", "title": "Writable Ledger Dirs", "type": "line", "expressions": ["aggregate_labels(meter_bookkeeper_bookie_ledger_writable_dirs,sum)"], "span": 4, "rowSpan": 2 },
+ { "id": "write_cache", "title": "Write Cache", "type": "line", "expressions": ["aggregate_labels(meter_bookkeeper_bookie_write_cache_size,sum)/1024/1024", "aggregate_labels(meter_bookkeeper_bookie_write_cache_count,sum)"], "expressionLabels": ["size (MB)", "entries"], "expressionAxes": [0, 1], "span": 6, "rowSpan": 2 },
+ { "id": "read_cache", "title": "Read Cache", "type": "line", "expressions": ["aggregate_labels(meter_bookkeeper_bookie_read_cache_size,sum)/1024/1024", "aggregate_labels(meter_bookkeeper_bookie_read_cache_count,sum)"], "expressionLabels": ["size (MB)", "entries"], "expressionAxes": [0, 1], "span": 6, "rowSpan": 2 },
+ { "id": "io_rates", "title": "Read / Write Rate (B/s)", "type": "line", "expressions": ["aggregate_labels(meter_bookkeeper_bookie_read_rate,sum)", "aggregate_labels(meter_bookkeeper_bookie_write_rate,sum)"], "expressionLabels": ["read", "write"], "span": 6, "rowSpan": 2 },
+ { "id": "dir_usage", "title": "Ledger Dir Usage", "type": "line", "expressions": ["aggregate_labels(meter_bookkeeper_bookie_ledger_dir_data_bookkeeper_ledgers_usage,sum)"], "span": 6, "rowSpan": 2 }
+ ],
+ "instance": [
+ { "id": "jvm_pool", "title": "JVM Memory Pool (MB)", "type": "line", "unit": "MB", "expressions": ["meter_bookkeeper_node_jvm_memory_pool_used/1024/1024"], "span": 6, "rowSpan": 2 },
+ { "id": "jvm_mem", "title": "JVM Memory (MB)", "type": "line", "unit": "MB", "expressions": ["meter_bookkeeper_node_jvm_memory_used/1024/1024", "meter_bookkeeper_node_jvm_memory_committed/1024/1024", "meter_bookkeeper_node_jvm_memory_init/1024/1024"], "expressionLabels": ["used", "committed", "init"], "span": 6, "rowSpan": 2 },
+ { "id": "jvm_threads", "title": "JVM Threads", "type": "line", "expressions": ["meter_bookkeeper_node_jvm_threads_current", "meter_bookkeeper_node_jvm_threads_daemon", "meter_bookkeeper_node_jvm_threads_peak", "meter_bookkeeper_node_jvm_threads_deadlocked"], "expressionLabels": ["current", "daemon", "peak", "deadlocked"], "span": 6, "rowSpan": 2 },
+ { "id": "gc", "title": "GC", "type": "line", "expressions": ["meter_bookkeeper_node_jvm_gc_collection_seconds_sum", "meter_bookkeeper_node_jvm_gc_collection_seconds_count"], "expressionLabels": ["seconds", "count"], "expressionAxes": [0, 1], "span": 6, "rowSpan": 2 },
+ { "id": "thread_executor", "title": "Thread Executor", "type": "line", "expressions": ["meter_bookkeeper_node_thread_executor_completed", "meter_bookkeeper_node_thread_executor_tasks_completed", "meter_bookkeeper_node_thread_executor_tasks_rejected", "meter_bookkeeper_node_thread_executor_tasks_failed"], "expressionLabels": ["completed", "tasks completed", "rejected", "failed"], "span": 6, "rowSpan": 2 },
+ { "id": "pooled_threads", "title": "Pooled Threads", "type": "line", "expressions": ["meter_bookkeeper_node_high_priority_threads", "meter_bookkeeper_node_read_thread_pool_threads"], "expressionLabels": ["high-priority", "read"], "span": 6, "rowSpan": 2 },
+ { "id": "queue_size", "title": "Pool Max Queue Size", "type": "line", "expressions": ["meter_bookkeeper_node_high_priority_thread_max_queue_size", "meter_bookkeeper_node_read_thread_pool_max_queue_size"], "expressionLabels": ["high-priority", "read"], "span": 12, "rowSpan": 2 }
+ ]
+ }
+}
diff --git a/apps/bff/src/bundled_templates/layers/browser.json b/apps/bff/src/bundled_templates/layers/browser.json
new file mode 100644
index 0000000..3c31b33
--- /dev/null
+++ b/apps/bff/src/bundled_templates/layers/browser.json
@@ -0,0 +1,244 @@
+{
+ "key": "BROWSER",
+ "alias": "Browser",
+ "color": "var(--sw-cyan)",
+ "documentLink": "https://skywalking.apache.org/docs/main/next/en/setup/service-agent/browser-agent/",
+ "aliases": {
+ "services": "Apps",
+ "instances": "Versions",
+ "endpoints": "Pages"
+ },
+ "components": {
+ "service": true,
+ "instances": true,
+ "endpoints": true,
+ "topology": false,
+ "traces": true,
+ "logs": true
+ },
+ "layer-header": {
+ "orderBy": "pv",
+ "columns": [
+ { "metric": "pv", "label": "Page Views", "unit": "/min", "mqe": "browser_app_pv", "aggregation": "sum" },
+ { "metric": "errorRate", "label": "Error Rate", "unit": "%", "mqe": "browser_app_error_rate/100", "aggregation": "avg" },
+ { "metric": "errorSum", "label": "Errors", "mqe": "browser_app_error_sum", "aggregation": "sum" }
+ ]
+ },
+ "overview": {
+ "groups": [
+ {
+ "title": "Page health",
+ "size": "auto",
+ "metrics": [
+ { "id": "fmp_p95", "label": "FMP P95", "tip": "95th-percentile first-meaningful-paint latency across the app.", "mqe": "browser_app_page_fmp_percentile{p='95'}", "unit": "ms", "aggregation": "avg" },
+ { "id": "errorRate", "label": "Error Rate", "tip": "Percentage of page loads that produced an error.", "mqe": "browser_app_error_rate/100", "unit": "%", "aggregation": "avg" }
+ ]
+ },
+ {
+ "title": "Throughput",
+ "size": "square",
+ "metrics": [
+ { "id": "pv", "label": "PV / min", "tip": "Page views per minute.", "mqe": "browser_app_pv", "unit": "/min", "aggregation": "sum" }
+ ]
+ }
+ ]
+ },
+ "dashboards": {
+ "service": [
+ {
+ "id": "app_pv",
+ "title": "App Load (PV)",
+ "type": "line",
+ "unit": "/min",
+ "expressions": ["browser_app_pv"],
+ "span": 4,
+ "rowSpan": 2
+ },
+ {
+ "id": "app_error_rate",
+ "title": "App Error Rate",
+ "type": "line",
+ "unit": "%",
+ "expressions": ["browser_app_error_rate/100"],
+ "span": 4,
+ "rowSpan": 2
+ },
+ {
+ "id": "app_error_sum",
+ "title": "App Error Count",
+ "type": "line",
+ "expressions": ["browser_app_error_sum"],
+ "span": 4,
+ "rowSpan": 2
+ },
+ {
+ "id": "top_pages_pv",
+ "title": "Top Hot Pages",
+ "tip": "Pages ranked by page views, errors, and error rate.",
+ "type": "top",
+ "expressions": [
+ "top_n(browser_app_page_pv,10,des)",
+ "top_n(browser_app_page_error_sum,10,des)",
+ "top_n(browser_app_page_error_rate,10,des)/100"
+ ],
+ "expressionLabels": ["PV", "Errors", "Error Rate"],
+ "expressionUnits": ["/min", "", "%"],
+ "span": 6,
+ "rowSpan": 3
+ },
+ {
+ "id": "top_versions",
+ "title": "Top Versions",
+ "tip": "App version breakdown — load + errors + error rate.",
+ "type": "top",
+ "expressions": [
+ "top_n(browser_app_single_version_pv,10,des)",
+ "top_n(browser_app_single_version_error_sum,10,des)",
+ "top_n(browser_app_single_version_error_rate,10,des)/100"
+ ],
+ "expressionLabels": ["PV", "Errors", "Error Rate"],
+ "expressionUnits": ["/min", "", "%"],
+ "span": 6,
+ "rowSpan": 3
+ }
+ ],
+ "instance": [
+ {
+ "id": "ver_pv",
+ "title": "Version PV",
+ "type": "line",
+ "unit": "/min",
+ "expressions": ["browser_app_single_version_pv"],
+ "span": 4,
+ "rowSpan": 2
+ },
+ {
+ "id": "ver_error_rate",
+ "title": "Version Error Rate",
+ "type": "line",
+ "unit": "%",
+ "expressions": ["browser_app_single_version_error_rate/100"],
+ "span": 4,
+ "rowSpan": 2
+ },
+ {
+ "id": "ver_error_sum",
+ "title": "Version Error Count",
+ "type": "line",
+ "expressions": ["browser_app_single_version_error_sum"],
+ "span": 4,
+ "rowSpan": 2
+ }
+ ],
+ "endpoint": [
+ {
+ "id": "page_fmp_percentile",
+ "title": "First Meaningful Paint Percentile",
+ "tip": "p50–p99 of FMP latency for the selected page. Below 1s for p75 is a common target.",
+ "type": "line",
+ "unit": "ms",
+ "expressions": [
+ "relabels(browser_app_page_fmp_percentile{p='50,75,90,95,99'},p='50,75,90,95,99',percentile='50,75,90,95,99')"
+ ],
+ "span": 6,
+ "rowSpan": 2
+ },
+ {
+ "id": "page_load_percentile",
+ "title": "Page Load Percentile",
+ "type": "line",
+ "unit": "ms",
+ "expressions": [
+ "relabels(browser_app_page_load_page_percentile{p='50,75,90,95,99'},p='50,75,90,95,99',percentile='50,75,90,95,99')"
+ ],
+ "span": 6,
+ "rowSpan": 2
+ },
+ {
+ "id": "page_ttl_percentile",
+ "title": "Time-to-Live Percentile",
+ "type": "line",
+ "unit": "ms",
+ "expressions": [
+ "relabels(browser_app_page_ttl_percentile{p='50,75,90,95,99'},p='50,75,90,95,99',percentile='50,75,90,95,99')"
+ ],
+ "span": 6,
+ "rowSpan": 2
+ },
+ {
+ "id": "page_first_pack_percentile",
+ "title": "First Pack Latency Percentile",
+ "type": "line",
+ "unit": "ms",
+ "expressions": [
+ "relabels(browser_app_page_first_pack_percentile{p='50,75,90,95,99'},p='50,75,90,95,99',percentile='50,75,90,95,99')"
+ ],
+ "span": 6,
+ "rowSpan": 2
+ },
+ {
+ "id": "page_performance",
+ "title": "Page Performance Breakdown",
+ "tip": "Avg time spent in each phase of page load — resolve, redirect, DNS, TCP, TTFB, transfer, DOM analysis, DOM ready, FPT, full load.",
+ "type": "line",
+ "unit": "ms",
+ "expressions": [
+ "browser_app_page_dns_avg",
+ "browser_app_page_redirect_avg",
+ "browser_app_page_tcp_avg",
+ "browser_app_page_ttfb_avg",
+ "browser_app_page_trans_avg",
+ "browser_app_page_dom_analysis_avg",
+ "browser_app_page_dom_ready_avg",
+ "browser_app_page_fpt_avg",
+ "browser_app_page_load_page_avg",
+ "browser_app_page_res_avg"
+ ],
+ "expressionLabels": ["dns", "redirect", "tcp", "ttfb", "transfer", "dom analysis", "dom ready", "fpt", "load", "resource"],
+ "span": 12,
+ "rowSpan": 3
+ },
+ {
+ "id": "page_error_breakdown",
+ "title": "Page Errors by Type",
+ "tip": "Resource / JS / AJAX / unknown error counters per minute.",
+ "type": "line",
+ "expressions": [
+ "browser_app_page_resource_error_sum",
+ "browser_app_page_js_error_sum",
+ "browser_app_page_ajax_error_sum",
+ "browser_app_page_unknown_error_sum"
+ ],
+ "expressionLabels": ["resource", "js", "ajax", "unknown"],
+ "span": 6,
+ "rowSpan": 2
+ },
+ {
+ "id": "web_vitals",
+ "title": "Web Vitals",
+ "tip": "Core Web Vitals — FMP, LCP, CLS (avg per minute). CLS is divided by 1000 to bring it into the typical 0–1 score range.",
+ "type": "line",
+ "expressions": [
+ "browser_app_web_vitals_fmp_avg",
+ "browser_app_web_vitals_lcp_avg",
+ "browser_app_web_vitals_cls_avg / 1000"
+ ],
+ "expressionLabels": ["FMP (ms)", "LCP (ms)", "CLS"],
+ "span": 6,
+ "rowSpan": 2
+ },
+ {
+ "id": "inp_percentile",
+ "title": "Interaction to Next Paint Percentile",
+ "tip": "INP — responsiveness metric replacing FID.",
+ "type": "line",
+ "unit": "ms",
+ "expressions": [
+ "relabels(browser_app_web_interaction_inp_percentile{p='50,75,90,95,99'},p='50,75,90,95,99',percentile='50,75,90,95,99')"
+ ],
+ "span": 12,
+ "rowSpan": 2
+ }
+ ]
+ }
+}
diff --git a/apps/bff/src/bundled_templates/layers/cilium_service.json b/apps/bff/src/bundled_templates/layers/cilium_service.json
new file mode 100644
index 0000000..327e93f
--- /dev/null
+++ b/apps/bff/src/bundled_templates/layers/cilium_service.json
@@ -0,0 +1,73 @@
+{
+ "key": "CILIUM_SERVICE",
+ "alias": "Cilium Service",
+ "color": "var(--sw-info)",
+ "documentLink": "https://skywalking.apache.org/docs/main/next/en/setup/backend/backend-k8s-monitoring-cilium/",
+ "aliases": { "services": "Services", "instances": "Pods", "endpoints": "Endpoints" },
+ "components": { "service": true, "instances": true, "endpoints": true, "topology": true, "traces": false, "logs": false },
+ "layer-header": {
+ "orderBy": "cpm",
+ "columns": [
+ { "metric": "cpm", "label": "RPM", "unit": "rpm", "mqe": "cilium_service_protocol_cpm", "aggregation": "sum" },
+ { "metric": "respTime", "label": "Latency", "unit": "ms", "mqe": "cilium_service_protocol_call_duration/cilium_service_protocol_cpm/1000000", "aggregation": "avg" },
+ { "metric": "sla", "label": "Success Rate", "unit": "%", "mqe": "cilium_service_protocol_call_success_count/cilium_service_protocol_cpm*100", "aggregation": "avg" },
+ { "metric": "drop", "label": "TCP Drop", "mqe": "cilium_service_l4_read_pkg_drop_cpm + cilium_service_l4_write_pkg_drop_cpm", "aggregation": "sum" }
+ ]
+ },
+ "overview": {
+ "groups": [
+ { "title": "Latency & errors", "size": "auto", "metrics": [
+ { "id": "respTime", "label": "Avg Latency", "tip": "Average protocol latency (ms).", "mqe": "cilium_service_protocol_call_duration/cilium_service_protocol_cpm/1000000", "unit": "ms", "aggregation": "avg" },
+ { "id": "successRate", "label": "Success Rate", "tip": "Successful calls / total calls.", "mqe": "cilium_service_protocol_call_success_count/cilium_service_protocol_cpm*100", "unit": "%", "aggregation": "avg" }
+ ]},
+ { "title": "Throughput", "size": "square", "metrics": [
+ { "id": "cpm", "label": "Protocol RPM", "tip": "Protocol calls per minute.", "mqe": "cilium_service_protocol_cpm", "aggregation": "sum" }
+ ]}
+ ]
+ },
+ "dashboards": {
+ "service": [
+ { "id": "l4_read", "title": "L4 Read Packages/min", "type": "line", "expressions": ["cilium_service_l4_read_pkg_cpm"], "span": 3, "rowSpan": 2 },
+ { "id": "l4_write", "title": "L4 Write Packages/min", "type": "line", "expressions": ["cilium_service_l4_write_pkg_cpm"], "span": 3, "rowSpan": 2 },
+ { "id": "tcp_drop", "title": "TCP Drop / min", "type": "line", "expressions": ["cilium_service_l4_read_pkg_drop_cpm", "cilium_service_l4_write_pkg_drop_cpm"], "expressionLabels": ["read", "write"], "span": 3, "rowSpan": 2 },
+ { "id": "tcp_drop_reason", "title": "TCP Drop by Reason", "type": "line", "expressions": ["cilium_service_l4_drop_reason_count"], "span": 3, "rowSpan": 2 },
+ { "id": "http_load", "title": "HTTP Load", "type": "line", "expressions": ["cilium_service_protocol_http_call_cpm", "cilium_service_protocol_http_call_success_count"], "expressionLabels": ["calls", "success"], "span": 4, "rowSpan": 2 },
+ { "id": "http_duration", "title": "HTTP Duration", "type": "line", "unit": "ms", "expressions": ["cilium_service_protocol_http_call_duration/cilium_service_protocol_http_call_cpm/1000000"], "span": 4, "rowSpan": 2 },
+ { "id": "http_status", "title": "HTTP Non-2xx Status", "type": "line", "expressions": ["cilium_service_protocol_http_status_1xx_cpm", "cilium_service_protocol_http_status_3xx_cpm", "cilium_service_protocol_http_status_4xx_cpm", "cilium_service_protocol_http_status_5xx_cpm"], "expressionLabels": ["1xx", "3xx", "4xx", "5xx"], "span": 4, "rowSpan": 2 },
+ { "id": "dns_load", "title": "DNS Load", "type": "line", "expressions": ["cilium_service_protocol_dns_call_cpm", "cilium_service_protocol_dns_call_success_count"], "expressionLabels": ["calls", "success"], "span": 4, "rowSpan": 2 },
+ { "id": "dns_duration", "title": "DNS Duration", "type": "line", "unit": "ms", "expressions": ["cilium_service_protocol_dns_call_duration/cilium_service_protocol_dns_call_cpm/1000000"], "span": 4, "rowSpan": 2 },
+ { "id": "dns_error", "title": "DNS Errors / min", "type": "line", "expressions": ["cilium_service_protocol_dns_error_count"], "span": 4, "rowSpan": 2 },
+ { "id": "kafka_load", "title": "Kafka Load", "type": "line", "expressions": ["cilium_service_protocol_kafka_call_cpm", "cilium_service_protocol_kafka_call_success_count"], "expressionLabels": ["calls", "success"], "span": 4, "rowSpan": 2 },
+ { "id": "kafka_duration", "title": "Kafka Duration", "type": "line", "unit": "ms", "expressions": ["cilium_service_protocol_kafka_call_duration/cilium_service_protocol_kafka_call_cpm/1000000"], "span": 4, "rowSpan": 2 },
+ { "id": "kafka_error", "title": "Kafka Errors / min", "type": "line", "expressions": ["cilium_service_protocol_kafka_call_error_count"], "span": 4, "rowSpan": 2 }
+ ],
+ "instance": [
+ { "id": "l4_read", "title": "L4 Read Packages/min", "type": "line", "expressions": ["cilium_service_instance_l4_read_pkg_cpm"], "span": 3, "rowSpan": 2 },
+ { "id": "l4_write", "title": "L4 Write Packages/min", "type": "line", "expressions": ["cilium_service_instance_l4_write_pkg_cpm"], "span": 3, "rowSpan": 2 },
+ { "id": "http_load", "title": "HTTP Load", "type": "line", "expressions": ["cilium_service_instance_protocol_http_call_cpm", "cilium_service_instance_protocol_http_call_success_count"], "expressionLabels": ["calls", "success"], "span": 3, "rowSpan": 2 },
+ { "id": "http_status", "title": "HTTP Non-2xx Status", "type": "line", "expressions": ["cilium_service_instance_protocol_http_status_1xx_cpm", "cilium_service_instance_protocol_http_status_3xx_cpm", "cilium_service_instance_protocol_http_status_4xx_cpm", "cilium_service_instance_protocol_http_status_5xx_cpm"], "expressionLabels": ["1xx", "3xx", "4xx", "5xx"], "span": 3, "rowSpan": 2 },
+ { "id": "dns_load", "title": "DNS Load", "type": "line", "expressions": ["cilium_service_instance_protocol_dns_call_cpm", "cilium_service_instance_protocol_dns_call_success_count"], "expressionLabels": ["calls", "success"], "span": 4, "rowSpan": 2 },
+ { "id": "dns_error", "title": "DNS Errors", "type": "line", "expressions": ["cilium_service_instance_protocol_dns_error_count"], "span": 4, "rowSpan": 2 },
+ { "id": "kafka_load", "title": "Kafka Load", "type": "line", "expressions": ["cilium_service_instance_protocol_kafka_call_cpm", "cilium_service_instance_protocol_kafka_call_success_count"], "expressionLabels": ["calls", "success"], "span": 4, "rowSpan": 2 }
+ ],
+ "endpoint": [
+ { "id": "http_load", "title": "HTTP Load", "type": "line", "expressions": ["cilium_endpoint_protocol_http_call_cpm", "cilium_endpoint_protocol_http_call_success_count"], "expressionLabels": ["calls", "success"], "span": 4, "rowSpan": 2 },
+ { "id": "http_duration", "title": "HTTP Duration", "type": "line", "unit": "ms", "expressions": ["cilium_endpoint_protocol_http_call_duration/cilium_endpoint_protocol_http_call_cpm/1000000"], "span": 4, "rowSpan": 2 },
+ { "id": "http_status", "title": "HTTP Non-2xx Status", "type": "line", "expressions": ["cilium_endpoint_protocol_http_status_1xx_cpm", "cilium_endpoint_protocol_http_status_3xx_cpm", "cilium_endpoint_protocol_http_status_4xx_cpm", "cilium_endpoint_protocol_http_status_5xx_cpm"], "expressionLabels": ["1xx", "3xx", "4xx", "5xx"], "span": 4, "rowSpan": 2 },
+ { "id": "dns_load", "title": "DNS Load", "type": "line", "expressions": ["cilium_endpoint_protocol_dns_call_cpm", "cilium_endpoint_protocol_dns_call_success_count"], "expressionLabels": ["calls", "success"], "span": 4, "rowSpan": 2 },
+ { "id": "dns_duration", "title": "DNS Duration", "type": "line", "unit": "ms", "expressions": ["cilium_endpoint_protocol_dns_call_duration/cilium_endpoint_protocol_dns_call_cpm/1000000"], "span": 4, "rowSpan": 2 },
+ { "id": "dns_error", "title": "DNS Errors", "type": "line", "expressions": ["cilium_endpoint_protocol_dns_error_count"], "span": 4, "rowSpan": 2 }
+ ]
+ },
+ "topology": {
+ "nodeMetrics": [
+ { "id": "cpm", "label": "RPM", "mqe": "cilium_service_protocol_cpm", "unit": "rpm", "role": "center", "aggregation": "avg" },
+ { "id": "success", "label": "Success %", "mqe": "cilium_service_protocol_call_success_count/cilium_service_protocol_cpm*100", "unit": "%", "role": "ring", "aggregation": "avg" },
+ { "id": "respTime", "label": "Latency", "mqe": "cilium_service_protocol_call_duration/cilium_service_protocol_cpm/1000000", "unit": "ms", "role": "secondary", "aggregation": "avg" }
+ ],
+ "linkServerMetrics": [
+ { "id": "cpm", "label": "HTTP RPM", "mqe": "cilium_service_relation_server_protocol_http_call_cpm", "unit": "rpm", "role": "lineServer", "aggregation": "avg" },
+ { "id": "respTime", "label": "Avg Latency", "mqe": "cilium_service_relation_server_protocol_http_call_duration/cilium_service_relation_server_protocol_http_call_cpm/1000000", "unit": "ms", "aggregation": "avg" }
+ ]
+ }
+}
diff --git a/apps/bff/src/bundled_templates/layers/clickhouse.json b/apps/bff/src/bundled_templates/layers/clickhouse.json
new file mode 100644
index 0000000..3d1222a
--- /dev/null
+++ b/apps/bff/src/bundled_templates/layers/clickhouse.json
@@ -0,0 +1,60 @@
+{
+ "key": "CLICKHOUSE",
+ "alias": "ClickHouse",
+ "color": "var(--sw-cyan)",
+ "documentLink": "https://skywalking.apache.org/docs/main/next/en/setup/backend/backend-clickhouse-monitoring/",
+ "aliases": { "services": "ClickHouse clusters", "instances": "Nodes" },
+ "components": { "service": true, "instances": true, "endpoints": false, "topology": false, "traces": false, "logs": false },
+ "layer-header": {
+ "orderBy": "selRate",
+ "columns": [
+ { "metric": "selRate", "label": "Select / s", "mqe": "aggregate_labels(meter_clickhouse_query_select_rate,sum)", "aggregation": "sum" },
+ { "metric": "insRate", "label": "Insert / s", "mqe": "aggregate_labels(meter_clickhouse_query_insert_rate,sum)", "aggregation": "sum" },
+ { "metric": "slowQ", "label": "Slow Reads", "mqe": "aggregate_labels(meter_clickhouse_query_slow,sum)", "aggregation": "sum" },
+ { "metric": "fileOpen", "label": "Open Files", "mqe": "latest(aggregate_labels(meter_clickhouse_file_open,sum))", "aggregation": "sum" }
+ ]
+ },
+ "overview": {
+ "groups": [
+ { "title": "Throughput", "size": "auto", "metrics": [
+ { "id": "selRate", "label": "Select QPS", "tip": "Selects per second across the cluster.", "mqe": "aggregate_labels(meter_clickhouse_query_select_rate,sum)", "aggregation": "sum" },
+ { "id": "insRate", "label": "Insert QPS", "tip": "Inserts per second.", "mqe": "aggregate_labels(meter_clickhouse_query_insert_rate,sum)", "aggregation": "sum" },
+ { "id": "slowQ", "label": "Slow Reads", "tip": "Slow file-reads counter.", "mqe": "aggregate_labels(meter_clickhouse_query_slow,sum)", "aggregation": "sum" }
+ ]}
+ ]
+ },
+ "dashboards": {
+ "service": [
+ { "id": "files_open", "title": "Files Open", "type": "line", "expressions": ["latest(aggregate_labels(meter_clickhouse_file_open,sum))"], "span": 3, "rowSpan": 2 },
+ { "id": "qps", "title": "QPS", "type": "line", "expressions": ["aggregate_labels(meter_clickhouse_query_select_rate,sum)", "aggregate_labels(meter_clickhouse_query_insert_rate,sum)"], "expressionLabels": ["select", "insert"], "span": 3, "rowSpan": 2 },
+ { "id": "queries", "title": "Queries", "type": "line", "expressions": ["aggregate_labels(meter_clickhouse_query,sum)", "aggregate_labels(meter_clickhouse_query_select,sum)", "aggregate_labels(meter_clickhouse_query_insert,sum)"], "expressionLabels": ["total", "select", "insert"], "span": 3, "rowSpan": 2 },
+ { "id": "query_time", "title": "Query Time (ms)", "type": "line", "unit": "ms", "expressions": ["aggregate_labels(meter_clickhouse_querytime_microseconds,sum)/aggregate_labels(meter_clickhouse_query,sum)/1000", "aggregate_labels(meter_clickhouse_querytime_select_microseconds,sum)/aggregate_labels(meter_clickhouse_query_select,sum)/1000", "aggregate_labels(meter_clickhouse_querytime_insert_microseconds,sum)/aggregate_labels(meter_clickhouse_query_insert,sum)/1000"], "expressionLabel [...]
+ { "id": "conns", "title": "Connections", "type": "line", "expressions": ["aggregate_labels(meter_clickhouse_tcp_connections,sum)", "aggregate_labels(meter_clickhouse_http_connections,sum)"], "expressionLabels": ["TCP", "HTTP"], "span": 4, "rowSpan": 2 },
+ { "id": "slow", "title": "Slow Reads", "type": "line", "expressions": ["aggregate_labels(meter_clickhouse_query_slow,sum)"], "span": 4, "rowSpan": 2 },
+ { "id": "merge_mut", "title": "Merge / Mutations", "type": "line", "expressions": ["aggregate_labels(meter_clickhouse_background_merge,sum)", "aggregate_labels(meter_clickhouse_mutations,sum)"], "expressionLabels": ["merge", "mutations"], "span": 4, "rowSpan": 2 },
+ { "id": "insert_io", "title": "Insert Throughput", "type": "line", "expressions": ["aggregate_labels(meter_clickhouse_inserted_bytes,sum)", "aggregate_labels(meter_clickhouse_inserted_rows,sum)"], "expressionLabels": ["bytes/s", "rows/s"], "expressionAxes": [0, 1], "span": 4, "rowSpan": 2 },
+ { "id": "delayed", "title": "Delayed Inserts (s)", "type": "line", "expressions": ["aggregate_labels(meter_clickhouse_delayed_inserts,sum)"], "span": 4, "rowSpan": 2 },
+ { "id": "parts_active", "title": "Active Data Parts", "type": "line", "expressions": ["aggregate_labels(meter_clickhouse_parts_active,sum)"], "span": 4, "rowSpan": 2 },
+ { "id": "replicated", "title": "Replicated Fetch / Send", "type": "line", "expressions": ["aggregate_labels(meter_clickhouse_replicated_fetch,sum)", "aggregate_labels(meter_clickhouse_replicated_send,sum)"], "expressionLabels": ["fetch", "send"], "span": 6, "rowSpan": 2 },
+ { "id": "zk", "title": "Zookeeper Activity", "type": "line", "expressions": ["latest(aggregate_labels(meter_clickhouse_zookeeper_session,sum))", "latest(aggregate_labels(meter_clickhouse_zookeeper_watch,sum))", "aggregate_labels(meter_clickhouse_zookeeper_bytes_sent,sum)", "aggregate_labels(meter_clickhouse_zookeeper_bytes_received,sum)"], "expressionLabels": ["sessions", "watches", "bytes sent", "bytes recv"], "span": 6, "rowSpan": 2 },
+ { "id": "keeper", "title": "Keeper", "type": "line", "expressions": ["latest(aggregate_labels(meter_clickhouse_keeper_connections_alive,sum))", "latest(aggregate_labels(meter_clickhouse_keeper_outstanding_requests,sum))"], "expressionLabels": ["alive conns", "outstanding"], "span": 6, "rowSpan": 2 }
+ ],
+ "instance": [
+ { "id": "uptime", "title": "Uptime (days)", "type": "line", "expressions": ["latest(meter_clickhouse_instance_uptime)/3600/24"], "span": 3, "rowSpan": 2 },
+ { "id": "version", "title": "Version", "type": "line", "expressions": ["latest(meter_clickhouse_instance_version)"], "span": 3, "rowSpan": 2 },
+ { "id": "cpu", "title": "CPU (cores)", "type": "line", "expressions": ["meter_clickhouse_instance_cpu_usage/1000000"], "span": 3, "rowSpan": 2 },
+ { "id": "mem", "title": "Memory (%)", "type": "line", "unit": "%", "expressions": ["meter_clickhouse_instance_memory_usage", "meter_clickhouse_instance_memory_available"], "expressionLabels": ["used", "available"], "span": 3, "rowSpan": 2 },
+ { "id": "network", "title": "Network (B)", "type": "line", "expressions": ["meter_clickhouse_instance_network_receive_bytes", "meter_clickhouse_instance_network_send_bytes"], "expressionLabels": ["receive", "send"], "span": 4, "rowSpan": 2 },
+ { "id": "conns", "title": "Connections", "type": "line", "expressions": ["meter_clickhouse_instance_tcp_connections", "meter_clickhouse_instance_http_connections"], "expressionLabels": ["TCP", "HTTP"], "span": 4, "rowSpan": 2 },
+ { "id": "queries", "title": "Queries", "type": "line", "expressions": ["meter_clickhouse_instance_query", "meter_clickhouse_instance_query_select", "meter_clickhouse_instance_query_insert"], "expressionLabels": ["total", "select", "insert"], "span": 4, "rowSpan": 2 },
+ { "id": "qps", "title": "QPS", "type": "line", "expressions": ["meter_clickhouse_instance_query_select_rate", "meter_clickhouse_instance_query_insert_rate"], "expressionLabels": ["select", "insert"], "span": 6, "rowSpan": 2 },
+ { "id": "query_time", "title": "Query Time (ms)", "type": "line", "unit": "ms", "expressions": ["meter_clickhouse_instance_querytime_microseconds/meter_clickhouse_instance_query/1000", "meter_clickhouse_instance_querytime_select_microseconds/meter_clickhouse_instance_query_select/1000", "meter_clickhouse_instance_querytime_insert_microseconds/meter_clickhouse_instance_query_insert/1000"], "expressionLabels": ["avg", "select", "insert"], "span": 6, "rowSpan": 2 },
+ { "id": "slow", "title": "File Slow Read", "type": "line", "expressions": ["meter_clickhouse_instance_query_slow"], "span": 4, "rowSpan": 2 },
+ { "id": "merge", "title": "Background Merge", "type": "line", "expressions": ["meter_clickhouse_instance_background_merge"], "span": 4, "rowSpan": 2 },
+ { "id": "mutations", "title": "Mutations", "type": "line", "expressions": ["meter_clickhouse_instance_mutations"], "span": 4, "rowSpan": 2 },
+ { "id": "files", "title": "Files Open", "type": "line", "expressions": ["latest(meter_clickhouse_instance_file_open)"], "span": 4, "rowSpan": 2 },
+ { "id": "insert_io", "title": "Insert Throughput", "type": "line", "expressions": ["meter_clickhouse_instance_inserted_bytes", "meter_clickhouse_instance_inserted_rows"], "expressionLabels": ["bytes/s", "rows/s"], "expressionAxes": [0, 1], "span": 4, "rowSpan": 2 },
+ { "id": "delayed", "title": "Delayed Inserts (s)", "type": "line", "expressions": ["meter_clickhouse_instance_delayed_inserts"], "span": 4, "rowSpan": 2 }
+ ]
+ }
+}
diff --git a/apps/bff/src/bundled_templates/layers/elasticsearch.json b/apps/bff/src/bundled_templates/layers/elasticsearch.json
new file mode 100644
index 0000000..86dd0f8
--- /dev/null
+++ b/apps/bff/src/bundled_templates/layers/elasticsearch.json
@@ -0,0 +1,59 @@
+{
+ "key": "ELASTICSEARCH",
+ "alias": "Elasticsearch",
+ "color": "var(--sw-cyan)",
+ "documentLink": "https://skywalking.apache.org/docs/main/next/en/setup/backend/backend-elasticsearch-monitoring/",
+ "aliases": { "services": "ES clusters", "instances": "Nodes", "endpoints": "Indices" },
+ "components": { "service": true, "instances": true, "endpoints": true, "topology": false, "traces": false, "logs": false },
+ "layer-header": {
+ "orderBy": "shards",
+ "columns": [
+ { "metric": "health", "label": "Health", "mqe": "meter_elasticsearch_cluster_health_status", "aggregation": "avg" },
+ { "metric": "shards", "label": "Shards", "mqe": "latest(meter_elasticsearch_cluster_shards_total)", "aggregation": "sum" },
+ { "metric": "nodes", "label": "Nodes", "mqe": "latest(meter_elasticsearch_cluster_nodes)", "aggregation": "sum" },
+ { "metric": "unassigned", "label": "Unassigned", "mqe": "latest(meter_elasticsearch_cluster_unassigned_shards_total)", "aggregation": "sum" }
+ ]
+ },
+ "overview": {
+ "groups": [
+ { "title": "Cluster", "size": "auto", "metrics": [
+ { "id": "health", "label": "Health", "tip": "0 green / 1 yellow / 2 red.", "mqe": "meter_elasticsearch_cluster_health_status", "aggregation": "avg" },
+ { "id": "unassigned", "label": "Unassigned Shards", "tip": "Shards not yet placed on any node.", "mqe": "latest(meter_elasticsearch_cluster_unassigned_shards_total)", "aggregation": "sum" },
+ { "id": "breakers", "label": "Tripped Breakers", "tip": "Cumulative circuit-breaker trips — high values indicate memory pressure.", "mqe": "latest(meter_elasticsearch_cluster_breakers_tripped)", "aggregation": "sum" }
+ ]}
+ ]
+ },
+ "dashboards": {
+ "service": [
+ { "id": "health", "title": "Cluster Health", "type": "line", "expressions": ["meter_elasticsearch_cluster_health_status"], "span": 3, "rowSpan": 2 },
+ { "id": "nodes", "title": "Nodes", "type": "line", "expressions": ["latest(meter_elasticsearch_cluster_nodes)"], "span": 3, "rowSpan": 2 },
+ { "id": "pending_tasks", "title": "Pending Tasks", "type": "line", "expressions": ["avg(meter_elasticsearch_cluster_pending_tasks_total)"], "span": 3, "rowSpan": 2 },
+ { "id": "primary_shards", "title": "Primary Shards", "type": "line", "expressions": ["latest(meter_elasticsearch_cluster_primary_shards_total)"], "span": 3, "rowSpan": 2 },
+ { "id": "shards_breakdown", "title": "Shards Breakdown", "type": "line", "expressions": ["latest(meter_elasticsearch_cluster_shards_total)", "latest(meter_elasticsearch_cluster_initializing_shards_total)", "latest(meter_elasticsearch_cluster_relocating_shards_total)", "latest(meter_elasticsearch_cluster_unassigned_shards_total)", "latest(meter_elasticsearch_cluster_delayed_unassigned_shards_total)"], "expressionLabels": ["active", "initializing", "relocating", "unassigned", "delaye [...]
+ { "id": "breakers", "title": "Tripped Breakers", "type": "line", "expressions": ["latest(meter_elasticsearch_cluster_breakers_tripped)"], "span": 3, "rowSpan": 2 },
+ { "id": "cluster_cpu", "title": "Cluster CPU Avg", "type": "line", "unit": "%", "expressions": ["avg(meter_elasticsearch_cluster_cpu_usage_avg)"], "span": 3, "rowSpan": 2 },
+ { "id": "cluster_jvm", "title": "JVM Memory Used Avg", "type": "line", "expressions": ["avg(meter_elasticsearch_cluster_jvm_memory_used_avg)"], "span": 4, "rowSpan": 2 },
+ { "id": "open_files", "title": "Open Files Avg", "type": "line", "expressions": ["avg(meter_elasticsearch_cluster_open_file_count)"], "span": 4, "rowSpan": 2 }
+ ],
+ "instance": [
+ { "id": "cpu", "title": "Process CPU (%)", "type": "line", "unit": "%", "expressions": ["meter_elasticsearch_node_process_cpu_percent"], "span": 4, "rowSpan": 2 },
+ { "id": "os_cpu", "title": "OS CPU (%)", "type": "line", "unit": "%", "expressions": ["meter_elasticsearch_node_os_cpu_percent"], "span": 4, "rowSpan": 2 },
+ { "id": "load_avg", "title": "Load Average", "type": "line", "expressions": ["meter_elasticsearch_node_os_load1", "meter_elasticsearch_node_os_load5", "meter_elasticsearch_node_os_load15"], "expressionLabels": ["1m", "5m", "15m"], "span": 4, "rowSpan": 2 },
+ { "id": "jvm_mem", "title": "JVM Memory (MB)", "type": "line", "expressions": ["meter_elasticsearch_node_jvm_memory_heap_used/1024/1024", "meter_elasticsearch_node_jvm_memory_heap_max/1024/1024", "meter_elasticsearch_node_jvm_memory_nonheap_used/1024/1024"], "expressionLabels": ["heap used", "heap max", "non-heap"], "span": 6, "rowSpan": 2 },
+ { "id": "gc", "title": "GC", "type": "line", "expressions": ["meter_elasticsearch_node_jvm_gc_count", "meter_elasticsearch_node_jvm_gc_time"], "expressionLabels": ["count", "time (ms/min)"], "expressionAxes": [0, 1], "span": 6, "rowSpan": 2 },
+ { "id": "translog", "title": "Translog", "type": "line", "expressions": ["meter_elasticsearch_node_indices_translog_operations", "meter_elasticsearch_node_indices_translog_size/1024/1024"], "expressionLabels": ["operations", "size (MB)"], "expressionAxes": [0, 1], "span": 6, "rowSpan": 2 },
+ { "id": "breakers", "title": "Breakers", "type": "line", "expressions": ["meter_elasticsearch_node_breakers_tripped", "meter_elasticsearch_node_breakers_estimated_size/1024/1024"], "expressionLabels": ["tripped", "estimated (MB)"], "span": 6, "rowSpan": 2 },
+ { "id": "segments", "title": "Segments", "type": "line", "expressions": ["meter_elasticsearch_node_segment_count", "meter_elasticsearch_node_segment_memory"], "expressionLabels": ["count", "memory (MB)"], "expressionAxes": [0, 1], "span": 6, "rowSpan": 2 },
+ { "id": "disk", "title": "Disk Usage", "type": "line", "expressions": ["meter_elasticsearch_node_disk_usage/1024/1024/1024", "meter_elasticsearch_node_disk_usage_percent"], "expressionLabels": ["GB", "%"], "expressionAxes": [0, 1], "span": 6, "rowSpan": 2 },
+ { "id": "network", "title": "Network", "type": "line", "expressions": ["meter_elasticsearch_node_network_send_bytes", "meter_elasticsearch_node_network_receive_bytes"], "expressionLabels": ["send", "receive"], "span": 6, "rowSpan": 2 },
+ { "id": "open_files", "title": "Open Files", "type": "line", "expressions": ["avg(meter_elasticsearch_node_open_file_count)"], "span": 6, "rowSpan": 2 }
+ ],
+ "endpoint": [
+ { "id": "indexing_rate", "title": "Indexing Rate", "type": "line", "expressions": ["meter_elasticsearch_index_stats_indexing_index_total_req_rate", "meter_elasticsearch_index_stats_indexing_index_total_proc_rate"], "expressionLabels": ["req", "proc"], "span": 6, "rowSpan": 2 },
+ { "id": "search_rate", "title": "Search Rate", "type": "line", "expressions": ["meter_elasticsearch_index_stats_search_query_total_req_rate", "meter_elasticsearch_index_stats_search_query_total_proc_rate"], "expressionLabels": ["req", "proc"], "span": 6, "rowSpan": 2 },
+ { "id": "data_size", "title": "Index Size", "type": "line", "unit": "GB", "expressions": ["latest(meter_elasticsearch_index_indices_store_size_bytes_total)/1024/1024/1024", "latest(meter_elasticsearch_index_indices_store_size_bytes_primary)/1024/1024/1024"], "expressionLabels": ["all shards", "primary"], "span": 6, "rowSpan": 2 },
+ { "id": "docs", "title": "Documents", "type": "line", "expressions": ["meter_elasticsearch_index_indices_docs_total", "meter_elasticsearch_index_indices_docs_primary", "latest(meter_elasticsearch_index_indices_deleted_docs_primary)"], "expressionLabels": ["all", "primary", "deleted"], "span": 6, "rowSpan": 2 },
+ { "id": "search_time", "title": "Avg Search Time / Req (s)", "type": "line", "expressions": ["meter_elasticsearch_index_search_fetch_avg_time", "meter_elasticsearch_index_search_query_avg_time", "meter_elasticsearch_index_search_scroll_avg_time", "meter_elasticsearch_index_search_suggest_avg_time"], "expressionLabels": ["fetch", "query", "scroll", "suggest"], "span": 12, "rowSpan": 2 }
+ ]
+ }
+}
diff --git a/apps/bff/src/bundled_templates/layers/envoy_ai_gateway.json b/apps/bff/src/bundled_templates/layers/envoy_ai_gateway.json
new file mode 100644
index 0000000..0898304
--- /dev/null
+++ b/apps/bff/src/bundled_templates/layers/envoy_ai_gateway.json
@@ -0,0 +1,61 @@
+{
+ "key": "ENVOY_AI_GATEWAY",
+ "alias": "Envoy AI Gateway",
+ "color": "var(--sw-pink)",
+ "documentLink": "https://skywalking.apache.org/docs/main/next/en/setup/backend/backend-envoy-ai-gateway-monitoring/",
+ "aliases": { "services": "AI Gateways", "instances": "Nodes" },
+ "components": { "service": true, "instances": true, "endpoints": false, "topology": false, "traces": false, "logs": false },
+ "layer-header": {
+ "orderBy": "cpm",
+ "columns": [
+ { "metric": "cpm", "label": "RPM", "unit": "rpm", "mqe": "meter_envoy_ai_gw_request_cpm", "aggregation": "sum" },
+ { "metric": "latency", "label": "Avg Latency", "unit": "ms", "mqe": "meter_envoy_ai_gw_request_latency_avg", "aggregation": "avg" },
+ { "metric": "inTokens", "label": "Input Tokens/min", "mqe": "meter_envoy_ai_gw_input_token_rate", "aggregation": "sum" },
+ { "metric": "outTokens", "label": "Output Tokens/min", "mqe": "meter_envoy_ai_gw_output_token_rate", "aggregation": "sum" }
+ ]
+ },
+ "overview": {
+ "groups": [
+ { "title": "Latency", "size": "auto", "metrics": [
+ { "id": "latency", "label": "Avg Latency", "tip": "Average end-to-end request latency.", "mqe": "meter_envoy_ai_gw_request_latency_avg", "unit": "ms", "aggregation": "avg" },
+ { "id": "ttft", "label": "TTFT Avg", "tip": "Time to first token — generative-model latency indicator.", "mqe": "meter_envoy_ai_gw_ttft_avg", "unit": "ms", "aggregation": "avg" }
+ ]},
+ { "title": "Tokens", "size": "square", "metrics": [
+ { "id": "tokens", "label": "Tokens/min", "tip": "Combined input + output token rate.", "mqe": "meter_envoy_ai_gw_input_token_rate + meter_envoy_ai_gw_output_token_rate", "aggregation": "sum" }
+ ]}
+ ]
+ },
+ "dashboards": {
+ "service": [
+ { "id": "cpm", "title": "Request RPM", "type": "line", "unit": "rpm", "expressions": ["meter_envoy_ai_gw_request_cpm"], "span": 3, "rowSpan": 2 },
+ { "id": "latency_avg", "title": "Request Latency Avg", "type": "line", "unit": "ms", "expressions": ["meter_envoy_ai_gw_request_latency_avg"], "span": 3, "rowSpan": 2 },
+ { "id": "input_tokens", "title": "Input Token Rate", "type": "line", "expressions": ["meter_envoy_ai_gw_input_token_rate"], "span": 3, "rowSpan": 2 },
+ { "id": "output_tokens", "title": "Output Token Rate", "type": "line", "expressions": ["meter_envoy_ai_gw_output_token_rate"], "span": 3, "rowSpan": 2 },
+ { "id": "latency_percentile", "title": "Request Latency Percentile", "type": "line", "unit": "ms", "expressions": ["meter_envoy_ai_gw_request_latency_percentile"], "span": 12, "rowSpan": 2 },
+ { "id": "ttft", "title": "TTFT (Time to First Token)", "type": "line", "unit": "ms", "expressions": ["meter_envoy_ai_gw_ttft_avg", "meter_envoy_ai_gw_ttft_percentile"], "expressionLabels": ["avg", "percentile"], "span": 6, "rowSpan": 2 },
+ { "id": "tpot", "title": "TPOT (Time Per Output Token)", "type": "line", "unit": "ms", "expressions": ["meter_envoy_ai_gw_tpot_avg", "meter_envoy_ai_gw_tpot_percentile"], "expressionLabels": ["avg", "percentile"], "span": 6, "rowSpan": 2 },
+ { "id": "provider_cpm", "title": "RPM by Provider", "type": "line", "expressions": ["aggregate_labels(meter_envoy_ai_gw_provider_request_cpm,sum(gen_ai_provider_name))"], "span": 4, "rowSpan": 2 },
+ { "id": "provider_tokens", "title": "Tokens by Provider", "type": "line", "expressions": ["aggregate_labels(meter_envoy_ai_gw_provider_token_rate,sum(gen_ai_provider_name))"], "span": 4, "rowSpan": 2 },
+ { "id": "provider_latency", "title": "Latency Avg by Provider", "type": "line", "unit": "ms", "expressions": ["aggregate_labels(meter_envoy_ai_gw_provider_latency_avg,avg(gen_ai_provider_name))"], "span": 4, "rowSpan": 2 },
+ { "id": "model_cpm", "title": "RPM by Model", "type": "line", "expressions": ["aggregate_labels(meter_envoy_ai_gw_model_request_cpm,sum(gen_ai_response_model))"], "span": 4, "rowSpan": 2 },
+ { "id": "model_tokens", "title": "Tokens by Model", "type": "line", "expressions": ["aggregate_labels(meter_envoy_ai_gw_model_token_rate,sum(gen_ai_response_model))"], "span": 4, "rowSpan": 2 },
+ { "id": "model_latency", "title": "Latency Avg by Model", "type": "line", "unit": "ms", "expressions": ["aggregate_labels(meter_envoy_ai_gw_model_latency_avg,avg(gen_ai_response_model))"], "span": 4, "rowSpan": 2 },
+ { "id": "model_ttft", "title": "TTFT by Model", "type": "line", "unit": "ms", "expressions": ["aggregate_labels(meter_envoy_ai_gw_model_ttft_avg,avg(gen_ai_response_model))"], "span": 6, "rowSpan": 2 },
+ { "id": "model_tpot", "title": "TPOT by Model", "type": "line", "unit": "ms", "expressions": ["aggregate_labels(meter_envoy_ai_gw_model_tpot_avg,avg(gen_ai_response_model))"], "span": 6, "rowSpan": 2 },
+ { "id": "mcp_cpm", "title": "MCP RPM", "tip": "Model Context Protocol — when used.", "type": "line", "expressions": ["meter_envoy_ai_gw_mcp_request_cpm"], "visibleWhen": "meter_envoy_ai_gw_mcp_request_cpm has value", "span": 4, "rowSpan": 2 },
+ { "id": "mcp_latency", "title": "MCP Avg Latency", "type": "line", "unit": "ms", "expressions": ["meter_envoy_ai_gw_mcp_request_latency_avg"], "visibleWhen": "meter_envoy_ai_gw_mcp_request_latency_avg has value", "span": 4, "rowSpan": 2 },
+ { "id": "mcp_err", "title": "MCP Error RPM", "type": "line", "expressions": ["meter_envoy_ai_gw_mcp_error_cpm"], "visibleWhen": "meter_envoy_ai_gw_mcp_error_cpm has value", "span": 4, "rowSpan": 2 },
+ { "id": "mcp_method", "title": "MCP by Method", "type": "line", "expressions": ["aggregate_labels(meter_envoy_ai_gw_mcp_method_cpm,sum(mcp_method_name))"], "visibleWhen": "meter_envoy_ai_gw_mcp_method_cpm has value", "span": 6, "rowSpan": 2 },
+ { "id": "mcp_backend", "title": "MCP by Backend", "type": "line", "expressions": ["aggregate_labels(meter_envoy_ai_gw_mcp_backend_request_cpm,sum(mcp_backend))"], "visibleWhen": "meter_envoy_ai_gw_mcp_backend_request_cpm has value", "span": 6, "rowSpan": 2 }
+ ],
+ "instance": [
+ { "id": "cpm", "title": "Request RPM", "type": "line", "unit": "rpm", "expressions": ["meter_envoy_ai_gw_instance_request_cpm"], "span": 3, "rowSpan": 2 },
+ { "id": "latency_avg", "title": "Request Latency Avg", "type": "line", "unit": "ms", "expressions": ["meter_envoy_ai_gw_instance_request_latency_avg"], "span": 3, "rowSpan": 2 },
+ { "id": "input_tokens", "title": "Input Token Rate", "type": "line", "expressions": ["meter_envoy_ai_gw_instance_input_token_rate"], "span": 3, "rowSpan": 2 },
+ { "id": "output_tokens", "title": "Output Token Rate", "type": "line", "expressions": ["meter_envoy_ai_gw_instance_output_token_rate"], "span": 3, "rowSpan": 2 },
+ { "id": "latency_percentile", "title": "Request Latency Percentile", "type": "line", "unit": "ms", "expressions": ["meter_envoy_ai_gw_instance_request_latency_percentile"], "span": 12, "rowSpan": 2 },
+ { "id": "ttft", "title": "TTFT", "type": "line", "unit": "ms", "expressions": ["meter_envoy_ai_gw_instance_ttft_avg", "meter_envoy_ai_gw_instance_ttft_percentile"], "expressionLabels": ["avg", "percentile"], "span": 6, "rowSpan": 2 },
+ { "id": "tpot", "title": "TPOT", "type": "line", "unit": "ms", "expressions": ["meter_envoy_ai_gw_instance_tpot_avg", "meter_envoy_ai_gw_instance_tpot_percentile"], "expressionLabels": ["avg", "percentile"], "span": 6, "rowSpan": 2 }
+ ]
+ }
+}
diff --git a/apps/bff/src/bundled_templates/layers/flink.json b/apps/bff/src/bundled_templates/layers/flink.json
new file mode 100644
index 0000000..397585d
--- /dev/null
+++ b/apps/bff/src/bundled_templates/layers/flink.json
@@ -0,0 +1,67 @@
+{
+ "key": "FLINK",
+ "alias": "Flink",
+ "color": "var(--sw-pink)",
+ "documentLink": "https://skywalking.apache.org/docs/main/next/en/setup/backend/backend-flink-monitoring/",
+ "aliases": { "services": "Flink JobManagers", "instances": "TaskManagers", "endpoints": "Jobs" },
+ "components": { "service": true, "instances": true, "endpoints": true, "topology": false, "traces": false, "logs": false },
+ "layer-header": {
+ "orderBy": "jobs",
+ "columns": [
+ { "metric": "jobs", "label": "Running Jobs", "mqe": "latest(meter_flink_jobManager_running_job_number)", "aggregation": "sum" },
+ { "metric": "taskMgrs", "label": "TaskManagers", "mqe": "latest(meter_flink_jobManager_taskManagers_registered_number)", "aggregation": "sum" },
+ { "metric": "slotsAvail", "label": "Slots Available", "mqe": "latest(meter_flink_jobManager_taskManagers_slots_available)", "aggregation": "sum" },
+ { "metric": "slotsTotal", "label": "Slots Total", "mqe": "latest(meter_flink_jobManager_taskManagers_slots_total)", "aggregation": "sum" }
+ ]
+ },
+ "overview": {
+ "groups": [
+ { "title": "Cluster", "size": "auto", "metrics": [
+ { "id": "jobs", "label": "Running Jobs", "tip": "Active Flink jobs.", "mqe": "latest(meter_flink_jobManager_running_job_number)", "aggregation": "sum" },
+ { "id": "slotsAvail", "label": "Slots Available", "tip": "Free task slots.", "mqe": "latest(meter_flink_jobManager_taskManagers_slots_available)", "aggregation": "sum" }
+ ]}
+ ]
+ },
+ "dashboards": {
+ "service": [
+ { "id": "jobs", "title": "Running Jobs", "type": "line", "expressions": ["latest(meter_flink_jobManager_running_job_number)"], "span": 3, "rowSpan": 2 },
+ { "id": "task_mgrs", "title": "TaskManagers", "type": "line", "expressions": ["latest(meter_flink_jobManager_taskManagers_registered_number)"], "span": 3, "rowSpan": 2 },
+ { "id": "slots", "title": "Slots Total / Available", "type": "line", "expressions": ["latest(meter_flink_jobManager_taskManagers_slots_total)", "latest(meter_flink_jobManager_taskManagers_slots_available)"], "expressionLabels": ["total", "available"], "span": 3, "rowSpan": 2 },
+ { "id": "cpu_load", "title": "JM JVM CPU Load (%)", "type": "line", "unit": "%", "expressions": ["meter_flink_jobManager_jvm_cpu_load/10"], "span": 3, "rowSpan": 2 },
+ { "id": "jm_threads", "title": "JM JVM Thread Count", "type": "line", "expressions": ["meter_flink_jobManager_jvm_thread_count"], "span": 3, "rowSpan": 2 },
+ { "id": "jm_cpu_time", "title": "JM CPU Time (ms)", "type": "line", "unit": "ms", "expressions": ["meter_flink_jobManager_jvm_cpu_time/1000/1000"], "span": 3, "rowSpan": 2 },
+ { "id": "jm_mem_heap", "title": "JM Heap (MB)", "type": "line", "unit": "MB", "expressions": ["meter_flink_jobManager_jvm_memory_heap_used/1024/1024", "(meter_flink_jobManager_jvm_memory_heap_available)/1024/1024"], "expressionLabels": ["used", "available"], "span": 3, "rowSpan": 2 },
+ { "id": "jm_mem_nonheap", "title": "JM NonHeap (MB)", "type": "line", "unit": "MB", "expressions": ["(meter_flink_jobManager_jvm_memory_nonHeap_used)/1024/1024", "(meter_flink_jobManager_jvm_memory_nonHeap_available)/1024/1024"], "expressionLabels": ["used", "available"], "span": 3, "rowSpan": 2 },
+ { "id": "jm_metaspace", "title": "JM Metaspace (MB)", "type": "line", "unit": "MB", "expressions": ["meter_flink_jobManager_jvm_memory_metaspace_used/1024/1024", "(meter_flink_jobManager_jvm_memory_metaspace_available)/1024/1024"], "expressionLabels": ["used", "available"], "span": 6, "rowSpan": 2 },
+ { "id": "g1_young", "title": "G1 Young GC", "type": "line", "expressions": ["meter_flink_jobManager_jvm_g1_young_generation_count", "meter_flink_jobManager_jvm_g1_young_generation_time"], "expressionLabels": ["count", "time (ms)"], "expressionAxes": [0, 1], "span": 6, "rowSpan": 2 },
+ { "id": "g1_old", "title": "G1 Old GC", "type": "line", "expressions": ["meter_flink_jobManager_jvm_g1_old_generation_count", "meter_flink_jobManager_jvm_g1_old_generation_time"], "expressionLabels": ["count", "time (ms)"], "expressionAxes": [0, 1], "span": 6, "rowSpan": 2 },
+ { "id": "all_gc", "title": "All GC", "type": "line", "expressions": ["meter_flink_jobManager_jvm_all_garbageCollector_count", "meter_flink_jobManager_jvm_all_garbageCollector_time"], "expressionLabels": ["count", "time (ms)"], "expressionAxes": [0, 1], "span": 6, "rowSpan": 2 },
+ { "id": "top_running", "title": "Top 10 Running Jobs", "type": "top", "expressions": ["top_n(meter_flink_job_runningTime,10,des)"], "span": 12, "rowSpan": 3 }
+ ],
+ "instance": [
+ { "id": "cpu_load", "title": "JVM CPU Load (%)", "type": "line", "unit": "%", "expressions": ["meter_flink_taskManager_jvm_cpu_load/10"], "span": 3, "rowSpan": 2 },
+ { "id": "threads", "title": "JVM Thread Count", "type": "line", "expressions": ["meter_flink_taskManager_jvm_thread_count"], "span": 3, "rowSpan": 2 },
+ { "id": "cpu_time", "title": "CPU Time (ms)", "type": "line", "unit": "ms", "expressions": ["meter_flink_taskManager_jvm_cpu_time/1000/1000"], "span": 3, "rowSpan": 2 },
+ { "id": "back_pressure", "title": "Back Pressured", "type": "line", "expressions": ["meter_flink_taskManager_isBackPressured"], "span": 3, "rowSpan": 2 },
+ { "id": "mem_heap", "title": "Heap (MB)", "type": "line", "unit": "MB", "expressions": ["meter_flink_taskManager_jvm_memory_heap_used/1024/1024", "(meter_flink_taskManager_jvm_memory_heap_available)/1024/1024"], "expressionLabels": ["used", "available"], "span": 4, "rowSpan": 2 },
+ { "id": "mem_nonheap", "title": "NonHeap (MB)", "type": "line", "unit": "MB", "expressions": ["(meter_flink_taskManager_jvm_memory_nonHeap_used)/1024/1024", "(meter_flink_taskManager_jvm_memory_nonHeap_available)/1024/1024"], "expressionLabels": ["used", "available"], "span": 4, "rowSpan": 2 },
+ { "id": "mem_meta", "title": "Metaspace (MB)", "type": "line", "unit": "MB", "expressions": ["meter_flink_taskManager_jvm_memory_metaspace_used/1024/1024", "(meter_flink_taskManager_jvm_memory_metaspace_available)/1024/1024"], "expressionLabels": ["used", "available"], "span": 4, "rowSpan": 2 },
+ { "id": "records", "title": "Records In / Out", "type": "line", "expressions": ["meter_flink_taskManager_numRecordsIn", "meter_flink_taskManager_numRecordsOut"], "expressionLabels": ["in", "out"], "span": 6, "rowSpan": 2 },
+ { "id": "bytes", "title": "Bytes In / Out / s", "type": "line", "expressions": ["meter_flink_taskManager_numBytesInPerSecond", "meter_flink_taskManager_numBytesOutPerSecond"], "expressionLabels": ["in", "out"], "span": 6, "rowSpan": 2 },
+ { "id": "netty", "title": "Netty Memory (MB)", "type": "line", "unit": "MB", "expressions": ["meter_flink_taskManager_netty_usedMemory/1024/1024", "meter_flink_taskManager_netty_availableMemory/1024/1024"], "expressionLabels": ["used", "available"], "span": 6, "rowSpan": 2 },
+ { "id": "pool_usage", "title": "Pool Usage (%)", "type": "line", "unit": "%", "expressions": ["meter_flink_taskManager_inPoolUsage*10", "meter_flink_taskManager_outPoolUsage"], "expressionLabels": ["in pool", "out pool"], "span": 6, "rowSpan": 2 },
+ { "id": "back_pressure_time", "title": "Back-Pressure Time (ms/s)", "type": "line", "unit": "ms/s", "expressions": ["meter_flink_taskManager_softBackPressuredTimeMsPerSecond", "meter_flink_taskManager_hardBackPressuredTimeMsPerSecond", "meter_flink_taskManager_idleTimeMsPerSecond", "meter_flink_taskManager_busyTimeMsPerSecond"], "expressionLabels": ["soft bp", "hard bp", "idle", "busy"], "span": 12, "rowSpan": 2 }
+ ],
+ "endpoint": [
+ { "id": "running_time", "title": "Job Running Time (min)", "type": "line", "expressions": ["latest(meter_flink_job_runningTime)/1000/60"], "span": 4, "rowSpan": 2 },
+ { "id": "restarting_time", "title": "Job Restarting Time (min)", "type": "line", "expressions": ["latest(meter_flink_job_restartingTime)/1000/60"], "span": 4, "rowSpan": 2 },
+ { "id": "cancelling_time", "title": "Job Cancelling Time (min)", "type": "line", "expressions": ["latest(meter_flink_job_cancellingTime)/1000/60"], "span": 4, "rowSpan": 2 },
+ { "id": "restarts", "title": "Job Restarts", "type": "line", "expressions": ["latest(meter_flink_job_restart_number)"], "span": 4, "rowSpan": 2 },
+ { "id": "checkpoints", "title": "Checkpoints", "type": "line", "expressions": ["meter_flink_job_checkpoints_total", "meter_flink_job_checkpoints_completed", "meter_flink_job_checkpoints_failed", "meter_flink_job_checkpoints_inProgress"], "expressionLabels": ["total", "completed", "failed", "in-progress"], "span": 8, "rowSpan": 2 },
+ { "id": "checkpoint_last", "title": "Last Checkpoint", "type": "line", "expressions": ["meter_flink_job_lastCheckpointSize", "meter_flink_job_lastCheckpointDuration"], "expressionLabels": ["size (bytes)", "duration (ms)"], "expressionAxes": [0, 1], "span": 6, "rowSpan": 2 },
+ { "id": "emit_lag", "title": "Current Emit Event Time Lag (ms)", "type": "line", "unit": "ms", "expressions": ["meter_flink_job_currentEmitEventTimeLag"], "span": 6, "rowSpan": 2 },
+ { "id": "records", "title": "Records In / Out", "type": "line", "expressions": ["meter_flink_job_numRecordsIn", "meter_flink_job_numRecordsOut"], "expressionLabels": ["in", "out"], "span": 6, "rowSpan": 2 },
+ { "id": "bytes", "title": "Bytes In / Out / s", "type": "line", "expressions": ["meter_flink_job_numBytesInPerSecond", "meter_flink_job_numBytesOutPerSecond"], "expressionLabels": ["in", "out"], "span": 6, "rowSpan": 2 }
+ ]
+ }
+}
diff --git a/apps/bff/src/layers/config/general.json b/apps/bff/src/bundled_templates/layers/general.json
similarity index 90%
rename from apps/bff/src/layers/config/general.json
rename to apps/bff/src/bundled_templates/layers/general.json
index cbb65bc..377fe4a 100644
--- a/apps/bff/src/layers/config/general.json
+++ b/apps/bff/src/bundled_templates/layers/general.json
@@ -32,21 +32,14 @@
"overview": {
"groups": [
{
- "title": "Throughput",
+ "title": "Latency & errors",
"size": "auto",
"metrics": [
{
- "id": "rpm",
- "label": "RPM",
- "tip": "Requests per minute across the layer.",
- "mqe": "service_cpm",
- "aggregation": "sum"
- },
- {
- "id": "p99",
- "label": "P99",
- "tip": "99th-percentile response time across the layer.",
- "mqe": "service_percentile{p='99'}",
+ "id": "p95",
+ "label": "P95",
+ "tip": "95th-percentile response time across the layer.",
+ "mqe": "service_percentile{p='95'}",
"unit": "ms",
"aggregation": "avg"
},
@@ -923,5 +916,65 @@
"traceProfiling": [],
"ebpfProfiling": [],
"asyncProfiling": []
+ },
+ "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,
+ "invertBase": 100,
+ "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" },
+ { "id": "respTime", "label": "Avg response time", "mqe": "service_relation_server_resp_time", "unit": "ms", "aggregation": "avg" },
+ { "id": "p95", "label": "p95", "mqe": "service_relation_server_percentile{p='95'}", "unit": "ms", "aggregation": "avg" },
+ { "id": "sla", "label": "SLA", "mqe": "service_relation_server_call_sla/100", "unit": "%", "aggregation": "avg" }
+ ],
+ "linkClientMetrics": [
+ { "id": "cpm", "label": "RPM", "mqe": "service_relation_client_cpm", "unit": "rpm", "role": "lineClient", "aggregation": "avg" },
+ { "id": "respTime", "label": "Avg response time", "mqe": "service_relation_client_resp_time", "unit": "ms", "aggregation": "avg" },
+ { "id": "p95", "label": "p95", "mqe": "service_relation_client_percentile{p='95'}", "unit": "ms", "aggregation": "avg" },
+ { "id": "sla", "label": "SLA", "mqe": "service_relation_client_call_sla/100", "unit": "%", "aggregation": "avg" }
+ ]
+ },
+ "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",
+ "thresholds": {
+ "invertHealth": true,
+ "invertBase": 100,
+ "ok": 0.1,
+ "warn": 1,
+ "danger": 5
+ }
+ },
+ { "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" },
+ { "id": "p95", "label": "p95", "mqe": "endpoint_relation_percentile{p='95'}", "unit": "ms", "aggregation": "avg" },
+ { "id": "sla", "label": "SLA", "mqe": "endpoint_relation_sla/100", "unit": "%", "aggregation": "avg" }
+ ]
}
}
diff --git a/apps/bff/src/bundled_templates/layers/ios.json b/apps/bff/src/bundled_templates/layers/ios.json
new file mode 100644
index 0000000..b6cec0e
--- /dev/null
+++ b/apps/bff/src/bundled_templates/layers/ios.json
@@ -0,0 +1,254 @@
+{
+ "key": "IOS",
+ "alias": "iOS",
+ "color": "var(--sw-cyan)",
+ "documentLink": "https://skywalking.apache.org/docs/main/next/en/setup/backend/backend-ios-monitoring/",
+ "aliases": {
+ "services": "Apps",
+ "instances": "App Sessions",
+ "endpoints": "Outbound APIs"
+ },
+ "components": {
+ "service": true,
+ "instances": true,
+ "endpoints": true,
+ "topology": false,
+ "traces": false,
+ "logs": true
+ },
+ "layer-header": {
+ "orderBy": "launch",
+ "columns": [
+ { "metric": "launch", "label": "Launch (P95)", "unit": "ms", "mqe": "meter_ios_app_launch_time_percentile{p='95'}", "aggregation": "avg" },
+ { "metric": "hangSum", "label": "Hang Time", "unit": "ms", "mqe": "meter_ios_hang_time_sum", "aggregation": "sum" },
+ { "metric": "abnormalExit", "label": "Crashes", "mqe": "meter_ios_foreground_abnormal_exit_count + meter_ios_background_abnormal_exit_count", "aggregation": "sum" },
+ { "metric": "outboundCpm", "label": "Outbound RPM", "unit": "rpm", "mqe": "service_cpm", "aggregation": "sum" }
+ ]
+ },
+ "overview": {
+ "groups": [
+ {
+ "title": "App health",
+ "size": "auto",
+ "metrics": [
+ { "id": "launch_p95", "label": "Launch P95", "tip": "95th-percentile cold-launch time as measured by MetricKit.", "mqe": "meter_ios_app_launch_time_percentile{p='95'}", "unit": "ms", "aggregation": "avg" },
+ { "id": "hang_p95", "label": "Hang P95", "tip": "95th-percentile UI hang duration.", "mqe": "meter_ios_hang_time_percentile{p='95'}", "unit": "ms", "aggregation": "avg" },
+ { "id": "crashes", "label": "Crashes", "tip": "Foreground + background abnormal exits.", "mqe": "meter_ios_foreground_abnormal_exit_count + meter_ios_background_abnormal_exit_count", "aggregation": "sum" }
+ ]
+ }
+ ]
+ },
+ "dashboards": {
+ "service": [
+ {
+ "id": "launch_percentile",
+ "title": "App Launch Time Percentile",
+ "type": "line",
+ "unit": "ms",
+ "expressions": [
+ "relabels(meter_ios_app_launch_time_percentile{p='50,75,90,95,99'},p='50,75,90,95,99',percentile='50,75,90,95,99')"
+ ],
+ "span": 6,
+ "rowSpan": 2
+ },
+ {
+ "id": "hang_percentile",
+ "title": "Hang Time Percentile",
+ "type": "line",
+ "unit": "ms",
+ "expressions": [
+ "relabels(meter_ios_hang_time_percentile{p='50,75,90,95,99'},p='50,75,90,95,99',percentile='50,75,90,95,99')"
+ ],
+ "span": 6,
+ "rowSpan": 2
+ },
+ {
+ "id": "hang_sum",
+ "title": "Hang Time (sum)",
+ "type": "line",
+ "unit": "ms",
+ "expressions": ["meter_ios_hang_time_sum"],
+ "span": 4,
+ "rowSpan": 2
+ },
+ {
+ "id": "abnormal_exits",
+ "title": "Abnormal Exits (Crashes)",
+ "tip": "MetricKit reports foreground vs background abnormal exits separately.",
+ "type": "line",
+ "expressions": [
+ "meter_ios_foreground_abnormal_exit_count",
+ "meter_ios_background_abnormal_exit_count"
+ ],
+ "expressionLabels": ["foreground", "background"],
+ "span": 4,
+ "rowSpan": 2
+ },
+ {
+ "id": "oom_kills",
+ "title": "OOM Kill Count",
+ "tip": "Background OOM kills — the iOS system reaping the app due to memory pressure.",
+ "type": "line",
+ "expressions": ["meter_ios_background_oom_kill_count"],
+ "span": 4,
+ "rowSpan": 2
+ },
+ {
+ "id": "peak_memory",
+ "title": "Peak Memory",
+ "type": "line",
+ "unit": "bytes",
+ "expressions": ["meter_ios_peak_memory"],
+ "span": 6,
+ "rowSpan": 2
+ },
+ {
+ "id": "scroll_hitch",
+ "title": "Scroll Hitch Ratio",
+ "tip": "Percentage of scroll frames classified as hitched — higher = laggier UI.",
+ "type": "line",
+ "expressions": ["meter_ios_scroll_hitch_ratio"],
+ "span": 6,
+ "rowSpan": 2
+ },
+ {
+ "id": "network_transfer",
+ "title": "Network Transfer",
+ "tip": "Bytes over wifi vs cellular, in + out.",
+ "type": "line",
+ "unit": "bytes",
+ "expressions": [
+ "meter_ios_wifi_download",
+ "meter_ios_wifi_upload",
+ "meter_ios_cellular_download",
+ "meter_ios_cellular_upload"
+ ],
+ "expressionLabels": ["wifi ↓", "wifi ↑", "cellular ↓", "cellular ↑"],
+ "span": 6,
+ "rowSpan": 2
+ },
+ {
+ "id": "outbound_http",
+ "title": "Outbound HTTP",
+ "tip": "Calls the app makes to backends — load, latency, success rate.",
+ "type": "line",
+ "expressions": [
+ "service_cpm",
+ "service_resp_time",
+ "service_sla/100"
+ ],
+ "expressionLabels": ["RPM", "Avg latency (ms)", "Success Rate (%)"],
+ "expressionAxes": [0, 0, 1],
+ "span": 6,
+ "rowSpan": 2
+ }
+ ],
+ "instance": [
+ {
+ "id": "instance_launch_p",
+ "title": "Launch Time Percentile",
+ "type": "line",
+ "unit": "ms",
+ "expressions": [
+ "relabels(meter_ios_instance_app_launch_time_percentile{p='50,75,90,95,99'},p='50,75,90,95,99',percentile='50,75,90,95,99')"
+ ],
+ "span": 6,
+ "rowSpan": 2
+ },
+ {
+ "id": "instance_hang_p",
+ "title": "Hang Time Percentile",
+ "type": "line",
+ "unit": "ms",
+ "expressions": [
+ "relabels(meter_ios_instance_hang_time_percentile{p='50,75,90,95,99'},p='50,75,90,95,99',percentile='50,75,90,95,99')"
+ ],
+ "span": 6,
+ "rowSpan": 2
+ },
+ {
+ "id": "instance_crashes",
+ "title": "Abnormal Exits",
+ "type": "line",
+ "expressions": [
+ "meter_ios_instance_foreground_abnormal_exit_count",
+ "meter_ios_instance_background_abnormal_exit_count"
+ ],
+ "expressionLabels": ["foreground", "background"],
+ "span": 4,
+ "rowSpan": 2
+ },
+ {
+ "id": "instance_oom",
+ "title": "OOM Kill Count",
+ "type": "line",
+ "expressions": ["meter_ios_instance_background_oom_kill_count"],
+ "span": 4,
+ "rowSpan": 2
+ },
+ {
+ "id": "instance_peak_mem",
+ "title": "Peak Memory",
+ "type": "line",
+ "unit": "bytes",
+ "expressions": ["meter_ios_instance_peak_memory"],
+ "span": 4,
+ "rowSpan": 2
+ },
+ {
+ "id": "instance_outbound",
+ "title": "Outbound HTTP",
+ "type": "line",
+ "expressions": [
+ "service_instance_cpm",
+ "service_instance_resp_time",
+ "service_instance_sla/100"
+ ],
+ "expressionLabels": ["RPM", "Avg latency (ms)", "Success Rate (%)"],
+ "expressionAxes": [0, 0, 1],
+ "span": 12,
+ "rowSpan": 2
+ }
+ ],
+ "endpoint": [
+ {
+ "id": "ep_cpm",
+ "title": "Outbound Load",
+ "type": "line",
+ "unit": "rpm",
+ "expressions": ["endpoint_cpm"],
+ "span": 4,
+ "rowSpan": 2
+ },
+ {
+ "id": "ep_resp",
+ "title": "Outbound Avg Latency",
+ "type": "line",
+ "unit": "ms",
+ "expressions": ["endpoint_resp_time"],
+ "span": 4,
+ "rowSpan": 2
+ },
+ {
+ "id": "ep_sla",
+ "title": "Outbound Success Rate",
+ "type": "line",
+ "unit": "%",
+ "expressions": ["endpoint_sla/100"],
+ "span": 4,
+ "rowSpan": 2
+ },
+ {
+ "id": "ep_percentile",
+ "title": "Outbound Latency Percentile",
+ "type": "line",
+ "unit": "ms",
+ "expressions": [
+ "relabels(endpoint_percentile{p='50,75,90,95,99'},p='50,75,90,95,99',percentile='50,75,90,95,99')"
+ ],
+ "span": 12,
+ "rowSpan": 2
+ }
+ ]
+ }
+}
diff --git a/apps/bff/src/bundled_templates/layers/k8s.json b/apps/bff/src/bundled_templates/layers/k8s.json
new file mode 100644
index 0000000..bb32eda
--- /dev/null
+++ b/apps/bff/src/bundled_templates/layers/k8s.json
@@ -0,0 +1,266 @@
+{
+ "key": "K8S",
+ "alias": "Kubernetes",
+ "color": "var(--sw-info)",
+ "documentLink": "https://skywalking.apache.org/docs/main/next/en/setup/backend/backend-k8s-monitoring/",
+ "aliases": {
+ "services": "Clusters",
+ "instances": "Nodes"
+ },
+ "components": {
+ "service": true,
+ "instances": true,
+ "endpoints": false,
+ "endpointDependency": false,
+ "topology": false,
+ "traces": false,
+ "logs": false
+ },
+ "layer-header": {
+ "orderBy": "pods",
+ "columns": [
+ { "metric": "pods", "label": "Pods", "mqe": "k8s_cluster_pod_total", "aggregation": "sum" },
+ { "metric": "nodes", "label": "Nodes", "mqe": "k8s_cluster_node_total", "aggregation": "sum" },
+ { "metric": "namespaces", "label": "Namespaces", "mqe": "k8s_cluster_namespace_total", "aggregation": "sum" },
+ { "metric": "deployments", "label": "Deployments", "mqe": "k8s_cluster_deployment_total", "aggregation": "sum" }
+ ]
+ },
+ "overview": {
+ "groups": [
+ {
+ "title": "Capacity",
+ "size": "auto",
+ "metrics": [
+ { "id": "pods", "label": "Pods", "tip": "Total pods reported across the cluster.", "mqe": "k8s_cluster_pod_total", "aggregation": "sum" },
+ { "id": "nodes", "label": "Nodes", "tip": "Node count.", "mqe": "k8s_cluster_node_total", "aggregation": "sum" },
+ { "id": "services", "label": "Services", "tip": "K8s Service objects.", "mqe": "k8s_cluster_service_total", "aggregation": "sum" }
+ ]
+ },
+ {
+ "title": "Trouble",
+ "size": "square",
+ "metrics": [
+ {
+ "id": "podsNotRunning",
+ "label": "Pods not running",
+ "tip": "Pods in any non-Running state — pending, failed, unknown.",
+ "mqe": "latest(k8s_cluster_pod_status_not_running)",
+ "aggregation": "sum"
+ }
+ ]
+ }
+ ]
+ },
+ "dashboards": {
+ "service": [
+ {
+ "id": "cpu_resources",
+ "title": "Cluster CPU Resources",
+ "tip": "Allocatable / capacity / requests / limits across the cluster (millicores).",
+ "type": "line",
+ "unit": "m",
+ "expressions": [
+ "k8s_cluster_cpu_cores",
+ "k8s_cluster_cpu_cores_allocatable",
+ "k8s_cluster_cpu_cores_requests",
+ "k8s_cluster_cpu_cores_limits"
+ ],
+ "expressionLabels": ["total", "allocatable", "requests", "limits"],
+ "span": 4,
+ "rowSpan": 2
+ },
+ {
+ "id": "mem_resources",
+ "title": "Cluster Memory Resources",
+ "type": "line",
+ "unit": "Gi",
+ "expressions": [
+ "k8s_cluster_memory_total/1024/1024/1024",
+ "k8s_cluster_memory_allocatable/1024/1024/1024",
+ "k8s_cluster_memory_requests/1024/1024/1024",
+ "k8s_cluster_memory_limits/1024/1024/1024"
+ ],
+ "expressionLabels": ["total", "allocatable", "requests", "limits"],
+ "span": 4,
+ "rowSpan": 2
+ },
+ {
+ "id": "storage_resources",
+ "title": "Cluster Storage Resources",
+ "type": "line",
+ "unit": "Gi",
+ "expressions": [
+ "k8s_cluster_storage_total/1024/1024/1024",
+ "k8s_cluster_storage_allocatable/1024/1024/1024"
+ ],
+ "expressionLabels": ["total", "allocatable"],
+ "span": 4,
+ "rowSpan": 2
+ },
+ {
+ "id": "pod_totals",
+ "title": "Pod / Container Totals",
+ "type": "line",
+ "expressions": ["k8s_cluster_pod_total", "k8s_cluster_container_total"],
+ "expressionLabels": ["pods", "containers"],
+ "span": 3,
+ "rowSpan": 2
+ },
+ {
+ "id": "deployment_replicas",
+ "title": "Deployment Replicas",
+ "tip": "Spec vs ready/status replica counts. Divergence indicates rollout issues.",
+ "type": "line",
+ "expressions": [
+ "latest(k8s_cluster_deployment_spec_replicas)",
+ "latest(k8s_cluster_deployment_status)"
+ ],
+ "expressionLabels": ["spec", "status"],
+ "span": 3,
+ "rowSpan": 2
+ },
+ {
+ "id": "pod_waiting",
+ "title": "Pods Waiting",
+ "type": "line",
+ "expressions": ["latest(k8s_cluster_pod_status_waiting)"],
+ "span": 3,
+ "rowSpan": 2
+ },
+ {
+ "id": "pod_not_running",
+ "title": "Pods Not Running",
+ "type": "line",
+ "expressions": ["latest(k8s_cluster_pod_status_not_running)"],
+ "span": 3,
+ "rowSpan": 2
+ },
+ {
+ "id": "service_pod_status",
+ "title": "Service Pod Status",
+ "tip": "Aggregate health of pods backing K8s Service objects.",
+ "type": "line",
+ "expressions": ["latest(k8s_cluster_service_pod_status)"],
+ "span": 4,
+ "rowSpan": 2
+ },
+ {
+ "id": "node_status",
+ "title": "Node Status",
+ "tip": "Sum of node Ready states.",
+ "type": "line",
+ "expressions": ["latest(k8s_cluster_node_status)"],
+ "span": 4,
+ "rowSpan": 2
+ },
+ {
+ "id": "namespace_totals",
+ "title": "Workload Totals",
+ "tip": "Namespaces / deployments / statefulsets / daemonsets across the cluster.",
+ "type": "line",
+ "expressions": [
+ "latest(k8s_cluster_namespace_total)",
+ "latest(k8s_cluster_deployment_total)",
+ "latest(k8s_cluster_statefulset_total)",
+ "latest(k8s_cluster_daemonset_total)"
+ ],
+ "expressionLabels": ["namespaces", "deployments", "statefulsets", "daemonsets"],
+ "span": 4,
+ "rowSpan": 2
+ }
+ ],
+ "instance": [
+ {
+ "id": "node_status",
+ "title": "Node Status",
+ "type": "line",
+ "expressions": ["latest(k8s_node_node_status)"],
+ "span": 3,
+ "rowSpan": 2
+ },
+ {
+ "id": "node_pod_total",
+ "title": "Pods on Node",
+ "type": "line",
+ "expressions": ["k8s_node_pod_total"],
+ "span": 3,
+ "rowSpan": 2
+ },
+ {
+ "id": "node_cpu_usage",
+ "title": "Node CPU Usage",
+ "tip": "CPU usage in millicores.",
+ "type": "line",
+ "unit": "m",
+ "expressions": ["k8s_node_cpu_usage"],
+ "span": 6,
+ "rowSpan": 2
+ },
+ {
+ "id": "node_cpu_resources",
+ "title": "Node CPU Resources",
+ "type": "line",
+ "unit": "m",
+ "expressions": [
+ "k8s_node_cpu_cores",
+ "k8s_node_cpu_cores_allocatable",
+ "k8s_node_cpu_cores_requests",
+ "k8s_node_cpu_cores_limits"
+ ],
+ "expressionLabels": ["total", "allocatable", "requests", "limits"],
+ "span": 6,
+ "rowSpan": 2
+ },
+ {
+ "id": "node_memory_usage",
+ "title": "Node Memory Usage",
+ "type": "line",
+ "unit": "Gi",
+ "expressions": ["k8s_node_memory_usage/1024/1024/1024"],
+ "span": 6,
+ "rowSpan": 2
+ },
+ {
+ "id": "node_memory_resources",
+ "title": "Node Memory Resources",
+ "type": "line",
+ "unit": "Gi",
+ "expressions": [
+ "k8s_node_memory_total/1024/1024/1024",
+ "k8s_node_memory_allocatable/1024/1024/1024",
+ "k8s_node_memory_requests/1024/1024/1024",
+ "k8s_node_memory_limits/1024/1024/1024"
+ ],
+ "expressionLabels": ["total", "allocatable", "requests", "limits"],
+ "span": 6,
+ "rowSpan": 2
+ },
+ {
+ "id": "node_network_io",
+ "title": "Node Network I/O",
+ "type": "line",
+ "unit": "KB/s",
+ "expressions": [
+ "k8s_node_network_receive/1024",
+ "k8s_node_network_transmit/1024"
+ ],
+ "expressionLabels": ["receive", "transmit"],
+ "span": 6,
+ "rowSpan": 2
+ },
+ {
+ "id": "node_storage_resources",
+ "title": "Node Storage Resources",
+ "type": "line",
+ "unit": "Gi",
+ "expressions": [
+ "k8s_node_storage_total/1024/1024/1024",
+ "k8s_node_storage_allocatable/1024/1024/1024"
+ ],
+ "expressionLabels": ["total", "allocatable"],
+ "span": 6,
+ "rowSpan": 2
+ }
+ ]
+ }
+}
diff --git a/apps/bff/src/bundled_templates/layers/k8s_service.json b/apps/bff/src/bundled_templates/layers/k8s_service.json
new file mode 100644
index 0000000..fb2cceb
--- /dev/null
+++ b/apps/bff/src/bundled_templates/layers/k8s_service.json
@@ -0,0 +1,404 @@
+{
+ "key": "K8S_SERVICE",
+ "alias": "K8s Service",
+ "color": "var(--sw-purple)",
+ "documentLink": "https://skywalking.apache.org/docs/main/next/en/setup/backend/backend-k8s-network-monitoring/",
+ "aliases": {
+ "services": "K8s services",
+ "instances": "Pods",
+ "endpoints": "Endpoints"
+ },
+ "components": {
+ "service": true,
+ "instances": true,
+ "endpoints": true,
+ "endpointDependency": false,
+ "topology": true,
+ "traces": true,
+ "logs": true,
+ "ebpfProfiling": true
+ },
+ "layer-header": {
+ "orderBy": "httpCpm",
+ "columns": [
+ { "metric": "pods", "label": "Pods", "mqe": "latest(k8s_service_pod_total)", "aggregation": "sum" },
+ { "metric": "httpCpm", "label": "HTTP RPM", "unit": "rpm", "mqe": "kubernetes_service_http_call_cpm", "aggregation": "sum" },
+ { "metric": "httpRespTime", "label": "Latency", "unit": "ms", "mqe": "kubernetes_service_http_call_time", "aggregation": "avg" },
+ { "metric": "httpSuccess", "label": "Success Rate", "unit": "%", "mqe": "kubernetes_service_http_call_successful_rate/100", "aggregation": "avg" }
+ ]
+ },
+ "overview": {
+ "groups": [
+ {
+ "title": "HTTP",
+ "size": "auto",
+ "metrics": [
+ { "id": "httpCpm", "label": "RPM", "tip": "HTTP requests per minute observed by eBPF instrumentation.", "mqe": "kubernetes_service_http_call_cpm", "unit": "rpm", "aggregation": "sum" },
+ { "id": "httpRespTime", "label": "Latency", "tip": "Avg HTTP response time across the cluster.", "mqe": "kubernetes_service_http_call_time", "unit": "ms", "aggregation": "avg" },
+ { "id": "httpErr", "label": "Error Rate", "tip": "100 − HTTP success rate.", "mqe": "100 - kubernetes_service_http_call_successful_rate/100", "unit": "%", "aggregation": "avg" }
+ ]
+ },
+ {
+ "title": "Health",
+ "size": "square",
+ "metrics": [
+ {
+ "id": "apdex",
+ "label": "Apdex",
+ "tip": "User-satisfaction score on a 0 – 1 scale.",
+ "mqe": "kubernetes_service_apdex/10000",
+ "aggregation": "avg"
+ }
+ ]
+ }
+ ]
+ },
+ "dashboards": {
+ "service": [
+ {
+ "id": "pod_capacity",
+ "title": "Service Pods",
+ "tip": "Pod count + waiting / restart counters.",
+ "type": "line",
+ "expressions": [
+ "k8s_service_pod_total",
+ "latest(k8s_service_pod_status_waiting)",
+ "latest(k8s_service_pod_status_restarts_total)"
+ ],
+ "expressionLabels": ["total", "waiting", "restarts"],
+ "span": 4,
+ "rowSpan": 2
+ },
+ {
+ "id": "cpu_resources",
+ "title": "CPU Resources",
+ "type": "line",
+ "unit": "m",
+ "expressions": [
+ "k8s_service_cpu_cores_requests",
+ "k8s_service_cpu_cores_limits"
+ ],
+ "expressionLabels": ["requests", "limits"],
+ "span": 4,
+ "rowSpan": 2
+ },
+ {
+ "id": "memory_resources",
+ "title": "Memory Resources",
+ "type": "line",
+ "unit": "Mi",
+ "expressions": [
+ "k8s_service_memory_requests/1024/1024",
+ "k8s_service_memory_limits/1024/1024"
+ ],
+ "expressionLabels": ["requests", "limits"],
+ "span": 4,
+ "rowSpan": 2
+ },
+ {
+ "id": "pod_cpu_usage",
+ "title": "Pod CPU Usage",
+ "type": "line",
+ "unit": "m",
+ "expressions": ["k8s_service_pod_cpu_usage"],
+ "span": 6,
+ "rowSpan": 2
+ },
+ {
+ "id": "pod_memory_usage",
+ "title": "Pod Memory Usage",
+ "type": "line",
+ "unit": "Mi",
+ "expressions": ["k8s_service_pod_memory_usage/1024/1024"],
+ "span": 6,
+ "rowSpan": 2
+ },
+ {
+ "id": "http_call_cpm",
+ "title": "HTTP Request RPM",
+ "type": "line",
+ "unit": "rpm",
+ "expressions": ["kubernetes_service_http_call_cpm"],
+ "span": 3,
+ "rowSpan": 2
+ },
+ {
+ "id": "http_call_time",
+ "title": "HTTP Response Time",
+ "type": "line",
+ "unit": "ms",
+ "expressions": ["kubernetes_service_http_call_time"],
+ "span": 3,
+ "rowSpan": 2
+ },
+ {
+ "id": "http_status",
+ "title": "HTTP Status Code RPM",
+ "tip": "Per-class status code counts (1xx / 2xx / 3xx / 4xx / 5xx).",
+ "type": "line",
+ "expressions": [
+ "kubernetes_service_http_status_1xx_cpm",
+ "kubernetes_service_http_status_2xx_cpm",
+ "kubernetes_service_http_status_3xx_cpm",
+ "kubernetes_service_http_status_4xx_cpm",
+ "kubernetes_service_http_status_5xx_cpm"
+ ],
+ "expressionLabels": ["1xx", "2xx", "3xx", "4xx", "5xx"],
+ "span": 6,
+ "rowSpan": 2
+ },
+ {
+ "id": "http_req_resp_size",
+ "title": "HTTP Request / Response Size",
+ "type": "line",
+ "unit": "KB",
+ "expressions": [
+ "kubernetes_service_http_avg_req_header_size/1024",
+ "kubernetes_service_http_avg_req_body_size/1024",
+ "kubernetes_service_http_avg_resp_header_size/1024",
+ "kubernetes_service_http_avg_resp_body_size/1024"
+ ],
+ "expressionLabels": ["req hdr", "req body", "resp hdr", "resp body"],
+ "span": 6,
+ "rowSpan": 2
+ },
+ {
+ "id": "tcp_connect",
+ "title": "TCP Connect",
+ "tip": "Client-side connect attempts and successes per minute.",
+ "type": "line",
+ "expressions": [
+ "kubernetes_service_connect_cpm",
+ "kubernetes_service_connect_success_cpm"
+ ],
+ "expressionLabels": ["connect", "success"],
+ "span": 4,
+ "rowSpan": 2
+ },
+ {
+ "id": "tcp_connect_time",
+ "title": "TCP Connect Duration",
+ "type": "line",
+ "unit": "ns",
+ "expressions": ["kubernetes_service_connect_time"],
+ "span": 4,
+ "rowSpan": 2
+ },
+ {
+ "id": "tcp_accept",
+ "title": "TCP Accept",
+ "type": "line",
+ "expressions": [
+ "kubernetes_service_accept_cpm"
+ ],
+ "span": 4,
+ "rowSpan": 2
+ },
+ {
+ "id": "tcp_packets",
+ "title": "TCP Packets",
+ "tip": "Read + write packet counts per minute, including retransmits.",
+ "type": "line",
+ "expressions": [
+ "kubernetes_service_read_package_cpm",
+ "kubernetes_service_write_package_cpm",
+ "kubernetes_service_write_retrains_package_cpm"
+ ],
+ "expressionLabels": ["read", "write", "write retrans"],
+ "span": 6,
+ "rowSpan": 2
+ },
+ {
+ "id": "tcp_bytes",
+ "title": "TCP Bytes",
+ "type": "line",
+ "unit": "KB",
+ "expressions": [
+ "kubernetes_service_read_package_size/1024",
+ "kubernetes_service_write_package_size/1024"
+ ],
+ "expressionLabels": ["read", "write"],
+ "span": 6,
+ "rowSpan": 2
+ }
+ ],
+ "instance": [
+ {
+ "id": "pod_http_cpm",
+ "title": "Pod HTTP RPM",
+ "type": "line",
+ "unit": "rpm",
+ "expressions": ["kubernetes_service_instance_http_call_cpm"],
+ "span": 4,
+ "rowSpan": 2
+ },
+ {
+ "id": "pod_http_time",
+ "title": "Pod HTTP Response Time",
+ "type": "line",
+ "unit": "ms",
+ "expressions": ["kubernetes_service_instance_http_call_time"],
+ "span": 4,
+ "rowSpan": 2
+ },
+ {
+ "id": "pod_http_status",
+ "title": "Pod HTTP Status Code RPM",
+ "type": "line",
+ "expressions": [
+ "kubernetes_service_instance_http_status_1xx_cpm",
+ "kubernetes_service_instance_http_status_2xx_cpm",
+ "kubernetes_service_instance_http_status_3xx_cpm",
+ "kubernetes_service_instance_http_status_4xx_cpm",
+ "kubernetes_service_instance_http_status_5xx_cpm"
+ ],
+ "expressionLabels": ["1xx", "2xx", "3xx", "4xx", "5xx"],
+ "span": 6,
+ "rowSpan": 2
+ },
+ {
+ "id": "pod_http_sizes",
+ "title": "Pod HTTP Sizes",
+ "type": "line",
+ "unit": "KB",
+ "expressions": [
+ "kubernetes_service_instance_http_avg_req_header_size/1024",
+ "kubernetes_service_instance_http_avg_req_body_size/1024",
+ "kubernetes_service_instance_http_avg_resp_header_size/1024",
+ "kubernetes_service_instance_http_avg_resp_body_size/1024"
+ ],
+ "expressionLabels": ["req hdr", "req body", "resp hdr", "resp body"],
+ "span": 6,
+ "rowSpan": 2
+ },
+ {
+ "id": "pod_tcp_connect",
+ "title": "Pod TCP Connect",
+ "type": "line",
+ "expressions": [
+ "kubernetes_service_instance_connect_cpm",
+ "kubernetes_service_instance_connect_success_cpm"
+ ],
+ "expressionLabels": ["connect", "success"],
+ "span": 4,
+ "rowSpan": 2
+ },
+ {
+ "id": "pod_tcp_packets",
+ "title": "Pod TCP Packets",
+ "type": "line",
+ "expressions": [
+ "kubernetes_service_instance_read_package_cpm",
+ "kubernetes_service_instance_write_package_cpm",
+ "kubernetes_service_instance_write_retrains_package_cpm"
+ ],
+ "expressionLabels": ["read", "write", "write retrans"],
+ "span": 4,
+ "rowSpan": 2
+ },
+ {
+ "id": "pod_tcp_bytes",
+ "title": "Pod TCP Bytes",
+ "type": "line",
+ "unit": "KB",
+ "expressions": [
+ "kubernetes_service_instance_read_package_size/1024",
+ "kubernetes_service_instance_write_package_size/1024"
+ ],
+ "expressionLabels": ["read", "write"],
+ "span": 4,
+ "rowSpan": 2
+ }
+ ],
+ "endpoint": [
+ {
+ "id": "ep_http_cpm",
+ "title": "Endpoint HTTP RPM",
+ "type": "line",
+ "unit": "rpm",
+ "expressions": ["kubernetes_service_endpoint_http_call_cpm"],
+ "span": 4,
+ "rowSpan": 2
+ },
+ {
+ "id": "ep_http_time",
+ "title": "Endpoint HTTP Response Time",
+ "type": "line",
+ "unit": "ms",
+ "expressions": ["kubernetes_service_endpoint_http_call_time"],
+ "span": 4,
+ "rowSpan": 2
+ },
+ {
+ "id": "ep_http_status",
+ "title": "Endpoint Status Code RPM",
+ "type": "line",
+ "expressions": [
+ "kubernetes_service_endpoint_http_status_1xx_cpm",
+ "kubernetes_service_endpoint_http_status_2xx_cpm",
+ "kubernetes_service_endpoint_http_status_3xx_cpm",
+ "kubernetes_service_endpoint_http_status_4xx_cpm",
+ "kubernetes_service_endpoint_http_status_5xx_cpm"
+ ],
+ "expressionLabels": ["1xx", "2xx", "3xx", "4xx", "5xx"],
+ "span": 4,
+ "rowSpan": 2
+ },
+ {
+ "id": "ep_http_req_sizes",
+ "title": "Endpoint Request Sizes",
+ "type": "line",
+ "unit": "KB",
+ "expressions": [
+ "kubernetes_service_endpoint_http_avg_req_header_size/1024",
+ "kubernetes_service_endpoint_http_avg_req_body_size/1024"
+ ],
+ "expressionLabels": ["header", "body"],
+ "span": 6,
+ "rowSpan": 2
+ },
+ {
+ "id": "ep_http_resp_sizes",
+ "title": "Endpoint Response Sizes",
+ "type": "line",
+ "unit": "KB",
+ "expressions": [
+ "kubernetes_service_endpoint_http_avg_resp_header_size/1024",
+ "kubernetes_service_endpoint_http_avg_resp_body_size/1024"
+ ],
+ "expressionLabels": ["header", "body"],
+ "span": 6,
+ "rowSpan": 2
+ }
+ ]
+ },
+ "topology": {
+ "nodeMetrics": [
+ { "id": "httpCpm", "label": "RPM", "mqe": "kubernetes_service_http_call_cpm", "unit": "rpm", "role": "center", "aggregation": "avg" },
+ {
+ "id": "httpSuccess",
+ "label": "Success Rate",
+ "mqe": "kubernetes_service_http_call_successful_rate/100",
+ "unit": "%",
+ "role": "ring",
+ "aggregation": "avg",
+ "thresholds": {
+ "invertHealth": true,
+ "invertBase": 100,
+ "ok": 0.1,
+ "warn": 1,
+ "danger": 5
+ }
+ },
+ { "id": "httpRespTime", "label": "Latency", "mqe": "kubernetes_service_http_call_time", "unit": "ms", "role": "secondary", "aggregation": "avg" }
+ ],
+ "linkServerMetrics": [
+ { "id": "cpm", "label": "RPM", "mqe": "kubernetes_service_relation_server_http_call_cpm", "unit": "rpm", "role": "lineServer", "aggregation": "avg" },
+ { "id": "respTime", "label": "Avg response time", "mqe": "kubernetes_service_relation_server_http_call_time", "unit": "ms", "aggregation": "avg" }
+ ],
+ "linkClientMetrics": [
+ { "id": "cpm", "label": "RPM", "mqe": "kubernetes_service_relation_client_http_call_cpm", "unit": "rpm", "role": "lineClient", "aggregation": "avg" },
+ { "id": "respTime", "label": "Avg response time", "mqe": "kubernetes_service_relation_client_http_call_time", "unit": "ms", "aggregation": "avg" }
+ ]
+ }
+}
diff --git a/apps/bff/src/bundled_templates/layers/kafka.json b/apps/bff/src/bundled_templates/layers/kafka.json
new file mode 100644
index 0000000..32d1280
--- /dev/null
+++ b/apps/bff/src/bundled_templates/layers/kafka.json
@@ -0,0 +1,54 @@
+{
+ "key": "KAFKA",
+ "alias": "Kafka",
+ "color": "var(--sw-purple)",
+ "documentLink": "https://skywalking.apache.org/docs/main/next/en/setup/backend/backend-kafka-monitoring/",
+ "aliases": { "services": "Kafka clusters", "instances": "Brokers" },
+ "components": { "service": true, "instances": true, "endpoints": false, "topology": false, "traces": false, "logs": false },
+ "layer-header": {
+ "orderBy": "partitions",
+ "columns": [
+ { "metric": "partitions", "label": "Partitions", "mqe": "meter_kafka_partition_count", "aggregation": "avg" },
+ { "metric": "offline", "label": "Offline Partitions", "mqe": "meter_kafka_offline_partitions_count", "aggregation": "sum" },
+ { "metric": "maxLag", "label": "Max Lag", "mqe": "meter_kafka_max_lag", "aggregation": "max" },
+ { "metric": "leaders", "label": "Leaders", "mqe": "meter_kafka_leader_count", "aggregation": "sum" }
+ ]
+ },
+ "overview": {
+ "groups": [
+ { "title": "Cluster", "size": "auto", "metrics": [
+ { "id": "offline", "label": "Offline Partitions", "tip": "Partitions without an active leader.", "mqe": "meter_kafka_offline_partitions_count", "aggregation": "sum" },
+ { "id": "underReplicated", "label": "Under-Replicated", "tip": "Partitions whose ISR set is below replication factor.", "mqe": "meter_kafka_under_replicated_partitions", "aggregation": "sum" },
+ { "id": "maxLag", "label": "Max Lag", "tip": "Largest consumer lag observed.", "mqe": "meter_kafka_max_lag", "aggregation": "max" }
+ ]}
+ ]
+ },
+ "dashboards": {
+ "service": [
+ { "id": "partitions", "title": "Partition Count", "type": "line", "expressions": ["meter_kafka_partition_count"], "span": 3, "rowSpan": 2 },
+ { "id": "leaders", "title": "Leader Count", "type": "line", "expressions": ["meter_kafka_leader_count"], "span": 3, "rowSpan": 2 },
+ { "id": "active_ctrl", "title": "Active Controllers", "type": "line", "expressions": ["meter_kafka_active_controller_count"], "span": 3, "rowSpan": 2 },
+ { "id": "max_lag", "title": "Max Lag", "type": "line", "expressions": ["meter_kafka_max_lag"], "span": 3, "rowSpan": 2 },
+ { "id": "under_replicated", "title": "Under-Replicated Partitions", "type": "line", "expressions": ["meter_kafka_under_replicated_partitions"], "span": 4, "rowSpan": 2 },
+ { "id": "offline", "title": "Offline Partitions", "type": "line", "expressions": ["meter_kafka_offline_partitions_count"], "span": 4, "rowSpan": 2 },
+ { "id": "leader_election", "title": "Leader Election Rate", "type": "line", "expressions": ["meter_kafka_leader_election_rate", "meter_kafka_unclean_leader_elections_per_second"], "expressionLabels": ["elections", "unclean"], "span": 4, "rowSpan": 2 }
+ ],
+ "instance": [
+ { "id": "cpu", "title": "CPU Usage", "type": "line", "unit": "%", "expressions": ["meter_kafka_broker_cpu_time_total"], "span": 3, "rowSpan": 2 },
+ { "id": "msg_rate", "title": "Incoming Messages / s", "type": "line", "expressions": ["meter_kafka_broker_messages_per_second"], "span": 3, "rowSpan": 2 },
+ { "id": "bytes_in", "title": "Bytes In / s", "type": "line", "expressions": ["meter_kafka_broker_bytes_in_per_second"], "span": 3, "rowSpan": 2 },
+ { "id": "bytes_out", "title": "Bytes Out / s", "type": "line", "expressions": ["meter_kafka_broker_bytes_out_per_second"], "span": 3, "rowSpan": 2 },
+ { "id": "req_rate", "title": "Requests / s", "type": "line", "expressions": ["meter_kafka_broker_requests_per_second"], "span": 3, "rowSpan": 2 },
+ { "id": "purgatory", "title": "Purgatory Size", "type": "line", "expressions": ["meter_kafka_broker_purgatory_size"], "span": 3, "rowSpan": 2 },
+ { "id": "isr", "title": "ISR Shrinks / Expands", "type": "line", "expressions": ["latest(meter_kafka_broker_isr_shrinks_per_second)", "latest(meter_kafka_broker_isr_expands_per_second)"], "expressionLabels": ["shrinks", "expands"], "span": 3, "rowSpan": 2 },
+ { "id": "mem_pct", "title": "Memory Usage (%)", "type": "line", "unit": "%", "expressions": ["meter_kafka_broker_memory_usage_percentage"], "span": 3, "rowSpan": 2 },
+ { "id": "under_replicated", "title": "Under-Replicated", "type": "line", "expressions": ["latest(meter_kafka_broker_under_replicated_partitions)", "latest(meter_kafka_broker_under_min_isr_partition_count)"], "expressionLabels": ["under-replicated", "under-min-isr"], "span": 6, "rowSpan": 2 },
+ { "id": "partitions_leaders", "title": "Partitions + Leaders", "type": "line", "expressions": ["meter_kafka_broker_partition_count", "meter_kafka_broker_leader_count"], "expressionLabels": ["partitions", "leaders"], "span": 6, "rowSpan": 2 },
+ { "id": "queue_times", "title": "Queue / Send Times", "type": "line", "unit": "ms", "expressions": ["meter_kafka_broker_request_queue_time_ms", "meter_kafka_broker_response_queue_time_ms", "meter_kafka_broker_response_send_time_ms", "meter_kafka_broker_remote_time_ms"], "expressionLabels": ["request q", "response q", "response send", "remote"], "span": 6, "rowSpan": 2 },
+ { "id": "topic_rates", "title": "Topic Rates", "type": "line", "expressions": ["meter_kafka_broker_topic_produce_requests_per_second", "meter_kafka_broker_topic_fetch_requests_per_second", "meter_kafka_broker_topic_bytesin_per_second"], "expressionLabels": ["produce req/s", "fetch req/s", "bytes-in/s"], "span": 6, "rowSpan": 2 },
+ { "id": "replication", "title": "Replication", "type": "line", "expressions": ["meter_kafka_broker_replication_bytes_in_per_second", "meter_kafka_broker_replication_bytes_out_per_second"], "expressionLabels": ["bytes in", "bytes out"], "span": 6, "rowSpan": 2 },
+ { "id": "gc", "title": "GC Count", "type": "line", "expressions": ["meter_kafka_broker_garbage_collector_count"], "span": 6, "rowSpan": 2 },
+ { "id": "max_lag", "title": "Max Lag (broker)", "type": "line", "expressions": ["sum(meter_kafka_broker_max_lag)"], "span": 6, "rowSpan": 2 }
+ ]
+ }
+}
diff --git a/apps/bff/src/bundled_templates/layers/kong.json b/apps/bff/src/bundled_templates/layers/kong.json
new file mode 100644
index 0000000..9eee3ce
--- /dev/null
+++ b/apps/bff/src/bundled_templates/layers/kong.json
@@ -0,0 +1,81 @@
+{
+ "key": "KONG",
+ "alias": "Kong",
+ "color": "var(--sw-ok)",
+ "documentLink": "https://skywalking.apache.org/docs/main/next/en/setup/backend/backend-kong-monitoring/",
+ "aliases": {
+ "services": "Kong services",
+ "instances": "Nodes",
+ "endpoints": "Routes"
+ },
+ "components": {
+ "service": true,
+ "instances": true,
+ "endpoints": true,
+ "topology": false,
+ "traces": false,
+ "logs": false
+ },
+ "layer-header": {
+ "orderBy": "rps",
+ "columns": [
+ { "metric": "rps", "label": "RPS", "mqe": "aggregate_labels(meter_kong_service_http_requests,sum)", "aggregation": "sum" },
+ { "metric": "status200", "label": "200/s", "mqe": "aggregate_labels(meter_kong_service_http_status{code='200'}, sum)", "aggregation": "sum" },
+ { "metric": "status404", "label": "404/s", "mqe": "aggregate_labels(meter_kong_service_http_status{code='404'}, sum)", "aggregation": "sum" },
+ { "metric": "status500", "label": "500/s", "mqe": "aggregate_labels(meter_kong_service_http_status{code='500'}, sum)", "aggregation": "sum" }
+ ]
+ },
+ "overview": {
+ "groups": [
+ {
+ "title": "Latency",
+ "size": "auto",
+ "metrics": [
+ { "id": "kongLatency", "label": "Kong Latency", "tip": "Time Kong itself spends per request, p50/p95.", "mqe": "aggregate_labels(meter_kong_service_kong_latency,avg(p))", "unit": "ms", "aggregation": "avg" },
+ { "id": "upstreamLatency", "label": "Upstream Latency", "tip": "Time waiting on the upstream service.", "mqe": "aggregate_labels(meter_kong_service_upstream_latency,avg(p))", "unit": "ms", "aggregation": "avg" }
+ ]
+ },
+ {
+ "title": "Throughput",
+ "size": "square",
+ "metrics": [
+ { "id": "rps", "label": "RPS", "tip": "Requests per second through Kong.", "mqe": "aggregate_labels(meter_kong_service_http_requests,sum)", "aggregation": "sum" }
+ ]
+ }
+ ]
+ },
+ "dashboards": {
+ "service": [
+ { "id": "rps", "title": "HTTP Request Trend", "type": "line", "expressions": ["aggregate_labels(meter_kong_service_http_requests,sum)"], "span": 4, "rowSpan": 2 },
+ { "id": "status", "title": "HTTP Status Trend", "type": "line", "expressions": ["aggregate_labels(meter_kong_service_http_status,sum(code))"], "span": 4, "rowSpan": 2 },
+ { "id": "bandwidth", "title": "HTTP Bandwidth", "type": "line", "unit": "KB/s", "expressions": ["aggregate_labels(meter_kong_service_http_bandwidth,sum(direction))/1024"], "span": 4, "rowSpan": 2 },
+ { "id": "kong_latency", "title": "Kong Latency", "type": "line", "unit": "ms", "expressions": ["aggregate_labels(meter_kong_service_kong_latency,avg(p))"], "span": 4, "rowSpan": 2 },
+ { "id": "req_latency", "title": "Request Latency", "type": "line", "unit": "ms", "expressions": ["aggregate_labels(meter_kong_service_request_latency,avg(p))"], "span": 4, "rowSpan": 2 },
+ { "id": "upstream_latency", "title": "Upstream Latency", "type": "line", "unit": "ms", "expressions": ["aggregate_labels(meter_kong_service_upstream_latency,avg(p))"], "span": 4, "rowSpan": 2 },
+ { "id": "conns", "title": "Nginx Connections", "type": "line", "expressions": ["aggregate_labels(meter_kong_service_nginx_connections_total,sum(state))"], "span": 6, "rowSpan": 2 },
+ { "id": "timers", "title": "Nginx Timers", "type": "line", "expressions": ["aggregate_labels(meter_kong_service_nginx_timers,sum(state))"], "span": 6, "rowSpan": 2 },
+ { "id": "datastore", "title": "Datastore Reachable", "type": "line", "expressions": ["latest(aggregate_labels(meter_kong_service_datastore_reachable,sum(service_instance_id)))"], "span": 6, "rowSpan": 2 },
+ { "id": "nginx_errors", "title": "Nginx Metric Errors", "type": "line", "expressions": ["latest(aggregate_labels(meter_kong_service_nginx_metric_errors_total,sum))"], "span": 6, "rowSpan": 2 }
+ ],
+ "instance": [
+ { "id": "rps", "title": "HTTP Request Trend", "type": "line", "expressions": ["meter_kong_instance_http_requests"], "span": 4, "rowSpan": 2 },
+ { "id": "status", "title": "HTTP Status Trend", "type": "line", "expressions": ["meter_kong_instance_http_status"], "span": 4, "rowSpan": 2 },
+ { "id": "bandwidth", "title": "HTTP Bandwidth", "type": "line", "unit": "KB/s", "expressions": ["meter_kong_instance_http_bandwidth/1024"], "span": 4, "rowSpan": 2 },
+ { "id": "kong_latency", "title": "Kong Latency", "type": "line", "unit": "ms", "expressions": ["meter_kong_instance_kong_latency"], "span": 4, "rowSpan": 2 },
+ { "id": "req_latency", "title": "Request Latency", "type": "line", "unit": "ms", "expressions": ["meter_kong_instance_request_latency"], "span": 4, "rowSpan": 2 },
+ { "id": "upstream_latency", "title": "Upstream Latency", "type": "line", "unit": "ms", "expressions": ["meter_kong_instance_upstream_latency"], "span": 4, "rowSpan": 2 },
+ { "id": "datastore", "title": "Datastore Reachable", "type": "line", "expressions": ["latest(meter_kong_instance_datastore_reachable)"], "span": 4, "rowSpan": 2 },
+ { "id": "conns", "title": "Nginx Connections", "type": "line", "expressions": ["meter_kong_instance_nginx_connections_total"], "span": 4, "rowSpan": 2 },
+ { "id": "timers", "title": "Nginx Timers", "type": "line", "expressions": ["meter_kong_instance_nginx_timers"], "span": 4, "rowSpan": 2 },
+ { "id": "shared_mem", "title": "Shared Memory Usage", "type": "line", "unit": "%", "expressions": ["(meter_kong_instance_shared_dict_bytes*100)/meter_kong_instance_shared_dict_total_bytes"], "span": 6, "rowSpan": 2 },
+ { "id": "lua_vm", "title": "Worker Lua VM Usage", "type": "line", "unit": "MB", "expressions": ["meter_kong_instance_memory_workers_lua_vms_bytes/1024/1024"], "span": 6, "rowSpan": 2 }
+ ],
+ "endpoint": [
+ { "id": "status", "title": "HTTP Status Trend", "type": "line", "expressions": ["meter_kong_endpoint_http_status"], "span": 4, "rowSpan": 2 },
+ { "id": "bandwidth", "title": "Total Bandwidth", "type": "line", "unit": "KB/s", "expressions": ["meter_kong_endpoint_http_bandwidth/1024"], "span": 4, "rowSpan": 2 },
+ { "id": "kong_latency", "title": "Kong Latency", "type": "line", "unit": "ms", "expressions": ["meter_kong_endpoint_kong_latency"], "span": 4, "rowSpan": 2 },
+ { "id": "req_latency", "title": "Request Latency", "type": "line", "unit": "ms", "expressions": ["meter_kong_endpoint_request_latency"], "span": 6, "rowSpan": 2 },
+ { "id": "upstream_latency", "title": "Upstream Latency", "type": "line", "unit": "ms", "expressions": ["meter_kong_endpoint_upstream_latency"], "span": 6, "rowSpan": 2 }
+ ]
+ }
+}
diff --git a/apps/bff/src/bundled_templates/layers/mesh.json b/apps/bff/src/bundled_templates/layers/mesh.json
new file mode 100644
index 0000000..d7b34b1
--- /dev/null
+++ b/apps/bff/src/bundled_templates/layers/mesh.json
@@ -0,0 +1,455 @@
+{
+ "key": "MESH",
+ "alias": "Service Mesh",
+ "color": "var(--sw-info)",
+ "documentLink": "https://skywalking.apache.org/docs/main/next/en/setup/envoy/als_setting/",
+ "aliases": {
+ "services": "Services",
+ "instances": "Sidecars",
+ "endpoints": "Endpoints"
+ },
+ "components": {
+ "service": true,
+ "instances": true,
+ "endpoints": true,
+ "endpointDependency": false,
+ "topology": true,
+ "traces": true,
+ "logs": true,
+ "ebpfProfiling": true
+ },
+ "layer-header": {
+ "orderBy": "cpm",
+ "columns": [
+ { "metric": "cpm", "label": "Traffic", "unit": "rpm", "mqe": "service_cpm", "aggregation": "sum" },
+ { "metric": "apdex", "label": "Apdex", "mqe": "service_apdex/10000", "aggregation": "avg" },
+ { "metric": "respTime", "label": "Latency", "unit": "ms", "mqe": "service_resp_time", "aggregation": "avg" },
+ { "metric": "err", "label": "Error Rate", "unit": "%", "mqe": "100 - service_sla/100", "aggregation": "avg" }
+ ]
+ },
+ "overview": {
+ "groups": [
+ {
+ "title": "Latency & errors",
+ "size": "auto",
+ "metrics": [
+ {
+ "id": "p95",
+ "label": "P95",
+ "tip": "95th-percentile sidecar-routed response time across the mesh.",
+ "mqe": "service_percentile{p='95'}",
+ "unit": "ms",
+ "aggregation": "avg"
+ },
+ {
+ "id": "err",
+ "label": "Error Rate",
+ "tip": "100 − mesh SLA. Calls with non-2xx response status from sidecar ALS.",
+ "mqe": "100 - service_sla/100",
+ "unit": "%",
+ "aggregation": "avg"
+ }
+ ]
+ },
+ {
+ "title": "Health",
+ "size": "square",
+ "metrics": [
+ {
+ "id": "apdex",
+ "label": "Apdex",
+ "tip": "User-satisfaction score on a 0 – 1 scale.",
+ "mqe": "service_apdex/10000",
+ "aggregation": "avg"
+ }
+ ]
+ }
+ ]
+ },
+ "dashboards": {
+ "service": [
+ {
+ "id": "top_apis",
+ "title": "Top 20 APIs",
+ "tip": "Layer-wide endpoint ranking. Switch tabs to re-rank by traffic, response time, or worst success rate.",
+ "type": "top",
+ "expressions": [
+ "top_n(endpoint_cpm,20,des)",
+ "top_n(endpoint_resp_time,20,des)",
+ "top_n(endpoint_sla,20,asc)/100"
+ ],
+ "expressionLabels": ["Traffic", "Slow", "Successful Rate"],
+ "expressionUnits": ["rpm", "ms", "%"],
+ "layerScope": true,
+ "span": 3,
+ "rowSpan": 4
+ },
+ {
+ "id": "traffic_line",
+ "title": "Traffic",
+ "tip": "Mesh-wide requests per minute observed by Envoy sidecars.",
+ "type": "line",
+ "unit": "rpm",
+ "expressions": ["service_cpm"],
+ "span": 3,
+ "rowSpan": 2
+ },
+ {
+ "id": "err_line",
+ "title": "Error Rate",
+ "type": "line",
+ "unit": "%",
+ "expressions": ["100 - service_sla/100"],
+ "span": 3,
+ "rowSpan": 2
+ },
+ {
+ "id": "apdex_line",
+ "title": "Apdex",
+ "tip": "User-satisfaction score on a 0 – 1 scale.",
+ "type": "line",
+ "expressions": ["service_apdex/10000"],
+ "span": 3,
+ "rowSpan": 2
+ },
+ {
+ "id": "resp_time_line",
+ "title": "Avg Response Time",
+ "type": "line",
+ "unit": "ms",
+ "expressions": ["service_resp_time"],
+ "span": 3,
+ "rowSpan": 2
+ },
+ {
+ "id": "percentile_line",
+ "title": "Response Time Percentile",
+ "tip": "p50 / p75 / p90 / p95 / p99 latency — the tail of the response-time distribution.",
+ "type": "line",
+ "unit": "ms",
+ "expressions": [
+ "relabels(service_percentile{p='50,75,90,95,99'},p='50,75,90,95,99',percentile='50,75,90,95,99')"
+ ],
+ "span": 3,
+ "rowSpan": 2
+ },
+ {
+ "id": "throughput_line",
+ "title": "Service Throughput",
+ "tip": "Bytes per minute through the sidecar — sent + received.",
+ "type": "line",
+ "unit": "B/min",
+ "expressions": [
+ "service_throughput_received",
+ "service_throughput_sent"
+ ],
+ "expressionLabels": ["received", "sent"],
+ "span": 3,
+ "rowSpan": 2
+ },
+ {
+ "id": "sidecar_internal_latency",
+ "title": "Sidecar Internal Latency",
+ "tip": "Envoy-internal request + response latency, in nanoseconds. Useful when sidecar overhead is suspected.",
+ "type": "line",
+ "unit": "ns",
+ "expressions": [
+ "service_sidecar_internal_req_latency_nanos",
+ "service_sidecar_internal_resp_latency_nanos"
+ ],
+ "expressionLabels": ["request", "response"],
+ "span": 3,
+ "rowSpan": 2
+ },
+ {
+ "id": "top_instances",
+ "title": "Top 10 sidecars",
+ "tip": "Ranked across the selected service's sidecar instances.",
+ "type": "top",
+ "expressions": [
+ "top_n(service_instance_cpm,10,des)",
+ "top_n(service_instance_resp_time,10,des)",
+ "top_n(service_instance_sla,10,asc)/100"
+ ],
+ "expressionLabels": ["Traffic", "Slow", "Successful Rate"],
+ "expressionUnits": ["rpm", "ms", "%"],
+ "span": 6,
+ "rowSpan": 3
+ },
+ {
+ "id": "top_endpoints_in_service",
+ "title": "Top 10 endpoints in service",
+ "tip": "Ranked across the selected service's endpoints.",
+ "type": "top",
+ "expressions": [
+ "top_n(endpoint_cpm,10,des)",
+ "top_n(endpoint_resp_time,10,des)",
+ "top_n(endpoint_sla,10,asc)/100"
+ ],
+ "expressionLabels": ["Traffic", "Slow", "Successful Rate"],
+ "expressionUnits": ["rpm", "ms", "%"],
+ "span": 6,
+ "rowSpan": 3
+ }
+ ],
+ "instance": [
+ {
+ "id": "instance_cpm_line",
+ "title": "Sidecar Load",
+ "tip": "Calls per minute against the selected sidecar instance.",
+ "type": "line",
+ "unit": "calls / min",
+ "expressions": ["service_instance_cpm"],
+ "span": 4,
+ "rowSpan": 2
+ },
+ {
+ "id": "instance_resp_line",
+ "title": "Sidecar Latency",
+ "type": "line",
+ "unit": "ms",
+ "expressions": ["service_instance_resp_time"],
+ "span": 4,
+ "rowSpan": 2
+ },
+ {
+ "id": "instance_sla_line",
+ "title": "Sidecar Success Rate",
+ "type": "line",
+ "unit": "%",
+ "expressions": ["service_instance_sla/100"],
+ "span": 4,
+ "rowSpan": 2
+ },
+ {
+ "id": "instance_throughput",
+ "title": "Sidecar Throughput",
+ "tip": "Bytes through the sidecar — received + sent.",
+ "type": "line",
+ "unit": "B/min",
+ "expressions": [
+ "service_instance_throughput_received",
+ "service_instance_throughput_sent"
+ ],
+ "expressionLabels": ["received", "sent"],
+ "span": 6,
+ "rowSpan": 2
+ },
+ {
+ "id": "instance_sidecar_internal_latency",
+ "title": "Sidecar Internal Latency",
+ "type": "line",
+ "unit": "ns",
+ "expressions": [
+ "service_instance_sidecar_internal_req_latency_nanos",
+ "service_instance_sidecar_internal_resp_latency_nanos"
+ ],
+ "expressionLabels": ["request", "response"],
+ "span": 6,
+ "rowSpan": 2
+ },
+ {
+ "id": "envoy_up_rq_active",
+ "title": "Envoy Upstream Request Active",
+ "tip": "In-flight upstream requests per cluster. Renders for Envoy-instrumented sidecars.",
+ "type": "line",
+ "expressions": ["envoy_cluster_up_rq_active"],
+ "visibleWhen": "envoy_cluster_up_rq_active has value",
+ "span": 4,
+ "rowSpan": 2
+ },
+ {
+ "id": "envoy_up_rq_incr",
+ "title": "Envoy Upstream Request Increase",
+ "tip": "Upstream requests added per minute.",
+ "type": "line",
+ "expressions": ["envoy_cluster_up_rq_incr"],
+ "visibleWhen": "envoy_cluster_up_rq_incr has value",
+ "span": 4,
+ "rowSpan": 2
+ },
+ {
+ "id": "envoy_up_rq_pending_active",
+ "title": "Envoy Upstream Pending Active",
+ "tip": "Pending upstream requests — indicates connection-pool back-pressure.",
+ "type": "line",
+ "expressions": ["envoy_cluster_up_rq_pending_active"],
+ "visibleWhen": "envoy_cluster_up_rq_pending_active has value",
+ "span": 4,
+ "rowSpan": 2
+ },
+ {
+ "id": "envoy_up_cx_active",
+ "title": "Envoy Upstream Connection Active",
+ "type": "line",
+ "expressions": ["envoy_cluster_up_cx_active"],
+ "visibleWhen": "envoy_cluster_up_cx_active has value",
+ "span": 4,
+ "rowSpan": 2
+ },
+ {
+ "id": "envoy_up_cx_incr",
+ "title": "Envoy Upstream Connection Increase",
+ "type": "line",
+ "expressions": ["envoy_cluster_up_cx_incr"],
+ "visibleWhen": "envoy_cluster_up_cx_incr has value",
+ "span": 4,
+ "rowSpan": 2
+ },
+ {
+ "id": "envoy_membership_healthy",
+ "title": "Envoy Cluster Healthy Membership",
+ "tip": "Healthy upstream members per cluster — a non-trivial drop signals upstream churn.",
+ "type": "line",
+ "expressions": ["envoy_cluster_membership_healthy"],
+ "visibleWhen": "envoy_cluster_membership_healthy has value",
+ "span": 4,
+ "rowSpan": 2
+ },
+ {
+ "id": "envoy_connections",
+ "title": "Envoy Total Connections",
+ "type": "line",
+ "expressions": [
+ "envoy_total_connections_used",
+ "envoy_parent_connections_used"
+ ],
+ "expressionLabels": ["total", "parent"],
+ "visibleWhen": "envoy_total_connections_used has value",
+ "span": 4,
+ "rowSpan": 2
+ },
+ {
+ "id": "envoy_heap_memory",
+ "title": "Envoy Heap Memory",
+ "tip": "Envoy memory usage. Heap vs physical, allocated vs max, in MB.",
+ "type": "line",
+ "unit": "MB",
+ "expressions": [
+ "envoy_heap_memory_used/1024/1024",
+ "envoy_heap_memory_max_used/1024/1024",
+ "envoy_memory_allocated/1024/1024",
+ "envoy_memory_allocated_max/1024/1024",
+ "envoy_memory_physical_size/1024/1024",
+ "envoy_memory_physical_size_max/1024/1024"
+ ],
+ "expressionLabels": ["heap used", "heap max", "allocated", "allocated max", "physical", "physical max"],
+ "visibleWhen": "envoy_heap_memory_used has value",
+ "span": 6,
+ "rowSpan": 2
+ },
+ {
+ "id": "envoy_worker_threads",
+ "title": "Envoy Worker Threads",
+ "type": "line",
+ "expressions": [
+ "envoy_worker_threads",
+ "envoy_worker_threads_max"
+ ],
+ "expressionLabels": ["live", "max"],
+ "visibleWhen": "envoy_worker_threads has value",
+ "span": 3,
+ "rowSpan": 2
+ },
+ {
+ "id": "envoy_bug_failures",
+ "title": "Envoy Bug Failures",
+ "tip": "Envoy's own assertion / bug counter. Should be zero in healthy clusters.",
+ "type": "line",
+ "expressions": ["envoy_bug_failures"],
+ "visibleWhen": "envoy_bug_failures has value",
+ "span": 3,
+ "rowSpan": 2
+ }
+ ],
+ "endpoint": [
+ {
+ "id": "endpoint_cpm_line",
+ "title": "Endpoint Traffic",
+ "type": "line",
+ "unit": "rpm",
+ "expressions": ["endpoint_cpm"],
+ "span": 4,
+ "rowSpan": 2
+ },
+ {
+ "id": "endpoint_resp_line",
+ "title": "Endpoint Avg Response Time",
+ "type": "line",
+ "unit": "ms",
+ "expressions": ["endpoint_resp_time"],
+ "span": 4,
+ "rowSpan": 2
+ },
+ {
+ "id": "endpoint_sla_line",
+ "title": "Endpoint Success Rate",
+ "type": "line",
+ "unit": "%",
+ "expressions": ["endpoint_sla/100"],
+ "span": 4,
+ "rowSpan": 2
+ },
+ {
+ "id": "endpoint_percentile",
+ "title": "Endpoint Response Time Percentile",
+ "type": "line",
+ "unit": "ms",
+ "expressions": [
+ "relabels(endpoint_percentile{p='50,75,90,95,99'},p='50,75,90,95,99',percentile='50,75,90,95,99')"
+ ],
+ "span": 12,
+ "rowSpan": 2
+ },
+ {
+ "id": "endpoint_sidecar_internal_latency",
+ "title": "Sidecar Internal Latency (endpoint scope)",
+ "tip": "Envoy-internal request + response latency on this endpoint, in nanoseconds.",
+ "type": "line",
+ "unit": "ns",
+ "expressions": [
+ "endpoint_sidecar_internal_req_latency_nanos",
+ "endpoint_sidecar_internal_resp_latency_nanos"
+ ],
+ "expressionLabels": ["request", "response"],
+ "span": 12,
+ "rowSpan": 2
+ }
+ ],
+ "trace": [],
+ "ebpfProfiling": []
+ },
+ "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,
+ "invertBase": 100,
+ "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" },
+ { "id": "respTime", "label": "Avg response time", "mqe": "service_relation_server_resp_time", "unit": "ms", "aggregation": "avg" },
+ { "id": "p95", "label": "p95", "mqe": "service_relation_server_percentile{p='95'}", "unit": "ms", "aggregation": "avg" },
+ { "id": "sla", "label": "SLA", "mqe": "service_relation_server_call_sla/100", "unit": "%", "aggregation": "avg" }
+ ],
+ "linkClientMetrics": [
+ { "id": "cpm", "label": "RPM", "mqe": "service_relation_client_cpm", "unit": "rpm", "role": "lineClient", "aggregation": "avg" },
+ { "id": "respTime", "label": "Avg response time", "mqe": "service_relation_client_resp_time", "unit": "ms", "aggregation": "avg" },
+ { "id": "p95", "label": "p95", "mqe": "service_relation_client_percentile{p='95'}", "unit": "ms", "aggregation": "avg" },
+ { "id": "sla", "label": "SLA", "mqe": "service_relation_client_call_sla/100", "unit": "%", "aggregation": "avg" }
+ ]
+ }
+}
diff --git a/apps/bff/src/bundled_templates/layers/mesh_cp.json b/apps/bff/src/bundled_templates/layers/mesh_cp.json
new file mode 100644
index 0000000..15e03e5
--- /dev/null
+++ b/apps/bff/src/bundled_templates/layers/mesh_cp.json
@@ -0,0 +1,40 @@
+{
+ "key": "MESH_CP",
+ "alias": "Mesh Control Plane",
+ "color": "var(--sw-info)",
+ "documentLink": "https://skywalking.apache.org/docs/main/next/en/setup/istio/readme/",
+ "aliases": { "services": "Control Planes" },
+ "components": { "service": true, "instances": false, "endpoints": false, "topology": false, "traces": false, "logs": false },
+ "layer-header": {
+ "orderBy": "cpu",
+ "columns": [
+ { "metric": "cpu", "label": "CPU", "mqe": "meter_istio_cpu", "aggregation": "avg" },
+ { "metric": "goroutines", "label": "Goroutines", "mqe": "meter_istio_go_goroutines", "aggregation": "sum" },
+ { "metric": "xds", "label": "Pilot xDS", "mqe": "meter_istio_pilot_xds", "aggregation": "sum" },
+ { "metric": "services", "label": "Services", "mqe": "meter_istio_pilot_services", "aggregation": "sum" }
+ ]
+ },
+ "overview": {
+ "groups": [
+ { "title": "Istio health", "size": "auto", "metrics": [
+ { "id": "cpu", "label": "CPU", "tip": "Istio control-plane process CPU.", "mqe": "meter_istio_cpu", "aggregation": "avg" },
+ { "id": "goroutines", "label": "Goroutines", "tip": "Live Go goroutines.", "mqe": "meter_istio_go_goroutines", "aggregation": "sum" }
+ ]}
+ ]
+ },
+ "dashboards": {
+ "service": [
+ { "id": "cpu", "title": "CPU", "type": "line", "expressions": ["meter_istio_cpu"], "span": 4, "rowSpan": 2 },
+ { "id": "goroutines", "title": "Goroutines", "type": "line", "expressions": ["meter_istio_go_goroutines"], "span": 4, "rowSpan": 2 },
+ { "id": "version", "title": "Istio Versions", "type": "line", "expressions": ["meter_istio_pilot_version"], "span": 4, "rowSpan": 2 },
+ { "id": "memory", "title": "Memory (MB)", "type": "line", "unit": "MB", "expressions": ["meter_istio_go_alloc/1024/1024", "meter_istio_go_heap_inuse/1024/1024", "meter_istio_go_stack_inuse/1024/1024", "meter_istio_virtual_memory/1024/1024", "meter_istio_resident_memory/1024/1024"], "expressionLabels": ["alloc", "heap inuse", "stack inuse", "virtual", "resident"], "span": 12, "rowSpan": 2 },
+ { "id": "pilot_errors", "title": "Pilot Errors", "type": "line", "expressions": ["meter_istio_pilot_xds_cds_reject", "meter_istio_pilot_xds_eds_reject", "meter_istio_pilot_xds_rds_reject", "meter_istio_pilot_xds_lds_reject", "meter_istio_pilot_xds_write_timeout"], "expressionLabels": ["cds reject", "eds reject", "rds reject", "lds reject", "write timeout"], "span": 6, "rowSpan": 2 },
+ { "id": "proxy_push_time", "title": "Proxy Push Time (percentile)", "type": "line", "unit": "ms", "expressions": ["meter_istio_pilot_proxy_push_percentile"], "span": 6, "rowSpan": 2 },
+ { "id": "pilot_pushes", "title": "Pilot Pushes", "type": "line", "expressions": ["meter_istio_pilot_xds_pushes"], "span": 4, "rowSpan": 2 },
+ { "id": "sidecar_inj", "title": "Sidecar Injection Success", "type": "line", "expressions": ["meter_istio_sidecar_injection_success_total"], "span": 4, "rowSpan": 2 },
+ { "id": "ads", "title": "ADS Monitoring", "type": "line", "expressions": ["meter_istio_pilot_xds", "meter_istio_pilot_services", "meter_istio_pilot_virt_services"], "expressionLabels": ["xds", "services", "virt services"], "span": 4, "rowSpan": 2 },
+ { "id": "validation", "title": "Configuration Validation", "type": "line", "expressions": ["meter_istio_galley_validation_passed", "meter_istio_galley_validation_failed"], "expressionLabels": ["passed", "failed"], "span": 6, "rowSpan": 2 },
+ { "id": "conflicts", "title": "Pilot Conflicts", "type": "line", "expressions": ["meter_istio_pilot_conflict_ol_tcp_tcp", "meter_istio_pilot_conflict_il", "meter_istio_pilot_conflict_ol_tcp_http", "meter_istio_pilot_conflict_ol_http_tcp"], "expressionLabels": ["ol tcp/tcp", "il", "ol tcp/http", "ol http/tcp"], "span": 6, "rowSpan": 2 }
+ ]
+ }
+}
diff --git a/apps/bff/src/bundled_templates/layers/mesh_dp.json b/apps/bff/src/bundled_templates/layers/mesh_dp.json
new file mode 100644
index 0000000..1a56ef6
--- /dev/null
+++ b/apps/bff/src/bundled_templates/layers/mesh_dp.json
@@ -0,0 +1,38 @@
+{
+ "key": "MESH_DP",
+ "alias": "Mesh Data Plane",
+ "color": "var(--sw-info)",
+ "documentLink": "https://skywalking.apache.org/docs/main/next/en/setup/envoy/metrics_service_setting/",
+ "aliases": { "services": "Sidecar services", "instances": "Sidecars" },
+ "components": { "service": true, "instances": true, "endpoints": false, "topology": false, "traces": false, "logs": true, "ebpfProfiling": true },
+ "layer-header": {
+ "orderBy": "conns",
+ "columns": [
+ { "metric": "conns", "label": "Connections", "mqe": "avg(envoy_total_connections_used)", "aggregation": "avg" },
+ { "metric": "threads", "label": "Worker Threads", "mqe": "avg(envoy_worker_threads)", "aggregation": "avg" },
+ { "metric": "bugFail", "label": "Bug Failures", "mqe": "avg(envoy_bug_failures)", "aggregation": "avg" }
+ ]
+ },
+ "overview": {
+ "groups": [
+ { "title": "Sidecar health", "size": "auto", "metrics": [
+ { "id": "conns", "label": "Connections", "tip": "Envoy connections used across sidecars.", "mqe": "avg(envoy_total_connections_used)", "aggregation": "avg" },
+ { "id": "bugFail", "label": "Bug Failures", "tip": "Envoy assertion / bug counter — should be 0 in healthy clusters.", "mqe": "avg(envoy_bug_failures)", "aggregation": "avg" }
+ ]}
+ ]
+ },
+ "dashboards": {
+ "instance": [
+ { "id": "up_rq_active", "title": "Upstream Request Active", "type": "line", "expressions": ["envoy_cluster_up_rq_active"], "span": 4, "rowSpan": 2 },
+ { "id": "up_rq_pending", "title": "Upstream Request Pending Active", "type": "line", "expressions": ["envoy_cluster_up_rq_pending_active"], "span": 4, "rowSpan": 2 },
+ { "id": "up_rq_incr", "title": "Upstream Request Increase", "type": "line", "expressions": ["envoy_cluster_up_rq_incr"], "span": 4, "rowSpan": 2 },
+ { "id": "up_cx_active", "title": "Upstream Connection Active", "type": "line", "expressions": ["envoy_cluster_up_cx_active"], "span": 4, "rowSpan": 2 },
+ { "id": "up_cx_incr", "title": "Upstream Connection Increase", "type": "line", "expressions": ["envoy_cluster_up_cx_incr"], "span": 4, "rowSpan": 2 },
+ { "id": "membership", "title": "Membership Healthy", "type": "line", "expressions": ["envoy_cluster_membership_healthy"], "span": 4, "rowSpan": 2 },
+ { "id": "connections", "title": "Total Connections", "type": "line", "expressions": ["envoy_total_connections_used", "envoy_parent_connections_used"], "expressionLabels": ["total", "parent"], "span": 6, "rowSpan": 2 },
+ { "id": "concurrency", "title": "Worker Threads", "type": "line", "expressions": ["envoy_worker_threads", "envoy_worker_threads_max"], "expressionLabels": ["live", "max"], "span": 6, "rowSpan": 2 },
+ { "id": "heap_memory", "title": "Heap Memory (Bytes)", "type": "line", "expressions": ["envoy_heap_memory_used", "envoy_heap_memory_max_used", "envoy_memory_allocated", "envoy_memory_allocated_max", "envoy_memory_physical_size", "envoy_memory_physical_size_max"], "expressionLabels": ["heap used", "heap max", "allocated", "allocated max", "physical", "physical max"], "span": 12, "rowSpan": 2 },
+ { "id": "bug_failures", "title": "Bug Failures", "type": "line", "expressions": ["envoy_bug_failures"], "span": 12, "rowSpan": 2 }
+ ]
+ }
+}
diff --git a/apps/bff/src/bundled_templates/layers/mongodb.json b/apps/bff/src/bundled_templates/layers/mongodb.json
new file mode 100644
index 0000000..98c164a
--- /dev/null
+++ b/apps/bff/src/bundled_templates/layers/mongodb.json
@@ -0,0 +1,62 @@
+{
+ "key": "MONGODB",
+ "alias": "MongoDB",
+ "color": "var(--sw-warn)",
+ "documentLink": "https://skywalking.apache.org/docs/main/next/en/setup/backend/backend-mongodb-monitoring/",
+ "aliases": { "services": "MongoDB clusters", "instances": "Nodes" },
+ "components": { "service": true, "instances": true, "endpoints": false, "topology": false, "traces": false, "logs": false },
+ "layer-header": {
+ "orderBy": "docQps",
+ "columns": [
+ { "metric": "docQps", "label": "Doc QPS", "mqe": "aggregate_labels(meter_mongodb_cluster_document_avg_qps, sum(doc_op_type))", "aggregation": "sum" },
+ { "metric": "opQps", "label": "Op QPS", "mqe": "aggregate_labels(meter_mongodb_cluster_operation_avg_qps, sum(legacy_op_type))", "aggregation": "sum" },
+ { "metric": "conns", "label": "Conns", "mqe": "aggregate_labels(meter_mongodb_cluster_connections,sum)", "aggregation": "sum" },
+ { "metric": "replLag", "label": "Repl Lag", "unit": "ms", "mqe": "meter_mongodb_cluster_repl_lag", "aggregation": "avg" }
+ ]
+ },
+ "overview": {
+ "groups": [
+ { "title": "Capacity", "size": "auto", "metrics": [
+ { "id": "dataSize", "label": "Data Size (GB)", "tip": "Total data size across the cluster.", "mqe": "latest(aggregate_labels(meter_mongodb_cluster_data_size,sum))/1024/1024/1024", "aggregation": "sum" },
+ { "id": "collCount", "label": "Collections", "tip": "Collection count across the cluster.", "mqe": "latest(aggregate_labels(meter_mongodb_cluster_collection_count,sum))", "aggregation": "sum" },
+ { "id": "objCount", "label": "Objects", "tip": "Document object count.", "mqe": "latest(aggregate_labels(meter_mongodb_cluster_object_count,sum))", "aggregation": "sum" }
+ ]},
+ { "title": "Throughput", "size": "square", "metrics": [
+ { "id": "docQps", "label": "Doc QPS", "tip": "Document operations / second.", "mqe": "aggregate_labels(meter_mongodb_cluster_document_avg_qps, sum(doc_op_type))", "aggregation": "sum" }
+ ]}
+ ]
+ },
+ "dashboards": {
+ "service": [
+ { "id": "uptime", "title": "Cluster Uptime (days)", "type": "line", "expressions": ["latest(aggregate_labels(meter_mongodb_cluster_uptime,max))/3600/24"], "span": 3, "rowSpan": 2 },
+ { "id": "data_size", "title": "Data Size (GB)", "type": "line", "expressions": ["latest(aggregate_labels(meter_mongodb_cluster_data_size,sum))/1024/1024/1024"], "span": 3, "rowSpan": 2 },
+ { "id": "coll_count", "title": "Collection Count", "type": "line", "expressions": ["latest(aggregate_labels(meter_mongodb_cluster_collection_count,sum))"], "span": 3, "rowSpan": 2 },
+ { "id": "obj_count", "title": "Object Count", "type": "line", "expressions": ["latest(aggregate_labels(meter_mongodb_cluster_object_count,sum))"], "span": 3, "rowSpan": 2 },
+ { "id": "doc_qps", "title": "Document QPS", "type": "line", "expressions": ["aggregate_labels(meter_mongodb_cluster_document_avg_qps, sum(doc_op_type))"], "span": 4, "rowSpan": 2 },
+ { "id": "op_qps", "title": "Operation QPS", "type": "line", "expressions": ["aggregate_labels(meter_mongodb_cluster_operation_avg_qps, sum(legacy_op_type))"], "span": 4, "rowSpan": 2 },
+ { "id": "conns", "title": "Total Connections", "type": "line", "expressions": ["aggregate_labels(meter_mongodb_cluster_connections,sum)"], "span": 4, "rowSpan": 2 },
+ { "id": "cursors", "title": "Cursor Total", "type": "line", "expressions": ["aggregate_labels(meter_mongodb_cluster_cursor_avg, sum(csr_type))"], "span": 4, "rowSpan": 2 },
+ { "id": "repl_lag", "title": "Replication Lag (ms)", "type": "line", "unit": "ms", "expressions": ["meter_mongodb_cluster_repl_lag"], "span": 4, "rowSpan": 2 },
+ { "id": "db_data_size", "title": "DB Total Data (GB)", "type": "line", "expressions": ["latest(aggregate_labels(meter_mongodb_cluster_db_data_size, sum(database)))/1024/1024/1024"], "span": 4, "rowSpan": 2 },
+ { "id": "db_index_size", "title": "DB Total Index (GB)", "type": "line", "expressions": ["latest(aggregate_labels(meter_mongodb_cluster_db_index_size, sum(database)))/1024/1024/1024"], "span": 4, "rowSpan": 2 }
+ ],
+ "instance": [
+ { "id": "uptime", "title": "Uptime (days)", "type": "line", "expressions": ["latest(meter_mongodb_node_uptime)/3600/24"], "span": 3, "rowSpan": 2 },
+ { "id": "qps", "title": "QPS", "type": "line", "expressions": ["meter_mongodb_node_qps"], "span": 3, "rowSpan": 2 },
+ { "id": "rs_state", "title": "ReplSet State", "type": "line", "expressions": ["meter_mongodb_node_rs_state"], "span": 3, "rowSpan": 2 },
+ { "id": "conns", "title": "Connections", "type": "line", "expressions": ["meter_mongodb_node_connections"], "span": 3, "rowSpan": 2 },
+ { "id": "cpu", "title": "CPU Usage (%)", "type": "line", "unit": "%", "expressions": ["meter_mongodb_node_cpu_total_percentage"], "span": 4, "rowSpan": 2 },
+ { "id": "mem", "title": "Memory Usage", "type": "line", "expressions": ["meter_mongodb_node_memory_usage"], "span": 4, "rowSpan": 2 },
+ { "id": "mem_free", "title": "Memory Free (GB)", "type": "line", "expressions": ["meter_mongodb_node_memory_free_kb/1024/1024", "meter_mongodb_node_swap_memory_free_kb/1024/1024"], "expressionLabels": ["mem", "swap"], "span": 4, "rowSpan": 2 },
+ { "id": "disk", "title": "Disk (GB)", "type": "line", "expressions": ["meter_mongodb_node_fs_used_size/1024/1024/1024", "meter_mongodb_node_fs_total_size/1024/1024/1024"], "expressionLabels": ["used", "total"], "span": 4, "rowSpan": 2 },
+ { "id": "network", "title": "Network (KB/s)", "type": "line", "expressions": ["meter_mongodb_node_network_bytes_in/1024", "meter_mongodb_node_network_bytes_out/1024"], "expressionLabels": ["in", "out"], "span": 4, "rowSpan": 2 },
+ { "id": "active_clients", "title": "Active Clients", "type": "line", "expressions": ["meter_mongodb_node_active_total_num", "meter_mongodb_node_active_writer_num", "meter_mongodb_node_active_reader_num"], "expressionLabels": ["total", "writers", "readers"], "span": 4, "rowSpan": 2 },
+ { "id": "doc_qps", "title": "Document QPS", "type": "line", "expressions": ["meter_mongodb_node_document_qps"], "span": 4, "rowSpan": 2 },
+ { "id": "op_qps", "title": "Operation QPS", "type": "line", "expressions": ["meter_mongodb_node_operation_qps"], "span": 4, "rowSpan": 2 },
+ { "id": "op_latency", "title": "Op Latency (µs)", "type": "line", "expressions": ["aggregate_labels(meter_mongodb_node_latency_rate,sum(op_type))/aggregate_labels(meter_mongodb_node_op_rate,sum(op_type))"], "span": 4, "rowSpan": 2 },
+ { "id": "transactions", "title": "Transactions", "type": "line", "expressions": ["meter_mongodb_node_transactions_active", "meter_mongodb_node_transactions_inactive"], "expressionLabels": ["active", "inactive"], "span": 4, "rowSpan": 2 },
+ { "id": "repl_buffer", "title": "Repl Buffer", "type": "line", "expressions": ["meter_mongodb_node_repl_buffer_count", "meter_mongodb_node_repl_buffer_size/1024/1024"], "expressionLabels": ["count", "size (MB)"], "span": 4, "rowSpan": 2 },
+ { "id": "queued_op", "title": "Queued Operations", "type": "line", "expressions": ["meter_mongodb_node_queued_operation"], "span": 4, "rowSpan": 2 }
+ ]
+ }
+}
diff --git a/apps/bff/src/bundled_templates/layers/mysql.json b/apps/bff/src/bundled_templates/layers/mysql.json
new file mode 100644
index 0000000..4ee21fc
--- /dev/null
+++ b/apps/bff/src/bundled_templates/layers/mysql.json
@@ -0,0 +1,228 @@
+{
+ "key": "MYSQL",
+ "alias": "MySQL / MariaDB",
+ "color": "var(--sw-warn)",
+ "documentLink": "https://skywalking.apache.org/docs/main/next/en/setup/backend/backend-mysql-monitoring/",
+ "aliases": {
+ "services": "MySQL clusters",
+ "instances": "Nodes"
+ },
+ "components": {
+ "service": true,
+ "instances": true,
+ "endpoints": false,
+ "topology": false,
+ "traces": false,
+ "logs": false
+ },
+ "layer-header": {
+ "orderBy": "qps",
+ "columns": [
+ { "metric": "qps", "label": "QPS", "mqe": "aggregate_labels(meter_mysql_qps,sum)", "aggregation": "sum" },
+ { "metric": "tps", "label": "TPS", "mqe": "aggregate_labels(meter_mysql_tps,sum)", "aggregation": "sum" },
+ { "metric": "slowQps", "label": "Slow QPS", "mqe": "aggregate_labels(meter_mysql_slow_queries_rate,sum)", "aggregation": "sum" },
+ { "metric": "connErr", "label": "Conn Errors", "mqe": "aggregate_labels(meter_mysql_connection_errors_internal,sum) + aggregate_labels(meter_mysql_connection_errors_max_connections,sum)", "aggregation": "sum" }
+ ]
+ },
+ "overview": {
+ "groups": [
+ {
+ "title": "Throughput",
+ "size": "auto",
+ "metrics": [
+ { "id": "qps", "label": "QPS", "tip": "Queries per second across the cluster.", "mqe": "aggregate_labels(meter_mysql_qps,sum)", "aggregation": "sum" },
+ { "id": "tps", "label": "TPS", "tip": "Transactions per second.", "mqe": "aggregate_labels(meter_mysql_tps,sum)", "aggregation": "sum" },
+ { "id": "slowRate", "label": "Slow Queries/s", "tip": "Slow query rate across the cluster.", "mqe": "aggregate_labels(meter_mysql_slow_queries_rate,sum)", "aggregation": "sum" }
+ ]
+ }
+ ]
+ },
+ "dashboards": {
+ "service": [
+ {
+ "id": "qps",
+ "title": "QPS",
+ "type": "line",
+ "expressions": ["aggregate_labels(meter_mysql_qps,sum)"],
+ "span": 3,
+ "rowSpan": 2
+ },
+ {
+ "id": "tps",
+ "title": "TPS",
+ "type": "line",
+ "expressions": ["aggregate_labels(meter_mysql_tps,sum)"],
+ "span": 3,
+ "rowSpan": 2
+ },
+ {
+ "id": "slow_qps",
+ "title": "Slow Queries / s",
+ "type": "line",
+ "expressions": ["aggregate_labels(meter_mysql_slow_queries_rate,sum)"],
+ "span": 3,
+ "rowSpan": 2
+ },
+ {
+ "id": "conn_errors",
+ "title": "Connection Errors",
+ "tip": "Internal vs max-connection rejects.",
+ "type": "line",
+ "expressions": [
+ "aggregate_labels(meter_mysql_connection_errors_internal,sum)",
+ "aggregate_labels(meter_mysql_connection_errors_max_connections,sum)"
+ ],
+ "expressionLabels": ["internal", "max-connections"],
+ "span": 3,
+ "rowSpan": 2
+ },
+ {
+ "id": "commands",
+ "title": "Commands Trend",
+ "tip": "Rows-affected rate per command (select / insert / update / delete).",
+ "type": "line",
+ "expressions": [
+ "aggregate_labels(meter_mysql_commands_select_rate,sum)",
+ "aggregate_labels(meter_mysql_commands_insert_rate,sum)",
+ "aggregate_labels(meter_mysql_commands_update_rate,sum)",
+ "aggregate_labels(meter_mysql_commands_delete_rate,sum)"
+ ],
+ "expressionLabels": ["select", "insert", "update", "delete"],
+ "span": 6,
+ "rowSpan": 2
+ },
+ {
+ "id": "threads",
+ "title": "Threads",
+ "type": "line",
+ "expressions": [
+ "aggregate_labels(meter_mysql_threads_connected,sum)",
+ "aggregate_labels(meter_mysql_threads_running,sum)",
+ "aggregate_labels(meter_mysql_threads_cached,sum)",
+ "aggregate_labels(meter_mysql_threads_created,sum)"
+ ],
+ "expressionLabels": ["connected", "running", "cached", "created"],
+ "span": 6,
+ "rowSpan": 2
+ },
+ {
+ "id": "slow_statements",
+ "title": "Slow Statements",
+ "tip": "Top 20 slowest captured statements across the cluster.",
+ "type": "record",
+ "unit": "ms",
+ "expressions": ["top_n(top_n_database_statement, 20, des)"],
+ "span": 12,
+ "rowSpan": 4
+ }
+ ],
+ "instance": [
+ {
+ "id": "uptime",
+ "title": "Uptime",
+ "type": "line",
+ "unit": "days",
+ "expressions": ["latest(meter_mysql_instance_uptime)/3600/24"],
+ "span": 3,
+ "rowSpan": 2
+ },
+ {
+ "id": "max_conns",
+ "title": "Max Connections",
+ "type": "line",
+ "expressions": ["latest(meter_mysql_instance_max_connections)"],
+ "span": 3,
+ "rowSpan": 2
+ },
+ {
+ "id": "buffer_pool",
+ "title": "InnoDB Buffer Pool",
+ "type": "line",
+ "unit": "MB",
+ "expressions": ["latest(meter_mysql_instance_innodb_buffer_pool_size)/1024/1024"],
+ "span": 3,
+ "rowSpan": 2
+ },
+ {
+ "id": "thread_cache",
+ "title": "Thread Cache Size",
+ "type": "line",
+ "expressions": ["latest(meter_mysql_instance_thread_cache_size)"],
+ "span": 3,
+ "rowSpan": 2
+ },
+ {
+ "id": "instance_qps_tps",
+ "title": "QPS / TPS",
+ "type": "line",
+ "expressions": [
+ "meter_mysql_instance_qps",
+ "meter_mysql_instance_tps"
+ ],
+ "expressionLabels": ["QPS", "TPS"],
+ "span": 6,
+ "rowSpan": 2
+ },
+ {
+ "id": "instance_slow_queries",
+ "title": "Slow Queries / s",
+ "type": "line",
+ "expressions": ["meter_mysql_instance_slow_queries_rate"],
+ "span": 6,
+ "rowSpan": 2
+ },
+ {
+ "id": "instance_commands",
+ "title": "Commands Trend",
+ "type": "line",
+ "expressions": [
+ "meter_mysql_instance_commands_select_rate",
+ "meter_mysql_instance_commands_insert_rate",
+ "meter_mysql_instance_commands_update_rate",
+ "meter_mysql_instance_commands_delete_rate"
+ ],
+ "expressionLabels": ["select", "insert", "update", "delete"],
+ "span": 6,
+ "rowSpan": 2
+ },
+ {
+ "id": "instance_threads",
+ "title": "Threads",
+ "type": "line",
+ "expressions": [
+ "meter_mysql_instance_threads_connected",
+ "meter_mysql_instance_threads_running",
+ "meter_mysql_instance_threads_cached",
+ "meter_mysql_instance_threads_created"
+ ],
+ "expressionLabels": ["connected", "running", "cached", "created"],
+ "span": 6,
+ "rowSpan": 2
+ },
+ {
+ "id": "instance_connects",
+ "title": "Connects",
+ "type": "line",
+ "expressions": [
+ "meter_mysql_instance_connects_available",
+ "meter_mysql_instance_connects_aborted"
+ ],
+ "expressionLabels": ["available", "aborted"],
+ "span": 6,
+ "rowSpan": 2
+ },
+ {
+ "id": "instance_conn_errors",
+ "title": "Connection Errors",
+ "type": "line",
+ "expressions": [
+ "meter_mysql_instance_connection_errors_internal",
+ "meter_mysql_instance_connection_errors_max_connections"
+ ],
+ "expressionLabels": ["internal", "max-connections"],
+ "span": 6,
+ "rowSpan": 2
+ }
+ ]
+ }
+}
diff --git a/apps/bff/src/bundled_templates/layers/nginx.json b/apps/bff/src/bundled_templates/layers/nginx.json
new file mode 100644
index 0000000..83ef787
--- /dev/null
+++ b/apps/bff/src/bundled_templates/layers/nginx.json
@@ -0,0 +1,76 @@
+{
+ "key": "NGINX",
+ "alias": "Nginx",
+ "color": "var(--sw-ok)",
+ "documentLink": "https://skywalking.apache.org/docs/main/next/en/setup/backend/backend-nginx-monitoring/",
+ "aliases": {
+ "services": "Nginx services",
+ "instances": "Nodes",
+ "endpoints": "Routes"
+ },
+ "components": {
+ "service": true,
+ "instances": true,
+ "endpoints": true,
+ "topology": false,
+ "traces": false,
+ "logs": true
+ },
+ "layer-header": {
+ "orderBy": "rps",
+ "columns": [
+ { "metric": "rps", "label": "RPS", "mqe": "aggregate_labels(meter_nginx_service_http_requests, sum)", "aggregation": "sum" },
+ { "metric": "err5xx", "label": "5xx %", "unit": "%", "mqe": "aggregate_labels(meter_nginx_service_http_5xx_requests_increment, sum)/aggregate_labels(meter_nginx_service_http_requests_increment, sum)*100", "aggregation": "avg" },
+ { "metric": "err4xx", "label": "4xx %", "unit": "%", "mqe": "aggregate_labels(meter_nginx_service_http_4xx_requests_increment, sum)/aggregate_labels(meter_nginx_service_http_requests_increment, sum)*100", "aggregation": "avg" }
+ ]
+ },
+ "overview": {
+ "groups": [
+ {
+ "title": "Errors",
+ "size": "auto",
+ "metrics": [
+ { "id": "err5xx", "label": "5xx Rate", "tip": "Percentage of requests returning 5xx per minute.", "mqe": "aggregate_labels(meter_nginx_service_http_5xx_requests_increment, sum)/aggregate_labels(meter_nginx_service_http_requests_increment, sum)*100", "unit": "%", "aggregation": "avg" },
+ { "id": "err4xx", "label": "4xx Rate", "tip": "Percentage of requests returning 4xx per minute.", "mqe": "aggregate_labels(meter_nginx_service_http_4xx_requests_increment, sum)/aggregate_labels(meter_nginx_service_http_requests_increment, sum)*100", "unit": "%", "aggregation": "avg" }
+ ]
+ },
+ {
+ "title": "Throughput",
+ "size": "square",
+ "metrics": [
+ { "id": "rps", "label": "RPS", "tip": "Requests per second across the Nginx service.", "mqe": "aggregate_labels(meter_nginx_service_http_requests, sum)", "aggregation": "sum" }
+ ]
+ }
+ ]
+ },
+ "dashboards": {
+ "service": [
+ { "id": "rps", "title": "HTTP Request Trend", "type": "line", "expressions": ["aggregate_labels(meter_nginx_service_http_requests, sum)"], "span": 4, "rowSpan": 2 },
+ { "id": "latency", "title": "HTTP Latency", "type": "line", "unit": "ms", "expressions": ["aggregate_labels(meter_nginx_service_http_latency, avg(p))"], "span": 4, "rowSpan": 2 },
+ { "id": "bandwidth", "title": "HTTP Bandwidth", "type": "line", "unit": "KB/s", "expressions": ["aggregate_labels(meter_nginx_service_http_bandwidth, sum(type))/1024"], "span": 4, "rowSpan": 2 },
+ { "id": "conns", "title": "HTTP Connections", "type": "line", "expressions": ["aggregate_labels(meter_nginx_service_http_connections, sum(state))"], "span": 6, "rowSpan": 2 },
+ { "id": "status", "title": "HTTP Status Trend", "type": "line", "expressions": ["aggregate_labels(meter_nginx_service_http_status, sum(status))"], "span": 6, "rowSpan": 2 },
+ { "id": "err4xx", "title": "4xx % / min", "type": "line", "unit": "%", "expressions": ["aggregate_labels(meter_nginx_service_http_4xx_requests_increment, sum)/aggregate_labels(meter_nginx_service_http_requests_increment, sum)*100"], "span": 4, "rowSpan": 2 },
+ { "id": "err5xx", "title": "5xx % / min", "type": "line", "unit": "%", "expressions": ["aggregate_labels(meter_nginx_service_http_5xx_requests_increment, sum)/aggregate_labels(meter_nginx_service_http_requests_increment, sum)*100"], "span": 4, "rowSpan": 2 },
+ { "id": "errlog", "title": "Error Log Count", "type": "line", "expressions": ["aggregate_labels(meter_nginx_service_error_log_count, sum(level))"], "span": 4, "rowSpan": 2 }
+ ],
+ "instance": [
+ { "id": "rps", "title": "HTTP Request Trend", "type": "line", "expressions": ["meter_nginx_instance_http_requests"], "span": 4, "rowSpan": 2 },
+ { "id": "latency", "title": "HTTP Latency", "type": "line", "unit": "ms", "expressions": ["meter_nginx_instance_http_latency"], "span": 4, "rowSpan": 2 },
+ { "id": "bandwidth", "title": "HTTP Bandwidth", "type": "line", "unit": "KB/s", "expressions": ["meter_nginx_instance_http_bandwidth/1024"], "span": 4, "rowSpan": 2 },
+ { "id": "conns", "title": "HTTP Connections", "type": "line", "expressions": ["meter_nginx_instance_http_connections"], "span": 6, "rowSpan": 2 },
+ { "id": "status", "title": "HTTP Status Trend", "type": "line", "expressions": ["meter_nginx_instance_http_status"], "span": 6, "rowSpan": 2 },
+ { "id": "err4xx", "title": "4xx % / min", "type": "line", "unit": "%", "expressions": ["(meter_nginx_instance_http_4xx_requests_increment/meter_nginx_instance_http_requests_increment)*100"], "span": 4, "rowSpan": 2 },
+ { "id": "err5xx", "title": "5xx % / min", "type": "line", "unit": "%", "expressions": ["(meter_nginx_instance_http_5xx_requests_increment/meter_nginx_instance_http_requests_increment)*100"], "span": 4, "rowSpan": 2 },
+ { "id": "errlog", "title": "Error Log Count", "type": "line", "expressions": ["meter_nginx_instance_error_log_count"], "span": 4, "rowSpan": 2 }
+ ],
+ "endpoint": [
+ { "id": "rps", "title": "HTTP Request Trend", "type": "line", "expressions": ["meter_nginx_endpoint_http_requests"], "span": 4, "rowSpan": 2 },
+ { "id": "latency", "title": "HTTP Latency", "type": "line", "unit": "ms", "expressions": ["meter_nginx_endpoint_http_latency"], "span": 4, "rowSpan": 2 },
+ { "id": "bandwidth", "title": "HTTP Bandwidth", "type": "line", "unit": "KB", "expressions": ["meter_nginx_endpoint_http_bandwidth/1024"], "span": 4, "rowSpan": 2 },
+ { "id": "status", "title": "HTTP Status Trend", "type": "line", "expressions": ["meter_nginx_endpoint_http_status"], "span": 6, "rowSpan": 2 },
+ { "id": "err4xx", "title": "4xx % / min", "type": "line", "unit": "%", "expressions": ["(meter_nginx_endpoint_http_4xx_requests_increment/meter_nginx_endpoint_http_requests_increment)*100"], "span": 3, "rowSpan": 2 },
+ { "id": "err5xx", "title": "5xx % / min", "type": "line", "unit": "%", "expressions": ["(meter_nginx_endpoint_http_5xx_requests_increment/meter_nginx_endpoint_http_requests_increment)*100"], "span": 3, "rowSpan": 2 }
+ ]
+ }
+}
diff --git a/apps/bff/src/bundled_templates/layers/os_linux.json b/apps/bff/src/bundled_templates/layers/os_linux.json
new file mode 100644
index 0000000..2fbc103
--- /dev/null
+++ b/apps/bff/src/bundled_templates/layers/os_linux.json
@@ -0,0 +1,38 @@
+{
+ "key": "OS_LINUX",
+ "alias": "Linux",
+ "color": "var(--sw-fg-1)",
+ "documentLink": "https://skywalking.apache.org/docs/main/next/en/setup/backend/backend-vm-monitoring/",
+ "aliases": { "services": "Hosts" },
+ "components": { "service": true, "instances": false, "endpoints": false, "topology": false, "traces": false, "logs": false },
+ "layer-header": {
+ "orderBy": "cpu",
+ "columns": [
+ { "metric": "cpu", "label": "CPU %", "unit": "%", "mqe": "meter_vm_cpu_total_percentage", "aggregation": "avg" },
+ { "metric": "memUsed", "label": "Memory MB", "unit": "MB", "mqe": "meter_vm_memory_used/1024/1024", "aggregation": "avg" },
+ { "metric": "load1", "label": "Load 1m", "mqe": "meter_vm_cpu_load1/100", "aggregation": "avg" },
+ { "metric": "fs", "label": "FS %", "unit": "%", "mqe": "meter_vm_filesystem_percentage", "aggregation": "avg" }
+ ]
+ },
+ "overview": {
+ "groups": [
+ { "title": "Resource", "size": "auto", "metrics": [
+ { "id": "cpu", "label": "CPU %", "tip": "Total CPU utilization.", "mqe": "meter_vm_cpu_total_percentage", "unit": "%", "aggregation": "avg" },
+ { "id": "memUsage", "label": "Mem %", "tip": "Memory used / total.", "mqe": "avg(meter_vm_memory_swap_percentage)", "unit": "%", "aggregation": "avg" }
+ ]}
+ ]
+ },
+ "dashboards": {
+ "service": [
+ { "id": "cpu", "title": "CPU Average Used (%)", "type": "line", "unit": "%", "expressions": ["meter_vm_cpu_average_used"], "span": 4, "rowSpan": 2 },
+ { "id": "cpu_load", "title": "CPU Load", "type": "line", "expressions": ["meter_vm_cpu_load1/100", "meter_vm_cpu_load5/100", "meter_vm_cpu_load15/100"], "expressionLabels": ["1m", "5m", "15m"], "span": 4, "rowSpan": 2 },
+ { "id": "filefd", "title": "File FD Allocated", "type": "line", "expressions": ["meter_vm_filefd_allocated"], "span": 4, "rowSpan": 2 },
+ { "id": "memory", "title": "Memory RAM (MB)", "type": "line", "unit": "MB", "expressions": ["meter_vm_memory_used/1024/1024", "meter_vm_memory_total/1024/1024", "meter_vm_memory_available/1024/1024", "meter_vm_memory_buff_cache/1024/1024"], "expressionLabels": ["used", "total", "available", "buff/cache"], "span": 6, "rowSpan": 2 },
+ { "id": "swap", "title": "Memory Swap (MB)", "type": "line", "unit": "MB", "expressions": ["meter_vm_memory_swap_free/1024/1024", "meter_vm_memory_swap_total/1024/1024"], "expressionLabels": ["free", "total"], "span": 6, "rowSpan": 2 },
+ { "id": "network_bandwidth", "title": "Network Bandwidth (KB/s)", "type": "line", "unit": "KB/s", "expressions": ["meter_vm_network_receive/1024", "meter_vm_network_transmit/1024"], "expressionLabels": ["receive", "transmit"], "span": 6, "rowSpan": 2 },
+ { "id": "disk", "title": "Disk R/W (KB/s)", "type": "line", "unit": "KB/s", "expressions": ["meter_vm_disk_read/1024", "meter_vm_disk_written/1024"], "expressionLabels": ["read", "written"], "span": 6, "rowSpan": 2 },
+ { "id": "fs", "title": "Filesystem Usage (%)", "type": "line", "unit": "%", "expressions": ["meter_vm_filesystem_percentage"], "span": 6, "rowSpan": 2 },
+ { "id": "network_status", "title": "Network Status", "type": "line", "expressions": ["meter_vm_tcp_curr_estab", "meter_vm_tcp_tw", "meter_vm_tcp_alloc", "meter_vm_sockets_used", "meter_vm_udp_inuse"], "expressionLabels": ["tcp estab", "tcp time-wait", "tcp alloc", "sockets used", "udp in-use"], "span": 6, "rowSpan": 2 }
+ ]
+ }
+}
diff --git a/apps/bff/src/bundled_templates/layers/os_windows.json b/apps/bff/src/bundled_templates/layers/os_windows.json
new file mode 100644
index 0000000..4ee43fb
--- /dev/null
+++ b/apps/bff/src/bundled_templates/layers/os_windows.json
@@ -0,0 +1,33 @@
+{
+ "key": "OS_WINDOWS",
+ "alias": "Windows",
+ "color": "var(--sw-fg-1)",
+ "documentLink": "https://skywalking.apache.org/docs/main/next/en/setup/backend/backend-win-monitoring/",
+ "aliases": { "services": "Hosts" },
+ "components": { "service": true, "instances": false, "endpoints": false, "topology": false, "traces": false, "logs": false },
+ "layer-header": {
+ "orderBy": "cpu",
+ "columns": [
+ { "metric": "cpu", "label": "CPU %", "unit": "%", "mqe": "meter_win_cpu_total_percentage", "aggregation": "avg" },
+ { "metric": "memUsed", "label": "Memory MB", "unit": "MB", "mqe": "meter_win_memory_used/1024/1024", "aggregation": "avg" },
+ { "metric": "vmem", "label": "VMem %", "unit": "%", "mqe": "avg(meter_win_memory_virtual_memory_percentage)", "aggregation": "avg" }
+ ]
+ },
+ "overview": {
+ "groups": [
+ { "title": "Resource", "size": "auto", "metrics": [
+ { "id": "cpu", "label": "CPU %", "tip": "Total CPU utilization.", "mqe": "meter_win_cpu_total_percentage", "unit": "%", "aggregation": "avg" },
+ { "id": "vmem", "label": "VMem %", "tip": "Virtual memory utilization.", "mqe": "avg(meter_win_memory_virtual_memory_percentage)", "unit": "%", "aggregation": "avg" }
+ ]}
+ ]
+ },
+ "dashboards": {
+ "service": [
+ { "id": "cpu", "title": "CPU Average Used (%)", "type": "line", "unit": "%", "expressions": ["meter_win_cpu_average_used"], "span": 4, "rowSpan": 2 },
+ { "id": "memory", "title": "Memory RAM (MB)", "type": "line", "unit": "MB", "expressions": ["meter_win_memory_used/1024/1024", "meter_win_memory_total/1024/1024", "meter_win_memory_available/1024/1024"], "expressionLabels": ["used", "total", "available"], "span": 4, "rowSpan": 2 },
+ { "id": "virtual_memory", "title": "Virtual Memory (MB)", "type": "line", "unit": "MB", "expressions": ["meter_win_memory_virtual_memory_free/1024/1024", "meter_win_memory_virtual_memory_total/1024/1024"], "expressionLabels": ["free", "total"], "span": 4, "rowSpan": 2 },
+ { "id": "network_bandwidth", "title": "Network Bandwidth (KB/s)", "type": "line", "unit": "KB/s", "expressions": ["meter_win_network_receive/1024", "meter_win_network_transmit/1024"], "expressionLabels": ["receive", "transmit"], "span": 6, "rowSpan": 2 },
+ { "id": "disk", "title": "Disk R/W (KB/s)", "type": "line", "unit": "KB/s", "expressions": ["meter_win_disk_read/1024", "meter_win_disk_written/1024"], "expressionLabels": ["read", "written"], "span": 6, "rowSpan": 2 }
+ ]
+ }
+}
diff --git a/apps/bff/src/bundled_templates/layers/postgresql.json b/apps/bff/src/bundled_templates/layers/postgresql.json
new file mode 100644
index 0000000..c6d1dee
--- /dev/null
+++ b/apps/bff/src/bundled_templates/layers/postgresql.json
@@ -0,0 +1,274 @@
+{
+ "key": "POSTGRESQL",
+ "alias": "PostgreSQL",
+ "color": "var(--sw-warn)",
+ "documentLink": "https://skywalking.apache.org/docs/main/next/en/setup/backend/backend-postgresql-monitoring/",
+ "aliases": {
+ "services": "PostgreSQL clusters",
+ "instances": "Nodes"
+ },
+ "components": {
+ "service": true,
+ "instances": true,
+ "endpoints": false,
+ "topology": false,
+ "traces": false,
+ "logs": false
+ },
+ "layer-header": {
+ "orderBy": "fetched",
+ "columns": [
+ { "metric": "fetched", "label": "Fetched / s", "mqe": "aggregate_labels(meter_pg_fetched_rows_rate,sum)", "aggregation": "sum" },
+ { "metric": "inserted", "label": "Inserted / s", "mqe": "aggregate_labels(meter_pg_inserted_rows_rate,sum)", "aggregation": "sum" },
+ { "metric": "cacheHit", "label": "Cache Hit", "unit": "%", "mqe": "aggregate_labels(meter_pg_cache_hit_rate,avg)", "aggregation": "avg" },
+ { "metric": "deadlocks", "label": "Deadlocks", "mqe": "aggregate_labels(meter_pg_deadlocks_rate,sum)", "aggregation": "sum" }
+ ]
+ },
+ "overview": {
+ "groups": [
+ {
+ "title": "Health",
+ "size": "auto",
+ "metrics": [
+ { "id": "cacheHit", "label": "Cache Hit", "tip": "Buffer-cache hit ratio across the cluster.", "mqe": "aggregate_labels(meter_pg_cache_hit_rate,avg)", "unit": "%", "aggregation": "avg" },
+ { "id": "deadlocks", "label": "Deadlocks/s", "tip": "Detected deadlocks per second.", "mqe": "aggregate_labels(meter_pg_deadlocks_rate,sum)", "aggregation": "sum" },
+ { "id": "conflicts", "label": "Conflicts/s", "tip": "Recovery conflicts per second.", "mqe": "aggregate_labels(meter_pg_conflicts_rate,sum)", "aggregation": "sum" }
+ ]
+ },
+ {
+ "title": "Throughput",
+ "size": "square",
+ "metrics": [
+ { "id": "fetched", "label": "Fetched /s", "tip": "Rows fetched per second.", "mqe": "aggregate_labels(meter_pg_fetched_rows_rate,sum)", "aggregation": "sum" }
+ ]
+ }
+ ]
+ },
+ "dashboards": {
+ "service": [
+ {
+ "id": "rows_fetched",
+ "title": "Fetched Rows / s",
+ "type": "line",
+ "expressions": ["aggregate_labels(meter_pg_fetched_rows_rate,sum)"],
+ "span": 3,
+ "rowSpan": 2
+ },
+ {
+ "id": "rows_inserted",
+ "title": "Inserted Rows / s",
+ "type": "line",
+ "expressions": ["aggregate_labels(meter_pg_inserted_rows_rate,sum)"],
+ "span": 3,
+ "rowSpan": 2
+ },
+ {
+ "id": "rows_updated",
+ "title": "Updated Rows / s",
+ "type": "line",
+ "expressions": ["aggregate_labels(meter_pg_updated_rows_rate,sum)"],
+ "span": 3,
+ "rowSpan": 2
+ },
+ {
+ "id": "rows_deleted",
+ "title": "Deleted Rows / s",
+ "type": "line",
+ "expressions": ["aggregate_labels(meter_pg_deleted_rows_rate,sum)"],
+ "span": 3,
+ "rowSpan": 2
+ },
+ {
+ "id": "rows_returned",
+ "title": "Returned Rows / s",
+ "type": "line",
+ "expressions": ["aggregate_labels(meter_pg_returned_rows_rate,sum)"],
+ "span": 4,
+ "rowSpan": 2
+ },
+ {
+ "id": "temp_files",
+ "title": "Temporary Files / s",
+ "type": "line",
+ "expressions": ["aggregate_labels(meter_pg_temporary_files_rate,sum)"],
+ "span": 4,
+ "rowSpan": 2
+ },
+ {
+ "id": "cache_hit",
+ "title": "Cache Hit Rate",
+ "type": "line",
+ "unit": "%",
+ "expressions": ["aggregate_labels(meter_pg_cache_hit_rate,avg)"],
+ "span": 4,
+ "rowSpan": 2
+ },
+ {
+ "id": "txn_rates",
+ "title": "Transactions / s",
+ "tip": "Committed vs rolled-back.",
+ "type": "line",
+ "expressions": [
+ "aggregate_labels(meter_pg_committed_transactions_rate,sum)",
+ "aggregate_labels(meter_pg_rolled_back_transactions_rate,sum)"
+ ],
+ "expressionLabels": ["committed", "rolled-back"],
+ "span": 6,
+ "rowSpan": 2
+ },
+ {
+ "id": "conflicts_deadlocks",
+ "title": "Conflicts + Deadlocks / s",
+ "type": "line",
+ "expressions": [
+ "aggregate_labels(meter_pg_conflicts_rate,sum)",
+ "aggregate_labels(meter_pg_deadlocks_rate,sum)"
+ ],
+ "expressionLabels": ["conflicts", "deadlocks"],
+ "span": 6,
+ "rowSpan": 2
+ },
+ {
+ "id": "sessions",
+ "title": "Sessions",
+ "type": "line",
+ "expressions": [
+ "aggregate_labels(meter_pg_active_sessions,sum)",
+ "aggregate_labels(meter_pg_idle_sessions,sum)",
+ "aggregate_labels(meter_pg_locks_count,sum)"
+ ],
+ "expressionLabels": ["active", "idle", "locks"],
+ "span": 6,
+ "rowSpan": 2
+ },
+ {
+ "id": "buffers",
+ "title": "Buffers / s",
+ "tip": "Checkpoint / clean / fsync / alloc / backend buffer activity.",
+ "type": "line",
+ "expressions": [
+ "aggregate_labels(meter_pg_buffers_checkpoint,sum)",
+ "aggregate_labels(meter_pg_buffers_clean,sum)",
+ "aggregate_labels(meter_pg_buffers_backend_fsync,sum)",
+ "aggregate_labels(meter_pg_buffers_alloc,sum)",
+ "aggregate_labels(meter_pg_buffers_backend,sum)"
+ ],
+ "expressionLabels": ["checkpoint", "clean", "backend fsync", "alloc", "backend"],
+ "span": 6,
+ "rowSpan": 2
+ },
+ {
+ "id": "checkpoint_stats",
+ "title": "Checkpoint Stats / s",
+ "type": "line",
+ "expressions": [
+ "aggregate_labels(meter_pg_checkpoints_timed_rate,sum)",
+ "aggregate_labels(meter_pg_checkpoint_req_rate,sum)",
+ "aggregate_labels(meter_pg_checkpoint_write_time_rate,sum)",
+ "aggregate_labels(meter_pg_checkpoint_sync_time_rate,sum)"
+ ],
+ "expressionLabels": ["timed", "requested", "write time", "sync time"],
+ "span": 12,
+ "rowSpan": 2
+ },
+ {
+ "id": "slow_statements",
+ "title": "Slow Statements",
+ "tip": "Top 20 slowest captured statements.",
+ "type": "record",
+ "unit": "ms",
+ "expressions": ["top_n(top_n_database_statement, 20, des)"],
+ "span": 12,
+ "rowSpan": 4
+ }
+ ],
+ "instance": [
+ {
+ "id": "shared_buffers",
+ "title": "Shared Buffers",
+ "type": "line",
+ "unit": "MB",
+ "expressions": ["latest(meter_pg_instance_shared_buffers)/1024/1024"],
+ "span": 3,
+ "rowSpan": 2
+ },
+ {
+ "id": "effective_cache",
+ "title": "Effective Cache",
+ "type": "line",
+ "unit": "GB",
+ "expressions": ["latest(meter_pg_instance_effective_cache)/1024/1024/1024"],
+ "span": 3,
+ "rowSpan": 2
+ },
+ {
+ "id": "work_mem",
+ "title": "Work Mem",
+ "type": "line",
+ "unit": "MB",
+ "expressions": ["latest(meter_pg_instance_work_mem)/1024/1024"],
+ "span": 3,
+ "rowSpan": 2
+ },
+ {
+ "id": "max_wal_size",
+ "title": "Max WAL Size",
+ "type": "line",
+ "unit": "GB",
+ "expressions": ["latest(meter_pg_instance_max_wal_size)/1024/1024/1024"],
+ "span": 3,
+ "rowSpan": 2
+ },
+ {
+ "id": "instance_fetched",
+ "title": "Fetched Rows / s",
+ "type": "line",
+ "expressions": ["meter_pg_instance_fetched_rows_rate"],
+ "span": 4,
+ "rowSpan": 2
+ },
+ {
+ "id": "instance_inserted",
+ "title": "Inserted Rows / s",
+ "type": "line",
+ "expressions": ["meter_pg_instance_inserted_rows_rate"],
+ "span": 4,
+ "rowSpan": 2
+ },
+ {
+ "id": "instance_cache_hit",
+ "title": "Cache Hit Rate",
+ "type": "line",
+ "unit": "%",
+ "expressions": ["meter_pg_instance_cache_hit_rate"],
+ "span": 4,
+ "rowSpan": 2
+ },
+ {
+ "id": "instance_sessions",
+ "title": "Sessions",
+ "type": "line",
+ "expressions": [
+ "meter_pg_instance_active_sessions",
+ "meter_pg_instance_idle_sessions",
+ "meter_pg_instance_locks_count"
+ ],
+ "expressionLabels": ["active", "idle", "locks"],
+ "span": 6,
+ "rowSpan": 2
+ },
+ {
+ "id": "instance_conflicts",
+ "title": "Conflicts + Deadlocks / s",
+ "type": "line",
+ "expressions": [
+ "meter_pg_instance_conflicts_rate",
+ "meter_pg_instance_deadlocks_rate"
+ ],
+ "expressionLabels": ["conflicts", "deadlocks"],
+ "span": 6,
+ "rowSpan": 2
+ }
+ ]
+ }
+}
diff --git a/apps/bff/src/bundled_templates/layers/pulsar.json b/apps/bff/src/bundled_templates/layers/pulsar.json
new file mode 100644
index 0000000..9f4ef4d
--- /dev/null
+++ b/apps/bff/src/bundled_templates/layers/pulsar.json
@@ -0,0 +1,50 @@
+{
+ "key": "PULSAR",
+ "alias": "Pulsar",
+ "color": "var(--sw-purple)",
+ "documentLink": "https://skywalking.apache.org/docs/main/next/en/setup/backend/backend-pulsar-monitoring/",
+ "aliases": { "services": "Pulsar clusters", "instances": "Brokers" },
+ "components": { "service": true, "instances": true, "endpoints": false, "topology": false, "traces": false, "logs": false },
+ "layer-header": {
+ "orderBy": "topics",
+ "columns": [
+ { "metric": "topics", "label": "Topics", "mqe": "aggregate_labels(meter_pulsar_total_topics,sum)", "aggregation": "sum" },
+ { "metric": "subs", "label": "Subscriptions", "mqe": "aggregate_labels(meter_pulsar_total_subscriptions,sum)", "aggregation": "sum" },
+ { "metric": "rateIn", "label": "Msg In", "mqe": "aggregate_labels(meter_pulsar_message_rate_in,sum)", "aggregation": "sum" },
+ { "metric": "rateOut", "label": "Msg Out", "mqe": "aggregate_labels(meter_pulsar_message_rate_out,sum)", "aggregation": "sum" }
+ ]
+ },
+ "overview": {
+ "groups": [
+ { "title": "Throughput", "size": "auto", "metrics": [
+ { "id": "rateIn", "label": "Msg In/s", "tip": "Messages produced per second.", "mqe": "aggregate_labels(meter_pulsar_message_rate_in,sum)", "aggregation": "sum" },
+ { "id": "rateOut", "label": "Msg Out/s", "tip": "Messages consumed per second.", "mqe": "aggregate_labels(meter_pulsar_message_rate_out,sum)", "aggregation": "sum" },
+ { "id": "storage", "label": "Storage MB", "tip": "Total managed-ledger storage size.", "mqe": "aggregate_labels(meter_pulsar_storage_size,sum)/1024/1024", "aggregation": "sum" }
+ ]}
+ ]
+ },
+ "dashboards": {
+ "service": [
+ { "id": "topics", "title": "Total Topics", "type": "line", "expressions": ["aggregate_labels(meter_pulsar_total_topics,sum)"], "span": 3, "rowSpan": 2 },
+ { "id": "subs", "title": "Subscriptions", "type": "line", "expressions": ["aggregate_labels(meter_pulsar_total_subscriptions,sum)"], "span": 3, "rowSpan": 2 },
+ { "id": "producers", "title": "Producers", "type": "line", "expressions": ["aggregate_labels(meter_pulsar_total_producers,sum)"], "span": 3, "rowSpan": 2 },
+ { "id": "consumers", "title": "Consumers", "type": "line", "expressions": ["aggregate_labels(meter_pulsar_total_consumers,sum)"], "span": 3, "rowSpan": 2 },
+ { "id": "msg_rate", "title": "Message Rate", "type": "line", "expressions": ["aggregate_labels(meter_pulsar_message_rate_in,sum)", "aggregate_labels(meter_pulsar_message_rate_out,sum)"], "expressionLabels": ["in", "out"], "span": 6, "rowSpan": 2 },
+ { "id": "throughput", "title": "Throughput", "type": "line", "expressions": ["aggregate_labels(meter_pulsar_throughput_in,sum)", "aggregate_labels(meter_pulsar_throughput_out,sum)"], "expressionLabels": ["in", "out"], "span": 6, "rowSpan": 2 },
+ { "id": "storage_rw", "title": "Storage Read/Write Rate", "type": "line", "expressions": ["aggregate_labels(meter_pulsar_storage_read_rate,sum)", "aggregate_labels(meter_pulsar_storage_write_rate,sum)"], "expressionLabels": ["read", "write"], "span": 6, "rowSpan": 2 },
+ { "id": "storage_size", "title": "Storage Size (MB)", "type": "line", "unit": "MB", "expressions": ["aggregate_labels(meter_pulsar_storage_size,sum)/1024/1024", "aggregate_labels(meter_pulsar_storage_logical_size,sum)/1024/1024"], "expressionLabels": ["physical", "logical"], "span": 6, "rowSpan": 2 }
+ ],
+ "instance": [
+ { "id": "active_conns", "title": "Active Connections", "type": "line", "expressions": ["meter_pulsar_broker_active_connections"], "span": 3, "rowSpan": 2 },
+ { "id": "total_conns", "title": "Total Connections", "type": "line", "expressions": ["meter_pulsar_broker_total_connections"], "span": 3, "rowSpan": 2 },
+ { "id": "conn_fail", "title": "Conn Create Fail", "type": "line", "expressions": ["meter_pulsar_broker_connection_create_fail_count"], "span": 3, "rowSpan": 2 },
+ { "id": "conn_success", "title": "Conn Create Success", "type": "line", "expressions": ["meter_pulsar_broker_connection_create_success_count"], "span": 3, "rowSpan": 2 },
+ { "id": "conn_closed", "title": "Connection Closed", "type": "line", "expressions": ["meter_pulsar_broker_connection_closed_total_count"], "span": 4, "rowSpan": 2 },
+ { "id": "jvm_buffer", "title": "JVM Buffer Pool (MB)", "type": "line", "unit": "MB", "expressions": ["meter_pulsar_broker_jvm_buffer_pool_used_bytes/1024/1024"], "span": 4, "rowSpan": 2 },
+ { "id": "jvm_pool", "title": "JVM Memory Pool Used (MB)", "type": "line", "unit": "MB", "expressions": ["meter_pulsar_broker_jvm_memory_pool_used/1024/1024"], "span": 4, "rowSpan": 2 },
+ { "id": "jvm_mem", "title": "JVM Memory (MB)", "type": "line", "unit": "MB", "expressions": ["meter_pulsar_broker_jvm_memory_used/1024/1024", "meter_pulsar_broker_jvm_memory_committed/1024/1024", "meter_pulsar_broker_jvm_memory_init/1024/1024"], "expressionLabels": ["used", "committed", "init"], "span": 6, "rowSpan": 2 },
+ { "id": "jvm_threads", "title": "JVM Threads", "type": "line", "expressions": ["meter_pulsar_broker_jvm_threads_current", "meter_pulsar_broker_jvm_threads_daemon", "meter_pulsar_broker_jvm_threads_peak", "meter_pulsar_broker_jvm_threads_deadlocked"], "expressionLabels": ["current", "daemon", "peak", "deadlocked"], "span": 6, "rowSpan": 2 },
+ { "id": "gc", "title": "GC", "type": "line", "expressions": ["meter_pulsar_broker_jvm_gc_collection_seconds_sum", "meter_pulsar_broker_jvm_gc_collection_seconds_count"], "expressionLabels": ["seconds", "count"], "expressionAxes": [0, 1], "span": 6, "rowSpan": 2 }
+ ]
+ }
+}
diff --git a/apps/bff/src/bundled_templates/layers/rabbitmq.json b/apps/bff/src/bundled_templates/layers/rabbitmq.json
new file mode 100644
index 0000000..8849f55
--- /dev/null
+++ b/apps/bff/src/bundled_templates/layers/rabbitmq.json
@@ -0,0 +1,52 @@
+{
+ "key": "RABBITMQ",
+ "alias": "RabbitMQ",
+ "color": "var(--sw-warn)",
+ "documentLink": "https://skywalking.apache.org/docs/main/next/en/setup/backend/backend-rabbitmq-monitoring/",
+ "aliases": { "services": "RabbitMQ clusters", "instances": "Nodes" },
+ "components": { "service": true, "instances": true, "endpoints": false, "topology": false, "traces": false, "logs": false },
+ "layer-header": {
+ "orderBy": "queues",
+ "columns": [
+ { "metric": "queues", "label": "Queues", "mqe": "aggregate_labels(meter_rabbitmq_queues,sum)", "aggregation": "sum" },
+ { "metric": "channels", "label": "Channels", "mqe": "aggregate_labels(meter_rabbitmq_channels,sum)", "aggregation": "sum" },
+ { "metric": "conns", "label": "Connections", "mqe": "aggregate_labels(meter_rabbitmq_connections,sum)", "aggregation": "sum" },
+ { "metric": "unconfirmed", "label": "Unconfirmed", "mqe": "aggregate_labels(meter_rabbitmq_messages_unconfirmed,sum)", "aggregation": "sum" }
+ ]
+ },
+ "overview": {
+ "groups": [
+ { "title": "Throughput", "size": "auto", "metrics": [
+ { "id": "published", "label": "Published / min", "tip": "Messages published per minute.", "mqe": "aggregate_labels(meter_rabbitmq_messages_published,sum)", "aggregation": "sum" },
+ { "id": "unconfirmed", "label": "Unconfirmed", "tip": "Messages awaiting publisher confirm.", "mqe": "aggregate_labels(meter_rabbitmq_messages_unconfirmed,sum)", "aggregation": "sum" },
+ { "id": "unroutable", "label": "Unroutable dropped", "tip": "Messages dropped because no exchange route matched.", "mqe": "meter_rabbitmq_messages_unroutable_dropped", "aggregation": "sum" }
+ ]}
+ ]
+ },
+ "dashboards": {
+ "service": [
+ { "id": "mem_available", "title": "Memory Available Before Block (MB)", "type": "line", "unit": "MB", "expressions": ["meter_rabbitmq_memory_available_before_publisher_blocked/1024/1024"], "span": 4, "rowSpan": 2 },
+ { "id": "disk_available", "title": "Disk Available Before Block (GB)", "type": "line", "unit": "GB", "expressions": ["meter_rabbitmq_disk_space_available_before_publisher_blocked/1024/1024/1024"], "span": 4, "rowSpan": 2 },
+ { "id": "fds", "title": "File Descriptors + Sockets", "type": "line", "expressions": ["meter_rabbitmq_file_descriptors_available", "meter_rabbitmq_tcp_socket_available"], "expressionLabels": ["fds", "sockets"], "span": 4, "rowSpan": 2 },
+ { "id": "messages_ready", "title": "Ready Messages", "type": "line", "expressions": ["meter_rabbitmq_message_ready_delivered_consumers"], "span": 6, "rowSpan": 2 },
+ { "id": "messages_pending", "title": "Pending Ack", "type": "line", "expressions": ["meter_rabbitmq_message_unacknowledged_delivered_consumers"], "span": 6, "rowSpan": 2 },
+ { "id": "publish_pipeline", "title": "Publish Pipeline", "type": "line", "expressions": ["meter_rabbitmq_messages_published", "meter_rabbitmq_messages_confirmed", "meter_rabbitmq_messages_routed", "meter_rabbitmq_messages_unconfirmed"], "expressionLabels": ["published", "confirmed", "routed", "unconfirmed"], "span": 6, "rowSpan": 2 },
+ { "id": "unroutable", "title": "Unroutable Messages", "type": "line", "expressions": ["meter_rabbitmq_messages_unroutable_dropped", "meter_rabbitmq_messages_unroutable_returned"], "expressionLabels": ["dropped", "returned"], "span": 6, "rowSpan": 2 },
+ { "id": "queue_totals", "title": "Queues", "type": "line", "expressions": ["meter_rabbitmq_queues", "meter_rabbitmq_queues_declared_total", "meter_rabbitmq_queues_created_total", "meter_rabbitmq_queues_deleted_total"], "expressionLabels": ["total", "declared", "created", "deleted"], "span": 6, "rowSpan": 2 },
+ { "id": "channel_totals", "title": "Channels", "type": "line", "expressions": ["meter_rabbitmq_channels", "meter_rabbitmq_channels_opened_total", "meter_rabbitmq_channels_closed_total"], "expressionLabels": ["total", "opened", "closed"], "span": 6, "rowSpan": 2 },
+ { "id": "conn_totals", "title": "Connections", "type": "line", "expressions": ["meter_rabbitmq_connections", "meter_rabbitmq_connections_opened_total", "meter_rabbitmq_connections_closed_total"], "expressionLabels": ["total", "opened", "closed"], "span": 12, "rowSpan": 2 }
+ ],
+ "instance": [
+ { "id": "ready_msgs", "title": "Ready Messages", "type": "line", "expressions": ["latest(meter_rabbitmq_node_queue_messages_ready)"], "span": 4, "rowSpan": 2 },
+ { "id": "incoming", "title": "Incoming Messages", "type": "line", "expressions": ["latest(meter_rabbitmq_node_incoming_messages)"], "span": 4, "rowSpan": 2 },
+ { "id": "outgoing", "title": "Outgoing Messages", "type": "line", "expressions": ["latest(meter_rabbitmq_node_outgoing_messages_total)"], "span": 4, "rowSpan": 2 },
+ { "id": "unack", "title": "Unacknowledged Messages", "type": "line", "expressions": ["latest(meter_rabbitmq_node_unacknowledged_messages)"], "span": 4, "rowSpan": 2 },
+ { "id": "conns", "title": "Connections / Publishers / Consumers", "type": "line", "expressions": ["latest(meter_rabbitmq_node_connections_total)", "latest(meter_rabbitmq_node_publisher_total)", "latest(meter_rabbitmq_node_consumer_total)"], "expressionLabels": ["connections", "publishers", "consumers"], "span": 4, "rowSpan": 2 },
+ { "id": "channels", "title": "Channels + Queues", "type": "line", "expressions": ["latest(meter_rabbitmq_node_channel_total)", "latest(meter_rabbitmq_node_queue_total)"], "expressionLabels": ["channels", "queues"], "span": 4, "rowSpan": 2 },
+ { "id": "memory_pct", "title": "Allocated Used %", "type": "line", "unit": "%", "expressions": ["latest(meter_rabbitmq_node_allocated_used_percent)/100"], "span": 4, "rowSpan": 2 },
+ { "id": "memory_bytes", "title": "Memory (MB)", "type": "line", "unit": "MB", "expressions": ["meter_rabbitmq_node_allocated_used_bytes/1024/1024", "meter_rabbitmq_node_allocated_unused_bytes/1024/1024", "meter_rabbitmq_node_process_resident_memory_bytes/1024/1024", "latest(meter_rabbitmq_node_allocated_total_bytes)/1024/1024"], "expressionLabels": ["used", "unused", "resident", "total"], "span": 8, "rowSpan": 2 },
+ { "id": "alloc_by_type", "title": "Allocated By Type (MB)", "type": "line", "unit": "MB", "expressions": ["meter_rabbitmq_node_allocated_by_type/1024/1024"], "span": 6, "rowSpan": 2 },
+ { "id": "blocks", "title": "Multi/Single-block Memory (MB)", "type": "line", "unit": "MB", "expressions": ["meter_rabbitmq_node_allocated_multiblock_used/1024/1024", "meter_rabbitmq_node_allocated_multiblock_unused/1024/1024", "meter_rabbitmq_node_allocated_singleblock_used/1024/1024", "meter_rabbitmq_node_allocated_singleblock_unused/1024/1024"], "expressionLabels": ["multi used", "multi unused", "single used", "single unused"], "span": 6, "rowSpan": 2 }
+ ]
+ }
+}
diff --git a/apps/bff/src/bundled_templates/layers/redis.json b/apps/bff/src/bundled_templates/layers/redis.json
new file mode 100644
index 0000000..6b45262
--- /dev/null
+++ b/apps/bff/src/bundled_templates/layers/redis.json
@@ -0,0 +1,55 @@
+{
+ "key": "REDIS",
+ "alias": "Redis",
+ "color": "var(--sw-warn)",
+ "documentLink": "https://skywalking.apache.org/docs/main/next/en/setup/backend/backend-redis-monitoring/",
+ "aliases": { "services": "Redis clusters", "instances": "Nodes" },
+ "components": { "service": true, "instances": true, "endpoints": false, "topology": false, "traces": false, "logs": false },
+ "layer-header": {
+ "orderBy": "cmds",
+ "columns": [
+ { "metric": "cmds", "label": "Commands/s", "mqe": "aggregate_labels(meter_redis_total_commands_rate,sum(cmd))", "aggregation": "sum" },
+ { "metric": "hitRate", "label": "Hit Rate", "unit": "%", "mqe": "aggregate_labels(meter_redis_hit_rate,avg)", "aggregation": "avg" },
+ { "metric": "memUsage", "label": "Memory %", "unit": "%", "mqe": "latest(aggregate_labels(meter_redis_memory_used_bytes,sum)/aggregate_labels(meter_redis_memory_max_bytes,sum))*100", "aggregation": "avg" },
+ { "metric": "conns", "label": "Clients", "mqe": "latest(aggregate_labels(meter_redis_connected_clients,sum))", "aggregation": "sum" }
+ ]
+ },
+ "overview": {
+ "groups": [
+ { "title": "Health", "size": "auto", "metrics": [
+ { "id": "hitRate", "label": "Hit Rate", "tip": "Cache hit ratio. Trending down = thrash.", "mqe": "aggregate_labels(meter_redis_hit_rate,avg)", "unit": "%", "aggregation": "avg" },
+ { "id": "memUsage", "label": "Memory %", "tip": "Used / max memory. >80% = warn.", "mqe": "latest(aggregate_labels(meter_redis_memory_used_bytes,sum)/aggregate_labels(meter_redis_memory_max_bytes,sum))*100", "unit": "%", "aggregation": "avg" }
+ ]},
+ { "title": "Throughput", "size": "square", "metrics": [
+ { "id": "cmds", "label": "Commands/s", "tip": "Total commands per second across the cluster.", "mqe": "aggregate_labels(meter_redis_total_commands_rate,sum(cmd))", "aggregation": "sum" }
+ ]}
+ ]
+ },
+ "dashboards": {
+ "service": [
+ { "id": "uptime", "title": "Uptime (days)", "type": "line", "expressions": ["latest(aggregate_labels(meter_redis_uptime,max))/3600/24"], "span": 3, "rowSpan": 2 },
+ { "id": "clients", "title": "Connected Clients", "type": "line", "expressions": ["latest(aggregate_labels(meter_redis_connected_clients,sum))"], "span": 3, "rowSpan": 2 },
+ { "id": "blocked", "title": "Blocked Clients", "type": "line", "expressions": ["latest(aggregate_labels(meter_redis_blocked_clients,sum))"], "span": 3, "rowSpan": 2 },
+ { "id": "mem_pct", "title": "Memory Usage", "type": "line", "unit": "%", "expressions": ["latest(aggregate_labels(meter_redis_memory_used_bytes,sum)/aggregate_labels(meter_redis_memory_max_bytes,sum))*100"], "span": 3, "rowSpan": 2 },
+ { "id": "commands", "title": "Total Commands / s", "type": "line", "expressions": ["aggregate_labels(meter_redis_total_commands_rate,sum(cmd))"], "span": 4, "rowSpan": 2 },
+ { "id": "hit_rate", "title": "Hit Rate", "type": "line", "unit": "%", "expressions": ["aggregate_labels(meter_redis_hit_rate,avg)"], "span": 4, "rowSpan": 2 },
+ { "id": "avg_cmd_time", "title": "Avg Command Time / s", "type": "line", "expressions": ["aggregate_labels(meter_redis_commands_duration,sum(cmd))/aggregate_labels(meter_redis_commands_total,sum(cmd))"], "span": 4, "rowSpan": 2 },
+ { "id": "net_io", "title": "Net I/O (KB)", "type": "line", "expressions": ["aggregate_labels(meter_redis_net_input_bytes_total,sum)/1024", "aggregate_labels(meter_redis_net_output_bytes_total,sum)/1024"], "expressionLabels": ["in", "out"], "span": 6, "rowSpan": 2 },
+ { "id": "db_keys", "title": "Keys", "type": "line", "expressions": ["aggregate_labels(meter_redis_db_keys,sum)", "aggregate_labels(meter_redis_evicted_keys_total,sum)", "aggregate_labels(meter_redis_expired_keys_total,sum)"], "expressionLabels": ["total", "evicted", "expired"], "span": 6, "rowSpan": 2 },
+ { "id": "slow_cmds", "title": "Slow Commands", "tip": "Top 10 slowest captured commands. Captured by SkyWalking agent at the call-site.", "type": "record", "unit": "ms", "expressions": ["top_n(top_n_database_statement,10,des)"], "span": 12, "rowSpan": 4 }
+ ],
+ "instance": [
+ { "id": "uptime", "title": "Uptime (days)", "type": "line", "expressions": ["latest(meter_redis_instance_uptime)/3600/24"], "span": 3, "rowSpan": 2 },
+ { "id": "clients", "title": "Connected Clients", "type": "line", "expressions": ["latest(meter_redis_instance_connected_clients)"], "span": 3, "rowSpan": 2 },
+ { "id": "blocked", "title": "Blocked Clients", "type": "line", "expressions": ["latest(meter_redis_instance_redis_blocked_clients)"], "span": 3, "rowSpan": 2 },
+ { "id": "max_mem", "title": "Memory Max (MB)", "type": "line", "expressions": ["latest(meter_redis_instance_memory_max_bytes)/1000/1000"], "span": 3, "rowSpan": 2 },
+ { "id": "mem_pct", "title": "Memory Usage (%)", "type": "line", "unit": "%", "expressions": ["meter_redis_instance_memory_usage"], "span": 4, "rowSpan": 2 },
+ { "id": "cmds", "title": "Commands / s", "type": "line", "expressions": ["meter_redis_instance_total_commands_rate"], "span": 4, "rowSpan": 2 },
+ { "id": "hit_rate", "title": "Hit Rate", "type": "line", "unit": "%", "expressions": ["meter_redis_instance_hit_rate"], "span": 4, "rowSpan": 2 },
+ { "id": "net_io", "title": "Net I/O (KB)", "type": "line", "expressions": ["meter_redis_instance_net_input_bytes_total/1024", "meter_redis_instance_net_output_bytes_total/1024"], "expressionLabels": ["in", "out"], "span": 6, "rowSpan": 2 },
+ { "id": "db_keys", "title": "Keys", "type": "line", "expressions": ["meter_redis_instance_db_keys", "meter_redis_instance_evicted_keys_total", "meter_redis_instance_expired_keys_total"], "expressionLabels": ["total", "evicted", "expired"], "span": 6, "rowSpan": 2 },
+ { "id": "cmd_dur", "title": "Total Command Time (s)", "type": "line", "expressions": ["meter_redis_instance_commands_duration_seconds_total_rate"], "span": 6, "rowSpan": 2 },
+ { "id": "avg_cmd_time", "title": "Avg Command Time", "type": "line", "expressions": ["meter_redis_instance_average_time_spent_by_command"], "span": 6, "rowSpan": 2 }
+ ]
+ }
+}
diff --git a/apps/bff/src/bundled_templates/layers/rocketmq.json b/apps/bff/src/bundled_templates/layers/rocketmq.json
new file mode 100644
index 0000000..1454fd1
--- /dev/null
+++ b/apps/bff/src/bundled_templates/layers/rocketmq.json
@@ -0,0 +1,54 @@
+{
+ "key": "ROCKETMQ",
+ "alias": "RocketMQ",
+ "color": "var(--sw-purple)",
+ "documentLink": "https://skywalking.apache.org/docs/main/next/en/setup/backend/backend-rocketmq-monitoring/",
+ "aliases": { "services": "RocketMQ clusters", "instances": "Brokers", "endpoints": "Topics" },
+ "components": { "service": true, "instances": true, "endpoints": true, "topology": false, "traces": false, "logs": false },
+ "layer-header": {
+ "orderBy": "produceTps",
+ "columns": [
+ { "metric": "produceTps", "label": "Produce TPS", "mqe": "meter_rocketmq_cluster_total_producer_tps", "aggregation": "avg" },
+ { "metric": "consumeTps", "label": "Consume TPS", "mqe": "meter_rocketmq_cluster_total_consumer_tps", "aggregation": "avg" },
+ { "metric": "topics", "label": "Topics", "mqe": "latest(meter_rocketmq_cluster_topic_count)", "aggregation": "sum" },
+ { "metric": "brokers", "label": "Brokers", "mqe": "latest(meter_rocketmq_cluster_broker_count)", "aggregation": "sum" }
+ ]
+ },
+ "overview": {
+ "groups": [
+ { "title": "Throughput", "size": "auto", "metrics": [
+ { "id": "produceTps", "label": "Produce TPS", "tip": "Messages produced per second across the cluster.", "mqe": "meter_rocketmq_cluster_total_producer_tps", "aggregation": "avg" },
+ { "id": "consumeTps", "label": "Consume TPS", "tip": "Messages consumed per second.", "mqe": "meter_rocketmq_cluster_total_consumer_tps", "aggregation": "avg" },
+ { "id": "maxLag", "label": "Max Consumer Latency", "tip": "Worst consumer latency across the cluster.", "mqe": "latest(meter_rocketmq_cluster_max_consumer_latency)", "aggregation": "max" }
+ ]}
+ ]
+ },
+ "dashboards": {
+ "service": [
+ { "id": "today_msgs", "title": "Messages Today", "type": "line", "expressions": ["latest(meter_rocketmq_cluster_messages_produced_today)", "latest(meter_rocketmq_cluster_messages_consumed_today)"], "expressionLabels": ["produced", "consumed"], "span": 6, "rowSpan": 2 },
+ { "id": "yesterday_msgs", "title": "Messages Yesterday", "type": "line", "expressions": ["latest(meter_rocketmq_cluster_messages_produced_until_yesterday)", "latest(meter_rocketmq_cluster_messages_consumed_until_yesterday)"], "expressionLabels": ["produced", "consumed"], "span": 6, "rowSpan": 2 },
+ { "id": "tps", "title": "Producer / Consumer TPS", "type": "line", "expressions": ["meter_rocketmq_cluster_total_producer_tps", "meter_rocketmq_cluster_total_consumer_tps"], "expressionLabels": ["producer", "consumer"], "span": 6, "rowSpan": 2 },
+ { "id": "msg_size", "title": "Producer / Consumer Message Size (MB)", "type": "line", "unit": "MB", "expressions": ["meter_rocketmq_cluster_producer_message_size/1024/1024", "meter_rocketmq_cluster_consumer_message_size/1024/1024"], "expressionLabels": ["produced", "consumed"], "span": 6, "rowSpan": 2 },
+ { "id": "consumer_lag", "title": "Max Consumer Latency", "type": "line", "expressions": ["latest(meter_rocketmq_cluster_max_consumer_latency)"], "span": 4, "rowSpan": 2 },
+ { "id": "commitlog_ratio", "title": "CommitLog Disk Ratio (%)", "type": "line", "unit": "%", "expressions": ["meter_rocketmq_cluster_commitLog_disk_ratio", "latest(meter_rocketmq_cluster_max_commitLog_disk_ratio)"], "expressionLabels": ["current", "max"], "span": 4, "rowSpan": 2 },
+ { "id": "queue_wait", "title": "ThreadPool Queue Head Wait (ms)", "type": "line", "unit": "ms", "expressions": ["meter_rocketmq_cluster_pull_threadPool_queue_head_wait_time", "meter_rocketmq_cluster_send_threadPool_queue_head_wait_time"], "expressionLabels": ["pull", "send"], "span": 4, "rowSpan": 2 },
+ { "id": "topics_brokers", "title": "Topics / Brokers", "type": "line", "expressions": ["latest(meter_rocketmq_cluster_topic_count)", "latest(meter_rocketmq_cluster_broker_count)"], "expressionLabels": ["topics", "brokers"], "span": 12, "rowSpan": 2 }
+ ],
+ "instance": [
+ { "id": "produce_tps", "title": "Produce TPS", "type": "line", "expressions": ["meter_rocketmq_broker_produce_tps"], "span": 3, "rowSpan": 2 },
+ { "id": "consume_qps", "title": "Consume QPS", "type": "line", "expressions": ["meter_rocketmq_broker_consume_qps"], "span": 3, "rowSpan": 2 },
+ { "id": "producer_size", "title": "Producer Msg Size (MB)", "type": "line", "unit": "MB", "expressions": ["meter_rocketmq_broker_producer_message_size/1024/1024"], "span": 3, "rowSpan": 2 },
+ { "id": "consumer_size", "title": "Consumer Msg Size (MB)", "type": "line", "unit": "MB", "expressions": ["meter_rocketmq_broker_consumer_message_size/1024/1024"], "span": 3, "rowSpan": 2 }
+ ],
+ "endpoint": [
+ { "id": "tps", "title": "Producer / Consumer Group TPS", "type": "line", "expressions": ["meter_rocketmq_topic_producer_tps", "meter_rocketmq_topic_consumer_group_tps"], "expressionLabels": ["producer", "consumer group"], "span": 6, "rowSpan": 2 },
+ { "id": "msg_size", "title": "Message Size (MB)", "type": "line", "unit": "MB", "expressions": ["meter_rocketmq_topic_producer_message_size/1024/1024", "meter_rocketmq_topic_consumer_message_size/1024/1024"], "expressionLabels": ["produced", "consumed"], "span": 6, "rowSpan": 2 },
+ { "id": "max_msg", "title": "Max Message Size (MB)", "type": "line", "unit": "MB", "expressions": ["latest(meter_rocketmq_topic_max_producer_message_size)/1024/1024", "latest(meter_rocketmq_topic_max_consumer_message_size)/1024/1024"], "expressionLabels": ["producer", "consumer"], "span": 6, "rowSpan": 2 },
+ { "id": "consumer_latency", "title": "Consumer Latency (s)", "type": "line", "unit": "s", "expressions": ["meter_rocketmq_topic_consumer_latency/1000"], "span": 6, "rowSpan": 2 },
+ { "id": "offsets", "title": "Producer / Consumer Offsets", "type": "line", "expressions": ["meter_rocketmq_topic_producer_offset", "meter_rocketmq_topic_consumer_group_offset"], "expressionLabels": ["producer", "consumer group"], "span": 6, "rowSpan": 2 },
+ { "id": "backlog", "title": "Backlogged Messages", "tip": "Producer offset minus consumer-group offset — the topic lag.", "type": "line", "expressions": ["meter_rocketmq_topic_producer_offset-meter_rocketmq_topic_consumer_group_offset"], "span": 6, "rowSpan": 2 },
+ { "id": "consumer_groups", "title": "Consumer Group Count", "type": "line", "expressions": ["latest(meter_rocketmq_topic_consumer_group_count)"], "span": 6, "rowSpan": 2 },
+ { "id": "brokers", "title": "Broker Count", "type": "line", "expressions": ["latest(meter_rocketmq_topic_broker_count)"], "span": 6, "rowSpan": 2 }
+ ]
+ }
+}
diff --git a/apps/bff/src/bundled_templates/layers/so11y_go_agent.json b/apps/bff/src/bundled_templates/layers/so11y_go_agent.json
new file mode 100644
index 0000000..a6b83a7
--- /dev/null
+++ b/apps/bff/src/bundled_templates/layers/so11y_go_agent.json
@@ -0,0 +1,34 @@
+{
+ "key": "SO11Y_GO_AGENT",
+ "alias": "Go Agent (self-obs)",
+ "color": "var(--sw-fg-2)",
+ "documentLink": "https://skywalking.apache.org/docs/main/next/en/setup/service-agent/server-agents/",
+ "aliases": { "services": "Agent services", "instances": "Agents" },
+ "components": { "service": true, "instances": true, "endpoints": false, "topology": false, "traces": false, "logs": false },
+ "layer-header": {
+ "orderBy": "created",
+ "columns": [
+ { "metric": "created", "label": "Tracing/min", "mqe": "aggregate_labels(meter_sw_go_created_tracing_context_count,sum)", "aggregation": "sum" },
+ { "metric": "ignored", "label": "Ignored/min", "mqe": "aggregate_labels(meter_sw_go_created_ignored_context_count,sum)", "aggregation": "sum" }
+ ]
+ },
+ "overview": {
+ "groups": [
+ { "title": "Agent throughput", "size": "auto", "metrics": [
+ { "id": "created", "label": "Tracing/min", "tip": "Tracing contexts created per minute (sum across instances).", "mqe": "aggregate_labels(meter_sw_go_created_tracing_context_count,sum)", "aggregation": "sum" },
+ { "id": "ignored", "label": "Ignored/min", "tip": "Ignored contexts created per minute.", "mqe": "aggregate_labels(meter_sw_go_created_ignored_context_count,sum)", "aggregation": "sum" }
+ ]}
+ ]
+ },
+ "dashboards": {
+ "instance": [
+ { "id": "tracing_created", "title": "Tracing Context Creation / min", "type": "line", "expressions": ["meter_sw_go_created_tracing_context_count"], "span": 6, "rowSpan": 2 },
+ { "id": "tracing_create_complete", "title": "Tracing Created + Finished / min", "type": "line", "expressions": ["aggregate_labels(meter_sw_go_created_tracing_context_count,sum)", "meter_sw_go_finished_tracing_context_count"], "expressionLabels": ["created", "finished"], "span": 6, "rowSpan": 2 },
+ { "id": "ignored_created", "title": "Ignored Context Creation / min", "type": "line", "expressions": ["meter_sw_go_created_ignored_context_count"], "span": 6, "rowSpan": 2 },
+ { "id": "ignored_create_complete", "title": "Ignored Created + Finished / min", "type": "line", "expressions": ["aggregate_labels(meter_sw_go_created_ignored_context_count,sum)", "meter_sw_go_finished_ignored_context_count"], "expressionLabels": ["created", "finished"], "span": 6, "rowSpan": 2 },
+ { "id": "leaked", "title": "Possible Leaked Context / min", "type": "line", "expressions": ["meter_sw_go_possible_leaked_context_count"], "span": 6, "rowSpan": 2 },
+ { "id": "interceptor_err", "title": "Interceptor Error Count / min", "type": "line", "expressions": ["meter_sw_go_interceptor_error_count"], "span": 6, "rowSpan": 2 },
+ { "id": "exec_time", "title": "Tracing Context Execution Time (ms)", "type": "line", "unit": "ms", "expressions": ["relabels(meter_sw_go_tracing_context_execution_time_percentile,p='50,75,90,95,99',p='50,75,90,95,99')/1000000"], "span": 12, "rowSpan": 2 }
+ ]
+ }
+}
diff --git a/apps/bff/src/bundled_templates/layers/so11y_java_agent.json b/apps/bff/src/bundled_templates/layers/so11y_java_agent.json
new file mode 100644
index 0000000..fe3c4c7
--- /dev/null
+++ b/apps/bff/src/bundled_templates/layers/so11y_java_agent.json
@@ -0,0 +1,34 @@
+{
+ "key": "SO11Y_JAVA_AGENT",
+ "alias": "Java Agent (self-obs)",
+ "color": "var(--sw-fg-2)",
+ "documentLink": "https://skywalking.apache.org/docs/main/next/en/setup/service-agent/server-agents/",
+ "aliases": { "services": "Agent services", "instances": "Agents" },
+ "components": { "service": true, "instances": true, "endpoints": false, "topology": false, "traces": false, "logs": false },
+ "layer-header": {
+ "orderBy": "created",
+ "columns": [
+ { "metric": "created", "label": "Tracing/min", "mqe": "aggregate_labels(meter_java_agent_created_tracing_context_count,sum)", "aggregation": "sum" },
+ { "metric": "ignored", "label": "Ignored/min", "mqe": "aggregate_labels(meter_java_agent_created_ignored_context_count,sum)", "aggregation": "sum" }
+ ]
+ },
+ "overview": {
+ "groups": [
+ { "title": "Agent throughput", "size": "auto", "metrics": [
+ { "id": "created", "label": "Tracing/min", "tip": "Tracing contexts created per minute (sum across instances).", "mqe": "aggregate_labels(meter_java_agent_created_tracing_context_count,sum)", "aggregation": "sum" },
+ { "id": "ignored", "label": "Ignored/min", "tip": "Ignored contexts created per minute.", "mqe": "aggregate_labels(meter_java_agent_created_ignored_context_count,sum)", "aggregation": "sum" }
+ ]}
+ ]
+ },
+ "dashboards": {
+ "instance": [
+ { "id": "tracing_created", "title": "Tracing Context Creation / min", "type": "line", "expressions": ["meter_java_agent_created_tracing_context_count"], "span": 6, "rowSpan": 2 },
+ { "id": "tracing_create_complete", "title": "Tracing Created + Finished / min", "type": "line", "expressions": ["aggregate_labels(meter_java_agent_created_tracing_context_count,sum)", "meter_java_agent_finished_tracing_context_count"], "expressionLabels": ["created", "finished"], "span": 6, "rowSpan": 2 },
+ { "id": "ignored_created", "title": "Ignored Context Creation / min", "type": "line", "expressions": ["meter_java_agent_created_ignored_context_count"], "span": 6, "rowSpan": 2 },
+ { "id": "ignored_create_complete", "title": "Ignored Created + Finished / min", "type": "line", "expressions": ["aggregate_labels(meter_java_agent_created_ignored_context_count,sum)", "meter_java_agent_finished_ignored_context_count"], "expressionLabels": ["created", "finished"], "span": 6, "rowSpan": 2 },
+ { "id": "leaked", "title": "Possible Leaked Context / min", "type": "line", "expressions": ["meter_java_agent_possible_leaked_context_count"], "span": 6, "rowSpan": 2 },
+ { "id": "interceptor_err", "title": "Interceptor Error Count / min", "type": "line", "expressions": ["meter_java_agent_interceptor_error_count"], "span": 6, "rowSpan": 2 },
+ { "id": "exec_time", "title": "Tracing Context Execution Time (ms)", "type": "line", "unit": "ms", "expressions": ["relabels(meter_java_agent_tracing_context_execution_time_percentile,p='50,75,90,95,99',p='50,75,90,95,99')/1000000"], "span": 12, "rowSpan": 2 }
+ ]
+ }
+}
diff --git a/apps/bff/src/bundled_templates/layers/so11y_oap.json b/apps/bff/src/bundled_templates/layers/so11y_oap.json
new file mode 100644
index 0000000..7f08d2b
--- /dev/null
+++ b/apps/bff/src/bundled_templates/layers/so11y_oap.json
@@ -0,0 +1,55 @@
+{
+ "key": "SO11Y_OAP",
+ "alias": "OAP (self-obs)",
+ "color": "var(--sw-fg-2)",
+ "documentLink": "https://skywalking.apache.org/docs/main/next/en/setup/backend/backend-setup/",
+ "aliases": { "services": "OAP services", "instances": "OAP nodes" },
+ "components": { "service": true, "instances": true, "endpoints": false, "topology": false, "traces": false, "logs": false },
+ "layer-header": {
+ "orderBy": "cpu",
+ "columns": [
+ { "metric": "cpu", "label": "CPU %", "unit": "%", "mqe": "avg(meter_oap_instance_cpu_percentage)", "aggregation": "avg" },
+ { "metric": "persist", "label": "Persist/min", "mqe": "sum(meter_oap_instance_persistence_execute_count)", "aggregation": "sum" },
+ { "metric": "graphql", "label": "GraphQL/min", "mqe": "sum(meter_oap_instance_graphql_query_count)", "aggregation": "sum" },
+ { "metric": "graphqlErr", "label": "GraphQL Err", "mqe": "sum(meter_oap_instance_graphql_query_error_count)", "aggregation": "sum" }
+ ]
+ },
+ "overview": {
+ "groups": [
+ { "title": "OAP health", "size": "auto", "metrics": [
+ { "id": "cpu", "label": "CPU %", "tip": "OAP process CPU usage averaged across instances.", "mqe": "avg(meter_oap_instance_cpu_percentage)", "unit": "%", "aggregation": "avg" },
+ { "id": "graphqlErr", "label": "GraphQL Err", "tip": "GraphQL query errors per minute summed across instances.", "mqe": "sum(meter_oap_instance_graphql_query_error_count)", "aggregation": "sum" }
+ ]}
+ ]
+ },
+ "dashboards": {
+ "instance": [
+ { "id": "cpu", "title": "CPU (%)", "type": "line", "unit": "%", "expressions": ["meter_oap_instance_cpu_percentage"], "span": 3, "rowSpan": 2 },
+ { "id": "mem", "title": "JVM Memory (MB)", "type": "line", "unit": "MB", "expressions": ["meter_oap_instance_jvm_memory_bytes_used/1024/1024"], "span": 3, "rowSpan": 2 },
+ { "id": "gc_count", "title": "GC Count / min", "type": "line", "expressions": ["meter_oap_instance_jvm_gc_count"], "span": 3, "rowSpan": 2 },
+ { "id": "gc_time", "title": "GC Time (ms / min)", "type": "line", "unit": "ms", "expressions": ["meter_oap_instance_jvm_gc_time"], "span": 3, "rowSpan": 2 },
+ { "id": "buffer_pool", "title": "Buffer Pool (MB)", "type": "line", "unit": "MB", "expressions": ["meter_oap_instance_jvm_buffer_pool_bytes_used/1024/1024"], "span": 3, "rowSpan": 2 },
+ { "id": "threads", "title": "Thread Count", "type": "line", "expressions": ["meter_oap_jvm_thread_live_count", "meter_oap_jvm_thread_peak_count", "meter_oap_jvm_thread_daemon_count"], "expressionLabels": ["live", "peak", "daemon"], "span": 5, "rowSpan": 2 },
+ { "id": "thread_states", "title": "Thread States", "type": "line", "expressions": ["meter_oap_jvm_thread_runnable_count", "meter_oap_jvm_thread_timed_waiting_count", "meter_oap_jvm_thread_blocked_count", "meter_oap_jvm_thread_waiting_count"], "expressionLabels": ["runnable", "timed-waiting", "blocked", "waiting"], "span": 4, "rowSpan": 2 },
+ { "id": "classes", "title": "Class Count", "type": "line", "expressions": ["meter_oap_jvm_class_loaded_count", "meter_oap_jvm_class_total_unloaded_count", "meter_oap_jvm_class_total_loaded_count"], "expressionLabels": ["loaded", "unloaded total", "loaded total"], "span": 12, "rowSpan": 2 },
+ { "id": "aggregation", "title": "Aggregation / min", "type": "line", "expressions": ["meter_oap_instance_metrics_aggregation"], "span": 4, "rowSpan": 2 },
+ { "id": "persistence_counts", "title": "Persistence Counts / min", "type": "line", "expressions": ["meter_oap_instance_persistence_prepare_count", "meter_oap_instance_persistence_execute_count"], "expressionLabels": ["prepare", "execute"], "span": 4, "rowSpan": 2 },
+ { "id": "persistence_cache", "title": "Persistent Cache / min", "type": "line", "expressions": ["meter_oap_instance_metrics_persistent_cache"], "span": 4, "rowSpan": 2 },
+ { "id": "persist_prepare_latency", "title": "Persistence Prepare Latency (ms)", "type": "line", "unit": "ms", "expressions": ["relabels(meter_oap_instance_persistence_prepare_percentile{p='50,75,90,95,99'},p='50,75,90,95,99',percentile='50,75,90,95,99')"], "span": 6, "rowSpan": 2 },
+ { "id": "persist_execute_latency", "title": "Persistence Execute Latency (ms)", "type": "line", "unit": "ms", "expressions": ["relabels(meter_oap_instance_persistence_execute_percentile{p='50,75,90,95,99'},p='50,75,90,95,99',percentile='50,75,90,95,99')"], "span": 6, "rowSpan": 2 },
+ { "id": "graphql_latency", "title": "GraphQL Latency (ms)", "type": "line", "unit": "ms", "expressions": ["relabels(meter_oap_graphql_query_latency_percentile{p='50,75,90,95,99'},p='50,75,90,95,99',percentile='50,75,90,95,99')"], "span": 6, "rowSpan": 2 },
+ { "id": "graphql_count", "title": "GraphQL Query Count", "type": "line", "expressions": ["meter_oap_instance_graphql_query_count", "meter_oap_instance_graphql_query_error_count"], "expressionLabels": ["queries", "errors"], "span": 6, "rowSpan": 2 },
+ { "id": "trace_analysis", "title": "Trace Analysis / min", "type": "line", "expressions": ["meter_oap_instance_trace_count", "meter_oap_instance_trace_analysis_error_count"], "expressionLabels": ["total", "errors"], "span": 6, "rowSpan": 2 },
+ { "id": "trace_latency", "title": "Trace Analysis Latency (ms)", "type": "line", "unit": "ms", "expressions": ["relabels(meter_oap_instance_trace_latency_percentile{p='50,75,90,95,99'},p='50,75,90,95,99',percentile='50,75,90,95,99')"], "span": 6, "rowSpan": 2 },
+ { "id": "mesh_analysis", "title": "Mesh Analysis / min", "type": "line", "expressions": ["meter_oap_instance_mesh_count", "meter_oap_instance_mesh_analysis_error_count"], "expressionLabels": ["total", "errors"], "span": 6, "rowSpan": 2 },
+ { "id": "mesh_latency", "title": "Mesh Analysis Latency (ms)", "type": "line", "unit": "ms", "expressions": ["relabels(meter_oap_instance_mesh_latency_percentile{p='50,75,90,95,99'},p='50,75,90,95,99',percentile='50,75,90,95,99')"], "span": 6, "rowSpan": 2 },
+ { "id": "otel_received", "title": "OTEL Received / s", "type": "line", "expressions": ["meter_oap_otel_metrics_received", "meter_oap_otel_logs_received", "meter_oap_otel_spans_received"], "expressionLabels": ["metrics", "logs", "spans"], "span": 6, "rowSpan": 2 },
+ { "id": "k8s_als", "title": "K8S ALS", "type": "line", "expressions": ["meter_oap_instance_k8s_als_count", "meter_oap_instance_k8s_als_drop", "meter_oap_instance_k8s_als_streams", "meter_oap_instance_k8s_als_error_streams"], "expressionLabels": ["count", "dropped", "streams", "err streams"], "span": 6, "rowSpan": 2 },
+ { "id": "watermark", "title": "Watermark Circuit Breaker", "tip": "Cumulative break + recover counters per listener.", "type": "line", "expressions": ["meter_oap_instance_watermark_circuit_breaker_break_count", "meter_oap_instance_watermark_circuit_breaker_recover_count"], "expressionLabels": ["break", "recover"], "span": 6, "rowSpan": 2 },
+ { "id": "aggr_queue", "title": "Aggregation Queue Usage (%)", "type": "line", "unit": "%", "expressions": ["sort_values(meter_oap_instance_metrics_aggregation_queue_used_per_ten_thousand{level='1'},10,des,avg)/100", "sort_values(meter_oap_instance_metrics_aggregation_queue_used_per_ten_thousand{level='2'},10,des,avg)/100"], "expressionLabels": ["L1", "L2"], "span": 6, "rowSpan": 2 },
+ { "id": "banyandb_latency", "title": "BanyanDB Write Latency (ms)", "type": "line", "unit": "ms", "expressions": ["meter_oap_banyandb_write_latency_percentile{catalog='measure',operation='bulk_write'}", "meter_oap_banyandb_write_latency_percentile{catalog='stream',operation='bulk_write'}", "meter_oap_banyandb_write_latency_percentile{catalog='trace',operation='bulk_write'}", "meter_oap_banyandb_write_latency_percentile{catalog='stream',operation='single_write'}", "meter_oap_banyand [...]
+ { "id": "es_latency", "title": "Elasticsearch Write Latency (ms)", "type": "line", "unit": "ms", "expressions": ["meter_oap_elasticsearch_write_latency_percentile{operation='single_write,single_update,single_delete'}", "meter_oap_elasticsearch_write_latency_percentile{operation='bulk_write'}"], "expressionLabels": ["single", "bulk"], "visibleWhen": "meter_oap_elasticsearch_write_latency_percentile has value", "span": 6, "rowSpan": 2 },
+ { "id": "zipkin_drop", "title": "Zipkin Spans Dropped", "type": "line", "expressions": ["meter_oap_instance_spans_dropped_count"], "span": 6, "rowSpan": 2 }
+ ]
+ }
+}
diff --git a/apps/bff/src/bundled_templates/layers/so11y_satellite.json b/apps/bff/src/bundled_templates/layers/so11y_satellite.json
new file mode 100644
index 0000000..9cd4722
--- /dev/null
+++ b/apps/bff/src/bundled_templates/layers/so11y_satellite.json
@@ -0,0 +1,35 @@
+{
+ "key": "SO11Y_SATELLITE",
+ "alias": "Satellite (self-obs)",
+ "color": "var(--sw-fg-2)",
+ "documentLink": "https://skywalking.apache.org/docs/skywalking-satellite/next/readme/",
+ "aliases": { "services": "Satellite services" },
+ "components": { "service": true, "instances": false, "endpoints": false, "topology": false, "traces": false, "logs": false },
+ "layer-header": {
+ "orderBy": "conns",
+ "columns": [
+ { "metric": "conns", "label": "gRPC Conns", "mqe": "satellite_service_grpc_connect_count", "aggregation": "sum" },
+ { "metric": "cpu", "label": "CPU %", "unit": "%", "mqe": "satellite_service_server_cpu_utilization", "aggregation": "avg" },
+ { "metric": "queueUsed", "label": "Queue Used", "mqe": "satellite_service_queue_used_count", "aggregation": "sum" },
+ { "metric": "recvEvents", "label": "Recv Events", "mqe": "satellite_service_receive_event_count", "aggregation": "sum" }
+ ]
+ },
+ "overview": {
+ "groups": [
+ { "title": "Satellite health", "size": "auto", "metrics": [
+ { "id": "cpu", "label": "CPU %", "tip": "Satellite process CPU.", "mqe": "satellite_service_server_cpu_utilization", "unit": "%", "aggregation": "avg" },
+ { "id": "queueUsed", "label": "Queue Used", "tip": "Outstanding queue items — backlog indicator.", "mqe": "satellite_service_queue_used_count", "aggregation": "sum" }
+ ]}
+ ]
+ },
+ "dashboards": {
+ "service": [
+ { "id": "conns", "title": "Connection Count", "type": "line", "expressions": ["satellite_service_grpc_connect_count"], "span": 4, "rowSpan": 2 },
+ { "id": "cpu", "title": "CPU (%)", "type": "line", "unit": "%", "expressions": ["satellite_service_server_cpu_utilization"], "span": 4, "rowSpan": 2 },
+ { "id": "queue_used", "title": "Queue Used", "type": "line", "expressions": ["satellite_service_queue_used_count"], "span": 4, "rowSpan": 2 },
+ { "id": "receive", "title": "Receive Events", "type": "line", "expressions": ["satellite_service_receive_event_count"], "span": 4, "rowSpan": 2 },
+ { "id": "fetch", "title": "Fetch Events", "type": "line", "expressions": ["satellite_service_fetch_event_count"], "span": 4, "rowSpan": 2 },
+ { "id": "queue_io", "title": "Queue Input / Output", "type": "line", "expressions": ["satellite_service_queue_input_count", "satellite_service_send_event_count"], "expressionLabels": ["input", "output"], "span": 4, "rowSpan": 2 }
+ ]
+ }
+}
diff --git a/apps/bff/src/bundled_templates/layers/virtual_cache.json b/apps/bff/src/bundled_templates/layers/virtual_cache.json
new file mode 100644
index 0000000..a6b8ac3
--- /dev/null
+++ b/apps/bff/src/bundled_templates/layers/virtual_cache.json
@@ -0,0 +1,184 @@
+{
+ "key": "VIRTUAL_CACHE",
+ "alias": "Virtual Cache",
+ "color": "var(--sw-warn)",
+ "documentLink": "https://skywalking.apache.org/docs/main/next/en/setup/service-agent/virtual-cache/",
+ "aliases": {
+ "services": "Caches"
+ },
+ "components": {
+ "service": true,
+ "instances": false,
+ "endpoints": false,
+ "endpointDependency": false,
+ "topology": false,
+ "traces": false,
+ "logs": false
+ },
+ "layer-header": {
+ "orderBy": "cpm",
+ "columns": [
+ { "metric": "cpm", "label": "Access RPM", "unit": "rpm", "mqe": "cache_access_cpm", "aggregation": "sum" },
+ { "metric": "respTime", "label": "Latency", "unit": "ms", "mqe": "cache_access_resp_time", "aggregation": "avg" },
+ { "metric": "p95", "label": "p95", "unit": "ms", "mqe": "cache_access_percentile{p='95'}", "aggregation": "avg" },
+ { "metric": "err", "label": "Error Rate", "unit": "%", "mqe": "100 - cache_access_sla/100", "aggregation": "avg" }
+ ]
+ },
+ "overview": {
+ "groups": [
+ {
+ "title": "Latency & errors",
+ "size": "auto",
+ "metrics": [
+ { "id": "p95", "label": "P95", "tip": "95th-percentile cache access latency.", "mqe": "cache_access_percentile{p='95'}", "unit": "ms", "aggregation": "avg" },
+ { "id": "err", "label": "Error Rate", "tip": "100 − cache access success rate. Cache exceptions captured by the calling service's agent.", "mqe": "100 - cache_access_sla/100", "unit": "%", "aggregation": "avg" }
+ ]
+ },
+ {
+ "title": "Throughput",
+ "size": "square",
+ "metrics": [
+ { "id": "cpm", "label": "Access RPM", "tip": "Cache operations per minute (read + write).", "mqe": "cache_access_cpm", "unit": "rpm", "aggregation": "sum" }
+ ]
+ }
+ ]
+ },
+ "dashboards": {
+ "service": [
+ {
+ "id": "access_cpm_line",
+ "title": "Access Traffic",
+ "type": "line",
+ "unit": "rpm",
+ "expressions": ["cache_access_cpm"],
+ "span": 4,
+ "rowSpan": 2
+ },
+ {
+ "id": "access_resp_line",
+ "title": "Avg Access Latency",
+ "type": "line",
+ "unit": "ms",
+ "expressions": ["cache_access_resp_time"],
+ "span": 4,
+ "rowSpan": 2
+ },
+ {
+ "id": "access_sla_line",
+ "title": "Access Success Rate",
+ "type": "line",
+ "unit": "%",
+ "expressions": ["cache_access_sla/100"],
+ "span": 4,
+ "rowSpan": 2
+ },
+ {
+ "id": "access_percentile",
+ "title": "Access Latency Percentile",
+ "type": "line",
+ "unit": "ms",
+ "expressions": [
+ "relabels(cache_access_percentile{p='50,75,90,95,99'},p='50,75,90,95,99',percentile='50,75,90,95,99')"
+ ],
+ "span": 12,
+ "rowSpan": 2
+ },
+ {
+ "id": "read_traffic",
+ "title": "Read Traffic",
+ "type": "line",
+ "unit": "rpm",
+ "expressions": ["cache_read_cpm"],
+ "span": 3,
+ "rowSpan": 2
+ },
+ {
+ "id": "read_resp",
+ "title": "Read Avg Latency",
+ "type": "line",
+ "unit": "ms",
+ "expressions": ["cache_read_resp_time"],
+ "span": 3,
+ "rowSpan": 2
+ },
+ {
+ "id": "write_traffic",
+ "title": "Write Traffic",
+ "type": "line",
+ "unit": "rpm",
+ "expressions": ["cache_write_cpm"],
+ "span": 3,
+ "rowSpan": 2
+ },
+ {
+ "id": "write_resp",
+ "title": "Write Avg Latency",
+ "type": "line",
+ "unit": "ms",
+ "expressions": ["cache_write_resp_time"],
+ "span": 3,
+ "rowSpan": 2
+ },
+ {
+ "id": "read_sla",
+ "title": "Read Success Rate",
+ "type": "line",
+ "unit": "%",
+ "expressions": ["cache_read_sla/100"],
+ "span": 6,
+ "rowSpan": 2
+ },
+ {
+ "id": "write_sla",
+ "title": "Write Success Rate",
+ "type": "line",
+ "unit": "%",
+ "expressions": ["cache_write_sla/100"],
+ "span": 6,
+ "rowSpan": 2
+ },
+ {
+ "id": "read_percentile",
+ "title": "Read Latency Percentile",
+ "type": "line",
+ "unit": "ms",
+ "expressions": [
+ "relabels(cache_read_percentile{p='50,75,90,95,99'},p='50,75,90,95,99',percentile='50,75,90,95,99')"
+ ],
+ "span": 6,
+ "rowSpan": 2
+ },
+ {
+ "id": "write_percentile",
+ "title": "Write Latency Percentile",
+ "type": "line",
+ "unit": "ms",
+ "expressions": [
+ "relabels(cache_write_percentile{p='50,75,90,95,99'},p='50,75,90,95,99',percentile='50,75,90,95,99')"
+ ],
+ "span": 6,
+ "rowSpan": 2
+ },
+ {
+ "id": "slow_read",
+ "title": "Slow Read Commands",
+ "tip": "Top 10 slowest captured read commands. Each row is a single execution — click to drill into the originating trace.",
+ "type": "record",
+ "unit": "ms",
+ "expressions": ["top_n(top_n_cache_read_command, 10, des)"],
+ "span": 6,
+ "rowSpan": 4
+ },
+ {
+ "id": "slow_write",
+ "title": "Slow Write Commands",
+ "tip": "Top 10 slowest captured write commands. Each row is a single execution — click to drill into the originating trace.",
+ "type": "record",
+ "unit": "ms",
+ "expressions": ["top_n(top_n_cache_write_command, 10, des)"],
+ "span": 6,
+ "rowSpan": 4
+ }
+ ]
+ }
+}
diff --git a/apps/bff/src/bundled_templates/layers/virtual_database.json b/apps/bff/src/bundled_templates/layers/virtual_database.json
new file mode 100644
index 0000000..cbbacb9
--- /dev/null
+++ b/apps/bff/src/bundled_templates/layers/virtual_database.json
@@ -0,0 +1,101 @@
+{
+ "key": "VIRTUAL_DATABASE",
+ "alias": "Virtual Database",
+ "color": "var(--sw-warn)",
+ "documentLink": "https://skywalking.apache.org/docs/main/next/en/setup/service-agent/virtual-database/",
+ "aliases": {
+ "services": "Databases"
+ },
+ "components": {
+ "service": true,
+ "instances": false,
+ "endpoints": false,
+ "endpointDependency": false,
+ "topology": false,
+ "traces": false,
+ "logs": false
+ },
+ "layer-header": {
+ "orderBy": "cpm",
+ "columns": [
+ { "metric": "cpm", "label": "Access RPM", "unit": "rpm", "mqe": "database_access_cpm", "aggregation": "sum" },
+ { "metric": "respTime", "label": "Latency", "unit": "ms", "mqe": "database_access_resp_time", "aggregation": "avg" },
+ { "metric": "p95", "label": "p95", "unit": "ms", "mqe": "database_access_percentile{p='95'}", "aggregation": "avg" },
+ { "metric": "err", "label": "Error Rate", "unit": "%", "mqe": "100 - database_access_sla/100", "aggregation": "avg" }
+ ]
+ },
+ "overview": {
+ "groups": [
+ {
+ "title": "Latency & errors",
+ "size": "auto",
+ "metrics": [
+ { "id": "p95", "label": "P95", "tip": "95th-percentile database access latency.", "mqe": "database_access_percentile{p='95'}", "unit": "ms", "aggregation": "avg" },
+ { "id": "err", "label": "Error Rate", "tip": "100 − database access success rate. Driven by query exceptions captured by the calling service's agent.", "mqe": "100 - database_access_sla/100", "unit": "%", "aggregation": "avg" }
+ ]
+ },
+ {
+ "title": "Throughput",
+ "size": "square",
+ "metrics": [
+ { "id": "cpm", "label": "Access RPM", "tip": "Queries per minute against this virtual database.", "mqe": "database_access_cpm", "unit": "rpm", "aggregation": "sum" }
+ ]
+ }
+ ]
+ },
+ "dashboards": {
+ "service": [
+ {
+ "id": "traffic_line",
+ "title": "Access Traffic",
+ "tip": "Queries per minute against the virtual database.",
+ "type": "line",
+ "unit": "rpm",
+ "expressions": ["database_access_cpm"],
+ "span": 4,
+ "rowSpan": 2
+ },
+ {
+ "id": "resp_time_line",
+ "title": "Avg Response Time",
+ "type": "line",
+ "unit": "ms",
+ "expressions": ["database_access_resp_time"],
+ "span": 4,
+ "rowSpan": 2
+ },
+ {
+ "id": "sla_line",
+ "title": "Success Rate",
+ "tip": "Percentage of queries that returned without throwing.",
+ "type": "line",
+ "unit": "%",
+ "expressions": ["database_access_sla/100"],
+ "span": 4,
+ "rowSpan": 2
+ },
+ {
+ "id": "percentile_line",
+ "title": "Response Time Percentile",
+ "tip": "p50 / p75 / p90 / p95 / p99 latency — the tail of the access-time distribution.",
+ "type": "line",
+ "unit": "ms",
+ "expressions": [
+ "relabels(database_access_percentile{p='50,75,90,95,99'},p='50,75,90,95,99',percentile='50,75,90,95,99')"
+ ],
+ "span": 12,
+ "rowSpan": 2
+ },
+ {
+ "id": "slow_statements",
+ "title": "Slow Statements",
+ "tip": "Top 20 slowest captured statements. Each row is a single statement execution — click to drill into the originating trace.",
+ "type": "record",
+ "unit": "ms",
+ "expressions": ["top_n(top_n_database_statement, 20, des)"],
+ "span": 12,
+ "rowSpan": 4
+ }
+ ]
+ }
+}
diff --git a/apps/bff/src/bundled_templates/layers/virtual_genai.json b/apps/bff/src/bundled_templates/layers/virtual_genai.json
new file mode 100644
index 0000000..bbfc62a
--- /dev/null
+++ b/apps/bff/src/bundled_templates/layers/virtual_genai.json
@@ -0,0 +1,49 @@
+{
+ "key": "VIRTUAL_GENAI",
+ "alias": "Virtual GenAI",
+ "color": "var(--sw-pink)",
+ "documentLink": "https://skywalking.apache.org/docs/main/next/en/setup/service-agent/virtual-genai/",
+ "aliases": { "services": "GenAI Providers", "endpoints": "Models" },
+ "components": { "service": true, "instances": false, "endpoints": true, "topology": false, "traces": false, "logs": false },
+ "layer-header": {
+ "orderBy": "cpm",
+ "columns": [
+ { "metric": "cpm", "label": "RPM", "unit": "rpm", "mqe": "gen_ai_provider_cpm", "aggregation": "sum" },
+ { "metric": "respTime", "label": "Latency", "unit": "ms", "mqe": "gen_ai_provider_resp_time", "aggregation": "avg" },
+ { "metric": "sla", "label": "Success Rate", "unit": "%", "mqe": "gen_ai_provider_sla/100", "aggregation": "avg" },
+ { "metric": "outTokens", "label": "Output Tokens", "mqe": "latest(gen_ai_provider_output_tokens_sum)", "aggregation": "sum" }
+ ]
+ },
+ "overview": {
+ "groups": [
+ { "title": "Latency & errors", "size": "auto", "metrics": [
+ { "id": "respTime", "label": "Avg Latency", "tip": "Avg call latency to the GenAI provider.", "mqe": "gen_ai_provider_resp_time", "unit": "ms", "aggregation": "avg" },
+ { "id": "err", "label": "Error Rate", "tip": "100 − provider SLA.", "mqe": "100 - gen_ai_provider_sla/100", "unit": "%", "aggregation": "avg" }
+ ]},
+ { "title": "Cost", "size": "square", "metrics": [
+ { "id": "totalCost", "label": "Total Estimated Cost", "tip": "Cumulative provider cost estimate.", "mqe": "latest(gen_ai_provider_total_estimated_cost)/1000000", "aggregation": "sum" }
+ ]}
+ ]
+ },
+ "dashboards": {
+ "service": [
+ { "id": "cpm", "title": "Calls / min", "type": "line", "unit": "rpm", "expressions": ["gen_ai_provider_cpm"], "span": 4, "rowSpan": 2 },
+ { "id": "resp_time", "title": "Avg Response Time", "type": "line", "unit": "ms", "expressions": ["gen_ai_provider_resp_time"], "span": 4, "rowSpan": 2 },
+ { "id": "sla", "title": "Success Rate", "type": "line", "unit": "%", "expressions": ["gen_ai_provider_sla/100"], "span": 4, "rowSpan": 2 },
+ { "id": "percentile", "title": "Latency Percentile", "type": "line", "unit": "ms", "expressions": ["gen_ai_provider_latency_percentile"], "span": 12, "rowSpan": 2 },
+ { "id": "input_tokens", "title": "Input Tokens", "type": "line", "expressions": ["latest(gen_ai_provider_input_tokens_sum)", "gen_ai_provider_input_tokens_avg"], "expressionLabels": ["sum", "avg"], "span": 6, "rowSpan": 2 },
+ { "id": "output_tokens", "title": "Output Tokens", "type": "line", "expressions": ["latest(gen_ai_provider_output_tokens_sum)", "gen_ai_provider_output_tokens_avg"], "expressionLabels": ["sum", "avg"], "span": 6, "rowSpan": 2 },
+ { "id": "cost", "title": "Estimated Cost", "type": "line", "expressions": ["latest(gen_ai_provider_total_estimated_cost)/1000000", "gen_ai_provider_avg_estimated_cost/1000000"], "expressionLabels": ["total", "avg"], "span": 12, "rowSpan": 2 }
+ ],
+ "endpoint": [
+ { "id": "cpm", "title": "Calls / min", "type": "line", "unit": "rpm", "expressions": ["gen_ai_model_call_cpm"], "span": 4, "rowSpan": 2 },
+ { "id": "latency_avg", "title": "Avg Latency", "type": "line", "unit": "ms", "expressions": ["gen_ai_model_latency_avg"], "span": 4, "rowSpan": 2 },
+ { "id": "sla", "title": "Success Rate", "type": "line", "unit": "%", "expressions": ["gen_ai_model_sla/100"], "span": 4, "rowSpan": 2 },
+ { "id": "latency_percentile", "title": "Latency Percentile", "type": "line", "unit": "ms", "expressions": ["gen_ai_model_latency_percentile"], "span": 6, "rowSpan": 2 },
+ { "id": "ttft", "title": "TTFT", "type": "line", "unit": "ms", "expressions": ["gen_ai_model_ttft_avg", "gen_ai_model_ttft_percentile"], "expressionLabels": ["avg", "percentile"], "span": 6, "rowSpan": 2 },
+ { "id": "input_tokens", "title": "Input Tokens", "type": "line", "expressions": ["latest(gen_ai_model_input_tokens_sum)", "gen_ai_model_input_tokens_avg"], "expressionLabels": ["sum", "avg"], "span": 6, "rowSpan": 2 },
+ { "id": "output_tokens", "title": "Output Tokens", "type": "line", "expressions": ["latest(gen_ai_model_output_tokens_sum)", "gen_ai_model_output_tokens_avg"], "expressionLabels": ["sum", "avg"], "span": 6, "rowSpan": 2 },
+ { "id": "cost", "title": "Estimated Cost", "type": "line", "expressions": ["latest(gen_ai_model_total_estimated_cost)/1000000", "gen_ai_model_avg_estimated_cost/1000000"], "expressionLabels": ["total", "avg"], "span": 12, "rowSpan": 2 }
+ ]
+ }
+}
diff --git a/apps/bff/src/bundled_templates/layers/virtual_mq.json b/apps/bff/src/bundled_templates/layers/virtual_mq.json
new file mode 100644
index 0000000..c8d902e
--- /dev/null
+++ b/apps/bff/src/bundled_templates/layers/virtual_mq.json
@@ -0,0 +1,168 @@
+{
+ "key": "VIRTUAL_MQ",
+ "alias": "Virtual MQ",
+ "color": "var(--sw-warn)",
+ "documentLink": "https://skywalking.apache.org/docs/main/next/en/setup/service-agent/virtual-mq/",
+ "aliases": {
+ "services": "MQ clusters",
+ "endpoints": "Topics"
+ },
+ "components": {
+ "service": true,
+ "instances": false,
+ "endpoints": true,
+ "endpointDependency": false,
+ "topology": false,
+ "traces": false,
+ "logs": false
+ },
+ "layer-header": {
+ "orderBy": "consumeCpm",
+ "columns": [
+ { "metric": "consumeCpm", "label": "Consume RPM", "unit": "rpm", "mqe": "mq_service_consume_cpm", "aggregation": "sum" },
+ { "metric": "produceCpm", "label": "Produce RPM", "unit": "rpm", "mqe": "mq_service_produce_cpm", "aggregation": "sum" },
+ { "metric": "consumeLatency", "label": "Consume Latency", "unit": "ms", "mqe": "mq_service_consume_latency", "aggregation": "avg" },
+ { "metric": "consumeErr", "label": "Consume Error Rate", "unit": "%", "mqe": "100 - mq_service_consume_sla/100", "aggregation": "avg" }
+ ]
+ },
+ "overview": {
+ "groups": [
+ {
+ "title": "Latency & errors",
+ "size": "auto",
+ "metrics": [
+ { "id": "p95", "label": "P95", "tip": "95th-percentile consume latency (timestamp(received) − timestamp(producing)).", "mqe": "mq_service_consume_percentile{p='95'}", "unit": "ms", "aggregation": "avg" },
+ { "id": "consumeErr", "label": "Consume Error Rate", "tip": "100 − consume success rate.", "mqe": "100 - mq_service_consume_sla/100", "unit": "%", "aggregation": "avg" },
+ { "id": "produceErr", "label": "Produce Error Rate", "tip": "100 − produce success rate.", "mqe": "100 - mq_service_produce_sla/100", "unit": "%", "aggregation": "avg" }
+ ]
+ },
+ {
+ "title": "Throughput",
+ "size": "square",
+ "metrics": [
+ { "id": "consumeCpm", "label": "Consume RPM", "tip": "Messages consumed per minute.", "mqe": "mq_service_consume_cpm", "unit": "rpm", "aggregation": "sum" }
+ ]
+ }
+ ]
+ },
+ "dashboards": {
+ "service": [
+ {
+ "id": "consume_cpm_line",
+ "title": "Consume Traffic",
+ "type": "line",
+ "unit": "rpm",
+ "expressions": ["mq_service_consume_cpm"],
+ "span": 3,
+ "rowSpan": 2
+ },
+ {
+ "id": "produce_cpm_line",
+ "title": "Produce Traffic",
+ "type": "line",
+ "unit": "rpm",
+ "expressions": ["mq_service_produce_cpm"],
+ "span": 3,
+ "rowSpan": 2
+ },
+ {
+ "id": "consume_latency_line",
+ "title": "Consume Avg Latency",
+ "tip": "Avg time between message production and consumption.",
+ "type": "line",
+ "unit": "ms",
+ "expressions": ["mq_service_consume_latency"],
+ "span": 3,
+ "rowSpan": 2
+ },
+ {
+ "id": "consume_sla_line",
+ "title": "Consume Success Rate",
+ "type": "line",
+ "unit": "%",
+ "expressions": ["mq_service_consume_sla/100"],
+ "span": 3,
+ "rowSpan": 2
+ },
+ {
+ "id": "produce_sla_line",
+ "title": "Produce Success Rate",
+ "type": "line",
+ "unit": "%",
+ "expressions": ["mq_service_produce_sla/100"],
+ "span": 6,
+ "rowSpan": 2
+ },
+ {
+ "id": "consume_percentile",
+ "title": "Consume Latency Percentile",
+ "tip": "p50 / p75 / p90 / p95 / p99 of consume latency.",
+ "type": "line",
+ "unit": "ms",
+ "expressions": [
+ "relabels(mq_service_consume_percentile{p='50,75,90,95,99'},p='50,75,90,95,99',percentile='50,75,90,95,99')"
+ ],
+ "span": 6,
+ "rowSpan": 2
+ }
+ ],
+ "endpoint": [
+ {
+ "id": "ep_consume_cpm",
+ "title": "Topic Consume Traffic",
+ "type": "line",
+ "unit": "rpm",
+ "expressions": ["mq_endpoint_consume_cpm"],
+ "span": 4,
+ "rowSpan": 2
+ },
+ {
+ "id": "ep_produce_cpm",
+ "title": "Topic Produce Traffic",
+ "type": "line",
+ "unit": "rpm",
+ "expressions": ["mq_endpoint_produce_cpm"],
+ "span": 4,
+ "rowSpan": 2
+ },
+ {
+ "id": "ep_consume_latency",
+ "title": "Topic Consume Avg Latency",
+ "type": "line",
+ "unit": "ms",
+ "expressions": ["mq_endpoint_consume_latency"],
+ "span": 4,
+ "rowSpan": 2
+ },
+ {
+ "id": "ep_consume_sla",
+ "title": "Topic Consume Success Rate",
+ "type": "line",
+ "unit": "%",
+ "expressions": ["mq_endpoint_consume_sla/100"],
+ "span": 6,
+ "rowSpan": 2
+ },
+ {
+ "id": "ep_produce_sla",
+ "title": "Topic Produce Success Rate",
+ "type": "line",
+ "unit": "%",
+ "expressions": ["mq_endpoint_produce_sla/100"],
+ "span": 6,
+ "rowSpan": 2
+ },
+ {
+ "id": "ep_consume_percentile",
+ "title": "Topic Consume Latency Percentile",
+ "type": "line",
+ "unit": "ms",
+ "expressions": [
+ "relabels(mq_endpoint_consume_percentile{p='50,75,90,95,99'},p='50,75,90,95,99',percentile='50,75,90,95,99')"
+ ],
+ "span": 12,
+ "rowSpan": 2
+ }
+ ]
+ }
+}
diff --git a/apps/bff/src/bundled_templates/layers/wechat_mini_program.json b/apps/bff/src/bundled_templates/layers/wechat_mini_program.json
new file mode 100644
index 0000000..92ad8b4
--- /dev/null
+++ b/apps/bff/src/bundled_templates/layers/wechat_mini_program.json
@@ -0,0 +1,234 @@
+{
+ "key": "WECHAT_MINI_PROGRAM",
+ "alias": "WeChat Mini Program",
+ "color": "var(--sw-cyan)",
+ "documentLink": "https://skywalking.apache.org/docs/main/next/en/setup/backend/backend-wechat-mini-program-monitoring/",
+ "aliases": {
+ "services": "Mini-programs",
+ "instances": "Versions",
+ "endpoints": "Pages"
+ },
+ "components": {
+ "service": true,
+ "instances": true,
+ "endpoints": true,
+ "topology": false,
+ "traces": true,
+ "logs": true
+ },
+ "layer-header": {
+ "orderBy": "cpm",
+ "columns": [
+ { "metric": "cpm", "label": "Request RPM", "unit": "rpm", "mqe": "meter_wechat_mp_request_cpm", "aggregation": "sum" },
+ { "metric": "launch", "label": "Launch", "unit": "ms", "mqe": "meter_wechat_mp_app_launch_duration", "aggregation": "avg" },
+ { "metric": "firstRender", "label": "First Render", "unit": "ms", "mqe": "meter_wechat_mp_first_render_duration", "aggregation": "avg" },
+ { "metric": "errors", "label": "Errors", "mqe": "meter_wechat_mp_error_count", "aggregation": "sum" }
+ ]
+ },
+ "overview": {
+ "groups": [
+ {
+ "title": "Latency & errors",
+ "size": "auto",
+ "metrics": [
+ { "id": "request_p95", "label": "Request P95", "tip": "95th-percentile request duration from the WeChat network bridge.", "mqe": "meter_wechat_mp_request_duration_percentile{p='95'}", "unit": "ms", "aggregation": "avg" },
+ { "id": "launch", "label": "Launch", "tip": "Mini-program launch duration as observed by the WeChat client.", "mqe": "meter_wechat_mp_app_launch_duration", "unit": "ms", "aggregation": "avg" },
+ { "id": "errors", "label": "Errors", "tip": "Reported error count.", "mqe": "meter_wechat_mp_error_count", "aggregation": "sum" }
+ ]
+ },
+ {
+ "title": "Throughput",
+ "size": "square",
+ "metrics": [
+ { "id": "cpm", "label": "Request RPM", "tip": "Bridge requests per minute.", "mqe": "meter_wechat_mp_request_cpm", "unit": "rpm", "aggregation": "sum" }
+ ]
+ }
+ ]
+ },
+ "dashboards": {
+ "service": [
+ {
+ "id": "launch_duration",
+ "title": "App Launch Duration",
+ "type": "line",
+ "unit": "ms",
+ "expressions": ["meter_wechat_mp_app_launch_duration"],
+ "span": 3,
+ "rowSpan": 2
+ },
+ {
+ "id": "first_render",
+ "title": "First Render Duration",
+ "type": "line",
+ "unit": "ms",
+ "expressions": ["meter_wechat_mp_first_render_duration"],
+ "span": 3,
+ "rowSpan": 2
+ },
+ {
+ "id": "package_load",
+ "title": "Package Load Duration",
+ "tip": "Time to download + parse the mini-program package bundle.",
+ "type": "line",
+ "unit": "ms",
+ "expressions": ["meter_wechat_mp_package_load_duration"],
+ "span": 3,
+ "rowSpan": 2
+ },
+ {
+ "id": "error_count",
+ "title": "Error Count",
+ "type": "line",
+ "expressions": ["meter_wechat_mp_error_count"],
+ "span": 3,
+ "rowSpan": 2
+ },
+ {
+ "id": "route_duration",
+ "title": "Route Duration",
+ "tip": "Time spent in mini-program page-route transitions.",
+ "type": "line",
+ "unit": "ms",
+ "expressions": ["meter_wechat_mp_route_duration"],
+ "span": 4,
+ "rowSpan": 2
+ },
+ {
+ "id": "script_duration",
+ "title": "Script Duration",
+ "type": "line",
+ "unit": "ms",
+ "expressions": ["meter_wechat_mp_script_duration"],
+ "span": 4,
+ "rowSpan": 2
+ },
+ {
+ "id": "request_cpm",
+ "title": "Request Load",
+ "type": "line",
+ "unit": "rpm",
+ "expressions": ["meter_wechat_mp_request_cpm"],
+ "span": 4,
+ "rowSpan": 2
+ },
+ {
+ "id": "request_percentile",
+ "title": "Request Duration Percentile",
+ "type": "line",
+ "unit": "ms",
+ "expressions": [
+ "relabels(meter_wechat_mp_request_duration_percentile{p='50,75,90,95,99'},p='50,75,90,95,99',percentile='50,75,90,95,99')"
+ ],
+ "span": 12,
+ "rowSpan": 2
+ }
+ ],
+ "instance": [
+ {
+ "id": "v_launch",
+ "title": "Launch Duration",
+ "type": "line",
+ "unit": "ms",
+ "expressions": ["meter_wechat_mp_instance_app_launch_duration"],
+ "span": 3,
+ "rowSpan": 2
+ },
+ {
+ "id": "v_first_render",
+ "title": "First Render Duration",
+ "type": "line",
+ "unit": "ms",
+ "expressions": ["meter_wechat_mp_instance_first_render_duration"],
+ "span": 3,
+ "rowSpan": 2
+ },
+ {
+ "id": "v_package_load",
+ "title": "Package Load Duration",
+ "type": "line",
+ "unit": "ms",
+ "expressions": ["meter_wechat_mp_instance_package_load_duration"],
+ "span": 3,
+ "rowSpan": 2
+ },
+ {
+ "id": "v_request_cpm",
+ "title": "Request Load",
+ "type": "line",
+ "unit": "rpm",
+ "expressions": ["meter_wechat_mp_instance_request_cpm"],
+ "span": 3,
+ "rowSpan": 2
+ },
+ {
+ "id": "v_route_duration",
+ "title": "Route Duration",
+ "type": "line",
+ "unit": "ms",
+ "expressions": ["meter_wechat_mp_instance_route_duration"],
+ "span": 4,
+ "rowSpan": 2
+ },
+ {
+ "id": "v_script_duration",
+ "title": "Script Duration",
+ "type": "line",
+ "unit": "ms",
+ "expressions": ["meter_wechat_mp_instance_script_duration"],
+ "span": 4,
+ "rowSpan": 2
+ },
+ {
+ "id": "v_request_percentile",
+ "title": "Request Duration Percentile",
+ "type": "line",
+ "unit": "ms",
+ "expressions": [
+ "relabels(meter_wechat_mp_instance_request_duration_percentile{p='50,75,90,95,99'},p='50,75,90,95,99',percentile='50,75,90,95,99')"
+ ],
+ "span": 4,
+ "rowSpan": 2
+ }
+ ],
+ "endpoint": [
+ {
+ "id": "ep_launch",
+ "title": "Launch Duration on Page",
+ "type": "line",
+ "unit": "ms",
+ "expressions": ["meter_wechat_mp_endpoint_app_launch_duration"],
+ "span": 4,
+ "rowSpan": 2
+ },
+ {
+ "id": "ep_first_render",
+ "title": "First Render Duration",
+ "type": "line",
+ "unit": "ms",
+ "expressions": ["meter_wechat_mp_endpoint_first_render_duration"],
+ "span": 4,
+ "rowSpan": 2
+ },
+ {
+ "id": "ep_request_cpm",
+ "title": "Request Load",
+ "type": "line",
+ "unit": "rpm",
+ "expressions": ["meter_wechat_mp_endpoint_request_cpm"],
+ "span": 4,
+ "rowSpan": 2
+ },
+ {
+ "id": "ep_request_percentile",
+ "title": "Request Duration Percentile",
+ "type": "line",
+ "unit": "ms",
+ "expressions": [
+ "relabels(meter_wechat_mp_endpoint_request_duration_percentile{p='50,75,90,95,99'},p='50,75,90,95,99',percentile='50,75,90,95,99')"
+ ],
+ "span": 12,
+ "rowSpan": 2
+ }
+ ]
+ }
+}
diff --git a/apps/bff/src/bundled_templates/overviews/general.json b/apps/bff/src/bundled_templates/overviews/general.json
new file mode 100644
index 0000000..fe17da1
--- /dev/null
+++ b/apps/bff/src/bundled_templates/overviews/general.json
@@ -0,0 +1,82 @@
+{
+ "id": "general",
+ "title": "General service",
+ "description": "Overview of services on the General layer with the Kubernetes service-count cross-referenced.",
+ "widgets": [
+ {
+ "id": "g_count",
+ "title": "Active services",
+ "tip": "Number of services reporting on this layer.",
+ "layer": "GENERAL",
+ "type": "service-count",
+ "span": 3,
+ "rowSpan": 1
+ },
+ {
+ "id": "g_rpm",
+ "title": "Throughput",
+ "tip": "Layer-wide requests per minute.",
+ "layer": "GENERAL",
+ "type": "metric",
+ "mqe": "service_cpm",
+ "unit": "rpm",
+ "aggregation": "sum",
+ "span": 3,
+ "rowSpan": 1
+ },
+ {
+ "id": "g_p99",
+ "title": "P99 latency",
+ "tip": "99th-percentile response time across the layer.",
+ "layer": "GENERAL",
+ "type": "metric",
+ "mqe": "service_percentile{p='99'}",
+ "unit": "ms",
+ "aggregation": "avg",
+ "span": 3,
+ "rowSpan": 1
+ },
+ {
+ "id": "g_err",
+ "title": "Error rate",
+ "tip": "100 − SLA. Higher = worse.",
+ "layer": "GENERAL",
+ "type": "metric",
+ "mqe": "100 - service_sla/100",
+ "unit": "%",
+ "aggregation": "avg",
+ "span": 3,
+ "rowSpan": 1
+ },
+ {
+ "id": "k8s_count",
+ "title": "K8s services",
+ "tip": "Services reporting through the K8s service layer.",
+ "layer": "K8S_SERVICE",
+ "type": "service-count",
+ "span": 3,
+ "rowSpan": 1
+ },
+ {
+ "id": "k8s_p99",
+ "title": "K8s P99",
+ "tip": "99th-percentile latency for K8s-routed traffic.",
+ "layer": "K8S_SERVICE",
+ "type": "metric",
+ "mqe": "k8s_service_resp_time_percentile{p='99'}",
+ "unit": "ms",
+ "aggregation": "avg",
+ "span": 3,
+ "rowSpan": 1
+ },
+ {
+ "id": "g_topo",
+ "title": "General service topology",
+ "tip": "Live service map for GENERAL — same renderer as the per-layer Topology tab.",
+ "layer": "GENERAL",
+ "type": "topology",
+ "span": 12,
+ "rowSpan": 4
+ }
+ ]
+}
diff --git a/apps/bff/src/bundled_templates/overviews/mesh.json b/apps/bff/src/bundled_templates/overviews/mesh.json
new file mode 100644
index 0000000..700f7f2
--- /dev/null
+++ b/apps/bff/src/bundled_templates/overviews/mesh.json
@@ -0,0 +1,82 @@
+{
+ "id": "mesh",
+ "title": "Mesh service",
+ "description": "Service-mesh overview — control-plane + data-plane signals with the K8s service-count cross-referenced.",
+ "widgets": [
+ {
+ "id": "m_count",
+ "title": "Active mesh services",
+ "tip": "Services routed through the mesh data-plane.",
+ "layer": "MESH",
+ "type": "service-count",
+ "span": 3,
+ "rowSpan": 1
+ },
+ {
+ "id": "m_rpm",
+ "title": "Mesh RPM",
+ "tip": "Mesh-wide requests per minute.",
+ "layer": "MESH",
+ "type": "metric",
+ "mqe": "service_mesh_cpm",
+ "unit": "rpm",
+ "aggregation": "sum",
+ "span": 3,
+ "rowSpan": 1
+ },
+ {
+ "id": "m_p99",
+ "title": "P99 latency",
+ "tip": "99th-percentile response time across mesh-routed traffic.",
+ "layer": "MESH",
+ "type": "metric",
+ "mqe": "service_mesh_percentile{p='99'}",
+ "unit": "ms",
+ "aggregation": "avg",
+ "span": 3,
+ "rowSpan": 1
+ },
+ {
+ "id": "m_err",
+ "title": "Error rate",
+ "tip": "100 − mesh SLA.",
+ "layer": "MESH",
+ "type": "metric",
+ "mqe": "100 - service_mesh_sla/100",
+ "unit": "%",
+ "aggregation": "avg",
+ "span": 3,
+ "rowSpan": 1
+ },
+ {
+ "id": "k8s_count",
+ "title": "K8s services",
+ "tip": "Services reporting through the K8s service layer.",
+ "layer": "K8S_SERVICE",
+ "type": "service-count",
+ "span": 3,
+ "rowSpan": 1
+ },
+ {
+ "id": "k8s_p99",
+ "title": "K8s P99",
+ "tip": "99th-percentile latency for K8s-routed traffic.",
+ "layer": "K8S_SERVICE",
+ "type": "metric",
+ "mqe": "k8s_service_resp_time_percentile{p='99'}",
+ "unit": "ms",
+ "aggregation": "avg",
+ "span": 3,
+ "rowSpan": 1
+ },
+ {
+ "id": "m_topo",
+ "title": "Mesh service topology",
+ "tip": "Live service map for the mesh data-plane.",
+ "layer": "MESH",
+ "type": "topology",
+ "span": 12,
+ "rowSpan": 4
+ }
+ ]
+}
diff --git a/apps/bff/src/config/schema.ts b/apps/bff/src/config/schema.ts
index 3103d69..370cacf 100644
--- a/apps/bff/src/config/schema.ts
+++ b/apps/bff/src/config/schema.ts
@@ -32,6 +32,17 @@ const oapSchema = z
// The OAP query/status host (GraphQL + /status/*).
statusUrl: z.string().url().default('http://127.0.0.1:12800'),
timeoutMs: z.number().int().positive().default(15000),
+ // Optional basic-auth credentials sent on every outbound OAP call
+ // (GraphQL, /status, /api/v2/* for Zipkin). The public demo host
+ // (`demo.skywalking.apache.org`) gates calls behind
+ // skywalking:skywalking — set both fields to use it.
+ auth: z
+ .object({
+ username: z.string().min(1),
+ password: z.string().min(1),
+ })
+ .strict()
+ .optional(),
mqe: z
.object({
host: z.string().optional(),
diff --git a/apps/bff/src/dashboard/defaults.ts b/apps/bff/src/dashboard/defaults.ts
index 01c7797..55ff193 100644
--- a/apps/bff/src/dashboard/defaults.ts
+++ b/apps/bff/src/dashboard/defaults.ts
@@ -180,7 +180,7 @@ const GENERIC_WIDGETS: DashboardWidget[] = [
/**
* Resolve the default widget set for `(layerKey)`. First tries the
- * JSON layer template (`src/layers/config/<key>.json`); falls back to
+ * JSON layer template (`src/bundled_templates/layers/<key>.json`); falls back to
* the hardcoded TS sets above when no JSON exists for the layer. JSON
* wins because that's where operators will eventually edit widgets
* via the admin page.
diff --git a/apps/bff/src/dashboard/routes.ts b/apps/bff/src/dashboard/routes.ts
index 29a8941..a09636b 100644
--- a/apps/bff/src/dashboard/routes.ts
+++ b/apps/bff/src/dashboard/routes.ts
@@ -42,7 +42,7 @@ import type {
import type { ConfigSource } from '../config/loader.js';
import type { SessionStore } from '../auth/sessions.js';
import { requireAuth } from '../auth/middleware.js';
-import { graphqlPost } from '../oap/graphql-client.js';
+import { graphqlPost, buildOapOpts } from '../oap/graphql-client.js';
import {
allLayerTemplates,
getLayerTemplate,
@@ -335,11 +335,7 @@ export function registerDashboardRoute(app: FastifyInstance, deps: DashboardRout
let serviceName = parsed.data.service ?? '';
let normal = true;
const cfgCurrent = deps.config.current;
- const opts = {
- statusUrl: cfgCurrent.oap.statusUrl,
- timeoutMs: cfgCurrent.oap.timeoutMs,
- fetch: deps.fetch,
- };
+ const opts = buildOapOpts(cfgCurrent, deps.fetch);
const window = defaultWindow();
const baseResp: DashboardResponse = {
diff --git a/apps/bff/src/layers/config/k8s_service.json b/apps/bff/src/layers/config/k8s_service.json
deleted file mode 100644
index 0ea174d..0000000
--- a/apps/bff/src/layers/config/k8s_service.json
+++ /dev/null
@@ -1,109 +0,0 @@
-{
- "key": "K8S_SERVICE",
- "alias": "K8s Service",
- "color": "var(--sw-purple)",
- "aliases": {
- "services": "K8s services",
- "instances": "Pods"
- },
- "components": {
- "service": true,
- "instances": true,
- "topology": true,
- "traces": true,
- "logs": true
- },
- "metrics": {
- "orderBy": "cpm",
- "columns": [
- {
- "metric": "cpm",
- "label": "Traffic",
- "unit": "rpm",
- "mqe": "service_cpm",
- "aggregation": "sum"
- },
- {
- "metric": "p99",
- "label": "p99",
- "unit": "ms",
- "mqe": "service_percentile{p='99'}",
- "aggregation": "avg"
- },
- {
- "metric": "sla",
- "label": "SLA",
- "unit": "%",
- "mqe": "service_sla/100",
- "aggregation": "avg"
- },
- {
- "metric": "err",
- "label": "err",
- "unit": "%",
- "mqe": "100 - service_sla/100",
- "aggregation": "avg"
- }
- ]
- },
- "overview": {
- "throughput": "cpm",
- "spark": "cpm"
- },
- "widgets": [
- {
- "id": "traffic",
- "title": "Traffic",
- "type": "card",
- "unit": "rpm",
- "expressions": [
- "avg(service_cpm)"
- ],
- "x": 0,
- "y": 0,
- "w": 12,
- "h": 5
- },
- {
- "id": "sla",
- "title": "Success Rate",
- "type": "card",
- "unit": "%",
- "expressions": [
- "avg(service_sla)/100"
- ],
- "x": 12,
- "y": 0,
- "w": 12,
- "h": 5
- },
- {
- "id": "traffic_line",
- "title": "Traffic",
- "type": "line",
- "unit": "rpm",
- "expressions": [
- "service_cpm"
- ],
- "x": 0,
- "y": 5,
- "w": 12,
- "h": 12
- },
- {
- "id": "percentile",
- "title": "Response Time Percentile",
- "type": "line",
- "unit": "ms",
- "expressions": [
- "service_percentile{p='50'}",
- "service_percentile{p='95'}",
- "service_percentile{p='99'}"
- ],
- "x": 12,
- "y": 5,
- "w": 12,
- "h": 12
- }
- ]
-}
diff --git a/apps/bff/src/layers/config/mesh.json b/apps/bff/src/layers/config/mesh.json
deleted file mode 100644
index dffd4ff..0000000
--- a/apps/bff/src/layers/config/mesh.json
+++ /dev/null
@@ -1,125 +0,0 @@
-{
- "key": "MESH",
- "alias": "Service Mesh",
- "color": "var(--sw-info)",
- "aliases": {
- "services": "Services",
- "instances": "Sidecars",
- "endpoints": "Endpoints"
- },
- "components": {
- "service": true,
- "instances": true,
- "endpoints": true,
- "endpointDependency": true,
- "topology": true,
- "traces": true,
- "logs": true
- },
- "metrics": {
- "orderBy": "cpm",
- "columns": [
- {
- "metric": "cpm",
- "label": "Traffic",
- "unit": "rpm",
- "mqe": "service_cpm",
- "aggregation": "sum"
- },
- {
- "metric": "p99",
- "label": "p99",
- "unit": "ms",
- "mqe": "service_percentile{p='99'}",
- "aggregation": "avg"
- },
- {
- "metric": "sla",
- "label": "SLA",
- "unit": "%",
- "mqe": "service_sla/100",
- "aggregation": "avg"
- },
- {
- "metric": "err",
- "label": "err",
- "unit": "%",
- "mqe": "100 - service_sla/100",
- "aggregation": "avg"
- }
- ]
- },
- "overview": {
- "throughput": "cpm",
- "spark": "cpm"
- },
- "widgets": [
- {
- "id": "sla",
- "title": "Success Rate",
- "type": "card",
- "unit": "%",
- "expressions": [
- "avg(service_sla)/100"
- ],
- "x": 0,
- "y": 0,
- "w": 8,
- "h": 5
- },
- {
- "id": "traffic",
- "title": "Traffic",
- "type": "card",
- "unit": "rpm",
- "expressions": [
- "avg(service_cpm)"
- ],
- "x": 8,
- "y": 0,
- "w": 8,
- "h": 5
- },
- {
- "id": "p99",
- "title": "p99 latency",
- "type": "card",
- "unit": "ms",
- "expressions": [
- "service_percentile{p='99'}"
- ],
- "x": 16,
- "y": 0,
- "w": 8,
- "h": 5
- },
- {
- "id": "traffic_line",
- "title": "Traffic",
- "type": "line",
- "unit": "rpm",
- "expressions": [
- "service_cpm"
- ],
- "x": 0,
- "y": 5,
- "w": 12,
- "h": 12
- },
- {
- "id": "percentile",
- "title": "Response Time Percentile",
- "type": "line",
- "unit": "ms",
- "expressions": [
- "service_percentile{p='50'}",
- "service_percentile{p='95'}",
- "service_percentile{p='99'}"
- ],
- "x": 12,
- "y": 5,
- "w": 12,
- "h": 12
- }
- ]
-}
diff --git a/apps/bff/src/layers/config/virtual_cache.json b/apps/bff/src/layers/config/virtual_cache.json
deleted file mode 100644
index a6c3bde..0000000
--- a/apps/bff/src/layers/config/virtual_cache.json
+++ /dev/null
@@ -1,75 +0,0 @@
-{
- "key": "VIRTUAL_CACHE",
- "alias": "Virtual Cache",
- "color": "var(--sw-warn)",
- "aliases": {
- "services": "Caches"
- },
- "components": {
- "service": true
- },
- "metrics": {
- "orderBy": "cpm",
- "columns": [
- {
- "metric": "cpm",
- "label": "Traffic",
- "unit": "rpm",
- "mqe": "service_cpm",
- "aggregation": "sum"
- },
- {
- "metric": "resp",
- "label": "resp",
- "unit": "ms",
- "mqe": "service_resp_time",
- "aggregation": "avg"
- }
- ]
- },
- "overview": {
- "throughput": "cpm",
- "spark": "cpm"
- },
- "widgets": [
- {
- "id": "traffic",
- "title": "Traffic",
- "type": "card",
- "unit": "rpm",
- "expressions": [
- "avg(service_cpm)"
- ],
- "x": 0,
- "y": 0,
- "w": 12,
- "h": 5
- },
- {
- "id": "resp",
- "title": "Avg Response Time",
- "type": "card",
- "unit": "ms",
- "expressions": [
- "avg(service_resp_time)"
- ],
- "x": 12,
- "y": 0,
- "w": 12,
- "h": 5
- },
- {
- "id": "traffic_line",
- "title": "Traffic",
- "type": "line",
- "unit": "rpm",
- "expressions": [
- "service_cpm"
- ],
- "x": 0,
- "y": 5,
- "w": 24,
- "h": 12
- }
- ]
-}
diff --git a/apps/bff/src/layers/config/virtual_database.json b/apps/bff/src/layers/config/virtual_database.json
deleted file mode 100644
index cf16f42..0000000
--- a/apps/bff/src/layers/config/virtual_database.json
+++ /dev/null
@@ -1,102 +0,0 @@
-{
- "key": "VIRTUAL_DATABASE",
- "alias": "Virtual Database",
- "color": "var(--sw-warn)",
- "aliases": {
- "services": "Databases"
- },
- "components": {
- "service": true
- },
- "metrics": {
- "orderBy": "cpm",
- "columns": [
- {
- "metric": "cpm",
- "label": "Traffic",
- "unit": "rpm",
- "mqe": "service_cpm",
- "aggregation": "sum"
- },
- {
- "metric": "resp",
- "label": "resp",
- "unit": "ms",
- "mqe": "service_resp_time",
- "aggregation": "avg"
- },
- {
- "metric": "p99",
- "label": "p99",
- "unit": "ms",
- "mqe": "service_percentile{p='99'}",
- "aggregation": "avg"
- },
- {
- "metric": "err",
- "label": "err",
- "unit": "%",
- "mqe": "100 - service_sla/100",
- "aggregation": "avg"
- }
- ]
- },
- "overview": {
- "throughput": "cpm",
- "spark": "cpm"
- },
- "widgets": [
- {
- "id": "traffic",
- "title": "Traffic",
- "type": "card",
- "unit": "rpm",
- "expressions": [
- "avg(service_cpm)"
- ],
- "x": 0,
- "y": 0,
- "w": 8,
- "h": 5
- },
- {
- "id": "resp",
- "title": "Avg Response Time",
- "type": "card",
- "unit": "ms",
- "expressions": [
- "avg(service_resp_time)"
- ],
- "x": 8,
- "y": 0,
- "w": 8,
- "h": 5
- },
- {
- "id": "sla",
- "title": "Success Rate",
- "type": "card",
- "unit": "%",
- "expressions": [
- "avg(service_sla)/100"
- ],
- "x": 16,
- "y": 0,
- "w": 8,
- "h": 5
- },
- {
- "id": "traffic_line",
- "title": "Traffic",
- "type": "line",
- "unit": "rpm",
- "expressions": [
- "service_cpm"
- ],
- "x": 0,
- "y": 5,
- "w": 24,
- "h": 12
- }
- ]
-}
diff --git a/apps/bff/src/layers/config/virtual_mq.json b/apps/bff/src/layers/config/virtual_mq.json
deleted file mode 100644
index 21bef7c..0000000
--- a/apps/bff/src/layers/config/virtual_mq.json
+++ /dev/null
@@ -1,74 +0,0 @@
-{
- "key": "VIRTUAL_MQ",
- "alias": "Virtual MQ",
- "color": "var(--sw-ok)",
- "aliases": {
- "services": "Queues"
- },
- "components": {
- "service": true
- },
- "metrics": {
- "orderBy": "cpm",
- "columns": [
- {
- "metric": "cpm",
- "label": "msg/s",
- "mqe": "service_cpm",
- "aggregation": "sum"
- },
- {
- "metric": "resp",
- "label": "consume",
- "unit": "ms",
- "mqe": "service_resp_time",
- "aggregation": "avg"
- }
- ]
- },
- "overview": {
- "throughput": "cpm",
- "spark": "cpm"
- },
- "widgets": [
- {
- "id": "traffic",
- "title": "Message rate",
- "type": "card",
- "unit": "msg / min",
- "expressions": [
- "avg(service_cpm)"
- ],
- "x": 0,
- "y": 0,
- "w": 12,
- "h": 5
- },
- {
- "id": "resp",
- "title": "Avg Consume Latency",
- "type": "card",
- "unit": "ms",
- "expressions": [
- "avg(service_resp_time)"
- ],
- "x": 12,
- "y": 0,
- "w": 12,
- "h": 5
- },
- {
- "id": "traffic_line",
- "title": "Message rate",
- "type": "line",
- "unit": "msg / min",
- "expressions": [
- "service_cpm"
- ],
- "x": 0,
- "y": 5,
- "w": 24,
- "h": 12
- }
- ]
-}
diff --git a/apps/bff/src/layers/loader.ts b/apps/bff/src/layers/loader.ts
index 74268ba..0f53a37 100644
--- a/apps/bff/src/layers/loader.ts
+++ b/apps/bff/src/layers/loader.ts
@@ -20,9 +20,9 @@
* presentation (alias, color, slots, enabled components) AND its data
* shape (landing card columns, dashboard widgets, MQE expressions).
*
- * The bundled defaults live under `./config/<key>.json`, one file per
- * OAP layer enum. Operator overrides land in the SetupStore (JSON file
- * on disk) and merge on top.
+ * The bundled defaults live under `../bundled_templates/layers/<key>.json`,
+ * one file per OAP layer enum. Operator overrides land in the SetupStore
+ * (JSON file on disk) and merge on top.
*
* Lifting these from TS code into JSON gets us:
* - One file per layer to review or copy
@@ -41,9 +41,10 @@ import type {
EndpointDependencyConfig,
TopologyConfig,
TopologyMetricDef,
+ TracesConfig,
} from '@skywalking-horizon-ui/api-client';
-export type { TopologyConfig, EndpointDependencyConfig, TopologyMetricDef };
+export type { TopologyConfig, EndpointDependencyConfig, TopologyMetricDef, TracesConfig };
export interface LayerComponentFlags {
service?: boolean;
@@ -190,6 +191,12 @@ export interface LayerTemplate {
/** API-dependency dashboard config — operator-editable node + edge MQE.
* When absent the loader fills it from {@link BOOSTER_ENDPOINT_DEP_DEFAULTS}. */
endpointDependency?: EndpointDependencyConfig;
+ /** Traces tab config. The `source` field picks which trace backend
+ * the UI's filter selector defaults to (`both` shows two parallel
+ * tables; `native` / `zipkin` pin to one). Default `both` when
+ * absent. The v2-vs-v3 split for native traces is decided at
+ * runtime by probing `hasQueryTracesV2Support`, not in this config. */
+ traces?: TracesConfig;
}
/**
@@ -255,7 +262,11 @@ export const BOOSTER_ENDPOINT_DEP_DEFAULTS: EndpointDependencyConfig = {
};
const __dirname = dirname(fileURLToPath(import.meta.url));
-const CONFIG_DIR = join(__dirname, 'config');
+// Bundled layer JSONs live alongside other static templates under
+// `apps/bff/src/bundled_templates/`. In the long run these should be
+// served by OAP; for now they ship inside the BFF so a fresh deploy
+// renders something sensible without any operator setup.
+const CONFIG_DIR = join(__dirname, '..', 'bundled_templates', 'layers');
let cache: Map<string, LayerTemplate> | null = null;
@@ -456,6 +467,13 @@ export function endpointDependencyConfigFor(
return BOOSTER_ENDPOINT_DEP_DEFAULTS;
}
+/** Resolve the traces tab config. Defaults to surfacing both
+ * SkyWalking-native and Zipkin trace lists side-by-side. */
+export function tracesConfigFor(template: LayerTemplate | null): TracesConfig {
+ if (template?.traces) return template.traces;
+ return { source: 'both' };
+}
+
/**
* Persist an operator-edited template back to its JSON file. Validates
* the basic shape, sorts keys for stable diffs, then refreshes the
diff --git a/apps/bff/src/oap/clients.ts b/apps/bff/src/oap/clients.ts
index 8838928..12af234 100644
--- a/apps/bff/src/oap/clients.ts
+++ b/apps/bff/src/oap/clients.ts
@@ -72,31 +72,41 @@ export function buildOapClients(
): OapClients {
const fetch = opts.fetch;
const primaryUrl = config.oap.adminUrls[0]!;
- // Single source of truth for per-call timeout — every client
- // constructed via this factory inherits it. 0 means no timeout
- // (passed through verbatim to the clients).
const timeoutMs = config.oap.timeoutMs;
+ // If config.oap.auth is set, every constructed client gets the
+ // basic-auth Authorization header so all upstream calls (status,
+ // runtime-rule, OAL catalog, DSL debug, inspect) authenticate.
+ const headers: Record<string, string> | undefined = config.oap.auth
+ ? {
+ authorization:
+ 'Basic ' +
+ Buffer.from(
+ `${config.oap.auth.username}:${config.oap.auth.password}`,
+ 'utf8',
+ ).toString('base64'),
+ }
+ : undefined;
return {
forUrl(adminUrl: string): RuntimeRuleClient {
- return new RuntimeRuleClient({ adminUrl, fetch, timeoutMs });
+ return new RuntimeRuleClient({ adminUrl, fetch, timeoutMs, headers });
},
primary(): RuntimeRuleClient {
- return new RuntimeRuleClient({ adminUrl: primaryUrl, fetch, timeoutMs });
+ return new RuntimeRuleClient({ adminUrl: primaryUrl, fetch, timeoutMs, headers });
},
status(): StatusClient {
- return new StatusClient({ statusUrl: config.oap.statusUrl, fetch, timeoutMs });
+ return new StatusClient({ statusUrl: config.oap.statusUrl, fetch, timeoutMs, headers });
},
oal(): OalClient {
- return new OalClient({ adminUrl: primaryUrl, fetch, timeoutMs });
+ return new OalClient({ adminUrl: primaryUrl, fetch, timeoutMs, headers });
},
debug(): DslDebuggingClient {
- return new DslDebuggingClient({ adminUrl: primaryUrl, fetch, timeoutMs });
+ return new DslDebuggingClient({ adminUrl: primaryUrl, fetch, timeoutMs, headers });
},
debugForUrl(adminUrl: string): DslDebuggingClient {
- return new DslDebuggingClient({ adminUrl, fetch, timeoutMs });
+ return new DslDebuggingClient({ adminUrl, fetch, timeoutMs, headers });
},
inspect(): InspectClient {
- return new InspectClient({ adminUrl: primaryUrl, fetch, timeoutMs });
+ return new InspectClient({ adminUrl: primaryUrl, fetch, timeoutMs, headers });
},
adminUrls(): readonly string[] {
return config.oap.adminUrls;
diff --git a/apps/bff/src/oap/endpoint-dependency-routes.ts b/apps/bff/src/oap/endpoint-dependency-routes.ts
index 24b3183..ec7d186 100644
--- a/apps/bff/src/oap/endpoint-dependency-routes.ts
+++ b/apps/bff/src/oap/endpoint-dependency-routes.ts
@@ -40,7 +40,7 @@ import type {
TopologyMetricDef,
} from '@skywalking-horizon-ui/api-client';
import { requireAuth } from '../auth/middleware.js';
-import { graphqlPost } from './graphql-client.js';
+import { graphqlPost, buildOapOpts } from './graphql-client.js';
import { endpointDependencyConfigFor, getLayerTemplate } from '../layers/loader.js';
export interface EndpointDependencyRouteDeps {
@@ -265,11 +265,7 @@ export function registerEndpointDependencyRoute(
const epCfg: EndpointDependencyConfig = endpointDependencyConfigFor(template);
const cfgCurrent = deps.config.current;
- const opts = {
- statusUrl: cfgCurrent.oap.statusUrl,
- timeoutMs: cfgCurrent.oap.timeoutMs,
- fetch: deps.fetch,
- };
+ const opts = buildOapOpts(cfgCurrent, deps.fetch);
const window = defaultWindow();
const oapLayer = layerKey.toUpperCase();
const durationVar = { start: window.start, end: window.end, step: 'MINUTE' };
@@ -399,22 +395,38 @@ export function registerEndpointDependencyRoute(
}
// ── Per-edge MQE under EndpointRelation. We also capture the
- // per-bucket series so the UI's edge sidebar can draw sparklines
- // instead of just printing scalars.
+ // per-bucket series so the UI's edge sidebar can draw sparklines.
+ //
+ // Endpoint relation metrics are SERVER-SIDE only (OAP has no
+ // `endpoint_relation_client_*` family) — they live on the
+ // callee that records the incoming call. So an edge is
+ // queryable when the DEST endpoint is real + named, regardless
+ // of whether the source is real (e.g. `User → consumer` edges
+ // still produce server-side numbers). When the source is
+ // virtual we use a synthetic source service name and
+ // `sourceNormal: false`, which is what booster does too.
const linkMetrics = epCfg.linkMetrics ?? [];
const edgeMetricVals = new Map<string, Record<string, number | null>>();
const edgeMetricSeries = new Map<string, Record<string, Array<number | null> | null>>();
const realEndpointMap = new Map(realNodes.map((n) => [n.id, n]));
- const realEdges = graph.calls.filter(
- (c) => realEndpointMap.has(c.source) && realEndpointMap.has(c.target),
- );
- if (realEdges.length > 0 && linkMetrics.length > 0) {
+ const nodeById = new Map(graph.nodes.map((n) => [n.id, n]));
+ const candidateEdges = graph.calls.filter((c) => {
+ const dst = nodeById.get(c.target);
+ return !!dst && dst.isReal && !!dst.name && !!dst.serviceName;
+ });
+ if (candidateEdges.length > 0 && linkMetrics.length > 0) {
const fragments: string[] = [];
const aliasMap = new Map<string, { callId: string; metric: TopologyMetricDef }>();
- realEdges.forEach((c, i) => {
- const src = realEndpointMap.get(c.source)!;
- const dst = realEndpointMap.get(c.target)!;
- const srcNormal = src.serviceId === serviceId ? normal : true;
+ candidateEdges.forEach((c, i) => {
+ const dst = realEndpointMap.get(c.target) ?? nodeById.get(c.target)!;
+ const src = nodeById.get(c.source);
+ // Source may be a synthetic node (`User`, `localhost:-1`).
+ // Booster's pattern: pass the source's name + serviceName
+ // through, and set `sourceNormal = isReal`. OAP accepts
+ // virtual sources for endpoint-relation queries.
+ const srcName = src?.name || 'User';
+ const srcServiceName = src?.serviceName || 'User';
+ const srcNormal = src ? src.isReal : false;
const dstNormal = dst.serviceId === serviceId ? normal : true;
linkMetrics.forEach((m, j) => {
const alias = `r${i}_${j}`;
@@ -423,8 +435,8 @@ export function registerEndpointDependencyRoute(
endpointRelationFragment(
alias,
m,
- src.serviceName,
- src.name,
+ srcServiceName,
+ srcName,
srcNormal,
dst.serviceName,
dst.name,
diff --git a/apps/bff/src/oap/endpoint-routes.ts b/apps/bff/src/oap/endpoint-routes.ts
index e40d7fc..6d6213e 100644
--- a/apps/bff/src/oap/endpoint-routes.ts
+++ b/apps/bff/src/oap/endpoint-routes.ts
@@ -38,7 +38,7 @@ import type { ConfigSource } from '../config/loader.js';
import type { SessionStore } from '../auth/sessions.js';
import type { FetchLike } from '@skywalking-horizon-ui/api-client';
import { requireAuth } from '../auth/middleware.js';
-import { graphqlPost } from './graphql-client.js';
+import { graphqlPost, buildOapOpts } from './graphql-client.js';
export interface EndpointRouteDeps {
config: ConfigSource;
@@ -115,11 +115,7 @@ export function registerEndpointRoute(app: FastifyInstance, deps: EndpointRouteD
const limit = Math.max(20, Math.min(50, Number(q.limit) || 20));
const cfgCurrent = deps.config.current;
- const opts = {
- statusUrl: cfgCurrent.oap.statusUrl,
- timeoutMs: cfgCurrent.oap.timeoutMs,
- fetch: deps.fetch,
- };
+ const opts = buildOapOpts(cfgCurrent, deps.fetch);
const window = defaultWindow();
// Resolve a plain service name to an OAP id when needed (id-shaped
diff --git a/apps/bff/src/oap/graphql-client.ts b/apps/bff/src/oap/graphql-client.ts
index 5b0833a..e90cbb0 100644
--- a/apps/bff/src/oap/graphql-client.ts
+++ b/apps/bff/src/oap/graphql-client.ts
@@ -16,11 +16,43 @@
*/
import type { FetchLike } from '@skywalking-horizon-ui/api-client';
+import type { HorizonConfig } from '../config/schema.js';
export interface GraphqlOptions {
statusUrl: string;
timeoutMs: number;
fetch?: FetchLike;
+ /** Optional basic-auth credentials for outbound OAP calls. When
+ * set, the client adds an `Authorization: Basic <b64>` header. */
+ auth?: { username: string; password: string };
+}
+
+/**
+ * Build a {@link GraphqlOptions}-shaped object from the live config
+ * plus an injected fetch. Use this in every route to keep `auth`,
+ * `timeoutMs`, etc. flowing consistently from `horizon.yaml`.
+ */
+export function buildOapOpts(
+ cfg: HorizonConfig,
+ fetch?: FetchLike,
+): GraphqlOptions {
+ return {
+ statusUrl: cfg.oap.statusUrl,
+ timeoutMs: cfg.oap.timeoutMs,
+ auth: cfg.oap.auth,
+ fetch,
+ };
+}
+
+/** Build a basic-auth header value from a username/password pair. */
+export function basicAuthHeader(username: string, password: string): string {
+ // btoa handles ASCII; encode utf-8 first for safety.
+ const raw = `${username}:${password}`;
+ const b64 = typeof Buffer !== 'undefined'
+ ? Buffer.from(raw, 'utf8').toString('base64')
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ : (globalThis as any).btoa(unescape(encodeURIComponent(raw)));
+ return `Basic ${b64}`;
}
export class GraphqlError extends Error {
@@ -51,11 +83,15 @@ export async function graphqlPost<T>(
const url = opts.statusUrl.replace(/\/$/, '') + '/graphql';
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), opts.timeoutMs);
+ const headers: Record<string, string> = { 'content-type': 'application/json' };
+ if (opts.auth) {
+ headers.authorization = basicAuthHeader(opts.auth.username, opts.auth.password);
+ }
let res: Response;
try {
res = await f(url, {
method: 'POST',
- headers: { 'content-type': 'application/json' },
+ headers,
body: JSON.stringify({ query, variables: variables ?? {} }),
signal: controller.signal,
});
diff --git a/apps/bff/src/oap/info-routes.ts b/apps/bff/src/oap/info-routes.ts
index cca0c6e..39941eb 100644
--- a/apps/bff/src/oap/info-routes.ts
+++ b/apps/bff/src/oap/info-routes.ts
@@ -20,7 +20,7 @@ import type { FetchLike } from '@skywalking-horizon-ui/api-client';
import type { ConfigSource } from '../config/loader.js';
import type { SessionStore } from '../auth/sessions.js';
import { requireAuth } from '../auth/middleware.js';
-import { graphqlPost } from './graphql-client.js';
+import { buildOapOpts, graphqlPost } from './graphql-client.js';
/**
* One round-trip combining `version`, `getTimeInfo`, and `checkHealth`.
@@ -66,10 +66,7 @@ export function registerOapInfoRoute(app: FastifyInstance, deps: InfoRouteDeps):
const cfg = deps.config.current;
const statusUrl = cfg.oap.statusUrl;
try {
- const raw = await graphqlPost<InfoRaw>(
- { statusUrl, timeoutMs: cfg.oap.timeoutMs, fetch: deps.fetch },
- INFO_QUERY,
- );
+ const raw = await graphqlPost<InfoRaw>(buildOapOpts(cfg, deps.fetch), INFO_QUERY);
const body: OapInfo = {
reachable: true,
statusUrl,
diff --git a/apps/bff/src/oap/instance-routes.ts b/apps/bff/src/oap/instance-routes.ts
index 2464d71..8b62d71 100644
--- a/apps/bff/src/oap/instance-routes.ts
+++ b/apps/bff/src/oap/instance-routes.ts
@@ -36,7 +36,7 @@ import type { ConfigSource } from '../config/loader.js';
import type { SessionStore } from '../auth/sessions.js';
import type { FetchLike } from '@skywalking-horizon-ui/api-client';
import { requireAuth } from '../auth/middleware.js';
-import { graphqlPost } from './graphql-client.js';
+import { graphqlPost, buildOapOpts } from './graphql-client.js';
export interface InstanceRouteDeps {
config: ConfigSource;
@@ -128,11 +128,7 @@ export function registerInstanceRoute(app: FastifyInstance, deps: InstanceRouteD
return reply.code(400).send({ error: 'missing_service' });
}
const cfgCurrent = deps.config.current;
- const opts = {
- statusUrl: cfgCurrent.oap.statusUrl,
- timeoutMs: cfgCurrent.oap.timeoutMs,
- fetch: deps.fetch,
- };
+ const opts = buildOapOpts(cfgCurrent, deps.fetch);
const window = defaultWindow();
// OAP-side ids look like base64-ish blobs (e.g. "Y2hlY2tvdXQ=.1");
// names are plain words. If we don't see a `.` separator, treat
diff --git a/apps/bff/src/oap/landing-routes.ts b/apps/bff/src/oap/landing-routes.ts
index d8cad1b..1badbad 100644
--- a/apps/bff/src/oap/landing-routes.ts
+++ b/apps/bff/src/oap/landing-routes.ts
@@ -48,7 +48,7 @@ import type {
import type { ConfigSource } from '../config/loader.js';
import type { SessionStore } from '../auth/sessions.js';
import { requireAuth } from '../auth/middleware.js';
-import { graphqlPost } from './graphql-client.js';
+import { graphqlPost, buildOapOpts } from './graphql-client.js';
import { expressionForServiceMetricSeries } from './mqe-catalog.js';
export interface LandingRouteDeps {
@@ -270,11 +270,7 @@ export function registerLandingRoute(app: FastifyInstance, deps: LandingRouteDep
const cfg = parsed.data;
const oapLayer = layerKey.toUpperCase();
const cfgCurrent = deps.config.current;
- const opts = {
- statusUrl: cfgCurrent.oap.statusUrl,
- timeoutMs: cfgCurrent.oap.timeoutMs,
- fetch: deps.fetch,
- };
+ const opts = buildOapOpts(cfgCurrent, deps.fetch);
const window = defaultWindow();
// Step 1 — service list.
diff --git a/apps/bff/src/oap/log-routes.ts b/apps/bff/src/oap/log-routes.ts
new file mode 100644
index 0000000..6fd5578
--- /dev/null
+++ b/apps/bff/src/oap/log-routes.ts
@@ -0,0 +1,313 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * `POST /api/layer/:key/logs`
+ *
+ * Wraps OAP's `queryLogs(LogQueryCondition)`. Body shape is the
+ * `LogQueryRequest` from `@skywalking-horizon-ui/api-client/logs`.
+ *
+ * Tag filters + content keyword filters are AND-joined server-side.
+ * We accept a `service` name on the body so the SPA doesn't have to
+ * pre-resolve names → ids; mirror of the topology + endpoint feeds.
+ *
+ * Returns at most one page of logs plus the OAP-reported total so
+ * the UI's "page N of M" + density histogram can scope correctly.
+ */
+
+import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';
+import type {
+ FetchLike,
+ LogFacetsResponse,
+ LogKeyValue,
+ LogQueryRequest,
+ LogRow,
+ LogsResponse,
+} from '@skywalking-horizon-ui/api-client';
+import type { ConfigSource } from '../config/loader.js';
+import type { SessionStore } from '../auth/sessions.js';
+import { requireAuth } from '../auth/middleware.js';
+import { graphqlPost, buildOapOpts, type GraphqlOptions } from './graphql-client.js';
+
+export interface LogRouteDeps {
+ config: ConfigSource;
+ sessions: SessionStore;
+ fetch?: FetchLike;
+}
+
+const DEFAULT_WINDOW_MIN = 30;
+function fmtMinute(d: Date): string {
+ const yyyy = d.getUTCFullYear();
+ const mm = String(d.getUTCMonth() + 1).padStart(2, '0');
+ const dd = String(d.getUTCDate()).padStart(2, '0');
+ const HH = String(d.getUTCHours()).padStart(2, '0');
+ const MM = String(d.getUTCMinutes()).padStart(2, '0');
+ return `${yyyy}-${mm}-${dd} ${HH}${MM}`;
+}
+function defaultWindow(): { start: string; end: string } {
+ const end = new Date();
+ const start = new Date(end.getTime() - DEFAULT_WINDOW_MIN * 60_000);
+ return { start: fmtMinute(start), end: fmtMinute(end) };
+}
+
+const LIST_SERVICES_FOR_RESOLVE = /* GraphQL */ `
+ query ListServicesForLogs($layer: String!) {
+ services: listServices(layer: $layer) {
+ id
+ name
+ }
+ }
+`;
+
+const QUERY_LOGS = /* GraphQL */ `
+ query QueryLogs($condition: LogQueryCondition) {
+ data: queryLogs(condition: $condition) {
+ logs {
+ serviceName
+ serviceId
+ serviceInstanceName
+ serviceInstanceId
+ endpointName
+ endpointId
+ traceId
+ timestamp
+ contentType
+ content
+ tags { key value }
+ }
+ total
+ }
+ }
+`;
+
+interface OapLogRow {
+ serviceName?: string | null;
+ serviceId?: string | null;
+ serviceInstanceName?: string | null;
+ serviceInstanceId?: string | null;
+ endpointName?: string | null;
+ endpointId?: string | null;
+ traceId?: string | null;
+ timestamp: number;
+ contentType: string;
+ content: string;
+ tags?: LogKeyValue[] | null;
+}
+
+async function resolveServiceId(
+ opts: GraphqlOptions,
+ layer: string,
+ serviceArg: string,
+): Promise<string | null> {
+ if (!serviceArg) return null;
+ if (serviceArg.includes('.') && !/\s/.test(serviceArg)) return serviceArg;
+ const data = await graphqlPost<{ services: Array<{ id: string; name: string }> }>(
+ opts,
+ LIST_SERVICES_FOR_RESOLVE,
+ { layer: layer.toUpperCase() },
+ );
+ return (
+ data.services.find((s) => s.name === serviceArg)?.id ??
+ data.services.find((s) => s.id === serviceArg)?.id ??
+ null
+ );
+}
+
+interface LogBody extends LogQueryRequest {
+ service?: string;
+}
+
+export function registerLogRoute(app: FastifyInstance, deps: LogRouteDeps): void {
+ const auth = requireAuth(deps);
+ app.post(
+ '/api/layer/:key/logs',
+ { preHandler: auth },
+ async (req: FastifyRequest, reply: FastifyReply) => {
+ const params = req.params as { key: string };
+ const layerKey = params.key;
+ if (!layerKey || !/^[a-z0-9_]+$/i.test(layerKey)) {
+ return reply.code(400).send({ error: 'invalid_layer_key' });
+ }
+ const body = (req.body ?? {}) as LogBody;
+ const opts = buildOapOpts(deps.config.current, deps.fetch);
+ const window = defaultWindow();
+
+ // Resolve a service NAME to an id if the caller used one.
+ let serviceId = body.serviceId ?? null;
+ if (!serviceId && body.service) {
+ try {
+ serviceId = await resolveServiceId(opts, layerKey, body.service);
+ } catch (err) {
+ return reply.send({
+ generatedAt: Date.now(),
+ query: body,
+ total: 0,
+ logs: [],
+ reachable: false,
+ error: err instanceof Error ? err.message : String(err),
+ } satisfies LogsResponse);
+ }
+ }
+ const condition = {
+ ...(serviceId ? { serviceId } : {}),
+ ...(body.serviceInstanceId ? { serviceInstanceId: body.serviceInstanceId } : {}),
+ ...(body.endpointId ? { endpointId: body.endpointId } : {}),
+ ...(body.traceId ? { relatedTrace: { traceId: body.traceId } } : {}),
+ ...(body.keywordsOfContent && body.keywordsOfContent.length > 0
+ ? { keywordsOfContent: body.keywordsOfContent }
+ : {}),
+ ...(body.tags && body.tags.length > 0 ? { tags: body.tags } : {}),
+ queryDuration: { start: window.start, end: window.end, step: 'MINUTE' },
+ paging: {
+ pageNum: body.page ?? 1,
+ pageSize: body.pageSize ?? 50,
+ },
+ };
+
+ try {
+ const env = await graphqlPost<{
+ data: { logs: OapLogRow[]; total: number };
+ }>(opts, QUERY_LOGS, { condition });
+ const logs: LogRow[] = (env.data?.logs ?? []).map((r) => ({
+ serviceName: r.serviceName ?? null,
+ serviceId: r.serviceId ?? null,
+ serviceInstanceName: r.serviceInstanceName ?? null,
+ serviceInstanceId: r.serviceInstanceId ?? null,
+ endpointName: r.endpointName ?? null,
+ endpointId: r.endpointId ?? null,
+ traceId: r.traceId ?? null,
+ timestamp: r.timestamp,
+ contentType: r.contentType,
+ content: r.content,
+ tags: r.tags ?? [],
+ }));
+ return reply.send({
+ generatedAt: Date.now(),
+ query: body,
+ total: env.data?.total ?? logs.length,
+ logs,
+ reachable: true,
+ } satisfies LogsResponse);
+ } catch (err) {
+ return reply.send({
+ generatedAt: Date.now(),
+ query: body,
+ total: 0,
+ logs: [],
+ reachable: false,
+ error: err instanceof Error ? err.message : String(err),
+ } satisfies LogsResponse);
+ }
+ },
+ );
+
+ /**
+ * POST /api/layer/:key/logs/facets
+ *
+ * Fetches a larger window-scoped sample (default 200 rows) just for
+ * facet aggregation. The UI calls this in parallel with the page
+ * fetch so the left-rail counts reflect the query window, not the
+ * displayed page.
+ */
+ app.post(
+ '/api/layer/:key/logs/facets',
+ { preHandler: auth },
+ async (req: FastifyRequest, reply: FastifyReply) => {
+ const params = req.params as { key: string };
+ const layerKey = params.key;
+ if (!layerKey || !/^[a-z0-9_]+$/i.test(layerKey)) {
+ return reply.code(400).send({ error: 'invalid_layer_key' });
+ }
+ const body = (req.body ?? {}) as LogBody & { sampleSize?: number };
+ const sampleSize = Math.max(50, Math.min(1000, body.sampleSize ?? 200));
+ const opts = buildOapOpts(deps.config.current, deps.fetch);
+ const window = defaultWindow();
+ let serviceId = body.serviceId ?? null;
+ if (!serviceId && body.service) {
+ try {
+ serviceId = await resolveServiceId(opts, layerKey, body.service);
+ } catch (err) {
+ return reply.send({
+ generatedAt: Date.now(),
+ total: 0,
+ sampled: 0,
+ level: { error: 0, warn: 0, info: 0, debug: 0, other: 0 },
+ services: [],
+ reachable: false,
+ error: err instanceof Error ? err.message : String(err),
+ } satisfies LogFacetsResponse);
+ }
+ }
+ const condition = {
+ ...(serviceId ? { serviceId } : {}),
+ ...(body.serviceInstanceId ? { serviceInstanceId: body.serviceInstanceId } : {}),
+ ...(body.endpointId ? { endpointId: body.endpointId } : {}),
+ ...(body.traceId ? { relatedTrace: { traceId: body.traceId } } : {}),
+ ...(body.keywordsOfContent && body.keywordsOfContent.length > 0
+ ? { keywordsOfContent: body.keywordsOfContent }
+ : {}),
+ // Facet sample intentionally ignores level/tag filters so the
+ // counts show the unfiltered distribution; the user picks a
+ // level from the breakdown.
+ queryDuration: { start: window.start, end: window.end, step: 'MINUTE' },
+ paging: { pageNum: 1, pageSize: sampleSize },
+ };
+
+ try {
+ const env = await graphqlPost<{
+ data: { logs: OapLogRow[]; total: number };
+ }>(opts, QUERY_LOGS, { condition });
+ const rows = env.data?.logs ?? [];
+ const level: LogFacetsResponse['level'] = { error: 0, warn: 0, info: 0, debug: 0, other: 0 };
+ const svcMap = new Map<string, number>();
+ for (const r of rows) {
+ const tag = (r.tags ?? []).find((t) => t.key.toLowerCase() === 'level');
+ const raw = (tag?.value ?? '').toLowerCase();
+ if (raw.includes('error') || raw === 'err' || raw === 'fatal') level.error++;
+ else if (raw.includes('warn')) level.warn++;
+ else if (raw.includes('info')) level.info++;
+ else if (raw.includes('debug') || raw.includes('trace')) level.debug++;
+ else level.other++;
+ const svc = r.serviceName ?? '(none)';
+ svcMap.set(svc, (svcMap.get(svc) ?? 0) + 1);
+ }
+ const services = Array.from(svcMap.entries())
+ .map(([name, count]) => ({ name, count }))
+ .sort((a, b) => b.count - a.count)
+ .slice(0, 12);
+ return reply.send({
+ generatedAt: Date.now(),
+ total: env.data?.total ?? rows.length,
+ sampled: rows.length,
+ level,
+ services,
+ reachable: true,
+ } satisfies LogFacetsResponse);
+ } catch (err) {
+ return reply.send({
+ generatedAt: Date.now(),
+ total: 0,
+ sampled: 0,
+ level: { error: 0, warn: 0, info: 0, debug: 0, other: 0 },
+ services: [],
+ reachable: false,
+ error: err instanceof Error ? err.message : String(err),
+ } satisfies LogFacetsResponse);
+ }
+ },
+ );
+}
diff --git a/apps/bff/src/oap/menu-routes.ts b/apps/bff/src/oap/menu-routes.ts
index b0432bf..29fef8a 100644
--- a/apps/bff/src/oap/menu-routes.ts
+++ b/apps/bff/src/oap/menu-routes.ts
@@ -26,7 +26,7 @@ import type {
import type { ConfigSource } from '../config/loader.js';
import type { SessionStore } from '../auth/sessions.js';
import { requireAuth } from '../auth/middleware.js';
-import { graphqlPost } from './graphql-client.js';
+import { buildOapOpts, graphqlPost, type GraphqlOptions } from './graphql-client.js';
import { getLayerTemplate, type LayerComponentFlags } from '../layers/loader.js';
/**
@@ -212,7 +212,7 @@ function deriveLayer(
*/
async function fetchCountsForLayers(
layers: readonly string[],
- opts: { statusUrl: string; timeoutMs: number; fetch?: FetchLike },
+ opts: GraphqlOptions,
): Promise<Map<string, number>> {
const map = new Map<string, number>();
if (layers.length === 0) return map;
@@ -238,7 +238,7 @@ export function registerMenuRoute(app: FastifyInstance, deps: MenuRouteDeps): vo
app.get('/api/menu', { preHandler: auth }, async (_req: FastifyRequest, reply: FastifyReply) => {
const cfg = deps.config.current;
const statusUrl = cfg.oap.statusUrl;
- const opts = { statusUrl, timeoutMs: cfg.oap.timeoutMs, fetch: deps.fetch };
+ const opts = buildOapOpts(cfg, deps.fetch);
try {
const raw = await graphqlPost<MenuRaw>(opts, MENU_QUERY);
diff --git a/apps/bff/src/oap/server-time.ts b/apps/bff/src/oap/server-time.ts
index ab49e37..6a174cc 100644
--- a/apps/bff/src/oap/server-time.ts
+++ b/apps/bff/src/oap/server-time.ts
@@ -101,9 +101,18 @@ async function resolveServerTime(deps: ServerTimeDeps): Promise<ServerTime> {
fetch: deps.fetch,
});
mqeBaseUrl = target.baseUrl;
+ const headers: Record<string, string> = {
+ 'Content-Type': 'application/json',
+ Accept: 'application/json',
+ };
+ const auth = deps.config().oap.auth;
+ if (auth) {
+ headers.authorization =
+ 'Basic ' + Buffer.from(`${auth.username}:${auth.password}`, 'utf8').toString('base64');
+ }
const init: RequestInit = {
method: 'POST',
- headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
+ headers,
body: JSON.stringify({ query: GRAPHQL_QUERY }),
...(ctrl ? { signal: ctrl.signal } : {}),
};
diff --git a/apps/bff/src/oap/topology-routes.ts b/apps/bff/src/oap/topology-routes.ts
index 8d9681c..7ab9aa4 100644
--- a/apps/bff/src/oap/topology-routes.ts
+++ b/apps/bff/src/oap/topology-routes.ts
@@ -45,7 +45,7 @@ import type {
TopologyResponse,
} from '@skywalking-horizon-ui/api-client';
import { requireAuth } from '../auth/middleware.js';
-import { graphqlPost } from './graphql-client.js';
+import { graphqlPost, buildOapOpts } from './graphql-client.js';
import { getLayerTemplate, topologyConfigFor } from '../layers/loader.js';
export interface TopologyRouteDeps {
@@ -268,11 +268,7 @@ export function registerTopologyRoute(app: FastifyInstance, deps: TopologyRouteD
const topoCfg: TopologyConfig = topologyConfigFor(template);
const cfgCurrent = deps.config.current;
- const opts = {
- statusUrl: cfgCurrent.oap.statusUrl,
- timeoutMs: cfgCurrent.oap.timeoutMs,
- fetch: deps.fetch,
- };
+ const opts = buildOapOpts(cfgCurrent, deps.fetch);
const window = defaultWindow();
const oapLayer = layerKey.toUpperCase();
const durationVar = { start: window.start, end: window.end, step: 'MINUTE' };
diff --git a/apps/bff/src/oap/trace-protocol-cache.ts b/apps/bff/src/oap/trace-protocol-cache.ts
new file mode 100644
index 0000000..b94398e
--- /dev/null
+++ b/apps/bff/src/oap/trace-protocol-cache.ts
@@ -0,0 +1,89 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Tiny per-process cache for the trace-protocol probe.
+ *
+ * OAP 9.6+ exposes `queryTraces` (v2) which returns the list AND
+ * the spans inline in a single roundtrip. Older OAP only exposes
+ * `queryBasicTraces` (v3) which returns trace summaries; the SPA
+ * fetches each detail via `queryTrace(traceId)`.
+ *
+ * The v2 query type carries a sibling boolean `hasQueryTracesV2Support`
+ * we can probe once and cache. Cached for 5 minutes so an OAP rollover
+ * or a redeploy is picked up without an explicit reload.
+ *
+ * The decision is per OAP target (keyed by `statusUrl`) — when the
+ * operator points horizon at a different cluster, the probe runs
+ * again.
+ */
+
+import { graphqlPost } from './graphql-client.js';
+import type { FetchLike } from '@skywalking-horizon-ui/api-client';
+
+export type TraceProtocol = 'v2' | 'v3';
+
+interface CacheEntry {
+ protocol: TraceProtocol;
+ expiresAt: number;
+}
+
+const CACHE_TTL_MS = 5 * 60_000;
+const cache = new Map<string, CacheEntry>();
+
+interface ProbeOpts {
+ statusUrl: string;
+ timeoutMs: number;
+ fetch?: FetchLike;
+ /** Optional basic-auth — same shape as GraphqlOptions so call
+ * sites can pass the same `opts` object through. */
+ auth?: { username: string; password: string };
+}
+
+const PROBE_QUERY = /* GraphQL */ `
+ query ProbeQueryTracesV2 {
+ hasQueryTracesV2Support
+ }
+`;
+
+/**
+ * Returns the protocol the BFF should use for trace queries against
+ * this OAP target. Probes once and caches; falls back to v3 on
+ * probe failure since v3's `queryBasicTraces` is supported by every
+ * shipped OAP version.
+ */
+export async function detectTraceProtocol(opts: ProbeOpts): Promise<TraceProtocol> {
+ const now = Date.now();
+ const cached = cache.get(opts.statusUrl);
+ if (cached && cached.expiresAt > now) return cached.protocol;
+ let protocol: TraceProtocol = 'v3';
+ try {
+ const data = await graphqlPost<{ hasQueryTracesV2Support?: boolean }>(opts, PROBE_QUERY);
+ if (data.hasQueryTracesV2Support === true) protocol = 'v2';
+ } catch {
+ // Probe failed — older OAP doesn't have the field, GraphQL errors
+ // out. Cache v3 so we don't re-probe on every list call.
+ }
+ cache.set(opts.statusUrl, { protocol, expiresAt: now + CACHE_TTL_MS });
+ return protocol;
+}
+
+/** Force-invalidate the cache. Wired into the future "Refresh" admin
+ * affordance; not used by the runtime today. */
+export function invalidateTraceProtocolCache(): void {
+ cache.clear();
+}
diff --git a/apps/bff/src/oap/trace-routes.ts b/apps/bff/src/oap/trace-routes.ts
new file mode 100644
index 0000000..0dc7be3
--- /dev/null
+++ b/apps/bff/src/oap/trace-routes.ts
@@ -0,0 +1,472 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Per-layer Traces feed.
+ *
+ * POST /api/layer/:key/traces — list
+ * GET /api/trace/:traceId — detail by id (native or zipkin)
+ *
+ * The route is dual-source aware: when the layer's `traces.source`
+ * is `both` (default) or the operator explicitly asks for both via
+ * the query string, the BFF fans out to SkyWalking-native AND
+ * Zipkin in parallel and returns each backend's results on its own
+ * slot. The UI renders two tables side-by-side; there's no field
+ * mapping between the two — zipkin spans keep their zipkin shape.
+ *
+ * Native v2 vs v3 is auto-detected via {@link detectTraceProtocol}
+ * — the caller doesn't need to know which OAP version is answering.
+ */
+
+import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';
+import type {
+ FetchLike,
+ NativeSpan,
+ NativeTraceDetailResponse,
+ NativeTraceListResponse,
+ TraceDetailResponse,
+ TraceListResponse,
+ TraceQueryOrder,
+ TraceQueryState,
+ TraceSource,
+ TracesConfig,
+ ZipkinTraceDetailResponse,
+ ZipkinTraceListResponse,
+} from '@skywalking-horizon-ui/api-client';
+import type { ConfigSource } from '../config/loader.js';
+import type { SessionStore } from '../auth/sessions.js';
+import { requireAuth } from '../auth/middleware.js';
+import { graphqlPost, buildOapOpts, type GraphqlOptions } from './graphql-client.js';
+import { getLayerTemplate, tracesConfigFor } from '../layers/loader.js';
+import { detectTraceProtocol } from './trace-protocol-cache.js';
+import { zipkinFetchTraces, zipkinFetchTraceById, summariseZipkinTrace } from './zipkin-client.js';
+
+export interface TraceRouteDeps {
+ config: ConfigSource;
+ sessions: SessionStore;
+ fetch?: FetchLike;
+}
+
+const DEFAULT_WINDOW_MIN = 30;
+const MAX_WINDOW_MIN = 60 * 24 * 7; // 1 week guard
+function fmtMinute(d: Date): string {
+ const yyyy = d.getUTCFullYear();
+ const mm = String(d.getUTCMonth() + 1).padStart(2, '0');
+ const dd = String(d.getUTCDate()).padStart(2, '0');
+ const HH = String(d.getUTCHours()).padStart(2, '0');
+ const MM = String(d.getUTCMinutes()).padStart(2, '0');
+ return `${yyyy}-${mm}-${dd} ${HH}${MM}`;
+}
+function rollingWindow(minutes: number): { start: string; end: string } {
+ const m = Math.max(1, Math.min(MAX_WINDOW_MIN, Math.round(minutes)));
+ const end = new Date();
+ const start = new Date(end.getTime() - m * 60_000);
+ return { start: fmtMinute(start), end: fmtMinute(end) };
+}
+function explicitWindow(startIso: string, endIso: string): { start: string; end: string } | null {
+ const s = new Date(startIso);
+ const e = new Date(endIso);
+ if (!Number.isFinite(s.getTime()) || !Number.isFinite(e.getTime()) || e.getTime() < s.getTime()) {
+ return null;
+ }
+ return { start: fmtMinute(s), end: fmtMinute(e) };
+}
+
+// ── Wire request shape ─────────────────────────────────────────────
+
+interface TraceListBody {
+ source?: TraceSource;
+ service?: string;
+ serviceId?: string;
+ instanceId?: string;
+ endpointId?: string;
+ traceId?: string;
+ traceState?: TraceQueryState;
+ queryOrder?: TraceQueryOrder;
+ minTraceDuration?: number;
+ maxTraceDuration?: number;
+ pageNum?: number;
+ pageSize?: number;
+ /** Free-form span tags (`http.status_code=500`, …). Matches OAP's
+ * `TraceQueryCondition.tags: [KeyValue]`. */
+ tags?: Array<{ key: string; value: string }>;
+ /** Rolling window in minutes. Default 30; clamped to [1, 10080]. */
+ windowMinutes?: number;
+ /** Explicit ISO start (UTC). When both `start` and `end` are
+ * provided they override `windowMinutes`. */
+ start?: string;
+ /** Explicit ISO end (UTC). Pair with `start`. */
+ end?: string;
+}
+
+// ── Native GraphQL queries ────────────────────────────────────────
+
+const LIST_SERVICES_FOR_RESOLVE = /* GraphQL */ `
+ query ListServicesForTrace($layer: String!) {
+ services: listServices(layer: $layer) {
+ id
+ name
+ normal
+ }
+ }
+`;
+
+const QUERY_BASIC_TRACES_V3 = /* GraphQL */ `
+ query QueryBasicTraces($condition: TraceQueryCondition) {
+ data: queryBasicTraces(condition: $condition) {
+ traces {
+ key: segmentId
+ endpointNames
+ duration
+ start
+ isError
+ traceIds
+ }
+ }
+ }
+`;
+
+const QUERY_TRACES_V2 = /* GraphQL */ `
+ query QueryTracesV2($condition: TraceQueryCondition) {
+ data: queryTraces(condition: $condition) {
+ traces {
+ spans {
+ traceId
+ segmentId
+ spanId
+ parentSpanId
+ refs { traceId parentSegmentId parentSpanId type }
+ serviceCode
+ serviceInstanceName
+ startTime
+ endTime
+ endpointName
+ type
+ peer
+ component
+ isError
+ layer
+ tags { key value }
+ logs { time data { key value } }
+ attachedEvents {
+ startTime { seconds nanos }
+ event
+ endTime { seconds nanos }
+ tags { key value }
+ summary { key value }
+ }
+ }
+ }
+ }
+ }
+`;
+
+const QUERY_TRACE_DETAIL = /* GraphQL */ `
+ query QueryTrace($traceId: ID!) {
+ trace: queryTrace(traceId: $traceId) {
+ spans {
+ traceId
+ segmentId
+ spanId
+ parentSpanId
+ refs { traceId parentSegmentId parentSpanId type }
+ serviceCode
+ serviceInstanceName
+ startTime
+ endTime
+ endpointName
+ type
+ peer
+ component
+ isError
+ layer
+ tags { key value }
+ logs { time data { key value } }
+ attachedEvents {
+ startTime { seconds nanos }
+ event
+ endTime { seconds nanos }
+ tags { key value }
+ summary { key value }
+ }
+ }
+ }
+ }
+`;
+
+// ── Helpers ────────────────────────────────────────────────────────
+
+async function resolveServiceId(
+ opts: GraphqlOptions,
+ layer: string,
+ serviceArg: string,
+): Promise<string | null> {
+ if (!serviceArg) return null;
+ // Ids contain `.`; names rarely do. Short-circuit when it's already
+ // an id.
+ if (serviceArg.includes('.') && !/\s/.test(serviceArg)) return serviceArg;
+ const data = await graphqlPost<{
+ services: Array<{ id: string; name: string }>;
+ }>(opts, LIST_SERVICES_FOR_RESOLVE, { layer: layer.toUpperCase() });
+ return (
+ data.services.find((s) => s.name === serviceArg)?.id ??
+ data.services.find((s) => s.id === serviceArg)?.id ??
+ null
+ );
+}
+
+function buildTraceCondition(body: TraceListBody, resolvedServiceId: string | null, w: { start: string; end: string }) {
+ return {
+ ...(resolvedServiceId ? { serviceId: resolvedServiceId } : {}),
+ ...(body.instanceId ? { serviceInstanceId: body.instanceId } : {}),
+ ...(body.endpointId ? { endpointId: body.endpointId } : {}),
+ ...(body.traceId ? { traceId: body.traceId } : {}),
+ ...(body.tags && body.tags.length > 0 ? { tags: body.tags } : {}),
+ ...(typeof body.minTraceDuration === 'number' ? { minTraceDuration: body.minTraceDuration } : {}),
+ ...(typeof body.maxTraceDuration === 'number' ? { maxTraceDuration: body.maxTraceDuration } : {}),
+ queryDuration: { start: w.start, end: w.end, step: 'MINUTE' },
+ traceState: (body.traceState ?? 'ALL') as TraceQueryState,
+ queryOrder: (body.queryOrder ?? 'BY_START_TIME') as TraceQueryOrder,
+ paging: {
+ pageNum: body.pageNum ?? 1,
+ pageSize: body.pageSize ?? 20,
+ },
+ };
+}
+
+async function fetchNativeList(
+ opts: GraphqlOptions,
+ body: TraceListBody,
+ layerKey: string,
+): Promise<NativeTraceListResponse> {
+ const protocol = await detectTraceProtocol(opts);
+ // Explicit start+end takes precedence over windowMinutes; falling
+ // back to the rolling default when the explicit range is invalid.
+ const explicit = body.start && body.end ? explicitWindow(body.start, body.end) : null;
+ const window = explicit ?? rollingWindow(body.windowMinutes ?? DEFAULT_WINDOW_MIN);
+ let serviceId: string | null = null;
+ try {
+ serviceId = body.serviceId
+ ? body.serviceId
+ : body.service
+ ? await resolveServiceId(opts, layerKey, body.service)
+ : null;
+ } catch (err) {
+ return {
+ source: 'native',
+ protocol,
+ traces: [],
+ reachable: false,
+ error: err instanceof Error ? err.message : String(err),
+ };
+ }
+ const condition = buildTraceCondition(body, serviceId, window);
+ try {
+ if (protocol === 'v2') {
+ const env = await graphqlPost<{
+ data: { traces: Array<{ spans: NativeSpan[] }> };
+ }>(opts, QUERY_TRACES_V2, { condition });
+ const traces = (env.data?.traces ?? []).map((t) => {
+ const root = t.spans.find((s) => s.parentSpanId === -1) ?? t.spans[0];
+ const ids = Array.from(new Set(t.spans.map((s) => s.traceId)));
+ return {
+ key: root?.segmentId ?? ids[0] ?? '',
+ segmentId: root?.segmentId ?? '',
+ endpointNames: root ? [root.endpointName] : [],
+ duration: root ? root.endTime - root.startTime : 0,
+ start: root ? String(root.startTime) : '',
+ isError: t.spans.some((s) => s.isError),
+ traceIds: ids,
+ spans: t.spans,
+ };
+ });
+ return { source: 'native', protocol, traces, reachable: true };
+ }
+ const env = await graphqlPost<{
+ data: {
+ traces: Array<{
+ key: string;
+ endpointNames: string[];
+ duration: number;
+ start: string;
+ isError: boolean;
+ traceIds: string[];
+ }>;
+ };
+ }>(opts, QUERY_BASIC_TRACES_V3, { condition });
+ const traces = (env.data?.traces ?? []).map((t) => ({
+ key: t.key,
+ segmentId: t.key,
+ endpointNames: t.endpointNames,
+ duration: t.duration,
+ start: t.start,
+ isError: t.isError,
+ traceIds: t.traceIds,
+ }));
+ return { source: 'native', protocol, traces, reachable: true };
+ } catch (err) {
+ return {
+ source: 'native',
+ protocol,
+ traces: [],
+ reachable: false,
+ error: err instanceof Error ? err.message : String(err),
+ };
+ }
+}
+
+async function fetchZipkinList(
+ opts: GraphqlOptions,
+ body: TraceListBody,
+): Promise<ZipkinTraceListResponse> {
+ try {
+ const traces = await zipkinFetchTraces(opts, {
+ serviceName: body.service,
+ minDuration: body.minTraceDuration,
+ maxDuration: body.maxTraceDuration,
+ limit: body.pageSize ?? 20,
+ });
+ return { source: 'zipkin', traces, reachable: true };
+ } catch (err) {
+ return {
+ source: 'zipkin',
+ traces: [],
+ reachable: false,
+ error: err instanceof Error ? err.message : String(err),
+ };
+ }
+}
+
+// ── Routes ─────────────────────────────────────────────────────────
+
+export function registerTraceRoutes(app: FastifyInstance, deps: TraceRouteDeps): void {
+ const auth = requireAuth(deps);
+
+ app.post(
+ '/api/layer/:key/traces',
+ { preHandler: auth },
+ async (req: FastifyRequest, reply: FastifyReply) => {
+ const params = req.params as { key: string };
+ const layerKey = params.key;
+ if (!layerKey || !/^[a-z0-9_]+$/i.test(layerKey)) {
+ return reply.code(400).send({ error: 'invalid_layer_key' });
+ }
+ const body = (req.body ?? {}) as TraceListBody;
+ const template = getLayerTemplate(layerKey);
+ const tracesCfg: TracesConfig = tracesConfigFor(template);
+ const requestedSource: TraceSource = body.source ?? tracesCfg.source;
+ const opts = buildOapOpts(deps.config.current, deps.fetch);
+
+ const wantNative = requestedSource === 'both' || requestedSource === 'native';
+ const wantZipkin = requestedSource === 'both' || requestedSource === 'zipkin';
+ // Fan out in parallel; partial failures don't drop the whole
+ // response — the UI's empty / error states cover each slot.
+ const [native, zipkin] = await Promise.all([
+ wantNative ? fetchNativeList(opts, body, layerKey) : Promise.resolve(undefined),
+ wantZipkin ? fetchZipkinList(opts, body) : Promise.resolve(undefined),
+ ]);
+
+ const response: TraceListResponse = {
+ generatedAt: Date.now(),
+ source: requestedSource,
+ ...(native ? { native } : {}),
+ ...(zipkin ? { zipkin } : {}),
+ };
+ return reply.send(response);
+ },
+ );
+
+ app.get(
+ '/api/trace/:traceId',
+ { preHandler: auth },
+ async (req: FastifyRequest, reply: FastifyReply) => {
+ const params = req.params as { traceId: string };
+ const q = req.query as { source?: 'native' | 'zipkin' };
+ const source: 'native' | 'zipkin' = q.source === 'zipkin' ? 'zipkin' : 'native';
+ const opts = buildOapOpts(deps.config.current, deps.fetch);
+
+ if (source === 'native') {
+ const protocol = await detectTraceProtocol(opts);
+ try {
+ const env = await graphqlPost<{ trace: { spans: NativeSpan[] } }>(
+ opts,
+ QUERY_TRACE_DETAIL,
+ { traceId: params.traceId },
+ );
+ const detail: NativeTraceDetailResponse = {
+ source: 'native',
+ protocol,
+ traceId: params.traceId,
+ spans: env.trace?.spans ?? [],
+ reachable: true,
+ };
+ return reply.send({
+ generatedAt: Date.now(),
+ source,
+ native: detail,
+ } satisfies TraceDetailResponse);
+ } catch (err) {
+ const detail: NativeTraceDetailResponse = {
+ source: 'native',
+ protocol,
+ traceId: params.traceId,
+ spans: [],
+ reachable: false,
+ error: err instanceof Error ? err.message : String(err),
+ };
+ return reply.send({
+ generatedAt: Date.now(),
+ source,
+ native: detail,
+ } satisfies TraceDetailResponse);
+ }
+ }
+ // Zipkin.
+ try {
+ const spans = await zipkinFetchTraceById(opts, params.traceId);
+ const detail: ZipkinTraceDetailResponse = {
+ source: 'zipkin',
+ traceId: params.traceId,
+ spans,
+ reachable: true,
+ };
+ return reply.send({
+ generatedAt: Date.now(),
+ source,
+ zipkin: detail,
+ } satisfies TraceDetailResponse);
+ } catch (err) {
+ const detail: ZipkinTraceDetailResponse = {
+ source: 'zipkin',
+ traceId: params.traceId,
+ spans: [],
+ reachable: false,
+ error: err instanceof Error ? err.message : String(err),
+ };
+ return reply.send({
+ generatedAt: Date.now(),
+ source,
+ zipkin: detail,
+ } satisfies TraceDetailResponse);
+ }
+ },
+ );
+}
+
+// Re-export the summariser so future callers can consume it; unused
+// here but useful for tests.
+export { summariseZipkinTrace };
diff --git a/apps/bff/src/oap/trace-tag-routes.ts b/apps/bff/src/oap/trace-tag-routes.ts
new file mode 100644
index 0000000..86c1a12
--- /dev/null
+++ b/apps/bff/src/oap/trace-tag-routes.ts
@@ -0,0 +1,116 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Trace tag autocomplete routes:
+ * GET /api/trace-tags/keys?windowMinutes=30
+ * GET /api/trace-tags/values?key=<k>&windowMinutes=30
+ *
+ * Both wrap OAP's `queryTraceTagAutocompleteKeys` /
+ * `queryTraceTagAutocompleteValues` GraphQL endpoints. Same pattern
+ * as booster-ui's `ConditionTags.vue` — operator types in the trace
+ * tab's Tag input, the suggestions list narrows.
+ */
+
+import type { FetchLike } from '@skywalking-horizon-ui/api-client';
+import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';
+import type { ConfigSource } from '../config/loader.js';
+import type { SessionStore } from '../auth/sessions.js';
+import { requireAuth } from '../auth/middleware.js';
+import { buildOapOpts, graphqlPost } from './graphql-client.js';
+
+export interface TraceTagRouteDeps {
+ config: ConfigSource;
+ sessions: SessionStore;
+ fetch?: FetchLike;
+}
+
+const DEFAULT_WINDOW_MIN = 30;
+const MAX_WINDOW_MIN = 60 * 24 * 7;
+function fmtMinute(d: Date): string {
+ const pad = (n: number) => String(n).padStart(2, '0');
+ return `${d.getUTCFullYear()}-${pad(d.getUTCMonth() + 1)}-${pad(d.getUTCDate())} ${pad(d.getUTCHours())}${pad(d.getUTCMinutes())}`;
+}
+function rollingWindow(minutes: number): { start: string; end: string; step: 'MINUTE' } {
+ const m = Math.max(1, Math.min(MAX_WINDOW_MIN, Math.round(minutes)));
+ const end = new Date();
+ const start = new Date(end.getTime() - m * 60_000);
+ return { start: fmtMinute(start), end: fmtMinute(end), step: 'MINUTE' };
+}
+
+const KEYS_QUERY = /* GraphQL */ `
+ query TagKeys($duration: Duration!) {
+ keys: queryTraceTagAutocompleteKeys(duration: $duration)
+ }
+`;
+const VALUES_QUERY = /* GraphQL */ `
+ query TagValues($tagKey: String!, $duration: Duration!) {
+ values: queryTraceTagAutocompleteValues(tagKey: $tagKey, duration: $duration)
+ }
+`;
+
+export function registerTraceTagRoutes(app: FastifyInstance, deps: TraceTagRouteDeps): void {
+ const auth = requireAuth(deps);
+
+ app.get(
+ '/api/trace-tags/keys',
+ { preHandler: auth },
+ async (req: FastifyRequest, reply: FastifyReply) => {
+ const q = req.query as { windowMinutes?: string };
+ const m = q.windowMinutes ? Number(q.windowMinutes) : DEFAULT_WINDOW_MIN;
+ const duration = rollingWindow(Number.isFinite(m) ? m : DEFAULT_WINDOW_MIN);
+ const opts = buildOapOpts(deps.config.current, deps.fetch);
+ try {
+ const env = await graphqlPost<{ keys: string[] | null }>(opts, KEYS_QUERY, { duration });
+ return reply.send({ keys: env.keys ?? [], generatedAt: Date.now() });
+ } catch (err) {
+ return reply
+ .code(200)
+ .send({ keys: [], generatedAt: Date.now(), error: err instanceof Error ? err.message : String(err) });
+ }
+ },
+ );
+
+ app.get(
+ '/api/trace-tags/values',
+ { preHandler: auth },
+ async (req: FastifyRequest, reply: FastifyReply) => {
+ const q = req.query as { key?: string; windowMinutes?: string };
+ const key = (q.key ?? '').trim();
+ if (!key) return reply.code(400).send({ error: 'missing_key' });
+ const m = q.windowMinutes ? Number(q.windowMinutes) : DEFAULT_WINDOW_MIN;
+ const duration = rollingWindow(Number.isFinite(m) ? m : DEFAULT_WINDOW_MIN);
+ const opts = buildOapOpts(deps.config.current, deps.fetch);
+ try {
+ const env = await graphqlPost<{ values: string[] | null }>(opts, VALUES_QUERY, {
+ tagKey: key,
+ duration,
+ });
+ return reply.send({ key, values: env.values ?? [], generatedAt: Date.now() });
+ } catch (err) {
+ return reply
+ .code(200)
+ .send({
+ key,
+ values: [],
+ generatedAt: Date.now(),
+ error: err instanceof Error ? err.message : String(err),
+ });
+ }
+ },
+ );
+}
diff --git a/apps/bff/src/oap/zipkin-client.ts b/apps/bff/src/oap/zipkin-client.ts
new file mode 100644
index 0000000..eafd2f7
--- /dev/null
+++ b/apps/bff/src/oap/zipkin-client.ts
@@ -0,0 +1,166 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Tiny REST client for OAP's Zipkin Query plugin endpoints.
+ *
+ * Zipkin lives on the SAME host:port as OAP's GraphQL (the
+ * `oap.statusUrl`) but uses standard zipkin v2 paths instead of
+ * GraphQL:
+ *
+ * GET /api/v2/services
+ * GET /api/v2/spans
+ * GET /api/v2/traces?serviceName=&spanName=&minDuration=&maxDuration=&endTs=&lookback=&limit=
+ * GET /api/v2/trace/{traceId}
+ *
+ * Wire shape is the standard Zipkin v2 JSON span format. We surface
+ * it verbatim — no field mapping into the SkyWalking-native shape.
+ *
+ * Errors are bubbled as plain `Error` instances; the route handler
+ * wraps them into a soft-fail response so the parallel native fetch
+ * still surfaces results.
+ */
+
+import type {
+ FetchLike,
+ ZipkinSpan,
+ ZipkinTraceListRow,
+} from '@skywalking-horizon-ui/api-client';
+
+export interface ZipkinClientOpts {
+ statusUrl: string;
+ timeoutMs: number;
+ fetch?: FetchLike;
+ /** Optional basic-auth — same shape as the GraphQL client. */
+ auth?: { username: string; password: string };
+}
+
+export interface ZipkinTracesQuery {
+ serviceName?: string;
+ spanName?: string;
+ /** Microseconds. */
+ minDuration?: number;
+ /** Microseconds. */
+ maxDuration?: number;
+ /** Upper bound time in millis since epoch (defaults to now). */
+ endTs?: number;
+ /** Lookback in millis (defaults 30 min). */
+ lookback?: number;
+ limit?: number;
+}
+
+const DEFAULT_LOOKBACK_MS = 30 * 60_000;
+
+async function zipkinFetch<T>(opts: ZipkinClientOpts, path: string): Promise<T> {
+ const f = opts.fetch ?? globalThis.fetch.bind(globalThis);
+ const url = opts.statusUrl.replace(/\/$/, '') + path;
+ const controller = new AbortController();
+ const timer = setTimeout(() => controller.abort(), opts.timeoutMs);
+ const headers: Record<string, string> = { Accept: 'application/json' };
+ if (opts.auth) {
+ const raw = `${opts.auth.username}:${opts.auth.password}`;
+ const b64 = Buffer.from(raw, 'utf8').toString('base64');
+ headers.authorization = `Basic ${b64}`;
+ }
+ let res: Response;
+ try {
+ res = await f(url, {
+ method: 'GET',
+ headers,
+ signal: controller.signal,
+ });
+ } finally {
+ clearTimeout(timer);
+ }
+ if (!res.ok) {
+ const body = await res.text().catch(() => '');
+ throw new Error(`zipkin ${res.status} ${url} — ${body.slice(0, 200)}`);
+ }
+ return (await res.json()) as T;
+}
+
+/**
+ * Summarise a zipkin trace's span array into a list-row. Zipkin
+ * doesn't ship a "traces summary" endpoint; the list endpoint
+ * returns `Span[][]` (one inner array per trace), so we compute
+ * the headline fields ourselves from the root span.
+ */
+export function summariseZipkinTrace(spans: ZipkinSpan[]): ZipkinTraceListRow {
+ if (spans.length === 0) {
+ return {
+ traceId: '',
+ rootName: null,
+ rootService: null,
+ timestamp: null,
+ duration: null,
+ spanCount: 0,
+ errorCount: 0,
+ };
+ }
+ const root = spans.find((s) => !s.parentId) ?? spans[0];
+ let errorCount = 0;
+ for (const s of spans) {
+ if (s.tags && (s.tags.error !== undefined || s.tags['otel.status_code'] === 'ERROR')) {
+ errorCount += 1;
+ }
+ }
+ return {
+ traceId: root.traceId,
+ rootName: root.name ?? null,
+ rootService: root.localEndpoint?.serviceName ?? null,
+ timestamp: root.timestamp ?? null,
+ duration: root.duration ?? null,
+ spanCount: spans.length,
+ errorCount,
+ };
+}
+
+/**
+ * List traces matching the operator's filter. Zipkin's wire
+ * response is `Span[][]` (one inner array per trace); we summarise
+ * each entry into a single list-row for the UI table.
+ */
+export async function zipkinFetchTraces(
+ opts: ZipkinClientOpts,
+ query: ZipkinTracesQuery,
+): Promise<ZipkinTraceListRow[]> {
+ const qs = new URLSearchParams();
+ if (query.serviceName) qs.set('serviceName', query.serviceName);
+ if (query.spanName) qs.set('spanName', query.spanName);
+ if (typeof query.minDuration === 'number') qs.set('minDuration', String(query.minDuration));
+ if (typeof query.maxDuration === 'number') qs.set('maxDuration', String(query.maxDuration));
+ const endTs = query.endTs ?? Date.now();
+ const lookback = query.lookback ?? DEFAULT_LOOKBACK_MS;
+ qs.set('endTs', String(endTs));
+ qs.set('lookback', String(lookback));
+ qs.set('limit', String(query.limit ?? 20));
+ const path = `/api/v2/traces?${qs.toString()}`;
+ const arr = await zipkinFetch<ZipkinSpan[][]>(opts, path);
+ return arr.map(summariseZipkinTrace).filter((r) => r.traceId);
+}
+
+/** Fetch a single trace by id. Returns the full span array
+ * unmodified — the UI's zipkin waterfall renders zipkin fields
+ * natively (kind / annotations / localEndpoint / remoteEndpoint /
+ * tags map). */
+export async function zipkinFetchTraceById(
+ opts: ZipkinClientOpts,
+ traceId: string,
+): Promise<ZipkinSpan[]> {
+ const path = `/api/v2/trace/${encodeURIComponent(traceId)}`;
+ return zipkinFetch<ZipkinSpan[]>(opts, path);
+}
diff --git a/apps/bff/src/overview/loader.ts b/apps/bff/src/overview/loader.ts
new file mode 100644
index 0000000..680c07c
--- /dev/null
+++ b/apps/bff/src/overview/loader.ts
@@ -0,0 +1,120 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Overview-dashboard config loader. Reads every `*.json` under
+ * `../bundled_templates/overviews/`, validates the wire shape, and keeps
+ * the parsed dashboards in-memory. The loader is intentionally eager
+ * (all dashboards loaded at boot) — config files are small and the
+ * SPA hits dashboard endpoints often enough that on-demand parsing
+ * isn't worth the ceremony.
+ */
+
+import fs from 'node:fs';
+import path from 'node:path';
+import { fileURLToPath } from 'node:url';
+import type { OverviewDashboard } from '@skywalking-horizon-ui/api-client';
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
+// Bundled overview dashboards live under `bundled_templates/overviews/`
+// (sibling to `bundled_templates/layers/`). Co-locating the static
+// templates keeps the future "operator-editable / OAP-served" migration
+// to one directory swap.
+const CONFIG_DIR = path.join(__dirname, '..', 'bundled_templates', 'overviews');
+
+let cache: OverviewDashboard[] | null = null;
+
+function isString(v: unknown): v is string {
+ return typeof v === 'string' && v.length > 0;
+}
+
+function validate(raw: unknown, file: string): OverviewDashboard | null {
+ if (!raw || typeof raw !== 'object') {
+ console.warn(`overview/${file}: not an object, skipped`);
+ return null;
+ }
+ const r = raw as Record<string, unknown>;
+ if (!isString(r.id) || !isString(r.title) || !Array.isArray(r.widgets)) {
+ console.warn(`overview/${file}: missing id/title/widgets`);
+ return null;
+ }
+ const widgets = (r.widgets as unknown[]).filter((w): w is Record<string, unknown> => {
+ if (!w || typeof w !== 'object') return false;
+ const rec = w as Record<string, unknown>;
+ return (
+ isString(rec.id) &&
+ isString(rec.title) &&
+ isString(rec.layer) &&
+ (rec.type === 'service-count' || rec.type === 'metric' || rec.type === 'topology')
+ );
+ });
+ return {
+ id: r.id,
+ title: r.title,
+ description: isString(r.description) ? r.description : undefined,
+ widgets: widgets.map((w) => ({
+ id: w.id as string,
+ title: w.title as string,
+ tip: isString(w.tip) ? w.tip : undefined,
+ layer: w.layer as string,
+ type: w.type as 'service-count' | 'metric' | 'topology',
+ mqe: isString(w.mqe) ? w.mqe : undefined,
+ unit: isString(w.unit) ? w.unit : undefined,
+ aggregation: w.aggregation === 'sum' ? 'sum' : w.aggregation === 'avg' ? 'avg' : undefined,
+ span: typeof w.span === 'number' ? w.span : undefined,
+ rowSpan: typeof w.rowSpan === 'number' ? w.rowSpan : undefined,
+ })),
+ };
+}
+
+/**
+ * Load every dashboard config under `../bundled_templates/overviews/*.json`.
+ * Memoised after the first call; restart the BFF to pick up file changes
+ * (in line with the layer-template loader's behaviour).
+ */
+export function loadOverviewDashboards(): OverviewDashboard[] {
+ if (cache) return cache;
+ if (!fs.existsSync(CONFIG_DIR)) {
+ cache = [];
+ return cache;
+ }
+ const files = fs.readdirSync(CONFIG_DIR).filter((f) => f.endsWith('.json'));
+ const out: OverviewDashboard[] = [];
+ for (const f of files) {
+ try {
+ const raw = JSON.parse(fs.readFileSync(path.join(CONFIG_DIR, f), 'utf8'));
+ const dash = validate(raw, f);
+ if (dash) out.push(dash);
+ } catch (err) {
+ console.warn(`overview/${f}: parse error`, err instanceof Error ? err.message : err);
+ }
+ }
+ // Keep stable ordering by id so the SPA's tab strip doesn't flicker
+ // on reload.
+ out.sort((a, b) => a.id.localeCompare(b.id));
+ cache = out;
+ return cache;
+}
+
+/** Force a re-read on the next call. Used in tests. */
+export function invalidateOverviewCache(): void {
+ cache = null;
+}
+
+export function getOverviewDashboard(id: string): OverviewDashboard | null {
+ return loadOverviewDashboards().find((d) => d.id === id) ?? null;
+}
diff --git a/apps/bff/src/overview/routes.ts b/apps/bff/src/overview/routes.ts
new file mode 100644
index 0000000..1da30c9
--- /dev/null
+++ b/apps/bff/src/overview/routes.ts
@@ -0,0 +1,88 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Overview-dashboard routes.
+ *
+ * GET /api/overview/dashboards — list (id, title, count).
+ * GET /api/overview/dashboards/:id — full dashboard config.
+ *
+ * Widget DATA is fetched by the SPA on a per-widget basis through
+ * existing routes — service-count + topology hit
+ * `/api/layer/:key/landing` and `/api/layer/:key/topology` directly,
+ * and `metric` widgets evaluate their MQE through the dashboard data
+ * route. Keeping the overview route lean (config only) lets the SPA
+ * mix and match — e.g. a "Mesh service" dashboard widget reusing
+ * the same topology endpoint that the per-layer page hits.
+ */
+
+import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';
+import type {
+ OverviewDashboardListResponse,
+ OverviewDashboardResponse,
+} from '@skywalking-horizon-ui/api-client';
+import type { ConfigSource } from '../config/loader.js';
+import type { SessionStore } from '../auth/sessions.js';
+import { requireAuth } from '../auth/middleware.js';
+import { getOverviewDashboard, loadOverviewDashboards } from './loader.js';
+
+export interface OverviewRouteDeps {
+ config: ConfigSource;
+ sessions: SessionStore;
+}
+
+export function registerOverviewRoutes(app: FastifyInstance, deps: OverviewRouteDeps): void {
+ const auth = requireAuth(deps);
+
+ app.get('/api/overview/dashboards', { preHandler: auth }, async (_req, reply) => {
+ const dashboards = loadOverviewDashboards();
+ const body: OverviewDashboardListResponse = {
+ generatedAt: Date.now(),
+ dashboards: dashboards.map((d) => ({
+ id: d.id,
+ title: d.title,
+ description: d.description,
+ widgetCount: d.widgets.length,
+ })),
+ };
+ return reply.send(body);
+ });
+
+ app.get(
+ '/api/overview/dashboards/:id',
+ { preHandler: auth },
+ async (req: FastifyRequest, reply: FastifyReply) => {
+ const { id } = req.params as { id: string };
+ const dash = getOverviewDashboard(id);
+ if (!dash) {
+ const body: OverviewDashboardResponse = {
+ generatedAt: Date.now(),
+ dashboard: { id, title: id, widgets: [] },
+ reachable: false,
+ error: `unknown dashboard "${id}"`,
+ };
+ return reply.code(404).send(body);
+ }
+ const body: OverviewDashboardResponse = {
+ generatedAt: Date.now(),
+ dashboard: dash,
+ reachable: true,
+ };
+ return reply.send(body);
+ },
+ );
+}
diff --git a/apps/bff/src/server.ts b/apps/bff/src/server.ts
index 3909a2b..7e3d955 100644
--- a/apps/bff/src/server.ts
+++ b/apps/bff/src/server.ts
@@ -15,8 +15,11 @@
* limitations under the License.
*/
+import { existsSync } from 'node:fs';
+import { resolve as resolvePath } from 'node:path';
import Fastify from 'fastify';
import cookie from '@fastify/cookie';
+import fastifyStatic from '@fastify/static';
import { AuditLogger } from './audit/logger.js';
import { registerAuthRoutes } from './auth/routes.js';
import { SessionStore } from './auth/sessions.js';
@@ -27,10 +30,14 @@ import { registerEndpointDependencyRoute } from './oap/endpoint-dependency-route
import { registerOapInfoRoute } from './oap/info-routes.js';
import { registerInstanceRoute } from './oap/instance-routes.js';
import { registerLandingRoute } from './oap/landing-routes.js';
+import { registerLogRoute } from './oap/log-routes.js';
import { registerMenuRoute } from './oap/menu-routes.js';
import { registerOapRoutes } from './oap/routes.js';
import { registerPreflightRoutes } from './oap/preflight-routes.js';
import { registerTopologyRoute } from './oap/topology-routes.js';
+import { registerTraceRoutes } from './oap/trace-routes.js';
+import { registerTraceTagRoutes } from './oap/trace-tag-routes.js';
+import { registerOverviewRoutes } from './overview/routes.js';
import { registerSetupRoutes } from './setup/routes.js';
import { SetupStore } from './setup/store.js';
import { HttpError } from './errors.js';
@@ -72,11 +79,35 @@ registerInstanceRoute(app, { config: source, sessions });
registerEndpointRoute(app, { config: source, sessions });
registerTopologyRoute(app, { config: source, sessions });
registerEndpointDependencyRoute(app, { config: source, sessions });
+registerTraceRoutes(app, { config: source, sessions });
+registerTraceTagRoutes(app, { config: source, sessions });
+registerLogRoute(app, { config: source, sessions });
+registerOverviewRoutes(app, { config: source, sessions });
registerDashboardRoute(app, { config: source, sessions });
registerSetupRoutes(app, { config: source, sessions, audit, store: setupStore });
registerOapRoutes(app, { config: source, sessions, audit });
registerPreflightRoutes(app, { config: source, sessions });
+// Serve the built SPA out of the BFF when HORIZON_STATIC_DIR points at a
+// directory (Docker image layout: /app/static contains the Vite dist).
+// Local dev keeps using the Vite dev-server on :9091 so this is a no-op
+// when the env var is absent.
+const staticDir = process.env.HORIZON_STATIC_DIR
+ ? resolvePath(process.env.HORIZON_STATIC_DIR)
+ : null;
+if (staticDir && existsSync(staticDir)) {
+ await app.register(fastifyStatic, { root: staticDir, prefix: '/', wildcard: false });
+ // SPA fallback — anything that isn't an `/api/*` request and didn't match
+ // a built file falls through to index.html so client-side routing works.
+ app.setNotFoundHandler((req, reply) => {
+ if (req.url.startsWith('/api/')) {
+ return reply.code(404).send({ code: 'not_found', message: req.url });
+ }
+ return reply.sendFile('index.html');
+ });
+ logger.info({ staticDir }, 'serving SPA from static dir');
+}
+
app.get('/api/health', async () => ({
status: 'ok',
version: process.env.HORIZON_VERSION ?? '0.1.0',
diff --git a/apps/ui/src/api/client.ts b/apps/ui/src/api/client.ts
index 76a8c69..7b7cce2 100644
--- a/apps/ui/src/api/client.ts
+++ b/apps/ui/src/api/client.ts
@@ -19,14 +19,25 @@ import type {
DashboardConfig,
DashboardResponse,
DashboardWidget,
+ EndpointDependencyConfig,
EndpointDependencyResponse,
LandingConfig,
LandingResponse,
+ LogFacetsResponse,
+ LogQueryRequest,
+ LogsResponse,
MenuResponse,
OapInfo,
SetupResponse,
SetupSavePayload,
+ TopologyConfig,
TopologyResponse,
+ TraceDetailResponse,
+ TraceListResponse,
+ TraceQueryOrder,
+ TraceQueryState,
+ TraceSource,
+ TracesConfig,
} from '@skywalking-horizon-ui/api-client';
export type {
@@ -55,6 +66,33 @@ export type {
EndpointDependencyNode,
EndpointDependencyCall,
EndpointDependencyResponse,
+ TraceSource,
+ TracesConfig,
+ TraceKeyValue,
+ TraceLogEntry,
+ TraceAttachedEvent,
+ TraceRef,
+ NativeSpan,
+ NativeTraceListRow,
+ NativeTraceListResponse,
+ NativeTraceDetailResponse,
+ TraceQueryOrder,
+ TraceQueryState,
+ ZipkinEndpoint,
+ ZipkinAnnotation,
+ ZipkinKind,
+ ZipkinSpan,
+ ZipkinTraceListRow,
+ ZipkinTraceListResponse,
+ ZipkinTraceDetailResponse,
+ TraceListResponse,
+ TraceDetailResponse,
+ LogKeyValue,
+ LogRow,
+ LogTagFilter,
+ LogQueryRequest,
+ LogsResponse,
+ LogFacetsResponse,
} from '@skywalking-horizon-ui/api-client';
@@ -80,6 +118,9 @@ export interface AdminLayerTemplate {
traces?: boolean;
logs?: boolean;
profiling?: boolean;
+ traceProfiling?: boolean;
+ ebpfProfiling?: boolean;
+ asyncProfiling?: boolean;
};
metrics: {
orderBy?: string;
@@ -98,6 +139,14 @@ export interface AdminLayerTemplate {
spark?: string;
};
widgets: DashboardWidget[];
+ /** Operator-editable service-map config — node + server + client
+ * metric definitions, with optional thresholds. */
+ topology?: TopologyConfig;
+ /** Operator-editable API-dependency config — node + server-side line
+ * metrics (OAP has no client family for endpoint relations). */
+ endpointDependency?: EndpointDependencyConfig;
+ /** Per-layer trace source default. */
+ traces?: TracesConfig;
}
export class BffApiError extends Error {
@@ -295,6 +344,83 @@ export class BffClient {
);
}
+ /** List traces — fans out to SW-native and / or Zipkin based on
+ * the layer's config and the operator's override. */
+ layerTraces(
+ layerKey: string,
+ body: {
+ source?: TraceSource;
+ service?: string;
+ serviceId?: string;
+ instanceId?: string;
+ endpointId?: string;
+ traceId?: string;
+ traceState?: TraceQueryState;
+ queryOrder?: TraceQueryOrder;
+ minTraceDuration?: number;
+ maxTraceDuration?: number;
+ pageNum?: number;
+ pageSize?: number;
+ tags?: Array<{ key: string; value: string }>;
+ windowMinutes?: number;
+ start?: string;
+ end?: string;
+ } = {},
+ ): Promise<TraceListResponse> {
+ return this.request<TraceListResponse>(
+ 'POST',
+ `/api/layer/${encodeURIComponent(layerKey)}/traces`,
+ body,
+ );
+ }
+
+ /** Trace detail by id. `source=zipkin` swaps to the Zipkin REST
+ * path; default is native (v2/v3 picked automatically). */
+ traceDetail(traceId: string, source: 'native' | 'zipkin' = 'native'): Promise<TraceDetailResponse> {
+ const qs = new URLSearchParams({ source });
+ return this.request<TraceDetailResponse>(
+ 'GET',
+ `/api/trace/${encodeURIComponent(traceId)}?${qs.toString()}`,
+ );
+ }
+
+ /** Log query — body shape mirrors OAP's `LogQueryCondition`. */
+ layerLogs(layerKey: string, body: LogQueryRequest & { service?: string } = {}): Promise<LogsResponse> {
+ return this.request<LogsResponse>(
+ 'POST',
+ `/api/layer/${encodeURIComponent(layerKey)}/logs`,
+ body,
+ );
+ }
+
+ /** Tag autocomplete — list known tag keys for the duration window. */
+ traceTagKeys(windowMinutes: number = 30): Promise<{ keys: string[]; generatedAt: number; error?: string }> {
+ return this.request('GET', `/api/trace-tags/keys?windowMinutes=${windowMinutes}`);
+ }
+ /** Tag autocomplete — list values for a specific key. */
+ traceTagValues(
+ key: string,
+ windowMinutes: number = 30,
+ ): Promise<{ key: string; values: string[]; generatedAt: number; error?: string }> {
+ return this.request(
+ 'GET',
+ `/api/trace-tags/values?key=${encodeURIComponent(key)}&windowMinutes=${windowMinutes}`,
+ );
+ }
+
+ /** Log facets — same body as `layerLogs`, but returns aggregated
+ * level + service counts across a larger window-scoped sample. */
+ layerLogFacets(
+ layerKey: string,
+ body: LogQueryRequest & { service?: string; sampleSize?: number } = {},
+ ): Promise<LogFacetsResponse> {
+ return this.request<LogFacetsResponse>(
+ 'POST',
+ `/api/layer/${encodeURIComponent(layerKey)}/logs/facets`,
+ body,
+ );
+ }
+
/** List active instances for a service. The per-layer Instance
* dashboard surfaces a second selector below the service picker;
* this feeds it. Accepts the service id or name. */
diff --git a/apps/ui/src/components/charts/Sparkline.vue b/apps/ui/src/components/charts/Sparkline.vue
index 2affa81..6125aeb 100644
--- a/apps/ui/src/components/charts/Sparkline.vue
+++ b/apps/ui/src/components/charts/Sparkline.vue
@@ -35,27 +35,77 @@ const props = withDefaults(
color?: string;
/** Stroke width in px. */
stroke?: number;
+ /**
+ * When true, the SVG renders at `width: 100%` of its container
+ * and `width` is used purely as the internal coordinate space
+ * (paths still draw in that resolution; the rendered chart
+ * stretches to fill). Use this whenever the Sparkline sits in a
+ * flex/grid cell whose width is dynamic.
+ */
+ fluid?: boolean;
+ /**
+ * Shared crosshair bucket index (0..values.length-1). When set,
+ * draws a vertical hairline + a dot at that bucket so two
+ * sparklines in the same panel can stay synced on hover. The
+ * parent owns the state; we just render the indicator and emit
+ * `bucket-hover` so siblings can follow.
+ */
+ crosshairBucket?: number | null;
}>(),
{
width: 56,
height: 14,
color: 'var(--sw-accent)',
stroke: 1.25,
+ fluid: false,
+ crosshairBucket: null,
},
);
+const emit = defineEmits<{
+ /** Mouse moved over the chart; argument is the nearest bucket
+ * index. Parent panels use it to drive a shared crosshair across
+ * sibling sparklines + a comparison tooltip. */
+ (e: 'bucket-hover', bucket: number): void;
+ /** Pointer left the chart. */
+ (e: 'bucket-leave'): void;
+}>();
+
+function onPointerMove(ev: PointerEvent): void {
+ const svg = ev.currentTarget as SVGSVGElement | null;
+ if (!svg) return;
+ const rect = svg.getBoundingClientRect();
+ if (rect.width === 0) return;
+ const x = ev.clientX - rect.left;
+ const frac = Math.max(0, Math.min(1, x / rect.width));
+ const n = props.values.length;
+ if (n < 2) return;
+ const bucket = Math.round(frac * (n - 1));
+ emit('bucket-hover', bucket);
+}
+function onPointerLeave(): void {
+ emit('bucket-leave');
+}
+
interface PlotState {
d: string;
fillD: string;
dotX: number | null;
dotY: number | null;
+ /** Actual internal-coord width the path was computed against. May
+ * differ from `props.width` in fluid mode where we scale up so
+ * high-resolution stretching looks smooth. */
+ internalW: number;
+ /** Per-bucket (x, y) points for the line, in internal coord space.
+ * Indexed positions match `props.values`. Null entries mark gaps. */
+ points: Array<{ x: number; y: number } | null>;
empty: boolean;
}
const plot = computed<PlotState>(() => {
const n = props.values.length;
if (n < 2) {
- return { d: '', fillD: '', dotX: null, dotY: null, empty: true };
+ return { d: '', fillD: '', dotX: null, dotY: null, internalW: props.width, points: [], empty: true };
}
let min = Infinity;
let max = -Infinity;
@@ -67,12 +117,18 @@ const plot = computed<PlotState>(() => {
if (v > max) max = v;
}
if (finiteCount < 2) {
- return { d: '', fillD: '', dotX: null, dotY: null, empty: true };
+ return { d: '', fillD: '', dotX: null, dotY: null, internalW: props.width, points: [], empty: true };
}
const range = max - min || 1;
// Inset a half-pixel so strokes don't get clipped by the SVG edge.
const padY = props.stroke;
- const w = props.width;
+ // In fluid mode the SVG stretches to its container width via
+ // `preserveAspectRatio="none"`. If we kept the default 56-unit
+ // internal width, the line would look blocky when n > 30 (xStep
+ // < 1, segments collapsing). Bump the internal coord space to a
+ // generous data-proportional value so the path is high-resolution
+ // and the stretch looks smooth.
+ const w = props.fluid ? Math.max(props.width, n * 12) : props.width;
const h = props.height;
const xStep = (w - 1) / (n - 1);
@@ -121,22 +177,39 @@ const plot = computed<PlotState>(() => {
fillD: fillParts.join(' '),
dotX: lastFinite?.x ?? null,
dotY: lastFinite?.y ?? null,
+ internalW: w,
+ points,
empty: false,
};
});
+
+/** The crosshair (x, y) on the path for `crosshairBucket`. Null when
+ * no bucket is hovered or the bucket has no data. */
+const crosshairPos = computed<{ x: number; y: number } | null>(() => {
+ const b = props.crosshairBucket;
+ if (b === null || b === undefined) return null;
+ const pt = plot.value.points[b];
+ if (!pt) return null;
+ return pt;
+});
</script>
<template>
<svg
v-if="!plot.empty"
class="sparkline"
- :width="width"
+ :width="fluid ? '100%' : width"
:height="height"
- :viewBox="`0 0 ${width} ${height}`"
+ :viewBox="`0 0 ${plot.internalW} ${height}`"
+ :preserveAspectRatio="fluid ? 'none' : 'xMidYMid meet'"
role="img"
aria-label="trend"
+ @pointermove="onPointerMove"
+ @pointerleave="onPointerLeave"
>
- <path :d="plot.fillD" :fill="color" fill-opacity="0.12" stroke="none" />
+ <!-- Fill area below the line. Kept faint and skipped in fluid
+ mode where the line-only look reads better at chart size. -->
+ <path v-if="!fluid" :d="plot.fillD" :fill="color" fill-opacity="0.12" stroke="none" />
<path
:d="plot.d"
fill="none"
@@ -144,16 +217,53 @@ const plot = computed<PlotState>(() => {
:stroke-width="stroke"
stroke-linecap="round"
stroke-linejoin="round"
+ vector-effect="non-scaling-stroke"
/>
+ <!-- End dot only on the compact (non-fluid) variant — at chart
+ size it reads as an oddly thick endpoint. -->
<circle
- v-if="plot.dotX !== null && plot.dotY !== null"
+ v-if="!fluid && plot.dotX !== null && plot.dotY !== null"
:cx="plot.dotX"
:cy="plot.dotY"
:r="stroke + 0.5"
:fill="color"
/>
+ <!-- Shared crosshair driven by the parent's hover state. The
+ hairline is drawn full height; the dot marks the point on
+ this series' line. Both render in non-scaling stroke so
+ they stay 1px regardless of the SVG's stretch. -->
+ <template v-if="crosshairPos">
+ <line
+ :x1="crosshairPos.x"
+ :x2="crosshairPos.x"
+ :y1="0"
+ :y2="height"
+ stroke="var(--sw-fg-3)"
+ stroke-width="1"
+ stroke-dasharray="2 3"
+ vector-effect="non-scaling-stroke"
+ pointer-events="none"
+ />
+ <circle
+ :cx="crosshairPos.x"
+ :cy="crosshairPos.y"
+ r="2.6"
+ :fill="color"
+ stroke="var(--sw-bg-0)"
+ stroke-width="1"
+ vector-effect="non-scaling-stroke"
+ pointer-events="none"
+ />
+ </template>
</svg>
- <span v-else class="sparkline-empty" :style="{ width: `${width}px`, height: `${height}px` }">—</span>
+ <span
+ v-else
+ class="sparkline-empty"
+ :style="{
+ width: fluid ? '100%' : `${width}px`,
+ height: `${height}px`,
+ }"
+ >—</span>
</template>
<style scoped>
diff --git a/apps/ui/src/components/shell/AppShell.vue b/apps/ui/src/components/shell/AppShell.vue
index 6482234..8e832d5 100644
--- a/apps/ui/src/components/shell/AppShell.vue
+++ b/apps/ui/src/components/shell/AppShell.vue
@@ -18,6 +18,7 @@
import { RouterView } from 'vue-router';
import AppSidebar from './AppSidebar.vue';
import AppTopbar from './AppTopbar.vue';
+import TracePopout from '@/components/trace/TracePopout.vue';
</script>
<template>
@@ -27,5 +28,8 @@ import AppTopbar from './AppTopbar.vue';
<main class="sw-main">
<RouterView />
</main>
+ <!-- Global trace-id popout: any page can call useTracePopout().openTrace(id)
+ and this modal renders the waterfall + span detail. -->
+ <TracePopout />
</div>
</template>
diff --git a/apps/ui/src/components/shell/AppSidebar.vue b/apps/ui/src/components/shell/AppSidebar.vue
index a4ecf46..699a3dd 100644
--- a/apps/ui/src/components/shell/AppSidebar.vue
+++ b/apps/ui/src/components/shell/AppSidebar.vue
@@ -185,10 +185,10 @@ const sections: NavSection[] = [
:class="{ 'is-active': isActive(`/layer/${L.key}`) }"
>
<span class="layer-dot" :style="{ background: L.color }" />
- <span class="layer-name">{{ L.name }}</span>
<span class="layer-count" :title="`${L.serviceCount} service${L.serviceCount === 1 ? '' : 's'} reporting`">
{{ L.serviceCount }}
</span>
+ <span class="layer-name">{{ L.name }}</span>
</RouterLink>
<!-- Multi-feature layer: expander row + children. -->
@@ -199,12 +199,12 @@ const sections: NavSection[] = [
@click="toggleLayer(L.key)"
>
<span class="layer-dot" :style="{ background: L.color }" />
- <span class="layer-name" :style="{ fontWeight: expandedLayer === L.key ? 600 : 500 }">
- {{ L.name }}
- </span>
<span class="layer-count" :title="`${L.serviceCount} service${L.serviceCount === 1 ? '' : 's'} reporting`">
{{ L.serviceCount }}
</span>
+ <span class="layer-name" :style="{ fontWeight: expandedLayer === L.key ? 600 : 500 }">
+ {{ L.name }}
+ </span>
<span class="caret" :class="{ open: expandedLayer === L.key }">
<Icon name="caret" :size="10" />
</span>
@@ -398,9 +398,13 @@ const sections: NavSection[] = [
background: var(--sw-bg-2);
border: 1px solid var(--sw-line-2);
border-radius: 4px;
- padding: 1px 6px;
- min-width: 24px;
- text-align: center;
+ padding: 1px 4px;
+ /* Fixed width so every row's name starts at the same column even when
+ counts swing wildly (1 ↔ 1000+). The chip is right-aligned inside
+ so the digit always sits flush against the name. */
+ width: 34px;
+ flex: 0 0 34px;
+ text-align: right;
}
.layer-row.is-active .layer-count {
color: var(--sw-accent-2);
diff --git a/apps/ui/src/components/shell/AppTopbar.vue b/apps/ui/src/components/shell/AppTopbar.vue
index c2aef3b..d243cf6 100644
--- a/apps/ui/src/components/shell/AppTopbar.vue
+++ b/apps/ui/src/components/shell/AppTopbar.vue
@@ -15,10 +15,11 @@
limitations under the License.
-->
<script setup lang="ts">
-import { computed } from 'vue';
+import { computed, ref, watch } from 'vue';
import { RouterLink, useRoute } from 'vue-router';
import Icon from '@/components/icons/Icon.vue';
import { useOapInfo } from '@/composables/useOapInfo';
+import { useAutoRefreshStore } from '@/stores/autoRefresh';
const route = useRoute();
@@ -68,6 +69,76 @@ const tzMismatch = computed<boolean>(() => {
const browserMin = -new Date().getTimezoneOffset();
return browserMin !== serverTzMin.value;
});
+
+/**
+ * Routes that own their own time range — the global topbar picker +
+ * refresh button get disabled (greyed + non-clickable) so the
+ * operator knows the page's local picker is the source of truth.
+ *
+ * Add more routes here as Logs / Traces / etc. each opt out of the
+ * global rolling window in favour of a per-page picker.
+ */
+const TIME_RANGE_OPT_OUT = [/^\/layer\/[^/]+\/trace$/];
+const ownsTimeRange = computed<boolean>(() => TIME_RANGE_OPT_OUT.some((r) => r.test(route.path)));
+const globalTimeTooltip = computed<string>(() => {
+ if (ownsTimeRange.value) {
+ return 'This page uses its own time range — disable the page picker to use the global one.';
+ }
+ return `Browser local time · ${localTzLabel.value}`;
+});
+
+/**
+ * Auto-refresh: store drives the ticker; the topbar drives the UI
+ * (countdown + spinning icon + interval dropdown). When the operator
+ * lands on an opt-out route the ticker suspends; on leaving the
+ * route it resumes + fires one immediate tick so the underlying page
+ * gets fresh data right away.
+ */
+const auto = useAutoRefreshStore();
+watch(
+ ownsTimeRange,
+ (now) => {
+ if (now) auto.suspend();
+ else auto.resume();
+ },
+ { immediate: true },
+);
+
+const REFRESH_PRESETS: Array<{ label: string; sec: number | null }> = [
+ { label: 'Off', sec: null },
+ { label: '5s', sec: 5 },
+ { label: '15s', sec: 15 },
+ { label: '30s', sec: 30 },
+ { label: '1m', sec: 60 },
+ { label: '5m', sec: 300 },
+];
+const refreshMenuOpen = ref(false);
+const refreshClusterEl = ref<HTMLElement | null>(null);
+function pickRefresh(sec: number | null): void {
+ auto.setInterval(sec);
+ refreshMenuOpen.value = false;
+}
+function onWindowClickClose(ev: MouseEvent): void {
+ if (!refreshMenuOpen.value) return;
+ const el = refreshClusterEl.value;
+ if (el && !el.contains(ev.target as Node)) {
+ refreshMenuOpen.value = false;
+ }
+}
+if (typeof window !== 'undefined') {
+ window.addEventListener('click', onWindowClickClose);
+}
+const refreshLabel = computed<string>(() => {
+ if (ownsTimeRange.value) return 'Paused';
+ if (auto.intervalSec === null) return 'Off';
+ if (auto.secondsUntilNext === null) return '—';
+ return `${auto.secondsUntilNext}s`;
+});
+const refreshTooltip = computed<string>(() => {
+ if (ownsTimeRange.value) return 'Auto-refresh paused on this page';
+ if (auto.intervalSec === null) return 'Auto-refresh off · click to refresh now';
+ return `Auto-refresh every ${auto.intervalSec}s · ${auto.secondsUntilNext ?? '—'}s remaining · click to refresh now`;
+});
</script>
<template>
@@ -94,18 +165,121 @@ const tzMismatch = computed<boolean>(() => {
{{ tzOffsetLabel }}
</span>
</RouterLink>
- <div class="sw-btn" :title="`Browser local time · ${localTzLabel}`">
+ <div class="sw-btn" :class="{ 'is-disabled': ownsTimeRange }" :title="globalTimeTooltip">
<Icon name="clock" :size="12" />
- <span>Last 30 minutes</span>
+ <span>{{ ownsTimeRange ? 'Page time range' : 'Last 30 minutes' }}</span>
<Icon name="caret" :size="10" />
</div>
- <div class="sw-btn is-icon"><Icon name="refresh" :size="12" /></div>
+ <!-- Auto-refresh cluster: countdown + spinning button on the
+ left, dropdown caret on the right. Click the icon to
+ refresh now; click the caret to pick an interval. -->
+ <div ref="refreshClusterEl" class="refresh-cluster" :class="{ 'is-disabled': ownsTimeRange }">
+ <button
+ type="button"
+ class="sw-btn is-icon refresh-now"
+ :class="{ spinning: auto.effectiveEnabled }"
+ :title="refreshTooltip"
+ :disabled="ownsTimeRange"
+ @click="auto.refreshNow()"
+ ><Icon name="refresh" :size="12" /></button>
+ <span class="refresh-countdown mono" :title="refreshTooltip">{{ refreshLabel }}</span>
+ <button
+ type="button"
+ class="sw-btn refresh-caret"
+ :title="'Pick refresh interval'"
+ :disabled="ownsTimeRange"
+ @click="refreshMenuOpen = !refreshMenuOpen"
+ ><Icon name="caret" :size="10" /></button>
+ <transition name="rf-menu">
+ <ul v-if="refreshMenuOpen" class="rf-menu">
+ <li
+ v-for="p in REFRESH_PRESETS"
+ :key="String(p.sec)"
+ :class="{ on: auto.intervalSec === p.sec }"
+ @click="pickRefresh(p.sec)"
+ >{{ p.label }}</li>
+ </ul>
+ </transition>
+ </div>
<div class="sw-btn is-icon"><Icon name="bell" :size="12" /></div>
</div>
</header>
</template>
<style scoped>
+/* Disabled state for global time-range / refresh chips when the
+ current page owns its own time range. Greys out without removing
+ the chip so the operator still sees the affordance + tooltip. */
+.sw-btn.is-disabled {
+ opacity: 0.45;
+ pointer-events: none;
+ filter: grayscale(0.6);
+}
+
+/* Auto-refresh cluster */
+.refresh-cluster {
+ position: relative;
+ display: inline-flex;
+ align-items: center;
+ gap: 2px;
+}
+.refresh-cluster.is-disabled {
+ opacity: 0.45;
+ filter: grayscale(0.6);
+}
+.refresh-now {
+ cursor: pointer;
+}
+.refresh-now.spinning :deep(svg) {
+ animation: refresh-spin 1.6s linear infinite;
+ transform-origin: 50% 50%;
+}
+@keyframes refresh-spin {
+ to { transform: rotate(360deg); }
+}
+.refresh-countdown {
+ font-size: 10.5px;
+ color: var(--sw-fg-2);
+ min-width: 28px;
+ text-align: center;
+ font-variant-numeric: tabular-nums;
+}
+.refresh-caret {
+ cursor: pointer;
+ padding: 0 4px;
+ min-width: auto;
+}
+.rf-menu {
+ position: absolute;
+ top: 100%;
+ right: 0;
+ margin-top: 4px;
+ list-style: none;
+ padding: 4px;
+ background: var(--sw-bg-1);
+ border: 1px solid var(--sw-line);
+ border-radius: 6px;
+ min-width: 96px;
+ z-index: 10;
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.45);
+}
+.rf-menu li {
+ padding: 4px 10px;
+ font-size: 11px;
+ color: var(--sw-fg-1);
+ cursor: pointer;
+ border-radius: 4px;
+ font-variant-numeric: tabular-nums;
+}
+.rf-menu li:hover { background: var(--sw-bg-2); }
+.rf-menu li.on { background: var(--sw-accent-soft); color: var(--sw-accent-2); font-weight: 600; }
+.rf-menu-enter-from, .rf-menu-leave-to {
+ opacity: 0;
+ transform: translateY(-4px);
+}
+.rf-menu-enter-active, .rf-menu-leave-active {
+ transition: opacity 0.15s ease, transform 0.15s ease;
+}
.oap-chip {
text-decoration: none;
font-family: var(--sw-mono);
diff --git a/apps/ui/src/components/trace/TracePopout.vue b/apps/ui/src/components/trace/TracePopout.vue
new file mode 100644
index 0000000..40fb6c4
--- /dev/null
+++ b/apps/ui/src/components/trace/TracePopout.vue
@@ -0,0 +1,828 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<!--
+ Global trace popout — opened by URL `?traceId=<id>` from anywhere
+ in the app (shareable links, cross-trace refs, log-row trace links,
+ alarm payloads). Distinct from the per-layer trace tab's *in-page*
+ detail layout: the popout is single-focus + horizontal split, while
+ the in-page detail uses a folded list rail + tabbed multi-view.
+
+ Popout layout (intentionally different from in-page):
+ - Waterfall on the left (always List view — no Tree/Table/Stats
+ tabs; the popout is a glance-link experience).
+ - Span detail in a sticky right panel (no nested modal). Click
+ a span row in the waterfall → the right panel updates. Cross-
+ trace refs there hot-swap the popout to the linked trace.
+-->
+<script setup lang="ts">
+import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
+import type { NativeSpan, TraceAttachedEvent, TraceLogEntry } from '@/api/client';
+import { useTraceDetail } from '@/composables/useLayerTraces';
+import { useTracePopout } from '@/composables/useTracePopout';
+import { componentIconOrNull } from '@/composables/useTopologyIcons';
+import { fmtMetric } from '@/utils/formatters';
+
+const { openTraceId, openTrace, closeTrace } = useTracePopout();
+
+const sourceRef = computed<'native'>(() => 'native');
+const traceIdRef = computed<string | null>(() => openTraceId.value);
+const { nativeDetail, isFetching } = useTraceDetail(traceIdRef, sourceRef);
+
+const spans = computed<NativeSpan[]>(() => nativeDetail.value?.spans ?? []);
+
+// Reset selected span when the trace changes (cross-trace ref jump).
+const selectedSpan = ref<NativeSpan | null>(null);
+watch(traceIdRef, () => { selectedSpan.value = null; });
+
+interface WaterfallRow {
+ span: NativeSpan;
+ depth: number;
+ startOffset: number;
+ duration: number;
+}
+const waterfall = computed<WaterfallRow[]>(() => {
+ const s = spans.value;
+ if (s.length === 0) return [];
+ const minStart = Math.min(...s.map((x) => x.startTime));
+ const key = (segId: string, spanId: number) => `${segId}/${spanId}`;
+ const byKey = new Map(s.map((x) => [key(x.segmentId, x.spanId), x]));
+ const childMap = new Map<string, NativeSpan[]>();
+ const roots: NativeSpan[] = [];
+ for (const x of s) {
+ if (x.parentSpanId === -1) {
+ const r = x.refs?.[0];
+ const pk = r ? key(r.parentSegmentId, r.parentSpanId) : null;
+ if (pk && byKey.has(pk)) {
+ const arr = childMap.get(pk) ?? [];
+ arr.push(x);
+ childMap.set(pk, arr);
+ } else {
+ roots.push(x);
+ }
+ } else {
+ const pk = key(x.segmentId, x.parentSpanId);
+ const arr = childMap.get(pk) ?? [];
+ arr.push(x);
+ childMap.set(pk, arr);
+ }
+ }
+ const out: WaterfallRow[] = [];
+ function walk(x: NativeSpan, depth: number): void {
+ const ck = key(x.segmentId, x.spanId);
+ const kids = childMap.get(ck) ?? [];
+ out.push({
+ span: x,
+ depth,
+ startOffset: x.startTime - minStart,
+ duration: Math.max(0, x.endTime - x.startTime),
+ });
+ for (const c of kids) walk(c, depth + 1);
+ }
+ for (const r of roots) walk(r, 0);
+ return out;
+});
+const totalDuration = computed(() => {
+ const r = waterfall.value;
+ if (r.length === 0) return 0;
+ return Math.max(...r.map((x) => x.startOffset + x.duration));
+});
+const rootStart = computed(() => {
+ const s = spans.value;
+ return s.length > 0 ? Math.min(...s.map((x) => x.startTime)) : null;
+});
+
+// Service color palette (matches the trace tab so the operator sees
+// the same colours across the in-page detail and the popout).
+const SERVICE_PALETTE = [
+ 'var(--sw-accent)', 'var(--sw-info)', 'var(--sw-cyan)', 'var(--sw-purple)',
+ 'var(--sw-ok)', 'var(--sw-warn)', 'var(--sw-pink)',
+ '#a78bfa', '#fb7185', '#34d399', '#fbbf24', '#60a5fa',
+];
+function hashString(s: string): number {
+ let h = 2166136261;
+ for (let i = 0; i < s.length; i++) {
+ h ^= s.charCodeAt(i);
+ h = Math.imul(h, 16777619);
+ }
+ return (h >>> 0);
+}
+const serviceColors = computed<Map<string, string>>(() => {
+ const m = new Map<string, string>();
+ for (const s of spans.value) {
+ if (!m.has(s.serviceCode)) {
+ m.set(s.serviceCode, SERVICE_PALETTE[hashString(s.serviceCode) % SERVICE_PALETTE.length]);
+ }
+ }
+ return m;
+});
+function serviceColor(c: string): string {
+ return serviceColors.value.get(c) ?? 'var(--sw-fg-2)';
+}
+function kindColor(type: string | null | undefined): string {
+ const t = (type ?? '').toUpperCase();
+ if (t.includes('SERVER')) return 'var(--sw-accent)';
+ if (t.includes('CLIENT')) return 'var(--sw-info)';
+ if (t.includes('PRODUCER') || t.includes('CONSUMER')) return 'var(--sw-purple)';
+ if (t.includes('DATABASE') || t.includes('SQL') || t.includes('CACHE')) return 'var(--sw-cyan)';
+ return 'var(--sw-fg-2)';
+}
+function fmtMs(v: number | null | undefined): string {
+ if (v === null || v === undefined) return '—';
+ return `${fmtMetric(v)} ms`;
+}
+function fmtDateTime(ts: number | null | undefined): string {
+ if (!ts || !Number.isFinite(ts)) return '—';
+ const d = new Date(ts);
+ const pad = (n: number) => String(n).padStart(2, '0');
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
+}
+function fmtAttachedTs(t: { seconds: number; nanos: number }): string {
+ const ms = t.seconds * 1000 + t.nanos / 1_000_000;
+ return fmtDateTime(ms) + '.' + String(t.nanos).padStart(9, '0').slice(0, 6);
+}
+
+// Copy affordances.
+const copyFlash = ref<'id' | 'url' | null>(null);
+let copyFlashTimer: ReturnType<typeof setTimeout> | null = null;
+function flashCopy(kind: 'id' | 'url'): void {
+ copyFlash.value = kind;
+ if (copyFlashTimer) clearTimeout(copyFlashTimer);
+ copyFlashTimer = setTimeout(() => { copyFlash.value = null; }, 1400);
+}
+function copyId(): void {
+ if (!openTraceId.value) return;
+ navigator.clipboard?.writeText(openTraceId.value).then(() => flashCopy('id'), () => {});
+}
+function copyShareableUrl(): void {
+ if (!openTraceId.value) return;
+ navigator.clipboard?.writeText(window.location.href).then(() => flashCopy('url'), () => {});
+}
+
+function selectSpan(s: NativeSpan): void { selectedSpan.value = s; }
+function clearSpan(): void { selectedSpan.value = null; }
+
+/** Esc-to-close. If a span is selected the first Esc closes the
+ * side panel (keeps the popout open). A second Esc — or Esc with
+ * no span selected — closes the popout itself. Always listening
+ * while the component is mounted; the no-op guard prevents
+ * intercepting Esc when the popout isn't open. */
+function onKeyDown(e: KeyboardEvent): void {
+ if (e.key !== 'Escape') return;
+ if (!openTraceId.value) return;
+ if (selectedSpan.value) {
+ selectedSpan.value = null;
+ e.preventDefault();
+ e.stopPropagation();
+ } else {
+ closeTrace();
+ e.preventDefault();
+ e.stopPropagation();
+ }
+}
+onMounted(() => window.addEventListener('keydown', onKeyDown, true));
+onBeforeUnmount(() => window.removeEventListener('keydown', onKeyDown, true));
+function isSelfRef(refTraceId: string): boolean { return refTraceId === openTraceId.value; }
+function jumpToTrace(id: string): void { openTrace(id); }
+function nativeSpanError(s: NativeSpan): boolean { return s.isError; }
+</script>
+
+<template>
+ <div v-if="openTraceId" class="tp-backdrop" @click.self="closeTrace">
+ <article class="tp-card sw-card">
+ <header class="tp-head">
+ <div class="tp-title">
+ <span class="dim">Trace</span>
+ <span class="mono trace-id-text" :title="openTraceId">{{ openTraceId }}</span>
+ <button class="sw-btn small ghost copy-btn" type="button" title="Copy trace id" @click="copyId">⧉ id</button>
+ <button class="sw-btn small ghost copy-btn" type="button" title="Copy shareable URL" @click="copyShareableUrl">⧉ url</button>
+ <transition name="copy-flash">
+ <span v-if="copyFlash" class="copy-flash-chip">{{ copyFlash === 'url' ? 'url copied' : 'id copied' }}</span>
+ </transition>
+ <span v-if="isFetching" class="hint">loading…</span>
+ </div>
+ <button class="sw-btn small ghost" type="button" title="Close" @click="closeTrace">×</button>
+ </header>
+
+ <div class="tp-kpis">
+ <div><div class="kpi-label">started</div><div class="kpi-val">{{ fmtDateTime(rootStart) }}</div></div>
+ <div><div class="kpi-label">duration</div><div class="kpi-val">{{ fmtMs(totalDuration) }}</div></div>
+ <div><div class="kpi-label">spans</div><div class="kpi-val">{{ waterfall.length }}</div></div>
+ <div><div class="kpi-label">services</div><div class="kpi-val">{{ serviceColors.size }}</div></div>
+ </div>
+
+ <div v-if="serviceColors.size > 0" class="tp-svc-legend">
+ <span v-for="[code, color] in serviceColors" :key="code" class="svc-chip">
+ <span class="svc-swatch" :style="{ background: color }" />
+ <span class="mono">{{ code }}</span>
+ </span>
+ </div>
+
+ <!-- Body: waterfall (left) + sticky span detail panel (right).
+ Distinct from the per-layer trace tab's modal-on-tabbed
+ detail; here the right panel is always visible, swapping
+ contents on span row click. -->
+ <div class="tp-body">
+ <div v-if="waterfall.length === 0 && !isFetching" class="tp-empty">no span data</div>
+ <div v-else class="tp-split" :class="{ 'no-selection': !selectedSpan }">
+ <div class="tp-waterfall">
+ <!-- Time axis at the top of the waterfall column. -->
+ <div class="tp-time-axis">
+ <span class="t-tick first">0</span>
+ <span class="t-tick">{{ fmtMs(totalDuration * 0.25) }}</span>
+ <span class="t-tick">{{ fmtMs(totalDuration * 0.5) }}</span>
+ <span class="t-tick">{{ fmtMs(totalDuration * 0.75) }}</span>
+ <span class="t-tick last">{{ fmtMs(totalDuration) }}</span>
+ </div>
+ <div
+ v-for="row in waterfall"
+ :key="`${row.span.segmentId}/${row.span.spanId}`"
+ class="tp-row"
+ :class="{ err: row.span.isError, on: selectedSpan && selectedSpan.segmentId === row.span.segmentId && selectedSpan.spanId === row.span.spanId }"
+ @click="selectSpan(row.span)"
+ >
+ <div class="tp-track">
+ <span class="t-grid q1" />
+ <span class="t-grid q2" />
+ <span class="t-grid q3" />
+ <div
+ class="tp-bar"
+ :style="{
+ left: totalDuration > 0 ? (row.startOffset / totalDuration * 100) + '%' : '0%',
+ width: totalDuration > 0 ? Math.max(0.8, row.duration / totalDuration * 100) + '%' : '0%',
+ background: serviceColor(row.span.serviceCode),
+ borderColor: row.span.isError ? 'var(--sw-err)' : 'transparent',
+ }"
+ >
+ <span class="bar-inner">
+ <span
+ class="status-flag sm"
+ :class="row.span.isError ? 'flag-err' : 'flag-ok'"
+ :title="row.span.isError ? 'Span errored' : 'Span OK'"
+ ><span class="flag-dot" /></span>
+ <img
+ v-if="componentIconOrNull(row.span.component)"
+ class="comp-icon"
+ :src="componentIconOrNull(row.span.component) ?? ''"
+ :alt="row.span.component || ''"
+ :title="row.span.component || ''"
+ />
+ <svg
+ v-else
+ class="comp-icon comp-icon-generic"
+ viewBox="0 0 18 18"
+ :aria-label="row.span.component || 'generic span'"
+ >
+ <rect x="3" y="4.5" width="12" height="3" rx="1.5" fill="currentColor" opacity="0.45" />
+ <rect x="5" y="10.5" width="10" height="3" rx="1.5" fill="currentColor" opacity="0.85" />
+ </svg>
+ <span class="bar-text mono">
+ <span class="bar-svc" :title="row.span.serviceCode">{{ row.span.serviceCode }}</span>
+ <span class="bar-name" :title="row.span.endpointName || row.span.component || row.span.peer || '—'">{{ row.span.endpointName || row.span.component || row.span.peer || '—' }}</span>
+ </span>
+ <span
+ v-if="row.span.attachedEvents && row.span.attachedEvents.length > 0"
+ class="evt-badge inside"
+ :title="`${row.span.attachedEvents.length} attached event${row.span.attachedEvents.length === 1 ? '' : 's'}`"
+ >
+ <span class="evt-flag">⚑</span>
+ <span class="evt-count">{{ row.span.attachedEvents.length }}</span>
+ </span>
+ </span>
+ </div>
+ <span
+ class="bar-dur-outside mono"
+ :style="{
+ left: totalDuration > 0
+ ? `calc(${((row.startOffset + row.duration) / totalDuration) * 100}% + 6px)`
+ : '0%',
+ }"
+ >{{ fmtMs(row.duration) }}</span>
+ </div>
+ </div>
+ </div>
+
+ <!-- Span detail panel — replaces the nested modal used in
+ the per-layer trace tab. Shown when a span is picked;
+ hidden otherwise to let the waterfall claim full width. -->
+ <aside v-if="selectedSpan" class="tp-span-panel">
+ <header class="tp-span-head">
+ <h5>Span detail</h5>
+ <button class="sw-btn small ghost" type="button" @click="clearSpan">×</button>
+ </header>
+ <div class="tp-span-body">
+ <section class="sd-section">
+ <h6>Meta</h6>
+ <dl class="kv">
+ <dt>Service</dt>
+ <dd class="mono" :style="{ color: serviceColor(selectedSpan.serviceCode) }">
+ <span class="svc-swatch inline" :style="{ background: serviceColor(selectedSpan.serviceCode) }" />
+ {{ selectedSpan.serviceCode }}
+ </dd>
+ <dt>Instance</dt><dd class="mono wba">{{ selectedSpan.serviceInstanceName }}</dd>
+ <dt>Endpoint</dt><dd class="mono wba">{{ selectedSpan.endpointName || '—' }}</dd>
+ <dt>Type</dt><dd><span class="tp-kind" :style="{ background: kindColor(selectedSpan.type) }">{{ selectedSpan.type }}</span></dd>
+ <dt>Component</dt><dd class="mono">{{ selectedSpan.component || '—' }}</dd>
+ <dt>Peer</dt><dd class="mono wba">{{ selectedSpan.peer || '—' }}</dd>
+ <dt>Layer</dt><dd class="mono dim">{{ selectedSpan.layer || '—' }}</dd>
+ <dt>Start</dt><dd class="mono">{{ fmtDateTime(selectedSpan.startTime) }}</dd>
+ <dt>Duration</dt><dd class="mono">{{ fmtMs(selectedSpan.endTime - selectedSpan.startTime) }}</dd>
+ <dt>Error</dt><dd><span class="status-flag" :class="nativeSpanError(selectedSpan) ? 'flag-err' : 'flag-ok'"><span class="flag-dot" />{{ nativeSpanError(selectedSpan) ? 'true' : 'false' }}</span></dd>
+ </dl>
+ </section>
+ <section
+ v-if="(selectedSpan.refs ?? []).some((r) => !isSelfRef(r.traceId))"
+ class="sd-section"
+ >
+ <h6>Cross-trace refs</h6>
+ <table class="kv-table">
+ <thead><tr><th>Trace ID</th><th class="num">Parent span</th></tr></thead>
+ <tbody>
+ <template v-for="(r, i) in selectedSpan.refs" :key="i">
+ <tr v-if="!isSelfRef(r.traceId)">
+ <td>
+ <button class="trace-link mono" type="button" @click="jumpToTrace(r.traceId)">{{ r.traceId }} ↗</button>
+ </td>
+ <td class="num mono">{{ r.parentSpanId }}</td>
+ </tr>
+ </template>
+ </tbody>
+ </table>
+ </section>
+ <section v-if="selectedSpan.tags && selectedSpan.tags.length > 0" class="sd-section">
+ <h6>Tags</h6>
+ <dl class="kv">
+ <template v-for="(t, i) in selectedSpan.tags" :key="i">
+ <dt class="mono">{{ t.key }}</dt>
+ <dd class="mono wba">{{ t.value }}</dd>
+ </template>
+ </dl>
+ </section>
+ <section v-if="selectedSpan.logs && selectedSpan.logs.length > 0" class="sd-section">
+ <h6>Logs</h6>
+ <div v-for="(log, i) in (selectedSpan.logs as TraceLogEntry[])" :key="i" class="span-log">
+ <div class="span-log-time mono dim">{{ fmtDateTime(log.time) }}</div>
+ <dl class="kv">
+ <template v-for="(d, j) in log.data" :key="j">
+ <dt class="mono">{{ d.key }}</dt>
+ <dd><pre class="mono pre wba">{{ d.value }}</pre></dd>
+ </template>
+ </dl>
+ </div>
+ </section>
+ <section v-if="selectedSpan.attachedEvents && selectedSpan.attachedEvents.length > 0" class="sd-section">
+ <h6>Attached Events</h6>
+ <div v-for="(ev, i) in (selectedSpan.attachedEvents as TraceAttachedEvent[])" :key="i" class="span-event">
+ <div class="span-event-head">
+ <span class="mono">{{ ev.event }}</span>
+ <span class="dim mono">{{ fmtAttachedTs(ev.startTime) }} → {{ fmtAttachedTs(ev.endTime) }}</span>
+ </div>
+ <dl v-if="ev.summary && ev.summary.length > 0" class="kv">
+ <template v-for="(s, j) in ev.summary" :key="`s${j}`">
+ <dt class="mono">{{ s.key }}</dt>
+ <dd class="mono wba">{{ s.value }}</dd>
+ </template>
+ </dl>
+ <dl v-if="ev.tags && ev.tags.length > 0" class="kv">
+ <template v-for="(t, j) in ev.tags" :key="`t${j}`">
+ <dt class="mono dim">tag · {{ t.key }}</dt>
+ <dd class="mono wba">{{ t.value }}</dd>
+ </template>
+ </dl>
+ </div>
+ </section>
+ </div>
+ </aside>
+ </div>
+ </div>
+ </article>
+ </div>
+</template>
+
+<style scoped>
+.tp-backdrop {
+ position: fixed;
+ inset: 0;
+ background: rgba(0, 0, 0, 0.66);
+ z-index: 999;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 4vh 0;
+}
+.tp-card {
+ width: 80vw;
+ height: 92vh;
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+}
+.tp-head {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ padding: 10px 14px;
+ border-bottom: 1px solid var(--sw-line);
+ flex: 0 0 auto;
+ flex-wrap: wrap;
+}
+.tp-title { display: flex; align-items: center; gap: 8px; flex: 1; min-width: 0; }
+.tp-title .dim { font-size: 12px; font-weight: 500; color: var(--sw-fg-3); }
+.trace-id-text {
+ font-family: var(--sw-mono);
+ color: var(--sw-fg-1);
+ font-size: 12px;
+ max-width: 360px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+.tp-kpis {
+ display: flex;
+ gap: 28px;
+ padding: 8px 14px;
+ border-bottom: 1px solid var(--sw-line);
+ flex: 0 0 auto;
+}
+.kpi-label {
+ font-size: 9.5px;
+ text-transform: uppercase;
+ letter-spacing: 0.06em;
+ color: var(--sw-fg-3);
+}
+.kpi-val {
+ font-family: var(--sw-mono);
+ font-size: 14px;
+ color: var(--sw-fg-0);
+ font-weight: 700;
+}
+.tp-svc-legend {
+ display: flex;
+ gap: 6px;
+ flex-wrap: wrap;
+ padding: 6px 14px;
+ border-bottom: 1px solid var(--sw-line);
+ background: var(--sw-bg-1);
+ flex: 0 0 auto;
+}
+.svc-chip {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ font-size: 10.5px;
+ color: var(--sw-fg-2);
+ padding: 1px 6px 1px 4px;
+ border: 1px solid var(--sw-line-2);
+ border-radius: 10px;
+ background: var(--sw-bg-2);
+}
+.svc-swatch {
+ width: 8px;
+ height: 8px;
+ border-radius: 2px;
+ flex: 0 0 auto;
+}
+.svc-swatch.inline {
+ display: inline-block;
+ margin-right: 4px;
+ vertical-align: middle;
+}
+.tp-empty {
+ padding: 40px;
+ text-align: center;
+ color: var(--sw-fg-3);
+ font-size: 12px;
+}
+.tp-body { flex: 1; overflow: hidden; min-height: 0; display: flex; }
+
+/* Horizontal split: waterfall + sticky span panel. */
+.tp-split {
+ display: grid;
+ grid-template-columns: 1fr 420px;
+ gap: 0;
+ flex: 1;
+ min-height: 0;
+ width: 100%;
+}
+.tp-split.no-selection { grid-template-columns: 1fr; }
+
+/* Waterfall */
+.tp-waterfall {
+ padding: 6px 0;
+ overflow-y: auto;
+ min-width: 0;
+}
+.tp-time-axis {
+ position: sticky;
+ top: 0;
+ z-index: 2;
+ display: flex;
+ justify-content: space-between;
+ padding: 4px 12px;
+ background: var(--sw-bg-1);
+ border-bottom: 1px solid var(--sw-line);
+ font-family: var(--sw-mono);
+ font-size: 10px;
+ color: var(--sw-fg-3);
+}
+.t-tick { white-space: nowrap; }
+.t-tick.first { text-align: left; }
+.t-tick.last { text-align: right; }
+.tp-row {
+ padding: 3px 12px;
+ cursor: pointer;
+}
+.tp-row:hover { background: var(--sw-bg-2); }
+.tp-row.err { background: rgba(239, 68, 68, 0.06); }
+.tp-row.on { background: var(--sw-accent-soft); }
+.tp-track {
+ position: relative;
+ height: 22px;
+ background: transparent;
+}
+.t-grid {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ width: 1px;
+ background: var(--sw-line);
+ opacity: 0.55;
+ pointer-events: none;
+}
+.t-grid.q1 { left: 25%; }
+.t-grid.q2 { left: 50%; }
+.t-grid.q3 { left: 75%; }
+.tp-bar {
+ position: absolute;
+ top: 2px;
+ bottom: 2px;
+ border-radius: 3px;
+ border: 1px solid transparent;
+ display: flex;
+ align-items: center;
+ overflow: hidden;
+ cursor: pointer;
+ transition: filter 0.12s ease;
+}
+.tp-bar:hover { filter: brightness(1.12); }
+.bar-inner {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ padding: 0 6px;
+ height: 100%;
+ width: 100%;
+ min-width: 0;
+}
+.bar-text {
+ display: inline-flex;
+ align-items: baseline;
+ gap: 6px;
+ min-width: 0;
+ flex: 1 1 auto;
+ overflow: hidden;
+ color: var(--sw-bg-0);
+ text-shadow: 0 0 1px rgba(0, 0, 0, 0.25);
+}
+.bar-svc {
+ font-weight: 700;
+ font-size: 10.5px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ flex: 0 1 auto;
+ max-width: 40%;
+ opacity: 0.9;
+}
+.bar-name {
+ flex: 1 1 auto;
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ font-size: 11px;
+ font-weight: 500;
+}
+.comp-icon {
+ width: 18px;
+ height: 18px;
+ flex: 0 0 auto;
+ object-fit: contain;
+ background: transparent;
+}
+.comp-icon-generic { color: var(--sw-fg-2); }
+.bar-inner .comp-icon-generic { color: rgba(0, 0, 0, 0.72); }
+.bar-inner .status-flag.sm {
+ background: rgba(255, 255, 255, 0.35);
+ color: var(--sw-bg-0);
+}
+.bar-inner .status-flag.flag-err {
+ background: var(--sw-err);
+ color: #fff;
+}
+.evt-badge {
+ display: inline-flex;
+ align-items: center;
+ gap: 3px;
+ height: 16px;
+ padding: 0 5px;
+ border-radius: 8px;
+ background: var(--sw-accent-soft);
+ color: var(--sw-accent-2);
+ font-size: 9.5px;
+ font-weight: 700;
+ flex: 0 0 auto;
+}
+.evt-badge.inside {
+ background: rgba(255, 255, 255, 0.85);
+ color: var(--sw-bg-0);
+}
+.evt-flag { font-size: 10px; line-height: 1; }
+.evt-count { font-family: var(--sw-mono); }
+.bar-dur-outside {
+ position: absolute;
+ top: 50%;
+ transform: translateY(-50%);
+ font-size: 10px;
+ color: var(--sw-fg-2);
+ white-space: nowrap;
+ pointer-events: none;
+}
+
+/* Span panel (sticky right side) */
+.tp-span-panel {
+ border-left: 1px solid var(--sw-line);
+ background: var(--sw-bg-1);
+ display: flex;
+ flex-direction: column;
+ min-width: 0;
+ overflow: hidden;
+}
+.tp-span-head {
+ display: flex;
+ align-items: center;
+ padding: 8px 12px;
+ border-bottom: 1px solid var(--sw-line);
+ flex: 0 0 auto;
+}
+.tp-span-head h5 {
+ margin: 0;
+ font-size: 11.5px;
+ font-weight: 600;
+ color: var(--sw-fg-0);
+ flex: 1;
+}
+.tp-span-body {
+ flex: 1;
+ overflow-y: auto;
+ padding: 10px 12px 14px;
+}
+.sd-section { margin-bottom: 14px; }
+.sd-section h6 {
+ margin: 0 0 6px;
+ font-size: 9.5px;
+ text-transform: uppercase;
+ letter-spacing: 0.08em;
+ color: var(--sw-accent);
+ font-weight: 700;
+}
+.kv {
+ display: grid;
+ grid-template-columns: 100px 1fr;
+ gap: 4px 10px;
+ margin: 0;
+ font-size: 11px;
+}
+.kv dt { color: var(--sw-fg-3); font-size: 10.5px; }
+.kv dd { margin: 0; color: var(--sw-fg-1); }
+.kv-table {
+ width: 100%;
+ border-collapse: collapse;
+ font-size: 11px;
+}
+.kv-table th {
+ text-align: left;
+ font-size: 9.5px;
+ text-transform: uppercase;
+ letter-spacing: 0.06em;
+ color: var(--sw-fg-3);
+ font-weight: 600;
+ padding: 4px 6px;
+ border-bottom: 1px solid var(--sw-line);
+}
+.kv-table th.num { text-align: right; }
+.kv-table td { padding: 4px 6px; border-bottom: 1px solid var(--sw-line); }
+.kv-table td.num { text-align: right; }
+.trace-link {
+ background: transparent;
+ border: none;
+ color: var(--sw-cyan);
+ cursor: pointer;
+ padding: 0;
+ font: inherit;
+ text-decoration: underline;
+ text-underline-offset: 2px;
+}
+.trace-link:hover { color: var(--sw-accent); }
+.span-log, .span-event {
+ border: 1px solid var(--sw-line);
+ border-radius: 4px;
+ padding: 6px 10px;
+ margin-bottom: 6px;
+ background: var(--sw-bg-0);
+}
+.span-log-time { font-size: 10px; margin-bottom: 4px; }
+.span-event-head {
+ display: flex;
+ justify-content: space-between;
+ gap: 10px;
+ font-size: 11px;
+ margin-bottom: 4px;
+}
+.pre {
+ background: var(--sw-bg-2);
+ padding: 4px 6px;
+ border-radius: 3px;
+ font-size: 10.5px;
+ margin: 0;
+ white-space: pre-wrap;
+ word-break: break-all;
+ color: var(--sw-fg-1);
+}
+
+/* Status flag */
+.status-flag {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ height: 18px;
+ padding: 0 6px;
+ border-radius: 9px;
+ font-size: 9.5px;
+ font-weight: 700;
+ letter-spacing: 0.06em;
+ text-transform: uppercase;
+ flex: 0 0 auto;
+}
+.status-flag.sm {
+ height: 12px;
+ width: 12px;
+ padding: 0;
+ border-radius: 50%;
+ justify-content: center;
+}
+.status-flag.sm .flag-dot { margin: 0; }
+.flag-dot {
+ width: 6px;
+ height: 6px;
+ border-radius: 50%;
+ background: currentColor;
+}
+.status-flag.flag-ok { background: rgba(34, 197, 94, 0.14); color: var(--sw-ok); }
+.status-flag.flag-err { background: rgba(239, 68, 68, 0.18); color: var(--sw-err); }
+
+/* Copy affordance */
+.copy-btn:active { transform: scale(0.94); }
+.copy-flash-chip {
+ display: inline-flex;
+ align-items: center;
+ height: 18px;
+ padding: 0 8px;
+ border-radius: 9px;
+ font-size: 9.5px;
+ font-weight: 700;
+ letter-spacing: 0.06em;
+ text-transform: uppercase;
+ background: rgba(34, 197, 94, 0.18);
+ color: var(--sw-ok);
+}
+.copy-flash-enter-from { opacity: 0; transform: translateX(-4px); }
+.copy-flash-enter-active { transition: opacity 0.18s ease, transform 0.18s ease; }
+.copy-flash-leave-to { opacity: 0; transform: translateX(4px); }
+.copy-flash-leave-active { transition: opacity 0.3s ease, transform 0.3s ease; }
+
+.dim { color: var(--sw-fg-3); }
+.mono { font-family: var(--sw-mono); }
+.wba { word-break: break-all; }
+.hint { font-size: 10.5px; color: var(--sw-fg-3); }
+.sw-btn.small { height: 24px; padding: 0 10px; font-size: 11px; }
+.sw-btn.ghost {
+ background: transparent;
+ border: 1px solid var(--sw-line-2);
+ color: var(--sw-fg-2);
+ cursor: pointer;
+}
+</style>
diff --git a/apps/ui/src/composables/useAutoRefreshSubscribe.ts b/apps/ui/src/composables/useAutoRefreshSubscribe.ts
new file mode 100644
index 0000000..81eb441
--- /dev/null
+++ b/apps/ui/src/composables/useAutoRefreshSubscribe.ts
@@ -0,0 +1,36 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { watch } from 'vue';
+import { useAutoRefreshStore } from '@/stores/autoRefresh';
+
+/**
+ * Subscribe a refetch callback to the global auto-refresh ticker.
+ * Composables (or views) call this with their query's `refetch` to
+ * pick up manual refreshes + interval-driven ticks from the topbar.
+ * The watch is anchored to the calling scope, so when the component
+ * unmounts the subscription is torn down automatically.
+ */
+export function useAutoRefreshSubscribe(refetch: () => Promise<unknown> | unknown): void {
+ const auto = useAutoRefreshStore();
+ watch(
+ () => auto.tickCount,
+ () => {
+ void refetch();
+ },
+ );
+}
diff --git a/apps/ui/src/composables/useLayerDashboard.ts b/apps/ui/src/composables/useLayerDashboard.ts
index d211623..e16700b 100644
--- a/apps/ui/src/composables/useLayerDashboard.ts
+++ b/apps/ui/src/composables/useLayerDashboard.ts
@@ -29,6 +29,7 @@
import { computed, type Ref } from 'vue';
import { useQuery } from '@tanstack/vue-query';
+import { useAutoRefreshSubscribe } from './useAutoRefreshSubscribe';
import { bffClient } from '@/api/client';
export function useLayerDashboardConfig(layerKey: Ref<string>, scope?: Ref<string>) {
@@ -38,6 +39,8 @@ export function useLayerDashboardConfig(layerKey: Ref<string>, scope?: Ref<strin
enabled: computed(() => layerKey.value.length > 0),
staleTime: 5 * 60_000,
});
+ useAutoRefreshSubscribe(() => q.refetch());
+
return {
config: computed(() => q.data.value ?? null),
isLoading: q.isLoading,
diff --git a/apps/ui/src/composables/useLayerEndpointDependency.ts b/apps/ui/src/composables/useLayerEndpointDependency.ts
index 492c5bf..1a8767e 100644
--- a/apps/ui/src/composables/useLayerEndpointDependency.ts
+++ b/apps/ui/src/composables/useLayerEndpointDependency.ts
@@ -23,6 +23,7 @@
import { computed, type Ref } from 'vue';
import { useQuery } from '@tanstack/vue-query';
+import { useAutoRefreshSubscribe } from './useAutoRefreshSubscribe';
import { bffClient } from '@/api/client';
export function useLayerEndpointDependency(
@@ -43,6 +44,8 @@ export function useLayerEndpointDependency(
),
staleTime: 30_000,
});
+ useAutoRefreshSubscribe(() => q.refetch());
+
return {
data: computed(() => q.data.value ?? null),
nodes: computed(() => q.data.value?.nodes ?? []),
diff --git a/apps/ui/src/composables/useLayerLanding.ts b/apps/ui/src/composables/useLayerLanding.ts
index e1fe790..8f4f96c 100644
--- a/apps/ui/src/composables/useLayerLanding.ts
+++ b/apps/ui/src/composables/useLayerLanding.ts
@@ -17,6 +17,7 @@
import { computed, type Ref } from 'vue';
import { useQuery } from '@tanstack/vue-query';
+import { useAutoRefreshSubscribe } from './useAutoRefreshSubscribe';
import type { LandingConfig, LandingResponse, LayerDef } from '@skywalking-horizon-ui/api-client';
import { bffClient } from '@/api/client';
@@ -54,6 +55,8 @@ export function useLayerLanding(
retry: 1,
});
+ useAutoRefreshSubscribe(() => q.refetch());
+
const data = computed<LandingResponse | null>(() => q.data.value ?? null);
const rows = computed(() => data.value?.rows ?? []);
const reachable = computed(() => data.value?.reachable ?? false);
diff --git a/apps/ui/src/composables/useLayerLogs.ts b/apps/ui/src/composables/useLayerLogs.ts
new file mode 100644
index 0000000..33d8b4c
--- /dev/null
+++ b/apps/ui/src/composables/useLayerLogs.ts
@@ -0,0 +1,118 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { computed, type Ref } from 'vue';
+import { useQuery } from '@tanstack/vue-query';
+import { useAutoRefreshSubscribe } from './useAutoRefreshSubscribe';
+import { bffClient } from '@/api/client';
+import type { LogFacetsResponse, LogsResponse, LogTagFilter } from '@/api/client';
+
+export interface LogListParams {
+ service: Ref<string | null>;
+ instanceId: Ref<string | null>;
+ endpointId: Ref<string | null>;
+ traceId: Ref<string | null>;
+ keywords: Ref<string[]>;
+ tags: Ref<LogTagFilter[]>;
+ page: Ref<number>;
+ pageSize: Ref<number>;
+}
+
+export function useLayerLogs(layerKey: Ref<string>, params: LogListParams) {
+ const q = useQuery<LogsResponse>({
+ queryKey: [
+ 'layer-logs',
+ layerKey,
+ params.service,
+ params.instanceId,
+ params.endpointId,
+ params.traceId,
+ params.keywords,
+ params.tags,
+ params.page,
+ params.pageSize,
+ ],
+ queryFn: () =>
+ bffClient.layerLogs(layerKey.value, {
+ ...(params.service.value ? { service: params.service.value } : {}),
+ ...(params.instanceId.value ? { serviceInstanceId: params.instanceId.value } : {}),
+ ...(params.endpointId.value ? { endpointId: params.endpointId.value } : {}),
+ ...(params.traceId.value ? { traceId: params.traceId.value } : {}),
+ ...(params.keywords.value.length > 0 ? { keywordsOfContent: params.keywords.value } : {}),
+ ...(params.tags.value.length > 0 ? { tags: params.tags.value } : {}),
+ page: params.page.value,
+ pageSize: params.pageSize.value,
+ }),
+ enabled: computed(() => layerKey.value.length > 0),
+ staleTime: 15_000,
+ });
+ useAutoRefreshSubscribe(() => q.refetch());
+
+ return {
+ data: computed(() => q.data.value ?? null),
+ logs: computed(() => q.data.value?.logs ?? []),
+ total: computed(() => q.data.value?.total ?? 0),
+ isLoading: q.isLoading,
+ isFetching: q.isFetching,
+ error: q.error,
+ refetch: q.refetch,
+ };
+}
+
+/**
+ * Window-scoped facets (level + service breakdown). Separate query so
+ * level toggles can update the row list without re-running the bigger
+ * facet sample. Keys intentionally omit `tags` + `page` + `pageSize`
+ * since facets are unfiltered + always sampled from page 1.
+ */
+export interface LogFacetParams {
+ service: Ref<string | null>;
+ instanceId: Ref<string | null>;
+ endpointId: Ref<string | null>;
+ traceId: Ref<string | null>;
+ keywords: Ref<string[]>;
+}
+
+export function useLayerLogFacets(layerKey: Ref<string>, params: LogFacetParams) {
+ const q = useQuery<LogFacetsResponse>({
+ queryKey: [
+ 'layer-log-facets',
+ layerKey,
+ params.service,
+ params.instanceId,
+ params.endpointId,
+ params.traceId,
+ params.keywords,
+ ],
+ queryFn: () =>
+ bffClient.layerLogFacets(layerKey.value, {
+ ...(params.service.value ? { service: params.service.value } : {}),
+ ...(params.instanceId.value ? { serviceInstanceId: params.instanceId.value } : {}),
+ ...(params.endpointId.value ? { endpointId: params.endpointId.value } : {}),
+ ...(params.traceId.value ? { traceId: params.traceId.value } : {}),
+ ...(params.keywords.value.length > 0 ? { keywordsOfContent: params.keywords.value } : {}),
+ sampleSize: 200,
+ }),
+ enabled: computed(() => layerKey.value.length > 0),
+ staleTime: 30_000,
+ });
+ return {
+ facets: computed(() => q.data.value ?? null),
+ isFetching: q.isFetching,
+ error: q.error,
+ };
+}
diff --git a/apps/ui/src/composables/useLayerTopology.ts b/apps/ui/src/composables/useLayerTopology.ts
index 77ad159..4cc1574 100644
--- a/apps/ui/src/composables/useLayerTopology.ts
+++ b/apps/ui/src/composables/useLayerTopology.ts
@@ -23,6 +23,7 @@
import { computed, type Ref } from 'vue';
import { useQuery } from '@tanstack/vue-query';
+import { useAutoRefreshSubscribe } from './useAutoRefreshSubscribe';
import { bffClient } from '@/api/client';
export function useLayerTopology(
@@ -37,6 +38,8 @@ export function useLayerTopology(
enabled: computed(() => layerKey.value.length > 0),
staleTime: 30_000,
});
+ useAutoRefreshSubscribe(() => q.refetch());
+
return {
data: computed(() => q.data.value ?? null),
nodes: computed(() => q.data.value?.nodes ?? []),
diff --git a/apps/ui/src/composables/useLayerTraces.ts b/apps/ui/src/composables/useLayerTraces.ts
new file mode 100644
index 0000000..d6442a1
--- /dev/null
+++ b/apps/ui/src/composables/useLayerTraces.ts
@@ -0,0 +1,131 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
... 6643 lines suppressed ...