Ejemplo n.º 1
0
public class WebPlugin extends StagemonitorPlugin implements ServletContainerInitializer {

  public static final String STAGEMONITOR_SHOW_WIDGET = "X-Stagemonitor-Show-Widget";

  private static final String WEB_PLUGIN = "Web Plugin";

  private static final Logger logger = LoggerFactory.getLogger(WebPlugin.class);

  static {
    Stagemonitor.init();
  }

  private final ConfigurationOption<Collection<Pattern>> requestParamsConfidential =
      ConfigurationOption.regexListOption()
          .key("stagemonitor.requestmonitor.http.requestparams.confidential.regex")
          .dynamic(true)
          .label("Confidential request parameters (regex)")
          .description(
              "A list of request parameter name patterns that should not be collected.\n"
                  + "A request parameter is either a query string or a application/x-www-form-urlencoded request "
                  + "body (POST form content)")
          .defaultValue(
              Arrays.asList(
                  Pattern.compile("(?i).*pass.*"),
                  Pattern.compile("(?i).*credit.*"),
                  Pattern.compile("(?i).*pwd.*")))
          .tags("security-relevant")
          .configurationCategory(WEB_PLUGIN)
          .build();
  private ConfigurationOption<Boolean> collectHttpHeaders =
      ConfigurationOption.booleanOption()
          .key("stagemonitor.requestmonitor.http.collectHeaders")
          .dynamic(true)
          .label("Collect HTTP headers")
          .description("Whether or not HTTP headers should be collected with a call stack.")
          .defaultValue(true)
          .configurationCategory(WEB_PLUGIN)
          .tags("security-relevant")
          .build();
  private ConfigurationOption<Boolean> parseUserAgent =
      ConfigurationOption.booleanOption()
          .key("stagemonitor.requestmonitor.http.parseUserAgent")
          .dynamic(true)
          .label("Analyze user agent")
          .description(
              "Whether or not the user-agent header should be parsed and analyzed to get information "
                  + "about the browser, device type and operating system.")
          .defaultValue(true)
          .configurationCategory(WEB_PLUGIN)
          .build();
  private ConfigurationOption<Collection<String>> excludeHeaders =
      ConfigurationOption.lowerStringsOption()
          .key("stagemonitor.requestmonitor.http.headers.excluded")
          .dynamic(true)
          .label("Do not collect headers")
          .description("A list of (case insensitive) header names that should not be collected.")
          .defaultValue(
              new LinkedHashSet<String>(
                  Arrays.asList("cookie", "authorization", STAGEMONITOR_SHOW_WIDGET)))
          .configurationCategory(WEB_PLUGIN)
          .tags("security-relevant")
          .build();
  private final ConfigurationOption<Boolean> widgetEnabled =
      ConfigurationOption.booleanOption()
          .key("stagemonitor.web.widget.enabled")
          .dynamic(true)
          .label("In browser widget enabled")
          .description(
              "If active, stagemonitor will inject a widget in the web site containing the calltrace "
                  + "metrics.\n"
                  + "Requires Servlet-Api >= 3.0")
          .defaultValue(true)
          .configurationCategory(WEB_PLUGIN)
          .build();
  private final ConfigurationOption<Map<Pattern, String>> groupUrls =
      ConfigurationOption.regexMapOption()
          .key("stagemonitor.groupUrls")
          .dynamic(true)
          .label("Group URLs regex")
          .description(
              "Combine url paths by regex to a single url group.\n"
                  + "E.g. `(.*).js: *.js` combines all URLs that end with `.js` to a group named `*.js`. "
                  + "The metrics for all URLs matching the pattern are consolidated and shown in one row in the request table. "
                  + "The syntax is `<regex>: <group name>[, <regex>: <group name>]*`")
          .defaultValue(
              new LinkedHashMap<Pattern, String>() {
                {
                  put(Pattern.compile("(.*).js$"), "*.js");
                  put(Pattern.compile("(.*).css$"), "*.css");
                  put(Pattern.compile("(.*).jpg$"), "*.jpg");
                  put(Pattern.compile("(.*).jpeg$"), "*.jpeg");
                  put(Pattern.compile("(.*).png$"), "*.png");
                }
              })
          .configurationCategory(WEB_PLUGIN)
          .build();
  private final ConfigurationOption<Boolean> rumEnabled =
      ConfigurationOption.booleanOption()
          .key("stagemonitor.web.rum.enabled")
          .dynamic(true)
          .label("Enable Real User Monitoring")
          .description(
              "The Real User Monitoring feature collects the browser, network and overall percieved "
                  + "execution time from the user's perspective. When activated, a piece of javascript will be "
                  + "injected to each html page that collects the data from real users and sends it back "
                  + "to the server. Servlet API 3.0 or higher is required for this.")
          .defaultValue(true)
          .configurationCategory(WEB_PLUGIN)
          .build();
  private final ConfigurationOption<Boolean> collectPageLoadTimesPerRequest =
      ConfigurationOption.booleanOption()
          .key("stagemonitor.web.collectPageLoadTimesPerRequest")
          .dynamic(true)
          .label("Collect Page Load Time data per request group")
          .description(
              "Whether or not browser, network and overall execution time should be collected per request group.\n"
                  + "If set to true, four additional timers will be created for each request group to record the page "
                  + "rendering time, dom processing time, network time and overall time per request. "
                  + "If set to false, the times of all requests will be aggregated.")
          .defaultValue(false)
          .configurationCategory(WEB_PLUGIN)
          .build();
  private final ConfigurationOption<Collection<String>> excludedRequestPaths =
      ConfigurationOption.stringsOption()
          .key("stagemonitor.web.paths.excluded")
          .dynamic(false)
          .label("Excluded paths")
          .description(
              "Request paths that should not be monitored. "
                  + "A value of `/aaa` means, that all paths starting with `/aaa` should not be monitored."
                  + " It's recommended to not monitor static resources, as they are typically not interesting to "
                  + "monitor but consume resources when you do.")
          .defaultValue(
              SetValueConverter.immutableSet(
                  // exclude paths of static vaadin resources
                  "/VAADIN/",
                  // don't monitor vaadin heatbeat
                  "/HEARTBEAT/"))
          .configurationCategory(WEB_PLUGIN)
          .build();
  private final ConfigurationOption<Boolean> monitorOnlyForwardedRequests =
      ConfigurationOption.booleanOption()
          .key("stagemonitor.web.monitorOnlyForwardedRequests")
          .dynamic(true)
          .label("Monitor only forwarded requests")
          .description(
              "Sometimes you only want to monitor forwarded requests, for example if you have a rewrite "
                  + "filter that translates a external URI (/a) to a internal URI (/b). If only /b should be monitored,"
                  + "set the value to true.")
          .defaultValue(false)
          .configurationCategory(WEB_PLUGIN)
          .build();
  private final ConfigurationOption<String> metricsServletAllowedOrigin =
      ConfigurationOption.stringOption()
          .key("stagemonitor.web.metricsServlet.allowedOrigin")
          .dynamic(true)
          .label("Allowed origin")
          .description("The Access-Control-Allow-Origin header value for the metrics servlet.")
          .defaultValue(null)
          .configurationCategory(WEB_PLUGIN)
          .build();
  private final ConfigurationOption<String> metricsServletJsonpParameter =
      ConfigurationOption.stringOption()
          .key("stagemonitor.web.metricsServlet.jsonpParameter")
          .dynamic(true)
          .label("The Jsonp callback parameter name")
          .description("The name of the parameter used to specify the jsonp callback.")
          .defaultValue(null)
          .configurationCategory(WEB_PLUGIN)
          .build();
  private ConfigurationOption<Boolean> monitorOnlySpringMvcOption =
      ConfigurationOption.booleanOption()
          .key("stagemonitor.requestmonitor.spring.monitorOnlySpringMvcRequests")
          .dynamic(true)
          .label("Monitor only SpringMVC requests")
          .description(
              "Whether or not requests should be ignored, if they will not be handled by a Spring MVC controller method.\n"
                  + "This is handy, if you are not interested in the performance of serving static files. "
                  + "Setting this to true can also significantly reduce the amount of files (and thus storing space) "
                  + "Graphite will allocate.")
          .defaultValue(false)
          .configurationCategory("Spring MVC Plugin")
          .build();
  private ConfigurationOption<Boolean> monitorOnlyResteasyOption =
      ConfigurationOption.booleanOption()
          .key("stagemonitor.requestmonitor.resteasy.monitorOnlyResteasyRequests")
          .dynamic(true)
          .label("Monitor only Resteasy reqeusts")
          .description(
              "Whether or not requests should be ignored, if they will not be handled by a Resteasy resource method.\n"
                  + "This is handy, if you are not interested in the performance of serving static files. "
                  + "Setting this to true can also significantly reduce the amount of files (and thus storing space) "
                  + "Graphite will allocate.")
          .defaultValue(false)
          .configurationCategory("Resteasy Plugin")
          .build();

