001/*
002 * (C) Copyright 2013 Coda Hale <coda.hale@gmail.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 *
017 * This is a patched version of Coda Hale CsvReporter, the sanitizer method is overrided to enable
018 * metric name with a '/' in the name.
019 *
020 * @since 8.3
021 */
022
023package org.nuxeo.runtime.metrics;
024
025import com.codahale.metrics.Clock;
026import com.codahale.metrics.Counter;
027import com.codahale.metrics.Gauge;
028import com.codahale.metrics.Histogram;
029import com.codahale.metrics.Meter;
030import com.codahale.metrics.MetricFilter;
031import com.codahale.metrics.MetricRegistry;
032import com.codahale.metrics.ScheduledReporter;
033import com.codahale.metrics.Snapshot;
034import com.codahale.metrics.Timer;
035
036import org.apache.commons.logging.Log;
037import org.apache.commons.logging.LogFactory;
038
039import java.io.*;
040import java.nio.charset.Charset;
041import java.util.Locale;
042import java.util.Map;
043import java.util.SortedMap;
044import java.util.concurrent.TimeUnit;
045
046
047/**
048 * A reporter which creates a comma-separated values file of the measurements for each metric.
049 */
050public class CsvReporter extends ScheduledReporter {
051    /**
052     * Returns a new {@link Builder} for {@link CsvReporter}.
053     *
054     * @param registry the registry to report
055     * @return a {@link Builder} instance for a {@link CsvReporter}
056     */
057    public static Builder forRegistry(MetricRegistry registry) {
058        return new Builder(registry);
059    }
060
061
062    /**
063     * A builder for {@link CsvReporter} instances. Defaults to using the default locale, converting
064     * rates to events/second, converting durations to milliseconds, and not filtering metrics.
065     */
066    public static class Builder {
067        private final MetricRegistry registry;
068        private Locale locale;
069        private TimeUnit rateUnit;
070        private TimeUnit durationUnit;
071        private Clock clock;
072        private MetricFilter filter;
073
074        private Builder(MetricRegistry registry) {
075            this.registry = registry;
076            this.locale = Locale.getDefault();
077            this.rateUnit = TimeUnit.SECONDS;
078            this.durationUnit = TimeUnit.MILLISECONDS;
079            this.clock = Clock.defaultClock();
080            this.filter = MetricFilter.ALL;
081        }
082
083        /**
084         * Format numbers for the given {@link Locale}.
085         *
086         * @param locale a {@link Locale}
087         * @return {@code this}
088         */
089        public Builder formatFor(Locale locale) {
090            this.locale = locale;
091            return this;
092        }
093
094        /**
095         * Convert rates to the given time unit.
096         *
097         * @param rateUnit a unit of time
098         * @return {@code this}
099         */
100        public Builder convertRatesTo(TimeUnit rateUnit) {
101            this.rateUnit = rateUnit;
102            return this;
103        }
104
105        /**
106         * Convert durations to the given time unit.
107         *
108         * @param durationUnit a unit of time
109         * @return {@code this}
110         */
111        public Builder convertDurationsTo(TimeUnit durationUnit) {
112            this.durationUnit = durationUnit;
113            return this;
114        }
115
116        /**
117         * Use the given {@link Clock} instance for the time.
118         *
119         * @param clock a {@link Clock} instance
120         * @return {@code this}
121         */
122        public Builder withClock(Clock clock) {
123            this.clock = clock;
124            return this;
125        }
126
127        /**
128         * Only report metrics which match the given filter.
129         *
130         * @param filter a {@link MetricFilter}
131         * @return {@code this}
132         */
133        public Builder filter(MetricFilter filter) {
134            this.filter = filter;
135            return this;
136        }
137
138        /**
139         * Builds a {@link CsvReporter} with the given properties, writing {@code .csv} files to the
140         * given directory.
141         *
142         * @param directory the directory in which the {@code .csv} files will be created
143         * @return a {@link CsvReporter}
144         */
145        public CsvReporter build(File directory) {
146            return new CsvReporter(registry,
147                    directory,
148                    locale,
149                    rateUnit,
150                    durationUnit,
151                    clock,
152                    filter);
153        }
154    }
155
156    private static final Log LOGGER = LogFactory.getLog(CsvReporter.class);
157    private static final Charset UTF_8 = Charset.forName("UTF-8");
158
159    private final File directory;
160    private final Locale locale;
161    private final Clock clock;
162
163    private CsvReporter(MetricRegistry registry,
164                        File directory,
165                        Locale locale,
166                        TimeUnit rateUnit,
167                        TimeUnit durationUnit,
168                        Clock clock,
169                        MetricFilter filter) {
170        super(registry, "csv-reporter", filter, rateUnit, durationUnit);
171        this.directory = directory;
172        this.locale = locale;
173        this.clock = clock;
174    }
175
176
177    @Override
178    public void report(SortedMap<String, Gauge> gauges,
179                       SortedMap<String, Counter> counters,
180                       SortedMap<String, Histogram> histograms,
181                       SortedMap<String, Meter> meters,
182                       SortedMap<String, Timer> timers) {
183        final long timestamp = TimeUnit.MILLISECONDS.toSeconds(clock.getTime());
184
185        for (Map.Entry<String, Gauge> entry : gauges.entrySet()) {
186            reportGauge(timestamp, entry.getKey(), entry.getValue());
187        }
188
189        for (Map.Entry<String, Counter> entry : counters.entrySet()) {
190            reportCounter(timestamp, entry.getKey(), entry.getValue());
191        }
192
193        for (Map.Entry<String, Histogram> entry : histograms.entrySet()) {
194            reportHistogram(timestamp, entry.getKey(), entry.getValue());
195        }
196
197        for (Map.Entry<String, Meter> entry : meters.entrySet()) {
198            reportMeter(timestamp, entry.getKey(), entry.getValue());
199        }
200
201        for (Map.Entry<String, Timer> entry : timers.entrySet()) {
202            reportTimer(timestamp, entry.getKey(), entry.getValue());
203        }
204    }
205
206    private void reportTimer(long timestamp, String name, Timer timer) {
207        final Snapshot snapshot = timer.getSnapshot();
208
209        report(timestamp,
210                name,
211                "count,max,mean,min,stddev,p50,p75,p95,p98,p99,p999,mean_rate,m1_rate,m5_rate,m15_rate,rate_unit,duration_unit",
212                "%d,%f,%f,%f,%f,%f,%f,%f,%f,%f,%f,%f,%f,%f,%f,calls/%s,%s",
213                timer.getCount(),
214                convertDuration(snapshot.getMax()),
215                convertDuration(snapshot.getMean()),
216                convertDuration(snapshot.getMin()),
217                convertDuration(snapshot.getStdDev()),
218                convertDuration(snapshot.getMedian()),
219                convertDuration(snapshot.get75thPercentile()),
220                convertDuration(snapshot.get95thPercentile()),
221                convertDuration(snapshot.get98thPercentile()),
222                convertDuration(snapshot.get99thPercentile()),
223                convertDuration(snapshot.get999thPercentile()),
224                convertRate(timer.getMeanRate()),
225                convertRate(timer.getOneMinuteRate()),
226                convertRate(timer.getFiveMinuteRate()),
227                convertRate(timer.getFifteenMinuteRate()),
228                getRateUnit(),
229                getDurationUnit());
230    }
231
232    private void reportMeter(long timestamp, String name, Meter meter) {
233        report(timestamp,
234                name,
235                "count,mean_rate,m1_rate,m5_rate,m15_rate,rate_unit",
236                "%d,%f,%f,%f,%f,events/%s",
237                meter.getCount(),
238                convertRate(meter.getMeanRate()),
239                convertRate(meter.getOneMinuteRate()),
240                convertRate(meter.getFiveMinuteRate()),
241                convertRate(meter.getFifteenMinuteRate()),
242                getRateUnit());
243    }
244
245    private void reportHistogram(long timestamp, String name, Histogram histogram) {
246        final Snapshot snapshot = histogram.getSnapshot();
247
248        report(timestamp,
249                name,
250                "count,max,mean,min,stddev,p50,p75,p95,p98,p99,p999",
251                "%d,%d,%f,%d,%f,%f,%f,%f,%f,%f,%f",
252                histogram.getCount(),
253                snapshot.getMax(),
254                snapshot.getMean(),
255                snapshot.getMin(),
256                snapshot.getStdDev(),
257                snapshot.getMedian(),
258                snapshot.get75thPercentile(),
259                snapshot.get95thPercentile(),
260                snapshot.get98thPercentile(),
261                snapshot.get99thPercentile(),
262                snapshot.get999thPercentile());
263    }
264
265    private void reportCounter(long timestamp, String name, Counter counter) {
266        report(timestamp, name, "count", "%d", counter.getCount());
267    }
268
269    private void reportGauge(long timestamp, String name, Gauge gauge) {
270        report(timestamp, name, "value", "%s", gauge.getValue());
271    }
272
273    private void report(long timestamp, String name, String header, String line, Object... values) {
274        try {
275            final File file = new File(directory, sanitize(name) + ".csv");
276            final boolean fileAlreadyExists = file.exists();
277            if (fileAlreadyExists || file.createNewFile()) {
278                final PrintWriter out = new PrintWriter(new OutputStreamWriter(new FileOutputStream(file,true), UTF_8));
279                try {
280                    if (!fileAlreadyExists) {
281                        out.println("t," + header);
282                    }
283                    out.printf(locale, String.format(locale, "%d,%s%n", timestamp, line), values);
284                } finally {
285                    out.close();
286                }
287            }
288        } catch (IOException e) {
289            LOGGER.warn("Error writing to " + name, e);
290        }
291    }
292
293    protected String sanitize(String name) {
294        return name.replace('/', '.');
295    }
296}