NIFI-8752: Automatic diagnostic at NiFi restart/stop

This closes #5195.

Signed-off-by: Tamas Palfy <tamas.bertalan.palfy@gmail.com>
diff --git a/nifi-commons/nifi-properties/src/main/java/org/apache/nifi/util/NiFiProperties.java b/nifi-commons/nifi-properties/src/main/java/org/apache/nifi/util/NiFiProperties.java
index a7bda62..c68bb5b 100644
--- a/nifi-commons/nifi-properties/src/main/java/org/apache/nifi/util/NiFiProperties.java
+++ b/nifi-commons/nifi-properties/src/main/java/org/apache/nifi/util/NiFiProperties.java
@@ -303,6 +303,18 @@
     public static final String MONITOR_LONG_RUNNING_TASK_SCHEDULE = "nifi.monitor.long.running.task.schedule";
     public static final String MONITOR_LONG_RUNNING_TASK_THRESHOLD = "nifi.monitor.long.running.task.threshold";
 
+    // automatic diagnostic properties
+    public static final String DIAGNOSTICS_ON_SHUTDOWN_ENABLED = "nifi.diagnostics.on.shutdown.enabled";
+    public static final String DIAGNOSTICS_ON_SHUTDOWN_VERBOSE = "nifi.diagnostics.on.shutdown.verbose";
+    public static final String DIAGNOSTICS_ON_SHUTDOWN_DIRECTORY = "nifi.diagnostics.on.shutdown.directory";
+    public static final String DIAGNOSTICS_ON_SHUTDOWN_MAX_FILE_COUNT = "nifi.diagnostics.on.shutdown.max.filecount";
+    public static final String DIAGNOSTICS_ON_SHUTDOWN_MAX_DIRECTORY_SIZE = "nifi.diagnostics.on.shutdown.max.directory.size";
+
+    // automatic diagnostic defaults
+    public static final String DEFAULT_DIAGNOSTICS_ON_SHUTDOWN_DIRECTORY = "./diagnostics";
+    public static final int DEFAULT_DIAGNOSTICS_ON_SHUTDOWN_MAX_FILE_COUNT = 10;
+    public static final String DEFAULT_DIAGNOSTICS_ON_SHUTDOWN_MAX_DIRECTORY_SIZE = "10 MB";
+
     // defaults
     public static final Boolean DEFAULT_AUTO_RESUME_STATE = true;
     public static final String DEFAULT_AUTHORIZER_CONFIGURATION_FILE = "conf/authorizers.xml";
@@ -770,6 +782,7 @@
 
     /**
      * Returns true if auto reload of the keystore and truststore is enabled.
+     *
      * @return true if auto reload of the keystore and truststore is enabled.
      */
     public boolean isSecurityAutoReloadEnabled() {
@@ -778,6 +791,7 @@
 
     /**
      * Returns the auto reload interval of the keystore and truststore.
+     *
      * @return The interval over which the keystore and truststore should auto-reload.
      */
     public String getSecurityAutoReloadInterval() {
@@ -1076,7 +1090,7 @@
             return Collections.emptyList();
         } else {
             List<String> fallbackClaims = Arrays.asList(rawProperty.split(","));
-            return fallbackClaims.stream().map(String::trim).filter(s->!s.isEmpty()).collect(Collectors.toList());
+            return fallbackClaims.stream().map(String::trim).filter(s -> !s.isEmpty()).collect(Collectors.toList());
         }
     }
 
@@ -1084,6 +1098,32 @@
         return Boolean.parseBoolean(getProperty(WEB_SHOULD_SEND_SERVER_VERSION, DEFAULT_WEB_SHOULD_SEND_SERVER_VERSION));
     }
 
