Browse Source

statisticsmanager refactoring

jrivard@gmail.com 6 years ago
parent
commit
62afbaf6cd
32 changed files with 800 additions and 572 deletions
  1. 2 5
      data-service/src/main/java/password/pwm/receiver/SummaryBean.java
  2. 2 0
      server/src/main/java/password/pwm/http/filter/SessionFilter.java
  3. 2 2
      server/src/main/java/password/pwm/http/servlet/changepw/ChangePasswordServlet.java
  4. 2 1
      server/src/main/java/password/pwm/http/servlet/configguide/ConfigGuideForm.java
  5. 69 1
      server/src/main/java/password/pwm/http/servlet/configmanager/DebugItemGenerator.java
  6. 1 1
      server/src/main/java/password/pwm/http/servlet/resource/ResourceFileServlet.java
  7. 1 1
      server/src/main/java/password/pwm/http/servlet/resource/ResourceServletService.java
  8. 2 1
      server/src/main/java/password/pwm/ldap/auth/LDAPAuthenticationRequest.java
  9. 2 2
      server/src/main/java/password/pwm/ldap/search/UserSearchEngine.java
  10. 1 1
      server/src/main/java/password/pwm/svc/report/ReportService.java
  11. 4 0
      server/src/main/java/password/pwm/svc/sessiontrack/UserAgentUtils.java
  12. 74 0
      server/src/main/java/password/pwm/svc/stats/AvgStatistic.java
  13. 79 0
      server/src/main/java/password/pwm/svc/stats/DailyKey.java
  14. 49 0
      server/src/main/java/password/pwm/svc/stats/EpsKey.java
  15. 9 21
      server/src/main/java/password/pwm/svc/stats/EpsStatistic.java
  16. 27 0
      server/src/main/java/password/pwm/svc/stats/StatKey.java
  17. 80 117
      server/src/main/java/password/pwm/svc/stats/Statistic.java
  18. 30 0
      server/src/main/java/password/pwm/svc/stats/StatisticType.java
  19. 63 106
      server/src/main/java/password/pwm/svc/stats/StatisticsBundle.java
  20. 41 177
      server/src/main/java/password/pwm/svc/stats/StatisticsManager.java
  21. 1 2
      server/src/main/java/password/pwm/util/EventRateMeter.java
  22. 60 0
      server/src/main/java/password/pwm/util/java/AtomicLoopLongIncrementer.java
  23. 1 2
      server/src/main/java/password/pwm/util/localdb/LocalDBFactory.java
  24. 132 103
      server/src/main/java/password/pwm/util/localdb/LocalDBUtility.java
  25. 1 1
      server/src/main/java/password/pwm/util/localdb/WorkQueueProcessor.java
  26. 2 1
      server/src/main/java/password/pwm/util/operations/PasswordUtility.java
  27. 1 1
      server/src/main/java/password/pwm/util/secure/X509Utils.java
  28. 19 9
      server/src/main/java/password/pwm/ws/server/rest/RestStatisticsServer.java
  29. 2 0
      server/src/main/resources/password/pwm/i18n/Admin.properties
  30. 18 7
      server/src/test/java/password/pwm/i18n/AdminPropertyKeysTest.java
  31. 1 1
      server/src/test/java/password/pwm/util/localdb/LocalDBLoggerExtendedTest.java
  32. 22 9
      webapp/src/main/webapp/WEB-INF/jsp/admin-analysis.jsp

+ 2 - 5
data-service/src/main/java/password/pwm/receiver/SummaryBean.java

@@ -125,11 +125,8 @@ public class SummaryBean
                     final Statistic statistic = Statistic.forKey( statKey );
                     if ( statistic != null )
                     {
-                        if ( statistic.getType() == Statistic.Type.INCREMENTER )
-                        {
-                            final int count = Integer.parseInt( bean.getStatistics().get( statKey ) );
-                            incrementCounterMap( statCount, statistic.getLabel( null ), count );
-                        }
+                        final int count = Integer.parseInt( bean.getStatistics().get( statKey ) );
+                        incrementCounterMap( statCount, statistic.getLabel( null ), count );
                     }
                 }
             }

+ 2 - 0
server/src/main/java/password/pwm/http/filter/SessionFilter.java

@@ -45,6 +45,7 @@ import password.pwm.http.PwmRequest;
 import password.pwm.http.PwmResponse;
 import password.pwm.http.PwmSession;
 import password.pwm.http.PwmURL;
+import password.pwm.svc.stats.AvgStatistic;
 import password.pwm.svc.stats.Statistic;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.TimeDuration;
@@ -137,6 +138,7 @@ public class SessionFilter extends AbstractPwmFilter
 
         final TimeDuration requestExecuteTime = TimeDuration.fromCurrent( startTime );
         pwmRequest.debugHttpRequestToLog( "completed requestID=" + requestID + " in " + requestExecuteTime.asCompactString() );
+        pwmRequest.getPwmApplication().getStatisticsManager().updateAverageValue( AvgStatistic.AVG_REQUEST_PROCESS_TIME, requestExecuteTime.asMillis() );
     }
 
     private ProcessStatus handleStandardRequestOperations(

+ 2 - 2
server/src/main/java/password/pwm/http/servlet/changepw/ChangePasswordServlet.java

@@ -53,7 +53,7 @@ import password.pwm.ldap.auth.AuthenticationType;
 import password.pwm.svc.event.AuditEvent;
 import password.pwm.svc.event.AuditRecord;
 import password.pwm.svc.event.AuditRecordFactory;
-import password.pwm.svc.stats.Statistic;
+import password.pwm.svc.stats.AvgStatistic;
 import password.pwm.util.PasswordData;
 import password.pwm.util.PwmPasswordRuleValidator;
 import password.pwm.util.RandomPasswordGenerator;
@@ -366,7 +366,7 @@ public abstract class ChangePasswordServlet extends ControlledPwmServlet
                 final TimeDuration totalTime = TimeDuration.fromCurrent( progressTracker.getBeginTime() );
                 try
                 {
-                    pwmRequest.getPwmApplication().getStatisticsManager().updateAverageValue( Statistic.AVG_PASSWORD_SYNC_TIME, totalTime.asMillis() );
+                    pwmRequest.getPwmApplication().getStatisticsManager().updateAverageValue( AvgStatistic.AVG_PASSWORD_SYNC_TIME, totalTime.asMillis() );
                     LOGGER.trace( pwmRequest, () -> "password sync process marked completed (" + totalTime.asCompactString() + ")" );
                 }
                 catch ( Exception e )

+ 2 - 1
server/src/main/java/password/pwm/http/servlet/configguide/ConfigGuideForm.java

@@ -38,6 +38,7 @@ import password.pwm.config.value.data.UserPermission;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.http.bean.ConfigGuideBean;
 import password.pwm.util.PasswordData;
+import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.logging.PwmLogger;
 
@@ -129,7 +130,7 @@ public class ConfigGuideForm
             storedConfiguration.writeSetting( PwmSetting.LDAP_SERVER_URLS, LDAP_PROFILE_NAME, newValue, null );
         }
 
-        if ( configGuideBean.isUseConfiguredCerts() )
+        if ( configGuideBean.isUseConfiguredCerts() && !JavaHelper.isEmpty( configGuideBean.getLdapCertificates() ) )
         {
             final StoredValue newStoredValue = new X509CertificateValue( configGuideBean.getLdapCertificates() );
             storedConfiguration.writeSetting( PwmSetting.LDAP_SERVER_CERTS, LDAP_PROFILE_NAME, newStoredValue, null );

+ 69 - 1
server/src/main/java/password/pwm/http/servlet/configmanager/DebugItemGenerator.java

@@ -43,6 +43,9 @@ import password.pwm.ldap.LdapDebugDataGenerator;
 import password.pwm.svc.PwmService;
 import password.pwm.svc.cache.CacheService;
 import password.pwm.svc.node.NodeService;
+import password.pwm.svc.stats.EpsStatistic;
+import password.pwm.svc.stats.Statistic;
+import password.pwm.svc.stats.StatisticsManager;
 import password.pwm.util.LDAPPermissionCalculator;
 import password.pwm.util.java.FileSystemUtility;
 import password.pwm.util.java.JavaHelper;
@@ -67,6 +70,7 @@ import java.io.StringWriter;
 import java.io.Writer;
 import java.lang.management.ManagementFactory;
 import java.lang.management.ThreadInfo;
+import java.math.BigDecimal;
 import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -106,7 +110,9 @@ public class DebugItemGenerator
             LdapRecentUserDebugGenerator.class,
             ClusterInfoDebugGenerator.class,
             CacheServiceDebugItemGenerator.class,
-            RootFileSystemDebugItemGenerator.class
+            RootFileSystemDebugItemGenerator.class,
+            StatisticsDataDebugItemGenerator.class,
+            StatisticsEpsDataDebugItemGenerator.class
     ) );
 
     static void outputZipDebugFile(
@@ -728,6 +734,68 @@ public class DebugItemGenerator
         }
     }
 
