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