Skip to content

Commit 0613ed4

Browse files
add proper logging to junit runs
1 parent bbb06b8 commit 0613ed4

File tree

9 files changed

+228
-59
lines changed

9 files changed

+228
-59
lines changed

airbyte-cdk/java/airbyte-cdk/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,7 @@ MavenLocal debugging steps:
166166

167167
| Version | Date | Pull Request | Subject |
168168
|:--------|:-----------|:-----------------------------------------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------|
169+
| 0.21.3 | 2024-02-20 | [\#35394](https://github.com/airbytehq/airbyte/pull/35394) | Add Junit progress information to the test logs |
169170
| 0.21.2 | 2024-02-20 | [\#34978](https://github.com/airbytehq/airbyte/pull/34978) | Reduce log noise in NormalizationLogParser. |
170171
| 0.21.1 | 2024-02-20 | [\#35199](https://github.com/airbytehq/airbyte/pull/35199) | Add thread names to the logs. |
171172
| 0.21.0 | 2024-02-16 | [\#35314](https://github.com/airbytehq/airbyte/pull/35314) | Delete S3StreamCopier classes. These have been superseded by the async destinations framework. |
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
version=0.21.2
1+
version=0.21.3

airbyte-cdk/java/airbyte-cdk/core/src/test/java/io/airbyte/cdk/integrations/base/AirbyteLogMessageTemplateTest.java

Lines changed: 19 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -25,59 +25,54 @@
2525
import org.apache.logging.log4j.core.config.Configuration;
2626
import org.apache.logging.log4j.core.config.Configurator;
2727
import org.apache.logging.log4j.core.config.LoggerConfig;
28-
import org.junit.jupiter.api.AfterAll;
29-
import org.junit.jupiter.api.BeforeAll;
30-
import org.junit.jupiter.api.BeforeEach;
28+
import org.apache.logging.log4j.spi.ExtendedLogger;
29+
import org.junit.jupiter.api.AfterEach;
3130
import org.junit.jupiter.api.Test;
3231
import org.junit.jupiter.params.ParameterizedTest;
3332
import org.junit.jupiter.params.provider.ValueSource;
3433
import org.junit.platform.commons.util.StringUtils;
35-
import org.slf4j.Logger;
36-
import org.slf4j.LoggerFactory;
3734

3835
public class AirbyteLogMessageTemplateTest {
3936

40-
private static final ByteArrayOutputStream outputContent = new ByteArrayOutputStream();
41-
private static final Logger LOGGER = LoggerFactory.getLogger(AirbyteLogMessageTemplateTest.class);
4237
public static final String OUTPUT_STREAM_APPENDER = "OutputStreamAppender";
4338
public static final String CONSOLE_JSON_APPENDER = "ConsoleJSONAppender";
44-
private static OutputStreamAppender outputStreamAppender;
45-
private static LoggerConfig rootLoggerConfig;
46-
private static LoggerContext loggerContext;
39+
private LoggerContext loggerContext;
40+
private LoggerConfig rootLoggerConfig;
41+
private ExtendedLogger logger;
42+
private OutputStreamAppender outputStreamAppender;
43+
private ByteArrayOutputStream outputContent;
4744

48-
@BeforeAll
49-
static void init() {
45+
void getLogger() {
5046
// We are creating a log appender with the same output pattern
5147
// as the console json appender defined in this project's log4j2.xml file.
5248
// We then attach this log appender with the LOGGER instance so that we can validate the logs
5349
// produced by code and assert that it matches the expected format.
5450
loggerContext = Configurator.initialize(null, "log4j2.xml");
51+
5552
final Configuration configuration = loggerContext.getConfiguration();
5653
rootLoggerConfig = configuration.getLoggerConfig("");
5754

55+
outputContent = new ByteArrayOutputStream();
5856
outputStreamAppender = OutputStreamAppender.createAppender(
5957
rootLoggerConfig.getAppenders().get(CONSOLE_JSON_APPENDER).getLayout(),
6058
null, outputContent, OUTPUT_STREAM_APPENDER, false, true);
6159
outputStreamAppender.start();
6260

6361
rootLoggerConfig.addAppender(outputStreamAppender, Level.ALL, null);
62+
logger = loggerContext.getLogger(AirbyteLogMessageTemplateTest.class);
6463
}
6564

66-
@BeforeEach
67-
void setup() {
68-
outputContent.reset();
69-
}
70-
71-
@AfterAll
72-
static void cleanUp() {
65+
@AfterEach
66+
void closeLogger() {
7367
outputStreamAppender.stop();
7468
rootLoggerConfig.removeAppender(OUTPUT_STREAM_APPENDER);
7569
loggerContext.close();
7670
}
7771

7872
@Test
7973
public void testAirbyteLogMessageFormat() throws java.io.IOException {
80-
LOGGER.info("hello");
74+
getLogger();
75+
logger.info("hello");
8176

8277
outputContent.flush();
8378
final String logMessage = outputContent.toString(StandardCharsets.UTF_8);
@@ -114,12 +109,13 @@ private AirbyteLogMessage validateAirbyteMessageIsLog(final AirbyteMessage airby
114109

115110
@ParameterizedTest
116111
@ValueSource(ints = {2, 100, 9000})
117-
public void testAirbyteLogMessageLength(int stringRepeatitions) throws java.io.IOException {
112+
public void testAirbyteLogMessageLength(int stringRepetitions) throws java.io.IOException {
113+
getLogger();
118114
final StringBuilder sb = new StringBuilder();
119-
for (int i = 0; i < stringRepeatitions; i++) {
115+
for (int i = 0; i < stringRepetitions; i++) {
120116
sb.append("abcd");
121117
}
122-
LOGGER.info(sb.toString(), new RuntimeException("aaaaa bbbbbb ccccccc dddddd"));
118+
logger.info(sb.toString(), new RuntimeException("aaaaa bbbbbb ccccccc dddddd"));
123119
outputContent.flush();
124120
final String logMessage = outputContent.toString(StandardCharsets.UTF_8);
125121

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
/*
2+
* Copyright (c) 2023 Airbyte, Inc., all rights reserved.
3+
*/
4+
5+
package io.airbyte.cdk.extensions;
6+
7+
import java.lang.reflect.Constructor;
8+
import java.lang.reflect.InvocationHandler;
9+
import java.lang.reflect.Method;
10+
import java.lang.reflect.Proxy;
11+
import java.time.Duration;
12+
import java.time.Instant;
13+
import java.util.Arrays;
14+
import java.util.regex.Matcher;
15+
import java.util.regex.Pattern;
16+
import java.util.stream.Collectors;
17+
import org.apache.commons.lang3.exception.ExceptionUtils;
18+
import org.junit.jupiter.api.extension.DynamicTestInvocationContext;
19+
import org.junit.jupiter.api.extension.ExtensionContext;
20+
import org.junit.jupiter.api.extension.InvocationInterceptor;
21+
import org.junit.jupiter.api.extension.ReflectiveInvocationContext;
22+
import org.slf4j.Logger;
23+
import org.slf4j.LoggerFactory;
24+
25+
/**
26+
* By default, junit only output logs to the console, and nothing makes it into log4j logs. This
27+
* class fixes that by using the interceptor facility to print progress and timing information. This
28+
* allows us to have junit loglines in our test logs. This is instanciated via <a href=
29+
* "https://docs.oracle.com/javase%2F9%2Fdocs%2Fapi%2F%2F/java/util/ServiceLoader.html">Java's
30+
* ServiceLoader</a> The declaration can be found in
31+
* resources/META-INF/services/org.junit.jupiter.api.extension.Extension
32+
*/
33+
public class LoggingInvocationInterceptor implements InvocationInterceptor {
34+
35+
private static final class LoggingInvocationInterceptorHandler implements InvocationHandler {
36+
37+
private static final Logger LOGGER = LoggerFactory.getLogger(LoggingInvocationInterceptor.class);
38+
39+
private static final Pattern methodPattern = Pattern.compile("intercept(.*)Method");
40+
41+
@Override
42+
@SuppressWarnings("unchecked")
43+
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
44+
if (LoggingInvocationInterceptor.class.getDeclaredMethod(method.getName(), Invocation.class, ReflectiveInvocationContext.class,
45+
ExtensionContext.class) == null) {
46+
LOGGER.error("Junit LoggingInvocationInterceptor executing unknown interception point {}", method.getName());
47+
return method.invoke(proxy, args);
48+
}
49+
var invocation = (Invocation<?>) args[0];
50+
var invocationContext = (ReflectiveInvocationContext<Method>) args[1];
51+
var extensionContext = (ExtensionContext) args[2];
52+
String methodName = method.getName();
53+
String logLineSuffix;
54+
Matcher methodMatcher = methodPattern.matcher(methodName);
55+
if (methodName.equals("interceptDynamicTest")) {
56+
logLineSuffix = "execution of DynamicTest %s".formatted(extensionContext.getDisplayName());
57+
} else if (methodName.equals("interceptTestClassConstructor")) {
58+
logLineSuffix = "instance creation for %s".formatted(invocationContext.getTargetClass());
59+
} else if (methodMatcher.matches()) {
60+
String interceptedEvent = methodMatcher.group(1);
61+
logLineSuffix = "execution of @%s method %s.%s".formatted(invocationContext.getExecutable().getDeclaringClass().getSimpleName(),
62+
interceptedEvent, invocationContext.getExecutable().getName());
63+
} else {
64+
logLineSuffix = "execution of unknown intercepted call %s".formatted(methodName);
65+
}
66+
LOGGER.info("Junit starting {}", logLineSuffix);
67+
try {
68+
Instant start = Instant.now();
69+
Object retVal = invocation.proceed();
70+
long elapsedMs = Duration.between(start, Instant.now()).toMillis();
71+
LOGGER.info("Junit completed {} in {} ms", logLineSuffix, elapsedMs);
72+
return retVal;
73+
} catch (Throwable t) {
74+
String stackTrace = Arrays.stream(ExceptionUtils.getStackFrames(t)).takeWhile(s -> !s.startsWith("\tat org.junit")).collect(
75+
Collectors.joining("\n "));
76+
LOGGER.warn("Junit exception throw during {}:\n{}", logLineSuffix, stackTrace);
77+
throw t;
78+
}
79+
}
80+
81+
}
82+
83+
private final InvocationInterceptor proxy = (InvocationInterceptor) Proxy.newProxyInstance(
84+
getClass().getClassLoader(),
85+
new Class[] {InvocationInterceptor.class},
86+
new LoggingInvocationInterceptorHandler());
87+
88+
@Override
89+
public void interceptAfterAllMethod(Invocation<Void> invocation,
90+
ReflectiveInvocationContext<Method> invocationContext,
91+
ExtensionContext extensionContext)
92+
throws Throwable {
93+
proxy.interceptAfterAllMethod(invocation, invocationContext, extensionContext);
94+
}
95+
96+
@Override
97+
public void interceptAfterEachMethod(Invocation<Void> invocation,
98+
ReflectiveInvocationContext<Method> invocationContext,
99+
ExtensionContext extensionContext)
100+
throws Throwable {
101+
proxy.interceptAfterEachMethod(invocation, invocationContext, extensionContext);
102+
}
103+
104+
@Override
105+
public void interceptBeforeAllMethod(Invocation<Void> invocation,
106+
ReflectiveInvocationContext<Method> invocationContext,
107+
ExtensionContext extensionContext)
108+
throws Throwable {
109+
proxy.interceptBeforeAllMethod(invocation, invocationContext, extensionContext);
110+
}
111+
112+
@Override
113+
public void interceptBeforeEachMethod(Invocation<Void> invocation,
114+
ReflectiveInvocationContext<Method> invocationContext,
115+
ExtensionContext extensionContext)
116+
throws Throwable {
117+
proxy.interceptBeforeEachMethod(invocation, invocationContext, extensionContext);
118+
}
119+
120+
@Override
121+
public void interceptDynamicTest(Invocation<Void> invocation,
122+
DynamicTestInvocationContext invocationContext,
123+
ExtensionContext extensionContext)
124+
throws Throwable {
125+
proxy.interceptDynamicTest(invocation, invocationContext, extensionContext);
126+
}
127+
128+
@Override
129+
public void interceptTestMethod(Invocation<Void> invocation,
130+
ReflectiveInvocationContext<Method> invocationContext,
131+
ExtensionContext extensionContext)
132+
throws Throwable {
133+
proxy.interceptTestMethod(invocation, invocationContext, extensionContext);
134+
}
135+
136+
@Override
137+
public void interceptTestTemplateMethod(Invocation<Void> invocation,
138+
ReflectiveInvocationContext<Method> invocationContext,
139+
ExtensionContext extensionContext)
140+
throws Throwable {
141+
proxy.interceptTestTemplateMethod(invocation, invocationContext, extensionContext);
142+
}
143+
144+
@Override
145+
public <T> T interceptTestFactoryMethod(Invocation<T> invocation,
146+
ReflectiveInvocationContext<Method> invocationContext,
147+
ExtensionContext extensionContext)
148+
throws Throwable {
149+
return proxy.interceptTestFactoryMethod(invocation, invocationContext, extensionContext);
150+
}
151+
152+
@Override
153+
public <T> T interceptTestClassConstructor(Invocation<T> invocation,
154+
ReflectiveInvocationContext<Constructor<T>> invocationContext,
155+
ExtensionContext extensionContext)
156+
throws Throwable {
157+
return proxy.interceptTestClassConstructor(invocation, invocationContext, extensionContext);
158+
}
159+
160+
}

airbyte-cdk/java/airbyte-cdk/core/src/testFixtures/java/io/airbyte/cdk/testutils/ContainerFactory.java

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,15 @@
1212
import java.util.List;
1313
import java.util.concurrent.ConcurrentHashMap;
1414
import java.util.concurrent.ConcurrentMap;
15+
import java.util.concurrent.atomic.AtomicInteger;
1516
import java.util.function.Supplier;
1617
import java.util.stream.Stream;
18+
import org.apache.commons.lang3.StringUtils;
1719
import org.slf4j.Logger;
1820
import org.slf4j.LoggerFactory;
1921
import org.testcontainers.containers.GenericContainer;
2022
import org.testcontainers.containers.JdbcDatabaseContainer;
23+
import org.testcontainers.containers.output.OutputFrame;
2124
import org.testcontainers.containers.output.Slf4jLogConsumer;
2225
import org.testcontainers.utility.DockerImageName;
2326

@@ -65,10 +68,13 @@ GenericContainer<?> container() {
6568
}
6669

6770
private static final ConcurrentMap<ContainerKey, ContainerOrException> SHARED_CONTAINERS = new ConcurrentHashMap<>();
71+
private static final AtomicInteger containerId = new AtomicInteger(0);
6872

69-
private static final MdcScope.Builder TESTCONTAINER_LOG_MDC_BUILDER = new MdcScope.Builder()
70-
.setLogPrefix("testcontainer")
71-
.setPrefixColor(LoggingHelper.Color.RED_BACKGROUND);
73+
private static final MdcScope.Builder getTestContainerLogMdcBuilder(DockerImageName imageName, List<String> methods) {
74+
return new MdcScope.Builder()
75+
.setLogPrefix("testcontainer %s (%s[%s]):".formatted(containerId.incrementAndGet(), imageName, StringUtils.join(methods, ",")))
76+
.setPrefixColor(LoggingHelper.Color.RED_BACKGROUND);
77+
}
7278

7379
/**
7480
* Creates a new, unshared testcontainer instance. This usually wraps the default constructor for
@@ -108,8 +114,16 @@ private GenericContainer<?> createAndStartContainer(DockerImageName imageName, L
108114
for (String methodName : methodNames) {
109115
methods.add(getClass().getMethod(methodName, container.getClass()));
110116
}
111-
final var logConsumer = new Slf4jLogConsumer(LOGGER);
112-
TESTCONTAINER_LOG_MDC_BUILDER.produceMappings(logConsumer::withMdc);
117+
final var logConsumer = new Slf4jLogConsumer(LOGGER) {
118+
119+
public void accept(OutputFrame frame) {
120+
if (frame.getUtf8StringWithoutLineEnding().trim().length() > 0) {
121+
super.accept(frame);
122+
}
123+
}
124+
125+
};
126+
getTestContainerLogMdcBuilder(imageName, methodNames).produceMappings(logConsumer::withMdc);
113127
container.withLogConsumer(logConsumer);
114128
for (Method method : methods) {
115129
LOGGER.info("Calling {} in {} on new shared container based on {}.",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
io.airbyte.cdk.extensions.LoggingInvocationInterceptor

airbyte-cdk/java/airbyte-cdk/dependencies/src/main/java/io/airbyte/commons/logging/LoggingHelper.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,10 @@ public String getCode() {
4141

4242
@VisibleForTesting
4343
public static final String RESET = "\u001B[0m";
44+
public static final String PREPARE_COLOR_CHAR = "\u001b[m";
4445

4546
public static String applyColor(final Color color, final String msg) {
46-
return color.getCode() + msg + RESET;
47+
return PREPARE_COLOR_CHAR + color.getCode() + msg + PREPARE_COLOR_CHAR + RESET;
4748
}
4849

4950
}

0 commit comments

Comments
 (0)