-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
More accurate log processing. #643
Changes from 9 commits
95a7887
fb7dd2d
503e0d8
1f544bf
656c80d
9a693e7
aec8bad
d5d8959
4da324f
489dd9b
2bacce8
6191e6e
25fec6c
64b1e0e
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,17 @@ | ||
package org.testcontainers.containers.output; | ||
|
||
import lombok.Getter; | ||
import lombok.Setter; | ||
|
||
import java.util.function.Consumer; | ||
|
||
public abstract class BaseConsumer<S extends BaseConsumer<S>> implements Consumer<OutputFrame> { | ||
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 a question, why 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. Initially I named this like 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 this has the same semantics than the |
||
@Getter | ||
@Setter | ||
private boolean removeColorCodes = true; | ||
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. please also add Lombok's 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. plus |
||
|
||
public S withRemoveAnsiCodes(boolean removeAnsiCodes) { | ||
this.removeColorCodes = removeAnsiCodes; | ||
return (S) this; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,28 +2,41 @@ | |
|
||
|
||
import com.github.dockerjava.api.model.Frame; | ||
import com.github.dockerjava.api.model.StreamType; | ||
import com.github.dockerjava.core.async.ResultCallbackTemplate; | ||
import org.slf4j.Logger; | ||
import org.slf4j.LoggerFactory; | ||
|
||
import java.io.IOException; | ||
import java.util.ArrayList; | ||
import java.util.Arrays; | ||
import java.util.HashMap; | ||
import java.util.HashSet; | ||
import java.util.List; | ||
import java.util.Map; | ||
import java.util.concurrent.CountDownLatch; | ||
import java.util.function.Consumer; | ||
import java.util.regex.Pattern; | ||
|
||
/** | ||
* This class can be used as a generic callback for docker-java commands that produce Frames. | ||
*/ | ||
public class FrameConsumerResultCallback extends ResultCallbackTemplate<FrameConsumerResultCallback, Frame> { | ||
|
||
private final static Logger LOGGER = LoggerFactory.getLogger(FrameConsumerResultCallback.class); | ||
private static final Logger LOGGER = LoggerFactory.getLogger(FrameConsumerResultCallback.class); | ||
|
||
private static final byte[] EMPTY_BYTES = new byte[0]; | ||
|
||
private static final Pattern ANSI_COLOR_PATTERN = Pattern.compile("\u001B\\[[0-9;]+m"); | ||
|
||
private Map<OutputFrame.OutputType, Consumer<OutputFrame>> consumers; | ||
|
||
private CountDownLatch completionLatch = new CountDownLatch(1); | ||
|
||
private StringBuilder logString = new StringBuilder(); | ||
|
||
private OutputFrame brokenFrame; | ||
|
||
public FrameConsumerResultCallback() { | ||
consumers = new HashMap<>(); | ||
} | ||
|
@@ -45,9 +58,13 @@ public void onNext(Frame frame) { | |
if (outputFrame != null) { | ||
Consumer<OutputFrame> consumer = consumers.get(outputFrame.getType()); | ||
if (consumer == null) { | ||
LOGGER.error("got frame with type " + frame.getStreamType() + ", for which no handler is configured"); | ||
LOGGER.error("got frame with type {}, for which no handler is configured", frame.getStreamType()); | ||
} else { | ||
consumer.accept(outputFrame); | ||
if (frame.getStreamType() == StreamType.RAW) { | ||
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. both methods have |
||
processRawFrame(outputFrame, consumer); | ||
} else { | ||
processOtherFrame(outputFrame, consumer); | ||
} | ||
} | ||
} | ||
} | ||
|
@@ -63,8 +80,17 @@ public void onError(Throwable throwable) { | |
|
||
@Override | ||
public void close() throws IOException { | ||
OutputFrame lastLine = null; | ||
|
||
if (logString.length() > 0) { | ||
lastLine = new OutputFrame(OutputFrame.OutputType.STDOUT, logString.toString().getBytes()); | ||
} | ||
|
||
// send an END frame to every consumer... but only once per consumer. | ||
for (Consumer<OutputFrame> consumer : new HashSet<>(consumers.values())) { | ||
if (lastLine != null) { | ||
consumer.accept(lastLine); | ||
} | ||
consumer.accept(OutputFrame.END); | ||
} | ||
super.close(); | ||
|
@@ -78,4 +104,79 @@ public void close() throws IOException { | |
public CountDownLatch getCompletionLatch() { | ||
return completionLatch; | ||
} | ||
|
||
private synchronized void processRawFrame(OutputFrame outputFrame, Consumer<OutputFrame> consumer) { | ||
if (outputFrame != null) { | ||
String utf8String = outputFrame.getUtf8String(); | ||
byte[] bytes = outputFrame.getBytes(); | ||
|
||
if (utf8String != null && !utf8String.isEmpty()) { | ||
// Merging the strings by bytes to solve the problem breaking non-latin unicode symbols. | ||
if (brokenFrame != null) { | ||
bytes = merge(brokenFrame.getBytes(), bytes); | ||
utf8String = new String(bytes); | ||
brokenFrame = null; | ||
} | ||
// Logger chunks can break the string in middle of multibyte unicode character. | ||
// Backup the bytes to reconstruct proper char sequence with bytes from next frame. | ||
if (Character.getType(utf8String.charAt(utf8String.length() - 1)) == Character.OTHER_SYMBOL) { | ||
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 would like |
||
brokenFrame = new OutputFrame(outputFrame.getType(), bytes); | ||
return; | ||
} | ||
|
||
utf8String = processAnsiColorCodes(utf8String, consumer); | ||
normalizeLogLines(utf8String, consumer); | ||
} | ||
} | ||
} | ||
|
||
private synchronized void processOtherFrame(OutputFrame outputFrame, Consumer<OutputFrame> consumer) { | ||
if (outputFrame != null) { | ||
String utf8String = outputFrame.getUtf8String(); | ||
|
||
if (utf8String != null && !utf8String.isEmpty()) { | ||
utf8String = processAnsiColorCodes(utf8String, consumer); | ||
consumer.accept(new OutputFrame(outputFrame.getType(), utf8String.getBytes())); | ||
} | ||
} | ||
} | ||
|
||
private void normalizeLogLines(String utf8String, Consumer<OutputFrame> consumer) { | ||
// Reformat strings to normalize enters. | ||
List<String> lines = new ArrayList<>(Arrays.asList(utf8String.split("((\\r?\\n)|(\\r))"))); | ||
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 would move split regex into a constant. |
||
if (lines.isEmpty()) { | ||
consumer.accept(new OutputFrame(OutputFrame.OutputType.STDOUT, EMPTY_BYTES)); | ||
return; | ||
} | ||
if (utf8String.startsWith("\n") || utf8String.startsWith("\r")) { | ||
lines.add(0, ""); | ||
} | ||
if (utf8String.endsWith("\n") || utf8String.endsWith("\r")) { | ||
lines.add(""); | ||
} | ||
for (int i = 0; i < lines.size() - 1; i++) { | ||
String line = lines.get(i); | ||
if (i == 0 && logString.length() > 0) { | ||
line = logString.toString() + line; | ||
logString.setLength(0); | ||
} | ||
consumer.accept(new OutputFrame(OutputFrame.OutputType.STDOUT, line.getBytes())); | ||
} | ||
logString.append(lines.get(lines.size() - 1)); | ||
} | ||
|
||
private String processAnsiColorCodes(String utf8String, Consumer<OutputFrame> consumer) { | ||
if (!(consumer instanceof BaseConsumer) || ((BaseConsumer) consumer).isRemoveColorCodes()) { | ||
return ANSI_COLOR_PATTERN.matcher(utf8String).replaceAll(""); | ||
} | ||
return utf8String; | ||
} | ||
|
||
|
||
private byte[] merge(byte[] str1, byte[] str2) { | ||
byte[] mergedString = new byte[str1.length + str2.length]; | ||
System.arraycopy(str1, 0, mergedString, 0, str1.length); | ||
System.arraycopy(str2, 0, mergedString, str1.length, str2.length); | ||
return mergedString; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,21 +5,26 @@ | |
import java.io.ByteArrayOutputStream; | ||
import java.io.IOException; | ||
import java.nio.charset.Charset; | ||
import java.util.function.Consumer; | ||
|
||
/** | ||
* Created by rnorth on 26/03/2016. | ||
*/ | ||
public class ToStringConsumer implements Consumer<OutputFrame> { | ||
public class ToStringConsumer extends BaseConsumer<ToStringConsumer> { | ||
private static final byte[] ENTER_BYTES = "\n".getBytes(); | ||
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. Naming: "NEW_LINE" or "LINE_BREAK" |
||
|
||
private boolean firstLine = true; | ||
private ByteArrayOutputStream stringBuffer = new ByteArrayOutputStream(); | ||
|
||
@Override | ||
public void accept(OutputFrame outputFrame) { | ||
try { | ||
if (outputFrame.getBytes() != null) { | ||
if (!firstLine) { | ||
stringBuffer.write(ENTER_BYTES); | ||
} | ||
stringBuffer.write(outputFrame.getBytes()); | ||
stringBuffer.flush(); | ||
firstLine = false; | ||
} | ||
} catch (IOException e) { | ||
throw new RuntimeException(e); | ||
|
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.