/*
* Copyright 2012-2017 the original author or authors.
*
* 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 org.springframework.boot.actuate.autoconfigure;
import java.io.IOException;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.regex.Pattern;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.boot.actuate.metrics.CounterService;
import org.springframework.boot.actuate.metrics.GaugeService;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.http.HttpStatus.Series;
import org.springframework.util.StopWatch;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.servlet.HandlerMapping;
import org.springframework.web.util.UrlPathHelper;
/**
* Filter that counts requests and measures processing times.
*/
@Order(Ordered.HIGHEST_PRECEDENCE)
final class MetricsFilter extends OncePerRequestFilter {
private static final String ATTRIBUTE_STOP_WATCH = MetricsFilter.class.getName()
+ ".StopWatch";
private static final int UNDEFINED_HTTP_STATUS = 999;
private static final String UNKNOWN_PATH_SUFFIX = "/unmapped";
private static final Log logger = LogFactory.getLog(MetricsFilter.class);
private final CounterService counterService;
private final GaugeService gaugeService;
private final MetricFilterProperties properties;
private static final Set<PatternReplacer> STATUS_REPLACERS;
static {
Set<PatternReplacer> replacements = new LinkedHashSet<>();
replacements.add(new PatternReplacer("\\{(.+?)(?::.+)?\\}", 0, "-$1-"));
replacements.add(new PatternReplacer("**", Pattern.LITERAL, "-star-star-"));
replacements.add(new PatternReplacer("*", Pattern.LITERAL, "-star-"));
replacements.add(new PatternReplacer("/-", Pattern.LITERAL, "/"));
replacements.add(new PatternReplacer("-/", Pattern.LITERAL, "/"));
STATUS_REPLACERS = Collections.unmodifiableSet(replacements);
}
private static final Set<PatternReplacer> KEY_REPLACERS;
static {
Set<PatternReplacer> replacements = new LinkedHashSet<>();
replacements.add(new PatternReplacer("/", Pattern.LITERAL, "."));
replacements.add(new PatternReplacer("..", Pattern.LITERAL, "."));
KEY_REPLACERS = Collections.unmodifiableSet(replacements);
}
MetricsFilter(CounterService counterService, GaugeService gaugeService,
MetricFilterProperties properties) {
this.counterService = counterService;
this.gaugeService = gaugeService;
this.properties = properties;
}
@Override
protected boolean shouldNotFilterAsyncDispatch() {
return false;
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
StopWatch stopWatch = createStopWatchIfNecessary(request);
String path = new UrlPathHelper().getPathWithinApplication(request);
int status = HttpStatus.INTERNAL_SERVER_ERROR.value();
try {
chain.doFilter(request, response);
status = getStatus(response);
}
finally {
if (!request.isAsyncStarted()) {
if (response.isCommitted()) {
status = getStatus(response);
}
stopWatch.stop();
request.removeAttribute(ATTRIBUTE_STOP_WATCH);
recordMetrics(request, path, status, stopWatch.getTotalTimeMillis());
}
}
}
private StopWatch createStopWatchIfNecessary(HttpServletRequest request) {
StopWatch stopWatch = (StopWatch) request.getAttribute(ATTRIBUTE_STOP_WATCH);
if (stopWatch == null) {
stopWatch = new StopWatch();
stopWatch.start();
request.setAttribute(ATTRIBUTE_STOP_WATCH, stopWatch);
}
return stopWatch;
}
private int getStatus(HttpServletResponse response) {
try {
return response.getStatus();
}
catch (Exception ex) {
return UNDEFINED_HTTP_STATUS;
}
}
private void recordMetrics(HttpServletRequest request, String path, int status,
long time) {
String suffix = determineMetricNameSuffix(request, path, status);
submitMetrics(MetricsFilterSubmission.MERGED, request, status, time, suffix);
submitMetrics(MetricsFilterSubmission.PER_HTTP_METHOD, request, status, time,
suffix);
}
private String determineMetricNameSuffix(HttpServletRequest request, String path,
int status) {
Object bestMatchingPattern = request
.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE);
if (bestMatchingPattern != null) {
return fixSpecialCharacters(bestMatchingPattern.toString());
}
Series series = getSeries(status);
if (Series.CLIENT_ERROR.equals(series) || Series.SERVER_ERROR.equals(series)
|| Series.REDIRECTION.equals(series)) {
return UNKNOWN_PATH_SUFFIX;
}
return path;
}
private String fixSpecialCharacters(String value) {
String result = value;
for (PatternReplacer replacer : STATUS_REPLACERS) {
result = replacer.apply(result);
}
if (result.endsWith("-")) {
result = result.substring(0, result.length() - 1);
}
if (result.startsWith("-")) {
result = result.substring(1);
}
return result;
}
private Series getSeries(int status) {
try {
return HttpStatus.valueOf(status).series();
}
catch (Exception ex) {
return null;
}
}
private void submitMetrics(MetricsFilterSubmission submission,
HttpServletRequest request, int status, long time, String suffix) {
String prefix = "";
if (submission == MetricsFilterSubmission.PER_HTTP_METHOD) {
prefix = request.getMethod() + ".";
}
if (this.properties.shouldSubmitToGauge(submission)) {
submitToGauge(getKey("response." + prefix + suffix), time);
}
if (this.properties.shouldSubmitToCounter(submission)) {
incrementCounter(getKey("status." + prefix + status + suffix));
}
}
private String getKey(String string) {
// graphite compatible metric names
String key = string;
for (PatternReplacer replacer : KEY_REPLACERS) {
key = replacer.apply(key);
}
if (key.endsWith(".")) {
key = key + "root";
}
if (key.startsWith("_")) {
key = key.substring(1);
}
return key;
}
private void submitToGauge(String metricName, double value) {
try {
this.gaugeService.submit(metricName, value);
}
catch (Exception ex) {
logger.warn("Unable to submit gauge metric '" + metricName + "'", ex);
}
}
private void incrementCounter(String metricName) {
try {
this.counterService.increment(metricName);
}
catch (Exception ex) {
logger.warn("Unable to submit counter metric '" + metricName + "'", ex);
}
}
private static class PatternReplacer {
private final Pattern pattern;
private final String replacement;
PatternReplacer(String regex, int flags, String replacement) {
this.pattern = Pattern.compile(regex, flags);
this.replacement = replacement;
}
public String apply(String input) {
return this.pattern.matcher(input).replaceAll(this.replacement);
}
}
}