Monitors.java

/**
 * Copyright 2013 Netflix, Inc.
 * <p/>
 * 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
 * <p/>
 * http://www.apache.org/licenses/LICENSE-2.0
 * <p/>
 * 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.netflix.servo.monitor;

import com.google.common.base.Function;
import com.google.common.cache.Cache;
import com.netflix.servo.DefaultMonitorRegistry;
import com.netflix.servo.annotations.DataSourceType;
import com.netflix.servo.annotations.MonitorTags;
import com.netflix.servo.tag.SortedTagList;
import com.netflix.servo.tag.TagList;
import com.netflix.servo.tag.TaggingContext;
import com.netflix.servo.util.Reflection;
import com.netflix.servo.util.Throwables;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

import static com.netflix.servo.util.Reflection.getFieldsAnnotatedBy;
import static com.netflix.servo.util.Reflection.getMethodsAnnotatedBy;

/**
 * Some helper functions for creating monitor objects.
 */
public final class Monitors {

  /**
   * Name used for composite objects that do not have an explicit id.
   */
  private static final String DEFAULT_ID = "default";

  /**
   * Function to create basic timers.
   */
  private static class TimerFactory implements Function<MonitorConfig, Timer> {
    private final TimeUnit unit;

    TimerFactory(TimeUnit unit) {
      this.unit = unit;
    }

    public Timer apply(MonitorConfig config) {
      return new BasicTimer(config, unit);
    }
  }

  /**
   * Function to create basic counters.
   */
  private static class CounterFactory implements Function<MonitorConfig, Counter> {
    public Counter apply(MonitorConfig config) {
      return new BasicCounter(config);
    }
  }

  private static final CounterFactory COUNTER_FUNCTION = new CounterFactory();

  private Monitors() {
  }

  /**
   * Create a new timer with only the name specified.
   */
  public static Timer newTimer(String name) {
    return newTimer(name, TimeUnit.MILLISECONDS);
  }

  /**
   * Create a new timer with a name and context. The returned timer will maintain separate
   * sub-monitors for each distinct set of tags returned from the context on an update operation.
   */
  public static Timer newTimer(String name, TaggingContext context) {
    return newTimer(name, TimeUnit.MILLISECONDS, context);
  }

  /**
   * Create a new timer with only the name specified.
   */
  public static Timer newTimer(String name, TimeUnit unit) {
    return new BasicTimer(MonitorConfig.builder(name).build(), unit);
  }

  /**
   * Create a new timer with a name and context. The returned timer will maintain separate
   * sub-monitors for each distinct set of tags returned from the context on an update operation.
   */
  public static Timer newTimer(String name, TimeUnit unit, TaggingContext context) {
    final MonitorConfig config = MonitorConfig.builder(name).build();
    return new ContextualTimer(config, context, new TimerFactory(unit));
  }

  /**
   * Create a new counter instance.
   */
  public static Counter newCounter(String name) {
    return new BasicCounter(MonitorConfig.builder(name).build());
  }

  /**
   * Create a new counter with a name and context. The returned counter will maintain separate
   * sub-monitors for each distinct set of tags returned from the context on an update operation.
   */
  public static Counter newCounter(String name, TaggingContext context) {
    final MonitorConfig config = MonitorConfig.builder(name).build();
    return new ContextualCounter(config, context, COUNTER_FUNCTION);
  }

  /**
   * Helper function to easily create a composite for all monitor fields and
   * annotated attributes of a given object.
   */
  public static CompositeMonitor<?> newObjectMonitor(Object obj) {
    return newObjectMonitor(null, obj);
  }

  /**
   * Helper function to easily create a composite for all monitor fields and
   * annotated attributes of a given object.
   *
   * @param id  a unique id associated with this particular instance of the
   *            object. If multiple objects of the same class are registered
   *            they will have the same config and conflict unless the id
   *            values are distinct.
   * @param obj object to search for monitors on. All fields of type
   *            {@link Monitor} and fields/methods with a
   *            {@link com.netflix.servo.annotations.Monitor} annotation
   *            will be extracted and returned using
   *            {@link CompositeMonitor#getMonitors()}.
   * @return composite monitor based on the fields of the class
   */
  public static CompositeMonitor<?> newObjectMonitor(String id, Object obj) {
    final TagList tags = getMonitorTags(obj);

    List<Monitor<?>> monitors = new ArrayList<>();
    addMonitors(monitors, id, tags, obj);

    final Class<?> c = obj.getClass();
    final String objectId = (id == null) ? DEFAULT_ID : id;
    return new BasicCompositeMonitor(newObjectConfig(c, objectId, tags), monitors);
  }

