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

com.arpnetworking.metrics.mad.Aggregator Maven / Gradle / Ivy

/*
 * Copyright 2015 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.metrics.mad;

import com.arpnetworking.commons.builder.OvalBuilder;
import com.arpnetworking.commons.observer.Observable;
import com.arpnetworking.commons.observer.Observer;
import com.arpnetworking.logback.annotations.LogValue;
import com.arpnetworking.metrics.mad.model.Record;
import com.arpnetworking.steno.LogValueMapFactory;
import com.arpnetworking.steno.Logger;
import com.arpnetworking.steno.LoggerFactory;
import com.arpnetworking.tsdcore.model.DefaultKey;
import com.arpnetworking.tsdcore.model.Key;
import com.arpnetworking.tsdcore.sinks.Sink;
import com.arpnetworking.tsdcore.statistics.Statistic;
import com.arpnetworking.utility.Launchable;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import net.sf.oval.constraint.NotNull;

import java.time.Duration;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;

/**
 * Performs aggregation of Record instances per Period.
 * This class is thread safe.
 *
 * @author Ville Koskela (ville dot koskela at inscopemetrics dot com)
 * @author Ryan Ascheman (rascheman at groupon dot com)
 */
// NOTE: The _periodWorkerExecutor is accessed both in synchronized lifecycle methods like launch() and shutdown() but
// also non-synchronized methods like notify(). Access to _periodWorkerExecutor does not need to be synchronized.
@SuppressFBWarnings("IS2_INCONSISTENT_SYNC")
public final class Aggregator implements Observer, Launchable {

    @Override
    public synchronized void launch() {
        LOGGER.debug()
                .setMessage("Launching aggregator")
                .addData("aggregator", this)
                .log();

        _periodWorkers.clear();
        if (!_periods.isEmpty()) {
            _periodWorkerExecutor = Executors.newCachedThreadPool(r -> new Thread(r, "PeriodWorker"));
        }
    }

    @Override
    public synchronized void shutdown() {
        LOGGER.debug()
                .setMessage("Stopping aggregator")
                .addData("aggregator", this)
                .log();

        for (final List periodCloserList : _periodWorkers.values()) {
            periodCloserList.forEach(com.arpnetworking.metrics.mad.PeriodWorker::shutdown);
        }
        _periodWorkers.clear();
        if (_periodWorkerExecutor != null) {
            _periodWorkerExecutor.shutdown();
            try {
                _periodWorkerExecutor.awaitTermination(10, TimeUnit.SECONDS);
            } catch (final InterruptedException e) {
                LOGGER.warn("Unable to shutdown period worker executor", e);
            }
            _periodWorkerExecutor = null;
        }
    }

    @Override
    public void notify(final Observable observable, final Object event) {
        if (!(event instanceof Record)) {
            LOGGER.error()
                    .setMessage("Observed unsupported event")
                    .addData("event", event)
                    .log();
            return;
        }

        final Record record = (Record) event;
        final Key key = new DefaultKey(record.getDimensions());
        LOGGER.trace()
                .setMessage("Processing record")
                .addData("record", record)
                .addData("key", key)
                .log();
        for (final PeriodWorker periodWorker : _periodWorkers.computeIfAbsent(key, this::createPeriodWorkers)) {
            periodWorker.record(record);
        }
    }

    /**
     * Generate a Steno log compatible representation.
     *
     * @return Steno log compatible representation.
     */
    @LogValue
    public Object toLogValue() {
        return LogValueMapFactory.builder(this)
                .put("sink", _sink)
                .put("timerStatistics", _specifiedTimerStatistics)
                .put("counterStatistics", _specifiedCounterStatistics)
                .put("gaugeStatistics", _specifiedGaugeStatistics)
                .put("periodWorkers", _periodWorkers)
                .build();
    }

    @Override
    public String toString() {
        return toLogValue().toString();
    }

    private List createPeriodWorkers(final Key key) {
        final List periodWorkerList = Lists.newArrayListWithExpectedSize(_periods.size());
        for (final Duration period : _periods) {
            final PeriodWorker periodWorker = new PeriodWorker.Builder()
                    .setPeriod(period)
                    .setBucketBuilder(
                            new Bucket.Builder()
                                    .setKey(key)
                                    .setSpecifiedCounterStatistics(_specifiedCounterStatistics)
                                    .setSpecifiedGaugeStatistics(_specifiedGaugeStatistics)
                                    .setSpecifiedTimerStatistics(_specifiedTimerStatistics)
                                    .setDependentCounterStatistics(_dependentCounterStatistics)
                                    .setDependentGaugeStatistics(_dependentGaugeStatistics)
                                    .setDependentTimerStatistics(_dependentTimerStatistics)
                                    .setSpecifiedStatistics(_cachedSpecifiedStatistics)
                                    .setDependentStatistics(_cachedDependentStatistics)
                                    .setPeriod(period)
                                    .setSink(_sink))
                    .build();
            periodWorkerList.add(periodWorker);
            _periodWorkerExecutor.execute(periodWorker);
        }
        LOGGER.debug()
                .setMessage("Created period workers")
                .addData("key", key)
                .addData("periodWorkersSize", periodWorkerList.size())
                .log();
        return periodWorkerList;
    }

