com.launchdarkly.logging.LogCapture Maven / Gradle / Ivy
Show all versions of launchdarkly-logging Show documentation
package com.launchdarkly.logging;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Objects;
/**
* A mechanism for capturing logger output in memory.
*
* Calling {@link Logs#capture()} provides a {@link LogCapture} object that accumulates
* all log output from any code that is configured to use it as the log adapter. This is
* mainly intended for testing.
*
* All messages that come to this object are captured regardless of the log level. If you
* want to filter out messages below a certain level, you can apply {@link Logs#level(LDLogAdapter, LDLogLevel)}
* and pass the resulting filtered adapter to whatever component will be doing the logging,
* in place of the original {@link LogCapture} object.
*
* Example of usage in the server-side Java SDK:
*
*
* LogCapture logSink = Logs.capture();
* LDConfig config = new LDConfig.Builder()
* .logging(
* Components.logging().adapter(logSink)
* )
* .build();
* // create the LDClient and do some things that might produce log output...
* // now, retrieve the captured output
* List<LogCapture.Message> messages = logSink.getMessages();
*
*/
public final class LogCapture implements LDLogAdapter {
private final List messages = new ArrayList<>();
private final Object messagesLock = new Object();
LogCapture() {}
/**
* Information about a captured log message.
*/
public static final class Message {
private final Date timestamp;
private final String loggerName;
private final LDLogLevel level;
private final String text;
/**
* Creates an instance.
*
* @param timestamp the time the message was generated
* @param loggerName the logger name
* @param level the log level
* @param text the text of the message, after any parameters have been substituted
*/
public Message(Date timestamp, String loggerName, LDLogLevel level, String text) {
this.timestamp = timestamp;
this.loggerName = loggerName;
this.level = level;
this.text = text;
}
/**
* Returns the time the message was generated.
*
* This is represented as a Date rather than a java.time.Instant
* because currently this code needs to support Android API versions that do not have java.time.
*
* @return the timestamp
*/
public Date getTimestamp() {
return timestamp;
}
/**
* Returns the name of the logger that produced the message.
*
* @return the logger name
*/
public String getLoggerName() {
return loggerName;
}
/**
* Returns the log level of the message.
*
* @return the log level
*/
public LDLogLevel getLevel() {
return level;
}
/**
* Returns the text of the message.
*
* @return the text of the message, after any parameters have been substituted
*/
public String getText() {
return text;
}
@Override
public boolean equals(Object other) {
if (other instanceof Message) {
Message o = (Message)other;
return Objects.equals(timestamp, o.timestamp) &&
Objects.equals(loggerName, o.loggerName) &&
level == o.level &&
Objects.equals(text, o.text);
}
return false;
}
@Override
public int hashCode() {
return Objects.hash(timestamp, loggerName, level, text);
}
/**
* Returns a basic string representation of the log item, in the format
* "[logger name] LEVEL: text".
*
* @return a string representation
* @see #toStringWithTimestamp()
*/
@Override
public String toString() {
return "[" + loggerName + "] " + level.name() + ":" + text;
}
/**
* Equivalent to {@link #toString()}, but also prefixes the line with a millisecond timestamp.
*
* @return a string representation
*/
public String toStringWithTimestamp() {
return SimpleLogging.getDefaultTimestampFormat().format(timestamp) + " " + toString();
}
}
/**
* Called internally by the SDK.
*
* @param name a logger name assigned by the SDK
*/
@Override
public Channel newChannel(String name) {
return new ChannelImpl(name);
}
/**
* Returns all captured messages.
*
* @return a copy of the messages
*/
public List getMessages() {
synchronized (messagesLock) {
return new ArrayList<>(messages);
}
}
/**
* Returns all captured messages converted to strings.
*
* The format is always "LEVEL:text".
*
* @return a copy of the messages as strings
*/
public List getMessageStrings() {
List ret = new ArrayList<>();
synchronized (messagesLock) {
for (Message m: messages) {
ret.add(m.getLevel().name() + ":" + m.getText());
}
}
return ret;
}
/**
* Removes and returns a captured log message in FIFO order, waiting if
* necessary until one is available.
*
* This method and {@link #requireMessage(long)} allow you to use
* {@link LogCapture} like a blocking queue, so a test can wait for log output
* that is being generated by another thread.
*
* @param timeoutMilliseconds the maximum time to wait
* @return the next available log message, or null if none
*/
public Message awaitMessage(long timeoutMilliseconds) {
return awaitMessage(null, timeoutMilliseconds);
}
/**
* Removes and returns a captured log message of the specified level in FIFO
* order, waiting if necessary until one is available.
*
* This method and {@link #requireMessage(long)} allow you to use
* {@link LogCapture} like a blocking queue, so a test can wait for log output
* that is being generated by another thread.
*
* @param level the desired message level, or null for any
* @param timeoutMilliseconds the maximum time to wait
* @return the next available log message, or null if none
*/
public Message awaitMessage(LDLogLevel level, long timeoutMilliseconds) {
long deadline = System.currentTimeMillis() + timeoutMilliseconds;
synchronized (messagesLock) {
for (;;) {
for (int i = 0; i < messages.size(); i++) {
Message m = messages.get(i);
if (level == null || m.level == level) {
messages.remove(i);
return m;
}
}
long remainingTime = deadline - System.currentTimeMillis();
if (remainingTime <= 0) {
return null;
}
try {
messagesLock.wait(remainingTime);
}
catch (InterruptedException e) {
return null;
}
}
}
}
/**
* Same as {@link #awaitMessage(long)}, but throws an exception on timeout.
*
* @param timeoutMilliseconds the maximum time to wait
* @return the next available log message
* @throws AssertionError if no log message was available within the timeout
*/
public Message requireMessage(long timeoutMilliseconds) {
return requireMessage(null, timeoutMilliseconds);
}
/**
* Same as {@link #awaitMessage(LDLogLevel, long)}, but throws an exception on timeout.
*
* @param level the desired message level, or null for any
* @param timeoutMilliseconds the maximum time to wait
* @return the next available log message
* @throws AssertionError if no log message was available within the timeout
*/
public Message requireMessage(LDLogLevel level, long timeoutMilliseconds) {
Message m = awaitMessage(level, timeoutMilliseconds);
if (m == null) {
throw new AssertionError("expected a log message but did not get one");
}
return m;
}
private final class ChannelImpl implements Channel {
private final String name;
ChannelImpl(String name) {
this.name = name;
}
private void addMessage(LDLogLevel level, String message) {
synchronized (messagesLock) {
messages.add(new Message(new Date(), name, level, message));
messagesLock.notifyAll();
}
}
@Override
public boolean isEnabled(LDLogLevel level) {
return true;
}
@Override
public void log(LDLogLevel level, Object message) {
addMessage(level, message == null ? "" : message.toString());
}
@Override
public void log(LDLogLevel level, String format, Object param) {
addMessage(level, SimpleFormat.format(format, param));
}
@Override
public void log(LDLogLevel level, String format, Object param1, Object param2) {
addMessage(level, SimpleFormat.format(format, param1, param2));
}
@Override
public void log(LDLogLevel level, String format, Object... params) {
addMessage(level, SimpleFormat.format(format, params));
}
}
}