Skip to content

Commit aef0621

Browse files
jglickDohbedoh
andauthored
Overhaul of SlaveLaunchLogs (#517)
* agent-logs sketch * `SlaveLaunchLogsTest.onlineInboundAgent` * `SlaveLaunchLogsTest.offlineAgent` * Javadoc * Exploring `SlaveLaunchLogs` behavior * More `SlaveLaunchLogsTest` * `SlaveLaunchLogsTest.passwords` * Worked out a better `SlaveLaunchLogs`, but depends on patch to `SlaveComputer` * Need to flush logs also for `deletedAgent` * Reverting changes extracted to #518 * Setting a timestamp, switching category * RC * No need to assert that `SupportTestUtils.invokeComponentToString` is non-null * `Security2186Test` failure caused by renamed bundle entry * File handle leak caught by Windows tests * Better handling of rotated logs * More robust way to wait for `Connection terminated` message * `SlaveLaunchLogsTest.offlineAgent` still flaky on Windows * jenkinsci/jenkins#9009 released * Working around lack of JENKINS-72799 to avoid requiring a weekly core * SpotBugs --------- Co-authored-by: Allan Burdajewicz <aburdajewicz@cloudbees.com>
1 parent 579a3fa commit aef0621

File tree

6 files changed

+354
-108
lines changed

6 files changed

+354
-108
lines changed

pom.xml

+6
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,12 @@
153153
</exclusion>
154154
</exclusions>
155155
</dependency>
156+
<dependency>
157+
<groupId>org.awaitility</groupId>
158+
<artifactId>awaitility</artifactId>
159+
<version>4.2.0</version>
160+
<scope>test</scope>
161+
</dependency>
156162
<!-- MockFolder that was used for SupportAbstractItem test does not have UI, hence need full Folder implementation.-->
157163
<dependency>
158164
<groupId>org.jenkins-ci.plugins</groupId>

src/main/java/com/cloudbees/jenkins/support/api/LaunchLogsFileContent.java

+4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.cloudbees.jenkins.support.api;
22

33
import com.cloudbees.jenkins.support.filter.PasswordRedactor;
4+
import com.cloudbees.jenkins.support.impl.SlaveLaunchLogs;
45
import java.io.ByteArrayInputStream;
56
import java.io.File;
67
import java.io.FileInputStream;
@@ -11,6 +12,9 @@
1112
import java.util.stream.Collectors;
1213
import org.apache.commons.io.IOUtils;
1314

15+
/**
16+
* @see SlaveLaunchLogs
17+
*/
1418
public class LaunchLogsFileContent extends FileContent {
1519

1620
public LaunchLogsFileContent(String name, String[] filterableParameters, File file, long maxSize) {

src/main/java/com/cloudbees/jenkins/support/impl/SlaveLaunchLogs.java

+137-91
Original file line numberDiff line numberDiff line change
@@ -26,25 +26,45 @@
2626

2727
import static com.cloudbees.jenkins.support.impl.JenkinsLogs.ROTATED_LOGFILE_FILTER;
2828

29+
import com.cloudbees.jenkins.support.SupportPlugin;
2930
import com.cloudbees.jenkins.support.api.Container;
3031
import com.cloudbees.jenkins.support.api.LaunchLogsFileContent;
3132
import com.cloudbees.jenkins.support.api.ObjectComponent;
3233
import com.cloudbees.jenkins.support.api.ObjectComponentDescriptor;
3334
import com.cloudbees.jenkins.support.timer.FileListCapComponent;
3435
import edu.umd.cs.findbugs.annotations.NonNull;
3536
import hudson.Extension;
37+
import hudson.ExtensionList;
38+
import hudson.FilePath;
39+
import hudson.console.ConsoleLogFilter;
40+
import hudson.console.LineTransformationOutputStream;
41+
import hudson.init.Terminator;
3642
import hudson.model.AbstractModelObject;
3743
import hudson.model.Computer;
38-
import hudson.model.Node;
44+
import hudson.model.Run;
45+
import hudson.model.TaskListener;
46+
import hudson.remoting.Channel;
3947
import hudson.security.Permission;
40-
import hudson.slaves.Cloud;
48+
import hudson.slaves.ComputerListener;
49+
import hudson.slaves.JNLPLauncher;
50+
import hudson.slaves.SlaveComputer;
51+
import hudson.util.io.RewindableRotatingFileOutputStream;
4152
import java.io.File;
42-
import java.util.ArrayList;
53+
import java.io.IOException;
54+
import java.io.OutputStream;
55+
import java.nio.charset.StandardCharsets;
56+
import java.time.Instant;
57+
import java.time.format.DateTimeFormatter;
58+
import java.time.temporal.ChronoUnit;
4359
import java.util.Collections;
44-
import java.util.List;
60+
import java.util.Map;
4561
import java.util.Set;
62+
import java.util.concurrent.ConcurrentHashMap;
4663
import java.util.concurrent.TimeUnit;
64+
import java.util.logging.Level;
65+
import java.util.logging.Logger;
4766
import jenkins.model.Jenkins;
67+
import org.apache.commons.io.output.TeeOutputStream;
4868
import org.jenkinsci.Symbol;
4969
import org.kohsuke.stapler.DataBoundConstructor;
5070

@@ -54,6 +74,11 @@
5474
@Extension
5575
public class SlaveLaunchLogs extends ObjectComponent<Computer> {
5676

77+
private static final int MAX_ROTATE_LOGS =
78+
Integer.getInteger(SlaveLaunchLogs.class.getName() + ".MAX_ROTATE_LOGS", 9);
79+
80+
private static final Logger LOGGER = Logger.getLogger(SlaveLaunchLogs.class.getName());
81+
5782
@DataBoundConstructor
5883
public SlaveLaunchLogs() {
5984
super();
@@ -73,128 +98,149 @@ public String getDisplayName() {
7398

7499
@Override
75100
public void addContents(@NonNull Container container) {
76-
addAgentsLaunchLogs(container);
101+
ExtensionList.lookupSingleton(LogArchiver.class).addContents(container);
77102
}
78103

79104
@NonNull
80105
@Override
81106
public ComponentCategory getCategory() {
82-
return ComponentCategory.AGENT; // TODO or LOGS?
107+
return ComponentCategory.LOGS;
83108
}
84109

85110
@Override
86111
public void addContents(@NonNull Container container, Computer item) {
87-
if (item.getNode() == null) {
88-
return;
89-
}
90-
File lastLog = item.getLogFile();
91-
if (lastLog.exists()) {
92-
Agent agent = new Agent(new File(Jenkins.get().getRootDir(), "logs/slaves/" + item.getName()), lastLog);
93-
if (!agent.isTooOld()) {
94-
addAgentLaunchLogs(container, agent);
112+
if (item.getNode() != null
113+
&& item.getLogFile().lastModified() >= System.currentTimeMillis() - TimeUnit.DAYS.toMillis(7)) {
114+
File dir = new File(Jenkins.get().getRootDir(), "logs/slaves/" + item.getName());
115+
File[] files = dir.listFiles(ROTATED_LOGFILE_FILTER);
116+
if (files != null) {
117+
for (File f : files) {
118+
container.add(new LaunchLogsFileContent(
119+
"nodes/slave/{0}/launchLogs/{1}",
120+
new String[] {dir.getName(), f.getName()}, f, FileListCapComponent.MAX_FILE_SIZE));
121+
}
95122
}
96123
}
97124
}
98125

99-
/**
100-
* <p>
101-
* In the presence of {@link Cloud} plugins like EC2, we want to find past agents, not just current ones.
102-
* So we don't try to loop through {@link Node} here but just try to look at the file systems to find them
103-
* all.
104-
*
105-
* <p>
106-
* Generally these cloud plugins do not clean up old logs, so if run for a long time, the log directory
107-
* will be full of old files that are not very interesting. Use some heuristics to cut off logs
108-
* that are old.
109-
*/
110-
private void addAgentsLaunchLogs(Container result) {
111-
112-
List<Agent> all = new ArrayList<>();
113-
114-
{ // find all the agent launch log files and sort them newer ones first
115-
File agentLogsDir = new File(Jenkins.get().getRootDir(), "logs/slaves");
116-
File[] logs = agentLogsDir.listFiles();
117-
if (logs != null) {
118-
for (File dir : logs) {
119-
File lastLog = new File(dir, "slave.log");
120-
if (lastLog.exists()) {
121-
Agent s = new Agent(dir, lastLog);
122-
if (s.isTooOld()) continue; // we don't care
123-
all.add(s);
124-
}
125-
}
126-
}
126+
@Extension
127+
public static final class LogArchiver extends ConsoleLogFilter {
127128

128-
Collections.sort(all);
129-
}
130-
{ // this might be still too many, so try to cap them.
131-
int acceptableSize = Math.max(256, Jenkins.get().getNodes().size() * 5);
129+
private final File logDir;
130+
private final RewindableRotatingFileOutputStream stream;
132131

133-
if (all.size() > acceptableSize) all = all.subList(0, acceptableSize);
132+
public LogArchiver() throws IOException {
133+
logDir = new File(SupportPlugin.getLogsDirectory(), "agent-launches");
134+
stream = new RewindableRotatingFileOutputStream(new File(logDir, "all.log"), MAX_ROTATE_LOGS);
135+
stream.rewind();
134136
}
135137

136-
// now add them all
137-
all.forEach(it -> addAgentLaunchLogs(result, it));
138-
}
139-
140-
private void addAgentLaunchLogs(Container container, Agent agent) {
141-
File[] files = agent.dir.listFiles(ROTATED_LOGFILE_FILTER);
142-
if (files != null) {
143-
for (File f : files) {
144-
container.add(new LaunchLogsFileContent(
145-
"nodes/slave/{0}/launchLogs/{1}",
146-
new String[] {agent.getName(), f.getName()}, f, FileListCapComponent.MAX_FILE_SIZE));
138+
@Override
139+
public OutputStream decorateLogger(Computer computer, OutputStream logger) {
140+
if (computer instanceof SlaveComputer) {
141+
return new TeeOutputStream(logger, new PrefixedStream(stream, computer.getName()));
142+
} else {
143+
return logger;
147144
}
148145
}
149-
}
150146

151-
static class Agent implements Comparable<Agent> {
152-
/**
153-
* Launch log directory of the agent: logs/slaves/NAME
154-
*/
155-
final File dir;
156-
157-
final long time;
147+
@SuppressWarnings("rawtypes")
148+
@Override
149+
public OutputStream decorateLogger(Run build, OutputStream logger) throws IOException, InterruptedException {
150+
return logger;
151+
}
158152

159-
Agent(File dir, File lastLog) {
160-
this.dir = dir;
161-
this.time = lastLog.lastModified();
153+
public void addContents(@NonNull Container container) {
154+
File[] files = logDir.listFiles(ROTATED_LOGFILE_FILTER);
155+
if (files != null) {
156+
for (File f : files) {
157+
container.add(new LaunchLogsFileContent(
158+
"nodes/slave/launches/" + f.getName(),
159+
new String[0],
160+
f,
161+
FileListCapComponent.MAX_FILE_SIZE));
162+
}
163+
}
162164
}
163165

164-
/** Agent name */
165-
String getName() {
166-
return dir.getName();
166+
@Terminator
167+
public static void close() {
168+
try {
169+
ExtensionList.lookupSingleton(LogArchiver.class).stream.close();
170+
} catch (IOException x) {
171+
LOGGER.log(Level.WARNING, null, x);
172+
}
167173
}
174+
}
175+
176+
// TODO delete if updating to 2.440.3 and JENKINS-72799 is backported, else 2.448+
177+
@Extension
178+
public static final class Jenkins72799Hack extends ComputerListener {
168179

169180
/**
170-
* Use the primary log file's timestamp to compare newer agents from older agents.
171-
*
172-
* sort in descending order; newer ones first.
181+
* Names of inbound agents which have recently gotten to {@link #preLaunch}
182+
* but for which we did not receive typical output in {@link SlaveComputer#setChannel(Channel, OutputStream, Channel.Listener)}
183+
* prior to {@link #preOnline}.
173184
*/
174-
public int compareTo(Agent that) {
175-
return Long.compare(that.time, this.time);
185+
private final Map<String, Boolean> launching = new ConcurrentHashMap<>();
186+
187+
@Override
188+
public void preLaunch(Computer c, TaskListener taskListener) {
189+
if (c instanceof SlaveComputer && ((SlaveComputer) c).getLauncher() instanceof JNLPLauncher) {
190+
String name = c.getName();
191+
LOGGER.fine(() -> "preLaunch " + name);
192+
launching.put(name, true);
193+
}
176194
}
177195

178196
@Override
179-
public boolean equals(Object o) {
180-
if (this == o) return true;
181-
if (o == null || getClass() != o.getClass()) return false;
197+
public void preOnline(Computer c, Channel channel, FilePath root, TaskListener listener) throws IOException {
198+
if (c instanceof SlaveComputer && ((SlaveComputer) c).getLauncher() instanceof JNLPLauncher) {
199+
String name = c.getName();
200+
if (launching.put(name, false)) {
201+
LOGGER.fine(() -> "preOnline " + name + " need to work around lack of JENKINS-72799");
202+
OutputStream stream = ExtensionList.lookupSingleton(LogArchiver.class).stream;
203+
synchronized (stream) {
204+
String nowish = DateTimeFormatter.ISO_INSTANT.format(
205+
Instant.now().truncatedTo(ChronoUnit.MILLIS));
206+
for (String line : c.getLog().trim().split("\n")) {
207+
LOGGER.fine(() -> "adding: " + line);
208+
stream.write(
209+
("[" + nowish + " " + name + "] " + line + "\n").getBytes(StandardCharsets.UTF_8));
210+
}
211+
}
212+
} else {
213+
LOGGER.fine(() -> "preOnline " + name + " OK, have JENKINS-72799");
214+
}
215+
}
216+
}
217+
}
182218

183-
Agent agent = (Agent) o;
219+
static class PrefixedStream extends LineTransformationOutputStream.Delegating {
220+
private final String name;
184221

185-
return time == agent.time;
222+
PrefixedStream(OutputStream out, String name) {
223+
super(out);
224+
this.name = name;
186225
}
187226

188227
@Override
189-
public int hashCode() {
190-
return (int) (time ^ (time >>> 32));
191-
}
192-
193-
/**
194-
* If the file is more than 7 days old, it is considered too old.
195-
*/
196-
public boolean isTooOld() {
197-
return time < System.currentTimeMillis() - TimeUnit.DAYS.toMillis(7);
228+
protected void eol(byte[] b, int len) throws IOException {
229+
if (new String(b, 0, len, StandardCharsets.UTF_8).startsWith("Remoting version: ")) {
230+
LOGGER.fine(() -> "receiving expected setChannel text on " + name);
231+
ExtensionList.lookupSingleton(Jenkins72799Hack.class).launching.put(name, false);
232+
}
233+
synchronized (out) {
234+
out.write('[');
235+
out.write(DateTimeFormatter.ISO_INSTANT
236+
.format(Instant.now().truncatedTo(ChronoUnit.MILLIS))
237+
.getBytes(StandardCharsets.US_ASCII));
238+
out.write(' ');
239+
out.write(name.getBytes(StandardCharsets.UTF_8));
240+
out.write(']');
241+
out.write(' ');
242+
out.write(b, 0, len);
243+
}
198244
}
199245
}
200246

src/test/java/com/cloudbees/jenkins/support/Security2186Test.java

+1-2
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,7 @@ public void secretsFilterWhenSystemPropertyContainsPasswordThenValueRedacted() t
7575
verifyFileIfContainsPassword(zip, "nodes/master/system.properties", "test2186.trustStorePassword");
7676
verifyFileIfContainsPassword(zip, "nodes/master/environment.txt", "test2186.trustStorePassword");
7777
verifyFileIfContainsPassword(zip, "nodes/slave/slave0/config.xml", "-Dtest2186.trustStoreAgentPassword");
78-
verifyFileIfContainsPassword(
79-
zip, "nodes/slave/slave0/launchLogs/slave.log", "-Dtest2186.trustStoreAgentPassword");
78+
verifyFileIfContainsPassword(zip, "nodes/slave/launches/all.log", "-Dtest2186.trustStoreAgentPassword");
8079
}
8180

8281
private void verifyFileIfContainsPassword(ZipFile zip, String fileName, String expectedPasswordKey)

0 commit comments

Comments
 (0)