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

com.arpnetworking.metrics.common.sources.HttpSource Maven / Gradle / Ivy

/*
 * Copyright 2016 Inscope Metrics, 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
 *
 *     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.metrics.common.sources;

import akka.Done;
import akka.NotUsed;
import akka.actor.AbstractActor;
import akka.actor.Props;
import akka.http.javadsl.model.HttpHeader;
import akka.http.javadsl.model.HttpRequest;
import akka.http.javadsl.model.HttpResponse;
import akka.http.javadsl.model.RequestEntity;
import akka.japi.Pair;
import akka.stream.ActorMaterializer;
import akka.stream.ActorMaterializerSettings;
import akka.stream.FanInShape2;
import akka.stream.FlowShape;
import akka.stream.Graph;
import akka.stream.Materializer;
import akka.stream.Supervision;
import akka.stream.UniformFanOutShape;
import akka.stream.javadsl.Broadcast;
import akka.stream.javadsl.Flow;
import akka.stream.javadsl.GraphDSL;
import akka.stream.javadsl.Keep;
import akka.stream.javadsl.Sink;
import akka.stream.javadsl.Zip;
import akka.util.ByteString;
import com.arpnetworking.http.RequestReply;
import com.arpnetworking.metrics.common.parsers.Parser;
import com.arpnetworking.metrics.common.parsers.exceptions.ParsingException;
import com.arpnetworking.metrics.mad.model.Record;
import com.arpnetworking.steno.Logger;
import com.arpnetworking.steno.LoggerFactory;
import com.google.common.collect.ImmutableMultimap;
import net.sf.oval.constraint.NotNull;

import java.time.Duration;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.function.Function;

/**
 * Source that uses HTTP POSTs as input.
 *
 * @author Brandon Arp (brandon dot arp at inscopemetrics dot io)
 */
public class HttpSource extends ActorSource {

    @Override
    protected Props createProps() {
        return Actor.props(this);
    }

    /**
     * Protected constructor.
     *
     * @param builder Instance of Builder.
     */
    protected HttpSource(final Builder builder) {
        super(builder);
        _parser = builder._parser;
    }

    private final Parser, com.arpnetworking.metrics.mad.model.HttpRequest> _parser;

