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

org.eclipse.ditto.internal.utils.tracing.TraceUtils Maven / Gradle / Ivy

/*
 * Copyright (c) 2017 Contributors to the Eclipse Foundation
 *
 * See the NOTICE file(s) distributed with this work for additional
 * information regarding copyright ownership.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0
 *
 * SPDX-License-Identifier: EPL-2.0
 */
package org.eclipse.ditto.internal.utils.tracing;

import java.util.regex.Pattern;

import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable;

import org.eclipse.ditto.base.model.auth.AuthorizationContextType;
import org.eclipse.ditto.base.model.headers.DittoHeaderDefinition;
import org.eclipse.ditto.internal.utils.metrics.DittoMetrics;
import org.eclipse.ditto.internal.utils.metrics.instruments.tag.Tag;
import org.eclipse.ditto.internal.utils.metrics.instruments.tag.TagSet;
import org.eclipse.ditto.internal.utils.metrics.instruments.timer.PreparedTimer;
import org.eclipse.ditto.internal.utils.tracing.span.SpanTagKey;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.apache.pekko.http.javadsl.model.HttpRequest;

/**
 * Utility for tracing Http requests.
 */
@Immutable
public final class TraceUtils {

    private static final Logger LOGGER = LoggerFactory.getLogger(TraceUtils.class);

    private static final String TRACING_FILTER_DELIMITER = "_";
    private static final String SLASH = "/";
    private static final Pattern DUPLICATE_SLASH_PATTERN = Pattern.compile("\\/+");

    private static final String HTTP_ROUNDTRIP_METRIC_NAME = "roundtrip_http";
    private static final String FILTER_AUTH_METRIC_NAME = "filter_auth";
    private static final String LIVE_CHANNEL_NAME = "live";
    private static final String TWIN_CHANNEL_NAME = "twin";
    private static final Pattern MESSAGE_PATTERN = Pattern.compile("(.*/(inbox|outbox)/messages/.*)|(.*/inbox/claim)");

    private TraceUtils() {
        throw new AssertionError();
    }

    /**
     * Prepares an {@link PreparedTimer} with default {@link #HTTP_ROUNDTRIP_METRIC_NAME} and tags.
     *
     * @param request The request to extract tags and request method.
     * @return The prepared {@link PreparedTimer}
     */
    public static PreparedTimer newHttpRoundTripTimer(final HttpRequest request) {
        return newExpiringTimer(HTTP_ROUNDTRIP_METRIC_NAME)
                .tags(getTraceInformationTags(request))
                .tag(SpanTagKey.REQUEST_METHOD_NAME.getTagForValue(request.method().name()))
                .tag(SpanTagKey.CHANNEL.getTagForValue(determineChannel(request)));
    }

    private static TagSet getTraceInformationTags(final HttpRequest httpRequest) {
        final TagSet result;
        @Nullable final var traceInformation = tryToDetermineTraceInformation(getRelativeUriPath(httpRequest));
        if (null == traceInformation) {
            result = TagSet.empty();
        } else {
            result = traceInformation.getTagSet();
        }
        return result;
    }

    @Nullable
    private static TraceInformation tryToDetermineTraceInformation(final String requestPath) {
        try {
            return determineTraceInformation(requestPath);
        } catch (final IllegalArgumentException e) {
            LOGGER.warn("Failed to determine trace information for request path <{}>: {}", requestPath, e.getMessage());
            return null;
        }
    }

    private static TraceInformation determineTraceInformation(final String requestPath) {
        final var traceInformationGenerator = TraceInformationGenerator.getInstance();
        return traceInformationGenerator.apply(requestPath);
    }

    private static String getRelativeUriPath(final HttpRequest httpRequest) {
        final var uri = httpRequest.getUri();
        final var relativeUri = uri.toRelative();
        return relativeUri.path();
    }

    /**
     * Prepares an {@link PreparedTimer} with default {@link #FILTER_AUTH_METRIC_NAME} and tags.
     *
     * @param authorizationContextType the authorization context type (i.e. JWT, ...).
     * @return the prepared {@link PreparedTimer}.
     * @throws NullPointerException if {@code authorizationContextType} is {@code null}.
     */
    public static PreparedTimer newAuthFilterTimer(final AuthorizationContextType authorizationContextType) {
        return newAuthFilterTimer(authorizationContextType, TagSet.empty());
    }

    private static PreparedTimer newAuthFilterTimer(
            final AuthorizationContextType authorizationContextType,
            final TagSet requestTags
    ) {
        return newExpiringTimer(FILTER_AUTH_METRIC_NAME)
                .tags(requestTags)
                .tag(SpanTagKey.AUTH_SUCCESS.getTagForValue(false))
                .tag(SpanTagKey.AUTH_ERROR.getTagForValue(false))
                .tag(getAuthTypeMetricsTag(authorizationContextType))
                .onExpiration(expiredTimer ->
                        expiredTimer
                                .tag(SpanTagKey.AUTH_SUCCESS.getTagForValue(false))
                                .tag(SpanTagKey.AUTH_ERROR.getTagForValue(true)));
    }

    private static PreparedTimer newExpiringTimer(final String tracingFilter) {
        return DittoMetrics.timer(metricizeTraceUri(tracingFilter));
    }

    private static Tag getAuthTypeMetricsTag(final AuthorizationContextType authorizationContextType) {
        return Tag.of(SpanTagKey.KEY_PREFIX + "auth.type", authorizationContextType.toString());
    }

    private static String determineChannel(final HttpRequest request) {
        // determine channel based on header or query parameter
        final boolean liveHeaderPresent = request.getHeader(DittoHeaderDefinition.CHANNEL.getKey())
                .filter(channelHeader -> LIVE_CHANNEL_NAME.equals(channelHeader.value()))
                .isPresent();
        final boolean liveQueryPresent = request.getUri().query().get(DittoHeaderDefinition.CHANNEL.getKey())
                .filter(LIVE_CHANNEL_NAME::equals)
                .isPresent();
        // messages are always live commands
        final String normalizePath = normalizePath(request.getUri().path());
        final boolean messageRequest = MESSAGE_PATTERN.matcher(normalizePath).matches();

        return liveHeaderPresent || liveQueryPresent || messageRequest ? LIVE_CHANNEL_NAME : TWIN_CHANNEL_NAME;
    }

    /**
     * Replaces all characters that are invalid for metrics (at least for Prometheus metrics).
     */
    public static String metricizeTraceUri(final String traceUri) {
        return traceUri.replaceAll("[./:-]", TRACING_FILTER_DELIMITER);
    }

    /**
     * Normalizes the path and removes duplicate slashes.
     */
    public static String normalizePath(final String path) {
        if (path.isEmpty()) {
            return SLASH;
        }

        // remove duplicate slashes
        String normalized = DUPLICATE_SLASH_PATTERN.matcher(path).replaceAll(SLASH);

        // strip trailing slash if necessary
        if (normalized.length() > 1 && normalized.endsWith(SLASH)) {
            normalized = normalized.substring(0, normalized.length() - 1);
        }

        // add leading slash if necessary
        if (!normalized.startsWith(SLASH)) {
            normalized = SLASH + normalized;
        }

        return normalized;
    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy