001/*
002 * (C) Copyright 2013-2014 Nuxeo SA (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 * Contributors:
017 *     Delbosc Benoit
018 */
019package org.nuxeo.runtime.metrics;
020
021import java.io.File;
022import java.io.Serializable;
023import java.net.InetAddress;
024import java.net.InetSocketAddress;
025import java.net.UnknownHostException;
026import java.text.DateFormat;
027import java.text.SimpleDateFormat;
028import java.util.ArrayList;
029import java.util.Arrays;
030import java.util.Calendar;
031import java.util.Collections;
032import java.util.Date;
033import java.util.List;
034import java.util.concurrent.TimeUnit;
035import java.util.stream.Stream;
036
037import javax.management.MalformedObjectNameException;
038import javax.management.ObjectName;
039
040import org.apache.commons.logging.LogFactory;
041import org.apache.log4j.LogManager;
042
043import org.nuxeo.common.Environment;
044import org.nuxeo.common.xmap.annotation.XNode;
045import org.nuxeo.common.xmap.annotation.XNodeList;
046import org.nuxeo.common.xmap.annotation.XObject;
047import org.nuxeo.runtime.api.Framework;
048import org.nuxeo.runtime.management.ServerLocator;
049
050import com.codahale.metrics.JmxAttributeGauge;
051import com.codahale.metrics.JmxReporter;
052import com.codahale.metrics.Metric;
053import com.codahale.metrics.MetricFilter;
054import com.codahale.metrics.MetricRegistry;
055import com.codahale.metrics.graphite.Graphite;
056import com.codahale.metrics.graphite.GraphiteReporter;
057import com.codahale.metrics.jvm.BufferPoolMetricSet;
058import com.codahale.metrics.jvm.FileDescriptorRatioGauge;
059import com.codahale.metrics.jvm.GarbageCollectorMetricSet;
060import com.codahale.metrics.jvm.MemoryUsageGaugeSet;
061import com.codahale.metrics.jvm.ThreadStatesGaugeSet;
062import com.codahale.metrics.log4j.InstrumentedAppender;
063
064@XObject("metrics")
065public class MetricsDescriptor implements Serializable {
066
067    private static final long serialVersionUID = 7833869486922092460L;
068
069    public MetricsDescriptor() {
070        super();
071        graphiteReporter = new GraphiteDescriptor();
072        csvReporter = new CsvDescriptor();
073        tomcatInstrumentation = new TomcatInstrumentationDescriptor();
074        log4jInstrumentation = new Log4jInstrumentationDescriptor();
075    }
076
077    @XObject(value = "graphiteReporter")
078    public static class GraphiteDescriptor {
079
080        public static final String ENABLED_PROPERTY = "metrics.graphite.enabled";
081
082        public static final String HOST_PROPERTY = "metrics.graphite.host";
083
084        public static final String PORT_PROPERTY = "metrics.graphite.port";
085
086        public static final String PERIOD_PROPERTY = "metrics.graphite.period";
087
088        public static final String PREFIX_PROPERTY = "metrics.graphite.prefix";
089
090        /**
091         * A list of metric prefixes that if defined should be kept reported, separated by commas
092         *
093         * @since 9.3
094         */
095        public static final String ALLOWED_METRICS_PROPERTY = "metrics.graphite.allowedMetrics";
096
097        /**
098         * A list of metric prefixes that if defined should not be reported, separated by commas
099         *
100         * @since 9.3
101         */
102        public static final String DENIED_METRICS_PROPERTY = "metrics.graphite.deniedMetrics";
103
104        /**
105         * @since 9.3
106         */
107        public static final String DEFAULT_ALLOWED_METRICS = "nuxeo.cache.user-entry-cache,nuxeo.cache.group-entry-cache,nuxeo.directories.userDirectory,nuxeo.directories.groupDirectory";
108
109        /**
110         * @since 9.3
111         */
112        public static final String DEFAULT_DENIED_METRICS = "nuxeo.cache,nuxeo.directories";
113
114        /**
115         * @since 9.3
116         */
117        public static final String ALL_METRICS = "ALL";
118
119        @XNode("@enabled")
120        protected Boolean enabled = Boolean.valueOf(Framework.getProperty(ENABLED_PROPERTY, "false"));
121
122        @XNode("@host")
123        public String host = Framework.getProperty(HOST_PROPERTY, "0.0.0.0");
124
125        @XNode("@port")
126        public Integer port = Integer.valueOf(Framework.getProperty(PORT_PROPERTY, "2030"));
127
128        @XNode("@periodInSecond")
129        public Integer period = Integer.valueOf(Framework.getProperty(PERIOD_PROPERTY, "10"));
130
131        @XNode("@prefix")
132        public String prefix = prefix();
133
134        /**
135         * A list of metric prefixes that if defined should be kept reported
136         *
137         * @since 9.3
138         */
139        @XNodeList(value = "allowedMetrics/metric", type = ArrayList.class, componentType = String.class)
140        public List<String> allowedMetrics = Arrays.asList(
141                Framework.getProperty(ALLOWED_METRICS_PROPERTY, DEFAULT_ALLOWED_METRICS).split(","));
142
143        /**
144         * A list of metric prefixes that if defined should not be reported
145         *
146         * @since 9.3
147         */
148        @XNodeList(value = "deniedMetrics/metric", type = ArrayList.class, componentType = String.class)
149        public List<String> deniedMetrics = Arrays.asList(
150                Framework.getProperty(DENIED_METRICS_PROPERTY, DEFAULT_DENIED_METRICS).split(","));
151
152        public String prefix() {
153            if (prefix == null) {
154                prefix = Framework.getProperty(PREFIX_PROPERTY, "servers.${hostname}.nuxeo");
155            }
156            String hostname;
157            try {
158                hostname = InetAddress.getLocalHost().getHostName().split("\\.")[0];
159            } catch (UnknownHostException e) {
160                hostname = "unknown";
161            }
162            return prefix.replace("${hostname}", hostname);
163        }
164
165        public boolean filter(String name) {
166            return allowedMetrics.stream().anyMatch(f -> ALL_METRICS.equals(f) || name.startsWith(f))
167                    || deniedMetrics.stream().noneMatch(f -> ALL_METRICS.equals(f) || name.startsWith(f));
168        }
169
170        @Override
171        public String toString() {
172            return String.format("graphiteReporter %s prefix: %s, host: %s, port: %d, period: %d", enabled ? "enabled"
173                    : "disabled", prefix, host, port, period);
174        }
175
176        protected GraphiteReporter reporter;
177
178        public void enable(MetricRegistry registry) {
179            if (!enabled) {
180                return;
181            }
182
183            InetSocketAddress address = new InetSocketAddress(host, port);
184            Graphite graphite = new Graphite(address);
185            reporter = GraphiteReporter.forRegistry(registry)
186                                       .convertRatesTo(TimeUnit.SECONDS)
187                                       .convertDurationsTo(TimeUnit.MICROSECONDS)
188                                       .prefixedWith(prefix())
189                                       .filter((name, metric) -> filter(name))
190                                       .build(graphite);
191            reporter.start(period, TimeUnit.SECONDS);
192        }
193
194        public void disable(MetricRegistry registry) {
195            if (reporter == null) {
196                return;
197            }
198            try {
199                reporter.stop();
200            } finally {
201                reporter = null;
202            }
203        }
204    }
205
206    @XObject(value = "csvReporter")
207    public static class CsvDescriptor {
208
209        public static final String ENABLED_PROPERTY = "metrics.csv.enabled";
210
211        public static final String PERIOD_PROPERTY = "metrics.csv.period";
212
213        public static final String OUTPUT_PROPERTY = "metrics.csv.output";
214
215        @XNode("@output")
216        public File outputDir = outputDir();
217
218        @XNode("@periodInSecond")
219        public Integer period = 10;
220
221        @XNode("@enabled")
222        public boolean enabled = Framework.isBooleanPropertyTrue(ENABLED_PROPERTY);
223
224        public int getPeriod() {
225            if (period == null) {
226                period = Integer.valueOf(Framework.getProperty(PERIOD_PROPERTY, "10"));
227            }
228            return period;
229        }
230
231        protected File outputDir() {
232            String path = Framework.getProperty(OUTPUT_PROPERTY, Framework.getProperty(Environment.NUXEO_LOG_DIR));
233            DateFormat df = new SimpleDateFormat("yyyyMMdd-HHmmss");
234            Date today = Calendar.getInstance().getTime();
235            outputDir = new File(path, "metrics-" + df.format(today));
236            return outputDir;
237        }
238
239        @Override
240        public String toString() {
241            return String.format("csvReporter %s, outputDir: %s, period: %d", enabled ? "enabled" : "disabled",
242                    outputDir().toString(), getPeriod());
243        }
244
245        protected CsvReporter reporter;
246
247        public void enable(MetricRegistry registry) {
248            if (!enabled) {
249                return;
250            }
251            File parentDir = outputDir.getParentFile();
252            if (parentDir.exists() && parentDir.isDirectory()) {
253                outputDir.mkdir();
254                reporter = CsvReporter.forRegistry(registry).build(outputDir);
255                reporter.start(Long.valueOf(period), TimeUnit.SECONDS);
256            } else {
257                enabled = false;
258                LogFactory.getLog(MetricsServiceImpl.class).error("Invalid output directory, disabling: " + this);
259            }
260        }
261
262        public void disable(MetricRegistry registry) {
263            if (reporter == null) {
264                return;
265            }
266            try {
267                reporter.stop();
268            } finally {
269                reporter = null;
270            }
271        }
272
273    }
274
275    @XObject(value = "log4jInstrumentation")
276    public static class Log4jInstrumentationDescriptor {
277
278        public static final String ENABLED_PROPERTY = "metrics.log4j.enabled";
279
280        @XNode("@enabled")
281        protected boolean enabled = Boolean.parseBoolean(Framework.getProperty(ENABLED_PROPERTY, "false"));
282
283        private InstrumentedAppender appender;
284
285        @Override
286        public String toString() {
287            return String.format("log4jInstrumentation %s", enabled ? "enabled" : "disabled");
288        }
289
290        public void enable(MetricRegistry registry) {
291            if (!enabled) {
292                return;
293            }
294            LogFactory.getLog(MetricsServiceImpl.class).info(this);
295            appender = new InstrumentedAppender(registry);
296            appender.activateOptions();
297            LogManager.getRootLogger().addAppender(appender);
298        }
299
300        public void disable(MetricRegistry registry) {
301            if (appender == null) {
302                return;
303            }
304            try {
305                LogManager.getRootLogger().removeAppender(appender);
306            } finally {
307                appender = null;
308            }
309        }
310
311    }
312
313    @XObject(value = "tomcatInstrumentation")
314    public static class TomcatInstrumentationDescriptor {
315
316        public static final String ENABLED_PROPERTY = "metrics.tomcat.enabled";
317
318        @XNode("@enabled")
319        protected boolean enabled = Boolean.parseBoolean(Framework.getProperty(ENABLED_PROPERTY, "false"));
320
321        @Override
322        public String toString() {
323            return String.format("tomcatInstrumentation %s", enabled ? "enabled" : "disabled");
324        }
325
326        protected void registerTomcatGauge(String mbean, String attribute, MetricRegistry registry, String name) {
327            try {
328                registry.register(MetricRegistry.name("tomcat", name), new JmxAttributeGauge(new ObjectName(mbean),
329                        attribute));
330            } catch (MalformedObjectNameException | IllegalArgumentException e) {
331                throw new UnsupportedOperationException("Cannot compute object name of " + mbean, e);
332            }
333        }
334
335        public void enable(MetricRegistry registry) {
336            if (!enabled) {
337                return;
338            }
339            LogFactory.getLog(MetricsServiceImpl.class).info(this);
340            // TODO: do not hard code the common datasource
341            // nameenable(registry)
342            String pool = "Catalina:type=DataSource,class=javax.sql.DataSource,name=\"jdbc/nuxeo\"";
343            String connector = String.format("Catalina:type=ThreadPool,name=\"http-bio-%s-%s\"",
344                    Framework.getProperty("nuxeo.bind.address", "0.0.0.0"),
345                    Framework.getProperty("nuxeo.bind.port", "8080"));
346            String requestProcessor = String.format("Catalina:type=GlobalRequestProcessor,name=\"http-bio-%s-%s\"",
347                    Framework.getProperty("nuxeo.bind.address", "0.0.0.0"),
348                    Framework.getProperty("nuxeo.bind.port", "8080"));
349            String manager = "Catalina:type=Manager,context=/nuxeo,host=localhost";
350            registerTomcatGauge(pool, "numActive", registry, "jdbc-numActive");
351            registerTomcatGauge(pool, "numIdle", registry, "jdbc-numIdle");
352            registerTomcatGauge(connector, "currentThreadCount", registry, "currentThreadCount");
353            registerTomcatGauge(connector, "currentThreadsBusy", registry, "currentThreadBusy");
354            registerTomcatGauge(requestProcessor, "errorCount", registry, "errorCount");
355            registerTomcatGauge(requestProcessor, "requestCount", registry, "requestCount");
356            registerTomcatGauge(requestProcessor, "processingTime", registry, "processingTime");
357            registerTomcatGauge(manager, "activeSessions", registry, "activeSessions");
358        }
359
360        public void disable(MetricRegistry registry) {
361            registry.remove("tomcat.jdbc-numActive");
362            registry.remove("tomcat.jdbc-numIdle");
363            registry.remove("tomcat.currentThreadCount");
364            registry.remove("tomcat.currentThreadBusy");
365            registry.remove("tomcat.errorCount");
366            registry.remove("tomcat.requestCount");
367            registry.remove("tomcat.processingTime");
368            registry.remove("tomcat.activeSessions");
369        }
370    }
371
372    @XObject(value = "jvmInstrumentation")
373    public static class JvmInstrumentationDescriptor {
374
375        public static final String ENABLED_PROPERTY = "metrics.jvm.enabled";
376
377        @XNode("@enabled")
378        protected boolean enabled = Boolean.parseBoolean(Framework.getProperty(ENABLED_PROPERTY, "true"));
379
380        public void enable(MetricRegistry registry) {
381            if (!enabled) {
382                return;
383            }
384            registry.register("jvm.memory", new MemoryUsageGaugeSet());
385            registry.register("jvm.garbage", new GarbageCollectorMetricSet());
386            registry.register("jvm.threads", new ThreadStatesGaugeSet());
387            registry.register("jvm.files", new FileDescriptorRatioGauge());
388            registry.register("jvm.buffers", new BufferPoolMetricSet(
389                    Framework.getService(ServerLocator.class).lookupServer()));
390        }
391
392        public void disable(MetricRegistry registry) {
393            if (!enabled) {
394                return;
395            }
396            registry.removeMatching(new MetricFilter() {
397
398                @Override
399                public boolean matches(String name, Metric metric) {
400                    return name.startsWith("jvm.");
401                }
402            });
403
404        }
405    }
406
407    @XNode("graphiteReporter")
408    public GraphiteDescriptor graphiteReporter = new GraphiteDescriptor();
409
410    @XNode("csvReporter")
411    public CsvDescriptor csvReporter = new CsvDescriptor();
412
413    @XNode("log4jInstrumentation")
414    public Log4jInstrumentationDescriptor log4jInstrumentation = new Log4jInstrumentationDescriptor();
415
416    @XNode("tomcatInstrumentation")
417    public TomcatInstrumentationDescriptor tomcatInstrumentation = new TomcatInstrumentationDescriptor();
418
419    @XNode(value = "jvmInstrumentation")
420    public JvmInstrumentationDescriptor jvmInstrumentation = new JvmInstrumentationDescriptor();
421
422    protected JmxReporter jmxReporter;
423
424    public void enable(MetricRegistry registry) {
425        jmxReporter = JmxReporter.forRegistry(registry).build();
426        jmxReporter.start();
427        graphiteReporter.enable(registry);
428        csvReporter.enable(registry);
429        log4jInstrumentation.enable(registry);
430        tomcatInstrumentation.enable(registry);
431        jvmInstrumentation.enable(registry);
432    }
433
434    public void disable(MetricRegistry registry) {
435        try {
436            graphiteReporter.disable(registry);
437            csvReporter.disable(registry);
438            log4jInstrumentation.disable(registry);
439            tomcatInstrumentation.disable(registry);
440            jvmInstrumentation.disable(registry);
441            jmxReporter.stop();
442        } finally {
443            jmxReporter = null;
444        }
445    }
446
447}