├── .all-contributorsrc ├── .github ├── CODEOWNERS ├── dependabot.yml ├── project.yml └── workflows │ ├── backport.yml │ ├── build.yml │ ├── pre-release.yml │ ├── quarkus-snapshot.yaml │ ├── release-perform.yml │ └── release-prepare.yml ├── .gitignore ├── LICENSE ├── README.md ├── deployment ├── pom.xml └── src │ ├── main │ └── java │ │ └── io │ │ └── quarkiverse │ │ └── logging │ │ └── splunk │ │ ├── DevServicesLoggingSplunkProcessor.java │ │ ├── LoggingSplunkProcessor.java │ │ └── SplunkContainer.java │ └── test │ ├── java │ └── io │ │ └── quarkiverse │ │ └── logging │ │ └── splunk │ │ ├── AbstractMockServerTest.java │ │ ├── LoggingSplunkAsyncTest.java │ │ ├── LoggingSplunkDisabledTest.java │ │ ├── LoggingSplunkFilteringTest.java │ │ ├── LoggingSplunkFlatSerializationTest.java │ │ ├── LoggingSplunkMandatoryConfigTest.java │ │ ├── LoggingSplunkMinimalConfigTest.java │ │ ├── LoggingSplunkNamedHandlerConfigTest.java │ │ ├── LoggingSplunkNestedSerializationTest.java │ │ ├── LoggingSplunkRawSerializationTest.java │ │ ├── LoggingSplunkSendErrorTest.java │ │ └── LoggingSplunkSendExceptionTest.java │ └── resources │ ├── application-splunk-logging-async.properties │ ├── application-splunk-logging-disabled.properties │ ├── application-splunk-logging-failure.properties │ ├── application-splunk-logging-filtering.properties │ ├── application-splunk-logging-flat.properties │ ├── application-splunk-logging-mandatory.properties │ ├── application-splunk-logging-minimal.properties │ ├── application-splunk-logging-named-handler.properties │ ├── application-splunk-logging-nested.properties │ └── application-splunk-logging-raw.properties ├── docs ├── antora.yml ├── modules │ └── ROOT │ │ ├── nav.adoc │ │ └── pages │ │ ├── includes │ │ ├── README.md │ │ ├── attributes.adoc │ │ └── quarkus-log-handler-splunk.adoc │ │ └── index.adoc ├── pom.xml └── templates │ └── includes │ └── attributes.adoc ├── integration-tests ├── pom.xml └── src │ ├── main │ ├── java │ │ └── io │ │ │ └── quarkiverse │ │ │ └── logging │ │ │ └── splunk │ │ │ ├── MyMiddleware.java │ │ │ ├── SensitiveLogFilter.java │ │ │ └── SplunkHandlerResource.java │ └── resources │ │ └── application.properties │ └── test │ └── java │ └── io │ └── quarkiverse │ └── logging │ └── splunk │ ├── SplunkLoggingIT.java │ └── SplunkLoggingTest.java ├── pom.xml ├── runtime ├── pom.xml └── src │ ├── main │ ├── java │ │ └── io │ │ │ └── quarkiverse │ │ │ └── logging │ │ │ └── splunk │ │ │ ├── AsyncConfig.java │ │ │ ├── DevServicesLoggingSplunkRuntimeConfig.java │ │ │ ├── SplunkConfig.java │ │ │ ├── SplunkErrorCallback.java │ │ │ ├── SplunkFlatEventSerializer.java │ │ │ ├── SplunkHandlerConfig.java │ │ │ ├── SplunkLogHandler.java │ │ │ ├── SplunkLogHandlerRecorder.java │ │ │ └── config │ │ │ └── build │ │ │ ├── DevServicesLoggingSplunkBuildTimeConfig.java │ │ │ └── SplunkBuildConfig.java │ └── resources │ │ └── META-INF │ │ └── quarkus-extension.yaml │ └── test │ └── java │ └── io │ └── quarkiverse │ └── logging │ └── splunk │ ├── SplunkErrorCallbackTest.java │ ├── SplunkLogHandlerRecorderTest.java │ ├── SplunkLogHandlerTest.java │ └── TestMiddleware.java └── test-utils ├── pom.xml └── src ├── main └── java │ └── io │ └── quarkiverse │ └── logging │ └── splunk │ └── test │ ├── LoggingSplunkApiUrl.java │ ├── LoggingSplunkHandlerUrl.java │ └── LoggingSplunkInjectingTestResource.java └── test └── java └── io └── quarkiverse └── logging └── splunk └── test └── LoggingSplunkInjectingTestResourceTest.java /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "README.md" 4 | ], 5 | "imageSize": 100, 6 | "commit": false, 7 | "contributors": [ 8 | { 9 | "login": "vietk", 10 | "name": "vietk", 11 | "avatar_url": "https://avatars.githubusercontent.com/u/1568850?v=4", 12 | "profile": "https://github.com/vietk", 13 | "contributions": [ 14 | "code", 15 | "maintenance" 16 | ] 17 | }, 18 | { 19 | "login": "rquinio1A", 20 | "name": "rquinio1A", 21 | "avatar_url": "https://avatars.githubusercontent.com/u/58322910?v=4", 22 | "profile": "https://github.com/rquinio1A", 23 | "contributions": [ 24 | "code", 25 | "maintenance" 26 | ] 27 | }, 28 | { 29 | "login": "lindseyburnett", 30 | "name": "Lindsey Burnett", 31 | "avatar_url": "https://avatars.githubusercontent.com/u/19955562?v=4", 32 | "profile": "https://github.com/lindseyburnett", 33 | "contributions": [ 34 | "code", 35 | "test" 36 | ] 37 | } 38 | ], 39 | "contributorsPerLine": 7, 40 | "projectName": "quarkus-logging-splunk", 41 | "projectOwner": "quarkiverse", 42 | "repoType": "github", 43 | "repoHost": "https://github.com", 44 | "skipCi": true 45 | } 46 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Lines starting with '#' are comments. 2 | # Each line is a file pattern followed by one or more owners. 3 | 4 | # More details are here: https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners 5 | 6 | # The '*' pattern is global owners. 7 | 8 | # Order is important. The last matching pattern has the most precedence. 9 | # The folders are ordered as follows: 10 | 11 | # In each subsection folders are ordered first by depth, then alphabetically. 12 | # This should make it easy to add new rules without breaking existing ones. 13 | 14 | * @quarkiverse/quarkiverse-logging-splunk 15 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "maven" 9 | directory: "/" 10 | schedule: 11 | interval: "daily" 12 | ignore: 13 | - dependency-name: "org.apache.maven.plugins:maven-compiler-plugin" 14 | -------------------------------------------------------------------------------- /.github/project.yml: -------------------------------------------------------------------------------- 1 | name: Quarkiverse Extension 2 | release: 3 | current-version: 4.0.3 4 | next-version: 4.0.0-SNAPSHOT 5 | -------------------------------------------------------------------------------- /.github/workflows/backport.yml: -------------------------------------------------------------------------------- 1 | name: Backport 2 | on: 3 | pull_request: 4 | types: 5 | - closed 6 | - labeled 7 | permissions: 8 | contents: write 9 | pull-requests: write 10 | jobs: 11 | backport: 12 | runs-on: ubuntu-18.04 13 | name: Backport 14 | steps: 15 | - name: Backport 16 | uses: tibdex/backport@v1 17 | with: 18 | github_token: ${{ secrets.GITHUB_TOKEN }} 19 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: 6 | - "main" 7 | paths-ignore: 8 | - '.gitignore' 9 | - 'CODEOWNERS' 10 | - 'LICENSE' 11 | - '*.md' 12 | - '*.adoc' 13 | - '*.txt' 14 | - '.all-contributorsrc' 15 | pull_request: 16 | paths-ignore: 17 | - '.gitignore' 18 | - 'CODEOWNERS' 19 | - 'LICENSE' 20 | - '*.md' 21 | - '*.adoc' 22 | - '*.txt' 23 | - '.all-contributorsrc' 24 | 25 | jobs: 26 | build: 27 | 28 | runs-on: ubuntu-latest 29 | 30 | steps: 31 | - uses: actions/checkout@v2 32 | 33 | - name: Set up JDK 17 34 | uses: actions/setup-java@v1 35 | with: 36 | java-version: 17 37 | 38 | - name: Get Date 39 | id: get-date 40 | run: | 41 | echo "::set-output name=date::$(/bin/date -u "+%Y-%m")" 42 | shell: bash 43 | - name: Cache Maven Repository 44 | id: cache-maven 45 | uses: actions/cache@v4 46 | with: 47 | path: ~/.m2/repository 48 | # refresh cache every month to avoid unlimited growth 49 | key: maven-repo-${{ runner.os }}-${{ steps.get-date.outputs.date }} 50 | 51 | - name: Build with Maven 52 | run: mvn -B formatter:validate verify -Pnative --file pom.xml 53 | 54 | -------------------------------------------------------------------------------- /.github/workflows/pre-release.yml: -------------------------------------------------------------------------------- 1 | name: Quarkiverse Pre Release 2 | 3 | on: 4 | pull_request: 5 | paths: 6 | - '.github/project.yml' 7 | 8 | concurrency: 9 | group: ${{ github.workflow }}-${{ github.ref }} 10 | cancel-in-progress: true 11 | 12 | jobs: 13 | pre-release: 14 | name: Pre-Release 15 | uses: quarkiverse/.github/.github/workflows/pre-release.yml@main 16 | secrets: inherit 17 | -------------------------------------------------------------------------------- /.github/workflows/quarkus-snapshot.yaml: -------------------------------------------------------------------------------- 1 | name: "Quarkus ecosystem CI" 2 | on: 3 | workflow_dispatch: 4 | watch: 5 | types: [started] 6 | 7 | # For this CI to work, ECOSYSTEM_CI_TOKEN needs to contain a GitHub with rights to close the Quarkus issue that the user/bot has opened, 8 | # while 'ECOSYSTEM_CI_REPO_PATH' needs to be set to the corresponding path in the 'quarkusio/quarkus-ecosystem-ci' repository 9 | 10 | env: 11 | ECOSYSTEM_CI_REPO: quarkusio/quarkus-ecosystem-ci 12 | ECOSYSTEM_CI_REPO_FILE: context.yaml 13 | JAVA_VERSION: 17 14 | 15 | ######################### 16 | # Repo specific setting # 17 | ######################### 18 | 19 | ECOSYSTEM_CI_REPO_PATH: quarkiverse-logging-splunk 20 | 21 | jobs: 22 | build: 23 | name: "Build against latest Quarkus snapshot" 24 | runs-on: ubuntu-latest 25 | # Allow to manually launch the ecosystem CI in addition to the bots 26 | if: github.actor == 'quarkusbot' || github.actor == 'quarkiversebot' || github.actor == 'vietk' 27 | 28 | steps: 29 | - name: Set up Java 30 | uses: actions/setup-java@v1 31 | with: 32 | java-version: ${{ env.JAVA_VERSION }} 33 | 34 | - name: Checkout repo 35 | uses: actions/checkout@v2 36 | with: 37 | path: current-repo 38 | 39 | - name: Checkout Ecosystem 40 | uses: actions/checkout@v2 41 | with: 42 | repository: ${{ env.ECOSYSTEM_CI_REPO }} 43 | path: ecosystem-ci 44 | 45 | - name: Setup and Run Tests 46 | run: ./ecosystem-ci/setup-and-test 47 | env: 48 | ECOSYSTEM_CI_TOKEN: ${{ secrets.ECOSYSTEM_CI_TOKEN }} 49 | -------------------------------------------------------------------------------- /.github/workflows/release-perform.yml: -------------------------------------------------------------------------------- 1 | name: Quarkiverse Perform Release 2 | run-name: Perform ${{github.event.inputs.tag || github.ref_name}} Release 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | workflow_dispatch: 8 | inputs: 9 | tag: 10 | description: 'Tag to release' 11 | required: true 12 | 13 | permissions: 14 | attestations: write 15 | id-token: write 16 | contents: read 17 | 18 | concurrency: 19 | group: ${{ github.workflow }}-${{ github.ref }} 20 | cancel-in-progress: true 21 | 22 | jobs: 23 | perform-release: 24 | name: Perform Release 25 | uses: quarkiverse/.github/.github/workflows/perform-release.yml@main 26 | secrets: inherit 27 | with: 28 | version: ${{github.event.inputs.tag || github.ref_name}} 29 | -------------------------------------------------------------------------------- /.github/workflows/release-prepare.yml: -------------------------------------------------------------------------------- 1 | name: Quarkiverse Prepare Release 2 | 3 | on: 4 | pull_request: 5 | types: [ closed ] 6 | paths: 7 | - '.github/project.yml' 8 | 9 | concurrency: 10 | group: ${{ github.workflow }}-${{ github.ref }} 11 | cancel-in-progress: true 12 | 13 | jobs: 14 | prepare-release: 15 | name: Prepare Release 16 | if: ${{ github.event.pull_request.merged == true}} 17 | uses: quarkiverse/.github/.github/workflows/prepare-release.yml@main 18 | secrets: inherit 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled class file 2 | *.class 3 | 4 | # Log file 5 | *.log 6 | 7 | # BlueJ files 8 | *.ctxt 9 | 10 | # Mobile Tools for Java (J2ME) 11 | .mtj.tmp/ 12 | 13 | # Package Files # 14 | *.jar 15 | *.war 16 | *.nar 17 | *.ear 18 | *.zip 19 | *.tar.gz 20 | *.rar 21 | 22 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 23 | hs_err_pid* 24 | 25 | # Eclipse 26 | .project 27 | .classpath 28 | .settings/ 29 | bin/ 30 | 31 | # IntelliJ 32 | .idea 33 | *.ipr 34 | *.iml 35 | *.iws 36 | 37 | # NetBeans 38 | nb-configuration.xml 39 | 40 | # Visual Studio Code 41 | .vscode 42 | .factorypath 43 | 44 | # OSX 45 | .DS_Store 46 | 47 | # Vim 48 | *.swp 49 | *.swo 50 | 51 | # patch 52 | *.orig 53 | *.rej 54 | 55 | # Gradle 56 | .gradle/ 57 | /build/ 58 | 59 | # Maven 60 | target/ 61 | pom.xml.tag 62 | pom.xml.releaseBackup 63 | pom.xml.versionsBackup 64 | release.properties 65 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Quarkus Splunk extension 2 | 3 | [![Build](https://github.com/quarkiverse/quarkus-logging-splunk/workflows/Build/badge.svg)](https://github.com/quarkiverse/quarkus-logging-splunk/actions?query=workflow%3ABuild) 4 | [![Maven Central](https://img.shields.io/maven-central/v/io.quarkiverse.logging.splunk/quarkus-logging-splunk.svg?label=Maven%20Central)](https://search.maven.org/artifact/io.quarkiverse.logging.splunk/quarkus-logging-splunk) 5 | [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) 6 | 7 | 8 | [![All Contributors](https://img.shields.io/badge/all_contributors-3-orange.svg?style=flat-square)](#contributors-) 9 | 10 | 11 | A Quarkus extension to send logs to a Splunk HTTP Event Collector (HEC). 12 | 13 | To get started, add the dependency: 14 | 15 | ```xml 16 | 17 | io.quarkiverse.logging.splunk 18 | quarkus-logging-splunk 19 | 20 | ``` 21 | 22 | For more details, check the complete [documentation](https://quarkiverse.github.io/quarkiverse-docs/quarkus-logging-splunk/dev/index.html). 23 | 24 | This extension is based on the [official Splunk's HEC client](https://github.com/splunk/splunk-library-javalogging). 25 | But, it defines its own [Java Logging Handler](https://docs.oracle.com/en/java/javase/11/docs/api/java.logging/java/util/logging/Handler.html) 26 | rather than using the [official one](https://github.com/splunk/splunk-library-javalogging/blob/1.8.0/src/main/java/com/splunk/logging/HttpEventCollectorLoggingHandler.java), 27 | since it was not compatible with JBoss logger implementation used in Quarkus and, as such, 28 | prevents to record steps at build time. 29 | 30 | ## Contributors ✨ 31 | 32 | Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 |

vietk

💻 🚧

rquinio1A

💻 🚧

Lindsey Burnett

💻 ⚠️
44 | 45 | 46 | 47 | 48 | 49 | 50 | This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! 51 | -------------------------------------------------------------------------------- /deployment/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | io.quarkiverse.logging.splunk 6 | quarkus-logging-splunk-parent 7 | 4.0.0-SNAPSHOT 8 | ../pom.xml 9 | 10 | 11 | quarkus-logging-splunk-deployment 12 | Splunk logging extension - Deployment 13 | 14 | 15 | 16 | io.quarkiverse.logging.splunk 17 | quarkus-logging-splunk 18 | ${project.version} 19 | 20 | 21 | io.quarkus 22 | quarkus-core-deployment 23 | 24 | 25 | io.quarkus 26 | quarkus-arc-deployment 27 | 28 | 29 | io.quarkus 30 | quarkus-devservices-deployment 31 | 32 | 33 | io.quarkus 34 | quarkus-junit5-internal 35 | test 36 | 37 | 38 | io.quarkus 39 | quarkus-bootstrap-runner 40 | test 41 | 42 | 43 | org.hamcrest 44 | hamcrest 45 | test 46 | 47 | 48 | org.awaitility 49 | awaitility 50 | test 51 | 52 | 53 | org.mock-server 54 | mockserver-netty 55 | 5.15.0 56 | test 57 | 58 | 59 | logback-classic 60 | ch.qos.logback 61 | 62 | 63 | 64 | 65 | org.testcontainers 66 | testcontainers 67 | 68 | 69 | 70 | 71 | 72 | 73 | org.apache.maven.plugins 74 | maven-compiler-plugin 75 | 76 | 77 | 78 | io.quarkus 79 | quarkus-extension-processor 80 | ${quarkus.version} 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /deployment/src/main/java/io/quarkiverse/logging/splunk/DevServicesLoggingSplunkProcessor.java: -------------------------------------------------------------------------------- 1 | package io.quarkiverse.logging.splunk; 2 | 3 | import java.time.Duration; 4 | import java.util.HashMap; 5 | import java.util.Map; 6 | import java.util.Optional; 7 | 8 | import org.jboss.logging.Logger; 9 | import org.testcontainers.utility.DockerImageName; 10 | 11 | import io.quarkiverse.logging.splunk.config.build.DevServicesLoggingSplunkBuildTimeConfig; 12 | import io.quarkiverse.logging.splunk.config.build.SplunkBuildConfig; 13 | import io.quarkus.deployment.IsNormal; 14 | import io.quarkus.deployment.annotations.BuildStep; 15 | import io.quarkus.deployment.annotations.BuildSteps; 16 | import io.quarkus.deployment.builditem.CuratedApplicationShutdownBuildItem; 17 | import io.quarkus.deployment.builditem.DevServicesResultBuildItem; 18 | import io.quarkus.deployment.builditem.DockerStatusBuildItem; 19 | import io.quarkus.deployment.builditem.LaunchModeBuildItem; 20 | import io.quarkus.deployment.console.ConsoleInstalledBuildItem; 21 | import io.quarkus.deployment.console.StartupLogCompressor; 22 | import io.quarkus.deployment.dev.devservices.GlobalDevServicesConfig; 23 | import io.quarkus.deployment.logging.LoggingSetupBuildItem; 24 | import io.quarkus.runtime.LaunchMode; 25 | 26 | @BuildSteps(onlyIfNot = IsNormal.class, onlyIf = { GlobalDevServicesConfig.Enabled.class }) 27 | public class DevServicesLoggingSplunkProcessor { 28 | private static final Logger log = Logger.getLogger(DevServicesLoggingSplunkProcessor.class); 29 | 30 | private static final String SPLUNK = "splunk"; 31 | 32 | private static final String HANDLER_URL_CONFIG_PROP = "quarkus.log.handler.splunk.url"; 33 | 34 | private static final String API_URL_CONFIG_PROP = "quarkus.log.handler.splunk.devservices.api-url"; 35 | 36 | private static final String SPLUNK_LATEST = "registry-1.docker.io/splunk/splunk:latest"; 37 | 38 | private static volatile DevServicesResultBuildItem.RunningDevService devService; 39 | 40 | private static volatile DevServicesLoggingSplunkBuildTimeConfig capturedDevServiceConfig; 41 | 42 | private static volatile boolean first = true; 43 | 44 | /** 45 | * Start one (or many in the future) SplunkContainer(s) depending on extension configuration. We also take care of 46 | * locating and re-using existing container if configured in shared mode. 47 | */ 48 | @BuildStep 49 | public DevServicesResultBuildItem startSplunkContainer(LaunchModeBuildItem launchMode, 50 | DockerStatusBuildItem dockerStatusBuildItem, 51 | SplunkBuildConfig config, 52 | Optional consoleInstalledBuildItem, 53 | CuratedApplicationShutdownBuildItem closeBuildItem, 54 | LoggingSetupBuildItem loggingSetupBuildItem, 55 | GlobalDevServicesConfig devServicesConfig) { 56 | 57 | // Figure out if we need to shut down and restart existing Splunk containers 58 | // if not and the Splunk containers have already started we just return 59 | if (devService != null) { 60 | if (config.devservices().equals(capturedDevServiceConfig)) { 61 | return devService.toBuildItem(); 62 | } 63 | try { 64 | devService.close(); 65 | } catch (Throwable e) { 66 | log.error("Failed to stop Splunk container", e); 67 | } 68 | devService = null; 69 | capturedDevServiceConfig = null; 70 | } 71 | 72 | // Re-initialize captured config and dev services. 73 | capturedDevServiceConfig = config.devservices(); 74 | 75 | StartupLogCompressor compressor = new StartupLogCompressor( 76 | (launchMode.isTest() ? "(test) " : "") + "Splunk Dev Services Starting:", consoleInstalledBuildItem, 77 | loggingSetupBuildItem); 78 | try { 79 | devService = startContainer(config.devservices(), dockerStatusBuildItem, 80 | launchMode.getLaunchMode(), devServicesConfig.timeout); 81 | 82 | if (devService == null) { 83 | compressor.closeAndDumpCaptured(); 84 | return null; 85 | } else { 86 | compressor.close(); 87 | log.infof("The Splunk container is ready on %s", devService.getConfig().get(HANDLER_URL_CONFIG_PROP)); 88 | } 89 | } catch (Throwable t) { 90 | compressor.closeAndDumpCaptured(); 91 | throw new RuntimeException(t); 92 | } 93 | 94 | if (first) { 95 | first = false; 96 | // Add close tasks on first run only. 97 | Runnable closeTask = () -> { 98 | if (devService != null) { 99 | try { 100 | devService.close(); 101 | } catch (Throwable t) { 102 | log.error("Failed to stop Splunk", t); 103 | } 104 | } 105 | first = true; 106 | devService = null; 107 | capturedDevServiceConfig = null; 108 | }; 109 | closeBuildItem.addCloseTask(closeTask, true); 110 | } 111 | 112 | return devService.toBuildItem(); 113 | } 114 | 115 | private DevServicesResultBuildItem.RunningDevService startContainer(DevServicesLoggingSplunkBuildTimeConfig config, 116 | DockerStatusBuildItem dockerStatusBuildItem, LaunchMode launchMode, Optional timeout) { 117 | if (!isEnabled(config)) { 118 | // explicitly disabled 119 | log.info("Not starting devservices for Splunk as it has been disabled in the config"); 120 | return null; 121 | } 122 | 123 | if (!dockerStatusBuildItem.isContainerRuntimeAvailable()) { 124 | log.warn("Configure quarkus.log.handler.splunk.url or have a working docker daemon"); 125 | return null; 126 | } 127 | 128 | DockerImageName dockerImageName = DockerImageName.parse(config.imageName().orElse(SPLUNK_LATEST)) 129 | .asCompatibleSubstituteFor(SPLUNK_LATEST); 130 | 131 | SplunkContainer splunkContainer = new SplunkContainer(dockerImageName); 132 | 133 | // Add envs and timeout if provided. 134 | splunkContainer.withEnv(config.containerEnv()); 135 | timeout.ifPresent(splunkContainer::withStartupTimeout); 136 | 137 | splunkContainer.start(); 138 | 139 | return new DevServicesResultBuildItem.RunningDevService(SPLUNK, splunkContainer.getContainerId(), 140 | splunkContainer::close, getDevServiceExposedConfig(splunkContainer, config)); 141 | } 142 | 143 | private Map getDevServiceExposedConfig(SplunkContainer container, 144 | DevServicesLoggingSplunkBuildTimeConfig config) { 145 | final Map exposedConfig = new HashMap<>(); 146 | exposedConfig.put(HANDLER_URL_CONFIG_PROP, container.getSplunkHandlerUrl()); 147 | exposedConfig.put(API_URL_CONFIG_PROP, container.getSplunkApiUrl()); 148 | exposedConfig.put("quarkus.log.handler.splunk.token", SplunkContainer.HEC_TOKEN); 149 | exposedConfig.put("quarkus.log.handler.splunk.disable-certificate-validation", "true"); 150 | exposedConfig.put("quarkus.log.handler.splunk.enabled", "true"); 151 | config.plugNamedHandlers().forEach((k, v) -> { 152 | // Named handlers are configured using runtime configuration, which we do not have access to here 153 | // so a dedicated build time map was introduced to still be able to generate the configuration 154 | // to inject for each named handler. 155 | if (v) { 156 | exposedConfig.put("quarkus.log.handler.splunk." + k + ".url", container.getSplunkHandlerUrl()); 157 | exposedConfig.put("quarkus.log.handler.splunk." + k + ".token", SplunkContainer.HEC_TOKEN); 158 | exposedConfig.put("quarkus.log.handler.splunk." + k + ".disable-certificate-validation", "true"); 159 | exposedConfig.put("quarkus.log.handler.splunk." + k + ".enabled", "true"); 160 | } 161 | }); 162 | return exposedConfig; 163 | } 164 | 165 | private boolean isEnabled(DevServicesLoggingSplunkBuildTimeConfig config) { 166 | return config.enabled(); 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /deployment/src/main/java/io/quarkiverse/logging/splunk/LoggingSplunkProcessor.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2021 Amadeus s.a.s. 3 | Contributor(s): Kevin Viet, Romain Quinio (Amadeus s.a.s.) 4 | */ 5 | package io.quarkiverse.logging.splunk; 6 | 7 | import java.util.Collection; 8 | import java.util.HashMap; 9 | import java.util.List; 10 | import java.util.Map; 11 | import java.util.logging.Filter; 12 | 13 | import org.jboss.jandex.AnnotationInstance; 14 | import org.jboss.jandex.AnnotationTarget; 15 | import org.jboss.jandex.ClassInfo; 16 | import org.jboss.jandex.DotName; 17 | import org.jboss.jandex.IndexView; 18 | 19 | import com.splunk.logging.HttpEventCollectorMiddleware; 20 | import com.splunk.logging.HttpEventCollectorSender; 21 | 22 | import io.quarkus.deployment.annotations.BuildProducer; 23 | import io.quarkus.deployment.annotations.BuildStep; 24 | import io.quarkus.deployment.annotations.ExecutionTime; 25 | import io.quarkus.deployment.annotations.Record; 26 | import io.quarkus.deployment.builditem.CombinedIndexBuildItem; 27 | import io.quarkus.deployment.builditem.ExtensionSslNativeSupportBuildItem; 28 | import io.quarkus.deployment.builditem.FeatureBuildItem; 29 | import io.quarkus.deployment.builditem.LogHandlerBuildItem; 30 | import io.quarkus.deployment.builditem.NamedLogHandlersBuildItem; 31 | import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; 32 | import io.quarkus.deployment.builditem.nativeimage.RuntimeInitializedClassBuildItem; 33 | import io.quarkus.deployment.util.JandexUtil; 34 | import io.quarkus.logging.LoggingFilter; 35 | import io.quarkus.runtime.logging.DiscoveredLogComponents; 36 | 37 | class LoggingSplunkProcessor { 38 | 39 | public static final DotName LOGGING_FILTER = DotName.createSimple(LoggingFilter.class.getName()); 40 | 41 | private static final DotName FILTER = DotName.createSimple(Filter.class.getName()); 42 | 43 | private static final String ILLEGAL_LOGGING_FILTER_USE_MESSAGE = "'@" + LoggingFilter.class.getName() 44 | + "' can only be used on classes that implement '" 45 | + Filter.class.getName() + "' and that are marked as final."; 46 | 47 | private static final String FEATURE = "logging-splunk"; 48 | 49 | @BuildStep 50 | FeatureBuildItem feature() { 51 | return new FeatureBuildItem(FEATURE); 52 | } 53 | 54 | @BuildStep 55 | @Record(ExecutionTime.RUNTIME_INIT) 56 | LogHandlerBuildItem logHandler(SplunkLogHandlerRecorder recorder, SplunkConfig config, 57 | CombinedIndexBuildItem combinedIndexBuildItem) { 58 | DiscoveredLogComponents discoveredLogComponents = discoverLogComponents(combinedIndexBuildItem.getIndex()); 59 | return new LogHandlerBuildItem(recorder.initializeHandler(config, discoveredLogComponents)); 60 | } 61 | 62 | @BuildStep 63 | @Record(ExecutionTime.RUNTIME_INIT) 64 | NamedLogHandlersBuildItem logNamedHandlers(SplunkLogHandlerRecorder recorder, SplunkConfig config, 65 | CombinedIndexBuildItem combinedIndexBuildItem) { 66 | DiscoveredLogComponents discoveredLogComponents = discoverLogComponents(combinedIndexBuildItem.getIndex()); 67 | return new NamedLogHandlersBuildItem(recorder.initializeHandlers(config, discoveredLogComponents)); 68 | } 69 | 70 | @BuildStep 71 | ExtensionSslNativeSupportBuildItem enableSSL() { 72 | // Enable SSL support by default 73 | return new ExtensionSslNativeSupportBuildItem(FEATURE); 74 | } 75 | 76 | @BuildStep 77 | RuntimeInitializedClassBuildItem runtimeInitialization() { 78 | return new RuntimeInitializedClassBuildItem(HttpEventCollectorSender.class.getCanonicalName()); 79 | } 80 | 81 | @BuildStep 82 | public void configureNativeExecutable(CombinedIndexBuildItem combinedIndex, 83 | BuildProducer reflectiveClass) { 84 | // HttpSenderMiddleware can be selected via the middleware configuration and is loaded 85 | // dynamically, so we need to make sure it is registered for reflection. 86 | Collection messages = combinedIndex.getIndex().getAllKnownSubclasses( 87 | HttpEventCollectorMiddleware.HttpSenderMiddleware.class); 88 | for (ClassInfo message : messages) { 89 | reflectiveClass.produce(ReflectiveClassBuildItem.builder(message.name().toString()) 90 | .constructors() 91 | .build()); 92 | } 93 | } 94 | 95 | /** 96 | * Copied from io.quarkus.deployment.logging.LoggingResourceProcessor, as not exposed as a build item. 97 | */ 98 | private DiscoveredLogComponents discoverLogComponents(IndexView index) { 99 | Collection loggingFilterInstances = index.getAnnotations(LOGGING_FILTER); 100 | DiscoveredLogComponents result = new DiscoveredLogComponents(); 101 | 102 | Map filtersMap = new HashMap<>(); 103 | for (AnnotationInstance instance : loggingFilterInstances) { 104 | AnnotationTarget target = instance.target(); 105 | if (target.kind() != AnnotationTarget.Kind.CLASS) { 106 | throw new IllegalStateException("Unimplemented mode of use of '" + LoggingFilter.class.getName() + "'"); 107 | } 108 | ClassInfo classInfo = target.asClass(); 109 | boolean isFilterImpl = false; 110 | ClassInfo currentClassInfo = classInfo; 111 | while ((currentClassInfo != null) && (!JandexUtil.DOTNAME_OBJECT.equals(currentClassInfo.name()))) { 112 | boolean hasFilterInterface = false; 113 | List ifaces = currentClassInfo.interfaceNames(); 114 | for (DotName iface : ifaces) { 115 | if (FILTER.equals(iface)) { 116 | hasFilterInterface = true; 117 | break; 118 | } 119 | } 120 | if (hasFilterInterface) { 121 | isFilterImpl = true; 122 | break; 123 | } 124 | currentClassInfo = index.getClassByName(currentClassInfo.superName()); 125 | } 126 | if (!isFilterImpl) { 127 | throw new RuntimeException( 128 | ILLEGAL_LOGGING_FILTER_USE_MESSAGE + " Offending class is '" + classInfo.name() + "'"); 129 | } 130 | 131 | String filterName = instance.value("name").asString(); 132 | if (filtersMap.containsKey(filterName)) { 133 | throw new RuntimeException("Filter '" + filterName + "' was defined multiple times."); 134 | } 135 | filtersMap.put(filterName, classInfo.name().toString()); 136 | } 137 | if (!filtersMap.isEmpty()) { 138 | result.setNameToFilterClass(filtersMap); 139 | } 140 | 141 | return result; 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /deployment/src/main/java/io/quarkiverse/logging/splunk/SplunkContainer.java: -------------------------------------------------------------------------------- 1 | package io.quarkiverse.logging.splunk; 2 | 3 | import static org.testcontainers.containers.wait.strategy.Wait.forLogMessage; 4 | 5 | import java.time.Duration; 6 | 7 | import org.jetbrains.annotations.NotNull; 8 | import org.testcontainers.containers.GenericContainer; 9 | import org.testcontainers.utility.DockerImageName; 10 | 11 | import io.quarkus.devservices.common.ConfigureUtil; 12 | 13 | public class SplunkContainer extends GenericContainer { 14 | 15 | public static final int SPLUNK_UI_PORT = 8000; 16 | 17 | public static final int SPLUNK_HEC_PORT = 8088; 18 | 19 | public static final int SPLUNK_API_PORT = 8089; 20 | 21 | public static final String HEC_TOKEN = "local-dev-token"; 22 | 23 | public static final String SPLUNK_PASSWORD = "admin123"; 24 | 25 | public SplunkContainer(DockerImageName dockerImageName) { 26 | super(dockerImageName); 27 | withEnv("SPLUNK_START_ARGS", "--accept-license"); 28 | withEnv("SPLUNK_PASSWORD", SPLUNK_PASSWORD); 29 | withEnv("SPLUNK_HEC_TOKEN", HEC_TOKEN); 30 | waitingFor(forLogMessage(".*Ansible playbook complete.*\\n", 1)); 31 | withStartupTimeout(Duration.ofMinutes(2)); 32 | } 33 | 34 | @Override 35 | protected void configure() { 36 | super.configure(); 37 | withExposedPorts(SPLUNK_UI_PORT, SPLUNK_HEC_PORT, SPLUNK_API_PORT); 38 | ConfigureUtil.configureSharedNetwork(this, "splunk"); 39 | } 40 | 41 | public String getSplunkUiUrl() { 42 | return "http://localhost:" + getMappedPort(SPLUNK_UI_PORT); 43 | } 44 | 45 | public String getSplunkHandlerUrl() { 46 | return getUrl(SPLUNK_HEC_PORT); 47 | } 48 | 49 | public String getSplunkApiUrl() { 50 | return getUrl(SPLUNK_API_PORT); 51 | } 52 | 53 | @NotNull 54 | private String getUrl(int port) { 55 | return "https://localhost:" + getMappedPort(port); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /deployment/src/test/java/io/quarkiverse/logging/splunk/AbstractMockServerTest.java: -------------------------------------------------------------------------------- 1 | package io.quarkiverse.logging.splunk; 2 | 3 | import static java.util.concurrent.TimeUnit.SECONDS; 4 | import static org.awaitility.Awaitility.await; 5 | import static org.mockserver.model.HttpRequest.request; 6 | import static org.mockserver.model.HttpResponse.response; 7 | 8 | import org.junit.jupiter.api.AfterAll; 9 | import org.junit.jupiter.api.AfterEach; 10 | import org.junit.jupiter.api.BeforeAll; 11 | import org.mockserver.configuration.ConfigurationProperties; 12 | import org.mockserver.integration.ClientAndServer; 13 | import org.mockserver.model.ClearType; 14 | import org.mockserver.model.HttpRequest; 15 | 16 | import io.quarkus.test.QuarkusUnitTest; 17 | 18 | public abstract class AbstractMockServerTest { 19 | 20 | static ClientAndServer httpServer; 21 | 22 | @BeforeAll 23 | public static void setUpOnce() { 24 | ConfigurationProperties.dynamicallyCreateCertificateAuthorityCertificate(false); 25 | ConfigurationProperties.assumeAllRequestsAreHttp(true); 26 | httpServer = ClientAndServer.startClientAndServer(8088); 27 | // This needs to be done as early as possible, so Quarkus startup logs don't fail to be sent 28 | httpServer 29 | .when(request().withPath("/services/collector/event/1.0")) 30 | .respond(response().withStatusCode(200).withBody("{}")); 31 | httpServer 32 | .when(request().withPath("/services/collector/raw")) 33 | .respond(response().withStatusCode(200).withBody("{}")); 34 | httpServer.when(request()).respond(response().withStatusCode(400)); 35 | } 36 | 37 | @AfterAll 38 | public static void tearDownOnce() { 39 | httpServer.stop(); 40 | } 41 | 42 | @AfterEach 43 | public void tearDown() { 44 | httpServer.clear(request(), ClearType.LOG); 45 | } 46 | 47 | /** 48 | * Splunk client uses an asynchronous HTTP queue, so to avoid race conditions we need to wait for mockserver to receive the 49 | * call. 50 | */ 51 | protected void awaitMockServer() { 52 | await().atMost(1, SECONDS).until(() -> httpServer.retrieveRecordedRequests(request()).length != 0); 53 | } 54 | 55 | protected HttpRequest requestToJsonEndpoint() { 56 | return request().withPath("/services/collector/event/1.0"); 57 | } 58 | 59 | protected HttpRequest requestToRawEndpoint() { 60 | return request().withPath("/services/collector/raw"); 61 | } 62 | 63 | /** 64 | * QuarkusUnitTest 3.16+ only supports one call to #withConfigurationResource 65 | * So use #overrideConfigKey instead of a properties. 66 | * See https://github.com/quarkusio/quarkus/issues/43914 67 | */ 68 | protected static QuarkusUnitTest withMockServerConfig() { 69 | 70 | return new QuarkusUnitTest() 71 | // Switch from HTTPS to HTTP 72 | .overrideConfigKey("quarkus.log.handler.splunk.url", "http://localhost:8088") 73 | // Avoid infinite loop of logging via splunk handler, mockserver must only log to stdout ! 74 | .overrideConfigKey("quarkus.log.handler.console.\"stdout\".format", "%s%e%n") 75 | .overrideConfigKey("quarkus.log.category.\"org.mockserver\".handlers", "stdout") 76 | .overrideConfigKey("quarkus.log.category.\"org.mockserver\".use-parent-handlers", "false") 77 | // Avoid batching and send events immediately, to make unit tests more synchronous 78 | // Note that OKHttp client still executes its I/O on a separate thread 79 | .overrideConfigKey("quarkus.log.handler.splunk.batch-interval", "0") 80 | .overrideConfigKey("quarkus.log.handler.splunk.batch-size-bytes", "0") 81 | .overrideConfigKey("quarkus.log.handler.splunk.batch-size-count", "0") 82 | .overrideConfigKey("quarkus.log.handler.splunk.send-mode", "sequential"); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /deployment/src/test/java/io/quarkiverse/logging/splunk/LoggingSplunkAsyncTest.java: -------------------------------------------------------------------------------- 1 | package io.quarkiverse.logging.splunk; 2 | 3 | import static org.mockserver.model.JsonBody.json; 4 | 5 | import org.jboss.logging.Logger; 6 | import org.jboss.shrinkwrap.api.ShrinkWrap; 7 | import org.jboss.shrinkwrap.api.spec.JavaArchive; 8 | import org.junit.jupiter.api.Test; 9 | import org.junit.jupiter.api.extension.RegisterExtension; 10 | 11 | import io.quarkus.test.QuarkusUnitTest; 12 | 13 | public class LoggingSplunkAsyncTest extends AbstractMockServerTest { 14 | 15 | @RegisterExtension 16 | static final QuarkusUnitTest unitTest = withMockServerConfig() 17 | .withConfigurationResource("application-splunk-logging-async.properties") 18 | .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)); 19 | 20 | static final Logger logger = Logger.getLogger(LoggingSplunkAsyncTest.class); 21 | 22 | @Test 23 | void eventIsAJsonObjectWithMetadata() { 24 | logger.warn("hello splunk"); 25 | awaitMockServer(); 26 | httpServer.verify(requestToJsonEndpoint().withBody(json("{ event: { message: 'hello splunk' }}"))); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /deployment/src/test/java/io/quarkiverse/logging/splunk/LoggingSplunkDisabledTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2021 Amadeus s.a.s. 3 | Contributor(s): Kevin Viet, Romain Quinio (Amadeus s.a.s.) 4 | */ 5 | package io.quarkiverse.logging.splunk; 6 | 7 | import static org.hamcrest.MatcherAssert.assertThat; 8 | import static org.hamcrest.Matchers.hasItemInArray; 9 | import static org.junit.jupiter.api.Assertions.assertNull; 10 | 11 | import java.util.Arrays; 12 | import java.util.logging.Handler; 13 | import java.util.logging.Logger; 14 | 15 | import org.jboss.logmanager.ExtHandler; 16 | import org.jboss.shrinkwrap.api.ShrinkWrap; 17 | import org.jboss.shrinkwrap.api.spec.JavaArchive; 18 | import org.junit.jupiter.api.Test; 19 | import org.junit.jupiter.api.extension.RegisterExtension; 20 | 21 | import io.quarkus.bootstrap.logging.InitialConfigurator; 22 | import io.quarkus.test.QuarkusUnitTest; 23 | 24 | class LoggingSplunkDisabledTest { 25 | 26 | @RegisterExtension 27 | static final QuarkusUnitTest unitTest = new QuarkusUnitTest() 28 | .withConfigurationResource("application-splunk-logging-disabled.properties") 29 | .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)); 30 | 31 | @Test 32 | void extensionDisabled() { 33 | ExtHandler delayedHandler = InitialConfigurator.DELAYED_HANDLER; 34 | assertThat(Logger.getLogger("").getHandlers(), hasItemInArray(delayedHandler)); 35 | Handler handler = Arrays.stream(delayedHandler.getHandlers()) 36 | .filter(h -> (h instanceof SplunkLogHandler)) 37 | .findFirst().orElse(null); 38 | assertNull(handler); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /deployment/src/test/java/io/quarkiverse/logging/splunk/LoggingSplunkFilteringTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2021 Amadeus s.a.s. 3 | Contributor(s): Kevin Viet, Romain Quinio (Amadeus s.a.s.) 4 | */ 5 | package io.quarkiverse.logging.splunk; 6 | 7 | import static org.mockserver.model.JsonBody.json; 8 | import static org.mockserver.model.RegexBody.regex; 9 | 10 | import java.util.logging.Filter; 11 | import java.util.logging.LogRecord; 12 | 13 | import org.jboss.logging.Logger; 14 | import org.jboss.logmanager.Level; 15 | import org.jboss.shrinkwrap.api.ShrinkWrap; 16 | import org.jboss.shrinkwrap.api.spec.JavaArchive; 17 | import org.junit.jupiter.api.Test; 18 | import org.junit.jupiter.api.extension.RegisterExtension; 19 | 20 | import io.quarkus.logging.LoggingFilter; 21 | import io.quarkus.test.QuarkusUnitTest; 22 | 23 | class LoggingSplunkFilteringTest extends AbstractMockServerTest { 24 | 25 | @RegisterExtension 26 | static final QuarkusUnitTest unitTest = withMockServerConfig() 27 | .withConfigurationResource("application-splunk-logging-filtering.properties") 28 | .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)); 29 | 30 | static final Logger logger = Logger.getLogger(LoggingSplunkFilteringTest.class); 31 | 32 | @Test 33 | void filterShouldBeCalled() { 34 | logger.info("hello splunk"); 35 | awaitMockServer(); 36 | httpServer.verify( 37 | requestToJsonEndpoint().withBody(regex(".*hello splunk.*")).withBody(json("{ event: { severity:'ERROR' }}"))); 38 | } 39 | 40 | @LoggingFilter(name = "my-filter") 41 | public static class MyFilter implements Filter { 42 | 43 | @Override 44 | public boolean isLoggable(LogRecord record) { 45 | record.setLevel(Level.ERROR); 46 | return true; 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /deployment/src/test/java/io/quarkiverse/logging/splunk/LoggingSplunkFlatSerializationTest.java: -------------------------------------------------------------------------------- 1 | package io.quarkiverse.logging.splunk; 2 | 3 | import static org.mockserver.model.JsonBody.json; 4 | import static org.mockserver.model.RegexBody.regex; 5 | 6 | import org.jboss.logging.MDC; 7 | import org.jboss.shrinkwrap.api.ShrinkWrap; 8 | import org.jboss.shrinkwrap.api.spec.JavaArchive; 9 | import org.junit.jupiter.api.Test; 10 | import org.junit.jupiter.api.extension.RegisterExtension; 11 | 12 | import io.quarkus.test.QuarkusUnitTest; 13 | 14 | public class LoggingSplunkFlatSerializationTest extends AbstractMockServerTest { 15 | 16 | @RegisterExtension 17 | static final QuarkusUnitTest unitTest = withMockServerConfig() 18 | .withConfigurationResource("application-splunk-logging-flat.properties") 19 | .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)); 20 | 21 | static final org.jboss.logging.Logger logger = org.jboss.logging.Logger.getLogger(LoggingSplunkFlatSerializationTest.class); 22 | 23 | @Test 24 | void handlerShouldFormatMessage() { 25 | logger.warnv("hello {0}", "splunk!"); 26 | awaitMockServer(); 27 | httpServer.verify(requestToJsonEndpoint().withBody(json("{ event: 'hello splunk!' }"))); 28 | } 29 | 30 | @Test 31 | void eventHasStandardMetadata() { 32 | logger.warn("hello splunk"); 33 | awaitMockServer(); 34 | httpServer.verify(requestToJsonEndpoint().withBody(json( 35 | "{ source: 'mysource', index: 'myindex'} ")) 36 | .withBody(regex(".*host.*"))); 37 | } 38 | 39 | @Test 40 | void tokenIsSentAsAuthorizationHeader() { 41 | logger.warn("hello splunk"); 42 | awaitMockServer(); 43 | httpServer 44 | .verify(requestToJsonEndpoint().withHeader("Authorization", "Splunk 12345678-1234-1234-1234-1234567890AB")); 45 | } 46 | 47 | @Test 48 | void eventHasSeverityMetadata() { 49 | logger.warn("hello splunk"); 50 | awaitMockServer(); 51 | httpServer.verify(requestToJsonEndpoint().withBody(json("{ fields: { 'level': 'WARN' } }"))); 52 | } 53 | 54 | @Test 55 | void eventHasOptionalMetadata() { 56 | logger.error("hello splunk", new RuntimeException("test exception")); 57 | awaitMockServer(); 58 | httpServer.verify(requestToJsonEndpoint().withBody(regex(".*hello splunk.*"))); 59 | httpServer.verify(requestToJsonEndpoint().withBody(json( 60 | "{ fields: { logger:'io.quarkiverse.logging.splunk.LoggingSplunkFlatSerializationTest', exception: 'test exception' }}"))); 61 | httpServer.verify(requestToJsonEndpoint().withBody(regex(".*thread.*"))); 62 | } 63 | 64 | @Test 65 | void mdcFieldsShouldBeSentAsMetadata() { 66 | MDC.put("mdc-key", "mdc-value"); 67 | logger.warn("hello mdc"); 68 | MDC.remove("mdc-key"); 69 | awaitMockServer(); 70 | httpServer.verify( 71 | requestToJsonEndpoint() 72 | .withBody(json("{ event: 'hello mdc', fields: { 'mdc-key': 'mdc-value' }}"))); 73 | } 74 | 75 | @Test 76 | void staticMetadataFields() { 77 | logger.warn("hello splunk"); 78 | awaitMockServer(); 79 | httpServer.verify(requestToJsonEndpoint().withBody( 80 | json("{ fields: { metadata-0: 'value0', metadata-1: 'value1' } }"))); 81 | } 82 | 83 | @Test 84 | void nestedJsonMessageIsNotParsed() { 85 | logger.warn("{ 'greeting': 'hello', 'user': 'splunk' }"); 86 | awaitMockServer(); 87 | httpServer.verify(requestToJsonEndpoint().withBody(json("{ event: \"{ 'greeting': 'hello', 'user': 'splunk' }\" }"))); 88 | } 89 | 90 | } 91 | -------------------------------------------------------------------------------- /deployment/src/test/java/io/quarkiverse/logging/splunk/LoggingSplunkMandatoryConfigTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2021 Amadeus s.a.s. 3 | Contributor(s): Kevin Viet, Romain Quinio (Amadeus s.a.s.) 4 | */ 5 | package io.quarkiverse.logging.splunk; 6 | 7 | import static org.junit.jupiter.api.Assertions.*; 8 | import static org.junit.jupiter.api.Assertions.assertTrue; 9 | 10 | import org.jboss.shrinkwrap.api.ShrinkWrap; 11 | import org.jboss.shrinkwrap.api.spec.JavaArchive; 12 | import org.junit.jupiter.api.Test; 13 | import org.junit.jupiter.api.extension.RegisterExtension; 14 | 15 | import io.quarkus.test.QuarkusUnitTest; 16 | 17 | class LoggingSplunkMandatoryConfigTest { 18 | 19 | @RegisterExtension 20 | static final QuarkusUnitTest unitTest = new QuarkusUnitTest() 21 | .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)) 22 | .withConfigurationResource("application-splunk-logging-mandatory.properties") 23 | .assertException(e -> { 24 | System.out.println(e.getMessage()); 25 | assertSame(IllegalArgumentException.class, e.getClass()); 26 | assertTrue(e.getMessage().contains("quarkus.log.handler.splunk.token")); 27 | }); 28 | 29 | @Test 30 | void missingTokenThrowsIllegalArgumentException() { 31 | fail("Bootstrap should have failed due to missing token"); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /deployment/src/test/java/io/quarkiverse/logging/splunk/LoggingSplunkMinimalConfigTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2021 Amadeus s.a.s. 3 | Contributor(s): Kevin Viet, Romain Quinio (Amadeus s.a.s.) 4 | */ 5 | package io.quarkiverse.logging.splunk; 6 | 7 | import static org.mockserver.model.JsonBody.json; 8 | import static org.mockserver.model.Not.not; 9 | 10 | import org.jboss.logging.Logger; 11 | import org.jboss.shrinkwrap.api.ShrinkWrap; 12 | import org.jboss.shrinkwrap.api.spec.JavaArchive; 13 | import org.junit.jupiter.api.Test; 14 | import org.junit.jupiter.api.extension.RegisterExtension; 15 | 16 | import io.quarkus.test.QuarkusUnitTest; 17 | 18 | class LoggingSplunkMinimalConfigTest extends AbstractMockServerTest { 19 | 20 | @RegisterExtension 21 | static final QuarkusUnitTest unitTest = withMockServerConfig() 22 | .withConfigurationResource("application-splunk-logging-minimal.properties") 23 | .overrideConfigKey("quarkus.log.handler.splunk.token", "12345678-1234-1234-1234-1234567890AB") 24 | .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)); 25 | 26 | static final Logger logger = Logger.getLogger(LoggingSplunkMinimalConfigTest.class); 27 | 28 | @Test 29 | void indexIsNotSentIfUnspecified() { 30 | logger.info("hello splunk"); 31 | awaitMockServer(); 32 | httpServer.verify(requestToJsonEndpoint().withBody(not(json("{ index: ''}")))); 33 | } 34 | 35 | @Test 36 | void sourceTypeDefaultsToJson() { 37 | logger.info("hello splunk"); 38 | awaitMockServer(); 39 | httpServer.verify(requestToJsonEndpoint().withBody(json("{ sourcetype: '_json'}"))); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /deployment/src/test/java/io/quarkiverse/logging/splunk/LoggingSplunkNamedHandlerConfigTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2023 Amadeus s.a.s. 3 | Contributor(s): Kevin Viet, Romain Quinio, Yohann Puyhaubert (Amadeus s.a.s.) 4 | */ 5 | package io.quarkiverse.logging.splunk; 6 | 7 | import static org.mockserver.model.JsonBody.json; 8 | 9 | import org.jboss.logging.Logger; 10 | import org.jboss.shrinkwrap.api.ShrinkWrap; 11 | import org.jboss.shrinkwrap.api.spec.JavaArchive; 12 | import org.junit.jupiter.api.Test; 13 | import org.junit.jupiter.api.extension.RegisterExtension; 14 | 15 | import io.quarkus.test.QuarkusUnitTest; 16 | 17 | class LoggingSplunkNamedHandlerConfigTest extends AbstractMockServerTest { 18 | 19 | @RegisterExtension 20 | static final QuarkusUnitTest unitTest = withMockServerConfig() 21 | .withConfigurationResource("application-splunk-logging-named-handler.properties") 22 | .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)); 23 | 24 | static final Logger logger = Logger.getLogger(LoggingSplunkNamedHandlerConfigTest.class); 25 | 26 | static final Logger monitoringLogger = Logger.getLogger("monitoring"); 27 | 28 | @Test 29 | void indexWithDefaultLoggerAndNamedLogger() { 30 | logger.warn("hello splunk"); 31 | monitoringLogger.info("{\"key\":\"value\"}"); 32 | awaitMockServer(); 33 | httpServer.verify(requestToJsonEndpoint() 34 | .withBody(json("{ index: 'mylogindex'}")) 35 | .withHeader("Authorization", "Splunk 12345678-1234-1234-1234-1234567890AB")); 36 | httpServer.verify(requestToJsonEndpoint() 37 | .withBody(json("{ index: 'mystatsindex'}")) 38 | .withHeader("Authorization", "Splunk 12345678-0000-0000-0000-1234567890AB")); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /deployment/src/test/java/io/quarkiverse/logging/splunk/LoggingSplunkNestedSerializationTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2021 Amadeus s.a.s. 3 | Contributor(s): Kevin Viet, Romain Quinio (Amadeus s.a.s.) 4 | */ 5 | package io.quarkiverse.logging.splunk; 6 | 7 | import static org.hamcrest.MatcherAssert.*; 8 | import static org.hamcrest.Matchers.*; 9 | import static org.mockserver.model.JsonBody.json; 10 | import static org.mockserver.model.RegexBody.regex; 11 | 12 | import java.util.Arrays; 13 | import java.util.logging.Handler; 14 | import java.util.logging.Logger; 15 | 16 | import org.jboss.logging.MDC; 17 | import org.jboss.logmanager.ExtHandler; 18 | import org.jboss.shrinkwrap.api.ShrinkWrap; 19 | import org.jboss.shrinkwrap.api.spec.JavaArchive; 20 | import org.junit.jupiter.api.Test; 21 | import org.junit.jupiter.api.extension.RegisterExtension; 22 | import org.mockserver.verify.VerificationTimes; 23 | 24 | import io.quarkus.bootstrap.logging.InitialConfigurator; 25 | import io.quarkus.test.QuarkusUnitTest; 26 | 27 | class LoggingSplunkNestedSerializationTest extends AbstractMockServerTest { 28 | 29 | @RegisterExtension 30 | static final QuarkusUnitTest unitTest = withMockServerConfig() 31 | .withConfigurationResource("application-splunk-logging-nested.properties") 32 | .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)); 33 | 34 | static final org.jboss.logging.Logger logger = org.jboss.logging.Logger 35 | .getLogger(LoggingSplunkNestedSerializationTest.class); 36 | 37 | @Test 38 | void handlerShouldBeCreated() { 39 | ExtHandler delayedHandler = InitialConfigurator.DELAYED_HANDLER; 40 | assertThat(Logger.getLogger("").getHandlers(), hasItemInArray(delayedHandler)); 41 | Handler handler = Arrays.stream(delayedHandler.getHandlers()) 42 | .filter(h -> (h instanceof SplunkLogHandler)) 43 | .findFirst().orElse(null); 44 | assertThat(handler, notNullValue()); 45 | } 46 | 47 | @Test 48 | void handlerShouldFormatMessage() { 49 | logger.warnv("hello {0}", "splunk!"); 50 | awaitMockServer(); 51 | httpServer.verify(requestToJsonEndpoint().withBody(json("{ event: { message: 'hello splunk!' }}"))); 52 | } 53 | 54 | @Test 55 | void eventIsAJsonObjectWithMetadata() { 56 | logger.warn("hello splunk"); 57 | awaitMockServer(); 58 | httpServer.verify(requestToJsonEndpoint().withBody(json("{ event: { message: 'hello splunk' }}"))); 59 | } 60 | 61 | @Test 62 | void eventHasStandardMetadata() { 63 | logger.warn("hello splunk"); 64 | awaitMockServer(); 65 | httpServer.verify(requestToJsonEndpoint().withBody(json( 66 | "{ source: 'mysource', sourcetype: 'mysourcetype', index: 'myindex'} ")) 67 | .withBody(regex(".*host.*"))); 68 | } 69 | 70 | @Test 71 | void tokenIsSentAsAuthorizationHeader() { 72 | logger.warn("hello splunk"); 73 | awaitMockServer(); 74 | httpServer 75 | .verify(requestToJsonEndpoint().withHeader("Authorization", "Splunk 12345678-1234-1234-1234-1234567890AB")); 76 | } 77 | 78 | @Test 79 | void clientAddsMinimalMetadata() { 80 | logger.warn("hello splunk"); 81 | awaitMockServer(); 82 | httpServer.verify(requestToJsonEndpoint().withBody(json( 83 | "{ event: { message: 'hello splunk', severity:'WARN' }}"))); 84 | } 85 | 86 | @Test 87 | void mdcFieldsShouldBeSentAsMetadata() { 88 | MDC.put("mdc-key", "mdc-value"); 89 | logger.warn("hello mdc"); 90 | MDC.remove("mdc-key"); 91 | awaitMockServer(); 92 | httpServer.verify( 93 | requestToJsonEndpoint() 94 | .withBody(json("{ event: { message: 'hello mdc', properties: { 'mdc-key': 'mdc-value' }}}"))); 95 | } 96 | 97 | @Test 98 | void messageShouldContainException() { 99 | logger.error("unexpected error", new RuntimeException("test exception")); 100 | awaitMockServer(); 101 | httpServer.verify(requestToJsonEndpoint() 102 | .withBody(regex(".*unexpected error: java.lang.RuntimeException: test exception.*"))); 103 | } 104 | 105 | @Test 106 | void logLevelShouldBeUsed() { 107 | logger.info("Info log"); 108 | httpServer.verify(requestToJsonEndpoint().withBody(json("{ event: { message: 'Info log' }}")), 109 | VerificationTimes.exactly(0)); 110 | } 111 | 112 | @Test 113 | void clientAddsStructuredMetadata() { 114 | logger.error("hello splunk", new RuntimeException("test exception")); 115 | awaitMockServer(); 116 | httpServer.verify(requestToJsonEndpoint().withBody(regex(".*hello splunk.*"))); 117 | httpServer.verify(requestToJsonEndpoint().withBody(json( 118 | "{ event: { logger:'io.quarkiverse.logging.splunk.LoggingSplunkNestedSerializationTest', " + 119 | "exception: 'test exception' }}"))); 120 | httpServer.verify(requestToJsonEndpoint().withBody(regex(".*thread.*"))); 121 | } 122 | 123 | @Test 124 | void staticMetadataFields() { 125 | logger.warn("hello splunk"); 126 | awaitMockServer(); 127 | httpServer.verify(requestToJsonEndpoint().withBody( 128 | json("{ fields: { metadata-0: 'value0', metadata-1: 'value1' } }"))); 129 | } 130 | 131 | @Test 132 | void nestedJsonMessageIsParsed() { 133 | logger.warn("{ 'greeting': 'hello', 'user': 'splunk' }"); 134 | awaitMockServer(); 135 | httpServer.verify( 136 | requestToJsonEndpoint().withBody(json("{ event: { message: { 'greeting': 'hello', 'user': 'splunk' }}}"))); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /deployment/src/test/java/io/quarkiverse/logging/splunk/LoggingSplunkRawSerializationTest.java: -------------------------------------------------------------------------------- 1 | package io.quarkiverse.logging.splunk; 2 | 3 | import org.jboss.shrinkwrap.api.ShrinkWrap; 4 | import org.jboss.shrinkwrap.api.spec.JavaArchive; 5 | import org.junit.jupiter.api.Test; 6 | import org.junit.jupiter.api.extension.RegisterExtension; 7 | 8 | import io.quarkus.test.QuarkusUnitTest; 9 | 10 | class LoggingSplunkRawSerializationTest extends AbstractMockServerTest { 11 | 12 | @RegisterExtension 13 | static final QuarkusUnitTest unitTest = withMockServerConfig() 14 | .withConfigurationResource("application-splunk-logging-raw.properties") 15 | .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)); 16 | 17 | static final org.jboss.logging.Logger logger = org.jboss.logging.Logger.getLogger(LoggingSplunkRawSerializationTest.class); 18 | 19 | @Test 20 | void sendsTheRawEvent() { 21 | logger.warn("hello splunk"); 22 | awaitMockServer(); 23 | httpServer.verify(requestToRawEndpoint().withBody("hello splunk")); 24 | } 25 | 26 | @Test 27 | void sendsMetadata() { 28 | logger.warn("hello splunk"); 29 | awaitMockServer(); 30 | httpServer.verify(requestToRawEndpoint() 31 | .withQueryStringParameter("index", "myindex") 32 | .withQueryStringParameter("source", "mysource") 33 | .withQueryStringParameter("sourcetype", "mysourcetype")); 34 | } 35 | 36 | @Test 37 | void sendsAuthenticationHeader() { 38 | logger.warn("hello splunk"); 39 | awaitMockServer(); 40 | httpServer.verify(requestToRawEndpoint().withHeader( 41 | "Authorization", "Splunk 12345678-1234-1234-1234-1234567890AB")); 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /deployment/src/test/java/io/quarkiverse/logging/splunk/LoggingSplunkSendErrorTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2021 Amadeus s.a.s. 3 | Contributor(s): Kevin Viet, Romain Quinio (Amadeus s.a.s.) 4 | */ 5 | package io.quarkiverse.logging.splunk; 6 | 7 | import static java.util.concurrent.TimeUnit.SECONDS; 8 | import static org.awaitility.Awaitility.await; 9 | import static org.mockserver.model.HttpRequest.request; 10 | import static org.mockserver.model.HttpResponse.response; 11 | import static org.mockserver.model.JsonBody.json; 12 | 13 | import org.jboss.logging.Logger; 14 | import org.jboss.shrinkwrap.api.ShrinkWrap; 15 | import org.jboss.shrinkwrap.api.spec.JavaArchive; 16 | import org.junit.jupiter.api.AfterAll; 17 | import org.junit.jupiter.api.AfterEach; 18 | import org.junit.jupiter.api.BeforeAll; 19 | import org.junit.jupiter.api.Disabled; 20 | import org.junit.jupiter.api.Test; 21 | import org.junit.jupiter.api.extension.RegisterExtension; 22 | import org.mockserver.integration.ClientAndServer; 23 | import org.mockserver.model.ClearType; 24 | import org.mockserver.verify.VerificationTimes; 25 | 26 | import io.quarkus.test.QuarkusUnitTest; 27 | 28 | @Disabled("This test is not so important, and there's no clear evidence of " + 29 | "why it's flaky and may come from the QuarkusUnitTest harness") 30 | class LoggingSplunkSendErrorTest { 31 | 32 | @RegisterExtension 33 | static final QuarkusUnitTest unitTest = AbstractMockServerTest.withMockServerConfig() 34 | .withConfigurationResource("application-splunk-logging-failure.properties") 35 | .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class).addClass(AbstractMockServerTest.class)); 36 | 37 | static final Logger logger = Logger.getLogger(LoggingSplunkSendErrorTest.class); 38 | 39 | static ClientAndServer httpServer; 40 | 41 | @BeforeAll 42 | public static void setUpOnce() { 43 | httpServer = ClientAndServer.startClientAndServer(8088); 44 | // Reject a specific request (ex: wrong token, ...) 45 | httpServer.when(request().withBody(json("{ event: { message:'error splunk'} }"))) 46 | .respond(response().withStatusCode(401)); 47 | } 48 | 49 | @AfterAll 50 | public static void tearDownOnce() { 51 | httpServer.stop(); 52 | } 53 | 54 | @AfterEach 55 | public void tearDown() { 56 | httpServer.clear(request(), ClearType.LOG); 57 | } 58 | 59 | @Test 60 | void testSendError() { 61 | logger.info("error splunk"); 62 | await().atMost(1, SECONDS).until(() -> httpServer.retrieveRecordedRequests(request()).length != 0); 63 | // The retries-on-error is not applicable in case of HTTP error 64 | httpServer.verify(request().withBody(json("{ event: { message:'error splunk'} }")), 65 | VerificationTimes.exactly(1)); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /deployment/src/test/java/io/quarkiverse/logging/splunk/LoggingSplunkSendExceptionTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2021 Amadeus s.a.s. 3 | Contributor(s): Kevin Viet, Romain Quinio (Amadeus s.a.s.) 4 | */ 5 | package io.quarkiverse.logging.splunk; 6 | 7 | import static org.mockserver.model.HttpError.error; 8 | import static org.mockserver.model.HttpRequest.request; 9 | import static org.mockserver.model.JsonBody.json; 10 | 11 | import org.jboss.logging.Logger; 12 | import org.jboss.shrinkwrap.api.ShrinkWrap; 13 | import org.jboss.shrinkwrap.api.spec.JavaArchive; 14 | import org.junit.jupiter.api.AfterAll; 15 | import org.junit.jupiter.api.BeforeAll; 16 | import org.junit.jupiter.api.Test; 17 | import org.junit.jupiter.api.extension.RegisterExtension; 18 | import org.mockserver.integration.ClientAndServer; 19 | import org.mockserver.verify.VerificationTimes; 20 | 21 | import io.quarkus.test.QuarkusUnitTest; 22 | 23 | class LoggingSplunkSendExceptionTest { 24 | 25 | @RegisterExtension 26 | static final QuarkusUnitTest unitTest = AbstractMockServerTest.withMockServerConfig() 27 | .withConfigurationResource("application-splunk-logging-failure.properties") 28 | .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class).addClass(AbstractMockServerTest.class)); 29 | 30 | static final Logger logger = Logger.getLogger(LoggingSplunkSendExceptionTest.class); 31 | 32 | static ClientAndServer httpServer; 33 | 34 | @BeforeAll 35 | public static void setUpOnce() { 36 | httpServer = ClientAndServer.startClientAndServer(8088); 37 | // Drop connections to trigger I/O exceptions in HTTP client 38 | httpServer.when(request()).error(error().withDropConnection(true)); 39 | } 40 | 41 | @AfterAll 42 | public static void tearDownOnce() { 43 | httpServer.stop(); 44 | } 45 | 46 | @Test 47 | void testSendError() throws InterruptedException { 48 | logger.info("error starting splunk"); 49 | // HTTP client connections happens on a separate thread. 50 | Thread.sleep(5000); 51 | // Should be retried at least once (actually more, maybe happens at a lower level). 52 | httpServer.verify(request().withBody(json("{ event: { message:'error starting splunk'} }")), 53 | VerificationTimes.atLeast(2)); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /deployment/src/test/resources/application-splunk-logging-async.properties: -------------------------------------------------------------------------------- 1 | quarkus.log.handler.splunk.enabled=true 2 | quarkus.log.handler.splunk.level=WARN 3 | quarkus.log.handler.splunk.format=%s%e 4 | quarkus.log.handler.splunk.token=12345678-1234-1234-1234-1234567890AB 5 | # Async at handler level 6 | quarkus.log.handler.splunk.async=true 7 | quarkus.log.handler.splunk.async.queue-length=512 8 | quarkus.log.handler.splunk.async.overflow=block 9 | # Async at HTTP client level 10 | quarkus.log.handler.splunk.send-mode=parallel 11 | -------------------------------------------------------------------------------- /deployment/src/test/resources/application-splunk-logging-disabled.properties: -------------------------------------------------------------------------------- 1 | quarkus.log.handler.splunk.enabled=false 2 | -------------------------------------------------------------------------------- /deployment/src/test/resources/application-splunk-logging-failure.properties: -------------------------------------------------------------------------------- 1 | # Disable console to verify original events are still logged in case of failure 2 | quarkus.log.console.enable=false 3 | quarkus.log.handler.splunk.enabled=true 4 | quarkus.log.handler.splunk.format=%s%e 5 | quarkus.log.handler.splunk.token=12345678-1234-1234-1234-1234567890AB 6 | quarkus.log.handler.splunk.max-retries=1 7 | -------------------------------------------------------------------------------- /deployment/src/test/resources/application-splunk-logging-filtering.properties: -------------------------------------------------------------------------------- 1 | quarkus.log.handler.splunk.enabled=true 2 | quarkus.log.handler.splunk.token=12345678-1234-1234-1234-1234567890AB 3 | quarkus.log.handler.splunk.filter=my-filter -------------------------------------------------------------------------------- /deployment/src/test/resources/application-splunk-logging-flat.properties: -------------------------------------------------------------------------------- 1 | quarkus.log.handler.splunk.enabled=true 2 | quarkus.log.level=INFO 3 | quarkus.log.handler.splunk.serialization=flat 4 | quarkus.log.handler.splunk.level=WARN 5 | quarkus.log.handler.splunk.format=%s%e 6 | quarkus.log.handler.splunk.token=12345678-1234-1234-1234-1234567890AB 7 | quarkus.log.handler.splunk.metadata-source=mysource 8 | quarkus.log.handler.splunk.metadata-index=myindex 9 | quarkus.log.handler.splunk.include-exception=true 10 | quarkus.log.handler.splunk.include-logger-name=true 11 | quarkus.log.handler.splunk.include-thread-name=true 12 | quarkus.log.handler.splunk.metadata-fields.metadata-0=value0 13 | quarkus.log.handler.splunk.metadata-fields.metadata-1=value1 14 | quarkus.log.handler.splunk.metadata-severity-field-name=level 15 | -------------------------------------------------------------------------------- /deployment/src/test/resources/application-splunk-logging-mandatory.properties: -------------------------------------------------------------------------------- 1 | quarkus.log.handler.splunk.enabled=true -------------------------------------------------------------------------------- /deployment/src/test/resources/application-splunk-logging-minimal.properties: -------------------------------------------------------------------------------- 1 | quarkus.log.handler.splunk.enabled=true 2 | quarkus.log.handler.splunk.token=12345678-1234-1234-1234-1234567890AB -------------------------------------------------------------------------------- /deployment/src/test/resources/application-splunk-logging-named-handler.properties: -------------------------------------------------------------------------------- 1 | quarkus.log.handler.splunk.token=12345678-1234-1234-1234-1234567890AB 2 | quarkus.log.handler.splunk.level=WARN 3 | quarkus.log.handler.splunk.format=%s%e 4 | quarkus.log.handler.splunk.metadata-index=mylogindex 5 | quarkus.log.handler.splunk."MONITORING".url=http://localhost:8088 6 | quarkus.log.handler.splunk."MONITORING".token=12345678-0000-0000-0000-1234567890AB 7 | quarkus.log.handler.splunk."MONITORING".level=INFO 8 | quarkus.log.handler.splunk."MONITORING".metadata-index=mystatsindex 9 | # Avoid batching and send events immediately, to make unit tests more synchronous 10 | # Note that OKHttp client still executes its I/O on a separate thread 11 | quarkus.log.handler.splunk."MONITORING".batch-interval=0 12 | quarkus.log.handler.splunk."MONITORING".batch-size-bytes=0 13 | quarkus.log.handler.splunk."MONITORING".batch-size-count=0 14 | quarkus.log.handler.splunk."MONITORING".send-mode=sequential 15 | quarkus.log.category."monitoring".handlers=MONITORING 16 | quarkus.log.category."monitoring".use-parent-handlers=false 17 | -------------------------------------------------------------------------------- /deployment/src/test/resources/application-splunk-logging-nested.properties: -------------------------------------------------------------------------------- 1 | quarkus.log.handler.splunk.enabled=true 2 | quarkus.log.level=INFO 3 | # quarkus.log.handler.splunk.serialization=nested 4 | quarkus.log.handler.splunk.level=WARN 5 | quarkus.log.handler.splunk.format=%s%e 6 | quarkus.log.handler.splunk.token=12345678-1234-1234-1234-1234567890AB 7 | quarkus.log.handler.splunk.metadata-source=mysource 8 | quarkus.log.handler.splunk.metadata-source-type=mysourcetype 9 | quarkus.log.handler.splunk.metadata-index=myindex 10 | quarkus.log.handler.splunk.include-exception=true 11 | quarkus.log.handler.splunk.include-logger-name=true 12 | quarkus.log.handler.splunk.include-thread-name=true 13 | quarkus.log.handler.splunk.metadata-fields.metadata-0=value0 14 | quarkus.log.handler.splunk.metadata-fields.metadata-1=value1 15 | -------------------------------------------------------------------------------- /deployment/src/test/resources/application-splunk-logging-raw.properties: -------------------------------------------------------------------------------- 1 | quarkus.log.handler.splunk.enabled=true 2 | quarkus.log.level=INFO 3 | quarkus.log.handler.splunk.level=WARN 4 | quarkus.log.handler.splunk.format=%s%e 5 | quarkus.log.handler.splunk.token=12345678-1234-1234-1234-1234567890AB 6 | quarkus.log.handler.splunk.metadata-source=mysource 7 | quarkus.log.handler.splunk.metadata-source-type=mysourcetype 8 | quarkus.log.handler.splunk.metadata-index=myindex 9 | # Deprecated => quarkus.log.handler.splunk.serialization=raw 10 | quarkus.log.handler.splunk.raw=true -------------------------------------------------------------------------------- /docs/antora.yml: -------------------------------------------------------------------------------- 1 | name: quarkus-logging-splunk 2 | title: Logging Splunk 3 | version: dev 4 | nav: 5 | - modules/ROOT/nav.adoc 6 | -------------------------------------------------------------------------------- /docs/modules/ROOT/nav.adoc: -------------------------------------------------------------------------------- 1 | * xref:index.adoc[Quarkus logging splunk] 2 | -------------------------------------------------------------------------------- /docs/modules/ROOT/pages/includes/README.md: -------------------------------------------------------------------------------- 1 | The file `quarkus-log-handler-splunk.adoc` needs to be committed to git for the build of quarkiverse-docs. 2 | It's regenerated when doing a full build from project root, while quarkiverse-docs only does a partial build of /docs folder. 3 | -------------------------------------------------------------------------------- /docs/modules/ROOT/pages/includes/attributes.adoc: -------------------------------------------------------------------------------- 1 | :project-version: 4.0.3 2 | 3 | :examples-dir: ./../examples/ -------------------------------------------------------------------------------- /docs/modules/ROOT/pages/index.adoc: -------------------------------------------------------------------------------- 1 | = Quarkus logging splunk 2 | 3 | == Introduction 4 | 5 | https://www.splunk.com/en_us/software/splunk-enterprise.html[Splunk] is a middleware 6 | solution that receives, stores, indexes and finally allows to exploit the logs of an application. 7 | 8 | This Quarkus extension provides the support of the official Splunk client library to index log events through the HTTP Event collection, provided by Splunk enterprise solution. 9 | 10 | - The official client is an opensource library available https://github.com/splunk/splunk-library-javalogging[here]. 11 | - The documentation of HTTP Event collection can be found https://docs.splunk.com/Documentation/Splunk/8.1.1/Data/UsetheHTTPEventCollector[here]. 12 | 13 | == Installation 14 | 15 | If you want to use this extension, you need to add the `quarkus-logging-splunk` extension first. 16 | In your `pom.xml` file, add: 17 | 18 | [source,xml] 19 | ---- 20 | 21 | io.quarkiverse.logging.splunk 22 | quarkus-logging-splunk 23 | {project-version} 24 | 25 | ---- 26 | 27 | == Features 28 | 29 | The extension can be used transparently with any log frontend used by Quarkus (Log4j, SLF4J, ... ). 30 | 31 | === Log message formatting 32 | 33 | In all cases the log message formatter is aligned by default with the one of Quarkus console handler: 34 | 35 | [source,properties] 36 | ---- 37 | quarkus.log.handler.splunk.format="%d{yyyy-MM-dd HH:mm:ss,SSS} %-5p [%c{3.}] (%t) %s%e%n" 38 | ---- 39 | 40 | This can be adapted in order to avoid duplication with metadata that are passed in a structured way. 41 | 42 | === Log event metadata 43 | 44 | The type of metadata depends on the serialization format. 45 | 46 | If `quarkus.log.handler.splunk.raw` is enabled or `quarkus.log.handler.splunk.serialization` is `raw`, there are no per-event metadata. 47 | Only few global metadata shared between all events of a batch are sent via HTTP headers and query parameters. 48 | 49 | In other cases, the extension uses structured logging, via JSON serialization. 50 | There are two supported structured formats: 51 | 52 | * The `nested` serialization is the default format of Splunk HEC Java client and defines the name of some pre-defined metadata. Combined with `quarkus.log.handler.splunk.format=%s%e` it also support log messages that are themselves JSON. 53 | * The `flat` serialization is a simpler and more generic format, also used by the https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/logs/data-model.md#splunk-hec[OpenTelemetry Splunk HEC exporter]. 54 | 55 | Some metadata can be indexed by Splunk, see link:++https://docs.splunk.com/Splexicon:Indexedfield++[indexed fields]. 56 | The default `_json` source type indexes metadata passed in the `fields` object. 57 | 58 | The extension provides the support of the resolution of MDC scoped properties, as defined in https://access.redhat.com/documentation/en-us/red_hat_jboss_enterprise_application_platform/7.0/html/configuration_guide/logging_with_jboss_eap#log_formatters[JBoss supported formatters]. 59 | 60 | [%header,cols="h,a,a"] 61 | |=== 62 | |Serialization format 63 | |`nested` 64 | |`flat` 65 | 66 | |HEC metadata 67 | 2+|`time` and `host` are always sent. 68 | `source`, `sourcetype`, `index` are sent if not empty. 69 | 70 | |Pre-defined metadata 71 | |Only `event.severity` is sent by default. 72 | Other metadata can be added: 73 | 74 | * `event.thread` via `quarkus.log.handler.splunk.include-thread-name` 75 | * `event.exception` via `quarkus.log.handler.splunk.include-exception` 76 | * `event.logger` via `quarkus.log.handler.splunk.include-logger-name` 77 | |Only `fields.severity` is sent by default. 78 | The metadata name can be customized via `quarkus.log.handler.splunk.metadataSeverityFieldName` 79 | Other metadata can be added: 80 | 81 | * `fields.thread` via `quarkus.log.handler.splunk.include-thread-name` 82 | * `fields.exception` via `quarkus.log.handler.splunk.include-exception` 83 | * `fields.logger` via `quarkus.log.handler.splunk.include-logger-name` 84 | |MDC properties 85 | |Passed via `event.properties` 86 | |Passed via `fields` 87 | 88 | |Static metadata 89 | 2+|Passed via `fields` 90 | 91 | |=== 92 | 93 | A structured query to Splunk HEC looks like: 94 | 95 | curl -k -v -X POST https://localhost:8080/services/collector/event/1.0 -H "Content-type: application/json; profile=\"urn:splunk:event:1.0\"; charset=utf-8" -H "Authorization: Splunk 29fe2838-cab6-4d17-a392-37b7b8f41f75" -d@events.json 96 | 97 | .Nested serialization example 98 | [source,json] 99 | ---- 100 | { 101 | "time": "1673001538.042", 102 | "host": "hostname", 103 | "source": "mysource", 104 | "sourcetype": "_json", 105 | "index": "main", 106 | "event": { 107 | "message": "2023-01-06 ERROR The log message", 108 | "logger": "com.acme.MyClass", 109 | "severity": "ERROR", 110 | "exception": "java.lang.NullPointerException", 111 | "properties": { 112 | "mdc-key": "mdc-value" 113 | } 114 | }, 115 | "fields": { 116 | "key": "static-value" 117 | } 118 | } 119 | ---- 120 | 121 | .Flat serialization example 122 | [source,json] 123 | ---- 124 | { 125 | "time": "1673001538.042", 126 | "host": "hostname", 127 | "source": "mysource", 128 | "index": "main", 129 | "event": "2023-01-06 ERROR The log message", 130 | "fields": { 131 | "severity": "ERROR", 132 | "mdc-key": "mdc-value", 133 | "key": "static-value" 134 | } 135 | } 136 | ---- 137 | 138 | === Connectivity failures 139 | 140 | Batched events that cannot be sent to the Splunk indexer will be logged to stdout: 141 | 142 | * Formatted using console handler settings if the console handler is enabled 143 | * Formatted using splunk handler settings otherwise 144 | 145 | In any case, the root cause of the failure is always logged to stderr. 146 | 147 | === Asynchronous handler 148 | 149 | By default, the log handler is synchronous and only the HTTP requests to HEC endpoint are done asynchronously: 150 | 151 | [plantuml, sync, format=svg] 152 | .... 153 | participant "Application" as App 154 | participant Slf4j 155 | participant SplunkLogHandler 156 | participant "Splunk library" as Lib 157 | participant "HTTP client" as OkHttp 158 | participant "Splunk HEC" as HEC 159 | 160 | group Application thread 161 | App -> Slf4j: info(message) 162 | Slf4j -> SplunkLogHandler: doPublish(record) 163 | SplunkLogHandler -> Lib: send(record) 164 | note left 165 | synchronized 166 | end note 167 | Lib -> Lib: Add event to batch 168 | alt batch is full 169 | Lib -> OkHttp: enqueue(HTTP request) 170 | OkHttp --> Lib 171 | end 172 | Lib --> App 173 | 174 | end 175 | group HTTP client - multiple connections in parallel mode. 176 | OkHttp -> OkHttp: Peek from queue 177 | OkHttp -> HEC: HTTP POST /services/collector/event/1.0 178 | HEC --> OkHttp: 200 179 | alt status code != 200 180 | OkHttp --> Lib: handle errors 181 | Lib -> Lib: stderr.println 182 | end 183 | end 184 | .... 185 | 186 | This can be an issue because the Splunk library `#send` is synchronized, so any preprocessing of the batch HTTP request itself happens on the application thread of the log event that triggered the batch to be full (either by reaching `quarkus.log.handler.splunk.batch-size-count` or `quarkus.log.handler.splunk.batch-size-bytes`) 187 | 188 | By enabling `quarkus.log.handler.splunk.async=true`, an intermediate event queue is used, which decouples the flushing of the batch from any application thread: 189 | 190 | [plantuml, async, format=svg] 191 | .... 192 | participant "Application" as App 193 | participant Slf4j 194 | participant AsyncHandler 195 | participant SplunkLogHandler 196 | participant "Splunk library" as Lib 197 | participant "HTTP client" as OkHttp 198 | participant "Splunk HEC" as HEC 199 | 200 | group Application thread 201 | App -> Slf4j: info(message) 202 | Slf4j -> AsyncHandler: doPublish(record) 203 | AsyncHandler -> AsyncHandler: Capture MDC 204 | AsyncHandler -> AsyncHandler: Add to queue 205 | AsyncHandler --> App 206 | end 207 | 208 | group AsyncHandler single (daemon) thread 209 | AsyncHandler -> AsyncHandler: Peek from queue 210 | AsyncHandler -> SplunkLogHandler: doPublish(record) 211 | SplunkLogHandler -> Lib: send(record) 212 | note left 213 | synchronized 214 | end note 215 | Lib -> Lib: Add event to batch 216 | alt batch is full 217 | Lib -> OkHttp: enqueue(HTTP request) 218 | OkHttp --> Lib 219 | end 220 | Lib --> SplunkLogHandler 221 | SplunkLogHandler --> AsyncHandler 222 | 223 | end 224 | group HTTP client - multiple connections in parallel mode. 225 | OkHttp -> OkHttp: Peek from queue 226 | OkHttp -> HEC: HTTP POST /services/collector/event/1.0 227 | HEC --> OkHttp: 200 228 | alt status code != 200 229 | OkHttp --> Lib: handle errors 230 | Lib -> Lib: stderr.println 231 | end 232 | end 233 | .... 234 | 235 | By default `quarkus.log.handler.splunk.async.overflow=block`, so applicative threads will block once the queue limit has reached `quarkus.log.handler.splunk.async.queue-length`. 236 | 237 | There's no link between `quarkus.log.handler.splunk.async.queue-length` and `quarkus.log.handler.splunk.batch-size-count`. 238 | 239 | === Sequential and parallel modes 240 | 241 | The number of events kept in memory for batching purposes is not limited. 242 | After tuning `quarkus.log.handler.splunk.batch-size-count` and `quarkus.log.handler.splunk.batch-size-bytes`, in case the HEC endpoint cannot keep up with the batch throughput, using multiple HTTP connections might help to reduce memory usage on the client. 243 | 244 | By setting `quarkus.log.handler.splunk.send-mode=parallel` multiple batches will be sent over the wire in parallel, potentially increasing throughput with the HEC endpoint. 245 | 246 | === Named Splunk log handlers 247 | 248 | A named log handler can be configured to manage multiple Splunk configurations for particular log emissions. Like for core Quarkus handlers (*console*, *file* or *syslog*), 249 | Splunk named handlers follow the same configuration: 250 | 251 | ``` 252 | # Global configuration 253 | quarkus.log.handler.splunk.token=12345678-1234-1234-1234-1234567890AB 254 | quarkus.log.handler.splunk.metadata-index=mylogindex 255 | 256 | # Splunk named handler configuration, named here MONITORING 257 | quarkus.log.handler.splunk."MONITORING".token=12345678-0000-0000-0000-1234567890AB 258 | quarkus.log.handler.splunk."MONITORING".metadata-index=mystatsindex 259 | 260 | # Registration of the custom handler through Quarkus core category management, here monitoring as the logging category 261 | quarkus.log.category."monitoring".handlers=MONITORING 262 | quarkus.log.category."monitoring".use-parent-handlers=false 263 | ``` 264 | 265 | Next to use such logger in actual code, you can rely on annotation or factory: 266 | 267 | * With annotation: 268 | ``` 269 | @LoggerName("monitoring") 270 | Logger monitoringLogger; 271 | ``` 272 | 273 | * With factory: 274 | ``` 275 | static final Logger monitoringLogger = Logger.getLogger("monitoring"); 276 | ``` 277 | 278 | ==== Some important considerations 279 | * Every handler is isolated and uses a separate Splunk client and connection pool, which means it has a cost. 280 | * The configuration from the root handler are not inherited by named handlers. 281 | * Use `quarkus.log.category."named-handler".use-parent-handlers=false` is required if you do not want the root handler to also receive log events already sent to named handlers. 282 | 283 | == Developer experience 284 | 285 | To enhance the developer experience, some integration in the Development mode of Quarkus is provided. 286 | 287 | === Dev service 288 | 289 | The extension provides a Dev Service that starts in background a splunk container. 290 | It is deactivated by default, to maintain the compatibility with disabling the splunk extension at runtime. 291 | 292 | ==== Activation 293 | 294 | To activate the dev service, the following needs to be configured with this link:#quarkus-log-handler-splunk_quarkus-log-handler-splunk-devservices-enabled[property]: 295 | 296 | [source,properties] 297 | ---- 298 | quarkus.log.handler.splunk.devservices.enabled=true 299 | ---- 300 | 301 | Obviously in "normal" mode (not dev, not test), this has no effect. 302 | The https://quarkus.io/guides/dev-services[Quarkus dev services] framework picks up the configuration and starts the splunk container. 303 | When eventually the container is considered started, some configuration is injected to expose the link:#quarkus-log-handler-splunk_quarkus-log-handler-splunk-url[random] port on which splunk is listening. 304 | It also injects the following: 305 | 306 | [source,properties] 307 | ---- 308 | quarkus.log.handler.splunk.token=local-dev-token 309 | quarkus.log.handler.splunk.disable-certificate-validation=true 310 | quarkus.log.handler.splunk.enabled=true 311 | ---- 312 | 313 | Namely: 314 | 315 | * the link:#quarkus-log-handler-splunk_quarkus-log-handler-splunk-token[HEC token] configured in the Splunk container at boot 316 | * Splunk enforces HTTPS on endpoints but with self-signed certificates, so we need to link:#quarkus-log-handler-splunk_quarkus-log-handler-splunk-disable-certificate-validation[ignore certificate validation] 317 | * forcing the link:#quarkus-log-handler-splunk_quarkus-log-handler-splunk-enabled[activation] of the `quarkus-logging-splunk` extension when its dev service has been activated at build time. 318 | 319 | ==== Support of named handlers 320 | 321 | Named handlers are supported through additional build time configuration. 322 | Example: 323 | 324 | [source,properties] 325 | ---- 326 | quarkus.log.handler.splunk.devservices.plug-named-handlers.=true 327 | ---- 328 | 329 | link:[Here] is the config property table entry. 330 | 331 | The result is that it will override for that named handler the following configuration: 332 | 333 | * the link:#quarkus-log-handler-splunk_quarkus-log-handler-splunk-named-handlers-url[HEC endpoint] 334 | * the link:#quarkus-log-handler-splunk_quarkus-log-handler-splunk-named-handlers-token[token] 335 | * remove of link:#quarkus-log-handler-splunk_quarkus-log-handler-splunk-named-handlers-disable-certificate-validation[certificate verification] 336 | * force link:#quarkus-log-handler-splunk_quarkus-log-handler-splunk-named-handlers-enabled[enabled] the splunk log handler 337 | 338 | ==== Usage in tests 339 | 340 | a `quarkus-logging-splunk-test-utils` module proposes some test framework layer allowing access to the link:#quarkus-log-handler-splunk_quarkus-log-handler-splunk-devservices-api-url[Splunk API URL]. 341 | This is useful to launch searches after the test run to do some assertions on the logs eventually sent to Splunk. 342 | It is definitely not to be used in all tests, as it quite lengthens the test run time. 343 | I.e. the start of splunk takes about 30s. 344 | And there needs to be added some delay after the test run to make sure log entries have been properly propagated to Splunk. 345 | 346 | The search in Splunk can be done using its https://dev.splunk.com/enterprise/reference/[API]. 347 | We propose a custom QuarkusTestResourceLifecycleManager to inject the URL to the Splunk API (for compatibility with `QuarkusIntegrationTest` when `microprofile-config` injection is disallowed): 348 | 349 | .Example of usage of test utils 350 | [source,java] 351 | ---- 352 | @QuarkusTest 353 | @QuarkusTestResource(LoggingSplunkInjectionTestResource.class) // <1> 354 | class MyQuarkusTest { 355 | @LoggingSplunkApiUrl // <2> 356 | String splunkApiUrl; 357 | 358 | @Test 359 | void test() { 360 | RestAssured.given() 361 | .request() 362 | .formParam("search", "search \"hello splunk\"") 363 | .formParam("exec_mode", "oneshot") 364 | .relaxedHTTPSValidation() 365 | .auth() 366 | .basic("admin", "admin123") // <3> 367 | .log() 368 | .ifValidationFails() 369 | .post(splunkApiUrl + "/services/search/jobs") // <4> 370 | .then() 371 | .statusCode(200) 372 | .body(containsString("hello splunk"), containsString("mdc-value")); 373 | } 374 | } 375 | ---- 376 | 377 | <1> The `QuarkusTestResource` to declare, which will also be picked by the potential `IT` test extending this `QuarkusTest` class. 378 | <2> The annotation to use on a `String` field, where the Splunk API will be injected 379 | <3> The default credentials configured in the Splunk container (user admin, password admin123) 380 | <4> The URL injected has the `https://host:port` pattern, so it need to be completed with the actual service path you want to access. 381 | 382 | ==== Additional configuration 383 | 384 | To customize a bit the splunk container started, a few configuration options are given: 385 | 386 | * customize the actual container image used through `link:#quarkus-log-handler-splunk_quarkus-log-handler-splunk-devservices-image-name[quarkus.log.handler.splunk.devservices.image-name]` 387 | * enforce that in dev mode the splunk instances are shared between runs of microservices run in Dev mode with `link:#quarkus-log-handler-splunk_quarkus-log-handler-splunk-devservices-shared[quarkus.log.handler.splunk.devservices.shared]` (boolean). 388 | Default is shared true. 389 | * Add/customize environment variables with `link:#quarkus-log-handler-splunk_quarkus-log-handler-splunk-devservices-container-env-container-env[quarkus.log.handler.splunk.devservices.container-env]`. 390 | Map of key values. 391 | 392 | == Extension Configuration Reference 393 | 394 | This extension follows the `log handlers` configuration domain that is defined by Quarkus, every configuration property of this extension will belong to the following configuration root : `quarkus.log.handler.splunk` 395 | 396 | When present this extension is enabled by default, meaning the client would expect a valid connection to a Splunk indexer and would print an error message for every log created by the application. 397 | 398 | So in local environment, the log handler can be disabled with the following property : 399 | 400 | [source,properties] 401 | ---- 402 | quarkus.log.handler.splunk.enabled=false 403 | ---- 404 | 405 | Every configuration property of the extension is overridable at runtime. 406 | 407 | include::includes/quarkus-log-handler-splunk.adoc[leveloffset=+1, opts=optional] 408 | -------------------------------------------------------------------------------- /docs/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | io.quarkiverse.logging.splunk 6 | quarkus-logging-splunk-parent 7 | 4.0.0-SNAPSHOT 8 | ../pom.xml 9 | 10 | 11 | quarkus-logging-splunk-docs 12 | Splunk logging extension - Documentation 13 | 14 | 15 | false 16 | 17 | 18 | 19 | 20 | 21 | io.quarkiverse.logging.splunk 22 | quarkus-logging-splunk-deployment 23 | ${project.version} 24 | 25 | 26 | 27 | 28 | 29 | 30 | it.ozimov 31 | yaml-properties-maven-plugin 32 | 33 | 34 | initialize 35 | 36 | read-project-properties 37 | 38 | 39 | 40 | ${project.basedir}/../.github/project.yml 41 | 42 | 43 | 44 | 45 | 46 | 47 | maven-resources-plugin 48 | 49 | 50 | copy-resources 51 | generate-resources 52 | 53 | copy-resources 54 | 55 | 56 | ${project.basedir}/modules/ROOT/pages/includes/ 57 | 58 | 59 | ${project.basedir}/../target/asciidoc/generated/config/ 60 | 61 | quarkus-log-handler-splunk.adoc 62 | false 63 | 64 | 65 | ${project.basedir}/templates/includes 66 | attributes.adoc 67 | true 68 | 69 | 70 | ${skip.config.copy} 71 | 72 | 73 | 74 | copy-images 75 | prepare-package 76 | 77 | copy-resources 78 | 79 | 80 | ${project.build.directory}/generated-docs/_images/ 81 | 82 | 83 | ${project.basedir}/modules/ROOT/assets/images/ 84 | false 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | org.asciidoctor 93 | asciidoctor-maven-plugin 94 | 95 | 96 | asciidoctor-diagram 97 | 98 | 99 | 100 | 101 | org.asciidoctor 102 | asciidoctorj-diagram 103 | 2.3.1 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | release 113 | 114 | 115 | true 116 | 117 | 118 | 119 | 120 | -------------------------------------------------------------------------------- /docs/templates/includes/attributes.adoc: -------------------------------------------------------------------------------- 1 | :project-version: ${release.current-version} 2 | 3 | :examples-dir: ./../examples/ -------------------------------------------------------------------------------- /integration-tests/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | io.quarkiverse.logging.splunk 5 | quarkus-logging-splunk-parent 6 | 4.0.0-SNAPSHOT 7 | 8 | 4.0.0 9 | 10 | Splunk logging extension - Integration tests 11 | quarkus-logging-splunk-integration-tests 12 | 13 | 14 | 15 | jakarta.enterprise 16 | jakarta.enterprise.cdi-api 17 | 18 | 19 | jakarta.ws.rs 20 | jakarta.ws.rs-api 21 | 22 | 23 | io.quarkus 24 | quarkus-resteasy 25 | 26 | 27 | io.quarkiverse.logging.splunk 28 | quarkus-logging-splunk 29 | 30 | 31 | io.quarkus 32 | quarkus-junit5 33 | test 34 | 35 | 36 | org.testcontainers 37 | junit-jupiter 38 | test 39 | 40 | 41 | io.rest-assured 42 | rest-assured 43 | test 44 | 45 | 46 | org.hamcrest 47 | hamcrest 48 | test 49 | 50 | 51 | io.quarkiverse.logging.splunk 52 | quarkus-logging-splunk-test-utils 53 | test 54 | 55 | 56 | 57 | 58 | 59 | 60 | io.quarkus 61 | quarkus-maven-plugin 62 | 63 | 64 | 65 | build 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | native 75 | 76 | 77 | native 78 | 79 | 80 | 81 | native 82 | 83 | 84 | 85 | 86 | maven-failsafe-plugin 87 | 88 | 89 | 90 | integration-test 91 | verify 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | native-docker 101 | 102 | 103 | native-docker 104 | 105 | 106 | 107 | native 108 | true 109 | docker 110 | true 111 | false 112 | 113 | 114 | 115 | io.quarkus 116 | quarkus-container-image-jib 117 | 118 | 119 | 120 | 121 | 122 | maven-failsafe-plugin 123 | 124 | 125 | 126 | 127 | integration-test 128 | verify 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | -------------------------------------------------------------------------------- /integration-tests/src/main/java/io/quarkiverse/logging/splunk/MyMiddleware.java: -------------------------------------------------------------------------------- 1 | package io.quarkiverse.logging.splunk; 2 | 3 | import java.util.List; 4 | 5 | import com.splunk.logging.HttpEventCollectorEventInfo; 6 | import com.splunk.logging.HttpEventCollectorMiddleware; 7 | 8 | public class MyMiddleware extends HttpEventCollectorMiddleware.HttpSenderMiddleware { 9 | @Override 10 | public void postEvents( 11 | List events, 12 | HttpEventCollectorMiddleware.IHttpSender sender, 13 | HttpEventCollectorMiddleware.IHttpSenderCallback callback) { 14 | for (HttpEventCollectorEventInfo event : events) { 15 | event.getProperties().put("myProperty", "myValue"); 16 | } 17 | sender.postEvents(events, callback); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /integration-tests/src/main/java/io/quarkiverse/logging/splunk/SensitiveLogFilter.java: -------------------------------------------------------------------------------- 1 | package io.quarkiverse.logging.splunk; 2 | 3 | import java.util.logging.Filter; 4 | import java.util.logging.LogRecord; 5 | 6 | import io.quarkus.logging.LoggingFilter; 7 | 8 | @LoggingFilter(name = "sensitive-filter") 9 | public class SensitiveLogFilter implements Filter { 10 | 11 | @Override 12 | public boolean isLoggable(LogRecord record) { 13 | if (record.getMessage().contains("Sensitive")) { 14 | record.setMessage(record.getMessage().replace("Sensitive", "*********")); 15 | } 16 | return true; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /integration-tests/src/main/java/io/quarkiverse/logging/splunk/SplunkHandlerResource.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2021 Amadeus s.a.s. 3 | Contributor(s): Kevin Viet, Romain Quinio (Amadeus s.a.s.) 4 | */ 5 | package io.quarkiverse.logging.splunk; 6 | 7 | import jakarta.enterprise.context.ApplicationScoped; 8 | import jakarta.ws.rs.GET; 9 | import jakarta.ws.rs.Path; 10 | 11 | import org.jboss.logging.Logger; 12 | import org.jboss.logging.MDC; 13 | 14 | @Path("/log-to-splunk") 15 | @ApplicationScoped 16 | public class SplunkHandlerResource { 17 | 18 | private static final Logger logger = Logger.getLogger(SplunkHandlerResource.class); 19 | 20 | @GET 21 | public void log() { 22 | MDC.put("mdc-key", "mdc-value"); 23 | logger.info("hello splunk"); 24 | logger.info("Sensitive log"); 25 | MDC.remove("mdc-key"); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /integration-tests/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | quarkus.log.console.enable=true 2 | quarkus.log.console.filter=sensitive-filter 3 | quarkus.log.handler.splunk.metadata-index=main 4 | quarkus.log.handler.splunk.batch-interval=1s 5 | quarkus.log.handler.splunk.middleware=io.quarkiverse.logging.splunk.MyMiddleware 6 | quarkus.log.handler.splunk.serialization=nested 7 | quarkus.log.handler.splunk.filter=sensitive-filter 8 | quarkus.log.handler.splunk.devservices.enabled=true -------------------------------------------------------------------------------- /integration-tests/src/test/java/io/quarkiverse/logging/splunk/SplunkLoggingIT.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2021 Amadeus s.a.s. 3 | Contributor(s): Kevin Viet, Romain Quinio (Amadeus s.a.s.) 4 | */ 5 | package io.quarkiverse.logging.splunk; 6 | 7 | import io.quarkus.test.junit.QuarkusIntegrationTest; 8 | 9 | @QuarkusIntegrationTest 10 | public class SplunkLoggingIT extends SplunkLoggingTest { 11 | } 12 | -------------------------------------------------------------------------------- /integration-tests/src/test/java/io/quarkiverse/logging/splunk/SplunkLoggingTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2021 Amadeus s.a.s. 3 | Contributor(s): Kevin Viet, Romain Quinio (Amadeus s.a.s.) 4 | */ 5 | package io.quarkiverse.logging.splunk; 6 | 7 | import static jakarta.ws.rs.core.Response.Status.*; 8 | import static org.hamcrest.CoreMatchers.*; 9 | import static org.junit.jupiter.api.Assertions.*; 10 | 11 | import org.junit.jupiter.api.Test; 12 | 13 | import io.quarkiverse.logging.splunk.test.LoggingSplunkApiUrl; 14 | import io.quarkiverse.logging.splunk.test.LoggingSplunkInjectingTestResource; 15 | import io.quarkus.test.common.QuarkusTestResource; 16 | import io.quarkus.test.junit.QuarkusTest; 17 | import io.restassured.RestAssured; 18 | import io.restassured.response.Response; 19 | 20 | @QuarkusTest 21 | @QuarkusTestResource(LoggingSplunkInjectingTestResource.class) 22 | public class SplunkLoggingTest { 23 | @LoggingSplunkApiUrl 24 | String splunkApiUrl; 25 | 26 | @Test 27 | public void test() throws InterruptedException { 28 | RestAssured.given().when().get("/log-to-splunk").then().statusCode(NO_CONTENT.getStatusCode()); 29 | Thread.sleep(2000); 30 | 31 | searchSplunk("hello splunk").then().statusCode(200).body(containsString("hello splunk"), containsString("mdc-value"), 32 | containsString("myValue")); 33 | searchSplunk("********* log").then().statusCode(200); 34 | } 35 | 36 | private Response searchSplunk(String log) { 37 | // XML REST API - see https://docs.splunk.com/Documentation/Splunk/latest/RESTREF/RESTsearch#search.2Fjobs 38 | // Note: we can't assert on fields, which require 2 calls: GET /services/search/jobs and GET /services/search/jobs/ 39 | return RestAssured.given() 40 | .request() 41 | .formParam("search", "search \"" + log + "\"") 42 | .formParam("exec_mode", "oneshot") 43 | //.formParam("output_mode", "json") 44 | .relaxedHTTPSValidation() 45 | .auth().basic("admin", "admin123") 46 | .log().all() 47 | .post(splunkApiUrl + "/services/search/jobs"); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | io.quarkiverse 6 | quarkiverse-parent 7 | 18 8 | 9 | io.quarkiverse.logging.splunk 10 | quarkus-logging-splunk-parent 11 | 4.0.0-SNAPSHOT 12 | Splunk logging extension - Parent 13 | Send logs to a Splunk HTTP Event Collector (HEC) 14 | pom 15 | https://github.com/quarkiverse/quarkus-logging-splunk 16 | 17 | 18 | The Apache License, Version 2.0 19 | http://www.apache.org/licenses/LICENSE-2.0.txt 20 | 21 | 22 | 23 | 24 | rquinio 25 | Romain Quinio 26 | 27 | 28 | vietk 29 | Kevin Viet 30 | 31 | 32 | 33 | UTF-8 34 | UTF-8 35 | 17 36 | 17 37 | 17 38 | true 39 | 3.15.1 40 | 1.11.8 41 | 42 | false 43 | ${skipTests} 44 | ${skipTests} 45 | 46 | 47 | scm:git:git@github.com:quarkiverse/quarkus-logging-splunk.git 48 | scm:git:git@github.com:quarkiverse/quarkus-logging-splunk.git 49 | https://github.com/quarkiverse/quarkus-logging-splunk 50 | HEAD 51 | 52 | 53 | test-utils 54 | runtime 55 | deployment 56 | 57 | 58 | 59 | central 60 | Central Repository 61 | https://repo.maven.apache.org/maven2 62 | 63 | false 64 | 65 | 66 | 67 | splunk 68 | Splunk Releases 69 | https://splunk.jfrog.io/splunk/ext-releases-local 70 | 71 | false 72 | 73 | 74 | 75 | 76 | 77 | 78 | io.quarkus 79 | quarkus-bom 80 | ${quarkus.version} 81 | pom 82 | import 83 | 84 | 85 | com.splunk.logging 86 | splunk-library-javalogging 87 | ${splunk.logging.version} 88 | 89 | 90 | io.quarkiverse.logging.splunk 91 | quarkus-logging-splunk-test-utils 92 | ${project.version} 93 | 94 | 95 | io.quarkiverse.logging.splunk 96 | quarkus-logging-splunk 97 | ${project.version} 98 | 99 | 100 | io.quarkiverse.logging.splunk 101 | quarkus-logging-splunk-deployment 102 | ${project.version} 103 | 104 | 105 | org.hamcrest 106 | hamcrest 107 | 3.0 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | org.apache.maven.plugins 116 | maven-surefire-plugin 117 | 118 | ${skipUTs} 119 | 120 | org.jboss.logmanager.LogManager 121 | 122 | 123 | 124 | 125 | io.quarkus 126 | quarkus-maven-plugin 127 | ${quarkus.version} 128 | 129 | 130 | io.quarkus 131 | quarkus-extension-maven-plugin 132 | ${quarkus.version} 133 | 134 | 135 | maven-failsafe-plugin 136 | 137 | 138 | 139 | integration-test 140 | verify 141 | 142 | 143 | ${skipTests} 144 | ${skipITs} 145 | 146 | org.jboss.logmanager.LogManager 147 | ${project.build.directory}/${project.build.finalName}-runner 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | not-to-release 159 | 160 | 161 | !performRelease 162 | 163 | 164 | 165 | docs 166 | integration-tests 167 | 168 | 169 | 170 | 171 | -------------------------------------------------------------------------------- /runtime/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | io.quarkiverse.logging.splunk 6 | quarkus-logging-splunk-parent 7 | 4.0.0-SNAPSHOT 8 | ../pom.xml 9 | 10 | 11 | quarkus-logging-splunk 12 | Splunk logging extension - Runtime 13 | 14 | 15 | 16 | com.splunk.logging 17 | splunk-library-javalogging 18 | 19 | 20 | io.quarkus 21 | quarkus-core 22 | 23 | 24 | io.quarkus 25 | quarkus-arc 26 | 27 | 28 | org.junit.jupiter 29 | junit-jupiter-api 30 | test 31 | 32 | 33 | org.hamcrest 34 | hamcrest 35 | test 36 | 37 | 38 | org.mockito 39 | mockito-core 40 | test 41 | 42 | 43 | org.mockito 44 | mockito-junit-jupiter 45 | test 46 | 47 | 48 | org.slf4j 49 | slf4j-api 50 | test 51 | 52 | 53 | 54 | 55 | 56 | 57 | io.quarkus 58 | quarkus-extension-maven-plugin 59 | 60 | 61 | 62 | extension-descriptor 63 | 64 | compile 65 | 66 | ${project.groupId}:${project.artifactId}-deployment:${project.version} 67 | 68 | 69 | 70 | 71 | 72 | 73 | org.apache.maven.plugins 74 | maven-compiler-plugin 75 | 76 | 77 | 78 | io.quarkus 79 | quarkus-extension-processor 80 | ${quarkus.version} 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /runtime/src/main/java/io/quarkiverse/logging/splunk/AsyncConfig.java: -------------------------------------------------------------------------------- 1 | package io.quarkiverse.logging.splunk; 2 | 3 | import org.jboss.logmanager.handlers.AsyncHandler; 4 | 5 | import io.smallrye.config.WithDefault; 6 | import io.smallrye.config.WithParentName; 7 | 8 | /** 9 | * Copy of io.quarkus.runtime.logging, as the fields are package-private. 10 | */ 11 | public interface AsyncConfig { 12 | 13 | /** 14 | * Indicates whether to log asynchronously 15 | */ 16 | @WithDefault("false") 17 | @WithParentName 18 | boolean enable(); 19 | 20 | /** 21 | * The queue length to use before flushing writing 22 | */ 23 | @WithDefault("512") 24 | int queueLength(); 25 | 26 | /** 27 | * Determine whether to block the publisher (rather than drop the message) when the queue is full 28 | */ 29 | @WithDefault("block") 30 | AsyncHandler.OverflowAction overflow(); 31 | } 32 | -------------------------------------------------------------------------------- /runtime/src/main/java/io/quarkiverse/logging/splunk/DevServicesLoggingSplunkRuntimeConfig.java: -------------------------------------------------------------------------------- 1 | package io.quarkiverse.logging.splunk; 2 | 3 | import java.util.Optional; 4 | 5 | public interface DevServicesLoggingSplunkRuntimeConfig { 6 | /** 7 | * The API URL the splunk dev service listens on. 8 | */ 9 | Optional apiUrl(); 10 | } 11 | -------------------------------------------------------------------------------- /runtime/src/main/java/io/quarkiverse/logging/splunk/SplunkConfig.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2023 Amadeus s.a.s. 3 | Contributor(s): Kevin Viet, Romain Quinio, Yohann Puyhaubert (Amadeus s.a.s.) 4 | */ 5 | package io.quarkiverse.logging.splunk; 6 | 7 | import java.util.Map; 8 | 9 | import io.quarkus.runtime.annotations.ConfigPhase; 10 | import io.quarkus.runtime.annotations.ConfigRoot; 11 | import io.smallrye.config.ConfigMapping; 12 | import io.smallrye.config.WithParentName; 13 | 14 | /** 15 | * Configuration for Splunk HEC logging 16 | */ 17 | @ConfigRoot(phase = ConfigPhase.RUN_TIME) 18 | @ConfigMapping(prefix = "quarkus.log.handler.splunk") 19 | public interface SplunkConfig { 20 | 21 | /** 22 | * Configuration for Splunk HEC logging for the root level. 23 | */ 24 | @WithParentName 25 | SplunkHandlerConfig config(); 26 | 27 | /** 28 | * Map of all the custom/named handlers configuration using Splunk implementation. 29 | */ 30 | @WithParentName 31 | Map namedHandlers(); 32 | 33 | /** 34 | * Runtime configuration for the Splunk DevService. 35 | */ 36 | DevServicesLoggingSplunkRuntimeConfig devservices(); 37 | } 38 | -------------------------------------------------------------------------------- /runtime/src/main/java/io/quarkiverse/logging/splunk/SplunkErrorCallback.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2021 Amadeus s.a.s. 3 | Contributor(s): Kevin Viet, Romain Quinio (Amadeus s.a.s.) 4 | */ 5 | package io.quarkiverse.logging.splunk; 6 | 7 | import java.io.PrintStream; 8 | import java.io.PrintWriter; 9 | import java.io.StringWriter; 10 | import java.util.Arrays; 11 | import java.util.List; 12 | import java.util.logging.Handler; 13 | import java.util.logging.Level; 14 | 15 | import org.jboss.logmanager.ExtHandler; 16 | import org.jboss.logmanager.handlers.ConsoleHandler; 17 | 18 | import com.splunk.logging.HttpEventCollectorErrorHandler.ErrorCallback; 19 | import com.splunk.logging.HttpEventCollectorEventInfo; 20 | 21 | import io.quarkus.bootstrap.logging.InitialConfigurator; 22 | 23 | public class SplunkErrorCallback implements ErrorCallback { 24 | 25 | Boolean consoleEnabled; 26 | 27 | PrintStream stdout; 28 | 29 | PrintStream stderr; 30 | 31 | public SplunkErrorCallback() { 32 | this(System.out, System.err); // NOSONAR 33 | } 34 | 35 | /** 36 | * For unit tests 37 | */ 38 | public SplunkErrorCallback(PrintStream stdout, PrintStream stderr) { 39 | this.stdout = stdout; 40 | this.stderr = stderr; 41 | } 42 | 43 | /** 44 | * Logs the original event to stdout (if console handler is disabled). 45 | * Logs the error to stderr. 46 | */ 47 | @Override 48 | public void error(List list, Exception e) { 49 | final StringWriter stringWriter = new StringWriter(); 50 | stringWriter.append("Error while sending events to Splunk HEC: "); 51 | stringWriter.append(e.getMessage()).append(System.lineSeparator()); 52 | e.printStackTrace(new PrintWriter(stringWriter)); 53 | this.stderr.println(stringWriter.toString()); 54 | 55 | if (!isConsoleHandlerEnabled()) { 56 | for (HttpEventCollectorEventInfo logEvent : list) { 57 | this.stdout.println(logEvent.getMessage()); 58 | } 59 | } 60 | } 61 | 62 | /** 63 | * This has to be determined lazily, as handlers are not yet registered when splunk is initialized. 64 | * An alternative was to check for config "quarkus.log.console.enable", but adds a microprofile-config dependency. 65 | */ 66 | private boolean isConsoleHandlerEnabled() { 67 | if (consoleEnabled == null) { 68 | ExtHandler delayedHandler = InitialConfigurator.DELAYED_HANDLER; 69 | Handler consoleHandler = Arrays.stream(delayedHandler.getHandlers()) 70 | .filter(h -> (h instanceof ConsoleHandler)) 71 | .findFirst().orElse(null); 72 | consoleEnabled = (consoleHandler != null && !consoleHandler.getLevel().equals(Level.OFF)); 73 | } 74 | return consoleEnabled; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /runtime/src/main/java/io/quarkiverse/logging/splunk/SplunkFlatEventSerializer.java: -------------------------------------------------------------------------------- 1 | package io.quarkiverse.logging.splunk; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | 6 | import com.splunk.logging.EventBodySerializer; 7 | import com.splunk.logging.EventHeaderSerializer; 8 | import com.splunk.logging.HttpEventCollectorEventInfo; 9 | 10 | class SplunkFlatEventSerializer implements EventHeaderSerializer, EventBodySerializer { 11 | 12 | private final String metadataSeverityFieldName; 13 | 14 | SplunkFlatEventSerializer(String metadataSeverityFieldName) { 15 | this.metadataSeverityFieldName = metadataSeverityFieldName; 16 | } 17 | 18 | ; 19 | 20 | /** 21 | * Serialization of the root JSON object of the event 22 | */ 23 | @Override 24 | public Map serializeEventHeader(HttpEventCollectorEventInfo eventInfo, Map metadata) { 25 | Map fields = (Map) metadata.computeIfAbsent("fields", k -> new HashMap<>()); 26 | fields.put(this.metadataSeverityFieldName, eventInfo.getSeverity()); 27 | if (eventInfo.getLoggerName() != null) { 28 | fields.put("logger", eventInfo.getLoggerName()); 29 | } 30 | if (eventInfo.getThreadName() != null) { 31 | fields.put("thread", eventInfo.getThreadName()); 32 | } 33 | if (eventInfo.getExceptionMessage() != null) { 34 | fields.put("exception", eventInfo.getExceptionMessage()); 35 | } 36 | fields.putAll(eventInfo.getProperties()); 37 | return metadata; 38 | } 39 | 40 | /** 41 | * Serialization of the "event" field 42 | */ 43 | @Override 44 | public String serializeEventBody(HttpEventCollectorEventInfo eventInfo, Object formattedMessage) { 45 | return eventInfo.getMessage(); 46 | } 47 | 48 | /** 49 | * We have to override this, because, by default, splunk-library-java does not send timestamp to Splunk. 50 | * Refer to this pull-request on github 51 | */ 52 | @Override 53 | public double getEventTime(HttpEventCollectorEventInfo eventInfo) { 54 | return eventInfo.getTime(); 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /runtime/src/main/java/io/quarkiverse/logging/splunk/SplunkHandlerConfig.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2023 Amadeus s.a.s. 3 | Contributor(s): Kevin Viet, Romain Quinio, Yohann Puyhaubert (Amadeus s.a.s.) 4 | */ 5 | package io.quarkiverse.logging.splunk; 6 | 7 | import java.time.Duration; 8 | import java.util.Map; 9 | import java.util.Optional; 10 | import java.util.logging.Level; 11 | 12 | import com.splunk.logging.HttpEventCollectorSender; 13 | 14 | import io.smallrye.config.WithDefault; 15 | 16 | /** 17 | * The configuration of the Splunk root or any Splunk named handler. 18 | */ 19 | public interface SplunkHandlerConfig { 20 | /** 21 | * Determine whether to enable the handler 22 | */ 23 | @WithDefault("true") 24 | boolean enabled(); 25 | 26 | /** 27 | * The splunk handler log level. By default, it is no more strict than the root handler level. 28 | */ 29 | @WithDefault("ALL") 30 | Level level(); 31 | 32 | /** 33 | * Splunk HEC endpoint base url. 34 | *

35 | * With raw events, the endpoint targeted is /services/collector/raw. 36 | * With flat or nested JSON events, the endpoint targeted is /services/collector/event/1.0. 37 | */ 38 | @WithDefault("https://localhost:8088/") 39 | String url(); 40 | 41 | /** 42 | * Disable TLS certificate validation with HEC endpoint 43 | */ 44 | @WithDefault("false") 45 | boolean disableCertificateValidation(); 46 | 47 | /** 48 | * The application token to authenticate with HEC, the token is mandatory if the extension is enabled 49 | * https://docs.splunk.com/Documentation/Splunk/latest/Data/FormateventsforHTTPEventCollector#HEC_token 50 | */ 51 | Optional token(); 52 | 53 | /** 54 | * The strategy to send events to HEC. 55 | *

56 | * In sequential mode, there is only one HTTP connection to HEC and the order of events is preserved, but performance is 57 | * lower. 58 | * In parallel mode, event batches are sent asynchronously over multiple HTTP connections, and events with the same 59 | * timestamp 60 | * (that has 1 millisecond resolution) may be indexed out of order by Splunk. 61 | */ 62 | @WithDefault("sequential") 63 | SendMode sendMode(); 64 | 65 | /** 66 | * A GUID to identify an HEC client and guarantee isolation at HEC level in case of slow clients. 67 | * 68 | * @see splunk 70 | * guide 71 | */ 72 | Optional channel(); 73 | 74 | /** 75 | * Batching delay before sending a group of events. 76 | *

77 | * If 0, the events are sent immediately. 78 | *

79 | */ 80 | @WithDefault("10s") 81 | Duration batchInterval(); 82 | 83 | /** 84 | * Maximum number of events in a batch. By default 10, if 0 no batching. 85 | */ 86 | @WithDefault("10") 87 | long batchSizeCount(); 88 | 89 | /** 90 | * Maximum total size in bytes of events in a batch. By default 10KB, if 0 no batching. 91 | */ 92 | @WithDefault("10240") 93 | long batchSizeBytes(); 94 | 95 | /** 96 | * Maximum number of retries in case of I/O exceptions with HEC connection. 97 | */ 98 | @WithDefault("0") 99 | long maxRetries(); 100 | 101 | /** 102 | * A middleware to customize the behavior of sending events to Splunk. 103 | * 104 | * @see com.splunk.logging.HttpEventCollectorMiddleware 105 | */ 106 | Optional middleware(); 107 | 108 | /** 109 | * The log format, defining which metadata are inlined inside the log main payload. 110 | *

111 | * Specific metadata (hostname, category, thread name, ...), as well as MDC key/value map, can also be sent in a structured 112 | * way. 113 | */ 114 | @WithDefault("%d{yyyy-MM-dd HH:mm:ss,SSS} %-5p [%c{3.}] (%t) %s%e%n") 115 | String format(); 116 | 117 | /** 118 | * Whether to send the thrown exception message as a structured metadata of the log event (as opposed to %e in a formatted 119 | * message, it does not include the exception name or stacktrace). 120 | * Only applicable to 'nested' serialization. 121 | */ 122 | @WithDefault("false") 123 | boolean includeException(); 124 | 125 | /** 126 | * Whether to send the logger name as a structured metadata of the log event (equivalent of %c in a formatted message). 127 | * Only applicable to 'nested' serialization. 128 | */ 129 | @WithDefault("false") 130 | boolean includeLoggerName(); 131 | 132 | /** 133 | * Whether to send the thread name as a structured metadata of the log event (equivalent of %t in a formatted message). 134 | * Only applicable to 'nested' serialization. 135 | */ 136 | @WithDefault("false") 137 | boolean includeThreadName(); 138 | 139 | /** 140 | * Overrides the host name metadata value. 141 | *

142 | * Default value: the equivalent of %h in a formatted message. 143 | *

144 | */ 145 | Optional metadataHost(); 146 | 147 | /** 148 | * The source value to assign to the event data. For example, if you're sending data from an app you're developing, 149 | * you could set this key to the name of the app. 150 | * 151 | * @see splunk 153 | * guide 154 | */ 155 | Optional metadataSource(); 156 | 157 | /** 158 | * The optional format of the events, to enable some parsing on Splunk side. 159 | * 160 | *

161 | * A given source type may have indexed fields extraction enabled, which is the case of the built-in _json used for nested 162 | * serialization. 163 | *

164 | *

165 | * Default value: _json for nested serialization, not set otherwise 166 | *

167 | * 168 | * @see splunk 170 | * guide 171 | */ 172 | Optional metadataSourceType(); 173 | 174 | /** 175 | * The optional name of the index by which the event data is to be stored. If set, it must be within the 176 | * list of allowed indexes of the token (if it has the indexes parameter set). 177 | * 178 | * @see splunk 180 | * guide 181 | */ 182 | Optional metadataIndex(); 183 | 184 | /** 185 | * Optional static key/value pairs to populate the "fields" key of event metadata. This isn't 186 | * applicable to raw serialization. 187 | * 188 | * @see splunk 190 | * guide 191 | */ 192 | Map metadataFields(); 193 | 194 | /** 195 | * The name of the key used to convey the severity / log level in the metadata fields. 196 | * Only applicable to 'flat' serialization. 197 | * With 'nested' serialization, there is already a 'severity' field. 198 | */ 199 | @WithDefault("severity") 200 | String metadataSeverityFieldName(); 201 | 202 | /** 203 | * Determines whether the events are sent in raw mode. In case the raw event (i.e. the actual log message) 204 | * is not a JSON object you need to explicitly set a source type or Splunk will reject the event (the 205 | * default source type, _json, assumes that the incoming event can be parsed as JSON) 206 | * 207 | * @deprecated Use {@link #serialization} 208 | */ 209 | @Deprecated(forRemoval = true) 210 | @WithDefault("false") 211 | boolean raw(); 212 | 213 | /** 214 | * The format of the payload. 215 | *
    216 | *
  • With raw serialization, the log message is sent 'as is' in the HTTP body. Metadata can only be common to a whole 217 | * batch and are sent via HTTP parameters. 218 | *
  • With nested serialization, the log message is sent into a 'message' field of a JSON structure which also contains 219 | * dynamic metadata. 220 | *
  • With flat serialization, the log message is sent into the root 'event' field. Dynamic metadata is sent via the 221 | * 'fields' root object. 222 | *
223 | */ 224 | @WithDefault("nested") 225 | SerializationFormat serialization(); 226 | 227 | /** 228 | * The name of the named filter to link to the splunk handler. 229 | */ 230 | Optional filter(); 231 | 232 | /** 233 | * AsyncHandler config 234 | *

235 | * This is independent of the SendMode, i.e. whether the HTTP client is async or not. 236 | */ 237 | AsyncConfig async(); 238 | 239 | /** 240 | * Mirrors com.splunk.logging.HttpEventCollectorSender.SendMode 241 | */ 242 | enum SendMode { 243 | SEQUENTIAL, 244 | PARALLEL 245 | } 246 | 247 | enum SerializationFormat { 248 | RAW, 249 | NESTED, 250 | FLAT 251 | } 252 | 253 | /** 254 | * Sets the default connect timeout for new connections in milliseconds. 255 | */ 256 | @WithDefault("3000") 257 | long connectTimeout(); 258 | 259 | /** 260 | * Sets the default timeout for complete calls in milliseconds. 261 | */ 262 | @WithDefault("0") 263 | long callTimeout(); 264 | 265 | /** 266 | * Sets the default read timeout for new connections in milliseconds. 267 | */ 268 | @WithDefault("10000") 269 | long readTimeout(); 270 | 271 | /** 272 | * Sets the default write timeout for new connections in milliseconds. 273 | */ 274 | @WithDefault("10000") 275 | long writeTimeout(); 276 | 277 | /** 278 | * Sets the default termination timeout during a flush in milliseconds. 279 | */ 280 | @WithDefault("0") 281 | long terminationTimeout(); 282 | } 283 | -------------------------------------------------------------------------------- /runtime/src/main/java/io/quarkiverse/logging/splunk/SplunkLogHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2021 Amadeus s.a.s. 3 | Contributor(s): Kevin Viet, Romain Quinio (Amadeus s.a.s.) 4 | */ 5 | package io.quarkiverse.logging.splunk; 6 | 7 | import java.util.List; 8 | import java.util.Locale; 9 | import java.util.logging.ErrorManager; 10 | import java.util.logging.Filter; 11 | import java.util.logging.Formatter; 12 | 13 | import org.jboss.logmanager.ExtHandler; 14 | import org.jboss.logmanager.ExtLogRecord; 15 | import org.jboss.logmanager.filters.AllFilter; 16 | 17 | import com.splunk.logging.HttpEventCollectorResendMiddleware; 18 | import com.splunk.logging.HttpEventCollectorSender; 19 | 20 | public class SplunkLogHandler extends ExtHandler { 21 | 22 | private final HttpEventCollectorSender sender; 23 | 24 | private final boolean includeException; 25 | 26 | private final boolean includeLoggerName; 27 | 28 | private final boolean includeThreadName; 29 | 30 | public SplunkLogHandler(HttpEventCollectorSender sender, boolean includeException, boolean includeLoggerName, 31 | boolean includeThreadName, boolean disableCertificateValidation, long retriesOnError) { 32 | this.sender = sender; 33 | this.includeException = includeException; 34 | this.includeLoggerName = includeLoggerName; 35 | this.includeThreadName = includeThreadName; 36 | 37 | if (disableCertificateValidation) { 38 | this.sender.disableCertificateValidation(); 39 | } 40 | if (retriesOnError > 0) { 41 | this.sender.addMiddleware(new HttpEventCollectorResendMiddleware(retriesOnError)); 42 | } 43 | } 44 | 45 | @Override 46 | public void doPublish(ExtLogRecord record) { 47 | String formatted = formatMessage(record); 48 | if (formatted.length() == 0) { 49 | // nothing to write; don't bother 50 | return; 51 | } 52 | this.sender.send( 53 | record.getMillis(), 54 | record.getLevel().toString(), 55 | formatted, 56 | includeLoggerName ? record.getLoggerName() : null, 57 | includeThreadName ? String.format(Locale.US, "%d", record.getThreadID()) : null, 58 | record.getMdcCopy(), 59 | (!includeException || record.getThrown() == null) ? null : record.getThrown().getMessage(), 60 | null); 61 | } 62 | 63 | /** 64 | * {@inheritDoc} 65 | *

66 | * Warning: explicit calls to flush bypass event batching checks, so events are sent too early. Do not rely on APIs 67 | * calling flush directly, like the AsyncHandler's autoflush mechanism. 68 | */ 69 | @Override 70 | public void flush() { 71 | this.sender.flush(); 72 | } 73 | 74 | @Override 75 | public void close() throws SecurityException { 76 | this.sender.flush(true); 77 | this.sender.cancel(); 78 | } 79 | 80 | private String formatMessage(ExtLogRecord record) { 81 | String formatted = ""; 82 | final Formatter formatter = getFormatter(); 83 | try { 84 | formatted = formatter.format(record); 85 | } catch (Exception ex) { 86 | reportError("Formatting error", ex, ErrorManager.FORMAT_FAILURE); 87 | } 88 | return formatted; 89 | } 90 | 91 | @Override 92 | public void setFilter(Filter newFilter) throws SecurityException { 93 | if (this.getFilter() != null) { 94 | // setFilter gets called by io.quarkus.runtime.logging.LoggingSetupRecorder with cleanupFilter 95 | super.setFilter(new AllFilter(List.of(this.getFilter(), newFilter))); 96 | } else { 97 | super.setFilter(newFilter); 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /runtime/src/main/java/io/quarkiverse/logging/splunk/SplunkLogHandlerRecorder.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2023 Amadeus s.a.s. 3 | Contributor(s): Kevin Viet, Romain Quinio, Yohann Puyhaubert (Amadeus s.a.s.) 4 | */ 5 | package io.quarkiverse.logging.splunk; 6 | 7 | import java.net.InetAddress; 8 | import java.net.UnknownHostException; 9 | import java.util.Collections; 10 | import java.util.HashMap; 11 | import java.util.Map; 12 | import java.util.Optional; 13 | import java.util.function.BiConsumer; 14 | import java.util.logging.Filter; 15 | import java.util.logging.Handler; 16 | import java.util.logging.Level; 17 | import java.util.stream.Collectors; 18 | 19 | import org.jboss.logmanager.formatters.PatternFormatter; 20 | import org.jboss.logmanager.handlers.AsyncHandler; 21 | 22 | import com.splunk.logging.HttpEventCollectorErrorHandler; 23 | import com.splunk.logging.HttpEventCollectorMiddleware.HttpSenderMiddleware; 24 | import com.splunk.logging.HttpEventCollectorSender; 25 | import com.splunk.logging.hec.MetadataTags; 26 | 27 | import io.quarkus.runtime.RuntimeValue; 28 | import io.quarkus.runtime.annotations.Recorder; 29 | import io.quarkus.runtime.logging.DiscoveredLogComponents; 30 | import io.quarkus.runtime.logging.LogFilterFactory; 31 | 32 | @Recorder 33 | public class SplunkLogHandlerRecorder { 34 | 35 | public RuntimeValue> initializeHandler(SplunkConfig rootConfig, 36 | DiscoveredLogComponents discoveredLogComponents) { 37 | if (!rootConfig.config().enabled()) { 38 | return new RuntimeValue<>(Optional.empty()); 39 | } 40 | 41 | Handler handler = buildHandlerFromConfig(rootConfig.config(), discoveredLogComponents); 42 | return new RuntimeValue<>(Optional.of(handler)); 43 | } 44 | 45 | public RuntimeValue> initializeHandlers(SplunkConfig rootConfig, 46 | DiscoveredLogComponents discoveredLogComponents) { 47 | if (rootConfig.namedHandlers() == null || rootConfig.namedHandlers().isEmpty()) { 48 | return new RuntimeValue<>(Collections.EMPTY_MAP); 49 | } 50 | 51 | Map namedHandlers = rootConfig.namedHandlers() 52 | .entrySet() 53 | .stream() 54 | .filter(e -> !"devservices".equals(e.getKey())) 55 | .filter(e -> e.getValue().enabled()) 56 | .collect(Collectors.toMap( 57 | e -> e.getKey(), 58 | e -> buildHandlerFromConfig(e.getValue(), discoveredLogComponents))); 59 | 60 | return new RuntimeValue<>(namedHandlers); 61 | } 62 | 63 | private Handler buildHandlerFromConfig(SplunkHandlerConfig config, DiscoveredLogComponents discoveredLogComponents) { 64 | if (!config.token().isPresent()) { 65 | throw new IllegalArgumentException("The property quarkus.log.handler.splunk.token is mandatory"); 66 | } 67 | HttpEventCollectorSender sender = createSender(config); 68 | SplunkLogHandler splunkLogHandler = createSplunkLogHandler(sender, config); 69 | splunkLogHandler.setLevel(config.level()); 70 | splunkLogHandler.setFormatter( 71 | new PatternFormatter(config.format())); 72 | applyFilter(discoveredLogComponents, config.filter(), splunkLogHandler); 73 | 74 | return config.async().enable() 75 | ? createAsyncHandler(config.async(), config.level(), splunkLogHandler) 76 | : splunkLogHandler; 77 | } 78 | 79 | static HttpEventCollectorSender createSender(SplunkHandlerConfig config) { 80 | HttpEventCollectorErrorHandler.onError(new SplunkErrorCallback()); 81 | String type = ""; 82 | if (config.raw() || config.serialization() == SplunkHandlerConfig.SerializationFormat.RAW) { 83 | type = "Raw"; 84 | } 85 | 86 | HttpEventCollectorSender.TimeoutSettings rto = null; 87 | if ((config.connectTimeout() != HttpEventCollectorSender.TimeoutSettings.DEFAULT_CONNECT_TIMEOUT) 88 | || (config.callTimeout() != HttpEventCollectorSender.TimeoutSettings.DEFAULT_CALL_TIMEOUT) || 89 | (config.readTimeout() != HttpEventCollectorSender.TimeoutSettings.DEFAULT_READ_TIMEOUT) 90 | || (config.writeTimeout() != HttpEventCollectorSender.TimeoutSettings.DEFAULT_WRITE_TIMEOUT) || 91 | (config.terminationTimeout() != HttpEventCollectorSender.TimeoutSettings.DEFAULT_TERMINATION_TIMEOUT)) { 92 | rto = new HttpEventCollectorSender.TimeoutSettings(config.connectTimeout(), config.callTimeout(), 93 | config.readTimeout(), config.writeTimeout(), config.terminationTimeout()); 94 | } 95 | // Timeout settings is not used and passing a null is correct regarding the code 96 | HttpEventCollectorSender sender = new HttpEventCollectorSender( 97 | config.url(), 98 | config.token().get(), 99 | config.channel().orElse(""), 100 | type, 101 | config.batchInterval().toMillis(), 102 | config.batchSizeCount(), 103 | config.batchSizeBytes(), 104 | config.sendMode().name().toLowerCase(), 105 | buildMetadata(config), rto); 106 | if (config.serialization() == SplunkHandlerConfig.SerializationFormat.FLAT) { 107 | SplunkFlatEventSerializer serializer = new SplunkFlatEventSerializer(config.metadataSeverityFieldName()); 108 | sender.setEventHeaderSerializer(serializer); 109 | sender.setEventBodySerializer(serializer); 110 | } 111 | if (config.middleware().isPresent()) { 112 | try { 113 | sender.addMiddleware( 114 | Thread.currentThread().getContextClassLoader().loadClass(config.middleware().get()) 115 | .asSubclass(HttpSenderMiddleware.class) 116 | .getDeclaredConstructor() 117 | .newInstance()); 118 | } catch (Exception e) { 119 | throw new IllegalArgumentException("Could not instantiate middleware " + config.middleware().get(), e); 120 | } 121 | } 122 | return sender; 123 | } 124 | 125 | static Map buildMetadata(SplunkHandlerConfig config) { 126 | HashMap metadata = new HashMap<>(); 127 | // Note: sending an empty index is invalid, the index property has to be omitted 128 | config.metadataIndex().ifPresent(s -> metadata.put(MetadataTags.INDEX, s)); 129 | try { 130 | String hostName = InetAddress.getLocalHost().getHostName(); 131 | metadata.put(MetadataTags.HOST, config.metadataHost().orElse(hostName)); 132 | } catch (UnknownHostException e) { 133 | // Ignore 134 | } 135 | config.metadataSource().ifPresent(s -> metadata.put(MetadataTags.SOURCE, s)); 136 | 137 | if (config.metadataSourceType().isPresent()) { 138 | metadata.put(MetadataTags.SOURCETYPE, config.metadataSourceType().get()); 139 | } else if (config.serialization() == SplunkHandlerConfig.SerializationFormat.NESTED) { 140 | metadata.put(MetadataTags.SOURCETYPE, "_json"); 141 | } 142 | 143 | metadata.putAll(config.metadataFields()); 144 | return metadata; 145 | } 146 | 147 | private static SplunkLogHandler createSplunkLogHandler(HttpEventCollectorSender sender, 148 | SplunkHandlerConfig config) { 149 | return new SplunkLogHandler(sender, 150 | config.includeException(), 151 | config.includeLoggerName(), 152 | config.includeThreadName(), 153 | config.disableCertificateValidation(), 154 | config.maxRetries()); 155 | } 156 | 157 | private static AsyncHandler createAsyncHandler(AsyncConfig asyncConfig, Level level, Handler handler) { 158 | final AsyncHandler asyncHandler = new AsyncHandler(asyncConfig.queueLength()); 159 | asyncHandler.setOverflowAction(asyncConfig.overflow()); 160 | asyncHandler.addHandler(handler); 161 | asyncHandler.setLevel(level); 162 | // autoflush ignores event batch configuration and sends events immediately, disable it 163 | asyncHandler.setAutoFlush(false); 164 | return asyncHandler; 165 | } 166 | 167 | private static void applyFilter(DiscoveredLogComponents discoveredLogComponents, 168 | Optional filterName, Handler handler) { 169 | if (filterName.isPresent()) { 170 | Map namedFilters = createNamedFilters(discoveredLogComponents); 171 | String name = filterName.get(); 172 | Filter namedFilter = namedFilters.get(name); 173 | if (namedFilter == null) { 174 | throw new IllegalStateException("Unable to find named filter '" + name); 175 | } else { 176 | handler.setFilter(namedFilter); 177 | } 178 | } 179 | } 180 | 181 | private static Map createNamedFilters(DiscoveredLogComponents discoveredLogComponents) { 182 | if (discoveredLogComponents.getNameToFilterClass().isEmpty()) { 183 | return Collections.emptyMap(); 184 | } 185 | 186 | Map nameToFilter = new HashMap<>(); 187 | LogFilterFactory logFilterFactory = LogFilterFactory.load(); 188 | discoveredLogComponents.getNameToFilterClass().forEach(new BiConsumer<>() { 189 | @Override 190 | public void accept(String name, String className) { 191 | try { 192 | nameToFilter.put(name, logFilterFactory.create(className)); 193 | } catch (Exception e) { 194 | throw new RuntimeException("Unable to create instance of Logging Filter '" + className + "'"); 195 | } 196 | } 197 | }); 198 | return nameToFilter; 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /runtime/src/main/java/io/quarkiverse/logging/splunk/config/build/DevServicesLoggingSplunkBuildTimeConfig.java: -------------------------------------------------------------------------------- 1 | package io.quarkiverse.logging.splunk.config.build; 2 | 3 | import java.util.Map; 4 | import java.util.Optional; 5 | 6 | import io.smallrye.config.WithDefault; 7 | 8 | /** 9 | * The build time configuration around the Splunk dev services. 10 | */ 11 | public interface DevServicesLoggingSplunkBuildTimeConfig { 12 | /** 13 | * whether to activate dev services or not 14 | */ 15 | @WithDefault("false") 16 | boolean enabled(); 17 | 18 | /** 19 | * Override the docker image used for the Splunk dev service 20 | */ 21 | Optional imageName(); 22 | 23 | /** 24 | * Whether the instance of splunk can be shared between runs in DEV mode. 25 | */ 26 | @WithDefault("true") 27 | boolean shared(); 28 | 29 | /** 30 | * Additional environment variables to inject. 31 | */ 32 | Map containerEnv(); 33 | 34 | /** 35 | * Map that allows to tell to plug the following named handlers to the dev service 36 | *

37 | * It is necessary as we do not have access to runtime configuration when starting the Splunk container. 38 | *

39 | */ 40 | Map plugNamedHandlers(); 41 | } 42 | -------------------------------------------------------------------------------- /runtime/src/main/java/io/quarkiverse/logging/splunk/config/build/SplunkBuildConfig.java: -------------------------------------------------------------------------------- 1 | package io.quarkiverse.logging.splunk.config.build; 2 | 3 | import io.quarkus.runtime.annotations.ConfigPhase; 4 | import io.quarkus.runtime.annotations.ConfigRoot; 5 | import io.smallrye.config.ConfigMapping; 6 | 7 | /** 8 | * The build time configuration for the Splunk logging extension. 9 | */ 10 | @ConfigRoot(phase = ConfigPhase.BUILD_AND_RUN_TIME_FIXED) 11 | @ConfigMapping(prefix = "quarkus.log.handler.splunk") 12 | public interface SplunkBuildConfig { 13 | /** 14 | * Configuration for the dev services. 15 | */ 16 | DevServicesLoggingSplunkBuildTimeConfig devservices(); 17 | } 18 | -------------------------------------------------------------------------------- /runtime/src/main/resources/META-INF/quarkus-extension.yaml: -------------------------------------------------------------------------------- 1 | # https://quarkus.io/version/main/guides/extension-metadata#quarkus-extension-yaml 2 | name: "Logging Splunk" 3 | description: "Send logs to a Splunk HTTP Event Collector (HEC)" 4 | sponsor: "Amadeus IT Group" 5 | metadata: 6 | short-name: "logging-splunk" 7 | keywords: 8 | - "logging" 9 | - "splunk" 10 | - "hec" 11 | categories: 12 | - "logging" 13 | status: "stable" 14 | guide: https://docs.quarkiverse.io/quarkus-logging-splunk/dev/index.html 15 | config: 16 | - "quarkus.log.handler.splunk" 17 | -------------------------------------------------------------------------------- /runtime/src/test/java/io/quarkiverse/logging/splunk/SplunkErrorCallbackTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2021 Amadeus s.a.s. 3 | Contributor(s): Kevin Viet, Romain Quinio (Amadeus s.a.s.) 4 | */ 5 | package io.quarkiverse.logging.splunk; 6 | 7 | import static org.hamcrest.MatcherAssert.assertThat; 8 | import static org.hamcrest.Matchers.containsString; 9 | import static org.mockito.Mockito.verify; 10 | import static org.mockito.Mockito.verifyNoInteractions; 11 | 12 | import java.io.ByteArrayOutputStream; 13 | import java.io.PrintStream; 14 | import java.util.Collections; 15 | import java.util.logging.Handler; 16 | import java.util.logging.Level; 17 | 18 | import org.jboss.logmanager.handlers.ConsoleHandler; 19 | import org.junit.jupiter.api.AfterEach; 20 | import org.junit.jupiter.api.Test; 21 | import org.junit.jupiter.api.extension.ExtendWith; 22 | import org.mockito.Spy; 23 | import org.mockito.junit.jupiter.MockitoExtension; 24 | 25 | import com.splunk.logging.HttpEventCollectorEventInfo; 26 | 27 | import io.quarkus.bootstrap.logging.InitialConfigurator; 28 | 29 | @ExtendWith(MockitoExtension.class) 30 | public class SplunkErrorCallbackTest { 31 | 32 | ByteArrayOutputStream outContent = new ByteArrayOutputStream(); 33 | 34 | ByteArrayOutputStream errContent = new ByteArrayOutputStream(); 35 | 36 | @Spy 37 | PrintStream stdout = new PrintStream(outContent); 38 | 39 | @Spy 40 | PrintStream stderr = new PrintStream(errContent); 41 | 42 | HttpEventCollectorEventInfo logEvent = new HttpEventCollectorEventInfo(System.currentTimeMillis(), "INFO", "Hello", 43 | SplunkErrorCallbackTest.class.getName(), "thread", null, null, null); 44 | 45 | @AfterEach 46 | public void tearDown() { 47 | InitialConfigurator.DELAYED_HANDLER.setHandlers(new Handler[] {}); 48 | } 49 | 50 | @Test 51 | public void splunkErrorShouldBeLoggedToStderr() { 52 | SplunkErrorCallback callback = new SplunkErrorCallback(stdout, stderr); 53 | 54 | callback.error(Collections.singletonList(logEvent), new Exception("Test exception")); 55 | 56 | assertThat(errContent.toString(), containsString("Test exception")); 57 | } 58 | 59 | @Test 60 | public void originalLogShouldNotBeLoggedIfConsoleHandlerEnabled() { 61 | InitialConfigurator.DELAYED_HANDLER.addHandler(new ConsoleHandler()); 62 | SplunkErrorCallback callback = new SplunkErrorCallback(stdout, stderr); 63 | 64 | callback.error(Collections.singletonList(logEvent), new Exception("Test exception")); 65 | 66 | verifyNoInteractions(stdout); 67 | } 68 | 69 | @Test 70 | public void originalLogShouldNotBeLoggedIfConsoleHandlerLevelOff() { 71 | Handler handler = new ConsoleHandler(); 72 | handler.setLevel(Level.OFF); 73 | InitialConfigurator.DELAYED_HANDLER.addHandler(handler); 74 | SplunkErrorCallback callback = new SplunkErrorCallback(stdout, stderr); 75 | 76 | callback.error(Collections.singletonList(logEvent), new Exception("Test exception")); 77 | 78 | verify(stdout).println("Hello"); 79 | } 80 | 81 | @Test 82 | public void originalLogShouldBeLoggedToStdoutIfConsoleHandlerDisabled() { 83 | SplunkErrorCallback callback = new SplunkErrorCallback(stdout, stderr); 84 | 85 | callback.error(Collections.singletonList(logEvent), new Exception("Test exception")); 86 | 87 | verify(stdout).println("Hello"); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /runtime/src/test/java/io/quarkiverse/logging/splunk/SplunkLogHandlerRecorderTest.java: -------------------------------------------------------------------------------- 1 | package io.quarkiverse.logging.splunk; 2 | 3 | import static org.junit.jupiter.api.Assertions.*; 4 | import static org.junit.jupiter.api.Assertions.assertEquals; 5 | import static org.mockito.Mockito.mock; 6 | import static org.mockito.Mockito.when; 7 | 8 | import java.time.Duration; 9 | import java.util.Optional; 10 | import java.util.logging.Handler; 11 | import java.util.logging.Level; 12 | 13 | import org.jboss.logmanager.handlers.AsyncHandler; 14 | import org.junit.jupiter.api.Test; 15 | 16 | import com.splunk.logging.HttpEventCollectorSender; 17 | 18 | import io.quarkus.runtime.RuntimeValue; 19 | 20 | class SplunkLogHandlerRecorderTest { 21 | 22 | @Test 23 | void shouldSetupSenderWithConfiguredMiddleware() { 24 | // Arrange 25 | SplunkHandlerConfig config = createConfig(); 26 | when(config.middleware()).thenReturn(Optional.of(TestMiddleware.class.getName())); 27 | 28 | // Act 29 | HttpEventCollectorSender sender = SplunkLogHandlerRecorder.createSender(config); 30 | sender.send("A message"); 31 | sender.flush(); 32 | 33 | // Assert 34 | assertNotNull(TestMiddleware.recordedEvents); 35 | assertEquals(1, TestMiddleware.recordedEvents.size()); 36 | assertEquals("A message", TestMiddleware.recordedEvents.get(0).getMessage()); 37 | } 38 | 39 | @Test 40 | void shouldThrowIfMiddlewareIsNotInstantiable() { 41 | // Arrange 42 | SplunkHandlerConfig config = createConfig(); 43 | when(config.middleware()).thenReturn(Optional.of("NonExistentMiddleware")); 44 | 45 | // Act & Assert 46 | assertThrows(IllegalArgumentException.class, () -> SplunkLogHandlerRecorder.createSender(config)); 47 | } 48 | 49 | @Test 50 | void shouldThrowIfMiddlewareIsNotOfCorrectType() { 51 | // Arrange 52 | SplunkHandlerConfig config = createConfig(); 53 | when(config.middleware()).thenReturn(Optional.of(Object.class.getName())); 54 | 55 | // Act & Assert 56 | assertThrows(IllegalArgumentException.class, () -> SplunkLogHandlerRecorder.createSender(config)); 57 | } 58 | 59 | @Test 60 | void shouldCreateAsyncHandlerWithoutAutoflush() { 61 | // Arrange 62 | SplunkConfig rootConfig = createAsyncRootConfig(); 63 | 64 | // Act 65 | RuntimeValue> handler = new SplunkLogHandlerRecorder().initializeHandler(rootConfig, null); 66 | 67 | // Assert 68 | assertTrue(handler.getValue().isPresent()); 69 | AsyncHandler asyncHandler = assertInstanceOf(AsyncHandler.class, handler.getValue().get()); 70 | assertFalse(asyncHandler.isAutoFlush()); 71 | } 72 | 73 | private SplunkHandlerConfig createConfig() { 74 | SplunkHandlerConfig config = mock(SplunkHandlerConfig.class); 75 | when(config.token()).thenReturn(Optional.of("token")); 76 | when(config.channel()).thenReturn(Optional.empty()); 77 | when(config.metadataIndex()).thenReturn(Optional.empty()); 78 | when(config.metadataHost()).thenReturn(Optional.empty()); 79 | when(config.metadataSource()).thenReturn(Optional.empty()); 80 | when(config.metadataSourceType()).thenReturn(Optional.empty()); 81 | 82 | // override with default values 83 | when(config.enabled()).thenReturn(true); 84 | when(config.level()).thenReturn(Level.ALL); 85 | when(config.url()).thenReturn("https://localhost:8088/"); 86 | when(config.disableCertificateValidation()).thenReturn(false); 87 | when(config.sendMode()).thenReturn(SplunkHandlerConfig.SendMode.SEQUENTIAL); 88 | when(config.batchInterval()).thenReturn(Duration.ofSeconds(10)); 89 | when(config.batchSizeCount()).thenReturn(10L); 90 | when(config.batchSizeBytes()).thenReturn(10240L); 91 | when(config.maxRetries()).thenReturn(0L); 92 | when(config.format()).thenReturn("%d{yyyy-MM-dd HH:mm:ss,SSS} %-5p [%c{3.}] (%t) %s%e%n"); 93 | when(config.includeException()).thenReturn(false); 94 | when(config.includeLoggerName()).thenReturn(false); 95 | when(config.includeThreadName()).thenReturn(false); 96 | when(config.metadataSeverityFieldName()).thenReturn("severity"); 97 | when(config.serialization()).thenReturn(SplunkHandlerConfig.SerializationFormat.NESTED); 98 | when(config.connectTimeout()).thenReturn(3000L); 99 | when(config.callTimeout()).thenReturn(0L); 100 | when(config.readTimeout()).thenReturn(10000L); 101 | when(config.writeTimeout()).thenReturn(10000L); 102 | when(config.terminationTimeout()).thenReturn(0L); 103 | 104 | return config; 105 | } 106 | 107 | private SplunkConfig createAsyncRootConfig() { 108 | SplunkHandlerConfig handlerConfig = createConfig(); 109 | when(handlerConfig.middleware()).thenReturn(Optional.of(TestMiddleware.class.getName())); 110 | // Override batchInterval duration 111 | when(handlerConfig.batchInterval()).thenReturn(Duration.ofSeconds(10)); 112 | 113 | SplunkConfig rootConfig = mock(SplunkConfig.class); 114 | when(rootConfig.config()).thenReturn(handlerConfig); 115 | 116 | // Enable async 117 | AsyncConfig asyncConfig = mock(AsyncConfig.class); 118 | when(asyncConfig.enable()).thenReturn(true); 119 | when(asyncConfig.queueLength()).thenReturn(512); 120 | when(asyncConfig.overflow()).thenReturn(AsyncHandler.OverflowAction.BLOCK); 121 | when(handlerConfig.async()).thenReturn(asyncConfig); 122 | 123 | return rootConfig; 124 | } 125 | 126 | } 127 | -------------------------------------------------------------------------------- /runtime/src/test/java/io/quarkiverse/logging/splunk/SplunkLogHandlerTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2021 Amadeus s.a.s. 3 | Contributor(s): Kevin Viet, Romain Quinio (Amadeus s.a.s.) 4 | */ 5 | package io.quarkiverse.logging.splunk; 6 | 7 | import static org.mockito.ArgumentMatchers.anyLong; 8 | import static org.mockito.ArgumentMatchers.anyMap; 9 | import static org.mockito.ArgumentMatchers.isA; 10 | import static org.mockito.ArgumentMatchers.isNull; 11 | import static org.mockito.Mockito.eq; 12 | import static org.mockito.Mockito.verify; 13 | import static org.mockito.Mockito.verifyNoMoreInteractions; 14 | 15 | import java.util.logging.Formatter; 16 | import java.util.logging.Level; 17 | 18 | import org.jboss.logmanager.ExtLogRecord; 19 | import org.jboss.logmanager.formatters.PatternFormatter; 20 | import org.junit.jupiter.api.Test; 21 | import org.junit.jupiter.api.extension.ExtendWith; 22 | import org.mockito.Mock; 23 | import org.mockito.Spy; 24 | import org.mockito.junit.jupiter.MockitoExtension; 25 | 26 | import com.splunk.logging.HttpEventCollectorResendMiddleware; 27 | import com.splunk.logging.HttpEventCollectorSender; 28 | 29 | @ExtendWith(MockitoExtension.class) 30 | class SplunkLogHandlerTest { 31 | 32 | @Mock 33 | HttpEventCollectorSender sender; 34 | 35 | @Spy 36 | Formatter formatter = new PatternFormatter("%s"); 37 | 38 | @Test 39 | void handlerShouldSetSenderOptions() { 40 | SplunkLogHandler handler = new SplunkLogHandler(sender, true, true, true, true, 1); 41 | verify(sender).disableCertificateValidation(); 42 | verify(sender).addMiddleware(isA(HttpEventCollectorResendMiddleware.class)); 43 | } 44 | 45 | @Test 46 | void handlerShouldFormatMessages() { 47 | SplunkLogHandler handler = new SplunkLogHandler(sender, true, true, true, true, 1); 48 | handler.setFormatter(formatter); 49 | ExtLogRecord record = new ExtLogRecord(Level.ALL, "Hello {0}", SplunkLogHandlerTest.class.getName()); 50 | record.setParameters(new String[] { "world" }); 51 | 52 | handler.publish(record); 53 | 54 | verify(formatter).format(eq(record)); 55 | verify(sender).send(anyLong(), 56 | eq(record.getLevel().toString()), 57 | eq("Hello world"), 58 | eq(record.getLoggerName()), 59 | eq("1"), 60 | anyMap(), 61 | isNull(), 62 | isNull()); 63 | } 64 | 65 | @Test 66 | void shouldSendRecordUsingHec() { 67 | SplunkLogHandler handler = new SplunkLogHandler(sender, true, true, true, true, 1); 68 | handler.setFormatter(formatter); 69 | ExtLogRecord record = new ExtLogRecord(Level.ALL, "Log Message", SplunkLogHandlerTest.class.getName()); 70 | record.setLoggerName("Logger"); 71 | record.setThreadID(1); 72 | record.setThrown(new RuntimeException("Exception occurred")); 73 | 74 | handler.publish(record); 75 | 76 | verify(sender).send(anyLong(), 77 | eq(record.getLevel().toString()), 78 | eq(record.getMessage()), 79 | eq(record.getLoggerName()), 80 | eq("1"), 81 | anyMap(), 82 | eq("Exception occurred"), 83 | isNull()); 84 | } 85 | 86 | @Test 87 | void handlerShouldFlushHec() { 88 | SplunkLogHandler handler = new SplunkLogHandler(sender, true, false, false, false, 0); 89 | handler.flush(); 90 | verify(sender).flush(); 91 | verifyNoMoreInteractions(sender); 92 | } 93 | 94 | @Test 95 | void handlerShouldCloseHecProperly() { 96 | SplunkLogHandler handler = new SplunkLogHandler(sender, true, false, false, false, 0); 97 | handler.close(); 98 | verify(sender).flush(eq(true)); 99 | verify(sender).cancel(); 100 | verifyNoMoreInteractions(sender); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /runtime/src/test/java/io/quarkiverse/logging/splunk/TestMiddleware.java: -------------------------------------------------------------------------------- 1 | package io.quarkiverse.logging.splunk; 2 | 3 | import java.util.List; 4 | 5 | import com.splunk.logging.HttpEventCollectorEventInfo; 6 | import com.splunk.logging.HttpEventCollectorMiddleware; 7 | 8 | /** 9 | * Middleware for testing purposes. It only records the events. 10 | */ 11 | public class TestMiddleware extends HttpEventCollectorMiddleware.HttpSenderMiddleware { 12 | 13 | static List recordedEvents; 14 | 15 | @Override 16 | public void postEvents( 17 | List events, 18 | HttpEventCollectorMiddleware.IHttpSender sender, 19 | HttpEventCollectorMiddleware.IHttpSenderCallback callback) { 20 | recordedEvents = events; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /test-utils/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | io.quarkiverse.logging.splunk 6 | quarkus-logging-splunk-parent 7 | 4.0.0-SNAPSHOT 8 | ../pom.xml 9 | 10 | 11 | quarkus-logging-splunk-test-utils 12 | Splunk logging extension - Test Utils 13 | 14 | 15 | 16 | io.quarkus 17 | quarkus-test-common 18 | 19 | 20 | org.jboss.logmanager 21 | jboss-logmanager 22 | 23 | 24 | org.junit.jupiter 25 | junit-jupiter-api 26 | test 27 | 28 | 29 | org.junit.jupiter 30 | junit-jupiter-engine 31 | test 32 | 33 | 34 | org.mockito 35 | mockito-core 36 | test 37 | 38 | 39 | org.mockito 40 | mockito-junit-jupiter 41 | test 42 | 43 | 44 | org.hamcrest 45 | hamcrest 46 | test 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /test-utils/src/main/java/io/quarkiverse/logging/splunk/test/LoggingSplunkApiUrl.java: -------------------------------------------------------------------------------- 1 | package io.quarkiverse.logging.splunk.test; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | @Retention(RetentionPolicy.RUNTIME) 9 | @Target({ ElementType.FIELD }) 10 | public @interface LoggingSplunkApiUrl { 11 | } 12 | -------------------------------------------------------------------------------- /test-utils/src/main/java/io/quarkiverse/logging/splunk/test/LoggingSplunkHandlerUrl.java: -------------------------------------------------------------------------------- 1 | package io.quarkiverse.logging.splunk.test; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | @Retention(RetentionPolicy.RUNTIME) 9 | @Target({ ElementType.FIELD }) 10 | public @interface LoggingSplunkHandlerUrl { 11 | } 12 | -------------------------------------------------------------------------------- /test-utils/src/main/java/io/quarkiverse/logging/splunk/test/LoggingSplunkInjectingTestResource.java: -------------------------------------------------------------------------------- 1 | package io.quarkiverse.logging.splunk.test; 2 | 3 | import java.lang.annotation.Annotation; 4 | import java.lang.reflect.Field; 5 | import java.lang.reflect.Modifier; 6 | import java.util.HashSet; 7 | import java.util.Map; 8 | import java.util.Set; 9 | 10 | import org.jboss.logging.Logger; 11 | 12 | import io.quarkus.test.common.DevServicesContext; 13 | import io.quarkus.test.common.QuarkusTestResourceLifecycleManager; 14 | 15 | public class LoggingSplunkInjectingTestResource 16 | implements QuarkusTestResourceLifecycleManager, DevServicesContext.ContextAware { 17 | private static final Logger log = Logger.getLogger(LoggingSplunkInjectingTestResource.class); 18 | 19 | private static final String HANDLER_URL_CONFIG_PROP = "quarkus.log.handler.splunk.url"; 20 | 21 | private static final String API_URL_CONFIG_PROP = "quarkus.log.handler.splunk.devservices.api-url"; 22 | 23 | private String handlerUrl; 24 | 25 | private String apiUrl; 26 | 27 | @Override 28 | public void setIntegrationTestContext(DevServicesContext context) { 29 | handlerUrl = context.devServicesProperties().get(HANDLER_URL_CONFIG_PROP); 30 | apiUrl = context.devServicesProperties().get(API_URL_CONFIG_PROP); 31 | if (handlerUrl == null || apiUrl == null) { 32 | log.warnf("Did not receive any {} and/or {} property values from the DevServiceContext!", HANDLER_URL_CONFIG_PROP, 33 | API_URL_CONFIG_PROP); 34 | } 35 | } 36 | 37 | @Override 38 | public Map start() { 39 | return Map.of(); 40 | } 41 | 42 | @Override 43 | public void stop() { 44 | 45 | } 46 | 47 | @Override 48 | public void inject(Object testInstance) { 49 | for (Field field : getFields(testInstance.getClass(), true)) { 50 | field.setAccessible(true); 51 | Object currentValue = getValue(field, testInstance); 52 | if (currentValue == null) { 53 | inject(field, testInstance, handlerUrl, LoggingSplunkHandlerUrl.class); 54 | inject(field, testInstance, apiUrl, LoggingSplunkApiUrl.class); 55 | } 56 | } 57 | } 58 | 59 | private Set getFields(Class klazz, boolean isInstanceClass) { 60 | if (Object.class.equals(klazz)) { 61 | return new HashSet<>(); 62 | } 63 | Set set = getFields(klazz.getSuperclass(), false); 64 | for (Field field : klazz.getDeclaredFields()) { 65 | if (isInstanceClass || !Modifier.isPrivate(field.getModifiers())) { 66 | set.add(field); 67 | } 68 | } 69 | return set; 70 | } 71 | 72 | private static Object getValue(Field field, Object testInstance) { 73 | try { 74 | return field.get(testInstance); 75 | } catch (IllegalAccessException e) { 76 | log.warn("Could not get value from field {}", field, e); 77 | } 78 | return null; 79 | } 80 | 81 | private static void inject(Field field, Object testInstance, String value, 82 | Class annotationClass) { 83 | if (field.getType().isAssignableFrom(String.class) && field.isAnnotationPresent(annotationClass)) { 84 | try { 85 | field.set(testInstance, value); 86 | } catch (IllegalAccessException e) { 87 | log.warnf("Could not inject {} into {}", value, testInstance.getClass(), e); 88 | } 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /test-utils/src/test/java/io/quarkiverse/logging/splunk/test/LoggingSplunkInjectingTestResourceTest.java: -------------------------------------------------------------------------------- 1 | package io.quarkiverse.logging.splunk.test; 2 | 3 | import static org.hamcrest.MatcherAssert.assertThat; 4 | import static org.hamcrest.Matchers.equalTo; 5 | import static org.hamcrest.Matchers.nullValue; 6 | import static org.mockito.Mockito.when; 7 | 8 | import java.util.Map; 9 | 10 | import org.junit.jupiter.api.BeforeEach; 11 | import org.junit.jupiter.api.Test; 12 | import org.junit.jupiter.api.extension.ExtendWith; 13 | import org.mockito.Mock; 14 | import org.mockito.junit.jupiter.MockitoExtension; 15 | 16 | import io.quarkus.test.common.DevServicesContext; 17 | 18 | @ExtendWith(MockitoExtension.class) 19 | class LoggingSplunkInjectingTestResourceTest { 20 | @Mock 21 | DevServicesContext context; 22 | 23 | LoggingSplunkInjectingTestResource resource = new LoggingSplunkInjectingTestResource(); 24 | 25 | @BeforeEach 26 | void setUp() { 27 | when(context.devServicesProperties()).thenReturn(Map.of()); 28 | } 29 | 30 | @Test 31 | void disabled() { 32 | FieldWithAnnotation testInstance = new FieldWithAnnotation(); 33 | 34 | resource.setIntegrationTestContext(context); 35 | resource.inject(testInstance); 36 | 37 | assertThat(testInstance.splunkApiUrl, nullValue()); 38 | assertThat(testInstance.splunkHandlerUrl, nullValue()); 39 | } 40 | 41 | @Test 42 | void enabled() { 43 | when(context.devServicesProperties()).thenReturn(Map.of("quarkus.log.handler.splunk.url", 44 | "http://localhost:8088", "quarkus.log.handler.splunk.devservices.api-url", "http://localhost:8089")); 45 | FieldWithAnnotation testInstance = new FieldWithAnnotation(); 46 | 47 | resource.setIntegrationTestContext(context); 48 | resource.inject(testInstance); 49 | 50 | assertThat(testInstance.splunkApiUrl, equalTo("http://localhost:8089")); 51 | assertThat(testInstance.splunkHandlerUrl, equalTo("http://localhost:8088")); 52 | } 53 | 54 | @Test 55 | void noAnnotationORWrongType() { 56 | when(context.devServicesProperties()).thenReturn(Map.of("quarkus.log.handler.splunk.url", 57 | "http://localhost:8088", "quarkus.log.handler.splunk.devservices.api-url", "http://localhost:8089")); 58 | FieldWithNoAnnotation testInstance = new FieldWithNoAnnotation(); 59 | 60 | resource.setIntegrationTestContext(context); 61 | resource.inject(testInstance); 62 | 63 | assertThat(testInstance.splunkApiUrl, nullValue()); 64 | assertThat(testInstance.splunkHandlerUrl, nullValue()); 65 | } 66 | 67 | public static class NoFields { 68 | 69 | } 70 | 71 | public static class FieldWithNoAnnotation { 72 | String splunkApiUrl; 73 | @LoggingSplunkHandlerUrl 74 | Integer splunkHandlerUrl; 75 | } 76 | 77 | public static class FieldWithAnnotation { 78 | @LoggingSplunkApiUrl 79 | String splunkApiUrl; 80 | @LoggingSplunkHandlerUrl 81 | String splunkHandlerUrl; 82 | } 83 | 84 | } --------------------------------------------------------------------------------