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

com.arpnetworking.metrics.impl.TsdMetricsFactory Maven / Gradle / Ivy

There is a newer version: 0.13.0
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.impl;

import com.arpnetworking.commons.hostresolver.BackgroundCachingHostResolver;
import com.arpnetworking.commons.hostresolver.HostResolver;
import com.arpnetworking.commons.uuidfactory.SplittableRandomUuidFactory;
import com.arpnetworking.commons.uuidfactory.UuidFactory;
import com.arpnetworking.metrics.Metrics;
import com.arpnetworking.metrics.MetricsFactory;
import com.arpnetworking.metrics.Sink;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.reflect.Method;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.function.Supplier;
import javax.annotation.Nullable;

/**
 * Default implementation of {@link MetricsFactory} for creating
 * {@link Metrics} instances for publication of time series data (TSD).
 *
 * For more information about the semantics of this class and its methods
 * please refer to the {@link MetricsFactory} interface documentation.
 *
 * The simplest way to create an instance of this class is to use the
 * {@link TsdMetricsFactory#newInstance(String, String)} static factory method.
 * This method will use default settings where possible.
 *
 * {@code
 * final MetricsFactory metricsFactory = TsdMetricsFactory.newInstance(
 *     "MyService",
 *     "MyService-US-Prod");
 * }
 *
 * To customize the factory instance use the nested {@link Builder} class:
 *
 * {@code
 * final MetricsFactory metricsFactory = new TsdMetricsFactory.Builder()
 *     .setServiceName("MyService")
 *     .setClusterName("MyService-US-Prod")
 *     .setSinks(Collections.singletonList(
 *         new ApacheHttpSink.Builder().build()));
 *     .build();
 * }
 *
 * The above will write metrics to http://localhost:7090/metrics/v1/application.
 * This is the default port and path of the Metrics Aggregator Daemon (MAD). It
 * is sometimes desirable to customize this path; for example, when running MAD
 * under Docker:
 *
 * {@code
 * final MetricsFactory metricsFactory = new TsdMetricsFactory.Builder()
 *     .setServiceName("MyService")
 *     .setClusterName("MyService-US-Prod")
 *     .setSinks(Collections.singletonList(
 *         new ApacheHttpSink.Builder()
 *             .setUri(URI.create("http://192.168.0.1:1234/metrics/v1/application"))
 *             .build()));
 *     .build();
 * }
 *
 * Alternatively, metrics may be written to a file:
 *
 * {@code
 * final MetricsFactory metricsFactory = new TsdMetricsFactory.Builder()
 *     .setServiceName("MyService")
 *     .setClusterName("MyService-US-Prod")
 *     .setSinks(Collections.singletonList(
 *         new FileLogSink.Builder().build()));
 *     .build();
 * }
 *
 * The above will write metrics to query.log in the current directory. It is
 * advised that at least the directory be set when using the FileLogSink:
 *
 * {@code
 * final MetricsFactory metricsFactory = new TsdMetricsFactory.Builder()
 *     .setServiceName("MyService")
 *     .setClusterName("MyService-US-Prod")
 *     .setSinks(Collections.singletonList(
 *         new FileLogSink.Builder()
 *             .setDirectory("/usr/local/var/my-app/logs")
 *             .build()));
 *     .build();
 * }
 *
 * The above will write metrics to /usr/local/var/my-app/logs in query.log.
 * Additionally, you can customize the base file name and extension for your
 * application. However, if you are using MAD remember to configure it to
 * match:
 *
 * {@code
 * final MetricsFactory metricsFactory = new TsdMetricsFactory.Builder()
 *     .setServiceName("MyService")
 *     .setClusterName("MyService-US-Prod")
 *     .setSinks(Collections.singletonList(
 *         new StenoLogSink.Builder()
 *             .setDirectory("/usr/local/var/my-app/logs")
 *             .setName("tsd")
 *             .setExtension(".txt")
 *             .build()));
 *     .build();
 * }
 *
 * The above will write metrics to /usr/local/var/my-app/logs in tsd.txt. The
 * extension is configured separately as the files are rolled over every hour
 * inserting a date-time between the name and extension like:
 *
 * query-log.YYYY-MM-DD-HH.log
 *
 * This class is thread safe.
 *
 * @author Ville Koskela (ville dot koskela at inscopemetrics dot io)
 */
public class TsdMetricsFactory implements MetricsFactory {

    /**
     * Static factory. Construct an instance of {@link TsdMetricsFactory}
     * using the first available default {@link Sink}.
     *
     * @param serviceName The name of the service/application publishing metrics.
     * @param clusterName The name of the cluster (e.g. instance) publishing metrics.
     * @return Instance of {@link TsdMetricsFactory}.
     */
    public static MetricsFactory newInstance(
            final String serviceName,
            final String clusterName) {
        return new Builder()
                .setClusterName(clusterName)
                .setServiceName(serviceName)
                .build();
    }

