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/06/11 04:13:28 UTC

(skywalking) branch master updated: SWIP-15: implement BanyanDB self-observability (cluster / container / group model) (#13903)

This is an automated email from the ASF dual-hosted git repository.

wu-sheng pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/skywalking.git


The following commit(s) were added to refs/heads/master by this push:
     new c9f816a4de SWIP-15: implement BanyanDB self-observability (cluster / container / group model) (#13903)
c9f816a4de is described below

commit c9f816a4de32c53e87d4ac27ec2f54c6c1cbe8a4
Author: 吴晟 Wu Sheng <wu...@foxmail.com>
AuthorDate: Thu Jun 11 12:13:22 2026 +0800

    SWIP-15: implement BanyanDB self-observability (cluster / container / group model) (#13903)
    
    ### Implement SWIP-15 BanyanDB self-observability around the cluster / container / group model
    
    Rebuilds the `otel-rules/banyandb/` self-observability feature to match how BanyanDB is actually built and operated (**requires BanyanDB 0.11+**):
    
    - **Entity model**: `Service` = one BanyanDB **cluster** (`service(['cluster'])`); `ServiceInstance` = one **container** keyed on `pod_name` + `container_name` (with `node_role` / `node_type` / `container_name` / `pod_name` as instance attributes); `Endpoint` = one storage **group**.
    - **Rules**: new `banyandb-endpoint.yaml`; `banyandb-service.yaml` (7 rules) and `banyandb-instance.yaml` (43 rules) redesigned. Source expressions mirror the upstream BanyanDB FODC-proxy Grafana boards (`grafana-fodc-nodes.json` / `grafana-fodc-workload.json`) so the SkyWalking dashboards stay in lockstep. The stale single-node `host_name` model and the removed `etcd_operation_rate` / `up`-derived `active_instance` / `queue_sub_total_msg_sent_err` metrics are dropped.
    - **Docs**: `docs/en/banyandb/dashboards-banyandb.md` rewritten to the cluster/container/group model.
    - **E2E**: rewritten to a no-FODC, file-discovery BanyanDB 0.11 cluster (1 liaison + 1 hot data node, no etcd). The OTel collector scrapes each node's `:2121` directly and injects the FODC-equivalent identity labels as static per-scrape-job labels, so the full rule set is exercised without a FODC proxy.
---
 docs/en/banyandb/dashboards-banyandb.md            |  212 +++-
 docs/en/changes/changes.md                         |    8 +
 docs/en/setup/backend/opentelemetry-receiver.md    |    3 +-
 .../banyandb/banyandb-endpoint.data.yaml           |  382 +++++++
 .../banyandb/banyandb-instance.data.yaml           | 1128 ++++++++++++++------
 .../banyandb/banyandb-service.data.yaml            |  551 +++-------
 .../otel-rules/banyandb/banyandb-endpoint.yaml     |   97 ++
 .../otel-rules/banyandb/banyandb-instance.yaml     |  217 ++--
 .../otel-rules/banyandb/banyandb-service.yaml      |  113 +-
 test/e2e-v2/cases/banyandb/banyandb-cases.yaml     |   63 +-
 test/e2e-v2/cases/banyandb/docker-compose.yml      |   60 +-
 test/e2e-v2/cases/banyandb/e2e.yaml                |    5 +-
 .../metrics-has-label-value.yml}                   |   57 +-
 .../{otel-collector-config.yaml => nodes.yaml}     |   39 +-
 .../cases/banyandb/otel-collector-config.yaml      |   33 +-
 .../zk/banyandb/banyandb-cluster-cases.yaml        |   98 ++
 test/e2e-v2/cases/cluster/zk/banyandb/e2e.yaml     |    2 +-
 .../zk/banyandb/expected/metrics-day-first.yml}    |   51 +-
 .../zk/banyandb/expected/metrics-day-second.yml}   |   51 +-
 .../zk/banyandb/expected/metrics-hour-first.yml}   |   51 +-
 .../zk/banyandb/expected/metrics-hour-second.yml}  |   51 +-
 .../zk/banyandb/expected/metrics-minute-first.yml} |   51 +-
 .../banyandb/expected/metrics-minute-second.yml}   |   51 +-
 test/e2e-v2/script/docker-compose/base-compose.yml |    6 +-
 test/e2e-v2/script/env                             |    2 +-
 25 files changed, 2196 insertions(+), 1186 deletions(-)

diff --git a/docs/en/banyandb/dashboards-banyandb.md b/docs/en/banyandb/dashboards-banyandb.md
index 1d07018f41..78e9094144 100644
--- a/docs/en/banyandb/dashboards-banyandb.md
+++ b/docs/en/banyandb/dashboards-banyandb.md
@@ -1,49 +1,177 @@
-# BanyanDB self observability dashboard
+# BanyanDB self-observability dashboard
 
