├── .dotty-ide-disabled ├── .gitattributes ├── .github └── workflows │ └── workflow.yml ├── .gitignore ├── .sdkmanrc ├── BENCHMARKS.md ├── CHANGELOG.md ├── LICENSE ├── README.md ├── RELEASING.md ├── api ├── build.gradle └── src │ ├── main │ ├── java │ │ └── echopraxia │ │ │ └── api │ │ │ ├── Attribute.java │ │ │ ├── AttributeKey.java │ │ │ ├── Attributes.java │ │ │ ├── AttributesAware.java │ │ │ ├── DefaultField.java │ │ │ ├── DefaultToStringFormatter.java │ │ │ ├── Field.java │ │ │ ├── FieldBuilder.java │ │ │ ├── FieldBuilderResult.java │ │ │ ├── FieldConstants.java │ │ │ ├── FieldVisitor.java │ │ │ ├── FindPathMethods.java │ │ │ ├── PresentationHintAttributes.java │ │ │ ├── PresentationHints.java │ │ │ ├── SimpleFieldVisitor.java │ │ │ ├── ToStringFormatter.java │ │ │ └── Value.java │ └── resources │ │ └── echopraxia │ │ └── fields.properties │ └── test │ └── java │ └── echopraxia │ └── api │ ├── AbbreviateAfterTests.java │ ├── AbbreviationTests.java │ ├── CardinalTests.java │ ├── DisplayNameTests.java │ ├── ElidedTests.java │ ├── EqualityTests.java │ ├── FormatTests.java │ ├── ToStringFormatTests.java │ ├── ToStringValueTests.java │ └── ValueTests.java ├── build.gradle ├── docs ├── faq.md ├── frameworks │ ├── log4j2.md │ └── logback.md ├── index.md ├── installation.md ├── logging.png └── usage │ ├── basics.md │ ├── conditions.md │ ├── context.md │ ├── fieldbuilder.md │ ├── filters.md │ ├── logger.md │ └── scripting.md ├── filewatch ├── build.gradle └── src │ ├── main │ ├── java │ │ └── echopraxia │ │ │ └── filewatch │ │ │ ├── FileWatchEvent.java │ │ │ ├── FileWatchService.java │ │ │ ├── FileWatchServiceFactory.java │ │ │ ├── FileWatchServiceProvider.java │ │ │ └── dirwatcher │ │ │ ├── DefaultFileWatchService.java │ │ │ └── DefaultFileWatchServiceProvider.java │ └── resources │ │ └── META-INF │ │ └── services │ │ └── echopraxia.filewatch.FileWatchServiceProvider │ └── test │ └── java │ └── echopraxia │ └── filewatch │ └── FileWatchTest.java ├── gradle.properties ├── gradle ├── java-publication.gradle ├── release.gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── jackson ├── build.gradle └── src │ ├── main │ ├── java │ │ └── echopraxia │ │ │ └── jackson │ │ │ ├── EchopraxiaModule.java │ │ │ ├── FieldSerializer.java │ │ │ ├── ObjectMapperProvider.java │ │ │ ├── ValueDeserializer.java │ │ │ └── ValueSerializer.java │ └── resources │ │ └── META-INF │ │ └── services │ │ └── com.fasterxml.jackson.databind.Module │ └── test │ └── java │ └── echopraxia │ └── jackson │ └── VisitorTests.java ├── jsonpath ├── build.gradle └── src │ ├── main │ └── java │ │ └── echopraxia │ │ └── jsonpath │ │ ├── AbstractJsonPathFinder.java │ │ ├── EchopraxiaJsonProvider.java │ │ └── EchopraxiaMappingProvider.java │ └── test │ └── java │ └── echopraxia │ └── jsonpath │ └── JsonPathTests.java ├── jul ├── build.gradle └── src │ ├── main │ ├── java │ │ └── echopraxia │ │ │ └── jul │ │ │ ├── EchopraxiaLogRecord.java │ │ │ ├── JULCoreLogger.java │ │ │ ├── JULEchopraxiaService.java │ │ │ ├── JULEchopraxiaServiceProvider.java │ │ │ ├── JULJSONFormatter.java │ │ │ ├── JULLoggerContext.java │ │ │ └── JULLoggingContext.java │ └── resources │ │ ├── META-INF │ │ └── services │ │ │ └── echopraxia.logging.spi.EchopraxiaServiceProvider │ │ └── echopraxia │ │ └── jsonformatter.properties │ └── test │ ├── java │ └── echopraxia │ │ └── jul │ │ ├── EncodedListHandler.java │ │ ├── ExceptionHandlerTests.java │ │ ├── JSONFormatterTest.java │ │ ├── JULLoggerTest.java │ │ ├── LoggerFactoryTest.java │ │ ├── StaticExceptionHandler.java │ │ ├── TestBase.java │ │ ├── TestEchopraxiaService.java │ │ └── TestEchopraxiaServiceProvider.java │ └── resources │ └── META-INF │ └── services │ └── echopraxia.logging.spi.EchopraxiaServiceProvider ├── log4j ├── benchmarks │ └── 17.0.3.6.1-amzn │ │ └── 20220715T110146.json ├── build.gradle └── src │ ├── jmh │ ├── java │ │ └── echopraxia │ │ │ └── log4j │ │ │ ├── CoreLoggerBenchmarks.java │ │ │ ├── Log4JBenchmarks.java │ │ │ └── LoggerBenchmarks.java │ └── resources │ │ └── log4j2.xml │ ├── main │ ├── java │ │ └── echopraxia │ │ │ └── log4j │ │ │ ├── Log4JCoreLogger.java │ │ │ ├── Log4JEchopraxiaService.java │ │ │ ├── Log4JEchopraxiaServiceProvider.java │ │ │ ├── Log4JLoggingContext.java │ │ │ └── layout │ │ │ ├── AbstractEchopraxiaResolver.java │ │ │ ├── EchopraxiaArgumentFieldsResolverFactory.java │ │ │ ├── EchopraxiaContextFieldsResolverFactory.java │ │ │ ├── EchopraxiaFieldResolverFactory.java │ │ │ └── EchopraxiaFieldsMessage.java │ └── resources │ │ └── META-INF │ │ └── services │ │ └── echopraxia.logging.spi.EchopraxiaServiceProvider │ └── test │ ├── java │ └── echopraxia │ │ └── log4j │ │ ├── ConditionTest.java │ │ ├── ContextTest.java │ │ ├── ExceptionHandlerTests.java │ │ ├── Log4JLoggerTest.java │ │ ├── StaticExceptionHandler.java │ │ ├── TestBase.java │ │ ├── TestEchopraxiaService.java │ │ ├── TestEchopraxiaServiceProvider.java │ │ └── appender │ │ └── ListAppender.java │ └── resources │ ├── META-INF │ └── services │ │ └── echopraxia.logging.spi.EchopraxiaServiceProvider │ └── log4j2.xml ├── logback ├── build.gradle └── src │ ├── main │ └── java │ │ └── echopraxia │ │ └── logback │ │ ├── AbstractEventLoggingContext.java │ │ ├── AbstractPathConverter.java │ │ ├── ArgumentFieldConverter.java │ │ ├── ArgumentLoggingContext.java │ │ ├── BaseMarker.java │ │ ├── CallerDataAppender.java │ │ ├── CallerMarker.java │ │ ├── ConditionMarker.java │ │ ├── ConditionTurboFilter.java │ │ ├── DirectFieldMarker.java │ │ ├── FieldConverter.java │ │ ├── FieldLoggingContext.java │ │ ├── LogbackLoggerContext.java │ │ ├── LogbackLoggingContext.java │ │ ├── LoggerFieldConverter.java │ │ ├── MarkerLoggingContext.java │ │ └── TransformingAppender.java │ └── test │ ├── java │ └── echopraxia │ │ └── logback │ │ ├── ConditionTurboFilterTest.java │ │ └── TestBase.java │ └── resources │ └── logback-test.xml ├── logger ├── build.gradle └── src │ └── main │ └── java │ └── echopraxia │ └── logger │ ├── DefaultLoggerMethods.java │ ├── Logger.java │ ├── LoggerFactory.java │ └── LoggerMethods.java ├── logging ├── build.gradle └── src │ ├── main │ └── java │ │ └── echopraxia │ │ └── logging │ │ ├── api │ │ ├── Condition.java │ │ ├── JsonPathCondition.java │ │ ├── Level.java │ │ ├── LoggerHandle.java │ │ ├── LoggingContext.java │ │ └── LoggingContextWithFindPathMethods.java │ │ └── spi │ │ ├── AbstractEchopraxiaService.java │ │ ├── AbstractLoggerSupport.java │ │ ├── Caller.java │ │ ├── CoreLogger.java │ │ ├── CoreLoggerFactory.java │ │ ├── CoreLoggerFilter.java │ │ ├── DefaultMethodsSupport.java │ │ ├── DelegateCoreLogger.java │ │ ├── EchopraxiaService.java │ │ ├── EchopraxiaServiceProvider.java │ │ ├── ExceptionHandler.java │ │ ├── Filters.java │ │ ├── LoggerContext.java │ │ └── Utilities.java │ ├── test │ ├── java │ │ └── echopraxia │ │ │ └── logging │ │ │ └── api │ │ │ ├── ConditionTests.java │ │ │ ├── FilterTests.java │ │ │ ├── LevelTests.java │ │ │ └── TestFilter.java │ └── resources │ │ └── echopraxia.properties │ └── testFixtures │ ├── java │ └── echopraxia │ │ └── logging │ │ └── fake │ │ ├── FakeCoreLogger.java │ │ ├── FakeEchopraxiaService.java │ │ ├── FakeEchopraxiaServiceProvider.java │ │ ├── FakeLoggerContext.java │ │ └── FakeLoggingContext.java │ └── resources │ └── META-INF │ └── services │ └── echopraxia.logging.spi.EchopraxiaServiceProvider ├── logstash ├── benchmarks │ └── 17.0.3.6.1-amzn │ │ └── 20220715T101003.json ├── build.gradle └── src │ ├── jmh │ ├── java │ │ └── echopraxia │ │ │ └── logstash │ │ │ ├── CoreLoggerBenchmarks.java │ │ │ ├── FakeLoggingContext.java │ │ │ ├── JsonPathBenchmarks.java │ │ │ ├── LoggerBenchmarks.java │ │ │ └── SLF4JLoggerBenchmarks.java │ └── resources │ │ └── logback.xml │ ├── main │ ├── java │ │ └── echopraxia │ │ │ └── logstash │ │ │ ├── FieldMarker.java │ │ │ ├── LogstashCoreLogger.java │ │ │ ├── LogstashEchopraxiaService.java │ │ │ ├── LogstashEchopraxiaServiceProvider.java │ │ │ ├── LogstashFieldAppender.java │ │ │ └── MappedFieldMarker.java │ └── resources │ │ └── META-INF │ │ └── services │ │ └── echopraxia.logging.spi.EchopraxiaServiceProvider │ └── test │ ├── java │ └── echopraxia │ │ └── logstash │ │ ├── ConditionTest.java │ │ ├── ContextTest.java │ │ ├── ConverterTest.java │ │ ├── DirectTest.java │ │ ├── EncodingListAppender.java │ │ ├── ExceptionHandlerTests.java │ │ ├── LoggerFactoryTest.java │ │ ├── LogstashLoggerTest.java │ │ ├── StaticExceptionHandler.java │ │ ├── TestBase.java │ │ ├── TestEchopraxiaService.java │ │ └── TestEchopraxiaServiceProvider.java │ └── resources │ ├── META-INF │ └── services │ │ └── echopraxia.logging.spi.EchopraxiaServiceProvider │ ├── logback-converter.xml │ ├── logback-direct-test.xml │ ├── logback-test.xml │ └── logback.xml ├── mkdocs.yml ├── noop ├── build.gradle └── src │ └── main │ ├── java │ └── echopraxia │ │ └── noop │ │ ├── NoopCoreLogger.java │ │ ├── NoopEchopraxiaService.java │ │ ├── NoopEchopraxiaServiceProvider.java │ │ ├── NoopLoggerContext.java │ │ └── NoopLoggingContext.java │ └── resources │ └── META-INF │ └── services │ └── echopraxia.logging.spi.EchopraxiaServiceProvider ├── scripting ├── benchmarks │ └── 17.0.3.6.1-amzn │ │ └── 20220715T144743.json ├── build.gradle └── src │ ├── jmh │ ├── java │ │ └── echopraxia │ │ │ └── scripting │ │ │ ├── FakeLoggingContext.java │ │ │ └── ScriptingBenchmarks.java │ ├── resources │ │ └── logback.xml │ └── tweakflow │ │ └── condition.tf │ ├── main │ └── java │ │ └── echopraxia │ │ └── scripting │ │ ├── FileScriptHandle.java │ │ ├── ScriptCondition.java │ │ ├── ScriptException.java │ │ ├── ScriptFunction.java │ │ ├── ScriptHandle.java │ │ ├── ScriptManager.java │ │ ├── ScriptWatchService.java │ │ └── ValueMapEntry.java │ └── test │ ├── java │ └── echopraxia │ │ └── scripting │ │ ├── FakeLoggingContext.java │ │ ├── Main.java │ │ ├── ScriptConditionTest.java │ │ └── ScriptManagerTest.java │ ├── resources │ └── logback-test.xml │ └── tweakflow │ ├── condition.tf │ └── exception.tf ├── settings.gradle └── simple ├── build.gradle └── src ├── main └── java │ └── echopraxia │ └── simple │ ├── Logger.java │ └── LoggerFactory.java └── test └── java └── echopraxia └── MyLogger.java /.dotty-ide-disabled: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tersesystems/echopraxia/c7d34f295654b57e7e78d3f625c8130c15c7e46b/.dotty-ide-disabled -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # 2 | # https://help.github.com/articles/dealing-with-line-endings/ 3 | # 4 | # These are explicitly windows files and should use crlf 5 | *.bat text eol=crlf 6 | 7 | -------------------------------------------------------------------------------- /.github/workflows/workflow.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | 6 | name: Java CI 7 | 8 | on: [push] 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout sources 15 | uses: actions/checkout@v4 16 | - name: Setup Java 17 | uses: actions/setup-java@v4 18 | with: 19 | distribution: 'temurin' 20 | java-version: 17 21 | - name: Setup Gradle 22 | uses: gradle/actions/setup-gradle@v4 23 | - name: Build with Gradle 24 | run: ./gradlew build -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore Gradle project-specific cache directory 2 | .gradle 3 | 4 | # Ignore Gradle build output directory 5 | build 6 | 7 | .idea/ 8 | .bsp/ 9 | .bloop/ 10 | .metals/ 11 | .vscode/ 12 | 13 | bin/ 14 | 15 | project/ 16 | target/ 17 | 18 | *.log 19 | 20 | target/ 21 | .bsp/ 22 | 23 | .uuid -------------------------------------------------------------------------------- /.sdkmanrc: -------------------------------------------------------------------------------- 1 | # run `sdk env` to set the java version 2 | java=17.0.6-amzn 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2021 Terse Systems 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. -------------------------------------------------------------------------------- /RELEASING.md: -------------------------------------------------------------------------------- 1 | ## Release 2 | 3 | To make sure everything works: 4 | 5 | ```bash 6 | ./gradlew clean build check 7 | ``` 8 | 9 | To format everything using [Spotless](https://github.com/diffplug/spotless/tree/master/plugin-gradle): 10 | 11 | ```bash 12 | ./gradlew spotlessApply 13 | ``` 14 | 15 | First, try publishing to maven local: 16 | 17 | ```bash 18 | ./gradlew publishToMavenLocal 19 | ``` 20 | 21 | If that works, then publish to Sonatype's staging repository and close: 22 | 23 | ```bash 24 | ./gradlew publishToSonatype closeSonatypeStagingRepository 25 | ``` 26 | 27 | Inspect this in Sonatype OHSSH repository. Delete the staging repository after inspection. 28 | 29 | And then to promote it: 30 | 31 | ```bash 32 | ./gradlew publishToSonatype closeAndReleaseSonatypeStagingRepository 33 | ``` 34 | 35 | If it looks weird that you have to specify "publishToSonatype" with another task, that's because [it is weird](https://github.com/gradle-nexus/publish-plugin/issues/19). 36 | 37 | ## Gradle Signing 38 | 39 | If you run into errors with signing doing a `publishToSonaType`, this is common and underdocumented. 40 | 41 | ``` 42 | No value has been specified for property 'signatory.keyId'. 43 | ``` 44 | 45 | For the `signatory.keyId` error message, you need to set `signing.gnupg.keyName` if you 46 | are using GPG 2.1 and a Yubikey 4. 47 | 48 | https://docs.gradle.org/current/userguide/signing_plugin.html#sec:signatory_credentials 49 | https://github.com/gradle/gradle/pull/1703/files#diff-6c52391bbdceb4cca64ce7b03e78212fR6 50 | 51 | Note you need to use `gpg -K` and pick only the LAST EIGHT CHARS of the public signing key. 52 | 53 | > signing.gnupg.keyName = 5F798D53 54 | 55 | ### PinEntry 56 | 57 | Also note that if you are using a Yubikey, it'll require you to type in a PIN, which screws up Gradle. 58 | 59 | ``` 60 | gpg: signing failed: No pinentry 61 | ``` 62 | 63 | So you need to use pinentry-mode loopback, which is helpfully supplied by passphrase. 64 | 65 | - https://github.com/sbt/sbt-pgp/pull/142 66 | - https://wiki.archlinux.org/index.php/GnuPG#Unattended_passphrase 67 | - https://github.com/gradle/gradle/pull/1703/files#diff-790036df959521791fdafe474b673924 68 | 69 | You want this specified only the command line, i.e. 70 | 71 | > $ HISTCONTROL=ignoreboth ./gradlew publishToMavenLocal -Psigning.gnupg.passphrase=$PGP_PASSPHRASE --info 72 | 73 | ### Cannot Allocate Memory 74 | 75 | gpg can't be run in parallel. You'll get this error message. 76 | 77 | ``` 78 | gpg: signing failed: Cannot allocate memory 79 | ``` 80 | [Gradle is not smart enough to disable this](https://github.com/gradle/gradle/issues/12167). 81 | 82 | Do not use `-Porg.gradle.parallel=false` and don't use `--parallel` when publishing. 83 | 84 | ## Snapshots 85 | 86 | If you deploy snapshots to sonatype, you don't get the jar files. 87 | 88 | No idea why this happens, but for now only use staging without the -SNAPSHOT prefix. 89 | -------------------------------------------------------------------------------- /api/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java-library' 3 | } 4 | 5 | dependencies { 6 | implementation 'org.pcollections:pcollections:4.0.1' 7 | } 8 | -------------------------------------------------------------------------------- /api/src/main/java/echopraxia/api/Attribute.java: -------------------------------------------------------------------------------- 1 | package echopraxia.api; 2 | 3 | import java.util.Objects; 4 | 5 | /** 6 | * A typed attribute with a key and value. 7 | * 8 | * @param the type of the attribute value. 9 | * @since 3.0 10 | */ 11 | public final class Attribute { 12 | private final AttributeKey key; 13 | private final A value; 14 | 15 | public Attribute(AttributeKey key, A value) { 16 | this.key = key; 17 | this.value = value; 18 | } 19 | 20 | public AttributeKey key() { 21 | return key; 22 | } 23 | 24 | public A value() { 25 | return value; 26 | } 27 | 28 | @Override 29 | public boolean equals(Object o) { 30 | if (this == o) return true; 31 | if (o == null || getClass() != o.getClass()) return false; 32 | Attribute attribute = (Attribute) o; 33 | return Objects.equals(key, attribute.key) && Objects.equals(value, attribute.value); 34 | } 35 | 36 | @Override 37 | public int hashCode() { 38 | return Objects.hash(key, value); 39 | } 40 | 41 | @Override 42 | public String toString() { 43 | return "Attribute{" + "key=" + key + ", value=" + value + '}'; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /api/src/main/java/echopraxia/api/AttributeKey.java: -------------------------------------------------------------------------------- 1 | package echopraxia.api; 2 | 3 | import java.util.Optional; 4 | import org.jetbrains.annotations.NotNull; 5 | 6 | /** 7 | * A typed attribute key. Can be used with a display name argument for better visibility. 8 | * 9 | * @param The type of the attribute. 10 | * @since 3.0 11 | */ 12 | public final class AttributeKey { 13 | 14 | private final String displayName; 15 | 16 | private AttributeKey() { 17 | this.displayName = null; 18 | } 19 | 20 | private AttributeKey(@NotNull String displayName) { 21 | this.displayName = displayName; 22 | } 23 | 24 | @NotNull 25 | public Attribute bindValue(A value) { 26 | return new Attribute<>(this, value); 27 | } 28 | 29 | @NotNull 30 | public static AttributeKey create() { 31 | return new AttributeKey<>(); 32 | } 33 | 34 | @NotNull 35 | public static AttributeKey create(@NotNull String displayName) { 36 | return new AttributeKey<>(displayName); 37 | } 38 | 39 | @Override 40 | public String toString() { 41 | return Optional.ofNullable(displayName).orElse(super.toString()); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /api/src/main/java/echopraxia/api/AttributesAware.java: -------------------------------------------------------------------------------- 1 | package echopraxia.api; 2 | 3 | import java.util.Collection; 4 | import org.jetbrains.annotations.NotNull; 5 | 6 | public interface AttributesAware { 7 | 8 | /** 9 | * @return a field with the given attribute added. 10 | * @since 3.0 11 | */ 12 | @NotNull 13 | F withAttribute(@NotNull Attribute attr); 14 | 15 | /** 16 | * @return a field with the given attributes added. 17 | * @since 3.0 18 | */ 19 | @NotNull 20 | F withAttributes(@NotNull Attributes attrs); 21 | 22 | /** 23 | * @return a field without the attribute with the given key. 24 | * @since 3.0 25 | */ 26 | @NotNull 27 | F withoutAttribute(@NotNull AttributeKey key); 28 | 29 | /** 30 | * @return a field without the attributes with the given keys. 31 | * @since 3.0 32 | */ 33 | @NotNull 34 | F withoutAttributes(@NotNull Collection> keys); 35 | 36 | /** 37 | * @return a field without no attributes set. 38 | * @since 3.0 39 | */ 40 | @NotNull 41 | F clearAttributes(); 42 | } 43 | -------------------------------------------------------------------------------- /api/src/main/java/echopraxia/api/FieldBuilderResult.java: -------------------------------------------------------------------------------- 1 | package echopraxia.api; 2 | 3 | import java.util.*; 4 | import java.util.function.Supplier; 5 | import java.util.stream.Collectors; 6 | import java.util.stream.Stream; 7 | import java.util.stream.StreamSupport; 8 | import org.jetbrains.annotations.Contract; 9 | import org.jetbrains.annotations.NotNull; 10 | 11 | /** 12 | * This interface is the result end of the FieldBuilder -> FieldBuilderResult function used for 13 | * arguments in loggers. It abstracts away the "list of fields" vs "single field" result issue we'd 14 | * have otherwise. 15 | */ 16 | public interface FieldBuilderResult { 17 | 18 | @NotNull 19 | List fields(); 20 | 21 | @Contract(pure = true) 22 | static @NotNull FieldBuilderResult empty() { 23 | return Collections::emptyList; 24 | } 25 | 26 | @Contract(pure = true) 27 | static @NotNull FieldBuilderResult flatten(Supplier supplier) { 28 | return () -> supplier.get().fields(); 29 | } 30 | 31 | @Contract(pure = true) 32 | static @NotNull FieldBuilderResult only(@NotNull Field field) { 33 | return () -> Collections.singletonList(field); 34 | } 35 | 36 | @Contract(pure = true) 37 | static @NotNull FieldBuilderResult list(@NotNull List list) { 38 | return () -> list; 39 | } 40 | 41 | @Contract(pure = true) 42 | static @NotNull FieldBuilderResult list(@NotNull Field[] array) { 43 | return () -> Arrays.asList(array); 44 | } 45 | 46 | @Contract(pure = true) 47 | static @NotNull FieldBuilderResult list(FieldBuilderResult[] results) { 48 | return () -> 49 | Arrays.stream(results).flatMap(f -> f.fields().stream()).collect(Collectors.toList()); 50 | } 51 | 52 | @Contract(pure = true) 53 | static @NotNull FieldBuilderResult list(@NotNull Iterable iterable) { 54 | return list(iterable.spliterator()); 55 | } 56 | 57 | @Contract(pure = true) 58 | static @NotNull FieldBuilderResult list(@NotNull Spliterator fieldSpliterator) { 59 | return list(StreamSupport.stream(fieldSpliterator, false)); 60 | } 61 | 62 | @Contract(pure = true) 63 | static @NotNull FieldBuilderResult list(@NotNull Iterator iterator) { 64 | return list(Spliterators.spliteratorUnknownSize(iterator, 0)); 65 | } 66 | 67 | @Contract(pure = true) 68 | static @NotNull FieldBuilderResult list(@NotNull Stream stream) { 69 | return () -> stream.collect(Collectors.toList()); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /api/src/main/java/echopraxia/api/FieldConstants.java: -------------------------------------------------------------------------------- 1 | package echopraxia.api; 2 | 3 | import java.util.ResourceBundle; 4 | 5 | public final class FieldConstants { 6 | 7 | private static final ResourceBundle bundle = ResourceBundle.getBundle("echopraxia/fields"); 8 | 9 | public static final String EXCEPTION = bundle.getString("exception"); 10 | public static final String CLASS_NAME = bundle.getString("className"); 11 | public static final String MESSAGE = bundle.getString("message"); 12 | public static final String CAUSE = bundle.getString("cause"); 13 | public static final String STACK_TRACE = bundle.getString("stackTrace"); 14 | public static final String FILE_NAME = bundle.getString("fileName"); 15 | public static final String LINE_NUMBER = bundle.getString("lineNumber"); 16 | public static final String METHOD_NAME = bundle.getString("methodName"); 17 | } 18 | -------------------------------------------------------------------------------- /api/src/main/java/echopraxia/api/FieldVisitor.java: -------------------------------------------------------------------------------- 1 | package echopraxia.api; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | /** 6 | * A visitor interface that transforms the field in a structured JSON view. 7 | * 8 | * @since 3.0 9 | */ 10 | public interface FieldVisitor { 11 | 12 | @NotNull 13 | Field visit(@NotNull Field field); 14 | 15 | void visitAttributes(@NotNull Attributes attributes); 16 | 17 | void visitName(@NotNull String name); 18 | 19 | @NotNull 20 | Field visitString(@NotNull Value stringValue); 21 | 22 | @NotNull 23 | Field visitException(@NotNull Value exceptionValue); 24 | 25 | @NotNull 26 | Field visitBoolean(@NotNull Value booleanValue); 27 | 28 | @NotNull 29 | Field visitNumber(@NotNull Value numberValue); 30 | 31 | @NotNull 32 | Field visitNull(); 33 | 34 | @NotNull 35 | ArrayVisitor visitArray(); 36 | 37 | @NotNull 38 | ObjectVisitor visitObject(); 39 | 40 | interface ArrayVisitor { 41 | @NotNull 42 | Field done(); 43 | 44 | void visitElement(@NotNull Value value); 45 | 46 | void visitStringElement(Value.StringValue stringValue); 47 | 48 | void visitNumberElement(Value.NumberValue numberValue); 49 | 50 | void visitBooleanElement(Value.BooleanValue booleanValue); 51 | 52 | void visitArrayElement(Value.ArrayValue arrayValue); 53 | 54 | void visitObjectElement(Value.ObjectValue objectValue); 55 | 56 | void visitExceptionElement(Value.ExceptionValue exceptionValue); 57 | 58 | void visitNullElement(); 59 | } 60 | 61 | interface ObjectVisitor { 62 | @NotNull 63 | Field done(); 64 | 65 | void visit(@NotNull Field childField); 66 | 67 | @NotNull 68 | FieldVisitor visitChild(); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /api/src/main/java/echopraxia/api/FindPathMethods.java: -------------------------------------------------------------------------------- 1 | package echopraxia.api; 2 | 3 | import java.util.List; 4 | import java.util.Map; 5 | import java.util.Optional; 6 | import org.intellij.lang.annotations.Language; 7 | import org.jetbrains.annotations.NotNull; 8 | 9 | /** Methods for finding values and fields using JSON path syntax. */ 10 | public interface FindPathMethods { 11 | 12 | /** 13 | * Finds a string value from the JSON path. 14 | * 15 | * @param jsonPath a JSON path to evaluate. 16 | * @return an optional string if found, otherwise empty(). 17 | */ 18 | @NotNull 19 | Optional findString(@NotNull @Language("JSONPath") String jsonPath); 20 | 21 | /** 22 | * Finds a boolean value from the JSON path. 23 | * 24 | * @param jsonPath a JSON path to evaluate. 25 | * @return an optional boolean if found, otherwise empty(). 26 | */ 27 | @NotNull 28 | Optional findBoolean(@NotNull @Language("JSONPath") String jsonPath); 29 | 30 | /** 31 | * Finds a number value from the JSON path. 32 | * 33 | * @param jsonPath a JSON path to evaluate. 34 | * @return an optional number if found, otherwise empty(). 35 | */ 36 | @NotNull 37 | Optional findNumber(@NotNull @Language("JSONPath") String jsonPath); 38 | 39 | /** 40 | * Finds a null value from the JSON path. 41 | * 42 | * @param jsonPath a JSON path to evaluate. 43 | * @return true if null found, false otherwise. 44 | */ 45 | boolean findNull(@NotNull @Language("JSONPath") String jsonPath); 46 | 47 | /** 48 | * Finds a throwable value from the JSON path. 49 | * 50 | * @param jsonPath a JSON path to evaluate. 51 | * @return optional throwable if found, empty() otherwise. 52 | */ 53 | @NotNull 54 | Optional findThrowable(@NotNull @Language("JSONPath") String jsonPath); 55 | 56 | /** 57 | * Finds a throwable value using the default path 58 | * 59 | * @return optional throwable if found, empty() otherwise. 60 | */ 61 | @NotNull 62 | Optional findThrowable(); 63 | 64 | /** 65 | * Finds an object value using a json path. 66 | * 67 | * @param jsonPath a JSON path to evaluate. 68 | * @return optional map if found, empty() otherwise. 69 | */ 70 | @NotNull 71 | Optional> findObject(@NotNull @Language("JSONPath") String jsonPath); 72 | 73 | /** 74 | * Finds a list using a json path. 75 | * 76 | * @param jsonPath a JSON path to evaluate. 77 | * @return list containing elements, may be empty if nothing found. 78 | */ 79 | @NotNull 80 | List findList(@NotNull @Language("JSONPath") String jsonPath); 81 | } 82 | -------------------------------------------------------------------------------- /api/src/main/java/echopraxia/api/PresentationHints.java: -------------------------------------------------------------------------------- 1 | package echopraxia.api; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | /** 6 | * An interface for fields that know about presentation hint attributes. 7 | * 8 | * @since 3.0 9 | */ 10 | public interface PresentationHints { 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 find(String path) { 43 | if (path == null) { 44 | return Optional.empty(); 45 | } 46 | return optionalFind(path, Object.class); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /logback/src/main/java/echopraxia/logback/AbstractPathConverter.java: -------------------------------------------------------------------------------- 1 | package echopraxia.logback; 2 | 3 | import ch.qos.logback.classic.pattern.ClassicConverter; 4 | import ch.qos.logback.classic.spi.ILoggingEvent; 5 | import java.util.Optional; 6 | import org.jetbrains.annotations.NotNull; 7 | 8 | abstract class AbstractPathConverter extends ClassicConverter { 9 | 10 | protected String jsonPath; 11 | 12 | public void start() { 13 | String optStr = this.getFirstOption(); 14 | if (optStr != null) { 15 | this.jsonPath = optStr; 16 | super.start(); 17 | } 18 | 19 | if (this.jsonPath == null) { 20 | throw new IllegalStateException("JSON path is not specified"); 21 | } 22 | } 23 | 24 | @Override 25 | public String convert(ILoggingEvent event) { 26 | AbstractEventLoggingContext ctx = getLoggingContext(event); 27 | try { 28 | if (ctx.getFields().isEmpty()) { 29 | return ""; 30 | } else { 31 | final Optional optObject = ctx.find(jsonPath); 32 | return optObject.map(o -> o.toString()).orElse(""); 33 | } 34 | } catch (Exception e) { 35 | addError("Cannot convert path " + jsonPath, e); 36 | return ""; 37 | } 38 | } 39 | 40 | @NotNull 41 | protected abstract AbstractEventLoggingContext getLoggingContext(ILoggingEvent event); 42 | } 43 | -------------------------------------------------------------------------------- /logback/src/main/java/echopraxia/logback/ArgumentFieldConverter.java: -------------------------------------------------------------------------------- 1 | package echopraxia.logback; 2 | 3 | import ch.qos.logback.classic.spi.ILoggingEvent; 4 | import org.jetbrains.annotations.NotNull; 5 | 6 | /** This converter searches field arguments using a JSON path. */ 7 | public class ArgumentFieldConverter extends AbstractPathConverter { 8 | 9 | @NotNull 10 | @Override 11 | protected AbstractEventLoggingContext getLoggingContext(ILoggingEvent event) { 12 | return new ArgumentLoggingContext(null, event); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /logback/src/main/java/echopraxia/logback/ArgumentLoggingContext.java: -------------------------------------------------------------------------------- 1 | package echopraxia.logback; 2 | 3 | import static echopraxia.logging.spi.Utilities.memoize; 4 | 5 | import ch.qos.logback.classic.spi.ILoggingEvent; 6 | import echopraxia.api.Field; 7 | import echopraxia.logging.spi.CoreLogger; 8 | import java.util.*; 9 | import java.util.function.Supplier; 10 | import org.jetbrains.annotations.NotNull; 11 | import org.jetbrains.annotations.Nullable; 12 | 13 | public class ArgumentLoggingContext extends AbstractEventLoggingContext { 14 | 15 | private final Supplier> argumentFields; 16 | private final CoreLogger core; 17 | 18 | public ArgumentLoggingContext(@Nullable CoreLogger core, @NotNull ILoggingEvent event) { 19 | this.core = core; 20 | this.argumentFields = memoize(() -> fieldArguments(event)); 21 | } 22 | 23 | @Override 24 | public CoreLogger getCore() { 25 | return core; 26 | } 27 | 28 | @Override 29 | public @NotNull List getFields() { 30 | return argumentFields.get(); 31 | } 32 | 33 | @Override 34 | public @NotNull List getLoggerFields() { 35 | return Collections.emptyList(); 36 | } 37 | 38 | @Override 39 | public @NotNull List getArgumentFields() { 40 | return argumentFields.get(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /logback/src/main/java/echopraxia/logback/CallerDataAppender.java: -------------------------------------------------------------------------------- 1 | package echopraxia.logback; 2 | 3 | import ch.qos.logback.classic.LoggerContext; 4 | import ch.qos.logback.classic.spi.CallerData; 5 | import ch.qos.logback.classic.spi.ILoggingEvent; 6 | import ch.qos.logback.classic.spi.LoggingEvent; 7 | import java.util.Iterator; 8 | import org.slf4j.Marker; 9 | 10 | /** An appender that sets the caller data on the event from a marker if it exists. */ 11 | public class CallerDataAppender extends TransformingAppender { 12 | @Override 13 | protected ILoggingEvent decorateEvent(ILoggingEvent eventObject) { 14 | return setCallerData(eventObject); 15 | } 16 | 17 | protected ILoggingEvent setCallerData(ILoggingEvent event) { 18 | final LoggingEvent internalEvent = (LoggingEvent) event; 19 | final Marker marker = event.getMarker(); 20 | if (marker == null) { 21 | return event; 22 | } else if (marker instanceof CallerMarker) { 23 | final StackTraceElement[] stackTraceElements = extractFromMarker((CallerMarker) marker); 24 | internalEvent.setCallerData(stackTraceElements); 25 | return internalEvent; 26 | } else { 27 | final Iterator iterator = marker.iterator(); 28 | while (iterator.hasNext()) { 29 | final Marker next = iterator.next(); 30 | if (next instanceof CallerMarker) { 31 | final StackTraceElement[] stackTraceElements = extractFromMarker((CallerMarker) next); 32 | internalEvent.setCallerData(stackTraceElements); 33 | return event; 34 | } 35 | } 36 | } 37 | return event; 38 | } 39 | 40 | private StackTraceElement[] extractFromMarker(CallerMarker callerMarker) { 41 | final String fqcn = callerMarker.getFqcn(); 42 | final Throwable callSite = callerMarker.getCallSite(); 43 | return extractCallerData(fqcn, callSite); 44 | } 45 | 46 | protected StackTraceElement[] extractCallerData(String fqcn, Throwable callsite) { 47 | LoggerContext loggerContext = (LoggerContext) getContext(); 48 | return CallerData.extract( 49 | callsite, 50 | fqcn, 51 | loggerContext.getMaxCallerDataDepth(), 52 | loggerContext.getFrameworkPackages()); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /logback/src/main/java/echopraxia/logback/CallerMarker.java: -------------------------------------------------------------------------------- 1 | package echopraxia.logback; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | /** 6 | * A marker containing caller data. This can be used by a filter to set caller data on a logging 7 | * event prior to encoding. 8 | */ 9 | public class CallerMarker extends BaseMarker { 10 | private final String fqcn; 11 | private final Throwable callsite; 12 | 13 | public CallerMarker(@NotNull String fqcn, @NotNull Throwable callsite) { 14 | super("caller"); 15 | this.fqcn = fqcn; 16 | this.callsite = callsite; 17 | } 18 | 19 | public Throwable getCallSite() { 20 | return callsite; 21 | } 22 | 23 | public String getFqcn() { 24 | return fqcn; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /logback/src/main/java/echopraxia/logback/ConditionMarker.java: -------------------------------------------------------------------------------- 1 | package echopraxia.logback; 2 | 3 | import echopraxia.logging.api.Condition; 4 | import org.jetbrains.annotations.NotNull; 5 | 6 | public class ConditionMarker extends BaseMarker { 7 | private final Condition condition; 8 | 9 | ConditionMarker(@NotNull Condition condition) { 10 | super(condition.toString()); 11 | this.condition = condition; 12 | } 13 | 14 | @NotNull 15 | public static ConditionMarker apply(@NotNull Condition condition) { 16 | return new ConditionMarker(condition); 17 | } 18 | 19 | public Condition getCondition() { 20 | return condition; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /logback/src/main/java/echopraxia/logback/DirectFieldMarker.java: -------------------------------------------------------------------------------- 1 | package echopraxia.logback; 2 | 3 | import echopraxia.api.Field; 4 | import echopraxia.api.FieldBuilderResult; 5 | import java.util.List; 6 | import org.jetbrains.annotations.NotNull; 7 | 8 | /** A marker that contains an echopraxia field, used as part of the direct API. */ 9 | public class DirectFieldMarker extends BaseMarker { 10 | 11 | private final FieldBuilderResult result; 12 | 13 | DirectFieldMarker(@NotNull FieldBuilderResult field) { 14 | super(field.toString()); 15 | this.result = field; 16 | } 17 | 18 | @NotNull 19 | public static DirectFieldMarker apply(@NotNull FieldBuilderResult field) { 20 | return new DirectFieldMarker(field); 21 | } 22 | 23 | public List getFields() { 24 | return result.fields(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /logback/src/main/java/echopraxia/logback/FieldConverter.java: -------------------------------------------------------------------------------- 1 | package echopraxia.logback; 2 | 3 | import ch.qos.logback.classic.spi.ILoggingEvent; 4 | import org.jetbrains.annotations.NotNull; 5 | 6 | /** 7 | * This class renders a fields in the logging event based off the JSON path. 8 | * 9 | *

