Skip to content

Commit

Permalink
First "MVP" support for Spring @scheduled / EJB @schedule annotation (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
timmhirsens authored and eyalkoren committed Apr 11, 2019
1 parent 93fb8e6 commit 3fdca7d
Show file tree
Hide file tree
Showing 8 changed files with 332 additions and 2 deletions.
30 changes: 30 additions & 0 deletions apm-agent-plugins/apm-scheduled-annotation-plugin/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>apm-agent-plugins</artifactId>
<groupId>co.elastic.apm</groupId>
<version>1.5.1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>apm-scheduled-annotation-plugin</artifactId>
<name>${project.groupId}:${project.artifactId}</name>

<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${version.spring}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>javax</groupId>
<artifactId>javaee-api</artifactId>
<version>7.0</version>
<scope>test</scope>
</dependency>
</dependencies>

</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/*-
* #%L
* Elastic APM Java agent
* %%
* Copyright (C) 2018 - 2019 Elastic and contributors
* %%
* Licensed 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.
* #L%
*/
package co.elastic.apm.agent.spring.scheduled;

import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;

import javax.annotation.Nullable;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import co.elastic.apm.agent.bci.ElasticApmInstrumentation;
import co.elastic.apm.agent.bci.VisibleForAdvice;
import co.elastic.apm.agent.bci.bytebuddy.SimpleMethodSignatureOffsetMappingFactory.SimpleMethodSignature;
import co.elastic.apm.agent.impl.ElasticApmTracer;
import co.elastic.apm.agent.impl.stacktrace.StacktraceConfiguration;
import co.elastic.apm.agent.impl.transaction.TraceContext;
import co.elastic.apm.agent.impl.transaction.TraceContextHolder;
import co.elastic.apm.agent.impl.transaction.Transaction;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.description.NamedElement;
import net.bytebuddy.description.method.MethodDescription;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.matcher.ElementMatcher;
import net.bytebuddy.matcher.ElementMatchers;

import static co.elastic.apm.agent.bci.bytebuddy.CustomElementMatchers.isInAnyPackage;
import static net.bytebuddy.matcher.ElementMatchers.declaresMethod;
import static net.bytebuddy.matcher.ElementMatchers.isAnnotatedWith;
import static net.bytebuddy.matcher.ElementMatchers.named;

public class ScheduledTransactionNameInstrumentation extends ElasticApmInstrumentation {

@VisibleForAdvice
public static final Logger logger = LoggerFactory.getLogger(ScheduledTransactionNameInstrumentation.class);

private Collection<String> applicationPackages = Collections.emptyList();

@Advice.OnMethodEnter(suppress = Throwable.class)
private static void setTransactionName(@SimpleMethodSignature String signature, @Advice.Origin Class<?> clazz, @Advice.Local("transaction") Transaction transaction) {
if (tracer != null) {
TraceContextHolder<?> active = tracer.getActive();
if (active == null) {
transaction = tracer.startTransaction(TraceContext.asRoot(), null, clazz.getClassLoader())
.withName(signature)
.withType("scheduled")
.activate();

} else {
logger.debug("Not creating transaction for method {} because there is already a transaction running ({})", signature, active);
}
}
}

@Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class)
public static void onMethodExit(@Nullable @Advice.Local("transaction") Transaction transaction,
@Advice.Thrown Throwable t) {
if (transaction != null) {
transaction.captureException(t)
.deactivate()
.end();
}
}

@Override
public void init(ElasticApmTracer tracer) {
applicationPackages = tracer.getConfig(StacktraceConfiguration.class).getApplicationPackages();
}

@Override
public ElementMatcher<? super TypeDescription> getTypeMatcher() {
return isInAnyPackage(applicationPackages, ElementMatchers.<NamedElement>none())
.and(declaresMethod(getMethodMatcher()));
}

@Override
public ElementMatcher<? super MethodDescription> getMethodMatcher() {
return isAnnotatedWith(
named("org.springframework.scheduling.annotation.Scheduled")
.or(named("org.springframework.scheduling.annotation.Schedules"))
.or(named("javax.ejb.Schedule"))
.or(named("javax.ejb.Schedules"))
);
}

@Override
public Collection<String> getInstrumentationGroupNames() {
return Arrays.asList("concurrent", "scheduled");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*-
* #%L
* Elastic APM Java agent
* %%
* Copyright (C) 2018 - 2019 Elastic and contributors
* %%
* Licensed 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.
* #L%
*/
@NonnullApi
package co.elastic.apm.agent.spring.scheduled;

import co.elastic.apm.agent.annotation.NonnullApi;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
co.elastic.apm.agent.spring.scheduled.ScheduledTransactionNameInstrumentation
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
/*-
* #%L
* Elastic APM Java agent
* %%
* Copyright (C) 2018 - 2019 Elastic and contributors
* %%
* Licensed 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.
* #L%
*/
package co.elastic.apm.agent.spring.scheduled;

import java.util.Collections;
import java.util.concurrent.atomic.AtomicInteger;

import javax.ejb.Schedule;

import org.junit.BeforeClass;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.scheduling.annotation.Schedules;

import co.elastic.apm.agent.MockReporter;
import co.elastic.apm.agent.bci.ElasticApmAgent;
import co.elastic.apm.agent.configuration.SpyConfiguration;
import co.elastic.apm.agent.impl.ElasticApmTracer;
import co.elastic.apm.agent.impl.ElasticApmTracerBuilder;
import net.bytebuddy.agent.ByteBuddyAgent;

import static org.assertj.core.api.Assertions.assertThat;


class ScheduledTransactionNameInstrumentationTest {

private static MockReporter reporter;
private static ElasticApmTracer tracer;

@BeforeClass
@BeforeAll
static void setUpAll() {
reporter = new MockReporter();
tracer = new ElasticApmTracerBuilder()
.configurationRegistry(SpyConfiguration.createSpyConfig())
.reporter(reporter)
.build();
ElasticApmAgent.initInstrumentation(tracer, ByteBuddyAgent.install(),
Collections.singletonList(new ScheduledTransactionNameInstrumentation()));
}


@Test
void testSpringScheduledAnnotatedMethodsAreTraced() {
reporter.reset();
SpringCounter springCounter = new SpringCounter();
springCounter.scheduled();
springCounter.scheduled();
assertThat(reporter.getTransactions().size()).isEqualTo(springCounter.getInvocationCount());
assertThat(reporter.getTransactions().get(0).getName()).isEqualToIgnoringCase("SpringCounter#scheduled");
}

@Test
void testSpringJ8RepeatableScheduledAnnotatedMethodsAreTraced() {
reporter.reset();
SpringCounter springCounter = new SpringCounter();
springCounter.scheduledJava8Repeatable();
springCounter.scheduledJava8Repeatable();
assertThat(reporter.getTransactions().size()).isEqualTo(springCounter.getInvocationCount());
assertThat(reporter.getTransactions().get(0).getName()).isEqualToIgnoringCase("SpringCounter#scheduledJava8Repeatable");
}

@Test
void testSpringJ7RepeatableScheduledAnnotatedMethodsAreTraced() {
reporter.reset();
SpringCounter springCounter = new SpringCounter();
springCounter.scheduledJava7Repeatable();
springCounter.scheduledJava7Repeatable();
assertThat(reporter.getTransactions().size()).isEqualTo(springCounter.getInvocationCount());
assertThat(reporter.getTransactions().get(0).getName()).isEqualToIgnoringCase("SpringCounter#scheduledJava7Repeatable");
}

@Test
void testJeeScheduledAnnotatedMethodsAreTraced() {
reporter.reset();
JeeCounter jeeCounter = new JeeCounter();
jeeCounter.scheduled();
jeeCounter.scheduled();
assertThat(reporter.getTransactions().size()).isEqualTo(jeeCounter.getInvocationCount());
assertThat(reporter.getTransactions().get(0).getName()).isEqualToIgnoringCase("JeeCounter#scheduled");
}

@Test
void testJeeJ7RepeatableScheduledAnnotatedMethodsAreTraced() {
reporter.reset();
JeeCounter jeeCounter = new JeeCounter();
jeeCounter.scheduledJava7Repeatable();
jeeCounter.scheduledJava7Repeatable();
assertThat(reporter.getTransactions().size()).isEqualTo(jeeCounter.getInvocationCount());
assertThat(reporter.getTransactions().get(0).getName()).isEqualToIgnoringCase("JeeCounter#scheduledJava7Repeatable");
}


private class SpringCounter {
private AtomicInteger count = new AtomicInteger(0);

@Scheduled(fixedDelay = 5)
public void scheduled() {
this.count.incrementAndGet();
}

@Scheduled(fixedDelay = 5)
@Scheduled(fixedDelay = 10)
public void scheduledJava8Repeatable() {
this.count.incrementAndGet();
}

@Schedules({
@Scheduled(fixedDelay = 5),
@Scheduled(fixedDelay = 10)
})
public void scheduledJava7Repeatable() {
this.count.incrementAndGet();
}

public int getInvocationCount() {
return this.count.get();
}
}

private class JeeCounter {
private AtomicInteger count = new AtomicInteger(0);

@Schedule(minute = "5")
public void scheduled() {
this.count.incrementAndGet();
}

@javax.ejb.Schedules({
@Schedule(minute = "5"),
@Schedule(minute = "10")
})
public void scheduledJava7Repeatable() {
this.count.incrementAndGet();
}

public int getInvocationCount() {
return this.count.get();
}
}


}
1 change: 1 addition & 0 deletions apm-agent-plugins/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
<module>apm-java-concurrent-plugin</module>
<module>apm-urlconnection-plugin</module>
<module>apm-jaxws-plugin</module>
<module>apm-scheduled-annotation-plugin</module>
</modules>

<properties>
Expand Down
4 changes: 2 additions & 2 deletions docs/configuration.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,7 @@ you should add an additional entry to this list (make sure to also include the d
==== `disable_instrumentations`

A list of instrumentations which should be disabled.
Valid options are `annotations`, `apache-httpclient`, `concurrent`, `dispatcher-servlet`, `elasticsearch-restclient`, `executor`, `http-client`, `incubating`, `jax-rs`, `jax-ws`, `jdbc`, `jsf`, `okhttp`, `opentracing`, `public-api`, `render`, `servlet-api`, `servlet-api-async`, `servlet-input-stream`, `servlet-service-name`, `spring-mvc`, `spring-resttemplate`, `spring-service-name`, `urlconnection`.
Valid options are `annotations`, `apache-httpclient`, `concurrent`, `dispatcher-servlet`, `elasticsearch-restclient`, `executor`, `http-client`, `incubating`, `jax-rs`, `jax-ws`, `jdbc`, `jsf`, `okhttp`, `opentracing`, `public-api`, `render`, `scheduled`, `servlet-api`, `servlet-api-async`, `servlet-input-stream`, `servlet-service-name`, `spring-mvc`, `spring-resttemplate`, `spring-service-name`, `urlconnection`.
If you want to try out incubating features,
set the value to an empty string.

Expand Down Expand Up @@ -1204,7 +1204,7 @@ If the service name is set explicitly, it overrides all of the above.
# sanitize_field_names=password,passwd,pwd,secret,*key,*token*,*session*,*credit*,*card*,authorization,set-cookie
# A list of instrumentations which should be disabled.
# Valid options are `annotations`, `apache-httpclient`, `concurrent`, `dispatcher-servlet`, `elasticsearch-restclient`, `executor`, `http-client`, `incubating`, `jax-rs`, `jax-ws`, `jdbc`, `jsf`, `okhttp`, `opentracing`, `public-api`, `render`, `servlet-api`, `servlet-api-async`, `servlet-input-stream`, `servlet-service-name`, `spring-mvc`, `spring-resttemplate`, `spring-service-name`, `urlconnection`.
# Valid options are `annotations`, `apache-httpclient`, `concurrent`, `dispatcher-servlet`, `elasticsearch-restclient`, `executor`, `http-client`, `incubating`, `jax-rs`, `jax-ws`, `jdbc`, `jsf`, `okhttp`, `opentracing`, `public-api`, `render`, `scheduled`, `servlet-api`, `servlet-api-async`, `servlet-input-stream`, `servlet-service-name`, `spring-mvc`, `spring-resttemplate`, `spring-service-name`, `urlconnection`.
# If you want to try out incubating features,
# set the value to an empty string.
#
Expand Down
5 changes: 5 additions & 0 deletions elastic-apm-agent/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,11 @@
<artifactId>apm-web-plugin</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>apm-scheduled-annotation-plugin</artifactId>
<version>${project.version}</version>
</dependency>

<!-- For auto-generating configuration docs -->
<dependency>
Expand Down

0 comments on commit 3fdca7d

Please sign in to comment.