-[BanyanDB](https://skywalking.apache.org/docs/skywalking-banyandb/next/readme/), as an observability database, aims to ingest, analyze and store Metrics, Tracing, and Logging data. It's designed to handle observability data generated by **Apache SkyWalking**,it also provides a dashboard to visualize the self-observability metrics.
+[Apache SkyWalking BanyanDB](https://skywalking.apache.org/docs/skywalking-banyandb/next/readme/) is the
+native storage for SkyWalking. A production deployment is one **cluster** made of many **nodes**, each
+running one or more **containers** with a role (`liaison` front door, `data` backend, and the `lifecycle`
+tier-migration sidecar), and data is organized into **groups**. SkyWalking models that reality directly
+and renders it on the `Layer: BANYANDB` dashboards in the Horizon UI:
+
+| SkyWalking entity | BanyanDB concept | Identity |
+| ----------------- | ---------------- | -------- |
+| `Service` | one BanyanDB **cluster** | the `cluster` label |
+| `ServiceInstance` | one **container** on a node | `pod_name` + `container_name` (joined by `@`) |
+| &nbsp;&nbsp;↳ attributes | role / tier | `container_name` (`liaison`/`data`/`lifecycle`), `node_type` (`hot`/`warm`/`cold`), `node_role`, `pod_name` |
+| `Endpoint` | one **group** (storage partition) | the `group` label (e.g. `sw_metricsMinute`) |
+
+> **Requires BanyanDB 0.11+.** This feature reads the FODC-proxy cluster-observability metric families
+> and the queue / lifecycle metric families that BanyanDB introduced after 0.10. Run a 0.11+ cluster
+> with the FODC proxy and the Prometheus metrics provider enabled.
 
 ## Data flow
-1. [BanyanDB](https://skywalking.apache.org/docs/skywalking-banyandb/next/readme/) collects metrics data internally and exposes a Prometheus http endpoint to retrieve the metrics.
-2. OpenTelemetry Collector fetches metrics from BanyanDB and pushes metrics to SkyWalking OAP Server via OpenTelemetry gRPC exporter.
-3. The SkyWalking OAP Server parses the expression with [MAL](../concepts-and-designs/mal.md) to filter/calculate/aggregate and store the results.
+
+1. Each BanyanDB container exposes its metrics; in a cluster the
+   [FODC proxy](https://skywalking.apache.org/docs/skywalking-banyandb/next/operation/fodc/overview/)
+   aggregates every container's Prometheus metrics onto a single `/metrics` endpoint (default `:17913`)
+   and stamps each sample with per-container identity labels (`pod_name`, `container_name`, `node_role`,
+   and `node_type` on data containers).
+2. An OpenTelemetry Collector scrapes the FODC proxy `/metrics` as the single Prometheus target, adds a
+   static `cluster: <name>` label (the only label SkyWalking must inject), and pushes via the
+   OpenTelemetry gRPC exporter to the SkyWalking OAP Server.
+3. The OAP Server parses the [MAL](../concepts-and-designs/mal.md) rules under `otel-rules/banyandb/` to
+   filter / calculate / aggregate and store the cluster, instance and group metrics.
 
 ## Set up
-1. Start [BanyanDB](https://skywalking.apache.org/docs/skywalking-banyandb/next/readme/),supporting both [Standalone Mode](https://skywalking.apache.org/docs/skywalking-banyandb/next/installation/standalone/) and [Cluster Mode](https://skywalking.apache.org/docs/skywalking-banyandb/next/installation/cluster/).
-2. Set up [OpenTelemetry Collector ](https://opentelemetry.io/docs/collector/getting-started/#docker). For details on Prometheus Receiver in OpenTelemetry Collector, refer to [here](../../../test/e2e-v2/cases/banyandb/otel-collector-config.yaml).
-3. Config SkyWalking [OpenTelemetry receiver](https://skywalking.apache.org/docs/main/next/en/setup/backend/opentelemetry-receiver/).
-
-## BanyanDB monitoring
-Self observability monitoring provides monitoring of the status and resources of the [BanyanDB](https://skywalking.apache.org/docs/skywalking-banyandb/next/readme/) server itself. `banyandb-server` is a `Service` in BanyanDB, and land on the `Layer: BANYANDB`.
-
-### Self observability metrics
-
-| Unit | Metric Name                                       | Description | Data Source |
-|------|---------------------------------------------------|-------------|-------------|
-| o/s | meter_banyandb_write_rate                        | Write Rate (Operations per Second) | BanyanDB |
-| GiB | meter_banyandb_total_memory                      | Total Memory | BanyanDB |
-| GiB | meter_banyandb_disk_usage                        | Disk Usage | BanyanDB |
-| r/s | meter_banyandb_query_rate                        | Query Rate (Requests per Second) | BanyanDB |
-| Count | meter_banyandb_total_cpu                        | Total CPU Cores | BanyanDB |
-| c/m | meter_banyandb_write_and_query_errors_rate      | Write and Query Errors Rate(Counts per Minute) | BanyanDB |
-| c/s | meter_banyandb_etcd_operation_rate               | Etcd Operation Rate(Counts per Second) | BanyanDB |
-| Count | meter_banyandb_active_instance                  | Active Instances | BanyanDB |
-| % | meter_banyandb_cpu_usage                        | CPU Usage Percentage | BanyanDB |
-| % | meter_banyandb_rss_memory_usage                 | RSS Memory Usage Percentage | BanyanDB |
-| % | meter_banyandb_disk_usage_all                   | Disk Usage Percentage | BanyanDB |
-| KiB/s | meter_banyandb_network_usage_recv               | Network Receive Rate | BanyanDB |
-| KiB/s | meter_banyandb_network_usage_sent               | Network Send Rate | BanyanDB |
-| o/s | meter_banyandb_storage_write_rate               | Storage Write Rate (Operations per Second) | BanyanDB |
-| s | meter_banyandb_query_latency                    | Query Latency (s) | BanyanDB |
-| Count | meter_banyandb_total_data                      | Total Data Elements | BanyanDB |
-| r/m | meter_banyandb_merge_file_data                 | Merge File Data Rate(Revolutions per Minute) | BanyanDB |
-| s | meter_banyandb_merge_file_latency              | Merge File Latency(s) | BanyanDB |
-| Count | meter_banyandb_merge_file_partitions          | Merge File Partitions | BanyanDB |
-| o/s | meter_banyandb_series_write_rate               | Series Write Rate (Operations per Second) | BanyanDB |
-| o/s | meter_banyandb_series_term_search_rate         | Series Term Search Rate (Operations per Second) | BanyanDB |
-| Count | meter_banyandb_total_series                   | Total Series Count | BanyanDB |
-| ops | meter_banyandb_stream_write_rate              | Stream Write Rate (Operations per Second) | BanyanDB |
-| ops | meter_banyandb_term_search_rate                | Term Search Rate (Operations per Second) | BanyanDB |
-| Count | meter_banyandb_total_document                 | Total Document Count | BanyanDB |
+
+1. Run a BanyanDB **0.11+** cluster (liaison + data nodes; data nodes may be tiered hot/warm/cold) with
+   the **FODC proxy** enabled and the Prometheus metrics provider on (default). Standalone mode is the
+   degenerate case — one cluster, one node, one `container_name=standalone`.
+2. Run an **OpenTelemetry Collector** whose `prometheus` receiver scrapes the FODC proxy `/metrics`
+   (`:17913`) as the single target and adds a static `cluster: <name>` label, exporting OTLP to OAP.
+   The FODC proxy already stamps the per-node identity labels (`pod_name` / `container_name` /
+   `node_role` / `node_type`), so `cluster` is the only label the collector must inject.
+
+   > **The scrape `job_name` MUST be `banyandb-monitoring`.** Every rule file filters on
+   > `{ tags -> tags.job_name == 'banyandb-monitoring' }` (the OTel receiver maps the Prometheus `job`
+   > to the `job_name` tag), so a differently-named job produces no metrics.
+
+   ```yaml
+   receivers:
+     prometheus:
+       config:
+         scrape_configs:
+           - job_name: "banyandb-monitoring"          # REQUIRED — the rules filter on it
+             scrape_interval: 15s
+             static_configs:
+               - targets: ["<fodc-proxy-host>:17913"] # the FODC proxy aggregates every node's metrics
+                 labels:
+                   cluster: <your-cluster-name>        # the only label SkyWalking must inject
+   exporters:
+     otlp:
+       endpoint: <oap-host>:11800
+       tls:
+         insecure: true
+   service:
+     pipelines:
+       metrics:
+         receivers: [prometheus]
+         processors: [batch]
+         exporters: [otlp]
+   ```
+
+   Scrape the **FODC proxy**, not the individual nodes. The proxy resolves each node's identity from
+   the cluster and stamps `pod_name` / `container_name` / `node_role` and the data-node tier
+   (`node_type`) onto every sample — context the raw per-node `:2121` endpoints do not carry. Direct
+   per-node scraping is not recommended: it would have to hand-inject all of those identity labels for
+   every node. (The e2e does exactly that only because it runs no FODC proxy — see the
+   [test collector config](../../../test/e2e-v2/cases/banyandb/otel-collector-config.yaml) — and is not
+   a production pattern.)
+3. Enable SkyWalking's
+   [OpenTelemetry receiver](https://skywalking.apache.org/docs/main/next/en/setup/backend/opentelemetry-receiver/).
+   The `banyandb/*` rules are enabled by default in `enabledOtelMetricsRules`.
+4. Open the **Horizon UI** → `BanyanDB` layer.
+
+## Metrics
+
+The metric source expressions mirror the upstream BanyanDB Grafana boards, so the SkyWalking dashboards
+stay in lockstep with the BanyanDB catalog. The rule files are
+`otel-rules/banyandb/banyandb-service.yaml`, `banyandb-instance.yaml` and `banyandb-endpoint.yaml`.
+
+### Service scope — cluster summary (`meter_banyandb_*`)
+
+| Unit | Metric | Description |
+| ---- | ------ | ----------- |
+| w/s | `meter_banyandb_cluster_write_rate` | Cluster write rate across measure/stream/trace |
+| r/s | `meter_banyandb_cluster_query_rate` | Cluster query rate |
+| c/m | `meter_banyandb_cluster_error_rate` | Cluster error rate (counts/min) |
+| Count | `meter_banyandb_reporting_instances` | Live container count by role |
+| Count | `meter_banyandb_total_cpu_cores` | Cluster CPU capacity |
+| Bytes | `meter_banyandb_total_memory_used` | Cluster memory used |
+| Bytes | `meter_banyandb_total_disk_used` | Cluster disk used |
+
+### Instance scope — per container (`meter_banyandb_instance_*`)
+
+**All roles** (every container emits these):
+
+| Unit | Metric | Description |
+| ---- | ------ | ----------- |
+| s | `node_uptime` | Node uptime |
+| Cores | `cpu_usage` | CPU usage |
+| Bytes | `rss_memory` | Resident memory |
+| percentunit | `system_memory_percent` | System memory used fraction |
+| percentunit | `disk_usage_percent` | Disk used fraction (Σused/Σtotal) |
+| Bytes | `disk_used_by_path` / `disk_total_by_path` | Disk used / total by mount path |
+| percentunit | `disk_used_percent_by_path` | Disk used fraction by mount path |
+| Bytes/s | `network_recv` / `network_sent` | Network throughput by interface |
+| Count | `goroutines` | Go goroutines |
+| s | `gc_pause_avg` | Average GC pause |
+| Bytes | `heap_inuse` / `heap_next_gc` | Go heap in-use / next-GC threshold |
+| Bytes/s | `alloc_rate` | Go allocation rate |
+
+**Liaison** (front door; the dashboard gates these on `container_name == 'liaison'`):
+
+| Unit | Metric | Description |
+| ---- | ------ | ----------- |
+| r/s | `query_rate_by_service` | Query rate by data-model service |
+| c/m | `grpc_error_rate` | gRPC error rate |
+| r/s | `non_query_op_rate` | Registry / non-query operation rate |
+| w/s | `write_rate` | Write rate seen at the front door |
+| ops | `publish_throughput` | Tier-2 publish throughput by operation |
+| Bytes/s | `publish_bytes` | Publish bytes |
+| s | `publish_latency_p99` | Publish send latency p99 |
+| Count | `wqueue_pending` / `wqueue_file_parts` / `wqueue_mem_part` | Write-queue depth |
+
+**Data** (backend; the dashboard gates these on `container_name == 'data'`):
+
+| Unit | Metric | Description |
+| ---- | ------ | ----------- |
+| Count | `total_data` | Total stored data elements |
+| o/s | `merge_file_rate` | Merge-loop rate |
+| Count | `merge_file_partitions` | Avg parts merged per loop |
+| s | `merge_file_latency` | Avg file-merge latency |
+| o/s | `series_write_rate` / `series_term_search_rate` | Inverted-index write / term-search rate |
+| Count | `total_series` | Inverted-index documents |
+| o/s | `stream_tst_write_rate` / `stream_tst_term_search_rate` | Stream tst index write / term-search rate |
+| Count | `stream_tst_total_docs` | Stream tst index documents |
+| ops | `queue_sub_throughput` | Subscribe-queue throughput by operation |
+| s | `queue_sub_latency_p99` | Subscribe-queue latency p99 |
+| percent | `retention_measure_disk_usage_percent` / `retention_stream_disk_usage_percent` / `retention_trace_disk_usage_percent` | Retention disk-usage % per scope |
+
+**Lifecycle** (the tier-migration sidecar on hot/warm data pods; `container_name == 'lifecycle'`):
+
+| Unit | Metric | Description |
+| ---- | ------ | ----------- |
+| Count | `lifecycle_cycles` | Cumulative migration cycles |
+| s | `lifecycle_last_run` | Seconds since the last migration cycle started |
+| Status | `lifecycle_last_run_success` | Last cycle status (1 = OK, 0 = failed) |
+
+### Endpoint scope — per group (`meter_banyandb_endpoint_*`)
+
+| Unit | Metric | Description |
+| ---- | ------ | ----------- |
+| w/s | `write_rate` | Write rate for the group |
+| s | `query_latency` | Mean query latency for the group |
+| Count | `total_data` | Total stored data elements for the group |
+| o/s | `merge_file_rate` | Merge-loop rate for the group |
+| s | `merge_file_latency` | Avg file-merge latency for the group |
+| Count | `merge_file_partitions` | Avg parts merged per loop for the group |
+| o/s | `series_write_rate` | Inverted-index write rate for the group |
+| Count | `total_series` | Inverted-index documents for the group |
+| ops | `queue_throughput` | Subscribe-queue throughput by operation for the group |
+| s | `queue_latency_p99` | Publish-queue latency p99 for the group |
+| Bytes/s | `publish_bytes` | Publish bytes for the group |
 
 ## Customizations
-You can customize your own metrics/expression/dashboard panel.The metrics definition and expression rules are found in `/config/otel-rules/banyandb`.The [BanyanDB](https://skywalking.apache.org/docs/skywalking-banyandb/next/readme/) dashboard panel configurations ship from the SkyWalking Horizon UI bundle (apache/skywalking-horizon-ui); the OAP backend no longer hosts UI dashboard JSONs.
+
+You can customize your own metrics / expressions. The metric definitions and expression rules are in
+`/config/otel-rules/banyandb`. The dashboard panel configurations ship from the SkyWalking Horizon UI
+bundle (apache/skywalking-horizon-ui); the OAP backend does not host UI dashboard JSONs.
diff --git a/docs/en/changes/changes.md b/docs/en/changes/changes.md
index a01478649c..3cc1f13e18 100644
--- a/docs/en/changes/changes.md
+++ b/docs/en/changes/changes.md
@@ -242,6 +242,14 @@
   admin-host only" entry above for the public REST retirement.
 
 #### OAP Server
+* SWIP-15: rebuild BanyanDB self-observability around the cluster / container / group model
+  (requires BanyanDB 0.11+). `otel-rules/banyandb/` now models a BanyanDB cluster as one `Service`
+  (`service(['cluster'])`), each container as a `ServiceInstance` keyed on `pod_name` + `container_name`
+  (with `node_role` / `node_type` / `container_name` / `pod_name` as instance attributes), and each
+  storage group as an `Endpoint`. New `banyandb-endpoint.yaml`; `banyandb-service.yaml` and
+  `banyandb-instance.yaml` redesigned to mirror the upstream FODC-proxy Grafana boards. The stale
+  single-node `host_name` model and the removed `etcd_operation_rate` / `up`-derived `active_instance`
+  metrics are gone.
 * Runtime MAL/LAL hot-update rules can declare `layerDefinitions:` to introduce new
   layers. Ordinals are operator-pinned in the `100_000+` tier; the layer is
   refcount-tracked and unregistered when the last declaring rule is removed. See
diff --git a/docs/en/setup/backend/opentelemetry-receiver.md b/docs/en/setup/backend/opentelemetry-receiver.md
index ac7924ea40..337f67dfb1 100644
--- a/docs/en/setup/backend/opentelemetry-receiver.md
+++ b/docs/en/setup/backend/opentelemetry-receiver.md
@@ -83,5 +83,6 @@ the natural dot-to-underscore conversion (e.g., `host.name` → `host_name` in M
 | Metrics of Flink                        | otel-rules/flink/flink-jobManager.yaml              | flink jobManager -> OpenTelemetry Collector -- OTLP exporter --> SkyWalking OAP Server                                 |
 | Metrics of Flink                        | otel-rules/flink/flink-taskManager.yaml             | flink taskManager -> OpenTelemetry Collector -- OTLP exporter --> SkyWalking OAP Server                                |
 | Metrics of Flink                        | otel-rules/flink/flink-job.yaml                     | flink jobManager &  flink taskManager-> OpenTelemetry Collector -- OTLP exporter --> SkyWalking OAP Server             |
+| Metrics of BanyanDB                     | otel-rules/banyandb/banyandb-service.yaml           | BanyanDB(embedded prometheus endpoint) -> OpenTelemetry Collector – OTLP exporter –> SkyWalking OAP Server             |
 | Metrics of BanyanDB                     | otel-rules/banyandb/banyandb-instance.yaml          | BanyanDB(embedded prometheus endpoint) -> OpenTelemetry Collector – OTLP exporter –> SkyWalking OAP Server             |
-| Metrics of BanyanDB                     | otel-rules/banyandb/banyandb-service.yaml           | BanyanDB(embedded prometheus endpoint) -> OpenTelemetry Collector – OTLP exporter –> SkyWalking OAP Server             |
\ No newline at end of file
+| Metrics of BanyanDB                     | otel-rules/banyandb/banyandb-endpoint.yaml          | BanyanDB(embedded prometheus endpoint) -> OpenTelemetry Collector – OTLP exporter –> SkyWalking OAP Server             |
\ No newline at end of file
diff --git a/oap-server/analyzer/meter-analyzer-scripts-test/src/test/resources/scripts/mal/test-otel-rules/banyandb/banyandb-endpoint.data.yaml b/oap-server/analyzer/meter-analyzer-scripts-test/src/test/resources/scripts/mal/test-otel-rules/banyandb/banyandb-endpoint.data.yaml
new file mode 100644
index 0000000000..0cf20a0121
--- /dev/null
+++ b/oap-server/analyzer/meter-analyzer-scripts-test/src/test/resources/scripts/mal/test-otel-rules/banyandb/banyandb-endpoint.data.yaml
@@ -0,0 +1,382 @@
+# 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.
+
+script: oap-server/server-starter/src/main/resources/otel-rules/banyandb/banyandb-endpoint.yaml
+input:
+  banyandb_liaison_grpc_total_latency:
+    - labels:
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+        method: 'query'
+        service: 'measure'
+        group: 'test-group'
+      value: 100.0
+  banyandb_liaison_grpc_total_started:
+    - labels:
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+        method: 'query'
+        service: 'measure'
+        group: 'test-group'
+      value: 100.0
+    - labels:
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+        method: 'write'
+        service: 'measure'
+        group: 'test-group'
+      value: 100.0
+  banyandb_measure_inverted_index_total_doc_count:
+    - labels:
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+        group: 'test-group'
+        seg: 'seg1'
+      value: 100.0
+  banyandb_measure_inverted_index_total_updates:
+    - labels:
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+        group: 'test-group'
+        seg: 'seg1'
+      value: 100.0
+  banyandb_measure_total_file_elements:
+    - labels:
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+        group: 'test-group'
+      value: 100.0
+  banyandb_measure_total_merge_latency:
+    - labels:
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+        type: 'file'
+        group: 'test-group'
+      value: 100.0
+  banyandb_measure_total_merge_loop_started:
+    - labels:
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+        group: 'test-group'
+      value: 100.0
+  banyandb_measure_total_merged_parts:
+    - labels:
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+        type: 'file'
+        group: 'test-group'
+      value: 100.0
+  banyandb_measure_total_written:
+    - labels:
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+        group: 'test-group'
+      value: 100.0
+  banyandb_queue_pub_sent_bytes:
+    - labels:
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+        operation: 'batch-write'
+        group: 'test-group'
+        remote_node: 'n1'
+        remote_role: 'data'
+        remote_tier: 'hot'
+      value: 100.0
+  banyandb_queue_pub_total_latency:
+    - labels:
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+        operation: 'batch-write'
+        group: 'test-group'
+        le: '50'
+        remote_node: 'n1'
+        remote_role: 'data'
+        remote_tier: 'hot'
+      value: 5.0
+    - labels:
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+        operation: 'batch-write'
+        group: 'test-group'
+        le: '100'
+        remote_node: 'n1'
+        remote_role: 'data'
+        remote_tier: 'hot'
+      value: 10.0
+    - labels:
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+        operation: 'batch-write'
+        group: 'test-group'
+        le: '500'
+        remote_node: 'n1'
+        remote_role: 'data'
+        remote_tier: 'hot'
+      value: 12.0
+  banyandb_queue_sub_total_finished:
+    - labels:
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+        operation: 'batch-write'
+        group: 'test-group'
+        remote_node: 'n1'
+        remote_role: 'data'
+      value: 100.0
+  banyandb_stream_storage_inverted_index_total_doc_count:
+    - labels:
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+        group: 'test-group'
+        seg: 'seg1'
+      value: 100.0
+  banyandb_stream_storage_inverted_index_total_updates:
+    - labels:
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+        group: 'test-group'
+        seg: 'seg1'
+      value: 100.0
+  banyandb_stream_tst_inverted_index_total_doc_count:
+    - labels:
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+        group: 'test-group'
+        seg: 'seg1'
+      value: 100.0
+  banyandb_stream_tst_inverted_index_total_updates:
+    - labels:
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+        group: 'test-group'
+        seg: 'seg1'
+      value: 100.0
+  banyandb_stream_tst_total_file_elements:
+    - labels:
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+        group: 'test-group'
+      value: 100.0
+  banyandb_stream_tst_total_merge_latency:
+    - labels:
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+        type: 'file'
+        group: 'test-group'
+      value: 100.0
+  banyandb_stream_tst_total_merge_loop_started:
+    - labels:
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+        group: 'test-group'
+      value: 100.0
+  banyandb_stream_tst_total_merged_parts:
+    - labels:
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+        type: 'file'
+        group: 'test-group'
+      value: 100.0
+  banyandb_stream_tst_total_written:
+    - labels:
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+        group: 'test-group'
+      value: 100.0
+  banyandb_trace_tst_total_file_elements:
+    - labels:
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+        group: 'test-group'
+      value: 100.0
+  banyandb_trace_tst_total_merge_latency:
+    - labels:
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+        type: 'file'
+        group: 'test-group'
+      value: 100.0
+  banyandb_trace_tst_total_merge_loop_started:
+    - labels:
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+        group: 'test-group'
+      value: 100.0
+  banyandb_trace_tst_total_merged_parts:
+    - labels:
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+        type: 'file'
+        group: 'test-group'
+      value: 100.0
+  banyandb_trace_tst_total_written:
+    - labels:
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+        group: 'test-group'
+      value: 100.0
+expected:
+  meter_banyandb_endpoint_write_rate:
+    entities:
+      - scope: ENDPOINT
+        service: test-cluster
+        endpoint: test-group
+        layer: BANYANDB
+  meter_banyandb_endpoint_query_latency:
+    entities:
+      - scope: ENDPOINT
+        service: test-cluster
+        endpoint: test-group
+        layer: BANYANDB
+  meter_banyandb_endpoint_total_data:
+    entities:
+      - scope: ENDPOINT
+        service: test-cluster
+        endpoint: test-group
+        layer: BANYANDB
+  meter_banyandb_endpoint_merge_file_rate:
+    entities:
+      - scope: ENDPOINT
+        service: test-cluster
+        endpoint: test-group
+        layer: BANYANDB
+  meter_banyandb_endpoint_merge_file_latency:
+    entities:
+      - scope: ENDPOINT
+        service: test-cluster
+        endpoint: test-group
+        layer: BANYANDB
+  meter_banyandb_endpoint_merge_file_partitions:
+    entities:
+      - scope: ENDPOINT
+        service: test-cluster
+        endpoint: test-group
+        layer: BANYANDB
+  meter_banyandb_endpoint_series_write_rate:
+    entities:
+      - scope: ENDPOINT
+        service: test-cluster
+        endpoint: test-group
+        layer: BANYANDB
+  meter_banyandb_endpoint_total_series:
+    entities:
+      - scope: ENDPOINT
+        service: test-cluster
+        endpoint: test-group
+        layer: BANYANDB
+  meter_banyandb_endpoint_queue_throughput:
+    entities:
+      - scope: ENDPOINT
+        service: test-cluster
+        endpoint: test-group
+        layer: BANYANDB
+  meter_banyandb_endpoint_queue_latency_p99:
+    entities:
+      - scope: ENDPOINT
+        service: test-cluster
+        endpoint: test-group
+        layer: BANYANDB
+  meter_banyandb_endpoint_publish_bytes:
+    entities:
+      - scope: ENDPOINT
+        service: test-cluster
+        endpoint: test-group
+        layer: BANYANDB
diff --git a/oap-server/analyzer/meter-analyzer-scripts-test/src/test/resources/scripts/mal/test-otel-rules/banyandb/banyandb-instance.data.yaml b/oap-server/analyzer/meter-analyzer-scripts-test/src/test/resources/scripts/mal/test-otel-rules/banyandb/banyandb-instance.data.yaml
index 5eb1c4f919..357a109c84 100644
--- a/oap-server/analyzer/meter-analyzer-scripts-test/src/test/resources/scripts/mal/test-otel-rules/banyandb/banyandb-instance.data.yaml
+++ b/oap-server/analyzer/meter-analyzer-scripts-test/src/test/resources/scripts/mal/test-otel-rules/banyandb/banyandb-instance.data.yaml
@@ -15,466 +15,948 @@
 
 script: oap-server/server-starter/src/main/resources/otel-rules/banyandb/banyandb-instance.yaml
 input:
-  banyandb_measure_total_written:
+  banyandb_liaison_grpc_total_err:
     - labels:
-        group: test-value
-        host_name: test-host
-        service_instance_id: test-instance
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+        method: 'query'
+        service: 'measure'
+        group: 'test-group'
       value: 100.0
-  banyandb_stream_tst_total_written:
+  banyandb_liaison_grpc_total_registry_err:
     - labels:
-        group: test-value
-        host_name: test-host
-        service_instance_id: test-instance
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+        method: 'query'
+        service: 'measure'
+        group: 'test-group'
       value: 100.0
-  banyandb_system_memory_state:
+  banyandb_liaison_grpc_total_registry_started:
     - labels:
-        host_name: test-host
-        service_instance_id: test-instance
-        kind: total
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+        method: 'list'
+        service: 'measure'
       value: 100.0
+  banyandb_liaison_grpc_total_started:
     - labels:
-        host_name: test-host
-        service_instance_id: test-instance
-        kind: used
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+        method: 'query'
+        service: 'measure'
+        group: 'test-group'
       value: 100.0
-  banyandb_system_disk:
     - labels:
-        host_name: test-host
-        service_instance_id: test-instance
-        kind: used
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+        method: 'write'
+        service: 'measure'
+        group: 'test-group'
       value: 100.0
+  banyandb_liaison_grpc_total_stream_msg_received_err:
     - labels:
-        host_name: test-host
-        service_instance_id: test-instance
-        kind: total
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+        method: 'query'
+        service: 'measure'
+        group: 'test-group'
       value: 100.0
-  banyandb_liaison_grpc_total_started:
+  banyandb_lifecycle_cycles_total:
     - labels:
-        method: query
-        host_name: test-host
-        service_instance_id: test-instance
-        group: test-value
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
       value: 100.0
-  banyandb_system_cpu_num:
+  banyandb_lifecycle_last_run_success:
     - labels:
-        host_name: test-host
-        service_instance_id: test-instance
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
       value: 100.0
-  banyandb_liaison_grpc_total_err:
+  banyandb_lifecycle_last_run_timestamp_seconds:
     - labels:
-        method: query
-        host_name: test-host
-        service_instance_id: test-instance
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
       value: 100.0
-  banyandb_liaison_grpc_total_stream_msg_sent_err:
+  banyandb_measure_inverted_index_total_doc_count:
     - labels:
-        method: query
-        host_name: test-host
-        service_instance_id: test-instance
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+        group: 'test-group'
+        seg: 'seg1'
       value: 100.0
-  banyandb_liaison_grpc_total_stream_msg_received_err:
+  banyandb_measure_inverted_index_total_term_searchers_started:
     - labels:
-        method: query
-        host_name: test-host
-        service_instance_id: test-instance
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+        group: 'test-group'
+        seg: 'seg1'
       value: 100.0
-  banyandb_queue_sub_total_msg_sent_err:
+  banyandb_measure_inverted_index_total_updates:
     - labels:
-        method: query
-        host_name: test-host
-        service_instance_id: test-instance
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+        group: 'test-group'
+        seg: 'seg1'
       value: 100.0
-  banyandb_liaison_grpc_total_registry_started:
+  banyandb_measure_pending_data_count:
     - labels:
-        host_name: test-host
-        service_instance_id: test-instance
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+        group: 'test-group'
       value: 100.0
-  up:
+  banyandb_measure_total_file_elements:
     - labels:
-        host_name: test-host
-        service_instance_id: test-instance
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+        group: 'test-group'
       value: 100.0
-  process_cpu_seconds_total:
+  banyandb_measure_total_file_parts:
     - labels:
-        host_name: test-host
-        service_instance_id: test-instance
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+        group: 'test-group'
       value: 100.0
-  process_resident_memory_bytes:
+  banyandb_measure_total_mem_part:
     - labels:
-        host_name: test-host
-        service_instance_id: test-instance
-        kind: total
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+        group: 'test-group'
       value: 100.0
-  banyandb_system_net_state:
+  banyandb_measure_total_merge_latency:
     - labels:
-        host_name: test-host
-        service_instance_id: test-instance
-        kind: bytes_recv
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+        type: 'file'
+        group: 'test-group'
       value: 100.0
+  banyandb_measure_total_merge_loop_started:
     - labels:
-        host_name: test-host
-        service_instance_id: test-instance
-        kind: bytes_sent
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+        group: 'test-group'
       value: 100.0
-  banyandb_liaison_grpc_total_latency:
+  banyandb_measure_total_merged_parts:
     - labels:
-        group: test-value
-        host_name: test-host
-        service_instance_id: test-instance
-        method: query
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+        type: 'file'
+        group: 'test-group'
       value: 100.0
-  banyandb_measure_total_file_elements:
+  banyandb_measure_total_written:
     - labels:
-        group: test-value
-        host_name: test-host
-        service_instance_id: test-instance
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+        group: 'test-group'
       value: 100.0
-  banyandb_measure_total_merge_loop_started:
+  banyandb_queue_pub_sent_bytes:
     - labels:
-        group: test-value
-        host_name: test-host
-        service_instance_id: test-instance
-        type: file
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+        operation: 'batch-write'
+        group: 'test-group'
+        remote_node: 'n1'
+        remote_role: 'data'
+        remote_tier: 'hot'
       value: 100.0
-  banyandb_measure_total_merge_latency:
+  banyandb_queue_pub_total_finished:
     - labels:
-        group: test-value
-        host_name: test-host
-        service_instance_id: test-instance
-        type: file
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+        operation: 'batch-write'
+        group: 'test-group'
+        remote_node: 'n1'
+        remote_role: 'data'
+        remote_tier: 'hot'
       value: 100.0
-  banyandb_measure_total_merged_parts:
+  banyandb_queue_pub_total_latency:
+    - labels:
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+        operation: 'batch-write'
+        group: 'test-group'
+        le: '50'
+        remote_node: 'n1'
+        remote_role: 'data'
+        remote_tier: 'hot'
+      value: 5.0
+    - labels:
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+        operation: 'batch-write'
+        group: 'test-group'
+        le: '100'
+        remote_node: 'n1'
+        remote_role: 'data'
+        remote_tier: 'hot'
+      value: 10.0
+    - labels:
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+        operation: 'batch-write'
+        group: 'test-group'
+        le: '500'
+        remote_node: 'n1'
+        remote_role: 'data'
+        remote_tier: 'hot'
+      value: 12.0
+  banyandb_queue_sub_total_finished:
     - labels:
-        group: test-value
-        host_name: test-host
-        service_instance_id: test-instance
-        type: file
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+        operation: 'batch-write'
+        group: 'test-group'
+        remote_node: 'n1'
+        remote_role: 'data'
       value: 100.0
-  banyandb_measure_inverted_index_total_updates:
+  banyandb_queue_sub_total_latency:
+    - labels:
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+        operation: 'batch-write'
+        group: 'test-group'
+        le: '50'
+        remote_node: 'n1'
+        remote_role: 'data'
+      value: 5.0
+    - labels:
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+        operation: 'batch-write'
+        group: 'test-group'
+        le: '100'
+        remote_node: 'n1'
+        remote_role: 'data'
+      value: 10.0
+    - labels:
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+        operation: 'batch-write'
+        group: 'test-group'
+        le: '500'
+        remote_node: 'n1'
+        remote_role: 'data'
+      value: 12.0
+  banyandb_storage_retention_measure_disk_usage_percent:
+    - labels:
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+        service: 'measure'
+      value: 100.0
+  banyandb_storage_retention_stream_disk_usage_percent:
+    - labels:
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+        service: 'measure'
+      value: 100.0
+  banyandb_storage_retention_trace_disk_usage_percent:
     - labels:
-        group: test-value
-        host_name: test-host
-        service_instance_id: test-instance
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+        service: 'measure'
+      value: 100.0
+  banyandb_stream_storage_inverted_index_total_doc_count:
+    - labels:
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+        group: 'test-group'
+        seg: 'seg1'
       value: 100.0
   banyandb_stream_storage_inverted_index_total_term_searchers_started:
     - labels:
-        group: test-value
-        host_name: test-host
-        service_instance_id: test-instance
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+        group: 'test-group'
+        seg: 'seg1'
       value: 100.0
-  banyandb_measure_inverted_index_total_doc_count:
+  banyandb_stream_storage_inverted_index_total_updates:
     - labels:
-        group: test-value
-        host_name: test-host
-        service_instance_id: test-instance
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+        group: 'test-group'
+        seg: 'seg1'
       value: 100.0
-  banyandb_stream_tst_inverted_index_total_updates:
+  banyandb_stream_tst_inverted_index_total_doc_count:
     - labels:
-        group: test-value
-        host_name: test-host
-        service_instance_id: test-instance
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+        group: 'test-group'
+        seg: 'seg1'
       value: 100.0
   banyandb_stream_tst_inverted_index_total_term_searchers_started:
     - labels:
-        group: test-value
-        host_name: test-host
-        service_instance_id: test-instance
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+        group: 'test-group'
+        seg: 'seg1'
       value: 100.0
-  banyandb_stream_tst_inverted_index_total_doc_count:
+  banyandb_stream_tst_inverted_index_total_updates:
+    - labels:
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+        group: 'test-group'
+        seg: 'seg1'
+      value: 100.0
+  banyandb_stream_tst_pending_data_count:
+    - labels:
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+        group: 'test-group'
+      value: 100.0
+  banyandb_stream_tst_total_file_elements:
+    - labels:
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+        group: 'test-group'
+      value: 100.0
+  banyandb_stream_tst_total_file_parts:
+    - labels:
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+        group: 'test-group'
+      value: 100.0
+  banyandb_stream_tst_total_mem_part:
+    - labels:
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+        group: 'test-group'
+      value: 100.0
+  banyandb_stream_tst_total_merge_latency:
+    - labels:
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+        type: 'file'
+        group: 'test-group'
+      value: 100.0
+  banyandb_stream_tst_total_merge_loop_started:
+    - labels:
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+        group: 'test-group'
+      value: 100.0
+  banyandb_stream_tst_total_merged_parts:
+    - labels:
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+        type: 'file'
+        group: 'test-group'
+      value: 100.0
+  banyandb_stream_tst_total_written:
+    - labels:
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+        group: 'test-group'
+      value: 100.0
+  banyandb_system_disk:
+    - labels:
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+        kind: 'used'
+        path: '/tmp/measure'
+      value: 100.0
+    - labels:
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+        kind: 'total'
+        path: '/tmp/measure'
+      value: 100.0
+    - labels:
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+        kind: 'used_percent'
+        path: '/tmp/measure'
+      value: 100.0
+  banyandb_system_memory_state:
+    - labels:
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+        kind: 'used'
+      value: 100.0
+    - labels:
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+        kind: 'total'
+      value: 100.0
+    - labels:
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+        kind: 'used_percent'
+      value: 100.0
+  banyandb_system_net_state:
+    - labels:
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+        kind: 'bytes_recv'
+        name: 'eth0'
+      value: 100.0
+    - labels:
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+        kind: 'bytes_sent'
+        name: 'eth0'
+      value: 100.0
+  banyandb_system_up_time:
+    - labels:
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+      value: 100.0
+  banyandb_trace_tst_pending_data_count:
+    - labels:
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+        group: 'test-group'
+      value: 100.0
+  banyandb_trace_tst_total_file_elements:
+    - labels:
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+        group: 'test-group'
+      value: 100.0
+  banyandb_trace_tst_total_file_parts:
+    - labels:
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+        group: 'test-group'
+      value: 100.0
+  banyandb_trace_tst_total_mem_part:
+    - labels:
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+        group: 'test-group'
+      value: 100.0
+  banyandb_trace_tst_total_merge_latency:
+    - labels:
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+        type: 'file'
+        group: 'test-group'
+      value: 100.0
+  banyandb_trace_tst_total_merge_loop_started:
+    - labels:
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+        group: 'test-group'
+      value: 100.0
+  banyandb_trace_tst_total_merged_parts:
+    - labels:
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+        type: 'file'
+        group: 'test-group'
+      value: 100.0
+  banyandb_trace_tst_total_written:
     - labels:
-        group: test-value
-        host_name: test-host
-        service_instance_id: test-instance
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+        group: 'test-group'
+      value: 100.0
+  go_gc_duration_seconds_count:
+    - labels:
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+      value: 100.0
+  go_gc_duration_seconds_sum:
+    - labels:
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+      value: 100.0
+  go_goroutines:
+    - labels:
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+      value: 100.0
+  go_memstats_alloc_bytes_total:
+    - labels:
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+      value: 100.0
+  go_memstats_heap_inuse_bytes:
+    - labels:
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+      value: 100.0
+  go_memstats_next_gc_bytes:
+    - labels:
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+      value: 100.0
+  process_cpu_seconds_total:
+    - labels:
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+      value: 100.0
+  process_resident_memory_bytes:
+    - labels:
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
       value: 100.0
 expected:
-  meter_banyandb_instance_write_rate:
+  meter_banyandb_instance_node_uptime:
     entities:
       - scope: SERVICE_INSTANCE
-        service: 'banyandb::test-host'
-        instance: test-instance
+        service: test-cluster
+        instance: test-pod@data
         layer: BANYANDB
-    samples:
-      - labels:
-          host_name: 'banyandb::test-host'
-          service_instance_id: test-instance
-          group: test-value
-        value: 50.0
-  meter_banyandb_instance_total_memory:
+  meter_banyandb_instance_cpu_usage:
     entities:
       - scope: SERVICE_INSTANCE
-        service: 'banyandb::test-host'
-        instance: test-instance
+        service: test-cluster
+        instance: test-pod@data
         layer: BANYANDB
-    samples:
-      - labels:
-          kind: total
-          host_name: 'banyandb::test-host'
-          service_instance_id: test-instance
-        value: 100.0
-  meter_banyandb_instance_disk_usage:
+  meter_banyandb_instance_rss_memory:
     entities:
       - scope: SERVICE_INSTANCE
-        service: 'banyandb::test-host'
-        instance: test-instance
+        service: test-cluster
+        instance: test-pod@data
         layer: BANYANDB
-    samples:
-      - labels:
-          host_name: 'banyandb::test-host'
-          service_instance_id: test-instance
-        value: 100.0
-  meter_banyandb_instance_query_rate:
+  meter_banyandb_instance_system_memory_percent:
     entities:
       - scope: SERVICE_INSTANCE
-        service: 'banyandb::test-host'
-        instance: test-instance
+        service: test-cluster
+        instance: test-pod@data
         layer: BANYANDB
-    samples:
-      - labels:
-          method: query
-          host_name: 'banyandb::test-host'
-          service_instance_id: test-instance
-        value: 100.0
-  meter_banyandb_instance_total_cpu:
+  meter_banyandb_instance_disk_usage_percent:
     entities:
       - scope: SERVICE_INSTANCE
-        service: 'banyandb::test-host'
-        instance: test-instance
+        service: test-cluster
+        instance: test-pod@data
         layer: BANYANDB
-    samples:
-      - labels:
-          host_name: 'banyandb::test-host'
-          service_instance_id: test-instance
-        value: 100.0
-  meter_banyandb_instance_write_and_query_errors_rate:
+  meter_banyandb_instance_disk_used_by_path:
     entities:
       - scope: SERVICE_INSTANCE
-        service: 'banyandb::test-host'
-        instance: test-instance
+        service: test-cluster
+        instance: test-pod@data
         layer: BANYANDB
-    samples:
-      - labels:
-          host_name: 'banyandb::test-host'
-          service_instance_id: test-instance
-        value: 3000.0
-  meter_banyandb_instance_etcd_operation_rate:
+  meter_banyandb_instance_disk_total_by_path:
     entities:
       - scope: SERVICE_INSTANCE
-        service: 'banyandb::test-host'
-        instance: test-instance
+        service: test-cluster
+        instance: test-pod@data
         layer: BANYANDB
-    samples:
-      - labels:
-          host_name: 'banyandb::test-host'
-          service_instance_id: test-instance
-        value: 50.0
-  meter_banyandb_instance_active_instance:
+  meter_banyandb_instance_disk_used_percent_by_path:
     entities:
       - scope: SERVICE_INSTANCE
-        service: 'banyandb::test-host'
-        instance: test-instance
+        service: test-cluster
+        instance: test-pod@data
         layer: BANYANDB
-    samples:
-      - labels:
-          host_name: 'banyandb::test-host'
-          service_instance_id: test-instance
-        value: 100.0
-  meter_banyandb_instance_cpu_usage:
+  meter_banyandb_instance_network_recv:
     entities:
       - scope: SERVICE_INSTANCE
-        service: 'banyandb::test-host'
-        instance: test-instance
+        service: test-cluster
+        instance: test-pod@data
         layer: BANYANDB
-    samples:
-      - labels:
-          host_name: 'banyandb::test-host'
-          service_instance_id: test-instance
-        value: 250.0
-  meter_banyandb_instance_rss_memory_usage:
+  meter_banyandb_instance_network_sent:
     entities:
       - scope: SERVICE_INSTANCE
-        service: 'banyandb::test-host'
-        instance: test-instance
+        service: test-cluster
+        instance: test-pod@data
         layer: BANYANDB
-    samples:
-      - labels:
-          host_name: 'banyandb::test-host'
-          service_instance_id: test-instance
-        value: 1000.0
-  meter_banyandb_instance_disk_usage_all:
+  meter_banyandb_instance_goroutines:
     entities:
       - scope: SERVICE_INSTANCE
-        service: 'banyandb::test-host'
-        instance: test-instance
+        service: test-cluster
+        instance: test-pod@data
         layer: BANYANDB
-    samples:
-      - labels:
-          host_name: 'banyandb::test-host'
-          service_instance_id: test-instance
-        value: 1000.0
-  meter_banyandb_instance_network_usage_recv:
+  meter_banyandb_instance_gc_pause_avg:
     entities:
       - scope: SERVICE_INSTANCE
-        service: 'banyandb::test-host'
-        instance: test-instance
+        service: test-cluster
+        instance: test-pod@data
         layer: BANYANDB
-    samples:
-      - labels:
-          host_name: 'banyandb::test-host'
-          service_instance_id: test-instance
-        value: 25.0
-  meter_banyandb_instance_network_usage_sent:
+  meter_banyandb_instance_heap_inuse:
     entities:
       - scope: SERVICE_INSTANCE
-        service: 'banyandb::test-host'
-        instance: test-instance
+        service: test-cluster
+        instance: test-pod@data
         layer: BANYANDB
-    samples:
-      - labels:
-          host_name: 'banyandb::test-host'
-          service_instance_id: test-instance
-        value: 25.0
-  meter_banyandb_instance_storage_write_rate:
+  meter_banyandb_instance_heap_next_gc:
     entities:
       - scope: SERVICE_INSTANCE
-        service: 'banyandb::test-host'
-        instance: test-instance
+        service: test-cluster
+        instance: test-pod@data
         layer: BANYANDB
-    samples:
-      - labels:
-          host_name: 'banyandb::test-host'
-          group: test-value
-          service_instance_id: test-instance
-        value: 25000.0
-  meter_banyandb_instance_query_latency:
+  meter_banyandb_instance_alloc_rate:
     entities:
       - scope: SERVICE_INSTANCE
-        service: 'banyandb::test-host'
-        instance: test-instance
+        service: test-cluster
+        instance: test-pod@data
         layer: BANYANDB
-    samples:
-      - labels:
-          host_name: 'banyandb::test-host'
-          group: test-value
-          service_instance_id: test-instance
-        value: 1000.0
-  meter_banyandb_instance_total_data:
+  meter_banyandb_instance_query_rate_by_service:
     entities:
       - scope: SERVICE_INSTANCE
-        service: 'banyandb::test-host'
-        instance: test-instance
+        service: test-cluster
+        instance: test-pod@data
         layer: BANYANDB
-    samples:
-      - labels:
-          host_name: 'banyandb::test-host'
-          group: test-value
-          service_instance_id: test-instance
-        value: 100.0
-  meter_banyandb_instance_merge_file_data:
+  meter_banyandb_instance_grpc_error_rate:
     entities:
       - scope: SERVICE_INSTANCE
-        service: 'banyandb::test-host'
-        instance: test-instance
+        service: test-cluster
+        instance: test-pod@data
         layer: BANYANDB
-    samples:
-      - labels:
-          host_name: 'banyandb::test-host'
-          group: test-value
-          service_instance_id: test-instance
-        value: 1500000.0
-  meter_banyandb_instance_merge_file_latency:
+  meter_banyandb_instance_non_query_op_rate:
+    entities:
+      - scope: SERVICE_INSTANCE
+        service: test-cluster
+        instance: test-pod@data
+        layer: BANYANDB
+  meter_banyandb_instance_write_rate:
+    entities:
+      - scope: SERVICE_INSTANCE
+        service: test-cluster
+        instance: test-pod@data
+        layer: BANYANDB
+  meter_banyandb_instance_publish_throughput:
+    entities:
+      - scope: SERVICE_INSTANCE
+        service: test-cluster
+        instance: test-pod@data
+        layer: BANYANDB
+  meter_banyandb_instance_publish_bytes:
+    entities:
+      - scope: SERVICE_INSTANCE
+        service: test-cluster
+        instance: test-pod@data
+        layer: BANYANDB
+  meter_banyandb_instance_publish_latency_p99:
     entities:
       - scope: SERVICE_INSTANCE
-        service: 'banyandb::test-host'
-        instance: test-instance
+        service: test-cluster
+        instance: test-pod@data
+        layer: BANYANDB
+  meter_banyandb_instance_wqueue_pending:
+    entities:
+      - scope: SERVICE_INSTANCE
+        service: test-cluster
+        instance: test-pod@data
+        layer: BANYANDB
+  meter_banyandb_instance_wqueue_file_parts:
+    entities:
+      - scope: SERVICE_INSTANCE
+        service: test-cluster
+        instance: test-pod@data
+        layer: BANYANDB
+  meter_banyandb_instance_wqueue_mem_part:
+    entities:
+      - scope: SERVICE_INSTANCE
+        service: test-cluster
+        instance: test-pod@data
+        layer: BANYANDB
+  meter_banyandb_instance_total_data:
+    entities:
+      - scope: SERVICE_INSTANCE
+        service: test-cluster
+        instance: test-pod@data
+        layer: BANYANDB
+  meter_banyandb_instance_merge_file_rate:
+    entities:
+      - scope: SERVICE_INSTANCE
+        service: test-cluster
+        instance: test-pod@data
         layer: BANYANDB
-    samples:
-      - labels:
-          host_name: 'banyandb::test-host'
-          group: test-value
-          service_instance_id: test-instance
-        value: 1000.0
   meter_banyandb_instance_merge_file_partitions:
     entities:
       - scope: SERVICE_INSTANCE
-        service: 'banyandb::test-host'
-        instance: test-instance
+        service: test-cluster
+        instance: test-pod@data
+        layer: BANYANDB
+  meter_banyandb_instance_merge_file_latency:
+    entities:
+      - scope: SERVICE_INSTANCE
+        service: test-cluster
+        instance: test-pod@data
         layer: BANYANDB
-    samples:
-      - labels:
-          host_name: 'banyandb::test-host'
-          group: test-value
-          service_instance_id: test-instance
-        value: 1000.0
   meter_banyandb_instance_series_write_rate:
     entities:
       - scope: SERVICE_INSTANCE
-        service: 'banyandb::test-host'
-        instance: test-instance
+        service: test-cluster
+        instance: test-pod@data
         layer: BANYANDB
-    samples:
-      - labels:
-          host_name: 'banyandb::test-host'
-          group: test-value
-          service_instance_id: test-instance
-        value: 25000.0
   meter_banyandb_instance_series_term_search_rate:
     entities:
       - scope: SERVICE_INSTANCE
-        service: 'banyandb::test-host'
-        instance: test-instance
+        service: test-cluster
+        instance: test-pod@data
         layer: BANYANDB
-    samples:
-      - labels:
-          host_name: 'banyandb::test-host'
-          group: test-value
-          service_instance_id: test-instance
-        value: 25.0
   meter_banyandb_instance_total_series:
     entities:
       - scope: SERVICE_INSTANCE
-        service: 'banyandb::test-host'
-        instance: test-instance
+        service: test-cluster
+        instance: test-pod@data
+        layer: BANYANDB
+  meter_banyandb_instance_stream_tst_write_rate:
+    entities:
+      - scope: SERVICE_INSTANCE
+        service: test-cluster
+        instance: test-pod@data
+        layer: BANYANDB
+  meter_banyandb_instance_stream_tst_term_search_rate:
+    entities:
+      - scope: SERVICE_INSTANCE
+        service: test-cluster
+        instance: test-pod@data
+        layer: BANYANDB
+  meter_banyandb_instance_stream_tst_total_docs:
+    entities:
+      - scope: SERVICE_INSTANCE
+        service: test-cluster
+        instance: test-pod@data
+        layer: BANYANDB
+  meter_banyandb_instance_queue_sub_throughput:
+    entities:
+      - scope: SERVICE_INSTANCE
+        service: test-cluster
+        instance: test-pod@data
+        layer: BANYANDB
+  meter_banyandb_instance_queue_sub_latency_p99:
+    entities:
+      - scope: SERVICE_INSTANCE
+        service: test-cluster
+        instance: test-pod@data
+        layer: BANYANDB
+  meter_banyandb_instance_retention_measure_disk_usage_percent:
+    entities:
+      - scope: SERVICE_INSTANCE
+        service: test-cluster
+        instance: test-pod@data
+        layer: BANYANDB
+  meter_banyandb_instance_retention_stream_disk_usage_percent:
+    entities:
+      - scope: SERVICE_INSTANCE
+        service: test-cluster
+        instance: test-pod@data
+        layer: BANYANDB
+  meter_banyandb_instance_retention_trace_disk_usage_percent:
+    entities:
+      - scope: SERVICE_INSTANCE
+        service: test-cluster
+        instance: test-pod@data
         layer: BANYANDB
-    samples:
-      - labels:
-          host_name: 'banyandb::test-host'
-          group: test-value
-          service_instance_id: test-instance
-        value: 100.0
-  meter_banyandb_instance_stream_write_rate:
+  meter_banyandb_instance_lifecycle_cycles:
     entities:
       - scope: SERVICE_INSTANCE
-        service: 'banyandb::test-host'
-        instance: test-instance
+        service: test-cluster
+        instance: test-pod@data
         layer: BANYANDB
-    samples:
-      - labels:
-          host_name: 'banyandb::test-host'
-          group: test-value
-          service_instance_id: test-instance
-        value: 25.0
-  meter_banyandb_instance_term_search_rate:
+  meter_banyandb_instance_lifecycle_last_run:
     entities:
       - scope: SERVICE_INSTANCE
-        service: 'banyandb::test-host'
-        instance: test-instance
+        service: test-cluster
+        instance: test-pod@data
         layer: BANYANDB
-    samples:
-      - labels:
-          host_name: 'banyandb::test-host'
-          group: test-value
-          service_instance_id: test-instance
-        value: 25000.0
-  meter_banyandb_instance_total_document:
+  meter_banyandb_instance_lifecycle_last_run_success:
     entities:
       - scope: SERVICE_INSTANCE
-        service: 'banyandb::test-host'
-        instance: test-instance
+        service: test-cluster
+        instance: test-pod@data
         layer: BANYANDB
-    samples:
-      - labels:
-          host_name: 'banyandb::test-host'
-          group: test-value
-          service_instance_id: test-instance
-        value: 100.0
diff --git a/oap-server/analyzer/meter-analyzer-scripts-test/src/test/resources/scripts/mal/test-otel-rules/banyandb/banyandb-service.data.yaml b/oap-server/analyzer/meter-analyzer-scripts-test/src/test/resources/scripts/mal/test-otel-rules/banyandb/banyandb-service.data.yaml
index 87eea6b9cf..bc01f21612 100644
--- a/oap-server/analyzer/meter-analyzer-scripts-test/src/test/resources/scripts/mal/test-otel-rules/banyandb/banyandb-service.data.yaml
+++ b/oap-server/analyzer/meter-analyzer-scripts-test/src/test/resources/scripts/mal/test-otel-rules/banyandb/banyandb-service.data.yaml
@@ -15,440 +15,229 @@
 
 script: oap-server/server-starter/src/main/resources/otel-rules/banyandb/banyandb-service.yaml
 input:
-  banyandb_measure_total_written:
-    - labels:
-        host_name: test-host
-        service_instance_id: test-instance
-        group: test-value
-      value: 100.0
-  banyandb_stream_tst_total_written:
-    - labels:
-        group: test-value
-        host_name: test-host
-        service_instance_id: test-instance
-      value: 100.0
-  banyandb_system_memory_state:
-    - labels:
-        host_name: test-host
-        service_instance_id: test-instance
-        kind: total
-      value: 100.0
-    - labels:
-        host_name: test-host
-        service_instance_id: test-instance
-        kind: used
-      value: 100.0
-  banyandb_system_disk:
-    - labels:
-        host_name: test-host
-        service_instance_id: test-instance
-        kind: used
-      value: 100.0
-    - labels:
-        host_name: test-host
-        service_instance_id: test-instance
-        kind: total
-      value: 100.0
-  banyandb_liaison_grpc_total_started:
-    - labels:
-        method: query
-        host_name: test-host
-        service_instance_id: test-instance
-        group: test-value
-      value: 100.0
-  banyandb_system_cpu_num:
-    - labels:
-        method: test-value
-        host_name: test-host
-        service_instance_id: test-instance
-      value: 100.0
   banyandb_liaison_grpc_total_err:
     - labels:
-        method: query
-        host_name: test-host
-        service_instance_id: test-instance
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+        method: 'query'
+        service: 'measure'
+        group: 'test-group'
+      value: 100.0
+  banyandb_liaison_grpc_total_registry_err:
+    - labels:
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+        method: 'query'
+        service: 'measure'
+        group: 'test-group'
       value: 100.0
-  banyandb_liaison_grpc_total_stream_msg_sent_err:
+  banyandb_liaison_grpc_total_started:
     - labels:
-        method: query
-        host_name: test-host
-        service_instance_id: test-instance
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+        method: 'query'
+        service: 'measure'
+        group: 'test-group'
+      value: 100.0
+    - labels:
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+        method: 'write'
+        service: 'measure'
+        group: 'test-group'
       value: 100.0
   banyandb_liaison_grpc_total_stream_msg_received_err:
     - labels:
-        method: query
-        host_name: test-host
-        service_instance_id: test-instance
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+        method: 'query'
+        service: 'measure'
+        group: 'test-group'
       value: 100.0
-  banyandb_queue_sub_total_msg_sent_err:
+  banyandb_measure_total_sync_loop_err:
     - labels:
-        method: query
-        host_name: test-host
-        service_instance_id: test-instance
-      value: 100.0
-  banyandb_liaison_grpc_total_registry_started:
-    - labels:
-        host_name: test-host
-        service_instance_id: test-instance
-      value: 100.0
-  up:
-    - labels:
-        host_name: test-host
-        service_instance_id: test-instance
-      value: 100.0
-  process_cpu_seconds_total:
-    - labels:
-        host_name: test-host
-        service_instance_id: test-instance
-      value: 100.0
-  process_resident_memory_bytes:
-    - labels:
-        host_name: test-host
-        service_instance_id: test-instance
-        kind: total
-      value: 100.0
-  banyandb_system_net_state:
-    - labels:
-        host_name: test-host
-        service_instance_id: test-instance
-        kind: bytes_recv
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+        group: 'test-group'
       value: 100.0
+  banyandb_measure_total_written:
     - labels:
-        host_name: test-host
-        service_instance_id: test-instance
-        kind: bytes_sent
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+        group: 'test-group'
+      value: 100.0
+  banyandb_queue_pub_total_err:
+    - labels:
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+        operation: 'batch-write'
+        group: 'test-group'
+        remote_node: 'n1'
+        remote_role: 'data'
+        remote_tier: 'hot'
+      value: 100.0
+  banyandb_stream_tst_total_sync_loop_err:
+    - labels:
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+        group: 'test-group'
       value: 100.0
-  banyandb_liaison_grpc_total_latency:
+  banyandb_stream_tst_total_written:
     - labels:
-        group: test-value
-        host_name: test-host
-        service_instance_id: test-instance
-        method: query
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+        group: 'test-group'
       value: 100.0
-  banyandb_measure_total_file_elements:
+  banyandb_system_cpu_num:
     - labels:
-        group: test-value
-        host_name: test-host
-        service_instance_id: test-instance
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
       value: 100.0
-  banyandb_measure_total_merge_loop_started:
+  banyandb_system_disk:
     - labels:
-        group: test-value
-        host_name: test-host
-        service_instance_id: test-instance
-        type: file
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+        kind: 'used'
+        path: '/tmp/measure'
       value: 100.0
-  banyandb_measure_total_merge_latency:
     - labels:
-        group: test-value
-        host_name: test-host
-        service_instance_id: test-instance
-        type: file
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+        kind: 'total'
+        path: '/tmp/measure'
       value: 100.0
-  banyandb_measure_total_merged_parts:
     - labels:
-        group: test-value
-        host_name: test-host
-        service_instance_id: test-instance
-        type: file
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+        kind: 'used_percent'
+        path: '/tmp/measure'
       value: 100.0
-  banyandb_measure_inverted_index_total_updates:
+  banyandb_system_memory_state:
     - labels:
-        group: test-value
-        host_name: test-host
-        service_instance_id: test-instance
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+        kind: 'used'
       value: 100.0
-  banyandb_stream_storage_inverted_index_total_term_searchers_started:
     - labels:
-        group: test-value
-        host_name: test-host
-        service_instance_id: test-instance
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+        kind: 'total'
       value: 100.0
-  banyandb_measure_inverted_index_total_doc_count:
     - labels:
-        group: test-value
-        host_name: test-host
-        service_instance_id: test-instance
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+        kind: 'used_percent'
       value: 100.0
-  banyandb_stream_tst_inverted_index_total_updates:
+  banyandb_system_up_time:
     - labels:
-        group: test-value
-        host_name: test-host
-        service_instance_id: test-instance
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
       value: 100.0
-  banyandb_stream_tst_inverted_index_total_term_searchers_started:
+  banyandb_trace_tst_total_sync_loop_err:
     - labels:
-        group: test-value
-        host_name: test-host
-        service_instance_id: test-instance
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+        group: 'test-group'
       value: 100.0
-  banyandb_stream_tst_inverted_index_total_doc_count:
+  banyandb_trace_tst_total_written:
     - labels:
-        group: test-value
-        host_name: test-host
-        service_instance_id: test-instance
+        cluster: 'test-cluster'
+        pod_name: 'test-pod'
+        container_name: 'data'
+        node_role: 'ROLE_DATA'
+        node_type: 'hot'
+        group: 'test-group'
       value: 100.0
 expected:
-  meter_banyandb_write_rate:
-    entities:
-      - scope: SERVICE
-        service: 'banyandb::test-host'
-        layer: BANYANDB
-    samples:
-      - labels:
-          host_name: 'banyandb::test-host'
-          service_instance_id: test-instance
-        value: 50.0
-  meter_banyandb_total_memory:
-    entities:
-      - scope: SERVICE
-        service: 'banyandb::test-host'
-        layer: BANYANDB
-    samples:
-      - labels:
-          host_name: 'banyandb::test-host'
-        value: 100.0
-  meter_banyandb_disk_usage:
-    entities:
-      - scope: SERVICE
-        service: 'banyandb::test-host'
-        layer: BANYANDB
-    samples:
-      - labels:
-          host_name: 'banyandb::test-host'
-          service_instance_id: test-instance
-        value: 100.0
-  meter_banyandb_query_rate:
-    entities:
-      - scope: SERVICE
-        service: 'banyandb::test-host'
-        layer: BANYANDB
-    samples:
-      - labels:
-          method: query
-          host_name: 'banyandb::test-host'
-          service_instance_id: test-instance
-        value: 100.0
-  meter_banyandb_total_cpu:
-    entities:
-      - scope: SERVICE
-        service: 'banyandb::test-host'
-        layer: BANYANDB
-    samples:
-      - labels:
-          method: test-value
-          host_name: 'banyandb::test-host'
-          service_instance_id: test-instance
-        value: 100.0
-  meter_banyandb_write_and_query_errors_rate:
-    entities:
-      - scope: SERVICE
-        service: 'banyandb::test-host'
-        layer: BANYANDB
-    samples:
-      - labels:
-          host_name: 'banyandb::test-host'
-          service_instance_id: test-instance
-        value: 3000.0
-  meter_banyandb_etcd_operation_rate:
-    entities:
-      - scope: SERVICE
-        service: 'banyandb::test-host'
-        layer: BANYANDB
-    samples:
-      - labels:
-          host_name: 'banyandb::test-host'
-          service_instance_id: test-instance
-        value: 50.0
-  meter_banyandb_active_instance:
-    entities:
-      - scope: SERVICE
-        service: 'banyandb::test-host'
-        layer: BANYANDB
-    samples:
-      - labels:
-          host_name: 'banyandb::test-host'
-          service_instance_id: test-instance
-        value: 100.0
-  meter_banyandb_cpu_usage:
-    entities:
-      - scope: SERVICE
-        service: 'banyandb::test-host'
-        layer: BANYANDB
-    samples:
-      - labels:
-          host_name: 'banyandb::test-host'
-          service_instance_id: test-instance
-        value: 250.0
-  meter_banyandb_rss_memory_usage:
-    entities:
-      - scope: SERVICE
-        service: 'banyandb::test-host'
-        layer: BANYANDB
-    samples:
-      - labels:
-          host_name: 'banyandb::test-host'
-          service_instance_id: test-instance
-        value: 1000.0
-  meter_banyandb_disk_usage_all:
-    entities:
-      - scope: SERVICE
-        service: 'banyandb::test-host'
-        layer: BANYANDB
-    samples:
-      - labels:
-          host_name: 'banyandb::test-host'
-          service_instance_id: test-instance
-        value: 1000.0
-  meter_banyandb_network_usage_recv:
-    entities:
-      - scope: SERVICE
-        service: 'banyandb::test-host'
-        layer: BANYANDB
-    samples:
-      - labels:
-          host_name: 'banyandb::test-host'
-          service_instance_id: test-instance
-        value: 25.0
-  meter_banyandb_network_usage_sent:
-    entities:
-      - scope: SERVICE
-        service: 'banyandb::test-host'
-        layer: BANYANDB
-    samples:
-      - labels:
-          host_name: 'banyandb::test-host'
-          service_instance_id: test-instance
-        value: 25.0
-  meter_banyandb_storage_write_rate:
-    entities:
-      - scope: SERVICE
-        service: 'banyandb::test-host'
-        layer: BANYANDB
-    samples:
-      - labels:
-          host_name: 'banyandb::test-host'
-          group: test-value
-          service_instance_id: test-instance
-        value: 25000.0
-  meter_banyandb_query_latency:
-    entities:
-      - scope: SERVICE
-        service: 'banyandb::test-host'
-        layer: BANYANDB
-    samples:
-      - labels:
-          host_name: 'banyandb::test-host'
-          group: test-value
-          service_instance_id: test-instance
-        value: 1000.0
-  meter_banyandb_total_data:
-    entities:
-      - scope: SERVICE
-        service: 'banyandb::test-host'
-        layer: BANYANDB
-    samples:
-      - labels:
-          host_name: 'banyandb::test-host'
-          group: test-value
-          service_instance_id: test-instance
-        value: 100.0
-  meter_banyandb_merge_file_data:
-    entities:
-      - scope: SERVICE
-        service: 'banyandb::test-host'
-        layer: BANYANDB
-    samples:
-      - labels:
-          host_name: 'banyandb::test-host'
-          group: test-value
-          service_instance_id: test-instance
-        value: 1500000.0
-  meter_banyandb_merge_file_latency:
-    entities:
-      - scope: SERVICE
-        service: 'banyandb::test-host'
-        layer: BANYANDB
-    samples:
-      - labels:
-          host_name: 'banyandb::test-host'
-          group: test-value
-          service_instance_id: test-instance
-        value: 1000.0
-  meter_banyandb_merge_file_partitions:
+  meter_banyandb_cluster_write_rate:
     entities:
       - scope: SERVICE
-        service: 'banyandb::test-host'
+        service: test-cluster
         layer: BANYANDB
-    samples:
-      - labels:
-          host_name: 'banyandb::test-host'
-          group: test-value
-          service_instance_id: test-instance
-        value: 1000.0
-  meter_banyandb_series_write_rate:
+  meter_banyandb_cluster_query_rate:
     entities:
       - scope: SERVICE
-        service: 'banyandb::test-host'
+        service: test-cluster
         layer: BANYANDB
-    samples:
-      - labels:
-          host_name: 'banyandb::test-host'
-          group: test-value
-          service_instance_id: test-instance
-        value: 25000.0
-  meter_banyandb_series_term_search_rate:
+  meter_banyandb_cluster_error_rate:
     entities:
       - scope: SERVICE
-        service: 'banyandb::test-host'
+        service: test-cluster
         layer: BANYANDB
-    samples:
-      - labels:
-          host_name: 'banyandb::test-host'
-          group: test-value
-          service_instance_id: test-instance
-        value: 25.0
-  meter_banyandb_total_series:
+  meter_banyandb_reporting_instances:
     entities:
       - scope: SERVICE
-        service: 'banyandb::test-host'
+        service: test-cluster
         layer: BANYANDB
-    samples:
-      - labels:
-          host_name: 'banyandb::test-host'
-          group: test-value
-          service_instance_id: test-instance
-        value: 100.0
-  meter_banyandb_stream_write_rate:
+  meter_banyandb_total_cpu_cores:
     entities:
       - scope: SERVICE
-        service: 'banyandb::test-host'
+        service: test-cluster
         layer: BANYANDB
-    samples:
-      - labels:
-          host_name: 'banyandb::test-host'
-          group: test-value
-          service_instance_id: test-instance
-        value: 25.0
-  meter_banyandb_term_search_rate:
+  meter_banyandb_total_memory_used:
     entities:
       - scope: SERVICE
-        service: 'banyandb::test-host'
+        service: test-cluster
         layer: BANYANDB
-    samples:
-      - labels:
-          host_name: 'banyandb::test-host'
-          group: test-value
-          service_instance_id: test-instance
-        value: 25000.0
-  meter_banyandb_total_document:
+  meter_banyandb_total_disk_used:
     entities:
       - scope: SERVICE
-        service: 'banyandb::test-host'
+        service: test-cluster
         layer: BANYANDB
-    samples:
-      - labels:
-          host_name: 'banyandb::test-host'
-          group: test-value
-          service_instance_id: test-instance
-        value: 100.0
diff --git a/oap-server/server-starter/src/main/resources/otel-rules/banyandb/banyandb-endpoint.yaml b/oap-server/server-starter/src/main/resources/otel-rules/banyandb/banyandb-endpoint.yaml
new file mode 100644
index 0000000000..13ecb57fb3
--- /dev/null
+++ b/oap-server/server-starter/src/main/resources/otel-rules/banyandb/banyandb-endpoint.yaml
@@ -0,0 +1,97 @@
+# 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.
+
+# SWIP-15 section 3.3 Endpoint scope: a BanyanDB `group` (storage group, e.g. sw_metricsMinute,
+# sw_trace) is modeled as an Endpoint under the cluster Service. The `cluster` label is the
+# single static label the OTel collector injects per scrape job (it is NOT on the raw FODC
+# wire); `group` is carried natively by every family referenced below. Every metric here is
+# aggregated across all cluster nodes per group, so each rule's .sum() collapses the per-node /
+# per-seg / per-shard / per-operation / per-remote dimensions down to ['cluster','group']
+# before any rate/histogram/division. MAL arithmetic ('+', '/') inner-joins on exact label
+# equality, so every operand is reduced to the identical ['cluster','group'] (or
+# ['cluster','group','le'] for histograms) label set first.
+# Source expressions mirror the upstream BanyanDB Grafana "Workload" board
+# (docs/operation/grafana-fodc-workload.json).
+filter: "{ tags -> tags.job_name == 'banyandb-monitoring' }"
+expSuffix: endpoint(['cluster'], ['group'], Layer.BANYANDB)
+metricPrefix: meter_banyandb_endpoint
+metricsRules:
+  # writes/s for the group, across the three data-model scopes (measure, stream, trace). The
+  # write counter carries `group` regardless of which role records it, so the by-group roll-up
+  # is exact.
+  - name: write_rate
+    exp: (banyandb_measure_total_written.sum(['cluster', 'group']).rate('PT1M') + banyandb_stream_tst_total_written.sum(['cluster', 'group']).rate('PT1M') + banyandb_trace_tst_total_written.sum(['cluster', 'group']).rate('PT1M'))
+
+  # mean query latency (ms) for the group = rate(latency) / rate(count). liaison_grpc_total_latency
+  # and _started are BOTH counters (not a histogram), so a WINDOWED average is taken by rate()-ing
+  # both operands (matching the upstream Grafana "Query Latency" panel) rather than dividing the raw
+  # cumulative counters (which would give an all-time average). Both filtered to method='query' and
+  # reduced to ['cluster','group'] (collapsing the `service` facet); safeDiv yields 0 in idle windows.
+  - name: query_latency
+    exp: banyandb_liaison_grpc_total_latency.tagEqual('method', 'query').sum(['cluster', 'group']).rate('PT1M').safeDiv(banyandb_liaison_grpc_total_started.tagEqual('method', 'query').sum(['cluster', 'group']).rate('PT1M')) * 1000
+
+  # current total stored data elements for the group (gauge). Dimensioned by seg+shard+node_type
+  # across data nodes; .sum(['cluster','group']) collapses them into one per-group total.
+  - name: total_data
+    exp: (banyandb_measure_total_file_elements.sum(['cluster', 'group']) + banyandb_stream_tst_total_file_elements.sum(['cluster', 'group']) + banyandb_trace_tst_total_file_elements.sum(['cluster', 'group']))
+
+  # merge-loop iterations/min for the group (matches the upstream "Merge File Rate" rotrpm panel,
+  # which is rate(merge_loop_started) * 60). merge_loop_started carries node_type (NOT a `type`
+  # label), so no type filter applies here.
+  - name: merge_file_rate
+    exp: (banyandb_measure_total_merge_loop_started.sum(['cluster', 'group']).rate('PT1M') + banyandb_stream_tst_total_merge_loop_started.sum(['cluster', 'group']).rate('PT1M') + banyandb_trace_tst_total_merge_loop_started.sum(['cluster', 'group']).rate('PT1M')) * 60
+
+  # mean file-merge latency (ms) per merge loop for the group. merge_latency carries a `type`
+  # label (file/hot/mem); type='file' selects on-disk merges and is DATA-only on the wire
+  # (liaison emits only type='mem'). Divide accumulated merge-seconds by merge loops, both
+  # type/scope-aligned to ['cluster','group']. Matches the upstream "Merge File Latency" panel.
+  - name: merge_file_latency
+    exp: (banyandb_measure_total_merge_latency.tagEqual('type', 'file').sum(['cluster', 'group']).rate('PT1M').safeDiv(banyandb_measure_total_merge_loop_started.sum(['cluster', 'group']).rate('PT1M')) + banyandb_stream_tst_total_merge_latency.tagEqual('type', 'file').sum(['cluster', 'group']).rate('PT1M').safeDiv(banyandb_stream_tst_total_merge_loop_started.sum(['cluster', 'group']).rate('PT1M')) + banyandb_trace_tst_total_merge_latency.tagEqual('type', 'file').sum(['cluster', 'group']). [...]
+
+  # avg parts merged per merge loop on the on-disk merge path for the group (matches the upstream
+  # "Merge File Partitions" panel = rate(merged_parts{type=file}) / rate(merge_loop_started)).
+  # merged_parts carries `type`; type='file' is DATA-only (liaison emits only type='mem').
+  - name: merge_file_partitions
+    exp: (banyandb_measure_total_merged_parts.tagEqual('type', 'file').sum(['cluster', 'group']).rate('PT1M').safeDiv(banyandb_measure_total_merge_loop_started.sum(['cluster', 'group']).rate('PT1M')) + banyandb_stream_tst_total_merged_parts.tagEqual('type', 'file').sum(['cluster', 'group']).rate('PT1M').safeDiv(banyandb_stream_tst_total_merge_loop_started.sum(['cluster', 'group']).rate('PT1M')) + banyandb_trace_tst_total_merged_parts.tagEqual('type', 'file').sum(['cluster', 'group']).rat [...]
+
+  # inverted-index updates/s for the group. NOTE: *_inverted_index_total_updates is # TYPE=gauge
+  # though cumulative; rate() over a cumulative gauge yields a per-window delta (updates/s). Stream
+  # uses two index scopes -- both stream_storage_* and stream_tst_* are summed in. Data-only family.
+  - name: series_write_rate
+    exp: (banyandb_measure_inverted_index_total_updates.sum(['cluster', 'group']).rate('PT1M') + banyandb_stream_storage_inverted_index_total_updates.sum(['cluster', 'group']).rate('PT1M') + banyandb_stream_tst_inverted_index_total_updates.sum(['cluster', 'group']).rate('PT1M'))
+
+  # total inverted-index documents (series proxy) for the group (gauge, direct read, no rate).
+  # Both stream index scopes summed. Dimensioned by seg across data nodes; sum collapses to group.
+  - name: total_series
+    exp: (banyandb_measure_inverted_index_total_doc_count.sum(['cluster', 'group']) + banyandb_stream_storage_inverted_index_total_doc_count.sum(['cluster', 'group']) + banyandb_stream_tst_inverted_index_total_doc_count.sum(['cluster', 'group']))
+
+  # subscribe-side queue throughput (msgs/s) for the group, broken out by operation. queue_sub is
+  # emitted on BOTH data (operations batch-write/control/file-sync/query) and liaison
+  # (batch-write only); `operation` is kept in the group-by so the dashboard can split per op.
+  - name: queue_throughput
+    exp: banyandb_queue_sub_total_finished.sum(['cluster', 'group', 'operation']).rate('PT1M')
+
+  # publish-side queue p99 latency for the group. queue_pub_total_latency IS a histogram on the
+  # wire (_bucket carries le); keep le + group + operation in the .sum() group-by, then
+  # .histogram().histogram_percentile([99]). queue_pub is liaison-only. Precedent:
+  # oap.yaml / nginx-endpoint.yaml histogram idiom.
+  - name: queue_latency_p99
+    exp: banyandb_queue_pub_total_latency.sum(['le', 'cluster', 'group', 'operation']).histogram().histogram_percentile([99])
+
+  # publish bytes/s for the group. Wire family is banyandb_queue_pub_sent_bytes -- NO `total`
+  # infix (unlike queue_pub_total_started/_finished). Liaison-only; sum collapses
+  # operation/remote_node/remote_role/remote_tier before the rate.
+  - name: publish_bytes
+    exp: banyandb_queue_pub_sent_bytes.sum(['cluster', 'group']).rate('PT1M')
diff --git a/oap-server/server-starter/src/main/resources/otel-rules/banyandb/banyandb-instance.yaml b/oap-server/server-starter/src/main/resources/otel-rules/banyandb/banyandb-instance.yaml
index 21955331f3..6604455b81 100644
--- a/oap-server/server-starter/src/main/resources/otel-rules/banyandb/banyandb-instance.yaml
+++ b/oap-server/server-starter/src/main/resources/otel-rules/banyandb/banyandb-instance.yaml
@@ -13,74 +13,157 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# This will parse a textual representation of a duration. The formats
-# accepted are based on the ISO-8601 duration format {@code PnDTnHnMn.nS}
-# with days considered to be exactly 24 hours.
-# <p>
-# Examples:
-# <pre>
-#    "PT20.345S" -- parses as "20.345 seconds"
-#    "PT15M"     -- parses as "15 minutes" (where a minute is 60 seconds)
-#    "PT10H"     -- parses as "10 hours" (where an hour is 3600 seconds)
-#    "P2D"       -- parses as "2 days" (where a day is 24 hours or 86400 seconds)
-#    "P2DT3H4M"  -- parses as "2 days, 3 hours and 4 minutes"
-#    "P-6H3M"    -- parses as "-6 hours and +3 minutes"
-#    "-P6H3M"    -- parses as "-6 hours and -3 minutes"
-#    "-P-6H+3M"  -- parses as "+6 hours and -3 minutes"
-# </pre>
+# SWIP-15: BanyanDB self-observability, ServiceInstance scope = one container on a node.
+# The instance identity is pod_name + container_name (a data hot/warm pod co-hosts a data and a
+# lifecycle container under one pod_name), joined by '@'. role (container_name) and tier (node_type)
+# ride as instance attributes via the 6-arg instance() properties closure; node_type Elvis-defaults
+# to 'n/a' off data containers (it is absent on liaison samples, present on every ROLE_DATA sample).
+#
+# Every rule that aggregates keeps ['cluster','pod_name','container_name','node_role','node_type'] in
+# its .sum()/.avg()/.max() group-by: SampleFamily.aggregate() drops labels not in the group-by, and
+# the properties closure reads them from the post-aggregation sample (SampleFamily.java:810). node_type
+# rides on every ROLE_DATA sample (system_*, go_*, process_* included), so a data instance resolves a
+# stable tier across all rules; liaison families carry none, so liaison resolves 'n/a' consistently.
+#
+# Source expressions mirror the upstream BanyanDB Grafana "Nodes" board
+# (docs/operation/grafana-fodc-nodes.json) plus the liaison/data rows of the "Workload" board, so the
+# SkyWalking instance dashboard stays in lockstep with the upstream catalog.
 filter: "{ tags -> tags.job_name == 'banyandb-monitoring' }"
-expSuffix:  tag({tags -> tags.host_name = 'banyandb::' + tags.host_name}).service(['host_name'] , Layer.BANYANDB).instance(['host_name'], ['service_instance_id'], Layer.BANYANDB)
-metricPrefix: meter_banyandb
+expSuffix: |-
+  service(['cluster'], Layer.BANYANDB)
+  .instance(['cluster'], '::', ['pod_name', 'container_name'], '@', Layer.BANYANDB, { tags -> ['node_role': tags.node_role, 'node_type': tags.node_type ?: 'n/a', 'pod_name': tags.pod_name, 'container_name': tags.container_name] })
+metricPrefix: meter_banyandb_instance
 metricsRules:
-  - name: instance_write_rate
-    exp: banyandb_measure_total_written.rate('PT15S')+banyandb_stream_tst_total_written.rate('PT15S')
-  - name: instance_total_memory
-    exp: banyandb_system_memory_state.tagEqual('kind','total')
-  - name: instance_disk_usage
-    exp: banyandb_system_disk.tagEqual('kind','used').sum(['host_name','service_instance_id'])
-  - name: instance_query_rate
-    exp: banyandb_liaison_grpc_total_started.sum(['method','host_name','service_instance_id'])
-  - name: instance_total_cpu
-    exp: banyandb_system_cpu_num
-  - name: instance_write_and_query_errors_rate
-    exp: banyandb_liaison_grpc_total_err.tagEqual('method','query').sum(['method','host_name','service_instance_id']).rate('PT15S')*60 + banyandb_liaison_grpc_total_stream_msg_sent_err.sum(['host_name','service_instance_id']).rate('PT15S')*60 + banyandb_liaison_grpc_total_stream_msg_received_err.sum(['host_name','service_instance_id']).rate('PT15S')*60 + banyandb_queue_sub_total_msg_sent_err.sum(['host_name','service_instance_id']).rate('PT15S')*60
-  - name: instance_etcd_operation_rate
-    exp: banyandb_liaison_grpc_total_registry_started.sum(['host_name','service_instance_id']).rate('PT15S') + banyandb_liaison_grpc_total_started.sum(['host_name','service_instance_id']).rate('PT15S')
-  - name: instance_active_instance
-    exp: up.sum(['host_name','service_instance_id']).downsampling(MIN)
-  - name: instance_cpu_usage
-    exp: (((process_cpu_seconds_total.sum(['host_name','service_instance_id']).rate('PT15S') / banyandb_system_cpu_num.sum(['host_name','service_instance_id']))).max(['host_name','service_instance_id']))*1000
-  - name: instance_rss_memory_usage
-    exp: ((process_resident_memory_bytes.sum(['host_name','service_instance_id']).downsampling(MAX) / banyandb_system_memory_state.tagEqual('kind','total').sum(['host_name','service_instance_id'])).max(['host_name','service_instance_id']))*1000
-  - name: instance_disk_usage_all
-    exp: ((banyandb_system_disk.tagEqual('kind','used').sum(['host_name','service_instance_id']) / banyandb_system_memory_state.tagEqual('kind','total').sum(['host_name','service_instance_id'])).max(['host_name','service_instance_id']))*1000
-  - name: instance_network_usage_recv
-    exp: banyandb_system_net_state.tagEqual('kind','bytes_recv').sum(['host_name','service_instance_id']).rate('PT15S')
-  - name: instance_network_usage_sent
-    exp: banyandb_system_net_state.tagEqual('kind','bytes_sent').sum(['host_name','service_instance_id']).rate('PT15S')
-  - name: instance_storage_write_rate
-    exp: banyandb_measure_total_written.sum(['group','host_name','service_instance_id']).rate('PT15S')*1000
-  - name: instance_query_latency
-    exp: (banyandb_liaison_grpc_total_latency.tagEqual('method','query').sum(['group','host_name','service_instance_id']).rate('PT15S') / banyandb_liaison_grpc_total_started.tagEqual('method','query').sum(['group','host_name','service_instance_id']).rate('PT15S'))*1000
-  - name: instance_total_data
-    exp: banyandb_measure_total_file_elements.sum(['group','host_name','service_instance_id'])
-  - name: instance_merge_file_data
-    exp: banyandb_measure_total_merge_loop_started.sum(['group','host_name','service_instance_id']).rate('PT15S') * 60 *1000
-  - name: instance_merge_file_latency
-    exp: (banyandb_measure_total_merge_latency.tagEqual('type','file').sum(['group','host_name','service_instance_id']).rate('PT15S') / banyandb_measure_total_merge_loop_started.sum(['group','host_name','service_instance_id']).rate('PT15S'))*1000
-  - name: instance_merge_file_partitions
-    exp: (banyandb_measure_total_merged_parts.tagEqual('type','file').sum(['group','host_name','service_instance_id']).rate('PT15S') / banyandb_measure_total_merge_loop_started.sum(['group','host_name','service_instance_id']).rate('PT15S'))*1000
-  - name: instance_series_write_rate
-    exp: (banyandb_measure_inverted_index_total_updates.sum(['group','host_name','service_instance_id']).rate('PT15S'))*1000
-  - name: instance_series_term_search_rate
-    exp: banyandb_stream_storage_inverted_index_total_term_searchers_started.sum(['group','host_name','service_instance_id']).rate('PT15S')
-  - name: instance_total_series
-    exp: banyandb_measure_inverted_index_total_doc_count.sum(['group','host_name','service_instance_id'])
-  - name: instance_stream_write_rate
-    exp: banyandb_stream_tst_inverted_index_total_updates.sum(['group','host_name','service_instance_id']).rate('PT15S')
-  - name: instance_term_search_rate
-    exp: banyandb_stream_tst_inverted_index_total_term_searchers_started.sum(['group','host_name','service_instance_id']).rate('PT15S')* 1000
-  - name: instance_total_document
-    exp: banyandb_stream_tst_inverted_index_total_doc_count.sum(['group','host_name','service_instance_id'])
+  # ---- All roles: Resources / Disk by Path / Go Runtime (every container emits these) ----
+  # node uptime (s). Raw gauge; ABSENT on lifecycle containers (their binary runs the metric service
+  # without the system collector), so the lifecycle instance shows no uptime.
+  - name: node_uptime
+    exp: banyandb_system_up_time
+  # CPU usage (cores). process_* rides on every container including lifecycle.
+  - name: cpu_usage
+    exp: process_cpu_seconds_total.sum(['cluster','pod_name','container_name','node_role','node_type']).rate('PT15S')
+  # resident memory (bytes). Raw gauge, present on all containers.
+  - name: rss_memory
+    exp: process_resident_memory_bytes
+  # system memory used %. kind='used_percent' is emitted directly (a 0-1 fraction; source divides by 100).
+  - name: system_memory_percent
+    exp: banyandb_system_memory_state.tagEqual('kind','used_percent')
+  # disk used % = Σused / Σtotal across the node's data paths (matches the Grafana "Disk Usage %" panel).
+  - name: disk_usage_percent
+    exp: banyandb_system_disk.tagEqual('kind','used').sum(['cluster','pod_name','container_name','node_role','node_type']).safeDiv(banyandb_system_disk.tagEqual('kind','total').sum(['cluster','pod_name','container_name','node_role','node_type']))
+  # disk used / total / used% broken out per mount path.
+  - name: disk_used_by_path
+    exp: banyandb_system_disk.tagEqual('kind','used').sum(['cluster','pod_name','container_name','node_role','node_type','path'])
+  - name: disk_total_by_path
+    exp: banyandb_system_disk.tagEqual('kind','total').sum(['cluster','pod_name','container_name','node_role','node_type','path'])
+  - name: disk_used_percent_by_path
+    exp: banyandb_system_disk.tagEqual('kind','used').sum(['cluster','pod_name','container_name','node_role','node_type','path']).safeDiv(banyandb_system_disk.tagEqual('kind','total').sum(['cluster','pod_name','container_name','node_role','node_type','path']))
+  # network throughput (bytes/s) by interface name.
+  - name: network_recv
+    exp: banyandb_system_net_state.tagEqual('kind','bytes_recv').sum(['cluster','pod_name','container_name','node_role','node_type','name']).rate('PT15S')
+  - name: network_sent
+    exp: banyandb_system_net_state.tagEqual('kind','bytes_sent').sum(['cluster','pod_name','container_name','node_role','node_type','name']).rate('PT15S')
+  # Go runtime.
+  - name: goroutines
+    exp: go_goroutines
+  # average GC pause (s) = rate(Σpause) / rate(Σcount). go_gc_duration_seconds is a summary (no buckets),
+  # so this ratio of _sum/_count is the only valid average — do not apply histogram_percentile to it.
+  - name: gc_pause_avg
+    exp: go_gc_duration_seconds_sum.sum(['cluster','pod_name','container_name','node_role','node_type']).rate('PT15S').safeDiv(go_gc_duration_seconds_count.sum(['cluster','pod_name','container_name','node_role','node_type']).rate('PT15S'))
+  - name: heap_inuse
+    exp: go_memstats_heap_inuse_bytes
+  - name: heap_next_gc
+    exp: go_memstats_next_gc_bytes
+  - name: alloc_rate
+    exp: go_memstats_alloc_bytes_total.sum(['cluster','pod_name','container_name','node_role','node_type']).rate('PT15S')
+
+  # ---- Liaison only (front door; the dashboard gates these on container_name == liaison) ----
+  # query rate (req/s) by data-model service (measure/stream/trace/property). method literal is "query".
+  - name: query_rate_by_service
+    exp: banyandb_liaison_grpc_total_started.tagEqual('method','query').sum(['cluster','pod_name','container_name','node_role','node_type','service']).rate('PT15S')
+  # gRPC errors/min. Three liaison-side error families (mirrors the Grafana "gRPC Error Rate" panel,
+  # which sums total_err + registry_err + stream_msg_received_err). All lazily registered -> empty on a
+  # healthy cluster; each pre-aggregated to the same label set before '+'.
+  - name: grpc_error_rate
+    exp: (banyandb_liaison_grpc_total_err.sum(['cluster','pod_name','container_name','node_role','node_type']).rate('PT15S') + banyandb_liaison_grpc_total_registry_err.sum(['cluster','pod_name','container_name','node_role','node_type']).rate('PT15S') + banyandb_liaison_grpc_total_stream_msg_received_err.sum(['cluster','pod_name','container_name','node_role','node_type']).rate('PT15S')) * 60
+  # non-query operation rate (req/s): registry ops + any non-query unary call. total_started is
+  # query-only on the wire, so tagNotEqual('method','query') is empty today; registry_started carries it.
+  - name: non_query_op_rate
+    exp: banyandb_liaison_grpc_total_registry_started.sum(['cluster','pod_name','container_name','node_role','node_type']).rate('PT15S') + banyandb_liaison_grpc_total_started.tagNotEqual('method','query').sum(['cluster','pod_name','container_name','node_role','node_type']).rate('PT15S')
+  # write rate (writes/s) seen at the liaison front door. group label dropped (instance-level total).
+  - name: write_rate
+    exp: banyandb_measure_total_written.sum(['cluster','pod_name','container_name','node_role','node_type']).rate('PT15S') + banyandb_stream_tst_total_written.sum(['cluster','pod_name','container_name','node_role','node_type']).rate('PT15S') + banyandb_trace_tst_total_written.sum(['cluster','pod_name','container_name','node_role','node_type']).rate('PT15S')
+  # tier-2 publish pipeline (liaison -> data): throughput by operation, bytes/s, and p99 send latency.
+  - name: publish_throughput
+    exp: banyandb_queue_pub_total_finished.sum(['cluster','pod_name','container_name','node_role','node_type','operation']).rate('PT15S')
+  - name: publish_bytes
+    exp: banyandb_queue_pub_sent_bytes.sum(['cluster','pod_name','container_name','node_role','node_type']).rate('PT15S')
+  - name: publish_latency_p99
+    exp: banyandb_queue_pub_total_latency.sum(['cluster','pod_name','container_name','node_role','node_type','operation','le']).histogram().histogram_percentile([99])
+  # write-queue (wqueue) depth: pending records, on-disk file parts, in-memory parts. On the liaison
+  # these reflect the write buffer; the same families on data containers reflect storage parts (the
+  # dashboard gates on container_name). Gauges, summed to the instance.
+  - name: wqueue_pending
+    exp: banyandb_measure_pending_data_count.sum(['cluster','pod_name','container_name','node_role','node_type']) + banyandb_stream_tst_pending_data_count.sum(['cluster','pod_name','container_name','node_role','node_type']) + banyandb_trace_tst_pending_data_count.sum(['cluster','pod_name','container_name','node_role','node_type'])
+  - name: wqueue_file_parts
+    exp: banyandb_measure_total_file_parts.sum(['cluster','pod_name','container_name','node_role','node_type']) + banyandb_stream_tst_total_file_parts.sum(['cluster','pod_name','container_name','node_role','node_type']) + banyandb_trace_tst_total_file_parts.sum(['cluster','pod_name','container_name','node_role','node_type'])
+  - name: wqueue_mem_part
+    exp: banyandb_measure_total_mem_part.sum(['cluster','pod_name','container_name','node_role','node_type']) + banyandb_stream_tst_total_mem_part.sum(['cluster','pod_name','container_name','node_role','node_type']) + banyandb_trace_tst_total_mem_part.sum(['cluster','pod_name','container_name','node_role','node_type'])
 
+  # ---- Data only (backend; the dashboard gates these on container_name == data) ----
+  # total stored data elements (gauge).
+  - name: total_data
+    exp: banyandb_measure_total_file_elements.sum(['cluster','pod_name','container_name','node_role','node_type']) + banyandb_stream_tst_total_file_elements.sum(['cluster','pod_name','container_name','node_role','node_type']) + banyandb_trace_tst_total_file_elements.sum(['cluster','pod_name','container_name','node_role','node_type'])
+  # merge-loop iterations/s.
+  - name: merge_file_rate
+    exp: banyandb_measure_total_merge_loop_started.sum(['cluster','pod_name','container_name','node_role','node_type']).rate('PT15S') + banyandb_stream_tst_total_merge_loop_started.sum(['cluster','pod_name','container_name','node_role','node_type']).rate('PT15S') + banyandb_trace_tst_total_merge_loop_started.sum(['cluster','pod_name','container_name','node_role','node_type']).rate('PT15S')
+  # avg parts merged per merge loop on the file path (matches Grafana = rate(merged_parts{type=file}) /
+  # rate(merge_loop_started)). type='file' is data-only on the wire (liaison emits only type='mem').
+  - name: merge_file_partitions
+    exp: banyandb_measure_total_merged_parts.tagEqual('type','file').sum(['cluster','pod_name','container_name','node_role','node_type']).rate('PT15S').safeDiv(banyandb_measure_total_merge_loop_started.sum(['cluster','pod_name','container_name','node_role','node_type']).rate('PT15S')) + banyandb_stream_tst_total_merged_parts.tagEqual('type','file').sum(['cluster','pod_name','container_name','node_role','node_type']).rate('PT15S').safeDiv(banyandb_stream_tst_total_merge_loop_started.sum([ [...]
+  # avg file-merge latency (ms) per merge loop.
+  - name: merge_file_latency
+    exp: (banyandb_measure_total_merge_latency.tagEqual('type','file').sum(['cluster','pod_name','container_name','node_role','node_type']).rate('PT15S').safeDiv(banyandb_measure_total_merge_loop_started.sum(['cluster','pod_name','container_name','node_role','node_type']).rate('PT15S')) + banyandb_stream_tst_total_merge_latency.tagEqual('type','file').sum(['cluster','pod_name','container_name','node_role','node_type']).rate('PT15S').safeDiv(banyandb_stream_tst_total_merge_loop_started.su [...]
+  # inverted-index (series) write rate / term-search rate / total docs. *_inverted_index_total_* are
+  # # TYPE=gauge but cumulative, so rate() yields a per-window delta. Stream's series index is the
+  # storage scope (stream_storage_*); the tst scope is reported separately below.
+  - name: series_write_rate
+    exp: banyandb_measure_inverted_index_total_updates.sum(['cluster','pod_name','container_name','node_role','node_type']).rate('PT15S') + banyandb_stream_storage_inverted_index_total_updates.sum(['cluster','pod_name','container_name','node_role','node_type']).rate('PT15S')
+  - name: series_term_search_rate
+    exp: banyandb_measure_inverted_index_total_term_searchers_started.sum(['cluster','pod_name','container_name','node_role','node_type']).rate('PT15S') + banyandb_stream_storage_inverted_index_total_term_searchers_started.sum(['cluster','pod_name','container_name','node_role','node_type']).rate('PT15S')
+  - name: total_series
+    exp: banyandb_measure_inverted_index_total_doc_count.sum(['cluster','pod_name','container_name','node_role','node_type']) + banyandb_stream_storage_inverted_index_total_doc_count.sum(['cluster','pod_name','container_name','node_role','node_type'])
+  # stream time-series-table (tst) index, distinct from the stream series (storage) index above.
+  - name: stream_tst_write_rate
+    exp: banyandb_stream_tst_inverted_index_total_updates.sum(['cluster','pod_name','container_name','node_role','node_type']).rate('PT15S')
+  - name: stream_tst_term_search_rate
+    exp: banyandb_stream_tst_inverted_index_total_term_searchers_started.sum(['cluster','pod_name','container_name','node_role','node_type']).rate('PT15S')
+  - name: stream_tst_total_docs
+    exp: banyandb_stream_tst_inverted_index_total_doc_count.sum(['cluster','pod_name','container_name','node_role','node_type'])
+  # subscribe-side queue (data receives from liaison): throughput by operation + p99 latency.
+  - name: queue_sub_throughput
+    exp: banyandb_queue_sub_total_finished.sum(['cluster','pod_name','container_name','node_role','node_type','operation']).rate('PT15S')
+  - name: queue_sub_latency_p99
+    exp: banyandb_queue_sub_total_latency.sum(['cluster','pod_name','container_name','node_role','node_type','operation','le']).histogram().histogram_percentile([99])
+  # retention disk-usage % per data-model scope (0-100 gauge). Kept per scope rather than summed (a sum
+  # of three percentages is meaningless). Not in the upstream Grafana boards; a SkyWalking addition.
+  - name: retention_measure_disk_usage_percent
+    exp: banyandb_storage_retention_measure_disk_usage_percent
+  - name: retention_stream_disk_usage_percent
+    exp: banyandb_storage_retention_stream_disk_usage_percent
+  - name: retention_trace_disk_usage_percent
+    exp: banyandb_storage_retention_trace_disk_usage_percent
 
+  # ---- Lifecycle only (the tier-migration sidecar on hot/warm data pods; container_name == lifecycle) ----
+  # cumulative migration cycles. Compiled into build #1166 (BanyanDB #1164) but lazily registered:
+  # emits no series until the first migration cycle fires. Dashboard renders absent-as-0.
+  - name: lifecycle_cycles
+    exp: banyandb_lifecycle_cycles_total
+  # seconds since the last migration cycle started = now - epoch. time() is the MAL ingest-time scalar
+  # (MQE has no current-time function), computed when the rule runs. BUILD-GATED: the source gauge is
+  # BanyanDB #1167+, absent on build #1166 -> no series until the cluster runs >= #1167 AND a cycle ends.
+  - name: lifecycle_last_run
+    exp: (time() - banyandb_lifecycle_last_run_timestamp_seconds.max(['cluster','pod_name','container_name','node_role','node_type']))
+  # last cycle status (1 = OK, 0 = failed). Same #1167 build gate as lifecycle_last_run.
+  - name: lifecycle_last_run_success
+    exp: banyandb_lifecycle_last_run_success
diff --git a/oap-server/server-starter/src/main/resources/otel-rules/banyandb/banyandb-service.yaml b/oap-server/server-starter/src/main/resources/otel-rules/banyandb/banyandb-service.yaml
index 566f893cc4..97c6cac8f6 100644
--- a/oap-server/server-starter/src/main/resources/otel-rules/banyandb/banyandb-service.yaml
+++ b/oap-server/server-starter/src/main/resources/otel-rules/banyandb/banyandb-service.yaml
@@ -13,74 +13,51 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# This will parse a textual representation of a duration. The formats
-# accepted are based on the ISO-8601 duration format {@code PnDTnHnMn.nS}
-# with days considered to be exactly 24 hours.
-# <p>
-# Examples:
-# <pre>
-#    "PT20.345S" -- parses as "20.345 seconds"
-#    "PT15M"     -- parses as "15 minutes" (where a minute is 60 seconds)
-#    "PT10H"     -- parses as "10 hours" (where an hour is 3600 seconds)
-#    "P2D"       -- parses as "2 days" (where a day is 24 hours or 86400 seconds)
-#    "P2DT3H4M"  -- parses as "2 days, 3 hours and 4 minutes"
-#    "P-6H3M"    -- parses as "-6 hours and +3 minutes"
-#    "-P6H3M"    -- parses as "-6 hours and -3 minutes"
-#    "-P-6H+3M"  -- parses as "+6 hours and -3 minutes"
-# </pre>
+# SWIP-15: BanyanDB self-observability, Service scope = one BanyanDB cluster.
+# The FODC proxy is the single scrape target; the collector injects one static label
+# `cluster` (the only label SkyWalking must add). Every BanyanDB-native family carries
+# the `banyandb_` prefix; only the Go-runtime / process exporter families (go_* / process_*)
+# are bare. All cluster KPIs collapse to the single `cluster` series via `.sum(['cluster'])`,
+# which is also what makes the heterogeneous error families joinable by MAL `+`
+# (MAL arithmetic inner-joins on exact label equality).
+# Source expressions mirror the upstream BanyanDB Grafana boards
+# (docs/operation/grafana-fodc-workload.json) so the SkyWalking dashboards stay in lockstep.
 filter: "{ tags -> tags.job_name == 'banyandb-monitoring' }"
-expSuffix:  tag({tags -> tags.host_name = 'banyandb::' + tags.host_name}).service(['host_name'] , Layer.BANYANDB)
+expSuffix: service(['cluster'], Layer.BANYANDB)
 metricPrefix: meter_banyandb
 metricsRules:
-  - name: write_rate
-    exp: (banyandb_measure_total_written.sum(['host_name','service_instance_id']).rate('PT15S') + banyandb_stream_tst_total_written.sum(['host_name','service_instance_id']).rate('PT15S'))
-  - name: total_memory
-    exp: banyandb_system_memory_state.tagEqual('kind','total').sum(['host_name'])
-  - name: disk_usage
-    exp: banyandb_system_disk.tagEqual('kind','used').sum(['host_name','service_instance_id'])
-  - name: query_rate
-    exp: banyandb_liaison_grpc_total_started.sum(['method','host_name','service_instance_id'])
-  - name: total_cpu
-    exp: banyandb_system_cpu_num.sum(['method','host_name','service_instance_id'])
-  - name: write_and_query_errors_rate
-    exp: banyandb_liaison_grpc_total_err.tagEqual('method','query').sum(['method','host_name','service_instance_id']).rate('PT15S')*60 + banyandb_liaison_grpc_total_stream_msg_sent_err.sum(['host_name','service_instance_id']).rate('PT15S')*60 + banyandb_liaison_grpc_total_stream_msg_received_err.sum(['host_name','service_instance_id']).rate('PT15S')*60 + banyandb_queue_sub_total_msg_sent_err.sum(['host_name','service_instance_id']).rate('PT15S')*60
-  - name: etcd_operation_rate
-    exp: banyandb_liaison_grpc_total_registry_started.sum(['host_name','service_instance_id']).rate('PT15S') + banyandb_liaison_grpc_total_started.sum(['host_name','service_instance_id']).rate('PT15S')
-  - name: active_instance
-    exp: up.sum(['host_name','service_instance_id']).downsampling(MIN)
-  - name: cpu_usage
-    exp: (((process_cpu_seconds_total.sum(['host_name','service_instance_id']).rate('PT15S') / banyandb_system_cpu_num.sum(['host_name','service_instance_id']))).max(['host_name','service_instance_id']))*1000
-  - name: rss_memory_usage
-    exp: ((process_resident_memory_bytes.sum(['host_name','service_instance_id']).downsampling(MAX) / banyandb_system_memory_state.tagEqual('kind','total').sum(['host_name','service_instance_id'])).max(['host_name','service_instance_id']))*1000
-  - name: disk_usage_all
-    exp: ((banyandb_system_disk.tagEqual('kind','used').sum(['host_name','service_instance_id']) / banyandb_system_memory_state.tagEqual('kind','total').sum(['host_name','service_instance_id'])).max(['host_name','service_instance_id']))*1000
-  - name: network_usage_recv
-    exp: banyandb_system_net_state.tagEqual('kind','bytes_recv').sum(['host_name','service_instance_id']).rate('PT15S')
-  - name: network_usage_sent
-    exp: banyandb_system_net_state.tagEqual('kind','bytes_sent').sum(['host_name','service_instance_id']).rate('PT15S')
-  - name: storage_write_rate
-    exp: banyandb_measure_total_written.sum(['group','host_name','service_instance_id']).rate('PT15S')*1000
-  - name: query_latency
-    exp: (banyandb_liaison_grpc_total_latency.tagEqual('method','query').sum(['group','host_name','service_instance_id']).rate('PT15S') / banyandb_liaison_grpc_total_started.tagEqual('method','query').sum(['group','host_name','service_instance_id']).rate('PT15S'))*1000
-  - name: total_data
-    exp: banyandb_measure_total_file_elements.sum(['group','host_name','service_instance_id'])
-  - name: merge_file_data
-    exp: banyandb_measure_total_merge_loop_started.sum(['group','host_name','service_instance_id']).rate('PT15S') * 60 *1000
-  - name: merge_file_latency
-    exp: (banyandb_measure_total_merge_latency.tagEqual('type','file').sum(['group','host_name','service_instance_id']).rate('PT15S') / banyandb_measure_total_merge_loop_started.sum(['group','host_name','service_instance_id']).rate('PT15S'))*1000
-  - name: merge_file_partitions
-    exp: (banyandb_measure_total_merged_parts.tagEqual('type','file').sum(['group','host_name','service_instance_id']).rate('PT15S') / banyandb_measure_total_merge_loop_started.sum(['group','host_name','service_instance_id']).rate('PT15S'))*1000
-  - name: series_write_rate
-    exp: (banyandb_measure_inverted_index_total_updates.sum(['group','host_name','service_instance_id']).rate('PT15S'))*1000
-  - name: series_term_search_rate
-    exp: banyandb_stream_storage_inverted_index_total_term_searchers_started.sum(['group','host_name','service_instance_id']).rate('PT15S')
-  - name: total_series
-    exp: banyandb_measure_inverted_index_total_doc_count.sum(['group','host_name','service_instance_id'])
-  - name: stream_write_rate
-    exp: banyandb_stream_tst_inverted_index_total_updates.sum(['group','host_name','service_instance_id']).rate('PT15S')
-  - name: term_search_rate
-    exp: banyandb_stream_tst_inverted_index_total_term_searchers_started.sum(['group','host_name','service_instance_id']).rate('PT15S')* 1000
-  - name: total_document
-    exp: banyandb_stream_tst_inverted_index_total_doc_count.sum(['group','host_name','service_instance_id'])
-
-
+  # cluster writes/s across the three data-model scopes (measure, stream, trace). Each scope's
+  # write counter is collapsed to one per-cluster series before `+`.
+  - name: cluster_write_rate
+    exp: (banyandb_measure_total_written.sum(['cluster']).rate('PT15S') + banyandb_stream_tst_total_written.sum(['cluster']).rate('PT15S') + banyandb_trace_tst_total_written.sum(['cluster']).rate('PT15S'))
+  # cluster queries/s. `service` on this family is BanyanDB's data-model facet
+  # (measure/stream/trace/property), not a SkyWalking service; method literal is "query".
+  - name: cluster_query_rate
+    exp: banyandb_liaison_grpc_total_started.tagEqual('method','query').sum(['cluster']).rate('PT15S')
+  # cluster errors/min. The seven liaison-side error families mirror the upstream Grafana
+  # "Error Rate" stat (grafana-fodc-workload.json). Each is pre-aggregated to ['cluster']
+  # BEFORE `+` because their wire label sets differ (stream_msg_received_err carries
+  # group/method/service, registry_err carries method/service, sync_loop_err carries group)
+  # and MAL `+` joins on exact label equality. On a healthy cluster most of these are lazily
+  # registered and emit no series; MAL treats an empty operand as the additive identity, so the
+  # sum emits from whatever has fired and renders absent-as-0 when nothing has.
+  - name: cluster_error_rate
+    exp: (banyandb_liaison_grpc_total_err.sum(['cluster']).rate('PT15S') + banyandb_liaison_grpc_total_registry_err.sum(['cluster']).rate('PT15S') + banyandb_liaison_grpc_total_stream_msg_received_err.sum(['cluster']).rate('PT15S') + banyandb_queue_pub_total_err.sum(['cluster']).rate('PT15S') + banyandb_measure_total_sync_loop_err.sum(['cluster']).rate('PT15S') + banyandb_stream_tst_total_sync_loop_err.sum(['cluster']).rate('PT15S') + banyandb_trace_tst_total_sync_loop_err.sum(['cluster' [...]
+  # live container count by role. count(['cluster','container_name','pod_name']) groups by all
+  # three then re-groups excluding the last key (pod_name), yielding one sample per
+  # (cluster, container_name) whose value = distinct pod_name count -> data=N, liaison=M.
+  # Mirrors the upstream "Nodes by Role" stat (count(banyandb_system_up_time) by container_name).
+  # CAVEAT: banyandb_system_up_time has NO lifecycle series (the lifecycle sidecar runs its
+  # metric service without the system collector), so this never emits a lifecycle row.
+  - name: reporting_instances
+    exp: banyandb_system_up_time.count(['cluster','container_name','pod_name'])
+  # cluster CPU capacity = sum of per-container visible core counts (no lifecycle series).
+  - name: total_cpu_cores
+    exp: banyandb_system_cpu_num.sum(['cluster'])
+  # cluster memory used (bytes). kind='used' is a real wire value (kind in total/used/used_percent).
+  - name: total_memory_used
+    exp: banyandb_system_memory_state.tagEqual('kind','used').sum(['cluster'])
+  # cluster disk used (bytes). system_disk carries a `path` label with multiple data roots;
+  # .sum(['cluster']) collapses all paths into one cluster total.
+  - name: total_disk_used
+    exp: banyandb_system_disk.tagEqual('kind','used').sum(['cluster'])
diff --git a/test/e2e-v2/cases/banyandb/banyandb-cases.yaml b/test/e2e-v2/cases/banyandb/banyandb-cases.yaml
index dfc901f490..83367a20c9 100644
--- a/test/e2e-v2/cases/banyandb/banyandb-cases.yaml
+++ b/test/e2e-v2/cases/banyandb/banyandb-cases.yaml
@@ -13,30 +13,59 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# This file contains BanyanDB instance metrics queries, referencing
-# oap-server/server-starter/src/main/resources/otel-rules/banyandb.yaml
-
+# SWIP-15 BanyanDB self-observability metrics, the cluster / container / group model. References
+# oap-server/server-starter/src/main/resources/otel-rules/banyandb/{banyandb-service,banyandb-instance,banyandb-endpoint}.yaml
+# Entity identities come from the collector's injected labels (otel-collector-config.yaml):
+#   Service  = cluster                       -> e2e-banyandb
+#   Instance = pod_name '@' container_name    -> banyandb-liaison-0@liaison, banyandb-data-hot-0@data
+#   Endpoint = group                          -> sw_metricsMinute (an OAP self-telemetry group)
+# Expected templates:
+#   metrics-has-value.yml        — single unlabeled series (service/endpoint metrics summed to the entity key)
+#   metrics-has-label-value.yml  — labeled series. Instance metrics retain node_role/node_type labels
+#                                  (kept in the .sum() group-by so the instance properties closure resolves
+#                                  role/tier); reporting_instances is labeled by container_name; queue
+#                                  metrics are labeled by operation.
+# This minimal cluster (1 liaison + 1 hot data, no FODC) intentionally does NOT cover: lifecycle
+# (no migration sidecar / cycle), warm & cold tiers, error counters (lazily registered -> empty on a
+# healthy cluster), or multi-node aggregation.
 cases:
-  - query: swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql metrics exec --expression=meter_banyandb_total_memory --service-name=banyandb::server
-    expected: expected/metrics-has-value.yml
-  - query: swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql metrics exec --expression=meter_banyandb_instance_write_rate --service-name=banyandb::server --instance-name=banyandb:2121
-    expected: expected/metrics-has-value.yml
-  - query: swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql metrics exec --expression=meter_banyandb_instance_total_memory --service-name=banyandb::server --instance-name=banyandb:2121
+  # ---- Service scope (cluster KPIs) ----
+  - query: swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql metrics exec --expression=meter_banyandb_cluster_write_rate --service-name=e2e-banyandb
     expected: expected/metrics-has-value.yml
-  - query: swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql metrics exec --expression=meter_banyandb_instance_total_cpu --service-name=banyandb::server --instance-name=banyandb:2121
+  - query: swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql metrics exec --expression=meter_banyandb_cluster_query_rate --service-name=e2e-banyandb
     expected: expected/metrics-has-value.yml
-  - query: swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql metrics exec --expression=meter_banyandb_instance_etcd_operation_rate --service-name=banyandb::server --instance-name=banyandb:2121
+  - query: swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql metrics exec --expression=meter_banyandb_total_cpu_cores --service-name=e2e-banyandb
     expected: expected/metrics-has-value.yml
-  - query: swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql metrics exec --expression=meter_banyandb_instance_active_instance --service-name=banyandb::server --instance-name=banyandb:2121
+  - query: swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql metrics exec --expression=meter_banyandb_total_memory_used --service-name=e2e-banyandb
     expected: expected/metrics-has-value.yml
-  - query: swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql metrics exec --expression=meter_banyandb_instance_cpu_usage --service-name=banyandb::server --instance-name=banyandb:2121
+  - query: swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql metrics exec --expression=meter_banyandb_total_disk_used --service-name=e2e-banyandb
     expected: expected/metrics-has-value.yml
-  - query: swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql metrics exec --expression=meter_banyandb_instance_rss_memory_usage --service-name=banyandb::server --instance-name=banyandb:2121
+  # live container count by role (labeled by container_name)
+  - query: swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql metrics exec --expression=meter_banyandb_reporting_instances --service-name=e2e-banyandb
+    expected: expected/metrics-has-label-value.yml
+
+  # ---- Instance scope (labeled by node_role/node_type, and operation where applicable) ----
+  # data node (banyandb-data-hot-0@data)
+  - query: swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql metrics exec --expression=meter_banyandb_instance_node_uptime --service-name=e2e-banyandb --instance-name=banyandb-data-hot-0@data
     expected: expected/metrics-has-value.yml
-  - query: swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql metrics exec --expression=meter_banyandb_instance_disk_usage_all --service-name=banyandb::server --instance-name=banyandb:2121
+  - query: swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql metrics exec --expression=meter_banyandb_instance_cpu_usage --service-name=e2e-banyandb --instance-name=banyandb-data-hot-0@data
+    expected: expected/metrics-has-label-value.yml
+  - query: swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql metrics exec --expression=meter_banyandb_instance_total_data --service-name=e2e-banyandb --instance-name=banyandb-data-hot-0@data
+    expected: expected/metrics-has-label-value.yml
+  - query: swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql metrics exec --expression=meter_banyandb_instance_queue_sub_throughput --service-name=e2e-banyandb --instance-name=banyandb-data-hot-0@data
+    expected: expected/metrics-has-label-value.yml
+  # liaison node (banyandb-liaison-0@liaison)
+  - query: swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql metrics exec --expression=meter_banyandb_instance_node_uptime --service-name=e2e-banyandb --instance-name=banyandb-liaison-0@liaison
     expected: expected/metrics-has-value.yml
-  - query: swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql metrics exec --expression=meter_banyandb_instance_network_usage_recv --service-name=banyandb::server --instance-name=banyandb:2121
+  - query: swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql metrics exec --expression=meter_banyandb_instance_write_rate --service-name=e2e-banyandb --instance-name=banyandb-liaison-0@liaison
+    expected: expected/metrics-has-label-value.yml
+  - query: swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql metrics exec --expression=meter_banyandb_instance_publish_throughput --service-name=e2e-banyandb --instance-name=banyandb-liaison-0@liaison
+    expected: expected/metrics-has-label-value.yml
+
+  # ---- Endpoint scope (storage group sw_metricsMinute) ----
+  - query: swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql metrics exec --expression=meter_banyandb_endpoint_write_rate --service-name=e2e-banyandb --endpoint-name=sw_metricsMinute
     expected: expected/metrics-has-value.yml
-  - query: swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql metrics exec --expression=meter_banyandb_instance_network_usage_sent --service-name=banyandb::server --instance-name=banyandb:2121
+  - query: swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql metrics exec --expression=meter_banyandb_endpoint_total_data --service-name=e2e-banyandb --endpoint-name=sw_metricsMinute
     expected: expected/metrics-has-value.yml
-
+  - query: swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql metrics exec --expression=meter_banyandb_endpoint_queue_throughput --service-name=e2e-banyandb --endpoint-name=sw_metricsMinute
+    expected: expected/metrics-has-label-value.yml
diff --git a/test/e2e-v2/cases/banyandb/docker-compose.yml b/test/e2e-v2/cases/banyandb/docker-compose.yml
index 2b0d618a81..3bd2202ee6 100644
--- a/test/e2e-v2/cases/banyandb/docker-compose.yml
+++ b/test/e2e-v2/cases/banyandb/docker-compose.yml
@@ -13,25 +13,40 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+# SWIP-15 BanyanDB self-observability e2e: a minimal CLUSTER (1 liaison + 1 hot data node), scraped
+# WITHOUT the FODC proxy. The OTel collector scrapes each node's own :2121 Prometheus endpoint
+# directly and injects the FODC-equivalent identity labels
+# (cluster / container_name / node_role / node_type / pod_name) as static per-scrape-job labels
+# (see otel-collector-config.yaml). The MAL rules read those tags regardless of origin, so the
+# cluster / instance / endpoint rule set is exercised without a FODC deployment.
+#
+# BanyanDB 0.11+ is required: the FODC-proxy cluster observability and the queue / lifecycle metric
+# families this feature reads were introduced after 0.10, and 0.11 uses file/DNS node discovery
+# (no etcd). The image comes from the repo-wide ${SW_BANYANDB_COMMIT} (a 0.11+ build) via base-compose.
 services:
-  oap:
+  data-hot:
     extends:
       file: ../../script/docker-compose/base-compose.yml
-      service: oap
-    expose:
-      - 11800
-    ports:
-      - "11800:11800"
-      - "12800:12800"
+      service: banyandb-data
+    hostname: data-hot
+    command: data --node-discovery-mode=file --node-discovery-file-path=/etc/banyandb/nodes.yaml --node-labels type=hot
+    volumes:
+      - ./nodes.yaml:/etc/banyandb/nodes.yaml
     networks:
       - e2e
-  banyandb:
+
+  liaison:
     extends:
       file: ../../script/docker-compose/base-compose.yml
-      service: banyandb
-    ports:
-      - "17913:17913"
-      - "2121:2121"
+      service: liaison
+    command: liaison --node-discovery-mode=file --node-discovery-file-path=/etc/banyandb/nodes.yaml --data-node-selector type=hot
+    volumes:
+      - ./nodes.yaml:/etc/banyandb/nodes.yaml
+    depends_on:
+      data-hot:
+        condition: service_healthy
+    networks:
+      - e2e
 
   otel-collector:
     image: otel/opentelemetry-collector:${OTEL_COLLECTOR_VERSION}
@@ -42,6 +57,27 @@ services:
       - ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
     expose:
       - 55678
+    depends_on:
+      liaison:
+        condition: service_healthy
+      data-hot:
+        condition: service_healthy
+
+  oap:
+    extends:
+      file: ../../script/docker-compose/base-compose.yml
+      service: oap
+    environment:
+      SW_STORAGE: banyandb
+      SW_STORAGE_BANYANDB_TARGETS: "liaison:17912"
+    ports:
+      - "11800:11800"
+      - "12800:12800"
+    networks:
+      - e2e
+    depends_on:
+      liaison:
+        condition: service_healthy
 
 networks:
   e2e:
diff --git a/test/e2e-v2/cases/banyandb/e2e.yaml b/test/e2e-v2/cases/banyandb/e2e.yaml
index 965f8da653..96b4ba2371 100644
--- a/test/e2e-v2/cases/banyandb/e2e.yaml
+++ b/test/e2e-v2/cases/banyandb/e2e.yaml
@@ -44,7 +44,7 @@ cleanup:
     on: failure
     output-dir: $SW_INFRA_E2E_LOG_DIR/banyandb-data
     items:
-      - service: banyandb
+      - service: data-hot
         paths:
           - /tmp/trace/
           - /tmp/stream/
@@ -52,3 +52,6 @@ cleanup:
           - /tmp/property/
           - /tmp/schema-property/
           - /tmp/accesslog/
+      - service: liaison
+        paths:
+          - /tmp/accesslog/
diff --git a/test/e2e-v2/cases/banyandb/docker-compose.yml b/test/e2e-v2/cases/banyandb/expected/metrics-has-label-value.yml
similarity index 52%
copy from test/e2e-v2/cases/banyandb/docker-compose.yml
copy to test/e2e-v2/cases/banyandb/expected/metrics-has-label-value.yml
index 2b0d618a81..6fc7bffccc 100644
--- a/test/e2e-v2/cases/banyandb/docker-compose.yml
+++ b/test/e2e-v2/cases/banyandb/expected/metrics-has-label-value.yml
@@ -13,35 +13,28 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-services:
-  oap:
-    extends:
-      file: ../../script/docker-compose/base-compose.yml
-      service: oap
-    expose:
-      - 11800
-    ports:
-      - "11800:11800"
-      - "12800:12800"
-    networks:
-      - e2e
-  banyandb:
-    extends:
-      file: ../../script/docker-compose/base-compose.yml
-      service: banyandb
-    ports:
-      - "17913:17913"
-      - "2121:2121"
-
-  otel-collector:
-    image: otel/opentelemetry-collector:${OTEL_COLLECTOR_VERSION}
-    networks:
-      - e2e
-    command: [ "--config=/etc/otel-collector-config.yaml" ]
-    volumes:
-      - ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
-    expose:
-      - 55678
-
-networks:
-  e2e:
+# For labeled metric series (e.g. reporting_instances by container_name, queue throughput by
+# operation): at least one result carries a non-empty label and at least one non-null value bucket.
+debuggingtrace: null
+type: TIME_SERIES_VALUES
+results:
+  {{- contains .results }}
+  - metric:
+      labels:
+        {{- contains .metric.labels }}
+        - key: {{ notEmpty .key }}
+          value: {{ notEmpty .value }}
+        {{- end }}
+    values:
+      {{- contains .values }}
+      - id: {{ notEmpty .id }}
+        value: {{ notEmpty .value }}
+        traceid: null
+        owner: null
+      - id: {{ notEmpty .id }}
+        value: null
+        traceid: null
+        owner: null
+      {{- end }}
+  {{- end}}
+error: null
diff --git a/test/e2e-v2/cases/banyandb/otel-collector-config.yaml b/test/e2e-v2/cases/banyandb/nodes.yaml
similarity index 59%
copy from test/e2e-v2/cases/banyandb/otel-collector-config.yaml
copy to test/e2e-v2/cases/banyandb/nodes.yaml
index 30d45627d2..9c0602822a 100644
--- a/test/e2e-v2/cases/banyandb/otel-collector-config.yaml
+++ b/test/e2e-v2/cases/banyandb/nodes.yaml
@@ -13,36 +13,9 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-receivers:
-  prometheus:
-    config:
-      scrape_configs:
-        - job_name: "banyandb-monitoring" 
-          scrape_interval: 5s
-          static_configs:
-            - targets: ["banyandb:2121"]
-              labels:
-                host_name: server
-
-processors:
-  batch:
-
-exporters:
-  otlp:
-    endpoint: oap:11800
-    tls:
-      insecure: true
-  debug:  
-    verbosity: detailed  
-
-service:
-  pipelines:
-    metrics:
-      receivers:
-        - prometheus
-      processors:
-        - batch
-      exporters:
-        - otlp
-
-
+# Static node-discovery file for the BanyanDB 0.11 cluster (file discovery mode; no etcd).
+nodes:
+  - name: data-hot
+    grpc_address: data-hot:17912
+  - name: liaison
+    grpc_address: liaison:17912
diff --git a/test/e2e-v2/cases/banyandb/otel-collector-config.yaml b/test/e2e-v2/cases/banyandb/otel-collector-config.yaml
index 30d45627d2..cb2d071bbf 100644
--- a/test/e2e-v2/cases/banyandb/otel-collector-config.yaml
+++ b/test/e2e-v2/cases/banyandb/otel-collector-config.yaml
@@ -13,16 +13,39 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+# No FODC proxy in this e2e: the collector scrapes each BanyanDB node's own :2121 Prometheus
+# endpoint directly and injects the identity labels the SWIP-15 MAL rules key on. On a real
+# FODC deployment these are stamped by the proxy; here they are static per-target scrape labels
+# (the same mechanism the previous single-node e2e used to inject host_name, extended to two
+# targets and the full identity set). The OTel Prometheus receiver maps the Prometheus `job` to
+# service.name, which OAP's receiver maps back to the `job_name` tag the rules filter on; all
+# other static labels arrive as datapoint attributes (tags). Hence:
+#   - one job_name "banyandb-monitoring" (matches the filter on all three rule files)
+#   - per-target: cluster (service key), container_name (role discriminator + instance key),
+#     node_role, pod_name, and node_type (the data node's tier; liaison has no tier, so it is set
+#     explicitly to 'n/a' here — matching the instance() closure default — so labeled liaison
+#     metrics carry node_type='n/a' rather than an empty string).
 receivers:
   prometheus:
     config:
       scrape_configs:
-        - job_name: "banyandb-monitoring" 
+        - job_name: "banyandb-monitoring"
           scrape_interval: 5s
           static_configs:
-            - targets: ["banyandb:2121"]
+            - targets: ["liaison:2121"]
               labels:
-                host_name: server
+                cluster: e2e-banyandb
+                container_name: liaison
+                node_role: ROLE_LIAISON
+                node_type: n/a
+                pod_name: banyandb-liaison-0
+            - targets: ["data-hot:2121"]
+              labels:
+                cluster: e2e-banyandb
+                container_name: data
+                node_role: ROLE_DATA
+                node_type: hot
+                pod_name: banyandb-data-hot-0
 
 processors:
   batch:
@@ -32,8 +55,6 @@ exporters:
     endpoint: oap:11800
     tls:
       insecure: true
-  debug:  
-    verbosity: detailed  
 
 service:
   pipelines:
@@ -44,5 +65,3 @@ service:
         - batch
       exporters:
         - otlp
-
-
diff --git a/test/e2e-v2/cases/cluster/zk/banyandb/banyandb-cluster-cases.yaml b/test/e2e-v2/cases/cluster/zk/banyandb/banyandb-cluster-cases.yaml
new file mode 100644
index 0000000000..7f551acbdc
--- /dev/null
+++ b/test/e2e-v2/cases/cluster/zk/banyandb/banyandb-cluster-cases.yaml
@@ -0,0 +1,98 @@
+# 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.
+
+# BanyanDB variant of cluster-cases.yaml. Same OAP-cluster (ZooKeeper) coordination checks, but the
+# meter-downsampling data is sent at RECENT timestamps instead of the shared file's fixed 2022 time:
+# BanyanDB 0.11+ only serves queries within its retention window, so an old fixed time returns empty.
+# Two recent, same-hour minute slots (T1, T2) are computed once and shared via a temp file; each
+# downsampling query targets EXACTLY ONE slot (start == end) so the response is deterministically the
+# slot the data was sent into, asserted against the exact downsampled value.
+# (This file is banyandb-only; the shared cluster-cases.yaml / cluster/expected are unchanged.)
+  cases:
+    # verify OAP cluster is up (checks peer-forward counters, not storage). Sent ~25h ago so these
+    # batches land on a different day from the downsampling data below and never pollute its sums.
+    - query: |
+        NOW=$(( ($(date -u +%s) - 90000) * 1000 ));
+        curl -s -XPOST http://${sender1_host}:${sender1_9093}/sendBatchMetrics/$NOW/100 > /dev/null;
+        curl -s -XPOST http://${sender2_host}:${sender2_9093}/sendBatchMetrics/$NOW/100 > /dev/null;
+        sleep 10;
+        curl -s http://${oap1_host}:${oap1_1234} | grep remote_out_count{ | awk '{print $1}'
+      expected: ../../expected/cluster-up-oap1.yml
+    - query: |
+        NOW=$(( ($(date -u +%s) - 90000) * 1000 ));
+        curl -s -XPOST http://${sender1_host}:${sender1_9093}/sendBatchMetrics/$NOW/100 > /dev/null;
+        curl -s -XPOST http://${sender2_host}:${sender2_9093}/sendBatchMetrics/$NOW/100 > /dev/null;
+        sleep 5;
+        curl -s http://${oap2_host}:${oap2_1234} | grep remote_out_count{ | awk '{print $1}'
+      expected: ../../expected/cluster-up-oap2.yml
+    # compute two recent, same-hour minute slots and send the FIRST batch (2 senders x 100 = 200) at T1.
+    # T1 = 5 minutes ago (truncated to the minute); the :59 guard keeps T1 and T2 in the same hour/day.
+    - query: |
+        T1=$(( ($(date -u +%s) - 300) / 60 * 60 ));
+        [ $(( (T1 % 3600) / 60 )) -eq 59 ] && T1=$(( T1 - 60 ));
+        T2=$(( T1 + 60 ));
+        echo "$T1 $T2" > /tmp/banyandb_ds_slots;
+        curl -s -XPOST http://${sender1_host}:${sender1_9093}/sendBatchMetrics/$(( T1 * 1000 ))/100;
+        curl -s -XPOST http://${sender2_host}:${sender2_9093}/sendBatchMetrics/$(( T1 * 1000 ))/100;
+        sleep 10;
+      expected: ../../expected/send-metrics-rsp.yml
+    - query: swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql service ls
+      expected: ../../expected/service.yml
+    # service instance list
+    - query: swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql instance list --service-name=test-service
+      expected: ../../expected/service-instance.yml
+    # metrics minute — one slot = T1's minute -> 200
+    - query: |
+        read T1 T2 < /tmp/banyandb_ds_slots;
+        S=$(date -u -d @$T1 +"%Y-%m-%d %H%M" 2>/dev/null || date -u -r $T1 +"%Y-%m-%d %H%M");
+        swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql metrics exec --expression=test_meter_downsampling_sum --instance-name=test-instance --service-name=test-service --start "$S" --end "$S"
+      expected: expected/metrics-minute-first.yml
+    # metrics toHour — one slot = T1's hour -> 200
+    - query: |
+        read T1 T2 < /tmp/banyandb_ds_slots;
+        S=$(date -u -d @$T1 +"%Y-%m-%d %H" 2>/dev/null || date -u -r $T1 +"%Y-%m-%d %H");
+        swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql metrics exec --expression=test_meter_downsampling_sum --instance-name=test-instance --service-name=test-service --start "$S" --end "$S"
+      expected: expected/metrics-hour-first.yml
+    # metrics toDay — one slot = T1's day -> 200
+    - query: |
+        read T1 T2 < /tmp/banyandb_ds_slots;
+        S=$(date -u -d @$T1 +"%Y-%m-%d" 2>/dev/null || date -u -r $T1 +"%Y-%m-%d");
+        swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql metrics exec --expression=test_meter_downsampling_sum --instance-name=test-instance --service-name=test-service --start "$S" --end "$S"
+      expected: expected/metrics-day-first.yml
+    # send the SECOND batch (2 senders x 100 = 200) at T2 (a distinct minute in the same hour)
+    - query: |
+        read T1 T2 < /tmp/banyandb_ds_slots;
+        curl -s -XPOST http://${sender1_host}:${sender1_9093}/sendBatchMetrics/$(( T2 * 1000 ))/100;
+        curl -s -XPOST http://${sender2_host}:${sender2_9093}/sendBatchMetrics/$(( T2 * 1000 ))/100;
+        sleep 10;
+      expected: ../../expected/send-metrics-rsp.yml
+    # metrics minute — one slot = T2's minute -> 200
+    - query: |
+        read T1 T2 < /tmp/banyandb_ds_slots;
+        S=$(date -u -d @$T2 +"%Y-%m-%d %H%M" 2>/dev/null || date -u -r $T2 +"%Y-%m-%d %H%M");
+        swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql metrics exec --expression=test_meter_downsampling_sum --instance-name=test-instance --service-name=test-service --start "$S" --end "$S"
+      expected: expected/metrics-minute-second.yml
+    # metrics toHour — one slot = T1's hour, now sums both batches -> 400
+    - query: |
+        read T1 T2 < /tmp/banyandb_ds_slots;
+        S=$(date -u -d @$T1 +"%Y-%m-%d %H" 2>/dev/null || date -u -r $T1 +"%Y-%m-%d %H");
+        swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql metrics exec --expression=test_meter_downsampling_sum --instance-name=test-instance --service-name=test-service --start "$S" --end "$S"
+      expected: expected/metrics-hour-second.yml
+    # metrics toDay — one slot = T1's day, both batches -> 400
+    - query: |
+        read T1 T2 < /tmp/banyandb_ds_slots;
+        S=$(date -u -d @$T1 +"%Y-%m-%d" 2>/dev/null || date -u -r $T1 +"%Y-%m-%d");
+        swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql metrics exec --expression=test_meter_downsampling_sum --instance-name=test-instance --service-name=test-service --start "$S" --end "$S"
+      expected: expected/metrics-day-second.yml
diff --git a/test/e2e-v2/cases/cluster/zk/banyandb/e2e.yaml b/test/e2e-v2/cases/cluster/zk/banyandb/e2e.yaml
index 7db97cd61c..419bfa342c 100644
--- a/test/e2e-v2/cases/cluster/zk/banyandb/e2e.yaml
+++ b/test/e2e-v2/cases/cluster/zk/banyandb/e2e.yaml
@@ -37,7 +37,7 @@ verify:
     interval: 3s
   cases:
     - includes:
-        - ../../cluster-cases.yaml
+        - banyandb-cluster-cases.yaml
 
 cleanup:
   on: always
diff --git a/test/e2e-v2/cases/banyandb/otel-collector-config.yaml b/test/e2e-v2/cases/cluster/zk/banyandb/expected/metrics-day-first.yml
similarity index 58%
copy from test/e2e-v2/cases/banyandb/otel-collector-config.yaml
copy to test/e2e-v2/cases/cluster/zk/banyandb/expected/metrics-day-first.yml
index 30d45627d2..e28920acc6 100644
--- a/test/e2e-v2/cases/banyandb/otel-collector-config.yaml
+++ b/test/e2e-v2/cases/cluster/zk/banyandb/expected/metrics-day-first.yml
@@ -13,36 +13,21 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-receivers:
-  prometheus:
-    config:
-      scrape_configs:
-        - job_name: "banyandb-monitoring" 
-          scrape_interval: 5s
-          static_configs:
-            - targets: ["banyandb:2121"]
-              labels:
-                host_name: server
-
-processors:
-  batch:
-
-exporters:
-  otlp:
-    endpoint: oap:11800
-    tls:
-      insecure: true
-  debug:  
-    verbosity: detailed  
-
-service:
-  pipelines:
-    metrics:
-      receivers:
-        - prometheus
-      processors:
-        - batch
-      exporters:
-        - otlp
-
-
+# Single-slot query (start == end): exactly one downsampled bucket for the slot the data was
+# sent into. The id is the dynamic slot timestamp (notEmpty); the value is the exact downsampled
+# sum. Uses the contains pattern so each value item is in template scope.
+type: TIME_SERIES_VALUES
+results:
+  {{- contains .results }}
+  - metric:
+      labels: []
+    values:
+      {{- contains .values }}
+      - id: "{{ notEmpty .id }}"
+        value: "200"
+        owner: null
+        traceid: null
+      {{- end }}
+  {{- end }}
+error: null
+debuggingtrace: null
diff --git a/test/e2e-v2/cases/banyandb/otel-collector-config.yaml b/test/e2e-v2/cases/cluster/zk/banyandb/expected/metrics-day-second.yml
similarity index 58%
copy from test/e2e-v2/cases/banyandb/otel-collector-config.yaml
copy to test/e2e-v2/cases/cluster/zk/banyandb/expected/metrics-day-second.yml
index 30d45627d2..3462f9d5e4 100644
--- a/test/e2e-v2/cases/banyandb/otel-collector-config.yaml
+++ b/test/e2e-v2/cases/cluster/zk/banyandb/expected/metrics-day-second.yml
@@ -13,36 +13,21 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-receivers:
-  prometheus:
-    config:
-      scrape_configs:
-        - job_name: "banyandb-monitoring" 
-          scrape_interval: 5s
-          static_configs:
-            - targets: ["banyandb:2121"]
-              labels:
-                host_name: server
-
-processors:
-  batch:
-
-exporters:
-  otlp:
-    endpoint: oap:11800
-    tls:
-      insecure: true
-  debug:  
-    verbosity: detailed  
-
-service:
-  pipelines:
-    metrics:
-      receivers:
-        - prometheus
-      processors:
-        - batch
-      exporters:
-        - otlp
-
-
+# Single-slot query (start == end): exactly one downsampled bucket for the slot the data was
+# sent into. The id is the dynamic slot timestamp (notEmpty); the value is the exact downsampled
+# sum. Uses the contains pattern so each value item is in template scope.
+type: TIME_SERIES_VALUES
+results:
+  {{- contains .results }}
+  - metric:
+      labels: []
+    values:
+      {{- contains .values }}
+      - id: "{{ notEmpty .id }}"
+        value: "400"
+        owner: null
+        traceid: null
+      {{- end }}
+  {{- end }}
+error: null
+debuggingtrace: null
diff --git a/test/e2e-v2/cases/banyandb/otel-collector-config.yaml b/test/e2e-v2/cases/cluster/zk/banyandb/expected/metrics-hour-first.yml
similarity index 58%
copy from test/e2e-v2/cases/banyandb/otel-collector-config.yaml
copy to test/e2e-v2/cases/cluster/zk/banyandb/expected/metrics-hour-first.yml
index 30d45627d2..e28920acc6 100644
--- a/test/e2e-v2/cases/banyandb/otel-collector-config.yaml
+++ b/test/e2e-v2/cases/cluster/zk/banyandb/expected/metrics-hour-first.yml
@@ -13,36 +13,21 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-receivers:
-  prometheus:
-    config:
-      scrape_configs:
-        - job_name: "banyandb-monitoring" 
-          scrape_interval: 5s
-          static_configs:
-            - targets: ["banyandb:2121"]
-              labels:
-                host_name: server
-
-processors:
-  batch:
-
-exporters:
-  otlp:
-    endpoint: oap:11800
-    tls:
-      insecure: true
-  debug:  
-    verbosity: detailed  
-
-service:
-  pipelines:
-    metrics:
-      receivers:
-        - prometheus
-      processors:
-        - batch
-      exporters:
-        - otlp
-
-
+# Single-slot query (start == end): exactly one downsampled bucket for the slot the data was
+# sent into. The id is the dynamic slot timestamp (notEmpty); the value is the exact downsampled
+# sum. Uses the contains pattern so each value item is in template scope.
+type: TIME_SERIES_VALUES
+results:
+  {{- contains .results }}
+  - metric:
+      labels: []
+    values:
+      {{- contains .values }}
+      - id: "{{ notEmpty .id }}"
+        value: "200"
+        owner: null
+        traceid: null
+      {{- end }}
+  {{- end }}
+error: null
+debuggingtrace: null
diff --git a/test/e2e-v2/cases/banyandb/otel-collector-config.yaml b/test/e2e-v2/cases/cluster/zk/banyandb/expected/metrics-hour-second.yml
similarity index 58%
copy from test/e2e-v2/cases/banyandb/otel-collector-config.yaml
copy to test/e2e-v2/cases/cluster/zk/banyandb/expected/metrics-hour-second.yml
index 30d45627d2..3462f9d5e4 100644
--- a/test/e2e-v2/cases/banyandb/otel-collector-config.yaml
+++ b/test/e2e-v2/cases/cluster/zk/banyandb/expected/metrics-hour-second.yml
@@ -13,36 +13,21 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-receivers:
-  prometheus:
-    config:
-      scrape_configs:
-        - job_name: "banyandb-monitoring" 
-          scrape_interval: 5s
-          static_configs:
-            - targets: ["banyandb:2121"]
-              labels:
-                host_name: server
-
-processors:
-  batch:
-
-exporters:
-  otlp:
-    endpoint: oap:11800
-    tls:
-      insecure: true
-  debug:  
-    verbosity: detailed  
-
-service:
-  pipelines:
-    metrics:
-      receivers:
-        - prometheus
-      processors:
-        - batch
-      exporters:
-        - otlp
-
-
+# Single-slot query (start == end): exactly one downsampled bucket for the slot the data was
+# sent into. The id is the dynamic slot timestamp (notEmpty); the value is the exact downsampled
+# sum. Uses the contains pattern so each value item is in template scope.
+type: TIME_SERIES_VALUES
+results:
+  {{- contains .results }}
+  - metric:
+      labels: []
+    values:
+      {{- contains .values }}
+      - id: "{{ notEmpty .id }}"
+        value: "400"
+        owner: null
+        traceid: null
+      {{- end }}
+  {{- end }}
+error: null
+debuggingtrace: null
diff --git a/test/e2e-v2/cases/banyandb/otel-collector-config.yaml b/test/e2e-v2/cases/cluster/zk/banyandb/expected/metrics-minute-first.yml
similarity index 58%
copy from test/e2e-v2/cases/banyandb/otel-collector-config.yaml
copy to test/e2e-v2/cases/cluster/zk/banyandb/expected/metrics-minute-first.yml
index 30d45627d2..e28920acc6 100644
--- a/test/e2e-v2/cases/banyandb/otel-collector-config.yaml
+++ b/test/e2e-v2/cases/cluster/zk/banyandb/expected/metrics-minute-first.yml
@@ -13,36 +13,21 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-receivers:
-  prometheus:
-    config:
-      scrape_configs:
-        - job_name: "banyandb-monitoring" 
-          scrape_interval: 5s
-          static_configs:
-            - targets: ["banyandb:2121"]
-              labels:
-                host_name: server
-
-processors:
-  batch:
-
-exporters:
-  otlp:
-    endpoint: oap:11800
-    tls:
-      insecure: true
-  debug:  
-    verbosity: detailed  
-
-service:
-  pipelines:
-    metrics:
-      receivers:
-        - prometheus
-      processors:
-        - batch
-      exporters:
-        - otlp
-
-
+# Single-slot query (start == end): exactly one downsampled bucket for the slot the data was
+# sent into. The id is the dynamic slot timestamp (notEmpty); the value is the exact downsampled
+# sum. Uses the contains pattern so each value item is in template scope.
+type: TIME_SERIES_VALUES
+results:
+  {{- contains .results }}
+  - metric:
+      labels: []
+    values:
+      {{- contains .values }}
+      - id: "{{ notEmpty .id }}"
+        value: "200"
+        owner: null
+        traceid: null
+      {{- end }}
+  {{- end }}
+error: null
+debuggingtrace: null
diff --git a/test/e2e-v2/cases/banyandb/otel-collector-config.yaml b/test/e2e-v2/cases/cluster/zk/banyandb/expected/metrics-minute-second.yml
similarity index 58%
copy from test/e2e-v2/cases/banyandb/otel-collector-config.yaml
copy to test/e2e-v2/cases/cluster/zk/banyandb/expected/metrics-minute-second.yml
index 30d45627d2..e28920acc6 100644
--- a/test/e2e-v2/cases/banyandb/otel-collector-config.yaml
+++ b/test/e2e-v2/cases/cluster/zk/banyandb/expected/metrics-minute-second.yml
@@ -13,36 +13,21 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-receivers:
-  prometheus:
-    config:
-      scrape_configs:
-        - job_name: "banyandb-monitoring" 
-          scrape_interval: 5s
-          static_configs:
-            - targets: ["banyandb:2121"]
-              labels:
-                host_name: server
-
-processors:
-  batch:
-
-exporters:
-  otlp:
-    endpoint: oap:11800
-    tls:
-      insecure: true
-  debug:  
-    verbosity: detailed  
-
-service:
-  pipelines:
-    metrics:
-      receivers:
-        - prometheus
-      processors:
-        - batch
-      exporters:
-        - otlp
-
-
+# Single-slot query (start == end): exactly one downsampled bucket for the slot the data was
+# sent into. The id is the dynamic slot timestamp (notEmpty); the value is the exact downsampled
+# sum. Uses the contains pattern so each value item is in template scope.
+type: TIME_SERIES_VALUES
+results:
+  {{- contains .results }}
+  - metric:
+      labels: []
+    values:
+      {{- contains .values }}
+      - id: "{{ notEmpty .id }}"
+        value: "200"
+        owner: null
+        traceid: null
+      {{- end }}
+  {{- end }}
+error: null
+debuggingtrace: null
diff --git a/test/e2e-v2/script/docker-compose/base-compose.yml b/test/e2e-v2/script/docker-compose/base-compose.yml
index b05b07946a..5d18faf416 100644
--- a/test/e2e-v2/script/docker-compose/base-compose.yml
+++ b/test/e2e-v2/script/docker-compose/base-compose.yml
@@ -71,7 +71,9 @@ services:
       - 17912
       - 2121
       - 6060
-    command: data --etcd-endpoints=http://etcd:2379
+    # BanyanDB 0.11+ uses file/DNS node discovery (no etcd). Cluster cases mount their own
+    # nodes.yaml at this path and add --node-labels / --data-node-selector as needed.
+    command: data --node-discovery-mode=file --node-discovery-file-path=/etc/banyandb/nodes.yaml
     healthcheck:
       test: [ "CMD", "./bydbctl", "health", "--addr=http://127.0.0.1:17913" ]
       interval: 5s
@@ -87,7 +89,7 @@ services:
     expose:
       - 2121
       - 6060
-    command: liaison --etcd-endpoints=http://etcd:2379 --measure-metadata-cache-wait-duration 1m --stream-metadata-cache-wait-duration 1m
+    command: liaison --node-discovery-mode=file --node-discovery-file-path=/etc/banyandb/nodes.yaml --measure-metadata-cache-wait-duration 1m --stream-metadata-cache-wait-duration 1m
     healthcheck:
       test: [ "CMD", "./bydbctl", "health", "--addr=http://liaison:17913" ]
       interval: 5s
diff --git a/test/e2e-v2/script/env b/test/e2e-v2/script/env
index d51720c7b9..5a87ea4991 100644
--- a/test/e2e-v2/script/env
+++ b/test/e2e-v2/script/env
@@ -23,7 +23,7 @@ SW_AGENT_CLIENT_JS_COMMIT=f08776d909eb1d9bc79c600e493030651b97e491
 SW_AGENT_CLIENT_JS_TEST_COMMIT=4f1eb1dcdbde3ec4a38534bf01dded4ab5d2f016
 SW_KUBERNETES_COMMIT_SHA=da0e267f877b9b8e5f7728ae4ea7dc7723a2a073
 SW_ROVER_COMMIT=79292fe07f17f98f486e0c4471213e1961fb2d1d
-SW_BANYANDB_COMMIT=84b919efca3fee3d51df9e97a734a9f10ae6f1d2
+SW_BANYANDB_COMMIT=8a1936ce96653e89d3d13250a42abc6e3d42fae7
 SW_AGENT_PHP_COMMIT=d1114e7be5d89881eec76e5b56e69ff844691e35
 SW_PREDICTOR_COMMIT=54a0197654a3781a6f73ce35146c712af297c994