Make TraceKeys configurable

User can now configure additional HTTP headers to tag via
spring.sleuth.keys.http.headers.

Fixes gh-118
This commit is contained in:
Dave Syer
2016-01-19 09:50:17 +00:00
parent 8e5c9bec53
commit fa5fab9175
10 changed files with 274 additions and 140 deletions

View File

@@ -16,80 +16,103 @@
package org.springframework.cloud.sleuth.instrument;
import java.util.Collection;
import java.util.LinkedHashSet;
import org.springframework.boot.context.properties.ConfigurationProperties;
import lombok.Data;
/**
* Well-known {@link org.springframework.cloud.sleuth.Span#tag(String, String) span tag} keys.
* Well-known {@link org.springframework.cloud.sleuth.Span#tag(String, String) span tag}
* keys.
*
* <h3>Overhead of adding Trace Data</h3>
*
* Overhead is directly related to the size of trace data exported out of process. Accordingly, it
* is better to tag what's important for latency troubleshooting, ie a whitelist, vs collecting
* everything and filtering downstream. The keys listed here are very common in tracing tools, and
* are considerate to the issue of overhead.
* Overhead is directly related to the size of trace data exported out of process.
* Accordingly, it is better to tag what's important for latency troubleshooting, i.e. a
* whitelist vs. collecting everything and filtering downstream. The keys listed here are
* very common in tracing tools, and are considerate to the issue of overhead.
*
* <p>When evaluating new keys, consider how much additional data it implies, and if that data is
* critical to classifying, filtering or displaying traces. More data often means larger systems,
* less retention, or a lower sample rate.
* <p>
* When evaluating new keys, consider how much additional data it implies, and if that
* data is critical to classifying, filtering or displaying traces. More data often means
* larger systems, less retention, or a lower sample rate.
*
* <p>For example, in zipkin, a thrift-encoded span with an "sr" annotation is 82 bytes plus the
* size of its name and associated service. The maximum size of an HTTP cookie is 4096 bytes,
* roughly 50x that. Even if compression helps, if you aren't analyzing based on cookies, storing
* them displaces resources that could be used for more traces. Meanwhile, you have another system
* storing private data! The takeaway isn't never store cookies, as there are valid cases for this.
* The takeaway is to be conscious about what's you are storing.
* <p>
* For example, in zipkin, a thrift-encoded span with an "sr" annotation is 82 bytes plus
* the size of its name and associated service. The maximum size of an HTTP cookie is 4096
* bytes, roughly 50x that. Even if compression helps, if you aren't analyzing based on
* cookies, storing them displaces resources that could be used for more traces.
* Meanwhile, you have another system storing private data! The takeaway isn't never store
* cookies, as there are valid cases for this. The takeaway is to be conscious about
* what's you are storing.
*/
public final class TraceKeys {
@ConfigurationProperties("spring.sleuth.keys")
@Data
public class TraceKeys {
/**
* The domain portion of the URL or host header. Ex. "mybucket.s3.amazonaws.com"
*
* <p>Used to filter by host as opposed to ip address.
*/
public static final String HTTP_HOST = "http/host";
private Http http = new Http();
/**
* The HTTP method, or verb, such as "GET" or "POST".
*
* <p>Used to filter against an http route.
*/
public static final String HTTP_METHOD = "http/method";
@Data
public static class Http {
/**
* The absolute http path, without any query parameters. Ex. "/objects/abcd-ff"
*
* <p>Used to filter against an http route, portably with zipkin v1.
*
* <p>In zipkin v1, only equals filters are supported. Dropping query parameters makes the number
* of distinct URIs less. For example, one can query for the same resource, regardless of signing
* parameters encoded in the query line. This does not reduce cardinality to a HTTP single route.
* For example, it is common to express a route as an http URI template like
* "/resource/{resource_id}". In systems where only equals queries are available, searching for
* {@code http.uri=/resource} won't match if the actual request was "/resource/abcd-ff".
*
* <p>Historical note: This was commonly expressed as "http.uri" in zipkin, eventhough it was most
* often just a path.
*/
public static final String HTTP_PATH = "http/path";
/**
* The domain portion of the URL or host header. Example:
* "mybucket.s3.amazonaws.com". Used to filter by host as opposed to ip address.
*/
private String host = "http/host";
/**
* The entire URL, including the scheme, host and query parameters if available. Ex.
* "https://mybucket.s3.amazonaws.com/objects/abcd-ff?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Algorithm=AWS4-HMAC-SHA256..."
*
* <p>Combined with {@link #HTTP_METHOD}, you can understand the fully-qualified request line.
*
* <p>This is optional as it may include private data or be of considerable length.
*/
public static final String HTTP_URL = "http/url";
/**
* The HTTP method, or verb, such as "GET" or "POST". Used to filter against an
* http route.
*/
private String method = "http/method";
/**
* The HTTP response code, when not in 2xx range. Ex. "503"
*
* <p>Used to filter for error status.
*
* <p>2xx range are not logged as success codes are less interesting for latency troubleshooting.
* Omitting saves at least 20 bytes per span.
*/
public static final String HTTP_STATUS_CODE = "http/status_code";
/**
* The absolute http path, without any query parameters. Example:
* "/objects/abcd-ff". Used to filter against an http route, portably with zipkin
* v1. In zipkin v1, only equals filters are supported. Dropping query parameters
* makes the number of distinct URIs less. For example, one can query for the same
* resource, regardless of signing parameters encoded in the query line. This does
* not reduce cardinality to a HTTP single route. For example, it is common to
* express a route as an http URI template like "/resource/{resource_id}". In
* systems where only equals queries are available, searching for
* {@code http.uri=/resource} won't match if the actual request was
* "/resource/abcd-ff". Historical note: This was commonly expressed as "http.uri"
* in zipkin, eventhough it was most often just a path.
*/
private String path = "http/path";
/**
* The entire URL, including the scheme, host and query parameters if available.
* Ex.
* "https://mybucket.s3.amazonaws.com/objects/abcd-ff?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Algorithm=AWS4-HMAC-SHA256..."
* Combined with {@link #HTTP_METHOD}, you can understand the fully-qualified
* request line. This is optional as it may include private data or be of
* considerable length.
*/
private String url = "http/url";
/**
* The HTTP response code, when not in 2xx range. Ex. "503" Used to filter for
* error status. 2xx range are not logged as success codes are less interesting
* for latency troubleshooting. Omitting saves at least 20 bytes per span.
*/
private String statusCode = "http/status_code";
/**
* Prefix for header names if they are added as tags.
*/
private String prefix = "http/";
/**
* Additional headers that should be added as tags if they exist. If the header
* value is multi-valued, the tag value will be a comma-separated, single-quoted
* list.
*/
private Collection<String> headers = new LinkedHashSet<String>();
}
private TraceKeys() { // no instances
}
}

View File

@@ -15,8 +15,25 @@
*/
package org.springframework.cloud.sleuth.instrument.web;
import org.springframework.cloud.sleuth.*;
import static org.springframework.util.StringUtils.hasText;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.Random;
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.springframework.cloud.sleuth.MilliSpan;
import org.springframework.cloud.sleuth.MilliSpan.MilliSpanBuilder;
import org.springframework.cloud.sleuth.Span;
import org.springframework.cloud.sleuth.Trace;
import org.springframework.cloud.sleuth.Tracer;
import org.springframework.cloud.sleuth.event.ServerReceivedEvent;
import org.springframework.cloud.sleuth.event.ServerSentEvent;
import org.springframework.cloud.sleuth.instrument.TraceKeys;
@@ -31,16 +48,6 @@ import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.UrlPathHelper;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Random;
import java.util.regex.Pattern;
import static org.springframework.util.StringUtils.hasText;
/**
* Filter that takes the value of the {@link Trace#SPAN_ID_NAME} and
* {@link Trace#TRACE_ID_NAME} header from either request or response and uses them to
@@ -71,6 +78,7 @@ public class TraceFilter extends OncePerRequestFilter
"/api-docs.*|/autoconfig|/configprops|/dump|/info|/metrics.*|/mappings|/trace|/swagger.*|.*\\.png|.*\\.css|.*\\.js|.*\\.html|/favicon.ico|/hystrix.stream");
private final Tracer tracer;
private final TraceKeys traceKeys;
private final Pattern skipPattern;
private final Random random;
@@ -78,14 +86,13 @@ public class TraceFilter extends OncePerRequestFilter
private ApplicationEventPublisher publisher;
public TraceFilter(Tracer tracer) {
this.tracer = tracer;
this.skipPattern = DEFAULT_SKIP_PATTERN;
this.random = new Random();
public TraceFilter(Tracer tracer, TraceKeys traceKeys) {
this(tracer, traceKeys, DEFAULT_SKIP_PATTERN, new Random());
}
public TraceFilter(Tracer tracer, Pattern skipPattern, Random random) {
public TraceFilter(Tracer tracer, TraceKeys traceKeys, Pattern skipPattern, Random random) {
this.tracer = tracer;
this.traceKeys = traceKeys;
this.skipPattern = skipPattern;
this.random = random;
}
@@ -117,7 +124,7 @@ public class TraceFilter extends OncePerRequestFilter
if (hasHeader(request, response, Trace.TRACE_ID_NAME)) {
long traceId = Span.IdConverter.fromHex(getHeader(request, response, Trace.TRACE_ID_NAME));
long spanId = hasHeader(request, response, Trace.SPAN_ID_NAME) ?
Span.IdConverter.fromHex(getHeader(request, response, Trace.SPAN_ID_NAME)) : random.nextLong();
Span.IdConverter.fromHex(getHeader(request, response, Trace.SPAN_ID_NAME)) : this.random.nextLong();
MilliSpanBuilder span = MilliSpan.builder().traceId(traceId).spanId(spanId);
if (skip) {
@@ -203,10 +210,22 @@ public class TraceFilter extends OncePerRequestFilter
/** Override to add annotations not defined in {@link TraceKeys}. */
protected void addRequestTags(HttpServletRequest request) {
String uri = this.urlPathHelper.getPathWithinApplication(request);
this.tracer.addTag(TraceKeys.HTTP_URL, getFullUrl(request));
this.tracer.addTag(TraceKeys.HTTP_HOST, request.getServerName());
this.tracer.addTag(TraceKeys.HTTP_PATH, uri);
this.tracer.addTag(TraceKeys.HTTP_METHOD, request.getMethod());
this.tracer.addTag(this.traceKeys.getHttp().getUrl(), getFullUrl(request));
this.tracer.addTag(this.traceKeys.getHttp().getHost(),
request.getServerName());
this.tracer.addTag(this.traceKeys.getHttp().getPath(), uri);
this.tracer.addTag(this.traceKeys.getHttp().getMethod(),
request.getMethod());
for (String name : this.traceKeys.getHttp().getHeaders()) {
Enumeration<String> values = request.getHeaders(name);
if (values.hasMoreElements()) {
String key = this.traceKeys.getHttp().getPrefix() + name.toLowerCase();
ArrayList<String> list = Collections.list(values);
String value = list.size() == 1 ? list.get(0)
: StringUtils.collectionToDelimitedString(list, ",", "'", "'");
this.tracer.addTag(key, value);
}
}
}
/** Override to add annotations not defined in {@link TraceKeys}. */
@@ -215,11 +234,11 @@ public class TraceFilter extends OncePerRequestFilter
if (httpStatus == HttpServletResponse.SC_OK && e != null) {
// Filter chain threw exception but the response status may not have been set
// yet, so we have to guess.
this.tracer.addTag(TraceKeys.HTTP_STATUS_CODE,
this.tracer.addTag(this.traceKeys.getHttp().getStatusCode(),
String.valueOf(HttpServletResponse.SC_INTERNAL_SERVER_ERROR));
}
else if ((httpStatus < 200) || (httpStatus > 299)){
this.tracer.addTag(TraceKeys.HTTP_STATUS_CODE,
this.tracer.addTag(this.traceKeys.getHttp().getStatusCode(),
String.valueOf(response.getStatus()));
}
}

View File

@@ -26,9 +26,11 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.context.embedded.FilterRegistrationBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.sleuth.TraceAccessor;
import org.springframework.cloud.sleuth.Tracer;
import org.springframework.cloud.sleuth.autoconfig.TraceAutoConfiguration;
import org.springframework.cloud.sleuth.instrument.TraceKeys;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -47,6 +49,7 @@ import org.springframework.util.StringUtils;
@ConditionalOnWebApplication
@ConditionalOnBean(Tracer.class)
@AutoConfigureAfter(TraceAutoConfiguration.class)
@EnableConfigurationProperties(TraceKeys.class)
public class TraceWebAutoConfiguration {
/**
@@ -61,6 +64,9 @@ public class TraceWebAutoConfiguration {
@Autowired
private TraceAccessor accessor;
@Autowired
private TraceKeys traceKeys;
@Bean
public TraceWebAspect traceWebAspect() {
return new TraceWebAspect(this.tracer, this.accessor);
@@ -71,7 +77,7 @@ public class TraceWebAutoConfiguration {
public TraceFilter traceFilter(ApplicationEventPublisher publisher, Random random) {
Pattern pattern = StringUtils.hasText(this.skipPattern) ? Pattern.compile(this.skipPattern)
: TraceFilter.DEFAULT_SKIP_PATTERN;
TraceFilter filter = new TraceFilter(this.tracer, pattern, random);
TraceFilter filter = new TraceFilter(this.tracer, this.traceKeys, pattern, random);
filter.setApplicationEventPublisher(publisher);
return filter;
}