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

com.arpnetworking.clusteraggregator.http.Routes Maven / Gradle / Ivy

Go to download

(Re)Aggregates host level statistics across clusters and writes both host and cluster statistics to various destinations.

There is a newer version: 1.13.7
Show newest version
/*
 * Copyright 2014 Groupon.com
 *
 * 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 com.arpnetworking.clusteraggregator.http;

import akka.actor.ActorSystem;
import akka.cluster.Member;
import akka.http.javadsl.model.ContentType;
import akka.http.javadsl.model.ContentTypes;
import akka.http.javadsl.model.HttpHeader;
import akka.http.javadsl.model.HttpMethods;
import akka.http.javadsl.model.HttpRequest;
import akka.http.javadsl.model.HttpResponse;
import akka.http.javadsl.model.StatusCodes;
import akka.http.javadsl.model.headers.CacheControl;
import akka.http.javadsl.model.headers.CacheDirectives;
import akka.japi.function.Function;
import akka.pattern.PatternsCS;
import akka.util.ByteString;
import com.arpnetworking.clusteraggregator.Status;
import com.arpnetworking.clusteraggregator.models.StatusResponse;
import com.arpnetworking.clusteraggregator.models.VersionInfo;
import com.arpnetworking.commons.jackson.databind.ObjectMapperFactory;
import com.arpnetworking.configuration.jackson.akka.AkkaModule;
import com.arpnetworking.metrics.Metrics;
import com.arpnetworking.metrics.MetricsFactory;
import com.arpnetworking.metrics.Timer;
import com.arpnetworking.steno.LogBuilder;
import com.arpnetworking.steno.Logger;
import com.arpnetworking.steno.LoggerFactory;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.google.common.collect.ImmutableList;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.google.inject.name.Named;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;

import java.io.IOException;
import java.time.Duration;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;

/**
 * Http server routes.
 *
 * @author Ville Koskela (ville dot koskela at inscopemetrics dot com)
 */
@Singleton
public final class Routes implements Function> {

    /**
     * Public constructor.
     *
     * @param actorSystem Instance of ActorSystem.
     * @param metricsFactory Instance of MetricsFactory.
     * @param healthCheckPath The path for the health check.
     * @param statusPath The path for the status.
     * @param versionPath The path for the version.
     */
    @Inject
    public Routes(
            final ActorSystem actorSystem,
            final MetricsFactory metricsFactory,
            @Named("health-check-path")
            final String healthCheckPath,
            @Named("status-path")
            final String statusPath,
            @Named("version-path")
            final String versionPath) {
        _actorSystem = actorSystem;
        _metricsFactory = metricsFactory;
        _healthCheckPath = healthCheckPath;
        _statusPath = statusPath;
        _versionPath = versionPath;

        _objectMapper = ObjectMapperFactory.createInstance();
        _objectMapper.registerModule(new SimpleModule().addSerializer(Member.class, new MemberSerializer()));
        _objectMapper.registerModule(new AkkaModule(actorSystem));
    }

