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

com.arpnetworking.metrics.mad.sources.MappingSource Maven / Gradle / Ivy

There is a newer version: 1.22.6
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.metrics.mad.sources;

import com.arpnetworking.commons.builder.ThreadLocalBuilder;
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.MetricType;
import com.arpnetworking.metrics.mad.model.Quantity;
import com.arpnetworking.metrics.mad.model.Record;
import com.arpnetworking.metrics.mad.model.statistics.Statistic;
import com.arpnetworking.steno.LogValueMapFactory;
import com.arpnetworking.steno.Logger;
import com.arpnetworking.steno.LoggerFactory;
import com.arpnetworking.tsdcore.model.CalculatedValue;
import com.google.common.base.MoreObjects;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import net.sf.oval.constraint.NotNull;

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

/**
 * Implementation of {@link Source} which wraps another {@link Source}
 * and merges {@link Metric} instances within each {@link Record}
 * 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 io)
 */
public final class MappingSource extends BaseSource {

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

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

    @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> 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> _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> 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 mergedMetrics = Maps.newHashMap();
            for (final Map.Entry metric : record.getMetrics().entrySet()) {
                boolean found = false;
                for (final Map.Entry> 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(
                    ThreadLocalBuilder.build(
                            DefaultRecord.Builder.class,
                            b1 -> b1.setMetrics(
                                    mergedMetrics.entrySet().stream().collect(
                                            ImmutableMap.toImmutableMap(
                                                    Map.Entry::getKey,
                                                    e -> ThreadLocalBuilder.clone(
                                                            e.getValue(),
                                                            DefaultMetric.Builder.class))))
                                    .setId(record.getId())
                                    .setTime(record.getTime())
                                    .setAnnotations(record.getAnnotations())
                                    .setDimensions(record.getDimensions())));
        }

        private void merge(final Metric metric, final String key, final Map 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> _findAndReplace;
    }

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

        /* package private */ MergingMetric(final Metric metric) {
            _type = metric.getType();
            _values.addAll(metric.getValues());
            for (final Map.Entry>> entry : metric.getStatistics().entrySet()) {
                _statistics.computeIfAbsent(entry.getKey(), k-> ImmutableList.builder()).addAll(entry.getValue());
            }
        }

        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());
            for (final Map.Entry>> entry : metric.getStatistics().entrySet()) {
                _statistics.computeIfAbsent(entry.getKey(), k-> ImmutableList.builder()).addAll(entry.getValue());
            }
        }

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

        @Override
        public ImmutableList getValues() {
            return _values.build();
        }

        @Override
        public ImmutableMap>> getStatistics() {
            return _statistics.entrySet().stream().collect(
                    ImmutableMap.toImmutableMap(
                            Map.Entry::getKey,
                            entry -> entry.getValue().build()));
        }

        @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 ImmutableList.Builder _values = ImmutableList.builder();
        private final Map>> _statistics = Maps.newHashMap();
    }

    /**
     * Implementation of builder pattern for {@link MappingSource}.
     *
     * @author Ville Koskela (ville dot koskela at inscopemetrics dot io)
     */
    public static final class Builder extends BaseSource.Builder {

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

        /**
         * Sets the underlying source. Cannot be null.
         *
         * @param value The underlying source.
         * @return This instance of {@link Builder}.
         */
        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 {@link Builder}.
         */
        public Builder setFindAndReplace(final Map> value) {
            _findAndReplace = value;
            return this;
        }

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

        @NotNull
        private Source _source;
        @NotNull
        private Map> _findAndReplace;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy