diff --git a/ext/micrometer/pom.xml b/ext/micrometer/pom.xml new file mode 100644 index 00000000000..5e19969a978 --- /dev/null +++ b/ext/micrometer/pom.xml @@ -0,0 +1,104 @@ + + + + + + project + org.glassfish.jersey.ext + 2.41-SNAPSHOT + + 4.0.0 + + jersey-micrometer + + + + + io.micrometer + micrometer-core + ${micrometer.version} + + + + org.glassfish.jersey.core + jersey-common + ${project.version} + + + + org.glassfish.jersey.core + jersey-server + ${project.version} + + + + org.glassfish.jersey.test-framework.providers + jersey-test-framework-provider-inmemory + ${project.version} + test + + + + org.glassfish.jersey.test-framework + jersey-test-framework-core + ${project.version} + test + + + + org.aspectj + aspectjweaver + 1.6.11 + test + + + + io.micrometer + micrometer-tracing-integration-test + ${micrometer-tracing.version} + test + + + + + + + org.apache.felix + maven-bundle-plugin + true + true + + + + org.glassfish.jersey.micrometer.server.*;version=${project.version} + + + org.eclipse.microprofile.micrometer.server.*;version="!", + * + + + true + + + + + + diff --git a/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/AnnotationFinder.java b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/AnnotationFinder.java new file mode 100644 index 00000000000..bdfa49c123a --- /dev/null +++ b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/AnnotationFinder.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2019, 2023 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ +package org.glassfish.jersey.micrometer.server; + +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; + +public interface AnnotationFinder { + + AnnotationFinder DEFAULT = new AnnotationFinder() { + }; + + /** + * The default implementation performs a simple search for a declared annotation + * matching the search type. Spring provides a more sophisticated annotation search + * utility that matches on meta-annotations as well. + * @param annotatedElement The element to search. + * @param annotationType The annotation type class. + * @param Annotation type to search for. + * @return A matching annotation. + */ + @SuppressWarnings("unchecked") + default A findAnnotation(AnnotatedElement annotatedElement, Class annotationType) { + Annotation[] anns = annotatedElement.getDeclaredAnnotations(); + for (Annotation ann : anns) { + if (ann.annotationType() == annotationType) { + return (A) ann; + } + } + return null; + } + +} diff --git a/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/DefaultJerseyObservationConvention.java b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/DefaultJerseyObservationConvention.java new file mode 100644 index 00000000000..b1eb4f9b901 --- /dev/null +++ b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/DefaultJerseyObservationConvention.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2019, 2023 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ +package org.glassfish.jersey.micrometer.server; + +import io.micrometer.common.KeyValues; +import io.micrometer.common.lang.Nullable; +import org.glassfish.jersey.server.ContainerRequest; +import org.glassfish.jersey.server.ContainerResponse; +import org.glassfish.jersey.server.monitoring.RequestEvent; + +/** + * Default implementation for {@link JerseyObservationConvention}. + * + * @author Marcin Grzejszczak + * @since 2.41 + */ +public class DefaultJerseyObservationConvention implements JerseyObservationConvention { + + private final String metricsName; + + public DefaultJerseyObservationConvention(String metricsName) { + this.metricsName = metricsName; + } + + @Override + public KeyValues getLowCardinalityKeyValues(JerseyContext context) { + RequestEvent event = context.getRequestEvent(); + ContainerRequest request = context.getCarrier(); + ContainerResponse response = context.getResponse(); + return KeyValues.of(JerseyKeyValues.method(request), JerseyKeyValues.uri(event), + JerseyKeyValues.exception(event), JerseyKeyValues.status(response), JerseyKeyValues.outcome(response)); + } + + @Override + public String getName() { + return this.metricsName; + } + + @Nullable + @Override + public String getContextualName(JerseyContext context) { + if (context.getCarrier() == null) { + return null; + } + return "HTTP " + context.getCarrier().getMethod(); + } + +} diff --git a/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/DefaultJerseyTagsProvider.java b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/DefaultJerseyTagsProvider.java new file mode 100644 index 00000000000..3ce14198090 --- /dev/null +++ b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/DefaultJerseyTagsProvider.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2019, 2023 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ +package org.glassfish.jersey.micrometer.server; + +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.Tags; +import org.glassfish.jersey.server.ContainerResponse; +import org.glassfish.jersey.server.monitoring.RequestEvent; + +/** + * Default implementation for {@link JerseyTagsProvider}. + * + * @author Michael Weirauch + * @author Johnny Lim + * @since 2.41 + */ +public final class DefaultJerseyTagsProvider implements JerseyTagsProvider { + + @Override + public Iterable httpRequestTags(RequestEvent event) { + ContainerResponse response = event.getContainerResponse(); + return Tags.of(JerseyTags.method(event.getContainerRequest()), JerseyTags.uri(event), + JerseyTags.exception(event), JerseyTags.status(response), JerseyTags.outcome(response)); + } + + @Override + public Iterable httpLongRequestTags(RequestEvent event) { + return Tags.of(JerseyTags.method(event.getContainerRequest()), JerseyTags.uri(event)); + } + +} diff --git a/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/JerseyContext.java b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/JerseyContext.java new file mode 100644 index 00000000000..14354eb21e9 --- /dev/null +++ b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/JerseyContext.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2019, 2023 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ +package org.glassfish.jersey.micrometer.server; + +import java.util.List; + +import io.micrometer.observation.transport.ReceiverContext; +import io.micrometer.observation.transport.RequestReplyReceiverContext; +import org.glassfish.jersey.server.ContainerRequest; +import org.glassfish.jersey.server.ContainerResponse; +import org.glassfish.jersey.server.monitoring.RequestEvent; + +/** + * A {@link ReceiverContext} for Jersey. + * + * @author Marcin Grzejszczak + * @since 2.41 + */ +public class JerseyContext extends RequestReplyReceiverContext { + + private RequestEvent requestEvent; + + public JerseyContext(RequestEvent requestEvent) { + super((carrier, key) -> { + List requestHeader = carrier.getRequestHeader(key); + if (requestHeader == null || requestHeader.isEmpty()) { + return null; + } + return requestHeader.get(0); + }); + this.requestEvent = requestEvent; + setCarrier(requestEvent.getContainerRequest()); + } + + public void setRequestEvent(RequestEvent requestEvent) { + this.requestEvent = requestEvent; + } + + public RequestEvent getRequestEvent() { + return requestEvent; + } + +} diff --git a/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/JerseyKeyValues.java b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/JerseyKeyValues.java new file mode 100644 index 00000000000..e035f78c522 --- /dev/null +++ b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/JerseyKeyValues.java @@ -0,0 +1,144 @@ +/* + * Copyright (c) 2019, 2023 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ +package org.glassfish.jersey.micrometer.server; + +import io.micrometer.common.KeyValue; +import io.micrometer.common.util.StringUtils; +import io.micrometer.core.instrument.binder.http.Outcome; +import org.glassfish.jersey.server.ContainerRequest; +import org.glassfish.jersey.server.ContainerResponse; +import org.glassfish.jersey.server.ExtendedUriInfo; +import org.glassfish.jersey.server.monitoring.RequestEvent; + +/** + * Factory methods for {@link KeyValue KeyValues} associated with a request-response + * exchange that is handled by Jersey server. + */ +class JerseyKeyValues { + + private static final KeyValue URI_NOT_FOUND = JerseyObservationDocumentation.JerseyLegacyLowCardinalityTags.URI + .withValue("NOT_FOUND"); + + private static final KeyValue URI_REDIRECTION = JerseyObservationDocumentation.JerseyLegacyLowCardinalityTags.URI + .withValue("REDIRECTION"); + + private static final KeyValue URI_ROOT = JerseyObservationDocumentation.JerseyLegacyLowCardinalityTags.URI + .withValue("root"); + + private static final KeyValue EXCEPTION_NONE = JerseyObservationDocumentation.JerseyLegacyLowCardinalityTags.EXCEPTION + .withValue("None"); + + private static final KeyValue STATUS_SERVER_ERROR = JerseyObservationDocumentation.JerseyLegacyLowCardinalityTags.STATUS + .withValue("500"); + + private static final KeyValue METHOD_UNKNOWN = JerseyObservationDocumentation.JerseyLegacyLowCardinalityTags.METHOD + .withValue("UNKNOWN"); + + private JerseyKeyValues() { + } + + /** + * Creates a {@code method} KeyValue based on the {@link ContainerRequest#getMethod() + * method} of the given {@code request}. + * @param request the container request + * @return the method KeyValue whose value is a capitalized method (e.g. GET). + */ + static KeyValue method(ContainerRequest request) { + return (request != null) + ? JerseyObservationDocumentation.JerseyLegacyLowCardinalityTags.METHOD.withValue(request.getMethod()) + : METHOD_UNKNOWN; + } + + /** + * Creates a {@code status} KeyValue based on the status of the given + * {@code response}. + * @param response the container response + * @return the status KeyValue derived from the status of the response + */ + static KeyValue status(ContainerResponse response) { + /* In case there is no response we are dealing with an unmapped exception. */ + return (response != null) ? JerseyObservationDocumentation.JerseyLegacyLowCardinalityTags.STATUS + .withValue(Integer.toString(response.getStatus())) : STATUS_SERVER_ERROR; + } + + /** + * Creates a {@code uri} KeyValue based on the URI of the given {@code event}. Uses + * the {@link ExtendedUriInfo#getMatchedTemplates()} if available. {@code REDIRECTION} + * for 3xx responses, {@code NOT_FOUND} for 404 responses. + * @param event the request event + * @return the uri KeyValue derived from the request event + */ + static KeyValue uri(RequestEvent event) { + ContainerResponse response = event.getContainerResponse(); + if (response != null) { + int status = response.getStatus(); + if (JerseyTags.isRedirection(status) && event.getUriInfo().getMatchedResourceMethod() == null) { + return URI_REDIRECTION; + } + if (status == 404 && event.getUriInfo().getMatchedResourceMethod() == null) { + return URI_NOT_FOUND; + } + } + String matchingPattern = JerseyTags.getMatchingPattern(event); + if (matchingPattern.equals("/")) { + return URI_ROOT; + } + return JerseyObservationDocumentation.JerseyLegacyLowCardinalityTags.URI.withValue(matchingPattern); + } + + /** + * Creates an {@code exception} KeyValue based on the {@link Class#getSimpleName() + * simple name} of the class of the given {@code exception}. + * @param event the request event + * @return the exception KeyValue derived from the exception + */ + static KeyValue exception(RequestEvent event) { + Throwable exception = event.getException(); + if (exception == null) { + return EXCEPTION_NONE; + } + ContainerResponse response = event.getContainerResponse(); + if (response != null) { + int status = response.getStatus(); + if (status == 404 || JerseyTags.isRedirection(status)) { + return EXCEPTION_NONE; + } + } + if (exception.getCause() != null) { + exception = exception.getCause(); + } + String simpleName = exception.getClass().getSimpleName(); + return JerseyObservationDocumentation.JerseyLegacyLowCardinalityTags.EXCEPTION + .withValue(StringUtils.isNotEmpty(simpleName) ? simpleName : exception.getClass().getName()); + } + + /** + * Creates an {@code outcome} KeyValue based on the status of the given + * {@code response}. + * @param response the container response + * @return the outcome KeyValue derived from the status of the response + */ + static KeyValue outcome(ContainerResponse response) { + if (response != null) { + Outcome outcome = Outcome.forStatus(response.getStatus()); + return JerseyObservationDocumentation.JerseyLegacyLowCardinalityTags.OUTCOME.withValue(outcome.name()); + } + /* In case there is no response we are dealing with an unmapped exception. */ + return JerseyObservationDocumentation.JerseyLegacyLowCardinalityTags.OUTCOME + .withValue(Outcome.SERVER_ERROR.name()); + } + +} diff --git a/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/JerseyObservationConvention.java b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/JerseyObservationConvention.java new file mode 100644 index 00000000000..20b4da200cf --- /dev/null +++ b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/JerseyObservationConvention.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2019, 2023 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ +package org.glassfish.jersey.micrometer.server; + +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationConvention; + +/** + * Provides names and {@link io.micrometer.common.KeyValues} for Jersey request + * observations. + * + * @author Marcin Grzejszczak + * @since 2.41 + */ +public interface JerseyObservationConvention extends ObservationConvention { + + @Override + default boolean supportsContext(Observation.Context context) { + return context instanceof JerseyContext; + } + +} diff --git a/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/JerseyObservationDocumentation.java b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/JerseyObservationDocumentation.java new file mode 100644 index 00000000000..cf049397668 --- /dev/null +++ b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/JerseyObservationDocumentation.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2019, 2023 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ +package org.glassfish.jersey.micrometer.server; + +import io.micrometer.common.docs.KeyName; +import io.micrometer.common.lang.NonNullApi; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationConvention; +import io.micrometer.observation.docs.ObservationDocumentation; + +/** + * An {@link ObservationDocumentation} for Jersey. + * + * @author Marcin Grzejszczak + * @since 2.41 + */ +@NonNullApi +public enum JerseyObservationDocumentation implements ObservationDocumentation { + + /** + * Default observation for Jersey. + */ + DEFAULT { + @Override + public Class> getDefaultConvention() { + return DefaultJerseyObservationConvention.class; + } + + @Override + public KeyName[] getLowCardinalityKeyNames() { + return JerseyLegacyLowCardinalityTags.values(); + } + }; + + @NonNullApi + enum JerseyLegacyLowCardinalityTags implements KeyName { + + OUTCOME { + @Override + public String asString() { + return "outcome"; + } + }, + + METHOD { + @Override + public String asString() { + return "method"; + } + }, + + URI { + @Override + public String asString() { + return "uri"; + } + }, + + EXCEPTION { + @Override + public String asString() { + return "exception"; + } + }, + + STATUS { + @Override + public String asString() { + return "status"; + } + } + + } + +} diff --git a/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/JerseyTags.java b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/JerseyTags.java new file mode 100644 index 00000000000..b415189e0d1 --- /dev/null +++ b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/JerseyTags.java @@ -0,0 +1,162 @@ +/* + * Copyright (c) 2019, 2023 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ +package org.glassfish.jersey.micrometer.server; + +import java.util.List; +import java.util.regex.Pattern; + +import io.micrometer.common.util.StringUtils; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.binder.http.Outcome; +import org.glassfish.jersey.server.ContainerRequest; +import org.glassfish.jersey.server.ContainerResponse; +import org.glassfish.jersey.server.ExtendedUriInfo; +import org.glassfish.jersey.server.monitoring.RequestEvent; +import org.glassfish.jersey.uri.UriTemplate; + +/** + * Factory methods for {@link Tag Tags} associated with a request-response exchange that + * is handled by Jersey server. + * + * @author Michael Weirauch + * @author Johnny Lim + * @since 2.41 + */ +public final class JerseyTags { + + private static final Tag URI_NOT_FOUND = Tag.of("uri", "NOT_FOUND"); + + private static final Tag URI_REDIRECTION = Tag.of("uri", "REDIRECTION"); + + private static final Tag URI_ROOT = Tag.of("uri", "root"); + + private static final Tag EXCEPTION_NONE = Tag.of("exception", "None"); + + private static final Tag STATUS_SERVER_ERROR = Tag.of("status", "500"); + + private static final Tag METHOD_UNKNOWN = Tag.of("method", "UNKNOWN"); + + static final Pattern TRAILING_SLASH_PATTERN = Pattern.compile("/$"); + + static final Pattern MULTIPLE_SLASH_PATTERN = Pattern.compile("//+"); + + private JerseyTags() { + } + + /** + * Creates a {@code method} tag based on the {@link ContainerRequest#getMethod() + * method} of the given {@code request}. + * @param request the container request + * @return the method tag whose value is a capitalized method (e.g. GET). + */ + public static Tag method(ContainerRequest request) { + return (request != null) ? Tag.of("method", request.getMethod()) : METHOD_UNKNOWN; + } + + /** + * Creates a {@code status} tag based on the status of the given {@code response}. + * @param response the container response + * @return the status tag derived from the status of the response + */ + public static Tag status(ContainerResponse response) { + /* In case there is no response we are dealing with an unmapped exception. */ + return (response != null) ? Tag.of("status", Integer.toString(response.getStatus())) : STATUS_SERVER_ERROR; + } + + /** + * Creates a {@code uri} tag based on the URI of the given {@code event}. Uses the + * {@link ExtendedUriInfo#getMatchedTemplates()} if available. {@code REDIRECTION} for + * 3xx responses, {@code NOT_FOUND} for 404 responses. + * @param event the request event + * @return the uri tag derived from the request event + */ + public static Tag uri(RequestEvent event) { + ContainerResponse response = event.getContainerResponse(); + if (response != null) { + int status = response.getStatus(); + if (isRedirection(status) && event.getUriInfo().getMatchedResourceMethod() == null) { + return URI_REDIRECTION; + } + if (status == 404 && event.getUriInfo().getMatchedResourceMethod() == null) { + return URI_NOT_FOUND; + } + } + String matchingPattern = getMatchingPattern(event); + if (matchingPattern.equals("/")) { + return URI_ROOT; + } + return Tag.of("uri", matchingPattern); + } + + static boolean isRedirection(int status) { + return 300 <= status && status < 400; + } + + static String getMatchingPattern(RequestEvent event) { + ExtendedUriInfo uriInfo = event.getUriInfo(); + List templates = uriInfo.getMatchedTemplates(); + + StringBuilder sb = new StringBuilder(); + sb.append(uriInfo.getBaseUri().getPath()); + for (int i = templates.size() - 1; i >= 0; i--) { + sb.append(templates.get(i).getTemplate()); + } + String multipleSlashCleaned = MULTIPLE_SLASH_PATTERN.matcher(sb.toString()).replaceAll("/"); + if (multipleSlashCleaned.equals("/")) { + return multipleSlashCleaned; + } + return TRAILING_SLASH_PATTERN.matcher(multipleSlashCleaned).replaceAll(""); + } + + /** + * Creates an {@code exception} tag based on the {@link Class#getSimpleName() simple + * name} of the class of the given {@code exception}. + * @param event the request event + * @return the exception tag derived from the exception + */ + public static Tag exception(RequestEvent event) { + Throwable exception = event.getException(); + if (exception == null) { + return EXCEPTION_NONE; + } + ContainerResponse response = event.getContainerResponse(); + if (response != null) { + int status = response.getStatus(); + if (status == 404 || isRedirection(status)) { + return EXCEPTION_NONE; + } + } + if (exception.getCause() != null) { + exception = exception.getCause(); + } + String simpleName = exception.getClass().getSimpleName(); + return Tag.of("exception", StringUtils.isNotEmpty(simpleName) ? simpleName : exception.getClass().getName()); + } + + /** + * Creates an {@code outcome} tag based on the status of the given {@code response}. + * @param response the container response + * @return the outcome tag derived from the status of the response + */ + public static Tag outcome(ContainerResponse response) { + if (response != null) { + return Outcome.forStatus(response.getStatus()).asTag(); + } + /* In case there is no response we are dealing with an unmapped exception. */ + return Outcome.SERVER_ERROR.asTag(); + } + +} diff --git a/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/JerseyTagsProvider.java b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/JerseyTagsProvider.java new file mode 100644 index 00000000000..77fba18be0b --- /dev/null +++ b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/JerseyTagsProvider.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2019, 2023 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ +package org.glassfish.jersey.micrometer.server; + +import io.micrometer.core.instrument.Tag; +import org.glassfish.jersey.server.monitoring.RequestEvent; + +/** + * Provides {@link Tag Tags} for Jersey request metrics. + * + * @author Michael Weirauch + * @since 2.41 + */ +public interface JerseyTagsProvider { + + /** + * Provides tags to be associated with metrics for the given {@code event}. + * @param event the request event + * @return tags to associate with metrics recorded for the request + */ + Iterable httpRequestTags(RequestEvent event); + + /** + * Provides tags to be associated with the + * {@link io.micrometer.core.instrument.LongTaskTimer} which instruments the given + * long-running {@code event}. + * @param event the request event + * @return tags to associate with metrics recorded for the request + */ + Iterable httpLongRequestTags(RequestEvent event); + +} diff --git a/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/MetricsApplicationEventListener.java b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/MetricsApplicationEventListener.java new file mode 100644 index 00000000000..e1df4519045 --- /dev/null +++ b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/MetricsApplicationEventListener.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2019, 2023 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ +package org.glassfish.jersey.micrometer.server; + +import io.micrometer.core.instrument.MeterRegistry; +import org.glassfish.jersey.server.monitoring.ApplicationEvent; +import org.glassfish.jersey.server.monitoring.ApplicationEventListener; +import org.glassfish.jersey.server.monitoring.RequestEvent; +import org.glassfish.jersey.server.monitoring.RequestEventListener; + +import static java.util.Objects.requireNonNull; + +/** + * The Micrometer {@link ApplicationEventListener} which registers + * {@link RequestEventListener} for instrumenting Jersey server requests. + * + * @author Michael Weirauch + * @since 2.41 + */ +public class MetricsApplicationEventListener implements ApplicationEventListener { + + private final MeterRegistry meterRegistry; + + private final JerseyTagsProvider tagsProvider; + + private final String metricName; + + private final AnnotationFinder annotationFinder; + + private final boolean autoTimeRequests; + + public MetricsApplicationEventListener(MeterRegistry registry, JerseyTagsProvider tagsProvider, String metricName, + boolean autoTimeRequests) { + this(registry, tagsProvider, metricName, autoTimeRequests, AnnotationFinder.DEFAULT); + } + + public MetricsApplicationEventListener(MeterRegistry registry, JerseyTagsProvider tagsProvider, String metricName, + boolean autoTimeRequests, AnnotationFinder annotationFinder) { + this.meterRegistry = requireNonNull(registry); + this.tagsProvider = requireNonNull(tagsProvider); + this.metricName = requireNonNull(metricName); + this.annotationFinder = requireNonNull(annotationFinder); + this.autoTimeRequests = autoTimeRequests; + } + + @Override + public void onEvent(ApplicationEvent event) { + } + + @Override + public RequestEventListener onRequest(RequestEvent requestEvent) { + return new MetricsRequestEventListener(meterRegistry, tagsProvider, metricName, autoTimeRequests, + annotationFinder); + } + +} diff --git a/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/MetricsRequestEventListener.java b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/MetricsRequestEventListener.java new file mode 100644 index 00000000000..0b2418b7257 --- /dev/null +++ b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/MetricsRequestEventListener.java @@ -0,0 +1,178 @@ +/* + * Copyright (c) 2019, 2023 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ +package org.glassfish.jersey.micrometer.server; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import io.micrometer.core.annotation.Timed; +import io.micrometer.core.instrument.LongTaskTimer; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Timer; +import org.glassfish.jersey.server.ContainerRequest; +import org.glassfish.jersey.server.model.ResourceMethod; +import org.glassfish.jersey.server.monitoring.RequestEvent; +import org.glassfish.jersey.server.monitoring.RequestEventListener; + +import static java.util.Objects.requireNonNull; + +/** + * {@link RequestEventListener} recording timings for Jersey server requests. + * + * @author Michael Weirauch + * @author Jon Schneider + * @since 2.41 + */ +public class MetricsRequestEventListener implements RequestEventListener { + + private final Map shortTaskSample = Collections + .synchronizedMap(new IdentityHashMap<>()); + + private final Map> longTaskSamples = Collections + .synchronizedMap(new IdentityHashMap<>()); + + private final Map> timedAnnotationsOnRequest = Collections + .synchronizedMap(new IdentityHashMap<>()); + + private final MeterRegistry registry; + + private final JerseyTagsProvider tagsProvider; + + private boolean autoTimeRequests; + + private final TimedFinder timedFinder; + + private final String metricName; + + public MetricsRequestEventListener(MeterRegistry registry, JerseyTagsProvider tagsProvider, String metricName, + boolean autoTimeRequests, AnnotationFinder annotationFinder) { + this.registry = requireNonNull(registry); + this.tagsProvider = requireNonNull(tagsProvider); + this.metricName = requireNonNull(metricName); + this.autoTimeRequests = autoTimeRequests; + this.timedFinder = new TimedFinder(annotationFinder); + } + + @Override + public void onEvent(RequestEvent event) { + ContainerRequest containerRequest = event.getContainerRequest(); + Set timedAnnotations; + + switch (event.getType()) { + case ON_EXCEPTION: + if (!isNotFoundException(event)) { + break; + } + time(event, containerRequest); + break; + case REQUEST_MATCHED: + time(event, containerRequest); + break; + case FINISHED: + timedAnnotations = timedAnnotationsOnRequest.remove(containerRequest); + Timer.Sample shortSample = shortTaskSample.remove(containerRequest); + + if (shortSample != null) { + for (Timer timer : shortTimers(timedAnnotations, event)) { + shortSample.stop(timer); + } + } + + Collection longSamples = this.longTaskSamples.remove(containerRequest); + if (longSamples != null) { + for (LongTaskTimer.Sample longSample : longSamples) { + longSample.stop(); + } + } + break; + } + } + + private void time(RequestEvent event, ContainerRequest containerRequest) { + Set timedAnnotations; + timedAnnotations = annotations(event); + + timedAnnotationsOnRequest.put(containerRequest, timedAnnotations); + shortTaskSample.put(containerRequest, Timer.start(registry)); + + List longTaskSamples = longTaskTimers(timedAnnotations, event).stream() + .map(LongTaskTimer::start) + .collect(Collectors.toList()); + if (!longTaskSamples.isEmpty()) { + this.longTaskSamples.put(containerRequest, longTaskSamples); + } + } + + private boolean isNotFoundException(RequestEvent event) { + Throwable t = event.getException(); + if (t == null) { + return false; + } + String className = t.getClass().getCanonicalName(); + return className.equals("jakarta.ws.rs.NotFoundException") || className.equals("javax.ws.rs.NotFoundException"); + } + + private Set shortTimers(Set timed, RequestEvent event) { + /* + * Given we didn't find any matching resource method, 404s will be only recorded + * when auto-time-requests is enabled. On par with WebMVC instrumentation. + */ + if ((timed == null || timed.isEmpty()) && autoTimeRequests) { + return Collections.singleton(registry.timer(metricName, tagsProvider.httpRequestTags(event))); + } + + if (timed == null) { + return Collections.emptySet(); + } + + return timed.stream() + .filter(annotation -> !annotation.longTask()) + .map(t -> Timer.builder(t, metricName).tags(tagsProvider.httpRequestTags(event)).register(registry)) + .collect(Collectors.toSet()); + } + + private Set longTaskTimers(Set timed, RequestEvent event) { + return timed.stream() + .filter(Timed::longTask) + .map(LongTaskTimer::builder) + .map(b -> b.tags(tagsProvider.httpLongRequestTags(event)).register(registry)) + .collect(Collectors.toSet()); + } + + private Set annotations(RequestEvent event) { + final Set timed = new HashSet<>(); + + final ResourceMethod matchingResourceMethod = event.getUriInfo().getMatchedResourceMethod(); + if (matchingResourceMethod != null) { + // collect on method level + timed.addAll(timedFinder.findTimedAnnotations(matchingResourceMethod.getInvocable().getHandlingMethod())); + + // fallback on class level + if (timed.isEmpty()) { + timed.addAll(timedFinder.findTimedAnnotations( + matchingResourceMethod.getInvocable().getHandlingMethod().getDeclaringClass())); + } + } + return timed; + } + +} diff --git a/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/ObservationApplicationEventListener.java b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/ObservationApplicationEventListener.java new file mode 100644 index 00000000000..419828f3855 --- /dev/null +++ b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/ObservationApplicationEventListener.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2019, 2023 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ +package org.glassfish.jersey.micrometer.server; + +import io.micrometer.observation.ObservationRegistry; +import org.glassfish.jersey.server.monitoring.ApplicationEvent; +import org.glassfish.jersey.server.monitoring.ApplicationEventListener; +import org.glassfish.jersey.server.monitoring.RequestEvent; +import org.glassfish.jersey.server.monitoring.RequestEventListener; + +import static java.util.Objects.requireNonNull; + +/** + * The Micrometer {@link ApplicationEventListener} which registers + * {@link RequestEventListener} for instrumenting Jersey server requests with + * observations. + * + * @author Marcin Grzejszczak + * @since 2.41 + */ +public class ObservationApplicationEventListener implements ApplicationEventListener { + + private final ObservationRegistry observationRegistry; + + private final String metricName; + + private final JerseyObservationConvention jerseyObservationConvention; + + public ObservationApplicationEventListener(ObservationRegistry observationRegistry, String metricName) { + this(observationRegistry, metricName, null); + } + + public ObservationApplicationEventListener(ObservationRegistry observationRegistry, String metricName, + JerseyObservationConvention jerseyObservationConvention) { + this.observationRegistry = requireNonNull(observationRegistry); + this.metricName = requireNonNull(metricName); + this.jerseyObservationConvention = jerseyObservationConvention; + } + + @Override + public void onEvent(ApplicationEvent event) { + } + + @Override + public RequestEventListener onRequest(RequestEvent requestEvent) { + return new ObservationRequestEventListener(observationRegistry, metricName, jerseyObservationConvention); + } + +} diff --git a/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/ObservationRequestEventListener.java b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/ObservationRequestEventListener.java new file mode 100644 index 00000000000..37517e4b61f --- /dev/null +++ b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/ObservationRequestEventListener.java @@ -0,0 +1,145 @@ +/* + * Copyright (c) 2019, 2023 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ +package org.glassfish.jersey.micrometer.server; + +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.Map; +import java.util.Objects; + +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationRegistry; +import org.glassfish.jersey.server.ContainerRequest; +import org.glassfish.jersey.server.monitoring.RequestEvent; +import org.glassfish.jersey.server.monitoring.RequestEventListener; + +import static java.util.Objects.requireNonNull; + +/** + * {@link RequestEventListener} recording observations for Jersey server requests. + * + * @author Marcin Grzejszczak + * @since 2.41 + */ +public class ObservationRequestEventListener implements RequestEventListener { + + private final Map observations = Collections + .synchronizedMap(new IdentityHashMap<>()); + + private final ObservationRegistry registry; + + private final JerseyObservationConvention customConvention; + + private final String metricName; + + private final JerseyObservationConvention defaultConvention; + + public ObservationRequestEventListener(ObservationRegistry registry, String metricName) { + this(registry, metricName, null); + } + + public ObservationRequestEventListener(ObservationRegistry registry, String metricName, + JerseyObservationConvention customConvention) { + this.registry = requireNonNull(registry); + this.metricName = requireNonNull(metricName); + this.customConvention = customConvention; + this.defaultConvention = new DefaultJerseyObservationConvention(this.metricName); + } + + @Override + public void onEvent(RequestEvent event) { + ContainerRequest containerRequest = event.getContainerRequest(); + + switch (event.getType()) { + case ON_EXCEPTION: + if (!isNotFoundException(event)) { + break; + } + startObservation(event); + break; + case REQUEST_MATCHED: + startObservation(event); + break; + case RESP_FILTERS_START: + ObservationScopeAndContext observationScopeAndContext = observations.get(containerRequest); + if (observationScopeAndContext != null) { + observationScopeAndContext.jerseyContext.setResponse(event.getContainerResponse()); + observationScopeAndContext.jerseyContext.setRequestEvent(event); + } + break; + case FINISHED: + ObservationScopeAndContext finishedObservation = observations.remove(containerRequest); + if (finishedObservation != null) { + finishedObservation.jerseyContext.setRequestEvent(event); + Observation.Scope observationScope = finishedObservation.observationScope; + observationScope.close(); + observationScope.getCurrentObservation().stop(); + } + break; + default: + break; + } + } + + private void startObservation(RequestEvent event) { + JerseyContext jerseyContext = new JerseyContext(event); + Observation observation = JerseyObservationDocumentation.DEFAULT.start(this.customConvention, + this.defaultConvention, () -> jerseyContext, this.registry); + Observation.Scope scope = observation.openScope(); + observations.put(event.getContainerRequest(), new ObservationScopeAndContext(scope, jerseyContext)); + } + + private boolean isNotFoundException(RequestEvent event) { + Throwable t = event.getException(); + if (t == null) { + return false; + } + String className = t.getClass().getCanonicalName(); + return className.equals("jakarta.ws.rs.NotFoundException") || className.equals("javax.ws.rs.NotFoundException"); + } + + private static class ObservationScopeAndContext { + + final Observation.Scope observationScope; + + final JerseyContext jerseyContext; + + ObservationScopeAndContext(Observation.Scope observationScope, JerseyContext jerseyContext) { + this.observationScope = observationScope; + this.jerseyContext = jerseyContext; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ObservationScopeAndContext that = (ObservationScopeAndContext) o; + return Objects.equals(observationScope, that.observationScope) + && Objects.equals(jerseyContext, that.jerseyContext); + } + + @Override + public int hashCode() { + return Objects.hash(observationScope, jerseyContext); + } + + } + +} diff --git a/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/TimedFinder.java b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/TimedFinder.java new file mode 100644 index 00000000000..82b93038d52 --- /dev/null +++ b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/TimedFinder.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2019, 2023 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ +package org.glassfish.jersey.micrometer.server; + +import java.lang.reflect.AnnotatedElement; +import java.util.Arrays; +import java.util.Collections; +import java.util.Set; +import java.util.stream.Collectors; + +import io.micrometer.core.annotation.Timed; +import io.micrometer.core.annotation.TimedSet; + +class TimedFinder { + + private final AnnotationFinder annotationFinder; + + TimedFinder(AnnotationFinder annotationFinder) { + this.annotationFinder = annotationFinder; + } + + Set findTimedAnnotations(AnnotatedElement element) { + Timed t = annotationFinder.findAnnotation(element, Timed.class); + if (t != null) { + return Collections.singleton(t); + } + + TimedSet ts = annotationFinder.findAnnotation(element, TimedSet.class); + if (ts != null) { + return Arrays.stream(ts.value()).collect(Collectors.toSet()); + } + + return Collections.emptySet(); + } + +} diff --git a/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/package-info.java b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/package-info.java new file mode 100644 index 00000000000..d09ece7c892 --- /dev/null +++ b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2019, 2023 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +/** + * Binders for Jersey. Code ported from Micrometer repository. + */ +package org.glassfish.jersey.micrometer.server; diff --git a/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/DefaultJerseyTagsProviderTest.java b/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/DefaultJerseyTagsProviderTest.java new file mode 100644 index 00000000000..3ede36b443e --- /dev/null +++ b/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/DefaultJerseyTagsProviderTest.java @@ -0,0 +1,141 @@ +/* + * Copyright (c) 2019, 2023 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ +package org.glassfish.jersey.micrometer.server; + +import java.net.URI; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import javax.ws.rs.NotAcceptableException; + +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.Tags; +import org.glassfish.jersey.server.ContainerRequest; +import org.glassfish.jersey.server.ContainerResponse; +import org.glassfish.jersey.server.ExtendedUriInfo; +import org.glassfish.jersey.server.internal.monitoring.RequestEventImpl.Builder; +import org.glassfish.jersey.server.monitoring.RequestEvent; +import org.glassfish.jersey.server.monitoring.RequestEvent.Type; +import org.glassfish.jersey.uri.UriTemplate; +import org.junit.jupiter.api.Test; + +import static java.util.stream.StreamSupport.stream; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Tests for {@link DefaultJerseyTagsProvider}. + * + * @author Michael Weirauch + * @author Johnny Lim + */ +class DefaultJerseyTagsProviderTest { + + private final DefaultJerseyTagsProvider tagsProvider = new DefaultJerseyTagsProvider(); + + @Test + void testRootPath() { + assertThat(tagsProvider.httpRequestTags(event(200, null, "/", (String[]) null))) + .containsExactlyInAnyOrder(tagsFrom("root", 200, null, "SUCCESS")); + } + + @Test + void templatedPathsAreReturned() { + assertThat(tagsProvider.httpRequestTags(event(200, null, "/", "/", "/hello/{name}"))) + .containsExactlyInAnyOrder(tagsFrom("/hello/{name}", 200, null, "SUCCESS")); + } + + @Test + void applicationPathIsPresent() { + assertThat(tagsProvider.httpRequestTags(event(200, null, "/app", "/", "/hello"))) + .containsExactlyInAnyOrder(tagsFrom("/app/hello", 200, null, "SUCCESS")); + } + + @Test + void notFoundsAreShunted() { + assertThat(tagsProvider.httpRequestTags(event(404, null, "/app", "/", "/not-found"))) + .containsExactlyInAnyOrder(tagsFrom("NOT_FOUND", 404, null, "CLIENT_ERROR")); + } + + @Test + void redirectsAreShunted() { + assertThat(tagsProvider.httpRequestTags(event(301, null, "/app", "/", "/redirect301"))) + .containsExactlyInAnyOrder(tagsFrom("REDIRECTION", 301, null, "REDIRECTION")); + assertThat(tagsProvider.httpRequestTags(event(302, null, "/app", "/", "/redirect302"))) + .containsExactlyInAnyOrder(tagsFrom("REDIRECTION", 302, null, "REDIRECTION")); + assertThat(tagsProvider.httpRequestTags(event(399, null, "/app", "/", "/redirect399"))) + .containsExactlyInAnyOrder(tagsFrom("REDIRECTION", 399, null, "REDIRECTION")); + } + + @Test + @SuppressWarnings("serial") + void exceptionsAreMappedCorrectly() { + assertThat(tagsProvider.httpRequestTags(event(500, new IllegalArgumentException(), "/app", (String[]) null))) + .containsExactlyInAnyOrder(tagsFrom("/app", 500, "IllegalArgumentException", "SERVER_ERROR")); + assertThat(tagsProvider.httpRequestTags( + event(500, new IllegalArgumentException(new NullPointerException()), "/app", (String[]) null))) + .containsExactlyInAnyOrder(tagsFrom("/app", 500, "NullPointerException", "SERVER_ERROR")); + assertThat(tagsProvider.httpRequestTags(event(406, new NotAcceptableException(), "/app", (String[]) null))) + .containsExactlyInAnyOrder(tagsFrom("/app", 406, "NotAcceptableException", "CLIENT_ERROR")); + assertThat(tagsProvider.httpRequestTags(event(500, new Exception("anonymous") { + }, "/app", (String[]) null))).containsExactlyInAnyOrder(tagsFrom("/app", 500, + "org.glassfish.jersey.micrometer.server.DefaultJerseyTagsProviderTest$1", "SERVER_ERROR")); + } + + @Test + void longRequestTags() { + assertThat(tagsProvider.httpLongRequestTags(event(0, null, "/app", (String[]) null))) + .containsExactlyInAnyOrder(Tag.of("method", "GET"), Tag.of("uri", "/app")); + } + + private static RequestEvent event(Integer status, Exception exception, String baseUri, + String... uriTemplateStrings) { + Builder builder = new Builder(); + + ContainerRequest containerRequest = mock(ContainerRequest.class); + when(containerRequest.getMethod()).thenReturn("GET"); + builder.setContainerRequest(containerRequest); + + ContainerResponse containerResponse = mock(ContainerResponse.class); + when(containerResponse.getStatus()).thenReturn(status); + builder.setContainerResponse(containerResponse); + + builder.setException(exception, null); + + ExtendedUriInfo extendedUriInfo = mock(ExtendedUriInfo.class); + when(extendedUriInfo.getBaseUri()) + .thenReturn(URI.create("http://localhost:8080" + (baseUri == null ? "/" : baseUri))); + List uriTemplates = uriTemplateStrings == null ? Collections.emptyList() + : Arrays.stream(uriTemplateStrings).map(uri -> new UriTemplate(uri)).collect(Collectors.toList()); + // UriTemplate are returned in reverse order + Collections.reverse(uriTemplates); + when(extendedUriInfo.getMatchedTemplates()).thenReturn(uriTemplates); + builder.setExtendedUriInfo(extendedUriInfo); + + return builder.build(Type.FINISHED); + } + + private static Tag[] tagsFrom(String uri, int status, String exception, String outcome) { + Iterable expectedTags = Tags.of("method", "GET", "uri", uri, "status", String.valueOf(status), "exception", + exception == null ? "None" : exception, "outcome", outcome); + + return stream(expectedTags.spliterator(), false).toArray(Tag[]::new); + } + +} diff --git a/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/MetricsRequestEventListenerTest.java b/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/MetricsRequestEventListenerTest.java new file mode 100644 index 00000000000..eb96e5dc3b6 --- /dev/null +++ b/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/MetricsRequestEventListenerTest.java @@ -0,0 +1,174 @@ +/* + * Copyright (c) 2019, 2023 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ +package org.glassfish.jersey.micrometer.server; + +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.ws.rs.NotFoundException; +import javax.ws.rs.core.Application; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.Tags; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import org.glassfish.jersey.micrometer.server.mapper.ResourceGoneExceptionMapper; +import org.glassfish.jersey.micrometer.server.resources.TestResource; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link MetricsApplicationEventListener}. + * + * @author Michael Weirauch + * @author Johnny Lim + */ +class MetricsRequestEventListenerTest extends JerseyTest { + + static { + Logger.getLogger("org.glassfish.jersey").setLevel(Level.OFF); + } + + private static final String METRIC_NAME = "http.server.requests"; + + private MeterRegistry registry; + + @Override + protected Application configure() { + registry = new SimpleMeterRegistry(); + + final MetricsApplicationEventListener listener = new MetricsApplicationEventListener(registry, + new DefaultJerseyTagsProvider(), METRIC_NAME, true); + + final ResourceConfig config = new ResourceConfig(); + config.register(listener); + config.register(TestResource.class); + config.register(ResourceGoneExceptionMapper.class); + + return config; + } + + @Test + void resourcesAreTimed() { + target("/").request().get(); + target("hello").request().get(); + target("hello/").request().get(); + target("hello/peter").request().get(); + target("sub-resource/sub-hello/peter").request().get(); + + assertThat(registry.get(METRIC_NAME).tags(tagsFrom("root", "200", "SUCCESS", null)).timer().count()) + .isEqualTo(1); + + assertThat(registry.get(METRIC_NAME).tags(tagsFrom("/hello", "200", "SUCCESS", null)).timer().count()) + .isEqualTo(2); + + assertThat(registry.get(METRIC_NAME).tags(tagsFrom("/hello/{name}", "200", "SUCCESS", null)).timer().count()) + .isEqualTo(1); + + assertThat(registry.get(METRIC_NAME) + .tags(tagsFrom("/sub-resource/sub-hello/{name}", "200", "SUCCESS", null)) + .timer() + .count()).isEqualTo(1); + + // assert we are not auto-timing long task @Timed + assertThat(registry.getMeters()).hasSize(4); + } + + @Test + void notFoundIsAccumulatedUnderSameUri() { + try { + target("not-found").request().get(); + } + catch (NotFoundException ignored) { + } + + assertThat(registry.get(METRIC_NAME).tags(tagsFrom("NOT_FOUND", "404", "CLIENT_ERROR", null)).timer().count()) + .isEqualTo(1); + } + + @Test + void notFoundIsReportedWithUriOfMatchedResource() { + try { + target("throws-not-found-exception").request().get(); + } + catch (NotFoundException ignored) { + } + + assertThat(registry.get(METRIC_NAME) + .tags(tagsFrom("/throws-not-found-exception", "404", "CLIENT_ERROR", null)) + .timer() + .count()).isEqualTo(1); + } + + @Test + void redirectsAreReportedWithUriOfMatchedResource() { + target("redirect/302").request().get(); + target("redirect/307").request().get(); + + assertThat(registry.get(METRIC_NAME) + .tags(tagsFrom("/redirect/{status}", "302", "REDIRECTION", null)) + .timer() + .count()).isEqualTo(1); + + assertThat(registry.get(METRIC_NAME) + .tags(tagsFrom("/redirect/{status}", "307", "REDIRECTION", null)) + .timer() + .count()).isEqualTo(1); + } + + @Test + void exceptionsAreMappedCorrectly() { + try { + target("throws-exception").request().get(); + } + catch (Exception ignored) { + } + try { + target("throws-webapplication-exception").request().get(); + } + catch (Exception ignored) { + } + try { + target("throws-mappable-exception").request().get(); + } + catch (Exception ignored) { + } + + assertThat(registry.get(METRIC_NAME) + .tags(tagsFrom("/throws-exception", "500", "SERVER_ERROR", "IllegalArgumentException")) + .timer() + .count()).isEqualTo(1); + + assertThat(registry.get(METRIC_NAME) + .tags(tagsFrom("/throws-webapplication-exception", "401", "CLIENT_ERROR", "NotAuthorizedException")) + .timer() + .count()).isEqualTo(1); + + assertThat(registry.get(METRIC_NAME) + .tags(tagsFrom("/throws-mappable-exception", "410", "CLIENT_ERROR", "ResourceGoneException")) + .timer() + .count()).isEqualTo(1); + } + + private static Iterable tagsFrom(String uri, String status, String outcome, String exception) { + return Tags.of("method", "GET", "uri", uri, "status", status, "outcome", outcome, "exception", + exception == null ? "None" : exception); + } + +} diff --git a/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/MetricsRequestEventListenerTimedTest.java b/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/MetricsRequestEventListenerTimedTest.java new file mode 100644 index 00000000000..a95e831846b --- /dev/null +++ b/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/MetricsRequestEventListenerTimedTest.java @@ -0,0 +1,188 @@ +/* + * Copyright (c) 2019, 2023 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ +package org.glassfish.jersey.micrometer.server; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.ws.rs.ProcessingException; +import javax.ws.rs.core.Application; +import javax.ws.rs.core.Response; + +import io.micrometer.core.Issue; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.Tags; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import org.glassfish.jersey.micrometer.server.resources.TimedOnClassResource; +import org.glassfish.jersey.micrometer.server.resources.TimedResource; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * @author Michael Weirauch + */ +class MetricsRequestEventListenerTimedTest extends JerseyTest { + + static { + Logger.getLogger("org.glassfish.jersey").setLevel(Level.OFF); + } + + private static final String METRIC_NAME = "http.server.requests"; + + private MeterRegistry registry; + + private CountDownLatch longTaskRequestStartedLatch; + + private CountDownLatch longTaskRequestReleaseLatch; + + @Override + protected Application configure() { + registry = new SimpleMeterRegistry(); + longTaskRequestStartedLatch = new CountDownLatch(1); + longTaskRequestReleaseLatch = new CountDownLatch(1); + + final MetricsApplicationEventListener listener = new MetricsApplicationEventListener(registry, + new DefaultJerseyTagsProvider(), METRIC_NAME, false); + + final ResourceConfig config = new ResourceConfig(); + config.register(listener); + config.register(new TimedResource(longTaskRequestStartedLatch, longTaskRequestReleaseLatch)); + config.register(TimedOnClassResource.class); + + return config; + } + + @Test + void resourcesAndNotFoundsAreNotAutoTimed() { + target("not-timed").request().get(); + target("not-found").request().get(); + + assertThat(registry.find(METRIC_NAME).tags(tagsFrom("/not-timed", 200)).timer()).isNull(); + + assertThat(registry.find(METRIC_NAME).tags(tagsFrom("NOT_FOUND", 404)).timer()).isNull(); + } + + @Test + void resourcesWithAnnotationAreTimed() { + target("timed").request().get(); + target("multi-timed").request().get(); + + assertThat(registry.get(METRIC_NAME).tags(tagsFrom("/timed", 200)).timer().count()).isEqualTo(1); + + assertThat(registry.get("multi1").tags(tagsFrom("/multi-timed", 200)).timer().count()).isEqualTo(1); + + assertThat(registry.get("multi2").tags(tagsFrom("/multi-timed", 200)).timer().count()).isEqualTo(1); + } + + @Test + void longTaskTimerSupported() throws InterruptedException, ExecutionException, TimeoutException { + final Future future = target("long-timed").request().async().get(); + + /* + * Wait until the request has arrived at the server side. (Async client processing + * might be slower in triggering the request resulting in the assertions below to + * fail. Thread.sleep() is not an option, so resort to CountDownLatch.) + */ + longTaskRequestStartedLatch.await(5, TimeUnit.SECONDS); + + // the request is not timed, yet + assertThat(registry.find(METRIC_NAME).tags(tagsFrom("/timed", 200)).timer()).isNull(); + + // the long running task is timed + assertThat(registry.get("long.task.in.request") + .tags(Tags.of("method", "GET", "uri", "/long-timed")) + .longTaskTimer() + .activeTasks()).isEqualTo(1); + + // finish the long running request + longTaskRequestReleaseLatch.countDown(); + future.get(5, TimeUnit.SECONDS); + + // the request is timed after the long running request completed + assertThat(registry.get(METRIC_NAME).tags(tagsFrom("/long-timed", 200)).timer().count()).isEqualTo(1); + } + + @Test + @Issue("gh-2861") + void longTaskTimerOnlyOneMeter() throws InterruptedException, ExecutionException, TimeoutException { + final Future future = target("just-long-timed").request().async().get(); + + /* + * Wait until the request has arrived at the server side. (Async client processing + * might be slower in triggering the request resulting in the assertions below to + * fail. Thread.sleep() is not an option, so resort to CountDownLatch.) + */ + longTaskRequestStartedLatch.await(5, TimeUnit.SECONDS); + + // the long running task is timed + assertThat(registry.get("long.task.in.request") + .tags(Tags.of("method", "GET", "uri", "/just-long-timed")) + .longTaskTimer() + .activeTasks()).isEqualTo(1); + + // finish the long running request + longTaskRequestReleaseLatch.countDown(); + future.get(5, TimeUnit.SECONDS); + + // no meters registered except the one checked above + assertThat(registry.getMeters().size()).isOne(); + } + + @Test + void unnamedLongTaskTimerIsNotSupported() { + assertThatExceptionOfType(ProcessingException.class) + .isThrownBy(() -> target("long-timed-unnamed").request().get()) + .withCauseInstanceOf(IllegalArgumentException.class); + } + + @Test + void classLevelAnnotationIsInherited() { + target("/class/inherited").request().get(); + + assertThat(registry.get(METRIC_NAME) + .tags(Tags.concat(tagsFrom("/class/inherited", 200), Tags.of("on", "class"))) + .timer() + .count()).isEqualTo(1); + } + + @Test + void methodLevelAnnotationOverridesClassLevel() { + target("/class/on-method").request().get(); + + assertThat(registry.get(METRIC_NAME) + .tags(Tags.concat(tagsFrom("/class/on-method", 200), Tags.of("on", "method"))) + .timer() + .count()).isEqualTo(1); + + // class level annotation is not picked up + assertThat(registry.getMeters()).hasSize(1); + } + + private static Iterable tagsFrom(String uri, int status) { + return Tags.of("method", "GET", "uri", uri, "status", String.valueOf(status), "exception", "None"); + } + +} diff --git a/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/exception/ResourceGoneException.java b/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/exception/ResourceGoneException.java new file mode 100644 index 00000000000..9a2bd2544b4 --- /dev/null +++ b/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/exception/ResourceGoneException.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2019, 2023 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ +package org.glassfish.jersey.micrometer.server.exception; + +public class ResourceGoneException extends RuntimeException { + + public ResourceGoneException() { + super(); + } + + public ResourceGoneException(String message) { + super(message); + } + + public ResourceGoneException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/mapper/ResourceGoneExceptionMapper.java b/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/mapper/ResourceGoneExceptionMapper.java new file mode 100644 index 00000000000..b532c0364a1 --- /dev/null +++ b/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/mapper/ResourceGoneExceptionMapper.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2019, 2023 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ +package org.glassfish.jersey.micrometer.server.mapper; + +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; +import javax.ws.rs.ext.ExceptionMapper; + +import org.glassfish.jersey.micrometer.server.exception.ResourceGoneException; + +public class ResourceGoneExceptionMapper implements ExceptionMapper { + + @Override + public Response toResponse(ResourceGoneException exception) { + return Response.status(Status.GONE).build(); + } + +} diff --git a/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/observation/AbstractObservationRequestEventListenerTest.java b/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/observation/AbstractObservationRequestEventListenerTest.java new file mode 100644 index 00000000000..d018bc9d69c --- /dev/null +++ b/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/observation/AbstractObservationRequestEventListenerTest.java @@ -0,0 +1,140 @@ +/* + * Copyright (c) 2019, 2023 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ +package org.glassfish.jersey.micrometer.server.observation; + +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.ws.rs.core.Application; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.Tags; +import io.micrometer.core.instrument.observation.DefaultMeterObservationHandler; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import io.micrometer.observation.ObservationHandler.FirstMatchingCompositeObservationHandler; +import io.micrometer.observation.ObservationRegistry; +import io.micrometer.tracing.Tracer; +import io.micrometer.tracing.exporter.FinishedSpan; +import io.micrometer.tracing.handler.DefaultTracingObservationHandler; +import io.micrometer.tracing.handler.PropagatingReceiverTracingObservationHandler; +import io.micrometer.tracing.handler.PropagatingSenderTracingObservationHandler; +import io.micrometer.tracing.propagation.Propagator; +import io.micrometer.tracing.test.simple.SpanAssert; +import io.micrometer.tracing.test.simple.SpansAssert; +import org.glassfish.jersey.micrometer.server.ObservationApplicationEventListener; +import org.glassfish.jersey.micrometer.server.ObservationRequestEventListener; +import org.glassfish.jersey.micrometer.server.resources.TestResource; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; +import org.junit.jupiter.api.Test; +import zipkin2.CheckResult; +import zipkin2.reporter.Sender; +import zipkin2.reporter.urlconnection.URLConnectionSender; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ObservationRequestEventListener}. + * + * @author Marcin Grzejsczak + */ +abstract class AbstractObservationRequestEventListenerTest extends JerseyTest { + + static { + Logger.getLogger("org.glassfish.jersey").setLevel(Level.OFF); + } + + private static final String METRIC_NAME = "http.server.requests"; + + ObservationRegistry observationRegistry; + + MeterRegistry registry; + + Boolean zipkinAvailable; + + Sender sender; + + @Override + protected Application configure() { + observationRegistry = ObservationRegistry.create(); + registry = new SimpleMeterRegistry(); + sender = URLConnectionSender.create("http://localhost:9411/api/v2/spans"); + + observationRegistry.observationConfig().observationHandler(new DefaultMeterObservationHandler(registry)); + + configureRegistry(observationRegistry); + + final ObservationApplicationEventListener listener = + new ObservationApplicationEventListener(observationRegistry, METRIC_NAME); + + final ResourceConfig config = new ResourceConfig(); + config.register(listener); + config.register(TestResource.class); + + return config; + } + + abstract void configureRegistry(ObservationRegistry registry); + + abstract List getFinishedSpans(); + + boolean isZipkinAvailable() { + if (zipkinAvailable == null) { + CheckResult checkResult = sender.check(); + zipkinAvailable = checkResult.ok(); + } + return zipkinAvailable; + } + + void setupTracing(Tracer tracer, Propagator propagator) { + observationRegistry.observationConfig() + .observationHandler(new FirstMatchingCompositeObservationHandler( + new PropagatingSenderTracingObservationHandler<>(tracer, propagator), + new PropagatingReceiverTracingObservationHandler<>(tracer, propagator), + new DefaultTracingObservationHandler(tracer))); + } + + @Test + void resourcesAreTimed() { + target("sub-resource/sub-hello/peter").request().get(); + + assertThat(registry.get(METRIC_NAME) + .tags(tagsFrom("/sub-resource/sub-hello/{name}", "200", "SUCCESS", null)) + .timer() + .count()).isEqualTo(1); + // Timer and Long Task Timer + assertThat(registry.getMeters()).hasSize(2); + + List finishedSpans = getFinishedSpans(); + SpansAssert.assertThat(finishedSpans).hasSize(1); + FinishedSpan finishedSpan = finishedSpans.get(0); + System.out.println("Trace Id [" + finishedSpan.getTraceId() + "]"); + SpanAssert.assertThat(finishedSpan) + .hasNameEqualTo("HTTP GET") + .hasTag("exception", "None") + .hasTag("method", "GET") + .hasTag("outcome", "SUCCESS") + .hasTag("status", "200") + .hasTag("uri", "/sub-resource/sub-hello/{name}"); + } + + private static Iterable tagsFrom(String uri, String status, String outcome, String exception) { + return Tags.of("method", "GET", "uri", uri, "status", status, "outcome", outcome, "exception", + exception == null ? "None" : exception); + } +} diff --git a/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/observation/ObservationApplicationEventListenerTest.java b/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/observation/ObservationApplicationEventListenerTest.java new file mode 100644 index 00000000000..896e49b93f3 --- /dev/null +++ b/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/observation/ObservationApplicationEventListenerTest.java @@ -0,0 +1,197 @@ +/* + * Copyright (c) 2019, 2023 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ +package org.glassfish.jersey.micrometer.server.observation; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import brave.Tracing; +import brave.Tracing.Builder; +import brave.context.slf4j.MDCScopeDecorator; +import brave.handler.SpanHandler; +import brave.propagation.B3Propagation; +import brave.propagation.B3Propagation.Format; +import brave.propagation.ThreadLocalCurrentTraceContext; +import brave.sampler.Sampler; +import brave.test.TestSpanHandler; +import io.micrometer.observation.ObservationRegistry; +import io.micrometer.tracing.CurrentTraceContext; +import io.micrometer.tracing.Tracer; +import io.micrometer.tracing.brave.bridge.BraveBaggageManager; +import io.micrometer.tracing.brave.bridge.BraveCurrentTraceContext; +import io.micrometer.tracing.brave.bridge.BraveFinishedSpan; +import io.micrometer.tracing.brave.bridge.BravePropagator; +import io.micrometer.tracing.brave.bridge.BraveTracer; +import io.micrometer.tracing.exporter.FinishedSpan; +import io.micrometer.tracing.otel.bridge.ArrayListSpanProcessor; +import io.micrometer.tracing.otel.bridge.OtelBaggageManager; +import io.micrometer.tracing.otel.bridge.OtelCurrentTraceContext; +import io.micrometer.tracing.otel.bridge.OtelFinishedSpan; +import io.micrometer.tracing.otel.bridge.OtelPropagator; +import io.micrometer.tracing.otel.bridge.OtelTracer; +import io.micrometer.tracing.otel.bridge.Slf4JBaggageEventListener; +import io.micrometer.tracing.otel.bridge.Slf4JEventListener; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.context.propagation.ContextPropagators; +import io.opentelemetry.exporter.zipkin.ZipkinSpanExporterBuilder; +import io.opentelemetry.extension.trace.propagation.B3Propagator; +import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import io.opentelemetry.sdk.trace.SdkTracerProviderBuilder; +import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor; +import io.opentelemetry.sdk.trace.export.SpanExporter; +import io.opentelemetry.semconv.resource.attributes.ResourceAttributes; +import org.glassfish.jersey.micrometer.server.ObservationApplicationEventListener; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Nested; +import zipkin2.Span; +import zipkin2.reporter.AsyncReporter; +import zipkin2.reporter.brave.ZipkinSpanHandler; + +import static io.opentelemetry.sdk.trace.samplers.Sampler.alwaysOn; + +/** + * Tests for {@link ObservationApplicationEventListener}. + * + * @author Marcin Grzejsczak + */ +class ObservationApplicationEventListenerTest { + + @Nested + class BraveObservationRequestEventListenerTest extends AbstractObservationRequestEventListenerTest { + + Tracing tracing; + + TestSpanHandler testSpanHandler; + + AsyncReporter reporter; + + @Override + void configureRegistry(ObservationRegistry registry) { + testSpanHandler = new TestSpanHandler(); + + reporter = AsyncReporter.create(sender); + + SpanHandler spanHandler = ZipkinSpanHandler + .create(reporter); + + ThreadLocalCurrentTraceContext braveCurrentTraceContext = ThreadLocalCurrentTraceContext.newBuilder() + .addScopeDecorator(MDCScopeDecorator.get()) // Example of Brave's + // automatic MDC setup + .build(); + + CurrentTraceContext bridgeContext = new BraveCurrentTraceContext(braveCurrentTraceContext); + + Builder builder = Tracing.newBuilder() + .currentTraceContext(braveCurrentTraceContext) + .supportsJoin(false) + .traceId128Bit(true) + .propagationFactory(B3Propagation.newFactoryBuilder().injectFormat(Format.SINGLE).build()) + .sampler(Sampler.ALWAYS_SAMPLE) + .addSpanHandler(testSpanHandler) + .localServiceName("brave-test"); + + if (isZipkinAvailable()) { + builder.addSpanHandler(spanHandler); + } + + tracing = builder + .build(); + brave.Tracer braveTracer = tracing.tracer(); + Tracer tracer = new BraveTracer(braveTracer, bridgeContext, new BraveBaggageManager()); + BravePropagator bravePropagator = new BravePropagator(tracing); + setupTracing(tracer, bravePropagator); + } + + @Override + List getFinishedSpans() { + return testSpanHandler.spans().stream().map(BraveFinishedSpan::new).collect(Collectors.toList()); + } + + @AfterEach + void cleanup() { + if (isZipkinAvailable()) { + reporter.flush(); + reporter.close(); + } + tracing.close(); + } + } + + @Nested + class OtelObservationRequestEventListenerTest extends AbstractObservationRequestEventListenerTest { + + SdkTracerProvider sdkTracerProvider; + + ArrayListSpanProcessor processor; + + @Override + void configureRegistry(ObservationRegistry registry) { + processor = new ArrayListSpanProcessor(); + + SpanExporter spanExporter = new ZipkinSpanExporterBuilder() + .setSender(sender) + .build(); + + SdkTracerProviderBuilder builder = SdkTracerProvider.builder() + .setSampler(alwaysOn()) + .addSpanProcessor(processor) + .setResource(Resource.create(Attributes.of(ResourceAttributes.SERVICE_NAME, "otel-test"))); + + if (isZipkinAvailable()) { + builder.addSpanProcessor(SimpleSpanProcessor.create(spanExporter)); + } + + sdkTracerProvider = builder + .build(); + + ContextPropagators contextPropagators = ContextPropagators.create(B3Propagator.injectingSingleHeader()); + + OpenTelemetrySdk openTelemetrySdk = OpenTelemetrySdk.builder() + .setTracerProvider(sdkTracerProvider) + .setPropagators(contextPropagators) + .build(); + + io.opentelemetry.api.trace.Tracer otelTracer = openTelemetrySdk.getTracerProvider() + .get("io.micrometer.micrometer-tracing"); + + OtelCurrentTraceContext otelCurrentTraceContext = new OtelCurrentTraceContext(); + + Slf4JEventListener slf4JEventListener = new Slf4JEventListener(); + + Slf4JBaggageEventListener slf4JBaggageEventListener = new Slf4JBaggageEventListener(Collections.emptyList()); + + OtelTracer tracer = new OtelTracer(otelTracer, otelCurrentTraceContext, event -> { + slf4JEventListener.onEvent(event); + slf4JBaggageEventListener.onEvent(event); + }, new OtelBaggageManager(otelCurrentTraceContext, Collections.emptyList(), Collections.emptyList())); + OtelPropagator otelPropagator = new OtelPropagator(contextPropagators, otelTracer); + setupTracing(tracer, otelPropagator); + } + + @Override + List getFinishedSpans() { + return processor.spans().stream().map(OtelFinishedSpan::fromOtel).collect(Collectors.toList()); + } + + @AfterEach + void cleanup() { + sdkTracerProvider.close(); + } + } +} diff --git a/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/resources/TestResource.java b/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/resources/TestResource.java new file mode 100644 index 00000000000..19bc45a445d --- /dev/null +++ b/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/resources/TestResource.java @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2019, 2023 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ +package org.glassfish.jersey.micrometer.server.resources; + +import java.net.URI; + +import javax.ws.rs.GET; +import javax.ws.rs.NotAuthorizedException; +import javax.ws.rs.NotFoundException; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.RedirectionException; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; + +import org.glassfish.jersey.micrometer.server.exception.ResourceGoneException; + +/** + * @author Michael Weirauch + */ +@Path("/") +@Produces(MediaType.TEXT_PLAIN) +public class TestResource { + + @Produces(MediaType.TEXT_PLAIN) + public static class SubResource { + + @GET + @Path("sub-hello/{name}") + public String hello(@PathParam("name") String name) { + return "hello " + name; + } + + } + + @GET + public String index() { + return "index"; + } + + @GET + @Path("hello") + public String hello() { + return "hello"; + } + + @GET + @Path("hello/{name}") + public String hello(@PathParam("name") String name) { + return "hello " + name; + } + + @GET + @Path("throws-not-found-exception") + public String throwsNotFoundException() { + throw new NotFoundException(); + } + + @GET + @Path("throws-exception") + public String throwsException() { + throw new IllegalArgumentException(); + } + + @GET + @Path("throws-webapplication-exception") + public String throwsWebApplicationException() { + throw new NotAuthorizedException("notauth", Response.status(Status.UNAUTHORIZED).build()); + } + + @GET + @Path("throws-mappable-exception") + public String throwsMappableException() { + throw new ResourceGoneException("Resource has been permanently removed."); + } + + @GET + @Path("redirect/{status}") + public Response redirect(@PathParam("status") int status) { + if (status == 307) { + throw new RedirectionException(status, URI.create("hello")); + } + return Response.status(status).header("Location", "/hello").build(); + } + + @Path("/sub-resource") + public SubResource subResource() { + return new SubResource(); + } + +} diff --git a/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/resources/TimedOnClassResource.java b/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/resources/TimedOnClassResource.java new file mode 100644 index 00000000000..ff040af3262 --- /dev/null +++ b/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/resources/TimedOnClassResource.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2019, 2023 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ +package org.glassfish.jersey.micrometer.server.resources; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +import io.micrometer.core.annotation.Timed; + +/** + * @author Michael Weirauch + */ +@Path("/class") +@Produces(MediaType.TEXT_PLAIN) +@Timed(extraTags = { "on", "class" }) +public class TimedOnClassResource { + + @GET + @Path("inherited") + public String inherited() { + return "inherited"; + } + + @GET + @Path("on-method") + @Timed(extraTags = { "on", "method" }) + public String onMethod() { + return "on-method"; + } + +} diff --git a/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/resources/TimedResource.java b/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/resources/TimedResource.java new file mode 100644 index 00000000000..4515b415e8a --- /dev/null +++ b/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/resources/TimedResource.java @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2019, 2023 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ +package org.glassfish.jersey.micrometer.server.resources; + +import java.util.concurrent.CountDownLatch; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +import io.micrometer.core.annotation.Timed; + +import static java.util.Objects.requireNonNull; + +/** + * @author Michael Weirauch + */ +@Path("/") +@Produces(MediaType.TEXT_PLAIN) +public class TimedResource { + + private final CountDownLatch longTaskRequestStartedLatch; + + private final CountDownLatch longTaskRequestReleaseLatch; + + public TimedResource(CountDownLatch longTaskRequestStartedLatch, CountDownLatch longTaskRequestReleaseLatch) { + this.longTaskRequestStartedLatch = requireNonNull(longTaskRequestStartedLatch); + this.longTaskRequestReleaseLatch = requireNonNull(longTaskRequestReleaseLatch); + } + + @GET + @Path("not-timed") + public String notTimed() { + return "not-timed"; + } + + @GET + @Path("timed") + @Timed + public String timed() { + return "timed"; + } + + @GET + @Path("multi-timed") + @Timed("multi1") + @Timed("multi2") + public String multiTimed() { + return "multi-timed"; + } + + /* + * Async server side processing (AsyncResponse) is not supported in the in-memory test + * container. + */ + @GET + @Path("long-timed") + @Timed + @Timed(value = "long.task.in.request", longTask = true) + public String longTimed() { + longTaskRequestStartedLatch.countDown(); + try { + longTaskRequestReleaseLatch.await(); + } + catch (InterruptedException e) { + throw new RuntimeException(e); + } + return "long-timed"; + } + + @GET + @Path("just-long-timed") + @Timed(value = "long.task.in.request", longTask = true) + public String justLongTimed() { + longTaskRequestStartedLatch.countDown(); + try { + longTaskRequestReleaseLatch.await(); + } + catch (InterruptedException e) { + throw new RuntimeException(e); + } + return "long-timed"; + } + + @GET + @Path("long-timed-unnamed") + @Timed + @Timed(longTask = true) + public String longTimedUnnamed() { + return "long-timed-unnamed"; + } + +} diff --git a/ext/pom.xml b/ext/pom.xml index 01ebf574f8d..9ed36a142ae 100644 --- a/ext/pom.xml +++ b/ext/pom.xml @@ -44,6 +44,7 @@ cdi entity-filtering metainf-services + micrometer mvc mvc-bean-validation mvc-freemarker diff --git a/pom.xml b/pom.xml index 60b47161d1a..f656fb1b5c0 100644 --- a/pom.xml +++ b/pom.xml @@ -2299,6 +2299,8 @@ 2.3.6 1.2.7 1.3.3 + 1.10.10 + 1.0.9 2.0.1 2.0 1.9.15