diff --git a/api/src/main/java/com/google/common/flogger/LogContext.java b/api/src/main/java/com/google/common/flogger/LogContext.java index 4996057e..71b982ca 100644 --- a/api/src/main/java/com/google/common/flogger/LogContext.java +++ b/api/src/main/java/com/google/common/flogger/LogContext.java @@ -174,6 +174,20 @@ public void emit(Tags tags, KeyValueHandler out) { */ public static final MetadataKey CONTEXT_STACK_SIZE = MetadataKey.single("stack_size", StackSize.class); + + /** + * Key associated with the metadata for specifying {@code KeyValueFormatter.prefix} with a log + * statement. + */ + public static final MetadataKey CONTEXT_PREFIX = + MetadataKey.single("prefix", String.class); + + /** + * Key associated with the metadata for specifying {@code KeyValueFormatter.suffix} with a log + * statement. + */ + public static final MetadataKey CONTEXT_SUFFIX = + MetadataKey.single("suffix", String.class); } static final class MutableMetadata extends Metadata { diff --git a/api/src/main/java/com/google/common/flogger/backend/SimpleMessageFormatter.java b/api/src/main/java/com/google/common/flogger/backend/SimpleMessageFormatter.java index 273af60c..df6cc249 100644 --- a/api/src/main/java/com/google/common/flogger/backend/SimpleMessageFormatter.java +++ b/api/src/main/java/com/google/common/flogger/backend/SimpleMessageFormatter.java @@ -19,6 +19,7 @@ import com.google.common.flogger.LogContext; import com.google.common.flogger.MetadataKey; import com.google.common.flogger.MetadataKey.KeyValueHandler; +import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.Set; @@ -53,12 +54,15 @@ public final class SimpleMessageFormatter { @SuppressWarnings("ConstantCaseForConstants") private static final Set> DEFAULT_KEYS_TO_IGNORE = - Collections.>singleton(LogContext.Key.LOG_CAUSE); + Collections.unmodifiableSet(new HashSet>(Arrays.asList( + LogContext.Key.LOG_CAUSE, + LogContext.Key.CONTEXT_PREFIX, + LogContext.Key.CONTEXT_SUFFIX))); private static final LogMessageFormatter DEFAULT_FORMATTER = newFormatter(DEFAULT_KEYS_TO_IGNORE); /** - * Returns the singleton default log message formatter. This formats log messages in the form: + * Returns the default log message formatter. This formats log messages in the form: * *
{@code
    * Log message [CONTEXT key="value" id=42 ]
@@ -71,9 +75,20 @@ public final class SimpleMessageFormatter {
    * 

The {@code cause} is omitted from the context section, since it's handled separately by most * logger backends and not considered part of the formatted message. Other internal metadata keys * may also be suppressed. + * + *

{@code + * LogContext.addMetadata(MetadataKey, Object)} can be used to change prefix and suffix, instead + * of using the default. */ - public static LogMessageFormatter getDefaultFormatter() { - return DEFAULT_FORMATTER; + public static LogMessageFormatter getDefaultFormatter(Metadata metadata) { + if (metadata == null + || metadata.equals(Metadata.empty()) + || metadata.findValue(LogContext.Key.CONTEXT_PREFIX) == null) { + return DEFAULT_FORMATTER; + } + return newFormatter(DEFAULT_KEYS_TO_IGNORE, + metadata.findValue(LogContext.Key.CONTEXT_PREFIX), + metadata.findValue(LogContext.Key.CONTEXT_SUFFIX)); } /** @@ -95,7 +110,7 @@ public static LogMessageFormatter getDefaultFormatter() { */ public static LogMessageFormatter getSimpleFormatterIgnoring(MetadataKey... extraIgnoredKeys) { if (extraIgnoredKeys.length == 0) { - return getDefaultFormatter(); + return DEFAULT_FORMATTER; } Set> ignored = new HashSet>(DEFAULT_KEYS_TO_IGNORE); Collections.addAll(ignored, extraIgnoredKeys); @@ -121,8 +136,13 @@ public static LogMessageFormatter getSimpleFormatterIgnoring(MetadataKey... e public static StringBuilder appendContext( MetadataProcessor metadataProcessor, MetadataHandler metadataHandler, + String prefix, + String suffix, StringBuilder buffer) { - KeyValueFormatter kvf = new KeyValueFormatter("[CONTEXT ", " ]", buffer); + KeyValueFormatter kvf = new KeyValueFormatter( + prefix == null ? "" : prefix, + suffix == null ? "" : suffix, + buffer); metadataProcessor.process(metadataHandler, kvf); kvf.done(); return buffer; @@ -179,7 +199,9 @@ public static boolean mustBeFormatted( * Returns a new "simple" formatter which ignores the given set of metadata keys. The caller must * ensure that the given set is effectively immutable. */ - private static LogMessageFormatter newFormatter(final Set> keysToIgnore) { + private static LogMessageFormatter newFormatter(final Set> keysToIgnore, + final String prefix, + final String suffix) { return new LogMessageFormatter() { private final MetadataHandler handler = MetadataKeyValueHandlers.getDefaultHandler(keysToIgnore); @@ -188,7 +210,7 @@ private static LogMessageFormatter newFormatter(final Set> keysTo public StringBuilder append( LogData logData, MetadataProcessor metadata, StringBuilder buffer) { BaseMessageFormatter.appendFormattedMessage(logData, buffer); - return appendContext(metadata, handler, buffer); + return appendContext(metadata, handler, prefix, suffix, buffer); } @Override @@ -202,6 +224,14 @@ public String format(LogData logData, MetadataProcessor metadata) { }; } + /** + * Returns a new "simple" default formatter which ignores the given set of metadata keys. The caller must + * ensure that the given set is effectively immutable. + */ + private static LogMessageFormatter newFormatter(final Set> keysToIgnore) { + return newFormatter(keysToIgnore, "[CONTEXT ", " ]"); + } + // ---- Everything below this point is deprecated and will be removed. ---- /** @deprecated Use a {@link LogMessageFormatter} and obtain the level and cause separately. */ @@ -213,7 +243,7 @@ public static void format(LogData logData, SimpleLogHandler receiver) { MetadataProcessor.forScopeAndLogSite(Metadata.empty(), logData.getMetadata()); receiver.handleFormattedLogMessage( logData.getLevel(), - getDefaultFormatter().format(logData, metadata), + getDefaultFormatter(logData.getMetadata()).format(logData, metadata), metadata.getSingleValue(LogContext.Key.LOG_CAUSE)); } diff --git a/api/src/main/java/com/google/common/flogger/backend/system/AbstractLogRecord.java b/api/src/main/java/com/google/common/flogger/backend/system/AbstractLogRecord.java index 0cdf9641..b8b4152e 100644 --- a/api/src/main/java/com/google/common/flogger/backend/system/AbstractLogRecord.java +++ b/api/src/main/java/com/google/common/flogger/backend/system/AbstractLogRecord.java @@ -152,7 +152,7 @@ protected AbstractLogRecord(RuntimeException error, LogData data, Metadata scope * requiring a new field in this class (to cut down on instance size). */ protected LogMessageFormatter getLogMessageFormatter() { - return SimpleMessageFormatter.getDefaultFormatter(); + return SimpleMessageFormatter.getDefaultFormatter(data.getMetadata()); } @Override diff --git a/api/src/test/java/com/google/common/flogger/backend/SimpleMessageFormatterTest.java b/api/src/test/java/com/google/common/flogger/backend/SimpleMessageFormatterTest.java index e1997440..559b5844 100644 --- a/api/src/test/java/com/google/common/flogger/backend/SimpleMessageFormatterTest.java +++ b/api/src/test/java/com/google/common/flogger/backend/SimpleMessageFormatterTest.java @@ -55,6 +55,31 @@ public void testFormatFastPath() { assertThat(format(logData, Metadata.empty())).isEqualTo("Hello World [CONTEXT bool=true ]"); } + @Test + public void testCustomContextFormatFastPath() { + // Just formatting a literal argument with no metadata avoids copying the message in a buffer. + String literal = "Hello World"; + FakeLogData logData = FakeLogData.of(literal); + assertThat(format(logData, Metadata.empty())).isSameInstanceAs(literal); + + // It also works if the log data has a "cause" (which is a special case and never formatted). + Throwable cause = new IllegalArgumentException("Badness"); + logData.addMetadata(LogContext.Key.LOG_CAUSE, cause); + assertThat(format(logData, Metadata.empty())).isSameInstanceAs(literal); + + // Add metadata of custom format. + logData.addMetadata(LogContext.Key.CONTEXT_PREFIX, "{ "); + logData.addMetadata(LogContext.Key.CONTEXT_SUFFIX, " }"); + + // However it does not work as soon as there's any scope metadata. + assertThat(format(logData, new FakeMetadata().add(INT_KEY, 42))) + .isEqualTo("Hello World { int=42 }"); + + // Or if there's more metadata added to the log site. + logData.addMetadata(BOOL_KEY, true); + assertThat(format(logData, Metadata.empty())).isEqualTo("Hello World { bool=true }"); + } + // Parsing and basic formatting is well tested in BaseMessageFormatterTest. @Test public void testAppendFormatted() { @@ -84,9 +109,41 @@ public void testAppendFormatted() { .isEqualTo("answer=42 [CONTEXT int=1 int=2 string=\"Hello\" first=\"foo\" last=\"bar\" ]"); } + // Parsing and basic formatting is well tested in BaseMessageFormatterTest. + @Test + public void testCustomContextAppendFormatted() { + FakeLogData logData = FakeLogData.withPrintfStyle("answer=%d", 42); + assertThat(appendFormatted(logData, Metadata.empty())).isEqualTo("answer=42"); + + // Add metadata of custom format. + logData.addMetadata(LogContext.Key.CONTEXT_PREFIX, "{ "); + logData.addMetadata(LogContext.Key.CONTEXT_SUFFIX, " }"); + + FakeMetadata scope = new FakeMetadata().add(INT_KEY, 1); + assertThat(appendFormatted(logData, scope)).isEqualTo("answer=42 { int=1 }"); + + Throwable cause = new IllegalArgumentException("Badness"); + logData.addMetadata(LogContext.Key.LOG_CAUSE, cause); + assertThat(appendFormatted(logData, scope)).isEqualTo("answer=42 { int=1 }"); + + logData.addMetadata(INT_KEY, 2); + assertThat(appendFormatted(logData, scope)).isEqualTo("answer=42 { int=1 int=2 }"); + + // Note that values are grouped by key, and keys are emitted in "encounter order" (scope first). + scope.add(STRING_KEY, "Hello"); + assertThat(appendFormatted(logData, scope)) + .isEqualTo("answer=42 { int=1 int=2 string=\"Hello\" }"); + + // Tags get embedded as metadata, and format in metadata order. So while tag keys are ordered + // locally, mixing tags and metadata does not result in a global ordering of context keys. + Tags tags = Tags.builder().addTag("last", "bar").addTag("first", "foo").build(); + logData.addMetadata(LogContext.Key.TAGS, tags); + assertThat(appendFormatted(logData, scope)) + .isEqualTo("answer=42 { int=1 int=2 string=\"Hello\" first=\"foo\" last=\"bar\" }"); + } + @Test public void testLogMessageFormatter() { - LogMessageFormatter formatter = SimpleMessageFormatter.getDefaultFormatter(); FakeLogData logData = FakeLogData.of("message"); FakeMetadata scope = new FakeMetadata(); @@ -94,11 +151,33 @@ public void testLogMessageFormatter() { Tags tags = Tags.builder().addTag("last", "bar").addTag("first", "foo").build(); logData.addMetadata(LogContext.Key.TAGS, tags); + LogMessageFormatter formatter = SimpleMessageFormatter.getDefaultFormatter(logData.getMetadata()); + MetadataProcessor metadata = MetadataProcessor.forScopeAndLogSite(scope, logData.getMetadata()); + assertThat(formatter.format(logData, metadata)) + .isEqualTo("message [CONTEXT string=\"Hello\" first=\"foo\" last=\"bar\" ]"); + assertThat(formatter.append(logData, metadata, new StringBuilder("PREFIX: ")).toString()) + .isEqualTo("PREFIX: message [CONTEXT string=\"Hello\" first=\"foo\" last=\"bar\" ]"); + } + + @Test + public void testCustomContextLogMessageFormatter() { + FakeLogData logData = FakeLogData.of("message"); + + FakeMetadata scope = new FakeMetadata(); + scope.add(STRING_KEY, "Hello"); + Tags tags = Tags.builder().addTag("last", "bar").addTag("first", "foo").build(); + logData.addMetadata(LogContext.Key.TAGS, tags); + + // Add metadata of custom format. + logData.addMetadata(LogContext.Key.CONTEXT_PREFIX, "{ "); + logData.addMetadata(LogContext.Key.CONTEXT_SUFFIX, " }"); + + LogMessageFormatter formatter = SimpleMessageFormatter.getDefaultFormatter(logData.getMetadata()); MetadataProcessor metadata = MetadataProcessor.forScopeAndLogSite(scope, logData.getMetadata()); assertThat(formatter.format(logData, metadata)) - .isEqualTo("message [CONTEXT string=\"Hello\" first=\"foo\" last=\"bar\" ]"); + .isEqualTo("message { string=\"Hello\" first=\"foo\" last=\"bar\" }"); assertThat(formatter.append(logData, metadata, new StringBuilder("PREFIX: ")).toString()) - .isEqualTo("PREFIX: message [CONTEXT string=\"Hello\" first=\"foo\" last=\"bar\" ]"); + .isEqualTo("PREFIX: message { string=\"Hello\" first=\"foo\" last=\"bar\" }"); } // As above but also ignore the STRING_KEY metadata. @@ -126,12 +205,12 @@ public void testLogMessageFormatter_ignoreExtra() { private static String format(LogData logData, Metadata scope) { MetadataProcessor metadata = MetadataProcessor.forScopeAndLogSite(scope, logData.getMetadata()); - return SimpleMessageFormatter.getDefaultFormatter().format(logData, metadata); + return SimpleMessageFormatter.getDefaultFormatter(logData.getMetadata()).format(logData, metadata); } private static String appendFormatted(LogData logData, Metadata scope) { MetadataProcessor metadata = MetadataProcessor.forScopeAndLogSite(scope, logData.getMetadata()); - return SimpleMessageFormatter.getDefaultFormatter() + return SimpleMessageFormatter.getDefaultFormatter(logData.getMetadata()) .append(logData, metadata, new StringBuilder()) .toString(); } diff --git a/api/src/test/java/com/google/common/flogger/backend/system/AbstractLogRecordTest.java b/api/src/test/java/com/google/common/flogger/backend/system/AbstractLogRecordTest.java index d93f4981..c96052c0 100644 --- a/api/src/test/java/com/google/common/flogger/backend/system/AbstractLogRecordTest.java +++ b/api/src/test/java/com/google/common/flogger/backend/system/AbstractLogRecordTest.java @@ -31,7 +31,7 @@ @RunWith(JUnit4.class) public final class AbstractLogRecordTest { private static final LogMessageFormatter DEFAULT_FORMATTER = - SimpleMessageFormatter.getDefaultFormatter(); + SimpleMessageFormatter.getDefaultFormatter(Metadata.empty()); private static final LogMessageFormatter TEST_MESSAGE_FORMATTER = new LogMessageFormatter() {