You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@lucene.apache.org by ja...@apache.org on 2016/10/20 13:25:58 UTC

lucene-solr:branch_6x: SOLR-99570: Various log tidying at Solr startup

Repository: lucene-solr
Updated Branches:
  refs/heads/branch_6x 22b0d5f4d -> ed3b268d6


SOLR-99570: Various log tidying at Solr startup

(cherry picked from commit 9776196)


Project: http://git-wip-us.apache.org/repos/asf/lucene-solr/repo
Commit: http://git-wip-us.apache.org/repos/asf/lucene-solr/commit/ed3b268d
Tree: http://git-wip-us.apache.org/repos/asf/lucene-solr/tree/ed3b268d
Diff: http://git-wip-us.apache.org/repos/asf/lucene-solr/diff/ed3b268d

Branch: refs/heads/branch_6x
Commit: ed3b268d62f23e212c410cb35aa4318afa088f55
Parents: 22b0d5f
Author: Jan H�ydahl <ja...@apache.org>
Authored: Thu Oct 20 14:47:32 2016 +0200
Committer: Jan H�ydahl <ja...@apache.org>
Committed: Thu Oct 20 15:14:27 2016 +0200

----------------------------------------------------------------------
 solr/CHANGES.txt                                |  15 +-
 solr/bin/solr                                   |  18 +-
 solr/bin/solr.cmd                               |  28 +-
 .../src/java/org/apache/solr/util/SolrCLI.java  | 271 ++++++++++++++++++-
 .../org/apache/solr/util/UtilsToolTest.java     | 185 +++++++++++++
 5 files changed, 479 insertions(+), 38 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/ed3b268d/solr/CHANGES.txt
----------------------------------------------------------------------
diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt
index 4e68c55..1b1c9cb 100644
--- a/solr/CHANGES.txt
+++ b/solr/CHANGES.txt
@@ -42,8 +42,12 @@ Upgrade Notes
   prefix, then you will now get an error as these options are incompatible with numeric faceting.
 
 * Solr's logging verbosity at the INFO level has been greatly reduced, and
-  you may need to update the log configs to use the DEBUG level to get the
-  same logging messages as before.
+  you may need to update the log configs to use the DEBUG level to see all the
+  logging messages you used to see at INFO level before.
+
+* We are no longer backing up solr.log and solr_gc.log files in date-stamped copies forever. If you relied on
+  the solr_log_<date> or solr_gc_log_<date> being in the logs folder that will no longer be the case. 
+  See SOLR-9570 for details.
 
 * The create/deleteCollection methods on MiniSolrCloudCluster have been
   deprecated.  Clients should instead use the CollectionAdminRequest API.  In
@@ -272,6 +276,13 @@ Other Changes
 * SOLR-7850: Moved defaults within bin/solr.in.sh (and bin/solr.in.cmd on Windows) to bin/solr (and bin/solr.cmd)
   such that the default state of these files is to set nothing. This makes Solr work better with Docker. (David Smiley)
 
+* SOLR-9570: Various log tidying now happens at Solr startup:
+  Old solr_log_<date> and solr_gc_log_<date> files are removed, avoiding disks to fill up,
+  solr.log.X files are rotated, preserving solr.log from last run in solr.log.1, solr.log.1 => solr.log.2 etc
+  solr-*-console.log files are moved into $SOLR_LOGS_DIR/archived/ instead of being overwritten
+  Last JVM garbage collection log solr_gc.log is moved into $SOLR_LOGS_DIR/archived/
+  (janhoy)  
+
 ==================  6.2.1 ==================
 
 Bug Fixes

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/ed3b268d/solr/bin/solr
----------------------------------------------------------------------
diff --git a/solr/bin/solr b/solr/bin/solr
index df6b4d0..6aa5709 100755
--- a/solr/bin/solr
+++ b/solr/bin/solr
@@ -1387,20 +1387,10 @@ if [ ! -e "$SOLR_HOME" ]; then
   exit 1
 fi
 
