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

Lines changed: 4 additions & 3 deletions
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

Lines changed: 17 additions & 0 deletions
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

Lines changed: 12 additions & 0 deletions
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
}
Lines changed: 28 additions & 0 deletions
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

Lines changed: 17 additions & 0 deletions
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

Lines changed: 22 additions & 0 deletions
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

Lines changed: 6 additions & 0 deletions
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

Lines changed: 2 additions & 2 deletions
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

Lines changed: 4 additions & 0 deletions
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

Lines changed: 2 additions & 2 deletions
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
}

0 commit comments

Comments
 (0)