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

[JENKINS-27394] Collapsible sections in log #21

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -61,6 +61,7 @@
import java.io.PrintStream;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashMap;
Expand Down Expand Up @@ -94,8 +95,11 @@
import org.jenkinsci.plugins.workflow.flow.GraphListener;
import org.jenkinsci.plugins.workflow.flow.StashManager;
import org.jenkinsci.plugins.workflow.graph.BlockEndNode;
import org.jenkinsci.plugins.workflow.graph.BlockStartNode;
import org.jenkinsci.plugins.workflow.graph.FlowEndNode;
import org.jenkinsci.plugins.workflow.graph.FlowNode;
import org.jenkinsci.plugins.workflow.job.console.NestingNote;
import org.jenkinsci.plugins.workflow.job.console.ShowHideNote;
import org.jenkinsci.plugins.workflow.job.console.WorkflowConsoleLogger;
import org.jenkinsci.plugins.workflow.steps.FlowInterruptedException;
import org.jenkinsci.plugins.workflow.steps.StepContext;
Expand Down Expand Up @@ -370,14 +374,17 @@ private void copyLogs() {
AnnotatedLargeText<? extends FlowNode> logText = la.getLogText();
try {
long old = entry.getValue();
OutputStream logger;

String prefix = getLogPrefix(node);
List<String> nesting = getNesting(node);
String encodedNesting = new NestingNote(nesting).encode();
String linePrefix;
if (prefix != null) {
logger = new LogLinePrefixOutputFilter(listener.getLogger(), "[" + prefix + "] ");
linePrefix = encodedNesting + "[" + prefix + "] ";
} else {
logger = listener.getLogger();
linePrefix = encodedNesting;
}
OutputStream logger = new LogLinePrefixOutputFilter(listener.getLogger(), linePrefix);

try {
long revised = writeRawLogTo(logText, old, logger);
Expand Down Expand Up @@ -454,6 +461,37 @@ private long writeRawLogTo(AnnotatedLargeText<?> text, long start, OutputStream
}
}

@GuardedBy("completed")
private transient LoadingCache<FlowNode,List<String>> nestingCache;
private @Nonnull List<String> getNesting(FlowNode node) {
// TODO could also use FlowScanningUtils.fetchEnclosingBlocks(node) but this would not let us cache intermediate results
synchronized (completed) {
if (nestingCache == null) {
nestingCache = CacheBuilder.newBuilder().weakKeys().build(new CacheLoader<FlowNode,List<String>>() {
@Override public @Nonnull List<String> load(FlowNode node) {
if (node instanceof BlockEndNode) {
return getNesting(((BlockEndNode) node).getStartNode());
} else {
List<FlowNode> parents = node.getParents();
if (parents.isEmpty()) { // FlowStartNode
return Collections.emptyList();
}
List<String> parent = getNesting(parents.get(0)); // multiple parents is only for BlockEndNode after parallel
if (node instanceof BlockStartNode) {
Copy link
Member Author

Choose a reason for hiding this comment

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

🐛 produces incorrect results for

stage('one') {}
stage('two') // should return singleton list; instead returns two stage nodes
{}

Copy link
Member

Choose a reason for hiding this comment

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

@jglick Consider using https://github.com/jenkinsci/workflow-api-plugin/blob/master/src/main/java/org/jenkinsci/plugins/workflow/graphanalysis/SimpleChunkVisitor.java to collect ForkScanner results - you can easily roll up the result with a simple ArrayDequeue of blocks.

Copy link
Member Author

Choose a reason for hiding this comment

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

For this case I want incremental caching, which AFAICT is not yet supported.

List<String> appended = new ArrayList<>(parent);
appended.add(node.getId());
return appended;
} else { // AtomNode
return parent;
}
}
}
});
}
return nestingCache.getUnchecked(node);
}
}

private static final class LogLinePrefixOutputFilter extends LineTransformationOutputStream {

private final PrintStream logger;
Expand Down Expand Up @@ -833,12 +871,26 @@ private final class GraphL implements GraphListener {
}

private void logNodeMessage(FlowNode node) {
List<String> nesting = getNesting(node);
if (!nesting.isEmpty() && nesting.get(nesting.size() - 1).equals(node.getId())) {
// For a BlockStartNode, we do not want to hide itself.
nesting = new ArrayList<>(nesting.subList(0, nesting.size() - 1));
}
try {
listener.annotate(new NestingNote(nesting));
} catch (IOException x) {
LOGGER.log(Level.WARNING, null, x);
}
WorkflowConsoleLogger wfLogger = new WorkflowConsoleLogger(listener);
String prefix = getLogPrefix(node);
String text = node.getDisplayFunctionName();
if (node instanceof BlockStartNode) {
Copy link
Member Author

Choose a reason for hiding this comment

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

Would be useful to be able to collapse AtomNode output as well: for example, long sh steps, or build if JENKINS-26124 is implemented.

text += " (" + ShowHideNote.create(node.getId()) + ")";
}
if (prefix != null) {
wfLogger.log(String.format("[%s] %s", prefix, node.getDisplayFunctionName()));
wfLogger.log(String.format("[%s] %s", prefix, text));
} else {
wfLogger.log(node.getDisplayFunctionName());
wfLogger.log(text);
}
// Flushing to keep logs printed in order as much as possible. The copyLogs method uses
// LargeText and possibly LogLinePrefixOutputFilter. Both of these buffer and flush, causing strange
Expand Down
@@ -0,0 +1,60 @@
/*
* The MIT License
*
* Copyright 2016 CloudBees, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/

package org.jenkinsci.plugins.workflow.job.console;

import hudson.MarkupText;
import hudson.console.ConsoleAnnotator;
import hudson.console.ConsoleNote;
import hudson.model.Run;
import java.util.List;

/**
* Encodes the block-scoped nesting of a step.
*/
public class NestingNote extends ConsoleNote<Run<?,?>> {
Copy link
Member Author

Choose a reason for hiding this comment

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

🐛 needs to be Object to avoid breaking per-step LogAction display


private static final long serialVersionUID = 1L;

private final List<String> nesting;

public NestingNote(List<String> nesting) {
this.nesting = nesting;
}

@SuppressWarnings("rawtypes")
@Override public ConsoleAnnotator annotate(Run<?,?> context, MarkupText text, int charPos) {
StringBuilder b = new StringBuilder("<span class=\"");
for (int i = 0; i < nesting.size(); i++) {
if (i > 0) {
b.append(' ');
}
b.append("pipeline-sect-").append(nesting.get(i));
}
b.append("\">");
text.addMarkup(0, text.length(), b.toString(), "</span>");
return null;
}

}
@@ -0,0 +1,66 @@
/*
* The MIT License
*
* Copyright 2016 CloudBees, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/

package org.jenkinsci.plugins.workflow.job.console;

import hudson.Extension;
import hudson.console.ConsoleAnnotationDescriptor;
import hudson.console.HyperlinkNote;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
* Shows or hides a block by nesting.
*/
public class ShowHideNote extends HyperlinkNote {

private static final Logger LOGGER = Logger.getLogger(ShowHideNote.class.getName());
private static final long serialVersionUID = 1L;

public static String create(String id) {
String text = "hide";
Copy link
Member Author

Choose a reason for hiding this comment

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

Perhaps hide all but the first branch of parallel by default?

try {
return new ShowHideNote(id, text.length()).encode() + text;
} catch (IOException e) {
// impossible, but don't make this a fatal problem
LOGGER.log(Level.WARNING, "Failed to serialize " + ShowHideNote.class, e);
return text;
}
}

private final String id;

private ShowHideNote(String id, int length) {
super("#", length);
Copy link
Member Author

Choose a reason for hiding this comment

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

Might as well use "#" + id here and add an a tag in logNodeMessage, allowing us to create direct links to the section—useful for getLogURL in jenkinsci/workflow-basic-steps-plugin#2, for example.

this.id = id;
}

@Override protected String extraAttributes() {
return " show-hide-id=\"" + id + "\" onclick=\"showHidePipelineSection(this); return false\"";
}

@Extension public static class DescriptorImpl extends ConsoleAnnotationDescriptor {}

}
@@ -0,0 +1,45 @@
/*
* The MIT License
*
* Copyright 2016 CloudBees, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/

function showHidePipelineSection(link) {
var id = link.getAttribute('show-hide-id')
var display
if (link.textContent === 'hide') {
display = 'none'
link.textContent = 'show'
} else {
display = 'inline'
link.textContent = 'hide'
}
var sect = '.pipeline-sect-' + id
var ss = document.styleSheets[0]
for (var i = 0; i < ss.rules.length; i++) {
if (ss.rules[i].selectorText === sect) {
ss.rules[i].style.display = display
return
}
}
// TODO order rules, so that hiding and reshowing a high-level section will restore expansion of a lower-level section
ss.insertRule(sect + ' {display: ' + display + '}', ss.rules.length)
}