passwords) {
+ this(logger, passwords, null);
+ }
+
+ // TODO: The logic relies on the default encoding, which may cause issues when master and agent have different encodings
+ @SuppressFBWarnings(value = "DM_DEFAULT_ENCODING", justification = "Open TODO item for wider rework")
+ @Override
+ protected void eol(byte[] bytes, int len) throws IOException {
+ String line = new String(bytes, 0, len);
+ if(passwordsAsPattern != null) {
+ line = passwordsAsPattern.matcher(line).replaceAll(MASKED_PASSWORD);
+ }
+ logger.write(line.getBytes());
+ }
+
+ /**
+ * {@inheritDoc}
+ * @throws IOException
+ */
+ @Override
+ public void close() throws IOException {
+ super.close();
+ logger.close();
+ }
+
+ /**
+ * {@inheritDoc}
+ * @throws IOException
+ */
+ @Override
+ public void flush() throws IOException {
+ super.flush();
+ logger.flush();
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/io/jenkins/plugins/extlogging/api/util/UniqueIdHelper.java b/src/main/java/io/jenkins/plugins/extlogging/api/util/UniqueIdHelper.java
new file mode 100644
index 0000000..5e8c05b
--- /dev/null
+++ b/src/main/java/io/jenkins/plugins/extlogging/api/util/UniqueIdHelper.java
@@ -0,0 +1,35 @@
+package io.jenkins.plugins.extlogging.api.util;
+
+import hudson.model.Run;
+import javax.annotation.CheckForNull;
+import javax.annotation.Nonnull;
+import jenkins.model.Jenkins;
+import org.jenkinsci.plugins.uniqueid.IdStore;
+import org.kohsuke.accmod.Restricted;
+import org.kohsuke.accmod.restrictions.NoExternalUse;
+
+/**
+ * Creates on-demand Unique IDs.
+ * @author Oleg Nenashev
+ */
+public class UniqueIdHelper {
+
+ @CheckForNull
+ public static String getOrCreateId(@Nonnull Run,?> run) {
+ return getOrCreateId(run.getParent());
+ }
+
+ @CheckForNull
+ public static String getOrCreateId(@Nonnull hudson.model.Job,?> job) {
+ if (Jenkins.getInstance() == null) {
+ return null;
+ }
+
+ String id = IdStore.getId(job);
+ if (id == null) {
+ IdStore.makeId(job);
+ id = IdStore.getId(job);;
+ }
+ return id;
+ }
+}
diff --git a/src/main/resources/index.jelly b/src/main/resources/index.jelly
new file mode 100644
index 0000000..dd3e91d
--- /dev/null
+++ b/src/main/resources/index.jelly
@@ -0,0 +1,5 @@
+
+
+
+ A Jenkins plugin to keep artifacts and Pipeline stashes in Amazon S3.
+
diff --git a/src/main/resources/io/jenkins/plugins/extlogging/api/impl/ExternalLoggingGlobalConfiguration/config.jelly b/src/main/resources/io/jenkins/plugins/extlogging/api/impl/ExternalLoggingGlobalConfiguration/config.jelly
new file mode 100644
index 0000000..5500e56
--- /dev/null
+++ b/src/main/resources/io/jenkins/plugins/extlogging/api/impl/ExternalLoggingGlobalConfiguration/config.jelly
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/io/jenkins/plugins/extlogging/api/util/AbstractConsoleAction/buildCaption.jelly b/src/main/resources/io/jenkins/plugins/extlogging/api/util/AbstractConsoleAction/buildCaption.jelly
new file mode 100644
index 0000000..3def4f6
--- /dev/null
+++ b/src/main/resources/io/jenkins/plugins/extlogging/api/util/AbstractConsoleAction/buildCaption.jelly
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+
+
+
+ |
+ ${%Progress}:
+ |
+
+ |
+
+ |
+
+
+
+
+
+ ${%Console Output from} ${it.dataSourceDisplayName}
+
+
+
diff --git a/src/test/java/io/jenkins/plugins/extlogging/api/FreestyleJobTest.java b/src/test/java/io/jenkins/plugins/extlogging/api/FreestyleJobTest.java
new file mode 100644
index 0000000..dcf2fea
--- /dev/null
+++ b/src/test/java/io/jenkins/plugins/extlogging/api/FreestyleJobTest.java
@@ -0,0 +1,56 @@
+package io.jenkins.plugins.extlogging.api;
+
+import hudson.model.FreeStyleBuild;
+import hudson.model.FreeStyleProject;
+import hudson.tasks.Shell;
+import io.jenkins.plugins.extlogging.api.impl.ExternalLoggingGlobalConfiguration;
+import io.jenkins.plugins.extlogging.api.util.MockExternalLoggingEventWriter;
+import io.jenkins.plugins.extlogging.api.util.MockLogBrowserFactory;
+import io.jenkins.plugins.extlogging.api.util.MockLoggingMethod;
+import io.jenkins.plugins.extlogging.api.util.MockLoggingMethodFactory;
+import io.jenkins.plugins.extlogging.api.util.MockLoggingTestUtil;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.jvnet.hudson.test.JenkinsRule;
+
+import java.io.File;
+
+/**
+ * @author Oleg Nenashev
+ * @since TODO
+ */
+public class FreestyleJobTest {
+
+ @Rule
+ public JenkinsRule j = new JenkinsRule();
+
+ @Rule
+ public TemporaryFolder tmpDir = new TemporaryFolder();
+
+ @Test
+ public void spotcheck_Default() throws Exception {
+ FreeStyleProject project = j.createFreeStyleProject();
+ project.getBuildersList().add(new Shell("echo hello"));
+
+ FreeStyleBuild build = j.buildAndAssertSuccess(project);
+ j.assertLogContains("hello", build);
+ }
+
+ @Test
+ public void spotcheck_Mock() throws Exception {
+ MockLoggingTestUtil.setup(tmpDir);
+ FreeStyleProject project = j.createFreeStyleProject();
+ project.getBuildersList().add(new Shell("echo hello"));
+
+ FreeStyleBuild build = j.buildAndAssertSuccess(project);
+ MockLoggingMethod lm = (MockLoggingMethod)build.getLoggingMethod();
+ MockExternalLoggingEventWriter writer = lm.getWriter();
+ Assert.assertTrue(writer.isEventWritten());
+ j.assertLogContains("hello", build);
+ }
+
+}
diff --git a/src/test/java/io/jenkins/plugins/extlogging/api/PipelineSmokeTest.java b/src/test/java/io/jenkins/plugins/extlogging/api/PipelineSmokeTest.java
new file mode 100644
index 0000000..9fe0898
--- /dev/null
+++ b/src/test/java/io/jenkins/plugins/extlogging/api/PipelineSmokeTest.java
@@ -0,0 +1,61 @@
+package io.jenkins.plugins.extlogging.api;
+
+import hudson.model.Run;
+import io.jenkins.plugins.extlogging.api.impl.ExternalLoggingGlobalConfiguration;
+import io.jenkins.plugins.extlogging.api.util.MockExternalLoggingEventWriter;
+import io.jenkins.plugins.extlogging.api.util.MockLogBrowser;
+import io.jenkins.plugins.extlogging.api.util.MockLogBrowserFactory;
+import io.jenkins.plugins.extlogging.api.util.MockLoggingMethod;
+import io.jenkins.plugins.extlogging.api.util.MockLoggingMethodFactory;
+import io.jenkins.plugins.extlogging.api.util.MockLoggingTestUtil;
+import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition;
+import org.jenkinsci.plugins.workflow.job.WorkflowJob;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.jvnet.hudson.test.JenkinsRule;
+
+import java.io.File;
+
+import static org.hamcrest.Matchers.instanceOf;
+import static org.hamcrest.Matchers.notNullValue;
+import static org.junit.Assert.assertThat;
+
+/**
+ * @author Oleg Nenashev
+ * @since TODO
+ */
+public class PipelineSmokeTest {
+
+ @Rule
+ public JenkinsRule j = new JenkinsRule();
+
+ @Rule
+ public TemporaryFolder tmpDir = new TemporaryFolder();
+
+ @Test
+ public void spotcheck_Default() throws Exception {
+ WorkflowJob project = j.createProject(WorkflowJob.class);
+ project.setDefinition(new CpsFlowDefinition("echo 'Hello'", true));
+ Run build = j.buildAndAssertSuccess(project);
+ j.assertLogContains("Hello", build);
+ }
+
+ @Test
+ public void spotcheck_Mock() throws Exception {
+ MockLoggingTestUtil.setup(tmpDir);
+ WorkflowJob project = j.createProject(WorkflowJob.class);
+ project.setDefinition(new CpsFlowDefinition("echo 'Hello'", true));
+
+ Run build = j.buildAndAssertSuccess(project);
+ assertThat(build.getLoggingMethod(), instanceOf(MockLoggingMethod.class));
+ MockLoggingMethod loggingMethod = (MockLoggingMethod)build.getLoggingMethod();
+ MockExternalLoggingEventWriter writer = loggingMethod.getWriter();
+ // Do not try to add it. Pipeline creates separate PipelineLogListeners for each call
+ // Assert.assertTrue(writer.isEventWritten());
+ j.assertLogContains("Hello", build);
+ }
+
+}
diff --git a/src/test/java/io/jenkins/plugins/extlogging/api/util/MockExternalLoggingEventWriter.java b/src/test/java/io/jenkins/plugins/extlogging/api/util/MockExternalLoggingEventWriter.java
new file mode 100644
index 0000000..c4233f7
--- /dev/null
+++ b/src/test/java/io/jenkins/plugins/extlogging/api/util/MockExternalLoggingEventWriter.java
@@ -0,0 +1,46 @@
+package io.jenkins.plugins.extlogging.api.util;
+
+import io.jenkins.plugins.extlogging.api.Event;
+import io.jenkins.plugins.extlogging.api.ExternalLoggingEventWriter;
+
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.util.logging.Logger;
+
+/**
+ * Mock {@link ExternalLoggingEventWriter} for testing purposes.
+ * @author Oleg Nenashev
+ * @see MockLoggingMethod
+ */
+public class MockExternalLoggingEventWriter extends ExternalLoggingEventWriter {
+
+ private static final Logger LOGGER =
+ Logger.getLogger(MockExternalLoggingEventWriter.class.getName());
+
+ private final File dest;
+
+ // Debug flags
+ private boolean eventWritten;
+
+ public MockExternalLoggingEventWriter(File dest) {
+ this.dest = dest;
+ }
+
+ @Override
+ public void writeEvent(Event event) throws IOException {
+ eventWritten = true;
+ FileWriter writer = new FileWriter(dest, true);
+ writer.write(event.toString() + "\n");
+ writer.flush();
+ writer.close();
+ }
+
+ public File getDest() {
+ return dest;
+ }
+
+ public boolean isEventWritten() {
+ return eventWritten;
+ }
+}
diff --git a/src/test/java/io/jenkins/plugins/extlogging/api/util/MockLogBrowser.java b/src/test/java/io/jenkins/plugins/extlogging/api/util/MockLogBrowser.java
new file mode 100644
index 0000000..789c972
--- /dev/null
+++ b/src/test/java/io/jenkins/plugins/extlogging/api/util/MockLogBrowser.java
@@ -0,0 +1,38 @@
+package io.jenkins.plugins.extlogging.api.util;
+
+import hudson.model.Run;
+import jenkins.model.logging.Loggable;
+import jenkins.model.logging.impl.FileLogBrowser;
+
+
+import java.io.File;
+import java.io.IOException;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * @author Oleg Nenashev
+ * @since TODO
+ */
+public class MockLogBrowser extends FileLogBrowser {
+
+ private static final Logger LOGGER =
+ Logger.getLogger(MockLogBrowser.class.getName());
+
+ private File baseDir;
+
+ public MockLogBrowser(Run, ?> run, File baseDir) {
+ super(run);
+ this.baseDir = baseDir;
+ }
+
+ @Override
+ protected Run, ?> getOwner() {
+ return (Run, ?>)super.getOwner();
+ }
+
+ @Override
+ public File getLogFileOrFail(Loggable loggable) throws IOException {
+ return new File(baseDir, getOwner().getFullDisplayName() + ".txt");
+ }
+}
diff --git a/src/test/java/io/jenkins/plugins/extlogging/api/util/MockLogBrowserFactory.java b/src/test/java/io/jenkins/plugins/extlogging/api/util/MockLogBrowserFactory.java
new file mode 100644
index 0000000..bb939ff
--- /dev/null
+++ b/src/test/java/io/jenkins/plugins/extlogging/api/util/MockLogBrowserFactory.java
@@ -0,0 +1,26 @@
+package io.jenkins.plugins.extlogging.api.util;
+
+import hudson.model.Run;
+import io.jenkins.plugins.extlogging.api.ExternalLogBrowserFactory;
+import jenkins.model.logging.LogBrowser;
+import jenkins.model.logging.Loggable;
+
+import java.io.File;
+
+/**
+ * @author Oleg Nenashev
+ * @since TODO
+ */
+public class MockLogBrowserFactory extends ExternalLogBrowserFactory {
+
+ private File baseDir;
+
+ public MockLogBrowserFactory(File baseDir) {
+ this.baseDir = baseDir;
+ }
+
+ @Override
+ public LogBrowser create(Loggable loggable) {
+ return new MockLogBrowser((Run, ?>)loggable, baseDir);
+ }
+}
diff --git a/src/test/java/io/jenkins/plugins/extlogging/api/util/MockLoggingMethod.java b/src/test/java/io/jenkins/plugins/extlogging/api/util/MockLoggingMethod.java
new file mode 100644
index 0000000..01e8dc2
--- /dev/null
+++ b/src/test/java/io/jenkins/plugins/extlogging/api/util/MockLoggingMethod.java
@@ -0,0 +1,46 @@
+package io.jenkins.plugins.extlogging.api.util;
+
+import hudson.model.Run;
+import io.jenkins.plugins.extlogging.api.ExternalLoggingEventWriter;
+import io.jenkins.plugins.extlogging.api.ExternalLoggingMethod;
+import jenkins.model.logging.LogBrowser;
+
+import javax.annotation.CheckForNull;
+import java.io.File;
+
+/**
+ * Mock logging method for testing purposes
+ * @author Oleg Nenashev
+ * @see MockExternalLoggingEventWriter
+ */
+public class MockLoggingMethod extends ExternalLoggingMethod {
+
+ private File baseDir;
+ transient MockExternalLoggingEventWriter writer;
+
+ public MockLoggingMethod(Run,?> run, File baseDir) {
+ super(run);
+ this.baseDir = baseDir;
+ }
+
+ @Override
+ protected Run, ?> getOwner() {
+ return (Run)super.getOwner();
+ }
+
+ @Override
+ public ExternalLoggingEventWriter _createWriter() {
+ writer = new MockExternalLoggingEventWriter(new File(baseDir, getOwner().getFullDisplayName() + ".txt"));
+ return writer;
+ }
+
+ @CheckForNull
+ public MockExternalLoggingEventWriter getWriter() {
+ return writer;
+ }
+
+ @Override
+ public LogBrowser getDefaultLogBrowser() {
+ return new MockLogBrowser(getOwner(), baseDir);
+ }
+}
diff --git a/src/test/java/io/jenkins/plugins/extlogging/api/util/MockLoggingMethodFactory.java b/src/test/java/io/jenkins/plugins/extlogging/api/util/MockLoggingMethodFactory.java
new file mode 100644
index 0000000..20c5c05
--- /dev/null
+++ b/src/test/java/io/jenkins/plugins/extlogging/api/util/MockLoggingMethodFactory.java
@@ -0,0 +1,29 @@
+package io.jenkins.plugins.extlogging.api.util;
+
+import hudson.model.Run;
+import io.jenkins.plugins.extlogging.api.ExternalLoggingMethod;
+import io.jenkins.plugins.extlogging.api.ExternalLoggingMethodFactory;
+import jenkins.model.logging.Loggable;
+
+import javax.annotation.CheckForNull;
+import java.io.File;
+import java.util.HashMap;
+
+/**
+ * @author Oleg Nenashev
+ * @since TODO
+ */
+public class MockLoggingMethodFactory extends ExternalLoggingMethodFactory {
+
+ private File baseDir;
+
+ public MockLoggingMethodFactory(File baseDir) {
+ this.baseDir = baseDir;
+ }
+
+ @CheckForNull
+ @Override
+ public ExternalLoggingMethod create(Loggable loggable) {
+ return new MockLoggingMethod((Run, ?>)loggable, baseDir);
+ }
+}
diff --git a/src/test/java/io/jenkins/plugins/extlogging/api/util/MockLoggingTestUtil.java b/src/test/java/io/jenkins/plugins/extlogging/api/util/MockLoggingTestUtil.java
new file mode 100644
index 0000000..3d4f3db
--- /dev/null
+++ b/src/test/java/io/jenkins/plugins/extlogging/api/util/MockLoggingTestUtil.java
@@ -0,0 +1,24 @@
+package io.jenkins.plugins.extlogging.api.util;
+
+import io.jenkins.plugins.extlogging.api.impl.ExternalLoggingGlobalConfiguration;
+import org.junit.Before;
+import org.junit.rules.TemporaryFolder;
+
+import javax.annotation.Nonnull;
+import java.io.File;
+
+/**
+ * Test util for mock classes.
+ * @author Oleg Nenashev
+ * @see MockLogBrowser
+ * @see MockLoggingMethod
+ */
+public class MockLoggingTestUtil {
+
+ public static void setup(@Nonnull TemporaryFolder tmpDir) throws Exception {
+ ExternalLoggingGlobalConfiguration cfg = ExternalLoggingGlobalConfiguration.getInstance();
+ File logDir = tmpDir.newFolder("logs");
+ cfg.setLoggingMethod(new MockLoggingMethodFactory(logDir));
+ cfg.setLogBrowser(new MockLogBrowserFactory(logDir));
+ }
+}