com.googlecode.jmxtrans.model.output.RRDToolWriter.java Source code

Java tutorial

Introduction

Here is the source code for com.googlecode.jmxtrans.model.output.RRDToolWriter.java

Source

/**
 * The MIT License
 * Copyright  2010 JmxTrans team
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package com.googlecode.jmxtrans.model.output;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.base.MoreObjects;
import com.google.common.collect.ImmutableList;
import com.googlecode.jmxtrans.model.Query;
import com.googlecode.jmxtrans.model.Result;
import com.googlecode.jmxtrans.model.Server;
import com.googlecode.jmxtrans.model.ValidationException;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.WordUtils;
import org.jrobin.core.ArcDef;
import org.jrobin.core.DsDef;
import org.jrobin.core.RrdDef;
import org.jrobin.core.RrdDefTemplate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.BufferedReader;
import java.io.File;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.TreeMap;

import static com.google.common.base.Preconditions.checkState;
import static com.googlecode.jmxtrans.util.NumberUtils.isNumeric;

/**
 * This takes a JRobin template.xml file and then creates the database if it
 * doesn't already exist.
 * 
 * It will then write the contents of the Query (the Results) to the database.
 * 
 * This method exec's out to use the command line version of rrdtool. You need
 * to specify the path to the directory where the binary rrdtool lives.
 * 
 * @author jon
 */
public class RRDToolWriter extends BaseOutputWriter {
    private static final Logger log = LoggerFactory.getLogger(RRDToolWriter.class);

    public static final String GENERATE = "generate";
    private static final char[] INITIALS = { ' ', '.' };

    private final File outputFile;
    private final File templateFile;
    private final File binaryPath;
    private final boolean generate;

    @JsonCreator
    public RRDToolWriter(@JsonProperty("typeNames") ImmutableList<String> typeNames,
            @JsonProperty("booleanAsNumber") boolean booleanAsNumber, @JsonProperty("debug") Boolean debugEnabled,
            @JsonProperty("outputFile") String outputFile, @JsonProperty("templateFile") String templateFile,
            @JsonProperty("binaryPath") String binaryPath, @JsonProperty("generate") Boolean generate,
            @JsonProperty("settings") Map<String, Object> settings) {
        super(typeNames, booleanAsNumber, debugEnabled, settings);
        this.outputFile = new File(MoreObjects.firstNonNull(outputFile, (String) getSettings().get(OUTPUT_FILE)));
        this.templateFile = new File(
                MoreObjects.firstNonNull(templateFile, (String) getSettings().get(TEMPLATE_FILE)));
        this.binaryPath = new File(MoreObjects.firstNonNull(binaryPath, (String) getSettings().get(BINARY_PATH)));
        this.generate = MoreObjects.firstNonNull(generate,
                Settings.getBooleanSetting(getSettings(), "generate", Boolean.TRUE));

        checkState(this.outputFile.exists(), "Output file must exist");
        checkState(this.templateFile.exists(), "Template file must exist");
        checkState(this.binaryPath.exists(), "RRD Binary must exist");
    }

    @Override
    public void validateSetup(Server server, Query query) throws ValidationException {
    }

    /**
     * rrd datasources must be less than 21 characters in length, so work to
     * make it shorter. Not ideal at all, but works fairly well it seems.
     */
    public String getDataSourceName(String typeName, String attributeName, String entry) {
        String result;
        if (typeName != null) {
            result = typeName + attributeName + entry;
        } else {
            result = attributeName + entry;
        }

        if (attributeName.length() > 15) {
            String[] split = StringUtils.splitByCharacterTypeCamelCase(attributeName);
            String join = StringUtils.join(split, '.');
            attributeName = WordUtils.initials(join, INITIALS);
        }
        result = attributeName + DigestUtils.md5Hex(result);

        result = StringUtils.left(result, 19);

        return result;
    }

