Skip to content

Commit 6eded96

Browse files
committed
Instrument Servlet server apps for Observability
This commit introduces the new `HttpRequestsObservationFilter` This `Filter` can be used to instrument Servlet-based web frameworks for Micrometer Observations. While the Servlet request and responses are automatically used for extracting KeyValues for observations, web frameworks still need to provide the matching URL pattern, if supported. This can be done by fetching the observation context from the request attributes and contributing to it. This commit instruments Spring MVC (annotation and functional variants), effectively replacing Spring Boot's `WebMvcMetricsFilter`. See gh-28880
1 parent ac9360b commit 6eded96

File tree

13 files changed

+756
-0
lines changed

13 files changed

+756
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
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;
18+
19+
import io.micrometer.common.KeyValue;
20+
import io.micrometer.common.KeyValues;
21+
22+
import org.springframework.http.HttpStatus;
23+
24+
/**
25+
* Default {@link HttpRequestsObservationConvention}.
26+
* @author Brian Clozel
27+
* @since 6.0
28+
*/
29+
public class DefaultHttpRequestsObservationConvention implements HttpRequestsObservationConvention {
30+
31+
private static final String DEFAULT_NAME = "http.server.requests";
32+
33+
private static final KeyValue METHOD_UNKNOWN = KeyValue.of(HttpRequestsObservation.LowCardinalityKeyNames.METHOD, "UNKNOWN");
34+
35+
private static final KeyValue STATUS_UNKNOWN = KeyValue.of(HttpRequestsObservation.LowCardinalityKeyNames.STATUS, "UNKNOWN");
36+
37+
private static final KeyValue URI_UNKNOWN = KeyValue.of(HttpRequestsObservation.LowCardinalityKeyNames.URI, "UNKNOWN");
38+
39+
private static final KeyValue URI_ROOT = KeyValue.of(HttpRequestsObservation.LowCardinalityKeyNames.URI, "root");
40+
41+
private static final KeyValue URI_NOT_FOUND = KeyValue.of(HttpRequestsObservation.LowCardinalityKeyNames.URI, "NOT_FOUND");
42+
43+
private static final KeyValue URI_REDIRECTION = KeyValue.of(HttpRequestsObservation.LowCardinalityKeyNames.URI, "REDIRECTION");
44+
45+
private static final KeyValue EXCEPTION_NONE = KeyValue.of(HttpRequestsObservation.LowCardinalityKeyNames.EXCEPTION, "none");
46+
47+
private static final KeyValue OUTCOME_UNKNOWN = KeyValue.of(HttpRequestsObservation.LowCardinalityKeyNames.OUTCOME, "UNKNOWN");
48+
49+
private static final KeyValue URI_EXPANDED_UNKNOWN = KeyValue.of(HttpRequestsObservation.HighCardinalityKeyNames.URI_EXPANDED, "UNKNOWN");
50+
51+
private final String name;
52+
53+
/**
54+
* Create a convention with the default name {@code "http.server.requests"}.
55+
*/
56+
public DefaultHttpRequestsObservationConvention() {
57+
this(DEFAULT_NAME);
58+
}
59+
60+
/**
61+
* Create a convention with a custom name.
62+
* @param name the observation name
63+
*/
64+
public DefaultHttpRequestsObservationConvention(String name) {
65+
this.name = name;
66+
}
67+
68+
@Override
69+
public String getName() {
70+
return this.name;
71+
}
72+
73+
@Override
74+
public KeyValues getLowCardinalityKeyValues(HttpRequestsObservationContext context) {
75+
return KeyValues.of(method(context), uri(context), status(context), exception(context), outcome(context));
76+
}
77+
78+
@Override
79+
public KeyValues getHighCardinalityKeyValues(HttpRequestsObservationContext context) {
80+
return KeyValues.of(uriExpanded(context));
81+
}
82+
83+
protected KeyValue method(HttpRequestsObservationContext context) {
84+
return (context.getCarrier() != null) ? KeyValue.of(HttpRequestsObservation.LowCardinalityKeyNames.METHOD, context.getCarrier().getMethod()) : METHOD_UNKNOWN;
85+
}
86+
87+
protected KeyValue status(HttpRequestsObservationContext context) {
88+
return (context.getResponse() != null) ? KeyValue.of(HttpRequestsObservation.LowCardinalityKeyNames.STATUS, Integer.toString(context.getResponse().getStatus())) : STATUS_UNKNOWN;
89+
}
90+
91+
protected KeyValue uri(HttpRequestsObservationContext context) {
92+
if (context.getCarrier() != null) {
93+
String pattern = context.getPathPattern();
94+
if (pattern != null) {
95+
if (pattern.isEmpty()) {
96+
return URI_ROOT;
97+
}
98+
return KeyValue.of("uri", pattern);
99+
}
100+
if (context.getResponse() != null) {
101+
HttpStatus status = HttpStatus.resolve(context.getResponse().getStatus());
102+
if (status != null) {
103+
if (status.is3xxRedirection()) {
104+
return URI_REDIRECTION;
105+
}
106+
if (status == HttpStatus.NOT_FOUND) {
107+
return URI_NOT_FOUND;
108+
}
109+
}
110+
}
111+
}
112+
return URI_UNKNOWN;
113+
}
114+
115+
protected KeyValue exception(HttpRequestsObservationContext context) {
116+
return context.getError().map(throwable ->
117+
KeyValue.of(HttpRequestsObservation.LowCardinalityKeyNames.EXCEPTION, throwable.getClass().getSimpleName()))
118+
.orElse(EXCEPTION_NONE);
119+
}
120+
121+
protected KeyValue outcome(HttpRequestsObservationContext context) {
122+
if (context.getResponse() != null) {
123+
HttpStatus status = HttpStatus.resolve(context.getResponse().getStatus());
124+
if (status != null) {
125+
return KeyValue.of(HttpRequestsObservation.LowCardinalityKeyNames.OUTCOME, status.series().name());
126+
}
127+
}
128+
return OUTCOME_UNKNOWN;
129+
}
130+
131+
protected KeyValue uriExpanded(HttpRequestsObservationContext context) {
132+
if (context.getCarrier() != null) {
133+
String uriExpanded = (context.getCarrier().getPathInfo() != null) ? context.getCarrier().getPathInfo() : "/";
134+
return KeyValue.of(HttpRequestsObservation.HighCardinalityKeyNames.URI_EXPANDED, uriExpanded);
135+
}
136+
return URI_EXPANDED_UNKNOWN;
137+
}
138+
139+
}
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;
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,51 @@
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;
18+
19+
import io.micrometer.observation.transport.RequestReplyReceiverContext;
20+
import jakarta.servlet.http.HttpServletRequest;
21+
import jakarta.servlet.http.HttpServletResponse;
22+
23+
import org.springframework.lang.Nullable;
24+
25+
/**
26+
* Context that holds information for metadata collection during observations for Servlet web application.
27+
* <p>This context also extends {@link RequestReplyReceiverContext} for propagating
28+
* tracing information with the HTTP server exchange.
29+
* @author Brian Clozel
30+
* @since 6.0
31+
*/
32+
public class HttpRequestsObservationContext extends RequestReplyReceiverContext<HttpServletRequest, HttpServletResponse> {
33+
34+
@Nullable
35+
private String pathPattern;
36+
37+
public HttpRequestsObservationContext(HttpServletRequest request, HttpServletResponse response) {
38+
super(HttpServletRequest::getHeader);
39+
this.setCarrier(request);
40+
this.setResponse(response);
41+
}
42+
43+
@Nullable
44+
public String getPathPattern() {
45+
return this.pathPattern;
46+
}
47+
48+
public void setPathPattern(@Nullable String pathPattern) {
49+
this.pathPattern = pathPattern;
50+
}
51+
}
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;
18+
19+
import io.micrometer.observation.Observation;
20+
import io.micrometer.observation.ObservationConvention;
21+
22+
/**
23+
* Interface for an {@link ObservationConvention} related to Servlet 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)