1 /*
2  * Copyright 2008-2019 by Emeric Vernat
3  *
4  *     This file is part of Java Melody.
5  *
6  * Licensed under the Apache License, Version 2.0 (the "License");
7  * you may not use this file except in compliance with the License.
8  * You may obtain a copy of the License at
9  *
10  *     http://www.apache.org/licenses/LICENSE-2.0
11  *
12  * Unless required by applicable law or agreed to in writing, software
13  * distributed under the License is distributed on an "AS IS" BASIS,
14  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15  * See the License for the specific language governing permissions and
16  * limitations under the License.
17  */

18 package net.bull.javamelody.internal.web;
19
20 import java.io.IOException;
21 import java.io.PrintWriter;
22 import java.text.DecimalFormat;
23 import java.text.DecimalFormatSymbols;
24 import java.util.Collection;
25 import java.util.LinkedHashMap;
26 import java.util.List;
27 import java.util.Locale;
28 import java.util.Map;
29 import java.util.regex.Pattern;
30
31 import net.bull.javamelody.internal.common.Parameters;
32 import net.bull.javamelody.internal.model.CacheInformations;
33 import net.bull.javamelody.internal.model.Collector;
34 import net.bull.javamelody.internal.model.Counter;
35 import net.bull.javamelody.internal.model.CounterRequest;
36 import net.bull.javamelody.internal.model.JCacheInformations;
37 import net.bull.javamelody.internal.model.JRobin;
38 import net.bull.javamelody.internal.model.JavaInformations;
39 import net.bull.javamelody.internal.model.MemoryInformations;
40 import net.bull.javamelody.internal.model.TomcatInformations;
41
42 /**
43  * Produces a report of the data in {@link JavaInformations} in the Prometheus text format
44  * to enable collection by a <a href='https://prometheus.io/'>Prometheus</a> server.
45  *<br/><br/>
46  * Metric names have been adjusted to match Prometheus recommendations.
47  *
48  * The {@link JavaInformations} fields to Prometheus metric mappings are done statically
49  * to avoid any performance penalties of Java Reflection.  Additional metadata (description, type)
50  * about each statistic is merged in as well.
51  *
52  * In the spirit of JavaMelody, special attention is paid to performance so that exposing
53  * these metrics should have very little performance overhead on applications.
54  *
55  * This implementation directly outputs the Prometheus text format avoiding dependence on any
56  * additional libraries.
57  *<br/><br/>
58  * Exposed Metrics: <br/>
59  * (From {@link JavaInformations})
60  * <pre>
61  *  javamelody_memory_used_bytes
62  *  javamelody_memory_max_bytes
63  *  javamelody_memory_used_pct
64  *  javamelody_memory_perm_gen_used_bytes
65  *  javamelody_memory_perm_gen_max_bytes
66  *  javamelody_memory_perm_gen_used_pct
67  *  javamelody_memory_gc_millis
68  *  javamelody_sessions_active_count
69  *  javamelody_sessions_age_avg_minutes
70  *  javamelody_transactions_count
71  *  javamelody_connections_used_count
72  *  javamelody_connections_max_count
73  *  javamelody_connections_active_count
74  *  javamelody_connections_used_pct
75  *  javamelody_system_load_avg
76  *  javamelody_system_cpu_load_pct
77  *  javamelody_system_unix_file_descriptors_open_count
78  *  javamelody_system_unix_file_descriptors_max
79  *  javamelody_system_unix_file_descriptors_open_pct
80  *  javamelody_system_processors_count
81  *  javamelody_system_tmp_space_free_bytes
82  *  javamelody_jvm_start_time
83  *  javamelody_jvm_cpu_millis
84  *  javamelody_threads_count
85  *  javamelody_threads_max_count
86  *  javamelody_threads_started_count
87  *  javamelody_threads_active_count
88  *  javamelody_job_executing_count
89  *  javamelody_tomcat_threads_max{tomcat_name="__name__"}
90  *  javamelody_tomcat_thread_busy_count{tomcat_name="__name__"}
91  *  javamelody_tomcat_received_bytes{tomcat_name="__name__"}
92  *  javamelody_tomcat_sent_bytes{tomcat_name="__name__"}
93  *  javamelody_tomcat_request_count{tomcat_name="__name__"}
94  *  javamelody_tomcat_error_count{tomcat_name="__name__"}
95  *  javamelody_tomcat_processing_time_millis{tomcat_name="__name__"}
96  *  javamelody_tomcat_max_time_millis{tomcat_name="__name__"}
97  *  javamelody_cache_in_memory_count{cache_name="__name__"}
98  *  javamelody_cache_in_memory_used_pct{cache_name="__name__"}
99  *  javamelody_cache_in_memory_hits_pct{cache_name="__name__"}
100  *  javamelody_cache_on_disk_count{cache_name="__name__"}
101  *  javamelody_cache_hits_pct{cache_name="__name__"}
102  *  </pre>
103  *  (from {@link Collector} counters)
104  *  <pre>
105  *  javamelody_http_hits_count
106  *  javamelody_http_errors_count
107  *  javamelody_http_duration_millis
108  *  javamelody_sql_hits_count
109  *  javamelody_sql_errors_count
110  *  javamelody_sql_duration_millis
111  *  javamelody_jpa_hits_count
112  *  javamelody_jpa_errors_count
113  *  javamelody_jpa_duration_millis
114  *  javamelody_ejb_hits_count
115  *  javamelody_ejb_errors_count
116  *  javamelody_ejb_duration_millis
117  *  javamelody_spring_hits_count
118  *  javamelody_spring_errors_count
119  *  javamelody_spring_duration_millis
120  *  javamelody_guice_hits_count
121  *  javamelody_guice_errors_count
122  *  javamelody_guice_duration_millis
123  *  javamelody_services_hits_count
124  *  javamelody_services_errors_count
125  *  javamelody_services_duration_millis
126  *  javamelody_struts_hits_count
127  *  javamelody_struts_errors_count
128  *  javamelody_struts_duration_millis
129  *  javamelody_jsf_hits_count
130  *  javamelody_jsf_errors_count
131  *  javamelody_jsf_duration_millis
132  *  javamelody_jsp_hits_count
133  *  javamelody_jsp_errors_count
134  *  javamelody_jsp_duration_millis
135  *  javamelody_error_hits_count
136  *  javamelody_error_errors_count
137  *  javamelody_error_duration_millis
138  *  javamelody_log_hits_count
139  *  javamelody_log_errors_count
140  *  javamelody_log_duration_millis
141  *  </pre>
142  *  Additionally, the `lastValue` metrics can also be exported by adding the http parameter includeLastValue=true.
143  *  Note: the `lastValue` metrics are already aggregated over time, where Prometheus prefers the raw counters and gauges.
144  *  Also, obtaining the `lastValue` metrics appears to have a 5-10ms overhead.
145  *
146  *  The `lastValue` metrics are DISABLED by default.
147  *
148  * @author https://github.com/slynn1324, Stefan Penndorf, Emeric Vernat
149  */

150 class PrometheusController {
151
152     private static final String METRIC_PREFIX = "javamelody_";
153     // Pre-Compiled Patterns. Pattern is thread-safe. Matcher is not.
154     private static final Pattern CAMEL_TO_SNAKE_PATTERN = Pattern.compile("([a-z])([A-Z]+)");
155     private static final Pattern SANITIZE_TO_UNDERSCORE_PATTERN = Pattern.compile("[- :]");
156     private static final Pattern SANITIZE_REMOVE_PATTERN = Pattern.compile("[^a-z0-9_]");
157
158     private static final String EMPTY_STRING = "";
159     private static final String UNDERSCORE = "_";
160
161     private enum MetricType {
162         GAUGE("gauge"), COUNTER("counter");
163
164         private final String code;
165
166         MetricType(String code) {
167             this.code = code;
168         }
169
170         public String getCode() {
171             return code;
172         }
173     }
174
175     private final JavaInformations javaInformations;
176     private final Collector collector;
177     private final PrintWriter out;
178     private final DecimalFormat decimalFormat;
179
180     PrometheusController(List<JavaInformations> javaInformations, Collector collector,
181             PrintWriter out) throws IOException {
182         super();
183         assert javaInformations != null && !javaInformations.isEmpty();
184         assert collector != null;
185         assert out != null;
186         // it doesn't make much sense to use a JavaMelody collector server with Prometheus
187         // (which is effectively it's own collector server)
188         // and at least not for several nodes in a single application
189         if (javaInformations.size() > 1) {
190             throw new IOException(
191                     "Prometheus from collector server is not supported for several nodes in one application"
192                             + " - configure Prometheus to scrape nodes directly or declare several applications in the collector server.");
193         }
194         this.javaInformations = javaInformations.get(0);
195         this.collector = collector;
196         this.out = out;
197
198         decimalFormat = new DecimalFormat();
199         final DecimalFormatSymbols decimalFormatSymbols = DecimalFormatSymbols
200                 .getInstance(Locale.US);
201         // setNaN for #806: on Java 8 and before, decimalFormat prints \uFFFD ('<?>') instead of NaN
202         decimalFormatSymbols.setNaN("NaN");
203         decimalFormat.setDecimalFormatSymbols(decimalFormatSymbols);
204         decimalFormat.setGroupingUsed(false);
205         decimalFormat.setMinimumIntegerDigits(1);
206         decimalFormat.setMaximumFractionDigits(15);
207     }
208
209     /**
210      * Produce the full report.
211      * @param includeLastValue boolean
212      * @throws IOException e
213      */

214     void report(boolean includeLastValue) throws IOException {
215         // see https://prometheus.io/docs/instrumenting/exposition_formats/ for text format
216         // memory
217         reportOnMemoryInformations(javaInformations.getMemoryInformations());
218
219         // jvm & system
220         reportOnJavaInformations();
221
222         // tomcat
223         if (javaInformations.getTomcatInformationsList() != null) {
224             reportOnTomcatInformations();
225         }
226
227         // caches
228         if (javaInformations.isCacheEnabled()) {
229             reportOnCacheInformations();
230         }
231         if (javaInformations.isJCacheEnabled()) {
232             reportOnJCacheInformations();
233         }
234
235         reportOnCollector();
236
237         if (includeLastValue) {
238             reportOnLastValues();
239         }
240     }
241
242     // CHECKSTYLE:OFF
243     private void reportOnCacheInformations() { // NOPMD
244         // CHECKSTYLE:ON
245         final List<CacheInformations> cacheInformationsList = javaInformations
246                 .getCacheInformationsList();
247         final Map<String, CacheInformations> cacheInfos = new LinkedHashMap<>(
248                 cacheInformationsList.size());
249         for (final CacheInformations cacheInfo : cacheInformationsList) {
250             final String fields = "{cache_name=\"" + sanitizeName(cacheInfo.getName()) + "\"}";
251             cacheInfos.put(fields, cacheInfo);
252         }
253         printHeader(MetricType.GAUGE, "cache_in_memory_count""cache in memory count");
254         for (final Map.Entry<String, CacheInformations> entry : cacheInfos.entrySet()) {
255             printLongWithFields("cache_in_memory_count", entry.getKey(),
256                     entry.getValue().getInMemoryObjectCount());
257         }
258         printHeader(MetricType.GAUGE, "cache_in_memory_used_pct""in memory used percent");
259         for (final Map.Entry<String, CacheInformations> entry : cacheInfos.entrySet()) {
260             printDoubleWithFields("cache_in_memory_used_pct", entry.getKey(),
261                     (double) entry.getValue().getInMemoryPercentUsed() / 100);
262         }
263         printHeader(MetricType.GAUGE, "cache_in_memory_hits_pct""cache in memory hit percent");
264         for (final Map.Entry<String, CacheInformations> entry : cacheInfos.entrySet()) {
265             printDoubleWithFields("cache_in_memory_hits_pct", entry.getKey(),
266                     (double) entry.getValue().getInMemoryHitsRatio() / 100);
267         }
268         printHeader(MetricType.GAUGE, "cache_on_disk_count""cache on disk count");
269         for (final Map.Entry<String, CacheInformations> entry : cacheInfos.entrySet()) {
270             printLongWithFields("cache_on_disk_count", entry.getKey(),
271                     entry.getValue().getOnDiskObjectCount());
272         }
273         printHeader(MetricType.GAUGE, "cache_hits_pct""cache hits percent");
274         for (final Map.Entry<String, CacheInformations> entry : cacheInfos.entrySet()) {
275             printDoubleWithFields("cache_hits_pct", entry.getKey(),
276                     (double) entry.getValue().getHitsRatio() / 100);
277         }
278         printHeader(MetricType.COUNTER, "cache_in_memory_hits_count",
279                 "total cache in memory hit count");
280         for (final Map.Entry<String, CacheInformations> entry : cacheInfos.entrySet()) {
281             printLongWithFields("cache_in_memory_hits_count", entry.getKey(),
282                     entry.getValue().getInMemoryHits());
283         }
284         printHeader(MetricType.COUNTER, "cache_hits_count""total cache hit count");
285         for (final Map.Entry<String, CacheInformations> entry : cacheInfos.entrySet()) {
286             printLongWithFields("cache_hits_count", entry.getKey(),
287                     entry.getValue().getCacheHits());
288         }
289         printHeader(MetricType.COUNTER, "cache_misses_count""total cache misses count");
290         for (final Map.Entry<String, CacheInformations> entry : cacheInfos.entrySet()) {
291             printLongWithFields("cache_misses_count", entry.getKey(),
292                     entry.getValue().getCacheMisses());
293         }
294     }
295
296     private void reportOnJCacheInformations() { // NOPMD
297         final List<JCacheInformations> jcacheInformationsList = javaInformations
298                 .getJCacheInformationsList();
299         final Map<String, JCacheInformations> cacheInfos = new LinkedHashMap<>(
300                 jcacheInformationsList.size());
301         for (final JCacheInformations cacheInfo : jcacheInformationsList) {
302             final String fields = "{cache_name=\"" + sanitizeName(cacheInfo.getName()) + "\"}";
303             cacheInfos.put(fields, cacheInfo);
304         }
305         printHeader(MetricType.GAUGE, "jcache_hits_pct""cache hits percent");
306         for (final Map.Entry<String, JCacheInformations> entry : cacheInfos.entrySet()) {
307             printDoubleWithFields("jcache_hits_pct", entry.getKey(),
308                     (double) entry.getValue().getHitsRatio() / 100);
309         }
310         printHeader(MetricType.COUNTER, "jcache_hits_count""total cache hit count");
311         for (final Map.Entry<String, JCacheInformations> entry : cacheInfos.entrySet()) {
312             printLongWithFields("jcache_hits_count", entry.getKey(),
313                     entry.getValue().getCacheHits());
314         }
315         printHeader(MetricType.COUNTER, "jcache_misses_count""total cache misses count");
316         for (final Map.Entry<String, JCacheInformations> entry : cacheInfos.entrySet()) {
317             printLongWithFields("jcache_misses_count", entry.getKey(),
318                     entry.getValue().getCacheMisses());
319         }
320     }
321
322     // CHECKSTYLE:OFF
323     private void reportOnTomcatInformations() { // NOPMD
324         // CHECKSTYLE:ON
325         final Map<String, TomcatInformations> tcInfos = new LinkedHashMap<>();
326         for (final TomcatInformations tcInfo : javaInformations.getTomcatInformationsList()) {
327             if (tcInfo.getRequestCount() > 0) {
328                 final String fields = "{tomcat_name=\"" + sanitizeName(tcInfo.getName()) + "\"}";
329                 tcInfos.put(fields, tcInfo);
330             }
331         }
332         if (tcInfos.isEmpty()) {
333             return;
334         }
335         printHeader(MetricType.GAUGE, "tomcat_threads_max""tomcat max threads");
336         for (final Map.Entry<String, TomcatInformations> entry : tcInfos.entrySet()) {
337             printLongWithFields("tomcat_threads_max", entry.getKey(),
338                     entry.getValue().getMaxThreads());
339         }
340         printHeader(MetricType.GAUGE, "tomcat_thread_busy_count""tomcat currently busy threads");
341         for (final Map.Entry<String, TomcatInformations> entry : tcInfos.entrySet()) {
342             printLongWithFields("tomcat_thread_busy_count", entry.getKey(),
343                     entry.getValue().getCurrentThreadsBusy());
344         }
345         printHeader(MetricType.COUNTER, "tomcat_received_bytes""tomcat total received bytes");
346         for (final Map.Entry<String, TomcatInformations> entry : tcInfos.entrySet()) {
347             printLongWithFields("tomcat_received_bytes", entry.getKey(),
348                     entry.getValue().getBytesReceived());
349         }
350         printHeader(MetricType.COUNTER, "tomcat_sent_bytes""tomcat total sent bytes");
351         for (final Map.Entry<String, TomcatInformations> entry : tcInfos.entrySet()) {
352             printLongWithFields("tomcat_sent_bytes", entry.getKey(),
353                     entry.getValue().getBytesSent());
354         }
355         printHeader(MetricType.COUNTER, "tomcat_request_count""tomcat total request count");
356         for (final Map.Entry<String, TomcatInformations> entry : tcInfos.entrySet()) {
357             printLongWithFields("tomcat_request_count", entry.getKey(),
358                     entry.getValue().getRequestCount());
359         }
360         printHeader(MetricType.COUNTER, "tomcat_error_count""tomcat total error count");
361         for (final Map.Entry<String, TomcatInformations> entry : tcInfos.entrySet()) {
362             printLongWithFields("tomcat_error_count", entry.getKey(),
363                     entry.getValue().getErrorCount());
364         }
365         printHeader(MetricType.COUNTER, "tomcat_processing_time_millis",
366                 "tomcat total processing time");
367         for (final Map.Entry<String, TomcatInformations> entry : tcInfos.entrySet()) {
368             printLongWithFields("tomcat_processing_time_millis", entry.getKey(),
369                     entry.getValue().getProcessingTime());
370         }
371         printHeader(MetricType.GAUGE, "tomcat_max_time_millis",
372                 "tomcat max time for single request");
373         for (final Map.Entry<String, TomcatInformations> entry : tcInfos.entrySet()) {
374             printLongWithFields("tomcat_max_time_millis", entry.getKey(),
375                     entry.getValue().getMaxTime());
376         }
377     }
378
379     /**
380      * Reports on hits, errors, and duration sum for all counters in the collector.
381      *
382      * Bypasses the {@link JRobin#getLastValue()} methods to provide real-time counters as well as
383      * improving performance from bypassing JRobin reads in the getLastValue() method.
384      */

385     private void reportOnCollector() {
386         for (final Counter counter : collector.getCounters()) {
387             if (!counter.isDisplayed()) {
388                 continue;
389             }
390             final List<CounterRequest> requests = counter.getRequests();
391             long hits = 0;
392             long duration = 0;
393             long errors = 0;
394             for (final CounterRequest cr : requests) {
395                 hits += cr.getHits();
396                 duration += cr.getDurationsSum();
397                 errors += cr.getSystemErrors();
398             }
399
400             final String sanitizedName = sanitizeName(counter.getName());
401             printLong(MetricType.COUNTER, sanitizedName + "_hits_count""javamelody counter",
402                     hits);
403             if (!counter.isErrorCounter() || counter.isJobCounter()) {
404                 // errors has no sense for the error and log counters
405                 printLong(MetricType.COUNTER, sanitizedName + "_errors_count""javamelody counter",
406                         errors);
407             }
408             if (duration >= 0) {
409                 // duration is negative and has no sense for the log counter
410                 printLong(MetricType.COUNTER, sanitizedName + "_duration_millis",
411                         "javamelody counter", duration);
412             }
413         }
414     }
415
416     /**
417      * Includes the traditional 'graph' fields from the 'lastValue' API.
418      *
419      * These fields are summary fields aggregated over `javamelody.resolutions-seconds` (default 60), which
420      * is normally an odd thing to pass to Prometheus.  Most (all?) of these can be calculated inside
421      * Prometheus from the Collector stats.
422      *
423      * Note: This lookup seems to take the longest execution time -- 5-10ms per request due to JRobin reads.
424      *
425      * Disabled by default.  To enable set the 'prometheus-include-last-value' property to 'true'.
426      *
427      * @throws IOException e
428      */

429     private void reportOnLastValues() throws IOException {
430         Collection<JRobin> jrobins = collector.getDisplayedCounterJRobins();
431         for (final JRobin jrobin : jrobins) {
432             printDouble(MetricType.GAUGE, "last_value_" + camelToSnake(jrobin.getName()),
433                     "javamelody value per minute", jrobin.getLastValue());
434         }
435
436         jrobins = collector.getDisplayedOtherJRobins();
437         for (final JRobin jrobin : jrobins) {
438             printDouble(MetricType.GAUGE, "last_value_" + camelToSnake(jrobin.getName()),
439                     "javamelody value per minute", jrobin.getLastValue());
440         }
441     }
442
443     /**
444      * Reports on information vailable in the {@link JavaInformations} class.
445      */

446     private void reportOnJavaInformations() {
447         // sessions
448         if (javaInformations.getSessionCount() >= 0) {
449             printLong(MetricType.GAUGE, "sessions_active_count""active session count",
450                     javaInformations.getSessionCount());
451             printLong(MetricType.GAUGE, "sessions_age_avg_minutes""session avg age in minutes",
452                     javaInformations.getSessionMeanAgeInMinutes());
453         }
454
455         // connections
456         if (!Parameters.isNoDatabase()) {
457             printLong(MetricType.COUNTER, "transactions_count""transactions count",
458                     javaInformations.getTransactionCount());
459             printLong(MetricType.GAUGE, "connections_used_count""used connections count",
460                     javaInformations.getUsedConnectionCount());
461             printLong(MetricType.GAUGE, "connections_active_count""active connections",
462                     javaInformations.getActiveConnectionCount());
463             if (javaInformations.getMaxConnectionCount() > 0) {
464                 printLong(MetricType.GAUGE, "connections_max_count""max connections",
465                         javaInformations.getMaxConnectionCount());
466                 printDouble(MetricType.GAUGE, "connections_used_pct""used connections percentage",
467                         javaInformations.getUsedConnectionPercentage());
468             }
469         }
470
471         // system
472         if (javaInformations.getSystemLoadAverage() >= 0) {
473             printDouble(MetricType.GAUGE, "system_load_avg""system load average",
474                     javaInformations.getSystemLoadAverage());
475         }
476         if (javaInformations.getSystemCpuLoad() >= 0) {
477             printDouble(MetricType.GAUGE, "system_cpu_load_pct""system cpu load",
478                     javaInformations.getSystemCpuLoad());
479         }
480         if (javaInformations.getUnixOpenFileDescriptorCount() >= 0) {
481             printDouble(MetricType.GAUGE, "system_unix_file_descriptors_open_count",
482                     "unix open file descriptors count",
483                     javaInformations.getUnixOpenFileDescriptorCount());
484             printDouble(MetricType.GAUGE, "system_unix_file_descriptors_max",
485                     "unix file descriptors max", javaInformations.getUnixMaxFileDescriptorCount());
486             printDouble(MetricType.GAUGE, "system_unix_file_descriptors_open_pct",
487                     "unix open file descriptors percentage",
488                     javaInformations.getUnixOpenFileDescriptorPercentage());
489         }
490         printLong(MetricType.GAUGE, "system_tmp_space_free_bytes""tmp space available",
491                 javaInformations.getFreeDiskSpaceInTemp());
492         printLong(MetricType.GAUGE, "system_tmp_space_usable_bytes""tmp space usable",
493                 javaInformations.getUsableDiskSpaceInTemp());
494
495         // jvm
496         printLong(MetricType.GAUGE, "jvm_start_time""jvm start time",
497                 javaInformations.getStartDate().getTime());
498         printLong(MetricType.COUNTER, "jvm_cpu_millis""jvm cpu millis",
499                 javaInformations.getProcessCpuTimeMillis());
500         printLong(MetricType.GAUGE, "system_processors_count""processors available",
501                 javaInformations.getAvailableProcessors());
502
503         // threads
504         printLong(MetricType.GAUGE, "threads_count""threads count",
505                 javaInformations.getThreadCount());
506         printLong(MetricType.GAUGE, "threads_max_count""threads peak count",
507                 javaInformations.getPeakThreadCount());
508         printLong(MetricType.COUNTER, "threads_started_count""total threads started",
509                 javaInformations.getTotalStartedThreadCount());
510         printLong(MetricType.GAUGE, "threads_active_count""active thread count",
511                 javaInformations.getActiveThreadCount());
512
513         // jobs
514         if (javaInformations.isJobEnabled()) {
515             printLong(MetricType.GAUGE, "job_executing_count""executing job count",
516                     javaInformations.getCurrentlyExecutingJobCount());
517         }
518     }
519
520     private void reportOnMemoryInformations(MemoryInformations memoryInformations) {
521         printLong(MetricType.GAUGE, "memory_used_bytes""used memory in bytes",
522                 memoryInformations.getUsedMemory());
523         printLong(MetricType.GAUGE, "memory_max_bytes""max memory in bytes",
524                 memoryInformations.getMaxMemory());
525         printDouble(MetricType.GAUGE, "memory_used_pct""memory used percentage",
526                 memoryInformations.getUsedMemoryPercentage());
527         if (memoryInformations.getUsedPermGen() > 0) {
528             printLong(MetricType.GAUGE, "memory_perm_gen_used_bytes",
529                     "used perm gen memory in bytes", memoryInformations.getUsedPermGen());
530             if (memoryInformations.getMaxPermGen() > 0) {
531                 printLong(MetricType.GAUGE, "memory_perm_gen_max_bytes",
532                         "max perm gen memory in bytes", memoryInformations.getMaxPermGen());
533                 printDouble(MetricType.GAUGE, "memory_perm_gen_used_pct",
534                         "used perm gen memory percentage",
535                         memoryInformations.getUsedPermGenPercentage());
536             }
537         }
538
539         printDouble(MetricType.COUNTER, "memory_gc_millis""gc time millis",
540                 memoryInformations.getGarbageCollectionTimeMillis());
541
542         if (memoryInformations.getUsedBufferedMemory() >= 0) {
543             printLong(MetricType.GAUGE, "memory_used_buffered_bytes",
544                     "used buffered memory in bytes", memoryInformations.getUsedBufferedMemory());
545         }
546         printLong(MetricType.GAUGE, "memory_used_non_heap_bytes""used non-heap memory in bytes",
547                 memoryInformations.getUsedNonHeapMemory());
548         if (memoryInformations.getUsedSwapSpaceSize() >= 0) {
549             printLong(MetricType.GAUGE, "memory_used_swap_space_bytes",
550                     "used memory in the OS swap space in bytes",
551                     memoryInformations.getUsedSwapSpaceSize());
552         }
553         if (memoryInformations.getUsedPhysicalMemorySize() > 0) {
554             printLong(MetricType.GAUGE, "memory_used_physical_bytes",
555                     "used memory in the OS in bytes",
556                     memoryInformations.getUsedPhysicalMemorySize());
557         }
558         printLong(MetricType.GAUGE, "loaded_classes_count""loaded classes count",
559                 memoryInformations.getLoadedClassesCount());
560     }
561
562     /**
563      * Converts a camelCase or CamelCase string to camel_case
564      * @param camel String
565      * @return String
566      */

567     private static String camelToSnake(String camel) {
568         return CAMEL_TO_SNAKE_PATTERN.matcher(camel).replaceAll("$1_$2").toLowerCase(Locale.US);
569     }
570
571     /**
572      * converts to lowercase, replaces common separators with underscores, and strips all remaining non-alpha-numeric characters.
573      * @param name String
574      * @return String
575      */

576     private static String sanitizeName(String name) {
577         final String lowerCaseName = name.toLowerCase(Locale.US);
578         final String separatorReplacedName = SANITIZE_TO_UNDERSCORE_PATTERN.matcher(lowerCaseName)
579                 .replaceAll(UNDERSCORE);
580         return SANITIZE_REMOVE_PATTERN.matcher(separatorReplacedName).replaceAll(EMPTY_STRING);
581     }
582
583     // prints a long metric value, including HELP and TYPE rows
584     private void printLong(MetricType metricType, String name, String description, long value) {
585         printHeader(metricType, name, description);
586         printLongWithFields(name, null, value);
587     }
588
589     // prints a double metric value, including HELP and TYPE rows
590     private void printDouble(MetricType metricType, String name, String description, double value) {
591         printHeader(metricType, name, description);
592         printDoubleWithFields(name, null, value);
593     }
594
595     // prints a long metric value with optional fields
596     private void printLongWithFields(String name, String fields, long value) {
597         print(METRIC_PREFIX);
598         print(name);
599         if (fields != null) {
600             print(fields);
601         }
602         print(' ');
603         println(String.valueOf(value));
604     }
605
606     // prints a double metric value with optional fields
607     private void printDoubleWithFields(String name, String fields, double value) {
608         print(METRIC_PREFIX);
609         print(name);
610         if (fields != null) {
611             print(fields);
612         }
613         print(' ');
614         println(decimalFormat.format(value));
615     }
616
617     // prints the HELP and TYPE rows
618     private void printHeader(MetricType metricType, String name, String description) {
619         print("# HELP ");
620         print(METRIC_PREFIX);
621         print(name);
622         print(' ');
623         println(description);
624
625         print("# TYPE ");
626         print(METRIC_PREFIX);
627         print(name);
628         print(' ');
629         println(metricType.getCode());
630     }
631
632     private void print(String s) {
633         out.print(s);
634     }
635
636     private void print(char c) {
637         out.print(c);
638     }
639
640     private void println(String s) {
641         out.print(s);
642         // out.println() prints "\r\n" on Windows and Prometheus does not recognize "\r\n" as EOL
643         // (in Prometheus: "no token found" and in promtool check metrics:
644         // error while linting: text format parsing error in line 2: unknown metric type "gauge\r")
645         out.print('\n');
646     }
647 }
648