    /**
     * Internal actor to process requests.
     */
    /* package private */ static final class Actor extends AbstractActor {
        /**
         * Creates a {@link Props} for this actor.
         *
         * @param source The {@link HttpSource} to send notifications through.
         * @return A new {@link Props}
         */
        /* package private */ static Props props(final HttpSource source) {
            return Props.create(Actor.class, source);
        }

        @Override
        public Receive createReceive() {
            return receiveBuilder()
                    .match(RequestReply.class, requestReply -> {
                        // TODO(barp): Fix the ugly HttpRequest cast here due to java vs scala dsl
                        akka.stream.javadsl.Source.single(requestReply.getRequest())
                                .via(_processGraph)
                                .toMat(_sink, Keep.right())
                                .run(_materializer)
                                .whenComplete((done, err) -> {
                                    final CompletableFuture responseFuture = requestReply.getResponse();
                                    if (err == null) {
                                        responseFuture.complete(HttpResponse.create().withStatus(200));
                                    } else {
                                        BAD_REQUEST_LOGGER.warn()
                                                .setMessage("Error handling http post")
                                                .setThrowable(err)
                                                .log();
                                        if (err instanceof ParsingException) {
                                            responseFuture.complete(HttpResponse.create().withStatus(400));
                                        } else {
                                            responseFuture.complete(HttpResponse.create().withStatus(500));
                                        }
                                    }
                                });
                    })
                    .build();
        }

        /**
         * Constructor.
         *
         * @param source The {@link HttpSource} to send notifications through.
         */
        /* package private */ Actor(final HttpSource source) {
            _parser = source._parser;
            _sink = Sink.foreach(source::notify);
            _materializer = ActorMaterializer.create(
                    ActorMaterializerSettings.create(context().system())
                            .withSupervisionStrategy(Supervision.stoppingDecider()),
                    context());

            _processGraph = GraphDSL.create(builder -> {

                // Flows
                final Flow getBodyFlow = Flow.create()
                        .map(HttpRequest::entity)
                        .flatMapConcat(RequestEntity::getDataBytes)
                        .reduce(ByteString::concat)
                        .named("getBody");

                final Flow, NotUsed> getHeadersFlow = Flow.create()
                        .map(HttpRequest::getHeaders)
                        .map(Actor::createHeaderMultimap) // Transform to array form
                        .named("getHeaders");

                final Flow>, Record, NotUsed> createAndParseFlow =
                        Flow.>>create()
                                .map(Actor::mapModel)
                                .mapConcat(this::parseRecords) // Parse the json string into a record builder
                                // NOTE: this should be _parser::parse, but aspectj NPEs with that currently
                                .named("createAndParseRequest");

                // Shapes
                final UniformFanOutShape split = builder.add(Broadcast.create(2));

                final FlowShape getBody = builder.add(getBodyFlow);
                final FlowShape> getHeaders = builder.add(getHeadersFlow);
                final FanInShape2<
                        ByteString,
                        ImmutableMultimap,
                        Pair>> join = builder.add(Zip.create());
                final FlowShape>, Record> createRequest =
                        builder.add(createAndParseFlow);

                // Wire the shapes
                builder.from(split.out(0)).via(getBody).toInlet(join.in0()); // Split to get the body bytes
                builder.from(split.out(1)).via(getHeaders).toInlet(join.in1()); // Split to get the headers
                builder.from(join.out()).toInlet(createRequest.in()); // Join to create the Request and parse it

                return new FlowShape<>(split.in(), createRequest.out());
            });
        }

        private static ImmutableMultimap createHeaderMultimap(final Iterable headers) {
            final ImmutableMultimap.Builder headersBuilder = ImmutableMultimap.builder();

            for (final HttpHeader httpHeader : headers) {
                headersBuilder.put(httpHeader.lowercaseName(), httpHeader.value());
            }

            return headersBuilder.build();
        }

        private static com.arpnetworking.metrics.mad.model.HttpRequest mapModel(
                final Pair> pair) {
            return new com.arpnetworking.metrics.mad.model.HttpRequest(pair.second(), pair.first());
        }

        private List parseRecords(final com.arpnetworking.metrics.mad.model.HttpRequest request)
                throws ParsingException {
            return _parser.parse(request);
        }

        private final Sink> _sink;
        private final Parser, com.arpnetworking.metrics.mad.model.HttpRequest> _parser;
        private final Materializer _materializer;
        private final Graph, NotUsed> _processGraph;

        private static final Logger BAD_REQUEST_LOGGER =
                LoggerFactory.getRateLimitLogger(HttpSource.class, Duration.ofSeconds(30));
    }

    /**
     * HttpSource {@link BaseSource.Builder} implementation.
     *
     * @param  type of the builder
     * @param  type of the object to be built
     *
     * @author Brandon Arp (brandon dot arp at smartsheet dot com)
     */
    public abstract static class Builder, S extends HttpSource> extends ActorSource.Builder {

        /**
         * Protected constructor for subclasses.
         *
         * @param targetConstructor The constructor for the concrete type to be created by this builder.
         */
        protected Builder(final Function targetConstructor) {
            super(targetConstructor);
        }

        /**
         * Sets the parser to use to parse the data. Required. Cannot be null.
         *
         * @param value Value
         * @return This builder
         */
        public B setParser(final Parser, com.arpnetworking.metrics.mad.model.HttpRequest> value) {
            _parser = value;
            return self();
        }

        @NotNull
        private Parser, com.arpnetworking.metrics.mad.model.HttpRequest> _parser;
    }
}