org.jenkinsci.plugins.elasticsearchquery.ElasticsearchQueryBuilder.java Source code

Java tutorial

Introduction

Here is the source code for org.jenkinsci.plugins.elasticsearchquery.ElasticsearchQueryBuilder.java

Source

/*
 * 
 * Copyright (c) 2016 Michael Epstein mikee805@aol.com
 * 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 org.jenkinsci.plugins.elasticsearchquery;

import static hudson.util.FormValidation.error;
import static hudson.util.FormValidation.ok;
import static java.lang.Long.parseLong;
import static java.lang.System.currentTimeMillis;
import static java.util.concurrent.TimeUnit.DAYS;
import static java.util.concurrent.TimeUnit.HOURS;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.MINUTES;
import static org.apache.commons.io.IOUtils.closeQuietly;
import static org.apache.commons.lang.StringUtils.deleteWhitespace;
import static org.apache.commons.lang.StringUtils.endsWith;
import static org.apache.commons.lang.StringUtils.isBlank;
import static org.apache.commons.lang.StringUtils.isEmpty;
import static org.apache.commons.lang.StringUtils.isNotBlank;
import static org.apache.commons.lang.StringUtils.replace;
import static org.apache.commons.lang.StringUtils.trim;
import static org.apache.commons.lang.math.NumberUtils.isNumber;
import static org.apache.commons.lang.math.NumberUtils.toInt;
import static org.apache.commons.lang.time.DateUtils.addDays;
import static org.apache.http.params.HttpConnectionParams.setSoTimeout;
import hudson.AbortException;
import hudson.Extension;
import hudson.FilePath;
import hudson.Launcher;
import hudson.model.TaskListener;
import hudson.model.AbstractProject;
import hudson.model.Run;
import hudson.tasks.BuildStepDescriptor;
import hudson.tasks.Builder;
import hudson.util.FormValidation;
import hudson.util.ListBoxModel;

import java.io.IOException;
import java.io.InputStream;
import java.util.Date;
import java.util.Map;
import java.util.concurrent.TimeUnit;

import javax.servlet.ServletException;

import jenkins.tasks.SimpleBuildStep;
import net.sf.json.JSONObject;

import org.apache.commons.codec.EncoderException;
import org.apache.commons.codec.net.URLCodec;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.time.FastDateFormat;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.DefaultHttpClient;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.QueryParameter;
import org.kohsuke.stapler.StaplerRequest;

import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;

/**
 * Simple builder that queries elasticsearch to use for downstream notifications
 *
 * @author Michael Epstein 
 */
public class ElasticsearchQueryBuilder extends Builder implements SimpleBuildStep {

    private final static FastDateFormat LOGSTASH_INDEX_FORMAT = FastDateFormat.getInstance("yyyy.MM.dd");
    private final static String LOGSTASH_INDEX_PREFIX = "logstash-";

    private final String query;
    private final String aboveOrBelow;
    private final Long threshold;
    private final Long since;
    private final String units;

    // Fields in config.jelly must match the parameter names in the "DataBoundConstructor"
    @DataBoundConstructor
    public ElasticsearchQueryBuilder(String query, String aboveOrBelow, Long threshold, Long since, String units) {
        this.query = trim(query);
        this.aboveOrBelow = trim(aboveOrBelow);
        this.threshold = threshold;
        this.since = since;
        this.units = units;
    }

    public String getQuery() {
        return query;
    }

    public String getAboveOrBelow() {
        return aboveOrBelow;
    }

    public Long getThreshold() {
        return threshold;
    }

    public Long getSince() {
        return since;
    }

    public String getUnits() {
        return units;
    }

    private String buildLogstashIndexes(final long past) {
        final StringBuilder stringBuilder = new StringBuilder();
        final String pastDateString = LOGSTASH_INDEX_FORMAT.format(new Date(past));
        Date currentDate = new Date();
        String currentDateString = LOGSTASH_INDEX_FORMAT.format(currentDate);
        stringBuilder.append(LOGSTASH_INDEX_PREFIX);
        stringBuilder.append(currentDateString);
        while (!currentDateString.equals(pastDateString)) {
            currentDate = addDays(currentDate, -1);
            currentDateString = LOGSTASH_INDEX_FORMAT.format(currentDate);
            stringBuilder.append(",");
            stringBuilder.append(LOGSTASH_INDEX_PREFIX);
            stringBuilder.append(currentDateString);
        }

        return stringBuilder.toString();
    }

    @Override
    public void perform(Run<?, ?> build, FilePath workspace, Launcher launcher, TaskListener listener)
            throws AbortException {
        //print our arguments
        listener.getLogger().println("Query: " + query);
        listener.getLogger().println("Fail when: " + aboveOrBelow);
        listener.getLogger().println("Threshold: " + threshold);
        listener.getLogger().println("Since: " + since);
        listener.getLogger().println("Time units: " + units);
        //get
        final String user = getDescriptor().getUser();
        //get
        final String password = getDescriptor().getPassword();
        //validate global user and password config
        if (isEmpty(user) != isEmpty(password)) {
            throw new AbortException(
                    "user and password must both be provided or empty! Please set value of user and password in Jenkins > Manage Jenkins > Configure System > Elasticsearch Query Builder");
        }

        final String creds = isEmpty(user) ? "" : user + ":" + password + "@";

        //get
        final String host = getDescriptor().getHost();
        //print
        listener.getLogger().println("host: " + host);
        //validate global host config
        if (isEmpty(host)) {
            throw new AbortException(
                    "Host cannot be empty! Please set value of host in Jenkins > Manage Jenkins > Configure System > ElasticSearch Query Builder");
        }

        //Calculate time in past for search and indexes
        Long past = currentTimeMillis() - MILLISECONDS.convert(since, TimeUnit.valueOf(units));

        //create the date clause to be added the query to restrict by relative time
        final String dateClause = " AND @timestamp:>=" + past;
        //use past to calculate specific indexes to search similar to kibana 3
        //ie if we are looking back to yesterday we dont need to search every index 
        //only today and yesterday
        final String queryIndexes = isNotBlank(getDescriptor().getIndexes()) ? getDescriptor().getIndexes()
                : buildLogstashIndexes(past);
        listener.getLogger().println("queryIndexes: " + queryIndexes);

        //we have all the parts now build the query URL
        String url = null;
        try {
            url = "http" + (getDescriptor().getUseSSL() ? "s" : "") + "://" + creds + getDescriptor().getHost()
                    + "/" + queryIndexes + "/_count?pretty=true&q=" + new URLCodec().encode(query + dateClause);
        } catch (EncoderException ee) {
            throw new RuntimeException(ee);
        }
        listener.getLogger().println("query url: " + url);

        HttpClient httpClient = new DefaultHttpClient();
        final HttpGet httpget = new HttpGet(url);
        final Integer queryRequestTimeout = getDescriptor().getQueryRequestTimeout();
        setSoTimeout(httpClient.getParams(),
                queryRequestTimeout == null || queryRequestTimeout < 1
                        ? getDescriptor().defaultQueryRequestTimeout()
                        : queryRequestTimeout);
        HttpResponse response = null;

        try {
            response = httpClient.execute(httpget);
            listener.getLogger().println("response: " + response);
        } catch (ClientProtocolException e) {
            throw new RuntimeException(e);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

        HttpEntity entity = response.getEntity();

        if (entity != null) {
            String content = null;
            Long count = null;
            InputStream instream = null;
            try {

                instream = entity.getContent();

                // do something useful with the response
                content = IOUtils.toString(instream);
                listener.getLogger().println("content: " + content);
                Map<String, Object> map = new Gson().fromJson(content, new TypeToken<Map<String, Object>>() {
                }.getType());
                listener.getLogger().println("count: " + map.get("count"));
                count = Math.round((Double) map.get("count"));

            } catch (Exception ex) {

                // In case of an unexpected exception you may want to abort
                // the HTTP request in order to shut down the underlying
                // connection and release it back to the connection manager.
                httpget.abort();
                throw new RuntimeException(ex);

            } finally {

                // Closing the input stream will trigger connection release
                closeQuietly(instream);

            }

            listener.getLogger().println("search url: " + replace(url, "_count", "_search"));

            final String abortMessage = threshold + ". Failing build!\n" + "URL: " + url + "\n"
                    + "response content: " + content;
            if (aboveOrBelow.equals("gte")) {
                if (count >= threshold) {
                    throw new AbortException("Count: " + count + " is >= " + abortMessage);
                }
            } else {
                if (count <= threshold) {
                    throw new AbortException("Count: " + count + " is <= " + abortMessage);
                }
            }
        }
    }

    // Overridden for better type safety.
    @Override
    public DescriptorImpl getDescriptor() {
        return (DescriptorImpl) super.getDescriptor();
    }

    /**
     * Descriptor for {@link ElasticsearchQueryBuilder}. Used as a singleton.
     * The class is marked as public so that it can be accessed from views.
     */
    @Extension // This indicates to Jenkins that this is an implementation of an extension point.
    public static final class DescriptorImpl extends BuildStepDescriptor<Builder> {
        /**
         * To persist global configuration information,
         * simply store it in a field and call save().
         *
         * <p>
         * If you don't want fields to be persisted, use <tt>transient</tt>.
         */
        private String host;
        private String indexes;
        private String user;
        private String password;
        private boolean useSSL;
        private Integer queryRequestTimeout;

        /**
         * In order to load the persisted global configuration, you have to 
         * call load() in the constructor.
         */
        public DescriptorImpl() {
            load();
        }

        /**
         * Performs on-the-fly validation of the form field 'query'.
         *
         * @param value
         *      This parameter receives the value that the user has typed.
         * @return
         *      Indicates the outcome of the validation. This is sent to the browser.
         *      <p>
         *      Note that returning {@link FormValidation#error(String)} does not
         *      prevent the form from being saved. It just means that a message
         *      will be displayed to the user. 
         */
        public FormValidation doCheckQuery(@QueryParameter String value) throws IOException, ServletException {
            if (isBlank(value))
                return FormValidation.error("Please set a query");
            return ok();
        }

        public FormValidation doCheckIndexes(@QueryParameter String value) throws IOException, ServletException {
            if (isNotBlank(value)) {
                if (!deleteWhitespace(value).equals(value)) {
                    return error("Indexes cannot contain whitespace");
                }
                if (endsWith(value, ",")) {
                    return error("Indexes cannot end with a comma");
                }
            }
            return ok();
        }

        public FormValidation doCheckThreshold(@QueryParameter String value) throws IOException, ServletException {
            value = trim(value);
            if (!isNumber(value) || parseLong(value) < 0)
                return FormValidation.error("Please set a threshold greater than or equal to 0");
            return FormValidation.ok();
        }

        public FormValidation doCheckSince(@QueryParameter Long value) throws IOException, ServletException {
            if (value == null || value < 1)
                return FormValidation.error("Please set a since value greater than 0");
            return FormValidation.ok();
        }

        public FormValidation doCheckQueryRequestTimeout(@QueryParameter Integer value)
                throws IOException, ServletException {
            if (value == null || value < 1)
                return FormValidation.error("Please set a value greater than 0");
            return FormValidation.ok();
        }

        public boolean isApplicable(@SuppressWarnings("rawtypes") Class<? extends AbstractProject> aClass) {
            // Indicates that this builder can be used with all kinds of project types 
            return true;
        }

        public ListBoxModel doFillAboveOrBelowItems() {
            ListBoxModel items = new ListBoxModel();
            items.add("gte");
            items.add("lte");
            return items;
        }

        public ListBoxModel doFillUnitsItems() {
            ListBoxModel items = new ListBoxModel();
            items.add(MINUTES.name());
            items.add(HOURS.name());
            items.add(DAYS.name());
            return items;
        }

        public Integer defaultQueryRequestTimeout() {
            return 120000;
        }

        /**
         * This human readable name is used in the configuration screen.
         */
        public String getDisplayName() {
            return "Elasticsearch Query";
        }

        @Override
        public boolean configure(StaplerRequest req, JSONObject formData) throws FormException {
            // To persist global configuration information,
            // set that to properties and call save().
            host = trim(formData.getString("host"));
            indexes = trim(formData.getString("indexes"));
            user = formData.getString("user");
            password = trim(formData.getString("password"));
            useSSL = formData.getBoolean("useSSL");
            queryRequestTimeout = toInt(trim(formData.getString("queryRequestTimeout")),
                    defaultQueryRequestTimeout());

            // ^Can also use req.bindJSON(this, formData);
            //  (easier when there are many fields; need set* methods for this, like setUseFrench)
            save();
            return super.configure(req, formData);
        }

        public String getHost() {
            return host;
        }

        public String getIndexes() {
            return indexes;
        }

        public String getUser() {
            return user;
        }

        public String getPassword() {
            return password;
        }

        public boolean getUseSSL() {
            return useSSL;
        }

        public Integer getQueryRequestTimeout() {
            return queryRequestTimeout;
        }

    }
}