blob: 1c24c8a00fd58c569ca028f5b9f08661087a25ca [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.apache.sling.commons.metrics.rrd4j.impl;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.ScheduledReporter;
import org.apache.felix.inventory.Format;
import org.apache.felix.inventory.InventoryPrinter;
import org.apache.felix.inventory.ZipAttachmentProvider;
import org.osgi.framework.BundleContext;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.ConfigurationPolicy;
import org.osgi.service.component.annotations.Deactivate;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.component.annotations.ReferenceCardinality;
import org.osgi.service.component.annotations.ReferencePolicy;
import org.osgi.service.metatype.annotations.AttributeDefinition;
import org.osgi.service.metatype.annotations.Designate;
import org.osgi.service.metatype.annotations.ObjectClassDefinition;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.file.Files;
import java.util.Arrays;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import static org.apache.sling.commons.metrics.rrd4j.impl.RRD4JReporter.DEFAULT_PATH;
import static org.apache.sling.commons.metrics.rrd4j.impl.RRD4JReporter.DEFAULT_STEP;
@Component(
immediate = true,
configurationPolicy = ConfigurationPolicy.REQUIRE,
service = {InventoryPrinter.class, ZipAttachmentProvider.class},
property = {
InventoryPrinter.NAME + "=rrd4j-reporter",
InventoryPrinter.TITLE + "=Sling Metrics RRD4J reporter",
InventoryPrinter.FORMAT + "=TEXT"
}
)
@Designate(ocd = CodahaleMetricsReporter.Configuration.class)
public class CodahaleMetricsReporter implements InventoryPrinter, ZipAttachmentProvider {
private static final Logger LOG = LoggerFactory.getLogger(CodahaleMetricsReporter.class);
private Configuration configuration;
private File rrd;
private ScheduledReporter reporter;
private Map<String, CopyMetricRegistryListener> listeners = new ConcurrentHashMap<>();
@ObjectClassDefinition(name = "Apache Sling Metrics reporter writing to RRD4J",
description = "For syntax details on RRD data-source and round " +
"robin archive definitions see " +
"https://oss.oetiker.ch/rrdtool/doc/rrdcreate.en.html and " +
"https://github.com/rrd4j/rrd4j/wiki/Tutorial. Changing " +
"any attribute in this configuration will replace an " +
"existing RRD file with a empty one!")
public @interface Configuration {
@AttributeDefinition(
name = "Data sources",
description = "RRDTool data source definitions " +
"(e.g. 'DS:oak_SESSION_LOGIN_COUNTER:COUNTER:300:0:U'). " +
"Replace colon characters in the metric name with an " +
"underscore!"
)
String[] datasources() default {};
@AttributeDefinition(
name = "Step",
description = "The base interval in seconds with which data " +
"will be fed into the RRD"
)
int step() default DEFAULT_STEP;
@AttributeDefinition(
name = "Archives",
description = "RRDTool round robin archive definitions. The " +
"default configuration defines four archives based " +
"on a default step of five seconds: " +
"1) per minute averages for six hours, " +
"2) per five minute averages 48 hours, " +
"3) per hour averages for four weeks, " +
"4) per day averages for one year."
)
String[] archives() default {
"RRA:AVERAGE:0.5:12:360", "RRA:AVERAGE:0.5:60:576", "RRA:AVERAGE:0.5:720:336", "RRA:AVERAGE:0.5:17280:365"
};
@AttributeDefinition(
name = "Path",
description = "Path of the RRD file where metrics are stored. " +
"If the path is relative, it is resolved relative to " +
"the value of the framework property 'sling.home' when " +
"available, otherwise relative to the current working " +
"directory."
)
String path() default DEFAULT_PATH;
}
private MetricRegistry metricRegistry = new MetricRegistry();
@Activate
void activate(BundleContext context, Configuration config) throws Exception {
LOG.info("Starting RRD4J Metrics reporter");
configuration = config;
rrd = getSafePath(context, config);
reporter = RRD4JReporter.forRegistry(metricRegistry)
.withPath(rrd)
.withDatasources(config.datasources())
.withArchives(config.archives())
.withStep(config.step())
.build();
if (reporter != null) {
reporter.start(config.step(), TimeUnit.SECONDS);
LOG.info("Started RRD4J Metrics reporter: {}.", reporter);
} else {
LOG.warn("Illegal config will not start the RRD reporter. [path={}, datasources={}, archives={}, step={}].",
rrd.getPath(), config.datasources(), config.archives(), config.step());
}
}
private static File getSafePath(BundleContext context, Configuration config) {
String path = config.path();
if (path == null || path.isEmpty()) {
path = DEFAULT_PATH;
}
File rrd = new File(path);
if (!rrd.isAbsolute()) {
String home = context.getProperty("sling.home");
if (home != null) {
rrd = new File(home, rrd.getPath());
}
}
return rrd;
}
@Deactivate
void deactivate() {
LOG.info("Stopping RRD4J Metrics reporter");
if (reporter != null) {
reporter.close();
reporter = null;
}
configuration = null;
rrd = null;
LOG.info("Stopped RRD4J Metrics reporter");
}
@Reference(
service = MetricRegistry.class,
cardinality = ReferenceCardinality.MULTIPLE,
policy = ReferencePolicy.DYNAMIC)
synchronized void addMetricRegistry(MetricRegistry metricRegistry,
Map<String, Object> properties) {
String name = (String) properties.get("name");
if (name == null) {
name = metricRegistry.toString();
}
CopyMetricRegistryListener listener = new CopyMetricRegistryListener(this.metricRegistry, name);
listener.start(metricRegistry);
this.listeners.put(name, listener);
LOG.info("Bound Metrics Registry {} ",name);
}
synchronized void removeMetricRegistry(MetricRegistry metricRegistry,
Map<String, Object> properties) {
String name = (String) properties.get("name");
if (name == null) {
name = metricRegistry.toString();
}
CopyMetricRegistryListener metricRegistryListener = listeners.get(name);
if ( metricRegistryListener != null) {
metricRegistryListener.stop(metricRegistry);
this.listeners.remove(name);
}
LOG.info("Unbound Metrics Registry {} ",name);
}
//------------------------< InventoryPrinter >------------------------------
@Override
public void print(PrintWriter pw, Format format, boolean isZip) {
if (format == Format.TEXT) {
pw.println("Sling Metrics RRD4J reporter");
pw.println("Path: " + rrd.getAbsolutePath());
pw.println("Step: " + configuration.step());
pw.println("Datasources: " + Arrays.asList(configuration.datasources()));
pw.println("Archives: " + Arrays.asList(configuration.archives()));
}
}
//----------------------< ZipAttachmentProvider >---------------------------
@Override
public void addAttachments(ZipOutputStream zos, String namePrefix)
throws IOException {
if (rrd.exists()) {
appendFile(zos, rrd, namePrefix + configuration.path());
}
File props = new File(rrd.getParentFile(), rrd.getName() + ".properties");
if (props.exists()) {
appendFile(zos, props, namePrefix + configuration.path() + ".properties");
}
}
private void appendFile(ZipOutputStream zos, File file, String name)
throws IOException {
ZipEntry entry = new ZipEntry(name);
entry.setSize(file.length());
zos.putNextEntry(entry);
try {
Files.copy(file.toPath(), zos);
zos.flush();
} finally {
zos.closeEntry();
}
}
}