    @Override
    public Metrics create() {
        final UUID uuid = _uuidFactory.get();
        try {
            return new TsdMetrics(
                    uuid,
                    _serviceName,
                    _clusterName,
                    _hostResolver.get(),
                    _sinks);
            // CHECKSTYLE.OFF: IllegalCatch - Suppliers do not throw checked exceptions
        } catch (final RuntimeException e) {
            // CHECKSTYLE.ON: IllegalCatch
            final List failures = Collections.singletonList("Unable to determine hostname");
            _logger.warn(
                    String.format(
                            "Unable to construct TsdMetrics, metrics disabled; failures=%s",
                            failures),
                    e);
            return new TsdMetrics(
                    uuid,
                    _serviceName,
                    _clusterName,
                    DEFAULT_HOST_NAME,
                    Collections.singletonList(
                            new WarningSink.Builder()
                                    .setReasons(failures)
                                    .build()));
        }
    }

    @Override
    public String toString() {
        return String.format(
                "TsdMetricsFactory{Sinks=%s, ServiceName=%s, ClusterName=%s, HostResolver=%s}",
                _sinks,
                _serviceName,
                _clusterName,
                _hostResolver);
    }

    /* package private */ List getSinks() {
        return Collections.unmodifiableList(_sinks);
    }

    /* package private */ String getServiceName() {
        return _serviceName;
    }

    /* package private */ Supplier getHostResolver() {
        return _hostResolver;
    }

    /* package private */ Supplier getUuidFactory() {
        return _uuidFactory;
    }

    /* package private */ String getClusterName() {
        return _clusterName;
    }

    /* package private */ static @Nullable List createDefaultSinks(final List defaultSinkClassNames) {
        for (final String sinkClassName : defaultSinkClassNames) {
            final Optional> sinkClass = getSinkClass(sinkClassName);
            if (sinkClass.isPresent()) {
                final Optional sink = createSink(sinkClass.get());
                if (sink.isPresent()) {
                    return Collections.singletonList(sink.get());
                }
            }
        }

        return Collections.unmodifiableList(
                Collections.singletonList(
                        new WarningSink.Builder()
                                .setReasons(Collections.singletonList("No default sink found."))
                                .build()));
    }

    @SuppressWarnings("unchecked")
    @SuppressFBWarnings("REC_CATCH_EXCEPTION")
    /* package private */ static Optional createSink(final Class sinkClass) {
        try {
            final Class sinkBuilderClass = Class.forName(sinkClass.getName() + "$Builder");
            final Object sinkBuilder = sinkBuilderClass.newInstance();
            final Method buildMethod = sinkBuilderClass.getMethod("build");
            return Optional.of((Sink) buildMethod.invoke(sinkBuilder));
            // CHECKSTYLE.OFF: IllegalCatch - Much cleaner than catching the half-dozen checked exceptions
        } catch (final Exception e) {
            // CHECKSTYLE.ON: IllegalCatch
            LOGGER.warn(
                    String.format(
                            "Unable to load sink; sinkClass=%s",
                            sinkClass),
                    e);
            return Optional.empty();
        }
    }

    @SuppressWarnings("unchecked")
    /* package private */ static Optional> getSinkClass(final String name) {
        try {
            return Optional.of((Class) Class.forName(name));
        } catch (final ClassNotFoundException e) {
            return Optional.empty();
        }
    }

    /**
     * Protected constructor.
     *
     * @param builder Instance of {@link Builder}.
     */
    protected TsdMetricsFactory(final Builder builder) {
        this(builder, LOGGER);
    }

    /**
     * Protected constructor.
     *
     * @param builder Instance of {@link Builder}.
     */
    /* package private */ TsdMetricsFactory(final Builder builder, final Logger logger) {
        _sinks = Collections.unmodifiableList(new ArrayList<>(builder._sinks));
        _uuidFactory = builder._uuidFactory;
        _serviceName = builder._serviceName;
        _clusterName = builder._clusterName;
        _hostResolver = builder._hostResolver;
        _logger = logger;
    }

    private final List _sinks;
    private final Supplier _uuidFactory;
    private final String _serviceName;
    private final String _clusterName;
    private final Supplier _hostResolver;
    private final Logger _logger;

    private static final String DEFAULT_SERVICE_NAME = "";
    private static final String DEFAULT_CLUSTER_NAME = "";
    private static final String DEFAULT_HOST_NAME = "";
    private static final List DEFAULT_SINK_CLASS_NAMES;
    private static final Logger LOGGER = LoggerFactory.getLogger(TsdMetricsFactory.class);

    static {
        final List sinkClassNames = new ArrayList<>();
        sinkClassNames.add("com.arpnetworking.metrics.impl.ApacheHttpSink");
        sinkClassNames.add("com.arpnetworking.metrics.impl.FileSink");
        DEFAULT_SINK_CLASS_NAMES = Collections.unmodifiableList(sinkClassNames);
    }

    /**
     * Builder for {@link TsdMetricsFactory}.
     *
     * This class does not throw exceptions if it is used improperly. An
     * example of improper use would be if the constraints on a field are
     * not satisfied. To prevent breaking the client application no
     * exception is thrown; instead a warning is logged using the SLF4J
     * {@link LoggerFactory} for this class.
     *
     * Further, the constructed {@link TsdMetricsFactory} will operate
     * normally except that instead of publishing metrics to the sinks it
     * will log a warning each time {@link Metrics#close()} is invoked on the
     * {@link Metrics} instance.
     *
     * This class is thread safe.
     *
     * @author Ville Koskela (ville dot koskela at inscopemetrics dot io)
     */
    public static class Builder implements com.arpnetworking.commons.builder.Builder {

        /**
         * Public constructor.
         */
        public Builder() {
            this(DEFAULT_HOST_RESOLVER, LOGGER);
        }

        /**
         * Public constructor.
         *
         * @param hostResolver The {@link HostResolver} instance to use
         * to determine the default host name.
         */
        public Builder(final HostResolver hostResolver) {
            this(hostResolver, LOGGER);
        }

        // NOTE: Package private for testing
        /* package private */ Builder(@Nullable final Supplier hostResolver, @Nullable final Logger logger) {
            _hostResolver = hostResolver;
            _logger = logger;
        }

        /**
         * Create an instance of {@link MetricsFactory}.
         *
         * @return Instance of {@link MetricsFactory}.
         */
        @Override
        public MetricsFactory build() {
            final List failures = new ArrayList<>();

            // Defaults
            if (_sinks == null) {
                _sinks = DEFAULT_SINKS;
                _logger.info(String.format("Defaulted null sinks; sinks=%s", _sinks));
            }
            if (_hostResolver == null) {
                _hostResolver = DEFAULT_HOST_RESOLVER;
                _logger.info(String.format("Defaulted null host resolver; resolver=%s", _hostResolver));
            }

            // Validate
            if (_serviceName == null) {
                _serviceName = DEFAULT_SERVICE_NAME;
                failures.add("ServiceName cannot be null");
            }
            if (_clusterName == null) {
                _clusterName = DEFAULT_CLUSTER_NAME;
                failures.add("ClusterName cannot be null");
            }

            // Apply fallback
            if (!failures.isEmpty()) {
                _logger.warn(String.format(
                        "Unable to construct TsdMetricsFactory, metrics disabled; failures=%s",
                        failures));
                _sinks = Collections.singletonList(
                        new WarningSink.Builder()
                                .setReasons(failures)
                                .build());
            }

            return new TsdMetricsFactory(this);
        }

        /**
         * Set the sinks to publish to. Cannot be null.
         *
         * @param value The sinks to publish to.
         * @return This {@link Builder} instance.
         */
        public Builder setSinks(@Nullable final List value) {
            _sinks = value;
            return this;
        }

        /**
         * Set the UuidFactory to be used to create UUIDs assigned to instances
         * of {@link Metrics} created by this {@link MetricsFactory}.
         * Cannot be null. Optional. Defaults to using the Java native 
         * {@link java.util.UUID#randomUUID()}.
         *
         * @param uuidFactory The {@link UuidFactory} instance.
         * @return This {@link Builder} instance.
         */
        public Builder setUuidFactory(@Nullable final UuidFactory uuidFactory) {
            _uuidFactory = uuidFactory;
            return this;
        }

        /**
         * Set the service name to publish as. Cannot be null.
         *
         * @param value The service name to publish as.
         * @return This {@link Builder} instance.
         */
        public Builder setServiceName(@Nullable final String value) {
            _serviceName = value;
            return this;
        }

        /**
         * Set the cluster name to publish as. Cannot be null.
         *
         * @param value The cluster name to publish as.
         * @return This {@link Builder} instance.
         */
        public Builder setClusterName(@Nullable final String value) {
            _clusterName = value;
            return this;
        }

        /**
         * Set the host name to publish as. Cannot be null. Optional. Default
         * is the host name provided by the provided {@code hostResolver}
         * or its default instance if one was not specified. If the
         * {@code hostResolver} fails to provide a host name the builder
         * will produce a fake instance of {@link Metrics} on create. This
         * is to ensure the library remains exception neutral.
         *
         * @param value The host name to publish as.
         * @return This {@link Builder} instance.
         */
        public Builder setHostName(@Nullable final String value) {
            _hostResolver = () -> value;
            return this;
        }

        private final Logger _logger;

        private List _sinks = DEFAULT_SINKS;
        private Supplier _uuidFactory = DEFAULT_UUID_FACTORY;
        private String _serviceName;
        private String _clusterName;
        private Supplier _hostResolver;

        private static final List DEFAULT_SINKS = createDefaultSinks(DEFAULT_SINK_CLASS_NAMES);
        private static final Supplier DEFAULT_HOST_RESOLVER = new BackgroundCachingHostResolver(Duration.ofMinutes(1));
        private static final Supplier DEFAULT_UUID_FACTORY = new SplittableRandomUuidFactory();
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy