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()));
+ }
+}