Skip to content

Commit 82e47db

Browse files
committed
Instrument reactive servers for Observability
This commit introduces a `HttpRequestsObservationWebFilter` which instruments web frameworks using Spring's reactive `ServerHttpRequest` and `ServerHttpResponse` interfaces. This replaces Spring Boot's `MetricsWebFilter`. See gh-28880
1 parent 6eded96 commit 82e47db

8 files changed

+771
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
/*
2+
* Copyright 2002-2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.web.observation.reactive;
18+
19+
import io.micrometer.common.KeyValue;
20+
import io.micrometer.common.KeyValues;
21+
22+
import org.springframework.http.HttpStatus;
23+
import org.springframework.web.util.pattern.PathPattern;
24+
25+
/**
26+
* Default {@link HttpRequestsObservationConvention}.
27+
*
28+
* @author Brian Clozel
29+
* @since 6.0
30+
*/
31+
public class DefaultHttpRequestsObservationConvention implements HttpRequestsObservationConvention {
32+
33+
private static final String DEFAULT_NAME = "http.server.requests";
34+
35+
private static final KeyValue METHOD_UNKNOWN = KeyValue.of(HttpRequestsObservation.LowCardinalityKeyNames.METHOD, "UNKNOWN");
36+
37+
private static final KeyValue STATUS_UNKNOWN = KeyValue.of(HttpRequestsObservation.LowCardinalityKeyNames.STATUS, "UNKNOWN");
38+
39+
private static final KeyValue URI_UNKNOWN = KeyValue.of(HttpRequestsObservation.LowCardinalityKeyNames.URI, "UNKNOWN");
40+
41+
private static final KeyValue URI_ROOT = KeyValue.of(HttpRequestsObservation.LowCardinalityKeyNames.URI, "root");
42+
43+
private static final KeyValue URI_NOT_FOUND = KeyValue.of(HttpRequestsObservation.LowCardinalityKeyNames.URI, "NOT_FOUND");
44+
45+
private static final KeyValue URI_REDIRECTION = KeyValue.of(HttpRequestsObservation.LowCardinalityKeyNames.URI, "REDIRECTION");
46+
47+
private static final KeyValue EXCEPTION_NONE = KeyValue.of(HttpRequestsObservation.LowCardinalityKeyNames.EXCEPTION, "none");
48+
49+
private static final KeyValue OUTCOME_UNKNOWN = KeyValue.of(HttpRequestsObservation.LowCardinalityKeyNames.OUTCOME, "UNKNOWN");
50+
51+
private static final KeyValue URI_EXPANDED_UNKNOWN = KeyValue.of(HttpRequestsObservation.HighCardinalityKeyNames.URI_EXPANDED, "UNKNOWN");
52+
53+
private final String name;
54+
55+
/**
56+
* Create a convention with the default name {@code "http.server.requests"}.
57+
*/
58+
public DefaultHttpRequestsObservationConvention() {
59+
this(DEFAULT_NAME);
60+
}
61+
62+
/**
63+
* Create a convention with a custom name.
64+
*
65+
* @param name the observation name
66+
*/
67+
public DefaultHttpRequestsObservationConvention(String name) {
68+
this.name = name;
69+
}
70+
71+
@Override
72+
public String getName() {
73+
return this.name;
74+
}
75+
76+
@Override
77+
public KeyValues getLowCardinalityKeyValues(HttpRequestsObservationContext context) {
78+
return KeyValues.of(method(context), uri(context), status(context), exception(context), outcome(context));
79+
}
80+
81+
@Override
82+
public KeyValues getHighCardinalityKeyValues(HttpRequestsObservationContext context) {
83+
return KeyValues.of(uriExpanded(context));
84+
}
85+
86+
protected KeyValue method(HttpRequestsObservationContext context) {
87+
return (context.getCarrier() != null) ? KeyValue.of(HttpRequestsObservation.LowCardinalityKeyNames.METHOD, context.getCarrier().getMethod().name()) : METHOD_UNKNOWN;
88+
}
89+
90+
protected KeyValue status(HttpRequestsObservationContext context) {
91+
return (context.getResponse() != null) ? KeyValue.of(HttpRequestsObservation.LowCardinalityKeyNames.STATUS, Integer.toString(context.getResponse().getStatusCode().value())) : STATUS_UNKNOWN;
92+
}
93+
94+
protected KeyValue uri(HttpRequestsObservationContext context) {
95+
if (context.getCarrier() != null) {
96+
PathPattern pattern = context.getPathPattern();
97+
if (pattern != null) {
98+
if (pattern.toString().isEmpty()) {
99+
return URI_ROOT;
100+
}
101+
return KeyValue.of("uri", pattern.toString());
102+
}
103+
if (context.getResponse() != null) {
104+
HttpStatus status = HttpStatus.resolve(context.getResponse().getStatusCode().value());
105+
if (status != null) {
106+
if (status.is3xxRedirection()) {
107+
return URI_REDIRECTION;
108+
}
109+
if (status == HttpStatus.NOT_FOUND) {
110+
return URI_NOT_FOUND;
111+
}
112+
}
113+
}
114+
}
115+
return URI_UNKNOWN;
116+
}
117+
118+
protected KeyValue exception(HttpRequestsObservationContext context) {
119+
return context.getError().map(throwable ->
120+
KeyValue.of(HttpRequestsObservation.LowCardinalityKeyNames.EXCEPTION, throwable.getClass().getSimpleName()))
121+
.orElse(EXCEPTION_NONE);
122+
}
123+
124+
protected KeyValue outcome(HttpRequestsObservationContext context) {
125+
if (context.isConnectionAborted()) {
126+
return OUTCOME_UNKNOWN;
127+
}
128+
else if (context.getResponse() != null) {
129+
HttpStatus status = HttpStatus.resolve(context.getResponse().getStatusCode().value());
130+
if (status != null) {
131+
return KeyValue.of(HttpRequestsObservation.LowCardinalityKeyNames.OUTCOME, status.series().name());
132+
}
133+
}
134+
return OUTCOME_UNKNOWN;
135+
}
136+
137+
protected KeyValue uriExpanded(HttpRequestsObservationContext context) {
138+
if (context.getCarrier() != null) {
139+
String uriExpanded = context.getCarrier().getPath().toString();
140+
return KeyValue.of(HttpRequestsObservation.HighCardinalityKeyNames.URI_EXPANDED, uriExpanded);
141+
}
142+
return URI_EXPANDED_UNKNOWN;
143+
}
144+
145+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
/*
2+
* Copyright 2002-2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.web.observation.reactive;
18+
19+
import io.micrometer.common.docs.KeyName;
20+
import io.micrometer.observation.Observation;
21+
import io.micrometer.observation.ObservationConvention;
22+
import io.micrometer.observation.docs.DocumentedObservation;
23+
24+
/**
25+
* Documented {@link io.micrometer.common.KeyValue KeyValues} for the HTTP server observations
26+
* for Servlet-based web applications.
27+
* <p>This class is used by automated tools to document KeyValues attached to the HTTP server observations.
28+
* @author Brian Clozel
29+
* @since 6.0
30+
*/
31+
public enum HttpRequestsObservation implements DocumentedObservation {
32+
33+
/**
34+
* HTTP server request observations.
35+
*/
36+
HTTP_REQUESTS {
37+
@Override
38+
public Class<? extends ObservationConvention<? extends Observation.Context>> getDefaultConvention() {
39+
return DefaultHttpRequestsObservationConvention.class;
40+
}
41+
42+
@Override
43+
public KeyName[] getLowCardinalityKeyNames() {
44+
return LowCardinalityKeyNames.values();
45+
}
46+
47+
@Override
48+
public KeyName[] getHighCardinalityKeyNames() {
49+
return HighCardinalityKeyNames.values();
50+
}
51+
52+
};
53+
54+
public enum LowCardinalityKeyNames implements KeyName {
55+
56+
/**
57+
* Name of HTTP request method or {@code "None"} if the request was not received properly.
58+
*/
59+
METHOD {
60+
@Override
61+
public String asString() {
62+
return "method";
63+
}
64+
65+
},
66+
67+
/**
68+
* HTTP response raw status code, or {@code "STATUS_UNKNOWN"} if no response was created.
69+
*/
70+
STATUS {
71+
@Override
72+
public String asString() {
73+
return "status";
74+
}
75+
},
76+
77+
/**
78+
* URI pattern for the matching handler if available, falling back to {@code REDIRECTION} for 3xx responses,
79+
* {@code NOT_FOUND} for 404 responses, {@code root} for requests with no path info,
80+
* and {@code UNKNOWN} for all other requests.
81+
*/
82+
URI {
83+
@Override
84+
public String asString() {
85+
return "uri";
86+
}
87+
},
88+
89+
/**
90+
* Name of the exception thrown during the exchange, or {@code "None"} if no exception happened.
91+
*/
92+
EXCEPTION {
93+
@Override
94+
public String asString() {
95+
return "exception";
96+
}
97+
},
98+
99+
/**
100+
* Outcome of the HTTP server exchange.
101+
* @see org.springframework.http.HttpStatus.Series
102+
*/
103+
OUTCOME {
104+
@Override
105+
public String asString() {
106+
return "outcome";
107+
}
108+
}
109+
}
110+
111+
public enum HighCardinalityKeyNames implements KeyName {
112+
113+
/**
114+
* HTTP request URI.
115+
*/
116+
URI_EXPANDED {
117+
@Override
118+
public String asString() {
119+
return "uri.expanded";
120+
}
121+
}
122+
123+
}
124+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/*
2+
* Copyright 2002-2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.web.observation.reactive;
18+
19+
import io.micrometer.observation.transport.RequestReplyReceiverContext;
20+
21+
import org.springframework.http.server.reactive.ServerHttpRequest;
22+
import org.springframework.http.server.reactive.ServerHttpResponse;
23+
import org.springframework.lang.Nullable;
24+
import org.springframework.web.server.ServerWebExchange;
25+
import org.springframework.web.util.pattern.PathPattern;
26+
27+
/**
28+
* Context that holds information for metadata collection during observations for reactive web applications.
29+
* <p>This context also extends {@link RequestReplyReceiverContext} for propagating
30+
* tracing information with the HTTP server exchange.
31+
* @author Brian Clozel
32+
* @since 6.0
33+
*/
34+
public class HttpRequestsObservationContext extends RequestReplyReceiverContext<ServerHttpRequest, ServerHttpResponse> {
35+
36+
@Nullable
37+
private PathPattern pathPattern;
38+
39+
private boolean connectionAborted;
40+
41+
public HttpRequestsObservationContext(ServerWebExchange exchange) {
42+
super((request, key) -> request.getHeaders().getFirst(key));
43+
this.setCarrier(exchange.getRequest());
44+
this.setResponse(exchange.getResponse());
45+
}
46+
47+
/**
48+
* Return the path pattern for the handler that matches the current request.
49+
* For example, {@code "/projects/{name}"}.
50+
* <p>Path patterns must have a low cardinality for the entire application.
51+
* @return the path pattern, or {@code null} if none found
52+
*/
53+
@Nullable
54+
public PathPattern getPathPattern() {
55+
return this.pathPattern;
56+
}
57+
58+
/**
59+
* Set the path pattern for the handler that matches the current request.
60+
* <p>Path patterns must have a low cardinality for the entire application.
61+
* @param pathPattern the path pattern, for example {@code "/projects/{name}"}.
62+
*/
63+
public void setPathPattern(@Nullable PathPattern pathPattern) {
64+
this.pathPattern = pathPattern;
65+
}
66+
67+
/**
68+
* Whether the current connection was aborted by the client, resulting
69+
* in a {@link reactor.core.publisher.SignalType#CANCEL cancel signal} on te reactive chain,
70+
* or an {@code AbortedException} when reading the request.
71+
* @return if the connection has been aborted
72+
*/
73+
public boolean isConnectionAborted() {
74+
return this.connectionAborted;
75+
}
76+
77+
void setConnectionAborted(boolean connectionAborted) {
78+
this.connectionAborted = connectionAborted;
79+
}
80+
81+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* Copyright 2002-2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.web.observation.reactive;
18+
19+
import io.micrometer.observation.Observation;
20+
import io.micrometer.observation.ObservationConvention;
21+
22+
/**
23+
* Interface for an {@link ObservationConvention} related to reactive HTTP exchanges.
24+
* @author Brian Clozel
25+
* @since 6.0
26+
*/
27+
public interface HttpRequestsObservationConvention extends ObservationConvention<HttpRequestsObservationContext> {
28+
29+
@Override
30+
default boolean supportsContext(Observation.Context context) {
31+
return context instanceof HttpRequestsObservationContext;
32+
}
33+
34+
}

0 commit comments

Comments
 (0)