Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

avoid duplicate sending and global notifier mode #66

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@

import hudson.Extension;
import hudson.console.ConsoleLogFilter;
import hudson.model.AbstractBuild;
import hudson.model.AbstractProject;
import hudson.model.Run;

Expand All @@ -19,59 +18,100 @@ public class LogstashConsoleLogFilter extends ConsoleLogFilter implements Serial
private static final Logger LOGGER = Logger.getLogger(LogstashConsoleLogFilter.class.getName());

private transient Run<?, ?> run;

public LogstashConsoleLogFilter() {}

public LogstashConsoleLogFilter(Run<?, ?> run)
{
this.run = run;
}

private static final long serialVersionUID = 1L;

/**
* {@inheritDoc}
*
* Currently pipeline is not supporting global ConsoleLogFilters. So even when we have it enabled globally
* we would not log to an indexer without the logstash step.
* But when pipeline supports global ConsoleLogFilters we don't want to log twice when we have the step in
* the pipeline.
* With the LogstashMarkerRunAction we can detect if it is enabled globally and if pipeline is supporting
* global ConsoleLogFilters.
* The LogstashMarkerRunAction will be only attached to a WorkflowRun when pipeline supports global
* ConsoleLogFilters (JENKINS-45693). And the assumption is that the marker action is attached before the
* logstashStep is initialized.
* This marker action will also disable the notifier.
*
*/
@Override
public OutputStream decorateLogger(Run build, OutputStream logger) throws IOException, InterruptedException
{

LogstashConfiguration configuration = LogstashConfiguration.getInstance();
if (!configuration.isEnabled())
{
LOGGER.log(Level.FINE, "Logstash is disabled. Logs will not be forwarded.");
return logger;
}

if (build != null && build instanceof AbstractBuild<?, ?>)

// A pipeline step uses the constructor which sets run.
if (run != null)
{
if (isLogstashEnabled(build))
{
LogstashWriter logstash = getLogStashWriter(build, logger);
return new LogstashOutputStream(logger, logstash);
}
else
if (run.getAction(LogstashMarkerAction.class) != null)
{
LOGGER.log(Level.FINEST, "Logstash is enabled globally. No need to decorate the logger another time for {0}",
run.toString());
return logger;
}
return getLogstashOutputStream(run, logger);
}
if (run != null)
{
LogstashWriter logstash = getLogStashWriter(run, logger);
return new LogstashOutputStream(logger, logstash);
}
else

// Not pipeline step so @{code build} should be set.
if (isLogstashEnabled(build))
{
return logger;
if (build.getAction(LogstashMarkerAction.class) == null)
{
build.addAction(new LogstashMarkerAction());
}
return getLogstashOutputStream(build, logger);
}

return logger;
}

private LogstashOutputStream getLogstashOutputStream(Run<?, ?> run, OutputStream logger)
{
LogstashWriter logstash = getLogStashWriter(run, logger);
return new LogstashOutputStream(logger, logstash);
}

LogstashWriter getLogStashWriter(Run<?, ?> build, OutputStream errorStream)
{
return new LogstashWriter(build, errorStream, null, build.getCharset());
}

private boolean isLogstashEnabled(Run<?, ?> build)
private boolean isLogstashEnabledGlobally()
{
LogstashConfiguration configuration = LogstashConfiguration.getInstance();
if (configuration.isEnableGlobally())
{
return true;
}
return false;
}