  /**
   * Creates a new monitor for a thread pool with standard metrics for the pool size, queue size,
   * task counts, etc.
   *
   * @param id   id to differentiate metrics for this pool from others.
   * @param pool thread pool instance to monitor.
   * @return composite monitor based on stats provided for the pool
   */
  public static CompositeMonitor<?> newThreadPoolMonitor(String id, ThreadPoolExecutor pool) {
    return newObjectMonitor(id, new MonitoredThreadPool(pool));
  }

  /**
   * Creates a new monitor for a cache with standard metrics for the hits, misses, and loads.
   *
   * @param id    id to differentiate metrics for this cache from others.
   * @param cache cache instance to monitor.
   * @return composite monitor based on stats provided for the cache
   */
  public static CompositeMonitor<?> newCacheMonitor(String id, Cache<?, ?> cache) {
    return newObjectMonitor(id, new MonitoredCache(cache));
  }

  /**
   * Register an object with the default registry. Equivalent to
   * {@code DefaultMonitorRegistry.getInstance().register(Monitors.newObjectMonitor(obj))}.
   */
  public static void registerObject(Object obj) {
    registerObject(null, obj);
  }

  /**
   * Unregister an object from the default registry. Equivalent to
   * {@code DefaultMonitorRegistry.getInstance().unregister(Monitors.newObjectMonitor(obj))}.
   *
   * @param obj Previously registered using {@code Monitors.registerObject(obj)}
   */
  public static void unregisterObject(Object obj) {
    unregisterObject(null, obj);
  }

  /**
   * Unregister an object from the default registry. Equivalent to
   * {@code DefaultMonitorRegistry.getInstance().unregister(Monitors.newObjectMonitor(id, obj))}.
   *
   * @param obj Previously registered using {@code Monitors.registerObject(id, obj)}
   */
  public static void unregisterObject(String id, Object obj) {
    DefaultMonitorRegistry.getInstance().unregister(newObjectMonitor(id, obj));
  }

  /**
   * Register an object with the default registry. Equivalent to
   * {@code DefaultMonitorRegistry.getInstance().register(Monitors.newObjectMonitor(id, obj))}.
   */
  public static void registerObject(String id, Object obj) {
    DefaultMonitorRegistry.getInstance().register(newObjectMonitor(id, obj));
  }

  /**
   * Check whether an object is currently registered with the default registry.
   */
  public static boolean isObjectRegistered(Object obj) {
    return isObjectRegistered(null, obj);
  }

  /**
   * Check whether an object is currently registered with the default registry.
   */
  public static boolean isObjectRegistered(String id, Object obj) {
    return DefaultMonitorRegistry.getInstance().isRegistered(newObjectMonitor(id, obj));
  }

  /**
   * Returns a new monitor that adds the provided tags to the configuration returned by the
   * wrapped monitor.
   */
  @SuppressWarnings("unchecked")
  static <T> Monitor<T> wrap(TagList tags, Monitor<T> monitor) {
    Monitor<T> m;
    if (monitor instanceof CompositeMonitor<?>) {
      m = new CompositeMonitorWrapper<>(tags, (CompositeMonitor<T>) monitor);
    } else {
      m = MonitorWrapper.create(tags, monitor);
    }
    return m;
  }

  /**
   * Extract all monitors across class hierarchy.
   */
  static void addMonitors(List<Monitor<?>> monitors, String id, TagList tags, Object obj) {
    addMonitorFields(monitors, id, tags, obj);
    addAnnotatedFields(monitors, id, tags, obj);
  }

  /**
   * Extract all fields of {@code obj} that are of type {@link Monitor} and add them to
   * {@code monitors}.
   */
  static void addMonitorFields(
      List<Monitor<?>> monitors, String id, TagList tags, Object obj) {
    try {
      final SortedTagList.Builder builder = SortedTagList.builder();
      builder.withTag("class", className(obj.getClass()));
      if (tags != null) {
        builder.withTags(tags);
      }
      if (id != null) {
        builder.withTag("id", id);
      }
      final TagList classTags = builder.build();

      final Set<Field> fields = Reflection.getAllFields(obj.getClass());
      for (Field field : fields) {
        if (isMonitorType(field.getType())) {
          field.setAccessible(true);
          final Monitor<?> m = (Monitor<?>) field.get(obj);
          if (m == null) {
            throw new NullPointerException("field " + field.getName()
                + " in class " + field.getDeclaringClass().getName()
                + " is null, all monitor fields must be"
                + " initialized before registering");
          }
          monitors.add(wrap(classTags, m));
        }
      }
    } catch (Exception e) {
      throw Throwables.propagate(e);
    }
  }