    @Override
    public void internalWrite(Server server, Query query, ImmutableList<Result> results) throws Exception {
        RrdDef def = getDatabaseTemplateSpec();

        List<String> dsNames = getDsNames(def.getDsDefs());

        Map<String, String> dataMap = new TreeMap<>();

        // go over all the results and look for datasource names that map to
        // keys from the result values
        for (Result res : results) {
            log.debug(res.toString());
            Map<String, Object> values = res.getValues();
            for (Entry<String, Object> entry : values.entrySet()) {
                String key = getDataSourceName(getConcatedTypeNameValues(res.getTypeName()), res.getAttributeName(),
                        entry.getKey());

                if (isNumeric(entry.getValue())) {
                    log.debug("Generated DataSource name:value: {} : {}", key, entry.getValue());
                    if (dsNames.contains(key)) {
                        dataMap.put(key, entry.getValue().toString());
                    }
                }
            }
        }

        doGenerate(results);

        if (!dataMap.keySet().isEmpty() && !dataMap.values().isEmpty()) {
            rrdToolUpdate(StringUtils.join(dataMap.keySet(), ':'), StringUtils.join(dataMap.values(), ':'));
        } else {
            log.error("Nothing was logged for query: " + query);
        }
    }

    private void doGenerate(List<Result> results) throws Exception {
        if (isDebugEnabled() && generate) {
            StringBuilder sb = new StringBuilder("\n");
            List<String> keys = new ArrayList<>();

            for (Result res : results) {
                for (Entry<String, Object> entry : res.getValues().entrySet()) {
                    if (isNumeric(entry.getValue())) {
                        String key = getDataSourceName(getConcatedTypeNameValues(res.getTypeName()),
                                res.getAttributeName(), entry.getKey());
                        if (keys.contains(key)) {
                            throw new Exception("Duplicate datasource name found: '" + key
                                    + "'. Please try to add more typeName keys to the writer to make the name more unique. "
                                    + res.toString());
                        }
                        keys.add(key);

                        sb.append("<datasource><!-- ").append(res.getTypeName()).append(":")
                                .append(res.getAttributeName()).append(":").append(entry.getKey())
                                .append(" --><name>").append(key)
                                .append("</name><type>GAUGE</type><heartbeat>400</heartbeat><min>U</min><max>U</max></datasource>\n");
                    }
                }
            }
            log.debug(sb.toString());
        }
    }

    /**
     * Executes the rrdtool update command.
     */
    protected void rrdToolUpdate(String template, String data) throws Exception {
        List<String> commands = new ArrayList<>();
        commands.add(binaryPath + "/rrdtool");
        commands.add("update");
        commands.add(outputFile.getCanonicalPath());
        commands.add("-t");
        commands.add(template);
        commands.add("N:" + data);

        ProcessBuilder pb = new ProcessBuilder(commands);
        Process process = pb.start();
        checkErrorStream(process);
    }

    /**
     * If the database file doesn't exist, it'll get created, otherwise, it'll
     * be returned in r/w mode.
     */
    protected RrdDef getDatabaseTemplateSpec() throws Exception {
        RrdDefTemplate t = new RrdDefTemplate(templateFile);
        t.setVariable("database", this.outputFile.getCanonicalPath());
        RrdDef def = t.getRrdDef();
        if (!this.outputFile.exists()) {
            FileUtils.forceMkdir(this.outputFile.getParentFile());
            rrdToolCreateDatabase(def);
        }
        return def;
    }

    /**
     * Calls out to the rrdtool binary with the 'create' command.
     */
    protected void rrdToolCreateDatabase(RrdDef def) throws Exception {
        List<String> commands = new ArrayList<>();
        commands.add(this.binaryPath + "/rrdtool");
        commands.add("create");
        commands.add(this.outputFile.getCanonicalPath());
        commands.add("-s");
        commands.add(String.valueOf(def.getStep()));

        for (DsDef dsdef : def.getDsDefs()) {
            commands.add(getDsDefStr(dsdef));
        }

        for (ArcDef adef : def.getArcDefs()) {
            commands.add(getRraStr(adef));
        }

        ProcessBuilder pb = new ProcessBuilder(commands);
        Process process = pb.start();
        try {
            checkErrorStream(process);
        } finally {
            IOUtils.closeQuietly(process.getInputStream());
            IOUtils.closeQuietly(process.getOutputStream());
            IOUtils.closeQuietly(process.getErrorStream());
        }
    }

