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

Server log report extension #230

Merged
merged 12 commits into from
Dec 17, 2019
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
17 changes: 16 additions & 1 deletion jbehave-support-core/docs/Reporting.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,18 @@ There are several extensions already prepared and ready to use:
- `JmsXmlReporterExtension` (copies [JMS](Jms.md) message headers, prints out the message if it is `javax.jms.TextMessage`)
- `ScreenshotReporterExtension` (prints out screenshots (except error) from [Web testing](Web-testing.md) - if any were generated)
- Frequency of screenshot taking can be controlled by property: 'web.screenshot.reporting.mode'
- MANUAL: screenshots from manual step only
- MANUAL: screenshots from a manual step only
- WAIT: screenshots after every web wait
- STEP: screenshots after every web step
- DEBUG: screenshots after every web step and action
- `ServerLogXmlReporterExtension` (copies server logs, or their parts used by [SshSteps](Ssh.md))
- Content can be controlled by property: ssh.reporting.mode
- FULL: copies server log(s) for each system with configured [SshTemplate](Ssh.md)
- TEMPLATE: copies server log(s) for each system with configured SshTemplate with an attribute reportable = true
- CACHE: copies cached server log(s) used within scenario execution
- Extension contains fail mode, which acts like TEMPLATE mode if test fails
- it can be turned on by using property: ssh.reporting.logOnFailure with value "true"


To use these extensions simply register the wanted extension as a bean, e.g.:
```
Expand All @@ -50,4 +58,11 @@ public WsXmlReporterExtension wsXmlReporterExtension() {
}
```

#### Report steps
Following step allows setting specific server log report extension mode
Step throws AssertionError when ServerLogXmlReporterExtension isn't registered.
```
Given ssh reporter mode is set to [TEMPLATE]
```

---
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package org.jbehavesupport.core.report;

import lombok.extern.slf4j.Slf4j;
import org.jbehave.core.annotations.Given;
import org.jbehavesupport.core.expression.ExpressionEvaluatingParameter;
import org.jbehavesupport.core.report.extension.ServerLogXmlReporterExtension;
import org.jbehavesupport.core.ssh.SshReportType;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import static org.junit.Assert.assertNotNull;

@Slf4j
@Component
public class ReportSteps {

@Autowired(required = false)
private ServerLogXmlReporterExtension serverLogXmlReporterExtension;

@Given("ssh reporter mode is set to [$mode]")
public void setSshReporterMode(ExpressionEvaluatingParameter<String> mode) {
assertNotNull("ServerLogXmlReporterExtension is not registered", serverLogXmlReporterExtension);
serverLogXmlReporterExtension.setSshReportMode(SshReportType.valueOf(mode.getValue()));
}
}
Original file line number Diff line number Diff line change
@@ -1,33 +1,115 @@
package org.jbehavesupport.core.report.extension;

import java.io.File;
import java.io.IOException;
import java.io.Writer;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.time.LocalTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import lombok.RequiredArgsConstructor;
import lombok.Setter;
import org.apache.commons.collections4.keyvalue.MultiKey;
import org.apache.commons.collections4.map.LRUMap;
import org.apache.commons.collections4.map.MultiKeyMap;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.commons.io.FileUtils;
import org.jbehave.core.annotations.AfterScenario;
import org.jbehave.core.annotations.BeforeScenario;
import org.jbehave.core.annotations.ScenarioType;
import org.jbehavesupport.core.TestContext;
import org.jbehavesupport.core.report.ReportContext;
import org.jbehavesupport.core.ssh.SshLog;
import org.jbehavesupport.core.internal.FileNameResolver;
import org.jbehavesupport.core.report.ReportRenderingPhase;
import org.jbehavesupport.core.ssh.SshHandler;
import org.jbehavesupport.core.ssh.SshReportType;
import org.jbehavesupport.core.ssh.SshTemplate;

import org.apache.commons.lang3.exception.ExceptionUtils;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.RootBeanDefinition;

