Skip to content

Commit 770334b

Browse files
authored
Added internal option to fully redact exceptions (#3528)
1 parent 274de8b commit 770334b

File tree

18 files changed

+165
-11
lines changed

18 files changed

+165
-11
lines changed

CHANGELOG.asciidoc

+4-3
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ Use subheadings with the "=====" level for adding notes for unreleased changes:
3131
3232
=== Unreleased
3333
34+
[float]
35+
===== Features
36+
* Added internal `safe_exceptions` config option to workaround JVM bugs related to touching exceptions - {pull}3528[#3528]
37+
3438
[float]
3539
===== Bug fixes
3640
* Cleanup extra servlet request attribute used for Spring exception handler - {pull}3527[#3527]
@@ -44,9 +48,6 @@ Use subheadings with the "=====" level for adding notes for unreleased changes:
4448
[float]
4549
===== Features
4650
* Added a configuration option to use queues in names of spring-rabbit transactions - {pull}3424[#3424]
47-
48-
[float]
49-
===== Features
5051
* Add option to retry JMX metrics capture in case of exception - {pull}3511[#3511]
5152
5253
[float]

apm-agent-core/src/main/java/co/elastic/apm/agent/configuration/CoreConfiguration.java

+17
Original file line numberDiff line numberDiff line change
@@ -517,6 +517,14 @@ public String toString(Collection<String> value) {
517517
.tags("added[1.44.0]")
518518
.buildWithDefault(false);
519519

520+
521+
private final ConfigurationOption<Integer> safeExceptions = ConfigurationOption.<Boolean>integerOption()
522+
.key("safe_exceptions")
523+
.tags("internal")
524+
.configurationCategory(CORE_CATEGORY)
525+
.dynamic(true)
526+
.buildWithDefault(0);
527+
520528
private final ConfigurationOption<List<WildcardMatcher>> classesExcludedFromInstrumentation = ConfigurationOption
521529
.builder(new ValueConverter<List<WildcardMatcher>>() {
522530

@@ -1163,6 +1171,15 @@ public boolean isContextPropagationOnly() {
11631171
return contextPropagationOnly.get();
11641172
}
11651173

1174+
public boolean isRedactExceptions() {
1175+
return (safeExceptions.get() & 1) != 0;
1176+
}
1177+
1178+
@Override
1179+
public boolean isUseServletAttributesForExceptionPropagation() {
1180+
return (safeExceptions.get() & 2) == 0;
1181+
}
1182+
11661183
public enum CloudProvider {
11671184
AUTO,
11681185
AWS,

apm-agent-core/src/main/java/co/elastic/apm/agent/impl/ElasticApmTracer.java

+12
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import co.elastic.apm.agent.configuration.CoreConfiguration;
2727
import co.elastic.apm.agent.configuration.MetricsConfiguration;
2828
import co.elastic.apm.agent.configuration.ServerlessConfiguration;
29+
import co.elastic.apm.agent.impl.error.RedactedException;
2930
import co.elastic.apm.agent.impl.metadata.ServiceFactory;
3031
import co.elastic.apm.agent.tracer.service.Service;
3132
import co.elastic.apm.agent.tracer.service.ServiceInfo;
@@ -446,6 +447,8 @@ private ErrorCapture captureException(long epochMicros, @Nullable Throwable e, E
446447
return null;
447448
}
448449

450+
e = redactExceptionIfRequired(e);
451+
449452
while (e != null && WildcardMatcher.anyMatch(coreConfiguration.getUnnestExceptions(), e.getClass().getName()) != null) {
450453
e = e.getCause();
451454
}
@@ -995,4 +998,13 @@ public Service createService(String ephemeralId) {
995998
configurationRegistry.getConfig(ServerlessConfiguration.class).runsOnAwsLambda()
996999
);
9971000
}
1001+
1002+
@Override
1003+
@Nullable
1004+
public Throwable redactExceptionIfRequired(@Nullable Throwable original) {
1005+
if (original != null && coreConfiguration.isRedactExceptions() && !(original instanceof RedactedException)) {
1006+
return new RedactedException();
1007+
}
1008+
return original;
1009+
}
9981010
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/*
2+
* Licensed to Elasticsearch B.V. under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch B.V. licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package co.elastic.apm.agent.impl.error;
20+
21+
public class RedactedException extends Exception {
22+
23+
public RedactedException() {
24+
super("This exception is a placeholder for an exception which has occurred in the application." +
25+
"The stacktrace corresponds to where elastic APM detected the exception.");
26+
}
27+
28+
}

apm-agent-core/src/main/java/co/elastic/apm/agent/impl/transaction/Transaction.java

+17
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,9 @@ public class Transaction extends AbstractSpan<Transaction> implements co.elastic
104104
@Nullable
105105
private String frameworkVersion;
106106

107+
@Nullable
108+
private Throwable pendingException;
109+
107110
/**
108111
* Faas
109112
* <p>
@@ -337,6 +340,7 @@ public void resetState() {
337340
frameworkVersion = null;
338341
faas.resetState();
339342
wasActivated.set(false);
343+
pendingException = null;
340344
// don't clear timerBySpanTypeAndSubtype map (see field-level javadoc)
341345
}
342346

@@ -535,4 +539,17 @@ private void trackMetrics() {
535539
phaser.readerUnlock();
536540
}
537541
}
542+
543+
544+
@Override
545+
public void setPendingTransactionException(@Nullable Throwable exception) {
546+
this.pendingException = exception;
547+
}
548+
549+
@Nullable
550+
@Override
551+
public Throwable getPendingTransactionException() {
552+
return this.pendingException;
553+
}
554+
538555
}

apm-agent-core/src/test/java/org/example/stacktrace/ErrorCaptureTest.java

+22
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import co.elastic.apm.agent.impl.ElasticApmTracer;
2525
import co.elastic.apm.agent.impl.context.Request;
2626
import co.elastic.apm.agent.impl.error.ErrorCapture;
27+
import co.elastic.apm.agent.impl.error.RedactedException;
2728
import co.elastic.apm.agent.impl.stacktrace.StacktraceConfiguration;
2829
import co.elastic.apm.agent.impl.transaction.Transaction;
2930
import org.junit.jupiter.api.BeforeEach;
@@ -107,6 +108,27 @@ void testNonConfiguredWrappingConfigured() {
107108
assertThat(errorCapture.getException()).isInstanceOf(WrapperException.class);
108109
}
109110

111+
@Test
112+
void testExceptionRedaction() {
113+
doReturn(true).when(coreConfiguration).isRedactExceptions();
114+
assertThat(tracer.redactExceptionIfRequired(null)).isNull();
115+
116+
Exception exception = new CustomException();
117+
Throwable redacted = tracer.redactExceptionIfRequired(exception);
118+
assertThat(redacted).isInstanceOf(RedactedException.class);
119+
120+
assertThat(tracer.redactExceptionIfRequired(redacted)).isSameAs(redacted);
121+
122+
ErrorCapture errorCapture = tracer.captureException(exception, tracer.currentContext(), null);
123+
assertThat(errorCapture).isNotNull();
124+
assertThat(errorCapture.getException()).isInstanceOf(RedactedException.class);
125+
126+
ErrorCapture alreadyRedacted = tracer.captureException(redacted, tracer.currentContext(), null);
127+
assertThat(alreadyRedacted).isNotNull();
128+
assertThat(alreadyRedacted.getException()).isSameAs(redacted);
129+
}
130+
131+
110132
private static class NestedException extends Exception {
111133
public NestedException(Throwable cause) {
112134
super(cause);

apm-agent-plugins/apm-servlet-plugin/src/main/java/co/elastic/apm/agent/servlet/ServletApiAdvice.java

+6
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,12 @@ public static <HttpServletRequest, HttpServletResponse, ServletContext, ServletC
253253
break;
254254
}
255255
}
256+
if (t2 == null) {
257+
t2 = transaction.getPendingTransactionException();
258+
if(t2 != null) {
259+
overrideStatusCodeOnThrowable = false;
260+
}
261+
}
256262
}
257263

258264
ServletContext servletContext = adapter.getServletContext(httpServletRequest);

apm-agent-plugins/apm-servlet-plugin/src/main/java/co/elastic/apm/agent/servlet/helper/JakartaApmAsyncListener.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ public void onComplete(AsyncEvent event) {
7979

8080
@Override
8181
public void onTimeout(AsyncEvent event) {
82-
throwable = event.getThrowable();
82+
throwable = asyncContextAdviceHelperImpl.getTracer().redactExceptionIfRequired(event.getThrowable());
8383
if (isJBossEap6(event)) {
8484
endTransaction(event);
8585
}
@@ -98,7 +98,7 @@ public void onTimeout(AsyncEvent event) {
9898

9999
@Override
100100
public void onError(AsyncEvent event) {
101-
throwable = event.getThrowable();
101+
throwable = asyncContextAdviceHelperImpl.getTracer().redactExceptionIfRequired(event.getThrowable());
102102
if (isJBossEap6(event)) {
103103
endTransaction(event);
104104
}

apm-agent-plugins/apm-servlet-plugin/src/main/java/co/elastic/apm/agent/servlet/helper/JakartaAsyncContextAdviceHelper.java

+4
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ public JakartaAsyncContextAdviceHelper(Tracer tracer) {
4444
new JakartaAsyncContextAdviceHelper.JakartaApmAsyncListenerAllocator());
4545
}
4646

47+
public Tracer getTracer() {
48+
return tracer;
49+
}
50+
4751
private final class JakartaApmAsyncListenerAllocator implements Allocator<JakartaApmAsyncListener> {
4852
@Override
4953
public JakartaApmAsyncListener createInstance() {

apm-agent-plugins/apm-servlet-plugin/src/main/java/co/elastic/apm/agent/servlet/helper/JavaxApmAsyncListener.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ public void onComplete(AsyncEvent event) {
7878

7979
@Override
8080
public void onTimeout(AsyncEvent event) {
81-
throwable = event.getThrowable();
81+
throwable = asyncContextAdviceHelperImpl.getTracer().redactExceptionIfRequired(event.getThrowable());
8282
if (isJBossEap6(event)) {
8383
endTransaction(event);
8484
}
@@ -97,7 +97,7 @@ public void onTimeout(AsyncEvent event) {
9797

9898
@Override
9999
public void onError(AsyncEvent event) {
100-
throwable = event.getThrowable();
100+
throwable = asyncContextAdviceHelperImpl.getTracer().redactExceptionIfRequired(event.getThrowable());
101101
if (isJBossEap6(event)) {
102102
endTransaction(event);
103103
}

apm-agent-plugins/apm-servlet-plugin/src/main/java/co/elastic/apm/agent/servlet/helper/JavaxAsyncContextAdviceHelper.java

+4
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ public JavaxAsyncContextAdviceHelper(Tracer tracer) {
4545
new JavaxAsyncContextAdviceHelper.ApmAsyncListenerAllocator());
4646
}
4747

48+
public Tracer getTracer() {
49+
return tracer;
50+
}
51+
4852
private final class ApmAsyncListenerAllocator implements Allocator<JavaxApmAsyncListener> {
4953
@Override
5054
public JavaxApmAsyncListener createInstance() {

apm-agent-plugins/apm-spring-webmvc/apm-spring-webmvc-spring5/src/main/java/co/elastic/apm/agent/springwebmvc/AbstractSpringExceptionHandlerInstrumentation.java

+15-1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@
2121
import co.elastic.apm.agent.sdk.ElasticApmInstrumentation;
2222
import co.elastic.apm.agent.servlet.Constants;
2323
import co.elastic.apm.agent.servlet.adapter.ServletRequestAdapter;
24+
import co.elastic.apm.agent.tracer.GlobalTracer;
25+
import co.elastic.apm.agent.tracer.Tracer;
26+
import co.elastic.apm.agent.tracer.Transaction;
27+
import co.elastic.apm.agent.tracer.configuration.CoreConfiguration;
2428
import net.bytebuddy.description.method.MethodDescription;
2529
import net.bytebuddy.description.type.TypeDescription;
2630
import net.bytebuddy.matcher.ElementMatcher;
@@ -63,7 +67,17 @@ public static <HttpServletRequest> void captureRequestError(ServletRequestAdapte
6367
@Nullable HttpServletRequest request,
6468
@Nullable Exception e) {
6569
if (request != null && e != null) {
66-
adapter.setAttribute(request, "co.elastic.apm.exception", e);
70+
Tracer tracer = GlobalTracer.get();
71+
boolean useAttribs = tracer.getConfig(CoreConfiguration.class).isUseServletAttributesForExceptionPropagation();
72+
Throwable maybeRedacted = tracer.redactExceptionIfRequired(e);
73+
if (useAttribs) {
74+
adapter.setAttribute(request, "co.elastic.apm.exception", maybeRedacted);
75+
} else {
76+
Transaction<?> transaction = tracer.currentContext().getTransaction();
77+
if(transaction != null) {
78+
transaction.setPendingTransactionException(maybeRedacted);
79+
}
80+
}
6781
}
6882
}
6983
}

apm-agent-plugins/apm-spring-webmvc/apm-spring-webmvc-spring5/src/test/java/co/elastic/apm/agent/springwebmvc/exception/AbstractExceptionHandlerInstrumentationWithExceptionResolverTest.java

+10-3
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,11 @@
1818
*/
1919
package co.elastic.apm.agent.springwebmvc.exception;
2020

21+
import co.elastic.apm.agent.configuration.CoreConfiguration;
2122
import co.elastic.apm.agent.springwebmvc.exception.testapp.exception_resolver.ExceptionResolverController;
2223
import co.elastic.apm.agent.springwebmvc.exception.testapp.exception_resolver.ExceptionResolverRuntimeException;
23-
import org.junit.jupiter.api.Test;
24+
import org.junit.jupiter.params.ParameterizedTest;
25+
import org.junit.jupiter.params.provider.ValueSource;
2426
import org.springframework.mock.web.MockHttpServletResponse;
2527
import org.springframework.test.context.ContextConfiguration;
2628
import org.springframework.test.web.servlet.MvcResult;
@@ -30,6 +32,7 @@
3032

3133
import static org.assertj.core.api.Assertions.assertThat;
3234
import static org.junit.Assert.assertEquals;
35+
import static org.mockito.Mockito.doReturn;
3336
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
3437

3538
/**
@@ -42,8 +45,12 @@
4245
})
4346
public abstract class AbstractExceptionHandlerInstrumentationWithExceptionResolverTest extends AbstractExceptionHandlerInstrumentationTest {
4447

45-
@Test
46-
public void testCallApiWithExceptionThrown() throws Exception {
48+
@ParameterizedTest
49+
@ValueSource(booleans = {true,false})
50+
public void testCallApiWithExceptionThrown(boolean useAttributeBasedPropagation) throws Exception {
51+
CoreConfiguration coreConfig = config.getConfig(CoreConfiguration.class);
52+
doReturn(useAttributeBasedPropagation).when(coreConfig).isUseServletAttributesForExceptionPropagation();
53+
4754
ResultActions resultActions = this.mockMvc.perform(get("/exception-resolver/throw-exception"));
4855
MvcResult result = resultActions.andReturn();
4956
MockHttpServletResponse response = result.getResponse();

apm-agent-tracer/src/main/java/co/elastic/apm/agent/tracer/GlobalTracer.java

+6
Original file line numberDiff line numberDiff line change
@@ -150,4 +150,10 @@ public void reportLog(byte[] log) {
150150
public Service createService(String ephemeralId) {
151151
return tracer.createService(ephemeralId);
152152
}
153+
154+
@Nullable
155+
@Override
156+
public Throwable redactExceptionIfRequired(@Nullable Throwable original) {
157+
return tracer.redactExceptionIfRequired(original);
158+
}
153159
}

apm-agent-tracer/src/main/java/co/elastic/apm/agent/tracer/NoopTracer.java

+6
Original file line numberDiff line numberDiff line change
@@ -125,4 +125,10 @@ public void reportLog(byte[] log) {
125125
public Service createService(String ephemeralId) {
126126
return null;
127127
}
128+
129+
@Nullable
130+
@Override
131+
public Throwable redactExceptionIfRequired(@Nullable Throwable original) {
132+
return original;
133+
}
128134
}

apm-agent-tracer/src/main/java/co/elastic/apm/agent/tracer/Tracer.java

+3
Original file line numberDiff line numberDiff line change
@@ -88,4 +88,7 @@ public interface Tracer {
8888

8989
@Nullable
9090
Service createService(String ephemeralId);
91+
92+
@Nullable
93+
Throwable redactExceptionIfRequired(@Nullable Throwable original);
9194
}

apm-agent-tracer/src/main/java/co/elastic/apm/agent/tracer/Transaction.java

+5
Original file line numberDiff line numberDiff line change
@@ -56,4 +56,9 @@ public interface Transaction<T extends Transaction<T>> extends AbstractSpan<T> {
5656
void setFrameworkName(@Nullable String frameworkName);
5757

5858
void setFrameworkVersion(@Nullable String frameworkVersion);
59+
60+
void setPendingTransactionException(Throwable exception);
61+
62+
@Nullable
63+
Throwable getPendingTransactionException();
5964
}

apm-agent-tracer/src/main/java/co/elastic/apm/agent/tracer/configuration/CoreConfiguration.java

+2
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ public interface CoreConfiguration {
5050

5151
TimeDuration getSpanMinDuration();
5252

53+
boolean isUseServletAttributesForExceptionPropagation();
54+
5355
enum EventType {
5456
/**
5557
* Request bodies will never be reported

0 commit comments

Comments
 (0)