Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement GlobalLogCollector for automatic collecting logs when test fails #124

Merged
merged 1 commit into from
Jul 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions test-frame-log-collector/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,43 @@ the logs path will then look like this:
```
The tree path will look similarly to above examples, there will be folders for Namespaces matching the specified labels.

### Global log collector
`CollectLogs` is an annotation which can handle collecting logs which user want automatically in case of test failure or before/after failure.
It gets configuration passed into `GlobalLogCollector` and call collecting in proper callbacks.

Register `GlobalLogCollector` handlers and configure

```java
import io.skodjob.testframe.LogCollectorBuilder;
import io.skodjob.testframe.annotations.CollectLogs;
import io.skodjob.testframe.listeners.GlobalLogCollector;
import io.skodjob.testframe.resources.KubeResourceManager;
import org.junit.jupiter.api.Test;

@CollectLogs
class TestClass() {
static {
// Setup global log collector and handlers
GlobalLogCollector.setupGlobalLogCollector(new LogCollectorBuilder()
.withNamespacedResources("sa", "deployment", "configmaps", "secret")
.withClusterWideResources("nodes")
.withKubeClient(KubeResourceManager.getKubeClient())
.withKubeCmdClient(KubeResourceManager.getKubeCmdClient())
.withRootFolderPath("/some-path/path/")
.build());
GlobalLogCollector.addLogCallback(() -> {
GlobalLogCollector.getGlobalLogCollector().collectFromNamespaces("test-namespace", "test-namespace-2");
GlobalLogCollector.getGlobalLogCollector().collectClusterWideResources();
});
}

@Test
void test() {
...
}
}
```

### Specifying additional folder path

In case that you would like to collect the logs to additional sub-directories of your root folder, the `LogCollector` contains
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* Copyright Skodjob authors.
* License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
*/
package io.skodjob.testframe.annotations;

import io.skodjob.testframe.listeners.GlobalLogCollector;
import org.junit.jupiter.api.extension.ExtendWith;

import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.RetentionPolicy.RUNTIME;

/**
* This annotation is used to automatically call log collecting
* when test of prepare and post phase fails in JUnit tests.
* It is applied at the class level.
* <p>
* It uses the {@link GlobalLogCollector}
*/
@Target(ElementType.TYPE)
@Retention(RUNTIME)
@Inherited
@ExtendWith(GlobalLogCollector.class)
public @interface CollectLogs {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
/*
* Copyright Skodjob authors.
* License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
*/
package io.skodjob.testframe.listeners;

import io.skodjob.testframe.LogCollector;
import io.skodjob.testframe.annotations.CollectLogs;
import io.skodjob.testframe.interfaces.ThrowableRunner;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.LifecycleMethodExecutionExceptionHandler;
import org.junit.jupiter.api.extension.TestExecutionExceptionHandler;

import java.util.LinkedList;
import java.util.List;

/**
* Represents global log collector which is automatically called on text fail or error even in setup or post methods
*/
public class GlobalLogCollector implements TestExecutionExceptionHandler, LifecycleMethodExecutionExceptionHandler {
private static final Logger LOGGER = LogManager.getLogger(GlobalLogCollector.class);
private static LogCollector globalInstance;
private static final List<ThrowableRunner> COLLECT_CALLBACKS = new LinkedList<>();

/**
* Private constructor
*/
private GlobalLogCollector() {
// empty constructor
}

/**
* Handler when test fails
*
* @param extensionContext extension context
* @param throwable throwable
* @throws Throwable original throwable
*/
@Override
public void handleTestExecutionException(ExtensionContext extensionContext, Throwable throwable) throws Throwable {
saveKubeState();
throw throwable;
}

/**
* Handles beforeAll exception
*
* @param context extensionContext
* @param throwable throwable
* @throws Throwable original throwable
*/
@Override
public void handleBeforeAllMethodExecutionException(ExtensionContext context, Throwable throwable)throws Throwable {
saveKubeState();
LifecycleMethodExecutionExceptionHandler.super.handleBeforeAllMethodExecutionException(context, throwable);
}

/**
* Handles beforeEach exception
*
* @param context extensionContext
* @param throwable throwable
* @throws Throwable original throwable
*/
@Override
public void handleBeforeEachMethodExecutionException(ExtensionContext context,
Throwable throwable) throws Throwable {
saveKubeState();
LifecycleMethodExecutionExceptionHandler.super.handleBeforeEachMethodExecutionException(context, throwable);
}

/**
* Handles afterEach exception
*
* @param context extensionContext
* @param throwable throwable
* @throws Throwable original throwable
*/
@Override
public void handleAfterEachMethodExecutionException(ExtensionContext context,
Throwable throwable) throws Throwable {
saveKubeState();
LifecycleMethodExecutionExceptionHandler.super.handleAfterEachMethodExecutionException(context, throwable);
}

/**
* Handles afterAll exception
*
* @param context extensionContext
* @param throwable throwable
* @throws Throwable original throwable
*/
@Override
public void handleAfterAllMethodExecutionException(ExtensionContext context, Throwable throwable) throws Throwable {
saveKubeState();
LifecycleMethodExecutionExceptionHandler.super.handleAfterAllMethodExecutionException(context, throwable);
}

/**
* Setup globalLogCollector which is automatically used within {@link CollectLogs} annotation
*
* @param globalLogCollector log collector instance
*/
public static void setupGlobalLogCollector(LogCollector globalLogCollector) {
globalInstance = globalLogCollector;
}

/**
* Returns globalLogCollector instance
*
* @return global log collector instance
*/
public static LogCollector getGlobalLogCollector() {
if (globalInstance == null) {
throw new NullPointerException("Global log collector is not initialized");
}
return globalInstance;
}

/**
* Adds callback for running log collecting
*
* @param callback callback method with log collecting
*/
public static void addLogCallback(ThrowableRunner callback) {
COLLECT_CALLBACKS.add(callback);
}

private void saveKubeState() {
try {
for (ThrowableRunner runner : COLLECT_CALLBACKS) {
runner.run();
}
} catch (Exception ex) {
LOGGER.error("Cannot collect all data", ex);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
*/
package io.skodjob.testframe.test.integration;

import io.skodjob.testframe.LogCollectorBuilder;
import io.skodjob.testframe.annotations.CollectLogs;
import io.skodjob.testframe.listeners.GlobalLogCollector;
import io.skodjob.testframe.test.integration.helpers.GlobalLogCollectorTestHandler;
import io.skodjob.testframe.utils.LoggerUtils;
import io.skodjob.testframe.annotations.ResourceManager;
import io.skodjob.testframe.annotations.TestVisualSeparator;
Expand All @@ -12,33 +16,61 @@
import io.skodjob.testframe.resources.NamespaceType;
import io.skodjob.testframe.resources.ServiceAccountType;
import io.skodjob.testframe.utils.KubeUtils;
import org.junit.jupiter.api.extension.ExtendWith;

import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.atomic.AtomicBoolean;

@ExtendWith(GlobalLogCollectorTestHandler.class) // For testing purpose
@ResourceManager
@CollectLogs
@TestVisualSeparator
public abstract class AbstractIT {
static AtomicBoolean isCreateHandlerCalled = new AtomicBoolean(false);
static AtomicBoolean isDeleteHandlerCalled = new AtomicBoolean(false);
public static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm");
public static final Path LOG_DIR = Paths.get(System.getProperty("user.dir"), "target", "logs")
.resolve("test-run-" + DATE_FORMAT.format(LocalDateTime.now()));

static {
// Register resources which KRM uses for handling instead of native status check
KubeResourceManager.getInstance().setResourceTypes(
new NamespaceType(),
new ServiceAccountType(),
new DeploymentType()
);

// Register callback which are called with every create resource method for every resource
KubeResourceManager.getInstance().addCreateCallback(r -> {
isCreateHandlerCalled.set(true);
if (r.getKind().equals("Namespace")) {
KubeUtils.labelNamespace(r.getMetadata().getName(), "test-label", "true");
}
});

// Register callback which are called with every delete resource method for every resource
KubeResourceManager.getInstance().addDeleteCallback(r -> {
isDeleteHandlerCalled.set(true);
if (r.getKind().equals("Namespace")) {
LoggerUtils.logResource("Deleted", r);
}
});

// Setup global log collector and handlers
GlobalLogCollector.setupGlobalLogCollector(new LogCollectorBuilder()
.withNamespacedResources("sa", "deployment", "configmaps", "secret")
.withClusterWideResources("nodes")
.withKubeClient(KubeResourceManager.getKubeClient())
.withKubeCmdClient(KubeResourceManager.getKubeCmdClient())
.withRootFolderPath(LOG_DIR.toString())
.build());
GlobalLogCollector.addLogCallback(() -> {
GlobalLogCollector.getGlobalLogCollector().collectFromNamespaces("default");
GlobalLogCollector.getGlobalLogCollector().collectClusterWideResources();
});
}

protected String nsName1 = "test";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* Copyright Skodjob authors.
* License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
*/
package io.skodjob.testframe.test.integration;

import org.apache.commons.io.FileUtils;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;

import java.io.IOException;

import static org.junit.jupiter.api.Assertions.fail;

@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class GlobalLogCollectorIT extends AbstractIT {
@Test
void testGlobalLogCollector() {
fail("Expected issue");
}

@AfterEach
void clean() throws IOException {
FileUtils.deleteDirectory(LOG_DIR.toFile());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Copyright Skodjob authors.
* License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
*/
package io.skodjob.testframe.test.integration.helpers;

import io.skodjob.testframe.test.integration.AbstractIT;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.TestExecutionExceptionHandler;

import static org.junit.jupiter.api.Assertions.assertTrue;

/**
* Override test failure for expected test issue in GlobalLogCollectorIT test
*/
public class GlobalLogCollectorTestHandler implements TestExecutionExceptionHandler {

/**
* Check cause message and if it is from GlobalLogCollectorIT check if logs
* are collected and do not mark test as failure
*
* @param context extension context
* @param cause throwable object
* @throws Throwable throwable object
*/
@Override
public void handleTestExecutionException(ExtensionContext context, Throwable cause) throws Throwable {
if (cause.getMessage().contains("Expected issue")) {
assertTrue(AbstractIT.LOG_DIR.toFile().exists());
assertTrue(AbstractIT.LOG_DIR.resolve("default").toFile().exists());
assertTrue(AbstractIT.LOG_DIR.resolve("cluster-wide-resources").toFile().exists());
} else {
throw cause;
}
}
}
Loading