/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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 org.apache.drill.exec.metrics;

import java.lang.management.ManagementFactory;
import java.util.concurrent.TimeUnit;

import org.apache.drill.exec.util.SystemPropertyUtil;

import com.codahale.metrics.Metric;
import com.codahale.metrics.MetricFilter;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.Slf4jReporter;
import com.codahale.metrics.jmx.JmxReporter;
import com.codahale.metrics.jvm.BufferPoolMetricSet;
import com.codahale.metrics.jvm.CachedThreadStatesGaugeSet;
import com.codahale.metrics.jvm.ClassLoadingGaugeSet;
import com.codahale.metrics.jvm.FileDescriptorRatioGauge;
import com.codahale.metrics.jvm.GarbageCollectorMetricSet;
import com.codahale.metrics.jvm.MemoryUsageGaugeSet;
import com.codahale.metrics.jvm.ThreadStatesGaugeSet;

public final class DrillMetrics {
  private static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(DrillMetrics.class);

  public static final boolean METRICS_JMX_OUTPUT_ENABLED =
      SystemPropertyUtil.getBoolean("drill.metrics.jmx.enabled", true);
  public static final boolean METRICS_LOG_OUTPUT_ENABLED =
      SystemPropertyUtil.getBoolean("drill.metrics.log.enabled", false);
  public static final int METRICS_LOG_OUTPUT_INTERVAL =
      SystemPropertyUtil.getInt("drill.metrics.log.interval", 60);

  private static class RegistryHolder {

    private static final MetricRegistry REGISTRY;

    static {
      REGISTRY = new MetricRegistry();
      registerSystemMetrics();
      getJmxReporter();
      getLogReporter();
    }

    private static void registerSystemMetrics() {
      REGISTRY.registerAll(new GarbageCollectorMetricSet());
      REGISTRY.registerAll(MetricsCategory.BUFFERPOOL_PREFIX, new BufferPoolMetricSet(ManagementFactory.getPlatformMBeanServer()));
      REGISTRY.registerAll(MetricsCategory.MEMORY_PREFIX, new MemoryUsageGaugeSet());
      REGISTRY.registerAll(MetricsCategory.THREADS_PREFIX, new ThreadStatesGaugeSet());
      REGISTRY.registerAll(MetricsCategory.CACHED_THREADS_PREFIX, new CachedThreadStatesGaugeSet(1, TimeUnit.MINUTES));
      REGISTRY.registerAll(MetricsCategory.CLASS_PREFIX, new ClassLoadingGaugeSet());
      REGISTRY.registerAll(new CpuGaugeSet());
      register("fd.usage", new FileDescriptorRatioGauge());
    }

    private static JmxReporter getJmxReporter() {
      if (METRICS_JMX_OUTPUT_ENABLED) {
        JmxReporter reporter = JmxReporter.forRegistry(REGISTRY).build();
        reporter.start();

        return reporter;
      }
      return null;
    }

    private static Slf4jReporter getLogReporter() {
      if (METRICS_LOG_OUTPUT_ENABLED) {
        Slf4jReporter reporter = Slf4jReporter.forRegistry(REGISTRY)
            .outputTo(logger)
            .convertRatesTo(TimeUnit.SECONDS)
            .convertDurationsTo(TimeUnit.MILLISECONDS)
            .build();
        reporter.start(METRICS_LOG_OUTPUT_INTERVAL, TimeUnit.SECONDS);

        return reporter;
      }
      return null;
    }
  }

  /**
   * Note: For counters, histograms, meters and timers, use get or create methods on {@link #getRegistry the
   * registry} (e.g. {@link MetricRegistry#counter}). For {@link com.codahale.metrics.Gauge gauges} or custom
   * metric implementations use this method. The registry does not allow registering multiple metrics with
   * the same name, which is a problem when multiple drillbits are started in the same JVM (e.g. unit tests).
   *
   * @param name metric name
   * @param metric metric instance
   * @param <T> metric type
   */
  public synchronized static <T extends Metric> void register(String name, T metric) {
    boolean removed = RegistryHolder.REGISTRY.remove(name);
    if (removed) {
      logger.warn("Removing old metric since name matched newly registered metric. Metric name: {}", name);
    }
    RegistryHolder.REGISTRY.register(name, metric);
  }

  public static MetricRegistry getRegistry() {
    return RegistryHolder.REGISTRY;
  }

  public static void resetMetrics() {
    RegistryHolder.REGISTRY.removeMatching(MetricFilter.ALL);
    RegistryHolder.registerSystemMetrics();
  }

  // prevents instantiation
  private DrillMetrics() {
  }
}
