Jenkins plugin for publishing build data to InfluxDB

Problem: There is no official plugin from Jenkins for publishing build data to InfluxDB. Google helped a lot and pointed me to the InfluxDB plugin from jrajala: https://github.com/jrajala-eficode/jenkins-ci.influxdb-plugin. And actually, this is a quite nice plugin, but somehow it won’t work with latest release of InfluxDB 0.10.1 as there was meanwhile a new version of the javaclient for InfluxDB released. The jrajala plugin was already 10 months old when writing this blogpost, so I decided to say „thank you“ to them and fork the idea of it and write my own „simple influxdb plugin“ that should be a good base for any extension.

jenkins_simple-influxdb-plugin

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
	<modelVersion>4.0.0</modelVersion>

	<parent>
		<groupId>org.jenkins-ci.plugins</groupId>
		<artifactId>plugin</artifactId>
		<version>1.580.1</version>
	</parent>
	<groupId>eu.christophburmeister.jenkins-ci.plugins</groupId>
	<artifactId>simple-influxdb</artifactId>
	
	<version>1.0-SNAPSHOT</version>
	<packaging>hpi</packaging>

	<name>simple influx plugin</name>
	<description>Plugin for pushing time series data to influxdb, inspired by https://github.com/jrajala-eficode/jenkins-ci.influxdb-plugin</description>
	
	<repositories>
		<repository>
			<id>repo.jenkins-ci.org</id>
			<url>http://repo.jenkins-ci.org/public/</url>
		</repository>
	</repositories>
	<pluginRepositories>
		<pluginRepository>
			<id>repo.jenkins-ci.org</id>
			<url>http://repo.jenkins-ci.org/public/</url>
		</pluginRepository>
	</pluginRepositories>
	<dependencies>
		<dependency>
			<groupId>org.influxdb</groupId>
			<artifactId>influxdb-java</artifactId>
			<version>2.1</version>
		</dependency>
	</dependencies>
</project>

the models:

package eu.christophburmeister.jenkinsci.plugins.simpleinfluxdb.models;

public class BuildData {

	private String jobName;
	private long jobDurationSeconds;

	public BuildData() {
		//nop
	}

	public String getJobName() {
		return jobName;
	}

	public void setJobName(String jobName) {
		this.jobName = jobName;
	}

	public long getJobDurationSeconds() {
		return jobDurationSeconds;
	}

	public void setJobDurationSeconds(long jobDurationSeconds) {
		this.jobDurationSeconds = jobDurationSeconds;
	}

	@Override
	public String toString() {
		return "BuildData [jobName=" + this.jobName + ", jobDurationSeconds=" + this.jobDurationSeconds + "]";
	}
}
package eu.christophburmeister.jenkinsci.plugins.simpleinfluxdb.models;

public class Target {

	String description;
	String url;
	String username;
	String password;
	String database;
	
	public Target(){
		//nop
	}

	public String getDescription() {
		return description;
	}

	public void setDescription(String description) {
		this.description = description;
	}

	public String getUrl() {
		return url;
	}

	public void setUrl(String url) {
		this.url = url;
	}

	public String getUsername() {
		return username;
	}

	public void setUsername(String username) {
		this.username = username;
	}

	public String getPassword() {
		return password;
	}

	public void setPassword(String password) {
		this.password = password;
	}

	public String getDatabase() {
		return database;
	}

	public void setDatabase(String database) {
		this.database = database;
	}

	@Override
	public String toString() {
		return "Target [url=" + this.url + ", description=" + this.description + ", username=" + this.username
				+ ", password=*****, database=" + this.database + "]";
	}
}

The Descriptor class that is responsible for the configuration handling:

package eu.christophburmeister.jenkinsci.plugins.simpleinfluxdb;

import java.util.Iterator;

import org.kohsuke.stapler.StaplerRequest;

import eu.christophburmeister.jenkinsci.plugins.simpleinfluxdb.models.Target;
import hudson.model.AbstractProject;
import hudson.model.ModelObject;
import hudson.tasks.BuildStepDescriptor;
import hudson.tasks.Publisher;
import hudson.util.CopyOnWriteList;
import net.sf.json.JSONObject;

