Codebase as of c53e4aed26 as an initial commit for the shrunk repo

Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
Kai Kreuzer
2010-02-20 19:23:32 +01:00
committed by Kai Kreuzer
commit bbf1a7fd29
302 changed files with 29726 additions and 0 deletions

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.persistence.rrd4j-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
<feature name="openhab-persistence-rrd4j" description="RRD4j Persistence" version="${project.version}">
<feature>openhab-runtime-base</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.persistence.rrd4j/${project.version}</bundle>
<configfile finalname="${openhab.conf}/services/rrd4j.cfg" override="false">mvn:${project.groupId}/openhab-addons-external3/${project.version}/cfg/rrd4j</configfile>
</feature>
</features>

View File

@@ -0,0 +1,58 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.persistence.rrd4j.internal;
import java.text.DateFormat;
import java.time.ZonedDateTime;
import org.openhab.core.persistence.HistoricItem;
import org.openhab.core.types.State;
/**
* This is a Java bean used to return historic items from a rrd4j database.
*
* @author Kai Kreuzer - Initial contribution
*
*/
public class RRD4jItem implements HistoricItem {
private final String name;
private final State state;
private final ZonedDateTime timestamp;
public RRD4jItem(String name, State state, ZonedDateTime timestamp) {
this.name = name;
this.state = state;
this.timestamp = timestamp;
}
@Override
public String getName() {
return name;
}
@Override
public State getState() {
return state;
}
@Override
public ZonedDateTime getTimestamp() {
return timestamp;
}
@Override
public String toString() {
return DateFormat.getDateTimeInstance().format(timestamp) + ": " + name + " -> " + state.toString();
}
}

View File

@@ -0,0 +1,610 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.persistence.rrd4j.internal;
import java.io.File;
import java.io.IOException;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.OpenHAB;
import org.openhab.core.common.NamedThreadFactory;
import org.openhab.core.items.Item;
import org.openhab.core.items.ItemNotFoundException;
import org.openhab.core.items.ItemRegistry;
import org.openhab.core.library.items.ContactItem;
import org.openhab.core.library.items.DimmerItem;
import org.openhab.core.library.items.NumberItem;
import org.openhab.core.library.items.RollershutterItem;
import org.openhab.core.library.items.SwitchItem;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.OpenClosedType;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.persistence.FilterCriteria;
import org.openhab.core.persistence.FilterCriteria.Ordering;
import org.openhab.core.persistence.HistoricItem;
import org.openhab.core.persistence.PersistenceItemInfo;
import org.openhab.core.persistence.PersistenceService;
import org.openhab.core.persistence.QueryablePersistenceService;
import org.openhab.core.persistence.strategy.PersistenceCronStrategy;
import org.openhab.core.persistence.strategy.PersistenceStrategy;
import org.openhab.core.types.State;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.rrd4j.ConsolFun;
import org.rrd4j.DsType;
import org.rrd4j.core.FetchData;
import org.rrd4j.core.FetchRequest;
import org.rrd4j.core.RrdDb;
import org.rrd4j.core.RrdDef;
import org.rrd4j.core.Sample;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This is the implementation of the RRD4j {@link PersistenceService}. To learn
* more about RRD4j please visit their
* <a href="https://github.com/rrd4j/rrd4j">website</a>.
*
* @author Kai Kreuzer - Initial contribution
* @author Jan N. Klug - some improvements
* @author Karel Goderis - remove TimerThread dependency
*/
@NonNullByDefault
@Component(service = { PersistenceService.class,
QueryablePersistenceService.class }, configurationPid = "org.openhab.rrd4j")
public class RRD4jPersistenceService implements QueryablePersistenceService {
private static final String DEFAULT_OTHER = "default_other";
private static final String DEFAULT_NUMERIC = "default_numeric";
private static final String DEFAULT_QUANTIFIABLE = "default_quantifiable";
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(3,
new NamedThreadFactory("RRD4j"));
private final Map<String, @Nullable RrdDefConfig> rrdDefs = new ConcurrentHashMap<>();
private static final String DATASOURCE_STATE = "state";
public static final String DB_FOLDER = getUserPersistenceDataFolder() + File.separator + "rrd4j";
private final Logger logger = LoggerFactory.getLogger(RRD4jPersistenceService.class);
private final Map<String, @Nullable ScheduledFuture<?>> scheduledJobs = new HashMap<>();
protected final ItemRegistry itemRegistry;
@Activate
public RRD4jPersistenceService(final @Reference ItemRegistry itemRegistry) {
this.itemRegistry = itemRegistry;
}
@Override
public String getId() {
return "rrd4j";
}
@Override
public String getLabel(@Nullable Locale locale) {
return "RRD4j";
}
@Override
public synchronized void store(final Item item, @Nullable final String alias) {
final String name = alias == null ? item.getName() : alias;
RrdDb db = getDB(name);
if (db != null) {
ConsolFun function = getConsolidationFunction(db);
long now = System.currentTimeMillis() / 1000;
if (function != ConsolFun.AVERAGE) {
try {
// we store the last value again, so that the value change
// in the database is not interpolated, but
// happens right at this spot
if (now - 1 > db.getLastUpdateTime()) {
// only do it if there is not already a value
double lastValue = db.getLastDatasourceValue(DATASOURCE_STATE);
if (!Double.isNaN(lastValue)) {
Sample sample = db.createSample();
sample.setTime(now - 1);
sample.setValue(DATASOURCE_STATE, lastValue);
sample.update();
logger.debug("Stored '{}' with state '{}' in rrd4j database (again)", name,
mapToState(lastValue, item.getName()));
}
}
} catch (IOException e) {
logger.debug("Error storing last value (again): {}", e.getMessage());
}
}
try {
Sample sample = db.createSample();
sample.setTime(now);
DecimalType state = item.getStateAs(DecimalType.class);
if (state != null) {
double value = state.toBigDecimal().doubleValue();
if (db.getDatasource(DATASOURCE_STATE).getType() == DsType.COUNTER) { // counter values must be
// adjusted by stepsize
value = value * db.getRrdDef().getStep();
}
sample.setValue(DATASOURCE_STATE, value);
sample.update();
logger.debug("Stored '{}' with state '{}' in rrd4j database", name, state);
}
} catch (IllegalArgumentException e) {
if (e.getMessage().contains("at least one second step is required")) {
// we try to store the value one second later
ScheduledFuture<?> job = scheduledJobs.get(name);
if (job != null) {
job.cancel(true);
scheduledJobs.remove(name);
}
job = scheduler.schedule(() -> store(item, name), 1, TimeUnit.SECONDS);
scheduledJobs.put(name, job);
} else {
logger.warn("Could not persist '{}' to rrd4j database: {}", name, e.getMessage());
}
} catch (Exception e) {
logger.warn("Could not persist '{}' to rrd4j database: {}", name, e.getMessage());
}
try {
db.close();
} catch (IOException e) {
logger.debug("Error closing rrd4j database: {}", e.getMessage());
}
}
}
@Override
public void store(Item item) {
store(item, null);
}
@Override
public Iterable<HistoricItem> query(FilterCriteria filter) {
String itemName = filter.getItemName();
RrdDb db = getDB(itemName);
if (db != null) {
ConsolFun consolidationFunction = getConsolidationFunction(db);
long start = 0L;
long end = filter.getEndDate() == null ? System.currentTimeMillis() / 1000
: filter.getEndDate().toInstant().getEpochSecond();
try {
if (filter.getBeginDate() == null) {
// as rrd goes back for years and gets more and more
// inaccurate, we only support descending order
// and a single return value
// if there is no begin date is given - this case is
// required specifically for the historicState()
// query, which we want to support
if (filter.getOrdering() == Ordering.DESCENDING && filter.getPageSize() == 1
&& filter.getPageNumber() == 0) {
if (filter.getEndDate() == null) {
// we are asked only for the most recent value!
double lastValue = db.getLastDatasourceValue(DATASOURCE_STATE);
if (!Double.isNaN(lastValue)) {
HistoricItem rrd4jItem = new RRD4jItem(itemName, mapToState(lastValue, itemName),
ZonedDateTime.ofInstant(
Instant.ofEpochMilli(db.getLastArchiveUpdateTime() * 1000),
ZoneId.systemDefault()));
return Collections.singletonList(rrd4jItem);
} else {
return Collections.emptyList();
}
} else {
start = end;
}
} else {
throw new UnsupportedOperationException("rrd4j does not allow querys without a begin date, "
+ "unless order is descending and a single value is requested");
}
} else {
start = filter.getBeginDate().toInstant().getEpochSecond();
}
FetchRequest request = db.createFetchRequest(consolidationFunction, start, end, 1);
List<HistoricItem> items = new ArrayList<>();
FetchData result = request.fetchData();
long ts = result.getFirstTimestamp();
long step = result.getRowCount() > 1 ? result.getStep() : 0;
for (double value : result.getValues(DATASOURCE_STATE)) {
if (!Double.isNaN(value) && (((ts >= start) && (ts <= end)) || (start == end))) {
RRD4jItem rrd4jItem = new RRD4jItem(itemName, mapToState(value, itemName),
ZonedDateTime.ofInstant(Instant.ofEpochMilli(ts * 1000), ZoneId.systemDefault()));
items.add(rrd4jItem);
}
ts += step;
}
return items;
} catch (IOException e) {
logger.warn("Could not query rrd4j database for item '{}': {}", itemName, e.getMessage());
}
}
return Collections.emptyList();
}
@Override
public Set<PersistenceItemInfo> getItemInfo() {
return Collections.emptySet();
}
protected @Nullable synchronized RrdDb getDB(String alias) {
RrdDb db = null;
File file = new File(DB_FOLDER + File.separator + alias + ".rrd");
try {
if (file.exists()) {
// recreate the RrdDb instance from the file
db = new RrdDb(file.getAbsolutePath());
} else {
File folder = new File(DB_FOLDER);
if (!folder.exists()) {
folder.mkdirs();
}
// create a new database file
db = new RrdDb(getRrdDef(alias, file));
}
} catch (IOException e) {
logger.error("Could not create rrd4j database file '{}': {}", file.getAbsolutePath(), e.getMessage());
} catch (RejectedExecutionException e) {
// this happens if the system is shut down
logger.debug("Could not create rrd4j database file '{}': {}", file.getAbsolutePath(), e.getMessage());
}
return db;
}
private @Nullable RrdDefConfig getRrdDefConfig(String itemName) {
RrdDefConfig useRdc = null;
for (Map.Entry<String, @Nullable RrdDefConfig> e : rrdDefs.entrySet()) {
// try to find special config
RrdDefConfig rdc = e.getValue();
if (rdc != null && rdc.appliesTo(itemName)) {
useRdc = rdc;
break;
}
}
if (useRdc == null) { // not defined, use defaults
try {
Item item = itemRegistry.getItem(itemName);
if (item instanceof NumberItem) {
NumberItem numberItem = (NumberItem) item;
return numberItem.getDimension() != null ? rrdDefs.get(DEFAULT_QUANTIFIABLE)
: rrdDefs.get(DEFAULT_NUMERIC);
}
} catch (ItemNotFoundException e) {
logger.debug("Could not find item '{}' in registry", itemName);
}
}
return rrdDefs.get(DEFAULT_OTHER);
}
private RrdDef getRrdDef(String itemName, File file) {
RrdDef rrdDef = new RrdDef(file.getAbsolutePath());
RrdDefConfig useRdc = getRrdDefConfig(itemName);
if (useRdc != null) {
rrdDef.setStep(useRdc.step);
rrdDef.setStartTime(System.currentTimeMillis() / 1000 - 1);
rrdDef.addDatasource(DATASOURCE_STATE, useRdc.dsType, useRdc.heartbeat, useRdc.min, useRdc.max);
for (RrdArchiveDef rad : useRdc.archives) {
rrdDef.addArchive(rad.fcn, rad.xff, rad.steps, rad.rows);
}
}
return rrdDef;
}
public ConsolFun getConsolidationFunction(RrdDb db) {
try {
return db.getRrdDef().getArcDefs()[0].getConsolFun();
} catch (IOException e) {
return ConsolFun.MAX;
}
}
private State mapToState(double value, String itemName) {
try {
Item item = itemRegistry.getItem(itemName);
if (item instanceof SwitchItem && !(item instanceof DimmerItem)) {
return value == 0.0d ? OnOffType.OFF : OnOffType.ON;
} else if (item instanceof ContactItem) {
return value == 0.0d ? OpenClosedType.CLOSED : OpenClosedType.OPEN;
} else if (item instanceof DimmerItem || item instanceof RollershutterItem) {
// make sure Items that need PercentTypes instead of DecimalTypes do receive the right information
return new PercentType((int) Math.round(value * 100));
}
} catch (ItemNotFoundException e) {
logger.debug("Could not find item '{}' in registry", itemName);
}
// just return a DecimalType as a fallback
return new DecimalType(value);
}
private static String getUserPersistenceDataFolder() {
return OpenHAB.getUserDataFolder() + File.separator + "persistence";
}
/**
* @{inheritDoc
*/
public void activate(final Map<String, Object> config) {
// add default configurations
RrdDefConfig defaultNumeric = new RrdDefConfig(DEFAULT_NUMERIC);
// use 10 seconds as a step size for numeric values and allow a 10 minute silence between updates
defaultNumeric.setDef("GAUGE,600,U,U,10");
// define 5 different boxes:
// 1. granularity of 10s for the last hour
// 2. granularity of 1m for the last week
// 3. granularity of 15m for the last year
// 4. granularity of 1h for the last 5 years
// 5. granularity of 1d for the last 10 years
defaultNumeric.addArchives("LAST,0.5,1,360:LAST,0.5,6,10080:LAST,0.5,90,36500:LAST,0.5,8640,3650");
rrdDefs.put(DEFAULT_NUMERIC, defaultNumeric);
RrdDefConfig defaultQuantifiable = new RrdDefConfig(DEFAULT_QUANTIFIABLE);
// use 10 seconds as a step size for numeric values and allow a 10 minute silence between updates
defaultQuantifiable.setDef("GAUGE,600,U,U,10");
// define 5 different boxes:
// 1. granularity of 10s for the last hour
// 2. granularity of 1m for the last week
// 3. granularity of 15m for the last year
// 4. granularity of 1h for the last 5 years
// 5. granularity of 1d for the last 10 years
defaultQuantifiable
.addArchives("AVERAGE,0.5,1,360:AVERAGE,0.5,6,10080:LAST,0.5,90,36500:AVERAGE,0.5,8640,3650");
rrdDefs.put(DEFAULT_QUANTIFIABLE, defaultQuantifiable);
RrdDefConfig defaultOther = new RrdDefConfig(DEFAULT_OTHER);
// use 5 seconds as a step size for discrete values and allow a 1h silence between updates
defaultOther.setDef("GAUGE,3600,U,U,5");
// define 4 different boxes:
// 1. granularity of 5s for the last hour
// 2. granularity of 1m for the last week
// 3. granularity of 15m for the last year
// 4. granularity of 4h for the last 10 years
defaultOther.addArchives("LAST,0.5,1,1440:LAST,0.5,12,10080:LAST,0.5,180,35040:LAST,0.5,240,21900");
rrdDefs.put(DEFAULT_OTHER, defaultOther);
if (config.isEmpty()) {
logger.debug("using default configuration only");
return;
}
Iterator<String> keys = config.keySet().iterator();
while (keys.hasNext()) {
String key = keys.next();
if (key.equals("service.pid") || key.equals("component.name")) {
// ignore service.pid and name
continue;
}
String[] subkeys = key.split("\\.");
if (subkeys.length != 2) {
logger.debug("config '{}' should have the format 'name.configkey'", key);
continue;
}
Object v = config.get(key);
if (v instanceof String) {
String value = (String) v;
String name = subkeys[0].toLowerCase();
String property = subkeys[1].toLowerCase();
if (value.isBlank()) {
logger.trace("Config is empty: {}", property);
continue;
} else {
logger.trace("Processing config: {} = {}", property, value);
}
RrdDefConfig rrdDef = rrdDefs.get(name);
if (rrdDef == null) {
rrdDef = new RrdDefConfig(name);
rrdDefs.put(name, rrdDef);
}
try {
if (property.equals("def")) {
rrdDef.setDef(value);
} else if (property.equals("archives")) {
rrdDef.addArchives(value);
} else if (property.equals("items")) {
rrdDef.addItems(value);
} else {
logger.debug("Unknown property {} : {}", property, value);
}
} catch (IllegalArgumentException e) {
logger.warn("Ignoring illegal configuration: {}", e.getMessage());
}
}
}
for (RrdDefConfig rrdDef : rrdDefs.values()) {
if (rrdDef != null) {
if (rrdDef.isValid()) {
logger.debug("Created {}", rrdDef);
} else {
logger.info("Removing invalid definition {}", rrdDef);
rrdDefs.remove(rrdDef.name);
}
}
}
}
private class RrdArchiveDef {
public @Nullable ConsolFun fcn;
public double xff;
public int steps, rows;
@Override
public String toString() {
StringBuilder sb = new StringBuilder(" " + fcn);
sb.append(" xff = ").append(xff);
sb.append(" steps = ").append(steps);
sb.append(" rows = ").append(rows);
return sb.toString();
}
}
private class RrdDefConfig {
public String name;
public @Nullable DsType dsType;
public int heartbeat, step;
public double min, max;
public List<RrdArchiveDef> archives;
public List<String> itemNames;
private boolean isInitialized;
public RrdDefConfig(String name) {
this.name = name;
archives = new ArrayList<>();
itemNames = new ArrayList<>();
isInitialized = false;
}
public void setDef(String defString) {
String[] opts = defString.split(",");
if (opts.length != 5) { // check if correct number of parameters
logger.warn("invalid number of parameters {}: {}", name, defString);
return;
}
if (opts[0].equals("ABSOLUTE")) { // dsType
dsType = DsType.ABSOLUTE;
} else if (opts[0].equals("COUNTER")) {
dsType = DsType.COUNTER;
} else if (opts[0].equals("DERIVE")) {
dsType = DsType.DERIVE;
} else if (opts[0].equals("GAUGE")) {
dsType = DsType.GAUGE;
} else {
logger.warn("{}: dsType {} not supported", name, opts[0]);
}
heartbeat = Integer.parseInt(opts[1]);
if (opts[2].equals("U")) {
min = Double.NaN;
} else {
min = Double.parseDouble(opts[2]);
}
if (opts[3].equals("U")) {
max = Double.NaN;
} else {
max = Double.parseDouble(opts[3]);
}
step = Integer.parseInt(opts[4]);
isInitialized = true; // successfully initialized
return;
}
public void addArchives(String archivesString) {
String splitArchives[] = archivesString.split(":");
for (String archiveString : splitArchives) {
String[] opts = archiveString.split(",");
if (opts.length != 4) { // check if correct number of parameters
logger.warn("invalid number of parameters {}: {}", name, archiveString);
return;
}
RrdArchiveDef arc = new RrdArchiveDef();
if (opts[0].equals("AVERAGE")) {
arc.fcn = ConsolFun.AVERAGE;
} else if (opts[0].equals("MIN")) {
arc.fcn = ConsolFun.MIN;
} else if (opts[0].equals("MAX")) {
arc.fcn = ConsolFun.MAX;
} else if (opts[0].equals("LAST")) {
arc.fcn = ConsolFun.LAST;
} else if (opts[0].equals("FIRST")) {
arc.fcn = ConsolFun.FIRST;
} else if (opts[0].equals("TOTAL")) {
arc.fcn = ConsolFun.TOTAL;
} else {
logger.warn("{}: consolidation function {} not supported", name, opts[0]);
}
arc.xff = Double.parseDouble(opts[1]);
arc.steps = Integer.parseInt(opts[2]);
arc.rows = Integer.parseInt(opts[3]);
archives.add(arc);
}
}
public void addItems(String itemsString) {
String splitItems[] = itemsString.split(",");
for (String item : splitItems) {
itemNames.add(item);
}
}
public boolean appliesTo(String item) {
return itemNames.contains(item);
}
public boolean isValid() { // a valid configuration must be initialized
// and contain at least one function
return (isInitialized && (archives.size() > 0));
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder(name);
sb.append(" = ").append(dsType);
sb.append(" heartbeat = ").append(heartbeat);
sb.append(" min/max = ").append(min).append("/").append(max);
sb.append(" step = ").append(step);
sb.append(" ").append(archives.size()).append(" archives(s) = [");
for (RrdArchiveDef arc : archives) {
sb.append(arc.toString());
}
sb.append("] ");
sb.append(itemNames.size()).append(" items(s) = [");
for (String item : itemNames) {
sb.append(item).append(" ");
}
sb.append("]");
return sb.toString();
}
}
@Override
public List<PersistenceStrategy> getDefaultStrategies() {
return List.of(PersistenceStrategy.Globals.RESTORE, PersistenceStrategy.Globals.CHANGE,
new PersistenceCronStrategy("everyMinute", "0 * * * * ?"));
}
}

View File

@@ -0,0 +1,286 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.persistence.rrd4j.internal.charts;
import java.awt.Color;
import java.awt.Font;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.Date;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;
import javax.imageio.ImageIO;
import javax.servlet.Servlet;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import org.openhab.core.items.GroupItem;
import org.openhab.core.items.Item;
import org.openhab.core.items.ItemNotFoundException;
import org.openhab.core.library.items.NumberItem;
import org.openhab.core.ui.chart.ChartProvider;
import org.openhab.core.ui.items.ItemUIRegistry;
import org.openhab.persistence.rrd4j.internal.RRD4jPersistenceService;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Deactivate;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.http.HttpService;
import org.osgi.service.http.NamespaceException;
import org.rrd4j.ConsolFun;
import org.rrd4j.core.RrdDb;
import org.rrd4j.graph.RrdGraph;
import org.rrd4j.graph.RrdGraphDef;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This servlet generates time-series charts for a given set of items.
* It accepts the following HTTP parameters:
* <ul>
* <li>w: width in pixels of image to generate</li>
* <li>h: height in pixels of image to generate</li>
* <li>period: the time span for the x-axis. Value can be h,4h,8h,12h,D,3D,W,2W,M,2M,4M,Y</li>
* <li>items: A comma separated list of item names to display
* <li>groups: A comma separated list of group names, whose members should be displayed
* </ul>
*
* @author Kai Kreuzer - Initial contribution
* @author Chris Jackson - a few improvements
* @author Jan N. Klug - a few improvements
*
*/
@Component(service = ChartProvider.class)
public class RRD4jChartServlet implements Servlet, ChartProvider {
private final Logger logger = LoggerFactory.getLogger(RRD4jChartServlet.class);
/** the URI of this servlet */
public static final String SERVLET_NAME = "/rrdchart.png";
protected static final Color[] LINECOLORS = new Color[] { Color.RED, Color.GREEN, Color.BLUE, Color.MAGENTA,
Color.ORANGE, Color.CYAN, Color.PINK, Color.DARK_GRAY, Color.YELLOW };
protected static final Color[] AREACOLORS = new Color[] { new Color(255, 0, 0, 30), new Color(0, 255, 0, 30),
new Color(0, 0, 255, 30), new Color(255, 0, 255, 30), new Color(255, 128, 0, 30),
new Color(0, 255, 255, 30), new Color(255, 0, 128, 30), new Color(255, 128, 128, 30),
new Color(255, 255, 0, 30) };
protected static final Map<String, Long> PERIODS = new HashMap<>();
static {
PERIODS.put("h", -3600000L);
PERIODS.put("4h", -14400000L);
PERIODS.put("8h", -28800000L);
PERIODS.put("12h", -43200000L);
PERIODS.put("D", -86400000L);
PERIODS.put("3D", -259200000L);
PERIODS.put("W", -604800000L);
PERIODS.put("2W", -1209600000L);
PERIODS.put("M", -2592000000L);
PERIODS.put("2M", -5184000000L);
PERIODS.put("4M", -10368000000L);
PERIODS.put("Y", -31536000000L);
}
@Reference
protected HttpService httpService;
@Reference
protected ItemUIRegistry itemUIRegistry;
@Activate
protected void activate() {
try {
logger.debug("Starting up rrd chart servlet at {}", SERVLET_NAME);
httpService.registerServlet(SERVLET_NAME, this, new Hashtable<>(), httpService.createDefaultHttpContext());
} catch (NamespaceException e) {
logger.error("Error during servlet startup", e);
} catch (ServletException e) {
logger.error("Error during servlet startup", e);
}
}
@Deactivate
protected void deactivate() {
httpService.unregister(SERVLET_NAME);
}
@Override
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
logger.debug("RRD4J received incoming chart request: {}", req);
int width = 480;
try {
width = Integer.parseInt(req.getParameter("w"));
} catch (Exception e) {
}
int height = 240;
try {
height = Integer.parseInt(req.getParameter("h"));
} catch (Exception e) {
}
Long period = PERIODS.get(req.getParameter("period"));
if (period == null) {
// use a day as the default period
period = PERIODS.get("D");
}
// Create the start and stop time
Date timeEnd = new Date();
Date timeBegin = new Date(timeEnd.getTime() + period);
// Set the content type to that provided by the chart provider
res.setContentType("image/" + getChartType());
try {
BufferedImage chart = createChart(null, null, timeBegin, timeEnd, height, width, req.getParameter("items"),
req.getParameter("groups"), null, null);
ImageIO.write(chart, getChartType().toString(), res.getOutputStream());
} catch (ItemNotFoundException e) {
logger.debug("Item not found error while generating chart.");
} catch (IllegalArgumentException e) {
logger.debug("Illegal argument in chart", e);
}
}
/**
* Adds a line for the item to the graph definition.
* The color of the line is determined by the counter, it simply picks the according index from LINECOLORS (and
* rolls over if necessary).
*
* @param graphDef the graph definition to fill
* @param item the item to add a line for
* @param counter defines the number of the datasource and is used to determine the line color
*/
protected void addLine(RrdGraphDef graphDef, Item item, int counter) {
Color color = LINECOLORS[counter % LINECOLORS.length];
String label = itemUIRegistry.getLabel(item.getName());
String rrdName = RRD4jPersistenceService.DB_FOLDER + File.separator + item.getName() + ".rrd";
ConsolFun consolFun;
if (label != null && label.contains("[") && label.contains("]")) {
label = label.substring(0, label.indexOf('['));
}
try {
RrdDb db = new RrdDb(rrdName);
consolFun = db.getRrdDef().getArcDefs()[0].getConsolFun();
db.close();
} catch (IOException e) {
consolFun = ConsolFun.MAX;
}
if (item instanceof NumberItem) {
// we only draw a line
graphDef.datasource(Integer.toString(counter), rrdName, "state", consolFun); // RRD4jService.getConsolidationFunction(item));
graphDef.line(Integer.toString(counter), color, label, 2);
} else {
// we draw a line and fill the area beneath it with a transparent color
graphDef.datasource(Integer.toString(counter), rrdName, "state", consolFun); // RRD4jService.getConsolidationFunction(item));
Color areaColor = AREACOLORS[counter % LINECOLORS.length];
graphDef.area(Integer.toString(counter), areaColor);
graphDef.line(Integer.toString(counter), color, label, 2);
}
}
@Override
public void init(ServletConfig config) throws ServletException {
}
@Override
public ServletConfig getServletConfig() {
return null;
}
@Override
public String getServletInfo() {
return null;
}
@Override
public void destroy() {
}
// ----------------------------------------------------------
// The following methods implement the ChartServlet interface
@Override
public String getName() {
return "rrd4j";
}
@Override
public BufferedImage createChart(String service, String theme, Date startTime, Date endTime, int height, int width,
String items, String groups, Integer dpi, Boolean legend) throws ItemNotFoundException {
RrdGraphDef graphDef = new RrdGraphDef();
long period = (startTime.getTime() - endTime.getTime()) / 1000;
graphDef.setWidth(width);
graphDef.setHeight(height);
graphDef.setAntiAliasing(true);
graphDef.setImageFormat("PNG");
graphDef.setStartTime(period);
graphDef.setTextAntiAliasing(true);
graphDef.setLargeFont(new Font("SansSerif", Font.PLAIN, 15));
graphDef.setSmallFont(new Font("SansSerif", Font.PLAIN, 11));
int seriesCounter = 0;
// Loop through all the items
if (items != null) {
String[] itemNames = items.split(",");
for (String itemName : itemNames) {
Item item = itemUIRegistry.getItem(itemName);
addLine(graphDef, item, seriesCounter++);
}
}
// Loop through all the groups and add each item from each group
if (groups != null) {
String[] groupNames = groups.split(",");
for (String groupName : groupNames) {
Item item = itemUIRegistry.getItem(groupName);
if (item instanceof GroupItem) {
GroupItem groupItem = (GroupItem) item;
for (Item member : groupItem.getMembers()) {
addLine(graphDef, member, seriesCounter++);
}
} else {
throw new ItemNotFoundException("Item '" + item.getName() + "' defined in groups is not a group.");
}
}
}
// Write the chart as a PNG image
RrdGraph graph;
try {
graph = new RrdGraph(graphDef);
BufferedImage bi = new BufferedImage(graph.getRrdGraphInfo().getWidth(),
graph.getRrdGraphInfo().getHeight(), BufferedImage.TYPE_INT_RGB);
graph.render(bi.getGraphics());
return bi;
} catch (IOException e) {
logger.error("Error generating graph.", e);
}
return null;
}
@Override
public ImageType getChartType() {
return ImageType.png;
}
}