diff --git a/pom.xml b/pom.xml
index 518f0589..21b25781 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,12 +5,14 @@
UTF-8
true
+ 2.7.3
+ 7
org.jenkins-ci.plugins
plugin
- 1.599
+ 2.9
@@ -18,6 +20,11 @@
repo.jenkins-ci.org
https://repo.jenkins-ci.org/public/
+
+
+ jitpack.io
+ https://jitpack.io
+
@@ -88,7 +95,7 @@
org.mockito
mockito-core
- 2.1.0
+ 2.13.0
jar
test
@@ -113,7 +120,6 @@
org.jenkins-ci.plugins
mask-passwords
2.8
- true
@@ -127,6 +133,19 @@
structs
1.2
+
+
+ com.github.akostadinov
+ script-security-plugin
+ master-SNAPSHOT
+
+
+
+ javax.validation
+ validation-api
+ 1.0.0.GA
+ provided
+
diff --git a/src/main/java/jenkins/plugins/logstash/LogstashBuildWrapper.java b/src/main/java/jenkins/plugins/logstash/LogstashBuildWrapper.java
index 2e4a0100..f55772b1 100644
--- a/src/main/java/jenkins/plugins/logstash/LogstashBuildWrapper.java
+++ b/src/main/java/jenkins/plugins/logstash/LogstashBuildWrapper.java
@@ -32,13 +32,19 @@
import hudson.model.BuildableItemWithBuildWrappers;
import hudson.tasks.BuildWrapper;
import hudson.tasks.BuildWrapperDescriptor;
+import org.jenkinsci.plugins.scriptsecurity.sandbox.groovy.SecureGroovyScript;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
+import javax.annotation.CheckForNull;
+
+import groovy.lang.Binding;
+
import org.kohsuke.stapler.DataBoundConstructor;
+import org.kohsuke.stapler.DataBoundSetter;
import com.michelin.cio.hudson.plugins.maskpasswords.MaskPasswordsBuildWrapper;
import com.michelin.cio.hudson.plugins.maskpasswords.MaskPasswordsBuildWrapper.VarPasswordPair;
@@ -46,18 +52,26 @@
/**
* Build wrapper that decorates the build's logger to insert a
- * {@link LogstashNote} on each output line.
+ * Logstash note on each output line.
*
* @author K Jonathan Harker
*/
public class LogstashBuildWrapper extends BuildWrapper {
+ @CheckForNull
+ private SecureGroovyScript secureGroovyScript;
+
/**
* Create a new {@link LogstashBuildWrapper}.
*/
@DataBoundConstructor
public LogstashBuildWrapper() {}
+ @DataBoundSetter
+ public void setSecureGroovyScript(@CheckForNull SecureGroovyScript script) {
+ this.secureGroovyScript = script != null ? script.configuringWithNonKeyItem() : null;
+ }
+
/**
* {@inheritDoc}
*/
@@ -106,9 +120,19 @@ public DescriptorImpl getDescriptor() {
return (DescriptorImpl) super.getDescriptor();
}
+ @CheckForNull
+ public SecureGroovyScript getSecureGroovyScript() {
+ return secureGroovyScript;
+ }
+
// Method to encapsulate calls for unit-testing
LogstashWriter getLogStashWriter(AbstractBuild, ?> build, OutputStream errorStream) {
- return new LogstashWriter(build, errorStream, null);
+ LogstashScriptProcessor processor = null;
+ if (secureGroovyScript != null) {
+ processor = new LogstashScriptProcessor(secureGroovyScript, errorStream);
+ }
+
+ return new LogstashWriter(build, errorStream, null, processor);
}
/**
diff --git a/src/main/java/jenkins/plugins/logstash/LogstashInstallation.java b/src/main/java/jenkins/plugins/logstash/LogstashInstallation.java
index a806d95b..9f2161dd 100644
--- a/src/main/java/jenkins/plugins/logstash/LogstashInstallation.java
+++ b/src/main/java/jenkins/plugins/logstash/LogstashInstallation.java
@@ -43,6 +43,8 @@
import org.kohsuke.stapler.QueryParameter;
import org.kohsuke.stapler.StaplerRequest;
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+
/**
* POJO for storing global configurations shared between components.
*
@@ -65,6 +67,9 @@ public static Descriptor getLogstashDescriptor() {
public static final class Descriptor extends ToolDescriptor {
public IndexerType type;
public SyslogFormat syslogFormat;
+ @SuppressFBWarnings(
+ value="UUF_UNUSED_PUBLIC_OR_PROTECTED_FIELD",
+ justification="TODO: do we need this?")
public SyslogProtocol syslogProtocol;
public String host;
public Integer port = -1;
@@ -85,6 +90,9 @@ public boolean configure(StaplerRequest req, JSONObject formData) throws FormExc
}
@Override
+ @SuppressFBWarnings(
+ value="NP_PARAMETER_MUST_BE_NONNULL_BUT_MARKED_AS_NULLABLE",
+ justification="TODO: investigate")
public ToolInstallation newInstance(StaplerRequest req, JSONObject formData) throws FormException {
req.bindJSON(this, formData.getJSONObject("logstash"));
save();
diff --git a/src/main/java/jenkins/plugins/logstash/LogstashOutputStream.java b/src/main/java/jenkins/plugins/logstash/LogstashOutputStream.java
index 929abb47..0e9a561b 100644
--- a/src/main/java/jenkins/plugins/logstash/LogstashOutputStream.java
+++ b/src/main/java/jenkins/plugins/logstash/LogstashOutputStream.java
@@ -35,6 +35,8 @@
import com.michelin.cio.hudson.plugins.maskpasswords.MaskPasswordsBuildWrapper.VarPasswordPair;
import com.michelin.cio.hudson.plugins.maskpasswords.MaskPasswordsOutputStream;
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+
/**
* Output stream that writes each line to the provided delegate output stream
* and also sends it to an indexer for logstash to consume.
@@ -61,6 +63,9 @@ public MaskPasswordsOutputStream maskPasswords(List passwords)
}
@Override
+ @SuppressFBWarnings(
+ value="DM_DEFAULT_ENCODING",
+ justification="TODO: not sure how to fix this")
protected void eol(byte[] b, int len) throws IOException {
delegate.write(b, 0, len);
this.flush();
@@ -86,6 +91,7 @@ public void flush() throws IOException {
*/
@Override
public void close() throws IOException {
+ logstash.close();
delegate.close();
super.close();
}
diff --git a/src/main/java/jenkins/plugins/logstash/LogstashPayloadProcessor.java b/src/main/java/jenkins/plugins/logstash/LogstashPayloadProcessor.java
new file mode 100644
index 00000000..e39f60ea
--- /dev/null
+++ b/src/main/java/jenkins/plugins/logstash/LogstashPayloadProcessor.java
@@ -0,0 +1,50 @@
+/*
+ * The MIT License
+ *
+ * Copyright 2017 Red Hat inc, and individual contributors
+ *
+ * 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 jenkins.plugins.logstash;
+
+import net.sf.json.JSONObject;
+
+/**
+ * Interface describing processors of persisted payload.
+ *
+ * @author Aleksandar Kostadinov
+ * @since 1.4.0
+ */
+public interface LogstashPayloadProcessor {
+ /**
+ * Modifies a JSON payload compatible with the Logstash schema.
+ *
+ * @param payload the JSON payload that has been constructed so far.
+ * @return The formatted JSON object, can be null to ignore this payload.
+ */
+ JSONObject process(JSONObject payload) throws Exception;
+
+ /**
+ * Finalizes any operations, for example returns cashed lines at end of build.
+ *
+ * @return A formatted JSON object, can be null when it has nothing.
+ */
+ JSONObject finish() throws Exception;
+}
diff --git a/src/main/java/jenkins/plugins/logstash/LogstashScriptProcessor.java b/src/main/java/jenkins/plugins/logstash/LogstashScriptProcessor.java
new file mode 100644
index 00000000..a0cf545a
--- /dev/null
+++ b/src/main/java/jenkins/plugins/logstash/LogstashScriptProcessor.java
@@ -0,0 +1,123 @@
+/*
+ * The MIT License
+ *
+ * Copyright 2017 Red Hat inc. and individual contributors
+ *
+ * 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 jenkins.plugins.logstash;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.PrintWriter;
+import java.util.LinkedHashMap;
+
+import javax.annotation.Nonnull;
+
+import groovy.lang.Binding;
+
+import net.sf.json.JSONObject;
+
+import jenkins.model.Jenkins;
+import org.jenkinsci.plugins.scriptsecurity.sandbox.groovy.SecureGroovyScript;
+import org.jenkinsci.plugins.scriptsecurity.sandbox.whitelists.Whitelisted;
+
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+
+/**
+ * This class is handling custom groovy script processing of JSON payload.
+ * Each call to process executes the script provided in job configuration.
+ * Script is executed under the same binding each time so that it has ability
+ * to persist data during build execution if desired by script author.
+ * When build is finished, script will receive null as the payload and can
+ * return any cached but non-sent data back for persisting.
+ * The return value of script is the payload to be persisted unless null.
+ *
+ * @author Aleksandar Kostadinov
+ * @since 1.4.0
+ */
+public class LogstashScriptProcessor implements LogstashPayloadProcessor{
+ @Nonnull
+ private final SecureGroovyScript script;
+
+ @Nonnull
+ private final OutputStream consoleOut;
+
+ /** Groovy binding for script execution */
+ @Nonnull
+ private final Binding binding;
+
+ /** Classloader for script execution */
+ @Nonnull
+ private final ClassLoader classLoader;
+
+ public LogstashScriptProcessor(SecureGroovyScript script, OutputStream consoleOut) {
+ this.script = script;
+ this.consoleOut = consoleOut;
+
+ // TODO: should we put variables in the binding like manager, job, etc.?
+ binding = new Binding();
+ binding.setVariable("console", new BuildConsoleWrapper());
+
+ // not sure what the diff is compared to getClass().getClassLoader();
+ final Jenkins jenkins = Jenkins.getInstance();
+ classLoader = jenkins.getPluginManager().uberClassLoader;
+ }
+
+ /**
+ * Helper method to allow logging to build console.
+ */
+ @SuppressFBWarnings(
+ value="DM_DEFAULT_ENCODING",
+ justification="TODO: not sure how to fix this")
+ private void buildLogPrintln(Object o) throws IOException {
+ consoleOut.write(o.toString().getBytes());
+ consoleOut.write("\n".getBytes());
+ consoleOut.flush();
+ }
+
+ /*
+ * good examples in:
+ * https://github.com/jenkinsci/envinject-plugin/blob/master/src/main/java/org/jenkinsci/plugins/envinject/service/EnvInjectEnvVars.java
+ * https://github.com/jenkinsci/groovy-postbuild-plugin/pull/11/files
+ */
+ @Override
+ public JSONObject process(JSONObject payload) throws Exception {
+ binding.setVariable("payload", payload);
+ script.evaluate(classLoader, binding);
+ return (JSONObject) binding.getVariable("payload");
+ }
+
+ @Override
+ public JSONObject finish() throws Exception {
+ buildLogPrintln("Tearing down Script Log Processor..");
+ return process(null);
+ }
+
+ /**
+ * Helper to allow access from sandboxed script to output messages to console.
+ */
+ private class BuildConsoleWrapper {
+ @Whitelisted
+ public void println(Object o) throws IOException {
+ buildLogPrintln(o);
+ }
+ }
+}
diff --git a/src/main/java/jenkins/plugins/logstash/LogstashWriter.java b/src/main/java/jenkins/plugins/logstash/LogstashWriter.java
index 68134d30..05d8d609 100644
--- a/src/main/java/jenkins/plugins/logstash/LogstashWriter.java
+++ b/src/main/java/jenkins/plugins/logstash/LogstashWriter.java
@@ -36,12 +36,15 @@
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.exception.ExceptionUtils;
+import javax.annotation.CheckForNull;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+
/**
* A writer that wraps all Logstash DAOs. Handles error reporting and per build connection state.
* Each call to write (one line or multiple lines) sends a Logstash payload to the DAO.
@@ -49,6 +52,7 @@
*
* @author Rusty Gerard
* @author Liam Newman
+ * @author Aleksandar Kostadinov
* @since 1.0.5
*/
public class LogstashWriter {
@@ -61,10 +65,18 @@ public class LogstashWriter {
final LogstashIndexerDao dao;
private boolean connectionBroken;
+ @CheckForNull
+ private final LogstashPayloadProcessor payloadProcessor;
+
public LogstashWriter(Run, ?> run, OutputStream error, TaskListener listener) {
+ this(run, error, listener, null);
+ }
+
+ public LogstashWriter(Run, ?> run, OutputStream error, TaskListener listener, LogstashPayloadProcessor payloadProcessor) {
this.errorStream = error != null ? error : System.err;
this.build = run;
this.listener = listener;
+ this.payloadProcessor = payloadProcessor;
this.dao = this.getDaoOrNull();
if (this.dao == null) {
this.jenkinsUrl = "";
@@ -149,7 +161,35 @@ String getJenkinsUrl() {
* Write a list of lines to the indexer as one Logstash payload.
*/
private void write(List lines) {
- JSONObject payload = dao.buildPayload(buildData, jenkinsUrl, lines);
+ write(dao.buildPayload(buildData, jenkinsUrl, lines));
+ }
+
+ /**
+ * Write JSONObject payload to the Logstash indexer.
+ * @since 1.0.5
+ */
+ private void write(JSONObject payload) {
+ if (payloadProcessor != null) {
+ JSONObject processedPayload = payload;
+ try {
+ processedPayload = payloadProcessor.process(payload);
+ } catch (Exception e) {
+ String msg = ExceptionUtils.getMessage(e) + "\n" +
+ "[logstash-plugin]: Error in payload processing.\n";
+
+ logErrorMessage(msg);
+ }
+ if (processedPayload != null) { writeRaw(processedPayload); }
+ } else {
+ writeRaw(payload);
+ }
+ }
+
+ /**
+ * Write JSONObject payload to the Logstash indexer.
+ * @since 1.0.5
+ */
+ private void writeRaw(JSONObject payload) {
try {
dao.push(payload.toString());
} catch (IOException e) {
@@ -181,6 +221,9 @@ private LogstashIndexerDao getDaoOrNull() {
/**
* Write error message to errorStream and set connectionBroken to true.
*/
+ @SuppressFBWarnings(
+ value="DM_DEFAULT_ENCODING",
+ justification="TODO: not sure how to fix this")
private void logErrorMessage(String msg) {
try {
connectionBroken = true;
@@ -191,4 +234,23 @@ private void logErrorMessage(String msg) {
ex.printStackTrace();
}
}
+
+ /**
+ * Signal payload processor that there will be no more lines
+ */
+ public void close() {
+ if (payloadProcessor != null) {
+ JSONObject payload = null;
+ try {
+ // calling finish() is mandatory to avoid memory leaks
+ payload = payloadProcessor.finish();
+ } catch (Exception e) {
+ String msg = ExceptionUtils.getMessage(e) + "\n" +
+ "[logstash-plugin]: Error with payload processor on finish.\n";
+
+ logErrorMessage(msg);
+ }
+ if (payload != null) writeRaw(payload);
+ }
+ }
}
diff --git a/src/main/java/jenkins/plugins/logstash/persistence/AbstractLogstashIndexerDao.java b/src/main/java/jenkins/plugins/logstash/persistence/AbstractLogstashIndexerDao.java
index 25bd93c6..1c1ddaf8 100644
--- a/src/main/java/jenkins/plugins/logstash/persistence/AbstractLogstashIndexerDao.java
+++ b/src/main/java/jenkins/plugins/logstash/persistence/AbstractLogstashIndexerDao.java
@@ -24,13 +24,15 @@
package jenkins.plugins.logstash.persistence;
-import java.util.Calendar;
+import java.util.Date;
import java.util.List;
import org.apache.commons.lang.StringUtils;
import net.sf.json.JSONObject;
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+
/**
* Abstract data access object for Logstash indexers.
*
@@ -64,7 +66,7 @@ public JSONObject buildPayload(BuildData buildData, String jenkinsUrl, List testResultAction = null;
if (action instanceof AbstractTestResultAction) {
@@ -113,8 +119,8 @@ public TestData(Action action) {
}
}
+ @CheckForNull protected String result;
protected String id;
- protected String result;
protected String projectName;
protected String fullProjectName;
protected String displayName;
@@ -207,8 +213,14 @@ public BuildData(Run, ?> build, Date currentTime, TaskListener listener) {
}
}
+ // ISO 8601 date format
+ public static String formatDateIso(Date date) {
+ return DATE_FORMATTER.format(date);
+ }
+
private void initData(Run, ?> build, Date currentTime) {
- result = build.getResult() == null ? null : build.getResult().toString();
+ Result buildResult = build.getResult();
+ result = buildResult == null ? null : buildResult.toString();
id = build.getId();
projectName = build.getParent().getName();
fullProjectName = build.getParent().getFullName();
@@ -224,7 +236,7 @@ private void initData(Run, ?> build, Date currentTime) {
}
buildDuration = currentTime.getTime() - build.getStartTimeInMillis();
- timestamp = DATE_FORMATTER.format(build.getTimestamp().getTime());
+ timestamp = formatDateIso(build.getTime());
}
@Override
@@ -339,7 +351,7 @@ public String getTimestamp() {
}
public void setTimestamp(Calendar timestamp) {
- this.timestamp = DATE_FORMATTER.format(timestamp.getTime());
+ this.timestamp = formatDateIso(timestamp.getTime());
}
public String getRootProjectName() {
diff --git a/src/main/java/jenkins/plugins/logstash/persistence/ElasticSearchDao.java b/src/main/java/jenkins/plugins/logstash/persistence/ElasticSearchDao.java
index 91c29df2..94297e9f 100644
--- a/src/main/java/jenkins/plugins/logstash/persistence/ElasticSearchDao.java
+++ b/src/main/java/jenkins/plugins/logstash/persistence/ElasticSearchDao.java
@@ -45,6 +45,8 @@
import com.google.common.collect.Range;
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+
/**
* Elastic Search Data Access Object.
*
@@ -63,6 +65,9 @@ public ElasticSearchDao(String host, int port, String key, String username, Stri
}
// Factored for unit testing
+ @SuppressFBWarnings(
+ value="DM_DEFAULT_ENCODING",
+ justification="TODO: not sure how to fix this")
ElasticSearchDao(HttpClientBuilder factory, String host, int port, String key, String username, String password) {
super(host, port, key, username, password);
@@ -127,6 +132,9 @@ public void push(String data) throws IOException {
}
}
+ @SuppressFBWarnings(
+ value="DM_DEFAULT_ENCODING",
+ justification="TODO: not sure how to fix this")
private String getErrorMessage(CloseableHttpResponse response) {
ByteArrayOutputStream byteStream = null;
PrintStream stream = null;
diff --git a/src/main/java/jenkins/plugins/logstash/persistence/IndexerDaoFactory.java b/src/main/java/jenkins/plugins/logstash/persistence/IndexerDaoFactory.java
index ffd79345..76f5b0d7 100644
--- a/src/main/java/jenkins/plugins/logstash/persistence/IndexerDaoFactory.java
+++ b/src/main/java/jenkins/plugins/logstash/persistence/IndexerDaoFactory.java
@@ -35,6 +35,8 @@
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.exception.ExceptionUtils;
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+
/**
* Factory for AbstractLogstashIndexerDao objects.
*
@@ -74,6 +76,9 @@ public final class IndexerDaoFactory {
* @return The instance of the appropriate indexer DAO, never null
* @throws InstantiationException
*/
+ @SuppressFBWarnings(
+ value="BX_UNBOXING_IMMEDIATELY_REBOXED",
+ justification="TODO: not sure how to fix this")
public static synchronized LogstashIndexerDao getInstance(IndexerType type, String host, Integer port, String key, String username, String password) throws InstantiationException {
if (!INDEXER_MAP.containsKey(type)) {
throw new InstantiationException("[logstash-plugin]: Unknown IndexerType '" + type + "'. Did you forget to configure the plugin?");
diff --git a/src/main/java/jenkins/plugins/logstash/persistence/RabbitMqDao.java b/src/main/java/jenkins/plugins/logstash/persistence/RabbitMqDao.java
index d49e2ccb..0817af33 100644
--- a/src/main/java/jenkins/plugins/logstash/persistence/RabbitMqDao.java
+++ b/src/main/java/jenkins/plugins/logstash/persistence/RabbitMqDao.java
@@ -32,6 +32,8 @@
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+
/**
* RabbitMQ Data Access Object.
*
@@ -68,6 +70,9 @@ public RabbitMqDao(String host, int port, String key, String username, String pa
}
@Override
+ @SuppressFBWarnings(
+ value="DM_DEFAULT_ENCODING",
+ justification="TODO: not sure how to fix this")
public void push(String data) throws IOException {
Connection connection = null;
Channel channel = null;
diff --git a/src/main/resources/jenkins/plugins/logstash/LogstashBuildWrapper/config.jelly b/src/main/resources/jenkins/plugins/logstash/LogstashBuildWrapper/config.jelly
new file mode 100644
index 00000000..4c7cbf08
--- /dev/null
+++ b/src/main/resources/jenkins/plugins/logstash/LogstashBuildWrapper/config.jelly
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/src/main/resources/jenkins/plugins/logstash/LogstashBuildWrapper/help-script.html b/src/main/resources/jenkins/plugins/logstash/LogstashBuildWrapper/help-script.html
new file mode 100644
index 00000000..27404758
--- /dev/null
+++ b/src/main/resources/jenkins/plugins/logstash/LogstashBuildWrapper/help-script.html
@@ -0,0 +1,37 @@
+
+ With this script you can filter, modify and/or group messages that are sent to the
+ configured logstash backend. The following variables are available during execution:
+
+ - payload - JSONObject contaning the payload to be persisted.
+ - console - a class to output messages to build log without persisting them. It also works in a sandbox:
console.println("my logged message").
+
+
+
+ The script will be executed once per each line of build console output with
+ the variable payload set to the complete JSON payload to be
+ sent including message, timestamp, build url, etc.
+ The line text in particular will be present under the "message"
+ key as an array with a single string element.
+
+
+ Once script completes execution, the payload variable will be read out
+ of current Binding and passed down to the Logstash backend to be persisted. If
+ payload is set to null by the script, then nothing will be
+ persisted.
+
+
+ The script within a build will be executed always using the same Binding.
+ This means that variables can be saved between script invocations.
+
+
+ At the end of the build a payload == null will be submitted. You can use this to
+ output any messages that you have cached for grouping or other purposes.
+
+
+ Example script to filter out some console messages by pattern:
+
+ if (payload && payload["message"][0] =~ /my needless pattern/) {
+ payload = null
+ }
+
+
diff --git a/src/main/resources/jenkins/plugins/logstash/LogstashBuildWrapper/help.html b/src/main/resources/jenkins/plugins/logstash/LogstashBuildWrapper/help.html
index fd472c59..d0e26e2b 100644
--- a/src/main/resources/jenkins/plugins/logstash/LogstashBuildWrapper/help.html
+++ b/src/main/resources/jenkins/plugins/logstash/LogstashBuildWrapper/help.html
@@ -1,3 +1,6 @@
-
Send individual log lines to Logstash.
+
+ Send individual log lines to Logstash. You can optionally filter and modify them
+ via groovy script in from advanced configuration.
+
diff --git a/src/test/java/jenkins/plugins/logstash/LogstashOutputStreamTest.java b/src/test/java/jenkins/plugins/logstash/LogstashOutputStreamTest.java
index 5b445916..7ee3065a 100644
--- a/src/test/java/jenkins/plugins/logstash/LogstashOutputStreamTest.java
+++ b/src/test/java/jenkins/plugins/logstash/LogstashOutputStreamTest.java
@@ -14,6 +14,7 @@
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Mockito;
+import org.mockito.InOrder;
import org.mockito.runners.MockitoJUnitRunner;
@SuppressWarnings("resource")
@@ -118,4 +119,17 @@ public void eolSuccessNoDao() throws Exception {
assertEquals("Results don't match", msg, buffer.toString());
verify(mockWriter).isConnectionBroken();
}
+
+ @Test
+ public void writerClosedBeforeDelegate() throws Exception {
+ ByteArrayOutputStream mockBuffer = Mockito.spy(buffer);
+ new LogstashOutputStream(mockBuffer, mockWriter).close();
+
+ InOrder inOrder = Mockito.inOrder(mockBuffer, mockWriter);
+ inOrder.verify(mockWriter).close();
+ inOrder.verify(mockBuffer).close();
+
+ // Verify results
+ assertEquals("Results don't match", "", buffer.toString());
+ }
}
diff --git a/src/test/java/jenkins/plugins/logstash/LogstashWriterTest.java b/src/test/java/jenkins/plugins/logstash/LogstashWriterTest.java
index 8e643e39..8121e237 100644
--- a/src/test/java/jenkins/plugins/logstash/LogstashWriterTest.java
+++ b/src/test/java/jenkins/plugins/logstash/LogstashWriterTest.java
@@ -9,20 +9,26 @@
import jenkins.plugins.logstash.persistence.BuildData;
import jenkins.plugins.logstash.persistence.LogstashIndexerDao;
import jenkins.plugins.logstash.persistence.LogstashIndexerDao.IndexerType;
+import org.jenkinsci.plugins.scriptsecurity.sandbox.groovy.SecureGroovyScript;
+import org.jvnet.hudson.test.JenkinsRule;
import net.sf.json.JSONObject;
+import net.sf.json.JSONArray;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
+import org.junit.Rule;
import org.junit.runner.RunWith;
import org.mockito.*;
+import org.mockito.invocation.InvocationOnMock;
import org.mockito.runners.MockitoJUnitRunner;
+import org.mockito.stubbing.Answer;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.Collections;
-import java.util.GregorianCalendar;
+import java.util.Date;
import java.util.List;
import static org.hamcrest.core.StringContains.containsString;
@@ -31,13 +37,16 @@
@RunWith(MockitoJUnitRunner.class)
public class LogstashWriterTest {
+ @Rule public JenkinsRule j = new JenkinsRule();
+
// Extension of the unit under test that avoids making calls to getInstance() to get the DAO singleton
static LogstashWriter createLogstashWriter(final AbstractBuild, ?> testBuild,
OutputStream error,
final String url,
final LogstashIndexerDao indexer,
- final BuildData data) {
- return new LogstashWriter(testBuild, error, null) {
+ final BuildData data,
+ final LogstashPayloadProcessor processor) {
+ return new LogstashWriter(testBuild, error, null, processor) {
@Override
LogstashIndexerDao getDao() throws InstantiationException {
if (indexer == null) {
@@ -66,6 +75,14 @@ String getJenkinsUrl() {
};
}
+ static LogstashWriter createLogstashWriter(final AbstractBuild, ?> testBuild,
+ OutputStream error,
+ final String url,
+ final LogstashIndexerDao indexer,
+ final BuildData data) {
+ return createLogstashWriter(testBuild, error, url, indexer, data, null);
+ }
+
ByteArrayOutputStream errorBuffer;
@Mock LogstashIndexerDao mockDao;
@@ -87,15 +104,13 @@ public void before() throws Exception {
when(mockBuild.getParent()).thenReturn(mockProject);
when(mockBuild.getBuiltOn()).thenReturn(null);
when(mockBuild.getNumber()).thenReturn(123456);
- when(mockBuild.getTimestamp()).thenReturn(new GregorianCalendar());
+ when(mockBuild.getTime()).thenReturn(new Date());
when(mockBuild.getRootBuild()).thenReturn(mockBuild);
when(mockBuild.getBuildVariables()).thenReturn(Collections.emptyMap());
when(mockBuild.getSensitiveBuildVariables()).thenReturn(Collections.emptySet());
when(mockBuild.getEnvironments()).thenReturn(null);
when(mockBuild.getAction(AbstractTestResultAction.class)).thenReturn(mockTestResultAction);
- when(mockBuild.getLog(0)).thenReturn(Arrays.asList());
when(mockBuild.getLog(3)).thenReturn(Arrays.asList("line 1", "line 2", "line 3", "Log truncated..."));
- when(mockBuild.getLog(Integer.MAX_VALUE)).thenReturn(Arrays.asList("line 1", "line 2", "line 3", "line 4"));
when(mockBuild.getEnvironment(null)).thenReturn(new EnvVars());
when(mockTestResultAction.getTotalCount()).thenReturn(0);
@@ -107,7 +122,15 @@ public void before() throws Exception {
when(mockProject.getFullName()).thenReturn("parent/LogstashWriterTest");
when(mockDao.buildPayload(Matchers.any(BuildData.class), Matchers.anyString(), Matchers.anyListOf(String.class)))
- .thenReturn(JSONObject.fromObject("{\"data\":{},\"message\":[\"test\"],\"source\":\"jenkins\",\"source_host\":\"http://my-jenkins-url\",\"@version\":1}"));
+ .thenAnswer(new Answer() {
+ @Override
+ public JSONObject answer(InvocationOnMock invocation) {
+ Object[] args = invocation.getArguments();
+ JSONObject json = JSONObject.fromObject("{\"data\":{},\"message\": null,\"source\":\"jenkins\",\"source_host\":\"http://my-jenkins-url\",\"@version\":1}");
+ json.element("message", args[2]);
+ return json;
+ }
+ });
Mockito.doNothing().when(mockDao).push(Matchers.anyString());
when(mockDao.getIndexerType()).thenReturn(IndexerType.REDIS);
@@ -133,7 +156,7 @@ public void constructorSuccess() throws Exception {
// Verify that the BuildData constructor is what is being called here.
// This also lets us verify that in the instantiation failure cases we do not construct BuildData.
verify(mockBuild).getId();
- verify(mockBuild, times(2)).getResult();
+ verify(mockBuild, times(1)).getResult();
verify(mockBuild, times(2)).getParent();
verify(mockBuild, times(2)).getProject();
verify(mockBuild, times(1)).getStartTimeInMillis();
@@ -144,7 +167,7 @@ public void constructorSuccess() throws Exception {
verify(mockBuild).getAction(AbstractTestResultAction.class);
verify(mockBuild).getBuiltOn();
verify(mockBuild, times(2)).getNumber();
- verify(mockBuild).getTimestamp();
+ verify(mockBuild).getTime();
verify(mockBuild, times(4)).getRootBuild();
verify(mockBuild).getBuildVariables();
verify(mockBuild).getSensitiveBuildVariables();
@@ -235,10 +258,55 @@ public void writeBuildLogSuccess() throws Exception {
// Verify results
// No error output
assertEquals("Results don't match", "", errorBuffer.toString());
- verify(mockBuild).getLog(3);
+ String lines = JSONArray.fromObject(mockBuild.getLog(3)).toString();
+ verify(mockBuild, times(2)).getLog(3);
verify(mockDao).buildPayload(Matchers.eq(mockBuildData), Matchers.eq("http://my-jenkins-url"), Matchers.anyListOf(String.class));
- verify(mockDao).push("{\"data\":{},\"message\":[\"test\"],\"source\":\"jenkins\",\"source_host\":\"http://my-jenkins-url\",\"@version\":1}");
+ verify(mockDao).push("{\"data\":{},\"message\":" + lines + ",\"source\":\"jenkins\",\"source_host\":\"http://my-jenkins-url\",\"@version\":1}");
+ }
+
+ @Test
+ public void writeProcessedSuccess() throws Exception {
+ String goodMsg = "test message";
+ String ignoredMsg = "ignored input";
+ String scriptString =
+ "if (payload) {\n" +
+ " if (payload['message'][0] =~ /" + ignoredMsg + "/) {\n" +
+ " payload = null\n" +
+ " } else {\n" +
+ " console.println('l');\n" +
+ " }\n" +
+ " lastPayload = payload\n" +
+ "} else {\n" +
+ " console.println('test build console message')\n" +
+ " payload = lastPayload\n" +
+ "}";
+
+ SecureGroovyScript script = new SecureGroovyScript(scriptString, true, null);
+ script.configuringWithNonKeyItem();
+ LogstashScriptProcessor processor = new LogstashScriptProcessor(script, errorBuffer);
+ LogstashWriter writer = createLogstashWriter(mockBuild, errorBuffer, "http://my-jenkins-url", mockDao, mockBuildData, processor);
+ errorBuffer.reset();
+
+ // Unit under test
+ writer.write(goodMsg);
+ writer.write(ignoredMsg);
+ writer.write(goodMsg);
+ writer.close();
+
+ // Verify results
+ // buffer contains 2 lines logged by the script, then standard tear down message and finally test message at close
+ assertEquals("Results don't match", "l\nl\nTearing down Script Log Processor..\ntest build console message\n", errorBuffer.toString());
+
+ InOrder inOrder = Mockito.inOrder(mockDao);
+
+ // first message is generated and pushed to DAO
+ inOrder.verify(mockDao).buildPayload(Matchers.eq(mockBuildData), Matchers.eq("http://my-jenkins-url"), Matchers.anyListOf(String.class));
+ inOrder.verify(mockDao).push("{\"data\":{},\"message\":[\"" + goodMsg + "\"],\"source\":\"jenkins\",\"source_host\":\"http://my-jenkins-url\",\"@version\":1}");
+ // now message only generated but filtered out by script thus not pushed to DAO
+ inOrder.verify(mockDao, times(2)).buildPayload(Matchers.eq(mockBuildData), Matchers.eq("http://my-jenkins-url"), Matchers.anyListOf(String.class));
+ // the message at close time is generated by the script so no call to DAO for that
+ inOrder.verify(mockDao, times(2)).push("{\"data\":{},\"message\":[\"" + goodMsg + "\"],\"source\":\"jenkins\",\"source_host\":\"http://my-jenkins-url\",\"@version\":1}");
}
@Test
@@ -302,9 +370,10 @@ public void writeBuildLogGetLogError() throws Exception {
List expectedErrorLines = Arrays.asList(
"[logstash-plugin]: Unable to serialize log data.",
"java.io.IOException: Unable to read log file");
- verify(mockDao).push("{\"data\":{},\"message\":[\"test\"],\"source\":\"jenkins\",\"source_host\":\"http://my-jenkins-url\",\"@version\":1}");
verify(mockDao).buildPayload(eq(mockBuildData), eq("http://my-jenkins-url"), logLinesCaptor.capture());
List actualLogLines = logLinesCaptor.getValue();
+ String linesJSON = JSONArray.fromObject(actualLogLines).toString();
+ verify(mockDao).push("{\"data\":{},\"message\":" + linesJSON + ",\"source\":\"jenkins\",\"source_host\":\"http://my-jenkins-url\",\"@version\":1}");
assertThat("The exception was not sent to Logstash", actualLogLines.get(0), containsString(expectedErrorLines.get(0)));
assertThat("The exception was not sent to Logstash", actualLogLines.get(1), containsString(expectedErrorLines.get(1)));
diff --git a/src/test/java/jenkins/plugins/logstash/persistence/BuildDataTest.java b/src/test/java/jenkins/plugins/logstash/persistence/BuildDataTest.java
index 51dfb09b..857029f5 100644
--- a/src/test/java/jenkins/plugins/logstash/persistence/BuildDataTest.java
+++ b/src/test/java/jenkins/plugins/logstash/persistence/BuildDataTest.java
@@ -9,7 +9,6 @@
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
-import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
@@ -69,10 +68,9 @@ public void before() throws Exception {
when(mockBuild.getDisplayName()).thenReturn("BuildData Test");
when(mockBuild.getFullDisplayName()).thenReturn("BuildData Test #123456");
when(mockBuild.getDescription()).thenReturn("Mock project for testing BuildData");
- when(mockBuild.getProject()).thenReturn(mockProject);
when(mockBuild.getParent()).thenReturn(mockProject);
when(mockBuild.getNumber()).thenReturn(123456);
- when(mockBuild.getTimestamp()).thenReturn(new GregorianCalendar());
+ when(mockBuild.getTime()).thenReturn(new Date());
when(mockBuild.getRootBuild()).thenReturn(mockBuild);
when(mockBuild.getBuildVariables()).thenReturn(Collections.emptyMap());
when(mockBuild.getSensitiveBuildVariables()).thenReturn(Collections.emptySet());
@@ -116,7 +114,7 @@ private void verifyMocks() throws Exception
verify(mockProject).getFullName();
verify(mockBuild).getId();
- verify(mockBuild, times(2)).getResult();
+ verify(mockBuild, times(1)).getResult();
verify(mockBuild, times(2)).getParent();
verify(mockBuild).getDisplayName();
verify(mockBuild).getFullDisplayName();
@@ -126,7 +124,7 @@ private void verifyMocks() throws Exception
verify(mockBuild).getAction(AbstractTestResultAction.class);
verify(mockBuild).getBuiltOn();
verify(mockBuild).getNumber();
- verify(mockBuild).getTimestamp();
+ verify(mockBuild).getTime();
verify(mockBuild, times(4)).getRootBuild();
verify(mockBuild).getBuildVariables();
verify(mockBuild).getSensitiveBuildVariables();