  @Override
  public void initializePlugin(Metric2Registry registry, Configuration config) {
    registerPooledResources(registry, tomcatThreadPools());
    final CorePlugin corePlugin = config.getConfig(CorePlugin.class);
    ElasticsearchClient elasticsearchClient = corePlugin.getElasticsearchClient();
    if (corePlugin.isReportToGraphite()) {
      elasticsearchClient.sendGrafana1DashboardAsync("grafana/Grafana1GraphiteServer.json");
      elasticsearchClient.sendGrafana1DashboardAsync("grafana/Grafana1GraphiteKPIsOverTime.json");
    }
    if (corePlugin.isReportToElasticsearch()) {
      final GrafanaClient grafanaClient = corePlugin.getGrafanaClient();
      elasticsearchClient.sendBulkAsync("kibana/ApplicationServer.bulk");
      grafanaClient.sendGrafanaDashboardAsync("grafana/ElasticsearchApplicationServer.json");
    }
  }

  @Override
  public List<ConfigurationOption<?>> getConfigurationOptions() {
    final List<ConfigurationOption<?>> configurationOptions = super.getConfigurationOptions();
    if (!ClassUtils.isPresent("org.springframework.web.servlet.HandlerMapping")) {
      configurationOptions.remove(monitorOnlySpringMvcOption);
    }

    if (!ClassUtils.isPresent("org.jboss.resteasy.core.ResourceMethodRegistry")) {
      configurationOptions.remove(monitorOnlyResteasyOption);
    }

    return configurationOptions;
  }

  public boolean isCollectHttpHeaders() {
    return collectHttpHeaders.getValue();
  }

  public boolean isParseUserAgent() {
    return parseUserAgent.getValue();
  }

  public Collection<String> getExcludeHeaders() {
    return excludeHeaders.getValue();
  }

  public boolean isWidgetEnabled() {
    return widgetEnabled.getValue();
  }

  public Map<Pattern, String> getGroupUrls() {
    return groupUrls.getValue();
  }

  public Collection<Pattern> getRequestParamsConfidential() {
    return requestParamsConfidential.getValue();
  }

  public boolean isRealUserMonitoringEnabled() {
    return rumEnabled.getValue();
  }

  public boolean isCollectPageLoadTimesPerRequest() {
    return collectPageLoadTimesPerRequest.getValue();
  }

  public Collection<String> getExcludedRequestPaths() {
    return excludedRequestPaths.getValue();
  }

  public boolean isMonitorOnlyForwardedRequests() {
    return monitorOnlyForwardedRequests.getValue();
  }

  public String getMetricsServletAllowedOrigin() {
    return metricsServletAllowedOrigin.getValue();
  }

  public String getMetricsServletJsonpParamName() {
    return metricsServletJsonpParameter.getValue();
  }

