diff --git a/apm-agent-api/src/main/java/co/elastic/apm/api/NoopSpan.java b/apm-agent-api/src/main/java/co/elastic/apm/api/NoopSpan.java index bfd6c9aaa4..137d21c6c5 100644 --- a/apm-agent-api/src/main/java/co/elastic/apm/api/NoopSpan.java +++ b/apm-agent-api/src/main/java/co/elastic/apm/api/NoopSpan.java @@ -55,6 +55,12 @@ public String getId() { return ""; } + @Nonnull + @Override + public String getTraceId() { + return ""; + } + @Override public Scope activate() { return NoopScope.INSTANCE; 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 11e9e4fa6b..a00eaf9765 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 @@ -61,6 +61,18 @@ public String getId() { return ""; } + @Nonnull + @Override + public String ensureParentId() { + return ""; + } + + @Nonnull + @Override + public String getTraceId() { + return ""; + } + @Override public Scope activate() { return NoopScope.INSTANCE; diff --git a/apm-agent-api/src/main/java/co/elastic/apm/api/Span.java b/apm-agent-api/src/main/java/co/elastic/apm/api/Span.java index fd7ffb916c..6a7310507d 100644 --- a/apm-agent-api/src/main/java/co/elastic/apm/api/Span.java +++ b/apm-agent-api/src/main/java/co/elastic/apm/api/Span.java @@ -132,6 +132,22 @@ public interface Span { @Nonnull String getId(); + /** + * Returns the id of this trace (never {@code null}) + *
+ * The trace-ID is consistent across all transactions and spans which belong to the same logical trace, + * even for spans which happened in another service (given this service is also monitored by Elastic APM). + *
+ *+ * If this span represents a noop, + * this method returns an empty string. + *
+ * + * @return the id of this span (never {@code null}) + */ + @Nonnull + String getTraceId(); + /** * Makes this span the active span on the current thread until {@link Scope#close()} has been called. *diff --git a/apm-agent-api/src/main/java/co/elastic/apm/api/SpanImpl.java b/apm-agent-api/src/main/java/co/elastic/apm/api/SpanImpl.java index 7368042b78..b4061a3ded 100644 --- a/apm-agent-api/src/main/java/co/elastic/apm/api/SpanImpl.java +++ b/apm-agent-api/src/main/java/co/elastic/apm/api/SpanImpl.java @@ -82,6 +82,13 @@ public String getId() { return ""; } + @Nonnull + @Override + public String getTraceId() { + // co.elastic.apm.plugin.api.SpanInstrumentation.GetTraceIdInstrumentation + return ""; + } + @Override public Scope activate() { // co.elastic.apm.plugin.api.SpanInstrumentation.ActivateInstrumentation 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 cf6fe8248a..95b8110bb2 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 @@ -117,6 +117,50 @@ public interface Transaction extends Span { @Nonnull String getId(); + /** + *
+ * If the transaction does not have a parent-ID yet, + * calling this method generates a new ID, + * sets it as the parent-ID of this transaction, + * and returns it as a `String`. + *
+ *+ * This enables the correlation of the spans the JavaScript Real User Monitoring (RUM) agent creates for the initial page load + * with the transaction of the backend service. + * If your backend service generates the HTML page dynamically, + * initializing the JavaScript RUM agent with the value of this method allows analyzing the time spent in the browser vs in the backend services. + *
+ *+ * To enable the JavaScript RUM agent when using an HTML templating language like Freemarker, + * add {@code ElasticApm.currentTransaction()} with the key {@code "transaction"} to the model. + *
+ *+ * Also, add a snippet similar to this to the body of your HTML pages, + * preferably before other JS libraries: + *
+ * + *{@code
+ *
+ *
+ * }
+ *
+ * + * See the JavaScript RUM agent documentation for more information. + *
+ * + * @return the parent-ID for this transaction. Updates the transaction to use a new parent-ID if it has previously been unset. + */ + @Nonnull + String ensureParentId(); + /** * Makes this transaction the active transaction on the current thread until {@link Scope#close()} has been called. *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 a3f4d70280..cea6c2569e 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 @@ -40,4 +40,11 @@ public void setUser(String id, String email, String username) { // co.elastic.apm.plugin.api.TransactionInstrumentation$SetUserInstrumentation.setUser } + @Nonnull + @Override + public String ensureParentId() { + // co.elastic.apm.plugin.api.TransactionInstrumentation.EnsureParentIdInstrumentation + return ""; + } + } diff --git a/apm-agent-plugins/apm-api-plugin/src/main/java/co/elastic/apm/plugin/api/SpanInstrumentation.java b/apm-agent-plugins/apm-api-plugin/src/main/java/co/elastic/apm/plugin/api/SpanInstrumentation.java index 1ce0884d2f..178e901d1b 100644 --- a/apm-agent-plugins/apm-api-plugin/src/main/java/co/elastic/apm/plugin/api/SpanInstrumentation.java +++ b/apm-agent-plugins/apm-api-plugin/src/main/java/co/elastic/apm/plugin/api/SpanInstrumentation.java @@ -146,6 +146,21 @@ public static void getId(@Advice.FieldValue(value = "span", typing = Assigner.Ty } } + public static class GetTraceIdInstrumentation extends SpanInstrumentation { + public GetTraceIdInstrumentation() { + super(named("getTraceId").and(takesArguments(0))); + } + + @VisibleForAdvice + @Advice.OnMethodExit + public static void getTraceId(@Advice.FieldValue(value = "span", typing = Assigner.Typing.DYNAMIC) AbstractSpan> span, + @Advice.Return(readOnly = false) String traceId) { + if (tracer != null) { + traceId = span.getTraceContext().getTraceId().toString(); + } + } + } + public static class AddTagInstrumentation extends SpanInstrumentation { public AddTagInstrumentation() { super(named("addTag")); diff --git a/apm-agent-plugins/apm-api-plugin/src/main/java/co/elastic/apm/plugin/api/TransactionInstrumentation.java b/apm-agent-plugins/apm-api-plugin/src/main/java/co/elastic/apm/plugin/api/TransactionInstrumentation.java index 878629c9a4..dfe0bb822c 100644 --- a/apm-agent-plugins/apm-api-plugin/src/main/java/co/elastic/apm/plugin/api/TransactionInstrumentation.java +++ b/apm-agent-plugins/apm-api-plugin/src/main/java/co/elastic/apm/plugin/api/TransactionInstrumentation.java @@ -21,6 +21,7 @@ import co.elastic.apm.bci.ElasticApmInstrumentation; import co.elastic.apm.bci.VisibleForAdvice; +import co.elastic.apm.impl.transaction.TraceContext; import co.elastic.apm.impl.transaction.Transaction; import net.bytebuddy.asm.Advice; import net.bytebuddy.description.method.MethodDescription; @@ -77,4 +78,23 @@ public static void setUser(@Advice.FieldValue(value = "span", typing = Assigner. transaction.setUser(id, email, username); } } + + public static class EnsureParentIdInstrumentation extends TransactionInstrumentation { + public EnsureParentIdInstrumentation() { + super(named("ensureParentId")); + } + + @VisibleForAdvice + @Advice.OnMethodExit + public static void ensureParentId(@Advice.FieldValue(value = "span", typing = Assigner.Typing.DYNAMIC) Transaction transaction, + @Advice.Return(readOnly = false) String spanId) { + if (tracer != null) { + final TraceContext traceContext = transaction.getTraceContext(); + if (traceContext.getParentId().isEmpty()) { + traceContext.getParentId().setToRandomValue(); + } + spanId = traceContext.getParentId().toString(); + } + } + } } diff --git a/apm-agent-plugins/apm-api-plugin/src/main/resources/META-INF/services/co.elastic.apm.bci.ElasticApmInstrumentation b/apm-agent-plugins/apm-api-plugin/src/main/resources/META-INF/services/co.elastic.apm.bci.ElasticApmInstrumentation index 1762fd996c..3dd2fec598 100644 --- a/apm-agent-plugins/apm-api-plugin/src/main/resources/META-INF/services/co.elastic.apm.bci.ElasticApmInstrumentation +++ b/apm-agent-plugins/apm-api-plugin/src/main/resources/META-INF/services/co.elastic.apm.bci.ElasticApmInstrumentation @@ -3,12 +3,14 @@ co.elastic.apm.plugin.api.ElasticApmApiInstrumentation$CurrentTransactionInstrum co.elastic.apm.plugin.api.ElasticApmApiInstrumentation$CurrentSpanInstrumentation co.elastic.apm.plugin.api.ElasticApmApiInstrumentation$CaptureExceptionInstrumentation co.elastic.apm.plugin.api.TransactionInstrumentation$SetUserInstrumentation +co.elastic.apm.plugin.api.TransactionInstrumentation$EnsureParentIdInstrumentation co.elastic.apm.plugin.api.SpanInstrumentation$SetNameInstrumentation co.elastic.apm.plugin.api.SpanInstrumentation$SetTypeInstrumentation co.elastic.apm.plugin.api.SpanInstrumentation$DoCreateSpanInstrumentation co.elastic.apm.plugin.api.SpanInstrumentation$EndInstrumentation co.elastic.apm.plugin.api.SpanInstrumentation$CaptureExceptionInstrumentation co.elastic.apm.plugin.api.SpanInstrumentation$GetIdInstrumentation +co.elastic.apm.plugin.api.SpanInstrumentation$GetTraceIdInstrumentation co.elastic.apm.plugin.api.SpanInstrumentation$AddTagInstrumentation co.elastic.apm.plugin.api.SpanInstrumentation$ActivateInstrumentation co.elastic.apm.plugin.api.CaptureExceptionInstrumentation diff --git a/apm-agent-plugins/apm-api-plugin/src/test/java/co/elastic/apm/api/ElasticApmApiInstrumentationTest.java b/apm-agent-plugins/apm-api-plugin/src/test/java/co/elastic/apm/api/ElasticApmApiInstrumentationTest.java index 482e52f457..a5675cdf57 100644 --- a/apm-agent-plugins/apm-api-plugin/src/test/java/co/elastic/apm/api/ElasticApmApiInstrumentationTest.java +++ b/apm-agent-plugins/apm-api-plugin/src/test/java/co/elastic/apm/api/ElasticApmApiInstrumentationTest.java @@ -108,10 +108,13 @@ void testGetId_distributedTracingEnabled() { co.elastic.apm.impl.transaction.Transaction transaction = tracer.startTransaction().withType(Transaction.TYPE_REQUEST); try (Scope scope = transaction.activateInScope()) { assertThat(ElasticApm.currentTransaction().getId()).isEqualTo(transaction.getTraceContext().getId().toString()); + assertThat(ElasticApm.currentTransaction().getTraceId()).isEqualTo(transaction.getTraceContext().getTraceId().toString()); assertThat(ElasticApm.currentSpan().getId()).isEqualTo(transaction.getTraceContext().getId().toString()); + assertThat(ElasticApm.currentSpan().getTraceId()).isEqualTo(transaction.getTraceContext().getTraceId().toString()); co.elastic.apm.impl.transaction.Span span = transaction.createSpan().withType("db").withName("SELECT"); try (Scope spanScope = span.activateInScope()) { assertThat(ElasticApm.currentSpan().getId()).isEqualTo(span.getTraceContext().getId().toString()); + assertThat(ElasticApm.currentSpan().getTraceId()).isEqualTo(span.getTraceContext().getTraceId().toString()); } finally { span.end(); } @@ -162,4 +165,16 @@ void testScopes() { assertThat(ElasticApm.currentTransaction()).isSameAs(NoopTransaction.INSTANCE); } + + @Test + void testEnsureParentId() { + final Transaction transaction = ElasticApm.startTransaction(); + try (co.elastic.apm.api.Scope scope = transaction.activate()) { + assertThat(tracer.currentTransaction()).isNotNull(); + assertThat(tracer.currentTransaction().getTraceContext().getParentId().isEmpty()).isTrue(); + String rumTransactionId = transaction.ensureParentId(); + assertThat(tracer.currentTransaction().getTraceContext().getParentId().toString()).isEqualTo(rumTransactionId); + assertThat(transaction.ensureParentId()).isEqualTo(rumTransactionId); + } + } } diff --git a/docs/public-api.asciidoc b/docs/public-api.asciidoc index 3e33c711fc..1efca6ade6 100644 --- a/docs/public-api.asciidoc +++ b/docs/public-api.asciidoc @@ -224,6 +224,52 @@ Returns the id of this transaction (never `null`) If this transaction represents a noop, this method returns an empty string. +[float] +[[api-transaction-get-trace-id]] +==== `String getTraceId()` +Returns the trace-id of this transaction. + +The trace-id is consistent across all transactions and spans which belong to the same logical trace, +even for transactions and spans which happened in another service (given this service is also monitored by Elastic APM). + +If this span represents a noop, +this method returns an empty string. + +[float] +[[api-ensure-parent-id]] +==== `String ensureParentId()` +If the transaction does not have a parent-ID yet, +calling this method generates a new ID, +sets it as the parent-ID of this transaction, +and returns it as a `String`. + +This enables the correlation of the spans the JavaScript Real User Monitoring (RUM) agent creates for the initial page load +with the transaction of the backend service. +If your backend service generates the HTML page dynamically, +initializing the JavaScript RUM agent with the value of this method allows analyzing the time spent in the browser vs in the backend services. + +To enable the JavaScript RUM agent when using an HTML templating language like Freemarker, +add `ElasticApm.currentTransaction()` with the key `"transaction"` to the model. + +Also, add a snippet similar to this to the body of your HTML page, +preferably before other JS libraries: + +[source,html] +---- + + +---- + +See the {apm-rum-ref}[JavaScript RUM agent documentation] for more information. + [float] [[api-transaction-start-span]] ==== `Span createSpan()` @@ -364,6 +410,17 @@ Returns the id of this span (never `null`) If this span represents a noop, this method returns an empty string. +[float] +[[api-span-get-trace-id]] +==== `String getTraceId()` +Returns the trace-ID of this span. + +The trace-ID is consistent across all transactions and spans which belong to the same logical trace, +even for transactions and spans which happened in another service (given this service is also monitored by Elastic APM). + +If this span represents a noop, +this method returns an empty string. + [float] [[api-span-end]] ==== `void end()`