Skip to content

Commit 0be34f2

Browse files
authored
Merge pull request #37 from bliblidotcom/feature/newrelic-module
add new relic reactor instrumentation module
2 parents a529d9b + 301d593 commit 0be34f2

27 files changed

+1396
-0
lines changed
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Reactor New Relic Instrumentation
2+
3+
A plugin to enable new relic instrumentation in Spring Boot 2 Webflux App.
4+
5+
List of supported modules:
6+
- command-plugin: anything that implement `Command` interface.
7+
- spring-reactive-mongo: ReactiveMongoRepository all inherited method and custom query.
8+
9+
Planned future modules:
10+
- Reactive relational DB (via R2DBC)
11+
- Reactive elastic search
12+
- Reactive redis
13+
14+
## How to use
15+
16+
1. Include the library in your POM.
17+
18+
```
19+
<dependency>
20+
<groupId>com.blibli.oss</groupId>
21+
<artifactId>blibli-backend-framework-newrelic</artifactId>
22+
</dependency>
23+
```
24+
25+
And viola! See your app instrumented in New Relic.
26+
27+
![New Relic instrumentation](docs/newrelic-sample.png)
28+
29+
## How it works
30+
31+
1. Inject new relic Token and Transaction for every web request in reactor Context. See `NewRelicTokenInjectorFilter` filter class.
32+
2. When code is entering a known module (targeted by our AOP pointcut), then append additional reactor operator to time those method execution using New Relic `Transaction.startSegment()`.
33+
34+
## Known issues
35+
36+
- Command timing report will include ReactiveMongoRepository timing.
37+
- There are still "Remainder" time on New Relic report. It may caused by the time the thread waiting until executed by Spring thread pool because of traffic congestion.
65.9 KB
Loading
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
5+
<parent>
6+
<artifactId>blibli-backend-framework</artifactId>
7+
<groupId>com.blibli.oss</groupId>
8+
<version>0.0.6-SNAPSHOT</version>
9+
</parent>
10+
<modelVersion>4.0.0</modelVersion>
11+
12+
<artifactId>blibli-backend-framework-newrelic</artifactId>
13+
14+
<dependencies>
15+
<dependency>
16+
<groupId>com.blibli.oss</groupId>
17+
<artifactId>blibli-backend-framework-command</artifactId>
18+
</dependency>
19+
<dependency>
20+
<groupId>org.springframework.boot</groupId>
21+
<artifactId>spring-boot-starter-webflux</artifactId>
22+
<scope>provided</scope>
23+
</dependency>
24+
<dependency>
25+
<groupId>org.springframework.boot</groupId>
26+
<artifactId>spring-boot-starter-aop</artifactId>
27+
<scope>provided</scope>
28+
</dependency>
29+
<dependency>
30+
<groupId>org.springframework.boot</groupId>
31+
<artifactId>spring-boot-configuration-processor</artifactId>
32+
<optional>true</optional>
33+
<scope>provided</scope>
34+
</dependency>
35+
<dependency>
36+
<groupId>com.newrelic.agent.java</groupId>
37+
<artifactId>newrelic-api</artifactId>
38+
</dependency>
39+
<dependency>
40+
<groupId>io.projectreactor</groupId>
41+
<artifactId>reactor-core</artifactId>
42+
<scope>provided</scope>
43+
</dependency>
44+
<dependency>
45+
<groupId>org.springframework.boot</groupId>
46+
<artifactId>spring-boot-starter-data-mongodb-reactive</artifactId>
47+
<scope>provided</scope>
48+
</dependency>
49+
<dependency>
50+
<groupId>org.projectlombok</groupId>
51+
<artifactId>lombok</artifactId>
52+
<optional>true</optional>
53+
</dependency>
54+
<dependency>
55+
<groupId>org.springframework.boot</groupId>
56+
<artifactId>spring-boot-starter-test</artifactId>
57+
<scope>test</scope>
58+
</dependency>
59+
<dependency>
60+
<groupId>io.projectreactor</groupId>
61+
<artifactId>reactor-test</artifactId>
62+
<scope>test</scope>
63+
</dependency>
64+
</dependencies>
65+
66+
</project>
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package com.blibli.oss.backend.newrelic.aspect;
2+
3+
import com.blibli.oss.backend.command.Command;
4+
import com.blibli.oss.backend.newrelic.aspect.service.AspectModifyService;
5+
import com.blibli.oss.backend.newrelic.aspect.service.util.SegmentType;
6+
import lombok.AllArgsConstructor;
7+
import org.aspectj.lang.ProceedingJoinPoint;
8+
import org.aspectj.lang.annotation.Around;
9+
import org.aspectj.lang.annotation.Aspect;
10+
import org.aspectj.lang.annotation.Pointcut;
11+
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
12+
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
13+
import org.springframework.context.annotation.Configuration;
14+
15+
/**
16+
* Aspect to target Blibli Command plugin for timing with New Relic Segment.
17+
*/
18+
@Aspect
19+
@Configuration
20+
@ConditionalOnClass(Command.class)
21+
@ConditionalOnProperty(
22+
prefix = "blibli.newrelic.command",
23+
name = "enabled",
24+
matchIfMissing = true
25+
)
26+
@AllArgsConstructor
27+
public class CommandAspect {
28+
29+
private AspectModifyService aspectModifyService;
30+
31+
@Pointcut("execution(* com.blibli.oss.backend.command.Command.execute(..))")
32+
private void commandExecute() {}
33+
34+
@Around(value = "commandExecute()")
35+
public Object afterCommandExecute(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
36+
return aspectModifyService.modifyRetValWithTiming(
37+
proceedingJoinPoint, SegmentType.COMMAND
38+
);
39+
}
40+
41+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package com.blibli.oss.backend.newrelic.aspect;
2+
3+
import com.blibli.oss.backend.newrelic.aspect.service.AspectModifyService;
4+
import com.blibli.oss.backend.newrelic.aspect.service.util.SegmentType;
5+
import lombok.AllArgsConstructor;
6+
import org.aspectj.lang.ProceedingJoinPoint;
7+
import org.aspectj.lang.annotation.Around;
8+
import org.aspectj.lang.annotation.Aspect;
9+
import org.aspectj.lang.annotation.Pointcut;
10+
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
11+
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
12+
import org.springframework.boot.autoconfigure.mongo.MongoProperties;
13+
import org.springframework.context.annotation.Configuration;
14+
import org.springframework.data.mongodb.core.ReactiveMongoTemplate;
15+
import org.springframework.data.mongodb.repository.ReactiveMongoRepository;
16+
17+
/**
18+
* Aspect to target Spring Reactive MongoDB for timing with New Relic Segment.
19+
*/
20+
@Aspect
21+
@Configuration
22+
@ConditionalOnClass({
23+
ReactiveMongoRepository.class,
24+
ReactiveMongoTemplate.class,
25+
MongoProperties.class
26+
})
27+
@ConditionalOnProperty(
28+
prefix = "blibli.newrelic.reactive-mongodb",
29+
name = "enabled",
30+
matchIfMissing = true
31+
)
32+
@AllArgsConstructor
33+
public class ReactiveMongoDbAspect {
34+
35+
private AspectModifyService aspectModifyService;
36+
37+
@Pointcut("execution(public * *.*(..)) && within(org.springframework.data.mongodb.repository.ReactiveMongoRepository+)")
38+
void mongoRepositoryInterface() {}
39+
40+
@Around(value = "mongoRepositoryInterface()")
41+
public Object afterCommandExecute(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
42+
return aspectModifyService.modifyRetValWithTiming(
43+
proceedingJoinPoint, SegmentType.REACTIVE_MONGODB
44+
);
45+
}
46+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package com.blibli.oss.backend.newrelic.aspect.service;
2+
3+
import com.blibli.oss.backend.newrelic.aspect.service.util.SegmentType;
4+
import org.aspectj.lang.ProceedingJoinPoint;
5+
6+
public interface AspectModifyService {
7+
8+
Object modifyRetValWithTiming(
9+
ProceedingJoinPoint proceedingJoinPoint, SegmentType segmentType
10+
) throws Throwable;
11+
12+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
package com.blibli.oss.backend.newrelic.aspect.service.impl;
2+
3+
import com.blibli.oss.backend.newrelic.aspect.service.AspectModifyService;
4+
import com.blibli.oss.backend.newrelic.aspect.service.util.AspectHelper;
5+
import com.blibli.oss.backend.newrelic.aspect.service.util.RetValType;
6+
import com.blibli.oss.backend.newrelic.aspect.service.util.SegmentType;
7+
import com.blibli.oss.backend.newrelic.injector.NewRelicTokenInjectorFilter;
8+
import com.blibli.oss.backend.newrelic.reporter.ExternalReporter;
9+
import com.blibli.oss.backend.newrelic.reporter.helper.ExternalReporterHelper;
10+
import com.newrelic.api.agent.Segment;
11+
import lombok.Setter;
12+
import lombok.extern.slf4j.Slf4j;
13+
import org.aspectj.lang.JoinPoint;
14+
import org.aspectj.lang.ProceedingJoinPoint;
15+
import org.springframework.beans.factory.InitializingBean;
16+
import org.springframework.context.ApplicationContext;
17+
import org.springframework.context.ApplicationContextAware;
18+
import reactor.core.publisher.Flux;
19+
import reactor.core.publisher.Mono;
20+
import reactor.core.publisher.Signal;
21+
import reactor.util.context.Context;
22+
23+
import java.util.Collections;
24+
import java.util.List;
25+
import java.util.Map;
26+
import java.util.concurrent.atomic.AtomicReference;
27+
28+
@Slf4j
29+
public class AspectModifyServiceImpl implements AspectModifyService, ApplicationContextAware, InitializingBean {
30+
31+
@Setter
32+
private ApplicationContext applicationContext;
33+
34+
private Map<SegmentType, List<ExternalReporter>> externalReporters;
35+
36+
@Override
37+
public void afterPropertiesSet() {
38+
externalReporters = ExternalReporterHelper.getExternalReporters(applicationContext);
39+
}
40+
41+
@Override
42+
public Object modifyRetValWithTiming(
43+
ProceedingJoinPoint proceedingJoinPoint, SegmentType segmentType) throws Throwable {
44+
Object retVal = proceedingJoinPoint.proceed(proceedingJoinPoint.getArgs());
45+
if (!AspectHelper.retValIsType(retVal, segmentType.getReturnValueType())) {
46+
return retVal;
47+
}
48+
49+
AtomicReference<Segment> segmentRef = new AtomicReference<>();
50+
51+
if (AspectHelper.retValIsType(retVal, RetValType.MONO)) {
52+
retVal = ((Mono<Object>) retVal)
53+
.subscriberContext(ctx -> this.startSegment(segmentType, ctx, segmentRef, proceedingJoinPoint))
54+
.doOnEach(signal -> this.stopSegment(signal, segmentRef));
55+
} else if (AspectHelper.retValIsType(retVal, RetValType.FLUX)) {
56+
retVal = ((Flux<Object>) retVal)
57+
.subscriberContext(ctx -> this.startSegment(segmentType, ctx, segmentRef, proceedingJoinPoint))
58+
.doOnEach(signal -> this.stopSegment(signal, segmentRef));
59+
}
60+
return retVal;
61+
}
62+
63+
Context startSegment(SegmentType segmentType, Context ctx, AtomicReference<Segment> segmentRef, JoinPoint jp) {
64+
// TODO starting segment through `subscriberContext` may make the timing longer than the actual execution block.
65+
// Especially if we have long/blocking Mono operation before Command layer.
66+
// Not likely though, because we discourage fat controller pattern.
67+
// This is a hack for this issue https://github.com/reactor/reactor-core/issues/1526
68+
NewRelicTokenInjectorFilter.getTransaction(ctx)
69+
.ifPresent(t -> {
70+
String segmentName = AspectHelper.constructSegmentName(jp, segmentType);
71+
Segment segment = t.startSegment(segmentName);
72+
segmentRef.set(segment);
73+
74+
startExternalReporters(segmentType, segment, jp);
75+
});
76+
return ctx;
77+
}
78+
79+
private void startExternalReporters(SegmentType segmentType, Segment segment, JoinPoint jp) {
80+
List<ExternalReporter> reporters = externalReporters.getOrDefault(segmentType, Collections.EMPTY_LIST);
81+
82+
reporters.forEach(
83+
reporter -> reporter.report(segment, jp)
84+
);
85+
}
86+
87+
private void stopSegment(Signal signal, AtomicReference<Segment> segmentRef) {
88+
if (signal.isOnComplete()) {
89+
if (segmentRef.get() != null) {
90+
segmentRef.get().end();
91+
} else {
92+
log.warn("New Relic segment does not exist!");
93+
}
94+
}
95+
}
96+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package com.blibli.oss.backend.newrelic.aspect.service.util;
2+
3+
import lombok.experimental.UtilityClass;
4+
import lombok.extern.slf4j.Slf4j;
5+
import org.aspectj.lang.JoinPoint;
6+
import reactor.core.publisher.Flux;
7+
import reactor.core.publisher.Mono;
8+
9+
@Slf4j
10+
@UtilityClass
11+
public class AspectHelper {
12+
13+
public static String constructSegmentName(JoinPoint joinPoint, SegmentType segmentType) {
14+
return String.format(
15+
segmentType.getStringFormat(),
16+
joinPoint.getTarget().getClass().getSimpleName(),
17+
joinPoint.getSignature().toShortString()
18+
);
19+
}
20+
21+
public static boolean retValIsType(Object retVal, RetValType type) {
22+
switch (type) {
23+
case FLUX:
24+
return retVal instanceof Flux;
25+
case MONO:
26+
return retVal instanceof Mono;
27+
case MONO_OR_FLUX:
28+
return retVal instanceof Flux || retVal instanceof Mono;
29+
default:
30+
throw new IllegalArgumentException("RetValType is unknown");
31+
}
32+
}
33+
34+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package com.blibli.oss.backend.newrelic.aspect.service.util;
2+
3+
import org.springframework.boot.autoconfigure.mongo.MongoProperties;
4+
5+
import java.util.Optional;
6+
import java.util.regex.Matcher;
7+
import java.util.regex.Pattern;
8+
9+
public class MongoUriParser {
10+
11+
// mongodb://[username:password@]host1[:port1][,...hostN[:portN]][/[defaultauthdb][?options]]
12+
private static final String MONGO_CREDENTIALS_PATTERN = "([^:]+:[^@]+@)?";
13+
private static final String MONGO_HOST_PORT_PATTERN = "[^@:,/]+(:[\\d]+)?";
14+
private static final String MONGO_HOST_PORT_OTHERS_PATTERN = "(,[^@:,/]+(:[\\d]+)?)*";
15+
private static final String MONGO_DB_OPTIONS_PATTERN = "(/.*)?";
16+
17+
private static final Pattern MONGO_URI_PATTERN = Pattern.compile(String.format("^mongodb://%s(%s%s)%s$",
18+
MONGO_CREDENTIALS_PATTERN, MONGO_HOST_PORT_PATTERN, MONGO_HOST_PORT_OTHERS_PATTERN, MONGO_DB_OPTIONS_PATTERN));
19+
20+
public static String[] getHosts(String mongoUri) {
21+
Matcher m = MONGO_URI_PATTERN.matcher(mongoUri);
22+
23+
if (m.find()) {
24+
return Optional.ofNullable(m.group(2))
25+
.map(hosts -> hosts.split(","))
26+
.orElseThrow(() -> new RuntimeException("unsupported mongo uri"));
27+
} else {
28+
throw new RuntimeException("unsupported mongo uri");
29+
}
30+
}
31+
32+
33+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package com.blibli.oss.backend.newrelic.aspect.service.util;
2+
3+
public enum RetValType {
4+
MONO, FLUX, MONO_OR_FLUX;
5+
}

0 commit comments

Comments
 (0)