001/*
002 * (C) Copyright 2013-2014 Nuxeo SA (http://nuxeo.com/) and contributors.
003 *
004 * All rights reserved. This program and the accompanying materials
005 * are made available under the terms of the GNU Lesser General Public License
006 * (LGPL) version 2.1 which accompanies this distribution, and is available at
007 * http://www.gnu.org/licenses/lgpl-2.1.html
008 *
009 * This library is distributed in the hope that it will be useful,
010 * but WITHOUT ANY WARRANTY; without even the implied warranty of
011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
012 * Lesser General Public License for more details.
013 *
014 * Contributors:
015 *     Delbosc Benoit
016 */
017package org.nuxeo.runtime.metrics;
018
019import java.io.File;
020import java.io.Serializable;
021import java.net.InetAddress;
022import java.net.InetSocketAddress;
023import java.net.UnknownHostException;
024import java.text.DateFormat;
025import java.text.SimpleDateFormat;
026import java.util.Calendar;
027import java.util.Date;
028import java.util.concurrent.TimeUnit;
029
030import javax.management.MalformedObjectNameException;
031import javax.management.ObjectName;
032
033import org.apache.commons.logging.LogFactory;
034import org.apache.log4j.LogManager;
035
036import org.nuxeo.common.Environment;
037import org.nuxeo.common.xmap.annotation.XNode;
038import org.nuxeo.common.xmap.annotation.XObject;
039import org.nuxeo.runtime.api.Framework;
040import org.nuxeo.runtime.management.ServerLocator;
041
042import com.codahale.metrics.CsvReporter;
043import com.codahale.metrics.JmxAttributeGauge;
044import com.codahale.metrics.JmxReporter;
045import com.codahale.metrics.Metric;
046import com.codahale.metrics.MetricFilter;
047import com.codahale.metrics.MetricRegistry;
048import com.codahale.metrics.graphite.Graphite;
049import com.codahale.metrics.graphite.GraphiteReporter;
050import com.codahale.metrics.jvm.BufferPoolMetricSet;
051import com.codahale.metrics.jvm.FileDescriptorRatioGauge;
052import com.codahale.metrics.jvm.GarbageCollectorMetricSet;
053import com.codahale.metrics.jvm.MemoryUsageGaugeSet;
054import com.codahale.metrics.jvm.ThreadStatesGaugeSet;
055import com.codahale.metrics.log4j.InstrumentedAppender;
056
057@XObject("metrics")
058public class MetricsDescriptor implements Serializable {
059
060    private static final long serialVersionUID = 7833869486922092460L;
061
062    public MetricsDescriptor() {
063        super();
064        graphiteReporter = new GraphiteDescriptor();
065        csvReporter = new CsvDescriptor();
066        tomcatInstrumentation = new TomcatInstrumentationDescriptor();
067        log4jInstrumentation = new Log4jInstrumentationDescriptor();
068    }
069
070    @XObject(value = "graphiteReporter")
071    public static class GraphiteDescriptor {
072
073        public static final String ENABLED_PROPERTY = "metrics.graphite.enabled";
074
075        public static final String HOST_PROPERTY = "metrics.graphite.host";
076
077        public static final String PORT_PROPERTY = "metrics.graphite.port";
078
079        public static final String PERIOD_PROPERTY = "metrics.graphite.period";
080
081        public static final String PREFIX_PROPERTY = "metrics.graphite.prefix";
082
083        @XNode("@enabled")
084        protected Boolean enabled = Boolean.valueOf(Framework.getProperty(ENABLED_PROPERTY, "false"));
085
086        @XNode("@host")
087        public String host = Framework.getProperty(HOST_PROPERTY, "0.0.0.0");
088
089        @XNode("@port")
090        public Integer port = Integer.valueOf(Framework.getProperty(PORT_PROPERTY, "2030"));
091
092        @XNode("@periodInSecond")
093        public Integer period = Integer.valueOf(Framework.getProperty(PERIOD_PROPERTY, "10"));
094
095        @XNode("@prefix")
096        public String prefix = prefix();
097
098        public String prefix() {
099            if (prefix == null) {
100                prefix = Framework.getProperty(PREFIX_PROPERTY, "servers.${hostname}.nuxeo");
101            }
102            String hostname;
103            try {
104                hostname = InetAddress.getLocalHost().getHostName().split("\\.")[0];
105            } catch (UnknownHostException e) {
106                hostname = "unknown";
107            }
108            return prefix.replace("${hostname}", hostname);
109        }
110
111        @Override
112        public String toString() {
113            return String.format("graphiteReporter %s prefix: %s, host: %s, port: %d, period: %d", enabled ? "enabled"
114                    : "disabled", prefix, host, port, period);
115        }
116
117        protected GraphiteReporter reporter;
118
119        public void enable(MetricRegistry registry) {
120            if (!enabled) {
121                return;
122            }
123            InetSocketAddress address = new InetSocketAddress(host, port);
124            Graphite graphite = new Graphite(address);
125            reporter = GraphiteReporter.forRegistry(registry).convertRatesTo(TimeUnit.SECONDS).convertDurationsTo(
126                    TimeUnit.MICROSECONDS).prefixedWith(prefix()).build(graphite);
127            reporter.start(period, TimeUnit.SECONDS);
128        }
129
130        public void disable(MetricRegistry registry) {
131            if (reporter == null) {
132                return;
133            }
134            try {
135                reporter.stop();
136            } finally {
137                reporter = null;
138            }
139        }
140    }
141
142    @XObject(value = "csvReporter")
143    public static class CsvDescriptor {
144
145        public static final String ENABLED_PROPERTY = "metrics.csv.enabled";
146
147        public static final String PERIOD_PROPERTY = "metrics.csv.period";
148
149        public static final String OUTPUT_PROPERTY = "metrics.csv.output";
150
151        @XNode("@output")
152        public File outputDir = outputDir();
153
154        @XNode("@periodInSecond")
155        public Integer period = 10;
156
157        @XNode("@enabled")
158        public boolean enabled = Framework.isBooleanPropertyTrue(ENABLED_PROPERTY);
159
160        public int getPeriod() {
161            if (period == null) {
162                period = Integer.valueOf(Framework.getProperty(PERIOD_PROPERTY, "10"));
163            }
164            return period;
165        }
166
167        protected File outputDir() {
168            String path = Framework.getProperty(OUTPUT_PROPERTY, Framework.getProperty(Environment.NUXEO_LOG_DIR));
169            DateFormat df = new SimpleDateFormat("yyyyMMdd-HHmmss");
170            Date today = Calendar.getInstance().getTime();
171            outputDir = new File(path, "metrics-" + df.format(today));
172            return outputDir;
173        }
174
175        @Override
176        public String toString() {
177            return String.format("csvReporter %s, outputDir: %s, period: %d", enabled ? "enabled" : "disabled",
178                    outputDir().toString(), getPeriod());
179        }
180
181        protected CsvReporter reporter;
182
183        public void enable(MetricRegistry registry) {
184            if (!enabled) {
185                return;
186            }
187            File parentDir = outputDir.getParentFile();
188            if (parentDir.exists() && parentDir.isDirectory()) {
189                outputDir.mkdir();
190                reporter = CsvReporter.forRegistry(registry).build(outputDir);
191                reporter.start(Long.valueOf(period), TimeUnit.SECONDS);
192            } else {
193                enabled = false;
194                LogFactory.getLog(MetricsServiceImpl.class).error("Invalid output directory, disabling: " + this);
195            }
196        }
197
198        public void disable(MetricRegistry registry) {
199            if (reporter == null) {
200                return;
201            }
202            try {
203                reporter.stop();
204            } finally {
205                reporter = null;
206            }
207        }
208
209    }
210
211    @XObject(value = "log4jInstrumentation")
212    public static class Log4jInstrumentationDescriptor {
213
214        public static final String ENABLED_PROPERTY = "metrics.log4j.enabled";
215
216        @XNode("@enabled")
217        protected boolean enabled = Boolean.getBoolean(Framework.getProperty(ENABLED_PROPERTY, "false"));
218
219        private InstrumentedAppender appender;
220
221        @Override
222        public String toString() {
223            return String.format("log4jInstrumentation %s", enabled ? "enabled" : "disabled");
224        }
225
226        public void enable(MetricRegistry registry) {
227            if (!enabled) {
228                return;
229            }
230            LogFactory.getLog(MetricsServiceImpl.class).info(this);
231            appender = new InstrumentedAppender(registry);
232            LogManager.getRootLogger().addAppender(appender);
233        }
234
235        public void disable(MetricRegistry registry) {
236            if (appender == null) {
237                return;
238            }
239            try {
240                LogManager.getRootLogger().removeAppender(appender);
241            } finally {
242                appender = null;
243            }
244        }
245
246    }
247
248    @XObject(value = "tomcatInstrumentation")
249    public static class TomcatInstrumentationDescriptor {
250
251        public static final String ENABLED_PROPERTY = "metrics.tomcat.enabled";
252
253        @XNode("@enabled")
254        protected boolean enabled = Boolean.parseBoolean(Framework.getProperty(ENABLED_PROPERTY, "false"));
255
256        @Override
257        public String toString() {
258            return String.format("tomcatInstrumentation %s", enabled ? "enabled" : "disabled");
259        }
260
261        protected void registerTomcatGauge(String mbean, String attribute, MetricRegistry registry, String name) {
262            try {
263                registry.register(MetricRegistry.name("tomcat", name), new JmxAttributeGauge(new ObjectName(mbean),
264                        attribute));
265            } catch (MalformedObjectNameException | IllegalArgumentException e) {
266                throw new UnsupportedOperationException("Cannot compute object name of " + mbean, e);
267            }
268        }
269
270        public void enable(MetricRegistry registry) {
271            if (!enabled) {
272                return;
273            }
274            LogFactory.getLog(MetricsServiceImpl.class).info(this);
275            // TODO: do not hard code the common datasource
276            // nameenable(registry)
277            String pool = "Catalina:type=DataSource,class=javax.sql.DataSource,name=\"jdbc/nuxeo\"";
278            String connector = String.format("Catalina:type=ThreadPool,name=\"http-bio-%s-%s\"",
279                    Framework.getProperty("nuxeo.bind.address", "0.0.0.0"),
280                    Framework.getProperty("nuxeo.bind.port", "8080"));
281            String requestProcessor = String.format("Catalina:type=GlobalRequestProcessor,name=\"http-bio-%s-%s\"",
282                    Framework.getProperty("nuxeo.bind.address", "0.0.0.0"),
283                    Framework.getProperty("nuxeo.bind.port", "8080"));
284            String manager = "Catalina:type=Manager,context=/nuxeo,host=localhost";
285            registerTomcatGauge(pool, "numActive", registry, "jdbc-numActive");
286            registerTomcatGauge(pool, "numIdle", registry, "jdbc-numIdle");
287            registerTomcatGauge(connector, "currentThreadCount", registry, "currentThreadCount");
288            registerTomcatGauge(connector, "currentThreadsBusy", registry, "currentThreadBusy");
289            registerTomcatGauge(requestProcessor, "errorCount", registry, "errorCount");
290            registerTomcatGauge(requestProcessor, "requestCount", registry, "requestCount");
291            registerTomcatGauge(requestProcessor, "processingTime", registry, "processingTime");
292            registerTomcatGauge(manager, "activeSessions", registry, "activeSessions");
293        }
294
295        public void disable(MetricRegistry registry) {
296            registry.remove("tomcat.jdbc-numActive");
297            registry.remove("tomcat.jdbc-numIdle");
298            registry.remove("tomcat.currentThreadCount");
299            registry.remove("tomcat.currentThreadBusy");
300            registry.remove("tomcat.errorCount");
301            registry.remove("tomcat.requestCount");
302            registry.remove("tomcat.processingTime");
303            registry.remove("tomcat.activeSessions");
304        }
305    }
306
307    @XObject(value = "jvmInstrumentation")
308    public static class JvmInstrumentationDescriptor {
309
310        public static final String ENABLED_PROPERTY = "metrics.jvm.enabled";
311
312        @XNode("@enabled")
313        protected boolean enabled = Boolean.parseBoolean(Framework.getProperty(ENABLED_PROPERTY, "true"));
314
315        public void enable(MetricRegistry registry) {
316            if (!enabled) {
317                return;
318            }
319            registry.register("jvm.memory", new MemoryUsageGaugeSet());
320            registry.register("jvm.garbage", new GarbageCollectorMetricSet());
321            registry.register("jvm.threads", new ThreadStatesGaugeSet());
322            registry.register("jvm.files", new FileDescriptorRatioGauge());
323            registry.register("jvm.buffers", new BufferPoolMetricSet(
324                    Framework.getLocalService(ServerLocator.class).lookupServer()));
325        }
326
327        public void disable(MetricRegistry registry) {
328            if (!enabled) {
329                return;
330            }
331            registry.removeMatching(new MetricFilter() {
332
333                @Override
334                public boolean matches(String name, Metric metric) {
335                    return name.startsWith("jvm.");
336                }
337            });
338
339        }
340    }
341
342    @XNode("graphiteReporter")
343    public GraphiteDescriptor graphiteReporter = new GraphiteDescriptor();
344
345    @XNode("csvReporter")
346    public CsvDescriptor csvReporter = new CsvDescriptor();
347
348    @XNode("log4jInstrumentation")
349    public Log4jInstrumentationDescriptor log4jInstrumentation = new Log4jInstrumentationDescriptor();
350
351    @XNode("tomcatInstrumentation")
352    public TomcatInstrumentationDescriptor tomcatInstrumentation = new TomcatInstrumentationDescriptor();
353
354    @XNode(value = "jvmInstrumentation")
355    public JvmInstrumentationDescriptor jvmInstrumentation = new JvmInstrumentationDescriptor();
356
357    protected JmxReporter jmxReporter;
358
359    public void enable(MetricRegistry registry) {
360        jmxReporter = JmxReporter.forRegistry(registry).build();
361        jmxReporter.start();
362        graphiteReporter.enable(registry);
363        csvReporter.enable(registry);
364        log4jInstrumentation.enable(registry);
365        tomcatInstrumentation.enable(registry);
366        jvmInstrumentation.enable(registry);
367    }
368
369    public void disable(MetricRegistry registry) {
370        try {
371            graphiteReporter.disable(registry);
372            csvReporter.disable(registry);
373            log4jInstrumentation.disable(registry);
374            tomcatInstrumentation.disable(registry);
375            jvmInstrumentation.disable(registry);
376            jmxReporter.stop();
377        } finally {
378            jmxReporter = null;
379        }
380    }
381
382}