Note that the logging context has a null core logger as it is created outside the usual flow. 10 | * 11 | *

If the same path matches arguments and logger context, the arguments take precedence. 12 | */ 13 | public class FieldConverter extends AbstractPathConverter { 14 | 15 | @Override 16 | protected @NotNull AbstractEventLoggingContext getLoggingContext(ILoggingEvent event) { 17 | return new FieldLoggingContext(null, event); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /logback/src/main/java/echopraxia/logback/FieldLoggingContext.java: -------------------------------------------------------------------------------- 1 | package echopraxia.logback; 2 | 3 | import static echopraxia.logging.spi.Utilities.memoize; 4 | 5 | import ch.qos.logback.classic.spi.ILoggingEvent; 6 | import echopraxia.api.Field; 7 | import echopraxia.logging.spi.CoreLogger; 8 | import java.util.*; 9 | import java.util.function.Supplier; 10 | import org.jetbrains.annotations.NotNull; 11 | 12 | public class FieldLoggingContext extends AbstractEventLoggingContext { 13 | 14 | private final CoreLogger core; 15 | 16 | private final Supplier> argumentFields; 17 | private final Supplier> markerFields; 18 | 19 | private final Supplier> fields; 20 | 21 | public FieldLoggingContext(CoreLogger core, @NotNull ILoggingEvent event) { 22 | this.core = core; 23 | this.argumentFields = memoize(() -> fieldArguments(event)); 24 | this.markerFields = memoize(() -> fieldMarkers(event)); 25 | this.fields = 26 | memoize( 27 | () -> { 28 | List fields = new ArrayList<>(); 29 | fields.addAll(getArgumentFields()); // argument fields should take precedence 30 | fields.addAll(getLoggerFields()); 31 | return fields; 32 | }); 33 | } 34 | 35 | @Override 36 | public CoreLogger getCore() { 37 | return this.core; 38 | } 39 | 40 | @Override 41 | public @NotNull List getFields() { 42 | return fields.get(); 43 | } 44 | 45 | @Override 46 | public @NotNull List getLoggerFields() { 47 | return markerFields.get(); 48 | } 49 | 50 | @Override 51 | public @NotNull List getArgumentFields() { 52 | return argumentFields.get(); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /logback/src/main/java/echopraxia/logback/LogbackLoggerContext.java: -------------------------------------------------------------------------------- 1 | package echopraxia.logback; 2 | 3 | import echopraxia.logging.spi.LoggerContext; 4 | import java.util.List; 5 | import org.jetbrains.annotations.NotNull; 6 | import org.slf4j.Marker; 7 | 8 | /** The logback context associated with the logger across multiple logging events. */ 9 | public interface LogbackLoggerContext extends LoggerContext { 10 | 11 | @NotNull 12 | List getMarkers(); 13 | } 14 | -------------------------------------------------------------------------------- /logback/src/main/java/echopraxia/logback/LogbackLoggingContext.java: -------------------------------------------------------------------------------- 1 | package echopraxia.logback; 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.jsonpath.AbstractJsonPathFinder; 8 | import echopraxia.logging.api.LoggingContextWithFindPathMethods; 9 | import echopraxia.logging.spi.CoreLogger; 10 | import java.util.Collections; 11 | import java.util.List; 12 | import java.util.function.Supplier; 13 | import org.jetbrains.annotations.NotNull; 14 | import org.slf4j.Marker; 15 | 16 | /** 17 | * The logging context composes the "logger context" (markers/fields associated with the logger) and 18 | * the field arguments associated with the individual logging event. 19 | */ 20 | public class LogbackLoggingContext extends AbstractJsonPathFinder 21 | implements LoggingContextWithFindPathMethods { 22 | 23 | private final Supplier> argumentFields; 24 | private final Supplier> loggerFields; 25 | 26 | private final LogbackLoggerContext loggerContext; 27 | private final Supplier> fields; 28 | private final CoreLogger core; 29 | 30 | public LogbackLoggingContext(CoreLogger core, LogbackLoggerContext loggerContext) { 31 | this(core, loggerContext, Collections::emptyList); 32 | } 33 | 34 | public LogbackLoggingContext( 35 | CoreLogger core, LogbackLoggerContext loggerContext, Supplier> arguments) { 36 | this.core = core; 37 | this.loggerContext = loggerContext; 38 | this.argumentFields = memoize(arguments); 39 | this.loggerFields = memoize(loggerContext::getLoggerFields); 40 | this.fields = memoize(joinFields(this.loggerFields, this.argumentFields)); 41 | } 42 | 43 | @Override 44 | public CoreLogger getCore() { 45 | return core; 46 | } 47 | 48 | @Override 49 | public @NotNull List getFields() { 50 | return fields.get(); 51 | } 52 | 53 | @Override 54 | public List getLoggerFields() { 55 | return loggerFields.get(); 56 | } 57 | 58 | @Override 59 | public List getArgumentFields() { 60 | return argumentFields.get(); 61 | } 62 | 63 | public List getMarkers() { 64 | return loggerContext.getMarkers(); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /logback/src/main/java/echopraxia/logback/LoggerFieldConverter.java: -------------------------------------------------------------------------------- 1 | package echopraxia.logback; 2 | 3 | import ch.qos.logback.classic.spi.ILoggingEvent; 4 | import org.jetbrains.annotations.NotNull; 5 | 6 | /** 7 | * This converter renders the fields in the logger context based off JSON path. 8 | * 9 | *

Note that the core logger is null as this context was created outside the usual flow. 10 | */ 11 | public class LoggerFieldConverter extends AbstractPathConverter { 12 | @Override 13 | protected @NotNull AbstractEventLoggingContext getLoggingContext(ILoggingEvent event) { 14 | return new MarkerLoggingContext(null, event); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /logback/src/main/java/echopraxia/logback/MarkerLoggingContext.java: -------------------------------------------------------------------------------- 1 | package echopraxia.logback; 2 | 3 | import static echopraxia.logging.spi.Utilities.memoize; 4 | 5 | import ch.qos.logback.classic.spi.ILoggingEvent; 6 | import echopraxia.api.Field; 7 | import echopraxia.logging.spi.CoreLogger; 8 | import java.util.Collections; 9 | import java.util.List; 10 | import java.util.function.Supplier; 11 | import org.jetbrains.annotations.NotNull; 12 | 13 | public class MarkerLoggingContext extends AbstractEventLoggingContext { 14 | 15 | private final CoreLogger core; 16 | 17 | private final Supplier> markerFields; 18 | 19 | public MarkerLoggingContext(CoreLogger core, @NotNull ILoggingEvent event) { 20 | this.core = core; 21 | this.markerFields = memoize(() -> fieldMarkers(event)); 22 | } 23 | 24 | @Override 25 | public CoreLogger getCore() { 26 | return this.core; 27 | } 28 | 29 | @Override 30 | public @NotNull List getFields() { 31 | return getLoggerFields(); 32 | } 33 | 34 | @Override 35 | public @NotNull List getLoggerFields() { 36 | return markerFields.get(); 37 | } 38 | 39 | @Override 40 | public @NotNull List getArgumentFields() { 41 | return Collections.emptyList(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /logback/src/main/java/echopraxia/logback/TransformingAppender.java: -------------------------------------------------------------------------------- 1 | package echopraxia.logback; 2 | 3 | import ch.qos.logback.core.Appender; 4 | import ch.qos.logback.core.UnsynchronizedAppenderBase; 5 | import ch.qos.logback.core.spi.AppenderAttachable; 6 | import ch.qos.logback.core.spi.AppenderAttachableImpl; 7 | import java.util.Iterator; 8 | 9 | /** 10 | * An abstract appender that provides a base to wrap other appenders. 11 | * 12 | * @param the logging event 13 | */ 14 | public abstract class TransformingAppender extends UnsynchronizedAppenderBase 15 | implements AppenderAttachable { 16 | 17 | protected AppenderAttachableImpl aai = new AppenderAttachableImpl(); 18 | 19 | protected abstract E decorateEvent(E eventObject); 20 | 21 | @Override 22 | protected void append(E eventObject) { 23 | aai.appendLoopOnAppenders(decorateEvent(eventObject)); 24 | } 25 | 26 | public void addAppender(Appender newAppender) { 27 | addInfo("Attaching appender named [" + newAppender.getName() + "] to " + this.toString()); 28 | aai.addAppender(newAppender); 29 | } 30 | 31 | public Iterator> iteratorForAppenders() { 32 | return aai.iteratorForAppenders(); 33 | } 34 | 35 | public Appender getAppender(String name) { 36 | return aai.getAppender(name); 37 | } 38 | 39 | public boolean isAttached(Appender eAppender) { 40 | return aai.isAttached(eAppender); 41 | } 42 | 43 | public void detachAndStopAllAppenders() { 44 | aai.detachAndStopAllAppenders(); 45 | } 46 | 47 | public boolean detachAppender(Appender eAppender) { 48 | return aai.detachAppender(eAppender); 49 | } 50 | 51 | public boolean detachAppender(String name) { 52 | return aai.detachAppender(name); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /logback/src/test/java/echopraxia/logback/TestBase.java: -------------------------------------------------------------------------------- 1 | package echopraxia.logback; 2 | 3 | import static org.slf4j.Logger.ROOT_LOGGER_NAME; 4 | 5 | import ch.qos.logback.classic.LoggerContext; 6 | import ch.qos.logback.classic.joran.JoranConfigurator; 7 | import ch.qos.logback.classic.spi.ILoggingEvent; 8 | import ch.qos.logback.core.joran.spi.JoranException; 9 | import ch.qos.logback.core.read.ListAppender; 10 | import java.net.MalformedURLException; 11 | import java.net.URISyntaxException; 12 | import org.junit.jupiter.api.BeforeEach; 13 | 14 | public class TestBase { 15 | protected LoggerContext loggerContext; 16 | 17 | @BeforeEach 18 | public void before() { 19 | try { 20 | LoggerContext factory = new LoggerContext(); 21 | JoranConfigurator joran = new JoranConfigurator(); 22 | joran.setContext(factory); 23 | factory.reset(); 24 | joran.doConfigure(getClass().getResource("/logback-test.xml").toURI().toURL()); 25 | this.loggerContext = factory; 26 | } catch (JoranException | URISyntaxException | MalformedURLException je) { 27 | je.printStackTrace(); 28 | } 29 | } 30 | 31 | LoggerContext loggerContext() { 32 | return loggerContext; 33 | } 34 | 35 | ListAppender getListAppender() { 36 | final ch.qos.logback.classic.Logger logger = loggerContext().getLogger(ROOT_LOGGER_NAME); 37 | return (ListAppender) logger.iteratorForAppenders().next(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /logback/src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /logger/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java-library' 3 | } 4 | 5 | dependencies { 6 | api project(":logging") 7 | 8 | jmhImplementation project(":logstash") 9 | testImplementation project(":logstash") 10 | 11 | testImplementation "ch.qos.logback:logback-classic:$logbackVersion" 12 | testImplementation "net.logstash.logback:logstash-logback-encoder:$logstashVersion" 13 | } 14 | -------------------------------------------------------------------------------- /logging/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java-library' 3 | id 'java-test-fixtures' 4 | } 5 | 6 | dependencies { 7 | api project(":api") 8 | } 9 | -------------------------------------------------------------------------------- /logging/src/main/java/echopraxia/logging/api/JsonPathCondition.java: -------------------------------------------------------------------------------- 1 | package echopraxia.logging.api; 2 | 3 | import java.util.function.BiFunction; 4 | import java.util.function.Function; 5 | import org.jetbrains.annotations.Contract; 6 | import org.jetbrains.annotations.NotNull; 7 | 8 | public interface JsonPathCondition extends Condition { 9 | 10 | boolean jsonPathTest(Level level, LoggingContextWithFindPathMethods context); 11 | 12 | default boolean test(Level level, LoggingContext context) { 13 | if (context instanceof LoggingContextWithFindPathMethods) { 14 | return jsonPathTest(level, (LoggingContextWithFindPathMethods) context); 15 | } else { 16 | throw new IllegalStateException("test requires LoggingContextWithFindPathMethods instance!"); 17 | } 18 | } 19 | 20 | @Contract(pure = true) 21 | public static @NotNull JsonPathCondition pathCondition( 22 | Function o) { 23 | return (level, context) -> { 24 | if (context != null) { 25 | return o.apply(context); 26 | } else { 27 | throw new IllegalStateException( 28 | "pathCondition requires LoggingContextWithFindPathMethods instance!"); 29 | } 30 | }; 31 | } 32 | 33 | @Contract(pure = true) 34 | public static @NotNull JsonPathCondition pathCondition( 35 | BiFunction o) { 36 | return (level, context) -> { 37 | if (context != null) { 38 | return o.apply(level, context); 39 | } else { 40 | throw new IllegalStateException( 41 | "pathCondition requires LoggingContextWithFindPathMethods instance!"); 42 | } 43 | }; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /logging/src/main/java/echopraxia/logging/api/Level.java: -------------------------------------------------------------------------------- 1 | package echopraxia.logging.api; 2 | 3 | /** 4 | * An enumeration of the logging levels. 5 | * 6 | *

Note that ordinal values may not match with Logback/Log4J/SLF4J levels, and you should 7 | * exercise caution when looking at cardinal/numeric values because they're all different. 8 | */ 9 | public enum Level { 10 | 11 | // Order is significant, and compareTo uses ordinal values internally. 12 | TRACE, // 0 13 | DEBUG, // 1 14 | INFO, // 2 15 | WARN, // 3 16 | ERROR; // 4 17 | 18 | public boolean isGreater(Level r) { 19 | return compareTo(r) > 0; 20 | } 21 | 22 | public boolean isGreaterOrEqual(Level r) { 23 | return compareTo(r) >= 0; 24 | } 25 | 26 | public boolean isLess(Level r) { 27 | return compareTo(r) < 0; 28 | } 29 | 30 | public boolean isLessOrEqual(Level r) { 31 | return compareTo(r) <= 0; 32 | } 33 | 34 | public boolean isEqual(Level r) { 35 | return equals(r); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /logging/src/main/java/echopraxia/logging/api/LoggerHandle.java: -------------------------------------------------------------------------------- 1 | package echopraxia.logging.api; 2 | 3 | import echopraxia.api.FieldBuilderResult; 4 | import java.util.function.Function; 5 | import org.jetbrains.annotations.NotNull; 6 | import org.jetbrains.annotations.Nullable; 7 | 8 | /** The LoggerHandle class is used as a handle to a logger at a specific level. */ 9 | public interface LoggerHandle { 10 | 11 | /** 12 | * Logs using a message. 13 | * 14 | * @param message the message. 15 | */ 16 | void log(@Nullable String message); 17 | 18 | /** 19 | * Logs using a message template with a field builder function. 20 | * 21 | * @param message the message template. 22 | * @param f the field builder function. 23 | */ 24 | void log(@Nullable String message, @NotNull Function f); 25 | } 26 | -------------------------------------------------------------------------------- /logging/src/main/java/echopraxia/logging/api/LoggingContext.java: -------------------------------------------------------------------------------- 1 | package echopraxia.logging.api; 2 | 3 | import echopraxia.api.Field; 4 | import echopraxia.logging.spi.CoreLogger; 5 | import java.util.List; 6 | import org.jetbrains.annotations.NotNull; 7 | import org.jetbrains.annotations.Nullable; 8 | 9 | /** 10 | * The logging context interface is exposed to conditions as the way to inspect the available fields 11 | * for evaluation. 12 | */ 13 | public interface LoggingContext { 14 | 15 | /** 16 | * A reference back to the core logger. This may be null if the context is constructed out of the 17 | * usual flow and the core logger is unavailable i.e. in a Logback custom converter. 18 | * 19 | * @return core logger if possible, else null. 20 | */ 21 | @Nullable 22 | CoreLogger getCore(); 23 | 24 | /** 25 | * @return both context and argument fields, in that order. 26 | */ 27 | @NotNull 28 | List getFields(); 29 | 30 | /** 31 | * @return the fields passed in as arguments to the logger. 32 | */ 33 | List getArgumentFields(); 34 | 35 | /** 36 | * @return the list of fields that are part of logger's context. 37 | */ 38 | List getLoggerFields(); 39 | } 40 | -------------------------------------------------------------------------------- /logging/src/main/java/echopraxia/logging/api/LoggingContextWithFindPathMethods.java: -------------------------------------------------------------------------------- 1 | package echopraxia.logging.api; 2 | 3 | import echopraxia.api.FindPathMethods; 4 | 5 | /** An extended logging context with JSON find methods. */ 6 | public interface LoggingContextWithFindPathMethods extends LoggingContext, FindPathMethods {} 7 | -------------------------------------------------------------------------------- /logging/src/main/java/echopraxia/logging/spi/AbstractEchopraxiaService.java: -------------------------------------------------------------------------------- 1 | package echopraxia.logging.spi; 2 | 3 | import echopraxia.api.DefaultToStringFormatter; 4 | import echopraxia.api.ToStringFormatter; 5 | import java.util.Collections; 6 | import org.jetbrains.annotations.NotNull; 7 | 8 | /** 9 | * Abstract service with default implementations of exception handler and toString formatter. 10 | * 11 | * @since 3.0 12 | */ 13 | public abstract class AbstractEchopraxiaService implements EchopraxiaService { 14 | 15 | private static final ClassLoader[] classLoaders = {ClassLoader.getSystemClassLoader()}; 16 | 17 | /** The filters used by the service. */ 18 | protected Filters filters; 19 | 20 | /** The formatter used by the service. */ 21 | protected ToStringFormatter toStringFormatter; 22 | 23 | /** The exception handler used by the service. */ 24 | protected ExceptionHandler exceptionHandler; 25 | 26 | /** Creates a service with defaults. */ 27 | public AbstractEchopraxiaService() { 28 | this.exceptionHandler = Throwable::printStackTrace; 29 | this.toStringFormatter = new DefaultToStringFormatter(); 30 | this.filters = initFilters(); 31 | } 32 | 33 | private Filters initFilters() { 34 | try { 35 | return new Filters(classLoaders); 36 | } catch (Exception e) { 37 | // If we get to this point, something has gone horribly wrong. 38 | exceptionHandler.handleException(e); 39 | // Keep going with no filters. 40 | return new Filters(Collections.emptyList()); 41 | } 42 | } 43 | 44 | @NotNull 45 | @Override 46 | public Filters getFilters() { 47 | return filters; 48 | } 49 | 50 | @Override 51 | public @NotNull ExceptionHandler getExceptionHandler() { 52 | return exceptionHandler; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /logging/src/main/java/echopraxia/logging/spi/Caller.java: -------------------------------------------------------------------------------- 1 | package echopraxia.logging.spi; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | public class Caller { 6 | 7 | @NotNull 8 | public static String resolveClassName() { 9 | // If we're on JDK 9, we can use 10 | // StackWalker walker = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE); 11 | // Class callerClass = walker.getCallerClass(); 12 | // However, this works fine: https://stackoverflow.com/a/11306854 13 | StackTraceElement[] stElements = Thread.currentThread().getStackTrace(); 14 | String callerClassName = null; 15 | for (int i = 1; i < stElements.length; i++) { 16 | StackTraceElement ste = stElements[i]; 17 | if (!ste.getClassName().equals(Caller.class.getName()) 18 | && ste.getClassName().indexOf("java.lang.Thread") != 0) { 19 | if (callerClassName == null) { 20 | callerClassName = ste.getClassName(); 21 | } else if (!callerClassName.equals(ste.getClassName())) { 22 | return ste.getClassName(); 23 | } 24 | } 25 | } 26 | throw new IllegalStateException("No stack trace elements found in thread!"); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /logging/src/main/java/echopraxia/logging/spi/CoreLoggerFactory.java: -------------------------------------------------------------------------------- 1 | package echopraxia.logging.spi; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | /** 6 | * The core logger factory. 7 | * 8 | *

This is internal, and is intended for service provider implementations. 9 | */ 10 | public class CoreLoggerFactory { 11 | 12 | @NotNull 13 | public static CoreLogger getLogger(@NotNull String fqcn, @NotNull Class clazz) { 14 | CoreLogger core = EchopraxiaService.getInstance().getCoreLogger(fqcn, clazz); 15 | return processFilters(core); 16 | } 17 | 18 | @NotNull 19 | public static CoreLogger getLogger(@NotNull String fqcn, @NotNull String name) { 20 | CoreLogger core = EchopraxiaService.getInstance().getCoreLogger(fqcn, name); 21 | return processFilters(core); 22 | } 23 | 24 | @NotNull 25 | private static CoreLogger processFilters(@NotNull CoreLogger core) { 26 | return EchopraxiaService.getInstance().getFilters().apply(core); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /logging/src/main/java/echopraxia/logging/spi/CoreLoggerFilter.java: -------------------------------------------------------------------------------- 1 | package echopraxia.logging.spi; 2 | 3 | import java.util.function.Function; 4 | 5 | /** 6 | * A filter which can modify a core logger in some way, typically by adding fields or conditions. 7 | * 8 | *

This interface is used in "global" situations where you don't want to modify individual 9 | * loggers, but want something in the pipeline to `getLogger`. 10 | */ 11 | @FunctionalInterface 12 | public interface CoreLoggerFilter extends Function {} 13 | -------------------------------------------------------------------------------- /logging/src/main/java/echopraxia/logging/spi/DefaultMethodsSupport.java: -------------------------------------------------------------------------------- 1 | package echopraxia.logging.spi; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | /** 6 | * Methods that are used by the defaults to do delegation to the core logger. 7 | * 8 | * @param the field builder type. 9 | */ 10 | public interface DefaultMethodsSupport { 11 | /** 12 | * @return the name associated with the logger 13 | */ 14 | @NotNull 15 | String getName(); 16 | 17 | /** 18 | * @return The core logger underlying this logger 19 | */ 20 | @NotNull 21 | CoreLogger core(); 22 | 23 | /** 24 | * @return the field builder being used by this logger. 25 | */ 26 | @NotNull 27 | FB fieldBuilder(); 28 | } 29 | -------------------------------------------------------------------------------- /logging/src/main/java/echopraxia/logging/spi/EchopraxiaService.java: -------------------------------------------------------------------------------- 1 | package echopraxia.logging.spi; 2 | 3 | import java.util.Iterator; 4 | import java.util.ServiceConfigurationError; 5 | import java.util.ServiceLoader; 6 | import org.jetbrains.annotations.NotNull; 7 | 8 | /** 9 | * The main SPI interface. Call getInstance() to get the service from service provider. 10 | * 11 | * @since 3.0 12 | */ 13 | public interface EchopraxiaService { 14 | 15 | /** 16 | * @return the exception handler used by the service. 17 | */ 18 | @NotNull 19 | ExceptionHandler getExceptionHandler(); 20 | 21 | /** 22 | * @return the filters used by the service. 23 | */ 24 | @NotNull 25 | Filters getFilters(); 26 | 27 | /** 28 | * @param fqcn the fully qualified class name of the caller. 29 | * @param clazz the logger class. 30 | * @return the core logger associated with the service. 31 | */ 32 | @NotNull 33 | CoreLogger getCoreLogger(@NotNull String fqcn, @NotNull Class clazz); 34 | 35 | /** 36 | * @param fqcn the fully qualified class name of the caller. 37 | * @param name the logger name. 38 | * @return the core logger associated with the service. 39 | */ 40 | @NotNull 41 | CoreLogger getCoreLogger(@NotNull String fqcn, @NotNull String name); 42 | 43 | /** 44 | * @return an instance of the service. 45 | */ 46 | @NotNull 47 | static EchopraxiaService getInstance() { 48 | return EchopraxiaServiceLazyHolder.INSTANCE; 49 | } 50 | } 51 | 52 | class EchopraxiaServiceLazyHolder { 53 | private static EchopraxiaService init() { 54 | ServiceLoader loader = 55 | ServiceLoader.load(EchopraxiaServiceProvider.class); 56 | Iterator iterator = loader.iterator(); 57 | if (iterator.hasNext()) { 58 | return iterator.next().getEchopraxiaService(); 59 | } else { 60 | throw new ServiceConfigurationError("No EchopraxiaService implementation found!"); 61 | } 62 | } 63 | 64 | static final EchopraxiaService INSTANCE = init(); 65 | } 66 | -------------------------------------------------------------------------------- /logging/src/main/java/echopraxia/logging/spi/EchopraxiaServiceProvider.java: -------------------------------------------------------------------------------- 1 | package echopraxia.logging.spi; 2 | 3 | /** 4 | * @since 3.0 5 | */ 6 | public interface EchopraxiaServiceProvider { 7 | 8 | EchopraxiaService getEchopraxiaService(); 9 | } 10 | -------------------------------------------------------------------------------- /logging/src/main/java/echopraxia/logging/spi/ExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package echopraxia.logging.spi; 2 | 3 | public interface ExceptionHandler { 4 | 5 | /** */ 6 | void handleException(Throwable e); 7 | } 8 | -------------------------------------------------------------------------------- /logging/src/main/java/echopraxia/logging/spi/LoggerContext.java: -------------------------------------------------------------------------------- 1 | package echopraxia.logging.spi; 2 | 3 | import echopraxia.api.Field; 4 | import java.util.List; 5 | import org.jetbrains.annotations.NotNull; 6 | 7 | public interface LoggerContext { 8 | 9 | @NotNull 10 | List getLoggerFields(); 11 | } 12 | -------------------------------------------------------------------------------- /logging/src/main/java/echopraxia/logging/spi/Utilities.java: -------------------------------------------------------------------------------- 1 | package echopraxia.logging.spi; 2 | 3 | import echopraxia.api.Field; 4 | import echopraxia.api.Value; 5 | import java.util.ArrayList; 6 | import java.util.Collections; 7 | import java.util.List; 8 | import java.util.Map; 9 | import java.util.function.Function; 10 | import java.util.function.Supplier; 11 | import java.util.stream.Collectors; 12 | import java.util.stream.Stream; 13 | import org.jetbrains.annotations.NotNull; 14 | 15 | /** Utilities for classes implementing a logger. */ 16 | public final class Utilities { 17 | 18 | @NotNull 19 | public static Function>, Supplier>> 20 | getThreadContextFunction(@NotNull Function, List> f) { 21 | return mapSupplier -> () -> f.apply(mapSupplier.get()); 22 | } 23 | 24 | public static @NotNull Supplier memoize(@NotNull Supplier supplier) { 25 | return new MemoizingSupplier<>(supplier); 26 | } 27 | 28 | public @NotNull static List buildThreadContext(Map contextMap) { 29 | if (contextMap == null || contextMap.isEmpty()) { 30 | return Collections.emptyList(); 31 | } 32 | List list = new ArrayList<>(); 33 | for (Map.Entry e : contextMap.entrySet()) { 34 | Field field = Field.keyValue(e.getKey(), Value.string(e.getValue())); 35 | list.add(field); 36 | } 37 | return list; 38 | } 39 | 40 | public static Supplier> joinFields( 41 | Supplier> first, Supplier> second) { 42 | return () -> { 43 | List firstFields = first.get(); 44 | List secondFields = second.get(); 45 | 46 | if (firstFields.isEmpty()) { 47 | return secondFields; 48 | } else if (secondFields.isEmpty()) { 49 | return firstFields; 50 | } else { 51 | // Stream.concat is actually faster than explicit ArrayList! 52 | // https://blog.soebes.de/blog/2020/03/31/performance-stream-concat/ 53 | return Stream.concat(firstFields.stream(), secondFields.stream()) 54 | .collect(Collectors.toList()); 55 | } 56 | }; 57 | } 58 | 59 | public @NotNull static Function>, Supplier>> 60 | threadContext() { 61 | return getThreadContextFunction(Utilities::buildThreadContext); 62 | } 63 | 64 | @NotNull 65 | static class MemoizingSupplier implements Supplier { 66 | final Supplier delegate; 67 | transient volatile boolean initialized; 68 | transient T value; 69 | 70 | MemoizingSupplier(Supplier delegate) { 71 | this.delegate = delegate; 72 | } 73 | 74 | @Override 75 | public T get() { 76 | if (!initialized) { 77 | synchronized (this) { 78 | if (!initialized) { 79 | T t = delegate.get(); 80 | value = t; 81 | initialized = true; 82 | return t; 83 | } 84 | } 85 | } 86 | return value; 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /logging/src/test/java/echopraxia/logging/api/FilterTests.java: -------------------------------------------------------------------------------- 1 | package echopraxia.logging.api; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | 5 | import echopraxia.api.Field; 6 | import echopraxia.logging.spi.CoreLogger; 7 | import echopraxia.logging.spi.CoreLoggerFactory; 8 | import java.util.List; 9 | import java.util.concurrent.atomic.AtomicReference; 10 | import org.junit.jupiter.api.Test; 11 | 12 | public class FilterTests { 13 | 14 | @Test 15 | public void testFilter() { 16 | AtomicReference> myFields = new AtomicReference<>(); 17 | 18 | CoreLogger logger = CoreLoggerFactory.getLogger(FilterTests.class.getName(), "example.Logger"); 19 | Condition condition = 20 | new Condition() { 21 | @Override 22 | public boolean test(Level level, LoggingContext context) { 23 | List fields = context.getFields(); 24 | myFields.set(fields); 25 | return true; 26 | } 27 | }; 28 | logger.log(Level.INFO, condition, "Hello"); 29 | 30 | List fields = myFields.get(); 31 | Field field = fields.get(0); 32 | assertThat(field.name()).isEqualTo("example_field"); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /logging/src/test/java/echopraxia/logging/api/LevelTests.java: -------------------------------------------------------------------------------- 1 | package echopraxia.logging.api; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | 5 | import org.junit.jupiter.api.Test; 6 | 7 | public class LevelTests { 8 | 9 | @Test 10 | public void testGreaterThan() { 11 | assertThat(Level.INFO.isGreater(Level.DEBUG)).isTrue(); 12 | } 13 | 14 | @Test 15 | public void testIsNotGreaterThan() { 16 | assertThat(Level.INFO.isGreater(Level.ERROR)).isFalse(); 17 | } 18 | 19 | @Test 20 | public void testIsGreaterThanOrEqual() { 21 | assertThat(Level.INFO.isGreaterOrEqual(Level.DEBUG)).isTrue(); 22 | } 23 | 24 | @Test 25 | public void testIsGreaterThanOrEqualEquality() { 26 | assertThat(Level.ERROR.isGreaterOrEqual(Level.ERROR)).isTrue(); 27 | } 28 | 29 | @Test 30 | public void testIsLess() { 31 | assertThat(Level.DEBUG.isLess(Level.INFO)).isTrue(); 32 | } 33 | 34 | @Test 35 | public void testIsNotLess() { 36 | assertThat(Level.INFO.isLess(Level.DEBUG)).isFalse(); 37 | } 38 | 39 | @Test 40 | public void testIsLessOrEqual() { 41 | assertThat(Level.TRACE.isLessOrEqual(Level.DEBUG)).isTrue(); 42 | } 43 | 44 | @Test 45 | public void testIsLessOrEqualEquality() { 46 | assertThat(Level.TRACE.isLessOrEqual(Level.TRACE)).isTrue(); 47 | } 48 | 49 | @Test 50 | public void testIsEqual() { 51 | assertThat(Level.TRACE.isEqual(Level.TRACE)).isTrue(); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /logging/src/test/java/echopraxia/logging/api/TestFilter.java: -------------------------------------------------------------------------------- 1 | package echopraxia.logging.api; 2 | 3 | import echopraxia.api.FieldBuilder; 4 | import echopraxia.logging.spi.CoreLogger; 5 | import echopraxia.logging.spi.CoreLoggerFilter; 6 | 7 | public class TestFilter implements CoreLoggerFilter { 8 | @Override 9 | public CoreLogger apply(CoreLogger coreLogger) { 10 | return coreLogger.withFields( 11 | fb -> fb.string("example_field", "example_value"), FieldBuilder.instance()); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /logging/src/test/resources/echopraxia.properties: -------------------------------------------------------------------------------- 1 | filter.0=echopraxia.logging.api.TestFilter -------------------------------------------------------------------------------- /logging/src/testFixtures/java/echopraxia/logging/fake/FakeEchopraxiaService.java: -------------------------------------------------------------------------------- 1 | package echopraxia.logging.fake; 2 | 3 | import echopraxia.logging.spi.AbstractEchopraxiaService; 4 | import echopraxia.logging.spi.CoreLogger; 5 | import org.jetbrains.annotations.NotNull; 6 | 7 | public class FakeEchopraxiaService extends AbstractEchopraxiaService { 8 | 9 | public FakeEchopraxiaService() { 10 | super(); 11 | } 12 | 13 | @Override 14 | public @NotNull CoreLogger getCoreLogger(@NotNull String fqcn, @NotNull Class clazz) { 15 | return new FakeCoreLogger(fqcn); 16 | } 17 | 18 | @Override 19 | public @NotNull CoreLogger getCoreLogger(@NotNull String fqcn, @NotNull String name) { 20 | return new FakeCoreLogger(fqcn); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /logging/src/testFixtures/java/echopraxia/logging/fake/FakeEchopraxiaServiceProvider.java: -------------------------------------------------------------------------------- 1 | package echopraxia.logging.fake; 2 | 3 | import echopraxia.logging.spi.EchopraxiaService; 4 | import echopraxia.logging.spi.EchopraxiaServiceProvider; 5 | 6 | public class FakeEchopraxiaServiceProvider implements EchopraxiaServiceProvider { 7 | 8 | @Override 9 | public EchopraxiaService getEchopraxiaService() { 10 | return new FakeEchopraxiaService(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /logging/src/testFixtures/java/echopraxia/logging/fake/FakeLoggerContext.java: -------------------------------------------------------------------------------- 1 | package echopraxia.logging.fake; 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.spi.LoggerContext; 8 | import java.util.Collections; 9 | import java.util.List; 10 | import java.util.function.Supplier; 11 | import org.jetbrains.annotations.NotNull; 12 | 13 | public class FakeLoggerContext implements LoggerContext { 14 | private final Supplier> fieldsSupplier; 15 | 16 | public static FakeLoggerContext empty() { 17 | return new FakeLoggerContext(Collections::emptyList); 18 | } 19 | 20 | public FakeLoggerContext(Supplier> fieldsSupplier) { 21 | this.fieldsSupplier = memoize(fieldsSupplier); 22 | } 23 | 24 | @Override 25 | public @NotNull List getLoggerFields() { 26 | return fieldsSupplier.get(); 27 | } 28 | 29 | public LoggerContext withFields(Supplier> extraFields) { 30 | return new FakeLoggerContext(joinFields(fieldsSupplier, extraFields)); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /logging/src/testFixtures/resources/META-INF/services/echopraxia.logging.spi.EchopraxiaServiceProvider: -------------------------------------------------------------------------------- 1 | echopraxia.logging.fake.FakeEchopraxiaServiceProvider -------------------------------------------------------------------------------- /logstash/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java-library' 3 | } 4 | 5 | dependencies { 6 | api project(":api") 7 | api project(":logback") 8 | implementation project(":jackson") 9 | 10 | testImplementation project(":logger") 11 | jmhImplementation project(":logger") 12 | jmhImplementation "net.logstash.logback:logstash-logback-encoder:$logstashVersion" 13 | 14 | compileOnly "org.slf4j:slf4j-api:$slf4jApiVersion" 15 | compileOnly "ch.qos.logback:logback-classic:$logbackVersion" 16 | compileOnly "net.logstash.logback:logstash-logback-encoder:$logstashVersion" 17 | 18 | testImplementation "ch.qos.logback:logback-classic:$logbackVersion" 19 | testImplementation "net.logstash.logback:logstash-logback-encoder:$logstashVersion" 20 | } 21 | -------------------------------------------------------------------------------- /logstash/src/jmh/java/echopraxia/logstash/CoreLoggerBenchmarks.java: -------------------------------------------------------------------------------- 1 | package echopraxia.logstash; 2 | 3 | import echopraxia.api.FieldBuilder; 4 | import echopraxia.logging.api.Level; 5 | import echopraxia.logging.spi.CoreLogger; 6 | import echopraxia.logging.spi.CoreLoggerFactory; 7 | import java.util.concurrent.TimeUnit; 8 | import org.openjdk.jmh.annotations.*; 9 | import org.openjdk.jmh.infra.Blackhole; 10 | 11 | @BenchmarkMode(Mode.AverageTime) 12 | @OutputTimeUnit(TimeUnit.NANOSECONDS) 13 | @Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) 14 | @Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) 15 | @Fork(1) 16 | public class CoreLoggerBenchmarks { 17 | private static final CoreLogger logger = 18 | CoreLoggerFactory.getLogger(CoreLoggerBenchmarks.class.getName(), CoreLoggerBenchmarks.class); 19 | private static final Exception exception = new RuntimeException(); 20 | private static final FieldBuilder builder = FieldBuilder.instance(); 21 | 22 | private static final CoreLogger contextLogger = 23 | logger.withFields(fb -> fb.string("foo", "bar"), builder); 24 | 25 | @Benchmark 26 | public void info() { 27 | logger.log(Level.INFO, "Message"); 28 | } 29 | 30 | @Benchmark 31 | public void isEnabled(Blackhole blackhole) { 32 | blackhole.consume(logger.isEnabled(Level.INFO)); 33 | } 34 | 35 | @Benchmark 36 | public void infoWithParameterizedString() { 37 | logger.log(Level.INFO, "Message {}", fb -> fb.string("foo", "bar"), builder); 38 | } 39 | 40 | @Benchmark 41 | public void infoWithContext() { 42 | contextLogger.log(Level.INFO, "Message"); 43 | } 44 | 45 | @Benchmark 46 | public void infoWithException() { 47 | logger.log(Level.INFO, "Message", fb -> fb.exception(exception), builder); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /logstash/src/jmh/java/echopraxia/logstash/FakeLoggingContext.java: -------------------------------------------------------------------------------- 1 | package echopraxia.logstash; 2 | 3 | import echopraxia.api.Field; 4 | import echopraxia.jsonpath.AbstractJsonPathFinder; 5 | import echopraxia.logging.api.LoggingContextWithFindPathMethods; 6 | import echopraxia.logging.spi.CoreLogger; 7 | import java.util.Arrays; 8 | import java.util.Collections; 9 | import java.util.List; 10 | import org.jetbrains.annotations.NotNull; 11 | import org.jetbrains.annotations.Nullable; 12 | 13 | public class FakeLoggingContext extends AbstractJsonPathFinder 14 | implements LoggingContextWithFindPathMethods { 15 | private final List loggerFields; 16 | 17 | public FakeLoggingContext(Field... loggerFields) { 18 | this.loggerFields = Arrays.asList(loggerFields); 19 | } 20 | 21 | @Override 22 | public @Nullable CoreLogger getCore() { 23 | return null; 24 | } 25 | 26 | @Override 27 | public @NotNull List getFields() { 28 | return loggerFields; 29 | } 30 | 31 | @Override 32 | public List getArgumentFields() { 33 | return Collections.emptyList(); 34 | } 35 | 36 | @Override 37 | public List getLoggerFields() { 38 | return loggerFields; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /logstash/src/jmh/java/echopraxia/logstash/JsonPathBenchmarks.java: -------------------------------------------------------------------------------- 1 | package echopraxia.logstash; 2 | 3 | import echopraxia.api.*; 4 | import echopraxia.logging.api.Condition; 5 | import echopraxia.logging.api.JsonPathCondition; 6 | import echopraxia.logging.api.Level; 7 | import echopraxia.logging.api.LoggingContext; 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 = 10, timeUnit = TimeUnit.SECONDS) 16 | @Fork(1) 17 | public class JsonPathBenchmarks { 18 | 19 | private static final Condition streamCondition = 20 | Condition.valueMatch("some_field", f -> f.raw().equals("testing")); 21 | private static final Condition pathCondition = 22 | JsonPathCondition.pathCondition( 23 | context -> 24 | context.findString("$.some_field").filter(f -> f.equals("testing")).isPresent()); 25 | 26 | private static final LoggingContext passContext = 27 | new FakeLoggingContext(Field.value("some_field", Value.string("testing"))); 28 | 29 | private static final LoggingContext failContext = new FakeLoggingContext(); 30 | 31 | @Benchmark 32 | public void testStreamConditionPass(Blackhole blackhole) { 33 | blackhole.consume(streamCondition.test(Level.INFO, passContext)); 34 | } 35 | 36 | @Benchmark 37 | public void testStreamConditionFail(Blackhole blackhole) { 38 | blackhole.consume(streamCondition.test(Level.INFO, failContext)); 39 | } 40 | 41 | @Benchmark 42 | public void testPathConditionPass(Blackhole blackhole) { 43 | blackhole.consume(pathCondition.test(Level.INFO, passContext)); 44 | } 45 | 46 | @Benchmark 47 | public void testPathConditionFail(Blackhole blackhole) { 48 | blackhole.consume(pathCondition.test(Level.INFO, failContext)); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /logstash/src/jmh/java/echopraxia/logstash/SLF4JLoggerBenchmarks.java: -------------------------------------------------------------------------------- 1 | package echopraxia.logstash; 2 | 3 | import static net.logstash.logback.argument.StructuredArguments.kv; 4 | 5 | import java.util.concurrent.TimeUnit; 6 | import org.openjdk.jmh.annotations.*; 7 | import org.openjdk.jmh.infra.Blackhole; 8 | import org.slf4j.Logger; 9 | 10 | @BenchmarkMode(Mode.AverageTime) 11 | @OutputTimeUnit(TimeUnit.NANOSECONDS) 12 | @Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) 13 | @Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) 14 | @Fork(1) 15 | public class SLF4JLoggerBenchmarks { 16 | private static final Logger logger = 17 | org.slf4j.LoggerFactory.getLogger(SLF4JLoggerBenchmarks.class); 18 | 19 | private static final Exception exception = new RuntimeException(); 20 | 21 | @Benchmark 22 | public void info() { 23 | logger.info("message"); 24 | } 25 | 26 | @Benchmark 27 | public void isInfoEnabled(Blackhole blackhole) { 28 | blackhole.consume(logger.isInfoEnabled()); 29 | } 30 | 31 | @Benchmark 32 | public void infoWithArgument() { 33 | logger.info("message {}", kv("key", "value")); 34 | } 35 | 36 | @Benchmark 37 | public void infoWithArrayArgs() { 38 | logger.info( 39 | "message {} {} {} {}", 40 | kv("key1", "one"), 41 | kv("key2", "two"), 42 | kv("key3", "three"), 43 | kv("key4", "four")); 44 | } 45 | 46 | @Benchmark 47 | public void infoWithException() { 48 | logger.info("Message", exception); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /logstash/src/jmh/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /logstash/src/main/java/echopraxia/logstash/FieldMarker.java: -------------------------------------------------------------------------------- 1 | package echopraxia.logstash; 2 | 3 | import echopraxia.api.*; 4 | import echopraxia.api.Attributes; 5 | import echopraxia.api.Field; 6 | import echopraxia.api.Value; 7 | import java.util.Collection; 8 | import java.util.Collections; 9 | import java.util.List; 10 | import net.logstash.logback.marker.ObjectAppendingMarker; 11 | import org.jetbrains.annotations.NotNull; 12 | 13 | /** 14 | * This class is used by logstash-logback-encoder to turn a field into something that looks like a 15 | * StructuredArgument/Marker. 16 | */ 17 | public class FieldMarker extends ObjectAppendingMarker implements Field { 18 | 19 | private final Field field; 20 | 21 | public FieldMarker(Field field) { 22 | super(field.name(), field.value()); 23 | this.field = field; 24 | } 25 | 26 | @Override 27 | public @NotNull String name() { 28 | return field.name(); 29 | } 30 | 31 | @Override 32 | public @NotNull Value value() { 33 | return field.value(); 34 | } 35 | 36 | @Override 37 | public @NotNull List fields() { 38 | return Collections.singletonList(this); 39 | } 40 | 41 | @Override 42 | public @NotNull Attributes attributes() { 43 | return field.attributes(); 44 | } 45 | 46 | @Override 47 | public String toStringSelf() { 48 | return field.toString(); 49 | } 50 | 51 | public String toString() { 52 | return field.toString(); 53 | } 54 | 55 | @Override 56 | public @NotNull Field withAttribute(@NotNull Attribute attr) { 57 | return new FieldMarker(field.withAttribute(attr)); 58 | } 59 | 60 | @Override 61 | public @NotNull Field withAttributes(@NotNull Attributes attrs) { 62 | return new FieldMarker(field.withAttributes(attrs)); 63 | } 64 | 65 | @Override 66 | public @NotNull Field withoutAttribute(@NotNull AttributeKey key) { 67 | return new FieldMarker(field.withoutAttribute(key)); 68 | } 69 | 70 | @Override 71 | public @NotNull Field withoutAttributes(@NotNull Collection> keys) { 72 | return new FieldMarker(field.withoutAttributes(keys)); 73 | } 74 | 75 | @Override 76 | public @NotNull Field clearAttributes() { 77 | return new FieldMarker(field.clearAttributes()); 78 | } 79 | 80 | @Override 81 | public @NotNull Field asValueOnly() { 82 | return new FieldMarker(field.asValueOnly()); 83 | } 84 | 85 | @Override 86 | public @NotNull Field asElided() { 87 | return new FieldMarker(field.asElided()); 88 | } 89 | 90 | @Override 91 | public @NotNull Field withDisplayName(@NotNull String displayName) { 92 | return new FieldMarker(field.withDisplayName(displayName)); 93 | } 94 | 95 | @Override 96 | public @NotNull Field withStructuredFormat(@NotNull FieldVisitor fieldVisitor) { 97 | return new FieldMarker(field.withStructuredFormat(fieldVisitor)); 98 | } 99 | 100 | @Override 101 | public @NotNull Field withToStringFormat(@NotNull FieldVisitor fieldVisitor) { 102 | return new FieldMarker(field.withToStringFormat(fieldVisitor)); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /logstash/src/main/java/echopraxia/logstash/LogstashEchopraxiaService.java: -------------------------------------------------------------------------------- 1 | package echopraxia.logstash; 2 | 3 | import ch.qos.logback.classic.LoggerContext; 4 | import echopraxia.logging.spi.AbstractEchopraxiaService; 5 | import echopraxia.logging.spi.CoreLogger; 6 | import java.util.ServiceConfigurationError; 7 | import org.jetbrains.annotations.NotNull; 8 | import org.slf4j.ILoggerFactory; 9 | import org.slf4j.LoggerFactory; 10 | 11 | public class LogstashEchopraxiaService extends AbstractEchopraxiaService { 12 | 13 | // Customize the number of retries, using 10 as the default 14 | private static final int retryCount = 15 | Integer.parseInt(System.getProperty("echopraxia.logback.retries", "10")); 16 | 17 | protected volatile LoggerContext loggerContext; 18 | 19 | protected void initialize() { 20 | if (loggerContext == null) { 21 | // Delay initialization as long as possible, and attempt to account for 22 | // a logging component that may invoke code which logs (and therefore uses 23 | // a substitute logger. Until Logback has finished initializing, we'll get 24 | // back org.slf4j.helpers.SubstituteLoggerFactory and that's no good. 25 | // 26 | // Sleeping here isn't ideal, but: 27 | // 28 | // a) it should only happen once 29 | // b) it's happening at start up, in application code 30 | // c) I don't know what else to do here. 31 | // 32 | // https://www.slf4j.org/codes.html#substituteLogger 33 | // https://www.slf4j.org/codes.html#replay 34 | if (loggerContext == null) { 35 | int retries = retryCount; 36 | ILoggerFactory factory = null; 37 | while (retries > 0) { 38 | try { 39 | factory = LoggerFactory.getILoggerFactory(); 40 | if (factory instanceof LoggerContext) { 41 | loggerContext = (LoggerContext) factory; 42 | break; 43 | } else { 44 | System.err.println( 45 | "LogstashLoggerProvider: Logback still initializing, sleeping for 100 ms..."); 46 | retries -= 1; 47 | // https://logback.qos.ch/manual/configuration.html#auto_configuration 48 | // It takes about 100 milliseconds for Joran to parse a given logback configuration 49 | // file. 50 | Thread.sleep(100L); 51 | } 52 | } catch (InterruptedException e) { 53 | Thread.currentThread().interrupt(); 54 | } 55 | } 56 | if (loggerContext == null) { 57 | System.err.println( 58 | "LogstashLoggerProvider: No Logback implementation can be found after 10 retries. Giving up."); 59 | throw new ServiceConfigurationError("Invalid ILoggerFactory implementation " + factory); 60 | } 61 | } 62 | } 63 | } 64 | 65 | public LogstashEchopraxiaService() { 66 | super(); 67 | initialize(); 68 | } 69 | 70 | @Override 71 | public @NotNull CoreLogger getCoreLogger(@NotNull String fqcn, @NotNull Class clazz) { 72 | return getCoreLogger(fqcn, clazz.getName()); 73 | } 74 | 75 | @Override 76 | public @NotNull CoreLogger getCoreLogger(@NotNull String fqcn, @NotNull String name) { 77 | return new LogstashCoreLogger(fqcn, loggerContext.getLogger(name)); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /logstash/src/main/java/echopraxia/logstash/LogstashEchopraxiaServiceProvider.java: -------------------------------------------------------------------------------- 1 | package echopraxia.logstash; 2 | 3 | import echopraxia.logging.spi.EchopraxiaService; 4 | import echopraxia.logging.spi.EchopraxiaServiceProvider; 5 | 6 | public class LogstashEchopraxiaServiceProvider implements EchopraxiaServiceProvider { 7 | @Override 8 | public EchopraxiaService getEchopraxiaService() { 9 | return new LogstashEchopraxiaService(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /logstash/src/main/java/echopraxia/logstash/LogstashFieldAppender.java: -------------------------------------------------------------------------------- 1 | package echopraxia.logstash; 2 | 3 | import ch.qos.logback.classic.spi.ILoggingEvent; 4 | import ch.qos.logback.classic.spi.LoggingEvent; 5 | import ch.qos.logback.classic.spi.ThrowableProxy; 6 | import echopraxia.api.Field; 7 | import echopraxia.api.Value; 8 | import echopraxia.logback.DirectFieldMarker; 9 | import echopraxia.logback.TransformingAppender; 10 | import java.util.ArrayList; 11 | import java.util.Iterator; 12 | import java.util.List; 13 | import net.logstash.logback.marker.Markers; 14 | import org.slf4j.Marker; 15 | 16 | /** 17 | * An appender that converts the logging event from containing Field into containing 18 | * StructuredArgument so that it can be rendered as JSON. 19 | * 20 | *

This should only really be used if you are working with SLF4J directly, as it's a hack. 21 | */ 22 | public class LogstashFieldAppender extends TransformingAppender { 23 | 24 | @Override 25 | protected ILoggingEvent decorateEvent(ILoggingEvent eventObject) { 26 | // Run through the markers and convert FieldMarker to LogstashMarker 27 | final Marker marker = eventObject.getMarker(); 28 | if (marker != null) { 29 | List markers = new ArrayList<>(); 30 | if (marker instanceof DirectFieldMarker) { 31 | final List fields = ((DirectFieldMarker) marker).getFields(); 32 | for (Field field : fields) { 33 | markers.add(new FieldMarker(field)); 34 | } 35 | } 36 | final Iterator iterator = marker.iterator(); 37 | for (Marker m; iterator.hasNext(); ) { 38 | m = iterator.next(); 39 | if (m instanceof DirectFieldMarker) { 40 | final List fields = ((DirectFieldMarker) m).getFields(); 41 | for (Field field : fields) { 42 | markers.add(new FieldMarker(field)); 43 | } 44 | } 45 | } 46 | // Add the original marker to the end of the list... 47 | marker.add(Markers.aggregate(markers)); 48 | } 49 | 50 | // what comes in is fields, what comes out is structured arguments 51 | final Object[] argumentArray = eventObject.getArgumentArray(); 52 | if (argumentArray != null) { 53 | for (int i = 0; i < argumentArray.length; i++) { 54 | final Object arg = argumentArray[i]; 55 | if (arg instanceof Field) { 56 | Field field = (Field) arg; 57 | argumentArray[i] = new FieldMarker(field); 58 | 59 | // swap out the throwable if one is found 60 | if (eventObject.getThrowableProxy() == null) { 61 | final Throwable throwable = extractThrowable(field); 62 | if (throwable != null) { 63 | ((LoggingEvent) eventObject).setThrowableProxy(new ThrowableProxy(throwable)); 64 | } 65 | } 66 | } 67 | } 68 | } 69 | return eventObject; 70 | } 71 | 72 | protected Throwable extractThrowable(Field field) { 73 | Value value = field.value(); 74 | if (value.type() == Value.Type.EXCEPTION) { 75 | Value.ExceptionValue throwable = (Value.ExceptionValue) value; 76 | return throwable.raw(); 77 | } else { 78 | return null; 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /logstash/src/main/resources/META-INF/services/echopraxia.logging.spi.EchopraxiaServiceProvider: -------------------------------------------------------------------------------- 1 | echopraxia.logstash.LogstashEchopraxiaServiceProvider -------------------------------------------------------------------------------- /logstash/src/test/java/echopraxia/logstash/ConverterTest.java: -------------------------------------------------------------------------------- 1 | package echopraxia.logstash; 2 | 3 | import ch.qos.logback.classic.LoggerContext; 4 | import echopraxia.logger.LoggerFactory; 5 | import java.net.MalformedURLException; 6 | import java.net.URISyntaxException; 7 | import org.junit.jupiter.api.AfterEach; 8 | import org.junit.jupiter.api.BeforeEach; 9 | import org.junit.jupiter.api.Test; 10 | 11 | public class ConverterTest { 12 | protected LoggerContext loggerContext; 13 | 14 | @BeforeEach 15 | public void before() { 16 | try { 17 | System.setProperty( 18 | "logback.configurationFile", 19 | getClass().getResource("/logback-converter.xml").toURI().toURL().toString()); 20 | } catch (MalformedURLException | URISyntaxException e) { 21 | throw new RuntimeException(e); 22 | } 23 | loggerContext = (LoggerContext) org.slf4j.LoggerFactory.getILoggerFactory(); 24 | } 25 | 26 | @AfterEach 27 | public void after() { 28 | loggerContext.stop(); 29 | } 30 | 31 | @Test 32 | public void testLog() { 33 | var logger = LoggerFactory.getLogger(getClass()); 34 | logger.info("Only arguments", fb -> fb.string("book", "The Cask")); 35 | logger.withFields(fb -> fb.string("book", "The Cask")).info("Only logger context"); 36 | logger 37 | .withFields(fb -> fb.string("book", "From Context")) 38 | .info("Both context and arguments", fb -> fb.string("book", "From Argument")); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /logstash/src/test/java/echopraxia/logstash/EncodingListAppender.java: -------------------------------------------------------------------------------- 1 | package echopraxia.logstash; 2 | 3 | import ch.qos.logback.core.UnsynchronizedAppenderBase; 4 | import ch.qos.logback.core.encoder.Encoder; 5 | import java.nio.charset.StandardCharsets; 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | 9 | public class EncodingListAppender extends UnsynchronizedAppenderBase { 10 | 11 | public List list = new ArrayList<>(); 12 | 13 | protected Encoder encoder; 14 | 15 | public Encoder getEncoder() { 16 | return encoder; 17 | } 18 | 19 | public void setEncoder(Encoder encoder) { 20 | this.encoder = encoder; 21 | } 22 | 23 | protected void append(E e) { 24 | final byte[] encode = encoder.encode(e); 25 | final String s = new String(encode, StandardCharsets.UTF_8); 26 | list.add(s); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /logstash/src/test/java/echopraxia/logstash/LoggerFactoryTest.java: -------------------------------------------------------------------------------- 1 | package echopraxia.logstash; 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 | -------------------------------------------------------------------------------- /logstash/src/test/java/echopraxia/logstash/StaticExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package echopraxia.logstash; 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 | -------------------------------------------------------------------------------- /logstash/src/test/java/echopraxia/logstash/TestBase.java: -------------------------------------------------------------------------------- 1 | package echopraxia.logstash; 2 | 3 | import static org.slf4j.Logger.ROOT_LOGGER_NAME; 4 | 5 | import ch.qos.logback.classic.LoggerContext; 6 | import ch.qos.logback.classic.joran.JoranConfigurator; 7 | import ch.qos.logback.classic.spi.ILoggingEvent; 8 | import ch.qos.logback.core.joran.spi.JoranException; 9 | import ch.qos.logback.core.read.ListAppender; 10 | import echopraxia.api.FieldBuilder; 11 | import echopraxia.logback.TransformingAppender; 12 | import echopraxia.logger.Logger; 13 | import echopraxia.logger.LoggerFactory; 14 | import java.net.MalformedURLException; 15 | import java.net.URISyntaxException; 16 | import org.junit.jupiter.api.BeforeEach; 17 | 18 | public class TestBase { 19 | protected LoggerContext loggerContext; 20 | 21 | @BeforeEach 22 | public void before() { 23 | try { 24 | StaticExceptionHandler.clear(); 25 | LoggerContext factory = new LoggerContext(); 26 | JoranConfigurator joran = new JoranConfigurator(); 27 | joran.setContext(factory); 28 | factory.reset(); 29 | joran.doConfigure(getClass().getResource("/logback-test.xml").toURI().toURL()); 30 | this.loggerContext = factory; 31 | } catch (JoranException | URISyntaxException | MalformedURLException je) { 32 | je.printStackTrace(); 33 | } 34 | } 35 | 36 | void waitUntilMessages() { 37 | final EncodingListAppender appender = getStringAppender(); 38 | org.awaitility.Awaitility.await().until(() -> !appender.list.isEmpty()); 39 | } 40 | 41 | LoggerContext loggerContext() { 42 | return loggerContext; 43 | } 44 | 45 | LogstashCoreLogger getCoreLogger() { 46 | return new LogstashCoreLogger(Logger.FQCN, loggerContext().getLogger(getClass().getName())); 47 | } 48 | 49 | Logger getLogger() { 50 | return LoggerFactory.getLogger(getCoreLogger(), FieldBuilder.instance()); 51 | } 52 | 53 | ListAppender getListAppender() { 54 | final ch.qos.logback.classic.Logger logger = loggerContext().getLogger(ROOT_LOGGER_NAME); 55 | final TransformingAppender next = 56 | (TransformingAppender) logger.iteratorForAppenders().next(); 57 | return (ListAppender) next.getAppender("LIST"); 58 | } 59 | 60 | EncodingListAppender getStringAppender() { 61 | final ch.qos.logback.classic.Logger logger = loggerContext().getLogger(ROOT_LOGGER_NAME); 62 | final TransformingAppender next = 63 | (TransformingAppender) logger.iteratorForAppenders().next(); 64 | return (EncodingListAppender) next.getAppender("STRINGLIST"); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /logstash/src/test/java/echopraxia/logstash/TestEchopraxiaService.java: -------------------------------------------------------------------------------- 1 | package echopraxia.logstash; 2 | 3 | public class TestEchopraxiaService extends LogstashEchopraxiaService { 4 | public TestEchopraxiaService() { 5 | super(); 6 | this.exceptionHandler = new StaticExceptionHandler(); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /logstash/src/test/java/echopraxia/logstash/TestEchopraxiaServiceProvider.java: -------------------------------------------------------------------------------- 1 | package echopraxia.logstash; 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 | -------------------------------------------------------------------------------- /logstash/src/test/resources/META-INF/services/echopraxia.logging.spi.EchopraxiaServiceProvider: -------------------------------------------------------------------------------- 1 | echopraxia.logstash.TestEchopraxiaServiceProvider -------------------------------------------------------------------------------- /logstash/src/test/resources/logback-converter.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | %-4relative [%thread] %-5level [%fields{$.book}] [%loggerctx{$.book}] [%argctx{$.book}] %logger - %msg%n 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /logstash/src/test/resources/logback-direct-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /logstash/src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /logstash/src/test/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | 11 | 100 12 | 13 | 14 | 15 | 16 | true 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | UTC 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: Echopraxia 2 | 3 | # Meta tags (placed in header) 4 | site_description: "Echopraxia - Structured logging for Java" 5 | 6 | site_author: "Will Sargent" 7 | site_url: "https://tersesystems.github.io/echopraxia" 8 | 9 | # Repository (add link to repository on each page) 10 | repo_name: echopraxia 11 | 12 | repo_url: "https://github.com/tersesystems/echopraxia" 13 | edit_uri: "edit/main/docs/" 14 | 15 | #Copyright (shown at the footer) 16 | #copyright: 'Copyright © 2017 Your Name' 17 | 18 | # Meterial theme 19 | # https://github.com/squidfunk/mkdocs-material 20 | # pip install mkdocs-material 21 | theme: 'material' 22 | 23 | extra: 24 | version: 25 | provider: mike 26 | 27 | palette: 28 | primary: 'indigo' 29 | accent: 'indigo' 30 | 31 | social: 32 | - icon: fontawesome/brands/mastodon 33 | link: 'https://mastodon.xyz/web/@will_sargent' 34 | 35 | 36 | # Google Analytics 37 | #google_analytics: 38 | # - 'UA-111111111-1' 39 | # - 'auto' 40 | 41 | # Extensions 42 | markdown_extensions: 43 | - admonition 44 | - codehilite: 45 | guess_lang: false 46 | - footnotes 47 | - meta 48 | - toc: 49 | permalink: true 50 | - pymdownx.betterem: 51 | smart_enable: all 52 | - pymdownx.caret 53 | - pymdownx.inlinehilite 54 | - pymdownx.magiclink 55 | - pymdownx.smartsymbols 56 | - pymdownx.superfences 57 | 58 | nav: 59 | - Home: index.md 60 | - Installation: installation.md 61 | - Usage: 62 | - Basics: usage/basics.md 63 | - Field Builders: usage/fieldbuilder.md 64 | - Context: usage/context.md 65 | - Conditions: usage/conditions.md 66 | - Custom Logger: usage/logger.md 67 | - Filters: usage/filters.md 68 | - Scripting: usage/scripting.md 69 | - Frameworks: 70 | - Logback Framework: frameworks/logback.md 71 | - Log4J2 Framework: frameworks/log4j2.md 72 | - FAQ: faq.md 73 | -------------------------------------------------------------------------------- /noop/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java-library' 3 | } 4 | 5 | dependencies { 6 | api project(":logging") 7 | } 8 | -------------------------------------------------------------------------------- /noop/src/main/java/echopraxia/noop/NoopEchopraxiaService.java: -------------------------------------------------------------------------------- 1 | package echopraxia.noop; 2 | 3 | import echopraxia.logging.spi.AbstractEchopraxiaService; 4 | import echopraxia.logging.spi.CoreLogger; 5 | import org.jetbrains.annotations.NotNull; 6 | 7 | public class NoopEchopraxiaService extends AbstractEchopraxiaService { 8 | 9 | public NoopEchopraxiaService() { 10 | super(); 11 | } 12 | 13 | @Override 14 | public @NotNull CoreLogger getCoreLogger(@NotNull String fqcn, @NotNull Class clazz) { 15 | return new NoopCoreLogger(fqcn); 16 | } 17 | 18 | @Override 19 | public @NotNull CoreLogger getCoreLogger(@NotNull String fqcn, @NotNull String name) { 20 | return new NoopCoreLogger(fqcn); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /noop/src/main/java/echopraxia/noop/NoopEchopraxiaServiceProvider.java: -------------------------------------------------------------------------------- 1 | package echopraxia.noop; 2 | 3 | import echopraxia.logging.spi.EchopraxiaService; 4 | import echopraxia.logging.spi.EchopraxiaServiceProvider; 5 | 6 | public class NoopEchopraxiaServiceProvider implements EchopraxiaServiceProvider { 7 | 8 | @Override 9 | public EchopraxiaService getEchopraxiaService() { 10 | return new NoopEchopraxiaService(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /noop/src/main/java/echopraxia/noop/NoopLoggerContext.java: -------------------------------------------------------------------------------- 1 | package echopraxia.noop; 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.spi.LoggerContext; 8 | import java.util.Collections; 9 | import java.util.List; 10 | import java.util.function.Supplier; 11 | import org.jetbrains.annotations.NotNull; 12 | 13 | public class NoopLoggerContext implements LoggerContext { 14 | private final Supplier> fieldsSupplier; 15 | 16 | public static NoopLoggerContext empty() { 17 | return new NoopLoggerContext(Collections::emptyList); 18 | } 19 | 20 | public NoopLoggerContext(Supplier> fieldsSupplier) { 21 | this.fieldsSupplier = memoize(fieldsSupplier); 22 | } 23 | 24 | @Override 25 | public @NotNull List getLoggerFields() { 26 | return fieldsSupplier.get(); 27 | } 28 | 29 | public LoggerContext withFields(Supplier> extraFields) { 30 | return new NoopLoggerContext(joinFields(fieldsSupplier, extraFields)); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /noop/src/main/resources/META-INF/services/echopraxia.logging.spi.EchopraxiaServiceProvider: -------------------------------------------------------------------------------- 1 | echopraxia.noop.NoopEchopraxiaServiceProvider -------------------------------------------------------------------------------- /scripting/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java-library' 3 | } 4 | 5 | dependencies { 6 | api project(":logging") 7 | api project(":jsonpath") 8 | 9 | implementation project(":filewatch") 10 | implementation "com.twineworks:tweakflow:$tweakflowVersion" 11 | 12 | // https://github.com/playframework/play-file-watch 13 | 14 | jmhImplementation project(":logstash") 15 | 16 | testImplementation project(":logstash") 17 | testImplementation project(":logger") 18 | 19 | testImplementation "ch.qos.logback:logback-classic:$logbackVersion" 20 | testImplementation "net.logstash.logback:logstash-logback-encoder:$logstashVersion" 21 | } 22 | -------------------------------------------------------------------------------- /scripting/src/jmh/java/echopraxia/scripting/FakeLoggingContext.java: -------------------------------------------------------------------------------- 1 | package echopraxia.scripting; 2 | 3 | import echopraxia.api.Field; 4 | import echopraxia.jsonpath.AbstractJsonPathFinder; 5 | import echopraxia.logging.api.LoggingContext; 6 | import echopraxia.logging.spi.CoreLogger; 7 | import java.util.Arrays; 8 | import java.util.Collections; 9 | import java.util.List; 10 | import org.jetbrains.annotations.NotNull; 11 | import org.jetbrains.annotations.Nullable; 12 | 13 | public class FakeLoggingContext extends AbstractJsonPathFinder implements LoggingContext { 14 | private final List loggerFields; 15 | 16 | public FakeLoggingContext(Field... loggerFields) { 17 | this.loggerFields = Arrays.asList(loggerFields); 18 | } 19 | 20 | @Override 21 | public @Nullable CoreLogger getCore() { 22 | return null; 23 | } 24 | 25 | @Override 26 | public @NotNull List getFields() { 27 | return loggerFields; 28 | } 29 | 30 | @Override 31 | public List getArgumentFields() { 32 | return Collections.emptyList(); 33 | } 34 | 35 | @Override 36 | public List getLoggerFields() { 37 | return loggerFields; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /scripting/src/jmh/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /scripting/src/jmh/tweakflow/condition.tf: -------------------------------------------------------------------------------- 1 | library echopraxia { 2 | function evaluate: (string level, dict ctx) -> 3 | let { 4 | find_number: ctx[:find_number]; 5 | } 6 | find_number("$.some_field") == 1; 7 | } 8 | -------------------------------------------------------------------------------- /scripting/src/main/java/echopraxia/scripting/FileScriptHandle.java: -------------------------------------------------------------------------------- 1 | package echopraxia.scripting; 2 | 3 | import java.io.BufferedReader; 4 | import java.io.IOException; 5 | import java.nio.file.Files; 6 | import java.nio.file.Path; 7 | import java.util.function.Consumer; 8 | import java.util.stream.Collectors; 9 | 10 | /** 11 | * A script handle that uses a direct path to a file. 12 | * 13 | *

Errors are sent to the reporter. 14 | */ 15 | public class FileScriptHandle implements ScriptHandle { 16 | 17 | private final Path path; 18 | private final Consumer reporter; 19 | 20 | FileScriptHandle(Path path, Consumer reporter) { 21 | this.path = path; 22 | this.reporter = reporter; 23 | } 24 | 25 | @Override 26 | public boolean isInvalid() { 27 | return false; 28 | } 29 | 30 | @Override 31 | public String script() { 32 | if (!Files.exists(path)) { 33 | throw new ScriptException("No path found at " + path.toAbsolutePath()); 34 | } 35 | 36 | try { 37 | return readString(path); 38 | } catch (IOException e) { 39 | throw new ScriptException(e); 40 | } 41 | } 42 | 43 | @Override 44 | public String path() { 45 | return path.toString(); 46 | } 47 | 48 | protected String readString(Path path) throws IOException { 49 | try (BufferedReader reader = Files.newBufferedReader(path)) { 50 | return reader.lines().collect(Collectors.joining("\n")); 51 | } 52 | } 53 | 54 | public void report(Throwable e) { 55 | reporter.accept(e); 56 | } 57 | 58 | @Override 59 | public void close() throws IOException { 60 | // do nothing 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /scripting/src/main/java/echopraxia/scripting/ScriptException.java: -------------------------------------------------------------------------------- 1 | package echopraxia.scripting; 2 | 3 | public class ScriptException extends RuntimeException { 4 | public ScriptException() {} 5 | 6 | public ScriptException(String message) { 7 | super(message); 8 | } 9 | 10 | public ScriptException(String message, Throwable cause) { 11 | super(message, cause); 12 | } 13 | 14 | public ScriptException(Throwable cause) { 15 | super(cause); 16 | } 17 | 18 | public ScriptException( 19 | String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { 20 | super(message, cause, enableSuppression, writableStackTrace); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /scripting/src/main/java/echopraxia/scripting/ScriptHandle.java: -------------------------------------------------------------------------------- 1 | package echopraxia.scripting; 2 | 3 | /** A script handle returns the script source, and can say if the script is invalid. */ 4 | public interface ScriptHandle extends AutoCloseable { 5 | 6 | /** 7 | * @return true if the script is invalid and should be re-evaluated, false otherwise. 8 | */ 9 | boolean isInvalid(); 10 | 11 | /** 12 | * @return the code of the script. 13 | */ 14 | String script(); 15 | 16 | /** 17 | * The path to use when debugging / evaulating the script. 18 | * 19 | * @return the path 20 | */ 21 | String path(); 22 | 23 | /** 24 | * The library name for the script handle, "echopraxia" by default 25 | * 26 | * @return the library name 27 | */ 28 | default String libraryName() { 29 | return "echopraxia"; 30 | } 31 | 32 | /** 33 | * The function name for the script handle, "evaluate" by default 34 | * 35 | * @return the function name. 36 | */ 37 | default String functionName() { 38 | return "evaluate"; 39 | } 40 | 41 | /** 42 | * If evaluating or parsing the script throws an exception, this method is called. 43 | * 44 | * @param e throwable caused by failure in script 45 | */ 46 | void report(Throwable e); 47 | } 48 | -------------------------------------------------------------------------------- /scripting/src/main/java/echopraxia/scripting/ValueMapEntry.java: -------------------------------------------------------------------------------- 1 | package echopraxia.scripting; 2 | 3 | import com.twineworks.tweakflow.lang.values.UserFunctionValue; 4 | import com.twineworks.tweakflow.lang.values.Values; 5 | import java.util.Map; 6 | import java.util.Objects; 7 | 8 | /** A value map entry that maps a string to a valeu. */ 9 | public final class ValueMapEntry 10 | implements Map.Entry { 11 | final String key; 12 | final com.twineworks.tweakflow.lang.values.Value value; 13 | 14 | private ValueMapEntry(String k, com.twineworks.tweakflow.lang.values.Value v) { 15 | key = Objects.requireNonNull(k); 16 | value = Objects.requireNonNull(v); 17 | } 18 | 19 | /** 20 | * @param name the name of the entry 21 | * @param userFunction the user function to apply 22 | * @return the value map entry 23 | */ 24 | public static ValueMapEntry make(String name, UserFunctionValue userFunction) { 25 | return new ValueMapEntry(name, Values.make(userFunction)); 26 | } 27 | 28 | @Override 29 | public String getKey() { 30 | return key; 31 | } 32 | 33 | @Override 34 | public com.twineworks.tweakflow.lang.values.Value getValue() { 35 | return value; 36 | } 37 | 38 | @Override 39 | public com.twineworks.tweakflow.lang.values.Value setValue( 40 | com.twineworks.tweakflow.lang.values.Value value) { 41 | throw new UnsupportedOperationException("not supported"); 42 | } 43 | 44 | @Override 45 | public boolean equals(Object o) { 46 | if (!(o instanceof Map.Entry)) return false; 47 | Map.Entry e = (Map.Entry) o; 48 | return key.equals(e.getKey()) && value.equals(e.getValue()); 49 | } 50 | 51 | @Override 52 | public int hashCode() { 53 | return key.hashCode() ^ value.hashCode(); 54 | } 55 | 56 | @Override 57 | public String toString() { 58 | return key + "=" + value; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /scripting/src/test/java/echopraxia/scripting/FakeLoggingContext.java: -------------------------------------------------------------------------------- 1 | package echopraxia.scripting; 2 | 3 | import echopraxia.api.Field; 4 | import echopraxia.jsonpath.AbstractJsonPathFinder; 5 | import echopraxia.logging.api.LoggingContextWithFindPathMethods; 6 | import echopraxia.logging.spi.CoreLogger; 7 | import java.util.Arrays; 8 | import java.util.Collections; 9 | import java.util.List; 10 | import org.jetbrains.annotations.NotNull; 11 | import org.jetbrains.annotations.Nullable; 12 | 13 | public class FakeLoggingContext extends AbstractJsonPathFinder 14 | implements LoggingContextWithFindPathMethods { 15 | private final List loggerFields; 16 | 17 | public FakeLoggingContext(Field... loggerFields) { 18 | this.loggerFields = Arrays.asList(loggerFields); 19 | } 20 | 21 | @Override 22 | public @Nullable CoreLogger getCore() { 23 | return null; 24 | } 25 | 26 | @Override 27 | public @NotNull List getFields() { 28 | return loggerFields; 29 | } 30 | 31 | @Override 32 | public List getArgumentFields() { 33 | return Collections.emptyList(); 34 | } 35 | 36 | @Override 37 | public List getLoggerFields() { 38 | return loggerFields; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /scripting/src/test/java/echopraxia/scripting/Main.java: -------------------------------------------------------------------------------- 1 | package echopraxia.scripting; 2 | 3 | import static echopraxia.logging.api.Level.INFO; 4 | 5 | import echopraxia.api.Field; 6 | import echopraxia.api.Value; 7 | import echopraxia.logging.api.Condition; 8 | import echopraxia.logging.api.LoggingContext; 9 | import java.io.IOException; 10 | import java.nio.file.Files; 11 | import java.nio.file.Path; 12 | import java.util.Arrays; 13 | import java.util.List; 14 | 15 | public class Main { 16 | 17 | public static void main(String[] args) throws IOException, InterruptedException { 18 | final Path dir = Files.createTempDirectory("echopraxia"); 19 | try (ScriptWatchService watchService = new ScriptWatchService(dir)) { 20 | Path filePath = dir.resolve("testfile"); 21 | Files.write(filePath, lines("INFO")); 22 | 23 | final ScriptHandle handle = watchService.watchScript(filePath, Throwable::printStackTrace); 24 | final Condition condition = ScriptCondition.create(handle); 25 | LoggingContext context = 26 | new FakeLoggingContext(Field.keyValue("correlation_id", Value.string("match"))); 27 | if (condition.test(INFO, context)) { 28 | System.out.println("First test should eval but take a while..."); 29 | } 30 | 31 | for (int i = 0; i < 100; i++) { 32 | // Write a change and then sleep 33 | Files.write(filePath, lines("DEBUG")); 34 | Thread.sleep(300); 35 | 36 | // and then we should be able to see the changed condition evaluate. 37 | if (condition.test(INFO, context)) { 38 | System.out.println( 39 | "this doesn't log, because even though we pass in INFO the script changed to DEBUG"); 40 | } 41 | 42 | // Write the condition again and give some time for the watcher 43 | Files.write(filePath, lines("INFO")); 44 | Thread.sleep(300); 45 | 46 | // Now the first time it sees, it'll re-evaluate and process again! 47 | if (condition.test(INFO, context)) { 48 | System.out.println("this logs because we have changed the script back to INFO."); 49 | } 50 | 51 | Files.deleteIfExists(filePath); 52 | Thread.sleep(300); 53 | if (condition.test(INFO, context)) { 54 | System.out.println("see what happens when script is deleted"); 55 | } 56 | } 57 | } 58 | } 59 | 60 | private static List lines(String name) { 61 | return Arrays.asList( 62 | "library echopraxia {", 63 | " function evaluate: (string level, dict ctx) ->", 64 | " level == \"" + name + "\";", 65 | "}"); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /scripting/src/test/java/echopraxia/scripting/ScriptManagerTest.java: -------------------------------------------------------------------------------- 1 | package echopraxia.scripting; 2 | 3 | import static java.util.concurrent.TimeUnit.SECONDS; 4 | import static org.assertj.core.api.Assertions.assertThat; 5 | import static org.awaitility.Awaitility.await; 6 | 7 | import echopraxia.logging.api.Level; 8 | import java.util.concurrent.CompletableFuture; 9 | import java.util.concurrent.ExecutorService; 10 | import java.util.concurrent.Executors; 11 | import java.util.concurrent.atomic.LongAdder; 12 | import org.junit.jupiter.api.Test; 13 | 14 | public class ScriptManagerTest { 15 | 16 | public String buildScript() { 17 | StringBuilder b = new StringBuilder("library echopraxia {"); 18 | b.append(" function evaluate: (string level, dict ctx) ->"); 19 | b.append(" true;"); 20 | b.append("}"); 21 | return b.toString(); 22 | } 23 | 24 | @Test 25 | public void testConcurrency() { 26 | String script = buildScript(); 27 | final ScriptHandle handle = 28 | new ScriptHandle() { 29 | @Override 30 | public boolean isInvalid() { 31 | return false; 32 | } 33 | 34 | @Override 35 | public String script() { 36 | return script; 37 | } 38 | 39 | @Override 40 | public String path() { 41 | return ""; 42 | } 43 | 44 | @Override 45 | public void report(Throwable e) { 46 | e.printStackTrace(); 47 | } 48 | 49 | @Override 50 | public void close() throws Exception {} 51 | }; 52 | final ScriptManager scriptManager = new ScriptManager(handle); 53 | 54 | var empty = new FakeLoggingContext(); 55 | int parallel = 4; 56 | final ExecutorService executorService = Executors.newWorkStealingPool(parallel); 57 | LongAdder count = new LongAdder(); 58 | int limit = 3000000; // about 27 seconds on my laptop 59 | 60 | try { 61 | for (int j = 0; j < parallel; j++) { 62 | CompletableFuture.runAsync( 63 | () -> { 64 | for (int i = 0; i < limit; i++) { 65 | try { 66 | if (scriptManager.execute(false, Level.INFO, empty)) { 67 | count.increment(); 68 | } 69 | } catch (Exception e) { 70 | e.printStackTrace(); 71 | } 72 | } 73 | }, 74 | executorService); 75 | } 76 | 77 | await().atMost(30, SECONDS).until(() -> count.intValue() >= limit * parallel); 78 | assertThat(count.intValue()).isEqualTo(limit * parallel); 79 | } finally { 80 | executorService.shutdown(); 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /scripting/src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | %date{H:mm:ss.SSS} [%highlight(%-5level)] %logger{15} - %message%ex%n 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /scripting/src/test/tweakflow/condition.tf: -------------------------------------------------------------------------------- 1 | # https://marketplace.visualstudio.com/items?itemName=twineworks.tweakflow 2 | # https://twineworks.github.io/tweakflow/reference.html 3 | 4 | library echopraxia { 5 | 6 | doc 'Evaluates if correlation_id matches given value' 7 | function evaluate: (string level, dict ctx) -> 8 | let { 9 | find_string: ctx[:find_string]; 10 | } 11 | find_string("correlation_id") == "match"; 12 | } 13 | -------------------------------------------------------------------------------- /scripting/src/test/tweakflow/exception.tf: -------------------------------------------------------------------------------- 1 | # https://marketplace.visualstudio.com/items?itemName=twineworks.tweakflow 2 | # https://twineworks.github.io/tweakflow/reference.html 3 | 4 | library echopraxia { 5 | 6 | doc 'Evaluates if exception matches' 7 | function evaluate: (string level, dict ctx) -> 8 | let { 9 | find_string: ctx[:find_string]; 10 | } 11 | find_string("$.exception.message") == "testing"; 12 | } 13 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'echopraxia' 2 | 3 | include('api') 4 | include('logstash') 5 | include('logback') 6 | include('log4j') 7 | include('jackson') 8 | include('jul') 9 | include('noop') 10 | 11 | include('logging') 12 | include('jsonpath') 13 | 14 | include('simple') 15 | include('logger') 16 | include('scripting') 17 | include('filewatch') 18 | -------------------------------------------------------------------------------- /simple/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java-library' 3 | } 4 | 5 | dependencies { 6 | api project(":logging") 7 | 8 | jmhImplementation project(":logstash") 9 | 10 | testImplementation project(":logstash") 11 | testImplementation project(":logger") 12 | 13 | testImplementation "ch.qos.logback:logback-classic:$logbackVersion" 14 | testImplementation "net.logstash.logback:logstash-logback-encoder:$logstashVersion" 15 | } 16 | -------------------------------------------------------------------------------- /simple/src/main/java/echopraxia/simple/LoggerFactory.java: -------------------------------------------------------------------------------- 1 | package echopraxia.simple; 2 | 3 | import echopraxia.logging.spi.Caller; 4 | import echopraxia.logging.spi.CoreLogger; 5 | import echopraxia.logging.spi.CoreLoggerFactory; 6 | import org.jetbrains.annotations.NotNull; 7 | 8 | public class LoggerFactory { 9 | 10 | private static final @NotNull String FQCN = "echopraxia.simple.Logger"; 11 | 12 | public static @NotNull Logger getLogger(String name) { 13 | var core = CoreLoggerFactory.getLogger(FQCN, name); 14 | return new Logger(core); 15 | } 16 | 17 | public static @NotNull Logger getLogger(Class clazz) { 18 | var core = CoreLoggerFactory.getLogger(FQCN, clazz); 19 | return new Logger(core); 20 | } 21 | 22 | /** 23 | * Creates a logger using the caller's class name. 24 | * 25 | * @return the logger. 26 | */ 27 | @NotNull 28 | public static Logger getLogger() { 29 | CoreLogger core = CoreLoggerFactory.getLogger(FQCN, Caller.resolveClassName()); 30 | return new Logger(core); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /simple/src/test/java/echopraxia/MyLogger.java: -------------------------------------------------------------------------------- 1 | package echopraxia; 2 | 3 | import echopraxia.api.FieldBuilder; 4 | import echopraxia.logging.api.*; 5 | import echopraxia.logging.spi.*; 6 | import echopraxia.simple.Logger; 7 | 8 | class MyLoggerFactory { 9 | public static class MyFieldBuilder implements FieldBuilder { 10 | // Add your own field builder methods in here 11 | } 12 | 13 | public static final MyFieldBuilder FIELD_BUILDER = new MyFieldBuilder(); 14 | 15 | public static MyLogger getLogger(Class clazz) { 16 | final CoreLogger core = CoreLoggerFactory.getLogger(Logger.class.getName(), clazz); 17 | return new MyLogger(core); 18 | } 19 | 20 | public static final class MyLogger extends Logger { 21 | public static final String FQCN = MyLogger.class.getName(); 22 | 23 | public MyLogger(CoreLogger logger) { 24 | super(logger); 25 | } 26 | 27 | public void notice(String message) { 28 | // the caller is MyLogger specifically, so we need to let the logging framework know how to 29 | // address it. 30 | core() 31 | .withFQCN(FQCN) 32 | .withFields(fb -> fb.bool("notice", true), FIELD_BUILDER) 33 | .log(Level.INFO, message); 34 | } 35 | } 36 | } 37 | 38 | class Main { 39 | private static final MyLoggerFactory.MyLogger logger = MyLoggerFactory.getLogger(Main.class); 40 | 41 | public static void main(String[] args) { 42 | logger.notice("this has a notice field added"); 43 | } 44 | } 45 | --------------------------------------------------------------------------------