public final class DescriptorImpl extends BuildStepDescriptor<Publisher> implements ModelObject {

	public static final String DISPLAY_NAME = "Publish build data to InfluxDb target";
	private final CopyOnWriteList<Target> targets = new CopyOnWriteList<Target>();

	public DescriptorImpl() {
		super(SimpleInfluxdbPublisher.class);
		load();
	}
	
	public Target[] getTargets() {
		Iterator<Target> it = targets.iterator();
		int size = 0;
		while (it.hasNext()) {
			it.next();
			size++;
		}
		return targets.toArray(new Target[size]);
	}

	@Override
	public String getDisplayName() {
		return DISPLAY_NAME;
	}

	@Override
	public boolean isApplicable(Class<? extends AbstractProject> jobType) {
		return true;
	}

	@Override
	public Publisher newInstance(StaplerRequest req, JSONObject formData) {
		SimpleInfluxdbPublisher publisher = new SimpleInfluxdbPublisher();
		req.bindParameters(publisher, "publisherBinding.");
		return publisher;
	}

	@Override
	public boolean configure(StaplerRequest req, JSONObject formData) {
		targets.replaceBy(req.bindParametersToList(Target.class, "targetBinding."));
		save();
		return true;
	}
}

The heart of this plugin, the publisher where the data for is fetched from the jenkins context and pushed as measurement points to the influxdb:

package eu.christophburmeister.jenkinsci.plugins.simpleinfluxdb;

import java.io.IOException;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.influxdb.InfluxDB;
import org.influxdb.InfluxDB.ConsistencyLevel;
import org.influxdb.InfluxDBFactory;
import org.influxdb.dto.BatchPoints;
import org.influxdb.dto.Point;

import eu.christophburmeister.jenkinsci.plugins.simpleinfluxdb.models.BuildData;
import eu.christophburmeister.jenkinsci.plugins.simpleinfluxdb.models.Target;
import hudson.Extension;
import hudson.Launcher;
import hudson.model.AbstractBuild;
import hudson.model.BuildListener;
import hudson.tasks.BuildStepDescriptor;
import hudson.tasks.BuildStepMonitor;
import hudson.tasks.Notifier;
import hudson.tasks.Publisher;

public class SimpleInfluxdbPublisher extends Notifier {

	/** The logger. **/
	private static final Logger logger = Logger.getLogger(SimpleInfluxdbPublisher.class.getName());

	private static final String INFLUX_MEASUREMENT_PREFIX = "build";
	private static final String INFLUX_FIELDNAME_JOBDURATION = "jobduration";

	@Extension
	public static final DescriptorImpl DESCRIPTOR = new DescriptorImpl();

	private String selectedTarget;

	public SimpleInfluxdbPublisher() {
	}

	public SimpleInfluxdbPublisher(String target) {
		this.selectedTarget = target;
	}

	public String getSelectedTarget() {
		String ipTemp = selectedTarget;
		if (ipTemp == null) {
			Target[] targets = DESCRIPTOR.getTargets();
			if (targets.length > 0) {
				ipTemp = targets[0].getUrl();
			}
		}
		return ipTemp;
	}

	public void setSelectedTarget(String target) {
		this.selectedTarget = target;
	}

	public Target getTarget() {
		Target[] targets = DESCRIPTOR.getTargets();
		if (selectedTarget == null && targets.length > 0) {
			return targets[0];
		}
		for (Target target : targets) {
			if (target.getUrl().equals(selectedTarget)) {
				return target;
			}
		}
		return null;
	}

	@Override
	public boolean prebuild(AbstractBuild<?, ?> build, BuildListener listener) {
		return true;
	}

	@Override
	public boolean needsToRunAfterFinalized() {
		return true;
	}

	@Override
	public BuildStepMonitor getRequiredMonitorService() {
		return BuildStepMonitor.NONE;
	}

	@Override
	public BuildStepDescriptor<Publisher> getDescriptor() {
		return DESCRIPTOR;
	}

