瀏覽代碼

statistics refactoring and stand-alone viewer

Jason Rivard 2 年之前
父節點
當前提交
81e90fc63a
共有 53 個文件被更改,包括 1134 次插入620 次删除
  1. 1 1
      server/src/main/java/password/pwm/PwmApplication.java
  2. 2 2
      server/src/main/java/password/pwm/PwmDomain.java
  3. 1 1
      server/src/main/java/password/pwm/http/JspUrl.java
  4. 9 9
      server/src/main/java/password/pwm/http/bean/DisplayElement.java
  5. 5 5
      server/src/main/java/password/pwm/http/filter/RequestInitializationFilter.java
  6. 1 1
      server/src/main/java/password/pwm/http/filter/SessionFilter.java
  7. 1 1
      server/src/main/java/password/pwm/http/servlet/ClientApiServlet.java
  8. 0 1
      server/src/main/java/password/pwm/http/servlet/GuestRegistrationServlet.java
  9. 9 6
      server/src/main/java/password/pwm/http/servlet/PwmServletDefinition.java
  10. 6 7
      server/src/main/java/password/pwm/http/servlet/admin/AdminMenuServlet.java
  11. 1 33
      server/src/main/java/password/pwm/http/servlet/admin/SystemAdminServlet.java
  12. 3 4
      server/src/main/java/password/pwm/http/servlet/admin/domain/DomainAdminReportServlet.java
  13. 193 0
      server/src/main/java/password/pwm/http/servlet/admin/domain/DomainAdminStatisticsServlet.java
  14. 5 6
      server/src/main/java/password/pwm/http/servlet/admin/domain/DomainAdminUserDebugServlet.java
  15. 0 1
      server/src/main/java/password/pwm/http/servlet/admin/system/ConfigManagerLocalDBServlet.java
  16. 0 1
      server/src/main/java/password/pwm/http/servlet/admin/system/ConfigManagerLoginServlet.java
  17. 0 1
      server/src/main/java/password/pwm/http/servlet/admin/system/ConfigManagerServlet.java
  18. 0 1
      server/src/main/java/password/pwm/http/servlet/admin/system/ConfigManagerWordlistServlet.java
  19. 0 1
      server/src/main/java/password/pwm/http/servlet/admin/system/SystemAdminCertificatesServlet.java
  20. 1 1
      server/src/main/java/password/pwm/http/servlet/changepw/ChangePasswordServlet.java
  21. 1 1
      server/src/main/java/password/pwm/ldap/LdapDomainService.java
  22. 1 1
      server/src/main/java/password/pwm/ldap/LdapOperationsHelper.java
  23. 1 1
      server/src/main/java/password/pwm/ldap/auth/LDAPAuthenticationRequest.java
  24. 2 2
      server/src/main/java/password/pwm/ldap/search/UserSearchJob.java
  25. 15 0
      server/src/main/java/password/pwm/svc/stats/AvgStatistic.java
  26. 0 77
      server/src/main/java/password/pwm/svc/stats/DailyKey.java
  27. 0 25
      server/src/main/java/password/pwm/svc/stats/StatKey.java
  28. 258 0
      server/src/main/java/password/pwm/svc/stats/StatisticsBundleKey.java
  29. 3 3
      server/src/main/java/password/pwm/svc/stats/StatisticsClient.java
  30. 51 123
      server/src/main/java/password/pwm/svc/stats/StatisticsService.java
  31. 166 0
      server/src/main/java/password/pwm/svc/stats/StatisticsUtils.java
  32. 3 4
      server/src/main/java/password/pwm/svc/telemetry/TelemetryService.java
  33. 2 2
      server/src/main/java/password/pwm/util/DailySummaryJob.java
  34. 10 4
      server/src/main/java/password/pwm/util/cli/commands/ExportStatsCommand.java
  35. 8 2
      server/src/main/java/password/pwm/util/debug/StatisticsDataDebugItemGenerator.java
  36. 1 1
      server/src/main/java/password/pwm/util/debug/StatisticsEpsDataDebugItemGenerator.java
  37. 2 2
      server/src/main/java/password/pwm/util/password/PasswordUtility.java
  38. 29 25
      server/src/main/java/password/pwm/ws/server/rest/RestStatisticsServer.java
  39. 1 1
      server/src/main/resources/password/pwm/i18n/Admin.properties
  40. 75 0
      server/src/test/java/password/pwm/svc/stats/StatisticsBundleKeyTest.java
  41. 0 213
      webapp/src/main/webapp/WEB-INF/jsp/admin-analysis.jsp
  42. 121 0
      webapp/src/main/webapp/WEB-INF/jsp/admin-statistics.jsp
  43. 44 27
      webapp/src/main/webapp/WEB-INF/jsp/admin-user-debug.jsp
  44. 0 6
      webapp/src/main/webapp/WEB-INF/jsp/fragment/admin-modular-nav.jsp
  45. 0 7
      webapp/src/main/webapp/WEB-INF/jsp/fragment/admin-nav.jsp
  46. 1 1
      webapp/src/main/webapp/WEB-INF/jsp/fragment/header-menu.jsp
  47. 2 2
      webapp/src/main/webapp/WEB-INF/jsp/fragment/message.jsp
  48. 13 2
      webapp/src/main/webapp/private/admin/index.jsp
  49. 1 1
      webapp/src/main/webapp/private/index.jsp
  50. 82 0
      webapp/src/main/webapp/public/resources/js/admin-statistics.js
  51. 1 1
      webapp/src/main/webapp/public/resources/js/main.js
  52. 2 2
      webapp/src/main/webapp/public/resources/js/uilibrary.js
  53. 0 1
      webapp/src/main/webapp/public/resources/style.css

+ 1 - 1
server/src/main/java/password/pwm/PwmApplication.java

@@ -734,7 +734,7 @@ public class PwmApplication
         return ( DatabaseService ) pwmServiceManager.getService( PwmServiceEnum.DatabaseService );
     }
 
-    public StatisticsService getStatisticsManager( )
+    public StatisticsService getStatisticsService( )
     {
         return ( StatisticsService ) pwmServiceManager.getService( PwmServiceEnum.StatisticsService );
     }

+ 2 - 2
server/src/main/java/password/pwm/PwmDomain.java

@@ -119,9 +119,9 @@ public class PwmDomain
         return pwmApplication.getApplicationMode();
     }
 
-    public StatisticsService getStatisticsManager( )
+    public StatisticsService getStatisticsService( )
     {
-        return pwmApplication.getStatisticsManager();
+        return pwmApplication.getStatisticsService();
     }
 
     public OtpService getOtpService( )

+ 1 - 1
server/src/main/java/password/pwm/http/JspUrl.java

@@ -27,9 +27,9 @@ public enum JspUrl
     SUCCESS( "success.jsp" ),
     APP_UNAVAILABLE( "application-unavailable.jsp" ),
     ADMIN_DASHBOARD( "admin-dashboard.jsp" ),
-    ADMIN_ANALYSIS( "admin-analysis.jsp" ),
     ADMIN_ACTIVITY( "admin-activity.jsp" ),
     ADMIN_REPORTING( "admin-reporting.jsp" ),
+    ADMIN_STATISTICS( "admin-statistics.jsp" ),
     ADMIN_TOKEN_LOOKUP( "admin-tokenlookup.jsp" ),
     ADMIN_LOGVIEW_WINDOW( "admin-logview-window.jsp" ),
     ADMIN_LOGVIEW( "admin-logview.jsp" ),

+ 9 - 9
server/src/main/java/password/pwm/http/bean/DisplayElement.java

@@ -20,21 +20,19 @@
 
 package password.pwm.http.bean;
 
-import lombok.EqualsAndHashCode;
-import lombok.Getter;
+import lombok.Value;
 
 import java.io.Serializable;
 import java.util.List;
 
-@Getter
-@EqualsAndHashCode
+@Value
 public class DisplayElement implements Serializable
 {
-    private String key;
-    private Type type;
-    private String label;
-    private String value;
-    private List<String> values;
+    private final String key;
+    private final Type type;
+    private final String label;
+    private final String value;
+    private final List<String> values;
 
     public enum Type
     {
@@ -50,6 +48,7 @@ public class DisplayElement implements Serializable
         this.type = type;
         this.label = label;
         this.value = value;
+        this.values = null;
     }
 
     public DisplayElement( final String key, final Type type, final String label, final List<String> values )
@@ -57,6 +56,7 @@ public class DisplayElement implements Serializable
         this.key = key;
         this.type = type;
         this.label = label;
+        this.value = null;
         this.values = values;
     }
 }

+ 5 - 5
server/src/main/java/password/pwm/http/filter/RequestInitializationFilter.java

@@ -293,9 +293,9 @@ public class RequestInitializationFilter implements Filter
     {
         if ( localPwmApplication != null && localPwmApplication.getApplicationMode() == PwmApplicationMode.RUNNING )
         {
-            if ( localPwmApplication.getStatisticsManager() != null )
+            if ( localPwmApplication.getStatisticsService() != null )
             {
-                localPwmApplication.getStatisticsManager().updateEps( EpsStatistic.REQUESTS, 1 );
+                localPwmApplication.getStatisticsService().updateEps( EpsStatistic.REQUESTS, 1 );
             }
         }
     }