-# backup the log files before starting
-if [ -f "$SOLR_LOGS_DIR/solr.log" ]; then
-  if $verbose ; then
-    echo "Backing up $SOLR_LOGS_DIR/solr.log"
-  fi
-  mv "$SOLR_LOGS_DIR/solr.log" "$SOLR_LOGS_DIR/solr_log_$(date +"%Y%m%d_%H%M")"
-fi
-
-if [ -f "$SOLR_LOGS_DIR/solr_gc.log" ]; then
-  if $verbose ; then
-    echo "Backing up $SOLR_LOGS_DIR/solr_gc.log"
-  fi
-  mv "$SOLR_LOGS_DIR/solr_gc.log" "$SOLR_LOGS_DIR/solr_gc_log_$(date +"%Y%m%d_%H%M")"
-fi
+run_tool utils -s "$DEFAULT_SERVER_DIR" -l "$SOLR_LOGS_DIR" -remove_old_solr_logs 7 || echo "Failed removing old solr logs"
+run_tool utils -s "$DEFAULT_SERVER_DIR" -l "$SOLR_LOGS_DIR" -archive_gc_logs        || echo "Failed archiving old GC logs"
+run_tool utils -s "$DEFAULT_SERVER_DIR" -l "$SOLR_LOGS_DIR" -archive_console_logs   || echo "Failed archiving old console logs"
+run_tool utils -s "$DEFAULT_SERVER_DIR" -l "$SOLR_LOGS_DIR" -rotate_solr_logs 9     || echo "Failed rotating old solr logs"
 
 java_ver_out=`echo "$("$JAVA" -version 2>&1)"`
 JAVA_VERSION=`echo $java_ver_out | grep "java version" | awk '{ print substr($3, 2, length($3)-2); }'`

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/ed3b268d/solr/bin/solr.cmd
----------------------------------------------------------------------
diff --git a/solr/bin/solr.cmd b/solr/bin/solr.cmd
index 10ea6d6..317a789 100644
--- a/solr/bin/solr.cmd
+++ b/solr/bin/solr.cmd
@@ -860,19 +860,11 @@ IF ERRORLEVEL 1 (
   set IS_64bit=true
 )
 
-REM backup log files (use current timestamp for backup name)
-For /f "tokens=2-4 delims=/ " %%a in ('date /t') do (set mydate=%%c-%%a-%%b)
-For /f "tokens=1-2 delims=/:" %%a in ("%TIME%") do (set mytime=%%a%%b)
-set now_ts=!mydate!_!mytime!
-IF EXIST "!SOLR_LOGS_DIR!\solr.log" (
-  echo Backing up !SOLR_LOGS_DIR!\solr.log
-  move /Y "!SOLR_LOGS_DIR!\solr.log" "!SOLR_LOGS_DIR!\solr_log_!now_ts!"
-)
-
-IF EXIST "!SOLR_LOGS_DIR!\solr_gc.log" (
-  echo Backing up !SOLR_LOGS_DIR!\solr_gc.log
-  move /Y "!SOLR_LOGS_DIR!\solr_gc.log" "!SOLR_LOGS_DIR!\solr_gc_log_!now_ts!"
-)
+REM Clean up and rotate logs
+call :run_utils "-remove_old_solr_logs 7" || echo "Failed removing old solr logs"
+call :run_utils "-archive_gc_logs"        || echo "Failed archiving old GC logs"
+call :run_utils "-archive_console_logs"   || echo "Failed archiving old console logs"
+call :run_utils "-rotate_solr_logs 9"     || echo "Failed rotating old solr logs"
 
 IF NOT "%ZK_HOST%"=="" set SOLR_MODE=solrcloud
 
@@ -1136,6 +1128,16 @@ goto done
   org.apache.solr.util.SolrCLI version
 goto done
 
