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

io.helidon.webserver.observe.metrics.MetricsFeature Maven / Gradle / Ivy

There is a newer version: 4.1.4
Show newest version
/*
 * Copyright (c) 2022, 2024 Oracle and/or its affiliates.
 *
 * 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
 *
 *     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 io.helidon.webserver.observe.metrics;

import java.util.List;
import java.util.Optional;
import java.util.ServiceLoader;
import java.util.Set;
import java.util.function.Supplier;

import io.helidon.common.HelidonServiceLoader;
import io.helidon.common.media.type.MediaType;
import io.helidon.common.media.type.MediaTypes;
import io.helidon.http.HeaderValues;
import io.helidon.http.HttpException;
import io.helidon.http.Status;
import io.helidon.metrics.api.Meter;
import io.helidon.metrics.api.MeterRegistry;
import io.helidon.metrics.api.MeterRegistryFormatter;
import io.helidon.metrics.api.MetricsConfig;
import io.helidon.metrics.api.MetricsFactory;
import io.helidon.metrics.api.SystemTagsManager;
import io.helidon.metrics.spi.MeterRegistryFormatterProvider;
import io.helidon.webserver.KeyPerformanceIndicatorSupport;
import io.helidon.webserver.http.Handler;
import io.helidon.webserver.http.HttpRouting;
import io.helidon.webserver.http.HttpRules;
import io.helidon.webserver.http.HttpService;
import io.helidon.webserver.http.SecureHandler;
import io.helidon.webserver.http.ServerRequest;
import io.helidon.webserver.http.ServerResponse;

import static io.helidon.http.HeaderNames.ALLOW;
import static io.helidon.http.Status.METHOD_NOT_ALLOWED_405;
import static io.helidon.http.Status.NOT_FOUND_404;
import static io.helidon.http.Status.OK_200;

class MetricsFeature {

    /**
     * Prefix for key performance indicator metrics names.
     */
    static final String KPI_METER_NAME_PREFIX = "requests";
    private static final String KPI_METER_NAME_PREFIX_WITH_DOT = KPI_METER_NAME_PREFIX + ".";

    private static final Handler DISABLED_ENDPOINT_HANDLER = (req, res) -> res.status(Status.NOT_FOUND_404)
            .send("Metrics are disabled");

    private final MetricsConfig metricsConfig;
    private final MeterRegistry meterRegistry;
    private KeyPerformanceIndicatorSupport.Metrics kpiMetrics;

    MetricsFeature(MetricsObserverConfig config) {
        this.metricsConfig = config.metricsConfig();
        this.meterRegistry = config.meterRegistry().orElseGet(() -> MetricsFactory.getInstance().globalRegistry(metricsConfig));
    }

    /**
     * Configure Helidon specific metrics.
     *
     * @param rules rules to use
     */
    void configureVendorMetrics(HttpRouting.Builder rules) {
        kpiMetrics =
                KeyPerformanceIndicatorMetricsImpls.get(meterRegistry,
                                                        KPI_METER_NAME_PREFIX_WITH_DOT,
                                                        metricsConfig
                                                                .keyPerformanceIndicatorMetricsConfig());

        rules.addFilter((chain, req, res) -> {
            KeyPerformanceIndicatorSupport.Context kpiContext = kpiContext(req);
            PostRequestMetricsSupport prms = PostRequestMetricsSupport.create();
            req.context().register(prms);

            kpiContext.requestHandlingStarted(kpiMetrics);
            try {
                chain.proceed();
                postRequestProcessing(prms, req, res, null, kpiContext);
            } catch (Exception e) {
                postRequestProcessing(prms, req, res, e, kpiContext);
            }
        });
    }

    void register(HttpRouting.Builder routing, String endpoint) {
        configureVendorMetrics(routing);
        routing.register(endpoint, new MetricsService());
    }

    Optional output(MediaType mediaType,
                       Iterable scopeSelection,
                       Iterable nameSelection) {
        MeterRegistryFormatter formatter = chooseFormatter(meterRegistry,
                                                           mediaType,
                                                           SystemTagsManager.instance().scopeTagName(),
                                                           scopeSelection,
                                                           nameSelection);

        return formatter.format();
    }

    Optional outputMetadata(MediaType mediaType,
                       Iterable scopeSelection,
                       Iterable nameSelection) {
        MeterRegistryFormatter formatter = chooseFormatter(meterRegistry,
                                                           mediaType,
                                                           SystemTagsManager.instance().scopeTagName(),
                                                           scopeSelection,
                                                           nameSelection);

        return formatter.formatMetadata();
    }



    private static MediaType bestAccepted(ServerRequest req) {
        return req.headers()
                .bestAccepted(MediaTypes.TEXT_PLAIN,
                              MediaTypes.APPLICATION_OPENMETRICS_TEXT,
                              MediaTypes.APPLICATION_JSON)
                .orElse(null);
    }

    private static MediaType bestAcceptedForMetadata(ServerRequest req) {
        return req.headers()
                .bestAccepted(MediaTypes.APPLICATION_JSON)
                .orElse(null);
    }

    private static KeyPerformanceIndicatorSupport.Context kpiContext(ServerRequest request) {
        return request.context()
                .get(KeyPerformanceIndicatorSupport.Context.class)
                .orElseGet(KeyPerformanceIndicatorSupport.Context::create);
    }

    private MeterRegistryFormatter chooseFormatter(MeterRegistry meterRegistry,
                                                   MediaType mediaType,
                                                   Optional scopeTagName,
                                                   Iterable scopeSelection,
                                                   Iterable nameSelection) {
        Optional formatter = HelidonServiceLoader.builder(
                        ServiceLoader.load(MeterRegistryFormatterProvider.class))
                .build()
                .stream()
                .map(provider -> provider.formatter(mediaType,
                                                    metricsConfig,
                                                    meterRegistry,
                                                    scopeTagName,
                                                    scopeSelection,
                                                    nameSelection))
                .filter(Optional::isPresent)
                .map(Optional::get)
                .findFirst();

        if (formatter.isPresent()) {
            return formatter.get();
        }
        throw new HttpException("Unsupported media type for metrics formatting: " + mediaType,
                                Status.UNSUPPORTED_MEDIA_TYPE_415,
                                true);
    }

    private void getAll(ServerRequest req, ServerResponse res) {
        getMatching(req, res, req.query().all("scope", List::of), req.query().all("name", List::of));
    }

    private void getMatching(ServerRequest req,
                             ServerResponse res,
                             Iterable scopeSelection,
                             Iterable nameSelection) {
        MediaType mediaType = bestAccepted(req);
        res.header(HeaderValues.CACHE_NO_CACHE);
        if (mediaType == null) {
            res.status(Status.NOT_ACCEPTABLE_406);
            res.send();
            return;
        }

        getOrOptionsMatching(mediaType, res, () -> output(mediaType,
                                                          scopeSelection,
                                                          nameSelection));
    }

    private void getOrOptionsMatching(MediaType mediaType,
                                      ServerResponse res,
                                      Supplier> dataSupplier) {
        Optional output = dataSupplier.get();

        if (output.isPresent()) {
            res.status(OK_200)
                    .headers().contentType(mediaType);
            res.send(output.get());
        } else {
            res.status(NOT_FOUND_404);
            res.send();
        }
    }

    private void setUpEndpoints(HttpRules rules) {
        if (!metricsConfig.permitAll()) {
            rules.any(SecureHandler.authorize(metricsConfig.roles().toArray(new String[0])));
        }
        // routing to root of metrics
        // As of Helidon 4, this is the only path we should need because scope-based or metric-name-based
        // selection should use query parameters instead of paths.
        rules.get("/", this::getAll)
                .options("/", this::optionsAll);

        // routing to each scope
        // As of Helidon 4, users should use /metrics?scope=xyz instead of /metrics/xyz, and
        // /metrics/?scope=xyz&name=abc instead of /metrics/xyz/abc. These routings are kept
        // temporarily for backward compatibility.

        Meter.Scope.BUILT_IN_SCOPES
                .forEach(scope -> {
                    boolean isScopeEnabled = metricsConfig.isScopeEnabled(scope);
                    rules.get("/" + scope,
                              isScopeEnabled ? (req, res) -> getMatching(req, res, Set.of(scope), Set.of())
                                      : DISABLED_ENDPOINT_HANDLER)
                            .get("/" + scope + "/{metric}",
                                 isScopeEnabled ? (req, res) -> getByName(req, res, Set.of(scope)) // should use ?scope=
                                         : DISABLED_ENDPOINT_HANDLER)
                            .options("/" + scope,
                                     isScopeEnabled ? (req, res) -> optionsMatching(req, res, Set.of(scope), Set.of())
                                             : DISABLED_ENDPOINT_HANDLER)
                            .options("/" + scope + "/{metric}",
                                     isScopeEnabled ? (req, res) -> optionsByName(req, res, Set.of(scope))
                                             : DISABLED_ENDPOINT_HANDLER);
                });
    }

    private void getByName(ServerRequest req, ServerResponse res, Iterable scopeSelection) {
        String metricName = req.path().pathParameters().get("metric");
        getMatching(req, res, scopeSelection, Set.of(metricName));
    }

    private void postRequestProcessing(PostRequestMetricsSupport prms,
                                       ServerRequest request,
                                       ServerResponse response,
                                       Throwable throwable,
                                       KeyPerformanceIndicatorSupport.Context kpiContext) {
        kpiContext.requestProcessingCompleted(throwable == null && response.status().code() < 500);
        prms.runTasks(request, response, throwable);
    }

    private void optionsAll(ServerRequest req, ServerResponse res) {
        optionsMatching(req, res, req.query().all("scope", List::of), req.query().all("name", List::of));
    }

    private void optionsByName(ServerRequest req, ServerResponse res, Iterable scopeSelection) {
        String metricName = req.path().pathParameters().get("metric");
        optionsMatching(req, res, scopeSelection, Set.of(metricName));
    }

    private void optionsMatching(ServerRequest req,
                                 ServerResponse res,
                                 Iterable scopeSelection,
                                 Iterable nameSelection) {
        MediaType mediaType = bestAcceptedForMetadata(req);
        if (mediaType == null) {
            res.header(ALLOW, "GET");
            res.status(METHOD_NOT_ALLOWED_405);
            res.send();
        }

        getOrOptionsMatching(mediaType, res, () -> outputMetadata(mediaType,
                                                                  scopeSelection,
                                                                  nameSelection));
    }

    private void setUpDisabledEndpoints(HttpRules rules) {
        rules.get("/", DISABLED_ENDPOINT_HANDLER)
                .options("/", DISABLED_ENDPOINT_HANDLER);
    }

    /**
     * Separate metrics service class with an afterStop method that is properly invoked.
     */
    private class MetricsService implements HttpService {
        @Override
        public void routing(HttpRules rules) {
            if (metricsConfig.enabled()) {
                setUpEndpoints(rules);
            } else {
                setUpDisabledEndpoints(rules);
            }
        }

        @Override
        public void afterStop() {
            kpiMetrics.close();
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy