| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more contributor license |
| * agreements. See the NOTICE file distributed with this work for additional information regarding |
| * copyright ownership. The ASF licenses this file to You under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance with the License. You may obtain a |
| * copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software distributed under the License |
| * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express |
| * or implied. See the License for the specific language governing permissions and limitations under |
| * the License. |
| * |
| */ |
| package org.apache.geode.management.internal.cli.commands; |
| |
| import static java.util.stream.Collectors.joining; |
| import static java.util.stream.Collectors.toList; |
| import static java.util.stream.Collectors.toSet; |
| import static org.apache.commons.io.FileUtils.listFiles; |
| import static org.apache.geode.management.internal.cli.commands.ExportLogsCommand.FORMAT; |
| import static org.apache.geode.management.internal.cli.commands.ExportLogsCommand.ONLY_DATE_FORMAT; |
| import static org.assertj.core.api.Assertions.assertThat; |
| |
| import java.io.File; |
| import java.io.IOException; |
| import java.io.Serializable; |
| import java.nio.charset.Charset; |
| import java.nio.file.Path; |
| import java.time.LocalDateTime; |
| import java.time.ZoneId; |
| import java.time.ZonedDateTime; |
| import java.time.format.DateTimeFormatter; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Objects; |
| import java.util.Properties; |
| import java.util.Set; |
| import java.util.stream.Stream; |
| |
| import org.apache.commons.io.FileUtils; |
| import org.apache.logging.log4j.Logger; |
| import org.junit.After; |
| import org.junit.Before; |
| import org.junit.Rule; |
| import org.junit.Test; |
| import org.junit.rules.TemporaryFolder; |
| |
| import org.apache.geode.cache.Cache; |
| import org.apache.geode.distributed.ConfigurationProperties; |
| import org.apache.geode.internal.cache.GemFireCacheImpl; |
| import org.apache.geode.logging.internal.log4j.api.LogService; |
| import org.apache.geode.management.internal.cli.functions.ExportLogsFunction; |
| import org.apache.geode.management.internal.cli.util.CommandStringBuilder; |
| import org.apache.geode.management.internal.configuration.utils.ZipUtils; |
| import org.apache.geode.test.dunit.IgnoredException; |
| import org.apache.geode.test.dunit.rules.ClusterStartupRule; |
| import org.apache.geode.test.dunit.rules.MemberVM; |
| import org.apache.geode.test.junit.rules.GfshCommandRule; |
| import org.apache.geode.test.junit.rules.Member; |
| |
| public class ExportLogsDistributedTestBase { |
| |
| private static final String ERROR_LOG_PREFIX = "[IGNORE]"; |
| |
| @Rule |
| public ClusterStartupRule lsRule = new ClusterStartupRule().withLogFile(); |
| |
| @Rule |
| public GfshCommandRule gfshConnector = new GfshCommandRule(); |
| |
| @Rule |
| public TemporaryFolder temporaryFolder = new TemporaryFolder(); |
| |
| protected MemberVM locator; |
| private MemberVM server1; |
| private MemberVM server2; |
| |
| private Map<MemberVM, List<LogLine>> expectedMessages; |
| |
| public File getWorkingDirectory() throws Exception { |
| return locator.getWorkingDir(); |
| } |
| |
| @Before |
| public void setup() throws Exception { |
| Properties properties = new Properties(); |
| properties.setProperty(ConfigurationProperties.LOG_LEVEL, "debug"); |
| |
| locator = lsRule.startLocatorVM(0, l -> l.withProperties(properties).withHttpService()); |
| server1 = lsRule.startServerVM(1, properties, locator.getPort()); |
| server2 = lsRule.startServerVM(2, properties, locator.getPort()); |
| |
| IgnoredException.addIgnoredException(ERROR_LOG_PREFIX); |
| |
| expectedMessages = new HashMap<>(); |
| expectedMessages.put(locator, listOfLogLines(locator, "info", "error", "debug")); |
| expectedMessages.put(server1, listOfLogLines(server1, "info", "error", "debug")); |
| expectedMessages.put(server2, listOfLogLines(server2, "info", "error", "debug")); |
| |
| // log the messages in each of the members |
| for (MemberVM member : expectedMessages.keySet()) { |
| List<LogLine> logLines = expectedMessages.get(member); |
| |
| member.invoke(() -> { |
| Logger logger = LogService.getLogger(); |
| logLines.forEach((LogLine logLine) -> logLine.writeLog(logger)); |
| }); |
| } |
| |
| connect(); |
| } |
| |
| public void connect() throws Exception { |
| gfshConnector.connectAndVerify(locator); |
| } |
| |
| @After |
| public void after() throws Exception { |
| Stream.of(getWorkingDirectory().listFiles()) |
| .filter(f -> f.getName().endsWith(".zip")).forEach(file -> file.delete()); |
| } |
| |
| @Test |
| public void withFiles_savedToLocatorWorkingDir() throws Exception { |
| String[] extensions = {"zip"}; |
| // Expects locator to produce file in own working directory when connected via JMX |
| gfshConnector.executeCommand("export logs"); |
| assertThat(listFiles(getWorkingDirectory(), extensions, false)).isNotEmpty(); |
| } |
| |
| @Test |
| public void withFiles_savedToLocatorSpecifiedRelativeDir() throws Exception { |
| String[] extensions = {"zip"}; |
| Path workingDirPath = getWorkingDirectory().toPath(); |
| Path subdirPath = workingDirPath.resolve("some").resolve("test").resolve("directory"); |
| Path relativeDir = workingDirPath.relativize(subdirPath); |
| // Expects locator to produce file in own working directory when connected via JMX |
| gfshConnector.executeCommand("export logs --dir=" + relativeDir.toString()); |
| assertThat(listFiles(getWorkingDirectory(), extensions, false)).isEmpty(); |
| assertThat(listFiles(getWorkingDirectory(), extensions, true)).isNotEmpty(); |
| assertThat(listFiles(subdirPath.toFile(), extensions, false)).isNotEmpty(); |
| } |
| |
| @Test |
| public void withFiles_savedToLocatorSpecifiedAbsoluteDir() throws Exception { |
| String[] extensions = {"zip"}; |
| Path workingDirPath = getWorkingDirectory().toPath(); |
| Path absoluteDirPath = |
| workingDirPath.resolve("some").resolve("test").resolve("directory").toAbsolutePath(); |
| // Expects locator to produce file in own working directory when connected via JMX |
| gfshConnector.executeCommand("export logs --dir=" + absoluteDirPath.toString()); |
| assertThat(listFiles(getWorkingDirectory(), extensions, false)).isEmpty(); |
| assertThat(listFiles(getWorkingDirectory(), extensions, true)).isNotEmpty(); |
| assertThat(listFiles(absoluteDirPath.toFile(), extensions, false)).isNotEmpty(); |
| } |
| |
| @Test |
| public void startAndEndDateCanIncludeLogs() throws Exception { |
| ZonedDateTime now = LocalDateTime.now().atZone(ZoneId.systemDefault()); |
| ZonedDateTime yesterday = now.minusDays(1); |
| ZonedDateTime tomorrow = now.plusDays(1); |
| |
| DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(ONLY_DATE_FORMAT); |
| |
| CommandStringBuilder commandStringBuilder = new CommandStringBuilder("export logs"); |
| commandStringBuilder.addOption("start-time", dateTimeFormatter.format(yesterday)); |
| commandStringBuilder.addOption("end-time", dateTimeFormatter.format(tomorrow)); |
| commandStringBuilder.addOption("log-level", "debug"); |
| |
| gfshConnector.executeAndAssertThat(commandStringBuilder.toString()).statusIsSuccess(); |
| |
| Set<String> acceptedLogLevels = Stream.of("info", "error", "debug").collect(toSet()); |
| verifyZipFileContents(acceptedLogLevels); |
| } |
| |
| @Test |
| public void testExportWithStartAndEndDateTimeFiltering() throws Exception { |
| ZonedDateTime cutoffTime = LocalDateTime.now().atZone(ZoneId.systemDefault()); |
| |
| // wait for atleast 1 second to reduce flakiness on windows |
| // on windows the flakiness is caused due to the cutoffTime |
| // being same as the log message logged on server1. |
| Thread.sleep(1); |
| |
| String messageAfterCutoffTime = |
| "[this message should not show up since it is after cutoffTime]"; |
| LogLine logLineAfterCutoffTime = new LogLine(messageAfterCutoffTime, "info", true); |
| server1.invoke(() -> { |
| Logger logger = LogService.getLogger(); |
| logLineAfterCutoffTime.writeLog(logger); |
| }); |
| |
| DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(FORMAT); |
| String cutoffTimeString = dateTimeFormatter.format(cutoffTime); |
| |
| CommandStringBuilder commandStringBuilder = new CommandStringBuilder("export logs"); |
| commandStringBuilder.addOption("start-time", dateTimeFormatter.format(cutoffTime.minusDays(1))); |
| commandStringBuilder.addOption("end-time", cutoffTimeString); |
| commandStringBuilder.addOption("log-level", "debug"); |
| |
| gfshConnector.executeAndAssertThat(commandStringBuilder.toString()).statusIsSuccess(); |
| |
| expectedMessages.get(server1).add(logLineAfterCutoffTime); |
| Set<String> acceptedLogLevels = Stream.of("info", "error", "debug").collect(toSet()); |
| verifyZipFileContents(acceptedLogLevels); |
| } |
| |
| @Test |
| public void testExportWithThresholdLogLevelFilter() throws Exception { |
| |
| gfshConnector.executeAndAssertThat("export logs --log-level=info --only-log-level=false") |
| .statusIsSuccess(); |
| |
| Set<String> acceptedLogLevels = Stream.of("info", "error").collect(toSet()); |
| verifyZipFileContents(acceptedLogLevels); |
| |
| } |
| |
| @Test |
| public void testExportWithExactLogLevelFilter() throws Exception { |
| gfshConnector.executeAndAssertThat("export logs --log-level=info --only-log-level=true") |
| .statusIsSuccess(); |
| Set<String> acceptedLogLevels = Stream.of("info").collect(toSet()); |
| verifyZipFileContents(acceptedLogLevels); |
| } |
| |
| @Test |
| public void testExportWithNoOptionsGiven() throws Exception { |
| gfshConnector.executeAndAssertThat("export logs").statusIsSuccess(); |
| Set<String> acceptedLogLevels = Stream.of("info", "error", "debug").collect(toSet()); |
| verifyZipFileContents(acceptedLogLevels); |
| } |
| |
| @Test |
| public void testExportWithNoFilters() throws Exception { |
| gfshConnector.executeAndAssertThat("export logs --log-level=all").statusIsSuccess(); |
| |
| Set<String> acceptedLogLevels = Stream.of("info", "error", "debug").collect(toSet()); |
| verifyZipFileContents(acceptedLogLevels); |
| |
| // Ensure export logs region gets cleaned up |
| server1.invoke(ExportLogsDistributedTestBase::verifyExportLogsRegionWasDestroyed); |
| server2.invoke(ExportLogsDistributedTestBase::verifyExportLogsRegionWasDestroyed); |
| locator.invoke(ExportLogsDistributedTestBase::verifyExportLogsRegionWasDestroyed); |
| } |
| |
| @Test |
| public void exportLogsRegionIsCleanedUpProperly() throws IOException, ClassNotFoundException { |
| locator.invoke(() -> { |
| GemFireCacheImpl cache = GemFireCacheImpl.getInstance(); |
| ExportLogsFunction.createOrGetExistingExportLogsRegion(true, cache); |
| assertThat(cache.getRegion(ExportLogsFunction.EXPORT_LOGS_REGION)).isNotNull(); |
| }); |
| |
| server1.invoke(() -> { |
| GemFireCacheImpl cache = GemFireCacheImpl.getInstance(); |
| ExportLogsFunction.createOrGetExistingExportLogsRegion(false, cache); |
| assertThat(cache.getRegion(ExportLogsFunction.EXPORT_LOGS_REGION)).isNotNull(); |
| }); |
| |
| locator.invoke(() -> { |
| GemFireCacheImpl cache = GemFireCacheImpl.getInstance(); |
| ExportLogsFunction.destroyExportLogsRegion(cache); |
| |
| assertThat(cache.getRegion(ExportLogsFunction.EXPORT_LOGS_REGION)).isNull(); |
| }); |
| |
| server1.invoke(() -> { |
| Cache cache = GemFireCacheImpl.getInstance(); |
| assertThat(cache.getRegion(ExportLogsFunction.EXPORT_LOGS_REGION)).isNull(); |
| }); |
| } |
| |
| |
| private void verifyZipFileContents(Set<String> acceptedLogLevels) throws Exception { |
| File unzippedLogFileDir = unzipExportedLogs(); |
| |
| Set<File> dirsFromZipFile = |
| Stream.of(unzippedLogFileDir.listFiles()).filter(File::isDirectory).collect(toSet()); |
| assertThat(dirsFromZipFile).hasSize(expectedMessages.keySet().size()); |
| |
| Set<String> expectedDirNames = |
| expectedMessages.keySet().stream().map(Member::getName).collect(toSet()); |
| Set<String> actualDirNames = dirsFromZipFile.stream().map(File::getName).collect(toSet()); |
| assertThat(actualDirNames).isEqualTo(expectedDirNames); |
| |
| System.out.println("Unzipped artifacts:"); |
| for (File dir : dirsFromZipFile) { |
| verifyLogFileContents(acceptedLogLevels, dir); |
| } |
| } |
| |
| private void verifyLogFileContents(Set<String> acceptedLogLevels, File dirForMember) |
| throws IOException { |
| |
| String memberName = dirForMember.getName(); |
| MemberVM member = expectedMessages.keySet().stream() |
| .filter((Member aMember) -> aMember.getName().equals(memberName)).findFirst().get(); |
| |
| assertThat(member).isNotNull(); |
| |
| Set<String> fileNamesInDir = |
| Stream.of(dirForMember.listFiles()).map(File::getName).collect(toSet()); |
| |
| System.out.println(dirForMember.getCanonicalPath() + " : " + fileNamesInDir); |
| |
| File logFileForMember = new File(dirForMember, memberName + ".log"); |
| assertThat(logFileForMember).exists(); |
| |
| String logFileContents = FileUtils.readLines(logFileForMember, Charset.defaultCharset()) |
| .stream().collect(joining("\n")); |
| |
| for (LogLine logLine : expectedMessages.get(member)) { |
| boolean shouldExpectLogLine = |
| acceptedLogLevels.contains(logLine.level) && !logLine.shouldBeIgnoredDueToTimestamp; |
| |
| if (shouldExpectLogLine) { |
| assertThat(logFileContents).contains(logLine.getMessage()); |
| } else { |
| assertThat(logFileContents).doesNotContain(logLine.getMessage()); |
| } |
| } |
| |
| } |
| |
| private File unzipExportedLogs() throws Exception { |
| File locatorWorkingDir = getWorkingDirectory(); |
| List<File> filesInDir = Stream.of(getWorkingDirectory().listFiles()).collect(toList()); |
| assertThat(filesInDir).isNotEmpty(); |
| |
| |
| List<File> zipFilesInDir = Stream.of(getWorkingDirectory().listFiles()) |
| .filter(f -> f.getName().endsWith(".zip")).collect(toList()); |
| assertThat(zipFilesInDir) |
| .describedAs(filesInDir.stream().map(File::getAbsolutePath).collect(joining(","))) |
| .hasSize(1); |
| |
| File unzippedLogFileDir = temporaryFolder.newFolder("unzippedLogs"); |
| ZipUtils.unzip(zipFilesInDir.get(0).getCanonicalPath(), unzippedLogFileDir.getCanonicalPath()); |
| return unzippedLogFileDir; |
| } |
| |
| private List<LogLine> listOfLogLines(Member member, String... levels) { |
| return Stream.of(levels).map(level -> new LogLine(member, level)).collect(toList()); |
| } |
| |
| private static void verifyExportLogsRegionWasDestroyed() { |
| Cache cache = GemFireCacheImpl.getInstance(); |
| assertThat(cache.getRegion(ExportLogsFunction.EXPORT_LOGS_REGION)).isNull(); |
| } |
| |
| public static class LogLine implements Serializable { |
| String level; |
| String message; |
| boolean shouldBeIgnoredDueToTimestamp; |
| |
| LogLine(String message, String level, boolean shouldBeIgnoredDueToTimestamp) { |
| this.message = message; |
| this.level = level; |
| this.shouldBeIgnoredDueToTimestamp = shouldBeIgnoredDueToTimestamp; |
| } |
| |
| LogLine(Member member, String level) { |
| this.level = level; |
| this.message = buildMessage(member.getName()); |
| } |
| |
| public String getMessage() { |
| return message; |
| } |
| |
| private String buildMessage(String memberName) { |
| StringBuilder stringBuilder = new StringBuilder(); |
| if (Objects.equals(level, "error")) { |
| stringBuilder.append(ERROR_LOG_PREFIX + "-"); |
| } |
| stringBuilder.append(level).append("-"); |
| |
| return stringBuilder.append(memberName).toString(); |
| } |
| |
| |
| void writeLog(Logger logger) { |
| switch (this.level) { |
| case "info": |
| logger.info(getMessage()); |
| break; |
| case "error": |
| logger.error(getMessage()); |
| break; |
| case "debug": |
| logger.debug(getMessage()); |
| } |
| } |
| } |
| } |