  public boolean isWidgetAndStagemonitorEndpointsAllowed(
      HttpServletRequest request, Configuration configuration) {
    final Boolean showWidgetAttr = (Boolean) request.getAttribute(STAGEMONITOR_SHOW_WIDGET);
    if (showWidgetAttr != null) {
      logger.debug("isWidgetAndStagemonitorEndpointsAllowed: showWidgetAttr={}", showWidgetAttr);
      return showWidgetAttr;
    }

    final boolean widgetEnabled = isWidgetEnabled();
    final boolean passwordInShowWidgetHeaderCorrect =
        isPasswordInShowWidgetHeaderCorrect(request, configuration);
    final boolean result = widgetEnabled || passwordInShowWidgetHeaderCorrect;
    logger.debug(
        "isWidgetAndStagemonitorEndpointsAllowed: isWidgetEnabled={}, isPasswordInShowWidgetHeaderCorrect={}, result={}",
        widgetEnabled,
        passwordInShowWidgetHeaderCorrect,
        result);
    return result;
  }

  private boolean isPasswordInShowWidgetHeaderCorrect(
      HttpServletRequest request, Configuration configuration) {
    String password = request.getHeader(STAGEMONITOR_SHOW_WIDGET);
    if (configuration.isPasswordCorrect(password)) {
      return true;
    } else {
      if (StringUtils.isNotEmpty(password)) {
        logger.error(
            "The password transmitted via the header {} is not correct. "
                + "This might be a malicious attempt to guess the value of {}. "
                + "The request was initiated from the ip {}.",
            STAGEMONITOR_SHOW_WIDGET,
            Stagemonitor.STAGEMONITOR_PASSWORD,
            MonitoredHttpRequest.getClientIp(request));
      }
      return false;
    }
  }

  public boolean isMonitorOnlySpringMvcRequests() {
    return monitorOnlySpringMvcOption.getValue();
  }

  public boolean isMonitorOnlyResteasyRequests() {
    return monitorOnlyResteasyOption.getValue();
  }

  @Override
  public void onStartup(Set<Class<?>> c, ServletContext ctx) throws ServletException {
    ctx.addServlet(ConfigurationServlet.class.getSimpleName(), new ConfigurationServlet())
        .addMapping(ConfigurationServlet.CONFIGURATION_ENDPOINT);
    ctx.addServlet(
            StagemonitorMetricsServlet.class.getSimpleName(), new StagemonitorMetricsServlet())
        .addMapping("/stagemonitor/metrics");
    ctx.addServlet(RumServlet.class.getSimpleName(), new RumServlet())
        .addMapping("/stagemonitor/public/rum");
    ctx.addServlet(FileServlet.class.getSimpleName(), new FileServlet())
        .addMapping("/stagemonitor/static/*", "/stagemonitor/public/static/*");
    ctx.addServlet(WidgetServlet.class.getSimpleName(), new WidgetServlet())
        .addMapping("/stagemonitor");

    final ServletRegistration.Dynamic requestTraceServlet =
        ctx.addServlet(RequestTraceServlet.class.getSimpleName(), new RequestTraceServlet());
    requestTraceServlet.addMapping("/stagemonitor/request-traces");
    requestTraceServlet.setAsyncSupported(true);

    final FilterRegistration.Dynamic securityFilter =
        ctx.addFilter(
            StagemonitorSecurityFilter.class.getSimpleName(), new StagemonitorSecurityFilter());
    // Add as last filter so that other filters have the chance to set the
    // WebPlugin.STAGEMONITOR_SHOW_WIDGET request attribute that overrides the widget visibility.
    // That way the application can decide whether a particular user is allowed to see the widget.P
    securityFilter.addMappingForUrlPatterns(
        EnumSet.of(DispatcherType.REQUEST), true, "/stagemonitor/*");
    securityFilter.setAsyncSupported(true);

    final FilterRegistration.Dynamic monitorFilter =
        ctx.addFilter(
            HttpRequestMonitorFilter.class.getSimpleName(), new HttpRequestMonitorFilter());
    monitorFilter.addMappingForUrlPatterns(
        EnumSet.of(DispatcherType.REQUEST, DispatcherType.FORWARD), false, "/*");
    monitorFilter.setAsyncSupported(true);

    final FilterRegistration.Dynamic userFilter =
        ctx.addFilter(UserNameFilter.class.getSimpleName(), new UserNameFilter());
    // Have this filter run last because user information may be populated by other filters e.g.
    // Spring Security
    userFilter.addMappingForUrlPatterns(
        EnumSet.of(DispatcherType.REQUEST, DispatcherType.FORWARD), true, "/*");
    userFilter.setAsyncSupported(true);

    ctx.addListener(MDCListener.class);
    ctx.addListener(MonitoredHttpRequest.StagemonitorServletContextListener.class);
    ctx.addListener(SpringMonitoredHttpRequest.HandlerMappingServletContextListener.class);
    ctx.addListener(SessionCounter.class);
  }
}
Ejemplo n.º 2
0
/** This class contains the configuration options for stagemonitor's core functionality */
public class CorePlugin extends StagemonitorPlugin {

  public static final String DEFAULT_APPLICATION_NAME = "My Application";

  private static final String CORE_PLUGIN_NAME = "Core";
  public static final String POOLS_QUEUE_CAPACITY_LIMIT_KEY =
      "stagemonitor.threadPools.queueCapacityLimit";

  private final Logger logger = LoggerFactory.getLogger(getClass());

  final ConfigurationOption<Boolean> stagemonitorActive =
      ConfigurationOption.booleanOption()
          .key("stagemonitor.active")
          .dynamic(true)
          .label("Activate stagemonitor")
          .description("If set to `false` stagemonitor will be completely deactivated.")
          .defaultValue(true)
          .configurationCategory(CORE_PLUGIN_NAME)
          .build();
  private final ConfigurationOption<Boolean> internalMonitoring =
      ConfigurationOption.booleanOption()
          .key("stagemonitor.internal.monitoring")
          .dynamic(true)
          .label("Internal monitoring")
          .description("If active, stagemonitor will collect internal performance data")
          .defaultValue(false)
          .configurationCategory(CORE_PLUGIN_NAME)
          .build();
  private final ConfigurationOption<Integer> reportingIntervalConsole =
      ConfigurationOption.integerOption()
          .key("stagemonitor.reporting.interval.console")
          .dynamic(false)
          .label("Reporting interval console")
          .description(
              "The amount of time between console reports (in seconds). "
                  + "To deactivate console reports, set this to a value below 1.")
          .defaultValue(0)
          .configurationCategory(CORE_PLUGIN_NAME)
          .build();
  private final ConfigurationOption<Integer> reportingIntervalAggregation =
      ConfigurationOption.integerOption()
          .key("stagemonitor.reporting.interval.aggregation")
          .dynamic(false)
          .label("Metrics aggregation interval")
          .description(
              "The amount of time between all registered metrics are aggregated for a report on server "
                  + "shutdown that shows aggregated values for all metrics of the measurement session. "
                  + "To deactivate a aggregate report on shutdown, set this to a value below 1.")
          .defaultValue(30)
          .configurationCategory(CORE_PLUGIN_NAME)
          .build();
  private final ConfigurationOption<Boolean> reportingJmx =
      ConfigurationOption.booleanOption()
          .key("stagemonitor.reporting.jmx")
          .dynamic(false)
          .label("Expose MBeans")
          .description("Whether or not to expose all metrics as MBeans.")
          .defaultValue(true)
          .configurationCategory(CORE_PLUGIN_NAME)
          .build();
  private final ConfigurationOption<Integer> reportingIntervalGraphite =
      ConfigurationOption.integerOption()
          .key("stagemonitor.reporting.interval.graphite")
          .dynamic(false)
          .label("Reporting interval graphite")
          .description(
              "The amount of time between the metrics are reported to graphite (in seconds).\n"
                  + "To deactivate graphite reporting, set this to a value below 1, or don't provide "
                  + "stagemonitor.reporting.graphite.hostName.")
          .defaultValue(60)
          .tags("metrics-store", "graphite")
          .configurationCategory(CORE_PLUGIN_NAME)
          .build();
  private final ConfigurationOption<String> graphiteHostName =
      ConfigurationOption.stringOption()
          .key("stagemonitor.reporting.graphite.hostName")
          .dynamic(false)
          .label("Graphite host name")
          .description(
              "The name of the host where graphite is running. This setting is mandatory, if you want "
                  + "to use the grafana dashboards.")
          .defaultValue(null)
          .tags("metrics-store", "graphite")
          .configurationCategory(CORE_PLUGIN_NAME)
          .build();
  private final ConfigurationOption<Integer> graphitePort =
      ConfigurationOption.integerOption()
          .key("stagemonitor.reporting.graphite.port")
          .dynamic(false)
          .label("Carbon port")
          .description("The port where carbon is listening.")
          .defaultValue(2003)
          .tags("metrics-store", "graphite")
          .configurationCategory(CORE_PLUGIN_NAME)
          .build();
  private final ConfigurationOption<String> influxDbUrl =
      ConfigurationOption.stringOption()
          .key("stagemonitor.reporting.influxdb.url")
          .dynamic(true)
          .label("InfluxDB URL")
          .description("The URL of your InfluxDB installation.")
          .defaultValue(null)
          .tags("metrics-store", "influx-db")
          .configurationCategory(CORE_PLUGIN_NAME)
          .build();
  private final ConfigurationOption<String> influxDbDb =
      ConfigurationOption.stringOption()
          .key("stagemonitor.reporting.influxdb.db")
          .dynamic(true)
          .label("InfluxDB database")
          .description("The target database")
          .defaultValue("stagemonitor")
          .tags("metrics-store", "influx-db")
          .configurationCategory(CORE_PLUGIN_NAME)
          .build();
  private final ConfigurationOption<Integer> reportingIntervalInfluxDb =
      ConfigurationOption.integerOption()
          .key("stagemonitor.reporting.interval.influxdb")
          .dynamic(false)
          .label("Reporting interval InfluxDb")
          .description(
              "The amount of time between the metrics are reported to InfluxDB (in seconds).")
          .defaultValue(60)
          .tags("metrics-store", "influx-db")
          .configurationCategory(CORE_PLUGIN_NAME)
          .build();
  private final ConfigurationOption<Integer> reportingIntervalElasticsearch =
      ConfigurationOption.integerOption()
          .key("stagemonitor.reporting.interval.elasticsearch")
          .dynamic(false)
          .label("Reporting interval Elasticsearch")
          .description(
              "The amount of time between the metrics are reported to Elasticsearch (in seconds).")
          .defaultValue(60)
          .tags("metrics-store", "elasticsearch")
          .configurationCategory(CORE_PLUGIN_NAME)
          .build();
  private final ConfigurationOption<Integer> deleteElasticsearchMetricsAfterDays =
      ConfigurationOption.integerOption()
          .key("stagemonitor.reporting.elasticsearch.deleteMetricsAfterDays")
          .dynamic(false)
          .label("Delete ES metrics after (days)")
          .description(
              "The number of days after the metrics stored in elasticsearch should be deleted. Set below 1 to deactivate.")
          .defaultValue(-1)
          .tags("metrics-store", "elasticsearch")
          .configurationCategory(CORE_PLUGIN_NAME)
          .build();
  private final ConfigurationOption<Integer> moveToColdNodesAfterDays =
      ConfigurationOption.integerOption()
          .key("stagemonitor.elasticsearch.hotColdArchitecture.moveToColdNodesAfterDays")
          .dynamic(false)
          .label("Activate Hot-Cold Architecture")
          .description(
              "Setting this to a value > 1 activates the hot-cold architecture described in https://www.elastic.co/blog/hot-warm-architecture "
                  + "where new data that is more frequently queried and updated is stored on beefy nodes (SSDs, more RAM and CPU). "
                  + "When the indexes reach a certain age, they are allocated on cold nodes. For this to work, you have to tag your "
                  + "beefy nodes with node.box_type: hot (either in elasticsearch.yml or start the node using ./bin/elasticsearch --node.box_type hot)"
                  + "and your historical nodes with node.box_type: cold.")
          .defaultValue(-1)
          .tags("metrics-store", "elasticsearch")
          .configurationCategory(CORE_PLUGIN_NAME)
          .build();
  private final ConfigurationOption<String> applicationName =
      ConfigurationOption.stringOption()
          .key("stagemonitor.applicationName")
          .dynamic(false)
          .label("Application name")
          .description(
              "The name of the application.\n"
                  + "Either this property or the display-name in web.xml is mandatory!")
          .defaultValue(null)
          .configurationCategory(CORE_PLUGIN_NAME)
          .tags("important")
          .build();
  private final ConfigurationOption<String> instanceName =
      ConfigurationOption.stringOption()
          .key("stagemonitor.instanceName")
          .dynamic(false)
          .label("Instance name")
          .description(
              "The instance name.\n"
                  + "If this property is not set, the instance name set to the first request's "
                  + "javax.servlet.ServletRequest#getServerName()\n"
                  + "That means that the collection of metrics does not start before the first request is executed!")
          .defaultValue(null)
          .configurationCategory(CORE_PLUGIN_NAME)
          .tags("important")
          .build();
  private final ConfigurationOption<String> elasticsearchUrl =
      ConfigurationOption.stringOption()
          .key("stagemonitor.elasticsearch.url")
          .dynamic(true)
          .label("Elasticsearch URL")
          .description(
              "The URL of the elasticsearch server that stores the call stacks. If the URL is not "
                  + "provided, the call stacks won't get stored.")
          .defaultValue(null)
          .configurationCategory(CORE_PLUGIN_NAME)
          .build();
  private final ConfigurationOption<Collection<String>> elasticsearchConfigurationSourceProfiles =
      ConfigurationOption.stringsOption()
          .key("stagemonitor.elasticsearch.configurationSourceProfiles")
          .dynamic(false)
          .label("Elasticsearch configuration source profiles")
          .description(
              "Set configuration profiles of configuration stored in elasticsearch as a centralized configuration source "
                  + "that can be shared between multiple server instances. Set the profiles appropriate to the current "
                  + "environment e.g. `production,common`, `local`, `test`, ... The configuration will be stored under "
                  + "`{stagemonitor.elasticsearch.url}/stagemonitor/configuration/{configurationSourceProfile}`.")
          .defaultValue(Collections.<String>emptyList())
          .configurationCategory(CORE_PLUGIN_NAME)
          .build();
  private final ConfigurationOption<Boolean> deactivateStagemonitorIfEsConfigSourceIsDown =
      ConfigurationOption.booleanOption()
          .key("stagemonitor.elasticsearch.configurationSource.deactivateStagemonitorIfEsIsDown")
          .dynamic(false)
          .label("Deactivate stagemonitor if elasticsearch configuration source is down")
          .description(
              "Set to true if stagemonitor should be deactivated if "
                  + "stagemonitor.elasticsearch.configurationSourceProfiles is set but elasticsearch can't be reached "
                  + "under stagemonitor.elasticsearch.url. Defaults to true to prevent starting stagemonitor with "
                  + "wrong configuration.")
          .defaultValue(true)
          .configurationCategory(CORE_PLUGIN_NAME)
          .build();
  private final ConfigurationOption<Collection<Pattern>> excludedMetrics =
      ConfigurationOption.regexListOption()
          .key("stagemonitor.metrics.excluded.pattern")
          .dynamic(false)
          .label("Excluded metrics (regex)")
          .description("A comma separated list of metric names that should not be collected.")
          .defaultValue(Collections.<Pattern>emptyList())
          .configurationCategory(CORE_PLUGIN_NAME)
          .build();
  private final ConfigurationOption<Collection<String>> disabledPlugins =
      ConfigurationOption.stringsOption()
          .key("stagemonitor.plugins.disabled")
          .dynamic(false)
          .label("Disabled plugins")
          .description(
              "A comma separated list of plugin names (the simple class name) that should not be active.")
          .defaultValue(Collections.<String>emptyList())
          .configurationCategory(CORE_PLUGIN_NAME)
          .build();
  private final ConfigurationOption<Integer> reloadConfigurationInterval =
      ConfigurationOption.integerOption()
          .key("stagemonitor.configuration.reload.interval")
          .dynamic(false)
          .label("Configuration reload interval")
          .description(
              "The interval in seconds a reload of all configuration sources is performed. "
                  + "Set to a value below `1` to deactivate periodic reloading the configuration.")
          .defaultValue(60)
          .configurationCategory(CORE_PLUGIN_NAME)
          .build();
  private final ConfigurationOption<Collection<String>> excludePackages =
      ConfigurationOption.stringsOption()
          .key("stagemonitor.instrument.exclude")
          .dynamic(true)
          .label("Excluded packages")
          .description(
              "Exclude packages and their sub-packages from the instrumentation (for example the profiler).")
          .defaultValue(
              new LinkedHashSet<String>() {
                {
                  add("antlr");
                  add("aopalliance");
                  add("asm");
                  add("c3p0");
                  add("ch.qos");
                  add("com.amazon");
                  add("com.codahale");
                  add("com.fasterxml");
                  add("com.github");
                  add("com.google");
                  add("com.maxmind");
                  add("com.oracle");
                  add("com.p6spy");
                  add("com.rome");
                  add("com.spartial");
                  add("com.sun");
                  add("com.thoughtworks");
                  add("com.vaadin");
                  add("commons-");
                  add("dom4j");
                  add("eclipse");
                  add("java.");
                  add("javax.");
                  add("junit");
                  add("net.java");
                  add("net.sf");
                  add("net.sourceforge");
                  add("nz.net");
                  add("ognl");
                  add("oracle");
                  add("org.antlr");
                  add("org.apache");
                  add("org.aspectj");
                  add("org.codehaus");
                  add("org.eclipse");
                  add("org.freemarker");
                  add("org.glassfish");
                  add("org.groovy");
                  add("org.hibernate");
                  add("org.hsqldb");
                  add("org.jadira");
                  add("org.javassist");
                  add("org.jboss");
                  add("org.jdom");
                  add("org.joda");
                  add("org.jsoup");
                  add("org.json");
                  add("org.unbescape");
                  add("org.elasticsearch");
                  add("org.slf4j");
                  add("org.springframework");
                  add("org.stagemonitor");
                  add("org.thymeleaf");
                  add("org.yaml");
                  add("org.wildfly");
                  add("org.zeroturnaround");
                  add("org.xml");
                  add("io.dropwizard");
                  add("freemarker");
                  add("javassist");
                  add("uadetector");
                  add("p6spy");
                  add("rome");
                  add("sun");
                  add("xerces");
                  add("xml");
                  add("xmpp");
                }
              })
          .configurationCategory(CORE_PLUGIN_NAME)
          .build();
  private final ConfigurationOption<Collection<String>> excludeContaining =
      ConfigurationOption.stringsOption()
          .key("stagemonitor.instrument.excludeContaining")
          .dynamic(true)
          .label("Exclude containing")
          .description(
              "Exclude classes from the instrumentation (for example from profiling) that contain one of the "
                  + "following strings as part of their class name.")
          .defaultValue(
              new LinkedHashSet<String>() {
                {
                  add("$JaxbAccessor");
                  add("$$");
                  add("CGLIB");
                  add("EnhancerBy");
                  add("$Proxy");
                }
              })
          .configurationCategory(CORE_PLUGIN_NAME)
          .build();
  private final ConfigurationOption<Collection<String>> includePackages =
      ConfigurationOption.stringsOption()
          .key("stagemonitor.instrument.include")
          .dynamic(true)
          .label("Included packages")
          .description(
              "The packages that should be included for instrumentation (for example the profiler). "
                  + "If this property is empty, all packages (except for the excluded ones) are instrumented. "
                  + "You can exclude subpackages of a included package via `stagemonitor.instrument.exclude`.")
          .defaultValue(Collections.<String>emptySet())
          .configurationCategory(CORE_PLUGIN_NAME)
          .build();
  private final ConfigurationOption<Boolean> attachAgentAtRuntime =
      ConfigurationOption.booleanOption()
          .key("stagemonitor.instrument.runtimeAttach")
          .dynamic(false)
          .label("Attach agent at runtime")
          .description(
              "Attaches the agent via the Attach API at runtime and retransforms all currently loaded classes.")
          .defaultValue(true)
          .configurationCategory(CORE_PLUGIN_NAME)
          .build();
  private final ConfigurationOption<Collection<String>> excludedInstrumenters =
      ConfigurationOption.stringsOption()
          .key("stagemonitor.instrument.excludedInstrumenter")
          .dynamic(false)
          .label("Excluded Instrumenters")
          .description(
              "A list of the simple class names of StagemonitorJavassistInstrumenters that should not be applied")
          .defaultValue(Collections.<String>emptySet())
          .configurationCategory(CORE_PLUGIN_NAME)
          .build();
  private final ConfigurationOption<String> grafanaUrl =
      ConfigurationOption.stringOption()
          .key("stagemonitor.grafana.url")
          .dynamic(true)
          .label("Grafana URL")
          .description("The URL of your Grafana 2.0 installation")
          .defaultValue(null)
          .configurationCategory(CORE_PLUGIN_NAME)
          .tags("grafana")
          .build();
  private final ConfigurationOption<String> grafanaApiKey =
      ConfigurationOption.stringOption()
          .key("stagemonitor.grafana.apiKey")
          .dynamic(true)
          .label("Grafana API Key")
          .description(
              "The API Key of your Grafana 2.0 installation. "
                  + "See http://docs.grafana.org/reference/http_api/#create-api-token how to create a key. "
                  + "The key has to have the admin role. This is necessary so that stagemonitor can automatically add "
                  + "datasources and dashboards to Grafana.")
          .defaultValue(null)
          .configurationCategory(CORE_PLUGIN_NAME)
          .tags("grafana")
          .sensitive()
          .build();
  private final ConfigurationOption<Integer> threadPoolQueueCapacityLimit =
      ConfigurationOption.integerOption()
          .key(POOLS_QUEUE_CAPACITY_LIMIT_KEY)
          .dynamic(false)
          .label("Thread Pool Queue Capacity Limit")
          .description(
              "Sets a limit to the number of pending tasks in the ExecutorServices stagemonitor uses. "
                  + "These are thread pools that are used for example to report request traces to elasticsearch. "
                  + "If elasticsearch is unreachable or your application encounters a spike in incoming requests this limit could be reached. "
                  + "It is used to prevent the queue from growing indefinitely. ")
          .defaultValue(1000)
          .configurationCategory(CORE_PLUGIN_NAME)
          .tags("advanced")
          .build();

  private static MetricsAggregationReporter aggregationReporter;

  private List<Closeable> reporters = new CopyOnWriteArrayList<Closeable>();

  private ElasticsearchClient elasticsearchClient;
  private GrafanaClient grafanaClient;
  private IndexSelector indexSelector = new IndexSelector(new Clock.UserTimeClock());

  @Override
  public void initializePlugin(Metric2Registry metricRegistry, Configuration configuration) {
    final Integer reloadInterval = getReloadConfigurationInterval();
    if (reloadInterval > 0) {
      configuration.scheduleReloadAtRate(reloadInterval, TimeUnit.SECONDS);
    }

    metricRegistry.register(
        MetricName.name("online").build(),
        new Gauge<Integer>() {
          @Override
          public Integer getValue() {
            return 1;
          }
        });

    ElasticsearchClient elasticsearchClient = getElasticsearchClient();
    if (isReportToGraphite()) {
      elasticsearchClient.sendGrafana1DashboardAsync("Grafana1GraphiteCustomMetrics.json");
    }
    elasticsearchClient.createIndex(
        "stagemonitor", IOUtils.getResourceAsStream("stagemonitor-elasticsearch-mapping.json"));
    if (isReportToElasticsearch()) {
      final GrafanaClient grafanaClient = getGrafanaClient();
      grafanaClient.createElasticsearchDatasource(getElasticsearchUrl());
    }
    registerReporters(metricRegistry, configuration);
  }

  private void registerReporters(Metric2Registry metric2Registry, Configuration configuration) {
    RegexMetricFilter regexFilter = new RegexMetricFilter(getExcludedMetricsPatterns());
    MetricFilter allFilters = new OrMetricFilter(regexFilter, new MetricsWithCountFilter());
    MetricRegistry metricRegistry = metric2Registry.getMetricRegistry();

    reportToGraphite(
        metricRegistry,
        getGraphiteReportingInterval(),
        Stagemonitor.getMeasurementSession(),
        allFilters);
    reportToInfluxDb(
        metric2Registry,
        reportingIntervalInfluxDb.getValue(),
        Stagemonitor.getMeasurementSession());
    reportToElasticsearch(
        metric2Registry,
        reportingIntervalElasticsearch.getValue(),
        Stagemonitor.getMeasurementSession(),
        configuration.getConfig(CorePlugin.class));

    List<ScheduledReporter> onShutdownReporters = new LinkedList<ScheduledReporter>();
    onShutdownReporters.add(
        new SimpleElasticsearchReporter(
            getElasticsearchClient(), metricRegistry, "simple-es-reporter", allFilters));

    reportToConsole(metricRegistry, getConsoleReportingInterval(), allFilters, onShutdownReporters);
    registerAggregationReporter(
        metricRegistry, allFilters, onShutdownReporters, getAggregationReportingInterval());
    if (reportToJMX()) {
      reportToJMX(metricRegistry, allFilters);
    }
  }

