com.arpnetworking.metrics.mad.sources.MappingSource.java Source code

Java tutorial

Introduction

Here is the source code for com.arpnetworking.metrics.mad.sources.MappingSource.java

Source

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

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.common.sources.BaseSource;
import com.arpnetworking.metrics.common.sources.Source;
import com.arpnetworking.metrics.mad.model.DefaultMetric;
import com.arpnetworking.metrics.mad.model.DefaultRecord;
import com.arpnetworking.metrics.mad.model.Metric;
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.MetricType;
import com.arpnetworking.tsdcore.model.Quantity;
import com.google.common.base.MoreObjects;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import net.sf.oval.constraint.NotNull;

import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Implementation of <code>Source</code> which wraps another <code>Source</code>
 * and merges <code>Metric</code> instances within each <code>Record</code>
 * together if the name matches a regular expression with a new name generated
 * through replacement of all matches in the original name.
 *
 * @author Ville Koskela (ville dot koskela at inscopemetrics dot com)
 */
public final class MappingSource extends BaseSource {

    /**
     * {@inheritDoc}
     */
    @Override
    public void start() {
        _source.start();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void stop() {
        _source.stop();
    }

    /**
     * Generate a Steno log compatible representation.
     *
     * @return Steno log compatible representation.
     */
    @LogValue
    public Object toLogValue() {
        return LogValueMapFactory.builder(this).put("source", _source).put("findAndReplace", _findAndReplace)
                .build();
    }

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

    private MappingSource(final Builder builder) {
        super(builder);
        _source = builder._source;

        _findAndReplace = Maps.newHashMapWithExpectedSize(builder._findAndReplace.size());
        for (final Map.Entry<String, ? extends List<String>> entry : builder._findAndReplace.entrySet()) {
            _findAndReplace.put(Pattern.compile(entry.getKey()), ImmutableList.copyOf(entry.getValue()));
        }

        _source.attach(new MappingObserver(this, _findAndReplace));
    }

    private final Source _source;
    private final Map<Pattern, List<String>> _findAndReplace;

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

    // NOTE: Package private for testing
    /* package private */ static final class MappingObserver implements Observer {

        /* package private */ MappingObserver(final MappingSource source,
                final Map<Pattern, List<String>> findAndReplace) {
            _source = source;
            _findAndReplace = findAndReplace;
        }

        @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;
            }

            // Merge the metrics in the record together
            final Record record = (Record) event;
            final Map<String, MergingMetric> mergedMetrics = Maps.newHashMap();
            for (final Map.Entry<String, ? extends Metric> metric : record.getMetrics().entrySet()) {
                boolean found = false;
                for (final Map.Entry<Pattern, List<String>> findAndReplace : _findAndReplace.entrySet()) {
                    final Matcher matcher = findAndReplace.getKey().matcher(metric.getKey());
                    if (matcher.find()) {
                        for (final String replacement : findAndReplace.getValue()) {
                            merge(metric.getValue(), matcher.replaceAll(replacement), mergedMetrics);
                        }
                        //Having "found" set here means that mapping a metric to an empty list suppresses that metric
                        found = true;
                    }
                }
                if (!found) {
                    merge(metric.getValue(), metric.getKey(), mergedMetrics);
                }
            }

            // Raise the merged record event with this source's observers
            // NOTE: Do not leak instances of MergingMetric since it is mutable
            _source.notify(new DefaultRecord.Builder()
                    .setMetrics(ImmutableMap.copyOf(Maps.transformEntries(mergedMetrics,
                            (key, mergingMetric) -> OvalBuilder.clone(mergingMetric, new DefaultMetric.Builder())
                                    .build())))
                    .setId(record.getId()).setTime(record.getTime()).setAnnotations(record.getAnnotations())
                    .setDimensions(record.getDimensions()).build());
        }

        private void merge(final Metric metric, final String key, final Map<String, MergingMetric> mergedMetrics) {
            final MergingMetric mergedMetric = mergedMetrics.get(key);
            if (mergedMetric == null) {
                // This is the first time this metric is being merged into
                mergedMetrics.put(key, new MergingMetric(metric));
            } else if (!mergedMetric.isMergable(metric)) {
                // This instance of the metric is not mergable with previous
                LOGGER.error().setMessage("Discarding metric").addData("reason", "failed to merge")
                        .addData("metric", metric).addData("mergedMetric", mergedMetric).log();
            } else {
                // Merge the new instance in
                mergedMetric.merge(metric);
            }
        }

        private final MappingSource _source;
        private final Map<Pattern, List<String>> _findAndReplace;
    }

    // NOTE: Package private for testing
    /* package private */ static final class MergingMetric implements Metric {

        /* package private */ MergingMetric(final Metric metric) {
            _type = metric.getType();
            _values = Lists.newArrayList(metric.getValues());
        }

        public boolean isMergable(final Metric metric) {
            return _type.equals(metric.getType());
        }

        public void merge(final Metric metric) {
            if (!isMergable(metric)) {
                throw new IllegalArgumentException(String.format("Metric cannot be merged; metric=%s", metric));
            }
            _values.addAll(metric.getValues());
        }

        @Override
        public MetricType getType() {
            return _type;
        }

        @Override
        public List<Quantity> getValues() {
            return Collections.unmodifiableList(_values);
        }

        @Override
        public String toString() {
            return MoreObjects.toStringHelper(this).add("id", Integer.toHexString(System.identityHashCode(this)))
                    .add("Type", _type).add("Values", _values).toString();
        }

        private final MetricType _type;
        private final List<Quantity> _values;
    }

    /**
     * Implementation of builder pattern for <code>FileSource</code>.
     *
     * @author Ville Koskela (ville dot koskela at inscopemetrics dot com)
     */
    public static final class Builder extends BaseSource.Builder<Builder, MappingSource> {

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

        /**
         * Sets the underlying source. Cannot be null.
         *
         * @param value The underlying source.
         * @return This instance of <code>Builder</code>.
         */
        public Builder setSource(final Source value) {
            _source = value;
            return this;
        }

        /**
         * Sets find and replace expression map. Cannot be null.
         *
         * @param value The find and replace expression map.
         * @return This instance of <code>Builder</code>.
         */
        public Builder setFindAndReplace(final Map<String, ? extends List<String>> value) {
            _findAndReplace = value;
            return this;
        }

        /**
         * {@inheritDoc}
         */
        @Override
        protected Builder self() {
            return this;
        }

        @NotNull
        private Source _source;
        @NotNull
        private Map<String, ? extends List<String>> _findAndReplace;
    }
}