All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.micrometer.core.instrument.HttpServerTimingInstrumentationVerificationTests Maven / Gradle / Ivy

/*
 * Copyright 2022 VMware, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package io.micrometer.core.instrument;

import io.micrometer.common.lang.Nullable;
import io.micrometer.core.annotation.Incubating;
import io.micrometer.core.instrument.search.RequiredSearch;
import io.micrometer.core.ipc.http.HttpSender;
import io.micrometer.core.ipc.http.HttpUrlConnectionSender;
import io.micrometer.observation.Observation;
import io.micrometer.observation.ObservationHandler;
import io.micrometer.observation.ObservationRegistry;
import io.micrometer.observation.tck.TestObservationRegistry;
import io.micrometer.observation.transport.ReceiverContext;
import org.assertj.core.api.Condition;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;

import java.net.URI;
import java.time.Duration;
import java.util.function.Function;

import static org.assertj.core.api.Assertions.assertThat;
import static org.awaitility.Awaitility.await;
import static org.junit.jupiter.api.Assumptions.assumeTrue;

/**
 * Verify the instrumentation of an HTTP server has the minimum expected results. See
 * {@link #startInstrumentedWithMetricsServer()} for the required specification the
 * instrumented HTTP server must handle.
 */
@Incubating(since = "1.8.9")
@ExtendWith(InstrumentationVerificationTests.AfterBeforeParameterResolver.class)
public abstract class HttpServerTimingInstrumentationVerificationTests extends InstrumentationTimingVerificationTests {

    private final HttpSender sender = new HttpUrlConnectionSender();

    private URI baseUri;

    private boolean assumptionSucceeded = true;

    @Override
    protected String timerName() {
        return "http.server.requests";
    }

