diff --git a/agent-model/src/main/java/com/newrelic/agent/model/SpanEvent.java b/agent-model/src/main/java/com/newrelic/agent/model/SpanEvent.java index b7d40eaa48..03b2fd9a86 100644 --- a/agent-model/src/main/java/com/newrelic/agent/model/SpanEvent.java +++ b/agent-model/src/main/java/com/newrelic/agent/model/SpanEvent.java @@ -13,16 +13,14 @@ import java.io.IOException; import java.io.Writer; import java.util.Arrays; -import java.util.Collection; import java.util.HashMap; import java.util.Map; import java.util.Objects; -import java.util.Set; public class SpanEvent extends AnalyticsEvent implements JSONStreamAware { public static final String SPAN = "Span"; - static final String SPAN_KIND = "client"; + static final String CLIENT_SPAN_KIND = "client"; private final String appName; private final Map intrinsics; @@ -120,6 +118,7 @@ public static class Builder { private float priority; private boolean decider; private long timestamp; + private Object spanKind; public Builder appName(String appName) { this.appName = appName; @@ -177,9 +176,19 @@ public Builder putAgentAttribute(String key, Object value) { return this; } + public Builder spanKind(Object spanKind) { + putIntrinsic("span.kind", spanKind); + this.spanKind = spanKind; + return this; + } + + public boolean isClientSpan() { + return CLIENT_SPAN_KIND.equals(spanKind); + } + public Object getSpanKindFromUserAttributes() { Object result = userAttributes.get("span.kind"); - return result == null ? SPAN_KIND : result; + return result == null ? CLIENT_SPAN_KIND : result; } public Builder decider(boolean decider) { diff --git a/newrelic-agent/src/main/java/com/newrelic/agent/attributes/AttributeNames.java b/newrelic-agent/src/main/java/com/newrelic/agent/attributes/AttributeNames.java index 6aae781be2..2c4160b8fa 100644 --- a/newrelic-agent/src/main/java/com/newrelic/agent/attributes/AttributeNames.java +++ b/newrelic-agent/src/main/java/com/newrelic/agent/attributes/AttributeNames.java @@ -28,6 +28,7 @@ public final class AttributeNames { public static final String TIMEOUT_CAUSE = "nr.timeoutCause"; public static final String ERROR_EXPECTED = "error.expected"; + public static final String CODE_STACKTRACE = "code.stacktrace"; public static final String COMPONENT = "component"; public static final String HTTP_METHOD = "http.method"; public static final String HTTP_STATUS_CODE = "http.statusCode"; diff --git a/newrelic-agent/src/main/java/com/newrelic/agent/service/analytics/SpanEventFactory.java b/newrelic-agent/src/main/java/com/newrelic/agent/service/analytics/SpanEventFactory.java index d292e69849..7581304fd4 100644 --- a/newrelic-agent/src/main/java/com/newrelic/agent/service/analytics/SpanEventFactory.java +++ b/newrelic-agent/src/main/java/com/newrelic/agent/service/analytics/SpanEventFactory.java @@ -17,7 +17,9 @@ import com.newrelic.agent.model.SpanError; import com.newrelic.agent.model.SpanEvent; import com.newrelic.agent.service.ServiceFactory; +import com.newrelic.agent.tracers.DefaultTracer; import com.newrelic.agent.util.ExternalsUtil; +import com.newrelic.agent.util.StackTraces; import com.newrelic.api.agent.DatastoreParameters; import com.newrelic.api.agent.ExternalParameters; import com.newrelic.api.agent.HttpParameters; @@ -25,6 +27,7 @@ import java.net.URI; import java.text.MessageFormat; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.function.Supplier; @@ -40,6 +43,7 @@ public class SpanEventFactory { private static final Joiner TRACE_STATE_VENDOR_JOINER = Joiner.on(","); // Truncate `db.statement` at 2000 characters private static final int DB_STATEMENT_TRUNCATE_LENGTH = 2000; + private static final int MAX_EVENT_ATTRIBUTE_STRING_LENGTH = 4095; public static final Supplier DEFAULT_SYSTEM_TIMESTAMP_SUPPLIER = System::currentTimeMillis; @@ -115,13 +119,30 @@ public SpanEventFactory putAllAgentAttributes(Map agentAttributes) { return this; } + /** + * This should be called after the span kind is set. + */ + public SpanEventFactory setStackTraceAttributes(Map agentAttributes) { + if (builder.isClientSpan()) { + final List stackTraceList = (List) agentAttributes.get(DefaultTracer.BACKTRACE_PARAMETER_NAME); + if (stackTraceList != null) { + final List preStackTraces = StackTraces.scrubAndTruncate(stackTraceList); + final List postParentRemovalTrace = StackTraces.toStringList(preStackTraces); + + putAgentAttribute(AttributeNames.CODE_STACKTRACE, truncateWithEllipsis( + Joiner.on(',').join(postParentRemovalTrace), MAX_EVENT_ATTRIBUTE_STRING_LENGTH)); + } + } + return this; + } + public SpanEventFactory setClmAttributes(Map agentAttributes) { if (agentAttributes == null || agentAttributes.isEmpty()) { return this; } final Object threadId = agentAttributes.get(AttributeNames.THREAD_ID); if (threadId != null) { - builder.putAgentAttribute(AttributeNames.THREAD_ID, threadId); + builder.putIntrinsic(AttributeNames.THREAD_ID, threadId); } if (agentAttributes.containsKey(AttributeNames.CLM_NAMESPACE) && agentAttributes.containsKey(AttributeNames.CLM_FUNCTION)) { builder.putAgentAttribute(AttributeNames.CLM_NAMESPACE, agentAttributes.get(AttributeNames.CLM_NAMESPACE)); @@ -136,6 +157,12 @@ public SpanEventFactory putAllUserAttributes(Map userAttributes) { return this; } + + public SpanEventFactory putAllUserAttributesIfAbsent(Map userAttributes) { + builder.putAllUserAttributesIfAbsent(filter.filterUserAttributes(appName, userAttributes)); + return this; + } + public SpanEventFactory putAgentAttribute(String key, Object value) { builder.putAgentAttribute(key, value); return this; @@ -164,8 +191,7 @@ public SpanEventFactory setCategory(SpanCategory category) { } public SpanEventFactory setKindFromUserAttributes() { - Object spanKind = builder.getSpanKindFromUserAttributes(); - builder.putIntrinsic("span.kind", spanKind); + builder.spanKind(builder.getSpanKindFromUserAttributes()); return this; } diff --git a/newrelic-agent/src/main/java/com/newrelic/agent/service/analytics/TracerToSpanEvent.java b/newrelic-agent/src/main/java/com/newrelic/agent/service/analytics/TracerToSpanEvent.java index b2337f817f..e40c7d243b 100644 --- a/newrelic-agent/src/main/java/com/newrelic/agent/service/analytics/TracerToSpanEvent.java +++ b/newrelic-agent/src/main/java/com/newrelic/agent/service/analytics/TracerToSpanEvent.java @@ -94,6 +94,7 @@ public SpanEvent createSpanEvent(Tracer tracer, TransactionData transactionData, .setTimestamp(tracer.getStartTimeInMillis()) .setPriority(transactionData.getPriority()) .setExternalParameterAttributes(tracer.getExternalParameters()) + .setStackTraceAttributes(tracer.getAgentAttributes()) .setIsRootSpanEvent(isRoot) .setDecider(inboundPayload == null || inboundPayload.priority == null); diff --git a/newrelic-agent/src/main/resources/META-INF/excludes b/newrelic-agent/src/main/resources/META-INF/excludes index ba6c1b2df3..5ff4474e5e 100644 --- a/newrelic-agent/src/main/resources/META-INF/excludes +++ b/newrelic-agent/src/main/resources/META-INF/excludes @@ -88,4 +88,9 @@ # exclusions for Open Liberty 21+ so transactions start properly ^com/ibm/ws/security/jaspi/JaspiServletFilter # Websphere specific servlet wrapper class -^com/ibm/ws/webcontainer/servlet/ServletWrapperImpl \ No newline at end of file +^com/ibm/ws/webcontainer/servlet/ServletWrapperImpl +# Sonarqube9.9 ClassCircularityErrors +^java/util/AbstractList\$RandomAccessSpliterator +^java/util/stream/MatchOps\$MatchOp +^java/util/stream/MatchOps\$BooleanTerminalSink +^javax/security/auth/Subject\$SecureSet\$1 diff --git a/newrelic-agent/src/test/java/com/newrelic/agent/service/analytics/SpanEventFactoryTest.java b/newrelic-agent/src/test/java/com/newrelic/agent/service/analytics/SpanEventFactoryTest.java index d1b17269df..0ffaf80cf9 100644 --- a/newrelic-agent/src/test/java/com/newrelic/agent/service/analytics/SpanEventFactoryTest.java +++ b/newrelic-agent/src/test/java/com/newrelic/agent/service/analytics/SpanEventFactoryTest.java @@ -8,23 +8,29 @@ package com.newrelic.agent.service.analytics; import com.google.common.collect.ImmutableMap; +import com.newrelic.agent.MockConfigService; +import com.newrelic.agent.MockServiceManager; import com.newrelic.agent.attributes.AttributeNames; +import com.newrelic.agent.config.AgentConfig; import com.newrelic.agent.model.AttributeFilter; import com.newrelic.agent.model.SpanCategory; import com.newrelic.agent.model.SpanError; import com.newrelic.agent.model.SpanEvent; +import com.newrelic.agent.service.ServiceFactory; import com.newrelic.agent.tracers.DefaultTracer; import com.newrelic.api.agent.DatastoreParameters; import com.newrelic.api.agent.HttpParameters; import org.junit.Test; import java.net.URI; +import java.util.Arrays; import java.util.Collections; import java.util.Map; import static com.newrelic.agent.service.analytics.SpanEventFactory.DEFAULT_SYSTEM_TIMESTAMP_SUPPLIER; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -196,6 +202,21 @@ public void shouldSetDataStoreParameters() { assertEquals("dbserver:3306", target.getAgentAttributes().get("peer.address")); } + @Test + public void shouldStoreStackTrace() { + SpanEventFactory spanEventFactory = new SpanEventFactory("MyApp", new AttributeFilter.PassEverythingAttributeFilter(), + DEFAULT_SYSTEM_TIMESTAMP_SUPPLIER); + spanEventFactory.setKindFromUserAttributes(); + MockServiceManager serviceManager = new MockServiceManager(); + serviceManager.setConfigService(new MockConfigService(mock(AgentConfig.class))); + ServiceFactory.setServiceManager(serviceManager); + spanEventFactory.setStackTraceAttributes( + ImmutableMap.of(DefaultTracer.BACKTRACE_PARAMETER_NAME, Arrays.asList(Thread.currentThread().getStackTrace()))); + + final Object stackTrace = spanEventFactory.build().getAgentAttributes().get(AttributeNames.CODE_STACKTRACE); + assertNotNull(stackTrace); + } + @Test public void shouldSetCLMParameters() { Map agentAttributes = ImmutableMap.of( @@ -208,7 +229,7 @@ public void shouldSetCLMParameters() { assertEquals("nr", target.getAgentAttributes().get(AttributeNames.CLM_NAMESPACE)); assertEquals("process", target.getAgentAttributes().get(AttributeNames.CLM_FUNCTION)); - assertEquals(666, target.getAgentAttributes().get(AttributeNames.THREAD_ID)); + assertEquals(666, target.getIntrinsics().get(AttributeNames.THREAD_ID)); } @Test