@RequiredArgsConstructor
public class ServerLogXmlReporterExtension extends AbstractXmlReporterExtension {

private final TestContext testContext;
private final FileNameResolver fileNameResolver;
private final SshHandler sshHandler;
private final ConfigurableListableBeanFactory beanFactory;

private static final String SSH_XML_REPORTER_EXTENSION = "serverLog";
private static final String LOG = "log";
private static final String FILE = "file";
private static final String TEXT = "text";
private static final String SYSTEM = "system";
private static final String FAIL = "fail";
private static final String FILE_NAME_PATTERN = "LOG_%s_%s.txt";

private final ConfigurableListableBeanFactory beanFactory;
private MultiKeyMap<String, String> logContents = MultiKeyMap.multiKeyMap(new LRUMap());

@Value("${ssh.reporting.maxLogLength:10000}")
private Long maxLength;

@Setter
private SshReportType sshReportMode;

@Value("${ssh.reporting.logOnFailure:false}")

private boolean loggingOnFailure;

@Value("${web.reporting.directory:./target/reports}")
private String logFileDirectory;

@Value("${ssh.reporting.mode:CACHE}")
SshReportType defaultReportType;

@BeforeScenario
public void init() {
sshReportMode = defaultReportType;
}

@AfterScenario
public void after() {
if (sshReportMode == SshReportType.CACHE) {
sshHandler.getLogCache().forEach((key, value) -> {
MultiKey newKey = new MultiKey(key.getKey(0), key.getKey(1), key.getKey(2), "N/A", "N/A");
registerLogContent(newKey, value);
});
} else if (sshReportMode == SshReportType.FULL) {
registerLogContent(sshHandler.getTemplateLogs(getSshTemplates()));
} else if (sshReportMode == SshReportType.TEMPLATE) {
registerLogContent(sshHandler.getTemplateLogs(getReportableSshTemplates(getSshTemplates())));
}
}

@AfterScenario(uponType = ScenarioType.ANY, uponOutcome = AfterScenario.Outcome.FAILURE)
public void afterFailedScenario() {
if (sshReportMode != SshReportType.TEMPLATE && loggingOnFailure) {
registerLogContent(sshHandler.getTemplateLogs(getReportableSshTemplates(getSshTemplates())));
}
}

@Override
public ReportRenderingPhase getReportRenderingPhase() {
return ReportRenderingPhase.AFTER_STORY;
}

public void registerLogContent(MultiKeyMap<String, String> logContents) {
logContents.forEach(this::registerLogContent);
}

public void registerLogContent(MultiKey multiKey, String logContent) {
if (logContents.containsKey(multiKey)) {
logContent += logContents.get(multiKey);
}
logContents.put(multiKey, logContent);
}

@Override
public String getName() {
Expand All @@ -42,38 +124,68 @@ public Long getPriority() {

@Override
public void print(final Writer writer, final ReportContext reportContext) {
Map<String, List<SshTemplate>> sshTemplates = getSshTemplates();
for (String qualifier : sshTemplates.keySet()) {
printBegin(writer, SYSTEM, getSshQualifierAttributes(qualifier));
for (SshTemplate sshTemplate : sshTemplates.get(qualifier)) {
printBegin(writer, LOG, getSshTemplateAttributes(reportContext, sshTemplate));
try {
SshLog sshLog = sshTemplate.copyLog(reportContext.startExecutionZoned(), reportContext.endExecutionZoned());
printCData(writer, sshLog.getLogContents());
} catch (Exception e) {
printBegin(writer, FAIL);
printCData(writer, ExceptionUtils.getStackTrace(e));
printEnd(writer, FAIL);
}
printEnd(writer, LOG);
}
printContent(writer);
logContents.clear();
}

private void printContent(Writer writer) {
logContents.forEach((key, value) -> {
printBegin(writer, SYSTEM, getSshQualifierAttributes(key.getKey(0)));
printBegin(writer, LOG, getSshAttributesFromKey(key));
printLogContent(writer, key.getKey(0), value);
printEnd(writer, LOG);
printEnd(writer, SYSTEM);
});
}

private void printLogContent(Writer writer, String qualifier, String sshLog) {
if (sshLog.length() > maxLength) {
printLogFile(writer, sshLog, qualifier);
} else {
printBegin(writer, TEXT);
printCData(writer, sshLog);
printEnd(writer, TEXT);
}
}

private void printLogFile(Writer writer, String logContent, String qualifier) {
File logFile = new File(LocalTime.now().toNanoOfDay() + ".txt");
try {
prepareDirectory();
FileUtils.writeStringToFile(logFile, logContent, (String) null);
File destinationFile = fileNameResolver.resolveFilePath(FILE_NAME_PATTERN, logFileDirectory, qualifier).toFile();
FileUtils.copyFile(logFile, destinationFile);
printBegin(writer, FILE);
printString(writer, destinationFile.getName());
printEnd(writer, FILE);
} catch (IOException e) {
printBegin(writer, FAIL);
printCData(writer, ExceptionUtils.getStackTrace(e));
printEnd(writer, FAIL);
}
logFile.delete();
}

private void prepareDirectory() throws IOException {
if (!Paths.get(logFileDirectory).toFile().exists()) {
Files.createDirectory(Paths.get(logFileDirectory));
testContext.put("logFileDirectory", logFileDirectory);
}
}

private Map<String, String> getSshQualifierAttributes(String qualifier) {
Map<String, String> sshQualifierAttributes = new HashMap<>();
sshQualifierAttributes.put("id", qualifier);
sshQualifierAttributes.put(SYSTEM, qualifier);
return sshQualifierAttributes;
}

private Map<String, String> getSshTemplateAttributes(ReportContext reportContext, SshTemplate sshTemplate) {
Map<String, String> sshTemplateAttributes = new HashMap<>();
sshTemplateAttributes.put("startDate", reportContext.startExecutionZoned().toString());
sshTemplateAttributes.put("endDate", reportContext.endExecutionZoned().toString());
sshTemplateAttributes.put("host", sshTemplate.getSshSetting().getHostname() + ":" + sshTemplate.getSshSetting().getPort());
sshTemplateAttributes.put("logPath", sshTemplate.getSshSetting().getLogPath());
return sshTemplateAttributes;
private Map<String, String> getSshAttributesFromKey(MultiKey key) {
Map<String, String> sshContextAttributes = new HashMap<>();
sshContextAttributes.put("startDate", key.getKey(1).toString());
sshContextAttributes.put("endDate", key.getKey(2).toString());
sshContextAttributes.put("host", key.getKey(3).toString());
sshContextAttributes.put("logPath", key.getKey(4).toString());
return sshContextAttributes;
}

private <T> Map<String, List<T>> getSshTemplatesForType(Class<T> clazz) {
Expand All @@ -97,20 +209,26 @@ private Map<String, List<SshTemplate>> getSshTemplates() {
Map<String, List<SshTemplate>> sshTemplates = getSshTemplatesForType(SshTemplate.class);

//merge both maps together to simple Map<String, List<SshTemplate>>
sshTemplatesArray.entrySet()
.stream()
.forEach(entry -> {
List<SshTemplate> flattenedSshTemplatesArray = entry.getValue()
.stream()
.flatMap(Arrays::stream)
.collect(Collectors.toList());
if (sshTemplates.get(entry.getKey()) != null) {
sshTemplates.get(entry.getKey()).addAll(flattenedSshTemplatesArray);
} else {
sshTemplates.put(entry.getKey(), flattenedSshTemplatesArray);
}
//sshTemplatesArray.
sshTemplatesArray.forEach((qualifier, listOfArrays) -> {
List<SshTemplate> flattenedSshTemplatesArray = listOfArrays.stream()
.flatMap(Arrays::stream)
.collect(Collectors.toList());

sshTemplates.merge(qualifier, flattenedSshTemplatesArray, (firstList, secondList) -> {
secondList.addAll(firstList);
return secondList;
});
});

return sshTemplates;
}

private Map<String, List<SshTemplate>> getReportableSshTemplates(Map<String, List<SshTemplate>> sshTemplates) {
return sshTemplates.entrySet().stream()
.collect(Collectors.toMap(Map.Entry::getKey,
e -> e.getValue().stream()
.filter(SshTemplate::isReportable)
.collect(Collectors.toList())));
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package org.jbehavesupport.core.ssh;

import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.keyvalue.MultiKey;
import org.apache.commons.collections4.map.LRUMap;
import org.apache.commons.collections4.map.MultiKeyMap;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.assertj.core.api.SoftAssertions;
import org.jbehave.core.annotations.BeforeScenario;
import org.jbehave.core.model.ExamplesTable;
Expand Down Expand Up @@ -64,14 +66,14 @@ public class SshHandler {
private final ExamplesEvaluationTableConverter tableConverter;
private final VerifierResolver verifierResolver;


@Value("${ssh.max.assert.count:10}")
private int maxSoftAssertCount;

private ZonedDateTime scenarioStart;
private ZonedDateTime logStartTime;
private ZonedDateTime logEndTime;

@Getter
private MultiKeyMap<MultiKey, String> logCache = MultiKeyMap.multiKeyMap(new LRUMap());

@BeforeScenario
Expand Down Expand Up @@ -110,6 +112,7 @@ public void checkLogDataPresence(String systemQualifier, String stringTable, Ver
* @deprecated use logContainsData(String systemQualifier, String stringTable) instead
* If you set timestamps via separate steps, log reading is more accurate and use cache
*/
@Deprecated
public void checkLogDataPresence(String systemQualifier, String startTimeAlias, String stringTable, Verifier verifier) {
checkDataPresence(systemQualifier, testContext.get(startTimeAlias), stringTable, verifier);
}
Expand All @@ -122,9 +125,9 @@ public void checkLogDataPresence(String systemQualifier, String startTimeAlias,
* @param endTime log search end timestamp
*/
protected String readLog(String systemQualifier, ZonedDateTime startTime, ZonedDateTime endTime) {
if (logCache.containsKey(startTime, endTime, systemQualifier)) {
if (logCache.containsKey(systemQualifier, startTime, endTime)) {
log.info("Log found in cache.");
return logCache.get(startTime, endTime, systemQualifier);
return logCache.get(systemQualifier, startTime, endTime);
}
StringBuilder fetchedLog = new StringBuilder();
List<SshTemplate> sshTemplates = resolveSshTemplates(systemQualifier);
Expand All @@ -136,7 +139,7 @@ protected String readLog(String systemQualifier, ZonedDateTime startTime, ZonedD
log.error("error fetching {}({}) log: {}", systemQualifier, sshTemplate.getSshSetting(), ex);
}
}
logCache.put(new MultiKey(startTime, endTime, systemQualifier), fetchedLog.toString());
logCache.put(new MultiKey(systemQualifier, startTime, endTime), fetchedLog.toString());
return fetchedLog.toString();
}

Expand Down Expand Up @@ -216,4 +219,25 @@ protected String getSearchColumnName(ExamplesTable searchData) {
throw new IllegalArgumentException("No search column found in example table");
}
}

public MultiKeyMap<String, String> getTemplateLogs(Map<String, List<SshTemplate>> sshTemplates){
ZonedDateTime scenarioEnd = ZonedDateTime.now();
MultiKeyMap<String, String> templateLogs = MultiKeyMap.multiKeyMap(new LRUMap());
sshTemplates.entrySet().forEach(entry ->
entry.getValue().forEach(sshTemplate -> {
MultiKey<String> multiKey = new MultiKey(entry.getKey(),
scenarioStart,
scenarioEnd,
sshTemplate.getSshSetting().getHostname() + ":" + sshTemplate.getSshSetting().getPort(),
sshTemplate.getSshSetting().getLogPath());
try {
String sshLog = sshTemplate.copyLog(scenarioStart, scenarioEnd).getLogContents();
templateLogs.put(multiKey, sshLog);
} catch (Exception e) {
templateLogs.put(multiKey, ExceptionUtils.getStackTrace(e));
}
})
);
return templateLogs;
}
}
Loading