+    // Automatic diagnostic getters
+
+    public boolean isDiagnosticsOnShutdownEnabled() {
+        return Boolean.parseBoolean(getProperty(DIAGNOSTICS_ON_SHUTDOWN_ENABLED));
+    }
+
+    public boolean isDiagnosticsOnShutdownVerbose() {
+        return Boolean.parseBoolean(getProperty(DIAGNOSTICS_ON_SHUTDOWN_VERBOSE));
+    }
+
+    public String getDiagnosticsOnShutdownDirectory() {
+        return getProperty(DIAGNOSTICS_ON_SHUTDOWN_DIRECTORY, DEFAULT_DIAGNOSTICS_ON_SHUTDOWN_DIRECTORY);
+    }
+
+    public int getDiagnosticsOnShutdownMaxFileCount() {
+        try {
+            return Integer.parseInt(getProperty(DIAGNOSTICS_ON_SHUTDOWN_MAX_FILE_COUNT));
+        } catch (NumberFormatException e) {
+            return DEFAULT_DIAGNOSTICS_ON_SHUTDOWN_MAX_FILE_COUNT;
+        }
+    }
+
+    public String getDiagnosticsOnShutdownDirectoryMaxSize() {
+        return getProperty(DIAGNOSTICS_ON_SHUTDOWN_MAX_DIRECTORY_SIZE, DEFAULT_DIAGNOSTICS_ON_SHUTDOWN_MAX_DIRECTORY_SIZE);
+    }
+
     /**
      * Returns whether Knox SSO is enabled.
      *
@@ -1165,7 +1205,7 @@
 
     /**
      * The name of an attribute in the SAML assertions that contains the user identity.
-     *
+     * <p>
      * If not specified, or missing, the NameID of the Subject will be used.
      *
      * @return the attribute name containing the user identity
@@ -1587,17 +1627,17 @@
 
     public boolean isZooKeeperTlsConfigurationPresent() {
         return StringUtils.isNotBlank(getProperty(NiFiProperties.ZOOKEEPER_CLIENT_SECURE))
-            && StringUtils.isNotBlank(getProperty(NiFiProperties.ZOOKEEPER_SECURITY_KEYSTORE))
-            && getProperty(NiFiProperties.ZOOKEEPER_SECURITY_KEYSTORE_PASSWD) != null
-            && StringUtils.isNotBlank(getProperty(NiFiProperties.ZOOKEEPER_SECURITY_TRUSTSTORE))
-            && getProperty(NiFiProperties.ZOOKEEPER_SECURITY_TRUSTSTORE_PASSWD) != null;
+                && StringUtils.isNotBlank(getProperty(NiFiProperties.ZOOKEEPER_SECURITY_KEYSTORE))
+                && getProperty(NiFiProperties.ZOOKEEPER_SECURITY_KEYSTORE_PASSWD) != null
+                && StringUtils.isNotBlank(getProperty(NiFiProperties.ZOOKEEPER_SECURITY_TRUSTSTORE))
+                && getProperty(NiFiProperties.ZOOKEEPER_SECURITY_TRUSTSTORE_PASSWD) != null;
     }
 
     public boolean isTlsConfigurationPresent() {
         return StringUtils.isNotBlank(getProperty(SECURITY_KEYSTORE))
-            && getProperty(SECURITY_KEYSTORE_PASSWD) != null
-            && StringUtils.isNotBlank(getProperty(SECURITY_TRUSTSTORE))
-            && getProperty(SECURITY_TRUSTSTORE_PASSWD) != null;
+                && getProperty(SECURITY_KEYSTORE_PASSWD) != null
+                && StringUtils.isNotBlank(getProperty(SECURITY_TRUSTSTORE))
+                && getProperty(SECURITY_TRUSTSTORE_PASSWD) != null;
     }
 
     public String getFlowFileRepoEncryptionKeyId() {
@@ -1915,7 +1955,6 @@
      *
      * @param prefix The exact string the returned properties should start with. Dots are considered, thus prefix "item" will return both
      *               properties starting with "item." and "items". Properties with empty value will be included as well.
-     *
      * @return A map of properties starting with the prefix.
      */
     public Map<String, String> getPropertiesWithPrefix(final String prefix) {
@@ -1924,13 +1963,12 @@
 
     /**
      * Returns with all the possible next "tokens" after the given prefix. An alphanumeric string between dots is considered as a "token".
-     *
+     * <p>
      * For example if there are "parent.sub1" and a "parent.sub2" properties are set, and the prefix is "parent", the method will return
      * with a set, consisting of "sub1" and "sub2. Only directly subsequent tokens are considered, so in case of "parent.sub1.subsub1", the
      * result will contain "sub1" as well.
      *
      * @param prefix The prefix of the request.
-     *
      * @return A set of direct subsequent tokens.
      */
     public Set<String> getDirectSubsequentTokens(final String prefix) {
@@ -1951,9 +1989,9 @@
      * file specified cannot be found/read a runtime exception will be thrown.
      * If one is not specified an empty object will be returned.
      *
-     * @param propertiesFilePath   if provided properties will be loaded from
-     *                             given file; else will be loaded from System property.
-     *                             Can be null. Passing {@code ""} skips any attempt to load from the file system.
+     * @param propertiesFilePath if provided properties will be loaded from
+     *                           given file; else will be loaded from System property.
+     *                           Can be null. Passing {@code ""} skips any attempt to load from the file system.
      * @return NiFiProperties
      */
     public static NiFiProperties createBasicNiFiProperties(final String propertiesFilePath) {
diff --git a/nifi-docs/src/main/asciidoc/administration-guide.adoc b/nifi-docs/src/main/asciidoc/administration-guide.adoc
index 95fcaa3..8c7897c 100644
--- a/nifi-docs/src/main/asciidoc/administration-guide.adoc
+++ b/nifi-docs/src/main/asciidoc/administration-guide.adoc
@@ -2640,8 +2640,13 @@
 defined in the `notification.services.file` property. The services with the specified identifiers will be used to notify their
 configured recipients whenever NiFi is stopped.
 |`nifi.died.notification.services`|This property is a comma-separated list of Notification Service identifiers that correspond to the Notification Services
-defined in the `notification.services.file` property. The services with the specified identifiers will be used to notify their
-configured recipients if the bootstrap determines that NiFi has unexpectedly died.
+                                 defined in the `notification.services.file` property. The services with the specified identifiers will be used to notify their
+                                 configured recipients if the bootstrap determines that NiFi has unexpectedly died.
+|`nifi.diagnostics.on.shutdown.enabled`|(true or false) This property decides whether to run NiFi diagnostics before shutting down.
+|`nifi.diagnostics.on.shutdown.verbose`|(true or false) This property decides whether to run NiFi diagnostics in verbose mode.
+|`nifi.diagnostics.on.shutdown.directory`|This property specifies the location of the NiFi diagnostics directory.
+|`nifi.diagnostics.on.shutdown.max.filecount`|This property specifies the maximum permitted number of diagnostic files. If the limit is exceeded, the oldest files are deleted.
+|`nifi.diagnostics.on.shutdown.max.directory.size`|This property specifies the maximum permitted size of the diagnostics directory. If the limit is exceeded, the oldest files are deleted.
 |====
 
 [[notification_services]]
@@ -4262,3 +4267,21 @@
    nifi.nar.library.provider.hdfs2.implementation=org.apache.nifi.nar.hadoop.HDFSNarProvider
    nifi.nar.library.provider.hdfs2.resources=/etc/hadoop/core-site.xml
    nifi.nar.library.provider.hdfs2.source.directory=/other/dir/for/customNars
+
+== NiFi diagnostics
+
+It is possible to run diagnostics on NiFi with
+
+```
+$ ./bin/nifi.sh --diagnostics --verbose <dumpfilePath>
+```
+
+During the diagnostic, NiFi sends a request to an already running NiFi instance, which collects information about clusters,
+components, part of the configuration, memory usage, etc., and writes it to the specified file or, failing that, to the logs.
+
+The verbose switch is optional and can be used to control the level of diagnostic detail. In case of a missing dump file path, NiFi writes the diagnostics information to the bootstrap.log file.
+
+=== Automatic diagnostics on restart and shutdown
+
+NiFi supports automatic diagnostics in the event of a shutdown. The feature is disabled by default. The settings can be found in the nifi.properties file and the feature can be enabled there also.
+In the case of a lengthy diagnostic, NiFi may terminate before the diagnostics are completed. In this case, the graceful.shutdown.seconds property should be set to a higher value in the bootstrap.conf.
\ No newline at end of file
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-resources/src/main/resources/conf/nifi.properties b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-resources/src/main/resources/conf/nifi.properties
index b5d9a6a..272d15f 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-resources/src/main/resources/conf/nifi.properties
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-resources/src/main/resources/conf/nifi.properties
@@ -327,3 +327,23 @@
 # runtime monitoring properties
 nifi.monitor.long.running.task.schedule=
 nifi.monitor.long.running.task.threshold=
+
+# Create automatic diagnostics when stopping/restarting NiFi.
+
+# Enable automatic diagnostic at shutdown.
+nifi.diagnostics.on.shutdown.enabled=false
+
+# Include verbose diagnostic information.
+nifi.diagnostics.on.shutdown.verbose=false
+
+# The location of the diagnostics folder.
+nifi.diagnostics.on.shutdown.directory=./diagnostics
+
+# The maximum number of files permitted in the directory. If the limit is exceeded, the oldest files are deleted.
+nifi.diagnostics.on.shutdown.max.filecount=10
+
+# The diagnostics folder's maximum permitted size in bytes. If the limit is exceeded, the oldest files are deleted.
+nifi.diagnostics.on.shutdown.max.directory.size=10 MB
+
+
+
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-runtime/src/main/java/org/apache/nifi/NiFi.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-runtime/src/main/java/org/apache/nifi/NiFi.java
index 445217d..01e1c71 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-runtime/src/main/java/org/apache/nifi/NiFi.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-runtime/src/main/java/org/apache/nifi/NiFi.java
@@ -17,11 +17,14 @@
 package org.apache.nifi;
 
 import org.apache.nifi.bundle.Bundle;
+import org.apache.nifi.diagnostics.DiagnosticsDump;
 import org.apache.nifi.nar.ExtensionMapping;
 import org.apache.nifi.nar.NarClassLoaders;
 import org.apache.nifi.nar.NarClassLoadersHolder;
 import org.apache.nifi.nar.NarUnpacker;
 import org.apache.nifi.nar.SystemBundle;
+import org.apache.nifi.processor.DataUnit;
+import org.apache.nifi.util.DiagnosticUtils;
 import org.apache.nifi.util.FileUtils;
 import org.apache.nifi.util.NiFiProperties;
 import org.slf4j.Logger;
@@ -29,9 +32,10 @@
 import org.slf4j.bridge.SLF4JBridgeHandler;
 
 import java.io.File;
+import java.io.FileOutputStream;
 import java.io.FileWriter;
 import java.io.IOException;
-import java.lang.Thread.UncaughtExceptionHandler;
+import java.io.OutputStream;
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
 import java.net.MalformedURLException;
@@ -39,7 +43,10 @@
 import java.net.URLClassLoader;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
+import java.nio.file.Path;
 import java.nio.file.Paths;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
@@ -54,33 +61,38 @@
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.concurrent.atomic.AtomicLong;
+import java.util.stream.Stream;
 
 public class NiFi implements NiFiEntryPoint {
 
+    public static final String BOOTSTRAP_PORT_PROPERTY = "nifi.bootstrap.listen.port";
+    public static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss");
+
     private static final Logger LOGGER = LoggerFactory.getLogger(NiFi.class);
     private static final String KEY_FILE_FLAG = "-K";
+
     private final NiFiServer nifiServer;
     private final BootstrapListener bootstrapListener;
+    private final NiFiProperties properties;
 
-    public static final String BOOTSTRAP_PORT_PROPERTY = "nifi.bootstrap.listen.port";
     private volatile boolean shutdown = false;
 
     public NiFi(final NiFiProperties properties)
-            throws ClassNotFoundException, IOException, NoSuchMethodException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
-
+            throws ClassNotFoundException, IOException, IllegalArgumentException {
         this(properties, ClassLoader.getSystemClassLoader());
-
     }
 
     public NiFi(final NiFiProperties properties, ClassLoader rootClassLoader)
-            throws ClassNotFoundException, IOException, NoSuchMethodException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
+            throws ClassNotFoundException, IOException, IllegalArgumentException {
+
+        this.properties = properties;
 
         // There can only be one krb5.conf for the overall Java process so set this globally during
         // start up so that processors and our Kerberos authentication code don't have to set this
         final File kerberosConfigFile = properties.getKerberosConfigurationFile();
         if (kerberosConfigFile != null) {
             final String kerberosConfigFilePath = kerberosConfigFile.getAbsolutePath();
-            LOGGER.info("Setting java.security.krb5.conf to {}", new Object[]{kerberosConfigFilePath});
+            LOGGER.info("Setting java.security.krb5.conf to {}", kerberosConfigFilePath);
             System.setProperty("java.security.krb5.conf", kerberosConfigFilePath);
         }
 
@@ -164,8 +176,8 @@
             }
 
             final long duration = System.nanoTime() - startTime;
-            LOGGER.info("Controller initialization took " + duration + " nanoseconds "
-                    + "(" + (int) TimeUnit.SECONDS.convert(duration, TimeUnit.NANOSECONDS) + " seconds).");
+            LOGGER.info("Controller initialization took {} nanoseconds ( {}  seconds).",
+                    duration, (int) TimeUnit.SECONDS.convert(duration, TimeUnit.NANOSECONDS));
         }
     }
 
@@ -174,23 +186,17 @@
     }
 
     protected void setDefaultUncaughtExceptionHandler() {
-        Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandler() {
-            @Override
-            public void uncaughtException(final Thread t, final Throwable e) {
-                LOGGER.error("An Unknown Error Occurred in Thread {}: {}", t, e.toString());
-                LOGGER.error("", e);
-            }
+        Thread.setDefaultUncaughtExceptionHandler((thread, exception) -> {
+            LOGGER.error("An Unknown Error Occurred in Thread {}: {}", thread, exception.toString());
+            LOGGER.error("", exception);
         });
     }
 
     protected void addShutdownHook() {
-        Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
-            @Override
-            public void run() {
+        Runtime.getRuntime().addShutdownHook(new Thread(() ->
                 // shutdown the jetty server
-                shutdownHook(false);
-            }
-        }));
+                shutdownHook(false)
+        ));
     }
 
     protected void initLogging() {
@@ -201,8 +207,8 @@
     private static ClassLoader createBootstrapClassLoader() {
         //Get list of files in bootstrap folder
         final List<URL> urls = new ArrayList<>();
-        try {
-            Files.list(Paths.get("lib/bootstrap")).forEach(p -> {
+        try (final Stream<Path> files = Files.list(Paths.get("lib/bootstrap"))) {
+            files.forEach(p -> {
                 try {
                     urls.add(p.toUri().toURL());
                 } catch (final MalformedURLException mef) {
@@ -216,14 +222,40 @@
         return new URLClassLoader(urls.toArray(new URL[0]), Thread.currentThread().getContextClassLoader());
     }
 
-    public void shutdownHook(boolean isReload) {
+    public void shutdownHook(final boolean isReload) {
         try {
+            runDiagnosticsOnShutdown();
             shutdown();
         } catch (final Throwable t) {
-            LOGGER.warn("Problem occurred ensuring Jetty web server was properly terminated due to " + t);
+            LOGGER.warn("Problem occurred ensuring Jetty web server was properly terminated due to ", t);
         }
     }
 
+    private void runDiagnosticsOnShutdown() throws IOException {
+        if (properties.isDiagnosticsOnShutdownEnabled()) {
+            final String diagnosticDirectoryPath = properties.getDiagnosticsOnShutdownDirectory();
+            final boolean isCreated = DiagnosticUtils.createDiagnosticDirectory(diagnosticDirectoryPath);
+            if (isCreated) {
+                LOGGER.debug("Diagnostic directory has successfully been created.");
+            }
+            while (DiagnosticUtils.isFileCountExceeded(diagnosticDirectoryPath, properties.getDiagnosticsOnShutdownMaxFileCount())
+                    || DiagnosticUtils.isSizeExceeded(diagnosticDirectoryPath, DataUnit.parseDataSize(properties.getDiagnosticsOnShutdownDirectoryMaxSize(), DataUnit.B).longValue())) {
+                final Path oldestFile = DiagnosticUtils.getOldestFile(diagnosticDirectoryPath);
+                Files.delete(oldestFile);
+            }
+            final String fileName = String.format("%s/diagnostic-%s.log", diagnosticDirectoryPath, DATE_TIME_FORMATTER.format(LocalDateTime.now()));
+            diagnose(new File(fileName), properties.isDiagnosticsOnShutdownVerbose());
+        }
+    }
+
+    private void diagnose(final File file, final boolean verbose) throws IOException {
+        final DiagnosticsDump diagnosticsDump = getServer().getDiagnosticsFactory().create(verbose);
+        try (final OutputStream fileOutputStream = new FileOutputStream(file)) {
+            diagnosticsDump.writeTo(fileOutputStream);
+        }
+    }
+
+
     protected void shutdown() {
         this.shutdown = true;
 
@@ -249,8 +281,8 @@
             private final ThreadFactory defaultFactory = Executors.defaultThreadFactory();
 
             @Override
-            public Thread newThread(final Runnable r) {
-                final Thread t = defaultFactory.newThread(r);
+            public Thread newThread(final Runnable runnable) {
+                final Thread t = defaultFactory.newThread(runnable);
                 t.setDaemon(true);
                 t.setName("Detect Timing Issues");
                 return t;
@@ -259,18 +291,15 @@
 
         final AtomicInteger occurrencesOutOfRange = new AtomicInteger(0);
         final AtomicInteger occurrences = new AtomicInteger(0);
-        final Runnable command = new Runnable() {
-            @Override
-            public void run() {
-                final long curMillis = System.currentTimeMillis();
-                final long difference = curMillis - lastTriggerMillis.get();
-                final long millisOff = Math.abs(difference - 2000L);
-                occurrences.incrementAndGet();
-                if (millisOff > 500L) {
-                    occurrencesOutOfRange.incrementAndGet();
-                }
-                lastTriggerMillis.set(curMillis);
+        final Runnable command = () -> {
+            final long curMillis = System.currentTimeMillis();
+            final long difference = curMillis - lastTriggerMillis.get();
+            final long millisOff = Math.abs(difference - 2000L);
+            occurrences.incrementAndGet();
+            if (millisOff > 500L) {
+                occurrencesOutOfRange.incrementAndGet();
             }
+            lastTriggerMillis.set(curMillis);
         };
 
         final ScheduledFuture<?> future = service.scheduleWithFixedDelay(command, 2000L, 2000L, TimeUnit.MILLISECONDS);
@@ -384,38 +413,38 @@
             throw new IllegalArgumentException("The bootstrap process provided the " + KEY_FILE_FLAG + " flag but no key");
         }
         try {
-          String passwordfile_path = parsedArgs.get(i + 1);
-          // Slurp in the contents of the file:
-          byte[] encoded = Files.readAllBytes(Paths.get(passwordfile_path));
-          key = new String(encoded,StandardCharsets.UTF_8);
-          if (0 == key.length())
-            throw new IllegalArgumentException("Key in keyfile " + passwordfile_path + " yielded an empty key");
+            String passwordfilePath = parsedArgs.get(i + 1);
+            // Slurp in the contents of the file:
+            byte[] encoded = Files.readAllBytes(Paths.get(passwordfilePath));
+            key = new String(encoded, StandardCharsets.UTF_8);
+            if (0 == key.length())
+                throw new IllegalArgumentException("Key in keyfile " + passwordfilePath + " yielded an empty key");
 
-          LOGGER.info("Now overwriting file in "+passwordfile_path);
+            LOGGER.info("Now overwriting file in {}", passwordfilePath);
 
-          // Overwrite the contents of the file (to avoid littering file system
-          // unlinked with key material):
-          File password_file = new File(passwordfile_path);
-          FileWriter overwriter = new FileWriter(password_file,false);
+            // Overwrite the contents of the file (to avoid littering file system
+            // unlinked with key material):
+            File passwordFile = new File(passwordfilePath);
+            FileWriter overwriter = new FileWriter(passwordFile, false);
 
-          // Construe a random pad:
-          Random r = new Random();
-          StringBuffer sb = new StringBuffer();
-          // Note on correctness: this pad is longer, but equally sufficient.
-          while(sb.length() < encoded.length){
-            sb.append(Integer.toHexString(r.nextInt()));
-          }
-          String pad = sb.toString();
-          LOGGER.info("Overwriting key material with pad: "+pad);
-          overwriter.write(pad);
-          overwriter.close();
+            // Construe a random pad:
+            Random random = new Random();
+            StringBuffer sb = new StringBuffer();
+            // Note on correctness: this pad is longer, but equally sufficient.
+            while (sb.length() < encoded.length) {
+                sb.append(Integer.toHexString(random.nextInt()));
+            }
+            String pad = sb.toString();
+            LOGGER.info("Overwriting key material with pad: {}", pad);
+            overwriter.write(pad);
+            overwriter.close();
 
-          LOGGER.info("Removing/unlinking file: "+passwordfile_path);
-          password_file.delete();
+            LOGGER.info("Removing/unlinking file: {}", passwordfilePath);
+            passwordFile.delete();
 
         } catch (IOException e) {
-          LOGGER.error("Caught IOException while retrieving the "+KEY_FILE_FLAG+"-passed keyfile; aborting: "+e.toString());
-          System.exit(1);
+            LOGGER.error("Caught IOException while retrieving the {} -passed keyfile; aborting: {}", KEY_FILE_FLAG, e.toString());
+            System.exit(1);
         }
 
         LOGGER.info("Read property protection key from key file provided by bootstrap process");
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-runtime/src/main/java/org/apache/nifi/util/DiagnosticUtils.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-runtime/src/main/java/org/apache/nifi/util/DiagnosticUtils.java
new file mode 100644
index 0000000..9cc47ae
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-runtime/src/main/java/org/apache/nifi/util/DiagnosticUtils.java
@@ -0,0 +1,98 @@
+/*

+ * 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.nifi.util;

+

+import org.slf4j.Logger;

+import org.slf4j.LoggerFactory;

+

+import java.io.File;

+import java.io.IOException;

+import java.nio.file.Files;

+import java.nio.file.Path;

+import java.nio.file.Paths;

+import java.util.Comparator;

+import java.util.Optional;

+import java.util.function.ToLongFunction;

+import java.util.stream.Stream;

+

+public final class DiagnosticUtils {

+

+    private static final Logger logger = LoggerFactory.getLogger(DiagnosticUtils.class);

+

+    private DiagnosticUtils() {

+        // utility class, not meant to be instantiated

+    }

+

+    public static Path getOldestFile(final String diagnosticDirectoryPath) throws IOException {

+        Comparator<? super Path> lastModifiedComparator = Comparator.comparingLong(p -> p.toFile().lastModified());

+

+        final Optional<Path> oldestFile;

+

+        try (Stream<Path> paths = Files.walk(Paths.get(diagnosticDirectoryPath))) {

+            oldestFile = paths

+                    .filter(Files::isRegularFile)

+                    .min(lastModifiedComparator);

+        }

+

+        return oldestFile.orElseThrow(

+                () -> new RuntimeException(String.format("Could not find oldest file in diagnostic directory: %s", diagnosticDirectoryPath)));

+    }

+

+    public static boolean isFileCountExceeded(final String diagnosticDirectoryPath, final int maxFileCount) {

+        final String[] fileNames = new File(diagnosticDirectoryPath).list();

+        if (fileNames == null) {

+            logger.error("The diagnostic directory path provided is either invalid or not permitted to be listed.");

+            return false;

+        }

+        return fileNames.length >= maxFileCount;

+    }

+

+    public static boolean isSizeExceeded(final String diagnosticDirectoryPath, final long maxSizeInBytes) {

+        return getDirectorySize(Paths.get(diagnosticDirectoryPath)) >= maxSizeInBytes;

+    }

+

+

+    public static boolean createDiagnosticDirectory(final String diagnosticDirectoryPath) {

+        File file = new File(diagnosticDirectoryPath);

+        return file.mkdir();

+    }

+

+    private static long getDirectorySize(Path path) {

+        long size = 0;

+        try (Stream<Path> walk = Files.walk(path)) {

+            size = walk

+                    .filter(Files::isRegularFile)

+                    .mapToLong(getFileSizeByPathFunction())

+                    .sum();

+

+        } catch (IOException e) {

+            logger.error("Directory [{}] size calculation failed", path, e);

+        }

+        return size;

+    }

+

+    private static ToLongFunction<Path> getFileSizeByPathFunction() {

+        return path -> {

+            try {

+                return Files.size(path);

+            } catch (IOException e) {

+                logger.error("Failed to get size of file {}", path, e);

+                return 0L;

+            }

+        };

+    }

+}