+:run_utils
+set "TOOL_CMD=%~1"
+"%JAVA%" %SOLR_SSL_OPTS% %SOLR_ZK_CREDS_AND_ACLS% -Dsolr.install.dir="%SOLR_TIP%" -Dlog4j.configuration="file:%DEFAULT_SERVER_DIR%\scripts\cloud-scripts\log4j.properties" ^
+  -classpath "%DEFAULT_SERVER_DIR%\solr-webapp\webapp\WEB-INF\lib\*;%DEFAULT_SERVER_DIR%\lib\ext\*" ^
+  org.apache.solr.util.SolrCLI utils -s "%DEFAULT_SERVER_DIR%" -l "%SOLR_LOGS_DIR%" %TOOL_CMD%
+if errorlevel 1 (
+   exit /b 1
+)
+goto done
+
 :parse_create_args
 IF [%1]==[] goto run_create
 IF "%1"=="-c" goto set_create_name

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/ed3b268d/solr/core/src/java/org/apache/solr/util/SolrCLI.java
----------------------------------------------------------------------
diff --git a/solr/core/src/java/org/apache/solr/util/SolrCLI.java b/solr/core/src/java/org/apache/solr/util/SolrCLI.java
index a60620e..351638f 100644
--- a/solr/core/src/java/org/apache/solr/util/SolrCLI.java
+++ b/solr/core/src/java/org/apache/solr/util/SolrCLI.java
@@ -30,7 +30,10 @@ import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
+import java.nio.file.StandardCopyOption;
 import java.nio.file.attribute.FileOwnerAttributeView;
+import java.time.Instant;
+import java.time.Period;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
@@ -45,6 +48,8 @@ import java.util.Set;
 import java.util.TreeSet;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
 import java.util.zip.ZipEntry;
 import java.util.zip.ZipInputStream;
 
