diff --git a/README.md b/README.md index a23935d5..dee6b770 100644 --- a/README.md +++ b/README.md @@ -2409,6 +2409,9 @@ LOGGER.info("{\"type\":\"example\",\"msg\":\"example of json message with type\" Note that the value that is sent for `line_long` is a number even though in your pattern it is a quoted text. And the json_message field value is a json object, not a string. +You can escape an operation by prefixing it with `\` if you don't want it to be interpreted. + + #### Omitting fields with empty values The pattern provider can be configured to omit fields with the following _empty_ values: diff --git a/src/main/java/net/logstash/logback/composite/AbstractPatternJsonProvider.java b/src/main/java/net/logstash/logback/composite/AbstractPatternJsonProvider.java index 657bd00a..0b95c8b0 100644 --- a/src/main/java/net/logstash/logback/composite/AbstractPatternJsonProvider.java +++ b/src/main/java/net/logstash/logback/composite/AbstractPatternJsonProvider.java @@ -19,6 +19,7 @@ import java.util.Objects; import net.logstash.logback.pattern.AbstractJsonPatternParser; +import net.logstash.logback.pattern.AbstractJsonPatternParser.JsonPatternException; import net.logstash.logback.pattern.NodeWriter; import ch.qos.logback.access.spi.IAccessEvent; @@ -79,7 +80,13 @@ public void start() { if (jsonFactory == null) { throw new IllegalStateException("JsonFactory has not been set"); } - initializeNodeWriter(); + + try { + this.nodeWriter = initializeNodeWriter(); + } catch (JsonPatternException e) { + this.nodeWriter = null; + addError("Invalid [pattern]: " + e.getMessage(), e); + } super.start(); } @@ -87,11 +94,14 @@ public void start() { /** * Parses the pattern into a {@link NodeWriter}. + * + * @return a {@link NodeWriter} + * @throws JsonPatternException thrown in case of invalid pattern */ - private void initializeNodeWriter() { + private NodeWriter initializeNodeWriter() throws JsonPatternException { AbstractJsonPatternParser parser = createParser(this.jsonFactory); parser.setOmitEmptyFields(omitEmptyFields); - this.nodeWriter = parser.parse(pattern); + return parser.parse(pattern); } diff --git a/src/main/java/net/logstash/logback/composite/accessevent/AccessEventPatternJsonProvider.java b/src/main/java/net/logstash/logback/composite/accessevent/AccessEventPatternJsonProvider.java index 4986fa4e..a8103cd2 100644 --- a/src/main/java/net/logstash/logback/composite/accessevent/AccessEventPatternJsonProvider.java +++ b/src/main/java/net/logstash/logback/composite/accessevent/AccessEventPatternJsonProvider.java @@ -29,7 +29,7 @@ public class AccessEventPatternJsonProvider extends AbstractPatternJsonProvider< @Override protected AbstractJsonPatternParser createParser(JsonFactory jsonFactory) { - return new AccessEventJsonPatternParser(this, jsonFactory); + return new AccessEventJsonPatternParser(getContext(), jsonFactory); } } diff --git a/src/main/java/net/logstash/logback/composite/loggingevent/LoggingEventPatternJsonProvider.java b/src/main/java/net/logstash/logback/composite/loggingevent/LoggingEventPatternJsonProvider.java index 20618d9a..449449fd 100644 --- a/src/main/java/net/logstash/logback/composite/loggingevent/LoggingEventPatternJsonProvider.java +++ b/src/main/java/net/logstash/logback/composite/loggingevent/LoggingEventPatternJsonProvider.java @@ -29,7 +29,7 @@ public class LoggingEventPatternJsonProvider extends AbstractPatternJsonProvider @Override protected AbstractJsonPatternParser createParser(JsonFactory jsonFactory) { - return new LoggingEventJsonPatternParser(this, jsonFactory); + return new LoggingEventJsonPatternParser(getContext(), jsonFactory); } } diff --git a/src/main/java/net/logstash/logback/pattern/AbstractJsonPatternParser.java b/src/main/java/net/logstash/logback/pattern/AbstractJsonPatternParser.java index 355afca4..a40597c8 100644 --- a/src/main/java/net/logstash/logback/pattern/AbstractJsonPatternParser.java +++ b/src/main/java/net/logstash/logback/pattern/AbstractJsonPatternParser.java @@ -22,6 +22,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.function.Function; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -29,13 +30,13 @@ import net.logstash.logback.composite.JsonWritingUtils; import net.logstash.logback.util.StringUtils; +import ch.qos.logback.core.Context; import ch.qos.logback.core.pattern.PatternLayoutBase; -import ch.qos.logback.core.spi.ContextAware; import com.fasterxml.jackson.core.JsonFactory; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParseException; import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.TreeNode; +import com.fasterxml.jackson.core.JsonPointer; import com.fasterxml.jackson.core.filter.FilteringGeneratorDelegate; import com.fasterxml.jackson.core.filter.TokenFilter; import com.fasterxml.jackson.core.filter.TokenFilter.Inclusion; @@ -55,10 +56,11 @@ public abstract class AbstractJsonPatternParser { /** * Pattern used to parse and detect {@link AbstractJsonPatternParser.Operation} in a string. + * An operation starts with a #, followed by a name and a pair of {} with possible arguments in between. */ - public static final Pattern OPERATION_PATTERN = Pattern.compile("\\# (\\w+) (?: \\{ (.*) \\} )?", Pattern.COMMENTS); + public static final Pattern OPERATION_PATTERN = Pattern.compile("\\# (\\w+) (?: \\{ (.*) \\} )", Pattern.COMMENTS); - private final ContextAware contextAware; + private final Context context; private final JsonFactory jsonFactory; private final Map> operations = new HashMap<>(); @@ -70,8 +72,8 @@ public abstract class AbstractJsonPatternParser { */ private boolean omitEmptyFields; - public AbstractJsonPatternParser(final ContextAware contextAware, final JsonFactory jsonFactory) { - this.contextAware = Objects.requireNonNull(contextAware); + AbstractJsonPatternParser(final Context context, final JsonFactory jsonFactory) { + this.context = Objects.requireNonNull(context); this.jsonFactory = Objects.requireNonNull(jsonFactory); addOperation("asLong", new AsLongOperation()); addOperation("asDouble", new AsDoubleOperation()); @@ -89,85 +91,49 @@ protected void addOperation(String name, Operation operation) { this.operations.put(name, operation); } - protected abstract class Operation { - private final boolean requiresData; - - Operation(boolean requiresData) { - this.requiresData = requiresData; - } - - public boolean requiresData() { - return requiresData; - } - - public abstract ValueGetter createValueGetter(String data); + protected interface Operation extends Function { } - protected class AsLongOperation extends Operation { - public AsLongOperation() { - super(true); - } - + protected static class AsLongOperation implements Operation { @Override - public ValueGetter createValueGetter(String data) { - return makeLayoutValueGetter(data).andThen(Long::parseLong); + public Long apply(String value) { + try { + return Long.parseLong(value); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Failed to convert '" + value + "' into a Long numeric value"); + } } } - protected class AsDoubleOperation extends Operation { - public AsDoubleOperation() { - super(true); - } - + protected static class AsDoubleOperation implements Operation { @Override - public ValueGetter createValueGetter(String data) { - return makeLayoutValueGetter(data).andThen(Double::parseDouble); + public Double apply(String value) { + try { + return Double.parseDouble(value); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Failed to convert '" + value + "' into a Double numeric value"); + } } } - protected class AsJsonOperation extends Operation { - public AsJsonOperation() { - super(true); - } - + protected class AsJsonOperation implements Operation { @Override - public ValueGetter createValueGetter(String data) { - return makeLayoutValueGetter(data).andThen(this::convert); - } - - private JsonNode convert(final String value) { + public JsonNode apply(final String value) { try { return JsonReadingUtils.readFully(jsonFactory, value); + } catch (JsonParseException e) { + throw new IllegalArgumentException("Failed to convert '" + value + "' into a JSON object", e); } catch (IOException e) { throw new IllegalStateException("Unexpected IOException when reading JSON value (was '" + value + "')", e); } } } - protected class TryJsonOperation extends Operation { - public TryJsonOperation() { - super(true); - } - + protected class TryJsonOperation implements Operation { @Override - public ValueGetter createValueGetter(String data) { - return makeLayoutValueGetter(data).andThen(this::convert); - } - - private Object convert(final String value) { - final String trimmedValue = StringUtils.trimToEmpty(value); - - try (JsonParser parser = jsonFactory.createParser(trimmedValue)) { - final TreeNode tree = parser.readValueAsTree(); - if (parser.getCurrentLocation().getCharOffset() < trimmedValue.length()) { - /* - * If the full trimmed string was not read, then the full trimmed string contains a json value plus other text. - * For example, trimmedValue = '10 foobar', or 'true foobar', or '{"foo","bar"} baz'. - * In these cases readTree will only read the first part, and will not read the remaining text. - */ - return value; - } - return tree; + public Object apply(final String value) { + try { + return JsonReadingUtils.readFully(jsonFactory, value); } catch (JsonParseException e) { return value; } catch (IOException e) { @@ -177,7 +143,7 @@ private Object convert(final String value) { } - private ValueGetter makeComputableValueGetter(String pattern) { + private ValueGetter makeComputableValueGetter(String pattern) { Matcher matcher = OPERATION_PATTERN.matcher(pattern); if (matcher.matches()) { @@ -187,18 +153,23 @@ private ValueGetter makeComputableValueGetter(String pattern) { : null; Operation operation = this.operations.get(operationName); - if (operation != null) { - if (operation.requiresData() && operationData == null) { - contextAware.addError("No parameter provided to operation: " + operationName); - } else { - return operation.createValueGetter(operationData); - } + if (operation == null) { + throw new IllegalArgumentException("Unknown operation '#" + operationName + "{}'"); } + + final ValueGetter layoutValueGetter = makeLayoutValueGetter(operationData); + return event -> operation.apply(layoutValueGetter.getValue(event)); + + } else { + // Unescape pattern if needed + if (pattern != null && pattern.startsWith("\\#")) { + pattern = pattern.substring(1); + } + return makeLayoutValueGetter(pattern); } - return makeLayoutValueGetter(pattern); } - protected ValueGetter makeLayoutValueGetter(final String data) { + protected ValueGetter makeLayoutValueGetter(final String data) { /* * PatternLayout emits an ERROR status when pattern is null or empty and * defaults to an empty string. Better to handle it here to avoid the error @@ -223,18 +194,21 @@ protected ValueGetter makeLayoutValueGetter(final String data) { } + /** + * Initialize a PatternLayout with the supplied format and throw an {@link IllegalArgumentException} + * if the format is invalid. + * + * @param format the pattern layout format + * @return a configured and started {@link PatternLayoutAdapter} instance around the supplied format + * @throws IllegalArgumentException if the supplied format is not a valid PatternLayout + */ protected PatternLayoutAdapter buildLayout(String format) { - PatternLayoutBase layout = createLayout(); + PatternLayoutAdapter adapter = new PatternLayoutAdapter<>(createLayout()); + adapter.setPattern(format); + adapter.setContext(context); + adapter.start(); - if (layout.isStarted()) { - throw new IllegalStateException("PatternLayout should not be started"); - } - - layout.setContext(contextAware.getContext()); - layout.setPattern(format); - layout.setPostCompileProcessor(null); // Remove EnsureLineSeparation which is there by default - - return new PatternLayoutAdapter<>(layout); + return adapter; } @@ -247,7 +221,7 @@ protected PatternLayoutAdapter buildLayout(String format) { protected abstract PatternLayoutBase createLayout(); - protected static class LayoutValueGetter implements ValueGetter { + protected static class LayoutValueGetter implements ValueGetter { /** * The PatternLayout from which the value is generated */ @@ -294,8 +268,9 @@ public String getValue(final Event event) { * * @param pattern the JSON pattern to parse * @return a {@link NodeWriter} configured according to the pattern + * @throws JsonPatternException denotes an invalid pattern */ - public NodeWriter parse(String pattern) { + public NodeWriter parse(String pattern) throws JsonPatternException { if (StringUtils.isEmpty(pattern)) { return null; } @@ -304,11 +279,10 @@ public NodeWriter parse(String pattern) { try (JsonParser jsonParser = jsonFactory.createParser(pattern)) { node = JsonReadingUtils.readFullyAsObjectNode(jsonFactory, pattern); } catch (IOException e) { - contextAware.addError("[pattern] is not a valid JSON object", e); - return null; + throw new JsonPatternException("pattern is not a valid JSON object", e); } - NodeWriter nodeWriter = new RootWriter<>(parseObject(node)); + NodeWriter nodeWriter = new RootWriter<>(parseObject(JsonPointer.compile("/"), node)); if (omitEmptyFields) { nodeWriter = new OmitEmptyFieldWriter<>(nodeWriter); } @@ -319,19 +293,26 @@ public NodeWriter parse(String pattern) { * Parse a {@link JsonNode} and produce the corresponding {@link NodeWriter}. * * @param node the {@link JsonNode} to parse. - * @return a {@link NodeWriter} corresponding to the given json node + * @return a {@link NodeWriter} corresponding to the given JSON node + * @throws JsonPatternException denotes an invalid pattern */ - private NodeWriter parseNode(JsonNode node) { + private NodeWriter parseNode(JsonPointer location, JsonNode node) throws JsonPatternException { if (node.isTextual()) { - ValueGetter getter = makeComputableValueGetter(node.asText()); - return new ValueWriter<>(getter); + try { + ValueGetter getter = makeComputableValueGetter(node.asText()); + return new ValueWriter<>(getter); + } catch (RuntimeException e) { + String msg = "Invalid JSON property '" + location + "' (was '" + node.asText() + "'): " + e.getMessage(); + throw new JsonPatternException(msg, e); + } } if (node.isArray()) { - return parseArray((ArrayNode) node); + return parseArray(location, (ArrayNode) node); } if (node.isObject()) { - return parseObject((ObjectNode) node); + return parseObject(location, (ObjectNode) node); } + // Anything else, we will be just writing as is (nulls, numbers, booleans and whatnot) return new ValueWriter<>(g -> node); } @@ -342,11 +323,14 @@ private NodeWriter parseNode(JsonNode node) { * * @param node the {@link ArrayNode} to parse * @return a {@link ArrayWriter} + * @throws JsonPatternException denotes an invalid pattern */ - private ArrayWriter parseArray(ArrayNode node) { + private ArrayWriter parseArray(JsonPointer location, ArrayNode node) throws JsonPatternException { List> children = new ArrayList<>(); + + int index = 0; for (JsonNode item : node) { - children.add(parseNode(item)); + children.add(parseNode(appendPath(location, Integer.toString(index++)), item)); } return new ArrayWriter<>(children); @@ -354,12 +338,13 @@ private ArrayWriter parseArray(ArrayNode node) { /** - * Parse an OBJECT json node + * Parse an JSON object node * * @param node the {@link ObjectNode} to parse * @return a {@link ObjectWriter} + * @throws JsonPatternException denotes an invalid pattern */ - private ObjectWriter parseObject(ObjectNode node) { + private ObjectWriter parseObject(JsonPointer location, ObjectNode node) throws JsonPatternException { ObjectWriter writer = new ObjectWriter<>(); for (Iterator> nodeFields = node.fields(); nodeFields.hasNext();) { @@ -368,7 +353,7 @@ private ObjectWriter parseObject(ObjectNode node) { String fieldName = field.getKey(); JsonNode fieldValue = field.getValue(); - NodeWriter fieldWriter = parseNode(fieldValue); + NodeWriter fieldWriter = parseNode(appendPath(location, fieldName), fieldValue); writer.addField(fieldName, fieldWriter); } @@ -376,6 +361,18 @@ private ObjectWriter parseObject(ObjectNode node) { } + /** + * Append a path to an existing {@link JsonPointer} + * + * @param ptr the pointer to add the path + * @param path the path to add + * @return a new {@link JsonPointer} + */ + private static JsonPointer appendPath(JsonPointer ptr, String path) { + return ptr.append(JsonPointer.compile("/" + path)); + } + + // // -- NodeWriters ----------------------------------------------------------------------------- // @@ -435,9 +432,9 @@ public void write(JsonGenerator generator, Event event) throws IOException { protected static class ValueWriter implements NodeWriter { - private final ValueGetter getter; + private final ValueGetter getter; - ValueWriter(final ValueGetter getter) { + ValueWriter(final ValueGetter getter) { this.getter = getter; } @@ -546,4 +543,16 @@ public boolean isOmitEmptyFields() { public void setOmitEmptyFields(boolean omitEmptyFields) { this.omitEmptyFields = omitEmptyFields; } + + + @SuppressWarnings("serial") + public static class JsonPatternException extends Exception { + public JsonPatternException(String message, Throwable cause) { + super(message, cause); + } + + public JsonPatternException(String message) { + super(message); + } + } } diff --git a/src/main/java/net/logstash/logback/pattern/AccessEventJsonPatternParser.java b/src/main/java/net/logstash/logback/pattern/AccessEventJsonPatternParser.java index 9ea14cfa..f09988f5 100644 --- a/src/main/java/net/logstash/logback/pattern/AccessEventJsonPatternParser.java +++ b/src/main/java/net/logstash/logback/pattern/AccessEventJsonPatternParser.java @@ -17,8 +17,8 @@ import ch.qos.logback.access.PatternLayout; import ch.qos.logback.access.spi.IAccessEvent; +import ch.qos.logback.core.Context; import ch.qos.logback.core.pattern.PatternLayoutBase; -import ch.qos.logback.core.spi.ContextAware; import com.fasterxml.jackson.core.JsonFactory; /** @@ -26,23 +26,15 @@ */ public class AccessEventJsonPatternParser extends AbstractJsonPatternParser { - public AccessEventJsonPatternParser(final ContextAware contextAware, final JsonFactory jsonFactory) { - super(contextAware, jsonFactory); + public AccessEventJsonPatternParser(final Context context, final JsonFactory jsonFactory) { + super(context, jsonFactory); addOperation("nullNA", new NullNaValueOperation()); } - protected class NullNaValueOperation extends AbstractJsonPatternParser.Operation { - public NullNaValueOperation() { - super(true); - } - + protected class NullNaValueOperation implements Operation { @Override - public ValueGetter createValueGetter(String data) { - return makeLayoutValueGetter(data).andThen(this::convert); - } - - private String convert(final String value) { + public String apply(final String value) { return "-".equals(value) ? null : value; } } @@ -52,5 +44,4 @@ private String convert(final String value) { protected PatternLayoutBase createLayout() { return new PatternLayout(); } - } diff --git a/src/main/java/net/logstash/logback/pattern/LoggingEventJsonPatternParser.java b/src/main/java/net/logstash/logback/pattern/LoggingEventJsonPatternParser.java index a8d8e437..5f254d38 100644 --- a/src/main/java/net/logstash/logback/pattern/LoggingEventJsonPatternParser.java +++ b/src/main/java/net/logstash/logback/pattern/LoggingEventJsonPatternParser.java @@ -17,8 +17,8 @@ import ch.qos.logback.classic.PatternLayout; import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.Context; import ch.qos.logback.core.pattern.PatternLayoutBase; -import ch.qos.logback.core.spi.ContextAware; import com.fasterxml.jackson.core.JsonFactory; /** @@ -26,8 +26,8 @@ */ public class LoggingEventJsonPatternParser extends AbstractJsonPatternParser { - public LoggingEventJsonPatternParser(final ContextAware contextAware, final JsonFactory jsonFactory) { - super(contextAware, jsonFactory); + public LoggingEventJsonPatternParser(final Context context, final JsonFactory jsonFactory) { + super(context, jsonFactory); } @Override diff --git a/src/main/java/net/logstash/logback/pattern/PatternLayoutAdapter.java b/src/main/java/net/logstash/logback/pattern/PatternLayoutAdapter.java index 864a7907..568f18d4 100644 --- a/src/main/java/net/logstash/logback/pattern/PatternLayoutAdapter.java +++ b/src/main/java/net/logstash/logback/pattern/PatternLayoutAdapter.java @@ -15,31 +15,120 @@ */ package net.logstash.logback.pattern; +import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import net.logstash.logback.util.StringUtils; + import ch.qos.logback.core.Context; import ch.qos.logback.core.pattern.Converter; import ch.qos.logback.core.pattern.LiteralConverter; import ch.qos.logback.core.pattern.PatternLayoutBase; import ch.qos.logback.core.pattern.PostCompileProcessor; + /** * Adapter around a {@link PatternLayoutBase} to allow writing the pattern into a supplied {@link StringBuilder} * instead of returning a String. * + * The adapter also throws an {@link IllegalArgumentException} upon start when the configured pattern is not a + * valid pattern layout instead of simply emitting an ERROR status. + * * @author brenuart */ public class PatternLayoutAdapter { + /** + * Regex pattern matching error place holders inserted by PatternLayout when it + * fails to parse the pattern + */ + private static final Pattern ERROR_PATTERN = Pattern.compile("%PARSER_ERROR\\[(.*)\\]"); + + /** + * The wrapped pattern layout instance + */ + private final PatternLayoutBase layout; + /** * The "head" converter of the pattern. * Initialized when the pattern is started. - * Can stay null after the pattern is started when the pattern is empty or invalid. + * Can stay null after the PatternLayout is started when the pattern is empty + * or a parse error occurred. */ private Converter head; - + private boolean headCaptured; + + public PatternLayoutAdapter(PatternLayoutBase layout) { + this.layout = Objects.requireNonNull(layout); + } + + /** + * Set the {@link Context} + * + * @param context the context + */ + public void setContext(Context context) { + this.layout.setContext(context); + } + + /** + * Set the layout pattern + * + * @param pattern the layout pattern + */ + public void setPattern(String pattern) { + this.layout.setPattern(pattern); + } + + + /** + * Start the underlying PatternLayoutBase and throw an {@link IllegalArgumentException} if the + * configured pattern is not a valid PatternLayout. + * + * @throws IllegalArgumentException thrown when the configured pattern is not a valid PatternLayout + */ + public void start() throws IllegalArgumentException { + if (layout.isStarted()) { + throw new IllegalStateException("Layout is already started"); + } + + /* + * Start layout... + */ layout.setPostCompileProcessor(new HeadConverterCapture()); layout.start(); + + /* + * PostCompileProcessor is not invoked when parser fails to parse the + * input or when the pattern is empty... + */ + if (!headCaptured && StringUtils.isEmpty(layout.getPattern())) { + throw new IllegalArgumentException("Failed to parse PatternLayout. See previous error statuses for more information."); + } + + /* + * Detect %PARSER_ERROR[]... + */ + StringBuilder sb = new StringBuilder(); + Converter c = this.head; + while (c != null) { + if (isConstantConverter(c)) { + c.write(sb, null); + + Matcher matcher = ERROR_PATTERN.matcher(sb.toString()); + if (matcher.matches()) { + String conversionWord = matcher.group(1); + throw new IllegalArgumentException("Failed to interpret '%" + conversionWord + "' conversion word. See previous error statuses for more information."); + } + + sb.setLength(0); + } + c = c.getNext(); + } } + /** * Apply the PatternLayout to the event and write result into the supplied {@link StringBuilder}. * @@ -65,12 +154,12 @@ public boolean isConstant() { return true; } - Converter current = this.head; - while (current != null) { - if (!isConstantConverter(current)) { + Converter c = this.head; + while (c != null) { + if (!isConstantConverter(c)) { return false; } - current = current.getNext(); + c = c.getNext(); } return true; } @@ -101,6 +190,7 @@ private class HeadConverterCapture implements PostCompileProcessor { @Override public void process(Context context, Converter head) { PatternLayoutAdapter.this.head = head; + PatternLayoutAdapter.this.headCaptured = true; } } } diff --git a/src/main/java/net/logstash/logback/pattern/ValueGetter.java b/src/main/java/net/logstash/logback/pattern/ValueGetter.java index 533367dd..123d0b1e 100644 --- a/src/main/java/net/logstash/logback/pattern/ValueGetter.java +++ b/src/main/java/net/logstash/logback/pattern/ValueGetter.java @@ -15,8 +15,6 @@ */ package net.logstash.logback.pattern; -import java.util.function.Function; - /** * Computes a value given an event. * @@ -25,7 +23,7 @@ * * @author Dmitry Andrianov */ -public interface ValueGetter { +public interface ValueGetter { /** * Get the result of applying the ValueGetter to the event * @@ -33,25 +31,4 @@ public interface ValueGetter { * @return the result of applying this ValueGetter on the event */ T getValue(Event event); - - - /** - * Returns a composed ValueGetter that first applies this ValueGetter to - * its input, and then applies the {@code after} function to the result. - * If evaluation of either function throws an exception, it is relayed to - * the caller of the composed function. - * - * @param the type of output of the {@code after} function, and of the - * composed ValueGetter - * @param after the function to apply after this ValueGetter is applied - * @return a composed ValueGetter that first applies this ValueGetter and then - * applies the {@code after} function - * @throws NullPointerException if after is null - */ - default ValueGetter andThen(Function after) { - return event -> { - T value = getValue(event); - return after.apply(value); - }; - } } diff --git a/src/test/java/net/logstash/logback/composite/AbstractPatternJsonProviderTest.java b/src/test/java/net/logstash/logback/composite/AbstractPatternJsonProviderTest.java index 07fa155a..d806134a 100644 --- a/src/test/java/net/logstash/logback/composite/AbstractPatternJsonProviderTest.java +++ b/src/test/java/net/logstash/logback/composite/AbstractPatternJsonProviderTest.java @@ -15,82 +15,120 @@ */ package net.logstash.logback.composite; +import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; - -import java.io.IOException; +import static org.mockito.Mockito.verifyNoInteractions; import net.logstash.logback.pattern.AbstractJsonPatternParser; +import net.logstash.logback.pattern.AbstractJsonPatternParser.JsonPatternException; import net.logstash.logback.pattern.NodeWriter; +import net.logstash.logback.test.AbstractLogbackTest; import ch.qos.logback.core.spi.DeferredProcessingAware; +import ch.qos.logback.core.status.Status; import com.fasterxml.jackson.core.JsonFactory; import com.fasterxml.jackson.core.JsonGenerator; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -/** - * This test just verifies that AbstractPatternJsonProvider delegates all the work to Parser. - * - * @param - type of the event (ILoggingEvent, IAccessEvent) - * - * @author Dmitry Andrianov - */ -@ExtendWith(MockitoExtension.class) -public abstract class AbstractPatternJsonProviderTest { - // What our TestNodeWriter generates when invoked - public static final String TEST_NODEWRITER_RESULT = "generated string"; +public class AbstractPatternJsonProviderTest extends AbstractLogbackTest { @Mock - private Event event; - - @Mock - private JsonGenerator generator; + private DeferredProcessingAware event; @Mock private JsonFactory jsonFactory; - private AbstractJsonPatternParser parser; - @Mock - private NodeWriter nodeWriter; + private AbstractJsonPatternParser parser; - private AbstractPatternJsonProvider provider; + private AbstractPatternJsonProvider provider; + @BeforeEach - public void setUp() throws Exception { - provider = createProvider(); - } - - protected abstract AbstractPatternJsonProvider createProvider(); - - protected AbstractJsonPatternParser decorateParser(AbstractJsonPatternParser parser) { - this.parser = spy(parser); - doReturn(nodeWriter).when(this.parser).parse(anyString()); - return this.parser; + public void setup() throws Exception { + super.setup(); + + provider = new AbstractPatternJsonProvider() { + @Override + protected AbstractJsonPatternParser createParser(JsonFactory jsonFactory) { + return parser; + } + }; + provider.setContext(context); + provider.setJsonFactory(jsonFactory); } + + /* + * Verify that AbstractPatternJsonProvider delegates all the work to Parser. + */ @Test - public void shouldDelegateToParser() throws IOException { - // pattern used does not matter because decorated "parser" will always generate TEST_NODEWRITER_RESULT - final String pattern = "{\"key\":\"value\"}"; + public void shouldDelegateToParser() throws Exception { + /* + * Configure the Parser to returned a mocked NodeWriter + */ + @SuppressWarnings("unchecked") + NodeWriter nodeWriter = mock(NodeWriter.class); + doReturn(nodeWriter).when(this.parser).parse(anyString()); + + JsonGenerator generator = mock(JsonGenerator.class); + + /* + * Configure and start the provider + */ + final String pattern = "does not matter"; // pattern does not actually matter since we mocked the Parser... provider.setPattern(pattern); - provider.setJsonFactory(jsonFactory); + provider.setOmitEmptyFields(true); provider.start(); // should actually invoke parser with the pattern requested + verify(parser).setOmitEmptyFields(true); verify(parser).parse(pattern); provider.writeTo(generator, event); // and the end result should be what NodeWriter returned by the parser produces verify(nodeWriter).write(generator, event); - + } + + + /* + * Test behavior when provider is configured with an invalid pattern. + * Expected: + * - logs an ERROR + * - does not output anything at runtime + */ + @Test + public void invalidConfiguration() throws Exception { + /* + * Make the Parser throw a JsonPatternException to simulate an invalid pattern + */ + doThrow(new JsonPatternException("Invalid pattern")).when(parser).parse(anyString()); + + /* + * Configure and start the provider + */ + final String pattern = "does not matter"; // pattern does not actually matter since we mocked the Parser... + provider.setPattern(pattern); + provider.start(); + + // Provider is started even when pattern is invalid + assertThat(provider.isStarted()).isTrue(); + + // An ERROR status describing the problem is emitted + assertThat(statusManager.getCopyOfStatusList()).anyMatch(status -> + status.getLevel() == Status.ERROR + && status.getMessage().startsWith("Invalid [pattern]") + && status.getOrigin() == provider); + + // Provider output "nothing" after a configuration error + verifyNoInteractions(jsonFactory); } } diff --git a/src/test/java/net/logstash/logback/composite/accessevent/AccessEventPatternJsonProviderTest.java b/src/test/java/net/logstash/logback/composite/accessevent/AccessEventPatternJsonProviderTest.java deleted file mode 100644 index efe415ef..00000000 --- a/src/test/java/net/logstash/logback/composite/accessevent/AccessEventPatternJsonProviderTest.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2013-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package net.logstash.logback.composite.accessevent; - -import net.logstash.logback.composite.AbstractPatternJsonProvider; -import net.logstash.logback.composite.AbstractPatternJsonProviderTest; -import net.logstash.logback.pattern.AbstractJsonPatternParser; - -import ch.qos.logback.access.spi.IAccessEvent; -import com.fasterxml.jackson.core.JsonFactory; - -/** - * @author Dmitry Andrianov - */ -public class AccessEventPatternJsonProviderTest extends AbstractPatternJsonProviderTest { - - @Override - protected AbstractPatternJsonProvider createProvider() { - return new AccessEventPatternJsonProvider() { - @Override - protected AbstractJsonPatternParser createParser(JsonFactory jsonFactory) { - // The base class needs to hook into the parser for some assertions - return decorateParser(super.createParser(jsonFactory)); - } - }; - } -} diff --git a/src/test/java/net/logstash/logback/composite/loggingevent/LoggingEventPatternJsonProviderTest.java b/src/test/java/net/logstash/logback/composite/loggingevent/LoggingEventPatternJsonProviderTest.java deleted file mode 100644 index 20ea1f72..00000000 --- a/src/test/java/net/logstash/logback/composite/loggingevent/LoggingEventPatternJsonProviderTest.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2013-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package net.logstash.logback.composite.loggingevent; - -import net.logstash.logback.composite.AbstractPatternJsonProvider; -import net.logstash.logback.composite.AbstractPatternJsonProviderTest; -import net.logstash.logback.pattern.AbstractJsonPatternParser; - -import ch.qos.logback.classic.spi.ILoggingEvent; -import com.fasterxml.jackson.core.JsonFactory; - -/** - * @author Dmitry Andrianov - */ -public class LoggingEventPatternJsonProviderTest extends AbstractPatternJsonProviderTest { - - @Override - protected AbstractPatternJsonProvider createProvider() { - return new LoggingEventPatternJsonProvider() { - @Override - protected AbstractJsonPatternParser createParser(JsonFactory jsonFactory) { - // The base class needs to hook into the parser for some assertions - return decorateParser(super.createParser(jsonFactory)); - } - }; - } -} diff --git a/src/test/java/net/logstash/logback/pattern/AbstractJsonPatternParserTest.java b/src/test/java/net/logstash/logback/pattern/AbstractJsonPatternParserTest.java index e2665055..905deb36 100644 --- a/src/test/java/net/logstash/logback/pattern/AbstractJsonPatternParserTest.java +++ b/src/test/java/net/logstash/logback/pattern/AbstractJsonPatternParserTest.java @@ -16,68 +16,60 @@ package net.logstash.logback.pattern; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.BDDMockito.given; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import java.io.IOException; import java.io.StringWriter; import java.util.Map; +import net.logstash.logback.pattern.AbstractJsonPatternParser.JsonPatternException; +import net.logstash.logback.test.AbstractLogbackTest; + import ch.qos.logback.core.Context; -import ch.qos.logback.core.spi.ContextAware; import com.fasterxml.jackson.core.JsonFactory; import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.MappingJsonFactory; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; /** * @author Dmitry Andrianov */ @ExtendWith(MockitoExtension.class) -public abstract class AbstractJsonPatternParserTest { - - @Mock(lenient = true) - protected ContextAware contextAware; - - protected JsonFactory jsonFactory; - - private JsonGenerator jsonGenerator; +public abstract class AbstractJsonPatternParserTest extends AbstractLogbackTest { - private StringWriter buffer = new StringWriter(); + private JsonFactory jsonFactory = new MappingJsonFactory(); private Event event; protected AbstractJsonPatternParser parser; - @Mock - private Context context; @BeforeEach - public void setUp() throws Exception { - - event = createEvent(); - - given(contextAware.getContext()).willReturn(context); - - jsonFactory = new MappingJsonFactory(); - jsonGenerator = jsonFactory.createGenerator(buffer); - - parser = createParser(); + public void setup() throws Exception { + super.setup(); + + this.event = createEvent(); + this.parser = createParser(context, jsonFactory); } protected abstract Event createEvent(); - protected abstract AbstractJsonPatternParser createParser(); + protected abstract AbstractJsonPatternParser createParser(Context context, JsonFactory jsonFactory); - private Map parseJson(final String text) throws IOException { - return jsonFactory.createParser(text).readValueAs(new TypeReference>() { }); + private Map parseJson(final String text) { + try (JsonParser jsonParser = jsonFactory.createParser(text)) { + return jsonParser.readValueAs(new TypeReference>() { }); + } catch (IOException e) { + throw new IllegalStateException("Unexpected IOException while writing to the JsonGenerator"); + } } - protected void verifyFields(String patternJson, String expectedJson) throws IOException { + protected void verifyFields(String patternJson, String expectedJson) throws JsonPatternException { Map actualResult = parseJson(process(patternJson)); Map expectedResult = parseJson(expectedJson); @@ -85,329 +77,349 @@ protected void verifyFields(String patternJson, String expectedJson) throws IOEx assertThat(actualResult).isEqualTo(expectedResult); } - private String process(final String patternJson) throws IOException { + private String process(final String patternJson) throws JsonPatternException { NodeWriter root = parser.parse(patternJson); assertThat(root).isNotNull(); - jsonGenerator.writeStartObject(); - root.write(jsonGenerator, event); - jsonGenerator.writeEndObject(); - jsonGenerator.flush(); - + StringWriter buffer = new StringWriter(); + + try (JsonGenerator jsonGenerator = jsonFactory.createGenerator(buffer)) { + jsonGenerator.writeStartObject(); + root.write(jsonGenerator, event); + jsonGenerator.writeEndObject(); + jsonGenerator.flush(); + + } catch (IOException e) { + throw new IllegalStateException("Unexpected IOException while writing to the JsonGenerator", e); + } + return buffer.toString(); } @Test - public void shouldKeepPrimitiveConstantValues() throws IOException { - - String pattern = "" - + "{\n" - + " \"const\": null,\n" - + " \"string\": \"value\",\n" - + " \"integer\": 1024,\n" - + " \"double\": 0.1,\n" - + " \"bool\": false\n" - + "}"; - - String expected = "" - + "{\n" - + " \"const\": null,\n" - + " \"string\": \"value\",\n" - + " \"integer\": 1024,\n" - + " \"double\": 0.1,\n" - + " \"bool\": false\n" - + "}"; + public void shouldKeepPrimitiveConstantValues() throws Exception { + + String pattern = toJson( + "{ " + + " 'const' : null, " + + " 'string' : 'value', " + + " 'integer': 1024, " + + " 'double' : 0.1, " + + " 'bool' : false " + + "} "); + + String expected = toJson( + "{ " + + " 'const' : null, " + + " 'string' : 'value', " + + " 'integer': 1024, " + + " 'double' : 0.1, " + + " 'bool' : false " + + "} "); verifyFields(pattern, expected); } @Test - public void shouldAllowUsingArraysAsValues() throws IOException { - - String pattern = "" - + "{\n" - + " \"list\": [\n" - + " \"value\",\n" - + " 100,\n" - + " 0.33,\n" - + " true\n" - + " ]\n" - + "}"; - - String expected = "" - + "{\n" - + " \"list\": [\n" - + " \"value\",\n" - + " 100,\n" - + " 0.33,\n" - + " true\n" - + " ]\n" - + "}"; + public void shouldAllowUsingArraysAsValues() throws Exception { + + String pattern = toJson( + "{ " + + " 'list': [ " + + " 'value', " + + " 100, " + + " 0.33, " + + " true " + + " ] " + + "} "); + + String expected = toJson( + "{ " + + " 'list': [ " + + " 'value', " + + " 100, " + + " 0.33, " + + " true " + + " ] " + + "} "); verifyFields(pattern, expected); } @Test - public void shouldAllowUsingObjectsAsValues() throws IOException { - - String pattern = "" - + "{\n" - + " \"map\": {\n" - + " \"string\": \"value\",\n" - + " \"int\": 100,\n" - + " \"double\": 0.33,\n" - + " \"bool\": true\n" - + " }\n" - + "}"; - - String expected = "" - + "{\n" - + " \"map\": {\n" - + " \"string\": \"value\",\n" - + " \"int\": 100,\n" - + " \"double\": 0.33,\n" - + " \"bool\": true\n" - + " }\n" - + "}"; + public void shouldAllowUsingObjectsAsValues() throws Exception { + + String pattern = toJson( + "{ " + + " 'map': { " + + " 'string': 'value', " + + " 'int' : 100, " + + " 'double': 0.33, " + + " 'bool' : true " + + " } " + + "} "); + + String expected = toJson( + "{ " + + " 'map': { " + + " 'string': 'value', " + + " 'int' : 100, " + + " 'double': 0.33, " + + " 'bool' : true " + + " } " + + "} "); verifyFields(pattern, expected); } @Test - public void asLongShouldTransformTextValueToLong() throws IOException { + public void asLongShouldTransformTextValueToLong() throws Exception { - String pattern = "" - + "{\n" - + " \"key\": \"#asLong{555}\"\n" - + "}"; + String pattern = toJson( + "{ 'key': '#asLong{555}' }"); - String expected = "" - + "{\n" - + " \"key\": 555\n" - + "}"; + String expected = toJson( + "{ 'key': 555 }"); verifyFields(pattern, expected); } @Test - public void asDoubleShouldTransformTextValueToDouble() throws IOException { + public void asDoubleShouldTransformTextValueToDouble() throws Exception { - String pattern = "" - + "{\n" - + " \"key\": \"#asDouble{0.5}\"\n" - + "}"; + String pattern = toJson( + "{ 'key': '#asDouble{0.5}' }"); - String expected = "" - + "{\n" - + " \"key\": 0.5\n" - + "}"; + String expected = toJson( + "{ 'key': 0.5 }"); verifyFields(pattern, expected); } @Test - public void asJsonShouldTransformTextValueToJson() throws IOException { - - String pattern = "" - + "{\n" - + " \"key1\": \"#asJson{true}\",\n" - + " \"key2\": \"#asJson{123}\",\n" - + " \"key3\": \"#asJson{123.4}\",\n" - + " \"key4\": \"#asJson{\\\"123\\\"}\",\n" - + " \"key5\": \"#asJson{[1, 2]}\",\n" - + " \"key6\": \"#asJson{[1, \\\"2\\\"]}\",\n" - + " \"key7\": \"#asJson{{\\\"field\\\":\\\"value\\\"}}\",\n" - + " \"key8\": \"#asJson{{\\\"field\\\":\\\"value\\\",\\\"num\\\":123}}\",\n" - + " \"key9\": \"#asJson{one two three}\",\n" - + " \"key10\": \"#asJson{1 suffix}\"\n" - + "}"; - - String expected = "" - + "{\n" - + " \"key1\": true,\n" - + " \"key2\": 123,\n" - + " \"key3\": 123.4,\n" - + " \"key4\": \"123\",\n" - + " \"key5\": [1, 2],\n" - + " \"key6\": [1, \"2\"],\n" - + " \"key7\": {\"field\":\"value\"},\n" - + " \"key8\": {\"field\":\"value\", \"num\":123},\n" - + " \"key9\": null,\n" - + " \"key10\": null\n" - + "}"; + public void asJsonShouldTransformTextValueToJson() throws Exception { + + String pattern = toJson( + "{ " + + " 'key1' : '#asJson{true}', " + + " 'key2' : '#asJson{123}', " + + " 'key3' : '#asJson{123.4}', " + + " 'key4' : '#asJson{\\'123\\'}', " + + " 'key5' : '#asJson{[1, 2]}', " + + " 'key6' : '#asJson{[1, \\'2\\']}', " + + " 'key7' : '#asJson{{\\'field\\':\\'value\\'}}', " + + " 'key8' : '#asJson{{\\'field\\':\\'value\\',\\'num\\':123}}', " + + " 'key9' : '#asJson{one two three}', " + + " 'key10': '#asJson{1 suffix}' " + + "} "); + + String expected = toJson( + "{ " + + " 'key1' : true, " + + " 'key2' : 123, " + + " 'key3' : 123.4, " + + " 'key4' : '123', " + + " 'key5' : [1, 2], " + + " 'key6' : [1, '2'], " + + " 'key7' : {'field':'value'}, " + + " 'key8' : {'field':'value', 'num':123}, " + + " 'key9' : null, " + + " 'key10': null " + + "} "); verifyFields(pattern, expected); } @Test - public void tryJsonShouldTransformTextValueToJson() throws IOException { - - String pattern = "" - + "{\n" - + " \"key1\": \"#tryJson{true}\",\n" - + " \"key2\": \"#tryJson{123}\",\n" - + " \"key3\": \"#tryJson{123.4}\",\n" - + " \"key4\": \"#tryJson{\\\"123\\\"}\",\n" - + " \"key5\": \"#tryJson{[1, 2]}\",\n" - + " \"key6\": \"#tryJson{[1, \\\"2\\\"]}\",\n" - + " \"key7\": \"#tryJson{{\\\"field\\\":\\\"value\\\"}}\",\n" - + " \"key8\": \"#tryJson{{\\\"field\\\":\\\"value\\\",\\\"num\\\":123}}\",\n" - + " \"key9\": \"#tryJson{{\\\"field\\\":\\\"value\\\"} extra}\",\n" - + " \"key10\": \"#tryJson{one two three}\",\n" - + " \"key11\": \"#tryJson{ false }\",\n" - + " \"key12\": \"#tryJson{ false true}\",\n" - + " \"key13\": \"#tryJson{123 foo}\",\n" - + " \"key14\": \"#tryJson{ 123 }\"\n" - + "}"; - - String expected = "" - + "{\n" - + " \"key1\": true,\n" - + " \"key2\": 123,\n" - + " \"key3\": 123.4,\n" - + " \"key4\": \"123\",\n" - + " \"key5\": [1, 2],\n" - + " \"key6\": [1, \"2\"],\n" - + " \"key7\": {\"field\":\"value\"},\n" - + " \"key8\": {\"field\":\"value\", \"num\":123},\n" - + " \"key9\": \"{\\\"field\\\":\\\"value\\\"} extra\",\n" - + " \"key10\": \"one two three\",\n" - + " \"key11\": false,\n" - + " \"key12\": \" false true\",\n" - + " \"key13\": \"123 foo\",\n" - + " \"key14\": 123\n" - + "}"; + public void tryJsonShouldTransformTextValueToJson() throws Exception { + + String pattern = toJson( + "{ " + + " 'key1' : '#tryJson{true}', " + + " 'key2' : '#tryJson{123}', " + + " 'key3' : '#tryJson{123.4}', " + + " 'key4' : '#tryJson{\\'123\\'}', " + + " 'key5' : '#tryJson{[1, 2]}', " + + " 'key6' : '#tryJson{[1, \\'2\\']}', " + + " 'key7' : '#tryJson{{\\'field\\':\\'value\\'}}', " + + " 'key8' : '#tryJson{{\\'field\\':\\'value\\',\\'num\\':123}}', " + + " 'key9' : '#tryJson{{\\'field\\':\\'value\\'} extra}', " + + " 'key10': '#tryJson{one two three}', " + + " 'key11': '#tryJson{ false }', " + + " 'key12': '#tryJson{ false true}', " + + " 'key13': '#tryJson{123 foo}', " + + " 'key14': '#tryJson{ 123 }' " + + "} "); + + String expected = toJson( + "{ " + + " 'key1' : true, " + + " 'key2' : 123, " + + " 'key3' : 123.4, " + + " 'key4' : '123', " + + " 'key5' : [1, 2], " + + " 'key6' : [1, '2'], " + + " 'key7' : {'field':'value'}, " + + " 'key8' : {'field':'value', 'num':123}, " + + " 'key9' : '{\\'field\\':\\'value\\'} extra', " + + " 'key10': 'one two three', " + + " 'key11': false, " + + " 'key12': ' false true', " + + " 'key13': '123 foo', " + + " 'key14': 123 " + + "} "); verifyFields(pattern, expected); } @Test - public void shouldSendNonTransformableValuesAsNulls() throws IOException { - - String pattern = "" - + "{\n" - + " \"key1\": \"#asLong{abc}\",\n" - + " \"key2\": \"test\",\n" - + " \"key3\": \"#asDouble{abc}\",\n" - + " \"key4\": \"#asJson{[1, 2}\"\n" - + "}"; - - String expected = "" - + "{\n" - + " \"key1\": null,\n" - + " \"key2\": \"test\",\n" - + " \"key3\": null,\n" - + " \"key4\": null\n" - + "}"; + public void shouldSendNonTransformableValuesAsNulls() throws Exception { + + String pattern = toJson( + "{ " + + " 'key1': '#asLong{abc}', " + + " 'key2': 'test', " + + " 'key3': '#asDouble{abc}', " + + " 'key4': '#asJson{[1, 2}' " + + "} "); + + String expected = toJson( + "{ " + + " 'key1': null, " + + " 'key2': 'test', " + + " 'key3': null, " + + " 'key4': null " + + "} "); verifyFields(pattern, expected); } @Test - public void shouldKeepUnrecognisedOrInvalidOperationsAsStringLiterals() throws IOException { - - String pattern = "" - + "{\n" - + " \"key1\": \"#asDouble{0\",\n" - + " \"key2\": \"#asDouble\",\n" - + " \"key3\": \"#something\",\n" - + " \"key4\": \"#asJson{[1, 2]\"\n" - + "}"; - - String expected = "" - + "{\n" - + " \"key1\": \"#asDouble{0\",\n" - + " \"key2\": \"#asDouble\",\n" - + " \"key3\": \"#something\",\n" - + " \"key4\": \"#asJson{[1, 2]\"\n" - + "}"; + public void shouldKeepUnrecognisedOrInvalidOperationsAsStringLiterals() throws Exception { + + String pattern = toJson( + "{ " + + " 'key1': '#asDouble{0', " + + " 'key2': '#asDouble', " + + " 'key3': '#something', " + + " 'key4': '#asJson{[1, 2]' " + + "} "); + + String expected = toJson( + "{ " + + " 'key1': '#asDouble{0', " + + " 'key2': '#asDouble', " + + " 'key3': '#something', " + + " 'key4': '#asJson{[1, 2]' " + + "} "); verifyFields(pattern, expected); } @Test - public void shouldOmitNullConstants() throws IOException { + public void shouldOmitNullConstants() throws Exception { parser.setOmitEmptyFields(true); - String pattern = "" - + "{\n" - + " \"const\": null\n" - + "}"; + String pattern = toJson( + "{ 'const': null }"); - String expected = "" - + "{\n" - + "}"; + String expected = toJson( + "{}"); verifyFields(pattern, expected); } @Test - public void shouldOmitEmptyConstants() throws IOException { + public void shouldOmitEmptyConstants() throws Exception { parser.setOmitEmptyFields(true); - String pattern = "" - + "{\n" - + " \"string\": \"\",\n" - + " \"list\": [\n" - + " ],\n" - + " \"map\": {\n" - + " }\n" - + "}"; + String pattern = toJson( + "{ " + + " 'string': '', " + + " 'list' : [], " + + " 'map' : {} " + + "} "); - String expected = "" - + "{\n" - + "}"; + String expected = toJson("{}"); verifyFields(pattern, expected); } @Test - public void shouldOmitEmptyJsonValues() throws IOException { + public void shouldOmitEmptyJsonValues() throws Exception { parser.setOmitEmptyFields(true); - String pattern = "" - + "{\n" - + " \"null\": \"#asJson{null}\",\n" - + " \"string\": \"#asJson{}\",\n" - + " \"list\": \"#asJson{[]}\",\n" - + " \"object\": \"#asJson{{}}\"\n" - + "}"; - - String expected = "" - + "{\n" - + "}"; + String pattern = toJson( + "{ " + + " 'null' : '#asJson{null}', " + + " 'string:': '#asJson{}', " + + " 'list' : '#asJson{[]}', " + + " 'object' : '#asJson{{}}' " + + "} "); + + String expected = toJson("{}"); verifyFields(pattern, expected); } @Test - public void shouldOmitEmptyConstantsRecursively() throws IOException { + public void shouldOmitEmptyConstantsRecursively() throws JsonPatternException { parser.setOmitEmptyFields(true); - String pattern = "" - + "{\n" - + " \"object\": {\n" - + " \"string\": \"\",\n" - + " \"list\": [\n" - + " ],\n" - + " \"map\": {\n" - + " }\n" - + " },\n" - + " \"list\": [\n" - + " {\n" - + " \"string\": \"\",\n" - + " \"list\": [\n" - + " ],\n" - + " \"map\": {\n" - + " }\n" - + " }\n" - + " ]\n" - + "}"; - - String expected = "" - + "{\n" - + "}"; + String pattern = toJson( + "{ " + + " 'object': { " + + " 'string': '', " + + " 'list' : [], " + + " 'map' : {} " + + " }, " + + " 'list': [ " + + " { " + + " 'string': '', " + + " 'list' : [], " + + " 'map' : {} " + + " } " + + " ] " + + "} "); + + String expected = toJson("{}"); verifyFields(pattern, expected); } + + @Test + public void unknownOperation() { + assertThatExceptionOfType(JsonPatternException.class).isThrownBy(() -> parser.parse(toJson("{'msg':'#unknown{foo}"))); + } + + @Test + public void invalidPatternLayout() { + assertThatExceptionOfType(JsonPatternException.class).isThrownBy(() -> parser.parse(toJson("{'msg':'%foo'}"))); + } + + @Test + public void invalidJSON() throws JsonPatternException { + parser.parse(toJson("{'msg' : '#asJson{foo}' }")); + assertThatExceptionOfType(JsonPatternException.class).isThrownBy(() -> parser.parse(toJson("{'msg' = #asJson{foo} }"))); + } + + @Test + public void escapePattern() throws JsonPatternException { + String pattern = toJson( + "{ " + + " 'key1': '\\\\#asLong{1}' " + + "} "); + + String expected = toJson( + "{ " + + " 'key1': '#asLong{1}' " + + "} "); + + verifyFields(pattern, expected); + } + + + protected static String toJson(String str) { + return str.replace("'", "\""); + } } diff --git a/src/test/java/net/logstash/logback/pattern/AccessEventJsonPatternParserTest.java b/src/test/java/net/logstash/logback/pattern/AccessEventJsonPatternParserTest.java index c686839a..05413227 100644 --- a/src/test/java/net/logstash/logback/pattern/AccessEventJsonPatternParserTest.java +++ b/src/test/java/net/logstash/logback/pattern/AccessEventJsonPatternParserTest.java @@ -17,9 +17,9 @@ import static org.mockito.BDDMockito.given; -import java.io.IOException; - import ch.qos.logback.access.spi.IAccessEvent; +import ch.qos.logback.core.Context; +import com.fasterxml.jackson.core.JsonFactory; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; @@ -42,40 +42,40 @@ protected IAccessEvent createEvent() { } @Override - protected AbstractJsonPatternParser createParser() { - return new AccessEventJsonPatternParser(contextAware, jsonFactory); + protected AbstractJsonPatternParser createParser(Context context, JsonFactory jsonFactory) { + return new AccessEventJsonPatternParser(context, jsonFactory); } @Test - public void shouldRunPatternLayoutConversions() throws IOException { + public void shouldRunPatternLayoutConversions() throws Exception { - String pattern = "" - + "{\n" - + " \"level\": \"%requestMethod\"\n" - + "}"; + String pattern = toJson( + "{ " + + " 'level': '%requestMethod' " + + "} "); - String expected = "" - + "{\n" - + " \"level\": \"PUT\"\n" - + "}"; + String expected = toJson( + "{ " + + " 'level': 'PUT' " + + "} "); verifyFields(pattern, expected); } @Test - public void noNaOperationShouldNullifySingleDash() throws IOException { + public void noNaOperationShouldNullifySingleDash() throws Exception { - String pattern = "" - + "{\n" - + " \"cookie1\": \"%requestAttribute{MISSING}\",\n" - + " \"cookie2\": \"#nullNA{%requestAttribute{MISSING}}\"\n" - + "}"; + String pattern = toJson( + "{ " + + " 'cookie1': '%requestAttribute{MISSING}', " + + " 'cookie2': '#nullNA{%requestAttribute{MISSING}}' " + + "} "); - String expected = "" - + "{\n" - + " \"cookie1\": \"-\",\n" - + " \"cookie2\": null\n" - + "}"; + String expected = toJson( + "{ " + + " 'cookie1': '-', " + + " 'cookie2': null " + + "} "); verifyFields(pattern, expected); } diff --git a/src/test/java/net/logstash/logback/pattern/LoggingEventJsonPatternParserTest.java b/src/test/java/net/logstash/logback/pattern/LoggingEventJsonPatternParserTest.java index 4d8b41e8..79e90fc5 100644 --- a/src/test/java/net/logstash/logback/pattern/LoggingEventJsonPatternParserTest.java +++ b/src/test/java/net/logstash/logback/pattern/LoggingEventJsonPatternParserTest.java @@ -17,12 +17,13 @@ import static org.mockito.BDDMockito.given; -import java.io.IOException; import java.util.HashMap; import java.util.Map; import ch.qos.logback.classic.Level; import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.Context; +import com.fasterxml.jackson.core.JsonFactory; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; @@ -49,56 +50,56 @@ protected ILoggingEvent createEvent() { } @Override - protected AbstractJsonPatternParser createParser() { - return new LoggingEventJsonPatternParser(contextAware, jsonFactory); + protected AbstractJsonPatternParser createParser(Context context, JsonFactory jsonFactory) { + return new LoggingEventJsonPatternParser(context, jsonFactory); } @Test - public void shouldRunPatternLayoutConversions() throws IOException { + public void shouldRunPatternLayoutConversions() throws Exception { - String pattern = "" - + "{\n" - + " \"level\": \"%level\"\n" - + "}"; + String pattern = toJson( + "{ " + + " 'level': '%level' " + + "} "); - String expected = "" - + "{\n" - + " \"level\": \"DEBUG\"\n" - + "}"; + String expected = toJson( + "{ " + + " 'level': 'DEBUG' " + + "} "); verifyFields(pattern, expected); } @Test - public void shouldAllowIndividualMdcItemsToBeIncludedUsingConverter() throws IOException { + public void shouldAllowIndividualMdcItemsToBeIncludedUsingConverter() throws Exception { - String pattern = "" - + "{\n" - + " \"mdc.key1\": \"%mdc{key1}\"\n" - + "}"; + String pattern = toJson( + "{ " + + " 'mdc.key1': '%mdc{key1}' " + + "} "); - String expected = "" - + "{\n" - + " \"mdc.key1\": \"value1\"\n" - + "}"; + String expected = toJson( + "{ " + + " 'mdc.key1': 'value1' " + + "} "); verifyFields(pattern, expected); } @Test - public void shouldOmitNullMdcValue() throws IOException { + public void shouldOmitNullMdcValue() throws Exception { parser.setOmitEmptyFields(true); - String pattern = "" - + "{\n" - + " \"mdc.key1\": \"%mdc{key1}\",\n" - + " \"mdc.key3\": \"%mdc{key3}\"\n" - + "}"; + String pattern = toJson( + "{ " + + " 'mdc.key1': '%mdc{key1}', " + + " 'mdc.key3': '%mdc{key3}' " + + "} "); - String expected = "" - + "{\n" - + " \"mdc.key1\": \"value1\"\n" - + "}"; + String expected = toJson( + "{ " + + " 'mdc.key1': 'value1' " + + "} "); verifyFields(pattern, expected); }