Repository: tersesystems/terse-logback Branch: master Commit: 7e86ac867de1 Files: 309 Total size: 534.5 KB Directory structure: gitextract_k2vdfda5/ ├── .github/ │ └── ISSUE_TEMPLATE/ │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── .java-version ├── LICENSE ├── README.md ├── RELEASING.md ├── build.gradle ├── docs/ │ ├── guide/ │ │ ├── audio.md │ │ ├── budget.md │ │ ├── censor.md │ │ ├── composite.md │ │ ├── compression.md │ │ ├── correlationid.md │ │ ├── exception-mapping.md │ │ ├── instrumentation.md │ │ ├── jdbc.md │ │ ├── relativens.md │ │ ├── select.md │ │ ├── slf4jbridge.md │ │ ├── tracing.md │ │ ├── turbomarker.md │ │ ├── typesafeconfig.md │ │ └── uniqueid.md │ ├── index.md │ └── reading/ │ └── reading.md ├── gradle/ │ ├── LICENSE_HEADER │ ├── java-publication.gradle │ ├── release.gradle │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat ├── logback-audio/ │ ├── gradle.properties │ ├── logback-audio.gradle │ └── src/ │ ├── main/ │ │ └── java/ │ │ └── com/ │ │ └── tersesystems/ │ │ └── logback/ │ │ └── audio/ │ │ ├── AudioAppender.java │ │ ├── AudioMarker.java │ │ ├── AudioMarkerAppender.java │ │ ├── FilePlayer.java │ │ ├── PlayMethods.java │ │ ├── Player.java │ │ ├── PlayerAction.java │ │ ├── PlayerAttachable.java │ │ ├── PlayerConverter.java │ │ ├── PlayerException.java │ │ ├── ResourcePlayer.java │ │ ├── SimplePlayer.java │ │ ├── SystemPlayer.java │ │ └── URLPlayer.java │ └── test/ │ ├── java/ │ │ └── com/ │ │ └── tersesystems/ │ │ └── logback/ │ │ └── audio/ │ │ ├── TestAudio.java │ │ └── TestNested.java │ └── resources/ │ ├── bark.ogg │ ├── drip.ogg │ ├── glass.ogg │ ├── logback-with-converter.xml │ ├── logback-with-marker-appender.xml │ ├── logback-with-nested-appender.xml │ ├── message.ogg │ └── sample.ogg ├── logback-budget/ │ ├── gradle.properties │ ├── logback-budget.gradle │ └── src/ │ ├── main/ │ │ └── java/ │ │ └── com/ │ │ └── tersesystems/ │ │ └── logback/ │ │ └── budget/ │ │ ├── BudgetEvaluator.java │ │ ├── BudgetRule.java │ │ ├── BudgetRuleAction.java │ │ ├── BudgetRuleAttachable.java │ │ └── BudgetTurboFilter.java │ └── test/ │ ├── java/ │ │ └── com.tersesystems.logback.budget/ │ │ ├── BudgetEvaluatorTest.java │ │ └── BudgetTurboFilterTest.java │ └── resources/ │ ├── logback-budget.xml │ └── logback-turbofilter.xml ├── logback-bytebuddy/ │ ├── gradle.properties │ ├── logback-bytebuddy.gradle │ └── src/ │ ├── main/ │ │ └── java/ │ │ └── com/ │ │ └── tersesystems/ │ │ └── logback/ │ │ └── bytebuddy/ │ │ ├── AdviceConfig.java │ │ ├── LogbackInstrumentationAgent.java │ │ ├── LoggingInstrumentationAdvice.java │ │ ├── LoggingInstrumentationByteBuddyBuilder.java │ │ ├── MethodInfo.java │ │ ├── MethodInfoLookup.java │ │ └── impl/ │ │ ├── DeclaringTypeLoggerResolver.java │ │ ├── Enter.java │ │ ├── Exit.java │ │ ├── FixedLoggerResolver.java │ │ ├── LoggerResolver.java │ │ ├── SafeArguments.java │ │ └── SystemFlow.java │ └── test/ │ ├── java/ │ │ └── com/ │ │ └── tersesystems/ │ │ └── logback/ │ │ └── bytebuddy/ │ │ ├── AdviceConfigTest.java │ │ ├── ClassCalledByAgent.java │ │ ├── InProcessInstrumentationExample.java │ │ └── PreloadedInstrumentationExample.java │ └── resources/ │ ├── logback-test.xml │ └── logback.conf ├── logback-censor/ │ ├── gradle.properties │ ├── logback-censor.gradle │ └── src/ │ ├── main/ │ │ └── java/ │ │ └── com/ │ │ └── tersesystems/ │ │ └── logback/ │ │ └── censor/ │ │ ├── Censor.java │ │ ├── CensorAction.java │ │ ├── CensorAttachable.java │ │ ├── CensorConstants.java │ │ ├── CensorContextAware.java │ │ ├── CensorConverter.java │ │ ├── CensorRefAction.java │ │ ├── CensoringJsonGeneratorDecorator.java │ │ ├── CensoringPrettyPrintingJsonGeneratorDecorator.java │ │ └── RegexCensor.java │ └── test/ │ ├── java/ │ │ └── com/ │ │ └── tersesystems/ │ │ └── logback/ │ │ └── censor/ │ │ ├── CensorActionTest.java │ │ ├── CensoringJsonGeneratorDecoratorTest.java │ │ ├── RegexCensorTest.java │ │ └── TestAppender.java │ └── resources/ │ ├── test1.xml │ ├── test2.xml │ ├── test3.xml │ └── test4.xml ├── logback-classic/ │ ├── gradle.properties │ ├── logback-classic.gradle │ └── src/ │ ├── main/ │ │ └── java/ │ │ └── com/ │ │ └── tersesystems/ │ │ └── logback/ │ │ └── classic/ │ │ ├── ChangeLogLevel.java │ │ ├── ContainerEventAppender.java │ │ ├── ContainerProxyLoggingEvent.java │ │ ├── ContextAwareBasicMarker.java │ │ ├── ExceptionMessageConverter.java │ │ ├── FormatParamsDecider.java │ │ ├── IContainerLoggingEvent.java │ │ ├── ILoggingEventFactory.java │ │ ├── LoggerDecider.java │ │ ├── LoggingEventFactory.java │ │ ├── MarkerLoggerDecider.java │ │ ├── NanoTime.java │ │ ├── NanoTimeComponentAppender.java │ │ ├── NanoTimeConverter.java │ │ ├── NanoTimeMarker.java │ │ ├── NanoTimeSupplier.java │ │ ├── ProxyLoggingEvent.java │ │ ├── SLF4JBridgeHandlerAction.java │ │ ├── SetLoggerLevelsAction.java │ │ ├── StartTime.java │ │ ├── StartTimeConverter.java │ │ ├── StartTimeMarker.java │ │ ├── StartTimeSupplier.java │ │ ├── TapFilter.java │ │ ├── TerseBasicMarker.java │ │ ├── TerseHighlightConverter.java │ │ ├── TimeSinceEpochConverter.java │ │ ├── TurboFilterDecider.java │ │ ├── Utils.java │ │ ├── encoder/ │ │ │ └── PatternLayoutEncoder.java │ │ ├── functional/ │ │ │ ├── GetAppenderFunction.java │ │ │ ├── GetSiftedAppenderFunction.java │ │ │ └── RootLoggerSupplier.java │ │ └── sift/ │ │ ├── DiscriminatingMarker.java │ │ ├── DiscriminatingMarkerFactory.java │ │ ├── DiscriminatingValue.java │ │ └── MarkerBasedDiscriminator.java │ └── test/ │ ├── java/ │ │ └── com/ │ │ └── tersesystems/ │ │ └── logback/ │ │ └── classic/ │ │ ├── ChangeLogLevelTest.java │ │ ├── CorrelationIdMarker.java │ │ ├── CorrelationIdTurboFilter.java │ │ ├── EnabledFilterTest.java │ │ ├── ExceptionMessageConverterTest.java │ │ ├── SetLoggerLevelsActionTest.java │ │ ├── TapFilterTest.java │ │ ├── TerseHighlightConverterTest.java │ │ └── UtilsTest.java │ └── resources/ │ ├── logback-tapfilter-correlation.xml │ └── logback-tapfilter.xml ├── logback-compress-encoder/ │ ├── gradle.properties │ ├── logback-compress-encoder.gradle │ └── src/ │ ├── main/ │ │ └── java/ │ │ └── com.tersesystems.logback.compress/ │ │ ├── CompressingEncoder.java │ │ └── CompressingFileAppender.java │ └── test/ │ ├── java/ │ │ └── com/ │ │ └── tersesystems/ │ │ └── logback/ │ │ └── compress/ │ │ └── Utils.java │ └── resources/ │ └── logback-with-zstd-encoder.xml ├── logback-core/ │ ├── gradle.properties │ ├── logback-core.gradle │ └── src/ │ ├── main/ │ │ └── java/ │ │ └── com/ │ │ └── tersesystems/ │ │ └── logback/ │ │ └── core/ │ │ ├── AbstractAppender.java │ │ ├── Component.java │ │ ├── ComponentContainer.java │ │ ├── CompositeAppender.java │ │ ├── DecoratingAppender.java │ │ ├── DefaultAppenderAttachable.java │ │ ├── EnabledFilter.java │ │ ├── SelectAppender.java │ │ ├── encoder/ │ │ │ └── LayoutWrappingEncoder.java │ │ └── pattern/ │ │ └── PatternLayoutEncoderBase.java │ └── test/ │ ├── java/ │ │ └── com/ │ │ └── tersesystems/ │ │ └── logback/ │ │ └── core/ │ │ ├── CompositeAppenderTest.java │ │ ├── SelectAppenderTest.java │ │ └── TestAppender.java │ └── resources/ │ ├── logback-with-composite-appender.xml │ └── logback-with-select-appender.xml ├── logback-correlationid/ │ ├── gradle.properties │ ├── logback-correlationid.gradle │ └── src/ │ ├── main/ │ │ └── java/ │ │ └── com/ │ │ └── tersesystems/ │ │ └── logback/ │ │ └── correlationid/ │ │ ├── CorrelationIdDecider.java │ │ ├── CorrelationIdFilter.java │ │ ├── CorrelationIdMarker.java │ │ ├── CorrelationIdProvider.java │ │ ├── CorrelationIdTapFilter.java │ │ └── CorrelationIdUtils.java │ └── test/ │ ├── java/ │ │ └── com.tersesystems.logback.correlationid/ │ │ ├── CorrelationIdFilterTest.java │ │ └── CorrelationIdTapFilterTest.java │ └── resources/ │ ├── logback-correlationid-jdbc.xml │ ├── logback-correlationid-tapfilter.xml │ ├── logback-correlationid.xml │ └── spy.properties ├── logback-exception-mapping/ │ ├── gradle.properties │ ├── logback-exception-mapping.gradle │ └── src/ │ ├── main/ │ │ └── java/ │ │ └── com/ │ │ └── tersesystems/ │ │ └── logback/ │ │ └── exceptionmapping/ │ │ ├── BeanExceptionMapping.java │ │ ├── Constants.java │ │ ├── DefaultExceptionMappingRegistry.java │ │ ├── ExceptionCauseIterator.java │ │ ├── ExceptionHierarchyIterator.java │ │ ├── ExceptionMapping.java │ │ ├── ExceptionMappingAction.java │ │ ├── ExceptionMappingRegistry.java │ │ ├── ExceptionMappingRegistryAction.java │ │ ├── ExceptionMessageWithMappingsConverter.java │ │ ├── ExceptionProperty.java │ │ ├── FunctionExceptionMapping.java │ │ └── KeyValueExceptionProperty.java │ └── test/ │ ├── java/ │ │ └── com/ │ │ └── tersesystems/ │ │ └── logback/ │ │ └── exceptionmapping/ │ │ ├── ExceptionMappingTest.java │ │ ├── MyCustomException.java │ │ └── Thrower.java │ └── resources/ │ └── logback-test.xml ├── logback-exception-mapping-providers/ │ ├── gradle.properties │ ├── logback-exception-mapping-providers.gradle │ └── src/ │ ├── main/ │ │ └── java/ │ │ └── com/ │ │ └── tersesystems/ │ │ └── logback/ │ │ └── exceptionmapping/ │ │ ├── config/ │ │ │ └── TypesafeConfigMappingsAction.java │ │ └── json/ │ │ └── ExceptionArgumentsProvider.java │ └── test/ │ ├── java/ │ │ └── com/ │ │ └── tersesystems/ │ │ └── logback/ │ │ └── exceptionmapping/ │ │ └── json/ │ │ ├── ExceptionArgumentsProviderTest.java │ │ ├── MySpecialException.java │ │ └── TypesafeConfigMappingsActionTest.java │ └── resources/ │ ├── logback-with-exception-mapping.xml │ └── logback.conf ├── logback-honeycomb-appender/ │ ├── gradle.properties │ ├── logback-honeycomb-appender.gradle │ └── src/ │ └── main/ │ └── java/ │ └── com/ │ └── tersesystems/ │ └── logback/ │ └── honeycomb/ │ └── HoneycombAppender.java ├── logback-honeycomb-client/ │ ├── gradle.properties │ ├── logback-honeycomb-client.gradle │ └── src/ │ └── main/ │ └── java/ │ └── com/ │ └── tersesystems/ │ └── logback/ │ └── honeycomb/ │ └── client/ │ ├── HoneycombClient.java │ ├── HoneycombClientService.java │ ├── HoneycombHeaders.java │ ├── HoneycombRequest.java │ └── HoneycombResponse.java ├── logback-honeycomb-okhttp/ │ ├── gradle.properties │ ├── logback-honeycomb-okhttp.gradle │ └── src/ │ └── main/ │ ├── java/ │ │ └── com/ │ │ └── tersesystems/ │ │ └── logback/ │ │ └── honeycomb/ │ │ └── okhttp/ │ │ ├── HoneycombOkHTTPClient.java │ │ └── HoneycombOkHTTPClientService.java │ └── resources/ │ └── META-INF/ │ └── services/ │ └── com.tersesystems.logback.honeycomb.client.HoneycombClientService ├── logback-jdbc-appender/ │ ├── gradle.properties │ ├── logback-jdbc-appender.gradle │ └── src/ │ ├── main/ │ │ └── java/ │ │ └── com/ │ │ └── tersesystems/ │ │ └── logback/ │ │ └── jdbc/ │ │ └── JDBCAppender.java │ └── test/ │ ├── java/ │ │ └── com/ │ │ └── tersesystems/ │ │ └── logback/ │ │ └── jdbc/ │ │ └── JDBCAppenderTest.java │ └── resources/ │ ├── logback-reference.conf │ └── logback-test.xml ├── logback-postgresjson-appender/ │ ├── gradle.properties │ ├── logback-postgresjson-appender.gradle │ └── src/ │ ├── main/ │ │ └── java/ │ │ └── com/ │ │ └── tersesystems/ │ │ └── logback/ │ │ └── postgresjson/ │ │ └── PostgresJsonAppender.java │ └── test/ │ ├── java/ │ │ └── com/ │ │ └── tersesystems/ │ │ └── logback/ │ │ └── postgresjson/ │ │ └── PostgresJsonAppenderTest.java │ └── resources/ │ ├── db/ │ │ └── migration/ │ │ └── V1__logging_table.sql │ └── logback-postgres-json.xml ├── logback-tracing/ │ ├── gradle.properties │ ├── logback-tracing.gradle │ └── src/ │ └── main/ │ └── java/ │ └── com/ │ └── tersesystems/ │ └── logback/ │ └── tracing/ │ ├── EventInfo.java │ ├── EventMarkerFactory.java │ ├── LinkInfo.java │ ├── LinkMarkerFactory.java │ ├── Nullable.java │ ├── SpanInfo.java │ ├── SpanMarkerFactory.java │ └── Tracer.java ├── logback-turbomarker/ │ ├── gradle.properties │ ├── logback-turbomarker.gradle │ └── src/ │ ├── main/ │ │ └── java/ │ │ └── com/ │ │ └── tersesystems/ │ │ └── logback/ │ │ └── turbomarker/ │ │ ├── ContextAwareTurboFilterDecider.java │ │ ├── ContextAwareTurboMarker.java │ │ ├── ContextDecider.java │ │ ├── LoggerContextDecider.java │ │ ├── MarkerContextDecider.java │ │ ├── TurboMarker.java │ │ └── TurboMarkerTurboFilter.java │ └── test/ │ ├── java/ │ │ └── com/ │ │ └── tersesystems/ │ │ └── logback/ │ │ └── turbomarker/ │ │ ├── ApplicationContext.java │ │ ├── DiagnosticLoggingExample.java │ │ ├── LDMarkerFactory.java │ │ ├── LDMarkerTest.java │ │ ├── UserMarker.java │ │ ├── UserMarkerFactory.java │ │ └── UserMarkerTest.java │ └── resources/ │ └── logback-test.xml ├── logback-typesafe-config/ │ ├── gradle.properties │ ├── logback-typesafe-config.gradle │ └── src/ │ ├── main/ │ │ └── java/ │ │ └── com/ │ │ └── tersesystems/ │ │ └── logback/ │ │ └── typesafeconfig/ │ │ ├── ConfigConstants.java │ │ ├── ConfigConversion.java │ │ ├── ConfigListConverter.java │ │ └── TypesafeConfigAction.java │ └── test/ │ ├── java/ │ │ └── com/ │ │ └── tersesystems/ │ │ └── logback/ │ │ └── typesafeconfig/ │ │ ├── ConfigListConverterTest.java │ │ └── TypesafeConfigActionTest.java │ └── resources/ │ ├── logback-test.conf │ └── typesafeconfig/ │ ├── config-with-context.xml │ ├── config-with-default.xml │ └── config-with-local.xml ├── logback-uniqueid-appender/ │ ├── gradle.properties │ ├── logback-uniqueid-appender.gradle │ └── src/ │ ├── main/ │ │ └── java/ │ │ └── com/ │ │ └── tersesystems/ │ │ └── logback/ │ │ └── uniqueid/ │ │ ├── FlakeIdGenerator.java │ │ ├── IdGenerator.java │ │ ├── KsuidSubsecondIdGenerator.java │ │ ├── RandomUUIDIdGenerator.java │ │ ├── TsidIdgenerator.java │ │ ├── UlidIdGenerator.java │ │ ├── UniqueIdComponentAppender.java │ │ ├── UniqueIdConverter.java │ │ └── UniqueIdProvider.java │ └── test/ │ ├── java/ │ │ └── com/ │ │ └── tersesystems/ │ │ └── logback/ │ │ └── uniqueid/ │ │ └── UniqueIdAppenderTest.java │ └── resources/ │ └── logback-with-uniqueid-appender.xml ├── mkdocs.yml ├── settings.gradle └── version.properties ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Create a report to help us improve title: '' labels: '' assignees: '' --- **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' 4. See error **Expected behavior** A clear and concise description of what you expected to happen. **Screenshots** If applicable, add screenshots to help explain your problem. **Desktop (please complete the following information):** - OS: [e.g. iOS] - Browser [e.g. chrome, safari] - Version [e.g. 22] **Smartphone (please complete the following information):** - Device: [e.g. iPhone6] - OS: [e.g. iOS8.1] - Browser [e.g. stock browser, safari] - Version [e.g. 22] **Additional context** Add any other context about the problem here. ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature request about: Suggest an idea for this project title: '' labels: '' assignees: '' --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context or screenshots about the feature request here. ================================================ FILE: .gitignore ================================================ # Ignore Gradle project-specific cache directory .gradle logback-sigar/native/ # Ignore Gradle build output directory build target/ site/ .idea log/ out/ logback-example/log/* .classpath .project .settings .vscode/ bin/ *.iml *.ipr *.iws ================================================ FILE: .java-version ================================================ 1.8 ================================================ FILE: LICENSE ================================================ License ------- Written in 2019 by Will Sargent will@tersesystems.com To the extent possible under law, the author(s) have dedicated all copyright and related and neighboring rights to this software to the public domain worldwide. This software is distributed without any warranty. You should have received a copy of the CC0 Public Domain Dedication along with this software. If not, see ================================================ FILE: README.md ================================================ [![Maven Central](https://img.shields.io/maven-central/v/com.tersesystems.logback/logback-classic)](https://search.maven.org/search?q=g:com.tersesystems.logback) [![License CC0](https://img.shields.io/badge/license-CC0-blue.svg)](https://tldrlegal.com/license/creative-commons-cc0-1.0-universal) # Terse Logback Terse Logback is a collection of [Logback](https://logback.qos.ch/) extensions that shows how to use [Logback](https://logback.qos.ch/manual/index.html) effectively. Other logging projects you may be interested in: * [Blacklite](https://github.com/tersesystems/blacklite/), an SQLite appender with memory-mapping and zstandard dictionary compression that clocks around 800K statements per second. * [Blindsight](https://github.com/tersesystems/blindsight), a Scala logging API that extends SLF4J. * [Echopraxia](https://github.com/tersesystems/echopraxia), a Java and Scala logging API built around structured logging. ## Documentation Documentation is available at [https://tersesystems.github.io/terse-logback](https://tersesystems.github.io/terse-logback/1.0.3). ## Showcase There is a showcase project at [https://github.com/tersesystems/terse-logback-showcase](https://github.com/tersesystems/terse-logback-showcase). ## Modules - [Audio](https://tersesystems.github.io/terse-logback/guide/audio): Play audio when you log by attaching markers to your logging statements. - [Budgeting / Rate Limiting](https://tersesystems.github.io/terse-logback/guide/budget): Limit the amount of debugging or tracing statements in a time period. - [Censors](https://tersesystems.github.io/terse-logback/guide/censor): Censor sensitive information in logging statements. - [Composite](https://tersesystems.github.io/terse-logback/guide/composite): Presents a single appender that composes several appenders. - [Compression](https://tersesystems.github.io/terse-logback/guide/compression): Write to a compressed zstandard file. - [Correlation Id](https://tersesystems.github.io/terse-logback/guide/correlationid): Adds markers and filters for correlation id. - [Exception Mapping](https://tersesystems.github.io/terse-logback/guide/exception-mapping): Show the important details of an exception, including the root cause in a summary format. - [Instrumentation](https://tersesystems.github.io/terse-logback/guide/instrumentation): Decorates any (including JVM) class with enter and exit logging statements at runtime. - [JDBC](https://tersesystems.github.io/terse-logback/guide/jdbc): Use Postgres JSON to write structured logging to a single table. - [JUL to SLF4J Bridge](https://tersesystems.github.io/terse-logback/guide/slf4jbridge): Configure java.util.logging to write to SLF4J with no [manual coding](https://mkyong.com/logging/how-to-load-logging-properties-for-java-util-logging/). - [Relative Nanos](https://tersesystems.github.io/terse-logback/guide/relativens): Composes a logging event to contain relative nanoseconds based off `System.nanoTime`. - [Select Appender](https://tersesystems.github.io/terse-logback/guide/select): Appender that selects an appender from a list based on key. - [Tracing](https://tersesystems.github.io/terse-logback/guide/tracing): Sends logging events and traces to [Honeycomb Event API](https://docs.honeycomb.io/api/events/). - [Typesafe Config](https://tersesystems.github.io/terse-logback/guide/typesafeconfig): Configure Logback properties using [HOCON](https://github.com/lightbend/config/blob/main/HOCON.md). - [Turbo Markers](https://tersesystems.github.io/terse-logback/guide/turbomarker): [Turbo Filters](https://logback.qos.ch/manual/filters.html#TurboFilter) that depend on arbitrary deciders that can log at debug level for sessions. - [Unique ID Appender](https://tersesystems.github.io/terse-logback/guide/uniqueid): Composes logging event to contain a unique id across multiple appenders. ================================================ FILE: RELEASING.md ================================================ ## Release To make sure everything works: ```bash ./gradlew clean build check ``` To format everything using [Spotless](https://github.com/diffplug/spotless/tree/master/plugin-gradle): ```bash ./gradlew spotlessApply ``` First, try publishing to maven local: ```bash ./gradlew publishToMavenLocal ``` If that works, then publish to Sonatype's staging repository and close: ```bash ./gradlew publishToSonatype closeSonatypeStagingRepository ``` Inspect this in Sonatype OHSSH repository. Delete the staging repository after inspection. And then to promote it: ```bash ./gradlew publishToSonatype closeAndReleaseSonatypeStagingRepository ``` 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). ## Gradle Signing If you run into errors with signing doing a `publishToSonaType`, this is common and underdocumented. ``` No value has been specified for property 'signatory.keyId'. ``` For the `signatory.keyId` error message, you need to set `signing.gnupg.keyName` if you are using GPG 2.1 and a Yubikey 4. https://docs.gradle.org/current/userguide/signing_plugin.html#sec:signatory_credentials https://github.com/gradle/gradle/pull/1703/files#diff-6c52391bbdceb4cca64ce7b03e78212fR6 Note you need to use `gpg -K` and pick only the LAST EIGHT CHARS of the public signing key. > signing.gnupg.keyName = 5F798D53 ### PinEntry Also note that if you are using a Yubikey, it'll require you to type in a PIN, which screws up Gradle. ``` gpg: signing failed: No pinentry ``` So you need to use pinentry-mode loopback, which is helpfully supplied by passphrase. - https://github.com/sbt/sbt-pgp/pull/142 - https://wiki.archlinux.org/index.php/GnuPG#Unattended_passphrase - https://github.com/gradle/gradle/pull/1703/files#diff-790036df959521791fdafe474b673924 You want this specified only the command line, i.e. > $ HISTCONTROL=ignoreboth ./gradlew publishToMavenLocal -Psigning.gnupg.passphrase=$PGP_PASSPHRASE --info ### Cannot Allocate Memory gpg can't be run in parallel. You'll get this error message. ``` gpg: signing failed: Cannot allocate memory ``` [Gradle is not smart enough to disable this](https://github.com/gradle/gradle/issues/12167). Do not use `-Porg.gradle.parallel=false` and don't use `--parallel` when publishing. ## Documentation Documentation is done with [gradle-mkdocs-plugin](https://xvik.github.io/gradle-mkdocs-plugin/) and works best on Linux. Need to have [Python 3.8](https://tech.serhatteker.com/post/2019-12/upgrade-python38-on-ubuntu/), virtualenv is not enough. To see documentation: ```bash ./gradlew mkdocsServe --no-daemon ``` To deploy documentation: ```bash ./gradlew mkdocsPublish ``` ================================================ FILE: build.gradle ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ plugins { id 'java' id "com.github.hierynomus.license" version "0.15.0" id "com.diffplug.spotless" version "6.11.0" id 'ru.vyarus.use-python' version '3.0.0' id 'ru.vyarus.mkdocs' version '2.4.0' id "io.github.gradle-nexus.publish-plugin" version "1.1.0" id "org.shipkit.shipkit-auto-version" version "1.1.19" //id 'org.inferred.processors' version '2.3.0' } apply from: "gradle/release.gradle" mkdocs { sourcesDir = projectDir strict = true } python { minPythonVersion = '3.7' // mkdocs requires 3.7.x scope = VIRTUALENV } spotless { freshmark { target 'README.md' propertiesFile('gradle.properties') propertiesFile('version.properties') } java { googleJavaFormat() } } allprojects { repositories { mavenCentral() } } // Root project shouldn't publish tasks.withType(PublishToMavenRepository).configureEach { it.enabled = false } subprojects { subproj -> apply plugin: 'java' apply plugin: 'com.diffplug.spotless' spotless { java { googleJavaFormat() } } java { toolchain { languageVersion = JavaLanguageVersion.of(8) } } dependencies { testImplementation 'org.apiguardian:apiguardian-api:1.1.0' testImplementation 'org.assertj:assertj-core:3.13.2' testImplementation "junit:junit:$junitVersion" testImplementation "org.junit.jupiter:junit-jupiter-api:$junitJupiterVersion" testImplementation "org.junit.jupiter:junit-jupiter-engine:$junitJupiterVersion" testImplementation "org.junit.vintage:junit-vintage-engine:$junitVintageVersion" //testImplementation group: 'org.junit.platform', name: 'junit-platform-runner', version: '1.5.0' } test { useJUnitPlatform() } } // Go through all the artifacts and find javadoc for it... static List javadocFromDependencies(Configuration config) { List javadocs = [] config.dependencies.each { dep -> javadocs.add(artifactToJavadoc(dep.group, dep.name, dep.version)) } javadocs } static String jvmToJavadoc(JavaVersion jvmVersion) { if (jvmVersion.java8) { 'https://docs.oracle.com/javase/8/docs/api/' } else if (jvmVersion.java9) { 'https://docs.oracle.com/javase/9/docs/api/' }else if (jvmVersion.java10) { 'https://docs.oracle.com/javase/10/docs/api/' }else if (jvmVersion.java11) { 'https://docs.oracle.com/en/java/javase/11/docs/api/' } else { 'https://docs.oracle.com/javase/8/docs/api/' } } static String artifactToJavadoc(String organization, String name, String apiVersion) { String slashedOrg = organization.replace('.', '/') "https://oss.sonatype.org/service/local/repositories/releases/archive/$slashedOrg/$name/$apiVersion/$name-$apiVersion-javadoc.jar/!/" } ================================================ FILE: docs/guide/audio.md ================================================ # Audio The audio appender uses a system beep configured through `SystemPlayer` to notify on warnings and errors, and limits excessive beeps with a budget evaluator. ## Installation Add the library dependency using [com.tersesystems.logback:logback-audio](https://mvnrepository.com/artifact/com.tersesystems.logback/logback-audio). ## Usage The XML is as follows: ```xml WARN NEUTRAL DENY ERROR ACCEPT DENY DENY NEUTRAL ``` See [Application Logging in Java: Appenders]( https://tersesystems.com/blog/2019/05/27/application-logging-in-java-part-5/) for more details. ================================================ FILE: docs/guide/budget.md ================================================ # Budget Aware Logging There are instances where logging may be overly chatty, and will log more than necessary. Rather than hunt down all the individual loggers and whitelist or blacklist the lot of them, you can assign a budget that will budget INFO messages to 5 statements a second. This is easy to do with the `logback-budget` module, which uses an internal [circuit breaker](https://commons.apache.org/proper/commons-lang/apidocs/org/apache/commons/lang3/concurrent/CircuitBreaker.html) to regulate the flow of messages. ## Installation Add the library dependency using [com.tersesystems.logback:logback-budget](https://mvnrepository.com/artifact/com.tersesystems.logback/logback-budget). ## Usage The time unit corresponds to the text value of `java.util.concurrent.TimeUnit` i.e. `nanoseconds`, `microseconds`, `milliseconds`, `seconds`, `minutes`, `hours`, `days`, case-insensitive. ```xml INFO 5 1 seconds DENY NEUTRAL %-5relative %-5level %logger{35} - %msg%n ``` ## Turbo Filter You can also apply the budget rule as a turbo filter if you want to have the rule apply across all appenders, using `com.tersesystems.logback.budget.BudgetTurboFilter`. ```xml INFO 5 1 second DENY NEUTRAL %-5relative %-5level %logger{35} - %msg%n ``` ================================================ FILE: docs/guide/censor.md ================================================ # Censors There may be sensitive information that you don't want to show up in the logs. You can get around this by passing your information through a censor. This is a custom bit of code written for Logback, but it's not too complex. There are two rules and a converter that are used in Logback to define and reference censors: `CensorAction`, `CensorRefAction` and the `censor` converter. ## Installation Add the library dependency using [com.tersesystems.logback:logback-censor](https://mvnrepository.com/artifact/com.tersesystems.logback/logback-censor). ## Usage ```xml ``` The `CensorAction` defines a censor that can be referred to by the `CensorRef` action and the `censor` conversionWord, using the censor name. The default implementation is the regex censor, which will look for a regular expression and replace it with the replacement text defined: ```xml [CENSORED BY CENSOR1] hunter1 [CENSORED BY CENSOR2] hunter2 ``` Once you have the censors defined, you can use the censor word by specifying the target as defined in the [pattern encoder format](https://logback.qos.ch/manual/layouts.html#conversionWord), and adding the name as the option list using curly braces, i.e. `%censor(%msg){censor-name1}`. If you don't define the censor, then the first available censor will be picked. ```xml file1.log %censor(%msg){censor-name1}%n file2.log %censor(%msg){censor-name2}%n ``` If you are working with a componentized framework, you'll want to use the `censor-ref` action instead. Here's an example using logstash-logback-encoder. ```xml file3.log ``` In this case, `CensoringJsonGeneratorDecorator` implements the `CensorAttachable` interface and so will run message text through the censor if it exists. See [Application Logging in Java: Converters](https://tersesystems.com/blog/2019/05/11/application-logging-in-java-part-3/) for more details. ================================================ FILE: docs/guide/composite.md ================================================ # Composite Appender The composite appender presents a single appender and appends to several appenders. It is very useful for referring to a list of appenders by a single name. ## Installation Add the library dependency using [com.tersesystems.logback:logback-core](https://mvnrepository.com/artifact/com.tersesystems.logback/logback-core). ## Usage ```xml ``` You can leverage nesting to keep your filtering logic under control. For example, you may want to have several things happen when you hit an error in your logs. Appenders will always write when they receive an event, unless they are filtered. Using nesting, you can declare the filter once, and have the child appenders "inherit" that filter: ERROR ACCEPT DENY error.log %date - %message /error.ogg This makes your appender logic much cleaner. See [Application Logging in Java: Appenders](https://tersesystems.com/blog/2019/05/27/application-logging-in-java-part-5/) for more details. ================================================ FILE: docs/guide/compression.md ================================================ # Compression Encoders are powerful and useful. They give you access to the raw bytes, and let you manipulate them before they get to an appender. But you'll have to put them together inside an appender if you want to do byte transformation. ## Installation Add the library dependency using [com.tersesystems.logback:logback-compress-encoder](https://mvnrepository.com/artifact/com.tersesystems.logback/logback-compress-encoder). ## Usage As an example, say that we want to write out files directly in [zstandard](http://facebook.github.io/zstd/) or [brotli](https://en.wikipedia.org/wiki/Brotli) using Logback. The easiest way to do this is to provide a `FileAppender` with a swapped out compression encoder, while presenting a public API that looks just like a regular encoder. Here's the appender as `logback.xml` sees it: ```xml encoded.zst zstd 1024000 UTF-8 %-5level %logger{35} - %msg%n ``` Under the hood, `CompressingFileAppender` delegates to a regular file appender, but uses [commons-compress](https://commons.apache.org/proper/commons-compress/) and a `CompressingEncoder` to wrap `PatternLayoutEncoder`: ```java public class CompressingFileAppender extends UnsynchronizedAppenderBase { // ... @Override public void start() { fileAppender = new FileAppender<>(); fileAppender.setContext(getContext()); fileAppender.setFile(getFile()); fileAppender.setImmediateFlush(false); fileAppender.setPrudent(isPrudent()); fileAppender.setAppend(isAppend()); fileAppender.setName(name+"-embedded-file"); CompressingEncoder compressedEncoder = createCompressingEncoder(getEncoder()); fileAppender.setEncoder(compressedEncoder); fileAppender.start(); super.start(); } public void stop() { fileAppender.stop(); super.stop(); } @Override protected void append(E eventObject) { fileAppender.doAppend(eventObject); } protected CompressingEncoder createCompressingEncoder(Encoder e) { int bufferSize = getBufferSize(); String compressAlgo = getCompressAlgo(); CompressorStreamFactory factory = CompressorStreamFactory.getSingleton(); Set names = factory.getOutputStreamCompressorNames(); if (names.contains(getCompressAlgo())) { try { return new CompressingEncoder<>(e, compressAlgo, factory, bufferSize); } catch (CompressorException ex) { throw new RuntimeException("Cannot create CompressingEncoder", ex); } } else { throw new RuntimeException("No such compression algorithm: " + compressAlgo); } } } ``` From there, the encoder will shove all the input bytes into a compressed stream until there's enough data to make compression worthwhile, and then flush the compressed bytes out through a byte array output stream: ```java public class CompressingEncoder extends EncoderBase { private final Accumulator accumulator; private final Encoder encoder; public CompressingEncoder(Encoder encoder, String compressAlgo, CompressorStreamFactory factory, int bufferSize) throws CompressorException { this.encoder = encoder; this.accumulator = new Accumulator(compressAlgo, factory, bufferSize); } @Override public byte[] headerBytes() { try { return accumulator.apply(encoder.headerBytes()); } catch (IOException e) { throw new RuntimeException(e); } } @Override public byte[] encode(E event) { try { return accumulator.apply(encoder.encode(event)); } catch (IOException e) { throw new RuntimeException(e); } } @Override public byte[] footerBytes() { try { return accumulator.drain(encoder.footerBytes()); } catch (IOException e) { throw new RuntimeException(e); } } static class Accumulator { private final ByteArrayOutputStream byteOutputStream; private final CompressorOutputStream stream; private final LongAdder count = new LongAdder(); private final int bufferSize; public Accumulator(String compressAlgo, CompressorStreamFactory factory, int bufferSize) throws CompressorException { this.bufferSize = bufferSize; this.byteOutputStream = new ByteArrayOutputStream(); this.stream = factory.createCompressorOutputStream(compressAlgo, byteOutputStream); } boolean isFlushable() { return count.intValue() >= bufferSize; } byte[] apply(byte[] bytes) throws IOException { count.add(bytes.length); stream.write(bytes); if (isFlushable()) { stream.flush(); byte[] output = byteOutputStream.toByteArray(); byteOutputStream.reset(); count.reset(); return output; } else { return new byte[0]; } } byte[] drain(byte[] inputBytes) throws IOException { if (inputBytes != null) { stream.write(inputBytes); } stream.close(); count.reset(); return byteOutputStream.toByteArray(); } } } ``` This keeps both `FileAppender` and `PatternLayoutEncoder` happy, while feeding compressed bytes as the stream. Using delegation is generally much easier than trying to extend from `FileAppender`, because `FileAppender` has very definite ideas about what kind of output stream it is using, and has all the logic of file rotation and backups encorporated into it, including its own gzip compression scheme for rotated files. You can also extend this to add [dictionary support](https://facebook.github.io/zstd/#small-data) for ZStandard, and that would remove the need for a buffer to provide effective compression. This does come with the downside of needing to pass the dictionary out of band though. See [Application Logging in Java: Encoders](https://tersesystems.com/blog/2019/06/09/application-logging-in-java-part-7/) for more details. ================================================ FILE: docs/guide/correlationid.md ================================================ # Correlation ID The `logback-correlationid` module is a set of classes designed to encompass the idea of a correlation id in events. ## Installation Add the library dependency using [com.tersesystems.logback:logback-correlationid](https://mvnrepository.com/artifact/com.tersesystems.logback/logback-correlationid). ## Usage It consists of a correlation id filter, a tap filter that always logs events with a correlation id to an appender, and a correlation id marker. ### Correlation ID Filter A correlation id filter will filter for a correlation id set either as an MDC value, or as a marker created from `CorrelationIdMarker`. ```xml correlationId ``` If an appender passes the filter, it will log the event. ```java public class CorrelationIdFilterTest { public void testFilter() { // Write something that never gets logged explicitly... Logger logger = loggerFactory.getLogger("com.example.Debug"); String correlationId = "12345"; CorrelationIdMarker correlationIdMarker = CorrelationIdMarker.create(correlationId); // should be logged because marker logger.info(correlationIdMarker, "info one"); logger.info("info two"); // should not be logged // Everything below this point should be logged. MDC.put("correlationId", correlationId); logger.info("info three"); // should not be logged logger.info(correlationIdMarker, "info four"); } } ``` ### CorrelationIdTapFilter The `CorrelationIdTapFilter` is a turbofilter that always logs to a given appender if the correlation id appears, even if the appender is not configured for logging. This functions as a wiretap. Tap Filters are very useful as a way to send data to an appender. They completely bypass any kind of logging level configured on the front end, so you can set a logger to INFO level but still have access to all TRACE events when an error occurs, through the tap filter's appenders. For example, a tap filter can automatically log everything with a correlation id at a TRACE level, without requiring filters or altering the log level as a whole. Let's run a simple HTTP client program that calls out to Google and prints a result. ```xml correlationId %-5relative %-5level %logger{35} - %msg%n ``` ### CorrelationIdMarker A `CorrelationIdMarker` implements the `CorrelationIdProvider` interface to expose a marker which is known to contain a correlation id. ```java CorrelationIdMarker correlationIdMarker = CorrelationIdMarker.create(correlationId); String sameId = correlationIdMarker.getCorrelationId(); ``` ### CorrelationIdUtils `CorrelationIdUtils` contains utility methods like `get` which retrieve a correlation id from either a marker or MDC. ================================================ FILE: docs/guide/exception-mapping.md ================================================ # Exception Mapping Exception Mapping is done to show the important details of an exception, including the root cause in a summary format. This is especially useful in line oriented formats, because rendering a stacktrace can take up screen real estate without providing much value. ## Installation Add the library dependency using [com.tersesystems.logback:logback-exception-mapping](https://mvnrepository.com/artifact/com.tersesystems.logback/logback-exception-mapping) and [com.tersesystems.logback:logback-exception-mapping-providers](https://mvnrepository.com/artifact/com.tersesystems.logback/logback-exception-mapping-providers). ## Usage Given the following program: ```java public class Thrower { private static final Logger logger = LoggerFactory.getLogger(Thrower.class); public static void main(String[] progArgs) { try { doSomethingExceptional(); } catch (RuntimeException e) { logger.error("domain specific message", e); } } static void doSomethingExceptional() { Throwable cause = new BatchUpdateException(); throw new MyCustomException("This is my message", "one is one", "two is more than one", "three is more than two and one", cause); } } public class MyCustomException extends RuntimeException { public MyCustomException(String message, String one, String two, String three, Throwable cause) { // ... } public String getOne() { return one; } public String getTwo() { return two; } public String getThree() { return three; } } ``` and the Logback file: ```xml %-5relative %-5level %logger{35} - %msg%richex{1, 10, exception=[}%n ``` Then this renders the following: ``` 184 ERROR c.t.l.exceptionmapping.Thrower - domain specific message exception=[com.tersesystems.logback.exceptionmapping.MyCustomException(one="one is one" two="two is more than one" three="three is more than two and one" message="This is my message") > java.sql.BatchUpdateException(updateCounts="null" errorCode="0" SQLState="null" message="null")] ``` You can integrate exception mapping with Typesafe Config and `logstash-logback-encoder` by adding extra mappings. For example, you can map a whole bunch of exceptions at once in HOCON, and not have to do it line by line in XML: ```xml ``` and ```hocon exceptionmappings { example.MySpecialException: ["timestamp"] } ``` and configure it in JSON using `ExceptionArgumentsProvider`: ```xml exception ``` and get the following `exception` that contains an array of exceptions and the associated properties, in this case `timestamp`: ```json { "id" : "Fa6x8H0EqomdHaINzdiAAA", "sequence" : 3, "@timestamp" : "2019-07-06T03:52:48.730+00:00", "@version" : "1", "message" : "I am an error", "logger_name" : "example.Main$Runner", "thread_name" : "pool-1-thread-1", "level" : "ERROR", "stack_hash" : "233f3cf1", "exception" : [ { "name" : "example.MySpecialException", "properties" : { "message" : "Level 1", "timestamp" : "2019-07-06T03:52:48.728Z" } }, { "name" : "example.MySpecialException", "properties" : { "message" : "Level 2", "timestamp" : "2019-07-06T03:52:48.728Z" } }, { "name" : "example.MySpecialException", "properties" : { "message" : "Level 3", "timestamp" : "2019-07-06T03:52:48.728Z" } }, { "name" : "example.MySpecialException", "properties" : { "message" : "Level 4", "timestamp" : "2019-07-06T03:52:48.728Z" } }, { "name" : "example.MySpecialException", "properties" : { "message" : "Level 5", "timestamp" : "2019-07-06T03:52:48.728Z" } }, { "name" : "example.MySpecialException", "properties" : { "message" : "Level 6", "timestamp" : "2019-07-06T03:52:48.728Z" } }, { "name" : "example.MySpecialException", "properties" : { "message" : "Level 7", "timestamp" : "2019-07-06T03:52:48.728Z" } }, { "name" : "example.MySpecialException", "properties" : { "message" : "Level 8", "timestamp" : "2019-07-06T03:52:48.728Z" } }, { "name" : "example.MySpecialException", "properties" : { "message" : "Level 9", "timestamp" : "2019-07-06T03:52:48.728Z" } } ], "stack_trace" : "<#1165e3b1> example.MySpecialException: Level 9\n\tat example.Main$Runner.nestException(Main.java:56)\n\t... 9 common frames omitted\nWrapped by: <#eb336a2d> example.MySpecialException: Level 8\n\tat example.Main$Runner.nestException(Main.java:56)\n\t... 10 common frames omitted\nWrapped by: <#cc1fb404> example.MySpecialException: Level 7\n\tat example.Main$Runner.nestException(Main.java:56)\n\t... 11 common frames omitted\nWrapped by: <#2af187a0> example.MySpecialException: Level 6\n\tat example.Main$Runner.nestException(Main.java:56)\n\t... 12 common frames omitted\nWrapped by: <#7dac62d1> example.MySpecialException: Level 5\n\tat example.Main$Runner.nestException(Main.java:56)\n\t... 13 common frames omitted\nWrapped by: <#2ea4460d> example.MySpecialException: Level 4\n\tat example.Main$Runner.nestException(Main.java:56)\n\t... 14 common frames omitted\nWrapped by: <#261bed64> example.MySpecialException: Level 3\n\tat example.Main$Runner.nestException(Main.java:56)\n\t... 15 common frames omitted\nWrapped by: <#e660d440> example.MySpecialException: Level 2\n\tat example.Main$Runner.nestException(Main.java:56)\n\t... 16 common frames omitted\nWrapped by: <#233f3cf1> example.MySpecialException: Level 1\n\tat example.Main$Runner.nestException(Main.java:56)\n\tat example.Main$Runner.nestException(Main.java:57)\n\tat example.Main$Runner.nestException(Main.java:57)\n\tat example.Main$Runner.nestException(Main.java:57)\n\tat example.Main$Runner.nestException(Main.java:57)\n\tat example.Main$Runner.nestException(Main.java:57)\n\tat example.Main$Runner.nestException(Main.java:57)\n\tat example.Main$Runner.nestException(Main.java:57)\n\tat example.Main$Runner.nestException(Main.java:57)\n\tat example.Main$Runner.generateException(Main.java:51)\n\tat example.Main$Runner.doError(Main.java:44)\n\tat java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)\n\tat java.util.concurrent.FutureTask.runAndReset(FutureTask.java:308)\n\tat java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$301(ScheduledThreadPoolExecutor.java:180)\n\tat java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:294)\n\tat java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)\n\tat java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)\n\tat java.lang.Thread.run(Thread.java:748)\n" } ``` This is a lot easier for structured logging parsers to grok than the associated stacktrace. See [How to Log an Exception](https://tersesystems.com/blog/2019/06/29/how-to-log-an-exception/) and [How to Log an Exception, Part 2](https://tersesystems.com/blog/2019/07/06/how-to-log-an-exception-part-2/) for more details. ================================================ FILE: docs/guide/instrumentation.md ================================================ # Instrumentation If you have library code that doesn't pass around `ILoggerFactory` and doesn't let you add information to logging, then you can get around this by instrumenting the code with [Byte Buddy](https://bytebuddy.net/). Using Byte Buddy, you can do fun things like override `Security.setSystemManager` with [your own implementation](https://tersesystems.com/blog/2016/01/19/redefining-java-dot-lang-dot-system/), so using Byte Buddy to decorate code with `enter` and `exit` logging statements is relatively straightforward. Instrumentation is configuration driven and simple. Instead of debugging using printf statements and recompiling or stepping through a debugger, you can just add lines to a config file. I like this approach better than the annotation or aspect-oriented programming approaches, because it is completely transparent to the code and gives roughly the same performance as inline code, adding [130 ns/op](https://github.com/raphw/byte-buddy/issues/714) by calling `class.getMethod`. A major advantage of instrumentation is that because it logs `throwing` exceptions in instrumented code, you can log exceptions that would be swallowed by the caller. For example, imagine that a library has the following method: ```java public class Foo { public void throwException() throws Exception { throw new PlumException("I am sweet and cold"); } public void swallowException() { try { throwException(); } catch (Exception e) { // forgive me, the exception was delicious } } } ``` By instrumenting the `throwException` method, you can see the logged exception at runtime when `swallowException` is called. See [Application Logging in Java: Tracing 3rd Party Code](https://tersesystems.com/blog/2019/06/11/application-logging-in-java-part-8/) and [Hierarchical Instrumented Tracing with Logback](https://tersesystems.com/blog/2019/09/15/hierarchical-instrumented-tracing-with-logback/) for more details. ## Installation You'll need to install [com.tersesystems.logback:logback-bytebuddy](https://mvnrepository.com/artifact/com.tersesystems.logback/logback-bytebuddy) and [com.tersesystems.logback:logback-tracing](https://mvnrepository.com/artifact/com.tersesystems.logback/logback-tracing). You should also install [byte-buddy](https://mvnrepository.com/artifact/net.bytebuddy/byte-buddy). ``` implementation group: 'com.tersesystems.logback', name: 'logback-classic', version: 'LATEST' implementation group: 'com.tersesystems.logback', name: 'logback-bytebuddy', version: 'LATEST' implementation group: 'com.tersesystems.logback', name: 'logback-tracing', version: 'LATEST' implementation group: 'net.bytebuddy', name: 'byte-buddy', version: 'LATEST' ``` There are two ways you can install instrumentation -- you can do it using an agent, or you can do it manually. > NOTE: Because Byte Buddy must inspect each class on JVM initialization, it will have a (generally small) impact on the start up time of your application. ### Agent Installation Using the agent is generally easier (less code) and more powerful (can change JDK classes), but it does require some explicit command line options. First, you set the java agent, either directly on the command line: ```bash java \ -javaagent:path/to/logback-bytebuddy-x.x.x.jar=debug \ -Dterse.logback.configurationFile=conf/logback.conf \ -Dlogback.configurationFile=conf/logback-test.xml \ com.example.PreloadedInstrumentationExample ``` or by using the [`JAVA_TOOLS_OPTIONS` environment variable](https://docs.oracle.com/javase/8/docs/technotes/guides/troubleshoot/envvars002.html). ```bash export JAVA_TOOLS_OPTIONS="..." ``` Generally you'll be setting up these options in a build system. There are example projects in Gradle and sbt set up with agent-based instrumentation at [https://github.com/tersesystems/logging-instrumentation-example](https://github.com/tersesystems/logging-instrumentation-example). ### Manual Installation You also have the option of installing the agent manually. The in process instrumentation is done with `com.tersesystems.logback.bytebuddy.LoggingInstrumentationByteBuddyBuilder`, which takes in some configuration and then installs itself on the byte buddy agent. ```java new LoggingInstrumentationByteBuddyBuilder() .builderFromConfig(loggingInstrumentationAdviceConfig) .with(debugListener) .installOnByteBuddyAgent(); ``` ## Configuration There are two parts to seeing tracing logs with instrumentation -- indicating the classes and methods you want instrumented, and then setting those loggers to TRACE. ### Setting Instrumented Classes and Methods The instrumentation is configured using [HOCON](https://github.com/lightbend/config/blob/main/HOCON.md) in a `logback.conf` file in `src/main/resources`. Settings are under the `logback.bytebuddy` section. The `tracing` section contains a mapping of class names and methods, or the wildcard "*" to indicate all methods. ``` logback.bytebuddy { service-name = "my-service" tracing { "fully.qualified.class.Name" = ["method1", "method2"] "play.api.mvc.ActionBuilder" = ["*"] } } ``` NOTE: There are some limitations to what you can trace. You can only instrument JDK classes when using the agent, and you cannot instrument native methods like `java.lang.System.currentTimeMillis()` for example. ### Setting Loggers to TRACE Because instrumentation inserts `logger.trace` calls into the code, you must enable logging at `TRACE` level for those loggers to see output. Setting the level from `logback.xml` works fine: ```xml ``` If you are using the [Config](typesafeconfig.md) module, you can also do this from `logback.conf`: ```hocon levels { fully.qualified.class.Name = TRACE play.api.mvc.ActionBuilder = TRACE } ``` Or you can use `ChangeLogLevel` at run time. ## Examples Instrumentation is a tool that can be hard to explain, so here's some use cases showing how you can quickly instrument your code. Also don't forget the example projects at [https://github.com/tersesystems/logging-instrumentation-example](https://github.com/tersesystems/logging-instrumentation-example). ### Instrumenting java.lang.Thread Assuming an agent based instrumentation, in `logback.conf`: ```hocon levels { java.lang.Thread = TRACE } logback.bytebuddy { service-name = "some-service" tracing { "java.lang.Thread" = [ "run" ] } } ``` and the code as follows: ```java public class PreloadedInstrumentationExample { public static void main(String[] args) throws Exception { Thread thread = Thread.currentThread(); thread.run(); } } ``` yields ```text [Byte Buddy] DISCOVERY java.lang.Thread [null, null, loaded=true] [Byte Buddy] TRANSFORM java.lang.Thread [null, null, loaded=true] [Byte Buddy] COMPLETE java.lang.Thread [null, null, loaded=true] 92 TRACE java.lang.Thread - entering: java.lang.Thread.run() with arguments=[] 93 TRACE java.lang.Thread - exiting: java.lang.Thread.run() with arguments=[] => returnType=void ``` ### Instrumenting javax.net.ssl.SSLContext This is especially helpful when you're trying to debug SSL issues: ```hocon levels { sun.security.ssl = TRACE javax.net.ssl = TRACE } logback.bytebuddy { service-name = "some-service" tracing { "javax.net.ssl.SSLContext" = ["*"] } } ``` will result in: ``` FcJ3XfsdKnM6O0Qbm7EAAA 12:31:55.498 [TRACE] j.n.s.SSLContext - entering: javax.net.ssl.SSLContext.getInstance(java.lang.String) with arguments=[TLS] from source SSLContext.java:155 FcJ3XfsdKng6O0Qbm7EAAA 12:31:55.503 [TRACE] j.n.s.SSLContext - exiting: javax.net.ssl.SSLContext.getInstance(java.lang.String) with arguments=[TLS] => returnType=javax.net.ssl.SSLContext from source SSLContext.java:157 FcJ3XfsdKng6O0Qbm7EAAB 12:31:55.504 [TRACE] j.n.s.SSLContext - entering: javax.net.ssl.SSLContext.init([Ljavax.net.ssl.KeyManager;,[Ljavax.net.ssl.TrustManager;,java.security.SecureRandom) with arguments=[[org.postgresql.ssl.LazyKeyManager@27a97e08], [org.postgresql.ssl.NonValidatingFactory$NonValidatingTM@5918c260], null] from source SSLContext.java:282 FcJ3XfsdKnk6O0Qbm7EAAA 12:31:55.504 [TRACE] j.n.s.SSLContext - exiting: javax.net.ssl.SSLContext.init([Ljavax.net.ssl.KeyManager;,[Ljavax.net.ssl.TrustManager;,java.security.SecureRandom) with arguments=[[org.postgresql.ssl.LazyKeyManager@27a97e08], [org.postgresql.ssl.NonValidatingFactory$NonValidatingTM@5918c260], null] => returnType=void from source SSLContext.java:283 ``` Be warned that JSSE can be extremely verbose in its `toString` output. ### Instrumenting ClassCalledByAgent If you are already developing an agent, or want finer grained control over Byte Buddy, you can create the agent in process and inspect how Byte Buddy works. This is an advanced use case, but it's useful to get familiar. With the following code: ```java public class ClassCalledByAgent { public void printStatement() { System.out.println("I am a simple println method with no logging"); } public void printArgument(String arg) { System.out.println("I am a simple println, printing " + arg); } public void throwException(String arg) { throw new RuntimeException("I'm a squirrel!"); } } ``` And the following configuration in `logback.conf`: ```hocon logback.bytebuddy { service-name = "example-service" tracing { "com.tersesystems.logback.bytebuddy.ClassCalledByAgent" = [ "printStatement", "printArgument", "throwException", ] } } ``` and have `com.tersesystems.logback.bytebuddy.ClassCalledByAgent` logging level set to `TRACE` in `logback.xml`. We can start up the agent, add in the builder and run through the methods: ```java public class InProcessInstrumentationExample { public static AgentBuilder.Listener createDebugListener(List classNames) { return new AgentBuilder.Listener.Filtering( LoggingInstrumentationAdvice.stringMatcher(classNames), AgentBuilder.Listener.StreamWriting.toSystemOut()); } public static void main(String[] args) throws Exception { // Helps if you install the byte buddy agents before anything else at all happens... ByteBuddyAgent.install(); Logger logger = LoggerFactory.getLogger(InProcessInstrumentationExample.class); SystemFlow.setLoggerResolver(new FixedLoggerResolver(logger)); Config config = LoggingInstrumentationAdvice.generateConfig(ClassLoader.getSystemClassLoader(), false); LoggingInstrumentationAdviceConfig adviceConfig = LoggingInstrumentationAdvice.generateAdviceConfig(config); // The debugging listener shows what classes are being picked up by the instrumentation Listener debugListener = createDebugListener(adviceConfig.classNames()); new LoggingInstrumentationByteBuddyBuilder() .builderFromConfig(adviceConfig) .with(debugListener) .installOnByteBuddyAgent(); // No code change necessary here, you can wrap completely in the agent... ClassCalledByAgent classCalledByAgent = new ClassCalledByAgent(); classCalledByAgent.printStatement(); classCalledByAgent.printArgument("42"); try { classCalledByAgent.throwException("hello world"); } catch (Exception e) { // I am too lazy to catch this exception. I hope someone does it for me. } } } ``` And get the following: ```text [Byte Buddy] DISCOVERY com.tersesystems.logback.bytebuddy.ClassCalledByAgent [sun.misc.Launcher$AppClassLoader@75b84c92, null, loaded=true] [Byte Buddy] TRANSFORM com.tersesystems.logback.bytebuddy.ClassCalledByAgent [sun.misc.Launcher$AppClassLoader@75b84c92, null, loaded=true] [Byte Buddy] COMPLETE com.tersesystems.logback.bytebuddy.ClassCalledByAgent [sun.misc.Launcher$AppClassLoader@75b84c92, null, loaded=true] 524 TRACE c.t.l.b.InProcessInstrumentationExample - entering: com.tersesystems.logback.bytebuddy.ClassCalledByAgent.printStatement() with arguments=[] from source ClassCalledByAgent.java:18 I am a simple println method with no logging 529 TRACE c.t.l.b.InProcessInstrumentationExample - exiting: com.tersesystems.logback.bytebuddy.ClassCalledByAgent.printStatement() with arguments=[] => returnType=void from source ClassCalledByAgent.java:19 529 TRACE c.t.l.b.InProcessInstrumentationExample - entering: com.tersesystems.logback.bytebuddy.ClassCalledByAgent.printArgument(java.lang.String) with arguments=[42] from source ClassCalledByAgent.java:22 I am a simple println, printing 42 529 TRACE c.t.l.b.InProcessInstrumentationExample - exiting: com.tersesystems.logback.bytebuddy.ClassCalledByAgent.printArgument(java.lang.String) with arguments=[42] => returnType=void from source ClassCalledByAgent.java:23 529 TRACE c.t.l.b.InProcessInstrumentationExample - entering: com.tersesystems.logback.bytebuddy.ClassCalledByAgent.throwException(java.lang.String) with arguments=[hello world] from source ClassCalledByAgent.java:26 532 ERROR c.t.l.b.InProcessInstrumentationExample - throwing: com.tersesystems.logback.bytebuddy.ClassCalledByAgent.throwException(java.lang.String) with arguments=[hello world] ! thrown=java.lang.RuntimeException: I'm a squirrel! java.lang.RuntimeException: I'm a squirrel! at com.tersesystems.logback.bytebuddy.ClassCalledByAgent.throwException(ClassCalledByAgent.java:26) at com.tersesystems.logback.bytebuddy.InProcessInstrumentationExample.main(InProcessInstrumentationExample.java:65) ``` The `[Byte Buddy]` statements up top are caused by the debug listener, and let you know that Byte Buddy has successfully instrumented the class. Note also that there is no runtime overhead in pulling line numbers or source files into the enter/exit methods, as these are pulled directly from bytecode and do not involve `fillInStackTrace`. ================================================ FILE: docs/guide/jdbc.md ================================================ # JDBC There is a JDBC appender included which can be subclassed and extended as necessary in the `logback-jdbc-appender` module. Using a database for logging can be a big help when you just want to get at the logs of the last 30 seconds from inside the application. Because JDBC is both accessible and understandable, there's very little work required for querying. Also consider using [Blacklite](https://github.com/tersesystems/blacklite/), an SQLite appender configured for low latency and high throughput. Logback **does** have a native JDBC appender, but unfortunately it requires three tables and is not set up for easy subclassing. This one is better. This implementation assumes a single table, with a user defined extensible schema, and is set up with [HikariCP](https://github.com/brettwooldridge/HikariCP) and a thread pool executor to serve JDBC with minimal blocking. Note that you should **always** use a JDBC appender behind an async appender like `LoggingEventAsyncDisruptorAppender` and you should have an [appropriately sized connection pool](https://github.com/brettwooldridge/HikariCP/wiki/About-Pool-Sizing) for your database traffic. Database timestamps record time with microsecond resolution, whereas millisecond resolution is commonplace for logging, so for convenience both the timestamp with time zone and the time since epoch are recorded. For span information, the start time must also be recorded as TSE. Likewise, the level is recorded as both a text string for visual reference, and a level value so that you can order and filter database queries. Querying a database can be helpful when errors occur, because you can pull out all logs with a correlation id. See the [correlationid module](correlationid.md) for an example. ## Installation Add the library dependency using [com.tersesystems.logback:logback-jdbc-appender](https://mvnrepository.com/artifact/com.tersesystems.logback/logback-jdbc-appender). ## Usage ### Logging using in-memory H2 Database Using an in memory H2 database is a cheap and easy way to expose logs from inside the application without having to parse files. ```xml jdbc:h2:mem:logback org.h2.Driver sa CREATE TABLE IF NOT EXISTS events ( ID INT NOT NULL PRIMARY KEY AUTO_INCREMENT, ts TIMESTAMP(9) WITH TIME ZONE NOT NULL, tse_ms numeric NOT NULL, start_ms numeric NULL, level_value int NOT NULL, level VARCHAR(7) NOT NULL, evt JSON NOT NULL ); insert into events(ts, tse_ms, start_ms, level_value, level, evt) values(?, ?, ?, ?, ?, ?) delete from events where ts < ? PT30 ``` ### Logging using PostgresSQL If you want something larger scale, you'll probably be using Postgres instead of H2. You can log JSON to PostgreSQL, using the [built-in JSON datatype](https://www.postgresql.org/docs/current/functions-json.html). Postgres uses a custom JDBC type of `PGObject`, so the `insertEvent` method must be subclassed. This is what's in the `logback-postgresjson-appender` module: ```java public class PostgresJsonAppender extends JDBCAppender { private String objectType = "json"; public String getObjectType() { return objectType; } public void setObjectType(String objectType) { this.objectType = objectType; } @Override public void start() { super.start(); setDriver("org.postgresql.Driver"); } @Override protected void insertEvent(ILoggingEvent event, LongAdder adder, PreparedStatement statement) throws SQLException { PGobject jsonObject = new PGobject(); jsonObject.setType(getObjectType()); byte[] bytes = getEncoder().encode(event); jsonObject.setValue(new String(bytes, StandardCharsets.UTF_8)); statement.setObject(adder.intValue(), jsonObject); adder.increment(); } } ``` First, install PostgreSQL, create a database `logback`, a role `logback` and a password `logback` and add the following table: ```sql CREATE TABLE logging_table ( ID serial NOT NULL PRIMARY KEY, ts TIMESTAMPTZ(6) NOT NULL, tse_ms numeric NOT NULL, start_ms numeric NULL, level_value int NOT NULL, level VARCHAR(7) NOT NULL, evt jsonb NOT NULL ); CREATE INDEX idxgin ON logging_table USING gin (evt); ``` Because logs are inherently time-series data, you can use the [timescaleDB postgresql extension](https://docs.timescale.com/latest/introduction) as described in [Store application logs in timescaleDB/postgres](https://www.komu.engineer/blogs/timescaledb/timescaledb-for-logs), but that's not required. Then, add the following `logback.xml`: ```xml CREATE TABLE IF NOT EXISTS logging_table ( ID serial NOT NULL PRIMARY KEY, ts TIMESTAMPTZ(6) NOT NULL, tse_ms bigint NOT NULL, start_ms bigint NULL, level_value int NOT NULL, level VARCHAR(7) NOT NULL, evt jsonb NOT NULL ); CREATE INDEX idxgin ON logging_table USING gin (evt); insert into logging_table(ts, tse_ms, start_ms, level_value, level, evt) values(?, ?, ?, ?, ?, ?) jdbc:postgresql://localhost:5432/logback logback logback { "start_ms": "#asLong{%%startTime}" } true %-5relative %-5level %logger{35} - %msg%n ``` [Querying](http://clarkdave.net/2013/06/what-can-you-do-with-postgresql-and-json/) requires a little bit of extra syntax, using `evt->'myfield'` to select: ```sql select ts as end_date, start_ms as epoch_start, tse_ms as epoch_end, evt->'trace.span_id' as span_id, evt->'name' as name, evt->'message' as message, evt->'trace.parent_id' as parent, evt->'duration_ms' as duration_ms from logging_table where evt->'trace.trace_id' IS NOT NULL order by ts desc limit 5 ``` If you have extra logs that you want to import into PostgreSQL, you can [use PSQL to do that](https://stackoverflow.com/questions/39224382/how-can-i-import-a-json-file-into-postgresql/57445995#57445995). ### Extending JDBC Appender with extra fields The JDBC appender can be extended so you can add extra information to the table. In the `logback-correlationid` module, there's a `CorrelationIdJdbcAppender` that adds extra information into the event so you can query by the correlation id specifically, by using the `insertAdditionalData` hook: ```java public class CorrelationIdJdbcAppender extends JDBCAppender { private String mdcKey = "correlation_id"; public String getMdcKey() { return mdcKey; } public void setMdcKey(String mdcKey) { this.mdcKey = mdcKey; } protected CorrelationIdUtils utils; @Override public void start() { super.start(); utils = new CorrelationIdUtils(mdcKey); } @Override protected void insertAdditionalData( ILoggingEvent event, LongAdder adder, PreparedStatement statement) throws SQLException { insertCorrelationId(event, adder, statement); } private void insertCorrelationId( ILoggingEvent event, LongAdder adder, PreparedStatement statement) throws SQLException { Optional maybeCorrelationId = utils.get(event.getMarker()); if (maybeCorrelationId.isPresent()) { statement.setString(adder.intValue(), maybeCorrelationId.get()); } else { statement.setNull(adder.intValue(), Types.VARCHAR); } adder.increment(); } } ``` Then set up the table and add an index on the correlation id: ```sql CREATE TABLE IF NOT EXISTS events ( ID NUMERIC NOT NULL PRIMARY KEY AUTO_INCREMENT, ts TIMESTAMP(9) WITH TIME ZONE NOT NULL, tse_ms numeric NOT NULL, start_ms numeric NULL, level_value int NOT NULL, level VARCHAR(7) NOT NULL, evt JSON NOT NULL, correlation_id VARCHAR(255) NULL ); CREATE INDEX correlation_id_idx ON events(correlation_id); ``` And then you can query from there. See [Logging Structured Data to Database](https://tersesystems.com/blog/2019/09/18/logging-structured-data-to-database/) for more details. ================================================ FILE: docs/guide/relativens.md ================================================ # Relative Nanoseconds Appender `LoggingEvent` already has a timestamp associated with it, but that timestamp is generated by `System.currentTimeMillis`. When your logging is fast enough that you can log several statements in the same millisecond, it can be frustrating to not know which came first. Adding a `relative_ns` field provides resolution down to the nanosecond, [sort of](https://shipilev.net/blog/2014/nanotrusting-nanotime/). For example, here's two different records with the same millisecond. ```json {"id":"FfwJtsNLLWo6O0Qbm7EAAA","relative_ns":11808036,"tse_ms":1584163603315,"start_ms":null,"@timestamp":"2020-03-14T05:26:43.315Z","@version":"1","message":"HikariPool-2 - Start completed.","logger_name":"com.zaxxer.hikari.HikariDataSource","thread_name":"play-dev-mode-akka.actor.default-dispatcher-7","level":"INFO","level_value":20000} {"id":"FfwJtsNLLWo6O0Qbm7EAAB","relative_ns":11981656,"tse_ms":1584163603315,"start_ms":null,"@timestamp":"2020-03-14T05:26:43.315Z","@version":"1","message":"jdbc-appender-pool-1584163602961 - Start completed.","logger_name":"com.zaxxer.hikari.HikariDataSource","thread_name":"logback-appender-ASYNC_JDBC-2","level":"INFO","level_value":20000} ``` Note that the timestamp is `2020-03-14T05:26:43.315Z` and the time since epoch is `1584163603315`. The flake ids distinguish between log entries by using a counter when millisecond precision is exceeded, so the first record is `FfwJtsNLLWo6O0Qbm7EAAA` ending in `A` and the second record is `FfwJtsNLLWo6O0Qbm7EAAB` ending in `B`. The relative time tells us exactly how much time has elapsed between the two events: `11981656 - 11808036` is 0.17362 milliseconds. All logging events are computed using `System.nanoTime - NanoTime.start`, where `NanoTime.start` is a static final field initialized JVM start time (technically at class loading but close enough). This value may be negative to begin with, but always increments. See the [showcase](https://github.com/tersesystems/terse-logback-showcase) for an example. ## Installation Add the library dependency using [com.tersesystems.logback:logback-classic](https://mvnrepository.com/artifact/com.tersesystems.logback/logback-classic). ## Usage ```xml ``` You can extract the nanotime using a converter: ```xml ``` There are no configuration options. ================================================ FILE: docs/guide/select.md ================================================ # Select Appender Different appenders are useful in different environments. Development wants: * Want colorized output on their consoles, with line oriented logs. * Would also like to be able to read through logs with debug, info and warnings in them, to track control flow. If you have the logs seperated, that makes it harder. * Generally don't want to run a local ELK stack or TCP appenders to see their logs. Operations wants: * Really want centralized logging, and a way to drill out on it. Structured logging especially. * May want to have everything write to STDOUT, as is case for Docker / 12 Factor Apps. * May have duplicate logs from the underlying architecture, that need to be dedupped. * May not want redundant / repeated messages, which developers are not as sensitive to. * Really hate getting paged with the same error repeatedly. Logback is not aware of different environments. There's no out of the box way to say "in this environment I want these sets of appenders, but in this other environment I want these other sets of appenders." ## Installation Add the library dependency using [com.tersesystems.logback:logback-core](https://mvnrepository.com/artifact/com.tersesystems.logback/logback-core). ## Usage The logback appenders under selection must have the name defined as an element, because Logback only looks for the name attribute at the top level, but otherwise they're the same. Here, we select the set of appenders we want based on the `LOGBACK_ENVIRONMENT` environment variable. ```xml ${LOGBACK_ENVIRONMENT} test test-list development development-console %-5relative %-5level %logger{35} - %msg%n staging staging-console %-5relative %-5level %logger{35} - %msg%n staging-file file.log %-5relative %-5level %logger{35} - %msg%n ``` This is a much cleaner way to organize appenders than putting Janino logic into the configuration. ================================================ FILE: docs/guide/slf4jbridge.md ================================================ # JUL to SLF4J Bridge It's easy to assume that all Java libraries will depend on SLF4J. But one of the oddities of Java logging is that there's a built-in logging framework called `java.util.logging` (JUL) which is rarely used but does appear in libraries such as [Guice](https://groups.google.com/g/google-guice/c/J2M64gM6Yao), [GRPC](https://github.com/grpc/grpc-java/issues/1577), and [Guava](https://github.com/google/guava/issues/829). When errors happen in these frameworks, they may never show up in logging at all, because JUL will write out to standard output and standard error by default. ## Installation Add the library dependency using [com.tersesystems.logback:logback-classic](https://mvnrepository.com/artifact/com.tersesystems.logback/logback-classic). ## Usage `SLF4JBridgeHandler` is a logging bridge, which is available in [jul-to-slf4j](http://www.slf4j.org/legacy.html#jul-to-slf4j). It does the job, but it does require some custom code to be added on startup to tell JUL that the handler is SLF4J: ```java SLF4JBridgeHandler.removeHandlersForRootLogger(); SLF4JBridgeHandler.install(); ``` This isn't ideal, as it's very easy to miss that you have to add these lines of code. Some frameworks such as [Play Framework]() are [smart enough](https://github.com/playframework/playframework/blob/master/core/play-logback/src/main/scala/play/api/libs/logback/LogbackLoggerConfigurator.scala#L86) are smart enough to handle this for you, but there are cases where you're not using those frameworks, and we'd like JUL to just work. This isn't so easy. JUL is very basic, and accepts configuration from system properties. The LogManager has two system properties: - java.util.logging.config.class - java.util.logging.config.file If it doesn't find either, then it looks in `${java.home}/conf/logging.properties` if you're on JDK 11. There's no way to configure it from classpath, you have to do that [by hand](https://mkyong.com/logging/how-to-load-logging-properties-for-java-util-logging/). There is discussion on [Stack Overflow](https://stackoverflow.com/a/11245040/5266) and the [SLF4J mailing list](https://www.mail-archive.com/slf4j-dev@qos.ch/msg00738.html) suggesting that JUL looks for `logging.properties` in the classpath. This is incorrect -- the only way you'll see `logging.properties` is from setting `java.util.logging.config.file` or if you're overwriting `${java.home}/logging.properties`. Here's the [source code](https://github.com/AdoptOpenJDK/openjdk-jdk/blob/master/src/java.logging/share/classes/java/util/logging/LogManager.java#L1347) so you can check for yourself. However, since we're using Logback, we can leverage the fact that Logback searches through the classpath for `logback.xml`. All we need is a custom action to wrap `SLF4JBridgeHandler` and we can have a code free solution. This is what `SLF4JBridgeHandlerAction` does. You should also configure the `LevelChangePropagator`, to [reduce the impact of logging](http://logback.qos.ch/manual/configuration.html#LevelChangePropagator), and you must make sure that the `LoggerFactory` is called before any JUL dependent code. You should set your `logback.xml` roughly as follows: ```xml true %date{H:mm:ss.SSS} [%highlight(%-5level)] %logger - %message%ex%n ``` And then you should call `org.slf4j.LoggerFactory.getLogger` as a static final to prevent any initialization problems: ```java package example; import com.google.inject.*; import org.slf4j.*; public class App { // Ensure that logback.xml is parsed by LoggerFactory _before_ Guice calls JUL. private static final Logger logger = org.slf4j.LoggerFactory.getLogger(App.class); public String getGreeting() { return "Hello World!"; } public static void main(String[] args) { final Injector injector = Guice.createInjector(); final App instance = injector.getInstance(App.class); logger.info(instance.getGreeting()); } } ``` And that should render the following: ``` 19:51:45.493 [DEBUG] com.google.inject.internal.util.ContinuousStopwatch - Module execution: 64ms 19:51:45.494 [DEBUG] com.google.inject.internal.util.ContinuousStopwatch - Interceptors creation: 2ms 19:51:45.496 [DEBUG] com.google.inject.internal.util.ContinuousStopwatch - TypeListeners & ProvisionListener creation: 1ms 19:51:45.511 [DEBUG] com.google.inject.internal.util.ContinuousStopwatch - Scopes creation: 15ms 19:51:45.511 [DEBUG] com.google.inject.internal.util.ContinuousStopwatch - Converters creation: 0ms 19:51:45.514 [DEBUG] com.google.inject.internal.util.ContinuousStopwatch - Binding creation: 2ms 19:51:45.515 [DEBUG] com.google.inject.internal.util.ContinuousStopwatch - Module annotated method scanners creation: 0ms 19:51:45.515 [DEBUG] com.google.inject.internal.util.ContinuousStopwatch - Private environment creation: 0ms 19:51:45.515 [DEBUG] com.google.inject.internal.util.ContinuousStopwatch - Injector construction: 0ms 19:51:45.515 [DEBUG] com.google.inject.internal.util.ContinuousStopwatch - Binding initialization: 0ms 19:51:45.515 [DEBUG] com.google.inject.internal.util.ContinuousStopwatch - Binding indexing: 0ms 19:51:45.515 [DEBUG] com.google.inject.internal.util.ContinuousStopwatch - Collecting injection requests: 0ms 19:51:45.515 [DEBUG] com.google.inject.internal.util.ContinuousStopwatch - Binding validation: 0ms 19:51:45.515 [DEBUG] com.google.inject.internal.util.ContinuousStopwatch - Static validation: 0ms 19:51:45.515 [DEBUG] com.google.inject.internal.util.ContinuousStopwatch - Instance member validation: 0ms 19:51:45.516 [DEBUG] com.google.inject.internal.util.ContinuousStopwatch - Provider verification: 0ms 19:51:45.516 [DEBUG] com.google.inject.internal.util.ContinuousStopwatch - Delayed Binding initialization: 0ms 19:51:45.516 [DEBUG] com.google.inject.internal.util.ContinuousStopwatch - Static member injection: 0ms 19:51:45.516 [DEBUG] com.google.inject.internal.util.ContinuousStopwatch - Instance injection: 0ms 19:51:45.516 [DEBUG] com.google.inject.internal.util.ContinuousStopwatch - Preloading singletons: 0ms 19:51:45.545 [INFO ] example.App - Hello World! ``` ================================================ FILE: docs/guide/tracing.md ================================================ # Tracing to Honeycomb You can connect Logback to Honeycomb directly through the Honeycomb Logback appender, using the [Events API](https://docs.honeycomb.io/api/events/). Posting data directly to Honeycomb lets you leverage Honeycomb's trace API to show logs as hierarchical traces and spans. Bear in mind that the tracing feature here is optional -- you can use the Honeycomb appender out of the box without tracing with just plain logs. However, adding tracing through logging is interesting in a couple of different ways. Using Honeycomb means logs can be immediately visualized and queried without setting up extensive infrastructure. From a tracing perspective, it completely avoids the OpenTelemetry manual instrumentation usually needed for tracing, and allows for tweaks and customization without the sampling or collector assumptions involved. ## Implementation Add the library dependency using [com.tersesystems.logback:logback-honeycomb-okhttp](https://mvnrepository.com/artifact/com.tersesystems.logback/logback-honeycomb-okhttp) for the honeycomb appender. To set up tracing, add [com.tersesystems.logback:logback-tracing](https://mvnrepository.com/artifact/com.tersesystems.logback/logback-tracing) and [com.tersesystems.logback:logback-classic](https://mvnrepository.com/artifact/com.tersesystems.logback/logback-classic) for the start time converter. ## Usage The appender is of type `com.tersesystems.logback.honeycomb.HoneycombAppender`, and makes use of the client under the hood. Because the honeycomb appender uses an HTTP client under the hood, there are a couple of important notes. > **NOTE**: Because the HTTP client runs on a different thread, you should make sure you either shutdown Logback explicitly by calling `loggerContext.stop`, or use [shutdown hook](http://logback.qos.ch/manual/configuration.html#shutdownHook) configured so that shutting down can be delayed until the events are posted. The appender is as follows: ```xml 1000 ${HONEYCOMB_API_KEY} terse-logback 1 10 true false { "start_ms": "#asLong{%startTime}" } true ``` You can also send tracing information to Honeycomb through SLF4J markers, using the `SpanMarkerFactory`. Underneath the hood, the SpanInfo puts together logstash markers according to [manual tracing](https://docs.honeycomb.io/working-with-your-data/tracing/send-trace-data/#manual-tracing). The way this works in practice is that you start up a `SpanInfo` at the beginning of a request, and call `buildNow` to mark the start of the span. At the end of the operation, you log with a marker, by passing through the marker factory: ```java SpanInfo spanInfo = builder.setRootSpan("index").buildNow(); // ... logger.info(markerFactory.apply(spanInfo), "completed successfully!"); ``` If you want to create a child span, you can do it from the parent using `withChild`: ```java return spanInfo.withChild("doSomething", childInfo -> { return doSomething(childInfo); }); ``` or asking for a child builder that you can build yourself: ```java SpanInfo childInfo = spanInfo.childBuilder().setSpanName("doSomething").buildNow(); ``` The start time information is captured in a `StartTimeMarker` which can be extracted by `StartTime.from` and is used in building the Honeycomb Request. The event timestamp serves as the span's end time. This is useful in Honeycomb Tracing, as the timestamp is the start time, not the time that the log entry was posted. For example, in Play you might run a controller as follows: ```scala import com.tersesystems.logback.tracing.SpanMarkerFactory import com.tersesystems.logback.tracing.SpanInfo import javax.inject._ import org.slf4j.LoggerFactory import play.api.libs.concurrent.Futures import play.api.mvc._ import scala.concurrent.{ExecutionContext, Future} import scala.util.{Failure, Success} import scala.concurrent.duration._ @Singleton class HomeController @Inject()(cc: ControllerComponents, futures: Futures) (implicit ec: ExecutionContext) extends AbstractController(cc) { private val markerFactory = new SpanMarkerFactory() private val logger = LoggerFactory.getLogger(getClass) private def builder: SpanInfo.Builder = SpanInfo.builder().setServiceName("play_hello_world") def index(): Action[AnyContent] = Action.async { implicit request: Request[AnyContent] => val spanInfo = builder.setRootSpan("index").buildNow() val f: Future[Result] = spanInfo.withChild("renderPage", renderPage(_)) f.andThen { case Success(_) => logger.info(markerFactory(spanInfo), "index completed successfully!") case Failure(e) => logger.error(markerFactory(spanInfo), "index completed with error", e) } } def renderPage(spanInfo: SpanInfo): Future[Result] = { futures.delay(5.seconds).map { _ => Ok(views.html.index()) }.andThen { case Success(_) => logger.info(markerFactory(spanInfo), "renderPage completed successfully!") case Failure(e) => logger.error(markerFactory(spanInfo), "renderPage completed with error", e) } } } ``` This generates a trace with a root span of "index", a child span of "renderPage" each with their own durations. You can also send [span events](https://docs.honeycomb.io/working-with-your-data/tracing/send-trace-data/#span-events) and [span links](https://docs.honeycomb.io/working-with-your-data/tracing/send-trace-data/#links) using the `LinkMarkerFactory` and `EventMarkerFactory`, similar to the `SpanMarkerFactory`. See [Tracing With Logback and Honeycomb](https://tersesystems.com/blog/2019/08/22/tracing-with-logback-and-honeycomb/) and [Hierarchical Instrumented Tracing with Logback](https://tersesystems.com/blog/2019/09/15/hierarchical-instrumented-tracing-with-logback/) for more details. ================================================ FILE: docs/guide/turbomarker.md ================================================ # Turbo Markers [Turbo filters](https://logback.qos.ch/manual/filters.html#TurboFilter) are filters that decide whether a logging event should be created or not. They are not appender specific in the way that normal filters are, and so are used to override logger levels. However, there's a problem with the way that the turbo filter is set up: the two implementing classes are `ch.qos.logback.classic.turbo.MarkerFilter` and `ch.qos.logback.classic.turbo.MDCFilter`. The marker filter will always log if the given marker is applied, and the MDC filter relies on an attribute being populated in the MDC map. What we'd really like to do is say "for this particular user, log everything he does at DEBUG level" and not have it rely on thread-local state at all, and carry out an arbitrary computation at call time. We can do this by adding a decider to a turbo filter, and adding "turbo markers." ## Installation Add the library dependency using [com.tersesystems.logback:logback-turbomarker](https://mvnrepository.com/artifact/com.tersesystems.logback/logback-turbomarker). ## Usage We start by pulling the `decide` method to an interface, [`TurboFilterDecider`](https://github.com/tersesystems/terse-logback/blob/master/logback-classic/src/main/java/com/tersesystems/logback/classic/TurboFilterDecider.java): ```java package com.tersesystems.logback.classic; public interface TurboFilterDecider { FilterReply decide(Marker marker, Logger logger, Level level, String format, Object[] params, Throwable t); } ``` And have the turbo filter [delegate to markers that implement the TurboFilterDecider interface](https://github.com/tersesystems/terse-logback/blob/master/logback-turbomarker/src/main/java/com/tersesystems/logback/turbomarker/TurboMarkerTurboFilter.java): ```java package com.tersesystems.logback.turbomarker; public class TurboMarkerTurboFilter extends TurboFilter { @Override public FilterReply decide(Marker rootMarker, Logger logger, Level level, String format, Object[] params, Throwable t) { // ... } private FilterReply evaluateMarker(Marker marker, Marker rootMarker, Logger logger, Level level, String format, Object[] params, Throwable t) { if (marker instanceof TurboFilterDecider) { TurboFilterDecider decider = (TurboFilterDecider) marker; return decider.decide(rootMarker, logger, level, format, params, t); } return FilterReply.NEUTRAL; } } ``` This gets us part of the way there. We can then set up a [`ContextAwareTurboFilterDecider`](https://github.com/tersesystems/terse-logback/blob/master/logback-turbomarker/src/main/java/com/tersesystems/logback/turbomarker/ContextAwareTurboFilterDecider.java), which does the same thing but assumes that you have a type `C` that is your external context. ```java public interface ContextAwareTurboFilterDecider { FilterReply decide(ContextAwareTurboMarker marker, C context, Marker rootMarker, Logger logger, Level level, String format, Object[] params, Throwable t); } ``` Then we add a marker class that [incorporates that context in decision making](https://github.com/tersesystems/terse-logback/blob/master/logback-turbomarker/src/main/java/com/tersesystems/logback/turbomarker/ContextAwareTurboMarker.java): ```java public class ContextAwareTurboMarker extends TurboMarker implements TurboFilterDecider { private final C context; private final ContextAwareTurboFilterDecider contextAwareDecider; // ... initializers and such @Override public FilterReply decide(Marker rootMarker, Logger logger, Level level, String format, Object[] params, Throwable t) { return contextAwareDecider.decide(this, context, rootMarker, logger, level, format, params, t); } } ``` This may look good in the abstract, but it may make more sense to see it in action. To do this, we'll set up an example application context: ```java public class ApplicationContext { private final String userId; public ApplicationContext(String userId) { this.userId = userId; } public String currentUserId() { return userId; } } ``` and a factory that contains the decider: ```java import com.tersesystems.logback.turbomarker.*; public class UserMarkerFactory { private final Set userIdSet = new ConcurrentSkipListSet<>(); private final ContextDecider decider = context -> userIdSet.contains(context.currentUserId()) ? FilterReply.ACCEPT : FilterReply.NEUTRAL; public void addUserId(String userId) { userIdSet.add(userId); } public void clear() { userIdSet.clear(); } public UserMarker create(ApplicationContext applicationContext) { return new UserMarker("userMarker", applicationContext, decider); } } ``` and a `UserMarker`, which is only around for the logging evaluation: ```java public class UserMarker extends ContextAwareTurboMarker { public UserMarker(String name, ApplicationContext applicationContext, ContextAwareTurboFilterDecider decider) { super(name, applicationContext, decider); } } ``` and then we can set up logging that will only work for user "28": ```java String userId = "28"; ApplicationContext applicationContext = new ApplicationContext(userId); UserMarkerFactory userMarkerFactory = new UserMarkerFactory(); userMarkerFactory.addUserId(userId); // say we want logging events created for this user id UserMarker userMarker = userMarkerFactory.create(applicationContext); logger.info(userMarker, "Hello world, I am info and log for everyone"); logger.debug(userMarker, "Hello world, I am debug and only log for user 28"); ``` This works especially well with a configuration management service like [Launch Darkly](https://docs.launchdarkly.com/docs/java-sdk-reference#section-variation), where you can [target particular users](https://docs.launchdarkly.com/docs/targeting-users#section-assigning-users-to-a-variation) and set up logging based on the user variation. The LaunchDarkly blog has [best practices for operational flags](https://launchdarkly.com/blog/operational-flags-best-practices/): > Verbose logs are great for debugging and troubleshooting but always running an application in debug mode is not viable. The amount of log data generated would be overwhelming. Changing logging levels on the fly typically requires changing a configuration file and restarting the application. A multivariate operational flag enables you to change the logging level from WARNING to DEBUG in real-time. But we can give an example using the Java SDK. You can set up a factory like so: ```java import ch.qos.logback.classic.Logger; import ch.qos.logback.core.spi.FilterReply; import com.launchdarkly.client.LDClientInterface; import com.launchdarkly.client.LDUser; public class LDMarkerFactory { private final LaunchDarklyDecider decider; public LDMarkerFactory(LDClientInterface client) { this.decider = new LaunchDarklyDecider(requireNonNull(client)); } public LDMarker create(String featureFlag, LDUser user) { return new LDMarker(featureFlag, user, decider); } static class LaunchDarklyDecider implements MarkerContextDecider { private final LDClientInterface ldClient; LaunchDarklyDecider(LDClientInterface ldClient) { this.ldClient = ldClient; } @Override public FilterReply apply(ContextAwareTurboMarker marker, LDUser ldUser) { return ldClient.boolVariation(marker.getName(), ldUser, false) ? FilterReply.ACCEPT : FilterReply.NEUTRAL; } } public static class LDMarker extends ContextAwareTurboMarker { LDMarker(String name, LDUser context, ContextAwareTurboFilterDecider decider) { super(name, context, decider); } } } ``` and then use the feature flag as the marker name and target the beta testers group: ```java public class LDMarkerTest { private static LDClientInterface client; @BeforeAll public static void setUp() { client = new LDClient("sdk-key"); } @AfterAll public static void shutDown() { try { client.close(); } catch (IOException e) { e.printStackTrace(); } } public void testMatchingMarker() throws JoranException { LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory(); ch.qos.logback.classic.Logger logger = loggerContext.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME); LDMarkerFactory markerFactory = new LDMarkerFactory(client); LDUser ldUser = new LDUser.Builder("UNIQUE IDENTIFIER") .firstName("Bob") .lastName("Loblaw") .customString("groups", singletonList("beta_testers")) .build(); LDMarkerFactory.LDMarker ldMarker = markerFactory.create("turbomarker", ldUser); logger.info(ldMarker, "Hello world, I am info"); logger.debug(ldMarker, "Hello world, I am debug"); ListAppender appender = (ListAppender) logger.getAppender("LIST"); assertThat(appender.list.size()).isEqualTo(2); appender.list.clear(); } } ``` This is also a reason why [diagnostic logging is better than a debugger](https://lemire.me/blog/2016/06/21/i-do-not-use-a-debugger/). Debuggers are ephemeral, can't be used in production, and don't produce a consistent record of events: debugging log statements are the single best way to dump internal state and manage code flows in an application. See [Targeted Diagnostic Logging in Production](https://tersesystems.com/blog/2019/07/22/targeted-diagnostic-logging-in-production/) for more details. ================================================ FILE: docs/guide/typesafeconfig.md ================================================ # Typesafe Config The `TypesafeConfigAction` will search in a variety of places for configuration using [standard fallback behavior](https://github.com/lightbend/config#standard-behavior) for Typesafe Config, which gives a richer experience to end users. ## Installation Add the library dependency using [com.tersesystems.logback:logback-typesafe-config](https://mvnrepository.com/artifact/com.tersesystems.logback/logback-typesafe-config). ## Usage The configuration is derived as follows: ```java Config config = systemProperties // Look for a property from system properties first... .withFallback(file) // if we don't find it, then look in an explicitly defined file... .withFallback(testResources) // if not, then if logback-test.conf exists, look for it there... .withFallback(resources) // then look in logback.conf... .withFallback(reference) // and then finally in logback-reference.conf. .resolve(); // Tell config that we want to use ${?ENV_VAR} type stuff. ``` The configuration is then placed in the `LoggerContext` which is available to all of Logback. ```java lc.putObject(ConfigConstants.TYPESAFE_CONFIG_CTX_KEY, config); ``` And then all properties are made available to Logback, either at the `local` scope or at the `context` scope. Properties must be strings, but you can also provide Maps and Lists to the Logback Context, through `context.getObject`. ### Log Levels and Properties through Typesafe Config Configuration of properties and setting log levels is done through [Typesafe Config](https://github.com/lightbend/config#overview), using `TypesafeConfigAction` Here's the `logback.conf` from the example application. It's in Human-Optimized Config Object Notation or [HOCON](https://github.com/lightbend/config/blob/master/HOCON.md). ```hocon # Set logger levels here. levels = { # Override the default root log level with ROOT_LOG_LEVEL environment variable, if defined... ROOT = ${?ROOT_LOG_LEVEL} # You can set a logger with a simple package name. example = DEBUG # You can also do nested overrides here. deeply.nested { package = TRACE } } # Overrides the properties from logback-reference.conf local { logback.environment=production censor { regex = """hunter2""" // http://bash.org/?244321 replacementText = "*******" json.keys += "password" // adding password key will remove the key/value pair entirely } # Overwrite text file on every run. textfile { append = false } # Override the color code in console for info statements highlight { info = "black" } } # You can also include settings from other places include "myothersettings" ``` For tests, there's a `logback-test.conf` that will override (rather than completely replace) any settings that you have in `logback.conf`: ```hocon include "logback-reference" levels { example = TRACE } local { logback.environment=test textfile { location = "log/test/application-test.log" append = false } jsonfile { location = "log/test/application-test.json" prettyprint = true } } ``` There is also a `logback-reference.conf` file that handles the default configuration for the appenders, and those settings can be overridden. They are written out individually in the encoder configuration so I won't go over it here. Note that appender logic is not available here -- it's all defined through the `structured-config` in `logback.xml`. Using Typesafe Config is not a requirement -- the point here is to show that there are more options to configuring Logback than using a straight XML file. See [Application Logging in Java: Adding Configuration](https://tersesystems.com/blog/2019/05/05/application-logging-in-java-part-2/) for more details. ================================================ FILE: docs/guide/uniqueid.md ================================================ # Unique ID Appenders The unique id appender allows the logging event to carry a unique id. When used in conjunction with `SelectAppender` or `CompositeAppender`, this allows for a log record to use the same id across different logs. For example, in `application.log`, you'll see a single line that starts with `FfwJtsNHYSw6O0Qbm7EAAA`: ```text FfwJtsNHYSw6O0Qbm7EAAA 2020-03-14T05:30:14.965+0000 [INFO ] play.api.db.HikariCPConnectionPool in play-dev-mode-akka.actor.default-dispatcher-7 - Creating Pool for datasource 'logging' ``` You can search for this string in `application.json` and see more detail on the log record: ```json {"id":"FfwJtsNHYSw6O0Qbm7EAAA","relative_ns":20921024,"tse_ms":1584163814965,"start_ms":null,"@timestamp":"2020-03-14T05:30:14.965Z","@version":"1","message":"Creating Pool for datasource 'logging'","logger_name":"play.api.db.HikariCPConnectionPool","thread_name":"play-dev-mode-akka.actor.default-dispatcher-7","level":"INFO","level_value":20000} ``` See the [showcase](https://github.com/tersesystems/terse-logback-showcase) for an example. ## Installation Add the library dependency using [com.tersesystems.logback:logback-uniqueid-appender](https://mvnrepository.com/artifact/com.tersesystems.logback/logback-uniqueid-appender). ## Usage ```xml ``` To extract the unique ID, register a converter: ```xml ``` ## ID Generators Unique IDs come with several options. Flake ID is the default. ### Flake ID Generator Flake IDs are decentralized and k-ordered, meaning that they are "roughly time-ordered when sorted lexicographically." This implementation uses [idem](https://github.com/mguenther/idem) with `Flake128S`. ```xml ``` ### Random UUID Generator Generates a Random UUIDv4 using a ThreadLocalRandom according to https://github.com/f4b6a3/uuid-creator. ```xml ``` ### TSID Generator Generates a TSID according to https://github.com/f4b6a3/tsid-creator. **Highly recommended to set a *tsidcreator.node* system property in your application to configure the node id.** ```xml ``` ### ULID Generator Creates a monotonic ULID using a threadlocal random according to https://github.com/f4b6a3/ulid-creator. ```xml ``` ### KSU ID Generator Creates a subsecond KSUID according to https://github.com/f4b6a3/ksuid-creator. ```xml ``` ================================================ FILE: docs/index.md ================================================ # Terse Logback Terse Logback is a collection of [Logback](https://logback.qos.ch/) modules that extend [Logback](https://logback.qos.ch/manual/index.html) functionality. I've written about the reasoning and internal architecture in a series of blog posts. The [full list](https://tersesystems.com/category/logging/) is available on [https://tersesystems.com](https://tersesystems.com). ## Showcase If you want to see a running application, there is a [showcase web application](https://github.com/tersesystems/terse-logback-showcase) that run out of the box that demonstrates some of the more advanced features, and shows you can integrate terse-logback with [Sentry](https://sentry.io) and [Honeycomb](https://www.honeycomb.io). ## Modules - [Audio](guide/audio.md): Play audio when you log by attaching markers to your logging statements. - [Budgeting / Rate Limiting](guide/budget.md): Limit the amount of debugging or tracing statements in a time period. - [Censors](guide/censor.md): Censor sensitive information in logging statements. - [Composite](guide/composite.md): Presents a single appender that composes several appenders. - [Compression](guide/compression.md): Write to a compressed zstandard file. - [Correlation Id](guide/correlationid.md): Adds markers and filters for correlation id. - [Exception Mapping](guide/exception-mapping.md): Show the important details of an exception, including the root cause in a summary format. - [Instrumentation](guide/instrumentation.md): Decorates any (including JVM) class with enter and exit logging statements at runtime. - [JDBC](guide/jdbc.md): Use Postgres JSON to write structured logging to a single table. - [JUL to SLF4J Bridge](guide/slf4jbridge.md): Configure java.util.logging to write to SLF4J with no [manual coding](https://mkyong.com/logging/how-to-load-logging-properties-for-java-util-logging/). - [Relative Nanos](guide/relativens.md): Composes a logging event to contain relative nanoseconds based off `System.nanoTime`. - [Select Appender](guide/select.md): Appender that selects an appender from a list based on key. - [Tracing](guide/tracing.md): Sends logging events and traces to [Honeycomb Event API](https://docs.honeycomb.io/api/events/). - [Typesafe Config](guide/typesafeconfig.md): Configure Logback properties using [HOCON](https://github.com/lightbend/config/blob/main/HOCON.md). - [Turbo Markers](guide/turbomarker.md): [Turbo Filters](https://logback.qos.ch/manual/filters.html#TurboFilter) that depend on arbitrary deciders that can log at debug level for sessions. - [Unique ID Appender](guide/uniqueid.md): Composes logging event to contain a unique id across multiple appenders. ================================================ FILE: docs/reading/reading.md ================================================ # Further Reading Everything I write on logging is going to be here: * [Terse Systems](https://tersesystems.com/category/logging/) ### Best Practices Many of these are logback specific, but still good overall. * [9 Logging Best Practices Based on Hands-on Experience](https://www.loomsystems.com/blog/single-post/2017/01/26/9-logging-best-practices-based-on-hands-on-experience) * [Woofer: logging in (best) practices](https://orange-opensource.github.io/woofer/logging-code/): Spring Boot * [A whole product concern logging implementation](http://stevetarver.github.io/2016/04/20/whole-product-logging.html) * [There is more to logging than meets the eye](https://allegro.tech/2015/10/there-is-more-to-logging-than-meets-the-eye.html) * [Monitoring demystified: A guide for logging, tracing, metrics](https://techbeacon.com/enterprise-it/monitoring-demystified-guide-logging-tracing-metrics) * [Application-Level Logging Best Practices](https://news.ycombinator.com/item?id=19497788) Stack Overflow has a couple of good tips on SLF4J and Logging: * [When to use the different log levels](https://stackoverflow.com/questions/2031163/when-to-use-the-different-log-levels) * [Why does the TRACE level exist, and when should I use it rather than DEBUG?](https://softwareengineering.stackexchange.com/questions/279690/why-does-the-trace-level-exist-and-when-should-i-use-it-rather-than-debug) * [Best practices for using Markers in SLF4J/Logback](https://stackoverflow.com/questions/4165558/best-practices-for-using-markers-in-slf4j-logback) * [Stackoverflow: Logging best practices in multi-node environment](https://stackoverflow.com/questions/43496695/java-logging-best-practices-in-multi-node-environment) #### Level Up Logs [Alberto Navarro](https://looking4q.blogspot.com/) has a great series
  1. Introduction (Everyone)
  2. JSON as logs format (Everyone)
  3. Logging best practices with Logback (Targetting Java DEVs)
  4. Logging cutting-edge practices (Targetting Java DEVs) 
  5. Contract first log generator (Targetting Java DEVs)
  6. ElasticSearch VRR Estimation Strategy (Targetting OPS)
  7. VRR Java + Logback configuration (Targetting OPS)
  8. VRR FileBeat configuration (Targetting OPS)
  9. VRR Logstash configuration and Index templates (Targetting OPS)
  10. VRR Curator configuration (Targetting OPS)
  11. Logstash Grok, JSON Filter and JSON Input performance comparison (Targetting OPS)
#### Logging Anti Patterns Logging Anti-Patterns by [Rolf Engelhard](https://rolf-engelhard.de/): * [Logging Anti-Patterns](http://rolf-engelhard.de/2013/03/logging-anti-patterns-part-i/) * [Logging Anti-Patterns, Part II](http://rolf-engelhard.de/2013/04/logging-anti-patterns-part-ii/) * [Logging Anti-Patterns, Part III](https://rolf-engelhard.de/2013/10/logging-anti-patterns-part-iii/) #### Clean Code, clean logs [Tomasz Nurkiewicz](https://www.nurkiewicz.com/) has a great series on logging: * [Clean code, clean logs: use appropriate tools (1/10)](https://www.nurkiewicz.com/2010/05/clean-code-clean-logs-use-appropriate.html) * [Clean code, clean logs: logging levels are there for you (2/10)](https://www.nurkiewicz.com/2010/05/clean-code-clean-logs-tune-your-pattern.html) * [Clean code, clean logs: do you know what you are logging? (3/10)](https://www.nurkiewicz.com/2010/05/clean-code-clean-logs-do-you-know-what.html) * [Clean code, clean logs: avoid side effects (4/10)](https://www.nurkiewicz.com/2010/05/clean-code-clean-logs-avoid-side.html) * [Clean code, clean logs: concise and descriptive (5/10)](https://www.nurkiewicz.com/2010/05/clean-code-clean-logs-concise-and.html) * [Clean code, clean logs: tune your pattern (6/10)](https://www.nurkiewicz.com/2010/05/clean-code-clean-logs-tune-your-pattern.html) * [Clean code, clean logs: log method arguments and return values (7/10)](https://www.nurkiewicz.com/2010/05/clean-code-clean-logs-log-method.html) * [Clean code, clean logs: watch out for external systems (8/10)](https://www.nurkiewicz.com/2010/05/clean-code-clean-logs-watch-out-for.html) * [Clean code, clean logs: log exceptions properly (9/10)](https://www.nurkiewicz.com/2010/05/clean-code-clean-logs-log-exceptions.html) * [Clean code, clean logs: easy to read, easy to parse (10/10)](https://www.nurkiewicz.com/2010/05/clean-code-clean-logs-easy-to-read-easy.html) * [Condensed 10 Tips on javacodegeeks](https://www.javacodegeeks.com/2011/01/10-tips-proper-application-logging.html) ### JSON Logging * [Logging in JSON](http://www.asynchronous.org/blog/archives/2006/01/25/logging-in-json) * [Write Logs for Machines, not Humans](https://paul.querna.org/articles/2011/12/26/log-for-machines-in-json/) ### Maple * [Maple](https://github.com/Randgalt/maple) ### Eliot * [Eliot](https://eliot.readthedocs.io/en/stable/quickstart.html) * [Eliot Tree](https://github.com/jonathanj/eliottree) ### TreeLog * [Treelog](https://github.com/lancewalton/treelog) ### Bunyan Bunyan stands out for a number of innovations: ring buffers and JSON specifically. * [Bunyan](https://timboudreau.com/blog/bunyan/read) * [Comparison of Winston and Bunyan](https://strongloop.com/strongblog/compare-node-js-logging-winston-bunyan/) * [Service logging in JSON with Bunyan](https://trentm.com/2012/03/service-logging-in-json-with-bunyan.html) * [Bunyan Logging in Production at Joyent](https://trentm.com/talk-bunyan-in-prod/#/8) ### Timbre * [Timbre](https://github.com/ptaoussanis/timbre/blob/master/README.md) ### Logback Encoders and Appenders * [concurrent-build-logger](https://github.com/takari/concurrent-build-logger) (encoders and appenders both) * [logzio-logback-appender](https://github.com/logzio/logzio-logback-appender) * [logback-elasticsearch-appender](https://github.com/internetitem/logback-elasticsearch-appender) * [logback-more-appenders](https://github.com/sndyuk/logback-more-appenders) * [logback-steno](https://github.com/ArpNetworking/logback-steno) * [logslack](https://github.com/gmethvin/logslack) * [Lessons Learned Writing New Logback Appender](https://logz.io/blog/lessons-learned-writing-new-logback-appender/) * [Extending logstash-logback-encoder](https://zenidas.wordpress.com/recipes/extending-logstash-logback-encoder/) ================================================ FILE: gradle/LICENSE_HEADER ================================================ SPDX-License-Identifier: CC0-1.0 Copyright ${copyrightYear} ${author}. Licensed under the CC0 Public Domain Dedication; You may obtain a copy of the License at http://creativecommons.org/publicdomain/zero/1.0/ ================================================ FILE: gradle/java-publication.gradle ================================================ //Auxiliary jar files required by Maven module publications task sourcesJar(type: Jar, dependsOn: classes) { archiveClassifier = 'sources' from sourceSets.main.allSource } //TODO: java.withSourcesJar(), java.withJavadocJar() task javadocJar(type: Jar, dependsOn: javadoc) { archiveClassifier = 'javadoc' from javadoc.destinationDir } apply plugin: 'maven-publish' publishing { //https://docs.gradle.org/current/userguide/publishing_maven.html publications { maven(MavenPublication) { //name of the publication from components.java artifact sourcesJar artifact javadocJar pom { name = tasks.jar.archiveBaseName description = "Terse Logback is a collection of Logback extensions that shows how to use Logback effectively for structured logging, ringbuffer logging, system instrumentation, and JDBC." url = "https://github.com/tersesystems/terse-logback" licenses { license { name = 'Apache2' url = 'https://github.com/tersesystems/terse-logback/blob/master/LICENSE' } } developers { developer { id = 'tersesystems' name = 'Terse Systems' url = 'https://github.com/tersesystems' } } scm { url = 'https://github.com/tersesystems/terse-logback.git' } } } } repositories { // useful for testing - running "publish" will create artifacts/pom in a local dir maven { url = "$rootDir/build/repo" } } } apply plugin: 'signing' signing { // https://docs.gradle.org/current/userguide/signing_plugin.html // Give up on using PGP_KEY, leverage gpg-agent and release locally useGpgCmd() sign publishing.publications.maven } ================================================ FILE: gradle/release.gradle ================================================ apply plugin: "io.github.gradle-nexus.publish-plugin" //https://github.com/gradle-nexus/publish-plugin/ nexusPublishing { repositories { if (System.getenv("SONATYPE_PWD")) { sonatype { username = System.getenv("SONATYPE_USER") password = System.getenv("SONATYPE_PWD") } } } } allprojects { p -> plugins.withId("java") { p.apply from: "$rootDir/gradle/java-publication.gradle" } } ================================================ FILE: gradle/wrapper/gradle-wrapper.properties ================================================ # # SPDX-License-Identifier: CC0-1.0 # # Copyright 2018-2020 Will Sargent. # # Licensed under the CC0 Public Domain Dedication; # You may obtain a copy of the License at # # http://creativecommons.org/publicdomain/zero/1.0/ # #Fri Mar 06 17:39:40 PST 2020 distributionUrl=https\://services.gradle.org/distributions/gradle-6.8-all.zip distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME ================================================ FILE: gradle.properties ================================================ # # SPDX-License-Identifier: CC0-1.0 # # Copyright 2018-2020 Will Sargent. # # Licensed under the CC0 Public Domain Dedication; # You may obtain a copy of the License at # # http://creativecommons.org/publicdomain/zero/1.0/ # group = com.tersesystems.logback org.gradle.caching=true # Set to true to attach a debugger org.gradle.debug=false # Set the memory size of the daemon to be higher because animalsniffer needs it. #org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 bytebuddyVersion = 1.11.5 junitVersion = 4.12 junitJupiterVersion = 5.0.1 junitVintageVersion = 4.12.1 junitPlatformVersion = 1.0.1 slf4jVersion = 1.7.36 logstashVersion = 6.3 logbackVersion = 1.2.10 configVersion = 1.4.0 zstdVersion = 1.5.2-2 ================================================ FILE: gradlew ================================================ #!/usr/bin/env sh # # Copyright 2015 the original author or authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ############################################################################## ## ## Gradle start up script for UN*X ## ############################################################################## # Attempt to set APP_HOME # Resolve links: $0 may be a link PRG="$0" # Need this for relative symlinks. while [ -h "$PRG" ] ; do ls=`ls -ld "$PRG"` link=`expr "$ls" : '.*-> \(.*\)$'` if expr "$link" : '/.*' > /dev/null; then PRG="$link" else PRG=`dirname "$PRG"`"/$link" fi done SAVED="`pwd`" cd "`dirname \"$PRG\"`/" >/dev/null APP_HOME="`pwd -P`" cd "$SAVED" >/dev/null APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" warn () { echo "$*" } die () { echo echo "$*" echo exit 1 } # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false case "`uname`" in CYGWIN* ) cygwin=true ;; Darwin* ) darwin=true ;; MINGW* ) msys=true ;; NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD="$JAVA_HOME/jre/sh/java" else JAVACMD="$JAVA_HOME/bin/java" fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else JAVACMD="java" which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi # Increase the maximum file descriptors if we can. if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then MAX_FD_LIMIT=`ulimit -H -n` if [ $? -eq 0 ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then MAX_FD="$MAX_FD_LIMIT" fi ulimit -n $MAX_FD if [ $? -ne 0 ] ; then warn "Could not set maximum file descriptor limit: $MAX_FD" fi else warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" fi fi # For Darwin, add options to specify how the application appears in the dock if $darwin; then GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" fi # For Cygwin or MSYS, switch paths to Windows format before running java if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` JAVACMD=`cygpath --unix "$JAVACMD"` # We build the pattern for arguments to be converted via cygpath ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` SEP="" for dir in $ROOTDIRSRAW ; do ROOTDIRS="$ROOTDIRS$SEP$dir" SEP="|" done OURCYGPATTERN="(^($ROOTDIRS))" # Add a user-defined pattern to the cygpath arguments if [ "$GRADLE_CYGPATTERN" != "" ] ; then OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" fi # Now convert the arguments - kludge to limit ourselves to /bin/sh i=0 for arg in "$@" ; do CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` else eval `echo args$i`="\"$arg\"" fi i=`expr $i + 1` done case $i in 0) set -- ;; 1) set -- "$args0" ;; 2) set -- "$args0" "$args1" ;; 3) set -- "$args0" "$args1" "$args2" ;; 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; esac fi # Escape application args save () { for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done echo " " } APP_ARGS=`save "$@"` # Collect all arguments for the java command, following the shell quoting and substitution rules eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" exec "$JAVACMD" "$@" ================================================ FILE: gradlew.bat ================================================ @rem @rem Copyright 2015 the original author or authors. @rem @rem Licensed under the Apache License, Version 2.0 (the "License"); @rem you may not use this file except in compliance with the License. @rem You may obtain a copy of the License at @rem @rem https://www.apache.org/licenses/LICENSE-2.0 @rem @rem Unless required by applicable law or agreed to in writing, software @rem distributed under the License is distributed on an "AS IS" BASIS, @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem @if "%DEBUG%" == "" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @rem @rem ########################################################################## @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @rem Resolve any "." and ".." in APP_HOME to make it shorter. for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if "%ERRORLEVEL%" == "0" goto init echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto init echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :init @rem Get command-line arguments, handling Windows variants if not "%OS%" == "Windows_NT" goto win9xME_args :win9xME_args @rem Slurp the command line arguments. set CMD_LINE_ARGS= set _SKIP=2 :win9xME_args_slurp if "x%~1" == "x" goto execute set CMD_LINE_ARGS=%* :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar @rem Execute Gradle "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% :end @rem End local scope for the variables with windows NT shell if "%ERRORLEVEL%"=="0" goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 exit /b 1 :mainEnd if "%OS%"=="Windows_NT" endlocal :omega ================================================ FILE: logback-audio/gradle.properties ================================================ # # SPDX-License-Identifier: CC0-1.0 # # Copyright 2018-2020 Will Sargent. # # Licensed under the CC0 Public Domain Dedication; # You may obtain a copy of the License at # # http://creativecommons.org/publicdomain/zero/1.0/ # project_description = Logback Audio Markers ================================================ FILE: logback-audio/logback-audio.gradle ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ dependencies { implementation project(':logback-core') implementation project(':logback-classic') implementation group: 'com.googlecode.soundlibs', name: 'mp3spi', version: '1.9.5.4' implementation group: 'com.github.trilarion', name: 'vorbis-support', version: '1.1.0' } ================================================ FILE: logback-audio/src/main/java/com/tersesystems/logback/audio/AudioAppender.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.audio; import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.core.AppenderBase; public class AudioAppender extends AppenderBase implements PlayerAttachable { private Player player; @Override protected void append(ILoggingEvent eventObject) { player.play(); } @Override public void addPlayer(Player player) { this.player = player; } @Override public void clearAllPlayers() { this.player = null; } @Override public void start() { if (player == null) { addError("No player found!"); } else { super.start(); } } } ================================================ FILE: logback-audio/src/main/java/com/tersesystems/logback/audio/AudioMarker.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.audio; import com.tersesystems.logback.classic.TerseBasicMarker; import java.io.InputStream; import java.net.URL; import java.nio.file.Path; public class AudioMarker extends TerseBasicMarker implements Player { private static final String MARKER_NAME = "TS_AUDIO_MARKER"; private final Player player; public AudioMarker(URL url) { super(MARKER_NAME); player = SimplePlayer.fromURL(url); } public AudioMarker(Path path) { super(MARKER_NAME); player = SimplePlayer.fromPath(path); } public AudioMarker(InputStream inputStream) { super(MARKER_NAME); player = SimplePlayer.fromInputStream(inputStream); } public AudioMarker(Player player) { super(MARKER_NAME); this.player = player; } public void play() { player.play(); } } ================================================ FILE: logback-audio/src/main/java/com/tersesystems/logback/audio/AudioMarkerAppender.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.audio; import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.core.AppenderBase; import java.util.Iterator; import org.slf4j.Marker; public class AudioMarkerAppender extends AppenderBase { @Override protected void append(ILoggingEvent eventObject) { writePlayerMarkerIfNecessary(eventObject.getMarker()); } private void writePlayerMarkerIfNecessary(Marker marker) { if (marker != null) { if (isPlayerMarker(marker)) { ((Player) marker).play(); } if (marker.hasReferences()) { for (Iterator i = marker.iterator(); i.hasNext(); ) { writePlayerMarkerIfNecessary(i.next()); } } } } private static boolean isPlayerMarker(Marker marker) { return marker instanceof Player; } } ================================================ FILE: logback-audio/src/main/java/com/tersesystems/logback/audio/FilePlayer.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.audio; import ch.qos.logback.core.spi.ContextAwareBase; import ch.qos.logback.core.spi.LifeCycle; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; public class FilePlayer extends ContextAwareBase implements Player, LifeCycle { private String file; private Path path; private volatile boolean started = false; public FilePlayer() {} public void setFile(String file) { this.file = file; } @Override public void play() { SimplePlayer.fromPath(path).play(); } @Override public void start() { path = Paths.get(file); if (Files.exists(path)) { started = true; } else { addError(String.format("Path %s does not exist!", path)); started = false; } } @Override public void stop() { path = null; started = false; } @Override public boolean isStarted() { return started; } } ================================================ FILE: logback-audio/src/main/java/com/tersesystems/logback/audio/PlayMethods.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.audio; import java.io.IOException; import java.util.function.Supplier; import javax.sound.sampled.*; public interface PlayMethods { default void play(Supplier supplier) { // https://docs.oracle.com/javase/tutorial/sound/playing.html try (final AudioInputStream in = supplier.get()) { AudioFormat baseFormat = in.getFormat(); AudioFormat targetFormat = new AudioFormat( AudioFormat.Encoding.PCM_SIGNED, baseFormat.getSampleRate(), 16, baseFormat.getChannels(), baseFormat.getChannels() * 2, baseFormat.getSampleRate(), false); try (final AudioInputStream dataIn = AudioSystem.getAudioInputStream(targetFormat, in)) { DataLine.Info info = new DataLine.Info(Clip.class, targetFormat); Clip clip = (Clip) AudioSystem.getLine(info); if (clip != null) { clip.addLineListener( event -> { if (event.getType() == LineEvent.Type.STOP) clip.close(); }); clip.open(dataIn); clip.start(); } } } catch (LineUnavailableException | IOException e) { throw new IllegalStateException(e); } } } ================================================ FILE: logback-audio/src/main/java/com/tersesystems/logback/audio/Player.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.audio; /** This can play audio sounds. */ public interface Player { void play(); } ================================================ FILE: logback-audio/src/main/java/com/tersesystems/logback/audio/PlayerAction.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.audio; import ch.qos.logback.core.Context; import ch.qos.logback.core.joran.action.Action; import ch.qos.logback.core.joran.spi.ActionException; import ch.qos.logback.core.joran.spi.InterpretationContext; import ch.qos.logback.core.spi.ContextAwareBase; import ch.qos.logback.core.spi.LifeCycle; import ch.qos.logback.core.util.OptionHelper; import org.xml.sax.Attributes; public class PlayerAction extends Action { Player player; private boolean inError = false; @Override public void begin(InterpretationContext ic, String localName, Attributes attributes) throws ActionException { Object o = ic.peekObject(); if (!(o instanceof PlayerAttachable)) { String errMsg = "Could not find a PlayerAttachable at the top of execution stack. Near [" + localName + "] line " + getLineNumber(ic); inError = true; addInfo(errMsg); // This can trigger in an "if" block from janino, so it may not be serious... return; } PlayerAttachable playerAttachable = (PlayerAttachable) o; String className = attributes.getValue(CLASS_ATTRIBUTE); if (OptionHelper.isEmpty(className)) { addError("Missing class name for player. Near [" + localName + "] line " + getLineNumber(ic)); inError = true; return; } try { addInfo("About to instantiate player of type [" + className + "]"); player = (Player) OptionHelper.instantiateByClassName(className, Player.class, context); Context icContext = ic.getContext(); if (player instanceof ContextAwareBase) { ((ContextAwareBase) player).setContext(icContext); } ic.pushObject(player); } catch (Exception oops) { inError = true; addError("Could not create player.", oops); throw new ActionException(oops); } playerAttachable.addPlayer(player); } @Override public void end(InterpretationContext ic, String name) throws ActionException { if (inError) { return; } if (player instanceof LifeCycle) { ((LifeCycle) player).start(); } Object o = ic.peekObject(); if (o != player) { addWarn("The object at the end of the stack is not the player pushed earlier."); } else { ic.popObject(); } } } ================================================ FILE: logback-audio/src/main/java/com/tersesystems/logback/audio/PlayerAttachable.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.audio; public interface PlayerAttachable { void addPlayer(Player player); void clearAllPlayers(); } ================================================ FILE: logback-audio/src/main/java/com/tersesystems/logback/audio/PlayerConverter.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.audio; import ch.qos.logback.classic.pattern.ClassicConverter; import ch.qos.logback.classic.spi.ILoggingEvent; import java.util.Iterator; import org.slf4j.Marker; public class PlayerConverter extends ClassicConverter { @Override public String convert(ILoggingEvent event) { writePlayerMarkerIfNecessary(event.getMarker()); return null; } private void writePlayerMarkerIfNecessary(Marker marker) { if (marker != null) { if (isPlayerMarker(marker)) { ((Player) marker).play(); } if (marker.hasReferences()) { for (Iterator i = marker.iterator(); i.hasNext(); ) { writePlayerMarkerIfNecessary(i.next()); } } } } private static boolean isPlayerMarker(Marker marker) { return marker instanceof Player; } } ================================================ FILE: logback-audio/src/main/java/com/tersesystems/logback/audio/PlayerException.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.audio; public class PlayerException extends RuntimeException { public PlayerException() {} public PlayerException(String s) { super(s); } public PlayerException(String s, Throwable throwable) { super(s, throwable); } public PlayerException(Throwable throwable) { super(throwable); } public PlayerException(String s, Throwable throwable, boolean b, boolean b1) { super(s, throwable, b, b1); } } ================================================ FILE: logback-audio/src/main/java/com/tersesystems/logback/audio/ResourcePlayer.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.audio; import ch.qos.logback.core.spi.ContextAwareBase; import ch.qos.logback.core.spi.LifeCycle; import java.net.URL; public class ResourcePlayer extends ContextAwareBase implements Player, LifeCycle { private String resource; private URL resourceURL; private volatile boolean started = false; public ResourcePlayer() {} public void setResource(String resource) { this.resource = resource; } @Override public void play() { try { SimplePlayer.fromURL(resourceURL).play(); } catch (Exception e) { addError(String.format("Cannot play resource %s", resourceURL), e); } } @Override public void start() { resourceURL = getClass().getResource(resource); if (resourceURL != null) { started = true; } else { addError(String.format("Resource %s does not exist!", resource)); started = false; } } @Override public void stop() { resource = null; started = false; } @Override public boolean isStarted() { return started; } } ================================================ FILE: logback-audio/src/main/java/com/tersesystems/logback/audio/SimplePlayer.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.audio; import static javax.sound.sampled.AudioSystem.getAudioInputStream; import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.nio.file.Path; import java.util.function.Supplier; import javax.sound.sampled.AudioInputStream; import javax.sound.sampled.UnsupportedAudioFileException; public class SimplePlayer implements PlayMethods, Player { private final Supplier supplier; protected SimplePlayer(Supplier supplier) { this.supplier = supplier; } public static Player fromURL(URL url) { return new SimplePlayer( () -> { try { return getAudioInputStream(url); } catch (UnsupportedAudioFileException | IOException e) { throw new PlayerException(e); } }); } public static Player fromPath(Path path) { return new SimplePlayer( () -> { try { return getAudioInputStream(path.toFile()); } catch (UnsupportedAudioFileException | IOException e) { throw new PlayerException(e); } }); } public static Player fromInputStream(InputStream inputStream) { return new SimplePlayer( () -> { try { return getAudioInputStream(inputStream); } catch (UnsupportedAudioFileException | IOException e) { throw new PlayerException(e); } }); } @Override public void play() { play(supplier); } } ================================================ FILE: logback-audio/src/main/java/com/tersesystems/logback/audio/SystemPlayer.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.audio; import java.awt.*; public class SystemPlayer implements Player { @Override public void play() { Toolkit toolkit = Toolkit.getDefaultToolkit(); final Runnable exclam = (Runnable) toolkit.getDesktopProperty("win.sound.exclamation"); if (exclam != null) { exclam.run(); } else { toolkit.beep(); } } } ================================================ FILE: logback-audio/src/main/java/com/tersesystems/logback/audio/URLPlayer.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.audio; import ch.qos.logback.core.spi.ContextAwareBase; import java.net.URL; public class URLPlayer extends ContextAwareBase implements Player { private URL url; public URLPlayer() {} public void URLPlayer(URL url) { this.url = url; } @Override public void play() { SimplePlayer.fromURL(url).play(); } } ================================================ FILE: logback-audio/src/test/java/com/tersesystems/logback/audio/TestAudio.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.audio; import ch.qos.logback.classic.Logger; import ch.qos.logback.classic.LoggerContext; import ch.qos.logback.classic.joran.JoranConfigurator; import ch.qos.logback.core.joran.spi.JoranException; import java.net.URL; import java.nio.file.Path; import java.nio.file.Paths; import org.junit.Test; import org.slf4j.Marker; public class TestAudio { @Test public void testMarkerWithURL() throws JoranException, InterruptedException { LoggerContext context = new LoggerContext(); URL resource = getClass().getResource("/logback-with-marker-appender.xml"); JoranConfigurator configurator = new JoranConfigurator(); configurator.setContext(context); configurator.doConfigure(resource); Logger logger = context.getLogger("some.random.Logger"); URL audioURL = getClass().getResource("/bark.ogg"); Marker marker = new AudioMarker(audioURL); logger.info(marker, "Bark!"); Thread.sleep(1000); } // Can't keep a path steady with different starting directories.. @Test public void testMarkerWithURLWithConverter() throws JoranException, InterruptedException { LoggerContext context = new LoggerContext(); URL resource = getClass().getResource("/logback-with-converter.xml"); JoranConfigurator configurator = new JoranConfigurator(); configurator.setContext(context); configurator.doConfigure(resource); Logger logger = context.getLogger("some.random.Logger"); URL audioURL = getClass().getResource("/bark.ogg"); Marker marker = new AudioMarker(audioURL); logger.info(marker, "Bark!"); Thread.sleep(1000); } // Can't keep a path steady with different starting directories.. // @Test public void testMarkerWithPath() throws JoranException, InterruptedException { LoggerContext context = new LoggerContext(); URL resource = getClass().getResource("/logback-with-marker-appender.xml"); JoranConfigurator configurator = new JoranConfigurator(); configurator.setContext(context); configurator.doConfigure(resource); Logger logger = context.getLogger("some.random.Logger"); String path = System.getProperty("user.dir"); Path audioPath = Paths.get(path, "src", "test", "resources", "bark.ogg"); Marker marker = new AudioMarker(audioPath); logger.warn(marker, "Bark!"); Thread.sleep(1000); } } ================================================ FILE: logback-audio/src/test/java/com/tersesystems/logback/audio/TestNested.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.audio; import ch.qos.logback.classic.Logger; import ch.qos.logback.classic.LoggerContext; import ch.qos.logback.classic.joran.JoranConfigurator; import ch.qos.logback.core.joran.spi.JoranException; import java.net.URL; import org.junit.Test; public class TestNested { @Test public void testLogger() throws JoranException, InterruptedException { LoggerContext context = new LoggerContext(); URL resource = getClass().getResource("/logback-with-nested-appender.xml"); JoranConfigurator configurator = new JoranConfigurator(); configurator.setContext(context); configurator.doConfigure(resource); Logger logger = context.getLogger("some.random.Logger"); for (int i = 0; i < 1; i++) { logger.trace("TRACE"); } for (int i = 0; i < 2; i++) { logger.debug("DEBUG"); } for (int i = 0; i < 2; i++) { logger.info("INFO"); } for (int i = 0; i < 2; i++) { logger.warn("WARN"); } logger.error("ERROR"); } } ================================================ FILE: logback-audio/src/test/resources/logback-with-converter.xml ================================================ %-5relative %-5level %logger{35} %audio - %msg%n ================================================ FILE: logback-audio/src/test/resources/logback-with-marker-appender.xml ================================================ %-5relative %-5level %logger{35} - %msg%n ================================================ FILE: logback-audio/src/test/resources/logback-with-nested-appender.xml ================================================ %-5relative %-5level %logger{35} - %msg%n TRACE ACCEPT DENY /bark.ogg DEBUG ACCEPT DENY /drip.ogg INFO ACCEPT DENY /glass.ogg WARN ACCEPT DENY /message.ogg ERROR ACCEPT DENY /sample.ogg ================================================ FILE: logback-budget/gradle.properties ================================================ # # SPDX-License-Identifier: CC0-1.0 # # Copyright 2018-2020 Will Sargent. # # Licensed under the CC0 Public Domain Dedication; # You may obtain a copy of the License at # # http://creativecommons.org/publicdomain/zero/1.0/ # project_description = Logback Budget ================================================ FILE: logback-budget/logback-budget.gradle ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ dependencies { implementation project(':logback-core') implementation project(':logback-classic') // https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 implementation group: 'org.apache.commons', name: 'commons-lang3', version: '3.9' } ================================================ FILE: logback-budget/src/main/java/com/tersesystems/logback/budget/BudgetEvaluator.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.budget; import ch.qos.logback.classic.Level; import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.core.boolex.EventEvaluatorBase; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; import org.apache.commons.lang3.concurrent.CircuitBreaker; import org.apache.commons.lang3.concurrent.EventCountCircuitBreaker; /** Returns true if logging the event is within budget, false otherwise. */ public class BudgetEvaluator extends EventEvaluatorBase implements BudgetRuleAttachable { private List budgetRules = new ArrayList<>(); private Map> levelRules = new HashMap<>(); @Override public void start() { for (BudgetRule budgetRule : budgetRules) { CircuitBreaker breaker = createCircuitBreaker(budgetRule); levelRules.put(budgetRule.getName(), breaker); } super.start(); } private CircuitBreaker createCircuitBreaker(BudgetRule budgetRule) { addInfo("budgetRule = " + budgetRule); final int threshold = budgetRule.getThreshold(); final long checkInterval = budgetRule.getInterval(); String timeUnit = budgetRule.getTimeUnit(); if (timeUnit == null) { addError("No time unit found for budget rule"); throw new IllegalStateException("No time unit found for budget rule " + budgetRule); } else { TimeUnit checkUnit; try { checkUnit = TimeUnit.valueOf(timeUnit.toUpperCase()); } catch (IllegalArgumentException iae) { try { // Try adding an S on the end checkUnit = TimeUnit.valueOf(timeUnit.toUpperCase() + "S"); } catch (Exception e) { addError( "Invalid time unit found for budget rule, use java.util.concurrent.TimeUnit enums " + budgetRule, e); throw e; } } return new EventCountCircuitBreaker(threshold, checkInterval, checkUnit); } } @Override public boolean evaluate(ILoggingEvent event) { if (levelRules.isEmpty()) { return true; // not applicable } Level level = event.getLevel(); CircuitBreaker breaker = levelRules.get(level.levelStr); if (breaker == null) { return true; // does not apply to this level } if (breaker.checkState()) { return breaker.incrementAndCheckState(1); } else { return false; } } @Override public void addBudgetRule(BudgetRule budget) { budgetRules.add(budget); } @Override public void clearAllBudgetRules() { budgetRules.clear(); } } ================================================ FILE: logback-budget/src/main/java/com/tersesystems/logback/budget/BudgetRule.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.budget; import org.apache.commons.lang3.builder.ToStringBuilder; public class BudgetRule { private String name; private int threshold; private long interval; private String timeUnit; public BudgetRule() {} public String getName() { return name; } public void setName(String name) { this.name = name; } public int getThreshold() { return threshold; } public void setThreshold(int threshold) { this.threshold = threshold; } public long getInterval() { return interval; } public void setInterval(long interval) { this.interval = interval; } public String getTimeUnit() { return timeUnit; } public void setTimeUnit(String timeUnit) { this.timeUnit = timeUnit; } @Override public String toString() { return ToStringBuilder.reflectionToString(this); } } ================================================ FILE: logback-budget/src/main/java/com/tersesystems/logback/budget/BudgetRuleAction.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.budget; import ch.qos.logback.core.joran.action.Action; import ch.qos.logback.core.joran.spi.ActionException; import ch.qos.logback.core.joran.spi.InterpretationContext; import ch.qos.logback.core.util.OptionHelper; import org.xml.sax.Attributes; public class BudgetRuleAction extends Action { private BudgetRule budgetRule; private boolean inError = false; @Override public void begin(InterpretationContext ic, String localName, Attributes attributes) throws ActionException { Object o = ic.peekObject(); if (!(o instanceof BudgetRuleAttachable)) { String errMsg = "Could not find a BudgetRuleAttachable at the top of execution stack. Near [" + localName + "] line " + getLineNumber(ic); inError = true; addInfo(errMsg); // This can trigger in an "if" block from janino, so it may not be serious... return; } BudgetRuleAttachable budgetRuleAttachable = (BudgetRuleAttachable) o; String className = attributes.getValue(CLASS_ATTRIBUTE); if (OptionHelper.isEmpty(className)) { className = BudgetRule.class.getName(); } String name = attributes.getValue(NAME_ATTRIBUTE); long interval = Long.parseLong(attributes.getValue("interval")); int threshold = Integer.parseInt(attributes.getValue("threshold")); String timeUnit = attributes.getValue("timeUnit"); try { addInfo("About to instantiate budgetRule of type [" + className + "]"); budgetRule = (BudgetRule) OptionHelper.instantiateByClassName(className, BudgetRule.class, context); budgetRule.setName(name); budgetRule.setInterval(interval); budgetRule.setThreshold(threshold); budgetRule.setTimeUnit(timeUnit); ic.pushObject(budgetRule); } catch (Exception oops) { inError = true; addError("Could not create budgetRule.", oops); throw new ActionException(oops); } budgetRuleAttachable.addBudgetRule(budgetRule); } @Override public void end(InterpretationContext ic, String name) throws ActionException { if (inError) { return; } Object o = ic.peekObject(); if (o != budgetRule) { addWarn("The object at the end of the stack is not the budgetRule pushed earlier."); } else { ic.popObject(); } } } ================================================ FILE: logback-budget/src/main/java/com/tersesystems/logback/budget/BudgetRuleAttachable.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.budget; public interface BudgetRuleAttachable { void addBudgetRule(BudgetRule budget); void clearAllBudgetRules(); } ================================================ FILE: logback-budget/src/main/java/com/tersesystems/logback/budget/BudgetTurboFilter.java ================================================ package com.tersesystems.logback.budget; import ch.qos.logback.classic.Level; import ch.qos.logback.classic.Logger; import ch.qos.logback.classic.turbo.TurboFilter; import ch.qos.logback.core.spi.FilterReply; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; import org.apache.commons.lang3.concurrent.CircuitBreaker; import org.apache.commons.lang3.concurrent.EventCountCircuitBreaker; import org.slf4j.Marker; public class BudgetTurboFilter extends TurboFilter implements BudgetRuleAttachable { private List budgetRules = new ArrayList<>(); private Map> levelRules = new HashMap<>(); public void start() { for (BudgetRule budgetRule : budgetRules) { CircuitBreaker breaker = createCircuitBreaker(budgetRule); levelRules.put(budgetRule.getName(), breaker); } super.start(); } private CircuitBreaker createCircuitBreaker(BudgetRule budgetRule) { addInfo("budgetRule = " + budgetRule); final int threshold = budgetRule.getThreshold(); final long checkInterval = budgetRule.getInterval(); String timeUnit = budgetRule.getTimeUnit(); if (timeUnit == null) { addError("No time unit found for budget rule"); throw new IllegalStateException("No time unit found for budget rule " + budgetRule); } else { TimeUnit checkUnit; try { checkUnit = TimeUnit.valueOf(timeUnit.toUpperCase()); } catch (IllegalArgumentException iae) { try { // Try adding an S on the end checkUnit = TimeUnit.valueOf(timeUnit.toUpperCase() + "S"); } catch (Exception e) { addError( "Invalid time unit found for budget rule, use java.util.concurrent.TimeUnit enums " + budgetRule, e); throw e; } } return new EventCountCircuitBreaker(threshold, checkInterval, checkUnit); } } @Override public FilterReply decide( Marker marker, Logger logger, Level level, String format, Object[] params, Throwable t) { if (levelRules.isEmpty()) { return getOnMatch(); // not applicable } CircuitBreaker breaker = levelRules.get(level.levelStr); if (breaker == null) { return getOnMatch(); // does not apply to this level } if (breaker.checkState()) { return breaker.incrementAndCheckState(1) ? getOnMatch() : getOnMismatch(); } else { return getOnMismatch(); } } @Override public void addBudgetRule(BudgetRule budget) { budgetRules.add(budget); } @Override public void clearAllBudgetRules() { budgetRules.clear(); } protected FilterReply onMatch = FilterReply.NEUTRAL; protected FilterReply onMismatch = FilterReply.DENY; public final void setOnMatch(FilterReply reply) { this.onMatch = reply; } public final void setOnMismatch(FilterReply reply) { this.onMismatch = reply; } public final FilterReply getOnMatch() { return onMatch; } public final FilterReply getOnMismatch() { return onMismatch; } } ================================================ FILE: logback-budget/src/test/java/com.tersesystems.logback.budget/BudgetEvaluatorTest.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.budget; import ch.qos.logback.classic.Logger; import ch.qos.logback.classic.LoggerContext; import ch.qos.logback.classic.joran.JoranConfigurator; import ch.qos.logback.core.joran.spi.JoranException; import java.net.URL; import org.junit.Test; public class BudgetEvaluatorTest { @Test public void testBudget() throws JoranException, InterruptedException { LoggerContext context = new LoggerContext(); URL resource = getClass().getResource("/logback-budget.xml"); JoranConfigurator configurator = new JoranConfigurator(); configurator.setContext(context); configurator.doConfigure(resource); Logger logger = context.getLogger("some.random.Logger"); for (int i = 0; i < 10; i++) { logger.info("Hello world"); } Thread.sleep(1000); logger.info("Hello world"); } } ================================================ FILE: logback-budget/src/test/java/com.tersesystems.logback.budget/BudgetTurboFilterTest.java ================================================ package com.tersesystems.logback.budget; import ch.qos.logback.classic.Logger; import ch.qos.logback.classic.LoggerContext; import ch.qos.logback.classic.joran.JoranConfigurator; import ch.qos.logback.core.joran.spi.JoranException; import java.net.URL; import org.junit.Test; public class BudgetTurboFilterTest { @Test public void testBudget() throws JoranException, InterruptedException { LoggerContext context = new LoggerContext(); URL resource = getClass().getResource("/logback-turbofilter.xml"); JoranConfigurator configurator = new JoranConfigurator(); configurator.setContext(context); configurator.doConfigure(resource); Logger logger = context.getLogger("some.random.Logger"); for (int i = 0; i < 10; i++) { logger.info("Hello world"); } Thread.sleep(1000); logger.info("Hello world"); } } ================================================ FILE: logback-budget/src/test/resources/logback-budget.xml ================================================ INFO 5 1 second DENY NEUTRAL %-5relative %-5level %logger{35} - %msg%n ================================================ FILE: logback-budget/src/test/resources/logback-turbofilter.xml ================================================ INFO 5 1 second DENY NEUTRAL %-5relative %-5level %logger{35} - %msg%n ================================================ FILE: logback-bytebuddy/gradle.properties ================================================ # # SPDX-License-Identifier: CC0-1.0 # # Copyright 2018-2020 Will Sargent. # # Licensed under the CC0 Public Domain Dedication; # You may obtain a copy of the License at # # http://creativecommons.org/publicdomain/zero/1.0/ # project_description = Logback ByteBuddy Instrumentation ================================================ FILE: logback-bytebuddy/logback-bytebuddy.gradle ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ plugins { id 'com.github.johnrengelman.shadow' version '5.1.0' } dependencies { implementation "net.bytebuddy:byte-buddy:$bytebuddyVersion" implementation "com.typesafe:config:${configVersion}" // We cannot allow dependencies from logback-tracing in here. shadow project(":logback-tracing") shadow "org.slf4j:slf4j-api:$slf4jVersion" shadow "ch.qos.logback:logback-classic:$logbackVersion" shadow "ch.qos.logback:logback-core:$logbackVersion" shadow "net.logstash.logback:logstash-logback-encoder:$logstashVersion" shadow "com.google.auto.value:auto-value-annotations:1.6.2" shadow group: 'com.fasterxml.uuid', name: 'java-uuid-generator', version: '3.2.0' // You'll need this, but better to use the version already defined in project testImplementation "net.bytebuddy:byte-buddy-agent:$bytebuddyVersion" testImplementation "ch.qos.logback:logback-classic:$logbackVersion" } jar { manifest { attributes "Premain-Class": "com.tersesystems.logback.bytebuddy.LogbackInstrumentationAgent" attributes "Agent-Class": "com.tersesystems.logback.bytebuddy.LogbackInstrumentationAgent" attributes "Can-Redefine-Classes": "true" attributes "Can-Retransform-Classes": "true" } } shadowJar { archiveFileName = "$archiveBaseName-$archiveVersion.$archiveExtension" // Shade typesafe config and bytebuddy itself since this is used in bootstrap classloader // and so would be visible to everything downstream relocate 'com.typesafe', 'com.tersesystems.logback.shadow.typesafe' relocate 'net.bytebuddy', 'com.tersesystems.logback.shadow.bytebuddy' exclude '**/module-info.class' dependencies { exclude(dependency("com.fasterxml.jackson.core:")) exclude(dependency("com.fasterxml.jackson.annotation:")) exclude(dependency("com.fasterxml.jackson.databind:")) exclude(dependency("net.logstash.logback:logstash-logback-encoder:$logstashVersion")) exclude(dependency("org.slf4j:slf4j-api:$slf4jVersion")) } } artifacts { archives shadowJar } ================================================ FILE: logback-bytebuddy/src/main/java/com/tersesystems/logback/bytebuddy/AdviceConfig.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.bytebuddy; import static java.util.Collections.singletonList; import static net.bytebuddy.matcher.ElementMatchers.*; import java.util.*; import net.bytebuddy.description.method.MethodDescription; import net.bytebuddy.description.method.MethodList; import net.bytebuddy.description.type.TypeDescription; import net.bytebuddy.matcher.ElementMatcher; public class AdviceConfig { private final ClassLoader classLoader; private final Collection traceConfigCollection; private final String serviceName; private TraceConfig traceConfig; public AdviceConfig(ClassLoader classLoader, String serviceName) { this.classLoader = classLoader; this.serviceName = serviceName; traceConfigCollection = new HashSet<>(); } public String getServiceName() { return serviceName; } public void addTrace(String className, List methodNames) throws Exception { TraceConfig traceConfig = TraceConfig.create(classLoader, className, methodNames); traceConfigCollection.add(traceConfig); } public void addTrace(String className) throws Exception { TraceConfig traceConfig = TraceConfig.create(classLoader, className); traceConfigCollection.add(traceConfig); } public List classNames() { return getTraceConfig().classNames; } public ElementMatcher methods() { return getTraceConfig().methods(); } public ElementMatcher types() { return getTraceConfig().types(); } private TraceConfig getTraceConfig() { if (traceConfig == null) { traceConfig = traceConfigCollection.stream().reduce(TraceConfig::join).get(); } return traceConfig; } static final class TraceConfig { private final List classNames; private final ElementMatcher.Junction typeMatcher; private final ElementMatcher.Junction methodMatcher; private TraceConfig( List classNames, ElementMatcher.Junction typeMatcher, ElementMatcher.Junction methodMatcher) { this.typeMatcher = Objects.requireNonNull(typeMatcher); this.methodMatcher = Objects.requireNonNull(methodMatcher); this.classNames = classNames; } static TraceConfig create(ClassLoader classLoader, String className, List methodNames) throws Exception { TypeDescription aClass = createTypeDescription(classLoader, className); return new TraceConfig( singletonList(className), is(aClass), methodNames.stream() .map(m -> named(m).and(isDeclaredBy(aClass)).and(not(isNative().or(isConstructor())))) .reduce(none(), ElementMatcher.Junction::or)); } static TraceConfig create(ClassLoader classLoader, String className) throws Exception { TypeDescription aClass = createTypeDescription(classLoader, className); // Get a list of all non-native methods from the class, with no constructor. MethodList methods = aClass.getDeclaredMethods().filter(not(isNative().or(isConstructor()))); return new TraceConfig(singletonList(className), is(aClass), anyOf(methods)); } public List classNames() { return this.classNames; } public ElementMatcher.Junction methods() { return methodMatcher; } public ElementMatcher.Junction types() { return typeMatcher; } public TraceConfig join(TraceConfig other) { final ElementMatcher.Junction methodMatcher = methods().or(other.methods()); final ElementMatcher.Junction typeMatcher = types().or(other.types()); List classNames = new ArrayList<>(classNames()); classNames.addAll(other.classNames()); return new TraceConfig(classNames, typeMatcher, methodMatcher); } private static TypeDescription createTypeDescription(ClassLoader classLoader, String className) throws Exception { return new TypeDescription.ForLoadedType(classLoader.loadClass(className)); } } @Override public String toString() { return "AdviceConfig{service-name = " + getServiceName() + ", methods=" + methods() + ", types='" + types() + '\'' + '}'; } } ================================================ FILE: logback-bytebuddy/src/main/java/com/tersesystems/logback/bytebuddy/LogbackInstrumentationAgent.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.bytebuddy; import static java.util.Collections.singletonMap; import static net.bytebuddy.dynamic.ClassFileLocator.ForClassLoader.read; import static net.bytebuddy.dynamic.loading.ClassInjector.UsingInstrumentation.Target.BOOTSTRAP; import java.io.File; import java.io.IOException; import java.lang.instrument.Instrumentation; import java.nio.file.Files; import net.bytebuddy.description.type.TypeDescription; import net.bytebuddy.dynamic.loading.ClassInjector; /** * The agent class. This has the magic "premain" and "agentmain" methods for the Java * Instrumentation API. */ public class LogbackInstrumentationAgent { private static final Class INSTRUMENTATION_ADVICE_CLASS = LoggingInstrumentationAdvice.class; public static void premain(String arg, Instrumentation instrumentation) throws Exception { injectBootstrapClasses(instrumentation); LoggingInstrumentationAdvice logbackInst = (LoggingInstrumentationAdvice) INSTRUMENTATION_ADVICE_CLASS.newInstance(); boolean debug = parseDebug(arg); logbackInst.initialize(instrumentation, debug); } private static boolean parseDebug(String arg) { return "debug".equalsIgnoreCase(arg); } public static void agentmain(String arg, Instrumentation instrumentation) throws Exception { injectBootstrapClasses(instrumentation); boolean debug = parseDebug(arg); LoggingInstrumentationAdvice logbackInst = (LoggingInstrumentationAdvice) INSTRUMENTATION_ADVICE_CLASS.newInstance(); logbackInst.initialize(instrumentation, debug); } /** * Loads the advice class into the bootstrap target of instrumentation. * * @param instrumentation * @throws IOException */ private static void injectBootstrapClasses(Instrumentation instrumentation) throws IOException { File tempDir = Files.createTempDirectory("logback-bytebuddy").toFile(); tempDir.deleteOnExit(); // Inject the instrumentation advice class directly byte[] classData = read(INSTRUMENTATION_ADVICE_CLASS); TypeDescription typeDescription = new TypeDescription.ForLoadedType(INSTRUMENTATION_ADVICE_CLASS); ClassInjector classInjector = ClassInjector.UsingInstrumentation.of(tempDir, BOOTSTRAP, instrumentation); classInjector.inject(singletonMap(typeDescription, classData)); } } ================================================ FILE: logback-bytebuddy/src/main/java/com/tersesystems/logback/bytebuddy/LoggingInstrumentationAdvice.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.bytebuddy; import com.tersesystems.logback.bytebuddy.impl.SystemFlow; import com.typesafe.config.*; import java.io.File; import java.lang.instrument.Instrumentation; import java.lang.reflect.Method; import java.util.*; import java.util.stream.Collectors; import net.bytebuddy.agent.builder.AgentBuilder; import net.bytebuddy.asm.Advice; import net.bytebuddy.implementation.bytecode.assign.Assigner; import net.bytebuddy.matcher.ElementMatcher; import net.bytebuddy.matcher.ElementMatchers; import net.bytebuddy.matcher.StringMatcher; /** The code to be added on entry / exit to the methods under instrumentation. */ public class LoggingInstrumentationAdvice { private static final String LOGBACK = "logback"; private static final String LOGBACK_TEST = "logback-test"; private static final String LOGBACK_REFERENCE_CONF = "logback-reference.conf"; private static final String CONFIG_FILE_PROPERTY = "terse.logback.configurationFile"; private static final ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader(); // We need to load the implementation of enter / exit methods from the system classloader, // so that we don't end up hauling SLF4J impl factory into bootstrap classloader, which // will hopelessly confuse the JVM. public static Method enterMethod; static { try { String className = "com.tersesystems.logback.bytebuddy.impl.Enter"; Class enterClass = systemClassLoader.loadClass(className); enterMethod = enterClass.getMethod("apply", String.class, Object[].class); } catch (Exception e) { e.printStackTrace(); } } public static Method exitMethod; static { try { String className = "com.tersesystems.logback.bytebuddy.impl.Exit"; Class exitClass = systemClassLoader.loadClass(className); exitMethod = exitClass.getMethod("apply", String.class, Object[].class, Throwable.class, Object.class); } catch (Exception e) { e.printStackTrace(); } } void initialize(Instrumentation instrumentation, boolean debug) { try { Config config = generateConfig(systemClassLoader, debug); AdviceConfig adviceConfig = generateAdviceConfig(systemClassLoader, config, debug); if (debug) { System.out.println("Generated Advice Config = " + adviceConfig); } SystemFlow.setServiceName(adviceConfig.getServiceName()); AgentBuilder agentBuilder = new LoggingInstrumentationByteBuddyBuilder() .builderFromConfigWithRetransformation(adviceConfig); // The debugging listener shows what classes are being picked up by the instrumentation if (debug) { AgentBuilder.Listener debugListener = createDebugListener(adviceConfig.classNames()); agentBuilder = agentBuilder.with(debugListener); } agentBuilder.installOn(instrumentation); } catch (Exception e) { e.printStackTrace(); } } // The code here recapitulates the logback-config code, but in a bootstrap classloader. // This does mean that typesafe-config classes are pulled from bootstrap thereafter, but // this is pretty safe. public static Config generateConfig(ClassLoader classLoader, boolean debug) { // Look for logback.json, logback.conf, logback.properties Config systemProperties = ConfigFactory.systemProperties(); String fileName = System.getProperty(CONFIG_FILE_PROPERTY); Config file = ConfigFactory.empty(); if (fileName != null) { file = ConfigFactory.parseFile(new File(fileName)); } Config testResources = ConfigFactory.parseResourcesAnySyntax(classLoader, LOGBACK_TEST); Config resources = ConfigFactory.parseResourcesAnySyntax(classLoader, LOGBACK); Config reference = ConfigFactory.parseResources(classLoader, LOGBACK_REFERENCE_CONF); Config config = systemProperties // Look for a property from system properties first... .withFallback(file) // if we don't find it, then look in an explicitly defined file... .withFallback( testResources) // if not, then if logback-test.conf exists, look for it there... .withFallback(resources) // then look in logback.conf... .withFallback(reference) // and then finally in logback-reference.conf. .resolve(); // Tell config that we want to use ${?ENV_VAR} type stuff. // Add a check to show the config value if nothing is working... if (debug) { String configString = config.root().render(ConfigRenderOptions.defaults()); System.out.println(configString); } return config; } public static AdviceConfig generateAdviceConfig( ClassLoader classLoader, Config config, boolean debug) throws Exception { List configs = new ArrayList<>(); String serviceName = config.getString("logback.bytebuddy.service-name"); AdviceConfig adviceConfig = new AdviceConfig(classLoader, serviceName); Set> entries = config.getConfig("logback.bytebuddy.tracing").entrySet(); for (Map.Entry entry : entries) { String className = clean(entry.getKey()); ConfigValue value = entry.getValue(); if (value.valueType() == ConfigValueType.LIST) { List methodNames = ((List) value.unwrapped()) .stream().map(LoggingInstrumentationAdvice::clean).collect(Collectors.toList()); if (methodNames.size() == 1 && Objects.equals(methodNames.get(0), "*")) { if (debug) { System.out.println("Using wildcard matching for class " + className); } adviceConfig.addTrace(className); } else { adviceConfig.addTrace(className, methodNames); } } else { throw new IllegalStateException("unknown config!"); } } return adviceConfig; } private static String clean(String key) { return key.replaceAll("\"", "").trim(); } private static AgentBuilder.Listener createDebugListener(List classNames) { return new AgentBuilder.Listener.Filtering( stringMatcher(classNames), AgentBuilder.Listener.StreamWriting.toSystemOut()); } public static ElementMatcher.Junction stringMatcher( Collection typeNames) { boolean seen = false; ElementMatcher.Junction acc = ElementMatchers.none(); for (String typeName : typeNames) { StringMatcher stringMatcher = new StringMatcher(typeName, StringMatcher.Mode.EQUALS_FULLY); if (!seen) { seen = true; acc = stringMatcher; } else { acc = acc.or(stringMatcher); } } return acc; } @Advice.OnMethodEnter public static void enter( @Advice.Origin("#t|#m|#d|#s") String origin, @Advice.AllArguments Object[] allArguments) throws Exception { enterMethod.invoke(null, origin, allArguments); } @Advice.OnMethodExit(onThrowable = Throwable.class) public static void exit( @Advice.Origin("#t|#m|#d|#s|#r") String origin, @Advice.AllArguments Object[] allArguments, @Advice.Thrown Throwable thrown, @Advice.Return(typing = Assigner.Typing.DYNAMIC) Object returnValue) throws Exception { exitMethod.invoke(null, origin, allArguments, thrown, returnValue); } } ================================================ FILE: logback-bytebuddy/src/main/java/com/tersesystems/logback/bytebuddy/LoggingInstrumentationByteBuddyBuilder.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.bytebuddy; import static net.bytebuddy.matcher.ElementMatchers.*; import static net.bytebuddy.matcher.ElementMatchers.any; import net.bytebuddy.agent.builder.AgentBuilder; import net.bytebuddy.asm.Advice; import net.bytebuddy.asm.AsmVisitorWrapper; import net.bytebuddy.description.field.FieldDescription; import net.bytebuddy.description.field.FieldList; import net.bytebuddy.description.method.MethodDescription; import net.bytebuddy.description.method.MethodList; import net.bytebuddy.description.type.TypeDescription; import net.bytebuddy.implementation.Implementation; import net.bytebuddy.jar.asm.ClassVisitor; import net.bytebuddy.jar.asm.Label; import net.bytebuddy.jar.asm.MethodVisitor; import net.bytebuddy.jar.asm.Opcodes; import net.bytebuddy.matcher.ElementMatcher; import net.bytebuddy.pool.TypePool; /** Creates byte buddy agent builders with the LoggingInstrumentation advice. */ public class LoggingInstrumentationByteBuddyBuilder { private static final MethodInfoLookup METHOD_INFO_LOOKUP = MethodInfoLookup.getInstance(); private static final Class INSTRUMENTATION_ADVICE_CLASS = LoggingInstrumentationAdvice.class; // Is there a better way to handle upgrades here? // As far as I can tell, we just want the highest number possible. // https://stackoverflow.com/questions/63399682/how-do-i-map-asms-api-version-in-opcodes-to-java-version private static final int ASM_API = Opcodes.ASM9; /** * Creates a builder from the element matchers. * * @param typesMatcher an element matcher for types we should instrument. * @param methodsMatcher an element matcher for the methods in the types that should be * instrumented. * @return the agent builder */ public AgentBuilder builderFromConfig( ElementMatcher typesMatcher, ElementMatcher methodsMatcher) { return new AgentBuilder.Default() .with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION) .with(AgentBuilder.InitializationStrategy.NoOp.INSTANCE) .with(AgentBuilder.TypeStrategy.Default.REDEFINE) .disableClassFormatChanges() // frozen instrumented types .type(typesMatcher) // for these classes... .transform( (builder, type, classLoader, module) -> { // ...apply this advice to these methods. Advice to = Advice.to(INSTRUMENTATION_ADVICE_CLASS); AsmVisitorWrapper on = to.on(methodsMatcher); AsmVisitorWrapper lineWrapper = new LineWrapper(); return builder.visit(lineWrapper).visit(on); }); } static class LineWrapper extends AsmVisitorWrapper.AbstractBase { @Override public ClassVisitor wrap( TypeDescription instrumentedType, ClassVisitor classVisitor, Implementation.Context implementationContext, TypePool typePool, FieldList fields, MethodList methods, int writerFlags, int readerFlags) { return new ClassVisitor(ASM_API, classVisitor) { private String className; private String source; @Override public void visitSource(String source, String debug) { this.source = source; super.visitSource(source, debug); } @Override public void visit( int version, int access, String name, String signature, String superName, String[] interfaces) { this.className = name != null ? name.replace('/', '.') : null; super.visit(version, access, name, signature, superName, interfaces); } @Override public MethodVisitor visitMethod(int access, String n, String d, String s, String[] e) { MethodVisitor methodVisitor = super.visitMethod(access, n, d, s, e); return new MethodVisitor(Opcodes.ASM5, methodVisitor) { int line; final MethodInfo methodInfo = new MethodInfo(n, d, source); boolean isStart = false; @Override public void visitCode() { isStart = true; super.visitCode(); } @Override public void visitLineNumber(int line, Label start) { if (isStart) { methodInfo.setStartLine(line); isStart = false; } this.line = line; super.visitLineNumber(line, start); } @Override public void visitEnd() { methodInfo.setEndLine(line); METHOD_INFO_LOOKUP.add(className, methodInfo); super.visitEnd(); } }; } }; } } /** * Use this method if you want to redefine system classloader classes. * * @param typesMatcher an element matcher for types we should instrument. * @param methodsMatcher an element matcher for the methods in the types that should be * instrumented. * @return agent builder with ignore and RETRANSFORMATION set. */ public AgentBuilder builderFromConfigWithRetransformation( ElementMatcher typesMatcher, ElementMatcher methodsMatcher) { return withSystemClassLoaderMatching(builderFromConfig(typesMatcher, methodsMatcher)); } protected AgentBuilder withSystemClassLoaderMatching(AgentBuilder builder) { return builder .ignore(ignoreMatchers()) // do not ignore system classes .with( AgentBuilder.RedefinitionStrategy .RETRANSFORMATION); // try to retransform already loaded classes } public AgentBuilder.RawMatcher.ForElementMatchers ignoreMatchers() { ElementMatcher.Junction matchers = nameStartsWith("net.bytebuddy.") .or(nameStartsWith("com.tersesystems.logback.bytebuddy.")) .or(nameStartsWith("org.slf4j.")) .or(nameStartsWith("ch.qos.logback.")) .or(isSynthetic()); return new AgentBuilder.RawMatcher.ForElementMatchers(matchers, any(), any()); } public AgentBuilder builderFromConfig( ElementMatcher typesMatcher, ElementMatcher methodsMatcher, AgentBuilder.Listener listener) { return builderFromConfig(typesMatcher, methodsMatcher).with(listener); } public AgentBuilder builderFromConfig(AdviceConfig c) { return builderFromConfig(c.types(), c.methods()); } public AgentBuilder builderFromConfigWithRetransformation(AdviceConfig c) { return builderFromConfigWithRetransformation(c.types(), c.methods()); } } ================================================ FILE: logback-bytebuddy/src/main/java/com/tersesystems/logback/bytebuddy/MethodInfo.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.bytebuddy; import java.util.Objects; /** * Provides line number and source at implementation time without the overhead of fillInStackTrace. */ public class MethodInfo { final String methodName; final String descriptor; public final String source; private int startLine; private int endLine; MethodInfo(String methodName, String descriptor, String source) { this.methodName = Objects.requireNonNull(methodName); this.descriptor = descriptor; this.source = source; } public void setStartLine(int line) { this.startLine = line; } public void setEndLine(int line) { this.endLine = line; } public int getStartLine() { return startLine; } public int getEndLine() { return endLine; } @Override public String toString() { return "MethodInfo{" + "methodName='" + methodName + '\'' + ", descriptor='" + descriptor + '\'' + ", source='" + source + '\'' + '}'; } } ================================================ FILE: logback-bytebuddy/src/main/java/com/tersesystems/logback/bytebuddy/MethodInfoLookup.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.bytebuddy; import java.util.HashSet; import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.function.Predicate; public class MethodInfoLookup { private final ConcurrentMap> classNameToMethods = new ConcurrentHashMap<>(); public static MethodInfoLookup getInstance() { return SingletonHolder.instance; } static class SingletonHolder { public static MethodInfoLookup instance = new MethodInfoLookup(); } public void add(String className, MethodInfo methodInfo) { Set infos = classNameToMethods.computeIfAbsent(className, k -> new HashSet<>()); infos.add(methodInfo); } public Optional find(String className, String methodName, String descriptor) { Set infos = classNameToMethods.computeIfAbsent(className, k -> new HashSet<>()); return infos.stream().filter(matchingInfo(methodName, descriptor)).findFirst(); } private Predicate matchingInfo(String methodName, String descriptor) { return info -> Objects.equals(info.methodName, methodName) && Objects.equals(descriptor, info.descriptor); } } ================================================ FILE: logback-bytebuddy/src/main/java/com/tersesystems/logback/bytebuddy/impl/DeclaringTypeLoggerResolver.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.bytebuddy.impl; import java.util.Objects; import java.util.function.Supplier; import org.slf4j.ILoggerFactory; import org.slf4j.Logger; /** Returns the logger with the class that was instrumented. */ public class DeclaringTypeLoggerResolver implements LoggerResolver { private final Supplier loggerFactory; public DeclaringTypeLoggerResolver(Supplier loggerFactory) { this.loggerFactory = Objects.requireNonNull(loggerFactory); } @Override public Logger resolve(String origin) { int firstPipe = origin.indexOf('|'); String declaringType = origin.substring(0, firstPipe); return loggerFactory.get().getLogger(declaringType); } } ================================================ FILE: logback-bytebuddy/src/main/java/com/tersesystems/logback/bytebuddy/impl/Enter.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.bytebuddy.impl; import static com.tersesystems.logback.bytebuddy.impl.SystemFlow.*; import static net.logstash.logback.argument.StructuredArguments.v; import static net.logstash.logback.marker.Markers.append; import com.tersesystems.logback.bytebuddy.MethodInfo; import com.tersesystems.logback.bytebuddy.MethodInfoLookup; import java.util.Optional; import net.logstash.logback.argument.StructuredArgument; import net.logstash.logback.marker.LogstashMarker; import org.slf4j.Logger; import org.slf4j.Marker; public class Enter { private static final String format = "entering: {}.{}{} with {}"; private static final String formatWithSource = "entering: {}.{}{} with {} from source {}:{}"; public static void apply(String origin, Object[] allArguments) { Logger logger = getLogger(origin); if (logger != null && logger.isTraceEnabled(ENTRY_MARKER)) { String[] args = origin.split("\\|"); String declaringType = args[0]; String method = args[1]; String descriptor = args[2]; String signature = args[3]; StructuredArgument aClass = v("class", declaringType); StructuredArgument aMethod = v("method", method); StructuredArgument aSignature = v("signature", signature); StructuredArgument arrayParameters = safeArguments(allArguments); String name = createName(declaringType, method, signature); pushSpan(name); LogstashMarker nameMarker = append("name", name); Marker markers = baseMarkers().and(nameMarker).and(ENTRY_MARKER); MethodInfoLookup lookup = MethodInfoLookup.getInstance(); Optional methodInfo = lookup.find(declaringType, method, descriptor); if (methodInfo.isPresent()) { MethodInfo mi = methodInfo.get(); StructuredArgument aSource = v("source", mi.source); StructuredArgument aLineNumber = v("line", mi.getStartLine()); logger.trace( markers, formatWithSource, aClass, aMethod, aSignature, arrayParameters, aSource, aLineNumber); } else { logger.trace(markers, format, aClass, aMethod, aSignature, arrayParameters); } } } } ================================================ FILE: logback-bytebuddy/src/main/java/com/tersesystems/logback/bytebuddy/impl/Exit.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.bytebuddy.impl; import static com.tersesystems.logback.bytebuddy.impl.SystemFlow.*; import static net.logstash.logback.argument.StructuredArguments.kv; import static net.logstash.logback.argument.StructuredArguments.v; import static net.logstash.logback.marker.Markers.empty; import com.tersesystems.logback.bytebuddy.MethodInfo; import com.tersesystems.logback.bytebuddy.MethodInfoLookup; import java.util.Optional; import net.logstash.logback.argument.StructuredArgument; import net.logstash.logback.marker.LogstashMarker; import org.slf4j.Logger; import org.slf4j.Marker; public class Exit { private static String exitFormatWithSource = "exiting: {}.{}{} with {} => ({} {}) from source {}:{}"; private static String exitFormat = "exiting: {}.{}{} with {} => ({} {})"; public static void apply( String origin, Object[] allArguments, Throwable thrown, Object returnValue) { Logger logger = getLogger(origin); if (logger != null && logger.isTraceEnabled(EXIT_MARKER)) { String[] args = origin.split("\\|"); String declaringType = args[0]; String method = args[1]; String descriptor = args[2]; String signature = args[3]; String returnType = args[4]; StructuredArgument aClass = v("class", declaringType); // ClassCalledByAgent StructuredArgument aMethod = v("method", method); // printArgument StructuredArgument aSignature = v("signature", signature); // (java.lang.String) // StructuredArgument aDescriptor = kv("descriptor", descriptor); // // descriptor=(Ljava/lang/String;)V StructuredArgument safeArguments = safeArguments(allArguments); LogstashMarker spanMarker = popSpan().map(SystemFlow::createMarker).orElse(empty()); if (thrown != null) { Marker markers = spanMarker.and(THROWING_MARKER); // Always include the thrown at the end of the list as SLF4J will take care of stack trace. logger.error( markers, "throwing: {}.{}{} with {}", aClass, aMethod, aSignature, safeArguments, thrown); } else { StructuredArgument aReturnType = kv("return_type", returnType); StructuredArgument safeReturnValue = safeReturnValue(returnValue); MethodInfoLookup lookup = MethodInfoLookup.getInstance(); Optional methodInfo = lookup.find(declaringType, method, descriptor); Marker markers = spanMarker.and(EXIT_MARKER); if (methodInfo.isPresent()) { MethodInfo mi = methodInfo.get(); StructuredArgument aSource = v("source", mi.source); StructuredArgument aLineNumber = v("line", mi.getEndLine()); ; logger.trace( markers, exitFormatWithSource, aClass, aMethod, aSignature, safeArguments, aReturnType, safeReturnValue, aSource, aLineNumber); } else { logger.trace( markers, exitFormat, aClass, aMethod, aSignature, safeArguments, aReturnType, safeReturnValue); } } } } } ================================================ FILE: logback-bytebuddy/src/main/java/com/tersesystems/logback/bytebuddy/impl/FixedLoggerResolver.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.bytebuddy.impl; import static java.util.Objects.requireNonNull; import org.slf4j.Logger; /** Always returns the same logger. */ public class FixedLoggerResolver implements LoggerResolver { private final Logger logger; public FixedLoggerResolver(Logger logger) { this.logger = requireNonNull(logger); } @Override public Logger resolve(String origin) { return logger; } } ================================================ FILE: logback-bytebuddy/src/main/java/com/tersesystems/logback/bytebuddy/impl/LoggerResolver.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.bytebuddy.impl; import org.slf4j.Logger; /** Finds a logger given some input. */ public interface LoggerResolver { /** * @param origin the class name plus other stuff, provided from bytebuddy advice. * @return a logger. */ Logger resolve(String origin); } ================================================ FILE: logback-bytebuddy/src/main/java/com/tersesystems/logback/bytebuddy/impl/SafeArguments.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.bytebuddy.impl; import java.security.cert.X509Certificate; import java.util.*; import java.util.stream.Collectors; import java.util.stream.Stream; public class SafeArguments { public List apply(Object[] allArguments) { return Arrays.stream(allArguments).map(this::apply).collect(Collectors.toList()); } public String apply(Object returnValue) { try { if (returnValue instanceof Collection) { return parseCollection((Collection) returnValue); } if (returnValue instanceof Object[]) { return parseArray((Object[]) returnValue); } if (returnValue instanceof X509Certificate) { return parseCertificate((X509Certificate) returnValue); } return Objects.toString(returnValue); } catch (Exception e) { return "Exception rendering safeArguments: " + e.toString(); } } private String parseCertificate(X509Certificate cert) { String s = cert.getSerialNumber().toString(16); String sub = cert.getSubjectDN().getName(); return "X509Certificate(serialNumber = " + s + ", subject = " + sub + ")"; } private String parseArray(Object[] returnValue) { return parseStream(Arrays.stream(returnValue)); } private String parseCollection(Collection coll) { return parseStream(coll.stream()); } private String parseStream(Stream stream) { return stream.map(this::apply).collect(Collectors.joining(",")); } } ================================================ FILE: logback-bytebuddy/src/main/java/com/tersesystems/logback/bytebuddy/impl/SystemFlow.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.bytebuddy.impl; import static net.logstash.logback.argument.StructuredArguments.kv; import com.fasterxml.uuid.impl.RandomBasedGenerator; import com.tersesystems.logback.tracing.SpanInfo; import com.tersesystems.logback.tracing.SpanMarkerFactory; import com.tersesystems.logback.tracing.Tracer; import java.util.List; import java.util.Optional; import java.util.function.Supplier; import net.logstash.logback.argument.StructuredArgument; import net.logstash.logback.marker.LogstashMarker; import net.logstash.logback.marker.Markers; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.slf4j.Marker; import org.slf4j.MarkerFactory; public final class SystemFlow { // https://github.com/qos-ch/slf4j/blob/master/slf4j-ext/src/main/java/org/slf4j/ext/XLogger.java#L44 public static final Marker FLOW_MARKER = MarkerFactory.getMarker("FLOW"); public static final Marker ENTRY_MARKER = MarkerFactory.getMarker("ENTRY"); public static final Marker EXIT_MARKER = MarkerFactory.getMarker("EXIT"); public static final Marker EXCEPTION_MARKER = MarkerFactory.getMarker("EXCEPTION"); public static final Marker THROWING_MARKER = MarkerFactory.getMarker("THROWING"); private static final SpanMarkerFactory markerFactory = new SpanMarkerFactory(); static { ENTRY_MARKER.add(FLOW_MARKER); EXIT_MARKER.add(FLOW_MARKER); THROWING_MARKER.add(EXCEPTION_MARKER); } private static String serviceName; private static Supplier idGenerator; private static SafeArguments safeArguments = new SafeArguments(); static { // The out of the box UUID.randomUUID() is synchronized, which will block threads and // generally gum things up. The faster XML one is better, but still need to benchmark // considering how tracing can be injected anywhere. RandomBasedGenerator uuidGenerator = new RandomBasedGenerator(null); SystemFlow.setIdGenerator(() -> uuidGenerator.generate().toString()); } private static LoggerResolver loggerResolver = new DeclaringTypeLoggerResolver(LoggerFactory::getILoggerFactory); public static LoggerResolver getLoggerResolver() { return loggerResolver; } public static void setLoggerResolver(LoggerResolver loggerResolver) { SystemFlow.loggerResolver = loggerResolver; } public static Logger getLogger(String origin) { return loggerResolver.resolve(origin); } public static void setServiceName(String serviceName) { SystemFlow.serviceName = serviceName; } public static void setIdGenerator(Supplier idGenerator) { SystemFlow.idGenerator = idGenerator; } public static SafeArguments getSafeArguments() { return safeArguments; } public static void setSafeArguments(SafeArguments safeArguments) { SystemFlow.safeArguments = safeArguments; } public static LogstashMarker createMarker(SpanInfo span) { return baseMarkers().and(markerFactory.create(span)); } public static void pushSpan(String name) { Tracer.pushSpan(name, serviceName, idGenerator); } public static Optional popSpan() { return Tracer.popSpan(); } static StructuredArgument safeReturnValue(Object returnValue) { String safeReturnValue = safeArguments.apply(returnValue); return kv("return_value", safeReturnValue); } static StructuredArgument safeArguments(Object[] allArguments) { List safeArgs = safeArguments.apply(allArguments); return kv("arguments", safeArgs); } static String createName(String className, String method, String signature) { return className + "." + method + signature; } static LogstashMarker baseMarkers() { // Thread t = Thread.currentThread(); // //LogstashMarker threadNameMarker = append("trace.thread_name", t.getName()); // LogstashMarker threadIdMarker = append("thread_id", t.getId()); // return threadIdMarker.and(threadIdMarker); return Markers.empty(); } } ================================================ FILE: logback-bytebuddy/src/test/java/com/tersesystems/logback/bytebuddy/AdviceConfigTest.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.bytebuddy; import static org.assertj.core.api.Assertions.assertThat; import com.typesafe.config.Config; import org.junit.jupiter.api.Test; public class AdviceConfigTest { @Test public void testConfig() throws Exception { ClassLoader classLoader = ClassLoader.getSystemClassLoader(); Config config = LoggingInstrumentationAdvice.generateConfig(classLoader, false); AdviceConfig adviceConfig = LoggingInstrumentationAdvice.generateAdviceConfig(classLoader, config, false); assertThat(adviceConfig.classNames()) .contains("com.tersesystems.logback.bytebuddy.ClassCalledByAgent"); } } ================================================ FILE: logback-bytebuddy/src/test/java/com/tersesystems/logback/bytebuddy/ClassCalledByAgent.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.bytebuddy; /** This class does no logging. */ public class ClassCalledByAgent { public void printStatement() { System.out.println("I am a simple println method with no logging"); } public void printArgument(String arg) { System.out.println("I am a simple println, printing " + arg); } public void throwException(String arg) { throw new RuntimeException("I'm a squirrel!"); } } ================================================ FILE: logback-bytebuddy/src/test/java/com/tersesystems/logback/bytebuddy/InProcessInstrumentationExample.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.bytebuddy; import static net.bytebuddy.agent.builder.AgentBuilder.Listener; import com.tersesystems.logback.bytebuddy.impl.FixedLoggerResolver; import com.tersesystems.logback.bytebuddy.impl.SystemFlow; import com.typesafe.config.Config; import java.util.List; import net.bytebuddy.agent.ByteBuddyAgent; import net.bytebuddy.agent.builder.AgentBuilder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Run the agent inside an already running JVM. * *

This will instrument classes that have not already been loaded into the JVM, such as * ClassCalledByAgent, but will not allow you to instrument classes loaded by the system * classloader, such as java.lang.Thread. * *

This should still be perfectly fine for 99% of users who don't need an agent loaded from the * command line. */ public class InProcessInstrumentationExample { public static AgentBuilder.Listener createDebugListener(List classNames) { return new AgentBuilder.Listener.Filtering( LoggingInstrumentationAdvice.stringMatcher(classNames), AgentBuilder.Listener.StreamWriting.toSystemOut()); } public static void main(String[] args) throws Exception { // Helps if you install the byte buddy agents before anything else at all happens... ByteBuddyAgent.install(); Logger logger = LoggerFactory.getLogger(InProcessInstrumentationExample.class); SystemFlow.setLoggerResolver(new FixedLoggerResolver(logger)); ClassLoader classLoader = ClassLoader.getSystemClassLoader(); Config config = LoggingInstrumentationAdvice.generateConfig(classLoader, false); AdviceConfig adviceConfig = LoggingInstrumentationAdvice.generateAdviceConfig(classLoader, config, false); // The debugging listener shows what classes are being picked up by the instrumentation Listener debugListener = createDebugListener(adviceConfig.classNames()); new LoggingInstrumentationByteBuddyBuilder() .builderFromConfig(adviceConfig) .with(debugListener) .installOnByteBuddyAgent(); // No code change necessary here, you can wrap completely in the agent... ClassCalledByAgent classCalledByAgent = new ClassCalledByAgent(); classCalledByAgent.printStatement(); classCalledByAgent.printArgument("42"); try { classCalledByAgent.throwException("hello world"); } catch (Exception e) { // I am too lazy to catch this exception. I hope someone does it for me. } } } ================================================ FILE: logback-bytebuddy/src/test/java/com/tersesystems/logback/bytebuddy/PreloadedInstrumentationExample.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.bytebuddy; /** * Borrowed from securityfixer, showing tracing when you set the security manager. * *

Will not work on native methods, i.e. `System.currentTimeMillis`. * *

Move this into the main source path and redeploy if you want to test (I can't figure out how * to do agent stuff in Gradle) */ public class PreloadedInstrumentationExample { public static void main(String[] args) throws Exception { Thread thread = Thread.currentThread(); thread.run(); } } ================================================ FILE: logback-bytebuddy/src/test/resources/logback-test.xml ================================================ %-5relative %-5level %logger{35} - %msg%n ================================================ FILE: logback-bytebuddy/src/test/resources/logback.conf ================================================ logback.bytebuddy { service-name = "example-app" tracing { "com.tersesystems.logback.bytebuddy.ClassCalledByAgent" = [ "printStatement", "printArgument", "throwException", ] "java.lang.Thread" = [ "run" ] } } ================================================ FILE: logback-censor/gradle.properties ================================================ # # SPDX-License-Identifier: CC0-1.0 # # Copyright 2018-2020 Will Sargent. # # Licensed under the CC0 Public Domain Dedication; # You may obtain a copy of the License at # # http://creativecommons.org/publicdomain/zero/1.0/ # project_description = Logback Censor Project (regular esxpression and Jackson JSON filtering) ================================================ FILE: logback-censor/logback-censor.gradle ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ dependencies { implementation project(":logback-core") implementation "org.slf4j:slf4j-api:$slf4jVersion" implementation "net.logstash.logback:logstash-logback-encoder:$logstashVersion" implementation "ch.qos.logback:logback-classic:$logbackVersion" } ================================================ FILE: logback-censor/src/main/java/com/tersesystems/logback/censor/Censor.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.censor; import com.tersesystems.logback.core.Component; /** Basic censor functionality. */ public interface Censor extends Component { CharSequence censorText(CharSequence input); } ================================================ FILE: logback-censor/src/main/java/com/tersesystems/logback/censor/CensorAction.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.censor; import static com.tersesystems.logback.censor.CensorConstants.CENSOR_BAG; import ch.qos.logback.core.Context; import ch.qos.logback.core.CoreConstants; import ch.qos.logback.core.joran.action.Action; import ch.qos.logback.core.joran.spi.ActionException; import ch.qos.logback.core.joran.spi.InterpretationContext; import ch.qos.logback.core.util.OptionHelper; import java.util.HashMap; import java.util.Map; import org.xml.sax.Attributes; public class CensorAction extends Action { CensorContextAware censor; private boolean inError = false; @SuppressWarnings("unchecked") public void begin(InterpretationContext ic, String localName, Attributes attributes) throws ActionException { // We are just beginning, reset variables censor = null; inError = false; // Ensure idempotency of a CENSOR_BAG Map omap = ic.getObjectMap(); if (!omap.containsKey(CENSOR_BAG)) { omap.put(CENSOR_BAG, new HashMap()); } String className = attributes.getValue(CLASS_ATTRIBUTE); if (OptionHelper.isEmpty(className)) { addError("Missing class name for censor. Near [" + localName + "] line " + getLineNumber(ic)); inError = true; return; } try { addInfo("About to instantiate censor of type [" + className + "]"); censor = (CensorContextAware) OptionHelper.instantiateByClassName(className, CensorContextAware.class, context); // XXX we can get the censor here but it still doesn't have the parameters we need. // OptionHelper.substVars() Context icContext = ic.getContext(); if (censor != null) { censor.setContext(icContext); } String censorName = ic.subst(attributes.getValue(NAME_ATTRIBUTE)); if (OptionHelper.isEmpty(censorName)) { addWarn("No censor name given for censor of type " + className + "]."); } else { censor.setName(censorName); addInfo("Naming censor as [" + censorName + "]"); } // The execution context contains a bag which contains the censors // created thus far. HashMap censorBag = (HashMap) ic.getObjectMap().get(CENSOR_BAG); getContext().putObject(CENSOR_BAG, censorBag); // add the censorText just created to the censorText bag. censorBag.put(censorName, censor); ic.pushObject(censor); } catch (Exception oops) { inError = true; addError("Could not create a Censor of type [" + className + "].", oops); throw new ActionException(oops); } } private void addConverter() { // Add a conversion rule automatically Map ruleRegistry = (Map) context.getObject(CoreConstants.PATTERN_RULE_REGISTRY); if (ruleRegistry == null) { ruleRegistry = new HashMap(); context.putObject(CoreConstants.PATTERN_RULE_REGISTRY, ruleRegistry); } ruleRegistry.putIfAbsent(CensorConstants.CENSOR_RULE_NAME, CensorConverter.class.getName()); } /** * Once the children elements are also parsed, now is the time to activate the appender options. */ public void end(InterpretationContext ec, String name) { if (inError) { return; } if (censor != null) { censor.start(); } Object o = ec.peekObject(); if (o != censor) { addWarn( "The object at the end of the stack is not the censor named [" + censor.getName() + "] pushed earlier."); } else { ec.popObject(); } } } ================================================ FILE: logback-censor/src/main/java/com/tersesystems/logback/censor/CensorAttachable.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.censor; public interface CensorAttachable { void addCensor(CensorContextAware censor); } ================================================ FILE: logback-censor/src/main/java/com/tersesystems/logback/censor/CensorConstants.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.censor; public class CensorConstants { public static final String CENSOR_BAG = "CENSOR_BAG"; public static final String REF_ATTRIBUTE = "ref"; public static final String CENSOR_RULE_NAME = "censor"; } ================================================ FILE: logback-censor/src/main/java/com/tersesystems/logback/censor/CensorContextAware.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.censor; import ch.qos.logback.core.spi.ContextAware; import ch.qos.logback.core.spi.LifeCycle; public interface CensorContextAware extends Censor, ContextAware, LifeCycle { /** Get the name of this appender. The name uniquely identifies the appender. */ String getName(); /** * Set the name of this appender. The name is used by other components to identify this appender. */ void setName(String name); } ================================================ FILE: logback-censor/src/main/java/com/tersesystems/logback/censor/CensorConverter.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.censor; import static com.tersesystems.logback.censor.CensorConstants.CENSOR_BAG; import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.core.pattern.CompositeConverter; import java.util.List; import java.util.Map; /** * Censoring message converter for text. * *

Note that this does not filter out marker text or additional information related to the event, * i.e. it does not filter out exception text. * *

Note also that the censor converter only picks out one censor from the list. * *

{@code
 * 
 * }
*/ public class CensorConverter extends CompositeConverter { private CensorContextAware censor; @Override public void start() { super.start(); // There isn't a good way of referring to other objects without going through // the context here, as the IC is not available to converters. Map censorBag = (Map) getContext().getObject(CENSOR_BAG); if (censorBag == null || censorBag.isEmpty()) { addError("Null or empty censor bag found in context!"); } // The censor name is given in the pattern encoder in the form "%censor(%msg, censor-name)" // See logstash-logback-encoder for a more complex example: // https://github.com/logstash/logstash-logback-encoder/blob/master/src/main/java/net/logstash/logback/stacktrace/ShortenedThrowableConverter.java List optionList = getOptionList(); addInfo(String.format("Pulling options %s", optionList)); String censorName = getFirstOption(); if (censorName == null) { censorName = censorBag.keySet().iterator().next(); addInfo( String.format("Pulling first censor name %s from censor bag converter: ", censorName)); } else { addInfo(String.format("Referencing explicit censor name %s in converter: ", censorName)); } censor = censorBag.get(censorName); if (censor == null) { addError(String.format("No censor with name %s found in censor bag!", censorName)); } } // // @Override // public String convert(ILoggingEvent event) { // return String.valueOf(censor.censorText(in)); // } @SuppressWarnings("unchecked") public String transform(ILoggingEvent event, String in) { return String.valueOf(censor.censorText(in)); } } ================================================ FILE: logback-censor/src/main/java/com/tersesystems/logback/censor/CensorRefAction.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.censor; import ch.qos.logback.core.joran.action.Action; import ch.qos.logback.core.joran.spi.InterpretationContext; import ch.qos.logback.core.util.OptionHelper; import java.util.Map; import org.xml.sax.Attributes; public class CensorRefAction extends Action { boolean inError = false; @SuppressWarnings("unchecked") public void begin(InterpretationContext ec, String tagName, Attributes attributes) { // Let us forget about previous errors (in this object) inError = false; // logger.debug("begin called"); Object o = ec.peekObject(); if (!(o instanceof CensorAttachable)) { String errMsg = "Could not find an CensorAttachable at the top of execution stack. Near [" + tagName + "] line " + getLineNumber(ec); inError = true; addInfo(errMsg); // This can trigger in an "if" block from janino, so it may not be serious... return; } CensorAttachable censorAttachable = (CensorAttachable) o; String censorName = ec.subst(attributes.getValue(CensorConstants.REF_ATTRIBUTE)); if (OptionHelper.isEmpty(censorName)) { // print a meaningful error message and return String errMsg = "Missing censor ref attribute in tag."; inError = true; addError(errMsg); return; } Map censorBag = (Map) ec.getObjectMap().get(CensorConstants.CENSOR_BAG); CensorContextAware censor = censorBag.get(censorName); if (censor == null) { String msg = "Could not find an censor named [" + censorName + "]. Did you define it below instead of above in the configuration file?"; inError = true; addError(msg); return; } addInfo( "Attaching censor named [" + censorName + "] to " + censorAttachable + "at " + getLineNumber(ec)); censorAttachable.addCensor(censor); } public void end(InterpretationContext ec, String n) {} } ================================================ FILE: logback-censor/src/main/java/com/tersesystems/logback/censor/CensoringJsonGeneratorDecorator.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.censor; import ch.qos.logback.core.spi.ContextAwareBase; import ch.qos.logback.core.spi.LifeCycle; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.SerializableString; import com.fasterxml.jackson.core.filter.FilteringGeneratorDelegate; import com.fasterxml.jackson.core.filter.TokenFilter; import com.fasterxml.jackson.core.util.JsonGeneratorDelegate; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; import net.logstash.logback.decorate.JsonGeneratorDecorator; // https://github.com/FasterXML/jackson-core/issues/185 public class CensoringJsonGeneratorDecorator extends ContextAwareBase implements CensorAttachable, JsonGeneratorDecorator, LifeCycle { private final List censors = new ArrayList<>(); private boolean started; @Override public JsonGenerator decorate(JsonGenerator generator) { CensoringJsonGeneratorDelegate substitutionDelegate = new CensoringJsonGeneratorDelegate(generator); return new FilteringGeneratorDelegate( substitutionDelegate, new CensoringTokenFilter(), true, true); } public List getCensors() { return censors; } public void addCensor(CensorContextAware censor) { this.censors.add(censor); } @Override public void start() { started = true; } @Override public void stop() { filterKeys.clear(); censors.clear(); started = false; } @Override public boolean isStarted() { return started; } private final List filterKeys = new ArrayList<>(); public void addFilterKey(String filterKey) { this.filterKeys.add(filterKey); } // Removes entire value attached to the key. private class CensoringTokenFilter extends TokenFilter { @Override public TokenFilter includeElement(int index) { return this; } @Override public TokenFilter includeProperty(String name) { if (shouldFilter(name)) { return null; } return TokenFilter.INCLUDE_ALL; } private boolean shouldFilter(String name) { return filterKeys.contains(name); } @Override protected boolean _includeScalar() { return false; } } // Filters text inside JSON private class CensoringJsonGeneratorDelegate extends JsonGeneratorDelegate { public CensoringJsonGeneratorDelegate(JsonGenerator d) { super(d); } private String censorSensitiveMessage(String original) { String value = original; final List censors = getCensors(); for (CensorContextAware censor : censors) { value = String.valueOf(censor.censorText(value)); } return value; } @Override public void writeString(String original) throws IOException { final String value = censorSensitiveMessage(original); delegate.writeString(value); } @Override public void writeString(char[] text, int offset, int len) throws IOException { final String original = new String(text, offset, len); final String value = censorSensitiveMessage(original); delegate.writeString(value); } @Override public void writeString(SerializableString serializableString) throws IOException { final String original = serializableString.getValue(); final String value = censorSensitiveMessage(original); delegate.writeString(value); } @Override public void writeRawUTF8String(byte[] text, int offset, int length) throws IOException { String original = new String(text, offset, length, StandardCharsets.UTF_8); final String value = censorSensitiveMessage(original); delegate.writeString(value); } @Override public void writeUTF8String(byte[] text, int offset, int length) throws IOException { String original = new String(text, offset, length, StandardCharsets.UTF_8); final String value = censorSensitiveMessage(original); delegate.writeString(value); } } } ================================================ FILE: logback-censor/src/main/java/com/tersesystems/logback/censor/CensoringPrettyPrintingJsonGeneratorDecorator.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.censor; import com.fasterxml.jackson.core.JsonGenerator; public class CensoringPrettyPrintingJsonGeneratorDecorator extends CensoringJsonGeneratorDecorator implements CensorAttachable { @Override public JsonGenerator decorate(JsonGenerator generator) { return super.decorate(generator.useDefaultPrettyPrinter()); } } ================================================ FILE: logback-censor/src/main/java/com/tersesystems/logback/censor/RegexCensor.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.censor; import ch.qos.logback.core.spi.ContextAwareBase; import ch.qos.logback.core.spi.LifeCycle; import java.util.regex.Pattern; public class RegexCensor extends ContextAwareBase implements CensorContextAware, LifeCycle { protected volatile boolean started = false; private Pattern pattern = null; private String regex = null; private String replacementText; protected String name; public String getName() { return name; } public void setName(String name) { this.name = name; } public String getReplacementText() { return replacementText; } public void setReplacementText(String replacementText) { this.replacementText = replacementText; } public void setRegex(String regex) { this.regex = regex; } @Override public boolean isStarted() { return started; } @Override public void start() { if (replacementText == null) { addError("replacementText cannot be null!"); return; } if (regex == null) { addError("No regular expressions found!"); return; } this.pattern = Pattern.compile(regex, (regex.contains("\n")) ? Pattern.MULTILINE : 0); this.started = true; } @Override public void stop() { this.replacementText = null; this.pattern = null; this.regex = null; this.started = false; } @Override public CharSequence censorText(CharSequence original) { return pattern.matcher(original).replaceAll(replacementText); } } ================================================ FILE: logback-censor/src/test/java/com/tersesystems/logback/censor/CensorActionTest.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.censor; import static java.util.Objects.requireNonNull; import static org.assertj.core.api.Assertions.assertThat; import ch.qos.logback.classic.Level; import ch.qos.logback.classic.LoggerContext; import ch.qos.logback.classic.joran.JoranConfigurator; import ch.qos.logback.classic.spi.LoggingEvent; import ch.qos.logback.core.joran.spi.JoranException; import java.nio.charset.StandardCharsets; import org.junit.Before; import org.junit.Test; public class CensorActionTest { private JoranConfigurator jc = new JoranConfigurator(); private LoggerContext loggerContext = new LoggerContext(); @Before public void setUp() { jc.setContext(loggerContext); } @Test public void testFirstTest1() throws JoranException { jc.doConfigure(requireNonNull(this.getClass().getClassLoader().getResource("test1.xml"))); ch.qos.logback.classic.Logger root = loggerContext.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME); TestAppender test = (TestAppender) root.getAppender("TEST1"); assertThat(test).isNotNull(); byte[] bytes = test.getEncoder().encode(createLoggingEvent(root, "hunter1")); assertThat(new String(bytes, StandardCharsets.UTF_8)).contains("[CENSORED BY CENSOR1]"); } @Test public void testSecondTest1() throws JoranException { jc.doConfigure(requireNonNull(this.getClass().getClassLoader().getResource("test2.xml"))); ch.qos.logback.classic.Logger root = loggerContext.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME); TestAppender test = (TestAppender) root.getAppender("TEST2"); assertThat(test).isNotNull(); byte[] bytes = test.getEncoder().encode(createLoggingEvent(root, "hunter2")); assertThat(new String(bytes, StandardCharsets.UTF_8)).contains("[CENSORED BY CENSOR2]"); } @Test public void testJsonTest3() throws JoranException { jc.doConfigure(requireNonNull(this.getClass().getClassLoader().getResource("test3.xml"))); ch.qos.logback.classic.Logger root = loggerContext.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME); TestAppender test = (TestAppender) root.getAppender("TEST3"); assertThat(test).isNotNull(); byte[] bytes = test.getEncoder().encode(createLoggingEvent(root, "hunter3 hunter4")); assertThat(new String(bytes, StandardCharsets.UTF_8)) .contains("\"message\":\"[CENSOR3] [CENSOR4]\""); } @Test public void testJsonTest4() throws JoranException { jc.doConfigure(requireNonNull(this.getClass().getClassLoader().getResource("test4.xml"))); ch.qos.logback.classic.Logger root = loggerContext.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME); TestAppender test = (TestAppender) root.getAppender("TEST4"); assertThat(test).isNotNull(); byte[] bytes = test.getEncoder().encode(createLoggingEvent(root, "hunter4")); assertThat(new String(bytes, StandardCharsets.UTF_8)).contains("\"message\":\"[CENSOR4]\""); } private LoggingEvent createLoggingEvent(ch.qos.logback.classic.Logger logger, String message) { return new LoggingEvent(this.getClass().getName(), logger, Level.DEBUG, message, null, null); } } ================================================ FILE: logback-censor/src/test/java/com/tersesystems/logback/censor/CensoringJsonGeneratorDecoratorTest.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.censor; import static org.assertj.core.api.Assertions.assertThat; import ch.qos.logback.classic.LoggerContext; import com.fasterxml.jackson.core.JsonFactory; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.MappingJsonFactory; import java.io.StringWriter; import org.junit.Test; public class CensoringJsonGeneratorDecoratorTest { @Test public void basicCensor() throws Exception { LoggerContext context = new LoggerContext(); RegexCensor censor1 = new RegexCensor(); censor1.setContext(context); censor1.setReplacementText("*******"); censor1.setRegex("hunter2"); censor1.start(); RegexCensor censor2 = new RegexCensor(); censor2.setContext(context); censor2.setReplacementText("!!!!!!"); censor2.setRegex("message"); censor2.start(); CensoringJsonGeneratorDecorator decorator = new CensoringJsonGeneratorDecorator(); decorator.setContext(context); decorator.addCensor(censor1); decorator.addCensor(censor2); decorator.start(); StringWriter writer = new StringWriter(); JsonFactory factory = new MappingJsonFactory(); JsonGenerator generator = decorator.decorate(factory.createGenerator(writer)); generator.writeStartObject(); generator.writeStringField("message", "My hunter2 message"); generator.writeEndObject(); generator.flush(); assertThat(writer.toString()).isEqualTo("{\"message\":\"My ******* !!!!!!\"}"); } @Test public void filterKey() throws Exception { CensoringJsonGeneratorDecorator decorator = new CensoringJsonGeneratorDecorator(); decorator.addFilterKey("password"); decorator.start(); StringWriter writer = new StringWriter(); JsonFactory factory = new MappingJsonFactory(); JsonGenerator generator = decorator.decorate(factory.createGenerator(writer)); generator.writeStartObject(); generator.writeStringField("password", "this entire field should be gone"); generator.writeEndObject(); generator.flush(); assertThat(writer.toString()).isEqualTo(""); } @Test public void prettyPrintCensor() throws Exception { LoggerContext context = new LoggerContext(); RegexCensor censor = new RegexCensor(); censor.setContext(context); censor.setReplacementText("*******"); censor.setRegex("hunter2"); censor.start(); CensoringJsonGeneratorDecorator decorator = new CensoringPrettyPrintingJsonGeneratorDecorator(); decorator.setContext(context); decorator.addCensor(censor); decorator.start(); StringWriter writer = new StringWriter(); JsonFactory factory = new MappingJsonFactory(); JsonGenerator generator = decorator.decorate(factory.createGenerator(writer)); generator.writeStartObject(); generator.writeStringField("message", "My hunter2 message"); generator.writeEndObject(); generator.flush(); assertThat(writer.toString()).isEqualTo("{\n \"message\" : \"My ******* message\"\n}"); } } ================================================ FILE: logback-censor/src/test/java/com/tersesystems/logback/censor/RegexCensorTest.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.censor; import static org.assertj.core.api.Assertions.assertThat; import org.junit.Test; public class RegexCensorTest { @Test public void testCensor() throws Exception { String replacementText = "*******"; RegexCensor censor = new RegexCensor(); censor.setReplacementText(replacementText); censor.setRegex("hunter2"); censor.start(); assertThat(censor.censorText("hunter2")).isEqualTo("*******"); } @Test public void testCensorWithNoMatch() throws Exception { String replacementText = "*******"; RegexCensor censor = new RegexCensor(); censor.setReplacementText(replacementText); censor.setRegex("hunter2"); censor.start(); assertThat(censor.censorText("password1")).isEqualTo("password1"); } } ================================================ FILE: logback-censor/src/test/java/com/tersesystems/logback/censor/TestAppender.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.censor; import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.core.AppenderBase; import ch.qos.logback.core.encoder.Encoder; import java.util.ArrayList; import java.util.List; public class TestAppender extends AppenderBase { protected Encoder encoder; public static List events = new ArrayList<>(); public Encoder getEncoder() { return encoder; } public void setEncoder(Encoder encoder) { this.encoder = encoder; } @Override protected void append(ILoggingEvent e) { events.add(e); } } ================================================ FILE: logback-censor/src/test/resources/test1.xml ================================================ [CENSORED BY CENSOR1] hunter1 [CENSORED BY CENSOR2] hunter2 %censor(%msg){censor-name1}%n ================================================ FILE: logback-censor/src/test/resources/test2.xml ================================================ [CENSORED BY CENSOR1] hunter1 [CENSORED BY CENSOR2] hunter2 %censor(%msg){censor-name2}%n ================================================ FILE: logback-censor/src/test/resources/test3.xml ================================================ hunter3 [CENSOR3] hunter4 [CENSOR4] ================================================ FILE: logback-censor/src/test/resources/test4.xml ================================================ hunter4 [CENSOR4] { "custom_constant": "123", "tags": ["one", "two"], "logger": "%logger", "level": "%level", "thread": "%thread", "message": "%censor(%message){json-censor}" } ================================================ FILE: logback-classic/gradle.properties ================================================ # # SPDX-License-Identifier: CC0-1.0 # # Copyright 2018-2020 Will Sargent. # # Licensed under the CC0 Public Domain Dedication; # You may obtain a copy of the License at # # http://creativecommons.org/publicdomain/zero/1.0/ # project_description = Logback Classic ================================================ FILE: logback-classic/logback-classic.gradle ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ plugins { id 'java-library' } dependencies { api project(':logback-core') api "org.slf4j:jul-to-slf4j:$slf4jVersion" api "ch.qos.logback:logback-classic:$logbackVersion" } ================================================ FILE: logback-classic/src/main/java/com/tersesystems/logback/classic/ChangeLogLevel.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.classic; import ch.qos.logback.classic.Level; import ch.qos.logback.classic.Logger; import org.slf4j.ILoggerFactory; import org.slf4j.LoggerFactory; /** Provide a way to change the logging level dynamically in Logback. */ public class ChangeLogLevel { private final ILoggerFactory loggerFactory; public ChangeLogLevel() { this(LoggerFactory.getILoggerFactory()); } public ChangeLogLevel(ILoggerFactory loggerFactory) { this.loggerFactory = loggerFactory; } public void changeLogLevel(String loggerName, String levelName) { changeLogLevel(loggerFactory.getLogger(loggerName), levelName); } public final void changeLogLevel(String loggerName, int levelNumber) { changeLogLevel(loggerFactory.getLogger(loggerName), levelNumber); } public final void changeLogLevel(org.slf4j.Logger logger, String levelName) { Logger logbackLogger = (Logger) logger; Level level = Level.toLevel(levelName); logbackLogger.setLevel(level); } public final void changeLogLevel(org.slf4j.Logger logger, int levelNumber) { Logger logbackLogger = (Logger) logger; Level level = Level.toLevel(levelNumber); logbackLogger.setLevel(level); } } ================================================ FILE: logback-classic/src/main/java/com/tersesystems/logback/classic/ContainerEventAppender.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.classic; import ch.qos.logback.classic.spi.ILoggingEvent; import com.tersesystems.logback.core.DecoratingAppender; /** * This appender decorates the out of the box logging event with a component system, which allows * extra attributes to be added to the event. */ public class ContainerEventAppender extends DecoratingAppender { @Override protected IContainerLoggingEvent decorateEvent(ILoggingEvent eventObject) { return new ContainerProxyLoggingEvent(eventObject); } } ================================================ FILE: logback-classic/src/main/java/com/tersesystems/logback/classic/ContainerProxyLoggingEvent.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.classic; import ch.qos.logback.classic.spi.ILoggingEvent; import java.util.HashMap; import java.util.Map; import java.util.Objects; /** A logging event that implements a container and proxies another logging event. */ public class ContainerProxyLoggingEvent extends ProxyLoggingEvent implements IContainerLoggingEvent { private Map, Object> components = new HashMap<>(); public ContainerProxyLoggingEvent(ILoggingEvent delegate) { super(delegate); } public void putComponent(Class type, T instance) { components.put(Objects.requireNonNull(type), instance); } public T getComponent(Class type) { return type.cast(components.get(type)); } @Override public boolean hasComponent(Class type) { return components.containsKey(type); } } ================================================ FILE: logback-classic/src/main/java/com/tersesystems/logback/classic/ContextAwareBasicMarker.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.classic; import ch.qos.logback.core.Context; import ch.qos.logback.core.spi.ContextAware; import ch.qos.logback.core.status.*; /** Extend the marker interface so that we can make it context aware. */ public class ContextAwareBasicMarker extends TerseBasicMarker implements ContextAware { private int noContextWarning = 0; protected Context context; public ContextAwareBasicMarker(String name) { super(name); } public void setContext(Context context) { if (this.context == null) { this.context = context; } else if (this.context != context) { throw new IllegalStateException("Context has been already set"); } } public Context getContext() { return this.context; } public StatusManager getStatusManager() { if (context == null) { return null; } return context.getStatusManager(); } /** * The declared origin of status messages. By default 'this'. Derived classes may override this * method to declare other origin. * * @return the declared origin, by default 'this' */ protected Object getDeclaredOrigin() { return this; } public void addStatus(Status status) { if (context == null) { if (noContextWarning++ == 0) { System.out.println("LOGBACK: No context given for " + this); } return; } StatusManager sm = context.getStatusManager(); if (sm != null) { sm.add(status); } } public void addInfo(String msg) { addStatus(new InfoStatus(msg, getDeclaredOrigin())); } public void addInfo(String msg, Throwable ex) { addStatus(new InfoStatus(msg, getDeclaredOrigin(), ex)); } public void addWarn(String msg) { addStatus(new WarnStatus(msg, getDeclaredOrigin())); } public void addWarn(String msg, Throwable ex) { addStatus(new WarnStatus(msg, getDeclaredOrigin(), ex)); } public void addError(String msg) { addStatus(new ErrorStatus(msg, getDeclaredOrigin())); } public void addError(String msg, Throwable ex) { addStatus(new ErrorStatus(msg, getDeclaredOrigin(), ex)); } } ================================================ FILE: logback-classic/src/main/java/com/tersesystems/logback/classic/ExceptionMessageConverter.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.classic; import ch.qos.logback.classic.pattern.ThrowableHandlingConverter; import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.classic.spi.IThrowableProxy; import java.util.Collections; import java.util.List; import java.util.Optional; /** * Exception message converter that only prints out the messages of the nested exception. * *

The first argument is the amount of leading whitespace to add before the exception. * *

The second argument is the maximum depth of the nested exceptions. * *

The third, fourth, and fifth arguments are the prefix, separator, and suffix, respectively. * *

Use in a pattern encoder, i.e. "%exmessage{1, 10, cause=[}" */ public class ExceptionMessageConverter extends ThrowableHandlingConverter { @Override public String convert(ILoggingEvent event) { Integer whitespace = getLeadingWhitespace(); if (whitespace < 0) { addWarn("Cannot render whitespace less than 0!"); whitespace = 0; } Integer depth = getDepth(); if (depth < 1) { addWarn("Cannot render depth less than 1!"); depth = 1; } String prefix = getPrefix(); String sep = getSeparator(); String suffix = getSuffix(); IThrowableProxy ex = event.getThrowableProxy(); if (ex == null) { return ""; } return processException(ex, whitespace, depth, prefix, sep, suffix); } private Integer getLeadingWhitespace() { return Integer.parseInt(getOption(0).orElse("1")); } protected Integer getDepth() { return Integer.parseInt(getOption(1).orElse("10")); } protected String getPrefix() { return getOption(2).orElse("["); } protected String getSeparator() { return getOption(3).orElse(" > "); } protected String getSuffix() { return getOption(4).orElse("]"); } protected Optional getOption(int index) { List optionList = getOptionList(); if (optionList != null && optionList.size() >= index + 1) { return Optional.of(optionList.get(index)); } return Optional.empty(); } protected String processException( IThrowableProxy throwableProxy, Integer whitespace, Integer depth, String prefix, String sep, String suffix) { String ws = String.join("", Collections.nCopies(whitespace, " ")); StringBuilder b = new StringBuilder(ws + prefix); IThrowableProxy ex = throwableProxy; for (int i = 0; i < depth; i++) { String message = constructMessage(ex); b.append(message); ex = ex.getCause(); if (ex == null || i + 1 == depth) break; b.append(sep); } b.append(suffix); return b.toString(); } protected String constructMessage(IThrowableProxy ex) { return (ex.getMessage() == null) ? ex.getClassName() : ex.getMessage(); } } ================================================ FILE: logback-classic/src/main/java/com/tersesystems/logback/classic/FormatParamsDecider.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.classic; import ch.qos.logback.classic.Level; import ch.qos.logback.classic.Logger; import ch.qos.logback.core.spi.FilterReply; import java.util.function.BiFunction; import org.slf4j.Marker; @FunctionalInterface public interface FormatParamsDecider extends BiFunction, TurboFilterDecider { @Override default FilterReply decide( Marker marker, Logger logger, Level level, String format, Object[] params, Throwable t) { return apply(format, params); } } ================================================ FILE: logback-classic/src/main/java/com/tersesystems/logback/classic/IContainerLoggingEvent.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.classic; import ch.qos.logback.classic.spi.ILoggingEvent; import com.tersesystems.logback.core.ComponentContainer; /** A logging event that is a container of components. */ public interface IContainerLoggingEvent extends ILoggingEvent, ComponentContainer {} ================================================ FILE: logback-classic/src/main/java/com/tersesystems/logback/classic/ILoggingEventFactory.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.classic; import ch.qos.logback.classic.Level; import ch.qos.logback.classic.Logger; import ch.qos.logback.classic.spi.ILoggingEvent; import org.slf4j.Marker; public interface ILoggingEventFactory { E create(Marker marker, Logger logger, Level level, String msg, Object[] params, Throwable t); } ================================================ FILE: logback-classic/src/main/java/com/tersesystems/logback/classic/LoggerDecider.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.classic; import ch.qos.logback.classic.Logger; import ch.qos.logback.core.spi.FilterReply; import java.util.function.Function; import org.slf4j.Marker; @FunctionalInterface public interface LoggerDecider extends Function, TurboFilterDecider { default FilterReply decide( Marker marker, Logger logger, ch.qos.logback.classic.Level level, String format, Object[] params, Throwable t) { return apply(logger); } } ================================================ FILE: logback-classic/src/main/java/com/tersesystems/logback/classic/LoggingEventFactory.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.classic; import static ch.qos.logback.classic.Logger.FQCN; import ch.qos.logback.classic.Level; import ch.qos.logback.classic.Logger; import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.classic.spi.LoggingEvent; import org.slf4j.Marker; public class LoggingEventFactory implements ILoggingEventFactory { public ILoggingEvent create( Marker marker, Logger logger, Level level, String msg, Object[] params, Throwable t) { LoggingEvent le = new LoggingEvent(FQCN, logger, level, msg, t, params); le.setMarker(marker); return le; } } ================================================ FILE: logback-classic/src/main/java/com/tersesystems/logback/classic/MarkerLoggerDecider.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.classic; import ch.qos.logback.classic.Logger; import ch.qos.logback.core.spi.FilterReply; import java.util.function.BiFunction; import org.slf4j.Marker; @FunctionalInterface public interface MarkerLoggerDecider extends BiFunction, TurboFilterDecider { default FilterReply decide( Marker marker, Logger logger, ch.qos.logback.classic.Level level, String format, Object[] params, Throwable t) { return apply(marker, logger); } } ================================================ FILE: logback-classic/src/main/java/com/tersesystems/logback/classic/NanoTime.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.classic; import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.core.Context; import ch.qos.logback.core.spi.ContextAware; import com.tersesystems.logback.core.ComponentContainer; import java.util.Iterator; import java.util.Optional; import org.slf4j.Marker; public final class NanoTime { public static final long start = System.nanoTime(); public static Optional fromOptional(Context context, ILoggingEvent event) { if (event instanceof ComponentContainer) { ComponentContainer container = (ComponentContainer) event; if (container.hasComponent(NanoTimeSupplier.class)) { return fromContainer(container); } } return fromMarker(context, event.getMarker()); } static Optional fromMarker(Context context, Marker m) { if (m instanceof NanoTimeSupplier) { NanoTimeSupplier supplier = ((NanoTimeSupplier) m); return Optional.of(supplier.getNanoTime()); } if (m != null && m.hasReferences()) { for (Iterator iter = m.iterator(); iter.hasNext(); ) { Marker child = iter.next(); if (child instanceof ContextAware) { ((ContextAware) child).setContext(context); } if (child instanceof NanoTimeSupplier) { NanoTimeSupplier supplier = ((NanoTimeSupplier) child); return Optional.of(supplier.getNanoTime()); } if (child.hasReferences()) { return fromMarker(context, child); } } } return Optional.empty(); } public static Optional fromContainer(ComponentContainer container) { long nanoTime = container.getComponent(NanoTimeSupplier.class).getNanoTime(); return Optional.of(nanoTime); } } ================================================ FILE: logback-classic/src/main/java/com/tersesystems/logback/classic/NanoTimeComponentAppender.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.classic; import ch.qos.logback.classic.spi.ILoggingEvent; import com.tersesystems.logback.core.DecoratingAppender; /** This appender adds a relative nanotime component to the logging event. */ public class NanoTimeComponentAppender extends DecoratingAppender { @Override protected IContainerLoggingEvent decorateEvent(ILoggingEvent eventObject) { IContainerLoggingEvent containerEvent; if (eventObject instanceof IContainerLoggingEvent) { containerEvent = (IContainerLoggingEvent) eventObject; } else { containerEvent = new ContainerProxyLoggingEvent(eventObject); } long nanoTime = System.nanoTime() - NanoTime.start; containerEvent.putComponent(NanoTimeSupplier.class, () -> nanoTime); return containerEvent; } } ================================================ FILE: logback-classic/src/main/java/com/tersesystems/logback/classic/NanoTimeConverter.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.classic; import ch.qos.logback.classic.pattern.ClassicConverter; import ch.qos.logback.classic.spi.ILoggingEvent; /** A relative time converter that returns number of nanoseconds from NanoTime.start. */ public class NanoTimeConverter extends ClassicConverter { @Override public String convert(ILoggingEvent event) { return NanoTime.fromOptional(getContext(), event).map(st -> Long.toString(st)).orElse(null); } } ================================================ FILE: logback-classic/src/main/java/com/tersesystems/logback/classic/NanoTimeMarker.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.classic; public class NanoTimeMarker extends TerseBasicMarker implements NanoTimeSupplier { private static final String NANOTIME_MARKER_NAME = "TS_NANOTIME_MARKER"; private final long nanoTime; public NanoTimeMarker() { super(NANOTIME_MARKER_NAME); this.nanoTime = System.nanoTime() - NanoTime.start; } public long getNanoTime() { return nanoTime; } public static NanoTimeMarker create() { return new NanoTimeMarker(); } @Override public String toString() { return "NanoTimeMarker{" + "nanoTime=" + nanoTime + '}'; } } ================================================ FILE: logback-classic/src/main/java/com/tersesystems/logback/classic/NanoTimeSupplier.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.classic; import com.tersesystems.logback.core.Component; public interface NanoTimeSupplier extends Component { long getNanoTime(); } ================================================ FILE: logback-classic/src/main/java/com/tersesystems/logback/classic/ProxyLoggingEvent.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.classic; import ch.qos.logback.classic.Level; import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.classic.spi.IThrowableProxy; import ch.qos.logback.classic.spi.LoggerContextVO; import java.util.Map; import org.slf4j.Marker; /** * Create a LoggingEvent that takes a proxy. * *

This looks a bit like LoggingEventVO, but does not implement serializable behavior or * additional logic. */ public class ProxyLoggingEvent implements ILoggingEvent { private final ILoggingEvent delegate; public ProxyLoggingEvent(ILoggingEvent delegate) { this.delegate = delegate; } public ILoggingEvent getDelegate() { return delegate; } @Override public String getThreadName() { return delegate.getThreadName(); } @Override public Level getLevel() { return delegate.getLevel(); } @Override public String getMessage() { return delegate.getMessage(); } @Override public Object[] getArgumentArray() { return delegate.getArgumentArray(); } @Override public String getFormattedMessage() { return delegate.getFormattedMessage(); } @Override public String getLoggerName() { return delegate.getLoggerName(); } @Override public LoggerContextVO getLoggerContextVO() { return delegate.getLoggerContextVO(); } @Override public IThrowableProxy getThrowableProxy() { return delegate.getThrowableProxy(); } @Override public StackTraceElement[] getCallerData() { return delegate.getCallerData(); } @Override public boolean hasCallerData() { return delegate.hasCallerData(); } @Override public Marker getMarker() { return delegate.getMarker(); } @Override public Map getMDCPropertyMap() { return delegate.getMDCPropertyMap(); } @Override public Map getMdc() { return delegate.getMdc(); } @Override public long getTimeStamp() { return delegate.getTimeStamp(); } @Override public void prepareForDeferredProcessing() { delegate.prepareForDeferredProcessing(); } } ================================================ FILE: logback-classic/src/main/java/com/tersesystems/logback/classic/SLF4JBridgeHandlerAction.java ================================================ package com.tersesystems.logback.classic; import ch.qos.logback.core.joran.action.Action; import ch.qos.logback.core.joran.spi.ActionException; import ch.qos.logback.core.joran.spi.InterpretationContext; import org.slf4j.bridge.SLF4JBridgeHandler; import org.xml.sax.Attributes; /** * Provides SLF4JBridgeHandler installation as an action. This is useful because it means you don't * have to add custom code to your main method, and can completely initialize JUL by adding this. * *

Easiest way to do this is to use a custom rule: * *

"<newRule pattern="configuration/slf4jBridgeHandler" * actionClass="com.tersesystems.logback.classic.SLF4JBridgeHandlerAction"/>" * *

and then call it: * *

"<slf4jBridgeHandler/>" * *

You should use this in conjunction with the "LevelChangePropagator": * *

"<contextListener class="ch.qos.logback.classic.jul.LevelChangePropagator"/>" */ public class SLF4JBridgeHandlerAction extends Action { @Override public void begin(InterpretationContext ic, String name, Attributes attributes) throws ActionException { SLF4JBridgeHandler.removeHandlersForRootLogger(); SLF4JBridgeHandler.install(); } @Override public void end(InterpretationContext ic, String name) throws ActionException {} } ================================================ FILE: logback-classic/src/main/java/com/tersesystems/logback/classic/SetLoggerLevelsAction.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.classic; import ch.qos.logback.classic.Logger; import ch.qos.logback.classic.LoggerContext; import ch.qos.logback.core.joran.action.Action; import ch.qos.logback.core.joran.spi.ActionException; import ch.qos.logback.core.joran.spi.InterpretationContext; import java.util.Map; import org.xml.sax.Attributes; /** Sets the logger levels using a map with the levels key. */ public class SetLoggerLevelsAction extends Action { public static final String LEVELS_KEY = "levels"; public String levelsKey = LEVELS_KEY; public String getLevelsKey() { return levelsKey; } public void setLevelsKey(String levelsKey) { this.levelsKey = levelsKey; } @Override public void begin(InterpretationContext ic, String name, Attributes attributes) throws ActionException { doConfigure(ic); } @Override public void end(InterpretationContext ic, String name) throws ActionException {} @SuppressWarnings("unchecked") protected void doConfigure(InterpretationContext ic) { LoggerContext ctx = (LoggerContext) ic.getContext(); Map levelsMap = (Map) ctx.getObject(levelsKey); if (levelsMap == null) { addWarn("No levels found in context, cannot set levels."); return; } for (Map.Entry entry : levelsMap.entrySet()) { String name = entry.getKey(); try { Logger logger = ctx.getLogger(name); String level = entry.getValue(); new ChangeLogLevel().changeLogLevel(logger, level); addInfo("Setting level of " + name + " logger to " + level); } catch (Exception e) { addError("Unexpected exception resolving " + name, e); } } } } ================================================ FILE: logback-classic/src/main/java/com/tersesystems/logback/classic/StartTime.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.classic; import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.core.Context; import ch.qos.logback.core.spi.ContextAware; import com.tersesystems.logback.core.ComponentContainer; import java.time.Instant; import java.util.Iterator; import java.util.Optional; import org.slf4j.Marker; /** This class pulls an Instant from a StartTimeSupplier. */ public final class StartTime { /** * Returns a start time from the event, then marker, then finally if not found will use the * event's timestamp as the marker. * * @param context the logging context * @param event the logging event * @return an instant representing the start time. */ public static Instant from(Context context, ILoggingEvent event) { return fromOptional(context, event).orElse(Instant.ofEpochMilli(event.getTimeStamp())); } /** * Pulls a start time from the logging event, looking for the supplier on the event first, and * then looking for a StartTimeMarker. * * @param context the logback context * @param event the event * @return an optional start time, using both a container and marker as possible sources. */ public static Optional fromOptional(Context context, ILoggingEvent event) { if (event instanceof ComponentContainer) { ComponentContainer container = (ComponentContainer) event; if (container.hasComponent(StartTimeSupplier.class)) { return fromContainer(container); } } return fromMarker(context, event.getMarker()); } /** * Looks for a StartTimeMarker in the marker and in all the children of the marker. * * @param context the logback context * @param m the logback marker * @return an optional start time. */ public static Optional fromMarker(Context context, Marker m) { if (m instanceof StartTimeSupplier) { StartTimeSupplier supplier = ((StartTimeSupplier) m); return Optional.of(supplier.getStartTime()); } if (m != null && m.hasReferences()) { for (Iterator iter = m.iterator(); iter.hasNext(); ) { Marker child = iter.next(); if (child instanceof ContextAware) { ((ContextAware) child).setContext(context); } if (child instanceof StartTimeSupplier) { StartTimeSupplier supplier = ((StartTimeSupplier) child); return Optional.of(supplier.getStartTime()); } if (child.hasReferences()) { return fromMarker(context, child); } } } return Optional.empty(); } public static Optional fromContainer(ComponentContainer container) { StartTimeSupplier supplier = container.getComponent(StartTimeSupplier.class); return Optional.ofNullable(supplier.getStartTime()); } } ================================================ FILE: logback-classic/src/main/java/com/tersesystems/logback/classic/StartTimeConverter.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.classic; import ch.qos.logback.classic.pattern.ClassicConverter; import ch.qos.logback.classic.spi.ILoggingEvent; import java.util.Optional; /** Returns start time in milliseconds. */ public class StartTimeConverter extends ClassicConverter { @Override public String convert(ILoggingEvent event) { Optional optStartTime = StartTime.fromOptional(getContext(), event).map(st -> Long.toString(st.toEpochMilli())); return optStartTime.orElse(null); } } ================================================ FILE: logback-classic/src/main/java/com/tersesystems/logback/classic/StartTimeMarker.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.classic; import java.time.Instant; import java.util.Objects; public class StartTimeMarker extends TerseBasicMarker implements StartTimeSupplier { private static final String TS_STARTTIME_MARKER = "TS_STARTTIME_MARKER"; private final Instant startTime; public StartTimeMarker(Instant start) { super(TS_STARTTIME_MARKER); this.startTime = Objects.requireNonNull(start); } @Override public Instant getStartTime() { return startTime; } @Override public String toString() { return "StartTimeMarker{" + "startTime=" + startTime + '}'; } } ================================================ FILE: logback-classic/src/main/java/com/tersesystems/logback/classic/StartTimeSupplier.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.classic; import java.time.Instant; public interface StartTimeSupplier { Instant getStartTime(); } ================================================ FILE: logback-classic/src/main/java/com/tersesystems/logback/classic/TapFilter.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.classic; import ch.qos.logback.classic.Level; import ch.qos.logback.classic.Logger; import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.classic.spi.TurboFilterList; import ch.qos.logback.classic.turbo.TurboFilter; import ch.qos.logback.core.spi.AppenderAttachableImpl; import ch.qos.logback.core.spi.FilterReply; import com.tersesystems.logback.core.DefaultAppenderAttachable; import org.slf4j.Marker; /** * A tap filter is used to tap some amount of incoming process and pass them to a specially * configured appender even if they do not qualify as a logging event under normal circumstances. * This is a wiretap * pattern from Enterprise Integration Patterns. * *

It completely bypasses any kind of logging level configured on the front end, so you can set a * logger to INFO level but still have access to all TRACE events when an error occurs, through the * tap filter's appenders. * *

NOTE: This means that isLoggingTrace etc always returns true. */ public class TapFilter extends TurboFilter implements DefaultAppenderAttachable, TurboFilterDecider { private final AppenderAttachableImpl aae = new AppenderAttachableImpl<>(); private ILoggingEventFactory loggingEventFactory; private TurboFilterList evaluatorList = new TurboFilterList(); public void addTurboFilter(TurboFilter turboFilter) { evaluatorList.add(turboFilter); } public TurboFilterList getTurboFilters() { return evaluatorList; } public void getTurboFilters(TurboFilterList tapEvaluator) { this.evaluatorList = tapEvaluator; } @Override public AppenderAttachableImpl appenderAttachableImpl() { return aae; } public ILoggingEventFactory getLoggingEventFactory() { return loggingEventFactory; } public void setLoggingEventFactory(ILoggingEventFactory loggingEventFactory) { this.loggingEventFactory = loggingEventFactory; } @Override public void start() { if (this.loggingEventFactory == null) { this.loggingEventFactory = new LoggingEventFactory(); } if (evaluatorList.isEmpty()) { TurboFilter acceptAllTurboFilter = new TurboFilter() { @Override public FilterReply decide( Marker marker, Logger logger, Level level, String format, Object[] params, Throwable t) { return FilterReply.ACCEPT; } }; evaluatorList.add(acceptAllTurboFilter); } super.start(); } @Override public FilterReply decide( Marker marker, Logger logger, Level level, String format, Object[] params, Throwable t) { // Called by isLoggingTrace() -- there's no actual message here. if (logger != null && format == null && params == null) { // Need to turn on events for everything so that we can cover conditional events. return FilterReply.ACCEPT; } // Only tap if the internal filters pass. FilterReply turboFilterChainDecision = evaluatorList.getTurboFilterChainDecision(marker, logger, level, format, params, t); if (turboFilterChainDecision.equals(FilterReply.ACCEPT)) { ILoggingEvent loggingEvent = loggingEventFactory.create(marker, logger, level, format, params, t); // initialize the mdc in the logging event... loggingEvent.prepareForDeferredProcessing(); // For every message that is acceptable, store it in the appender and return. aae.appendLoopOnAppenders(loggingEvent); } return FilterReply.NEUTRAL; } } ================================================ FILE: logback-classic/src/main/java/com/tersesystems/logback/classic/TerseBasicMarker.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.classic; import static java.util.Objects.requireNonNull; import java.util.*; import org.slf4j.Marker; /** * A marker that can be extended with custom behavior. * *

Following on from logstash-logback-marker */ public class TerseBasicMarker implements Marker { private final String name; private List referenceList; public TerseBasicMarker(String name) { requireNonNull(name, "A marker name cannot be null"); this.name = name; } public String getName() { return name; } public synchronized void add(Marker reference) { requireNonNull(reference, "A null value cannot be added to a Marker as reference."); if (!(this.contains(reference) || reference.contains(this))) { if (referenceList == null) { referenceList = new Vector<>(); } referenceList.add(reference); } } public synchronized boolean hasReferences() { return referenceList != null && referenceList.size() > 0; } /** * @deprecated Replaced by {@link #hasReferences()}. */ @Deprecated public boolean hasChildren() { return hasReferences(); } public synchronized Iterator iterator() { return hasReferences() ? referenceList.iterator() : Collections.emptyIterator(); } public synchronized boolean remove(Marker referenceToRemove) { if (hasReferences()) { return referenceList.remove(referenceToRemove); } else { return false; } } public boolean contains(Marker other) { requireNonNull(other, "other cannot be null"); if (this.equals(other)) { return true; } else if (hasReferences()) { return referenceList.stream().anyMatch(ref -> ref.contains(other)); } else { return false; } } public boolean contains(String name) { requireNonNull(name, "name cannot be null"); if (this.name.equals(name)) { return true; } else if (hasReferences()) { return referenceList.stream().anyMatch(ref -> ref.contains(name)); } else { return false; } } private static final String OPEN = "[ "; private static final String CLOSE = " ]"; private static final String SEP = ", "; public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (!(obj instanceof Marker)) return false; final Marker other = (Marker) obj; return name.equals(other.getName()); } public int hashCode() { return name.hashCode(); } public String toString() { if (!this.hasReferences()) { return this.getName(); } Iterator it = this.iterator(); Marker reference; StringBuilder sb = new StringBuilder(this.getName()); sb.append(' ').append(OPEN); while (it.hasNext()) { reference = it.next(); sb.append(reference.getName()); if (it.hasNext()) { sb.append(SEP); } } sb.append(CLOSE); return sb.toString(); } } ================================================ FILE: logback-classic/src/main/java/com/tersesystems/logback/classic/TerseHighlightConverter.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.classic; import ch.qos.logback.classic.Level; import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.core.pattern.color.ANSIConstants; import ch.qos.logback.core.pattern.color.ForegroundCompositeConverterBase; import java.util.Map; import java.util.Optional; /** * Prints out a colored level using ANSI codes. Jansi is included here for Windows. * *

This is like %highlight but uses configured colors instead. * *

{@code
 * 
 * }
*/ public class TerseHighlightConverter extends ForegroundCompositeConverterBase { public static final String HIGHLIGHT_CTX_KEY = "highlight"; enum Color { BLACK(ANSIConstants.BLACK_FG), RED(ANSIConstants.RED_FG), GREEN(ANSIConstants.GREEN_FG), YELLOW(ANSIConstants.YELLOW_FG), BLUE(ANSIConstants.BLUE_FG), MAGENTA(ANSIConstants.MAGENTA_FG), CYAN(ANSIConstants.CYAN_FG), WHITE(ANSIConstants.WHITE_FG); final String code; Color(String code) { this.code = code; } } @Override protected String getForegroundColorCode(ILoggingEvent event) { String configKey = Optional.ofNullable(getFirstOption()).orElse(HIGHLIGHT_CTX_KEY); Map config = (Map) getContext().getObject(configKey); if (config == null) { addWarn("No map found in context with key " + configKey); return Color.BLACK.code; } Level level = event.getLevel(); String levelColor = config.get(level.levelStr.toLowerCase()).toUpperCase(); return Color.valueOf(levelColor).code; } } ================================================ FILE: logback-classic/src/main/java/com/tersesystems/logback/classic/TimeSinceEpochConverter.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.classic; import ch.qos.logback.classic.pattern.ClassicConverter; import ch.qos.logback.classic.spi.ILoggingEvent; public class TimeSinceEpochConverter extends ClassicConverter { @Override public String convert(ILoggingEvent event) { return Long.toString(event.getTimeStamp()); } } ================================================ FILE: logback-classic/src/main/java/com/tersesystems/logback/classic/TurboFilterDecider.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.classic; import ch.qos.logback.classic.Level; import ch.qos.logback.classic.Logger; import ch.qos.logback.core.spi.FilterReply; import org.slf4j.Marker; /** * An interface that decides what sort of filter reply there is. * *

Logback doesn't provide an interface for this out of the box for all turbofilters, so we have * to add one in by hand when we want decisions without the whole turbo filter. * *

This comes in handy for turbomarkers and tap filters. */ public interface TurboFilterDecider { FilterReply decide( Marker marker, Logger logger, Level level, String format, Object[] params, Throwable t); } ================================================ FILE: logback-classic/src/main/java/com/tersesystems/logback/classic/Utils.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.classic; import static java.util.Objects.requireNonNull; import ch.qos.logback.classic.Logger; import ch.qos.logback.classic.LoggerContext; import ch.qos.logback.classic.joran.JoranConfigurator; import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.classic.turbo.TurboFilter; import ch.qos.logback.classic.util.ContextSelectorStaticBinder; import ch.qos.logback.classic.util.LogbackMDCAdapter; import ch.qos.logback.core.Appender; import ch.qos.logback.core.joran.spi.JoranException; import ch.qos.logback.core.status.Status; import ch.qos.logback.core.status.StatusManager; import com.tersesystems.logback.classic.functional.GetAppenderFunction; import com.tersesystems.logback.classic.functional.RootLoggerSupplier; import java.net.URL; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Optional; import org.slf4j.ILoggerFactory; import org.slf4j.LoggerFactory; import org.slf4j.MDC; import org.slf4j.spi.MDCAdapter; public class Utils { private final LoggerContext loggerContext; Utils(LoggerContext loggerContext) { this.loggerContext = requireNonNull(loggerContext); } public static LoggerContext defaultContext() { ContextSelectorStaticBinder singleton = ContextSelectorStaticBinder.getSingleton(); if (singleton != null && singleton.getContextSelector() != null) { return singleton.getContextSelector().getLoggerContext(); } else { ILoggerFactory loggerFactory = LoggerFactory.getILoggerFactory(); return (LoggerContext) loggerFactory; } } public static LoggerContext contextFromResource(String resourcePath) throws JoranException { LoggerContext context = new LoggerContext(); URL resource = requireNonNull(Utils.class.getResource(requireNonNull(resourcePath))); JoranConfigurator configurator = new JoranConfigurator(); configurator.setContext(context); configurator.doConfigure(resource); return context; } public static Utils create(LoggerContext loggerContext) { return new Utils(loggerContext); } public static Utils create(String resourcePath) throws JoranException { return new Utils(contextFromResource(resourcePath)); } public static Utils create() { return new Utils(defaultContext()); } public List getStatusList() { StatusManager statusManager = getLoggerContext().getStatusManager(); return statusManager.getCopyOfStatusList(); } public LoggerContext getLoggerContext() { return loggerContext; } public Logger getRootLogger() { return RootLoggerSupplier.create(loggerContext).get(); } public Logger getLogger(String loggerName) { return (loggerContext.getLogger(loggerName)); } public Logger getLogger(Class clazz) { return (loggerContext.getLogger(clazz)); } public Optional getObject(Class classType, String name) { return Optional.ofNullable(loggerContext.getObject(name)) .filter(tf -> classType.isAssignableFrom(tf.getClass())) .map(classType::cast); } public Optional getTurboFilter( Class classType, String turboFilterName) { return loggerContext.getTurboFilterList().stream() .filter(tf -> tf.getName().equals(turboFilterName)) .filter(tf -> classType.isAssignableFrom(tf.getClass())) .map(classType::cast) .findFirst(); } public > Optional getAppender(String appenderName) { return GetAppenderFunction.create(loggerContext).apply(appenderName); } public Map getMDCPropertyMap() { MDCAdapter mdc = MDC.getMDCAdapter(); Map mdcPropertyMap; if (mdc instanceof LogbackMDCAdapter) mdcPropertyMap = ((LogbackMDCAdapter) mdc).getPropertyMap(); else mdcPropertyMap = mdc.getCopyOfContextMap(); // mdcPropertyMap still null, use emptyMap() if (mdcPropertyMap == null) mdcPropertyMap = Collections.emptyMap(); return mdcPropertyMap; } public LoggingEventFactory getLoggingEventFactory() { return new LoggingEventFactory(); } } ================================================ FILE: logback-classic/src/main/java/com/tersesystems/logback/classic/encoder/PatternLayoutEncoder.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.classic.encoder; import ch.qos.logback.classic.PatternLayout; import ch.qos.logback.classic.spi.ILoggingEvent; import com.tersesystems.logback.core.pattern.PatternLayoutEncoderBase; /** * Create a pattern layout encoder that doesn't require that the parent is an appender. * *

This allows for encoders that can take encoders and so on. */ public class PatternLayoutEncoder extends PatternLayoutEncoderBase { @Override public void start() { PatternLayout patternLayout = new PatternLayout(); patternLayout.setContext(context); patternLayout.setPattern(getPattern()); patternLayout.setOutputPatternAsHeader(outputPatternAsHeader); patternLayout.start(); this.layout = patternLayout; super.start(); } } ================================================ FILE: logback-classic/src/main/java/com/tersesystems/logback/classic/functional/GetAppenderFunction.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.classic.functional; import static java.util.Objects.requireNonNull; import ch.qos.logback.classic.Logger; import ch.qos.logback.classic.LoggerContext; import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.core.Appender; import com.tersesystems.logback.classic.Utils; import java.util.Optional; import java.util.function.Function; public class GetAppenderFunction> implements Function> { private final Logger rootLogger; public GetAppenderFunction(Logger rootLogger) { this.rootLogger = rootLogger; } @SuppressWarnings("unchecked") @Override public Optional apply(String appenderName) { Appender appender = rootLogger.getAppender(requireNonNull(appenderName)); try { return Optional.ofNullable((A) appender); } catch (ClassCastException e) { return Optional.empty(); } } public static > GetAppenderFunction create() { return create(Utils.defaultContext()); } public static > GetAppenderFunction create( LoggerContext context) { Logger logger = RootLoggerSupplier.create(context).get(); return new GetAppenderFunction<>(logger); } } ================================================ FILE: logback-classic/src/main/java/com/tersesystems/logback/classic/functional/GetSiftedAppenderFunction.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.classic.functional; import ch.qos.logback.classic.sift.SiftingAppender; import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.core.Appender; import ch.qos.logback.core.sift.AppenderTracker; import java.util.Optional; import java.util.function.Function; public class GetSiftedAppenderFunction implements Function> { private final String key; public GetSiftedAppenderFunction(String key) { this.key = key; } @SuppressWarnings("unchecked") @Override public Optional apply(SiftingAppender siftingAppender) { AppenderTracker appenderTracker = siftingAppender.getAppenderTracker(); try { return Optional.ofNullable((A) appenderTracker.find(key)); } catch (ClassCastException e) { return Optional.empty(); } } public static > GetSiftedAppenderFunction create( String key) { return new GetSiftedAppenderFunction(key); } } ================================================ FILE: logback-classic/src/main/java/com/tersesystems/logback/classic/functional/RootLoggerSupplier.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.classic.functional; import ch.qos.logback.classic.Logger; import ch.qos.logback.classic.LoggerContext; import com.tersesystems.logback.classic.Utils; import java.util.function.Supplier; public class RootLoggerSupplier implements Supplier { private final LoggerContext loggerContext; public RootLoggerSupplier(LoggerContext loggerContext) { this.loggerContext = loggerContext; } public Logger get() { return loggerContext.getLogger(Logger.ROOT_LOGGER_NAME); } public static RootLoggerSupplier create(LoggerContext loggerContext) { return new RootLoggerSupplier(loggerContext); } public static RootLoggerSupplier create() { return create(Utils.defaultContext()); } } ================================================ FILE: logback-classic/src/main/java/com/tersesystems/logback/classic/sift/DiscriminatingMarker.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.classic.sift; import ch.qos.logback.classic.spi.ILoggingEvent; import com.tersesystems.logback.classic.TerseBasicMarker; import java.util.function.Function; public class DiscriminatingMarker extends TerseBasicMarker implements DiscriminatingValue { private static final String TS_DISCRIMINATING_MARKER = "TS_DESCRIMINATING_MARKER"; private final Function discriminatingFunction; public DiscriminatingMarker(Function discriminatingFunction) { super(TS_DISCRIMINATING_MARKER); this.discriminatingFunction = discriminatingFunction; } @Override public String getDiscriminatingValue(ILoggingEvent loggingEvent) { return discriminatingFunction.apply(loggingEvent); } } ================================================ FILE: logback-classic/src/main/java/com/tersesystems/logback/classic/sift/DiscriminatingMarkerFactory.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.classic.sift; import ch.qos.logback.classic.spi.ILoggingEvent; import java.util.function.Function; public class DiscriminatingMarkerFactory { private final Function discriminatingFunction; public DiscriminatingMarkerFactory(Function discriminatingFunction) { this.discriminatingFunction = discriminatingFunction; } public static DiscriminatingMarkerFactory create( Function discriminatingFunction) { return new DiscriminatingMarkerFactory(discriminatingFunction); } public DiscriminatingMarker createMarker() { return new DiscriminatingMarker(discriminatingFunction); } } ================================================ FILE: logback-classic/src/main/java/com/tersesystems/logback/classic/sift/DiscriminatingValue.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.classic.sift; import ch.qos.logback.classic.spi.ILoggingEvent; public interface DiscriminatingValue { String getDiscriminatingValue(ILoggingEvent loggingEvent); } ================================================ FILE: logback-classic/src/main/java/com/tersesystems/logback/classic/sift/MarkerBasedDiscriminator.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.classic.sift; import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.core.sift.AbstractDiscriminator; import ch.qos.logback.core.sift.DefaultDiscriminator; import java.util.Iterator; import java.util.Optional; import org.slf4j.Marker; /** * A discriminator that looks for a marker containing discriminating logic. * * @param the logging event type */ public class MarkerBasedDiscriminator extends AbstractDiscriminator { private String key = "key"; private String defaultValue = DefaultDiscriminator.DEFAULT; @Override public String getDiscriminatingValue(ILoggingEvent loggingEvent) { Optional optMarker = getDiscriminatorMarker(loggingEvent); return optMarker.map(m -> m.getDiscriminatingValue(loggingEvent)).orElse(getDefaultValue()); } public Optional getDiscriminatorMarker(ILoggingEvent loggingEvent) { return fromMarker(loggingEvent.getMarker()); } static Optional fromMarker(Marker m) { if (m instanceof DiscriminatingValue) { DiscriminatingValue value = ((DiscriminatingValue) m); return Optional.of(value); } for (Iterator iter = m.iterator(); iter.hasNext(); ) { Marker child = iter.next(); if (child instanceof DiscriminatingValue) { DiscriminatingValue value = ((DiscriminatingValue) child); return Optional.of(value); } if (child.hasReferences()) { return fromMarker(child); } } return Optional.empty(); } @Override public String getKey() { return this.key; } public void setKey(String key) { this.key = key; } public String getDefaultValue() { return defaultValue; } public void setDefaultValue(String defaultValue) { this.defaultValue = defaultValue; } } ================================================ FILE: logback-classic/src/test/java/com/tersesystems/logback/classic/ChangeLogLevelTest.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.classic; import org.junit.Test; public class ChangeLogLevelTest { @Test public void testChangeLogLevel() { // ILoggerFactory loggerFactory = LoggerFactory.getILoggerFactory(); // ChangeLogLevel changeLogLevel = new ChangeLogLevel(); // Logger logger = loggerFactory.getLogger("example"); // assertThat(logger.isTraceEnabled()).isFalse(); // changeLogLevel.changeLogLevel(logger, "TRACE"); // assertThat(logger.isTraceEnabled()).isTrue(); } } ================================================ FILE: logback-classic/src/test/java/com/tersesystems/logback/classic/CorrelationIdMarker.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.classic; import org.slf4j.Marker; /** A very simple correlation id marker. */ public interface CorrelationIdMarker extends Marker { String getCorrelationId(); static CorrelationIdMarker create(String value) { return new CorrelationIdBasicMarker(value); } } /** Implementation of correlation id. */ class CorrelationIdBasicMarker extends TerseBasicMarker implements CorrelationIdMarker { private final String value; public CorrelationIdBasicMarker(String value) { super("TS_CORRELATION_ID"); this.value = value; } public String getCorrelationId() { return value; } } ================================================ FILE: logback-classic/src/test/java/com/tersesystems/logback/classic/CorrelationIdTurboFilter.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.classic; import ch.qos.logback.classic.Level; import ch.qos.logback.classic.Logger; import ch.qos.logback.classic.LoggerContext; import ch.qos.logback.classic.turbo.TurboFilter; import ch.qos.logback.core.spi.FilterReply; import java.util.Iterator; import java.util.Map; import java.util.function.Predicate; import org.slf4j.Marker; /** Tells the tap filter to create an event and append it if a correlation id is found. */ public class CorrelationIdTurboFilter extends TurboFilter { private String mdcKey = "correlation_id"; private Utils utils; public String getMdcKey() { return mdcKey; } public void setMdcKey(String mdcKey) { this.mdcKey = mdcKey; } @Override public void start() { super.start(); utils = Utils.create((LoggerContext) getContext()); } boolean doMarker(Marker m, Predicate predicate) { if (predicate.test(m)) { return true; } if (m != null && m.hasReferences()) { for (Iterator iter = m.iterator(); iter.hasNext(); ) { Marker child = iter.next(); if (predicate.test(child)) { return true; } if (child.hasReferences()) { return doMarker(child, predicate); } } } return false; } @Override public FilterReply decide( Marker marker, Logger logger, Level level, String format, Object[] params, Throwable t) { // If there's a correlation id marker somewhere in the hierarchy, then good. if (doMarker(marker, m -> m instanceof CorrelationIdMarker)) { return FilterReply.ACCEPT; } // Look in MDC for a correlation id as well... Map mdcPropertyMap = utils.getMDCPropertyMap(); String mdcKey = getMdcKey(); if (mdcKey != null) { if (mdcPropertyMap.containsKey(mdcKey)) { return FilterReply.ACCEPT; } } // Otherwise no. return FilterReply.DENY; } } ================================================ FILE: logback-classic/src/test/java/com/tersesystems/logback/classic/EnabledFilterTest.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.classic; import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.classic.spi.LoggingEventVO; import ch.qos.logback.core.spi.FilterReply; import com.tersesystems.logback.core.EnabledFilter; import org.assertj.core.api.Assertions; import org.junit.Test; public class EnabledFilterTest { @Test public void testFilterFalse() { EnabledFilter enabledFilter = new EnabledFilter(); enabledFilter.setEnabled(false); enabledFilter.start(); ILoggingEvent loggingEvent = new LoggingEventVO(); Assertions.assertThat(enabledFilter.decide(loggingEvent)).isEqualTo(FilterReply.DENY); } @Test public void testFilterTrue() { EnabledFilter enabledFilter = new EnabledFilter(); enabledFilter.setEnabled(true); enabledFilter.start(); ILoggingEvent loggingEvent = new LoggingEventVO(); Assertions.assertThat(enabledFilter.decide(loggingEvent)).isEqualTo(FilterReply.NEUTRAL); } } ================================================ FILE: logback-classic/src/test/java/com/tersesystems/logback/classic/ExceptionMessageConverterTest.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.classic; import static org.assertj.core.api.Assertions.assertThat; import ch.qos.logback.classic.Level; import ch.qos.logback.classic.LoggerContext; import ch.qos.logback.classic.spi.LoggingEvent; import java.util.Arrays; import org.junit.Test; public class ExceptionMessageConverterTest { @Test public void testNoException() { ExceptionMessageConverter converter = new ExceptionMessageConverter(); LoggerContext context = new LoggerContext(); converter.setContext(context); converter.start(); LoggingEvent infoEvent = new LoggingEvent("fcqn", context.getLogger("fcqn"), Level.INFO, "info", null, null); String actual = converter.convert(infoEvent); assertThat(actual).contains(""); } @Test public void testSingleMessage() { ExceptionMessageConverter converter = new ExceptionMessageConverter(); LoggerContext context = new LoggerContext(); converter.setContext(context); converter.start(); RuntimeException ex = new RuntimeException("Hello world"); LoggingEvent infoEvent = new LoggingEvent("fcqn", context.getLogger("fcqn"), Level.INFO, "info", ex, null); String actual = converter.convert(infoEvent); assertThat(actual).isEqualTo(" [Hello world]"); } @Test public void testNestedMessages() { ExceptionMessageConverter converter = new ExceptionMessageConverter(); LoggerContext context = new LoggerContext(); converter.setContext(context); converter.start(); RuntimeException one = new RuntimeException("One"); RuntimeException two = new RuntimeException("Two", one); RuntimeException three = new RuntimeException("Three", two); RuntimeException four = new RuntimeException("Four", three); LoggingEvent infoEvent = new LoggingEvent("fcqn", context.getLogger("fcqn"), Level.INFO, "info", four, null); String actual = converter.convert(infoEvent); assertThat(actual).isEqualTo(" [Four > Three > Two > One]"); } @Test public void testNestedMessagesWithCutOff() { ExceptionMessageConverter converter = new ExceptionMessageConverter(); converter.setOptionList(Arrays.asList("1", "2")); LoggerContext context = new LoggerContext(); converter.setContext(context); converter.start(); RuntimeException one = new RuntimeException("One"); RuntimeException two = new RuntimeException("Two", one); RuntimeException three = new RuntimeException("Three", two); RuntimeException four = new RuntimeException("Four", three); LoggingEvent infoEvent = new LoggingEvent("fcqn", context.getLogger("fcqn"), Level.INFO, "info", four, null); String actual = converter.convert(infoEvent); assertThat(actual).isEqualTo(" [Four > Three]"); } @Test public void testNestedMessagesSeperator() { ExceptionMessageConverter converter = new ExceptionMessageConverter(); converter.setOptionList(Arrays.asList("1", "4", "[", " ! ")); LoggerContext context = new LoggerContext(); converter.setContext(context); converter.start(); RuntimeException one = new RuntimeException("One"); RuntimeException two = new RuntimeException("Two", one); RuntimeException three = new RuntimeException("Three", two); RuntimeException four = new RuntimeException("Four", three); LoggingEvent infoEvent = new LoggingEvent("fcqn", context.getLogger("fcqn"), Level.INFO, "info", four, null); String actual = converter.convert(infoEvent); assertThat(actual).isEqualTo(" [Four ! Three ! Two ! One]"); } @Test public void testCustomPrefixSuffix() { ExceptionMessageConverter converter = new ExceptionMessageConverter(); converter.setOptionList(Arrays.asList("0", "4", "<", "|", ">")); LoggerContext context = new LoggerContext(); converter.setContext(context); converter.start(); RuntimeException one = new RuntimeException("One"); RuntimeException two = new RuntimeException("Two", one); RuntimeException three = new RuntimeException("Three", two); RuntimeException four = new RuntimeException("Four", three); LoggingEvent infoEvent = new LoggingEvent("fcqn", context.getLogger("fcqn"), Level.INFO, "info", four, null); String actual = converter.convert(infoEvent); assertThat(actual).isEqualTo(""); } } ================================================ FILE: logback-classic/src/test/java/com/tersesystems/logback/classic/SetLoggerLevelsActionTest.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.classic; public class SetLoggerLevelsActionTest {} ================================================ FILE: logback-classic/src/test/java/com/tersesystems/logback/classic/TapFilterTest.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.classic; import static java.util.Objects.requireNonNull; import static org.assertj.core.api.Assertions.assertThat; import ch.qos.logback.classic.Logger; import ch.qos.logback.classic.LoggerContext; import ch.qos.logback.classic.joran.JoranConfigurator; import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.classic.spi.TurboFilterList; import ch.qos.logback.core.Appender; import ch.qos.logback.core.joran.spi.JoranException; import ch.qos.logback.core.read.ListAppender; import ch.qos.logback.core.spi.AppenderAttachable; import java.net.URL; import java.util.Optional; import org.junit.After; import org.junit.Before; import org.junit.jupiter.api.Test; import org.slf4j.MDC; public class TapFilterTest { @Before @After public void clearMDC() { MDC.clear(); } @Test public void testSimple() throws JoranException { LoggerContext loggerFactory = createLoggerFactory("/logback-tapfilter.xml"); // Write something that never gets logged explicitly... Logger debugLogger = loggerFactory.getLogger("com.example.Debug"); debugLogger.debug("debug one"); debugLogger.debug("debug two"); debugLogger.debug("debug three"); debugLogger.debug("debug four"); Logger logger = loggerFactory.getLogger("com.example.Test"); logger.error("Write out error message to console"); ListAppender listAppender = getListAppender(loggerFactory); assertThat(listAppender.list.size()).isEqualTo(5); } @Test public void testCorrelationWithNoMarker() throws JoranException { LoggerContext loggerFactory = createLoggerFactory("/logback-tapfilter-correlation.xml"); // Because there's no correlation id, it should never make it past the filter here. Logger debugLogger = loggerFactory.getLogger("com.example.Debug"); debugLogger.debug("debug one"); debugLogger.debug("debug two"); debugLogger.debug("debug three"); debugLogger.debug("debug four"); Logger logger = loggerFactory.getLogger("com.example.Test"); logger.error("Write out error message to console"); ListAppender listAppender = getListAppender(loggerFactory); assertThat(listAppender.list.size()).isEqualTo(0); } @Test public void testCorrelationWithMarker() throws JoranException { LoggerContext loggerFactory = createLoggerFactory("/logback-tapfilter-correlation.xml"); CorrelationIdMarker correlationIdMarker = CorrelationIdMarker.create("12345"); // Because there's no correlation id, it should never make it past the filter here. Logger debugLogger = loggerFactory.getLogger("com.example.Debug"); debugLogger.debug(correlationIdMarker, "debug one"); debugLogger.debug(correlationIdMarker, "debug two"); debugLogger.debug("debug three"); debugLogger.debug("debug four"); Logger logger = loggerFactory.getLogger("com.example.Test"); logger.error("Write out error message to console"); ListAppender listAppender = getListAppender(loggerFactory); assertThat(listAppender.list.size()).isEqualTo(2); } @Test public void testCorrelationWithMDC() throws JoranException { LoggerContext loggerFactory = createLoggerFactory("/logback-tapfilter-correlation.xml"); MDC.put("correlationId", "12345"); CorrelationIdMarker correlationIdMarker = CorrelationIdMarker.create("12345"); // Because there's no correlation id, it should never make it past the filter here. Logger debugLogger = loggerFactory.getLogger("com.example.Debug"); debugLogger.debug(correlationIdMarker, "debug one"); debugLogger.debug(correlationIdMarker, "debug two"); debugLogger.debug("debug three"); debugLogger.debug("debug four"); Logger logger = loggerFactory.getLogger("com.example.Test"); logger.error("Write out error message to console"); ListAppender listAppender = getListAppender(loggerFactory); assertThat(listAppender.list.size()).isEqualTo(5); } LoggerContext createLoggerFactory(String resourceName) throws JoranException { LoggerContext context = new LoggerContext(); URL resource = getClass().getResource(resourceName); JoranConfigurator configurator = new JoranConfigurator(); configurator.setContext(context); configurator.doConfigure(resource); return context; } Optional> getFilterAppender(TurboFilterList turboFilterList) { return turboFilterList.stream() .filter(f -> f instanceof AppenderAttachable) .map(f -> ((AppenderAttachable) f).iteratorForAppenders().next()) .findFirst(); } ListAppender getListAppender(LoggerContext context) { Optional> maybeAppender = getFilterAppender(context.getTurboFilterList()); if (maybeAppender.isPresent()) { return (ListAppender) requireNonNull(maybeAppender.get()); } else { throw new IllegalStateException("Cannot find appender"); } } } ================================================ FILE: logback-classic/src/test/java/com/tersesystems/logback/classic/TerseHighlightConverterTest.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.classic; import static org.assertj.core.api.Assertions.assertThat; import ch.qos.logback.classic.Level; import ch.qos.logback.classic.LoggerContext; import ch.qos.logback.classic.spi.LoggingEvent; import java.util.HashMap; import java.util.Map; import org.junit.Test; public class TerseHighlightConverterTest { @Test public void testHighlighter() { TerseHighlightConverter converter = new TerseHighlightConverter(); LoggerContext context = new LoggerContext(); converter.setContext(context); Map properties = new HashMap<>(); properties.put("info", "red"); context.putObject(TerseHighlightConverter.HIGHLIGHT_CTX_KEY, properties); converter.start(); LoggingEvent infoEvent = new LoggingEvent("fcqn", context.getLogger("fcqn"), Level.INFO, "info", null, null); String actual = converter.convert(infoEvent); assertThat(actual).contains(TerseHighlightConverter.Color.valueOf("RED").code); } } ================================================ FILE: logback-classic/src/test/java/com/tersesystems/logback/classic/UtilsTest.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.classic; import static org.assertj.core.api.Assertions.assertThat; import ch.qos.logback.classic.Level; import ch.qos.logback.classic.Logger; import ch.qos.logback.classic.LoggerContext; import ch.qos.logback.classic.turbo.MDCFilter; import ch.qos.logback.classic.turbo.TurboFilter; import ch.qos.logback.core.spi.FilterReply; import org.junit.jupiter.api.Test; import org.slf4j.Marker; public class UtilsTest { static class FancyTurboFilter extends TurboFilter { @Override public FilterReply decide( Marker marker, Logger logger, Level level, String format, Object[] params, Throwable t) { return null; } } @Test public void testTurboFilterMatchingType() { LoggerContext loggerContext = new LoggerContext(); FancyTurboFilter fancyTurboFilter = new FancyTurboFilter(); fancyTurboFilter.setName("fancyTurboFilter"); fancyTurboFilter.setContext(loggerContext); loggerContext.addTurboFilter(fancyTurboFilter); Utils utils = Utils.create(loggerContext); assertThat(utils.getTurboFilter(FancyTurboFilter.class, "fancyTurboFilter")).isNotEmpty(); } @Test public void testTurboFilterNonMatchingType() { LoggerContext loggerContext = new LoggerContext(); FancyTurboFilter fancyTurboFilter = new FancyTurboFilter(); fancyTurboFilter.setName("fancyTurboFilter"); fancyTurboFilter.setContext(loggerContext); loggerContext.addTurboFilter(fancyTurboFilter); Utils utils = Utils.create(loggerContext); assertThat(utils.getTurboFilter(MDCFilter.class, "fancyTurboFilter")).isEmpty(); } } ================================================ FILE: logback-classic/src/test/resources/logback-tapfilter-correlation.xml ================================================ correlationId %-5relative %-5level %logger{35} - %msg%n ================================================ FILE: logback-classic/src/test/resources/logback-tapfilter.xml ================================================ %-5relative %-5level %logger{35} - %msg%n ================================================ FILE: logback-compress-encoder/gradle.properties ================================================ # # SPDX-License-Identifier: CC0-1.0 # # Copyright 2018-2020 Will Sargent. # # Licensed under the CC0 Public Domain Dedication; # You may obtain a copy of the License at # # http://creativecommons.org/publicdomain/zero/1.0/ # project_description = Logback Compress Encoder ================================================ FILE: logback-compress-encoder/logback-compress-encoder.gradle ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ dependencies { implementation project(':logback-classic') implementation group: 'org.apache.commons', name: 'commons-compress', version: '1.18' implementation "com.github.luben:zstd-jni:$zstdVersion" testImplementation "ch.qos.logback:logback-classic:$logbackVersion" } ================================================ FILE: logback-compress-encoder/src/main/java/com.tersesystems.logback.compress/CompressingEncoder.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.compress; import ch.qos.logback.core.encoder.Encoder; import ch.qos.logback.core.encoder.EncoderBase; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.concurrent.atomic.LongAdder; import org.apache.commons.compress.compressors.CompressorException; import org.apache.commons.compress.compressors.CompressorOutputStream; import org.apache.commons.compress.compressors.CompressorStreamFactory; public class CompressingEncoder extends EncoderBase { private final Accumulator accumulator; private final Encoder encoder; public CompressingEncoder( Encoder encoder, String compressAlgo, CompressorStreamFactory factory, int bufferSize) throws CompressorException { this.encoder = encoder; this.accumulator = new Accumulator(compressAlgo, factory, bufferSize); } @Override public byte[] headerBytes() { try { return accumulator.apply(encoder.headerBytes()); } catch (IOException e) { throw new RuntimeException(e); } } @Override public byte[] encode(E event) { try { return accumulator.apply(encoder.encode(event)); } catch (IOException e) { throw new RuntimeException(e); } } @Override public byte[] footerBytes() { try { return accumulator.drain(encoder.footerBytes()); } catch (IOException e) { throw new RuntimeException(e); } } static class Accumulator { private final ByteArrayOutputStream byteOutputStream; private final CompressorOutputStream stream; private final LongAdder count = new LongAdder(); private final int bufferSize; public Accumulator(String compressAlgo, CompressorStreamFactory factory, int bufferSize) throws CompressorException { this.bufferSize = bufferSize; this.byteOutputStream = new ByteArrayOutputStream(); this.stream = factory.createCompressorOutputStream(compressAlgo, byteOutputStream); } boolean isFlushable() { return count.intValue() >= bufferSize; } byte[] apply(byte[] bytes) throws IOException { count.add(bytes.length); stream.write(bytes); if (isFlushable()) { stream.flush(); byte[] output = byteOutputStream.toByteArray(); byteOutputStream.reset(); count.reset(); return output; } else { return new byte[0]; } } byte[] drain(byte[] inputBytes) throws IOException { if (inputBytes != null) { stream.write(inputBytes); } stream.close(); count.reset(); return byteOutputStream.toByteArray(); } } } ================================================ FILE: logback-compress-encoder/src/main/java/com.tersesystems.logback.compress/CompressingFileAppender.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.compress; import ch.qos.logback.core.FileAppender; import ch.qos.logback.core.UnsynchronizedAppenderBase; import ch.qos.logback.core.encoder.Encoder; import java.util.Set; import org.apache.commons.compress.compressors.CompressorException; import org.apache.commons.compress.compressors.CompressorStreamFactory; public class CompressingFileAppender extends UnsynchronizedAppenderBase { protected Encoder encoder; private FileAppender fileAppender; protected boolean append = true; protected String fileName = null; private boolean prudent = false; private int bufferSize = 1024000; private String compressAlgo = CompressorStreamFactory.getGzip(); public Encoder getEncoder() { return encoder; } public void setEncoder(Encoder encoder) { this.encoder = encoder; } public boolean isPrudent() { return prudent; } public void setPrudent(boolean prudent) { this.prudent = prudent; } public void setAppend(boolean append) { this.append = append; } public void setFile(String file) { fileName = file; } public boolean isAppend() { return append; } public String getFile() { return fileName; } public int getBufferSize() { return bufferSize; } public void setBufferSize(int bufferSize) { this.bufferSize = bufferSize; } public String getCompressAlgo() { return compressAlgo; } public void setCompressAlgo(String compressAlgo) { this.compressAlgo = compressAlgo; } @Override public void start() { fileAppender = new FileAppender<>(); fileAppender.setContext(getContext()); fileAppender.setFile(getFile()); fileAppender.setImmediateFlush(false); fileAppender.setPrudent(isPrudent()); fileAppender.setAppend(isAppend()); fileAppender.setName(name + "-embedded-file"); CompressingEncoder compressedEncoder = createCompressingEncoder(getEncoder()); fileAppender.setEncoder(compressedEncoder); fileAppender.start(); super.start(); } public void stop() { fileAppender.stop(); super.stop(); } @Override protected void append(E eventObject) { fileAppender.doAppend(eventObject); } protected CompressingEncoder createCompressingEncoder(Encoder e) { int bufferSize = getBufferSize(); String compressAlgo = getCompressAlgo(); CompressorStreamFactory factory = CompressorStreamFactory.getSingleton(); Set names = factory.getOutputStreamCompressorNames(); if (names.contains(getCompressAlgo())) { try { return new CompressingEncoder<>(e, compressAlgo, factory, bufferSize); } catch (CompressorException ex) { throw new RuntimeException("Cannot create CompressingEncoder", ex); } } else { throw new RuntimeException("No such compression algorithm: " + compressAlgo); } } } ================================================ FILE: logback-compress-encoder/src/test/java/com/tersesystems/logback/compress/Utils.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.compress; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; public class Utils { public static byte[] readAllBytes(InputStream inputStream) throws IOException { final int bufLen = 4 * 0x400; // 4KB byte[] buf = new byte[bufLen]; int readLen; IOException exception = null; try { try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { while ((readLen = inputStream.read(buf, 0, bufLen)) != -1) outputStream.write(buf, 0, readLen); return outputStream.toByteArray(); } } catch (IOException e) { exception = e; throw e; } finally { if (exception == null) inputStream.close(); else try { inputStream.close(); } catch (IOException e) { exception.addSuppressed(e); } } } } ================================================ FILE: logback-compress-encoder/src/test/resources/logback-with-zstd-encoder.xml ================================================ encoded.zst zstd 10240 UTF-8 %-5level %logger{35} - %msg%n ================================================ FILE: logback-core/gradle.properties ================================================ # # SPDX-License-Identifier: CC0-1.0 # # Copyright 2018-2020 Will Sargent. # # Licensed under the CC0 Public Domain Dedication; # You may obtain a copy of the License at # # http://creativecommons.org/publicdomain/zero/1.0/ # project_description = Logback Core (code only) Library ================================================ FILE: logback-core/logback-core.gradle ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ plugins { id 'java-library' } dependencies { api "org.slf4j:slf4j-api:$slf4jVersion" implementation "ch.qos.logback:logback-core:$logbackVersion" testImplementation "ch.qos.logback:logback-classic:$logbackVersion" } ================================================ FILE: logback-core/src/main/java/com/tersesystems/logback/core/AbstractAppender.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.core; import ch.qos.logback.core.Appender; import ch.qos.logback.core.UnsynchronizedAppenderBase; import ch.qos.logback.core.spi.AppenderAttachable; import ch.qos.logback.core.spi.AppenderAttachableImpl; import java.util.Iterator; /** * Provides abstract appender behavior with pre / post behavior. * * @param the input type, usually ILoggingEvent */ public abstract class AbstractAppender extends UnsynchronizedAppenderBase implements AppenderAttachable { protected AppenderAttachableImpl aai = new AppenderAttachableImpl(); protected abstract E appendEvent(E eventObject); @Override protected void append(E eventObject) { preAppend(); aai.appendLoopOnAppenders(appendEvent(eventObject)); postAppend(); } protected void postAppend() {} protected void preAppend() {} public void addAppender(Appender newAppender) { addInfo("Attaching appender named [" + newAppender.getName() + "] to " + this.toString()); aai.addAppender(newAppender); } public Iterator> iteratorForAppenders() { return aai.iteratorForAppenders(); } public void stop() { super.stop(); aai.detachAndStopAllAppenders(); } public Appender getAppender(String name) { return aai.getAppender(name); } public boolean isAttached(Appender eAppender) { return aai.isAttached(eAppender); } public void detachAndStopAllAppenders() { aai.detachAndStopAllAppenders(); } public boolean detachAppender(Appender eAppender) { return aai.detachAppender(eAppender); } public boolean detachAppender(String name) { return aai.detachAppender(name); } } ================================================ FILE: logback-core/src/main/java/com/tersesystems/logback/core/Component.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.core; /** A marker interface to let the caller know this can be placed in a ContainerComponent. */ public interface Component {} ================================================ FILE: logback-core/src/main/java/com/tersesystems/logback/core/ComponentContainer.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.core; /** * A component container. * *

Entries are encouraged but not required to extend Component. */ public interface ComponentContainer { void putComponent(Class type, T instance); T getComponent(Class type); boolean hasComponent(Class type); } ================================================ FILE: logback-core/src/main/java/com/tersesystems/logback/core/CompositeAppender.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.core; import ch.qos.logback.core.Appender; import ch.qos.logback.core.UnsynchronizedAppenderBase; import ch.qos.logback.core.spi.AppenderAttachable; import ch.qos.logback.core.spi.AppenderAttachableImpl; import java.util.Iterator; /** * This appender creates a composite of the underlying appenders but does not add or change any * functionality of those appenders. * *

It is very useful for referring to a list of appenders by a single name. * * @param the event type, usually ILoggingEvent. */ public class CompositeAppender extends UnsynchronizedAppenderBase implements AppenderAttachable { protected AppenderAttachableImpl aai = new AppenderAttachableImpl(); @Override public void stop() { if (isStarted()) { detachAndStopAllAppenders(); } super.stop(); } @Override protected void append(E eventObject) { aai.appendLoopOnAppenders(eventObject); } public void addAppender(Appender newAppender) { addInfo("Attaching appender named [" + newAppender.getName() + "] to " + this.toString()); aai.addAppender(newAppender); } public Iterator> iteratorForAppenders() { return aai.iteratorForAppenders(); } public Appender getAppender(String name) { return aai.getAppender(name); } public boolean isAttached(Appender eAppender) { return aai.isAttached(eAppender); } public void detachAndStopAllAppenders() { aai.detachAndStopAllAppenders(); } public boolean detachAppender(Appender eAppender) { return aai.detachAppender(eAppender); } public boolean detachAppender(String name) { return aai.detachAppender(name); } } ================================================ FILE: logback-core/src/main/java/com/tersesystems/logback/core/DecoratingAppender.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.core; import ch.qos.logback.core.Appender; import ch.qos.logback.core.UnsynchronizedAppenderBase; import ch.qos.logback.core.spi.AppenderAttachable; import ch.qos.logback.core.spi.AppenderAttachableImpl; import java.util.Iterator; /** * Decorates an event with additional class, using {@code decorateEvent}, and makes it available to * the appenders underneath it. * * @param the input type, usually ILoggingEvent * @param the decorating type, must extend ILoggingEvent */ public abstract class DecoratingAppender extends UnsynchronizedAppenderBase implements AppenderAttachable { protected AppenderAttachableImpl aai = new AppenderAttachableImpl(); protected abstract EE decorateEvent(E eventObject); @Override protected void append(E eventObject) { aai.appendLoopOnAppenders(decorateEvent(eventObject)); } public void addAppender(Appender newAppender) { addInfo("Attaching appender named [" + newAppender.getName() + "] to " + this.toString()); aai.addAppender(newAppender); } public Iterator> iteratorForAppenders() { return aai.iteratorForAppenders(); } public Appender getAppender(String name) { return aai.getAppender(name); } public boolean isAttached(Appender eAppender) { return aai.isAttached(eAppender); } public void detachAndStopAllAppenders() { aai.detachAndStopAllAppenders(); } public boolean detachAppender(Appender eAppender) { return aai.detachAppender(eAppender); } public boolean detachAppender(String name) { return aai.detachAppender(name); } public void stop() { if (isStarted()) { aai.detachAndStopAllAppenders(); } super.stop(); } } ================================================ FILE: logback-core/src/main/java/com/tersesystems/logback/core/DefaultAppenderAttachable.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.core; import ch.qos.logback.core.Appender; import ch.qos.logback.core.spi.AppenderAttachable; import ch.qos.logback.core.spi.AppenderAttachableImpl; import java.util.Iterator; public interface DefaultAppenderAttachable extends AppenderAttachable { AppenderAttachableImpl appenderAttachableImpl(); default void addAppender(Appender newAppender) { appenderAttachableImpl().addAppender(newAppender); } default Iterator> iteratorForAppenders() { return appenderAttachableImpl().iteratorForAppenders(); } default Appender getAppender(String name) { return appenderAttachableImpl().getAppender(name); } default boolean isAttached(Appender eAppender) { return appenderAttachableImpl().isAttached(eAppender); } default void detachAndStopAllAppenders() { appenderAttachableImpl().detachAndStopAllAppenders(); } default boolean detachAppender(Appender eAppender) { return appenderAttachableImpl().detachAppender(eAppender); } default boolean detachAppender(String name) { return appenderAttachableImpl().detachAppender(name); } } ================================================ FILE: logback-core/src/main/java/com/tersesystems/logback/core/EnabledFilter.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.core; import ch.qos.logback.core.filter.Filter; import ch.qos.logback.core.spi.FilterReply; /** Used to enable and disable appenders. */ public class EnabledFilter extends Filter { private boolean enabled; @Override public FilterReply decide(E event) { if (isStarted() && isEnabled()) { return FilterReply.NEUTRAL; } else { return FilterReply.DENY; } } public boolean isEnabled() { return enabled; } public void setEnabled(boolean enabled) { this.enabled = enabled; } } ================================================ FILE: logback-core/src/main/java/com/tersesystems/logback/core/SelectAppender.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.core; import ch.qos.logback.core.Appender; import ch.qos.logback.core.AppenderBase; import ch.qos.logback.core.spi.AppenderAttachable; import ch.qos.logback.core.spi.AppenderAttachableImpl; import java.util.Iterator; /** This class selects an appender by the appender key. */ public class SelectAppender extends AppenderBase implements AppenderAttachable { private AppenderAttachableImpl aai = new AppenderAttachableImpl(); private String appenderKey; @Override public void start() { if (appenderKey == null || appenderKey.isEmpty()) { addError("Null or empty appenderKey"); } else { super.start(); } } @Override public void stop() { if (isStarted()) { detachAndStopAllAppenders(); } super.stop(); } @Override public boolean isStarted() { return super.isStarted(); } @Override protected void append(E eventObject) { Appender appender = aai.getAppender(appenderKey); if (appender == null) { addError("No appender found for appenderKey " + appenderKey); } else { appender.doAppend(eventObject); } } public String getAppenderKey() { return appenderKey; } public void setAppenderKey(String appenderKey) { this.appenderKey = appenderKey; } @Override public void addAppender(Appender newAppender) { aai.addAppender(newAppender); } @Override public Iterator> iteratorForAppenders() { return aai.iteratorForAppenders(); } @Override public Appender getAppender(String name) { return aai.getAppender(name); } @Override public boolean isAttached(Appender appender) { return aai.isAttached(appender); } @Override public void detachAndStopAllAppenders() { aai.detachAndStopAllAppenders(); } @Override public boolean detachAppender(Appender appender) { return aai.detachAppender(appender); } @Override public boolean detachAppender(String name) { return aai.detachAppender(name); } } ================================================ FILE: logback-core/src/main/java/com/tersesystems/logback/core/encoder/LayoutWrappingEncoder.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ /** * Logback: the reliable, generic, fast and flexible logging framework. Copyright (C) 1999-2015, * QOS.ch. All rights reserved. * *

This program and the accompanying materials are dual-licensed under either the terms of the * Eclipse Public License v1.0 as published by the Eclipse Foundation * *

or (per the licensee's choosing) * *

under the terms of the GNU Lesser General Public License version 2.1 as published by the Free * Software Foundation. */ package com.tersesystems.logback.core.encoder; import ch.qos.logback.core.CoreConstants; import ch.qos.logback.core.Layout; import ch.qos.logback.core.OutputStreamAppender; import ch.qos.logback.core.encoder.EncoderBase; import java.nio.charset.Charset; /** * A LayoutWrappingEncoder that doesn't require the parent be an appender. * * @param */ public class LayoutWrappingEncoder extends EncoderBase { protected Layout layout; /** * The charset to use when converting a String into bytes. *

* By default this property has the value * null which corresponds to * the system's default charset. */ private Charset charset; Object parent; Boolean immediateFlush = null; public Layout getLayout() { return layout; } public void setLayout(Layout layout) { this.layout = layout; } public Charset getCharset() { return charset; } /** * Set the charset to use when converting the string returned by the layout into bytes. * *

By default this property has the value null which corresponds to the system's * default charset. * * @param charset the charset */ public void setCharset(Charset charset) { this.charset = charset; } /** * Sets the immediateFlush option. The default value for immediateFlush is 'true'. If set to true, * the doEncode() method will immediately flush the underlying OutputStream. Although immediate * flushing is safer, it also significantly degrades logging throughput. * * @since 1.0.3 */ public void setImmediateFlush(boolean immediateFlush) { addWarn( "As of version 1.2.0 \"immediateFlush\" property should be set within the enclosing Appender."); addWarn("Please move \"immediateFlush\" property into the enclosing appender."); this.immediateFlush = immediateFlush; } @Override public byte[] headerBytes() { if (layout == null) return null; StringBuilder sb = new StringBuilder(); appendIfNotNull(sb, layout.getFileHeader()); appendIfNotNull(sb, layout.getPresentationHeader()); if (sb.length() > 0) { // If at least one of file header or presentation header were not // null, then append a line separator. // This should be useful in most cases and should not hurt. sb.append(CoreConstants.LINE_SEPARATOR); } return convertToBytes(sb.toString()); } @Override public byte[] footerBytes() { if (layout == null) return null; StringBuilder sb = new StringBuilder(); appendIfNotNull(sb, layout.getPresentationFooter()); appendIfNotNull(sb, layout.getFileFooter()); return convertToBytes(sb.toString()); } private byte[] convertToBytes(String s) { if (charset == null) { return s.getBytes(); } else { return s.getBytes(charset); } } public byte[] encode(E event) { String txt = layout.doLayout(event); return convertToBytes(txt); } public boolean isStarted() { return false; } public void start() { if (immediateFlush != null) { if (parent instanceof OutputStreamAppender) { addWarn( "Setting the \"immediateFlush\" property of the enclosing appender to " + immediateFlush); @SuppressWarnings("unchecked") OutputStreamAppender parentOutputStreamAppender = (OutputStreamAppender) parent; parentOutputStreamAppender.setImmediateFlush(immediateFlush); } else { addError("Could not set the \"immediateFlush\" property of the enclosing appender."); } } started = true; } public void stop() { started = false; } private void appendIfNotNull(StringBuilder sb, String s) { if (s != null) { sb.append(s); } } // This needs to be changed from the out of the box appender in logback because we don't // want to only be able to use a PatternLayoutEncoder in an appender. public void setParent(Object parent) { this.parent = parent; } } ================================================ FILE: logback-core/src/main/java/com/tersesystems/logback/core/pattern/PatternLayoutEncoderBase.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ /** * Logback: the reliable, generic, fast and flexible logging framework. Copyright (C) 1999-2015, * QOS.ch. All rights reserved. * *

This program and the accompanying materials are dual-licensed under either the terms of the * Eclipse Public License v1.0 as published by the Eclipse Foundation * *

or (per the licensee's choosing) * *

under the terms of the GNU Lesser General Public License version 2.1 as published by the Free * Software Foundation. */ package com.tersesystems.logback.core.pattern; import ch.qos.logback.core.Layout; import com.tersesystems.logback.core.encoder.LayoutWrappingEncoder; /** * A PatternLayoutEncoderBase that doesn't require the parent is an appender. * * @param */ public class PatternLayoutEncoderBase extends LayoutWrappingEncoder { String pattern; // due to popular demand outputPatternAsHeader is set to false by default protected boolean outputPatternAsHeader = false; public String getPattern() { return pattern; } public void setPattern(String pattern) { this.pattern = pattern; } public boolean isOutputPatternAsHeader() { return outputPatternAsHeader; } /** * Print the pattern string as a header in log files * * @param outputPatternAsHeader * @since 1.0.3 */ public void setOutputPatternAsHeader(boolean outputPatternAsHeader) { this.outputPatternAsHeader = outputPatternAsHeader; } public boolean isOutputPatternAsPresentationHeader() { return outputPatternAsHeader; } /** * @deprecated replaced by {@link #setOutputPatternAsHeader(boolean)} */ public void setOutputPatternAsPresentationHeader(boolean outputPatternAsHeader) { addWarn( "[outputPatternAsPresentationHeader] property is deprecated. Please use [outputPatternAsHeader] option instead."); this.outputPatternAsHeader = outputPatternAsHeader; } @Override public void setLayout(Layout layout) { throw new UnsupportedOperationException( "one cannot set the layout of " + this.getClass().getName()); } } ================================================ FILE: logback-core/src/test/java/com/tersesystems/logback/core/CompositeAppenderTest.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.core; import static org.assertj.core.api.Assertions.assertThat; import ch.qos.logback.classic.Logger; import ch.qos.logback.classic.LoggerContext; import ch.qos.logback.classic.joran.JoranConfigurator; import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.core.joran.spi.JoranException; import ch.qos.logback.core.read.ListAppender; import java.net.URL; import org.junit.Test; public class CompositeAppenderTest { @Test public void testSimpleAppender() throws JoranException { LoggerContext context = new LoggerContext(); URL resource = getClass().getResource("/logback-with-composite-appender.xml"); JoranConfigurator configurator = new JoranConfigurator(); configurator.setContext(context); configurator.doConfigure(resource); ch.qos.logback.classic.Logger logger = context.getLogger(Logger.ROOT_LOGGER_NAME); CompositeAppender composite = (CompositeAppender) logger.getAppender("CONSOLE_AND_FILE"); ListAppender file = (ListAppender) composite.getAppender("FILE"); ListAppender console = (ListAppender) composite.getAppender("CONSOLE"); logger.info("hello world"); assertThat(file.list.get(0).getMessage()).isEqualTo("hello world"); assertThat(console.list.get(0).getMessage()).isEqualTo("hello world"); } } ================================================ FILE: logback-core/src/test/java/com/tersesystems/logback/core/SelectAppenderTest.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.core; import static org.assertj.core.api.Assertions.assertThat; import ch.qos.logback.classic.Logger; import ch.qos.logback.classic.LoggerContext; import ch.qos.logback.classic.joran.JoranConfigurator; import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.core.joran.spi.JoranException; import ch.qos.logback.core.read.ListAppender; import java.net.URL; import org.junit.Test; public class SelectAppenderTest { @Test public void testWithTestEnvironment() throws JoranException { LoggerContext context = new LoggerContext(); URL resource = getClass().getResource("/logback-with-select-appender.xml"); JoranConfigurator configurator = new JoranConfigurator(); context.putProperty("APPENDER_KEY", "test"); configurator.setContext(context); configurator.doConfigure(resource); ch.qos.logback.classic.Logger logger = context.getLogger(Logger.ROOT_LOGGER_NAME); SelectAppender selectAppender = (SelectAppender) logger.getAppender("SELECT"); CompositeAppender development = (CompositeAppender) selectAppender.getAppender("test"); ListAppender listAppender = (ListAppender) development.getAppender("test-list"); logger.info("hello world"); assertThat(listAppender.list.get(0).getMessage()).isEqualTo("hello world"); } @Test public void testWithDevelopmentEnvironment() throws JoranException { LoggerContext context = new LoggerContext(); URL resource = getClass().getResource("/logback-with-select-appender.xml"); JoranConfigurator configurator = new JoranConfigurator(); context.putProperty("APPENDER_KEY", "development"); configurator.setContext(context); configurator.doConfigure(resource); ch.qos.logback.classic.Logger logger = context.getLogger(Logger.ROOT_LOGGER_NAME); SelectAppender selectAppender = (SelectAppender) logger.getAppender("SELECT"); CompositeAppender development = (CompositeAppender) selectAppender.getAppender("test"); ListAppender listAppender = (ListAppender) development.getAppender("test-list"); logger.info("hello world"); assertThat(listAppender.list.size()).isEqualTo(0); } } ================================================ FILE: logback-core/src/test/java/com/tersesystems/logback/core/TestAppender.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.core; import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.core.AppenderBase; import ch.qos.logback.core.encoder.Encoder; import java.util.ArrayList; import java.util.List; public class TestAppender extends AppenderBase { protected Encoder encoder; public static List events = new ArrayList<>(); public Encoder getEncoder() { return encoder; } public void setEncoder(Encoder encoder) { this.encoder = encoder; } @Override protected void append(ILoggingEvent e) { events.add(e); } } ================================================ FILE: logback-core/src/test/resources/logback-with-composite-appender.xml ================================================ ================================================ FILE: logback-core/src/test/resources/logback-with-select-appender.xml ================================================ ${APPENDER_KEY} test test-list development development-console %-5relative %-5level %logger{35} - %msg%n staging staging-console %-5relative %-5level %logger{35} - %msg%n ================================================ FILE: logback-correlationid/gradle.properties ================================================ # # SPDX-License-Identifier: CC0-1.0 # # Copyright 2018-2020 Will Sargent. # # Licensed under the CC0 Public Domain Dedication; # You may obtain a copy of the License at # # http://creativecommons.org/publicdomain/zero/1.0/ # project_description = Logback Correlation ID Utilities ================================================ FILE: logback-correlationid/logback-correlationid.gradle ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ dependencies { implementation project(":logback-classic") testImplementation 'org.awaitility:awaitility:4.0.2' testImplementation "net.logstash.logback:logstash-logback-encoder:$logstashVersion" } ================================================ FILE: logback-correlationid/src/main/java/com/tersesystems/logback/correlationid/CorrelationIdDecider.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.correlationid; import ch.qos.logback.classic.Level; import ch.qos.logback.classic.Logger; import ch.qos.logback.core.spi.FilterReply; import com.tersesystems.logback.classic.TurboFilterDecider; import java.util.Optional; import org.slf4j.Marker; public class CorrelationIdDecider implements TurboFilterDecider { protected final CorrelationIdUtils utils; public CorrelationIdDecider(CorrelationIdUtils utils) { this.utils = utils; } @Override public FilterReply decide( Marker marker, Logger logger, Level level, String format, Object[] params, Throwable t) { Optional maybeCorrelationId = utils.get(utils.getMDCPropertyMap(), marker); return maybeCorrelationId.isPresent() ? FilterReply.ACCEPT : FilterReply.NEUTRAL; } } ================================================ FILE: logback-correlationid/src/main/java/com/tersesystems/logback/correlationid/CorrelationIdFilter.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.correlationid; import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.core.filter.Filter; import ch.qos.logback.core.spi.FilterReply; import java.util.Optional; public class CorrelationIdFilter extends Filter { private String mdcKey = "correlation_id"; public String getMdcKey() { return mdcKey; } public void setMdcKey(String mdcKey) { this.mdcKey = mdcKey; } protected CorrelationIdUtils utils; @Override public void start() { super.start(); utils = new CorrelationIdUtils(mdcKey); } @Override public FilterReply decide(ILoggingEvent event) { Optional maybeCorrelationId = utils.get(event.getMDCPropertyMap(), event.getMarker()); return maybeCorrelationId.isPresent() ? FilterReply.ACCEPT : FilterReply.DENY; } } ================================================ FILE: logback-correlationid/src/main/java/com/tersesystems/logback/correlationid/CorrelationIdMarker.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.correlationid; import com.tersesystems.logback.classic.TerseBasicMarker; import org.slf4j.Marker; /** A very simple correlation id marker. */ public interface CorrelationIdMarker extends Marker, CorrelationIdProvider { static CorrelationIdMarker create(String value) { return new CorrelationIdBasicMarker(value); } } /** Implementation of correlation id. */ class CorrelationIdBasicMarker extends TerseBasicMarker implements CorrelationIdMarker { private final String value; public CorrelationIdBasicMarker(String value) { super("TS_CORRELATION_ID"); this.value = value; } public String getCorrelationId() { return value; } } ================================================ FILE: logback-correlationid/src/main/java/com/tersesystems/logback/correlationid/CorrelationIdProvider.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.correlationid; import com.tersesystems.logback.core.Component; /** A correlation id component. */ public interface CorrelationIdProvider extends Component { String getCorrelationId(); } ================================================ FILE: logback-correlationid/src/main/java/com/tersesystems/logback/correlationid/CorrelationIdTapFilter.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.correlationid; import ch.qos.logback.classic.Level; import ch.qos.logback.classic.Logger; import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.core.spi.FilterReply; import com.tersesystems.logback.classic.TapFilter; import java.util.Map; import org.slf4j.Marker; /** Tells the tap filter to create an event and append it if a correlation id is found. */ public class CorrelationIdTapFilter extends TapFilter { private String mdcKey = "correlation_id"; private CorrelationIdUtils utils; public String getMdcKey() { return mdcKey; } public void setMdcKey(String mdcKey) { this.mdcKey = mdcKey; } @Override public void start() { super.start(); utils = new CorrelationIdUtils(mdcKey); } @Override public FilterReply decide( Marker marker, Logger logger, Level level, String format, Object[] params, Throwable t) { if (logger != null && format == null && params == null) { // Need to turn on events for everything so that we can cover conditional events. return FilterReply.ACCEPT; } Map mdcPropertyMap = utils.getMDCPropertyMap(); if (utils.get(mdcPropertyMap, marker).isPresent()) { ILoggingEvent loggingEvent = getLoggingEventFactory().create(marker, logger, level, format, params, t); // initialize the mdc in the logging event... loggingEvent.prepareForDeferredProcessing(); // For every message that is acceptable, store it in the appender and return. appenderAttachableImpl().appendLoopOnAppenders(loggingEvent); } return FilterReply.NEUTRAL; } } ================================================ FILE: logback-correlationid/src/main/java/com/tersesystems/logback/correlationid/CorrelationIdUtils.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.correlationid; import ch.qos.logback.classic.util.LogbackMDCAdapter; import java.util.Collections; import java.util.Iterator; import java.util.Map; import java.util.Optional; import org.slf4j.MDC; import org.slf4j.Marker; import org.slf4j.spi.MDCAdapter; public class CorrelationIdUtils { private final String mdcKey; public CorrelationIdUtils(String mdcKey) { this.mdcKey = mdcKey; } public Optional get(Map mdcPropertyMap, Marker marker) { Optional first = fromMarker(marker); if (first.isPresent()) { return first; } else { return get(mdcPropertyMap); } } /** Pulls the correlation id from a marker which is a CorrelationIdProvider. */ public Optional fromMarker(Marker m) { if (m instanceof CorrelationIdProvider) { CorrelationIdProvider provider = ((CorrelationIdProvider) m); return Optional.of(provider.getCorrelationId()); } if (m != null && m.hasReferences()) { for (Iterator iter = m.iterator(); iter.hasNext(); ) { Marker child = iter.next(); if (child instanceof CorrelationIdProvider) { CorrelationIdProvider provider = ((CorrelationIdProvider) child); return Optional.of(provider.getCorrelationId()); } if (child.hasReferences()) { return fromMarker(child); } } } return Optional.empty(); } public Optional get(Map mdcPropertyMap) { // Look in MDC for a correlation id as well... if (mdcKey != null) { String s = mdcPropertyMap.get(mdcKey); if (s != null) { return Optional.of(s); } } return Optional.empty(); } public Map getMDCPropertyMap() { MDCAdapter mdc = MDC.getMDCAdapter(); Map mdcPropertyMap; if (mdc instanceof LogbackMDCAdapter) mdcPropertyMap = ((LogbackMDCAdapter) mdc).getPropertyMap(); else mdcPropertyMap = mdc.getCopyOfContextMap(); // mdcPropertyMap still null, use emptyMap() if (mdcPropertyMap == null) mdcPropertyMap = Collections.emptyMap(); return mdcPropertyMap; } } ================================================ FILE: logback-correlationid/src/test/java/com.tersesystems.logback.correlationid/CorrelationIdFilterTest.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.correlationid; import static java.util.Objects.requireNonNull; import static org.assertj.core.api.Assertions.assertThat; import ch.qos.logback.classic.Logger; import ch.qos.logback.classic.LoggerContext; import ch.qos.logback.classic.joran.JoranConfigurator; import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.core.Appender; import ch.qos.logback.core.joran.spi.JoranException; import ch.qos.logback.core.read.ListAppender; import java.net.URL; import java.util.Iterator; import java.util.Optional; import org.junit.After; import org.junit.Before; import org.junit.jupiter.api.Test; import org.slf4j.MDC; public class CorrelationIdFilterTest { @Before @After public void clearMDC() { MDC.clear(); } @Test public void testFilter() throws JoranException { MDC.clear(); LoggerContext loggerFactory = createLoggerFactory("/logback-correlationid.xml"); // Write something that never gets logged explicitly... Logger logger = loggerFactory.getLogger("com.example.Debug"); String correlationId = "12345"; CorrelationIdMarker correlationIdMarker = CorrelationIdMarker.create(correlationId); // should be logged because marker logger.info(correlationIdMarker, "info one"); logger.info("info two"); // should not be logged // Everything below this point should be logged. MDC.put("correlationId", correlationId); logger.info("info three"); logger.info(correlationIdMarker, "info four"); ListAppender listAppender = getListAppender(loggerFactory); assertThat(listAppender.list.size()).isEqualTo(3); } LoggerContext createLoggerFactory(String resourceName) throws JoranException { LoggerContext context = new LoggerContext(); URL resource = getClass().getResource(resourceName); JoranConfigurator configurator = new JoranConfigurator(); configurator.setContext(context); configurator.doConfigure(resource); return context; } ListAppender getListAppender(LoggerContext context) { Optional> maybeAppender = getFirstAppender(context.getLogger(Logger.ROOT_LOGGER_NAME)); if (maybeAppender.isPresent()) { return (ListAppender) requireNonNull(maybeAppender.get()); } else { throw new IllegalStateException("Cannot find appender"); } } private Optional> getFirstAppender(Logger logger) { for (Iterator> iter = logger.iteratorForAppenders(); iter.hasNext(); ) { Appender next = logger.iteratorForAppenders().next(); return Optional.of(next); } return Optional.empty(); } } ================================================ FILE: logback-correlationid/src/test/java/com.tersesystems.logback.correlationid/CorrelationIdTapFilterTest.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.correlationid; import static java.util.Objects.requireNonNull; import static org.assertj.core.api.Assertions.assertThat; import ch.qos.logback.classic.Logger; import ch.qos.logback.classic.LoggerContext; import ch.qos.logback.classic.joran.JoranConfigurator; import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.classic.spi.TurboFilterList; import ch.qos.logback.core.Appender; import ch.qos.logback.core.joran.spi.JoranException; import ch.qos.logback.core.read.ListAppender; import ch.qos.logback.core.spi.AppenderAttachable; import java.net.URL; import java.util.Optional; import org.junit.After; import org.junit.Before; import org.junit.jupiter.api.Test; import org.slf4j.MDC; public class CorrelationIdTapFilterTest { @Before @After public void clearMDC() { MDC.clear(); } @Test public void testCorrelationWithNoMarker() throws JoranException { MDC.clear(); LoggerContext loggerFactory = createLoggerFactory("/logback-correlationid-tapfilter.xml"); // Because there's no correlation id, it should never make it past the filter here. Logger debugLogger = loggerFactory.getLogger("com.example.Debug"); debugLogger.debug("debug one"); debugLogger.debug("debug two"); debugLogger.debug("debug three"); debugLogger.debug("debug four"); Logger logger = loggerFactory.getLogger("com.example.Test"); logger.error("Write out error message to console"); ListAppender listAppender = getListAppender(loggerFactory); assertThat(listAppender.list.size()).isEqualTo(0); } @Test public void testCorrelationWithMarker() throws JoranException { MDC.clear(); LoggerContext loggerFactory = createLoggerFactory("/logback-correlationid-tapfilter.xml"); CorrelationIdMarker correlationIdMarker = CorrelationIdMarker.create("12345"); // Because there's no correlation id, it should never make it past the filter here. Logger debugLogger = loggerFactory.getLogger("com.example.Debug"); debugLogger.debug(correlationIdMarker, "debug one"); debugLogger.debug(correlationIdMarker, "debug two"); debugLogger.debug("debug three"); debugLogger.debug("debug four"); Logger logger = loggerFactory.getLogger("com.example.Test"); logger.error("Write out error message to console"); ListAppender listAppender = getListAppender(loggerFactory); assertThat(listAppender.list.size()).isEqualTo(2); } @Test public void testCorrelationWithMDC() throws JoranException { MDC.clear(); LoggerContext loggerFactory = createLoggerFactory("/logback-correlationid-tapfilter.xml"); MDC.put("correlationId", "12345"); CorrelationIdMarker correlationIdMarker = CorrelationIdMarker.create("12345"); // Because there's no correlation id, it should never make it past the filter here. Logger debugLogger = loggerFactory.getLogger("com.example.Debug"); debugLogger.debug(correlationIdMarker, "debug one"); debugLogger.debug(correlationIdMarker, "debug two"); debugLogger.debug("debug three"); debugLogger.debug("debug four"); Logger logger = loggerFactory.getLogger("com.example.Test"); logger.error("Write out error message to console"); ListAppender listAppender = getListAppender(loggerFactory); assertThat(listAppender.list.size()).isEqualTo(5); } LoggerContext createLoggerFactory(String resourceName) throws JoranException { LoggerContext context = new LoggerContext(); URL resource = getClass().getResource(resourceName); JoranConfigurator configurator = new JoranConfigurator(); configurator.setContext(context); configurator.doConfigure(resource); return context; } Optional> getFilterAppender(TurboFilterList turboFilterList) { return turboFilterList.stream() .filter(f -> f instanceof AppenderAttachable) .map(f -> ((AppenderAttachable) f).iteratorForAppenders().next()) .findFirst(); } ListAppender getListAppender(LoggerContext context) { Optional> maybeAppender = getFilterAppender(context.getTurboFilterList()); if (maybeAppender.isPresent()) { return (ListAppender) requireNonNull(maybeAppender.get()); } else { throw new IllegalStateException("Cannot find appender"); } } } ================================================ FILE: logback-correlationid/src/test/resources/logback-correlationid-jdbc.xml ================================================ correlationId org.h2.Driver jdbc:h2:mem:terse-logback;DB_CLOSE_DELAY=-1 sa CREATE TABLE IF NOT EXISTS events ( ID NUMERIC NOT NULL PRIMARY KEY AUTO_INCREMENT, ts TIMESTAMP(9) WITH TIME ZONE NOT NULL, relative_ns BIGINT NULL, start_ms BIGINT NULL, level_value int NOT NULL, level VARCHAR(7) NOT NULL, evt JSON NOT NULL, correlation_id VARCHAR(255) NOT NULL, event_id VARCHAR(255) NULL ); CREATE INDEX IF NOT EXISTS event_id_idx ON events(event_id); CREATE INDEX IF NOT EXISTS correlation_id_idx ON events(correlation_id); insert into events(ts, relative_ns, start_ms, level_value, level, evt, correlation_id, event_id) values(?, ?, ?, ?, ?, ?, ?, ?) %-5relative %-5level %logger{35} - %msg%n ================================================ FILE: logback-correlationid/src/test/resources/logback-correlationid-tapfilter.xml ================================================ correlationId %-5relative %-5level %logger{35} - %msg%n ================================================ FILE: logback-correlationid/src/test/resources/logback-correlationid.xml ================================================ correlationId ================================================ FILE: logback-correlationid/src/test/resources/spy.properties ================================================ # # SPDX-License-Identifier: CC0-1.0 # # Copyright 2018-2020 Will Sargent. # # Licensed under the CC0 Public Domain Dedication; # You may obtain a copy of the License at # # http://creativecommons.org/publicdomain/zero/1.0/ # driverlist=org.h2.Driver ================================================ FILE: logback-exception-mapping/gradle.properties ================================================ # # SPDX-License-Identifier: CC0-1.0 # # Copyright 2018-2020 Will Sargent. # # Licensed under the CC0 Public Domain Dedication; # You may obtain a copy of the License at # # http://creativecommons.org/publicdomain/zero/1.0/ # project_description = Logback Exception Mapping ================================================ FILE: logback-exception-mapping/logback-exception-mapping.gradle ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ plugins { id 'java-library' } dependencies { api project(':logback-classic') } ================================================ FILE: logback-exception-mapping/src/main/java/com/tersesystems/logback/exceptionmapping/BeanExceptionMapping.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.exceptionmapping; import ch.qos.logback.core.joran.util.beans.BeanUtil; import java.lang.reflect.InvocationTargetException; import java.util.*; import java.util.function.Consumer; import java.util.stream.Collectors; import java.util.stream.Stream; public class BeanExceptionMapping implements ExceptionMapping { private final List methodNames; private final Consumer reporter; private final String name; public BeanExceptionMapping( String name, List propertyNames, Consumer reporter) { this.name = name; this.methodNames = propertyNames; this.reporter = reporter; } @Override public String getName() { return name; } @Override public List apply(Throwable e) { return methodNames.stream() .flatMap(methodName -> findMethod(e, methodName)) .collect(Collectors.toList()); } protected Stream findMethod(Throwable e, String methodName) { return Arrays.stream(e.getClass().getMethods()) .filter( method -> methodName.equals(BeanUtil.getPropertyName(method)) && BeanUtil.isGetter(method)) .map( method -> { try { Object invokeResult = method.invoke(e, (Object[]) null); return Optional.of(ExceptionProperty.create(methodName, invokeResult)); } catch (IllegalAccessException | InvocationTargetException ex) { reporter.accept(ex); } return Optional.empty(); }) .filter(Optional::isPresent) .map(Optional::get); } } ================================================ FILE: logback-exception-mapping/src/main/java/com/tersesystems/logback/exceptionmapping/Constants.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.exceptionmapping; public final class Constants { public static final String REGISTRY_BAG = "EXCEPTION_REGISTRY_BAG"; public static final String DEFAULT_MAPPINGS_KEY = "default"; } ================================================ FILE: logback-exception-mapping/src/main/java/com/tersesystems/logback/exceptionmapping/DefaultExceptionMappingRegistry.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.exceptionmapping; import static java.util.Arrays.asList; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Consumer; import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; public class DefaultExceptionMappingRegistry implements ExceptionMappingRegistry { private Consumer exceptionHandler; private Map classNameToMappings; public DefaultExceptionMappingRegistry(Consumer exceptionHandler) { this.classNameToMappings = new ConcurrentHashMap<>(); this.exceptionHandler = exceptionHandler; } // --------------------- // register maps @Override public void register(ClassLoader classLoader, Map> mappers) { mappers.forEach( (className, methodNames) -> register(classLoader, className, methodNames.toArray(new String[0]))); } @Override public void register(Map> mappers) { ClassLoader classLoader = ClassLoader.getSystemClassLoader(); mappers.forEach( (className, methodNames) -> register(classLoader, className, methodNames.toArray(new String[0]))); } // --------------------- // register methodNames @Override public void register(String className, String... methodNames) { register(ClassLoader.getSystemClassLoader(), className, methodNames); } @SuppressWarnings("unchecked") @Override public void register(ClassLoader classLoader, String className, String... methodNames) { try { Class clazz = (Class) classLoader.loadClass(className); register(clazz, methodNames); } catch (Exception e) { exceptionHandler.accept(e); } } @Override public void register(Class exceptionClass, String... propertyNames) { register( new BeanExceptionMapping( exceptionClass.getName(), asList(propertyNames), exceptionHandler)); } @SuppressWarnings("unchecked") @Override public void register( Class exceptionClass, Function> f) { register( new FunctionExceptionMapping( exceptionClass.getName(), (Function>) f)); } @Override public void register(String className, Function> f) { register(new FunctionExceptionMapping(className, f)); } // --------------------- // register ExceptionMapping @Override public void register(ExceptionMapping mapping) { classNameToMappings.put(mapping.getName(), mapping); } // --------------------- // apply @Override public List apply(Throwable e) { Stream> classStream = new ExceptionHierarchyIterator(e.getClass()).stream(); List exceptionMappings = classStream .map(Class::getName) .filter(className -> classNameToMappings.containsKey(className)) .map(className -> classNameToMappings.get(className)) .collect(Collectors.toList()); List exceptionProperties = new ArrayList<>(); for (ExceptionMapping exceptionMapping : exceptionMappings) { List propertyList = exceptionMapping.apply(e); exceptionProperties.addAll(propertyList); } return exceptionProperties; } @Override public Iterator iterator() { return classNameToMappings.values().iterator(); } @Override public ExceptionMapping get(String name) { return classNameToMappings.get(name); } @Override public boolean contains(ExceptionMapping exceptionMapping) { return contains(exceptionMapping.getName()); } @Override public boolean contains(String name) { return classNameToMappings.containsKey(name); } @Override public boolean remove(ExceptionMapping exceptionMapping) { return classNameToMappings.remove(exceptionMapping.getName()) != null; } @Override public boolean remove(String name) { return classNameToMappings.remove(name) != null; } } ================================================ FILE: logback-exception-mapping/src/main/java/com/tersesystems/logback/exceptionmapping/ExceptionCauseIterator.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.exceptionmapping; import java.util.Iterator; import java.util.Spliterator; import java.util.Spliterators; import java.util.stream.Stream; import java.util.stream.StreamSupport; public class ExceptionCauseIterator implements Iterator { private Throwable throwable; ExceptionCauseIterator(Throwable throwable) { this.throwable = throwable; } @Override public boolean hasNext() { return throwable != null; } @SuppressWarnings("unchecked") @Override public Throwable next() { Throwable oldThrowable = throwable; if (throwable != null) { throwable = throwable.getCause(); } return oldThrowable; } @SuppressWarnings("unchecked") public Stream stream() { Spliterator spliterator = Spliterators.spliteratorUnknownSize(this, 0); return (Stream) StreamSupport.stream(spliterator, false); } public static ExceptionCauseIterator create(Throwable throwable) { return new ExceptionCauseIterator(throwable); } } ================================================ FILE: logback-exception-mapping/src/main/java/com/tersesystems/logback/exceptionmapping/ExceptionHierarchyIterator.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.exceptionmapping; import java.util.Iterator; import java.util.Spliterator; import java.util.Spliterators; import java.util.stream.Stream; import java.util.stream.StreamSupport; public class ExceptionHierarchyIterator implements Iterator> { private Class clazz; ExceptionHierarchyIterator(Class clazz) { this.clazz = clazz; } @Override public boolean hasNext() { return clazz != null; } @SuppressWarnings("unchecked") @Override public Class next() { Class oldClass = clazz; if (clazz != null) { clazz = clazz.getSuperclass(); } return oldClass; } @SuppressWarnings("unchecked") public Stream> stream() { Spliterator spliterator = Spliterators.spliteratorUnknownSize(this, 0); return (Stream>) StreamSupport.stream(spliterator, false); } public static ExceptionHierarchyIterator create(Class clazz) { return new ExceptionHierarchyIterator(clazz); } } ================================================ FILE: logback-exception-mapping/src/main/java/com/tersesystems/logback/exceptionmapping/ExceptionMapping.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.exceptionmapping; import java.util.List; import java.util.function.Function; public interface ExceptionMapping extends Function> { String getName(); } ================================================ FILE: logback-exception-mapping/src/main/java/com/tersesystems/logback/exceptionmapping/ExceptionMappingAction.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.exceptionmapping; import ch.qos.logback.core.joran.action.Action; import ch.qos.logback.core.joran.spi.InterpretationContext; import ch.qos.logback.core.util.OptionHelper; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.function.Consumer; import org.xml.sax.Attributes; public class ExceptionMappingAction extends Action { Consumer handler = e -> addError("Cannot map exception", e); boolean inError = false; @SuppressWarnings("unchecked") public void begin(InterpretationContext ec, String tagName, Attributes attributes) { // Let us forget about previous errors (in this object) inError = false; // logger.debug("begin called"); Object o = ec.peekObject(); if (!(o instanceof ExceptionMappingRegistry)) { String errMsg = "Could not find a registry at the top of execution stack. Near [" + tagName + "] line " + getLineNumber(ec); inError = true; addInfo(errMsg); // This can trigger in an "if" block from janino, so it may not be serious... return; } ExceptionMappingRegistry registry = (ExceptionMappingRegistry) o; String mappingName = ec.subst(attributes.getValue("name")); String properties = ec.subst(attributes.getValue("properties")); if (OptionHelper.isEmpty(mappingName)) { // print a meaningful error message and return String errMsg = "Missing name attribute in tag."; inError = true; addError(errMsg); return; } if (OptionHelper.isEmpty(properties)) { // print a meaningful error message and return String errMsg = "Missing properties attribute in tag."; inError = true; addError(errMsg); return; } List mappingPropertyNames = new ArrayList(Arrays.asList(properties.split(","))); ExceptionMapping newMapping = new BeanExceptionMapping(mappingName, mappingPropertyNames, handler); addInfo( "Attaching mapping named [" + mappingName + "] to " + registry + "at " + getLineNumber(ec)); registry.register(newMapping); } public void end(InterpretationContext ec, String n) {} } ================================================ FILE: logback-exception-mapping/src/main/java/com/tersesystems/logback/exceptionmapping/ExceptionMappingRegistry.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.exceptionmapping; import java.util.*; import java.util.function.Function; public interface ExceptionMappingRegistry { void register(Map> mappers); void register(ClassLoader classLoader, Map> mappers); void register(String className, String... methodNames); void register(ClassLoader classLoader, String className, String... methodNames); void register(Class exceptionClass, String... propertyNames); void register( Class exceptionClass, Function> f); void register(String className, Function> f); void register(ExceptionMapping mapper); List apply(Throwable e); Iterator iterator(); ExceptionMapping get(String name); boolean contains(ExceptionMapping exceptionMapping); boolean contains(String name); boolean remove(ExceptionMapping exceptionMapping); boolean remove(String name); } ================================================ FILE: logback-exception-mapping/src/main/java/com/tersesystems/logback/exceptionmapping/ExceptionMappingRegistryAction.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.exceptionmapping; import static com.tersesystems.logback.exceptionmapping.Constants.*; import static java.lang.String.*; import static java.util.Arrays.asList; import static java.util.Collections.singletonList; import ch.qos.logback.core.Context; import ch.qos.logback.core.joran.action.Action; import ch.qos.logback.core.joran.spi.ActionException; import ch.qos.logback.core.joran.spi.InterpretationContext; import ch.qos.logback.core.util.OptionHelper; import java.io.InterruptedIOException; import java.util.*; import java.util.function.Consumer; import javax.xml.stream.Location; import javax.xml.stream.XMLStreamException; import org.w3c.dom.DOMException; import org.w3c.dom.events.EventException; import org.xml.sax.Attributes; public class ExceptionMappingRegistryAction extends Action { ExceptionMappingRegistry mappingsRegistry; private boolean inError = false; private final Consumer handler = e -> addError("Cannot map exception!", e); @SuppressWarnings("unchecked") public void begin(InterpretationContext ic, String localName, Attributes attributes) throws ActionException { mappingsRegistry = null; inError = false; Context context = getContext(); HashMap mappingsBag = (HashMap) context.getObject(REGISTRY_BAG); if (mappingsBag == null) { mappingsBag = new HashMap<>(); context.putObject(REGISTRY_BAG, mappingsBag); } try { mappingsRegistry = new DefaultExceptionMappingRegistry(handler); initializeRegistry(mappingsRegistry); String mappingsName = ic.subst(attributes.getValue(NAME_ATTRIBUTE)); if (OptionHelper.isEmpty(mappingsName)) { addInfo( format( "No mappingsRegistry name given for mappingsRegistry, using default \"%s\"", DEFAULT_MAPPINGS_KEY)); mappingsName = DEFAULT_MAPPINGS_KEY; } else { addInfo("Naming mappingsRegistry as [" + mappingsName + "]"); } mappingsBag.put(mappingsName, mappingsRegistry); ic.pushObject(mappingsRegistry); } catch (Exception oops) { inError = true; addError("Could not create registry.", oops); throw new ActionException(oops); } } protected void initializeRegistry(ExceptionMappingRegistry registry) { List exceptionMappings = initialMappings(); for (ExceptionMapping exceptionMapping : exceptionMappings) { registry.register(exceptionMapping); } complexMappings(registry); } protected List initialMappings() { return Arrays.asList( new BeanExceptionMapping("java.lang.Throwable", asList("message"), handler), new BeanExceptionMapping( "java.nio.file.FileSystemException", asList("file", "otherFile", "reason"), handler), new BeanExceptionMapping( "java.net.HttpRetryException", asList("responseCode", "reason", "location"), handler), new BeanExceptionMapping( "java.net.URISyntaxException", asList("input", "reason", "index"), handler), new BeanExceptionMapping( "java.nio.charset.IllegalCharsetNameException", asList("charsetName"), handler), new BeanExceptionMapping("java.sql.BatchUpdateException", asList("updateCounts"), handler), new BeanExceptionMapping("java.sql.SQLException", asList("errorCode", "SQLState"), handler), new BeanExceptionMapping("java.text.ParseException", asList("errorOffset"), handler), new BeanExceptionMapping( "java.time.format.DateTimeParseException", asList("parsedString", "errorIndex"), handler), new BeanExceptionMapping( "java.util.DuplicateFormatFlagsException", asList("flags"), handler), new BeanExceptionMapping( "java.util.FormatFlagsConversionMismatchException", asList("flags", "conversion"), handler), new BeanExceptionMapping( "java.util.IllegalFormatCodePointException", asList("codePoint"), handler), new BeanExceptionMapping( "java.util.IllegalFormatConversionException", asList("conversion", "argumentClass"), handler), new BeanExceptionMapping("java.util.IllegalFormatFlagsException", asList("flags"), handler), new BeanExceptionMapping( "java.util.IllegalFormatPrecisionException", asList("precision"), handler), new BeanExceptionMapping("java.util.IllegalFormatWidthException", asList("width"), handler), new BeanExceptionMapping( "java.util.IllformedLocaleException", asList("errorIndex"), handler), new BeanExceptionMapping( "java.util.InvalidPropertiesFormatException", asList("formatSpecifier"), handler), new BeanExceptionMapping( "java.util.MissingFormatArgumentException", asList("formatSpecifier"), handler), new BeanExceptionMapping( "java.util.MissingFormatWidthException", asList("formatSpecifier"), handler), new BeanExceptionMapping( "java.util.MissingResourceException", asList("className", "key"), handler), new BeanExceptionMapping( "java.util.UnknownFormatConversionException", asList("conversion"), handler), new BeanExceptionMapping("java.util.UnknownFormatFlagsException", asList("flags"), handler), new BeanExceptionMapping( "javax.naming.NamingException", asList("explanation", "remainingName", "resolvedName"), handler)); } protected void complexMappings(ExceptionMappingRegistry mappings) { mappings.register( EventException.class, (e -> singletonList(ExceptionProperty.create("code", e.code)))); mappings.register( DOMException.class, (e -> singletonList(ExceptionProperty.create("code", e.code)))); mappings.register( XMLStreamException.class, e -> { Location l = e.getLocation(); if (l == null) { return Collections.emptyList(); } return asList( ExceptionProperty.create("lineNumber", l.getLineNumber()), ExceptionProperty.create("columnNumber", l.getColumnNumber()), ExceptionProperty.create("systemId", l.getSystemId()), ExceptionProperty.create("publicId", l.getPublicId()), ExceptionProperty.create("characterOffset", l.getCharacterOffset())); }); mappings.register( InterruptedIOException.class, e -> singletonList(ExceptionProperty.create("bytesTransferred", e.bytesTransferred))); } /** * Once the children elements are also parsed, now is the time to activate the appender options. */ public void end(InterpretationContext ec, String name) { if (inError) { return; } Object o = ec.peekObject(); if (o != mappingsRegistry) { addWarn( "The object at the end of the stack is not the mappingsRegistry named [" + mappingsRegistry + "] pushed earlier."); } else { ec.popObject(); } } } ================================================ FILE: logback-exception-mapping/src/main/java/com/tersesystems/logback/exceptionmapping/ExceptionMessageWithMappingsConverter.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.exceptionmapping; import static com.tersesystems.logback.exceptionmapping.Constants.*; import ch.qos.logback.classic.spi.IThrowableProxy; import ch.qos.logback.classic.spi.ThrowableProxy; import com.tersesystems.logback.classic.ExceptionMessageConverter; import java.util.List; import java.util.Map; import java.util.stream.Collectors; public class ExceptionMessageWithMappingsConverter extends ExceptionMessageConverter { @Override protected String constructMessage(IThrowableProxy ex) { String className = ex.getClassName(); String arguments = findArgumentMappings(ex); return String.format("%s(%s)", className, arguments); } private String getMappingsKey() { return DEFAULT_MAPPINGS_KEY; } private String findArgumentMappings(IThrowableProxy ex) { if (ex instanceof ThrowableProxy) { ExceptionMappingRegistry registry = getRegistry(); if (registry == null) { return ""; } Throwable throwable = ((ThrowableProxy) ex).getThrowable(); return format(registry.apply(throwable)); } else { return ""; } } private String format(List args) { return args.stream() .map( arg -> { StringBuilder sb = new StringBuilder(); StringBufferExceptionPropertyWriter exceptionPropertyWriter = new StringBufferExceptionPropertyWriter(sb); exceptionPropertyWriter.write(arg); return sb.toString(); }) .collect(Collectors.joining(" ")); } @SuppressWarnings("unchecked") private ExceptionMappingRegistry getRegistry() { String key = getMappingsKey(); Map mappingsBag = (Map) getContext().getObject(REGISTRY_BAG); if (mappingsBag == null) { addError("No mappingsRegistry bag found for converter!"); return null; } ExceptionMappingRegistry exceptionMappingRegistry = mappingsBag.get(key); if (exceptionMappingRegistry == null) { addError("No mappingsRegistry found for converter for key " + key); } return exceptionMappingRegistry; } class StringBufferExceptionPropertyWriter { private final StringBuilder sb; StringBufferExceptionPropertyWriter(StringBuilder sb) { this.sb = sb; } public void write(ExceptionProperty exceptionProperty) { if (exceptionProperty instanceof KeyValueExceptionProperty) { KeyValueExceptionProperty kv = (KeyValueExceptionProperty) exceptionProperty; sb.append(kv.getKey()); sb.append("="); sb.append("\""); sb.append(kv.getValue()); sb.append("\""); } } } ; } ================================================ FILE: logback-exception-mapping/src/main/java/com/tersesystems/logback/exceptionmapping/ExceptionProperty.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.exceptionmapping; import java.util.Arrays; public interface ExceptionProperty { public static ExceptionProperty create(String name, String value) { return new KeyValueExceptionProperty(name, value); } public static ExceptionProperty create(String name, Object value) { return new KeyValueExceptionProperty(name, toString(value)); } static String toString(Object value) { if (value instanceof boolean[]) return Arrays.toString((boolean[]) value); if (value instanceof byte[]) return Arrays.toString((byte[]) value); if (value instanceof short[]) return Arrays.toString((short[]) value); if (value instanceof char[]) return Arrays.toString((char[]) value); if (value instanceof int[]) return Arrays.toString((int[]) value); if (value instanceof long[]) return Arrays.toString((long[]) value); if (value instanceof float[]) return Arrays.toString((float[]) value); if (value instanceof double[]) return Arrays.toString((double[]) value); if (value instanceof Object[]) return Arrays.deepToString((Object[]) value); if (value == null) { return "null"; } return value.toString(); } } ================================================ FILE: logback-exception-mapping/src/main/java/com/tersesystems/logback/exceptionmapping/FunctionExceptionMapping.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.exceptionmapping; import java.util.List; import java.util.function.Function; public class FunctionExceptionMapping implements ExceptionMapping { private final Function> function; private final String name; public FunctionExceptionMapping(String name, Function> f) { this.name = name; this.function = f; } @Override public List apply(Throwable e) { return function.apply(e); } @Override public String getName() { return name; } } ================================================ FILE: logback-exception-mapping/src/main/java/com/tersesystems/logback/exceptionmapping/KeyValueExceptionProperty.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.exceptionmapping; import java.util.Objects; public class KeyValueExceptionProperty implements ExceptionProperty { private final String key; private final String value; KeyValueExceptionProperty(String key, String value) { this.key = key; this.value = value; } public String getKey() { return key; } public String getValue() { return value; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; KeyValueExceptionProperty that = (KeyValueExceptionProperty) o; return key.equals(that.key) && value.equals(that.value); } @Override public int hashCode() { return Objects.hash(key, value); } @Override public String toString() { return String.format("KeyValueExceptionProperty(key=%s, value=%s)", key, getValue()); } } ================================================ FILE: logback-exception-mapping/src/test/java/com/tersesystems/logback/exceptionmapping/ExceptionMappingTest.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.exceptionmapping; import static java.util.Arrays.asList; import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; import java.io.InterruptedIOException; import java.sql.BatchUpdateException; import java.sql.SQLException; import java.util.*; import java.util.function.Consumer; import javax.xml.stream.Location; import javax.xml.stream.XMLStreamException; import org.junit.Test; import org.w3c.dom.DOMException; import org.w3c.dom.events.EventException; public class ExceptionMappingTest { @Test public void testSimpleArgument() { Consumer reporter = Throwable::printStackTrace; DefaultExceptionMappingRegistry mappings = new DefaultExceptionMappingRegistry(reporter); populate(mappings); Exception ex = new EventException(EventException.UNSPECIFIED_EVENT_TYPE_ERR, "unspecified"); List args = mappings.apply(ex); assertThat(args.get(0)) .isEqualTo(ExceptionProperty.create("code", EventException.UNSPECIFIED_EVENT_TYPE_ERR)); } @Test public void testComplexArgument() { Consumer reporter = Throwable::printStackTrace; DefaultExceptionMappingRegistry mappings = new DefaultExceptionMappingRegistry(reporter); populate(mappings); String reason = "felt like it"; String SQLState = "SELECT reason FROM possible_excuses"; int vendorCode = 1337; int[] updateCounts = {1}; Throwable cause = new SQLException("cause of exception"); Exception ex = new BatchUpdateException(reason, SQLState, vendorCode, updateCounts, cause); List args = mappings.apply(ex); // combination of BatchUpdateException + SQLException arguments assertThat(args.get(0)).isEqualTo(ExceptionProperty.create("updateCounts", updateCounts)); assertThat(args.get(1)).isEqualTo(ExceptionProperty.create("errorCode", 1337)); assertThat(args.get(2)).isEqualTo(ExceptionProperty.create("SQLState", SQLState)); } private void populate(DefaultExceptionMappingRegistry mappings) { mappings.register(BatchUpdateException.class, "updateCounts"); mappings.register(SQLException.class, "errorCode", "SQLState"); mappings.register( EventException.class, (e -> singletonList(ExceptionProperty.create("code", e.code)))); mappings.register( XMLStreamException.class, e -> { Location l = e.getLocation(); if (l == null) { return Collections.emptyList(); } return asList( ExceptionProperty.create("lineNumber", l.getLineNumber()), ExceptionProperty.create("columnNumber", l.getColumnNumber()), ExceptionProperty.create("systemId", l.getSystemId()), ExceptionProperty.create("publicId", l.getPublicId()), ExceptionProperty.create("characterOffset", l.getCharacterOffset())); }); mappings.register( InterruptedIOException.class, e -> singletonList(ExceptionProperty.create("bytesTransferred", e.bytesTransferred))); mappings.register( DOMException.class, (e -> singletonList(ExceptionProperty.create("code", e.code)))); } } ================================================ FILE: logback-exception-mapping/src/test/java/com/tersesystems/logback/exceptionmapping/MyCustomException.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.exceptionmapping; public class MyCustomException extends RuntimeException { private final String one; private final String two; private final String three; public MyCustomException(String message, String one, String two, String three, Throwable cause) { super(message, cause); this.one = one; this.two = two; this.three = three; } public String getOne() { return one; } public String getTwo() { return two; } public String getThree() { return three; } } ================================================ FILE: logback-exception-mapping/src/test/java/com/tersesystems/logback/exceptionmapping/Thrower.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.exceptionmapping; import java.sql.BatchUpdateException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class Thrower { private static final Logger logger = LoggerFactory.getLogger(Thrower.class); public static void main(String[] progArgs) { try { doSomethingExceptional(); } catch (RuntimeException e) { logger.error("domain specific message", e); } } static void doSomethingExceptional() { Throwable cause = new BatchUpdateException(); throw new MyCustomException( "This is my message", "one is one", "two is more than one", "three is more than two and one", cause); } } ================================================ FILE: logback-exception-mapping/src/test/resources/logback-test.xml ================================================ %-5relative %-5level %logger{35} - %msg%richex{1, 10, exception=[}%n ================================================ FILE: logback-exception-mapping-providers/gradle.properties ================================================ # # SPDX-License-Identifier: CC0-1.0 # # Copyright 2018-2020 Will Sargent. # # Licensed under the CC0 Public Domain Dedication; # You may obtain a copy of the License at # # http://creativecommons.org/publicdomain/zero/1.0/ # project_description = Extra exception mapping providers ================================================ FILE: logback-exception-mapping-providers/logback-exception-mapping-providers.gradle ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ dependencies { implementation project(':logback-exception-mapping') implementation project(':logback-typesafe-config') implementation "net.logstash.logback:logstash-logback-encoder:$logstashVersion" } ================================================ FILE: logback-exception-mapping-providers/src/main/java/com/tersesystems/logback/exceptionmapping/config/TypesafeConfigMappingsAction.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.exceptionmapping.config; import ch.qos.logback.core.joran.action.Action; import ch.qos.logback.core.joran.spi.ActionException; import ch.qos.logback.core.joran.spi.InterpretationContext; import com.tersesystems.logback.exceptionmapping.ExceptionMappingRegistry; import com.tersesystems.logback.typesafeconfig.ConfigConstants; import com.typesafe.config.Config; import com.typesafe.config.ConfigException; import com.typesafe.config.ConfigValue; import java.util.List; import java.util.Map; import java.util.stream.Collectors; import org.xml.sax.Attributes; public class TypesafeConfigMappingsAction extends Action { @SuppressWarnings("unchecked") private ExceptionMappingRegistry getRegistry(InterpretationContext ic) { Object obj = ic.peekObject(); if (obj == null) { addError("Not in an exception registry"); return null; } if (obj instanceof ExceptionMappingRegistry) { return (ExceptionMappingRegistry) obj; } addError("Parent type is not an exception mapping registry!"); return null; } @Override public void begin(InterpretationContext ic, String name, Attributes attributes) throws ActionException { ExceptionMappingRegistry registry = getRegistry(ic); if (registry == null) { addError("Required exception registry is missing!"); return; } Config config = getConfig(ic); if (config == null) { addError("Required typesafe config is missing!"); return; } String mappingsPath = attributes.getValue("path"); if (mappingsPath == null) { addError("Required attribute 'path' is missing!"); return; } try { Map> mappings = getMappingsFromConfig(config, mappingsPath); registry.register(mappings); } catch (ConfigException e) { addError("Could not resolve configuration using path " + mappingsPath, e); } } private Config getConfig(InterpretationContext ic) { Object obj = ic.getObjectMap().get(ConfigConstants.TYPESAFE_CONFIG_CTX_KEY); if (obj == null) { return null; } if (obj instanceof Config) { return (Config) obj; } addError("Type is not a Config!"); return null; } @Override public void end(InterpretationContext ic, String name) throws ActionException {} public Map> getMappingsFromConfig(Config config, String mappingsPath) { Config mappingsConfig = config.getConfig(mappingsPath); return mappingsConfig.entrySet().stream() .collect( Collectors.toMap( Map.Entry::getKey, (Map.Entry e) -> mappingsConfig.getStringList(e.getKey()))); } } ================================================ FILE: logback-exception-mapping-providers/src/main/java/com/tersesystems/logback/exceptionmapping/json/ExceptionArgumentsProvider.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.exceptionmapping.json; import static com.tersesystems.logback.exceptionmapping.Constants.REGISTRY_BAG; import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.classic.spi.IThrowableProxy; import ch.qos.logback.classic.spi.ThrowableProxy; import com.fasterxml.jackson.core.JsonGenerator; import com.tersesystems.logback.exceptionmapping.*; import java.io.IOException; import java.util.HashMap; import java.util.List; import java.util.Map; import net.logstash.logback.composite.AbstractFieldJsonProvider; import net.logstash.logback.composite.JsonWritingUtils; public class ExceptionArgumentsProvider extends AbstractFieldJsonProvider { @SuppressWarnings("unchecked") private ExceptionMappingRegistry getRegistry() { final String key = Constants.DEFAULT_MAPPINGS_KEY; final Map mappingsBag = (Map) getContext().getObject(REGISTRY_BAG); if (mappingsBag == null) { addError("No mappingsRegistry bag found for converter!"); return null; } ExceptionMappingRegistry exceptionMappingRegistry = mappingsBag.get(key); if (exceptionMappingRegistry == null) { addError("No mappingsRegistry found for converter for key " + key); } return exceptionMappingRegistry; } @Override public void writeTo(JsonGenerator generator, ILoggingEvent event) throws IOException { writeExceptionIfNecessary(generator, event.getThrowableProxy()); } private void writeExceptionIfNecessary(JsonGenerator generator, IThrowableProxy throwableProxy) throws IOException { if (throwableProxy instanceof ThrowableProxy) { ExceptionMappingRegistry registry = getRegistry(); if (registry == null) { addError("No registry found!"); return; } ThrowableProxy proxy = (ThrowableProxy) throwableProxy; Throwable throwable = proxy.getThrowable(); if (getFieldName() != null) { generator.writeArrayFieldStart(getFieldName()); } ExceptionCauseIterator.create(throwable).stream() .forEach( t -> { try { renderException(generator, registry, t); } catch (IOException e) { addError("Cannot render exception", e); } }); if (getFieldName() != null) { generator.writeEndArray(); } } } private void renderException( JsonGenerator generator, ExceptionMappingRegistry registry, Throwable throwable) throws IOException { Map propertyMap = new HashMap<>(); List properties = registry.apply(throwable); for (ExceptionProperty property : properties) { if (property instanceof KeyValueExceptionProperty) { KeyValueExceptionProperty p = ((KeyValueExceptionProperty) property); propertyMap.put(p.getKey(), p.getValue()); } } generator.writeStartObject(); JsonWritingUtils.writeStringField(generator, "name", throwable.getClass().getName()); JsonWritingUtils.writeMapStringFields(generator, "properties", propertyMap); generator.writeEndObject(); } } ================================================ FILE: logback-exception-mapping-providers/src/test/java/com/tersesystems/logback/exceptionmapping/json/ExceptionArgumentsProviderTest.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.exceptionmapping.json; import static com.tersesystems.logback.exceptionmapping.Constants.DEFAULT_MAPPINGS_KEY; import static com.tersesystems.logback.exceptionmapping.Constants.REGISTRY_BAG; import static java.util.Collections.singletonMap; import static org.assertj.core.api.Assertions.assertThat; import ch.qos.logback.classic.Level; import ch.qos.logback.classic.LoggerContext; import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.classic.spi.LoggingEvent; import ch.qos.logback.core.status.OnConsoleStatusListener; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.MappingJsonFactory; import com.tersesystems.logback.exceptionmapping.DefaultExceptionMappingRegistry; import com.tersesystems.logback.exceptionmapping.ExceptionMappingRegistry; import java.io.IOException; import java.io.StringWriter; import java.util.function.Consumer; import org.junit.Test; public class ExceptionArgumentsProviderTest { @Test public void testProvider() throws IOException { LoggerContext context = new LoggerContext(); context.getStatusManager().add(new OnConsoleStatusListener()); createExceptionMappingRegistry(context); StringWriter writer = new StringWriter(); JsonGenerator g = mkJsonGenerator(writer); ExceptionArgumentsProvider provider = new ExceptionArgumentsProvider(); provider.setContext(context); provider.setFieldName("exception"); provider.start(); ILoggingEvent event = mkLoggingEvent(context); g.writeStartObject(); provider.writeTo(g, event); g.writeEndObject(); g.flush(); g.close(); String s = writer.toString(); assertThat(s) .isEqualTo( "{\"exception\":[{\"name\":\"java.lang.RuntimeException\",\"properties\":{\"message\":\"derp\"}}]}"); } private void createExceptionMappingRegistry(LoggerContext context) { Consumer handler = Throwable::printStackTrace; ExceptionMappingRegistry registry = new DefaultExceptionMappingRegistry(handler); registry.register(Throwable.class.getName(), "message"); context.putObject(REGISTRY_BAG, singletonMap(DEFAULT_MAPPINGS_KEY, registry)); } private ILoggingEvent mkLoggingEvent(LoggerContext context) { Exception ex = new RuntimeException("derp"); return new LoggingEvent("fcqn", context.getLogger("fcqn"), Level.INFO, "info", ex, null); } private JsonGenerator mkJsonGenerator(StringWriter writer) throws IOException { MappingJsonFactory jsonFactory = new MappingJsonFactory(); JsonGenerator g = jsonFactory.createGenerator(writer); g.enable(JsonGenerator.Feature.STRICT_DUPLICATE_DETECTION); return g; } } ================================================ FILE: logback-exception-mapping-providers/src/test/java/com/tersesystems/logback/exceptionmapping/json/MySpecialException.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.exceptionmapping.json; import java.time.Instant; public class MySpecialException extends Exception { private final Instant timestamp; public MySpecialException(String message, Instant timestamp) { super(message); this.timestamp = timestamp; } public MySpecialException(String message, Instant timestamp, Throwable cause) { super(message, cause); this.timestamp = timestamp; } public Instant getTimestamp() { return timestamp; } } ================================================ FILE: logback-exception-mapping-providers/src/test/java/com/tersesystems/logback/exceptionmapping/json/TypesafeConfigMappingsActionTest.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.exceptionmapping.json; import static com.tersesystems.logback.exceptionmapping.Constants.DEFAULT_MAPPINGS_KEY; import static com.tersesystems.logback.exceptionmapping.Constants.REGISTRY_BAG; import static java.util.Objects.requireNonNull; import static org.assertj.core.api.Assertions.assertThat; import ch.qos.logback.classic.LoggerContext; import ch.qos.logback.classic.joran.JoranConfigurator; import ch.qos.logback.core.joran.spi.JoranException; import com.tersesystems.logback.exceptionmapping.ExceptionMappingRegistry; import java.util.Map; import org.junit.Before; import org.junit.Test; public class TypesafeConfigMappingsActionTest { private final JoranConfigurator jc = new JoranConfigurator(); private final LoggerContext loggerContext = new LoggerContext(); @Before public void setUp() { jc.setContext(loggerContext); } @Test public void testConfig() throws JoranException { jc.doConfigure( requireNonNull( this.getClass().getClassLoader().getResource("logback-with-exception-mapping.xml"))); Map registryMap = (Map) loggerContext.getObject(REGISTRY_BAG); ExceptionMappingRegistry registry = registryMap.get(DEFAULT_MAPPINGS_KEY); assertThat( registry.contains("com.tersesystems.logback.exceptionmapping.json.MySpecialException")) .isTrue(); } } ================================================ FILE: logback-exception-mapping-providers/src/test/resources/logback-with-exception-mapping.xml ================================================ [%-5level] %logger{15} - %msg%n%xException{10} ================================================ FILE: logback-exception-mapping-providers/src/test/resources/logback.conf ================================================ levels { root = INFO } exceptionmappings { com.tersesystems.logback.exceptionmapping.json.MySpecialException: ["timestamp"] } ================================================ FILE: logback-honeycomb-appender/gradle.properties ================================================ # # SPDX-License-Identifier: CC0-1.0 # # Copyright 2018-2020 Will Sargent. # # Licensed under the CC0 Public Domain Dedication; # You may obtain a copy of the License at # # http://creativecommons.org/publicdomain/zero/1.0/ # project_description = Logback Honeycomb Appender ================================================ FILE: logback-honeycomb-appender/logback-honeycomb-appender.gradle ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ plugins { id 'java-library' } dependencies { api project(":logback-honeycomb-client") implementation project(":logback-classic") implementation "net.logstash.logback:logstash-logback-encoder:$logstashVersion" } ================================================ FILE: logback-honeycomb-appender/src/main/java/com/tersesystems/logback/honeycomb/HoneycombAppender.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.honeycomb; import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.core.UnsynchronizedAppenderBase; import ch.qos.logback.core.encoder.Encoder; import com.tersesystems.logback.classic.StartTime; import com.tersesystems.logback.honeycomb.client.*; import java.time.Instant; import java.util.*; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; import java.util.concurrent.CompletionStage; import java.util.concurrent.ExecutionException; import java.util.stream.StreamSupport; /** Creates an appender that sends data to Honeycomb. */ public class HoneycombAppender extends UnsynchronizedAppenderBase { private String dataSet; private String apiKey; private Encoder encoder; private Integer sampleRate = 1; private Integer queueSize = 50; private BlockingQueue> eventQueue; private boolean batch = true; private boolean includeCallerData = false; private HoneycombClient honeycombClient; public Encoder getEncoder() { return encoder; } public void setQueueSize(Integer queueSize) { this.queueSize = queueSize; } public void setDataSet(String dataSet) { this.dataSet = dataSet; } public void setApiKey(String apiKey) { this.apiKey = apiKey; } public void setEncoder(Encoder encoder) { this.encoder = encoder; } public void setSampleRate(Integer sampleRate) { this.sampleRate = sampleRate; } public void setBatch(boolean batch) { this.batch = batch; } public boolean isIncludeCallerData() { return includeCallerData; } public void setIncludeCallerData(boolean includeCallerData) { this.includeCallerData = includeCallerData; } protected void prepareForDeferredProcessing(ILoggingEvent event) { event.prepareForDeferredProcessing(); if (includeCallerData) { event.getCallerData(); } } @Override public void start() { boolean errorsFound = false; if (encoder == null) { addError("No encoder found!"); errorsFound = true; } if (apiKey == null) { addError("No apiKey found!"); errorsFound = true; } if (dataSet == null) { addError("No dataSet found!"); errorsFound = true; } if (errorsFound) { return; } try { HoneycombClientService honeycombClientService = clientService(); honeycombClient = honeycombClientService.newClient(apiKey, dataSet, this::serialize); if (batch) { eventQueue = new ArrayBlockingQueue<>(queueSize); } super.start(); } catch (Exception e) { addError("Cannot start appender!", e); } } @Override public void stop() { if (started && batch) { dumpQueue(); } if (honeycombClient != null) { try { honeycombClient.close(); } finally { honeycombClient = null; } } super.stop(); } protected void dumpQueue() { try { // Post and then block until we get a response // Probably overkill, but we're shutting down in any case. if (!eventQueue.isEmpty()) { List> list = new ArrayList<>(); eventQueue.drainTo(list); postBatch(list).toCompletableFuture().get(); } } catch (InterruptedException | ExecutionException e) { addError("drainQueue: Cannot generate JSON", e); } } @Override protected void append(ILoggingEvent eventObject) { try { prepareForDeferredProcessing(eventObject); } catch (RuntimeException e) { addWarn( "Unable to prepare event for deferred processing. Event output might be missing data.", e); } Instant startTime = StartTime.from(context, eventObject); HoneycombRequest request = new HoneycombRequest<>(sampleRate, startTime, eventObject); if (batch) { // If queue is full, then drain and post it. if (!eventQueue.offer(request)) { List> list = new ArrayList<>(); // empty the queue eventQueue.drainTo(list); // put one back... eventQueue.offer(request); // post the contents. postBatch(list); } } else { postEvent(request); } } private CompletionStage postEvent(HoneycombRequest honeycombRequest) { return honeycombClient.post(honeycombRequest).thenAccept(this::accept); } private CompletionStage postBatch( Iterable> honeycombRequests) { return honeycombClient.postBatch(honeycombRequests).thenAccept(this::accept); } private byte[] serialize(HoneycombRequest honeycombRequest) { return encoder.encode(honeycombRequest.getEvent()); } private HoneycombClientService clientService() { ServiceLoader loader = ServiceLoader.load(HoneycombClientService.class); Optional first = StreamSupport.stream(loader.spliterator(), false).findFirst(); if (first.isPresent()) { return first.get(); } throw new IllegalStateException("No service found -- do you have a library loaded?"); } private void accept(HoneycombResponse response) { if (!response.isSuccess()) { if (response.isRateLimited()) { addInfo("postEvent: Rate Limited: " + response.getReason()); } else if (response.isBlacklisted() || response.isInvalidKey()) { addError("postEvent: Unrecoverable error: " + response.getReason()); } else { addWarn("postEvent: Transient error: " + response.getReason()); } } else { addInfo("postEvent: successful post"); } } private void accept(List responses) { for (HoneycombResponse response : responses) { if (!response.isSuccess()) { if (response.isRateLimited()) { addInfo("postBatch: Rate Limited: " + response.getReason()); } else if (response.isBlacklisted() || response.isInvalidKey()) { addError("postBatch: Unrecoverable error: " + response.getReason()); } else { addWarn("postBatch: Transient error: " + response.getReason()); } } else { addInfo("postBatch: successful post"); } } } } ================================================ FILE: logback-honeycomb-client/gradle.properties ================================================ # # SPDX-License-Identifier: CC0-1.0 # # Copyright 2018-2020 Will Sargent. # # Licensed under the CC0 Public Domain Dedication; # You may obtain a copy of the License at # # http://creativecommons.org/publicdomain/zero/1.0/ # project_description = Logback Honeycomb Client API ================================================ FILE: logback-honeycomb-client/logback-honeycomb-client.gradle ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ ================================================ FILE: logback-honeycomb-client/src/main/java/com/tersesystems/logback/honeycomb/client/HoneycombClient.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.honeycomb.client; import java.util.List; import java.util.concurrent.CompletionStage; import java.util.function.Function; public interface HoneycombClient { CompletionStage post(HoneycombRequest request); CompletionStage post( HoneycombRequest request, Function, byte[]> encodeFunction); CompletionStage> postBatch(Iterable> requests); CompletionStage> postBatch( Iterable> requests, Function, byte[]> encodeFunction); CompletionStage close(); } ================================================ FILE: logback-honeycomb-client/src/main/java/com/tersesystems/logback/honeycomb/client/HoneycombClientService.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.honeycomb.client; import java.util.function.Function; public interface HoneycombClientService { HoneycombClient newClient( String apiKey, String dataset, Function, byte[]> encodeFunction); } ================================================ FILE: logback-honeycomb-client/src/main/java/com/tersesystems/logback/honeycomb/client/HoneycombHeaders.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.honeycomb.client; public class HoneycombHeaders { public static String teamHeader() { return "X-Honeycomb-Team"; } public static String eventTimeHeader() { return "X-Honeycomb-Event-Time"; } public static String sampleRateHeader() { return "X-Honeycomb-Samplerate"; } } ================================================ FILE: logback-honeycomb-client/src/main/java/com/tersesystems/logback/honeycomb/client/HoneycombRequest.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.honeycomb.client; import java.time.Instant; public class HoneycombRequest { private final Integer sampleRate; private final E event; private final Instant timestamp; public HoneycombRequest(Integer sampleRate, Instant timestamp, E event) { this.sampleRate = sampleRate; this.timestamp = timestamp; this.event = event; } public E getEvent() { return event; } public Integer getSampleRate() { return sampleRate; } public Instant getTimestamp() { return timestamp; } } ================================================ FILE: logback-honeycomb-client/src/main/java/com/tersesystems/logback/honeycomb/client/HoneycombResponse.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.honeycomb.client; public class HoneycombResponse { private final String reason; private final int status; public HoneycombResponse(int status, String reason) { this.status = status; this.reason = reason; } public int getStatus() { return status; } public String getReason() { return reason; } public boolean isSuccess() { return (getStatus() == 200 || getStatus() == 202); } public boolean isInvalidKey() { return is400() && getReason().contains("credentials"); } public boolean isMalformed() { return is400() && getReason().contains("malformed"); } public boolean isTooLarge() { return is400() && getReason().contains("too large"); } public boolean isRateLimited() { return is429() && getReason().contains("rate limiting"); } public boolean isBlacklisted() { return is429() && getReason().contains("blacklisted"); } @Override public String toString() { return String.format("HoneyCombResponse(code = %s, text = %s)", getStatus(), getReason()); } private boolean is400() { return getStatus() == 400; } private boolean is429() { return getStatus() == 429; } } ================================================ FILE: logback-honeycomb-okhttp/gradle.properties ================================================ # # SPDX-License-Identifier: CC0-1.0 # # Copyright 2018-2020 Will Sargent. # # Licensed under the CC0 Public Domain Dedication; # You may obtain a copy of the License at # # http://creativecommons.org/publicdomain/zero/1.0/ # project_description = Logback Honeycomb Client Implementation using OKHTTP ================================================ FILE: logback-honeycomb-okhttp/logback-honeycomb-okhttp.gradle ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ plugins { id 'java-library' } dependencies { api project(":logback-honeycomb-client") implementation "ch.qos.logback:logback-classic:$logbackVersion" // https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.9.9' implementation("com.squareup.okhttp3:okhttp:4.1.0") implementation group: 'com.google.auto.service', name: 'auto-service', version: '1.0-rc6' annotationProcessor "com.google.auto.service:auto-service:1.0-rc6" } ================================================ FILE: logback-honeycomb-okhttp/src/main/java/com/tersesystems/logback/honeycomb/okhttp/HoneycombOkHTTPClient.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.honeycomb.okhttp; import com.fasterxml.jackson.core.JsonFactory; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonToken; import com.tersesystems.logback.honeycomb.client.*; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.time.Instant; import java.time.format.DateTimeFormatter; import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; import java.util.function.Function; import okhttp3.*; /** This class implements a honeycomb client using OK HTTP. */ public class HoneycombOkHTTPClient implements HoneycombClient { private static final MediaType JSON = MediaType.get("application/json; charset=utf-8"); private final JsonFactory jsonFactory; private final OkHttpClient client; private final String apiKey; private final String dataset; private final Function, byte[]> defaultEncodeFunction; public HoneycombOkHTTPClient( OkHttpClient client, JsonFactory jsonFactory, String apiKey, String dataset, Function, byte[]> defaultEncodeFunction) { this.client = client; this.jsonFactory = jsonFactory; this.dataset = dataset; this.apiKey = apiKey; this.defaultEncodeFunction = defaultEncodeFunction; } /** Posts a single event to honeycomb, using the "1/events" endpoint. */ @Override public CompletionStage post(HoneycombRequest honeycombRequest) { return post(honeycombRequest, this.defaultEncodeFunction); } @Override public CompletionStage post( HoneycombRequest honeycombRequest, Function, byte[]> encodeFunction) { String honeycombURL = eventURL(dataset); byte[] bytes = encodeFunction.apply(honeycombRequest); RequestBody body = RequestBody.create(bytes, JSON); Request request = new Request.Builder() .url(honeycombURL) .addHeader(HoneycombHeaders.teamHeader(), apiKey) .addHeader(HoneycombHeaders.eventTimeHeader(), isoTime(honeycombRequest.getTimestamp())) .addHeader( HoneycombHeaders.sampleRateHeader(), honeycombRequest.getSampleRate().toString()) .post(body) .build(); Call call = client.newCall(request); OkHttpResponseFuture result = new OkHttpResponseFuture(); call.enqueue(result); return result.future; } @Override public CompletionStage> postBatch( Iterable> requests) { return postBatch(requests, this.defaultEncodeFunction); } @Override public CompletionStage> postBatch( Iterable> honeycombRequests, Function, byte[]> encodeFunction) { String honeycombURL = batchURL(dataset); try { byte[] batchedJson = generateBatchJson(honeycombRequests, encodeFunction); RequestBody body = RequestBody.create(batchedJson, JSON); Request request = new Request.Builder() .url(honeycombURL) .post(body) .addHeader(HoneycombHeaders.teamHeader(), apiKey) .build(); Call call = client.newCall(request); OkHttpBatchedResponseFuture result = new OkHttpBatchedResponseFuture(); call.enqueue(result); return result.future; } catch (IOException e) { throw new IllegalStateException("should never happen", e); } } private String eventURL(String dataset) { String eventURL = "https://api.honeycomb.io/1/events/"; return eventURL + dataset; } private String batchURL(String dataset) { String batchURL = "https://api.honeycomb.io/1/batch/"; return batchURL + dataset; } public CompletionStage close() { return CompletableFuture.runAsync( () -> { client.dispatcher().executorService().shutdown(); }); } private byte[] generateBatchJson( Iterable> requests, Function, byte[]> encodeFunction) throws IOException { ByteArrayOutputStream stream = new ByteArrayOutputStream(); JsonGenerator generator = jsonFactory.createGenerator(stream); HoneycombRequestFormatter formatter = new HoneycombRequestFormatter(generator); formatter.start(); for (HoneycombRequest request : requests) { formatter.format(request, encodeFunction); } formatter.end(); generator.close(); return stream.toByteArray(); } private String isoTime(Instant eventTime) { return DateTimeFormatter.ISO_INSTANT.format(eventTime); } class HoneycombRequestFormatter { private final JsonGenerator generator; HoneycombRequestFormatter(JsonGenerator generator) { this.generator = generator; } void start() throws IOException { this.generator.writeStartArray(); } void end() throws IOException { this.generator.writeEndArray(); } void format(HoneycombRequest request, Function, byte[]> encodeFunction) throws IOException { byte[] bytes = encodeFunction.apply(request); generator.writeStartObject(); generator.writeStringField("time", isoTime(request.getTimestamp())); generator.writeNumberField("samplerate", request.getSampleRate()); generator.writeFieldName("data"); generator.writeRaw(":"); generator.writeRaw(new String(bytes, StandardCharsets.UTF_8)); generator.writeEndObject(); } } static class OkHttpResponseFuture implements Callback { private final CompletableFuture future = new CompletableFuture<>(); OkHttpResponseFuture() {} @Override public void onFailure(Call call, IOException e) { future.completeExceptionally(e); } @Override public void onResponse(Call call, Response response) throws IOException { HoneycombResponse honeycombResponse = new HoneycombResponse(response.code(), response.body().string()); future.complete(honeycombResponse); } } class OkHttpBatchedResponseFuture implements Callback { private final CompletableFuture> future = new CompletableFuture<>(); OkHttpBatchedResponseFuture() {} @Override public void onFailure(Call call, IOException e) { future.completeExceptionally(e); } @Override public void onResponse(Call call, Response response) throws IOException { List honeycombResponses = parseResponse(response); future.complete(honeycombResponses); } private List parseResponse(Response wsResponse) throws IOException { String body = wsResponse.body().string(); List list = new ArrayList<>(); JsonParser parser = jsonFactory.createParser(body); while (!parser.isClosed()) { JsonToken jsonToken = parser.nextToken(); if (JsonToken.FIELD_NAME.equals(jsonToken)) { String fieldName = parser.getCurrentName(); jsonToken = parser.nextToken(); String reason = ""; int status = 0; if ("error".equals(fieldName)) { reason = parser.getValueAsString(); } else if ("status".equals(fieldName)) { status = parser.getValueAsInt(); } HoneycombResponse response = new HoneycombResponse(status, reason); list.add(response); } } return list; } } } ================================================ FILE: logback-honeycomb-okhttp/src/main/java/com/tersesystems/logback/honeycomb/okhttp/HoneycombOkHTTPClientService.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.honeycomb.okhttp; import com.fasterxml.jackson.core.JsonFactory; import com.tersesystems.logback.honeycomb.client.HoneycombClient; import com.tersesystems.logback.honeycomb.client.HoneycombClientService; import com.tersesystems.logback.honeycomb.client.HoneycombRequest; import java.util.function.Function; import okhttp3.OkHttpClient; public class HoneycombOkHTTPClientService implements HoneycombClientService { @Override public HoneycombClient newClient( String apiKey, String dataset, Function, byte[]> encodeFunction) { return new HoneycombOkHTTPClient( new OkHttpClient(), new JsonFactory(), apiKey, dataset, encodeFunction); } } ================================================ FILE: logback-honeycomb-okhttp/src/main/resources/META-INF/services/com.tersesystems.logback.honeycomb.client.HoneycombClientService ================================================ # # SPDX-License-Identifier: CC0-1.0 # # Copyright 2018-2020 Will Sargent. # # Licensed under the CC0 Public Domain Dedication; # You may obtain a copy of the License at # # http://creativecommons.org/publicdomain/zero/1.0/ # # # SPDX-License-Identifier: CC0-1.0 # # Copyright 2018-2020 Will Sargent. # # Licensed under the CC0 Public Domain Dedication; # You may obtain a copy of the License at # # http://creativecommons.org/publicdomain/zero/1.0/ # com.tersesystems.logback.honeycomb.okhttp.HoneycombOkHTTPClientService ================================================ FILE: logback-jdbc-appender/gradle.properties ================================================ # # SPDX-License-Identifier: CC0-1.0 # # Copyright 2018-2020 Will Sargent. # # Licensed under the CC0 Public Domain Dedication; # You may obtain a copy of the License at # # http://creativecommons.org/publicdomain/zero/1.0/ # project_description = Logback JDBC Appender ================================================ FILE: logback-jdbc-appender/logback-jdbc-appender.gradle ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ plugins { id 'java-library' } dependencies { api project(':logback-classic') // .200 has the JSON data type implementation "com.zaxxer:HikariCP:3.4.2" testImplementation "com.h2database:h2:1.4.200" testImplementation project(':logback-typesafe-config') testImplementation 'org.awaitility:awaitility:4.0.2' testImplementation 'org.apiguardian:apiguardian-api:1.1.0' testImplementation "net.logstash.logback:logstash-logback-encoder:$logstashVersion" } ================================================ FILE: logback-jdbc-appender/src/main/java/com/tersesystems/logback/jdbc/JDBCAppender.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.jdbc; import static java.util.Objects.requireNonNull; import ch.qos.logback.classic.Level; import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.core.UnsynchronizedAppenderBase; import ch.qos.logback.core.encoder.Encoder; import com.tersesystems.logback.classic.NanoTime; import com.tersesystems.logback.classic.StartTime; import com.zaxxer.hikari.HikariConfig; import com.zaxxer.hikari.HikariDataSource; import java.sql.*; import java.time.Duration; import java.time.Instant; import java.util.*; import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.LongAdder; import java.util.function.Consumer; /** * This appender writes out to a single table through JDBC. * *

It uses HikariCP and a thread pool executor to set up the appropriate thread pool * size. * *

Note that despite using a thread pool sized to the database connection pool, you should always * use the JDBC appender behind an async appender of some sort, as you'll want to ensure that * there's a queue feeding into the workers if they're all busy. */ public class JDBCAppender extends UnsynchronizedAppenderBase { private final AtomicBoolean initialized = new AtomicBoolean(false); private Encoder encoder; private HikariDataSource dataSource; private Duration reaperDuration; private String driver; private String url; private String username; private String password; private String insertStatement; private String createStatements; private String reaperStatement; private String reaperSchedule; private String poolName = "jdbc-appender-pool-" + System.currentTimeMillis(); private int poolSize = 2; private ExecutorService executorService; // Debug flag for checking that a row was inserted. private InsertConsumer insertConsumer; protected boolean loggingInsert = false; public boolean isLoggingInsert() { return loggingInsert; } public String getUrl() { return url; } public void setUrl(String url) { this.url = url; } public Encoder getEncoder() { return encoder; } public void setEncoder(Encoder encoder) { this.encoder = encoder; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public void setPoolSize(int poolSize) { this.poolSize = poolSize; } public String getPoolName() { return poolName; } public void setPoolName(String poolName) { this.poolName = poolName; } public String getDriver() { return driver; } public void setDriver(String driver) { this.driver = driver; } public String getReaperSchedule() { return reaperSchedule; } public void setReaperSchedule(String reaperSchedule) { this.reaperSchedule = reaperSchedule; } public String getReaperStatement() { return reaperStatement; } public void setReaperStatement(String reaperStatement) { this.reaperStatement = reaperStatement; } public String getCreateStatements() { return createStatements; } public void setCreateStatements(String createStatements) { this.createStatements = createStatements; } public String getInsertStatement() { return insertStatement; } public void setInsertStatement(String insertStatement) { this.insertStatement = insertStatement; } @Override public void start() { super.start(); executorService = Executors.newFixedThreadPool(poolSize); } @Override public void stop() { super.stop(); closeConnection(); shutdownThreadPool(); initialized.set(false); } @Override protected void append(ILoggingEvent event) { if (!initialized.get()) { initialize(); } executorService.submit(() -> insertConsumer.accept(event)); } // When the appender is starting, then Logback hasn't started up yet and so // we'll get very odd errors if the driver starts trying to log things itself. // Instead, we're going to register something that will start a datasource // when something comes through the pipeline. protected void initialize() { if (!initialized.getAndSet(true)) { addInfo("initialize: "); try { dataSource = createDataSource(driver, url, username, password); insertConsumer = new InsertConsumer(); checkConnection(); createTableIfNecessary(); scheduleReaper(); } catch (Exception e) { addError("Cannot configure database connection", e); } } } protected HikariDataSource createDataSource( String driver, String url, String username, String password) { HikariConfig config = new HikariConfig(); config.setDriverClassName(Objects.requireNonNull(driver, "Null driver")); config.setJdbcUrl(requireNonNull(url, "Null url")); config.setUsername(username); config.setPassword(password); config.setPoolName(poolName); config.setAutoCommit(true); // always use autocommit mode here. config.setMinimumIdle(poolSize); config.setMaximumPoolSize(poolSize); Properties props = new Properties(); // props.put("dataSource.logWriter", new PrintWriter(System.out)); config.setDataSourceProperties(props); return new HikariDataSource(config); } protected void shutdownThreadPool() { if (executorService == null || executorService.isTerminated()) { return; } try { executorService.awaitTermination(1, TimeUnit.SECONDS); executorService = null; } catch (InterruptedException e) { // This isn't worth reporting. } } protected void checkConnection() throws SQLException { if (dataSource != null) { try (Connection conn = dataSource.getConnection()) { addInfo("checkConnection: trying isValid with a 1000 msec timeout"); if (conn.isValid(1000)) { addInfo("checkConnection: isValid returned true!"); } else { addWarn("checkConnection: isValid returned false!"); } } } } protected void createTableIfNecessary() { // Initialize with DDL // XXX should really check if the table exists already String createStatements = getCreateStatements(); if (createStatements == null || createStatements.trim().isEmpty()) { return; } addInfo("createTable: " + createStatements); try { try (Connection conn = dataSource.getConnection()) { try (Statement stmt = conn.createStatement()) { stmt.executeUpdate(createStatements); } } } catch (SQLException e) { addWarn("Cannot create table, assuming it exists already", e); } } protected void scheduleReaper() { String reaperSchedule = getReaperSchedule(); if (reaperSchedule == null || reaperSchedule.trim().isEmpty()) { return; } addInfo("scheduleReaper: " + reaperSchedule); String reaperStatement = getReaperStatement(); if (reaperStatement == null || reaperStatement.trim().isEmpty()) { addError( "scheduleReaper: reaperSchedule exists, but there is no reaperStatement to execute!"); return; } reaperDuration = Duration.parse(reaperSchedule); ScheduledExecutorService ses = context.getScheduledExecutorService(); ScheduledFuture scheduledFuture = ses.scheduleAtFixedRate( this::reapOldEvents, reaperDuration.toMillis(), reaperDuration.toMillis(), TimeUnit.MILLISECONDS); context.addScheduledFuture(scheduledFuture); } protected void reapOldEvents() { addInfo("reapOldEvents: "); try { try (Connection conn = dataSource.getConnection()) { try (PreparedStatement stmt = conn.prepareStatement(getReaperStatement())) { Instant reapAtInstant = now().minus(reaperDuration); stmt.setTimestamp(1, new java.sql.Timestamp(reapAtInstant.toEpochMilli())); int results = stmt.executeUpdate(); addInfo(String.format("Reaped %d statements older than %s", results, reapAtInstant)); } } } catch (SQLException e) { addWarn("Cannot reap old events!", e); } } protected Instant now() { return Instant.now(); } protected void closeConnection() { insertConsumer = null; if (dataSource != null) { try { if (!dataSource.isClosed()) { dataSource.close(); } dataSource = null; } catch (Exception e) { addError("Exception closing datasource", e); } } } protected int insertStatement(ILoggingEvent event, LongAdder adder, PreparedStatement statement) throws SQLException { insertTimestamp(event, adder, statement); insertRelativeTime(event, adder, statement); insertStartTime(event, adder, statement); insertIntLevel(event, adder, statement); insertStringLevel(event, adder, statement); insertEvent(event, adder, statement); insertAdditionalData(event, adder, statement); return statement.executeUpdate(); } /** * An empty method for use by subclasses who want to add additional fields. * *

Make sure to call adder.increment() * * @param event logging event * @param adder adder * @param statement prepared statement * @throws SQLException if something goes wrong */ protected void insertAdditionalData( ILoggingEvent event, LongAdder adder, PreparedStatement statement) throws SQLException { // do nothing } protected void insertEvent(ILoggingEvent event, LongAdder adder, PreparedStatement statement) throws SQLException { statement.setBytes(adder.intValue(), encoder.encode(event)); adder.increment(); } protected void insertStringLevel( ILoggingEvent event, LongAdder adder, PreparedStatement statement) throws SQLException { statement.setString(adder.intValue(), event.getLevel().toString()); adder.increment(); } protected void insertIntLevel(ILoggingEvent event, LongAdder adder, PreparedStatement statement) throws SQLException { Level level = event.getLevel(); statement.setInt(adder.intValue(), level.toInt()); adder.increment(); } protected void insertStartTime(ILoggingEvent event, LongAdder adder, PreparedStatement statement) throws SQLException { Optional startTime = StartTime.fromOptional(context, event).map(Instant::toEpochMilli); if (startTime.isPresent()) { statement.setLong(adder.intValue(), startTime.get()); } else { statement.setNull(adder.intValue(), Types.BIGINT); } adder.increment(); } protected void insertRelativeTime( ILoggingEvent event, LongAdder adder, PreparedStatement statement) throws SQLException { Optional aLong = NanoTime.fromOptional(context, event); if (aLong.isPresent()) { statement.setLong(adder.intValue(), aLong.get()); } else { statement.setNull(adder.intValue(), Types.BIGINT); } adder.increment(); } protected void insertTimestamp(ILoggingEvent event, LongAdder adder, PreparedStatement statement) throws SQLException { long eventMillis = event.getTimeStamp(); statement.setTimestamp(adder.intValue(), new Timestamp(eventMillis)); adder.increment(); } class InsertConsumer implements Consumer { public void accept(ILoggingEvent event) { // Will need to check state here because executor service runs in a different thread. if (isStarted()) { try (Connection conn = dataSource.getConnection()) { String insertStatement = requireNonNull(getInsertStatement()); if (conn.isValid(100)) { try (PreparedStatement statement = conn.prepareStatement(insertStatement)) { LongAdder adder = new LongAdder(); adder.increment(); int result = insertStatement(event, adder, statement); if (isLoggingInsert()) { String msg = String.format("Inserted resulted in %d rows added", result); addInfo(msg); } } } else { addError("Connection is not valid!"); } } catch (Exception e) { addError("Cannot insert event, please check you are using a valid encoder!", e); } } else { addError("Not started yet, returning to queue!"); } } } } ================================================ FILE: logback-jdbc-appender/src/test/java/com/tersesystems/logback/jdbc/JDBCAppenderTest.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.jdbc; import static java.util.Objects.requireNonNull; import static java.util.concurrent.TimeUnit.SECONDS; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; import static org.junit.Assert.fail; import ch.qos.logback.classic.Logger; import ch.qos.logback.classic.LoggerContext; import ch.qos.logback.classic.joran.JoranConfigurator; import ch.qos.logback.core.joran.spi.JoranException; import java.net.URL; import java.sql.*; import org.junit.After; import org.junit.Before; import org.junit.jupiter.api.Test; public class JDBCAppenderTest { @Before @After public void clear() { try (Connection conn = DriverManager.getConnection("jdbc:h2:mem:terse-logback", "sa", "")) { try (Statement s = conn.createStatement()) { s.execute("truncate table events"); } } catch (SQLException e) { fail(e.getMessage()); } } @Test public void testSimple() throws JoranException, SQLException { LoggerContext loggerFactory = createLoggerFactory("/logback-test.xml"); // Write something that never gets logged explicitly... Logger logger = loggerFactory.getLogger("some.example.ExampleClass"); logger.info("info one"); await().atMost(5, SECONDS).until(this::assertTablesExist); logger.info("info two"); logger.info("info three"); await().atMost(1, SECONDS).untilAsserted(() -> assertRowsEntered(3)); } private Boolean assertTablesExist() { try (Connection conn = DriverManager.getConnection("jdbc:h2:mem:terse-logback", "sa", "")) { try (PreparedStatement p = conn.prepareStatement("select count(*) from events")) { return p.execute(); } } catch (SQLException e) { return false; } } public void assertRowsEntered(Integer expectedCount) throws SQLException { try (Connection conn = DriverManager.getConnection("jdbc:h2:mem:terse-logback", "sa", "")) { try (PreparedStatement p = conn.prepareStatement("select count(*) from events")) { assertThat(getCount(p)).isEqualTo(expectedCount); } } } public int getCount(PreparedStatement p) throws SQLException { try (ResultSet rs = p.executeQuery()) { if (rs.next()) { return rs.getInt(1); } else { return 0; } } } LoggerContext createLoggerFactory(String resourceName) throws JoranException { LoggerContext context = new LoggerContext(); URL resource = getClass().getResource(resourceName); JoranConfigurator configurator = new JoranConfigurator(); configurator.setContext(context); configurator.doConfigure(resource); return context; } JDBCAppender getJDBCAppender(LoggerContext context) { return (JDBCAppender) requireNonNull(context.getLogger(Logger.ROOT_LOGGER_NAME).getAppender("JDBC")); } } ================================================ FILE: logback-jdbc-appender/src/test/resources/logback-reference.conf ================================================ levels { ROOT = INFO } local { jdbc { url = "jdbc:h2:mem:terse-logback" driver = "org.h2.Driver" username = "sa" password = "" insertStatement = "insert into events(ts, relative_ns, start_ms, level_value, level, evt) values(?, ?, ?, ?, ?, ?)" createStatements = """ CREATE TABLE IF NOT EXISTS events ( ID INT NOT NULL PRIMARY KEY AUTO_INCREMENT, ts TIMESTAMP(9) WITH TIME ZONE NOT NULL, relative_ns numeric NULL, start_ms numeric NULL, level_value int NOT NULL, level VARCHAR(7) NOT NULL, evt JSON NOT NULL ); """ reaperStatement = "delete from events where ts < ?" reaperSchedule = PT30 } } # Defines properties (Strings) to be set in context scope (loggerContext.putProperty) # See https://logback.qos.ch/manual/configuration.html#scopes context { } ================================================ FILE: logback-jdbc-appender/src/test/resources/logback-test.xml ================================================ ${jdbc.driver} ${jdbc.url} ${jdbc.username} ${jdbc.password} ${jdbc.createStatements} ${jdbc.insertStatement} ${jdbc.reaperStatement} ${jdbc.reaperSchedule} ================================================ FILE: logback-postgresjson-appender/gradle.properties ================================================ # # SPDX-License-Identifier: CC0-1.0 # # Copyright 2018-2020 Will Sargent. # # Licensed under the CC0 Public Domain Dedication; # You may obtain a copy of the License at # # http://creativecommons.org/publicdomain/zero/1.0/ # project_description = Logback Postgres JSON Appender ================================================ FILE: logback-postgresjson-appender/logback-postgresjson-appender.gradle ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ dependencies { implementation project(":logback-jdbc-appender") implementation group: 'org.postgresql', name: 'postgresql', version: '42.2.6' // Need to set up a FlywayBaseTest, the gradle plugin won't run a "testFlywayMigrate" task // testImplementation "org.flywaydb:flyway-core:6.0.0" // technically any JSON string is valid input, so we only require logstash-logback-encoder for testing testImplementation "net.logstash.logback:logstash-logback-encoder:$logstashVersion" } ================================================ FILE: logback-postgresjson-appender/src/main/java/com/tersesystems/logback/postgresjson/PostgresJsonAppender.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.postgresjson; import ch.qos.logback.classic.spi.ILoggingEvent; import com.tersesystems.logback.jdbc.JDBCAppender; import java.nio.charset.StandardCharsets; import java.sql.PreparedStatement; import java.sql.SQLException; import java.util.concurrent.atomic.LongAdder; import org.postgresql.util.PGobject; /** Extends the JDBC appender to write out Postgres JSON object. */ public class PostgresJsonAppender extends JDBCAppender { private String objectType = "json"; public String getObjectType() { return objectType; } public void setObjectType(String objectType) { this.objectType = objectType; } @Override public void start() { super.start(); setDriver("org.postgresql.Driver"); } @Override protected void insertEvent(ILoggingEvent event, LongAdder adder, PreparedStatement statement) throws SQLException { PGobject jsonObject = new PGobject(); jsonObject.setType(getObjectType()); byte[] bytes = getEncoder().encode(event); jsonObject.setValue(new String(bytes, StandardCharsets.UTF_8)); statement.setObject(adder.intValue(), jsonObject); adder.increment(); } } ================================================ FILE: logback-postgresjson-appender/src/test/java/com/tersesystems/logback/postgresjson/PostgresJsonAppenderTest.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.postgresjson; import ch.qos.logback.classic.Logger; import ch.qos.logback.core.joran.spi.JoranException; import com.tersesystems.logback.classic.Utils; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; public class PostgresJsonAppenderTest { @Disabled @Test public void testJson() throws JoranException, InterruptedException { Utils utils = Utils.create("/logback-postgres-json.xml"); Logger logger1 = utils.getLogger("com.example.Test"); logger1.info("THIS IS A TEST"); Thread.sleep(1000); utils.getStatusList().forEach(System.out::println); } } ================================================ FILE: logback-postgresjson-appender/src/test/resources/db/migration/V1__logging_table.sql ================================================ -- -- SPDX-License-Identifier: CC0-1.0 -- -- Copyright 2018-2019 Will Sargent. -- -- Licensed under the CC0 Public Domain Dedication; -- You may obtain a copy of the License at -- -- http://creativecommons.org/publicdomain/zero/1.0/ -- -- timestamp will only give microsecond precision, so we store both timestamp and time since epoch in milliseconds. -- store the start time in milliseconds. CREATE TABLE logging_table ( ID serial NOT NULL PRIMARY KEY, ts TIMESTAMPTZ(6) NOT NULL, tse_ms numeric NOT NULL, start_ms numeric NULL, level_value int NOT NULL, level VARCHAR(7) NOT NULL, evt jsonb NOT NULL ); CREATE INDEX idxgin ON logging_table USING gin (evt); ================================================ FILE: logback-postgresjson-appender/src/test/resources/logback-postgres-json.xml ================================================ CREATE TABLE logging_table ( ID serial NOT NULL PRIMARY KEY, ts TIMESTAMPTZ(6) NOT NULL, tse_ms bigint NOT NULL, start_ms bigint NULL, level_value int NOT NULL, level VARCHAR(7) NOT NULL, evt jsonb NOT NULL ); CREATE INDEX idxgin ON logging_table USING gin (evt); insert into logging_table(ts, tse_ms, start_ms, level_value, level, evt) values(?, ?, ?, ?, ?, ?) jdbc:postgresql://localhost:5432/logback logback logback %-5relative %-5level %logger{35} - %msg%n ================================================ FILE: logback-tracing/gradle.properties ================================================ # # SPDX-License-Identifier: CC0-1.0 # # Copyright 2018-2020 Will Sargent. # # Licensed under the CC0 Public Domain Dedication; # You may obtain a copy of the License at # # http://creativecommons.org/publicdomain/zero/1.0/ # project_description = Logback Honeycomb Tracing ================================================ FILE: logback-tracing/logback-tracing.gradle ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ dependencies { //implementation "org.slf4j:slf4j-api:$slf4jVersion" //implementation project(':logback-uniqueid-appender') implementation project(':logback-classic') implementation "com.google.auto.value:auto-value-annotations:1.6.2" annotationProcessor "com.google.auto.value:auto-value:1.6.2" implementation "net.logstash.logback:logstash-logback-encoder:$logstashVersion" implementation "ch.qos.logback:logback-classic:$logbackVersion" } ================================================ FILE: logback-tracing/src/main/java/com/tersesystems/logback/tracing/EventInfo.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.tracing; import com.google.auto.value.AutoValue; /** * An event info is a span without a duration. It cannot be used as a parent. * *

https://docs.honeycomb.io/working-with-your-data/tracing/send-trace-data/#span-events */ @AutoValue public abstract class EventInfo { public static Builder builder() { return new AutoValue_EventInfo.Builder(); } public abstract Builder toBuilder(); @Nullable public abstract String parentId(); public abstract String traceId(); public abstract String name(); @AutoValue.Builder public abstract static class Builder { public abstract Builder setName(String name); public abstract Builder setParentId(String parentId); public abstract Builder setTraceId(String traceId); public abstract EventInfo build(); } } ================================================ FILE: logback-tracing/src/main/java/com/tersesystems/logback/tracing/EventMarkerFactory.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.tracing; import net.logstash.logback.marker.LogstashMarker; import net.logstash.logback.marker.Markers; /** * This is a marker factory that adds several logstash markers to create a span in Honeycomb format. */ public class EventMarkerFactory { // Java API public LogstashMarker create(EventInfo eventInfo) { LogstashMarker[] markers = generateMarkers(eventInfo); return Markers.aggregate(markers); } // Scala API public LogstashMarker apply(EventInfo eventInfo) { return create(eventInfo); } protected LogstashMarker[] generateMarkers(EventInfo eventInfo) { // XXX Should have a field name registry that lets you define field names by dataset LogstashMarker nameMarker = Markers.append("name", eventInfo.name()); LogstashMarker parentIdMarker = Markers.append("trace.parent_id", eventInfo.parentId()); LogstashMarker traceIdMarker = Markers.append("trace.trace_id", eventInfo.traceId()); LogstashMarker spanTypeMarker = Markers.append("meta.span_type", "span_event"); // Don't include the timestamp marker, as it'll be handled by Logback LogstashMarker[] markers = {nameMarker, parentIdMarker, traceIdMarker, spanTypeMarker}; return markers; } } ================================================ FILE: logback-tracing/src/main/java/com/tersesystems/logback/tracing/LinkInfo.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.tracing; import com.google.auto.value.AutoValue; /** https://docs.honeycomb.io/working-with-your-data/tracing/send-trace-data/#links */ @AutoValue public abstract class LinkInfo { public static Builder builder() { return new AutoValue_LinkInfo.Builder(); } public abstract Builder toBuilder(); @Nullable public abstract String parentId(); public abstract String traceId(); public abstract String linkedSpanId(); public abstract String linkedTraceId(); @AutoValue.Builder public abstract static class Builder { public abstract Builder setLinkedSpanId(String linkedSpanId); public abstract Builder setLinkedTraceId(String linkedTraceId); public abstract Builder setParentId(String parentId); public abstract Builder setTraceId(String traceId); public abstract LinkInfo build(); } } ================================================ FILE: logback-tracing/src/main/java/com/tersesystems/logback/tracing/LinkMarkerFactory.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.tracing; import net.logstash.logback.marker.LogstashMarker; import net.logstash.logback.marker.Markers; public class LinkMarkerFactory { // Java API public LogstashMarker create(LinkInfo linkInfo) { LogstashMarker[] markers = generateMarkers(linkInfo); return Markers.aggregate(markers); } // Scala API public LogstashMarker apply(LinkInfo linkInfo) { return create(linkInfo); } protected LogstashMarker[] generateMarkers(LinkInfo linkInfo) { // XXX Should have a field name registry that lets you define field names by dataset LogstashMarker traceIdMarker = Markers.append("trace.trace_id", linkInfo.traceId()); LogstashMarker parentIdMarker = Markers.append("trace.parent_id", linkInfo.parentId()); LogstashMarker linkedSpanMarker = Markers.append("trace.link.span_id", linkInfo.linkedSpanId()); LogstashMarker linkedTraceMarker = Markers.append("trace.link.trace_id", linkInfo.linkedTraceId()); LogstashMarker spanTypeMarker = Markers.append("meta.span_type", "link"); LogstashMarker[] markers = { parentIdMarker, traceIdMarker, linkedSpanMarker, linkedTraceMarker, spanTypeMarker }; return markers; } } ================================================ FILE: logback-tracing/src/main/java/com/tersesystems/logback/tracing/Nullable.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.tracing; import static java.lang.annotation.ElementType.TYPE_USE; import static java.lang.annotation.RetentionPolicy.SOURCE; import java.lang.annotation.Retention; import java.lang.annotation.Target; /** * autovalue wants a Nullable but doesn't tell us from where. * *

anything will work, so defining one here. * *

https://github.com/google/auto/issues/283#issuecomment-337281043 */ @Target(TYPE_USE) @Retention(SOURCE) @interface Nullable {} ================================================ FILE: logback-tracing/src/main/java/com/tersesystems/logback/tracing/SpanInfo.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.tracing; import com.google.auto.value.AutoValue; import java.time.Duration; import java.time.Instant; import java.util.function.Function; import java.util.function.Supplier; @AutoValue public abstract class SpanInfo { public static Builder builder() { return new AutoValue_SpanInfo.Builder(); } public abstract Builder toBuilder(); public abstract String spanId(); @Nullable public abstract String parentId(); public abstract String traceId(); public abstract String name(); public abstract String serviceName(); public abstract Instant startTime(); public Duration duration() { return durationSupplier().get(); } public abstract Supplier durationSupplier(); public abstract Supplier idGenerator(); /** * Creates a child builder with the parent id set to the current span id, a random UUID set to the * span id. * * @return a child builder. */ public Builder childBuilder() { return this.toBuilder() .setSpanId(idGenerator().get()) .setIdGenerator(idGenerator()) .setParentId(spanId()); } /** * Provides a function with a child that can be used as a convenience wrapper, which calls {@code * childBuilder().setName().buildNow()} under the hood. * *

{@code

return parentSpanInfo.withChild("doThing", childSpan -> { return
   * doThing(childSpan); }); 
} * * @param methodName the name of the child span * @param childFunction the child function * @param the type of the return value * @return the return value */ public T withChild(String methodName, Function childFunction) { return childFunction.apply(childBuilder().setName(methodName).buildNow()); } @AutoValue.Builder public abstract static class Builder { public abstract Builder setName(String name); public abstract Builder setSpanId(String spanId); public abstract Builder setParentId(String parentId); public abstract Builder setTraceId(String traceId); public abstract Builder setIdGenerator(Supplier idGenerator); public abstract Builder setStartTime(Instant startTime); public abstract Builder setServiceName(String serviceName); public abstract Builder setDurationSupplier(Supplier duration); public abstract SpanInfo build(); /** * Creates a random UUID for the trace id and span id and set the name. * * @param idGenerator the id generator for span and trace. * @param name the span name * @return the configured builder. */ public Builder setRootSpan(Supplier idGenerator, String name) { return this.setTraceId(idGenerator.get()) .setSpanId(idGenerator.get()) .setIdGenerator(idGenerator) .setName(name); } public Builder startNow() { Instant startTime = Instant.now(); return this.setDurationSupplier(() -> Duration.between(startTime, Instant.now())) .setStartTime(startTime); } /** * Builds a span info, setting the duration supplier to be {@code Duration.between(now, * Instant.now())} * * @return the span info already started. */ public SpanInfo buildNow() { return startNow().build(); } } } ================================================ FILE: logback-tracing/src/main/java/com/tersesystems/logback/tracing/SpanMarkerFactory.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.tracing; import com.tersesystems.logback.classic.StartTimeMarker; import net.logstash.logback.marker.LogstashMarker; import net.logstash.logback.marker.Markers; /** * This is a marker factory that adds several logstash markers to create a span in Honeycomb format. */ public class SpanMarkerFactory { // Java API public LogstashMarker create(SpanInfo spanInfo) { StartTimeMarker startTime = new StartTimeMarker(spanInfo.startTime()); LogstashMarker[] markers = generateMarkers(spanInfo); return Markers.aggregate(markers).and(startTime); } // Scala API public LogstashMarker apply(SpanInfo spanInfo) { return create(spanInfo); } protected LogstashMarker[] generateMarkers(SpanInfo spanInfo) { LogstashMarker nameMarker = Markers.append("name", spanInfo.name()); LogstashMarker spanIdMarker = Markers.append("trace.span_id", spanInfo.spanId()); LogstashMarker parentIdMarker = Markers.append("trace.parent_id", spanInfo.parentId()); LogstashMarker traceIdMarker = Markers.append("trace.trace_id", spanInfo.traceId()); LogstashMarker serviceNameMarker = Markers.append("service_name", spanInfo.serviceName()); LogstashMarker durationMs = Markers.append("duration_ms", spanInfo.duration().toMillis()); LogstashMarker[] markers = { nameMarker, spanIdMarker, parentIdMarker, traceIdMarker, serviceNameMarker, durationMs }; return markers; } } ================================================ FILE: logback-tracing/src/main/java/com/tersesystems/logback/tracing/Tracer.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.tracing; import java.util.ArrayDeque; import java.util.Deque; import java.util.Optional; import java.util.function.Supplier; public class Tracer { // We don't want to measure a stack more than 300 elements deep, because after that it's just no // fun. private static final int MAX_THREAD_SIZE = 300; private static final ThreadLocal> threadLocal = ThreadLocal.withInitial(() -> new ArrayDeque<>(MAX_THREAD_SIZE)); private static Deque stack() { return threadLocal.get(); } public static Optional popSpan() { return Optional.ofNullable(stack().poll()); } /** * Pushes the event onto the stack, using a parent id. * *

If there is no span or trace, then return empty. * * @param name the name of the span. * @return the event if it was successfully added, otherwise empty. */ public static Optional pushEvent(String name) { Deque stack = stack(); SpanInfo parent = stack.peek(); if (parent == null) { return Optional.empty(); } else { EventInfo info = EventInfo.builder() .setName(name) .setTraceId(parent.traceId()) .setParentId(parent.spanId()) .build(); return Optional.of(info); } } /** * Creates a span, using the parent, and adds it to the stack. * * @param name the name of the span. * @param serviceName the service name, only needed if this is the root span. * @param idGenerator the span's id generator. * @return the span if it was successfully added, otherwise empty. */ public static Optional pushSpan( String name, String serviceName, Supplier idGenerator) { Deque stack = stack(); SpanInfo parent = stack.peek(); SpanInfo span; if (parent != null) { span = parent.childBuilder().setName(name).buildNow(); } else { span = SpanInfo.builder().setRootSpan(idGenerator, name).setServiceName(serviceName).buildNow(); } if (stack.offerFirst(span)) { return Optional.of(span); } else { return Optional.empty(); } } public static Optional activeSpan() { Deque stack = stack(); return !stack.isEmpty() ? Optional.ofNullable(stack.peek()) : Optional.empty(); } public static void clear() { stack().clear(); } } ================================================ FILE: logback-turbomarker/gradle.properties ================================================ # # SPDX-License-Identifier: CC0-1.0 # # Copyright 2018-2020 Will Sargent. # # Licensed under the CC0 Public Domain Dedication; # You may obtain a copy of the License at # # http://creativecommons.org/publicdomain/zero/1.0/ # project_description = Logback TurboMarker ================================================ FILE: logback-turbomarker/logback-turbomarker.gradle ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ dependencies { implementation project(':logback-classic') implementation "ch.qos.logback:logback-classic:$logbackVersion" // https://mvnrepository.com/artifact/org.mockito/mockito-core testImplementation group: 'org.mockito', name: 'mockito-core', version: '3.0.0' testImplementation "com.launchdarkly:launchdarkly-java-server-sdk:4.6.6" testImplementation "net.logstash.logback:logstash-logback-encoder:$logstashVersion" testImplementation "com.typesafe:config:$configVersion" //testImplementation "com.fasterxml.jackson.module:jackson-datatype-jdk8" //testImplementation "com.fasterxml.jackson.module:jackson-datatype-jsr310" testImplementation 'org.apiguardian:apiguardian-api:1.1.0' testImplementation group: 'com.fasterxml.jackson.core', name: 'jackson-annotations', version: '2.9.9' } ================================================ FILE: logback-turbomarker/src/main/java/com/tersesystems/logback/turbomarker/ContextAwareTurboFilterDecider.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.turbomarker; import ch.qos.logback.classic.Level; import ch.qos.logback.classic.Logger; import ch.qos.logback.core.spi.FilterReply; import org.slf4j.Marker; public interface ContextAwareTurboFilterDecider { FilterReply decide( ContextAwareTurboMarker marker, C context, Marker rootMarker, Logger logger, Level level, String format, Object[] params, Throwable t); } ================================================ FILE: logback-turbomarker/src/main/java/com/tersesystems/logback/turbomarker/ContextAwareTurboMarker.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.turbomarker; import static java.util.Objects.requireNonNull; import ch.qos.logback.classic.Level; import ch.qos.logback.classic.Logger; import ch.qos.logback.core.spi.FilterReply; import com.tersesystems.logback.classic.TurboFilterDecider; import org.slf4j.Marker; /** * This class passes through a custom application context and a matcher, which makes the ultimate * decision. * * @param the context of the predicate marker. */ public class ContextAwareTurboMarker extends TurboMarker implements TurboFilterDecider { private final C context; private final ContextAwareTurboFilterDecider contextAwareDecider; public ContextAwareTurboMarker( String name, C context, ContextAwareTurboFilterDecider decider) { super(name); this.context = requireNonNull(context); this.contextAwareDecider = requireNonNull(decider); } ContextAwareTurboFilterDecider getContextAwareDecider() { return contextAwareDecider; } C getContext() { return context; } @Override public FilterReply decide( Marker rootMarker, Logger logger, Level level, String format, Object[] params, Throwable t) { return contextAwareDecider.decide(this, context, rootMarker, logger, level, format, params, t); } } ================================================ FILE: logback-turbomarker/src/main/java/com/tersesystems/logback/turbomarker/ContextDecider.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.turbomarker; import ch.qos.logback.classic.Level; import ch.qos.logback.classic.Logger; import ch.qos.logback.core.spi.FilterReply; import java.util.function.Function; import org.slf4j.Marker; public interface ContextDecider extends Function, ContextAwareTurboFilterDecider { @Override default FilterReply decide( ContextAwareTurboMarker marker, C context, Marker rootMarker, Logger logger, Level level, String format, Object[] params, Throwable t) { return apply(context); } } ================================================ FILE: logback-turbomarker/src/main/java/com/tersesystems/logback/turbomarker/LoggerContextDecider.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.turbomarker; import ch.qos.logback.classic.Level; import ch.qos.logback.classic.Logger; import ch.qos.logback.core.spi.FilterReply; import java.util.function.BiFunction; import org.slf4j.Marker; @FunctionalInterface public interface LoggerContextDecider extends BiFunction, ContextAwareTurboFilterDecider { default FilterReply decide( ContextAwareTurboMarker marker, C context, Marker rootMarker, Logger logger, Level level, String format, Object[] params, Throwable t) { return apply(logger, context); } } ================================================ FILE: logback-turbomarker/src/main/java/com/tersesystems/logback/turbomarker/MarkerContextDecider.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.turbomarker; import ch.qos.logback.classic.Level; import ch.qos.logback.classic.Logger; import ch.qos.logback.core.spi.FilterReply; import java.util.function.BiFunction; import org.slf4j.Marker; @FunctionalInterface public interface MarkerContextDecider extends BiFunction, C, FilterReply>, ContextAwareTurboFilterDecider { @Override default FilterReply decide( ContextAwareTurboMarker marker, C context, Marker rootMarker, Logger logger, Level level, String format, Object[] params, Throwable t) { return apply(marker, context); } } ================================================ FILE: logback-turbomarker/src/main/java/com/tersesystems/logback/turbomarker/TurboMarker.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.turbomarker; import com.tersesystems.logback.classic.TerseBasicMarker; import com.tersesystems.logback.classic.TurboFilterDecider; /** * This class is a marker that can test to see whether an event should be allowed through a turbo * filter. */ public abstract class TurboMarker extends TerseBasicMarker implements TurboFilterDecider { public TurboMarker(String name) { super(name); } } ================================================ FILE: logback-turbomarker/src/main/java/com/tersesystems/logback/turbomarker/TurboMarkerTurboFilter.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.turbomarker; import static java.util.Objects.requireNonNull; import ch.qos.logback.classic.Level; import ch.qos.logback.classic.Logger; import ch.qos.logback.classic.turbo.TurboFilter; import ch.qos.logback.core.spi.FilterReply; import com.tersesystems.logback.classic.TurboFilterDecider; import java.util.Spliterator; import java.util.Spliterators; import java.util.stream.Stream; import java.util.stream.StreamSupport; import org.slf4j.Marker; /** * This class is a turbo filter that hands off the evaluation of whether a logging event should be * created to the marker, if it is a predicate marker. */ public class TurboMarkerTurboFilter extends TurboFilter implements TurboFilterDecider { @Override public FilterReply decide( Marker rootMarker, Logger logger, Level level, String format, Object[] params, Throwable t) { if (!isStarted()) { return FilterReply.NEUTRAL; } if (rootMarker == null) { return FilterReply.NEUTRAL; } if (evaluateMarker(rootMarker, rootMarker, logger, level, format, params, t) == FilterReply.ACCEPT) { return FilterReply.ACCEPT; } return stream(rootMarker) .map(m -> evaluateMarker(m, rootMarker, logger, level, format, params, t)) .filter(reply -> reply != FilterReply.NEUTRAL) .findFirst() .orElse(FilterReply.NEUTRAL); } private FilterReply evaluateMarker( Marker marker, Marker rootMarker, Logger logger, Level level, String format, Object[] params, Throwable t) { if (marker instanceof TurboFilterDecider) { TurboFilterDecider decider = (TurboFilterDecider) marker; return decider.decide(rootMarker, logger, level, format, params, t); } return FilterReply.NEUTRAL; } @SuppressWarnings("unchecked") private Stream stream(Marker marker) { requireNonNull(marker); Spliterator spliterator = Spliterators.spliteratorUnknownSize(marker.iterator(), 0); return (Stream) StreamSupport.stream(spliterator, false); } } ================================================ FILE: logback-turbomarker/src/test/java/com/tersesystems/logback/turbomarker/ApplicationContext.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.turbomarker; public class ApplicationContext { private final String userId; public ApplicationContext(String userId) { this.userId = userId; } public String currentUserId() { return userId; } } ================================================ FILE: logback-turbomarker/src/test/java/com/tersesystems/logback/turbomarker/DiagnosticLoggingExample.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.turbomarker; import static java.util.Collections.singletonList; import static net.logstash.logback.argument.StructuredArguments.kv; import com.fasterxml.jackson.annotation.JsonProperty; import com.launchdarkly.client.LDClient; import com.launchdarkly.client.LDClientInterface; import com.launchdarkly.client.LDUser; import com.typesafe.config.Config; import com.typesafe.config.ConfigFactory; import net.logstash.logback.argument.StructuredArgument; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.slf4j.Marker; public class DiagnosticLoggingExample { public static void main(String[] args) { Config config = ConfigFactory.load(); LDClientInterface client = new LDClient(config.getString("launchdarkly.sdkkey")); Logger logger = LoggerFactory.getLogger(Order.class); LDMarkerFactory markerFactory = new LDMarkerFactory(client); LDUser ldUser = new LDUser.Builder("UNIQUE IDENTIFIER") .firstName("Bob") .lastName("Loblaw") .customString("groups", singletonList("beta_testers")) .build(); Marker marker = markerFactory.create("diagnostics-order", ldUser); OrderDiagnosticLogging diagnostics = new OrderDiagnosticLogging(logger, marker); Order order = new Order("id1337", diagnostics); order.addToCart(new LineItem()); order.addPayment(new Payment()); order.addShipping(new Shipping()); order.checkout(); order.fulfill(); order.complete(); } static class Order { private final OrderDiagnosticLogging diagnostics; @JsonProperty("id") // Make available to logstash-logback-encoder private final String id; public Order(String id, OrderDiagnosticLogging diagnostics) { this.id = id; this.diagnostics = diagnostics; } public String getId() { return id; } public void addToCart(LineItem lineItem) { diagnostics.reportAddToCart(this, lineItem); } public void addPayment(Payment payment) { diagnostics.reportAddPayment(this, payment); } public void addShipping(Shipping shipping) { diagnostics.reportAddShipping(this, shipping); } public void checkout() { diagnostics.reportCheckout(this); } public void fulfill() { diagnostics.reportFulfill(this); } public void complete() { diagnostics.reportComplete(this); } @Override public String toString() { return String.format("Order(id = %s)", id); } } static class OrderDiagnosticLogging { private final Logger logger; private final Marker marker; OrderDiagnosticLogging(Logger logger, Marker marker) { this.logger = logger; this.marker = marker; } void reportAddToCart(Order order, LineItem lineItem) { reportArg("addToCart", order, kv("lineItem", lineItem)); } void reportAddPayment(Order order, Payment payment) { reportArg("addPayment", order, kv("payment", payment)); } void reportAddShipping(Order order, Shipping shipping) { reportArg("addShipping", order, kv("shipping", shipping)); } void reportCheckout(Order order) { report("checkout", order); } void reportFulfill(Order order) { report("fulfill", order); } void reportComplete(Order order) { report("fulfill", order); } private void reportArg(String methodName, Order order, StructuredArgument arg) { if (logger.isDebugEnabled(marker)) { logger.debug(marker, "{}: {}, {}", kv("method", methodName), kv("order", order), arg); } } private void report(String methodName, Order order) { if (logger.isDebugEnabled(marker)) { logger.debug(marker, "{}: {}", kv("method", methodName), kv("order", order)); } } } private static class Payment { @Override public String toString() { return "Payment()"; } } private static class Shipping { @Override public String toString() { return "Shipping()"; } } private static class LineItem { @Override public String toString() { return "LineItem()"; } } } ================================================ FILE: logback-turbomarker/src/test/java/com/tersesystems/logback/turbomarker/LDMarkerFactory.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.turbomarker; import static java.util.Objects.requireNonNull; import ch.qos.logback.core.spi.FilterReply; import com.launchdarkly.client.LDClientInterface; import com.launchdarkly.client.LDUser; public class LDMarkerFactory { private final LaunchDarklyDecider decider; public LDMarkerFactory(LDClientInterface client) { this.decider = new LaunchDarklyDecider(requireNonNull(client)); } public LDMarker create(String featureFlag, LDUser user) { return new LDMarker(featureFlag, user, decider); } static class LaunchDarklyDecider implements MarkerContextDecider { private final LDClientInterface ldClient; LaunchDarklyDecider(LDClientInterface ldClient) { this.ldClient = ldClient; } @Override public FilterReply apply(ContextAwareTurboMarker marker, LDUser ldUser) { return ldClient.boolVariation(marker.getName(), ldUser, false) ? FilterReply.ACCEPT : FilterReply.NEUTRAL; } } public static class LDMarker extends ContextAwareTurboMarker { LDMarker(String name, LDUser context, ContextAwareTurboFilterDecider decider) { super(name, context, decider); } } } ================================================ FILE: logback-turbomarker/src/test/java/com/tersesystems/logback/turbomarker/LDMarkerTest.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.turbomarker; import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.when; import ch.qos.logback.classic.LoggerContext; import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.core.read.ListAppender; import com.launchdarkly.client.LDClientInterface; import com.launchdarkly.client.LDUser; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.mockito.Mockito; import org.slf4j.LoggerFactory; public class LDMarkerTest { @Test @DisplayName("Matching Marker") public void testMatchingMarker() { LDClientInterface client = Mockito.mock(LDClientInterface.class); when(client.boolVariation(anyString(), any(), anyBoolean())).thenReturn(true); LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory(); ch.qos.logback.classic.Logger logger = loggerContext.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME); LDMarkerFactory markerFactory = new LDMarkerFactory(client); LDUser ldUser = new LDUser.Builder("UNIQUE IDENTIFIER") .firstName("Bob") .lastName("Loblaw") .customString("groups", singletonList("beta_testers")) .build(); // Register the user if not already seen client.identify(ldUser); LDMarkerFactory.LDMarker ldMarker = markerFactory.create("turbomarker", ldUser); logger.info(ldMarker, "Hello world, I am info"); logger.debug(ldMarker, "Hello world, I am debug"); ListAppender appender = (ListAppender) logger.getAppender("LIST"); assertThat(appender.list.size()).isEqualTo(2); appender.list.clear(); } @Test @DisplayName("Non Matching Marker") public void testNonMatchingUserMarker() { LDClientInterface client = Mockito.mock(LDClientInterface.class); when(client.boolVariation(anyString(), any(), anyBoolean())).thenReturn(false); LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory(); ch.qos.logback.classic.Logger logger = loggerContext.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME); LDMarkerFactory markerFactory = new LDMarkerFactory(client); LDUser ldUser = new LDUser.Builder("NON_MATCHING").firstName("Not").lastName("Beta").build(); // Register the user if not already seen client.identify(ldUser); LDMarkerFactory.LDMarker ldMarker = markerFactory.create("turbomarker", ldUser); logger.info(ldMarker, "Hello world, I am info"); logger.debug(ldMarker, "Hello world, I am debug"); ListAppender appender = (ListAppender) logger.getAppender("LIST"); assertThat(appender.list.size()).isEqualTo(0); appender.list.clear(); } } ================================================ FILE: logback-turbomarker/src/test/java/com/tersesystems/logback/turbomarker/UserMarker.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.turbomarker; public class UserMarker extends ContextAwareTurboMarker { public UserMarker( String name, ApplicationContext applicationContext, ContextAwareTurboFilterDecider decider) { super(name, applicationContext, decider); } } ================================================ FILE: logback-turbomarker/src/test/java/com/tersesystems/logback/turbomarker/UserMarkerFactory.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.turbomarker; import ch.qos.logback.core.spi.FilterReply; import java.util.Set; import java.util.concurrent.ConcurrentSkipListSet; public class UserMarkerFactory { private final Set userIdSet = new ConcurrentSkipListSet<>(); private final ContextDecider decider = context -> userIdSet.contains(context.currentUserId()) ? FilterReply.ACCEPT : FilterReply.NEUTRAL; public void addUserId(String userId) { userIdSet.add(userId); } public void clear() { userIdSet.clear(); } public UserMarker create(ApplicationContext applicationContext) { return new UserMarker("userMarker", applicationContext, decider); } } ================================================ FILE: logback-turbomarker/src/test/java/com/tersesystems/logback/turbomarker/UserMarkerTest.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.turbomarker; import static org.assertj.core.api.Assertions.assertThat; import ch.qos.logback.classic.LoggerContext; import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.core.joran.spi.JoranException; import ch.qos.logback.core.read.ListAppender; import org.junit.Test; import org.slf4j.LoggerFactory; public class UserMarkerTest { @Test public void testMatchingUserMarker() throws JoranException { LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory(); ch.qos.logback.classic.Logger logger = loggerContext.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME); String userId = "28"; ApplicationContext applicationContext = new ApplicationContext(userId); UserMarkerFactory userMarkerFactory = new UserMarkerFactory(); userMarkerFactory.addUserId(userId); // say we want logging events created for this user id UserMarker userMarker = userMarkerFactory.create(applicationContext); logger.info(userMarker, "Hello world, I am info"); logger.debug(userMarker, "Hello world, I am debug"); ListAppender appender = (ListAppender) logger.getAppender("LIST"); assertThat(appender.list.size()).isEqualTo(2); appender.list.clear(); } @Test public void testNonMatchingUserMarker() throws JoranException { LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory(); ch.qos.logback.classic.Logger logger = loggerContext.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME); String userId = "28"; ApplicationContext applicationContext = new ApplicationContext(userId); UserMarkerFactory userMarkerFactory = new UserMarkerFactory(); UserMarker userMatchMarker = userMarkerFactory.create(applicationContext); logger.info(userMatchMarker, "Hello world, I am info"); logger.debug(userMatchMarker, "Hello world, I am debug"); ListAppender appender = (ListAppender) logger.getAppender("LIST"); assertThat(appender.list.size()).isEqualTo(0); appender.list.clear(); } } ================================================ FILE: logback-turbomarker/src/test/resources/logback-test.xml ================================================ ================================================ FILE: logback-typesafe-config/gradle.properties ================================================ # # SPDX-License-Identifier: CC0-1.0 # # Copyright 2018-2020 Will Sargent. # # Licensed under the CC0 Public Domain Dedication; # You may obtain a copy of the License at # # http://creativecommons.org/publicdomain/zero/1.0/ # project_description = Logback using Typesafe Config for configuration ================================================ FILE: logback-typesafe-config/logback-typesafe-config.gradle ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ plugins { id 'java-library' } dependencies { api "ch.qos.logback:logback-classic:$logbackVersion" api "com.typesafe:config:$configVersion" } ================================================ FILE: logback-typesafe-config/src/main/java/com/tersesystems/logback/typesafeconfig/ConfigConstants.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.typesafeconfig; /** Constants having to do with typesafe config. */ public final class ConfigConstants { public static final String TYPESAFE_CONFIG_CTX_KEY = "typesafeConfig"; public static final String LEVELS_KEY = "levels"; public static final String LOGBACK = "logback"; public static final String LOGBACK_TEST = "logback-test"; public static final String LOGBACK_REFERENCE_CONF = "logback-reference.conf"; public static final String CONFIG_FILE_PROPERTY = "terse.logback.configurationFile"; public static final String CONTEXT_SCOPE = "context"; public static final String LOCAL_SCOPE = "local"; public static final String PATH_ATTRIBUTE = "path"; public static final String DEBUG_ATTRIBUTE = "debug"; } ================================================ FILE: logback-typesafe-config/src/main/java/com/tersesystems/logback/typesafeconfig/ConfigConversion.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.typesafeconfig; import ch.qos.logback.core.spi.ContextAware; import com.typesafe.config.Config; import com.typesafe.config.ConfigException; import com.typesafe.config.ConfigValue; import java.util.HashMap; import java.util.Map; import java.util.Set; public interface ConfigConversion extends ContextAware { default Map configAsMap(Config levelsConfig) { Map levelsMap = new HashMap<>(); Set> levelsEntrySet = levelsConfig.entrySet(); for (Map.Entry entry : levelsEntrySet) { String name = entry.getKey(); try { String levelFromConfig = entry.getValue().unwrapped().toString(); levelsMap.put(name, levelFromConfig); } catch (ConfigException.Missing e) { addInfo("No custom setting found for " + name + " in config, ignoring"); } catch (Exception e) { addError("Unexpected exception resolving " + name, e); } } return levelsMap; } } ================================================ FILE: logback-typesafe-config/src/main/java/com/tersesystems/logback/typesafeconfig/ConfigListConverter.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.typesafeconfig; import ch.qos.logback.classic.pattern.ClassicConverter; import ch.qos.logback.classic.spi.ILoggingEvent; import com.typesafe.config.*; import java.util.List; /** * Queries a list in typesafe config by specifying the full path and index. * *

This is a means of working around #30. * *

You must have a typesafe config in context, usually through typesafeConfigAction. * *

{@code
 * 
 * }
* * And then define the option list in the layout as the path and the index: * *
{@code
 * %configList{some.property.array,2}
 * }
*/ public class ConfigListConverter extends ClassicConverter { @Override public String convert(ILoggingEvent event) { List options = getOptionList(); String path = options.get(0); String index = options.get(1); try { Config config = (Config) getContext().getObject(ConfigConstants.TYPESAFE_CONFIG_CTX_KEY); if (path == null) { addError("No option found - you must specify property as %config{some.property.array,0} "); return "%PARSER_ERROR"; } ConfigList configList = config.getList(path); ConfigValue configValue = configList.get(Integer.parseInt(index)); return configValue.unwrapped().toString(); } catch (ConfigException e) { addError( String.format( "Exception rendering path %s, index %s, exception %s", path, index, e.getMessage())); return "%PARSER_ERROR"; } } } ================================================ FILE: logback-typesafe-config/src/main/java/com/tersesystems/logback/typesafeconfig/TypesafeConfigAction.java ================================================ /* * SPDX-License-Identifier: CC0-1.0 * * Copyright 2018-2020 Will Sargent. * * Licensed under the CC0 Public Domain Dedication; * You may obtain a copy of the License at * * http://creativecommons.org/publicdomain/zero/1.0/ */ package com.tersesystems.logback.typesafeconfig; import static com.tersesystems.logback.typesafeconfig.ConfigConstants.*; import ch.qos.logback.core.Context; import ch.qos.logback.core.joran.action.Action; import ch.qos.logback.core.joran.action.ActionUtil; import ch.qos.logback.core.joran.spi.ActionException; import ch.qos.logback.core.joran.spi.ElementSelector; import ch.qos.logback.core.joran.spi.InterpretationContext; import ch.qos.logback.core.joran.spi.RuleStore; import ch.qos.logback.core.util.OptionHelper; import com.typesafe.config.*; import java.io.File; import java.util.Map; import java.util.Set; import org.xml.sax.Attributes; /** * This class reads in configuration from a series of files using Typesafe Config, an easy to * use configuration library. * *

A property is resolved in the following resources in order of priority. If there is no setting * found, it will fall back to the next available resource, which is * *

    *
  • System Properties *
  • -Dterse.logback.configurationFile=somefile.conf *
  • logback.conf *
  • logback-test.conf *
  • logback-reference.conf *
* * The configuration will be available in the LoggerContext's object map, so you can use it from * your own code. * *
{@code
 * LoggerContext context = (LoggerContext) org.slf4j.LoggerFactory.getILoggerFactory();
 * com.typesafe.config.Config config = (com.typesafe.config.Config) context.getObject("config");
 * }
* * This action will also set up the levels for a "setLoggingLevelsAction" used in terse-logback * core. * *
{@code
 * context.putObject(LEVELS_KEY, levelsMap);
 * }
* * You may want to made subsections of config available to other actions and components. You can use * the {@code object} action for this. * *
{@code
 * 
 *   
 * 
 * }
 *
 * which will do a {@code context.putObject("contextObjectFoo", pathValue); }
 */
public class TypesafeConfigAction extends Action implements ConfigConversion {

  @Override
  public void begin(InterpretationContext ic, String name, Attributes attributes)
      throws ActionException {
    RuleStore ruleStore = ic.getJoranInterpreter().getRuleStore();

    ruleStore.addRule(
        new ElementSelector("configuration/" + name + "/object"), new ContextObjectAction());

    String debugAttr = attributes.getValue(DEBUG_ATTRIBUTE);

    Config config = generateConfig(ic.getClass().getClassLoader(), Boolean.valueOf(debugAttr));
    Context context = ic.getContext();

    context.putObject(TYPESAFE_CONFIG_CTX_KEY, config);
    ic.getObjectMap().put(TYPESAFE_CONFIG_CTX_KEY, config);
    configureLevels(config);

    try {
      Set> contextProperties =
          config.getConfig(CONTEXT_SCOPE).entrySet();
      configureContextScope(config, context, contextProperties);
    } catch (ConfigException.Missing e) {
      // do nothing
    }

    try {
      Set> localProperties =
          config.getConfig(LOCAL_SCOPE).entrySet();
      configureLocalScope(config, ic, localProperties);
    } catch (ConfigException.Missing e) {
      // do nothing
    }
  }

  protected void configureLevels(Config config) {
    // Try to set up the levels as they're important...
    try {
      Map levelsMap = configAsMap(config.getConfig(LEVELS_KEY));
      context.putObject(LEVELS_KEY, levelsMap);
    } catch (ConfigException e) {
      addWarn("Cannot set levels in context!", e);
    }
  }

  @Override
  public void end(InterpretationContext ic, String name) throws ActionException {}

  protected void configureContextScope(
      Config config, Context lc, Set> properties) {
    for (Map.Entry propertyEntry : properties) {
      String key = propertyEntry.getKey();
      String value = propertyEntry.getValue().unwrapped().toString();
      lc.putProperty(key, value);
    }
  }

  protected void configureLocalScope(
      Config config, InterpretationContext ic, Set> properties) {
    for (Map.Entry propertyEntry : properties) {
      String key = propertyEntry.getKey();
      String value = propertyEntry.getValue().unwrapped().toString();
      ic.addSubstitutionProperty(key, value);
    }
  }

  protected Config generateConfig(ClassLoader classLoader, boolean debug) {
    // Look for logback.json, logback.conf, logback.properties
    Config systemProperties = ConfigFactory.systemProperties();
    String fileName = System.getProperty(CONFIG_FILE_PROPERTY);
    Config file = ConfigFactory.empty();
    if (fileName != null) {
      file = ConfigFactory.parseFile(new File(fileName));
    }

    Config testResources = ConfigFactory.parseResourcesAnySyntax(classLoader, LOGBACK_TEST);
    Config resources = ConfigFactory.parseResourcesAnySyntax(classLoader, LOGBACK);
    Config reference = ConfigFactory.parseResources(classLoader, LOGBACK_REFERENCE_CONF);

    Config config =
        systemProperties // Look for a property from system properties first...
            .withFallback(file) // if we don't find it, then look in an explicitly defined file...
            .withFallback(
                testResources) // if not, then if logback-test.conf exists, look for it there...
            .withFallback(resources) // then look in logback.conf...
            .withFallback(reference) // and then finally in logback-reference.conf.
            .resolve(); // Tell config that we want to use ${?ENV_VAR} type stuff.

    // Add a check to show the config value if nothing is working...
    if (debug) {
      String configString = config.root().render(ConfigRenderOptions.defaults());
      addInfo(configString);
    }
    return config;
  }

  /**
   * Lets you put objects into the context's object map, as the correct type, using typesafe config
   * paths as the source.
   */
  public static class ContextObjectAction extends Action implements ConfigConversion {
    private String nameAttr;
    private String path;
    private ActionUtil.Scope scope;

    static ActionUtil.Scope stringToScope(String scopeStr) {
      if (ActionUtil.Scope.LOCAL.toString().equalsIgnoreCase(scopeStr))
        return ActionUtil.Scope.LOCAL;
      if (ActionUtil.Scope.CONTEXT.toString().equalsIgnoreCase(scopeStr))
        return ActionUtil.Scope.CONTEXT;

      // default to context.
      return ActionUtil.Scope.CONTEXT;
    }

    Config resolveConfig(InterpretationContext ic) {
      Config config = (Config) getContext().getObject(TYPESAFE_CONFIG_CTX_KEY);
      if (config == null) {
        config = (Config) ic.getObjectMap().get(TYPESAFE_CONFIG_CTX_KEY);
      }
      return config;
    }

    /**
     * Set a new property for the execution context by name, value pair, or adds all the properties
     * found in the given file.
     */
    public void begin(InterpretationContext ic, String localName, Attributes attributes) {
      String nameAttr = attributes.getValue(NAME_ATTRIBUTE);
      setNameAttr(nameAttr);
      String path = attributes.getValue(PATH_ATTRIBUTE);
      setPath(path);

      ActionUtil.Scope scope = stringToScope(attributes.getValue(SCOPE_ATTRIBUTE));
      setScope(scope);
    }

    boolean isValid(String name, String value) {
      return !(OptionHelper.isEmpty(name) || OptionHelper.isEmpty(value));
    }

    public void end(InterpretationContext ic, String name) {
      Config config = resolveConfig(ic);

      if (isValid(nameAttr, path)) {
        if (config == null) {
          addError("No config object found in context's object map!");
          return;
        }

        ConfigValue configValue = null;
        Object contextValue = null;
        try {
          configValue = config.getValue(path);
          if (configValue != null) {
            if (configValue.valueType().equals(ConfigValueType.OBJECT)) {
              String msg = "The value found at path %s is an object, assuming you want a map...";
              addInfo(String.format(msg, path));
              contextValue = configAsMap(config.getConfig(path));
            } else {
              contextValue = configValue.unwrapped();
            }
          }

          switch (scope) {
            case LOCAL:
              ic.getObjectMap().put(nameAttr, contextValue);
              break;
            case CONTEXT:
              context.putObject(nameAttr, contextValue);
              break;
            case SYSTEM:
              // never used.
              break;
          }

        } catch (ConfigException e) {
          addError(
              String.format(
                  "Cannot set value %s typesafe config path %s to name %s",
                  contextValue, path, nameAttr),
              e);
        }
      } else {
        addError("Cannot set property, it is invalid!");
      }
    }

    private String getNameAttr() {
      return this.nameAttr;
    }

    void setNameAttr(String name) {
      this.nameAttr = name;
    }

    public void setPath(String path) {
      this.path = path;
    }

    public void setScope(ActionUtil.Scope scope) {
      this.scope = scope;
    }
  }
}


================================================
FILE: logback-typesafe-config/src/test/java/com/tersesystems/logback/typesafeconfig/ConfigListConverterTest.java
================================================
/*
 * SPDX-License-Identifier: CC0-1.0
 *
 * Copyright 2018-2020 Will Sargent.
 *
 * Licensed under the CC0 Public Domain Dedication;
 * You may obtain a copy of the License at
 *
 *  http://creativecommons.org/publicdomain/zero/1.0/
 */
package com.tersesystems.logback.typesafeconfig;

import static com.tersesystems.logback.typesafeconfig.ConfigConstants.TYPESAFE_CONFIG_CTX_KEY;
import static org.assertj.core.api.Assertions.assertThat;

import ch.qos.logback.classic.LoggerContext;
import com.typesafe.config.Config;
import com.typesafe.config.ConfigFactory;
import java.util.Arrays;
import org.junit.Test;

public class ConfigListConverterTest {

  @Test
  public void testConversion() {
    LoggerContext context = new LoggerContext();
    ConfigListConverter configValueConverter = new ConfigListConverter();

    Config config = ConfigFactory.parseString("some.property.name=[one,two,three]");
    context.putObject(TYPESAFE_CONFIG_CTX_KEY, config);

    configValueConverter.setContext(context);
    configValueConverter.setOptionList(Arrays.asList("some.property.name", "1"));
    configValueConverter.start();

    String actual = configValueConverter.convert(null);

    assertThat(actual).isEqualTo("two");
  }
}


================================================
FILE: logback-typesafe-config/src/test/java/com/tersesystems/logback/typesafeconfig/TypesafeConfigActionTest.java
================================================
/*
 * SPDX-License-Identifier: CC0-1.0
 *
 * Copyright 2018-2020 Will Sargent.
 *
 * Licensed under the CC0 Public Domain Dedication;
 * You may obtain a copy of the License at
 *
 *  http://creativecommons.org/publicdomain/zero/1.0/
 */
package com.tersesystems.logback.typesafeconfig;

import static com.tersesystems.logback.typesafeconfig.ConfigConstants.LEVELS_KEY;
import static com.tersesystems.logback.typesafeconfig.ConfigConstants.TYPESAFE_CONFIG_CTX_KEY;
import static java.util.Objects.requireNonNull;
import static org.assertj.core.api.Assertions.assertThat;

import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.joran.JoranConfigurator;
import ch.qos.logback.core.joran.spi.JoranException;
import com.typesafe.config.Config;
import org.junit.Test;

public class TypesafeConfigActionTest {

  @Test
  public void testConfigWithDefault() throws JoranException {
    LoggerContext loggerContext = new LoggerContext();
    JoranConfigurator jc = new JoranConfigurator();
    jc.setContext(loggerContext);
    jc.doConfigure(
        requireNonNull(
            this.getClass()
                .getClassLoader()
                .getResource("typesafeconfig/config-with-default.xml")));

    Object levels = loggerContext.getObject(LEVELS_KEY);
    assertThat(levels).isNotNull();

    Config config = (Config) loggerContext.getObject(TYPESAFE_CONFIG_CTX_KEY);
    assertThat(config).isNotNull();

    String exportedToContext = loggerContext.getProperty("localKey");
    assertThat(exportedToContext).isNull();

    String exportedFoo = loggerContext.getProperty("exportedFoo");
    assertThat(exportedFoo).isEqualTo("bar");
  }

  @Test
  public void testConfigWithContext() throws JoranException {
    LoggerContext loggerContext = new LoggerContext();
    JoranConfigurator jc = new JoranConfigurator();
    jc.setContext(loggerContext);
    jc.doConfigure(
        requireNonNull(
            this.getClass()
                .getClassLoader()
                .getResource("typesafeconfig/config-with-context.xml")));

    Object levels = loggerContext.getObject(LEVELS_KEY);
    assertThat(levels).isNotNull();

    Config config = (Config) loggerContext.getObject(TYPESAFE_CONFIG_CTX_KEY);
    assertThat(config).isNotNull();

    String foo = loggerContext.getProperty("contextKey");
    assertThat(foo).isEqualTo("bar");

    String exportedFoo = loggerContext.getProperty("exportedFoo");
    assertThat(exportedFoo).isEqualTo("bar");
  }

  @Test
  public void testConfigWithLocal() throws JoranException {
    LoggerContext loggerContext = new LoggerContext();
    JoranConfigurator jc = new JoranConfigurator();
    jc.setContext(loggerContext);
    jc.doConfigure(
        requireNonNull(
            this.getClass().getClassLoader().getResource("typesafeconfig/config-with-local.xml")));

    Object levels = loggerContext.getObject(LEVELS_KEY);
    assertThat(levels).isNotNull();

    Config config = (Config) loggerContext.getObject(TYPESAFE_CONFIG_CTX_KEY);
    assertThat(config).isNotNull();

    String foo = loggerContext.getProperty("localKey");
    assertThat(foo).isNull();

    String exportedFoo = loggerContext.getProperty("exportedFoo");
    assertThat(exportedFoo).isEqualTo("bar");

    Object contextObjectFoo = loggerContext.getObject("contextObjectFoo");
    assertThat(contextObjectFoo).isEqualTo("pathValue");
  }
}


================================================
FILE: logback-typesafe-config/src/test/resources/logback-test.conf
================================================
levels {
  examples = "INFO"
}

some.random.path = "pathValue"

local {
  localKey = "bar"
}

context {
  contextKey = "bar"
}


================================================
FILE: logback-typesafe-config/src/test/resources/typesafeconfig/config-with-context.xml
================================================



    

    
        
    

    

    
        
            [%-5level] %logger{15} - %msg%n%xException{10}
        
    

    
        
    


================================================
FILE: logback-typesafe-config/src/test/resources/typesafeconfig/config-with-default.xml
================================================



    

    
        
    

    

    
        
            [%-5level] %logger{15} - %msg%n%xException{10}
        
    

    
        
    


================================================
FILE: logback-typesafe-config/src/test/resources/typesafeconfig/config-with-local.xml
================================================



    

    
        
    

    

    
        
            [%-5level] %logger{15} - %msg%n%xException{10}
        
    

    
        
    


================================================
FILE: logback-uniqueid-appender/gradle.properties
================================================
#
# SPDX-License-Identifier: CC0-1.0
#
# Copyright 2018-2020 Will Sargent.
#
# Licensed under the CC0 Public Domain Dedication;
# You may obtain a copy of the License at
#
#  http://creativecommons.org/publicdomain/zero/1.0/
#

project_description = Logback Unique ID Appender

================================================
FILE: logback-uniqueid-appender/logback-uniqueid-appender.gradle
================================================
/*
 * SPDX-License-Identifier: CC0-1.0
 *
 * Copyright 2018-2020 Will Sargent.
 *
 * Licensed under the CC0 Public Domain Dedication;
 * You may obtain a copy of the License at
 *
 *  http://creativecommons.org/publicdomain/zero/1.0/
 */
dependencies {
    implementation project(':logback-classic')

    // https://github.com/f4b6a3/ulid-creator
    implementation 'com.github.f4b6a3:ulid-creator:5.1.0'

    // https://github.com/f4b6a3/tsid-creator
    implementation 'com.github.f4b6a3:tsid-creator:5.2.0'

    // https://github.com/f4b6a3/uuid-creator
    implementation 'com.github.f4b6a3:uuid-creator:5.2.0'

    // https://github.com/f4b6a3/ksuid-creator
    implementation 'com.github.f4b6a3:ksuid-creator:4.1.0'

    // https://github.com/mguenther/idem
    implementation 'net.mguenther.idem:idem-core:0.1.0'
}


================================================
FILE: logback-uniqueid-appender/src/main/java/com/tersesystems/logback/uniqueid/FlakeIdGenerator.java
================================================
/*
 * SPDX-License-Identifier: CC0-1.0
 *
 * Copyright 2018-2020 Will Sargent.
 *
 * Licensed under the CC0 Public Domain Dedication;
 * You may obtain a copy of the License at
 *
 *  http://creativecommons.org/publicdomain/zero/1.0/
 */
package com.tersesystems.logback.uniqueid;

import net.mguenther.idem.flake.Flake128S;
import net.mguenther.idem.provider.LinearTimeProvider;
import net.mguenther.idem.provider.MacAddressWorkerIdProvider;

/**
 * This class generates a 128 bit flake id with a macaddress workerid according to https://github.com/mguenther/idem.
 */
public class FlakeIdGenerator implements IdGenerator {

  private static final Flake128S flake64 =
      new Flake128S(new LinearTimeProvider(), new MacAddressWorkerIdProvider());

  @Override
  public String generateId() {
    return flake64.nextId();
  }
}


================================================
FILE: logback-uniqueid-appender/src/main/java/com/tersesystems/logback/uniqueid/IdGenerator.java
================================================
/*
 * SPDX-License-Identifier: CC0-1.0
 *
 * Copyright 2018-2020 Will Sargent.
 *
 * Licensed under the CC0 Public Domain Dedication;
 * You may obtain a copy of the License at
 *
 *  http://creativecommons.org/publicdomain/zero/1.0/
 */
package com.tersesystems.logback.uniqueid;

public interface IdGenerator {
  String generateId();
}


================================================
FILE: logback-uniqueid-appender/src/main/java/com/tersesystems/logback/uniqueid/KsuidSubsecondIdGenerator.java
================================================
package com.tersesystems.logback.uniqueid;

import com.github.f4b6a3.ksuid.*;
import java.util.Random;
import java.util.concurrent.ThreadLocalRandom;

/**
 * Creates a subsecond KSUID according to https://github.com/f4b6a3/ksuid-creator.
 */
public class KsuidSubsecondIdGenerator implements IdGenerator {

  private Random random() {
    return ThreadLocalRandom.current();
  }

  private final KsuidFactory factory = KsuidFactory.newSubsecondInstance(() -> random().nextLong());

  @Override
  public String generateId() {
    return factory.create().toString();
  }
}


================================================
FILE: logback-uniqueid-appender/src/main/java/com/tersesystems/logback/uniqueid/RandomUUIDIdGenerator.java
================================================
/*
 * SPDX-License-Identifier: CC0-1.0
 *
 * Copyright 2018-2020 Will Sargent.
 *
 * Licensed under the CC0 Public Domain Dedication;
 * You may obtain a copy of the License at
 *
 *  http://creativecommons.org/publicdomain/zero/1.0/
 */
package com.tersesystems.logback.uniqueid;

import com.github.f4b6a3.uuid.factory.rfc4122.RandomBasedFactory;
import java.util.Random;
import java.util.concurrent.ThreadLocalRandom;

/**
 * Generates a Random UUIDv4 using a ThreadLocalRandom from https://github.com/f4b6a3/uuid-creator
 */
public class RandomUUIDIdGenerator implements IdGenerator {
  private Random random() {
    return ThreadLocalRandom.current();
  }

  private final RandomBasedFactory factory = new RandomBasedFactory(() -> random().nextLong());

  @Override
  public String generateId() {
    return factory.create().toString();
  }
}


================================================
FILE: logback-uniqueid-appender/src/main/java/com/tersesystems/logback/uniqueid/TsidIdgenerator.java
================================================
package com.tersesystems.logback.uniqueid;

import com.github.f4b6a3.tsid.TsidFactory;

/**
 * Generates a TSID according to https://github.com/f4b6a3/tsid-creator.
 */
public class TsidIdgenerator implements IdGenerator {

  // "tsidcreator.node" system property should be set,
  // but small hope of that happening, so choose a large node count.
  private final TsidFactory factory = TsidFactory.newInstance4096();

  @Override
  public String generateId() {
    return factory.create().toString();
  }
}


================================================
FILE: logback-uniqueid-appender/src/main/java/com/tersesystems/logback/uniqueid/UlidIdGenerator.java
================================================
package com.tersesystems.logback.uniqueid;

import com.github.f4b6a3.ulid.UlidFactory;
import java.util.Random;
import java.util.concurrent.ThreadLocalRandom;

/**
 * Creates a monotonic ULID using a threadlocal random according to https://github.com/f4b6a3/ulid-creator.
 */
public class UlidIdGenerator implements IdGenerator {

  private Random random() {
    return ThreadLocalRandom.current();
  }

  private final UlidFactory factory = UlidFactory.newMonotonicInstance(() -> random().nextLong());

  @Override
  public String generateId() {
    return factory.create().toString();
  }
}


================================================
FILE: logback-uniqueid-appender/src/main/java/com/tersesystems/logback/uniqueid/UniqueIdComponentAppender.java
================================================
/*
 * SPDX-License-Identifier: CC0-1.0
 *
 * Copyright 2018-2020 Will Sargent.
 *
 * Licensed under the CC0 Public Domain Dedication;
 * You may obtain a copy of the License at
 *
 *  http://creativecommons.org/publicdomain/zero/1.0/
 */
package com.tersesystems.logback.uniqueid;

import ch.qos.logback.classic.spi.ILoggingEvent;
import com.tersesystems.logback.classic.ContainerProxyLoggingEvent;
import com.tersesystems.logback.classic.IContainerLoggingEvent;
import com.tersesystems.logback.core.DecoratingAppender;

public class UniqueIdComponentAppender
    extends DecoratingAppender {

  private IdGenerator idGenerator = new FlakeIdGenerator();

  public IdGenerator getIdGenerator() {
    return idGenerator;
  }

  public void setIdGenerator(IdGenerator idGenerator) {
    this.idGenerator = idGenerator;
  }

  @Override
  protected IContainerLoggingEvent decorateEvent(ILoggingEvent eventObject) {
    IContainerLoggingEvent containerEvent;
    if (eventObject instanceof IContainerLoggingEvent) {
      containerEvent = (IContainerLoggingEvent) eventObject;
    } else {
      containerEvent = new ContainerProxyLoggingEvent(eventObject);
    }
    String uniqueId = idGenerator.generateId();
    containerEvent.putComponent(UniqueIdProvider.class, () -> uniqueId);
    return containerEvent;
  }
}


================================================
FILE: logback-uniqueid-appender/src/main/java/com/tersesystems/logback/uniqueid/UniqueIdConverter.java
================================================
/*
 * SPDX-License-Identifier: CC0-1.0
 *
 * Copyright 2018-2020 Will Sargent.
 *
 * Licensed under the CC0 Public Domain Dedication;
 * You may obtain a copy of the License at
 *
 *  http://creativecommons.org/publicdomain/zero/1.0/
 */
package com.tersesystems.logback.uniqueid;

import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.pattern.DynamicConverter;
import com.tersesystems.logback.core.ComponentContainer;

public class UniqueIdConverter extends DynamicConverter {
  @Override
  public String convert(ILoggingEvent event) {
    if (event instanceof ComponentContainer) {
      return ((ComponentContainer) event).getComponent(UniqueIdProvider.class).uniqueId();
    } else {
      return null;
    }
  }
}


================================================
FILE: logback-uniqueid-appender/src/main/java/com/tersesystems/logback/uniqueid/UniqueIdProvider.java
================================================
/*
 * SPDX-License-Identifier: CC0-1.0
 *
 * Copyright 2018-2020 Will Sargent.
 *
 * Licensed under the CC0 Public Domain Dedication;
 * You may obtain a copy of the License at
 *
 *  http://creativecommons.org/publicdomain/zero/1.0/
 */

package com.tersesystems.logback.uniqueid;

import com.tersesystems.logback.core.Component;

/** This interface returns a unique id identifying the entity. */
public interface UniqueIdProvider extends Component {
  String uniqueId();
}


================================================
FILE: logback-uniqueid-appender/src/test/java/com/tersesystems/logback/uniqueid/UniqueIdAppenderTest.java
================================================
/*
 * SPDX-License-Identifier: CC0-1.0
 *
 * Copyright 2018-2020 Will Sargent.
 *
 * Licensed under the CC0 Public Domain Dedication;
 * You may obtain a copy of the License at
 *
 *  http://creativecommons.org/publicdomain/zero/1.0/
 */
package com.tersesystems.logback.uniqueid;

import static org.assertj.core.api.Assertions.assertThat;

import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.joran.JoranConfigurator;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.joran.spi.JoranException;
import ch.qos.logback.core.read.ListAppender;
import com.tersesystems.logback.core.ComponentContainer;
import com.tersesystems.logback.core.DecoratingAppender;
import java.net.URL;
import org.junit.Test;

public class UniqueIdAppenderTest {

  @Test
  public void testUniqueIdEventAppender() throws JoranException {
    LoggerContext context = new LoggerContext();
    URL resource = getClass().getResource("/logback-with-uniqueid-appender.xml");
    JoranConfigurator configurator = new JoranConfigurator();
    configurator.setContext(context);
    configurator.doConfigure(resource);

    ch.qos.logback.classic.Logger logger = context.getLogger(Logger.ROOT_LOGGER_NAME);

    logger.info("hello world");
    DecoratingAppender appender =
        (DecoratingAppender)
            logger.getAppender("DECORATE_WITH_UNIQUEID");

    ListAppender listAppender =
        (ListAppender) appender.getAppender("LIST");
    ILoggingEvent event = listAppender.list.get(0);
    ComponentContainer container = (ComponentContainer) event;
    UniqueIdProvider idComponent = container.getComponent(UniqueIdProvider.class);
    assertThat(idComponent.uniqueId()).isNotBlank();
  }
}


================================================
FILE: logback-uniqueid-appender/src/test/resources/logback-with-uniqueid-appender.xml
================================================



    

    
        
        
        
         
        
            LIST
        

        
            
                %-5relative %-5level %uniqueId %logger{35} - %msg%n
            
        
    

    
        
    


================================================
FILE: mkdocs.yml
================================================
site_name: Terse Logback

# Meta tags (placed in header)
site_description: "Terse Logback Documentation"

site_author: "Will Sargent"
site_url: "https://tersesystems.github.io/terse-logback"

# Repository (add link to repository on each page)
repo_name: terse-logback

repo_url: "https://github.com/tersesystems/terse-logback"
edit_uri: "edit/master/docs/"

#Copyright (shown at the footer)
#copyright: 'Copyright © 2017 Your Name'

# Meterial theme
# https://github.com/squidfunk/mkdocs-material
# pip install mkdocs-material
theme: 'material'

extra:
  version:
    provider: mike

  palette:
    primary: 'indigo'
    accent: 'indigo'

  social:
    - icon: fontawesome/brands/mastodon
      link: 'https://mastodon.xyz/web/@will_sargent'


# Google Analytics
#google_analytics:
#  - 'UA-111111111-1'
#  - 'auto'

# Extensions
markdown_extensions:
  - admonition
  - codehilite:
      guess_lang: false
  - footnotes
  - meta
  - toc:
      permalink: true
  - pymdownx.betterem:
      smart_enable: all
  - pymdownx.caret
  - pymdownx.inlinehilite
  - pymdownx.magiclink
  - pymdownx.smartsymbols
  - pymdownx.superfences

nav:
  - Home: index.md
  - Modules:
      - Audio: guide/audio.md
      - Budgeting / Rate Limiting: guide/budget.md
      - Censors: guide/censor.md    
      - Composite: guide/composite.md
      - Compression: guide/compression.md
      - Correlation Id: guide/correlationid.md
      - Exception Mapping: guide/exception-mapping.md
      - Instrumentation: guide/instrumentation.md
      - JDBC: guide/jdbc.md
      - JUL to SLF4J Bridge: guide/slf4jbridge.md
      - Relative Nanos: guide/relativens.md
      - Select Appender: guide/select.md
      - Tracing with Honeycomb: guide/tracing.md
      - Typesafe Config: guide/typesafeconfig.md
      - Turbo Markers: guide/turbomarker.md
      - Unique ID Appender: guide/uniqueid.md


================================================
FILE: settings.gradle
================================================
/*
 * SPDX-License-Identifier: CC0-1.0
 *
 * Copyright 2018-2020 Will Sargent.
 *
 * Licensed under the CC0 Public Domain Dedication;
 * You may obtain a copy of the License at
 *
 *  http://creativecommons.org/publicdomain/zero/1.0/
 */
pluginManagement {
    repositories {
        jcenter()
        maven { url 'https://plugins.gradle.org/m2/' }
    }
}

rootProject.name = 'terse-logback'

def includeProject = { String projectName ->
    File projectDir = new File(settingsDir, projectName)
    String buildFileName = "${projectName}.gradle"

    assert projectDir.isDirectory()
    assert new File(projectDir, buildFileName).isFile()

    include projectName
    project(":${projectName}").projectDir    = projectDir
    project(":${projectName}").buildFileName = buildFileName
}

includeProject 'logback-bytebuddy'
includeProject 'logback-censor'
includeProject 'logback-core'
includeProject 'logback-correlationid'
includeProject 'logback-classic'
includeProject 'logback-typesafe-config'
includeProject 'logback-audio'
includeProject 'logback-compress-encoder'
includeProject 'logback-budget'
includeProject 'logback-uniqueid-appender'
includeProject 'logback-exception-mapping'
includeProject 'logback-exception-mapping-providers'
includeProject 'logback-turbomarker'
includeProject 'logback-tracing'
includeProject 'logback-jdbc-appender'
includeProject 'logback-honeycomb-client'
includeProject 'logback-honeycomb-appender'
includeProject 'logback-honeycomb-okhttp'
includeProject 'logback-postgresjson-appender'

//includeProject 'guide'


================================================
FILE: version.properties
================================================
#
# SPDX-License-Identifier: CC0-1.0
#
# Copyright 2018-2020 Will Sargent.
#
# Licensed under the CC0 Public Domain Dedication;
# You may obtain a copy of the License at
#
#  http://creativecommons.org/publicdomain/zero/1.0/
#

#Version of the produced binaries. This file is intended to be checked-in.
#It will be automatically bumped by release automation.
version=1.2.*