	@Override
	public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListener listener)
			throws InterruptedException, IOException {

		// get the target from the job's config
		Target target = getTarget();

		// extract the required buildData from the build context
		BuildData buildData = getBuildData(build);

		// prepare a meaningful logmessage
		String logMessage = "publishing data: " + buildData.toString() + " to " + target.toString();

		// write to jenkins logger
		logger.log(Level.INFO, logMessage);
		// write to jenkins console
		listener.getLogger().println(logMessage);

		// connect to InfluxDB
		InfluxDB influxDB = InfluxDBFactory.connect(target.getUrl(), target.getUsername(), target.getPassword());
		// finally write to InfluxDB
		influxDB.write(generateInfluxData(buildData, target));

		return true;

	}

	private BatchPoints generateInfluxData(BuildData buildData, Target target) {
		// create the list of batchpoints
		BatchPoints batchPoints = BatchPoints.database(target.getDatabase())
				.tag("async", "true")
				.retentionPolicy("default")
				.consistency(ConsistencyLevel.ALL)
				.build();

		// prepare the measurement point for the timeseries
		Point point = Point.measurement(INFLUX_MEASUREMENT_PREFIX + "_" + buildData.getJobName())
				.time(System.currentTimeMillis(), TimeUnit.MILLISECONDS)
				.field(INFLUX_FIELDNAME_JOBDURATION, buildData.getJobDurationSeconds())
				.build();

		batchPoints.point(point);
		return batchPoints;
	}

	private BuildData getBuildData(AbstractBuild<?, ?> build) {
		BuildData buildData = new BuildData();
		buildData.setJobName(build.getProject().getName());
		buildData.setJobDurationSeconds(build.getDuration());
		return buildData;
	}
}

The rest is some jelly-stuff for the configuration parts.
the job-configuration:

<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form">

  
  <f:entry title="influxdb target">
  
  	<select class="setting-input" name="publisherBinding.selectedTarget" >
  	
    	<j:forEach var="currentTarget" items="${descriptor.targets}">
    		<f:option 
    			selected="${currentTarget.url==instance.selectedTarget}" 
    			value="${currentTarget.url}" >
    				${currentTarget.description}
    		</f:option>
     	</j:forEach>
 
    </select>

  </f:entry>

</j:jelly>

the global configuration, where the fields are automatically mapped to the Target model;

<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form">
  
	<f:section title="influxdb target">
		<f:entry>
 
 			 <f:repeatable 
 			 	var="currentTarget" 
 			 	items="${descriptor.targets}" 
 			 	add="new influxdb target">
   
       			<table width="100%" >
			
			          <f:entry title="description">
			            <f:textbox name="targetBinding.description" value="${currentTarget.description}" />
			          </f:entry>
			
			          <f:entry title="url" field="url">
			            <f:textbox name="targetBinding.url"   value="${currentTarget.url}" />
			          </f:entry>
			
			          <f:entry title="username" field="username">
			            <f:textbox name="targetBinding.username"  value="${currentTarget.username}"/>
			          </f:entry>
			
			          <f:entry title="password" field="password">
			            <f:password name="targetBinding.password"  value="${currentTarget.password}"/>
			          </f:entry>
			
			          <f:entry title="database" field="database" >
			            <f:textbox name="targetBinding.database" value="${currentTarget.database}" />
			          </f:entry>
			
			          <f:entry title="delete target" >
			            <div align="right">
			              <f:repeatableDeleteButton value="delete target"/>
			            </div>
			          </f:entry>
			
			      </table>
			        		
      		</f:repeatable>
      		
		</f:entry>
  	</f:section>
</j:jelly>

and the description of the plugin:

<?jelly escape-by-default='true'?>
<div>
    This plugin allows sending build results to InfluxDB. It was inspired by https://github.com/jrajala-eficode/jenkins-ci.influxdb-plugin
</div>

I will put this also to Github and add some more comments to it, but for now, it’s straight forward and easy to understand (I hope 🙂 )

How this plugin is used, is described in one of my other blogposts: Visualize Jenkins build data through InfluxDB in Grafana