[
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**Describe the bug**\nA clear and concise description of what the bug is.\n\n**To Reproduce**\nSteps to reproduce the behavior:\n1. Go to '...'\n2. Click on '....'\n3. Scroll down to '....'\n4. See error\n\n**Expected behavior**\nA clear and concise description of what you expected to happen.\n\n**Screenshots**\nIf applicable, add screenshots to help explain your problem.\n\n**Desktop (please complete the following information):**\n - OS: [e.g. iOS]\n - Browser [e.g. chrome, safari]\n - Version [e.g. 22]\n\n**Smartphone (please complete the following information):**\n - Device: [e.g. iPhone6]\n - OS: [e.g. iOS8.1]\n - Browser [e.g. stock browser, safari]\n - Version [e.g. 22]\n\n**Additional context**\nAdd any other context about the problem here.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "content": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**Is your feature request related to a problem? Please describe.**\nA clear and concise description of what the problem is. Ex. I'm always frustrated when [...]\n\n**Describe the solution you'd like**\nA clear and concise description of what you want to happen.\n\n**Describe alternatives you've considered**\nA clear and concise description of any alternative solutions or features you've considered.\n\n**Additional context**\nAdd any other context or screenshots about the feature request here.\n"
  },
  {
    "path": ".gitignore",
    "content": "# Ignore Gradle project-specific cache directory\n.gradle\n\nlogback-sigar/native/\n\n# Ignore Gradle build output directory\nbuild\ntarget/\n\nsite/\n\n.idea\nlog/\n\nout/\nlogback-example/log/*\n\n.classpath\n.project\n.settings\n.vscode/\nbin/\n*.iml\n*.ipr\n*.iws\n"
  },
  {
    "path": ".java-version",
    "content": "1.8\n"
  },
  {
    "path": "LICENSE",
    "content": "License\n-------\nWritten in 2019 by Will Sargent will@tersesystems.com\nTo 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.\nYou should have received a copy of the CC0 Public Domain Dedication along with this software. If not, see <http://creativecommons.org/publicdomain/zero/1.0/>"
  },
  {
    "path": "README.md",
    "content": "[![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)\n\n# Terse Logback\n\nTerse 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. \n\nOther logging projects you may be interested in: \n\n* [Blacklite](https://github.com/tersesystems/blacklite/), an SQLite appender with memory-mapping and zstandard dictionary compression that clocks around 800K statements per second.\n* [Blindsight](https://github.com/tersesystems/blindsight), a Scala logging API that extends SLF4J.\n* [Echopraxia](https://github.com/tersesystems/echopraxia), a Java and Scala logging API built around structured logging.\n\n## Documentation\n\nDocumentation is available at [https://tersesystems.github.io/terse-logback](https://tersesystems.github.io/terse-logback/1.0.3).\n\n## Showcase\n\nThere is a showcase project at [https://github.com/tersesystems/terse-logback-showcase](https://github.com/tersesystems/terse-logback-showcase).\n\n## Modules\n\n- [Audio](https://tersesystems.github.io/terse-logback/guide/audio): Play audio when you log by attaching markers to your logging statements.\n- [Budgeting / Rate Limiting](https://tersesystems.github.io/terse-logback/guide/budget): Limit the amount of debugging or tracing statements in a time period.\n- [Censors](https://tersesystems.github.io/terse-logback/guide/censor): Censor sensitive information in logging statements.\n- [Composite](https://tersesystems.github.io/terse-logback/guide/composite): Presents a single appender that composes several appenders.\n- [Compression](https://tersesystems.github.io/terse-logback/guide/compression): Write to a compressed zstandard file.\n- [Correlation Id](https://tersesystems.github.io/terse-logback/guide/correlationid): Adds markers and filters for correlation id.\n- [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.\n- [Instrumentation](https://tersesystems.github.io/terse-logback/guide/instrumentation): Decorates any (including JVM) class with enter and exit logging statements at runtime.\n- [JDBC](https://tersesystems.github.io/terse-logback/guide/jdbc): Use Postgres JSON to write structured logging to a single table.\n- [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/).\n- [Relative Nanos](https://tersesystems.github.io/terse-logback/guide/relativens): Composes a logging event to contain relative nanoseconds based off `System.nanoTime`.\n- [Select Appender](https://tersesystems.github.io/terse-logback/guide/select): Appender that selects an appender from a list based on key.\n- [Tracing](https://tersesystems.github.io/terse-logback/guide/tracing): Sends logging events and traces to [Honeycomb Event API](https://docs.honeycomb.io/api/events/).\n- [Typesafe Config](https://tersesystems.github.io/terse-logback/guide/typesafeconfig): Configure Logback properties using [HOCON](https://github.com/lightbend/config/blob/main/HOCON.md).\n- [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.\n- [Unique ID Appender](https://tersesystems.github.io/terse-logback/guide/uniqueid): Composes logging event to contain a unique id across multiple appenders. "
  },
  {
    "path": "RELEASING.md",
    "content": "## Release\n\nTo make sure everything works:\n\n```bash\n./gradlew clean build check\n```\n\nTo format everything using [Spotless](https://github.com/diffplug/spotless/tree/master/plugin-gradle):\n\n```bash\n./gradlew spotlessApply\n```\n\nFirst, try publishing to maven local:\n\n```bash\n./gradlew publishToMavenLocal\n```\n\nIf that works, then publish to Sonatype's staging repository and close:\n\n```bash\n./gradlew publishToSonatype closeSonatypeStagingRepository\n```\n\nInspect this in Sonatype OHSSH repository.  Delete the staging repository after inspection.\n\nAnd then to promote it:\n\n```bash\n./gradlew publishToSonatype closeAndReleaseSonatypeStagingRepository\n```\n\nIf 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).\n\n## Gradle Signing\n\nIf you run into errors with signing doing a `publishToSonaType`, this is common and underdocumented.\n\n```\nNo value has been specified for property 'signatory.keyId'.\n```\n\nFor the `signatory.keyId` error message, you need to set `signing.gnupg.keyName` if you\nare using GPG 2.1 and a Yubikey 4.\n\nhttps://docs.gradle.org/current/userguide/signing_plugin.html#sec:signatory_credentials\nhttps://github.com/gradle/gradle/pull/1703/files#diff-6c52391bbdceb4cca64ce7b03e78212fR6\n\nNote you need to use `gpg -K` and pick only the LAST EIGHT CHARS of the public signing key.\n\n> signing.gnupg.keyName = 5F798D53\n\n### PinEntry\n\nAlso note that if you are using a Yubikey, it'll require you to type in a PIN, which screws up Gradle.\n\n```\ngpg: signing failed: No pinentry\n```\n\nSo you need to use pinentry-mode loopback, which is helpfully supplied by passphrase.\n\n- https://github.com/sbt/sbt-pgp/pull/142\n- https://wiki.archlinux.org/index.php/GnuPG#Unattended_passphrase\n- https://github.com/gradle/gradle/pull/1703/files#diff-790036df959521791fdafe474b673924\n\nYou want this specified only the command line, i.e.\n\n> $ HISTCONTROL=ignoreboth ./gradlew publishToMavenLocal -Psigning.gnupg.passphrase=$PGP_PASSPHRASE --info\n\n### Cannot Allocate Memory\n\ngpg can't be run in parallel.  You'll get this error message.\n\n```\ngpg: signing failed: Cannot allocate memory\n```\n[Gradle is not smart enough to disable this](https://github.com/gradle/gradle/issues/12167). \n\nDo not use `-Porg.gradle.parallel=false` and don't use `--parallel` when publishing.\n\n## Documentation\n\nDocumentation is done with [gradle-mkdocs-plugin](https://xvik.github.io/gradle-mkdocs-plugin/) and works best on Linux.\n\nNeed to have [Python 3.8](https://tech.serhatteker.com/post/2019-12/upgrade-python38-on-ubuntu/), virtualenv is not enough.\n\nTo see documentation:\n\n```bash\n./gradlew mkdocsServe --no-daemon\n```\n\nTo deploy documentation:\n\n```bash\n./gradlew mkdocsPublish\n```"
  },
  {
    "path": "build.gradle",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\nplugins {\n    id 'java'\n    id \"com.github.hierynomus.license\" version \"0.15.0\"\n    id \"com.diffplug.spotless\"         version \"6.11.0\"\n    id 'ru.vyarus.use-python'          version '3.0.0'\n    id 'ru.vyarus.mkdocs'              version '2.4.0'\n    id \"io.github.gradle-nexus.publish-plugin\" version \"1.1.0\"\n    id \"org.shipkit.shipkit-auto-version\" version \"1.1.19\"\n    //id 'org.inferred.processors'    version '2.3.0'\n}\n\napply from: \"gradle/release.gradle\"\n\nmkdocs {\n    sourcesDir = projectDir\n    strict = true\n}\n\npython {\n    minPythonVersion = '3.7'\n    // mkdocs requires 3.7.x\n    scope = VIRTUALENV\n}\n\nspotless {\n    freshmark {\n        target 'README.md'\n        propertiesFile('gradle.properties')\t\n        propertiesFile('version.properties')\t\n    }\n\n    java {\n        googleJavaFormat()\n    }\n}\n\nallprojects {\n    repositories {\n        mavenCentral()\n    }\n}\n\n// Root project shouldn't publish\ntasks.withType(PublishToMavenRepository).configureEach { it.enabled = false }\n\nsubprojects { subproj ->\n    apply plugin: 'java'\n    apply plugin: 'com.diffplug.spotless'\n\n    spotless {\n        java {\n            googleJavaFormat()\n        }\n    }\n\n    java {\n        toolchain {\n            languageVersion = JavaLanguageVersion.of(8)\n        }\n    }\n\n    dependencies {\n        testImplementation 'org.apiguardian:apiguardian-api:1.1.0'\n        testImplementation 'org.assertj:assertj-core:3.13.2'\n        testImplementation \"junit:junit:$junitVersion\"\n        testImplementation \"org.junit.jupiter:junit-jupiter-api:$junitJupiterVersion\"\n        testImplementation \"org.junit.jupiter:junit-jupiter-engine:$junitJupiterVersion\"\n        testImplementation \"org.junit.vintage:junit-vintage-engine:$junitVintageVersion\"\n        //testImplementation group: 'org.junit.platform', name: 'junit-platform-runner', version: '1.5.0'\n    }\n\n    test {\n        useJUnitPlatform()\n    }\n}\n\n// Go through all the artifacts and find javadoc for it...\nstatic List<String> javadocFromDependencies(Configuration config) {\n    List<String> javadocs = []\n    config.dependencies.each { dep ->\n        javadocs.add(artifactToJavadoc(dep.group, dep.name, dep.version))\n    }\n    javadocs\n}\n\nstatic String jvmToJavadoc(JavaVersion jvmVersion) {\n    if (jvmVersion.java8) {\n        'https://docs.oracle.com/javase/8/docs/api/'\n    } else if (jvmVersion.java9) {\n        'https://docs.oracle.com/javase/9/docs/api/'\n    }else if (jvmVersion.java10) {\n        'https://docs.oracle.com/javase/10/docs/api/'\n    }else if (jvmVersion.java11) {\n        'https://docs.oracle.com/en/java/javase/11/docs/api/'\n    } else {\n        'https://docs.oracle.com/javase/8/docs/api/'\n    }\n}\n\nstatic String artifactToJavadoc(String organization, String name, String apiVersion) {\n    String slashedOrg = organization.replace('.', '/')\n    \"https://oss.sonatype.org/service/local/repositories/releases/archive/$slashedOrg/$name/$apiVersion/$name-$apiVersion-javadoc.jar/!/\"\n}\n"
  },
  {
    "path": "docs/guide/audio.md",
    "content": "# Audio\n\nThe audio appender uses a system beep configured through `SystemPlayer` to notify on warnings and errors, and limits excessive beeps with a budget evaluator.\n\n## Installation\n\nAdd the library dependency using [com.tersesystems.logback:logback-audio](https://mvnrepository.com/artifact/com.tersesystems.logback/logback-audio).\n\n## Usage\n\nThe XML is as follows:\n\n```xml\n<included>\n\n    <appender name=\"AUDIO-WARN\" class=\"com.tersesystems.logback.audio.AudioAppender\">\n        <filter class=\"ch.qos.logback.classic.filter.LevelFilter\">\n            <level>WARN</level>\n            <onMatch>NEUTRAL</onMatch>\n            <onMismatch>DENY</onMismatch>\n        </filter>\n\n        <player class=\"com.tersesystems.logback.audio.SystemPlayer\"/>\n    </appender>\n\n    <appender name=\"AUDIO-ERROR\" class=\"com.tersesystems.logback.audio.AudioAppender\">\n        <filter class=\"ch.qos.logback.classic.filter.LevelFilter\">\n            <level>ERROR</level>\n            <onMatch>ACCEPT</onMatch>\n            <onMismatch>DENY</onMismatch>\n        </filter>\n\n        <player class=\"com.tersesystems.logback.audio.SystemPlayer\"/>\n    </appender>\n\n    <appender name=\"AUDIO\" class=\"com.tersesystems.logback.core.CompositeAppender\">\n        <filter class=\"ch.qos.logback.core.filter.EvaluatorFilter\">\n            <evaluator class=\"com.tersesystems.logback.budget.BudgetEvaluator\">\n                <budgetRule name=\"WARN\" threshold=\"1\" interval=\"5\" timeUnit=\"seconds\"/>\n                <budgetRule name=\"ERROR\" threshold=\"1\" interval=\"5\" timeUnit=\"seconds\"/>\n            </evaluator>\n            <OnMismatch>DENY</OnMismatch>\n            <OnMatch>NEUTRAL</OnMatch>\n        </filter>\n\n        <appender-ref ref=\"AUDIO-WARN\"/>\n        <appender-ref ref=\"AUDIO-ERROR\"/>\n    </appender>\n\n</included>\n```\n\nSee [Application Logging in Java: Appenders](\nhttps://tersesystems.com/blog/2019/05/27/application-logging-in-java-part-5/) for more details.\n"
  },
  {
    "path": "docs/guide/budget.md",
    "content": "# Budget Aware Logging\n\nThere are instances where logging may be overly chatty, and will log more than necessary.  \n\nRather than hunt down all the individual loggers and whitelist or blacklist the lot of them, you can assign a \nbudget that will budget INFO messages to 5 statements a second.\n\nThis 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.\n\n## Installation\n\nAdd the library dependency using [com.tersesystems.logback:logback-budget](https://mvnrepository.com/artifact/com.tersesystems.logback/logback-budget).\n\n## Usage\n\nThe time unit corresponds to the text value of `java.util.concurrent.TimeUnit` i.e. `nanoseconds`, `microseconds`, `milliseconds`, `seconds`, `minutes`, `hours`, `days`, case-insensitive.\n\n```xml\n<configuration>\n\n    <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n        <filter class=\"ch.qos.logback.core.filter.EvaluatorFilter\">\n            <evaluator class=\"com.tersesystems.logback.budget.BudgetEvaluator\">\n                <budgetRule>\n                    <name>INFO</name>\n                    <threshold>5</threshold>\n                    <interval>1</interval>\n                    <timeUnit>seconds</timeUnit>\n                </budgetRule>\n            </evaluator>\n            <OnMismatch>DENY</OnMismatch>\n            <OnMatch>NEUTRAL</OnMatch>\n        </filter>\n\n        <encoder>\n            <pattern>%-5relative %-5level %logger{35} - %msg%n</pattern>\n        </encoder>\n    </appender>\n\n    <root level=\"TRACE\">\n        <appender-ref ref=\"STDOUT\"/>\n    </root>\n\n</configuration>\n```\n\n## Turbo Filter\n\nYou 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`.\n\n```xml\n<configuration>\n    \n    <turboFilter class=\"com.tersesystems.logback.budget.BudgetTurboFilter\">\n        <budgetRule>\n            <name>INFO</name>\n            <threshold>5</threshold>\n            <interval>1</interval>\n            <timeUnit>second</timeUnit>\n        </budgetRule>\n        <OnMismatch>DENY</OnMismatch>\n        <OnMatch>NEUTRAL</OnMatch>\n    </turboFilter>\n    \n    <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n        <encoder>\n            <pattern>%-5relative %-5level %logger{35} - %msg%n</pattern>\n        </encoder>\n    </appender>\n\n    <root level=\"TRACE\">\n        <appender-ref ref=\"STDOUT\"/>\n    </root>\n\n</configuration>\n```\n"
  },
  {
    "path": "docs/guide/censor.md",
    "content": "# Censors\n\nThere 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.\n\nThere are two rules and a converter that are used in Logback to define and reference censors: `CensorAction`, `CensorRefAction` and the `censor` converter.\n\n## Installation\n\nAdd the library dependency using [com.tersesystems.logback:logback-censor](https://mvnrepository.com/artifact/com.tersesystems.logback/logback-censor).\n\n## Usage\n\n```xml\n<configuration>\n    <newRule pattern=\"*/censor\"\n             actionClass=\"com.tersesystems.logback.censor.CensorAction\"/>\n\n    <newRule pattern=\"*/censor-ref\"\n             actionClass=\"com.tersesystems.logback.censor.CensorRefAction\"/>\n\n    <conversionRule conversionWord=\"censor\" converterClass=\"com.tersesystems.logback.censor.CensorConverter\" />\n\n    <!-- ... -->\n</configuration>\n```\n\nThe `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:\n\n```xml\n<configuration>\n    <censor name=\"censor-name1\" class=\"com.tersesystems.logback.censor.RegexCensor\">\n        <replacementText>[CENSORED BY CENSOR1]</replacementText>\n        <regex>hunter1</regex>\n    </censor>\n\n    <censor name=\"censor-name2\" class=\"com.tersesystems.logback.censor.RegexCensor\">\n        <replacementText>[CENSORED BY CENSOR2]</replacementText>\n        <regex>hunter2</regex>\n    </censor>\n</configuration>\n```\n\nOnce 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.\n\n```xml\n<configuration>\n    <appender name=\"TEST1\" class=\"ch.qos.logback.core.FileAppender\">\n        <file>file1.log</file>\n        <encoder>\n            <pattern>%censor(%msg){censor-name1}%n</pattern>\n        </encoder>\n    </appender>\n\n    <appender name=\"TEST2\" class=\"ch.qos.logback.core.FileAppender\">\n        <file>file2.log</file>\n        <encoder>\n            <pattern>%censor(%msg){censor-name2}%n</pattern>\n        </encoder>\n    </appender>\n</configuration>\n```\n\nIf 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.\n\n```xml\n<configuration>\n    <appender name=\"TEST3\" class=\"ch.qos.logback.core.FileAppender\">\n        <file>file3.log</file>\n        <encoder class=\"net.logstash.logback.encoder.LogstashEncoder\">\n            <jsonGeneratorDecorator class=\"com.tersesystems.logback.censor.CensoringJsonGeneratorDecorator\">\n                <censor-ref ref=\"first-censor\"/>\n                <censor-ref ref=\"second-censor\"/>\n            </jsonGeneratorDecorator>\n        </encoder>\n    </appender>\n</configuration>\n```\n\nIn this case, `CensoringJsonGeneratorDecorator` implements the `CensorAttachable` interface and so will run message text through the censor if it exists.\n\nSee [Application Logging in Java: Converters](https://tersesystems.com/blog/2019/05/11/application-logging-in-java-part-3/) for more details.\n"
  },
  {
    "path": "docs/guide/composite.md",
    "content": "# Composite Appender\n\nThe 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.\n\n## Installation\n\nAdd the library dependency using [com.tersesystems.logback:logback-core](https://mvnrepository.com/artifact/com.tersesystems.logback/logback-core).\n\n## Usage\n\n```xml\n<configuration debug=\"true\">\n\n    <appender name=\"CONSOLE\" class=\"ch.qos.logback.core.read.ListAppender\">\n    </appender>\n\n    <appender name=\"FILE\" class=\"ch.qos.logback.core.read.ListAppender\">\n    </appender>\n\n    <appender name=\"CONSOLE_AND_FILE\" class=\"com.tersesystems.logback.core.CompositeAppender\">\n        <appender-ref ref=\"CONSOLE\"/>\n        <appender-ref ref=\"FILE\"/>\n    </appender>\n\n    <root level=\"TRACE\">\n        <appender-ref ref=\"CONSOLE_AND_FILE\"/>\n    </root>\n</configuration>\n```\n\nYou 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.\n\nUsing nesting, you can declare the filter once, and have the child appenders \"inherit\" that filter:\n\n<configuration>\n    <newRule pattern=\"*/player\"\n           actionClass=\"com.tersesystems.logback.audio.PlayerAction\"/>\n \n    <!-- Filter is on the appender chain -->\n    <appender name=\"ERROR-APPENDER\" class=\"com.tersesystems.logback.CompositeAppender\">\n        <filter class=\"ch.qos.logback.classic.filter.LevelFilter\">\n            <level>ERROR</level>\n            <onMatch>ACCEPT</onMatch>\n            <onMismatch>DENY</onMismatch>\n        </filter>\n \n        <appender class=\"ch.qos.logback.core.FileAppender\">\n            <file>error.log</file>\n            <encoder>\n                <pattern>%date - %message</pattern>\n            </encoder>\n        </appender>\n \n        <appender class=\"com.tersesystems.logback.audio.AudioAppender\">\n            <player class=\"com.tersesystems.logback.audio.ResourcePlayer\">\n                <resource>/error.ogg</resource>\n            </player>\n        </appender>\n    </appender>\n \n    <root level=\"TRACE\">\n        <appender-ref ref=\"ALL-APPENDER\"/>\n        <appender-ref ref=\"TRACE-APPENDER\"/>\n        <appender-ref ref=\"DEBUG-APPENDER\"/>\n        <appender-ref ref=\"INFO-APPENDER\"/>\n        <appender-ref ref=\"WARN-APPENDER\"/>\n        <appender-ref ref=\"ERROR-APPENDER\"/>\n    </root>\n</configuration>\n\nThis makes your appender logic much cleaner.\n\nSee [Application Logging in Java: Appenders](https://tersesystems.com/blog/2019/05/27/application-logging-in-java-part-5/) for more details.\n"
  },
  {
    "path": "docs/guide/compression.md",
    "content": "# Compression\n\nEncoders 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.\n\n## Installation\n\nAdd the library dependency using [com.tersesystems.logback:logback-compress-encoder](https://mvnrepository.com/artifact/com.tersesystems.logback/logback-compress-encoder).\n\n## Usage\n\nAs 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.\n\nHere's the appender as `logback.xml` sees it:\n\n```xml\n<appender name=\"COMPRESS_FILE\" class=\"com.tersesystems.logback.compress.CompressingFileAppender\">\n    <file>encoded.zst</file>\n\n    <compressAlgo>zstd</compressAlgo>\n    <bufferSize>1024000</bufferSize>\n\n    <encoder class=\"ch.qos.logback.classic.encoder.PatternLayoutEncoder\">\n        <charset>UTF-8</charset>\n        <pattern>%-5level %logger{35} - %msg%n</pattern>\n    </encoder>\n</appender>\n```\n\nUnder 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`:\n\n```java\npublic class CompressingFileAppender<E> extends UnsynchronizedAppenderBase<E> {\n    // ...\n\n    @Override\n    public void start() {\n        fileAppender = new FileAppender<>();\n        fileAppender.setContext(getContext());\n        fileAppender.setFile(getFile());\n        fileAppender.setImmediateFlush(false);\n        fileAppender.setPrudent(isPrudent());\n        fileAppender.setAppend(isAppend());\n        fileAppender.setName(name+\"-embedded-file\");\n\n        CompressingEncoder<E> compressedEncoder = createCompressingEncoder(getEncoder());\n        fileAppender.setEncoder(compressedEncoder);\n        fileAppender.start();\n\n        super.start();\n    }\n\n    public void stop() {\n        fileAppender.stop();\n        super.stop();\n    }\n\n    @Override\n    protected void append(E eventObject) {\n        fileAppender.doAppend(eventObject);\n    }\n\n    protected CompressingEncoder<E> createCompressingEncoder(Encoder<E> e) {\n        int bufferSize = getBufferSize();\n        String compressAlgo = getCompressAlgo();\n\n        CompressorStreamFactory factory = CompressorStreamFactory.getSingleton();\n        Set<String> names = factory.getOutputStreamCompressorNames();\n        if (names.contains(getCompressAlgo())) {\n            try {\n                return new CompressingEncoder<>(e, compressAlgo, factory, bufferSize);\n            } catch (CompressorException ex) {\n                throw new RuntimeException(\"Cannot create CompressingEncoder\", ex);\n            }\n        } else {\n            throw new RuntimeException(\"No such compression algorithm: \" + compressAlgo);\n        }\n    }\n}\n```\n\nFrom 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:\n\n```java\npublic class CompressingEncoder<E> extends EncoderBase<E> {\n    private final Accumulator accumulator;\n    private final Encoder<E> encoder;\n\n    public CompressingEncoder(Encoder<E> encoder, \n                              String compressAlgo,\n                              CompressorStreamFactory factory, \n                              int bufferSize) throws CompressorException {\n        this.encoder = encoder;\n        this.accumulator = new Accumulator(compressAlgo, factory, bufferSize);\n    }\n\n    @Override\n    public byte[] headerBytes() {\n        try {\n            return accumulator.apply(encoder.headerBytes());\n        } catch (IOException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    @Override\n    public byte[] encode(E event) {\n        try {\n            return accumulator.apply(encoder.encode(event));\n        } catch (IOException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    @Override\n    public byte[] footerBytes() {\n        try {\n            return accumulator.drain(encoder.footerBytes());\n        } catch (IOException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    static class Accumulator {\n        private final ByteArrayOutputStream byteOutputStream;\n        private final CompressorOutputStream stream;\n        private final LongAdder count = new LongAdder();\n        private final int bufferSize;\n\n        public Accumulator(String compressAlgo, \n                           CompressorStreamFactory factory, \n                           int bufferSize) throws CompressorException {\n            this.bufferSize = bufferSize;\n            this.byteOutputStream = new ByteArrayOutputStream();\n            this.stream = factory.createCompressorOutputStream(compressAlgo, byteOutputStream);\n        }\n\n        boolean isFlushable() {\n            return count.intValue() >= bufferSize;\n        }\n\n        byte[] apply(byte[] bytes) throws IOException {\n            count.add(bytes.length);\n            stream.write(bytes);\n\n            if (isFlushable()) {\n                stream.flush();\n                byte[] output = byteOutputStream.toByteArray();\n                byteOutputStream.reset();\n                count.reset();\n                return output;\n            } else {\n                return new byte[0];\n            }\n        }\n\n        byte[] drain(byte[] inputBytes) throws IOException {\n            if (inputBytes != null) {\n                stream.write(inputBytes);\n            }\n            stream.close();\n            count.reset();\n            return byteOutputStream.toByteArray();\n        }\n    }\n}\n```\n\nThis 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.\n\nYou 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.\n\nSee [Application Logging in Java: Encoders](https://tersesystems.com/blog/2019/06/09/application-logging-in-java-part-7/) for more details.\n"
  },
  {
    "path": "docs/guide/correlationid.md",
    "content": "# Correlation ID\n\nThe `logback-correlationid` module is a set of classes designed to encompass the idea of a correlation id in events.\n\n## Installation\n\nAdd the library dependency using [com.tersesystems.logback:logback-correlationid](https://mvnrepository.com/artifact/com.tersesystems.logback/logback-correlationid).\n\n## Usage\n\nIt 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.\n\n### Correlation ID Filter\n\nA correlation id filter will filter for a correlation id set either as an MDC value, or as a marker created from `CorrelationIdMarker`.  \n\n```xml\n  <appender name=\"LIST\" class=\"ch.qos.logback.core.read.ListAppender\">\n    <filter class=\"com.tersesystems.logback.correlationid.CorrelationIdFilter\">\n        <mdcKey>correlationId</mdcKey>\n    </filter>\n</appender>\n```\n\nIf an appender passes the filter, it will log the event.\n\n```java\npublic class CorrelationIdFilterTest {\n    public void testFilter() {\n        // Write something that never gets logged explicitly...\n        Logger logger = loggerFactory.getLogger(\"com.example.Debug\");\n        String correlationId = \"12345\";\n        CorrelationIdMarker correlationIdMarker = CorrelationIdMarker.create(correlationId);\n\n        // should be logged because marker\n        logger.info(correlationIdMarker, \"info one\");\n\n        logger.info(\"info two\"); // should not be logged\n\n        // Everything below this point should be logged.\n        MDC.put(\"correlationId\", correlationId);\n        logger.info(\"info three\"); // should not be logged\n        logger.info(correlationIdMarker, \"info four\");\n    }\n}\n```\n\n### CorrelationIdTapFilter\n\nThe `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.  \n\nThis functions as a <a href=\"https://www.enterpriseintegrationpatterns.com/patterns/messaging/WireTap.html\">wiretap</a>.\n\nTap 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.\n\nFor 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.\n\n\n```xml\n<configuration>\n\n    <newRule pattern=\"configuration/turboFilter/appender-ref\"\n             actionClass=\"ch.qos.logback.core.joran.action.AppenderRefAction\"/>\n\n    <appender name=\"TAP_LIST\" class=\"ch.qos.logback.core.read.ListAppender\">\n    </appender>\n\n    <turboFilter class=\"com.tersesystems.logback.correlationid.CorrelationIdTapFilter\">\n        <mdcKey>correlationId</mdcKey>\n        <appender-ref ref=\"TAP_LIST\"/>\n    </turboFilter>\n\n    <appender name=\"CONSOLE\" class=\"ch.qos.logback.core.ConsoleAppender\">\n        <encoder>\n            <pattern>%-5relative %-5level %logger{35} - %msg%n</pattern>\n        </encoder>\n    </appender>\n\n    <root level=\"INFO\">\n        <appender-ref ref=\"CONSOLE\" />\n    </root>\n</configuration>\n```\n\n### CorrelationIdMarker\n\nA `CorrelationIdMarker` implements the `CorrelationIdProvider` interface to expose a marker which is known to contain a correlation id.\n\n```java\nCorrelationIdMarker correlationIdMarker = CorrelationIdMarker.create(correlationId);\nString sameId = correlationIdMarker.getCorrelationId();\n```\n\n### CorrelationIdUtils\n\n`CorrelationIdUtils` contains utility methods like `get` which retrieve a correlation id from either a marker or MDC.\n"
  },
  {
    "path": "docs/guide/exception-mapping.md",
    "content": "# Exception Mapping\n\nException 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.\n\n## Installation\n\nAdd 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).\n\n## Usage\n\nGiven the following program:\n\n```java\npublic class Thrower {\n    private static final Logger logger = LoggerFactory.getLogger(Thrower.class);\n\n    public static void main(String[] progArgs) {\n        try {\n            doSomethingExceptional();\n        } catch (RuntimeException e) {\n            logger.error(\"domain specific message\", e);\n        }\n    }\n\n    static void doSomethingExceptional() {\n        Throwable cause = new BatchUpdateException();\n        throw new MyCustomException(\"This is my message\", \"one is one\", \"two is more than one\", \"three is more than two and one\", cause);\n    }\n}\n\npublic class MyCustomException extends RuntimeException {\n    public MyCustomException(String message, String one, String two, String three, Throwable cause) {\n       // ...\n    }\n    public String getOne() { return one; }\n    public String getTwo() { return two; }\n    public String getThree() { return three; }\n}\n```\n\nand the Logback file:\n\n```xml\n<configuration>\n\n  <newRule pattern=\"*/exceptionMappings\"\n           actionClass=\"com.tersesystems.logback.exceptionmapping.ExceptionMappingRegistryAction\"/>\n\n  <newRule pattern=\"*/exceptionMappings/mapping\"\n           actionClass=\"com.tersesystems.logback.exceptionmapping.ExceptionMappingAction\"/>\n\n  <conversionRule conversionWord=\"richex\" converterClass=\"com.tersesystems.logback.exceptionmapping.ExceptionMessageWithMappingsConverter\" />\n\n  <exceptionMappings>\n    <!-- comes with default mappings for JDK exceptions, but you can add your own -->\n    <mapping name=\"com.tersesystems.logback.exceptionmapping.MyCustomException\" properties=\"one,two,three\"/>\n  </exceptionMappings>\n\n  <appender name=\"CONSOLE\" class=\"ch.qos.logback.core.ConsoleAppender\">\n    <encoder>\n      <pattern>%-5relative %-5level %logger{35} - %msg%richex{1, 10, exception=[}%n</pattern>\n    </encoder>\n  </appender>\n\n  <root level=\"TRACE\">\n    <appender-ref ref=\"CONSOLE\"/>\n  </root>\n\n</configuration>\n```\n\nThen this renders the following:\n\n```\n184   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\")]\n```\n\nYou can integrate exception mapping with Typesafe Config and `logstash-logback-encoder` by adding extra mappings.\n\nFor example, you can map a whole bunch of exceptions at once in HOCON, and not have to do it line by line in XML:\n\n```xml\n<configuration>\n  <newRule pattern=\"*/exceptionMappings/configMappings\"\n           actionClass=\"com.tersesystems.logback.exceptionmapping.config.TypesafeConfigMappingsAction\"/>\n\n  <exceptionMappings>\n    <!-- Or point to HOCON path -->\n    <configMappings path=\"exceptionmappings\"/>\n  </exceptionMappings>\n</configuration>\n```\n\nand\n\n```hocon\nexceptionmappings {\n   example.MySpecialException: [\"timestamp\"]\n}\n```\n\nand configure it in JSON using `ExceptionArgumentsProvider`:\n\n```xml\n<encoder class=\"net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder\">\n  <providers>\n    <provider class=\"com.tersesystems.logback.exceptionmapping.json.ExceptionArgumentsProvider\">\n      <fieldName>exception</fieldName>\n    </provider>\n  </providers>\n</encoder>\n```\n\nand get the following `exception` that contains an array of exceptions and the associated properties, in this case `timestamp`:\n\n```json\n{\n  \"id\" : \"Fa6x8H0EqomdHaINzdiAAA\",\n  \"sequence\" : 3,\n  \"@timestamp\" : \"2019-07-06T03:52:48.730+00:00\",\n  \"@version\" : \"1\",\n  \"message\" : \"I am an error\",\n  \"logger_name\" : \"example.Main$Runner\",\n  \"thread_name\" : \"pool-1-thread-1\",\n  \"level\" : \"ERROR\",\n  \"stack_hash\" : \"233f3cf1\",\n  \"exception\" : [ {\n    \"name\" : \"example.MySpecialException\",\n    \"properties\" : {\n      \"message\" : \"Level 1\",\n      \"timestamp\" : \"2019-07-06T03:52:48.728Z\"\n    }\n  }, {\n    \"name\" : \"example.MySpecialException\",\n    \"properties\" : {\n      \"message\" : \"Level 2\",\n      \"timestamp\" : \"2019-07-06T03:52:48.728Z\"\n    }\n  }, {\n    \"name\" : \"example.MySpecialException\",\n    \"properties\" : {\n      \"message\" : \"Level 3\",\n      \"timestamp\" : \"2019-07-06T03:52:48.728Z\"\n    }\n  }, {\n    \"name\" : \"example.MySpecialException\",\n    \"properties\" : {\n      \"message\" : \"Level 4\",\n      \"timestamp\" : \"2019-07-06T03:52:48.728Z\"\n    }\n  }, {\n    \"name\" : \"example.MySpecialException\",\n    \"properties\" : {\n      \"message\" : \"Level 5\",\n      \"timestamp\" : \"2019-07-06T03:52:48.728Z\"\n    }\n  }, {\n    \"name\" : \"example.MySpecialException\",\n    \"properties\" : {\n      \"message\" : \"Level 6\",\n      \"timestamp\" : \"2019-07-06T03:52:48.728Z\"\n    }\n  }, {\n    \"name\" : \"example.MySpecialException\",\n    \"properties\" : {\n      \"message\" : \"Level 7\",\n      \"timestamp\" : \"2019-07-06T03:52:48.728Z\"\n    }\n  }, {\n    \"name\" : \"example.MySpecialException\",\n    \"properties\" : {\n      \"message\" : \"Level 8\",\n      \"timestamp\" : \"2019-07-06T03:52:48.728Z\"\n    }\n  }, {\n    \"name\" : \"example.MySpecialException\",\n    \"properties\" : {\n      \"message\" : \"Level 9\",\n      \"timestamp\" : \"2019-07-06T03:52:48.728Z\"\n    }\n  } ],\n  \"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\"\n}\n```\n\nThis is a lot easier for structured logging parsers to grok than the associated stacktrace.\n\nSee [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.\n"
  },
  {
    "path": "docs/guide/instrumentation.md",
    "content": "# Instrumentation\n\nIf 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.\n\nInstrumentation 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.\n\nI 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`.\n\nA 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:\n\n```java\npublic class Foo {\n    public void throwException() throws Exception {\n        throw new PlumException(\"I am sweet and cold\");\n    }\n\n    public void swallowException() {\n        try {\n            throwException();\n        } catch (Exception e) {\n            // forgive me, the exception was delicious\n        }\n    }\n}\n```\n\nBy instrumenting the `throwException` method, you can see the logged exception at runtime when `swallowException` is called.\n\nSee [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.\n\n## Installation\n\n\nYou'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).\n\nYou should also install [byte-buddy](https://mvnrepository.com/artifact/net.bytebuddy/byte-buddy).\n\n\n```\nimplementation group: 'com.tersesystems.logback', name: 'logback-classic', version: 'LATEST'\nimplementation group: 'com.tersesystems.logback', name: 'logback-bytebuddy', version: 'LATEST'\nimplementation group: 'com.tersesystems.logback', name: 'logback-tracing', version: 'LATEST'\n\nimplementation group: 'net.bytebuddy', name: 'byte-buddy', version: 'LATEST'\n```\n\nThere are two ways you can install instrumentation -- you can do it using an agent, or you can do it manually.\n\n> 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.\n\n### Agent Installation\n\nUsing the agent is generally easier (less code) and more powerful (can change JDK classes), but it does require some explicit command line options.\n\nFirst, you set the java agent, either directly on the command line:\n\n```bash\njava \\\n  -javaagent:path/to/logback-bytebuddy-x.x.x.jar=debug \\\n  -Dterse.logback.configurationFile=conf/logback.conf \\\n  -Dlogback.configurationFile=conf/logback-test.xml \\\n  com.example.PreloadedInstrumentationExample\n```\n\nor by using the [`JAVA_TOOLS_OPTIONS` environment variable](https://docs.oracle.com/javase/8/docs/technotes/guides/troubleshoot/envvars002.html).\n\n```bash\nexport JAVA_TOOLS_OPTIONS=\"...\"\n```\n\nGenerally 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).\n\n### Manual Installation\n\nYou also have the option of installing the agent manually.\n\nThe 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.\n\n```java\nnew LoggingInstrumentationByteBuddyBuilder()\n        .builderFromConfig(loggingInstrumentationAdviceConfig)\n        .with(debugListener)\n        .installOnByteBuddyAgent();\n```\n\n## Configuration\n\nThere are two parts to seeing tracing logs with instrumentation -- indicating the classes and methods you want instrumented, and then setting those loggers to TRACE.\n\n### Setting Instrumented Classes and Methods\n\nThe instrumentation is configured using [HOCON](https://github.com/lightbend/config/blob/main/HOCON.md) in a `logback.conf` file in `src/main/resources`.\n\nSettings are under the `logback.bytebuddy` section.  The `tracing` section contains a mapping of class names and methods, or the wildcard \"*\" to indicate all methods.\n\n```\nlogback.bytebuddy {\n  service-name = \"my-service\"\n\n  tracing {    \n    \"fully.qualified.class.Name\" = [\"method1\", \"method2\"]\n    \"play.api.mvc.ActionBuilder\" = [\"*\"]\n  }\n}\n```\n\nNOTE: 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.\n\n### Setting Loggers to TRACE\n\nBecause 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:\n\n```xml\n<configuration>\n    <!-- ... -->\n    <logger name=\"fully.qualified.class.Name\" level=\"TRACE\"/>\n    <logger name=\"play.api.mvc.ActionBuilder\" level=\"TRACE\"/>\n    <!-- ... -->\n</configuration>\n```\n\nIf you are using the [Config](typesafeconfig.md) module, you can also do this from `logback.conf`:\n\n```hocon\nlevels {\n  fully.qualified.class.Name = TRACE\n  play.api.mvc.ActionBuilder = TRACE\n}\n```\n\nOr you can use `ChangeLogLevel` at run time.\n\n## Examples\n\nInstrumentation is a tool that can be hard to explain, so here's some use cases showing how you can quickly instrument your code.\n\nAlso don't forget the example projects at [https://github.com/tersesystems/logging-instrumentation-example](https://github.com/tersesystems/logging-instrumentation-example).\n\n### Instrumenting java.lang.Thread\n\nAssuming an agent based instrumentation, in `logback.conf`:\n\n```hocon\nlevels {\n  java.lang.Thread = TRACE\n}\n\nlogback.bytebuddy {\n  service-name = \"some-service\"\n  tracing {\n    \"java.lang.Thread\" = [\n      \"run\"\n    ]\n  }\n}\n```\n\nand the code as follows:\n\n```java\npublic class PreloadedInstrumentationExample {\n    public static void main(String[] args) throws Exception {\n        Thread thread = Thread.currentThread();\n        thread.run();\n    }\n}\n```\n\nyields\n\n```text\n[Byte Buddy] DISCOVERY java.lang.Thread [null, null, loaded=true]\n[Byte Buddy] TRANSFORM java.lang.Thread [null, null, loaded=true]\n[Byte Buddy] COMPLETE java.lang.Thread [null, null, loaded=true]\n92    TRACE java.lang.Thread - entering: java.lang.Thread.run() with arguments=[]\n93    TRACE java.lang.Thread - exiting: java.lang.Thread.run() with arguments=[] => returnType=void\n```\n\n### Instrumenting javax.net.ssl.SSLContext\n\nThis is especially helpful when you're trying to debug SSL issues:\n\n```hocon\nlevels {\n  sun.security.ssl = TRACE\n  javax.net.ssl = TRACE\n}\n\nlogback.bytebuddy {\n  service-name = \"some-service\"\n  tracing {  \n    \"javax.net.ssl.SSLContext\" = [\"*\"]\n  }\n}\n```\n\nwill result in:\n\n```\nFcJ3XfsdKnM6O0Qbm7EAAA 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\nFcJ3XfsdKng6O0Qbm7EAAA 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\nFcJ3XfsdKng6O0Qbm7EAAB 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\nFcJ3XfsdKnk6O0Qbm7EAAA 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\n```\n\nBe warned that JSSE can be extremely verbose in its `toString` output.\n\n### Instrumenting ClassCalledByAgent\n\nIf 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.\n\nWith the following code:\n\n```java\npublic class ClassCalledByAgent {\n    public void printStatement() {\n        System.out.println(\"I am a simple println method with no logging\");\n    }\n\n    public void printArgument(String arg) {\n        System.out.println(\"I am a simple println, printing \" + arg);\n    }\n\n    public void throwException(String arg) {\n        throw new RuntimeException(\"I'm a squirrel!\");\n    }\n}\n```\n\nAnd the following configuration in `logback.conf`:\n\n```hocon\nlogback.bytebuddy {\n  service-name = \"example-service\"\n  tracing {\n    \"com.tersesystems.logback.bytebuddy.ClassCalledByAgent\" = [\n      \"printStatement\",\n      \"printArgument\",\n      \"throwException\",\n    ]\n  }\n}\n```\n\nand have `com.tersesystems.logback.bytebuddy.ClassCalledByAgent` logging level set to `TRACE` in `logback.xml`.\n\nWe can start up the agent, add in the builder and run through the methods:\n\n```java\npublic class InProcessInstrumentationExample {\n\n    public static AgentBuilder.Listener createDebugListener(List<String> classNames) {\n        return new AgentBuilder.Listener.Filtering(\n                LoggingInstrumentationAdvice.stringMatcher(classNames),\n                AgentBuilder.Listener.StreamWriting.toSystemOut());\n    }\n\n    public static void main(String[] args) throws Exception {\n        // Helps if you install the byte buddy agents before anything else at all happens...\n        ByteBuddyAgent.install();\n\n        Logger logger = LoggerFactory.getLogger(InProcessInstrumentationExample.class);\n        SystemFlow.setLoggerResolver(new FixedLoggerResolver(logger));\n\n        Config config = LoggingInstrumentationAdvice.generateConfig(ClassLoader.getSystemClassLoader(), false);\n        LoggingInstrumentationAdviceConfig adviceConfig = LoggingInstrumentationAdvice.generateAdviceConfig(config);\n\n        // The debugging listener shows what classes are being picked up by the instrumentation\n        Listener debugListener = createDebugListener(adviceConfig.classNames());\n        new LoggingInstrumentationByteBuddyBuilder()\n                .builderFromConfig(adviceConfig)\n                .with(debugListener)\n                .installOnByteBuddyAgent();\n\n        // No code change necessary here, you can wrap completely in the agent...\n        ClassCalledByAgent classCalledByAgent = new ClassCalledByAgent();\n        classCalledByAgent.printStatement();\n        classCalledByAgent.printArgument(\"42\");\n\n        try {\n            classCalledByAgent.throwException(\"hello world\");\n        } catch (Exception e) {\n            // I am too lazy to catch this exception.  I hope someone does it for me.\n        }\n    }\n}\n```\n\nAnd get the following:\n\n```text\n[Byte Buddy] DISCOVERY com.tersesystems.logback.bytebuddy.ClassCalledByAgent [sun.misc.Launcher$AppClassLoader@75b84c92, null, loaded=true]\n[Byte Buddy] TRANSFORM com.tersesystems.logback.bytebuddy.ClassCalledByAgent [sun.misc.Launcher$AppClassLoader@75b84c92, null, loaded=true]\n[Byte Buddy] COMPLETE com.tersesystems.logback.bytebuddy.ClassCalledByAgent [sun.misc.Launcher$AppClassLoader@75b84c92, null, loaded=true]\n524   TRACE c.t.l.b.InProcessInstrumentationExample - entering: com.tersesystems.logback.bytebuddy.ClassCalledByAgent.printStatement() with arguments=[] from source ClassCalledByAgent.java:18\nI am a simple println method with no logging\n529   TRACE c.t.l.b.InProcessInstrumentationExample - exiting: com.tersesystems.logback.bytebuddy.ClassCalledByAgent.printStatement() with arguments=[] => returnType=void from source ClassCalledByAgent.java:19\n529   TRACE c.t.l.b.InProcessInstrumentationExample - entering: com.tersesystems.logback.bytebuddy.ClassCalledByAgent.printArgument(java.lang.String) with arguments=[42] from source ClassCalledByAgent.java:22\nI am a simple println, printing 42\n529   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\n529   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\n532   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!\njava.lang.RuntimeException: I'm a squirrel!\n\tat com.tersesystems.logback.bytebuddy.ClassCalledByAgent.throwException(ClassCalledByAgent.java:26)\n\tat com.tersesystems.logback.bytebuddy.InProcessInstrumentationExample.main(InProcessInstrumentationExample.java:65)\n```\n\nThe `[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`.\n"
  },
  {
    "path": "docs/guide/jdbc.md",
    "content": "# JDBC\n\nThere 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.\n\nAlso consider using [Blacklite](https://github.com/tersesystems/blacklite/), an SQLite appender configured for low latency and high throughput.\n\nLogback **does** have a native JDBC appender, but unfortunately it requires three tables and is not set up for easy subclassing.  This one is better.\n\nThis 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.\n\nDatabase 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.\n\nQuerying 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.\n\n## Installation\n\nAdd the library dependency using [com.tersesystems.logback:logback-jdbc-appender](https://mvnrepository.com/artifact/com.tersesystems.logback/logback-jdbc-appender).\n\n## Usage\n\n### Logging using in-memory H2 Database\n\nUsing an in memory H2 database is a cheap and easy way to expose logs from inside the application without having to parse files.\n\n```xml\n<appender name=\"H2_JDBC\" class=\"com.tersesystems.logback.jdbc.JDBCAppender\">\n    <driver>jdbc:h2:mem:logback</driver>\n    <url>org.h2.Driver</url>\n    <username>sa</username>\n    <password></password>\n    \n    <createStatements>\n      CREATE TABLE IF NOT EXISTS events (\n         ID INT NOT NULL PRIMARY KEY AUTO_INCREMENT,\n         ts TIMESTAMP(9) WITH TIME ZONE NOT NULL,\n         tse_ms numeric NOT NULL,\n         start_ms numeric NULL,\n         level_value int NOT NULL,\n         level VARCHAR(7) NOT NULL,\n         evt JSON NOT NULL\n      );\n    </createStatements>\n    <insertStatement>insert into events(ts, tse_ms, start_ms, level_value, level, evt) values(?, ?, ?, ?, ?, ?)</insertStatement>\n    <reaperStatement>delete from events where ts &lt; ?</reaperStatement>\n    <reaperSchedule>PT30</reaperSchedule>\n\n    <encoder class=\"net.logstash.logback.encoder.LogstashEncoder\">\n    </encoder>\n</appender>\n```\n\n### Logging using PostgresSQL\n\nIf 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:\n\n```java\npublic class PostgresJsonAppender extends JDBCAppender {\n\n  private String objectType = \"json\";\n\n  public String getObjectType() {\n    return objectType;\n  }\n\n  public void setObjectType(String objectType) {\n    this.objectType = objectType;\n  }\n\n  @Override\n  public void start() {\n    super.start();\n    setDriver(\"org.postgresql.Driver\");\n  }\n\n  @Override\n  protected void insertEvent(ILoggingEvent event, LongAdder adder, PreparedStatement statement)\n      throws SQLException {\n    PGobject jsonObject = new PGobject();\n    jsonObject.setType(getObjectType());\n    byte[] bytes = getEncoder().encode(event);\n    jsonObject.setValue(new String(bytes, StandardCharsets.UTF_8));\n    statement.setObject(adder.intValue(), jsonObject);\n    adder.increment();\n  }\n}\n```\n\nFirst, install PostgreSQL, create a database `logback`, a role `logback` and a password `logback` and add the following table:\n\n```sql\nCREATE TABLE logging_table (\n   ID serial NOT NULL PRIMARY KEY,\n   ts TIMESTAMPTZ(6) NOT NULL,\n   tse_ms numeric NOT NULL,\n   start_ms numeric NULL,\n   level_value int NOT NULL,\n   level VARCHAR(7) NOT NULL,\n   evt jsonb NOT NULL\n);\nCREATE INDEX idxgin ON logging_table USING gin (evt);\n```\n\nBecause 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.\n\nThen, add the following `logback.xml`:\n\n```xml\n<configuration>\n    <!-- async appender needs a shutdown hook to make sure this clears -->\n    <shutdownHook class=\"ch.qos.logback.core.hook.DelayingShutdownHook\"/>\n\n    <conversionRule conversionWord=\"startTime\" converterClass=\"com.tersesystems.logback.classic.StartTimeConverter\" />\n\n    <!-- SQL is blocking, so use an async lmax appender here -->\n    <appender name=\"ASYNC_POSTGRES\" class=\"net.logstash.logback.appender.LoggingEventAsyncDisruptorAppender\">\n        <appender class=\"com.tersesystems.logback.postgresjson.PostgresJsonAppender\">\n            <createStatements>\n                CREATE TABLE IF NOT EXISTS logging_table (\n                ID serial NOT NULL PRIMARY KEY,\n                ts TIMESTAMPTZ(6) NOT NULL,\n                tse_ms bigint NOT NULL,\n                start_ms bigint NULL,\n                level_value int NOT NULL,\n                level VARCHAR(7) NOT NULL,\n                evt jsonb NOT NULL\n                );\n                CREATE INDEX idxgin ON logging_table USING gin (evt);\n           </createStatements>\n\n           <!-- SQL statement takes a TIMESTAMP, LONG, INT, VARCHAR, PGObject -->\n           <insertStatement>insert into logging_table(ts, tse_ms, start_ms, level_value, level, evt) values(?, ?, ?, ?, ?, ?)</insertStatement>\n\n            <url>jdbc:postgresql://localhost:5432/logback</url>\n            <username>logback</username>\n            <password>logback</password>\n\n            <encoder class=\"net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder\">\n                <providers>\n                    <message/>\n                    <loggerName/>\n                    <threadName/>\n                    <logLevel/>\n                    <stackHash/>\n                    <mdc/>\n                    <logstashMarkers/>\n                    <pattern>\n                        <pattern>\n                            { \"start_ms\": \"#asLong{%%startTime}\" }\n                        </pattern>\n                    </pattern>\n                    <arguments/>\n                    <stackTrace>\n                        <throwableConverter class=\"net.logstash.logback.stacktrace.ShortenedThrowableConverter\">\n                            <rootCauseFirst>true</rootCauseFirst>\n                        </throwableConverter>\n                    </stackTrace>\n                </providers>\n            </encoder>\n        </appender>\n    </appender>\n\n    <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n        <encoder>\n            <pattern>%-5relative %-5level %logger{35} - %msg%n</pattern>\n        </encoder>\n    </appender>\n\n    <root level=\"INFO\">\n        <appender-ref ref=\"ASYNC_POSTGRES\"/>\n        <appender-ref ref=\"STDOUT\"/>\n    </root>\n</configuration>\n```\n\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:\n\n```sql\nselect \n  ts as end_date, \n  start_ms as epoch_start, \n  tse_ms as epoch_end, \n  evt->'trace.span_id' as span_id, \n  evt->'name' as name, \n  evt->'message' as message,  \n  evt->'trace.parent_id' as parent,\n  evt->'duration_ms' as duration_ms \nfrom logging_table where evt->'trace.trace_id' IS NOT NULL order by ts desc limit 5\n```\n\nIf 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).\n\n### Extending JDBC Appender with extra fields\n\nThe JDBC appender can be extended so you can add extra information to the table. \n  \nIn 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:\n\n```java\npublic class CorrelationIdJdbcAppender extends JDBCAppender {\n  private String mdcKey = \"correlation_id\";\n\n  public String getMdcKey() {\n    return mdcKey;\n  }\n\n  public void setMdcKey(String mdcKey) {\n    this.mdcKey = mdcKey;\n  }\n\n  protected CorrelationIdUtils utils;\n\n  @Override\n  public void start() {\n    super.start();\n    utils = new CorrelationIdUtils(mdcKey);\n  }\n\n  @Override\n  protected void insertAdditionalData(\n      ILoggingEvent event, LongAdder adder, PreparedStatement statement) throws SQLException {\n    insertCorrelationId(event, adder, statement);\n  }\n\n  private void insertCorrelationId(\n      ILoggingEvent event, LongAdder adder, PreparedStatement statement) throws SQLException {\n    Optional<String> maybeCorrelationId = utils.get(event.getMarker());\n    if (maybeCorrelationId.isPresent()) {\n      statement.setString(adder.intValue(), maybeCorrelationId.get());\n    } else {\n      statement.setNull(adder.intValue(), Types.VARCHAR);\n    }\n    adder.increment();\n  }\n}\n```\n\nThen set up the table and add an index on the correlation id:\n\n```sql\nCREATE TABLE IF NOT EXISTS events (\n   ID NUMERIC NOT NULL PRIMARY KEY AUTO_INCREMENT,\n   ts TIMESTAMP(9) WITH TIME ZONE NOT NULL,\n   tse_ms numeric NOT NULL,\n   start_ms numeric NULL,\n   level_value int NOT NULL,\n   level VARCHAR(7) NOT NULL,\n   evt JSON NOT NULL,\n   correlation_id VARCHAR(255) NULL\n);\nCREATE INDEX correlation_id_idx ON events(correlation_id);\n```\n\nAnd then you can query from there.\n\nSee [Logging Structured Data to Database](https://tersesystems.com/blog/2019/09/18/logging-structured-data-to-database/) for more details.\n"
  },
  {
    "path": "docs/guide/relativens.md",
    "content": "# Relative Nanoseconds Appender\n\n`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/).\n\nFor example, here's two different records with the same millisecond.\n\n```json\n{\"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}\n{\"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}\n```\n\nNote 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.  \n\nAll 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.  \n\nSee the [showcase](https://github.com/tersesystems/terse-logback-showcase) for an example.\n\n## Installation\n\nAdd the library dependency using [com.tersesystems.logback:logback-classic](https://mvnrepository.com/artifact/com.tersesystems.logback/logback-classic).\n\n## Usage\n\n```xml\n <appender class=\"com.tersesystems.logback.classic.NanoTimeComponentAppender\">\n    <appender ...>\n\n    </appender>\n</appender>\n```\n\nYou can extract the nanotime using a converter:\n\n```xml\n<!-- available as \"%nanoTime\" in a pattern layout -->\n<conversionRule conversionWord=\"nanoTime\"\n                    converterClass=\"com.tersesystems.logback.classic.NanoTimeConverter\" />\n```\n\nThere are no configuration options.\n"
  },
  {
    "path": "docs/guide/select.md",
    "content": "# Select Appender\n\nDifferent appenders are useful in different environments.\n\nDevelopment wants:\n\n* Want colorized output on their consoles, with line oriented logs.  \n* 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.\n* Generally don't want to run a local ELK stack or TCP appenders to see their logs.\n\nOperations wants:\n\n* Really want centralized logging, and a way to drill out on it.  Structured logging especially.\n* May want to have everything write to STDOUT, as is case for Docker / 12 Factor Apps.\n* May have duplicate logs from the underlying architecture, that need to be dedupped.\n* May not want redundant / repeated messages, which developers are not as sensitive to.\n* Really hate getting paged with the same error repeatedly.\n\nLogback 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.\"\n\n## Installation\n\nAdd the library dependency using [com.tersesystems.logback:logback-core](https://mvnrepository.com/artifact/com.tersesystems.logback/logback-core).\n\n## Usage\n\nThe 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.\n\n```xml\n<configuration>\n    <appender name=\"SELECT\" class=\"com.tersesystems.logback.core.SelectAppender\">\n        <appenderKey>${LOGBACK_ENVIRONMENT}</appenderKey>\n\n        <appender class=\"com.tersesystems.logback.core.CompositeAppender\">\n            <name>test</name>\n            <appender class=\"ch.qos.logback.core.read.ListAppender\">\n                <name>test-list</name>\n            </appender>\n        </appender>\n\n        <appender class=\"com.tersesystems.logback.core.CompositeAppender\">\n            <name>development</name>\n            <appender class=\"ch.qos.logback.core.ConsoleAppender\">\n                <name>development-console</name>\n                <encoder>\n                    <pattern>%-5relative %-5level %logger{35} - %msg%n</pattern>\n                </encoder>\n            </appender>\n        </appender>\n\n        <appender class=\"com.tersesystems.logback.core.CompositeAppender\">\n            <name>staging</name>\n            <appender class=\"ch.qos.logback.core.ConsoleAppender\">\n                <name>staging-console</name>\n                <encoder>\n                    <pattern>%-5relative %-5level %logger{35} - %msg%n</pattern>\n                </encoder>\n            </appender>\n\n            <appender class=\"ch.qos.logback.core.FileAppender\">\n                <name>staging-file</name>\n                <file>file.log</file>\n                <encoder>\n                    <pattern>%-5relative %-5level %logger{35} - %msg%n</pattern>\n                </encoder>\n            </appender>\n        </appender>\n    </appender>\n\n    <root level=\"TRACE\">\n        <appender-ref ref=\"SELECT\"/>\n    </root>\n\n</configuration>\n```\n\nThis is a much cleaner way to organize appenders than putting Janino logic into the configuration.\n\n"
  },
  {
    "path": "docs/guide/slf4jbridge.md",
    "content": "# JUL to SLF4J Bridge\n\nIt'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).\n\nWhen 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.\n\n## Installation\n\nAdd the library dependency using [com.tersesystems.logback:logback-classic](https://mvnrepository.com/artifact/com.tersesystems.logback/logback-classic).\n\n## Usage\n\n`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:\n\n```java\nSLF4JBridgeHandler.removeHandlersForRootLogger();\nSLF4JBridgeHandler.install();\n```\n\nThis 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.\n\nThis isn't so easy.  JUL is very basic, and accepts configuration from system properties.  The LogManager has two system properties:\n\n- java.util.logging.config.class\n- java.util.logging.config.file\n\nIf 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/).\n\nThere 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.\n\nHowever, 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.\n\nYou should set your `logback.xml` roughly as follows:\n\n```xml\n<configuration>\n\n    <!-- set up the rule -->\n    <newRule pattern=\"configuration/slf4jBridgeHandler\"\n             actionClass=\"com.tersesystems.logback.classic.SLF4JBridgeHandlerAction\"/>\n\n    <!-- calls removeHandlersForRootLogger / install -->\n    <slf4jBridgeHandler/>\n\n    <!-- reset all previous level configurations of all j.u.l. loggers -->\n    <contextListener class=\"ch.qos.logback.classic.jul.LevelChangePropagator\">\n        <resetJUL>true</resetJUL>\n    </contextListener>\n\n    <!-- Add Guice tracing -->\n    <logger name=\"com.google.inject\" level=\"TRACE\"/>\n\n    <root level=\"INFO\">\n        <appender class=\"ch.qos.logback.core.ConsoleAppender\">\n            <encoder>\n                <pattern>%date{H:mm:ss.SSS} [%highlight(%-5level)] %logger -  %message%ex%n</pattern>\n            </encoder>\n        </appender>\n    </root>\n</configuration>\n```\n\nAnd then you should call `org.slf4j.LoggerFactory.getLogger` as a static final to prevent any initialization problems:\n\n```java\npackage example;\n\nimport com.google.inject.*;\nimport org.slf4j.*;\n\npublic class App {\n    // Ensure that logback.xml is parsed by LoggerFactory _before_ Guice calls JUL.\n    private static final Logger logger = org.slf4j.LoggerFactory.getLogger(App.class);\n\n    public String getGreeting() {\n        return \"Hello World!\";\n    }\n\n    public static void main(String[] args) {\n        final Injector injector = Guice.createInjector();\n        final App instance = injector.getInstance(App.class);\n        logger.info(instance.getGreeting());\n    }\n}\n```\n\nAnd that should render the following:\n\n```\n19:51:45.493 [DEBUG] com.google.inject.internal.util.ContinuousStopwatch -  Module execution: 64ms\n19:51:45.494 [DEBUG] com.google.inject.internal.util.ContinuousStopwatch -  Interceptors creation: 2ms\n19:51:45.496 [DEBUG] com.google.inject.internal.util.ContinuousStopwatch -  TypeListeners & ProvisionListener creation: 1ms\n19:51:45.511 [DEBUG] com.google.inject.internal.util.ContinuousStopwatch -  Scopes creation: 15ms\n19:51:45.511 [DEBUG] com.google.inject.internal.util.ContinuousStopwatch -  Converters creation: 0ms\n19:51:45.514 [DEBUG] com.google.inject.internal.util.ContinuousStopwatch -  Binding creation: 2ms\n19:51:45.515 [DEBUG] com.google.inject.internal.util.ContinuousStopwatch -  Module annotated method scanners creation: 0ms\n19:51:45.515 [DEBUG] com.google.inject.internal.util.ContinuousStopwatch -  Private environment creation: 0ms\n19:51:45.515 [DEBUG] com.google.inject.internal.util.ContinuousStopwatch -  Injector construction: 0ms\n19:51:45.515 [DEBUG] com.google.inject.internal.util.ContinuousStopwatch -  Binding initialization: 0ms\n19:51:45.515 [DEBUG] com.google.inject.internal.util.ContinuousStopwatch -  Binding indexing: 0ms\n19:51:45.515 [DEBUG] com.google.inject.internal.util.ContinuousStopwatch -  Collecting injection requests: 0ms\n19:51:45.515 [DEBUG] com.google.inject.internal.util.ContinuousStopwatch -  Binding validation: 0ms\n19:51:45.515 [DEBUG] com.google.inject.internal.util.ContinuousStopwatch -  Static validation: 0ms\n19:51:45.515 [DEBUG] com.google.inject.internal.util.ContinuousStopwatch -  Instance member validation: 0ms\n19:51:45.516 [DEBUG] com.google.inject.internal.util.ContinuousStopwatch -  Provider verification: 0ms\n19:51:45.516 [DEBUG] com.google.inject.internal.util.ContinuousStopwatch -  Delayed Binding initialization: 0ms\n19:51:45.516 [DEBUG] com.google.inject.internal.util.ContinuousStopwatch -  Static member injection: 0ms\n19:51:45.516 [DEBUG] com.google.inject.internal.util.ContinuousStopwatch -  Instance injection: 0ms\n19:51:45.516 [DEBUG] com.google.inject.internal.util.ContinuousStopwatch -  Preloading singletons: 0ms\n19:51:45.545 [INFO ] example.App -  Hello World!\n```\n"
  },
  {
    "path": "docs/guide/tracing.md",
    "content": "# Tracing to Honeycomb\n\nYou 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.  \n\nBear 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.\n\nHowever, 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.\n\n## Implementation\n\nAdd the library dependency using [com.tersesystems.logback:logback-honeycomb-okhttp](https://mvnrepository.com/artifact/com.tersesystems.logback/logback-honeycomb-okhttp) for the honeycomb appender.\n\nTo 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.\n\n## Usage\n\nThe 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.\n\n> **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.\n\nThe appender is as follows:\n\n```xml\n<configuration>\n  <shutdownHook class=\"ch.qos.logback.core.hook.DelayingShutdownHook\">\n    <delay>1000</delay>\n  </shutdownHook>\n\n  <conversionRule conversionWord=\"startTime\" converterClass=\"com.tersesystems.logback.classic.StartTimeConverter\" />\n\n  <appender name=\"HONEYCOMB\" class=\"com.tersesystems.logback.honeycomb.HoneycombAppender\">\n      <apiKey>${HONEYCOMB_API_KEY}</apiKey>\n      <dataSet>terse-logback</dataSet>\n      <sampleRate>1</sampleRate>\n      <queueSize>10</queueSize>\n      <batch>true</batch>\n      <includeCallerData>false</includeCallerData>\n\n      <encoder class=\"net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder\">\n          <providers>\n              <message/>\n              <loggerName/>\n              <threadName/>\n              <logLevel/>\n              <stackHash/>\n              <mdc/>\n              <logstashMarkers/>\n              <pattern>\n                <pattern>\n                    { \"start_ms\": \"#asLong{%startTime}\" }\n                </pattern>\n             </pattern>\n              <arguments/>\n              <stackTrace>\n                  <throwableConverter class=\"net.logstash.logback.stacktrace.ShortenedThrowableConverter\">\n                      <rootCauseFirst>true</rootCauseFirst>\n                  </throwableConverter>\n              </stackTrace>\n          </providers>\n      </encoder>\n  </appender>\n\n  <!-- \n    don't send the logs from the http engine to the appender or you\n    may end up in a loop\n  -->\n  <logger name=\"okhttp\" level=\"ERROR\"/>\n\n  <root level=\"INFO\">\n      <appender-ref ref=\"HONEYCOMB\" />\n  </root>\n</configuration>\n```\n\nYou 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).\n\nThe 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:\n\n```java\nSpanInfo spanInfo = builder.setRootSpan(\"index\").buildNow();\n// ...\nlogger.info(markerFactory.apply(spanInfo), \"completed successfully!\");\n```\n\nIf you want to create a child span, you can do it from the parent using `withChild`:\n\n```java\nreturn spanInfo.withChild(\"doSomething\", childInfo -> {\n   return doSomething(childInfo);\n});\n```\n\nor asking for a child builder that you can build yourself:\n\n```java\nSpanInfo childInfo = spanInfo.childBuilder().setSpanName(\"doSomething\").buildNow();\n```\n\nThe 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.\n\nFor example, in Play you might run a controller as follows:\n\n```scala\nimport com.tersesystems.logback.tracing.SpanMarkerFactory\nimport com.tersesystems.logback.tracing.SpanInfo\nimport javax.inject._\nimport org.slf4j.LoggerFactory\nimport play.api.libs.concurrent.Futures\nimport play.api.mvc._\n\nimport scala.concurrent.{ExecutionContext, Future}\nimport scala.util.{Failure, Success}\n\nimport scala.concurrent.duration._\n\n@Singleton\nclass HomeController @Inject()(cc: ControllerComponents, futures: Futures)\n  (implicit ec: ExecutionContext) extends AbstractController(cc) {\n  private val markerFactory = new SpanMarkerFactory()\n\n  private val logger = LoggerFactory.getLogger(getClass)\n\n  private def builder: SpanInfo.Builder = SpanInfo.builder().setServiceName(\"play_hello_world\")\n\n  def index(): Action[AnyContent] = Action.async { implicit request: Request[AnyContent] =>\n    val spanInfo = builder.setRootSpan(\"index\").buildNow()\n\n    val f: Future[Result] = spanInfo.withChild(\"renderPage\", renderPage(_))\n    f.andThen {\n      case Success(_) =>\n        logger.info(markerFactory(spanInfo), \"index completed successfully!\")\n      case Failure(e) =>\n        logger.error(markerFactory(spanInfo), \"index completed with error\", e)\n    }\n  }\n\n  def renderPage(spanInfo: SpanInfo): Future[Result] = {\n    futures.delay(5.seconds).map { _ =>\n      Ok(views.html.index())\n    }.andThen {\n       case Success(_) =>\n         logger.info(markerFactory(spanInfo), \"renderPage completed successfully!\")\n       case Failure(e) =>\n         logger.error(markerFactory(spanInfo), \"renderPage completed with error\", e)\n    }\n  }\n}\n```\n\nThis generates a trace with a root span of \"index\", a child span of \"renderPage\" each with their own durations.\n\nYou 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`.\n\nSee [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.\n"
  },
  {
    "path": "docs/guide/turbomarker.md",
    "content": "# Turbo Markers\n\n[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.\n\nWhat 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.\"\n\n## Installation\n\nAdd the library dependency using [com.tersesystems.logback:logback-turbomarker](https://mvnrepository.com/artifact/com.tersesystems.logback/logback-turbomarker).\n\n## Usage\n\nWe 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):\n\n```java\npackage com.tersesystems.logback.classic;\n\npublic interface TurboFilterDecider {\n    FilterReply decide(Marker marker, Logger logger, Level level, String format, Object[] params, Throwable t);\n}\n```\n\nAnd 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):\n\n```java\npackage com.tersesystems.logback.turbomarker;\n\npublic class TurboMarkerTurboFilter extends TurboFilter {\n\n    @Override\n    public FilterReply decide(Marker rootMarker, Logger logger, Level level, String format, Object[] params, Throwable t) {\n        // ...\n    }\n\n    private FilterReply evaluateMarker(Marker marker, Marker rootMarker, Logger logger, Level level, String format, Object[] params, Throwable t) {\n        if (marker instanceof TurboFilterDecider) {\n            TurboFilterDecider decider = (TurboFilterDecider) marker;\n            return decider.decide(rootMarker, logger, level, format, params, t);\n        }\n        return FilterReply.NEUTRAL;\n    }\n}\n```\n\nThis 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.\n\n```java\npublic interface ContextAwareTurboFilterDecider<C> {\n    FilterReply decide(ContextAwareTurboMarker<C> marker, C context, Marker rootMarker, Logger logger, Level level, String format, Object[] params, Throwable t);\n}\n```\n\nThen 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):\n\n```java\npublic class ContextAwareTurboMarker<C> extends TurboMarker implements TurboFilterDecider {\n    private final C context;\n    private final ContextAwareTurboFilterDecider<C> contextAwareDecider;\n    // ... initializers and such\n    @Override\n    public FilterReply decide(Marker rootMarker, Logger logger, Level level, String format, Object[] params, Throwable t) {\n        return contextAwareDecider.decide(this, context, rootMarker, logger, level, format, params, t);\n    }\n}\n```\n\nThis 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:\n\n```java\npublic class ApplicationContext {\n\n    private final String userId;\n\n    public ApplicationContext(String userId) {\n        this.userId = userId;\n    }\n\n    public String currentUserId() {\n        return userId;\n    }\n}\n```\n\nand a factory that contains the decider:\n\n```java\nimport com.tersesystems.logback.turbomarker.*;\n\npublic class UserMarkerFactory {\n\n    private final Set<String> userIdSet = new ConcurrentSkipListSet<>();\n\n    private final ContextDecider<ApplicationContext> decider = context ->\n        userIdSet.contains(context.currentUserId()) ? FilterReply.ACCEPT : FilterReply.NEUTRAL;\n\n    public void addUserId(String userId) {\n        userIdSet.add(userId);\n    }\n\n    public void clear() {\n        userIdSet.clear();\n    }\n\n    public UserMarker create(ApplicationContext applicationContext) {\n        return new UserMarker(\"userMarker\", applicationContext, decider);\n    }\n}\n```\n\nand a `UserMarker`, which is only around for the logging evaluation:\n\n```java\npublic class UserMarker extends ContextAwareTurboMarker<ApplicationContext> {\n    public UserMarker(String name,\n                      ApplicationContext applicationContext,\n                      ContextAwareTurboFilterDecider<ApplicationContext> decider) {\n        super(name, applicationContext, decider);\n    }\n}\n```\n\nand then we can set up logging that will only work for user \"28\":\n\n```java\nString userId = \"28\";\nApplicationContext applicationContext = new ApplicationContext(userId);\nUserMarkerFactory userMarkerFactory = new UserMarkerFactory();\nuserMarkerFactory.addUserId(userId); // say we want logging events created for this user id\n\nUserMarker userMarker = userMarkerFactory.create(applicationContext);\n\nlogger.info(userMarker, \"Hello world, I am info and log for everyone\");\nlogger.debug(userMarker, \"Hello world, I am debug and only log for user 28\");\n```\n\nThis 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.  \n\nThe LaunchDarkly blog has [best practices for operational flags](https://launchdarkly.com/blog/operational-flags-best-practices/):\n\n> 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.\n\nBut we can give an example using the Java SDK.  You can set up a factory like so:\n\n```java\nimport ch.qos.logback.classic.Logger;\nimport ch.qos.logback.core.spi.FilterReply;\nimport com.launchdarkly.client.LDClientInterface;\nimport com.launchdarkly.client.LDUser;\n\npublic class LDMarkerFactory {\n    private final LaunchDarklyDecider decider;\n\n    public LDMarkerFactory(LDClientInterface client) {\n        this.decider = new LaunchDarklyDecider(requireNonNull(client));\n    }\n\n    public LDMarker create(String featureFlag, LDUser user) {\n        return new LDMarker(featureFlag, user, decider);\n    }\n\n    static class LaunchDarklyDecider implements MarkerContextDecider<LDUser> {\n        private final LDClientInterface ldClient;\n\n        LaunchDarklyDecider(LDClientInterface ldClient) {\n            this.ldClient = ldClient;\n        }\n\n        @Override\n        public FilterReply apply(ContextAwareTurboMarker<LDUser> marker, LDUser ldUser) {\n            return ldClient.boolVariation(marker.getName(), ldUser, false) ?\n                    FilterReply.ACCEPT :\n                    FilterReply.NEUTRAL;\n        }\n    }\n\n    public static class LDMarker extends ContextAwareTurboMarker<LDUser> {\n        LDMarker(String name, LDUser context, ContextAwareTurboFilterDecider<LDUser> decider) {\n            super(name, context, decider);\n        }\n    }\n}\n```\n\nand then use the feature flag as the marker name and target the beta testers group:\n\n```java\npublic class LDMarkerTest {\n  private static LDClientInterface client;\n\n  @BeforeAll\n  public static void setUp() {\n      client = new LDClient(\"sdk-key\");\n  }\n\n  @AfterAll\n  public static void shutDown() {\n    try {\n      client.close();\n    } catch (IOException e) {\n      e.printStackTrace();\n    }\n  }\n\n  public void testMatchingMarker() throws JoranException {\n    LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory();\n    ch.qos.logback.classic.Logger logger = loggerContext.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME);\n\n    LDMarkerFactory markerFactory = new LDMarkerFactory(client);\n    LDUser ldUser = new LDUser.Builder(\"UNIQUE IDENTIFIER\")\n            .firstName(\"Bob\")\n            .lastName(\"Loblaw\")\n            .customString(\"groups\", singletonList(\"beta_testers\"))\n            .build();\n\n    LDMarkerFactory.LDMarker ldMarker = markerFactory.create(\"turbomarker\", ldUser);\n\n    logger.info(ldMarker, \"Hello world, I am info\");\n    logger.debug(ldMarker, \"Hello world, I am debug\");\n\n    ListAppender<ILoggingEvent> appender = (ListAppender<ILoggingEvent>) logger.getAppender(\"LIST\");\n    assertThat(appender.list.size()).isEqualTo(2);\n\n    appender.list.clear();\n  }\n}\n```\n\nThis 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.\n\nSee [Targeted Diagnostic Logging in Production](https://tersesystems.com/blog/2019/07/22/targeted-diagnostic-logging-in-production/) for more details.\n"
  },
  {
    "path": "docs/guide/typesafeconfig.md",
    "content": "# Typesafe Config\n\nThe `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.\n\n## Installation\n\nAdd the library dependency using [com.tersesystems.logback:logback-typesafe-config](https://mvnrepository.com/artifact/com.tersesystems.logback/logback-typesafe-config).\n\n## Usage\n\nThe configuration is derived as follows:\n\n```java\nConfig config = systemProperties        // Look for a property from system properties first...\n        .withFallback(file)          // if we don't find it, then look in an explicitly defined file...\n        .withFallback(testResources) // if not, then if logback-test.conf exists, look for it there...\n        .withFallback(resources)     // then look in logback.conf...\n        .withFallback(reference)     // and then finally in logback-reference.conf.\n        .resolve();                  // Tell config that we want to use ${?ENV_VAR} type stuff.\n```\n\nThe configuration is then placed in the `LoggerContext` which is available to all of Logback.\n\n```java\nlc.putObject(ConfigConstants.TYPESAFE_CONFIG_CTX_KEY, config);\n```\n\nAnd then all properties are made available to Logback, either at the `local` scope or at the `context` scope.\n\nProperties must be strings, but you can also provide Maps and Lists to the Logback Context, through `context.getObject`.\n\n### Log Levels and Properties through Typesafe Config\n\nConfiguration of properties and setting log levels is done through [Typesafe Config](https://github.com/lightbend/config#overview), using `TypesafeConfigAction`\n\nHere'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).\n\n```hocon\n# Set logger levels here.\nlevels = {\n    # Override the default root log level with ROOT_LOG_LEVEL environment variable, if defined...\n    ROOT = ${?ROOT_LOG_LEVEL}\n\n    # You can set a logger with a simple package name.\n    example = DEBUG\n\n    # You can also do nested overrides here.\n    deeply.nested {\n        package = TRACE\n    }\n}\n\n# Overrides the properties from logback-reference.conf\nlocal {\n\n    logback.environment=production\n\n    censor {\n        regex = \"\"\"hunter2\"\"\" // http://bash.org/?244321\n        replacementText = \"*******\"\n        json.keys += \"password\" // adding password key will remove the key/value pair entirely\n    }\n\n    # Overwrite text file on every run.\n    textfile {\n        append = false\n    }\n\n    # Override the color code in console for info statements\n    highlight {\n        info = \"black\"\n    }\n}\n\n# You can also include settings from other places\ninclude \"myothersettings\"\n```\n\nFor tests, there's a `logback-test.conf` that will override (rather than completely replace) any settings that you have in `logback.conf`:\n\n```hocon\ninclude \"logback-reference\"\n\nlevels {\n  example = TRACE\n}\n\nlocal {\n  logback.environment=test\n\n  textfile {\n    location = \"log/test/application-test.log\"\n    append = false\n  }\n\n  jsonfile {\n    location = \"log/test/application-test.json\"\n    prettyprint = true\n  }\n}\n```\n\nThere 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.\n\nNote that appender logic is not available here -- it's all defined through the `structured-config` in `logback.xml`.\n\nUsing 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.\n\nSee [Application Logging in Java: Adding Configuration](https://tersesystems.com/blog/2019/05/05/application-logging-in-java-part-2/) for more details.\n"
  },
  {
    "path": "docs/guide/uniqueid.md",
    "content": "# Unique ID Appenders\n\nThe 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.\n\nFor example, in `application.log`, you'll see a single line that starts with `FfwJtsNHYSw6O0Qbm7EAAA`:\n\n```text\nFfwJtsNHYSw6O0Qbm7EAAA 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'\n```\n\nYou can search for this string in `application.json` and see more detail on the log record:\n\n```json\n{\"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}\n```\n\nSee the [showcase](https://github.com/tersesystems/terse-logback-showcase) for an example.\n\n## Installation\n\nAdd the library dependency using [com.tersesystems.logback:logback-uniqueid-appender](https://mvnrepository.com/artifact/com.tersesystems.logback/logback-uniqueid-appender).\n\n## Usage\n\n```xml\n<appender name=\"selector-with-unique-id\" class=\"com.tersesystems.logback.uniqueid.UniqueIdComponentAppender\">\n  <appender ...>\n\n  </appender>\n</appender>\n```\n\nTo extract the unique ID, register a converter:\n\n```xml\n<!-- available as \"%uniqueId\" in a pattern layout -->\n<conversionRule conversionWord=\"uniqueId\" converterClass=\"com.tersesystems.logback.uniqueid.UniqueIdConverter\" />\n```\n\n## ID Generators\n\nUnique IDs come with several options.  Flake ID is the default.\n\n### Flake ID Generator\n\nFlake IDs are decentralized and k-ordered, meaning that they are \"roughly time-ordered when sorted lexicographically.\"\n\nThis implementation uses [idem](https://github.com/mguenther/idem) with `Flake128S`.\n\n```xml\n<appender name=\"selector-with-unique-id\" class=\"com.tersesystems.logback.uniqueid.UniqueIdComponentAppender\">\n  <idGenerator class=\"com.tersesystems.logback.uniqueid.FlakeIdGenerator\"/>\n  <!-- ... -->\n</appender>\n```\n\n### Random UUID Generator\n\nGenerates a Random UUIDv4 using a ThreadLocalRandom according to <a href=\"https://github.com/f4b6a3/uuid-creator\">https://github.com/f4b6a3/uuid-creator</a>.\n\n```xml\n<appender name=\"selector-with-unique-id\" class=\"com.tersesystems.logback.uniqueid.UniqueIdComponentAppender\">\n  <idGenerator class=\"com.tersesystems.logback.uniqueid.RandomUUIDIdGenerator\"/>\n  <!-- ... -->\n</appender>\n```\n\n### TSID Generator\n\nGenerates a TSID according to <a href=\"https://github.com/f4b6a3/tsid-creator\">https://github.com/f4b6a3/tsid-creator</a>.\n\n**Highly recommended to set a *tsidcreator.node* system property in your application to configure the node id.**\n\n```xml\n<appender name=\"selector-with-unique-id\" class=\"com.tersesystems.logback.uniqueid.UniqueIdComponentAppender\">\n  <idGenerator class=\"com.tersesystems.logback.uniqueid.TsidIdgenerator\"/>\n  <!-- ... -->\n</appender>\n```\n\n### ULID Generator\n\nCreates a monotonic ULID using a threadlocal random according to <a href=\"https://github.com/f4b6a3/ulid-creator\">https://github.com/f4b6a3/ulid-creator</a>.\n\n```xml\n<appender name=\"selector-with-unique-id\" class=\"com.tersesystems.logback.uniqueid.UniqueIdComponentAppender\">\n  <idGenerator class=\"com.tersesystems.logback.uniqueid.UlidIdGenerator\"/>\n  <!-- ... -->\n</appender>\n```\n\n### KSU ID Generator\n\nCreates a subsecond KSUID according to <a href=\"https://github.com/f4b6a3/ksuid-creator\">https://github.com/f4b6a3/ksuid-creator</a>.\n\n```xml\n<appender name=\"selector-with-unique-id\" class=\"com.tersesystems.logback.uniqueid.UniqueIdComponentAppender\">\n  <idGenerator class=\"com.tersesystems.logback.uniqueid.KsuidSubsecondIdGenerator\"/>\n  <!-- ... -->\n</appender>\n```\n\n"
  },
  {
    "path": "docs/index.md",
    "content": "# Terse Logback\n\nTerse Logback is a collection of [Logback](https://logback.qos.ch/) modules that extend [Logback](https://logback.qos.ch/manual/index.html) functionality.  \n\nI'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).\n\n## Showcase\n\nIf 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).\n\n## Modules\n\n- [Audio](guide/audio.md): Play audio when you log by attaching markers to your logging statements.\n- [Budgeting / Rate Limiting](guide/budget.md): Limit the amount of debugging or tracing statements in a time period.\n- [Censors](guide/censor.md): Censor sensitive information in logging statements.     \n- [Composite](guide/composite.md): Presents a single appender that composes several appenders. \n- [Compression](guide/compression.md): Write to a compressed zstandard file. \n- [Correlation Id](guide/correlationid.md): Adds markers and filters for correlation id.\n- [Exception Mapping](guide/exception-mapping.md): Show the important details of an exception, including the root cause in a summary format.\n- [Instrumentation](guide/instrumentation.md): Decorates any (including JVM) class with enter and exit logging statements at runtime.\n- [JDBC](guide/jdbc.md): Use Postgres JSON to write structured logging to a single table.\n- [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/).\n- [Relative Nanos](guide/relativens.md): Composes a logging event to contain relative nanoseconds based off `System.nanoTime`.\n- [Select Appender](guide/select.md): Appender that selects an appender from a list based on key.\n- [Tracing](guide/tracing.md): Sends logging events and traces to [Honeycomb Event API](https://docs.honeycomb.io/api/events/).\n- [Typesafe Config](guide/typesafeconfig.md): Configure Logback properties using [HOCON](https://github.com/lightbend/config/blob/main/HOCON.md).\n- [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. \n- [Unique ID Appender](guide/uniqueid.md): Composes logging event to contain a unique id across multiple appenders. \n"
  },
  {
    "path": "docs/reading/reading.md",
    "content": "# Further Reading\n\nEverything I write on logging is going to be here:\n\n* [Terse Systems](https://tersesystems.com/category/logging/)\n\n### Best Practices\n\nMany of these are logback specific, but still good overall.\n\n* [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)\n* [Woofer: logging in (best) practices](https://orange-opensource.github.io/woofer/logging-code/): Spring Boot\n* [A whole product concern logging implementation](http://stevetarver.github.io/2016/04/20/whole-product-logging.html)\n* [There is more to logging than meets the eye](https://allegro.tech/2015/10/there-is-more-to-logging-than-meets-the-eye.html)\n* [Monitoring demystified: A guide for logging, tracing, metrics](https://techbeacon.com/enterprise-it/monitoring-demystified-guide-logging-tracing-metrics)\n* [Application-Level Logging Best Practices](https://news.ycombinator.com/item?id=19497788)\n\nStack Overflow has a couple of good tips on SLF4J and Logging:\n\n* [When to use the different log levels](https://stackoverflow.com/questions/2031163/when-to-use-the-different-log-levels)\n* [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)\n* [Best practices for using Markers in SLF4J/Logback](https://stackoverflow.com/questions/4165558/best-practices-for-using-markers-in-slf4j-logback)\n* [Stackoverflow: Logging best practices in multi-node environment](https://stackoverflow.com/questions/43496695/java-logging-best-practices-in-multi-node-environment)\n\n#### Level Up Logs\n\n[Alberto Navarro](https://looking4q.blogspot.com/) has a great series\n\n<ol>\n<li><a href=\"http://looking4q.blogspot.com/2018/09/level-up-logs-and-elk-introduction.html\">Introduction</a> (Everyone)</li>\n<li><a href=\"http://looking4q.blogspot.com/2018/09/level-up-your-logs-and-elk-json-logs.html\">JSON as logs format</a> (Everyone)</li>\n<li><b><a href=\"http://looking4q.blogspot.com/2018/09/level-up-logs-and-elk-logging-best.html\">Logging best practices with Logback</a> (Targetting Java DEVs)</b></li>\n<li><a href=\"https://looking4q.blogspot.com/2018/11/logging-cutting-edge-practices.html\">Logging cutting-edge practices</a> (Targetting Java DEVs)&nbsp;</li>\n<li><a href=\"https://looking4q.blogspot.com/2019/01/level-up-logs-and-elk-contract-first.html\">Contract first log generator</a> (Targetting Java DEVs) </li>\n<li><a href=\"http://looking4q.blogspot.com/2018/09/level-up-logs-and-elk-elasticsearch.html\">ElasticSearch VRR Estimation Strategy</a> (Targetting OPS)</li>\n<li><a href=\"http://looking4q.blogspot.com/2018/09/level-up-logs-and-elk-vrr-java-logback.html\">VRR Java + Logback configuration</a> (Targetting OPS)</li>\n<li><a href=\"http://looking4q.blogspot.com/2018/09/level-up-logs-and-elk-vrr-filebeat.html\">VRR FileBeat configuration</a> (Targetting OPS)</li>\n<li><a href=\"http://looking4q.blogspot.com/2018/09/level-up-logs-and-elk-vrr-logstash.html\">VRR Logstash configuration and Index templates</a> (Targetting OPS)</li>\n<li><a href=\"http://looking4q.blogspot.com/2018/09/level-up-logs-and-elk-vrr-curator.html\">VRR Curator configuration</a> (Targetting OPS)</li>\n<li><a href=\"https://looking4q.blogspot.com/2018/10/level-up-logs-and-elk-logstash-grok.html\">Logstash Grok, JSON Filter and JSON Input performance comparison</a> (Targetting OPS) </li>\n</ol>\n\n#### Logging Anti Patterns\n\nLogging Anti-Patterns by [Rolf Engelhard](https://rolf-engelhard.de/):\n\n* [Logging Anti-Patterns](http://rolf-engelhard.de/2013/03/logging-anti-patterns-part-i/)\n* [Logging Anti-Patterns, Part II](http://rolf-engelhard.de/2013/04/logging-anti-patterns-part-ii/)\n* [Logging Anti-Patterns, Part III](https://rolf-engelhard.de/2013/10/logging-anti-patterns-part-iii/)\n\n#### Clean Code, clean logs\n\n[Tomasz Nurkiewicz](https://www.nurkiewicz.com/) has a great series on logging:\n\n* [Clean code, clean logs: use appropriate tools (1/10)](https://www.nurkiewicz.com/2010/05/clean-code-clean-logs-use-appropriate.html)\n* [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)\n* [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)\n* [Clean code, clean logs: avoid side effects (4/10)](https://www.nurkiewicz.com/2010/05/clean-code-clean-logs-avoid-side.html)\n* [Clean code, clean logs: concise and descriptive (5/10)](https://www.nurkiewicz.com/2010/05/clean-code-clean-logs-concise-and.html)\n* [Clean code, clean logs: tune your pattern (6/10)](https://www.nurkiewicz.com/2010/05/clean-code-clean-logs-tune-your-pattern.html)\n* [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)\n* [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)\n* [Clean code, clean logs: log exceptions properly (9/10)](https://www.nurkiewicz.com/2010/05/clean-code-clean-logs-log-exceptions.html)\n* [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)\n* [Condensed 10 Tips on javacodegeeks](https://www.javacodegeeks.com/2011/01/10-tips-proper-application-logging.html)\n\n### JSON Logging\n\n* [Logging in JSON](http://www.asynchronous.org/blog/archives/2006/01/25/logging-in-json)\n* [Write Logs for Machines, not Humans](https://paul.querna.org/articles/2011/12/26/log-for-machines-in-json/)\n\n### Maple\n\n* [Maple](https://github.com/Randgalt/maple)\n\n### Eliot\n\n* [Eliot](https://eliot.readthedocs.io/en/stable/quickstart.html)\n* [Eliot Tree](https://github.com/jonathanj/eliottree)\n\n### TreeLog\n\n* [Treelog](https://github.com/lancewalton/treelog)\n\n### Bunyan\n\nBunyan stands out for a number of innovations: ring buffers and JSON specifically.\n\n* [Bunyan](https://timboudreau.com/blog/bunyan/read)\n* [Comparison of Winston and Bunyan](https://strongloop.com/strongblog/compare-node-js-logging-winston-bunyan/)\n* [Service logging in JSON with Bunyan](https://trentm.com/2012/03/service-logging-in-json-with-bunyan.html)\n* [Bunyan Logging in Production at Joyent](https://trentm.com/talk-bunyan-in-prod/#/8)\n\n### Timbre\n\n* [Timbre](https://github.com/ptaoussanis/timbre/blob/master/README.md)\n\n### Logback Encoders and Appenders\n\n* [concurrent-build-logger](https://github.com/takari/concurrent-build-logger) (encoders and appenders both)\n* [logzio-logback-appender](https://github.com/logzio/logzio-logback-appender)\n* [logback-elasticsearch-appender](https://github.com/internetitem/logback-elasticsearch-appender)\n* [logback-more-appenders](https://github.com/sndyuk/logback-more-appenders)\n* [logback-steno](https://github.com/ArpNetworking/logback-steno)\n* [logslack](https://github.com/gmethvin/logslack)\n* [Lessons Learned Writing New Logback Appender](https://logz.io/blog/lessons-learned-writing-new-logback-appender/)\n* [Extending logstash-logback-encoder](https://zenidas.wordpress.com/recipes/extending-logstash-logback-encoder/)\n"
  },
  {
    "path": "gradle/LICENSE_HEADER",
    "content": "SPDX-License-Identifier: CC0-1.0\n\nCopyright ${copyrightYear} ${author}.\n\nLicensed under the CC0 Public Domain Dedication;\nYou may obtain a copy of the License at\n\n    http://creativecommons.org/publicdomain/zero/1.0/\n\n"
  },
  {
    "path": "gradle/java-publication.gradle",
    "content": "//Auxiliary jar files required by Maven module publications\ntask sourcesJar(type: Jar, dependsOn: classes) {\n    archiveClassifier = 'sources'\n    from sourceSets.main.allSource\n}\n\n//TODO: java.withSourcesJar(), java.withJavadocJar()\ntask javadocJar(type: Jar, dependsOn: javadoc) {\n    archiveClassifier = 'javadoc'\n    from javadoc.destinationDir\n}\n\napply plugin: 'maven-publish'\npublishing { //https://docs.gradle.org/current/userguide/publishing_maven.html\n    publications {\n        maven(MavenPublication) { //name of the publication\n            from components.java\n            artifact sourcesJar\n            artifact javadocJar\n\n            pom {\n                name = tasks.jar.archiveBaseName\n                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.\"\n                url = \"https://github.com/tersesystems/terse-logback\"\n                licenses {\n                    license {\n                        name = 'Apache2'\n                        url = 'https://github.com/tersesystems/terse-logback/blob/master/LICENSE'\n                    }\n                }\n                developers {\n                    developer {\n                        id = 'tersesystems'\n                        name = 'Terse Systems'\n                        url = 'https://github.com/tersesystems'\n                    }\n                }\n                scm {\n                    url = 'https://github.com/tersesystems/terse-logback.git'\n                }\n            }\n        }\n    }\n\n    repositories {\n        // useful for testing - running \"publish\" will create artifacts/pom in a local dir\n        maven { url = \"$rootDir/build/repo\" }\n    }\n}\n\napply plugin: 'signing'\nsigning { // https://docs.gradle.org/current/userguide/signing_plugin.html\n    // Give up on using PGP_KEY, leverage gpg-agent and release locally\n    useGpgCmd()\n    sign publishing.publications.maven\n}\n"
  },
  {
    "path": "gradle/release.gradle",
    "content": "apply plugin: \"io.github.gradle-nexus.publish-plugin\" //https://github.com/gradle-nexus/publish-plugin/\nnexusPublishing {\n    repositories {\n        if (System.getenv(\"SONATYPE_PWD\")) {\n            sonatype {\n                username = System.getenv(\"SONATYPE_USER\")\n                password = System.getenv(\"SONATYPE_PWD\")\n            }\n        }\n    }\n}\n\nallprojects { p ->\n    plugins.withId(\"java\") {\n        p.apply from: \"$rootDir/gradle/java-publication.gradle\"\n    }\n}"
  },
  {
    "path": "gradle/wrapper/gradle-wrapper.properties",
    "content": "#\n# SPDX-License-Identifier: CC0-1.0\n#\n# Copyright 2018-2020 Will Sargent.\n#\n# Licensed under the CC0 Public Domain Dedication;\n# You may obtain a copy of the License at\n#\n#  http://creativecommons.org/publicdomain/zero/1.0/\n#\n\n#Fri Mar 06 17:39:40 PST 2020\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-6.8-all.zip\ndistributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\nzipStorePath=wrapper/dists\nzipStoreBase=GRADLE_USER_HOME\n"
  },
  {
    "path": "gradle.properties",
    "content": "#\n# SPDX-License-Identifier: CC0-1.0\n#\n# Copyright 2018-2020 Will Sargent.\n#\n# Licensed under the CC0 Public Domain Dedication;\n# You may obtain a copy of the License at\n#\n#  http://creativecommons.org/publicdomain/zero/1.0/\n#\n\ngroup          = com.tersesystems.logback\n\norg.gradle.caching=true\n\n# Set to true to attach a debugger\norg.gradle.debug=false\n\n# Set the memory size of the daemon to be higher because animalsniffer needs it.\n#org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8\n\nbytebuddyVersion     = 1.11.5\njunitVersion         = 4.12\njunitJupiterVersion  = 5.0.1\njunitVintageVersion  = 4.12.1\njunitPlatformVersion = 1.0.1\nslf4jVersion         = 1.7.36\nlogstashVersion      = 6.3\nlogbackVersion       = 1.2.10\nconfigVersion        = 1.4.0\nzstdVersion          = 1.5.2-2\n"
  },
  {
    "path": "gradlew",
    "content": "#!/usr/bin/env sh\n\n#\n# Copyright 2015 the original author or authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      https://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n##############################################################################\n##\n##  Gradle start up script for UN*X\n##\n##############################################################################\n\n# Attempt to set APP_HOME\n# Resolve links: $0 may be a link\nPRG=\"$0\"\n# Need this for relative symlinks.\nwhile [ -h \"$PRG\" ] ; do\n    ls=`ls -ld \"$PRG\"`\n    link=`expr \"$ls\" : '.*-> \\(.*\\)$'`\n    if expr \"$link\" : '/.*' > /dev/null; then\n        PRG=\"$link\"\n    else\n        PRG=`dirname \"$PRG\"`\"/$link\"\n    fi\ndone\nSAVED=\"`pwd`\"\ncd \"`dirname \\\"$PRG\\\"`/\" >/dev/null\nAPP_HOME=\"`pwd -P`\"\ncd \"$SAVED\" >/dev/null\n\nAPP_NAME=\"Gradle\"\nAPP_BASE_NAME=`basename \"$0\"`\n\n# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\nDEFAULT_JVM_OPTS='\"-Xmx64m\" \"-Xms64m\"'\n\n# Use the maximum available, or set MAX_FD != -1 to use that value.\nMAX_FD=\"maximum\"\n\nwarn () {\n    echo \"$*\"\n}\n\ndie () {\n    echo\n    echo \"$*\"\n    echo\n    exit 1\n}\n\n# OS specific support (must be 'true' or 'false').\ncygwin=false\nmsys=false\ndarwin=false\nnonstop=false\ncase \"`uname`\" in\n  CYGWIN* )\n    cygwin=true\n    ;;\n  Darwin* )\n    darwin=true\n    ;;\n  MINGW* )\n    msys=true\n    ;;\n  NONSTOP* )\n    nonstop=true\n    ;;\nesac\n\nCLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar\n\n# Determine the Java command to use to start the JVM.\nif [ -n \"$JAVA_HOME\" ] ; then\n    if [ -x \"$JAVA_HOME/jre/sh/java\" ] ; then\n        # IBM's JDK on AIX uses strange locations for the executables\n        JAVACMD=\"$JAVA_HOME/jre/sh/java\"\n    else\n        JAVACMD=\"$JAVA_HOME/bin/java\"\n    fi\n    if [ ! -x \"$JAVACMD\" ] ; then\n        die \"ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\n    fi\nelse\n    JAVACMD=\"java\"\n    which java >/dev/null 2>&1 || die \"ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\nfi\n\n# Increase the maximum file descriptors if we can.\nif [ \"$cygwin\" = \"false\" -a \"$darwin\" = \"false\" -a \"$nonstop\" = \"false\" ] ; then\n    MAX_FD_LIMIT=`ulimit -H -n`\n    if [ $? -eq 0 ] ; then\n        if [ \"$MAX_FD\" = \"maximum\" -o \"$MAX_FD\" = \"max\" ] ; then\n            MAX_FD=\"$MAX_FD_LIMIT\"\n        fi\n        ulimit -n $MAX_FD\n        if [ $? -ne 0 ] ; then\n            warn \"Could not set maximum file descriptor limit: $MAX_FD\"\n        fi\n    else\n        warn \"Could not query maximum file descriptor limit: $MAX_FD_LIMIT\"\n    fi\nfi\n\n# For Darwin, add options to specify how the application appears in the dock\nif $darwin; then\n    GRADLE_OPTS=\"$GRADLE_OPTS \\\"-Xdock:name=$APP_NAME\\\" \\\"-Xdock:icon=$APP_HOME/media/gradle.icns\\\"\"\nfi\n\n# For Cygwin or MSYS, switch paths to Windows format before running java\nif [ \"$cygwin\" = \"true\" -o \"$msys\" = \"true\" ] ; then\n    APP_HOME=`cygpath --path --mixed \"$APP_HOME\"`\n    CLASSPATH=`cygpath --path --mixed \"$CLASSPATH\"`\n    JAVACMD=`cygpath --unix \"$JAVACMD\"`\n\n    # We build the pattern for arguments to be converted via cygpath\n    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`\n    SEP=\"\"\n    for dir in $ROOTDIRSRAW ; do\n        ROOTDIRS=\"$ROOTDIRS$SEP$dir\"\n        SEP=\"|\"\n    done\n    OURCYGPATTERN=\"(^($ROOTDIRS))\"\n    # Add a user-defined pattern to the cygpath arguments\n    if [ \"$GRADLE_CYGPATTERN\" != \"\" ] ; then\n        OURCYGPATTERN=\"$OURCYGPATTERN|($GRADLE_CYGPATTERN)\"\n    fi\n    # Now convert the arguments - kludge to limit ourselves to /bin/sh\n    i=0\n    for arg in \"$@\" ; do\n        CHECK=`echo \"$arg\"|egrep -c \"$OURCYGPATTERN\" -`\n        CHECK2=`echo \"$arg\"|egrep -c \"^-\"`                                 ### Determine if an option\n\n        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition\n            eval `echo args$i`=`cygpath --path --ignore --mixed \"$arg\"`\n        else\n            eval `echo args$i`=\"\\\"$arg\\\"\"\n        fi\n        i=`expr $i + 1`\n    done\n    case $i in\n        0) set -- ;;\n        1) set -- \"$args0\" ;;\n        2) set -- \"$args0\" \"$args1\" ;;\n        3) set -- \"$args0\" \"$args1\" \"$args2\" ;;\n        4) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" ;;\n        5) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" ;;\n        6) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" ;;\n        7) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" ;;\n        8) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" \"$args7\" ;;\n        9) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" \"$args7\" \"$args8\" ;;\n    esac\nfi\n\n# Escape application args\nsave () {\n    for i do printf %s\\\\n \"$i\" | sed \"s/'/'\\\\\\\\''/g;1s/^/'/;\\$s/\\$/' \\\\\\\\/\" ; done\n    echo \" \"\n}\nAPP_ARGS=`save \"$@\"`\n\n# Collect all arguments for the java command, following the shell quoting and substitution rules\neval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS \"\\\"-Dorg.gradle.appname=$APP_BASE_NAME\\\"\" -classpath \"\\\"$CLASSPATH\\\"\" org.gradle.wrapper.GradleWrapperMain \"$APP_ARGS\"\n\nexec \"$JAVACMD\" \"$@\"\n"
  },
  {
    "path": "gradlew.bat",
    "content": "@rem\r\n@rem Copyright 2015 the original author or authors.\r\n@rem\r\n@rem Licensed under the Apache License, Version 2.0 (the \"License\");\r\n@rem you may not use this file except in compliance with the License.\r\n@rem You may obtain a copy of the License at\r\n@rem\r\n@rem      https://www.apache.org/licenses/LICENSE-2.0\r\n@rem\r\n@rem Unless required by applicable law or agreed to in writing, software\r\n@rem distributed under the License is distributed on an \"AS IS\" BASIS,\r\n@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r\n@rem See the License for the specific language governing permissions and\r\n@rem limitations under the License.\r\n@rem\r\n\r\n@if \"%DEBUG%\" == \"\" @echo off\r\n@rem ##########################################################################\r\n@rem\r\n@rem  Gradle startup script for Windows\r\n@rem\r\n@rem ##########################################################################\r\n\r\n@rem Set local scope for the variables with windows NT shell\r\nif \"%OS%\"==\"Windows_NT\" setlocal\r\n\r\nset DIRNAME=%~dp0\r\nif \"%DIRNAME%\" == \"\" set DIRNAME=.\r\nset APP_BASE_NAME=%~n0\r\nset APP_HOME=%DIRNAME%\r\n\r\n@rem Resolve any \".\" and \"..\" in APP_HOME to make it shorter.\r\nfor %%i in (\"%APP_HOME%\") do set APP_HOME=%%~fi\r\n\r\n@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\r\nset DEFAULT_JVM_OPTS=\"-Xmx64m\" \"-Xms64m\"\r\n\r\n@rem Find java.exe\r\nif defined JAVA_HOME goto findJavaFromJavaHome\r\n\r\nset JAVA_EXE=java.exe\r\n%JAVA_EXE% -version >NUL 2>&1\r\nif \"%ERRORLEVEL%\" == \"0\" goto init\r\n\r\necho.\r\necho ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\r\necho.\r\necho Please set the JAVA_HOME variable in your environment to match the\r\necho location of your Java installation.\r\n\r\ngoto fail\r\n\r\n:findJavaFromJavaHome\r\nset JAVA_HOME=%JAVA_HOME:\"=%\r\nset JAVA_EXE=%JAVA_HOME%/bin/java.exe\r\n\r\nif exist \"%JAVA_EXE%\" goto init\r\n\r\necho.\r\necho ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%\r\necho.\r\necho Please set the JAVA_HOME variable in your environment to match the\r\necho location of your Java installation.\r\n\r\ngoto fail\r\n\r\n:init\r\n@rem Get command-line arguments, handling Windows variants\r\n\r\nif not \"%OS%\" == \"Windows_NT\" goto win9xME_args\r\n\r\n:win9xME_args\r\n@rem Slurp the command line arguments.\r\nset CMD_LINE_ARGS=\r\nset _SKIP=2\r\n\r\n:win9xME_args_slurp\r\nif \"x%~1\" == \"x\" goto execute\r\n\r\nset CMD_LINE_ARGS=%*\r\n\r\n:execute\r\n@rem Setup the command line\r\n\r\nset CLASSPATH=%APP_HOME%\\gradle\\wrapper\\gradle-wrapper.jar\r\n\r\n@rem Execute Gradle\r\n\"%JAVA_EXE%\" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% \"-Dorg.gradle.appname=%APP_BASE_NAME%\" -classpath \"%CLASSPATH%\" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%\r\n\r\n:end\r\n@rem End local scope for the variables with windows NT shell\r\nif \"%ERRORLEVEL%\"==\"0\" goto mainEnd\r\n\r\n:fail\r\nrem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of\r\nrem the _cmd.exe /c_ return code!\r\nif  not \"\" == \"%GRADLE_EXIT_CONSOLE%\" exit 1\r\nexit /b 1\r\n\r\n:mainEnd\r\nif \"%OS%\"==\"Windows_NT\" endlocal\r\n\r\n:omega\r\n"
  },
  {
    "path": "logback-audio/gradle.properties",
    "content": "#\n# SPDX-License-Identifier: CC0-1.0\n#\n# Copyright 2018-2020 Will Sargent.\n#\n# Licensed under the CC0 Public Domain Dedication;\n# You may obtain a copy of the License at\n#\n#  http://creativecommons.org/publicdomain/zero/1.0/\n#\n\nproject_description = Logback Audio Markers"
  },
  {
    "path": "logback-audio/logback-audio.gradle",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\ndependencies {\n    implementation project(':logback-core')\n    implementation project(':logback-classic')\n    implementation group: 'com.googlecode.soundlibs', name: 'mp3spi', version: '1.9.5.4'\n    implementation group: 'com.github.trilarion', name: 'vorbis-support', version: '1.1.0'\n}\n"
  },
  {
    "path": "logback-audio/src/main/java/com/tersesystems/logback/audio/AudioAppender.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.audio;\n\nimport ch.qos.logback.classic.spi.ILoggingEvent;\nimport ch.qos.logback.core.AppenderBase;\n\npublic class AudioAppender extends AppenderBase<ILoggingEvent> implements PlayerAttachable {\n\n  private Player player;\n\n  @Override\n  protected void append(ILoggingEvent eventObject) {\n    player.play();\n  }\n\n  @Override\n  public void addPlayer(Player player) {\n    this.player = player;\n  }\n\n  @Override\n  public void clearAllPlayers() {\n    this.player = null;\n  }\n\n  @Override\n  public void start() {\n    if (player == null) {\n      addError(\"No player found!\");\n    } else {\n      super.start();\n    }\n  }\n}\n"
  },
  {
    "path": "logback-audio/src/main/java/com/tersesystems/logback/audio/AudioMarker.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.audio;\n\nimport com.tersesystems.logback.classic.TerseBasicMarker;\nimport java.io.InputStream;\nimport java.net.URL;\nimport java.nio.file.Path;\n\npublic class AudioMarker extends TerseBasicMarker implements Player {\n\n  private static final String MARKER_NAME = \"TS_AUDIO_MARKER\";\n\n  private final Player player;\n\n  public AudioMarker(URL url) {\n    super(MARKER_NAME);\n    player = SimplePlayer.fromURL(url);\n  }\n\n  public AudioMarker(Path path) {\n    super(MARKER_NAME);\n    player = SimplePlayer.fromPath(path);\n  }\n\n  public AudioMarker(InputStream inputStream) {\n    super(MARKER_NAME);\n    player = SimplePlayer.fromInputStream(inputStream);\n  }\n\n  public AudioMarker(Player player) {\n    super(MARKER_NAME);\n    this.player = player;\n  }\n\n  public void play() {\n    player.play();\n  }\n}\n"
  },
  {
    "path": "logback-audio/src/main/java/com/tersesystems/logback/audio/AudioMarkerAppender.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.audio;\n\nimport ch.qos.logback.classic.spi.ILoggingEvent;\nimport ch.qos.logback.core.AppenderBase;\nimport java.util.Iterator;\nimport org.slf4j.Marker;\n\npublic class AudioMarkerAppender extends AppenderBase<ILoggingEvent> {\n\n  @Override\n  protected void append(ILoggingEvent eventObject) {\n    writePlayerMarkerIfNecessary(eventObject.getMarker());\n  }\n\n  private void writePlayerMarkerIfNecessary(Marker marker) {\n    if (marker != null) {\n      if (isPlayerMarker(marker)) {\n        ((Player) marker).play();\n      }\n\n      if (marker.hasReferences()) {\n        for (Iterator<Marker> i = marker.iterator(); i.hasNext(); ) {\n          writePlayerMarkerIfNecessary(i.next());\n        }\n      }\n    }\n  }\n\n  private static boolean isPlayerMarker(Marker marker) {\n    return marker instanceof Player;\n  }\n}\n"
  },
  {
    "path": "logback-audio/src/main/java/com/tersesystems/logback/audio/FilePlayer.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.audio;\n\nimport ch.qos.logback.core.spi.ContextAwareBase;\nimport ch.qos.logback.core.spi.LifeCycle;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\n\npublic class FilePlayer extends ContextAwareBase implements Player, LifeCycle {\n\n  private String file;\n  private Path path;\n  private volatile boolean started = false;\n\n  public FilePlayer() {}\n\n  public void setFile(String file) {\n    this.file = file;\n  }\n\n  @Override\n  public void play() {\n    SimplePlayer.fromPath(path).play();\n  }\n\n  @Override\n  public void start() {\n    path = Paths.get(file);\n    if (Files.exists(path)) {\n      started = true;\n    } else {\n      addError(String.format(\"Path %s does not exist!\", path));\n      started = false;\n    }\n  }\n\n  @Override\n  public void stop() {\n    path = null;\n    started = false;\n  }\n\n  @Override\n  public boolean isStarted() {\n    return started;\n  }\n}\n"
  },
  {
    "path": "logback-audio/src/main/java/com/tersesystems/logback/audio/PlayMethods.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.audio;\n\nimport java.io.IOException;\nimport java.util.function.Supplier;\nimport javax.sound.sampled.*;\n\npublic interface PlayMethods {\n\n  default void play(Supplier<AudioInputStream> supplier) {\n    // https://docs.oracle.com/javase/tutorial/sound/playing.html\n    try (final AudioInputStream in = supplier.get()) {\n      AudioFormat baseFormat = in.getFormat();\n      AudioFormat targetFormat =\n          new AudioFormat(\n              AudioFormat.Encoding.PCM_SIGNED,\n              baseFormat.getSampleRate(),\n              16,\n              baseFormat.getChannels(),\n              baseFormat.getChannels() * 2,\n              baseFormat.getSampleRate(),\n              false);\n      try (final AudioInputStream dataIn = AudioSystem.getAudioInputStream(targetFormat, in)) {\n        DataLine.Info info = new DataLine.Info(Clip.class, targetFormat);\n        Clip clip = (Clip) AudioSystem.getLine(info);\n        if (clip != null) {\n          clip.addLineListener(\n              event -> {\n                if (event.getType() == LineEvent.Type.STOP) clip.close();\n              });\n\n          clip.open(dataIn);\n          clip.start();\n        }\n      }\n    } catch (LineUnavailableException | IOException e) {\n      throw new IllegalStateException(e);\n    }\n  }\n}\n"
  },
  {
    "path": "logback-audio/src/main/java/com/tersesystems/logback/audio/Player.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.audio;\n\n/** This can play audio sounds. */\npublic interface Player {\n  void play();\n}\n"
  },
  {
    "path": "logback-audio/src/main/java/com/tersesystems/logback/audio/PlayerAction.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.audio;\n\nimport ch.qos.logback.core.Context;\nimport ch.qos.logback.core.joran.action.Action;\nimport ch.qos.logback.core.joran.spi.ActionException;\nimport ch.qos.logback.core.joran.spi.InterpretationContext;\nimport ch.qos.logback.core.spi.ContextAwareBase;\nimport ch.qos.logback.core.spi.LifeCycle;\nimport ch.qos.logback.core.util.OptionHelper;\nimport org.xml.sax.Attributes;\n\npublic class PlayerAction extends Action {\n  Player player;\n  private boolean inError = false;\n\n  @Override\n  public void begin(InterpretationContext ic, String localName, Attributes attributes)\n      throws ActionException {\n    Object o = ic.peekObject();\n\n    if (!(o instanceof PlayerAttachable)) {\n      String errMsg =\n          \"Could not find a PlayerAttachable at the top of execution stack. Near [\"\n              + localName\n              + \"] line \"\n              + getLineNumber(ic);\n      inError = true;\n      addInfo(errMsg); // This can trigger in an \"if\" block from janino, so it may not be serious...\n      return;\n    }\n\n    PlayerAttachable playerAttachable = (PlayerAttachable) o;\n\n    String className = attributes.getValue(CLASS_ATTRIBUTE);\n    if (OptionHelper.isEmpty(className)) {\n      addError(\"Missing class name for player. Near [\" + localName + \"] line \" + getLineNumber(ic));\n      inError = true;\n      return;\n    }\n\n    try {\n      addInfo(\"About to instantiate player of type [\" + className + \"]\");\n      player = (Player) OptionHelper.instantiateByClassName(className, Player.class, context);\n\n      Context icContext = ic.getContext();\n      if (player instanceof ContextAwareBase) {\n        ((ContextAwareBase) player).setContext(icContext);\n      }\n\n      ic.pushObject(player);\n    } catch (Exception oops) {\n      inError = true;\n      addError(\"Could not create player.\", oops);\n      throw new ActionException(oops);\n    }\n    playerAttachable.addPlayer(player);\n  }\n\n  @Override\n  public void end(InterpretationContext ic, String name) throws ActionException {\n    if (inError) {\n      return;\n    }\n\n    if (player instanceof LifeCycle) {\n      ((LifeCycle) player).start();\n    }\n\n    Object o = ic.peekObject();\n    if (o != player) {\n      addWarn(\"The object at the end of the stack is not the player pushed earlier.\");\n    } else {\n      ic.popObject();\n    }\n  }\n}\n"
  },
  {
    "path": "logback-audio/src/main/java/com/tersesystems/logback/audio/PlayerAttachable.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.audio;\n\npublic interface PlayerAttachable {\n  void addPlayer(Player player);\n\n  void clearAllPlayers();\n}\n"
  },
  {
    "path": "logback-audio/src/main/java/com/tersesystems/logback/audio/PlayerConverter.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.audio;\n\nimport ch.qos.logback.classic.pattern.ClassicConverter;\nimport ch.qos.logback.classic.spi.ILoggingEvent;\nimport java.util.Iterator;\nimport org.slf4j.Marker;\n\npublic class PlayerConverter extends ClassicConverter {\n\n  @Override\n  public String convert(ILoggingEvent event) {\n    writePlayerMarkerIfNecessary(event.getMarker());\n    return null;\n  }\n\n  private void writePlayerMarkerIfNecessary(Marker marker) {\n    if (marker != null) {\n      if (isPlayerMarker(marker)) {\n        ((Player) marker).play();\n      }\n\n      if (marker.hasReferences()) {\n        for (Iterator<Marker> i = marker.iterator(); i.hasNext(); ) {\n          writePlayerMarkerIfNecessary(i.next());\n        }\n      }\n    }\n  }\n\n  private static boolean isPlayerMarker(Marker marker) {\n    return marker instanceof Player;\n  }\n}\n"
  },
  {
    "path": "logback-audio/src/main/java/com/tersesystems/logback/audio/PlayerException.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.audio;\n\npublic class PlayerException extends RuntimeException {\n  public PlayerException() {}\n\n  public PlayerException(String s) {\n    super(s);\n  }\n\n  public PlayerException(String s, Throwable throwable) {\n    super(s, throwable);\n  }\n\n  public PlayerException(Throwable throwable) {\n    super(throwable);\n  }\n\n  public PlayerException(String s, Throwable throwable, boolean b, boolean b1) {\n    super(s, throwable, b, b1);\n  }\n}\n"
  },
  {
    "path": "logback-audio/src/main/java/com/tersesystems/logback/audio/ResourcePlayer.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.audio;\n\nimport ch.qos.logback.core.spi.ContextAwareBase;\nimport ch.qos.logback.core.spi.LifeCycle;\nimport java.net.URL;\n\npublic class ResourcePlayer extends ContextAwareBase implements Player, LifeCycle {\n\n  private String resource;\n  private URL resourceURL;\n  private volatile boolean started = false;\n\n  public ResourcePlayer() {}\n\n  public void setResource(String resource) {\n    this.resource = resource;\n  }\n\n  @Override\n  public void play() {\n    try {\n      SimplePlayer.fromURL(resourceURL).play();\n    } catch (Exception e) {\n      addError(String.format(\"Cannot play resource %s\", resourceURL), e);\n    }\n  }\n\n  @Override\n  public void start() {\n    resourceURL = getClass().getResource(resource);\n    if (resourceURL != null) {\n      started = true;\n    } else {\n      addError(String.format(\"Resource %s does not exist!\", resource));\n      started = false;\n    }\n  }\n\n  @Override\n  public void stop() {\n    resource = null;\n    started = false;\n  }\n\n  @Override\n  public boolean isStarted() {\n    return started;\n  }\n}\n"
  },
  {
    "path": "logback-audio/src/main/java/com/tersesystems/logback/audio/SimplePlayer.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.audio;\n\nimport static javax.sound.sampled.AudioSystem.getAudioInputStream;\n\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.net.URL;\nimport java.nio.file.Path;\nimport java.util.function.Supplier;\nimport javax.sound.sampled.AudioInputStream;\nimport javax.sound.sampled.UnsupportedAudioFileException;\n\npublic class SimplePlayer implements PlayMethods, Player {\n\n  private final Supplier<AudioInputStream> supplier;\n\n  protected SimplePlayer(Supplier<AudioInputStream> supplier) {\n    this.supplier = supplier;\n  }\n\n  public static Player fromURL(URL url) {\n    return new SimplePlayer(\n        () -> {\n          try {\n            return getAudioInputStream(url);\n          } catch (UnsupportedAudioFileException | IOException e) {\n            throw new PlayerException(e);\n          }\n        });\n  }\n\n  public static Player fromPath(Path path) {\n    return new SimplePlayer(\n        () -> {\n          try {\n            return getAudioInputStream(path.toFile());\n          } catch (UnsupportedAudioFileException | IOException e) {\n            throw new PlayerException(e);\n          }\n        });\n  }\n\n  public static Player fromInputStream(InputStream inputStream) {\n    return new SimplePlayer(\n        () -> {\n          try {\n            return getAudioInputStream(inputStream);\n          } catch (UnsupportedAudioFileException | IOException e) {\n            throw new PlayerException(e);\n          }\n        });\n  }\n\n  @Override\n  public void play() {\n    play(supplier);\n  }\n}\n"
  },
  {
    "path": "logback-audio/src/main/java/com/tersesystems/logback/audio/SystemPlayer.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.audio;\n\nimport java.awt.*;\n\npublic class SystemPlayer implements Player {\n  @Override\n  public void play() {\n    Toolkit toolkit = Toolkit.getDefaultToolkit();\n\n    final Runnable exclam = (Runnable) toolkit.getDesktopProperty(\"win.sound.exclamation\");\n    if (exclam != null) {\n      exclam.run();\n    } else {\n      toolkit.beep();\n    }\n  }\n}\n"
  },
  {
    "path": "logback-audio/src/main/java/com/tersesystems/logback/audio/URLPlayer.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.audio;\n\nimport ch.qos.logback.core.spi.ContextAwareBase;\nimport java.net.URL;\n\npublic class URLPlayer extends ContextAwareBase implements Player {\n\n  private URL url;\n\n  public URLPlayer() {}\n\n  public void URLPlayer(URL url) {\n    this.url = url;\n  }\n\n  @Override\n  public void play() {\n    SimplePlayer.fromURL(url).play();\n  }\n}\n"
  },
  {
    "path": "logback-audio/src/test/java/com/tersesystems/logback/audio/TestAudio.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.audio;\n\nimport ch.qos.logback.classic.Logger;\nimport ch.qos.logback.classic.LoggerContext;\nimport ch.qos.logback.classic.joran.JoranConfigurator;\nimport ch.qos.logback.core.joran.spi.JoranException;\nimport java.net.URL;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport org.junit.Test;\nimport org.slf4j.Marker;\n\npublic class TestAudio {\n\n  @Test\n  public void testMarkerWithURL() throws JoranException, InterruptedException {\n    LoggerContext context = new LoggerContext();\n\n    URL resource = getClass().getResource(\"/logback-with-marker-appender.xml\");\n    JoranConfigurator configurator = new JoranConfigurator();\n    configurator.setContext(context);\n    configurator.doConfigure(resource);\n\n    Logger logger = context.getLogger(\"some.random.Logger\");\n\n    URL audioURL = getClass().getResource(\"/bark.ogg\");\n    Marker marker = new AudioMarker(audioURL);\n    logger.info(marker, \"Bark!\");\n    Thread.sleep(1000);\n  }\n\n  // Can't keep a path steady with different starting directories..\n  @Test\n  public void testMarkerWithURLWithConverter() throws JoranException, InterruptedException {\n    LoggerContext context = new LoggerContext();\n\n    URL resource = getClass().getResource(\"/logback-with-converter.xml\");\n    JoranConfigurator configurator = new JoranConfigurator();\n    configurator.setContext(context);\n    configurator.doConfigure(resource);\n\n    Logger logger = context.getLogger(\"some.random.Logger\");\n\n    URL audioURL = getClass().getResource(\"/bark.ogg\");\n    Marker marker = new AudioMarker(audioURL);\n    logger.info(marker, \"Bark!\");\n    Thread.sleep(1000);\n  }\n\n  // Can't keep a path steady with different starting directories..\n  // @Test\n  public void testMarkerWithPath() throws JoranException, InterruptedException {\n    LoggerContext context = new LoggerContext();\n\n    URL resource = getClass().getResource(\"/logback-with-marker-appender.xml\");\n    JoranConfigurator configurator = new JoranConfigurator();\n    configurator.setContext(context);\n    configurator.doConfigure(resource);\n\n    Logger logger = context.getLogger(\"some.random.Logger\");\n\n    String path = System.getProperty(\"user.dir\");\n    Path audioPath = Paths.get(path, \"src\", \"test\", \"resources\", \"bark.ogg\");\n    Marker marker = new AudioMarker(audioPath);\n    logger.warn(marker, \"Bark!\");\n    Thread.sleep(1000);\n  }\n}\n"
  },
  {
    "path": "logback-audio/src/test/java/com/tersesystems/logback/audio/TestNested.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.audio;\n\nimport ch.qos.logback.classic.Logger;\nimport ch.qos.logback.classic.LoggerContext;\nimport ch.qos.logback.classic.joran.JoranConfigurator;\nimport ch.qos.logback.core.joran.spi.JoranException;\nimport java.net.URL;\nimport org.junit.Test;\n\npublic class TestNested {\n\n  @Test\n  public void testLogger() throws JoranException, InterruptedException {\n    LoggerContext context = new LoggerContext();\n\n    URL resource = getClass().getResource(\"/logback-with-nested-appender.xml\");\n    JoranConfigurator configurator = new JoranConfigurator();\n    configurator.setContext(context);\n    configurator.doConfigure(resource);\n\n    Logger logger = context.getLogger(\"some.random.Logger\");\n\n    for (int i = 0; i < 1; i++) {\n      logger.trace(\"TRACE\");\n    }\n\n    for (int i = 0; i < 2; i++) {\n      logger.debug(\"DEBUG\");\n    }\n\n    for (int i = 0; i < 2; i++) {\n      logger.info(\"INFO\");\n    }\n\n    for (int i = 0; i < 2; i++) {\n      logger.warn(\"WARN\");\n    }\n\n    logger.error(\"ERROR\");\n  }\n}\n"
  },
  {
    "path": "logback-audio/src/test/resources/logback-with-converter.xml",
    "content": "<!--\n  ~ SPDX-License-Identifier: CC0-1.0\n  ~\n  ~ Copyright 2018-2020 Will Sargent.\n  ~\n  ~ Licensed under the CC0 Public Domain Dedication;\n  ~ You may obtain a copy of the License at\n  ~\n  ~  http://creativecommons.org/publicdomain/zero/1.0/\n  -->\n<configuration>\n    <conversionRule conversionWord=\"audio\" converterClass=\"com.tersesystems.logback.audio.PlayerConverter\" />\n\n    <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n        <encoder>\n            <pattern>%-5relative %-5level %logger{35} %audio - %msg%n</pattern>\n        </encoder>\n    </appender>\n\n    <root level=\"TRACE\">\n        <appender-ref ref=\"STDOUT\"/>\n    </root>\n</configuration>"
  },
  {
    "path": "logback-audio/src/test/resources/logback-with-marker-appender.xml",
    "content": "<!--\n  ~ SPDX-License-Identifier: CC0-1.0\n  ~\n  ~ Copyright 2018-2020 Will Sargent.\n  ~\n  ~ Licensed under the CC0 Public Domain Dedication;\n  ~ You may obtain a copy of the License at\n  ~\n  ~  http://creativecommons.org/publicdomain/zero/1.0/\n  -->\n<configuration>\n\n    <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n        <encoder>\n            <pattern>%-5relative %-5level %logger{35} - %msg%n</pattern>\n        </encoder>\n    </appender>\n\n    <appender name=\"AUDIO-MARKER\" class=\"com.tersesystems.logback.audio.AudioMarkerAppender\">\n    </appender>\n\n    <root level=\"TRACE\">\n        <appender-ref ref=\"STDOUT\"/>\n        <appender-ref ref=\"AUDIO-MARKER\"/>\n    </root>\n</configuration>"
  },
  {
    "path": "logback-audio/src/test/resources/logback-with-nested-appender.xml",
    "content": "<!--\n  ~ SPDX-License-Identifier: CC0-1.0\n  ~\n  ~ Copyright 2018-2020 Will Sargent.\n  ~\n  ~ Licensed under the CC0 Public Domain Dedication;\n  ~ You may obtain a copy of the License at\n  ~\n  ~  http://creativecommons.org/publicdomain/zero/1.0/\n  -->\n<configuration>\n\n    <newRule pattern=\"*/player\"\n             actionClass=\"com.tersesystems.logback.audio.PlayerAction\"/>\n\n    <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n        <encoder>\n            <pattern>%-5relative %-5level %logger{35} - %msg%n</pattern>\n        </encoder>\n    </appender>\n\n    <appender name=\"TRACE\" class=\"com.tersesystems.logback.core.CompositeAppender\">\n        <filter class=\"ch.qos.logback.classic.filter.LevelFilter\">\n            <level>TRACE</level>\n            <onMatch>ACCEPT</onMatch>\n            <onMismatch>DENY</onMismatch>\n        </filter>\n\n        <appender class=\"com.tersesystems.logback.audio.AudioAppender\">\n            <player class=\"com.tersesystems.logback.audio.ResourcePlayer\">\n                <resource>/bark.ogg</resource>\n            </player>\n        </appender>\n    </appender>\n\n    <appender name=\"DEBUG\" class=\"com.tersesystems.logback.core.CompositeAppender\">\n        <filter class=\"ch.qos.logback.classic.filter.LevelFilter\">\n            <level>DEBUG</level>\n            <onMatch>ACCEPT</onMatch>\n            <onMismatch>DENY</onMismatch>\n        </filter>\n\n        <appender class=\"com.tersesystems.logback.audio.AudioAppender\">\n            <player class=\"com.tersesystems.logback.audio.ResourcePlayer\">\n                <resource>/drip.ogg</resource>\n            </player>\n        </appender>\n    </appender>\n\n    <appender name=\"INFO\" class=\"com.tersesystems.logback.core.CompositeAppender\">\n        <filter class=\"ch.qos.logback.classic.filter.LevelFilter\">\n            <level>INFO</level>\n            <onMatch>ACCEPT</onMatch>\n            <onMismatch>DENY</onMismatch>\n        </filter>\n\n        <appender class=\"com.tersesystems.logback.audio.AudioAppender\">\n            <player class=\"com.tersesystems.logback.audio.ResourcePlayer\">\n                <resource>/glass.ogg</resource>\n            </player>\n        </appender>\n    </appender>\n\n    <appender name=\"WARN\" class=\"com.tersesystems.logback.core.CompositeAppender\">\n        <filter class=\"ch.qos.logback.classic.filter.LevelFilter\">\n            <level>WARN</level>\n            <onMatch>ACCEPT</onMatch>\n            <onMismatch>DENY</onMismatch>\n        </filter>\n\n        <appender class=\"com.tersesystems.logback.audio.AudioAppender\">\n            <player class=\"com.tersesystems.logback.audio.ResourcePlayer\">\n                <resource>/message.ogg</resource>\n            </player>\n        </appender>\n    </appender>\n\n    <appender name=\"ERROR\" class=\"com.tersesystems.logback.core.CompositeAppender\">\n        <filter class=\"ch.qos.logback.classic.filter.LevelFilter\">\n            <level>ERROR</level>\n            <onMatch>ACCEPT</onMatch>\n            <onMismatch>DENY</onMismatch>\n        </filter>\n\n        <appender class=\"com.tersesystems.logback.audio.AudioAppender\">\n            <player class=\"com.tersesystems.logback.audio.ResourcePlayer\">\n                <resource>/sample.ogg</resource>\n            </player>\n        </appender>\n    </appender>\n\n    <root level=\"TRACE\">\n        <appender-ref ref=\"ALL-APPENDER\"/>\n        <appender-ref ref=\"TRACE-APPENDER\"/>\n        <appender-ref ref=\"DEBUG-APPENDER\"/>\n        <appender-ref ref=\"INFO-APPENDER\"/>\n        <appender-ref ref=\"WARN-APPENDER\"/>\n        <appender-ref ref=\"ERROR-APPENDER\"/>\n    </root>\n</configuration>"
  },
  {
    "path": "logback-budget/gradle.properties",
    "content": "#\n# SPDX-License-Identifier: CC0-1.0\n#\n# Copyright 2018-2020 Will Sargent.\n#\n# Licensed under the CC0 Public Domain Dedication;\n# You may obtain a copy of the License at\n#\n#  http://creativecommons.org/publicdomain/zero/1.0/\n#\n\nproject_description = Logback Budget"
  },
  {
    "path": "logback-budget/logback-budget.gradle",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\ndependencies {\n    implementation project(':logback-core')\n    implementation project(':logback-classic')\n    // https://mvnrepository.com/artifact/org.apache.commons/commons-lang3\n    implementation group: 'org.apache.commons', name: 'commons-lang3', version: '3.9'\n}\n"
  },
  {
    "path": "logback-budget/src/main/java/com/tersesystems/logback/budget/BudgetEvaluator.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.budget;\n\nimport ch.qos.logback.classic.Level;\nimport ch.qos.logback.classic.spi.ILoggingEvent;\nimport ch.qos.logback.core.boolex.EventEvaluatorBase;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.concurrent.TimeUnit;\nimport org.apache.commons.lang3.concurrent.CircuitBreaker;\nimport org.apache.commons.lang3.concurrent.EventCountCircuitBreaker;\n\n/** Returns true if logging the event is within budget, false otherwise. */\npublic class BudgetEvaluator extends EventEvaluatorBase<ILoggingEvent>\n    implements BudgetRuleAttachable {\n\n  private List<BudgetRule> budgetRules = new ArrayList<>();\n  private Map<String, CircuitBreaker<Integer>> levelRules = new HashMap<>();\n\n  @Override\n  public void start() {\n    for (BudgetRule budgetRule : budgetRules) {\n      CircuitBreaker<Integer> breaker = createCircuitBreaker(budgetRule);\n      levelRules.put(budgetRule.getName(), breaker);\n    }\n    super.start();\n  }\n\n  private CircuitBreaker<Integer> createCircuitBreaker(BudgetRule budgetRule) {\n    addInfo(\"budgetRule = \" + budgetRule);\n    final int threshold = budgetRule.getThreshold();\n    final long checkInterval = budgetRule.getInterval();\n    String timeUnit = budgetRule.getTimeUnit();\n    if (timeUnit == null) {\n      addError(\"No time unit found for budget rule\");\n      throw new IllegalStateException(\"No time unit found for budget rule \" + budgetRule);\n    } else {\n      TimeUnit checkUnit;\n      try {\n        checkUnit = TimeUnit.valueOf(timeUnit.toUpperCase());\n      } catch (IllegalArgumentException iae) {\n        try {\n          // Try adding an S on the end\n          checkUnit = TimeUnit.valueOf(timeUnit.toUpperCase() + \"S\");\n        } catch (Exception e) {\n          addError(\n              \"Invalid time unit found for budget rule, use java.util.concurrent.TimeUnit enums \"\n                  + budgetRule,\n              e);\n          throw e;\n        }\n      }\n      return new EventCountCircuitBreaker(threshold, checkInterval, checkUnit);\n    }\n  }\n\n  @Override\n  public boolean evaluate(ILoggingEvent event) {\n    if (levelRules.isEmpty()) {\n      return true; // not applicable\n    }\n\n    Level level = event.getLevel();\n    CircuitBreaker<Integer> breaker = levelRules.get(level.levelStr);\n    if (breaker == null) {\n      return true; // does not apply to this level\n    }\n\n    if (breaker.checkState()) {\n      return breaker.incrementAndCheckState(1);\n    } else {\n      return false;\n    }\n  }\n\n  @Override\n  public void addBudgetRule(BudgetRule budget) {\n    budgetRules.add(budget);\n  }\n\n  @Override\n  public void clearAllBudgetRules() {\n    budgetRules.clear();\n  }\n}\n"
  },
  {
    "path": "logback-budget/src/main/java/com/tersesystems/logback/budget/BudgetRule.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.budget;\n\nimport org.apache.commons.lang3.builder.ToStringBuilder;\n\npublic class BudgetRule {\n\n  private String name;\n  private int threshold;\n  private long interval;\n  private String timeUnit;\n\n  public BudgetRule() {}\n\n  public String getName() {\n    return name;\n  }\n\n  public void setName(String name) {\n    this.name = name;\n  }\n\n  public int getThreshold() {\n    return threshold;\n  }\n\n  public void setThreshold(int threshold) {\n    this.threshold = threshold;\n  }\n\n  public long getInterval() {\n    return interval;\n  }\n\n  public void setInterval(long interval) {\n    this.interval = interval;\n  }\n\n  public String getTimeUnit() {\n    return timeUnit;\n  }\n\n  public void setTimeUnit(String timeUnit) {\n    this.timeUnit = timeUnit;\n  }\n\n  @Override\n  public String toString() {\n    return ToStringBuilder.reflectionToString(this);\n  }\n}\n"
  },
  {
    "path": "logback-budget/src/main/java/com/tersesystems/logback/budget/BudgetRuleAction.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.budget;\n\nimport ch.qos.logback.core.joran.action.Action;\nimport ch.qos.logback.core.joran.spi.ActionException;\nimport ch.qos.logback.core.joran.spi.InterpretationContext;\nimport ch.qos.logback.core.util.OptionHelper;\nimport org.xml.sax.Attributes;\n\npublic class BudgetRuleAction extends Action {\n\n  private BudgetRule budgetRule;\n  private boolean inError = false;\n\n  @Override\n  public void begin(InterpretationContext ic, String localName, Attributes attributes)\n      throws ActionException {\n    Object o = ic.peekObject();\n\n    if (!(o instanceof BudgetRuleAttachable)) {\n      String errMsg =\n          \"Could not find a BudgetRuleAttachable at the top of execution stack. Near [\"\n              + localName\n              + \"] line \"\n              + getLineNumber(ic);\n      inError = true;\n      addInfo(errMsg); // This can trigger in an \"if\" block from janino, so it may not be serious...\n      return;\n    }\n\n    BudgetRuleAttachable budgetRuleAttachable = (BudgetRuleAttachable) o;\n\n    String className = attributes.getValue(CLASS_ATTRIBUTE);\n    if (OptionHelper.isEmpty(className)) {\n      className = BudgetRule.class.getName();\n    }\n\n    String name = attributes.getValue(NAME_ATTRIBUTE);\n    long interval = Long.parseLong(attributes.getValue(\"interval\"));\n    int threshold = Integer.parseInt(attributes.getValue(\"threshold\"));\n    String timeUnit = attributes.getValue(\"timeUnit\");\n\n    try {\n      addInfo(\"About to instantiate budgetRule of type [\" + className + \"]\");\n      budgetRule =\n          (BudgetRule) OptionHelper.instantiateByClassName(className, BudgetRule.class, context);\n      budgetRule.setName(name);\n      budgetRule.setInterval(interval);\n      budgetRule.setThreshold(threshold);\n      budgetRule.setTimeUnit(timeUnit);\n\n      ic.pushObject(budgetRule);\n    } catch (Exception oops) {\n      inError = true;\n      addError(\"Could not create budgetRule.\", oops);\n      throw new ActionException(oops);\n    }\n    budgetRuleAttachable.addBudgetRule(budgetRule);\n  }\n\n  @Override\n  public void end(InterpretationContext ic, String name) throws ActionException {\n    if (inError) {\n      return;\n    }\n\n    Object o = ic.peekObject();\n    if (o != budgetRule) {\n      addWarn(\"The object at the end of the stack is not the budgetRule pushed earlier.\");\n    } else {\n      ic.popObject();\n    }\n  }\n}\n"
  },
  {
    "path": "logback-budget/src/main/java/com/tersesystems/logback/budget/BudgetRuleAttachable.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.budget;\n\npublic interface BudgetRuleAttachable {\n\n  void addBudgetRule(BudgetRule budget);\n\n  void clearAllBudgetRules();\n}\n"
  },
  {
    "path": "logback-budget/src/main/java/com/tersesystems/logback/budget/BudgetTurboFilter.java",
    "content": "package com.tersesystems.logback.budget;\n\nimport ch.qos.logback.classic.Level;\nimport ch.qos.logback.classic.Logger;\nimport ch.qos.logback.classic.turbo.TurboFilter;\nimport ch.qos.logback.core.spi.FilterReply;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.concurrent.TimeUnit;\nimport org.apache.commons.lang3.concurrent.CircuitBreaker;\nimport org.apache.commons.lang3.concurrent.EventCountCircuitBreaker;\nimport org.slf4j.Marker;\n\npublic class BudgetTurboFilter extends TurboFilter implements BudgetRuleAttachable {\n\n  private List<BudgetRule> budgetRules = new ArrayList<>();\n  private Map<String, CircuitBreaker<Integer>> levelRules = new HashMap<>();\n\n  public void start() {\n    for (BudgetRule budgetRule : budgetRules) {\n      CircuitBreaker<Integer> breaker = createCircuitBreaker(budgetRule);\n      levelRules.put(budgetRule.getName(), breaker);\n    }\n    super.start();\n  }\n\n  private CircuitBreaker<Integer> createCircuitBreaker(BudgetRule budgetRule) {\n    addInfo(\"budgetRule = \" + budgetRule);\n    final int threshold = budgetRule.getThreshold();\n    final long checkInterval = budgetRule.getInterval();\n    String timeUnit = budgetRule.getTimeUnit();\n    if (timeUnit == null) {\n      addError(\"No time unit found for budget rule\");\n      throw new IllegalStateException(\"No time unit found for budget rule \" + budgetRule);\n    } else {\n      TimeUnit checkUnit;\n      try {\n        checkUnit = TimeUnit.valueOf(timeUnit.toUpperCase());\n      } catch (IllegalArgumentException iae) {\n        try {\n          // Try adding an S on the end\n          checkUnit = TimeUnit.valueOf(timeUnit.toUpperCase() + \"S\");\n        } catch (Exception e) {\n          addError(\n              \"Invalid time unit found for budget rule, use java.util.concurrent.TimeUnit enums \"\n                  + budgetRule,\n              e);\n          throw e;\n        }\n      }\n      return new EventCountCircuitBreaker(threshold, checkInterval, checkUnit);\n    }\n  }\n\n  @Override\n  public FilterReply decide(\n      Marker marker, Logger logger, Level level, String format, Object[] params, Throwable t) {\n    if (levelRules.isEmpty()) {\n      return getOnMatch(); // not applicable\n    }\n\n    CircuitBreaker<Integer> breaker = levelRules.get(level.levelStr);\n    if (breaker == null) {\n      return getOnMatch(); // does not apply to this level\n    }\n\n    if (breaker.checkState()) {\n      return breaker.incrementAndCheckState(1) ? getOnMatch() : getOnMismatch();\n    } else {\n      return getOnMismatch();\n    }\n  }\n\n  @Override\n  public void addBudgetRule(BudgetRule budget) {\n    budgetRules.add(budget);\n  }\n\n  @Override\n  public void clearAllBudgetRules() {\n    budgetRules.clear();\n  }\n\n  protected FilterReply onMatch = FilterReply.NEUTRAL;\n  protected FilterReply onMismatch = FilterReply.DENY;\n\n  public final void setOnMatch(FilterReply reply) {\n    this.onMatch = reply;\n  }\n\n  public final void setOnMismatch(FilterReply reply) {\n    this.onMismatch = reply;\n  }\n\n  public final FilterReply getOnMatch() {\n    return onMatch;\n  }\n\n  public final FilterReply getOnMismatch() {\n    return onMismatch;\n  }\n}\n"
  },
  {
    "path": "logback-budget/src/test/java/com.tersesystems.logback.budget/BudgetEvaluatorTest.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.budget;\n\nimport ch.qos.logback.classic.Logger;\nimport ch.qos.logback.classic.LoggerContext;\nimport ch.qos.logback.classic.joran.JoranConfigurator;\nimport ch.qos.logback.core.joran.spi.JoranException;\nimport java.net.URL;\nimport org.junit.Test;\n\npublic class BudgetEvaluatorTest {\n\n  @Test\n  public void testBudget() throws JoranException, InterruptedException {\n    LoggerContext context = new LoggerContext();\n\n    URL resource = getClass().getResource(\"/logback-budget.xml\");\n    JoranConfigurator configurator = new JoranConfigurator();\n    configurator.setContext(context);\n    configurator.doConfigure(resource);\n\n    Logger logger = context.getLogger(\"some.random.Logger\");\n\n    for (int i = 0; i < 10; i++) {\n      logger.info(\"Hello world\");\n    }\n    Thread.sleep(1000);\n\n    logger.info(\"Hello world\");\n  }\n}\n"
  },
  {
    "path": "logback-budget/src/test/java/com.tersesystems.logback.budget/BudgetTurboFilterTest.java",
    "content": "package com.tersesystems.logback.budget;\n\nimport ch.qos.logback.classic.Logger;\nimport ch.qos.logback.classic.LoggerContext;\nimport ch.qos.logback.classic.joran.JoranConfigurator;\nimport ch.qos.logback.core.joran.spi.JoranException;\nimport java.net.URL;\nimport org.junit.Test;\n\npublic class BudgetTurboFilterTest {\n\n  @Test\n  public void testBudget() throws JoranException, InterruptedException {\n    LoggerContext context = new LoggerContext();\n\n    URL resource = getClass().getResource(\"/logback-turbofilter.xml\");\n    JoranConfigurator configurator = new JoranConfigurator();\n    configurator.setContext(context);\n    configurator.doConfigure(resource);\n\n    Logger logger = context.getLogger(\"some.random.Logger\");\n\n    for (int i = 0; i < 10; i++) {\n      logger.info(\"Hello world\");\n    }\n    Thread.sleep(1000);\n\n    logger.info(\"Hello world\");\n  }\n}\n"
  },
  {
    "path": "logback-budget/src/test/resources/logback-budget.xml",
    "content": "<!--\n  ~ SPDX-License-Identifier: CC0-1.0\n  ~\n  ~ Copyright 2018-2020 Will Sargent.\n  ~\n  ~ Licensed under the CC0 Public Domain Dedication;\n  ~ You may obtain a copy of the License at\n  ~\n  ~  http://creativecommons.org/publicdomain/zero/1.0/\n  -->\n<configuration>\n\n    <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n        <filter class=\"ch.qos.logback.core.filter.EvaluatorFilter\">\n            <evaluator class=\"com.tersesystems.logback.budget.BudgetEvaluator\">\n              <budgetRule>\n                  <name>INFO</name>\n                  <threshold>5</threshold>\n                  <interval>1</interval>\n                  <timeUnit>second</timeUnit>\n              </budgetRule>\n            </evaluator>\n            <OnMismatch>DENY</OnMismatch>\n            <OnMatch>NEUTRAL</OnMatch>\n        </filter>\n\n        <encoder>\n            <pattern>%-5relative %-5level %logger{35} - %msg%n</pattern>\n        </encoder>\n    </appender>\n\n    <root level=\"TRACE\">\n        <appender-ref ref=\"STDOUT\"/>\n    </root>\n\n</configuration>"
  },
  {
    "path": "logback-budget/src/test/resources/logback-turbofilter.xml",
    "content": "<!--\n  ~ SPDX-License-Identifier: CC0-1.0\n  ~\n  ~ Copyright 2018-2020 Will Sargent.\n  ~\n  ~ Licensed under the CC0 Public Domain Dedication;\n  ~ You may obtain a copy of the License at\n  ~\n  ~  http://creativecommons.org/publicdomain/zero/1.0/\n  -->\n<configuration>\n\n    <turboFilter class=\"com.tersesystems.logback.budget.BudgetTurboFilter\">\n        <budgetRule>\n            <name>INFO</name>\n            <threshold>5</threshold>\n            <interval>1</interval>\n            <timeUnit>second</timeUnit>\n        </budgetRule>\n        <OnMismatch>DENY</OnMismatch>\n        <OnMatch>NEUTRAL</OnMatch>\n    </turboFilter>\n\n    <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n        <encoder>\n            <pattern>%-5relative %-5level %logger{35} - %msg%n</pattern>\n        </encoder>\n    </appender>\n\n    <root level=\"TRACE\">\n        <appender-ref ref=\"STDOUT\"/>\n    </root>\n\n</configuration>"
  },
  {
    "path": "logback-bytebuddy/gradle.properties",
    "content": "#\n# SPDX-License-Identifier: CC0-1.0\n#\n# Copyright 2018-2020 Will Sargent.\n#\n# Licensed under the CC0 Public Domain Dedication;\n# You may obtain a copy of the License at\n#\n#  http://creativecommons.org/publicdomain/zero/1.0/\n#\n\nproject_description = Logback ByteBuddy Instrumentation"
  },
  {
    "path": "logback-bytebuddy/logback-bytebuddy.gradle",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\nplugins {\n    id 'com.github.johnrengelman.shadow' version '5.1.0'\n}\n\ndependencies {\n    implementation \"net.bytebuddy:byte-buddy:$bytebuddyVersion\"\n    implementation \"com.typesafe:config:${configVersion}\"\n\n    // We cannot allow dependencies from logback-tracing in here.\n    shadow project(\":logback-tracing\")\n    shadow \"org.slf4j:slf4j-api:$slf4jVersion\"\n    shadow \"ch.qos.logback:logback-classic:$logbackVersion\"\n    shadow \"ch.qos.logback:logback-core:$logbackVersion\"\n    shadow \"net.logstash.logback:logstash-logback-encoder:$logstashVersion\"\n    shadow \"com.google.auto.value:auto-value-annotations:1.6.2\"\n    shadow group: 'com.fasterxml.uuid', name: 'java-uuid-generator', version: '3.2.0'\n\n    // You'll need this, but better to use the version already defined in project\n    testImplementation \"net.bytebuddy:byte-buddy-agent:$bytebuddyVersion\"\n    testImplementation \"ch.qos.logback:logback-classic:$logbackVersion\"\n}\n\njar {\n    manifest {\n        attributes \"Premain-Class\": \"com.tersesystems.logback.bytebuddy.LogbackInstrumentationAgent\"\n        attributes \"Agent-Class\": \"com.tersesystems.logback.bytebuddy.LogbackInstrumentationAgent\"\n        attributes \"Can-Redefine-Classes\": \"true\"\n        attributes \"Can-Retransform-Classes\": \"true\"\n    }\n}\n\nshadowJar {\n    archiveFileName = \"$archiveBaseName-$archiveVersion.$archiveExtension\"\n\n    // Shade typesafe config and bytebuddy itself since this is used in bootstrap classloader\n    // and so would be visible to everything downstream\n    relocate 'com.typesafe', 'com.tersesystems.logback.shadow.typesafe'\n    relocate 'net.bytebuddy', 'com.tersesystems.logback.shadow.bytebuddy'\n\n    exclude '**/module-info.class'\n\n    dependencies {\n        exclude(dependency(\"com.fasterxml.jackson.core:\"))\n        exclude(dependency(\"com.fasterxml.jackson.annotation:\"))\n        exclude(dependency(\"com.fasterxml.jackson.databind:\"))\n        exclude(dependency(\"net.logstash.logback:logstash-logback-encoder:$logstashVersion\"))\n        exclude(dependency(\"org.slf4j:slf4j-api:$slf4jVersion\"))\n    }\n}\n\nartifacts {\n    archives shadowJar\n}"
  },
  {
    "path": "logback-bytebuddy/src/main/java/com/tersesystems/logback/bytebuddy/AdviceConfig.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.bytebuddy;\n\nimport static java.util.Collections.singletonList;\nimport static net.bytebuddy.matcher.ElementMatchers.*;\n\nimport java.util.*;\nimport net.bytebuddy.description.method.MethodDescription;\nimport net.bytebuddy.description.method.MethodList;\nimport net.bytebuddy.description.type.TypeDescription;\nimport net.bytebuddy.matcher.ElementMatcher;\n\npublic class AdviceConfig {\n\n  private final ClassLoader classLoader;\n\n  private final Collection<TraceConfig> traceConfigCollection;\n  private final String serviceName;\n\n  private TraceConfig traceConfig;\n\n  public AdviceConfig(ClassLoader classLoader, String serviceName) {\n    this.classLoader = classLoader;\n    this.serviceName = serviceName;\n    traceConfigCollection = new HashSet<>();\n  }\n\n  public String getServiceName() {\n    return serviceName;\n  }\n\n  public void addTrace(String className, List<String> methodNames) throws Exception {\n    TraceConfig traceConfig = TraceConfig.create(classLoader, className, methodNames);\n    traceConfigCollection.add(traceConfig);\n  }\n\n  public void addTrace(String className) throws Exception {\n    TraceConfig traceConfig = TraceConfig.create(classLoader, className);\n    traceConfigCollection.add(traceConfig);\n  }\n\n  public List<String> classNames() {\n    return getTraceConfig().classNames;\n  }\n\n  public ElementMatcher<? super MethodDescription> methods() {\n    return getTraceConfig().methods();\n  }\n\n  public ElementMatcher<? super TypeDescription> types() {\n    return getTraceConfig().types();\n  }\n\n  private TraceConfig getTraceConfig() {\n    if (traceConfig == null) {\n      traceConfig = traceConfigCollection.stream().reduce(TraceConfig::join).get();\n    }\n    return traceConfig;\n  }\n\n  static final class TraceConfig {\n\n    private final List<String> classNames;\n    private final ElementMatcher.Junction<? super TypeDescription> typeMatcher;\n    private final ElementMatcher.Junction<? super MethodDescription> methodMatcher;\n\n    private TraceConfig(\n        List<String> classNames,\n        ElementMatcher.Junction<? super TypeDescription> typeMatcher,\n        ElementMatcher.Junction<? super MethodDescription> methodMatcher) {\n      this.typeMatcher = Objects.requireNonNull(typeMatcher);\n      this.methodMatcher = Objects.requireNonNull(methodMatcher);\n      this.classNames = classNames;\n    }\n\n    static TraceConfig create(ClassLoader classLoader, String className, List<String> methodNames)\n        throws Exception {\n      TypeDescription aClass = createTypeDescription(classLoader, className);\n\n      return new TraceConfig(\n          singletonList(className),\n          is(aClass),\n          methodNames.stream()\n              .map(m -> named(m).and(isDeclaredBy(aClass)).and(not(isNative().or(isConstructor()))))\n              .reduce(none(), ElementMatcher.Junction::or));\n    }\n\n    static TraceConfig create(ClassLoader classLoader, String className) throws Exception {\n      TypeDescription aClass = createTypeDescription(classLoader, className);\n\n      // Get a list of all non-native methods from the class, with no constructor.\n      MethodList<MethodDescription.InDefinedShape> methods =\n          aClass.getDeclaredMethods().filter(not(isNative().or(isConstructor())));\n      return new TraceConfig(singletonList(className), is(aClass), anyOf(methods));\n    }\n\n    public List<String> classNames() {\n      return this.classNames;\n    }\n\n    public ElementMatcher.Junction<? super MethodDescription> methods() {\n      return methodMatcher;\n    }\n\n    public ElementMatcher.Junction<? super TypeDescription> types() {\n      return typeMatcher;\n    }\n\n    public TraceConfig join(TraceConfig other) {\n      final ElementMatcher.Junction<? super MethodDescription> methodMatcher =\n          methods().or(other.methods());\n      final ElementMatcher.Junction<? super TypeDescription> typeMatcher =\n          types().or(other.types());\n      List<String> classNames = new ArrayList<>(classNames());\n      classNames.addAll(other.classNames());\n      return new TraceConfig(classNames, typeMatcher, methodMatcher);\n    }\n\n    private static TypeDescription createTypeDescription(ClassLoader classLoader, String className)\n        throws Exception {\n      return new TypeDescription.ForLoadedType(classLoader.loadClass(className));\n    }\n  }\n\n  @Override\n  public String toString() {\n    return \"AdviceConfig{service-name = \"\n        + getServiceName()\n        + \", methods=\"\n        + methods()\n        + \", types='\"\n        + types()\n        + '\\''\n        + '}';\n  }\n}\n"
  },
  {
    "path": "logback-bytebuddy/src/main/java/com/tersesystems/logback/bytebuddy/LogbackInstrumentationAgent.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.bytebuddy;\n\nimport static java.util.Collections.singletonMap;\nimport static net.bytebuddy.dynamic.ClassFileLocator.ForClassLoader.read;\nimport static net.bytebuddy.dynamic.loading.ClassInjector.UsingInstrumentation.Target.BOOTSTRAP;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.lang.instrument.Instrumentation;\nimport java.nio.file.Files;\nimport net.bytebuddy.description.type.TypeDescription;\nimport net.bytebuddy.dynamic.loading.ClassInjector;\n\n/**\n * The agent class. This has the magic \"premain\" and \"agentmain\" methods for the Java\n * Instrumentation API.\n */\npublic class LogbackInstrumentationAgent {\n\n  private static final Class<?> INSTRUMENTATION_ADVICE_CLASS = LoggingInstrumentationAdvice.class;\n\n  public static void premain(String arg, Instrumentation instrumentation) throws Exception {\n    injectBootstrapClasses(instrumentation);\n    LoggingInstrumentationAdvice logbackInst =\n        (LoggingInstrumentationAdvice) INSTRUMENTATION_ADVICE_CLASS.newInstance();\n\n    boolean debug = parseDebug(arg);\n    logbackInst.initialize(instrumentation, debug);\n  }\n\n  private static boolean parseDebug(String arg) {\n    return \"debug\".equalsIgnoreCase(arg);\n  }\n\n  public static void agentmain(String arg, Instrumentation instrumentation) throws Exception {\n    injectBootstrapClasses(instrumentation);\n\n    boolean debug = parseDebug(arg);\n    LoggingInstrumentationAdvice logbackInst =\n        (LoggingInstrumentationAdvice) INSTRUMENTATION_ADVICE_CLASS.newInstance();\n    logbackInst.initialize(instrumentation, debug);\n  }\n\n  /**\n   * Loads the advice class into the bootstrap target of instrumentation.\n   *\n   * @param instrumentation\n   * @throws IOException\n   */\n  private static void injectBootstrapClasses(Instrumentation instrumentation) throws IOException {\n    File tempDir = Files.createTempDirectory(\"logback-bytebuddy\").toFile();\n    tempDir.deleteOnExit();\n\n    // Inject the instrumentation advice class directly\n    byte[] classData = read(INSTRUMENTATION_ADVICE_CLASS);\n    TypeDescription typeDescription =\n        new TypeDescription.ForLoadedType(INSTRUMENTATION_ADVICE_CLASS);\n    ClassInjector classInjector =\n        ClassInjector.UsingInstrumentation.of(tempDir, BOOTSTRAP, instrumentation);\n    classInjector.inject(singletonMap(typeDescription, classData));\n  }\n}\n"
  },
  {
    "path": "logback-bytebuddy/src/main/java/com/tersesystems/logback/bytebuddy/LoggingInstrumentationAdvice.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.bytebuddy;\n\nimport com.tersesystems.logback.bytebuddy.impl.SystemFlow;\nimport com.typesafe.config.*;\nimport java.io.File;\nimport java.lang.instrument.Instrumentation;\nimport java.lang.reflect.Method;\nimport java.util.*;\nimport java.util.stream.Collectors;\nimport net.bytebuddy.agent.builder.AgentBuilder;\nimport net.bytebuddy.asm.Advice;\nimport net.bytebuddy.implementation.bytecode.assign.Assigner;\nimport net.bytebuddy.matcher.ElementMatcher;\nimport net.bytebuddy.matcher.ElementMatchers;\nimport net.bytebuddy.matcher.StringMatcher;\n\n/** The code to be added on entry / exit to the methods under instrumentation. */\npublic class LoggingInstrumentationAdvice {\n\n  private static final String LOGBACK = \"logback\";\n\n  private static final String LOGBACK_TEST = \"logback-test\";\n\n  private static final String LOGBACK_REFERENCE_CONF = \"logback-reference.conf\";\n\n  private static final String CONFIG_FILE_PROPERTY = \"terse.logback.configurationFile\";\n\n  private static final ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();\n\n  // We need to load the implementation of enter / exit methods from the system classloader,\n  // so that we don't end up hauling SLF4J impl factory into bootstrap classloader, which\n  // will hopelessly confuse the JVM.\n  public static Method enterMethod;\n\n  static {\n    try {\n      String className = \"com.tersesystems.logback.bytebuddy.impl.Enter\";\n      Class<?> enterClass = systemClassLoader.loadClass(className);\n      enterMethod = enterClass.getMethod(\"apply\", String.class, Object[].class);\n    } catch (Exception e) {\n      e.printStackTrace();\n    }\n  }\n\n  public static Method exitMethod;\n\n  static {\n    try {\n      String className = \"com.tersesystems.logback.bytebuddy.impl.Exit\";\n      Class<?> exitClass = systemClassLoader.loadClass(className);\n      exitMethod =\n          exitClass.getMethod(\"apply\", String.class, Object[].class, Throwable.class, Object.class);\n    } catch (Exception e) {\n      e.printStackTrace();\n    }\n  }\n\n  void initialize(Instrumentation instrumentation, boolean debug) {\n    try {\n      Config config = generateConfig(systemClassLoader, debug);\n      AdviceConfig adviceConfig = generateAdviceConfig(systemClassLoader, config, debug);\n      if (debug) {\n        System.out.println(\"Generated Advice Config = \" + adviceConfig);\n      }\n\n      SystemFlow.setServiceName(adviceConfig.getServiceName());\n\n      AgentBuilder agentBuilder =\n          new LoggingInstrumentationByteBuddyBuilder()\n              .builderFromConfigWithRetransformation(adviceConfig);\n\n      // The debugging listener shows what classes are being picked up by the instrumentation\n      if (debug) {\n        AgentBuilder.Listener debugListener = createDebugListener(adviceConfig.classNames());\n        agentBuilder = agentBuilder.with(debugListener);\n      }\n      agentBuilder.installOn(instrumentation);\n    } catch (Exception e) {\n      e.printStackTrace();\n    }\n  }\n\n  // The code here recapitulates the logback-config code, but in a bootstrap classloader.\n  // This does mean that typesafe-config classes are pulled from bootstrap thereafter, but\n  // this is pretty safe.\n  public static Config generateConfig(ClassLoader classLoader, boolean debug) {\n    // Look for logback.json, logback.conf, logback.properties\n    Config systemProperties = ConfigFactory.systemProperties();\n    String fileName = System.getProperty(CONFIG_FILE_PROPERTY);\n    Config file = ConfigFactory.empty();\n    if (fileName != null) {\n      file = ConfigFactory.parseFile(new File(fileName));\n    }\n\n    Config testResources = ConfigFactory.parseResourcesAnySyntax(classLoader, LOGBACK_TEST);\n    Config resources = ConfigFactory.parseResourcesAnySyntax(classLoader, LOGBACK);\n    Config reference = ConfigFactory.parseResources(classLoader, LOGBACK_REFERENCE_CONF);\n\n    Config config =\n        systemProperties // Look for a property from system properties first...\n            .withFallback(file) // if we don't find it, then look in an explicitly defined file...\n            .withFallback(\n                testResources) // if not, then if logback-test.conf exists, look for it there...\n            .withFallback(resources) // then look in logback.conf...\n            .withFallback(reference) // and then finally in logback-reference.conf.\n            .resolve(); // Tell config that we want to use ${?ENV_VAR} type stuff.\n\n    // Add a check to show the config value if nothing is working...\n    if (debug) {\n      String configString = config.root().render(ConfigRenderOptions.defaults());\n      System.out.println(configString);\n    }\n    return config;\n  }\n\n  public static AdviceConfig generateAdviceConfig(\n      ClassLoader classLoader, Config config, boolean debug) throws Exception {\n    List<AdviceConfig> configs = new ArrayList<>();\n\n    String serviceName = config.getString(\"logback.bytebuddy.service-name\");\n    AdviceConfig adviceConfig = new AdviceConfig(classLoader, serviceName);\n\n    Set<Map.Entry<String, ConfigValue>> entries =\n        config.getConfig(\"logback.bytebuddy.tracing\").entrySet();\n    for (Map.Entry<String, ConfigValue> entry : entries) {\n      String className = clean(entry.getKey());\n      ConfigValue value = entry.getValue();\n      if (value.valueType() == ConfigValueType.LIST) {\n        List<String> methodNames =\n            ((List<String>) value.unwrapped())\n                .stream().map(LoggingInstrumentationAdvice::clean).collect(Collectors.toList());\n        if (methodNames.size() == 1 && Objects.equals(methodNames.get(0), \"*\")) {\n          if (debug) {\n            System.out.println(\"Using wildcard matching for class \" + className);\n          }\n          adviceConfig.addTrace(className);\n        } else {\n          adviceConfig.addTrace(className, methodNames);\n        }\n      } else {\n        throw new IllegalStateException(\"unknown config!\");\n      }\n    }\n    return adviceConfig;\n  }\n\n  private static String clean(String key) {\n    return key.replaceAll(\"\\\"\", \"\").trim();\n  }\n\n  private static AgentBuilder.Listener createDebugListener(List<String> classNames) {\n    return new AgentBuilder.Listener.Filtering(\n        stringMatcher(classNames), AgentBuilder.Listener.StreamWriting.toSystemOut());\n  }\n\n  public static ElementMatcher.Junction<? super String> stringMatcher(\n      Collection<String> typeNames) {\n    boolean seen = false;\n    ElementMatcher.Junction<? super String> acc = ElementMatchers.none();\n    for (String typeName : typeNames) {\n      StringMatcher stringMatcher = new StringMatcher(typeName, StringMatcher.Mode.EQUALS_FULLY);\n      if (!seen) {\n        seen = true;\n        acc = stringMatcher;\n      } else {\n        acc = acc.or(stringMatcher);\n      }\n    }\n    return acc;\n  }\n\n  @Advice.OnMethodEnter\n  public static void enter(\n      @Advice.Origin(\"#t|#m|#d|#s\") String origin, @Advice.AllArguments Object[] allArguments)\n      throws Exception {\n    enterMethod.invoke(null, origin, allArguments);\n  }\n\n  @Advice.OnMethodExit(onThrowable = Throwable.class)\n  public static void exit(\n      @Advice.Origin(\"#t|#m|#d|#s|#r\") String origin,\n      @Advice.AllArguments Object[] allArguments,\n      @Advice.Thrown Throwable thrown,\n      @Advice.Return(typing = Assigner.Typing.DYNAMIC) Object returnValue)\n      throws Exception {\n    exitMethod.invoke(null, origin, allArguments, thrown, returnValue);\n  }\n}\n"
  },
  {
    "path": "logback-bytebuddy/src/main/java/com/tersesystems/logback/bytebuddy/LoggingInstrumentationByteBuddyBuilder.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.bytebuddy;\n\nimport static net.bytebuddy.matcher.ElementMatchers.*;\nimport static net.bytebuddy.matcher.ElementMatchers.any;\n\nimport net.bytebuddy.agent.builder.AgentBuilder;\nimport net.bytebuddy.asm.Advice;\nimport net.bytebuddy.asm.AsmVisitorWrapper;\nimport net.bytebuddy.description.field.FieldDescription;\nimport net.bytebuddy.description.field.FieldList;\nimport net.bytebuddy.description.method.MethodDescription;\nimport net.bytebuddy.description.method.MethodList;\nimport net.bytebuddy.description.type.TypeDescription;\nimport net.bytebuddy.implementation.Implementation;\nimport net.bytebuddy.jar.asm.ClassVisitor;\nimport net.bytebuddy.jar.asm.Label;\nimport net.bytebuddy.jar.asm.MethodVisitor;\nimport net.bytebuddy.jar.asm.Opcodes;\nimport net.bytebuddy.matcher.ElementMatcher;\nimport net.bytebuddy.pool.TypePool;\n\n/** Creates byte buddy agent builders with the LoggingInstrumentation advice. */\npublic class LoggingInstrumentationByteBuddyBuilder {\n\n  private static final MethodInfoLookup METHOD_INFO_LOOKUP = MethodInfoLookup.getInstance();\n\n  private static final Class<?> INSTRUMENTATION_ADVICE_CLASS = LoggingInstrumentationAdvice.class;\n\n  // Is there a better way to handle upgrades here?\n  // As far as I can tell, we just want the highest number possible.\n  // https://stackoverflow.com/questions/63399682/how-do-i-map-asms-api-version-in-opcodes-to-java-version\n  private static final int ASM_API = Opcodes.ASM9;\n\n  /**\n   * Creates a builder from the element matchers.\n   *\n   * @param typesMatcher an element matcher for types we should instrument.\n   * @param methodsMatcher an element matcher for the methods in the types that should be\n   *     instrumented.\n   * @return the agent builder\n   */\n  public AgentBuilder builderFromConfig(\n      ElementMatcher<? super TypeDescription> typesMatcher,\n      ElementMatcher<? super MethodDescription> methodsMatcher) {\n    return new AgentBuilder.Default()\n        .with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)\n        .with(AgentBuilder.InitializationStrategy.NoOp.INSTANCE)\n        .with(AgentBuilder.TypeStrategy.Default.REDEFINE)\n        .disableClassFormatChanges() // frozen instrumented types\n        .type(typesMatcher) // for these classes...\n        .transform(\n            (builder, type, classLoader, module) -> {\n              // ...apply this advice to these methods.\n              Advice to = Advice.to(INSTRUMENTATION_ADVICE_CLASS);\n              AsmVisitorWrapper on = to.on(methodsMatcher);\n              AsmVisitorWrapper lineWrapper = new LineWrapper();\n              return builder.visit(lineWrapper).visit(on);\n            });\n  }\n\n  static class LineWrapper extends AsmVisitorWrapper.AbstractBase {\n    @Override\n    public ClassVisitor wrap(\n        TypeDescription instrumentedType,\n        ClassVisitor classVisitor,\n        Implementation.Context implementationContext,\n        TypePool typePool,\n        FieldList<FieldDescription.InDefinedShape> fields,\n        MethodList<?> methods,\n        int writerFlags,\n        int readerFlags) {\n      return new ClassVisitor(ASM_API, classVisitor) {\n        private String className;\n        private String source;\n\n        @Override\n        public void visitSource(String source, String debug) {\n          this.source = source;\n          super.visitSource(source, debug);\n        }\n\n        @Override\n        public void visit(\n            int version,\n            int access,\n            String name,\n            String signature,\n            String superName,\n            String[] interfaces) {\n          this.className = name != null ? name.replace('/', '.') : null;\n          super.visit(version, access, name, signature, superName, interfaces);\n        }\n\n        @Override\n        public MethodVisitor visitMethod(int access, String n, String d, String s, String[] e) {\n          MethodVisitor methodVisitor = super.visitMethod(access, n, d, s, e);\n          return new MethodVisitor(Opcodes.ASM5, methodVisitor) {\n            int line;\n            final MethodInfo methodInfo = new MethodInfo(n, d, source);\n            boolean isStart = false;\n\n            @Override\n            public void visitCode() {\n              isStart = true;\n              super.visitCode();\n            }\n\n            @Override\n            public void visitLineNumber(int line, Label start) {\n              if (isStart) {\n                methodInfo.setStartLine(line);\n                isStart = false;\n              }\n              this.line = line;\n              super.visitLineNumber(line, start);\n            }\n\n            @Override\n            public void visitEnd() {\n              methodInfo.setEndLine(line);\n              METHOD_INFO_LOOKUP.add(className, methodInfo);\n              super.visitEnd();\n            }\n          };\n        }\n      };\n    }\n  }\n\n  /**\n   * Use this method if you want to redefine system classloader classes.\n   *\n   * @param typesMatcher an element matcher for types we should instrument.\n   * @param methodsMatcher an element matcher for the methods in the types that should be\n   *     instrumented.\n   * @return agent builder with ignore and RETRANSFORMATION set.\n   */\n  public AgentBuilder builderFromConfigWithRetransformation(\n      ElementMatcher<? super TypeDescription> typesMatcher,\n      ElementMatcher<? super MethodDescription> methodsMatcher) {\n    return withSystemClassLoaderMatching(builderFromConfig(typesMatcher, methodsMatcher));\n  }\n\n  protected AgentBuilder withSystemClassLoaderMatching(AgentBuilder builder) {\n    return builder\n        .ignore(ignoreMatchers()) // do not ignore system classes\n        .with(\n            AgentBuilder.RedefinitionStrategy\n                .RETRANSFORMATION); // try to retransform already loaded classes\n  }\n\n  public AgentBuilder.RawMatcher.ForElementMatchers ignoreMatchers() {\n    ElementMatcher.Junction<? super TypeDescription> matchers =\n        nameStartsWith(\"net.bytebuddy.\")\n            .or(nameStartsWith(\"com.tersesystems.logback.bytebuddy.\"))\n            .or(nameStartsWith(\"org.slf4j.\"))\n            .or(nameStartsWith(\"ch.qos.logback.\"))\n            .or(isSynthetic());\n    return new AgentBuilder.RawMatcher.ForElementMatchers(matchers, any(), any());\n  }\n\n  public AgentBuilder builderFromConfig(\n      ElementMatcher<? super TypeDescription> typesMatcher,\n      ElementMatcher<? super MethodDescription> methodsMatcher,\n      AgentBuilder.Listener listener) {\n    return builderFromConfig(typesMatcher, methodsMatcher).with(listener);\n  }\n\n  public AgentBuilder builderFromConfig(AdviceConfig c) {\n    return builderFromConfig(c.types(), c.methods());\n  }\n\n  public AgentBuilder builderFromConfigWithRetransformation(AdviceConfig c) {\n    return builderFromConfigWithRetransformation(c.types(), c.methods());\n  }\n}\n"
  },
  {
    "path": "logback-bytebuddy/src/main/java/com/tersesystems/logback/bytebuddy/MethodInfo.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.bytebuddy;\n\nimport java.util.Objects;\n\n/**\n * Provides line number and source at implementation time without the overhead of fillInStackTrace.\n */\npublic class MethodInfo {\n  final String methodName;\n  final String descriptor;\n  public final String source;\n  private int startLine;\n  private int endLine;\n\n  MethodInfo(String methodName, String descriptor, String source) {\n    this.methodName = Objects.requireNonNull(methodName);\n    this.descriptor = descriptor;\n    this.source = source;\n  }\n\n  public void setStartLine(int line) {\n    this.startLine = line;\n  }\n\n  public void setEndLine(int line) {\n    this.endLine = line;\n  }\n\n  public int getStartLine() {\n    return startLine;\n  }\n\n  public int getEndLine() {\n    return endLine;\n  }\n\n  @Override\n  public String toString() {\n    return \"MethodInfo{\"\n        + \"methodName='\"\n        + methodName\n        + '\\''\n        + \", descriptor='\"\n        + descriptor\n        + '\\''\n        + \", source='\"\n        + source\n        + '\\''\n        + '}';\n  }\n}\n"
  },
  {
    "path": "logback-bytebuddy/src/main/java/com/tersesystems/logback/bytebuddy/MethodInfoLookup.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.bytebuddy;\n\nimport java.util.HashSet;\nimport java.util.Objects;\nimport java.util.Optional;\nimport java.util.Set;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.ConcurrentMap;\nimport java.util.function.Predicate;\n\npublic class MethodInfoLookup {\n\n  private final ConcurrentMap<String, Set<MethodInfo>> classNameToMethods =\n      new ConcurrentHashMap<>();\n\n  public static MethodInfoLookup getInstance() {\n    return SingletonHolder.instance;\n  }\n\n  static class SingletonHolder {\n    public static MethodInfoLookup instance = new MethodInfoLookup();\n  }\n\n  public void add(String className, MethodInfo methodInfo) {\n    Set<MethodInfo> infos = classNameToMethods.computeIfAbsent(className, k -> new HashSet<>());\n    infos.add(methodInfo);\n  }\n\n  public Optional<MethodInfo> find(String className, String methodName, String descriptor) {\n    Set<MethodInfo> infos = classNameToMethods.computeIfAbsent(className, k -> new HashSet<>());\n    return infos.stream().filter(matchingInfo(methodName, descriptor)).findFirst();\n  }\n\n  private Predicate<MethodInfo> matchingInfo(String methodName, String descriptor) {\n    return info ->\n        Objects.equals(info.methodName, methodName) && Objects.equals(descriptor, info.descriptor);\n  }\n}\n"
  },
  {
    "path": "logback-bytebuddy/src/main/java/com/tersesystems/logback/bytebuddy/impl/DeclaringTypeLoggerResolver.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.bytebuddy.impl;\n\nimport java.util.Objects;\nimport java.util.function.Supplier;\nimport org.slf4j.ILoggerFactory;\nimport org.slf4j.Logger;\n\n/** Returns the logger with the class that was instrumented. */\npublic class DeclaringTypeLoggerResolver implements LoggerResolver {\n\n  private final Supplier<ILoggerFactory> loggerFactory;\n\n  public DeclaringTypeLoggerResolver(Supplier<ILoggerFactory> loggerFactory) {\n    this.loggerFactory = Objects.requireNonNull(loggerFactory);\n  }\n\n  @Override\n  public Logger resolve(String origin) {\n    int firstPipe = origin.indexOf('|');\n    String declaringType = origin.substring(0, firstPipe);\n    return loggerFactory.get().getLogger(declaringType);\n  }\n}\n"
  },
  {
    "path": "logback-bytebuddy/src/main/java/com/tersesystems/logback/bytebuddy/impl/Enter.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.bytebuddy.impl;\n\nimport static com.tersesystems.logback.bytebuddy.impl.SystemFlow.*;\nimport static net.logstash.logback.argument.StructuredArguments.v;\nimport static net.logstash.logback.marker.Markers.append;\n\nimport com.tersesystems.logback.bytebuddy.MethodInfo;\nimport com.tersesystems.logback.bytebuddy.MethodInfoLookup;\nimport java.util.Optional;\nimport net.logstash.logback.argument.StructuredArgument;\nimport net.logstash.logback.marker.LogstashMarker;\nimport org.slf4j.Logger;\nimport org.slf4j.Marker;\n\npublic class Enter {\n\n  private static final String format = \"entering: {}.{}{} with {}\";\n  private static final String formatWithSource = \"entering: {}.{}{} with {} from source {}:{}\";\n\n  public static void apply(String origin, Object[] allArguments) {\n    Logger logger = getLogger(origin);\n    if (logger != null && logger.isTraceEnabled(ENTRY_MARKER)) {\n\n      String[] args = origin.split(\"\\\\|\");\n      String declaringType = args[0];\n      String method = args[1];\n      String descriptor = args[2];\n      String signature = args[3];\n      StructuredArgument aClass = v(\"class\", declaringType);\n      StructuredArgument aMethod = v(\"method\", method);\n      StructuredArgument aSignature = v(\"signature\", signature);\n      StructuredArgument arrayParameters = safeArguments(allArguments);\n\n      String name = createName(declaringType, method, signature);\n      pushSpan(name);\n      LogstashMarker nameMarker = append(\"name\", name);\n      Marker markers = baseMarkers().and(nameMarker).and(ENTRY_MARKER);\n\n      MethodInfoLookup lookup = MethodInfoLookup.getInstance();\n      Optional<MethodInfo> methodInfo = lookup.find(declaringType, method, descriptor);\n      if (methodInfo.isPresent()) {\n        MethodInfo mi = methodInfo.get();\n        StructuredArgument aSource = v(\"source\", mi.source);\n        StructuredArgument aLineNumber = v(\"line\", mi.getStartLine());\n\n        logger.trace(\n            markers,\n            formatWithSource,\n            aClass,\n            aMethod,\n            aSignature,\n            arrayParameters,\n            aSource,\n            aLineNumber);\n      } else {\n        logger.trace(markers, format, aClass, aMethod, aSignature, arrayParameters);\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "logback-bytebuddy/src/main/java/com/tersesystems/logback/bytebuddy/impl/Exit.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.bytebuddy.impl;\n\nimport static com.tersesystems.logback.bytebuddy.impl.SystemFlow.*;\nimport static net.logstash.logback.argument.StructuredArguments.kv;\nimport static net.logstash.logback.argument.StructuredArguments.v;\nimport static net.logstash.logback.marker.Markers.empty;\n\nimport com.tersesystems.logback.bytebuddy.MethodInfo;\nimport com.tersesystems.logback.bytebuddy.MethodInfoLookup;\nimport java.util.Optional;\nimport net.logstash.logback.argument.StructuredArgument;\nimport net.logstash.logback.marker.LogstashMarker;\nimport org.slf4j.Logger;\nimport org.slf4j.Marker;\n\npublic class Exit {\n\n  private static String exitFormatWithSource =\n      \"exiting: {}.{}{} with {} => ({} {}) from source {}:{}\";\n  private static String exitFormat = \"exiting: {}.{}{} with {} => ({} {})\";\n\n  public static void apply(\n      String origin, Object[] allArguments, Throwable thrown, Object returnValue) {\n    Logger logger = getLogger(origin);\n    if (logger != null && logger.isTraceEnabled(EXIT_MARKER)) {\n      String[] args = origin.split(\"\\\\|\");\n      String declaringType = args[0];\n      String method = args[1];\n      String descriptor = args[2];\n      String signature = args[3];\n      String returnType = args[4];\n      StructuredArgument aClass = v(\"class\", declaringType); // ClassCalledByAgent\n      StructuredArgument aMethod = v(\"method\", method); // printArgument\n      StructuredArgument aSignature = v(\"signature\", signature); // (java.lang.String)\n      // StructuredArgument aDescriptor = kv(\"descriptor\", descriptor); //\n      // descriptor=(Ljava/lang/String;)V\n\n      StructuredArgument safeArguments = safeArguments(allArguments);\n\n      LogstashMarker spanMarker = popSpan().map(SystemFlow::createMarker).orElse(empty());\n      if (thrown != null) {\n        Marker markers = spanMarker.and(THROWING_MARKER);\n        // Always include the thrown at the end of the list as SLF4J will take care of stack trace.\n        logger.error(\n            markers,\n            \"throwing: {}.{}{} with {}\",\n            aClass,\n            aMethod,\n            aSignature,\n            safeArguments,\n            thrown);\n      } else {\n        StructuredArgument aReturnType = kv(\"return_type\", returnType);\n        StructuredArgument safeReturnValue = safeReturnValue(returnValue);\n\n        MethodInfoLookup lookup = MethodInfoLookup.getInstance();\n        Optional<MethodInfo> methodInfo = lookup.find(declaringType, method, descriptor);\n        Marker markers = spanMarker.and(EXIT_MARKER);\n        if (methodInfo.isPresent()) {\n          MethodInfo mi = methodInfo.get();\n          StructuredArgument aSource = v(\"source\", mi.source);\n          StructuredArgument aLineNumber = v(\"line\", mi.getEndLine());\n          ;\n          logger.trace(\n              markers,\n              exitFormatWithSource,\n              aClass,\n              aMethod,\n              aSignature,\n              safeArguments,\n              aReturnType,\n              safeReturnValue,\n              aSource,\n              aLineNumber);\n        } else {\n          logger.trace(\n              markers,\n              exitFormat,\n              aClass,\n              aMethod,\n              aSignature,\n              safeArguments,\n              aReturnType,\n              safeReturnValue);\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "logback-bytebuddy/src/main/java/com/tersesystems/logback/bytebuddy/impl/FixedLoggerResolver.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.bytebuddy.impl;\n\nimport static java.util.Objects.requireNonNull;\n\nimport org.slf4j.Logger;\n\n/** Always returns the same logger. */\npublic class FixedLoggerResolver implements LoggerResolver {\n  private final Logger logger;\n\n  public FixedLoggerResolver(Logger logger) {\n    this.logger = requireNonNull(logger);\n  }\n\n  @Override\n  public Logger resolve(String origin) {\n    return logger;\n  }\n}\n"
  },
  {
    "path": "logback-bytebuddy/src/main/java/com/tersesystems/logback/bytebuddy/impl/LoggerResolver.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.bytebuddy.impl;\n\nimport org.slf4j.Logger;\n\n/** Finds a logger given some input. */\npublic interface LoggerResolver {\n  /**\n   * @param origin the class name plus other stuff, provided from bytebuddy advice.\n   * @return a logger.\n   */\n  Logger resolve(String origin);\n}\n"
  },
  {
    "path": "logback-bytebuddy/src/main/java/com/tersesystems/logback/bytebuddy/impl/SafeArguments.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.bytebuddy.impl;\n\nimport java.security.cert.X509Certificate;\nimport java.util.*;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\n\npublic class SafeArguments {\n  public List<String> apply(Object[] allArguments) {\n    return Arrays.stream(allArguments).map(this::apply).collect(Collectors.toList());\n  }\n\n  public String apply(Object returnValue) {\n    try {\n      if (returnValue instanceof Collection<?>) {\n        return parseCollection((Collection<Object>) returnValue);\n      }\n\n      if (returnValue instanceof Object[]) {\n        return parseArray((Object[]) returnValue);\n      }\n\n      if (returnValue instanceof X509Certificate) {\n        return parseCertificate((X509Certificate) returnValue);\n      }\n\n      return Objects.toString(returnValue);\n    } catch (Exception e) {\n      return \"Exception rendering safeArguments: \" + e.toString();\n    }\n  }\n\n  private String parseCertificate(X509Certificate cert) {\n    String s = cert.getSerialNumber().toString(16);\n    String sub = cert.getSubjectDN().getName();\n    return \"X509Certificate(serialNumber = \" + s + \", subject = \" + sub + \")\";\n  }\n\n  private String parseArray(Object[] returnValue) {\n    return parseStream(Arrays.stream(returnValue));\n  }\n\n  private String parseCollection(Collection<Object> coll) {\n    return parseStream(coll.stream());\n  }\n\n  private String parseStream(Stream<Object> stream) {\n    return stream.map(this::apply).collect(Collectors.joining(\",\"));\n  }\n}\n"
  },
  {
    "path": "logback-bytebuddy/src/main/java/com/tersesystems/logback/bytebuddy/impl/SystemFlow.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.bytebuddy.impl;\n\nimport static net.logstash.logback.argument.StructuredArguments.kv;\n\nimport com.fasterxml.uuid.impl.RandomBasedGenerator;\nimport com.tersesystems.logback.tracing.SpanInfo;\nimport com.tersesystems.logback.tracing.SpanMarkerFactory;\nimport com.tersesystems.logback.tracing.Tracer;\nimport java.util.List;\nimport java.util.Optional;\nimport java.util.function.Supplier;\nimport net.logstash.logback.argument.StructuredArgument;\nimport net.logstash.logback.marker.LogstashMarker;\nimport net.logstash.logback.marker.Markers;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.slf4j.Marker;\nimport org.slf4j.MarkerFactory;\n\npublic final class SystemFlow {\n  // https://github.com/qos-ch/slf4j/blob/master/slf4j-ext/src/main/java/org/slf4j/ext/XLogger.java#L44\n  public static final Marker FLOW_MARKER = MarkerFactory.getMarker(\"FLOW\");\n  public static final Marker ENTRY_MARKER = MarkerFactory.getMarker(\"ENTRY\");\n  public static final Marker EXIT_MARKER = MarkerFactory.getMarker(\"EXIT\");\n  public static final Marker EXCEPTION_MARKER = MarkerFactory.getMarker(\"EXCEPTION\");\n  public static final Marker THROWING_MARKER = MarkerFactory.getMarker(\"THROWING\");\n\n  private static final SpanMarkerFactory markerFactory = new SpanMarkerFactory();\n\n  static {\n    ENTRY_MARKER.add(FLOW_MARKER);\n    EXIT_MARKER.add(FLOW_MARKER);\n    THROWING_MARKER.add(EXCEPTION_MARKER);\n  }\n\n  private static String serviceName;\n  private static Supplier<String> idGenerator;\n  private static SafeArguments safeArguments = new SafeArguments();\n\n  static {\n    // The out of the box UUID.randomUUID() is synchronized, which will block threads and\n    // generally gum things up.  The faster XML one is better, but still need to benchmark\n    // considering how tracing can be injected anywhere.\n    RandomBasedGenerator uuidGenerator = new RandomBasedGenerator(null);\n    SystemFlow.setIdGenerator(() -> uuidGenerator.generate().toString());\n  }\n\n  private static LoggerResolver loggerResolver =\n      new DeclaringTypeLoggerResolver(LoggerFactory::getILoggerFactory);\n\n  public static LoggerResolver getLoggerResolver() {\n    return loggerResolver;\n  }\n\n  public static void setLoggerResolver(LoggerResolver loggerResolver) {\n    SystemFlow.loggerResolver = loggerResolver;\n  }\n\n  public static Logger getLogger(String origin) {\n    return loggerResolver.resolve(origin);\n  }\n\n  public static void setServiceName(String serviceName) {\n    SystemFlow.serviceName = serviceName;\n  }\n\n  public static void setIdGenerator(Supplier<String> idGenerator) {\n    SystemFlow.idGenerator = idGenerator;\n  }\n\n  public static SafeArguments getSafeArguments() {\n    return safeArguments;\n  }\n\n  public static void setSafeArguments(SafeArguments safeArguments) {\n    SystemFlow.safeArguments = safeArguments;\n  }\n\n  public static LogstashMarker createMarker(SpanInfo span) {\n    return baseMarkers().and(markerFactory.create(span));\n  }\n\n  public static void pushSpan(String name) {\n    Tracer.pushSpan(name, serviceName, idGenerator);\n  }\n\n  public static Optional<SpanInfo> popSpan() {\n    return Tracer.popSpan();\n  }\n\n  static StructuredArgument safeReturnValue(Object returnValue) {\n    String safeReturnValue = safeArguments.apply(returnValue);\n    return kv(\"return_value\", safeReturnValue);\n  }\n\n  static StructuredArgument safeArguments(Object[] allArguments) {\n    List<String> safeArgs = safeArguments.apply(allArguments);\n    return kv(\"arguments\", safeArgs);\n  }\n\n  static String createName(String className, String method, String signature) {\n    return className + \".\" + method + signature;\n  }\n\n  static LogstashMarker baseMarkers() {\n    //        Thread t = Thread.currentThread();\n    //        //LogstashMarker threadNameMarker = append(\"trace.thread_name\", t.getName());\n    //        LogstashMarker threadIdMarker = append(\"thread_id\", t.getId());\n    //        return threadIdMarker.and(threadIdMarker);\n    return Markers.empty();\n  }\n}\n"
  },
  {
    "path": "logback-bytebuddy/src/test/java/com/tersesystems/logback/bytebuddy/AdviceConfigTest.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.bytebuddy;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport com.typesafe.config.Config;\nimport org.junit.jupiter.api.Test;\n\npublic class AdviceConfigTest {\n\n  @Test\n  public void testConfig() throws Exception {\n    ClassLoader classLoader = ClassLoader.getSystemClassLoader();\n    Config config = LoggingInstrumentationAdvice.generateConfig(classLoader, false);\n    AdviceConfig adviceConfig =\n        LoggingInstrumentationAdvice.generateAdviceConfig(classLoader, config, false);\n    assertThat(adviceConfig.classNames())\n        .contains(\"com.tersesystems.logback.bytebuddy.ClassCalledByAgent\");\n  }\n}\n"
  },
  {
    "path": "logback-bytebuddy/src/test/java/com/tersesystems/logback/bytebuddy/ClassCalledByAgent.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.bytebuddy;\n\n/** This class does no logging. */\npublic class ClassCalledByAgent {\n  public void printStatement() {\n    System.out.println(\"I am a simple println method with no logging\");\n  }\n\n  public void printArgument(String arg) {\n    System.out.println(\"I am a simple println, printing \" + arg);\n  }\n\n  public void throwException(String arg) {\n    throw new RuntimeException(\"I'm a squirrel!\");\n  }\n}\n"
  },
  {
    "path": "logback-bytebuddy/src/test/java/com/tersesystems/logback/bytebuddy/InProcessInstrumentationExample.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.bytebuddy;\n\nimport static net.bytebuddy.agent.builder.AgentBuilder.Listener;\n\nimport com.tersesystems.logback.bytebuddy.impl.FixedLoggerResolver;\nimport com.tersesystems.logback.bytebuddy.impl.SystemFlow;\nimport com.typesafe.config.Config;\nimport java.util.List;\nimport net.bytebuddy.agent.ByteBuddyAgent;\nimport net.bytebuddy.agent.builder.AgentBuilder;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n * Run the agent inside an already running JVM.\n *\n * <p>This will instrument classes that have not already been loaded into the JVM, such as\n * ClassCalledByAgent, but will not allow you to instrument classes loaded by the system\n * classloader, such as java.lang.Thread.\n *\n * <p>This should still be perfectly fine for 99% of users who don't need an agent loaded from the\n * command line.\n */\npublic class InProcessInstrumentationExample {\n\n  public static AgentBuilder.Listener createDebugListener(List<String> classNames) {\n    return new AgentBuilder.Listener.Filtering(\n        LoggingInstrumentationAdvice.stringMatcher(classNames),\n        AgentBuilder.Listener.StreamWriting.toSystemOut());\n  }\n\n  public static void main(String[] args) throws Exception {\n    // Helps if you install the byte buddy agents before anything else at all happens...\n    ByteBuddyAgent.install();\n\n    Logger logger = LoggerFactory.getLogger(InProcessInstrumentationExample.class);\n    SystemFlow.setLoggerResolver(new FixedLoggerResolver(logger));\n\n    ClassLoader classLoader = ClassLoader.getSystemClassLoader();\n    Config config = LoggingInstrumentationAdvice.generateConfig(classLoader, false);\n    AdviceConfig adviceConfig =\n        LoggingInstrumentationAdvice.generateAdviceConfig(classLoader, config, false);\n\n    // The debugging listener shows what classes are being picked up by the instrumentation\n    Listener debugListener = createDebugListener(adviceConfig.classNames());\n    new LoggingInstrumentationByteBuddyBuilder()\n        .builderFromConfig(adviceConfig)\n        .with(debugListener)\n        .installOnByteBuddyAgent();\n\n    // No code change necessary here, you can wrap completely in the agent...\n    ClassCalledByAgent classCalledByAgent = new ClassCalledByAgent();\n    classCalledByAgent.printStatement();\n    classCalledByAgent.printArgument(\"42\");\n\n    try {\n      classCalledByAgent.throwException(\"hello world\");\n    } catch (Exception e) {\n      // I am too lazy to catch this exception.  I hope someone does it for me.\n    }\n  }\n}\n"
  },
  {
    "path": "logback-bytebuddy/src/test/java/com/tersesystems/logback/bytebuddy/PreloadedInstrumentationExample.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.bytebuddy;\n\n/**\n * Borrowed from securityfixer, showing tracing when you set the security manager.\n *\n * <p>Will not work on native methods, i.e. `System.currentTimeMillis`.\n *\n * <p>Move this into the main source path and redeploy if you want to test (I can't figure out how\n * to do agent stuff in Gradle)\n */\npublic class PreloadedInstrumentationExample {\n  public static void main(String[] args) throws Exception {\n    Thread thread = Thread.currentThread();\n    thread.run();\n  }\n}\n"
  },
  {
    "path": "logback-bytebuddy/src/test/resources/logback-test.xml",
    "content": "<!--\n  ~ SPDX-License-Identifier: CC0-1.0\n  ~\n  ~ Copyright 2018-2020 Will Sargent.\n  ~\n  ~ Licensed under the CC0 Public Domain Dedication;\n  ~ You may obtain a copy of the License at\n  ~\n  ~  http://creativecommons.org/publicdomain/zero/1.0/\n  -->\n<configuration>\n\n    <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n        <encoder>\n            <pattern>%-5relative %-5level %logger{35} - %msg%n</pattern>\n        </encoder>\n    </appender>\n\n    <root level=\"TRACE\">\n        <appender-ref ref=\"STDOUT\" />\n    </root>\n</configuration>\n\n"
  },
  {
    "path": "logback-bytebuddy/src/test/resources/logback.conf",
    "content": "logback.bytebuddy {\n  service-name = \"example-app\"\n\n  tracing {\n    \"com.tersesystems.logback.bytebuddy.ClassCalledByAgent\" = [\n      \"printStatement\",\n      \"printArgument\",\n      \"throwException\",\n    ]\n\n    \"java.lang.Thread\" = [\n      \"run\"\n    ]\n  }\n}"
  },
  {
    "path": "logback-censor/gradle.properties",
    "content": "#\n# SPDX-License-Identifier: CC0-1.0\n#\n# Copyright 2018-2020 Will Sargent.\n#\n# Licensed under the CC0 Public Domain Dedication;\n# You may obtain a copy of the License at\n#\n#  http://creativecommons.org/publicdomain/zero/1.0/\n#\n\nproject_description = Logback Censor Project (regular esxpression and Jackson JSON filtering)"
  },
  {
    "path": "logback-censor/logback-censor.gradle",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\ndependencies {\n    implementation project(\":logback-core\")\n\n    implementation \"org.slf4j:slf4j-api:$slf4jVersion\"\n    implementation \"net.logstash.logback:logstash-logback-encoder:$logstashVersion\"\n    implementation \"ch.qos.logback:logback-classic:$logbackVersion\"\n}\n"
  },
  {
    "path": "logback-censor/src/main/java/com/tersesystems/logback/censor/Censor.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.censor;\n\nimport com.tersesystems.logback.core.Component;\n\n/** Basic censor functionality. */\npublic interface Censor extends Component {\n  CharSequence censorText(CharSequence input);\n}\n"
  },
  {
    "path": "logback-censor/src/main/java/com/tersesystems/logback/censor/CensorAction.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.censor;\n\nimport static com.tersesystems.logback.censor.CensorConstants.CENSOR_BAG;\n\nimport ch.qos.logback.core.Context;\nimport ch.qos.logback.core.CoreConstants;\nimport ch.qos.logback.core.joran.action.Action;\nimport ch.qos.logback.core.joran.spi.ActionException;\nimport ch.qos.logback.core.joran.spi.InterpretationContext;\nimport ch.qos.logback.core.util.OptionHelper;\nimport java.util.HashMap;\nimport java.util.Map;\nimport org.xml.sax.Attributes;\n\npublic class CensorAction extends Action {\n  CensorContextAware censor;\n  private boolean inError = false;\n\n  @SuppressWarnings(\"unchecked\")\n  public void begin(InterpretationContext ic, String localName, Attributes attributes)\n      throws ActionException {\n    // We are just beginning, reset variables\n    censor = null;\n    inError = false;\n\n    // Ensure idempotency of a CENSOR_BAG\n    Map<String, Object> omap = ic.getObjectMap();\n    if (!omap.containsKey(CENSOR_BAG)) {\n      omap.put(CENSOR_BAG, new HashMap<String, CensorContextAware>());\n    }\n\n    String className = attributes.getValue(CLASS_ATTRIBUTE);\n    if (OptionHelper.isEmpty(className)) {\n      addError(\"Missing class name for censor. Near [\" + localName + \"] line \" + getLineNumber(ic));\n      inError = true;\n      return;\n    }\n\n    try {\n      addInfo(\"About to instantiate censor of type [\" + className + \"]\");\n      censor =\n          (CensorContextAware)\n              OptionHelper.instantiateByClassName(className, CensorContextAware.class, context);\n\n      // XXX we can get the censor here but it still doesn't have the parameters we need.\n      // OptionHelper.substVars()\n\n      Context icContext = ic.getContext();\n      if (censor != null) {\n        censor.setContext(icContext);\n      }\n\n      String censorName = ic.subst(attributes.getValue(NAME_ATTRIBUTE));\n\n      if (OptionHelper.isEmpty(censorName)) {\n        addWarn(\"No censor name given for censor of type \" + className + \"].\");\n      } else {\n        censor.setName(censorName);\n        addInfo(\"Naming censor as [\" + censorName + \"]\");\n      }\n\n      // The execution context contains a bag which contains the censors\n      // created thus far.\n      HashMap<String, CensorContextAware> censorBag =\n          (HashMap<String, CensorContextAware>) ic.getObjectMap().get(CENSOR_BAG);\n      getContext().putObject(CENSOR_BAG, censorBag);\n\n      // add the censorText just created to the censorText bag.\n      censorBag.put(censorName, censor);\n\n      ic.pushObject(censor);\n    } catch (Exception oops) {\n      inError = true;\n      addError(\"Could not create a Censor of type [\" + className + \"].\", oops);\n      throw new ActionException(oops);\n    }\n  }\n\n  private void addConverter() {\n    // Add a conversion rule automatically\n    Map<String, String> ruleRegistry =\n        (Map<String, String>) context.getObject(CoreConstants.PATTERN_RULE_REGISTRY);\n    if (ruleRegistry == null) {\n      ruleRegistry = new HashMap<String, String>();\n      context.putObject(CoreConstants.PATTERN_RULE_REGISTRY, ruleRegistry);\n    }\n    ruleRegistry.putIfAbsent(CensorConstants.CENSOR_RULE_NAME, CensorConverter.class.getName());\n  }\n\n  /**\n   * Once the children elements are also parsed, now is the time to activate the appender options.\n   */\n  public void end(InterpretationContext ec, String name) {\n    if (inError) {\n      return;\n    }\n\n    if (censor != null) {\n      censor.start();\n    }\n\n    Object o = ec.peekObject();\n\n    if (o != censor) {\n      addWarn(\n          \"The object at the end of the stack is not the censor named [\"\n              + censor.getName()\n              + \"] pushed earlier.\");\n    } else {\n      ec.popObject();\n    }\n  }\n}\n"
  },
  {
    "path": "logback-censor/src/main/java/com/tersesystems/logback/censor/CensorAttachable.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.censor;\n\npublic interface CensorAttachable {\n  void addCensor(CensorContextAware censor);\n}\n"
  },
  {
    "path": "logback-censor/src/main/java/com/tersesystems/logback/censor/CensorConstants.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.censor;\n\npublic class CensorConstants {\n\n  public static final String CENSOR_BAG = \"CENSOR_BAG\";\n  public static final String REF_ATTRIBUTE = \"ref\";\n  public static final String CENSOR_RULE_NAME = \"censor\";\n}\n"
  },
  {
    "path": "logback-censor/src/main/java/com/tersesystems/logback/censor/CensorContextAware.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.censor;\n\nimport ch.qos.logback.core.spi.ContextAware;\nimport ch.qos.logback.core.spi.LifeCycle;\n\npublic interface CensorContextAware extends Censor, ContextAware, LifeCycle {\n\n  /** Get the name of this appender. The name uniquely identifies the appender. */\n  String getName();\n\n  /**\n   * Set the name of this appender. The name is used by other components to identify this appender.\n   */\n  void setName(String name);\n}\n"
  },
  {
    "path": "logback-censor/src/main/java/com/tersesystems/logback/censor/CensorConverter.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.censor;\n\nimport static com.tersesystems.logback.censor.CensorConstants.CENSOR_BAG;\n\nimport ch.qos.logback.classic.spi.ILoggingEvent;\nimport ch.qos.logback.core.pattern.CompositeConverter;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * Censoring message converter for text.\n *\n * <p>Note that this does not filter out marker text or additional information related to the event,\n * i.e. it does not filter out exception text.\n *\n * <p>Note also that the censor converter only picks out one censor from the list.\n *\n * <pre>{@code\n * <conversionRule conversionWord=\"censor\"\n *   converterClass=\"com.tersesystems.logback.censor.CensorConverter\" />\n * }</pre>\n */\npublic class CensorConverter extends CompositeConverter<ILoggingEvent> {\n\n  private CensorContextAware censor;\n\n  @Override\n  public void start() {\n    super.start();\n    // There isn't a good way of referring to other objects without going through\n    // the context here, as the IC is not available to converters.\n    Map<String, CensorContextAware> censorBag =\n        (Map<String, CensorContextAware>) getContext().getObject(CENSOR_BAG);\n    if (censorBag == null || censorBag.isEmpty()) {\n      addError(\"Null or empty censor bag found in context!\");\n    }\n\n    // The censor name is given in the pattern encoder in the form \"%censor(%msg, censor-name)\"\n    // See logstash-logback-encoder for a more complex example:\n    // https://github.com/logstash/logstash-logback-encoder/blob/master/src/main/java/net/logstash/logback/stacktrace/ShortenedThrowableConverter.java\n    List<String> optionList = getOptionList();\n    addInfo(String.format(\"Pulling options %s\", optionList));\n    String censorName = getFirstOption();\n    if (censorName == null) {\n      censorName = censorBag.keySet().iterator().next();\n      addInfo(\n          String.format(\"Pulling first censor name %s from censor bag converter: \", censorName));\n    } else {\n      addInfo(String.format(\"Referencing explicit censor name %s in converter: \", censorName));\n    }\n\n    censor = censorBag.get(censorName);\n    if (censor == null) {\n      addError(String.format(\"No censor with name %s found in censor bag!\", censorName));\n    }\n  }\n  //\n  //    @Override\n  //    public String convert(ILoggingEvent event) {\n  //        return String.valueOf(censor.censorText(in));\n  //    }\n\n  @SuppressWarnings(\"unchecked\")\n  public String transform(ILoggingEvent event, String in) {\n    return String.valueOf(censor.censorText(in));\n  }\n}\n"
  },
  {
    "path": "logback-censor/src/main/java/com/tersesystems/logback/censor/CensorRefAction.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.censor;\n\nimport ch.qos.logback.core.joran.action.Action;\nimport ch.qos.logback.core.joran.spi.InterpretationContext;\nimport ch.qos.logback.core.util.OptionHelper;\nimport java.util.Map;\nimport org.xml.sax.Attributes;\n\npublic class CensorRefAction extends Action {\n  boolean inError = false;\n\n  @SuppressWarnings(\"unchecked\")\n  public void begin(InterpretationContext ec, String tagName, Attributes attributes) {\n    // Let us forget about previous errors (in this object)\n    inError = false;\n\n    // logger.debug(\"begin called\");\n\n    Object o = ec.peekObject();\n\n    if (!(o instanceof CensorAttachable)) {\n      String errMsg =\n          \"Could not find an CensorAttachable at the top of execution stack. Near [\"\n              + tagName\n              + \"] line \"\n              + getLineNumber(ec);\n      inError = true;\n      addInfo(errMsg); // This can trigger in an \"if\" block from janino, so it may not be serious...\n      return;\n    }\n\n    CensorAttachable censorAttachable = (CensorAttachable) o;\n\n    String censorName = ec.subst(attributes.getValue(CensorConstants.REF_ATTRIBUTE));\n\n    if (OptionHelper.isEmpty(censorName)) {\n      // print a meaningful error message and return\n      String errMsg = \"Missing censor ref attribute in <censor-ref> tag.\";\n      inError = true;\n      addError(errMsg);\n      return;\n    }\n\n    Map<String, CensorContextAware> censorBag =\n        (Map<String, CensorContextAware>) ec.getObjectMap().get(CensorConstants.CENSOR_BAG);\n    CensorContextAware censor = censorBag.get(censorName);\n\n    if (censor == null) {\n      String msg =\n          \"Could not find an censor named [\"\n              + censorName\n              + \"]. Did you define it below instead of above in the configuration file?\";\n      inError = true;\n      addError(msg);\n      return;\n    }\n\n    addInfo(\n        \"Attaching censor named [\"\n            + censorName\n            + \"] to \"\n            + censorAttachable\n            + \"at \"\n            + getLineNumber(ec));\n    censorAttachable.addCensor(censor);\n  }\n\n  public void end(InterpretationContext ec, String n) {}\n}\n"
  },
  {
    "path": "logback-censor/src/main/java/com/tersesystems/logback/censor/CensoringJsonGeneratorDecorator.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.censor;\n\nimport ch.qos.logback.core.spi.ContextAwareBase;\nimport ch.qos.logback.core.spi.LifeCycle;\nimport com.fasterxml.jackson.core.JsonGenerator;\nimport com.fasterxml.jackson.core.SerializableString;\nimport com.fasterxml.jackson.core.filter.FilteringGeneratorDelegate;\nimport com.fasterxml.jackson.core.filter.TokenFilter;\nimport com.fasterxml.jackson.core.util.JsonGeneratorDelegate;\nimport java.io.IOException;\nimport java.nio.charset.StandardCharsets;\nimport java.util.ArrayList;\nimport java.util.List;\nimport net.logstash.logback.decorate.JsonGeneratorDecorator;\n\n// https://github.com/FasterXML/jackson-core/issues/185\npublic class CensoringJsonGeneratorDecorator extends ContextAwareBase\n    implements CensorAttachable, JsonGeneratorDecorator, LifeCycle {\n\n  private final List<CensorContextAware> censors = new ArrayList<>();\n  private boolean started;\n\n  @Override\n  public JsonGenerator decorate(JsonGenerator generator) {\n    CensoringJsonGeneratorDelegate substitutionDelegate =\n        new CensoringJsonGeneratorDelegate(generator);\n    return new FilteringGeneratorDelegate(\n        substitutionDelegate, new CensoringTokenFilter(), true, true);\n  }\n\n  public List<CensorContextAware> getCensors() {\n    return censors;\n  }\n\n  public void addCensor(CensorContextAware censor) {\n    this.censors.add(censor);\n  }\n\n  @Override\n  public void start() {\n    started = true;\n  }\n\n  @Override\n  public void stop() {\n    filterKeys.clear();\n    censors.clear();\n    started = false;\n  }\n\n  @Override\n  public boolean isStarted() {\n    return started;\n  }\n\n  private final List<String> filterKeys = new ArrayList<>();\n\n  public void addFilterKey(String filterKey) {\n    this.filterKeys.add(filterKey);\n  }\n\n  // Removes entire value attached to the key.\n  private class CensoringTokenFilter extends TokenFilter {\n    @Override\n    public TokenFilter includeElement(int index) {\n      return this;\n    }\n\n    @Override\n    public TokenFilter includeProperty(String name) {\n      if (shouldFilter(name)) {\n        return null;\n      }\n      return TokenFilter.INCLUDE_ALL;\n    }\n\n    private boolean shouldFilter(String name) {\n      return filterKeys.contains(name);\n    }\n\n    @Override\n    protected boolean _includeScalar() {\n      return false;\n    }\n  }\n\n  // Filters text inside JSON\n  private class CensoringJsonGeneratorDelegate extends JsonGeneratorDelegate {\n    public CensoringJsonGeneratorDelegate(JsonGenerator d) {\n      super(d);\n    }\n\n    private String censorSensitiveMessage(String original) {\n      String value = original;\n      final List<CensorContextAware> censors = getCensors();\n      for (CensorContextAware censor : censors) {\n        value = String.valueOf(censor.censorText(value));\n      }\n\n      return value;\n    }\n\n    @Override\n    public void writeString(String original) throws IOException {\n      final String value = censorSensitiveMessage(original);\n      delegate.writeString(value);\n    }\n\n    @Override\n    public void writeString(char[] text, int offset, int len) throws IOException {\n      final String original = new String(text, offset, len);\n      final String value = censorSensitiveMessage(original);\n      delegate.writeString(value);\n    }\n\n    @Override\n    public void writeString(SerializableString serializableString) throws IOException {\n      final String original = serializableString.getValue();\n      final String value = censorSensitiveMessage(original);\n      delegate.writeString(value);\n    }\n\n    @Override\n    public void writeRawUTF8String(byte[] text, int offset, int length) throws IOException {\n      String original = new String(text, offset, length, StandardCharsets.UTF_8);\n      final String value = censorSensitiveMessage(original);\n      delegate.writeString(value);\n    }\n\n    @Override\n    public void writeUTF8String(byte[] text, int offset, int length) throws IOException {\n      String original = new String(text, offset, length, StandardCharsets.UTF_8);\n      final String value = censorSensitiveMessage(original);\n      delegate.writeString(value);\n    }\n  }\n}\n"
  },
  {
    "path": "logback-censor/src/main/java/com/tersesystems/logback/censor/CensoringPrettyPrintingJsonGeneratorDecorator.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.censor;\n\nimport com.fasterxml.jackson.core.JsonGenerator;\n\npublic class CensoringPrettyPrintingJsonGeneratorDecorator extends CensoringJsonGeneratorDecorator\n    implements CensorAttachable {\n  @Override\n  public JsonGenerator decorate(JsonGenerator generator) {\n    return super.decorate(generator.useDefaultPrettyPrinter());\n  }\n}\n"
  },
  {
    "path": "logback-censor/src/main/java/com/tersesystems/logback/censor/RegexCensor.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.censor;\n\nimport ch.qos.logback.core.spi.ContextAwareBase;\nimport ch.qos.logback.core.spi.LifeCycle;\nimport java.util.regex.Pattern;\n\npublic class RegexCensor extends ContextAwareBase implements CensorContextAware, LifeCycle {\n\n  protected volatile boolean started = false;\n\n  private Pattern pattern = null;\n  private String regex = null;\n\n  private String replacementText;\n\n  protected String name;\n\n  public String getName() {\n    return name;\n  }\n\n  public void setName(String name) {\n    this.name = name;\n  }\n\n  public String getReplacementText() {\n    return replacementText;\n  }\n\n  public void setReplacementText(String replacementText) {\n    this.replacementText = replacementText;\n  }\n\n  public void setRegex(String regex) {\n    this.regex = regex;\n  }\n\n  @Override\n  public boolean isStarted() {\n    return started;\n  }\n\n  @Override\n  public void start() {\n    if (replacementText == null) {\n      addError(\"replacementText cannot be null!\");\n      return;\n    }\n\n    if (regex == null) {\n      addError(\"No regular expressions found!\");\n      return;\n    }\n\n    this.pattern = Pattern.compile(regex, (regex.contains(\"\\n\")) ? Pattern.MULTILINE : 0);\n    this.started = true;\n  }\n\n  @Override\n  public void stop() {\n    this.replacementText = null;\n    this.pattern = null;\n    this.regex = null;\n    this.started = false;\n  }\n\n  @Override\n  public CharSequence censorText(CharSequence original) {\n    return pattern.matcher(original).replaceAll(replacementText);\n  }\n}\n"
  },
  {
    "path": "logback-censor/src/test/java/com/tersesystems/logback/censor/CensorActionTest.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.censor;\n\nimport static java.util.Objects.requireNonNull;\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport ch.qos.logback.classic.Level;\nimport ch.qos.logback.classic.LoggerContext;\nimport ch.qos.logback.classic.joran.JoranConfigurator;\nimport ch.qos.logback.classic.spi.LoggingEvent;\nimport ch.qos.logback.core.joran.spi.JoranException;\nimport java.nio.charset.StandardCharsets;\nimport org.junit.Before;\nimport org.junit.Test;\n\npublic class CensorActionTest {\n\n  private JoranConfigurator jc = new JoranConfigurator();\n  private LoggerContext loggerContext = new LoggerContext();\n\n  @Before\n  public void setUp() {\n    jc.setContext(loggerContext);\n  }\n\n  @Test\n  public void testFirstTest1() throws JoranException {\n    jc.doConfigure(requireNonNull(this.getClass().getClassLoader().getResource(\"test1.xml\")));\n\n    ch.qos.logback.classic.Logger root = loggerContext.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME);\n    TestAppender test = (TestAppender) root.getAppender(\"TEST1\");\n    assertThat(test).isNotNull();\n    byte[] bytes = test.getEncoder().encode(createLoggingEvent(root, \"hunter1\"));\n    assertThat(new String(bytes, StandardCharsets.UTF_8)).contains(\"[CENSORED BY CENSOR1]\");\n  }\n\n  @Test\n  public void testSecondTest1() throws JoranException {\n    jc.doConfigure(requireNonNull(this.getClass().getClassLoader().getResource(\"test2.xml\")));\n\n    ch.qos.logback.classic.Logger root = loggerContext.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME);\n    TestAppender test = (TestAppender) root.getAppender(\"TEST2\");\n    assertThat(test).isNotNull();\n    byte[] bytes = test.getEncoder().encode(createLoggingEvent(root, \"hunter2\"));\n    assertThat(new String(bytes, StandardCharsets.UTF_8)).contains(\"[CENSORED BY CENSOR2]\");\n  }\n\n  @Test\n  public void testJsonTest3() throws JoranException {\n    jc.doConfigure(requireNonNull(this.getClass().getClassLoader().getResource(\"test3.xml\")));\n\n    ch.qos.logback.classic.Logger root = loggerContext.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME);\n    TestAppender test = (TestAppender) root.getAppender(\"TEST3\");\n    assertThat(test).isNotNull();\n    byte[] bytes = test.getEncoder().encode(createLoggingEvent(root, \"hunter3 hunter4\"));\n    assertThat(new String(bytes, StandardCharsets.UTF_8))\n        .contains(\"\\\"message\\\":\\\"[CENSOR3] [CENSOR4]\\\"\");\n  }\n\n  @Test\n  public void testJsonTest4() throws JoranException {\n    jc.doConfigure(requireNonNull(this.getClass().getClassLoader().getResource(\"test4.xml\")));\n\n    ch.qos.logback.classic.Logger root = loggerContext.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME);\n    TestAppender test = (TestAppender) root.getAppender(\"TEST4\");\n    assertThat(test).isNotNull();\n    byte[] bytes = test.getEncoder().encode(createLoggingEvent(root, \"hunter4\"));\n    assertThat(new String(bytes, StandardCharsets.UTF_8)).contains(\"\\\"message\\\":\\\"[CENSOR4]\\\"\");\n  }\n\n  private LoggingEvent createLoggingEvent(ch.qos.logback.classic.Logger logger, String message) {\n    return new LoggingEvent(this.getClass().getName(), logger, Level.DEBUG, message, null, null);\n  }\n}\n"
  },
  {
    "path": "logback-censor/src/test/java/com/tersesystems/logback/censor/CensoringJsonGeneratorDecoratorTest.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.censor;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport ch.qos.logback.classic.LoggerContext;\nimport com.fasterxml.jackson.core.JsonFactory;\nimport com.fasterxml.jackson.core.JsonGenerator;\nimport com.fasterxml.jackson.databind.MappingJsonFactory;\nimport java.io.StringWriter;\nimport org.junit.Test;\n\npublic class CensoringJsonGeneratorDecoratorTest {\n\n  @Test\n  public void basicCensor() throws Exception {\n    LoggerContext context = new LoggerContext();\n    RegexCensor censor1 = new RegexCensor();\n    censor1.setContext(context);\n    censor1.setReplacementText(\"*******\");\n    censor1.setRegex(\"hunter2\");\n    censor1.start();\n\n    RegexCensor censor2 = new RegexCensor();\n    censor2.setContext(context);\n    censor2.setReplacementText(\"!!!!!!\");\n    censor2.setRegex(\"message\");\n    censor2.start();\n\n    CensoringJsonGeneratorDecorator decorator = new CensoringJsonGeneratorDecorator();\n    decorator.setContext(context);\n    decorator.addCensor(censor1);\n    decorator.addCensor(censor2);\n    decorator.start();\n\n    StringWriter writer = new StringWriter();\n    JsonFactory factory = new MappingJsonFactory();\n    JsonGenerator generator = decorator.decorate(factory.createGenerator(writer));\n\n    generator.writeStartObject();\n    generator.writeStringField(\"message\", \"My hunter2 message\");\n    generator.writeEndObject();\n    generator.flush();\n\n    assertThat(writer.toString()).isEqualTo(\"{\\\"message\\\":\\\"My ******* !!!!!!\\\"}\");\n  }\n\n  @Test\n  public void filterKey() throws Exception {\n    CensoringJsonGeneratorDecorator decorator = new CensoringJsonGeneratorDecorator();\n    decorator.addFilterKey(\"password\");\n    decorator.start();\n\n    StringWriter writer = new StringWriter();\n    JsonFactory factory = new MappingJsonFactory();\n    JsonGenerator generator = decorator.decorate(factory.createGenerator(writer));\n\n    generator.writeStartObject();\n    generator.writeStringField(\"password\", \"this entire field should be gone\");\n    generator.writeEndObject();\n    generator.flush();\n\n    assertThat(writer.toString()).isEqualTo(\"\");\n  }\n\n  @Test\n  public void prettyPrintCensor() throws Exception {\n    LoggerContext context = new LoggerContext();\n    RegexCensor censor = new RegexCensor();\n    censor.setContext(context);\n    censor.setReplacementText(\"*******\");\n    censor.setRegex(\"hunter2\");\n    censor.start();\n\n    CensoringJsonGeneratorDecorator decorator = new CensoringPrettyPrintingJsonGeneratorDecorator();\n    decorator.setContext(context);\n    decorator.addCensor(censor);\n    decorator.start();\n\n    StringWriter writer = new StringWriter();\n    JsonFactory factory = new MappingJsonFactory();\n    JsonGenerator generator = decorator.decorate(factory.createGenerator(writer));\n\n    generator.writeStartObject();\n    generator.writeStringField(\"message\", \"My hunter2 message\");\n    generator.writeEndObject();\n    generator.flush();\n\n    assertThat(writer.toString()).isEqualTo(\"{\\n  \\\"message\\\" : \\\"My ******* message\\\"\\n}\");\n  }\n}\n"
  },
  {
    "path": "logback-censor/src/test/java/com/tersesystems/logback/censor/RegexCensorTest.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.censor;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport org.junit.Test;\n\npublic class RegexCensorTest {\n\n  @Test\n  public void testCensor() throws Exception {\n    String replacementText = \"*******\";\n\n    RegexCensor censor = new RegexCensor();\n    censor.setReplacementText(replacementText);\n    censor.setRegex(\"hunter2\");\n    censor.start();\n\n    assertThat(censor.censorText(\"hunter2\")).isEqualTo(\"*******\");\n  }\n\n  @Test\n  public void testCensorWithNoMatch() throws Exception {\n    String replacementText = \"*******\";\n\n    RegexCensor censor = new RegexCensor();\n    censor.setReplacementText(replacementText);\n    censor.setRegex(\"hunter2\");\n    censor.start();\n\n    assertThat(censor.censorText(\"password1\")).isEqualTo(\"password1\");\n  }\n}\n"
  },
  {
    "path": "logback-censor/src/test/java/com/tersesystems/logback/censor/TestAppender.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.censor;\n\nimport ch.qos.logback.classic.spi.ILoggingEvent;\nimport ch.qos.logback.core.AppenderBase;\nimport ch.qos.logback.core.encoder.Encoder;\nimport java.util.ArrayList;\nimport java.util.List;\n\npublic class TestAppender extends AppenderBase<ILoggingEvent> {\n\n  protected Encoder<ILoggingEvent> encoder;\n\n  public static List<ILoggingEvent> events = new ArrayList<>();\n\n  public Encoder<ILoggingEvent> getEncoder() {\n    return encoder;\n  }\n\n  public void setEncoder(Encoder<ILoggingEvent> encoder) {\n    this.encoder = encoder;\n  }\n\n  @Override\n  protected void append(ILoggingEvent e) {\n    events.add(e);\n  }\n}\n"
  },
  {
    "path": "logback-censor/src/test/resources/test1.xml",
    "content": "<!--\n  ~ SPDX-License-Identifier: CC0-1.0\n  ~\n  ~ Copyright 2018-2020 Will Sargent.\n  ~\n  ~ Licensed under the CC0 Public Domain Dedication;\n  ~ You may obtain a copy of the License at\n  ~\n  ~  http://creativecommons.org/publicdomain/zero/1.0/\n  -->\n<configuration>\n\n    <newRule pattern=\"*/censor\"\n             actionClass=\"com.tersesystems.logback.censor.CensorAction\"/>\n\n    <newRule pattern=\"*/censor-ref\"\n             actionClass=\"com.tersesystems.logback.censor.CensorRefAction\"/>\n\n    <conversionRule conversionWord=\"censor\" converterClass=\"com.tersesystems.logback.censor.CensorConverter\" />\n\n    <censor name=\"censor-name1\" class=\"com.tersesystems.logback.censor.RegexCensor\">\n        <replacementText>[CENSORED BY CENSOR1]</replacementText>\n        <regex>hunter1</regex>\n    </censor>\n\n    <censor name=\"censor-name2\" class=\"com.tersesystems.logback.censor.RegexCensor\">\n        <replacementText>[CENSORED BY CENSOR2]</replacementText>\n        <regex>hunter2</regex>\n    </censor>\n\n    <appender name=\"TEST1\" class=\"com.tersesystems.logback.censor.TestAppender\">\n        <encoder>\n            <pattern>%censor(%msg){censor-name1}%n</pattern>\n        </encoder>\n    </appender>\n\n    <root>\n        <appender-ref ref=\"TEST1\"/>\n    </root>\n</configuration>"
  },
  {
    "path": "logback-censor/src/test/resources/test2.xml",
    "content": "<!--\n  ~ SPDX-License-Identifier: CC0-1.0\n  ~\n  ~ Copyright 2018-2020 Will Sargent.\n  ~\n  ~ Licensed under the CC0 Public Domain Dedication;\n  ~ You may obtain a copy of the License at\n  ~\n  ~  http://creativecommons.org/publicdomain/zero/1.0/\n  -->\n<configuration>\n\n    <newRule pattern=\"*/censor\"\n             actionClass=\"com.tersesystems.logback.censor.CensorAction\"/>\n\n    <newRule pattern=\"*/censor-ref\"\n             actionClass=\"com.tersesystems.logback.censor.CensorRefAction\"/>\n\n    <conversionRule conversionWord=\"censor\" converterClass=\"com.tersesystems.logback.censor.CensorConverter\" />\n\n    <censor name=\"censor-name1\" class=\"com.tersesystems.logback.censor.RegexCensor\">\n        <replacementText>[CENSORED BY CENSOR1]</replacementText>\n        <regex>hunter1</regex>\n    </censor>\n\n    <censor name=\"censor-name2\" class=\"com.tersesystems.logback.censor.RegexCensor\">\n        <replacementText>[CENSORED BY CENSOR2]</replacementText>\n        <regex>hunter2</regex>\n    </censor>\n\n    <appender name=\"TEST2\" class=\"com.tersesystems.logback.censor.TestAppender\">\n        <encoder>\n            <pattern>%censor(%msg){censor-name2}%n</pattern>\n        </encoder>\n    </appender>\n\n    <root>\n        <appender-ref ref=\"TEST2\"/>\n    </root>\n</configuration>"
  },
  {
    "path": "logback-censor/src/test/resources/test3.xml",
    "content": "<!--\n  ~ SPDX-License-Identifier: CC0-1.0\n  ~\n  ~ Copyright 2018-2020 Will Sargent.\n  ~\n  ~ Licensed under the CC0 Public Domain Dedication;\n  ~ You may obtain a copy of the License at\n  ~\n  ~  http://creativecommons.org/publicdomain/zero/1.0/\n  -->\n<configuration>\n\n    <newRule pattern=\"*/censor\"\n             actionClass=\"com.tersesystems.logback.censor.CensorAction\"/>\n\n    <newRule pattern=\"*/censor-ref\"\n             actionClass=\"com.tersesystems.logback.censor.CensorRefAction\"/>\n\n    <conversionRule conversionWord=\"censor\" converterClass=\"com.tersesystems.logback.censor.CensorConverter\" />\n\n    <censor name=\"hunter3-censor\" class=\"com.tersesystems.logback.censor.RegexCensor\">\n        <regex>hunter3</regex>\n        <replacementText>[CENSOR3]</replacementText>\n    </censor>\n\n    <censor name=\"hunter4-censor\" class=\"com.tersesystems.logback.censor.RegexCensor\">\n        <regex>hunter4</regex>\n        <replacementText>[CENSOR4]</replacementText>\n    </censor>\n\n    <appender name=\"TEST3\" class=\"com.tersesystems.logback.censor.TestAppender\">\n        <encoder class=\"net.logstash.logback.encoder.LogstashEncoder\">\n            <jsonGeneratorDecorator class=\"com.tersesystems.logback.censor.CensoringJsonGeneratorDecorator\">\n                <censor-ref ref=\"hunter3-censor\"/>\n                <censor-ref ref=\"hunter4-censor\"/>\n            </jsonGeneratorDecorator>\n        </encoder>\n    </appender>\n\n    <root>\n        <appender-ref ref=\"TEST3\"/>\n    </root>\n</configuration>"
  },
  {
    "path": "logback-censor/src/test/resources/test4.xml",
    "content": "<!--\n  ~ SPDX-License-Identifier: CC0-1.0\n  ~\n  ~ Copyright 2018-2020 Will Sargent.\n  ~\n  ~ Licensed under the CC0 Public Domain Dedication;\n  ~ You may obtain a copy of the License at\n  ~\n  ~  http://creativecommons.org/publicdomain/zero/1.0/\n  -->\n<configuration>\n\n    <newRule pattern=\"*/censor\"\n             actionClass=\"com.tersesystems.logback.censor.CensorAction\"/>\n\n    <newRule pattern=\"*/censor-ref\"\n             actionClass=\"com.tersesystems.logback.censor.CensorRefAction\"/>\n\n    <conversionRule conversionWord=\"censor\" converterClass=\"com.tersesystems.logback.censor.CensorConverter\" />\n\n    <censor name=\"json-censor\" class=\"com.tersesystems.logback.censor.RegexCensor\">\n        <regex>hunter4</regex>\n        <replacementText>[CENSOR4]</replacementText>\n    </censor>\n\n    <appender name=\"TEST4\" class=\"com.tersesystems.logback.censor.TestAppender\">\n        <encoder class=\"net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder\">\n            <providers>\n                <timestamp/>\n                <pattern>\n                    <pattern>\n                        {\n                        \"custom_constant\": \"123\",\n                        \"tags\": [\"one\", \"two\"],\n                        \"logger\": \"%logger\",\n                        \"level\": \"%level\",\n                        \"thread\": \"%thread\",\n                        \"message\": \"%censor(%message){json-censor}\"\n                        }\n                    </pattern>\n                </pattern>\n            </providers>\n        </encoder>\n    </appender>\n\n    <root>\n        <appender-ref ref=\"TEST4\"/>\n    </root>\n</configuration>"
  },
  {
    "path": "logback-classic/gradle.properties",
    "content": "#\n# SPDX-License-Identifier: CC0-1.0\n#\n# Copyright 2018-2020 Will Sargent.\n#\n# Licensed under the CC0 Public Domain Dedication;\n# You may obtain a copy of the License at\n#\n#  http://creativecommons.org/publicdomain/zero/1.0/\n#\n\nproject_description = Logback Classic"
  },
  {
    "path": "logback-classic/logback-classic.gradle",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\n\nplugins {\n    id 'java-library'\n}\n\ndependencies {\n    api project(':logback-core')\n    api \"org.slf4j:jul-to-slf4j:$slf4jVersion\"\n    api \"ch.qos.logback:logback-classic:$logbackVersion\"\n}"
  },
  {
    "path": "logback-classic/src/main/java/com/tersesystems/logback/classic/ChangeLogLevel.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.classic;\n\nimport ch.qos.logback.classic.Level;\nimport ch.qos.logback.classic.Logger;\nimport org.slf4j.ILoggerFactory;\nimport org.slf4j.LoggerFactory;\n\n/** Provide a way to change the logging level dynamically in Logback. */\npublic class ChangeLogLevel {\n\n  private final ILoggerFactory loggerFactory;\n\n  public ChangeLogLevel() {\n    this(LoggerFactory.getILoggerFactory());\n  }\n\n  public ChangeLogLevel(ILoggerFactory loggerFactory) {\n    this.loggerFactory = loggerFactory;\n  }\n\n  public void changeLogLevel(String loggerName, String levelName) {\n    changeLogLevel(loggerFactory.getLogger(loggerName), levelName);\n  }\n\n  public final void changeLogLevel(String loggerName, int levelNumber) {\n    changeLogLevel(loggerFactory.getLogger(loggerName), levelNumber);\n  }\n\n  public final void changeLogLevel(org.slf4j.Logger logger, String levelName) {\n    Logger logbackLogger = (Logger) logger;\n    Level level = Level.toLevel(levelName);\n    logbackLogger.setLevel(level);\n  }\n\n  public final void changeLogLevel(org.slf4j.Logger logger, int levelNumber) {\n    Logger logbackLogger = (Logger) logger;\n    Level level = Level.toLevel(levelNumber);\n    logbackLogger.setLevel(level);\n  }\n}\n"
  },
  {
    "path": "logback-classic/src/main/java/com/tersesystems/logback/classic/ContainerEventAppender.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\n\npackage com.tersesystems.logback.classic;\n\nimport ch.qos.logback.classic.spi.ILoggingEvent;\nimport com.tersesystems.logback.core.DecoratingAppender;\n\n/**\n * This appender decorates the out of the box logging event with a component system, which allows\n * extra attributes to be added to the event.\n */\npublic class ContainerEventAppender\n    extends DecoratingAppender<ILoggingEvent, IContainerLoggingEvent> {\n  @Override\n  protected IContainerLoggingEvent decorateEvent(ILoggingEvent eventObject) {\n    return new ContainerProxyLoggingEvent(eventObject);\n  }\n}\n"
  },
  {
    "path": "logback-classic/src/main/java/com/tersesystems/logback/classic/ContainerProxyLoggingEvent.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\n\npackage com.tersesystems.logback.classic;\n\nimport ch.qos.logback.classic.spi.ILoggingEvent;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.Objects;\n\n/** A logging event that implements a container and proxies another logging event. */\npublic class ContainerProxyLoggingEvent extends ProxyLoggingEvent\n    implements IContainerLoggingEvent {\n  private Map<Class<?>, Object> components = new HashMap<>();\n\n  public ContainerProxyLoggingEvent(ILoggingEvent delegate) {\n    super(delegate);\n  }\n\n  public <T> void putComponent(Class<T> type, T instance) {\n    components.put(Objects.requireNonNull(type), instance);\n  }\n\n  public <T> T getComponent(Class<T> type) {\n    return type.cast(components.get(type));\n  }\n\n  @Override\n  public <T> boolean hasComponent(Class<T> type) {\n    return components.containsKey(type);\n  }\n}\n"
  },
  {
    "path": "logback-classic/src/main/java/com/tersesystems/logback/classic/ContextAwareBasicMarker.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.classic;\n\nimport ch.qos.logback.core.Context;\nimport ch.qos.logback.core.spi.ContextAware;\nimport ch.qos.logback.core.status.*;\n\n/** Extend the marker interface so that we can make it context aware. */\npublic class ContextAwareBasicMarker extends TerseBasicMarker implements ContextAware {\n  private int noContextWarning = 0;\n  protected Context context;\n\n  public ContextAwareBasicMarker(String name) {\n    super(name);\n  }\n\n  public void setContext(Context context) {\n    if (this.context == null) {\n      this.context = context;\n    } else if (this.context != context) {\n      throw new IllegalStateException(\"Context has been already set\");\n    }\n  }\n\n  public Context getContext() {\n    return this.context;\n  }\n\n  public StatusManager getStatusManager() {\n    if (context == null) {\n      return null;\n    }\n    return context.getStatusManager();\n  }\n\n  /**\n   * The declared origin of status messages. By default 'this'. Derived classes may override this\n   * method to declare other origin.\n   *\n   * @return the declared origin, by default 'this'\n   */\n  protected Object getDeclaredOrigin() {\n    return this;\n  }\n\n  public void addStatus(Status status) {\n    if (context == null) {\n      if (noContextWarning++ == 0) {\n        System.out.println(\"LOGBACK: No context given for \" + this);\n      }\n      return;\n    }\n    StatusManager sm = context.getStatusManager();\n    if (sm != null) {\n      sm.add(status);\n    }\n  }\n\n  public void addInfo(String msg) {\n    addStatus(new InfoStatus(msg, getDeclaredOrigin()));\n  }\n\n  public void addInfo(String msg, Throwable ex) {\n    addStatus(new InfoStatus(msg, getDeclaredOrigin(), ex));\n  }\n\n  public void addWarn(String msg) {\n    addStatus(new WarnStatus(msg, getDeclaredOrigin()));\n  }\n\n  public void addWarn(String msg, Throwable ex) {\n    addStatus(new WarnStatus(msg, getDeclaredOrigin(), ex));\n  }\n\n  public void addError(String msg) {\n    addStatus(new ErrorStatus(msg, getDeclaredOrigin()));\n  }\n\n  public void addError(String msg, Throwable ex) {\n    addStatus(new ErrorStatus(msg, getDeclaredOrigin(), ex));\n  }\n}\n"
  },
  {
    "path": "logback-classic/src/main/java/com/tersesystems/logback/classic/ExceptionMessageConverter.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.classic;\n\nimport ch.qos.logback.classic.pattern.ThrowableHandlingConverter;\nimport ch.qos.logback.classic.spi.ILoggingEvent;\nimport ch.qos.logback.classic.spi.IThrowableProxy;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Optional;\n\n/**\n * Exception message converter that only prints out the messages of the nested exception.\n *\n * <p>The first argument is the amount of leading whitespace to add before the exception.\n *\n * <p>The second argument is the maximum depth of the nested exceptions.\n *\n * <p>The third, fourth, and fifth arguments are the prefix, separator, and suffix, respectively.\n *\n * <p>Use in a pattern encoder, i.e. \"%exmessage{1, 10, cause=[}\"\n */\npublic class ExceptionMessageConverter extends ThrowableHandlingConverter {\n\n  @Override\n  public String convert(ILoggingEvent event) {\n    Integer whitespace = getLeadingWhitespace();\n    if (whitespace < 0) {\n      addWarn(\"Cannot render whitespace less than 0!\");\n      whitespace = 0;\n    }\n\n    Integer depth = getDepth();\n    if (depth < 1) {\n      addWarn(\"Cannot render depth less than 1!\");\n      depth = 1;\n    }\n    String prefix = getPrefix();\n    String sep = getSeparator();\n    String suffix = getSuffix();\n    IThrowableProxy ex = event.getThrowableProxy();\n    if (ex == null) {\n      return \"\";\n    }\n    return processException(ex, whitespace, depth, prefix, sep, suffix);\n  }\n\n  private Integer getLeadingWhitespace() {\n    return Integer.parseInt(getOption(0).orElse(\"1\"));\n  }\n\n  protected Integer getDepth() {\n    return Integer.parseInt(getOption(1).orElse(\"10\"));\n  }\n\n  protected String getPrefix() {\n    return getOption(2).orElse(\"[\");\n  }\n\n  protected String getSeparator() {\n    return getOption(3).orElse(\" > \");\n  }\n\n  protected String getSuffix() {\n    return getOption(4).orElse(\"]\");\n  }\n\n  protected Optional<String> getOption(int index) {\n    List<String> optionList = getOptionList();\n    if (optionList != null && optionList.size() >= index + 1) {\n      return Optional.of(optionList.get(index));\n    }\n    return Optional.empty();\n  }\n\n  protected String processException(\n      IThrowableProxy throwableProxy,\n      Integer whitespace,\n      Integer depth,\n      String prefix,\n      String sep,\n      String suffix) {\n    String ws = String.join(\"\", Collections.nCopies(whitespace, \" \"));\n    StringBuilder b = new StringBuilder(ws + prefix);\n    IThrowableProxy ex = throwableProxy;\n    for (int i = 0; i < depth; i++) {\n      String message = constructMessage(ex);\n      b.append(message);\n      ex = ex.getCause();\n      if (ex == null || i + 1 == depth) break;\n      b.append(sep);\n    }\n    b.append(suffix);\n    return b.toString();\n  }\n\n  protected String constructMessage(IThrowableProxy ex) {\n    return (ex.getMessage() == null) ? ex.getClassName() : ex.getMessage();\n  }\n}\n"
  },
  {
    "path": "logback-classic/src/main/java/com/tersesystems/logback/classic/FormatParamsDecider.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.classic;\n\nimport ch.qos.logback.classic.Level;\nimport ch.qos.logback.classic.Logger;\nimport ch.qos.logback.core.spi.FilterReply;\nimport java.util.function.BiFunction;\nimport org.slf4j.Marker;\n\n@FunctionalInterface\npublic interface FormatParamsDecider\n    extends BiFunction<String, Object[], FilterReply>, TurboFilterDecider {\n  @Override\n  default FilterReply decide(\n      Marker marker, Logger logger, Level level, String format, Object[] params, Throwable t) {\n    return apply(format, params);\n  }\n}\n"
  },
  {
    "path": "logback-classic/src/main/java/com/tersesystems/logback/classic/IContainerLoggingEvent.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\n\npackage com.tersesystems.logback.classic;\n\nimport ch.qos.logback.classic.spi.ILoggingEvent;\nimport com.tersesystems.logback.core.ComponentContainer;\n\n/** A logging event that is a container of components. */\npublic interface IContainerLoggingEvent extends ILoggingEvent, ComponentContainer {}\n"
  },
  {
    "path": "logback-classic/src/main/java/com/tersesystems/logback/classic/ILoggingEventFactory.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.classic;\n\nimport ch.qos.logback.classic.Level;\nimport ch.qos.logback.classic.Logger;\nimport ch.qos.logback.classic.spi.ILoggingEvent;\nimport org.slf4j.Marker;\n\npublic interface ILoggingEventFactory<E extends ILoggingEvent> {\n  E create(Marker marker, Logger logger, Level level, String msg, Object[] params, Throwable t);\n}\n"
  },
  {
    "path": "logback-classic/src/main/java/com/tersesystems/logback/classic/LoggerDecider.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.classic;\n\nimport ch.qos.logback.classic.Logger;\nimport ch.qos.logback.core.spi.FilterReply;\nimport java.util.function.Function;\nimport org.slf4j.Marker;\n\n@FunctionalInterface\npublic interface LoggerDecider extends Function<Logger, FilterReply>, TurboFilterDecider {\n  default FilterReply decide(\n      Marker marker,\n      Logger logger,\n      ch.qos.logback.classic.Level level,\n      String format,\n      Object[] params,\n      Throwable t) {\n    return apply(logger);\n  }\n}\n"
  },
  {
    "path": "logback-classic/src/main/java/com/tersesystems/logback/classic/LoggingEventFactory.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.classic;\n\nimport static ch.qos.logback.classic.Logger.FQCN;\n\nimport ch.qos.logback.classic.Level;\nimport ch.qos.logback.classic.Logger;\nimport ch.qos.logback.classic.spi.ILoggingEvent;\nimport ch.qos.logback.classic.spi.LoggingEvent;\nimport org.slf4j.Marker;\n\npublic class LoggingEventFactory implements ILoggingEventFactory<ILoggingEvent> {\n  public ILoggingEvent create(\n      Marker marker, Logger logger, Level level, String msg, Object[] params, Throwable t) {\n    LoggingEvent le = new LoggingEvent(FQCN, logger, level, msg, t, params);\n    le.setMarker(marker);\n    return le;\n  }\n}\n"
  },
  {
    "path": "logback-classic/src/main/java/com/tersesystems/logback/classic/MarkerLoggerDecider.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.classic;\n\nimport ch.qos.logback.classic.Logger;\nimport ch.qos.logback.core.spi.FilterReply;\nimport java.util.function.BiFunction;\nimport org.slf4j.Marker;\n\n@FunctionalInterface\npublic interface MarkerLoggerDecider\n    extends BiFunction<Marker, Logger, FilterReply>, TurboFilterDecider {\n  default FilterReply decide(\n      Marker marker,\n      Logger logger,\n      ch.qos.logback.classic.Level level,\n      String format,\n      Object[] params,\n      Throwable t) {\n    return apply(marker, logger);\n  }\n}\n"
  },
  {
    "path": "logback-classic/src/main/java/com/tersesystems/logback/classic/NanoTime.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\n\npackage com.tersesystems.logback.classic;\n\nimport ch.qos.logback.classic.spi.ILoggingEvent;\nimport ch.qos.logback.core.Context;\nimport ch.qos.logback.core.spi.ContextAware;\nimport com.tersesystems.logback.core.ComponentContainer;\nimport java.util.Iterator;\nimport java.util.Optional;\nimport org.slf4j.Marker;\n\npublic final class NanoTime {\n  public static final long start = System.nanoTime();\n\n  public static Optional<Long> fromOptional(Context context, ILoggingEvent event) {\n    if (event instanceof ComponentContainer) {\n      ComponentContainer container = (ComponentContainer) event;\n      if (container.hasComponent(NanoTimeSupplier.class)) {\n        return fromContainer(container);\n      }\n    }\n\n    return fromMarker(context, event.getMarker());\n  }\n\n  static Optional<Long> fromMarker(Context context, Marker m) {\n    if (m instanceof NanoTimeSupplier) {\n      NanoTimeSupplier supplier = ((NanoTimeSupplier) m);\n      return Optional.of(supplier.getNanoTime());\n    }\n    if (m != null && m.hasReferences()) {\n      for (Iterator<Marker> iter = m.iterator(); iter.hasNext(); ) {\n        Marker child = iter.next();\n        if (child instanceof ContextAware) {\n          ((ContextAware) child).setContext(context);\n        }\n        if (child instanceof NanoTimeSupplier) {\n          NanoTimeSupplier supplier = ((NanoTimeSupplier) child);\n          return Optional.of(supplier.getNanoTime());\n        }\n        if (child.hasReferences()) {\n          return fromMarker(context, child);\n        }\n      }\n    }\n    return Optional.empty();\n  }\n\n  public static Optional<Long> fromContainer(ComponentContainer container) {\n    long nanoTime = container.getComponent(NanoTimeSupplier.class).getNanoTime();\n    return Optional.of(nanoTime);\n  }\n}\n"
  },
  {
    "path": "logback-classic/src/main/java/com/tersesystems/logback/classic/NanoTimeComponentAppender.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\n\npackage com.tersesystems.logback.classic;\n\nimport ch.qos.logback.classic.spi.ILoggingEvent;\nimport com.tersesystems.logback.core.DecoratingAppender;\n\n/** This appender adds a relative nanotime component to the logging event. */\npublic class NanoTimeComponentAppender\n    extends DecoratingAppender<ILoggingEvent, IContainerLoggingEvent> {\n  @Override\n  protected IContainerLoggingEvent decorateEvent(ILoggingEvent eventObject) {\n    IContainerLoggingEvent containerEvent;\n    if (eventObject instanceof IContainerLoggingEvent) {\n      containerEvent = (IContainerLoggingEvent) eventObject;\n    } else {\n      containerEvent = new ContainerProxyLoggingEvent(eventObject);\n    }\n    long nanoTime = System.nanoTime() - NanoTime.start;\n    containerEvent.putComponent(NanoTimeSupplier.class, () -> nanoTime);\n    return containerEvent;\n  }\n}\n"
  },
  {
    "path": "logback-classic/src/main/java/com/tersesystems/logback/classic/NanoTimeConverter.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\n\npackage com.tersesystems.logback.classic;\n\nimport ch.qos.logback.classic.pattern.ClassicConverter;\nimport ch.qos.logback.classic.spi.ILoggingEvent;\n\n/** A relative time converter that returns number of nanoseconds from NanoTime.start. */\npublic class NanoTimeConverter extends ClassicConverter {\n  @Override\n  public String convert(ILoggingEvent event) {\n    return NanoTime.fromOptional(getContext(), event).map(st -> Long.toString(st)).orElse(null);\n  }\n}\n"
  },
  {
    "path": "logback-classic/src/main/java/com/tersesystems/logback/classic/NanoTimeMarker.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\n\npackage com.tersesystems.logback.classic;\n\npublic class NanoTimeMarker extends TerseBasicMarker implements NanoTimeSupplier {\n  private static final String NANOTIME_MARKER_NAME = \"TS_NANOTIME_MARKER\";\n  private final long nanoTime;\n\n  public NanoTimeMarker() {\n    super(NANOTIME_MARKER_NAME);\n    this.nanoTime = System.nanoTime() - NanoTime.start;\n  }\n\n  public long getNanoTime() {\n    return nanoTime;\n  }\n\n  public static NanoTimeMarker create() {\n    return new NanoTimeMarker();\n  }\n\n  @Override\n  public String toString() {\n    return \"NanoTimeMarker{\" + \"nanoTime=\" + nanoTime + '}';\n  }\n}\n"
  },
  {
    "path": "logback-classic/src/main/java/com/tersesystems/logback/classic/NanoTimeSupplier.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\n\npackage com.tersesystems.logback.classic;\n\nimport com.tersesystems.logback.core.Component;\n\npublic interface NanoTimeSupplier extends Component {\n  long getNanoTime();\n}\n"
  },
  {
    "path": "logback-classic/src/main/java/com/tersesystems/logback/classic/ProxyLoggingEvent.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.classic;\n\nimport ch.qos.logback.classic.Level;\nimport ch.qos.logback.classic.spi.ILoggingEvent;\nimport ch.qos.logback.classic.spi.IThrowableProxy;\nimport ch.qos.logback.classic.spi.LoggerContextVO;\nimport java.util.Map;\nimport org.slf4j.Marker;\n\n/**\n * Create a LoggingEvent that takes a proxy.\n *\n * <p>This looks a bit like LoggingEventVO, but does not implement serializable behavior or\n * additional logic.\n */\npublic class ProxyLoggingEvent implements ILoggingEvent {\n  private final ILoggingEvent delegate;\n\n  public ProxyLoggingEvent(ILoggingEvent delegate) {\n    this.delegate = delegate;\n  }\n\n  public ILoggingEvent getDelegate() {\n    return delegate;\n  }\n\n  @Override\n  public String getThreadName() {\n    return delegate.getThreadName();\n  }\n\n  @Override\n  public Level getLevel() {\n    return delegate.getLevel();\n  }\n\n  @Override\n  public String getMessage() {\n    return delegate.getMessage();\n  }\n\n  @Override\n  public Object[] getArgumentArray() {\n    return delegate.getArgumentArray();\n  }\n\n  @Override\n  public String getFormattedMessage() {\n    return delegate.getFormattedMessage();\n  }\n\n  @Override\n  public String getLoggerName() {\n    return delegate.getLoggerName();\n  }\n\n  @Override\n  public LoggerContextVO getLoggerContextVO() {\n    return delegate.getLoggerContextVO();\n  }\n\n  @Override\n  public IThrowableProxy getThrowableProxy() {\n    return delegate.getThrowableProxy();\n  }\n\n  @Override\n  public StackTraceElement[] getCallerData() {\n    return delegate.getCallerData();\n  }\n\n  @Override\n  public boolean hasCallerData() {\n    return delegate.hasCallerData();\n  }\n\n  @Override\n  public Marker getMarker() {\n    return delegate.getMarker();\n  }\n\n  @Override\n  public Map<String, String> getMDCPropertyMap() {\n    return delegate.getMDCPropertyMap();\n  }\n\n  @Override\n  public Map<String, String> getMdc() {\n    return delegate.getMdc();\n  }\n\n  @Override\n  public long getTimeStamp() {\n    return delegate.getTimeStamp();\n  }\n\n  @Override\n  public void prepareForDeferredProcessing() {\n    delegate.prepareForDeferredProcessing();\n  }\n}\n"
  },
  {
    "path": "logback-classic/src/main/java/com/tersesystems/logback/classic/SLF4JBridgeHandlerAction.java",
    "content": "package com.tersesystems.logback.classic;\n\nimport ch.qos.logback.core.joran.action.Action;\nimport ch.qos.logback.core.joran.spi.ActionException;\nimport ch.qos.logback.core.joran.spi.InterpretationContext;\nimport org.slf4j.bridge.SLF4JBridgeHandler;\nimport org.xml.sax.Attributes;\n\n/**\n * Provides SLF4JBridgeHandler installation as an action. This is useful because it means you don't\n * have to add custom code to your main method, and can completely initialize JUL by adding this.\n *\n * <p>Easiest way to do this is to use a custom rule:\n *\n * <p>\"&lt;newRule pattern=\"configuration/slf4jBridgeHandler\"\n * actionClass=\"com.tersesystems.logback.classic.SLF4JBridgeHandlerAction\"/&gt;\"\n *\n * <p>and then call it:\n *\n * <p>\"&lt;slf4jBridgeHandler/&gt;\"\n *\n * <p>You should use this in conjunction with the \"LevelChangePropagator\":\n *\n * <p>\"&lt;contextListener class=\"ch.qos.logback.classic.jul.LevelChangePropagator\"/&gt;\"\n */\npublic class SLF4JBridgeHandlerAction extends Action {\n\n  @Override\n  public void begin(InterpretationContext ic, String name, Attributes attributes)\n      throws ActionException {\n    SLF4JBridgeHandler.removeHandlersForRootLogger();\n    SLF4JBridgeHandler.install();\n  }\n\n  @Override\n  public void end(InterpretationContext ic, String name) throws ActionException {}\n}\n"
  },
  {
    "path": "logback-classic/src/main/java/com/tersesystems/logback/classic/SetLoggerLevelsAction.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.classic;\n\nimport ch.qos.logback.classic.Logger;\nimport ch.qos.logback.classic.LoggerContext;\nimport ch.qos.logback.core.joran.action.Action;\nimport ch.qos.logback.core.joran.spi.ActionException;\nimport ch.qos.logback.core.joran.spi.InterpretationContext;\nimport java.util.Map;\nimport org.xml.sax.Attributes;\n\n/** Sets the logger levels using a map with the levels key. */\npublic class SetLoggerLevelsAction extends Action {\n\n  public static final String LEVELS_KEY = \"levels\";\n  public String levelsKey = LEVELS_KEY;\n\n  public String getLevelsKey() {\n    return levelsKey;\n  }\n\n  public void setLevelsKey(String levelsKey) {\n    this.levelsKey = levelsKey;\n  }\n\n  @Override\n  public void begin(InterpretationContext ic, String name, Attributes attributes)\n      throws ActionException {\n    doConfigure(ic);\n  }\n\n  @Override\n  public void end(InterpretationContext ic, String name) throws ActionException {}\n\n  @SuppressWarnings(\"unchecked\")\n  protected void doConfigure(InterpretationContext ic) {\n    LoggerContext ctx = (LoggerContext) ic.getContext();\n    Map<String, String> levelsMap = (Map<String, String>) ctx.getObject(levelsKey);\n    if (levelsMap == null) {\n      addWarn(\"No levels found in context, cannot set levels.\");\n      return;\n    }\n\n    for (Map.Entry<String, String> entry : levelsMap.entrySet()) {\n      String name = entry.getKey();\n      try {\n        Logger logger = ctx.getLogger(name);\n        String level = entry.getValue();\n        new ChangeLogLevel().changeLogLevel(logger, level);\n        addInfo(\"Setting level of \" + name + \" logger to \" + level);\n      } catch (Exception e) {\n        addError(\"Unexpected exception resolving \" + name, e);\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "logback-classic/src/main/java/com/tersesystems/logback/classic/StartTime.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.classic;\n\nimport ch.qos.logback.classic.spi.ILoggingEvent;\nimport ch.qos.logback.core.Context;\nimport ch.qos.logback.core.spi.ContextAware;\nimport com.tersesystems.logback.core.ComponentContainer;\nimport java.time.Instant;\nimport java.util.Iterator;\nimport java.util.Optional;\nimport org.slf4j.Marker;\n\n/** This class pulls an Instant from a StartTimeSupplier. */\npublic final class StartTime {\n\n  /**\n   * Returns a start time from the event, then marker, then finally if not found will use the\n   * event's timestamp as the marker.\n   *\n   * @param context the logging context\n   * @param event the logging event\n   * @return an instant representing the start time.\n   */\n  public static Instant from(Context context, ILoggingEvent event) {\n    return fromOptional(context, event).orElse(Instant.ofEpochMilli(event.getTimeStamp()));\n  }\n\n  /**\n   * Pulls a start time from the logging event, looking for the supplier on the event first, and\n   * then looking for a StartTimeMarker.\n   *\n   * @param context the logback context\n   * @param event the event\n   * @return an optional start time, using both a container and marker as possible sources.\n   */\n  public static Optional<Instant> fromOptional(Context context, ILoggingEvent event) {\n    if (event instanceof ComponentContainer) {\n      ComponentContainer container = (ComponentContainer) event;\n      if (container.hasComponent(StartTimeSupplier.class)) {\n        return fromContainer(container);\n      }\n    }\n    return fromMarker(context, event.getMarker());\n  }\n\n  /**\n   * Looks for a StartTimeMarker in the marker and in all the children of the marker.\n   *\n   * @param context the logback context\n   * @param m the logback marker\n   * @return an optional start time.\n   */\n  public static Optional<Instant> fromMarker(Context context, Marker m) {\n    if (m instanceof StartTimeSupplier) {\n      StartTimeSupplier supplier = ((StartTimeSupplier) m);\n      return Optional.of(supplier.getStartTime());\n    }\n    if (m != null && m.hasReferences()) {\n      for (Iterator<Marker> iter = m.iterator(); iter.hasNext(); ) {\n        Marker child = iter.next();\n        if (child instanceof ContextAware) {\n          ((ContextAware) child).setContext(context);\n        }\n        if (child instanceof StartTimeSupplier) {\n          StartTimeSupplier supplier = ((StartTimeSupplier) child);\n          return Optional.of(supplier.getStartTime());\n        }\n        if (child.hasReferences()) {\n          return fromMarker(context, child);\n        }\n      }\n    }\n    return Optional.empty();\n  }\n\n  public static Optional<Instant> fromContainer(ComponentContainer container) {\n    StartTimeSupplier supplier = container.getComponent(StartTimeSupplier.class);\n    return Optional.ofNullable(supplier.getStartTime());\n  }\n}\n"
  },
  {
    "path": "logback-classic/src/main/java/com/tersesystems/logback/classic/StartTimeConverter.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.classic;\n\nimport ch.qos.logback.classic.pattern.ClassicConverter;\nimport ch.qos.logback.classic.spi.ILoggingEvent;\nimport java.util.Optional;\n\n/** Returns start time in milliseconds. */\npublic class StartTimeConverter extends ClassicConverter {\n  @Override\n  public String convert(ILoggingEvent event) {\n    Optional<String> optStartTime =\n        StartTime.fromOptional(getContext(), event).map(st -> Long.toString(st.toEpochMilli()));\n    return optStartTime.orElse(null);\n  }\n}\n"
  },
  {
    "path": "logback-classic/src/main/java/com/tersesystems/logback/classic/StartTimeMarker.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.classic;\n\nimport java.time.Instant;\nimport java.util.Objects;\n\npublic class StartTimeMarker extends TerseBasicMarker implements StartTimeSupplier {\n  private static final String TS_STARTTIME_MARKER = \"TS_STARTTIME_MARKER\";\n  private final Instant startTime;\n\n  public StartTimeMarker(Instant start) {\n    super(TS_STARTTIME_MARKER);\n    this.startTime = Objects.requireNonNull(start);\n  }\n\n  @Override\n  public Instant getStartTime() {\n    return startTime;\n  }\n\n  @Override\n  public String toString() {\n    return \"StartTimeMarker{\" + \"startTime=\" + startTime + '}';\n  }\n}\n"
  },
  {
    "path": "logback-classic/src/main/java/com/tersesystems/logback/classic/StartTimeSupplier.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.classic;\n\nimport java.time.Instant;\n\npublic interface StartTimeSupplier {\n  Instant getStartTime();\n}\n"
  },
  {
    "path": "logback-classic/src/main/java/com/tersesystems/logback/classic/TapFilter.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\n\npackage com.tersesystems.logback.classic;\n\nimport ch.qos.logback.classic.Level;\nimport ch.qos.logback.classic.Logger;\nimport ch.qos.logback.classic.spi.ILoggingEvent;\nimport ch.qos.logback.classic.spi.TurboFilterList;\nimport ch.qos.logback.classic.turbo.TurboFilter;\nimport ch.qos.logback.core.spi.AppenderAttachableImpl;\nimport ch.qos.logback.core.spi.FilterReply;\nimport com.tersesystems.logback.core.DefaultAppenderAttachable;\nimport org.slf4j.Marker;\n\n/**\n * A tap filter is used to tap some amount of incoming process and pass them to a specially\n * configured appender even if they do not qualify as a logging event under normal circumstances.\n * This is a <a\n * href=\"https://www.enterpriseintegrationpatterns.com/patterns/messaging/WireTap.html\">wiretap</a>\n * pattern from Enterprise Integration Patterns.\n *\n * <p>It completely bypasses any kind of logging level configured on the front end, so you can set a\n * logger to INFO level but still have access to all TRACE events when an error occurs, through the\n * tap filter's appenders.\n *\n * <p>NOTE: This means that isLoggingTrace etc always returns true.\n */\npublic class TapFilter extends TurboFilter\n    implements DefaultAppenderAttachable<ILoggingEvent>, TurboFilterDecider {\n\n  private final AppenderAttachableImpl<ILoggingEvent> aae = new AppenderAttachableImpl<>();\n\n  private ILoggingEventFactory<ILoggingEvent> loggingEventFactory;\n\n  private TurboFilterList evaluatorList = new TurboFilterList();\n\n  public void addTurboFilter(TurboFilter turboFilter) {\n    evaluatorList.add(turboFilter);\n  }\n\n  public TurboFilterList getTurboFilters() {\n    return evaluatorList;\n  }\n\n  public void getTurboFilters(TurboFilterList tapEvaluator) {\n    this.evaluatorList = tapEvaluator;\n  }\n\n  @Override\n  public AppenderAttachableImpl<ILoggingEvent> appenderAttachableImpl() {\n    return aae;\n  }\n\n  public ILoggingEventFactory<ILoggingEvent> getLoggingEventFactory() {\n    return loggingEventFactory;\n  }\n\n  public void setLoggingEventFactory(ILoggingEventFactory<ILoggingEvent> loggingEventFactory) {\n    this.loggingEventFactory = loggingEventFactory;\n  }\n\n  @Override\n  public void start() {\n    if (this.loggingEventFactory == null) {\n      this.loggingEventFactory = new LoggingEventFactory();\n    }\n\n    if (evaluatorList.isEmpty()) {\n      TurboFilter acceptAllTurboFilter =\n          new TurboFilter() {\n            @Override\n            public FilterReply decide(\n                Marker marker,\n                Logger logger,\n                Level level,\n                String format,\n                Object[] params,\n                Throwable t) {\n              return FilterReply.ACCEPT;\n            }\n          };\n      evaluatorList.add(acceptAllTurboFilter);\n    }\n\n    super.start();\n  }\n\n  @Override\n  public FilterReply decide(\n      Marker marker, Logger logger, Level level, String format, Object[] params, Throwable t) {\n    // Called by isLoggingTrace() -- there's no actual message here.\n    if (logger != null && format == null && params == null) {\n      // Need to turn on events for everything so that we can cover conditional events.\n      return FilterReply.ACCEPT;\n    }\n\n    // Only tap if the internal filters pass.\n    FilterReply turboFilterChainDecision =\n        evaluatorList.getTurboFilterChainDecision(marker, logger, level, format, params, t);\n    if (turboFilterChainDecision.equals(FilterReply.ACCEPT)) {\n      ILoggingEvent loggingEvent =\n          loggingEventFactory.create(marker, logger, level, format, params, t);\n      // initialize the mdc in the logging event...\n      loggingEvent.prepareForDeferredProcessing();\n      // For every message that is acceptable, store it in the appender and return.\n      aae.appendLoopOnAppenders(loggingEvent);\n    }\n    return FilterReply.NEUTRAL;\n  }\n}\n"
  },
  {
    "path": "logback-classic/src/main/java/com/tersesystems/logback/classic/TerseBasicMarker.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.classic;\n\nimport static java.util.Objects.requireNonNull;\n\nimport java.util.*;\nimport org.slf4j.Marker;\n\n/**\n * A marker that can be extended with custom behavior.\n *\n * <p>Following on from logstash-logback-marker\n */\npublic class TerseBasicMarker implements Marker {\n\n  private final String name;\n  private List<Marker> referenceList;\n\n  public TerseBasicMarker(String name) {\n    requireNonNull(name, \"A marker name cannot be null\");\n    this.name = name;\n  }\n\n  public String getName() {\n    return name;\n  }\n\n  public synchronized void add(Marker reference) {\n    requireNonNull(reference, \"A null value cannot be added to a Marker as reference.\");\n\n    if (!(this.contains(reference) || reference.contains(this))) {\n      if (referenceList == null) {\n        referenceList = new Vector<>();\n      }\n      referenceList.add(reference);\n    }\n  }\n\n  public synchronized boolean hasReferences() {\n    return referenceList != null && referenceList.size() > 0;\n  }\n\n  /**\n   * @deprecated Replaced by {@link #hasReferences()}.\n   */\n  @Deprecated\n  public boolean hasChildren() {\n    return hasReferences();\n  }\n\n  public synchronized Iterator<Marker> iterator() {\n    return hasReferences() ? referenceList.iterator() : Collections.emptyIterator();\n  }\n\n  public synchronized boolean remove(Marker referenceToRemove) {\n    if (hasReferences()) {\n      return referenceList.remove(referenceToRemove);\n    } else {\n      return false;\n    }\n  }\n\n  public boolean contains(Marker other) {\n    requireNonNull(other, \"other cannot be null\");\n\n    if (this.equals(other)) {\n      return true;\n    } else if (hasReferences()) {\n      return referenceList.stream().anyMatch(ref -> ref.contains(other));\n    } else {\n      return false;\n    }\n  }\n\n  public boolean contains(String name) {\n    requireNonNull(name, \"name cannot be null\");\n\n    if (this.name.equals(name)) {\n      return true;\n    } else if (hasReferences()) {\n      return referenceList.stream().anyMatch(ref -> ref.contains(name));\n    } else {\n      return false;\n    }\n  }\n\n  private static final String OPEN = \"[ \";\n  private static final String CLOSE = \" ]\";\n  private static final String SEP = \", \";\n\n  public boolean equals(Object obj) {\n    if (this == obj) return true;\n    if (obj == null) return false;\n    if (!(obj instanceof Marker)) return false;\n\n    final Marker other = (Marker) obj;\n    return name.equals(other.getName());\n  }\n\n  public int hashCode() {\n    return name.hashCode();\n  }\n\n  public String toString() {\n    if (!this.hasReferences()) {\n      return this.getName();\n    }\n    Iterator<Marker> it = this.iterator();\n    Marker reference;\n    StringBuilder sb = new StringBuilder(this.getName());\n    sb.append(' ').append(OPEN);\n    while (it.hasNext()) {\n      reference = it.next();\n      sb.append(reference.getName());\n      if (it.hasNext()) {\n        sb.append(SEP);\n      }\n    }\n    sb.append(CLOSE);\n\n    return sb.toString();\n  }\n}\n"
  },
  {
    "path": "logback-classic/src/main/java/com/tersesystems/logback/classic/TerseHighlightConverter.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.classic;\n\nimport ch.qos.logback.classic.Level;\nimport ch.qos.logback.classic.spi.ILoggingEvent;\nimport ch.qos.logback.core.pattern.color.ANSIConstants;\nimport ch.qos.logback.core.pattern.color.ForegroundCompositeConverterBase;\nimport java.util.Map;\nimport java.util.Optional;\n\n/**\n * Prints out a colored level using ANSI codes. Jansi is included here for Windows.\n *\n * <p>This is like %highlight but uses configured colors instead.\n *\n * <pre>{@code\n * <conversionRule conversionWord=\"terseHighlight\"\n *   converterClass=\"com.tersesystems.logback.TerseHighlightConverter\" />\n * }</pre>\n */\npublic class TerseHighlightConverter extends ForegroundCompositeConverterBase<ILoggingEvent> {\n\n  public static final String HIGHLIGHT_CTX_KEY = \"highlight\";\n\n  enum Color {\n    BLACK(ANSIConstants.BLACK_FG),\n    RED(ANSIConstants.RED_FG),\n    GREEN(ANSIConstants.GREEN_FG),\n    YELLOW(ANSIConstants.YELLOW_FG),\n    BLUE(ANSIConstants.BLUE_FG),\n    MAGENTA(ANSIConstants.MAGENTA_FG),\n    CYAN(ANSIConstants.CYAN_FG),\n    WHITE(ANSIConstants.WHITE_FG);\n\n    final String code;\n\n    Color(String code) {\n      this.code = code;\n    }\n  }\n\n  @Override\n  protected String getForegroundColorCode(ILoggingEvent event) {\n    String configKey = Optional.ofNullable(getFirstOption()).orElse(HIGHLIGHT_CTX_KEY);\n\n    Map<String, String> config = (Map<String, String>) getContext().getObject(configKey);\n    if (config == null) {\n      addWarn(\"No map found in context with key \" + configKey);\n      return Color.BLACK.code;\n    }\n\n    Level level = event.getLevel();\n    String levelColor = config.get(level.levelStr.toLowerCase()).toUpperCase();\n    return Color.valueOf(levelColor).code;\n  }\n}\n"
  },
  {
    "path": "logback-classic/src/main/java/com/tersesystems/logback/classic/TimeSinceEpochConverter.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.classic;\n\nimport ch.qos.logback.classic.pattern.ClassicConverter;\nimport ch.qos.logback.classic.spi.ILoggingEvent;\n\npublic class TimeSinceEpochConverter extends ClassicConverter {\n  @Override\n  public String convert(ILoggingEvent event) {\n    return Long.toString(event.getTimeStamp());\n  }\n}\n"
  },
  {
    "path": "logback-classic/src/main/java/com/tersesystems/logback/classic/TurboFilterDecider.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.classic;\n\nimport ch.qos.logback.classic.Level;\nimport ch.qos.logback.classic.Logger;\nimport ch.qos.logback.core.spi.FilterReply;\nimport org.slf4j.Marker;\n\n/**\n * An interface that decides what sort of filter reply there is.\n *\n * <p>Logback doesn't provide an interface for this out of the box for all turbofilters, so we have\n * to add one in by hand when we want decisions without the whole turbo filter.\n *\n * <p>This comes in handy for turbomarkers and tap filters.\n */\npublic interface TurboFilterDecider {\n  FilterReply decide(\n      Marker marker, Logger logger, Level level, String format, Object[] params, Throwable t);\n}\n"
  },
  {
    "path": "logback-classic/src/main/java/com/tersesystems/logback/classic/Utils.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.classic;\n\nimport static java.util.Objects.requireNonNull;\n\nimport ch.qos.logback.classic.Logger;\nimport ch.qos.logback.classic.LoggerContext;\nimport ch.qos.logback.classic.joran.JoranConfigurator;\nimport ch.qos.logback.classic.spi.ILoggingEvent;\nimport ch.qos.logback.classic.turbo.TurboFilter;\nimport ch.qos.logback.classic.util.ContextSelectorStaticBinder;\nimport ch.qos.logback.classic.util.LogbackMDCAdapter;\nimport ch.qos.logback.core.Appender;\nimport ch.qos.logback.core.joran.spi.JoranException;\nimport ch.qos.logback.core.status.Status;\nimport ch.qos.logback.core.status.StatusManager;\nimport com.tersesystems.logback.classic.functional.GetAppenderFunction;\nimport com.tersesystems.logback.classic.functional.RootLoggerSupplier;\nimport java.net.URL;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Optional;\nimport org.slf4j.ILoggerFactory;\nimport org.slf4j.LoggerFactory;\nimport org.slf4j.MDC;\nimport org.slf4j.spi.MDCAdapter;\n\npublic class Utils {\n  private final LoggerContext loggerContext;\n\n  Utils(LoggerContext loggerContext) {\n    this.loggerContext = requireNonNull(loggerContext);\n  }\n\n  public static LoggerContext defaultContext() {\n    ContextSelectorStaticBinder singleton = ContextSelectorStaticBinder.getSingleton();\n    if (singleton != null && singleton.getContextSelector() != null) {\n      return singleton.getContextSelector().getLoggerContext();\n    } else {\n      ILoggerFactory loggerFactory = LoggerFactory.getILoggerFactory();\n      return (LoggerContext) loggerFactory;\n    }\n  }\n\n  public static LoggerContext contextFromResource(String resourcePath) throws JoranException {\n    LoggerContext context = new LoggerContext();\n    URL resource = requireNonNull(Utils.class.getResource(requireNonNull(resourcePath)));\n    JoranConfigurator configurator = new JoranConfigurator();\n    configurator.setContext(context);\n    configurator.doConfigure(resource);\n    return context;\n  }\n\n  public static Utils create(LoggerContext loggerContext) {\n    return new Utils(loggerContext);\n  }\n\n  public static Utils create(String resourcePath) throws JoranException {\n    return new Utils(contextFromResource(resourcePath));\n  }\n\n  public static Utils create() {\n    return new Utils(defaultContext());\n  }\n\n  public List<Status> getStatusList() {\n    StatusManager statusManager = getLoggerContext().getStatusManager();\n    return statusManager.getCopyOfStatusList();\n  }\n\n  public LoggerContext getLoggerContext() {\n    return loggerContext;\n  }\n\n  public Logger getRootLogger() {\n    return RootLoggerSupplier.create(loggerContext).get();\n  }\n\n  public Logger getLogger(String loggerName) {\n    return (loggerContext.getLogger(loggerName));\n  }\n\n  public Logger getLogger(Class<?> clazz) {\n    return (loggerContext.getLogger(clazz));\n  }\n\n  public <E> Optional<E> getObject(Class<E> classType, String name) {\n    return Optional.ofNullable(loggerContext.getObject(name))\n        .filter(tf -> classType.isAssignableFrom(tf.getClass()))\n        .map(classType::cast);\n  }\n\n  public <E extends TurboFilter> Optional<E> getTurboFilter(\n      Class<E> classType, String turboFilterName) {\n    return loggerContext.getTurboFilterList().stream()\n        .filter(tf -> tf.getName().equals(turboFilterName))\n        .filter(tf -> classType.isAssignableFrom(tf.getClass()))\n        .map(classType::cast)\n        .findFirst();\n  }\n\n  public <E extends Appender<ILoggingEvent>> Optional<E> getAppender(String appenderName) {\n    return GetAppenderFunction.<E>create(loggerContext).apply(appenderName);\n  }\n\n  public Map<String, String> getMDCPropertyMap() {\n    MDCAdapter mdc = MDC.getMDCAdapter();\n\n    Map<String, String> mdcPropertyMap;\n    if (mdc instanceof LogbackMDCAdapter)\n      mdcPropertyMap = ((LogbackMDCAdapter) mdc).getPropertyMap();\n    else mdcPropertyMap = mdc.getCopyOfContextMap();\n\n    // mdcPropertyMap still null, use emptyMap()\n    if (mdcPropertyMap == null) mdcPropertyMap = Collections.emptyMap();\n\n    return mdcPropertyMap;\n  }\n\n  public LoggingEventFactory getLoggingEventFactory() {\n    return new LoggingEventFactory();\n  }\n}\n"
  },
  {
    "path": "logback-classic/src/main/java/com/tersesystems/logback/classic/encoder/PatternLayoutEncoder.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\n\npackage com.tersesystems.logback.classic.encoder;\n\nimport ch.qos.logback.classic.PatternLayout;\nimport ch.qos.logback.classic.spi.ILoggingEvent;\nimport com.tersesystems.logback.core.pattern.PatternLayoutEncoderBase;\n\n/**\n * Create a pattern layout encoder that doesn't require that the parent is an appender.\n *\n * <p>This allows for encoders that can take encoders and so on.\n */\npublic class PatternLayoutEncoder extends PatternLayoutEncoderBase<ILoggingEvent> {\n\n  @Override\n  public void start() {\n    PatternLayout patternLayout = new PatternLayout();\n    patternLayout.setContext(context);\n    patternLayout.setPattern(getPattern());\n    patternLayout.setOutputPatternAsHeader(outputPatternAsHeader);\n    patternLayout.start();\n    this.layout = patternLayout;\n    super.start();\n  }\n}\n"
  },
  {
    "path": "logback-classic/src/main/java/com/tersesystems/logback/classic/functional/GetAppenderFunction.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.classic.functional;\n\nimport static java.util.Objects.requireNonNull;\n\nimport ch.qos.logback.classic.Logger;\nimport ch.qos.logback.classic.LoggerContext;\nimport ch.qos.logback.classic.spi.ILoggingEvent;\nimport ch.qos.logback.core.Appender;\nimport com.tersesystems.logback.classic.Utils;\nimport java.util.Optional;\nimport java.util.function.Function;\n\npublic class GetAppenderFunction<A extends Appender<ILoggingEvent>>\n    implements Function<String, Optional<A>> {\n\n  private final Logger rootLogger;\n\n  public GetAppenderFunction(Logger rootLogger) {\n    this.rootLogger = rootLogger;\n  }\n\n  @SuppressWarnings(\"unchecked\")\n  @Override\n  public Optional<A> apply(String appenderName) {\n    Appender<ILoggingEvent> appender = rootLogger.getAppender(requireNonNull(appenderName));\n    try {\n      return Optional.ofNullable((A) appender);\n    } catch (ClassCastException e) {\n      return Optional.empty();\n    }\n  }\n\n  public static <AT extends Appender<ILoggingEvent>> GetAppenderFunction<AT> create() {\n    return create(Utils.defaultContext());\n  }\n\n  public static <AT extends Appender<ILoggingEvent>> GetAppenderFunction<AT> create(\n      LoggerContext context) {\n    Logger logger = RootLoggerSupplier.create(context).get();\n    return new GetAppenderFunction<>(logger);\n  }\n}\n"
  },
  {
    "path": "logback-classic/src/main/java/com/tersesystems/logback/classic/functional/GetSiftedAppenderFunction.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.classic.functional;\n\nimport ch.qos.logback.classic.sift.SiftingAppender;\nimport ch.qos.logback.classic.spi.ILoggingEvent;\nimport ch.qos.logback.core.Appender;\nimport ch.qos.logback.core.sift.AppenderTracker;\nimport java.util.Optional;\nimport java.util.function.Function;\n\npublic class GetSiftedAppenderFunction<A> implements Function<SiftingAppender, Optional<A>> {\n\n  private final String key;\n\n  public GetSiftedAppenderFunction(String key) {\n    this.key = key;\n  }\n\n  @SuppressWarnings(\"unchecked\")\n  @Override\n  public Optional<A> apply(SiftingAppender siftingAppender) {\n    AppenderTracker<ILoggingEvent> appenderTracker = siftingAppender.getAppenderTracker();\n    try {\n      return Optional.ofNullable((A) appenderTracker.find(key));\n    } catch (ClassCastException e) {\n      return Optional.empty();\n    }\n  }\n\n  public static <AT extends Appender<ILoggingEvent>> GetSiftedAppenderFunction<AT> create(\n      String key) {\n    return new GetSiftedAppenderFunction<AT>(key);\n  }\n}\n"
  },
  {
    "path": "logback-classic/src/main/java/com/tersesystems/logback/classic/functional/RootLoggerSupplier.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.classic.functional;\n\nimport ch.qos.logback.classic.Logger;\nimport ch.qos.logback.classic.LoggerContext;\nimport com.tersesystems.logback.classic.Utils;\nimport java.util.function.Supplier;\n\npublic class RootLoggerSupplier implements Supplier<Logger> {\n\n  private final LoggerContext loggerContext;\n\n  public RootLoggerSupplier(LoggerContext loggerContext) {\n    this.loggerContext = loggerContext;\n  }\n\n  public Logger get() {\n    return loggerContext.getLogger(Logger.ROOT_LOGGER_NAME);\n  }\n\n  public static RootLoggerSupplier create(LoggerContext loggerContext) {\n    return new RootLoggerSupplier(loggerContext);\n  }\n\n  public static RootLoggerSupplier create() {\n    return create(Utils.defaultContext());\n  }\n}\n"
  },
  {
    "path": "logback-classic/src/main/java/com/tersesystems/logback/classic/sift/DiscriminatingMarker.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.classic.sift;\n\nimport ch.qos.logback.classic.spi.ILoggingEvent;\nimport com.tersesystems.logback.classic.TerseBasicMarker;\nimport java.util.function.Function;\n\npublic class DiscriminatingMarker extends TerseBasicMarker implements DiscriminatingValue {\n\n  private static final String TS_DISCRIMINATING_MARKER = \"TS_DESCRIMINATING_MARKER\";\n  private final Function<ILoggingEvent, String> discriminatingFunction;\n\n  public DiscriminatingMarker(Function<ILoggingEvent, String> discriminatingFunction) {\n    super(TS_DISCRIMINATING_MARKER);\n    this.discriminatingFunction = discriminatingFunction;\n  }\n\n  @Override\n  public String getDiscriminatingValue(ILoggingEvent loggingEvent) {\n    return discriminatingFunction.apply(loggingEvent);\n  }\n}\n"
  },
  {
    "path": "logback-classic/src/main/java/com/tersesystems/logback/classic/sift/DiscriminatingMarkerFactory.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.classic.sift;\n\nimport ch.qos.logback.classic.spi.ILoggingEvent;\nimport java.util.function.Function;\n\npublic class DiscriminatingMarkerFactory {\n\n  private final Function<ILoggingEvent, String> discriminatingFunction;\n\n  public DiscriminatingMarkerFactory(Function<ILoggingEvent, String> discriminatingFunction) {\n    this.discriminatingFunction = discriminatingFunction;\n  }\n\n  public static DiscriminatingMarkerFactory create(\n      Function<ILoggingEvent, String> discriminatingFunction) {\n    return new DiscriminatingMarkerFactory(discriminatingFunction);\n  }\n\n  public DiscriminatingMarker createMarker() {\n    return new DiscriminatingMarker(discriminatingFunction);\n  }\n}\n"
  },
  {
    "path": "logback-classic/src/main/java/com/tersesystems/logback/classic/sift/DiscriminatingValue.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.classic.sift;\n\nimport ch.qos.logback.classic.spi.ILoggingEvent;\n\npublic interface DiscriminatingValue {\n  String getDiscriminatingValue(ILoggingEvent loggingEvent);\n}\n"
  },
  {
    "path": "logback-classic/src/main/java/com/tersesystems/logback/classic/sift/MarkerBasedDiscriminator.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.classic.sift;\n\nimport ch.qos.logback.classic.spi.ILoggingEvent;\nimport ch.qos.logback.core.sift.AbstractDiscriminator;\nimport ch.qos.logback.core.sift.DefaultDiscriminator;\nimport java.util.Iterator;\nimport java.util.Optional;\nimport org.slf4j.Marker;\n\n/**\n * A discriminator that looks for a marker containing discriminating logic.\n *\n * @param <LoggingEventT> the logging event type\n */\npublic class MarkerBasedDiscriminator<LoggingEventT extends ILoggingEvent>\n    extends AbstractDiscriminator<LoggingEventT> {\n\n  private String key = \"key\";\n  private String defaultValue = DefaultDiscriminator.DEFAULT;\n\n  @Override\n  public String getDiscriminatingValue(ILoggingEvent loggingEvent) {\n    Optional<DiscriminatingValue> optMarker = getDiscriminatorMarker(loggingEvent);\n    return optMarker.map(m -> m.getDiscriminatingValue(loggingEvent)).orElse(getDefaultValue());\n  }\n\n  public Optional<DiscriminatingValue> getDiscriminatorMarker(ILoggingEvent loggingEvent) {\n    return fromMarker(loggingEvent.getMarker());\n  }\n\n  static Optional<DiscriminatingValue> fromMarker(Marker m) {\n    if (m instanceof DiscriminatingValue) {\n      DiscriminatingValue value = ((DiscriminatingValue) m);\n      return Optional.of(value);\n    }\n    for (Iterator<Marker> iter = m.iterator(); iter.hasNext(); ) {\n      Marker child = iter.next();\n      if (child instanceof DiscriminatingValue) {\n        DiscriminatingValue value = ((DiscriminatingValue) child);\n        return Optional.of(value);\n      }\n      if (child.hasReferences()) {\n        return fromMarker(child);\n      }\n    }\n    return Optional.empty();\n  }\n\n  @Override\n  public String getKey() {\n    return this.key;\n  }\n\n  public void setKey(String key) {\n    this.key = key;\n  }\n\n  public String getDefaultValue() {\n    return defaultValue;\n  }\n\n  public void setDefaultValue(String defaultValue) {\n    this.defaultValue = defaultValue;\n  }\n}\n"
  },
  {
    "path": "logback-classic/src/test/java/com/tersesystems/logback/classic/ChangeLogLevelTest.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.classic;\n\nimport org.junit.Test;\n\npublic class ChangeLogLevelTest {\n\n  @Test\n  public void testChangeLogLevel() {\n    // ILoggerFactory loggerFactory = LoggerFactory.getILoggerFactory();\n    // ChangeLogLevel changeLogLevel = new ChangeLogLevel();\n    // Logger logger = loggerFactory.getLogger(\"example\");\n    // assertThat(logger.isTraceEnabled()).isFalse();\n    // changeLogLevel.changeLogLevel(logger, \"TRACE\");\n    // assertThat(logger.isTraceEnabled()).isTrue();\n  }\n}\n"
  },
  {
    "path": "logback-classic/src/test/java/com/tersesystems/logback/classic/CorrelationIdMarker.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\n\npackage com.tersesystems.logback.classic;\n\nimport org.slf4j.Marker;\n\n/** A very simple correlation id marker. */\npublic interface CorrelationIdMarker extends Marker {\n  String getCorrelationId();\n\n  static CorrelationIdMarker create(String value) {\n    return new CorrelationIdBasicMarker(value);\n  }\n}\n\n/** Implementation of correlation id. */\nclass CorrelationIdBasicMarker extends TerseBasicMarker implements CorrelationIdMarker {\n  private final String value;\n\n  public CorrelationIdBasicMarker(String value) {\n    super(\"TS_CORRELATION_ID\");\n    this.value = value;\n  }\n\n  public String getCorrelationId() {\n    return value;\n  }\n}\n"
  },
  {
    "path": "logback-classic/src/test/java/com/tersesystems/logback/classic/CorrelationIdTurboFilter.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\n\npackage com.tersesystems.logback.classic;\n\nimport ch.qos.logback.classic.Level;\nimport ch.qos.logback.classic.Logger;\nimport ch.qos.logback.classic.LoggerContext;\nimport ch.qos.logback.classic.turbo.TurboFilter;\nimport ch.qos.logback.core.spi.FilterReply;\nimport java.util.Iterator;\nimport java.util.Map;\nimport java.util.function.Predicate;\nimport org.slf4j.Marker;\n\n/** Tells the tap filter to create an event and append it if a correlation id is found. */\npublic class CorrelationIdTurboFilter extends TurboFilter {\n  private String mdcKey = \"correlation_id\";\n  private Utils utils;\n\n  public String getMdcKey() {\n    return mdcKey;\n  }\n\n  public void setMdcKey(String mdcKey) {\n    this.mdcKey = mdcKey;\n  }\n\n  @Override\n  public void start() {\n    super.start();\n    utils = Utils.create((LoggerContext) getContext());\n  }\n\n  boolean doMarker(Marker m, Predicate<Marker> predicate) {\n    if (predicate.test(m)) {\n      return true;\n    }\n    if (m != null && m.hasReferences()) {\n      for (Iterator<Marker> iter = m.iterator(); iter.hasNext(); ) {\n        Marker child = iter.next();\n        if (predicate.test(child)) {\n          return true;\n        }\n        if (child.hasReferences()) {\n          return doMarker(child, predicate);\n        }\n      }\n    }\n    return false;\n  }\n\n  @Override\n  public FilterReply decide(\n      Marker marker, Logger logger, Level level, String format, Object[] params, Throwable t) {\n    // If there's a correlation id marker somewhere in the hierarchy, then good.\n    if (doMarker(marker, m -> m instanceof CorrelationIdMarker)) {\n      return FilterReply.ACCEPT;\n    }\n\n    // Look in MDC for a correlation id as well...\n    Map<String, String> mdcPropertyMap = utils.getMDCPropertyMap();\n    String mdcKey = getMdcKey();\n    if (mdcKey != null) {\n      if (mdcPropertyMap.containsKey(mdcKey)) {\n        return FilterReply.ACCEPT;\n      }\n    }\n\n    // Otherwise no.\n    return FilterReply.DENY;\n  }\n}\n"
  },
  {
    "path": "logback-classic/src/test/java/com/tersesystems/logback/classic/EnabledFilterTest.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.classic;\n\nimport ch.qos.logback.classic.spi.ILoggingEvent;\nimport ch.qos.logback.classic.spi.LoggingEventVO;\nimport ch.qos.logback.core.spi.FilterReply;\nimport com.tersesystems.logback.core.EnabledFilter;\nimport org.assertj.core.api.Assertions;\nimport org.junit.Test;\n\npublic class EnabledFilterTest {\n  @Test\n  public void testFilterFalse() {\n    EnabledFilter enabledFilter = new EnabledFilter();\n    enabledFilter.setEnabled(false);\n    enabledFilter.start();\n\n    ILoggingEvent loggingEvent = new LoggingEventVO();\n\n    Assertions.assertThat(enabledFilter.decide(loggingEvent)).isEqualTo(FilterReply.DENY);\n  }\n\n  @Test\n  public void testFilterTrue() {\n    EnabledFilter enabledFilter = new EnabledFilter();\n    enabledFilter.setEnabled(true);\n    enabledFilter.start();\n\n    ILoggingEvent loggingEvent = new LoggingEventVO();\n\n    Assertions.assertThat(enabledFilter.decide(loggingEvent)).isEqualTo(FilterReply.NEUTRAL);\n  }\n}\n"
  },
  {
    "path": "logback-classic/src/test/java/com/tersesystems/logback/classic/ExceptionMessageConverterTest.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.classic;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport ch.qos.logback.classic.Level;\nimport ch.qos.logback.classic.LoggerContext;\nimport ch.qos.logback.classic.spi.LoggingEvent;\nimport java.util.Arrays;\nimport org.junit.Test;\n\npublic class ExceptionMessageConverterTest {\n\n  @Test\n  public void testNoException() {\n    ExceptionMessageConverter converter = new ExceptionMessageConverter();\n    LoggerContext context = new LoggerContext();\n    converter.setContext(context);\n    converter.start();\n\n    LoggingEvent infoEvent =\n        new LoggingEvent(\"fcqn\", context.getLogger(\"fcqn\"), Level.INFO, \"info\", null, null);\n    String actual = converter.convert(infoEvent);\n\n    assertThat(actual).contains(\"\");\n  }\n\n  @Test\n  public void testSingleMessage() {\n    ExceptionMessageConverter converter = new ExceptionMessageConverter();\n    LoggerContext context = new LoggerContext();\n    converter.setContext(context);\n    converter.start();\n\n    RuntimeException ex = new RuntimeException(\"Hello world\");\n\n    LoggingEvent infoEvent =\n        new LoggingEvent(\"fcqn\", context.getLogger(\"fcqn\"), Level.INFO, \"info\", ex, null);\n    String actual = converter.convert(infoEvent);\n\n    assertThat(actual).isEqualTo(\" [Hello world]\");\n  }\n\n  @Test\n  public void testNestedMessages() {\n    ExceptionMessageConverter converter = new ExceptionMessageConverter();\n    LoggerContext context = new LoggerContext();\n    converter.setContext(context);\n    converter.start();\n\n    RuntimeException one = new RuntimeException(\"One\");\n    RuntimeException two = new RuntimeException(\"Two\", one);\n    RuntimeException three = new RuntimeException(\"Three\", two);\n    RuntimeException four = new RuntimeException(\"Four\", three);\n\n    LoggingEvent infoEvent =\n        new LoggingEvent(\"fcqn\", context.getLogger(\"fcqn\"), Level.INFO, \"info\", four, null);\n    String actual = converter.convert(infoEvent);\n\n    assertThat(actual).isEqualTo(\" [Four > Three > Two > One]\");\n  }\n\n  @Test\n  public void testNestedMessagesWithCutOff() {\n    ExceptionMessageConverter converter = new ExceptionMessageConverter();\n    converter.setOptionList(Arrays.asList(\"1\", \"2\"));\n    LoggerContext context = new LoggerContext();\n    converter.setContext(context);\n    converter.start();\n\n    RuntimeException one = new RuntimeException(\"One\");\n    RuntimeException two = new RuntimeException(\"Two\", one);\n    RuntimeException three = new RuntimeException(\"Three\", two);\n    RuntimeException four = new RuntimeException(\"Four\", three);\n\n    LoggingEvent infoEvent =\n        new LoggingEvent(\"fcqn\", context.getLogger(\"fcqn\"), Level.INFO, \"info\", four, null);\n    String actual = converter.convert(infoEvent);\n\n    assertThat(actual).isEqualTo(\" [Four > Three]\");\n  }\n\n  @Test\n  public void testNestedMessagesSeperator() {\n    ExceptionMessageConverter converter = new ExceptionMessageConverter();\n    converter.setOptionList(Arrays.asList(\"1\", \"4\", \"[\", \" ! \"));\n    LoggerContext context = new LoggerContext();\n    converter.setContext(context);\n    converter.start();\n\n    RuntimeException one = new RuntimeException(\"One\");\n    RuntimeException two = new RuntimeException(\"Two\", one);\n    RuntimeException three = new RuntimeException(\"Three\", two);\n    RuntimeException four = new RuntimeException(\"Four\", three);\n\n    LoggingEvent infoEvent =\n        new LoggingEvent(\"fcqn\", context.getLogger(\"fcqn\"), Level.INFO, \"info\", four, null);\n    String actual = converter.convert(infoEvent);\n\n    assertThat(actual).isEqualTo(\" [Four ! Three ! Two ! One]\");\n  }\n\n  @Test\n  public void testCustomPrefixSuffix() {\n    ExceptionMessageConverter converter = new ExceptionMessageConverter();\n    converter.setOptionList(Arrays.asList(\"0\", \"4\", \"<\", \"|\", \">\"));\n    LoggerContext context = new LoggerContext();\n    converter.setContext(context);\n    converter.start();\n\n    RuntimeException one = new RuntimeException(\"One\");\n    RuntimeException two = new RuntimeException(\"Two\", one);\n    RuntimeException three = new RuntimeException(\"Three\", two);\n    RuntimeException four = new RuntimeException(\"Four\", three);\n\n    LoggingEvent infoEvent =\n        new LoggingEvent(\"fcqn\", context.getLogger(\"fcqn\"), Level.INFO, \"info\", four, null);\n    String actual = converter.convert(infoEvent);\n\n    assertThat(actual).isEqualTo(\"<Four|Three|Two|One>\");\n  }\n}\n"
  },
  {
    "path": "logback-classic/src/test/java/com/tersesystems/logback/classic/SetLoggerLevelsActionTest.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.classic;\n\npublic class SetLoggerLevelsActionTest {}\n"
  },
  {
    "path": "logback-classic/src/test/java/com/tersesystems/logback/classic/TapFilterTest.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.classic;\n\nimport static java.util.Objects.requireNonNull;\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport ch.qos.logback.classic.Logger;\nimport ch.qos.logback.classic.LoggerContext;\nimport ch.qos.logback.classic.joran.JoranConfigurator;\nimport ch.qos.logback.classic.spi.ILoggingEvent;\nimport ch.qos.logback.classic.spi.TurboFilterList;\nimport ch.qos.logback.core.Appender;\nimport ch.qos.logback.core.joran.spi.JoranException;\nimport ch.qos.logback.core.read.ListAppender;\nimport ch.qos.logback.core.spi.AppenderAttachable;\nimport java.net.URL;\nimport java.util.Optional;\nimport org.junit.After;\nimport org.junit.Before;\nimport org.junit.jupiter.api.Test;\nimport org.slf4j.MDC;\n\npublic class TapFilterTest {\n\n  @Before\n  @After\n  public void clearMDC() {\n    MDC.clear();\n  }\n\n  @Test\n  public void testSimple() throws JoranException {\n    LoggerContext loggerFactory = createLoggerFactory(\"/logback-tapfilter.xml\");\n\n    // Write something that never gets logged explicitly...\n    Logger debugLogger = loggerFactory.getLogger(\"com.example.Debug\");\n    debugLogger.debug(\"debug one\");\n    debugLogger.debug(\"debug two\");\n    debugLogger.debug(\"debug three\");\n    debugLogger.debug(\"debug four\");\n\n    Logger logger = loggerFactory.getLogger(\"com.example.Test\");\n    logger.error(\"Write out error message to console\");\n\n    ListAppender<ILoggingEvent> listAppender = getListAppender(loggerFactory);\n    assertThat(listAppender.list.size()).isEqualTo(5);\n  }\n\n  @Test\n  public void testCorrelationWithNoMarker() throws JoranException {\n    LoggerContext loggerFactory = createLoggerFactory(\"/logback-tapfilter-correlation.xml\");\n\n    // Because there's no correlation id, it should never make it past the filter here.\n    Logger debugLogger = loggerFactory.getLogger(\"com.example.Debug\");\n    debugLogger.debug(\"debug one\");\n    debugLogger.debug(\"debug two\");\n    debugLogger.debug(\"debug three\");\n    debugLogger.debug(\"debug four\");\n\n    Logger logger = loggerFactory.getLogger(\"com.example.Test\");\n    logger.error(\"Write out error message to console\");\n\n    ListAppender<ILoggingEvent> listAppender = getListAppender(loggerFactory);\n    assertThat(listAppender.list.size()).isEqualTo(0);\n  }\n\n  @Test\n  public void testCorrelationWithMarker() throws JoranException {\n    LoggerContext loggerFactory = createLoggerFactory(\"/logback-tapfilter-correlation.xml\");\n\n    CorrelationIdMarker correlationIdMarker = CorrelationIdMarker.create(\"12345\");\n\n    // Because there's no correlation id, it should never make it past the filter here.\n    Logger debugLogger = loggerFactory.getLogger(\"com.example.Debug\");\n    debugLogger.debug(correlationIdMarker, \"debug one\");\n    debugLogger.debug(correlationIdMarker, \"debug two\");\n    debugLogger.debug(\"debug three\");\n    debugLogger.debug(\"debug four\");\n\n    Logger logger = loggerFactory.getLogger(\"com.example.Test\");\n    logger.error(\"Write out error message to console\");\n\n    ListAppender<ILoggingEvent> listAppender = getListAppender(loggerFactory);\n    assertThat(listAppender.list.size()).isEqualTo(2);\n  }\n\n  @Test\n  public void testCorrelationWithMDC() throws JoranException {\n    LoggerContext loggerFactory = createLoggerFactory(\"/logback-tapfilter-correlation.xml\");\n\n    MDC.put(\"correlationId\", \"12345\");\n    CorrelationIdMarker correlationIdMarker = CorrelationIdMarker.create(\"12345\");\n\n    // Because there's no correlation id, it should never make it past the filter here.\n    Logger debugLogger = loggerFactory.getLogger(\"com.example.Debug\");\n    debugLogger.debug(correlationIdMarker, \"debug one\");\n    debugLogger.debug(correlationIdMarker, \"debug two\");\n    debugLogger.debug(\"debug three\");\n    debugLogger.debug(\"debug four\");\n\n    Logger logger = loggerFactory.getLogger(\"com.example.Test\");\n    logger.error(\"Write out error message to console\");\n\n    ListAppender<ILoggingEvent> listAppender = getListAppender(loggerFactory);\n    assertThat(listAppender.list.size()).isEqualTo(5);\n  }\n\n  LoggerContext createLoggerFactory(String resourceName) throws JoranException {\n    LoggerContext context = new LoggerContext();\n    URL resource = getClass().getResource(resourceName);\n    JoranConfigurator configurator = new JoranConfigurator();\n    configurator.setContext(context);\n    configurator.doConfigure(resource);\n    return context;\n  }\n\n  Optional<Appender<ILoggingEvent>> getFilterAppender(TurboFilterList turboFilterList) {\n    return turboFilterList.stream()\n        .filter(f -> f instanceof AppenderAttachable<?>)\n        .map(f -> ((AppenderAttachable<ILoggingEvent>) f).iteratorForAppenders().next())\n        .findFirst();\n  }\n\n  ListAppender<ILoggingEvent> getListAppender(LoggerContext context) {\n    Optional<Appender<ILoggingEvent>> maybeAppender =\n        getFilterAppender(context.getTurboFilterList());\n    if (maybeAppender.isPresent()) {\n      return (ListAppender<ILoggingEvent>) requireNonNull(maybeAppender.get());\n    } else {\n      throw new IllegalStateException(\"Cannot find appender\");\n    }\n  }\n}\n"
  },
  {
    "path": "logback-classic/src/test/java/com/tersesystems/logback/classic/TerseHighlightConverterTest.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.classic;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport ch.qos.logback.classic.Level;\nimport ch.qos.logback.classic.LoggerContext;\nimport ch.qos.logback.classic.spi.LoggingEvent;\nimport java.util.HashMap;\nimport java.util.Map;\nimport org.junit.Test;\n\npublic class TerseHighlightConverterTest {\n\n  @Test\n  public void testHighlighter() {\n    TerseHighlightConverter converter = new TerseHighlightConverter();\n    LoggerContext context = new LoggerContext();\n    converter.setContext(context);\n    Map<String, String> properties = new HashMap<>();\n    properties.put(\"info\", \"red\");\n    context.putObject(TerseHighlightConverter.HIGHLIGHT_CTX_KEY, properties);\n    converter.start();\n    LoggingEvent infoEvent =\n        new LoggingEvent(\"fcqn\", context.getLogger(\"fcqn\"), Level.INFO, \"info\", null, null);\n    String actual = converter.convert(infoEvent);\n\n    assertThat(actual).contains(TerseHighlightConverter.Color.valueOf(\"RED\").code);\n  }\n}\n"
  },
  {
    "path": "logback-classic/src/test/java/com/tersesystems/logback/classic/UtilsTest.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.classic;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport ch.qos.logback.classic.Level;\nimport ch.qos.logback.classic.Logger;\nimport ch.qos.logback.classic.LoggerContext;\nimport ch.qos.logback.classic.turbo.MDCFilter;\nimport ch.qos.logback.classic.turbo.TurboFilter;\nimport ch.qos.logback.core.spi.FilterReply;\nimport org.junit.jupiter.api.Test;\nimport org.slf4j.Marker;\n\npublic class UtilsTest {\n\n  static class FancyTurboFilter extends TurboFilter {\n    @Override\n    public FilterReply decide(\n        Marker marker, Logger logger, Level level, String format, Object[] params, Throwable t) {\n      return null;\n    }\n  }\n\n  @Test\n  public void testTurboFilterMatchingType() {\n    LoggerContext loggerContext = new LoggerContext();\n    FancyTurboFilter fancyTurboFilter = new FancyTurboFilter();\n    fancyTurboFilter.setName(\"fancyTurboFilter\");\n    fancyTurboFilter.setContext(loggerContext);\n    loggerContext.addTurboFilter(fancyTurboFilter);\n\n    Utils utils = Utils.create(loggerContext);\n    assertThat(utils.getTurboFilter(FancyTurboFilter.class, \"fancyTurboFilter\")).isNotEmpty();\n  }\n\n  @Test\n  public void testTurboFilterNonMatchingType() {\n    LoggerContext loggerContext = new LoggerContext();\n    FancyTurboFilter fancyTurboFilter = new FancyTurboFilter();\n    fancyTurboFilter.setName(\"fancyTurboFilter\");\n    fancyTurboFilter.setContext(loggerContext);\n    loggerContext.addTurboFilter(fancyTurboFilter);\n\n    Utils utils = Utils.create(loggerContext);\n    assertThat(utils.getTurboFilter(MDCFilter.class, \"fancyTurboFilter\")).isEmpty();\n  }\n}\n"
  },
  {
    "path": "logback-classic/src/test/resources/logback-tapfilter-correlation.xml",
    "content": "<!--\n  ~ SPDX-License-Identifier: CC0-1.0\n  ~\n  ~ Copyright 2018-2020 Will Sargent.\n  ~\n  ~ Licensed under the CC0 Public Domain Dedication;\n  ~ You may obtain a copy of the License at\n  ~\n  ~  http://creativecommons.org/publicdomain/zero/1.0/\n  -->\n<configuration>\n\n  <newRule pattern=\"configuration/turboFilter/appender-ref\"\n           actionClass=\"ch.qos.logback.core.joran.action.AppenderRefAction\"/>\n\n  <appender name=\"LIST\" class=\"ch.qos.logback.core.read.ListAppender\">\n  </appender>\n\n  <turboFilter class=\"com.tersesystems.logback.classic.TapFilter\">\n    <!-- only tap if there's a correlation id marker or correlationId is in MDC -->\n    <turboFilter class=\"com.tersesystems.logback.classic.CorrelationIdTurboFilter\">\n      <mdcKey>correlationId</mdcKey>\n    </turboFilter>\n    <appender-ref ref=\"LIST\"/>\n  </turboFilter>\n\n  <appender name=\"CONSOLE\" class=\"ch.qos.logback.core.ConsoleAppender\">\n    <encoder>\n      <pattern>%-5relative %-5level %logger{35} - %msg%n</pattern>\n    </encoder>\n  </appender>\n\n  <root level=\"INFO\">\n    <appender-ref ref=\"CONSOLE\" />\n  </root>\n</configuration>\n\n"
  },
  {
    "path": "logback-classic/src/test/resources/logback-tapfilter.xml",
    "content": "<!--\n  ~ SPDX-License-Identifier: CC0-1.0\n  ~\n  ~ Copyright 2018-2020 Will Sargent.\n  ~\n  ~ Licensed under the CC0 Public Domain Dedication;\n  ~ You may obtain a copy of the License at\n  ~\n  ~  http://creativecommons.org/publicdomain/zero/1.0/\n  -->\n<configuration>\n\n  <newRule pattern=\"configuration/turboFilter/appender-ref\"\n           actionClass=\"ch.qos.logback.core.joran.action.AppenderRefAction\"/>\n\n  <appender name=\"LIST\" class=\"ch.qos.logback.core.read.ListAppender\">\n  </appender>\n\n  <turboFilter class=\"com.tersesystems.logback.classic.TapFilter\">\n    <appender-ref ref=\"LIST\"/>\n  </turboFilter>\n\n  <appender name=\"CONSOLE\" class=\"ch.qos.logback.core.ConsoleAppender\">\n    <encoder>\n      <pattern>%-5relative %-5level %logger{35} - %msg%n</pattern>\n    </encoder>\n  </appender>\n\n  <root level=\"INFO\">\n    <appender-ref ref=\"CONSOLE\" />\n  </root>\n</configuration>\n\n"
  },
  {
    "path": "logback-compress-encoder/gradle.properties",
    "content": "#\n# SPDX-License-Identifier: CC0-1.0\n#\n# Copyright 2018-2020 Will Sargent.\n#\n# Licensed under the CC0 Public Domain Dedication;\n# You may obtain a copy of the License at\n#\n#  http://creativecommons.org/publicdomain/zero/1.0/\n#\n\nproject_description = Logback Compress Encoder"
  },
  {
    "path": "logback-compress-encoder/logback-compress-encoder.gradle",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\ndependencies {\n    implementation project(':logback-classic')\n\n    implementation group: 'org.apache.commons', name: 'commons-compress', version: '1.18'\n    implementation \"com.github.luben:zstd-jni:$zstdVersion\"\n\n    testImplementation \"ch.qos.logback:logback-classic:$logbackVersion\"\n}"
  },
  {
    "path": "logback-compress-encoder/src/main/java/com.tersesystems.logback.compress/CompressingEncoder.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.compress;\n\nimport ch.qos.logback.core.encoder.Encoder;\nimport ch.qos.logback.core.encoder.EncoderBase;\nimport java.io.ByteArrayOutputStream;\nimport java.io.IOException;\nimport java.util.concurrent.atomic.LongAdder;\nimport org.apache.commons.compress.compressors.CompressorException;\nimport org.apache.commons.compress.compressors.CompressorOutputStream;\nimport org.apache.commons.compress.compressors.CompressorStreamFactory;\n\npublic class CompressingEncoder<E> extends EncoderBase<E> {\n  private final Accumulator accumulator;\n  private final Encoder<E> encoder;\n\n  public CompressingEncoder(\n      Encoder<E> encoder, String compressAlgo, CompressorStreamFactory factory, int bufferSize)\n      throws CompressorException {\n    this.encoder = encoder;\n    this.accumulator = new Accumulator(compressAlgo, factory, bufferSize);\n  }\n\n  @Override\n  public byte[] headerBytes() {\n    try {\n      return accumulator.apply(encoder.headerBytes());\n    } catch (IOException e) {\n      throw new RuntimeException(e);\n    }\n  }\n\n  @Override\n  public byte[] encode(E event) {\n    try {\n      return accumulator.apply(encoder.encode(event));\n    } catch (IOException e) {\n      throw new RuntimeException(e);\n    }\n  }\n\n  @Override\n  public byte[] footerBytes() {\n    try {\n      return accumulator.drain(encoder.footerBytes());\n    } catch (IOException e) {\n      throw new RuntimeException(e);\n    }\n  }\n\n  static class Accumulator {\n    private final ByteArrayOutputStream byteOutputStream;\n    private final CompressorOutputStream stream;\n    private final LongAdder count = new LongAdder();\n    private final int bufferSize;\n\n    public Accumulator(String compressAlgo, CompressorStreamFactory factory, int bufferSize)\n        throws CompressorException {\n      this.bufferSize = bufferSize;\n      this.byteOutputStream = new ByteArrayOutputStream();\n      this.stream = factory.createCompressorOutputStream(compressAlgo, byteOutputStream);\n    }\n\n    boolean isFlushable() {\n      return count.intValue() >= bufferSize;\n    }\n\n    byte[] apply(byte[] bytes) throws IOException {\n      count.add(bytes.length);\n      stream.write(bytes);\n\n      if (isFlushable()) {\n        stream.flush();\n        byte[] output = byteOutputStream.toByteArray();\n        byteOutputStream.reset();\n        count.reset();\n        return output;\n      } else {\n        return new byte[0];\n      }\n    }\n\n    byte[] drain(byte[] inputBytes) throws IOException {\n      if (inputBytes != null) {\n        stream.write(inputBytes);\n      }\n      stream.close();\n      count.reset();\n      return byteOutputStream.toByteArray();\n    }\n  }\n}\n"
  },
  {
    "path": "logback-compress-encoder/src/main/java/com.tersesystems.logback.compress/CompressingFileAppender.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.compress;\n\nimport ch.qos.logback.core.FileAppender;\nimport ch.qos.logback.core.UnsynchronizedAppenderBase;\nimport ch.qos.logback.core.encoder.Encoder;\nimport java.util.Set;\nimport org.apache.commons.compress.compressors.CompressorException;\nimport org.apache.commons.compress.compressors.CompressorStreamFactory;\n\npublic class CompressingFileAppender<E> extends UnsynchronizedAppenderBase<E> {\n\n  protected Encoder<E> encoder;\n\n  private FileAppender<E> fileAppender;\n\n  protected boolean append = true;\n\n  protected String fileName = null;\n\n  private boolean prudent = false;\n\n  private int bufferSize = 1024000;\n\n  private String compressAlgo = CompressorStreamFactory.getGzip();\n\n  public Encoder<E> getEncoder() {\n    return encoder;\n  }\n\n  public void setEncoder(Encoder<E> encoder) {\n    this.encoder = encoder;\n  }\n\n  public boolean isPrudent() {\n    return prudent;\n  }\n\n  public void setPrudent(boolean prudent) {\n    this.prudent = prudent;\n  }\n\n  public void setAppend(boolean append) {\n    this.append = append;\n  }\n\n  public void setFile(String file) {\n    fileName = file;\n  }\n\n  public boolean isAppend() {\n    return append;\n  }\n\n  public String getFile() {\n    return fileName;\n  }\n\n  public int getBufferSize() {\n    return bufferSize;\n  }\n\n  public void setBufferSize(int bufferSize) {\n    this.bufferSize = bufferSize;\n  }\n\n  public String getCompressAlgo() {\n    return compressAlgo;\n  }\n\n  public void setCompressAlgo(String compressAlgo) {\n    this.compressAlgo = compressAlgo;\n  }\n\n  @Override\n  public void start() {\n    fileAppender = new FileAppender<>();\n    fileAppender.setContext(getContext());\n    fileAppender.setFile(getFile());\n    fileAppender.setImmediateFlush(false);\n    fileAppender.setPrudent(isPrudent());\n    fileAppender.setAppend(isAppend());\n    fileAppender.setName(name + \"-embedded-file\");\n\n    CompressingEncoder<E> compressedEncoder = createCompressingEncoder(getEncoder());\n    fileAppender.setEncoder(compressedEncoder);\n    fileAppender.start();\n\n    super.start();\n  }\n\n  public void stop() {\n    fileAppender.stop();\n    super.stop();\n  }\n\n  @Override\n  protected void append(E eventObject) {\n    fileAppender.doAppend(eventObject);\n  }\n\n  protected CompressingEncoder<E> createCompressingEncoder(Encoder<E> e) {\n    int bufferSize = getBufferSize();\n    String compressAlgo = getCompressAlgo();\n\n    CompressorStreamFactory factory = CompressorStreamFactory.getSingleton();\n    Set<String> names = factory.getOutputStreamCompressorNames();\n    if (names.contains(getCompressAlgo())) {\n      try {\n        return new CompressingEncoder<>(e, compressAlgo, factory, bufferSize);\n      } catch (CompressorException ex) {\n        throw new RuntimeException(\"Cannot create CompressingEncoder\", ex);\n      }\n    } else {\n      throw new RuntimeException(\"No such compression algorithm: \" + compressAlgo);\n    }\n  }\n}\n"
  },
  {
    "path": "logback-compress-encoder/src/test/java/com/tersesystems/logback/compress/Utils.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.compress;\n\nimport java.io.ByteArrayOutputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\n\npublic class Utils {\n\n  public static byte[] readAllBytes(InputStream inputStream) throws IOException {\n    final int bufLen = 4 * 0x400; // 4KB\n    byte[] buf = new byte[bufLen];\n    int readLen;\n    IOException exception = null;\n\n    try {\n      try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {\n        while ((readLen = inputStream.read(buf, 0, bufLen)) != -1)\n          outputStream.write(buf, 0, readLen);\n\n        return outputStream.toByteArray();\n      }\n    } catch (IOException e) {\n      exception = e;\n      throw e;\n    } finally {\n      if (exception == null) inputStream.close();\n      else\n        try {\n          inputStream.close();\n        } catch (IOException e) {\n          exception.addSuppressed(e);\n        }\n    }\n  }\n}\n"
  },
  {
    "path": "logback-compress-encoder/src/test/resources/logback-with-zstd-encoder.xml",
    "content": "<!--\n  ~ SPDX-License-Identifier: CC0-1.0\n  ~\n  ~ Copyright 2018-2020 Will Sargent.\n  ~\n  ~ Licensed under the CC0 Public Domain Dedication;\n  ~ You may obtain a copy of the License at\n  ~\n  ~  http://creativecommons.org/publicdomain/zero/1.0/\n  -->\n<configuration>\n\n    <appender name=\"FILE\" class=\"com.tersesystems.logback.compress.CompressingFileAppender\">\n        <file>encoded.zst</file>\n        <compressAlgo>zstd</compressAlgo>\n        <bufferSize>10240</bufferSize>\n        <encoder>\n            <charset>UTF-8</charset>\n            <pattern>%-5level %logger{35} - %msg%n</pattern>\n        </encoder>\n    </appender>\n\n    <root level=\"TRACE\">\n        <appender-ref ref=\"FILE\"/>\n    </root>\n</configuration>"
  },
  {
    "path": "logback-core/gradle.properties",
    "content": "#\n# SPDX-License-Identifier: CC0-1.0\n#\n# Copyright 2018-2020 Will Sargent.\n#\n# Licensed under the CC0 Public Domain Dedication;\n# You may obtain a copy of the License at\n#\n#  http://creativecommons.org/publicdomain/zero/1.0/\n#\n\nproject_description = Logback Core (code only) Library"
  },
  {
    "path": "logback-core/logback-core.gradle",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\nplugins {\n    id 'java-library'\n}\n\ndependencies {\n    api \"org.slf4j:slf4j-api:$slf4jVersion\"\n    implementation \"ch.qos.logback:logback-core:$logbackVersion\"\n    testImplementation \"ch.qos.logback:logback-classic:$logbackVersion\"\n}"
  },
  {
    "path": "logback-core/src/main/java/com/tersesystems/logback/core/AbstractAppender.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.core;\n\nimport ch.qos.logback.core.Appender;\nimport ch.qos.logback.core.UnsynchronizedAppenderBase;\nimport ch.qos.logback.core.spi.AppenderAttachable;\nimport ch.qos.logback.core.spi.AppenderAttachableImpl;\nimport java.util.Iterator;\n\n/**\n * Provides abstract appender behavior with pre / post behavior.\n *\n * @param <E> the input type, usually ILoggingEvent\n */\npublic abstract class AbstractAppender<E> extends UnsynchronizedAppenderBase<E>\n    implements AppenderAttachable<E> {\n\n  protected AppenderAttachableImpl<E> aai = new AppenderAttachableImpl<E>();\n\n  protected abstract E appendEvent(E eventObject);\n\n  @Override\n  protected void append(E eventObject) {\n    preAppend();\n    aai.appendLoopOnAppenders(appendEvent(eventObject));\n    postAppend();\n  }\n\n  protected void postAppend() {}\n\n  protected void preAppend() {}\n\n  public void addAppender(Appender<E> newAppender) {\n    addInfo(\"Attaching appender named [\" + newAppender.getName() + \"] to \" + this.toString());\n    aai.addAppender(newAppender);\n  }\n\n  public Iterator<Appender<E>> iteratorForAppenders() {\n    return aai.iteratorForAppenders();\n  }\n\n  public void stop() {\n    super.stop();\n    aai.detachAndStopAllAppenders();\n  }\n\n  public Appender<E> getAppender(String name) {\n    return aai.getAppender(name);\n  }\n\n  public boolean isAttached(Appender<E> eAppender) {\n    return aai.isAttached(eAppender);\n  }\n\n  public void detachAndStopAllAppenders() {\n    aai.detachAndStopAllAppenders();\n  }\n\n  public boolean detachAppender(Appender<E> eAppender) {\n    return aai.detachAppender(eAppender);\n  }\n\n  public boolean detachAppender(String name) {\n    return aai.detachAppender(name);\n  }\n}\n"
  },
  {
    "path": "logback-core/src/main/java/com/tersesystems/logback/core/Component.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\n\npackage com.tersesystems.logback.core;\n\n/** A marker interface to let the caller know this can be placed in a ContainerComponent. */\npublic interface Component {}\n"
  },
  {
    "path": "logback-core/src/main/java/com/tersesystems/logback/core/ComponentContainer.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\n\npackage com.tersesystems.logback.core;\n\n/**\n * A component container.\n *\n * <p>Entries are encouraged but not required to extend Component.\n */\npublic interface ComponentContainer {\n  <T> void putComponent(Class<T> type, T instance);\n\n  <T> T getComponent(Class<T> type);\n\n  <T> boolean hasComponent(Class<T> type);\n}\n"
  },
  {
    "path": "logback-core/src/main/java/com/tersesystems/logback/core/CompositeAppender.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.core;\n\nimport ch.qos.logback.core.Appender;\nimport ch.qos.logback.core.UnsynchronizedAppenderBase;\nimport ch.qos.logback.core.spi.AppenderAttachable;\nimport ch.qos.logback.core.spi.AppenderAttachableImpl;\nimport java.util.Iterator;\n\n/**\n * This appender creates a composite of the underlying appenders but does not add or change any\n * functionality of those appenders.\n *\n * <p>It is very useful for referring to a list of appenders by a single name.\n *\n * @param <E> the event type, usually ILoggingEvent.\n */\npublic class CompositeAppender<E> extends UnsynchronizedAppenderBase<E>\n    implements AppenderAttachable<E> {\n\n  protected AppenderAttachableImpl<E> aai = new AppenderAttachableImpl<E>();\n\n  @Override\n  public void stop() {\n    if (isStarted()) {\n      detachAndStopAllAppenders();\n    }\n    super.stop();\n  }\n\n  @Override\n  protected void append(E eventObject) {\n    aai.appendLoopOnAppenders(eventObject);\n  }\n\n  public void addAppender(Appender<E> newAppender) {\n    addInfo(\"Attaching appender named [\" + newAppender.getName() + \"] to \" + this.toString());\n    aai.addAppender(newAppender);\n  }\n\n  public Iterator<Appender<E>> iteratorForAppenders() {\n    return aai.iteratorForAppenders();\n  }\n\n  public Appender<E> getAppender(String name) {\n    return aai.getAppender(name);\n  }\n\n  public boolean isAttached(Appender<E> eAppender) {\n    return aai.isAttached(eAppender);\n  }\n\n  public void detachAndStopAllAppenders() {\n    aai.detachAndStopAllAppenders();\n  }\n\n  public boolean detachAppender(Appender<E> eAppender) {\n    return aai.detachAppender(eAppender);\n  }\n\n  public boolean detachAppender(String name) {\n    return aai.detachAppender(name);\n  }\n}\n"
  },
  {
    "path": "logback-core/src/main/java/com/tersesystems/logback/core/DecoratingAppender.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.core;\n\nimport ch.qos.logback.core.Appender;\nimport ch.qos.logback.core.UnsynchronizedAppenderBase;\nimport ch.qos.logback.core.spi.AppenderAttachable;\nimport ch.qos.logback.core.spi.AppenderAttachableImpl;\nimport java.util.Iterator;\n\n/**\n * Decorates an event with additional class, using {@code decorateEvent}, and makes it available to\n * the appenders underneath it.\n *\n * @param <E> the input type, usually ILoggingEvent\n * @param <EE> the decorating type, must extend ILoggingEvent\n */\npublic abstract class DecoratingAppender<E, EE extends E> extends UnsynchronizedAppenderBase<E>\n    implements AppenderAttachable<EE> {\n\n  protected AppenderAttachableImpl<EE> aai = new AppenderAttachableImpl<EE>();\n\n  protected abstract EE decorateEvent(E eventObject);\n\n  @Override\n  protected void append(E eventObject) {\n    aai.appendLoopOnAppenders(decorateEvent(eventObject));\n  }\n\n  public void addAppender(Appender<EE> newAppender) {\n    addInfo(\"Attaching appender named [\" + newAppender.getName() + \"] to \" + this.toString());\n    aai.addAppender(newAppender);\n  }\n\n  public Iterator<Appender<EE>> iteratorForAppenders() {\n    return aai.iteratorForAppenders();\n  }\n\n  public Appender<EE> getAppender(String name) {\n    return aai.getAppender(name);\n  }\n\n  public boolean isAttached(Appender<EE> eAppender) {\n    return aai.isAttached(eAppender);\n  }\n\n  public void detachAndStopAllAppenders() {\n    aai.detachAndStopAllAppenders();\n  }\n\n  public boolean detachAppender(Appender<EE> eAppender) {\n    return aai.detachAppender(eAppender);\n  }\n\n  public boolean detachAppender(String name) {\n    return aai.detachAppender(name);\n  }\n\n  public void stop() {\n    if (isStarted()) {\n      aai.detachAndStopAllAppenders();\n    }\n    super.stop();\n  }\n}\n"
  },
  {
    "path": "logback-core/src/main/java/com/tersesystems/logback/core/DefaultAppenderAttachable.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.core;\n\nimport ch.qos.logback.core.Appender;\nimport ch.qos.logback.core.spi.AppenderAttachable;\nimport ch.qos.logback.core.spi.AppenderAttachableImpl;\nimport java.util.Iterator;\n\npublic interface DefaultAppenderAttachable<E> extends AppenderAttachable<E> {\n\n  AppenderAttachableImpl<E> appenderAttachableImpl();\n\n  default void addAppender(Appender<E> newAppender) {\n    appenderAttachableImpl().addAppender(newAppender);\n  }\n\n  default Iterator<Appender<E>> iteratorForAppenders() {\n    return appenderAttachableImpl().iteratorForAppenders();\n  }\n\n  default Appender<E> getAppender(String name) {\n    return appenderAttachableImpl().getAppender(name);\n  }\n\n  default boolean isAttached(Appender<E> eAppender) {\n    return appenderAttachableImpl().isAttached(eAppender);\n  }\n\n  default void detachAndStopAllAppenders() {\n    appenderAttachableImpl().detachAndStopAllAppenders();\n  }\n\n  default boolean detachAppender(Appender<E> eAppender) {\n    return appenderAttachableImpl().detachAppender(eAppender);\n  }\n\n  default boolean detachAppender(String name) {\n    return appenderAttachableImpl().detachAppender(name);\n  }\n}\n"
  },
  {
    "path": "logback-core/src/main/java/com/tersesystems/logback/core/EnabledFilter.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.core;\n\nimport ch.qos.logback.core.filter.Filter;\nimport ch.qos.logback.core.spi.FilterReply;\n\n/** Used to enable and disable appenders. */\npublic class EnabledFilter<E> extends Filter<E> {\n\n  private boolean enabled;\n\n  @Override\n  public FilterReply decide(E event) {\n    if (isStarted() && isEnabled()) {\n      return FilterReply.NEUTRAL;\n    } else {\n      return FilterReply.DENY;\n    }\n  }\n\n  public boolean isEnabled() {\n    return enabled;\n  }\n\n  public void setEnabled(boolean enabled) {\n    this.enabled = enabled;\n  }\n}\n"
  },
  {
    "path": "logback-core/src/main/java/com/tersesystems/logback/core/SelectAppender.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.core;\n\nimport ch.qos.logback.core.Appender;\nimport ch.qos.logback.core.AppenderBase;\nimport ch.qos.logback.core.spi.AppenderAttachable;\nimport ch.qos.logback.core.spi.AppenderAttachableImpl;\nimport java.util.Iterator;\n\n/** This class selects an appender by the appender key. */\npublic class SelectAppender<E> extends AppenderBase<E> implements AppenderAttachable<E> {\n\n  private AppenderAttachableImpl<E> aai = new AppenderAttachableImpl<E>();\n\n  private String appenderKey;\n\n  @Override\n  public void start() {\n    if (appenderKey == null || appenderKey.isEmpty()) {\n      addError(\"Null or empty appenderKey\");\n    } else {\n      super.start();\n    }\n  }\n\n  @Override\n  public void stop() {\n    if (isStarted()) {\n      detachAndStopAllAppenders();\n    }\n    super.stop();\n  }\n\n  @Override\n  public boolean isStarted() {\n    return super.isStarted();\n  }\n\n  @Override\n  protected void append(E eventObject) {\n    Appender<E> appender = aai.getAppender(appenderKey);\n    if (appender == null) {\n      addError(\"No appender found for appenderKey \" + appenderKey);\n    } else {\n      appender.doAppend(eventObject);\n    }\n  }\n\n  public String getAppenderKey() {\n    return appenderKey;\n  }\n\n  public void setAppenderKey(String appenderKey) {\n    this.appenderKey = appenderKey;\n  }\n\n  @Override\n  public void addAppender(Appender<E> newAppender) {\n    aai.addAppender(newAppender);\n  }\n\n  @Override\n  public Iterator<Appender<E>> iteratorForAppenders() {\n    return aai.iteratorForAppenders();\n  }\n\n  @Override\n  public Appender<E> getAppender(String name) {\n    return aai.getAppender(name);\n  }\n\n  @Override\n  public boolean isAttached(Appender<E> appender) {\n    return aai.isAttached(appender);\n  }\n\n  @Override\n  public void detachAndStopAllAppenders() {\n    aai.detachAndStopAllAppenders();\n  }\n\n  @Override\n  public boolean detachAppender(Appender<E> appender) {\n    return aai.detachAppender(appender);\n  }\n\n  @Override\n  public boolean detachAppender(String name) {\n    return aai.detachAppender(name);\n  }\n}\n"
  },
  {
    "path": "logback-core/src/main/java/com/tersesystems/logback/core/encoder/LayoutWrappingEncoder.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\n\n/**\n * Logback: the reliable, generic, fast and flexible logging framework. Copyright (C) 1999-2015,\n * QOS.ch. All rights reserved.\n *\n * <p>This program and the accompanying materials are dual-licensed under either the terms of the\n * Eclipse Public License v1.0 as published by the Eclipse Foundation\n *\n * <p>or (per the licensee's choosing)\n *\n * <p>under the terms of the GNU Lesser General Public License version 2.1 as published by the Free\n * Software Foundation.\n */\npackage com.tersesystems.logback.core.encoder;\n\nimport ch.qos.logback.core.CoreConstants;\nimport ch.qos.logback.core.Layout;\nimport ch.qos.logback.core.OutputStreamAppender;\nimport ch.qos.logback.core.encoder.EncoderBase;\nimport java.nio.charset.Charset;\n\n/**\n * A LayoutWrappingEncoder that doesn't require the parent be an appender.\n *\n * @param <E>\n */\npublic class LayoutWrappingEncoder<E> extends EncoderBase<E> {\n\n  protected Layout<E> layout;\n\n  /**\n   * The charset to use when converting a String into bytes.\n   * <p/>\n   * By default this property has the value\n   * <code>null</null> which corresponds to\n   * the system's default charset.\n   */\n  private Charset charset;\n\n  Object parent;\n  Boolean immediateFlush = null;\n\n  public Layout<E> getLayout() {\n    return layout;\n  }\n\n  public void setLayout(Layout<E> layout) {\n    this.layout = layout;\n  }\n\n  public Charset getCharset() {\n    return charset;\n  }\n\n  /**\n   * Set the charset to use when converting the string returned by the layout into bytes.\n   *\n   * <p>By default this property has the value <code>null</code> which corresponds to the system's\n   * default charset.\n   *\n   * @param charset the charset\n   */\n  public void setCharset(Charset charset) {\n    this.charset = charset;\n  }\n\n  /**\n   * Sets the immediateFlush option. The default value for immediateFlush is 'true'. If set to true,\n   * the doEncode() method will immediately flush the underlying OutputStream. Although immediate\n   * flushing is safer, it also significantly degrades logging throughput.\n   *\n   * @since 1.0.3\n   */\n  public void setImmediateFlush(boolean immediateFlush) {\n    addWarn(\n        \"As of version 1.2.0 \\\"immediateFlush\\\" property should be set within the enclosing Appender.\");\n    addWarn(\"Please move \\\"immediateFlush\\\" property into the enclosing appender.\");\n    this.immediateFlush = immediateFlush;\n  }\n\n  @Override\n  public byte[] headerBytes() {\n    if (layout == null) return null;\n\n    StringBuilder sb = new StringBuilder();\n    appendIfNotNull(sb, layout.getFileHeader());\n    appendIfNotNull(sb, layout.getPresentationHeader());\n    if (sb.length() > 0) {\n      // If at least one of file header or presentation header were not\n      // null, then append a line separator.\n      // This should be useful in most cases and should not hurt.\n      sb.append(CoreConstants.LINE_SEPARATOR);\n    }\n    return convertToBytes(sb.toString());\n  }\n\n  @Override\n  public byte[] footerBytes() {\n    if (layout == null) return null;\n\n    StringBuilder sb = new StringBuilder();\n    appendIfNotNull(sb, layout.getPresentationFooter());\n    appendIfNotNull(sb, layout.getFileFooter());\n    return convertToBytes(sb.toString());\n  }\n\n  private byte[] convertToBytes(String s) {\n    if (charset == null) {\n      return s.getBytes();\n    } else {\n      return s.getBytes(charset);\n    }\n  }\n\n  public byte[] encode(E event) {\n    String txt = layout.doLayout(event);\n    return convertToBytes(txt);\n  }\n\n  public boolean isStarted() {\n    return false;\n  }\n\n  public void start() {\n    if (immediateFlush != null) {\n      if (parent instanceof OutputStreamAppender) {\n        addWarn(\n            \"Setting the \\\"immediateFlush\\\" property of the enclosing appender to \"\n                + immediateFlush);\n        @SuppressWarnings(\"unchecked\")\n        OutputStreamAppender<E> parentOutputStreamAppender = (OutputStreamAppender<E>) parent;\n        parentOutputStreamAppender.setImmediateFlush(immediateFlush);\n      } else {\n        addError(\"Could not set the \\\"immediateFlush\\\" property of the enclosing appender.\");\n      }\n    }\n    started = true;\n  }\n\n  public void stop() {\n    started = false;\n  }\n\n  private void appendIfNotNull(StringBuilder sb, String s) {\n    if (s != null) {\n      sb.append(s);\n    }\n  }\n\n  // This needs to be changed from the out of the box appender in logback because we don't\n  // want to only be able to use a PatternLayoutEncoder in an appender.\n  public void setParent(Object parent) {\n    this.parent = parent;\n  }\n}\n"
  },
  {
    "path": "logback-core/src/main/java/com/tersesystems/logback/core/pattern/PatternLayoutEncoderBase.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\n\n/**\n * Logback: the reliable, generic, fast and flexible logging framework. Copyright (C) 1999-2015,\n * QOS.ch. All rights reserved.\n *\n * <p>This program and the accompanying materials are dual-licensed under either the terms of the\n * Eclipse Public License v1.0 as published by the Eclipse Foundation\n *\n * <p>or (per the licensee's choosing)\n *\n * <p>under the terms of the GNU Lesser General Public License version 2.1 as published by the Free\n * Software Foundation.\n */\npackage com.tersesystems.logback.core.pattern;\n\nimport ch.qos.logback.core.Layout;\nimport com.tersesystems.logback.core.encoder.LayoutWrappingEncoder;\n\n/**\n * A PatternLayoutEncoderBase that doesn't require the parent is an appender.\n *\n * @param <E>\n */\npublic class PatternLayoutEncoderBase<E> extends LayoutWrappingEncoder<E> {\n\n  String pattern;\n\n  // due to popular demand outputPatternAsHeader is set to false by default\n  protected boolean outputPatternAsHeader = false;\n\n  public String getPattern() {\n    return pattern;\n  }\n\n  public void setPattern(String pattern) {\n    this.pattern = pattern;\n  }\n\n  public boolean isOutputPatternAsHeader() {\n    return outputPatternAsHeader;\n  }\n\n  /**\n   * Print the pattern string as a header in log files\n   *\n   * @param outputPatternAsHeader\n   * @since 1.0.3\n   */\n  public void setOutputPatternAsHeader(boolean outputPatternAsHeader) {\n    this.outputPatternAsHeader = outputPatternAsHeader;\n  }\n\n  public boolean isOutputPatternAsPresentationHeader() {\n    return outputPatternAsHeader;\n  }\n\n  /**\n   * @deprecated replaced by {@link #setOutputPatternAsHeader(boolean)}\n   */\n  public void setOutputPatternAsPresentationHeader(boolean outputPatternAsHeader) {\n    addWarn(\n        \"[outputPatternAsPresentationHeader] property is deprecated. Please use [outputPatternAsHeader] option instead.\");\n    this.outputPatternAsHeader = outputPatternAsHeader;\n  }\n\n  @Override\n  public void setLayout(Layout<E> layout) {\n    throw new UnsupportedOperationException(\n        \"one cannot set the layout of \" + this.getClass().getName());\n  }\n}\n"
  },
  {
    "path": "logback-core/src/test/java/com/tersesystems/logback/core/CompositeAppenderTest.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.core;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport ch.qos.logback.classic.Logger;\nimport ch.qos.logback.classic.LoggerContext;\nimport ch.qos.logback.classic.joran.JoranConfigurator;\nimport ch.qos.logback.classic.spi.ILoggingEvent;\nimport ch.qos.logback.core.joran.spi.JoranException;\nimport ch.qos.logback.core.read.ListAppender;\nimport java.net.URL;\nimport org.junit.Test;\n\npublic class CompositeAppenderTest {\n\n  @Test\n  public void testSimpleAppender() throws JoranException {\n    LoggerContext context = new LoggerContext();\n    URL resource = getClass().getResource(\"/logback-with-composite-appender.xml\");\n    JoranConfigurator configurator = new JoranConfigurator();\n    configurator.setContext(context);\n    configurator.doConfigure(resource);\n\n    ch.qos.logback.classic.Logger logger = context.getLogger(Logger.ROOT_LOGGER_NAME);\n    CompositeAppender<ILoggingEvent> composite =\n        (CompositeAppender<ILoggingEvent>) logger.getAppender(\"CONSOLE_AND_FILE\");\n    ListAppender<ILoggingEvent> file = (ListAppender<ILoggingEvent>) composite.getAppender(\"FILE\");\n    ListAppender<ILoggingEvent> console =\n        (ListAppender<ILoggingEvent>) composite.getAppender(\"CONSOLE\");\n\n    logger.info(\"hello world\");\n    assertThat(file.list.get(0).getMessage()).isEqualTo(\"hello world\");\n    assertThat(console.list.get(0).getMessage()).isEqualTo(\"hello world\");\n  }\n}\n"
  },
  {
    "path": "logback-core/src/test/java/com/tersesystems/logback/core/SelectAppenderTest.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.core;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport ch.qos.logback.classic.Logger;\nimport ch.qos.logback.classic.LoggerContext;\nimport ch.qos.logback.classic.joran.JoranConfigurator;\nimport ch.qos.logback.classic.spi.ILoggingEvent;\nimport ch.qos.logback.core.joran.spi.JoranException;\nimport ch.qos.logback.core.read.ListAppender;\nimport java.net.URL;\nimport org.junit.Test;\n\npublic class SelectAppenderTest {\n\n  @Test\n  public void testWithTestEnvironment() throws JoranException {\n    LoggerContext context = new LoggerContext();\n    URL resource = getClass().getResource(\"/logback-with-select-appender.xml\");\n    JoranConfigurator configurator = new JoranConfigurator();\n    context.putProperty(\"APPENDER_KEY\", \"test\");\n    configurator.setContext(context);\n    configurator.doConfigure(resource);\n\n    ch.qos.logback.classic.Logger logger = context.getLogger(Logger.ROOT_LOGGER_NAME);\n    SelectAppender selectAppender = (SelectAppender) logger.getAppender(\"SELECT\");\n    CompositeAppender<ILoggingEvent> development =\n        (CompositeAppender<ILoggingEvent>) selectAppender.getAppender(\"test\");\n    ListAppender<ILoggingEvent> listAppender =\n        (ListAppender<ILoggingEvent>) development.getAppender(\"test-list\");\n\n    logger.info(\"hello world\");\n    assertThat(listAppender.list.get(0).getMessage()).isEqualTo(\"hello world\");\n  }\n\n  @Test\n  public void testWithDevelopmentEnvironment() throws JoranException {\n    LoggerContext context = new LoggerContext();\n    URL resource = getClass().getResource(\"/logback-with-select-appender.xml\");\n    JoranConfigurator configurator = new JoranConfigurator();\n    context.putProperty(\"APPENDER_KEY\", \"development\");\n    configurator.setContext(context);\n    configurator.doConfigure(resource);\n\n    ch.qos.logback.classic.Logger logger = context.getLogger(Logger.ROOT_LOGGER_NAME);\n    SelectAppender selectAppender = (SelectAppender) logger.getAppender(\"SELECT\");\n    CompositeAppender<ILoggingEvent> development =\n        (CompositeAppender<ILoggingEvent>) selectAppender.getAppender(\"test\");\n    ListAppender<ILoggingEvent> listAppender =\n        (ListAppender<ILoggingEvent>) development.getAppender(\"test-list\");\n\n    logger.info(\"hello world\");\n    assertThat(listAppender.list.size()).isEqualTo(0);\n  }\n}\n"
  },
  {
    "path": "logback-core/src/test/java/com/tersesystems/logback/core/TestAppender.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.core;\n\nimport ch.qos.logback.classic.spi.ILoggingEvent;\nimport ch.qos.logback.core.AppenderBase;\nimport ch.qos.logback.core.encoder.Encoder;\nimport java.util.ArrayList;\nimport java.util.List;\n\npublic class TestAppender extends AppenderBase<ILoggingEvent> {\n\n  protected Encoder<ILoggingEvent> encoder;\n\n  public static List<ILoggingEvent> events = new ArrayList<>();\n\n  public Encoder<ILoggingEvent> getEncoder() {\n    return encoder;\n  }\n\n  public void setEncoder(Encoder<ILoggingEvent> encoder) {\n    this.encoder = encoder;\n  }\n\n  @Override\n  protected void append(ILoggingEvent e) {\n    events.add(e);\n  }\n}\n"
  },
  {
    "path": "logback-core/src/test/resources/logback-with-composite-appender.xml",
    "content": "<!--\n  ~ SPDX-License-Identifier: CC0-1.0\n  ~\n  ~ Copyright 2018-2020 Will Sargent.\n  ~\n  ~ Licensed under the CC0 Public Domain Dedication;\n  ~ You may obtain a copy of the License at\n  ~\n  ~  http://creativecommons.org/publicdomain/zero/1.0/\n  -->\n<configuration debug=\"true\">\n\n    <appender name=\"CONSOLE\" class=\"ch.qos.logback.core.read.ListAppender\">\n    </appender>\n\n    <appender name=\"FILE\" class=\"ch.qos.logback.core.read.ListAppender\">\n    </appender>\n\n    <appender name=\"CONSOLE_AND_FILE\" class=\"com.tersesystems.logback.core.CompositeAppender\">\n        <appender-ref ref=\"CONSOLE\"/>\n        <appender-ref ref=\"FILE\"/>\n    </appender>\n\n    <root level=\"TRACE\">\n        <appender-ref ref=\"CONSOLE_AND_FILE\"/>\n    </root>\n</configuration>"
  },
  {
    "path": "logback-core/src/test/resources/logback-with-select-appender.xml",
    "content": "<!--\n  ~ SPDX-License-Identifier: CC0-1.0\n  ~\n  ~ Copyright 2018-2020 Will Sargent.\n  ~\n  ~ Licensed under the CC0 Public Domain Dedication;\n  ~ You may obtain a copy of the License at\n  ~\n  ~  http://creativecommons.org/publicdomain/zero/1.0/\n  -->\n<configuration>\n\n    <appender name=\"SELECT\" class=\"com.tersesystems.logback.core.SelectAppender\">\n        <appenderKey>${APPENDER_KEY}</appenderKey>\n\n        <appender class=\"com.tersesystems.logback.core.CompositeAppender\">\n            <name>test</name>\n            <appender class=\"ch.qos.logback.core.read.ListAppender\">\n                <name>test-list</name>\n            </appender>\n        </appender>\n\n        <appender class=\"com.tersesystems.logback.core.CompositeAppender\">\n            <name>development</name>\n            <appender class=\"ch.qos.logback.core.ConsoleAppender\">\n                <name>development-console</name>\n                <encoder>\n                    <pattern>%-5relative %-5level %logger{35} - %msg%n</pattern>\n                </encoder>\n            </appender>\n        </appender>\n\n        <appender class=\"com.tersesystems.logback.core.CompositeAppender\">\n            <name>staging</name>\n            <appender class=\"ch.qos.logback.core.ConsoleAppender\">\n                <name>staging-console</name>\n                <encoder>\n                    <pattern>%-5relative %-5level %logger{35} - %msg%n</pattern>\n                </encoder>\n            </appender>\n\n            <!--<appender class=\"ch.qos.logback.core.FileAppender\">-->\n            <!--    <name>staging-file</name>-->\n            <!--    <file>file.log</file>-->\n            <!--    <encoder>-->\n            <!--        <pattern>%-5relative %-5level %logger{35} - %msg%n</pattern>-->\n            <!--    </encoder>-->\n            <!--</appender>-->\n        </appender>\n    </appender>\n\n    <root level=\"TRACE\">\n        <appender-ref ref=\"SELECT\"/>\n    </root>\n\n</configuration>"
  },
  {
    "path": "logback-correlationid/gradle.properties",
    "content": "#\n# SPDX-License-Identifier: CC0-1.0\n#\n# Copyright 2018-2020 Will Sargent.\n#\n# Licensed under the CC0 Public Domain Dedication;\n# You may obtain a copy of the License at\n#\n#  http://creativecommons.org/publicdomain/zero/1.0/\n#\n\nproject_description = Logback Correlation ID Utilities"
  },
  {
    "path": "logback-correlationid/logback-correlationid.gradle",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\ndependencies {\n    implementation project(\":logback-classic\")\n\n    testImplementation 'org.awaitility:awaitility:4.0.2'\n    testImplementation \"net.logstash.logback:logstash-logback-encoder:$logstashVersion\"\n}\n"
  },
  {
    "path": "logback-correlationid/src/main/java/com/tersesystems/logback/correlationid/CorrelationIdDecider.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\n\npackage com.tersesystems.logback.correlationid;\n\nimport ch.qos.logback.classic.Level;\nimport ch.qos.logback.classic.Logger;\nimport ch.qos.logback.core.spi.FilterReply;\nimport com.tersesystems.logback.classic.TurboFilterDecider;\nimport java.util.Optional;\nimport org.slf4j.Marker;\n\npublic class CorrelationIdDecider implements TurboFilterDecider {\n  protected final CorrelationIdUtils utils;\n\n  public CorrelationIdDecider(CorrelationIdUtils utils) {\n    this.utils = utils;\n  }\n\n  @Override\n  public FilterReply decide(\n      Marker marker, Logger logger, Level level, String format, Object[] params, Throwable t) {\n    Optional<String> maybeCorrelationId = utils.get(utils.getMDCPropertyMap(), marker);\n    return maybeCorrelationId.isPresent() ? FilterReply.ACCEPT : FilterReply.NEUTRAL;\n  }\n}\n"
  },
  {
    "path": "logback-correlationid/src/main/java/com/tersesystems/logback/correlationid/CorrelationIdFilter.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\n\npackage com.tersesystems.logback.correlationid;\n\nimport ch.qos.logback.classic.spi.ILoggingEvent;\nimport ch.qos.logback.core.filter.Filter;\nimport ch.qos.logback.core.spi.FilterReply;\nimport java.util.Optional;\n\npublic class CorrelationIdFilter extends Filter<ILoggingEvent> {\n  private String mdcKey = \"correlation_id\";\n\n  public String getMdcKey() {\n    return mdcKey;\n  }\n\n  public void setMdcKey(String mdcKey) {\n    this.mdcKey = mdcKey;\n  }\n\n  protected CorrelationIdUtils utils;\n\n  @Override\n  public void start() {\n    super.start();\n    utils = new CorrelationIdUtils(mdcKey);\n  }\n\n  @Override\n  public FilterReply decide(ILoggingEvent event) {\n    Optional<String> maybeCorrelationId = utils.get(event.getMDCPropertyMap(), event.getMarker());\n    return maybeCorrelationId.isPresent() ? FilterReply.ACCEPT : FilterReply.DENY;\n  }\n}\n"
  },
  {
    "path": "logback-correlationid/src/main/java/com/tersesystems/logback/correlationid/CorrelationIdMarker.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\n\npackage com.tersesystems.logback.correlationid;\n\nimport com.tersesystems.logback.classic.TerseBasicMarker;\nimport org.slf4j.Marker;\n\n/** A very simple correlation id marker. */\npublic interface CorrelationIdMarker extends Marker, CorrelationIdProvider {\n  static CorrelationIdMarker create(String value) {\n    return new CorrelationIdBasicMarker(value);\n  }\n}\n\n/** Implementation of correlation id. */\nclass CorrelationIdBasicMarker extends TerseBasicMarker implements CorrelationIdMarker {\n  private final String value;\n\n  public CorrelationIdBasicMarker(String value) {\n    super(\"TS_CORRELATION_ID\");\n    this.value = value;\n  }\n\n  public String getCorrelationId() {\n    return value;\n  }\n}\n"
  },
  {
    "path": "logback-correlationid/src/main/java/com/tersesystems/logback/correlationid/CorrelationIdProvider.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\n\npackage com.tersesystems.logback.correlationid;\n\nimport com.tersesystems.logback.core.Component;\n\n/** A correlation id component. */\npublic interface CorrelationIdProvider extends Component {\n  String getCorrelationId();\n}\n"
  },
  {
    "path": "logback-correlationid/src/main/java/com/tersesystems/logback/correlationid/CorrelationIdTapFilter.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\n\npackage com.tersesystems.logback.correlationid;\n\nimport ch.qos.logback.classic.Level;\nimport ch.qos.logback.classic.Logger;\nimport ch.qos.logback.classic.spi.ILoggingEvent;\nimport ch.qos.logback.core.spi.FilterReply;\nimport com.tersesystems.logback.classic.TapFilter;\nimport java.util.Map;\nimport org.slf4j.Marker;\n\n/** Tells the tap filter to create an event and append it if a correlation id is found. */\npublic class CorrelationIdTapFilter extends TapFilter {\n  private String mdcKey = \"correlation_id\";\n  private CorrelationIdUtils utils;\n\n  public String getMdcKey() {\n    return mdcKey;\n  }\n\n  public void setMdcKey(String mdcKey) {\n    this.mdcKey = mdcKey;\n  }\n\n  @Override\n  public void start() {\n    super.start();\n    utils = new CorrelationIdUtils(mdcKey);\n  }\n\n  @Override\n  public FilterReply decide(\n      Marker marker, Logger logger, Level level, String format, Object[] params, Throwable t) {\n    if (logger != null && format == null && params == null) {\n      // Need to turn on events for everything so that we can cover conditional events.\n      return FilterReply.ACCEPT;\n    }\n\n    Map<String, String> mdcPropertyMap = utils.getMDCPropertyMap();\n    if (utils.get(mdcPropertyMap, marker).isPresent()) {\n      ILoggingEvent loggingEvent =\n          getLoggingEventFactory().create(marker, logger, level, format, params, t);\n      // initialize the mdc in the logging event...\n      loggingEvent.prepareForDeferredProcessing();\n      // For every message that is acceptable, store it in the appender and return.\n      appenderAttachableImpl().appendLoopOnAppenders(loggingEvent);\n    }\n    return FilterReply.NEUTRAL;\n  }\n}\n"
  },
  {
    "path": "logback-correlationid/src/main/java/com/tersesystems/logback/correlationid/CorrelationIdUtils.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\n\npackage com.tersesystems.logback.correlationid;\n\nimport ch.qos.logback.classic.util.LogbackMDCAdapter;\nimport java.util.Collections;\nimport java.util.Iterator;\nimport java.util.Map;\nimport java.util.Optional;\nimport org.slf4j.MDC;\nimport org.slf4j.Marker;\nimport org.slf4j.spi.MDCAdapter;\n\npublic class CorrelationIdUtils {\n\n  private final String mdcKey;\n\n  public CorrelationIdUtils(String mdcKey) {\n    this.mdcKey = mdcKey;\n  }\n\n  public Optional<String> get(Map<String, String> mdcPropertyMap, Marker marker) {\n    Optional<String> first = fromMarker(marker);\n    if (first.isPresent()) {\n      return first;\n    } else {\n      return get(mdcPropertyMap);\n    }\n  }\n\n  /** Pulls the correlation id from a marker which is a CorrelationIdProvider. */\n  public Optional<String> fromMarker(Marker m) {\n    if (m instanceof CorrelationIdProvider) {\n      CorrelationIdProvider provider = ((CorrelationIdProvider) m);\n      return Optional.of(provider.getCorrelationId());\n    }\n    if (m != null && m.hasReferences()) {\n      for (Iterator<Marker> iter = m.iterator(); iter.hasNext(); ) {\n        Marker child = iter.next();\n        if (child instanceof CorrelationIdProvider) {\n          CorrelationIdProvider provider = ((CorrelationIdProvider) child);\n          return Optional.of(provider.getCorrelationId());\n        }\n        if (child.hasReferences()) {\n          return fromMarker(child);\n        }\n      }\n    }\n    return Optional.empty();\n  }\n\n  public Optional<String> get(Map<String, String> mdcPropertyMap) {\n    // Look in MDC for a correlation id as well...\n    if (mdcKey != null) {\n      String s = mdcPropertyMap.get(mdcKey);\n      if (s != null) {\n        return Optional.of(s);\n      }\n    }\n    return Optional.empty();\n  }\n\n  public Map<String, String> getMDCPropertyMap() {\n    MDCAdapter mdc = MDC.getMDCAdapter();\n\n    Map<String, String> mdcPropertyMap;\n    if (mdc instanceof LogbackMDCAdapter)\n      mdcPropertyMap = ((LogbackMDCAdapter) mdc).getPropertyMap();\n    else mdcPropertyMap = mdc.getCopyOfContextMap();\n\n    // mdcPropertyMap still null, use emptyMap()\n    if (mdcPropertyMap == null) mdcPropertyMap = Collections.emptyMap();\n\n    return mdcPropertyMap;\n  }\n}\n"
  },
  {
    "path": "logback-correlationid/src/test/java/com.tersesystems.logback.correlationid/CorrelationIdFilterTest.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\n\npackage com.tersesystems.logback.correlationid;\n\nimport static java.util.Objects.requireNonNull;\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport ch.qos.logback.classic.Logger;\nimport ch.qos.logback.classic.LoggerContext;\nimport ch.qos.logback.classic.joran.JoranConfigurator;\nimport ch.qos.logback.classic.spi.ILoggingEvent;\nimport ch.qos.logback.core.Appender;\nimport ch.qos.logback.core.joran.spi.JoranException;\nimport ch.qos.logback.core.read.ListAppender;\nimport java.net.URL;\nimport java.util.Iterator;\nimport java.util.Optional;\nimport org.junit.After;\nimport org.junit.Before;\nimport org.junit.jupiter.api.Test;\nimport org.slf4j.MDC;\n\npublic class CorrelationIdFilterTest {\n  @Before\n  @After\n  public void clearMDC() {\n    MDC.clear();\n  }\n\n  @Test\n  public void testFilter() throws JoranException {\n    MDC.clear();\n    LoggerContext loggerFactory = createLoggerFactory(\"/logback-correlationid.xml\");\n\n    // Write something that never gets logged explicitly...\n    Logger logger = loggerFactory.getLogger(\"com.example.Debug\");\n    String correlationId = \"12345\";\n    CorrelationIdMarker correlationIdMarker = CorrelationIdMarker.create(correlationId);\n\n    // should be logged because marker\n    logger.info(correlationIdMarker, \"info one\");\n\n    logger.info(\"info two\"); // should not be logged\n\n    // Everything below this point should be logged.\n    MDC.put(\"correlationId\", correlationId);\n    logger.info(\"info three\");\n    logger.info(correlationIdMarker, \"info four\");\n\n    ListAppender<ILoggingEvent> listAppender = getListAppender(loggerFactory);\n    assertThat(listAppender.list.size()).isEqualTo(3);\n  }\n\n  LoggerContext createLoggerFactory(String resourceName) throws JoranException {\n    LoggerContext context = new LoggerContext();\n    URL resource = getClass().getResource(resourceName);\n    JoranConfigurator configurator = new JoranConfigurator();\n    configurator.setContext(context);\n    configurator.doConfigure(resource);\n    return context;\n  }\n\n  ListAppender<ILoggingEvent> getListAppender(LoggerContext context) {\n    Optional<Appender<ILoggingEvent>> maybeAppender =\n        getFirstAppender(context.getLogger(Logger.ROOT_LOGGER_NAME));\n    if (maybeAppender.isPresent()) {\n      return (ListAppender<ILoggingEvent>) requireNonNull(maybeAppender.get());\n    } else {\n      throw new IllegalStateException(\"Cannot find appender\");\n    }\n  }\n\n  private Optional<Appender<ILoggingEvent>> getFirstAppender(Logger logger) {\n    for (Iterator<Appender<ILoggingEvent>> iter = logger.iteratorForAppenders(); iter.hasNext(); ) {\n      Appender<ILoggingEvent> next = logger.iteratorForAppenders().next();\n      return Optional.of(next);\n    }\n    return Optional.empty();\n  }\n}\n"
  },
  {
    "path": "logback-correlationid/src/test/java/com.tersesystems.logback.correlationid/CorrelationIdTapFilterTest.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\n\npackage com.tersesystems.logback.correlationid;\n\nimport static java.util.Objects.requireNonNull;\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport ch.qos.logback.classic.Logger;\nimport ch.qos.logback.classic.LoggerContext;\nimport ch.qos.logback.classic.joran.JoranConfigurator;\nimport ch.qos.logback.classic.spi.ILoggingEvent;\nimport ch.qos.logback.classic.spi.TurboFilterList;\nimport ch.qos.logback.core.Appender;\nimport ch.qos.logback.core.joran.spi.JoranException;\nimport ch.qos.logback.core.read.ListAppender;\nimport ch.qos.logback.core.spi.AppenderAttachable;\nimport java.net.URL;\nimport java.util.Optional;\nimport org.junit.After;\nimport org.junit.Before;\nimport org.junit.jupiter.api.Test;\nimport org.slf4j.MDC;\n\npublic class CorrelationIdTapFilterTest {\n\n  @Before\n  @After\n  public void clearMDC() {\n    MDC.clear();\n  }\n\n  @Test\n  public void testCorrelationWithNoMarker() throws JoranException {\n    MDC.clear();\n    LoggerContext loggerFactory = createLoggerFactory(\"/logback-correlationid-tapfilter.xml\");\n\n    // Because there's no correlation id, it should never make it past the filter here.\n    Logger debugLogger = loggerFactory.getLogger(\"com.example.Debug\");\n    debugLogger.debug(\"debug one\");\n    debugLogger.debug(\"debug two\");\n    debugLogger.debug(\"debug three\");\n    debugLogger.debug(\"debug four\");\n\n    Logger logger = loggerFactory.getLogger(\"com.example.Test\");\n    logger.error(\"Write out error message to console\");\n\n    ListAppender<ILoggingEvent> listAppender = getListAppender(loggerFactory);\n    assertThat(listAppender.list.size()).isEqualTo(0);\n  }\n\n  @Test\n  public void testCorrelationWithMarker() throws JoranException {\n    MDC.clear();\n    LoggerContext loggerFactory = createLoggerFactory(\"/logback-correlationid-tapfilter.xml\");\n\n    CorrelationIdMarker correlationIdMarker = CorrelationIdMarker.create(\"12345\");\n\n    // Because there's no correlation id, it should never make it past the filter here.\n    Logger debugLogger = loggerFactory.getLogger(\"com.example.Debug\");\n    debugLogger.debug(correlationIdMarker, \"debug one\");\n    debugLogger.debug(correlationIdMarker, \"debug two\");\n    debugLogger.debug(\"debug three\");\n    debugLogger.debug(\"debug four\");\n\n    Logger logger = loggerFactory.getLogger(\"com.example.Test\");\n    logger.error(\"Write out error message to console\");\n\n    ListAppender<ILoggingEvent> listAppender = getListAppender(loggerFactory);\n    assertThat(listAppender.list.size()).isEqualTo(2);\n  }\n\n  @Test\n  public void testCorrelationWithMDC() throws JoranException {\n    MDC.clear();\n    LoggerContext loggerFactory = createLoggerFactory(\"/logback-correlationid-tapfilter.xml\");\n\n    MDC.put(\"correlationId\", \"12345\");\n    CorrelationIdMarker correlationIdMarker = CorrelationIdMarker.create(\"12345\");\n\n    // Because there's no correlation id, it should never make it past the filter here.\n    Logger debugLogger = loggerFactory.getLogger(\"com.example.Debug\");\n    debugLogger.debug(correlationIdMarker, \"debug one\");\n    debugLogger.debug(correlationIdMarker, \"debug two\");\n    debugLogger.debug(\"debug three\");\n    debugLogger.debug(\"debug four\");\n\n    Logger logger = loggerFactory.getLogger(\"com.example.Test\");\n    logger.error(\"Write out error message to console\");\n\n    ListAppender<ILoggingEvent> listAppender = getListAppender(loggerFactory);\n    assertThat(listAppender.list.size()).isEqualTo(5);\n  }\n\n  LoggerContext createLoggerFactory(String resourceName) throws JoranException {\n    LoggerContext context = new LoggerContext();\n    URL resource = getClass().getResource(resourceName);\n    JoranConfigurator configurator = new JoranConfigurator();\n    configurator.setContext(context);\n    configurator.doConfigure(resource);\n    return context;\n  }\n\n  Optional<Appender<ILoggingEvent>> getFilterAppender(TurboFilterList turboFilterList) {\n    return turboFilterList.stream()\n        .filter(f -> f instanceof AppenderAttachable<?>)\n        .map(f -> ((AppenderAttachable<ILoggingEvent>) f).iteratorForAppenders().next())\n        .findFirst();\n  }\n\n  ListAppender<ILoggingEvent> getListAppender(LoggerContext context) {\n    Optional<Appender<ILoggingEvent>> maybeAppender =\n        getFilterAppender(context.getTurboFilterList());\n    if (maybeAppender.isPresent()) {\n      return (ListAppender<ILoggingEvent>) requireNonNull(maybeAppender.get());\n    } else {\n      throw new IllegalStateException(\"Cannot find appender\");\n    }\n  }\n}\n"
  },
  {
    "path": "logback-correlationid/src/test/resources/logback-correlationid-jdbc.xml",
    "content": "<!--\n  ~ SPDX-License-Identifier: CC0-1.0\n  ~\n  ~ Copyright 2018-2020 Will Sargent.\n  ~\n  ~ Licensed under the CC0 Public Domain Dedication;\n  ~ You may obtain a copy of the License at\n  ~\n  ~  http://creativecommons.org/publicdomain/zero/1.0/\n  -->\n<configuration>\n\n  <shutdownHook class=\"ch.qos.logback.core.hook.DelayingShutdownHook\"/>\n\n  <appender name=\"ASYNC_JDBC\" class=\"net.logstash.logback.appender.LoggingEventAsyncDisruptorAppender\">\n    <appender class=\"com.tersesystems.logback.correlationid.CorrelationIdJDBCAppender\">\n      <mdcKey>correlationId</mdcKey>\n\n<!--          <driver>com.p6spy.engine.spy.P6SpyDriver</driver>-->\n<!--          <url>jdbc:p6spy:h2:mem:terse-logback;DB_CLOSE_DELAY=-1</url>-->\n      <driver>org.h2.Driver</driver>\n      <url>jdbc:h2:mem:terse-logback;DB_CLOSE_DELAY=-1</url>\n      <username>sa</username>\n      <password></password>\n\n      <createStatements>\n        CREATE TABLE IF NOT EXISTS events (\n        ID NUMERIC NOT NULL PRIMARY KEY AUTO_INCREMENT,\n        ts TIMESTAMP(9) WITH TIME ZONE NOT NULL,\n        relative_ns BIGINT NULL,\n        start_ms BIGINT NULL,\n        level_value int NOT NULL,\n        level VARCHAR(7) NOT NULL,\n        evt JSON NOT NULL,\n        correlation_id VARCHAR(255) NOT NULL,\n        event_id VARCHAR(255) NULL\n        );\n        CREATE INDEX IF NOT EXISTS event_id_idx ON events(event_id);\n        CREATE INDEX IF NOT EXISTS correlation_id_idx ON events(correlation_id);\n      </createStatements>\n      <insertStatement>\n        insert into events(ts, relative_ns, start_ms, level_value, level, evt, correlation_id, event_id) values(?, ?, ?, ?, ?, ?, ?, ?)\n      </insertStatement>\n\n      <encoder class=\"net.logstash.logback.encoder.LogstashEncoder\">\n      </encoder>\n    </appender>\n  </appender>\n\n  <appender name=\"CONSOLE\" class=\"com.tersesystems.logback.uniqueid.UniqueIdComponentAppender\">\n    <appender class=\"ch.qos.logback.core.ConsoleAppender\">\n      <encoder>\n        <pattern>%-5relative %-5level %logger{35} - %msg%n</pattern>\n      </encoder>\n    </appender>\n  </appender>\n\n  <logger name=\"com.example.ExampleClass\" level=\"INFO\" additivity=\"false\">\n    <appender-ref ref=\"ASYNC_JDBC\" />\n  </logger>\n\n  <root level=\"INFO\">\n    <appender-ref ref=\"CONSOLE\" />\n  </root>\n</configuration>\n\n"
  },
  {
    "path": "logback-correlationid/src/test/resources/logback-correlationid-tapfilter.xml",
    "content": "<!--\n  ~ SPDX-License-Identifier: CC0-1.0\n  ~\n  ~ Copyright 2018-2020 Will Sargent.\n  ~\n  ~ Licensed under the CC0 Public Domain Dedication;\n  ~ You may obtain a copy of the License at\n  ~\n  ~  http://creativecommons.org/publicdomain/zero/1.0/\n  -->\n<configuration>\n\n  <newRule pattern=\"configuration/turboFilter/appender-ref\"\n           actionClass=\"ch.qos.logback.core.joran.action.AppenderRefAction\"/>\n\n  <appender name=\"TAP_LIST\" class=\"ch.qos.logback.core.read.ListAppender\">\n  </appender>\n\n  <turboFilter class=\"com.tersesystems.logback.correlationid.CorrelationIdTapFilter\">\n    <mdcKey>correlationId</mdcKey>\n    <appender-ref ref=\"TAP_LIST\"/>\n  </turboFilter>\n\n  <appender name=\"CONSOLE\" class=\"ch.qos.logback.core.ConsoleAppender\">\n    <encoder>\n      <pattern>%-5relative %-5level %logger{35} - %msg%n</pattern>\n    </encoder>\n  </appender>\n\n  <root level=\"INFO\">\n    <appender-ref ref=\"CONSOLE\" />\n  </root>\n</configuration>\n\n"
  },
  {
    "path": "logback-correlationid/src/test/resources/logback-correlationid.xml",
    "content": "<!--\n  ~ SPDX-License-Identifier: CC0-1.0\n  ~\n  ~ Copyright 2018-2020 Will Sargent.\n  ~\n  ~ Licensed under the CC0 Public Domain Dedication;\n  ~ You may obtain a copy of the License at\n  ~\n  ~  http://creativecommons.org/publicdomain/zero/1.0/\n  -->\n<configuration>\n  <appender name=\"LIST\" class=\"ch.qos.logback.core.read.ListAppender\">\n    <filter class=\"com.tersesystems.logback.correlationid.CorrelationIdFilter\">\n      <mdcKey>correlationId</mdcKey>\n    </filter>\n  </appender>\n\n  <root level=\"INFO\">\n    <appender-ref ref=\"LIST\" />\n  </root>\n</configuration>\n\n"
  },
  {
    "path": "logback-correlationid/src/test/resources/spy.properties",
    "content": "#\n# SPDX-License-Identifier: CC0-1.0\n#\n# Copyright 2018-2020 Will Sargent.\n#\n# Licensed under the CC0 Public Domain Dedication;\n# You may obtain a copy of the License at\n#\n#  http://creativecommons.org/publicdomain/zero/1.0/\n#\n\ndriverlist=org.h2.Driver"
  },
  {
    "path": "logback-exception-mapping/gradle.properties",
    "content": "#\n# SPDX-License-Identifier: CC0-1.0\n#\n# Copyright 2018-2020 Will Sargent.\n#\n# Licensed under the CC0 Public Domain Dedication;\n# You may obtain a copy of the License at\n#\n#  http://creativecommons.org/publicdomain/zero/1.0/\n#\n\nproject_description = Logback Exception Mapping"
  },
  {
    "path": "logback-exception-mapping/logback-exception-mapping.gradle",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\nplugins {\n    id 'java-library'\n}\n\ndependencies {\n    api project(':logback-classic')\n}"
  },
  {
    "path": "logback-exception-mapping/src/main/java/com/tersesystems/logback/exceptionmapping/BeanExceptionMapping.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.exceptionmapping;\n\nimport ch.qos.logback.core.joran.util.beans.BeanUtil;\nimport java.lang.reflect.InvocationTargetException;\nimport java.util.*;\nimport java.util.function.Consumer;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\n\npublic class BeanExceptionMapping implements ExceptionMapping {\n  private final List<String> methodNames;\n  private final Consumer<Exception> reporter;\n  private final String name;\n\n  public BeanExceptionMapping(\n      String name, List<String> propertyNames, Consumer<Exception> reporter) {\n    this.name = name;\n    this.methodNames = propertyNames;\n    this.reporter = reporter;\n  }\n\n  @Override\n  public String getName() {\n    return name;\n  }\n\n  @Override\n  public List<ExceptionProperty> apply(Throwable e) {\n    return methodNames.stream()\n        .flatMap(methodName -> findMethod(e, methodName))\n        .collect(Collectors.toList());\n  }\n\n  protected Stream<ExceptionProperty> findMethod(Throwable e, String methodName) {\n    return Arrays.stream(e.getClass().getMethods())\n        .filter(\n            method ->\n                methodName.equals(BeanUtil.getPropertyName(method)) && BeanUtil.isGetter(method))\n        .map(\n            method -> {\n              try {\n                Object invokeResult = method.invoke(e, (Object[]) null);\n                return Optional.of(ExceptionProperty.create(methodName, invokeResult));\n              } catch (IllegalAccessException | InvocationTargetException ex) {\n                reporter.accept(ex);\n              }\n              return Optional.<ExceptionProperty>empty();\n            })\n        .filter(Optional::isPresent)\n        .map(Optional::get);\n  }\n}\n"
  },
  {
    "path": "logback-exception-mapping/src/main/java/com/tersesystems/logback/exceptionmapping/Constants.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.exceptionmapping;\n\npublic final class Constants {\n\n  public static final String REGISTRY_BAG = \"EXCEPTION_REGISTRY_BAG\";\n\n  public static final String DEFAULT_MAPPINGS_KEY = \"default\";\n}\n"
  },
  {
    "path": "logback-exception-mapping/src/main/java/com/tersesystems/logback/exceptionmapping/DefaultExceptionMappingRegistry.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.exceptionmapping;\n\nimport static java.util.Arrays.asList;\n\nimport java.util.*;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.function.Consumer;\nimport java.util.function.Function;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\n\npublic class DefaultExceptionMappingRegistry implements ExceptionMappingRegistry {\n  private Consumer<Exception> exceptionHandler;\n  private Map<String, ExceptionMapping> classNameToMappings;\n\n  public DefaultExceptionMappingRegistry(Consumer<Exception> exceptionHandler) {\n    this.classNameToMappings = new ConcurrentHashMap<>();\n    this.exceptionHandler = exceptionHandler;\n  }\n\n  // ---------------------\n  // register maps\n\n  @Override\n  public void register(ClassLoader classLoader, Map<String, List<String>> mappers) {\n    mappers.forEach(\n        (className, methodNames) ->\n            register(classLoader, className, methodNames.toArray(new String[0])));\n  }\n\n  @Override\n  public void register(Map<String, List<String>> mappers) {\n    ClassLoader classLoader = ClassLoader.getSystemClassLoader();\n    mappers.forEach(\n        (className, methodNames) ->\n            register(classLoader, className, methodNames.toArray(new String[0])));\n  }\n\n  // ---------------------\n  // register methodNames\n\n  @Override\n  public void register(String className, String... methodNames) {\n    register(ClassLoader.getSystemClassLoader(), className, methodNames);\n  }\n\n  @SuppressWarnings(\"unchecked\")\n  @Override\n  public void register(ClassLoader classLoader, String className, String... methodNames) {\n    try {\n      Class<? extends Throwable> clazz =\n          (Class<? extends Throwable>) classLoader.loadClass(className);\n      register(clazz, methodNames);\n    } catch (Exception e) {\n      exceptionHandler.accept(e);\n    }\n  }\n\n  @Override\n  public <E extends Throwable> void register(Class<E> exceptionClass, String... propertyNames) {\n    register(\n        new BeanExceptionMapping(\n            exceptionClass.getName(), asList(propertyNames), exceptionHandler));\n  }\n\n  @SuppressWarnings(\"unchecked\")\n  @Override\n  public <E extends Throwable> void register(\n      Class<E> exceptionClass, Function<E, List<ExceptionProperty>> f) {\n    register(\n        new FunctionExceptionMapping(\n            exceptionClass.getName(), (Function<Throwable, List<ExceptionProperty>>) f));\n  }\n\n  @Override\n  public void register(String className, Function<Throwable, List<ExceptionProperty>> f) {\n    register(new FunctionExceptionMapping(className, f));\n  }\n\n  // ---------------------\n  // register ExceptionMapping\n\n  @Override\n  public void register(ExceptionMapping mapping) {\n    classNameToMappings.put(mapping.getName(), mapping);\n  }\n\n  // ---------------------\n  // apply\n\n  @Override\n  public List<ExceptionProperty> apply(Throwable e) {\n    Stream<Class<?>> classStream = new ExceptionHierarchyIterator(e.getClass()).stream();\n    List<ExceptionMapping> exceptionMappings =\n        classStream\n            .map(Class::getName)\n            .filter(className -> classNameToMappings.containsKey(className))\n            .map(className -> classNameToMappings.get(className))\n            .collect(Collectors.toList());\n\n    List<ExceptionProperty> exceptionProperties = new ArrayList<>();\n    for (ExceptionMapping exceptionMapping : exceptionMappings) {\n      List<ExceptionProperty> propertyList = exceptionMapping.apply(e);\n      exceptionProperties.addAll(propertyList);\n    }\n    return exceptionProperties;\n  }\n\n  @Override\n  public Iterator<ExceptionMapping> iterator() {\n    return classNameToMappings.values().iterator();\n  }\n\n  @Override\n  public ExceptionMapping get(String name) {\n    return classNameToMappings.get(name);\n  }\n\n  @Override\n  public boolean contains(ExceptionMapping exceptionMapping) {\n    return contains(exceptionMapping.getName());\n  }\n\n  @Override\n  public boolean contains(String name) {\n    return classNameToMappings.containsKey(name);\n  }\n\n  @Override\n  public boolean remove(ExceptionMapping exceptionMapping) {\n    return classNameToMappings.remove(exceptionMapping.getName()) != null;\n  }\n\n  @Override\n  public boolean remove(String name) {\n    return classNameToMappings.remove(name) != null;\n  }\n}\n"
  },
  {
    "path": "logback-exception-mapping/src/main/java/com/tersesystems/logback/exceptionmapping/ExceptionCauseIterator.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.exceptionmapping;\n\nimport java.util.Iterator;\nimport java.util.Spliterator;\nimport java.util.Spliterators;\nimport java.util.stream.Stream;\nimport java.util.stream.StreamSupport;\n\npublic class ExceptionCauseIterator implements Iterator<Throwable> {\n  private Throwable throwable;\n\n  ExceptionCauseIterator(Throwable throwable) {\n    this.throwable = throwable;\n  }\n\n  @Override\n  public boolean hasNext() {\n    return throwable != null;\n  }\n\n  @SuppressWarnings(\"unchecked\")\n  @Override\n  public Throwable next() {\n    Throwable oldThrowable = throwable;\n    if (throwable != null) {\n      throwable = throwable.getCause();\n    }\n    return oldThrowable;\n  }\n\n  @SuppressWarnings(\"unchecked\")\n  public Stream<Throwable> stream() {\n    Spliterator spliterator = Spliterators.spliteratorUnknownSize(this, 0);\n    return (Stream<Throwable>) StreamSupport.stream(spliterator, false);\n  }\n\n  public static ExceptionCauseIterator create(Throwable throwable) {\n    return new ExceptionCauseIterator(throwable);\n  }\n}\n"
  },
  {
    "path": "logback-exception-mapping/src/main/java/com/tersesystems/logback/exceptionmapping/ExceptionHierarchyIterator.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.exceptionmapping;\n\nimport java.util.Iterator;\nimport java.util.Spliterator;\nimport java.util.Spliterators;\nimport java.util.stream.Stream;\nimport java.util.stream.StreamSupport;\n\npublic class ExceptionHierarchyIterator implements Iterator<Class<?>> {\n  private Class<?> clazz;\n\n  ExceptionHierarchyIterator(Class<?> clazz) {\n    this.clazz = clazz;\n  }\n\n  @Override\n  public boolean hasNext() {\n    return clazz != null;\n  }\n\n  @SuppressWarnings(\"unchecked\")\n  @Override\n  public Class<?> next() {\n    Class<?> oldClass = clazz;\n    if (clazz != null) {\n      clazz = clazz.getSuperclass();\n    }\n    return oldClass;\n  }\n\n  @SuppressWarnings(\"unchecked\")\n  public Stream<Class<?>> stream() {\n    Spliterator spliterator = Spliterators.spliteratorUnknownSize(this, 0);\n    return (Stream<Class<?>>) StreamSupport.stream(spliterator, false);\n  }\n\n  public static ExceptionHierarchyIterator create(Class<?> clazz) {\n    return new ExceptionHierarchyIterator(clazz);\n  }\n}\n"
  },
  {
    "path": "logback-exception-mapping/src/main/java/com/tersesystems/logback/exceptionmapping/ExceptionMapping.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.exceptionmapping;\n\nimport java.util.List;\nimport java.util.function.Function;\n\npublic interface ExceptionMapping extends Function<Throwable, List<ExceptionProperty>> {\n  String getName();\n}\n"
  },
  {
    "path": "logback-exception-mapping/src/main/java/com/tersesystems/logback/exceptionmapping/ExceptionMappingAction.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.exceptionmapping;\n\nimport ch.qos.logback.core.joran.action.Action;\nimport ch.qos.logback.core.joran.spi.InterpretationContext;\nimport ch.qos.logback.core.util.OptionHelper;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.function.Consumer;\nimport org.xml.sax.Attributes;\n\npublic class ExceptionMappingAction extends Action {\n\n  Consumer<Exception> handler = e -> addError(\"Cannot map exception\", e);\n  boolean inError = false;\n\n  @SuppressWarnings(\"unchecked\")\n  public void begin(InterpretationContext ec, String tagName, Attributes attributes) {\n    // Let us forget about previous errors (in this object)\n    inError = false;\n\n    // logger.debug(\"begin called\");\n    Object o = ec.peekObject();\n\n    if (!(o instanceof ExceptionMappingRegistry)) {\n      String errMsg =\n          \"Could not find a registry at the top of execution stack. Near [\"\n              + tagName\n              + \"] line \"\n              + getLineNumber(ec);\n      inError = true;\n      addInfo(errMsg); // This can trigger in an \"if\" block from janino, so it may not be serious...\n      return;\n    }\n\n    ExceptionMappingRegistry registry = (ExceptionMappingRegistry) o;\n    String mappingName = ec.subst(attributes.getValue(\"name\"));\n    String properties = ec.subst(attributes.getValue(\"properties\"));\n\n    if (OptionHelper.isEmpty(mappingName)) {\n      // print a meaningful error message and return\n      String errMsg = \"Missing name attribute in tag.\";\n      inError = true;\n      addError(errMsg);\n      return;\n    }\n\n    if (OptionHelper.isEmpty(properties)) {\n      // print a meaningful error message and return\n      String errMsg = \"Missing properties attribute in tag.\";\n      inError = true;\n      addError(errMsg);\n      return;\n    }\n\n    List<String> mappingPropertyNames = new ArrayList<String>(Arrays.asList(properties.split(\",\")));\n    ExceptionMapping newMapping =\n        new BeanExceptionMapping(mappingName, mappingPropertyNames, handler);\n\n    addInfo(\n        \"Attaching mapping named [\" + mappingName + \"] to \" + registry + \"at \" + getLineNumber(ec));\n    registry.register(newMapping);\n  }\n\n  public void end(InterpretationContext ec, String n) {}\n}\n"
  },
  {
    "path": "logback-exception-mapping/src/main/java/com/tersesystems/logback/exceptionmapping/ExceptionMappingRegistry.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.exceptionmapping;\n\nimport java.util.*;\nimport java.util.function.Function;\n\npublic interface ExceptionMappingRegistry {\n\n  void register(Map<String, List<String>> mappers);\n\n  void register(ClassLoader classLoader, Map<String, List<String>> mappers);\n\n  void register(String className, String... methodNames);\n\n  void register(ClassLoader classLoader, String className, String... methodNames);\n\n  <E extends Throwable> void register(Class<E> exceptionClass, String... propertyNames);\n\n  <E extends Throwable> void register(\n      Class<E> exceptionClass, Function<E, List<ExceptionProperty>> f);\n\n  void register(String className, Function<Throwable, List<ExceptionProperty>> f);\n\n  void register(ExceptionMapping mapper);\n\n  List<ExceptionProperty> apply(Throwable e);\n\n  Iterator<ExceptionMapping> iterator();\n\n  ExceptionMapping get(String name);\n\n  boolean contains(ExceptionMapping exceptionMapping);\n\n  boolean contains(String name);\n\n  boolean remove(ExceptionMapping exceptionMapping);\n\n  boolean remove(String name);\n}\n"
  },
  {
    "path": "logback-exception-mapping/src/main/java/com/tersesystems/logback/exceptionmapping/ExceptionMappingRegistryAction.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.exceptionmapping;\n\nimport static com.tersesystems.logback.exceptionmapping.Constants.*;\nimport static java.lang.String.*;\nimport static java.util.Arrays.asList;\nimport static java.util.Collections.singletonList;\n\nimport ch.qos.logback.core.Context;\nimport ch.qos.logback.core.joran.action.Action;\nimport ch.qos.logback.core.joran.spi.ActionException;\nimport ch.qos.logback.core.joran.spi.InterpretationContext;\nimport ch.qos.logback.core.util.OptionHelper;\nimport java.io.InterruptedIOException;\nimport java.util.*;\nimport java.util.function.Consumer;\nimport javax.xml.stream.Location;\nimport javax.xml.stream.XMLStreamException;\nimport org.w3c.dom.DOMException;\nimport org.w3c.dom.events.EventException;\nimport org.xml.sax.Attributes;\n\npublic class ExceptionMappingRegistryAction extends Action {\n\n  ExceptionMappingRegistry mappingsRegistry;\n  private boolean inError = false;\n  private final Consumer<Exception> handler = e -> addError(\"Cannot map exception!\", e);\n\n  @SuppressWarnings(\"unchecked\")\n  public void begin(InterpretationContext ic, String localName, Attributes attributes)\n      throws ActionException {\n    mappingsRegistry = null;\n    inError = false;\n\n    Context context = getContext();\n    HashMap<String, ExceptionMappingRegistry> mappingsBag =\n        (HashMap<String, ExceptionMappingRegistry>) context.getObject(REGISTRY_BAG);\n    if (mappingsBag == null) {\n      mappingsBag = new HashMap<>();\n      context.putObject(REGISTRY_BAG, mappingsBag);\n    }\n\n    try {\n      mappingsRegistry = new DefaultExceptionMappingRegistry(handler);\n      initializeRegistry(mappingsRegistry);\n\n      String mappingsName = ic.subst(attributes.getValue(NAME_ATTRIBUTE));\n      if (OptionHelper.isEmpty(mappingsName)) {\n        addInfo(\n            format(\n                \"No mappingsRegistry name given for mappingsRegistry, using default \\\"%s\\\"\",\n                DEFAULT_MAPPINGS_KEY));\n        mappingsName = DEFAULT_MAPPINGS_KEY;\n      } else {\n        addInfo(\"Naming mappingsRegistry as [\" + mappingsName + \"]\");\n      }\n\n      mappingsBag.put(mappingsName, mappingsRegistry);\n\n      ic.pushObject(mappingsRegistry);\n    } catch (Exception oops) {\n      inError = true;\n      addError(\"Could not create registry.\", oops);\n      throw new ActionException(oops);\n    }\n  }\n\n  protected void initializeRegistry(ExceptionMappingRegistry registry) {\n    List<ExceptionMapping> exceptionMappings = initialMappings();\n    for (ExceptionMapping exceptionMapping : exceptionMappings) {\n      registry.register(exceptionMapping);\n    }\n    complexMappings(registry);\n  }\n\n  protected List<ExceptionMapping> initialMappings() {\n    return Arrays.asList(\n        new BeanExceptionMapping(\"java.lang.Throwable\", asList(\"message\"), handler),\n        new BeanExceptionMapping(\n            \"java.nio.file.FileSystemException\", asList(\"file\", \"otherFile\", \"reason\"), handler),\n        new BeanExceptionMapping(\n            \"java.net.HttpRetryException\", asList(\"responseCode\", \"reason\", \"location\"), handler),\n        new BeanExceptionMapping(\n            \"java.net.URISyntaxException\", asList(\"input\", \"reason\", \"index\"), handler),\n        new BeanExceptionMapping(\n            \"java.nio.charset.IllegalCharsetNameException\", asList(\"charsetName\"), handler),\n        new BeanExceptionMapping(\"java.sql.BatchUpdateException\", asList(\"updateCounts\"), handler),\n        new BeanExceptionMapping(\"java.sql.SQLException\", asList(\"errorCode\", \"SQLState\"), handler),\n        new BeanExceptionMapping(\"java.text.ParseException\", asList(\"errorOffset\"), handler),\n        new BeanExceptionMapping(\n            \"java.time.format.DateTimeParseException\",\n            asList(\"parsedString\", \"errorIndex\"),\n            handler),\n        new BeanExceptionMapping(\n            \"java.util.DuplicateFormatFlagsException\", asList(\"flags\"), handler),\n        new BeanExceptionMapping(\n            \"java.util.FormatFlagsConversionMismatchException\",\n            asList(\"flags\", \"conversion\"),\n            handler),\n        new BeanExceptionMapping(\n            \"java.util.IllegalFormatCodePointException\", asList(\"codePoint\"), handler),\n        new BeanExceptionMapping(\n            \"java.util.IllegalFormatConversionException\",\n            asList(\"conversion\", \"argumentClass\"),\n            handler),\n        new BeanExceptionMapping(\"java.util.IllegalFormatFlagsException\", asList(\"flags\"), handler),\n        new BeanExceptionMapping(\n            \"java.util.IllegalFormatPrecisionException\", asList(\"precision\"), handler),\n        new BeanExceptionMapping(\"java.util.IllegalFormatWidthException\", asList(\"width\"), handler),\n        new BeanExceptionMapping(\n            \"java.util.IllformedLocaleException\", asList(\"errorIndex\"), handler),\n        new BeanExceptionMapping(\n            \"java.util.InvalidPropertiesFormatException\", asList(\"formatSpecifier\"), handler),\n        new BeanExceptionMapping(\n            \"java.util.MissingFormatArgumentException\", asList(\"formatSpecifier\"), handler),\n        new BeanExceptionMapping(\n            \"java.util.MissingFormatWidthException\", asList(\"formatSpecifier\"), handler),\n        new BeanExceptionMapping(\n            \"java.util.MissingResourceException\", asList(\"className\", \"key\"), handler),\n        new BeanExceptionMapping(\n            \"java.util.UnknownFormatConversionException\", asList(\"conversion\"), handler),\n        new BeanExceptionMapping(\"java.util.UnknownFormatFlagsException\", asList(\"flags\"), handler),\n        new BeanExceptionMapping(\n            \"javax.naming.NamingException\",\n            asList(\"explanation\", \"remainingName\", \"resolvedName\"),\n            handler));\n  }\n\n  protected void complexMappings(ExceptionMappingRegistry mappings) {\n    mappings.register(\n        EventException.class, (e -> singletonList(ExceptionProperty.create(\"code\", e.code))));\n    mappings.register(\n        DOMException.class, (e -> singletonList(ExceptionProperty.create(\"code\", e.code))));\n\n    mappings.register(\n        XMLStreamException.class,\n        e -> {\n          Location l = e.getLocation();\n          if (l == null) {\n            return Collections.emptyList();\n          }\n          return asList(\n              ExceptionProperty.create(\"lineNumber\", l.getLineNumber()),\n              ExceptionProperty.create(\"columnNumber\", l.getColumnNumber()),\n              ExceptionProperty.create(\"systemId\", l.getSystemId()),\n              ExceptionProperty.create(\"publicId\", l.getPublicId()),\n              ExceptionProperty.create(\"characterOffset\", l.getCharacterOffset()));\n        });\n\n    mappings.register(\n        InterruptedIOException.class,\n        e -> singletonList(ExceptionProperty.create(\"bytesTransferred\", e.bytesTransferred)));\n  }\n\n  /**\n   * Once the children elements are also parsed, now is the time to activate the appender options.\n   */\n  public void end(InterpretationContext ec, String name) {\n    if (inError) {\n      return;\n    }\n\n    Object o = ec.peekObject();\n\n    if (o != mappingsRegistry) {\n      addWarn(\n          \"The object at the end of the stack is not the mappingsRegistry named [\"\n              + mappingsRegistry\n              + \"] pushed earlier.\");\n    } else {\n      ec.popObject();\n    }\n  }\n}\n"
  },
  {
    "path": "logback-exception-mapping/src/main/java/com/tersesystems/logback/exceptionmapping/ExceptionMessageWithMappingsConverter.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.exceptionmapping;\n\nimport static com.tersesystems.logback.exceptionmapping.Constants.*;\n\nimport ch.qos.logback.classic.spi.IThrowableProxy;\nimport ch.qos.logback.classic.spi.ThrowableProxy;\nimport com.tersesystems.logback.classic.ExceptionMessageConverter;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.stream.Collectors;\n\npublic class ExceptionMessageWithMappingsConverter extends ExceptionMessageConverter {\n\n  @Override\n  protected String constructMessage(IThrowableProxy ex) {\n    String className = ex.getClassName();\n    String arguments = findArgumentMappings(ex);\n    return String.format(\"%s(%s)\", className, arguments);\n  }\n\n  private String getMappingsKey() {\n    return DEFAULT_MAPPINGS_KEY;\n  }\n\n  private String findArgumentMappings(IThrowableProxy ex) {\n    if (ex instanceof ThrowableProxy) {\n      ExceptionMappingRegistry registry = getRegistry();\n      if (registry == null) {\n        return \"\";\n      }\n      Throwable throwable = ((ThrowableProxy) ex).getThrowable();\n      return format(registry.apply(throwable));\n    } else {\n      return \"\";\n    }\n  }\n\n  private String format(List<ExceptionProperty> args) {\n    return args.stream()\n        .map(\n            arg -> {\n              StringBuilder sb = new StringBuilder();\n              StringBufferExceptionPropertyWriter exceptionPropertyWriter =\n                  new StringBufferExceptionPropertyWriter(sb);\n              exceptionPropertyWriter.write(arg);\n              return sb.toString();\n            })\n        .collect(Collectors.joining(\" \"));\n  }\n\n  @SuppressWarnings(\"unchecked\")\n  private ExceptionMappingRegistry getRegistry() {\n    String key = getMappingsKey();\n\n    Map<String, ExceptionMappingRegistry> mappingsBag =\n        (Map<String, ExceptionMappingRegistry>) getContext().getObject(REGISTRY_BAG);\n    if (mappingsBag == null) {\n      addError(\"No mappingsRegistry bag found for converter!\");\n      return null;\n    }\n\n    ExceptionMappingRegistry exceptionMappingRegistry = mappingsBag.get(key);\n    if (exceptionMappingRegistry == null) {\n      addError(\"No mappingsRegistry found for converter for key \" + key);\n    }\n    return exceptionMappingRegistry;\n  }\n\n  class StringBufferExceptionPropertyWriter {\n    private final StringBuilder sb;\n\n    StringBufferExceptionPropertyWriter(StringBuilder sb) {\n      this.sb = sb;\n    }\n\n    public void write(ExceptionProperty exceptionProperty) {\n      if (exceptionProperty instanceof KeyValueExceptionProperty) {\n        KeyValueExceptionProperty kv = (KeyValueExceptionProperty) exceptionProperty;\n        sb.append(kv.getKey());\n        sb.append(\"=\");\n        sb.append(\"\\\"\");\n        sb.append(kv.getValue());\n        sb.append(\"\\\"\");\n      }\n    }\n  }\n  ;\n}\n"
  },
  {
    "path": "logback-exception-mapping/src/main/java/com/tersesystems/logback/exceptionmapping/ExceptionProperty.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.exceptionmapping;\n\nimport java.util.Arrays;\n\npublic interface ExceptionProperty {\n\n  public static ExceptionProperty create(String name, String value) {\n    return new KeyValueExceptionProperty(name, value);\n  }\n\n  public static ExceptionProperty create(String name, Object value) {\n    return new KeyValueExceptionProperty(name, toString(value));\n  }\n\n  static String toString(Object value) {\n    if (value instanceof boolean[]) return Arrays.toString((boolean[]) value);\n    if (value instanceof byte[]) return Arrays.toString((byte[]) value);\n    if (value instanceof short[]) return Arrays.toString((short[]) value);\n    if (value instanceof char[]) return Arrays.toString((char[]) value);\n    if (value instanceof int[]) return Arrays.toString((int[]) value);\n    if (value instanceof long[]) return Arrays.toString((long[]) value);\n    if (value instanceof float[]) return Arrays.toString((float[]) value);\n    if (value instanceof double[]) return Arrays.toString((double[]) value);\n    if (value instanceof Object[]) return Arrays.deepToString((Object[]) value);\n    if (value == null) {\n      return \"null\";\n    }\n    return value.toString();\n  }\n}\n"
  },
  {
    "path": "logback-exception-mapping/src/main/java/com/tersesystems/logback/exceptionmapping/FunctionExceptionMapping.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.exceptionmapping;\n\nimport java.util.List;\nimport java.util.function.Function;\n\npublic class FunctionExceptionMapping implements ExceptionMapping {\n  private final Function<Throwable, List<ExceptionProperty>> function;\n  private final String name;\n\n  public FunctionExceptionMapping(String name, Function<Throwable, List<ExceptionProperty>> f) {\n    this.name = name;\n    this.function = f;\n  }\n\n  @Override\n  public List<ExceptionProperty> apply(Throwable e) {\n    return function.apply(e);\n  }\n\n  @Override\n  public String getName() {\n    return name;\n  }\n}\n"
  },
  {
    "path": "logback-exception-mapping/src/main/java/com/tersesystems/logback/exceptionmapping/KeyValueExceptionProperty.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.exceptionmapping;\n\nimport java.util.Objects;\n\npublic class KeyValueExceptionProperty implements ExceptionProperty {\n\n  private final String key;\n  private final String value;\n\n  KeyValueExceptionProperty(String key, String value) {\n    this.key = key;\n    this.value = value;\n  }\n\n  public String getKey() {\n    return key;\n  }\n\n  public String getValue() {\n    return value;\n  }\n\n  @Override\n  public boolean equals(Object o) {\n    if (this == o) return true;\n    if (o == null || getClass() != o.getClass()) return false;\n    KeyValueExceptionProperty that = (KeyValueExceptionProperty) o;\n    return key.equals(that.key) && value.equals(that.value);\n  }\n\n  @Override\n  public int hashCode() {\n    return Objects.hash(key, value);\n  }\n\n  @Override\n  public String toString() {\n    return String.format(\"KeyValueExceptionProperty(key=%s, value=%s)\", key, getValue());\n  }\n}\n"
  },
  {
    "path": "logback-exception-mapping/src/test/java/com/tersesystems/logback/exceptionmapping/ExceptionMappingTest.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.exceptionmapping;\n\nimport static java.util.Arrays.asList;\nimport static java.util.Collections.singletonList;\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport java.io.InterruptedIOException;\nimport java.sql.BatchUpdateException;\nimport java.sql.SQLException;\nimport java.util.*;\nimport java.util.function.Consumer;\nimport javax.xml.stream.Location;\nimport javax.xml.stream.XMLStreamException;\nimport org.junit.Test;\nimport org.w3c.dom.DOMException;\nimport org.w3c.dom.events.EventException;\n\npublic class ExceptionMappingTest {\n\n  @Test\n  public void testSimpleArgument() {\n    Consumer<Exception> reporter = Throwable::printStackTrace;\n    DefaultExceptionMappingRegistry mappings = new DefaultExceptionMappingRegistry(reporter);\n    populate(mappings);\n\n    Exception ex = new EventException(EventException.UNSPECIFIED_EVENT_TYPE_ERR, \"unspecified\");\n    List<ExceptionProperty> args = mappings.apply(ex);\n\n    assertThat(args.get(0))\n        .isEqualTo(ExceptionProperty.create(\"code\", EventException.UNSPECIFIED_EVENT_TYPE_ERR));\n  }\n\n  @Test\n  public void testComplexArgument() {\n    Consumer<Exception> reporter = Throwable::printStackTrace;\n    DefaultExceptionMappingRegistry mappings = new DefaultExceptionMappingRegistry(reporter);\n    populate(mappings);\n\n    String reason = \"felt like it\";\n    String SQLState = \"SELECT reason FROM possible_excuses\";\n    int vendorCode = 1337;\n    int[] updateCounts = {1};\n    Throwable cause = new SQLException(\"cause of exception\");\n\n    Exception ex = new BatchUpdateException(reason, SQLState, vendorCode, updateCounts, cause);\n    List<ExceptionProperty> args = mappings.apply(ex);\n\n    // combination of BatchUpdateException + SQLException arguments\n    assertThat(args.get(0)).isEqualTo(ExceptionProperty.create(\"updateCounts\", updateCounts));\n    assertThat(args.get(1)).isEqualTo(ExceptionProperty.create(\"errorCode\", 1337));\n    assertThat(args.get(2)).isEqualTo(ExceptionProperty.create(\"SQLState\", SQLState));\n  }\n\n  private void populate(DefaultExceptionMappingRegistry mappings) {\n    mappings.register(BatchUpdateException.class, \"updateCounts\");\n    mappings.register(SQLException.class, \"errorCode\", \"SQLState\");\n    mappings.register(\n        EventException.class, (e -> singletonList(ExceptionProperty.create(\"code\", e.code))));\n    mappings.register(\n        XMLStreamException.class,\n        e -> {\n          Location l = e.getLocation();\n          if (l == null) {\n            return Collections.emptyList();\n          }\n          return asList(\n              ExceptionProperty.create(\"lineNumber\", l.getLineNumber()),\n              ExceptionProperty.create(\"columnNumber\", l.getColumnNumber()),\n              ExceptionProperty.create(\"systemId\", l.getSystemId()),\n              ExceptionProperty.create(\"publicId\", l.getPublicId()),\n              ExceptionProperty.create(\"characterOffset\", l.getCharacterOffset()));\n        });\n    mappings.register(\n        InterruptedIOException.class,\n        e -> singletonList(ExceptionProperty.create(\"bytesTransferred\", e.bytesTransferred)));\n    mappings.register(\n        DOMException.class, (e -> singletonList(ExceptionProperty.create(\"code\", e.code))));\n  }\n}\n"
  },
  {
    "path": "logback-exception-mapping/src/test/java/com/tersesystems/logback/exceptionmapping/MyCustomException.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.exceptionmapping;\n\npublic class MyCustomException extends RuntimeException {\n  private final String one;\n  private final String two;\n  private final String three;\n\n  public MyCustomException(String message, String one, String two, String three, Throwable cause) {\n    super(message, cause);\n    this.one = one;\n    this.two = two;\n    this.three = three;\n  }\n\n  public String getOne() {\n    return one;\n  }\n\n  public String getTwo() {\n    return two;\n  }\n\n  public String getThree() {\n    return three;\n  }\n}\n"
  },
  {
    "path": "logback-exception-mapping/src/test/java/com/tersesystems/logback/exceptionmapping/Thrower.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.exceptionmapping;\n\nimport java.sql.BatchUpdateException;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\npublic class Thrower {\n  private static final Logger logger = LoggerFactory.getLogger(Thrower.class);\n\n  public static void main(String[] progArgs) {\n    try {\n      doSomethingExceptional();\n    } catch (RuntimeException e) {\n      logger.error(\"domain specific message\", e);\n    }\n  }\n\n  static void doSomethingExceptional() {\n    Throwable cause = new BatchUpdateException();\n    throw new MyCustomException(\n        \"This is my message\",\n        \"one is one\",\n        \"two is more than one\",\n        \"three is more than two and one\",\n        cause);\n  }\n}\n"
  },
  {
    "path": "logback-exception-mapping/src/test/resources/logback-test.xml",
    "content": "<!--\n  ~ SPDX-License-Identifier: CC0-1.0\n  ~\n  ~ Copyright 2018-2020 Will Sargent.\n  ~\n  ~ Licensed under the CC0 Public Domain Dedication;\n  ~ You may obtain a copy of the License at\n  ~\n  ~  http://creativecommons.org/publicdomain/zero/1.0/\n  -->\n<configuration>\n\n  <newRule pattern=\"*/exceptionMappings\"\n           actionClass=\"com.tersesystems.logback.exceptionmapping.ExceptionMappingRegistryAction\"/>\n\n  <newRule pattern=\"*/exceptionMappings/mapping\"\n           actionClass=\"com.tersesystems.logback.exceptionmapping.ExceptionMappingAction\"/>\n\n  <conversionRule conversionWord=\"richex\" converterClass=\"com.tersesystems.logback.exceptionmapping.ExceptionMessageWithMappingsConverter\" />\n\n  <exceptionMappings>\n    <mapping name=\"com.tersesystems.logback.exceptionmapping.MyCustomException\" properties=\"one,two,three\"/>\n  </exceptionMappings>\n\n  <appender name=\"CONSOLE\" class=\"ch.qos.logback.core.ConsoleAppender\">\n    <encoder>\n      <pattern>%-5relative %-5level %logger{35} - %msg%richex{1, 10, exception=[}%n</pattern>\n    </encoder>\n  </appender>\n\n  <root level=\"TRACE\">\n    <appender-ref ref=\"CONSOLE\"/>\n  </root>\n\n</configuration>"
  },
  {
    "path": "logback-exception-mapping-providers/gradle.properties",
    "content": "#\n# SPDX-License-Identifier: CC0-1.0\n#  \n# Copyright 2018-2020 Will Sargent.\n#\n# Licensed under the CC0 Public Domain Dedication;\n# You may obtain a copy of the License at\n#\n#  http://creativecommons.org/publicdomain/zero/1.0/\n#\n\nproject_description = Extra exception mapping providers"
  },
  {
    "path": "logback-exception-mapping-providers/logback-exception-mapping-providers.gradle",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *  \n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\ndependencies {\n    implementation project(':logback-exception-mapping')\n    implementation project(':logback-typesafe-config')\n\n    implementation \"net.logstash.logback:logstash-logback-encoder:$logstashVersion\"\n}"
  },
  {
    "path": "logback-exception-mapping-providers/src/main/java/com/tersesystems/logback/exceptionmapping/config/TypesafeConfigMappingsAction.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.exceptionmapping.config;\n\nimport ch.qos.logback.core.joran.action.Action;\nimport ch.qos.logback.core.joran.spi.ActionException;\nimport ch.qos.logback.core.joran.spi.InterpretationContext;\nimport com.tersesystems.logback.exceptionmapping.ExceptionMappingRegistry;\nimport com.tersesystems.logback.typesafeconfig.ConfigConstants;\nimport com.typesafe.config.Config;\nimport com.typesafe.config.ConfigException;\nimport com.typesafe.config.ConfigValue;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.stream.Collectors;\nimport org.xml.sax.Attributes;\n\npublic class TypesafeConfigMappingsAction extends Action {\n\n  @SuppressWarnings(\"unchecked\")\n  private ExceptionMappingRegistry getRegistry(InterpretationContext ic) {\n    Object obj = ic.peekObject();\n    if (obj == null) {\n      addError(\"Not in an exception registry\");\n      return null;\n    }\n\n    if (obj instanceof ExceptionMappingRegistry) {\n      return (ExceptionMappingRegistry) obj;\n    }\n    addError(\"Parent type is not an exception mapping registry!\");\n    return null;\n  }\n\n  @Override\n  public void begin(InterpretationContext ic, String name, Attributes attributes)\n      throws ActionException {\n    ExceptionMappingRegistry registry = getRegistry(ic);\n    if (registry == null) {\n      addError(\"Required exception registry is missing!\");\n      return;\n    }\n\n    Config config = getConfig(ic);\n    if (config == null) {\n      addError(\"Required typesafe config is missing!\");\n      return;\n    }\n    String mappingsPath = attributes.getValue(\"path\");\n    if (mappingsPath == null) {\n      addError(\"Required attribute 'path' is missing!\");\n      return;\n    }\n\n    try {\n      Map<String, List<String>> mappings = getMappingsFromConfig(config, mappingsPath);\n      registry.register(mappings);\n    } catch (ConfigException e) {\n      addError(\"Could not resolve configuration using path \" + mappingsPath, e);\n    }\n  }\n\n  private Config getConfig(InterpretationContext ic) {\n    Object obj = ic.getObjectMap().get(ConfigConstants.TYPESAFE_CONFIG_CTX_KEY);\n    if (obj == null) {\n      return null;\n    }\n    if (obj instanceof Config) {\n      return (Config) obj;\n    }\n    addError(\"Type is not a Config!\");\n    return null;\n  }\n\n  @Override\n  public void end(InterpretationContext ic, String name) throws ActionException {}\n\n  public Map<String, List<String>> getMappingsFromConfig(Config config, String mappingsPath) {\n    Config mappingsConfig = config.getConfig(mappingsPath);\n    return mappingsConfig.entrySet().stream()\n        .collect(\n            Collectors.toMap(\n                Map.Entry::getKey,\n                (Map.Entry<String, ConfigValue> e) -> mappingsConfig.getStringList(e.getKey())));\n  }\n}\n"
  },
  {
    "path": "logback-exception-mapping-providers/src/main/java/com/tersesystems/logback/exceptionmapping/json/ExceptionArgumentsProvider.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.exceptionmapping.json;\n\nimport static com.tersesystems.logback.exceptionmapping.Constants.REGISTRY_BAG;\n\nimport ch.qos.logback.classic.spi.ILoggingEvent;\nimport ch.qos.logback.classic.spi.IThrowableProxy;\nimport ch.qos.logback.classic.spi.ThrowableProxy;\nimport com.fasterxml.jackson.core.JsonGenerator;\nimport com.tersesystems.logback.exceptionmapping.*;\nimport java.io.IOException;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport net.logstash.logback.composite.AbstractFieldJsonProvider;\nimport net.logstash.logback.composite.JsonWritingUtils;\n\npublic class ExceptionArgumentsProvider extends AbstractFieldJsonProvider<ILoggingEvent> {\n\n  @SuppressWarnings(\"unchecked\")\n  private ExceptionMappingRegistry getRegistry() {\n    final String key = Constants.DEFAULT_MAPPINGS_KEY;\n\n    final Map<String, ExceptionMappingRegistry> mappingsBag =\n        (Map<String, ExceptionMappingRegistry>) getContext().getObject(REGISTRY_BAG);\n\n    if (mappingsBag == null) {\n      addError(\"No mappingsRegistry bag found for converter!\");\n      return null;\n    }\n\n    ExceptionMappingRegistry exceptionMappingRegistry = mappingsBag.get(key);\n    if (exceptionMappingRegistry == null) {\n      addError(\"No mappingsRegistry found for converter for key \" + key);\n    }\n    return exceptionMappingRegistry;\n  }\n\n  @Override\n  public void writeTo(JsonGenerator generator, ILoggingEvent event) throws IOException {\n    writeExceptionIfNecessary(generator, event.getThrowableProxy());\n  }\n\n  private void writeExceptionIfNecessary(JsonGenerator generator, IThrowableProxy throwableProxy)\n      throws IOException {\n    if (throwableProxy instanceof ThrowableProxy) {\n      ExceptionMappingRegistry registry = getRegistry();\n      if (registry == null) {\n        addError(\"No registry found!\");\n        return;\n      }\n      ThrowableProxy proxy = (ThrowableProxy) throwableProxy;\n      Throwable throwable = proxy.getThrowable();\n      if (getFieldName() != null) {\n        generator.writeArrayFieldStart(getFieldName());\n      }\n\n      ExceptionCauseIterator.create(throwable).stream()\n          .forEach(\n              t -> {\n                try {\n                  renderException(generator, registry, t);\n                } catch (IOException e) {\n                  addError(\"Cannot render exception\", e);\n                }\n              });\n      if (getFieldName() != null) {\n        generator.writeEndArray();\n      }\n    }\n  }\n\n  private void renderException(\n      JsonGenerator generator, ExceptionMappingRegistry registry, Throwable throwable)\n      throws IOException {\n    Map<String, String> propertyMap = new HashMap<>();\n    List<ExceptionProperty> properties = registry.apply(throwable);\n\n    for (ExceptionProperty property : properties) {\n      if (property instanceof KeyValueExceptionProperty) {\n        KeyValueExceptionProperty p = ((KeyValueExceptionProperty) property);\n        propertyMap.put(p.getKey(), p.getValue());\n      }\n    }\n\n    generator.writeStartObject();\n    JsonWritingUtils.writeStringField(generator, \"name\", throwable.getClass().getName());\n    JsonWritingUtils.writeMapStringFields(generator, \"properties\", propertyMap);\n    generator.writeEndObject();\n  }\n}\n"
  },
  {
    "path": "logback-exception-mapping-providers/src/test/java/com/tersesystems/logback/exceptionmapping/json/ExceptionArgumentsProviderTest.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.exceptionmapping.json;\n\nimport static com.tersesystems.logback.exceptionmapping.Constants.DEFAULT_MAPPINGS_KEY;\nimport static com.tersesystems.logback.exceptionmapping.Constants.REGISTRY_BAG;\nimport static java.util.Collections.singletonMap;\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport ch.qos.logback.classic.Level;\nimport ch.qos.logback.classic.LoggerContext;\nimport ch.qos.logback.classic.spi.ILoggingEvent;\nimport ch.qos.logback.classic.spi.LoggingEvent;\nimport ch.qos.logback.core.status.OnConsoleStatusListener;\nimport com.fasterxml.jackson.core.JsonGenerator;\nimport com.fasterxml.jackson.databind.MappingJsonFactory;\nimport com.tersesystems.logback.exceptionmapping.DefaultExceptionMappingRegistry;\nimport com.tersesystems.logback.exceptionmapping.ExceptionMappingRegistry;\nimport java.io.IOException;\nimport java.io.StringWriter;\nimport java.util.function.Consumer;\nimport org.junit.Test;\n\npublic class ExceptionArgumentsProviderTest {\n\n  @Test\n  public void testProvider() throws IOException {\n    LoggerContext context = new LoggerContext();\n    context.getStatusManager().add(new OnConsoleStatusListener());\n    createExceptionMappingRegistry(context);\n\n    StringWriter writer = new StringWriter();\n    JsonGenerator g = mkJsonGenerator(writer);\n\n    ExceptionArgumentsProvider provider = new ExceptionArgumentsProvider();\n    provider.setContext(context);\n    provider.setFieldName(\"exception\");\n    provider.start();\n\n    ILoggingEvent event = mkLoggingEvent(context);\n\n    g.writeStartObject();\n    provider.writeTo(g, event);\n    g.writeEndObject();\n    g.flush();\n    g.close();\n\n    String s = writer.toString();\n    assertThat(s)\n        .isEqualTo(\n            \"{\\\"exception\\\":[{\\\"name\\\":\\\"java.lang.RuntimeException\\\",\\\"properties\\\":{\\\"message\\\":\\\"derp\\\"}}]}\");\n  }\n\n  private void createExceptionMappingRegistry(LoggerContext context) {\n    Consumer<Exception> handler = Throwable::printStackTrace;\n    ExceptionMappingRegistry registry = new DefaultExceptionMappingRegistry(handler);\n    registry.register(Throwable.class.getName(), \"message\");\n    context.putObject(REGISTRY_BAG, singletonMap(DEFAULT_MAPPINGS_KEY, registry));\n  }\n\n  private ILoggingEvent mkLoggingEvent(LoggerContext context) {\n    Exception ex = new RuntimeException(\"derp\");\n    return new LoggingEvent(\"fcqn\", context.getLogger(\"fcqn\"), Level.INFO, \"info\", ex, null);\n  }\n\n  private JsonGenerator mkJsonGenerator(StringWriter writer) throws IOException {\n    MappingJsonFactory jsonFactory = new MappingJsonFactory();\n    JsonGenerator g = jsonFactory.createGenerator(writer);\n    g.enable(JsonGenerator.Feature.STRICT_DUPLICATE_DETECTION);\n    return g;\n  }\n}\n"
  },
  {
    "path": "logback-exception-mapping-providers/src/test/java/com/tersesystems/logback/exceptionmapping/json/MySpecialException.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.exceptionmapping.json;\n\nimport java.time.Instant;\n\npublic class MySpecialException extends Exception {\n\n  private final Instant timestamp;\n\n  public MySpecialException(String message, Instant timestamp) {\n    super(message);\n    this.timestamp = timestamp;\n  }\n\n  public MySpecialException(String message, Instant timestamp, Throwable cause) {\n    super(message, cause);\n    this.timestamp = timestamp;\n  }\n\n  public Instant getTimestamp() {\n    return timestamp;\n  }\n}\n"
  },
  {
    "path": "logback-exception-mapping-providers/src/test/java/com/tersesystems/logback/exceptionmapping/json/TypesafeConfigMappingsActionTest.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.exceptionmapping.json;\n\nimport static com.tersesystems.logback.exceptionmapping.Constants.DEFAULT_MAPPINGS_KEY;\nimport static com.tersesystems.logback.exceptionmapping.Constants.REGISTRY_BAG;\nimport static java.util.Objects.requireNonNull;\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport ch.qos.logback.classic.LoggerContext;\nimport ch.qos.logback.classic.joran.JoranConfigurator;\nimport ch.qos.logback.core.joran.spi.JoranException;\nimport com.tersesystems.logback.exceptionmapping.ExceptionMappingRegistry;\nimport java.util.Map;\nimport org.junit.Before;\nimport org.junit.Test;\n\npublic class TypesafeConfigMappingsActionTest {\n\n  private final JoranConfigurator jc = new JoranConfigurator();\n  private final LoggerContext loggerContext = new LoggerContext();\n\n  @Before\n  public void setUp() {\n    jc.setContext(loggerContext);\n  }\n\n  @Test\n  public void testConfig() throws JoranException {\n    jc.doConfigure(\n        requireNonNull(\n            this.getClass().getClassLoader().getResource(\"logback-with-exception-mapping.xml\")));\n\n    Map<String, ExceptionMappingRegistry> registryMap =\n        (Map<String, ExceptionMappingRegistry>) loggerContext.getObject(REGISTRY_BAG);\n    ExceptionMappingRegistry registry = registryMap.get(DEFAULT_MAPPINGS_KEY);\n    assertThat(\n            registry.contains(\"com.tersesystems.logback.exceptionmapping.json.MySpecialException\"))\n        .isTrue();\n  }\n}\n"
  },
  {
    "path": "logback-exception-mapping-providers/src/test/resources/logback-with-exception-mapping.xml",
    "content": "<!--\n  ~ SPDX-License-Identifier: CC0-1.0\n  ~\n  ~ Copyright 2018-2020 Will Sargent.\n  ~\n  ~ Licensed under the CC0 Public Domain Dedication;\n  ~ You may obtain a copy of the License at\n  ~\n  ~  http://creativecommons.org/publicdomain/zero/1.0/\n  -->\n<configuration>\n\n    <newRule pattern=\"configuration/typesafeConfig\"\n             actionClass=\"com.tersesystems.logback.typesafeconfig.TypesafeConfigAction\"/>\n\n    <newRule pattern=\"*/exceptionMappings\"\n             actionClass=\"com.tersesystems.logback.exceptionmapping.ExceptionMappingRegistryAction\"/>\n\n    <newRule pattern=\"*/exceptionMappings/configMappings\"\n             actionClass=\"com.tersesystems.logback.exceptionmapping.config.TypesafeConfigMappingsAction\"/>\n\n    <typesafeConfig>\n\n    </typesafeConfig>\n\n    <exceptionMappings>\n        <configMappings path=\"exceptionmappings\"/>\n    </exceptionMappings>\n\n    <appender name=\"TEST\" class=\"ch.qos.logback.core.ConsoleAppender\">\n        <encoder>\n            <pattern>[%-5level] %logger{15} - %msg%n%xException{10}</pattern>\n        </encoder>\n    </appender>\n\n    <root>\n        <appender-ref ref=\"TEST\"/>\n    </root>\n</configuration>"
  },
  {
    "path": "logback-exception-mapping-providers/src/test/resources/logback.conf",
    "content": "levels {\n  root = INFO\n}\n\n\nexceptionmappings {\n  com.tersesystems.logback.exceptionmapping.json.MySpecialException: [\"timestamp\"]\n}"
  },
  {
    "path": "logback-honeycomb-appender/gradle.properties",
    "content": "#\n# SPDX-License-Identifier: CC0-1.0\n#\n# Copyright 2018-2020 Will Sargent.\n#\n# Licensed under the CC0 Public Domain Dedication;\n# You may obtain a copy of the License at\n#\n#  http://creativecommons.org/publicdomain/zero/1.0/\n#\n\nproject_description = Logback Honeycomb Appender"
  },
  {
    "path": "logback-honeycomb-appender/logback-honeycomb-appender.gradle",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\nplugins {\n    id 'java-library'\n}\n\ndependencies {\n    api project(\":logback-honeycomb-client\")\n\n    implementation project(\":logback-classic\")\n    implementation \"net.logstash.logback:logstash-logback-encoder:$logstashVersion\"\n}\n"
  },
  {
    "path": "logback-honeycomb-appender/src/main/java/com/tersesystems/logback/honeycomb/HoneycombAppender.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.honeycomb;\n\nimport ch.qos.logback.classic.spi.ILoggingEvent;\nimport ch.qos.logback.core.UnsynchronizedAppenderBase;\nimport ch.qos.logback.core.encoder.Encoder;\nimport com.tersesystems.logback.classic.StartTime;\nimport com.tersesystems.logback.honeycomb.client.*;\nimport java.time.Instant;\nimport java.util.*;\nimport java.util.concurrent.ArrayBlockingQueue;\nimport java.util.concurrent.BlockingQueue;\nimport java.util.concurrent.CompletionStage;\nimport java.util.concurrent.ExecutionException;\nimport java.util.stream.StreamSupport;\n\n/** Creates an appender that sends data to Honeycomb. */\npublic class HoneycombAppender extends UnsynchronizedAppenderBase<ILoggingEvent> {\n\n  private String dataSet;\n  private String apiKey;\n  private Encoder<ILoggingEvent> encoder;\n  private Integer sampleRate = 1;\n  private Integer queueSize = 50;\n  private BlockingQueue<HoneycombRequest<ILoggingEvent>> eventQueue;\n  private boolean batch = true;\n  private boolean includeCallerData = false;\n\n  private HoneycombClient<ILoggingEvent> honeycombClient;\n\n  public Encoder<ILoggingEvent> getEncoder() {\n    return encoder;\n  }\n\n  public void setQueueSize(Integer queueSize) {\n    this.queueSize = queueSize;\n  }\n\n  public void setDataSet(String dataSet) {\n    this.dataSet = dataSet;\n  }\n\n  public void setApiKey(String apiKey) {\n    this.apiKey = apiKey;\n  }\n\n  public void setEncoder(Encoder<ILoggingEvent> encoder) {\n    this.encoder = encoder;\n  }\n\n  public void setSampleRate(Integer sampleRate) {\n    this.sampleRate = sampleRate;\n  }\n\n  public void setBatch(boolean batch) {\n    this.batch = batch;\n  }\n\n  public boolean isIncludeCallerData() {\n    return includeCallerData;\n  }\n\n  public void setIncludeCallerData(boolean includeCallerData) {\n    this.includeCallerData = includeCallerData;\n  }\n\n  protected void prepareForDeferredProcessing(ILoggingEvent event) {\n    event.prepareForDeferredProcessing();\n    if (includeCallerData) {\n      event.getCallerData();\n    }\n  }\n\n  @Override\n  public void start() {\n    boolean errorsFound = false;\n    if (encoder == null) {\n      addError(\"No encoder found!\");\n      errorsFound = true;\n    }\n\n    if (apiKey == null) {\n      addError(\"No apiKey found!\");\n      errorsFound = true;\n    }\n\n    if (dataSet == null) {\n      addError(\"No dataSet found!\");\n      errorsFound = true;\n    }\n\n    if (errorsFound) {\n      return;\n    }\n\n    try {\n      HoneycombClientService honeycombClientService = clientService();\n      honeycombClient = honeycombClientService.newClient(apiKey, dataSet, this::serialize);\n      if (batch) {\n        eventQueue = new ArrayBlockingQueue<>(queueSize);\n      }\n      super.start();\n    } catch (Exception e) {\n      addError(\"Cannot start appender!\", e);\n    }\n  }\n\n  @Override\n  public void stop() {\n    if (started && batch) {\n      dumpQueue();\n    }\n\n    if (honeycombClient != null) {\n      try {\n        honeycombClient.close();\n      } finally {\n        honeycombClient = null;\n      }\n    }\n\n    super.stop();\n  }\n\n  protected void dumpQueue() {\n    try {\n      // Post and then block until we get a response\n      // Probably overkill, but we're shutting down in any case.\n      if (!eventQueue.isEmpty()) {\n        List<HoneycombRequest<ILoggingEvent>> list = new ArrayList<>();\n        eventQueue.drainTo(list);\n        postBatch(list).toCompletableFuture().get();\n      }\n    } catch (InterruptedException | ExecutionException e) {\n      addError(\"drainQueue: Cannot generate JSON\", e);\n    }\n  }\n\n  @Override\n  protected void append(ILoggingEvent eventObject) {\n    try {\n      prepareForDeferredProcessing(eventObject);\n    } catch (RuntimeException e) {\n      addWarn(\n          \"Unable to prepare event for deferred processing.  Event output might be missing data.\",\n          e);\n    }\n\n    Instant startTime = StartTime.from(context, eventObject);\n    HoneycombRequest<ILoggingEvent> request =\n        new HoneycombRequest<>(sampleRate, startTime, eventObject);\n\n    if (batch) {\n      // If queue is full, then drain and post it.\n      if (!eventQueue.offer(request)) {\n        List<HoneycombRequest<ILoggingEvent>> list = new ArrayList<>();\n\n        // empty the queue\n        eventQueue.drainTo(list);\n\n        // put one back...\n        eventQueue.offer(request);\n\n        // post the contents.\n        postBatch(list);\n      }\n    } else {\n      postEvent(request);\n    }\n  }\n\n  private CompletionStage<Void> postEvent(HoneycombRequest<ILoggingEvent> honeycombRequest) {\n    return honeycombClient.post(honeycombRequest).thenAccept(this::accept);\n  }\n\n  private CompletionStage<Void> postBatch(\n      Iterable<HoneycombRequest<ILoggingEvent>> honeycombRequests) {\n    return honeycombClient.postBatch(honeycombRequests).thenAccept(this::accept);\n  }\n\n  private byte[] serialize(HoneycombRequest<ILoggingEvent> honeycombRequest) {\n    return encoder.encode(honeycombRequest.getEvent());\n  }\n\n  private HoneycombClientService clientService() {\n    ServiceLoader<HoneycombClientService> loader = ServiceLoader.load(HoneycombClientService.class);\n    Optional<HoneycombClientService> first =\n        StreamSupport.stream(loader.spliterator(), false).findFirst();\n    if (first.isPresent()) {\n      return first.get();\n    }\n    throw new IllegalStateException(\"No service found -- do you have a library loaded?\");\n  }\n\n  private void accept(HoneycombResponse response) {\n    if (!response.isSuccess()) {\n      if (response.isRateLimited()) {\n        addInfo(\"postEvent: Rate Limited: \" + response.getReason());\n      } else if (response.isBlacklisted() || response.isInvalidKey()) {\n        addError(\"postEvent: Unrecoverable error: \" + response.getReason());\n      } else {\n        addWarn(\"postEvent: Transient error: \" + response.getReason());\n      }\n    } else {\n      addInfo(\"postEvent: successful post\");\n    }\n  }\n\n  private void accept(List<HoneycombResponse> responses) {\n    for (HoneycombResponse response : responses) {\n      if (!response.isSuccess()) {\n        if (response.isRateLimited()) {\n          addInfo(\"postBatch: Rate Limited: \" + response.getReason());\n        } else if (response.isBlacklisted() || response.isInvalidKey()) {\n          addError(\"postBatch: Unrecoverable error: \" + response.getReason());\n        } else {\n          addWarn(\"postBatch: Transient error: \" + response.getReason());\n        }\n      } else {\n        addInfo(\"postBatch: successful post\");\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "logback-honeycomb-client/gradle.properties",
    "content": "#\n# SPDX-License-Identifier: CC0-1.0\n#\n# Copyright 2018-2020 Will Sargent.\n#\n# Licensed under the CC0 Public Domain Dedication;\n# You may obtain a copy of the License at\n#\n#  http://creativecommons.org/publicdomain/zero/1.0/\n#\n\nproject_description = Logback Honeycomb Client API"
  },
  {
    "path": "logback-honeycomb-client/logback-honeycomb-client.gradle",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\n"
  },
  {
    "path": "logback-honeycomb-client/src/main/java/com/tersesystems/logback/honeycomb/client/HoneycombClient.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.honeycomb.client;\n\nimport java.util.List;\nimport java.util.concurrent.CompletionStage;\nimport java.util.function.Function;\n\npublic interface HoneycombClient<E> {\n\n  CompletionStage<HoneycombResponse> post(HoneycombRequest<E> request);\n\n  <F> CompletionStage<HoneycombResponse> post(\n      HoneycombRequest<F> request, Function<HoneycombRequest<F>, byte[]> encodeFunction);\n\n  CompletionStage<List<HoneycombResponse>> postBatch(Iterable<HoneycombRequest<E>> requests);\n\n  <F> CompletionStage<List<HoneycombResponse>> postBatch(\n      Iterable<HoneycombRequest<F>> requests, Function<HoneycombRequest<F>, byte[]> encodeFunction);\n\n  CompletionStage<Void> close();\n}\n"
  },
  {
    "path": "logback-honeycomb-client/src/main/java/com/tersesystems/logback/honeycomb/client/HoneycombClientService.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\n\npackage com.tersesystems.logback.honeycomb.client;\n\nimport java.util.function.Function;\n\npublic interface HoneycombClientService {\n  <E> HoneycombClient<E> newClient(\n      String apiKey, String dataset, Function<HoneycombRequest<E>, byte[]> encodeFunction);\n}\n"
  },
  {
    "path": "logback-honeycomb-client/src/main/java/com/tersesystems/logback/honeycomb/client/HoneycombHeaders.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.honeycomb.client;\n\npublic class HoneycombHeaders {\n  public static String teamHeader() {\n    return \"X-Honeycomb-Team\";\n  }\n\n  public static String eventTimeHeader() {\n    return \"X-Honeycomb-Event-Time\";\n  }\n\n  public static String sampleRateHeader() {\n    return \"X-Honeycomb-Samplerate\";\n  }\n}\n"
  },
  {
    "path": "logback-honeycomb-client/src/main/java/com/tersesystems/logback/honeycomb/client/HoneycombRequest.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.honeycomb.client;\n\nimport java.time.Instant;\n\npublic class HoneycombRequest<E> {\n\n  private final Integer sampleRate;\n  private final E event;\n  private final Instant timestamp;\n\n  public HoneycombRequest(Integer sampleRate, Instant timestamp, E event) {\n    this.sampleRate = sampleRate;\n    this.timestamp = timestamp;\n    this.event = event;\n  }\n\n  public E getEvent() {\n    return event;\n  }\n\n  public Integer getSampleRate() {\n    return sampleRate;\n  }\n\n  public Instant getTimestamp() {\n    return timestamp;\n  }\n}\n"
  },
  {
    "path": "logback-honeycomb-client/src/main/java/com/tersesystems/logback/honeycomb/client/HoneycombResponse.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.honeycomb.client;\n\npublic class HoneycombResponse {\n\n  private final String reason;\n  private final int status;\n\n  public HoneycombResponse(int status, String reason) {\n    this.status = status;\n    this.reason = reason;\n  }\n\n  public int getStatus() {\n    return status;\n  }\n\n  public String getReason() {\n    return reason;\n  }\n\n  public boolean isSuccess() {\n    return (getStatus() == 200 || getStatus() == 202);\n  }\n\n  public boolean isInvalidKey() {\n    return is400() && getReason().contains(\"credentials\");\n  }\n\n  public boolean isMalformed() {\n    return is400() && getReason().contains(\"malformed\");\n  }\n\n  public boolean isTooLarge() {\n    return is400() && getReason().contains(\"too large\");\n  }\n\n  public boolean isRateLimited() {\n    return is429() && getReason().contains(\"rate limiting\");\n  }\n\n  public boolean isBlacklisted() {\n    return is429() && getReason().contains(\"blacklisted\");\n  }\n\n  @Override\n  public String toString() {\n    return String.format(\"HoneyCombResponse(code = %s, text = %s)\", getStatus(), getReason());\n  }\n\n  private boolean is400() {\n    return getStatus() == 400;\n  }\n\n  private boolean is429() {\n    return getStatus() == 429;\n  }\n}\n"
  },
  {
    "path": "logback-honeycomb-okhttp/gradle.properties",
    "content": "#\n# SPDX-License-Identifier: CC0-1.0\n#\n# Copyright 2018-2020 Will Sargent.\n#\n# Licensed under the CC0 Public Domain Dedication;\n# You may obtain a copy of the License at\n#\n#  http://creativecommons.org/publicdomain/zero/1.0/\n#\n\nproject_description = Logback Honeycomb Client Implementation using OKHTTP"
  },
  {
    "path": "logback-honeycomb-okhttp/logback-honeycomb-okhttp.gradle",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\nplugins {\n    id 'java-library'\n}\n\ndependencies {\n    api project(\":logback-honeycomb-client\")\n\n    implementation \"ch.qos.logback:logback-classic:$logbackVersion\"\n\n    // https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind\n    implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.9.9'\n\n    implementation(\"com.squareup.okhttp3:okhttp:4.1.0\")\n\n    implementation group: 'com.google.auto.service', name: 'auto-service', version: '1.0-rc6'\n    annotationProcessor \"com.google.auto.service:auto-service:1.0-rc6\"\n}"
  },
  {
    "path": "logback-honeycomb-okhttp/src/main/java/com/tersesystems/logback/honeycomb/okhttp/HoneycombOkHTTPClient.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.honeycomb.okhttp;\n\nimport com.fasterxml.jackson.core.JsonFactory;\nimport com.fasterxml.jackson.core.JsonGenerator;\nimport com.fasterxml.jackson.core.JsonParser;\nimport com.fasterxml.jackson.core.JsonToken;\nimport com.tersesystems.logback.honeycomb.client.*;\nimport java.io.ByteArrayOutputStream;\nimport java.io.IOException;\nimport java.nio.charset.StandardCharsets;\nimport java.time.Instant;\nimport java.time.format.DateTimeFormatter;\nimport java.util.*;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.concurrent.CompletionStage;\nimport java.util.function.Function;\nimport okhttp3.*;\n\n/** This class implements a honeycomb client using OK HTTP. */\npublic class HoneycombOkHTTPClient<E> implements HoneycombClient<E> {\n  private static final MediaType JSON = MediaType.get(\"application/json; charset=utf-8\");\n\n  private final JsonFactory jsonFactory;\n  private final OkHttpClient client;\n  private final String apiKey;\n  private final String dataset;\n  private final Function<HoneycombRequest<E>, byte[]> defaultEncodeFunction;\n\n  public HoneycombOkHTTPClient(\n      OkHttpClient client,\n      JsonFactory jsonFactory,\n      String apiKey,\n      String dataset,\n      Function<HoneycombRequest<E>, byte[]> defaultEncodeFunction) {\n    this.client = client;\n    this.jsonFactory = jsonFactory;\n    this.dataset = dataset;\n    this.apiKey = apiKey;\n    this.defaultEncodeFunction = defaultEncodeFunction;\n  }\n\n  /** Posts a single event to honeycomb, using the \"1/events\" endpoint. */\n  @Override\n  public CompletionStage<HoneycombResponse> post(HoneycombRequest<E> honeycombRequest) {\n    return post(honeycombRequest, this.defaultEncodeFunction);\n  }\n\n  @Override\n  public <F> CompletionStage<HoneycombResponse> post(\n      HoneycombRequest<F> honeycombRequest, Function<HoneycombRequest<F>, byte[]> encodeFunction) {\n    String honeycombURL = eventURL(dataset);\n    byte[] bytes = encodeFunction.apply(honeycombRequest);\n\n    RequestBody body = RequestBody.create(bytes, JSON);\n    Request request =\n        new Request.Builder()\n            .url(honeycombURL)\n            .addHeader(HoneycombHeaders.teamHeader(), apiKey)\n            .addHeader(HoneycombHeaders.eventTimeHeader(), isoTime(honeycombRequest.getTimestamp()))\n            .addHeader(\n                HoneycombHeaders.sampleRateHeader(), honeycombRequest.getSampleRate().toString())\n            .post(body)\n            .build();\n\n    Call call = client.newCall(request);\n    OkHttpResponseFuture result = new OkHttpResponseFuture();\n    call.enqueue(result);\n    return result.future;\n  }\n\n  @Override\n  public CompletionStage<List<HoneycombResponse>> postBatch(\n      Iterable<HoneycombRequest<E>> requests) {\n    return postBatch(requests, this.defaultEncodeFunction);\n  }\n\n  @Override\n  public <F> CompletionStage<List<HoneycombResponse>> postBatch(\n      Iterable<HoneycombRequest<F>> honeycombRequests,\n      Function<HoneycombRequest<F>, byte[]> encodeFunction) {\n    String honeycombURL = batchURL(dataset);\n    try {\n      byte[] batchedJson = generateBatchJson(honeycombRequests, encodeFunction);\n      RequestBody body = RequestBody.create(batchedJson, JSON);\n      Request request =\n          new Request.Builder()\n              .url(honeycombURL)\n              .post(body)\n              .addHeader(HoneycombHeaders.teamHeader(), apiKey)\n              .build();\n\n      Call call = client.newCall(request);\n      OkHttpBatchedResponseFuture result = new OkHttpBatchedResponseFuture();\n      call.enqueue(result);\n      return result.future;\n    } catch (IOException e) {\n      throw new IllegalStateException(\"should never happen\", e);\n    }\n  }\n\n  private String eventURL(String dataset) {\n    String eventURL = \"https://api.honeycomb.io/1/events/\";\n    return eventURL + dataset;\n  }\n\n  private String batchURL(String dataset) {\n    String batchURL = \"https://api.honeycomb.io/1/batch/\";\n    return batchURL + dataset;\n  }\n\n  public CompletionStage<Void> close() {\n    return CompletableFuture.runAsync(\n        () -> {\n          client.dispatcher().executorService().shutdown();\n        });\n  }\n\n  private <F> byte[] generateBatchJson(\n      Iterable<HoneycombRequest<F>> requests, Function<HoneycombRequest<F>, byte[]> encodeFunction)\n      throws IOException {\n    ByteArrayOutputStream stream = new ByteArrayOutputStream();\n    JsonGenerator generator = jsonFactory.createGenerator(stream);\n    HoneycombRequestFormatter formatter = new HoneycombRequestFormatter(generator);\n\n    formatter.start();\n    for (HoneycombRequest<F> request : requests) {\n      formatter.format(request, encodeFunction);\n    }\n    formatter.end();\n    generator.close();\n\n    return stream.toByteArray();\n  }\n\n  private String isoTime(Instant eventTime) {\n    return DateTimeFormatter.ISO_INSTANT.format(eventTime);\n  }\n\n  class HoneycombRequestFormatter<F> {\n    private final JsonGenerator generator;\n\n    HoneycombRequestFormatter(JsonGenerator generator) {\n      this.generator = generator;\n    }\n\n    void start() throws IOException {\n      this.generator.writeStartArray();\n    }\n\n    void end() throws IOException {\n      this.generator.writeEndArray();\n    }\n\n    void format(HoneycombRequest<F> request, Function<HoneycombRequest<F>, byte[]> encodeFunction)\n        throws IOException {\n      byte[] bytes = encodeFunction.apply(request);\n\n      generator.writeStartObject();\n      generator.writeStringField(\"time\", isoTime(request.getTimestamp()));\n      generator.writeNumberField(\"samplerate\", request.getSampleRate());\n      generator.writeFieldName(\"data\");\n      generator.writeRaw(\":\");\n      generator.writeRaw(new String(bytes, StandardCharsets.UTF_8));\n      generator.writeEndObject();\n    }\n  }\n\n  static class OkHttpResponseFuture implements Callback {\n    private final CompletableFuture<HoneycombResponse> future = new CompletableFuture<>();\n\n    OkHttpResponseFuture() {}\n\n    @Override\n    public void onFailure(Call call, IOException e) {\n      future.completeExceptionally(e);\n    }\n\n    @Override\n    public void onResponse(Call call, Response response) throws IOException {\n      HoneycombResponse honeycombResponse =\n          new HoneycombResponse(response.code(), response.body().string());\n      future.complete(honeycombResponse);\n    }\n  }\n\n  class OkHttpBatchedResponseFuture implements Callback {\n    private final CompletableFuture<List<HoneycombResponse>> future = new CompletableFuture<>();\n\n    OkHttpBatchedResponseFuture() {}\n\n    @Override\n    public void onFailure(Call call, IOException e) {\n      future.completeExceptionally(e);\n    }\n\n    @Override\n    public void onResponse(Call call, Response response) throws IOException {\n      List<HoneycombResponse> honeycombResponses = parseResponse(response);\n      future.complete(honeycombResponses);\n    }\n\n    private List<HoneycombResponse> parseResponse(Response wsResponse) throws IOException {\n      String body = wsResponse.body().string();\n      List<HoneycombResponse> list = new ArrayList<>();\n\n      JsonParser parser = jsonFactory.createParser(body);\n      while (!parser.isClosed()) {\n        JsonToken jsonToken = parser.nextToken();\n\n        if (JsonToken.FIELD_NAME.equals(jsonToken)) {\n          String fieldName = parser.getCurrentName();\n          jsonToken = parser.nextToken();\n\n          String reason = \"\";\n          int status = 0;\n          if (\"error\".equals(fieldName)) {\n            reason = parser.getValueAsString();\n          } else if (\"status\".equals(fieldName)) {\n            status = parser.getValueAsInt();\n          }\n          HoneycombResponse response = new HoneycombResponse(status, reason);\n          list.add(response);\n        }\n      }\n\n      return list;\n    }\n  }\n}\n"
  },
  {
    "path": "logback-honeycomb-okhttp/src/main/java/com/tersesystems/logback/honeycomb/okhttp/HoneycombOkHTTPClientService.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\n\npackage com.tersesystems.logback.honeycomb.okhttp;\n\nimport com.fasterxml.jackson.core.JsonFactory;\nimport com.tersesystems.logback.honeycomb.client.HoneycombClient;\nimport com.tersesystems.logback.honeycomb.client.HoneycombClientService;\nimport com.tersesystems.logback.honeycomb.client.HoneycombRequest;\nimport java.util.function.Function;\nimport okhttp3.OkHttpClient;\n\npublic class HoneycombOkHTTPClientService implements HoneycombClientService {\n  @Override\n  public <E> HoneycombClient<E> newClient(\n      String apiKey, String dataset, Function<HoneycombRequest<E>, byte[]> encodeFunction) {\n    return new HoneycombOkHTTPClient(\n        new OkHttpClient(), new JsonFactory(), apiKey, dataset, encodeFunction);\n  }\n}\n"
  },
  {
    "path": "logback-honeycomb-okhttp/src/main/resources/META-INF/services/com.tersesystems.logback.honeycomb.client.HoneycombClientService",
    "content": "#\n# SPDX-License-Identifier: CC0-1.0\n#\n# Copyright 2018-2020 Will Sargent.\n#\n# Licensed under the CC0 Public Domain Dedication;\n# You may obtain a copy of the License at\n#\n#  http://creativecommons.org/publicdomain/zero/1.0/\n#\n\n#\n# SPDX-License-Identifier: CC0-1.0\n#\n# Copyright 2018-2020 Will Sargent.\n#\n# Licensed under the CC0 Public Domain Dedication;\n# You may obtain a copy of the License at\n#\n#  http://creativecommons.org/publicdomain/zero/1.0/\n#\n\ncom.tersesystems.logback.honeycomb.okhttp.HoneycombOkHTTPClientService"
  },
  {
    "path": "logback-jdbc-appender/gradle.properties",
    "content": "#\n# SPDX-License-Identifier: CC0-1.0\n#\n# Copyright 2018-2020 Will Sargent.\n#\n# Licensed under the CC0 Public Domain Dedication;\n# You may obtain a copy of the License at\n#\n#  http://creativecommons.org/publicdomain/zero/1.0/\n#\n\nproject_description = Logback JDBC Appender"
  },
  {
    "path": "logback-jdbc-appender/logback-jdbc-appender.gradle",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\nplugins {\n    id 'java-library'\n}\n\ndependencies {\n    api project(':logback-classic')\n\n    // .200 has the JSON data type\n    implementation \"com.zaxxer:HikariCP:3.4.2\"\n\n    testImplementation \"com.h2database:h2:1.4.200\"\n    testImplementation project(':logback-typesafe-config')\n    testImplementation 'org.awaitility:awaitility:4.0.2'\n\n    testImplementation 'org.apiguardian:apiguardian-api:1.1.0'\n    testImplementation \"net.logstash.logback:logstash-logback-encoder:$logstashVersion\"\n}"
  },
  {
    "path": "logback-jdbc-appender/src/main/java/com/tersesystems/logback/jdbc/JDBCAppender.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\n\npackage com.tersesystems.logback.jdbc;\n\nimport static java.util.Objects.requireNonNull;\n\nimport ch.qos.logback.classic.Level;\nimport ch.qos.logback.classic.spi.ILoggingEvent;\nimport ch.qos.logback.core.UnsynchronizedAppenderBase;\nimport ch.qos.logback.core.encoder.Encoder;\nimport com.tersesystems.logback.classic.NanoTime;\nimport com.tersesystems.logback.classic.StartTime;\nimport com.zaxxer.hikari.HikariConfig;\nimport com.zaxxer.hikari.HikariDataSource;\nimport java.sql.*;\nimport java.time.Duration;\nimport java.time.Instant;\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.concurrent.atomic.AtomicBoolean;\nimport java.util.concurrent.atomic.LongAdder;\nimport java.util.function.Consumer;\n\n/**\n * This appender writes out to a single table through JDBC.\n *\n * <p>It uses HikariCP and a thread pool executor to set up the <a\n * href=\"https://github.com/brettwooldridge/HikariCP/wiki/About-Pool-Sizing\">appropriate thread pool\n * size</a>.\n *\n * <p>Note that despite using a thread pool sized to the database connection pool, you should always\n * use the JDBC appender behind an async appender of some sort, as you'll want to ensure that\n * there's a queue feeding into the workers if they're all busy.\n */\npublic class JDBCAppender extends UnsynchronizedAppenderBase<ILoggingEvent> {\n  private final AtomicBoolean initialized = new AtomicBoolean(false);\n\n  private Encoder<ILoggingEvent> encoder;\n  private HikariDataSource dataSource;\n  private Duration reaperDuration;\n\n  private String driver;\n  private String url;\n  private String username;\n  private String password;\n\n  private String insertStatement;\n  private String createStatements;\n  private String reaperStatement;\n\n  private String reaperSchedule;\n  private String poolName = \"jdbc-appender-pool-\" + System.currentTimeMillis();\n  private int poolSize = 2;\n\n  private ExecutorService executorService;\n\n  // Debug flag for checking that a row was inserted.\n  private InsertConsumer insertConsumer;\n\n  protected boolean loggingInsert = false;\n\n  public boolean isLoggingInsert() {\n    return loggingInsert;\n  }\n\n  public String getUrl() {\n    return url;\n  }\n\n  public void setUrl(String url) {\n    this.url = url;\n  }\n\n  public Encoder<ILoggingEvent> getEncoder() {\n    return encoder;\n  }\n\n  public void setEncoder(Encoder<ILoggingEvent> encoder) {\n    this.encoder = encoder;\n  }\n\n  public String getUsername() {\n    return username;\n  }\n\n  public void setUsername(String username) {\n    this.username = username;\n  }\n\n  public String getPassword() {\n    return password;\n  }\n\n  public void setPassword(String password) {\n    this.password = password;\n  }\n\n  public void setPoolSize(int poolSize) {\n    this.poolSize = poolSize;\n  }\n\n  public String getPoolName() {\n    return poolName;\n  }\n\n  public void setPoolName(String poolName) {\n    this.poolName = poolName;\n  }\n\n  public String getDriver() {\n    return driver;\n  }\n\n  public void setDriver(String driver) {\n    this.driver = driver;\n  }\n\n  public String getReaperSchedule() {\n    return reaperSchedule;\n  }\n\n  public void setReaperSchedule(String reaperSchedule) {\n    this.reaperSchedule = reaperSchedule;\n  }\n\n  public String getReaperStatement() {\n    return reaperStatement;\n  }\n\n  public void setReaperStatement(String reaperStatement) {\n    this.reaperStatement = reaperStatement;\n  }\n\n  public String getCreateStatements() {\n    return createStatements;\n  }\n\n  public void setCreateStatements(String createStatements) {\n    this.createStatements = createStatements;\n  }\n\n  public String getInsertStatement() {\n    return insertStatement;\n  }\n\n  public void setInsertStatement(String insertStatement) {\n    this.insertStatement = insertStatement;\n  }\n\n  @Override\n  public void start() {\n    super.start();\n    executorService = Executors.newFixedThreadPool(poolSize);\n  }\n\n  @Override\n  public void stop() {\n    super.stop();\n    closeConnection();\n    shutdownThreadPool();\n    initialized.set(false);\n  }\n\n  @Override\n  protected void append(ILoggingEvent event) {\n    if (!initialized.get()) {\n      initialize();\n    }\n    executorService.submit(() -> insertConsumer.accept(event));\n  }\n\n  // When the appender is starting, then Logback hasn't started up yet and so\n  // we'll get very odd errors if the driver starts trying to log things itself.\n  // Instead, we're going to register something that will start a datasource\n  // when something comes through the pipeline.\n  protected void initialize() {\n    if (!initialized.getAndSet(true)) {\n      addInfo(\"initialize: \");\n      try {\n        dataSource = createDataSource(driver, url, username, password);\n        insertConsumer = new InsertConsumer();\n        checkConnection();\n        createTableIfNecessary();\n        scheduleReaper();\n      } catch (Exception e) {\n        addError(\"Cannot configure database connection\", e);\n      }\n    }\n  }\n\n  protected HikariDataSource createDataSource(\n      String driver, String url, String username, String password) {\n    HikariConfig config = new HikariConfig();\n    config.setDriverClassName(Objects.requireNonNull(driver, \"Null driver\"));\n    config.setJdbcUrl(requireNonNull(url, \"Null url\"));\n    config.setUsername(username);\n    config.setPassword(password);\n    config.setPoolName(poolName);\n    config.setAutoCommit(true); // always use autocommit mode here.\n    config.setMinimumIdle(poolSize);\n    config.setMaximumPoolSize(poolSize);\n    Properties props = new Properties();\n    // props.put(\"dataSource.logWriter\", new PrintWriter(System.out));\n    config.setDataSourceProperties(props);\n    return new HikariDataSource(config);\n  }\n\n  protected void shutdownThreadPool() {\n    if (executorService == null || executorService.isTerminated()) {\n      return;\n    }\n    try {\n      executorService.awaitTermination(1, TimeUnit.SECONDS);\n      executorService = null;\n    } catch (InterruptedException e) {\n      // This isn't worth reporting.\n    }\n  }\n\n  protected void checkConnection() throws SQLException {\n    if (dataSource != null) {\n      try (Connection conn = dataSource.getConnection()) {\n        addInfo(\"checkConnection: trying isValid with a 1000 msec timeout\");\n        if (conn.isValid(1000)) {\n          addInfo(\"checkConnection: isValid returned true!\");\n        } else {\n          addWarn(\"checkConnection: isValid returned false!\");\n        }\n      }\n    }\n  }\n\n  protected void createTableIfNecessary() {\n    // Initialize with DDL\n    // XXX should really check if the table exists already\n    String createStatements = getCreateStatements();\n    if (createStatements == null || createStatements.trim().isEmpty()) {\n      return;\n    }\n\n    addInfo(\"createTable: \" + createStatements);\n    try {\n      try (Connection conn = dataSource.getConnection()) {\n        try (Statement stmt = conn.createStatement()) {\n          stmt.executeUpdate(createStatements);\n        }\n      }\n    } catch (SQLException e) {\n      addWarn(\"Cannot create table, assuming it exists already\", e);\n    }\n  }\n\n  protected void scheduleReaper() {\n    String reaperSchedule = getReaperSchedule();\n    if (reaperSchedule == null || reaperSchedule.trim().isEmpty()) {\n      return;\n    }\n    addInfo(\"scheduleReaper: \" + reaperSchedule);\n\n    String reaperStatement = getReaperStatement();\n    if (reaperStatement == null || reaperStatement.trim().isEmpty()) {\n      addError(\n          \"scheduleReaper: reaperSchedule exists, but there is no reaperStatement to execute!\");\n      return;\n    }\n\n    reaperDuration = Duration.parse(reaperSchedule);\n    ScheduledExecutorService ses = context.getScheduledExecutorService();\n    ScheduledFuture<?> scheduledFuture =\n        ses.scheduleAtFixedRate(\n            this::reapOldEvents,\n            reaperDuration.toMillis(),\n            reaperDuration.toMillis(),\n            TimeUnit.MILLISECONDS);\n    context.addScheduledFuture(scheduledFuture);\n  }\n\n  protected void reapOldEvents() {\n    addInfo(\"reapOldEvents: \");\n    try {\n      try (Connection conn = dataSource.getConnection()) {\n        try (PreparedStatement stmt = conn.prepareStatement(getReaperStatement())) {\n          Instant reapAtInstant = now().minus(reaperDuration);\n          stmt.setTimestamp(1, new java.sql.Timestamp(reapAtInstant.toEpochMilli()));\n          int results = stmt.executeUpdate();\n          addInfo(String.format(\"Reaped %d statements older than %s\", results, reapAtInstant));\n        }\n      }\n    } catch (SQLException e) {\n      addWarn(\"Cannot reap old events!\", e);\n    }\n  }\n\n  protected Instant now() {\n    return Instant.now();\n  }\n\n  protected void closeConnection() {\n    insertConsumer = null;\n    if (dataSource != null) {\n      try {\n        if (!dataSource.isClosed()) {\n          dataSource.close();\n        }\n        dataSource = null;\n      } catch (Exception e) {\n        addError(\"Exception closing datasource\", e);\n      }\n    }\n  }\n\n  protected int insertStatement(ILoggingEvent event, LongAdder adder, PreparedStatement statement)\n      throws SQLException {\n    insertTimestamp(event, adder, statement);\n    insertRelativeTime(event, adder, statement);\n    insertStartTime(event, adder, statement);\n    insertIntLevel(event, adder, statement);\n    insertStringLevel(event, adder, statement);\n    insertEvent(event, adder, statement);\n\n    insertAdditionalData(event, adder, statement);\n\n    return statement.executeUpdate();\n  }\n\n  /**\n   * An empty method for use by subclasses who want to add additional fields.\n   *\n   * <p>Make sure to call adder.increment()\n   *\n   * @param event logging event\n   * @param adder adder\n   * @param statement prepared statement\n   * @throws SQLException if something goes wrong\n   */\n  protected void insertAdditionalData(\n      ILoggingEvent event, LongAdder adder, PreparedStatement statement) throws SQLException {\n    // do nothing\n  }\n\n  protected void insertEvent(ILoggingEvent event, LongAdder adder, PreparedStatement statement)\n      throws SQLException {\n    statement.setBytes(adder.intValue(), encoder.encode(event));\n    adder.increment();\n  }\n\n  protected void insertStringLevel(\n      ILoggingEvent event, LongAdder adder, PreparedStatement statement) throws SQLException {\n    statement.setString(adder.intValue(), event.getLevel().toString());\n    adder.increment();\n  }\n\n  protected void insertIntLevel(ILoggingEvent event, LongAdder adder, PreparedStatement statement)\n      throws SQLException {\n    Level level = event.getLevel();\n    statement.setInt(adder.intValue(), level.toInt());\n    adder.increment();\n  }\n\n  protected void insertStartTime(ILoggingEvent event, LongAdder adder, PreparedStatement statement)\n      throws SQLException {\n    Optional<Long> startTime = StartTime.fromOptional(context, event).map(Instant::toEpochMilli);\n    if (startTime.isPresent()) {\n      statement.setLong(adder.intValue(), startTime.get());\n    } else {\n      statement.setNull(adder.intValue(), Types.BIGINT);\n    }\n    adder.increment();\n  }\n\n  protected void insertRelativeTime(\n      ILoggingEvent event, LongAdder adder, PreparedStatement statement) throws SQLException {\n    Optional<Long> aLong = NanoTime.fromOptional(context, event);\n    if (aLong.isPresent()) {\n      statement.setLong(adder.intValue(), aLong.get());\n    } else {\n      statement.setNull(adder.intValue(), Types.BIGINT);\n    }\n\n    adder.increment();\n  }\n\n  protected void insertTimestamp(ILoggingEvent event, LongAdder adder, PreparedStatement statement)\n      throws SQLException {\n    long eventMillis = event.getTimeStamp();\n    statement.setTimestamp(adder.intValue(), new Timestamp(eventMillis));\n    adder.increment();\n  }\n\n  class InsertConsumer implements Consumer<ILoggingEvent> {\n    public void accept(ILoggingEvent event) {\n      // Will need to check state here because executor service runs in a different thread.\n      if (isStarted()) {\n        try (Connection conn = dataSource.getConnection()) {\n          String insertStatement = requireNonNull(getInsertStatement());\n          if (conn.isValid(100)) {\n            try (PreparedStatement statement = conn.prepareStatement(insertStatement)) {\n              LongAdder adder = new LongAdder();\n              adder.increment();\n              int result = insertStatement(event, adder, statement);\n              if (isLoggingInsert()) {\n                String msg = String.format(\"Inserted resulted in %d rows added\", result);\n                addInfo(msg);\n              }\n            }\n          } else {\n            addError(\"Connection is not valid!\");\n          }\n        } catch (Exception e) {\n          addError(\"Cannot insert event, please check you are using a valid encoder!\", e);\n        }\n      } else {\n        addError(\"Not started yet, returning to queue!\");\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "logback-jdbc-appender/src/test/java/com/tersesystems/logback/jdbc/JDBCAppenderTest.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\n\npackage com.tersesystems.logback.jdbc;\n\nimport static java.util.Objects.requireNonNull;\nimport static java.util.concurrent.TimeUnit.SECONDS;\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.awaitility.Awaitility.await;\nimport static org.junit.Assert.fail;\n\nimport ch.qos.logback.classic.Logger;\nimport ch.qos.logback.classic.LoggerContext;\nimport ch.qos.logback.classic.joran.JoranConfigurator;\nimport ch.qos.logback.core.joran.spi.JoranException;\nimport java.net.URL;\nimport java.sql.*;\nimport org.junit.After;\nimport org.junit.Before;\nimport org.junit.jupiter.api.Test;\n\npublic class JDBCAppenderTest {\n\n  @Before\n  @After\n  public void clear() {\n    try (Connection conn = DriverManager.getConnection(\"jdbc:h2:mem:terse-logback\", \"sa\", \"\")) {\n      try (Statement s = conn.createStatement()) {\n        s.execute(\"truncate table events\");\n      }\n    } catch (SQLException e) {\n      fail(e.getMessage());\n    }\n  }\n\n  @Test\n  public void testSimple() throws JoranException, SQLException {\n    LoggerContext loggerFactory = createLoggerFactory(\"/logback-test.xml\");\n\n    // Write something that never gets logged explicitly...\n    Logger logger = loggerFactory.getLogger(\"some.example.ExampleClass\");\n\n    logger.info(\"info one\");\n    await().atMost(5, SECONDS).until(this::assertTablesExist);\n\n    logger.info(\"info two\");\n    logger.info(\"info three\");\n\n    await().atMost(1, SECONDS).untilAsserted(() -> assertRowsEntered(3));\n  }\n\n  private Boolean assertTablesExist() {\n    try (Connection conn = DriverManager.getConnection(\"jdbc:h2:mem:terse-logback\", \"sa\", \"\")) {\n      try (PreparedStatement p = conn.prepareStatement(\"select count(*) from events\")) {\n        return p.execute();\n      }\n    } catch (SQLException e) {\n      return false;\n    }\n  }\n\n  public void assertRowsEntered(Integer expectedCount) throws SQLException {\n    try (Connection conn = DriverManager.getConnection(\"jdbc:h2:mem:terse-logback\", \"sa\", \"\")) {\n      try (PreparedStatement p = conn.prepareStatement(\"select count(*) from events\")) {\n        assertThat(getCount(p)).isEqualTo(expectedCount);\n      }\n    }\n  }\n\n  public int getCount(PreparedStatement p) throws SQLException {\n    try (ResultSet rs = p.executeQuery()) {\n      if (rs.next()) {\n        return rs.getInt(1);\n      } else {\n        return 0;\n      }\n    }\n  }\n\n  LoggerContext createLoggerFactory(String resourceName) throws JoranException {\n    LoggerContext context = new LoggerContext();\n    URL resource = getClass().getResource(resourceName);\n    JoranConfigurator configurator = new JoranConfigurator();\n    configurator.setContext(context);\n    configurator.doConfigure(resource);\n    return context;\n  }\n\n  JDBCAppender getJDBCAppender(LoggerContext context) {\n    return (JDBCAppender)\n        requireNonNull(context.getLogger(Logger.ROOT_LOGGER_NAME).getAppender(\"JDBC\"));\n  }\n}\n"
  },
  {
    "path": "logback-jdbc-appender/src/test/resources/logback-reference.conf",
    "content": "levels {\n  ROOT = INFO\n}\n\nlocal {\n  jdbc {\n    url = \"jdbc:h2:mem:terse-logback\"\n    driver = \"org.h2.Driver\"\n    username = \"sa\"\n    password = \"\"\n    insertStatement = \"insert into events(ts, relative_ns, start_ms, level_value, level, evt) values(?, ?, ?, ?, ?, ?)\"\n    createStatements = \"\"\"\n    CREATE TABLE IF NOT EXISTS events (\n       ID INT NOT NULL PRIMARY KEY AUTO_INCREMENT,\n       ts TIMESTAMP(9) WITH TIME ZONE NOT NULL,\n       relative_ns numeric NULL,\n       start_ms numeric NULL,\n       level_value int NOT NULL,\n       level VARCHAR(7) NOT NULL,\n       evt JSON NOT NULL\n    );\n    \"\"\"\n    reaperStatement = \"delete from events where ts < ?\"\n    reaperSchedule = PT30\n  }\n}\n\n# Defines properties (Strings) to be set in context scope (loggerContext.putProperty)\n# See https://logback.qos.ch/manual/configuration.html#scopes\ncontext {\n\n}\n"
  },
  {
    "path": "logback-jdbc-appender/src/test/resources/logback-test.xml",
    "content": "<!--\n  ~ SPDX-License-Identifier: CC0-1.0\n  ~\n  ~ Copyright 2018-2020 Will Sargent.\n  ~\n  ~ Licensed under the CC0 Public Domain Dedication;\n  ~ You may obtain a copy of the License at\n  ~\n  ~  http://creativecommons.org/publicdomain/zero/1.0/\n  -->\n\n<configuration>\n\n    <newRule pattern=\"configuration/typesafeConfig\"\n             actionClass=\"com.tersesystems.logback.typesafeconfig.TypesafeConfigAction\"/>\n\n    <typesafeConfig>\n    </typesafeConfig>\n\n    <appender name=\"JDBC\" class=\"com.tersesystems.logback.jdbc.JDBCAppender\">\n        <driver>${jdbc.driver}</driver>\n        <url>${jdbc.url}</url>\n        <username>${jdbc.username}</username>\n        <password>${jdbc.password}</password>\n\n        <!-- could also use jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;INIT=runscript from 'classpath:/db.sql' -->\n        <createStatements>${jdbc.createStatements}</createStatements>\n        <insertStatement>${jdbc.insertStatement}</insertStatement>\n        <reaperStatement>${jdbc.reaperStatement}</reaperStatement>\n        <reaperSchedule>${jdbc.reaperSchedule}</reaperSchedule>\n\n        <encoder class=\"net.logstash.logback.encoder.LogstashEncoder\">\n        </encoder>\n    </appender>\n\n    <logger name=\"some.example.ExampleClass\" level=\"INFO\"/>\n\n    <root level=\"ERROR\">\n       <appender-ref ref=\"JDBC\"/>\n    </root>\n\n</configuration>"
  },
  {
    "path": "logback-postgresjson-appender/gradle.properties",
    "content": "#\n# SPDX-License-Identifier: CC0-1.0\n#  \n# Copyright 2018-2020 Will Sargent.\n#\n# Licensed under the CC0 Public Domain Dedication;\n# You may obtain a copy of the License at\n#\n#  http://creativecommons.org/publicdomain/zero/1.0/\n#\n\nproject_description = Logback Postgres JSON Appender"
  },
  {
    "path": "logback-postgresjson-appender/logback-postgresjson-appender.gradle",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *  \n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\ndependencies {\n    implementation project(\":logback-jdbc-appender\")\n\n    implementation group: 'org.postgresql', name: 'postgresql', version: '42.2.6'\n\n    // Need to set up a FlywayBaseTest, the gradle plugin won't run a \"testFlywayMigrate\" task\n    // testImplementation \"org.flywaydb:flyway-core:6.0.0\"\n    // technically any JSON string is valid input, so we only require logstash-logback-encoder for testing\n    testImplementation \"net.logstash.logback:logstash-logback-encoder:$logstashVersion\"\n}\n"
  },
  {
    "path": "logback-postgresjson-appender/src/main/java/com/tersesystems/logback/postgresjson/PostgresJsonAppender.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.postgresjson;\n\nimport ch.qos.logback.classic.spi.ILoggingEvent;\nimport com.tersesystems.logback.jdbc.JDBCAppender;\nimport java.nio.charset.StandardCharsets;\nimport java.sql.PreparedStatement;\nimport java.sql.SQLException;\nimport java.util.concurrent.atomic.LongAdder;\nimport org.postgresql.util.PGobject;\n\n/** Extends the JDBC appender to write out Postgres JSON object. */\npublic class PostgresJsonAppender extends JDBCAppender {\n\n  private String objectType = \"json\";\n\n  public String getObjectType() {\n    return objectType;\n  }\n\n  public void setObjectType(String objectType) {\n    this.objectType = objectType;\n  }\n\n  @Override\n  public void start() {\n    super.start();\n    setDriver(\"org.postgresql.Driver\");\n  }\n\n  @Override\n  protected void insertEvent(ILoggingEvent event, LongAdder adder, PreparedStatement statement)\n      throws SQLException {\n    PGobject jsonObject = new PGobject();\n    jsonObject.setType(getObjectType());\n    byte[] bytes = getEncoder().encode(event);\n    jsonObject.setValue(new String(bytes, StandardCharsets.UTF_8));\n    statement.setObject(adder.intValue(), jsonObject);\n    adder.increment();\n  }\n}\n"
  },
  {
    "path": "logback-postgresjson-appender/src/test/java/com/tersesystems/logback/postgresjson/PostgresJsonAppenderTest.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.postgresjson;\n\nimport ch.qos.logback.classic.Logger;\nimport ch.qos.logback.core.joran.spi.JoranException;\nimport com.tersesystems.logback.classic.Utils;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\npublic class PostgresJsonAppenderTest {\n\n  @Disabled\n  @Test\n  public void testJson() throws JoranException, InterruptedException {\n\n    Utils utils = Utils.create(\"/logback-postgres-json.xml\");\n    Logger logger1 = utils.getLogger(\"com.example.Test\");\n    logger1.info(\"THIS IS A TEST\");\n\n    Thread.sleep(1000);\n\n    utils.getStatusList().forEach(System.out::println);\n  }\n}\n"
  },
  {
    "path": "logback-postgresjson-appender/src/test/resources/db/migration/V1__logging_table.sql",
    "content": "--\n-- SPDX-License-Identifier: CC0-1.0\n--\n-- Copyright 2018-2019 Will Sargent.\n--\n-- Licensed under the CC0 Public Domain Dedication;\n-- You may obtain a copy of the License at\n--\n--     http://creativecommons.org/publicdomain/zero/1.0/\n--\n\n-- timestamp will only give microsecond precision, so we store both timestamp and time since epoch in milliseconds.\n-- store the start time in milliseconds.\nCREATE TABLE logging_table (\n   ID serial NOT NULL PRIMARY KEY,\n   ts TIMESTAMPTZ(6) NOT NULL,\n   tse_ms numeric NOT NULL,\n   start_ms numeric NULL,\n   level_value int NOT NULL,\n   level VARCHAR(7) NOT NULL,\n   evt jsonb NOT NULL\n);\n\nCREATE INDEX idxgin ON logging_table USING gin (evt);"
  },
  {
    "path": "logback-postgresjson-appender/src/test/resources/logback-postgres-json.xml",
    "content": "<!--\n  ~ SPDX-License-Identifier: CC0-1.0\n  ~\n  ~ Copyright 2018-2020 Will Sargent.\n  ~\n  ~ Licensed under the CC0 Public Domain Dedication;\n  ~ You may obtain a copy of the License at\n  ~\n  ~  http://creativecommons.org/publicdomain/zero/1.0/\n  -->\n<configuration>\n\n    <!-- async appender needs a shutdown hook to make sure this clears -->\n    <shutdownHook class=\"ch.qos.logback.core.hook.DelayingShutdownHook\"/>\n\n    <!-- SQL is blocking, so use an async lmax appender here -->\n    <appender name=\"ASYNC_POSTGRES\" class=\"net.logstash.logback.appender.LoggingEventAsyncDisruptorAppender\">\n\n        <appender class=\"com.tersesystems.logback.postgresjson.PostgresJsonAppender\">\n            <createStatements>\n                CREATE TABLE logging_table (\n                ID serial NOT NULL PRIMARY KEY,\n                ts TIMESTAMPTZ(6) NOT NULL,\n                tse_ms bigint NOT NULL,\n                start_ms bigint NULL,\n                level_value int NOT NULL,\n                level VARCHAR(7) NOT NULL,\n                evt jsonb NOT NULL\n                );\n                CREATE INDEX idxgin ON logging_table USING gin (evt);\n            </createStatements>\n\n            <!-- SQL statement takes a TIMESTAMP, LONG, INT, VARCHAR, PGObject -->\n            <insertStatement>insert into logging_table(ts, tse_ms, start_ms, level_value, level, evt) values(?, ?, ?, ?, ?, ?)</insertStatement>\n\n            <url>jdbc:postgresql://localhost:5432/logback</url>\n            <username>logback</username>\n            <password>logback</password>\n\n            <encoder class=\"net.logstash.logback.encoder.LogstashEncoder\">\n            </encoder>\n        </appender>\n    </appender>\n\n    <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n        <encoder>\n            <pattern>%-5relative %-5level %logger{35} - %msg%n</pattern>\n        </encoder>\n    </appender>\n\n    <root level=\"INFO\">\n        <appender-ref ref=\"ASYNC_POSTGRES\"/>\n        <appender-ref ref=\"STDOUT\"/>\n    </root>\n</configuration>"
  },
  {
    "path": "logback-tracing/gradle.properties",
    "content": "#\n# SPDX-License-Identifier: CC0-1.0\n#\n# Copyright 2018-2020 Will Sargent.\n#\n# Licensed under the CC0 Public Domain Dedication;\n# You may obtain a copy of the License at\n#\n#  http://creativecommons.org/publicdomain/zero/1.0/\n#\n\nproject_description = Logback Honeycomb Tracing"
  },
  {
    "path": "logback-tracing/logback-tracing.gradle",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\ndependencies {\n    //implementation \"org.slf4j:slf4j-api:$slf4jVersion\"\n    //implementation project(':logback-uniqueid-appender')\n    implementation project(':logback-classic')\n\n    implementation      \"com.google.auto.value:auto-value-annotations:1.6.2\"\n    annotationProcessor \"com.google.auto.value:auto-value:1.6.2\"\n\n    implementation \"net.logstash.logback:logstash-logback-encoder:$logstashVersion\"\n    implementation \"ch.qos.logback:logback-classic:$logbackVersion\"\n}"
  },
  {
    "path": "logback-tracing/src/main/java/com/tersesystems/logback/tracing/EventInfo.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\n\npackage com.tersesystems.logback.tracing;\n\nimport com.google.auto.value.AutoValue;\n\n/**\n * An event info is a span without a duration. It cannot be used as a parent.\n *\n * <p>https://docs.honeycomb.io/working-with-your-data/tracing/send-trace-data/#span-events\n */\n@AutoValue\npublic abstract class EventInfo {\n\n  public static Builder builder() {\n    return new AutoValue_EventInfo.Builder();\n  }\n\n  public abstract Builder toBuilder();\n\n  @Nullable\n  public abstract String parentId();\n\n  public abstract String traceId();\n\n  public abstract String name();\n\n  @AutoValue.Builder\n  public abstract static class Builder {\n    public abstract Builder setName(String name);\n\n    public abstract Builder setParentId(String parentId);\n\n    public abstract Builder setTraceId(String traceId);\n\n    public abstract EventInfo build();\n  }\n}\n"
  },
  {
    "path": "logback-tracing/src/main/java/com/tersesystems/logback/tracing/EventMarkerFactory.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\n\npackage com.tersesystems.logback.tracing;\n\nimport net.logstash.logback.marker.LogstashMarker;\nimport net.logstash.logback.marker.Markers;\n\n/**\n * This is a marker factory that adds several logstash markers to create a span in Honeycomb format.\n */\npublic class EventMarkerFactory {\n\n  // Java API\n  public LogstashMarker create(EventInfo eventInfo) {\n    LogstashMarker[] markers = generateMarkers(eventInfo);\n    return Markers.aggregate(markers);\n  }\n\n  // Scala API\n  public LogstashMarker apply(EventInfo eventInfo) {\n    return create(eventInfo);\n  }\n\n  protected LogstashMarker[] generateMarkers(EventInfo eventInfo) {\n    // XXX Should have a field name registry that lets you define field names by dataset\n    LogstashMarker nameMarker = Markers.append(\"name\", eventInfo.name());\n    LogstashMarker parentIdMarker = Markers.append(\"trace.parent_id\", eventInfo.parentId());\n    LogstashMarker traceIdMarker = Markers.append(\"trace.trace_id\", eventInfo.traceId());\n    LogstashMarker spanTypeMarker = Markers.append(\"meta.span_type\", \"span_event\");\n    // Don't include the timestamp marker, as it'll be handled by Logback\n    LogstashMarker[] markers = {nameMarker, parentIdMarker, traceIdMarker, spanTypeMarker};\n    return markers;\n  }\n}\n"
  },
  {
    "path": "logback-tracing/src/main/java/com/tersesystems/logback/tracing/LinkInfo.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\n\npackage com.tersesystems.logback.tracing;\n\nimport com.google.auto.value.AutoValue;\n\n/** https://docs.honeycomb.io/working-with-your-data/tracing/send-trace-data/#links */\n@AutoValue\npublic abstract class LinkInfo {\n\n  public static Builder builder() {\n    return new AutoValue_LinkInfo.Builder();\n  }\n\n  public abstract Builder toBuilder();\n\n  @Nullable\n  public abstract String parentId();\n\n  public abstract String traceId();\n\n  public abstract String linkedSpanId();\n\n  public abstract String linkedTraceId();\n\n  @AutoValue.Builder\n  public abstract static class Builder {\n\n    public abstract Builder setLinkedSpanId(String linkedSpanId);\n\n    public abstract Builder setLinkedTraceId(String linkedTraceId);\n\n    public abstract Builder setParentId(String parentId);\n\n    public abstract Builder setTraceId(String traceId);\n\n    public abstract LinkInfo build();\n  }\n}\n"
  },
  {
    "path": "logback-tracing/src/main/java/com/tersesystems/logback/tracing/LinkMarkerFactory.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\n\npackage com.tersesystems.logback.tracing;\n\nimport net.logstash.logback.marker.LogstashMarker;\nimport net.logstash.logback.marker.Markers;\n\npublic class LinkMarkerFactory {\n\n  // Java API\n  public LogstashMarker create(LinkInfo linkInfo) {\n    LogstashMarker[] markers = generateMarkers(linkInfo);\n    return Markers.aggregate(markers);\n  }\n\n  // Scala API\n  public LogstashMarker apply(LinkInfo linkInfo) {\n    return create(linkInfo);\n  }\n\n  protected LogstashMarker[] generateMarkers(LinkInfo linkInfo) {\n    // XXX Should have a field name registry that lets you define field names by dataset\n    LogstashMarker traceIdMarker = Markers.append(\"trace.trace_id\", linkInfo.traceId());\n    LogstashMarker parentIdMarker = Markers.append(\"trace.parent_id\", linkInfo.parentId());\n    LogstashMarker linkedSpanMarker = Markers.append(\"trace.link.span_id\", linkInfo.linkedSpanId());\n    LogstashMarker linkedTraceMarker =\n        Markers.append(\"trace.link.trace_id\", linkInfo.linkedTraceId());\n    LogstashMarker spanTypeMarker = Markers.append(\"meta.span_type\", \"link\");\n    LogstashMarker[] markers = {\n      parentIdMarker, traceIdMarker, linkedSpanMarker, linkedTraceMarker, spanTypeMarker\n    };\n    return markers;\n  }\n}\n"
  },
  {
    "path": "logback-tracing/src/main/java/com/tersesystems/logback/tracing/Nullable.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.tracing;\n\nimport static java.lang.annotation.ElementType.TYPE_USE;\nimport static java.lang.annotation.RetentionPolicy.SOURCE;\n\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.Target;\n\n/**\n * autovalue wants a Nullable but doesn't tell us from where.\n *\n * <p>anything will work, so defining one here.\n *\n * <p>https://github.com/google/auto/issues/283#issuecomment-337281043\n */\n@Target(TYPE_USE)\n@Retention(SOURCE)\n@interface Nullable {}\n"
  },
  {
    "path": "logback-tracing/src/main/java/com/tersesystems/logback/tracing/SpanInfo.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.tracing;\n\nimport com.google.auto.value.AutoValue;\nimport java.time.Duration;\nimport java.time.Instant;\nimport java.util.function.Function;\nimport java.util.function.Supplier;\n\n@AutoValue\npublic abstract class SpanInfo {\n\n  public static Builder builder() {\n    return new AutoValue_SpanInfo.Builder();\n  }\n\n  public abstract Builder toBuilder();\n\n  public abstract String spanId();\n\n  @Nullable\n  public abstract String parentId();\n\n  public abstract String traceId();\n\n  public abstract String name();\n\n  public abstract String serviceName();\n\n  public abstract Instant startTime();\n\n  public Duration duration() {\n    return durationSupplier().get();\n  }\n\n  public abstract Supplier<Duration> durationSupplier();\n\n  public abstract Supplier<String> idGenerator();\n\n  /**\n   * Creates a child builder with the parent id set to the current span id, a random UUID set to the\n   * span id.\n   *\n   * @return a child builder.\n   */\n  public Builder childBuilder() {\n    return this.toBuilder()\n        .setSpanId(idGenerator().get())\n        .setIdGenerator(idGenerator())\n        .setParentId(spanId());\n  }\n\n  /**\n   * Provides a function with a child that can be used as a convenience wrapper, which calls {@code\n   * childBuilder().setName().buildNow()} under the hood.\n   *\n   * <p>{@code <pre>return parentSpanInfo.withChild(\"doThing\", childSpan -> { return\n   * doThing(childSpan); }); </pre>}\n   *\n   * @param methodName the name of the child span\n   * @param childFunction the child function\n   * @param <T> the type of the return value\n   * @return the return value\n   */\n  public <T> T withChild(String methodName, Function<SpanInfo, T> childFunction) {\n    return childFunction.apply(childBuilder().setName(methodName).buildNow());\n  }\n\n  @AutoValue.Builder\n  public abstract static class Builder {\n    public abstract Builder setName(String name);\n\n    public abstract Builder setSpanId(String spanId);\n\n    public abstract Builder setParentId(String parentId);\n\n    public abstract Builder setTraceId(String traceId);\n\n    public abstract Builder setIdGenerator(Supplier<String> idGenerator);\n\n    public abstract Builder setStartTime(Instant startTime);\n\n    public abstract Builder setServiceName(String serviceName);\n\n    public abstract Builder setDurationSupplier(Supplier<Duration> duration);\n\n    public abstract SpanInfo build();\n\n    /**\n     * Creates a random UUID for the trace id and span id and set the name.\n     *\n     * @param idGenerator the id generator for span and trace.\n     * @param name the span name\n     * @return the configured builder.\n     */\n    public Builder setRootSpan(Supplier<String> idGenerator, String name) {\n      return this.setTraceId(idGenerator.get())\n          .setSpanId(idGenerator.get())\n          .setIdGenerator(idGenerator)\n          .setName(name);\n    }\n\n    public Builder startNow() {\n      Instant startTime = Instant.now();\n      return this.setDurationSupplier(() -> Duration.between(startTime, Instant.now()))\n          .setStartTime(startTime);\n    }\n\n    /**\n     * Builds a span info, setting the duration supplier to be {@code Duration.between(now,\n     * Instant.now())}\n     *\n     * @return the span info already started.\n     */\n    public SpanInfo buildNow() {\n      return startNow().build();\n    }\n  }\n}\n"
  },
  {
    "path": "logback-tracing/src/main/java/com/tersesystems/logback/tracing/SpanMarkerFactory.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.tracing;\n\nimport com.tersesystems.logback.classic.StartTimeMarker;\nimport net.logstash.logback.marker.LogstashMarker;\nimport net.logstash.logback.marker.Markers;\n\n/**\n * This is a marker factory that adds several logstash markers to create a span in Honeycomb format.\n */\npublic class SpanMarkerFactory {\n\n  // Java API\n  public LogstashMarker create(SpanInfo spanInfo) {\n    StartTimeMarker startTime = new StartTimeMarker(spanInfo.startTime());\n    LogstashMarker[] markers = generateMarkers(spanInfo);\n    return Markers.aggregate(markers).and(startTime);\n  }\n\n  // Scala API\n  public LogstashMarker apply(SpanInfo spanInfo) {\n    return create(spanInfo);\n  }\n\n  protected LogstashMarker[] generateMarkers(SpanInfo spanInfo) {\n    LogstashMarker nameMarker = Markers.append(\"name\", spanInfo.name());\n    LogstashMarker spanIdMarker = Markers.append(\"trace.span_id\", spanInfo.spanId());\n    LogstashMarker parentIdMarker = Markers.append(\"trace.parent_id\", spanInfo.parentId());\n    LogstashMarker traceIdMarker = Markers.append(\"trace.trace_id\", spanInfo.traceId());\n    LogstashMarker serviceNameMarker = Markers.append(\"service_name\", spanInfo.serviceName());\n    LogstashMarker durationMs = Markers.append(\"duration_ms\", spanInfo.duration().toMillis());\n    LogstashMarker[] markers = {\n      nameMarker, spanIdMarker, parentIdMarker, traceIdMarker, serviceNameMarker, durationMs\n    };\n    return markers;\n  }\n}\n"
  },
  {
    "path": "logback-tracing/src/main/java/com/tersesystems/logback/tracing/Tracer.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.tracing;\n\nimport java.util.ArrayDeque;\nimport java.util.Deque;\nimport java.util.Optional;\nimport java.util.function.Supplier;\n\npublic class Tracer {\n\n  // We don't want to measure a stack more than 300 elements deep, because after that it's just no\n  // fun.\n  private static final int MAX_THREAD_SIZE = 300;\n\n  private static final ThreadLocal<Deque<SpanInfo>> threadLocal =\n      ThreadLocal.withInitial(() -> new ArrayDeque<>(MAX_THREAD_SIZE));\n\n  private static Deque<SpanInfo> stack() {\n    return threadLocal.get();\n  }\n\n  public static Optional<SpanInfo> popSpan() {\n    return Optional.ofNullable(stack().poll());\n  }\n\n  /**\n   * Pushes the event onto the stack, using a parent id.\n   *\n   * <p>If there is no span or trace, then return empty.\n   *\n   * @param name the name of the span.\n   * @return the event if it was successfully added, otherwise empty.\n   */\n  public static Optional<EventInfo> pushEvent(String name) {\n    Deque<SpanInfo> stack = stack();\n    SpanInfo parent = stack.peek();\n\n    if (parent == null) {\n      return Optional.empty();\n    } else {\n      EventInfo info =\n          EventInfo.builder()\n              .setName(name)\n              .setTraceId(parent.traceId())\n              .setParentId(parent.spanId())\n              .build();\n      return Optional.of(info);\n    }\n  }\n\n  /**\n   * Creates a span, using the parent, and adds it to the stack.\n   *\n   * @param name the name of the span.\n   * @param serviceName the service name, only needed if this is the root span.\n   * @param idGenerator the span's id generator.\n   * @return the span if it was successfully added, otherwise empty.\n   */\n  public static Optional<SpanInfo> pushSpan(\n      String name, String serviceName, Supplier<String> idGenerator) {\n    Deque<SpanInfo> stack = stack();\n    SpanInfo parent = stack.peek();\n\n    SpanInfo span;\n    if (parent != null) {\n      span = parent.childBuilder().setName(name).buildNow();\n    } else {\n      span =\n          SpanInfo.builder().setRootSpan(idGenerator, name).setServiceName(serviceName).buildNow();\n    }\n    if (stack.offerFirst(span)) {\n      return Optional.of(span);\n    } else {\n      return Optional.empty();\n    }\n  }\n\n  public static Optional<SpanInfo> activeSpan() {\n    Deque<SpanInfo> stack = stack();\n    return !stack.isEmpty() ? Optional.ofNullable(stack.peek()) : Optional.empty();\n  }\n\n  public static void clear() {\n    stack().clear();\n  }\n}\n"
  },
  {
    "path": "logback-turbomarker/gradle.properties",
    "content": "#\n# SPDX-License-Identifier: CC0-1.0\n#\n# Copyright 2018-2020 Will Sargent.\n#\n# Licensed under the CC0 Public Domain Dedication;\n# You may obtain a copy of the License at\n#\n#  http://creativecommons.org/publicdomain/zero/1.0/\n#\n\nproject_description = Logback TurboMarker"
  },
  {
    "path": "logback-turbomarker/logback-turbomarker.gradle",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\ndependencies {\n    implementation project(':logback-classic')\n    implementation \"ch.qos.logback:logback-classic:$logbackVersion\"\n\n    // https://mvnrepository.com/artifact/org.mockito/mockito-core\n    testImplementation group: 'org.mockito', name: 'mockito-core', version: '3.0.0'\n    testImplementation \"com.launchdarkly:launchdarkly-java-server-sdk:4.6.6\"\n    testImplementation \"net.logstash.logback:logstash-logback-encoder:$logstashVersion\"\n    testImplementation \"com.typesafe:config:$configVersion\"\n    //testImplementation \"com.fasterxml.jackson.module:jackson-datatype-jdk8\"\n    //testImplementation \"com.fasterxml.jackson.module:jackson-datatype-jsr310\"\n    testImplementation 'org.apiguardian:apiguardian-api:1.1.0'\n    testImplementation group: 'com.fasterxml.jackson.core', name: 'jackson-annotations', version: '2.9.9'\n\n}\n"
  },
  {
    "path": "logback-turbomarker/src/main/java/com/tersesystems/logback/turbomarker/ContextAwareTurboFilterDecider.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.turbomarker;\n\nimport ch.qos.logback.classic.Level;\nimport ch.qos.logback.classic.Logger;\nimport ch.qos.logback.core.spi.FilterReply;\nimport org.slf4j.Marker;\n\npublic interface ContextAwareTurboFilterDecider<C> {\n  FilterReply decide(\n      ContextAwareTurboMarker<C> marker,\n      C context,\n      Marker rootMarker,\n      Logger logger,\n      Level level,\n      String format,\n      Object[] params,\n      Throwable t);\n}\n"
  },
  {
    "path": "logback-turbomarker/src/main/java/com/tersesystems/logback/turbomarker/ContextAwareTurboMarker.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.turbomarker;\n\nimport static java.util.Objects.requireNonNull;\n\nimport ch.qos.logback.classic.Level;\nimport ch.qos.logback.classic.Logger;\nimport ch.qos.logback.core.spi.FilterReply;\nimport com.tersesystems.logback.classic.TurboFilterDecider;\nimport org.slf4j.Marker;\n\n/**\n * This class passes through a custom application context and a matcher, which makes the ultimate\n * decision.\n *\n * @param <C> the context of the predicate marker.\n */\npublic class ContextAwareTurboMarker<C> extends TurboMarker implements TurboFilterDecider {\n\n  private final C context;\n  private final ContextAwareTurboFilterDecider<C> contextAwareDecider;\n\n  public ContextAwareTurboMarker(\n      String name, C context, ContextAwareTurboFilterDecider<C> decider) {\n    super(name);\n    this.context = requireNonNull(context);\n    this.contextAwareDecider = requireNonNull(decider);\n  }\n\n  ContextAwareTurboFilterDecider<C> getContextAwareDecider() {\n    return contextAwareDecider;\n  }\n\n  C getContext() {\n    return context;\n  }\n\n  @Override\n  public FilterReply decide(\n      Marker rootMarker, Logger logger, Level level, String format, Object[] params, Throwable t) {\n    return contextAwareDecider.decide(this, context, rootMarker, logger, level, format, params, t);\n  }\n}\n"
  },
  {
    "path": "logback-turbomarker/src/main/java/com/tersesystems/logback/turbomarker/ContextDecider.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.turbomarker;\n\nimport ch.qos.logback.classic.Level;\nimport ch.qos.logback.classic.Logger;\nimport ch.qos.logback.core.spi.FilterReply;\nimport java.util.function.Function;\nimport org.slf4j.Marker;\n\npublic interface ContextDecider<C>\n    extends Function<C, FilterReply>, ContextAwareTurboFilterDecider<C> {\n  @Override\n  default FilterReply decide(\n      ContextAwareTurboMarker<C> marker,\n      C context,\n      Marker rootMarker,\n      Logger logger,\n      Level level,\n      String format,\n      Object[] params,\n      Throwable t) {\n    return apply(context);\n  }\n}\n"
  },
  {
    "path": "logback-turbomarker/src/main/java/com/tersesystems/logback/turbomarker/LoggerContextDecider.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.turbomarker;\n\nimport ch.qos.logback.classic.Level;\nimport ch.qos.logback.classic.Logger;\nimport ch.qos.logback.core.spi.FilterReply;\nimport java.util.function.BiFunction;\nimport org.slf4j.Marker;\n\n@FunctionalInterface\npublic interface LoggerContextDecider<C>\n    extends BiFunction<Logger, C, FilterReply>, ContextAwareTurboFilterDecider<C> {\n  default FilterReply decide(\n      ContextAwareTurboMarker<C> marker,\n      C context,\n      Marker rootMarker,\n      Logger logger,\n      Level level,\n      String format,\n      Object[] params,\n      Throwable t) {\n    return apply(logger, context);\n  }\n}\n"
  },
  {
    "path": "logback-turbomarker/src/main/java/com/tersesystems/logback/turbomarker/MarkerContextDecider.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.turbomarker;\n\nimport ch.qos.logback.classic.Level;\nimport ch.qos.logback.classic.Logger;\nimport ch.qos.logback.core.spi.FilterReply;\nimport java.util.function.BiFunction;\nimport org.slf4j.Marker;\n\n@FunctionalInterface\npublic interface MarkerContextDecider<C>\n    extends BiFunction<ContextAwareTurboMarker<C>, C, FilterReply>,\n        ContextAwareTurboFilterDecider<C> {\n  @Override\n  default FilterReply decide(\n      ContextAwareTurboMarker<C> marker,\n      C context,\n      Marker rootMarker,\n      Logger logger,\n      Level level,\n      String format,\n      Object[] params,\n      Throwable t) {\n    return apply(marker, context);\n  }\n}\n"
  },
  {
    "path": "logback-turbomarker/src/main/java/com/tersesystems/logback/turbomarker/TurboMarker.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.turbomarker;\n\nimport com.tersesystems.logback.classic.TerseBasicMarker;\nimport com.tersesystems.logback.classic.TurboFilterDecider;\n\n/**\n * This class is a marker that can test to see whether an event should be allowed through a turbo\n * filter.\n */\npublic abstract class TurboMarker extends TerseBasicMarker implements TurboFilterDecider {\n  public TurboMarker(String name) {\n    super(name);\n  }\n}\n"
  },
  {
    "path": "logback-turbomarker/src/main/java/com/tersesystems/logback/turbomarker/TurboMarkerTurboFilter.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.turbomarker;\n\nimport static java.util.Objects.requireNonNull;\n\nimport ch.qos.logback.classic.Level;\nimport ch.qos.logback.classic.Logger;\nimport ch.qos.logback.classic.turbo.TurboFilter;\nimport ch.qos.logback.core.spi.FilterReply;\nimport com.tersesystems.logback.classic.TurboFilterDecider;\nimport java.util.Spliterator;\nimport java.util.Spliterators;\nimport java.util.stream.Stream;\nimport java.util.stream.StreamSupport;\nimport org.slf4j.Marker;\n\n/**\n * This class is a turbo filter that hands off the evaluation of whether a logging event should be\n * created to the marker, if it is a predicate marker.\n */\npublic class TurboMarkerTurboFilter extends TurboFilter implements TurboFilterDecider {\n\n  @Override\n  public FilterReply decide(\n      Marker rootMarker, Logger logger, Level level, String format, Object[] params, Throwable t) {\n    if (!isStarted()) {\n      return FilterReply.NEUTRAL;\n    }\n\n    if (rootMarker == null) {\n      return FilterReply.NEUTRAL;\n    }\n\n    if (evaluateMarker(rootMarker, rootMarker, logger, level, format, params, t)\n        == FilterReply.ACCEPT) {\n      return FilterReply.ACCEPT;\n    }\n\n    return stream(rootMarker)\n        .map(m -> evaluateMarker(m, rootMarker, logger, level, format, params, t))\n        .filter(reply -> reply != FilterReply.NEUTRAL)\n        .findFirst()\n        .orElse(FilterReply.NEUTRAL);\n  }\n\n  private FilterReply evaluateMarker(\n      Marker marker,\n      Marker rootMarker,\n      Logger logger,\n      Level level,\n      String format,\n      Object[] params,\n      Throwable t) {\n    if (marker instanceof TurboFilterDecider) {\n      TurboFilterDecider decider = (TurboFilterDecider) marker;\n      return decider.decide(rootMarker, logger, level, format, params, t);\n    }\n    return FilterReply.NEUTRAL;\n  }\n\n  @SuppressWarnings(\"unchecked\")\n  private Stream<Marker> stream(Marker marker) {\n    requireNonNull(marker);\n    Spliterator spliterator = Spliterators.spliteratorUnknownSize(marker.iterator(), 0);\n    return (Stream<Marker>) StreamSupport.stream(spliterator, false);\n  }\n}\n"
  },
  {
    "path": "logback-turbomarker/src/test/java/com/tersesystems/logback/turbomarker/ApplicationContext.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.turbomarker;\n\npublic class ApplicationContext {\n\n  private final String userId;\n\n  public ApplicationContext(String userId) {\n    this.userId = userId;\n  }\n\n  public String currentUserId() {\n    return userId;\n  }\n}\n"
  },
  {
    "path": "logback-turbomarker/src/test/java/com/tersesystems/logback/turbomarker/DiagnosticLoggingExample.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.turbomarker;\n\nimport static java.util.Collections.singletonList;\nimport static net.logstash.logback.argument.StructuredArguments.kv;\n\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.launchdarkly.client.LDClient;\nimport com.launchdarkly.client.LDClientInterface;\nimport com.launchdarkly.client.LDUser;\nimport com.typesafe.config.Config;\nimport com.typesafe.config.ConfigFactory;\nimport net.logstash.logback.argument.StructuredArgument;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.slf4j.Marker;\n\npublic class DiagnosticLoggingExample {\n\n  public static void main(String[] args) {\n    Config config = ConfigFactory.load();\n    LDClientInterface client = new LDClient(config.getString(\"launchdarkly.sdkkey\"));\n    Logger logger = LoggerFactory.getLogger(Order.class);\n    LDMarkerFactory markerFactory = new LDMarkerFactory(client);\n    LDUser ldUser =\n        new LDUser.Builder(\"UNIQUE IDENTIFIER\")\n            .firstName(\"Bob\")\n            .lastName(\"Loblaw\")\n            .customString(\"groups\", singletonList(\"beta_testers\"))\n            .build();\n    Marker marker = markerFactory.create(\"diagnostics-order\", ldUser);\n    OrderDiagnosticLogging diagnostics = new OrderDiagnosticLogging(logger, marker);\n    Order order = new Order(\"id1337\", diagnostics);\n    order.addToCart(new LineItem());\n    order.addPayment(new Payment());\n    order.addShipping(new Shipping());\n    order.checkout();\n    order.fulfill();\n    order.complete();\n  }\n\n  static class Order {\n    private final OrderDiagnosticLogging diagnostics;\n\n    @JsonProperty(\"id\") // Make available to logstash-logback-encoder\n    private final String id;\n\n    public Order(String id, OrderDiagnosticLogging diagnostics) {\n      this.id = id;\n      this.diagnostics = diagnostics;\n    }\n\n    public String getId() {\n      return id;\n    }\n\n    public void addToCart(LineItem lineItem) {\n      diagnostics.reportAddToCart(this, lineItem);\n    }\n\n    public void addPayment(Payment payment) {\n      diagnostics.reportAddPayment(this, payment);\n    }\n\n    public void addShipping(Shipping shipping) {\n      diagnostics.reportAddShipping(this, shipping);\n    }\n\n    public void checkout() {\n      diagnostics.reportCheckout(this);\n    }\n\n    public void fulfill() {\n      diagnostics.reportFulfill(this);\n    }\n\n    public void complete() {\n      diagnostics.reportComplete(this);\n    }\n\n    @Override\n    public String toString() {\n      return String.format(\"Order(id = %s)\", id);\n    }\n  }\n\n  static class OrderDiagnosticLogging {\n    private final Logger logger;\n    private final Marker marker;\n\n    OrderDiagnosticLogging(Logger logger, Marker marker) {\n      this.logger = logger;\n      this.marker = marker;\n    }\n\n    void reportAddToCart(Order order, LineItem lineItem) {\n      reportArg(\"addToCart\", order, kv(\"lineItem\", lineItem));\n    }\n\n    void reportAddPayment(Order order, Payment payment) {\n      reportArg(\"addPayment\", order, kv(\"payment\", payment));\n    }\n\n    void reportAddShipping(Order order, Shipping shipping) {\n      reportArg(\"addShipping\", order, kv(\"shipping\", shipping));\n    }\n\n    void reportCheckout(Order order) {\n      report(\"checkout\", order);\n    }\n\n    void reportFulfill(Order order) {\n      report(\"fulfill\", order);\n    }\n\n    void reportComplete(Order order) {\n      report(\"fulfill\", order);\n    }\n\n    private void reportArg(String methodName, Order order, StructuredArgument arg) {\n      if (logger.isDebugEnabled(marker)) {\n        logger.debug(marker, \"{}: {}, {}\", kv(\"method\", methodName), kv(\"order\", order), arg);\n      }\n    }\n\n    private void report(String methodName, Order order) {\n      if (logger.isDebugEnabled(marker)) {\n        logger.debug(marker, \"{}: {}\", kv(\"method\", methodName), kv(\"order\", order));\n      }\n    }\n  }\n\n  private static class Payment {\n    @Override\n    public String toString() {\n      return \"Payment()\";\n    }\n  }\n\n  private static class Shipping {\n\n    @Override\n    public String toString() {\n      return \"Shipping()\";\n    }\n  }\n\n  private static class LineItem {\n\n    @Override\n    public String toString() {\n      return \"LineItem()\";\n    }\n  }\n}\n"
  },
  {
    "path": "logback-turbomarker/src/test/java/com/tersesystems/logback/turbomarker/LDMarkerFactory.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\n\npackage com.tersesystems.logback.turbomarker;\n\nimport static java.util.Objects.requireNonNull;\n\nimport ch.qos.logback.core.spi.FilterReply;\nimport com.launchdarkly.client.LDClientInterface;\nimport com.launchdarkly.client.LDUser;\n\npublic class LDMarkerFactory {\n  private final LaunchDarklyDecider decider;\n\n  public LDMarkerFactory(LDClientInterface client) {\n    this.decider = new LaunchDarklyDecider(requireNonNull(client));\n  }\n\n  public LDMarker create(String featureFlag, LDUser user) {\n    return new LDMarker(featureFlag, user, decider);\n  }\n\n  static class LaunchDarklyDecider implements MarkerContextDecider<LDUser> {\n    private final LDClientInterface ldClient;\n\n    LaunchDarklyDecider(LDClientInterface ldClient) {\n      this.ldClient = ldClient;\n    }\n\n    @Override\n    public FilterReply apply(ContextAwareTurboMarker<LDUser> marker, LDUser ldUser) {\n      return ldClient.boolVariation(marker.getName(), ldUser, false)\n          ? FilterReply.ACCEPT\n          : FilterReply.NEUTRAL;\n    }\n  }\n\n  public static class LDMarker extends ContextAwareTurboMarker<LDUser> {\n    LDMarker(String name, LDUser context, ContextAwareTurboFilterDecider<LDUser> decider) {\n      super(name, context, decider);\n    }\n  }\n}\n"
  },
  {
    "path": "logback-turbomarker/src/test/java/com/tersesystems/logback/turbomarker/LDMarkerTest.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.turbomarker;\n\nimport static java.util.Collections.singletonList;\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.mockito.ArgumentMatchers.*;\nimport static org.mockito.Mockito.when;\n\nimport ch.qos.logback.classic.LoggerContext;\nimport ch.qos.logback.classic.spi.ILoggingEvent;\nimport ch.qos.logback.core.read.ListAppender;\nimport com.launchdarkly.client.LDClientInterface;\nimport com.launchdarkly.client.LDUser;\nimport org.junit.jupiter.api.DisplayName;\nimport org.junit.jupiter.api.Test;\nimport org.mockito.Mockito;\nimport org.slf4j.LoggerFactory;\n\npublic class LDMarkerTest {\n\n  @Test\n  @DisplayName(\"Matching Marker\")\n  public void testMatchingMarker() {\n    LDClientInterface client = Mockito.mock(LDClientInterface.class);\n    when(client.boolVariation(anyString(), any(), anyBoolean())).thenReturn(true);\n\n    LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory();\n    ch.qos.logback.classic.Logger logger =\n        loggerContext.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME);\n\n    LDMarkerFactory markerFactory = new LDMarkerFactory(client);\n    LDUser ldUser =\n        new LDUser.Builder(\"UNIQUE IDENTIFIER\")\n            .firstName(\"Bob\")\n            .lastName(\"Loblaw\")\n            .customString(\"groups\", singletonList(\"beta_testers\"))\n            .build();\n\n    // Register the user if not already seen\n    client.identify(ldUser);\n\n    LDMarkerFactory.LDMarker ldMarker = markerFactory.create(\"turbomarker\", ldUser);\n\n    logger.info(ldMarker, \"Hello world, I am info\");\n    logger.debug(ldMarker, \"Hello world, I am debug\");\n\n    ListAppender<ILoggingEvent> appender = (ListAppender<ILoggingEvent>) logger.getAppender(\"LIST\");\n    assertThat(appender.list.size()).isEqualTo(2);\n\n    appender.list.clear();\n  }\n\n  @Test\n  @DisplayName(\"Non Matching Marker\")\n  public void testNonMatchingUserMarker() {\n    LDClientInterface client = Mockito.mock(LDClientInterface.class);\n    when(client.boolVariation(anyString(), any(), anyBoolean())).thenReturn(false);\n\n    LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory();\n    ch.qos.logback.classic.Logger logger =\n        loggerContext.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME);\n\n    LDMarkerFactory markerFactory = new LDMarkerFactory(client);\n    LDUser ldUser = new LDUser.Builder(\"NON_MATCHING\").firstName(\"Not\").lastName(\"Beta\").build();\n\n    // Register the user if not already seen\n    client.identify(ldUser);\n\n    LDMarkerFactory.LDMarker ldMarker = markerFactory.create(\"turbomarker\", ldUser);\n    logger.info(ldMarker, \"Hello world, I am info\");\n    logger.debug(ldMarker, \"Hello world, I am debug\");\n\n    ListAppender<ILoggingEvent> appender = (ListAppender<ILoggingEvent>) logger.getAppender(\"LIST\");\n    assertThat(appender.list.size()).isEqualTo(0);\n\n    appender.list.clear();\n  }\n}\n"
  },
  {
    "path": "logback-turbomarker/src/test/java/com/tersesystems/logback/turbomarker/UserMarker.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.turbomarker;\n\npublic class UserMarker extends ContextAwareTurboMarker<ApplicationContext> {\n  public UserMarker(\n      String name,\n      ApplicationContext applicationContext,\n      ContextAwareTurboFilterDecider<ApplicationContext> decider) {\n    super(name, applicationContext, decider);\n  }\n}\n"
  },
  {
    "path": "logback-turbomarker/src/test/java/com/tersesystems/logback/turbomarker/UserMarkerFactory.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.turbomarker;\n\nimport ch.qos.logback.core.spi.FilterReply;\nimport java.util.Set;\nimport java.util.concurrent.ConcurrentSkipListSet;\n\npublic class UserMarkerFactory {\n\n  private final Set<String> userIdSet = new ConcurrentSkipListSet<>();\n\n  private final ContextDecider<ApplicationContext> decider =\n      context ->\n          userIdSet.contains(context.currentUserId()) ? FilterReply.ACCEPT : FilterReply.NEUTRAL;\n\n  public void addUserId(String userId) {\n    userIdSet.add(userId);\n  }\n\n  public void clear() {\n    userIdSet.clear();\n  }\n\n  public UserMarker create(ApplicationContext applicationContext) {\n    return new UserMarker(\"userMarker\", applicationContext, decider);\n  }\n}\n"
  },
  {
    "path": "logback-turbomarker/src/test/java/com/tersesystems/logback/turbomarker/UserMarkerTest.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.turbomarker;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport ch.qos.logback.classic.LoggerContext;\nimport ch.qos.logback.classic.spi.ILoggingEvent;\nimport ch.qos.logback.core.joran.spi.JoranException;\nimport ch.qos.logback.core.read.ListAppender;\nimport org.junit.Test;\nimport org.slf4j.LoggerFactory;\n\npublic class UserMarkerTest {\n\n  @Test\n  public void testMatchingUserMarker() throws JoranException {\n    LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory();\n    ch.qos.logback.classic.Logger logger =\n        loggerContext.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME);\n\n    String userId = \"28\";\n    ApplicationContext applicationContext = new ApplicationContext(userId);\n    UserMarkerFactory userMarkerFactory = new UserMarkerFactory();\n    userMarkerFactory.addUserId(userId); // say we want logging events created for this user id\n\n    UserMarker userMarker = userMarkerFactory.create(applicationContext);\n\n    logger.info(userMarker, \"Hello world, I am info\");\n    logger.debug(userMarker, \"Hello world, I am debug\");\n\n    ListAppender<ILoggingEvent> appender = (ListAppender<ILoggingEvent>) logger.getAppender(\"LIST\");\n    assertThat(appender.list.size()).isEqualTo(2);\n\n    appender.list.clear();\n  }\n\n  @Test\n  public void testNonMatchingUserMarker() throws JoranException {\n    LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory();\n    ch.qos.logback.classic.Logger logger =\n        loggerContext.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME);\n\n    String userId = \"28\";\n    ApplicationContext applicationContext = new ApplicationContext(userId);\n    UserMarkerFactory userMarkerFactory = new UserMarkerFactory();\n    UserMarker userMatchMarker = userMarkerFactory.create(applicationContext);\n\n    logger.info(userMatchMarker, \"Hello world, I am info\");\n    logger.debug(userMatchMarker, \"Hello world, I am debug\");\n\n    ListAppender<ILoggingEvent> appender = (ListAppender<ILoggingEvent>) logger.getAppender(\"LIST\");\n    assertThat(appender.list.size()).isEqualTo(0);\n\n    appender.list.clear();\n  }\n}\n"
  },
  {
    "path": "logback-turbomarker/src/test/resources/logback-test.xml",
    "content": "<!--\n  ~ SPDX-License-Identifier: CC0-1.0\n  ~\n  ~ Copyright 2018-2020 Will Sargent.\n  ~\n  ~ Licensed under the CC0 Public Domain Dedication;\n  ~ You may obtain a copy of the License at\n  ~\n  ~  http://creativecommons.org/publicdomain/zero/1.0/\n  -->\n<configuration>\n\n    <turboFilter class=\"com.tersesystems.logback.turbomarker.TurboMarkerTurboFilter\">\n\n    </turboFilter>\n\n    <appender name=\"LIST\" class=\"ch.qos.logback.core.read.ListAppender\">\n    </appender>\n\n    <appender name=\"CONSOLE\" class=\"ch.qos.logback.core.ConsoleAppender\">\n        <encoder class=\"net.logstash.logback.encoder.LogstashEncoder\">\n        </encoder>\n    </appender>\n\n    <root level=\"ERROR\">\n        <appender-ref ref=\"LIST\" />\n        <appender-ref ref=\"CONSOLE\" />\n    </root>\n</configuration>\n\n"
  },
  {
    "path": "logback-typesafe-config/gradle.properties",
    "content": "#\n# SPDX-License-Identifier: CC0-1.0\n#\n# Copyright 2018-2020 Will Sargent.\n#\n# Licensed under the CC0 Public Domain Dedication;\n# You may obtain a copy of the License at\n#\n#  http://creativecommons.org/publicdomain/zero/1.0/\n#\n\nproject_description = Logback using Typesafe Config for configuration"
  },
  {
    "path": "logback-typesafe-config/logback-typesafe-config.gradle",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\nplugins {\n    id 'java-library'\n}\n\ndependencies {\n    api \"ch.qos.logback:logback-classic:$logbackVersion\"\n    api \"com.typesafe:config:$configVersion\"\n}\n"
  },
  {
    "path": "logback-typesafe-config/src/main/java/com/tersesystems/logback/typesafeconfig/ConfigConstants.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.typesafeconfig;\n\n/** Constants having to do with typesafe config. */\npublic final class ConfigConstants {\n\n  public static final String TYPESAFE_CONFIG_CTX_KEY = \"typesafeConfig\";\n\n  public static final String LEVELS_KEY = \"levels\";\n\n  public static final String LOGBACK = \"logback\";\n\n  public static final String LOGBACK_TEST = \"logback-test\";\n\n  public static final String LOGBACK_REFERENCE_CONF = \"logback-reference.conf\";\n\n  public static final String CONFIG_FILE_PROPERTY = \"terse.logback.configurationFile\";\n\n  public static final String CONTEXT_SCOPE = \"context\";\n\n  public static final String LOCAL_SCOPE = \"local\";\n\n  public static final String PATH_ATTRIBUTE = \"path\";\n\n  public static final String DEBUG_ATTRIBUTE = \"debug\";\n}\n"
  },
  {
    "path": "logback-typesafe-config/src/main/java/com/tersesystems/logback/typesafeconfig/ConfigConversion.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.typesafeconfig;\n\nimport ch.qos.logback.core.spi.ContextAware;\nimport com.typesafe.config.Config;\nimport com.typesafe.config.ConfigException;\nimport com.typesafe.config.ConfigValue;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.Set;\n\npublic interface ConfigConversion extends ContextAware {\n\n  default Map<String, String> configAsMap(Config levelsConfig) {\n    Map<String, String> levelsMap = new HashMap<>();\n    Set<Map.Entry<String, ConfigValue>> levelsEntrySet = levelsConfig.entrySet();\n    for (Map.Entry<String, ConfigValue> entry : levelsEntrySet) {\n      String name = entry.getKey();\n      try {\n        String levelFromConfig = entry.getValue().unwrapped().toString();\n        levelsMap.put(name, levelFromConfig);\n      } catch (ConfigException.Missing e) {\n        addInfo(\"No custom setting found for \" + name + \" in config, ignoring\");\n      } catch (Exception e) {\n        addError(\"Unexpected exception resolving \" + name, e);\n      }\n    }\n    return levelsMap;\n  }\n}\n"
  },
  {
    "path": "logback-typesafe-config/src/main/java/com/tersesystems/logback/typesafeconfig/ConfigListConverter.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.typesafeconfig;\n\nimport ch.qos.logback.classic.pattern.ClassicConverter;\nimport ch.qos.logback.classic.spi.ILoggingEvent;\nimport com.typesafe.config.*;\nimport java.util.List;\n\n/**\n * Queries a list in typesafe config by specifying the full path and index.\n *\n * <p>This is a means of working around <a\n * href=\"https://github.com/lightbend/config/issues/30\">#30</a>.\n *\n * <p>You must have a typesafe config in context, usually through typesafeConfigAction.\n *\n * <pre>{@code\n * <conversionRule conversionWord=\"configList\"\n *   converterClass=\"com.tersesystems.logback.typesafeconfig.ConfigListConverter\" />\n * }</pre>\n *\n * And then define the option list in the layout as the path and the index:\n *\n * <pre>{@code\n * %configList{some.property.array,2}\n * }</pre>\n */\npublic class ConfigListConverter extends ClassicConverter {\n  @Override\n  public String convert(ILoggingEvent event) {\n\n    List<String> options = getOptionList();\n    String path = options.get(0);\n    String index = options.get(1);\n    try {\n      Config config = (Config) getContext().getObject(ConfigConstants.TYPESAFE_CONFIG_CTX_KEY);\n      if (path == null) {\n        addError(\"No option found - you must specify property as %config{some.property.array,0} \");\n        return \"%PARSER_ERROR\";\n      }\n\n      ConfigList configList = config.getList(path);\n      ConfigValue configValue = configList.get(Integer.parseInt(index));\n      return configValue.unwrapped().toString();\n    } catch (ConfigException e) {\n      addError(\n          String.format(\n              \"Exception rendering path %s, index %s, exception %s\", path, index, e.getMessage()));\n      return \"%PARSER_ERROR\";\n    }\n  }\n}\n"
  },
  {
    "path": "logback-typesafe-config/src/main/java/com/tersesystems/logback/typesafeconfig/TypesafeConfigAction.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.typesafeconfig;\n\nimport static com.tersesystems.logback.typesafeconfig.ConfigConstants.*;\n\nimport ch.qos.logback.core.Context;\nimport ch.qos.logback.core.joran.action.Action;\nimport ch.qos.logback.core.joran.action.ActionUtil;\nimport ch.qos.logback.core.joran.spi.ActionException;\nimport ch.qos.logback.core.joran.spi.ElementSelector;\nimport ch.qos.logback.core.joran.spi.InterpretationContext;\nimport ch.qos.logback.core.joran.spi.RuleStore;\nimport ch.qos.logback.core.util.OptionHelper;\nimport com.typesafe.config.*;\nimport java.io.File;\nimport java.util.Map;\nimport java.util.Set;\nimport org.xml.sax.Attributes;\n\n/**\n * This class reads in configuration from a series of files using <a\n * href=\"https://github.com/lightbend/config/blob/master/README.md\">Typesafe Config</a>, an easy to\n * use configuration library.\n *\n * <p>A property is resolved in the following resources in order of priority. If there is no setting\n * found, it will fall back to the next available resource, which is\n *\n * <ul>\n *   <li>System Properties\n *   <li>-Dterse.logback.configurationFile=somefile.conf\n *   <li>logback.conf\n *   <li>logback-test.conf\n *   <li>logback-reference.conf\n * </ul>\n *\n * The configuration will be available in the LoggerContext's object map, so you can use it from\n * your own code.\n *\n * <pre>{@code\n * LoggerContext context = (LoggerContext) org.slf4j.LoggerFactory.getILoggerFactory();\n * com.typesafe.config.Config config = (com.typesafe.config.Config) context.getObject(\"config\");\n * }</pre>\n *\n * This action will also set up the levels for a \"setLoggingLevelsAction\" used in terse-logback\n * core.\n *\n * <pre>{@code\n * context.putObject(LEVELS_KEY, levelsMap);\n * }</pre>\n *\n * You may want to made subsections of config available to other actions and components. You can use\n * the {@code object} action for this.\n *\n * <pre>{@code\n * <typesafeConfig>\n *   <object name=\"contextObjectFoo\" path=\"some.random.path\" scope=\"context\"/>\n * </typesafeConfig>\n * }</pre>\n *\n * which will do a {@code context.putObject(\"contextObjectFoo\", pathValue); }\n */\npublic class TypesafeConfigAction extends Action implements ConfigConversion {\n\n  @Override\n  public void begin(InterpretationContext ic, String name, Attributes attributes)\n      throws ActionException {\n    RuleStore ruleStore = ic.getJoranInterpreter().getRuleStore();\n\n    ruleStore.addRule(\n        new ElementSelector(\"configuration/\" + name + \"/object\"), new ContextObjectAction());\n\n    String debugAttr = attributes.getValue(DEBUG_ATTRIBUTE);\n\n    Config config = generateConfig(ic.getClass().getClassLoader(), Boolean.valueOf(debugAttr));\n    Context context = ic.getContext();\n\n    context.putObject(TYPESAFE_CONFIG_CTX_KEY, config);\n    ic.getObjectMap().put(TYPESAFE_CONFIG_CTX_KEY, config);\n    configureLevels(config);\n\n    try {\n      Set<Map.Entry<String, ConfigValue>> contextProperties =\n          config.getConfig(CONTEXT_SCOPE).entrySet();\n      configureContextScope(config, context, contextProperties);\n    } catch (ConfigException.Missing e) {\n      // do nothing\n    }\n\n    try {\n      Set<Map.Entry<String, ConfigValue>> localProperties =\n          config.getConfig(LOCAL_SCOPE).entrySet();\n      configureLocalScope(config, ic, localProperties);\n    } catch (ConfigException.Missing e) {\n      // do nothing\n    }\n  }\n\n  protected void configureLevels(Config config) {\n    // Try to set up the levels as they're important...\n    try {\n      Map<String, String> levelsMap = configAsMap(config.getConfig(LEVELS_KEY));\n      context.putObject(LEVELS_KEY, levelsMap);\n    } catch (ConfigException e) {\n      addWarn(\"Cannot set levels in context!\", e);\n    }\n  }\n\n  @Override\n  public void end(InterpretationContext ic, String name) throws ActionException {}\n\n  protected void configureContextScope(\n      Config config, Context lc, Set<Map.Entry<String, ConfigValue>> properties) {\n    for (Map.Entry<String, ConfigValue> propertyEntry : properties) {\n      String key = propertyEntry.getKey();\n      String value = propertyEntry.getValue().unwrapped().toString();\n      lc.putProperty(key, value);\n    }\n  }\n\n  protected void configureLocalScope(\n      Config config, InterpretationContext ic, Set<Map.Entry<String, ConfigValue>> properties) {\n    for (Map.Entry<String, ConfigValue> propertyEntry : properties) {\n      String key = propertyEntry.getKey();\n      String value = propertyEntry.getValue().unwrapped().toString();\n      ic.addSubstitutionProperty(key, value);\n    }\n  }\n\n  protected Config generateConfig(ClassLoader classLoader, boolean debug) {\n    // Look for logback.json, logback.conf, logback.properties\n    Config systemProperties = ConfigFactory.systemProperties();\n    String fileName = System.getProperty(CONFIG_FILE_PROPERTY);\n    Config file = ConfigFactory.empty();\n    if (fileName != null) {\n      file = ConfigFactory.parseFile(new File(fileName));\n    }\n\n    Config testResources = ConfigFactory.parseResourcesAnySyntax(classLoader, LOGBACK_TEST);\n    Config resources = ConfigFactory.parseResourcesAnySyntax(classLoader, LOGBACK);\n    Config reference = ConfigFactory.parseResources(classLoader, LOGBACK_REFERENCE_CONF);\n\n    Config config =\n        systemProperties // Look for a property from system properties first...\n            .withFallback(file) // if we don't find it, then look in an explicitly defined file...\n            .withFallback(\n                testResources) // if not, then if logback-test.conf exists, look for it there...\n            .withFallback(resources) // then look in logback.conf...\n            .withFallback(reference) // and then finally in logback-reference.conf.\n            .resolve(); // Tell config that we want to use ${?ENV_VAR} type stuff.\n\n    // Add a check to show the config value if nothing is working...\n    if (debug) {\n      String configString = config.root().render(ConfigRenderOptions.defaults());\n      addInfo(configString);\n    }\n    return config;\n  }\n\n  /**\n   * Lets you put objects into the context's object map, as the correct type, using typesafe config\n   * paths as the source.\n   */\n  public static class ContextObjectAction extends Action implements ConfigConversion {\n    private String nameAttr;\n    private String path;\n    private ActionUtil.Scope scope;\n\n    static ActionUtil.Scope stringToScope(String scopeStr) {\n      if (ActionUtil.Scope.LOCAL.toString().equalsIgnoreCase(scopeStr))\n        return ActionUtil.Scope.LOCAL;\n      if (ActionUtil.Scope.CONTEXT.toString().equalsIgnoreCase(scopeStr))\n        return ActionUtil.Scope.CONTEXT;\n\n      // default to context.\n      return ActionUtil.Scope.CONTEXT;\n    }\n\n    Config resolveConfig(InterpretationContext ic) {\n      Config config = (Config) getContext().getObject(TYPESAFE_CONFIG_CTX_KEY);\n      if (config == null) {\n        config = (Config) ic.getObjectMap().get(TYPESAFE_CONFIG_CTX_KEY);\n      }\n      return config;\n    }\n\n    /**\n     * Set a new property for the execution context by name, value pair, or adds all the properties\n     * found in the given file.\n     */\n    public void begin(InterpretationContext ic, String localName, Attributes attributes) {\n      String nameAttr = attributes.getValue(NAME_ATTRIBUTE);\n      setNameAttr(nameAttr);\n      String path = attributes.getValue(PATH_ATTRIBUTE);\n      setPath(path);\n\n      ActionUtil.Scope scope = stringToScope(attributes.getValue(SCOPE_ATTRIBUTE));\n      setScope(scope);\n    }\n\n    boolean isValid(String name, String value) {\n      return !(OptionHelper.isEmpty(name) || OptionHelper.isEmpty(value));\n    }\n\n    public void end(InterpretationContext ic, String name) {\n      Config config = resolveConfig(ic);\n\n      if (isValid(nameAttr, path)) {\n        if (config == null) {\n          addError(\"No config object found in context's object map!\");\n          return;\n        }\n\n        ConfigValue configValue = null;\n        Object contextValue = null;\n        try {\n          configValue = config.getValue(path);\n          if (configValue != null) {\n            if (configValue.valueType().equals(ConfigValueType.OBJECT)) {\n              String msg = \"The value found at path %s is an object, assuming you want a map...\";\n              addInfo(String.format(msg, path));\n              contextValue = configAsMap(config.getConfig(path));\n            } else {\n              contextValue = configValue.unwrapped();\n            }\n          }\n\n          switch (scope) {\n            case LOCAL:\n              ic.getObjectMap().put(nameAttr, contextValue);\n              break;\n            case CONTEXT:\n              context.putObject(nameAttr, contextValue);\n              break;\n            case SYSTEM:\n              // never used.\n              break;\n          }\n\n        } catch (ConfigException e) {\n          addError(\n              String.format(\n                  \"Cannot set value %s typesafe config path %s to name %s\",\n                  contextValue, path, nameAttr),\n              e);\n        }\n      } else {\n        addError(\"Cannot set property, it is invalid!\");\n      }\n    }\n\n    private String getNameAttr() {\n      return this.nameAttr;\n    }\n\n    void setNameAttr(String name) {\n      this.nameAttr = name;\n    }\n\n    public void setPath(String path) {\n      this.path = path;\n    }\n\n    public void setScope(ActionUtil.Scope scope) {\n      this.scope = scope;\n    }\n  }\n}\n"
  },
  {
    "path": "logback-typesafe-config/src/test/java/com/tersesystems/logback/typesafeconfig/ConfigListConverterTest.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.typesafeconfig;\n\nimport static com.tersesystems.logback.typesafeconfig.ConfigConstants.TYPESAFE_CONFIG_CTX_KEY;\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport ch.qos.logback.classic.LoggerContext;\nimport com.typesafe.config.Config;\nimport com.typesafe.config.ConfigFactory;\nimport java.util.Arrays;\nimport org.junit.Test;\n\npublic class ConfigListConverterTest {\n\n  @Test\n  public void testConversion() {\n    LoggerContext context = new LoggerContext();\n    ConfigListConverter configValueConverter = new ConfigListConverter();\n\n    Config config = ConfigFactory.parseString(\"some.property.name=[one,two,three]\");\n    context.putObject(TYPESAFE_CONFIG_CTX_KEY, config);\n\n    configValueConverter.setContext(context);\n    configValueConverter.setOptionList(Arrays.asList(\"some.property.name\", \"1\"));\n    configValueConverter.start();\n\n    String actual = configValueConverter.convert(null);\n\n    assertThat(actual).isEqualTo(\"two\");\n  }\n}\n"
  },
  {
    "path": "logback-typesafe-config/src/test/java/com/tersesystems/logback/typesafeconfig/TypesafeConfigActionTest.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.typesafeconfig;\n\nimport static com.tersesystems.logback.typesafeconfig.ConfigConstants.LEVELS_KEY;\nimport static com.tersesystems.logback.typesafeconfig.ConfigConstants.TYPESAFE_CONFIG_CTX_KEY;\nimport static java.util.Objects.requireNonNull;\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport ch.qos.logback.classic.LoggerContext;\nimport ch.qos.logback.classic.joran.JoranConfigurator;\nimport ch.qos.logback.core.joran.spi.JoranException;\nimport com.typesafe.config.Config;\nimport org.junit.Test;\n\npublic class TypesafeConfigActionTest {\n\n  @Test\n  public void testConfigWithDefault() throws JoranException {\n    LoggerContext loggerContext = new LoggerContext();\n    JoranConfigurator jc = new JoranConfigurator();\n    jc.setContext(loggerContext);\n    jc.doConfigure(\n        requireNonNull(\n            this.getClass()\n                .getClassLoader()\n                .getResource(\"typesafeconfig/config-with-default.xml\")));\n\n    Object levels = loggerContext.getObject(LEVELS_KEY);\n    assertThat(levels).isNotNull();\n\n    Config config = (Config) loggerContext.getObject(TYPESAFE_CONFIG_CTX_KEY);\n    assertThat(config).isNotNull();\n\n    String exportedToContext = loggerContext.getProperty(\"localKey\");\n    assertThat(exportedToContext).isNull();\n\n    String exportedFoo = loggerContext.getProperty(\"exportedFoo\");\n    assertThat(exportedFoo).isEqualTo(\"bar\");\n  }\n\n  @Test\n  public void testConfigWithContext() throws JoranException {\n    LoggerContext loggerContext = new LoggerContext();\n    JoranConfigurator jc = new JoranConfigurator();\n    jc.setContext(loggerContext);\n    jc.doConfigure(\n        requireNonNull(\n            this.getClass()\n                .getClassLoader()\n                .getResource(\"typesafeconfig/config-with-context.xml\")));\n\n    Object levels = loggerContext.getObject(LEVELS_KEY);\n    assertThat(levels).isNotNull();\n\n    Config config = (Config) loggerContext.getObject(TYPESAFE_CONFIG_CTX_KEY);\n    assertThat(config).isNotNull();\n\n    String foo = loggerContext.getProperty(\"contextKey\");\n    assertThat(foo).isEqualTo(\"bar\");\n\n    String exportedFoo = loggerContext.getProperty(\"exportedFoo\");\n    assertThat(exportedFoo).isEqualTo(\"bar\");\n  }\n\n  @Test\n  public void testConfigWithLocal() throws JoranException {\n    LoggerContext loggerContext = new LoggerContext();\n    JoranConfigurator jc = new JoranConfigurator();\n    jc.setContext(loggerContext);\n    jc.doConfigure(\n        requireNonNull(\n            this.getClass().getClassLoader().getResource(\"typesafeconfig/config-with-local.xml\")));\n\n    Object levels = loggerContext.getObject(LEVELS_KEY);\n    assertThat(levels).isNotNull();\n\n    Config config = (Config) loggerContext.getObject(TYPESAFE_CONFIG_CTX_KEY);\n    assertThat(config).isNotNull();\n\n    String foo = loggerContext.getProperty(\"localKey\");\n    assertThat(foo).isNull();\n\n    String exportedFoo = loggerContext.getProperty(\"exportedFoo\");\n    assertThat(exportedFoo).isEqualTo(\"bar\");\n\n    Object contextObjectFoo = loggerContext.getObject(\"contextObjectFoo\");\n    assertThat(contextObjectFoo).isEqualTo(\"pathValue\");\n  }\n}\n"
  },
  {
    "path": "logback-typesafe-config/src/test/resources/logback-test.conf",
    "content": "levels {\n  examples = \"INFO\"\n}\n\nsome.random.path = \"pathValue\"\n\nlocal {\n  localKey = \"bar\"\n}\n\ncontext {\n  contextKey = \"bar\"\n}\n"
  },
  {
    "path": "logback-typesafe-config/src/test/resources/typesafeconfig/config-with-context.xml",
    "content": "<!--\n  ~ SPDX-License-Identifier: CC0-1.0\n  ~\n  ~ Copyright 2018-2020 Will Sargent.\n  ~\n  ~ Licensed under the CC0 Public Domain Dedication;\n  ~ You may obtain a copy of the License at\n  ~\n  ~  http://creativecommons.org/publicdomain/zero/1.0/\n  -->\n<configuration debug=\"true\">\n\n    <newRule pattern=\"configuration/typesafeConfig\"\n             actionClass=\"com.tersesystems.logback.typesafeconfig.TypesafeConfigAction\"/>\n\n    <typesafeConfig>\n        <object name=\"contextObjectFoo\" path=\"some.random.path\" scope=\"context\"/>\n    </typesafeConfig>\n\n    <property scope=\"context\" name=\"exportedFoo\" value=\"${contextKey}\" />\n\n    <appender name=\"TEST\" class=\"ch.qos.logback.core.ConsoleAppender\">\n        <encoder>\n            <pattern>[%-5level] %logger{15} - %msg%n%xException{10}</pattern>\n        </encoder>\n    </appender>\n\n    <root>\n        <appender-ref ref=\"TEST\"/>\n    </root>\n</configuration>"
  },
  {
    "path": "logback-typesafe-config/src/test/resources/typesafeconfig/config-with-default.xml",
    "content": "<!--\n  ~ SPDX-License-Identifier: CC0-1.0\n  ~\n  ~ Copyright 2018-2020 Will Sargent.\n  ~\n  ~ Licensed under the CC0 Public Domain Dedication;\n  ~ You may obtain a copy of the License at\n  ~\n  ~  http://creativecommons.org/publicdomain/zero/1.0/\n  -->\n<configuration debug=\"true\">\n\n    <newRule pattern=\"configuration/typesafeConfig\"\n             actionClass=\"com.tersesystems.logback.typesafeconfig.TypesafeConfigAction\"/>\n\n    <typesafeConfig>\n        <object name=\"contextObjectFoo\" path=\"some.random.path\"/>\n    </typesafeConfig>\n\n    <property scope=\"context\" name=\"exportedFoo\" value=\"${localKey}\" />\n\n    <appender name=\"TEST\" class=\"ch.qos.logback.core.ConsoleAppender\">\n        <encoder>\n            <pattern>[%-5level] %logger{15} - %msg%n%xException{10}</pattern>\n        </encoder>\n    </appender>\n\n    <root>\n        <appender-ref ref=\"TEST\"/>\n    </root>\n</configuration>"
  },
  {
    "path": "logback-typesafe-config/src/test/resources/typesafeconfig/config-with-local.xml",
    "content": "<!--\n  ~ SPDX-License-Identifier: CC0-1.0\n  ~\n  ~ Copyright 2018-2020 Will Sargent.\n  ~\n  ~ Licensed under the CC0 Public Domain Dedication;\n  ~ You may obtain a copy of the License at\n  ~\n  ~  http://creativecommons.org/publicdomain/zero/1.0/\n  -->\n<configuration debug=\"true\">\n\n    <newRule pattern=\"configuration/typesafeConfig\"\n             actionClass=\"com.tersesystems.logback.typesafeconfig.TypesafeConfigAction\"/>\n\n    <typesafeConfig>\n        <object name=\"contextObjectFoo\" path=\"some.random.path\"/>\n    </typesafeConfig>\n\n    <property scope=\"context\" name=\"exportedFoo\" value=\"${localKey}\" />\n\n    <appender name=\"TEST\" class=\"ch.qos.logback.core.ConsoleAppender\">\n        <encoder>\n            <pattern>[%-5level] %logger{15} - %msg%n%xException{10}</pattern>\n        </encoder>\n    </appender>\n\n    <root>\n        <appender-ref ref=\"TEST\"/>\n    </root>\n</configuration>"
  },
  {
    "path": "logback-uniqueid-appender/gradle.properties",
    "content": "#\n# SPDX-License-Identifier: CC0-1.0\n#\n# Copyright 2018-2020 Will Sargent.\n#\n# Licensed under the CC0 Public Domain Dedication;\n# You may obtain a copy of the License at\n#\n#  http://creativecommons.org/publicdomain/zero/1.0/\n#\n\nproject_description = Logback Unique ID Appender"
  },
  {
    "path": "logback-uniqueid-appender/logback-uniqueid-appender.gradle",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\ndependencies {\n    implementation project(':logback-classic')\n\n    // https://github.com/f4b6a3/ulid-creator\n    implementation 'com.github.f4b6a3:ulid-creator:5.1.0'\n\n    // https://github.com/f4b6a3/tsid-creator\n    implementation 'com.github.f4b6a3:tsid-creator:5.2.0'\n\n    // https://github.com/f4b6a3/uuid-creator\n    implementation 'com.github.f4b6a3:uuid-creator:5.2.0'\n\n    // https://github.com/f4b6a3/ksuid-creator\n    implementation 'com.github.f4b6a3:ksuid-creator:4.1.0'\n\n    // https://github.com/mguenther/idem\n    implementation 'net.mguenther.idem:idem-core:0.1.0'\n}\n"
  },
  {
    "path": "logback-uniqueid-appender/src/main/java/com/tersesystems/logback/uniqueid/FlakeIdGenerator.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.uniqueid;\n\nimport net.mguenther.idem.flake.Flake128S;\nimport net.mguenther.idem.provider.LinearTimeProvider;\nimport net.mguenther.idem.provider.MacAddressWorkerIdProvider;\n\n/**\n * This class generates a 128 bit flake id with a macaddress workerid according to <a\n * href=\"https://github.com/mguenther/idem\">https://github.com/mguenther/idem</a>.\n */\npublic class FlakeIdGenerator implements IdGenerator {\n\n  private static final Flake128S flake64 =\n      new Flake128S(new LinearTimeProvider(), new MacAddressWorkerIdProvider());\n\n  @Override\n  public String generateId() {\n    return flake64.nextId();\n  }\n}\n"
  },
  {
    "path": "logback-uniqueid-appender/src/main/java/com/tersesystems/logback/uniqueid/IdGenerator.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.uniqueid;\n\npublic interface IdGenerator {\n  String generateId();\n}\n"
  },
  {
    "path": "logback-uniqueid-appender/src/main/java/com/tersesystems/logback/uniqueid/KsuidSubsecondIdGenerator.java",
    "content": "package com.tersesystems.logback.uniqueid;\n\nimport com.github.f4b6a3.ksuid.*;\nimport java.util.Random;\nimport java.util.concurrent.ThreadLocalRandom;\n\n/**\n * Creates a subsecond KSUID according to <a\n * href=\"https://github.com/f4b6a3/ksuid-creator\">https://github.com/f4b6a3/ksuid-creator</a>.\n */\npublic class KsuidSubsecondIdGenerator implements IdGenerator {\n\n  private Random random() {\n    return ThreadLocalRandom.current();\n  }\n\n  private final KsuidFactory factory = KsuidFactory.newSubsecondInstance(() -> random().nextLong());\n\n  @Override\n  public String generateId() {\n    return factory.create().toString();\n  }\n}\n"
  },
  {
    "path": "logback-uniqueid-appender/src/main/java/com/tersesystems/logback/uniqueid/RandomUUIDIdGenerator.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.uniqueid;\n\nimport com.github.f4b6a3.uuid.factory.rfc4122.RandomBasedFactory;\nimport java.util.Random;\nimport java.util.concurrent.ThreadLocalRandom;\n\n/**\n * Generates a Random UUIDv4 using a ThreadLocalRandom from <a\n * href=\"https://github.com/f4b6a3/uuid-creator\">https://github.com/f4b6a3/uuid-creator</a>\n */\npublic class RandomUUIDIdGenerator implements IdGenerator {\n  private Random random() {\n    return ThreadLocalRandom.current();\n  }\n\n  private final RandomBasedFactory factory = new RandomBasedFactory(() -> random().nextLong());\n\n  @Override\n  public String generateId() {\n    return factory.create().toString();\n  }\n}\n"
  },
  {
    "path": "logback-uniqueid-appender/src/main/java/com/tersesystems/logback/uniqueid/TsidIdgenerator.java",
    "content": "package com.tersesystems.logback.uniqueid;\n\nimport com.github.f4b6a3.tsid.TsidFactory;\n\n/**\n * Generates a TSID according to <a\n * href=\"https://github.com/f4b6a3/tsid-creator\">https://github.com/f4b6a3/tsid-creator</a>.\n */\npublic class TsidIdgenerator implements IdGenerator {\n\n  // \"tsidcreator.node\" system property should be set,\n  // but small hope of that happening, so choose a large node count.\n  private final TsidFactory factory = TsidFactory.newInstance4096();\n\n  @Override\n  public String generateId() {\n    return factory.create().toString();\n  }\n}\n"
  },
  {
    "path": "logback-uniqueid-appender/src/main/java/com/tersesystems/logback/uniqueid/UlidIdGenerator.java",
    "content": "package com.tersesystems.logback.uniqueid;\n\nimport com.github.f4b6a3.ulid.UlidFactory;\nimport java.util.Random;\nimport java.util.concurrent.ThreadLocalRandom;\n\n/**\n * Creates a monotonic ULID using a threadlocal random according to <a\n * href=\"https://github.com/f4b6a3/ulid-creator\">https://github.com/f4b6a3/ulid-creator</a>.\n */\npublic class UlidIdGenerator implements IdGenerator {\n\n  private Random random() {\n    return ThreadLocalRandom.current();\n  }\n\n  private final UlidFactory factory = UlidFactory.newMonotonicInstance(() -> random().nextLong());\n\n  @Override\n  public String generateId() {\n    return factory.create().toString();\n  }\n}\n"
  },
  {
    "path": "logback-uniqueid-appender/src/main/java/com/tersesystems/logback/uniqueid/UniqueIdComponentAppender.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.uniqueid;\n\nimport ch.qos.logback.classic.spi.ILoggingEvent;\nimport com.tersesystems.logback.classic.ContainerProxyLoggingEvent;\nimport com.tersesystems.logback.classic.IContainerLoggingEvent;\nimport com.tersesystems.logback.core.DecoratingAppender;\n\npublic class UniqueIdComponentAppender\n    extends DecoratingAppender<ILoggingEvent, IContainerLoggingEvent> {\n\n  private IdGenerator idGenerator = new FlakeIdGenerator();\n\n  public IdGenerator getIdGenerator() {\n    return idGenerator;\n  }\n\n  public void setIdGenerator(IdGenerator idGenerator) {\n    this.idGenerator = idGenerator;\n  }\n\n  @Override\n  protected IContainerLoggingEvent decorateEvent(ILoggingEvent eventObject) {\n    IContainerLoggingEvent containerEvent;\n    if (eventObject instanceof IContainerLoggingEvent) {\n      containerEvent = (IContainerLoggingEvent) eventObject;\n    } else {\n      containerEvent = new ContainerProxyLoggingEvent(eventObject);\n    }\n    String uniqueId = idGenerator.generateId();\n    containerEvent.putComponent(UniqueIdProvider.class, () -> uniqueId);\n    return containerEvent;\n  }\n}\n"
  },
  {
    "path": "logback-uniqueid-appender/src/main/java/com/tersesystems/logback/uniqueid/UniqueIdConverter.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.uniqueid;\n\nimport ch.qos.logback.classic.spi.ILoggingEvent;\nimport ch.qos.logback.core.pattern.DynamicConverter;\nimport com.tersesystems.logback.core.ComponentContainer;\n\npublic class UniqueIdConverter extends DynamicConverter<ILoggingEvent> {\n  @Override\n  public String convert(ILoggingEvent event) {\n    if (event instanceof ComponentContainer) {\n      return ((ComponentContainer) event).getComponent(UniqueIdProvider.class).uniqueId();\n    } else {\n      return null;\n    }\n  }\n}\n"
  },
  {
    "path": "logback-uniqueid-appender/src/main/java/com/tersesystems/logback/uniqueid/UniqueIdProvider.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\n\npackage com.tersesystems.logback.uniqueid;\n\nimport com.tersesystems.logback.core.Component;\n\n/** This interface returns a unique id identifying the entity. */\npublic interface UniqueIdProvider extends Component {\n  String uniqueId();\n}\n"
  },
  {
    "path": "logback-uniqueid-appender/src/test/java/com/tersesystems/logback/uniqueid/UniqueIdAppenderTest.java",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npackage com.tersesystems.logback.uniqueid;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport ch.qos.logback.classic.Logger;\nimport ch.qos.logback.classic.LoggerContext;\nimport ch.qos.logback.classic.joran.JoranConfigurator;\nimport ch.qos.logback.classic.spi.ILoggingEvent;\nimport ch.qos.logback.core.joran.spi.JoranException;\nimport ch.qos.logback.core.read.ListAppender;\nimport com.tersesystems.logback.core.ComponentContainer;\nimport com.tersesystems.logback.core.DecoratingAppender;\nimport java.net.URL;\nimport org.junit.Test;\n\npublic class UniqueIdAppenderTest {\n\n  @Test\n  public void testUniqueIdEventAppender() throws JoranException {\n    LoggerContext context = new LoggerContext();\n    URL resource = getClass().getResource(\"/logback-with-uniqueid-appender.xml\");\n    JoranConfigurator configurator = new JoranConfigurator();\n    configurator.setContext(context);\n    configurator.doConfigure(resource);\n\n    ch.qos.logback.classic.Logger logger = context.getLogger(Logger.ROOT_LOGGER_NAME);\n\n    logger.info(\"hello world\");\n    DecoratingAppender<ILoggingEvent, ILoggingEvent> appender =\n        (DecoratingAppender<ILoggingEvent, ILoggingEvent>)\n            logger.getAppender(\"DECORATE_WITH_UNIQUEID\");\n\n    ListAppender<ILoggingEvent> listAppender =\n        (ListAppender<ILoggingEvent>) appender.getAppender(\"LIST\");\n    ILoggingEvent event = listAppender.list.get(0);\n    ComponentContainer container = (ComponentContainer) event;\n    UniqueIdProvider idComponent = container.getComponent(UniqueIdProvider.class);\n    assertThat(idComponent.uniqueId()).isNotBlank();\n  }\n}\n"
  },
  {
    "path": "logback-uniqueid-appender/src/test/resources/logback-with-uniqueid-appender.xml",
    "content": "<!--\n  ~ SPDX-License-Identifier: CC0-1.0\n  ~\n  ~ Copyright 2018-2020 Will Sargent.\n  ~\n  ~ Licensed under the CC0 Public Domain Dedication;\n  ~ You may obtain a copy of the License at\n  ~\n  ~  http://creativecommons.org/publicdomain/zero/1.0/\n  -->\n<configuration>\n\n    <conversionRule conversionWord=\"uniqueId\" converterClass=\"com.tersesystems.logback.uniqueid.UniqueIdConverter\" />\n\n    <appender name=\"DECORATE_WITH_UNIQUEID\" class=\"com.tersesystems.logback.uniqueid.UniqueIdComponentAppender\">\n        <!-- <idGenerator class=\"com.tersesystems.logback.uniqueid.TsidIdgenerator\"/>-->\n        <!--<idGenerator class=\"com.tersesystems.logback.uniqueid.UlidIdGenerator\"/>-->\n        <!--<idGenerator class=\"com.tersesystems.logback.uniqueid.KsuidSubsecondIdGenerator\"/>-->\n         <idGenerator class=\"com.tersesystems.logback.uniqueid.RandomUUIDIdGenerator\"/>\n        <appender class=\"ch.qos.logback.core.read.ListAppender\">\n            <name>LIST</name>\n        </appender>\n\n        <appender class=\"ch.qos.logback.core.ConsoleAppender\">\n            <encoder>\n                <pattern>%-5relative %-5level %uniqueId %logger{35} - %msg%n</pattern>\n            </encoder>\n        </appender>\n    </appender>\n\n    <root level=\"TRACE\">\n        <appender-ref ref=\"DECORATE_WITH_UNIQUEID\"/>\n    </root>\n</configuration>"
  },
  {
    "path": "mkdocs.yml",
    "content": "site_name: Terse Logback\n\n# Meta tags (placed in header)\nsite_description: \"Terse Logback Documentation\"\n\nsite_author: \"Will Sargent\"\nsite_url: \"https://tersesystems.github.io/terse-logback\"\n\n# Repository (add link to repository on each page)\nrepo_name: terse-logback\n\nrepo_url: \"https://github.com/tersesystems/terse-logback\"\nedit_uri: \"edit/master/docs/\"\n\n#Copyright (shown at the footer)\n#copyright: 'Copyright &copy; 2017 Your Name'\n\n# Meterial theme\n# https://github.com/squidfunk/mkdocs-material\n# pip install mkdocs-material\ntheme: 'material'\n\nextra:\n  version:\n    provider: mike\n\n  palette:\n    primary: 'indigo'\n    accent: 'indigo'\n\n  social:\n    - icon: fontawesome/brands/mastodon\n      link: 'https://mastodon.xyz/web/@will_sargent'\n\n\n# Google Analytics\n#google_analytics:\n#  - 'UA-111111111-1'\n#  - 'auto'\n\n# Extensions\nmarkdown_extensions:\n  - admonition\n  - codehilite:\n      guess_lang: false\n  - footnotes\n  - meta\n  - toc:\n      permalink: true\n  - pymdownx.betterem:\n      smart_enable: all\n  - pymdownx.caret\n  - pymdownx.inlinehilite\n  - pymdownx.magiclink\n  - pymdownx.smartsymbols\n  - pymdownx.superfences\n\nnav:\n  - Home: index.md\n  - Modules:\n      - Audio: guide/audio.md\n      - Budgeting / Rate Limiting: guide/budget.md\n      - Censors: guide/censor.md    \n      - Composite: guide/composite.md\n      - Compression: guide/compression.md\n      - Correlation Id: guide/correlationid.md\n      - Exception Mapping: guide/exception-mapping.md\n      - Instrumentation: guide/instrumentation.md\n      - JDBC: guide/jdbc.md\n      - JUL to SLF4J Bridge: guide/slf4jbridge.md\n      - Relative Nanos: guide/relativens.md\n      - Select Appender: guide/select.md\n      - Tracing with Honeycomb: guide/tracing.md\n      - Typesafe Config: guide/typesafeconfig.md\n      - Turbo Markers: guide/turbomarker.md\n      - Unique ID Appender: guide/uniqueid.md\n"
  },
  {
    "path": "settings.gradle",
    "content": "/*\n * SPDX-License-Identifier: CC0-1.0\n *\n * Copyright 2018-2020 Will Sargent.\n *\n * Licensed under the CC0 Public Domain Dedication;\n * You may obtain a copy of the License at\n *\n *  http://creativecommons.org/publicdomain/zero/1.0/\n */\npluginManagement {\n    repositories {\n        jcenter()\n        maven { url 'https://plugins.gradle.org/m2/' }\n    }\n}\n\nrootProject.name = 'terse-logback'\n\ndef includeProject = { String projectName ->\n    File projectDir = new File(settingsDir, projectName)\n    String buildFileName = \"${projectName}.gradle\"\n\n    assert projectDir.isDirectory()\n    assert new File(projectDir, buildFileName).isFile()\n\n    include projectName\n    project(\":${projectName}\").projectDir    = projectDir\n    project(\":${projectName}\").buildFileName = buildFileName\n}\n\nincludeProject 'logback-bytebuddy'\nincludeProject 'logback-censor'\nincludeProject 'logback-core'\nincludeProject 'logback-correlationid'\nincludeProject 'logback-classic'\nincludeProject 'logback-typesafe-config'\nincludeProject 'logback-audio'\nincludeProject 'logback-compress-encoder'\nincludeProject 'logback-budget'\nincludeProject 'logback-uniqueid-appender'\nincludeProject 'logback-exception-mapping'\nincludeProject 'logback-exception-mapping-providers'\nincludeProject 'logback-turbomarker'\nincludeProject 'logback-tracing'\nincludeProject 'logback-jdbc-appender'\nincludeProject 'logback-honeycomb-client'\nincludeProject 'logback-honeycomb-appender'\nincludeProject 'logback-honeycomb-okhttp'\nincludeProject 'logback-postgresjson-appender'\n\n//includeProject 'guide'\n"
  },
  {
    "path": "version.properties",
    "content": "#\n# SPDX-License-Identifier: CC0-1.0\n#\n# Copyright 2018-2020 Will Sargent.\n#\n# Licensed under the CC0 Public Domain Dedication;\n# You may obtain a copy of the License at\n#\n#  http://creativecommons.org/publicdomain/zero/1.0/\n#\n\n#Version of the produced binaries. This file is intended to be checked-in.\n#It will be automatically bumped by release automation.\nversion=1.2.*\n\n"
  }
]