    /**
     * Start the instrumented HTTP server to be tested. The server will be instrumented
     * using the {@link MeterRegistry} only. No request body or query parameters will be
     * sent, and any response body will be ignored. The server MUST serve the routes
     * described by {@link InstrumentedRoutes}. Constants are available in that class for
     * the routes that will have requests sent to them as part of the TCK. The server MUST
     * NOT have a route for the following:
     * 
    *
  • {@code GET /notFound} (returns 404 response)
  • *
* @return base URI where the instrumented server is receiving requests. Must end with * a slash (/). * @see InstrumentedRoutes */ protected abstract URI startInstrumentedWithMetricsServer() throws Exception; /** * Start the instrumented HTTP server to be tested. The server will be instrumented * using the {@link ObservationRegistry}. No request body or query parameters will be * sent, and any response body will be ignored. The server MUST serve the routes * described by {@link InstrumentedRoutes}. Constants are available in that class for * the routes that will have requests sent to them as part of the TCK. The server MUST * NOT have a route for the following: *
    *
  • {@code GET /notFound} (returns 404 response)
  • *
* @return base URI where the instrumented server is receiving requests. Must end with * a slash (/) or {@code null} if you don't support observations * @see InstrumentedRoutes */ @Nullable protected abstract URI startInstrumentedWithObservationsServer() throws Exception; /** * Stop the instrumented server that was started with * {@link #startInstrumentedWithMetricsServer()}. */ protected abstract void stopInstrumentedServer() throws Exception; @BeforeEach void beforeEach(TestType testType) throws Exception { if (testType == TestType.METRICS_VIA_METER_REGISTRY) { baseUri = startInstrumentedWithMetricsServer(); } else { baseUri = startInstrumentedWithObservationsServer(); assumptionSucceeded = baseUri != null; assumeTrue(assumptionSucceeded, "You must implement the method to test your instrumentation against an ObservationRegistry"); } } @AfterEach void afterEach() throws Exception { if (assumptionSucceeded) { stopInstrumentedServer(); } } @ParameterizedTest @EnumSource(TestType.class) void uriIsNotFound_whenRouteIsUnmapped(TestType testType) throws Throwable { sender.get(baseUri + "notFound").send(); checkTimer(rs -> rs.tags("uri", "NOT_FOUND", "status", "404", "method", "GET").timer().count() == 1); } @ParameterizedTest @EnumSource(TestType.class) void uriTemplateIsTagged(TestType testType) throws Throwable { sender.get(baseUri + "hello/world").send(); checkTimer(rs -> rs.tags("uri", InstrumentedRoutes.TEMPLATED_ROUTE, "status", "200", "method", "GET") .timer() .count() == 1); } @ParameterizedTest @EnumSource(TestType.class) void redirect(TestType testType) throws Throwable { sender.get(baseUri + "foundRedirect").send(); checkTimer(rs -> rs.tags("uri", InstrumentedRoutes.REDIRECT, "status", "302", "method", "GET") .timer() .count() == 1); } @ParameterizedTest @EnumSource(TestType.class) void errorResponse(TestType testType) throws Throwable { sender.post(baseUri + "error").send(); checkTimer( rs -> rs.tags("uri", InstrumentedRoutes.ERROR, "status", "500", "method", "POST").timer().count() == 1); } @ParameterizedTest @EnumSource(TestType.class) void canExtractContextFromHeaders(TestType testType) throws Throwable { sender.get(baseUri + "hello/micrometer").withHeader("Test-Propagation", "someValue").send(); // only applicable with Observation-based instrumentation // TODO documentation verification maybe shouldn't be done after each test? // That's why this has to be after the http request is made assumeTrue(testType == TestType.METRICS_VIA_OBSERVATIONS_WITH_METRICS_HANDLER); assertThat(getObservationRegistry()).hasSingleObservationThat() .has(new Condition<>( context -> "someValue".contentEquals((CharSequence) context.getRequired("Test-Propagation")), "has Test-Propagation in context with value 'someValue'")); } @Override protected TestObservationRegistry createObservationRegistryWithMetrics() { TestObservationRegistry observationRegistryWithMetrics = super.createObservationRegistryWithMetrics(); observationRegistryWithMetrics.observationConfig().observationHandler(new ExtractHeaderObservationHandler<>()); return observationRegistryWithMetrics; } @SuppressWarnings("rawtypes") static class ExtractHeaderObservationHandler implements ObservationHandler { @SuppressWarnings("unchecked") @Override public void onStart(T context) { String testPropagation = context.getGetter().get(context.getCarrier(), "Test-Propagation"); if (testPropagation != null) { context.put("Test-Propagation", testPropagation); } } @Override public boolean supportsContext(Observation.Context context) { return context instanceof ReceiverContext; } } private void checkTimer(Function timerCheck) { // jersey instrumentation finishes after response is sent, creating a race await().atLeast(Duration.ofMillis(25)) .atMost(Duration.ofMillis(150)) .until(() -> timerCheck.apply(getRegistry().get(timerName()))); } /** * Class containing constants that can be used for implementing the HTTP routes that * the instrumented server needs to serve to pass the * {@link HttpServerTimingInstrumentationVerificationTests}. The HTTP server MUST * serve the following: *
    *
  • {@link #ROOT}: {@code GET /}
  • *
  • {@link #TEMPLATED_ROUTE}: {@code GET /hello/{name}}
  • *
  • {@link #REDIRECT}: {@code GET /foundRedirect}
  • *
  • {@link #ERROR}: {@code POST /error}
  • *
*/ public static class InstrumentedRoutes { /** * Path for the route with a path variable. The templated route is expected to be * used in the URI tag. */ public static final String TEMPLATED_ROUTE = "/hello/{name}"; /** * Path for the root route. */ public static final String ROOT = "/"; /** * Path for the route that will respond with a 500 server error to a POST method * request. */ public static final String ERROR = "/error"; /** * Path for the route that will redirect with status code 302 to the {@link #ROOT} * route. */ public static final String REDIRECT = "/foundRedirect"; } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy