-
-
Notifications
You must be signed in to change notification settings - Fork 80
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
Changes from 12 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 "File Log Storage Factory"; | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| package org.jenkinsci.plugins.workflow.log; | ||
|
|
||
| import hudson.model.Descriptor; | ||
|
|
||
| public abstract class LogStorageFactoryDescriptor<T extends LogStorageFactory> extends Descriptor<LogStorageFactory> { | ||
|
|
||
| /** | ||
| * States if the factory descriptor is configurable as primary TeeLogStorage. | ||
| */ | ||
| public boolean isConfigurableAsPrimaryTeeLogStorageFactory() { | ||
| return true; | ||
| } | ||
| /** | ||
| * States if the factory descriptor is configurable as secondary TeeLogStorage. | ||
| */ | ||
| public boolean isConfigurableAsSecondaryTeeLogStorageFactory() { | ||
| return true; | ||
| } | ||
dwnusbaum marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| /** | ||
| * 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,46 @@ | ||
| package org.jenkinsci.plugins.workflow.log.configuration; | ||
|
|
||
| import hudson.Extension; | ||
| import hudson.ExtensionList; | ||
| import java.util.List; | ||
| import java.util.logging.Logger; | ||
| import jenkins.model.GlobalConfiguration; | ||
| 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.stapler.DataBoundSetter; | ||
|
|
||
| @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(); | ||
| } | ||
|
|
||
| public LogStorageFactory getFactory() { | ||
| if (factory == null) { | ||
| factory = LogStorageFactory.getDefaultFactory(); | ||
| } | ||
| return factory; | ||
| } | ||
|
|
||
| @DataBoundSetter | ||
| public void setFactory(LogStorageFactory factory) { | ||
| this.factory = factory; | ||
| save(); | ||
| } | ||
|
|
||
| public List<LogStorageFactoryDescriptor<?>> getLogStorageFactoryDescriptors() { | ||
| return LogStorageFactory.all(); | ||
| } | ||
|
|
||
| public static PipelineLoggingGlobalConfiguration get() { | ||
| return ExtensionList.lookupSingleton(PipelineLoggingGlobalConfiguration.class); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| 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) { | ||
| if (!(primary instanceof OutputStreamTaskListener)) { | ||
| throw new ClassCastException("Primary is not an instance of OutputStreamTaskListener: " + primary); | ||
| } | ||
| List.of(secondaries).forEach(secondary -> { | ||
| if (!(secondary instanceof OutputStreamTaskListener)) { | ||
| throw new ClassCastException("Secondary is not an instance of OutputStreamTaskListener: " + secondary); | ||
| } | ||
| }); | ||
jgreffe marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| 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 { | ||
|
Member
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
Author
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
Member
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
Author
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.
Author
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
Member
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
Member
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? |
||
| 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; | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,73 @@ | ||
| package org.jenkinsci.plugins.workflow.log.tee; | ||
|
|
||
| import edu.umd.cs.findbugs.annotations.NonNull; | ||
| import hudson.console.AnnotatedLargeText; | ||
| import hudson.model.BuildListener; | ||
| import hudson.model.TaskListener; | ||
| import java.io.IOException; | ||
| import java.util.ArrayList; | ||
| import java.util.Arrays; | ||
| import java.util.List; | ||
| import java.util.Objects; | ||
| import org.jenkinsci.plugins.workflow.flow.FlowExecutionOwner; | ||
| import org.jenkinsci.plugins.workflow.graph.FlowNode; | ||
| import org.jenkinsci.plugins.workflow.log.LogStorage; | ||
| import org.kohsuke.accmod.Restricted; | ||
| import org.kohsuke.accmod.restrictions.Beta; | ||
|
|
||
| /** | ||
| * Advancaed implementation of log storage allowing a primary log storage for read and write; and multiple secondary log storages for writes. | ||
| * This behaves as a tee execution. | ||
| */ | ||
| @Restricted(Beta.class) | ||
| public class TeeLogStorage implements LogStorage { | ||
|
|
||
| LogStorage primary; | ||
| List<LogStorage> secondaries = List.of(); | ||
|
|
||
| /** | ||
| * Log storage allowing a primary for read/write and multiple secondaries for write only | ||
| * @param primary primary log storage used for read and write | ||
| * @param secondaries secondary log storages used for write | ||
| */ | ||
| public TeeLogStorage(@NonNull LogStorage primary, LogStorage... secondaries) { | ||
| this.primary = primary; | ||
| if (secondaries != null) { | ||
| this.secondaries = | ||
| Arrays.stream(secondaries).filter(Objects::nonNull).toList(); | ||
| } | ||
| } | ||
|
|
||
| @NonNull | ||
| @Override | ||
| public BuildListener overallListener() throws IOException, InterruptedException { | ||
| List<BuildListener> secondaryListeners = new ArrayList<>(); | ||
| for (LogStorage secondary : secondaries) { | ||
| secondaryListeners.add(secondary.overallListener()); | ||
| } | ||
| return new TeeBuildListener(primary.overallListener(), secondaryListeners.toArray(BuildListener[]::new)); | ||
| } | ||
|
|
||
| @NonNull | ||
| @Override | ||
| public TaskListener nodeListener(@NonNull FlowNode node) throws IOException, InterruptedException { | ||
| List<TaskListener> secondaryListeners = new ArrayList<>(); | ||
| for (LogStorage secondary : secondaries) { | ||
| secondaryListeners.add(secondary.nodeListener(node)); | ||
| } | ||
| return new TeeBuildListener(primary.nodeListener(node), secondaryListeners.toArray(TaskListener[]::new)); | ||
| } | ||
|
|
||
| @NonNull | ||
| @Override | ||
| public AnnotatedLargeText<FlowExecutionOwner.Executable> overallLog( | ||
| @NonNull FlowExecutionOwner.Executable build, boolean complete) { | ||
| return primary.overallLog(build, complete); | ||
| } | ||
|
|
||
| @NonNull | ||
| @Override | ||
| public AnnotatedLargeText<FlowNode> stepLog(@NonNull FlowNode node, boolean complete) { | ||
| return primary.stepLog(node, complete); | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.