add TimeWindowQuantile, MetricsFilter, DefaultMetricsService (#10512)

diff --git a/dubbo-common/src/main/java/org/apache/dubbo/common/metrics/model/MetricsKey.java b/dubbo-common/src/main/java/org/apache/dubbo/common/metrics/model/MetricsKey.java
index 3c9ff02..07816a3 100644
--- a/dubbo-common/src/main/java/org/apache/dubbo/common/metrics/model/MetricsKey.java
+++ b/dubbo-common/src/main/java/org/apache/dubbo/common/metrics/model/MetricsKey.java
@@ -26,7 +26,7 @@
     METRIC_REQUESTS_TOTAL_AGG("requests.total.aggregate", "Aggregated Total Requests"),
     METRIC_REQUESTS_SUCCEED_AGG("requests.succeed.aggregate", "Aggregated Succeed Requests"),
     METRIC_REQUESTS_FAILED_AGG("requests.failed.aggregate", "Aggregated Failed Requests"),
-    METRIC_QPS_NAME("qps", "Query Per Seconds"),
+    METRIC_QPS("qps", "Query Per Seconds"),
     METRIC_RT_LAST("rt.last", "Last Response Time"),
     METRIC_RT_MIN("rt.min", "Min Response Time"),
     METRIC_RT_MAX("rt.max", "Max Response Time"),
diff --git a/dubbo-dependencies-bom/pom.xml b/dubbo-dependencies-bom/pom.xml
index fb3b390..2070914 100644
--- a/dubbo-dependencies-bom/pom.xml
+++ b/dubbo-dependencies-bom/pom.xml
@@ -130,6 +130,9 @@
         <commons_lang3_version>3.8.1</commons_lang3_version>
         <protostuff_version>1.5.9</protostuff_version>
         <envoy_api_version>0.1.23</envoy_api_version>
+        <micrometer.version>1.7.4</micrometer.version>
+        <t_digest.version>3.3</t_digest.version>
+        <prometheus_client.version>0.10.0</prometheus_client.version>
 
         <rs_api_version>2.0</rs_api_version>
         <resteasy_version>3.0.19.Final</resteasy_version>
@@ -773,6 +776,32 @@
                 <version>${snappy_java_version}</version>
                 <optional>true</optional>
             </dependency>
+            <!-- metrics related dependencies-->
+            <dependency>
+                <groupId>io.micrometer</groupId>
+                <artifactId>micrometer-core</artifactId>
+                <version>${micrometer.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>io.micrometer</groupId>
+                <artifactId>micrometer-registry-prometheus</artifactId>
+                <version>${micrometer.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>com.tdunning</groupId>
+                <artifactId>t-digest</artifactId>
+                <version>${t_digest.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>io.prometheus</groupId>
+                <artifactId>simpleclient</artifactId>
+                <version>${prometheus_client.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>io.prometheus</groupId>
+                <artifactId>simpleclient_pushgateway</artifactId>
+                <version>${prometheus_client.version}</version>
+            </dependency>
         </dependencies>
     </dependencyManagement>
 
diff --git a/dubbo-metrics/dubbo-metrics-api/pom.xml b/dubbo-metrics/dubbo-metrics-api/pom.xml
index cb6e664..445787d 100644
--- a/dubbo-metrics/dubbo-metrics-api/pom.xml
+++ b/dubbo-metrics/dubbo-metrics-api/pom.xml
@@ -35,5 +35,18 @@
             <artifactId>dubbo-common</artifactId>
             <version>${project.parent.version}</version>
         </dependency>
+        <dependency>
+            <groupId>org.apache.dubbo</groupId>
+            <artifactId>dubbo-rpc-api</artifactId>
+            <version>${project.parent.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>io.micrometer</groupId>
+            <artifactId>micrometer-core</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.tdunning</groupId>
+            <artifactId>t-digest</artifactId>
+        </dependency>
     </dependencies>
 </project>
diff --git a/dubbo-metrics/dubbo-metrics-api/src/main/java/org/apache/dubbo/metrics/AbstractMetricsReporter.java b/dubbo-metrics/dubbo-metrics-api/src/main/java/org/apache/dubbo/metrics/AbstractMetricsReporter.java
new file mode 100644
index 0000000..4e32c7e
--- /dev/null
+++ b/dubbo-metrics/dubbo-metrics-api/src/main/java/org/apache/dubbo/metrics/AbstractMetricsReporter.java
@@ -0,0 +1,169 @@
+/*
+ * 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.dubbo.metrics;
+
+import io.micrometer.core.instrument.Gauge;
+import io.micrometer.core.instrument.MeterRegistry;
+import io.micrometer.core.instrument.Tag;
+import io.micrometer.core.instrument.binder.jvm.ClassLoaderMetrics;
+import io.micrometer.core.instrument.binder.jvm.JvmGcMetrics;
+import io.micrometer.core.instrument.binder.jvm.JvmMemoryMetrics;
+import io.micrometer.core.instrument.binder.jvm.JvmThreadMetrics;
+import io.micrometer.core.instrument.binder.system.ProcessorMetrics;
+import io.micrometer.core.instrument.composite.CompositeMeterRegistry;
+import org.apache.dubbo.common.URL;
+import org.apache.dubbo.common.lang.ShutdownHookCallbacks;
+import org.apache.dubbo.common.logger.Logger;
+import org.apache.dubbo.common.logger.LoggerFactory;
+import org.apache.dubbo.common.metrics.MetricsReporter;
+import org.apache.dubbo.common.metrics.collector.DefaultMetricsCollector;
+import org.apache.dubbo.common.metrics.collector.MetricsCollector;
+import org.apache.dubbo.common.metrics.model.sample.GaugeMetricSample;
+import org.apache.dubbo.common.metrics.model.sample.MetricSample;
+import org.apache.dubbo.common.utils.NamedThreadFactory;
+import org.apache.dubbo.metrics.collector.AggregateMetricsCollector;
+import org.apache.dubbo.rpc.model.ApplicationModel;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import static org.apache.dubbo.common.constants.MetricsConstants.ENABLE_JVM_METRICS_KEY;
+
+/**
+ * AbstractMetricsReporter.
+ */
+public abstract class AbstractMetricsReporter implements MetricsReporter {
+
+    private final Logger logger = LoggerFactory.getLogger(AbstractMetricsReporter.class);
+
+    private final AtomicBoolean initialized = new AtomicBoolean(false);
+
+    protected final URL url;
+    protected final List<MetricsCollector> collectors = new ArrayList<>();
+    protected final CompositeMeterRegistry compositeRegistry = new CompositeMeterRegistry();
+
+    private final ApplicationModel applicationModel;
+    private ScheduledExecutorService collectorSyncJobExecutor = null;
+
+    private static final int DEFAULT_SCHEDULE_INITIAL_DELAY = 5;
+    private static final int DEFAULT_SCHEDULE_PERIOD = 30;
+
+    protected AbstractMetricsReporter(URL url, ApplicationModel applicationModel) {
+        this.url = url;
+        this.applicationModel = applicationModel;
+    }
+
+    @Override
+    public void init() {
+        if (initialized.compareAndSet(false, true)) {
+            addJvmMetrics();
+            initCollectors();
+            scheduleMetricsCollectorSyncJob();
+
+            doInit();
+
+            registerDubboShutdownHook();
+        }
+    }
+
+    protected void addMeterRegistry(MeterRegistry registry) {
+        compositeRegistry.add(registry);
+    }
+
+    protected ApplicationModel getApplicationModel() {
+        return applicationModel;
+    }
+
+    private void addJvmMetrics() {
+        boolean enableJvmMetrics = url.getParameter(ENABLE_JVM_METRICS_KEY, false);
+        if (enableJvmMetrics) {
+            new ClassLoaderMetrics().bindTo(compositeRegistry);
+            new JvmMemoryMetrics().bindTo(compositeRegistry);
+            new JvmGcMetrics().bindTo(compositeRegistry);
+            new ProcessorMetrics().bindTo(compositeRegistry);
+            new JvmThreadMetrics().bindTo(compositeRegistry);
+        }
+    }
+
+    private void initCollectors() {
+        applicationModel.getBeanFactory().getOrRegisterBean(AggregateMetricsCollector.class);
+
+        collectors.add(applicationModel.getBeanFactory().getBean(DefaultMetricsCollector.class));
+        collectors.add(applicationModel.getBeanFactory().getBean(AggregateMetricsCollector.class));
+    }
+
+    private void scheduleMetricsCollectorSyncJob() {
+        NamedThreadFactory threadFactory = new NamedThreadFactory("metrics-collector-sync-job", true);
+        collectorSyncJobExecutor = Executors.newScheduledThreadPool(1, threadFactory);
+        collectorSyncJobExecutor.scheduleWithFixedDelay(() -> {
+            collectors.forEach(collector -> {
+                List<MetricSample> samples = collector.collect();
+                for (MetricSample sample : samples) {
+                    try {
+                        switch (sample.getType()) {
+                            case GAUGE:
+                                GaugeMetricSample gaugeSample = (GaugeMetricSample) sample;
+                                List<Tag> tags = new ArrayList<>();
+                                gaugeSample.getTags().forEach((k, v) -> {
+                                    if (v == null) {
+                                        v = "";
+                                    }
+
+                                    tags.add(Tag.of(k, v));
+                                });
+
+                                Gauge.builder(gaugeSample.getName(), gaugeSample.getSupplier())
+                                    .description(gaugeSample.getDescription()).tags(tags).register(compositeRegistry);
+                                break;
+                            case COUNTER:
+                            case TIMER:
+                            case LONG_TASK_TIMER:
+                            case DISTRIBUTION_SUMMARY:
+                                // TODO
+                                break;
+                            default:
+                                break;
+                        }
+                    } catch (Exception e) {
+                        logger.error("error occurred when synchronize metrics collector.", e);
+                    }
+                }
+            });
+        }, DEFAULT_SCHEDULE_INITIAL_DELAY, DEFAULT_SCHEDULE_PERIOD, TimeUnit.SECONDS);
+    }
+
+    private void registerDubboShutdownHook() {
+        applicationModel.getBeanFactory().getBean(ShutdownHookCallbacks.class).addCallback(this::destroy);
+    }
+
+    public void destroy() {
+        if (collectorSyncJobExecutor != null) {
+            collectorSyncJobExecutor.shutdownNow();
+        }
+
+        doDestroy();
+    }
+
+    protected abstract void doInit();
+
+    protected abstract void doDestroy();
+}
diff --git a/dubbo-metrics/dubbo-metrics-api/src/main/java/org/apache/dubbo/metrics/AbstractMetricsReporterFactory.java b/dubbo-metrics/dubbo-metrics-api/src/main/java/org/apache/dubbo/metrics/AbstractMetricsReporterFactory.java
new file mode 100644
index 0000000..59763ed
--- /dev/null
+++ b/dubbo-metrics/dubbo-metrics-api/src/main/java/org/apache/dubbo/metrics/AbstractMetricsReporterFactory.java
@@ -0,0 +1,37 @@
+/*
+ * 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.dubbo.metrics;
+
+import org.apache.dubbo.common.metrics.MetricsReporterFactory;
+import org.apache.dubbo.rpc.model.ApplicationModel;
+
+/**
+ * AbstractMetricsReporterFactory.
+ */
+public abstract class AbstractMetricsReporterFactory implements MetricsReporterFactory {
+
+    private final ApplicationModel applicationModel;
+
+    public AbstractMetricsReporterFactory(ApplicationModel applicationModel) {
+        this.applicationModel = applicationModel;
+    }
+
+    protected ApplicationModel getApplicationModel() {
+        return applicationModel;
+    }
+}
diff --git a/dubbo-metrics/dubbo-metrics-api/src/main/java/org/apache/dubbo/metrics/aggregate/TimeWindowQuantile.java b/dubbo-metrics/dubbo-metrics-api/src/main/java/org/apache/dubbo/metrics/aggregate/TimeWindowQuantile.java
new file mode 100644
index 0000000..86249c8
--- /dev/null
+++ b/dubbo-metrics/dubbo-metrics-api/src/main/java/org/apache/dubbo/metrics/aggregate/TimeWindowQuantile.java
@@ -0,0 +1,75 @@
+/*
+ * 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.dubbo.metrics.aggregate;
+
+import com.tdunning.math.stats.TDigest;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Wrapper around TDigest.
+ * <p>
+ * Maintains a ring buffer of TDigest to provide quantiles over a sliding windows of time.
+ */
+public class TimeWindowQuantile {
+    private final double compression;
+    private final TDigest[] ringBuffer;
+    private int currentBucket;
+    private long lastRotateTimestampMillis;
+    private final long durationBetweenRotatesMillis;
+
+    public TimeWindowQuantile(double compression, int bucketNum, int timeWindowSeconds) {
+        this.compression = compression;
+        this.ringBuffer = new TDigest[bucketNum];
+        for (int i = 0; i < bucketNum; i++) {
+            this.ringBuffer[i] = TDigest.createDigest(compression);
+        }
+
+        this.currentBucket = 0;
+        this.lastRotateTimestampMillis = System.currentTimeMillis();
+        this.durationBetweenRotatesMillis = TimeUnit.SECONDS.toMillis(timeWindowSeconds) / bucketNum;
+    }
+
+    public synchronized double quantile(double q) {
+        TDigest currentBucket = rotate();
+
+        // This may return Double.NaN, and it's correct behavior.
+        // see: https://github.com/prometheus/client_golang/issues/85
+        return currentBucket.quantile(q);
+    }
+
+    public synchronized void add(double value) {
+        rotate();
+        for (TDigest bucket : ringBuffer) {
+            bucket.add(value);
+        }
+    }
+
+    private TDigest rotate() {
+        long timeSinceLastRotateMillis = System.currentTimeMillis() - lastRotateTimestampMillis;
+        while (timeSinceLastRotateMillis > durationBetweenRotatesMillis) {
+            ringBuffer[currentBucket] = TDigest.createDigest(compression);
+            if (++currentBucket >= ringBuffer.length) {
+                currentBucket = 0;
+            }
+            timeSinceLastRotateMillis -= durationBetweenRotatesMillis;
+            lastRotateTimestampMillis += durationBetweenRotatesMillis;
+        }
+        return ringBuffer[currentBucket];
+    }
+}
diff --git a/dubbo-metrics/dubbo-metrics-api/src/main/java/org/apache/dubbo/metrics/collector/AggregateMetricsCollector.java b/dubbo-metrics/dubbo-metrics-api/src/main/java/org/apache/dubbo/metrics/collector/AggregateMetricsCollector.java
new file mode 100644
index 0000000..b473d1f
--- /dev/null
+++ b/dubbo-metrics/dubbo-metrics-api/src/main/java/org/apache/dubbo/metrics/collector/AggregateMetricsCollector.java
@@ -0,0 +1,151 @@
+/*
+ * 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.dubbo.metrics.collector;
+
+import org.apache.dubbo.common.metrics.collector.DefaultMetricsCollector;
+import org.apache.dubbo.common.metrics.collector.MetricsCollector;
+import org.apache.dubbo.common.metrics.event.MetricsEvent;
+import org.apache.dubbo.common.metrics.event.RTEvent;
+import org.apache.dubbo.common.metrics.event.RequestEvent;
+import org.apache.dubbo.common.metrics.listener.MetricsListener;
+import org.apache.dubbo.common.metrics.model.MethodMetric;
+import org.apache.dubbo.common.metrics.model.MetricsKey;
+import org.apache.dubbo.common.metrics.model.sample.GaugeMetricSample;
+import org.apache.dubbo.common.metrics.model.sample.MetricSample;
+import org.apache.dubbo.config.MetricsConfig;
+import org.apache.dubbo.config.context.ConfigManager;
+import org.apache.dubbo.config.nested.AggregationConfig;
+import org.apache.dubbo.metrics.aggregate.TimeWindowCounter;
+import org.apache.dubbo.metrics.aggregate.TimeWindowQuantile;
+import org.apache.dubbo.rpc.model.ApplicationModel;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import static org.apache.dubbo.common.metrics.model.MetricsCategory.REQUESTS;
+import static org.apache.dubbo.common.metrics.model.MetricsCategory.QPS;
+import static org.apache.dubbo.common.metrics.model.MetricsCategory.RT;
+
+/**
+ * Aggregation metrics collector implementation of {@link MetricsCollector}.
+ * This collector only enabled when metrics aggregation config is enabled.
+ */
+public class AggregateMetricsCollector implements MetricsCollector, MetricsListener {
+    private int bucketNum;
+    private int timeWindowSeconds;
+
+    private final Map<MethodMetric, TimeWindowCounter> totalRequests = new ConcurrentHashMap<>();
+    private final Map<MethodMetric, TimeWindowCounter> succeedRequests = new ConcurrentHashMap<>();
+    private final Map<MethodMetric, TimeWindowCounter> failedRequests = new ConcurrentHashMap<>();
+    private final Map<MethodMetric, TimeWindowCounter> qps = new ConcurrentHashMap<>();
+    private final Map<MethodMetric, TimeWindowQuantile> rt = new ConcurrentHashMap<>();
+
+    private final ApplicationModel applicationModel;
+
+    private static final Integer DEFAULT_COMPRESSION = 100;
+    private static final Integer DEFAULT_BUCKET_NUM = 10;
+    private static final Integer DEFAULT_TIME_WINDOW_SECONDS = 120;
+
+    public AggregateMetricsCollector(ApplicationModel applicationModel) {
+        this.applicationModel = applicationModel;
+        ConfigManager configManager = applicationModel.getApplicationConfigManager();
+        MetricsConfig config = configManager.getMetrics().orElse(null);
+        if (config != null && config.getAggregation() != null && Boolean.TRUE.equals(config.getAggregation().getEnabled())) {
+            // only registered when aggregation is enabled.
+            registerListener();
+
+            AggregationConfig aggregation = config.getAggregation();
+            this.bucketNum = aggregation.getBucketNum() == null ? DEFAULT_BUCKET_NUM : aggregation.getBucketNum();
+            this.timeWindowSeconds = aggregation.getTimeWindowSeconds() == null ? DEFAULT_TIME_WINDOW_SECONDS : aggregation.getTimeWindowSeconds();
+        }
+    }
+
+    private void registerListener() {
+        applicationModel.getBeanFactory().getBean(DefaultMetricsCollector.class).addListener(this);
+    }
+
+    @Override
+    public void onEvent(MetricsEvent event) {
+        if (event instanceof RTEvent) {
+            onRTEvent((RTEvent) event);
+        } else if (event instanceof RequestEvent) {
+            onRequestEvent((RequestEvent) event);
+        }
+    }
+
+    private void onRTEvent(RTEvent event) {
+        MethodMetric metric = (MethodMetric) event.getSource();
+        Long responseTime = event.getRt();
+        TimeWindowQuantile quantile = rt.computeIfAbsent(metric, k -> new TimeWindowQuantile(DEFAULT_COMPRESSION, bucketNum, timeWindowSeconds));
+        quantile.add(responseTime);
+    }
+
+    private void onRequestEvent(RequestEvent event) {
+        MethodMetric metric = (MethodMetric) event.getSource();
+        RequestEvent.Type type = event.getType();
+        TimeWindowCounter counter = null;
+        switch (type) {
+            case TOTAL:
+                counter = totalRequests.computeIfAbsent(metric, k -> new TimeWindowCounter(bucketNum, timeWindowSeconds));
+                TimeWindowCounter qpsCounter = qps.computeIfAbsent(metric, k -> new TimeWindowCounter(bucketNum, timeWindowSeconds));
+                qpsCounter.increment();
+                break;
+            case SUCCEED:
+                counter = succeedRequests.computeIfAbsent(metric, k -> new TimeWindowCounter(bucketNum, timeWindowSeconds));
+                break;
+            case FAILED:
+                counter = failedRequests.computeIfAbsent(metric, k -> new TimeWindowCounter(bucketNum, timeWindowSeconds));
+                break;
+            default:
+                break;
+        }
+
+        if (counter != null) {
+            counter.increment();
+        }
+    }
+
+    @Override
+    public List<MetricSample> collect() {
+        List<MetricSample> list = new ArrayList<>();
+        collectRequests(list);
+        collectQPS(list);
+        collectRT(list);
+
+        return list;
+    }
+
+    private void collectRequests(List<MetricSample> list) {
+        totalRequests.forEach((k, v) -> list.add(new GaugeMetricSample(MetricsKey.METRIC_REQUESTS_TOTAL_AGG, k.getTags(), REQUESTS, v::get)));
+        succeedRequests.forEach((k, v) -> list.add(new GaugeMetricSample(MetricsKey.METRIC_REQUESTS_SUCCEED_AGG, k.getTags(), REQUESTS, v::get)));
+        failedRequests.forEach((k, v) -> list.add(new GaugeMetricSample(MetricsKey.METRIC_REQUESTS_FAILED_AGG, k.getTags(), REQUESTS, v::get)));
+    }
+
+    private void collectQPS(List<MetricSample> list) {
+        qps.forEach((k, v) -> list.add(new GaugeMetricSample(MetricsKey.METRIC_QPS, k.getTags(), QPS, () -> v.get() / v.bucketLivedSeconds())));
+    }
+
+    private void collectRT(List<MetricSample> list) {
+        rt.forEach((k, v) -> {
+            list.add(new GaugeMetricSample(MetricsKey.METRIC_RT_P99, k.getTags(), RT, () -> v.quantile(0.99)));
+            list.add(new GaugeMetricSample(MetricsKey.METRIC_RT_P95, k.getTags(), RT, () -> v.quantile(0.95)));
+        });
+    }
+}
diff --git a/dubbo-metrics/dubbo-metrics-api/src/main/java/org/apache/dubbo/metrics/filter/MetricsFilter.java b/dubbo-metrics/dubbo-metrics-api/src/main/java/org/apache/dubbo/metrics/filter/MetricsFilter.java
new file mode 100644
index 0000000..031f8ef
--- /dev/null
+++ b/dubbo-metrics/dubbo-metrics-api/src/main/java/org/apache/dubbo/metrics/filter/MetricsFilter.java
@@ -0,0 +1,85 @@
+/*
+ * 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.dubbo.metrics.filter;
+
+import org.apache.dubbo.common.extension.Activate;
+import org.apache.dubbo.common.metrics.collector.DefaultMetricsCollector;
+import org.apache.dubbo.rpc.Filter;
+import org.apache.dubbo.rpc.Invocation;
+import org.apache.dubbo.rpc.Invoker;
+import org.apache.dubbo.rpc.Result;
+import org.apache.dubbo.rpc.RpcException;
+import org.apache.dubbo.rpc.model.ApplicationModel;
+import org.apache.dubbo.rpc.model.ScopeModelAware;
+
+import static org.apache.dubbo.common.constants.CommonConstants.PROVIDER;
+
+@Activate(group = PROVIDER, order = -1)
+public class MetricsFilter implements Filter, ScopeModelAware {
+
+    private DefaultMetricsCollector collector = null;
+
+    private ApplicationModel applicationModel;
+
+    @Override
+    public void setApplicationModel(ApplicationModel applicationModel) {
+        this.applicationModel = applicationModel;
+
+        collector = applicationModel.getBeanFactory().getBean(DefaultMetricsCollector.class);
+    }
+
+    @Override
+    public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
+        if (collector == null || !collector.isCollectEnabled()) {
+            return invoker.invoke(invocation);
+        }
+
+        String serviceUniqueName = invocation.getTargetServiceUniqueName();
+        String methodName = invocation.getMethodName();
+        String group = null;
+        String interfaceAndVersion;
+        String[] arr = serviceUniqueName.split("/");
+        if (arr.length == 2) {
+            group = arr[0];
+            interfaceAndVersion = arr[1];
+        } else {
+            interfaceAndVersion = arr[0];
+        }
+
+        String[] ivArr = interfaceAndVersion.split(":");
+        String interfaceName = ivArr[0];
+        String version = ivArr.length == 2 ? ivArr[1] : null;
+        collector.increaseTotalRequests(interfaceName, methodName, group, version);
+        collector.increaseProcessingRequests(interfaceName, methodName, group, version);
+
+        Long startTime = System.currentTimeMillis();
+        try {
+            Result invoke = invoker.invoke(invocation);
+            collector.increaseSucceedRequests(interfaceName, methodName, group, version);
+            return invoke;
+        } catch (RpcException e) {
+            collector.increaseFailedRequests(interfaceName, methodName, group, version);
+            throw e;
+        } finally {
+            Long endTime = System.currentTimeMillis();
+            Long rt = endTime - startTime;
+            collector.addRT(interfaceName, methodName, group, version, rt);
+            collector.decreaseProcessingRequests(interfaceName, methodName, group, version);
+        }
+    }
+}
diff --git a/dubbo-metrics/dubbo-metrics-api/src/main/java/org/apache/dubbo/metrics/service/DefaultMetricsService.java b/dubbo-metrics/dubbo-metrics-api/src/main/java/org/apache/dubbo/metrics/service/DefaultMetricsService.java
new file mode 100644
index 0000000..c91e5f3
--- /dev/null
+++ b/dubbo-metrics/dubbo-metrics-api/src/main/java/org/apache/dubbo/metrics/service/DefaultMetricsService.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.dubbo.metrics.service;
+
+import org.apache.dubbo.common.metrics.collector.DefaultMetricsCollector;
+import org.apache.dubbo.common.metrics.collector.MetricsCollector;
+import org.apache.dubbo.common.metrics.model.MetricsCategory;
+import org.apache.dubbo.common.metrics.model.sample.GaugeMetricSample;
+import org.apache.dubbo.common.metrics.model.sample.MetricSample;
+import org.apache.dubbo.common.metrics.service.MetricsEntity;
+import org.apache.dubbo.common.metrics.service.MetricsService;
+import org.apache.dubbo.metrics.collector.AggregateMetricsCollector;
+import org.apache.dubbo.rpc.model.ApplicationModel;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Default implementation of {@link MetricsService}
+ */
+public class DefaultMetricsService implements MetricsService {
+
+    protected final List<MetricsCollector> collectors = new ArrayList<>();
+
+    private final ApplicationModel applicationModel;
+
+    public DefaultMetricsService(ApplicationModel applicationModel) {
+        this.applicationModel = applicationModel;
+        collectors.add(applicationModel.getBeanFactory().getBean(DefaultMetricsCollector.class));
+        collectors.add(applicationModel.getBeanFactory().getBean(AggregateMetricsCollector.class));
+    }
+
+    @Override
+    public Map<MetricsCategory, List<MetricsEntity>> getMetricsByCategories(List<MetricsCategory> categories) {
+        return getMetricsByCategories(null, categories);
+    }
+
+    @Override
+    public Map<MetricsCategory, List<MetricsEntity>> getMetricsByCategories(String serviceUniqueName, List<MetricsCategory> categories) {
+        return getMetricsByCategories(serviceUniqueName, null, null, categories);
+    }
+
+    @Override
+    public Map<MetricsCategory, List<MetricsEntity>> getMetricsByCategories(String serviceUniqueName, String methodName, Class<?>[] parameterTypes, List<MetricsCategory> categories) {
+        Map<MetricsCategory, List<MetricsEntity>> result = new HashMap<>();
+        for (MetricsCollector collector : collectors) {
+            List<MetricSample> samples = collector.collect();
+            for (MetricSample sample : samples) {
+                if (categories.contains(sample.getCategory())) {
+                    List<MetricsEntity> entities = result.computeIfAbsent(sample.getCategory(), k -> new ArrayList<>());
+                    entities.add(sampleToEntity(sample));
+                }
+            }
+        }
+
+        return result;
+    }
+
+    private MetricsEntity sampleToEntity(MetricSample sample) {
+        MetricsEntity entity = new MetricsEntity();
+
+        entity.setName(sample.getName());
+        entity.setTags(sample.getTags());
+        entity.setCategory(sample.getCategory());
+        switch (sample.getType()) {
+            case GAUGE:
+                GaugeMetricSample gaugeSample = (GaugeMetricSample) sample;
+                entity.setValue(gaugeSample.getSupplier().get());
+                break;
+            case COUNTER:
+            case LONG_TASK_TIMER:
+            case TIMER:
+            case DISTRIBUTION_SUMMARY:
+            default:
+                break;
+        }
+
+        return entity;
+    }
+}
diff --git a/dubbo-metrics/dubbo-metrics-api/src/test/java/org/apache/dubbo/metrics/aggregate/TimeWindowQuantileTest.java b/dubbo-metrics/dubbo-metrics-api/src/test/java/org/apache/dubbo/metrics/aggregate/TimeWindowQuantileTest.java
new file mode 100644
index 0000000..f3364be
--- /dev/null
+++ b/dubbo-metrics/dubbo-metrics-api/src/test/java/org/apache/dubbo/metrics/aggregate/TimeWindowQuantileTest.java
@@ -0,0 +1,37 @@
+/*
+ * 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.dubbo.metrics.aggregate;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class TimeWindowQuantileTest {
+
+    @Test
+    public void test() throws Exception {
+        TimeWindowQuantile quantile = new TimeWindowQuantile(100, 12, 1);
+        for (int i = 1; i <= 100; i++) {
+            quantile.add(i);
+        }
+
+        Assertions.assertEquals(quantile.quantile(0.01), 2);
+        Assertions.assertEquals(quantile.quantile(0.99), 100);
+        Thread.sleep(1000);
+        Assertions.assertEquals(quantile.quantile(0.99), Double.NaN);
+    }
+}
diff --git a/dubbo-metrics/dubbo-metrics-api/src/test/java/org/apache/dubbo/metrics/collector/AggregateMetricsCollectorTest.java b/dubbo-metrics/dubbo-metrics-api/src/test/java/org/apache/dubbo/metrics/collector/AggregateMetricsCollectorTest.java
new file mode 100644
index 0000000..308e238
--- /dev/null
+++ b/dubbo-metrics/dubbo-metrics-api/src/test/java/org/apache/dubbo/metrics/collector/AggregateMetricsCollectorTest.java
@@ -0,0 +1,130 @@
+/*
+ * 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.dubbo.metrics.collector;
+
+import org.apache.dubbo.common.metrics.collector.DefaultMetricsCollector;
+import org.apache.dubbo.common.metrics.model.sample.GaugeMetricSample;
+import org.apache.dubbo.common.metrics.model.sample.MetricSample;
+import org.apache.dubbo.config.ApplicationConfig;
+import org.apache.dubbo.config.MetricsConfig;
+import org.apache.dubbo.config.nested.AggregationConfig;
+import org.apache.dubbo.rpc.model.ApplicationModel;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import static org.apache.dubbo.common.constants.MetricsConstants.*;
+
+public class AggregateMetricsCollectorTest {
+
+    private ApplicationModel applicationModel;
+    private DefaultMetricsCollector defaultCollector;
+
+    private String interfaceName;
+    private String methodName;
+    private String group;
+    private String version;
+
+    @BeforeEach
+    public void setup() {
+        ApplicationConfig config = new ApplicationConfig();
+        config.setName("MockMetrics");
+
+        applicationModel = ApplicationModel.defaultModel();
+        applicationModel.getApplicationConfigManager().setApplication(config);
+
+        defaultCollector = new DefaultMetricsCollector(applicationModel);
+        defaultCollector.setCollectEnabled(true);
+        MetricsConfig metricsConfig = new MetricsConfig();
+        AggregationConfig aggregationConfig = new AggregationConfig();
+        aggregationConfig.setEnabled(true);
+        aggregationConfig.setBucketNum(12);
+        aggregationConfig.setTimeWindowSeconds(120);
+        metricsConfig.setAggregation(aggregationConfig);
+        applicationModel.getApplicationConfigManager().setMetrics(metricsConfig);
+        applicationModel.getBeanFactory().registerBean(defaultCollector);
+
+        interfaceName = "org.apache.dubbo.MockInterface";
+        methodName = "mockMethod";
+        group = "mockGroup";
+        version = "1.0.0";
+    }
+
+    @AfterEach
+    public void teardown() {
+        applicationModel.destroy();
+    }
+
+    @Test
+    public void testRequestsMetrics() {
+        AggregateMetricsCollector collector = new AggregateMetricsCollector(applicationModel);
+        defaultCollector.increaseTotalRequests(interfaceName, methodName, group, version);
+        defaultCollector.increaseSucceedRequests(interfaceName, methodName, group, version);
+        defaultCollector.increaseFailedRequests(interfaceName, methodName, group, version);
+
+        List<MetricSample> samples = collector.collect();
+        for (MetricSample sample : samples) {
+            Map<String, String> tags = sample.getTags();
+
+            Assertions.assertEquals(tags.get(TAG_INTERFACE_KEY), interfaceName);
+            Assertions.assertEquals(tags.get(TAG_METHOD_KEY), methodName);
+            Assertions.assertEquals(tags.get(TAG_GROUP_KEY), group);
+            Assertions.assertEquals(tags.get(TAG_VERSION_KEY), version);
+        }
+
+        samples = collector.collect();
+        Map<String, Long> sampleMap = samples.stream().collect(Collectors.toMap(MetricSample::getName, k -> {
+            Number number = ((GaugeMetricSample) k).getSupplier().get();
+            return number.longValue();
+        }));
+
+        Assertions.assertEquals(sampleMap.get("requests.total.aggregate"), 1L);
+        Assertions.assertEquals(sampleMap.get("requests.succeed.aggregate"), 1L);
+        Assertions.assertEquals(sampleMap.get("requests.failed.aggregate"), 1L);
+        Assertions.assertTrue(sampleMap.containsKey("qps"));
+    }
+
+    @Test
+    public void testRTMetrics() {
+        AggregateMetricsCollector collector = new AggregateMetricsCollector(applicationModel);
+        defaultCollector.addRT(interfaceName, methodName, group, version, 10L);
+
+        List<MetricSample> samples = collector.collect();
+        for (MetricSample sample : samples) {
+            Map<String, String> tags = sample.getTags();
+
+            Assertions.assertEquals(tags.get(TAG_INTERFACE_KEY), interfaceName);
+            Assertions.assertEquals(tags.get(TAG_METHOD_KEY), methodName);
+            Assertions.assertEquals(tags.get(TAG_GROUP_KEY), group);
+            Assertions.assertEquals(tags.get(TAG_VERSION_KEY), version);
+        }
+
+        Map<String, Long> sampleMap = samples.stream().collect(Collectors.toMap(MetricSample::getName, k -> {
+            Number number = ((GaugeMetricSample) k).getSupplier().get();
+            return number.longValue();
+        }));
+
+        Assertions.assertTrue(sampleMap.containsKey("rt.p99"));
+        Assertions.assertTrue(sampleMap.containsKey("rt.p95"));
+    }
+}
diff --git a/dubbo-metrics/dubbo-metrics-api/src/test/java/org/apache/dubbo/metrics/filter/MetricsFilterTest.java b/dubbo-metrics/dubbo-metrics-api/src/test/java/org/apache/dubbo/metrics/filter/MetricsFilterTest.java
new file mode 100644
index 0000000..5e65180
--- /dev/null
+++ b/dubbo-metrics/dubbo-metrics-api/src/test/java/org/apache/dubbo/metrics/filter/MetricsFilterTest.java
@@ -0,0 +1,199 @@
+/*
+ * 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.dubbo.metrics.filter;
+
+import org.apache.dubbo.common.metrics.collector.DefaultMetricsCollector;
+import org.apache.dubbo.common.metrics.model.sample.MetricSample;
+import org.apache.dubbo.config.ApplicationConfig;
+import org.apache.dubbo.rpc.AppResponse;
+import org.apache.dubbo.rpc.Invoker;
+import org.apache.dubbo.rpc.RpcException;
+import org.apache.dubbo.rpc.RpcInvocation;
+import org.apache.dubbo.rpc.model.ApplicationModel;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+import static org.apache.dubbo.common.constants.MetricsConstants.*;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.Mockito.mock;
+
+public class MetricsFilterTest {
+
+    private ApplicationModel applicationModel;
+    private MetricsFilter filter;
+    private DefaultMetricsCollector collector;
+    private RpcInvocation invocation;
+    private final Invoker<?> invoker = mock(Invoker.class);
+
+    private static final String INTERFACE_NAME = "org.apache.dubbo.MockInterface";
+    private static final String METHOD_NAME = "mockMethod";
+    private static final String GROUP = "mockGroup";
+    private static final String VERSION = "1.0.0";
+
+    @BeforeEach
+    public void setup() {
+        ApplicationConfig config = new ApplicationConfig();
+        config.setName("MockMetrics");
+
+        applicationModel = ApplicationModel.defaultModel();
+        applicationModel.getApplicationConfigManager().setApplication(config);
+
+        invocation = new RpcInvocation();
+        filter =  new MetricsFilter();
+
+        collector = applicationModel.getBeanFactory().getOrRegisterBean(DefaultMetricsCollector.class);
+        filter.setApplicationModel(applicationModel);
+    }
+
+    @AfterEach
+    public void teardown() {
+        applicationModel.destroy();
+    }
+
+    @Test
+    public void testCollectDisabled() {
+        given(invoker.invoke(invocation)).willReturn(new AppResponse("success"));
+
+        filter.invoke(invoker, invocation);
+        Map<String, MetricSample> metricsMap = getMetricsMap();
+        Assertions.assertTrue(metricsMap.isEmpty());
+    }
+
+    @Test
+    public void testFailedRequests() {
+        collector.setCollectEnabled(true);
+        given(invoker.invoke(invocation)).willThrow(new RpcException("failed"));
+        initParam();
+
+        try {
+            filter.invoke(invoker, invocation);
+        } catch (Exception ignore) {
+
+        }
+
+        Map<String, MetricSample> metricsMap = getMetricsMap();
+        Assertions.assertTrue(metricsMap.containsKey("requests.failed"));
+        Assertions.assertFalse(metricsMap.containsKey("requests.succeed"));
+
+        MetricSample sample = metricsMap.get("requests.failed");
+        Map<String, String> tags = sample.getTags();
+
+        Assertions.assertEquals(tags.get(TAG_INTERFACE_KEY), INTERFACE_NAME);
+        Assertions.assertEquals(tags.get(TAG_METHOD_KEY), METHOD_NAME);
+        Assertions.assertEquals(tags.get(TAG_GROUP_KEY), GROUP);
+        Assertions.assertEquals(tags.get(TAG_VERSION_KEY), VERSION);
+    }
+
+    @Test
+    public void testSucceedRequests() {
+        collector.setCollectEnabled(true);
+        given(invoker.invoke(invocation)).willReturn(new AppResponse("success"));
+        initParam();
+
+        filter.invoke(invoker, invocation);
+        Map<String, MetricSample> metricsMap = getMetricsMap();
+        Assertions.assertFalse(metricsMap.containsKey("requests.failed"));
+        Assertions.assertTrue(metricsMap.containsKey("requests.succeed"));
+
+        MetricSample sample = metricsMap.get("requests.succeed");
+        Map<String, String> tags = sample.getTags();
+
+        Assertions.assertEquals(tags.get(TAG_INTERFACE_KEY), INTERFACE_NAME);
+        Assertions.assertEquals(tags.get(TAG_METHOD_KEY), METHOD_NAME);
+        Assertions.assertEquals(tags.get(TAG_GROUP_KEY), GROUP);
+        Assertions.assertEquals(tags.get(TAG_VERSION_KEY), VERSION);
+    }
+
+    @Test
+    public void testMissingGroup() {
+        collector.setCollectEnabled(true);
+        given(invoker.invoke(invocation)).willReturn(new AppResponse("success"));
+        invocation.setTargetServiceUniqueName(INTERFACE_NAME + ":" + VERSION);
+        invocation.setMethodName(METHOD_NAME);
+        invocation.setParameterTypes(new Class[]{ String.class });
+
+        filter.invoke(invoker, invocation);
+        Map<String, MetricSample> metricsMap = getMetricsMap();
+
+        MetricSample sample = metricsMap.get("requests.succeed");
+        Map<String, String> tags = sample.getTags();
+
+        Assertions.assertEquals(tags.get(TAG_INTERFACE_KEY), INTERFACE_NAME);
+        Assertions.assertEquals(tags.get(TAG_METHOD_KEY), METHOD_NAME);
+        Assertions.assertNull(tags.get(TAG_GROUP_KEY));
+        Assertions.assertEquals(tags.get(TAG_VERSION_KEY), VERSION);
+    }
+
+    @Test
+    public void testMissingVersion() {
+        collector.setCollectEnabled(true);
+        given(invoker.invoke(invocation)).willReturn(new AppResponse("success"));
+        invocation.setTargetServiceUniqueName(GROUP + "/" + INTERFACE_NAME);
+        invocation.setMethodName(METHOD_NAME);
+        invocation.setParameterTypes(new Class[]{ String.class });
+
+        filter.invoke(invoker, invocation);
+        Map<String, MetricSample> metricsMap = getMetricsMap();
+
+        MetricSample sample = metricsMap.get("requests.succeed");
+        Map<String, String> tags = sample.getTags();
+
+        Assertions.assertEquals(tags.get(TAG_INTERFACE_KEY), INTERFACE_NAME);
+        Assertions.assertEquals(tags.get(TAG_METHOD_KEY), METHOD_NAME);
+        Assertions.assertEquals(tags.get(TAG_GROUP_KEY), GROUP);
+        Assertions.assertNull(tags.get(TAG_VERSION_KEY));
+    }
+
+    @Test
+    public void testMissingGroupAndVersion() {
+        collector.setCollectEnabled(true);
+        given(invoker.invoke(invocation)).willReturn(new AppResponse("success"));
+        invocation.setTargetServiceUniqueName(INTERFACE_NAME);
+        invocation.setMethodName(METHOD_NAME);
+        invocation.setParameterTypes(new Class[]{ String.class });
+
+        filter.invoke(invoker, invocation);
+        Map<String, MetricSample> metricsMap = getMetricsMap();
+
+        MetricSample sample = metricsMap.get("requests.succeed");
+        Map<String, String> tags = sample.getTags();
+
+        Assertions.assertEquals(tags.get(TAG_INTERFACE_KEY), INTERFACE_NAME);
+        Assertions.assertEquals(tags.get(TAG_METHOD_KEY), METHOD_NAME);
+        Assertions.assertNull(tags.get(TAG_GROUP_KEY));
+        Assertions.assertNull(tags.get(TAG_VERSION_KEY));
+    }
+
+    private void initParam() {
+        invocation.setTargetServiceUniqueName(GROUP + "/" + INTERFACE_NAME + ":" + VERSION);
+        invocation.setMethodName(METHOD_NAME);
+        invocation.setParameterTypes(new Class[]{ String.class });
+    }
+
+    private Map<String, MetricSample> getMetricsMap() {
+        List<MetricSample> samples = collector.collect();
+        return samples.stream().collect(Collectors.toMap(MetricSample::getName, Function.identity()));
+    }
+}