+    static class StatisticsDataDebugItemGenerator implements Generator
+    {
+        @Override
+        public String getFilename()
+        {
+            return "statistics.csv";
+        }
+
+        @Override
+        public void outputItem( final PwmApplication pwmApplication, final PwmRequest pwmRequest, final OutputStream outputStream )
+                throws Exception
+        {
+            final StatisticsManager statsManager = pwmApplication.getStatisticsManager();
+            statsManager.outputStatsToCsv( outputStream, pwmRequest.getLocale(), true );
+        }
+    }
+
+    static class StatisticsEpsDataDebugItemGenerator implements Generator
+    {
+        @Override
+        public String getFilename()
+        {
+            return "statistics-eps.csv";
+        }
+
+        @Override
+        public void outputItem( final PwmApplication pwmApplication, final PwmRequest pwmRequest, final OutputStream outputStream )
+                throws Exception
+        {
+            final StatisticsManager statsManager = pwmApplication.getStatisticsManager();
+            final CSVPrinter csvPrinter = JavaHelper.makeCsvPrinter( outputStream );
+            {
+                final List<String> headerRow = new ArrayList<>();
+                headerRow.add( "Counter" );
+                headerRow.add( "Duration" );
+                headerRow.add( "Events/Second" );
+                csvPrinter.printComment( StringUtil.join( headerRow, "," ) );
+            }
+            for ( final EpsStatistic epsStatistic : EpsStatistic.values() )
+            {
+                for ( final Statistic.EpsDuration epsDuration : Statistic.EpsDuration.values() )
+                {
+                    try
+                    {
+                        final List<String> dataRow = new ArrayList<>();
+                        final BigDecimal value = statsManager.readEps( epsStatistic, epsDuration );
+                        final String sValue = value.toPlainString();
+                        dataRow.add( epsStatistic.getLabel( PwmConstants.DEFAULT_LOCALE ) );
+                        dataRow.add( epsDuration.getTimeDuration().asCompactString() );
+                        dataRow.add( sValue );
+                        csvPrinter.printRecord( dataRow );
+                    }
+                    catch ( Exception e )
+                    {
+                        LOGGER.trace( () -> "error generating csv-stats summary info: " + e.getMessage() );
+                    }
+                }
+            }
+            csvPrinter.flush();
+        }
+    }
+
     static class RootFileSystemDebugItemGenerator implements Generator
     {
         @Override

+ 1 - 1
server/src/main/java/password/pwm/http/servlet/resource/ResourceFileServlet.java

@@ -32,7 +32,7 @@ import password.pwm.http.HttpHeader;
 import password.pwm.http.HttpMethod;
 import password.pwm.http.PwmRequest;
 import password.pwm.http.servlet.PwmServlet;
-import password.pwm.svc.stats.EventRateMeter;
+import password.pwm.util.EventRateMeter;
 import password.pwm.svc.stats.Statistic;
 import password.pwm.svc.stats.StatisticsManager;
 import password.pwm.util.java.JavaHelper;

+ 1 - 1
server/src/main/java/password/pwm/http/servlet/resource/ResourceServletService.java

@@ -33,7 +33,7 @@ import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.health.HealthRecord;
 import password.pwm.http.PwmRequest;
 import password.pwm.svc.PwmService;
-import password.pwm.svc.stats.EventRateMeter;
+import password.pwm.util.EventRateMeter;
 import password.pwm.util.java.FileSystemUtility;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.Percent;

+ 2 - 1
server/src/main/java/password/pwm/ldap/auth/LDAPAuthenticationRequest.java

@@ -52,6 +52,7 @@ import password.pwm.svc.event.AuditRecord;
 import password.pwm.svc.event.AuditRecordFactory;
 import password.pwm.svc.intruder.IntruderManager;
 import password.pwm.svc.intruder.RecordType;
+import password.pwm.svc.stats.AvgStatistic;
 import password.pwm.svc.stats.EpsStatistic;
 import password.pwm.svc.stats.Statistic;
 import password.pwm.svc.stats.StatisticsManager;
@@ -349,7 +350,7 @@ class LDAPAuthenticationRequest implements AuthenticationRequest
         final StatisticsManager statisticsManager = pwmApplication.getStatisticsManager();
         statisticsManager.incrementValue( Statistic.AUTHENTICATIONS );
         statisticsManager.updateEps( EpsStatistic.AUTHENTICATION, 1 );
-        statisticsManager.updateAverageValue( Statistic.AVG_AUTHENTICATION_TIME,
+        statisticsManager.updateAverageValue( AvgStatistic.AVG_AUTHENTICATION_TIME,
                 TimeDuration.fromCurrent( startTime ).asMillis() );
 
 

+ 2 - 2
server/src/main/java/password/pwm/ldap/search/UserSearchEngine.java

@@ -46,7 +46,7 @@ import password.pwm.error.PwmOperationalException;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.health.HealthRecord;
 import password.pwm.svc.PwmService;
-import password.pwm.svc.stats.Statistic;
+import password.pwm.svc.stats.AvgStatistic;
 import password.pwm.util.PwmScheduler;
 import password.pwm.util.java.ConditionalTaskExecutor;
 import password.pwm.util.java.JavaHelper;
@@ -533,7 +533,7 @@ public class UserSearchEngine implements PwmService
 
         if ( pwmApplication.getStatisticsManager() != null && pwmApplication.getStatisticsManager().status() == PwmService.STATUS.OPEN )
         {
-            pwmApplication.getStatisticsManager().updateAverageValue( Statistic.AVG_LDAP_SEARCH_TIME, searchDuration.asMillis() );
+            pwmApplication.getStatisticsManager().updateAverageValue( AvgStatistic.AVG_LDAP_SEARCH_TIME, searchDuration.asMillis() );
         }
 
         if ( results.isEmpty() )

+ 1 - 1
server/src/main/java/password/pwm/svc/report/ReportService.java

@@ -40,7 +40,7 @@ import password.pwm.ldap.LdapOperationsHelper;
 import password.pwm.ldap.UserInfo;
 import password.pwm.ldap.UserInfoFactory;
 import password.pwm.svc.PwmService;
-import password.pwm.svc.stats.EventRateMeter;
+import password.pwm.util.EventRateMeter;
 import password.pwm.util.PwmScheduler;
 import password.pwm.util.TransactionSizeCalculator;
 import password.pwm.util.java.BlockingThreadPool;

+ 4 - 0
server/src/main/java/password/pwm/svc/sessiontrack/UserAgentUtils.java

@@ -32,9 +32,11 @@ import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.http.HttpHeader;
 import password.pwm.http.PwmRequest;
 import password.pwm.util.java.StringUtil;
+import password.pwm.util.java.TimeDuration;
 import password.pwm.util.logging.PwmLogger;
 
 import java.io.IOException;
+import java.time.Instant;
 
 public class UserAgentUtils
 {
@@ -62,7 +64,9 @@ public class UserAgentUtils
 
     public static void initializeCache() throws PwmUnrecoverableException
     {
+        final Instant startTime = Instant.now();
         getUserAgentParser();
+        LOGGER.trace( () -> "loaded useragent parser in " + TimeDuration.compactFromCurrent( startTime ) );
     }
 
     public static void checkIfPreIE11( final PwmRequest pwmRequest ) throws PwmUnrecoverableException

+ 74 - 0
server/src/main/java/password/pwm/svc/stats/AvgStatistic.java

@@ -0,0 +1,74 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2018 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+package password.pwm.svc.stats;
+
+import password.pwm.i18n.Admin;
+import password.pwm.util.i18n.LocaleHelper;
+
+import java.util.Locale;
+
+public enum AvgStatistic
+{
+    AVG_PASSWORD_SYNC_TIME( "AvgPasswordSyncTime", null, "ms" ),
+    AVG_AUTHENTICATION_TIME( "AvgAuthenticationTime", null, "ms" ),
+    AVG_PASSWORD_STRENGTH( "AvgPasswordStrength", null, "" ),
+    AVG_LDAP_SEARCH_TIME( "AvgLdapSearchTime", null, "ms" ),
+    AVG_REQUEST_PROCESS_TIME( "AvgRequestProcessTime", null, "ms" ),;
+
+    private final String key;
+    private final Statistic.StatDetail statDetail;
+    private final String unit;
+
+    AvgStatistic(
+            final String key,
+            final Statistic.StatDetail statDetail,
+            final String unit
+    )
+    {
+        this.key = key;
+        this.statDetail = statDetail;
+        this.unit = unit;
+    }
+
+    public String getKey( )
+    {
+        return key;
+    }
+
+    public String getUnit()
+    {
+        return unit;
+    }
+
+    public String getLabel( final Locale locale )
+    {
+        final String keyName = Admin.STATISTICS_LABEL_PREFIX + this.getKey();
+        return LocaleHelper.getLocalizedMessage( locale, keyName, null, Admin.class );
+    }
+
+    public String getDescription( final Locale locale )
+    {
+        final String keyName = Admin.STATISTICS_DESCRIPTION_PREFIX + this.getKey();
+        return LocaleHelper.getLocalizedMessage( locale, keyName, null, Admin.class );
+    }
+}

+ 79 - 0
server/src/main/java/password/pwm/svc/stats/DailyKey.java

@@ -0,0 +1,79 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2018 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+package password.pwm.svc.stats;
+
+import lombok.Value;
+
+import java.time.LocalDate;
+
+@Value
+public class DailyKey
+{
+    private static final String DB_KEY_PREFIX_DAILY = "DAILY_";
+    private int year;
+    private int day;
+
+    private DailyKey()
+    {
+        final LocalDate localDate = LocalDate.now();
+        year = localDate.getYear();
+        day = localDate.getDayOfYear();
+    }
+
+    DailyKey( final String value )
+    {
+        final String strippedValue = value.substring( DB_KEY_PREFIX_DAILY.length() );
+        final String[] splitValue = strippedValue.split( "_" );
+        year = Integer.parseInt( splitValue[ 0 ] );
+        day = Integer.parseInt( splitValue[ 1 ] );
+    }
+
+    private DailyKey( final int year, final int day )
+    {
+        this.year = year;
+        this.day = day;
+    }
+
+    @Override
+    public String toString( )
+    {
+        return DB_KEY_PREFIX_DAILY + year + "_" + day;
+    }
+
+    public static DailyKey forToday()
+    {
+        return new DailyKey( );
+    }
+
+    public DailyKey previous( )
+    {
+        final LocalDate thisDay = localDate();
+        final LocalDate previousDay = thisDay.minusDays( 1 );
+        return new DailyKey( previousDay.getYear(), previousDay.getDayOfYear() );
+    }
+
+    public LocalDate localDate()
+    {
+        return LocalDate.ofYearDay( year, day );
+    }
+}

+ 49 - 0
server/src/main/java/password/pwm/svc/stats/EpsKey.java

@@ -0,0 +1,49 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2018 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+package password.pwm.svc.stats;
+
+import lombok.Value;
+
+import java.util.HashSet;
+import java.util.Set;
+
+@Value
+class EpsKey
+{
+    static final String DB_KEY_PREFIX = "EPS-";
+    private EpsStatistic epsStatistic;
+    private Statistic.EpsDuration epsDuration;
+
+    static Set<EpsKey> allKeys()
+    {
+        final Set<EpsKey> returnSet = new HashSet<>();
+        for ( final EpsStatistic epsStatistic : EpsStatistic.values() )
+        {
+            for ( final Statistic.EpsDuration epsDuration : Statistic.EpsDuration.values() )
+            {
+                returnSet.add( new EpsKey( epsStatistic, epsDuration ) );
+            }
+        }
+        return returnSet;
+    }
+}

+ 9 - 21
server/src/main/java/password/pwm/svc/stats/EpsStatistic.java

@@ -29,27 +29,15 @@ import java.util.Locale;
 
 public enum EpsStatistic
 {
-    REQUESTS( null ),
-    SESSIONS( null ),
-    PASSWORD_CHANGES( Statistic.PASSWORD_CHANGES ),
-    AUTHENTICATION( Statistic.AUTHENTICATIONS ),
-    INTRUDER_ATTEMPTS( Statistic.INTRUDER_ATTEMPTS ),
-    PWMDB_WRITES( null ),
-    PWMDB_READS( null ),
-    DB_WRITES( null ),
-    DB_READS( null ),;
-
-    private Statistic relatedStatistic;
-
-    EpsStatistic( final Statistic relatedStatistic )
-    {
-        this.relatedStatistic = relatedStatistic;
-    }
-
-    public Statistic getRelatedStatistic( )
-    {
-        return relatedStatistic;
-    }
+    REQUESTS(),
+    SESSIONS(),
+    PASSWORD_CHANGES(),
+    AUTHENTICATION(),
+    INTRUDER_ATTEMPTS(),
+    PWMDB_WRITES(),
+    PWMDB_READS(),
+    DB_WRITES(),
+    DB_READS(),;
 
     public String getLabel( final Locale locale )
     {

+ 27 - 0
server/src/main/java/password/pwm/svc/stats/StatKey.java

@@ -0,0 +1,27 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2018 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+package password.pwm.svc.stats;
+
+public interface StatKey
+{
+}

+ 80 - 117
server/src/main/java/password/pwm/svc/stats/Statistic.java

@@ -27,112 +27,101 @@ import password.pwm.config.PwmSetting;
 import password.pwm.i18n.Admin;
 import password.pwm.util.i18n.LocaleHelper;
 import password.pwm.util.java.TimeDuration;
-import password.pwm.util.logging.PwmLogger;
 
 import java.util.Arrays;
 import java.util.Comparator;
 import java.util.Locale;
-import java.util.MissingResourceException;
 import java.util.SortedSet;
 import java.util.TreeSet;
 
 public enum Statistic
 {
-    AUDIT_EVENTS( Type.INCREMENTER, "AuditEvents", null ),
-    AUTHENTICATIONS( Type.INCREMENTER, "Authentications", null ),
-    AUTHENTICATION_FAILURES( Type.INCREMENTER, "AuthenticationFailures", null ),
-    AUTHENTICATION_EXPIRED( Type.INCREMENTER, "Authentications_Expired", null ),
-    AUTHENTICATION_PRE_EXPIRED( Type.INCREMENTER, "Authentications_PreExpired", null ),
-    AUTHENTICATION_EXPIRED_WARNING( Type.INCREMENTER, "Authentications_ExpiredWarning", null ),
-    PWM_STARTUPS( Type.INCREMENTER, "PWM_Startups", null ),
-    PWM_UNKNOWN_ERRORS( Type.INCREMENTER, "PWM_UnknownErrors", null ),
-    PASSWORD_CHANGES( Type.INCREMENTER, "PasswordChanges", null ),
-    FORGOTTEN_USERNAME_FAILURES( Type.INCREMENTER, "ForgottenUsernameFailures", null ),
-    FORGOTTEN_USERNAME_SUCCESSES( Type.INCREMENTER, "ForgottenUsernameSuccesses", null ),
-    EMAIL_SEND_SUCCESSES( Type.INCREMENTER, "EmailSendSuccesses", null ),
-    EMAIL_SEND_FAILURES( Type.INCREMENTER, "EmailSendFailures", null ),
-    EMAIL_SEND_DISCARDS( Type.INCREMENTER, "EmailSendDiscards", null ),
-    SMS_SEND_SUCCESSES( Type.INCREMENTER, "SmsSendSuccesses", null ),
-    SMS_SEND_FAILURES( Type.INCREMENTER, "SmsSendFailures", null ),
-    SMS_SEND_DISCARDS( Type.INCREMENTER, "SmsSendDiscards", null ),
-    PASSWORD_RULE_CHECKS( Type.INCREMENTER, "PasswordRuleChecks", null ),
-    HTTP_REQUESTS( Type.INCREMENTER, "HttpRequests", null ),
-    HTTP_RESOURCE_REQUESTS( Type.INCREMENTER, "HttpResourceRequests", null ),
-    HTTP_SESSIONS( Type.INCREMENTER, "HttpSessions", null ),
-    ACTIVATED_USERS( Type.INCREMENTER, "ActivatedUsers", null ),
-    NEW_USERS( Type.INCREMENTER, "NewUsers", new ConfigSettingDetail( PwmSetting.NEWUSER_ENABLE ) ),
-    GUESTS( Type.INCREMENTER, "Guests", new ConfigSettingDetail( PwmSetting.GUEST_ENABLE ) ),
-    UPDATED_GUESTS( Type.INCREMENTER, "UpdatedGuests", new ConfigSettingDetail( PwmSetting.GUEST_ENABLE ) ),
-    LOCKED_USERS( Type.INCREMENTER, "LockedUsers", null ),
-    LOCKED_ADDRESSES( Type.INCREMENTER, "LockedAddresses", null ),
-    LOCKED_USERIDS( Type.INCREMENTER, "LockedUserDNs", null ),
-    LOCKED_ATTRIBUTES( Type.INCREMENTER, "LockedAttributes", null ),
-    LOCKED_TOKENDESTS( Type.INCREMENTER, "LockedTokenDests", null ),
-    CAPTCHA_SUCCESSES( Type.INCREMENTER, "CaptchaSuccessess", null ),
-    CAPTCHA_FAILURES( Type.INCREMENTER, "CaptchaFailures", null ),
-    CAPTCHA_PRESENTATIONS( Type.INCREMENTER, "CaptchaPresentations", null ),
-    LDAP_UNAVAILABLE_COUNT( Type.INCREMENTER, "LdapUnavailableCount", null ),
-    DB_UNAVAILABLE_COUNT( Type.INCREMENTER, "DatabaseUnavailableCount", null ),
-    SETUP_RESPONSES( Type.INCREMENTER, "SetupResponses", null ),
-    SETUP_OTP_SECRET( Type.INCREMENTER, "SetupOtpSecret", null ),
-    UPDATE_ATTRIBUTES( Type.INCREMENTER, "UpdateAttributes", new ConfigSettingDetail( PwmSetting.UPDATE_PROFILE_ENABLE ) ),
-    SHORTCUTS_SELECTED( Type.INCREMENTER, "ShortcutsSelected", new ConfigSettingDetail( PwmSetting.SHORTCUT_ENABLE ) ),
-    GENERATED_PASSWORDS( Type.INCREMENTER, "GeneratedPasswords", null ),
-    RECOVERY_SUCCESSES( Type.INCREMENTER, "RecoverySuccesses", null ),
-    RECOVERY_FAILURES( Type.INCREMENTER, "RecoveryFailures", null ),
-    TOKENS_SENT( Type.INCREMENTER, "TokensSent", null ),
-    TOKENS_PASSSED( Type.INCREMENTER, "TokensPassed", null ),
-    RECOVERY_TOKENS_SENT( Type.INCREMENTER, "RecoveryTokensSent", null ),
-    RECOVERY_TOKENS_PASSED( Type.INCREMENTER, "RecoveryTokensPassed", null ),
-    RECOVERY_TOKENS_FAILED( Type.INCREMENTER, "RecoveryTokensFailed", null ),
-    RECOVERY_OTP_PASSED( Type.INCREMENTER, "RecoveryOTPPassed", null ),
-    RECOVERY_OTP_FAILED( Type.INCREMENTER, "RecoveryOTPFailed", null ),
-    PEOPLESEARCH_CACHE_HITS( Type.INCREMENTER, "PeopleSearchCacheHits", new ConfigSettingDetail( PwmSetting.PEOPLE_SEARCH_ENABLE ) ),
-    PEOPLESEARCH_CACHE_MISSES( Type.INCREMENTER, "PeopleSearchCacheMisses", new ConfigSettingDetail( PwmSetting.PEOPLE_SEARCH_ENABLE ) ),
-    PEOPLESEARCH_SEARCHES( Type.INCREMENTER, "PeopleSearchSearches", new ConfigSettingDetail( PwmSetting.PEOPLE_SEARCH_ENABLE ) ),
-    PEOPLESEARCH_DETAILS( Type.INCREMENTER, "PeopleSearchDetails", new ConfigSettingDetail( PwmSetting.PEOPLE_SEARCH_ENABLE ) ),
-    PEOPLESEARCH_ORGCHART( Type.INCREMENTER, "PeopleSearchOrgChart", new ConfigSettingDetail( PwmSetting.PEOPLE_SEARCH_ENABLE ) ),
-    PWNOTIFY_JOBS ( Type.INCREMENTER, "PwNotifyJobs", null ),
-    PWNOTIFY_JOB_ERRORS ( Type.INCREMENTER, "PwNotifyJobErrors", null ),
-    PWNOTIFY_EMAILS_SENT ( Type.INCREMENTER, "PwNotifyJobEmailsSent", null ),
-    HELPDESK_PASSWORD_SET( Type.INCREMENTER, "HelpdeskPasswordSet", null ),
-    HELPDESK_USER_LOOKUP( Type.INCREMENTER, "HelpdeskUserLookup", null ),
-    HELPDESK_TOKENS_SENT( Type.INCREMENTER, "HelpdeskTokenSent", null ),
-    HELPDESK_UNLOCK( Type.INCREMENTER, "HelpdeskUnlock", null ),
-    HELPDESK_VERIFY_OTP( Type.INCREMENTER, "HelpdeskVerifyOTP", null ),
-    REST_STATUS( Type.INCREMENTER, "RestStatus", null ),
-    REST_CHECKPASSWORD( Type.INCREMENTER, "RestCheckPassword", null ),
-    REST_SETPASSWORD( Type.INCREMENTER, "RestSetPassword", null ),
-    REST_RANDOMPASSWORD( Type.INCREMENTER, "RestRandomPassword", null ),
-    REST_PROFILE( Type.INCREMENTER, "RestProfile", null ),
-    REST_SIGNING_FORM( Type.INCREMENTER, "RestSigningForm", null ),
-    REST_CHALLENGES( Type.INCREMENTER, "RestChallenges", null ),
-    REST_HEALTH( Type.INCREMENTER, "RestHealth", null ),
-    REST_STATISTICS( Type.INCREMENTER, "RestStatistics", null ),
-    REST_VERIFYCHALLENGES( Type.INCREMENTER, "RestVerifyChallenges", null ),
-    REST_VERIFYOTP( Type.INCREMENTER, "RestVerifyOTP", null ),
-    INTRUDER_ATTEMPTS( Type.INCREMENTER, "IntruderAttempts", null ),
-    FOREIGN_SESSIONS_ACCEPTED( Type.INCREMENTER, "ForeignSessionsAccepted", null ),
-    OBSOLETE_URL_REQUESTS( Type.INCREMENTER, "ObsoleteUrlRequests", null ),
-    SYSLOG_MESSAGES_SENT( Type.INCREMENTER, "SyslogMessagesSent", null ),
+    AUDIT_EVENTS( "AuditEvents", null ),
+    AUTHENTICATIONS( "Authentications", null ),
+    AUTHENTICATION_FAILURES( "AuthenticationFailures", null ),
+    AUTHENTICATION_EXPIRED( "Authentications_Expired", null ),
+    AUTHENTICATION_PRE_EXPIRED( "Authentications_PreExpired", null ),
+    AUTHENTICATION_EXPIRED_WARNING( "Authentications_ExpiredWarning", null ),
+    PWM_STARTUPS( "PWM_Startups", null ),
+    PWM_UNKNOWN_ERRORS( "PWM_UnknownErrors", null ),
+    PASSWORD_CHANGES( "PasswordChanges", null ),
+    FORGOTTEN_USERNAME_FAILURES( "ForgottenUsernameFailures", null ),
+    FORGOTTEN_USERNAME_SUCCESSES( "ForgottenUsernameSuccesses", null ),
+    EMAIL_SEND_SUCCESSES( "EmailSendSuccesses", null ),
+    EMAIL_SEND_FAILURES( "EmailSendFailures", null ),
+    EMAIL_SEND_DISCARDS( "EmailSendDiscards", null ),
+    SMS_SEND_SUCCESSES( "SmsSendSuccesses", null ),
+    SMS_SEND_FAILURES( "SmsSendFailures", null ),
+    SMS_SEND_DISCARDS( "SmsSendDiscards", null ),
+    PASSWORD_RULE_CHECKS( "PasswordRuleChecks", null ),
+    HTTP_REQUESTS( "HttpRequests", null ),
+    HTTP_RESOURCE_REQUESTS( "HttpResourceRequests", null ),
+    HTTP_SESSIONS( "HttpSessions", null ),
+    ACTIVATED_USERS( "ActivatedUsers", null ),
+    NEW_USERS( "NewUsers", new ConfigSettingDetail( PwmSetting.NEWUSER_ENABLE ) ),
+    GUESTS( "Guests", new ConfigSettingDetail( PwmSetting.GUEST_ENABLE ) ),
+    UPDATED_GUESTS( "UpdatedGuests", new ConfigSettingDetail( PwmSetting.GUEST_ENABLE ) ),
+    LOCKED_USERS( "LockedUsers", null ),
+    LOCKED_ADDRESSES( "LockedAddresses", null ),
+    LOCKED_USERIDS( "LockedUserDNs", null ),
+    LOCKED_ATTRIBUTES( "LockedAttributes", null ),
+    LOCKED_TOKENDESTS( "LockedTokenDests", null ),
+    CAPTCHA_SUCCESSES( "CaptchaSuccessess", null ),
+    CAPTCHA_FAILURES( "CaptchaFailures", null ),
+    CAPTCHA_PRESENTATIONS( "CaptchaPresentations", null ),
+    LDAP_UNAVAILABLE_COUNT( "LdapUnavailableCount", null ),
+    DB_UNAVAILABLE_COUNT( "DatabaseUnavailableCount", null ),
+    SETUP_RESPONSES( "SetupResponses", null ),
+    SETUP_OTP_SECRET( "SetupOtpSecret", null ),
+    UPDATE_ATTRIBUTES( "UpdateAttributes", new ConfigSettingDetail( PwmSetting.UPDATE_PROFILE_ENABLE ) ),
+    SHORTCUTS_SELECTED( "ShortcutsSelected", new ConfigSettingDetail( PwmSetting.SHORTCUT_ENABLE ) ),
+    GENERATED_PASSWORDS( "GeneratedPasswords", null ),
+    RECOVERY_SUCCESSES( "RecoverySuccesses", null ),
+    RECOVERY_FAILURES( "RecoveryFailures", null ),
+    TOKENS_SENT( "TokensSent", null ),
+    TOKENS_PASSSED( "TokensPassed", null ),
+    RECOVERY_TOKENS_SENT( "RecoveryTokensSent", null ),
+    RECOVERY_TOKENS_PASSED( "RecoveryTokensPassed", null ),
+    RECOVERY_TOKENS_FAILED( "RecoveryTokensFailed", null ),
+    RECOVERY_OTP_PASSED( "RecoveryOTPPassed", null ),
+    RECOVERY_OTP_FAILED( "RecoveryOTPFailed", null ),
+    PEOPLESEARCH_CACHE_HITS( "PeopleSearchCacheHits", new ConfigSettingDetail( PwmSetting.PEOPLE_SEARCH_ENABLE ) ),
+    PEOPLESEARCH_CACHE_MISSES( "PeopleSearchCacheMisses", new ConfigSettingDetail( PwmSetting.PEOPLE_SEARCH_ENABLE ) ),
+    PEOPLESEARCH_SEARCHES( "PeopleSearchSearches", new ConfigSettingDetail( PwmSetting.PEOPLE_SEARCH_ENABLE ) ),
+    PEOPLESEARCH_DETAILS( "PeopleSearchDetails", new ConfigSettingDetail( PwmSetting.PEOPLE_SEARCH_ENABLE ) ),
+    PEOPLESEARCH_ORGCHART( "PeopleSearchOrgChart", new ConfigSettingDetail( PwmSetting.PEOPLE_SEARCH_ENABLE ) ),
+    PWNOTIFY_JOBS ( "PwNotifyJobs", null ),
+    PWNOTIFY_JOB_ERRORS ( "PwNotifyJobErrors", null ),
+    PWNOTIFY_EMAILS_SENT ( "PwNotifyJobEmailsSent", null ),
+    HELPDESK_PASSWORD_SET( "HelpdeskPasswordSet", null ),
+    HELPDESK_USER_LOOKUP( "HelpdeskUserLookup", null ),
+    HELPDESK_TOKENS_SENT( "HelpdeskTokenSent", null ),
+    HELPDESK_UNLOCK( "HelpdeskUnlock", null ),
+    HELPDESK_VERIFY_OTP( "HelpdeskVerifyOTP", null ),
+    REST_STATUS( "RestStatus", null ),
+    REST_CHECKPASSWORD( "RestCheckPassword", null ),
+    REST_SETPASSWORD( "RestSetPassword", null ),
+    REST_RANDOMPASSWORD( "RestRandomPassword", null ),
+    REST_PROFILE( "RestProfile", null ),
+    REST_SIGNING_FORM( "RestSigningForm", null ),
+    REST_CHALLENGES( "RestChallenges", null ),
+    REST_HEALTH( "RestHealth", null ),
+    REST_STATISTICS( "RestStatistics", null ),
+    REST_VERIFYCHALLENGES( "RestVerifyChallenges", null ),
+    REST_VERIFYOTP( "RestVerifyOTP", null ),
+    INTRUDER_ATTEMPTS( "IntruderAttempts", null ),
+    FOREIGN_SESSIONS_ACCEPTED( "ForeignSessionsAccepted", null ),
+    OBSOLETE_URL_REQUESTS( "ObsoleteUrlRequests", null ),
+    SYSLOG_MESSAGES_SENT( "SyslogMessagesSent", null ),;
 
-    AVG_PASSWORD_SYNC_TIME( Type.AVERAGE, "AvgPasswordSyncTime", null ),
-    AVG_AUTHENTICATION_TIME( Type.AVERAGE, "AvgAuthenticationTime", null ),
-    AVG_PASSWORD_STRENGTH( Type.AVERAGE, "AvgPasswordStrength", null ),
-    AVG_LDAP_SEARCH_TIME( Type.AVERAGE, "AvgLdapSearchTime", null ),;
-
-    private static final PwmLogger LOGGER = PwmLogger.forClass( Statistic.class );
-    private final Type type;
     private final String key;
     private final StatDetail statDetail;
 
     Statistic(
-            final Type type,
             final String key,
             final StatDetail statDetail
     )
     {
-        this.type = type;
         this.key = key;
         this.statDetail = statDetail;
     }
@@ -142,11 +131,6 @@ public enum Statistic
         return key;
     }
 
-    public Type getType( )
-    {
-        return type;
-    }
-
     public boolean isActive( final PwmApplication pwmApplication )
     {
         if ( statDetail == null )
@@ -164,37 +148,16 @@ public enum Statistic
         return set;
     }
 
-    public enum Type
-    {
-        INCREMENTER,
-        AVERAGE,
-    }
-
     public String getLabel( final Locale locale )
     {
-        try
-        {
-            final String keyName = Admin.STATISTICS_LABEL_PREFIX + this.getKey();
-            return LocaleHelper.getLocalizedMessage( locale, keyName, null, Admin.class );
-        }
-        catch ( MissingResourceException e )
-        {
-            return "MISSING STATISTIC LABEL for " + this.getKey();
-        }
+        final String keyName = Admin.STATISTICS_LABEL_PREFIX + this.getKey();
+        return LocaleHelper.getLocalizedMessage( locale, keyName, null, Admin.class );
     }
 
     public String getDescription( final Locale locale )
     {
         final String keyName = Admin.STATISTICS_DESCRIPTION_PREFIX + this.getKey();
-        try
-        {
-            return LocaleHelper.getLocalizedMessage( locale, keyName, null, Admin.class );
-        }
-        catch ( Exception e )
-        {
-            LOGGER.error( "unable to load localization for " + keyName + ", error: " + e.getMessage() );
-            return "missing localization for " + keyName;
-        }
+        return LocaleHelper.getLocalizedMessage( locale, keyName, null, Admin.class );
     }
 
     public enum EpsDuration

+ 30 - 0
server/src/main/java/password/pwm/svc/stats/StatisticType.java

@@ -0,0 +1,30 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2018 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+package password.pwm.svc.stats;
+
+public enum StatisticType
+{
+    INCREMENTER,
+    AVERAGE,
+    EPS,
+}

+ 63 - 106
server/src/main/java/password/pwm/svc/stats/StatisticsBundle.java

@@ -22,153 +22,105 @@
 
 package password.pwm.svc.stats;
 
+import password.pwm.util.java.AtomicLoopLongIncrementer;
+import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.util.java.StringUtil;
-import password.pwm.util.logging.PwmLogger;
 
 import java.io.Serializable;
 import java.math.BigInteger;
-import java.text.SimpleDateFormat;
-import java.util.HashMap;
+import java.util.EnumMap;
+import java.util.LinkedHashMap;
 import java.util.Map;
-import java.util.TimeZone;
 
 public class StatisticsBundle
 {
+    private final Map<Statistic, AtomicLoopLongIncrementer> incrementerMap = new EnumMap<>( Statistic.class );
+    private final Map<AvgStatistic, AverageBean> avgMap = new EnumMap<>( AvgStatistic.class );
 
-    private static final PwmLogger LOGGER = PwmLogger.forClass( StatisticsBundle.class );
-
-    static final SimpleDateFormat STORED_DATETIME_FORMATTER = new SimpleDateFormat( "yyyy-MM-dd HH:mm:ss Z" );
-
-    static
-    {
-        STORED_DATETIME_FORMATTER.setTimeZone( TimeZone.getTimeZone( "Zulu" ) );
-    }
-
-
-    private final Map<Statistic, String> valueMap = new HashMap<>();
-
-    public StatisticsBundle( )
+    StatisticsBundle( )
     {
+        for ( final Statistic statistic : Statistic.values() )
+        {
+            incrementerMap.put( statistic, new AtomicLoopLongIncrementer( 0, Long.MAX_VALUE ) );
+        }
+        for ( final AvgStatistic avgStatistic : AvgStatistic.values() )
+        {
+            avgMap.put( avgStatistic, new AverageBean() );
+        }
     }
 
     public String output( )
     {
-        return JsonUtil.serializeMap( valueMap );
-    }
+        final Map<String, String> outputMap = new LinkedHashMap<>();
 
-    public static StatisticsBundle input( final String inputString )
-    {
-        final Map<Statistic, String> srcMap = new HashMap<>();
-        final Map<String, String> loadedMap = JsonUtil.deserializeStringMap( inputString );
-        for ( final Map.Entry<String, String> entry : loadedMap.entrySet() )
+        for ( final Statistic statistic : Statistic.values() )
         {
-            final String key = entry.getKey();
-            try
+            final long currentValue = incrementerMap.get( statistic ).get();
+            if ( currentValue > 0 )
             {
-                srcMap.put( Statistic.valueOf( key ), entry.getValue() );
-            }
-            catch ( IllegalArgumentException e )
-            {
-                LOGGER.error( "error parsing statistic key '" + key + "', reason: " + e.getMessage() );
+                outputMap.put( statistic.name(), Long.toString( currentValue ) );
             }
         }
-        final StatisticsBundle bundle = new StatisticsBundle();
-
-        for ( final Statistic loopStat : Statistic.values() )
+        for ( final AvgStatistic epsStatistic : AvgStatistic.values() )
         {
-            final String value = srcMap.get( loopStat );
-            if ( !StringUtil.isEmpty( value ) )
+            final AverageBean averageBean = avgMap.get( epsStatistic );
+            if ( !averageBean.isZero() )
             {
-                bundle.valueMap.put( loopStat, value );
+                outputMap.put( epsStatistic.name(), JsonUtil.serialize( averageBean ) );
             }
         }
 
-        return bundle;
+        return JsonUtil.serializeMap( outputMap );
     }
 
-    public synchronized void incrementValue( final Statistic statistic )
+    public static StatisticsBundle input( final String inputString )
     {
-        if ( Statistic.Type.INCREMENTER != statistic.getType() )
-        {
-            LOGGER.error( "attempt to increment non-counter/incremental stat " + statistic );
-            return;
-        }
+        final Map<String, String> loadedMap = JsonUtil.deserializeStringMap( inputString );
+        final StatisticsBundle bundle = new StatisticsBundle();
 
-        BigInteger currentValue = BigInteger.ZERO;
-        try
+        for ( final Statistic loopStat : Statistic.values() )
         {
-            if ( valueMap.containsKey( statistic ) )
-            {
-                currentValue = new BigInteger( valueMap.get( statistic ) );
-            }
-            else
+            final String value = loadedMap.get( loopStat.name() );
+            if ( !StringUtil.isEmpty( value ) )
             {
-                currentValue = BigInteger.ZERO;
+                final long longValue = JavaHelper.silentParseLong( value, 0 );
+                final AtomicLoopLongIncrementer incrementer = new AtomicLoopLongIncrementer( longValue, Long.MAX_VALUE );
+                bundle.incrementerMap.put( loopStat, incrementer );
             }
         }
-        catch ( NumberFormatException e )
-        {
-            LOGGER.error( "error reading counter/incremental stat " + statistic );
-        }
-        final BigInteger newValue = currentValue.add( BigInteger.ONE );
-        valueMap.put( statistic, newValue.toString() );
-    }
-
-    public synchronized void updateAverageValue( final Statistic statistic, final long timeDuration )
-    {
-        if ( Statistic.Type.AVERAGE != statistic.getType() )
-        {
-            LOGGER.error( "attempt to update average value of non-average stat " + statistic );
-            return;
-        }
 
-        final String avgStrValue = valueMap.get( statistic );
-
-        AverageBean avgBean = new AverageBean();
-        if ( avgStrValue != null && avgStrValue.length() > 0 )
+        for ( final AvgStatistic loopStat : AvgStatistic.values() )
         {
-            try
-            {
-                avgBean = JsonUtil.deserialize( avgStrValue, AverageBean.class );
-            }
-            catch ( Exception e )
+            final String value = loadedMap.get( loopStat.name() );
+            if ( !StringUtil.isEmpty( value ) )
             {
-                LOGGER.trace( () -> "unable to parse statistics value for stat " + statistic.toString() + ", value=" + avgStrValue );
+                final AverageBean avgBean = JsonUtil.deserialize( value, AverageBean.class );
+                bundle.avgMap.put( loopStat, avgBean );
             }
         }
 
-        avgBean.appendValue( timeDuration );
-        valueMap.put( statistic, JsonUtil.serialize( avgBean ) );
+        return bundle;
+    }
+
+    void incrementValue( final Statistic statistic )
+    {
+        incrementerMap.get( statistic ).incrementAndGet();
+    }
+
+    void updateAverageValue( final AvgStatistic statistic, final long timeDuration )
+    {
+        avgMap.get( statistic ).appendValue( timeDuration );
     }
 
     public String getStatistic( final Statistic statistic )
     {
-        switch ( statistic.getType() )
-        {
-            case INCREMENTER:
-                return valueMap.containsKey( statistic ) ? valueMap.get( statistic ) : "0";
-
-            case AVERAGE:
-                final String avgStrValue = valueMap.get( statistic );
-
-                AverageBean avgBean = new AverageBean();
-                if ( avgStrValue != null && avgStrValue.length() > 0 )
-                {
-                    try
-                    {
-                        avgBean = JsonUtil.deserialize( avgStrValue, AverageBean.class );
-                    }
-                    catch ( Exception e )
-                    {
-                        LOGGER.trace( () ->  "unable to parse statistics value for stat " + statistic.toString() + ", value=" + avgStrValue );
-                    }
-                }
-                return avgBean.getAverage().toString();
-
-            default:
-                return "";
-        }
+        return Long.toString( incrementerMap.get( statistic ).get() );
+    }
+
+    public String getAvgStatistic( final AvgStatistic statistic )
+    {
+        return avgMap.get( statistic ).getAverage().toString();
     }
 
     private static class AverageBean implements Serializable
@@ -180,7 +132,7 @@ public class StatisticsBundle
         {
         }
 
-        BigInteger getAverage( )
+        synchronized BigInteger getAverage( )
         {
             if ( BigInteger.ZERO.equals( count ) )
             {
@@ -190,10 +142,15 @@ public class StatisticsBundle
             return total.divide( count );
         }
 
-        void appendValue( final long value )
+        synchronized void appendValue( final long value )
         {
             count = count.add( BigInteger.ONE );
             total = total.add( BigInteger.valueOf( value ) );
         }
+
+        synchronized boolean isZero()
+        {
+            return total.equals( BigInteger.ZERO );
+        }
     }
 }

+ 41 - 177
server/src/main/java/password/pwm/svc/stats/StatisticsManager.java

@@ -30,9 +30,10 @@ import password.pwm.error.PwmException;
 import password.pwm.health.HealthRecord;
 import password.pwm.http.PwmRequest;
 import password.pwm.svc.PwmService;
+import password.pwm.util.EventRateMeter;
 import password.pwm.util.PwmScheduler;
 import password.pwm.util.java.JavaHelper;
-import password.pwm.util.java.JsonUtil;
+import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.TimeDuration;
 import password.pwm.util.localdb.LocalDB;
 import password.pwm.util.localdb.LocalDBException;
@@ -41,19 +42,14 @@ import password.pwm.util.logging.PwmLogger;
 import java.io.IOException;
 import java.io.OutputStream;
 import java.math.BigDecimal;
-import java.text.DateFormat;
-import java.text.SimpleDateFormat;
 import java.time.Instant;
 import java.util.ArrayList;
-import java.util.Calendar;
 import java.util.Collections;
-import java.util.Date;
 import java.util.HashMap;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
-import java.util.TimeZone;
 import java.util.TimerTask;
 import java.util.concurrent.ExecutorService;
 
@@ -68,7 +64,6 @@ public class StatisticsManager implements PwmService
     private static final String DB_KEY_VERSION = "STATS_VERSION";
     private static final String DB_KEY_CUMULATIVE = "CUMULATIVE";
     private static final String DB_KEY_INITIAL_DAILY_KEY = "INITIAL_DAILY_KEY";
-    private static final String DB_KEY_PREFIX_DAILY = "DAILY_";
     private static final String DB_KEY_TEMP = "TEMP_KEY";
 
     private static final String DB_VALUE_VERSION = "1";
@@ -78,15 +73,15 @@ public class StatisticsManager implements PwmService
 
     private LocalDB localDB;
 
-    private DailyKey currentDailyKey = new DailyKey( new Date() );
-    private DailyKey initialDailyKey = new DailyKey( new Date() );
+    private DailyKey currentDailyKey = DailyKey.forToday();
+    private DailyKey initialDailyKey = DailyKey.forToday();
 
     private ExecutorService executorService;
 
     private final StatisticsBundle statsCurrent = new StatisticsBundle();
     private StatisticsBundle statsDaily = new StatisticsBundle();
     private StatisticsBundle statsCummulative = new StatisticsBundle();
-    private Map<String, EventRateMeter> epsMeterMap = new HashMap<>();
+    private Map<EpsKey, EventRateMeter> epsMeterMap = new HashMap<>();
 
     private PwmApplication pwmApplication;
 
@@ -104,16 +99,20 @@ public class StatisticsManager implements PwmService
 
     public StatisticsManager( )
     {
+        for ( final EpsKey epsKey : EpsKey.allKeys() )
+        {
+            epsMeterMap.put( epsKey, new EventRateMeter( epsKey.getEpsDuration().getTimeDuration() ) );
+        }
     }
 
-    public synchronized void incrementValue( final Statistic statistic )
+    public void incrementValue( final Statistic statistic )
     {
         statsCurrent.incrementValue( statistic );
         statsDaily.incrementValue( statistic );
         statsCummulative.incrementValue( statistic );
     }
 
-    public synchronized void updateAverageValue( final Statistic statistic, final long value )
+    public void updateAverageValue( final AvgStatistic statistic, final long value )
     {
         statsCurrent.updateAverageValue( statistic, value );
         statsDaily.updateAverageValue( statistic, value );
@@ -130,7 +129,7 @@ public class StatisticsManager implements PwmService
             final StatisticsBundle bundle = getStatBundleForKey( loopKey.toString() );
             if ( bundle != null )
             {
-                final String key = ( new SimpleDateFormat( "MMM dd" ) ).format( loopKey.calendar().getTime() );
+                final String key = loopKey.toString();
                 final String value = bundle.getStatistic( statistic );
                 returnMap.put( key, value );
             }
@@ -171,7 +170,7 @@ public class StatisticsManager implements PwmService
         {
             final String storedStat = localDB.get( LocalDB.DB.PWM_STATS, key );
             final StatisticsBundle returnBundle;
-            if ( storedStat != null && storedStat.length() > 0 )
+            if ( !StringUtil.isEmpty( storedStat ) )
             {
                 returnBundle = StatisticsBundle.input( storedStat );
             }
@@ -192,13 +191,9 @@ public class StatisticsManager implements PwmService
 
     public Map<DailyKey, String> getAvailableKeys( final Locale locale )
     {
-        final DateFormat dateFormatter = SimpleDateFormat.getDateInstance( SimpleDateFormat.DEFAULT, locale );
-        final Map<DailyKey, String> returnMap = new LinkedHashMap<DailyKey, String>();
-
-        // add current time;
-        returnMap.put( currentDailyKey, dateFormatter.format( new Date() ) );
+        final Map<DailyKey, String> returnMap = new LinkedHashMap<>();
 
-        // if now historical data then we're done
+        // if no historical data then we're done
         if ( currentDailyKey.equals( initialDailyKey ) )
         {
             return returnMap;
@@ -208,8 +203,7 @@ public class StatisticsManager implements PwmService
         int safetyCounter = 0;
         while ( !loopKey.equals( initialDailyKey ) && safetyCounter < 5000 )
         {
-            final Calendar c = loopKey.calendar();
-            final String display = dateFormatter.format( c.getTime() );
+            final String display = loopKey.toString();
             returnMap.put( loopKey, display );
             loopKey = loopKey.previous();
             safetyCounter++;
@@ -239,14 +233,6 @@ public class StatisticsManager implements PwmService
 
     public void init( final PwmApplication pwmApplication ) throws PwmException
     {
-        for ( final EpsStatistic type : EpsStatistic.values() )
-        {
-            for ( final Statistic.EpsDuration duration : Statistic.EpsDuration.values() )
-            {
-                epsMeterMap.put( type.toString() + duration.toString(), new EventRateMeter( duration.getTimeDuration() ) );
-            }
-        }
-
         status = STATUS.OPENING;
         this.localDB = pwmApplication.getLocalDB();
         this.pwmApplication = pwmApplication;
@@ -259,56 +245,32 @@ public class StatisticsManager implements PwmService
         }
 
         {
-            final String storedCummulativeBundleStr = localDB.get( LocalDB.DB.PWM_STATS, DB_KEY_CUMULATIVE );
-            if ( storedCummulativeBundleStr != null && storedCummulativeBundleStr.length() > 0 )
+            final String storedCumulativeBundleSir = localDB.get( LocalDB.DB.PWM_STATS, DB_KEY_CUMULATIVE );
+            if ( !StringUtil.isEmpty( storedCumulativeBundleSir ) )
             {
                 try
                 {
-                    statsCummulative = StatisticsBundle.input( storedCummulativeBundleStr );
+                    statsCummulative = StatisticsBundle.input( storedCumulativeBundleSir );
                 }
                 catch ( Exception e )
                 {
-                    LOGGER.warn( "error loading saved stored statistics: " + e.getMessage() );
+                    LOGGER.warn( "error loading saved stored cumulative statistics: " + e.getMessage() );
                 }
             }
         }
 
-        {
-            for ( final EpsStatistic loopEpsType : EpsStatistic.values() )
-            {
-                for ( final EpsStatistic loopEpsDuration : EpsStatistic.values() )
-                {
-                    final String key = "EPS-" + loopEpsType.toString() + loopEpsDuration.toString();
-                    final String storedValue = localDB.get( LocalDB.DB.PWM_STATS, key );
-                    if ( storedValue != null && storedValue.length() > 0 )
-                    {
-                        try
-                        {
-                            final EventRateMeter eventRateMeter = JsonUtil.deserialize( storedValue, EventRateMeter.class );
-                            epsMeterMap.put( loopEpsType.toString() + loopEpsDuration.toString(), eventRateMeter );
-                        }
-                        catch ( Exception e )
-                        {
-                            LOGGER.error( "unexpected error reading last EPS rate for " + loopEpsType + " from LocalDB: " + e.getMessage() );
-                        }
-                    }
-                }
-            }
-
-        }
-
         {
             final String storedInitialString = localDB.get( LocalDB.DB.PWM_STATS, DB_KEY_INITIAL_DAILY_KEY );
-            if ( storedInitialString != null && storedInitialString.length() > 0 )
+            if ( !StringUtil.isEmpty( storedInitialString ) )
             {
                 initialDailyKey = new DailyKey( storedInitialString );
             }
         }
 
         {
-            currentDailyKey = new DailyKey( new Date() );
+            currentDailyKey = DailyKey.forToday();
             final String storedDailyStr = localDB.get( LocalDB.DB.PWM_STATS, currentDailyKey.toString() );
-            if ( storedDailyStr != null && storedDailyStr.length() > 0 )
+            if ( !StringUtil.isEmpty( storedDailyStr ) )
             {
                 statsDaily = StatisticsBundle.input( storedDailyStr );
             }
@@ -316,7 +278,7 @@ public class StatisticsManager implements PwmService
 
         try
         {
-            localDB.put( LocalDB.DB.PWM_STATS, DB_KEY_TEMP, JavaHelper.toIsoDate( new Date() ) );
+            localDB.put( LocalDB.DB.PWM_STATS, DB_KEY_TEMP, JavaHelper.toIsoDate( Instant.now() ) );
         }
         catch ( IllegalStateException e )
         {
@@ -340,30 +302,20 @@ public class StatisticsManager implements PwmService
 
     private void writeDbValues( )
     {
-        if ( localDB != null )
+        if ( localDB != null && status == STATUS.OPEN )
         {
             try
             {
-                localDB.put( LocalDB.DB.PWM_STATS, DB_KEY_CUMULATIVE, statsCummulative.output() );
-                localDB.put( LocalDB.DB.PWM_STATS, currentDailyKey.toString(), statsDaily.output() );
-
-                for ( final EpsStatistic loopEpsType : EpsStatistic.values() )
-                {
-                    for ( final Statistic.EpsDuration loopEpsDuration : Statistic.EpsDuration.values() )
-                    {
-                        final String key = "EPS-" + loopEpsType.toString();
-                        final String mapKey = loopEpsType.toString() + loopEpsDuration.toString();
-                        final String value = JsonUtil.serialize( this.epsMeterMap.get( mapKey ) );
-                        localDB.put( LocalDB.DB.PWM_STATS, key, value );
-                    }
-                }
+                final Map<String, String> dbData = new LinkedHashMap<>();
+                dbData.put( DB_KEY_CUMULATIVE, statsCummulative.output() );
+                dbData.put( currentDailyKey.toString(), statsDaily.output() );
+                localDB.putAll( LocalDB.DB.PWM_STATS, dbData );
             }
             catch ( LocalDBException e )
             {
                 LOGGER.error( "error outputting pwm statistics: " + e.getMessage() );
             }
         }
-
     }
 
     public Map<String, String> dailyStatisticsAsLabelValueMap()
@@ -381,7 +333,7 @@ public class StatisticsManager implements PwmService
 
     private void resetDailyStats( )
     {
-        currentDailyKey = new DailyKey( new Date() );
+        currentDailyKey = DailyKey.forToday();
         statsDaily = new StatisticsBundle();
         LOGGER.debug( () -> "reset daily statistics" );
     }
@@ -431,102 +383,19 @@ public class StatisticsManager implements PwmService
         }
     }
 
-
-    public static class DailyKey
-    {
-        int year;
-        int day;
-
-        public DailyKey( final Date date )
-        {
-            final Calendar calendar = Calendar.getInstance( TimeZone.getTimeZone( "Zulu" ) );
-            calendar.setTime( date );
-            year = calendar.get( Calendar.YEAR );
-            day = calendar.get( Calendar.DAY_OF_YEAR );
-        }
-
-        public DailyKey( final String value )
-        {
-            final String strippedValue = value.substring( DB_KEY_PREFIX_DAILY.length() );
-            final String[] splitValue = strippedValue.split( "_" );
-            year = Integer.parseInt( splitValue[ 0 ] );
-            day = Integer.parseInt( splitValue[ 1 ] );
-        }
-
-        private DailyKey( )
-        {
-        }
-
-        @Override
-        public String toString( )
-        {
-            return DB_KEY_PREFIX_DAILY + year + "_" + day;
-        }
-
-        public DailyKey previous( )
-        {
-            final Calendar calendar = calendar();
-            calendar.add( Calendar.HOUR, -24 );
-            final DailyKey newKey = new DailyKey();
-            newKey.year = calendar.get( Calendar.YEAR );
-            newKey.day = calendar.get( Calendar.DAY_OF_YEAR );
-            return newKey;
-        }
-
-        public Calendar calendar( )
-        {
-            final Calendar calendar = Calendar.getInstance( TimeZone.getTimeZone( "Zulu" ) );
-            calendar.set( Calendar.YEAR, year );
-            calendar.set( Calendar.DAY_OF_YEAR, day );
-            return calendar;
-        }
-
-        @Override
-        public boolean equals( final Object o )
-        {
-            if ( this == o )
-            {
-                return true;
-            }
-            if ( o == null || getClass() != o.getClass() )
-            {
-                return false;
-            }
-
-            final DailyKey key = ( DailyKey ) o;
-
-            if ( day != key.day )
-            {
-                return false;
-            }
-            if ( year != key.year )
-            {
-                return false;
-            }
-
-            return true;
-        }
-
-        @Override
-        public int hashCode( )
-        {
-            int result = year;
-            result = 31 * result + day;
-            return result;
-        }
-    }
-
     public void updateEps( final EpsStatistic type, final int itemCount )
     {
         for ( final Statistic.EpsDuration duration : Statistic.EpsDuration.values() )
         {
-            epsMeterMap.get( type.toString() + duration.toString() ).markEvents( itemCount );
+            final EpsKey epsKey = new EpsKey( type, duration );
+            epsMeterMap.get( epsKey ).markEvents( itemCount );
         }
     }
 
     public BigDecimal readEps( final EpsStatistic type, final Statistic.EpsDuration duration )
     {
-        return epsMeterMap.get( type.toString() + duration.toString() ).readEventRate();
+        final EpsKey epsKey = new EpsKey( type, duration );
+        return epsMeterMap.get( epsKey ).readEventRate();
     }
 
 
@@ -553,15 +422,15 @@ public class StatisticsManager implements PwmService
         }
 
         int counter = 0;
-        final Map<StatisticsManager.DailyKey, String> keys = statsManger.getAvailableKeys( PwmConstants.DEFAULT_LOCALE );
-        for ( final StatisticsManager.DailyKey loopKey : keys.keySet() )
+        final Map<DailyKey, String> keys = statsManger.getAvailableKeys( PwmConstants.DEFAULT_LOCALE );
+        for ( final DailyKey loopKey : keys.keySet() )
         {
             counter++;
             final StatisticsBundle bundle = statsManger.getStatBundleForKey( loopKey.toString() );
             final List<String> lineOutput = new ArrayList<>();
             lineOutput.add( loopKey.toString() );
-            lineOutput.add( String.valueOf( loopKey.year ) );
-            lineOutput.add( String.valueOf( loopKey.day ) );
+            lineOutput.add( String.valueOf( loopKey.getYear() ) );
+            lineOutput.add( String.valueOf( loopKey.getDay() ) );
             for ( final Statistic stat : Statistic.values() )
             {
                 lineOutput.add( bundle.getStatistic( stat ) );
@@ -580,14 +449,9 @@ public class StatisticsManager implements PwmService
 
     public ServiceInfoBean serviceInfo( )
     {
-        if ( status() == STATUS.OPEN )
-        {
-            return new ServiceInfoBean( Collections.singletonList( DataStorageMethod.LOCALDB ) );
-        }
-        else
-        {
-            return new ServiceInfoBean( Collections.<DataStorageMethod>emptyList() );
-        }
+        return status() == STATUS.OPEN
+                ? new ServiceInfoBean( Collections.singletonList( DataStorageMethod.LOCALDB ) )
+                : new ServiceInfoBean( Collections.emptyList() );
     }
 
     public static void incrementStat(

+ 1 - 2
server/src/main/java/password/pwm/svc/stats/EventRateMeter.java → server/src/main/java/password/pwm/util/EventRateMeter.java

@@ -20,7 +20,7 @@
  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
  */
 
-package password.pwm.svc.stats;
+package password.pwm.util;
 
 import password.pwm.util.java.TimeDuration;
 
@@ -29,7 +29,6 @@ import java.math.BigDecimal;
 
 public class EventRateMeter implements Serializable
 {
-
     private final TimeDuration maxDuration;
 
     private MovingAverage movingAverage;

+ 60 - 0
server/src/main/java/password/pwm/util/java/AtomicLoopLongIncrementer.java

@@ -0,0 +1,60 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2018 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+package password.pwm.util.java;
+
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * Thread safe rotating int incrementer with configurable floor and ceiling values.
+ */
+public class AtomicLoopLongIncrementer
+{
+    private final AtomicLong incrementer;
+    private final long ceiling;
+    private final long floor;
+
+    public AtomicLoopLongIncrementer( final long initialValue, final long ceiling )
+    {
+        this.ceiling = ceiling;
+        this.floor = 0;
+        incrementer = new AtomicLong( JavaHelper.rangeCheck( floor, ceiling, initialValue ) );
+    }
+
+    public long get()
+    {
+        return incrementer.get();
+    }
+
+    public long incrementAndGet( )
+    {
+        return incrementer.getAndUpdate( operand ->
+        {
+            operand++;
+            if ( operand >= ceiling )
+            {
+                operand = floor;
+            }
+            return operand;
+        } );
+    }
+}

+ 1 - 2
server/src/main/java/password/pwm/util/localdb/LocalDBFactory.java

@@ -94,8 +94,7 @@ public class LocalDBFactory
             if ( localDBUtility.readImportInprogressFlag() )
             {
                 LOGGER.error( "previous database import process did not complete successfully, clearing all data" );
-                localDBUtility.prepareForImport();
-                localDBUtility.markImportComplete();
+                localDBUtility.cancelImportProcess();
             }
         }
 

+ 132 - 103
server/src/main/java/password/pwm/util/localdb/LocalDBUtility.java

@@ -24,16 +24,17 @@ package password.pwm.util.localdb;
 
 import org.apache.commons.csv.CSVPrinter;
 import org.apache.commons.csv.CSVRecord;
-import org.apache.commons.io.IOUtils;
 import org.apache.commons.io.input.CountingInputStream;
 import password.pwm.PwmApplication;
 import password.pwm.PwmConstants;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmOperationalException;
-import password.pwm.svc.stats.EventRateMeter;
+import password.pwm.util.EventRateMeter;
 import password.pwm.util.ProgressInfo;
 import password.pwm.util.TransactionSizeCalculator;
+import password.pwm.util.java.ConditionalTaskExecutor;
 import password.pwm.util.java.JavaHelper;
+import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.TimeDuration;
 import password.pwm.util.logging.PwmLogger;
 
@@ -45,7 +46,7 @@ import java.io.InputStreamReader;
 import java.io.OutputStream;
 import java.io.PrintStream;
 import java.io.Reader;
-import java.math.BigDecimal;
+import java.math.RoundingMode;
 import java.text.DecimalFormat;
 import java.time.Instant;
 import java.util.Date;
@@ -62,10 +63,10 @@ public class LocalDBUtility
 {
 
     private static final PwmLogger LOGGER = PwmLogger.forClass( LocalDBUtility.class );
+    private static final String IN_PROGRESS_STATUS_VALUE = "in-progress";
 
     private final LocalDB localDB;
     private int exportLineCounter;
-    private int importLineCounter;
 
     private static final int GZIP_BUFFER_SIZE = 1024 * 512;
 
@@ -221,18 +222,20 @@ public class LocalDBUtility
     private void importLocalDB( final InputStream inputStream, final Appendable out, final long totalBytes )
             throws PwmOperationalException, IOException
     {
-        this.prepareForImport();
 
-        importLineCounter = 0;
-        if ( totalBytes > 0 )
-        {
-            writeStringToOut( out, "total bytes in localdb import source: " + totalBytes );
-        }
-
-        writeStringToOut( out, "beginning localdb import..." );
+        final ImportLocalDBMachine importLocalDBMachine = new ImportLocalDBMachine( localDB, totalBytes, out );
+        importLocalDBMachine.doImport( inputStream );
+    }
 
-        final Instant startTime = Instant.now();
-        final TransactionSizeCalculator transactionCalculator = new TransactionSizeCalculator(
+    private static class ImportLocalDBMachine
+    {
+        private int lineReaderCounter;
+        private long byteReaderCounter;
+        private int recordImportCounter;
+        private final Instant startTime = Instant.now();
+        final Map<LocalDB.DB, Map<String, String>> transactionMap = new HashMap<>();
+        private final EventRateMeter eventRateMeter = new EventRateMeter( TimeDuration.MINUTE );
+        private final TransactionSizeCalculator transactionCalculator = new TransactionSizeCalculator(
                 TransactionSizeCalculator.Settings.builder()
                         .durationGoal( TimeDuration.of( 100, TimeDuration.Unit.MILLISECONDS ) )
                         .minTransactions( 50 )
@@ -240,96 +243,139 @@ public class LocalDBUtility
                         .build()
         );
 
-        final Map<LocalDB.DB, Map<String, String>> transactionMap = new HashMap<>();
-        for ( final LocalDB.DB loopDB : LocalDB.DB.values() )
-        {
-            transactionMap.put( loopDB, new TreeMap<>() );
-        }
+        private final long totalBytes;
+        private final Appendable debugOutput;
+        private final LocalDB localDB;
 
-        final CountingInputStream countingInputStream = new CountingInputStream( inputStream );
-        final EventRateMeter eventRateMeter = new EventRateMeter( TimeDuration.MINUTE );
+        private final ConditionalTaskExecutor debugOutputWriter;
 
-        final Timer statTimer = new Timer( true );
-        statTimer.scheduleAtFixedRate( new TimerTask()
+        ImportLocalDBMachine( final LocalDB localDB, final long totalBytes, final Appendable debugOutput )
         {
-            @Override
-            public void run( )
+            this.localDB = localDB;
+            this.totalBytes = totalBytes;
+            this.debugOutput = debugOutput;
+
+            for ( final LocalDB.DB loopDB : LocalDB.DB.values() )
             {
-                String output = "";
-                if ( totalBytes > 0 )
-                {
-                    final ProgressInfo progressInfo = new ProgressInfo( startTime, totalBytes, countingInputStream.getByteCount() );
-                    output += progressInfo.debugOutput();
-                }
-                else
-                {
-                    output += "recordsImported=" + importLineCounter;
-                }
-                output += ", avgTransactionSize=" + transactionCalculator.getTransactionSize()
-                        + ", recordsPerMinute=" + eventRateMeter.readEventRate().setScale( 2, BigDecimal.ROUND_DOWN );
-                writeStringToOut( out, output );
+                transactionMap.put( loopDB, new TreeMap<>() );
             }
-        }, 30 * 1000, 30 * 1000 );
 
+            this.debugOutputWriter = ConditionalTaskExecutor.forPeriodicTask( () ->
+            {
+                writeStringToOut( debugOutput, debugStatsString() );
+            }, TimeDuration.of( 30, TimeDuration.Unit.SECONDS ) );
+        }
 
-        Reader csvReader = null;
-        try
+        void doImport( final InputStream inputStream )
+                throws IOException, LocalDBException
         {
-            csvReader = new InputStreamReader( new GZIPInputStream( countingInputStream, GZIP_BUFFER_SIZE ), PwmConstants.DEFAULT_CHARSET );
-            for ( final CSVRecord record : PwmConstants.DEFAULT_CSV_FORMAT.parse( csvReader ) )
+            this.prepareForImport();
+
+            if ( totalBytes > 0 )
             {
-                importLineCounter++;
-                eventRateMeter.markEvents( 1 );
-                final String dbNameRecordStr = record.get( 0 );
-                final LocalDB.DB db = JavaHelper.readEnumFromString( LocalDB.DB.class, null, dbNameRecordStr );
-                final String key = record.get( 1 );
-                final String value = record.get( 2 );
-                if ( db == null )
-                {
-                    writeStringToOut( out, "ignoring localdb import record #" + importLineCounter + ", invalid DB name '" + dbNameRecordStr + "'" );
-                }
-                else
+                writeStringToOut( debugOutput, "total bytes in localdb import source: " + totalBytes );
+            }
+
+            writeStringToOut( debugOutput, "beginning localdb import..." );
+
+            try ( CountingInputStream countingInputStream = new CountingInputStream( inputStream ) )
+            {
+                try ( Reader csvReader = new InputStreamReader( new GZIPInputStream( countingInputStream, GZIP_BUFFER_SIZE ), PwmConstants.DEFAULT_CHARSET ) )
                 {
-                    transactionMap.get( db ).put( key, value );
                     int cachedTransactions = 0;
-                    for ( final LocalDB.DB loopDB : LocalDB.DB.values() )
-                    {
-                        cachedTransactions += transactionMap.get( loopDB ).size();
-                    }
-                    if ( cachedTransactions >= transactionCalculator.getTransactionSize() )
+                    for ( final CSVRecord record : PwmConstants.DEFAULT_CSV_FORMAT.parse( csvReader ) )
                     {
-                        final long startTxnTime = System.currentTimeMillis();
-                        for ( final LocalDB.DB loopDB : LocalDB.DB.values() )
+                        lineReaderCounter++;
+                        eventRateMeter.markEvents( 1 );
+                        byteReaderCounter = countingInputStream.getByteCount();
+                        final String dbNameRecordStr = record.get( 0 );
+                        final LocalDB.DB db = JavaHelper.readEnumFromString( LocalDB.DB.class, null, dbNameRecordStr );
+                        final String key = record.get( 1 );
+                        final String value = record.get( 2 );
+                        if ( db == null )
                         {
-                            localDB.putAll( loopDB, transactionMap.get( loopDB ) );
-                            transactionMap.get( loopDB ).clear();
+                            writeStringToOut( debugOutput, "ignoring localdb import record #" + lineReaderCounter + ", invalid DB name '" + dbNameRecordStr + "'" );
                         }
-                        transactionCalculator.recordLastTransactionDuration( TimeDuration.fromCurrent( startTxnTime ) );
+                        else
+                        {
+                            transactionMap.get( db ).put( key, value );
+                            cachedTransactions++;
+                            if ( cachedTransactions >= transactionCalculator.getTransactionSize() )
+                            {
+                                flushCachedTransactions();
+                                cachedTransactions = 0;
+                            }
+                        }
+                        debugOutputWriter.conditionallyExecuteTask();
                     }
                 }
             }
+
+            flushCachedTransactions();
+            this.markImportComplete();
+
+            final String completeMsg = "import process completed: " + debugStatsString();
+            LOGGER.info( () -> completeMsg );
+            writeStringToOut( debugOutput, completeMsg );
+        }
+
+        private void flushCachedTransactions( )
+                throws LocalDBException
+        {
+            final Instant startTxnTime = Instant.now();
+            for ( final LocalDB.DB loopDB : LocalDB.DB.values() )
+            {
+                localDB.putAll( loopDB, transactionMap.get( loopDB ) );
+                recordImportCounter += transactionMap.get( loopDB ).size();
+                transactionMap.get( loopDB ).clear();
+            }
+            transactionCalculator.recordLastTransactionDuration( TimeDuration.fromCurrent( startTxnTime ) );
         }
-        finally
+
+        private void prepareForImport( )
+                throws LocalDBException
         {
-            LOGGER.trace( () -> "import process completed" );
-            statTimer.cancel();
-            IOUtils.closeQuietly( csvReader );
-            IOUtils.closeQuietly( countingInputStream );
+            LOGGER.info( () -> "preparing LocalDB for import procedure" );
+            localDB.put( LocalDB.DB.PWM_META, PwmApplication.AppAttribute.LOCALDB_IMPORT_STATUS.getKey(), IN_PROGRESS_STATUS_VALUE );
+            for ( final LocalDB.DB loopDB : LocalDB.DB.values() )
+            {
+                if ( loopDB != LocalDB.DB.PWM_META )
+                {
+                    localDB.truncate( loopDB );
+                }
+            }
+
+            // save meta for last so flag is cleared last.
+            localDB.truncate( LocalDB.DB.PWM_META );
+            localDB.put( LocalDB.DB.PWM_META, PwmApplication.AppAttribute.LOCALDB_IMPORT_STATUS.getKey(), IN_PROGRESS_STATUS_VALUE  );
         }
 
-        for ( final LocalDB.DB loopDB : LocalDB.DB.values() )
+        private void markImportComplete()
+                throws LocalDBException
         {
-            localDB.putAll( loopDB, transactionMap.get( loopDB ) );
-            transactionMap.get( loopDB ).clear();
+            LOGGER.info( () -> "marking LocalDB import procedure completed" );
+            localDB.remove( LocalDB.DB.PWM_META, PwmApplication.AppAttribute.LOCALDB_IMPORT_STATUS.getKey() );
         }
 
-        this.markImportComplete();
+        private String debugStatsString()
+        {
+            final Map<String, String> stats = new LinkedHashMap<>();
+            if ( totalBytes > 0 && byteReaderCounter > 0 )
+            {
+                final ProgressInfo progressInfo = new ProgressInfo( startTime, totalBytes, byteReaderCounter );
+                stats.put( "progress", progressInfo.debugOutput() );
+            }
 
-        writeStringToOut( out, "restore complete, restored " + importLineCounter + " records in " + TimeDuration.fromCurrent( startTime ).asLongString() );
-        statTimer.cancel();
+            stats.put( "linesRead", Integer.toString( lineReaderCounter ) );
+            stats.put( "bytesRead", Long.toString( byteReaderCounter ) );
+            stats.put( "recordsImported", Integer.toString( recordImportCounter ) );
+            stats.put( "avgTransactionSize", Integer.toString( transactionCalculator.getTransactionSize() ) );
+            stats.put( "recordsPerMinute", eventRateMeter.readEventRate().setScale( 2, RoundingMode.DOWN ).toString() );
+            stats.put( "duration", TimeDuration.compactFromCurrent( startTime ) );
+            return StringUtil.mapToString( stats );
+        }
     }
 
-
     public static Map<StatsKey, Object> dbStats(
             final LocalDB localDB,
             final LocalDB.DB db
@@ -381,35 +427,10 @@ public class LocalDBUtility
         AVG_VALUE_LENGTH,
     }
 
-    public void prepareForImport( )
-            throws LocalDBException
-    {
-        LOGGER.info( () -> "preparing LocalDB for import procedure" );
-        localDB.put( LocalDB.DB.PWM_META, PwmApplication.AppAttribute.LOCALDB_IMPORT_STATUS.getKey(), "inprogress" );
-        for ( final LocalDB.DB loopDB : LocalDB.DB.values() )
-        {
-            if ( loopDB != LocalDB.DB.PWM_META )
-            {
-                localDB.truncate( loopDB );
-            }
-        }
-
-        // save meta for last so flag is cleared last.
-        localDB.truncate( LocalDB.DB.PWM_META );
-        localDB.put( LocalDB.DB.PWM_META, PwmApplication.AppAttribute.LOCALDB_IMPORT_STATUS.getKey(), "inprogress" );
-    }
-
-    public void markImportComplete( )
-            throws LocalDBException
-    {
-        LOGGER.info( () -> "marking LocalDB import procedure completed" );
-        localDB.remove( LocalDB.DB.PWM_META, PwmApplication.AppAttribute.LOCALDB_IMPORT_STATUS.getKey() );
-    }
-
     public boolean readImportInprogressFlag( )
             throws LocalDBException
     {
-        return "inprogress".equals(
+        return IN_PROGRESS_STATUS_VALUE.equals(
                 localDB.get( LocalDB.DB.PWM_META, PwmApplication.AppAttribute.LOCALDB_IMPORT_STATUS.getKey() ) );
     }
 
@@ -417,4 +438,12 @@ public class LocalDBUtility
     {
         return parameters != null && parameters.containsKey( parameter ) && Boolean.parseBoolean( parameters.get( parameter ) );
     }
+
+    public void cancelImportProcess()
+            throws LocalDBException
+    {
+        final ImportLocalDBMachine importLocalDBMachine = new ImportLocalDBMachine( localDB, 0, new StringBuilder() );
+        importLocalDBMachine.prepareForImport();
+        importLocalDBMachine.markImportComplete();
+    }
 }

+ 1 - 1
server/src/main/java/password/pwm/util/localdb/WorkQueueProcessor.java

@@ -29,7 +29,7 @@ import password.pwm.PwmApplication;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmOperationalException;
-import password.pwm.svc.stats.EventRateMeter;
+import password.pwm.util.EventRateMeter;
 import password.pwm.util.PwmScheduler;
 import password.pwm.util.java.AtomicLoopIntIncrementer;
 import password.pwm.util.java.JavaHelper;

+ 2 - 1
server/src/main/java/password/pwm/util/operations/PasswordUtility.java

@@ -76,6 +76,7 @@ import password.pwm.svc.cache.CacheService;
 import password.pwm.svc.event.AuditEvent;
 import password.pwm.svc.event.AuditRecordFactory;
 import password.pwm.svc.event.HelpdeskAuditRecord;
+import password.pwm.svc.stats.AvgStatistic;
 import password.pwm.svc.stats.EpsStatistic;
 import password.pwm.svc.stats.Statistic;
 import password.pwm.util.PasswordCharCounter;
@@ -465,7 +466,7 @@ public class PasswordUtility
         pwmApplication.getStatisticsManager().updateEps( EpsStatistic.PASSWORD_CHANGES, 1 );
 
         final int passwordStrength = PasswordUtility.judgePasswordStrength( pwmApplication.getConfig(), newPassword.getStringValue() );
-        pwmApplication.getStatisticsManager().updateAverageValue( Statistic.AVG_PASSWORD_STRENGTH, passwordStrength );
+        pwmApplication.getStatisticsManager().updateAverageValue( AvgStatistic.AVG_PASSWORD_STRENGTH, passwordStrength );
 
         // at this point the password has been changed, so log it.
         final String msg = ( bindIsSelf

+ 1 - 1
server/src/main/java/password/pwm/util/secure/X509Utils.java

@@ -302,7 +302,7 @@ public abstract class X509Utils
                 {
                     try
                     {
-                        LOGGER.warn( sessionLabel, "blind trusting certificate during authType=" + authType + ", subject=" + cert.getSubjectDN().toString() );
+                        LOGGER.debug( sessionLabel, () -> "promiscuous trusting certificate during authType=" + authType + ", subject=" + cert.getSubjectDN().toString() );
                     }
                     catch ( Exception e )
                     {

+ 19 - 9
server/src/main/java/password/pwm/ws/server/rest/RestStatisticsServer.java

@@ -34,8 +34,11 @@ import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.http.HttpContentType;
 import password.pwm.http.HttpMethod;
 import password.pwm.http.PwmHttpRequestWrapper;
+import password.pwm.svc.stats.AvgStatistic;
+import password.pwm.svc.stats.DailyKey;
 import password.pwm.svc.stats.EpsStatistic;
 import password.pwm.svc.stats.Statistic;
+import password.pwm.svc.stats.StatisticType;
 import password.pwm.svc.stats.StatisticsBundle;
 import password.pwm.svc.stats.StatisticsManager;
 import password.pwm.util.java.JavaHelper;
@@ -54,9 +57,7 @@ import java.math.RoundingMode;
 import java.time.ZoneOffset;
 import java.time.format.DateTimeFormatter;
 import java.util.ArrayList;
-import java.util.Calendar;
 import java.util.Collections;
-import java.util.Date;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Locale;
@@ -194,7 +195,7 @@ public class RestStatisticsServer extends RestServlet
         {
             final List<HistoryData> outerOutput = new ArrayList<>();
 
-            StatisticsManager.DailyKey dailyKey = new StatisticsManager.DailyKey( new Date() );
+            DailyKey dailyKey = DailyKey.forToday();
 
             for ( int daysAgo = 0; daysAgo < days; daysAgo++ )
             {
@@ -210,10 +211,10 @@ public class RestStatisticsServer extends RestServlet
                 final HistoryData historyData = HistoryData.builder()
                         .name( dailyKey.toString() )
                         .date( DateTimeFormatter.ofPattern( "yyyy-MM-dd" ).withZone( ZoneOffset.UTC )
-                                .format( dailyKey.calendar().toInstant() ) )
-                        .year( dailyKey.calendar().get( Calendar.YEAR ) )
-                        .month( dailyKey.calendar().get( Calendar.MONTH ) )
-                        .day( dailyKey.calendar().get( Calendar.DAY_OF_MONTH ) )
+                                .format( dailyKey.localDate() ) )
+                        .year( dailyKey.localDate().getYear() )
+                        .month( dailyKey.localDate().getMonthValue() )
+                        .day( dailyKey.localDate().getDayOfMonth() )
                         .daysAgo( daysAgo )
                         .data( statValues )
                         .build();
@@ -250,7 +251,16 @@ public class RestStatisticsServer extends RestServlet
                 final StatLabelData statLabelData = new StatLabelData(
                         statistic.name(),
                         statistic.getLabel( locale ),
-                        statistic.getType().name(),
+                        StatisticType.INCREMENTER.name(),
+                        statistic.getDescription( locale ) );
+                output.put( statistic.name(), statLabelData );
+            }
+            for ( final AvgStatistic statistic : AvgStatistic.values() )
+            {
+                final StatLabelData statLabelData = new StatLabelData(
+                        statistic.name(),
+                        statistic.getLabel( locale ),
+                        StatisticType.AVERAGE.name(),
                         statistic.getDescription( locale ) );
                 output.put( statistic.name(), statLabelData );
             }
@@ -262,7 +272,7 @@ public class RestStatisticsServer extends RestServlet
                     final StatLabelData statLabelData = new StatLabelData(
                             name,
                             loopEps.getLabel( locale ),
-                            "EPS",
+                            StatisticType.EPS.name(),
                             null );
                     output.put( name, statLabelData );
                 }

+ 2 - 0
server/src/main/resources/password/pwm/i18n/Admin.properties

@@ -222,6 +222,8 @@ Statistic_Label.AvgPasswordSyncTime=Average Password Sync Time
 Statistic_Description.AvgPasswordSyncTime=Average time (in milliseconds) users spend waiting for the password sync progress to complete.
 Statistic_Label.AvgAuthenticationTime=Average Authentication Time
 Statistic_Description.AvgAuthenticationTime=Average time (in milliseconds) for authentications of all types to complete.
+Statistic_Label.AvgRequestProcessTime=Average Request Process Time
+Statistic_Description.AvgRequestProcessTime=Average time (in milliseconds) for page requests (not including resources) to process.
 Statistic_Label.RecoveryTokensSent=Forgotten Password Tokens Sent
 Statistic_Description.RecoveryTokensSent=Number of tokens used for forgotten password process issued and sent via email or SMS.
 Statistic_Label.RecoveryTokensPassed=Forgotten Password Tokens Passed

+ 18 - 7
server/src/test/java/password/pwm/i18n/AdminPropertyKeysTest.java

@@ -25,9 +25,11 @@ package password.pwm.i18n;
 import org.junit.Assert;
 import org.junit.Test;
 import password.pwm.PwmConstants;
+import password.pwm.svc.stats.AvgStatistic;
 import password.pwm.svc.stats.EpsStatistic;
 import password.pwm.svc.stats.Statistic;
 
+import java.util.Collections;
 import java.util.HashSet;
 import java.util.ResourceBundle;
 import java.util.Set;
@@ -48,13 +50,22 @@ public class AdminPropertyKeysTest
                     password.pwm.i18n.Admin.STATISTICS_DESCRIPTION_PREFIX + statistic.getKey(),
                     password.pwm.i18n.Admin.STATISTICS_LABEL_PREFIX + statistic.getKey(),
             };
-            for ( final String key : keys )
-            {
-                expectedKeys.add( key );
-                Assert.assertTrue(
-                        "Admin.properties missing record for " + key,
-                        resourceBundle.containsKey( key ) );
-            }
+            Collections.addAll( expectedKeys, keys );
+        }
+        for ( final AvgStatistic statistic : AvgStatistic.values() )
+        {
+            final String[] keys = new String[] {
+                    password.pwm.i18n.Admin.STATISTICS_DESCRIPTION_PREFIX + statistic.getKey(),
+                    password.pwm.i18n.Admin.STATISTICS_LABEL_PREFIX + statistic.getKey(),
+            };
+            Collections.addAll( expectedKeys, keys );
+        }
+
+        for ( final String key : expectedKeys )
+        {
+            Assert.assertTrue(
+                    "Admin.properties missing record for " + key,
+                    resourceBundle.containsKey( key ) );
         }
 
         final Set<String> extraKeys = new HashSet<>( resourceBundle.keySet() );

+ 1 - 1
server/src/test/java/password/pwm/util/localdb/LocalDBLoggerExtendedTest.java

@@ -30,7 +30,7 @@ import password.pwm.AppProperty;
 import password.pwm.config.Configuration;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.stored.StoredConfigurationImpl;
-import password.pwm.svc.stats.EventRateMeter;
+import password.pwm.util.EventRateMeter;
 import password.pwm.util.java.FileSystemUtility;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.util.java.Percent;

+ 22 - 9
webapp/src/main/webapp/WEB-INF/jsp/admin-analysis.jsp

@@ -20,16 +20,19 @@
  ~ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 --%>
 
-<%@ page import="password.pwm.error.PwmError" %>
 <%@ page import="password.pwm.error.PwmException" %>
 <%@ page import="password.pwm.i18n.Admin" %>
+<%@ page import="password.pwm.svc.stats.AvgStatistic" %>
+<%@ page import="password.pwm.svc.stats.DailyKey" %>
 <%@ page import="password.pwm.svc.stats.Statistic" %>
+<%@ page import="password.pwm.svc.stats.StatisticType" %>
 <%@ page import="password.pwm.svc.stats.StatisticsBundle" %>
 <%@ page import="password.pwm.svc.stats.StatisticsManager" %>
 <%@ page import="password.pwm.util.java.JavaHelper" %>
-<%@ page import="java.text.DateFormat" %>
 <%@ page import="java.util.Locale" %>
 <%@ page import="java.util.Map" %>
+<%@ page import="password.pwm.util.java.StringUtil" %>
+<%@ page import="java.time.format.DateTimeFormatter" %>
 
 <!DOCTYPE html>
 <%@ page language="java" session="true" isThreadSafe="true" contentType="text/html" %>
@@ -182,16 +185,16 @@
                                             <select name="statsPeriodSelect"
                                                     style="width: 350px;">
                                                 <option value="<%=StatisticsManager.KEY_CUMULATIVE%>" <%= StatisticsManager.KEY_CUMULATIVE.equals(statsPeriodSelect) ? "selected=\"selected\"" : "" %>>
-                                                    since installation - <%= JavaHelper.toIsoDate(analysis_pwmRequest.getPwmApplication().getInstallTime()) %>
+                                                    since installation - <span class="timestamp"><%= JavaHelper.toIsoDate(analysis_pwmRequest.getPwmApplication().getInstallTime()) %></span>
                                                 </option>
                                                 <option value="<%=StatisticsManager.KEY_CURRENT%>" <%= StatisticsManager.KEY_CURRENT.equals(statsPeriodSelect) ? "selected=\"selected\"" : "" %>>
-                                                    since startup - <%= JavaHelper.toIsoDate(analysis_pwmRequest.getPwmApplication().getStartupTime()) %>
+                                                    since startup - <span class="timestamp"><%= JavaHelper.toIsoDate(analysis_pwmRequest.getPwmApplication().getStartupTime()) %></span>
                                                 </option>
-                                                <% final Map<StatisticsManager.DailyKey, String> availableKeys = statsManager.getAvailableKeys(locale); %>
-                                                <% for (final Map.Entry<StatisticsManager.DailyKey, String> entry : availableKeys.entrySet()) { %>
-                                                <% final StatisticsManager.DailyKey key = entry.getKey(); %>
+                                                <% final Map<DailyKey, String> availableKeys = statsManager.getAvailableKeys(locale); %>
+                                                <% for (final Map.Entry<DailyKey, String> entry : availableKeys.entrySet()) { %>
+                                                <% final DailyKey key = entry.getKey(); %>
                                                 <option value="<%=key%>" <%= key.toString().equals(statsPeriodSelect) ? "selected=\"selected\"" : "" %>>
-                                                    <%= entry.getValue() %>
+                                                    <%=key.localDate().format(DateTimeFormatter.ISO_LOCAL_DATE)%>
                                                 </option>
                                                 <% } %>
                                             </select>
@@ -208,7 +211,17 @@
                                         <span id="Statistic_Key_<%=loopStat.getKey()%>"><%= loopStat.getLabel(locale) %><span/>
                                     </td>
                                     <td>
-                                        <%= stats.getStatistic(loopStat) %><%= loopStat.getType() == Statistic.Type.AVERAGE && loopStat != Statistic.AVG_PASSWORD_STRENGTH ? " ms" : "" %>
+                                        <%= stats.getStatistic(loopStat) %>
+                                    </td>
+                                </tr>
+                                <% } %>
+                                <% for (final AvgStatistic loopStat : AvgStatistic.values()) { %>
+                                <tr>
+                                    <td >
+                                        <span id="Statistic_Key_<%=loopStat.getKey()%>"><%= loopStat.getLabel(locale) %><span/>
+                                    </td>
+                                    <td>
+                                        <%= stats.getAvgStatistic(loopStat) %><%= loopStat.getUnit() %>
                                     </td>
                                 </tr>
                                 <% } %>