    private ImmutableSet computeDependentStatistics(final ImmutableSet statistics) {
        final ImmutableSet.Builder builder = ImmutableSet.builder();
        for (final Statistic statistic : statistics) {
            statistic.getDependencies().stream().filter(dependency -> !statistics.contains(dependency)).forEach(builder::add);
        }
        return builder.build();
    }

    private Aggregator(final Builder builder) {
        _periods = ImmutableSet.copyOf(builder._periods);
        _sink = builder._sink;
        _specifiedCounterStatistics = ImmutableSet.copyOf(builder._counterStatistics);
        _specifiedGaugeStatistics = ImmutableSet.copyOf(builder._gaugeStatistics);
        _specifiedTimerStatistics = ImmutableSet.copyOf(builder._timerStatistics);
        _dependentCounterStatistics = computeDependentStatistics(_specifiedCounterStatistics);
        _dependentGaugeStatistics = computeDependentStatistics(_specifiedGaugeStatistics);
        _dependentTimerStatistics = computeDependentStatistics(_specifiedTimerStatistics);
        final ImmutableMap.Builder> statisticsBuilder = ImmutableMap.builder();
        for (final Map.Entry> entry : builder._statistics.entrySet()) {
            final Pattern pattern = Pattern.compile(entry.getKey());
            final ImmutableSet statistics = ImmutableSet.copyOf(entry.getValue());
            statisticsBuilder.put(pattern, statistics);
        }
        _statistics = statisticsBuilder.build();

        _cachedSpecifiedStatistics = CacheBuilder
                .newBuilder()
                .concurrencyLevel(1)
                .build(
                        new CacheLoader>>() {
                            // TODO(vkoskela): Add @NonNull annotation to metric. [ISSUE-?]
                            @Override
                            public Optional> load(final String metric) throws Exception {
                                for (final Map.Entry> entry : _statistics.entrySet()) {
                                    final Pattern pattern = entry.getKey();
                                    final ImmutableSet statistics = entry.getValue();
                                    if (pattern.matcher(metric).matches()) {
                                        return Optional.of(statistics);
                                    }
                                }
                                return Optional.empty();
                            }
                        });
        _cachedDependentStatistics = CacheBuilder
                .newBuilder()
                .concurrencyLevel(1)
                .build(new CacheLoader>>() {
                            // TODO(vkoskela): Add @NonNull annotation to metric. [ISSUE-?]
                            @Override
                            public Optional> load(final String metric) throws Exception {
                                final Optional> statistics = _cachedSpecifiedStatistics.get(metric);
                                if (statistics.isPresent()) {
                                   return Optional.of(computeDependentStatistics(statistics.get()));
                                } else {
                                   return Optional.empty();
                                }
                           }
                        });
}

    private final ImmutableSet _periods;
    private final Sink _sink;
    private final ImmutableSet _specifiedTimerStatistics;
    private final ImmutableSet _specifiedCounterStatistics;
    private final ImmutableSet _specifiedGaugeStatistics;
    private final ImmutableSet _dependentTimerStatistics;
    private final ImmutableSet _dependentCounterStatistics;
    private final ImmutableSet _dependentGaugeStatistics;
    private final ImmutableMap> _statistics;
    private final LoadingCache>> _cachedSpecifiedStatistics;
    private final LoadingCache>> _cachedDependentStatistics;
    private final Map> _periodWorkers = Maps.newConcurrentMap();

    private ExecutorService _periodWorkerExecutor = null;

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

    /**
     * Builder implementation for Aggregator.
     */
    public static final class Builder extends OvalBuilder {

        /**
         * Public constructor.
         */
        public Builder() {
            super(Aggregator::new);
        }

        /**
         * Set the sink. Cannot be null or empty.
         *
         * @param value The sink.
         * @return This Builder instance.
         */
        public Builder setSink(final Sink value) {
            _sink = value;
            return this;
        }

        /**
         * Set the periods. Cannot be null or empty.
         *
         * @param value The periods.
         * @return This Builder instance.
         */
        public Builder setPeriods(final Set value) {
            _periods = value;
            return this;
        }

        /**
         * Set the timer statistics. Cannot be null or empty.
         *
         * @param value The timer statistics.
         * @return This Builder instance.
         */
        public Builder setTimerStatistics(final Set value) {
            _timerStatistics = value;
            return this;
        }

        /**
         * Set the counter statistics. Cannot be null or empty.
         *
         * @param value The counter statistics.
         * @return This Builder instance.
         */
        public Builder setCounterStatistics(final Set value) {
            _counterStatistics = value;
            return this;
        }

        /**
         * Set the gauge statistics. Cannot be null or empty.
         *
         * @param value The gauge statistics.
         * @return This Builder instance.
         */
        public Builder setGaugeStatistics(final Set value) {
            _gaugeStatistics = value;
            return this;
        }

        /**
         * The statistics to compute for a metric pattern. Optional. Cannot be null.
         * Default is empty.
         *
         * @param value The gauge statistics.
         * @return This instance of Builder.
         */
        public Builder setStatistics(final Map> value) {
            _statistics = value;
            return this;
        }

        @NotNull
        private Sink _sink;
        @NotNull
        private Set _periods;
        @NotNull
        private Set _timerStatistics;
        @NotNull
        private Set _counterStatistics;
        @NotNull
        private Set _gaugeStatistics;
        @NotNull
        private Map> _statistics = Collections.emptyMap();
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy