Skip to content

Commit c779bc9

Browse files
authored
Add API to capture logs in unit tests (#8513)
1 parent 6055d50 commit c779bc9

File tree

7 files changed

+181
-1
lines changed

7 files changed

+181
-1
lines changed

dd-java-agent/agent-iast/build.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ dependencies {
6262
testImplementation group: 'io.grpc', name: 'grpc-core', version: grpcVersion
6363
testImplementation group: 'io.grpc', name: 'grpc-protobuf', version: grpcVersion
6464

65+
testImplementation libs.logback.classic
66+
6567
jmh project(':utils:test-utils')
6668
jmh project(':dd-trace-core')
6769
jmh project(':dd-java-agent:agent-builder')

dd-java-agent/agent-iast/src/test/groovy/com/datadog/iast/IastSystemTest.groovy

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import datadog.trace.api.iast.sink.StacktraceLeakModule
2121
import datadog.trace.api.iast.sink.XContentTypeModule
2222
import datadog.trace.api.internal.TraceSegment
2323
import datadog.trace.bootstrap.Agent
24+
import datadog.trace.test.logging.TestLogCollector
2425
import datadog.trace.test.util.DDSpecification
2526

2627
import static com.datadog.iast.test.TaintedObjectsUtils.noOpTaintedObjects
@@ -33,8 +34,13 @@ class IastSystemTest extends DDSpecification {
3334
InstrumentationBridge.clearIastModules()
3435
}
3536

37+
def cleanup() {
38+
TestLogCollector.disable()
39+
}
40+
3641
void 'start'() {
3742
given:
43+
TestLogCollector.enable()
3844
final ig = new InstrumentationGateway()
3945
final ss = Spy(ig.getSubscriptionService(RequestContextSlot.IAST))
4046
final cbp = ig.getCallbackProvider(RequestContextSlot.IAST)
@@ -47,7 +53,6 @@ class IastSystemTest extends DDSpecification {
4753
}
4854
final igSpanInfo = Mock(IGSpanInfo)
4955

50-
5156
when:
5257
IastSystem.start(ss)
5358

@@ -57,6 +62,7 @@ class IastSystemTest extends DDSpecification {
5762
1 * ss.registerCallback(Events.get().requestHeader(), _)
5863
1 * ss.registerCallback(Events.get().grpcServerRequestMessage(), _)
5964
0 * _
65+
TestLogCollector.drainCapturedLogs().any { it.message.contains('IAST is starting') }
6066

6167
when:
6268
final startCallback = cbp.getCallback(Events.get().requestStarted())
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<configuration>
2+
<appender name="TEST" class="datadog.trace.test.logging.TestLogbackAppender"/>
3+
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
4+
<encoder>
5+
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
6+
</encoder>
7+
</appender>
8+
<root level="DEBUG">
9+
<appender-ref ref="TEST"/>
10+
<appender-ref ref="STDOUT"/>
11+
</root>
12+
</configuration>

utils/test-utils/build.gradle

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,7 @@ dependencies {
99

1010
api group: 'com.github.stefanbirkner', name: 'system-rules', version: '1.19.0'
1111
api group: 'commons-fileupload', name: 'commons-fileupload', version: '1.5'
12+
13+
compileOnly libs.logback.core
14+
compileOnly libs.logback.classic
1215
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package datadog.trace.test.logging;
2+
3+
import java.util.Arrays;
4+
import java.util.Objects;
5+
import org.slf4j.Marker;
6+
7+
public final class CapturedLog {
8+
public final Marker marker;
9+
public final String level;
10+
public final String template;
11+
public final Object[] arguments;
12+
public final String message;
13+
14+
public CapturedLog(
15+
final Marker marker,
16+
final String level,
17+
final String template,
18+
final Object[] arguments,
19+
final String message) {
20+
this.marker = marker;
21+
this.level = level;
22+
this.template = template;
23+
this.arguments = arguments;
24+
this.message = message;
25+
}
26+
27+
@Override
28+
public String toString() {
29+
return "CapturedLog{"
30+
+ "marker="
31+
+ marker
32+
+ ", level='"
33+
+ level
34+
+ '\''
35+
+ ", template='"
36+
+ template
37+
+ '\''
38+
+ ", arguments=..."
39+
+ ", message='"
40+
+ message
41+
+ '\''
42+
+ '}';
43+
}
44+
45+
@Override
46+
public int hashCode() {
47+
final int argsHash = arguments == null ? 0 : Arrays.hashCode(arguments);
48+
return Objects.hash(marker, level, template, argsHash, message);
49+
}
50+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package datadog.trace.test.logging;
2+
3+
import java.util.ArrayList;
4+
import java.util.List;
5+
import java.util.concurrent.LinkedBlockingDeque;
6+
import java.util.concurrent.atomic.AtomicReference;
7+
8+
public final class TestLogCollector {
9+
static final TestLogCollector INSTANCE = new TestLogCollector();
10+
11+
private final AtomicReference<LinkedBlockingDeque<CapturedLog>> logs =
12+
new AtomicReference<>(null);
13+
14+
private TestLogCollector() {}
15+
16+
/**
17+
* Enable the test log collector. This should be called before any test that needs the logs.
18+
* {@link #disable()} must always be called at cleanup.
19+
*/
20+
public static void enable() {
21+
INSTANCE._enable();
22+
}
23+
24+
/** Must be called at least once after {@link #enable()} to cleanup the test log collector. */
25+
public static void disable() {
26+
INSTANCE._disable();
27+
}
28+
29+
/**
30+
* Get all captured logs and clear the internal buffer. {@link #enable()} must have been called
31+
* before.
32+
*/
33+
public static List<CapturedLog> drainCapturedLogs() {
34+
return INSTANCE._drainCapturedLogs();
35+
}
36+
37+
private void _enable() {
38+
final LinkedBlockingDeque<CapturedLog> logs = new LinkedBlockingDeque<>();
39+
if (!this.logs.compareAndSet(null, logs)) {
40+
throw new IllegalStateException("TestLogCollector was enabled without prior cleanup");
41+
}
42+
}
43+
44+
private List<CapturedLog> _drainCapturedLogs() {
45+
final List<CapturedLog> result = new ArrayList<>();
46+
final LinkedBlockingDeque<CapturedLog> logs = this.logs.get();
47+
if (logs == null) {
48+
throw new IllegalStateException("TestLogCollector was not enabled before draining logs");
49+
}
50+
logs.drainTo(result);
51+
return result;
52+
}
53+
54+
private void _disable() {
55+
final LinkedBlockingDeque<CapturedLog> logs = this.logs.getAndSet(null);
56+
if (logs == null) {
57+
return;
58+
}
59+
logs.clear();
60+
}
61+
62+
void addLog(final CapturedLog log) {
63+
final LinkedBlockingDeque<CapturedLog> logs = this.logs.get();
64+
if (logs != null) {
65+
logs.add(log);
66+
}
67+
}
68+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package datadog.trace.test.logging;
2+
3+
import ch.qos.logback.classic.spi.ILoggingEvent;
4+
import ch.qos.logback.core.AppenderBase;
5+
6+
/**
7+
* Logback appender that captures logs for testing.
8+
*
9+
* <p>To set this up, add the following to your logback-test.xml:
10+
*
11+
* <pre>{@code
12+
* <configuration>
13+
* <appender name="TEST" class="datadog.trace.test.logging.TestLogbackAppender" />
14+
* <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
15+
* <encoder>
16+
* <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
17+
* </encoder>
18+
* </appender>
19+
* <root level="DEBUG">
20+
* <appender-ref ref="TEST" />
21+
* <appender-ref ref="STDOUT" />
22+
* </root>
23+
* </configuration>
24+
* }</pre>
25+
*/
26+
public final class TestLogbackAppender extends AppenderBase<ILoggingEvent> {
27+
28+
@Override
29+
protected void append(final ILoggingEvent event) {
30+
final CapturedLog log =
31+
new CapturedLog(
32+
event.getMarker(),
33+
event.getLevel().levelStr,
34+
event.getMessage(),
35+
event.getArgumentArray(),
36+
event.getFormattedMessage());
37+
TestLogCollector.INSTANCE.addLog(log);
38+
}
39+
}

0 commit comments

Comments
 (0)