Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
/*
* Copyright 2002-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.springframework.http.server.observation;

import io.micrometer.common.KeyValue;
import io.micrometer.common.docs.KeyName;
import io.micrometer.observation.Observation;
import io.micrometer.observation.ObservationConvention;
import io.micrometer.observation.docs.ObservationDocumentation;

/**
* Documented {@link KeyValue KeyValues} for the HTTP server
* observations for Servlet-based web applications, following the stable OpenTelemetry semantic conventions.
*
* <p>This class is used by automated tools to document KeyValues attached to the
* HTTP server observations.
*
* @author Brian Clozel
* @author Tommy Ludwig
* @since 7.0
* @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>
* @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>
*/
public enum OpenTelemetryServerHttpObservationDocumentation implements ObservationDocumentation {

/**
* HTTP request observations for Servlet-based servers.
*/
HTTP_SERVLET_SERVER_REQUESTS {
@Override
public Class<? extends ObservationConvention<? extends Observation.Context>> getDefaultConvention() {
return OpenTelemetryServerRequestObservationConvention.class;
}

@Override
public KeyName[] getLowCardinalityKeyNames() {
return LowCardinalityKeyNames.values();
}

@Override
public KeyName[] getHighCardinalityKeyNames() {
return HighCardinalityKeyNames.values();
}

};

public enum LowCardinalityKeyNames implements KeyName {

/**
* Name of the HTTP request method or {@value KeyValue#NONE_VALUE} if the
* request was not received properly. Normalized to known methods defined in internet standards.
*/
METHOD {
@Override
public String asString() {
return "http.request.method";
}

},

/**
* HTTP response raw status code, or {@code "UNKNOWN"} if no response was
* created.
*/
STATUS {
@Override
public String asString() {
return "http.response.status_code";
}
},

/**
* URI pattern for the matching handler if available, falling back to
* {@code REDIRECTION} for 3xx responses, {@code NOT_FOUND} for 404
* responses, {@code root} for requests with no path info, and
* {@code UNKNOWN} for all other requests.
*/
ROUTE {
@Override
public String asString() {
return "http.route";
}
},

/**
* Fully qualified name of the exception thrown during the exchange, or
* {@value KeyValue#NONE_VALUE} if no exception was thrown.
*/
EXCEPTION {
@Override
public String asString() {
return "error.type";
}
},

/**
* 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.
*/
SCHEME {
@Override
public String asString() {
return "url.scheme";
}
},

/**
* Outcome of the HTTP server exchange.
* @see org.springframework.http.HttpStatus.Series
*/
OUTCOME {
@Override
public String asString() {
return "outcome";
}
}
}

public enum HighCardinalityKeyNames implements KeyName {

/**
* HTTP request URL.
*/
URL_PATH {
@Override
public String asString() {
return "url.path";
}
},

/**
* Original HTTP method sent by the client in the request line.
*/
METHOD_ORIGINAL {
@Override
public String asString() {
return "http.request.method_original";
}
}

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
/*
* Copyright 2002-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.springframework.http.server.observation;

import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import io.micrometer.common.KeyValue;
import io.micrometer.common.KeyValues;
import org.jspecify.annotations.Nullable;

import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.server.observation.OpenTelemetryServerHttpObservationDocumentation.HighCardinalityKeyNames;
import org.springframework.http.server.observation.OpenTelemetryServerHttpObservationDocumentation.LowCardinalityKeyNames;

/**
* A {@link ServerRequestObservationConvention} based on the stable OpenTelemetry semantic conventions.
*
* @author Brian Clozel
* @author Tommy Ludwig
* @since 7.0
* @see OpenTelemetryServerHttpObservationDocumentation
*/
public class OpenTelemetryServerRequestObservationConvention implements ServerRequestObservationConvention {

private static final String NAME = "http.server.request.duration";

private static final KeyValue METHOD_UNKNOWN = KeyValue.of(LowCardinalityKeyNames.METHOD, "_OTHER");

private static final KeyValue SCHEME_UNKNOWN = KeyValue.of(LowCardinalityKeyNames.SCHEME, "UNKNOWN");

private static final KeyValue STATUS_UNKNOWN = KeyValue.of(LowCardinalityKeyNames.STATUS, "UNKNOWN");

private static final KeyValue HTTP_OUTCOME_SUCCESS = KeyValue.of(LowCardinalityKeyNames.OUTCOME, "SUCCESS");

private static final KeyValue HTTP_OUTCOME_UNKNOWN = KeyValue.of(LowCardinalityKeyNames.OUTCOME, "UNKNOWN");

private static final KeyValue ROUTE_UNKNOWN = KeyValue.of(LowCardinalityKeyNames.ROUTE, "UNKNOWN");

private static final KeyValue ROUTE_ROOT = KeyValue.of(LowCardinalityKeyNames.ROUTE, "root");

private static final KeyValue ROUTE_NOT_FOUND = KeyValue.of(LowCardinalityKeyNames.ROUTE, "NOT_FOUND");

private static final KeyValue ROUTE_REDIRECTION = KeyValue.of(LowCardinalityKeyNames.ROUTE, "REDIRECTION");

private static final KeyValue EXCEPTION_NONE = KeyValue.of(LowCardinalityKeyNames.EXCEPTION, KeyValue.NONE_VALUE);

private static final KeyValue HTTP_URL_UNKNOWN = KeyValue.of(HighCardinalityKeyNames.URL_PATH, "UNKNOWN");

private static final KeyValue ORIGINAL_METHOD_UNKNOWN = KeyValue.of(HighCardinalityKeyNames.METHOD_ORIGINAL, "UNKNOWN");

private static final Set<String> HTTP_METHODS = Stream.of(HttpMethod.values()).map(HttpMethod::name).collect(Collectors.toUnmodifiableSet());


/**
* Create a convention.
*/
public OpenTelemetryServerRequestObservationConvention() {
}


@Override
public String getName() {
return NAME;
}

/**
* HTTP span names SHOULD be {@code {method} {target}} if there is a (low-cardinality) {@code target}
* available. If there is no (low-cardinality) {@code {target}} available, HTTP span names
* SHOULD be {@code {method}}.
* <p>
* The {@code {method}} MUST be {@code {http.request.method}} if the method represents the original
* method known to the instrumentation. In other cases (when Customize Toolbar… is
* set to {@code _OTHER}), {@code {method}} MUST be HTTP.
* <p>
* The {@code target} SHOULD be the {@code {http.route}}.
* @param context context
* @return contextual name
* @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>
*/
@Override
public String getContextualName(ServerRequestObservationContext context) {
if (context.getCarrier() == null) {
return "HTTP";
}
String maybeMethod = getMethodValue(context);
String method = maybeMethod == null ? "HTTP" : maybeMethod;
String target = context.getPathPattern();
if (target != null) {
return method + " " + target;
}
return method;
}

@Override
public KeyValues getLowCardinalityKeyValues(ServerRequestObservationContext context) {
// Make sure that KeyValues entries are already sorted by name for better performance
return KeyValues.of(exception(context), method(context), status(context), pathTemplate(context), outcome(context), scheme(context));
}

@Override
public KeyValues getHighCardinalityKeyValues(ServerRequestObservationContext context) {
// Make sure that KeyValues entries are already sorted by name for better performance
return KeyValues.of(methodOriginal(context), httpUrl(context));
}

protected KeyValue method(ServerRequestObservationContext context) {
String method = getMethodValue(context);
if (method != null) {
return KeyValue.of(LowCardinalityKeyNames.METHOD, method);
}
return METHOD_UNKNOWN;
}

protected @Nullable String getMethodValue(ServerRequestObservationContext context) {
if (context.getCarrier() != null) {
String httpMethod = context.getCarrier().getMethod();
if (HTTP_METHODS.contains(httpMethod)) {
return httpMethod;
}
}
return null;
}

protected KeyValue scheme(ServerRequestObservationContext context) {
if (context.getCarrier() != null) {
return KeyValue.of(LowCardinalityKeyNames.SCHEME, context.getCarrier().getScheme());
}
return SCHEME_UNKNOWN;
}

protected KeyValue status(ServerRequestObservationContext context) {
return (context.getResponse() != null) ?
KeyValue.of(LowCardinalityKeyNames.STATUS, Integer.toString(context.getResponse().getStatus())) :
STATUS_UNKNOWN;
}

protected KeyValue pathTemplate(ServerRequestObservationContext context) {
if (context.getCarrier() != null) {
String pattern = context.getPathPattern();
if (pattern != null) {
if (pattern.isEmpty()) {
return ROUTE_ROOT;
}
return KeyValue.of(LowCardinalityKeyNames.ROUTE, pattern);
}
if (context.getResponse() != null) {
HttpStatus status = HttpStatus.resolve(context.getResponse().getStatus());
if (status != null) {
if (status.is3xxRedirection()) {
return ROUTE_REDIRECTION;
}
if (status == HttpStatus.NOT_FOUND) {
return ROUTE_NOT_FOUND;
}
}
}
}
return ROUTE_UNKNOWN;
}

protected KeyValue exception(ServerRequestObservationContext context) {
Throwable error = context.getError();
if (error != null) {
return KeyValue.of(LowCardinalityKeyNames.EXCEPTION, error.getClass().getName());
}
return EXCEPTION_NONE;
}

protected KeyValue outcome(ServerRequestObservationContext context) {
try {
if (context.getResponse() != null) {
HttpStatusCode statusCode = HttpStatusCode.valueOf(context.getResponse().getStatus());
return HttpOutcome.forStatus(statusCode);
}
}
catch (IllegalArgumentException ex) {
return HTTP_OUTCOME_UNKNOWN;
}
return HTTP_OUTCOME_UNKNOWN;
}

protected KeyValue httpUrl(ServerRequestObservationContext context) {
if (context.getCarrier() != null) {
return KeyValue.of(HighCardinalityKeyNames.URL_PATH, context.getCarrier().getRequestURI());
}
return HTTP_URL_UNKNOWN;
}

protected KeyValue methodOriginal(ServerRequestObservationContext context) {
if (context.getCarrier() != null) {
return KeyValue.of(HighCardinalityKeyNames.METHOD_ORIGINAL, context.getCarrier().getMethod());
}
return ORIGINAL_METHOD_UNKNOWN;
}

static class HttpOutcome {

static KeyValue forStatus(HttpStatusCode statusCode) {
if (statusCode.is2xxSuccessful()) {
return HTTP_OUTCOME_SUCCESS;
}
else if (statusCode instanceof HttpStatus status) {
return KeyValue.of(LowCardinalityKeyNames.OUTCOME, status.series().name());
}
else {
return HTTP_OUTCOME_UNKNOWN;
}
}
}

}
Loading