Skip to content

Commit c4ec549

Browse files
committed
OTel semantic conventions for HTTP server for Servlet-based instrumentation
Adds an ObservationDocumentation and ObservationConvention implementation that follows the OpenTelemetry semantic convention for HTTP Server metrics/spans.
1 parent d661550 commit c4ec549

File tree

2 files changed

+388
-0
lines changed

2 files changed

+388
-0
lines changed
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
/*
2+
* Copyright 2002-present 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.http.server.observation;
18+
19+
import io.micrometer.common.KeyValue;
20+
import io.micrometer.common.docs.KeyName;
21+
import io.micrometer.observation.Observation;
22+
import io.micrometer.observation.ObservationConvention;
23+
import io.micrometer.observation.docs.ObservationDocumentation;
24+
25+
/**
26+
* Documented {@link KeyValue KeyValues} for the HTTP server
27+
* observations for Servlet-based web applications, following the stable OpenTelemetry semantic conventions.
28+
*
29+
* <p>This class is used by automated tools to document KeyValues attached to the
30+
* HTTP server observations.
31+
*
32+
* @author Brian Clozel
33+
* @author Tommy Ludwig
34+
* @since 7.0
35+
* @see <a href="https://github.com/open-telemetry/semantic-conventions/blob/v1.36.0/docs/http/http-metrics.md">OpenTelemetry Semantic Conventions for HTTP Metrics (v1.36.0)</a>
36+
* @see <a href="https://github.com/open-telemetry/semantic-conventions/blob/v1.36.0/docs/http/http-spans.md">OpenTelemetry Semantic Conventions for HTTP Spans (v1.36.0)</a>
37+
*/
38+
public enum OpenTelemetryServerHttpObservationDocumentation implements ObservationDocumentation {
39+
40+
/**
41+
* HTTP request observations for Servlet-based servers.
42+
*/
43+
HTTP_SERVLET_SERVER_REQUESTS {
44+
@Override
45+
public Class<? extends ObservationConvention<? extends Observation.Context>> getDefaultConvention() {
46+
return OpenTelemetryServerRequestObservationConvention.class;
47+
}
48+
49+
@Override
50+
public KeyName[] getLowCardinalityKeyNames() {
51+
return LowCardinalityKeyNames.values();
52+
}
53+
54+
@Override
55+
public KeyName[] getHighCardinalityKeyNames() {
56+
return HighCardinalityKeyNames.values();
57+
}
58+
59+
};
60+
61+
public enum LowCardinalityKeyNames implements KeyName {
62+
63+
/**
64+
* Name of the HTTP request method or {@value KeyValue#NONE_VALUE} if the
65+
* request was not received properly. Normalized to known methods defined in internet standards.
66+
*/
67+
METHOD {
68+
@Override
69+
public String asString() {
70+
return "http.request.method";
71+
}
72+
73+
},
74+
75+
/**
76+
* HTTP response raw status code, or {@code "UNKNOWN"} if no response was
77+
* created.
78+
*/
79+
STATUS {
80+
@Override
81+
public String asString() {
82+
return "http.response.status_code";
83+
}
84+
},
85+
86+
/**
87+
* URI pattern for the matching handler if available, falling back to
88+
* {@code REDIRECTION} for 3xx responses, {@code NOT_FOUND} for 404
89+
* responses, {@code root} for requests with no path info, and
90+
* {@code UNKNOWN} for all other requests.
91+
*/
92+
ROUTE {
93+
@Override
94+
public String asString() {
95+
return "http.route";
96+
}
97+
},
98+
99+
/**
100+
* Name of the exception thrown during the exchange, or
101+
* {@value KeyValue#NONE_VALUE} if no exception was thrown.
102+
*/
103+
EXCEPTION {
104+
@Override
105+
public String asString() {
106+
return "error.type";
107+
}
108+
},
109+
110+
/**
111+
* The scheme of the original client request, if known (e.g. from Forwarded#proto, X-Forwarded-Proto, or a similar header). Otherwise, the scheme of the immediate peer request.
112+
*/
113+
SCHEME {
114+
@Override
115+
public String asString() {
116+
return "url.scheme";
117+
}
118+
},
119+
120+
/**
121+
* Outcome of the HTTP server exchange.
122+
* @see org.springframework.http.HttpStatus.Series
123+
*/
124+
OUTCOME {
125+
@Override
126+
public String asString() {
127+
return "outcome";
128+
}
129+
}
130+
}
131+
132+
public enum HighCardinalityKeyNames implements KeyName {
133+
134+
/**
135+
* HTTP request URL.
136+
*/
137+
URL_PATH {
138+
@Override
139+
public String asString() {
140+
return "url.path";
141+
}
142+
},
143+
144+
/**
145+
* Original HTTP method sent by the client in the request line.
146+
*/
147+
METHOD_ORIGINAL {
148+
@Override
149+
public String asString() {
150+
return "http.request.method_original";
151+
}
152+
}
153+
154+
}
155+
156+
}
Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
/*
2+
* Copyright 2002-present 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.http.server.observation;
18+
19+
import java.util.Set;
20+
import java.util.stream.Collectors;
21+
import java.util.stream.Stream;
22+
23+
import io.micrometer.common.KeyValue;
24+
import io.micrometer.common.KeyValues;
25+
import org.jspecify.annotations.Nullable;
26+
27+
import org.springframework.http.HttpMethod;
28+
import org.springframework.http.HttpStatus;
29+
import org.springframework.http.HttpStatusCode;
30+
import org.springframework.http.server.observation.OpenTelemetryServerHttpObservationDocumentation.HighCardinalityKeyNames;
31+
import org.springframework.http.server.observation.OpenTelemetryServerHttpObservationDocumentation.LowCardinalityKeyNames;
32+
import org.springframework.util.StringUtils;
33+
34+
/**
35+
* A {@link ServerRequestObservationConvention} based on the stable OpenTelemetry semantic conventions.
36+
*
37+
* @author Brian Clozel
38+
* @author Tommy Ludwig
39+
* @since 7.0
40+
* @see OpenTelemetryServerHttpObservationDocumentation
41+
*/
42+
public class OpenTelemetryServerRequestObservationConvention implements ServerRequestObservationConvention {
43+
44+
private static final String NAME = "http.server.request.duration";
45+
46+
private static final KeyValue METHOD_UNKNOWN = KeyValue.of(LowCardinalityKeyNames.METHOD, "_OTHER");
47+
48+
private static final KeyValue SCHEME_UNKNOWN = KeyValue.of(LowCardinalityKeyNames.SCHEME, "UNKNOWN");
49+
50+
private static final KeyValue STATUS_UNKNOWN = KeyValue.of(LowCardinalityKeyNames.STATUS, "UNKNOWN");
51+
52+
private static final KeyValue HTTP_OUTCOME_SUCCESS = KeyValue.of(LowCardinalityKeyNames.OUTCOME, "SUCCESS");
53+
54+
private static final KeyValue HTTP_OUTCOME_UNKNOWN = KeyValue.of(LowCardinalityKeyNames.OUTCOME, "UNKNOWN");
55+
56+
private static final KeyValue ROUTE_UNKNOWN = KeyValue.of(LowCardinalityKeyNames.ROUTE, "UNKNOWN");
57+
58+
private static final KeyValue ROUTE_ROOT = KeyValue.of(LowCardinalityKeyNames.ROUTE, "root");
59+
60+
private static final KeyValue ROUTE_NOT_FOUND = KeyValue.of(LowCardinalityKeyNames.ROUTE, "NOT_FOUND");
61+
62+
private static final KeyValue ROUTE_REDIRECTION = KeyValue.of(LowCardinalityKeyNames.ROUTE, "REDIRECTION");
63+
64+
private static final KeyValue EXCEPTION_NONE = KeyValue.of(LowCardinalityKeyNames.EXCEPTION, KeyValue.NONE_VALUE);
65+
66+
private static final KeyValue HTTP_URL_UNKNOWN = KeyValue.of(HighCardinalityKeyNames.URL_PATH, "UNKNOWN");
67+
68+
private static final KeyValue ORIGINAL_METHOD_UNKNOWN = KeyValue.of(HighCardinalityKeyNames.METHOD_ORIGINAL, "UNKNOWN");
69+
70+
private static final Set<String> HTTP_METHODS = Stream.of(HttpMethod.values()).map(HttpMethod::name).collect(Collectors.toUnmodifiableSet());
71+
72+
73+
/**
74+
* Create a convention.
75+
*/
76+
public OpenTelemetryServerRequestObservationConvention() {
77+
}
78+
79+
80+
@Override
81+
public String getName() {
82+
return NAME;
83+
}
84+
85+
/**
86+
* HTTP span names SHOULD be {@code {method} {target}} if there is a (low-cardinality) {@code target}
87+
* available. If there is no (low-cardinality) {@code {target}} available, HTTP span names
88+
* SHOULD be {@code {method}}.
89+
* <p>
90+
* The {@code {method}} MUST be {@code {http.request.method}} if the method represents the original
91+
* method known to the instrumentation. In other cases (when Customize Toolbar… is
92+
* set to {@code _OTHER}), {@code {method}} MUST be HTTP.
93+
* <p>
94+
* The {@code target} SHOULD be the {@code {http.route}}.
95+
* @param context context
96+
* @return contextual name
97+
* @see <a href="https://github.com/open-telemetry/semantic-conventions/blob/v1.36.0/docs/http/http-spans.md#name">OpenTelemetry Semantic Convention HTTP Span Name (v1.36.0)</a>
98+
*/
99+
@Override
100+
public String getContextualName(ServerRequestObservationContext context) {
101+
if (context.getCarrier() == null) {
102+
return "HTTP";
103+
}
104+
String maybeMethod = getMethodValue(context);
105+
String method = maybeMethod == null ? "HTTP" : maybeMethod;
106+
String target = context.getPathPattern();
107+
if (target != null) {
108+
return method + " " + target;
109+
}
110+
return method;
111+
}
112+
113+
@Override
114+
public KeyValues getLowCardinalityKeyValues(ServerRequestObservationContext context) {
115+
// Make sure that KeyValues entries are already sorted by name for better performance
116+
return KeyValues.of(exception(context), method(context), status(context), pathTemplate(context), outcome(context), scheme(context));
117+
}
118+
119+
@Override
120+
public KeyValues getHighCardinalityKeyValues(ServerRequestObservationContext context) {
121+
// Make sure that KeyValues entries are already sorted by name for better performance
122+
return KeyValues.of(methodOriginal(context), httpUrl(context));
123+
}
124+
125+
protected KeyValue method(ServerRequestObservationContext context) {
126+
String method = getMethodValue(context);
127+
if (method != null) {
128+
return KeyValue.of(LowCardinalityKeyNames.METHOD, method);
129+
}
130+
return METHOD_UNKNOWN;
131+
}
132+
133+
protected @Nullable String getMethodValue(ServerRequestObservationContext context) {
134+
if (context.getCarrier() != null) {
135+
String httpMethod = context.getCarrier().getMethod();
136+
if (HTTP_METHODS.contains(httpMethod)) {
137+
return httpMethod;
138+
}
139+
}
140+
return null;
141+
}
142+
143+
protected KeyValue scheme(ServerRequestObservationContext context) {
144+
if (context.getCarrier() != null) {
145+
return KeyValue.of(LowCardinalityKeyNames.SCHEME, context.getCarrier().getScheme());
146+
}
147+
return SCHEME_UNKNOWN;
148+
}
149+
150+
protected KeyValue status(ServerRequestObservationContext context) {
151+
return (context.getResponse() != null) ?
152+
KeyValue.of(LowCardinalityKeyNames.STATUS, Integer.toString(context.getResponse().getStatus())) :
153+
STATUS_UNKNOWN;
154+
}
155+
156+
protected KeyValue pathTemplate(ServerRequestObservationContext context) {
157+
if (context.getCarrier() != null) {
158+
String pattern = context.getPathPattern();
159+
if (pattern != null) {
160+
if (pattern.isEmpty()) {
161+
return ROUTE_ROOT;
162+
}
163+
return KeyValue.of(LowCardinalityKeyNames.ROUTE, pattern);
164+
}
165+
if (context.getResponse() != null) {
166+
HttpStatus status = HttpStatus.resolve(context.getResponse().getStatus());
167+
if (status != null) {
168+
if (status.is3xxRedirection()) {
169+
return ROUTE_REDIRECTION;
170+
}
171+
if (status == HttpStatus.NOT_FOUND) {
172+
return ROUTE_NOT_FOUND;
173+
}
174+
}
175+
}
176+
}
177+
return ROUTE_UNKNOWN;
178+
}
179+
180+
protected KeyValue exception(ServerRequestObservationContext context) {
181+
Throwable error = context.getError();
182+
if (error != null) {
183+
String simpleName = error.getClass().getSimpleName();
184+
return KeyValue.of(LowCardinalityKeyNames.EXCEPTION,
185+
StringUtils.hasText(simpleName) ? simpleName : error.getClass().getName());
186+
}
187+
return EXCEPTION_NONE;
188+
}
189+
190+
protected KeyValue outcome(ServerRequestObservationContext context) {
191+
try {
192+
if (context.getResponse() != null) {
193+
HttpStatusCode statusCode = HttpStatusCode.valueOf(context.getResponse().getStatus());
194+
return HttpOutcome.forStatus(statusCode);
195+
}
196+
}
197+
catch (IllegalArgumentException ex) {
198+
return HTTP_OUTCOME_UNKNOWN;
199+
}
200+
return HTTP_OUTCOME_UNKNOWN;
201+
}
202+
203+
protected KeyValue httpUrl(ServerRequestObservationContext context) {
204+
if (context.getCarrier() != null) {
205+
return KeyValue.of(HighCardinalityKeyNames.URL_PATH, context.getCarrier().getRequestURI());
206+
}
207+
return HTTP_URL_UNKNOWN;
208+
}
209+
210+
protected KeyValue methodOriginal(ServerRequestObservationContext context) {
211+
if (context.getCarrier() != null) {
212+
return KeyValue.of(HighCardinalityKeyNames.METHOD_ORIGINAL, context.getCarrier().getMethod());
213+
}
214+
return ORIGINAL_METHOD_UNKNOWN;
215+
}
216+
217+
static class HttpOutcome {
218+
219+
static KeyValue forStatus(HttpStatusCode statusCode) {
220+
if (statusCode.is2xxSuccessful()) {
221+
return HTTP_OUTCOME_SUCCESS;
222+
}
223+
else if (statusCode instanceof HttpStatus status) {
224+
return KeyValue.of(LowCardinalityKeyNames.OUTCOME, status.series().name());
225+
}
226+
else {
227+
return HTTP_OUTCOME_UNKNOWN;
228+
}
229+
}
230+
}
231+
232+
}

0 commit comments

Comments
 (0)