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); } }
/** 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(); } }