    /**
     * Check to see if there was an error processing an rrdtool command
     */
    private void checkErrorStream(Process process) throws Exception {
        // rrdtool should use platform encoding (unless you did something
        // very strange with your installation of rrdtool). So let's be
        // explicit and use the presumed correct encoding to read errors.
        try (InputStream is = process.getErrorStream();
                InputStreamReader isr = new InputStreamReader(is, Charset.defaultCharset());
                BufferedReader br = new BufferedReader(isr)) {
            StringBuilder sb = new StringBuilder();
            String line;
            while ((line = br.readLine()) != null) {
                sb.append(line);
            }
            if (sb.length() > 0) {
                throw new RuntimeException(sb.toString());
            }
        }
    }

    /**
     * Generate a RRA line for rrdtool
     */
    private String getRraStr(ArcDef def) {
        return "RRA:" + def.getConsolFun() + ":" + def.getXff() + ":" + def.getSteps() + ":" + def.getRows();
    }

    /**
     * "rrdtool create temperature.rrd --step 300 \\\n" +
     * "         DS:temp:GAUGE:600:-273:5000 \\\n" +
     * "         RRA:AVERAGE:0.5:1:1200 \\\n" +
     * "         RRA:MIN:0.5:12:2400 \\\n" + "         RRA:MAX:0.5:12:2400 \\\n"
     * + "         RRA:AVERAGE:0.5:12:2400"
     */
    private String getDsDefStr(DsDef def) {
        return "DS:" + def.getDsName() + ":" + def.getDsType() + ":" + def.getHeartbeat() + ":"
                + formatDouble(def.getMinValue()) + ":" + formatDouble(def.getMaxValue());
    }

    /**
     * Get a list of DsNames used to create the datasource.
     */
    private List<String> getDsNames(DsDef[] defs) {
        List<String> names = new ArrayList<>();
        for (DsDef def : defs) {
            names.add(def.getDsName());
        }
        return names;
    }

    /**
     * If dbl is NaN, then return U
     */
    private String formatDouble(double dbl) {
        if (Double.isNaN(dbl)) {
            return "U";
        }
        return String.valueOf(dbl);
    }

    public String getOutputFile() {
        return outputFile.getPath();
    }

    public String getTemplateFile() {
        return templateFile.getPath();
    }

    public String getBinaryPath() {
        return binaryPath.getPath();
    }

    public static Builder builder() {
        return new Builder();
    }

    public static final class Builder {
        private final ImmutableList.Builder<String> typeNames = ImmutableList.builder();
        private boolean booleanAsNumber;
        private Boolean debugEnabled;
        private File outputFile;
        private File templateFile;
        private File binaryPath;
        private Boolean generate;

        private Builder() {
        }

        public Builder addTypeNames(List<String> typeNames) {
            this.typeNames.addAll(typeNames);
            return this;
        }

        public Builder addTypeName(String typeName) {
            typeNames.add(typeName);
            return this;
        }

        public Builder setBooleanAsNumber(boolean booleanAsNumber) {
            this.booleanAsNumber = booleanAsNumber;
            return this;
        }

        public Builder setDebugEnabled(boolean debugEnabled) {
            this.debugEnabled = debugEnabled;
            return this;
        }

        public Builder setOutputFile(File outputFile) {
            this.outputFile = outputFile;
            return this;
        }

        public Builder setTemplateFile(File templateFile) {
            this.templateFile = templateFile;
            return this;
        }

        public Builder setBinaryPath(File binaryPath) {
            this.binaryPath = binaryPath;
            return this;
        }

        public Builder setGenerate(Boolean generate) {
            this.generate = generate;
            return this;
        }

        public RRDToolWriter build() {
            return new RRDToolWriter(typeNames.build(), booleanAsNumber, debugEnabled, outputFile.getPath(),
                    templateFile.getPath(), binaryPath.getPath(), generate, null);
        }
    }

}