package org.stagemonitor.web.monitor;
import org.stagemonitor.configuration.ConfigurationRegistry;
import org.stagemonitor.core.Stagemonitor;
import org.stagemonitor.core.metrics.metrics2.Metric2Registry;
import org.stagemonitor.core.metrics.metrics2.MetricName;
import org.stagemonitor.tracing.MonitoredRequest;
import org.stagemonitor.tracing.SpanContextInformation;
import org.stagemonitor.tracing.TracingPlugin;
import org.stagemonitor.tracing.utils.SpanUtils;
import org.stagemonitor.tracing.wrapper.SpanWrapper;
import org.stagemonitor.tracing.wrapper.StatelessSpanEventListener;
import org.stagemonitor.util.StringUtils;
import org.stagemonitor.web.WebPlugin;
import org.stagemonitor.web.monitor.filter.StatusExposingByteCountingServletResponse;
import org.stagemonitor.web.monitor.widget.WidgetAjaxSpanReporter;
import org.stagemonitor.web.tracing.HttpServletRequestTextMapExtractAdapter;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.Principal;
import java.util.Collection;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
import javax.servlet.FilterChain;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import io.opentracing.Span;
import io.opentracing.Tracer;
import io.opentracing.propagation.Format;
import io.opentracing.tag.Tags;
import static org.stagemonitor.core.metrics.metrics2.MetricName.name;
public class MonitoredHttpRequest extends MonitoredRequest {
public static final String CONNECTION_ID_ATTRIBUTE = "connectionId";
public static final String WIDGET_ALLOWED_ATTRIBUTE = "showWidgetAllowed";
public static final String MONITORED_HTTP_REQUEST_ATTRIBUTE = "MonitoredHttpRequest";
// has to be static so that the cache is shared between different requests
private static UserAgentParser userAgentParser;
protected final HttpServletRequest httpServletRequest;
protected final FilterChain filterChain;
protected final StatusExposingByteCountingServletResponse responseWrapper;
protected final WebPlugin webPlugin;
private final TracingPlugin tracingPlugin;
private static final MetricName.MetricNameTemplate throughputMetricNameTemplate = name("request_throughput").templateFor("request_name", "http_code");
private final String userAgentHeader;
private final String connectionId;
private final boolean widgetAndStagemonitorEndpointsAllowed;
private final String clientIp;
public MonitoredHttpRequest(HttpServletRequest httpServletRequest,
StatusExposingByteCountingServletResponse responseWrapper,
FilterChain filterChain, ConfigurationRegistry configuration) {
this.httpServletRequest = httpServletRequest;
this.filterChain = filterChain;
this.responseWrapper = responseWrapper;
this.webPlugin = configuration.getConfig(WebPlugin.class);
tracingPlugin = configuration.getConfig(TracingPlugin.class);
userAgentHeader = httpServletRequest.getHeader("user-agent");
connectionId = httpServletRequest.getHeader(WidgetAjaxSpanReporter.CONNECTION_ID);
widgetAndStagemonitorEndpointsAllowed = webPlugin.isWidgetAndStagemonitorEndpointsAllowed(httpServletRequest, configuration);
clientIp = getClientIp(httpServletRequest);
}
@Override
public Span createSpan() {
boolean sample = true;
if (webPlugin.isHonorDoNotTrackHeader() && "1".equals(httpServletRequest.getHeader("dnt"))) {
sample = false;
}
final Tracer tracer = tracingPlugin.getTracer();
io.opentracing.SpanContext spanCtx = tracer.extract(Format.Builtin.HTTP_HEADERS, new HttpServletRequestTextMapExtractAdapter(httpServletRequest));
Tracer.SpanBuilder spanBuilder = tracer.buildSpan(getRequestName())
.asChildOf(spanCtx)
.withTag(Tags.SPAN_KIND.getKey(), Tags.SPAN_KIND_SERVER);
if (widgetAndStagemonitorEndpointsAllowed) {
spanBuilder = spanBuilder.withTag(WIDGET_ALLOWED_ATTRIBUTE, (String) null);
}
if (!sample) {
spanBuilder = spanBuilder.withTag(Tags.SAMPLING_PRIORITY.getKey(), (short) 0);
}
final Span span = spanBuilder.start();
span.setTag(SpanUtils.OPERATION_TYPE, "http");
Tags.HTTP_URL.set(span, httpServletRequest.getRequestURI());
Tags.PEER_PORT.set(span, httpServletRequest.getRemotePort());
span.setTag("method", httpServletRequest.getMethod());
span.setTag("http.referring_site", getReferringSite());
if (webPlugin.isCollectHttpHeaders()) {
SpanUtils.setHttpHeaders(span, getHeaders(httpServletRequest));
}
SpanContextInformation info = SpanContextInformation.forSpan(span);
info.addRequestAttribute(CONNECTION_ID_ATTRIBUTE, connectionId);
info.addRequestAttribute(MONITORED_HTTP_REQUEST_ATTRIBUTE, this);
return span;
}
private String getReferringSite() {
final String refererHeader = httpServletRequest.getHeader("Referer");
if (StringUtils.isEmpty(refererHeader)) {
return null;
}
String referrerHost;
try {
referrerHost = new URI(refererHeader).getHost();
} catch (URISyntaxException e) {
referrerHost = null;
}
if (httpServletRequest.getServerName().equals(referrerHost)) {
return null;
} else {
return referrerHost;
}
}
public static String getClientIp(HttpServletRequest request) {
String ip = request.getHeader("X-Forwarded-For");
ip = getIpFromHeaderIfNotAlreadySet("X-Real-IP", request, ip);
ip = getIpFromHeaderIfNotAlreadySet("Proxy-Client-IP", request, ip);
ip = getIpFromHeaderIfNotAlreadySet("WL-Proxy-Client-IP", request, ip);
ip = getIpFromHeaderIfNotAlreadySet("HTTP_CLIENT_IP", request, ip);
ip = getIpFromHeaderIfNotAlreadySet("HTTP_X_FORWARDED_FOR", request, ip);
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return ip;
}
private static String getIpFromHeaderIfNotAlreadySet(String header, HttpServletRequest request, String ip) {
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader(header);
}
return ip;
}
public String getRequestName() {
if (webPlugin.isMonitorOnlySpringMvcRequests() || webPlugin.isMonitorOnlyResteasyRequests()) {
return null;
} else {
return getRequestNameByRequest(httpServletRequest, webPlugin);
}
}
public static String getRequestNameByRequest(HttpServletRequest request, WebPlugin webPlugin) {
String requestURI = removeSemicolonContent(request.getRequestURI().substring(request.getContextPath().length()));
for (Map.Entry<Pattern, String> entry : webPlugin.getGroupUrls().entrySet()) {
requestURI = entry.getKey().matcher(requestURI).replaceAll(entry.getValue());
}
return request.getMethod() + " " + requestURI;
}
private static String removeSemicolonContent(String requestUri) {
int semicolonIndex = requestUri.indexOf(';');
while (semicolonIndex != -1) {
int slashIndex = requestUri.indexOf('/', semicolonIndex);
String start = requestUri.substring(0, semicolonIndex);
requestUri = (slashIndex != -1) ? start + requestUri.substring(slashIndex) : start;
semicolonIndex = requestUri.indexOf(';', semicolonIndex);
}
return requestUri;
}
private Map<String, String> getHeaders(HttpServletRequest request) {
Map<String, String> headers = new HashMap<String, String>();
final Enumeration headerNames = request.getHeaderNames();
final Collection<String> excludedHeaders = webPlugin.getExcludeHeaders();
while (headerNames.hasMoreElements()) {
final String headerName = ((String) headerNames.nextElement()).toLowerCase();
if (!excludedHeaders.contains(headerName)) {
headers.put(headerName, request.getHeader(headerName));
}
}
return headers;
}
@Override
public void execute() throws Exception {
filterChain.doFilter(httpServletRequest, responseWrapper);
}
public static class HttpSpanEventListener extends StatelessSpanEventListener {
private final WebPlugin webPlugin;
private final TracingPlugin tracingPlugin;
private final Metric2Registry metricRegistry;
public HttpSpanEventListener() {
this(Stagemonitor.getPlugin(WebPlugin.class), Stagemonitor.getPlugin(TracingPlugin.class), Stagemonitor.getMetric2Registry());
}
public HttpSpanEventListener(WebPlugin webPlugin, TracingPlugin tracingPlugin, Metric2Registry metricRegistry) {
this.webPlugin = webPlugin;
this.tracingPlugin = tracingPlugin;
this.metricRegistry = metricRegistry;
}
@Override
public void onFinish(SpanWrapper span, String operationName, long durationNanos) {
final MonitoredHttpRequest monitoredHttpRequest = (MonitoredHttpRequest) SpanContextInformation.forSpan(span)
.getRequestAttribute(MONITORED_HTTP_REQUEST_ATTRIBUTE);
if (monitoredHttpRequest == null) {
return;
}
trackServletExceptions(span, monitoredHttpRequest.httpServletRequest);
setParams(span, monitoredHttpRequest.httpServletRequest);
setTrackingInformation(span, monitoredHttpRequest.httpServletRequest, monitoredHttpRequest.clientIp, monitoredHttpRequest.userAgentHeader);
setStatus(span, monitoredHttpRequest.responseWrapper.getStatus());
if (operationName != null) {
trackThroughput(operationName, monitoredHttpRequest.responseWrapper.getStatus());
}
span.setTag("bytes_written", monitoredHttpRequest.responseWrapper.getContentLength());
if (webPlugin.isParseUserAgent()) {
setUserAgentInformation(span, monitoredHttpRequest.userAgentHeader);
}
}
private void setStatus(Span span, int status) {
Tags.HTTP_STATUS.set(span, status);
if (status >= 400) {
Tags.ERROR.set(span, true);
}
}
private void trackThroughput(String operationName, int status) {
metricRegistry.meter(throughputMetricNameTemplate.build(operationName, Integer.toString(status))).mark();
metricRegistry.meter(throughputMetricNameTemplate.build("All", Integer.toString(status))).mark();
}
private void setUserAgentInformation(Span span, String userAgenHeader) {
// this is safe even though userAgentParser is static because onBeforeReport is not executed concurrently
if (userAgentParser == null) {
userAgentParser = new UserAgentParser();
}
userAgentParser.setUserAgentInformation(span, userAgenHeader);
}
private void setTrackingInformation(Span span, HttpServletRequest httpServletRequest, String clientIp, String userAgenHeader) {
final String userName = getUserName(httpServletRequest);
final String sessionId = getSessionId(httpServletRequest);
span.setTag(SpanUtils.USERNAME, userName);
span.setTag("session_id", sessionId);
if (userName != null) {
span.setTag("tracking.unique_visitor_id", StringUtils.sha1Hash(userName));
} else {
span.setTag("tracking.unique_visitor_id", StringUtils.sha1Hash(clientIp + sessionId + userAgenHeader));
}
SpanUtils.setClientIp(span, clientIp);
}
private void trackServletExceptions(Span span, HttpServletRequest httpServletRequest) {
// Search the configured exception attributes that may have been set
// by the servlet container/framework. Use the first exception found (if any)
for (String requestExceptionAttribute : webPlugin.getRequestExceptionAttributes()) {
Object exception = httpServletRequest.getAttribute(requestExceptionAttribute);
if (exception != null && exception instanceof Exception) {
SpanUtils.setException(span, (Exception) exception, tracingPlugin.getIgnoreExceptions(), tracingPlugin.getUnnestExceptions());
break;
}
}
}
private void setParams(Span span, HttpServletRequest httpServletRequest) {
// get the parameters after the execution and not on creation, because that could lead to wrong decoded
// parameters inside the application
@SuppressWarnings("unchecked") // according to javadoc, its always a Map<String, String[]>
final Map<String, String[]> parameterMap = httpServletRequest.getParameterMap();
Map<String, String> params = new HashMap<String, String>();
for (Map.Entry<String, String[]> entry : parameterMap.entrySet()) {
params.put(entry.getKey(), StringUtils.toCommaSeparatedString(entry.getValue()));
}
Set<Pattern> confidentialParams = new HashSet<Pattern>();
confidentialParams.addAll(webPlugin.getRequestParamsConfidential());
confidentialParams.addAll(tracingPlugin.getConfidentialParameters());
SpanUtils.setParameters(span, TracingPlugin.getSafeParameterMap(params, confidentialParams));
}
private String getSessionId(HttpServletRequest httpServletRequest) {
final HttpSession session = httpServletRequest.getSession(false);
return session != null ? session.getId() : null;
}
private String getUserName(HttpServletRequest httpServletRequest) {
final Principal userPrincipal = httpServletRequest.getUserPrincipal();
return userPrincipal != null ? userPrincipal.getName() : null;
}
}
}