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);
}
}