@@ -722,14 +722,14 @@ public class RequestInitializationFilter implements Filter
     {
         if ( PwmConstants.TRIAL_MODE )
         {
-            final StatisticsService statisticsManager = pwmRequest.getPwmDomain().getStatisticsManager();
-            final String currentAuthString = statisticsManager.getStatBundleForKey( StatisticsService.KEY_CURRENT ).getStatistic( Statistic.AUTHENTICATIONS );
+            final StatisticsService statisticsManager = pwmRequest.getPwmDomain().getStatisticsService();
+            final String currentAuthString = statisticsManager.getCurrentBundle().getStatistic( Statistic.AUTHENTICATIONS );
             if ( new BigInteger( currentAuthString ).compareTo( BigInteger.valueOf( PwmConstants.TRIAL_MAX_AUTHENTICATIONS ) ) > 0 )
             {
                 throw new PwmUnrecoverableException( new ErrorInformation( PwmError.ERROR_TRIAL_VIOLATION, "maximum usage per server startup exceeded" ) );
             }
 
-            final String totalAuthString = statisticsManager.getStatBundleForKey( StatisticsService.KEY_CUMULATIVE ).getStatistic( Statistic.AUTHENTICATIONS );
+            final String totalAuthString = statisticsManager.getCumulativeBundle().getStatistic( Statistic.AUTHENTICATIONS );
             if ( new BigInteger( totalAuthString ).compareTo( BigInteger.valueOf( PwmConstants.TRIAL_MAX_TOTAL_AUTH ) ) > 0 )
             {
                 throw new PwmUnrecoverableException( new ErrorInformation( PwmError.ERROR_TRIAL_VIOLATION, "maximum usage for this server has been exceeded" ) );

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

@@ -137,7 +137,7 @@ public class SessionFilter extends AbstractPwmFilter
 
         final TimeDuration requestExecuteTime = TimeDuration.fromCurrent( startTime );
         pwmRequest.debugHttpRequestToLog( "completed", requestExecuteTime );
-        pwmRequest.getPwmDomain().getStatisticsManager().updateAverageValue( AvgStatistic.AVG_REQUEST_PROCESS_TIME, requestExecuteTime.asMillis() );
+        pwmRequest.getPwmDomain().getStatisticsService().updateAverageValue( AvgStatistic.AVG_REQUEST_PROCESS_TIME, requestExecuteTime.asMillis() );
         pwmRequest.getPwmSession().getSessionStateBean().getRequestCount().incrementAndGet();
         pwmRequest.getPwmSession().getSessionStateBean().getAvgRequestDuration().update( requestExecuteTime.asDuration() );
         updateSessionLocale( pwmRequest );

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

@@ -464,7 +464,7 @@ public class ClientApiServlet extends ControlledPwmServlet
         final String statName = pwmRequest.readParameterAsString( "statName" );
         final String days = pwmRequest.readParameterAsString( "days" );
 
-        final StatisticsService statisticsManager = pwmRequest.getPwmDomain().getStatisticsManager();
+        final StatisticsService statisticsManager = pwmRequest.getPwmDomain().getStatisticsService();
         final RestStatisticsServer.OutputVersion1.JsonOutput jsonOutput = new RestStatisticsServer.OutputVersion1.JsonOutput();
         jsonOutput.EPS = RestStatisticsServer.OutputVersion1.addEpsStats( statisticsManager );
 

+ 0 - 1
server/src/main/java/password/pwm/http/servlet/GuestRegistrationServlet.java

@@ -88,7 +88,6 @@ import java.util.Set;
  */
 
 @WebServlet(
-        name = "GuestRegistrationServlet",
         urlPatterns = {
                 PwmConstants.URL_PREFIX_PRIVATE + "/guest-registration",
                 PwmConstants.URL_PREFIX_PRIVATE + "/GuestRegistration",

+ 9 - 6
server/src/main/java/password/pwm/http/servlet/PwmServletDefinition.java

@@ -40,10 +40,11 @@ import password.pwm.http.bean.ShortcutsBean;
 import password.pwm.http.bean.UpdateProfileBean;
 import password.pwm.http.servlet.accountinfo.AccountInformationServlet;
 import password.pwm.http.servlet.activation.ActivateUserServlet;
-import password.pwm.http.servlet.admin.domain.AdminReportServlet;
-import password.pwm.http.servlet.admin.AdminServlet;
+import password.pwm.http.servlet.admin.domain.DomainAdminReportServlet;
+import password.pwm.http.servlet.admin.AdminMenuServlet;
 import password.pwm.http.servlet.admin.SystemAdminServlet;
-import password.pwm.http.servlet.admin.domain.AdminUserDebugServlet;
+import password.pwm.http.servlet.admin.domain.DomainAdminStatisticsServlet;
+import password.pwm.http.servlet.admin.domain.DomainAdminUserDebugServlet;
 import password.pwm.http.servlet.changepw.PrivateChangePasswordServlet;
 import password.pwm.http.servlet.changepw.PublicChangePasswordServlet;
 import password.pwm.http.servlet.command.PrivateCommandServlet;
@@ -94,10 +95,12 @@ public enum PwmServletDefinition
 
     ClientApi( ClientApiServlet.class, null ),
 
-    Admin( AdminServlet.class, null, Flag.RequiresUserPasswordAndBind ),
+    AdminMenu( AdminMenuServlet.class, null, Flag.RequiresUserPasswordAndBind ),
     SystemAdmin( SystemAdminServlet.class, AdminBean.class, Flag.RequiresUserPasswordAndBind ),
-    AdminReport( AdminReportServlet.class, null, Flag.RequiresUserPasswordAndBind ),
-    AdminUserDebug( AdminUserDebugServlet.class, null, Flag.RequiresUserPasswordAndBind ),
+
+    DomainAdminReport( DomainAdminReportServlet.class, null, Flag.RequiresUserPasswordAndBind ),
+    DomainAdminStatistics( DomainAdminStatisticsServlet.class, null, Flag.RequiresUserPasswordAndBind ),
+    DomainAdminUserDebug( DomainAdminUserDebugServlet.class, null, Flag.RequiresUserPasswordAndBind ),
 
     ConfigGuide( ConfigGuideServlet.class, ConfigGuideBean.class ),
     ConfigEditor( ConfigEditorServlet.class, null, Flag.RequiresConfigAuth ),

+ 6 - 7
server/src/main/java/password/pwm/http/servlet/admin/AdminServlet.java → server/src/main/java/password/pwm/http/servlet/admin/AdminMenuServlet.java

@@ -43,17 +43,16 @@ import java.util.Optional;
  * Simple servlet to front requests to the otherwise standard index page at '/private/admin/index.jsp'.
  */
 @WebServlet(
-        name = "AdminServlet",
         urlPatterns = {
-                PwmConstants.URL_PREFIX_PRIVATE + "/oldadmin",
-                PwmConstants.URL_PREFIX_PRIVATE + "/oldadmin/",
-                PwmConstants.URL_PREFIX_PRIVATE + "/oldadministration",
-                PwmConstants.URL_PREFIX_PRIVATE + "/oldadministration/",
+                PwmConstants.URL_PREFIX_PRIVATE + "/admin",
+                PwmConstants.URL_PREFIX_PRIVATE + "/admin/",
+                PwmConstants.URL_PREFIX_PRIVATE + "/administration",
+                PwmConstants.URL_PREFIX_PRIVATE + "/administration/",
         }
 )
-public class AdminServlet extends ControlledPwmServlet
+public class AdminMenuServlet extends ControlledPwmServlet
 {
-    private static final PwmLogger LOGGER = PwmLogger.forClass( AdminServlet.class );
+    private static final PwmLogger LOGGER = PwmLogger.forClass( AdminMenuServlet.class );
 
     @Override
     protected PwmLogger getLogger()

+ 1 - 33
server/src/main/java/password/pwm/http/servlet/admin/SystemAdminServlet.java

@@ -52,12 +52,11 @@ import password.pwm.svc.intruder.IntruderRecordType;
 import password.pwm.svc.intruder.PublicIntruderRecord;
 import password.pwm.svc.pwnotify.PwNotifyService;
 import password.pwm.svc.pwnotify.PwNotifyStoredJobState;
-import password.pwm.svc.stats.StatisticsService;
 import password.pwm.util.i18n.LocaleHelper;
 import password.pwm.util.java.EnumUtil;
 import password.pwm.util.java.JavaHelper;
-import password.pwm.util.java.PwmUtil;
 import password.pwm.util.java.PwmTimeUtil;
+import password.pwm.util.java.PwmUtil;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.TimeDuration;
 import password.pwm.util.logging.LocalDBLogger;
@@ -96,7 +95,6 @@ import java.util.zip.ZipEntry;
 import java.util.zip.ZipOutputStream;
 
 @WebServlet(
-        name = "SystemAdminServlet",
         urlPatterns = {
                 PwmConstants.URL_PREFIX_PRIVATE + "/admin/system/dashboard",
                 PwmConstants.URL_PREFIX_PRIVATE + "/admin/system/Administration",
@@ -116,7 +114,6 @@ public class SystemAdminServlet extends ControlledPwmServlet
     {
         viewLogWindow( HttpMethod.GET ),
         downloadAuditLogCsv( HttpMethod.POST ),
-        downloadStatisticsLogCsv( HttpMethod.POST ),
         downloadSessionsCsv( HttpMethod.POST ),
         clearIntruderTable( HttpMethod.POST ),
         auditData( HttpMethod.GET ),
@@ -219,34 +216,6 @@ public class SystemAdminServlet extends ControlledPwmServlet
         return ProcessStatus.Halt;
     }
 
-    @ActionHandler( action = "downloadStatisticsLogCsv" )
-    public ProcessStatus downloadStatisticsLogCsv( final PwmRequest pwmRequest )
-            throws PwmUnrecoverableException, IOException, ChaiUnavailableException, ServletException
-    {
-        final PwmDomain pwmDomain = pwmRequest.getPwmDomain();
-
-        pwmRequest.getPwmResponse().markAsDownload(
-                HttpContentType.csv,
-                pwmRequest.getPwmDomain().getConfig().readAppProperty( AppProperty.DOWNLOAD_FILENAME_STATISTICS_CSV )
-        );
-
-        final OutputStream outputStream = pwmRequest.getPwmResponse().getOutputStream();
-        try
-        {
-            final StatisticsService statsManager = pwmDomain.getStatisticsManager();
-            statsManager.outputStatsToCsv( outputStream, pwmRequest.getLocale(), true );
-        }
-        catch ( final Exception e )
-        {
-            final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_INTERNAL, e.getMessage() );
-            pwmRequest.respondWithError( errorInformation );
-        }
-        finally
-        {
-            outputStream.close();
-        }
-        return ProcessStatus.Halt;
-    }
 
     @ActionHandler( action = "downloadSessionsCsv" )
     public ProcessStatus downloadSessionsCsv( final PwmRequest pwmRequest )
@@ -438,7 +407,6 @@ public class SystemAdminServlet extends ControlledPwmServlet
     public enum Page
     {
         dashboard( JspUrl.ADMIN_DASHBOARD, "/dashboard" ),
-        analysis( JspUrl.ADMIN_ANALYSIS, "/analysis" ),
         activity( JspUrl.ADMIN_ACTIVITY, "/activity" ),
         tokenLookup( JspUrl.ADMIN_TOKEN_LOOKUP, "/tokens" ),
         viewLog( JspUrl.ADMIN_LOGVIEW, "/logs" ),

+ 3 - 4
server/src/main/java/password/pwm/http/servlet/admin/domain/AdminReportServlet.java → server/src/main/java/password/pwm/http/servlet/admin/domain/DomainAdminReportServlet.java

@@ -50,15 +50,14 @@ import java.util.List;
 import java.util.Optional;
 
 @WebServlet(
-        name = "AdminReportServlet",
         urlPatterns = {
                 PwmConstants.URL_PREFIX_PRIVATE + "/admin/report",
                 PwmConstants.URL_PREFIX_PRIVATE + "/admin/report/*",
         }
 )
-public class AdminReportServlet extends ControlledPwmServlet
+public class DomainAdminReportServlet extends ControlledPwmServlet
 {
-    private static final PwmLogger LOGGER = PwmLogger.forClass( AdminReportServlet.class );
+    private static final PwmLogger LOGGER = PwmLogger.forClass( DomainAdminReportServlet.class );
 
     @Override
     protected PwmLogger getLogger()
@@ -89,7 +88,7 @@ public class AdminReportServlet extends ControlledPwmServlet
     @Override
     protected PwmServletDefinition getServletDefinition()
     {
-        return PwmServletDefinition.AdminReport;
+        return PwmServletDefinition.DomainAdminReport;
     }
 
     @Override

+ 193 - 0
server/src/main/java/password/pwm/http/servlet/admin/domain/DomainAdminStatisticsServlet.java

@@ -0,0 +1,193 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2021 The PWM Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package password.pwm.http.servlet.admin.domain;
+
+import com.novell.ldapchai.exception.ChaiUnavailableException;
+import lombok.Value;
+import password.pwm.AppProperty;
+import password.pwm.PwmConstants;
+import password.pwm.PwmDomain;
+import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.http.HttpContentType;
+import password.pwm.http.HttpMethod;
+import password.pwm.http.JspUrl;
+import password.pwm.http.ProcessStatus;
+import password.pwm.http.PwmRequest;
+import password.pwm.http.bean.DisplayElement;
+import password.pwm.http.servlet.ControlledPwmServlet;
+import password.pwm.http.servlet.PwmServletDefinition;
+import password.pwm.http.servlet.admin.SystemAdminServlet;
+import password.pwm.svc.stats.StatisticsBundle;
+import password.pwm.svc.stats.StatisticsBundleKey;
+import password.pwm.svc.stats.StatisticsService;
+import password.pwm.svc.stats.StatisticsUtils;
+import password.pwm.util.java.CollectorUtil;
+import password.pwm.util.logging.PwmLogger;
+import password.pwm.ws.server.RestResultBean;
+
+import javax.servlet.ServletException;
+import javax.servlet.annotation.WebServlet;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.Collection;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Optional;
+
+@WebServlet(
+        urlPatterns = {
+                PwmConstants.URL_PREFIX_PRIVATE + "/admin/statistics",
+        }
+)
+public class DomainAdminStatisticsServlet extends ControlledPwmServlet
+{
+    private static final PwmLogger LOGGER = PwmLogger.forClass( DomainAdminStatisticsServlet.class );
+    private static final String DISPLAY_KEY_PREFIX = "Statistic_Key_";
+
+    @Override
+    protected PwmLogger getLogger()
+    {
+        return LOGGER;
+    }
+
+    public enum DomainAdminStatisticsAction implements ProcessAction
+    {
+        downloadStatisticsLogCsv( HttpMethod.POST ),
+        readKeys( HttpMethod.POST ),
+        readStatistics( HttpMethod.POST ),;
+
+        private final Collection<HttpMethod> method;
+
+        DomainAdminStatisticsAction( final HttpMethod... method )
+        {
+            this.method = List.of( method );
+        }
+
+        @Override
+        public Collection<HttpMethod> permittedMethods( )
+        {
+            return method;
+        }
+    }
+
+    @Override
+    protected PwmServletDefinition getServletDefinition()
+    {
+        return PwmServletDefinition.DomainAdminStatistics;
+    }
+
+    @Override
+    public Optional<Class<? extends ProcessAction>> getProcessActionsClass()
+    {
+        return Optional.of( DomainAdminStatisticsAction.class );
+    }
+
+    @Override
+    protected void nextStep( final PwmRequest pwmRequest )
+            throws PwmUnrecoverableException, IOException, ChaiUnavailableException, ServletException
+    {
+        pwmRequest.forwardToJsp( JspUrl.ADMIN_STATISTICS );
+    }
+
+    @Override
+    public ProcessStatus preProcessCheck( final PwmRequest pwmRequest )
+            throws PwmUnrecoverableException, IOException, ServletException
+    {
+        return SystemAdminServlet.preProcessAdminCheck( pwmRequest );
+
+    }
+
+    @ActionHandler( action = "readKeys" )
+    public ProcessStatus restReadKeys( final PwmRequest pwmRequest )
+            throws IOException, PwmUnrecoverableException
+    {
+        final StatisticsService statisticsService = pwmRequest.getPwmDomain().getStatisticsService();
+        final Locale locale = pwmRequest.getLocale();
+
+        final Map<String, String> keys = statisticsService.allKeys().stream()
+                .collect( CollectorUtil.toUnmodifiableLinkedMap(
+                        Object::toString,
+                        entry -> entry.getLabel( locale ) ) );
+
+        final RestResultBean<Map> results = RestResultBean.withData( keys, Map.class );
+        pwmRequest.outputJsonResult( results );
+        return ProcessStatus.Halt;
+    }
+
+
+    @ActionHandler( action = "readStatistics" )
+    public ProcessStatus restReadStatistics( final PwmRequest pwmRequest )
+            throws IOException, PwmUnrecoverableException
+    {
+        final String selectedKey = pwmRequest.readParameterAsString( "statKey" );
+        final StatisticsBundleKey statKey = StatisticsBundleKey.fromStringOrDefaultCumulative( selectedKey );
+
+        final StatisticsService statisticsService = pwmRequest.getPwmDomain().getStatisticsService();
+        final StatisticsBundle statisticsBundle = statisticsService.getStatBundleForKey( statKey )
+                .orElseThrow();
+
+        final List<DisplayElement> displayStatistics = StatisticsUtils.statsDisplayElementsForBundle(
+                DISPLAY_KEY_PREFIX, pwmRequest.getLocale(), statisticsBundle );
+
+        final List<DisplayElement> averageDisplayStatistics = StatisticsUtils.avgStatsDisplayElementsForBundle(
+                DISPLAY_KEY_PREFIX, pwmRequest.getLocale(), statisticsBundle );
+
+        final StatisticsData statisticsData = new StatisticsData( displayStatistics, averageDisplayStatistics );
+
+        final RestResultBean<StatisticsData> results = RestResultBean.withData( statisticsData, StatisticsData.class );
+        pwmRequest.outputJsonResult( results );
+        return ProcessStatus.Halt;
+    }
+
+    @ActionHandler( action = "downloadStatisticsLogCsv" )
+    public ProcessStatus downloadStatisticsLogCsv( final PwmRequest pwmRequest )
+            throws IOException
+    {
+        final PwmDomain pwmDomain = pwmRequest.getPwmDomain();
+
+        pwmRequest.getPwmResponse().markAsDownload(
+                HttpContentType.csv,
+                pwmRequest.getPwmDomain().getConfig().readAppProperty( AppProperty.DOWNLOAD_FILENAME_STATISTICS_CSV )
+        );
+
+        try ( OutputStream outputStream = pwmRequest.getPwmResponse().getOutputStream() )
+        {
+           StatisticsUtils.outputStatsToCsv(
+                    pwmRequest.getLabel(),
+                    pwmDomain.getStatisticsService(),
+                    outputStream,
+                    pwmRequest.getLocale(),
+                    StatisticsUtils.CsvOutputFlag.includeHeader );
+        }
+
+        return ProcessStatus.Halt;
+    }
+
+    @Value
+    private static class StatisticsData
+    {
+        private final List<DisplayElement> statistics;
+        private final List<DisplayElement> averageStatistics;
+    }
+
+
+}

+ 5 - 6
server/src/main/java/password/pwm/http/servlet/admin/domain/AdminUserDebugServlet.java → server/src/main/java/password/pwm/http/servlet/admin/domain/DomainAdminUserDebugServlet.java

@@ -54,14 +54,13 @@ import java.util.List;
 import java.util.Optional;
 
 @WebServlet(
-        name = "AdminUserDebugServlet",
         urlPatterns = {
                 PwmConstants.URL_PREFIX_PRIVATE + "/admin/user-debug",
         }
 )
-public class AdminUserDebugServlet extends ControlledPwmServlet
+public class DomainAdminUserDebugServlet extends ControlledPwmServlet
 {
-    private static final PwmLogger LOGGER = PwmLogger.forClass( AdminUserDebugServlet.class );
+    private static final PwmLogger LOGGER = PwmLogger.forClass( DomainAdminUserDebugServlet.class );
 
     @Override
     protected PwmLogger getLogger()
@@ -91,7 +90,7 @@ public class AdminUserDebugServlet extends ControlledPwmServlet
     @Override
     protected PwmServletDefinition getServletDefinition()
     {
-        return PwmServletDefinition.AdminUserDebug;
+        return PwmServletDefinition.DomainAdminUserDebug;
     }
 
     @Override
@@ -140,8 +139,8 @@ public class AdminUserDebugServlet extends ControlledPwmServlet
                 setLastError( pwmRequest, e.getErrorInformation() );
             }
         }
-
-        return ProcessStatus.Continue;
+        pwmRequest.forwardToJsp( JspUrl.ADMIN_DEBUG );
+        return ProcessStatus.Halt;
     }
 
     @ActionHandler( action = "downloadUserDebug" )

+ 0 - 1
server/src/main/java/password/pwm/http/servlet/admin/system/ConfigManagerLocalDBServlet.java

@@ -64,7 +64,6 @@ import java.util.Collections;
 import java.util.Optional;
 
 @WebServlet(
-        name = "ConfigManagerLocalDBServlet",
         urlPatterns = {
                 PwmConstants.URL_PREFIX_PRIVATE + "/config/manager/localdb",
         }

+ 0 - 1
server/src/main/java/password/pwm/http/servlet/admin/system/ConfigManagerLoginServlet.java

@@ -72,7 +72,6 @@ import java.util.List;
 import java.util.Optional;
 
 @WebServlet(
-        name = "ConfigManagerLogin",
         urlPatterns = {
                 PwmConstants.URL_PREFIX_PRIVATE + "/config/login",
         }

+ 0 - 1
server/src/main/java/password/pwm/http/servlet/admin/system/ConfigManagerServlet.java

@@ -80,7 +80,6 @@ import java.util.stream.Collectors;
 import java.util.zip.ZipOutputStream;
 
 @WebServlet(
-        name = "ConfigManagerServlet",
         urlPatterns = {
                 PwmConstants.URL_PREFIX_PRIVATE + "/config/manager",
                 PwmConstants.URL_PREFIX_PRIVATE + "/config/ConfigManager"

+ 0 - 1
server/src/main/java/password/pwm/http/servlet/admin/system/ConfigManagerWordlistServlet.java

@@ -61,7 +61,6 @@ import java.util.Map;
 import java.util.Optional;
 
 @WebServlet(
-        name = "ConfigManagerWordlistServlet",
         urlPatterns = {
                 PwmConstants.URL_PREFIX_PRIVATE + "/config/manager/wordlists",
         }

+ 0 - 1
server/src/main/java/password/pwm/http/servlet/admin/system/SystemAdminCertificatesServlet.java

@@ -59,7 +59,6 @@ import java.util.Optional;
 import java.util.stream.Collectors;
 
 @WebServlet(
-        name = "SystemAdminCertificateServlet",
         urlPatterns = {
                 PwmConstants.URL_PREFIX_PRIVATE + "/admin/system/certificates",
         }

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

@@ -396,7 +396,7 @@ public abstract class ChangePasswordServlet extends ControlledPwmServlet
                 final TimeDuration totalTime = TimeDuration.fromCurrent( progressTracker.getBeginTime() );
                 try
                 {
-                    pwmRequest.getPwmDomain().getStatisticsManager().updateAverageValue( AvgStatistic.AVG_PASSWORD_SYNC_TIME, totalTime.asMillis() );
+                    pwmRequest.getPwmDomain().getStatisticsService().updateAverageValue( AvgStatistic.AVG_PASSWORD_SYNC_TIME, totalTime.asMillis() );
                     LOGGER.trace( pwmRequest, () -> "password sync process marked completed (" + totalTime.asCompactString() + ")" );
                 }
                 catch ( final Exception e )

+ 1 - 1
server/src/main/java/password/pwm/ldap/LdapDomainService.java

@@ -237,7 +237,7 @@ public class LdapDomainService extends AbstractPwmService implements PwmService
                     sessionLabel,
                     ldapProfile,
                     pwmDomain.getConfig(),
-                    pwmDomain.getStatisticsManager()
+                    pwmDomain.getStatisticsService()
             );
             LOGGER.trace( sessionLabel, () -> "created new system proxy chaiProvider id=" + chaiProvider.toString()
                     + " for ldap profile '" + ldapProfile.getId() + "'"

+ 1 - 1
server/src/main/java/password/pwm/ldap/LdapOperationsHelper.java

@@ -471,7 +471,7 @@ public class LdapOperationsHelper
                 userPassword
         );
 
-        pwmDomain.getStatisticsManager().updateEps( EpsStatistic.LDAP_BINDS, 1 );
+        pwmDomain.getStatisticsService().updateEps( EpsStatistic.LDAP_BINDS, 1 );
 
         return chaiProvider;
     }

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

@@ -345,7 +345,7 @@ class LDAPAuthenticationRequest implements AuthenticationRequest
             final boolean usingProxy
     )
     {
-        final StatisticsService statisticsManager = pwmDomain.getStatisticsManager();
+        final StatisticsService statisticsManager = pwmDomain.getStatisticsService();
         StatisticsClient.incrementStat( pwmDomain.getPwmApplication(), Statistic.AUTHENTICATIONS );
         StatisticsClient.updateEps( pwmDomain.getPwmApplication(), EpsStatistic.AUTHENTICATION );
         statisticsManager.updateAverageValue( AvgStatistic.AVG_AUTHENTICATION_TIME,

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

@@ -115,9 +115,9 @@ class UserSearchJob implements Callable<Map<UserIdentity, Map<String, String>>>
 
         final TimeDuration searchDuration = TimeDuration.fromCurrent( startTime );
 
-        if ( pwmDomain.getStatisticsManager() != null && pwmDomain.getStatisticsManager().status() == PwmService.STATUS.OPEN )
+        if ( pwmDomain.getStatisticsService() != null && pwmDomain.getStatisticsService().status() == PwmService.STATUS.OPEN )
         {
-            pwmDomain.getStatisticsManager().updateAverageValue( AvgStatistic.AVG_LDAP_SEARCH_TIME, searchDuration.asMillis() );
+            pwmDomain.getStatisticsService().updateAverageValue( AvgStatistic.AVG_LDAP_SEARCH_TIME, searchDuration.asMillis() );
         }
 
         if ( results.isEmpty() )

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

@@ -22,6 +22,8 @@ package password.pwm.svc.stats;
 
 import password.pwm.i18n.Admin;
 import password.pwm.util.i18n.LocaleHelper;
+import password.pwm.util.java.JavaHelper;
+import password.pwm.util.java.TimeDuration;
 
 import java.util.Locale;
 
@@ -66,4 +68,17 @@ public enum AvgStatistic
         final String keyName = Admin.STATISTICS_DESCRIPTION_PREFIX + this.getKey();
         return LocaleHelper.getLocalizedMessage( locale, keyName, null, Admin.class );
     }
+
+    public String prettyValue( final String stringValue, final Locale locale )
+    {
+        final long longValue = JavaHelper.silentParseLong( stringValue, 0 );
+
+        if ( this == AVG_PASSWORD_STRENGTH )
+        {
+            return Long.toString( longValue );
+        }
+
+        final TimeDuration timeDuration = TimeDuration.of( longValue, TimeDuration.Unit.SECONDS );
+        return timeDuration.asCompactString();
+    }
 }

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

@@ -1,77 +0,0 @@
-/*
- * Password Management Servlets (PWM)
- * http://www.pwm-project.org
- *
- * Copyright (c) 2006-2009 Novell, Inc.
- * Copyright (c) 2009-2021 The PWM Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package 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 );
-    }
-}

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

@@ -1,25 +0,0 @@
-/*
- * Password Management Servlets (PWM)
- * http://www.pwm-project.org
- *
- * Copyright (c) 2006-2009 Novell, Inc.
- * Copyright (c) 2009-2021 The PWM Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package password.pwm.svc.stats;
-
-public interface StatKey
-{
-}

+ 258 - 0
server/src/main/java/password/pwm/svc/stats/StatisticsBundleKey.java

@@ -0,0 +1,258 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2021 The PWM Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package password.pwm.svc.stats;
+
+import password.pwm.util.java.EnumUtil;
+import password.pwm.util.java.StringUtil;
+
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+
+public class StatisticsBundleKey implements Comparable<StatisticsBundleKey>
+{
+    private static final String DB_KEY_PREFIX_DAILY = "DAILY_";
+
+    static final Comparator<StatisticsBundleKey> COMPARATOR = Comparator
+            .comparing( StatisticsBundleKey::getKeyType )
+            .thenComparingInt( StatisticsBundleKey::getYear )
+            .thenComparingInt( StatisticsBundleKey::getDay );
+
+    static final StatisticsBundleKey CUMULATIVE = new StatisticsBundleKey( KeyType.CUMULATIVE, -1, -1 );
+    static final StatisticsBundleKey CURRENT  = new StatisticsBundleKey( KeyType.CURRENT, -1, -1 );
+
+    enum KeyType
+    {
+        CUMULATIVE( "Cumulative - since install" ),
+        CURRENT( "Current - since startup" ),
+        DAILY( null ),;
+
+
+        private final String label;
+
+        KeyType( final String label )
+        {
+            this.label = label;
+        }
+
+        public String getLabel( final Locale locale )
+        {
+            return label;
+        }
+    }
+
+    private final KeyType keyType;
+    private final int year;
+    private final int day;
+
+    private StatisticsBundleKey( final KeyType keyType, final int year, final int day )
+    {
+        this.keyType = Objects.requireNonNull( keyType );
+        this.year = year;
+        this.day = day;
+        checkParameterValidity();
+    }
+
+    private void checkParameterValidity()
+    {
+        if ( keyType == KeyType.DAILY )
+        {
+            if ( year <= 0 || year > 99999 )
+            {
+                throw new IllegalArgumentException( "invalid year value '" + year + "'" );
+            }
+            if ( day <= 0 || day > 366 )
+            {
+                throw new IllegalArgumentException( "invalid day value '" + day + "'" );
+            }
+        }
+    }
+
+    @Override
+    public String toString( )
+    {
+        if ( keyType == KeyType.CURRENT || keyType == KeyType.CUMULATIVE )
+        {
+            return keyType.name();
+        }
+
+        return DB_KEY_PREFIX_DAILY + year + "_" + day;
+    }
+
+    @Override
+    public int compareTo( final StatisticsBundleKey otherKey )
+    {
+        return COMPARATOR.compare( this, otherKey );
+    }
+
+    public KeyType getKeyType()
+    {
+        return keyType;
+    }
+
+    public int getYear()
+    {
+        return year;
+    }
+
+    public int getDay()
+    {
+        return day;
+    }
+
+    @Override
+    public boolean equals( final Object o )
+    {
+        if ( this == o )
+        {
+            return true;
+        }
+        if ( o == null || getClass() != o.getClass() )
+        {
+            return false;
+        }
+        final StatisticsBundleKey that = ( StatisticsBundleKey ) o;
+        return year == that.year && day == that.day && keyType == that.keyType;
+    }
+
+    @Override
+    public int hashCode()
+    {
+        return Objects.hash( keyType, year, day );
+    }
+
+    public static StatisticsBundleKey fromString( final String input )
+    {
+        final Optional<KeyType> optionalInputKeyType = EnumUtil.readEnumFromString( KeyType.class, input );
+
+        if ( optionalInputKeyType.isPresent() )
+        {
+            final KeyType theKey = optionalInputKeyType.get();
+            if ( theKey == KeyType.CUMULATIVE )
+            {
+                return CUMULATIVE;
+            }
+            if ( theKey == KeyType.CURRENT )
+            {
+                return CURRENT;
+            }
+        }
+
+        final String strippedValue = input.substring( DB_KEY_PREFIX_DAILY.length() );
+        final String[] splitValue = strippedValue.split( "_" );
+        final int year = Integer.parseInt( splitValue[ 0 ] );
+        final int day = Integer.parseInt( splitValue[ 1 ] );
+        return new StatisticsBundleKey( KeyType.DAILY, year, day );
+    }
+
+    public static StatisticsBundleKey fromStringOrDefaultCumulative( final String input )
+    {
+        if ( StringUtil.isEmpty( input ) )
+        {
+            return CUMULATIVE;
+        }
+
+        try
+        {
+            return fromString( input );
+        }
+        catch ( final Exception e )
+        {
+            /* ignore */
+        }
+
+        return CUMULATIVE;
+    }
+
+    public static StatisticsBundleKey forToday()
+    {
+        final LocalDate localDate = LocalDate.now();
+        final int year = localDate.getYear();
+        final int day = localDate.getDayOfYear();
+
+        return new StatisticsBundleKey( KeyType.DAILY, year, day );
+    }
+
+    public StatisticsBundleKey previous( )
+    {
+        final LocalDate thisDay = localDate();
+        final LocalDate previousDay = thisDay.minusDays( 1 );
+        return new StatisticsBundleKey( KeyType.DAILY, previousDay.getYear(), previousDay.getDayOfYear() );
+    }
+
+    public LocalDate localDate()
+    {
+        return LocalDate.ofYearDay( year, day );
+    }
+
+    public String getLabel( final Locale locale )
+    {
+        if ( keyType == KeyType.DAILY )
+        {
+            return localDate().format( DateTimeFormatter.ISO_LOCAL_DATE.localizedBy( locale ) );
+        }
+        return keyType.getLabel( locale );
+    }
+
+    public static Set<StatisticsBundleKey> range( final StatisticsBundleKey key1, final StatisticsBundleKey key2 )
+    {
+        Objects.requireNonNull( key1 );
+        Objects.requireNonNull( key2 );
+
+        if ( key1.getKeyType() != KeyType.DAILY || key2.getKeyType() != KeyType.DAILY )
+        {
+            throw new IllegalArgumentException( "both keys must be of type DAILY" );
+        }
+        if ( key1.equals( key2 ) )
+        {
+            return Collections.singleton( key1 );
+        }
+
+        final List<StatisticsBundleKey> sortedInput = new ArrayList<>();
+        sortedInput.add( key1 );
+        sortedInput.add( key2 );
+        sortedInput.sort( COMPARATOR );
+
+        final StatisticsBundleKey firstKey = sortedInput.get( 0 );
+        final StatisticsBundleKey lastKey = sortedInput.get( 1 );
+
+        final List<StatisticsBundleKey> results = new ArrayList<>();
+        results.add( firstKey );
+        StatisticsBundleKey loopKey = lastKey;
+        int safetyCounter = 0;
+        while ( !loopKey.equals( firstKey ) && safetyCounter < 50000 )
+        {
+            results.add( loopKey );
+            loopKey = loopKey.previous();
+            safetyCounter++;
+        }
+        results.sort( COMPARATOR );
+        return Collections.unmodifiableSet( new LinkedHashSet<>( results ) );
+    }
+}

+ 3 - 3
server/src/main/java/password/pwm/svc/stats/StatisticsClient.java

@@ -56,7 +56,7 @@ public class StatisticsClient
     {
         if ( pwmApplication != null && pwmApplication.getApplicationMode() == PwmApplicationMode.RUNNING )
         {
-            final StatisticsService statisticsService = pwmApplication.getStatisticsManager();
+            final StatisticsService statisticsService = pwmApplication.getStatisticsService();
             if ( statisticsService != null && statisticsService.status() == PwmService.STATUS.OPEN )
             {
                 statisticsService.updateEps( type, itemCount );
@@ -72,7 +72,7 @@ public class StatisticsClient
             return;
         }
 
-        final StatisticsService statisticsManager = pwmApplication.getStatisticsManager();
+        final StatisticsService statisticsManager = pwmApplication.getStatisticsService();
         if ( statisticsManager == null )
         {
             LOGGER.error( () -> "skipping requested statistic increment of " + statistic + " due to null statisticsManager" );
@@ -94,7 +94,7 @@ public class StatisticsClient
             return;
         }
 
-        final StatisticsService statisticsManager = pwmApplication.getStatisticsManager();
+        final StatisticsService statisticsManager = pwmApplication.getStatisticsService();
         if ( statisticsManager == null )
         {
             LOGGER.error( () -> "skipping requested statistic increment of " + statistic + " due to null statisticsManager" );

+ 51 - 123
server/src/main/java/password/pwm/svc/stats/StatisticsService.java

@@ -20,7 +20,6 @@
 
 package password.pwm.svc.stats;
 
-import org.apache.commons.csv.CSVPrinter;
 import password.pwm.PwmApplication;
 import password.pwm.PwmConstants;
 import password.pwm.bean.DomainID;
@@ -33,25 +32,21 @@ import password.pwm.util.DailySummaryJob;
 import password.pwm.util.EventRateMeter;
 import password.pwm.util.java.CollectorUtil;
 import password.pwm.util.java.EnumUtil;
-import password.pwm.util.java.PwmUtil;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.TimeDuration;
 import password.pwm.util.localdb.LocalDB;
 import password.pwm.util.localdb.LocalDBException;
 import password.pwm.util.logging.PwmLogger;
 
-import java.io.IOException;
-import java.io.OutputStream;
 import java.math.BigDecimal;
 import java.time.Instant;
-import java.util.ArrayList;
 import java.util.Collections;
 import java.util.LinkedHashMap;
 import java.util.List;
-import java.util.Locale;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Optional;
+import java.util.SortedSet;
 import java.util.TimerTask;
 import java.util.stream.Collectors;
 
@@ -69,28 +64,16 @@ public class StatisticsService extends AbstractPwmService implements PwmService
 
     private static final String DB_VALUE_VERSION = "1";
 
-    public static final String KEY_CURRENT = "CURRENT";
-    public static final String KEY_CUMULATIVE = "CUMULATIVE";
-
     private LocalDB localDB;
 
-    private DailyKey currentDailyKey = DailyKey.forToday();
-    private DailyKey initialDailyKey = DailyKey.forToday();
+    private StatisticsBundleKey currentDailyKey = StatisticsBundleKey.forToday();
+    private StatisticsBundleKey initialDailyKey = StatisticsBundleKey.forToday();
 
     private final StatisticsBundle statsCurrent = new StatisticsBundle();
     private final Map<EpsKey, EventRateMeter> epsMeterMap;
     private StatisticsBundle statsDaily = new StatisticsBundle();
-    private StatisticsBundle statsCummulative = new StatisticsBundle();
-
+    private StatisticsBundle statsCumulative = new StatisticsBundle();
 
-    private final Map<String, StatisticsBundle> cachedStoredStats = new LinkedHashMap<>()
-    {
-        @Override
-        protected boolean removeEldestEntry( final Map.Entry<String, StatisticsBundle> eldest )
-        {
-            return this.size() > 50;
-        }
-    };
 
     public StatisticsService( )
     {
@@ -105,99 +88,96 @@ public class StatisticsService extends AbstractPwmService implements PwmService
     {
         statsCurrent.incrementValue( statistic );
         statsDaily.incrementValue( statistic );
-        statsCummulative.incrementValue( statistic );
+        statsCumulative.incrementValue( statistic );
     }
 
     public void updateAverageValue( final AvgStatistic statistic, final long value )
     {
         statsCurrent.updateAverageValue( statistic, value );
         statsDaily.updateAverageValue( statistic, value );
-        statsCummulative.updateAverageValue( statistic, value );
+        statsCumulative.updateAverageValue( statistic, value );
     }
 
-    public Map<String, String> getStatHistory( final Statistic statistic, final int days )
+    public Map<StatisticsBundleKey, String> getStatHistory( final Statistic statistic, final int days )
     {
-        final Map<String, String> returnMap = new LinkedHashMap<>();
-        DailyKey loopKey = currentDailyKey;
+        final Map<StatisticsBundleKey, String> returnMap = new LinkedHashMap<>();
+        StatisticsBundleKey loopKey = currentDailyKey;
         int counter = days;
         while ( counter > 0 )
         {
-            final StatisticsBundle bundle = getStatBundleForKey( loopKey.toString() );
-            if ( bundle != null )
+            final StatisticsBundleKey finalKey = loopKey;
+            getStatBundleForKey( loopKey ).ifPresent( bundle ->
             {
-                final String key = loopKey.toString();
                 final String value = bundle.getStatistic( statistic );
-                returnMap.put( key, value );
-            }
+                returnMap.put( finalKey, value );
+            } );
+
             loopKey = loopKey.previous();
             counter--;
         }
         return returnMap;
     }
 
-    public StatisticsBundle getStatBundleForKey( final String key )
+    public StatisticsBundle getCumulativeBundle()
     {
-        if ( key == null || key.length() < 1 || KEY_CUMULATIVE.equals( key ) )
-        {
-            return statsCummulative;
-        }
+        return getStatBundleForKey( StatisticsBundleKey.CUMULATIVE ).orElseThrow();
+    }
 
-        if ( KEY_CURRENT.equals( key ) )
+    public StatisticsBundle getCurrentBundle()
+    {
+        return getStatBundleForKey( StatisticsBundleKey.CURRENT ).orElseThrow();
+    }
+
+    public Optional<StatisticsBundle> getStatBundleForKey( final StatisticsBundleKey key )
+    {
+        Objects.requireNonNull( key );
+
+        if ( StatisticsBundleKey.CUMULATIVE == key )
         {
-            return statsCurrent;
+            return Optional.of( statsCumulative );
         }
 
-        if ( Objects.equals( currentDailyKey.toString(), key ) )
+        if ( StatisticsBundleKey.CURRENT == key )
         {
-            return statsDaily;
+            return Optional.of( statsCurrent );
         }
 
-        if ( cachedStoredStats.containsKey( key ) )
+        if ( Objects.equals( currentDailyKey, key ) )
         {
-            return cachedStoredStats.get( key );
+            return Optional.of( statsDaily );
         }
 
         if ( localDB == null )
         {
-            return null;
+            return Optional.empty();
         }
 
         try
         {
-            final Optional<String> storedStat = localDB.get( LocalDB.DB.PWM_STATS, key );
-            final StatisticsBundle returnBundle;
-            returnBundle = storedStat.map( StatisticsBundle::input ).orElseGet( StatisticsBundle::new );
-            cachedStoredStats.put( key, returnBundle );
-            return returnBundle;
+            final Optional<String> storedStat = localDB.get( LocalDB.DB.PWM_STATS, key.toString() );
+            return storedStat.map( StatisticsBundle::input );
         }
         catch ( final LocalDBException e )
         {
             LOGGER.error( () -> "error retrieving stored stat for " + key + ": " + e.getMessage() );
         }
 
-        return null;
+        return Optional.empty();
     }
 
-    public Map<DailyKey, String> getAvailableKeys( final Locale locale )
+    StatisticsBundleKey getCurrentDailyKey()
     {
-        final Map<DailyKey, String> returnMap = new LinkedHashMap<>();
+        return currentDailyKey;
+    }
 
-        // if no historical data then we're done
-        if ( currentDailyKey.equals( initialDailyKey ) )
-        {
-            return returnMap;
-        }
+    StatisticsBundleKey getInitialDailyKey()
+    {
+        return initialDailyKey;
+    }
 
-        DailyKey loopKey = currentDailyKey;
-        int safetyCounter = 0;
-        while ( !loopKey.equals( initialDailyKey ) && safetyCounter < 5000 )
-        {
-            final String display = loopKey.toString();
-            returnMap.put( loopKey, display );
-            loopKey = loopKey.previous();
-            safetyCounter++;
-        }
-        return returnMap;
+    public SortedSet<StatisticsBundleKey> allKeys()
+    {
+        return StatisticsUtils.allKeys( this );
     }
 
     public String toString( )
@@ -238,7 +218,7 @@ public class StatisticsService extends AbstractPwmService implements PwmService
             {
                 try
                 {
-                    statsCummulative = StatisticsBundle.input( storedCumulativeBundleSir.get() );
+                    statsCumulative = StatisticsBundle.input( storedCumulativeBundleSir.get() );
                 }
                 catch ( final Exception e )
                 {
@@ -249,11 +229,11 @@ public class StatisticsService extends AbstractPwmService implements PwmService
 
         {
             final Optional<String> storedInitialString = localDB.get( LocalDB.DB.PWM_STATS, DB_KEY_INITIAL_DAILY_KEY );
-            storedInitialString.ifPresent( s -> initialDailyKey = new DailyKey( s ) );
+            storedInitialString.ifPresent( s -> initialDailyKey = StatisticsBundleKey.fromString( s ) );
         }
 
         {
-            currentDailyKey = DailyKey.forToday();
+            currentDailyKey = StatisticsBundleKey.forToday();
             final Optional<String> storedDailyStr = localDB.get( LocalDB.DB.PWM_STATS, currentDailyKey.toString() );
             storedDailyStr.ifPresent( s -> statsDaily = StatisticsBundle.input( s ) );
         }
@@ -288,7 +268,7 @@ public class StatisticsService extends AbstractPwmService implements PwmService
             try
             {
                 final Map<String, String> dbData = new LinkedHashMap<>();
-                dbData.put( DB_KEY_CUMULATIVE, statsCummulative.output() );
+                dbData.put( DB_KEY_CUMULATIVE, statsCumulative.output() );
                 dbData.put( currentDailyKey.toString(), statsDaily.output() );
                 localDB.putAll( LocalDB.DB.PWM_STATS, dbData );
             }
@@ -309,7 +289,7 @@ public class StatisticsService extends AbstractPwmService implements PwmService
 
     private void resetDailyStats( )
     {
-        currentDailyKey = DailyKey.forToday();
+        currentDailyKey = StatisticsBundleKey.forToday();
         statsDaily = new StatisticsBundle();
         LOGGER.debug( () -> "reset daily statistics" );
     }
@@ -369,58 +349,6 @@ public class StatisticsService extends AbstractPwmService implements PwmService
         return epsMeterMap.get( epsKey ).rawEps();
     }
 
-
-    public int outputStatsToCsv( final OutputStream outputStream, final Locale locale, final boolean includeHeader )
-            throws IOException
-    {
-        LOGGER.trace( () -> "beginning output stats to csv process" );
-        final Instant startTime = Instant.now();
-
-        final StatisticsService statsManger = getPwmApplication().getStatisticsManager();
-        final CSVPrinter csvPrinter = PwmUtil.makeCsvPrinter( outputStream );
-
-        if ( includeHeader )
-        {
-            final List<String> headers = Statistic.asSet().stream()
-                    .map( stat -> stat.getLabel( locale ) )
-                    .collect( Collectors.toList() );
-
-            headers.add( "KEY" );
-            headers.add( "YEAR" );
-            headers.add( "DAY" );
-
-            csvPrinter.printRecord( headers );
-        }
-
-        int counter = 0;
-        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<>( Statistic.asSet().size() );
-
-            lineOutput.add( loopKey.toString() );
-            lineOutput.add( String.valueOf( loopKey.getYear() ) );
-            lineOutput.add( String.valueOf( loopKey.getDay() ) );
-
-            lineOutput.addAll( EnumUtil.enumStream( Statistic.class )
-                    .map( bundle::getStatistic )
-                    .collect( Collectors.toList() ) );
-
-            csvPrinter.printRecord( lineOutput );
-        }
-
-        csvPrinter.flush();
-        {
-            final int finalCounter = counter;
-            LOGGER.trace( () -> "completed output stats to csv process; output " + finalCounter + " records in "
-                    + TimeDuration.compactFromCurrent( startTime ) );
-        }
-        return counter;
-    }
-
     @Override
     public ServiceInfoBean serviceInfo( )
     {

+ 166 - 0
server/src/main/java/password/pwm/svc/stats/StatisticsUtils.java

@@ -0,0 +1,166 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2021 The PWM Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package password.pwm.svc.stats;
+
+import org.apache.commons.csv.CSVPrinter;
+import password.pwm.bean.SessionLabel;
+import password.pwm.http.bean.DisplayElement;
+import password.pwm.util.java.EnumUtil;
+import password.pwm.util.java.PwmUtil;
+import password.pwm.util.java.TimeDuration;
+import password.pwm.util.logging.PwmLogger;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+import java.util.SortedSet;
+import java.util.TreeSet;
+import java.util.stream.Collectors;
+
+public class StatisticsUtils
+{
+    private static final PwmLogger LOGGER = PwmLogger.forClass( StatisticsUtils.class );
+
+    public enum CsvOutputFlag
+    {
+        includeHeader;
+    }
+
+    public static int outputStatsToCsv(
+            final SessionLabel sessionLabel,
+            final StatisticsService statisticsService,
+            final OutputStream outputStream,
+            final Locale locale,
+            final CsvOutputFlag... flags
+    )
+            throws IOException
+    {
+        LOGGER.trace( sessionLabel, () -> "beginning output stats to csv process" );
+        final Instant startTime = Instant.now();
+        final boolean includeHeader = EnumUtil.enumArrayContainsValue( flags, CsvOutputFlag.includeHeader );
+
+        final CSVPrinter csvPrinter = PwmUtil.makeCsvPrinter( outputStream );
+
+        if ( includeHeader )
+        {
+            final List<String> headers = Statistic.asSet().stream()
+                    .map( stat -> stat.getLabel( locale ) )
+                    .collect( Collectors.toList() );
+
+            headers.add( "KEY" );
+            headers.add( "YEAR" );
+            headers.add( "DAY" );
+
+            csvPrinter.printRecord( headers );
+        }
+
+        int counter = 0;
+        for ( final StatisticsBundleKey loopKey : allKeys( statisticsService ) )
+        {
+            counter++;
+            final StatisticsBundle bundle = statisticsService.getStatBundleForKey( loopKey )
+                    .orElseThrow();
+
+            final List<String> lineOutput = new ArrayList<>( Statistic.asSet().size() );
+
+            lineOutput.add( loopKey.toString() );
+
+            if ( loopKey.getKeyType() == StatisticsBundleKey.KeyType.DAILY )
+            {
+                lineOutput.add( Integer.toString( loopKey.getYear() ) );
+                lineOutput.add( Integer.toString( loopKey.getDay() ) );
+            }
+            else
+            {
+                lineOutput.add( "" );
+                lineOutput.add( "" );
+            }
+
+            lineOutput.addAll( EnumUtil.enumStream( Statistic.class )
+                    .map( bundle::getStatistic )
+                    .collect( Collectors.toList() ) );
+
+            csvPrinter.printRecord( lineOutput );
+        }
+
+        {
+            final int finalCounter = counter;
+            LOGGER.trace( sessionLabel, () -> "completed output stats to csv process; output "
+                    + finalCounter + " records in "
+                    + TimeDuration.compactFromCurrent( startTime ) );
+        }
+
+        return counter;
+    }
+
+    static SortedSet<StatisticsBundleKey> allKeys( final StatisticsService statisticsServices )
+    {
+        final SortedSet<StatisticsBundleKey> results = new TreeSet<>();
+        results.add( StatisticsBundleKey.CUMULATIVE );
+        results.add( StatisticsBundleKey.CURRENT );
+
+        // if no historical data then we're done
+        if ( statisticsServices.getInitialDailyKey().equals( statisticsServices.getCurrentDailyKey() ) )
+        {
+            return Collections.emptySortedSet();
+        }
+
+        results.addAll( StatisticsBundleKey.range(
+                statisticsServices.getInitialDailyKey(),
+                statisticsServices.getCurrentDailyKey() ) );
+
+        return Collections.unmodifiableSortedSet( results );
+    }
+
+    public static List<DisplayElement> statsDisplayElementsForBundle(
+            final String keyPrefix,
+            final Locale locale,
+            final StatisticsBundle statisticsBundle
+    )
+    {
+        return Statistic.sortedValues( locale ).stream()
+                .map( stat -> new DisplayElement(
+                        keyPrefix + stat.getKey(),
+                        DisplayElement.Type.number,
+                        stat.getLabel( locale ),
+                        statisticsBundle.getStatistic( stat )
+                ) ).collect( Collectors.toUnmodifiableList() );
+    }
+
+    public static List<DisplayElement> avgStatsDisplayElementsForBundle(
+            final String keyPrefix,
+            final Locale locale,
+            final StatisticsBundle statisticsBundle
+    )
+    {
+        return EnumUtil.enumStream( AvgStatistic.class )
+                .map( stat -> new DisplayElement(
+                        keyPrefix + stat.getKey(),
+                        DisplayElement.Type.string,
+                        stat.getLabel( locale ),
+                        stat.prettyValue( statisticsBundle.getAvgStatistic( stat ), locale )
+                ) ).collect( Collectors.toUnmodifiableList() );
+    }
+}

+ 3 - 4
server/src/main/java/password/pwm/svc/telemetry/TelemetryService.java

@@ -46,7 +46,6 @@ import password.pwm.svc.AbstractPwmService;
 import password.pwm.svc.PwmService;
 import password.pwm.svc.stats.Statistic;
 import password.pwm.svc.stats.StatisticsBundle;
-import password.pwm.svc.stats.StatisticsService;
 import password.pwm.util.java.CollectionUtil;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.TimeDuration;
@@ -103,7 +102,7 @@ public class TelemetryService extends AbstractPwmService implements PwmService
             return STATUS.CLOSED;
         }
 
-        if ( pwmApplication.getStatisticsManager().status() != STATUS.OPEN )
+        if ( pwmApplication.getStatisticsService().status() != STATUS.OPEN )
         {
             LOGGER.trace( getSessionLabel(), () -> "will remain closed, statistics manager is not enabled" );
             return STATUS.CLOSED;
@@ -165,7 +164,7 @@ public class TelemetryService extends AbstractPwmService implements PwmService
 
     private void executePublishJob( ) throws PwmUnrecoverableException, IOException, URISyntaxException
     {
-        final String authValue = getPwmApplication().getStatisticsManager().getStatBundleForKey( StatisticsService.KEY_CUMULATIVE ).getStatistic( Statistic.AUTHENTICATIONS );
+        final String authValue = getPwmApplication().getStatisticsService().getCumulativeBundle().getStatistic( Statistic.AUTHENTICATIONS );
         if ( StringUtil.isEmpty( authValue ) || Integer.parseInt( authValue ) < settings.getMinimumAuthentications() )
         {
             LOGGER.trace( getSessionLabel(), () -> "skipping telemetry send, authentication count is too low" );
@@ -250,7 +249,7 @@ public class TelemetryService extends AbstractPwmService implements PwmService
     public TelemetryPublishBean generatePublishableBean( )
             throws PwmUnrecoverableException
     {
-        final StatisticsBundle bundle = getPwmApplication().getStatisticsManager().getStatBundleForKey( StatisticsService.KEY_CUMULATIVE );
+        final StatisticsBundle bundle = getPwmApplication().getStatisticsService().getCumulativeBundle();
         final AppConfig config = getPwmApplication().getConfig();
         final Map<PwmAboutProperty, String> aboutPropertyStringMap = PwmAboutProperty.makeInfoBean( getPwmApplication() );
 

+ 2 - 2
server/src/main/java/password/pwm/util/DailySummaryJob.java

@@ -113,13 +113,13 @@ public class DailySummaryJob implements Runnable
             return;
         }
 
-        if ( pwmDomain.getStatisticsManager().status() != PwmService.STATUS.OPEN )
+        if ( pwmDomain.getStatisticsService().status() != PwmService.STATUS.OPEN )
         {
             LOGGER.debug( () -> "skipping daily summary alert job, statistics service is not open" );
             return;
         }
 
-        final Map<String, String> dailyStatistics = pwmDomain.getStatisticsManager().dailyStatisticsAsLabelValueMap();
+        final Map<String, String> dailyStatistics = pwmDomain.getStatisticsService().dailyStatisticsAsLabelValueMap();
 
         final Locale locale = PwmConstants.DEFAULT_LOCALE;
 

+ 10 - 4
server/src/main/java/password/pwm/util/cli/commands/ExportStatsCommand.java

@@ -21,7 +21,10 @@
 package password.pwm.util.cli.commands;
 
 import password.pwm.PwmApplication;
+import password.pwm.PwmConstants;
+import password.pwm.bean.SessionLabel;
 import password.pwm.svc.stats.StatisticsService;
+import password.pwm.svc.stats.StatisticsUtils;
 import password.pwm.util.cli.CliParameters;
 import password.pwm.util.java.PwmTimeUtil;
 import password.pwm.util.java.TimeDuration;
@@ -30,7 +33,6 @@ import java.io.File;
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.util.Collections;
-import java.util.Locale;
 
 public class ExportStatsCommand extends AbstractCliCommand
 {
@@ -40,7 +42,7 @@ public class ExportStatsCommand extends AbstractCliCommand
             throws IOException
     {
         final PwmApplication pwmApplication = cliEnvironment.getPwmApplication();
-        final StatisticsService statsManger = pwmApplication.getStatisticsManager();
+        final StatisticsService statsManger = pwmApplication.getStatisticsService();
 
         final File outputFile = ( File ) cliEnvironment.getOptions().get( CliParameters.REQUIRED_NEW_OUTPUT_FILE.getName() );
         final long startTime = System.currentTimeMillis();
@@ -48,8 +50,12 @@ public class ExportStatsCommand extends AbstractCliCommand
         final int counter;
         try ( FileOutputStream fileOutputStream = new FileOutputStream( outputFile, true ) )
         {
-            counter = statsManger.outputStatsToCsv( fileOutputStream, Locale.getDefault(), true );
-            fileOutputStream.close();
+            counter = StatisticsUtils.outputStatsToCsv(
+                    SessionLabel.CLI_SESSION_LABEL,
+                    statsManger,
+                    fileOutputStream,
+                    PwmConstants.DEFAULT_LOCALE,
+                    StatisticsUtils.CsvOutputFlag.includeHeader );
         }
         out( "completed writing " + counter + " rows of stats output in " + PwmTimeUtil.asLongString( TimeDuration.fromCurrent( startTime ) ) );
     }

+ 8 - 2
server/src/main/java/password/pwm/util/debug/StatisticsDataDebugItemGenerator.java

@@ -22,6 +22,7 @@ package password.pwm.util.debug;
 
 import password.pwm.PwmApplication;
 import password.pwm.svc.stats.StatisticsService;
+import password.pwm.svc.stats.StatisticsUtils;
 
 import java.io.IOException;
 import java.io.OutputStream;
@@ -39,7 +40,12 @@ class StatisticsDataDebugItemGenerator implements AppItemGenerator
             throws IOException
     {
         final PwmApplication pwmApplication = debugItemInput.getPwmApplication();
-        final StatisticsService statsManager = pwmApplication.getStatisticsManager();
-        statsManager.outputStatsToCsv( outputStream, debugItemInput.getLocale(), true );
+        final StatisticsService statsManager = pwmApplication.getStatisticsService();
+        StatisticsUtils.outputStatsToCsv(
+                debugItemInput.getSessionLabel(),
+                statsManager,
+                outputStream,
+                debugItemInput.getLocale(),
+                StatisticsUtils.CsvOutputFlag.includeHeader );
     }
 }

+ 1 - 1
server/src/main/java/password/pwm/util/debug/StatisticsEpsDataDebugItemGenerator.java

@@ -51,7 +51,7 @@ class StatisticsEpsDataDebugItemGenerator implements AppItemGenerator
 
     {
         final PwmApplication pwmDomain = debugItemInput.getPwmApplication();
-        final StatisticsService statsManager = pwmDomain.getStatisticsManager();
+        final StatisticsService statsManager = pwmDomain.getStatisticsService();
         final CSVPrinter csvPrinter = PwmUtil.makeCsvPrinter( outputStream );
         {
             final List<String> headerRow = new ArrayList<>();

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

@@ -484,10 +484,10 @@ public class PasswordUtility
         }
 
         // update stats
-        pwmDomain.getStatisticsManager().updateEps( EpsStatistic.PASSWORD_CHANGES, 1 );
+        pwmDomain.getStatisticsService().updateEps( EpsStatistic.PASSWORD_CHANGES, 1 );
 
         final int passwordStrength = PasswordUtility.judgePasswordStrength( pwmDomain.getConfig(), newPassword.getStringValue() );
-        pwmDomain.getStatisticsManager().updateAverageValue( AvgStatistic.AVG_PASSWORD_STRENGTH, passwordStrength );
+        pwmDomain.getStatisticsService().updateAverageValue( AvgStatistic.AVG_PASSWORD_STRENGTH, passwordStrength );
 
         // at this point the password has been changed, so log it.
         final String msg = ( bindIsSelf

+ 29 - 25
server/src/main/java/password/pwm/ws/server/rest/RestStatisticsServer.java

@@ -33,7 +33,7 @@ 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.StatisticsBundleKey;
 import password.pwm.svc.stats.EpsStatistic;
 import password.pwm.svc.stats.Statistic;
 import password.pwm.svc.stats.StatisticType;
@@ -59,7 +59,6 @@ import java.time.ZoneOffset;
 import java.time.format.DateTimeFormatter;
 import java.util.ArrayList;
 import java.util.Collections;
-import java.util.HashMap;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
@@ -161,10 +160,10 @@ public class RestStatisticsServer extends RestServlet
                     MAX_DAYS
             );
 
-            final StatisticsService statisticsManager = restRequest.getDomain().getStatisticsManager();
+            final StatisticsService statisticsManager = restRequest.getDomain().getStatisticsService();
             final JsonOutput jsonOutput = RestStatisticsServer.JsonOutput.builder()
-                    .cumulative( makeStatInfos( statisticsManager, StatisticsService.KEY_CUMULATIVE ) )
-                    .current( makeStatInfos( statisticsManager, StatisticsService.KEY_CURRENT ) )
+                    .cumulative( makeStatInfos( statisticsManager.getCumulativeBundle() ) )
+                    .current( makeStatInfos( statisticsManager.getCurrentBundle() ) )
                     .eventRates( makeEpsStatInfos( statisticsManager ) )
                     .history( makeHistoryStatInfos( statisticsManager, days ) )
                     .labels( makeLabels( locale ) )
@@ -172,12 +171,12 @@ public class RestStatisticsServer extends RestServlet
             return RestResultBean.withData( jsonOutput, JsonOutput.class );
         }
 
-        private static List<StatValue> makeStatInfos( final StatisticsService statisticsManager, final String key )
+        private static List<StatValue> makeStatInfos( final StatisticsBundle bundle )
         {
             final Map<String, StatValue> output = EnumUtil.enumStream( Statistic.class )
                     .collect( Collectors.toMap(
                             Enum::name,
-                            stat -> new StatValue( stat.name(), statisticsManager.getStatBundleForKey( key ).getStatistic( stat ) )
+                            stat -> new StatValue( stat.name(), bundle.getStatistic( stat ) )
                     ) );
 
             return List.copyOf( new TreeMap<>( output ).values() );
@@ -190,17 +189,19 @@ public class RestStatisticsServer extends RestServlet
         {
             final List<HistoryData> outerOutput = new ArrayList<>( days );
 
-            DailyKey dailyKey = DailyKey.forToday();
+            StatisticsBundleKey dailyKey = StatisticsBundleKey.forToday();
 
             for ( int daysAgo = 0; daysAgo < days; daysAgo++ )
             {
                 final Map<String, StatValue> output = new TreeMap<>();
                 for ( final Statistic statistic : Statistic.values() )
                 {
-                    final StatisticsBundle bundle = statisticsManager.getStatBundleForKey( dailyKey.toString() );
-                    final String value = bundle.getStatistic( statistic );
-                    final StatValue statValue = new StatValue( statistic.name(), value );
-                    output.put( statistic.name(), statValue );
+                    statisticsManager.getStatBundleForKey( dailyKey ).ifPresent( bundle ->
+                    {
+                        final String value = bundle.getStatistic( statistic );
+                        final StatValue statValue = new StatValue( statistic.name(), value );
+                        output.put( statistic.name(), statValue );
+                    } );
                 }
                 final List<StatValue> statValues = List.copyOf( output.values() );
                 final HistoryData historyData = HistoryData.builder()
@@ -298,7 +299,7 @@ public class RestStatisticsServer extends RestServlet
 
             try
             {
-                final StatisticsService statisticsManager = restRequest.getDomain().getStatisticsManager();
+                final StatisticsService statisticsManager = restRequest.getDomain().getStatisticsService();
                 final JsonOutput jsonOutput = new JsonOutput();
                 jsonOutput.EPS = addEpsStats( statisticsManager );
 
@@ -325,26 +326,29 @@ public class RestStatisticsServer extends RestServlet
 
         public static Map<String, Object> doNameStat( final StatisticsService statisticsManager, final String statName, final String days )
         {
-            final Statistic statistic = Statistic.valueOf( statName );
+            final Statistic statistic = Statistic.forKey( statName ).orElseThrow();
             final int historyDays = StringUtil.convertStrToInt( days, 30 );
 
-            return new HashMap<>( statisticsManager.getStatHistory( statistic, historyDays ) );
+            return statisticsManager.getStatHistory( statistic, historyDays )
+                    .entrySet().stream().collect( Collectors.toUnmodifiableMap(
+                            entry -> entry.getKey().toString(),
+                            Map.Entry::getValue
+                    ) );
         }
 
-        public static Map<String, Object> doKeyStat( final StatisticsService statisticsManager, final String statKey )
+        public static Map<String, Object> doKeyStat( final StatisticsService statisticsManager, final String keyInput )
         {
-            final String key = ( statKey == null )
-                    ? StatisticsService.KEY_CUMULATIVE
-                    : statKey;
+            final StatisticsBundleKey key = StatisticsBundleKey.fromStringOrDefaultCumulative( keyInput );
 
-            final StatisticsBundle statisticsBundle = statisticsManager.getStatBundleForKey( key );
             final Map<String, Object> outputValueMap = new TreeMap<>();
-            for ( final Statistic stat : Statistic.values() )
+            statisticsManager.getStatBundleForKey( key ).ifPresent( statisticsBundle ->
             {
-                outputValueMap.put( stat.name(), statisticsBundle.getStatistic( stat ) );
-            }
-
-            return outputValueMap;
+                for ( final Statistic stat : Statistic.values() )
+                {
+                    outputValueMap.put( stat.getKey(), statisticsBundle.getStatistic( stat ) );
+                }
+            } );
+            return Collections.unmodifiableMap( outputValueMap );
         }
 
         public static Map<String, String> addEpsStats( final StatisticsService statisticsManager )

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

@@ -315,7 +315,7 @@ Title_DirectoryReporting=Directory Reporting
 Title_DataViewer=Data Viewer
 Title_DirectoryReport=Directory Report
 Title_EventStatistics=Event Statistics
-Title_RawStatistics=Raw Statistics
+Title_Statistics=Statistics
 Title_StatisticsCharts=Event Charts
 Title_Sessions=Active Web Sessions
 Title_Intruders=Intruders

+ 75 - 0
server/src/test/java/password/pwm/svc/stats/StatisticsBundleKeyTest.java

@@ -0,0 +1,75 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2021 The PWM Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package password.pwm.svc.stats;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
+class StatisticsBundleKeyTest
+{
+    @Test
+    public void testKeySorting()
+    {
+        final List<StatisticsBundleKey> list = new ArrayList<>();
+        list.add( StatisticsBundleKey.fromString( "CURRENT" ) );
+        list.add( StatisticsBundleKey.fromString( "DAILY_2000_1" ) );
+        list.add( StatisticsBundleKey.fromString( "DAILY_2001_5" ) );
+        list.add( StatisticsBundleKey.fromString( "DAILY_2000_5" ) );
+        list.add( StatisticsBundleKey.fromString( "CUMULATIVE" ) );
+        Collections.sort( list );
+
+        Assertions.assertEquals( StatisticsBundleKey.CUMULATIVE, list.get( 0 ) );
+        Assertions.assertEquals( StatisticsBundleKey.CURRENT, list.get( 1 ) );
+
+        Assertions.assertEquals( 2000, list.get( 2 ).getYear() );
+        Assertions.assertEquals( 1, list.get( 2 ).getDay() );
+
+        Assertions.assertEquals( 2000, list.get( 3 ).getYear() );
+        Assertions.assertEquals( 5, list.get( 3 ).getDay() );
+
+        Assertions.assertEquals( 2001, list.get( 4 ).getYear() );
+        Assertions.assertEquals( 5, list.get( 4 ).getDay() );
+    }
+
+    @Test
+    public void testKeyRange()
+    {
+        final Set<StatisticsBundleKey> set = StatisticsBundleKey.range(
+                StatisticsBundleKey.fromString( "DAILY_2000_15" ),
+                StatisticsBundleKey.fromString( "DAILY_2000_10" ) );
+
+        final List<StatisticsBundleKey> list = new ArrayList<>( set );
+
+        Assertions.assertEquals( 6, list.size() );
+        Assertions.assertEquals( StatisticsBundleKey.fromString( "DAILY_2000_10" ), list.get( 0 ) );
+        Assertions.assertEquals( StatisticsBundleKey.fromString( "DAILY_2000_11" ), list.get( 1 ) );
+        Assertions.assertEquals( StatisticsBundleKey.fromString( "DAILY_2000_12" ), list.get( 2 ) );
+        Assertions.assertEquals( StatisticsBundleKey.fromString( "DAILY_2000_13" ), list.get( 3 ) );
+        Assertions.assertEquals( StatisticsBundleKey.fromString( "DAILY_2000_14" ), list.get( 4 ) );
+        Assertions.assertEquals( StatisticsBundleKey.fromString( "DAILY_2000_15" ), list.get( 5 ) );
+    }
+}
+

+ 0 - 213
webapp/src/main/webapp/WEB-INF/jsp/admin-analysis.jsp

@@ -1,213 +0,0 @@
-<%--
- ~ Password Management Servlets (PWM)
- ~ http://www.pwm-project.org
- ~
- ~ Copyright (c) 2006-2009 Novell, Inc.
- ~ Copyright (c) 2009-2021 The PWM Project
- ~
- ~ Licensed under the Apache License, Version 2.0 (the "License");
- ~ you may not use this file except in compliance with the License.
- ~ You may obtain a copy of the License at
- ~
- ~     http://www.apache.org/licenses/LICENSE-2.0
- ~
- ~ Unless required by applicable law or agreed to in writing, software
- ~ distributed under the License is distributed on an "AS IS" BASIS,
- ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- ~ See the License for the specific language governing permissions and
- ~ limitations under the License.
---%>
-<%--
-       THIS FILE IS NOT INTENDED FOR END USER MODIFICATION.
-       See the README.TXT file in WEB-INF/jsp before making changes.
---%>
-
-
-<%@ 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.StatisticsService" %>
-<%@ page import="password.pwm.util.java.JavaHelper" %>
-<%@ page import="java.util.Locale" %>
-<%@ page import="java.util.Map" %>
-<%@ page import="password.pwm.util.java.StringUtil" %>
-<%@ page import="java.time.format.DateTimeFormatter" %>
-<%@ page import="password.pwm.util.java.PwmUtil" %>
-
-<!DOCTYPE html>
-<%@ page language="java" session="true" isThreadSafe="true" contentType="text/html" %>
-<%@ taglib uri="pwm" prefix="pwm" %>
-<%
-    final Locale locale = JspUtility.locale(request);
-
-    StatisticsService statsManager = null;
-    String statsPeriodSelect = "";
-    String statsChartSelect = "";
-    StatisticsBundle stats = null;
-    PwmRequest analysis_pwmRequest = null;
-    try {
-        analysis_pwmRequest = PwmRequest.forRequest(request, response);
-        statsManager = analysis_pwmRequest.getPwmDomain().getStatisticsManager();
-        statsPeriodSelect = analysis_pwmRequest.readParameterAsString("statsPeriodSelect");
-        statsChartSelect = analysis_pwmRequest.readParameterAsString("statsChartSelect",Statistic.PASSWORD_CHANGES.toString());
-        stats = statsManager.getStatBundleForKey(statsPeriodSelect);
-    } catch (PwmException e) {
-        JspUtility.logError(pageContext, "error during page setup: " + e.getMessage());
-    }
-%>
-<html lang="<pwm:value name="<%=PwmValue.localeCode%>"/>" dir="<pwm:value name="<%=PwmValue.localeDir%>"/>">
-<%@ include file="/WEB-INF/jsp/fragment/header.jsp" %>
-<body class="nihilo">
-<div id="wrapper">
-    <% final String PageName = JspUtility.localizedString(pageContext,"Title_DataAnalysis",Admin.class);%>
-    <jsp:include page="/WEB-INF/jsp/fragment/header-body.jsp">
-        <jsp:param name="pwm.PageName" value="<%=PageName%>"/>
-    </jsp:include>
-    <div id="centerbody" class="wide">
-        <h1 id="page-content-title"><pwm:display key="Title_DataAnalysis" bundle="Admin"/></h1>
-        <%@ include file="fragment/admin-nav.jsp" %>
-        <div class="tab-container" style="width: 100%; height: 100%;">
-            <input name="es_tabs" type="radio" id="tab-2.1" checked="checked" class="input"/>
-            <label for="tab-2.1" class="label"><pwm:display key="Title_RawStatistics" bundle="Admin"/></label>
-            <div class="tab-content-pane" title="<pwm:display key="Title_RawStatistics" bundle="Admin"/>" class="tabContent">
-                <div style="max-height: 500px; overflow-y: auto">
-                    <table>
-                        <tr>
-                            <td colspan="10" style="text-align: center">
-                                <form action="<pwm:current-url/>" method="GET" enctype="application/x-www-form-urlencoded"
-                                      name="statsUpdateForm" id="statsUpdateForm">
-                                    <select name="statsPeriodSelect"
-                                            style="width: 350px;">
-                                        <option value="<%=StatisticsService.KEY_CUMULATIVE%>" <%= StatisticsService.KEY_CUMULATIVE.equals(statsPeriodSelect) ? "selected=\"selected\"" : "" %>>
-                                            since installation - <span class="timestamp"><%= StringUtil.toIsoDate(analysis_pwmRequest.getPwmApplication().getInstallTime()) %></span>
-                                        </option>
-                                        <option value="<%=StatisticsService.KEY_CURRENT%>" <%= StatisticsService.KEY_CURRENT.equals(statsPeriodSelect) ? "selected=\"selected\"" : "" %>>
-                                            since startup - <span class="timestamp"><%= StringUtil.toIsoDate(analysis_pwmRequest.getPwmApplication().getStartupTime()) %></span>
-                                        </option>
-                                        <% 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\"" : "" %>>
-                                            <%=key.localDate().format(DateTimeFormatter.ISO_LOCAL_DATE)%>
-                                        </option>
-                                        <% } %>
-                                    </select>
-                                    <button class="btn" type="submit">
-                                        <pwm:if test="<%=PwmIfTest.showIcons%>"><span class="btn-icon pwm-icon pwm-icon-refresh">&nbsp;</span></pwm:if>
-                                        <pwm:display key="Button_Refresh" bundle="Admin"/>
-                                    </button>
-                                </form>
-                            </td>
-                        </tr>
-                        <% for (final Statistic loopStat : Statistic.sortedValues(locale)) { %>
-                        <tr>
-                            <td >
-                                        <span id="Statistic_Key_<%=loopStat.getKey()%>"><%= loopStat.getLabel(locale) %><span/>
-                            </td>
-                            <td>
-                                <%= 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>
-                        <% } %>
-                    </table>
-                </div>
-                <div class="noticebar">
-                    <pwm:display key="Notice_EventStatistics" bundle="Admin"/>
-                </div>
-                <div style="text-align: center">
-                    <form class="submitToDownloadForm" action="<pwm:current-url/>" method="post" enctype="application/x-www-form-urlencoded">
-                        <button type="submit" class="btn" id="button-downloadStatisticsLogCsv">
-                            <pwm:if test="<%=PwmIfTest.showIcons%>"><span class="btn-icon pwm-icon pwm-icon-download"></span></pwm:if>
-                            <pwm:display key="Button_DownloadCSV" bundle="Admin"/>
-                        </button>
-                        <input type="hidden" name="processAction" value="downloadStatisticsLogCsv"/>
-                        <input type="hidden" name="pwmFormID" value="<pwm:FormID/>"/>
-                    </form>
-                </div>
-            </div>
-
-            <input name="es_tabs" type="radio" id="tab-2.2" class="input"/>
-            <label for="tab-2.2" class="label"><pwm:display key="Title_StatisticsCharts" bundle="Admin"/></label>
-            <div class="tab-content-pane" title="<pwm:display key="Title_StatisticsCharts" bundle="Admin"/>" class="tabContent">
-                <div style="height:100%; width: 100%">
-                    <div id="statsChartOptionsDiv" style="width:580px; text-align: center; margin:0 auto;">
-                        <label for="statsChartSelect">Statistic</label>
-                        <select name="statsChartSelect" id="statsChartSelect" style="width: 300px;">
-                            <% for (final Statistic loopStat : Statistic.sortedValues(locale)) { %>
-                            <option value="<%=loopStat %>"><%=loopStat.getLabel(locale)%></option>
-                            <% } %>
-                        </select>
-                        <label for="statsChartDays" style="padding-left: 10px">Days</label>
-                        <input id="statsChartDays" value="30" type="number" style="width: 60px" min="7" max="120"/>
-                    </div>
-                    <div id="statsChart">
-                    </div>
-                </div>
-            </div>
-
-            <div class="tab-end"></div>
-        </div>
-    </div>
-    <div class="push"></div>
-</div>
-<pwm:script>
-    <script type="text/javascript">
-        function refreshChart() {
-            var statsChartSelect = PWM_MAIN.getObject('statsChartSelect');
-            var keyName = statsChartSelect.options[statsChartSelect.selectedIndex].value;
-            var days = PWM_MAIN.getObject('statsChartDays').value;
-            PWM_ADMIN.showStatChart(keyName,days,'statsChart',{});
-        }
-
-
-        PWM_GLOBAL['startupFunctions'].push(function(){
-            require(["dojo","dojo/query"],function(dojo,query){
-                PWM_MAIN.JSLibrary.setValueOfSelectElement('statsChartSelect','<%=Statistic.PASSWORD_CHANGES%>');
-
-                setTimeout(function(){
-                    refreshChart();
-                },5*1000);
-
-                <% for (final Statistic loopStat : Statistic.sortedValues(locale)) { %>
-                PWM_MAIN.showTooltip({id:'Statistic_Key_<%=loopStat.getKey()%>',width:400,position:'above',text:PWM_ADMIN.showString("Statistic_Description.<%=loopStat.getKey()%>")});
-                <% } %>
-
-                PWM_MAIN.addEventHandler('button-refreshReportDataGrid','click',function(){
-                    PWM_ADMIN.refreshReportDataGrid();
-                });
-                PWM_MAIN.addEventHandler('reportStartButton','click',function(){ PWM_ADMIN.reportAction('Start') });
-                PWM_MAIN.addEventHandler('reportStopButton','click',function(){ PWM_ADMIN.reportAction('Stop') });
-                PWM_MAIN.addEventHandler('reportClearButton','click',function(){ PWM_ADMIN.reportAction('Clear') });
-                PWM_MAIN.addEventHandler('statsChartSelect','change',function(){ refreshChart() });
-
-                if (dojo.isIE) {
-                    <%--
-                    // The tab containers on this page go wacky in Internet Explorer if the form downloads are submitted
-                    // to the current page.  This is a workaround to submit the forms to a blank page instead.
-                    --%>
-                    query("form.submitToDownloadForm").forEach(function(node, index, arr) {
-                        node.target = "_blank";
-                    });
-                }
-            });
-        });
-    </script>
-</pwm:script>
-<% JspUtility.setFlag(pageContext, PwmRequestFlag.HIDE_LOCALE); %>
-<%@ include file="/WEB-INF/jsp/fragment/footer.jsp" %>
-</body>
-</html>

+ 121 - 0
webapp/src/main/webapp/WEB-INF/jsp/admin-statistics.jsp

@@ -0,0 +1,121 @@
+<%--
+ ~ Password Management Servlets (PWM)
+ ~ http://www.pwm-project.org
+ ~
+ ~ Copyright (c) 2006-2009 Novell, Inc.
+ ~ Copyright (c) 2009-2021 The PWM Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~     http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+--%>
+<%--
+       THIS FILE IS NOT INTENDED FOR END USER MODIFICATION.
+       See the README.TXT file in WEB-INF/jsp before making changes.
+--%>
+
+
+<%@ 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.StatisticsBundleKey" %>
+<%@ 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.StatisticsService" %>
+<%@ page import="password.pwm.util.java.JavaHelper" %>
+<%@ page import="java.util.Locale" %>
+<%@ page import="java.util.Map" %>
+<%@ page import="password.pwm.util.java.StringUtil" %>
+<%@ page import="java.time.format.DateTimeFormatter" %>
+<%@ page import="password.pwm.util.java.PwmUtil" %>
+<%@ page import="password.pwm.http.PwmRequestFlag" %>
+<%@ page import="password.pwm.http.tag.value.PwmValue" %>
+<%@ page import="password.pwm.http.tag.conditional.PwmIfTest" %>
+<%@ page import="password.pwm.http.JspUtility" %>
+
+<!DOCTYPE html>
+<%@ page language="java" session="true" isThreadSafe="true" contentType="text/html" %>
+<%@ taglib uri="pwm" prefix="pwm" %>
+<html lang="<pwm:value name="<%=PwmValue.localeCode%>"/>" dir="<pwm:value name="<%=PwmValue.localeDir%>"/>">
+<%@ include file="/WEB-INF/jsp/fragment/header.jsp" %>
+<body class="nihilo">
+<div id="wrapper">
+    <pwm:script-ref url="/public/resources/js/uilibrary.js"/>
+    <pwm:script-ref url="/public/resources/js/admin-statistics.js"/>
+    <% final String PageName = JspUtility.localizedString(pageContext,"Title_Statistics",Admin.class);%>
+    <jsp:include page="/WEB-INF/jsp/fragment/header-body.jsp">
+        <jsp:param name="pwm.PageName" value="<%=PageName%>"/>
+    </jsp:include>
+    <div id="centerbody">
+        <%@ include file="fragment/admin-modular-nav.jsp" %>
+        <div>
+            <br/><br/>
+            <h1 class="center">
+                <pwm:display key="Title_Statistics" bundle="Admin"/>
+                <form id="statsPeriodForm" name="statsPeriodForm">
+                    <select name="statsPeriodSelect" id="statsPeriodSelect">
+                    </select>
+                </form>
+            </h1>
+            <br/>
+            <table>
+                <thead>
+                <tr><td colspan="2" class="title">Counters</td></tr>
+                </thead>
+
+                <tbody id="statisticsTable">
+                <tr><td><pwm:display key="Display_PleaseWait"/></td></tr>
+                </tbody>
+            </table>
+            <br/>
+            <table>
+                <thead>
+                <tr><td colspan="2" class="title">Averages</td></tr>
+                </thead>
+
+                <tbody id="averageStatisticsTable">
+                <tr><td><pwm:display key="Display_PleaseWait"/></td></tr>
+                </tbody>
+            </table>
+        </div>
+        <br/>
+
+        <div class="noticebar">
+            <pwm:display key="Notice_EventStatistics" bundle="Admin"/>
+        </div>
+        <br/>
+
+        <div style="text-align: center">
+            <form class="submitToDownloadForm" action="<pwm:current-url/>" method="post" enctype="application/x-www-form-urlencoded">
+                <button type="submit" class="btn" id="button-downloadStatisticsLogCsv">
+                    <pwm:if test="<%=PwmIfTest.showIcons%>"><span class="btn-icon pwm-icon pwm-icon-download"></span></pwm:if>
+                    <pwm:display key="Button_DownloadCSV" bundle="Admin"/>
+                </button>
+                <input type="hidden" name="processAction" value="downloadStatisticsLogCsv"/>
+                <input type="hidden" name="pwmFormID" value="<pwm:FormID/>"/>
+            </form>
+        </div>
+    </div>
+</div>
+<div class="push"></div>
+</div>
+<pwm:script>
+    <script type="text/javascript">
+        PWM_GLOBAL['startupFunctions'].push(function() {
+            PWM_ADMIN_STATISTICS.initStatisticsPage();
+        });
+    </script>
+</pwm:script>
+<% JspUtility.setFlag(pageContext, PwmRequestFlag.HIDE_LOCALE); %>
+<%@ include file="/WEB-INF/jsp/fragment/footer.jsp" %>
+</body>
+</html>

+ 44 - 27
webapp/src/main/webapp/WEB-INF/jsp/admin-user-debug.jsp

@@ -38,7 +38,7 @@
 <%@ page import="password.pwm.util.i18n.LocaleHelper" %>
 <%@ page import="password.pwm.config.PwmSetting" %>
 <%@ page import="password.pwm.svc.PwmService" %>
-<%@ page import="password.pwm.http.servlet.admin.domain.AdminUserDebugServlet" %>
+<%@ page import="password.pwm.http.servlet.admin.domain.DomainAdminUserDebugServlet" %>
 <!DOCTYPE html>
 <%@ page language="java" session="true" isThreadSafe="true" contentType="text/html" %>
 <%@ taglib uri="pwm" prefix="pwm" %>
@@ -57,7 +57,7 @@
         <% if (userDebugDataBean == null) { %>
         <%@ include file="/WEB-INF/jsp/fragment/message.jsp" %>
         <div id="panel-searchbar" class="searchbar">
-            <form method="post" class="pwm-form" action="<pwm:current-url/>?processAction=<%=AdminUserDebugServlet.AdminUserDebugAction.searchUsername.toString()%>">
+            <form method="post" class="pwm-form" action="<pwm:current-url/>?processAction=<%=DomainAdminUserDebugServlet.AdminUserDebugAction.searchUsername.toString()%>">
                 <input id="username" name="username" placeholder="<pwm:display key="Placeholder_Search"/>" title="<pwm:display key="Placeholder_Search"/>" class="helpdesk-input-username" <pwm:autofocus/> autocomplete="off"/>
                 <input type="hidden" id="pwmFormID" name="pwmFormID" value="<pwm:FormID/>"/>
                 <button type="submit" class="btn"><pwm:display key="Button_Search"/></button>
@@ -65,9 +65,9 @@
         </div>
 
         <% } else { %>
-        <div class="buttonbar">
+        <div class="buttonbar center">
             <form method="get" class="pwm-form">
-                <button type="submit" class="btn"><pwm:display key="Button_Continue"/></button>
+                <button type="submit" class="btn"><pwm:display key="Button_Reset"/></button>
             </form>
         </div>
         <% final PublicUserInfoBean userInfo = userDebugDataBean.getPublicUserInfoBean(); %>
@@ -291,16 +291,20 @@
                 <td class="key">Profiles</td>
                 <td>
                     <table>
+                        <thead>
                         <tr>
                             <td class="key">Service</td>
                             <td class="key">ProfileID</td>
                         </tr>
+                        </thead>
+                        <tbody>
                         <% for (final ProfileDefinition profileDefinition : userDebugDataBean.getProfiles().keySet()) { %>
                         <tr>
                             <td><%=profileDefinition%></td>
                             <td><%=JspUtility.friendlyWrite(pageContext, userDebugDataBean.getProfiles().get(profileDefinition).stringValue())%></td>
                         </tr>
                         <% } %>
+                        </tbody>
                     </table>
                 </td>
             </tr>
@@ -308,16 +312,20 @@
                 <td class="key">Permissions</td>
                 <td>
                     <table>
+                        <thead>
                         <tr>
                             <td class="key">Permission</td>
                             <td class="key">Status</td>
                         </tr>
+                        </thead>
+                        <tbody>
                         <% for (final Permission permission : userDebugDataBean.getPermissions().keySet()) { %>
                         <tr>
                             <td><%=permission%></td>
                             <td><%=JspUtility.friendlyWrite(pageContext, userDebugDataBean.getPermissions().get(permission))%></td>
                         </tr>
                         <% } %>
+                        </tbody>
                     </table>
                 </td>
             </tr>
@@ -334,6 +342,7 @@
             <tr>
                 <td colspan="10">
                     <table>
+                        <thead>
                         <tr class="title">
                             <td class="key" style="width: 1px;">Rule</td>
                             <td class="key" style="width: 1px;">Rule Type</td>
@@ -342,28 +351,31 @@
                             <td class="key" style="width: 20%;">Effective Policy</td>
                         </tr>
                         <tr>
-                            <td>ID</td>
+                            <td class="key">ID</td>
                             <td><pwm:display key="<%=Display.Value_NotApplicable.toString()%>"/></td>
                             <td><%=JspUtility.friendlyWrite(pageContext, () -> configPolicy.getId().stringValue())%></td>
                             <td><%=JspUtility.friendlyWrite(pageContext, () -> ldapPolicy.getId().stringValue())%></td>
                             <td><%=JspUtility.friendlyWrite(pageContext, () -> userPolicy.getId().stringValue())%></td>
                         </tr>
                         <tr>
-                            <td>Display Name</td>
+                            <td class="key">Display Name</td>
                             <td><pwm:display key="<%=Display.Value_NotApplicable.toString()%>"/></td>
                             <td><%=JspUtility.friendlyWrite(pageContext, configPolicy.getDisplayName(JspUtility.locale(request)))%></td>
                             <td><%=JspUtility.friendlyWrite(pageContext, ldapPolicy.getDisplayName(JspUtility.locale(request)))%></td>
                             <td><%=JspUtility.friendlyWrite(pageContext, userPolicy.getDisplayName(JspUtility.locale(request)))%></td>
                         </tr>
+                        </thead>
+                        <tbody>
                         <% for (final PwmPasswordRule rule : PwmPasswordRule.sortedByLabel(JspUtility.locale(request), JspUtility.getPwmRequest(pageContext).getDomainConfig())) { %>
                         <tr>
-                            <td><span title="<%=rule.getKey()%>"><%=rule.getLabel(JspUtility.locale(request), JspUtility.getPwmRequest(pageContext).getDomainConfig())%></span></td>
+                            <td class="key"><span title="<%=rule.getKey()%>"><%=rule.getLabel(JspUtility.locale(request), JspUtility.getPwmRequest(pageContext).getDomainConfig())%></span></td>
                             <td><%=rule.getRuleType()%></td>
                             <td><%=JspUtility.friendlyWrite(pageContext, configPolicy.getValue(rule))%></td>
                             <td><%=JspUtility.friendlyWrite(pageContext, ldapPolicy.getValue(rule))%></td>
                             <td><%=JspUtility.friendlyWrite(pageContext, userPolicy.getValue(rule))%></td>
                         </tr>
                         <% } %>
+                        </tbody>
                     </table>
                 </td>
             </tr>
@@ -377,32 +389,32 @@
             <% final ResponseInfoBean responseInfoBean = userDebugDataBean.getUserInfo().getResponseInfoBean(); %>
             <% if (responseInfoBean == null) { %>
             <tr>
-                <td>Stored Responses</td>
+                <td class="key">Stored Responses</td>
                 <td><pwm:display key="<%=Display.Value_NotApplicable.toString()%>"/></td>
             </tr>
             <% } else { %>
             <tr>
-                <td>Identifier</td>
+                <td class="key">Identifier</td>
                 <td><%=responseInfoBean.getCsIdentifier()%></td>
             </tr>
             <tr>
-                <td>Storage Type</td>
+                <td class="key">Storage Type</td>
                 <td><%=responseInfoBean.getDataStorageMethod()%></td>
             </tr>
             <tr>
-                <td>Format</td>
+                <td class="key">Format</td>
                 <td><%=responseInfoBean.getFormatType()%></td>
             </tr>
             <tr>
-                <td>Locale</td>
+                <td class="key">Locale</td>
                 <td><%=responseInfoBean.getLocale()%></td>
             </tr>
             <tr>
-                <td>Storage Timestamp</td>
+                <td class="key">Storage Timestamp</td>
                 <td><%=JspUtility.friendlyWrite(pageContext, responseInfoBean.getTimestamp())%></td>
             </tr>
             <tr>
-                <td>Answered Challenges</td>
+                <td class="key">Answered Challenges</td>
                 <% final Map<Challenge,String> crMap = responseInfoBean.getCrMap(); %>
                 <% if (crMap == null) { %>
                 <td>
@@ -411,11 +423,14 @@
                 <% } else { %>
                 <td>
                     <table>
+                        <thead>
                         <tr>
                             <td class="key">Type</td>
                             <td class="key">Required</td>
                             <td class="key">Text</td>
                         </tr>
+                        </thead>
+                        <tbody>
                         <% for (final Challenge challenge : crMap.keySet()) { %>
                         <tr>
                             <td>
@@ -429,12 +444,13 @@
                             </td>
                         </tr>
                         <% } %>
+                        </tbody>
                     </table>
                 </td>
                 <% } %>
             </tr>
             <tr>
-                <td>
+                <td class="key">
                     Minimum Randoms Required
                 </td>
                 <td>
@@ -442,7 +458,7 @@
                 </td>
             </tr>
             <tr>
-                <td>Helpdesk Answered Challenges</td>
+                <td class="key">Helpdesk Answered Challenges</td>
                 <% final Map<Challenge,String> helpdeskCrMap = responseInfoBean.getHelpdeskCrMap(); %>
                 <% if (helpdeskCrMap == null) { %>
                 <td>
@@ -467,24 +483,24 @@
             <% final ChallengeProfile challengeProfile = userDebugDataBean.getUserInfo().getChallengeProfile(); %>
             <% if ( challengeProfile == null ) { %>
             <tr>
-                <td>Assigned Profile</td>
+                <td class="key">Assigned Profile</td>
                 <td><pwm:display key="<%=Display.Value_NotApplicable.toString()%>"/></td>
             </tr>
             <% } else { %>
             <tr>
-                <td>Display Name</td>
+                <td class="key">Display Name</td>
                 <td><%=challengeProfile.getDisplayName(JspUtility.locale(request))%></td>
             </tr>
             <tr>
-                <td>Identifier</td>
+                <td class="key">Identifier</td>
                 <td><%=challengeProfile.getId()%></td>
             </tr>
             <tr>
-                <td>Locale</td>
+                <td class="key">Locale</td>
                 <td><%=challengeProfile.getLocale()%></td>
             </tr>
             <tr>
-                <td>Challenges</td>
+                <td class="key">Challenges</td>
                 <td>
                     <table>
                         <tr>
@@ -527,9 +543,10 @@
                 </td>
             </tr>
             <tr>
-                <td>Helpdesk Challenges</td>
+                <td class="key">Helpdesk Challenges</td>
                 <td>
                     <table>
+                        <thead>
                         <tr>
                             <td class="key">Type</td>
                             <td class="key">Text</td>
@@ -539,6 +556,8 @@
                             <td class="key">Enforce Wordlist</td>
                             <td class="key">Max Question Characters</td>
                         </tr>
+                        </thead>
+                        <tbody>
                         <% if ( challengeProfile.hasHelpdeskChallenges() ) { %>
                         <% for (final Challenge challenge : challengeProfile.getHelpdeskChallengeSet().get().getChallenges()) { %>
                         <tr>
@@ -566,6 +585,7 @@
                         </tr>
                         <% } %>
                         <% } %>
+                        </tbody>
                     </table>
                 </td>
             </tr>
@@ -574,12 +594,9 @@
         <% } %><%-- End Challenge Profile --%>
         <% } %>
         </tr>
-        <div class="buttonbar">
-            <form method="get" class="pwm-form">
-                <button type="submit" class="btn"><pwm:display key="Button_Continue"/></button>
-            </form>
+        <div class="buttonbar center">
             <form method="get">
-                <input type="hidden" name="processAction" value="<%=AdminUserDebugServlet.AdminUserDebugAction.downloadUserDebug.toString()%>"/>
+                <input type="hidden" name="processAction" value="<%=DomainAdminUserDebugServlet.AdminUserDebugAction.downloadUserDebug.toString()%>"/>
                 <button type="submit" class="btn">Download</button>
             </form>
         </div>

+ 0 - 6
webapp/src/main/webapp/WEB-INF/jsp/fragment/admin-modular-nav.jsp

@@ -23,15 +23,9 @@
 --%>
 
 
-<%@ page import="password.pwm.http.servlet.admin.SystemAdminServlet" %>
-<%@ page import="password.pwm.http.servlet.PwmServletDefinition" %>
 <%@ page import="password.pwm.http.tag.conditional.PwmIfTest" %>
-<%@ page import="password.pwm.http.JspUtility" %>
 
 <%@ taglib uri="pwm" prefix="pwm" %>
-<pwm:script-ref url="/public/resources/js/uilibrary.js"/>
-<pwm:script-ref url="/public/resources/js/main.js"/>
-<pwm:script-ref url="/public/resources/js/admin.js"/>
 <link href="<pwm:url url='/public/resources/adminStyle.css' addContext="true"/>" rel="stylesheet" type="text/css" media="screen"/>
 
 <div class="admin-breadcrumb-navigation-button">

+ 0 - 7
webapp/src/main/webapp/WEB-INF/jsp/fragment/admin-nav.jsp

@@ -58,13 +58,6 @@
             <pwm:display key="Title_UserActivity" bundle="Admin"/>
         </button>
     </form>
-    <% selected = currentPage == SystemAdminServlet.Page.analysis; %>
-    <form action="<%=SystemAdminServlet.Page.analysis%>" method="get" id="analysis" name="analysis">
-        <button type="submit" class="navbutton<%=selected?" selected":""%>">
-            <pwm:if test="<%=PwmIfTest.showIcons%>"><span class="btn-icon pwm-icon pwm-icon-bar-chart-o"></span></pwm:if>
-            <pwm:display key="Title_DataAnalysis" bundle="Admin"/>
-        </button>
-    </form>
     <div style="display: inline" id="admin-nav-menu-container">
     </div>
 </div>

+ 1 - 1
webapp/src/main/webapp/WEB-INF/jsp/fragment/header-menu.jsp

@@ -64,7 +64,7 @@
                 <pwm:display key="MenuItem_ConfigEditor" bundle="Admin"/>
             </a>
             <pwm:if test="<%=PwmIfTest.permission%>" permission="<%=Permission.PWMADMIN%>">
-            <a class="header-warning-button" id="header_administrationButton" href="<pwm:url url='<%=PwmServletDefinition.Admin.servletUrl()%>' addContext="true"/>">
+            <a class="header-warning-button" id="header_administrationButton" href="<pwm:url url='<%=PwmServletDefinition.AdminMenu.servletUrl()%>' addContext="true"/>">
                     <pwm:if test="<%=PwmIfTest.showIcons%>"><span class="btn-icon pwm-icon pwm-icon-list-alt"></span></pwm:if>
                     <pwm:display key="Title_Admin"/>
                 </a>

+ 2 - 2
webapp/src/main/webapp/WEB-INF/jsp/fragment/message.jsp

@@ -35,8 +35,8 @@
 <% final ErrorInformation requestError = (ErrorInformation)JspUtility.getAttribute(pageContext, PwmRequestAttribute.PwmErrorInfo); %>
 <% if (requestError != null) { %>
     <span id="message" class="message message-error"><pwm:ErrorMessage/></span>
-    <span id="errorCode"><%=requestError.getError().getErrorCode()%></span>
-    <span id="errorName"><%=requestError.getError().toString()%></span>
+    <span id="errorCode" class="nodisplay"><%=requestError.getError().getErrorCode()%></span>
+    <span id="errorName" class="nodisplay"><%=JspUtility.friendlyWrite( pageContext, requestError.getError().toString())%></span>
 <% } else { %>
     <span id="message" class="message nodisplay">&nbsp;</span>
 <% } %>

+ 13 - 2
webapp/src/main/webapp/private/admin/index.jsp

@@ -50,7 +50,7 @@
             </form>
         </div>
         <pwm:if test="<%=PwmIfTest.permission%>" permission="<%=Permission.PWMADMIN%>">
-            <a id="button_reporting" href="<pwm:url url='<%=PwmServletDefinition.AdminReport.servletUrl()%>' addContext="true"/> ">
+            <a id="button_reporting" href="<pwm:url url='<%=PwmServletDefinition.DomainAdminReport.servletUrl()%>' addContext="true"/> ">
                 <div class="tile">
                     <div class="tile-content">
                         <div class="tile-image admin-image"></div>
@@ -61,7 +61,7 @@
             </a>
         </pwm:if>
         <pwm:if test="<%=PwmIfTest.permission%>" permission="<%=Permission.PWMADMIN%>">
-            <a id="button_userdebug" href="<pwm:url url='<%=PwmServletDefinition.AdminUserDebug.servletUrl()%>' addContext="true"/> ">
+            <a id="button_userdebug" href="<pwm:url url='<%=PwmServletDefinition.DomainAdminUserDebug.servletUrl()%>' addContext="true"/> ">
                 <div class="tile">
                     <div class="tile-content">
                         <div class="tile-image user-image"></div>
@@ -71,6 +71,17 @@
                 </div>
             </a>
         </pwm:if>
+        <pwm:if test="<%=PwmIfTest.permission%>" permission="<%=Permission.PWMADMIN%>">
+            <a id="button_userdebug" href="<pwm:url url='<%=PwmServletDefinition.DomainAdminStatistics.servletUrl()%>' addContext="true"/> ">
+                <div class="tile">
+                    <div class="tile-content">
+                        <div class="tile-image user-image"></div>
+                        <div class="tile-title" title="<pwm:display key='Title_Statistics' bundle="Admin"/>"><pwm:display key="Title_Statistics" bundle="Admin"/></div>
+                        <div class="tile-subtitle" title="<pwm:display key='Title_Statistics' bundle="Admin"/>"><pwm:display key="Title_Statistics" bundle="Admin"/></div>
+                    </div>
+                </div>
+            </a>
+        </pwm:if>
         <pwm:if test="<%=PwmIfTest.permission%>" permission="<%=Permission.PWMADMIN%>">
             <a id="button_systemadmin" href="<pwm:url url='<%=PwmServletDefinition.SystemAdmin.servletUrl()%>' addContext="true"/> ">
                 <div class="tile">

+ 1 - 1
webapp/src/main/webapp/private/index.jsp

@@ -180,7 +180,7 @@
             </pwm:if>
         </pwm:if>
         <pwm:if test="<%=PwmIfTest.permission%>" permission="<%=Permission.PWMADMIN%>">
-            <a id="button_Admin" href="<pwm:url url='<%=PwmServletDefinition.Admin.servletUrl()%>' addContext="true"/> ">
+            <a id="button_Admin" href="<pwm:url url='<%=PwmServletDefinition.AdminMenu.servletUrl()%>' addContext="true"/> ">
                 <div class="tile">
                     <div class="tile-content">
                         <div class="tile-image admin-image"></div>

+ 82 - 0
webapp/src/main/webapp/public/resources/js/admin-statistics.js

@@ -0,0 +1,82 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2021 The PWM Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+var PWM_ADMIN = PWM_ADMIN || {};
+var PWM_MAIN = PWM_MAIN || {};
+var PWM_GLOBAL = PWM_GLOBAL || {};
+
+var PWM_ADMIN_STATISTICS = PWM_ADMIN_STATISTICS || {};
+
+PWM_ADMIN_STATISTICS.initStatisticsPage=function() {
+    PWM_MAIN.addEventHandler('statsPeriodForm','change',function() {
+        PWM_ADMIN_STATISTICS.refreshStatistics();
+    });
+
+    const url = PWM_MAIN.addParamToUrl(window.location.pathname, 'processAction','readKeys');
+    const loadFunction = function(data) {
+        const selectElement = PWM_MAIN.getObject('statsPeriodSelect');
+
+        if (data['data'] && data['data']) {
+            const keys = data['data'];
+            let optionsHtml = '';
+            PWM_MAIN.JSLibrary.forEachInObject(keys, function (key, value) {
+                optionsHtml += '<option value="' + key + '">' + value + '</option>';
+            });
+            selectElement.innerHTML = optionsHtml;
+        }
+
+        PWM_ADMIN_STATISTICS.refreshStatistics();
+    };
+    PWM_MAIN.ajaxRequest(url,loadFunction);
+};
+
+PWM_ADMIN_STATISTICS.refreshStatistics=function() {
+    const waitInnerHtml = '<tr><td colspan="2">'
+        + PWM_MAIN.showString('Display_PleaseWait')
+        + '</td></tr>'
+
+    const tableElement = PWM_MAIN.getObject('statisticsTable');
+    const averageTableElement = PWM_MAIN.getObject('averageStatisticsTable');
+
+    tableElement.innerHTML = waitInnerHtml;
+    averageTableElement.innerHTML = waitInnerHtml;
+
+    const currentStatKey = PWM_MAIN.JSLibrary.readValueOfSelectElement('statsPeriodSelect');
+
+    let url = PWM_MAIN.addParamToUrl(window.location.pathname, 'processAction','readStatistics');
+    if ( currentStatKey ) {
+        url = PWM_MAIN.addParamToUrl(url, 'statKey',currentStatKey);
+    }
+
+    const loadFunction = function(data) {
+
+        if (data['data'] && data['data']['statistics']) {
+            const fields = data['data']['statistics'];
+            tableElement.innerHTML = UILibrary.displayElementsToTableContents(fields);
+            UILibrary.initElementsToTableContents(fields);
+        }
+        if (data['data'] && data['data']['averageStatistics']) {
+            const fields = data['data']['averageStatistics'];
+            averageTableElement.innerHTML = UILibrary.displayElementsToTableContents(fields);
+            UILibrary.initElementsToTableContents(fields);
+        }
+    };
+    PWM_MAIN.ajaxRequest(url,loadFunction);
+};

+ 1 - 1
webapp/src/main/webapp/public/resources/js/main.js

@@ -1310,7 +1310,7 @@ PWM_MAIN.JSLibrary.readValueOfSelectElement = function(nodeID) {
     if (element && element.options && element.selectedIndex >= 0) {
         return element.options[element.selectedIndex].value;
     }
-    return "";
+    return null;
 };
 
 PWM_MAIN.JSLibrary.setValueOfSelectElement = function(nodeID, value) {

+ 2 - 2
webapp/src/main/webapp/public/resources/js/uilibrary.js

@@ -785,7 +785,7 @@ UILibrary.displayElementsToTableContents = function(fields) {
     for (var field in fields) {(function(field){
         var fieldData = fields[field];
         var classValue = fieldData['type'] === 'timestamp' ? 'timestamp' : '';
-        htmlTable += '<tr><td>' + fieldData['label'] + '</td><td><span class="' + classValue + '" id="report_status_' + fieldData['key']  + '"</tr>';
+        htmlTable += '<tr><td class="key">' + fieldData['label'] + '</td><td><span class="' + classValue + '" id="' + fieldData['key']  + '"</tr>';
     }(field)); }
     return htmlTable;
 };
@@ -797,7 +797,7 @@ UILibrary.initElementsToTableContents = function(fields) {
         if (fieldData['type'] === 'number') {
             value = PWM_MAIN.numberFormat(value);
         }
-        PWM_MAIN.getObject('report_status_' + fieldData['key']).innerHTML = value;
+        PWM_MAIN.getObject( fieldData['key']).innerHTML = value;
         if (fieldData['type'] === 'timestamp') {
             PWM_MAIN.TimestampHandler.initElement(PWM_MAIN.getObject("report_status_" + fieldData['key']));
         }

+ 0 - 1
webapp/src/main/webapp/public/resources/style.css

@@ -1383,7 +1383,6 @@ dialog .closeIcon {
     content: "";
 }
 
-​
 .center {
     text-align: center;
 }