/*
 * 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.solr.metrics.reporters;

import java.io.IOException;
import java.lang.invoke.MethodHandles;
import java.util.Map;
import java.util.SortedMap;
import java.util.concurrent.TimeUnit;

import com.codahale.metrics.Counter;
import com.codahale.metrics.Gauge;
import com.codahale.metrics.Histogram;
import com.codahale.metrics.Meter;
import com.codahale.metrics.MetricFilter;
import com.codahale.metrics.ScheduledReporter;
import com.codahale.metrics.Slf4jReporter;
import com.codahale.metrics.Timer;
import org.apache.solr.metrics.FilteringSolrMetricReporter;
import org.apache.solr.metrics.SolrMetricManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;

/**
 * Metrics reporter that wraps {@link com.codahale.metrics.Slf4jReporter}.
 * The following init arguments are supported:
 * <ul>
 *   <li><code>period</code>: (optional, int) number of seconds between reports, default is 60,</li>
 *   <li><code>prefix</code>: (optional, str) prefix for metric names, in addition to
 *   registry name. Default is none, ie. just registry name.</li>
 *   <li><code>filter</code>: (optional, str) if not empty only metric names that start
 *   with this value will be reported, default is all metrics from a registry,</li>
 *   <li><code>logger</code>: (optional, str) logger name to use. Default is the
 *   metrics group, eg. <code>solr.jvm</code>, <code>solr.core</code>, etc</li>
 * </ul>
 */
public class SolrSlf4jReporter extends FilteringSolrMetricReporter {

  @SuppressWarnings("unused") // we need this to pass validate-source-patterns
  private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());

  private String instancePrefix = null;
  private String logger = null;
  private Map<String, String> mdcContext;
  private Slf4jReporterWrapper reporter;
  private boolean active;

  // this wrapper allows us to set MDC context - unfortunately it's not possible to
  // simply override {@link Slf4jReporter#report()} because its constructor is private
  private class Slf4jReporterWrapper extends ScheduledReporter {
    final Slf4jReporter delegate;
    final Map<String, String> mdcContext;

    Slf4jReporterWrapper(String logger, Map<String, String> mdcContext, Slf4jReporter delegate, TimeUnit rateUnit, TimeUnit durationUnit) {
      super(null, logger, null, rateUnit, durationUnit);
      this.delegate = delegate;
      this.mdcContext = mdcContext;
    }

    @Override
    public void report() {
      // set up MDC
      MDC.setContextMap(mdcContext);
      try {
        delegate.report();
      } finally {
        // clear MDC
        MDC.clear();
      }
    }

    @Override
    public void report(SortedMap<String, Gauge> gauges, SortedMap<String, Counter> counters, SortedMap<String, Histogram> histograms, SortedMap<String, Meter> meters, SortedMap<String, Timer> timers) {
      throw new UnsupportedOperationException("this method should never be called here!");
    }

    @Override
    public void close() {
      super.close();
      delegate.close();
    }
  }
  /**
   * Create a SLF4J reporter for metrics managed in a named registry.
   *
   * @param metricManager metric manager instance that manages the selected registry
   * @param registryName  registry to use, one of registries managed by
   *                      {@link SolrMetricManager}
   */
  public SolrSlf4jReporter(SolrMetricManager metricManager, String registryName) {
    super(metricManager, registryName);
  }

  public void setPrefix(String prefix) {
    this.instancePrefix = prefix;
  }

  public void setLogger(String logger) {
    this.logger = logger;
  }

  @Override
  protected void doInit() {
    mdcContext = MDC.getCopyOfContextMap();
    mdcContext.put("registry", "m:" + registryName);
    Slf4jReporter.Builder builder = Slf4jReporter
        .forRegistry(metricManager.registry(registryName))
        .convertRatesTo(TimeUnit.SECONDS)
        .convertDurationsTo(TimeUnit.MILLISECONDS);

    final MetricFilter filter = newMetricFilter();
    builder = builder.filter(filter);
    if (instancePrefix != null) {
      builder = builder.prefixedWith(instancePrefix);
    }
    if (logger == null || logger.isEmpty()) {
      // construct logger name from Group
      if (pluginInfo.attributes.containsKey("group")) {
        logger = SolrMetricManager.enforcePrefix(pluginInfo.attributes.get("group"));
      } else if (pluginInfo.attributes.containsKey("registry")) {
        String reg = SolrMetricManager.enforcePrefix(pluginInfo.attributes.get("registry"));
        String[] names = reg.split("\\.");
        if (names.length < 2) {
          logger = reg;
        } else {
          logger = names[0] + "." + names[1];
        }
      }
    }
    builder = builder.outputTo(LoggerFactory.getLogger(logger));
    // build BUT don't start - scheduled execution is handled by the wrapper
    Slf4jReporter delegate = builder.build();
    reporter = new Slf4jReporterWrapper(logger, mdcContext, delegate, TimeUnit.SECONDS, TimeUnit.MILLISECONDS);
    reporter.start(period, TimeUnit.SECONDS);
    active = true;
  }

  @Override
  protected void validate() throws IllegalStateException {
    if (period < 1) {
      throw new IllegalStateException("Init argument 'period' is in time unit 'seconds' and must be at least 1.");
    }
  }

  @Override
  public void close() throws IOException {
    if (reporter != null) {
      reporter.close();
    }
    active = false;
  }

  // for unit tests
  boolean isActive() {
    return active;
  }
}
