{
11 |
12 | /**
13 | * Tells the formatter that the field should be rendered with the value only, i.e. "value" and not
14 | * "name=value".
15 | *
16 | * @return valueOnly field
17 | */
18 | @NotNull
19 | F asValueOnly();
20 |
21 | /**
22 | * Tells the formatter that this field should be elided in text.
23 | *
24 | * @return field with elide attribute set
25 | */
26 | @NotNull
27 | F asElided();
28 |
29 | /**
30 | * Tells the formatter to render a display name in text.
31 | *
32 | * @return the field with displayName set
33 | */
34 | @NotNull
35 | F withDisplayName(@NotNull String displayName);
36 |
37 | /**
38 | * Tells the structured output to use the output generated by the field visitor.
39 | *
40 | * @param fieldVisitor the visitor generating the JSON specific output.
41 | * @return the field with the visitor set
42 | */
43 | @NotNull
44 | F withStructuredFormat(@NotNull FieldVisitor fieldVisitor);
45 |
46 | /**
47 | * Tells the ToStringFormatter to use the output generated by the field visitor.
48 | *
49 | * @param fieldVisitor the visitor generating the toString specific output.
50 | * @return the field with the visitor set
51 | */
52 | @NotNull
53 | F withToStringFormat(@NotNull FieldVisitor fieldVisitor);
54 | }
55 |
--------------------------------------------------------------------------------
/api/src/main/java/echopraxia/api/ToStringFormatter.java:
--------------------------------------------------------------------------------
1 | package echopraxia.api;
2 |
3 | import org.jetbrains.annotations.NotNull;
4 |
5 | /**
6 | * This is the text formaatter interface that handles the "logfmt" like text serialization of fields
7 | * and values.
8 | *
9 | * @since 3.0
10 | */
11 | public interface ToStringFormatter {
12 |
13 | static ToStringFormatter getInstance() {
14 | return DefaultToStringFormatter.getInstance();
15 | }
16 |
17 | /**
18 | * Formats a field, applying attributes to the field name and value as needed.
19 | *
20 | * This method is called by field.toString().
21 | *
22 | * @return a field formatted in text format.
23 | */
24 | @NotNull
25 | String formatField(@NotNull Field field);
26 |
27 | /**
28 | * Formats a value, without any attributes applied.
29 | *
30 | *
This method is called by value.toString().
31 | *
32 | * @return a value formatted in text format.
33 | */
34 | @NotNull
35 | String formatValue(@NotNull Value> value);
36 | }
37 |
--------------------------------------------------------------------------------
/api/src/main/resources/echopraxia/fields.properties:
--------------------------------------------------------------------------------
1 | exception=exception
2 | className=className
3 | message=message
4 | cause=cause
5 | stackTrace=stackTrace
6 | fileName=fileName
7 | lineNumber=lineNumber
8 | methodName=methodName
--------------------------------------------------------------------------------
/api/src/test/java/echopraxia/api/AbbreviateAfterTests.java:
--------------------------------------------------------------------------------
1 | package echopraxia.api;
2 |
3 | import static org.assertj.core.api.Assertions.assertThat;
4 |
5 | import org.junit.jupiter.api.Test;
6 |
7 | public class AbbreviateAfterTests {
8 |
9 | @Test
10 | void testStringWithAbbreviateAfter() {
11 | var array = Value.string("123456789");
12 | Attribute afterTwo = PresentationHintAttributes.abbreviateAfter(2);
13 | var abbrValue = array.withAttributes(Attributes.create(afterTwo));
14 | assertThat(abbrValue).hasToString("12...");
15 | }
16 |
17 | @Test
18 | void testArrayWithAbbreviateAfter() {
19 | var array = Value.array("one", "two", "three");
20 | Attribute afterTwo = PresentationHintAttributes.abbreviateAfter(2);
21 | var abbrValue = array.withAttributes(Attributes.create(afterTwo));
22 | assertThat(abbrValue).hasToString("[one, two...]");
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/api/src/test/java/echopraxia/api/AbbreviationTests.java:
--------------------------------------------------------------------------------
1 | package echopraxia.api;
2 |
3 | import static org.assertj.core.api.Assertions.assertThat;
4 |
5 | import org.junit.jupiter.api.Test;
6 |
7 | public class AbbreviationTests {
8 |
9 | @Test
10 | public void abbreviateString() {
11 | Value value = Value.string("123456789").abbreviateAfter(5);
12 | Field field = Field.keyValue("longString", value);
13 | assertThat(field.toString()).isEqualTo("longString=12345...");
14 | }
15 |
16 | @Test
17 | public void abbreviateStringWithExtended() {
18 | Value value = Value.string("123456789").abbreviateAfter(5);
19 | Field field = Field.keyValue("longString", value);
20 | assertThat(field.toString()).isEqualTo("longString=12345...");
21 | }
22 |
23 | @Test
24 | public void abbreviateStringWithValueOnly() {
25 | Value value = Value.string("123456789").abbreviateAfter(5);
26 | Field field = Field.value("longString", value);
27 | assertThat(field.toString()).isEqualTo("12345...");
28 | }
29 |
30 | @Test
31 | public void abbreviateShortString() {
32 | Value value = Value.string("12345").abbreviateAfter(5);
33 | Field field = Field.keyValue("longString", value);
34 | assertThat(field.toString()).isEqualTo("longString=12345");
35 | }
36 |
37 | @Test
38 | public void abbreviateArray() {
39 | Value> value = Value.array(1, 2, 3, 4, 5, 6, 7, 8, 9).abbreviateAfter(5);
40 | Field field = Field.keyValue("longArray", value);
41 | assertThat(field.toString()).isEqualTo("longArray=[1, 2, 3, 4, 5...]");
42 | }
43 |
44 | @Test
45 | public void abbreviateArrayWithValueOnly() {
46 | Value> value = Value.array(1, 2, 3, 4, 5, 6, 7, 8, 9).abbreviateAfter(5);
47 | Field field = Field.value("longArray", value);
48 | assertThat(field.toString()).isEqualTo("[1, 2, 3, 4, 5...]");
49 | }
50 |
51 | @Test
52 | public void abbreviateShortArray() {
53 | Value> value = Value.array(1, 2, 3, 4, 5).abbreviateAfter(5);
54 | Field field = Field.keyValue("longArray", value);
55 | assertThat(field.toString()).isEqualTo("longArray=[1, 2, 3, 4, 5]");
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/api/src/test/java/echopraxia/api/CardinalTests.java:
--------------------------------------------------------------------------------
1 | package echopraxia.api;
2 |
3 | import static echopraxia.api.PresentationHintAttributes.asCardinal;
4 | import static org.assertj.core.api.Assertions.assertThat;
5 |
6 | import java.util.UUID;
7 | import org.junit.jupiter.api.Test;
8 |
9 | public class CardinalTests {
10 |
11 | @Test
12 | public void testCardinalArray() {
13 | Value> value = Value.array(1, 2, 3, 4, 5, 6, 7, 8, 9);
14 | Field field =
15 | Field.keyValue("longArray", value, DefaultField.class).withAttribute(asCardinal());
16 | assertThat(field).hasToString("longArray=|9|");
17 | }
18 |
19 | @Test
20 | public void testCardinalArrayWithExtended() {
21 | Value> value = Value.array(1, 2, 3, 4, 5, 6, 7, 8, 9).asCardinal();
22 | Field field = Field.keyValue("longArray", value, DefaultField.class);
23 | assertThat(field).hasToString("longArray=|9|");
24 | }
25 |
26 | @Test
27 | public void testCardinalArrayWithValueOnly() {
28 | Value> value = Value.array(1, 2, 3, 4, 5, 6, 7, 8, 9).asCardinal();
29 | Field field = Field.value("longArray", value);
30 | assertThat(field).hasToString("|9|");
31 | }
32 |
33 | @Test
34 | public void testCardinalString() {
35 | String generatedString = UUID.randomUUID().toString();
36 | Value> value = Value.string(generatedString);
37 | Field field =
38 | Field.keyValue("longString", value, DefaultField.class).withAttribute(asCardinal());
39 | assertThat(field).hasToString("longString=|36|");
40 | }
41 |
42 | @Test
43 | public void testCardinalStringWithValueOnly() {
44 | String generatedString = UUID.randomUUID().toString();
45 | Value> value = Value.string(generatedString);
46 | Field field = Field.value("longString", value, DefaultField.class).withAttribute(asCardinal());
47 | assertThat(field).hasToString("|36|");
48 | }
49 |
50 | @Test
51 | void testStringWithAsCardinal() {
52 | var string = Value.string("foo");
53 | var asCardinal =
54 | string.withAttributes(Attributes.create(PresentationHintAttributes.asCardinal()));
55 | assertThat(asCardinal).hasToString("|3|");
56 | }
57 |
58 | @Test
59 | void testArrayWithAsCardinal() {
60 | var array = Value.array("one", "two", "three");
61 | var asCardinal =
62 | array.withAttributes(Attributes.create(PresentationHintAttributes.asCardinal()));
63 | assertThat(asCardinal).hasToString("|3|");
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/api/src/test/java/echopraxia/api/DisplayNameTests.java:
--------------------------------------------------------------------------------
1 | package echopraxia.api;
2 |
3 | import static echopraxia.api.PresentationHintAttributes.withDisplayName;
4 | import static org.assertj.core.api.Assertions.assertThat;
5 |
6 | import org.junit.jupiter.api.Test;
7 |
8 | public class DisplayNameTests {
9 |
10 | @Test
11 | public void testDisplayName() {
12 | Value> value = Value.string("derp");
13 | Field field =
14 | new DefaultField("longArray", value, Attributes.create(withDisplayName("My Display Name")));
15 | assertThat(field.toString()).isEqualTo("\"My Display Name\"=derp");
16 | }
17 |
18 | @Test
19 | public void testDisplayNameWithExtended() {
20 | Value> value = Value.string("derp");
21 | Field field =
22 | Field.keyValue("longArray", value, DefaultField.class).withDisplayName("My Display Name");
23 | assertThat(field.toString()).isEqualTo("\"My Display Name\"=derp");
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/api/src/test/java/echopraxia/api/FormatTests.java:
--------------------------------------------------------------------------------
1 | package echopraxia.api;
2 |
3 | import static echopraxia.api.Value.*;
4 | import static org.assertj.core.api.Assertions.assertThat;
5 |
6 | import org.junit.jupiter.api.Test;
7 |
8 | public class FormatTests {
9 |
10 | @Test
11 | public void testNull() {
12 | final FieldBuilder fb = FieldBuilder.instance();
13 | final Field f = fb.nullField("foo");
14 | assertThat(f).hasToString("foo=null");
15 | }
16 |
17 | @Test
18 | public void testString() {
19 | final FieldBuilder fb = FieldBuilder.instance();
20 | final Field f = fb.string("foo", "bar");
21 |
22 | assertThat(f).hasToString("foo=bar");
23 | }
24 |
25 | @Test
26 | public void testNumber() {
27 | final FieldBuilder fb = FieldBuilder.instance();
28 | final Field f = fb.number("foo", 1);
29 | assertThat(f).hasToString("foo=1");
30 | }
31 |
32 | @Test
33 | public void testBoolean() {
34 | final FieldBuilder fb = FieldBuilder.instance();
35 | final Field f = fb.bool("foo", true);
36 | assertThat(f).hasToString("foo=true");
37 | }
38 |
39 | @Test
40 | public void testArrayOfString() {
41 | final FieldBuilder fb = FieldBuilder.instance();
42 | final Field f = fb.array("foo", "one", "two", "three");
43 | assertThat(f).hasToString("foo=[one, two, three]");
44 | }
45 |
46 | @Test
47 | public void testArrayOfNumber() {
48 | final FieldBuilder fb = FieldBuilder.instance();
49 | final Field f = fb.array("foo", 1, 2, 3);
50 | assertThat(f).hasToString("foo=[1, 2, 3]");
51 | }
52 |
53 | @Test
54 | public void testArrayOfBoolean() {
55 | final FieldBuilder fb = FieldBuilder.instance();
56 | final Field f = fb.array("foo", false, true, false);
57 | assertThat(f).hasToString("foo=[false, true, false]");
58 | }
59 |
60 | @Test
61 | public void testArrayOfNull() {
62 | final FieldBuilder fb = FieldBuilder.instance();
63 | final Field f = fb.array("foo", Value.array(nullValue(), nullValue(), nullValue()));
64 | assertThat(f).hasToString("foo=[null, null, null]");
65 | }
66 |
67 | @Test
68 | public void testObject() {
69 | final FieldBuilder fb = FieldBuilder.instance();
70 | final Field f =
71 | fb.object(
72 | "foo",
73 | object(
74 | fb.string("stringName", "value"),
75 | fb.number("numName", 43),
76 | fb.bool("boolName", true),
77 | fb.array("arrayName", array(string("a"), nullValue())),
78 | fb.nullField("nullName")));
79 | assertThat(f)
80 | .hasToString(
81 | "foo={stringName=value, numName=43, boolName=true, arrayName=[a, null], nullName=null}");
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/api/src/test/java/echopraxia/api/ToStringFormatTests.java:
--------------------------------------------------------------------------------
1 | package echopraxia.api;
2 |
3 | import static echopraxia.api.Value.array;
4 | import static echopraxia.api.Value.string;
5 | import static org.assertj.core.api.Assertions.assertThat;
6 |
7 | import java.time.Duration;
8 | import java.util.*;
9 | import org.jetbrains.annotations.NotNull;
10 | import org.junit.jupiter.api.Test;
11 |
12 | public class ToStringFormatTests {
13 |
14 | @Test
15 | public void testSimpleFormat() {
16 | MyFieldBuilder fb = MyFieldBuilder.instance();
17 | Duration duration = Duration.ofDays(1);
18 | Field field = fb.duration("duration", duration);
19 | assertThat(field.toString()).isEqualTo("duration=1 day");
20 | assertThat(field.value().asString().raw()).isEqualTo("PT24H");
21 | }
22 |
23 | @Test
24 | public void testNestedSimpleFormat() {
25 | MyFieldBuilder fb = MyFieldBuilder.instance();
26 |
27 | Field nameField = fb.string("name", "event name");
28 | Field durationField = fb.duration("duration", Duration.ofDays(1));
29 | Field eventField = fb.keyValue("event", Value.object(nameField, durationField));
30 |
31 | var s = eventField.toString();
32 | assertThat(s).isEqualTo("event={name=event name, duration=1 day}");
33 | }
34 |
35 | @Test
36 | public void testArraySimpleFormat() {
37 | MyFieldBuilder fb = MyFieldBuilder.instance();
38 |
39 | Duration[] durationsArray = {Duration.ofDays(1), Duration.ofDays(2)};
40 | var visitor =
41 | new SimpleFieldVisitor() {
42 | @Override
43 | public @NotNull ArrayVisitor visitArray() {
44 | return new SimpleArrayVisitor() {
45 | @Override
46 | public @NotNull Field done() {
47 | return new DefaultField(
48 | name, array(f -> string(formatDuration(f)), durationsArray), attributes);
49 | }
50 | };
51 | }
52 | };
53 |
54 | var durationsField =
55 | fb.array("durations", array(f -> string(f.toString()), durationsArray))
56 | .withToStringFormat(visitor);
57 | var s = durationsField.toString();
58 | assertThat(s).isEqualTo("durations=[1 day, 2 days]");
59 | }
60 |
61 | static class MyFieldBuilder implements FieldBuilder {
62 | static MyFieldBuilder instance() {
63 | return new MyFieldBuilder();
64 | }
65 |
66 | public Field duration(String name, Duration duration) {
67 | return string(name, duration.toString())
68 | .withToStringFormat(
69 | new SimpleFieldVisitor() {
70 | @Override
71 | public @NotNull Field visitString(@NotNull Value stringValue) {
72 | return string(name, formatDuration(duration));
73 | }
74 | });
75 | }
76 | }
77 |
78 | private static String formatDuration(Duration duration) {
79 | List parts = new ArrayList<>();
80 | long days = duration.toDaysPart();
81 | if (days > 0) {
82 | parts.add(plural(days, "day"));
83 | }
84 | return String.join(", ", parts);
85 | }
86 |
87 | private static String plural(long num, String unit) {
88 | return num + " " + unit + (num == 1 ? "" : "s");
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/api/src/test/java/echopraxia/api/ToStringValueTests.java:
--------------------------------------------------------------------------------
1 | package echopraxia.api;
2 |
3 | import static echopraxia.api.PresentationHintAttributes.withToStringValue;
4 | import static org.assertj.core.api.Assertions.assertThat;
5 |
6 | import java.time.Duration;
7 | import java.util.ArrayList;
8 | import java.util.List;
9 | import org.junit.jupiter.api.Test;
10 |
11 | public class ToStringValueTests {
12 |
13 | @Test
14 | public void testString() {
15 | Duration duration = Duration.ofDays(1);
16 | var value = Value.string(duration.toString()); // PT24H in line and JSON
17 | Attribute stringAttribute = withToStringValue(formatDuration(duration));
18 | var durationWithToString =
19 | value.withAttributes(Attributes.create(stringAttribute)); // 1 day in toString()
20 | assertThat(durationWithToString).hasToString("1 day");
21 | }
22 |
23 | @Test
24 | public void testArray() {
25 | var one = Value.string("one");
26 | var two = Value.string("two").withAttributes(Attributes.create(withToStringValue("dos")));
27 | var three = Value.string("three");
28 | var array = Value.array(one, two, three);
29 | assertThat(array).hasToString("[one, dos, three]");
30 | }
31 |
32 | @Test
33 | public void testObjectWithToStringValue() {
34 | var frame = new IllegalStateException().getStackTrace()[0];
35 | var stackObject =
36 | Value.object(
37 | Field.keyValue("line_number", Value.number(frame.getLineNumber())),
38 | Field.keyValue("method_name", Value.string(frame.getMethodName())),
39 | Field.keyValue("method_name", Value.string(frame.getFileName())))
40 | .withToStringValue(frame.getFileName());
41 |
42 | assertThat(stackObject).hasToString("ToStringValueTests.java");
43 | }
44 |
45 | @Test
46 | public void testArrayWithToStringValue() {
47 | var stackObject = Value.array("1", "2", "3").withToStringValue("herp derp");
48 |
49 | assertThat(stackObject).hasToString("herp derp");
50 | }
51 |
52 | private static String formatDuration(Duration duration) {
53 | List parts = new ArrayList<>();
54 | long days = duration.toDaysPart();
55 | if (days > 0) {
56 | parts.add(plural(days, "day"));
57 | }
58 | return String.join(", ", parts);
59 | }
60 |
61 | private static String plural(long num, String unit) {
62 | return num + " " + unit + (num == 1 ? "" : "s");
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/api/src/test/java/echopraxia/api/ValueTests.java:
--------------------------------------------------------------------------------
1 | package echopraxia.api;
2 |
3 | import static echopraxia.api.Value.optional;
4 | import static echopraxia.api.Value.string;
5 | import static java.util.Collections.singleton;
6 | import static org.assertj.core.api.Assertions.assertThat;
7 |
8 | import java.time.Instant;
9 | import java.util.Optional;
10 | import org.junit.jupiter.api.Test;
11 |
12 | public class ValueTests {
13 |
14 | @Test
15 | void testObjectValueAdd() {
16 | Value.ObjectValue object = Value.object();
17 | Value.StringValue stringValue = string("some string value");
18 | Field stringField = Field.value("string", stringValue);
19 | Value.ObjectValue objectPlus = object.add(stringField);
20 | assertThat(objectPlus.raw()).hasSize(1);
21 | }
22 |
23 | @Test
24 | void testObjectValueAddAll() {
25 | Value.ObjectValue object = Value.object();
26 | Value.StringValue stringValue = string("some string value");
27 | Field stringField = Field.value("string", stringValue);
28 | Value.ObjectValue objectPlus = object.addAll(singleton(stringField));
29 | assertThat(objectPlus.raw()).hasSize(1);
30 | }
31 |
32 | @Test
33 | void testArrayValueAdd() {
34 | Value.StringValue stringValue = string("some string value");
35 | Value.NumberValue numberValue = Value.number(1);
36 | Value.ArrayValue arrayValue = Value.array(stringValue);
37 |
38 | Value.ArrayValue arrayPlus = arrayValue.add(numberValue);
39 | assertThat(arrayPlus.raw()).hasSize(2);
40 | }
41 |
42 | @Test
43 | void testArrayValueAddAll() {
44 | Value.StringValue stringValue = string("some string value");
45 | Value.NumberValue numberValue = Value.number(1);
46 | Value.ArrayValue arrayValue = Value.array(stringValue);
47 |
48 | Value.ArrayValue arrayPlus = arrayValue.addAll(singleton(numberValue));
49 | assertThat(arrayPlus.raw()).hasSize(2);
50 | }
51 |
52 | @Test
53 | void testOptionalWithNull() {
54 | Value> optional = optional(null);
55 | assertThat(optional).isEqualTo(Value.nullValue());
56 | }
57 |
58 | @Test
59 | void testOptionalWithEmpty() {
60 | Value> optional = optional(Optional.empty());
61 | assertThat(optional).isEqualTo(Value.nullValue());
62 | }
63 |
64 | @Test
65 | void testOptionalWithSome() {
66 | Value> optional = optional(Optional.of(string("foo")));
67 | assertThat(optional).isEqualTo(string("foo"));
68 | }
69 |
70 | @Test
71 | void testOptionalMap() {
72 | Instant instant = Instant.ofEpochSecond(0);
73 | var v = optional(Optional.ofNullable(instant).map(i -> string(i.toString())));
74 | assertThat(v).isEqualTo(Value.string("1970-01-01T00:00:00Z"));
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/docs/frameworks/log4j2.md:
--------------------------------------------------------------------------------
1 | # Log4J2 Framework
2 |
3 | Similar to Logstash, you can get access to Log4J specific features by casting to the underlying `Log4JCoreLogger` class.
4 |
5 | ```java
6 |
7 |
8 | Log4JCoreLogger core = (Log4JCoreLogger) CoreLoggerFactory.getLogger();
9 | ```
10 |
11 | ## Marker Access
12 |
13 | The `Log4JCoreLogger` has a `withMarker` method that takes a Log4J marker:
14 |
15 | ```java
16 | final Marker securityMarker = MarkerManager.getMarker("SECURITY");
17 | Logger logger = LoggerFactory.getLogger(
18 | core.withMarker(securityMarker), FieldBuilder.instance);
19 | ```
20 |
21 | If you have a marker set as context, you can evaluate it in a condition through casting to `Log4JLoggingContext`:
22 |
23 | ```java
24 | Condition hasAnyMarkers = (level, context) -> {
25 | Log4JLoggingContext c = (Log4JLoggingContext) context;
26 | Marker m = c.getMarker();
27 | return securityMarker.equals(m);
28 | };
29 | ```
30 |
31 | If you need to get the Log4j logger from a core logger, you can cast and call `core.logger()`:
32 |
33 | ```java
34 | Logger baseLogger = LoggerFactory.getLogger();
35 | Log4JCoreLogger core = (Log4JCoreLogger) baseLogger.core();
36 | org.apache.logging.log4j.Logger log4jLogger = core.logger();
37 | ```
38 |
39 | ## Direct Log4J API
40 |
41 | In the event that the Log4J2 API must be used directly, an `EchopraxiaFieldsMessage` can be sent in for JSON rendering.
42 |
43 | ```java
44 | import com.tersesystems.echopraxia.api.FieldBuilder;
45 | import com.tersesystems.echopraxia.api.FieldBuilderResult;
46 | import com.tersesystems.echopraxia.log4j.layout.EchopraxiaFieldsMessage;
47 | import org.apache.logging.log4j.LogManager;
48 | import org.apache.logging.log4j.Logger;
49 |
50 | FieldBuilder fb = FieldBuilder.instance();
51 | Logger logger = LogManager.getLogger();
52 | EchopraxiaFieldsMessage message = structured("echopraxia message {}", fb.string("foo", "bar"));
53 | logger.info(message);
54 |
55 | EchopraxiaFieldsMessage structured(String message, FieldBuilderResult args) {
56 | List loggerFields = Collections.emptyList();
57 | return new EchopraxiaFieldsMessage(message, loggerFields, result.fields());
58 | }
59 | ```
60 |
61 | Note that exceptions must also be passed outside the message to be fully processed by Log4J:
62 |
63 | ```java
64 | Exception e = new RuntimeException();
65 | EchopraxiaFieldsMessage message = structured("exception {}", fb.exception(e));
66 | logger.info(message, e);
67 | ```
68 |
69 | Unfortunately, I don't understand Log4J internals well enough to make conditions work using the Log4J API. One option could be to write a [Log4J Filter](https://logging.apache.org/log4j/2.x/manual/filters.html) to work on a message.
70 |
71 |
--------------------------------------------------------------------------------
/docs/logging.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tersesystems/echopraxia/c7d34f295654b57e7e78d3f625c8130c15c7e46b/docs/logging.png
--------------------------------------------------------------------------------
/docs/usage/basics.md:
--------------------------------------------------------------------------------
1 |
2 | # Basic Usage
3 |
4 | Echopraxia is simple and easy to use, and looks very similar to SLF4J.
5 |
6 | Add the import:
7 |
8 | ```java
9 | import echopraxia.api.*;
10 | import echopraxia.logger.*;
11 | ```
12 |
13 | Define a logger (usually in a controller or singleton -- `getClass()` is particularly useful for abstract controllers):
14 |
15 | ```java
16 | import echopraxia.simple.*;
17 |
18 | final Logger basicLogger = LoggerFactory.getLogger(getClass());
19 | ```
20 |
21 | Logging simple messages and exceptions are done as in SLF4J:
22 |
23 | ```java
24 | try {
25 | ...
26 | basicLogger.info("Simple message");
27 | } catch (Exception e) {
28 | basicLogger.error("Error message", e);
29 | }
30 | ```
31 |
32 | However, when you log arguments, you pass a function which provides you with a customizable field builder and returns a `FieldBuilderResult` -- a `Field` is a `FieldBuilderResult`, so you can do:
33 |
34 | ```java
35 | var fb = FieldBuilder.instance();
36 | basicLogger.info("Message name {}", fb.string("name", "value"));
37 | ```
38 |
39 | If you are returning multiple fields, then using `fb.list` will return a `FieldBuilderResult`:
40 |
41 | ```java
42 | basicLogger.info("Message name {} age {}", fb.list(
43 | fb.string("name", "value"),
44 | fb.number("age", 13)
45 | ));
46 | ```
47 |
48 | And `fb.list` can take many inputs as needed, for example a stream:
49 |
50 | ```java
51 | var arrayOfFields = { fb.string("name", "value") };
52 | basicLogger.info("Message name {}", fb.list(arrayOfFields));
53 | ```
54 |
55 | The field builder is customizable, so you can (and should!) define your own methods to construct fields out of complex objects:
56 |
57 | ```java
58 | class OrderFieldBuilder extends FieldBuilder {
59 | // Use apply to render order as a Field
60 | public Field apply(Order order) {
61 | // assume apply methods for line items etc
62 | return keyValue("order", Value.object(
63 | apply(order.lineItems),
64 | apply(order.paymentInfo),
65 | apply(order.shippingInfo),
66 | apply(order.userId)
67 | ));
68 | }
69 | }
70 |
71 | var fb = new OrderFieldBuilder();
72 | logger.info("Rendering order {}", fb.apply(order));
73 | ```
74 |
75 | Please read the [field builder](fieldbuilder.md) section for more information on making your own field builder methods.
76 |
77 | You can log multiple arguments and include the exception if you want the stack trace:
78 |
79 | ```java
80 | basicLogger.info("Message name {}", fb.list(
81 | fb.string("name", "value"),
82 | fb.exception(e)
83 | ));
84 | ```
85 |
86 | You can also create the fields yourself and pass them in directly:
87 |
88 | ```java
89 | var fb = FieldBuilder.instance();
90 | var nameField = fb.string("name", "value");
91 | var ageField = fb.number("age", 13);
92 | var exceptionField = fb.exception(e);
93 | logger.info(nameField, ageField, exceptionField);
94 | ```
95 |
96 | Note that unlike SLF4J, you don't have to worry about including the exception as an argument "swallowing" the stacktrace. If an exception is present, it's always applied to the underlying logger.
97 |
--------------------------------------------------------------------------------
/docs/usage/filters.md:
--------------------------------------------------------------------------------
1 | # Filters
2 |
3 | There are times when you want to add a field or a condition to all loggers.
4 |
5 | Echopraxia includes filters that wrap around the `CoreLogger` returned by `CoreLoggerFactory` that provides the ability to modify the core logger from a single pipeline in the code.
6 |
7 | For example, to add a `uses_filter` field to every Echopraxia logger:
8 |
9 | ```java
10 | package example;
11 |
12 | public class ExampleFilter implements CoreLoggerFilter {
13 | @Override
14 | public CoreLogger apply(CoreLogger coreLogger) {
15 | return coreLogger.withFields(fb -> fb.bool("uses_filter", true), FieldBuilder.instance());
16 | }
17 | }
18 | ```
19 |
20 | Filters must extend the `CoreLoggerFilter` interface, and must have a no-args constructor.
21 |
22 | Filters must have a fully qualified class name in the `/echopraxia.properties` file as a resource somewhere in your classpath. The format is `filter.N` where N is the order in which filters should be loaded.
23 |
24 | ```properties
25 | filter.0=example.ExampleFilter
26 | ```
27 |
28 | Filters are particularly helpful when you need to provide "out of context" information for your conditions.
29 |
30 | For example, imagine that you have a situation in which the program uses more CPU or memory than normal in production, but works fine in a staging environment. Using [OSHI](https://github.com/oshi/oshi) and a filter, you can provide the [machine statistics](https://speakerdeck.com/lyddonb/what-is-happening-attempting-to-understand-our-systems?slide=133) and evaluate with dynamic conditions.
31 |
32 | ```java
33 | public class SystemInfoFilter implements CoreLoggerFilter {
34 |
35 | private final SystemInfo systemInfo;
36 |
37 | public SystemInfoFilter() {
38 | systemInfo = new SystemInfo();
39 | }
40 |
41 | @Override
42 | public CoreLogger apply(CoreLogger coreLogger) {
43 | HardwareAbstractionLayer hardware = systemInfo.getHardware();
44 | GlobalMemory mem = hardware.getMemory();
45 | CentralProcessor proc = hardware.getProcessor();
46 | double[] loadAverage = proc.getSystemLoadAverage(3);
47 |
48 | // Now you can add conditions based on these fields, and conditionally
49 | // enable logging based on your load and memory!
50 | var fb = FieldBuilder.instance();
51 | Field loadField = fb.object("load_average", //
52 | fb.number("1min", loadAverage[0]), //
53 | fb.number("5min", loadAverage[1]), //
54 | fb.number("15min", loadAverage[2]));
55 | Field memField = fb.object("mem", //
56 | fb.number("available", mem.getAvailable()), //
57 | fb.number("total", mem.getTotal()));
58 | Field sysinfoField = fb.object("sysinfo", loadField, memField);
59 |
60 | return coreLogger.withFields(sysinfoField);
61 | }
62 | }
63 | ```
64 |
65 | Please see the [system info example](https://github.com/tersesystems/echopraxia-examples/tree/main/system-info) for details.
66 |
--------------------------------------------------------------------------------
/docs/usage/logger.md:
--------------------------------------------------------------------------------
1 | # Custom Logger
2 |
3 | If you want to create a custom `Logger` class that has its own methods, you can do so easily.
4 |
5 | First add the `simple` module:
6 |
7 | Maven:
8 |
9 | ```
10 |
11 | com.tersesystems.echopraxia
12 | simple
13 |
14 |
15 | ```
16 |
17 | Gradle:
18 |
19 | ```
20 | implementation "com.tersesystems.echopraxia:simple:"
21 | ```
22 |
23 | And then add a custom logger factory:
24 |
25 | ```java
26 |
27 | class MyLoggerFactory {
28 | public static class MyFieldBuilder implements FieldBuilder {
29 | // Add your own field builder methods in here
30 | }
31 |
32 | public static final MyFieldBuilder FIELD_BUILDER = new MyFieldBuilder();
33 |
34 | public static MyLogger getLogger(Class> clazz) {
35 | final CoreLogger core = CoreLoggerFactory.getLogger(Logger.class.getName(), clazz);
36 | return new MyLogger(core);
37 | }
38 |
39 | public static final class MyLogger extends Logger {
40 | public static final String FQCN = MyLogger.class.getName();
41 |
42 | public MyLogger(CoreLogger logger) {
43 | super(logger);
44 | }
45 |
46 | public void notice(String message) {
47 | // the caller is MyLogger specifically, so we need to let the logging framework know how to
48 | // address it.
49 | core().withFQCN(FQCN)
50 | .withFields(fb -> fb.bool("notice", true), FIELD_BUILDER)
51 | .log(Level.INFO, message);
52 | }
53 | }
54 | }
55 |
56 | ```
57 |
58 | and then you can log with a `notice` method:
59 |
60 | ```java
61 | class Main {
62 | private static final MyLoggerFactory.MyLogger logger = MyLoggerFactory.getLogger(Main.class);
63 |
64 | public static void main(String[] args) {
65 | logger.notice("this has a notice field added");
66 | }
67 | }
68 | ```
69 |
70 | There is no cache associated with logging, but adding a cache with `ConcurrentHashMap.computeIfAbsent` is straightforward.
--------------------------------------------------------------------------------
/filewatch/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'java-library'
3 | }
4 |
5 | dependencies {
6 | // intentionally no dependency on project(":api")
7 | compileOnly "org.slf4j:slf4j-api:$slf4jApiVersion"
8 | implementation "io.methvin:directory-watcher:$directoryWatcherVersion"
9 | testImplementation project(":logstash")
10 | }
11 |
--------------------------------------------------------------------------------
/filewatch/src/main/java/echopraxia/filewatch/FileWatchEvent.java:
--------------------------------------------------------------------------------
1 | package echopraxia.filewatch;
2 |
3 | import java.nio.file.Path;
4 | import java.nio.file.StandardWatchEventKinds;
5 | import java.nio.file.WatchEvent;
6 |
7 | public class FileWatchEvent {
8 | private final Path path;
9 | private final EventType eventType;
10 |
11 | public enum EventType {
12 |
13 | /* A new file was created */
14 | CREATE(StandardWatchEventKinds.ENTRY_CREATE),
15 |
16 | /* An existing file was modified */
17 | MODIFY(StandardWatchEventKinds.ENTRY_MODIFY),
18 |
19 | /* A file was deleted */
20 | DELETE(StandardWatchEventKinds.ENTRY_DELETE),
21 |
22 | /* An overflow occurred; some events were lost */
23 | OVERFLOW(StandardWatchEventKinds.OVERFLOW);
24 |
25 | private WatchEvent.Kind> kind;
26 |
27 | EventType(WatchEvent.Kind> kind) {
28 | this.kind = kind;
29 | }
30 |
31 | public WatchEvent.Kind> getWatchEventKind() {
32 | return kind;
33 | }
34 | }
35 |
36 | public FileWatchEvent(Path path, EventType eventType) {
37 | this.path = path;
38 | this.eventType = eventType;
39 | }
40 |
41 | public Path path() {
42 | return path;
43 | }
44 |
45 | public EventType eventType() {
46 | return eventType;
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/filewatch/src/main/java/echopraxia/filewatch/FileWatchService.java:
--------------------------------------------------------------------------------
1 | package echopraxia.filewatch;
2 |
3 | import java.io.IOException;
4 | import java.nio.file.Path;
5 | import java.util.List;
6 | import java.util.concurrent.ThreadFactory;
7 | import java.util.function.Consumer;
8 |
9 | /** The FileWatchService */
10 | public interface FileWatchService {
11 |
12 | /**
13 | * Watches the given directories, sending events to the event consumer.
14 | *
15 | * @param factory the thread factory to use. Use setDaemon(true) in most cases.
16 | * @param watchList the directories to watch.
17 | * @param eventConsumer the event consumer
18 | * @return the file watcher
19 | * @throws IOException if there's a problem setting up the watcher
20 | */
21 | FileWatcher watch(
22 | ThreadFactory factory, List watchList, Consumer eventConsumer)
23 | throws IOException;
24 |
25 | /** A watcher, that watches files. */
26 | interface FileWatcher {
27 | /** Stop watching the files. */
28 | void stop();
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/filewatch/src/main/java/echopraxia/filewatch/FileWatchServiceFactory.java:
--------------------------------------------------------------------------------
1 | package echopraxia.filewatch;
2 |
3 | import java.util.ServiceLoader;
4 |
5 | /**
6 | * The FileWatchServiceFactory creates `FileWatchService` instances from using a service provider
7 | * interface internally.
8 | *
9 | * A default implementation is provided if no external provider is available.
10 | */
11 | public class FileWatchServiceFactory {
12 |
13 | // lazy singleton holder for the SPI provider.
14 | // this might be overkill, but could be useful for unit testing / shims / etc
15 | // and it's self contained otherwise.
16 | private static class LazyHolder {
17 | private static FileWatchServiceProvider init() {
18 | ServiceLoader loader =
19 | ServiceLoader.load(FileWatchServiceProvider.class);
20 |
21 | // Look to see if the end user provided another implementation we should use
22 | for (FileWatchServiceProvider provider : loader) {
23 | final String name = provider.getClass().getName();
24 | if (!name.endsWith(".DefaultFileWatchService")) {
25 | return provider;
26 | }
27 | }
28 | // Otherwise fall back to DefaultFileWatchService.
29 | return loader.iterator().next();
30 | }
31 |
32 | static final FileWatchServiceProvider INSTANCE = init();
33 | }
34 |
35 | /**
36 | * Returns a file watch service with the file hash check enabled.
37 | *
38 | * @return the file watch service singleton.
39 | */
40 | public static FileWatchService fileWatchService() {
41 | return fileWatchService(false);
42 | }
43 |
44 | /**
45 | * Returns a file watch service.
46 | *
47 | * @param disableFileHashCheck true if the file hash check should be disabled.
48 | * @return the file watch service singleton.
49 | */
50 | public static FileWatchService fileWatchService(boolean disableFileHashCheck) {
51 | return LazyHolder.INSTANCE.fileWatchService(disableFileHashCheck);
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/filewatch/src/main/java/echopraxia/filewatch/FileWatchServiceProvider.java:
--------------------------------------------------------------------------------
1 | package echopraxia.filewatch;
2 |
3 | /** The SPI for FileWatchService. */
4 | public interface FileWatchServiceProvider {
5 |
6 | FileWatchService fileWatchService(boolean disableFileHashCheck);
7 | }
8 |
--------------------------------------------------------------------------------
/filewatch/src/main/java/echopraxia/filewatch/dirwatcher/DefaultFileWatchServiceProvider.java:
--------------------------------------------------------------------------------
1 | package echopraxia.filewatch.dirwatcher;
2 |
3 | import echopraxia.filewatch.FileWatchService;
4 | import echopraxia.filewatch.FileWatchServiceProvider;
5 |
6 | /** The provider for default filewatch service. */
7 | public class DefaultFileWatchServiceProvider implements FileWatchServiceProvider {
8 |
9 | @Override
10 | public FileWatchService fileWatchService(boolean disableHashCheck) {
11 | return new DefaultFileWatchService(disableHashCheck);
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/filewatch/src/main/resources/META-INF/services/echopraxia.filewatch.FileWatchServiceProvider:
--------------------------------------------------------------------------------
1 | echopraxia.filewatch.dirwatcher.DefaultFileWatchServiceProvider
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | group=com.tersesystems.echopraxia
2 |
3 | version=4.0.0
4 |
5 | org.gradle.jvmargs=--add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED \
6 | --add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED \
7 | --add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED \
8 | --add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED \
9 | --add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED
10 |
11 | # https://mvnrepository.com/artifact/ch.qos.logback/logback-classic
12 | logbackVersion=1.5.13
13 |
14 | # https://mvnrepository.com/artifact/net.logstash.logback/logstash-logback-encoder/
15 | logstashVersion=8.0
16 |
17 | # https://mvnrepository.com/artifact/com.twineworks/tweakflow
18 | tweakflowVersion=1.4.4
19 |
20 | # https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-core/2.24.3
21 | log4j2Version=2.24.3
22 |
23 | # https://mvnrepository.com/artifact/com.jayway.jsonpath/json-path
24 | jsonPathVersion=2.9.0
25 |
26 | # https://mvnrepository.com/artifact/org.slf4j/slf4j-api
27 | slf4jApiVersion=2.0.16
28 | slf4jJdk14Version=2.0.16
29 |
30 | directoryWatcherVersion=0.18.0
31 |
32 | # https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind
33 | jacksonDatabindVersion=2.18.2
34 |
35 | # https://mvnrepository.com/artifact/com.flipkart.zjsonpatch/zjsonpatch
36 | zjsonPatchVersion=0.4.16
37 |
--------------------------------------------------------------------------------
/gradle/java-publication.gradle:
--------------------------------------------------------------------------------
1 | //Auxiliary jar files required by Maven module publications
2 | task sourcesJar(type: Jar, dependsOn: classes) {
3 | archiveClassifier = 'sources'
4 | from sourceSets.main.allSource
5 | }
6 |
7 | //TODO: java.withSourcesJar(), java.withJavadocJar()
8 | task javadocJar(type: Jar, dependsOn: javadoc) {
9 | archiveClassifier = 'javadoc'
10 | from javadoc.destinationDir
11 | }
12 |
13 | apply plugin: 'maven-publish'
14 | publishing { //https://docs.gradle.org/current/userguide/publishing_maven.html
15 | publications {
16 | maven(MavenPublication) { //name of the publication
17 | from components.java
18 | artifact sourcesJar
19 | artifact javadocJar
20 |
21 | pom {
22 | name = tasks.jar.archiveBaseName
23 | description = "Java Structured Logging API"
24 | url = "https://github.com/tersesystems/echopraxia"
25 | licenses {
26 | license {
27 | name = 'Apache2'
28 | url = 'https://github.com/tersesystems/echopraxia/blob/master/LICENSE'
29 | }
30 | }
31 | developers {
32 | developer {
33 | id = 'tersesystems'
34 | name = 'Terse Systems'
35 | url = 'https://github.com/tersesystems'
36 | }
37 | }
38 | scm {
39 | url = 'https://github.com/tersesystems/echopraxia.git'
40 | }
41 | }
42 | }
43 | }
44 |
45 | repositories {
46 | // useful for testing - running "publish" will create artifacts/pom in a local dir
47 | maven { url = "$rootDir/build/repo" }
48 | }
49 | }
50 |
51 | apply plugin: 'signing'
52 | signing {
53 | // https://docs.gradle.org/current/userguide/signing_plugin.html
54 | sign publishing.publications.maven
55 | }
--------------------------------------------------------------------------------
/gradle/release.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: "io.github.gradle-nexus.publish-plugin" //https://github.com/gradle-nexus/publish-plugin/
2 |
3 | nexusPublishing {
4 | repositories {
5 | sonatype()
6 | }
7 | }
8 |
9 | allprojects { p ->
10 | plugins.withId("java-library") {
11 | p.apply from: "$rootDir/gradle/java-publication.gradle"
12 | }
13 | }
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tersesystems/echopraxia/c7d34f295654b57e7e78d3f625c8130c15c7e46b/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip
4 | networkTimeout=10000
5 | validateDistributionUrl=true
6 | zipStoreBase=GRADLE_USER_HOME
7 | zipStorePath=wrapper/dists
8 |
--------------------------------------------------------------------------------
/jackson/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'java-library'
3 | }
4 |
5 | dependencies {
6 | api project(":api")
7 |
8 | testImplementation 'net.javacrumbs.json-unit:json-unit-assertj:2.38.0'
9 | testImplementation(testFixtures(project(':logging')))
10 |
11 | // https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind
12 | api "com.fasterxml.jackson.core:jackson-databind:$jacksonDatabindVersion"
13 | }
14 |
--------------------------------------------------------------------------------
/jackson/src/main/java/echopraxia/jackson/EchopraxiaModule.java:
--------------------------------------------------------------------------------
1 | package echopraxia.jackson;
2 |
3 | import com.fasterxml.jackson.core.Version;
4 | import com.fasterxml.jackson.core.util.VersionUtil;
5 | import com.fasterxml.jackson.databind.Module;
6 | import com.fasterxml.jackson.databind.module.SimpleDeserializers;
7 | import com.fasterxml.jackson.databind.module.SimpleSerializers;
8 | import echopraxia.api.Field;
9 | import echopraxia.api.Value;
10 |
11 | /** A Jackson module that is loaded in automatically by mapper.findAndRegisterModules() */
12 | public class EchopraxiaModule extends Module {
13 | //
14 | // https://github.com/FasterXML/jackson-docs/wiki/JacksonHowToCustomSerializers
15 |
16 | public EchopraxiaModule() {
17 | super();
18 | }
19 |
20 | @Override
21 | public String getModuleName() {
22 | return EchopraxiaModule.class.getSimpleName();
23 | }
24 |
25 | @Override
26 | @SuppressWarnings("deprecation")
27 | public Version version() {
28 | final ClassLoader loader = EchopraxiaModule.class.getClassLoader();
29 | return VersionUtil.mavenVersionFor(loader, "com.tersesystems.echopraxia", "jackson");
30 | }
31 |
32 | @Override
33 | public void setupModule(final SetupContext context) {
34 | final SimpleSerializers serializers = new SimpleSerializers();
35 | serializers.addSerializer(Field.class, FieldSerializer.INSTANCE);
36 | serializers.addSerializer(Value.class, ValueSerializer.INSTANCE);
37 | context.addSerializers(serializers);
38 |
39 | final SimpleDeserializers deserializers = new SimpleDeserializers();
40 | deserializers.addDeserializer(Value.class, ValueDeserializer.INSTANCE);
41 | context.addDeserializers(deserializers);
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/jackson/src/main/java/echopraxia/jackson/ObjectMapperProvider.java:
--------------------------------------------------------------------------------
1 | package echopraxia.jackson;
2 |
3 | import com.fasterxml.jackson.databind.ObjectMapper;
4 |
5 | /**
6 | * This interface provides an object mapper, with a default pointing to a static final instance.
7 | *
8 | * You can override this method in your own field builder if you need a different object mapper.
9 | */
10 | public interface ObjectMapperProvider {
11 | default ObjectMapper _objectMapper() {
12 | return DefaultObjectMapper.OBJECT_MAPPER;
13 | }
14 | }
15 |
16 | final class DefaultObjectMapper {
17 | static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
18 |
19 | static {
20 | // if this fails for any reason, we'll get a "NoClassDefFoundError"
21 | // which can be very unintuitive.
22 | OBJECT_MAPPER.findAndRegisterModules();
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/jackson/src/main/java/echopraxia/jackson/ValueSerializer.java:
--------------------------------------------------------------------------------
1 | package echopraxia.jackson;
2 |
3 | import com.fasterxml.jackson.core.JsonGenerator;
4 | import com.fasterxml.jackson.databind.SerializerProvider;
5 | import com.fasterxml.jackson.databind.ser.std.StdSerializer;
6 | import echopraxia.api.Field;
7 | import echopraxia.api.Value;
8 | import java.io.IOException;
9 | import java.math.BigDecimal;
10 | import java.math.BigInteger;
11 | import java.util.List;
12 |
13 | /**
14 | * The ValueSerializer class plugs into the Jackson serializer system to serialize Value into JSON.
15 | */
16 | public class ValueSerializer extends StdSerializer {
17 |
18 | static final ValueSerializer INSTANCE = new ValueSerializer();
19 |
20 | public ValueSerializer() {
21 | super(Value.class);
22 | }
23 |
24 | @Override
25 | public void serialize(Value value, JsonGenerator gen, SerializerProvider provider)
26 | throws IOException {
27 | if (value == null || value.raw() == null) {
28 | gen.writeNull();
29 | return;
30 | }
31 |
32 | switch (value.type()) {
33 | case ARRAY:
34 | List> arrayValues = ((Value.ArrayValue) value).raw();
35 | gen.writeStartArray();
36 | for (Value> arrayValue : arrayValues) {
37 | gen.writeObject(arrayValue);
38 | }
39 | gen.writeEndArray();
40 | break;
41 | case OBJECT:
42 | List objFields = ((Value.ObjectValue) value).raw();
43 | gen.writeStartObject();
44 | for (Field objField : objFields) {
45 | gen.writeObject(objField);
46 | }
47 | gen.writeEndObject();
48 | break;
49 | case STRING:
50 | gen.writeString(value.raw().toString());
51 | break;
52 | case NUMBER:
53 | Number n = ((Value.NumberValue) value).raw();
54 | if (n instanceof Byte) {
55 | gen.writeNumber(n.byteValue());
56 | } else if (n instanceof Short) {
57 | gen.writeNumber(n.shortValue());
58 | } else if (n instanceof Integer) {
59 | gen.writeNumber(n.intValue());
60 | } else if (n instanceof Long) {
61 | gen.writeNumber(n.longValue());
62 | } else if (n instanceof Double) {
63 | gen.writeNumber(n.doubleValue());
64 | } else if (n instanceof BigInteger) {
65 | gen.writeNumber((BigInteger) n);
66 | } else if (n instanceof BigDecimal) {
67 | gen.writeNumber((BigDecimal) n);
68 | }
69 | break;
70 | case BOOLEAN:
71 | boolean b = ((Value.BooleanValue) value).raw();
72 | gen.writeBoolean(b);
73 | break;
74 | case EXCEPTION:
75 | final Throwable throwable = ((Value.ExceptionValue) value).raw();
76 | gen.writeString(throwable.toString());
77 | break;
78 | case NULL:
79 | gen.writeNull();
80 | break;
81 | }
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/jackson/src/main/resources/META-INF/services/com.fasterxml.jackson.databind.Module:
--------------------------------------------------------------------------------
1 | echopraxia.jackson.EchopraxiaModule
--------------------------------------------------------------------------------
/jsonpath/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'java-library'
3 | id 'java-test-fixtures'
4 | }
5 |
6 | dependencies {
7 | api project(":logging")
8 |
9 | // https://mvnrepository.com/artifact/com.jayway.jsonpath/json-path
10 | implementation "com.jayway.jsonpath:json-path:$jsonPathVersion"
11 |
12 | testImplementation(testFixtures(project(':logging')))
13 | }
14 |
--------------------------------------------------------------------------------
/jul/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'java-library'
3 | }
4 |
5 | dependencies {
6 | api project(":logging")
7 | api project(":jackson")
8 |
9 | // Cannot add a hard transitive dependency that can evict things here
10 | compileOnly "org.slf4j:slf4j-api:$slf4jApiVersion"
11 | compileOnly "org.slf4j:slf4j-jdk14:$slf4jJdk14Version"
12 |
13 | testImplementation "org.slf4j:slf4j-api:$slf4jApiVersion"
14 | testImplementation "org.slf4j:slf4j-jdk14:$slf4jJdk14Version"
15 |
16 | jmhImplementation project(":logger")
17 | testImplementation project(":logger")
18 | }
19 |
--------------------------------------------------------------------------------
/jul/src/main/java/echopraxia/jul/EchopraxiaLogRecord.java:
--------------------------------------------------------------------------------
1 | package echopraxia.jul;
2 |
3 | import static java.lang.Boolean.parseBoolean;
4 |
5 | import echopraxia.api.Field;
6 | import java.util.logging.Level;
7 | import java.util.logging.LogRecord;
8 |
9 | public class EchopraxiaLogRecord extends LogRecord {
10 |
11 | // Disable infer source, true by default
12 | private static final Boolean disableInferSource =
13 | parseBoolean(
14 | System.getProperty("com.tersesystems.echopraxia.jul.disableInferSource", "true"));
15 |
16 | private Field[] loggerFields;
17 |
18 | public EchopraxiaLogRecord(
19 | String name,
20 | Level level,
21 | String msg,
22 | Field[] parameters,
23 | Field[] loggerFields,
24 | Throwable thrown) {
25 | super(level, msg);
26 | this.setLoggerName(name);
27 |
28 | // JUL is really slow and calls sourceClassName lots when serializing.
29 | if (disableInferSource) {
30 | setSourceClassName(null);
31 | setSourceMethodName(null);
32 | }
33 |
34 | this.setParameters(parameters);
35 | this.setLoggerFields(loggerFields);
36 | this.setThrown(thrown);
37 | }
38 |
39 | public void setLoggerFields(Field[] loggerFields) {
40 | this.loggerFields = loggerFields;
41 | }
42 |
43 | public Field[] getLoggerFields() {
44 | return loggerFields;
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/jul/src/main/java/echopraxia/jul/JULEchopraxiaService.java:
--------------------------------------------------------------------------------
1 | package echopraxia.jul;
2 |
3 | import echopraxia.logging.spi.AbstractEchopraxiaService;
4 | import echopraxia.logging.spi.CoreLogger;
5 | import java.util.logging.Logger;
6 | import org.jetbrains.annotations.NotNull;
7 |
8 | public class JULEchopraxiaService extends AbstractEchopraxiaService {
9 |
10 | @Override
11 | public @NotNull CoreLogger getCoreLogger(@NotNull String fqcn, @NotNull Class> clazz) {
12 | return getCoreLogger(fqcn, clazz.getName());
13 | }
14 |
15 | @Override
16 | public @NotNull CoreLogger getCoreLogger(@NotNull String fqcn, @NotNull String name) {
17 | Logger logger = Logger.getLogger(name);
18 | return new JULCoreLogger(fqcn, logger);
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/jul/src/main/java/echopraxia/jul/JULEchopraxiaServiceProvider.java:
--------------------------------------------------------------------------------
1 | package echopraxia.jul;
2 |
3 | import echopraxia.logging.spi.EchopraxiaService;
4 | import echopraxia.logging.spi.EchopraxiaServiceProvider;
5 |
6 | public class JULEchopraxiaServiceProvider implements EchopraxiaServiceProvider {
7 | @Override
8 | public EchopraxiaService getEchopraxiaService() {
9 | return new JULEchopraxiaService();
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/jul/src/main/java/echopraxia/jul/JULLoggerContext.java:
--------------------------------------------------------------------------------
1 | package echopraxia.jul;
2 |
3 | import static echopraxia.logging.spi.Utilities.joinFields;
4 |
5 | import echopraxia.api.Field;
6 | import echopraxia.logging.spi.LoggerContext;
7 | import java.util.Collections;
8 | import java.util.List;
9 | import java.util.function.Supplier;
10 | import org.jetbrains.annotations.NotNull;
11 |
12 | public class JULLoggerContext implements LoggerContext {
13 | protected final Supplier> fieldsSupplier;
14 |
15 | private static final JULLoggerContext EMPTY = new JULLoggerContext();
16 |
17 | public static JULLoggerContext empty() {
18 | return EMPTY;
19 | }
20 |
21 | JULLoggerContext() {
22 | this.fieldsSupplier = Collections::emptyList;
23 | }
24 |
25 | protected JULLoggerContext(Supplier> f) {
26 | this.fieldsSupplier = f;
27 | }
28 |
29 | public @NotNull List getLoggerFields() {
30 | return fieldsSupplier.get();
31 | }
32 |
33 | public JULLoggerContext withFields(Supplier> o) {
34 | Supplier> joinedFields = joinFields(o, this::getLoggerFields);
35 | return new JULLoggerContext(joinedFields);
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/jul/src/main/java/echopraxia/jul/JULLoggingContext.java:
--------------------------------------------------------------------------------
1 | package echopraxia.jul;
2 |
3 | import static echopraxia.logging.spi.Utilities.joinFields;
4 | import static echopraxia.logging.spi.Utilities.memoize;
5 |
6 | import echopraxia.api.Field;
7 | import echopraxia.logging.api.LoggingContext;
8 | import echopraxia.logging.spi.CoreLogger;
9 | import java.util.Collections;
10 | import java.util.List;
11 | import java.util.function.Supplier;
12 | import org.jetbrains.annotations.NotNull;
13 |
14 | public class JULLoggingContext implements LoggingContext {
15 | private final Supplier> argumentFields;
16 | private final Supplier> loggerFields;
17 | private final Supplier> joinedFields;
18 | private final CoreLogger core;
19 |
20 | // Allow an empty context for testing
21 | public JULLoggingContext(CoreLogger core) {
22 | this.core = core;
23 | this.argumentFields = Collections::emptyList;
24 | this.loggerFields = Collections::emptyList;
25 | this.joinedFields = Collections::emptyList;
26 | }
27 |
28 | public JULLoggingContext(
29 | CoreLogger core, JULLoggerContext context, Supplier> arguments) {
30 | // Defers and memoizes the arguments and context fields for a single logging statement.
31 | this.core = core;
32 | this.argumentFields = memoize(arguments);
33 | this.loggerFields = memoize(context::getLoggerFields);
34 | this.joinedFields = memoize(joinFields(this.loggerFields, this.argumentFields));
35 | }
36 |
37 | public JULLoggingContext(CoreLogger core, JULLoggerContext context) {
38 | this(core, context, Collections::emptyList);
39 | }
40 |
41 | @Override
42 | public @NotNull List getFields() {
43 | return joinedFields.get();
44 | }
45 |
46 | @Override
47 | public List getArgumentFields() {
48 | return argumentFields.get();
49 | }
50 |
51 | @Override
52 | public List getLoggerFields() {
53 | return loggerFields.get();
54 | }
55 |
56 | @Override
57 | public CoreLogger getCore() {
58 | return core;
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/jul/src/main/resources/META-INF/services/echopraxia.logging.spi.EchopraxiaServiceProvider:
--------------------------------------------------------------------------------
1 | echopraxia.jul.JULEchopraxiaServiceProvider
--------------------------------------------------------------------------------
/jul/src/main/resources/echopraxia/jsonformatter.properties:
--------------------------------------------------------------------------------
1 | timestamp=@timestamp
2 | logger_name=logger_name
3 | level=level
4 | thread_name=thread_name
5 | class=caller_class_name
6 | method=caller_method_name
7 | message=message
8 | exception=exception
9 |
10 | timestamp_format=yyyy-MM-dd'T'HH:mm:ss.SSSXXX
11 | timestamp_zoneid=UTC
--------------------------------------------------------------------------------
/jul/src/test/java/echopraxia/jul/EncodedListHandler.java:
--------------------------------------------------------------------------------
1 | package echopraxia.jul;
2 |
3 | import java.util.ArrayList;
4 | import java.util.List;
5 | import java.util.logging.*;
6 |
7 | public class EncodedListHandler extends Handler {
8 | final Formatter jsonFormatter = new JULJSONFormatter(true);
9 | final Formatter textFormatter = new SimpleFormatter();
10 |
11 | private static final List jsonList = new ArrayList<>();
12 | private static final List linesList = new ArrayList<>();
13 | private static final List records = new ArrayList<>();
14 |
15 | public static List records() {
16 | return records;
17 | }
18 |
19 | public static List ndjson() {
20 | return jsonList;
21 | }
22 |
23 | public static List lines() {
24 | return linesList;
25 | }
26 |
27 | @Override
28 | public void publish(LogRecord record) {
29 | jsonList.add(jsonFormatter.format(record)); // you can never use formatMessage here
30 | linesList.add(textFormatter.formatMessage(record));
31 | records.add(record);
32 | }
33 |
34 | @Override
35 | public void flush() {}
36 |
37 | @Override
38 | public void close() throws SecurityException {}
39 |
40 | public static void clear() throws SecurityException {
41 | records.clear();
42 | jsonList.clear();
43 | linesList.clear();
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/jul/src/test/java/echopraxia/jul/ExceptionHandlerTests.java:
--------------------------------------------------------------------------------
1 | package echopraxia.jul;
2 |
3 | import static org.assertj.core.api.Assertions.assertThat;
4 |
5 | import echopraxia.logging.api.Condition;
6 | import org.junit.jupiter.api.Test;
7 |
8 | public class ExceptionHandlerTests extends TestBase {
9 |
10 | @Test
11 | public void testBadArgument() {
12 | var logger = getLogger();
13 | Integer number = null;
14 | logger.debug("this has a null value", fb -> fb.number("nullNumber", number.intValue()));
15 |
16 | Throwable throwable = StaticExceptionHandler.head();
17 | assertThat(throwable).isInstanceOf(NullPointerException.class);
18 | }
19 |
20 | @Test
21 | public void testBadWithField() {
22 | var logger = getLogger();
23 | Integer number = null;
24 | var badLogger = logger.withFields(fb -> fb.number("nullNumber", number.intValue()));
25 | badLogger.debug("this has a null value");
26 |
27 | Throwable throwable = StaticExceptionHandler.head();
28 | assertThat(throwable).isInstanceOf(NullPointerException.class);
29 | }
30 |
31 | @Test
32 | public void testConditionAndBadWithField() {
33 | var logger = getLogger();
34 | Integer number = null;
35 |
36 | Condition condition = Condition.numberMatch("testing", p -> p.raw().intValue() == 5);
37 |
38 | var badLogger = logger.withFields(fb -> fb.number("nullNumber", number.intValue()));
39 | badLogger.debug(condition, "I have a bad logger field and a good condition");
40 |
41 | Throwable throwable = StaticExceptionHandler.head();
42 | assertThat(throwable).isInstanceOf(NullPointerException.class);
43 | }
44 |
45 | @Test
46 | public void testBadConditionWithCondition() {
47 | var logger = getLogger();
48 | Integer number = null;
49 | // match on a null condition that will explode
50 | Condition badCondition =
51 | Condition.numberMatch("testing", p -> p.raw().intValue() == number.intValue());
52 |
53 | var badLogger = logger.withCondition(badCondition);
54 | badLogger.debug("I am passing in {}", fb -> fb.number("testing", 5));
55 |
56 | Throwable throwable = StaticExceptionHandler.head();
57 | assertThat(throwable).isInstanceOf(NullPointerException.class);
58 | }
59 |
60 | @Test
61 | public void testBadCondition() {
62 | var logger = getLogger();
63 | Integer number = null;
64 | Condition badCondition = (level, context) -> number.intValue() == 5;
65 |
66 | logger.debug(badCondition, "I am passing in {}");
67 |
68 | Throwable throwable = StaticExceptionHandler.head();
69 | assertThat(throwable).isInstanceOf(NullPointerException.class);
70 | }
71 |
72 | @Test
73 | public void testBadConditionAndArgument() {
74 | var logger = getLogger();
75 | Integer number = null;
76 | // match on a null condition that will explode
77 | Condition badCondition =
78 | Condition.numberMatch("testing", p -> p.raw().intValue() == number.intValue());
79 |
80 | logger.debug(
81 | badCondition, "I am passing in {}", fb -> fb.number("nullNumber", number.intValue()));
82 |
83 | Throwable throwable = StaticExceptionHandler.head();
84 | assertThat(throwable).isInstanceOf(NullPointerException.class);
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/jul/src/test/java/echopraxia/jul/JSONFormatterTest.java:
--------------------------------------------------------------------------------
1 | package echopraxia.jul;
2 |
3 | import static org.assertj.core.api.Assertions.assertThat;
4 |
5 | import com.fasterxml.jackson.core.JsonProcessingException;
6 | import com.fasterxml.jackson.databind.JsonNode;
7 | import com.fasterxml.jackson.databind.ObjectMapper;
8 | import java.util.List;
9 | import org.junit.jupiter.api.Test;
10 |
11 | public class JSONFormatterTest extends TestBase {
12 |
13 | @Test
14 | void testDebug() throws JsonProcessingException {
15 | var logger = getLogger();
16 | logger.debug("hello");
17 |
18 | List list = EncodedListHandler.ndjson();
19 | String logRecord = list.get(0);
20 |
21 | final ObjectMapper mapper = new ObjectMapper();
22 | final JsonNode jsonNode = mapper.readTree(logRecord);
23 |
24 | assertThat(jsonNode.get("level").asText()).isEqualTo("DEBUG");
25 | assertThat(jsonNode.get("message").asText()).isEqualTo("hello");
26 | }
27 |
28 | @Test
29 | void testInfo() throws JsonProcessingException {
30 | var logger = getLogger();
31 | logger.info("hello");
32 |
33 | List list = EncodedListHandler.ndjson();
34 | String logRecord = list.get(0);
35 |
36 | final ObjectMapper mapper = new ObjectMapper();
37 | final JsonNode jsonNode = mapper.readTree(logRecord);
38 |
39 | assertThat(jsonNode.get("level").asText()).isEqualTo("INFO");
40 | assertThat(jsonNode.get("message").asText()).isEqualTo("hello");
41 | }
42 |
43 | @Test
44 | void testArguments() throws JsonProcessingException {
45 | var logger = getLogger();
46 | logger.info(
47 | "hello {0}, you are {1}, citizen status {2}",
48 | fb -> fb.list(fb.string("name", "will"), fb.number("age", 13), fb.bool("citizen", true)));
49 |
50 | List list = EncodedListHandler.ndjson();
51 | String logRecord = list.get(0);
52 |
53 | final ObjectMapper mapper = new ObjectMapper();
54 | final JsonNode jsonNode = mapper.readTree(logRecord);
55 |
56 | assertThat(jsonNode.get("name").asText()).isEqualTo("will");
57 | assertThat(jsonNode.get("age").asInt()).isEqualTo(13);
58 | assertThat(jsonNode.get("citizen").asBoolean()).isEqualTo(true);
59 | }
60 |
61 | @Test
62 | void testException() throws JsonProcessingException {
63 | var logger = getLogger();
64 | Throwable expected = new IllegalStateException("oh noes");
65 | logger.error("Error", expected);
66 |
67 | List list = EncodedListHandler.ndjson();
68 | String logRecord = list.get(0);
69 |
70 | final ObjectMapper mapper = new ObjectMapper();
71 | final JsonNode jsonNode = mapper.readTree(logRecord);
72 |
73 | assertThat(jsonNode.get("exception").asText())
74 | .isEqualTo("java.lang.IllegalStateException: oh noes");
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/jul/src/test/java/echopraxia/jul/LoggerFactoryTest.java:
--------------------------------------------------------------------------------
1 | package echopraxia.jul;
2 |
3 | import static org.assertj.core.api.Assertions.assertThat;
4 |
5 | import echopraxia.api.FieldBuilder;
6 | import echopraxia.logger.Logger;
7 | import echopraxia.logger.LoggerFactory;
8 | import org.junit.jupiter.api.Test;
9 |
10 | public class LoggerFactoryTest {
11 |
12 | @Test
13 | public void testLoggerFactory() {
14 | // Check that the SPI works
15 | final Logger logger = LoggerFactory.getLogger(getClass());
16 | assertThat(logger).isNotNull();
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/jul/src/test/java/echopraxia/jul/StaticExceptionHandler.java:
--------------------------------------------------------------------------------
1 | package echopraxia.jul;
2 |
3 | import echopraxia.logging.spi.ExceptionHandler;
4 | import java.util.ArrayList;
5 | import java.util.List;
6 |
7 | public class StaticExceptionHandler implements ExceptionHandler {
8 |
9 | private static final List exceptions = new ArrayList<>();
10 |
11 | public static Throwable head() {
12 | return exceptions.get(0);
13 | }
14 |
15 | public static void clear() {
16 | exceptions.clear();
17 | }
18 |
19 | @Override
20 | public void handleException(Throwable e) {
21 | exceptions.add(e);
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/jul/src/test/java/echopraxia/jul/TestBase.java:
--------------------------------------------------------------------------------
1 | package echopraxia.jul;
2 |
3 | import echopraxia.api.FieldBuilder;
4 | import echopraxia.logger.Logger;
5 | import echopraxia.logger.LoggerFactory;
6 | import java.io.IOException;
7 | import java.util.logging.Handler;
8 | import java.util.logging.Level;
9 | import java.util.logging.LogManager;
10 | import org.junit.jupiter.api.BeforeEach;
11 |
12 | public class TestBase {
13 |
14 | @BeforeEach
15 | public void before() throws IOException {
16 | StaticExceptionHandler.clear();
17 | LogManager manager = LogManager.getLogManager();
18 | manager.reset();
19 |
20 | // Programmatic configuration
21 | System.setProperty(
22 | "java.util.logging.SimpleFormatter.format",
23 | "%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS.%1$tL %4$-7s [%3$s] (%2$s) %5$s %6$s%n");
24 |
25 | final Handler consoleHandler = new EncodedListHandler();
26 | consoleHandler.setLevel(Level.FINEST);
27 |
28 | java.util.logging.Logger logger = java.util.logging.Logger.getLogger("");
29 | logger.setLevel(Level.FINEST);
30 | logger.addHandler(consoleHandler);
31 |
32 | EncodedListHandler.clear();
33 | }
34 |
35 | Logger getLogger() {
36 | return LoggerFactory.getLogger(getCoreLogger(), FieldBuilder.instance());
37 | }
38 |
39 | JULCoreLogger getCoreLogger() {
40 | java.util.logging.Logger logger = java.util.logging.Logger.getLogger(getClass().getName());
41 | return new JULCoreLogger(Logger.FQCN, logger);
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/jul/src/test/java/echopraxia/jul/TestEchopraxiaService.java:
--------------------------------------------------------------------------------
1 | package echopraxia.jul;
2 |
3 | public class TestEchopraxiaService extends JULEchopraxiaService {
4 |
5 | public TestEchopraxiaService() {
6 | super();
7 | this.exceptionHandler = new StaticExceptionHandler();
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/jul/src/test/java/echopraxia/jul/TestEchopraxiaServiceProvider.java:
--------------------------------------------------------------------------------
1 | package echopraxia.jul;
2 |
3 | import echopraxia.logging.spi.EchopraxiaService;
4 | import echopraxia.logging.spi.EchopraxiaServiceProvider;
5 |
6 | public class TestEchopraxiaServiceProvider implements EchopraxiaServiceProvider {
7 | @Override
8 | public EchopraxiaService getEchopraxiaService() {
9 | return new TestEchopraxiaService();
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/jul/src/test/resources/META-INF/services/echopraxia.logging.spi.EchopraxiaServiceProvider:
--------------------------------------------------------------------------------
1 | echopraxia.jul.TestEchopraxiaServiceProvider
--------------------------------------------------------------------------------
/log4j/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'java-library'
3 | }
4 |
5 | dependencies {
6 | api project(":logging")
7 | api project(":jackson")
8 |
9 | compileOnly "org.apache.logging.log4j:log4j-core:$log4j2Version"
10 | compileOnly "org.apache.logging.log4j:log4j-api:$log4j2Version"
11 | compileOnly "org.apache.logging.log4j:log4j-layout-template-json:$log4j2Version"
12 |
13 | jmhImplementation project(":logger")
14 | testImplementation project(":logger")
15 |
16 | jmhImplementation "org.apache.logging.log4j:log4j-core:$log4j2Version"
17 | jmhImplementation "org.apache.logging.log4j:log4j-api:$log4j2Version"
18 | jmhImplementation "org.apache.logging.log4j:log4j-layout-template-json:$log4j2Version"
19 |
20 | testImplementation "org.apache.logging.log4j:log4j-core:$log4j2Version"
21 | testImplementation "org.apache.logging.log4j:log4j-api:$log4j2Version"
22 | testImplementation "org.apache.logging.log4j:log4j-layout-template-json:$log4j2Version"
23 | }
24 |
--------------------------------------------------------------------------------
/log4j/src/jmh/java/echopraxia/log4j/CoreLoggerBenchmarks.java:
--------------------------------------------------------------------------------
1 | package echopraxia.log4j;
2 |
3 | import echopraxia.api.FieldBuilder;
4 | import echopraxia.logger.Logger;
5 | import echopraxia.logging.api.Level;
6 | import echopraxia.logging.spi.CoreLogger;
7 | import echopraxia.logging.spi.CoreLoggerFactory;
8 | import java.util.concurrent.TimeUnit;
9 | import org.openjdk.jmh.annotations.*;
10 | import org.openjdk.jmh.infra.Blackhole;
11 |
12 | @BenchmarkMode(Mode.AverageTime)
13 | @OutputTimeUnit(TimeUnit.NANOSECONDS)
14 | @Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
15 | @Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
16 | @Fork(1)
17 | public class CoreLoggerBenchmarks {
18 | private static final CoreLogger logger =
19 | CoreLoggerFactory.getLogger(Logger.class.getName(), CoreLoggerBenchmarks.class.getName());
20 | private static final Exception exception = new RuntimeException();
21 | private static final FieldBuilder builder = FieldBuilder.instance();
22 |
23 | @Benchmark
24 | public void info() {
25 | logger.log(Level.INFO, "Message");
26 | }
27 |
28 | @Benchmark
29 | public void isEnabled(Blackhole blackhole) {
30 | blackhole.consume(logger.isEnabled(Level.INFO));
31 | }
32 |
33 | @Benchmark
34 | public void infoWithParameterizedString() {
35 | logger.log(Level.INFO, "Message {}", fb -> fb.string("foo", "bar"), builder);
36 | }
37 |
38 | @Benchmark
39 | public void infoWithException() {
40 | logger.log(Level.INFO, "Message", fb -> fb.exception(exception), builder);
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/log4j/src/jmh/java/echopraxia/log4j/Log4JBenchmarks.java:
--------------------------------------------------------------------------------
1 | package echopraxia.log4j;
2 |
3 | import static java.util.Collections.emptyList;
4 | import static java.util.Collections.singletonList;
5 |
6 | import echopraxia.api.Field;
7 | import echopraxia.api.Value;
8 | import echopraxia.log4j.layout.EchopraxiaFieldsMessage;
9 | import java.util.Arrays;
10 | import java.util.List;
11 | import java.util.concurrent.TimeUnit;
12 | import org.apache.logging.log4j.LogManager;
13 | import org.apache.logging.log4j.Logger;
14 | import org.apache.logging.log4j.message.Message;
15 | import org.openjdk.jmh.annotations.*;
16 | import org.openjdk.jmh.infra.Blackhole;
17 |
18 | @BenchmarkMode(Mode.AverageTime)
19 | @OutputTimeUnit(TimeUnit.NANOSECONDS)
20 | @Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
21 | @Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
22 | @Fork(1)
23 | public class Log4JBenchmarks {
24 | private static final Logger logger = LogManager.getLogger(Log4JBenchmarks.class);
25 |
26 | private static final Exception exception = new RuntimeException();
27 |
28 | private static final Field field = Field.keyValue("name", Value.string("value"));
29 |
30 | private static final List fields = Arrays.asList(field, field, field, field);
31 |
32 | private static final Message message =
33 | new EchopraxiaFieldsMessage("message", emptyList(), emptyList());
34 |
35 | private static final Message messageWithArgument =
36 | new EchopraxiaFieldsMessage("message {}", emptyList(), singletonList(field));
37 |
38 | private static final EchopraxiaFieldsMessage fieldsMessage =
39 | new EchopraxiaFieldsMessage("message {} {} {} {}", emptyList(), fields);
40 |
41 | @Benchmark
42 | public void info() {
43 | logger.info(message);
44 | }
45 |
46 | @Benchmark
47 | public void isInfoEnabled(Blackhole blackhole) {
48 | blackhole.consume(logger.isInfoEnabled());
49 | }
50 |
51 | @Benchmark
52 | public void infoWithArgument() {
53 | logger.info(messageWithArgument);
54 | }
55 |
56 | @Benchmark
57 | public void infoWithArrayArgs() {
58 | logger.info(fieldsMessage);
59 | }
60 |
61 | @Benchmark
62 | public void infoWithException() {
63 | logger.info(message, exception);
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/log4j/src/jmh/java/echopraxia/log4j/LoggerBenchmarks.java:
--------------------------------------------------------------------------------
1 | package echopraxia.log4j;
2 |
3 | import echopraxia.api.FieldBuilder;
4 | import echopraxia.logger.Logger;
5 | import echopraxia.logger.LoggerFactory;
6 | import echopraxia.logging.api.Condition;
7 | import echopraxia.logging.api.Level;
8 | import java.util.concurrent.TimeUnit;
9 | import org.openjdk.jmh.annotations.*;
10 | import org.openjdk.jmh.infra.Blackhole;
11 |
12 | @BenchmarkMode(Mode.AverageTime)
13 | @OutputTimeUnit(TimeUnit.NANOSECONDS)
14 | @Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
15 | @Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
16 | @Fork(1)
17 | public class LoggerBenchmarks {
18 | private static final Logger logger = LoggerFactory.getLogger();
19 | private static final Exception exception = new RuntimeException();
20 |
21 | private static final Logger neverLogger = logger.withCondition(Condition.never());
22 | private static final Logger alwaysLogger = logger.withCondition(Condition.always());
23 | private static final Logger conditionLogger =
24 | logger.withCondition((level, context) -> level.equals(Level.ERROR));
25 | private static final Logger fieldBuilderLogger =
26 | logger.withFieldBuilder(FieldBuilder.instance());
27 | private static final Logger contextLogger =
28 | logger.withFields(fb -> fb.string("foo", "bar"));
29 |
30 | @Benchmark
31 | public void info() {
32 | logger.info("Message");
33 | }
34 |
35 | @Benchmark
36 | public void infoWithNever() {
37 | neverLogger.info("Message");
38 | }
39 |
40 | @Benchmark
41 | public void infoWithAlways() {
42 | alwaysLogger.info("Message");
43 | }
44 |
45 | @Benchmark
46 | public void infoWithFieldBuilder() {
47 | fieldBuilderLogger.info("Message");
48 | }
49 |
50 | @Benchmark
51 | public void infoWithErrorCondition() {
52 | conditionLogger.info("Message");
53 | }
54 |
55 | @Benchmark
56 | public void isInfoEnabled(Blackhole blackhole) {
57 | blackhole.consume(logger.isInfoEnabled());
58 | }
59 |
60 | @Benchmark
61 | public void infoWithStringArg() {
62 | // No {} in the message template
63 | logger.info("Message", fb -> fb.string("foo", "bar"));
64 | }
65 |
66 | @Benchmark
67 | public void infoWithContextString() {
68 | contextLogger.info("Message");
69 | }
70 |
71 | @Benchmark
72 | public void infoWithParameterizedString() {
73 | // {} in message template
74 | logger.info("Message {}", fb -> fb.string("foo", "bar"));
75 | }
76 |
77 | @Benchmark
78 | public void infoWithException() {
79 | logger.info("Message", exception);
80 | }
81 |
82 | @Benchmark
83 | public void traceWithParameterizedString() {
84 | // should never log
85 | logger.trace("Message {}", fb -> fb.string("foo", "bar"));
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/log4j/src/jmh/resources/log4j2.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/log4j/src/main/java/echopraxia/log4j/Log4JEchopraxiaService.java:
--------------------------------------------------------------------------------
1 | package echopraxia.log4j;
2 |
3 | import echopraxia.logging.spi.AbstractEchopraxiaService;
4 | import echopraxia.logging.spi.CoreLogger;
5 | import org.apache.logging.log4j.LogManager;
6 | import org.apache.logging.log4j.spi.ExtendedLogger;
7 | import org.jetbrains.annotations.NotNull;
8 |
9 | public class Log4JEchopraxiaService extends AbstractEchopraxiaService {
10 |
11 | @Override
12 | public @NotNull CoreLogger getCoreLogger(@NotNull String fqcn, @NotNull Class> clazz) {
13 | return getCoreLogger(fqcn, clazz.getName());
14 | }
15 |
16 | @Override
17 | public @NotNull CoreLogger getCoreLogger(@NotNull String fqcn, @NotNull String name) {
18 | return new Log4JCoreLogger(fqcn, (ExtendedLogger) LogManager.getLogger(name));
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/log4j/src/main/java/echopraxia/log4j/Log4JEchopraxiaServiceProvider.java:
--------------------------------------------------------------------------------
1 | package echopraxia.log4j;
2 |
3 | import echopraxia.logging.spi.EchopraxiaService;
4 | import echopraxia.logging.spi.EchopraxiaServiceProvider;
5 |
6 | public class Log4JEchopraxiaServiceProvider implements EchopraxiaServiceProvider {
7 | @Override
8 | public EchopraxiaService getEchopraxiaService() {
9 | return new Log4JEchopraxiaService();
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/log4j/src/main/java/echopraxia/log4j/Log4JLoggingContext.java:
--------------------------------------------------------------------------------
1 | package echopraxia.log4j;
2 |
3 | import static echopraxia.logging.spi.Utilities.joinFields;
4 | import static echopraxia.logging.spi.Utilities.memoize;
5 |
6 | import echopraxia.api.Field;
7 | import echopraxia.logging.api.LoggingContext;
8 | import echopraxia.logging.spi.CoreLogger;
9 | import java.util.Collections;
10 | import java.util.List;
11 | import java.util.function.Supplier;
12 | import org.apache.logging.log4j.Marker;
13 | import org.jetbrains.annotations.NotNull;
14 |
15 | public class Log4JLoggingContext implements LoggingContext {
16 | private final Supplier> argumentFields;
17 | private final Supplier> loggerFields;
18 | private final Supplier> joinedFields;
19 | private final Log4JCoreLogger.Context context;
20 | private final CoreLogger core;
21 |
22 | public Log4JLoggingContext(
23 | CoreLogger core, Log4JCoreLogger.Context context, Supplier> arguments) {
24 | // Defers and memoizes the arguments and context fields for a single logging statement.
25 | this.core = core;
26 | this.context = context;
27 | this.argumentFields = memoize(arguments);
28 | this.loggerFields = memoize(context::getLoggerFields);
29 | this.joinedFields = memoize(joinFields(this.loggerFields, this.argumentFields));
30 | }
31 |
32 | public Log4JLoggingContext(CoreLogger core, Log4JCoreLogger.Context context) {
33 | this(core, context, Collections::emptyList);
34 | }
35 |
36 | @Override
37 | public @NotNull List getFields() {
38 | return joinedFields.get();
39 | }
40 |
41 | @Override
42 | public List getArgumentFields() {
43 | return argumentFields.get();
44 | }
45 |
46 | @Override
47 | public List getLoggerFields() {
48 | return loggerFields.get();
49 | }
50 |
51 | public @NotNull Marker getMarker() {
52 | return context.getMarker();
53 | }
54 |
55 | @Override
56 | public CoreLogger getCore() {
57 | return core;
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/log4j/src/main/java/echopraxia/log4j/layout/AbstractEchopraxiaResolver.java:
--------------------------------------------------------------------------------
1 | package echopraxia.log4j.layout;
2 |
3 | import com.fasterxml.jackson.databind.ObjectMapper;
4 | import echopraxia.api.Field;
5 | import echopraxia.api.Value;
6 | import java.io.IOException;
7 | import java.io.StringWriter;
8 | import java.util.List;
9 | import org.apache.logging.log4j.core.LogEvent;
10 | import org.apache.logging.log4j.layout.template.json.resolver.EventResolver;
11 | import org.apache.logging.log4j.layout.template.json.util.JsonWriter;
12 |
13 | /** Creates a resolver (but it only goes under the `fields` and flatten doesn't work) */
14 | abstract class AbstractEchopraxiaResolver implements EventResolver {
15 |
16 | private static final ObjectMapper mapper = (new ObjectMapper()).findAndRegisterModules();
17 |
18 | @Override
19 | public boolean isResolvable(LogEvent logEvent) {
20 | return logEvent.getMessage() instanceof EchopraxiaFieldsMessage;
21 | }
22 |
23 | @Override
24 | public void resolve(LogEvent logEvent, JsonWriter jsonWriter) {
25 | EchopraxiaFieldsMessage message = (EchopraxiaFieldsMessage) logEvent.getMessage();
26 | List fields = resolveFields(message);
27 | StringWriter stringWriter = new StringWriter();
28 | try {
29 | Value> objectValue = Value.object(fields);
30 | mapper.writeValue(stringWriter, objectValue);
31 | jsonWriter.writeRawString(stringWriter.toString());
32 | } catch (IOException e) {
33 | throw new RuntimeException(e);
34 | }
35 | }
36 |
37 | protected abstract List resolveFields(EchopraxiaFieldsMessage message);
38 | }
39 |
--------------------------------------------------------------------------------
/log4j/src/main/java/echopraxia/log4j/layout/EchopraxiaArgumentFieldsResolverFactory.java:
--------------------------------------------------------------------------------
1 | package echopraxia.log4j.layout;
2 |
3 | import echopraxia.api.Field;
4 | import java.util.List;
5 | import org.apache.logging.log4j.core.config.plugins.Plugin;
6 | import org.apache.logging.log4j.core.config.plugins.PluginFactory;
7 | import org.apache.logging.log4j.layout.template.json.resolver.EventResolverContext;
8 | import org.apache.logging.log4j.layout.template.json.resolver.EventResolverFactory;
9 | import org.apache.logging.log4j.layout.template.json.resolver.TemplateResolverConfig;
10 | import org.apache.logging.log4j.layout.template.json.resolver.TemplateResolverFactory;
11 |
12 | @Plugin(name = "ArgumentFieldResolverFactory", category = TemplateResolverFactory.CATEGORY)
13 | public class EchopraxiaArgumentFieldsResolverFactory implements EventResolverFactory {
14 |
15 | private static final EchopraxiaArgumentFieldsResolverFactory INSTANCE =
16 | new EchopraxiaArgumentFieldsResolverFactory();
17 |
18 | private EchopraxiaArgumentFieldsResolverFactory() {}
19 |
20 | @PluginFactory
21 | public static EchopraxiaArgumentFieldsResolverFactory getInstance() {
22 | return INSTANCE;
23 | }
24 |
25 | @Override
26 | public String getName() {
27 | return EchopraxiaArgumentFieldsResolver.getName();
28 | }
29 |
30 | @Override
31 | public EchopraxiaArgumentFieldsResolver create(
32 | final EventResolverContext context, final TemplateResolverConfig config) {
33 | return EchopraxiaArgumentFieldsResolver.getInstance();
34 | }
35 |
36 | static final class EchopraxiaArgumentFieldsResolver extends AbstractEchopraxiaResolver {
37 |
38 | private static final EchopraxiaArgumentFieldsResolver INSTANCE =
39 | new EchopraxiaArgumentFieldsResolver();
40 |
41 | static EchopraxiaArgumentFieldsResolver getInstance() {
42 | return INSTANCE;
43 | }
44 |
45 | static String getName() {
46 | return "echopraxiaArgumentFields";
47 | }
48 |
49 | @Override
50 | protected List resolveFields(EchopraxiaFieldsMessage message) {
51 | return message.getArgumentFields();
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/log4j/src/main/java/echopraxia/log4j/layout/EchopraxiaContextFieldsResolverFactory.java:
--------------------------------------------------------------------------------
1 | package echopraxia.log4j.layout;
2 |
3 | import echopraxia.api.Field;
4 | import java.util.List;
5 | import org.apache.logging.log4j.core.config.plugins.Plugin;
6 | import org.apache.logging.log4j.core.config.plugins.PluginFactory;
7 | import org.apache.logging.log4j.layout.template.json.resolver.EventResolverContext;
8 | import org.apache.logging.log4j.layout.template.json.resolver.EventResolverFactory;
9 | import org.apache.logging.log4j.layout.template.json.resolver.TemplateResolverConfig;
10 | import org.apache.logging.log4j.layout.template.json.resolver.TemplateResolverFactory;
11 |
12 | @Plugin(name = "ContextFieldResolverFactory", category = TemplateResolverFactory.CATEGORY)
13 | public class EchopraxiaContextFieldsResolverFactory implements EventResolverFactory {
14 |
15 | private static final EchopraxiaContextFieldsResolverFactory INSTANCE =
16 | new EchopraxiaContextFieldsResolverFactory();
17 |
18 | private EchopraxiaContextFieldsResolverFactory() {}
19 |
20 | @PluginFactory
21 | public static EchopraxiaContextFieldsResolverFactory getInstance() {
22 | return INSTANCE;
23 | }
24 |
25 | @Override
26 | public String getName() {
27 | return EchopraxiaContextFieldsResolver.getName();
28 | }
29 |
30 | @Override
31 | public EchopraxiaContextFieldsResolver create(
32 | final EventResolverContext context, final TemplateResolverConfig config) {
33 | return EchopraxiaContextFieldsResolver.getInstance();
34 | }
35 |
36 | static final class EchopraxiaContextFieldsResolver extends AbstractEchopraxiaResolver {
37 |
38 | private static final EchopraxiaContextFieldsResolver INSTANCE =
39 | new EchopraxiaContextFieldsResolver();
40 |
41 | static EchopraxiaContextFieldsResolver getInstance() {
42 | return INSTANCE;
43 | }
44 |
45 | static String getName() {
46 | return "echopraxiaContextFields";
47 | }
48 |
49 | @Override
50 | protected List resolveFields(EchopraxiaFieldsMessage message) {
51 | return message.getLoggerFields();
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/log4j/src/main/java/echopraxia/log4j/layout/EchopraxiaFieldResolverFactory.java:
--------------------------------------------------------------------------------
1 | package echopraxia.log4j.layout;
2 |
3 | import echopraxia.api.Field;
4 | import java.util.List;
5 | import org.apache.logging.log4j.core.config.plugins.Plugin;
6 | import org.apache.logging.log4j.core.config.plugins.PluginFactory;
7 | import org.apache.logging.log4j.layout.template.json.resolver.EventResolverContext;
8 | import org.apache.logging.log4j.layout.template.json.resolver.EventResolverFactory;
9 | import org.apache.logging.log4j.layout.template.json.resolver.TemplateResolverConfig;
10 | import org.apache.logging.log4j.layout.template.json.resolver.TemplateResolverFactory;
11 |
12 | @Plugin(name = "FieldResolverFactory", category = TemplateResolverFactory.CATEGORY)
13 | public class EchopraxiaFieldResolverFactory implements EventResolverFactory {
14 |
15 | private static final EchopraxiaFieldResolverFactory INSTANCE =
16 | new EchopraxiaFieldResolverFactory();
17 |
18 | private EchopraxiaFieldResolverFactory() {}
19 |
20 | @PluginFactory
21 | public static EchopraxiaFieldResolverFactory getInstance() {
22 | return INSTANCE;
23 | }
24 |
25 | @Override
26 | public String getName() {
27 | return EchopraxiaFieldsResolver.getName();
28 | }
29 |
30 | @Override
31 | public EchopraxiaFieldsResolver create(
32 | final EventResolverContext context, final TemplateResolverConfig config) {
33 | return EchopraxiaFieldsResolver.getInstance();
34 | }
35 |
36 | static final class EchopraxiaFieldsResolver extends AbstractEchopraxiaResolver {
37 |
38 | private static final EchopraxiaFieldsResolver INSTANCE = new EchopraxiaFieldsResolver();
39 |
40 | static EchopraxiaFieldsResolver getInstance() {
41 | return INSTANCE;
42 | }
43 |
44 | static String getName() {
45 | return "echopraxiaFields";
46 | }
47 |
48 | @Override
49 | protected List resolveFields(EchopraxiaFieldsMessage message) {
50 | return message.getFields();
51 | }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/log4j/src/main/java/echopraxia/log4j/layout/EchopraxiaFieldsMessage.java:
--------------------------------------------------------------------------------
1 | package echopraxia.log4j.layout;
2 |
3 | import echopraxia.api.Field;
4 | import java.util.List;
5 | import java.util.stream.Collectors;
6 | import java.util.stream.Stream;
7 | import org.apache.logging.log4j.message.Message;
8 | import org.apache.logging.log4j.message.ParameterizedMessage;
9 |
10 | /** Create the simplest possible message for Log4J. */
11 | public class EchopraxiaFieldsMessage implements Message {
12 |
13 | private final String format;
14 | private final List argumentFields;
15 | private final List loggerFields;
16 | private final String formattedMessage;
17 |
18 | public EchopraxiaFieldsMessage(
19 | String format, List loggerFields, List argumentFields) {
20 | this.format = format;
21 | this.argumentFields = argumentFields;
22 | this.loggerFields = loggerFields;
23 | this.formattedMessage = ParameterizedMessage.format(getFormat(), getParameters());
24 | }
25 |
26 | @Override
27 | public String getFormattedMessage() {
28 | return formattedMessage;
29 | }
30 |
31 | @Override
32 | public String getFormat() {
33 | return format;
34 | }
35 |
36 | @Override
37 | public Object[] getParameters() {
38 | return argumentFields.toArray();
39 | }
40 |
41 | public List getArgumentFields() {
42 | return argumentFields;
43 | }
44 |
45 | public List getLoggerFields() {
46 | return loggerFields;
47 | }
48 |
49 | public List getFields() {
50 | return Stream.concat(argumentFields.stream(), loggerFields.stream())
51 | .collect(Collectors.toList());
52 | }
53 |
54 | // It looks like nothing actually uses message.getThrowable() internally
55 | // apart from maybe LocalizedMessage, find usages returns nothing useful.
56 | public Throwable getThrowable() {
57 | return null;
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/log4j/src/main/resources/META-INF/services/echopraxia.logging.spi.EchopraxiaServiceProvider:
--------------------------------------------------------------------------------
1 | echopraxia.log4j.Log4JEchopraxiaServiceProvider
2 |
--------------------------------------------------------------------------------
/log4j/src/test/java/echopraxia/log4j/ExceptionHandlerTests.java:
--------------------------------------------------------------------------------
1 | package echopraxia.log4j;
2 |
3 | import static org.assertj.core.api.Assertions.assertThat;
4 |
5 | import echopraxia.logging.api.Condition;
6 | import org.junit.jupiter.api.Test;
7 |
8 | public class ExceptionHandlerTests extends TestBase {
9 |
10 | @Test
11 | public void testBadArgument() {
12 | var logger = getLogger();
13 | Integer number = null;
14 | logger.debug("this has a null value", fb -> fb.number("nullNumber", number.intValue()));
15 |
16 | Throwable throwable = StaticExceptionHandler.head();
17 | assertThat(throwable).isInstanceOf(NullPointerException.class);
18 | }
19 |
20 | @Test
21 | public void testBadWithField() {
22 | var logger = getLogger();
23 | Integer number = null;
24 | var badLogger = logger.withFields(fb -> fb.number("nullNumber", number.intValue()));
25 | badLogger.debug("this has a null value");
26 |
27 | Throwable throwable = StaticExceptionHandler.head();
28 | assertThat(throwable).isInstanceOf(NullPointerException.class);
29 | }
30 |
31 | @Test
32 | public void testConditionAndBadWithField() {
33 | var logger = getLogger();
34 | Integer number = null;
35 | Condition condition = Condition.numberMatch("testing", p -> p.raw().intValue() == 5);
36 |
37 | var badLogger = logger.withFields(fb -> fb.number("nullNumber", number.intValue()));
38 | badLogger.debug(condition, "I have a bad logger field and a good condition");
39 |
40 | Throwable throwable = StaticExceptionHandler.head();
41 | assertThat(throwable).isInstanceOf(NullPointerException.class);
42 | }
43 |
44 | @Test
45 | public void testBadConditionWithCondition() {
46 | var logger = getLogger();
47 | Integer number = null;
48 | Condition badCondition =
49 | Condition.numberMatch("testing", p -> p.raw().intValue() == number.intValue());
50 | var badLogger = logger.withCondition(badCondition);
51 | badLogger.debug("I am passing in {}", fb -> fb.number("testing", 5));
52 |
53 | Throwable throwable = StaticExceptionHandler.head();
54 | assertThat(throwable).isInstanceOf(NullPointerException.class);
55 | }
56 |
57 | @Test
58 | public void testBadCondition() {
59 | var logger = getLogger();
60 | Integer number = null;
61 | Condition badCondition = (level, context) -> number.intValue() == 5;
62 |
63 | logger.debug(badCondition, "I am passing in {}");
64 |
65 | Throwable throwable = StaticExceptionHandler.head();
66 | assertThat(throwable).isInstanceOf(NullPointerException.class);
67 | }
68 |
69 | @Test
70 | public void testBadConditionAndArgument() {
71 | var logger = getLogger();
72 | Integer number = null;
73 | Condition badCondition =
74 | Condition.numberMatch("testing", p -> p.raw().intValue() == number.intValue());
75 |
76 | logger.debug(
77 | badCondition, "I am passing in {}", fb -> fb.number("nullNumber", number.intValue()));
78 |
79 | Throwable throwable = StaticExceptionHandler.head();
80 | assertThat(throwable).isInstanceOf(NullPointerException.class);
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/log4j/src/test/java/echopraxia/log4j/StaticExceptionHandler.java:
--------------------------------------------------------------------------------
1 | package echopraxia.log4j;
2 |
3 | import echopraxia.logging.spi.ExceptionHandler;
4 | import java.util.ArrayList;
5 | import java.util.List;
6 |
7 | public class StaticExceptionHandler implements ExceptionHandler {
8 |
9 | private static final List exceptions = new ArrayList<>();
10 |
11 | public static Throwable head() {
12 | return exceptions.get(0);
13 | }
14 |
15 | public static void clear() {
16 | exceptions.clear();
17 | }
18 |
19 | @Override
20 | public void handleException(Throwable e) {
21 | exceptions.add(e);
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/log4j/src/test/java/echopraxia/log4j/TestBase.java:
--------------------------------------------------------------------------------
1 | package echopraxia.log4j;
2 |
3 | import static echopraxia.log4j.appender.ListAppender.getListAppender;
4 |
5 | import com.fasterxml.jackson.core.JsonProcessingException;
6 | import com.fasterxml.jackson.databind.JsonNode;
7 | import com.fasterxml.jackson.databind.ObjectMapper;
8 | import echopraxia.api.FieldBuilder;
9 | import echopraxia.log4j.appender.ListAppender;
10 | import echopraxia.logger.Logger;
11 | import echopraxia.logger.LoggerFactory;
12 | import java.util.List;
13 | import org.jetbrains.annotations.NotNull;
14 | import org.junit.jupiter.api.BeforeEach;
15 |
16 | public class TestBase {
17 |
18 | @BeforeEach
19 | void beforeEach() {
20 | StaticExceptionHandler.clear();
21 | final ListAppender listAppender = getListAppender("ListAppender");
22 | listAppender.clear();
23 | }
24 |
25 | @NotNull
26 | Logger getLogger() {
27 | return LoggerFactory.getLogger();
28 | }
29 |
30 | void waitUntilMessages() {
31 | final ListAppender listAppender = getListAppender("ListAppender");
32 | org.awaitility.Awaitility.await().until(() -> !listAppender.getMessages().isEmpty());
33 | }
34 |
35 | JsonNode getEntry() {
36 | final ListAppender listAppender = getListAppender("ListAppender");
37 | final List messages = listAppender.getMessages();
38 |
39 | final String jsonLine = messages.get(0);
40 | try {
41 | return mapper.readTree(jsonLine);
42 | } catch (JsonProcessingException e) {
43 | throw new RuntimeException(e);
44 | }
45 | }
46 |
47 | private static final ObjectMapper mapper = new ObjectMapper().findAndRegisterModules();
48 | }
49 |
--------------------------------------------------------------------------------
/log4j/src/test/java/echopraxia/log4j/TestEchopraxiaService.java:
--------------------------------------------------------------------------------
1 | package echopraxia.log4j;
2 |
3 | public class TestEchopraxiaService extends Log4JEchopraxiaService {
4 |
5 | public TestEchopraxiaService() {
6 | super();
7 | this.exceptionHandler = new StaticExceptionHandler();
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/log4j/src/test/java/echopraxia/log4j/TestEchopraxiaServiceProvider.java:
--------------------------------------------------------------------------------
1 | package echopraxia.log4j;
2 |
3 | import echopraxia.logging.spi.EchopraxiaService;
4 | import echopraxia.logging.spi.EchopraxiaServiceProvider;
5 |
6 | public class TestEchopraxiaServiceProvider implements EchopraxiaServiceProvider {
7 | @Override
8 | public EchopraxiaService getEchopraxiaService() {
9 | return new TestEchopraxiaService();
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/log4j/src/test/resources/META-INF/services/echopraxia.logging.spi.EchopraxiaServiceProvider:
--------------------------------------------------------------------------------
1 | echopraxia.log4j.TestEchopraxiaServiceProvider
--------------------------------------------------------------------------------
/log4j/src/test/resources/log4j2.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
14 |
18 |
22 |
26 |
27 |
28 |
29 |
30 |
34 |
38 |
42 |
46 |
47 |
48 |
49 |
50 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
--------------------------------------------------------------------------------
/logback/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'java-library'
3 | }
4 |
5 | dependencies {
6 | api project(":logging")
7 | api project(":jsonpath")
8 |
9 | // We don't depend on SLF4J 2.x or Logback 1.3 features, but we don't want to bind
10 | // consumers to them either.
11 | compileOnly "org.slf4j:slf4j-api:$slf4jApiVersion"
12 | compileOnly "ch.qos.logback:logback-classic:$logbackVersion"
13 |
14 | testImplementation(testFixtures(project(':logging')))
15 | testImplementation "org.slf4j:slf4j-api:$slf4jApiVersion"
16 | testImplementation "ch.qos.logback:logback-classic:$logbackVersion"
17 | }
18 |
--------------------------------------------------------------------------------
/logback/src/main/java/echopraxia/logback/AbstractEventLoggingContext.java:
--------------------------------------------------------------------------------
1 | package echopraxia.logback;
2 |
3 | import ch.qos.logback.classic.spi.ILoggingEvent;
4 | import echopraxia.api.Field;
5 | import echopraxia.jsonpath.AbstractJsonPathFinder;
6 | import echopraxia.logging.api.LoggingContextWithFindPathMethods;
7 | import java.util.*;
8 | import java.util.stream.Collectors;
9 | import java.util.stream.Stream;
10 | import java.util.stream.StreamSupport;
11 | import org.jetbrains.annotations.NotNull;
12 | import org.slf4j.Marker;
13 |
14 | public abstract class AbstractEventLoggingContext extends AbstractJsonPathFinder
15 | implements LoggingContextWithFindPathMethods {
16 |
17 | protected List fieldArguments(@NotNull ILoggingEvent event) {
18 | final Object[] argumentArray = event.getArgumentArray();
19 | if (argumentArray == null) {
20 | return Collections.emptyList();
21 | }
22 | return Arrays.stream(argumentArray).flatMap(this::toField).collect(Collectors.toList());
23 | }
24 |
25 | protected List fieldMarkers(@NotNull ILoggingEvent event) {
26 | Marker m = event.getMarker();
27 | if (m == null) {
28 | return Collections.emptyList();
29 | }
30 | return markerStream(m).flatMap(this::toField).collect(Collectors.toList());
31 | }
32 |
33 | protected Stream markerStream(@NotNull Marker m) {
34 | return StreamSupport.stream(
35 | Spliterators.spliteratorUnknownSize(m.iterator(), Spliterator.ORDERED), false);
36 | }
37 |
38 | protected Stream toField(Object arg) {
39 | return arg instanceof Field ? Stream.of((Field) arg) : Stream.empty();
40 | }
41 |
42 | public @NotNull Optional