JaxRsMetricsClientServerTest.java

/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The ASF licenses this file
 * to you 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
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.apache.cxf.systest.jaxrs;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;

import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import org.apache.cxf.jaxrs.JAXRSServerFactoryBean;
import org.apache.cxf.jaxrs.client.JAXRSClientFactoryBean;
import org.apache.cxf.jaxrs.lifecycle.SingletonResourceProvider;
import org.apache.cxf.metrics.MetricsFeature;
import org.apache.cxf.metrics.micrometer.MicrometerMetricsProperties;
import org.apache.cxf.metrics.micrometer.MicrometerMetricsProvider;
import org.apache.cxf.metrics.micrometer.provider.DefaultExceptionClassProvider;
import org.apache.cxf.metrics.micrometer.provider.DefaultTimedAnnotationProvider;
import org.apache.cxf.metrics.micrometer.provider.StandardTags;
import org.apache.cxf.metrics.micrometer.provider.StandardTagsProvider;
import org.apache.cxf.metrics.micrometer.provider.jaxrs.JaxrsOperationTagsCustomizer;
import org.apache.cxf.metrics.micrometer.provider.jaxrs.JaxrsTags;
import org.apache.cxf.testutil.common.AbstractBusClientServerTestBase;
import org.apache.cxf.testutil.common.AbstractBusTestServerBase;

import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;

import org.junit.BeforeClass;
import org.junit.Test;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;

/**
 * Test for CXF-9206: cxf.server.requests metric must not be
 * counted twice for JAX-RS server endpoints when MetricsFeature is used.
 *
 * Root cause: the CXF-9168 fix (de39a25) made createEndpointContext() eagerly
 * return a MicrometerServerMetricsContext for all server-side endpoints so that
 * requests failing before a BindingOperationInfo is available are still counted.
 * For JAX-RS, createResourceContext() already returns its own MetricsContext per
 * request, so ExchangeMetrics ended up incrementing cxf.server.requests twice.
 *
 * The fix skips creating a server context in createEndpointContext() when the
 * endpoint uses the JAX-RS binding (JAXRSBindingFactory.JAXRS_BINDING_ID).
 */
public class JaxRsMetricsClientServerTest extends AbstractBusClientServerTestBase {

    private static final MeterRegistry METER_REGISTRY = new SimpleMeterRegistry();
    private static final String PORT = allocatePort(JaxRsMetricsClientServerTest.class);


    @Path("/hello")
    public interface HelloService {
        @GET
        @Path("/{name}")
        @Produces(MediaType.TEXT_PLAIN)
        String sayHello(@PathParam("name") String name);
    }

    public static class HelloServiceImpl implements HelloService {
        @Override
        public String sayHello(String name) {
            return "Hello " + name;
        }
    }


    public static class Server extends AbstractBusTestServerBase {
        protected void run() {
            var jaxrsTags = new JaxrsTags();
            var operationsCustomizer = new JaxrsOperationTagsCustomizer(jaxrsTags);
            var tagsProvider = new StandardTagsProvider(
                new DefaultExceptionClassProvider(), new StandardTags());
            var properties = new MicrometerMetricsProperties();

            var provider = new MicrometerMetricsProvider(
                METER_REGISTRY,
                tagsProvider,
                List.of(operationsCustomizer),
                new DefaultTimedAnnotationProvider(),
                properties
            );

            JAXRSServerFactoryBean sf = new JAXRSServerFactoryBean();
            sf.setResourceClasses(HelloServiceImpl.class);
            sf.setResourceProvider(HelloServiceImpl.class,
                new SingletonResourceProvider(new HelloServiceImpl()));
            sf.setAddress("http://localhost:" + PORT + "/");
            sf.setFeatures(Arrays.asList(new MetricsFeature(provider)));
            sf.create();
        }
    }


    @BeforeClass
    public static void startServers() throws Exception {
        createStaticBus();
        assertTrue("server did not launch correctly", launchServer(Server.class, true));
    }


    private HelloService createClient() {
        JAXRSClientFactoryBean factory = new JAXRSClientFactoryBean();
        factory.setServiceClass(HelloService.class);
        factory.setAddress("http://localhost:" + PORT + "/");
        factory.setHeaders(Collections.singletonMap("Accept", "text/plain"));
        return (HelloService) factory.create();
    }


    /**
     * CXF-9206: a single successful JAX-RS request must produce exactly one
     * sample in cxf.server.requests (count == 1), not two.
     */
    @Test
    public void testSuccessfulRequestIsCountedOnce() {
        METER_REGISTRY.clear();

        String response = createClient().sayHello("world");
        assertEquals("Hello world", response);

        Timer timer = METER_REGISTRY.find("cxf.server.requests").timer();
        assertNotNull("Expected cxf.server.requests timer to be registered", timer);
        assertEquals(
            "cxf.server.requests count should be 1 for a single request",
            1,
            timer.count()
        );
    }

    /**
     * Two successive requests must accumulate to count == 2, not 4.
     */
    @Test
    public void testTwoRequestsAreCountedTwice() {
        METER_REGISTRY.clear();

        HelloService client = createClient();
        client.sayHello("alice");
        client.sayHello("bob");

        Timer timer = METER_REGISTRY.find("cxf.server.requests").timer();
        assertNotNull("Expected cxf.server.requests timer to be registered", timer);
        assertEquals(
            "cxf.server.requests count should be 2 for two requests",
            2,
            timer.count()
        );
    }
}