@@ -114,7 +119,6 @@ import static org.apache.solr.common.params.CommonParams.NAME;
  * Command-line utility for working with Solr.
  */
 public class SolrCLI {
-
   /**
    * Defines the interface to a Solr tool that can be run from this command-line app.
    */
@@ -235,7 +239,6 @@ public class SolrCLI {
   };
 
   private static void exit(int exitStatus) {
-    // TODO: determine if we're running in a test and don't exit
     try {
       System.exit(exitStatus);
     } catch (java.lang.SecurityException secExc) {
@@ -261,6 +264,17 @@ public class SolrCLI {
       exit(0);
     }
 
+    Tool tool = findTool(args);
+    CommandLine cli = parseCmdLine(args, tool.getOptions());
+    System.exit(tool.runTool(cli));
+  }
+
+  public static Tool findTool(String[] args) throws Exception {
+    String toolType = args[0].trim().toLowerCase(Locale.ROOT);
+    return newTool(toolType);
+  }
+
+  public static CommandLine parseCmdLine(String[] args, Option[] toolOptions) throws Exception {
     String configurerClassName = System.getProperty("solr.authentication.httpclient.configurer");
     if (configurerClassName!=null) {
       try {
@@ -274,10 +288,6 @@ public class SolrCLI {
       }
     }
 
-    // Determine the tool
-    String toolType = args[0].trim().toLowerCase(Locale.ROOT);
-    Tool tool = newTool(toolType);
-
     // the parser doesn't like -D props
     List<String> toolArgList = new ArrayList<String>();
     List<String> dashDList = new ArrayList<String>();
@@ -293,7 +303,7 @@ public class SolrCLI {
 
     // process command-line args to configure this application
     CommandLine cli = 
-        processCommandLineArgs(joinCommonAndToolOptions(tool.getOptions()), toolArgs);
+        processCommandLineArgs(joinCommonAndToolOptions(toolOptions), toolArgs);
 
     List argList = cli.getArgList();
     argList.addAll(dashDList);
@@ -305,8 +315,7 @@ public class SolrCLI {
       checkSslStoreSysProp(solrInstallDir, "trustStore");
     }
 
-    // run the tool
-    exit(tool.runTool(cli));
+    return cli;
   }
 
   protected static void checkSslStoreSysProp(String solrInstallDir, String key) {
@@ -370,6 +379,8 @@ public class SolrCLI {
       return new ZkLsTool();
     else if ("assert".equals(toolType))
       return new AssertTool();
+    else if ("utils".equals(toolType))
+      return new UtilsTool();
 
     // If you add a built-in tool to this class, add it here to avoid
     // classpath scanning
@@ -3342,4 +3353,246 @@ public class SolrCLI {
       }
     }
   } // end AssertTool class
+  
+  public static class UtilsTool extends ToolBase {
+    private Path serverPath;
+    private Path logsPath;
+    private boolean beQuiet;
+
+    public UtilsTool() { this(System.out); }
+    public UtilsTool(PrintStream stdout) { super(stdout); }
+
+    public String getName() {
+      return "prestart";
+    }
+
+    @SuppressWarnings("static-access")
+    public Option[] getOptions() {
+      return new Option[]{
+          OptionBuilder
+              .withArgName("path")
+              .hasArg()
+              .withDescription("Path to server dir. Required if logs path is relative")
+              .create("s"),
+          OptionBuilder
+              .withArgName("path")
+              .hasArg()
+              .withDescription("Path to logs dir. If relative, also provide server dir with -s")
+              .create("l"),
+          OptionBuilder
+              .withDescription("Be quiet, don't print to stdout, only return exit codes")
+              .create("q"),
+          OptionBuilder
+              .withArgName("daysToKeep")
+              .hasArg()
+              .withType(Integer.class)
+              .withDescription("Path to logs directory")
+              .create("remove_old_solr_logs"),
+          OptionBuilder
+              .withArgName("generations")
+              .hasArg()
+              .withType(Integer.class)
+              .withDescription("Rotate solr.log to solr.log.1 etc")
+              .create("rotate_solr_logs"),
+          OptionBuilder
+              .withDescription("Archive old garbage collection logs into archive/")
+              .create("archive_gc_logs"),
+          OptionBuilder
+              .withDescription("Archive old console logs into archive/")
+              .create("archive_console_logs")
+      };
+    }
+
+    @Override
+    public int runTool(CommandLine cli) throws Exception {
+      if (cli.getOptions().length == 0 || cli.getArgs().length > 0 || cli.hasOption("h")) {
+        new HelpFormatter().printHelp("bin/solr utils [OPTIONS]", getToolOptions(this));
+        return 1;
+      }
+      if (cli.hasOption("s")) {
+        serverPath = Paths.get(cli.getOptionValue("s"));
+      }
+      if (cli.hasOption("l")) {
+        logsPath = Paths.get(cli.getOptionValue("l"));
+      }
+      if (cli.hasOption("q")) {
+        beQuiet = cli.hasOption("q");
+      }
+      if (cli.hasOption("remove_old_solr_logs")) {
+        if (removeOldSolrLogs(Integer.parseInt(cli.getOptionValue("remove_old_solr_logs"))) > 0) return 1;
+      }
+      if (cli.hasOption("rotate_solr_logs")) {
+        if (rotateSolrLogs(Integer.parseInt(cli.getOptionValue("rotate_solr_logs"))) > 0) return 1;
+      }
+      if (cli.hasOption("archive_gc_logs")) {
+        if (archiveGcLogs() > 0) return 1;
+      }
+      if (cli.hasOption("archive_console_logs")) {
+        if (archiveConsoleLogs() > 0) return 1;
+      }
+      return 0;
+    }
+
+    /**
+     * Moves gc logs into archived/
+     * @return 0 on success
+     * @throws Exception on failure
+     */
+    public int archiveGcLogs() throws Exception {
+      prepareLogsPath();
+      Path archivePath = logsPath.resolve("archived");
+      if (!archivePath.toFile().exists()) {
+        Files.createDirectories(archivePath);
+      }
+      List<Path> archived = Files.find(archivePath, 1, (f, a) 
+          -> a.isRegularFile() && String.valueOf(f.getFileName()).startsWith("solr_gc_"))
+          .collect(Collectors.toList());
+      for (Path p : archived) {
+        Files.delete(p);
+      }
+      List<Path> files = Files.find(logsPath, 1, (f, a) 
+          -> a.isRegularFile() && String.valueOf(f.getFileName()).startsWith("solr_gc_"))
+          .collect(Collectors.toList());
+      if (files.size() > 0) {
+        out("Archiving " + files.size() + " old GC log files to " + archivePath);
+        for (Path p : files) {
+          Files.move(p, archivePath.resolve(p.getFileName()), StandardCopyOption.REPLACE_EXISTING);
+        }
+      }
+      return 0;
+    }
+
+    /**
+     * Moves console log(s) into archiced/
+     * @return 0 on success
+     * @throws Exception on failure
+     */
+    public int archiveConsoleLogs() throws Exception {
+      prepareLogsPath();
+      Path archivePath = logsPath.resolve("archived");
+      if (!archivePath.toFile().exists()) {
+        Files.createDirectories(archivePath);
+      }
+      List<Path> archived = Files.find(archivePath, 1, (f, a) 
+          -> a.isRegularFile() && String.valueOf(f.getFileName()).endsWith("-console.log"))
+          .collect(Collectors.toList());
+      for (Path p : archived) {        
+        Files.delete(p);
+      }
+      List<Path> files = Files.find(logsPath, 1, (f, a) 
+          -> a.isRegularFile() && String.valueOf(f.getFileName()).endsWith("-console.log"))
+          .collect(Collectors.toList());
+      if (files.size() > 0) {
+        out("Archiving " + files.size() + " console log files");
+        for (Path p : files) {
+          Files.move(p, archivePath.resolve(p.getFileName()), StandardCopyOption.REPLACE_EXISTING);
+        }
+      }
+      return 0;
+    }
+
+    /**
+     * Rotates solr.log before starting Solr. Mimics log4j2 behavior, i.e. with generations=9:
+     * <pre>
+     *   solr.log.9 (and higher) are deleted
+     *   solr.log.8 -&gt; solr.log.9
+     *   solr.log.7 -&gt; solr.log.8
+     *   ...
+     *   solr.log   -&gt; solr.log.1
+     * </pre>
+     * @param generations number of generations to keep. Should agree with setting in log4j.properties
+     * @return 0 if success
+     * @throws Exception if problems
+     */
+    public int rotateSolrLogs(int generations) throws Exception {
+      prepareLogsPath();
+      if (logsPath.toFile().exists() && logsPath.resolve("solr.log").toFile().exists()) {
+        out("Rotating solr logs, keeping a max of "+generations+" generations");
+        try (Stream<Path> files = Files.find(logsPath, 1, 
+            (f, a) -> a.isRegularFile() && String.valueOf(f.getFileName()).startsWith("solr.log."))
+            .sorted((b,a) -> new Integer(a.getFileName().toString().substring(9))
+                  .compareTo(new Integer(b.getFileName().toString().substring(9))))) {
+          files.forEach(p -> {
+            try {
+              int number = Integer.parseInt(p.getFileName().toString().substring(9));
+              if (number >= generations) {
+                Files.delete(p);
+              } else {
+                Path renamed = p.getParent().resolve("solr.log." + (number + 1));
+                Files.move(p, renamed);
+              }
+            } catch (IOException e) {
+              out("Problem during rotation of log files: " + e.getMessage());
+            }
+          });
+        } catch (NumberFormatException nfe) {
+          throw new Exception("Do not know how to rotate solr.log.<ext> with non-numeric extension. Rotate aborted.", nfe);
+        }
+        Files.move(logsPath.resolve("solr.log"), logsPath.resolve("solr.log.1"));
+      }
+      
+      return 0;
+    }
+
+    /**
+     * Deletes time-stamped old solr logs, if older than n days 
+     * @param daysToKeep number of days logs to keep before deleting
+     * @return 0 on success
+     * @throws Exception on failure
+     */
+    public int removeOldSolrLogs(int daysToKeep) throws Exception {
+      prepareLogsPath();
+      if (logsPath.toFile().exists()) {
+        try (Stream<Path> stream = Files.find(logsPath, 2, (f, a) -> a.isRegularFile() 
+            && Instant.now().minus(Period.ofDays(daysToKeep)).isAfter(a.lastModifiedTime().toInstant())
+            && String.valueOf(f.getFileName()).startsWith("solr_log_"))) {
+          List<Path> files = stream.collect(Collectors.toList());
+          if (files.size() > 0) {
+            out("Deleting "+files.size() + " solr_log_* files older than " + daysToKeep + " days.");
+            for (Path p : files) {
+              Files.delete(p);
+            }
+          }
+        }
+      }
+      return 0;
+    }
+
+    // Private methods to follow
+    
+    private void out(String message) {
+      if (!beQuiet) {
+        stdout.print(message + "\n");
+      }
+    }
+
+    private void prepareLogsPath() throws Exception {
+      if (logsPath == null) {
+        throw new Exception("Command requires the -l <log-directory> option");
+      }
+      if (!logsPath.isAbsolute()) {
+        if (serverPath != null && serverPath.isAbsolute() && serverPath.toFile().exists()) {
+          logsPath = serverPath.resolve(logsPath);
+        } else {
+          throw new Exception("Logs directory must be an absolute path, or -s must be supplied");
+        }
+      }
+    }
+    
+    @Override
+    protected void runImpl(CommandLine cli) throws Exception {
+    }
+    
+    public void setLogPath(Path logsPath) {
+      this.logsPath = logsPath; 
+    }
+
+    public void setServerPath(Path serverPath) {
+      this.serverPath = serverPath; 
+    }
+    
+    public void setQuiet(boolean shouldPrintStdout) {
+      this.beQuiet = shouldPrintStdout; 
+    }
+  } // end UtilsTool class  
 }

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/ed3b268d/solr/core/src/test/org/apache/solr/util/UtilsToolTest.java
----------------------------------------------------------------------
diff --git a/solr/core/src/test/org/apache/solr/util/UtilsToolTest.java b/solr/core/src/test/org/apache/solr/util/UtilsToolTest.java
new file mode 100644
index 0000000..fa39620
--- /dev/null
+++ b/solr/core/src/test/org/apache/solr/util/UtilsToolTest.java
@@ -0,0 +1,185 @@
+/*
+ * 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.solr.util;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.attribute.FileTime;
+import java.time.Instant;
+import java.time.Period;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.apache.commons.cli.CommandLine;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.apache.solr.util.SolrCLI.findTool;
+import static org.apache.solr.util.SolrCLI.parseCmdLine;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Unit test for SolrCLI's UtilsTool
+ */
+public class UtilsToolTest {
+
+  private Path dir;
+  private SolrCLI.UtilsTool tool;
+  private List<String> files = Arrays.asList(
+      "solr.log", 
+      "solr.log.1", 
+      "solr.log.2", 
+      "solr.log.3", 
+      "solr.log.9", 
+      "solr.log.10", 
+      "solr.log.11", 
+      "solr_log_20160102", 
+      "solr_log_20160304", 
+      "solr-8983-console.log",
+      "solr_gc_log_20160102", 
+      "solr_gc_log_2");
+  
+  @Before
+  public void setUp() throws IOException {
+    dir = Files.createTempDirectory("Utils Tool Test");
+    files.stream().forEach(f -> {
+      try {
+        dir.resolve(f).toFile().createNewFile();
+      } catch (IOException e) {
+        assertTrue(false);
+      }
+    });
+  }
+  
+  @After
+  public void tearDown() throws IOException {
+    org.apache.commons.io.FileUtils.deleteDirectory(dir.toFile());
+  }
+  
+  @Test
+  public void testEmptyAndQuiet() throws Exception {
+    String[] args = {"utils", "-remove_old_solr_logs", "7",  
+        "-rotate_solr_logs", "9",  
+        "-archive_gc_logs",
+        "-archive_console_logs",
+        "-q",
+        "-l", dir.toString()};
+    assertEquals(0, runTool(args));
+  }
+
+  @Test
+  public void testNonexisting() throws Exception {
+    String nonexisting = dir.resolve("non-existing").toString();
+    String[] args = {"utils", "-remove_old_solr_logs", "7",
+        "-rotate_solr_logs", "9",
+        "-archive_gc_logs",
+        "-archive_console_logs",
+        "-l", nonexisting};
+    assertEquals(0, runTool(args));
+  }
+  
+  @Test
+  public void testRemoveOldSolrLogs() throws Exception {
+    String[] args = {"utils", "-remove_old_solr_logs", "1", "-l", dir.toString()};
+    assertEquals(files.size(), fileCount());
+    assertEquals(0, runTool(args));
+    assertEquals(files.size(), fileCount());     // No logs older than 1 day
+    Files.setLastModifiedTime(dir.resolve("solr_log_20160102"), FileTime.from(Instant.now().minus(Period.ofDays(2))));
+    assertEquals(0, runTool(args));
+    assertEquals(files.size()-1, fileCount());   // One logs older than 1 day
+    Files.setLastModifiedTime(dir.resolve("solr_log_20160304"), FileTime.from(Instant.now().minus(Period.ofDays(3))));
+    assertEquals(0, runTool(args));
+    assertEquals(files.size()-2, fileCount());   // Two logs older than 1 day
+  }
+
+  @Test
+  public void testRelativePath() throws Exception {
+    String[] args = {"utils", "-remove_old_solr_logs", "0", "-l", dir.getFileName().toString(), "-s", dir.getParent().toString()};
+    assertEquals(files.size(), fileCount());
+    assertEquals(0, runTool(args));
+    assertEquals(files.size()-2, fileCount());
+  }
+
+  @Test
+  public void testRelativePathError() throws Exception {
+    String[] args = {"utils", "-remove_old_solr_logs", "0", "-l", dir.getFileName().toString()};
+    try {
+      runTool(args);
+    } catch (Exception e) {
+      return;
+    }
+    assertTrue(false);
+  }
+  
+  @Test
+  public void testRemoveOldGcLogs() throws Exception {
+    String[] args = {"utils", "-archive_gc_logs", "-l", dir.toString()};
+    assertEquals(files.size(), fileCount());
+    assertEquals(0, runTool(args));
+    assertEquals(files.size()-2, fileCount());
+    assertFalse(listFiles().contains("solr_gc_log_2"));
+    assertTrue(Files.exists(dir.resolve("archived").resolve("solr_gc_log_2")));
+    assertEquals(0, runTool(args));
+    assertFalse(Files.exists(dir.resolve("archived").resolve("solr_gc_log_2")));
+  }
+
+  @Test
+  public void testArchiveConsoleLogs() throws Exception {
+    String[] args = {"utils", "-archive_console_logs", "-l", dir.toString()};
+    assertEquals(files.size(), fileCount());
+    assertEquals(0, runTool(args));
+    assertEquals(files.size()-1, fileCount());
+    assertFalse(listFiles().contains("solr-8983-console.log"));
+    assertTrue(Files.exists(dir.resolve("archived").resolve("solr-8983-console.log")));
+    assertEquals(0, runTool(args));
+    assertFalse(Files.exists(dir.resolve("archived").resolve("solr-8983-console.log")));
+  }
+
+  @Test
+  public void testRotateSolrLogs() throws Exception {
+    String[] args = {"utils", "-rotate_solr_logs", "9", "-l", dir.toString()};
+    assertEquals(files.size(), fileCount());
+    assertTrue(listFiles().contains("solr.log"));
+    assertEquals(0, runTool(args));
+    assertEquals(files.size()-3, fileCount());
+    assertTrue(listFiles().contains("solr.log.4"));
+    assertFalse(listFiles().contains("solr.log"));
+    assertFalse(listFiles().contains("solr.log.9"));
+    assertFalse(listFiles().contains("solr.log.10"));
+    assertFalse(listFiles().contains("solr.log.11"));
+  }
+  
+  private List<String> listFiles() throws IOException {
+    return Files.find(dir, 1, (p, a) -> a.isRegularFile()).map(p -> p.getFileName().toString()).collect(Collectors.toList());
+  }
+  
+  private long fileCount() throws IOException {
+    return listFiles().size();
+  }
+
+  private int runTool(String[] args) throws Exception {
+    SolrCLI.Tool tool = findTool(args);
+    CommandLine cli = parseCmdLine(args, tool.getOptions());
+    return tool.runTool(cli);
+  }
+}
\ No newline at end of file