/* This file is part of VoltDB. * Copyright (C) 2008-2017 VoltDB Inc. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with VoltDB. If not, see <http://www.gnu.org/licenses/>. */ package org.voltdb.utils; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.nio.file.Files; import java.nio.file.Paths; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.zip.ZipEntry; import java.util.zip.ZipException; import java.util.zip.ZipOutputStream; import org.apache.log4j.Logger; import org.apache.log4j.varia.NullAppender; import org.json_voltpatches.JSONArray; import org.json_voltpatches.JSONException; import org.json_voltpatches.JSONObject; import org.voltcore.utils.CoreUtils; import org.voltdb.CLIConfig; import org.voltdb.VoltDB; import org.voltdb.common.Constants; import org.voltdb.settings.NodeSettings; import org.voltdb.types.TimestampType; import com.google_voltpatches.common.base.Throwables; public class Collector { private final static String ZIP_ENTRY_FOLDER_BASE_SUFFIX = "_volt_collect_logs_"; final static String PREFIX_DEFAULT_COLLECT_FILE = "voltdb_collect"; final static String COLLECT_FILE_EXTENSION = ".zip"; private static String m_configInfoPath = null; private static String m_catalogJarPath = null; private static String m_deploymentPath = null; private static String m_systemCheckPath = null; private static String m_pathPropertiesPath = null; private static String m_clusterPropertiesPath = null; private static CollectConfig m_config; public static long m_currentTimeMillis = System.currentTimeMillis(); private static String m_workingDir = null; private static Set<String> m_logPaths = new HashSet<String>(); private static Properties m_systemStats = new Properties(); private static VoltFile m_voltdbRoot = null; private static String m_zipFolderBase = ""; static String getZipCollectFolderBase() { return m_zipFolderBase; } public static String[] cmdFilenames = {"sardata", "dmesgdata", "syscheckdata"}; static class CollectConfig extends CLIConfig { @Option(desc = "file name prefix for uniquely identifying collection") String prefix = ""; @Option(desc = "file name prefix for uniquely identifying collection") String outputFile = ""; @Option(desc = "list the log files without collecting them") boolean dryrun = false; @Option(desc = "exclude heap dump file from collection") boolean skipheapdump = true; @Option(desc = "number of days of files to collect (files included are log, crash files), Current day value is 1") int days = 7; @Option(desc = "the voltdbroot path") String voltdbroot = ""; @Option(desc = "overwrite output file if it exists") boolean force= false; @Option String libPathForTest = ""; @Override public void validate() { if (days < 0) exitWithMessageAndUsage("days must be >= 0"); if (voltdbroot.trim().isEmpty()) exitWithMessageAndUsage("Invalid database directory"); } } private static void populateVoltDBCollectionPaths() { m_voltdbRoot = new VoltFile(m_config.voltdbroot); if (!m_voltdbRoot.exists()) { System.err.println(m_voltdbRoot.getParentFile().getAbsolutePath() + " does not contain a valid database " + "directory. Specify valid path to the database directory."); VoltDB.exit(-1); } if (!m_voltdbRoot.isDirectory()) { System.err.println(m_voltdbRoot.getParentFile().getAbsolutePath() + " is a not directory. Specify valid " + " database directory in --dir option."); VoltDB.exit(-1); } if (!m_voltdbRoot.canRead() || !m_voltdbRoot.canExecute()) { System.err.println(m_voltdbRoot.getParentFile().getAbsolutePath() + " does not have read/exceute permission."); VoltDB.exit(-1); } m_config.voltdbroot = m_voltdbRoot.getAbsolutePath(); // files to collect from config dir String configLogDirPath = m_voltdbRoot.getAbsolutePath() + File.separator + Constants.CONFIG_DIR + File.separator; m_configInfoPath = configLogDirPath + "config.json"; m_catalogJarPath = configLogDirPath + "catalog.jar"; m_deploymentPath = configLogDirPath + "deployment.xml"; m_pathPropertiesPath = configLogDirPath + "path.properties"; m_clusterPropertiesPath = configLogDirPath + "cluster.properties"; m_systemCheckPath = m_voltdbRoot.getAbsolutePath() + File.separator + "systemcheck"; // Validate voltdbroot path is valid or not - check if deployment and config info json exists File deploymentFile = new File(m_deploymentPath); File configInfoFile = new File(m_configInfoPath); if (!deploymentFile.exists() || !configInfoFile.exists()) { System.err.println("ERROR: Invalid database directory " + m_voltdbRoot.getParentFile().getAbsolutePath() + ". Specify valid database directory using --dir option."); VoltDB.exit(-1); } if (!m_config.prefix.isEmpty()) { m_config.outputFile = m_config.prefix + "_" + PREFIX_DEFAULT_COLLECT_FILE + "_" + CoreUtils.getHostnameOrAddress() + COLLECT_FILE_EXTENSION; } if (m_config.outputFile.isEmpty()) { m_config.outputFile = PREFIX_DEFAULT_COLLECT_FILE + "_" + CoreUtils.getHostnameOrAddress() + COLLECT_FILE_EXTENSION;; } File outputFile = new File(m_config.outputFile); if (outputFile.exists() && !m_config.force) { System.err.println("ERROR: Output file " + outputFile.getAbsolutePath() + " already exists." + " Use --force to overwrite an existing file."); VoltDB.exit(-1); } } public static void main(String[] args) { // get rid of log4j "no appenders could be found for logger" warning when called from VEM Logger.getRootLogger().addAppender(new NullAppender()); m_config = new CollectConfig(); m_config.parse(Collector.class.getName(), args); if (!m_config.outputFile.trim().isEmpty() && !m_config.prefix.trim().isEmpty()) { System.err.println("For outputfile, specify either --output or --prefix. Can't specify " + "both of them."); m_config.printUsage(); VoltDB.exit(-1); } populateVoltDBCollectionPaths(); JSONObject jsonObject = parseJSONFile(m_configInfoPath); parseJSONObject(jsonObject); String systemStatsPathBase; if (m_config.libPathForTest.isEmpty()) systemStatsPathBase = System.getenv("VOLTDB_LIB"); else systemStatsPathBase = m_config.libPathForTest; String systemStatsPath; if (System.getProperty("os.name").contains("Mac")) systemStatsPath = systemStatsPathBase + File.separator + "macstats.properties"; else systemStatsPath = systemStatsPathBase + File.separator + "linuxstats.properties"; try { InputStream systemStatsIS = new FileInputStream(systemStatsPath); m_systemStats.load(systemStatsIS); } catch (IOException e) { Throwables.propagate(e); } Set<String> collectionFilesList = setCollection(m_config.skipheapdump); if (m_config.dryrun) { System.out.println("List of the files to be collected:"); for (String path: collectionFilesList) { System.out.println(" " + path); } System.out.println("[dry-run] A tgz file containing above files would be generated in current dir"); System.out.println(" Use --upload option to enable uploading via SFTP"); } else { generateCollection(collectionFilesList); } } public static JSONObject parseJSONFile(String configInfoPath) { JSONObject jsonObject = null; try { BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(new FileInputStream(configInfoPath))); StringBuilder builder = new StringBuilder(); String line = null; while ((line = bufferedReader.readLine()) != null) { builder.append(line); } bufferedReader.close(); jsonObject = new JSONObject(builder.toString()); } catch (FileNotFoundException e) { System.err.println("config log file '" + configInfoPath + "' could not be found."); VoltDB.exit(-1); } catch (IOException e) { System.err.println(e.getMessage()); VoltDB.exit(-1); } catch (JSONException e) { System.err.println("Error with config file: " + configInfoPath); System.err.println(e.getLocalizedMessage()); VoltDB.exit(-1); } return jsonObject; } private static void parseJSONObject(JSONObject jsonObject) { try { m_workingDir = jsonObject.getString("workingDir"); m_logPaths.clear(); JSONArray jsonArray = jsonObject.getJSONArray("log4jDst"); for (int i = 0; i < jsonArray.length(); i++) { String path = jsonArray.getJSONObject(i).getString("path"); m_logPaths.add(path); } } catch (JSONException e) { System.err.println(e.getMessage()); } } private static String getLinuxOSInfo() { // Supported Linux OS for voltdb are CentOS, Redhat and Ubuntu String versionInfo = ""; BufferedReader br = null; // files containing the distribution info // Ubuntu - "/etc/lsb-release" // Redhat, CentOS - "/etc/redhat-release" final List<String> distInfoFilePaths = Arrays.asList("/etc/lsb-release", "/etc/redhat-release"); for (String filePath : distInfoFilePaths) { if (Files.exists(Paths.get(filePath))) { try { br = new BufferedReader(new FileReader(filePath)); } catch (FileNotFoundException excp) { System.err.println(excp.getMessage()); } break; } } if (br != null) { StringBuffer buffer = new StringBuffer(); try { while ((versionInfo = br.readLine()) != null) { buffer.append(versionInfo); } versionInfo = buffer.toString(); } catch (IOException io) { System.err.println(io.getMessage()); versionInfo = ""; } } return versionInfo; } private static Set<String> setCollection(boolean skipHeapDump) { Set<String> collectionFilesList = new HashSet<String>(); try { if (new File(m_deploymentPath).exists()) { collectionFilesList.add(m_deploymentPath); } if (new File(m_catalogJarPath).exists()) { collectionFilesList.add(m_catalogJarPath); } if (new File(m_systemCheckPath).exists()) { collectionFilesList.add(m_systemCheckPath); } if (new File(m_configInfoPath).exists()) { collectionFilesList.add(m_configInfoPath); } if (new File(m_pathPropertiesPath).exists()) { collectionFilesList.add(m_pathPropertiesPath); } if (new File(m_clusterPropertiesPath).exists()) { collectionFilesList.add(m_clusterPropertiesPath); } for (String path: m_logPaths) { for (File file: new File(path).getParentFile().listFiles()) { if (file.getName().startsWith(new File(path).getName()) && isFileModifiedInCollectionPeriod(file)) { collectionFilesList.add(file.getCanonicalPath()); } } } for (File file: new File(m_config.voltdbroot).listFiles()) { if (file.getName().startsWith("voltdb_crash") && file.getName().endsWith(".txt") && isFileModifiedInCollectionPeriod(file)) { collectionFilesList.add(file.getCanonicalPath()); } if (file.getName().startsWith("hs_err_pid") && file.getName().endsWith(".log") && isFileModifiedInCollectionPeriod(file)) { collectionFilesList.add(file.getCanonicalPath()); } } for (File file: new File(m_workingDir).listFiles()) { if (file.getName().startsWith("voltdb_crash") && file.getName().endsWith(".txt") && isFileModifiedInCollectionPeriod(file)) { collectionFilesList.add(file.getCanonicalPath()); } if (file.getName().startsWith("hs_err_pid") && file.getName().endsWith(".log") && isFileModifiedInCollectionPeriod(file)) { collectionFilesList.add(file.getCanonicalPath()); } } String systemLogBase; final String systemLogBaseDirPath = "/var/log/"; if (System.getProperty("os.name").startsWith("Mac")) { systemLogBase = "system.log"; } else { String versionInfo = getLinuxOSInfo(); if (versionInfo.contains("Ubuntu")) { systemLogBase = "syslog"; } else { systemLogBase = "messages"; if (versionInfo.isEmpty()) { System.err.println("Couldn't find distribution info for supported systems. Perform" + " lookup for system logs in files named: " + systemLogBase); } } } for (File file: new File(systemLogBaseDirPath).listFiles()) { if (file.getName().startsWith(systemLogBase)) { collectionFilesList.add(file.getCanonicalPath()); } } if (!skipHeapDump) { for (File file: new File("/tmp").listFiles()) { if (file.getName().startsWith("java_pid") && file.getName().endsWith(".hprof") && isFileModifiedInCollectionPeriod(file)) { collectionFilesList.add(file.getCanonicalPath()); } } } collectionFilesList.add("duvoltdbrootdata (result of executing \"du -h <voltdbroot>\")"); collectionFilesList.add("dudroverflowdata (result of executing \"du -h <droverflow>\")"); collectionFilesList.add("duexportoverflowdata (result of executing \"du -h <exportoverflow>\")"); collectionFilesList.add("ducommandlog (result of executing \"du -h <command_log>\")"); collectionFilesList.add("ducommandlogsnapshot (result of executing \"du -h <command_log_snapshot>\")"); for (String fileName : m_systemStats.stringPropertyNames()) { collectionFilesList.add(fileName + " (result of executing \"" + m_systemStats.getProperty(fileName) + "\")"); } File varlogDir = new File("/var/log"); if (varlogDir.canRead()) { for (File file: varlogDir.listFiles()) { if ((file.getName().startsWith("syslog") || file.getName().equals("dmesg")) && isFileModifiedInCollectionPeriod(file)) { if (file.canRead()) { collectionFilesList.add(file.getCanonicalPath()); } } } } } catch (IOException e) { System.err.println(e.getMessage()); } return collectionFilesList; } //checks whether the file is in the duration(days) specified by the user //value of diff = 0 indicates current day private static boolean isFileModifiedInCollectionPeriod(File file){ long diff = m_currentTimeMillis - file.lastModified(); if(diff >= 0) { return TimeUnit.MILLISECONDS.toDays(diff)+1 <= m_config.days; } return false; } public static void resetCurrentDay() { m_currentTimeMillis = System.currentTimeMillis(); } private static void generateCollection(Set<String> paths) { try { TimestampType ts = new TimestampType(new java.util.Date()); String timestamp = ts.toString().replace(' ', '-').replace(':', '-'); // get rid of microsecond part timestamp = timestamp.substring(0, "YYYY-mm-DD-HH-MM-ss".length()); m_zipFolderBase = (m_config.prefix.isEmpty() ? "" : m_config.prefix + "_") + CoreUtils.getHostnameOrAddress() + ZIP_ENTRY_FOLDER_BASE_SUFFIX + timestamp; String folderPath = m_zipFolderBase + File.separator; String collectionFilePath = m_config.outputFile; FileOutputStream collectionStream = new FileOutputStream(collectionFilePath); ZipOutputStream zipStream = new ZipOutputStream(collectionStream); Map<String, Integer> pathCounter = new HashMap<String, Integer>(); // Collect files with paths indicated in the list for (String path: paths) { // Skip particular items corresponding to temporary files that are only generated during collecting if (Arrays.asList(cmdFilenames).contains(path.split(" ")[0])) { continue; } File file = new File(path); String filename = file.getName(); String entryPath = file.getName(); for (String logPath: m_logPaths) { if (filename.startsWith(new File(logPath).getName())) { entryPath = "voltdb_logs" + File.separator + file.getName(); break; } } if (filename.startsWith("voltdb_crash")) { entryPath = "voltdb_crashfiles" + File.separator + file.getName(); } if (filename.startsWith("syslog") || filename.equals("dmesg") || filename.equals("systemcheck") || filename.startsWith("hs_err_pid") || path.startsWith("/var/log/")) { entryPath = "system_logs" + File.separator + file.getName(); } if (filename.equals("deployment.xml") || filename.equals("catalog.jar") || filename.equals("config.json") || filename.equals("path.properties") || filename.equals("cluster.properties")) { entryPath = "voltdb_files" + File.separator + file.getName(); } if (filename.endsWith(".hprof")) { entryPath = "heap_dumps" + File.separator + file.getName(); } if (file.isFile() && file.canRead() && file.length() > 0) { String zipPath = folderPath + entryPath; System.out.println(zipPath + "..."); if (pathCounter.containsKey(zipPath)) { Integer pathCount = pathCounter.get(zipPath); pathCounter.put(zipPath, pathCount + 1); zipPath = zipPath.concat("(" + pathCount.toString() + ")"); } else { pathCounter.put(zipPath, 1); } ZipEntry zEntry= new ZipEntry(zipPath); zipStream.putNextEntry(zEntry); FileInputStream in = new FileInputStream(path); int len; byte[] buffer = new byte[1024]; while ((len = in.read(buffer)) > 0) { zipStream.write(buffer, 0, len); } in.close(); zipStream.closeEntry(); } } String duCommand = m_systemStats.getProperty("dudata"); if (duCommand != null) { String[] duVoltdbrootCmd = {"bash", "-c", duCommand + " " + m_config.voltdbroot}; cmd(zipStream, duVoltdbrootCmd, folderPath + "system_logs" + File.separator, "duvoltdbrootdata"); String drOverflowPath = m_config.voltdbroot + File.separator + "dr_overflow"; String exportOverflowPath = m_config.voltdbroot + File.separator + "export_overflow"; String commandLogPath = m_config.voltdbroot + File.separator + "command_log"; String commandLogSnapshotPath = m_config.voltdbroot + File.separator + "command_log_snapshot"; if (m_pathPropertiesPath != null && !m_pathPropertiesPath.trim().isEmpty() && (new File(m_pathPropertiesPath)).exists()) { Properties prop = new Properties(); InputStream input = null; try { input = new FileInputStream(m_pathPropertiesPath); prop.load(input); } catch (IOException excp) { System.err.println(excp.getMessage()); } if (!prop.isEmpty()) { String cmdLogPropPath = prop.getProperty(NodeSettings.CL_PATH_KEY, null); if (cmdLogPropPath != null && !cmdLogPropPath.trim().isEmpty()) { commandLogPath = cmdLogPropPath; } String cmdLogSnapshotPropPath = prop.getProperty(NodeSettings.CL_SNAPSHOT_PATH_KEY, null); if (cmdLogSnapshotPropPath != null && !cmdLogSnapshotPropPath.trim().isEmpty()) { commandLogSnapshotPath = cmdLogSnapshotPropPath; } String drOverflowPropPath = prop.getProperty(NodeSettings.DR_OVERFLOW_PATH_KEY, null); if (drOverflowPropPath != null && !drOverflowPropPath.trim().isEmpty()) { drOverflowPath = drOverflowPropPath; } String exportOverflowPropPath = prop.getProperty(NodeSettings.EXPORT_OVERFLOW_PATH_KEY, null); if (exportOverflowPropPath != null && !exportOverflowPropPath.trim().isEmpty()) { exportOverflowPath = exportOverflowPropPath; } } } String[] duDrOverflowCmd = {"bash", "-c", duCommand + " " + drOverflowPath}; cmd(zipStream, duDrOverflowCmd, folderPath + "system_logs" + File.separator, "dudroverflowdata"); String[] duExportOverflowCmd = {"bash", "-c", duCommand + " " + exportOverflowPath}; cmd(zipStream, duExportOverflowCmd, folderPath + "system_logs" + File.separator, "duexportoverflowdata"); String[] duCommadLodCmd = {"bash", "-c", duCommand + " " + commandLogPath}; cmd(zipStream, duCommadLodCmd, folderPath + "system_logs" + File.separator, "ducommandlog"); String[] commandLogSnapshotCmd = {"bash", "-c", duCommand + " " + commandLogSnapshotPath}; cmd(zipStream, commandLogSnapshotCmd, folderPath + "system_logs" + File.separator, "ducommandlogsnapshot"); } for (String fileName : m_systemStats.stringPropertyNames()) { String[] statsCmd = {"bash", "-c", m_systemStats.getProperty(fileName)}; cmd(zipStream, statsCmd, folderPath + "system_logs" + File.separator, fileName); } zipStream.close(); long sizeInByte = new File(collectionFilePath).length(); String sizeStringInKB = String.format("%5.2f", (double)sizeInByte / 1000); System.out.println("\nCollection file created in " + collectionFilePath + "; file size: " + sizeStringInKB + " KB"); } catch (Exception e) { System.err.println(e.getMessage()); } } private static void cmd(ZipOutputStream zipStream, String[] command, String folderPath, String resFilename) throws IOException, ZipException { System.out.println(folderPath + resFilename + "..."); File tempFile = File.createTempFile(resFilename, null); tempFile.deleteOnExit(); Process p = Runtime.getRuntime().exec(command); BufferedReader reader = new BufferedReader(new InputStreamReader(p.getInputStream())); BufferedReader ereader = new BufferedReader(new InputStreamReader(p.getErrorStream())); BufferedWriter writer = new BufferedWriter(new FileWriter(tempFile)); String line = null; while ((line = reader.readLine()) != null) { writer.write(line); writer.newLine(); } // If we dont have anything in stdout look in stderr. if (tempFile.length() <= 0) { if (ereader != null) { while ((line = ereader.readLine()) != null) { writer.write(line); writer.newLine(); } } } writer.close(); if (tempFile.length() > 0) { ZipEntry zEntry= new ZipEntry(folderPath + resFilename); zipStream.putNextEntry(zEntry); FileInputStream in = new FileInputStream(tempFile); int len; byte[] buffer = new byte[1024]; while ((len = in.read(buffer)) > 0) { zipStream.write(buffer, 0, len); } in.close(); zipStream.closeEntry(); } } }