/**
* Copyright 2017 Pivotal Software, Inc.
*
* 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.metrics.instrument.web;
import org.springframework.http.HttpRequest;
import org.springframework.http.HttpStatus;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.metrics.instrument.Tag;
import org.springframework.util.StringUtils;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.servlet.HandlerMapping;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.stream.Stream;
/**
* Adds a sensible set of tags to Spring Web/Webflux timers and RestTemplate/WebClient timers. Note that
* HTTP status is added by default to Spring Web/Webflux timers, requiring Servlet 3.0+ (i.e. Tomcat 7+).
*
* @author Jon Schneider
*/
public class DefaultWebMetricsTagProvider implements WebMetricsTagProvider {
@Override
public Stream<Tag> clientHttpRequestTags(HttpRequest request,
ClientHttpResponse response) {
String urlTemplate = RestTemplateUrlTemplateHolder.getRestTemplateUrlTemplate();
if (urlTemplate == null) {
urlTemplate = "none";
}
String status;
try {
status = (response == null) ? "CLIENT_ERROR" : ((Integer) response
.getRawStatusCode()).toString();
} catch (IOException e) {
status = "IO_ERROR";
}
String host = request.getURI().getHost();
if (host == null) {
host = "none";
}
String strippedUrlTemplate = urlTemplate.replaceAll("^https?://[^/]+/", "");
return Stream.of(Tag.of("method", request.getMethod().name()),
Tag.of("uri", sanitizeUrlTemplate(strippedUrlTemplate)),
Tag.of("status", status),
Tag.of("clientName", host));
}
@Override
public Stream<Tag> httpLongRequestTags(HttpServletRequest request, Object handler) {
Stream.Builder<Tag> tags = Stream.builder();
tags.add(Tag.of("method", request.getMethod()));
String uri = (String) request.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE);
if (uri == null) {
uri = request.getPathInfo();
}
if (!StringUtils.hasText(uri)) {
uri = "/";
}
uri = sanitizeUrlTemplate(uri.substring(1));
tags.add(Tag.of("uri", uri.isEmpty() ? "root" : uri));
return tags.build();
}
@Override
public Stream<Tag> httpRequestTags(HttpServletRequest request,
HttpServletResponse response, Object handler) {
Stream.Builder<Tag> tags = Stream.builder();
tags.add(Tag.of("method", request.getMethod()));
tags.add(Tag.of("status", ((Integer) response.getStatus()).toString()));
String uri = (String) request.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE);
if (uri == null) {
uri = request.getPathInfo();
}
if (!StringUtils.hasText(uri)) {
uri = "/";
}
uri = sanitizeUrlTemplate(uri.substring(1));
tags.add(Tag.of("uri", uri.isEmpty() ? "root" : uri));
Object exception = request.getAttribute("exception");
if (exception != null) {
tags.add(Tag.of("exception", exception.getClass().getSimpleName()));
}
return tags.build();
}
@Override
public Stream<Tag> httpRequestTags(ServerWebExchange exchange, Throwable exception) {
Stream.Builder<Tag> tags = Stream.builder();
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
tags.add(Tag.of("method", request.getMethod().toString()));
HttpStatus status = response.getStatusCode();
if(status == null)
status = HttpStatus.OK;
tags.add(Tag.of("status", status.toString()));
String uri = (String) exchange.getAttribute(org.springframework.web.reactive.HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE).orElse(null);
if (!StringUtils.hasText(uri)) {
uri = "/";
}
uri = sanitizeUrlTemplate(uri.substring(1));
tags.add(Tag.of("uri", uri.isEmpty() ? "root" : uri));
if (exception != null) {
tags.add(Tag.of("exception", exception.getClass().getSimpleName()));
}
return tags.build();
}
@Override
public Stream<Tag> httpRequestTags(ServerRequest request, ServerResponse response, String uri, Throwable exception) {
Stream.Builder<Tag> tags = Stream.builder();
tags.add(Tag.of("method", request.method().toString()));
tags.add(Tag.of("status", response.statusCode().toString()));
if (!StringUtils.hasText(uri)) {
uri = "/";
}
uri = sanitizeUrlTemplate(uri.substring(1));
tags.add(Tag.of("uri", uri.isEmpty() ? "root" : uri));
if (exception != null) {
tags.add(Tag.of("exception", exception.getClass().getSimpleName()));
}
return tags.build();
}
/**
* As is, the urlTemplate is not suitable for use with Atlas, as all interactions with
* Atlas take place via query parameters
*/
protected String sanitizeUrlTemplate(String urlTemplate) {
// FIXME generalize this on a per-exporter basis (Prometheus will have different requirements than Atlas, etc).
String sanitized = urlTemplate
.replaceAll("\\{(\\w+):.+}(?=/|$)", "-$1-") // extract path variable names from regex expressions
.replaceAll("/", "_")
.replaceAll("[{}]", "-");
if (!StringUtils.hasText(sanitized)) {
sanitized = "none";
}
return sanitized;
}
}