    @Override
    public CompletionStage apply(final HttpRequest request) {
        final Metrics metrics = _metricsFactory.create();
        final Timer timer = metrics.createTimer(createMetricName(request, REQUEST_METRIC));
        metrics.setGauge(
                createMetricName(request, BODY_SIZE_METRIC),
                request.entity().getContentLengthOption().orElse(0L));
        final UUID requestId = UUID.randomUUID();
        if (LOGGER.isTraceEnabled()) {
            LOGGER.trace()
                    .setEvent("http.in.start")
                    .addContext("requestId", requestId)
                    .addData("method", request.method().toString())
                    .addData("url", request.getUri().toString())
                    .addData(
                            "headers",
                            StreamSupport.stream(request.getHeaders().spliterator(), false)
                                    .map(h -> h.name() + "=" + h.value())
                                    .collect(Collectors.toList()))
                    .log();
        }
        return process(request).whenComplete(
                (response, failure) -> {
                    timer.close();

                    final int responseStatus;
                    if (response != null) {
                        responseStatus = response.status().intValue();
                    } else {
                        // TODO(ville): Figure out how to intercept post-exception mapping.
                        responseStatus = 599;
                    }
                    final int responseStatusClass = responseStatus / 100;
                    for (final int i : STATUS_CLASSES) {
                        metrics.incrementCounter(
                                createMetricName(request, String.format("%s/%dxx", STATUS_METRIC, i)),
                                responseStatusClass == i ? 1 : 0);
                    }
                    metrics.close();

                    final LogBuilder log;
                    if (failure != null || responseStatusClass == 5) {
                        log = LOGGER.info().setEvent("http.in.failure");
                        if (failure != null) {
                            log.setThrowable(failure);
                        }
                        if (!LOGGER.isTraceEnabled() && LOGGER.isInfoEnabled()) {
                            log.addData("method", request.method().toString())
                                    .addData("url", request.getUri().toString())
                                    .addData(
                                            "headers",
                                            StreamSupport.stream(request.getHeaders().spliterator(), false)
                                                    .map(h -> h.name() + "=" + h.value())
                                                    .collect(Collectors.toList()));
                        }
                    } else {
                        log = LOGGER.trace().setEvent("http.in.complete");
                    }
                    log.addContext("requestId", requestId)
                            .addData("status", responseStatus)
                            .log();
                });
    }

    private CompletionStage process(final HttpRequest request) {
        if (HttpMethods.GET.equals(request.method())) {
            if (_healthCheckPath.equals(request.getUri().path())) {
                return ask("/user/status", new Status.HealthRequest(), Boolean.FALSE)
                        .thenApply(
                                isHealthy -> HttpResponse.create()
                                        .withStatus(isHealthy ? StatusCodes.OK : StatusCodes.INTERNAL_SERVER_ERROR)
                                        .addHeader(PING_CACHE_CONTROL_HEADER)
                                        .withEntity(
                                                JSON_CONTENT_TYPE,
                                                ByteString.fromString(
                                                        "{\"status\":\""
                                                                + (isHealthy ? HEALTHY_STATE : UNHEALTHY_STATE)
                                                                + "\"}")));
            } else if (_statusPath.equals(request.getUri().path())) {
                return ask("/user/status", new Status.StatusRequest(), (StatusResponse) null)
                        .thenApply(
                                status -> {
                                    try {
                                        return HttpResponse.create()
                                                .withEntity(
                                                        JSON_CONTENT_TYPE,
                                                        ByteString.fromString(_objectMapper.writeValueAsString(status)));
                                    } catch (final IOException e) {
                                        LOGGER.error()
                                                .setMessage("Failed to serialize status")
                                                .setThrowable(e)
                                                .log();
                                        return HttpResponse.create().withStatus(StatusCodes.INTERNAL_SERVER_ERROR);
                                    }
                                });
            } else if (_versionPath.equals(request.getUri().path())) {
                    return CompletableFuture.supplyAsync(() -> {
                            try {
                                return HttpResponse.create()
                                        .withEntity(
                                                JSON_CONTENT_TYPE,
                                                ByteString.fromString(_objectMapper.writeValueAsString(VersionInfo.getInstance())));
                            } catch (final IOException e) {
                                LOGGER.error()
                                        .setMessage("Failed to serialize version")
                                        .setThrowable(e)
                                        .log();
                                return HttpResponse.create().withStatus(StatusCodes.INTERNAL_SERVER_ERROR);
                            }
                    });
            }
        } else if (HttpMethods.POST.equals(request.method())) {
            if (INCOMING_DATA_V1_PATH.equals(request.getUri().path())
                    || INCOMING_DATA_PERSIST_V1_PATH.equals(request.getUri().path())
                    || INCOMING_DATA_REAGGREGATE_V1_PATH.equals(request.getUri().path())) {
                return ask("/user/http-ingest-v1", request, HttpResponse.create().withStatus(500));
            }
        }
        return CompletableFuture.completedFuture(HttpResponse.create().withStatus(404));
    }

