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
11 changes: 11 additions & 0 deletions components-starter/camel-micrometer-starter/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,17 @@
<artifactId>camel-http-common</artifactId>
<version>${camel-version}</version>
</dependency>
<!-- Testing dependencies -->
<dependency>
<groupId>org.apache.camel.springboot</groupId>
<artifactId>camel-servlet-starter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.awaitility</groupId>
<artifactId>awaitility</artifactId>
<scope>test</scope>
</dependency>
<!--START OF GENERATED CODE-->
<dependency>
<groupId>org.apache.camel.springboot</groupId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@
{
"name": "camel.metrics.uri-tag-dynamic",
"type": "java.lang.Boolean",
"description": "Whether to use static or dynamic values for HTTP uri tags in captured metrics. When using dynamic tags, then a REST service with base URL: \/users\/{id} will capture metrics with uri tag with the actual dynamic value such as: \/users\/123. However, this can lead to many tags as the URI is dynamic, so use this with care.",
"description": "Whether to use static or dynamic values for HTTP uri tags in captured metrics. When using dynamic tags, then a REST service with base URL: \/users\/{id} will capture metrics with uri tag with the actual dynamic value such as: \/users\/123. However, this can lead to many tags as the URI is dynamic, so use this with care. The dynamic value is only used for requests that are resolved to a Camel HTTP consumer, any other request is tagged by the default Spring convention.",
"sourceType": "org.apache.camel.component.micrometer.springboot.metrics.CamelMetricsConfiguration",
"defaultValue": false
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,19 @@

@AutoConfiguration(after = CamelAutoConfiguration.class)
@Conditional({ ConditionalOnCamelContextAndAutoConfigurationBeans.class })
@ConditionalOnProperty(prefix = "camel.metrics", name = "uriTagEnabled", havingValue = "true")
@ConditionalOnProperty(prefix = "camel.metrics", name = "uri-tag-enabled", havingValue = "true")
public class MicrometerTagsAutoConfiguration {

/**
* Name of the low cardinality key holding the http uri.
*/
private static final String URI = "uri";

/**
* Maximum length of the uri tag value when using dynamic uri tags, to keep the tag value bounded.
*/
private static final int MAX_URI_LENGTH = 200;

/**
* To integrate with micrometer to include expanded uri in tags when for example using camel rest-dsl with servlet.
*/
Expand All @@ -50,39 +60,71 @@ ServerRequestObservationConvention serverRequestObservationConvention(Optional<C
@Override
public KeyValues getLowCardinalityKeyValues(ServerRequestObservationContext context) {
// here, we just want to have an additional KeyValue to the observation, keeping the default values
return super.getLowCardinalityKeyValues(context).and(custom(context));
KeyValue uri = custom(context);
KeyValues answer = super.getLowCardinalityKeyValues(context);
// when the request is not for a camel consumer, then we keep the uri computed by the default
// spring convention (the mapped pattern, or a constant such as UNKNOWN or NOT_FOUND), instead of
// the requested path, which would add a new meter for every distinct path being requested
return uri != null ? answer.and(uri) : answer;
}

protected KeyValue custom(ServerRequestObservationContext context) {
HttpServletRequest request = context.getCarrier();
String uri = null;
if (servlet.isPresent() && !configuration.isUriTagDynamic()) {
HttpConsumer consumer = servlet.get().getServletResolveConsumerStrategy().resolve(request,
servlet.get().getConsumers());
if (consumer != null) {
uri = consumer.getPath();
}
if (request == null || servlet.isEmpty()) {
return null;
}
HttpConsumer consumer = servlet.get().getServletResolveConsumerStrategy().resolve(request,
servlet.get().getConsumers());
if (consumer == null) {
// the request is not for a camel consumer, so let the default spring convention resolve the uri
return null;
}

// the request may not be for camel servlet, so we need to capture uri from request
if (uri == null || uri.isEmpty()) {
// dynamic uri with the actual value from the http request
uri = request.getServletPath();
if (uri == null || uri.isEmpty()) {
uri = request.getPathInfo();
} else {
String p = request.getPathInfo();
if (p != null) {
uri = uri + p;
}
}
String uri;
if (configuration.isUriTagDynamic()) {
// dynamic uri with the actual value from the http request, this is opt-in as the uri is dynamic
// and therefore leads to a tag value per distinct request path
uri = dynamicUri(request);
} else {
// the static path of the camel consumer, such as /users/{id}
uri = consumer.getPath();
}
if (uri == null) {
uri = "";
if (uri == null || uri.isEmpty()) {
return null;
}

return KeyValue.of("uri", uri);
return KeyValue.of(URI, uri);
}
};
}

/**
* The uri from the http request, as requested by the client.
*/
private static String dynamicUri(HttpServletRequest request) {
StringBuilder sb = new StringBuilder();
String path = request.getServletPath();
if (path != null) {
sb.append(path);
}
String info = request.getPathInfo();
if (info != null) {
sb.append(info);
}
return sanitize(sb.toString());
}

/**
* The dynamic uri is client provided, so keep the tag value bounded in length and free of control characters that
* the monitoring system may not be able to render.
*/
private static String sanitize(String uri) {
String answer = uri.length() > MAX_URI_LENGTH ? uri.substring(0, MAX_URI_LENGTH) : uri;
StringBuilder sb = new StringBuilder(answer.length());
for (int i = 0; i < answer.length(); i++) {
char ch = answer.charAt(i);
sb.append(Character.isISOControl(ch) ? '_' : ch);
}
return sb.toString();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ public class CamelMetricsConfiguration {
*
* When using dynamic tags, then a REST service with base URL: /users/{id} will capture metrics with uri tag with
* the actual dynamic value such as: /users/123. However, this can lead to many tags as the URI is dynamic, so use
* this with care.
* this with care. The dynamic value is only used for requests that are resolved to a Camel HTTP consumer, any
* other request is tagged by the default Spring convention.
*/
private boolean uriTagDynamic;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*
* 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.camel.component.micrometer.springboot;

import java.util.Map;
import java.util.concurrent.TimeUnit;

import org.apache.camel.spring.boot.CamelAutoConfiguration;
import org.apache.camel.test.spring.junit6.CamelSpringBootTest;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.DirtiesContext;

import static org.awaitility.Awaitility.await;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

/**
* With dynamic uri tags, the requested path is used as uri tag, but only for requests that are resolved to a Camel
* consumer, and the tag value is kept bounded in length.
*/
@DirtiesContext
@CamelSpringBootTest
@EnableAutoConfiguration
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
classes = { CamelAutoConfiguration.class, MicrometerUriTagTestSupport.TestConfiguration.class },
// the legacy camelCase spelling of the properties must keep working as well
properties = { "camel.metrics.uriTagEnabled=true", "camel.metrics.uriTagDynamic=true" })
public class MicrometerUriTagDynamicTest extends MicrometerUriTagTestSupport {

private static final int REQUESTS = 10;
private static final int MAX_URI_LENGTH = 200;

@Order(1)
@Test
void unmatchedRequestsShareASingleMeter() throws Exception {
for (int i = 0; i < REQUESTS; i++) {
assertEquals(404, get("/camel/no-such-path-" + i));
}

await().atMost(10, TimeUnit.SECONDS).untilAsserted(() -> {
Map<String, Long> tags = uriTags();
assertEquals(1, tags.size(), "Expected a single uri tag value but got " + tags);
assertEquals(REQUESTS, tags.values().iterator().next());
assertFalse(tags.keySet().stream().anyMatch(uri -> uri.contains("no-such-path")),
"The requested path must not be used as uri tag but got " + tags);
});
}

@Order(2)
@Test
void matchedRequestsUseTheRequestedPath() throws Exception {
assertEquals(200, get("/camel/users/123"));

await().atMost(10, TimeUnit.SECONDS)
.untilAsserted(() -> assertEquals(1, count("/camel/users/123"), "Got uri tags " + uriTags()));
}

@Order(3)
@Test
void longRequestedPathIsCapped() throws Exception {
assertEquals(200, get("/camel/users/" + "a".repeat(300)));

await().atMost(10, TimeUnit.SECONDS).untilAsserted(() -> {
Map<String, Long> tags = uriTags();
assertTrue(tags.keySet().stream().anyMatch(uri -> uri.length() == MAX_URI_LENGTH
&& uri.startsWith("/camel/users/aaa")), "Expected a capped uri tag value but got " + tags);
assertFalse(tags.keySet().stream().anyMatch(uri -> uri.length() > MAX_URI_LENGTH),
"No uri tag value must be longer than " + MAX_URI_LENGTH + " but got " + tags);
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
* 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.camel.component.micrometer.springboot;

import java.util.Map;
import java.util.concurrent.TimeUnit;

import org.apache.camel.spring.boot.CamelAutoConfiguration;
import org.apache.camel.test.spring.junit6.CamelSpringBootTest;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.DirtiesContext;

import static org.awaitility.Awaitility.await;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;

/**
* The uri tag must be the static path of the Camel consumer, and requests that are not for a Camel consumer must not
* add a meter per requested path.
*/
@DirtiesContext
@CamelSpringBootTest
@EnableAutoConfiguration
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
classes = { CamelAutoConfiguration.class, MicrometerUriTagTestSupport.TestConfiguration.class },
properties = { "camel.metrics.uri-tag-enabled=true" })
public class MicrometerUriTagTest extends MicrometerUriTagTestSupport {

private static final int REQUESTS = 10;

@Order(1)
@Test
void unmatchedRequestsShareASingleMeter() throws Exception {
for (int i = 0; i < REQUESTS; i++) {
assertEquals(404, get("/camel/no-such-path-" + i));
}

await().atMost(10, TimeUnit.SECONDS).untilAsserted(() -> {
Map<String, Long> tags = uriTags();
assertEquals(1, tags.size(), "Expected a single uri tag value but got " + tags);
assertEquals(REQUESTS, tags.values().iterator().next());
assertFalse(tags.keySet().stream().anyMatch(uri -> uri.contains("no-such-path")),
"The requested path must not be used as uri tag but got " + tags);
});
}

@Order(2)
@Test
void matchedRequestsUseTheConsumerPath() throws Exception {
assertEquals(200, get("/camel/users/123"));
assertEquals(200, get("/camel/users/456"));

await().atMost(10, TimeUnit.SECONDS)
.untilAsserted(() -> assertEquals(2, count("/users/{id}"), "Got uri tags " + uriTags()));
}
}
Loading
Loading