  private void registerAggregationReporter(
      MetricRegistry metricRegistry,
      MetricFilter allFilters,
      List<ScheduledReporter> onShutdownReporters,
      long reportingInterval) {
    if (reportingInterval > 0) {
      aggregationReporter =
          new MetricsAggregationReporter(metricRegistry, allFilters, onShutdownReporters);
      aggregationReporter.start(reportingInterval, TimeUnit.SECONDS);
      aggregationReporter.report();
      reporters.add(aggregationReporter);
    }
  }

  private void reportToGraphite(
      MetricRegistry metricRegistry,
      long reportingInterval,
      MeasurementSession measurementSession,
      MetricFilter filter) {
    String graphiteHostName = getGraphiteHostName();
    if (isReportToGraphite()) {
      final GraphiteReporter graphiteReporter =
          GraphiteReporter.forRegistry(metricRegistry)
              .prefixedWith(getGraphitePrefix(measurementSession))
              .convertRatesTo(TimeUnit.SECONDS)
              .convertDurationsTo(TimeUnit.MILLISECONDS)
              .filter(filter)
              .build(new Graphite(new InetSocketAddress(graphiteHostName, getGraphitePort())));

      graphiteReporter.start(reportingInterval, TimeUnit.SECONDS);
      reporters.add(graphiteReporter);
    }
  }

  private void reportToInfluxDb(
      Metric2Registry metricRegistry,
      int reportingInterval,
      MeasurementSession measurementSession) {

    if (StringUtils.isNotEmpty(getInfluxDbUrl()) && reportingInterval > 0) {
      logger.info(
          "Sending metrics to InfluxDB ({}) every {}s", getInfluxDbUrl(), reportingInterval);
      final InfluxDbReporter reporter =
          new InfluxDbReporter(
              metricRegistry,
              Metric2Filter.ALL,
              TimeUnit.SECONDS,
              TimeUnit.MILLISECONDS,
              measurementSession.asMap(),
              new HttpClient(),
              this);

      reporter.start(reportingInterval, TimeUnit.SECONDS);
      reporters.add(reporter);
    } else {
      logger.info(
          "Not sending metrics to InfluxDB (url={}, interval={}s)",
          getInfluxDbUrl(),
          reportingInterval);
    }
  }

  private void reportToElasticsearch(
      Metric2Registry metricRegistry,
      int reportingInterval,
      final MeasurementSession measurementSession,
      CorePlugin corePlugin) {
    if (isReportToElasticsearch()) {
      elasticsearchClient.sendBulkAsync("KibanaConfig.bulk");
      logger.info(
          "Sending metrics to Elasticsearch ({}) every {}s",
          getElasticsearchUrl(),
          reportingInterval);
      final String mappingJson =
          ElasticsearchClient.requireBoxTypeHotIfHotColdAritectureActive(
              "stagemonitor-elasticsearch-metrics-index-template.json",
              corePlugin.moveToColdNodesAfterDays.getValue());
      elasticsearchClient.sendMappingTemplateAsync(mappingJson, "stagemonitor-metrics");
      final ElasticsearchReporter reporter =
          new ElasticsearchReporter(
              metricRegistry,
              Metric2Filter.ALL,
              TimeUnit.SECONDS,
              TimeUnit.MILLISECONDS,
              measurementSession.asMap(),
              new HttpClient(),
              this);

      reporter.start(reportingInterval, TimeUnit.SECONDS);
      reporters.add(reporter);
      elasticsearchClient.scheduleIndexManagement(
          ElasticsearchReporter.STAGEMONITOR_METRICS_INDEX_PREFIX,
          moveToColdNodesAfterDays.getValue(),
          deleteElasticsearchMetricsAfterDays.getValue());
    } else {
      logger.info(
          "Not sending metrics to Elasticsearch (url={}, interval={}s)",
          getElasticsearchUrl(),
          reportingInterval);
    }
  }