    @SuppressWarnings("unchecked")
    private  CompletionStage ask(final String actorPath, final Object request, final T defaultValue) {
        return
                PatternsCS.ask(
                        _actorSystem.actorSelection(actorPath),
                        request,
                        Duration.ofSeconds(5))
                .thenApply(o -> (T) o)
                .exceptionally(throwable -> {
                    LOGGER.error()
                            .setMessage("error when routing ask")
                            .addData("actorPath", actorPath)
                            .addData("request", request)
                            .setThrowable(throwable)
                            .log();
                    return defaultValue;
                });
    }

    private String createMetricName(final HttpRequest request, final String actionPart) {
        final StringBuilder nameBuilder = new StringBuilder()
                .append(REST_SERVICE_METRIC_ROOT)
                .append(request.method().value());
        if (!request.getUri().path().startsWith("/")) {
            nameBuilder.append("/");
        }
        nameBuilder.append(request.getUri().path());
        nameBuilder.append("/");
        nameBuilder.append(actionPart);
        return nameBuilder.toString();
    }

    @SuppressFBWarnings("SE_BAD_FIELD")
    private final ActorSystem _actorSystem;
    @SuppressFBWarnings("SE_BAD_FIELD")
    private final MetricsFactory _metricsFactory;
    private final String _healthCheckPath;
    private final String _statusPath;
    private final String _versionPath;
    private final ObjectMapper _objectMapper;

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

    private static final HttpHeader PING_CACHE_CONTROL_HEADER = CacheControl.create(
            CacheDirectives.PRIVATE(),
            CacheDirectives.NO_CACHE,
            CacheDirectives.NO_STORE,
            CacheDirectives.MUST_REVALIDATE);
    private static final String UNHEALTHY_STATE = "UNHEALTHY";
    private static final String HEALTHY_STATE = "HEALTHY";
    private static final String REST_SERVICE_METRIC_ROOT = "rest_service/";
    private static final String BODY_SIZE_METRIC = "body_size";
    private static final String REQUEST_METRIC = "request";
    private static final String STATUS_METRIC = "status";
    private static final ImmutableList STATUS_CLASSES = ImmutableList.of(2, 3, 4, 5);

    private static final ContentType JSON_CONTENT_TYPE = ContentTypes.APPLICATION_JSON;

    private static final long serialVersionUID = -1573473630801540757L;
    
    /**
     * The legacy path for incoming data over HTTP. The reaggregation behavior
     * on this path is controlled by the the {@code calculateClusterAggregations}
     * value. If this is set to {@code False} this endpoint behaves with
     * {@link com.arpnetworking.clusteraggregator.models.AggregationMode} set to
     * {@code PERSIST}. If the configuration key is set to {@code True} this endpoint
     * behaves with {@link com.arpnetworking.clusteraggregator.models.AggregationMode}
     * set to {@code PERSIST_AND_REAGGREGATE}.
     */
    public static final String INCOMING_DATA_V1_PATH = "/metrics/v1/data";

    /**
     * This HTTP endpoint is for only persisting data directly and through the
     * host emitter.
     */
    public static final String INCOMING_DATA_PERSIST_V1_PATH = "/metrics/v1/data/persist";

    /**
     * This HTTP endpoint is for only reaggregating data and then persisting
     * through the cluster emitter.
     */
    public static final String INCOMING_DATA_REAGGREGATE_V1_PATH = "/metrics/v1/data/reaggregate";

    private static class MemberSerializer extends JsonSerializer {
        @Override
        public void serialize(
                final Member value,
                final JsonGenerator jgen,
                final SerializerProvider provider) throws IOException {
            jgen.writeStartObject();
            jgen.writeObjectField("address", value.address().toString());
            jgen.writeObjectField("roles", value.getRoles().toArray());
            jgen.writeEndObject();
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy