Skip to content

Commit 1c940e1

Browse files
authored
Improve sauce connect logging (#315)
* Extract SC monitor class * Make it possible to customize ProcessOutputPrinter * Make it easier to replace SCMonitor implementation * Respect quietMode setting * Make it possible to customize logger * Print last healthcheck exception * Refactor openConnection * Change port to apiPort * Improve constructors of TunnelManager * Handle missing process streams properly * Update version to 2.4-SNAPSHOT * Improve error messages * A new instance of SCMonitor should be created for each sauceconnect instance * Pass logger to openConnection method * Log more information in SCMonitor * Provide a new approach for opening a connection * Mark SC monitor as failed on healthcheck timeout * Extract timeouts to const variables * Allow to pass logger instance to closeTunnelsForPlan * Log when making a request to a healthcheck endpoint
1 parent 6f59705 commit 1c940e1

12 files changed

+547
-279
lines changed

Diff for: pom.xml

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
<groupId>com.saucelabs</groupId>
77
<artifactId>ci-sauce</artifactId>
8-
<version>2.1-SNAPSHOT</version>
8+
<version>2.4-SNAPSHOT</version>
99
<packaging>jar</packaging>
1010

1111

Diff for: src/main/java/com/saucelabs/ci/sauceconnect/AbstractSauceTunnelManager.java

+166-244
Large diffs are not rendered by default.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package com.saucelabs.ci.sauceconnect;
2+
3+
4+
import java.io.*;
5+
6+
7+
public class DefaultProcessOutputPrinter implements ProcessOutputPrinter {
8+
public Runnable getStdoutPrinter(InputStream stdout, PrintStream printStream) {
9+
return () -> {
10+
if (stdout == null || printStream == null) {
11+
return;
12+
}
13+
14+
try (BufferedReader reader = new BufferedReader(new InputStreamReader(stdout))) {
15+
String line;
16+
while ((line = reader.readLine()) != null) {
17+
print(printStream, "[sauceconnect] [stdout] " + line);
18+
}
19+
} catch (IOException e) {
20+
//
21+
}
22+
};
23+
}
24+
25+
public Runnable getStderrPrinter(InputStream stderr, PrintStream printStream) {
26+
return () -> {
27+
if (stderr == null || printStream == null) {
28+
return;
29+
}
30+
31+
try (BufferedReader reader = new BufferedReader(new InputStreamReader(stderr))) {
32+
String line;
33+
while ((line = reader.readLine()) != null) {
34+
print(printStream, "[sauceconnect] [stderr] " + line);
35+
}
36+
} catch (IOException e) {
37+
//
38+
}
39+
};
40+
}
41+
42+
private void print(PrintStream printStream, String line) {
43+
if (printStream != null) {
44+
printStream.println(line);
45+
}
46+
}
47+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
package com.saucelabs.ci.sauceconnect;
2+
3+
import org.json.JSONObject;
4+
import org.slf4j.Logger;
5+
6+
import java.net.URI;
7+
import java.net.http.HttpClient;
8+
import java.net.http.HttpRequest;
9+
import java.net.http.HttpResponse;
10+
import java.util.concurrent.Semaphore;
11+
12+
/** Monitors SC Process via HTTP API */
13+
public class DefaultSCMonitor implements SCMonitor {
14+
public static class Factory implements SCMonitorFactory {
15+
public SCMonitor create(int port, Logger logger) {
16+
return new DefaultSCMonitor(port, logger);
17+
}
18+
}
19+
20+
private Semaphore semaphore;
21+
private final int port;
22+
private final Logger logger;
23+
24+
private boolean failed;
25+
private boolean apiResponse;
26+
27+
private HttpClient client = HttpClient.newHttpClient();
28+
private static final int sleepTime = 1000;
29+
30+
private Exception lastHealtcheckException;
31+
32+
public DefaultSCMonitor(final int port, final Logger logger) {
33+
this.port = port;
34+
this.logger = logger;
35+
}
36+
37+
public void setSemaphore(Semaphore semaphore) {
38+
this.semaphore = semaphore;
39+
}
40+
41+
public String getTunnelId() {
42+
HttpRequest request = HttpRequest.newBuilder()
43+
.uri(URI.create(String.format("http://localhost:%d/info", port)))
44+
.GET()
45+
.build();
46+
47+
try {
48+
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
49+
String responseBody = response.body();
50+
JSONObject jsonObject = new JSONObject(responseBody);
51+
if (jsonObject.has("tunnel_id")) {
52+
return jsonObject.getString("tunnel_id");
53+
}
54+
} catch (Exception e) {
55+
this.logger.info("Failed to get tunnel id", e);
56+
return null;
57+
}
58+
this.logger.info("Failed to get tunnel id");
59+
return null;
60+
}
61+
62+
public boolean isFailed() {
63+
return failed;
64+
}
65+
66+
public void markAsFailed() {
67+
failed = true;
68+
}
69+
70+
public Exception getLastHealtcheckException() {
71+
return lastHealtcheckException;
72+
}
73+
74+
public void run() {
75+
while (this.semaphore.availablePermits() == 0 && !this.failed) {
76+
pollEndpoint();
77+
78+
try {
79+
Thread.sleep(sleepTime);
80+
} catch ( java.lang.InterruptedException e ) {
81+
return;
82+
}
83+
}
84+
}
85+
86+
protected void pollEndpoint() {
87+
URI uri = URI.create(String.format("http://localhost:%d/readyz", port));
88+
HttpRequest request = HttpRequest.newBuilder()
89+
.uri(uri)
90+
.GET()
91+
.build();
92+
93+
this.logger.trace("Polling health check endpoint uri={}", uri);
94+
95+
try {
96+
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
97+
this.apiResponse = true;
98+
99+
if (response.statusCode() == 200) {
100+
this.logger.info("Health check got connected status");
101+
this.semaphore.release();
102+
return;
103+
}
104+
105+
this.lastHealtcheckException = new Exception("Invalid API response code: " + response.statusCode());
106+
} catch ( Exception e ) {
107+
this.lastHealtcheckException = e;
108+
if ( this.apiResponse ) {
109+
// We've had a successful API endpoint read, but then it stopped responding, which means the process failed to start
110+
this.failed = true;
111+
this.logger.warn("Health check API stopped responding", e);
112+
this.semaphore.release();
113+
return;
114+
}
115+
}
116+
117+
this.logger.trace("Waiting for sauceconnect to be ready err={}", this.lastHealtcheckException.toString());
118+
}
119+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package com.saucelabs.ci.sauceconnect;
2+
3+
import org.slf4j.Marker;
4+
import org.slf4j.event.Level;
5+
import org.slf4j.helpers.MessageFormatter;
6+
7+
import java.io.PrintStream;
8+
9+
public class LoggerUsingPrintStream extends org.slf4j.helpers.AbstractLogger {
10+
PrintStream printStream;
11+
12+
public LoggerUsingPrintStream(PrintStream printStream) {
13+
this.printStream = printStream;
14+
}
15+
16+
@Override
17+
protected String getFullyQualifiedCallerName() {
18+
return null;
19+
}
20+
21+
@Override
22+
protected void handleNormalizedLoggingCall(Level level, Marker marker, String s, Object[] objects, Throwable throwable) {
23+
String msg = MessageFormatter.basicArrayFormat(s, objects);
24+
printStream.println("[" + level + "] " + msg);
25+
if (throwable != null) {
26+
throwable.printStackTrace(printStream);
27+
}
28+
}
29+
30+
@Override
31+
public boolean isTraceEnabled() {
32+
return true;
33+
}
34+
35+
@Override
36+
public boolean isTraceEnabled(Marker marker) {
37+
return true;
38+
}
39+
40+
@Override
41+
public boolean isDebugEnabled() {
42+
return true;
43+
}
44+
45+
@Override
46+
public boolean isDebugEnabled(Marker marker) {
47+
return true;
48+
}
49+
50+
@Override
51+
public boolean isInfoEnabled() {
52+
return true;
53+
}
54+
55+
@Override
56+
public boolean isInfoEnabled(Marker marker) {
57+
return true;
58+
}
59+
60+
@Override
61+
public boolean isWarnEnabled() {
62+
return true;
63+
}
64+
65+
@Override
66+
public boolean isWarnEnabled(Marker marker) {
67+
return true;
68+
}
69+
70+
@Override
71+
public boolean isErrorEnabled() {
72+
return true;
73+
}
74+
75+
@Override
76+
public boolean isErrorEnabled(Marker marker) {
77+
return true;
78+
}
79+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package com.saucelabs.ci.sauceconnect;
2+
3+
import java.io.InputStream;
4+
import java.io.PrintStream;
5+
6+
public interface ProcessOutputPrinter {
7+
Runnable getStdoutPrinter(InputStream stdout, PrintStream printStream);
8+
Runnable getStderrPrinter(InputStream stderr, PrintStream printStream);
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.saucelabs.ci.sauceconnect;
2+
3+
import java.util.concurrent.Semaphore;
4+
5+
public interface SCMonitor extends Runnable {
6+
void setSemaphore(Semaphore semaphore);
7+
String getTunnelId();
8+
Exception getLastHealtcheckException();
9+
void markAsFailed();
10+
boolean isFailed();
11+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package com.saucelabs.ci.sauceconnect;
2+
3+
import org.slf4j.Logger;
4+
5+
public interface SCMonitorFactory {
6+
SCMonitor create(int port, Logger logger);
7+
}

Diff for: src/main/java/com/saucelabs/ci/sauceconnect/SauceConnectManager.java

+12-20
Original file line numberDiff line numberDiff line change
@@ -10,33 +10,26 @@
1010
import org.apache.commons.lang3.concurrent.LazyInitializer;
1111
import org.apache.commons.lang3.concurrent.LazyInitializer.Builder;
1212
import org.json.JSONObject;
13+
import org.slf4j.Logger;
1314

14-
import java.io.BufferedInputStream;
15-
import java.io.File;
16-
import java.io.IOException;
17-
import java.io.InputStream;
18-
import java.io.PrintStream;
15+
import java.io.*;
1916
import java.net.URI;
17+
import java.net.URL;
2018
import java.net.http.HttpClient;
2119
import java.net.http.HttpRequest;
2220
import java.net.http.HttpResponse;
2321
import java.nio.file.Files;
2422
import java.nio.file.Path;
25-
import java.text.MessageFormat;
2623
import java.util.ArrayList;
2724
import java.util.Arrays;
2825
import java.util.HashMap;
29-
import java.util.List;
30-
31-
import java.net.URL;
3226

3327
/**
3428
* Handles launching Sauce Connect (binary executable).
3529
*
3630
* @author Ross Rowe
3731
*/
38-
public class SauceConnectManager extends AbstractSauceTunnelManager
39-
implements SauceTunnelManager {
32+
public class SauceConnectManager extends AbstractSauceTunnelManager implements SauceTunnelManager {
4033
private boolean useLatestSauceConnect = false;
4134

4235
/** Remove all created files and directories on exit */
@@ -183,7 +176,7 @@ public SauceConnectManager(boolean quietMode) {
183176
public SauceConnectManager(boolean quietMode, String runner, int apiPort) {
184177
super(quietMode);
185178
this.runner = runner;
186-
this.apiPort = DEFAULT_API_PORT;
179+
this.apiPort = apiPort;
187180
}
188181

189182
/**
@@ -192,7 +185,7 @@ public SauceConnectManager(boolean quietMode, String runner, int apiPort) {
192185
* @param sauceConnectJar File which contains the Sauce Connect executables (typically the CI
193186
* plugin Jar file)
194187
* @param options the command line options used to launch Sauce Connect
195-
* @param printStream the output stream to send log messages
188+
* @param logger used for logging
196189
* @param sauceConnectPath if defined, Sauce Connect will be launched from the specified path and
197190
* won't be extracted from the jar file
198191
* @return new ProcessBuilder instance which will launch Sauce Connect
@@ -206,7 +199,7 @@ protected Process prepAndCreateProcess(
206199
int apiPort,
207200
File sauceConnectJar,
208201
String options,
209-
PrintStream printStream,
202+
Logger logger,
210203
String sauceConnectPath,
211204
boolean legacy)
212205
throws SauceConnectException {
@@ -234,11 +227,11 @@ protected Process prepAndCreateProcess(
234227
if (!sauceConnectBinary.exists()) {
235228
synchronized (this) {
236229
if (!sauceConnectBinary.exists()) {
237-
extractZipFile(workingDirectory, operatingSystem);
230+
extractZipFile(workingDirectory, operatingSystem, logger);
238231
}
239232
}
240233
} else {
241-
logMessage(printStream, sauceConnectBinary + " already exists, so not extracting");
234+
logger.info("File {} already exists, so not extracting", sauceConnectBinary);
242235
}
243236
} else {
244237
sauceConnectBinary = new File(sauceConnectPath);
@@ -262,7 +255,7 @@ protected Process prepAndCreateProcess(
262255
args = addExtraInfo(args);
263256
}
264257

265-
LOGGER.info("Launching Sauce Connect {} {}", getCurrentVersion(), hideSauceConnectCommandlineSecrets(args));
258+
logger.info("Launching Sauce Connect {} {}", getCurrentVersion(), hideSauceConnectCommandlineSecrets(args));
266259
return createProcess(args, sauceConnectBinary.getParentFile());
267260
} catch (IOException e) {
268261
throw new SauceConnectException(e);
@@ -400,7 +393,7 @@ protected String[] addExtraInfoLegacy(String[] args) {
400393
* @return the directory containing the extracted files
401394
* @throws IOException thrown if an error occurs extracting the files
402395
*/
403-
public File extractZipFile(File workingDirectory, OperatingSystem operatingSystem) throws IOException {
396+
public File extractZipFile(File workingDirectory, OperatingSystem operatingSystem, Logger logger) throws IOException {
404397
String archiveFileName = operatingSystem.getFileName(useLatestSauceConnect);
405398
File unzipDir = getUnzipDir(workingDirectory, operatingSystem);
406399
unzipDir.mkdirs();
@@ -417,8 +410,7 @@ public File extractZipFile(File workingDirectory, OperatingSystem operatingSyste
417410

418411
File sauceConnectBinary = new File(unzipDir, operatingSystem.getExecutable());
419412
if (!sauceConnectBinary.canExecute() && !sauceConnectBinary.setExecutable(true)) {
420-
LOGGER.warn("Unable to set the execute permission for SauceConnect binary file located at {}",
421-
sauceConnectBinary);
413+
logger.warn("Unable to set the execute permission for SauceConnect binary file located at {}", sauceConnectBinary);
422414
}
423415
return unzipDir;
424416
}

0 commit comments

Comments
 (0)