Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

package org.apache.dubbo.samples.metrics.prometheus.consumer;

import org.apache.dubbo.samples.metrics.prometheus.util.PrometheusMetricAssert;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
Expand All @@ -40,89 +41,96 @@
public class ConsumerMetricsIT {
private static final Logger logger = LoggerFactory.getLogger(ConsumerMetricsIT.class);


private final String port = "20889";

private final List<String> metricKeys = new ArrayList();
private final List<MetricExpectation> metricExpectations = new ArrayList<>();

@Before
public void setUp() {
add("dubbo_application_info_total");
addCounter("dubbo.application.info.total");

//config
add("dubbo_configcenter_total");

add("dubbo_provider_rt_milliseconds_p99");
add("dubbo_provider_requests_total_aggregate");
add("dubbo_provider_rt_milliseconds_p90");
add("dubbo_provider_rt_max_milliseconds_aggregate");
add("dubbo_provider_requests_succeed_total");
add("dubbo_provider_requests_business_failed_aggregate");
add("dubbo_provider_requests_business_failed_total");
add("dubbo_provider_requests_total");
add("dubbo_provider_requests_failed_service_unavailable_total_aggregate");
add("dubbo_provider_requests_limit_aggregate");
add("dubbo_provider_qps_total");
add("dubbo_provider_rt_milliseconds_p50");
add("dubbo_provider_requests_processing_total");
add("dubbo_provider_requests_failed_total");
add("dubbo_provider_requests_succeed_aggregate");
add("dubbo_provider_rt_milliseconds_p95");
add("dubbo_provider_requests_failed_aggregate");
add("dubbo_provider_rt_avg_milliseconds_aggregate");
add("dubbo_provider_requests_failed_total_aggregate");
add("dubbo_provider_requests_failed_network_total_aggregate");
add("dubbo_provider_rt_min_milliseconds_aggregate");
add("dubbo_provider_requests_failed_codec_total_aggregate");
add("dubbo_provider_requests_timeout_failed_aggregate");
addGauge("dubbo.configcenter.total");

addGauge("dubbo.provider.rt.milliseconds.p99");
addGauge("dubbo.provider.requests.total.aggregate");
addGauge("dubbo.provider.rt.milliseconds.p90");
addGauge("dubbo.provider.rt.max.milliseconds.aggregate");
addCounter("dubbo.provider.requests.succeed.total");
addGauge("dubbo.provider.requests.business.failed.aggregate");
addCounter("dubbo.provider.requests.business.failed.total");
addCounter("dubbo.provider.requests.total");
addGauge("dubbo.provider.requests.failed.service.unavailable.total.aggregate");
addGauge("dubbo.provider.requests.limit.aggregate");
addGauge("dubbo.provider.qps.total");
addGauge("dubbo.provider.rt.milliseconds.p50");
addGauge("dubbo.provider.requests.processing.total");
addCounter("dubbo.provider.requests.failed.total");
addGauge("dubbo.provider.requests.succeed.aggregate");
addGauge("dubbo.provider.rt.milliseconds.p95");
addGauge("dubbo.provider.requests.failed.aggregate");
addGauge("dubbo.provider.rt.avg.milliseconds.aggregate");
addGauge("dubbo.provider.requests.failed.total.aggregate");
addGauge("dubbo.provider.requests.failed.network.total.aggregate");
addGauge("dubbo.provider.rt.min.milliseconds.aggregate");
addGauge("dubbo.provider.requests.failed.codec.total.aggregate");
addGauge("dubbo.provider.requests.timeout.failed.aggregate");

//consumer
add("dubbo_consumer_requests_failed_service_unavailable_total");
addCounter("dubbo.consumer.requests.failed.service.unavailable.total");

//register
add("dubbo_registry_subscribe_num_total");
add("dubbo_registry_register_requests_succeed_total");
add("dubbo_register_rt_milliseconds_last");
add("dubbo_registry_register_requests_total");
add("dubbo_register_rt_milliseconds_avg");
add("dubbo_registry_subscribe_num_succeed_total");
add("dubbo_register_rt_milliseconds_max");
add("dubbo_register_rt_milliseconds_min");
add("dubbo_register_rt_milliseconds_sum");
add("dubbo_registry_notify_requests_total");
add("dubbo_registry_register_requests_failed_total");
add("dubbo_registry_subscribe_num_failed_total");
add("dubbo_register_rt_milliseconds_max");
add("dubbo_registry_register_requests_succeed_total");
addGauge("dubbo.registry.subscribe.num.total");
addGauge("dubbo.registry.register.requests.succeed.total");
addGauge("dubbo.register.rt.milliseconds.last");
addGauge("dubbo.registry.register.requests.total");
addGauge("dubbo.register.rt.milliseconds.avg");
addGauge("dubbo.registry.subscribe.num.succeed.total");
addGauge("dubbo.register.rt.milliseconds.max");
addGauge("dubbo.register.rt.milliseconds.min");
addGauge("dubbo.register.rt.milliseconds.sum");
addGauge("dubbo.registry.notify.requests.total");
addGauge("dubbo.registry.register.requests.failed.total");
addGauge("dubbo.registry.subscribe.num.failed.total");
addGauge("dubbo.register.rt.milliseconds.max");
addGauge("dubbo.registry.register.requests.succeed.total");

//metadata
add("dubbo_metadata_subscribe_num_failed_total");
add("dubbo_metadata_push_num_failed_total");
add("dubbo_metadata_subscribe_num_total");
add("dubbo_metadata_subscribe_num_succeed_total");
add("dubbo_metadata_push_num_succeed_total");
add("dubbo_metadata_push_num_total");
addGauge("dubbo.metadata.subscribe.num.failed.total");
addGauge("dubbo.metadata.push.num.failed.total");
addGauge("dubbo.metadata.subscribe.num.total");
addGauge("dubbo.metadata.subscribe.num.succeed.total");
addGauge("dubbo.metadata.push.num.succeed.total");
addGauge("dubbo.metadata.push.num.total");

//thread
add("dubbo_thread_pool_thread_count");
add("dubbo_thread_pool_largest_size");
add("dubbo_thread_pool_active_size");
add("dubbo_thread_pool_queue_size");
add("dubbo_thread_pool_core_size");
add("dubbo_thread_pool_max_size");
addGauge("dubbo.thread.pool.thread.count");
addGauge("dubbo.thread.pool.largest.size");
addGauge("dubbo.thread.pool.active.size");
addGauge("dubbo.thread.pool.queue.size");
addGauge("dubbo.thread.pool.core.size");
addGauge("dubbo.thread.pool.max.size");
}

// Use explicit kind only when it changes cross-registry compatible names.
// Example: `dubbo.application.info.total` resolves to `dubbo_application_total` on new clients.
private void addCounter(String rawName) {
metricExpectations.add(new MetricExpectation(rawName, PrometheusMetricAssert.MetricKind.COUNTER));
}

private void add(String dubboStoreProviderInterfaceRtMillisecondsMin) {
metricKeys.add(dubboStoreProviderInterfaceRtMillisecondsMin);
// Example: `...qps.total` is exposed without `_total` on new clients when treated as a gauge.
private void addGauge(String rawName) {
metricExpectations.add(new MetricExpectation(rawName, PrometheusMetricAssert.MetricKind.GAUGE));
}


@Test
public void test() throws Exception {
new EmbeddedZooKeeper(2181, false).start();

ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("dubbo-demo-consumer.xml");

List<String> notExistedList = new ArrayList<>();
List<MetricExpectation> notExistedList = new ArrayList<>();
HttpGet request = new HttpGet("http://localhost:" + port + "/metrics");
try (CloseableHttpClient client = HttpClients.createDefault()) {
// retry 3 times as all metrics data are not collected immediately.
Expand All @@ -132,9 +140,11 @@ public void test() throws Exception {
InputStream inputStream = response.getEntity().getContent();
String text = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8)).lines()
.collect(Collectors.joining("\n"));
for (String metricKey : metricKeys) {
if (!text.contains(metricKey)) {
notExistedList.add(metricKey);
for (MetricExpectation expectation : metricExpectations) {
try {
PrometheusMetricAssert.assertDubboMetricExposed(text, expectation.rawName, expectation.kind);
} catch (AssertionError ignored) {
notExistedList.add(expectation);
}
}
if (notExistedList.isEmpty()) {
Expand All @@ -146,10 +156,19 @@ public void test() throws Exception {
Assert.fail(e.getMessage());
}
context.stop();
for (String metricKey : notExistedList) {
logger.error("metric key:{} doesn't exist", metricKey);
for (MetricExpectation metric : notExistedList) {
logger.error("metric rawName:{} with kind:{} doesn't exist", metric.rawName, metric.kind);
}
Assert.assertTrue(notExistedList.isEmpty());
}

private static final class MetricExpectation {
private final String rawName;
private final PrometheusMetricAssert.MetricKind kind;

private MetricExpectation(String rawName, PrometheusMetricAssert.MetricKind kind) {
this.rawName = rawName;
this.kind = kind;
}
}
}
Original file line number Diff line number Diff line change
@@ -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.samples.metrics.prometheus.util;

import java.util.LinkedHashSet;
import java.util.Set;
import java.util.regex.Pattern;

public final class PrometheusMetricAssert {

private static final String[] RESERVED_SUFFIXES = {
"_total", "_created", "_bucket", "_info",
".total", ".created", ".bucket", ".info"
};

private PrometheusMetricAssert() {
}

public enum MetricKind {
COUNTER,
GAUGE,
INFO,
HISTOGRAM,
SUMMARY,
UNKNOWN
}

public static void assertDubboMetricExposed(String scrapeText, String dubboRawName, MetricKind kind) {
Set<String> compatibleNames = resolveCompatibleNames(dubboRawName, kind);
boolean matched = compatibleNames.stream().anyMatch(name -> containsMetricLine(scrapeText, name));
if (!matched) {
throw new AssertionError("Expected dubbo metric to be exposed. rawName=" + dubboRawName
+ ", kind=" + kind
+ ", compatibleNames=" + compatibleNames);
}
}

public static Set<String> resolveCompatibleNames(String dubboRawName, MetricKind kind) {
Set<String> names = new LinkedHashSet<>();
names.add(legacyPrometheusName(dubboRawName, kind));
names.add(newPrometheusName(dubboRawName, kind));
return names;
}

private static boolean containsMetricLine(String scrapeText, String metricName) {
Pattern pattern = Pattern.compile("(?m)^" + Pattern.quote(metricName) + "(\\{|\\s).*");
return pattern.matcher(scrapeText).find();
}

private static String legacyPrometheusName(String rawName, MetricKind kind) {
String conventionName = snakeCase(rawName);
if (kind == MetricKind.COUNTER && !conventionName.endsWith("_total")) {
conventionName += "_total";
}

String sanitized = conventionName.replaceAll("[^a-zA-Z0-9_:]", "_");
if (sanitized.isEmpty()) {
return "m_";
}
if (!Character.isLetter(sanitized.charAt(0))) {
return "m_" + sanitized;
}
return sanitized;
}

private static String snakeCase(String rawName) {
StringBuilder result = new StringBuilder();
char[] chars = rawName.toCharArray();
for (int i = 0; i < chars.length; i++) {
char current = chars[i];
if (Character.isUpperCase(current)) {
if (i > 0 && Character.isLowerCase(chars[i - 1])) {
result.append('_');
}
result.append(Character.toLowerCase(current));
} else if (Character.isLetterOrDigit(current) || current == '_') {
result.append(current);
} else {
result.append('_');
}
}
return result.toString();
}

private static String newPrometheusName(String rawName, MetricKind kind) {
String baseName = stripReservedSuffixesRepeatedly(rawName);
String prometheusName = sanitizePrometheusName(baseName.replace('.', '_'));

switch (kind) {
case COUNTER:
return appendSuffixIfAbsent(prometheusName, "_total");
case INFO:
return appendSuffixIfAbsent(prometheusName, "_info");
case GAUGE:
case UNKNOWN:
case HISTOGRAM:
case SUMMARY:
default:
return prometheusName;
}
}

private static String appendSuffixIfAbsent(String metricName, String suffix) {
if (metricName.endsWith(suffix)) {
return metricName;
}
return metricName + suffix;
}

private static String stripReservedSuffixesRepeatedly(String rawName) {
String value = rawName;
boolean stripped;
do {
stripped = false;
for (String suffix : RESERVED_SUFFIXES) {
if (value.endsWith(suffix)) {
value = value.substring(0, value.length() - suffix.length());
stripped = true;
break;
}
}
} while (stripped);
return value;
}

private static String sanitizePrometheusName(String value) {
String sanitized = value.replaceAll("[^a-zA-Z0-9_:]", "_");
if (sanitized.isEmpty()) {
return "m_";
}
if (!Character.isLetter(sanitized.charAt(0))) {
return "m_" + sanitized;
}
return sanitized;
}
}
Loading
Loading