-
Notifications
You must be signed in to change notification settings - Fork 77
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?
Conversation
src/main/java/org/jenkinsci/plugins/workflow/log/tee/TeeLogStorageFactory.java
Outdated
Show resolved
Hide resolved
...in/java/org/jenkinsci/plugins/workflow/configuration/PipelineLoggingGlobalConfiguration.java
Outdated
Show resolved
Hide resolved
src/main/java/org/jenkinsci/plugins/workflow/log/FileLogStorageFactory.java
Outdated
Show resolved
Hide resolved
src/main/java/org/jenkinsci/plugins/workflow/log/LogStorage.java
Outdated
Show resolved
Hide resolved
@@ -35,13 +38,21 @@ | |||
* Factory interface for {@link LogStorage}. | |||
*/ | |||
@Restricted(Beta.class) | |||
public interface LogStorageFactory extends ExtensionPoint { | |||
public interface LogStorageFactory extends Describable<LogStorageFactory> { | |||
|
|||
/** | |||
* Checks whether we should handle a given build. | |||
* @param b a build about to start | |||
* @return a mechanism for handling this build, or null to fall back to the next implementation or the default |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Javadoc here is no longer accurate, but we need to decide how we want to change it. I think the expectation is that all factories are expected to handle all builds, explicitly falling back to FileLogStorage
or BrokenLogStorage
as necessary. We can think about this as we look into migrating the existing implementations.
...in/java/org/jenkinsci/plugins/workflow/configuration/PipelineLoggingGlobalConfiguration.java
Outdated
Show resolved
Hide resolved
src/main/java/org/jenkinsci/plugins/workflow/log/tee/TeeBuildListener.java
Outdated
Show resolved
Hide resolved
src/main/java/org/jenkinsci/plugins/workflow/log/tee/TeeBuildListener.java
Outdated
Show resolved
Hide resolved
/** | ||
* Foundation for compliance tests of {@link TeeLogStorage} implementations. | ||
*/ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With the current approach, nothing else would implement TeeLogStorage
, so I would just inline all of this into TeeLogStorageTest
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would expect downstream plugins to have a test extending the current one and implement primaryStorage()
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure. I don't think we really expect any implementation to have a problem as a primary. If there is a problem, it's probably with TeeLogStorage
itself, or with code that won't be covered by these tests, for example CPE will need to adjust code that checks the LogStorageFactory
implementation, but that's not going to be covered here since it is only testing at the LogStorage
layer. The downstream plugins may also want special tests for UI-related stuff if we figure out how to get Descriptor check methods working, but that would also be separate. So IDK, maybe we can leave it like this for now and see as we refactor the other plugins whether this catches any bugs. If not, we can inline it.
|
||
@Extension | ||
@Symbol("teeLogStorageFactory") | ||
public static final class DescriptorImpl extends Descriptor<LogStorageFactory> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we might want a LogStorageFactoryDescriptor
type with a few special methods, like doCheckSecondaries(List<...> secondaries)
, doCheckPrimary(... primary)
, or whatever, so that implementations can reject unsupported configurations, e.g. trying to use the CPE factory and FileLogStorageFactory
at the same time is no good because they both use log
, or trying to use the CPE factory as a secondary is pointless since CPE wouldn't actually work, etc.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking into..
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please see dd6e930.
The new Descriptor doesn't provide specific methods yet because I need to find a way to have some server-side validation with <f:dropdownDescriptorSelector>
. The default doCheckXyz
doesn't work, see dropdownDescriptorSelector.
Tried:
- with an additional boolean
enabled
and adoCheckEnabled(@QueryParam boolean enabled, @QueryParam String primary)
butprimary
is alwaysnull
- by overriding
configure(StaplerRequest2 req, JSONObject json)
inTeeLogStorage.DescriptorImpl
but it's not called - adding some custom javascript, but
<f:dropdownDescriptorSelector>
doesn't acceptonchange
event - replacing
<f:dropdownDescriptorSelector>
with a<f:select>
, but each item value is aString
and therefore doesn't properly bindJSON, even when trying to set item values with a string representation of{"$class": "a log storage class name"}
src/main/java/org/jenkinsci/plugins/workflow/log/LogStorage.java
Outdated
Show resolved
Hide resolved
src/main/java/org/jenkinsci/plugins/workflow/log/tee/TeeBuildListener.java
Outdated
Show resolved
Hide resolved
src/main/java/org/jenkinsci/plugins/workflow/log/tee/TeeBuildListener.java
Outdated
Show resolved
Hide resolved
<f:dropdownDescriptorSelector | ||
field="primary" title="${%Primary}" | ||
descriptors="${descriptor.getFilteredDescriptors()}" | ||
default="${descriptor.getDefaultLogStorageFactoryDescriptor()}" | ||
/> | ||
<f:dropdownDescriptorSelector | ||
field="secondary" title="${%Secondary}" | ||
descriptors="${descriptor.getFilteredDescriptors()}" | ||
default="${descriptor.getDefaultLogStorageFactoryDescriptor()}" | ||
/> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't know your exact use case, but maybe repeatableHeteroProperty
would be a better fit (select multiple implementations and order them)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, this was used in a previous commit, but it seemed simpler to have only one primary and only one secondary at this point. But maybe additional checks are possible.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From recent discussions, there seem to be some factories which do not make sense as secondaries, so it would make sense to have two f:dropdownDescriptorSelector
s as here, but with different getFilteredDescriptors
calls.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please see 9090904
269b2ee
to
bd88176
Compare
src/main/java/org/jenkinsci/plugins/workflow/log/tee/TeeOutputStream.java
Outdated
Show resolved
Hide resolved
} | ||
|
||
@Override | ||
public void close() throws Exception { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The 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 TaskListener
s, but we don't actually close the TeeOutputStream
or the PrintStream
returned by getLogger()
. At first I thought maybe we could simplify by just calling getLogger().close()
, which would eventually close the TeeOutputStream
and we could get rid of the rest of the method, but at least BufferedBuildListener
and CloudWatchSender
have special close
methods that do more than just closing the internal streams, so we really do need to try to close the TaskListener
s themselves.
This means that after a call to close()
, getLogger()
and outputStream
will still exist as unclosed streams even after their delegates have been been closed, which is a bit weird. We might be able to call getLogger().close()
and then close the listeners, but I can't remember if any of the relevant streams have problems with being closed twice. I would maybe look into this a bit just to make sure whether the TeeOutputStream
not being closed is ok.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I update with something like:
Exception exception = null;
if (outputStream != null) {
try {
outputStream.close();
} catch (IOException e) {
exception = e;
}
}
then TeeLogStorageTest#smokes
fails with:
org.opentest4j.MultipleFailuresError: Multiple Failures (2 failures)
org.junit.ComparisonFailure: expected:<[<a href='http://nowhere.net/'>nikde</a>
]> but was:<[]>
java.lang.AssertionError:
Expected: is "starting\none #1\ntwo #1\ntwo #2\ninterrupting\none #2\none #3\npausing\nresuming\none #4\nthree #1\nending\n"
but: was "starting\none #1\ntwo #1\ntwo #2\ninterrupting\none #2\none #3\npausing\nresuming\none #4\nthree #1\nending\nha:////4AM+Xbq6l0DXt+Aa5ectJ4m2Ny0f1G1cNAXESjnHxoh7AAAAlB+LCAAAAAAAAP9b85aBtbiIQSajNKU4P08vOT+vOD8nVc+jsiC1KCczL9svvyTVzHb1RttJBUeZGJg8GdhyUvPSSzJ8GJhLi3JKGIR8shLLEvVzEvPS9YNLijLz0q0rihik0IxzhtAgwxgggJGJgaGiAMhgLWEQzigpKbDS18/LL89ILUrVy0st0QcAFd2f8JgAAAA=nikde\n"
...
Suppressed: org.junit.ComparisonFailure: expected:<[<a href='http://nowhere.net/'>nikde</a>
]> but was:<[]>
at org.jenkinsci.plugins.workflow.log.LogStorageTestBase.assertLog(LogStorageTestBase.java:324)
at org.jenkinsci.plugins.workflow.log.LogStorageTestBase.assertStepLog(LogStorageTestBase.java:305)
at org.jenkinsci.plugins.workflow.log.LogStorageTestBase.smokes(LogStorageTestBase.java:155)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you specifically need to use getLogger().close()
, not outputStream.close
, maybe so that the PrintStream
flushes its buffer and the stream, but I am not exactly sure why the former works but the latter does not without investigating more deeply. If things are ok without closing the PrintStream
or TeeOutputStream
itself that might be fine too, I would just check to make sure everything is getting GC'd as expected. Maybe there could be a difference in behavior if you tested writing lines without newlines or something like that, but I do not know.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The 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 comment
The reason will be displayed to describe this comment to others. Learn more.
But now with the additional getLogger().close()
, the logger is flushed and closed, making TeeOutputStreamTest#primary_fails_close
fail, and the close() method is called twice:
Breakpoint reached
at org.jenkinsci.plugins.workflow.log.tee.TeeOutputStreamTest$4.close(TeeOutputStreamTest.java:118)
at org.jenkinsci.plugins.workflow.log.tee.RemoteCustomFileLogStorage$Writer.close(RemoteCustomFileLogStorage.java:138)
at org.jenkinsci.plugins.workflow.log.tee.TeeOutputStream$$Lambda/0x000000d80168a838.apply(Unknown Source:-1)
at org.jenkinsci.plugins.workflow.log.tee.TeeOutputStream.handleAction(TeeOutputStream.java:45)
at org.jenkinsci.plugins.workflow.log.tee.TeeOutputStream.close(TeeOutputStream.java:34)
at java.io.PrintStream.implClose(PrintStream.java:500)
at java.io.PrintStream.close(PrintStream.java:484)
at org.jenkinsci.plugins.workflow.log.tee.TeeBuildListener.close(TeeBuildListener.java:51)
...
Breakpoint reached
at org.jenkinsci.plugins.workflow.log.tee.TeeOutputStreamTest$4.close(TeeOutputStreamTest.java:118)
at org.jenkinsci.plugins.workflow.log.tee.RemoteCustomFileLogStorage$Writer.close(RemoteCustomFileLogStorage.java:138)
at org.jenkinsci.plugins.workflow.log.tee.RemoteCustomFileLogStorage$MyListener.close(RemoteCustomFileLogStorage.java:99)
at org.jenkinsci.plugins.workflow.log.tee.TeeBuildListener.close(TeeBuildListener.java:55)
at org.jenkinsci.plugins.workflow.log.tee.TeeOutputStreamTest.primary_fails_close(TeeOutputStreamTest.java:123)
at java.lang.invoke.LambdaForm$DMH/0x000000d801218c00.invokeVirtual(LambdaForm$DMH:-1)
at java.lang.invoke.LambdaForm$MH/0x000000d801324000.invoke(LambdaForm$MH:-1)
at java.lang.invoke.Invokers$Holder.invokeExact_MT(Invokers$Holder:-1)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The 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 PrintStream
, not just the OutputStream
, and yes it means the stream will be closed twice as I mentioned in #417 (comment). It's just not clear to me whether it is preferable to leave the outer streams open and just let them be cleaned up by GC after the delegate TaskListener
s are closed, or to close the PrintStream
, causing the TeeOutputStreamTest
stream to be closed twice. I will try to investigate in more detail later today.
src/main/java/org/jenkinsci/plugins/workflow/log/LogStorageFactory.java
Outdated
Show resolved
Hide resolved
src/main/java/org/jenkinsci/plugins/workflow/log/tee/TeeLogStorageFactory.java
Show resolved
Hide resolved
...jenkinsci/plugins/workflow/log/configuration/PipelineLoggingGlobalConfiguration/config.jelly
Show resolved
Hide resolved
2b775e6
to
da22a49
Compare
src/test/java/org/jenkinsci/plugins/workflow/log/tee/TeeOutputStreamTest.java
Outdated
Show resolved
Hide resolved
src/test/java/org/jenkinsci/plugins/workflow/log/tee/RemoteCustomFileLogStorage.java
Outdated
Show resolved
Hide resolved
<f:dropdownDescriptorSelector | ||
field="factory" title="${%Logging Factory}" | ||
descriptors="${descriptor.getLogStorageFactoryDescriptors()}" | ||
default="${descriptor.getDefaultFactoryDescriptor()}" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jgreffe This is a bit notable because it means that the first time you install this update and save the "Configure System" page, the current default factory will be selected permanently, and so it will show up in things like CasC exports. It also means that even if you didn't specifically pick the setting, it will no longer be possible for a newly installed plugin to take over as the default once this has been saved once. You'll have to manually reconfigure things.
Is it possible to instead keep this as null
in that case? That might be preferable, so that someone who is using FileLogStorage
and installs a plugin with a custom LogStorageFactory
has the new factory take effect automatically instead of having to manually reconfigure things. That said, the way you have it is probably how we would make it work if we were doing things today from scratch, so IDK. What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added an empty/null entry so users can still use the default behavior and avoid any configuration change, see 6d543ad
Monosnap.screencast.2025-07-30.10-43-52.mp4
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about using Jenkins.get().getPluginManager().whichPlugin(LogStorageFactory.getDefaultFactory().getClass()
in order to display the plugin of the default factory?
Would give something like:


wdyt @dwnusbaum ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we go this route, could you check the label related to the plugin info @kellie-freeman? Please set it directly in this thread: https://github.com/jenkinsci/workflow-api-plugin/pull/417/files#r2244648496 🙏🏼
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, that seems fine. In general I think we'll need to go through all of this with Kellie to refine the user-facing text and symbols. For example IDK if we really want to refer to these as "factories" and "tee" might not be the clearest way to describe that mode for users.
@@ -0,0 +1 @@ | |||
description=Define the logging factory to use for pipeline logging. Without any explicit configuration, the default logging factory will be "{0}". |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please check the label here @kellie-freeman 🙏🏼
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As mentioned above, "logging factory" is a bit confusing to me. If this isn't an established term that customers will immediately recognize/understand, recommend updating slightly.
description=Define the logging factory to use for pipeline logging. Without any explicit configuration, the default logging factory will be "{0}". | |
description=Specify the Pipeline log storage location. The default location is "{0}". |
or maybe
description=Define the logging factory to use for pipeline logging. Without any explicit configuration, the default logging factory will be "{0}". | |
description=Specify the Pipeline logger. The default logger is "{0}". |
@@ -0,0 +1 @@ | |||
description=Controls the Tee Log Storage factories order. Primary allows read and writes. Secondary allows writes only. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please check the label here @kellie-freeman 🙏🏼
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- What is Tee Log Storage referring to? Based on a quick internet search, I’m guessing this? https://linuxcommandlibrary.com/man/tee
- Why are we using the term “factory/factories” in this context? Is this common terminology for logging methods or maybe this? https://refactoring.guru/design-patterns/factory-method
It seems confusing to me. It looks like Jenkins may use the term "logger" instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
tee
is a Unix shell utility that copies its input to its output while also copying the input to specified files. In our case we use the name tee
just because the functionality is similar (we let you pick two log handlers and write all of the content to both). I think tee
is a fine name for the code internally, but for the user facing text I think we should probably avoid it (maybe the help text could say something like "similar to the tee
Unix utility").
As far as "factory/factories", Jenkins has an extensible system for reading and writing Pipeline build logs. In the code, each of these implementations is a "LogStorageFactory". The default implementation writes to a file in JENKINS_HOME. CPE has an implementation that also writes to a file but adds special metadata. opentelemetry
and pipeline-cloudwatch-logs
have implementations that send logs to OTel and Amazon CloudWatch Logs, respectively. This new "TeeLogStorageFactory" allows you to pick 2 of these, the first of which will handle all log reads and writes, and the second of which will just get a copy of all writes. I am not sure how best to name these in the UI. Maybe "Log handlers" would be clearer?
"Tee Log Storage Factory" could maybe be something like "Multiple log handlers" or "Primary log handler with writes copied to second handler", idk. I'll think about it as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added some suggestions for the strings, but probably didn't catch all instances. Overall, I think the use of "Tee" and "factories" is a bit confusing, so I added some possible alternatives. I'll be OOTO until August 11th, so I added several options in most cases, depending on the route we may take.
@@ -0,0 +1 @@ | |||
description=Controls the Tee Log Storage factories order. Primary allows read and writes. Secondary allows writes only. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- What is Tee Log Storage referring to? Based on a quick internet search, I’m guessing this? https://linuxcommandlibrary.com/man/tee
- Why are we using the term “factory/factories” in this context? Is this common terminology for logging methods or maybe this? https://refactoring.guru/design-patterns/factory-method
It seems confusing to me. It looks like Jenkins may use the term "logger" instead?
@@ -0,0 +1 @@ | |||
description=Controls the Tee Log Storage factories order. Primary allows read and writes. Secondary allows writes only. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
description=Controls the Tee Log Storage factories order. Primary allows read and writes. Secondary allows writes only. | |
description=Controls the log storage location order; <b>Primary</b> allows read and writes; <b>Secondary</b> allows writes only. |
@@ -0,0 +1,12 @@ | |||
<?jelly escape-by-default='true'?> | |||
<j:jelly xmlns:j="jelly:core" xmlns:f="/lib/form"> | |||
<f:section title="${%Pipeline Logging}"> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If this isn't the correct location for this suggestion, then please disregard!
<f:section title="${%Pipeline Logging}"> | |
<f:section title="${%Pipeline logging}"> |
${%description(descriptor.getDefaultFactoryDescriptor().getDisplayName())} | ||
</f:block> | ||
<f:dropdownDescriptorSelector | ||
field="factory" title="${%Logging Factory}" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If this isn't the correct location for this suggestion, then please disregard!
field="factory" title="${%Logging Factory}" | |
field="factory" title="${%Log storage location}" |
or
field="factory" title="${%Logging Factory}" | |
field="factory" title="${%Log storage}" |
or
field="factory" title="${%Logging Factory}" | |
field="factory" title="${%Logger}" |
@@ -0,0 +1 @@ | |||
description=Define the logging factory to use for pipeline logging. Without any explicit configuration, the default logging factory will be "{0}". |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As mentioned above, "logging factory" is a bit confusing to me. If this isn't an established term that customers will immediately recognize/understand, recommend updating slightly.
description=Define the logging factory to use for pipeline logging. Without any explicit configuration, the default logging factory will be "{0}". | |
description=Specify the Pipeline log storage location. The default location is "{0}". |
or maybe
description=Define the logging factory to use for pipeline logging. Without any explicit configuration, the default logging factory will be "{0}". | |
description=Specify the Pipeline logger. The default logger is "{0}". |
@NonNull | ||
@Override | ||
public String getDisplayName() { | ||
return "Remote custom file log storage factory"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return "Remote custom file log storage factory"; | |
return "Remote custom log file storage"; |
@@ -0,0 +1,16 @@ | |||
<?jelly escape-by-default='true'?> | |||
<j:jelly xmlns:j="jelly:core" xmlns:f="/lib/form"> | |||
<f:section title="${%Tee Log Storage Factory}"> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think "Tee" and "Factory" are confusing. Maybe:
<f:section title="${%Tee Log Storage Factory}"> | |
<f:section title="${%Log storage location}"> |
or
<f:section title="${%Tee Log Storage Factory}"> | |
<f:section title="${%Pipeline log storage location}"> |
or
<f:section title="${%Tee Log Storage Factory}"> | |
<f:section title="${%Pipeline log storage}"> |
form.getFirstByXPath("//*[@id='pipeline-logging']/../descendant::select/option[@selected]/text()"); | ||
assertThat(selectedText, nullValue()); | ||
var description = form.getFirstByXPath("//*[@id='pipeline-logging']/../descendant::div[@colspan]/text()"); | ||
assertThat(description.toString(), containsString("My Custom Log")); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This suggestion applies to all instances of this string.
assertThat(description.toString(), containsString("My Custom Log")); | |
assertThat(description.toString(), containsString("My custom log")); |
This is a follow-up of #406 (comment).
This PR aims to allow defining multiple log storages, one primary for all reads/writes and secondaries for writes only.
This changes the contract of
LogStorageFactory
interface by:ExtensionPoint
Describable<LogStorageFactory>
, which allows to configure the log storages through a dropdown. When TeeLogStorageFactory is selected, one can choose mandatory primary and secondary through dropdowns.Monosnap.screencast.2025-07-18.15-01-23.mp4
Downstream changes will be listed here when available.
Testing done
Submitter checklist