  private String getGraphitePrefix(MeasurementSession measurementSession) {
    return name(
        "stagemonitor",
        sanitizeGraphiteMetricSegment(measurementSession.getApplicationName()),
        sanitizeGraphiteMetricSegment(measurementSession.getInstanceName()),
        sanitizeGraphiteMetricSegment(measurementSession.getHostName()));
  }

  private void reportToConsole(
      MetricRegistry metricRegistry,
      long reportingInterval,
      MetricFilter filter,
      List<ScheduledReporter> onShutdownReporters) {
    final SortedTableLogReporter reporter =
        SortedTableLogReporter.forRegistry(metricRegistry)
            .convertRatesTo(TimeUnit.SECONDS)
            .convertDurationsTo(TimeUnit.MILLISECONDS)
            .filter(filter)
            .build();
    onShutdownReporters.add(reporter);
    if (reportingInterval > 0) {
      reporter.start(reportingInterval, TimeUnit.SECONDS);
      reporters.add(reporter);
    }
  }

  private void reportToJMX(MetricRegistry metricRegistry, MetricFilter filter) {
    final JmxReporter reporter = JmxReporter.forRegistry(metricRegistry).filter(filter).build();
    reporter.start();
    reporters.add(reporter);
  }

  @Override
  public void onShutDown() {
    if (aggregationReporter != null) {
      logger.info(
          "\n####################################################\n"
              + "## Aggregated report for this measurement session ##\n"
              + "####################################################\n");
      aggregationReporter.onShutDown();
    }

    for (Closeable reporter : reporters) {
      try {
        reporter.close();
      } catch (IOException e) {
        logger.warn(e.getMessage(), e);
      }
    }

    getElasticsearchClient().close();
    getGrafanaClient().close();
  }

  public ElasticsearchClient getElasticsearchClient() {
    if (elasticsearchClient == null) {
      elasticsearchClient = new ElasticsearchClient();
    }
    return elasticsearchClient;
  }

  public GrafanaClient getGrafanaClient() {
    if (grafanaClient == null) {
      grafanaClient = new GrafanaClient(this, new HttpClient());
    }
    return grafanaClient;
  }

  public void setElasticsearchClient(ElasticsearchClient elasticsearchClient) {
    this.elasticsearchClient = elasticsearchClient;
  }

  public boolean isStagemonitorActive() {
    return Stagemonitor.isDisabled() ? false : stagemonitorActive.getValue();
  }

  public boolean isInternalMonitoringActive() {
    return internalMonitoring.getValue();
  }

  public long getConsoleReportingInterval() {
    return reportingIntervalConsole.getValue();
  }

  public long getAggregationReportingInterval() {
    return reportingIntervalAggregation.getValue();
  }

  public boolean reportToJMX() {
    return reportingJmx.getValue();
  }

  public int getGraphiteReportingInterval() {
    return reportingIntervalGraphite.getValue();
  }

  public String getGraphiteHostName() {
    return graphiteHostName.getValue();
  }

  public int getGraphitePort() {
    return graphitePort.getValue();
  }

  public String getApplicationName() {
    return applicationName.getValue();
  }

  public String getInstanceName() {
    return instanceName.getValue();
  }

  public String getElasticsearchUrl() {
    return removeTrailingSlash(elasticsearchUrl.getValue());
  }

  private String removeTrailingSlash(String url) {
    if (url != null && url.endsWith("/")) {
      return url.substring(0, url.length() - 1);
    }
    return url;
  }

  public Collection<String> getElasticsearchConfigurationSourceProfiles() {
    return elasticsearchConfigurationSourceProfiles.getValue();
  }

  public boolean isDeactivateStagemonitorIfEsConfigSourceIsDown() {
    return deactivateStagemonitorIfEsConfigSourceIsDown.getValue();
  }

  public Collection<Pattern> getExcludedMetricsPatterns() {
    return excludedMetrics.getValue();
  }

  public Collection<String> getDisabledPlugins() {
    return disabledPlugins.getValue();
  }

  public Integer getReloadConfigurationInterval() {
    return reloadConfigurationInterval.getValue();
  }

  public Collection<String> getExcludeContaining() {
    return excludeContaining.getValue();
  }

  public Collection<String> getIncludePackages() {
    return includePackages.getValue();
  }

  public Collection<String> getExcludePackages() {
    return excludePackages.getValue();
  }

  public boolean isAttachAgentAtRuntime() {
    return attachAgentAtRuntime.getValue();
  }

  public Collection<String> getExcludedInstrumenters() {
    return excludedInstrumenters.getValue();
  }

  public String getInfluxDbUrl() {
    return removeTrailingSlash(influxDbUrl.getValue());
  }

  public String getInfluxDbDb() {
    return influxDbDb.getValue();
  }

  public boolean isReportToElasticsearch() {
    return StringUtils.isNotEmpty(getElasticsearchUrl())
        && reportingIntervalElasticsearch.getValue() > 0;
  }

  public boolean isReportToGraphite() {
    return StringUtils.isNotEmpty(getGraphiteHostName());
  }

  public String getGrafanaUrl() {
    return removeTrailingSlash(grafanaUrl.getValue());
  }

  public String getGrafanaApiKey() {
    return grafanaApiKey.getValue();
  }

  public int getThreadPoolQueueCapacityLimit() {
    return threadPoolQueueCapacityLimit.getValue();
  }

  public IndexSelector getIndexSelector() {
    return indexSelector;
  }

  public int getElasticsearchReportingInterval() {
    return reportingIntervalElasticsearch.getValue();
  }

  public Integer getMoveToColdNodesAfterDays() {
    return moveToColdNodesAfterDays.getValue();
  }
}