package org.apereo.cas.web.report; import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.health.HealthCheckRegistry; import com.codahale.metrics.servlets.HealthCheckServlet; import com.codahale.metrics.servlets.MetricsServlet; import org.apereo.cas.CentralAuthenticationService; import org.apereo.cas.audit.spi.DelegatingAuditTrailManager; import org.apereo.cas.configuration.CasConfigurationProperties; import org.apereo.cas.ticket.ServiceTicket; import org.apereo.cas.ticket.Ticket; import org.apereo.cas.util.DateTimeUtils; import org.apereo.inspektr.audit.AuditActionContext; import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.context.ServletContextAware; import org.springframework.web.context.request.async.WebAsyncTask; import org.springframework.web.servlet.ModelAndView; import javax.servlet.ServletContext; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.time.Duration; import java.time.LocalDateTime; import java.time.ZoneOffset; import java.time.ZonedDateTime; import java.util.Collection; import java.util.Comparator; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.Callable; import java.util.stream.Collectors; /** * @author Scott Battaglia * @since 3.3.5 */ public class StatisticsController extends BaseCasMvcEndpoint implements ServletContextAware { private static final int NUMBER_OF_BYTES_IN_A_KILOBYTE = 1024; private static final String MONITORING_VIEW_STATISTICS = "monitoring/viewStatistics"; private final ZonedDateTime upTimeStartDate = ZonedDateTime.now(ZoneOffset.UTC); private final DelegatingAuditTrailManager auditTrailManager; private final CentralAuthenticationService centralAuthenticationService; private final MetricRegistry metricsRegistry; private final HealthCheckRegistry healthCheckRegistry; private final CasConfigurationProperties casProperties; public StatisticsController(final DelegatingAuditTrailManager auditTrailManager, final CentralAuthenticationService centralAuthenticationService, final MetricRegistry metricsRegistry, final HealthCheckRegistry healthCheckRegistry, final CasConfigurationProperties casProperties) { super("casstats", "/stats", casProperties.getMonitor().getEndpoints().getStatistics(), casProperties); this.auditTrailManager = auditTrailManager; this.centralAuthenticationService = centralAuthenticationService; this.metricsRegistry = metricsRegistry; this.healthCheckRegistry = healthCheckRegistry; this.casProperties = casProperties; } /** * Gets availability times of the server. * * @param request the http servlet request * @param response the http servlet response * @return the availability */ @GetMapping(value = "/getAvailability") @ResponseBody public Map<String, Object> getAvailability(final HttpServletRequest request, final HttpServletResponse response) { ensureEndpointAccessIsAuthorized(request, response); final Map<String, Object> model = new HashMap<>(); final Duration diff = Duration.between(this.upTimeStartDate, ZonedDateTime.now(ZoneOffset.UTC)); model.put("upTime", diff.getSeconds()); return model; } /** * Gets memory stats. * * @param request the http servlet request * @param response the http servlet response * @return the memory stats */ @GetMapping(value = "/getMemStats") @ResponseBody public Map<String, Object> getMemoryStats(final HttpServletRequest request, final HttpServletResponse response) { ensureEndpointAccessIsAuthorized(request, response); final Map<String, Object> model = new HashMap<>(); model.put("totalMemory", convertToMegaBytes(Runtime.getRuntime().totalMemory())); model.put("maxMemory", convertToMegaBytes(Runtime.getRuntime().maxMemory())); model.put("freeMemory", convertToMegaBytes(Runtime.getRuntime().freeMemory())); return model; } /** * Gets authn audit. * * @param request the request * @param response the response * @return the authn audit * @throws Exception the exception */ @GetMapping(value = "/getAuthnAudit") @ResponseBody public Set<AuditActionContext> getAuthnAudit(final HttpServletRequest request, final HttpServletResponse response) throws Exception { ensureEndpointAccessIsAuthorized(request, response); return this.auditTrailManager.get(); } /** * Gets authn audit summary. * * @param request the request * @param response the response * @param start the start * @param range the range * @param scale the scale * @return the authn audit * @throws Exception the exception */ @GetMapping(value = "/getAuthnAudit/summary") @ResponseBody public WebAsyncTask<Collection<AuthenticationAuditSummary>> getAuthnAuditSummary(final HttpServletRequest request, final HttpServletResponse response, @RequestParam final long start, @RequestParam final String range, @RequestParam final String scale) throws Exception { ensureEndpointAccessIsAuthorized(request, response); response.setContentType(MediaType.APPLICATION_JSON_VALUE); final Callable<Collection<AuthenticationAuditSummary>> asyncTask = () -> { final Set<AuditActionContext> audits = getAuthnAudit(request, response); final LocalDateTime startDate = DateTimeUtils.localDateTimeOf(start); final LocalDateTime endDate = startDate.plus(Duration.parse(range)); final List<AuditActionContext> authnEvents = audits.stream() .filter(a -> { final LocalDateTime actionTime = DateTimeUtils.localDateTimeOf(a.getWhenActionWasPerformed()); return (actionTime.isEqual(startDate) || actionTime.isAfter(startDate)) && (actionTime.isEqual(endDate) || actionTime.isBefore(endDate)) && a.getActionPerformed().matches("AUTHENTICATION_(SUCCESS|FAILED)"); }) .sorted(Comparator.comparing(AuditActionContext::getWhenActionWasPerformed)) .collect(Collectors.toList()); final Duration steps = Duration.parse(scale); final Map<Integer, LocalDateTime> buckets = new LinkedHashMap<>(); LocalDateTime dt = startDate; Integer index = 0; while (dt != null) { buckets.put(index++, dt); dt = dt.plus(steps); if (dt.isAfter(endDate)) { dt = null; } } final Map<LocalDateTime, AuthenticationAuditSummary> summary = new LinkedHashMap<>(); boolean foundBucket = false; for (final AuditActionContext event : authnEvents) { foundBucket = false; for (int i = 0; i < buckets.keySet().size(); i++) { final LocalDateTime actionTime = DateTimeUtils.localDateTimeOf(event.getWhenActionWasPerformed()); final LocalDateTime bucketDateTime = buckets.get(i); if (actionTime.isEqual(bucketDateTime) || actionTime.isAfter(bucketDateTime)) { for (int j = 0; j < buckets.keySet().size(); j++) { final LocalDateTime nextBucketDateTime = buckets.get(j); if (actionTime.isBefore(nextBucketDateTime)) { final LocalDateTime bucketToUse = buckets.get(j - 1); final AuthenticationAuditSummary values; if (summary.containsKey(bucketToUse)) { values = summary.get(bucketToUse); } else { values = new AuthenticationAuditSummary(bucketToUse.toString()); } if (event.getActionPerformed().contains("SUCCESS")) { values.incrementSuccess(); } else { values.incrementFailure(); } summary.put(bucketToUse, values); foundBucket = true; break; } } if (foundBucket) { break; } } } } final Collection<AuthenticationAuditSummary> values = summary.values(); //values.removeIf(a -> a.isEmpty()); return values; }; return new WebAsyncTask<>(casProperties.getHttpClient().getAsyncTimeout(), asyncTask); } private static class AuthenticationAuditSummary { private final String time; private long successes; private long failures; /** * Instantiates a new Authentication audit summary. * * @param time the time */ AuthenticationAuditSummary(final String time) { this.time = time; } public String getTime() { return time; } public long getSuccesses() { return successes; } public long getFailures() { return failures; } public void incrementSuccess() { successes++; } public void incrementFailure() { failures++; } } /** * Gets ticket stats. * * @param request the http servlet request * @param response the http servlet response * @return the ticket stats */ @GetMapping(value = "/getTicketStats") @ResponseBody public Map<String, Object> getTicketStats(final HttpServletRequest request, final HttpServletResponse response) { ensureEndpointAccessIsAuthorized(request, response); final Map<String, Object> model = new HashMap<>(); int unexpiredTgts = 0; int unexpiredSts = 0; int expiredTgts = 0; int expiredSts = 0; final Collection<Ticket> tickets = this.centralAuthenticationService.getTickets(ticket -> true); for (final Ticket ticket : tickets) { if (ticket instanceof ServiceTicket) { if (ticket.isExpired()) { this.centralAuthenticationService.deleteTicket(ticket.getId()); expiredSts++; } else { unexpiredSts++; } } else { if (ticket.isExpired()) { this.centralAuthenticationService.deleteTicket(ticket.getId()); expiredTgts++; } else { unexpiredTgts++; } } } model.put("unexpiredTgts", unexpiredTgts); model.put("unexpiredSts", unexpiredSts); model.put("expiredTgts", expiredTgts); model.put("expiredSts", expiredSts); return model; } /** * Handles the request. * * @param httpServletRequest the http servlet request * @param httpServletResponse the http servlet response * @return the model and view * @throws Exception the exception */ @GetMapping protected ModelAndView handleRequestInternal(final HttpServletRequest httpServletRequest, final HttpServletResponse httpServletResponse) throws Exception { ensureEndpointAccessIsAuthorized(httpServletRequest, httpServletResponse); final ModelAndView modelAndView = new ModelAndView(MONITORING_VIEW_STATISTICS); modelAndView.addObject("pageTitle", modelAndView.getViewName()); modelAndView.addObject("availableProcessors", Runtime.getRuntime().availableProcessors()); modelAndView.addObject("casTicketSuffix", casProperties.getHost().getName()); modelAndView.getModel().putAll(getAvailability(httpServletRequest, httpServletResponse)); modelAndView.addObject("startTime", this.upTimeStartDate.toLocalDateTime()); modelAndView.getModel().putAll(getMemoryStats(httpServletRequest, httpServletResponse)); return modelAndView; } /** * Convert to megabytes from bytes. * * @param bytes the total number of bytes * @return value converted to MB */ private static double convertToMegaBytes(final double bytes) { return bytes / NUMBER_OF_BYTES_IN_A_KILOBYTE / NUMBER_OF_BYTES_IN_A_KILOBYTE; } @Override public void setServletContext(final ServletContext servletContext) { servletContext.setAttribute(MetricsServlet.METRICS_REGISTRY, this.metricsRegistry); servletContext.setAttribute(MetricsServlet.SHOW_SAMPLES, Boolean.TRUE); servletContext.setAttribute(HealthCheckServlet.HEALTH_CHECK_REGISTRY, this.healthCheckRegistry); } }