Skip to content

Commit 103e5ad

Browse files
author
Adrian Cole
committed
Updates default naming policy to include route-based names
1 parent 0c7282c commit 103e5ad

File tree

4 files changed

+169
-32
lines changed

4 files changed

+169
-32
lines changed

instrumentation/http-tests/src/main/java/brave/http/ITHttpServer.java

+7-6
Original file line numberDiff line numberDiff line change
@@ -186,20 +186,21 @@ public void reportsServerKindToZipkin() throws Exception {
186186
}
187187

188188
@Test
189-
public void defaultSpanNameIsMethodName() throws Exception {
189+
public void defaultSpanNameIsMethodNameOrRoute() throws Exception {
190190
get("/foo");
191191

192192
Span span = takeSpan();
193-
assertThat(span.name())
194-
.isEqualTo("get");
193+
if (!span.name().equals("get")) {
194+
assertThat(span.name())
195+
.isEqualTo("/foo");
196+
}
195197
}
196198

197199
@Test
198200
public void supportsPortableCustomization() throws Exception {
199201
httpTracing = httpTracing.toBuilder().serverParser(new HttpServerParser() {
200202
@Override
201203
public <Req> void request(HttpAdapter<Req, ?> adapter, Req req, SpanCustomizer customizer) {
202-
customizer.name(adapter.method(req).toLowerCase() + " " + adapter.path(req));
203204
customizer.tag("http.url", adapter.url(req)); // just the path is logged by default
204205
customizer.tag("context.visible", String.valueOf(currentTraceContext.get() != null));
205206
}
@@ -210,8 +211,6 @@ public <Req> void request(HttpAdapter<Req, ?> adapter, Req req, SpanCustomizer c
210211
get(uri);
211212

212213
Span span = takeSpan();
213-
assertThat(span.name())
214-
.isEqualTo("get /foo");
215214
assertThat(span.tags())
216215
.containsEntry("http.url", url(uri))
217216
.containsEntry("context.visible", "true");
@@ -253,9 +252,11 @@ private void routeRequestsMatchPrefix(String prefix) throws Exception {
253252

254253
// verify that the path and url reflect the initial request (not a route expression)
255254
assertThat(span1.tags())
255+
.containsEntry("http.method", "GET")
256256
.containsEntry("http.path", prefix + "/1")
257257
.containsEntry("http.url", url(prefix + "/1?foo"));
258258
assertThat(span2.tags())
259+
.containsEntry("http.method", "GET")
259260
.containsEntry("http.path", prefix + "/2")
260261
.containsEntry("http.url", url(prefix + "/2?bar"));
261262

instrumentation/http/README.md

+103-5
Original file line numberDiff line numberDiff line change
@@ -9,26 +9,29 @@ instructions on what to put into http spans, and sampling policy.
99

1010
## Span data policy
1111
By default, the following are added to both http client and server spans:
12-
* Span.name as the http method in lowercase: ex "get"
12+
* Span.name is the http method in lowercase: ex "get" or a route described below
1313
* Tags/binary annotations:
14+
* "http.method", eg "GET"
1415
* "http.path", which does not include query parameters.
1516
* "http.status_code" when the status us not success.
1617
* "error", when there is an exception or status is >=400
1718
* Remote IP and port information
1819

20+
A route based span name looks like "/users/{userId}", "not_found" or
21+
"redirected". There's a longer section on Http Route later in this doc.
22+
1923
Naming and tags are configurable in a library-agnostic way. For example,
2024
the same `HttpTracing` component configures OkHttp or Apache HttpClient
2125
identically.
2226

23-
For example, to change the span and tag naming policy for clients, you
24-
can do something like this:
27+
For example, to change the tagging policy for clients, you can do
28+
something like this:
2529

2630
```java
2731
httpTracing = httpTracing.toBuilder()
2832
.clientParser(new HttpClientParser() {
2933
@Override
3034
public <Req> void request(HttpAdapter<Req, ?> adapter, Req req, SpanCustomizer customizer) {
31-
customizer.name(adapter.method(req).toLowerCase() + " " + adapter.path(req));
3235
customizer.tag(TraceKeys.HTTP_URL, adapter.url(req)); // the whole url, not just the path
3336
}
3437
})
@@ -85,6 +88,29 @@ httpTracingBuilder.serverSampler(HttpRuleSampler.newBuilder()
8588
.build());
8689
```
8790

91+
## Http Route
92+
The http route is an expression such as `/items/:itemId` representing an
93+
application endpoint. `HttpAdapter.route()` parses this from a response,
94+
returning the route that matched the request, empty if no route matched,
95+
or null if routes aren't supported. This value is either used to create
96+
a tag "http.route" or as an input to a span naming function.
97+
98+
### Http route cardinality
99+
The http route groups similar requests together, so results in limited
100+
cardinality, often better choice for a span name vs the http method.
101+
102+
For example, the route `/users/{userId}`, matches `/users/25f4c31d` and
103+
`/users/e3c553be`. If a span name function used the http path instead,
104+
it could DOS-style attack vector on your span name index, as it would
105+
grow unbounded vs `/users/{userId}`. Even if different frameworks use
106+
different formats, such as `/users/[0-9a-f]+` or `/users/:userId`, the
107+
cardinality is still fixed with regards to request count.
108+
109+
The http route can can be empty on redirect or not-found. If you are
110+
using the route for metrics, coerce empty to constants like "redirected"
111+
or "not_found" using the http status. For example, the default span name
112+
policy does this.
113+
88114
# Developing new instrumentation
89115

90116
Check for [instrumentation written here](../instrumentation/) and [Zipkin's list](http://zipkin.io/pages/existing_instrumentations.html)
@@ -177,4 +203,76 @@ try (Tracer.SpanInScope ws = tracer.withSpanInScope(span)) { // 2.
177203
} finally {
178204
handler.handleSend(response, error, span); // 5.
179205
}
180-
```
206+
```
207+
208+
### Supporting HttpAdapter.route(response)
209+
210+
Eventhough the route is associated with the request, not the response,
211+
it is parsed from the response object. The reason is that many server
212+
implementations process the request before they can identify the route.
213+
214+
Instrumentation authors implement support via extending HttpAdapter
215+
accordingly. There are a few patterns which might help.
216+
217+
#### Callback with non-final type
218+
When a framework uses an callback model, you are in control of the type
219+
being parsed. If the response type isn't final, simply subclass it with
220+
the route data.
221+
222+
For example, if spring MVC, it would be this:
223+
```java
224+
// check for a wrapper type which holds the template
225+
handler = HttpServerHandler.create(httpTracing, new HttpServletAdapter() {
226+
@Override public String template(HttpServletResponse response) {
227+
return response instanceof HttpServletResponseWithTemplate
228+
? ((HttpServletResponseWithTemplate) response).route : null;
229+
}
230+
231+
@Override public String toString() {
232+
return "WebMVCAdapter{}";
233+
}
234+
});
235+
236+
--snip--
237+
238+
// when parsing the response, scope the route from the request
239+
240+
Object template = request.getAttribute(BEST_MATCHING_PATTERN_ATTRIBUTE);
241+
if (route != null) {
242+
response = new HttpServletResponseWithTemplate(response, route.toString());
243+
}
244+
handler.handleSend(response, ex, span);
245+
```
246+
247+
#### Callback with final type
248+
If the response type is final, you may be able to make a copy and stash
249+
the route as a synthetic header. Since this is a copy of the response,
250+
there's no chance a user will receive this header in a real response.
251+
252+
Here's an example for Play, where the header "brave-http-template" holds
253+
the route temporarily until the parser can read it.
254+
```scala
255+
result.onComplete {
256+
case Failure(t) => handler.handleSend(null, t, span)
257+
case Success(r) => {
258+
// add a synthetic header if there was a routing path set
259+
var resp = template.map(t => r.withHeaders("brave-http-template" -> t)).getOrElse(r)
260+
handler.handleSend(resp, null, span)
261+
}
262+
}
263+
--snip--
264+
override def template(response: Result): String =
265+
response.header.headers.apply("brave-http-template")
266+
```
267+
268+
#### Common mistakes
269+
270+
For grouping to work, we want routes that are effectively the same, to
271+
in fact be the same. Here are a couple things on that.
272+
273+
* Always start with a leading slash
274+
* This allows you to differentiate the root path from empty (no route)
275+
* This prevents accidental partitioning like `users/:userId` from `/users/:userId`
276+
* Take care not to duplicate slashes
277+
* When joining nested paths, avoid writing templates like `/nested//users/:userId`
278+
* The `ITHttpServer` test will catch some of this

instrumentation/http/src/main/java/brave/http/HttpAdapter.java

+13-13
Original file line numberDiff line numberDiff line change
@@ -38,19 +38,9 @@ public abstract class HttpAdapter<Req, Resp> {
3838
@Nullable public abstract String requestHeader(Req request, String name);
3939

4040
/**
41-
* An expression such as "/items/:itemId" representing an application endpoint, conventionally
42-
* associated with the tag key "http.route".
43-
*
44-
* <p>The http route groups similar requests together, so results in limited cardinality, often
45-
* better choice for a span name vs the http method. However, the route can be absent on redirect
46-
* or file-not-found. Also, not all frameworks include support for http route expressions,
47-
* and some don't expose templates programmatically for readback.
48-
*
49-
* <p>For example, the route "/users/{userId}", matches "/users/25f4c31d" and "/users/e3c553be".
50-
* If a span name function used the http path instead, it could DOS-style attack vector on your
51-
* span name index, as it would grow unbounded vs "/users/{userId}". Even if different frameworks
52-
* use different formats, like "/users/[0-9a-f]+" or "/users/:userId", the cardinality is still
53-
* fixed with regards to request count.
41+
* Returns an expression such as "/items/:itemId" representing an application endpoint,
42+
* conventionally associated with the tag key "http.route". If no route matched, "" (empty string)
43+
* is returned. Null indicates this instrumentation doesn't understand http routes.
5444
*
5545
* <p>Eventhough the route is associated with the request, not the response, this is present
5646
* on the response object. The reasons is that many server implementations process the request
@@ -70,6 +60,16 @@ public abstract class HttpAdapter<Req, Resp> {
7060
*/
7161
@Nullable public abstract Integer statusCode(Resp response);
7262

63+
/**
64+
* The HTTP status code or 0 if unreadable.
65+
*
66+
* <p>Conventionally associated with the key "http.status_code"
67+
*/
68+
public int statusCodeAsInt(Resp response) {
69+
Integer maybeStatus = statusCode(response);
70+
return maybeStatus != null ? maybeStatus : 0;
71+
}
72+
7373
HttpAdapter() {
7474
}
7575
}

instrumentation/http/src/main/java/brave/http/HttpParser.java

+46-8
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
public class HttpParser {
77
/**
88
* Override to change what data from the http request are parsed into the span representing it. By
9-
* default, this sets the span name to the http method and tags "http.path"
9+
* default, this sets the span name to the http method and tags "http.method" and "http.path".
1010
*
1111
* <p>If you only want to change the span name, you can override {@link #spanName(HttpAdapter,
1212
* Object)} instead.
@@ -15,6 +15,8 @@ public class HttpParser {
1515
*/
1616
public <Req> void request(HttpAdapter<Req, ?> adapter, Req req, SpanCustomizer customizer) {
1717
customizer.name(spanName(adapter, req));
18+
String method = adapter.method(req);
19+
if (method != null) customizer.tag("http.method", method);
1820
String path = adapter.path(req);
1921
if (path != null) customizer.tag("http.path", path);
2022
}
@@ -26,8 +28,15 @@ protected <Req> String spanName(HttpAdapter<Req, ?> adapter, Req req) {
2628

2729
/**
2830
* Override to change what data from the http response or error are parsed into the span modeling
29-
* it. By default, this tags "http.status_code" when it is not 2xx. If there's an exception or the
30-
* status code is neither 2xx nor 3xx, it tags "error".
31+
* it.
32+
*
33+
* <p>By default, this tags "http.status_code" when it is not 2xx. If there's an exception or the
34+
* status code is neither 2xx nor 3xx, it tags "error". This also overrides the span name based on
35+
* the {@link HttpAdapter#route(Object)} where possible.
36+
*
37+
* <p>If routing is supported, but didn't match due to 404, the span name will be "not_found". If
38+
* it didn't match due to redirect, the span name will be "redirected". If routing is not
39+
* supported, the span name is left alone.
3140
*
3241
* <p>If you only want to change how exceptions are parsed, override {@link #error(Integer,
3342
* Throwable, SpanCustomizer)} instead.
@@ -40,11 +49,40 @@ protected <Req> String spanName(HttpAdapter<Req, ?> adapter, Req req) {
4049
// If this were not an abstraction, we'd use separate hooks for response and error.
4150
public <Resp> void response(HttpAdapter<?, Resp> adapter, @Nullable Resp res,
4251
@Nullable Throwable error, SpanCustomizer customizer) {
43-
Integer httpStatus = res != null ? adapter.statusCode(res) : null;
44-
if (httpStatus != null && (httpStatus < 200 || httpStatus > 299)) {
45-
customizer.tag("http.status_code", String.valueOf(httpStatus));
52+
int statusCode = 0;
53+
if (res != null) {
54+
statusCode = adapter.statusCodeAsInt(res);
55+
String route = adapter.route(res);
56+
if (route != null) {
57+
if (!"".equals(route)) {
58+
customizer.name(route);
59+
} else if (statusCode / 100 == 3) {
60+
customizer.name("redirected");
61+
} else if (statusCode == 404) {
62+
customizer.name("not_found");
63+
}
64+
}
65+
if (statusCode != 0 && (statusCode < 200 || statusCode > 299)) {
66+
customizer.tag("http.status_code", String.valueOf(statusCode));
67+
}
4668
}
47-
error(httpStatus, error, customizer);
69+
error(statusCode, error, customizer);
70+
}
71+
72+
/**
73+
* Returns a span name according to the {@link HttpAdapter#route(Object)}, or the constant
74+
* "redirected" or "not_found". Returns null if routing isn't supported or there was no http
75+
* status available.
76+
*/
77+
@Nullable protected <Resp> String spanNameFromRoute(HttpAdapter<?, Resp> adapter, Resp res) {
78+
String route = adapter.route(res);
79+
if (route == null) return null;
80+
if (!"".equals(route)) return route;
81+
Integer statusCode = adapter.statusCode(res);
82+
if (statusCode == null) return null;
83+
if (statusCode / 100 == 3) return "redirected";
84+
if (statusCode == 404) return "not_found";
85+
return null;
4886
}
4987

5088
/**
@@ -61,7 +99,7 @@ protected void error(@Nullable Integer httpStatus, @Nullable Throwable error,
6199
if (error != null) {
62100
message = error.getMessage();
63101
if (message == null) message = error.getClass().getSimpleName();
64-
} else if (httpStatus != null) {
102+
} else if (httpStatus != null && httpStatus != 0) {
65103
message = httpStatus < 200 || httpStatus > 399 ? String.valueOf(httpStatus) : null;
66104
}
67105
if (message != null) customizer.tag("error", message);

0 commit comments

Comments
 (0)