-
Notifications
You must be signed in to change notification settings - Fork 78
Allow multiple LogStorage with primary and secondaries #417
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
base: master
Are you sure you want to change the base?
Changes from all commits
2497d7e
aefa52d
2e0d37c
88322ca
2e2c97b
dd6e930
b2a5025
bd88176
9090904
2137275
ed2165c
dd5d67d
1512a75
3c9d50d
1476e12
05c1f4d
3fe4cc6
da22a49
cdc3958
8dec700
02e97a3
ee9a44e
d8fec33
423246b
6d543ad
77d2fc3
b707a36
b1e6d01
2b3f3d6
8da65e3
62b9450
3782d00
96d6dae
365c9e6
d93ae64
d83b6c0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
package org.jenkinsci.plugins.workflow.log; | ||
|
||
import edu.umd.cs.findbugs.annotations.NonNull; | ||
import hudson.Extension; | ||
import hudson.model.Descriptor; | ||
import java.io.File; | ||
import org.jenkinsci.Symbol; | ||
import org.jenkinsci.plugins.workflow.flow.FlowExecutionOwner; | ||
import org.kohsuke.accmod.Restricted; | ||
import org.kohsuke.accmod.restrictions.Beta; | ||
import org.kohsuke.stapler.DataBoundConstructor; | ||
|
||
@Restricted(Beta.class) | ||
public class FileLogStorageFactory implements LogStorageFactory { | ||
|
||
@DataBoundConstructor | ||
public FileLogStorageFactory() {} | ||
|
||
@Override | ||
public LogStorage forBuild(@NonNull FlowExecutionOwner b) { | ||
try { | ||
return FileLogStorage.forFile(new File(b.getRootDir(), "log")); | ||
} catch (Exception x) { | ||
return new BrokenLogStorage(x); | ||
} | ||
} | ||
|
||
@Extension | ||
@Symbol("file") | ||
public static final class DescriptorImpl extends LogStorageFactoryDescriptor<FileLogStorageFactory> { | ||
@NonNull | ||
@Override | ||
public String getDisplayName() { | ||
return "Standard file logger"; | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
package org.jenkinsci.plugins.workflow.log; | ||
|
||
import hudson.model.Descriptor; | ||
import org.kohsuke.accmod.Restricted; | ||
import org.kohsuke.accmod.restrictions.Beta; | ||
|
||
@Restricted(Beta.class) | ||
public abstract class LogStorageFactoryDescriptor<T extends LogStorageFactory> extends Descriptor<LogStorageFactory> { | ||
|
||
/** | ||
* Indicates whether the factory supports being used in read/write mode (e.g. as a top-level logger, or as a primary for {@link org.jenkinsci.plugins.workflow.log.tee.TeeLogStorageFactory}) | ||
*/ | ||
public boolean isReadWrite() { | ||
return true; | ||
} | ||
/** | ||
* Indicates whether the factory supports being used in write-only mode (as a secondary for {@link org.jenkinsci.plugins.workflow.log.tee.TeeLogStorageFactory}). | ||
*/ | ||
public boolean isWriteOnly() { | ||
return true; | ||
} | ||
|
||
/** | ||
* Allow to define the default factory instance to use if no configuration exists | ||
*/ | ||
public LogStorageFactory getDefaultInstance() { | ||
return null; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
package org.jenkinsci.plugins.workflow.log.configuration; | ||
|
||
import edu.umd.cs.findbugs.annotations.NonNull; | ||
import hudson.Extension; | ||
import hudson.ExtensionList; | ||
import java.util.ArrayList; | ||
import java.util.List; | ||
import java.util.logging.Logger; | ||
import jenkins.model.GlobalConfiguration; | ||
import jenkins.model.Jenkins; | ||
import net.sf.json.JSONObject; | ||
import org.jenkinsci.Symbol; | ||
import org.jenkinsci.plugins.workflow.log.LogStorageFactory; | ||
import org.jenkinsci.plugins.workflow.log.LogStorageFactoryDescriptor; | ||
import org.kohsuke.accmod.Restricted; | ||
import org.kohsuke.accmod.restrictions.Beta; | ||
import org.kohsuke.accmod.restrictions.NoExternalUse; | ||
import org.kohsuke.stapler.DataBoundSetter; | ||
import org.kohsuke.stapler.StaplerRequest2; | ||
|
||
@Extension | ||
@Symbol("pipelineLogging") | ||
@Restricted(Beta.class) | ||
public class PipelineLoggingGlobalConfiguration extends GlobalConfiguration { | ||
private static final Logger LOGGER = Logger.getLogger(PipelineLoggingGlobalConfiguration.class.getName()); | ||
private LogStorageFactory factory; | ||
|
||
public PipelineLoggingGlobalConfiguration() { | ||
load(); | ||
} | ||
|
||
/** | ||
* For configuration only. Use {@link #getFactoryOrDefault()} instead. | ||
*/ | ||
@Restricted(NoExternalUse.class) | ||
public LogStorageFactory getFactory() { | ||
return factory; | ||
} | ||
|
||
@DataBoundSetter | ||
public void setFactory(LogStorageFactory factory) { | ||
this.factory = factory; | ||
save(); | ||
} | ||
|
||
@Override | ||
public boolean configure(StaplerRequest2 req, JSONObject json) throws FormException { | ||
this.factory = null; | ||
return super.configure(req, json); | ||
} | ||
|
||
public LogStorageFactory getFactoryOrDefault() { | ||
if (factory == null) { | ||
return LogStorageFactory.getDefaultFactory(); | ||
} | ||
return factory; | ||
} | ||
|
||
public List<LogStorageFactoryDescriptor<?>> getLogStorageFactoryDescriptors() { | ||
List<LogStorageFactoryDescriptor<?>> result = new ArrayList<>(); | ||
result.add(null); // offer the option to use the default factory without any explicit configuration | ||
result.addAll(getFilteredLogStorageFactoryDescriptors()); | ||
return result; | ||
} | ||
|
||
private List<LogStorageFactoryDescriptor<?>> getFilteredLogStorageFactoryDescriptors() { | ||
return LogStorageFactory.all().stream() | ||
.filter(LogStorageFactoryDescriptor::isReadWrite) | ||
.toList(); | ||
} | ||
|
||
public LogStorageFactoryDescriptor<?> getDefaultFactoryDescriptor() { | ||
return LogStorageFactory.getDefaultFactory().getDescriptor(); | ||
} | ||
|
||
public String getDefaultFactoryPlugin() { | ||
var pluginWrapper = Jenkins.get().getPluginManager().whichPlugin(LogStorageFactory.getDefaultFactory().getClass()); | ||
return pluginWrapper != null ? pluginWrapper.getShortName() : "unknown"; | ||
} | ||
|
||
public static PipelineLoggingGlobalConfiguration get() { | ||
return ExtensionList.lookupSingleton(PipelineLoggingGlobalConfiguration.class); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
package org.jenkinsci.plugins.workflow.log.tee; | ||
|
||
import edu.umd.cs.findbugs.annotations.NonNull; | ||
import hudson.model.BuildListener; | ||
import hudson.model.TaskListener; | ||
import java.io.OutputStream; | ||
import java.io.Serial; | ||
import java.util.List; | ||
import org.jenkinsci.plugins.workflow.log.OutputStreamTaskListener; | ||
|
||
class TeeBuildListener extends OutputStreamTaskListener.Default | ||
implements BuildListener, OutputStreamTaskListener, AutoCloseable { | ||
|
||
@Serial | ||
private static final long serialVersionUID = 1L; | ||
|
||
private final TaskListener primary; | ||
|
||
private final List<TaskListener> secondaries; | ||
|
||
private transient OutputStream outputStream; | ||
|
||
TeeBuildListener(TaskListener primary, TaskListener... secondaries) { | ||
this.primary = primary; | ||
this.secondaries = List.of(secondaries); | ||
} | ||
|
||
@NonNull | ||
@Override | ||
public synchronized OutputStream getOutputStream() { | ||
if (outputStream == null) { | ||
outputStream = new TeeOutputStream( | ||
OutputStreamTaskListener.getOutputStream(primary), | ||
secondaries.stream() | ||
.map(OutputStreamTaskListener::getOutputStream) | ||
.toArray(OutputStream[]::new)); | ||
} | ||
return outputStream; | ||
} | ||
|
||
@Override | ||
public void close() throws Exception { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just to note: While I was looking at the code yesterday I realized the logic here is a bit confusing. We close the delegate This means that after a call to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If I update with something like:
then
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think you specifically need to use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. But now with the additional
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, sorry, I was not saying that we definitely need it, only that if we do need it, we need to close the full There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok, so now we close the stream twice, but everything seems to be ok IIUC. @jgreffe did you investigate to see if you prefer the behavior with the current code or how you had things prior to adding line 51? |
||
getLogger().close(); | ||
Exception exception = null; | ||
if (primary instanceof AutoCloseable) { | ||
try { | ||
((AutoCloseable) primary).close(); | ||
} catch (Exception e) { | ||
exception = e; | ||
} | ||
} | ||
for (TaskListener secondary : secondaries) { | ||
if (secondary instanceof AutoCloseable) { | ||
try { | ||
((AutoCloseable) secondary).close(); | ||
} catch (Exception e) { | ||
if (exception == null) { | ||
exception = e; | ||
} else { | ||
exception.addSuppressed(e); | ||
} | ||
} | ||
} | ||
} | ||
if (exception != null) { | ||
throw exception; | ||
} | ||
} | ||
} |
Uh oh!
There was an error while loading. Please reload this page.