All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.bull.javamelody.internal.web.PrometheusController Maven / Gradle / Ivy

There is a newer version: 2.3.0
Show newest version
/*
 * Copyright 2008-2017 by Emeric Vernat
 *
 *     This file is part of Java Melody.
 *
 * 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
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package net.bull.javamelody.internal.web;

import java.io.IOException;
import java.io.PrintWriter;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Pattern;

import net.bull.javamelody.internal.common.Parameters;
import net.bull.javamelody.internal.model.CacheInformations;
import net.bull.javamelody.internal.model.Collector;
import net.bull.javamelody.internal.model.Counter;
import net.bull.javamelody.internal.model.CounterRequest;
import net.bull.javamelody.internal.model.JRobin;
import net.bull.javamelody.internal.model.JavaInformations;
import net.bull.javamelody.internal.model.MemoryInformations;
import net.bull.javamelody.internal.model.TomcatInformations;

/**
 * Produces a report of the data in {@link JavaInformations} in the Prometheus text format
 * to enable collection by a Prometheus server.
 *

* Metric names have been adjusted to match Prometheus recommendations. * * The {@link JavaInformations} fields to Prometheus metric mappings are done statically * to avoid any performance penalties of Java Reflection. Additional metadata (description, type) * about each statistic is merged in as well. * * In the spirit of JavaMelody, special attention is paid to performance so that exposing * these metrics should have very little performance overhead on applications. * * This implementation directly outputs the Prometheus text format avoiding dependence on any * additional libraries. *

* Exposed Metrics:
* (From {@link JavaInformations}) *
 *  javamelody_memory_used_bytes
 *  javamelody_memory_max_bytes
 *  javamelody_memory_used_pct
 *  javamelody_memory_perm_gen_used_bytes
 *  javamelody_memory_perm_gen_max_bytes
 *  javamelody_memory_perm_gen_used_pct
 *  javamelody_memory_gc_millis
 *  javamelody_sessions_active_count
 *  javamelody_sessions_age_avg_minutes
 *  javamelody_transactions_count
 *  javamelody_connections_used_count
 *  javamelody_connections_max_count
 *  javamelody_connections_active_count
 *  javamelody_connections_used_pct
 *  javamelody_system_load_avg
 *  javamelody_system_cpu_load_pct
 *  javamelody_system_unix_file_descriptors_open_count
 *  javamelody_system_unix_file_descriptors_max
 *  javamelody_system_unix_file_descriptors_open_pct
 *  javamelody_system_processors_count
 *  javamelody_system_tmp_space_free_bytes
 *  javamelody_jvm_start_time
 *  javamelody_jvm_cpu_millis
 *  javamelody_threads_count
 *  javamelody_threads_max_count
 *  javamelody_threads_started_count
 *  javamelody_threads_active_count
 *  javamelody_job_executing_count
 *  javamelody_tomcat_threads_max{tomcat_name="__name__"}
 *  javamelody_tomcat_thread_busy_count{tomcat_name="__name__"}
 *  javamelody_tomcat_received_bytes{tomcat_name="__name__"}
 *  javamelody_tomcat_sent_bytes{tomcat_name="__name__"}
 *  javamelody_tomcat_request_count{tomcat_name="__name__"}
 *  javamelody_tomcat_error_count{tomcat_name="__name__"}
 *  javamelody_tomcat_processing_time_millis{tomcat_name="__name__"}
 *  javamelody_tomcat_max_time_millis{tomcat_name="__name__"}
 *  javamelody_cache_in_memory_count{cache_name="__name__"}
 *  javamelody_cache_in_memory_used_pct{cache_name="__name__"}
 *  javamelody_cache_in_memory_hits_pct{cache_name="__name__"}
 *  javamelody_cache_on_disk_count{cache_name="__name__"}
 *  javamelody_cache_hits_pct{cache_name="__name__"}
 *  
* (from {@link Collector} counters) *
 *  javamelody_http_hits_count
 *  javamelody_http_errors_count
 *  javamelody_http_duration_millis
 *  javamelody_sql_hits_count
 *  javamelody_sql_errors_count
 *  javamelody_sql_duration_millis
 *  javamelody_jpa_hits_count
 *  javamelody_jpa_errors_count
 *  javamelody_jpa_duration_millis
 *  javamelody_ejb_hits_count
 *  javamelody_ejb_errors_count
 *  javamelody_ejb_duration_millis
 *  javamelody_spring_hits_count
 *  javamelody_spring_errors_count
 *  javamelody_spring_duration_millis
 *  javamelody_guice_hits_count
 *  javamelody_guice_errors_count
 *  javamelody_guice_duration_millis
 *  javamelody_services_hits_count
 *  javamelody_services_errors_count
 *  javamelody_services_duration_millis
 *  javamelody_struts_hits_count
 *  javamelody_struts_errors_count
 *  javamelody_struts_duration_millis
 *  javamelody_jsf_hits_count
 *  javamelody_jsf_errors_count
 *  javamelody_jsf_duration_millis
 *  javamelody_jsp_hits_count
 *  javamelody_jsp_errors_count
 *  javamelody_jsp_duration_millis
 *  javamelody_error_hits_count
 *  javamelody_error_errors_count
 *  javamelody_error_duration_millis
 *  javamelody_log_hits_count
 *  javamelody_log_errors_count
 *  javamelody_log_duration_millis
 *  
* Additionally, the `lastValue` metrics can also be exported by adding the http parameter includeLastValue=true. * Note: the `lastValue` metrics are already aggregated over time, where Prometheus prefers the raw counters and gauges. * Also, obtaining the `lastValue` metrics appears to have a 5-10ms overhead. * * The `lastValue` metrics are DISABLED by default. * * @author https://github.com/slynn1324, Stefan Penndorf, Emeric Vernat */ class PrometheusController { private static final String METRIC_PREFIX = "javamelody_"; // Pre-Compiled Patterns. Pattern is thread-safe. Matcher is not. private static final Pattern CAMEL_TO_SNAKE_PATTERN = Pattern.compile("([a-z])([A-Z]+)"); private static final Pattern SANITIZE_TO_UNDERSCORE_PATTERN = Pattern.compile("[- :]"); private static final Pattern SANITIZE_REMOVE_PATTERN = Pattern.compile("[^a-z0-9_]"); private static final String EMPTY_STRING = ""; private static final String UNDERSCORE = "_"; private enum MetricType { GAUGE("gauge"), COUNTER("counter"); private final String code; MetricType(String code) { this.code = code; } public String getCode() { return code; } } private final JavaInformations javaInformations; private final Collector collector; private final PrintWriter out; private final DecimalFormat decimalFormat; PrometheusController(List javaInformations, Collector collector, PrintWriter out) throws IOException { super(); assert javaInformations != null && !javaInformations.isEmpty(); assert collector != null; assert out != null; // it doesn't make much sense to use a JavaMelody collector server with Prometheus // (which is effectively it's own collector server) if (javaInformations.size() > 1) { throw new IOException( "JavaMelody collector server not supported - configure Prometheus to scrape nodes."); } this.javaInformations = javaInformations.get(0); this.collector = collector; this.out = out; decimalFormat = new DecimalFormat(); final DecimalFormatSymbols decimalFormatSymbols = DecimalFormatSymbols .getInstance(Locale.US); // setNaN for #806: on Java 8 and before, decimalFormat prints \uFFFD ('') instead of NaN decimalFormatSymbols.setNaN("NaN"); decimalFormat.setDecimalFormatSymbols(decimalFormatSymbols); decimalFormat.setGroupingUsed(false); decimalFormat.setMinimumIntegerDigits(1); decimalFormat.setMaximumFractionDigits(15); } /** * Produce the full report. * @param includeLastValue boolean * @throws IOException e */ void report(boolean includeLastValue) throws IOException { // see https://prometheus.io/docs/instrumenting/exposition_formats/ for text format // memory reportOnMemoryInformations(javaInformations.getMemoryInformations()); // jvm & system reportOnJavaInformations(); // tomcat if (javaInformations.getTomcatInformationsList() != null) { reportOnTomcatInformations(); } // caches if (javaInformations.isCacheEnabled()) { reportOnCacheInformations(); } reportOnCollector(); if (includeLastValue) { reportOnLastValues(); } } // CHECKSTYLE:OFF private void reportOnCacheInformations() { // NOPMD // CHECKSTYLE:ON final List cacheInformationsList = javaInformations .getCacheInformationsList(); final Map cacheInfos = new LinkedHashMap( cacheInformationsList.size()); for (final CacheInformations cacheInfo : cacheInformationsList) { final String fields = "{cache_name=\"" + sanitizeName(cacheInfo.getName()) + "\"}"; cacheInfos.put(fields, cacheInfo); } printHeader(MetricType.GAUGE, "cache_in_memory_count", "cache in memory count"); for (final Map.Entry entry : cacheInfos.entrySet()) { printLongWithFields("cache_in_memory_count", entry.getKey(), entry.getValue().getInMemoryObjectCount()); } printHeader(MetricType.GAUGE, "cache_in_memory_used_pct", "in memory used percent"); for (final Map.Entry entry : cacheInfos.entrySet()) { printDoubleWithFields("cache_in_memory_used_pct", entry.getKey(), (double) entry.getValue().getInMemoryPercentUsed() / 100); } printHeader(MetricType.GAUGE, "cache_in_memory_hits_pct", "cache in memory hit percent"); for (final Map.Entry entry : cacheInfos.entrySet()) { printDoubleWithFields("cache_in_memory_hits_pct", entry.getKey(), (double) entry.getValue().getInMemoryHitsRatio() / 100); } printHeader(MetricType.GAUGE, "cache_on_disk_count", "cache on disk count"); for (final Map.Entry entry : cacheInfos.entrySet()) { printLongWithFields("cache_on_disk_count", entry.getKey(), entry.getValue().getOnDiskObjectCount()); } printHeader(MetricType.GAUGE, "cache_hits_pct", "cache hits percent"); for (final Map.Entry entry : cacheInfos.entrySet()) { printDoubleWithFields("cache_hits_pct", entry.getKey(), (double) entry.getValue().getHitsRatio() / 100); } printHeader(MetricType.COUNTER, "cache_in_memory_hits_count", "total cache in memory hit count"); for (final Map.Entry entry : cacheInfos.entrySet()) { printLongWithFields("cache_in_memory_hits_count", entry.getKey(), entry.getValue().getInMemoryHits()); } printHeader(MetricType.COUNTER, "cache_hits_count", "total cache hit count"); for (final Map.Entry entry : cacheInfos.entrySet()) { printLongWithFields("cache_hits_count", entry.getKey(), entry.getValue().getCacheHits()); } printHeader(MetricType.COUNTER, "cache_misses_count", "total cache misses count"); for (final Map.Entry entry : cacheInfos.entrySet()) { printLongWithFields("cache_misses_count", entry.getKey(), entry.getValue().getCacheMisses()); } } // CHECKSTYLE:OFF private void reportOnTomcatInformations() { // NOPMD // CHECKSTYLE:ON final Map tcInfos = new LinkedHashMap(); for (final TomcatInformations tcInfo : javaInformations.getTomcatInformationsList()) { if (tcInfo.getRequestCount() > 0) { final String fields = "{tomcat_name=\"" + sanitizeName(tcInfo.getName()) + "\"}"; tcInfos.put(fields, tcInfo); } } if (tcInfos.isEmpty()) { return; } printHeader(MetricType.GAUGE, "tomcat_threads_max", "tomcat max threads"); for (final Map.Entry entry : tcInfos.entrySet()) { printLongWithFields("tomcat_threads_max", entry.getKey(), entry.getValue().getMaxThreads()); } printHeader(MetricType.GAUGE, "tomcat_thread_busy_count", "tomcat currently busy threads"); for (final Map.Entry entry : tcInfos.entrySet()) { printLongWithFields("tomcat_thread_busy_count", entry.getKey(), entry.getValue().getCurrentThreadsBusy()); } printHeader(MetricType.COUNTER, "tomcat_received_bytes", "tomcat total received bytes"); for (final Map.Entry entry : tcInfos.entrySet()) { printLongWithFields("tomcat_received_bytes", entry.getKey(), entry.getValue().getBytesReceived()); } printHeader(MetricType.COUNTER, "tomcat_sent_bytes", "tomcat total sent bytes"); for (final Map.Entry entry : tcInfos.entrySet()) { printLongWithFields("tomcat_sent_bytes", entry.getKey(), entry.getValue().getBytesSent()); } printHeader(MetricType.COUNTER, "tomcat_request_count", "tomcat total request count"); for (final Map.Entry entry : tcInfos.entrySet()) { printLongWithFields("tomcat_request_count", entry.getKey(), entry.getValue().getRequestCount()); } printHeader(MetricType.COUNTER, "tomcat_error_count", "tomcat total error count"); for (final Map.Entry entry : tcInfos.entrySet()) { printLongWithFields("tomcat_error_count", entry.getKey(), entry.getValue().getErrorCount()); } printHeader(MetricType.COUNTER, "tomcat_processing_time_millis", "tomcat total processing time"); for (final Map.Entry entry : tcInfos.entrySet()) { printLongWithFields("tomcat_processing_time_millis", entry.getKey(), entry.getValue().getProcessingTime()); } printHeader(MetricType.GAUGE, "tomcat_max_time_millis", "tomcat max time for single request"); for (final Map.Entry entry : tcInfos.entrySet()) { printLongWithFields("tomcat_max_time_millis", entry.getKey(), entry.getValue().getMaxTime()); } } /** * Reports on hits, errors, and duration sum for all counters in the collector. * * Bypasses the {@link JRobin#getLastValue()} methods to provide real-time counters as well as * improving performance from bypassing JRobin reads in the getLastValue() method. */ private void reportOnCollector() { for (final Counter counter : collector.getCounters()) { if (!counter.isDisplayed()) { continue; } final List requests = counter.getRequests(); long hits = 0; long duration = 0; long errors = 0; for (final CounterRequest cr : requests) { hits += cr.getHits(); duration += cr.getDurationsSum(); errors += cr.getSystemErrors(); } final String sanitizedName = sanitizeName(counter.getName()); printLong(MetricType.COUNTER, sanitizedName + "_hits_count", "javamelody counter", hits); if (!counter.isErrorCounter() || counter.isJobCounter()) { // errors has no sense for the error and log counters printLong(MetricType.COUNTER, sanitizedName + "_errors_count", "javamelody counter", errors); } if (duration >= 0) { // duration is negative and has no sense for the log counter printLong(MetricType.COUNTER, sanitizedName + "_duration_millis", "javamelody counter", duration); } } } /** * Includes the traditional 'graph' fields from the 'lastValue' API. * * These fields are summary fields aggregated over `javamelody.resolutions-seconds` (default 60), which * is normally an odd thing to pass to Prometheus. Most (all?) of these can be calculated inside * Prometheus from the Collector stats. * * Note: This lookup seems to take the longest execution time -- 5-10ms per request due to JRobin reads. * * Disabled by default. To enable set the 'prometheus-include-last-value' property to 'true'. * * @throws IOException e */ private void reportOnLastValues() throws IOException { Collection jrobins = collector.getDisplayedCounterJRobins(); for (final JRobin jrobin : jrobins) { printDouble(MetricType.GAUGE, "last_value_" + camelToSnake(jrobin.getName()), "javamelody value per minute", jrobin.getLastValue()); } jrobins = collector.getDisplayedOtherJRobins(); for (final JRobin jrobin : jrobins) { printDouble(MetricType.GAUGE, "last_value_" + camelToSnake(jrobin.getName()), "javamelody value per minute", jrobin.getLastValue()); } } /** * Reports on information vailable in the {@link JavaInformations} class. */ private void reportOnJavaInformations() { // sessions if (javaInformations.getSessionCount() >= 0) { printLong(MetricType.GAUGE, "sessions_active_count", "active session count", javaInformations.getSessionCount()); printLong(MetricType.GAUGE, "sessions_age_avg_minutes", "session avg age in minutes", javaInformations.getSessionMeanAgeInMinutes()); } // connections if (!Parameters.isNoDatabase()) { printLong(MetricType.COUNTER, "transactions_count", "transactions count", javaInformations.getTransactionCount()); printLong(MetricType.GAUGE, "connections_used_count", "used connections count", javaInformations.getActiveConnectionCount()); printLong(MetricType.GAUGE, "connections_active_count", "active connections", javaInformations.getActiveConnectionCount()); if (javaInformations.getMaxConnectionCount() > 0) { printLong(MetricType.GAUGE, "connections_max_count", "max connections", javaInformations.getMaxConnectionCount()); printDouble(MetricType.GAUGE, "connections_used_pct", "used connections percentage", javaInformations.getUsedConnectionPercentage()); } } // system if (javaInformations.getSystemLoadAverage() >= 0) { printDouble(MetricType.GAUGE, "system_load_avg", "system load average", javaInformations.getSystemLoadAverage()); } if (javaInformations.getSystemCpuLoad() >= 0) { printDouble(MetricType.GAUGE, "system_cpu_load_pct", "system cpu load", javaInformations.getSystemCpuLoad()); } if (javaInformations.getUnixOpenFileDescriptorCount() >= 0) { printDouble(MetricType.GAUGE, "system_unix_file_descriptors_open_count", "unix open file descriptors count", javaInformations.getUnixOpenFileDescriptorCount()); printDouble(MetricType.GAUGE, "system_unix_file_descriptors_max", "unix file descriptors max", javaInformations.getUnixMaxFileDescriptorCount()); printDouble(MetricType.GAUGE, "system_unix_file_descriptors_open_pct", "unix open file descriptors percentage", javaInformations.getUnixOpenFileDescriptorPercentage()); } if (javaInformations.getFreeDiskSpaceInTemp() >= 0) { printLong(MetricType.GAUGE, "system_tmp_space_free_bytes", "tmp space available", javaInformations.getFreeDiskSpaceInTemp()); } // jvm printLong(MetricType.GAUGE, "jvm_start_time", "jvm start time", javaInformations.getStartDate().getTime()); printLong(MetricType.COUNTER, "jvm_cpu_millis", "jvm cpu millis", javaInformations.getProcessCpuTimeMillis()); printLong(MetricType.GAUGE, "system_processors_count", "processors available", javaInformations.getAvailableProcessors()); // threads printLong(MetricType.GAUGE, "threads_count", "threads count", javaInformations.getThreadCount()); printLong(MetricType.GAUGE, "threads_max_count", "threads peak count", javaInformations.getPeakThreadCount()); printLong(MetricType.COUNTER, "threads_started_count", "total threads started", javaInformations.getTotalStartedThreadCount()); printLong(MetricType.GAUGE, "threads_active_count", "active thread count", javaInformations.getActiveThreadCount()); // jobs if (javaInformations.isJobEnabled()) { printLong(MetricType.GAUGE, "job_executing_count", "executing job count", javaInformations.getCurrentlyExecutingJobCount()); } } private void reportOnMemoryInformations(MemoryInformations memoryInformations) { printLong(MetricType.GAUGE, "memory_used_bytes", "used memory in bytes", memoryInformations.getUsedMemory()); printLong(MetricType.GAUGE, "memory_max_bytes", "max memory in bytes", memoryInformations.getMaxMemory()); printDouble(MetricType.GAUGE, "memory_used_pct", "memory used percentage", memoryInformations.getUsedMemoryPercentage()); if (memoryInformations.getUsedPermGen() > 0) { printLong(MetricType.GAUGE, "memory_perm_gen_used_bytes", "used perm gen memory in bytes", memoryInformations.getUsedPermGen()); if (memoryInformations.getMaxPermGen() > 0) { printLong(MetricType.GAUGE, "memory_perm_gen_max_bytes", "max perm gen memory in bytes", memoryInformations.getMaxPermGen()); printDouble(MetricType.GAUGE, "memory_perm_gen_used_pct", "used perm gen memory percentage", memoryInformations.getUsedPermGenPercentage()); } } printDouble(MetricType.COUNTER, "memory_gc_millis", "gc time millis", memoryInformations.getGarbageCollectionTimeMillis()); } /** * Converts a camelCase or CamelCase string to camel_case * @param camel String * @return String */ private static String camelToSnake(String camel) { return CAMEL_TO_SNAKE_PATTERN.matcher(camel).replaceAll("$1_$2").toLowerCase(Locale.US); } /** * converts to lowercase, replaces common separators with underscores, and strips all remaining non-alpha-numeric characters. * @param name String * @return String */ private static String sanitizeName(String name) { final String lowerCaseName = name.toLowerCase(Locale.US); final String separatorReplacedName = SANITIZE_TO_UNDERSCORE_PATTERN.matcher(lowerCaseName) .replaceAll(UNDERSCORE); return SANITIZE_REMOVE_PATTERN.matcher(separatorReplacedName).replaceAll(EMPTY_STRING); } // prints a long metric value, including HELP and TYPE rows private void printLong(MetricType metricType, String name, String description, long value) { printHeader(metricType, name, description); printLongWithFields(name, null, value); } // prints a double metric value, including HELP and TYPE rows private void printDouble(MetricType metricType, String name, String description, double value) { printHeader(metricType, name, description); printDoubleWithFields(name, null, value); } // prints a long metric value with optional fields private void printLongWithFields(String name, String fields, long value) { print(METRIC_PREFIX); print(name); if (fields != null) { print(fields); } print(' '); println(String.valueOf(value)); } // prints a double metric value with optional fields private void printDoubleWithFields(String name, String fields, double value) { print(METRIC_PREFIX); print(name); if (fields != null) { print(fields); } print(' '); println(decimalFormat.format(value)); } // prints the HELP and TYPE rows private void printHeader(MetricType metricType, String name, String description) { print("# HELP "); print(METRIC_PREFIX); print(name); print(' '); println(description); print("# TYPE "); print(METRIC_PREFIX); print(name); print(' '); println(metricType.getCode()); } private void print(String s) { out.print(s); } private void print(char c) { out.print(c); } private void println(String s) { out.print(s); // out.println() prints "\r\n" on Windows and Prometheus does not recognize "\r\n" as EOL // (in Prometheus: "no token found" and in promtool check metrics: // error while linting: text format parsing error in line 2: unknown metric type "gauge\r") out.print('\n'); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy