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/08/04 13:29:52 UTC

(skywalking) branch master updated: Add the MAL functions tests back. (#13967)

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 c693967467 Add the MAL functions tests back. (#13967)
c693967467 is described below

commit c69396746755f53581f74a33fc1ba58c5285cd6f
Author: Wan Kai <wa...@foxmail.com>
AuthorDate: Tue Aug 4 21:29:46 2026 +0800

    Add the MAL functions tests back. (#13967)
    
    - ScopeTest: compare the produced/expected MeterEntity key sets and iterate over
      the expected map so a missing or extra entity fails the test.
    - K8sTagTest: capture and restore the KubernetesServices/KubernetesPods enum
      singletons so the mocked instances do not leak into other tests.
    - DecorateTest: move MeterEntity naming-control setup into @BeforeAll/@AfterAll
      and reset it so the global state is not leaked.
---
 .../oap/meter/analyzer/v2/dsl/AggregationTest.java | 238 ++++++++
 .../oap/meter/analyzer/v2/dsl/ArithmeticTest.java  | 473 +++++++++++++++
 .../oap/meter/analyzer/v2/dsl/BasicTest.java       |  78 +++
 .../oap/meter/analyzer/v2/dsl/DecorateTest.java    |  64 ++
 .../oap/meter/analyzer/v2/dsl/FilterTest.java      |  99 ++++
 .../oap/meter/analyzer/v2/dsl/FunctionTest.java    | 145 +++++
 .../oap/meter/analyzer/v2/dsl/IncreaseTest.java    | 240 ++++++++
 .../oap/meter/analyzer/v2/dsl/K8sTagTest.java      | 340 +++++++++++
 .../oap/meter/analyzer/v2/dsl/ScopeTest.java       | 642 +++++++++++++++++++++
 .../oap/meter/analyzer/v2/dsl/TagFilterTest.java   | 140 +++++
 .../oap/meter/analyzer/v2/dsl/ValueFilterTest.java | 148 +++++
 .../analyzer/v2/dsl/counter/CounterWindowTest.java |  97 ++++
 .../src/main/resources/application.yml             |   4 +
 13 files changed, 2708 insertions(+)

diff --git a/oap-server/analyzer/meter-analyzer/src/test/java/org/apache/skywalking/oap/meter/analyzer/v2/dsl/AggregationTest.java b/oap-server/analyzer/meter-analyzer/src/test/java/org/apache/skywalking/oap/meter/analyzer/v2/dsl/AggregationTest.java
new file mode 100644
index 0000000000..0e438ac883
--- /dev/null
+++ b/oap-server/analyzer/meter-analyzer/src/test/java/org/apache/skywalking/oap/meter/analyzer/v2/dsl/AggregationTest.java
@@ -0,0 +1,238 @@
+/*
+ * 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.
+ *
+ */
+
+package org.apache.skywalking.oap.meter.analyzer.v2.dsl;
+
+import com.google.common.collect.ImmutableMap;
+import lombok.extern.slf4j.Slf4j;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import java.util.Arrays;
+import java.util.Collection;
+
+import static com.google.common.collect.ImmutableMap.of;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.fail;
+
+/**
+ * Grouped aggregation expressions were adapted from the v1 named-arg Groovy idiom to the v2
+ * positional-list form: {@code sum(by = ['region', 'idc'])} became {@code sum(['region', 'idc'])}.
+ * v2's ANTLR grammar has no named arguments. Expected outputs are unchanged from v1.
+ *
+ * <p>Total (un-grouped) aggregation has no v2 MAL syntax and is pinned as a negative case per
+ * aggregation function: v1 {@code sum()} aggregated every sample into one, but v2's grammar
+ * ({@code stringList : L_BRACKET STRING (COMMA STRING)* R_BRACKET}) requires at least one grouping
+ * label and there is no no-arg overload, so both {@code sum()} and {@code sum([])} are rejected at
+ * parse time. Grouping labels are mandatory in v2; every production MAL rule supplies them.
+ */
+@Slf4j
+public class AggregationTest {
+    public static Collection<Object[]> data() {
+        return Arrays.asList(new Object[][] {
+            {
+                "sum",
+                of("http_success_request", SampleFamilyBuilder.newBuilder(
+                        Sample.builder().labels(of("idc", "t1")).value(50).name("http_success_request").build(),
+                        Sample.builder().labels(of("idc", "t2")).value(3).name("http_success_request").build()
+                    ).build()),
+                // total aggregation: no v2 MAL syntax (see class javadoc), pinned as a negative case
+                "http_success_request.sum()",
+                Result.success(SampleFamilyBuilder.newBuilder(Sample.builder().labels(ImmutableMap.of()).value(53).name("http_success_request").build()).build()),
+                true,
+            },
+            {
+                "sum-by",
+                of("http_success_request", SampleFamilyBuilder.newBuilder(
+                    Sample.builder().labels(of("idc", "t1")).value(50).name("http_success_request").build(),
+                    Sample.builder().labels(of("idc", "t3", "region", "cn", "svc", "catalog")).value(50).name("http_success_request").build(),
+                    Sample.builder().labels(of("idc", "t1", "region", "us", "svc", "product")).value(50).name("http_success_request").build(),
+                    Sample.builder().labels(of("idc", "t1", "region", "us", "instance", "10.0.0.1")).name("http_success_request").value(50).build(),
+                    Sample.builder().labels(of("idc", "t3", "region", "cn", "instance", "10.0.0.1")).name("http_success_request").value(3).build()
+                ).build()),
+                "http_success_request.sum(['region', 'idc'])",
+                Result.success(SampleFamilyBuilder.newBuilder(
+                    Sample.builder().labels(of("idc", "t1", "region", "")).value(50).name("http_success_request").build(),
+                    Sample.builder().labels(of("idc", "t1", "region", "us")).value(100).name("http_success_request").build(),
+                    Sample.builder().labels(of("idc", "t3", "region", "cn")).value(53).name("http_success_request").build()
+                ).build()),
+                false,
+            },
+
+            {
+                "min",
+                of("http_success_request", SampleFamilyBuilder.newBuilder(
+                    Sample.builder().labels(of("idc", "t3")).value(100).name("http_success_request").build(),
+                    Sample.builder().labels(of("idc", "t1")).value(50).name("http_success_request").build(),
+                    Sample.builder().labels(of("idc", "t2")).value(3).name("http_success_request").build()
+                ).build()),
+                // total aggregation: no v2 MAL syntax (see class javadoc), pinned as a negative case
+                "http_success_request.min()",
+                Result.success(SampleFamilyBuilder.newBuilder(Sample.builder().labels(ImmutableMap.of()).value(3).name("http_success_request").build()).build()),
+                true,
+            },
+            {
+                "min-by",
+                of("http_success_request", SampleFamilyBuilder.newBuilder(
+                    Sample.builder().labels(of("idc", "t1")).value(50).name("http_success_request").build(),
+                    Sample.builder().labels(of("idc", "t3", "region", "cn", "svc", "catalog")).value(50).name("http_success_request").build(),
+                    Sample.builder().labels(of("idc", "t1", "region", "us", "svc", "product")).value(50).name("http_success_request").build(),
+                    Sample.builder().labels(of("idc", "t1", "region", "us", "instance", "10.0.0.1")).value(100).name("http_success_request").build(),
+                    Sample.builder().labels(of("idc", "t3", "region", "cn", "instance", "10.0.0.1")).value(3).name("http_success_request").build()
+                ).build()),
+                "http_success_request.min(['region', 'idc'])",
+                Result.success(SampleFamilyBuilder.newBuilder(
+                    Sample.builder().labels(of("idc", "t1", "region", "")).value(50).name("http_success_request").build(),
+                    Sample.builder().labels(of("idc", "t1", "region", "us")).value(50).name("http_success_request").build(),
+                    Sample.builder().labels(of("idc", "t3", "region", "cn")).value(3).name("http_success_request").build()
+                ).build()),
+                false,
+            },
+            {
+                "max",
+                of("http_success_request", SampleFamilyBuilder.newBuilder(
+                    Sample.builder().labels(of("idc", "t3")).value(100).name("http_success_request").build(),
+                    Sample.builder().labels(of("idc", "t1")).value(50).name("http_success_request").build(),
+                    Sample.builder().labels(of("idc", "t2")).value(3).name("http_success_request").build()
+                ).build()),
+                // total aggregation: no v2 MAL syntax (see class javadoc), pinned as a negative case
+                "http_success_request.max()",
+                Result.success(SampleFamilyBuilder.newBuilder(Sample.builder().labels(ImmutableMap.of()).value(100).name("http_success_request").build()).build()),
+                true,
+            },
+            {
+                "max-by",
+                of("http_success_request", SampleFamilyBuilder.newBuilder(
+                    Sample.builder().labels(of("idc", "t1")).value(50).name("http_success_request").build(),
+                    Sample.builder().labels(of("idc", "t3", "region", "cn", "svc", "catalog")).value(50).name("http_success_request").build(),
+                    Sample.builder().labels(of("idc", "t1", "region", "us", "svc", "product")).value(50).name("http_success_request").build(),
+                    Sample.builder().labels(of("idc", "t1", "region", "us", "instance", "10.0.0.1")).value(100).name("http_success_request").build(),
+                    Sample.builder().labels(of("idc", "t3", "region", "cn", "instance", "10.0.0.1")).value(3).name("http_success_request").build()
+                ).build()),
+                "http_success_request.max(['region', 'idc'])",
+                Result.success(SampleFamilyBuilder.newBuilder(
+                    Sample.builder().labels(of("idc", "t1", "region", "")).value(50).name("http_success_request").build(),
+                    Sample.builder().labels(of("idc", "t1", "region", "us")).value(100).name("http_success_request").build(),
+                    Sample.builder().labels(of("idc", "t3", "region", "cn")).value(50).name("http_success_request").build()
+                ).build()),
+                false,
+            },
+
+            {
+                "avg",
+                of("http_success_request", SampleFamilyBuilder.newBuilder(
+                    Sample.builder().labels(of("idc", "t3")).value(100).name("http_success_request").build(),
+                    Sample.builder().labels(of("idc", "t1")).value(50).name("http_success_request").build(),
+                    Sample.builder().labels(of("idc", "t2")).value(3).name("http_success_request").build()
+                ).build()),
+                // total aggregation: no v2 MAL syntax (see class javadoc), pinned as a negative case
+                "http_success_request.avg()",
+                Result.success(SampleFamilyBuilder.newBuilder(Sample.builder().labels(ImmutableMap.of()).value(51).name("http_success_request").build()).build()),
+                true,
+            },
+            {
+                "avg-by",
+                of("http_success_request", SampleFamilyBuilder.newBuilder(
+                    Sample.builder().labels(of("idc", "t1")).value(50).name("http_success_request").build(),
+                    Sample.builder().labels(of("idc", "t3", "region", "cn", "svc", "catalog")).value(51).name("http_success_request").build(),
+                    Sample.builder().labels(of("idc", "t1", "region", "us", "svc", "product")).value(50).name("http_success_request").build(),
+                    Sample.builder().labels(of("idc", "t1", "region", "us", "instance", "10.0.0.1")).value(100).name("http_success_request").build(),
+                    Sample.builder().labels(of("idc", "t3", "region", "cn", "instance", "10.0.0.1")).value(3).name("http_success_request").build()
+                ).build()),
+                "http_success_request.avg(['region', 'idc'])",
+                Result.success(SampleFamilyBuilder.newBuilder(
+                    Sample.builder().labels(of("idc", "t1", "region", "")).value(50).name("http_success_request").build(),
+                    Sample.builder().labels(of("idc", "t1", "region", "us")).value(75).name("http_success_request").build(),
+                    Sample.builder().labels(of("idc", "t3", "region", "cn")).value(27).name("http_success_request").build()
+                ).build()),
+                false,
+            },
+
+            {
+                "count",
+                of("http_success_request", SampleFamilyBuilder.newBuilder(
+                        Sample.builder().labels(of("idc", "t3")).value(100).name("http_success_request").build(),
+                        Sample.builder().labels(of("idc", "t1")).value(50).name("http_success_request").build(),
+                        Sample.builder().labels(of("idc", "t2")).value(3).name("http_success_request").build()
+                ).build()),
+                // total aggregation: no v2 MAL syntax (see class javadoc), pinned as a negative case
+                "http_success_request.count()",
+                Result.success(SampleFamilyBuilder.newBuilder(Sample.builder().labels(ImmutableMap.of()).value(3).name("http_success_request").build()).build()),
+                true,
+            },
+            {
+                "count-by-one",
+                of("http_success_request", SampleFamilyBuilder.newBuilder(
+                        Sample.builder().labels(of("idc", "t1")).value(50).name("http_success_request").build(),
+                        Sample.builder().labels(of("idc", "t1", "region", "cn", "instance", "10.0.0.1")).value(50).name("http_success_request").build(),
+                        Sample.builder().labels(of("idc", "t2", "region", "us", "instance", "10.0.0.2")).value(50).name("http_success_request").build(),
+                        Sample.builder().labels(of("idc", "t1", "region", "us", "instance", "10.0.0.3")).value(100).name("http_success_request").build(),
+                        Sample.builder().labels(of("idc", "t2", "region", "cn", "instance", "10.0.0.3")).value(3).name("http_success_request").build()
+                ).build()),
+                "http_success_request.count(['instance'])",
+                Result.success(SampleFamilyBuilder.newBuilder(
+                        Sample.builder().labels(ImmutableMap.of()).value(3).name("http_success_request").build()
+                ).build()),
+                false,
+            },
+            {
+                "count-by-multi",
+                of("http_success_request", SampleFamilyBuilder.newBuilder(
+                        Sample.builder().labels(of("idc", "t1")).value(50).name("http_success_request").build(),
+                        Sample.builder().labels(of("idc", "t1", "region", "cn", "instance", "10.0.0.1")).value(50).name("http_success_request").build(),
+                        Sample.builder().labels(of("idc", "t1", "region", "cn", "instance", "10.0.0.1")).value(50).name("http_success_request").build(),
+                        Sample.builder().labels(of("idc", "t2", "region", "us", "instance", "10.0.0.2")).value(50).name("http_success_request").build(),
+                        Sample.builder().labels(of("idc", "t1", "region", "us", "instance", "10.0.0.3")).value(100).name("http_success_request").build(),
+                        Sample.builder().labels(of("idc", "t1", "region", "us", "instance", "10.0.0.4")).value(100).name("http_success_request").build(),
+                        Sample.builder().labels(of("idc", "t2", "region", "cn", "instance", "10.0.0.5")).value(3).name("http_success_request").build()
+                ).build()),
+                "http_success_request.count(['region','instance'])",
+                Result.success(SampleFamilyBuilder.newBuilder(
+                        Sample.builder().labels(of("region", "us")).value(3).name("http_success_request").build(),
+                        Sample.builder().labels(of("region", "cn")).value(2).name("http_success_request").build()
+                ).build()),
+                false,
+            },
+        });
+    }
+
+    @ParameterizedTest(name = "{0}")
+    @MethodSource("data")
+    public void test(String name,
+                     ImmutableMap<String, SampleFamily> input,
+                     String expression,
+                     Result want,
+                     boolean isThrow) {
+        Result r = null;
+        try {
+            // v2 compiles eagerly, so parse-time failures surface here too (v1 surfaced them at run()).
+            Expression e = DSL.parse(name, expression);
+            r = e.run(input);
+        } catch (Throwable t) {
+            if (isThrow) {
+                return;
+            }
+            log.error("Test failed", t);
+            fail("Should not throw anything");
+        }
+        if (isThrow) {
+            fail("Should throw something");
+        }
+        assertEquals(want, r);
+    }
+}
diff --git a/oap-server/analyzer/meter-analyzer/src/test/java/org/apache/skywalking/oap/meter/analyzer/v2/dsl/ArithmeticTest.java b/oap-server/analyzer/meter-analyzer/src/test/java/org/apache/skywalking/oap/meter/analyzer/v2/dsl/ArithmeticTest.java
new file mode 100644
index 0000000000..1ee98fce30
--- /dev/null
+++ b/oap-server/analyzer/meter-analyzer/src/test/java/org/apache/skywalking/oap/meter/analyzer/v2/dsl/ArithmeticTest.java
@@ -0,0 +1,473 @@
+/*
+ * 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.
+ *
+ */
+
+package org.apache.skywalking.oap.meter.analyzer.v2.dsl;
+
+import com.google.common.collect.ImmutableMap;
+import lombok.extern.slf4j.Slf4j;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import java.util.Arrays;
+import java.util.Collection;
+
+import static com.google.common.collect.ImmutableMap.of;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.fail;
+
+@Slf4j
+public class ArithmeticTest {
+    public static Collection<Object[]> data() {
+        return Arrays.asList(new Object[][] {
+            {
+                "plus-scalar-1",
+                of("http_success_request", SampleFamilyBuilder.newBuilder(
+                    Sample.builder()
+                          .labels(of("idc", "t1"))
+                          .value(1600592418480.0)
+                          .name("http_success_request")
+                          .build(),
+                    Sample.builder()
+                          .labels(of("idc", "t2"))
+                          .value(1600592418481.0)
+                          .name("http_success_request")
+                          .build()
+                ).build()),
+                "1000 + http_success_request.tagEqual('idc','t1')",
+                Result.success(SampleFamilyBuilder.newBuilder(
+                    Sample.builder()
+                          .labels(of("idc", "t1"))
+                          .value(1600592419480.0)
+                          .name("http_success_request")
+                          .build()
+                ).build()),
+                false,
+                },
+            {
+                "plus-scalar",
+                of("http_success_request", SampleFamilyBuilder.newBuilder(
+                    Sample.builder()
+                          .labels(of("idc", "t1"))
+                          .value(1600592418480.0)
+                          .name("http_success_request")
+                          .build(),
+                    Sample.builder()
+                          .labels(of("idc", "t2"))
+                          .value(1600592418481.0)
+                          .name("http_success_request")
+                          .build()
+                ).build()),
+                "http_success_request.tagEqual('idc','t1') + 1000",
+                Result.success(SampleFamilyBuilder.newBuilder(
+                    Sample.builder()
+                          .labels(of("idc", "t1"))
+                          .value(1600592419480.0)
+                          .name("http_success_request")
+                          .build()
+                ).build()),
+                false,
+                },
+            {
+                "minus-scalar",
+                of("http_success_request", SampleFamilyBuilder.newBuilder(
+                    Sample.builder()
+                          .labels(of("idc", "t1"))
+                          .value(1600592418480.0)
+                          .name("http_success_request")
+                          .build(),
+                    Sample.builder()
+                          .labels(of("idc", "t2"))
+                          .value(1600592418481.0)
+                          .name("http_success_request")
+                          .build()
+                ).build()),
+                "http_success_request.tagEqual('idc','t1') - 1000",
+                Result.success(SampleFamilyBuilder.newBuilder(
+                    Sample.builder()
+                          .labels(of("idc", "t1"))
+                          .value(1600592417480.0)
+                          .name("http_success_request")
+                          .build()
+                ).build()),
+                false,
+                },
+            {
+                "multiply-scalar",
+                of("http_success_request", SampleFamilyBuilder.newBuilder(
+                    Sample.builder()
+                          .labels(of("idc", "t1"))
+                          .value(1600592418480.0)
+                          .name("http_success_request")
+                          .build(),
+                    Sample.builder()
+                          .labels(of("idc", "t2"))
+                          .value(1600592418481.0)
+                          .name("http_success_request")
+                          .build()
+                ).build()),
+                "http_success_request.tagEqual('idc','t1') * 1000",
+                Result.success(SampleFamilyBuilder.newBuilder(
+                    Sample.builder()
+                          .labels(of("idc", "t1"))
+                          .value(1600592418480000.0)
+                          .name("http_success_request")
+                          .build()
+                ).build()),
+                false,
+                },
+            {
+                "divide-scalar",
+                of("http_success_request", SampleFamilyBuilder.newBuilder(
+                    Sample.builder()
+                          .labels(of("idc", "t1"))
+                          .value(1600592418480.0)
+                          .name("http_success_request")
+                          .build(),
+                    Sample.builder()
+                          .labels(of("idc", "t2"))
+                          .value(1600592418481.0)
+                          .name("http_success_request")
+                          .build()
+                ).build()),
+                "http_success_request.tagEqual('idc','t1') / 10",
+                Result.success(SampleFamilyBuilder.newBuilder(
+                    Sample.builder()
+                          .labels(of("idc", "t1"))
+                          .value(160059241848.0)
+                          .name("http_success_request")
+                          .build()
+                ).build()),
+                false,
+                },
+            {
+                "divide-zero",
+                of("http_success_request", SampleFamilyBuilder.newBuilder(
+                    Sample.builder()
+                          .labels(of("idc", "t1"))
+                          .value(1600592418480.0)
+                          .name("http_success_request")
+                          .build(),
+                    Sample.builder()
+                          .labels(of("idc", "t2"))
+                          .value(1600592418481.0)
+                          .name("http_success_request")
+                          .build()
+                ).build()),
+                "http_success_request.tagEqual('idc','t1') / 0",
+                Result.success(SampleFamilyBuilder.newBuilder(
+                    Sample.builder()
+                          .labels(of("idc", "t1"))
+                          .value(Double.POSITIVE_INFINITY)
+                          .name("http_success_request")
+                          .build()
+                ).build()),
+                false,
+                },
+            {
+                "empty-plus-empty",
+                of("http_success_request", SampleFamily.EMPTY,
+                   "http_error_request", SampleFamily.EMPTY
+                ),
+                "http_success_request + http_error_request",
+                Result.fail("Parsed result is an EMPTY sample family"),
+                false,
+                },
+            {
+                "empty-plus-sampleFamily",
+                of("http_success_request", SampleFamily.EMPTY,
+                   "http_error_request", SampleFamilyBuilder.newBuilder(
+                        Sample.builder().labels(of("idc", "t1")).value(50).name("http_error_request").build(),
+                        Sample.builder().labels(of("idc", "t2")).value(3).name("http_error_request").build()
+                    ).build()
+                ),
+                "http_success_request + http_error_request",
+                Result.success(SampleFamilyBuilder.newBuilder(
+                    Sample.builder().labels(of("idc", "t1")).value(50).name("http_error_request").build(),
+                    Sample.builder().labels(of("idc", "t2")).value(3).name("http_error_request").build()
+                ).build()),
+                false,
+                },
+            {
+                "sampleFamily-plus-empty",
+                of("http_success_request", SampleFamily.EMPTY,
+                   "http_error_request", SampleFamilyBuilder.newBuilder(
+                        Sample.builder().labels(of("idc", "t1")).value(50).name("http_error_request").build(),
+                        Sample.builder().labels(of("idc", "t2")).value(3).name("http_error_request").build()
+                    ).build()
+                ),
+                "http_error_request + http_success_request ",
+                Result.success(SampleFamilyBuilder.newBuilder(
+                    Sample.builder().labels(of("idc", "t1")).value(50).name("http_error_request").build(),
+                    Sample.builder().labels(of("idc", "t2")).value(3).name("http_error_request").build()
+                ).build()),
+                false,
+                },
+            {
+                "sampleFamily-plus-sampleFamily",
+                of("http_success_request", SampleFamilyBuilder.newBuilder(
+                    Sample.builder().labels(of("idc", "t1")).value(100).name("http_success_request").build(),
+                    Sample.builder().labels(of("idc", "t2")).value(30).name("http_success_request").build(),
+                    Sample.builder().labels(of("idc", "t3")).value(40).name("http_success_request").build(),
+                    Sample.builder().labels(of("region", "us")).value(80).name("http_success_request").build()
+                ).build(), "http_error_request", SampleFamilyBuilder.newBuilder(
+                    Sample.builder().labels(of("idc", "t1")).value(50).name("http_error_request").build(),
+                    Sample.builder().labels(of("idc", "t2")).value(3).name("http_error_request").build(),
+                    Sample.builder().labels(of("idc", "t5")).value(3).name("http_error_request").build(),
+                    Sample.builder().labels(of("tz", "en-US")).value(3).name("http_error_request").build()
+                ).build()),
+                "http_success_request + http_error_request",
+                Result.success(SampleFamilyBuilder.newBuilder(
+                    Sample.builder().labels(of("idc", "t1")).value(150).name("http_success_request").build(),
+                    Sample.builder().labels(of("idc", "t2")).value(33).name("http_success_request").build()
+                ).build()),
+                false,
+                },
+            {
+                "empty-minus-empty",
+                of("http_success_request", SampleFamily.EMPTY,
+                   "http_error_request", SampleFamily.EMPTY
+                ),
+                "http_success_request - http_error_request",
+                Result.fail("Parsed result is an EMPTY sample family"),
+                false,
+                },
+            {
+                "empty-minus-sampleFamily",
+                of("http_success_request", SampleFamily.EMPTY,
+                   "http_error_request", SampleFamilyBuilder.newBuilder(
+                        Sample.builder().labels(of("idc", "t1")).value(50).name("http_error_request").build(),
+                        Sample.builder().labels(of("idc", "t2")).value(3).name("http_error_request").build()
+                    ).build()
+                ),
+                "http_success_request - http_error_request",
+                Result.success(SampleFamilyBuilder.newBuilder(
+                    Sample.builder().labels(of("idc", "t1")).value(-50).name("http_error_request").build(),
+                    Sample.builder().labels(of("idc", "t2")).value(-3).name("http_error_request").build()
+                ).build()),
+                false,
+                },
+            {
+                "sampleFamily-minus-empty",
+                of("http_success_request", SampleFamily.EMPTY,
+                   "http_error_request", SampleFamilyBuilder.newBuilder(
+                        Sample.builder().labels(of("idc", "t1")).value(50).name("http_error_request").build(),
+                        Sample.builder().labels(of("idc", "t2")).value(3).name("http_error_request").build()
+                    ).build()
+                ),
+                "http_error_request - http_success_request ",
+                Result.success(SampleFamilyBuilder.newBuilder(
+                    Sample.builder().labels(of("idc", "t1")).value(50).name("http_error_request").build(),
+                    Sample.builder().labels(of("idc", "t2")).value(3).name("http_error_request").build()
+                ).build()),
+                false,
+                },
+            {
+                "sampleFamily-minus-sampleFamily",
+                of("http_success_request", SampleFamilyBuilder.newBuilder(
+                    Sample.builder().labels(of("idc", "t1")).value(100).name("http_success_request").build(),
+                    Sample.builder().labels(of("idc", "t2")).value(30).name("http_success_request").build(),
+                    Sample.builder().labels(of("idc", "t3")).value(40).name("http_success_request").build(),
+                    Sample.builder().labels(of("region", "us")).value(80).name("http_success_request").build()
+                ).build(), "http_error_request", SampleFamilyBuilder.newBuilder(
+                    Sample.builder().labels(of("idc", "t1")).value(50).name("http_error_request").build(),
+                    Sample.builder().labels(of("idc", "t2")).value(3).name("http_error_request").build(),
+                    Sample.builder().labels(of("idc", "t5")).value(3).name("http_error_request").build(),
+                    Sample.builder().labels(of("tz", "en-US")).value(3).name("http_error_request").build()
+                ).build()),
+                "http_success_request - http_error_request",
+                Result.success(SampleFamilyBuilder.newBuilder(
+                    Sample.builder().labels(of("idc", "t1")).value(50).name("http_success_request").build(),
+                    Sample.builder().labels(of("idc", "t2")).value(27).name("http_success_request").build()
+                ).build()),
+                false,
+                },
+            {
+                "sameSampleFamily-minus-sameSampleFamily",
+                of("http_success_request", SampleFamilyBuilder.newBuilder(
+                    Sample.builder()
+                          .labels(of("idc", "t1", "service", "service1"))
+                          .value(100)
+                          .name("http_success_request")
+                          .build(),
+                    Sample.builder()
+                          .labels(of("idc", "t2", "service", "service1"))
+                          .value(30)
+                          .name("http_success_request")
+                          .build(),
+                    Sample.builder()
+                          .labels(of("idc", "t3", "service", "service1"))
+                          .value(40)
+                          .name("http_success_request")
+                          .build(),
+                    Sample.builder()
+                          .labels(of("region", "us", "service", "service1"))
+                          .value(80)
+                          .name("http_success_request")
+                          .build()
+                ).build()),
+                "http_success_request.sum(['service']) - http_success_request.sum(['service'])",
+                Result.success(SampleFamilyBuilder.newBuilder(
+                    Sample.builder().labels(of("service", "service1")).value(0).name("http_success_request").build()
+                ).build()),
+                false,
+                },
+            {
+                "empty-multiple-empty",
+                of("http_success_request", SampleFamily.EMPTY,
+                   "http_error_request", SampleFamily.EMPTY
+                ),
+                "http_success_request * http_error_request",
+                Result.fail("Parsed result is an EMPTY sample family"),
+                false,
+                },
+            {
+                "empty-multiple-sampleFamily",
+                of("http_success_request", SampleFamily.EMPTY,
+                   "http_error_request", SampleFamilyBuilder.newBuilder(
+                        Sample.builder().labels(of("idc", "t1")).value(50).name("http_error_request").build(),
+                        Sample.builder().labels(of("idc", "t2")).value(3).name("http_error_request").build()
+                    ).build()
+                ),
+                "http_success_request * http_error_request",
+                Result.fail("Parsed result is an EMPTY sample family"),
+                false,
+                },
+            {
+                "sampleFamily-multiple-empty",
+                of("http_success_request", SampleFamily.EMPTY,
+                   "http_error_request", SampleFamilyBuilder.newBuilder(
+                        Sample.builder().labels(of("idc", "t1")).value(50).name("http_error_request").build(),
+                        Sample.builder().labels(of("idc", "t2")).value(3).name("http_error_request").build()
+                    ).build()
+                ),
+                "http_error_request * http_success_request ",
+                Result.fail("Parsed result is an EMPTY sample family"),
+                false,
+                },
+            {
+                "sampleFamily-multiple-sampleFamily",
+                of("http_success_request", SampleFamilyBuilder.newBuilder(
+                    Sample.builder().labels(of("idc", "t1")).value(100).name("http_success_request").build(),
+                    Sample.builder().labels(of("idc", "t2")).value(30).name("http_success_request").build(),
+                    Sample.builder().labels(of("idc", "t3")).value(40).name("http_success_request").build(),
+                    Sample.builder().labels(of("region", "us")).value(80).name("http_success_request").build()
+                ).build(), "http_error_request", SampleFamilyBuilder.newBuilder(
+                    Sample.builder().labels(of("idc", "t1")).value(50).name("http_error_request").build(),
+                    Sample.builder().labels(of("idc", "t2")).value(3).name("http_error_request").build(),
+                    Sample.builder().labels(of("idc", "t5")).value(3).name("http_error_request").build(),
+                    Sample.builder().labels(of("tz", "en-US")).value(3).name("http_error_request").build()
+                ).build()),
+                "http_success_request * http_error_request",
+                Result.success(SampleFamilyBuilder.newBuilder(
+                    Sample.builder().labels(of("idc", "t1")).value(5000).name("http_success_request").build(),
+                    Sample.builder().labels(of("idc", "t2")).value(90).name("http_success_request").build()
+                ).build()),
+                false,
+                },
+            {
+                "empty-divide-empty",
+                of("http_success_request", SampleFamily.EMPTY,
+                   "http_error_request", SampleFamily.EMPTY
+                ),
+                "http_success_request / http_error_request",
+                Result.fail("Parsed result is an EMPTY sample family"),
+                false,
+                },
+            {
+                "empty-divide-sampleFamily",
+                of("http_success_request", SampleFamily.EMPTY,
+                   "http_error_request", SampleFamilyBuilder.newBuilder(
+                        Sample.builder().labels(of("idc", "t1")).value(50).name("http_error_request").build(),
+                        Sample.builder().labels(of("idc", "t2")).value(3).name("http_error_request").build()
+                    ).build()
+                ),
+                "http_success_request / http_error_request",
+                Result.fail("Parsed result is an EMPTY sample family"),
+                false,
+                },
+            {
+                "sampleFamily-divide-empty",
+                of("http_success_request", SampleFamily.EMPTY,
+                   "http_error_request", SampleFamilyBuilder.newBuilder(
+                        Sample.builder().labels(of("idc", "t1")).value(50).name("http_error_request").build(),
+                        Sample.builder().labels(of("idc", "t2")).value(3).name("http_error_request").build()
+                    ).build()
+                ),
+                "http_error_request / http_success_request ",
+                Result.success(SampleFamilyBuilder.newBuilder(
+                    Sample.builder()
+                          .labels(of("idc", "t1"))
+                          .value(Double.POSITIVE_INFINITY)
+                          .name("http_error_request")
+                          .build(),
+                    Sample.builder()
+                          .labels(of("idc", "t2"))
+                          .value(Double.POSITIVE_INFINITY)
+                          .name("http_error_request")
+                          .build()
+                ).build()),
+                false,
+                },
+            {
+                "sampleFamily-divide-sampleFamily",
+                of("http_success_request", SampleFamilyBuilder.newBuilder(
+                    Sample.builder().labels(of("idc", "t1")).value(100).name("http_success_request").build(),
+                    Sample.builder().labels(of("idc", "t2")).value(30).name("http_success_request").build(),
+                    Sample.builder().labels(of("idc", "t3")).value(40).name("http_success_request").build(),
+                    Sample.builder().labels(of("region", "us")).value(80).name("http_success_request").build()
+                ).build(), "http_error_request", SampleFamilyBuilder.newBuilder(
+                    Sample.builder().labels(of("idc", "t1")).value(50).name("http_error_request").build(),
+                    Sample.builder().labels(of("idc", "t2")).value(3).name("http_error_request").build(),
+                    Sample.builder().labels(of("idc", "t5")).value(3).name("http_error_request").build(),
+                    Sample.builder().labels(of("tz", "en-US")).value(3).name("http_error_request").build()
+                ).build()),
+                "http_success_request / http_error_request",
+                Result.success(SampleFamilyBuilder.newBuilder(
+                    Sample.builder().labels(of("idc", "t1")).value(2).name("http_success_request").build(),
+                    Sample.builder().labels(of("idc", "t2")).value(10).name("http_success_request").build()
+                ).build()),
+                false,
+                },
+            });
+    }
+
+    @ParameterizedTest(name = "{0}")
+    @MethodSource("data")
+    public void test(String name,
+                     ImmutableMap<String, SampleFamily> input,
+                     String expression,
+                     Result want,
+                     boolean isThrow) {
+        Result r = null;
+        try {
+            // v2 compiles eagerly, so parse-time failures surface here too (v1 surfaced them at run()).
+            Expression e = DSL.parse(name, expression);
+            r = e.run(input);
+        } catch (Throwable t) {
+            if (isThrow) {
+                return;
+            }
+            log.error("Test failed", t);
+            fail("Should not throw anything");
+        }
+        if (isThrow) {
+            fail("Should throw something");
+        }
+        assertEquals(want, r);
+    }
+}
diff --git a/oap-server/analyzer/meter-analyzer/src/test/java/org/apache/skywalking/oap/meter/analyzer/v2/dsl/BasicTest.java b/oap-server/analyzer/meter-analyzer/src/test/java/org/apache/skywalking/oap/meter/analyzer/v2/dsl/BasicTest.java
new file mode 100644
index 0000000000..1f5e2ab7c4
--- /dev/null
+++ b/oap-server/analyzer/meter-analyzer/src/test/java/org/apache/skywalking/oap/meter/analyzer/v2/dsl/BasicTest.java
@@ -0,0 +1,78 @@
+/*
+ * 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.
+ *
+ */
+
+package org.apache.skywalking.oap.meter.analyzer.v2.dsl;
+
+import com.google.common.collect.ImmutableMap;
+import lombok.extern.slf4j.Slf4j;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import java.util.Arrays;
+import java.util.Collection;
+
+import static com.google.common.collect.ImmutableMap.of;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.fail;
+
+@Slf4j
+public class BasicTest {
+    public static Collection<Object[]> data() {
+        return Arrays.asList(new Object[][] {
+            {
+                "default",
+                of("instance_cpu_percentage", SampleFamily.EMPTY),
+                "instance_cpu_percentage",
+                Result.fail("Parsed result is an EMPTY sample family"),
+                false,
+            },
+            {
+                "single-value",
+                of("instance_cpu_percentage", SampleFamilyBuilder.newBuilder(Sample.builder().value(1600592418480.0).name("instance_cpu_percentage").build()).build()),
+                "instance_cpu_percentage",
+                Result.success(SampleFamilyBuilder.newBuilder(Sample.builder().value(1600592418480.0).name("instance_cpu_percentage").build()).build()),
+                false,
+            },
+        });
+    }
+
+    @ParameterizedTest(name = "{0}")
+    @MethodSource("data")
+    public void test(String name,
+                     ImmutableMap<String, SampleFamily> input,
+                     String expression,
+                     Result want,
+                     boolean isThrow) {
+        Result r = null;
+        try {
+            // v2 compiles eagerly, so parse-time failures surface here too (v1 surfaced them at run()).
+            Expression e = DSL.parse(name, expression);
+            r = e.run(input);
+        } catch (Throwable t) {
+            if (isThrow) {
+                return;
+            }
+            log.error("Test failed", t);
+            fail("Should not throw anything");
+        }
+        if (isThrow) {
+            fail("Should throw something");
+        }
+        assertEquals(want, r);
+    }
+}
diff --git a/oap-server/analyzer/meter-analyzer/src/test/java/org/apache/skywalking/oap/meter/analyzer/v2/dsl/DecorateTest.java b/oap-server/analyzer/meter-analyzer/src/test/java/org/apache/skywalking/oap/meter/analyzer/v2/dsl/DecorateTest.java
new file mode 100644
index 0000000000..d6eec9b843
--- /dev/null
+++ b/oap-server/analyzer/meter-analyzer/src/test/java/org/apache/skywalking/oap/meter/analyzer/v2/dsl/DecorateTest.java
@@ -0,0 +1,64 @@
+/*
+ * 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.
+ *
+ */
+
+package org.apache.skywalking.oap.meter.analyzer.v2.dsl;
+
+import com.google.common.collect.ImmutableMap;
+import org.apache.skywalking.oap.server.core.analysis.meter.MeterEntity;
+import org.apache.skywalking.oap.server.core.config.NamingControl;
+import org.apache.skywalking.oap.server.core.config.group.EndpointNameGrouping;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import static com.google.common.collect.ImmutableMap.of;
+
+public class DecorateTest {
+    @BeforeAll
+    public static void setup() {
+        MeterEntity.setNamingControl(
+            new NamingControl(512, 512, 512, new EndpointNameGrouping()));
+    }
+
+    @AfterAll
+    public static void tearDown() {
+        MeterEntity.setNamingControl(null);
+    }
+
+    @Test
+    public void testDecorate() {
+        ImmutableMap<String, SampleFamily> input = ImmutableMap.of(
+            "http_success_request",
+            SampleFamilyBuilder.newBuilder(
+                Sample.builder().labels(of("idc", "t1")).value(50).name("http_success_request").build(),
+                Sample.builder()
+                      .labels(of("idc", "t1", "region", "us", "instance", "10.0.0.1"))
+                      .value(100)
+                      .name("http_success_request")
+                      .build()
+            ).build()
+        );
+        String expression = "http_success_request.sum(['idc']).service(['idc'], Layer.GENERAL).decorate({ me -> me.attr0 = me.layer.name()})";
+        Expression e = DSL.parse("decorate", expression);
+        Result r = e.run(input);
+        Assertions.assertTrue(r.isSuccess());
+        Assertions.assertEquals(
+            "GENERAL", r.getData().context.getMeterSamples().keySet().stream().findFirst().get().getAttr0());
+    }
+}
diff --git a/oap-server/analyzer/meter-analyzer/src/test/java/org/apache/skywalking/oap/meter/analyzer/v2/dsl/FilterTest.java b/oap-server/analyzer/meter-analyzer/src/test/java/org/apache/skywalking/oap/meter/analyzer/v2/dsl/FilterTest.java
new file mode 100644
index 0000000000..476f82b9b5
--- /dev/null
+++ b/oap-server/analyzer/meter-analyzer/src/test/java/org/apache/skywalking/oap/meter/analyzer/v2/dsl/FilterTest.java
@@ -0,0 +1,99 @@
+/*
+ * 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.
+ *
+ */
+
+package org.apache.skywalking.oap.meter.analyzer.v2.dsl;
+
+import com.google.common.collect.ImmutableMap;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Map;
+import lombok.extern.slf4j.Slf4j;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import static com.google.common.collect.ImmutableMap.of;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * v1 filtered inline as a chain method: {@code metric.filter({tags -> ...})}. v2 has no inline
+ * {@code .filter()} chain method; a MAL filter closure is compiled on its own via {@link
+ * FilterExpression} (the rule's {@code filter:} field) into a {@link MalFilter} and applied to the
+ * whole sample-family map. This test drives the v2 {@link FilterExpression} with the same closures
+ * and inputs. {@code FilterExpression.filter} filters samples within each family and drops any
+ * family that becomes EMPTY, so expected values are maps rather than a single {@code Result}.
+ */
+@Slf4j
+public class FilterTest {
+    public static Collection<Object[]> data() {
+        final SampleFamily sf =
+            SampleFamilyBuilder.newBuilder(
+                                   Sample.builder()
+                                         .value(1600592418480.0)
+                                         .labels(ImmutableMap.of("str", "val1"))
+                                         .name("instance_cpu_percentage")
+                                         .build(),
+                                   Sample.builder()
+                                         .value(1600592418480.0)
+                                         .labels(ImmutableMap.of("str", "val2"))
+                                         .name("instance_cpu_percentage")
+                                         .build())
+                               .build();
+        return Arrays.asList(new Object[][]{
+            {
+                "filter-string",
+                of("instance_cpu_percentage", sf),
+                "{ tags -> tags.str == 'val1' }",
+                of("instance_cpu_percentage", SampleFamily.build(sf.context, sf.samples[0])),
+            },
+            {
+                "filter-equal-val2",
+                of("instance_cpu_percentage", sf),
+                "{ tags -> tags.str == 'val2' }",
+                of("instance_cpu_percentage", SampleFamily.build(sf.context, sf.samples[1])),
+            },
+            {
+                "filter-not-equal",
+                of("instance_cpu_percentage", sf),
+                "{ tags -> tags.str != 'val1' }",
+                of("instance_cpu_percentage", SampleFamily.build(sf.context, sf.samples[1])),
+            },
+            {
+                "filter-in-single",
+                of("instance_cpu_percentage", sf),
+                "{ tags -> tags.str in [ 'val2' ] }",
+                of("instance_cpu_percentage", SampleFamily.build(sf.context, sf.samples[1])),
+            },
+            {
+                "filter-in-multiple",
+                of("instance_cpu_percentage", sf),
+                "{ tags -> tags.str in [ 'val1', 'val2' ] }",
+                of("instance_cpu_percentage", sf),
+            },
+        });
+    }
+
+    @ParameterizedTest(name = "{0}")
+    @MethodSource("data")
+    public void test(String name,
+                     Map<String, SampleFamily> input,
+                     String filterLiteral,
+                     Map<String, SampleFamily> want) {
+        Map<String, SampleFamily> r = new FilterExpression(filterLiteral).filter(input);
+        assertEquals(want, r);
+    }
+}
diff --git a/oap-server/analyzer/meter-analyzer/src/test/java/org/apache/skywalking/oap/meter/analyzer/v2/dsl/FunctionTest.java b/oap-server/analyzer/meter-analyzer/src/test/java/org/apache/skywalking/oap/meter/analyzer/v2/dsl/FunctionTest.java
new file mode 100644
index 0000000000..65eb830d14
--- /dev/null
+++ b/oap-server/analyzer/meter-analyzer/src/test/java/org/apache/skywalking/oap/meter/analyzer/v2/dsl/FunctionTest.java
@@ -0,0 +1,145 @@
+/*
+ * 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.
+ *
+ */
+
+package org.apache.skywalking.oap.meter.analyzer.v2.dsl;
+
+import com.google.common.collect.ImmutableMap;
+import java.util.Arrays;
+import java.util.Collection;
+import lombok.extern.slf4j.Slf4j;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import static com.google.common.collect.ImmutableMap.of;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.fail;
+
+@Slf4j
+public class FunctionTest {
+    public static Collection<Object[]> data() {
+        return Arrays.asList(new Object[][] {
+            {
+                // v1 (interpreted Groovy) accepted a bare closure returning a map literal as a full
+                // tag override. v2's MAL grammar only accepts the arrow form ({tags -> ...}), so this
+                // exact v1 syntax is rejected at parse time. Kept as a negative case to pin that change;
+                // no production MAL rule uses the bare-map form.
+                "tag-override",
+                of("http_success_request", SampleFamilyBuilder.newBuilder(Sample.builder().labels(of("region", "us")).name("http_success_request").build()).build()),
+                "http_success_request.tag({ ['svc':'product', 'instance':'10.0.0.1'] })",
+                Result.success(SampleFamilyBuilder.newBuilder(Sample.builder().labels(of("svc", "product", "instance", "10.0.0.1")).name("http_success_request").build()).build()),
+                true,
+            },
+            {
+                "tag-add",
+                of("http_success_request", SampleFamilyBuilder.newBuilder(Sample.builder().labels(of("region", "us")).name("http_success_request").build()).build()),
+                "http_success_request.tag({tags -> tags.az = 'az1' })",
+                Result.success(SampleFamilyBuilder.newBuilder(Sample.builder().labels(of("region", "us", "az", "az1")).name("http_success_request").build()).build()),
+                false,
+            },
+            {
+                "tag-remove",
+                of("http_success_request", SampleFamilyBuilder.newBuilder(Sample.builder().labels(of("region", "us")).name("http_success_request").build()).build()),
+                "http_success_request.tag({tags -> tags.remove('region') })",
+                Result.success(SampleFamilyBuilder.newBuilder(Sample.builder().labels(ImmutableMap.of()).name("http_success_request").build()).build()),
+                false,
+            },
+            {
+                "tag-update",
+                of("http_success_request", SampleFamilyBuilder.newBuilder(Sample.builder().labels(of("region", "us")).name("http_success_request").build()).build()),
+                "http_success_request.tag({tags -> if (tags['region'] == 'us') {tags.region = 'zh'} })",
+                Result.success(SampleFamilyBuilder.newBuilder(Sample.builder().labels(of("region", "zh")).name("http_success_request").build()).build()),
+                false,
+            },
+            {
+                "tag-append",
+                of("http_success_request", SampleFamilyBuilder.newBuilder(Sample.builder().labels(of("region", "us")).name("http_success_request").build()).build()),
+                "http_success_request.tag({tags -> tags.region = 'prefix::' + tags.region})",
+                Result.success(SampleFamilyBuilder.newBuilder(Sample.builder().labels(of("region", "prefix::us")).name("http_success_request").build()).build()),
+                false,
+                },
+            {
+                "histogram",
+                of("http_success_request", SampleFamilyBuilder.newBuilder(
+                    Sample.builder().labels(of("le", "0.025")).value(100).name("http_success_request").build(),
+                    Sample.builder().labels(of("le", "0.75")).value(12).name("http_success_request").build(),
+                    Sample.builder().labels(of("le", "1.25")).value(36).name("http_success_request").build()).build()
+                ),
+                "http_success_request.histogram()",
+                Result.success(SampleFamilyBuilder.newBuilder(
+                    Sample.builder().labels(of("le", "25")).value(100).name("http_success_request").build(),
+                    Sample.builder().labels(of("le", "750")).value(12).name("http_success_request").build(),
+                    Sample.builder().labels(of("le", "1250")).value(36).name("http_success_request").build()).build()
+                ),
+                false,
+            },
+            {
+                "histogram_percentile",
+                of("http_success_request", SampleFamilyBuilder.newBuilder(
+                    Sample.builder().labels(of("le", "0.025")).value(100).name("http_success_request").build(),
+                    Sample.builder().labels(of("le", "0.75")).value(22).name("http_success_request").build(),
+                    Sample.builder().labels(of("le", "1.25")).value(30).name("http_success_request").build()).build()
+                ),
+                "http_success_request.histogram().histogram_percentile([75,99])",
+                Result.success(SampleFamilyBuilder.newBuilder(
+                    Sample.builder().labels(of("le", "25")).value(100).name("http_success_request").build(),
+                    Sample.builder().labels(of("le", "750")).value(22).name("http_success_request").build(),
+                    Sample.builder().labels(of("le", "1250")).value(30).name("http_success_request").build()).build()
+                ),
+                false,
+            },
+            {
+                "for-each",
+                of("http_success_request", SampleFamilyBuilder.newBuilder(
+                    Sample.builder().labels(of("region", "us")).name("http_success_request").build(),
+                    Sample.builder().labels(of("region", "cn")).name("http_success_request").build()
+                ).build()),
+                "http_success_request.forEach(['v1', 'v2'], {element, tags -> tags[element] = 'test'})",
+                Result.success(SampleFamilyBuilder.newBuilder(
+                    Sample.builder().labels(of("region", "us", "v1", "test", "v2", "test")).name("http_success_request").build(),
+                    Sample.builder().labels(of("region", "cn", "v1", "test", "v2", "test")).name("http_success_request").build()
+                ).build()),
+                false,
+            },
+        });
+    }
+
+    @ParameterizedTest(name = "{0}")
+    @MethodSource("data")
+    public void test(String name,
+                     ImmutableMap<String, SampleFamily> input,
+                     String expression,
+                     Result want,
+                     boolean isThrow) {
+        Result r = null;
+        try {
+            // v2 compiles eagerly, so parse-time failures surface here too (v1 surfaced them at run()).
+            Expression e = DSL.parse(name, expression);
+            r = e.run(input);
+        } catch (Throwable t) {
+            if (isThrow) {
+                return;
+            }
+            log.error("Test failed", t);
+            fail("Should not throw anything");
+        }
+        if (isThrow) {
+            fail("Should throw something");
+        }
+        assertEquals(want, r);
+    }
+}
\ No newline at end of file
diff --git a/oap-server/analyzer/meter-analyzer/src/test/java/org/apache/skywalking/oap/meter/analyzer/v2/dsl/IncreaseTest.java b/oap-server/analyzer/meter-analyzer/src/test/java/org/apache/skywalking/oap/meter/analyzer/v2/dsl/IncreaseTest.java
new file mode 100644
index 0000000000..86e77b6b9c
--- /dev/null
+++ b/oap-server/analyzer/meter-analyzer/src/test/java/org/apache/skywalking/oap/meter/analyzer/v2/dsl/IncreaseTest.java
@@ -0,0 +1,240 @@
+/*
+ * 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.
+ *
+ */
+
+package org.apache.skywalking.oap.meter.analyzer.v2.dsl;
+
+import com.google.common.collect.ImmutableMap;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.skywalking.oap.meter.analyzer.v2.dsl.counter.CounterWindow;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import java.util.Collection;
+import java.util.List;
+
+import static com.google.common.collect.ImmutableMap.of;
+import static java.time.Instant.parse;
+import static java.util.Arrays.asList;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.fail;
+
+@Slf4j
+public class IncreaseTest {
+    public static Collection<Object[]> data() {
+        return asList(new Object[][] {
+            {
+                "increase",
+                asList(
+                    of("http_success_request", SampleFamilyBuilder.newBuilder(
+                        Sample.builder().name("http_success_request").labels(of("svc", "product"))
+                              .timestamp(parse("2020-09-11T11:11:01.00Z").toEpochMilli()).value(50).name("http_success_request").build(),
+                        Sample.builder().name("http_success_request").labels(of("svc", "catalog"))
+                              .timestamp(parse("2020-09-11T11:11:01.00Z").toEpochMilli()).value(150).name("http_success_request").build()
+                    ).build()),
+                    of("http_success_request", SampleFamilyBuilder.newBuilder(
+                        Sample.builder().name("http_success_request").labels(of("svc", "product"))
+                              .timestamp(parse("2020-09-11T11:13:41.00Z").toEpochMilli()).value(330).name("http_success_request").build(),
+                        Sample.builder().name("http_success_request").labels(of("svc", "catalog"))
+                              .timestamp(parse("2020-09-11T11:13:41.00Z").toEpochMilli()).value(500).name("http_success_request").build()
+                    ).build()),
+                    of("http_success_request", SampleFamilyBuilder.newBuilder(
+                        Sample.builder().name("http_success_request").labels(of("svc", "product"))
+                              .timestamp(parse("2020-09-11T11:16:31.00Z").toEpochMilli()).value(380).name("http_success_request").build(),
+                        Sample.builder().name("http_success_request").labels(of("svc", "catalog"))
+                              .timestamp(parse("2020-09-11T11:16:31.00Z").toEpochMilli()).value(810).name("http_success_request").build()
+                    ).build()),
+                    of("http_success_request", SampleFamilyBuilder.newBuilder(
+                        Sample.builder().name("http_success_request").labels(of("svc", "product"))
+                              .timestamp(parse("2020-09-11T11:19:31.02Z").toEpochMilli()).value(1380).name("http_success_request").build(),
+                        Sample.builder().name("http_success_request").labels(of("svc", "catalog"))
+                              .timestamp(parse("2020-09-11T11:19:31.02Z").toEpochMilli()).value(1900).name("http_success_request").build()
+                    ).build())
+                ),
+                "http_success_request.increase('PT5M')",
+                asList(
+                    Result.success(SampleFamilyBuilder.newBuilder(
+                        Sample.builder().name("http_success_request").labels(of("svc", "product"))
+                            .timestamp(parse("2020-09-11T11:11:01.00Z").toEpochMilli()).value(0).name("http_success_request").build(),
+                        Sample.builder().name("http_success_request").labels(of("svc", "catalog"))
+                            .timestamp(parse("2020-09-11T11:11:01.00Z").toEpochMilli()).value(0).name("http_success_request").build()
+                    ).build()),
+                    Result.success(SampleFamilyBuilder.newBuilder(
+                        Sample.builder().name("http_success_request").labels(of("svc", "product"))
+                            .timestamp(parse("2020-09-11T11:13:41.00Z").toEpochMilli()).value(280).name("http_success_request").build(),
+                        Sample.builder().name("http_success_request").labels(of("svc", "catalog"))
+                            .timestamp(parse("2020-09-11T11:13:41.00Z").toEpochMilli()).value(350).name("http_success_request").build()
+                    ).build()),
+                    Result.success(SampleFamilyBuilder.newBuilder(
+                        Sample.builder().name("http_success_request").labels(of("svc", "product"))
+                            .timestamp(parse("2020-09-11T11:16:31.00Z").toEpochMilli()).value(330).name("http_success_request").build(),
+                        Sample.builder().name("http_success_request").labels(of("svc", "catalog"))
+                            .timestamp(parse("2020-09-11T11:16:31.00Z").toEpochMilli()).value(660).name("http_success_request").build()
+                    ).build()),
+                    Result.success(SampleFamilyBuilder.newBuilder(
+                        Sample.builder().name("http_success_request").labels(of("svc", "product"))
+                            .timestamp(parse("2020-09-11T11:19:31.02Z").toEpochMilli()).value(1050).name("http_success_request").build(),
+                        Sample.builder().name("http_success_request").labels(of("svc", "catalog"))
+                            .timestamp(parse("2020-09-11T11:19:31.02Z").toEpochMilli()).value(1400).name("http_success_request").build()
+                    ).build())
+                ),
+                false,
+            },
+            {
+                "rate",
+                asList(
+                    of("http_success_request", SampleFamilyBuilder.newBuilder(
+                        Sample.builder().name("http_success_request").labels(of("svc", "product"))
+                            .timestamp(parse("2020-09-11T11:11:01.00Z").toEpochMilli()).value(50).name("http_success_request").build(),
+                        Sample.builder().name("http_success_request").labels(of("svc", "catalog"))
+                            .timestamp(parse("2020-09-11T11:11:01.00Z").toEpochMilli()).value(150).name("http_success_request").build()
+                    ).build()),
+                    of("http_success_request", SampleFamilyBuilder.newBuilder(
+                        Sample.builder().name("http_success_request").labels(of("svc", "product"))
+                            .timestamp(parse("2020-09-11T11:13:41.00Z").toEpochMilli()).value(330).name("http_success_request").build(),
+                        Sample.builder().name("http_success_request").labels(of("svc", "catalog"))
+                            .timestamp(parse("2020-09-11T11:13:41.00Z").toEpochMilli()).value(500).name("http_success_request").build()
+                    ).build()),
+                    of("http_success_request", SampleFamilyBuilder.newBuilder(
+                        Sample.builder().name("http_success_request").labels(of("svc", "product"))
+                            .timestamp(parse("2020-09-11T11:16:31.00Z").toEpochMilli()).value(380).name("http_success_request").build(),
+                        Sample.builder().name("http_success_request").labels(of("svc", "catalog"))
+                            .timestamp(parse("2020-09-11T11:16:31.00Z").toEpochMilli()).value(810).name("http_success_request").build()
+                    ).build()),
+                    of("http_success_request", SampleFamilyBuilder.newBuilder(
+                        Sample.builder().name("http_success_request").labels(of("svc", "product"))
+                            .timestamp(parse("2020-09-11T11:19:31.02Z").toEpochMilli()).value(1380).name("http_success_request").build(),
+                        Sample.builder().name("http_success_request").labels(of("svc", "catalog"))
+                            .timestamp(parse("2020-09-11T11:19:31.02Z").toEpochMilli()).value(1900).name("http_success_request").build()
+                    ).build())
+                ),
+                "http_success_request.rate('PT5M')",
+                asList(
+                    Result.success(SampleFamilyBuilder.newBuilder(
+                        Sample.builder().name("http_success_request").labels(of("svc", "product"))
+                            .timestamp(parse("2020-09-11T11:11:01.00Z").toEpochMilli()).value(0).name("http_success_request").build(),
+                        Sample.builder().name("http_success_request").labels(of("svc", "catalog"))
+                            .timestamp(parse("2020-09-11T11:11:01.00Z").toEpochMilli()).value(0).name("http_success_request").build()
+                    ).build()),
+                    Result.success(SampleFamilyBuilder.newBuilder(
+                        Sample.builder().name("http_success_request").labels(of("svc", "product"))
+                            .timestamp(parse("2020-09-11T11:13:41.00Z").toEpochMilli()).value(1.75D).name("http_success_request").build(),
+                        Sample.builder().name("http_success_request").labels(of("svc", "catalog"))
+                            .timestamp(parse("2020-09-11T11:13:41.00Z").toEpochMilli()).value(2.1875D).name("http_success_request").build()
+                    ).build()),
+                    Result.success(SampleFamilyBuilder.newBuilder(
+                        Sample.builder().name("http_success_request").labels(of("svc", "product"))
+                            .timestamp(parse("2020-09-11T11:16:31.00Z").toEpochMilli()).value(1D).name("http_success_request").build(),
+                        Sample.builder().name("http_success_request").labels(of("svc", "catalog"))
+                            .timestamp(parse("2020-09-11T11:16:31.00Z").toEpochMilli()).value(2D).name("http_success_request").build()
+                    ).build()),
+                    Result.success(SampleFamilyBuilder.newBuilder(
+                        Sample.builder().name("http_success_request").labels(of("svc", "product"))
+                            .timestamp(parse("2020-09-11T11:19:31.02Z").toEpochMilli()).value(3D).name("http_success_request").build(),
+                        Sample.builder().name("http_success_request").labels(of("svc", "catalog"))
+                            .timestamp(parse("2020-09-11T11:19:31.02Z").toEpochMilli()).value(4D).name("http_success_request").build()
+                    ).build())
+                ),
+                false,
+            },
+            {
+                "irate",
+                asList(
+                    of("http_success_request", SampleFamilyBuilder.newBuilder(
+                        Sample.builder().name("http_success_request").labels(of("svc", "product"))
+                            .timestamp(parse("2020-09-11T11:11:01.00Z").toEpochMilli()).value(50).name("http_success_request").build(),
+                        Sample.builder().name("http_success_request").labels(of("svc", "catalog"))
+                            .timestamp(parse("2020-09-11T11:11:01.00Z").toEpochMilli()).value(150).name("http_success_request").build()
+                    ).build()),
+                    of("http_success_request", SampleFamilyBuilder.newBuilder(
+                        Sample.builder().name("http_success_request").labels(of("svc", "product"))
+                            .timestamp(parse("2020-09-11T11:13:41.00Z").toEpochMilli()).value(330).name("http_success_request").build(),
+                        Sample.builder().name("http_success_request").labels(of("svc", "catalog"))
+                            .timestamp(parse("2020-09-11T11:13:41.00Z").toEpochMilli()).value(500).name("http_success_request").build()
+                    ).build()),
+                    of("http_success_request", SampleFamilyBuilder.newBuilder(
+                        Sample.builder().name("http_success_request").labels(of("svc", "product"))
+                            .timestamp(parse("2020-09-11T11:16:31.00Z").toEpochMilli()).value(500).name("http_success_request").build(),
+                        Sample.builder().name("http_success_request").labels(of("svc", "catalog"))
+                            .timestamp(parse("2020-09-11T11:16:31.00Z").toEpochMilli()).value(840).name("http_success_request").build()
+                    ).build()),
+                    of("http_success_request", SampleFamilyBuilder.newBuilder(
+                        Sample.builder().name("http_success_request").labels(of("svc", "product"))
+                            .timestamp(parse("2020-09-11T11:19:31.02Z").toEpochMilli()).value(1040).name("http_success_request").build(),
+                        Sample.builder().name("http_success_request").labels(of("svc", "catalog"))
+                            .timestamp(parse("2020-09-11T11:19:31.02Z").toEpochMilli()).value(1560).name("http_success_request").build()
+                    ).build())
+                ),
+                "http_success_request.irate()",
+                asList(
+                    Result.success(SampleFamilyBuilder.newBuilder(
+                        Sample.builder().name("http_success_request").labels(of("svc", "product"))
+                            .timestamp(parse("2020-09-11T11:11:01.00Z").toEpochMilli()).value(0).name("http_success_request").build(),
+                        Sample.builder().name("http_success_request").labels(of("svc", "catalog"))
+                            .timestamp(parse("2020-09-11T11:11:01.00Z").toEpochMilli()).value(0).name("http_success_request").build()
+                    ).build()),
+                    Result.success(SampleFamilyBuilder.newBuilder(
+                        Sample.builder().name("http_success_request").labels(of("svc", "product"))
+                            .timestamp(parse("2020-09-11T11:13:41.00Z").toEpochMilli()).value(1.75D).name("http_success_request").build(),
+                        Sample.builder().name("http_success_request").labels(of("svc", "catalog"))
+                            .timestamp(parse("2020-09-11T11:13:41.00Z").toEpochMilli()).value(2.1875D).name("http_success_request").build()
+                    ).build()),
+                    Result.success(SampleFamilyBuilder.newBuilder(
+                        Sample.builder().name("http_success_request").labels(of("svc", "product"))
+                            .timestamp(parse("2020-09-11T11:16:31.00Z").toEpochMilli()).value(1D).name("http_success_request").build(),
+                        Sample.builder().name("http_success_request").labels(of("svc", "catalog"))
+                            .timestamp(parse("2020-09-11T11:16:31.00Z").toEpochMilli()).value(2D).name("http_success_request").build()
+                    ).build()),
+                    Result.success(SampleFamilyBuilder.newBuilder(
+                        Sample.builder().name("http_success_request").labels(of("svc", "product"))
+                            .timestamp(parse("2020-09-11T11:19:31.02Z").toEpochMilli()).value(3D).name("http_success_request").build(),
+                        Sample.builder().name("http_success_request").labels(of("svc", "catalog"))
+                            .timestamp(parse("2020-09-11T11:19:31.02Z").toEpochMilli()).value(4D).name("http_success_request").build()
+                    ).build())
+                ),
+                false,
+            },
+        });
+    }
+
+    @ParameterizedTest(name = "{0}")
+    @MethodSource("data")
+    public void test(String name,
+                     List<ImmutableMap<String, SampleFamily>> input,
+                     String expression,
+                     List<Result> want,
+                     boolean isThrow) {
+        Expression e = DSL.parse(name, expression);
+        CounterWindow.INSTANCE.reset();
+        for (int i = 0; i < input.size(); i++) {
+            Result r = null;
+            try {
+                r = e.run(input.get(i));
+            } catch (Throwable t) {
+                if (isThrow) {
+                    return;
+                }
+                log.error("Test failed", t);
+                fail("Should not throw anything");
+            }
+            if (isThrow) {
+                fail("Should throw something");
+            }
+            assertEquals(want.get(i), r);
+        }
+    }
+}
diff --git a/oap-server/analyzer/meter-analyzer/src/test/java/org/apache/skywalking/oap/meter/analyzer/v2/dsl/K8sTagTest.java b/oap-server/analyzer/meter-analyzer/src/test/java/org/apache/skywalking/oap/meter/analyzer/v2/dsl/K8sTagTest.java
new file mode 100644
index 0000000000..e2726e624f
--- /dev/null
+++ b/oap-server/analyzer/meter-analyzer/src/test/java/org/apache/skywalking/oap/meter/analyzer/v2/dsl/K8sTagTest.java
@@ -0,0 +1,340 @@
+/*
+ * 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.
+ *
+ */
+
+package org.apache.skywalking.oap.meter.analyzer.v2.dsl;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import io.fabric8.kubernetes.api.model.LoadBalancerIngress;
+import io.fabric8.kubernetes.api.model.LoadBalancerStatus;
+import io.fabric8.kubernetes.api.model.ObjectMeta;
+import io.fabric8.kubernetes.api.model.Pod;
+import io.fabric8.kubernetes.api.model.PodStatus;
+import io.fabric8.kubernetes.api.model.Service;
+import io.fabric8.kubernetes.api.model.ServiceSpec;
+import io.fabric8.kubernetes.api.model.ServiceStatus;
+import lombok.SneakyThrows;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.skywalking.library.kubernetes.KubernetesPods;
+import org.apache.skywalking.library.kubernetes.KubernetesServices;
+import org.apache.skywalking.library.kubernetes.ObjectID;
+import org.apache.skywalking.oap.meter.analyzer.v2.dsl.tagOpt.Retag;
+import org.apache.skywalking.oap.server.core.analysis.IDManager;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.mockito.Mockito;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.mockito.junit.jupiter.MockitoSettings;
+import org.mockito.quality.Strictness;
+import org.apache.skywalking.oap.server.testing.util.ReflectUtil;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Map;
+import java.util.Optional;
+
+import static com.google.common.collect.ImmutableMap.of;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.fail;
+import static org.mockito.Mockito.when;
+
+@Slf4j
+@ExtendWith(MockitoExtension.class)
+@MockitoSettings(strictness = Strictness.WARN)
+public class K8sTagTest {
+    public static Collection<Object[]> data() {
+        return Arrays.asList(new Object[][] {
+            {
+                "Pod2Service",
+                of("container_cpu_usage_seconds_total", SampleFamilyBuilder.newBuilder(
+                    Sample.builder()
+                          .labels(
+                              of(
+                                  "namespace", "default", "container", "my-nginx", "cpu", "total", "pod",
+                                  "my-nginx-5dc4865748-mbczh"
+                              ))
+                          .value(2)
+                          .name("container_cpu_usage_seconds_total")
+                          .build(),
+                    Sample.builder()
+                          .labels(
+                              of(
+                                  "namespace", "kube-system", "container", "kube-state-metrics", "cpu", "total", "pod",
+                                  "kube-state-metrics-6f979fd498-z7xwx"
+                              ))
+                          .value(1)
+                          .name("container_cpu_usage_seconds_total")
+                          .build()
+                ).build()),
+                "container_cpu_usage_seconds_total.retagByK8sMeta('service' , K8sRetagType.Pod2Service , 'pod' , 'namespace')",
+                Result.success(SampleFamilyBuilder.newBuilder(
+                    Sample.builder()
+                          .labels(
+                              of(
+                                  "namespace", "default", "container", "my-nginx", "cpu", "total", "pod",
+                                  "my-nginx-5dc4865748-mbczh",
+                                  "service", "nginx-service.default"
+                              ))
+                          .value(2)
+                          .name("container_cpu_usage_seconds_total")
+                          .build(),
+                    Sample.builder()
+                          .labels(
+                              of(
+                                  "namespace", "kube-system", "container", "kube-state-metrics", "cpu", "total", "pod",
+                                  "kube-state-metrics-6f979fd498-z7xwx",
+                                  "service", "kube-state-metrics.kube-system"
+                              ))
+                          .value(1)
+                          .name("container_cpu_usage_seconds_total")
+                          .build()
+                ).build()),
+                false,
+                },
+            {
+                "Pod2Service_no_pod",
+                of("container_cpu_usage_seconds_total", SampleFamilyBuilder.newBuilder(
+                    Sample.builder()
+                          .labels(
+                              of(
+                                  "namespace", "default", "container", "my-nginx", "cpu", "total", "pod",
+                                  "my-nginx-5dc4865748-no-pod"
+                              ))
+                          .value(2)
+                          .name("container_cpu_usage_seconds_total")
+                          .build(),
+                    Sample.builder()
+                          .labels(
+                              of(
+                                  "namespace", "kube-system", "container", "kube-state-metrics", "cpu", "total", "pod",
+                                  "kube-state-metrics-6f979fd498-z7xwx"
+                              ))
+                          .value(1)
+                          .name("container_cpu_usage_seconds_total")
+                          .build()
+                ).build()),
+                "container_cpu_usage_seconds_total.retagByK8sMeta('service' , K8sRetagType.Pod2Service , 'pod' , 'namespace')",
+                Result.success(SampleFamilyBuilder.newBuilder(
+                    Sample.builder()
+                          .labels(
+                              of(
+                                  "namespace", "default", "container", "my-nginx", "cpu", "total", "pod",
+                                  "my-nginx-5dc4865748-no-pod" , "service", Retag.BLANK
+                              ))
+                          .value(2)
+                          .name("container_cpu_usage_seconds_total")
+                          .build(),
+                    Sample.builder()
+                          .labels(
+                              of(
+                                  "namespace", "kube-system", "container", "kube-state-metrics", "cpu", "total", "pod",
+                                  "kube-state-metrics-6f979fd498-z7xwx",
+                                  "service", "kube-state-metrics.kube-system"
+                              ))
+                          .value(1)
+                          .name("container_cpu_usage_seconds_total")
+                          .build()
+                ).build()),
+                false,
+                },
+            {
+                "Pod2Service_no_service",
+                of("container_cpu_usage_seconds_total", SampleFamilyBuilder.newBuilder(
+                    Sample.builder()
+                          .labels(
+                              of(
+                                  "namespace", "default", "container", "my-nginx", "cpu", "total", "pod",
+                                  "my-nginx-5dc4865748-no-service"
+                              ))
+                          .value(2)
+                          .name("container_cpu_usage_seconds_total")
+                          .build(),
+                    Sample.builder()
+                          .labels(
+                              of(
+                                  "namespace", "kube-system", "container", "kube-state-metrics", "cpu", "total", "pod",
+                                  "kube-state-metrics-6f979fd498-z7xwx"
+                              ))
+                          .value(1)
+                          .name("container_cpu_usage_seconds_total")
+                          .build()
+                ).build()),
+                "container_cpu_usage_seconds_total.retagByK8sMeta('service' , K8sRetagType.Pod2Service , 'pod' , 'namespace')",
+                Result.success(SampleFamilyBuilder.newBuilder(
+                    Sample.builder()
+                          .labels(
+                              of(
+                                  "namespace", "default", "container", "my-nginx", "cpu", "total", "pod",
+                                  "my-nginx-5dc4865748-no-service" , "service", Retag.BLANK
+                              ))
+                          .value(2)
+                          .name("container_cpu_usage_seconds_total")
+                          .build(),
+                    Sample.builder()
+                          .labels(
+                              of(
+                                  "namespace", "kube-system", "container", "kube-state-metrics", "cpu", "total", "pod",
+                                  "kube-state-metrics-6f979fd498-z7xwx",
+                                  "service", "kube-state-metrics.kube-system"
+                              ))
+                          .value(1)
+                          .name("container_cpu_usage_seconds_total")
+                          .build()
+                ).build()),
+                false,
+                },
+            {
+                "IPAddress_to_name",
+                of("rover_network_profiling_process_write_bytes", SampleFamilyBuilder.newBuilder(
+                    Sample.builder()
+                        .labels(
+                            of("service", "test", "instance", "test-instance", "side", "client",
+                                "client_address", "1.1.1.1", "server_address", "2.2.2.2")
+                        )
+                        .value(2)
+                        .name("rover_network_profiling_process_write_bytes")
+                        .build()
+                ).build()),
+                "rover_network_profiling_process_write_bytes.forEach(['client', 'server'] , " +
+                    "{prefix, tags -> tags[prefix + '_process_id'] = ProcessRegistry.generateVirtualRemoteProcess(tags.service, tags.instance, tags[prefix + '_address'])})",
+                Result.success(SampleFamilyBuilder.newBuilder(
+                    Sample.builder()
+                        .labels(
+                            of("service", "test", "instance", "test-instance", "side", "client",
+                                "client_address", "1.1.1.1", "client_process_id", IDManager.ProcessID.buildId(
+                                    IDManager.ServiceInstanceID.buildId(IDManager.ServiceID.buildId("test", true), "test-instance"),
+                                    "my-nginx-5dc4865748-mbczh.default"),
+                                "server_address", "2.2.2.2", "server_process_id", IDManager.ProcessID.buildId(
+                                    IDManager.ServiceInstanceID.buildId(IDManager.ServiceID.buildId("test", true), "test-instance"),
+                                    "kube-state-metrics.kube-system"))
+                        )
+                        .value(2)
+                        .name("rover_network_profiling_process_write_bytes")
+                        .build()
+                ).build()),
+                false,
+            }
+            });
+    }
+
+    private KubernetesServices originalServices;
+    private KubernetesPods originalPods;
+
+    @SneakyThrows
+    @BeforeEach
+    public void setup() {
+        // KubernetesServices/KubernetesPods are enum singletons; capture the real INSTANCE so the
+        // mock swap below can be undone in tearDown and does not leak into other tests in this JVM.
+        originalServices = KubernetesServices.INSTANCE;
+        originalPods = KubernetesPods.INSTANCE;
+        ReflectUtil.setInternalState(KubernetesServices.class, "INSTANCE",
+                                  Mockito.mock(KubernetesServices.class)
+        );
+        ReflectUtil.setInternalState(KubernetesPods.class, "INSTANCE",
+                                  Mockito.mock(KubernetesPods.class)
+        );
+
+        when(KubernetesServices.INSTANCE.list()).thenReturn(ImmutableList.of(
+                mockService("nginx-service", "default", of("run", "nginx"), "2.2.2.1"),
+                mockService("kube-state-metrics", "kube-system", of("run", "kube-state-metrics"), "2.2.2.2")));
+        ImmutableList.of(
+            mockService("nginx-service", "default", of("run", "nginx"), "2.2.2.1"),
+            mockService("kube-state-metrics", "kube-system", of("run", "kube-state-metrics"), "2.2.2.2"))
+            .forEach(svc ->
+                when(KubernetesServices.INSTANCE.findByID(ObjectID.builder().namespace(svc.getMetadata().getNamespace()).name(svc.getMetadata().getName()).build()))
+                .thenReturn(Optional.of(svc))
+            );
+        ImmutableList.of(
+            mockPod("my-nginx-5dc4865748-mbczh", "default", of("run", "nginx"), "1.1.1.1"),
+            mockPod("kube-state-metrics-6f979fd498-z7xwx", "kube-system", of("run", "kube-state-metrics"), "1.1.1.2"))
+            .forEach(pod -> {
+                when(KubernetesPods.INSTANCE.findByIP(pod.getStatus().getPodIP())).thenReturn(Optional.of(pod));
+                when(KubernetesPods.INSTANCE.findByObjectID(ObjectID.builder().name(pod.getMetadata().getName()).namespace(pod.getMetadata().getNamespace()).build())).thenReturn(Optional.of(pod));
+        });
+    }
+
+    @SneakyThrows
+    @AfterEach
+    public void tearDown() {
+        ReflectUtil.setInternalState(KubernetesServices.class, "INSTANCE", originalServices);
+        ReflectUtil.setInternalState(KubernetesPods.class, "INSTANCE", originalPods);
+    }
+
+    @ParameterizedTest(name = "{index}: {0}")
+    @MethodSource("data")
+    public void test(String name,
+                     ImmutableMap<String, SampleFamily> input,
+                     String expression,
+                     Result want,
+                     boolean isThrow) {
+        Expression e = DSL.parse(name, expression);
+        Result r = null;
+        try {
+            r = e.run(input);
+        } catch (Throwable t) {
+            if (isThrow) {
+                return;
+            }
+            log.error("Test failed", t);
+            fail("Should not throw anything");
+        }
+        if (isThrow) {
+            fail("Should throw something");
+        }
+        assertEquals(want, r);
+    }
+
+    private Service mockService(String name, String namespace, Map<String, String> selector, String ipAddress) {
+        Service service = new Service();
+        ObjectMeta serviceMeta = new ObjectMeta();
+        ServiceSpec v1ServiceSpec = new ServiceSpec();
+
+        serviceMeta.setName(name);
+        serviceMeta.setNamespace(namespace);
+        service.setMetadata(serviceMeta);
+        v1ServiceSpec.setSelector(selector);
+        service.setSpec(v1ServiceSpec);
+
+        final ServiceStatus v1ServiceStatus = new ServiceStatus();
+        final LoadBalancerStatus balancerStatus = new LoadBalancerStatus();
+        final LoadBalancerIngress loadBalancerIngress = new LoadBalancerIngress();
+        loadBalancerIngress.setIp(ipAddress);
+        balancerStatus.setIngress(Arrays.asList(loadBalancerIngress));
+        v1ServiceStatus.setLoadBalancer(balancerStatus);
+        service.setStatus(v1ServiceStatus);
+
+        return service;
+    }
+
+    private Pod mockPod(String name, String namespace, Map<String, String> labels, String ipAddress) {
+        Pod v1Pod = new Pod();
+        ObjectMeta podMeta = new ObjectMeta();
+        podMeta.setName(name);
+        podMeta.setNamespace(namespace);
+        podMeta.setLabels(labels);
+        final PodStatus status = new PodStatus();
+        status.setPodIP(ipAddress);
+        v1Pod.setStatus(status);
+        v1Pod.setMetadata(podMeta);
+
+        return v1Pod;
+    }
+}
diff --git a/oap-server/analyzer/meter-analyzer/src/test/java/org/apache/skywalking/oap/meter/analyzer/v2/dsl/ScopeTest.java b/oap-server/analyzer/meter-analyzer/src/test/java/org/apache/skywalking/oap/meter/analyzer/v2/dsl/ScopeTest.java
new file mode 100644
index 0000000000..3529e5b521
--- /dev/null
+++ b/oap-server/analyzer/meter-analyzer/src/test/java/org/apache/skywalking/oap/meter/analyzer/v2/dsl/ScopeTest.java
@@ -0,0 +1,642 @@
+/*
+ * 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.
+ *
+ */
+
+package org.apache.skywalking.oap.meter.analyzer.v2.dsl;
+
+import com.google.common.collect.ImmutableMap;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.skywalking.oap.server.core.analysis.Layer;
+import org.apache.skywalking.oap.server.core.analysis.meter.MeterEntity;
+import org.apache.skywalking.oap.server.core.config.NamingControl;
+import org.apache.skywalking.oap.server.core.config.group.EndpointNameGrouping;
+import org.apache.skywalking.oap.server.core.source.DetectPoint;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+import static com.google.common.collect.ImmutableMap.of;
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+@Slf4j
+public class ScopeTest {
+    public static Collection<Object[]> data() {
+        // This method is called before `@BeforeAll`.
+        MeterEntity.setNamingControl(
+            new NamingControl(512, 512, 512, new EndpointNameGrouping()));
+
+        return Arrays.asList(new Object[][] {
+            {
+                "sum_service",
+                of("http_success_request", SampleFamilyBuilder.newBuilder(
+                    Sample.builder().labels(of("idc", "t1")).value(50).name("http_success_request").build(),
+                    Sample.builder()
+                          .labels(of("idc", "t3", "region", "cn", "svc", "catalog"))
+                          .value(51)
+                          .name("http_success_request")
+                          .build(),
+                    Sample.builder()
+                          .labels(of("idc", "t1", "region", "us", "svc", "product"))
+                          .value(50)
+                          .name("http_success_request")
+                          .build(),
+                    Sample.builder()
+                          .labels(of("idc", "t1", "region", "us", "instance", "10.0.0.1"))
+                          .value(100)
+                          .name("http_success_request")
+                          .build(),
+                    Sample.builder()
+                          .labels(of("idc", "t3", "region", "cn", "instance", "10.0.0.1"))
+                          .value(3)
+                          .name("http_success_request")
+                          .build()
+                ).build()),
+                "http_success_request.sum(['idc']).service(['idc'], Layer.GENERAL)",
+                false,
+                new HashMap<MeterEntity, Sample[]>() {
+                    {
+                        put(
+                            MeterEntity.newService("t1", Layer.GENERAL),
+                            new Sample[] {Sample.builder().labels(of()).value(200).name("http_success_request").build()}
+                        );
+                        put(
+                            MeterEntity.newService("t3", Layer.GENERAL),
+                            new Sample[] {Sample.builder().labels(of()).value(54).name("http_success_request").build()}
+                        );
+                    }
+                }
+            },
+            {
+                "sum_service_labels",
+                of("http_success_request", SampleFamilyBuilder.newBuilder(
+                    Sample.builder().labels(of("idc", "t1")).value(50).name("http_success_request").build(),
+                    Sample.builder()
+                          .labels(of("idc", "t3", "region", "cn", "svc", "catalog"))
+                          .value(51)
+                          .name("http_success_request")
+                          .build(),
+                    Sample.builder()
+                          .labels(of("idc", "t1", "region", "us", "svc", "product"))
+                          .value(50)
+                          .name("http_success_request")
+                          .build(),
+                    Sample.builder()
+                          .labels(of("idc", "t1", "region", "us", "instance", "10.0.0.1"))
+                          .value(100)
+                          .name("http_success_request")
+                          .build(),
+                    Sample.builder()
+                          .labels(of("idc", "t3", "region", "cn", "instance", "10.0.0.1"))
+                          .value(3)
+                          .name("http_success_request")
+                          .build()
+                ).build()),
+                "http_success_request.sum(['region', 'idc']).service(['idc'], Layer.GENERAL)",
+                false,
+                new HashMap<MeterEntity, Sample[]>() {
+                    {
+                        put(
+                            MeterEntity.newService("t1", Layer.GENERAL),
+                            new Sample[] {
+                                Sample.builder()
+                                      .labels(of("region", ""))
+                                      .value(50)
+                                      .name("http_success_request").build(),
+                                Sample.builder()
+                                      .labels(of("region", "us"))
+                                      .value(150)
+                                      .name("http_success_request").build()
+                            }
+                        );
+                        put(
+                            MeterEntity.newService("t3", Layer.GENERAL),
+                            new Sample[] {
+                                Sample.builder()
+                                      .labels(of("region", "cn"))
+                                      .value(54)
+                                      .name("http_success_request").build()
+                            }
+                        );
+                    }
+                }
+            },
+            {
+                "sum_service_m",
+                of("http_success_request", SampleFamilyBuilder.newBuilder(
+                    Sample.builder().labels(of("idc", "t1")).value(50).name("http_success_request").build(),
+                    Sample.builder()
+                          .labels(of("idc", "t3", "region", "cn", "svc", "catalog"))
+                          .value(51)
+                          .name("http_success_request")
+                          .build(),
+                    Sample.builder()
+                          .labels(of("idc", "t1", "region", "us", "svc", "product"))
+                          .value(50)
+                          .name("http_success_request")
+                          .build(),
+                    Sample.builder()
+                          .labels(of("idc", "t1", "region", "us", "instance", "10.0.0.1"))
+                          .value(100)
+                          .name("http_success_request")
+                          .build(),
+                    Sample.builder()
+                          .labels(of("idc", "t3", "region", "cn", "instance", "10.0.0.1"))
+                          .value(3)
+                          .name("http_success_request")
+                          .build()
+                ).build()),
+                "http_success_request.sum(['idc', 'region']).service(['idc' , 'region'], Layer.GENERAL)",
+                false,
+                new HashMap<MeterEntity, Sample[]>() {
+                    {
+                        put(
+                            MeterEntity.newService("t1.us", Layer.GENERAL),
+                            new Sample[] {Sample.builder().labels(of()).value(150).name("http_success_request").build()}
+                        );
+                        put(
+                            MeterEntity.newService("t3.cn", Layer.GENERAL),
+                            new Sample[] {Sample.builder().labels(of()).value(54).name("http_success_request").build()}
+                        );
+                        put(
+                            MeterEntity.newService("t1", Layer.GENERAL),
+                            new Sample[] {Sample.builder().labels(of()).value(50).name("http_success_request").build()}
+                        );
+                    }
+                }
+            },
+            {
+                "sum_service_endpoint",
+                of("http_success_request", SampleFamilyBuilder.newBuilder(
+                    Sample.builder().labels(of("idc", "t1")).value(50).name("http_success_request").build(),
+                    Sample.builder()
+                          .labels(of("idc", "t3", "region", "cn", "svc", "catalog"))
+                          .value(51)
+                          .name("http_success_request")
+                          .build(),
+                    Sample.builder()
+                          .labels(of("idc", "t1", "region", "us", "svc", "product"))
+                          .value(50)
+                          .name("http_success_request")
+                          .build(),
+                    Sample.builder()
+                          .labels(of("idc", "t1", "region", "us", "instance", "10.0.0.1"))
+                          .value(100)
+                          .name("http_success_request")
+                          .build(),
+                    Sample.builder()
+                          .labels(of("idc", "t3", "region", "cn", "instance", "10.0.0.1"))
+                          .value(3)
+                          .name("http_success_request")
+                          .build()
+                ).build()),
+                "http_success_request.sum(['region', 'idc']).endpoint(['idc'] , ['region'], Layer.GENERAL)",
+                false,
+                new HashMap<MeterEntity, Sample[]>() {
+                    {
+                        put(
+                            MeterEntity.newEndpoint("t1", "us", Layer.GENERAL),
+                            new Sample[] {Sample.builder().labels(of()).value(150).name("http_success_request").build()}
+                        );
+                        put(
+                            MeterEntity.newEndpoint("t3", "cn", Layer.GENERAL),
+                            new Sample[] {Sample.builder().labels(of()).value(54).name("http_success_request").build()}
+                        );
+                        put(
+                            MeterEntity.newEndpoint("t1", "", Layer.GENERAL),
+                            new Sample[] {Sample.builder().labels(of()).value(50).name("http_success_request").build()}
+                        );
+                    }
+                }
+            },
+
+            {
+                "sum_service_endpoint_labels",
+                of("http_success_request", SampleFamilyBuilder.newBuilder(
+                    Sample.builder().labels(of("idc", "t1")).value(50).name("http_success_request").build(),
+                    Sample.builder()
+                          .labels(of("idc", "t3", "region", "cn", "svc", "catalog"))
+                          .value(51)
+                          .name("http_success_request")
+                          .build(),
+                    Sample.builder()
+                          .labels(of("idc", "t1", "region", "us", "svc", "product"))
+                          .value(50)
+                          .name("http_success_request")
+                          .build(),
+                    Sample.builder()
+                          .labels(of("idc", "t1", "region", "us", "instance", "10.0.0.1"))
+                          .value(100)
+                          .name("http_success_request")
+                          .build(),
+                    Sample.builder()
+                          .labels(of("idc", "t3", "region", "cn", "instance", "10.0.0.1"))
+                          .value(3)
+                          .name("http_success_request")
+                          .build()
+                ).build()),
+                "http_success_request.sum(['region', 'idc' , 'instance']).endpoint(['idc'] , ['region'], Layer.GENERAL)",
+                false,
+                new HashMap<MeterEntity, Sample[]>() {
+                    {
+                        put(
+                            MeterEntity.newEndpoint("t1", "us", Layer.GENERAL),
+                            new Sample[] {
+                                Sample.builder()
+                                      .labels(of("instance", ""))
+                                      .value(50)
+                                      .name("http_success_request").build(),
+                                Sample.builder()
+                                      .labels(of("instance", "10.0.0.1"))
+                                      .value(100)
+                                      .name("http_success_request").build()
+                            }
+                        );
+                        put(
+                            MeterEntity.newEndpoint("t3", "cn", Layer.GENERAL),
+                            new Sample[] {
+                                Sample.builder()
+                                      .labels(of("instance", ""))
+                                      .value(51)
+                                      .name("http_success_request").build(),
+                                Sample.builder()
+                                      .labels(of("instance", "10.0.0.1"))
+                                      .value(3)
+                                      .name("http_success_request").build()
+                            }
+                        );
+                        put(
+                            MeterEntity.newEndpoint("t1", "", Layer.GENERAL),
+                            new Sample[] {
+                                Sample.builder()
+                                      .labels(of("instance", ""))
+                                      .value(50)
+                                      .name("http_success_request").build()
+                            }
+                        );
+                    }
+                }
+            },
+            {
+                "sum_service_endpoint_labels_m",
+                of("http_success_request", SampleFamilyBuilder.newBuilder(
+                    Sample.builder().labels(of("idc", "t1")).value(50).name("http_success_request").build(),
+                    Sample.builder()
+                          .labels(of("idc", "t3", "region", "cn", "svc", "product"))
+                          .value(51)
+                          .name("http_success_request")
+                          .build(),
+                    Sample.builder()
+                          .labels(of("idc", "t1", "region", "us", "svc", "catalog"))
+                          .value(50)
+                          .name("http_success_request")
+                          .build(),
+                    Sample.builder()
+                          .labels(of("idc", "t1", "region", "us", "svc", "catalog", "instance", "10.0.0.1"))
+                          .value(100)
+                          .name("http_success_request")
+                          .build(),
+                    Sample.builder()
+                          .labels(of("idc", "t3", "region", "cn", "svc", "product", "instance", "10.0.0.1"))
+                          .value(3)
+                          .name("http_success_request")
+                          .build()
+                ).build()),
+                "http_success_request.sum(['region', 'idc' , 'svc' , 'instance']).endpoint(['idc'] , ['region','svc'], Layer.GENERAL)",
+                false,
+                new HashMap<MeterEntity, Sample[]>() {
+                    {
+                        put(
+                            MeterEntity.newEndpoint("t1", "us.catalog", Layer.GENERAL),
+                            new Sample[] {
+                                Sample.builder()
+                                      .labels(of("instance", ""))
+                                      .value(50)
+                                      .name("http_success_request").build(),
+                                Sample.builder()
+                                      .labels(of("instance", "10.0.0.1"))
+                                      .value(100)
+                                      .name("http_success_request").build()
+                            }
+                        );
+                        put(
+                            MeterEntity.newEndpoint("t3", "cn.product", Layer.GENERAL),
+                            new Sample[] {
+                                Sample.builder()
+                                      .labels(of("instance", ""))
+                                      .value(51)
+                                      .name("http_success_request").build(),
+                                Sample.builder()
+                                      .labels(of("instance", "10.0.0.1"))
+                                      .value(3)
+                                      .name("http_success_request").build()
+                            }
+                        );
+                        put(
+                            MeterEntity.newEndpoint("t1", "", Layer.GENERAL),
+                            new Sample[] {
+                                Sample.builder()
+                                      .labels(of("instance", ""))
+                                      .value(50)
+                                      .name("http_success_request").build()
+                            }
+                        );
+                    }
+                }
+            },
+            {
+                "sum_service_instance",
+                of("http_success_request", SampleFamilyBuilder.newBuilder(
+                    Sample.builder().labels(of("idc", "t1")).value(50).name("http_success_request").build(),
+                    Sample.builder()
+                          .labels(of("idc", "t3", "region", "cn", "svc", "catalog"))
+                          .value(51)
+                          .name("http_success_request")
+                          .build(),
+                    Sample.builder()
+                          .labels(of("idc", "t1", "region", "us", "svc", "product"))
+                          .value(50)
+                          .name("http_success_request")
+                          .build(),
+                    Sample.builder()
+                          .labels(of("idc", "t1", "region", "us", "instance", "10.0.0.1"))
+                          .value(100)
+                          .name("http_success_request")
+                          .build(),
+                    Sample.builder()
+                          .labels(of("idc", "t3", "region", "cn", "instance", "10.0.0.1"))
+                          .value(3)
+                          .name("http_success_request")
+                          .build()
+                ).build()),
+                "http_success_request.sum(['region', 'idc']).instance(['idc'] , ['region'], Layer.GENERAL)",
+                false,
+                new HashMap<MeterEntity, Sample[]>() {
+                    {
+                        put(
+                            MeterEntity.newServiceInstance("t1", "us", Layer.GENERAL, null),
+                            new Sample[] {Sample.builder().labels(of()).value(150).name("http_success_request").build()}
+                        );
+                        put(
+                            MeterEntity.newServiceInstance("t3", "cn", Layer.GENERAL, null),
+                            new Sample[] {Sample.builder().labels(of()).value(54).name("http_success_request").build()}
+                        );
+                        put(
+                            MeterEntity.newServiceInstance("t1", "", Layer.GENERAL, null),
+                            new Sample[] {Sample.builder().labels(of()).value(50).name("http_success_request").build()}
+                        );
+                    }
+                }
+            },
+            {
+                "sum_service_instance_labels",
+                of("http_success_request", SampleFamilyBuilder.newBuilder(
+                    Sample.builder().labels(of("idc", "t1")).value(50).name("http_success_request").build(),
+                    Sample.builder()
+                          .labels(of("idc", "t3", "region", "cn", "svc", "catalog"))
+                          .value(51)
+                          .name("http_success_request")
+                          .build(),
+                    Sample.builder()
+                          .labels(of("idc", "t1", "region", "us", "svc", "product"))
+                          .value(50)
+                          .name("http_success_request")
+                          .build(),
+                    Sample.builder()
+                          .labels(of("idc", "t1", "region", "us", "instance", "10.0.0.1"))
+                          .value(100)
+                          .name("http_success_request")
+                          .build(),
+                    Sample.builder()
+                          .labels(of("idc", "t3", "region", "cn", "instance", "10.0.0.1"))
+                          .value(3)
+                          .name("http_success_request")
+                          .build()
+                ).build()),
+                "http_success_request.sum(['region', 'idc' , 'instance']).instance(['idc'] , ['region'], Layer.GENERAL)",
+                false,
+                new HashMap<MeterEntity, Sample[]>() {
+                    {
+                        put(
+                            MeterEntity.newServiceInstance("t1", "us", Layer.GENERAL, null),
+                            new Sample[] {
+                                Sample.builder()
+                                      .labels(of("instance", ""))
+                                      .value(50)
+                                      .name("http_success_request").build(),
+                                Sample.builder()
+                                      .labels(of("instance", "10.0.0.1"))
+                                      .value(100)
+                                      .name("http_success_request").build()
+                            }
+                        );
+                        put(
+                            MeterEntity.newServiceInstance("t3", "cn", Layer.GENERAL, null),
+                            new Sample[] {
+                                Sample.builder()
+                                      .labels(of("instance", ""))
+                                      .value(51)
+                                      .name("http_success_request").build(),
+                                Sample.builder()
+                                      .labels(of("instance", "10.0.0.1"))
+                                      .value(3)
+                                      .name("http_success_request").build()
+                            }
+                        );
+                        put(
+                            MeterEntity.newServiceInstance("t1", "", Layer.GENERAL, null),
+                            new Sample[] {
+                                Sample.builder()
+                                      .labels(of("instance", ""))
+                                      .value(50)
+                                      .name("http_success_request").build()
+                            }
+                        );
+                    }
+                }
+            },
+            {
+                "sum_service_relation",
+                of("envoy_cluster_metrics_up_cx_active", SampleFamilyBuilder.newBuilder(
+                    Sample.builder()
+                          .labels(of("app", "productpage", "cluster_name", "details"))
+                          .value(11)
+                          .name("envoy_cluster_metrics_up_cx_active")
+                          .build(),
+                    Sample.builder()
+                          .labels(of("app", "productpage", "cluster_name", "reviews"))
+                          .value(16)
+                          .name("envoy_cluster_metrics_up_cx_active")
+                          .build()
+                ).build()),
+                "envoy_cluster_metrics_up_cx_active.sum(['app' ,'cluster_name']).serviceRelation(DetectPoint.CLIENT, ['app'], ['cluster_name'], Layer.GENERAL)",
+                false,
+                new HashMap<MeterEntity, Sample[]>() {
+                    {
+                        put(
+                            MeterEntity.newServiceRelation("productpage", "details", DetectPoint.CLIENT, Layer.GENERAL, 0),
+                            new Sample[] {
+                                Sample.builder()
+                                      .labels(of())
+                                      .value(11)
+                                      .name("envoy_cluster_metrics_up_cx_active").build()
+                            }
+                        );
+                        put(
+                            MeterEntity.newServiceRelation("productpage", "reviews", DetectPoint.CLIENT, Layer.GENERAL, 0),
+                            new Sample[] {
+                                Sample.builder()
+                                      .labels(of())
+                                      .value(16)
+                                      .name("envoy_cluster_metrics_up_cx_active").build()
+                            }
+                        );
+                    }
+                }
+            },
+            {
+                "sum_service_relation_with_component",
+                of("envoy_cluster_metrics_up_cx_active", SampleFamilyBuilder.newBuilder(
+                    Sample.builder()
+                        .labels(of("app", "productpage", "cluster_name", "details", "env", "test", "component", "10"))
+                        .value(11)
+                        .name("envoy_cluster_metrics_up_cx_active")
+                        .build(),
+                    Sample.builder()
+                        .labels(of("app", "productpage", "cluster_name", "reviews", "env", "test", "component", "10"))
+                        .value(16)
+                        .name("envoy_cluster_metrics_up_cx_active")
+                        .build()
+                ).build()),
+                "envoy_cluster_metrics_up_cx_active.sum(['app' ,'cluster_name', 'env', 'component']).serviceRelation(DetectPoint.CLIENT, ['app', 'env'], ['cluster_name'], '-', Layer.GENERAL, 'component')",
+                false,
+                new HashMap<MeterEntity, Sample[]>() {
+                    {
+                        put(
+                            MeterEntity.newServiceRelation("productpage-test", "details", DetectPoint.CLIENT, Layer.GENERAL, 10),
+                            new Sample[] {
+                                Sample.builder()
+                                    .labels(of())
+                                    .value(11)
+                                    .name("envoy_cluster_metrics_up_cx_active").build()
+                            }
+                        );
+                        put(
+                            MeterEntity.newServiceRelation("productpage-test", "reviews", DetectPoint.CLIENT, Layer.GENERAL, 10),
+                            new Sample[] {
+                                Sample.builder()
+                                    .labels(of())
+                                    .value(16)
+                                    .name("envoy_cluster_metrics_up_cx_active").build()
+                            }
+                        );
+                    }
+                }
+            },
+            {
+                "sum_process_relation",
+                of("rover_network_profiling_process_write_bytes", SampleFamilyBuilder.newBuilder(
+                    Sample.builder()
+                        .labels(of("service", "test", "instance", "test-instance", "side", "server", "client_process_id", "abc", "server_process_id", "def", "component", "1"))
+                        .value(11)
+                        .name("rover_network_profiling_process_write_bytes")
+                        .build(),
+                    Sample.builder()
+                        .labels(of("service", "test", "instance", "test-instance", "side", "client", "client_process_id", "abc", "server_process_id", "def", "component", "2"))
+                        .value(12)
+                        .name("rover_network_profiling_process_write_bytes")
+                        .build()
+                ).build()),
+                "rover_network_profiling_process_write_bytes.sum(['service' ,'instance', 'side', 'client_process_id', 'server_process_id', 'component'])" +
+                    ".processRelation('side', ['service'], ['instance'], 'client_process_id', 'server_process_id', 'component')",
+                false,
+                new HashMap<MeterEntity, Sample[]>() {
+                    {
+                        put(
+                            MeterEntity.newProcessRelation("test", "test-instance", "abc", "def", 1, DetectPoint.SERVER),
+                            new Sample[] {
+                                Sample.builder()
+                                    .labels(of())
+                                    .value(11)
+                                    .name("rover_network_profiling_process_write_bytes").build()
+                            }
+                        );
+                        put(
+                            MeterEntity.newProcessRelation("test", "test-instance", "abc", "def", 2, DetectPoint.CLIENT),
+                            new Sample[] {
+                                Sample.builder()
+                                    .labels(of())
+                                    .value(12)
+                                    .name("rover_network_profiling_process_write_bytes").build()
+                            }
+                        );
+                    }
+                }
+            }
+        });
+    }
+
+    @BeforeAll
+    public static void setup() {
+        MeterEntity.setNamingControl(
+            new NamingControl(512, 512, 512, new EndpointNameGrouping()));
+    }
+
+    @AfterAll
+    public static void tearDown() {
+        MeterEntity.setNamingControl(null);
+    }
+
+    @ParameterizedTest(name = "{0}")
+    @MethodSource("data")
+    public void test(final String name,
+                     final ImmutableMap<String, SampleFamily> input,
+                     final String expression,
+                     final boolean isThrow,
+                     final Map<MeterEntity, Sample[]> want) {
+        Expression e = DSL.parse(name, expression);
+        Result r = null;
+        try {
+            r = e.run(input);
+        } catch (Throwable t) {
+            if (isThrow) {
+                return;
+            }
+            log.error("Test failed", t);
+            fail("Should not throw anything");
+        }
+        if (isThrow) {
+            fail("Should throw something");
+        }
+        assertTrue(r.isSuccess());
+        Map<MeterEntity, Sample[]> meterSamplesR = r.getData().context.getMeterSamples();
+        // Compare the entity sets first so a missing (or extra) expected entity fails the test,
+        // then iterate over `want` so every expected entity's samples are asserted.
+        assertEquals(want.keySet(), meterSamplesR.keySet());
+        want.forEach((meterEntity, samples) -> {
+            assertArrayEquals(samples, meterSamplesR.get(meterEntity));
+        });
+    }
+}
diff --git a/oap-server/analyzer/meter-analyzer/src/test/java/org/apache/skywalking/oap/meter/analyzer/v2/dsl/TagFilterTest.java b/oap-server/analyzer/meter-analyzer/src/test/java/org/apache/skywalking/oap/meter/analyzer/v2/dsl/TagFilterTest.java
new file mode 100644
index 0000000000..3808dc8f0a
--- /dev/null
+++ b/oap-server/analyzer/meter-analyzer/src/test/java/org/apache/skywalking/oap/meter/analyzer/v2/dsl/TagFilterTest.java
@@ -0,0 +1,140 @@
+/*
+ * 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.
+ *
+ */
+
+package org.apache.skywalking.oap.meter.analyzer.v2.dsl;
+
+import com.google.common.collect.ImmutableMap;
+import lombok.extern.slf4j.Slf4j;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import java.util.Arrays;
+import java.util.Collection;
+
+import static com.google.common.collect.ImmutableMap.of;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.fail;
+
+@Slf4j
+public class TagFilterTest {
+    public static Collection<Object[]> data() {
+        final SampleFamily sf =
+            SampleFamilyBuilder.newBuilder(
+                Sample.builder().labels(of("idc", "t2")).value(50).name("http_success_request").build(),
+                Sample.builder()
+                      .labels(of("idc", "t3", "region", "cn", "svc", "catalog"))
+                      .value(50)
+                      .name("http_success_request")
+                      .build(),
+                Sample.builder()
+                      .labels(of("idc", "t1", "region", "us", "svc", "product"))
+                      .value(50)
+                      .name("http_success_request")
+                      .build(),
+                Sample.builder()
+                      .labels(of("idc", "t1", "region", "us", "instance", "10.0.0.1"))
+                      .name("http_success_request")
+                      .value(50)
+                      .build(),
+                Sample.builder()
+                      .labels(of("idc", "t3", "region", "cn", "instance", "10.0.0.1"))
+                      .name("http_success_request")
+                      .value(3)
+                      .build()
+            ).build();
+        return Arrays.asList(new Object[][] {
+            {
+                "tagEqual",
+                of("http_success_request", sf),
+                "http_success_request.tagEqual('idc', 't3')",
+                Result.success(SampleFamilyBuilder.newBuilder(
+                    Sample.builder()
+                          .labels(of("idc", "t3", "region", "cn", "svc", "catalog"))
+                          .value(50)
+                          .name("http_success_request")
+                          .build(),
+                    Sample.builder()
+                          .labels(of("idc", "t3", "region", "cn", "instance", "10.0.0.1"))
+                          .name("http_success_request")
+                          .value(3)
+                          .build()
+                ).build()),
+                false,
+                },
+            {
+                "tagNotEqual",
+                of("http_success_request", sf),
+                "http_success_request.tagNotEqual('idc', 't1')",
+                Result.success(SampleFamilyBuilder.newBuilder(
+                    Sample.builder().labels(of("idc", "t2")).value(50).name("http_success_request").build(),
+                    Sample.builder()
+                          .labels(of("idc", "t3", "region", "cn", "svc", "catalog"))
+                          .value(50)
+                          .name("http_success_request")
+                          .build(),
+                    Sample.builder()
+                          .labels(of("idc", "t3", "region", "cn", "instance", "10.0.0.1"))
+                          .name("http_success_request")
+                          .value(3)
+                          .build()
+                ).build()),
+                false,
+                },
+            {
+                "tagMatch",
+                of("http_success_request", sf),
+                "http_success_request.tagMatch('idc', 't1|t2|t3')",
+                Result.success(sf),
+                false,
+                },
+            {
+                "tagNotMatch",
+                of("http_success_request", sf),
+                "http_success_request.tagNotMatch('idc', 't1|t3')",
+                Result.success(SampleFamilyBuilder.newBuilder(
+                    Sample.builder().labels(of("idc", "t2")).value(50).name("http_success_request").build()).build()),
+                false,
+                },
+            });
+    }
+
+    @ParameterizedTest(name = "{0}")
+    @MethodSource("data")
+    public void test(String name,
+                     ImmutableMap<String, SampleFamily> input,
+                     String expression,
+                     Result want,
+                     boolean isThrow) {
+        Result r = null;
+        try {
+            // v2 compiles eagerly, so parse-time failures surface here too (v1 surfaced them at run()).
+            Expression e = DSL.parse(name, expression);
+            r = e.run(input);
+        } catch (Throwable t) {
+            if (isThrow) {
+                return;
+            }
+            log.error("Test failed", t);
+            fail("Should not throw anything");
+        }
+        if (isThrow) {
+            fail("Should throw something");
+        }
+        assertEquals(want, r);
+    }
+}
diff --git a/oap-server/analyzer/meter-analyzer/src/test/java/org/apache/skywalking/oap/meter/analyzer/v2/dsl/ValueFilterTest.java b/oap-server/analyzer/meter-analyzer/src/test/java/org/apache/skywalking/oap/meter/analyzer/v2/dsl/ValueFilterTest.java
new file mode 100644
index 0000000000..3aa9db1e34
--- /dev/null
+++ b/oap-server/analyzer/meter-analyzer/src/test/java/org/apache/skywalking/oap/meter/analyzer/v2/dsl/ValueFilterTest.java
@@ -0,0 +1,148 @@
+/*
+ * 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.
+ *
+ */
+
+package org.apache.skywalking.oap.meter.analyzer.v2.dsl;
+
+import com.google.common.collect.ImmutableMap;
+import lombok.extern.slf4j.Slf4j;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import java.util.Arrays;
+import java.util.Collection;
+
+import static com.google.common.collect.ImmutableMap.of;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.fail;
+
+@Slf4j
+public class ValueFilterTest {
+    public static Collection<Object[]> data() {
+        return Arrays.asList(new Object[][] {
+            {
+                "valueEqual",
+                of("http_success_request", SampleFamilyBuilder.newBuilder(
+                    Sample.builder().labels(of("idc", "t1")).value(2).name("http_success_request").build(),
+                    Sample.builder().labels(of("idc", "t2")).value(2).name("http_success_request").build(),
+                    Sample.builder().labels(of("idc", "t3")).value(1).name("http_success_request").build()
+                ).build()),
+                "http_success_request.valueEqual(1)",
+                Result.success(SampleFamilyBuilder.newBuilder(
+                    Sample.builder().labels(of("idc", "t3")).value(1).name("http_success_request").build()
+                ).build()),
+                false,
+                },
+            {
+                "valueNotEqual",
+                of("http_success_request", SampleFamilyBuilder.newBuilder(
+                    Sample.builder().labels(of("idc", "t1")).value(2).name("http_success_request").build(),
+                    Sample.builder().labels(of("idc", "t2")).value(2).name("http_success_request").build(),
+                    Sample.builder().labels(of("idc", "t3")).value(1).name("http_success_request").build()
+                ).build()),
+                "http_success_request.valueNotEqual(1)",
+                Result.success(SampleFamilyBuilder.newBuilder(
+                    Sample.builder().labels(of("idc", "t1")).value(2).name("http_success_request").build(),
+                    Sample.builder().labels(of("idc", "t2")).value(2).name("http_success_request").build()
+                ).build()),
+                false,
+                },
+            {
+                "valueGreater",
+                of("http_success_request", SampleFamilyBuilder.newBuilder(
+                    Sample.builder().labels(of("idc", "t1")).value(2).name("http_success_request").build(),
+                    Sample.builder().labels(of("idc", "t2")).value(2).name("http_success_request").build(),
+                    Sample.builder().labels(of("idc", "t3")).value(1).name("http_success_request").build()
+                ).build()),
+                "http_success_request.valueGreater(1)",
+                Result.success(SampleFamilyBuilder.newBuilder(
+                    Sample.builder().labels(of("idc", "t1")).value(2).name("http_success_request").build(),
+                    Sample.builder().labels(of("idc", "t2")).value(2).name("http_success_request").build()
+                ).build()),
+                false,
+                },
+            {
+                "valueGreaterEqual",
+                of("http_success_request", SampleFamilyBuilder.newBuilder(
+                    Sample.builder().labels(of("idc", "t1")).value(2).name("http_success_request").build(),
+                    Sample.builder().labels(of("idc", "t2")).value(2).name("http_success_request").build(),
+                    Sample.builder().labels(of("idc", "t3")).value(1).name("http_success_request").build()
+                ).build()),
+                "http_success_request.valueGreaterEqual(1)",
+                Result.success(SampleFamilyBuilder.newBuilder(
+                    Sample.builder().labels(of("idc", "t1")).value(2).name("http_success_request").build(),
+                    Sample.builder().labels(of("idc", "t2")).value(2).name("http_success_request").build(),
+                    Sample.builder().labels(of("idc", "t3")).value(1).name("http_success_request").build()
+                ).build()),
+                false,
+                },
+            {
+                "valueLess",
+                of("http_success_request", SampleFamilyBuilder.newBuilder(
+                    Sample.builder().labels(of("idc", "t1")).value(2).name("http_success_request").build(),
+                    Sample.builder().labels(of("idc", "t2")).value(2).name("http_success_request").build(),
+                    Sample.builder().labels(of("idc", "t3")).value(1).name("http_success_request").build()
+                ).build()),
+                "http_success_request.valueLess(2)",
+                Result.success(SampleFamilyBuilder.newBuilder(
+                    Sample.builder().labels(of("idc", "t3")).value(1).name("http_success_request").build()
+                ).build()),
+                false,
+                },
+            {
+                "valueLessEqual",
+                of("http_success_request", SampleFamilyBuilder.newBuilder(
+                    Sample.builder().labels(of("idc", "t1")).value(2).name("http_success_request").build(),
+                    Sample.builder().labels(of("idc", "t2")).value(2).name("http_success_request").build(),
+                    Sample.builder().labels(of("idc", "t3")).value(1).name("http_success_request").build()
+                ).build()),
+                "http_success_request.valueLessEqual(2)",
+                Result.success(SampleFamilyBuilder.newBuilder(
+                    Sample.builder().labels(of("idc", "t1")).value(2).name("http_success_request").build(),
+                    Sample.builder().labels(of("idc", "t2")).value(2).name("http_success_request").build(),
+                    Sample.builder().labels(of("idc", "t3")).value(1).name("http_success_request").build()
+                ).build()),
+                false,
+                },
+            });
+    }
+
+    @ParameterizedTest(name = "{0}")
+    @MethodSource("data")
+    public void test(final String name,
+                     final ImmutableMap<String, SampleFamily> input,
+                     final String expression,
+                     final Result want,
+                     final boolean isThrow) {
+        Result r = null;
+        try {
+            // v2 compiles eagerly, so parse-time failures surface here too (v1 surfaced them at run()).
+            Expression e = DSL.parse(name, expression);
+            r = e.run(input);
+        } catch (Throwable t) {
+            if (isThrow) {
+                return;
+            }
+            log.error("Test failed", t);
+            fail("Should not throw anything");
+        }
+        if (isThrow) {
+            fail("Should throw something");
+        }
+        assertEquals(want, r);
+    }
+}
diff --git a/oap-server/analyzer/meter-analyzer/src/test/java/org/apache/skywalking/oap/meter/analyzer/v2/dsl/counter/CounterWindowTest.java b/oap-server/analyzer/meter-analyzer/src/test/java/org/apache/skywalking/oap/meter/analyzer/v2/dsl/counter/CounterWindowTest.java
new file mode 100644
index 0000000000..3e1a24f354
--- /dev/null
+++ b/oap-server/analyzer/meter-analyzer/src/test/java/org/apache/skywalking/oap/meter/analyzer/v2/dsl/counter/CounterWindowTest.java
@@ -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.
+ */
+
+package org.apache.skywalking.oap.meter.analyzer.v2.dsl.counter;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+import io.vavr.Tuple2;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.time.Duration;
+import java.util.List;
+
+import static java.time.Instant.parse;
+
+public class CounterWindowTest {
+
+    public static List<Tuple2<Long, Double>> parameters() {
+        return Lists.newArrayList(
+            new Tuple2<>(parse("2020-09-11T11:11:01.03Z").toEpochMilli(), 10d),
+            new Tuple2<>(parse("2020-09-11T11:11:15.99Z").toEpochMilli(), 11d),
+            new Tuple2<>(parse("2020-09-11T11:11:31.00Z").toEpochMilli(), 12d),
+            new Tuple2<>(parse("2020-09-11T11:11:46.09Z").toEpochMilli(), 13d),
+            new Tuple2<>(parse("2020-09-11T11:12:00.97Z").toEpochMilli(), 14d),
+            new Tuple2<>(parse("2020-09-11T11:11:00.97Z").toEpochMilli(), 15d),
+            new Tuple2<>(parse("2020-09-11T11:12:16.60Z").toEpochMilli(), 16d),
+            new Tuple2<>(parse("2020-09-11T11:12:31.66Z").toEpochMilli(), 17d)
+        );
+    }
+
+    @Test
+    public void testPT15S() {
+        double[] actuals = parameters().stream().mapToDouble(e -> {
+            Tuple2<Long, Double> increase = CounterWindow.INSTANCE.increase(
+                "test", ImmutableMap.<String, String>builder().build(), e._2,
+                Duration.parse("PT15S").getSeconds() * 1000, e._1
+            );
+            return e._2 - increase._2;
+        }).toArray();
+
+        Assertions.assertArrayEquals(new double[] {0, 1d, 1d, 1d, 1d, 0d, 2d, 1d}, actuals, 0.d);
+    }
+
+    @Test
+    public void testPT35S() {
+        double[] actuals = parameters().stream().mapToDouble(e -> {
+            Tuple2<Long, Double> increase = CounterWindow.INSTANCE.increase(
+                "test", ImmutableMap.<String, String>builder().build(), e._2,
+                Duration.parse("PT35S").getSeconds() * 1000, e._1
+            );
+            return e._2 - increase._2;
+        }).toArray();
+
+        Assertions.assertArrayEquals(new double[] {0, 1d, 2d, 2d, 2d, 0d, 3d, 3d}, actuals, 0.d);
+    }
+
+    @Test
+    public void testPT1M() {
+        double[] actuals = parameters().stream().mapToDouble(e -> {
+            Tuple2<Long, Double> increase = CounterWindow.INSTANCE.increase(
+                "test", ImmutableMap.<String, String>builder().build(), e._2,
+                Duration.parse("PT1M").getSeconds() * 1000, e._1
+            );
+            return e._2 - increase._2;
+        }).toArray();
+
+        Assertions.assertArrayEquals(new double[] {0, 1d, 2d, 3d, 4d, 0d, 5d, 5d}, actuals, 0.d);
+    }
+
+    @Test
+    public void testPT2M() {
+        double[] actuals = parameters().stream().mapToDouble(e -> {
+            Tuple2<Long, Double> increase = CounterWindow.INSTANCE.increase(
+                "test", ImmutableMap.<String, String>builder().build(), e._2,
+                Duration.parse("PT2M").getSeconds() * 1000, e._1
+            );
+            return e._2 - increase._2;
+        }).toArray();
+
+        Assertions.assertArrayEquals(new double[] {0, 1d, 2d, 3d, 4d, 0d, 1d, 2d}, actuals, 0.d);
+    }
+}
diff --git a/oap-server/server-starter/src/main/resources/application.yml b/oap-server/server-starter/src/main/resources/application.yml
index 9fa3eeb315..ef43d37de0 100644
--- a/oap-server/server-starter/src/main/resources/application.yml
+++ b/oap-server/server-starter/src/main/resources/application.yml
@@ -201,6 +201,8 @@ storage:
     enableCustomRouting: ${SW_STORAGE_ES_ENABLE_CUSTOM_ROUTING:false}
   mysql:
     properties:
+      # Please do not include the username and password in the jdbcUrl; set them through dataSource.user and dataSource.password below.
+      # If you want to hide the URL so that it cannot be seen in the configdump information, add `jdbcUrl` to keywords4MaskingSecretsOfConfig.
       jdbcUrl: ${SW_JDBC_URL:"jdbc:mysql://localhost:3306/swtest?rewriteBatchedStatements=true&allowMultiQueries=true"}
       dataSource.user: ${SW_DATA_SOURCE_USER:root}
       dataSource.password: ${SW_DATA_SOURCE_PASSWORD:root@1234}
@@ -213,6 +215,8 @@ storage:
     asyncBatchPersistentPoolSize: ${SW_STORAGE_ASYNC_BATCH_PERSISTENT_POOL_SIZE:4}
   postgresql:
     properties:
+      # Please do not include the username and password in the jdbcUrl; set them through dataSource.user and dataSource.password below.
+      # If you want to hide the URL so that it cannot be seen in the configdump information, add `jdbcUrl` to keywords4MaskingSecretsOfConfig.
       jdbcUrl: ${SW_JDBC_URL:"jdbc:postgresql://localhost:5432/skywalking"}
       dataSource.user: ${SW_DATA_SOURCE_USER:postgres}
       dataSource.password: ${SW_DATA_SOURCE_PASSWORD:123456}