diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index 9d18b00606..3c336570ef 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -627,6 +627,39 @@ metricset tag keys - {pull}1413[1413] * Fix service name error when updating a web app on a Servlet container - {pull}1326[#1326] * Fix remote attach 'jps' executable not found when 'java' binary is symlinked ot a JRE - {pull}1352[#1352] +[[release-notes-1.18.0]] +==== 1.18.0 - 2020/09/08 + +[float] +===== Features +* Deprecating `ignore_urls` config in favour of <> to align + with other agents, while still allowing the old config name for backward compatibility - {pull}1315[#1315] +* Enabling instrumentation of classes compiled with Java 1.4. This is reverting the restriction of instrumenting only + bytecode of Java 1.5 or higher ({pull}320[#320]), which was added due to potential `VerifyError`. Such errors should be + avoided now by the usage of `TypeConstantAdjustment` - {pull}1317[#1317] +* Enabling agent to work without attempting any communication with APM server, by allowing setting `server_urls` with + an empty string - {pull}1295[#1295] +* Add <> - {pull}1303[#1303] +* Add `profiling_inferred_spans_lib_directory` option to override the default temp directory used for exporting the async-profiler library. + This is useful for server-hardened environments where `/tmp` is often configured with `noexec`, leading to `java.lang.UnsatisfiedLinkError` errors - {pull}1350[#1350] +* Create spans for Servlet dispatches to FORWARD, INCLUDE and ERROR - {pull}1212[#1212] +* Support JDK 11 HTTPClient - {pull}1307[#1307] +* Lazily create profiler temporary files {pull}1360[#1360] +* Convert the followings to Indy Plugins (see details in <>): gRPC, + AsyncHttpClient, Apache HttpClient +* The agent now collects cgroup memory metrics (see details in <>) +* Update async-profiler to 1.8.1 {pull}1382[#1382] + +[float] +===== Bug fixes +* Fixes a `NoClassDefFoundError` in the JMS instrumentation of `MessageListener` - {pull}1287[#1287] +* Fix `/ by zero` error message when setting `server_urls` with an empty string - {pull}1295[#1295] +* Fix `ClassNotFoundException` or `ClassCastException` in some cases where special log4j configurations are used - {pull}1322[#1322] +* Fix `NumberFormatException` when using early access Java version - {pull}1325[#1325] +* Fix `service_name` config being ignored when set to the same auto-discovered default value - {pull}1324[#1324] +* Fix service name error when updating a web app on a Servlet container - {pull}1326[#1326] +* Fix remote attach 'jps' executable not found when 'java' binary is symlinked ot a JRE - {pull}1352[#1352] + [[release-notes-1.18.0.rc1]] ==== 1.18.0.RC1 - 2020/07/22 diff --git a/apm-agent-core/pom.xml b/apm-agent-core/pom.xml index bf264f3411..4e4fb678e4 100644 --- a/apm-agent-core/pom.xml +++ b/apm-agent-core/pom.xml @@ -146,6 +146,12 @@ 1.2 test + + + commons-codec + commons-codec + 1.7 + org.apache.httpcomponents httpclient @@ -198,10 +204,48 @@ testcontainers test + + org.json + json + 20180130 + + + maven-shade-plugin + 3.2.3 + + + package + + shade + + + true + true + false + + + *:* + + **/module-info.class + META-INF/versions/** + + + + + + org.apache.commons.codec + co.elastic.apm.agent.shaded.codec + + + + + + + maven-jar-plugin @@ -215,4 +259,4 @@ - + \ No newline at end of file diff --git a/apm-agent-core/src/main/java/co/elastic/apm/agent/bci/SFConfigInfo.java b/apm-agent-core/src/main/java/co/elastic/apm/agent/bci/SFConfigInfo.java new file mode 100644 index 0000000000..e645344c23 --- /dev/null +++ b/apm-agent-core/src/main/java/co/elastic/apm/agent/bci/SFConfigInfo.java @@ -0,0 +1,52 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. 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 co.elastic.apm.agent.bci; + +public class SFConfigInfo { + private String key; + + private String serviceName; + + private SFTagInfo tags; + + public String getKey() { + return this.key; + } + + public void setKey(String key) { + this.key = key; + } + + public String getServiceName() { + return this.serviceName; + } + + public void setServiceName(String serviceName) { + this.serviceName = serviceName; + } + + public SFTagInfo getTags() { + return this.tags; + } + + public void setTags(SFTagInfo tags) { + this.tags = tags; + } +} + diff --git a/apm-agent-core/src/main/java/co/elastic/apm/agent/bci/SFTagInfo.java b/apm-agent-core/src/main/java/co/elastic/apm/agent/bci/SFTagInfo.java new file mode 100644 index 0000000000..af7d8a4065 --- /dev/null +++ b/apm-agent-core/src/main/java/co/elastic/apm/agent/bci/SFTagInfo.java @@ -0,0 +1,52 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. 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 co.elastic.apm.agent.bci; + +public class SFTagInfo { + private String Name; + + private String appName; + + private String projectName; + + public String getName() { + return this.Name; + } + + public void setName(String name) { + this.Name = name; + } + + public String getAppName() { + return this.appName; + } + + public void setAppName(String appName) { + this.appName = appName; + } + + public String getProjectName() { + return this.projectName; + } + + public void setProjectName(String projectName) { + this.projectName = projectName; + } +} + diff --git a/apm-agent-core/src/main/java/co/elastic/apm/agent/bci/bytebuddy/SoftlyReferencingTypePoolCache.java b/apm-agent-core/src/main/java/co/elastic/apm/agent/bci/bytebuddy/SoftlyReferencingTypePoolCache.java new file mode 100644 index 0000000000..2985257821 --- /dev/null +++ b/apm-agent-core/src/main/java/co/elastic/apm/agent/bci/bytebuddy/SoftlyReferencingTypePoolCache.java @@ -0,0 +1,157 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. 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 co.elastic.apm.agent.bci.bytebuddy; + +import co.elastic.apm.agent.util.ExecutorUtils; +import com.blogspot.mydailyjava.weaklockfree.WeakConcurrentMap; +import net.bytebuddy.agent.builder.AgentBuilder; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import net.bytebuddy.pool.TypePool; + +import javax.annotation.Nullable; +import java.lang.ref.SoftReference; +import java.util.Map; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; + +/** + * Caches {@link TypeDescription}s which speeds up type matching - + * especially when the matching requires lookup of other {@link TypeDescription}s. + * Such as when in order to match a type, it's superclass has to be determined. + * Without a type pool cache those types would have to be re-loaded from the file system if their {@link Class} has not been loaded yet. + *

+ * In order to avoid {@link OutOfMemoryError}s because of this cache, + * the {@link TypePool.CacheProvider}s are wrapped in a {@link SoftReference} + *

+ */ +public class SoftlyReferencingTypePoolCache extends AgentBuilder.PoolStrategy.WithTypePoolCache { + + /* + * Weakly referencing ClassLoaders to avoid class loader leaks + * Softly referencing the type pool cache so that it does not cause OOMEs + * deliberately doesn't use WeakMapSupplier as this class manages the cleanup manually + */ + private final WeakConcurrentMap cacheProviders = + new WeakConcurrentMap(false); + private final ElementMatcher ignoredClassLoaders; + + public SoftlyReferencingTypePoolCache(final TypePool.Default.ReaderMode readerMode, + final int clearIfNotAccessedSinceMinutes, ElementMatcher.Junction ignoredClassLoaders) { + super(readerMode); + ExecutorUtils.createSingleThreadSchedulingDaemonPool("type-cache-pool-cleaner") + .scheduleWithFixedDelay(new Runnable() { + @Override + public void run() { + clearIfNotAccessedSince(clearIfNotAccessedSinceMinutes); + cacheProviders.expungeStaleEntries(); + } + }, 1, 1, TimeUnit.MINUTES); + this.ignoredClassLoaders = ignoredClassLoaders; + } + + @Override + protected TypePool.CacheProvider locate(ClassLoader classLoader) { + if (ignoredClassLoaders.matches(classLoader)) { + return TypePool.CacheProvider.Simple.withObjectType(); + } + classLoader = classLoader == null ? getBootstrapMarkerLoader() : classLoader; + CacheProviderWrapper cacheProviderRef = cacheProviders.get(classLoader); + if (cacheProviderRef == null || cacheProviderRef.get() == null) { + cacheProviderRef = new CacheProviderWrapper(); + cacheProviders.put(classLoader, cacheProviderRef); + // accommodate for race condition + cacheProviderRef = cacheProviders.get(classLoader); + } + final TypePool.CacheProvider cacheProvider = cacheProviderRef.get(); + // guard against edge case when the soft reference has already been cleared since evaluating the loop condition + return cacheProvider != null ? cacheProvider : TypePool.CacheProvider.Simple.withObjectType(); + } + + /** + * Clears the type pool cache if it has not been accessed for the specified amount of time. + *

+ * This cache is mostly useful while the application starts and warms up. + * After a certain point, all classes are loaded and this cache is not needed anymore + *

+ *

+ * Evicting the whole cache at once has advantages over evicting on an entry-based level: + * A resolution never gets stale or outdated, which is the main use case for having a max age for an entry. + * Also, this model only works when the cache is frequently accessed, + * as most caches only evict stale entries when interacting with the cache. + * In our scenario, + * the cache is not accessed at all once all classes have been loaded which means it would never get cleared. + *

+ *

+ * Two exceptions of that norm are (re-)deploying a web application at runtime and dynamically loading of classes, + * which cause interactions after the initial startup. + *

+ * + * @param clearIfNotAccessedSinceMinutes the time in minutes after which the cache should be cleared + */ + void clearIfNotAccessedSince(long clearIfNotAccessedSinceMinutes) { + for (Map.Entry entry : cacheProviders) { + if (System.currentTimeMillis() >= entry.getValue().getLastAccess() + TimeUnit.MINUTES.toMillis(clearIfNotAccessedSinceMinutes)) { + cacheProviders.remove(entry.getKey()); + } + } + } + + WeakConcurrentMap getCacheProviders() { + return cacheProviders; + } + + private static class CacheProviderWrapper { + private final AtomicLong lastAccess = new AtomicLong(System.currentTimeMillis()); + private final SoftReference delegate; + + private CacheProviderWrapper() { + this.delegate = new SoftReference(new TypePool.CacheProvider.Simple()); + } + + long getLastAccess() { + return lastAccess.get(); + } + + @Nullable + TypePool.CacheProvider get() { + return delegate.get(); + } + } + + /** + * Copied from {@link Simple#getBootstrapMarkerLoader()} + *

+ * Returns the class loader to serve as a cache key if a cache provider for the bootstrap class loader is requested. + * This class loader is represented by {@code null} in the JVM which is an invalid value for many {@link ConcurrentMap} + * implementations. + *

+ *

+ * By default, {@link ClassLoader#getSystemClassLoader()} is used as such a key as any resource location for the + * bootstrap class loader is performed via the system class loader within Byte Buddy as {@code null} cannot be queried + * for resources via method calls such that this does not make a difference. + *

+ * + * @return A class loader to represent the bootstrap class loader. + */ + private ClassLoader getBootstrapMarkerLoader() { + return ClassLoader.getSystemClassLoader(); + } +} diff --git a/apm-agent-core/src/main/java/co/elastic/apm/agent/context/ExecutorServiceShutdownLifecycleListener.java b/apm-agent-core/src/main/java/co/elastic/apm/agent/context/ExecutorServiceShutdownLifecycleListener.java new file mode 100644 index 0000000000..c344700237 --- /dev/null +++ b/apm-agent-core/src/main/java/co/elastic/apm/agent/context/ExecutorServiceShutdownLifecycleListener.java @@ -0,0 +1,36 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. 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 co.elastic.apm.agent.context; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.TimeUnit; + +public class ExecutorServiceShutdownLifecycleListener extends AbstractLifecycleListener { + private final ExecutorService executor; + + public ExecutorServiceShutdownLifecycleListener(ExecutorService executor) { + this.executor = executor; + } + + @Override + public void stop() throws Exception { + executor.shutdown(); + executor.awaitTermination(1, TimeUnit.SECONDS); + } +} diff --git a/apm-agent-core/src/main/java/co/elastic/apm/agent/tracemethods/package-info.java b/apm-agent-core/src/main/java/co/elastic/apm/agent/tracemethods/package-info.java index afcb827d14..bbb1cdd198 100644 --- a/apm-agent-core/src/main/java/co/elastic/apm/agent/tracemethods/package-info.java +++ b/apm-agent-core/src/main/java/co/elastic/apm/agent/tracemethods/package-info.java @@ -16,6 +16,25 @@ * specific language governing permissions and limitations * under the License. */ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. 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. + */ + @NonnullApi package co.elastic.apm.agent.tracemethods; diff --git a/apm-agent-core/src/test/java/co/elastic/apm/agent/AbstractInstrumentationTest.java b/apm-agent-core/src/test/java/co/elastic/apm/agent/AbstractInstrumentationTest.java index c01def00c0..1746e42793 100644 --- a/apm-agent-core/src/test/java/co/elastic/apm/agent/AbstractInstrumentationTest.java +++ b/apm-agent-core/src/test/java/co/elastic/apm/agent/AbstractInstrumentationTest.java @@ -101,7 +101,6 @@ public static ConfigurationRegistry getConfig() { @After @AfterEach public final void cleanUp() { - SpyConfiguration.reset(config); try { if (validateRecycling) { diff --git a/apm-agent-core/src/test/java/co/elastic/apm/agent/bci/InstrumentationTest.java b/apm-agent-core/src/test/java/co/elastic/apm/agent/bci/InstrumentationTest.java index c3edd1be62..a354b87222 100644 --- a/apm-agent-core/src/test/java/co/elastic/apm/agent/bci/InstrumentationTest.java +++ b/apm-agent-core/src/test/java/co/elastic/apm/agent/bci/InstrumentationTest.java @@ -854,7 +854,6 @@ public Collection getInstrumentationGroupNames() { } public static class LogManagerInstrumentation extends TracerAwareInstrumentation { - public static class AdviceClass { public static AtomicInteger enterCount = GlobalVariables.get(LogManagerInstrumentation.class, "enterCount", new AtomicInteger()); public static AtomicInteger exitCount = GlobalVariables.get(LogManagerInstrumentation.class, "exitCount", new AtomicInteger()); @@ -1026,7 +1025,6 @@ public ElementMatcher getMethodMatcher() { public Collection getInstrumentationGroupNames() { return Collections.singletonList("test"); } - } public static class GetClassLoaderInstrumentation extends TracerAwareInstrumentation { diff --git a/apm-agent-core/src/test/java/co/elastic/apm/agent/util/IOUtilsTest.java b/apm-agent-core/src/test/java/co/elastic/apm/agent/util/IOUtilsTest.java index c90d8395c4..128b815ec2 100644 --- a/apm-agent-core/src/test/java/co/elastic/apm/agent/util/IOUtilsTest.java +++ b/apm-agent-core/src/test/java/co/elastic/apm/agent/util/IOUtilsTest.java @@ -35,6 +35,8 @@ class IOUtilsTest { + private final String tempDirectory = System.getProperty("java.io.tmpdir"); + @Test void readUtf8Stream() throws IOException { final CharBuffer charBuffer = CharBuffer.allocate(8); diff --git a/apm-agent-core/src/test/resources/test.elasticapm.properties b/apm-agent-core/src/test/resources/test.elasticapm.properties index c65400acab..f8a14b6343 100644 --- a/apm-agent-core/src/test/resources/test.elasticapm.properties +++ b/apm-agent-core/src/test/resources/test.elasticapm.properties @@ -1,6 +1,5 @@ -enable_experimental_instrumentations=true +# experimental instrumentations should be active in tests +disable_instrumentations= application_packages=co.elastic.apm ship_agent_logs=false log_level=DEBUG -cloud_provider=NONE -warmup_byte_buddy=false diff --git a/apm-agent-plugins/apm-apache-httpclient-plugin/src/main/java/co/elastic/apm/agent/httpclient/ApacheHttpAsyncClientRedirectInstrumentation.java b/apm-agent-plugins/apm-apache-httpclient-plugin/src/main/java/co/elastic/apm/agent/httpclient/ApacheHttpAsyncClientRedirectInstrumentation.java index ab20099b0d..8274cb2c2c 100644 --- a/apm-agent-plugins/apm-apache-httpclient-plugin/src/main/java/co/elastic/apm/agent/httpclient/ApacheHttpAsyncClientRedirectInstrumentation.java +++ b/apm-agent-plugins/apm-apache-httpclient-plugin/src/main/java/co/elastic/apm/agent/httpclient/ApacheHttpAsyncClientRedirectInstrumentation.java @@ -83,4 +83,4 @@ public ElementMatcher.Junction getProtectionDomainPostFilter() public ElementMatcher getMethodMatcher() { return named("getRedirect"); } -} +} \ No newline at end of file diff --git a/apm-agent-plugins/apm-api-plugin/src/test/java/co/elastic/apm/agent/plugin/api/SpanInstrumentationTest.java b/apm-agent-plugins/apm-api-plugin/src/test/java/co/elastic/apm/agent/plugin/api/SpanInstrumentationTest.java new file mode 100644 index 0000000000..cdf162fe7a --- /dev/null +++ b/apm-agent-plugins/apm-api-plugin/src/test/java/co/elastic/apm/agent/plugin/api/SpanInstrumentationTest.java @@ -0,0 +1,155 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. 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 co.elastic.apm.agent.plugin.api; + +import co.elastic.apm.agent.AbstractInstrumentationTest; +import co.elastic.apm.agent.impl.TextHeaderMapAccessor; +import co.elastic.apm.agent.impl.transaction.TraceContext; +import co.elastic.apm.api.ElasticApm; +import co.elastic.apm.api.Scope; +import co.elastic.apm.api.Span; +import co.elastic.apm.api.Transaction; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +class SpanInstrumentationTest extends AbstractInstrumentationTest { + + private Transaction transaction; + + @BeforeEach + void setUp() { + transaction = ElasticApm.startTransaction(); + } + + @AfterEach + void tearDown() { + transaction.end(); + } + + @Test + void testSetName() { + Span span = transaction.startSpan(); + span.setName("foo"); + endSpan(span); + assertThat(reporter.getFirstSpan().getNameAsString()).isEqualTo("foo"); + } + + @Test + void testLegacyAPIs() { + Span span = transaction.createSpan(); + span.setType("foo.bar.baz"); + endSpan(span); + co.elastic.apm.agent.impl.transaction.Span internalSpan = reporter.getFirstSpan(); + assertThat(internalSpan.getType()).isEqualTo("foo"); + assertThat(internalSpan.getSubtype()).isEqualTo("bar"); + assertThat(internalSpan.getAction()).isEqualTo("baz"); + } + + @Test + void testTypes() { + Span span = transaction.startSpan("foo", "bar", "baz"); + endSpan(span); + co.elastic.apm.agent.impl.transaction.Span internalSpan = reporter.getFirstSpan(); + assertThat(internalSpan.getType()).isEqualTo("foo"); + assertThat(internalSpan.getSubtype()).isEqualTo("bar"); + assertThat(internalSpan.getAction()).isEqualTo("baz"); + } + + @Test + void testChaining() { + Span span = transaction.startSpan("foo", null, null).setName("foo").addLabel("foo", "bar"); + endSpan(span); + assertThat(reporter.getFirstSpan().getNameAsString()).isEqualTo("foo"); + assertThat(reporter.getFirstSpan().getType()).isEqualTo("foo"); + assertThat(reporter.getFirstSpan().getContext().getLabel("foo")).isEqualTo("bar"); + } + + private void endSpan(Span span) { + span.end(); + transaction.end(); + assertThat(reporter.getSpans()).hasSize(1); + assertThat(reporter.getTransactions()).hasSize(1); + } + + @Test + void testScope() { + Span span = transaction.startSpan(); + assertThat(ElasticApm.currentSpan().getId()).isNotEqualTo(span.getId()); + try (final Scope scope = span.activate()) { + assertThat(ElasticApm.currentSpan().getId()).isEqualTo(span.getId()); + ElasticApm.currentSpan().startSpan().end(); + } + span.end(); + transaction.end(); + assertThat(reporter.getSpans()).hasSize(2); + assertThat(reporter.getTransactions()).hasSize(1); + assertThat(reporter.getSpans().get(0).isChildOf(reporter.getSpans().get(1))).isTrue(); + assertThat(reporter.getSpans().get(1).isChildOf(reporter.getFirstTransaction())).isTrue(); + } + + @Test + void testSampled() { + assertThat(ElasticApm.currentSpan().isSampled()).isFalse(); + assertThat(ElasticApm.currentTransaction().isSampled()).isFalse(); + final Transaction transaction = ElasticApm.startTransaction(); + assertThat(transaction.isSampled()).isTrue(); + Span span = transaction.startSpan(); + assertThat(span.isSampled()).isTrue(); + span.end(); + transaction.end(); + } + + @Test + void testTraceHeadersNoop() { + assertContainsNoTracingHeaders(ElasticApm.currentSpan()); + assertContainsNoTracingHeaders(ElasticApm.currentTransaction()); + } + + @Test + void testTraceHeaders() { + Span span = transaction.startSpan(); + assertContainsTracingHeaders(span); + assertContainsTracingHeaders(transaction); + span.end(); + } + + private void assertContainsNoTracingHeaders(Span span) { + try (Scope scope = span.activate()) { + final Map tracingHeaders = new HashMap<>(); + span.injectTraceHeaders(tracingHeaders::put); + span.injectTraceHeaders(null); + assertThat(tracingHeaders).isEmpty(); + } + } + + private void assertContainsTracingHeaders(Span span) { + try (Scope scope = span.activate()) { + final Map tracingHeaders = new HashMap<>(); + span.injectTraceHeaders(tracingHeaders::put); + span.injectTraceHeaders(null); + assertThat(TraceContext.containsTraceContextTextHeaders(tracingHeaders, TextHeaderMapAccessor.INSTANCE)).isTrue(); + } + } +} diff --git a/apm-agent-plugins/apm-bootdelegation-plugin/src/main/java/co/elastic/apm/agent/bootdelegation/BootstrapDelegationClassLoaderInstrumentation.java b/apm-agent-plugins/apm-bootdelegation-plugin/src/main/java/co/elastic/apm/agent/bootdelegation/BootstrapDelegationClassLoaderInstrumentation.java new file mode 100644 index 0000000000..e617e7560f --- /dev/null +++ b/apm-agent-plugins/apm-bootdelegation-plugin/src/main/java/co/elastic/apm/agent/bootdelegation/BootstrapDelegationClassLoaderInstrumentation.java @@ -0,0 +1,111 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. 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 co.elastic.apm.agent.bootdelegation; + +import co.elastic.apm.agent.bci.TracerAwareInstrumentation; +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 javax.annotation.Nullable; +import java.util.Collection; +import java.util.Collections; + +import static net.bytebuddy.matcher.ElementMatchers.hasSuperType; +import static net.bytebuddy.matcher.ElementMatchers.is; +import static net.bytebuddy.matcher.ElementMatchers.nameContains; +import static net.bytebuddy.matcher.ElementMatchers.nameStartsWith; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.not; +import static net.bytebuddy.matcher.ElementMatchers.returns; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +/** + * Enables universal delegation to the bootstrap class loader for agent classes. + *

+ * Some frameworks like OSGi, JBoss modules or Mule have class loader that restrict the regular delegation to the bootstrap class loader + * to only a particular set of allowed packages. + * This is a generic solution that works for all of them. + *

+ *

+ * After all plugins have been migrated to indy plugins, + * this instrumentation can be removed. + * But having it in now allows to make runtime attachment more readily available sooner. + * Also, if indy plugins should not work out for some reason, + * we have already tested out this approach and thus have something to fall back to. + *

+ *

+ * This approach is inspired by {@code io.opentelemetry.auto.instrumentation.javaclassloader.ClassLoaderInstrumentation}, + * under Apache License 2.0 + *

+ */ +public class BootstrapDelegationClassLoaderInstrumentation extends TracerAwareInstrumentation { + + @Override + public ElementMatcher getTypeMatcherPreFilter() { + return ElementMatchers.nameContains("Loader"); + } + + @Override + public ElementMatcher getTypeMatcher() { + return not(nameStartsWith("java.")) + .and(not(nameStartsWith("jdk."))) + .and(not(nameStartsWith("com.sun."))) + .and(not(nameStartsWith("sun."))) + .and(not(nameContains("Bootstrap"))) + .and(hasSuperType(is(ClassLoader.class))); + } + + @Override + public ElementMatcher getMethodMatcher() { + return named("loadClass") + .and(returns(Class.class)) + .and( + takesArguments(String.class) + .or(takesArguments(String.class, boolean.class))); + } + + @Override + public Collection getInstrumentationGroupNames() { + return Collections.singletonList("bootdelegation"); + } + + @Advice.OnMethodExit(onThrowable = ClassNotFoundException.class) + private static void onExit(@Advice.Thrown(readOnly = false) @Nullable ClassNotFoundException thrown, + @Advice.Argument(0) String className, + @Advice.Return(readOnly = false) Class returnValue) { + // only if the class loader wasn't able to load the agent classes themselves we apply our magic + if (thrown != null && className.startsWith("co.elastic.apm.agent")) { + try { + returnValue = Class.forName(className, false, null); + thrown = null; + } catch (ClassNotFoundException e) { + thrown.addSuppressed(e); + } + } + } + + @Override + public boolean indyPlugin() { + return false; + } +} diff --git a/apm-agent-plugins/apm-hibernate-plugin/pom.xml b/apm-agent-plugins/apm-hibernate-plugin/pom.xml new file mode 100644 index 0000000000..2d0013f4d9 --- /dev/null +++ b/apm-agent-plugins/apm-hibernate-plugin/pom.xml @@ -0,0 +1,29 @@ + + + 4.0.0 + + co.elastic.apm + apm-agent-plugins + 1.29.0 + + apm-hibernate-plugin + ${project.groupId}:${project.artifactId} + + + ${project.basedir}/../.. + 1.12.0 + UTF-8 + + + + + org.hibernate + hibernate-core + 5.6.5.Final + + + + diff --git a/apm-agent-plugins/apm-hibernate-plugin/src/main/java/co/elastic/apm/agent/hibernate/HibernateInstrumentation.java b/apm-agent-plugins/apm-hibernate-plugin/src/main/java/co/elastic/apm/agent/hibernate/HibernateInstrumentation.java new file mode 100644 index 0000000000..8b4830c3a7 --- /dev/null +++ b/apm-agent-plugins/apm-hibernate-plugin/src/main/java/co/elastic/apm/agent/hibernate/HibernateInstrumentation.java @@ -0,0 +1,37 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. 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 co.elastic.apm.agent.hibernate; + +import co.elastic.apm.agent.bci.TracerAwareInstrumentation; +import co.elastic.apm.agent.hibernate.helper.HibernateHelper; + +import java.util.Collection; +import java.util.Collections; + +public abstract class HibernateInstrumentation extends TracerAwareInstrumentation { + + private static final Collection HIBERNATE_GROUPS = Collections.singleton("hibernate"); + + protected static HibernateHelper hh = new HibernateHelper(); + + @Override + public final Collection getInstrumentationGroupNames() { + return HIBERNATE_GROUPS; + } +} diff --git a/apm-agent-plugins/apm-hibernate-plugin/src/main/java/co/elastic/apm/agent/hibernate/SessionBuilderInstrumentation.java b/apm-agent-plugins/apm-hibernate-plugin/src/main/java/co/elastic/apm/agent/hibernate/SessionBuilderInstrumentation.java new file mode 100644 index 0000000000..8124c48273 --- /dev/null +++ b/apm-agent-plugins/apm-hibernate-plugin/src/main/java/co/elastic/apm/agent/hibernate/SessionBuilderInstrumentation.java @@ -0,0 +1,128 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. 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 co.elastic.apm.agent.hibernate; + +import static net.bytebuddy.matcher.ElementMatchers.hasSuperType; +import static net.bytebuddy.matcher.ElementMatchers.isInterface; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.nameStartsWith; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.not; +import static net.bytebuddy.matcher.ElementMatchers.returns; + +import javax.annotation.Nullable; + +import co.elastic.apm.agent.impl.ElasticApmTracer; +import co.elastic.apm.agent.impl.transaction.AbstractSpan; +import co.elastic.apm.agent.impl.transaction.Span; +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.ElementMatcher.Junction; + +import static co.elastic.apm.agent.hibernate.helper.HibernateHelper.*; + +/** + * Creates spans for Hibernate {@link SessionBuilder} execution + */ +public abstract class SessionBuilderInstrumentation extends HibernateInstrumentation { + + private final ElementMatcher methodMatcher; + + SessionBuilderInstrumentation(ElementMatcher methodMatcher) { + this.methodMatcher = methodMatcher; + } + + @Override + public ElementMatcher getTypeMatcherPreFilter() { + return nameStartsWith("org.hibernate"); + } + + @Override + public ElementMatcher getTypeMatcher() { + + Junction type = hasSuperType(named("org.hibernate.SessionBuilder")); + return not(isInterface()).and(type); + } + + @Override + public ElementMatcher getMethodMatcher() { + return methodMatcher; + } + + /** + * Instruments: + *
    + * + * Gives more accurate details on open session. + * after this method "Session openSession()" execution + * Create a span for session open + + *
+ */ + //@SuppressWarnings("DuplicatedCode") + public static class SessionOpenInstrumentation extends SessionBuilderInstrumentation { + + public SessionOpenInstrumentation(ElasticApmTracer tracer) { + super( + named("openSession") + .and(returns(named("org.hibernate.Session"))) + .and(isPublic()) + ); + } + + + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class, inline = false) + public static void onAfterExecute(@Advice.This Object sessionBuilder, + @Advice.Origin("#m") String methodName, + @Advice.Thrown @Nullable Throwable t, + @Advice.Return Object session) { + + String str = "After :"+methodName; + AbstractSpan parentSpan = tracer.getActive(); + hh.logMsg(str,"methodName="+methodName,"sessionBuilder="+sessionBuilder,"session="+session, + "parentSpan="+parentSpan,"throwable="+t); + if ( parentSpan == null ) return; + + Span span = tracer.getActiveSpan(); + hh.logMsg("tracer.getActiveSpan="+span); + + if( t != null ) { + hh.logMsg("Exception capture error span={}",span); + if( span != null) + span.captureException(t).deactivate().end(); + return; + } + + if( hh.isObjectAlreadyCreated(session) ) { + hh.logMsg("Session already created so don't create span again."); + } + else { + hh.createHibernateSpan(tracer.getActive(), + HIB_SUB_TYPE+" "+HIB_SPANNAME_SESSION,//SPAN Name + HIB_SPAN_TYPE,HIB_SUB_TYPE,HIB_SPAN_ACTION_CREATE, + session,null,methodName); + } + } + + } + +} diff --git a/apm-agent-plugins/apm-hibernate-plugin/src/main/java/co/elastic/apm/agent/hibernate/SessionInstrumentation.java b/apm-agent-plugins/apm-hibernate-plugin/src/main/java/co/elastic/apm/agent/hibernate/SessionInstrumentation.java new file mode 100644 index 0000000000..1d5e3189a5 --- /dev/null +++ b/apm-agent-plugins/apm-hibernate-plugin/src/main/java/co/elastic/apm/agent/hibernate/SessionInstrumentation.java @@ -0,0 +1,318 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. 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 co.elastic.apm.agent.hibernate; + +import static net.bytebuddy.matcher.ElementMatchers.hasSuperType; +import static net.bytebuddy.matcher.ElementMatchers.isInterface; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.nameStartsWith; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.not; +import static net.bytebuddy.matcher.ElementMatchers.returns; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import java.io.Serializable; +import java.sql.Statement; + +import javax.annotation.Nullable; +import javax.persistence.Entity; + +import org.hibernate.LockMode; +import org.hibernate.LockOptions; +import org.hibernate.Session; +import org.hibernate.SharedSessionContract; +import org.hibernate.Transaction; + +import co.elastic.apm.agent.impl.ElasticApmTracer; +import co.elastic.apm.agent.impl.context.AbstractContext; +import co.elastic.apm.agent.impl.transaction.AbstractSpan; +import co.elastic.apm.agent.impl.transaction.Span; +import co.elastic.apm.agent.impl.transaction.TraceContext; +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.ElementMatcher.Junction; + +/** + * Creates spans for Hibernate {@link SharedSessionContract} execution + */ +public abstract class SessionInstrumentation extends HibernateInstrumentation { + + private final ElementMatcher methodMatcher; + + SessionInstrumentation(ElementMatcher methodMatcher) { + this.methodMatcher = methodMatcher; + } + + @Override + public ElementMatcher getTypeMatcherPreFilter() { + return nameStartsWith("org.hibernate"); + } + + @Override + public ElementMatcher getTypeMatcher() { + + Junction supTypes = hasSuperType(named("org.hibernate.SharedSessionContract")); + return not(isInterface()).and(supTypes); + } + + @Override + public ElementMatcher getMethodMatcher() { + return methodMatcher; + } + + /** + * Instruments: + *
    + *
  • {@link Session#get(String, Serializable)}
  • + *
  • {@link Session#get(String, Serializable, LockMode)}
  • + *
  • {@link Session#get(String, Serializable, LockOptions)}
  • + *
+ */ + //@SuppressWarnings("DuplicatedCode") + public static class ReadObjectsInstrumentation extends SessionInstrumentation { + + public ReadObjectsInstrumentation(ElasticApmTracer tracer) { + super( + named("get") + .and(takesArgument(0, String.class)) + .and(isPublic()) + ); + } + + @Nullable + @Advice.OnMethodEnter(suppress = Throwable.class, inline = false) + public static Object onBeforeExecute(@Advice.This SharedSessionContract session, + @Advice.Origin("#m") String methodName, + @Advice.Argument(0) String entityName) { + + return hh.createHibernateSpan(tracer.getActive(),methodName+" "+entityName, + hh.HIB_SPAN_TYPE,hh.HIB_SPAN_TYPE,hh.HIB_SPAN_ACTION_QUERY,//Use subtype for action as well. + session,entityName,methodName); + } + + + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class, inline = false) + public static void onAfterExecute(@Advice.This SharedSessionContract session, + @Advice.Enter @Nullable Object span, + @Advice.Thrown @Nullable Throwable t) { + hh.logMsg("After Executing GET Method:", "session="+session,"Span="+span, + "throwable"+t); + if (span == null) { + return; + } + + ((Span) span).captureException(t) + .deactivate() + .end(); + } + } + + /** + * Instruments: CUD(Create, Update, Delete) operations + *
    + *
  • {@link Session#get(String, Serializable)}
  • + *
  • {@link Session#get(String, Serializable, LockMode)}
  • + *
  • {@link Session#get(String, Serializable, LockOptions)}
  • + * + * "save", + "replicate", + "saveOrUpdate", + "update", + "merge", + "persist", + "lock", + "refresh", + "insert", + "delete", + *
+ */ + //@SuppressWarnings("DuplicatedCode") + public static class CUDInstrumentation extends SessionInstrumentation { + + public CUDInstrumentation(ElasticApmTracer tracer) { + super( + named("save").or(named("saveOrUpdate")).or(named("update")) + .or(named("merge")).or(named("persist")).or(named("refresh")) + .or(named("delete")) + .and(takesArgument(0, Object.class)) + .and(isPublic()) + ); + } + + @Nullable + @Advice.OnMethodEnter(suppress = Throwable.class, inline = false) + public static Object onBeforeExecute(@Advice.This Object session, @Advice.Origin("#m") String methodName, + @Advice.Argument(0) Object entityObject) { + + String entityName = entityObject.getClass().getSimpleName(); + return hh.createHibernateSpan(tracer.getActive(),methodName+" "+entityName, + hh.HIB_SPAN_TYPE,hh.HIB_SPAN_TYPE,hh.HIB_SPAN_ACTION_QUERY,//Use subtype for action as well. + session,entityObject,methodName); + } + + + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class, inline = false) + public static void onAfterExecute(@Advice.This Object session, + @Advice.Enter @Nullable Object span, + @Advice.Thrown @Nullable Throwable t) { + + hh.logMsg("After Executing CUD methods:", "session="+session,"Span="+span, + "throwable="+t); + if (span == null) { + return; + } + + try { + //Exception while closing the session + if( t !=null ) ((Span) span).captureException(t); + + }finally { + ((Span) span).deactivate().end(); + } + } + } + + /** + * Instruments: + *
    + *
  • {@link Session#beginTransaction())}
  • + * + * Gives more accurate details on open session. + * after this method "Session openSession()" execution + * Create a span for session open + + *
+ */ + //@SuppressWarnings("DuplicatedCode") + public static class SessionOpenInstrumentation extends SessionInstrumentation { + + public SessionOpenInstrumentation(ElasticApmTracer tracer) { + super( + named("beginTransaction").or(named("getTransaction")) + .and(returns(named("org.hibernate.Transaction"))) + .and(isPublic()) + ); + } + + + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class, inline = false) + public static void onAfterExecute(@Advice.This SharedSessionContract session, + @Advice.Origin("#m") String methodName, + @Advice.Thrown @Nullable Throwable t, + @Advice.Return Transaction transaction) { + + String str = "After :"+methodName; + AbstractSpan parentSpan = tracer.getActive(); + hh.logMsg(str,"methodName="+methodName,"session="+session, + "transaction="+transaction,"parentSpan="+parentSpan,"throwable="+t); + if ( parentSpan == null ) return; + + TraceContext tc = parentSpan.getTraceContext(); + AbstractContext absCtx = parentSpan.getContext(); + hh.logMsg(str,"TraceContext="+tc,"abstractContext="+absCtx); + + Span span = tracer.getActiveSpan(); + hh.logMsg("tracer.getActiveSpan="+span); + + if( t != null ) { + hh.logMsg("Exception capture error span:"); + span.captureException(t).deactivate().end(); + return; + } + + if( hh.isObjectAlreadyCreated(session) ) { + hh.logMsg("Session already created so don't create span again."); + } + else { + hh.createHibernateSpan(tracer.getActive(), + hh.HIB_SPAN_TYPE+" "+hh.HIB_SPANNAME_SESSION,//SPAN Name + hh.HIB_SPAN_TYPE,hh.HIB_SUB_TYPE,hh.HIB_SPAN_ACTION_CREATE, + session,null,methodName); + } + } + } + + /** + * Instruments: + *
    + *
  • {@link Session#close())}
  • + + *
+ */ + //@SuppressWarnings("DuplicatedCode") + public static class SessionCloseInstrumentation extends SessionInstrumentation { + + public SessionCloseInstrumentation(ElasticApmTracer tracer) { + super( + named("close").and(takesArguments(0)) + .and(isPublic()) + ); + } + + + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class, inline = false) + public static void onSessionClose(@Advice.This SharedSessionContract session, + // @Advice.Enter @Nullable Object span, + @Advice.Thrown @Nullable Throwable t) { + + AbstractSpan parentSpan = tracer.getActive(); + hh.logMsg("onSessionClose:","parentSpan=",parentSpan,"session=",session,"throwable=",t); + + org.hibernate.internal.SessionImpl si = (org.hibernate.internal.SessionImpl)session; + hh.logMsg("Session Identifier:",si.getSessionIdentifier().toString(),"Session IdentityHashcode:",System.identityHashCode(session)); + + Span spanFromCache = hh.getSpanForObject(session); + hh.logMsg("spanFromCache = {}",spanFromCache); + if ( spanFromCache == null) { + //No Span for this Session in the cache. + // Close without session is not possible. Meaning it is duplicate call. + // Span is already close. should be ignored. + hh.logMsg("Span might have already closed. No Action required."); + return; + } + if (parentSpan == null) { + return; + } + //Just for information. + Span span = tracer.getActiveSpan(); + hh.logMsg("Current span {}",span); + + try { + + //Exception while closing the session + if( t !=null ) { + spanFromCache.captureException(t); + } + + }finally { + //Since Session is closed + //Remove it from the cache && DeActivate and End the span. + hh.logMsg("Name of the span getting closed ",spanFromCache.toString()); + + spanFromCache.deactivate().end(); + hh.removeObjectSpan(session); + } + + } + } +} diff --git a/apm-agent-plugins/apm-hibernate-plugin/src/main/java/co/elastic/apm/agent/hibernate/TransactionInstrumentation.java b/apm-agent-plugins/apm-hibernate-plugin/src/main/java/co/elastic/apm/agent/hibernate/TransactionInstrumentation.java new file mode 100644 index 0000000000..9f87404db9 --- /dev/null +++ b/apm-agent-plugins/apm-hibernate-plugin/src/main/java/co/elastic/apm/agent/hibernate/TransactionInstrumentation.java @@ -0,0 +1,157 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. 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 co.elastic.apm.agent.hibernate; + +import static net.bytebuddy.matcher.ElementMatchers.hasSuperType; +import static net.bytebuddy.matcher.ElementMatchers.isInterface; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.nameStartsWith; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.not; +import static net.bytebuddy.matcher.ElementMatchers.returns; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; +import static net.bytebuddy.matcher.ElementMatchers.is; + +import java.io.Serializable; +import java.sql.Statement; + +import javax.annotation.Nullable; +import javax.persistence.Entity; + +import org.hibernate.LockMode; +import org.hibernate.LockOptions; +import org.hibernate.Session; +import org.hibernate.SharedSessionContract; +import org.hibernate.Transaction; +import org.hibernate.resource.transaction.spi.TransactionStatus; + +import co.elastic.apm.agent.impl.ElasticApmTracer; +import co.elastic.apm.agent.impl.context.AbstractContext; +import co.elastic.apm.agent.impl.transaction.AbstractSpan; +import co.elastic.apm.agent.impl.transaction.Span; +import co.elastic.apm.agent.impl.transaction.TraceContext; +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.ElementMatcher.Junction; + +import static co.elastic.apm.agent.hibernate.helper.HibernateHelper.*; + +/** + * Creates spans for JDBC {@link Statement} execution + */ +public abstract class TransactionInstrumentation extends HibernateInstrumentation { + private final ElementMatcher methodMatcher; + + TransactionInstrumentation(ElementMatcher methodMatcher) { + this.methodMatcher = methodMatcher; + } + + @Override + public ElementMatcher getTypeMatcherPreFilter() { + return nameStartsWith("org.hibernate"); + } + + @Override + public ElementMatcher getTypeMatcher() { + Junction type = hasSuperType(named("org.hibernate.Transaction")); + return not(isInterface()).and(type); + } + + @Override + public ElementMatcher getMethodMatcher() { + return methodMatcher; + } + + // @SuppressWarnings("DuplicatedCode") + public static class BeginInstrumentation extends TransactionInstrumentation { + + public BeginInstrumentation(ElasticApmTracer tracer) { + super(named("begin").and(takesArguments(0)).and(isPublic())); + } + + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class, inline = false) + public static void onAfterExecute(@Advice.This Object transaction, @Advice.Origin("#m") String methodName, + @Advice.Thrown @Nullable Throwable t) { + + String str = "After :" + methodName; + AbstractSpan parentSpan = tracer.getActive(); + hh.logMsg(str, "methodName=" + methodName, "parentSpan=" + parentSpan, "throwable=" + t); + if (parentSpan == null) + return; + Span span = tracer.getActiveSpan(); + hh.logMsg("tracer.getActiveSpan=" + span); + // if( span == null ) return; + if (t != null) { + hh.logMsg("Exception capture error span."); + if (span == null) { + parentSpan.captureException(t); + return; + } + span.captureException(t).deactivate().end(); + return; + } + + hh.createHibernateSpan(parentSpan, HIB_SUB_TYPE + " " + HIB_SPANNAME_TRANSACTION, // SPAN Name + HIB_SPAN_TYPE, HIB_SUB_TYPE, HIB_SPAN_ACTION_CREATE, null, null, methodName); + } + } + + /** + * Instruments: + *
    + * Gives more + * accurate details on open session. after this method "Session openSession()" + * execution Create a span for session open + * + *
+ */ + // @SuppressWarnings("DuplicatedCode") + public static class CommitInstrumentation extends TransactionInstrumentation { + + public CommitInstrumentation(ElasticApmTracer tracer) { + super(named("commit").and(takesArguments(0)).and(isPublic())); + } + + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class, inline = false) + public static void onAfterExecute(@Advice.This Object transaction, @Advice.Origin("#m") String methodName, + @Advice.Thrown @Nullable Throwable t) { + hh.handleTransactionAfterExecute(transaction,methodName, tracer.getActive(), tracer.getActiveSpan(), t); + } + } + + // @SuppressWarnings("DuplicatedCode") + // Not used this to be deleted. + public static class RollbackInstrumentation extends TransactionInstrumentation { + + public RollbackInstrumentation(ElasticApmTracer tracer) { + super(named("rollback").and(takesArguments(0)).and(isPublic())); + } + + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class, inline = false) + public static void onAfterExecute(@Advice.This Object transaction,@Advice.Origin("#m") String methodName, + @Advice.Thrown @Nullable Throwable t) { + hh.handleTransactionAfterExecute(transaction, methodName, tracer.getActive(), tracer.getActiveSpan(), t); + } + } + +} diff --git a/apm-agent-plugins/apm-hibernate-plugin/src/main/java/co/elastic/apm/agent/hibernate/helper/HibernateGlobalState.java b/apm-agent-plugins/apm-hibernate-plugin/src/main/java/co/elastic/apm/agent/hibernate/helper/HibernateGlobalState.java new file mode 100644 index 0000000000..44cb35c15c --- /dev/null +++ b/apm-agent-plugins/apm-hibernate-plugin/src/main/java/co/elastic/apm/agent/hibernate/helper/HibernateGlobalState.java @@ -0,0 +1,35 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. 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 co.elastic.apm.agent.hibernate.helper; + +import java.sql.Connection; + +import co.elastic.apm.agent.impl.transaction.Span; +import co.elastic.apm.agent.sdk.state.GlobalState; +import co.elastic.apm.agent.sdk.weakconcurrent.WeakConcurrent; +import co.elastic.apm.agent.sdk.weakconcurrent.WeakMap; + +@GlobalState +public class HibernateGlobalState { + public static final WeakMap objectSpanMap = WeakConcurrent.buildMap(); + + public static void clearInternalStorage() { + objectSpanMap.clear(); + } +} diff --git a/apm-agent-plugins/apm-hibernate-plugin/src/main/java/co/elastic/apm/agent/hibernate/helper/HibernateHelper.java b/apm-agent-plugins/apm-hibernate-plugin/src/main/java/co/elastic/apm/agent/hibernate/helper/HibernateHelper.java new file mode 100644 index 0000000000..1e9128c1c7 --- /dev/null +++ b/apm-agent-plugins/apm-hibernate-plugin/src/main/java/co/elastic/apm/agent/hibernate/helper/HibernateHelper.java @@ -0,0 +1,184 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. 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 co.elastic.apm.agent.hibernate.helper; + +import static co.elastic.apm.agent.hibernate.helper.HibernateGlobalState.objectSpanMap; + +import javax.annotation.Nullable; + +import org.hibernate.Transaction; +import org.hibernate.resource.transaction.spi.TransactionStatus; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import antlr.debug.Tracer; +import co.elastic.apm.agent.impl.transaction.AbstractSpan; +import co.elastic.apm.agent.impl.transaction.Span; + +public class HibernateHelper { + + private static final Logger logger = LoggerFactory.getLogger(HibernateHelper.class); + public static final String HIB_SPAN_TYPE = "orm"; + public static final String HIB_SUB_TYPE = "hibernate"; + + public static final String HIB_SPAN_ACTION_CREATE = "create"; + public static final String HIB_SPAN_ACTION_QUERY = "query"; + public static final String HIB_SPAN_ACTION_LOAD = "load"; + public static final String HIB_SPANNAME_ACTION_EXEC = "execute"; + + public static final String HIB_SPANNAME_SESSION = "session"; + public static final String HIB_SPANNAME_TRANSACTION = "transaction"; + public static final String HIB_SPANNAME_ERROR = "error"; + + + /** + * Maps the provided Span with the session object. + * @param obj + * @param span + * @param methodName + */ + public void mapObjectSpan(Object obj, Span span, String methodName) { + objectSpanMap.putIfAbsent(obj, span); + } + + /** + * Returns the span associated to session + * @param obj + * @return + */ + public Span getSpanForObject(Object obj) { + return objectSpanMap.get(obj); + } + + /** + * Removes Span for the session soon after the session closes. + * @param obj + * @return + */ + public Span removeObjectSpan(Object obj) { + return objectSpanMap.remove(obj); + } + + /** + * To avoid duplicate sessions + * @param obj + * @return + */ + + public boolean isObjectAlreadyCreated(Object obj) { + return objectSpanMap.get(obj) != null; + } + + + + public void logMsg(Object... objs) { + for (Object obj : objs) { + logger.debug("HIBERNATE: {}", obj); + } + } + + /** + * It creates a span + * @param parent + * @param spanName + * @param spanType + * @param subType + * @param action + * @param object + * @param entity + * @param methodName + * @return + */ + @Nullable + public Span createHibernateSpan(@Nullable AbstractSpan parent, + String spanName, String spanType, String subType, String action, + Object object, @Nullable Object entity, String methodName ) { + + String entityName = null; + if ( entity !=null ) + entityName = (entity instanceof String) ? (String)entity :entity.getClass().getSimpleName(); + + logMsg("Context Info:methodName,entityName, entity, session, parent.", + methodName,entityName, entity, object, parent); + + if ( parent == null ) { + logMsg("Null Parent Span..."); + return null; + } + + Span span = parent.createSpan().activate(); + span.withName(spanName,Span.PRIO_METHOD_SIGNATURE); + if (span.isSampled()) { + logMsg("Span is sampled:",span); + StringBuilder sn = span.getAndOverrideName(AbstractSpan.PRIO_DEFAULT); + logMsg("SpanName=" + sn); + + } + + span.withType(spanType); + span.withSubtype(subType); + span.withAction(action); + + logMsg("SPAN=",span); + //So that It will not create a duplicate Span, even if this call again and again. + if( object != null) + mapObjectSpan(object, span, methodName); + return span; + } + + public boolean isItTransactionSpan(Span span) { + + String name = span.getNameAsString(); + logMsg("SpanName is:",name); + if( name == null) return false; + return name.contains(HIB_SPANNAME_TRANSACTION); + } + + public void handleTransactionAfterExecute(Object transaction, String methodName,AbstractSpan parentSpan, Span span, Throwable t) { + + String str = "After :" + methodName; + logMsg(str, "methodName=" + methodName, "parentSpan=" + parentSpan, "throwable=" + t); + if (parentSpan == null) + return; + logMsg("tracer.getActiveSpan=" + span); + + if (transaction instanceof org.hibernate.engine.transaction.internal.TransactionImpl) { + Transaction tr = (org.hibernate.engine.transaction.internal.TransactionImpl)transaction; + TransactionStatus trStatus = tr.getStatus(); + logMsg(trStatus); + } + + if (span == null) {// Should not happen this. + logMsg("Span shouldn't be null here"); + return; + } + + + try { + if (t != null) { + logMsg("Exception capture error span={}", span); + span.captureException(t); + } + } finally { + if( isItTransactionSpan(span) ) + span.deactivate().end(); + } + } + +} diff --git a/apm-agent-plugins/apm-hibernate-plugin/src/main/java/co/elastic/apm/agent/hibernate/helper/package-info.java b/apm-agent-plugins/apm-hibernate-plugin/src/main/java/co/elastic/apm/agent/hibernate/helper/package-info.java new file mode 100644 index 0000000000..f4852d741d --- /dev/null +++ b/apm-agent-plugins/apm-hibernate-plugin/src/main/java/co/elastic/apm/agent/hibernate/helper/package-info.java @@ -0,0 +1,22 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. 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. + */ +@NonnullApi +package co.elastic.apm.agent.hibernate.helper; + +import co.elastic.apm.agent.sdk.NonnullApi; diff --git a/apm-agent-plugins/apm-hibernate-plugin/src/main/java/co/elastic/apm/agent/hibernate/package-info.java b/apm-agent-plugins/apm-hibernate-plugin/src/main/java/co/elastic/apm/agent/hibernate/package-info.java new file mode 100644 index 0000000000..fdfe8ee95b --- /dev/null +++ b/apm-agent-plugins/apm-hibernate-plugin/src/main/java/co/elastic/apm/agent/hibernate/package-info.java @@ -0,0 +1,22 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. 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. + */ +@NonnullApi +package co.elastic.apm.agent.hibernate; + +import co.elastic.apm.agent.sdk.NonnullApi; diff --git a/apm-agent-plugins/apm-hibernate-plugin/src/main/resources/META-INF/services/co.elastic.apm.agent.sdk.ElasticApmInstrumentation b/apm-agent-plugins/apm-hibernate-plugin/src/main/resources/META-INF/services/co.elastic.apm.agent.sdk.ElasticApmInstrumentation new file mode 100644 index 0000000000..9b3d02642e --- /dev/null +++ b/apm-agent-plugins/apm-hibernate-plugin/src/main/resources/META-INF/services/co.elastic.apm.agent.sdk.ElasticApmInstrumentation @@ -0,0 +1,7 @@ +co.elastic.apm.agent.hibernate.SessionInstrumentation$ReadObjectsInstrumentation +co.elastic.apm.agent.hibernate.SessionInstrumentation$CUDInstrumentation +co.elastic.apm.agent.hibernate.SessionInstrumentation$SessionCloseInstrumentation +co.elastic.apm.agent.hibernate.SessionBuilderInstrumentation$SessionOpenInstrumentation +co.elastic.apm.agent.hibernate.TransactionInstrumentation$BeginInstrumentation +co.elastic.apm.agent.hibernate.TransactionInstrumentation$CommitInstrumentation +co.elastic.apm.agent.hibernate.TransactionInstrumentation$RollbackInstrumentation \ No newline at end of file diff --git a/apm-agent-plugins/apm-jdbc-plugin/src/main/java/co/elastic/apm/agent/jdbc/JdbcInstrumentation.java b/apm-agent-plugins/apm-jdbc-plugin/src/main/java/co/elastic/apm/agent/jdbc/JdbcInstrumentation.java index 98a1eb7c76..4f9ab8f8e5 100644 --- a/apm-agent-plugins/apm-jdbc-plugin/src/main/java/co/elastic/apm/agent/jdbc/JdbcInstrumentation.java +++ b/apm-agent-plugins/apm-jdbc-plugin/src/main/java/co/elastic/apm/agent/jdbc/JdbcInstrumentation.java @@ -31,5 +31,4 @@ public abstract class JdbcInstrumentation extends TracerAwareInstrumentation { public final Collection getInstrumentationGroupNames() { return JDBC_GROUPS; } - } diff --git a/apm-agent-plugins/apm-jdk-httpclient-plugin/src/main/java/co/elastic/apm/agent/httpclient/AbstractHttpClientInstrumentation.java b/apm-agent-plugins/apm-jdk-httpclient-plugin/src/main/java/co/elastic/apm/agent/httpclient/AbstractHttpClientInstrumentation.java index 83466de502..58387fd890 100644 --- a/apm-agent-plugins/apm-jdk-httpclient-plugin/src/main/java/co/elastic/apm/agent/httpclient/AbstractHttpClientInstrumentation.java +++ b/apm-agent-plugins/apm-jdk-httpclient-plugin/src/main/java/co/elastic/apm/agent/httpclient/AbstractHttpClientInstrumentation.java @@ -19,7 +19,6 @@ package co.elastic.apm.agent.httpclient; import co.elastic.apm.agent.bci.TracerAwareInstrumentation; - import java.util.Arrays; import java.util.Collection; diff --git a/apm-agent-plugins/apm-micrometer-plugin/src/main/java/co/elastic/apm/agent/micrometer/MicrometerInstrumentation.java b/apm-agent-plugins/apm-micrometer-plugin/src/main/java/co/elastic/apm/agent/micrometer/MicrometerInstrumentation.java index c758778ca8..4b10a87238 100644 --- a/apm-agent-plugins/apm-micrometer-plugin/src/main/java/co/elastic/apm/agent/micrometer/MicrometerInstrumentation.java +++ b/apm-agent-plugins/apm-micrometer-plugin/src/main/java/co/elastic/apm/agent/micrometer/MicrometerInstrumentation.java @@ -61,5 +61,4 @@ public static void onExit(@Advice.This MeterRegistry meterRegistry) { reporter.registerMeterRegistry(meterRegistry); } } - } diff --git a/apm-agent-plugins/apm-profiling-plugin/src/main/resources/asyncprofiler/libasyncProfiler-linux-arm.so b/apm-agent-plugins/apm-profiling-plugin/src/main/resources/asyncprofiler/libasyncProfiler-linux-arm.so old mode 100755 new mode 100644 diff --git a/apm-agent-plugins/apm-profiling-plugin/src/main/resources/asyncprofiler/libasyncProfiler-linux-x64.so b/apm-agent-plugins/apm-profiling-plugin/src/main/resources/asyncprofiler/libasyncProfiler-linux-x64.so old mode 100755 new mode 100644 diff --git a/apm-agent-plugins/apm-profiling-plugin/src/main/resources/asyncprofiler/libasyncProfiler-linux-x86.so b/apm-agent-plugins/apm-profiling-plugin/src/main/resources/asyncprofiler/libasyncProfiler-linux-x86.so old mode 100755 new mode 100644 diff --git a/apm-agent-plugins/apm-scheduled-annotation-plugin/src/test/java/co/elastic/apm/agent/spring/scheduled/ScheduledTransactionNameInstrumentationTest.java b/apm-agent-plugins/apm-scheduled-annotation-plugin/src/test/java/co/elastic/apm/agent/spring/scheduled/ScheduledTransactionNameInstrumentationTest.java new file mode 100644 index 0000000000..32e719a808 --- /dev/null +++ b/apm-agent-plugins/apm-scheduled-annotation-plugin/src/test/java/co/elastic/apm/agent/spring/scheduled/ScheduledTransactionNameInstrumentationTest.java @@ -0,0 +1,127 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. 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 co.elastic.apm.agent.spring.scheduled; + +import co.elastic.apm.agent.AbstractInstrumentationTest; +import org.junit.jupiter.api.Test; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.scheduling.annotation.Schedules; + +import javax.ejb.Schedule; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.assertj.core.api.Assertions.assertThat; + + +class ScheduledTransactionNameInstrumentationTest extends AbstractInstrumentationTest { + + @Test + void testSpringScheduledAnnotatedMethodsAreTraced() { + SpringCounter springCounter = new SpringCounter(); + springCounter.scheduled(); + springCounter.scheduled(); + assertThat(reporter.getTransactions().size()).isEqualTo(springCounter.getInvocationCount()); + assertThat(reporter.getTransactions().get(0).getNameAsString()).isEqualTo("SpringCounter#scheduled"); + } + + @Test + void testSpringJ8RepeatableScheduledAnnotatedMethodsAreTraced() { + SpringCounter springCounter = new SpringCounter(); + springCounter.scheduledJava8Repeatable(); + springCounter.scheduledJava8Repeatable(); + assertThat(reporter.getTransactions().size()).isEqualTo(springCounter.getInvocationCount()); + assertThat(reporter.getTransactions().get(0).getNameAsString()).isEqualTo("SpringCounter#scheduledJava8Repeatable"); + } + + @Test + void testSpringJ7RepeatableScheduledAnnotatedMethodsAreTraced() { + SpringCounter springCounter = new SpringCounter(); + springCounter.scheduledJava7Repeatable(); + springCounter.scheduledJava7Repeatable(); + assertThat(reporter.getTransactions().size()).isEqualTo(springCounter.getInvocationCount()); + assertThat(reporter.getTransactions().get(0).getNameAsString()).isEqualTo("SpringCounter#scheduledJava7Repeatable"); + } + + @Test + void testJeeScheduledAnnotatedMethodsAreTraced() { + JeeCounter jeeCounter = new JeeCounter(); + jeeCounter.scheduled(); + jeeCounter.scheduled(); + assertThat(reporter.getTransactions().size()).isEqualTo(jeeCounter.getInvocationCount()); + assertThat(reporter.getTransactions().get(0).getNameAsString()).isEqualTo("JeeCounter#scheduled"); + } + + @Test + void testJeeJ7RepeatableScheduledAnnotatedMethodsAreTraced() { + JeeCounter jeeCounter = new JeeCounter(); + jeeCounter.scheduledJava7Repeatable(); + jeeCounter.scheduledJava7Repeatable(); + assertThat(reporter.getTransactions().size()).isEqualTo(jeeCounter.getInvocationCount()); + assertThat(reporter.getTransactions().get(0).getNameAsString()).isEqualTo("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(); + } + } +} diff --git a/apm-agent-plugins/apm-scheduled-annotation-plugin/src/test/java/co/elastic/apm/agent/spring/scheduled/TimerTaskInstrumentationTest.java b/apm-agent-plugins/apm-scheduled-annotation-plugin/src/test/java/co/elastic/apm/agent/spring/scheduled/TimerTaskInstrumentationTest.java new file mode 100644 index 0000000000..dc6859ee6a --- /dev/null +++ b/apm-agent-plugins/apm-scheduled-annotation-plugin/src/test/java/co/elastic/apm/agent/spring/scheduled/TimerTaskInstrumentationTest.java @@ -0,0 +1,100 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. 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 co.elastic.apm.agent.spring.scheduled; + +import co.elastic.apm.agent.AbstractInstrumentationTest; +import co.elastic.apm.agent.impl.transaction.AbstractSpan; +import co.elastic.apm.agent.impl.transaction.Transaction; +import org.junit.jupiter.api.Test; + +import java.util.Timer; +import java.util.TimerTask; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.assertj.core.api.Assertions.assertThat; + +public class TimerTaskInstrumentationTest extends AbstractInstrumentationTest { + + @Test + void testTimerTask_scheduleWithFixedRate() { + new Timer(true) + .scheduleAtFixedRate(new TestTimerTask(2), 1, 1); + + reporter.awaitTransactionCount(2); + + assertThat(reporter.getTransactions() + .stream() + .map(AbstractSpan::getNameAsString)) + .containsExactly("TestTimerTask#run", "TestTimerTask#run"); + assertThat(reporter.getTransactions() + .stream() + .map(Transaction::getFrameworkName)) + .containsExactly("TimerTask", "TimerTask"); + } + + @Test + void testTimerTask_scheduleWithFixedDelay() { + new Timer("Timer") + .schedule(new TestTimerTask(2), 1, 1); + + reporter.awaitTransactionCount(2); + + assertThat(reporter.getTransactions() + .stream() + .map(AbstractSpan::getNameAsString)) + .containsExactly("TestTimerTask#run", "TestTimerTask#run"); + } + + @Test + void testTimerTask_scheduleOnce() { + new Timer("Timer") + .schedule(new TestTimerTask(1), 1); + + reporter.awaitTransactionCount(1); + assertThat(reporter.getTransactions().get(0).getNameAsString()).isEqualTo("TestTimerTask#run"); + } + + @Test + void testTimerTask_withAnonymousClass() { + new Timer("Timer") + .schedule(new TimerTask() { + public void run() { + cancel(); + } + }, 1); + + reporter.awaitTransactionCount(1); + assertThat(reporter.getTransactions().get(0).getNameAsString()).isEqualTo("1#run"); + } + + public static class TestTimerTask extends TimerTask { + private final AtomicInteger credits; + + public TestTimerTask(int maxInvocations) { + credits = new AtomicInteger(maxInvocations); + } + + @Override + public void run() { + if (credits.decrementAndGet() == 0) { + cancel(); + } + } + } +} diff --git a/apm-agent-plugins/apm-servlet-plugin/src/main/java/co/elastic/apm/agent/servlet/ServletApiAdvice.java b/apm-agent-plugins/apm-servlet-plugin/src/main/java/co/elastic/apm/agent/servlet/ServletApiAdvice.java index 74d76223a3..56083b607c 100644 --- a/apm-agent-plugins/apm-servlet-plugin/src/main/java/co/elastic/apm/agent/servlet/ServletApiAdvice.java +++ b/apm-agent-plugins/apm-servlet-plugin/src/main/java/co/elastic/apm/agent/servlet/ServletApiAdvice.java @@ -262,4 +262,4 @@ private static boolean areNotEqual(@Nullable Object first, @Nullable Object seco return !first.equals(second); } } -} +} \ No newline at end of file diff --git a/apm-agent-plugins/apm-spring-webflux/apm-spring-webflux-client-plugin/pom.xml b/apm-agent-plugins/apm-spring-webflux/apm-spring-webflux-client-plugin/pom.xml new file mode 100644 index 0000000000..663af9cda9 --- /dev/null +++ b/apm-agent-plugins/apm-spring-webflux/apm-spring-webflux-client-plugin/pom.xml @@ -0,0 +1,68 @@ + + + 4.0.0 + + + co.elastic.apm + apm-spring-webflux + 1.29.0 + + + apm-spring-webflux-client-plugin + ${project.groupId}:${project.artifactId} + + + ${project.basedir}/../../.. + + 8 + ${maven.compiler.target} + + true + + + + + co.elastic.apm + apm-httpclient-core + ${project.version} + + + org.springframework + spring-web + ${version.spring} + provided + + + org.springframework + spring-webflux + ${version.spring} + provided + + + io.projectreactor.netty + reactor-netty-http + 1.0.7 + test + + + io.projectreactor + reactor-core + 3.4.11 + provided + + + org.slf4j + slf4j-log4j12 + 2.0.0-alpha1 + test + + + ${project.groupId} + apm-httpclient-core + ${project.version} + test-jar + test + + + + diff --git a/apm-agent-plugins/apm-spring-webflux/apm-spring-webflux-client-plugin/src/main/java/co/elastic/apm/agent/webfluxclient/AbstractWebClientInstrumentation.java b/apm-agent-plugins/apm-spring-webflux/apm-spring-webflux-client-plugin/src/main/java/co/elastic/apm/agent/webfluxclient/AbstractWebClientInstrumentation.java new file mode 100644 index 0000000000..5e2f504858 --- /dev/null +++ b/apm-agent-plugins/apm-spring-webflux/apm-spring-webflux-client-plugin/src/main/java/co/elastic/apm/agent/webfluxclient/AbstractWebClientInstrumentation.java @@ -0,0 +1,32 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. 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 co.elastic.apm.agent.webfluxclient; + +import co.elastic.apm.agent.bci.TracerAwareInstrumentation; + +import java.util.Arrays; +import java.util.Collection; + +public abstract class AbstractWebClientInstrumentation extends TracerAwareInstrumentation { + + @Override + public Collection getInstrumentationGroupNames() { + return Arrays.asList("http-client", "spring-webclient", "experimental"); + } +} diff --git a/apm-agent-plugins/apm-spring-webflux/apm-spring-webflux-client-plugin/src/main/java/co/elastic/apm/agent/webfluxclient/WebClientExchangeFunctionInstrumentation.java b/apm-agent-plugins/apm-spring-webflux/apm-spring-webflux-client-plugin/src/main/java/co/elastic/apm/agent/webfluxclient/WebClientExchangeFunctionInstrumentation.java new file mode 100644 index 0000000000..53f7f13bda --- /dev/null +++ b/apm-agent-plugins/apm-spring-webflux/apm-spring-webflux-client-plugin/src/main/java/co/elastic/apm/agent/webfluxclient/WebClientExchangeFunctionInstrumentation.java @@ -0,0 +1,105 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. 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 co.elastic.apm.agent.webfluxclient; + +import co.elastic.apm.agent.http.client.HttpClientHelper; +import co.elastic.apm.agent.impl.transaction.AbstractSpan; +import co.elastic.apm.agent.impl.transaction.Span; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.asm.Advice.AssignReturned.ToArguments.ToArgument; +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.implementation.bytecode.assign.Assigner; +import net.bytebuddy.matcher.ElementMatcher; +import org.reactivestreams.Publisher; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.web.reactive.function.client.ClientRequest; + +import javax.annotation.Nullable; +import java.net.URI; +import java.io.*; + +import static net.bytebuddy.matcher.ElementMatchers.hasSuperType; +import static net.bytebuddy.matcher.ElementMatchers.isInterface; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.not; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +public class WebClientExchangeFunctionInstrumentation extends AbstractWebClientInstrumentation { + + private static final Logger logger = LoggerFactory.getLogger(WebClientExchangeFunctionInstrumentation.class); + + @Override + public ElementMatcher getTypeMatcher() { + return hasSuperType(named("org.springframework.web.reactive.function.client.ExchangeFunction")) + .and(not(isInterface())); + } + + @Override + public ElementMatcher getMethodMatcher() { + return named("exchange") + .and(takesArgument(0, named("org.springframework.web.reactive.function.client.ClientRequest"))); + } + + public static class AdviceClass { + + @Nullable + @Advice.OnMethodEnter(suppress = Throwable.class, inline = false) + @Advice.AssignReturned.ToArguments(@ToArgument(index = 0, value = 0, typing = Assigner.Typing.DYNAMIC)) + public static Object[] onBefore(@Advice.Argument(0) ClientRequest clientRequest) { + final AbstractSpan parent = tracer.getActive(); + if (parent == null) { + return null; + } + ClientRequest.Builder builder = ClientRequest.from(clientRequest); + URI uri = clientRequest.url(); + Span span = HttpClientHelper.startHttpClientSpan(parent, clientRequest.method().name(), uri, uri.getHost()); + if (span != null) { + span.activate(); + span.propagateTraceContext(builder, WebClientRequestHeaderSetter.INSTANCE); + } else { + parent.propagateTraceContext(builder, WebClientRequestHeaderSetter.INSTANCE); + } + clientRequest = builder.build(); + return new Object[]{clientRequest, span}; + } + + + @Advice.AssignReturned.ToReturned(typing = Assigner.Typing.DYNAMIC) + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class, inline = false) + public static Object afterExecute(@Advice.Return @Nullable Publisher returnValue, + @Advice.Enter @Nullable Object[] spanRequestObj, + @Advice.Thrown @Nullable Throwable t) { + if (spanRequestObj == null || spanRequestObj.length < 2) { + return returnValue; + } + Object spanObj = spanRequestObj[1]; + if (!(spanObj instanceof Span)) { + return returnValue; + } + Span span = (Span) spanObj; + span = span.captureException(t).deactivate(); + if (t != null || returnValue == null) { + return returnValue; + } + return WebfluxClientHelper.wrapSubscriber(returnValue, span, tracer); + } + } +} diff --git a/apm-agent-plugins/apm-spring-webflux/apm-spring-webflux-client-plugin/src/main/java/co/elastic/apm/agent/webfluxclient/WebClientRequestHeaderSetter.java b/apm-agent-plugins/apm-spring-webflux/apm-spring-webflux-client-plugin/src/main/java/co/elastic/apm/agent/webfluxclient/WebClientRequestHeaderSetter.java new file mode 100644 index 0000000000..79cf6d53d7 --- /dev/null +++ b/apm-agent-plugins/apm-spring-webflux/apm-spring-webflux-client-plugin/src/main/java/co/elastic/apm/agent/webfluxclient/WebClientRequestHeaderSetter.java @@ -0,0 +1,32 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. 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 co.elastic.apm.agent.webfluxclient; + +import co.elastic.apm.agent.impl.transaction.TextHeaderSetter; +import org.springframework.web.reactive.function.client.ClientRequest; + +public class WebClientRequestHeaderSetter implements TextHeaderSetter { + + public static final WebClientRequestHeaderSetter INSTANCE = new WebClientRequestHeaderSetter(); + + @Override + public void setHeader(String headerName, String headerValue, ClientRequest.Builder request) { + request.header(headerName, headerValue); + } +} diff --git a/apm-agent-plugins/apm-spring-webflux/apm-spring-webflux-client-plugin/src/main/java/co/elastic/apm/agent/webfluxclient/WebfluxClientHelper.java b/apm-agent-plugins/apm-spring-webflux/apm-spring-webflux-client-plugin/src/main/java/co/elastic/apm/agent/webfluxclient/WebfluxClientHelper.java new file mode 100644 index 0000000000..a0849d7d43 --- /dev/null +++ b/apm-agent-plugins/apm-spring-webflux/apm-spring-webflux-client-plugin/src/main/java/co/elastic/apm/agent/webfluxclient/WebfluxClientHelper.java @@ -0,0 +1,62 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. 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 co.elastic.apm.agent.webfluxclient; + +import co.elastic.apm.agent.impl.Tracer; +import co.elastic.apm.agent.impl.transaction.Span; +import org.reactivestreams.Publisher; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.CoreSubscriber; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.core.publisher.Operators; + +import java.util.function.BiFunction; +import java.util.function.Function; + +public class WebfluxClientHelper { + + private static final Logger log = LoggerFactory.getLogger(WebfluxClientHelper.class); + + public static Publisher wrapSubscriber(Publisher publisher, final Span span, final Tracer tracer) { + + Function, ? extends Publisher> lift = Operators.liftPublisher( + new BiFunction, CoreSubscriber>() { + @Override + public CoreSubscriber apply(Publisher publisher, CoreSubscriber subscriber) { + log.trace("Trying to subscribe with span {}", span); + log.info("In Core subscriber of web client {}", span); + if (tracer.getActive() == null) { + return subscriber; + } + return new WebfluxClientSubscriber<>(subscriber, span, tracer); + } + } + ); + + if (publisher instanceof Mono) { + publisher = ((Mono) publisher).transform(lift); + } else if (publisher instanceof Flux) { + publisher = ((Flux) publisher).transform(lift); + } + + return publisher; + } +} diff --git a/apm-agent-plugins/apm-spring-webflux/apm-spring-webflux-client-plugin/src/main/java/co/elastic/apm/agent/webfluxclient/WebfluxClientSubscriber.java b/apm-agent-plugins/apm-spring-webflux/apm-spring-webflux-client-plugin/src/main/java/co/elastic/apm/agent/webfluxclient/WebfluxClientSubscriber.java new file mode 100644 index 0000000000..465fc21a92 --- /dev/null +++ b/apm-agent-plugins/apm-spring-webflux/apm-spring-webflux-client-plugin/src/main/java/co/elastic/apm/agent/webfluxclient/WebfluxClientSubscriber.java @@ -0,0 +1,202 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. 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 co.elastic.apm.agent.webfluxclient; + +import co.elastic.apm.agent.collections.WeakConcurrentProviderImpl; +import co.elastic.apm.agent.impl.Tracer; +import co.elastic.apm.agent.impl.transaction.Outcome; +import co.elastic.apm.agent.impl.transaction.Span; +import co.elastic.apm.agent.sdk.weakconcurrent.WeakMap; +import org.reactivestreams.Subscription; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.web.reactive.function.client.ClientResponse; +import reactor.core.CoreSubscriber; + +import javax.annotation.Nullable; + +public class WebfluxClientSubscriber implements CoreSubscriber, Subscription { + public static final Logger logger = LoggerFactory.getLogger(WebfluxClientSubscriber.class); + + private final Tracer tracer; + private CoreSubscriber subscriber; + private static final WeakMap, Span> spanMap = WeakConcurrentProviderImpl.createWeakSpanMap(); + private Subscription subscription; + + public WebfluxClientSubscriber(CoreSubscriber subscriber, Span span, Tracer tracer) { + this.subscriber = subscriber; + this.tracer = tracer; + + spanMap.put(this, span); + } + + @Override + public void onSubscribe(Subscription s) { + this.subscription = s; + Span span = getSpan(); + + boolean hasActivated = doEnter("onSubscribe", span); + Throwable thrown = null; + try { + subscriber.onSubscribe(this); + } catch (Throwable e) { + thrown = e; + throw e; + } finally { + doExit(hasActivated, "onSubscribe", span); + discardIf(thrown != null); + } + } + + @Override + public void onNext(T t) { + final Span span = getSpan(); + boolean hasActivated = doEnter("onNext", span); + Throwable thrown = null; + try { + if (t instanceof ClientResponse) { + ClientResponse clientResponse = (ClientResponse) t; + int statusCode = clientResponse.rawStatusCode(); + if (400 <= statusCode && statusCode < 500 && span.getOutcome() == null){ + span.withOutcome(Outcome.FAILURE); + } + span.getContext().getHttp().withStatusCode(clientResponse.rawStatusCode()); + } + subscriber.onNext(t); + } catch (Throwable e) { + thrown = e; + throw e; + } finally { + doExit(hasActivated, "onNext", span); + discardIf(thrown != null); + } + } + + @Override + public void onError(Throwable throwable) { + Span span = getSpan(); + boolean hasActivated = doEnter("onError", span); + try { + subscriber.onError(throwable); + span = span.withOutcome(Outcome.FAILURE); + } finally { + doExit(hasActivated, "onError", span); + discardIf(true); + endSpan(throwable, span); + } + } + + @Override + public void onComplete() { + final Span span = getSpan(); + boolean hasActivated = doEnter("onComplete", span); + try { + subscriber.onComplete(); + if (span.getOutcome() == null) { + span.withOutcome(Outcome.SUCCESS); + } + } finally { + doExit(hasActivated, "onComplete", span); + discardIf(true); + endSpan(null, span); + } + } + + @Override + public void request(long n) { + subscription.request(n); + } + + @Override + public void cancel() { + subscription.cancel(); + cancelSpan(); + } + + @Nullable + private Span getSpan() { + return spanMap.get(this); + } + + + private void discardIf(boolean condition) { + if (!condition) { + return; + } + spanMap.remove(this); + } + + private boolean doEnter(String method, @Nullable Span span) { + debugTrace(true, method, span); + + if (span == null || tracer.getActive() == span) { + // already activated or discarded + return false; + } + + span.activate(); + return true; + } + + private void doExit(boolean deactivate, String method, @Nullable Span span) { + debugTrace(false, method, span); + + if (span == null || !deactivate) { + return; + } + + if (span != tracer.getActive()) { + // don't attempt to deactivate if not the active one + return; + } + // the current context has been activated on enter thus must be the active one + span.deactivate(); + } + + private void debugTrace(boolean isEnter, String method, @Nullable Span span) { + if (!logger.isTraceEnabled()) { + return; + } + logger.trace("{} r2dbc {} {}", isEnter ? ">>" : "<<", method, span); + } + + private void endSpan(@Nullable Throwable thrown, @Nullable Span span) { + if (span == null) { + // already discarded + return; + } + span.captureException(thrown).end(); + } + + private void cancelSpan() { + Span span = getSpan(); + debugTrace(true, "cancelSpan", span); + try { + if (span == null) { + return; + } + endSpan(null, span); + + spanMap.remove(this); + } finally { + debugTrace(false, "cancelSpan", span); + } + } + +} diff --git a/apm-agent-plugins/apm-spring-webflux/apm-spring-webflux-client-plugin/src/main/resources/META-INF/services/co.elastic.apm.agent.sdk.ElasticApmInstrumentation b/apm-agent-plugins/apm-spring-webflux/apm-spring-webflux-client-plugin/src/main/resources/META-INF/services/co.elastic.apm.agent.sdk.ElasticApmInstrumentation new file mode 100644 index 0000000000..9d65957723 --- /dev/null +++ b/apm-agent-plugins/apm-spring-webflux/apm-spring-webflux-client-plugin/src/main/resources/META-INF/services/co.elastic.apm.agent.sdk.ElasticApmInstrumentation @@ -0,0 +1 @@ +co.elastic.apm.agent.webfluxclient.WebClientExchangeFunctionInstrumentation diff --git a/apm-agent-plugins/apm-spring-webflux/apm-spring-webflux-client-plugin/src/test/java/co/elastic/apm/agent/webflux/client/AbstractWebClientInstrumentationTest.java b/apm-agent-plugins/apm-spring-webflux/apm-spring-webflux-client-plugin/src/test/java/co/elastic/apm/agent/webflux/client/AbstractWebClientInstrumentationTest.java new file mode 100644 index 0000000000..e384b98b13 --- /dev/null +++ b/apm-agent-plugins/apm-spring-webflux/apm-spring-webflux-client-plugin/src/test/java/co/elastic/apm/agent/webflux/client/AbstractWebClientInstrumentationTest.java @@ -0,0 +1,62 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. 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 co.elastic.apm.agent.webflux.client; + +import co.elastic.apm.agent.httpclient.AbstractHttpClientInstrumentationTest; +import org.springframework.http.client.reactive.ReactorClientHttpConnector; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.netty.http.client.HttpClient; + +public abstract class AbstractWebClientInstrumentationTest extends AbstractHttpClientInstrumentationTest { + + protected final WebClient webClient; + + /** + * reactor.netty.http.client.HttpClient#followRedirect(boolean) will redirect when 301|302|307|308 + * by this reason I add workaround via BiConsumer. + */ + public AbstractWebClientInstrumentationTest() { + HttpClient httpClient = HttpClient.create().followRedirect((req, res) -> res.status().code() == 303); + webClient = WebClient.builder() + .clientConnector(new ReactorClientHttpConnector(httpClient)) + .build(); + } + + /** + * reactor.core.Exceptions$ReactiveException: java.net.UnknownHostException: failed to resolve 'user:passwd@localhost' after 2 queries + *

+ * at reactor.core.Exceptions.propagate(Exceptions.java:392) + * at reactor.core.publisher.BlockingSingleSubscriber.blockingGet(BlockingSingleSubscriber.java:97) + * at reactor.core.publisher.Mono.block(Mono.java:1706) + * at co.elastic.apm.agent.webflux.client.WebClientExchangeFunctionInstrumentationTest.performGet(WebClientExchangeFunctionInstrumentationTest.java:31) + * at co.elastic.apm.agent.httpclient.AbstractHttpClientInstrumentationTest.testHttpCallWithUserInfo(AbstractHttpClientInstrumentationTest.java:138) + * at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) + * at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) + * at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) + * at java.base/java.lang.reflect.Method.invoke(Method.java:566) + * at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59) + * at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12) + * at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56) + * at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17) + * at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26) + * at org.junit.internal.runners.statements.RunAfters.evaluate(RunAfters.java:27) + * + * @return + */ +} diff --git a/apm-agent-plugins/apm-spring-webflux/apm-spring-webflux-client-plugin/src/test/java/co/elastic/apm/agent/webflux/client/WebClientExchangeFunctionInstrumentationTest.java b/apm-agent-plugins/apm-spring-webflux/apm-spring-webflux-client-plugin/src/test/java/co/elastic/apm/agent/webflux/client/WebClientExchangeFunctionInstrumentationTest.java new file mode 100644 index 0000000000..f0ff673aa1 --- /dev/null +++ b/apm-agent-plugins/apm-spring-webflux/apm-spring-webflux-client-plugin/src/test/java/co/elastic/apm/agent/webflux/client/WebClientExchangeFunctionInstrumentationTest.java @@ -0,0 +1,37 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. 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 co.elastic.apm.agent.webflux.client; + +import org.springframework.web.reactive.function.client.ClientResponse; + + +public class WebClientExchangeFunctionInstrumentationTest extends AbstractWebClientInstrumentationTest { + + public WebClientExchangeFunctionInstrumentationTest() { + super(); + } + + @Override + protected void performGet(String path) throws Exception { + ClientResponse response = this.webClient.get() + .uri(path) + .exchange() + .block(); + } +} diff --git a/apm-agent-plugins/apm-spring-webflux/apm-spring-webflux-client-plugin/src/test/java/co/elastic/apm/agent/webflux/client/WebClientRetrieveInstrumentationTest.java b/apm-agent-plugins/apm-spring-webflux/apm-spring-webflux-client-plugin/src/test/java/co/elastic/apm/agent/webflux/client/WebClientRetrieveInstrumentationTest.java new file mode 100644 index 0000000000..e193bc364a --- /dev/null +++ b/apm-agent-plugins/apm-spring-webflux/apm-spring-webflux-client-plugin/src/test/java/co/elastic/apm/agent/webflux/client/WebClientRetrieveInstrumentationTest.java @@ -0,0 +1,36 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. 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 co.elastic.apm.agent.webflux.client; + +public class WebClientRetrieveInstrumentationTest extends AbstractWebClientInstrumentationTest { + + public WebClientRetrieveInstrumentationTest() { + super(); + } + + @Override + protected void performGet(String path) throws Exception { + this.webClient.get() + .uri(path) + .retrieve() + .bodyToMono(String.class) + .block(); + } + +} diff --git a/apm-agent-plugins/apm-spring-webflux/apm-spring-webflux-client-plugin/src/test/resources/log4j.properties b/apm-agent-plugins/apm-spring-webflux/apm-spring-webflux-client-plugin/src/test/resources/log4j.properties new file mode 100644 index 0000000000..cd41865993 --- /dev/null +++ b/apm-agent-plugins/apm-spring-webflux/apm-spring-webflux-client-plugin/src/test/resources/log4j.properties @@ -0,0 +1,12 @@ +# Set root logger level to DEBUG and its only appender to A1. +log4j.rootLogger=INFO, A1 +# A1 is set to be a ConsoleAppender. +log4j.appender.A1=org.apache.log4j.ConsoleAppender + +# A1 uses PatternLayout. +log4j.appender.A1.layout=org.apache.log4j.PatternLayout +log4j.appender.A1.layout.ConversionPattern=%-4r [%t] %-5p %c %x - %m%n + +log4j.logger.A1.org.eclipse.jetty=INFO +log4j.logger.A1.wiremock.org.eclipse=INFO +log4j.logger.A1.co.elastic.apm.agent=DEBUG diff --git a/apm-agent-plugins/apm-spring-webflux/apm-spring-webflux-plugin/src/main/java/co/elastic/apm/agent/springwebflux/TransactionAwareSubscriber.java b/apm-agent-plugins/apm-spring-webflux/apm-spring-webflux-plugin/src/main/java/co/elastic/apm/agent/springwebflux/TransactionAwareSubscriber.java index 826947df9d..efb362785a 100644 --- a/apm-agent-plugins/apm-spring-webflux/apm-spring-webflux-plugin/src/main/java/co/elastic/apm/agent/springwebflux/TransactionAwareSubscriber.java +++ b/apm-agent-plugins/apm-spring-webflux/apm-spring-webflux-plugin/src/main/java/co/elastic/apm/agent/springwebflux/TransactionAwareSubscriber.java @@ -24,8 +24,8 @@ import co.elastic.apm.agent.sdk.weakconcurrent.WeakMap; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; -import co.elastic.apm.agent.sdk.logging.Logger; -import co.elastic.apm.agent.sdk.logging.LoggerFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.web.server.ServerWebExchange; import reactor.core.CoreSubscriber; import reactor.util.context.Context; diff --git a/apm-agent-plugins/apm-spring-webflux/apm-spring-webflux-plugin/src/main/java/co/elastic/apm/agent/springwebflux/WebfluxHelper.java b/apm-agent-plugins/apm-spring-webflux/apm-spring-webflux-plugin/src/main/java/co/elastic/apm/agent/springwebflux/WebfluxHelper.java index 226d3c6d18..ad6b000a1d 100644 --- a/apm-agent-plugins/apm-spring-webflux/apm-spring-webflux-plugin/src/main/java/co/elastic/apm/agent/springwebflux/WebfluxHelper.java +++ b/apm-agent-plugins/apm-spring-webflux/apm-spring-webflux-plugin/src/main/java/co/elastic/apm/agent/springwebflux/WebfluxHelper.java @@ -31,8 +31,8 @@ import co.elastic.apm.agent.util.PotentiallyMultiValuedMap; import co.elastic.apm.agent.util.TransactionNameUtils; import org.reactivestreams.Publisher; -import co.elastic.apm.agent.sdk.logging.Logger; -import co.elastic.apm.agent.sdk.logging.LoggerFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.http.HttpCookie; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; diff --git a/apm-agent-plugins/apm-spring-webflux/apm-spring-webflux-plugin/src/main/java/co/elastic/apm/agent/springwebflux/WebfluxServletHelper.java b/apm-agent-plugins/apm-spring-webflux/apm-spring-webflux-plugin/src/main/java/co/elastic/apm/agent/springwebflux/WebfluxServletHelper.java index e114262d70..4df4205d06 100644 --- a/apm-agent-plugins/apm-spring-webflux/apm-spring-webflux-plugin/src/main/java/co/elastic/apm/agent/springwebflux/WebfluxServletHelper.java +++ b/apm-agent-plugins/apm-spring-webflux/apm-spring-webflux-plugin/src/main/java/co/elastic/apm/agent/springwebflux/WebfluxServletHelper.java @@ -20,8 +20,8 @@ import co.elastic.apm.agent.cache.WeakKeySoftValueLoadingCache; import co.elastic.apm.agent.impl.transaction.Transaction; -import co.elastic.apm.agent.sdk.logging.Logger; -import co.elastic.apm.agent.sdk.logging.LoggerFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.http.server.reactive.AbstractServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.web.server.ServerWebExchange; diff --git a/apm-agent-plugins/apm-spring-webflux/apm-spring-webflux-testapp/src/main/java/co/elastic/apm/agent/springwebflux/testapp/GreetingAnnotated.java b/apm-agent-plugins/apm-spring-webflux/apm-spring-webflux-testapp/src/main/java/co/elastic/apm/agent/springwebflux/testapp/GreetingAnnotated.java index 785941cbd9..63d8082971 100644 --- a/apm-agent-plugins/apm-spring-webflux/apm-spring-webflux-testapp/src/main/java/co/elastic/apm/agent/springwebflux/testapp/GreetingAnnotated.java +++ b/apm-agent-plugins/apm-spring-webflux/apm-spring-webflux-testapp/src/main/java/co/elastic/apm/agent/springwebflux/testapp/GreetingAnnotated.java @@ -22,8 +22,8 @@ import co.elastic.apm.agent.impl.GlobalTracer; import co.elastic.apm.agent.impl.transaction.AbstractSpan; import co.elastic.apm.agent.impl.transaction.Transaction; -import co.elastic.apm.agent.sdk.logging.Logger; -import co.elastic.apm.agent.sdk.logging.LoggerFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; diff --git a/apm-agent-plugins/apm-spring-webflux/apm-spring-webflux-testapp/src/main/java/co/elastic/apm/agent/springwebflux/testapp/WebFluxApplication.java b/apm-agent-plugins/apm-spring-webflux/apm-spring-webflux-testapp/src/main/java/co/elastic/apm/agent/springwebflux/testapp/WebFluxApplication.java index 457b043346..df713618d4 100644 --- a/apm-agent-plugins/apm-spring-webflux/apm-spring-webflux-testapp/src/main/java/co/elastic/apm/agent/springwebflux/testapp/WebFluxApplication.java +++ b/apm-agent-plugins/apm-spring-webflux/apm-spring-webflux-testapp/src/main/java/co/elastic/apm/agent/springwebflux/testapp/WebFluxApplication.java @@ -18,8 +18,8 @@ */ package co.elastic.apm.agent.springwebflux.testapp; -import co.elastic.apm.agent.sdk.logging.Logger; -import co.elastic.apm.agent.sdk.logging.LoggerFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.boot.Banner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; diff --git a/apm-agent-plugins/apm-spring-webflux/pom.xml b/apm-agent-plugins/apm-spring-webflux/pom.xml index 26e6dcbea6..5aef3cdedc 100644 --- a/apm-agent-plugins/apm-spring-webflux/pom.xml +++ b/apm-agent-plugins/apm-spring-webflux/pom.xml @@ -26,6 +26,7 @@ apm-spring-webflux-plugin + apm-spring-webflux-client-plugin apm-spring-webflux-testapp diff --git a/apm-agent-plugins/pom.xml b/apm-agent-plugins/pom.xml index d790f0318a..313122d259 100644 --- a/apm-agent-plugins/pom.xml +++ b/apm-agent-plugins/pom.xml @@ -20,6 +20,7 @@ apm-jaxrs-plugin apm-jdbc-plugin + apm-hibernate-plugin apm-jsf-plugin apm-opentracing-plugin apm-servlet-plugin diff --git a/apm-agent/pom.xml b/apm-agent/pom.xml index 8023c02daf..a36aa49649 100644 --- a/apm-agent/pom.xml +++ b/apm-agent/pom.xml @@ -261,6 +261,11 @@ apm-scala-concurrent-plugin ${project.version} + + ${project.groupId} + apm-hibernate-plugin + ${project.version} + ${project.groupId} apm-scheduled-annotation-plugin @@ -281,6 +286,11 @@ apm-spring-resttemplate-plugin ${project.version} + + ${project.groupId} + apm-spring-webflux-client-plugin + ${project.version} + ${project.groupId} apm-spring-webflux-plugin diff --git a/docs/metrics.asciidoc b/docs/metrics.asciidoc index a953b9c5a9..c13c4bb07e 100644 --- a/docs/metrics.asciidoc +++ b/docs/metrics.asciidoc @@ -567,6 +567,181 @@ Fields: -- +*`FunctionCounter`*:: ++ +-- +Fields: + +* `${name}`: The value of `functionCounter.count()` (the delta when using `CountingMode.STEP`). +-- + + +[float] +[[metrics-micrometer]] +=== Custom metrics using Micrometer + +The Elastic APM Java agent lets you use the popular metrics collection framework https://micrometer.io/[Micrometer] to track custom application metrics. + +Some use cases for tracking custom metrics from your application include monitoring performance-related things like cache statistics, thread pools, or page hits. +However, you can also track business-related metrics such as revenue and correlate them with performance metrics. +Metrics registered to a Micrometer `MeterRegistry` are aggregated in memory and reported every <>. +Based on the metadata about the service and the timestamp, you can correlate metrics with traces. +The advantage is that the metrics won't be affected by the +<> and that they usually take up less space. +That is because not every event is stored individually. + +The limitation of tracking metrics is that you won't be able to attribute a value to a specific transaction. +If you'd like to do that, <> to your transaction instead of tracking the metric with micrometer. +The tradeoff here is that you either have to do 100% sampling or account for the missing events. +The reason for that is that if you set your sampling rate to 10%, for example, +you'll only be storing one out of 10 requests. +The labels you set on non-sampled transactions will be lost. + +[float] +[[metrics-micrometer-get-started-existing]] +==== Get started with existing Micrometer setup + +You only have to attach the agent, and you're done. +The agent automatically detects all `MeterRegistry` instances and reports all metrics to APM Server in addition to where they originally report. +When attaching the agent after the application has already started, +the agent detects a `MeterRegistry` when calling any public method on it. +If you are using multiple registries within a `CompoundMeterRegistry`, +the agent makes sure to only report the metrics once. + +[float] +[[metrics-micrometer-get-started-from-scratch]] +==== Get started from scratch + +Declare a dependency to Micrometer: +```xml + + io.micrometer + micrometer-core + ${micrometer.version} + +``` + +Create a Micrometer `MeterRegistry`. +```java +MeterRegistry registry = new SimpleMeterRegistry(new SimpleConfig() { + + @Override + public CountingMode mode() { + // to report the delta since the last report + // this makes building dashbaords a bit easier + return CountingMode.STEP; + } + + @Override + public Duration step() { + // the duration should match metrics_interval, which defaults to 30s + return Duration.ofSeconds(30); + } + + @Override + public String get(String key) { + return null; + } + }, Clock.SYSTEM); + +``` + +When using Spring Boot, you can use the `management.metrics.export.simple` prefix to configure via `application.properties` + +``` +management.metrics.export.simple.enabled=true +management.metrics.export.simple.step=1m +management.metrics.export.simple.mode=STEP +``` + +[float] +[[metrics-micrometer-fields]] +==== Supported Meters + +This section lists all supported Micrometer `Meter` s and describes how they are mapped to Elasticsearch documents. + +Micrometer tags are nested under `labels`. Example: + +[source,json] +---- +"labels": { + "tagKey1": "tagLabel1", + "tagKey2": "tagLabel2", +} +---- + +Labels are great to break down metrics by different dimensions. +Although there is no upper limit, note that a high number of distinct values per label (aka high cardinality) may lead to higher memory usage, +higher index sizes, and slower queries. +Also, make sure the number of distinct tag keys is limited to avoid {ref}/mapping.html#mapping-limit-settings[mapping explosions]. + +*`Timer`*:: ++ +-- +Fields: + +* `${name}.sum.us`: The total time of recorded events (the delta when using `CountingMode.STEP`). + This is equivalent to `timer.totalTime(TimeUnit.MICROSECONDS)`. +* `${name}.count`: The number of times that stop has been called on this timer (the delta when using `CountingMode.STEP`). + This is equivalent to `timer.count()`. + +-- + + +*`FunctionTimer`*:: ++ +-- +Fields: + +* `${name}.sum.us`: The total time of all occurrences of the timed event (the delta when using `CountingMode.STEP`). + This is equivalent to `functionTimer.totalTime(TimeUnit.MICROSECONDS)`. +* `${name}.count`: The total number of occurrences of the timed event (the delta when using `CountingMode.STEP`). + This is equivalent to `functionTimer.count()`. +-- + + +*`LongTaskTimer`*:: ++ +-- +Fields: + +* `${name}.sum.us`: The cumulative duration of all current tasks (the delta when using `CountingMode.STEP`). + This is equivalent to `longTaskTimer.totalTime(TimeUnit.MICROSECONDS)`. +* `${name}.count`: The current number of tasks being executed (the delta when using `CountingMode.STEP`) + This is equivalent to `longTaskTimer.activeTasks()`. +-- + + +*`DistributionSummary`*:: ++ +-- +Fields: + +* `${name}.sum`: The total amount of all recorded events (the delta when using `CountingMode.STEP`). + This is equivalent to `distributionSummary.totalAmount()`. +* `${name}.count`: The number of times that record has been called (the delta when using `CountingMode.STEP`). + This is equivalent to `distributionSummary.count()`. +-- + + +*`Gauge`*:: ++ +-- +Fields: + +* `${name}`: The value of `gauge.value()`. +-- + + +*`Counter`*:: ++ +-- +Fields: + +* `${name}`: The value of `counter.count()` (the delta when using `CountingMode.STEP`). +-- + + *`FunctionCounter`*:: + -- diff --git a/elastic-apm-agent/pom.xml b/elastic-apm-agent/pom.xml index 3c6b921728..da9eaec86c 100644 --- a/elastic-apm-agent/pom.xml +++ b/elastic-apm-agent/pom.xml @@ -10,6 +10,7 @@ elastic-apm-agent ${project.groupId}:${project.artifactId} + jar ${project.basedir}/.. @@ -24,9 +25,20 @@ apm-agent-common ${project.version} + + org.json + json + 20200518 + + + commons-codec + commons-codec + 1.9 + + org.apache.maven.plugins diff --git a/elastic-apm-agent/src/main/java/co/elastic/apm/agent/premain/AgentMain.java b/elastic-apm-agent/src/main/java/co/elastic/apm/agent/premain/AgentMain.java index 4f37a339ce..17a0f389ca 100644 --- a/elastic-apm-agent/src/main/java/co/elastic/apm/agent/premain/AgentMain.java +++ b/elastic-apm-agent/src/main/java/co/elastic/apm/agent/premain/AgentMain.java @@ -61,6 +61,7 @@ public static void agentmain(String agentArguments, Instrumentation instrumentat } public synchronized static void init(String agentArguments, Instrumentation instrumentation, boolean premain) { + SFsetAgentParameters(); if (Boolean.getBoolean("ElasticApm.attached")) { // agent is already attached; don't attach twice // don't fail as this is a valid case @@ -99,6 +100,16 @@ public synchronized static void init(String agentArguments, Instrumentation inst } } + private static void SFsetAgentParameters() { + try { + SFAgentUtil sfUtil = new SFAgentUtil(); + sfUtil.setAgentInputParams(); + } catch (Exception e) { + System.out.println("Exception in SFsetAgentParameters: " + e.getMessage()); + e.printStackTrace(); + } + } + /** * Returns whether agent initialization should be delayed when occurring through the {@code premain} route. * This works around a JVM bug (https://bugs.openjdk.java.net/browse/JDK-8041920) causing JIT fatal error if diff --git a/elastic-apm-agent/src/main/java/co/elastic/apm/agent/premain/SFAgentUtil.java b/elastic-apm-agent/src/main/java/co/elastic/apm/agent/premain/SFAgentUtil.java new file mode 100644 index 0000000000..a7e528edb3 --- /dev/null +++ b/elastic-apm-agent/src/main/java/co/elastic/apm/agent/premain/SFAgentUtil.java @@ -0,0 +1,208 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. 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 co.elastic.apm.agent.premain; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.nio.file.FileSystems; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; + +import javax.crypto.Cipher; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; + +import org.apache.commons.codec.binary.Base64; +import org.json.JSONObject; + +public class SFAgentUtil { + private static byte[] key; + + public SFConfigInfo parseSFAgentYamlFile() { + FileReader fr = null; + try { + File file = new File(setSFtraceConfig()); + fr = new FileReader(file); + BufferedReader br = new BufferedReader(fr); + SFConfigInfo cfgInfo = new SFConfigInfo(); + SFTagInfo tagInfo = new SFTagInfo(); + String line; + while ((line = br.readLine()) != null) { + String[] keyValue = line.split(":"); + if (keyValue.length == 2) { + String key = keyValue[0].trim(); + String value = keyValue[1].trim(); + if (key.equals("key")) { + cfgInfo.setKey(value); + continue; + } + if (key.equals("appName")) { + tagInfo.setAppName(value); + continue; + } + if (key.equals("projectName")) + tagInfo.setProjectName(value); + } + } + cfgInfo.setTags(tagInfo); + String serviceName = System.getProperty("sftrace.service_name"); + if (serviceName != null && !serviceName.isEmpty()) + cfgInfo.setServiceName(serviceName); + return cfgInfo; + } catch (IOException e) { + System.out.println("Exception in SFparseSFAgentYamlFile: " + e.getMessage()); + e.printStackTrace(); + return null; + } catch (Exception e) { + System.out.println("Exception in SFparseSFAgentYamlFile: " + e.getMessage()); + e.printStackTrace(); + return null; + } finally { + try { + fr.close(); + } catch (IOException iOException) { + + } catch (Exception exception) {} + } + } + + public String setSFtraceConfig() { + + String sfConfFileName = "config.yaml"; + String s = FileSystems.getDefault().getSeparator(); + String confProp = System.getProperty("sftrace.config"); + String confEnv = System.getenv("SFTRACE_CONFIG"); + String sfConf = confEnv != null ? confEnv : + confProp != null ? confProp: + "/opt/sfagent"; //Fallback to Linux path + + return sfConf+s+sfConfFileName; + + } + + public void setAgentInputParams() { + try { + SFConfigInfo cfgInfo = parseSFAgentYamlFile(); + if (cfgInfo == null) { + cfgInfo = getConfigFromSystemProperty(); + if (cfgInfo == null) { + System.out.println("Input parameters not found. Agent will not be attahced correctly"); + return; + } + } + String decryptedValue = decrypt(cfgInfo.getKey(), "SnappyFlow123456"); + JSONObject jsonbObj = new JSONObject(decryptedValue); + String URL = jsonbObj.get("trace_server_url").toString(); + System.out.println("SERVER URL: " + URL); + System.setProperty("elastic.apm.server_urls", URL); + System.setProperty("elastic.apm.verify_server_cert", "false"); + System.setProperty("elastic.apm.central_config", "false"); + String serviceName = cfgInfo.getServiceName(); + if (serviceName != null && !serviceName.isEmpty()) + System.setProperty("elastic.apm.service_name", serviceName); + String profileId = jsonbObj.get("profile_id").toString(); + String globalLabels = System.getProperty("elastic.apm.global_labels"); + if (globalLabels != null && !globalLabels.isEmpty()) { + globalLabels = globalLabels + ","; + } else { + globalLabels = ""; + } + String newLabels = ""; + if (cfgInfo.getTags() != null) + newLabels = "_tag_appName=" + cfgInfo.getTags().getAppName() + ",_tag_projectName=" + cfgInfo.getTags().getProjectName(); + if (!newLabels.isEmpty()) + newLabels = newLabels + ","; + newLabels = newLabels + "_tag_profileId=" + profileId; + globalLabels = globalLabels + newLabels; + System.setProperty("elastic.apm.global_labels", globalLabels); + } catch (Exception e) { + System.out.println("Exception in SFsetAgentInputParams: " + e.getMessage()); + e.printStackTrace(); + } + } + + public SFConfigInfo getConfigFromSystemProperty() { + try { + SFConfigInfo cfgInfo = null; + String profileKey = System.getenv("SFTRACE_PROFILE_KEY"); + String projectName = System.getenv("SFTRACE_PROJECT_NAME"); + String appName = System.getenv("SFTRACE_APP_NAME"); + String serviceName = System.getenv("SFTRACE_SERVICE_NAME"); + if (profileKey != null && !profileKey.isEmpty()) { + cfgInfo = new SFConfigInfo(); + cfgInfo.setKey(profileKey); + } + if (serviceName != null && !serviceName.isEmpty()) { + if (cfgInfo == null) + cfgInfo = new SFConfigInfo(); + cfgInfo.setServiceName(serviceName); + } + SFTagInfo tagInfo = new SFTagInfo(); + if (projectName != null && !projectName.isEmpty()) + tagInfo.setProjectName(projectName); + if (appName != null && !appName.isEmpty()) + tagInfo.setAppName(appName); + if (cfgInfo != null) { + if (tagInfo != null) + cfgInfo.setTags(tagInfo); + return cfgInfo; + } + return null; + } catch (Exception e) { + System.out.println("Exception in SFgetConfigFromSystemProperty: " + e.getMessage()); + e.printStackTrace(); + return null; + } + } + + public static String decrypt(String strToDecrypt, String secret) { + try { + byte[] decodedArray = Base64.decodeBase64(strToDecrypt.getBytes()); + setKey(secret); + byte[] ivArray = Arrays.copyOfRange(decodedArray, 0, 16); + byte[] toDecryptArray = Arrays.copyOfRange(decodedArray, 16, decodedArray.length); + IvParameterSpec iv = new IvParameterSpec(ivArray); + SecretKeySpec skeySpec = new SecretKeySpec(secret.getBytes("UTF-8"), "AES"); + Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING"); + cipher.init(2, skeySpec, iv); + return new String(cipher.doFinal(toDecryptArray)); + } catch (Exception e) { + System.err.println("Exception in decrypt()"); + return null; + } + } + + public static void setKey(String myKey) { + MessageDigest sha = null; + try { + key = myKey.getBytes("UTF-8"); + sha = MessageDigest.getInstance("SHA-1"); + key = sha.digest(key); + key = Arrays.copyOf(key, 16); + } catch (NoSuchAlgorithmException e) { + e.printStackTrace(); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } + } +} diff --git a/elastic-apm-agent/src/main/java/co/elastic/apm/agent/premain/SFConfigInfo.java b/elastic-apm-agent/src/main/java/co/elastic/apm/agent/premain/SFConfigInfo.java new file mode 100644 index 0000000000..36bab1c2fd --- /dev/null +++ b/elastic-apm-agent/src/main/java/co/elastic/apm/agent/premain/SFConfigInfo.java @@ -0,0 +1,52 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. 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 co.elastic.apm.agent.premain; + +public class SFConfigInfo { + private String key; + + private String serviceName; + + private SFTagInfo tags; + + public String getKey() { + return this.key; + } + + public void setKey(String key) { + this.key = key; + } + + public String getServiceName() { + return this.serviceName; + } + + public void setServiceName(String serviceName) { + this.serviceName = serviceName; + } + + public SFTagInfo getTags() { + return this.tags; + } + + public void setTags(SFTagInfo tags) { + this.tags = tags; + } +} + diff --git a/elastic-apm-agent/src/main/java/co/elastic/apm/agent/premain/SFTagInfo.java b/elastic-apm-agent/src/main/java/co/elastic/apm/agent/premain/SFTagInfo.java new file mode 100644 index 0000000000..abb010ff5a --- /dev/null +++ b/elastic-apm-agent/src/main/java/co/elastic/apm/agent/premain/SFTagInfo.java @@ -0,0 +1,52 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. 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 co.elastic.apm.agent.premain; + +public class SFTagInfo { + private String Name; + + private String appName; + + private String projectName; + + public String getName() { + return this.Name; + } + + public void setName(String name) { + this.Name = name; + } + + public String getAppName() { + return this.appName; + } + + public void setAppName(String appName) { + this.appName = appName; + } + + public String getProjectName() { + return this.projectName; + } + + public void setProjectName(String projectName) { + this.projectName = projectName; + } +} + diff --git a/elasticapm.properties b/elasticapm.properties new file mode 100644 index 0000000000..de49cfcdd7 --- /dev/null +++ b/elasticapm.properties @@ -0,0 +1,14 @@ +#******************stack trace configurations******************** + +#Will limit stack trace collection to spans with durations equal to or longer than the given value +span_frames_min_duration=1000ms + +#Maximum number of frames to be collected per span. Setting to 0 will disable stack trace collection. +stack_trace_limit=1 + +#****************transaction configurations*************************** +#Values can be set between 0.0 to 1.0. Setting sampling rate still records overall time and the result for unsampled transactions, but no context information, labels, or spans will be captured +#transaction_sample_rate=1.0 + +#Limits the amount of spans that are recorded per transaction +#transaction_max_spans=500 diff --git a/pom.xml b/pom.xml index 40a17cc2b9..fda6514d41 100644 --- a/pom.xml +++ b/pom.xml @@ -78,7 +78,6 @@ 7 11 - ${maven.compiler.target} ${maven.compiler.testTarget} diff --git a/scripts/jenkins/build_docker.sh b/scripts/jenkins/build_docker.sh deleted file mode 100755 index 13d73139e9..0000000000 --- a/scripts/jenkins/build_docker.sh +++ /dev/null @@ -1,72 +0,0 @@ -#!/usr/bin/env bash - -# See full documentation in the "Creating and publishing Docker images" section -# of CONTRIBUTING.md - -set -euxo pipefail - -if ! command -v docker -then - echo "ERROR: Building Docker image requires Docker binary to be installed" && exit 1 -elif ! docker version -then - echo "ERROR: Building Docker image requires Docker daemon to be running" && exit 1 -fi - -echo "INFO: Determining latest tag" -if [ ! -z ${TAG_NAME+x} ] -then - echo "INFO: Detected TAG_NAME variable. Probably a Jenkins instance." - readonly GIT_TAG_DEFAULT=$(echo $TAG_NAME|sed s/^v//) -else - echo "INFO: Did not detect TAG_NAME. Examining git log for latest tag" - readonly GIT_TAG_DEFAULT=$(git describe --abbrev=0|sed s/^v//) -fi - -readonly GIT_TAG=${GIT_TAG:-$GIT_TAG_DEFAULT} - -readonly SCRIPT_PATH="$( cd "$(dirname "$0")" ; pwd -P )" -readonly PROJECT_ROOT=$SCRIPT_PATH/../../ -readonly NAMESPACE="observability" - -if [ "$(ls -A ${PROJECT_ROOT}elastic-apm-agent/target/*.jar)" ] -then - # We have build files to use - echo "INFO: Found local build artifact. Using locally built for Docker build" - find -E ${PROJECT_ROOT}elastic-apm-agent/target -regex '.*/elastic-apm-agent-[0-9]+.[0-9]+.[0-9]+(-SNAPSHOT)?.jar' -exec cp {} ${PROJECT_ROOT}apm-agent-java.jar \; || echo "INFO: No locally built image found" -elif [ ! -z ${SONATYPE_FALLBACK+x} ] -then - echo "INFO: No local build artifact and SONATYPE_FALLBACK. Falling back to downloading artifact from Sonatype Nexus repository for version $GIT_TAG" - if ! command -v curl - then - echo "ERROR: Pulling images from Sonatype Nexus repo requires cURL to be installed" && exit 1 - fi - curl -L -s -o apm-agent-java.jar \ - "http://repository.sonatype.org/service/local/artifact/maven/redirect?r=central-proxy&g=co.elastic.apm&a=elastic-apm-agent&v=$GIT_TAG" - else - echo "ERROR: No suitable build artifact was found. Re-running this script with the SONATYPE_FALLBACK variable set to true will try to use the Sonatype artifact for the latest tag" - exit 1 -fi - -echo "INFO: Starting Docker build for version $GIT_TAG" - -docker build -t docker.elastic.co/$NAMESPACE/apm-agent-java:$GIT_TAG \ - --build-arg JAR_FILE=apm-agent-java.jar . - -if [ $? -eq 0 ] -then - echo "INFO: Docker image built successfully" -else - echo "ERROR: Problem building Docker image!" -fi - -function finish { - - if [ -f apm-agent-java.jar ] - then - echo "INFO: Cleaning up downloaded artifact" - rm apm-agent-java.jar - fi -} - -trap finish EXIT diff --git a/scripts/jenkins/check_maven.sh b/scripts/jenkins/check_maven.sh deleted file mode 100755 index aaaae1ae19..0000000000 --- a/scripts/jenkins/check_maven.sh +++ /dev/null @@ -1,66 +0,0 @@ -#!/usr/bin/env bash -set -eo pipefail - -# Use: check_maven.sh --url https://status.maven.org/api/v2/summary.json --component OSSRH -# -# This script checks a status page to determine if a service is down. If the requested service -# is up, no output is returned to standard out and a return code of 0 is set. If a service is down -# then a message which be printed to standard out and a return code of 1 is set. - -POSITIONAL=() -while [[ $# -gt 0 ]] -do -key="$1" - -JQ=$(which jq) -if [ -z $JQ ]; -then - echo "Must have jq installed" - exit 1 -fi - -CURL=$(which curl) -if [ -z $CURL ]; -then - echo "Must have curl installed" - exit 1 -fi - -case $key in - -f|--component) - FILTER="$2" - shift - shift - ;; - -u|--url) - URL="$2" - shift - shift - ;; - -h|--help) - echo "Use: check_maven.sh --url https://status.maven.org/api/v2/summary.json --component OSSRH" - exit 0 - ;; - *) # unknown option - POSITIONAL+=("$1") # save it in an array for later - shift # past argument - ;; -esac -done - -NON_OP=$($CURL -s $URL | $JQ '.components|map(select(."status"!="operational"))|map(.name)') - -if [[ $NON_OP == "[]" ]]; -then -exit 0 -else - if [ ${FILTER+x} ]; - then - FILT_RET=$(echo $NON_OP | $JQ "map(select(.==\"${COMPONENT}\"))" | tr -d "\n") - echo "The following services are down: $FILT_RET. Check failed." - else - RET=$(echo $NON_OP |tr -d "\n") - echo "The following services are down: $RET. Check failed" - exit 1 - fi -fi diff --git a/scripts/jenkins/fetch_nexus_id.sh b/scripts/jenkins/fetch_nexus_id.sh deleted file mode 100755 index 17cdfe3598..0000000000 --- a/scripts/jenkins/fetch_nexus_id.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/env bash - -export VAULT_TOKEN=$(vault write -field=token auth/approle/login role_id="$VAULT_ROLE_ID" secret_id="$VAULT_SECRET_ID") -vault read -field=staging-profile-id secret/apm-team/ci/nexus diff --git a/scripts/jenkins/push_docker.sh b/scripts/jenkins/push_docker.sh deleted file mode 100755 index 8d6715e8f2..0000000000 --- a/scripts/jenkins/push_docker.sh +++ /dev/null @@ -1,55 +0,0 @@ -#!/usr/bin/env bash - -# See full documentation in the "Creating and publishing Docker images" section -# of CONTRIBUTING.md - -set -euxo pipefail - -# This script is present on workers but may not be present in a development -# environment. - -if [ ${WORKSPACE+x} ] # We are on a CI worker -then - source /usr/local/bin/bash_standard_lib.sh -fi - -readonly RETRIES=3 - -# This script is intended to work in conjunction with the build_docker -# script. It assumes that build_docker.sh has been run at least once, thereby -# creating a Docker image to push. If this script does not detect an image -# to be uploaded, it will fail. - -# This script is intended to be run from a CI job and will not work if run in -# standalone manner unless certain envrionment variables are set. - -# 1. Grab the tag we are working with - -echo "INFO: Determining latest tag" -if [ ! -z ${TAG_NAME+x} ] -then - echo "INFO: Detected TAG_NAME variable. Probably a Jenkins instance." - readonly GIT_TAG_DEFAULT=$(echo $TAG_NAME|sed s/^v//) -else - echo "INFO: Did not detect TAG_NAME. Examining git log for latest tag" - readonly GIT_TAG_DEFAULT=$(git describe --abbrev=0|sed s/^v//) -fi - -readonly CUR_TAG=${CUR_TAG:-$GIT_TAG_DEFAULT} - -# 2. Construct the image:tag that we are working with -# This is roughly //image -readonly DOCKER_PUSH_IMAGE="docker.elastic.co/observability/apm-agent-java:$CUR_TAG" - -# 3. Proceed with pushing to the registry -readonly DOCKER_REGISTRY_URL=`echo $DOCKER_PUSH_IMAGE|cut -f1 -d/` -echo "INFO: Pushing image $DOCKER_PUSH_IMAGE to $DOCKER_REGISTRY_URL" - -if [ ${WORKERS+x} ] # We are on a CI worker -then - retry $RETRIES docker push $DOCKER_PUSH_IMAGE || echo "Push failed after 5 \ - retries" -else # We are in a local (non-CI) environment - docker push $DOCKER_PUSH_IMAGE || echo "You may need to run 'docker login' \ - first and then re-run this script" -fi diff --git a/scripts/jenkins/run-benchmarks.sh b/scripts/jenkins/run-benchmarks.sh deleted file mode 100755 index 3a46d82586..0000000000 --- a/scripts/jenkins/run-benchmarks.sh +++ /dev/null @@ -1,95 +0,0 @@ -#!/usr/bin/env bash - -set -exuo pipefail - -NOW_ISO_8601=${NOW_ISO_8601:-$(date -u "+%Y-%m-%dT%H%M%SZ")} -MAVEN_CONFIG=${MAVEN_CONFIG:-"-Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=warn"} - -echo $(pwd) - -function setUp() { - echo "Setting CPU frequency to base frequency" - - CPU_MODEL=$(lscpu | grep "Model name" | awk '{for(i=3;i<=NF;i++){printf "%s ", $i}; printf "\n"}') - if [ "${CPU_MODEL}" == "Intel(R) Xeon(R) CPU E3-1246 v3 @ 3.50GHz " ] - then - # could also use `nproc` - CORE_INDEX=7 - BASE_FREQ="3.5GHz" - elif [ "${CPU_MODEL}" == "Intel(R) Core(TM) i7-6700 CPU @ 3.40GHz " ] - then - CORE_INDEX=7 - BASE_FREQ="3.4GHz" - elif [ "${CPU_MODEL}" == "Intel(R) Core(TM) i7-7700 CPU @ 3.60GHz " ] - then - CORE_INDEX=7 - BASE_FREQ="3.6GHz" - elif [ "${CPU_MODEL}" == "Intel(R) Core(TM) i7-8665U CPU @ 1.90GHz " ] - then - CORE_INDEX=7 - BASE_FREQ="1.9GHz" - else - >&2 echo "Cannot determine base frequency for CPU model [${CPU_MODEL}]. Please adjust the build script." - exit 1 - fi - MIN_FREQ=$(cpufreq-info -l -c 0 | awk '{print $1}') - # This is the frequency including Turbo Boost. See also http://ark.intel.com/products/80916/Intel-Xeon-Processor-E3-1246-v3-8M-Cache-3_50-GHz - MAX_FREQ=$(cpufreq-info -l -c 0 | awk '{print $2}') - - # set all CPUs to the base frequency - for (( cpu=0; cpu<=${CORE_INDEX}; cpu++ )) - do - sudo -n cpufreq-set -c ${cpu} --min ${BASE_FREQ} --max ${BASE_FREQ} - done - - # Build cgroups to isolate microbenchmarks and JVM threads - echo "Creating groups for OS and microbenchmarks" - # Isolate the OS to the first core - sudo -n cset set --set=/os --cpu=0-1 - sudo -n cset proc --move --fromset=/ --toset=/os - - # Isolate the microbenchmarks to all cores except the first two (first physical core) - # On a 4 core CPU with hyper threading, this would be 6 cores (3 physical cores) - sudo -n cset set --set=/benchmark --cpu=2-${CORE_INDEX} -} - -function benchmark() { - COMMIT_ISO_8601=$(git log -1 -s --format=%cI) - COMMIT_UNIX=$(git log -1 -s --format=%ct) - - [ -z "${NO_BUILD}" ] && ./mvnw clean package -DskipTests=true - - RESULT_FILE=apm-agent-benchmark-results-${COMMIT_ISO_8601}.json - BULK_UPLOAD_FILE=apm-agent-bulk-${NOW_ISO_8601}.json - - sudo -n cset proc --exec /benchmark -- \ - $JAVA_HOME/bin/java -jar apm-agent-benchmarks/target/benchmarks.jar ".*ContinuousBenchmark" \ - -prof gc \ - -prof co.elastic.apm.agent.benchmark.profiler.ReporterProfiler \ - -rf json \ - -rff ${RESULT_FILE} - - # remove strange non unicode chars inserted by JMH; see org.openjdk.jmh.results.Defaults.PREFIX - tr -cd '\11\12\40-\176' < ${RESULT_FILE} > "${RESULT_FILE}.clean" - rm -f ${RESULT_FILE} ${BULK_UPLOAD_FILE} - mv "${RESULT_FILE}.clean" ${RESULT_FILE} - - $JAVA_HOME/bin/java -cp apm-agent-benchmarks/target/benchmarks.jar co.elastic.apm.agent.benchmark.PostProcessBenchmarkResults ${RESULT_FILE} ${BULK_UPLOAD_FILE} ${COMMIT_UNIX} -} - -function tearDown() { - echo "Destroying cgroups" - sudo -n cset set --destroy /os - sudo -n cset set --destroy /benchmark - - echo "Setting normal frequency range" - for (( cpu=0; cpu<=${CORE_INDEX}; cpu++ )) - do - sudo -n cpufreq-set -c ${cpu} --min ${MIN_FREQ} --max ${MAX_FREQ} - done -} - -trap "tearDown" EXIT - -setUp -benchmark