  /**
   * Extract all fields/methods of {@code obj} that have a monitor annotation and add them to
   * {@code monitors}.
   */
  static void addAnnotatedFields(
      List<Monitor<?>> monitors, String id, TagList tags, Object obj) {
    final Class<com.netflix.servo.annotations.Monitor> annoClass =
        com.netflix.servo.annotations.Monitor.class;
    try {
      Set<Field> fields = getFieldsAnnotatedBy(obj.getClass(), annoClass);
      for (Field field : fields) {
        final com.netflix.servo.annotations.Monitor anno = field.getAnnotation(annoClass);
        if (anno != null) {
          final MonitorConfig config =
              newConfig(obj.getClass(), field.getName(), id, anno, tags);
          if (anno.type() == DataSourceType.INFORMATIONAL) {
            monitors.add(new AnnotatedStringMonitor(config, obj, field));
          } else {
            checkType(anno, field.getType(), field.getDeclaringClass());
            monitors.add(new AnnotatedNumberMonitor(config, obj, field));
          }
        }
      }

      Set<Method> methods = getMethodsAnnotatedBy(obj.getClass(), annoClass);
      for (Method method : methods) {
        final com.netflix.servo.annotations.Monitor anno = method.getAnnotation(annoClass);
        if (anno != null) {
          final MonitorConfig config =
              newConfig(obj.getClass(), method.getName(), id, anno, tags);
          if (anno.type() == DataSourceType.INFORMATIONAL) {
            monitors.add(new AnnotatedStringMonitor(config, obj, method));
          } else {
            checkType(anno, method.getReturnType(), method.getDeclaringClass());
            monitors.add(new AnnotatedNumberMonitor(config, obj, method));
          }
        }
      }
    } catch (Exception e) {
      throw Throwables.propagate(e);
    }
  }

  /**
   * Get tags from annotation.
   */
  private static TagList getMonitorTags(Object obj) {
    try {
      Set<Field> fields = getFieldsAnnotatedBy(obj.getClass(), MonitorTags.class);
      for (Field field : fields) {
        field.setAccessible(true);
        return (TagList) field.get(obj);
      }

      Set<Method> methods = getMethodsAnnotatedBy(obj.getClass(), MonitorTags.class);
      for (Method method : methods) {
        method.setAccessible(true);
        return (TagList) method.invoke(obj);
      }
    } catch (Exception e) {
      throw Throwables.propagate(e);
    }

    return null;
  }

  /**
   * Verify that the type for the annotated field is numeric.
   */
  private static void checkType(
      com.netflix.servo.annotations.Monitor anno, Class<?> type, Class<?> container) {
    if (!isNumericType(type)) {
      final String msg = "annotation of type " + anno.type().name() + " can only be used"
          + " with numeric values, " + anno.name() + " in class " + container.getName()
          + " is applied to a field or method of type " + type.getName();
      throw new IllegalArgumentException(msg);
    }
  }

  /**
   * Returns true if {@code c} can be assigned to a number.
   */
  private static boolean isNumericType(Class<?> c) {
    return Number.class.isAssignableFrom(c)
        || double.class == c
        || float.class == c
        || long.class == c
        || int.class == c
        || short.class == c
        || byte.class == c;
  }

  /**
   * Returns true if {@code c} can be assigned to a monitor.
   */
  private static boolean isMonitorType(Class<?> c) {
    return Monitor.class.isAssignableFrom(c);
  }

  /**
   * Creates a monitor config for a composite object.
   */
  private static MonitorConfig newObjectConfig(Class<?> c, String id, TagList tags) {
    final MonitorConfig.Builder builder = MonitorConfig.builder(id);
    final String className = className(c);
    if (!className.isEmpty()) {
      builder.withTag("class", className);
    }

    if (tags != null) {
      builder.withTags(tags);
    }
    return builder.build();
  }

  private static String className(Class c) {
    final String simpleName = c.getSimpleName();

    return simpleName.isEmpty() ? className(c.getEnclosingClass()) : simpleName;
  }

  /**
   * Creates a monitor config based on an annotation.
   */
  private static MonitorConfig newConfig(
      Class<?> c,
      String defaultName,
      String id,
      com.netflix.servo.annotations.Monitor anno,
      TagList tags) {
    String name = anno.name();
    if (name.isEmpty()) {
      name = defaultName;
    }
    MonitorConfig.Builder builder = MonitorConfig.builder(name);
    builder.withTag("class", className(c));
    builder.withTag(anno.type());
    builder.withTag(anno.level());
    if (tags != null) {
      builder.withTags(tags);
    }
    if (id != null) {
      builder.withTag("id", id);
    }
    return builder.build();
  }
}