Skip to content

Commit

Permalink
Merge pull request #10 from lukashinsch/replace-library
Browse files Browse the repository at this point in the history
Replace underlying user agent parser with Yauaa library (https://gith…
  • Loading branch information
lukashinsch authored Sep 22, 2019
2 parents b37278e + a6f451f commit 89f855f
Show file tree
Hide file tree
Showing 10 changed files with 51 additions and 150 deletions.
14 changes: 8 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,27 @@
[![Maven Central](https://maven-badges.herokuapp.com/maven-central/eu.hinsch/spring-boot-actuator-user-agent-metrics/badge.svg)](https://maven-badges.herokuapp.com/maven-central/eu.hinsch/spring-boot-actuator-user-agent-metrics/)

# spring-boot-actuator-user-agent-metrics
Filter to log user agent statistics as spring boot actuator metrics
Filter to log user agent statistics as spring boot actuator metrics. Uses Yauaa library (https://github.com/nielsbasjes/yauaa) under the hood.

## Upgrade notice
With version 0.2 the baseline has been updated to java 11 and spring boot 2.1.
To match the new metrics framework (micrometer), the metrics concept has been changed from individual metrics keys per value (e.g. browser version) to a single metric with (configurable) tags. Therefore, the `keys` namespace in the configuration has been replaced with a `tags` configuration map.
With version 0.3 the baseline has been updated to java 11 and spring boot 2.1 and the underlying user agent parser library has been replaced with a more recent project.
To match the new metrics framework (micrometer), the metrics concept has been changed from individual metrics keys per value (e.g. browser version) to a single metric with (configurable) tags. Therefore, the `keys` namespace in the configuration has been renamed to `tags`.

Note: do not use version 0.2.0, while it was updated to work with current framework versions, the library used for user agent parsing was outdated and could not correctly identify a number of current browsers.

## Howto use

### Gradle
```
runtime("eu.hinsch:spring-boot-actuator-user-agent-metrics:0.2.0")
runtime("eu.hinsch:spring-boot-actuator-user-agent-metrics:0.3.0")
```

### Maven
```
<dependency>
<groupId>eu.hinsch</groupId>
<artifactId>spring-boot-actuator-user-agent-metrics</artifactId>
<version>0.2.0</version>
<version>0.3.0</version>
</dependency>
```

Expand All @@ -33,7 +35,7 @@ All config properties are located beneath the prefix `user-agent-metric`
|--------------|------------|----------------------------|
| enabled | false | Turn metrics filter on/off |
| url-patterns | empty list | List of patterns to match the servlet filter on |
| tags | empty map | key/value maps of tag name to expressions matching on net.sf.uadetector.ReadableUserAgent |
| tags | empty list | list of fields to be added as micrometer tags. For a list of available fields see [https://github.com/nielsbasjes/yauaa/blob/master/analyzer/src/main/java/nl/basjes/parse/useragent/UserAgent.java](https://github.com/nielsbasjes/yauaa/blob/master/analyzer/src/main/java/nl/basjes/parse/useragent/UserAgent.java) |

For an example see [SpringBootActuatorUserAgentMetricsTestApplication](https://github.com/lukashinsch/spring-boot-actuator-user-agent-metrics/blob/master/src/test/java/eu/hinsch/spring/boot/actuator/useragent/SpringBootActuatorUserAgentMetricsTestApplication.java)
and [application.yml](https://github.com/lukashinsch/spring-boot-actuator-user-agent-metrics/blob/master/src/test/resources/application.yml)
4 changes: 2 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ apply plugin: 'io.spring.dependency-management'
apply plugin: 'maven-publish'
apply plugin: 'signing'

version = '0.2.0'
version = '0.3.0'
group = 'eu.hinsch'

jar {
Expand All @@ -45,7 +45,7 @@ jar.enabled true
dependencies {
compile("org.springframework.boot:spring-boot-starter-actuator")
compile("org.springframework.boot:spring-boot-starter-web")
compile("net.sf.uadetector:uadetector-resources:2014.04")
compile("nl.basjes.parse.useragent:yauaa:5.12")
testCompile("org.springframework.boot:spring-boot-starter-test")
annotationProcessor("org.springframework.boot:spring-boot-configuration-processor")
compileOnly("org.projectlombok:lombok")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,15 @@
import io.micrometer.core.instrument.Tag;
import java.io.IOException;
import java.util.List;
import javax.annotation.PostConstruct;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import net.sf.uadetector.ReadableUserAgent;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import nl.basjes.parse.useragent.UserAgent;
import nl.basjes.parse.useragent.UserAgentAnalyzer;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

Expand All @@ -30,10 +28,17 @@ public class UserAgentMetricFilter extends OncePerRequestFilter {
private static final String METRIC_NAME = "user-agent";

private final MeterRegistry meterRegistry;
private final BeanFactory beanFactory;
private final UserAgentMetricFilterConfiguration configuration;
private final UserAgentParser userAgentParser;
private final SpelExpressionParser parser = new SpelExpressionParser();
private UserAgentAnalyzer uaa;

@PostConstruct
public void buildAnalyzer() {
uaa = UserAgentAnalyzer
.newBuilder()
.hideMatcherLoadStats()
.withCache(configuration.getCacheSize())
.build();
}

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
Expand All @@ -43,21 +48,13 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse
String userAgentString = request.getHeader("User-Agent");
if (StringUtils.hasText(userAgentString)) {
log.debug("User agent: " + userAgentString);
ReadableUserAgent userAgent = userAgentParser.parseUserAgentString(userAgentString);

UserAgent agent = uaa.parse(userAgentString);
List<Tag> tags = configuration.getTags()
.entrySet()
.stream()
.map(entry -> Tag.of(entry.getKey(), evaluatePattern(userAgent, entry.getValue(), request)))
.map(tag -> Tag.of(tag, agent.getValue(tag)))
.collect(toList());
meterRegistry.counter(METRIC_NAME, tags).increment();
}
}

private String evaluatePattern(ReadableUserAgent userAgent, String pattern, HttpServletRequest request) {
Expression expression = parser.parseExpression(pattern);
StandardEvaluationContext context = new StandardEvaluationContext();
context.setBeanResolver(new WithCurrentRequestBeanFactoryResolver(beanFactory, request));
context.setRootObject(userAgent);
return String.valueOf(expression.getValue(context));
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package eu.hinsch.spring.boot.actuator.useragent;

import io.micrometer.core.instrument.MeterRegistry;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
Expand Down Expand Up @@ -31,10 +30,8 @@ FilterRegistrationBean<UserAgentMetricFilter> userAgentMetricFilterRegistrationB
@ConditionalOnProperty(value = "user-agent-metric.enabled", havingValue = "true")
@Bean
UserAgentMetricFilter userAgentMetricFilter(MeterRegistry meterRegistry,
BeanFactory beanFactory,
UserAgentParser userAgentParser,
UserAgentMetricFilterConfiguration configuration) {
return new UserAgentMetricFilter(meterRegistry, beanFactory, configuration, userAgentParser);
return new UserAgentMetricFilter(meterRegistry, configuration);

}
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
package eu.hinsch.spring.boot.actuator.useragent;

import java.util.HashMap;
import java.util.Map;
import java.util.ArrayList;
import java.util.List;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.List;

/**
* Created by lh on 01/06/15.
*/
Expand All @@ -22,14 +19,18 @@ public class UserAgentMetricFilterConfiguration {
*/
private boolean enabled;

/**
* Number of user agent strings to cache
*/
private int cacheSize = 10000;

/**
* List of url patterns to apply the servlet filter to.
*/
private List<String> urlPatterns = new ArrayList<>();

/**
* Map of micrometer tag name to expression that will be evaluated.
* Context root will be {@link net.sf.uadetector.ReadableUserAgent}
* List of fields that will be added as micrometer tags
*/
private Map<String, String> tags = new HashMap<>();
private List<String> tags = new ArrayList<>();
}

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,7 @@
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Tag;
import java.util.List;
import java.util.Map;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.junit.Before;
Expand All @@ -17,10 +14,8 @@
import org.mockito.Captor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Spy;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import org.springframework.beans.factory.BeanFactory;

import static org.hamcrest.Matchers.hasItem;
import static org.hamcrest.Matchers.hasSize;
Expand All @@ -43,9 +38,6 @@ public class UserAgentMetricFilterTest {
@Rule
public MockitoRule rule = MockitoJUnit.rule();

@Mock
private BeanFactory beanFactory;

@Mock
private MeterRegistry meterRegistry;

Expand All @@ -64,15 +56,17 @@ public class UserAgentMetricFilterTest {
@Mock
private Counter counter;

@Spy
private UserAgentParser userAgentParser = new UserAgentParser();

@InjectMocks
private UserAgentMetricFilter filter;

@Captor
private ArgumentCaptor<List<Tag>> tagListCaptor;

@Before
public void initAnalyzer() {
filter.buildAnalyzer();
}

@Test
public void shouldLogExecutionWitoutTagsWithDefaultConfig() throws Exception {
// given
Expand All @@ -92,44 +86,7 @@ public void shouldLogExecutionWitoutTagsWithDefaultConfig() throws Exception {
public void shouldLogUserAgentWithConfiguredKeys() throws Exception {
// given
mockUserAgentHeader(CHROME);
mockTagConfiguration(Map.of("name", "#this.name", "osName", "#this.operatingSystem.name"));
when(meterRegistry.counter(anyString(), anyList())).thenReturn(counter);

// when
filter.doFilter(request, response, filterChain);

// then
verify(meterRegistry).counter(eq("user-agent"), tagListCaptor.capture());
verify(counter).increment();
List<Tag> tags = tagListCaptor.getValue();
assertThat(tags, hasItem(Tag.of("name", "Chrome")));
assertThat(tags, hasItem(Tag.of("osName", "Windows 7")));
}

@Test
public void shouldLogRequestData() throws Exception {
// given
mockUserAgentHeader(CHROME);
mockTagConfiguration(Map.of("customHeader", "@currentRequest.getHeader('MyHeader')"));
when(request.getHeader("MyHeader")).thenReturn("MyHeaderValue");
when(meterRegistry.counter(anyString(), anyList())).thenReturn(counter);

// when
filter.doFilter(request, response, filterChain);

// then
verify(meterRegistry).counter(eq("user-agent"), tagListCaptor.capture());
verify(counter).increment();
List<Tag> tags = tagListCaptor.getValue();
assertThat(tags, hasItem(Tag.of("customHeader", "MyHeaderValue")));
}

@Test
public void shouldUseOtherBean() throws Exception {
// given
mockUserAgentHeader(CHROME);
when(beanFactory.getBean("myBean")).thenReturn("value");
mockTagConfiguration(Map.of("customBean", "@myBean"));
mockTagConfiguration(List.of("AgentName", "OperatingSystemNameVersionMajor"));
when(meterRegistry.counter(anyString(), anyList())).thenReturn(counter);

// when
Expand All @@ -139,7 +96,8 @@ public void shouldUseOtherBean() throws Exception {
verify(meterRegistry).counter(eq("user-agent"), tagListCaptor.capture());
verify(counter).increment();
List<Tag> tags = tagListCaptor.getValue();
assertThat(tags, hasItem(Tag.of("customBean", "value")));
assertThat(tags, hasItem(Tag.of("AgentName", "Chrome")));
assertThat(tags, hasItem(Tag.of("OperatingSystemNameVersionMajor", "Windows 7")));
}

@Test
Expand All @@ -153,7 +111,7 @@ public void shouldCallFilterChainWithoutUserAgent() throws Exception {
verify(meterRegistry, never()).counter(anyString(), anyList());
}

private void mockTagConfiguration(Map<String, String> config) {
private void mockTagConfiguration(List<String> config) {
when(configuration.getTags()).thenReturn(config);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@TestPropertySource(properties = {
"user-agent-metric.tags.browserName = #this.name",
"user-agent-metric.tags[0] = AgentName",
"user-agent-metric.url-patterns[0] = /testmvc",
"user-agent-metric.enabled = true"
})
Expand Down Expand Up @@ -73,6 +73,6 @@ public void shouldLogRequest() throws Exception {
mockMvc.perform(get("/testmvc").header("User-Agent", CHROME)).andExpect(content().string("ok"));

// then
assertThat(meterRegistry.counter("user-agent", List.of(Tag.of("browserName", "Chrome"))).count(), is(2.0));
assertThat(meterRegistry.counter("user-agent", List.of(Tag.of("AgentName", "Chrome"))).count(), is(2.0));
}
}
10 changes: 4 additions & 6 deletions src/test/resources/application.yml
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
user-agent-metric:
enabled: true
tags:
# e.g. chrome
name: '#this.name'
# e.g. chrome.41
nameVersion: '#this.name + "." + #this.versionNumber.major'
# e.g. os-x
osName: '#this.operatingSystem.name'
- OperatingSystemNameVersionMajor
- DeviceClass
- AgentName
- AgentNameVersionMajor

url-patterns: /test
# alternatively:
Expand Down

0 comments on commit 89f855f

Please sign in to comment.