private boolean isLogstashEnabled(Run<?, ?> build)
{
if (build == null)
{
return false;
}

if (isLogstashEnabledGlobally())
{
return true;
}

if (build.getParent() instanceof AbstractProject)
{
Expand All @@ -83,5 +123,4 @@ private boolean isLogstashEnabled(Run<?, ?> build)
}
return false;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package jenkins.plugins.logstash;

import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;

import hudson.Extension;
import hudson.model.InvisibleAction;

/*
* A marker for Runs if logstash is enabled globally or set via JobProperty
* When enabled globally or per JobProperty it doesn't make sense to send the data another time via a logstash step in a pipeline or
* the logstashSend notifier
*/
@Extension
@Restricted(NoExternalUse.class)
public class LogstashMarkerAction extends InvisibleAction
{}
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,12 @@ private boolean perform(Run<?, ?> run, TaskListener listener) {
return true;
}

// globally enabled and active. No need to send the data another time.
if (run.getAction(LogstashMarkerAction.class) != null)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This effectively makes you unable to have a single-document log generated from any build if global is enabled?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes but why you would want single document when you have it already line by line? Currently I don't distinguish between globally and set via JobProperty. We could also add a force option the Notifier.

Copy link

@jakub-bochenski jakub-bochenski Jun 7, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO best thing would be to have options to enable both modes globally and an ability to override this per-project.

Minimally I'm concerned about a situation when you want to enable the global option but you can't because of some existing projects rely on sending the single document

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Enabling globally to send everything at the end could be done in a RunListener.onFinalized() or onCompleted()with the limitation that it can't fail the build.
onFinalized() would have the advantage that it includes the Final Result line, while onCompleted() would allow to write something to the log but which would not end up in the log sent to the indexer.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the RunListener we could even get it right for pipeline as the Result is set when it's run. Avoiding all those questions about how to get the result in.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, though it is a complex topic and not yet sure how to implement in detail.
E.g. when you enable globally the notifier behaviour (just a single event at the end with the log), how to disable this in a project. Currently I would prefer the JobProperty for this. What if one specifies an explicit notifier in his job while it is also globally enabled. Which one should be taken and what when he wants to fail the build if upload fails (a runlistener can't modify the result anymore)? One could also enable the notifier behaviour via the JobProperty to replace the wrapper behaviour enabled globally. Should we allow a force of the notifier even when globally we have the wrapper enabled.

If we want to have full flexibility we might confuse people when they want to configure the jobs.

Maybe one could reduce it to the following.
A job specific setting overrides a global setting. You can disable the global setting without enabling a job setting. if a job enables the wrapper and configures the notifier only the wrapper will be used.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if I understand the "You can disable the global setting without enabling a job setting. " part.
Agree that what is enabled on the job should win with the global setting. Also agree to only choose one if both are configured directly on the job. It would be nice to emit a warning in that case.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider you have enabled it globally but for some reason, you don't want to upload the logs at all for a certain project.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, but how do you propose to handle this? There is no explicit disable option at the moment (you could set maxLines=1)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

via the JobProperty. It gets a checkbox to disable a global setting.

{
return true;
}

PrintStream errorPrintStream = listener.getLogger();
LogstashWriter logstash = getLogStashWriter(run, errorPrintStream, listener);
logstash.writeBuildLog(maxLines);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -284,4 +284,43 @@ public void disabledWillNotWrite() throws Exception
List<JSONObject> dataLines = memoryDao.getOutput();
assertThat(dataLines.size(), equalTo(0));
}

public void enabledGloballyNotifierWillNotSend() throws Exception
{
LogstashConfiguration.getInstance().setEnableGlobally(true);
project.getPublishersList().add(new LogstashNotifier(10, false));

QueueTaskFuture<FreeStyleBuild> f = project.scheduleBuild2(0);

FreeStyleBuild build = f.get();
assertThat(build.getResult(), equalTo(Result.SUCCESS));
List<JSONObject> dataLines = memoryDao.getOutput();
assertThat(dataLines.size(), is(4));
JSONObject firstLine = dataLines.get(0);
JSONObject lastLine = dataLines.get(dataLines.size()-1);
JSONObject data = firstLine.getJSONObject("data");
assertThat(data.getString("buildHost"),equalTo("Jenkins"));
assertThat(data.getString("buildLabel"),equalTo("master"));
assertThat(lastLine.getJSONArray("message").get(0).toString(),equalTo("Finished: SUCCESS"));
}

@Test
public void enabledJobPropertyNotifierWillNotSend() throws Exception
{
project.addProperty(new LogstashJobProperty());
project.getPublishersList().add(new LogstashNotifier(10, false));

QueueTaskFuture<FreeStyleBuild> f = project.scheduleBuild2(0);

FreeStyleBuild build = f.get();
assertThat(build.getResult(), equalTo(Result.SUCCESS));
List<JSONObject> dataLines = memoryDao.getOutput();
assertThat(dataLines.size(), is(4));
JSONObject firstLine = dataLines.get(0);
JSONObject lastLine = dataLines.get(dataLines.size()-1);
JSONObject data = firstLine.getJSONObject("data");
assertThat(data.getString("buildHost"),equalTo("Jenkins"));
assertThat(data.getString("buildLabel"),equalTo("master"));
assertThat(lastLine.getJSONArray("message").get(0).toString(),equalTo("Finished: SUCCESS"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import hudson.model.BuildListener;
import hudson.model.AbstractBuild;
import hudson.model.AbstractProject;
import hudson.model.Action;
import hudson.model.TaskListener;
import hudson.model.Run;
import hudson.model.Result;
Expand Down Expand Up @@ -62,6 +63,7 @@ LogstashWriter getLogStashWriter(Run<?, ?> run, OutputStream errorStream, TaskLi
}

static class MockRun<P extends AbstractProject<P,R>, R extends AbstractBuild<P,R>> extends Run<P,R> {

Result result;

MockRun(P job) throws IOException {
Expand Down Expand Up @@ -136,6 +138,7 @@ public void performSuccess() throws Exception {
verify(mockListener).getLogger();
verify(mockWriter).writeBuildLog(3);
verify(mockWriter).isConnectionBroken();
verify(mockBuild).getAction(LogstashMarkerAction.class);

assertEquals("Errors were written", "", errorBuffer.toString());

Expand Down Expand Up @@ -172,6 +175,7 @@ public void performBadWriterDoNotFailBuild() throws Exception {
verify(mockListener).getLogger();
verify(mockWriter).writeBuildLog(3);
verify(mockWriter).isConnectionBroken();
verify(mockBuild).getAction(LogstashMarkerAction.class);

assertEquals("Error was not written", "Mocked Constructor failure", errorBuffer.toString());
}
Expand Down Expand Up @@ -213,6 +217,7 @@ public void performBadWriterDoFailBuild() throws Exception {
verify(mockListener).getLogger();
verify(mockWriter).writeBuildLog(3);
verify(mockWriter, times(2)).isConnectionBroken();
verify(mockBuild).getAction(LogstashMarkerAction.class);
assertEquals("Error was not written", "Mocked Constructor failure", errorBuffer.toString());
}

Expand Down Expand Up @@ -264,6 +269,7 @@ public Object answer(InvocationOnMock invocationOnMock) throws Throwable {
verify(mockListener).getLogger();
verify(mockWriter).writeBuildLog(3);
verify(mockWriter, times(3)).isConnectionBroken();
verify(mockBuild).getAction(LogstashMarkerAction.class);

assertThat("Wrong error message", errorBuffer.toString(), containsString(errorMsg));
}
Expand All @@ -286,6 +292,7 @@ public void performAllLines() throws Exception {
verify(mockListener).getLogger();
verify(mockWriter).writeBuildLog(-1);
verify(mockWriter, times(2)).isConnectionBroken();
verify(mockBuild).getAction(LogstashMarkerAction.class);

assertEquals("Errors were written", "", errorBuffer.toString());
}
Expand All @@ -308,6 +315,7 @@ public void performZeroLines() throws Exception {
verify(mockListener).getLogger();
verify(mockWriter).writeBuildLog(0);
verify(mockWriter, times(2)).isConnectionBroken();
verify(mockBuild).getAction(LogstashMarkerAction.class);

assertEquals("Errors were written", "", errorBuffer.toString());
}
Expand Down