diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index 29c978b05b..7668c2836d 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -58,6 +58,7 @@ Elasticsearch's REST client - {pull}1883[#1883] service map and downstream service in the dependencies table - {pull}1898[#1898] * Basic support for com.sun.net.httpserver.HttpServer - {pull}1854[#1854] * Update to async-profiler 1.8.6 {pull}1907[#1907] +* Added support for setting the framework using the public api (#1908) - {pull}1909[#1909] [float] ===== Bug fixes diff --git a/apm-agent-api/src/main/java/co/elastic/apm/api/NoopTransaction.java b/apm-agent-api/src/main/java/co/elastic/apm/api/NoopTransaction.java index d7745ec83c..fe66caf0da 100644 --- a/apm-agent-api/src/main/java/co/elastic/apm/api/NoopTransaction.java +++ b/apm-agent-api/src/main/java/co/elastic/apm/api/NoopTransaction.java @@ -32,6 +32,8 @@ public Transaction setName(String name) { return this; } + + @Nonnull @Override public Transaction setType(String type) { @@ -39,6 +41,13 @@ public Transaction setType(String type) { return this; } + @Nonnull + @Override + public Transaction setFrameworkName(String frameworkName) { + // noop + return this; + } + @Nonnull @Deprecated @Override diff --git a/apm-agent-api/src/main/java/co/elastic/apm/api/Transaction.java b/apm-agent-api/src/main/java/co/elastic/apm/api/Transaction.java index eec5379770..a4a4f1d4ee 100644 --- a/apm-agent-api/src/main/java/co/elastic/apm/api/Transaction.java +++ b/apm-agent-api/src/main/java/co/elastic/apm/api/Transaction.java @@ -57,6 +57,16 @@ public interface Transaction extends Span { @Nonnull Transaction setType(String type); + /** + * Provides a way to manually set the {@code service.framework.name} field. + * Any value set through this method will take precedence over automatically-set value for supported frameworks. + + * @param frameworkName The name of the framework. {@code null} and empty values will cause the exclusion of the + * framework name from the transaction context. + */ + @Nonnull + Transaction setFrameworkName(String frameworkName); + /** * {@inheritDoc} * diff --git a/apm-agent-api/src/main/java/co/elastic/apm/api/TransactionImpl.java b/apm-agent-api/src/main/java/co/elastic/apm/api/TransactionImpl.java index d6edd6a5f6..da0ce5bfe5 100644 --- a/apm-agent-api/src/main/java/co/elastic/apm/api/TransactionImpl.java +++ b/apm-agent-api/src/main/java/co/elastic/apm/api/TransactionImpl.java @@ -42,6 +42,13 @@ public Transaction setName(String name) { return this; } + @Nonnull + @Override + public Transaction setFrameworkName(String frameworkName) { + // co.elastic.apm.agent.pluginapi.TransactionInstrumentation$SetFrameworkNameInstrumentation + return this; + } + @Nonnull @Override public Transaction setType(String type) { diff --git a/apm-agent-core/src/main/java/co/elastic/apm/agent/impl/transaction/Transaction.java b/apm-agent-core/src/main/java/co/elastic/apm/agent/impl/transaction/Transaction.java index 236f5958ec..3c81358032 100644 --- a/apm-agent-core/src/main/java/co/elastic/apm/agent/impl/transaction/Transaction.java +++ b/apm-agent-core/src/main/java/co/elastic/apm/agent/impl/transaction/Transaction.java @@ -31,10 +31,7 @@ import org.HdrHistogram.WriterReaderPhaser; import javax.annotation.Nullable; -import java.util.Arrays; -import java.util.HashSet; import java.util.List; -import java.util.Set; /** * Data captured by an agent representing an event occurring in a monitored service @@ -93,6 +90,8 @@ protected Labels.Mutable initialValue() { @Nullable private String frameworkName; + private boolean frameworkNameSetByUser; + @Nullable private String frameworkVersion; @@ -319,9 +318,21 @@ protected void recycle() { } public void setFrameworkName(@Nullable String frameworkName) { + if (frameworkNameSetByUser) { + return; + } this.frameworkName = frameworkName; } + public void setUserFrameworkName(@Nullable String frameworkName) { + if (frameworkName != null && frameworkName.isEmpty()) { + this.frameworkName = null; + } else { + this.frameworkName = frameworkName; + } + this.frameworkNameSetByUser = true; + } + @Nullable public String getFrameworkName() { return this.frameworkName; diff --git a/apm-agent-core/src/test/java/co/elastic/apm/agent/report/serialize/DslJsonSerializerTest.java b/apm-agent-core/src/test/java/co/elastic/apm/agent/report/serialize/DslJsonSerializerTest.java index 4939e40797..d3b6180879 100644 --- a/apm-agent-core/src/test/java/co/elastic/apm/agent/report/serialize/DslJsonSerializerTest.java +++ b/apm-agent-core/src/test/java/co/elastic/apm/agent/report/serialize/DslJsonSerializerTest.java @@ -395,6 +395,24 @@ void testSpanDestinationContextSerialization() { assertThat(service.get("type").textValue()).isEqualTo(""); } + @Test + void testTransactionNullFrameworkNameSerialization() { + Transaction transaction = new Transaction(MockTracer.create()); + transaction.getTraceContext().setServiceName("service-name"); + transaction.setUserFrameworkName(null); + JsonNode transactionJson = readJsonString(serializer.toJsonString(transaction)); + assertThat(transactionJson.get("context").get("service").get("framework")).isNull(); + } + + @Test + void testTransactionEmptyFrameworkNameSerialization() { + Transaction transaction = new Transaction(MockTracer.create()); + transaction.getTraceContext().setServiceName("service-name"); + transaction.setUserFrameworkName(""); + JsonNode transactionJson = readJsonString(serializer.toJsonString(transaction)); + assertThat(transactionJson.get("context").get("service").get("framework")).isNull(); + } + @Test void testSpanInvalidDestinationSerialization() { Span span = new Span(MockTracer.create()); diff --git a/apm-agent-plugins/apm-api-plugin/src/main/java/co/elastic/apm/agent/pluginapi/TransactionInstrumentation.java b/apm-agent-plugins/apm-api-plugin/src/main/java/co/elastic/apm/agent/pluginapi/TransactionInstrumentation.java index c63059dee2..d1aea56b78 100644 --- a/apm-agent-plugins/apm-api-plugin/src/main/java/co/elastic/apm/agent/pluginapi/TransactionInstrumentation.java +++ b/apm-agent-plugins/apm-api-plugin/src/main/java/co/elastic/apm/agent/pluginapi/TransactionInstrumentation.java @@ -53,6 +53,20 @@ public ElementMatcher getMethodMatcher() { return methodMatcher; } + public static class SetFrameworkNameInstrumentation extends TransactionInstrumentation { + public SetFrameworkNameInstrumentation() { + super(named("setFrameworkName")); + } + + @Advice.OnMethodEnter(suppress = Throwable.class, inline = false) + public static void setFrameworkName(@Advice.FieldValue(value = "span", typing = Assigner.Typing.DYNAMIC) Object transaction, + @Advice.Argument(0) String frameworkName) { + if (transaction instanceof Transaction) { + ((Transaction) transaction).setUserFrameworkName(frameworkName); + } + } + } + public static class SetUserInstrumentation extends TransactionInstrumentation { public SetUserInstrumentation() { super(named("setUser")); diff --git a/apm-agent-plugins/apm-api-plugin/src/main/resources/META-INF/services/co.elastic.apm.agent.sdk.ElasticApmInstrumentation b/apm-agent-plugins/apm-api-plugin/src/main/resources/META-INF/services/co.elastic.apm.agent.sdk.ElasticApmInstrumentation index f0503805d6..161e9bceca 100644 --- a/apm-agent-plugins/apm-api-plugin/src/main/resources/META-INF/services/co.elastic.apm.agent.sdk.ElasticApmInstrumentation +++ b/apm-agent-plugins/apm-api-plugin/src/main/resources/META-INF/services/co.elastic.apm.agent.sdk.ElasticApmInstrumentation @@ -3,6 +3,7 @@ co.elastic.apm.agent.pluginapi.ElasticApmApiInstrumentation$StartTransactionWith co.elastic.apm.agent.pluginapi.ElasticApmApiInstrumentation$CurrentTransactionInstrumentation co.elastic.apm.agent.pluginapi.ElasticApmApiInstrumentation$CurrentSpanInstrumentation co.elastic.apm.agent.pluginapi.ElasticApmApiInstrumentation$CaptureExceptionInstrumentation +co.elastic.apm.agent.pluginapi.TransactionInstrumentation$SetFrameworkNameInstrumentation co.elastic.apm.agent.pluginapi.TransactionInstrumentation$SetUserInstrumentation co.elastic.apm.agent.pluginapi.TransactionInstrumentation$EnsureParentIdInstrumentation co.elastic.apm.agent.pluginapi.TransactionInstrumentation$SetResultInstrumentation diff --git a/apm-agent-plugins/apm-api-plugin/src/test/java/co/elastic/apm/agent/pluginapi/TransactionInstrumentationTest.java b/apm-agent-plugins/apm-api-plugin/src/test/java/co/elastic/apm/agent/pluginapi/TransactionInstrumentationTest.java index 6deef7c721..92e584d083 100644 --- a/apm-agent-plugins/apm-api-plugin/src/test/java/co/elastic/apm/agent/pluginapi/TransactionInstrumentationTest.java +++ b/apm-agent-plugins/apm-api-plugin/src/test/java/co/elastic/apm/agent/pluginapi/TransactionInstrumentationTest.java @@ -20,6 +20,7 @@ import co.elastic.apm.AbstractApiTest; import co.elastic.apm.agent.impl.TracerInternalApiUtils; +import co.elastic.apm.api.AbstractSpanImplAccessor; import co.elastic.apm.api.ElasticApm; import co.elastic.apm.api.Outcome; import co.elastic.apm.api.Span; @@ -27,7 +28,10 @@ import org.apache.commons.lang3.RandomStringUtils; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import javax.annotation.Nullable; import java.security.SecureRandom; import static org.assertj.core.api.Assertions.assertThat; @@ -57,6 +61,44 @@ void testFrameworkName() { assertThat(reporter.getFirstTransaction().getFrameworkName()).isEqualTo("API"); } + @Test + void testSetUserFrameworkValidNameBeforeSetByInternalAPI() { + transaction.setFrameworkName("foo"); + AbstractSpanImplAccessor.accessTransaction(transaction).setFrameworkName("bar"); + endTransaction(); + assertThat(reporter.getFirstTransaction().getFrameworkName()).isEqualTo("foo"); + } + + @Test + void testSetUserFrameworkValidNameAfterSetByInternalAPI() { + AbstractSpanImplAccessor.accessTransaction(transaction).setFrameworkName("bar"); + transaction.setFrameworkName("foo"); + endTransaction(); + assertThat(reporter.getFirstTransaction().getFrameworkName()).isEqualTo("foo"); + } + + static String[] invalidFrameworkNames() { + return new String[]{null, ""}; + } + + @ParameterizedTest + @MethodSource("invalidFrameworkNames") + void testSetUserFrameworkInvalidNameBeforeSetByInternalAPI(@Nullable String frameworkName) { + transaction.setFrameworkName(frameworkName); + AbstractSpanImplAccessor.accessTransaction(transaction).setFrameworkName("bar"); + endTransaction(); + assertThat(reporter.getFirstTransaction().getFrameworkName()).isNull(); + } + + @ParameterizedTest + @MethodSource("invalidFrameworkNames") + void testSetUserFrameworkInvalidNameAfterSetByInternalAPI(@Nullable String frameworkName) { + AbstractSpanImplAccessor.accessTransaction(transaction).setFrameworkName("bar"); + transaction.setFrameworkName(frameworkName); + endTransaction(); + assertThat(reporter.getFirstTransaction().getFrameworkName()).isNull(); + } + @Test void testSetType() { transaction.setType("foo"); @@ -129,7 +171,6 @@ private void checkResult(String expected) { } - @Test void testChaining() { int randomInt = random.nextInt(); diff --git a/apm-agent-plugins/apm-api-plugin/src/test/java/co/elastic/apm/api/AbstractSpanImplAccessor.java b/apm-agent-plugins/apm-api-plugin/src/test/java/co/elastic/apm/api/AbstractSpanImplAccessor.java new file mode 100644 index 0000000000..ada066ad77 --- /dev/null +++ b/apm-agent-plugins/apm-api-plugin/src/test/java/co/elastic/apm/api/AbstractSpanImplAccessor.java @@ -0,0 +1,26 @@ +/* + * 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.api; + +public class AbstractSpanImplAccessor { + + public static co.elastic.apm.agent.impl.transaction.Transaction accessTransaction(Transaction t) { + return (co.elastic.apm.agent.impl.transaction.Transaction) ((TransactionImpl) t).span; + } +} diff --git a/docs/public-api.asciidoc b/docs/public-api.asciidoc index 7a275eff84..2861f1c11e 100644 --- a/docs/public-api.asciidoc +++ b/docs/public-api.asciidoc @@ -311,6 +311,24 @@ transaction.setType(Transaction.TYPE_REQUEST); * `type`: The type of the transaction +[float] +[[api-transaction-set-framework-name]] +==== `Transaction setFrameworkName(String frameworkName)` added[1.25.0] +Provides a way to manually set the `service.framework.name` field. +For supported frameworks, +the framework name is determined automatically, +and can be overridden using this function. +`null` or the empty string will make the agent omit this field. + +Example: + +[source,java] +---- +transaction.setFrameworkName("My Framework"); +---- + +* `frameworkName`: The name of the framework + [float] [[api-transaction-add-tag]] ==== `Transaction setLabel(String key, value)` added[1.5.0 as `addLabel`,Number and boolean labels require APM Server 6.7]