├── .github └── workflows │ ├── ci-vert.x-4.x.yml │ ├── ci-vert.x-5.x.yml │ └── sonar.yml ├── .gitignore ├── CHANGELOG.md ├── ES_README.md ├── LA_README.md ├── LICENSE.txt ├── README.md ├── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── src ├── main ├── java │ └── com │ │ └── romanpierson │ │ └── vertx │ │ └── web │ │ └── accesslogger │ │ ├── AccessLoggerConstants.java │ │ ├── AccessLoggerHandler.java │ │ ├── appender │ │ ├── Appender.java │ │ ├── console │ │ │ └── impl │ │ │ │ └── ConsoleAppender.java │ │ ├── elasticsearch │ │ │ └── impl │ │ │ │ └── ElasticSearchAppender.java │ │ ├── eventbus │ │ │ └── impl │ │ │ │ └── EventBusAppender.java │ │ └── logging │ │ │ └── impl │ │ │ └── LoggingAppender.java │ │ ├── configuration │ │ ├── element │ │ │ ├── AccessLogElement.java │ │ │ └── impl │ │ │ │ ├── BytesSentElement.java │ │ │ │ ├── CookieElement.java │ │ │ │ ├── DateTimeElement.java │ │ │ │ ├── DurationElement.java │ │ │ │ ├── EnvironmentValueElement.java │ │ │ │ ├── HeaderElement.java │ │ │ │ ├── HostElement.java │ │ │ │ ├── MethodElement.java │ │ │ │ ├── RequestElement.java │ │ │ │ ├── StaticValueElement.java │ │ │ │ ├── StatusElement.java │ │ │ │ └── VersionElement.java │ │ └── pattern │ │ │ ├── ExtractedPosition.java │ │ │ ├── PatternResolver.java │ │ │ └── ResolvedPatternResult.java │ │ ├── exception │ │ └── AccessLoggerException.java │ │ ├── impl │ │ └── AccessLoggerHandlerImpl.java │ │ ├── util │ │ ├── FormatUtility.java │ │ └── VersionUtility.java │ │ └── verticle │ │ ├── AccessLoggerProducerVerticle.java │ │ └── ResolvedLoggerConfiguration.java └── resources │ └── META-INF │ └── services │ └── com.romanpierson.vertx.web.accesslogger.configuration.element.AccessLogElement └── test ├── java └── com │ └── romanpierson │ ├── custom │ └── element │ │ ├── ShouldNotShowElement.java │ │ └── ShouldShowElement.java │ └── vertx │ ├── test │ └── verticle │ │ └── SimpleJsonResponseVerticle.java │ └── web │ └── accesslogger │ ├── AcessLogServerTest.java │ ├── ConsoleAppenderTest.java │ ├── ElasticSearchAppenderTest.java │ ├── EventBusAppenderTest.java │ ├── LoggingAppenderTest.java │ ├── appender │ └── MemoryAppender.java │ ├── configuration │ ├── element │ │ └── impl │ │ │ ├── AccessLogElementTest.java │ │ │ ├── InvalidElement.java │ │ │ ├── InvalidElement2.java │ │ │ └── ShouldNotShowElement.java │ └── pattern │ │ ├── DurationElementCustomTest.java │ │ ├── PatternResolverTest.java │ │ └── RequestElementTest.java │ ├── element │ └── ElementTest.java │ └── util │ ├── FormatUtilityTest.java │ └── VersionUtilityTest.java └── resources ├── META-INF └── services │ └── com.romanpierson.vertx.web.accesslogger.configuration.element.AccessLogElement ├── accesslog-config-all-valid-console-appender-autocreation-verticle.yaml ├── accesslog-config-all-valid-console-appender.yaml ├── accesslog-config-all-valid-memory-appender.yaml ├── accesslog-config-console-appender-valid.yaml ├── accesslog-config-elasticsearch-appender-invalid-fieldnames.yaml ├── accesslog-config-elasticsearch-appender-invalid-instance.yaml ├── accesslog-config-elasticsearch-appender-valid.yaml ├── accesslog-config-elements-empty-result.yaml ├── accesslog-config-elements-result.yaml ├── accesslog-config-eventbus-appender-existing-address.yaml ├── accesslog-config-eventbus-appender-inexisting-address.yaml ├── accesslog-config-eventbus-appender-invalid-config.yaml ├── accesslog-config-invalid-console-appender.yaml ├── accesslog-config-invalid.yaml ├── accesslog-config-logging-appender-invalid-loggername.yaml ├── accesslog-config-logging-appender-invalid-logpattern.yaml ├── accesslog-config-logging-appender-valid.yaml └── logback.xml /.github/workflows/ci-vert.x-4.x.yml: -------------------------------------------------------------------------------- 1 | name: vert.x (4.x) 2 | 3 | on: 4 | push: 5 | branches: 6 | - vert.x-4.x 7 | 8 | jobs: 9 | ci: 10 | name: Any (supported) 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | os: [ ubuntu-latest ] 15 | java: [ 11 ] 16 | runs-on: ${{ matrix.os }} 17 | steps: 18 | - uses: actions/checkout@v3 19 | - uses: actions/setup-java@v3 20 | with: 21 | java-version: ${{ matrix.java }} 22 | distribution: temurin 23 | - name: Build project 24 | run: ./gradlew clean build 25 | -------------------------------------------------------------------------------- /.github/workflows/ci-vert.x-5.x.yml: -------------------------------------------------------------------------------- 1 | name: vert.x (5.x) 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | ci: 10 | name: Any (supported) 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | os: [ ubuntu-latest ] 15 | java: [ 11 ] 16 | runs-on: ${{ matrix.os }} 17 | steps: 18 | - uses: actions/checkout@v3 19 | - uses: actions/setup-java@v3 20 | with: 21 | java-version: ${{ matrix.java }} 22 | distribution: temurin 23 | - name: Build project 24 | run: ./gradlew clean build 25 | -------------------------------------------------------------------------------- /.github/workflows/sonar.yml: -------------------------------------------------------------------------------- 1 | name: Sonar 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | sonar: 10 | name: SonarCloud 11 | env: 12 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 13 | SONAR_TOKEN: ${{ secrets.sonarcloud }} 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | - uses: actions/setup-java@v3 18 | with: 19 | java-version: 17 20 | distribution: temurin 21 | - name: Build project 22 | run: ./gradlew clean build sonar -Dsonar.host.url=https://sonarcloud.io -Dsonar.organization=romanpierson -Dsonar.projectKey=romanpierson_vertx-web-accesslog 23 | 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /bin/ 2 | .classpath 3 | build/tmp/jar/MANIFEST.MF 4 | build/ 5 | .gradle/ 6 | .project 7 | .settings/org.eclipse.jdt.core.prefs 8 | gradle.properties 9 | .vertx 10 | .metadata 11 | .settings 12 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Changelog 2 | 3 | ### 2.7.0 (Vert.x 5) 4 | 5 | (2025-05-15) 6 | 7 | * Functional identical version of `1.7.0` for `vert.x 5` 8 | 9 | 10 | ### 1.7.0 11 | 12 | (2024-01-13) 13 | 14 | * Native handling of values 15 | * All Log Elements return now native objects if possible 16 | * String based Appenders have to stringify themselves 17 | * Appenders that understand native objects benefit from this change 18 | * **BREAKING CHANGE**: `Appender.push` signature changed and simplified - only relevant if you have a custom Appender 19 | * Upgrade to Vert.x 4.5.1 -> Ready for Virtual Threads 20 | * Automatic injection of `timestamp` for `ElasticSearchAppender` - no need anymore to define it artifically in logPattern 21 | 22 | ### 1.6.1 23 | 24 | (2023-11-10) 25 | 26 | * Headers case insensitive 27 | * Upgrade to latest versions 28 | 29 | ### 1.6.0 30 | 31 | (2023-04-19) 32 | 33 | * Refactored and simplified PatternResolver and Element code using java functions 34 | * Raised test coverage 35 | * Allow easy overwriting of existing patterns with custom elements 36 | * Upgrade to latest versions 37 | 38 | ### 1.5.0 39 | 40 | (2022-12-30) 41 | 42 | * Moved package `com.mdac` to `com.romanpierson` (for maven central) 43 | * Upgrade to latest versions 44 | * Moved from Travis CI to Github Actions / Gradle Sonarqube Plugin 45 | * Removed slf4j dependency 46 | * Integrated `LoggingAppender` and `ElasticSearchAppender` into core library 47 | 48 | ### 1.4.0 49 | 50 | (2020-12-17) 51 | 52 | * Upgrade to Vertx 4, Junit 5, Gradle, all libraries latest versions 53 | * Added `EnvironmentValueElement` 54 | 55 | ### 1.3.1 56 | 57 | (2019-04-17) 58 | 59 | * Fixed a bug with pattern resolver (https://github.com/romanpierson/vertx-web-accesslog/issues/11) 60 | 61 | ### 1.3.0 62 | 63 | (2019-02-14) 64 | 65 | * Changed configuration from custom Option classes to plain JsonObject 66 | * Added plain timestamp as log element 67 | * Replaced `PrintStreamAppender` with `ConsoleAppender` 68 | * Added `EventBusAppender` 69 | * Fixed a bug with picking up the best log element if multiple ones potentially fit 70 | * Added `StaticValueElement` 71 | 72 | ### 1.2.0 73 | 74 | (2019-01-10) 75 | 76 | * Introduced Appender API and removed all except `PrintStreamAppender` to separate projects 77 | * `AccessLogElerment` is able to claim what data it requires 78 | * General refactoring into Constants 79 | * Raw values are translated into formatted values and only those get passed to appenders 80 | * Appenders do not get passed anymore `AccessLogElement` instances 81 | * Fixed a bug in `DateTimeElement` that caused pattern definition not to work 82 | * Fixed a bug in `findInRawPatternInternal` method of several `AccessLogElement` implementations that handle several element patterns and in the case of having a pattern with those following each other this might have led to the situation of bypassing the earlier one 83 | * Extracting correct hostname for local host (%v) 84 | -------------------------------------------------------------------------------- /ES_README.md: -------------------------------------------------------------------------------- 1 | 2 | # Usage 3 | 4 | ## Appender configuration 5 | 6 | As configuration is now done by plain JsonObject its very simple to use and inject configuration eg by yaml, see as an example: 7 | 8 | ```yaml 9 | configurations: 10 | - identifier: accesslog-plain 11 | logPattern: "%D cs-uri" 12 | appenders: 13 | - appenderClassName : com.romanpierson.vertx.web.accesslogger.appender.elasticsearch.impl.ElasticSearchAppender 14 | config: 15 | instanceIdentifier: accesslog 16 | fieldNames: 17 | - duration 18 | - uri 19 | ``` 20 | 21 | ## Conventions 22 | 23 | By default all log elements configured will be sent to the indexer to be interpreted as message to be indexed. However the timestamp is passed to the indexer as meta data as the indexer potentially requires that raw value eg to determinate the target index name. 24 | 25 | The instance identifier tells the indexer verticle to what ES instance the data should be indexed. All the detailed configuration of this is done directly on the indexer verticle itself (so the appender does not have to know about this). 26 | 27 | ## Timestamp 28 | 29 | The timestamp value is automatically added - there is no need to add this on the logPattern. 30 | 31 | The real fieldname used in the indexer for the ES timestamp field is configured in the indexer verticle itself. 32 | 33 | ## Field Names Definition 34 | 35 | As for each field to be indexed a specific name needs to be used this has to be explicitly set by configuration property `fieldNames`. Be aware that you need to put a name for all fields of your logpattern. 36 | -------------------------------------------------------------------------------- /LA_README.md: -------------------------------------------------------------------------------- 1 | Generating the access log files is performed in a transparent way by vertx logger. Therefore there is any restriction regarding the logging framework used behind (however logback is recommended and test case is implemented using logback / SLF4J). Main reason for this is to not have a dependency on a specific logging framework and also to ensure that implementation details like for example rollover strategies (size, daily, etc) are dealt with by the logging framework. 2 | 3 | 4 | # Usage 5 | 6 | ## Appender Configuration 7 | 8 | As configuration is now done by plain JsonObject its very simple to use and inject configuration eg by yaml, see as an example: 9 | 10 | ```yaml 11 | configurations: 12 | - identifier: accesslog-formatted 13 | logPattern: '%{}t %D "cs-uri"' 14 | appenders: 15 | - appenderClassName : com.romanpierson.vertx.web.accesslogger.appender.logging.impl.LoggingAppender 16 | config: 17 | loggerName: accesslog 18 | ``` 19 | 20 | 21 | ## Configure Logger 22 | 23 | The logger itself in the current solution does not has a built in mechanism to write to the physical access file. Instead this is done by the logging framework used behind. 24 | 25 | ### Logback Configuration 26 | 27 | This shows how eg you define with logback.xml - as you see the loggerName has to match with what is defined in the appender configuration 28 | 29 | ```xml 30 | 31 | 32 | 33 | ${access.location}/access.log 34 | 35 | access.%d{yyyy-MM-dd}.log 36 | 10 37 | 38 | 39 | %msg%n 40 | true 41 | 42 | 43 | 44 | 45 | 46 | 0 47 | 500 48 | 5000 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | %d{yyyy-MM-dd HH:mm:ss} %-5level %logger{36} - %msg%n 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | ``` 70 | 71 | If you use a dynamic part like the root log folder you have make sure it is set like this when you create the vertx instance byself: 72 | 73 | ```java 74 | System.setProperty("access.location", "/tmp/accesslog "); 75 | 76 | // Start vertx here... 77 | 78 | ``` 79 | 80 | If you are using a fatjar it needs to be like this (Be aware that the properties have to go before the jar in order to get picked up): 81 | 82 | ```java 83 | java \ 84 | -Daccess.location=/Users/x/y/logs \ 85 | -jar myFatJar.jar 86 | ``` 87 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2016-2025 Roman Pierson 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status (5.x)](https://github.com/romanpierson/vertx-web-accesslog/actions/workflows/ci-vert.x-5.x.yml/badge.svg)](https://github.com/romanpierson/vertx-web-accesslog/actions/workflows/ci-vert.x-5.x.yml) 2 | [![Build Status (4.x)](https://github.com/romanpierson/vertx-web-accesslog/actions/workflows/ci-vert.x-4.x.yml/badge.svg)](https://github.com/romanpierson/vertx-web-accesslog/actions/workflows/ci-vert.x-4.x.yml) 3 | [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=romanpierson_vertx-web-accesslog&metric=coverage)](https://sonarcloud.io/dashboard?id=romanpierson_vertx-web-accesslog) 4 | [![Awesome](https://cdn.rawgit.com/sindresorhus/awesome/d7305f38d29fed78fa85652e3a63e154dd8e8829/media/badge.svg)](https://github.com/vert-x3/vertx-awesome) 5 | 6 | # vertx-web-accesslog 7 | 8 | An access log implementation to be used in vertx web routes. 9 | 10 | Inspired and with intention to be compliant with 11 | 12 | * Apache HTTP Server mod_log_config module (http://httpd.apache.org/docs/2.4/en/mod/mod_log_config.html) 13 | 14 | * W3C Extended Log File Format (http://www.w3.org/TR/WD-logfile.html) 15 | 16 | The main idea is to have an event based logging, with the possibility (but not enforcing you to do so) to directly export a log event natively to a target system, and not having to first write and persist it to a file and then read and parse it again from there like eg ELK is doing it. The drawback that those kind of solutions have is performance but even more issues like recognizing complete stacktraces etc. 17 | The ElasticSearch Appender is the only appender for now that takes advantage of those benefits. 18 | However you are free of course to use a traditional ELK setup style and just write your output to either console or a specific logfile - and let eg logstash take over from there. 19 | 20 | ## Key facts 21 | 22 | * Zero dependencies (apart of vertx-web obviously) 23 | * Easily extensible and customizable 24 | * Small memory footprint 25 | 26 | 27 | ## Technical Usage 28 | 29 | The artefact is published on maven central. 30 | 31 | Just add it as a dependency to your project (gradle example) 32 | 33 | ```xml 34 | dependencies { 35 | compile 'com.romanpierson:vertx-web-accesslog:2.7.0' 36 | } 37 | ``` 38 | 39 | ## Compatibility with Vert.x core 40 | 41 | Since introduction of `vert.x 5` due to some architectural changes the master contains `vert.5` compatible version and its `vert.x 4` compatible counterpart continues on branch `vert.x-4.x`. 42 | 43 | Those two versions are functional equivalent and you should just be able to switch to `vert.5` without any code changes. The plan is also to keep the two versions with same functionality. 44 | 45 | Therefore minor version will stay identical but major version will identify if the library is targeted to be used with `vert.x 4` (1) or `vert.x 5` (2) 46 | 47 | Accesslog version 4.x / 5.x | Vertx version 48 | ----|------ 49 | 1.7.0 / 2.7.0 | 4.5.1 > / 5.0.0 > 50 | 1.6.0 / - | 4.3.0 > / - 51 | 1.5.0 / - | 4.2.0 > / - 52 | 1.4.0 / - | 4.0.0 - 4.1.x / - 53 | 1.3.1 / - | 3.3.0 - 3.7.0 / - 54 | 1.2.0 / - | 3.3.0 - 3.7.0 / - 55 | 56 | Previous versions are listed for completeness only and not supported anymore. 57 | 58 | ## Access Log Pattern Configuration 59 | 60 | The logger supports mixing of both log formats and is also designed to easily add custom log elements 61 | 62 | ### Define your custom AccessLogElement 63 | 64 | You can easily create your custom implementation of `AccessLogElement` by creating your own element class implementing `AccessLogElement` interface. The available `AccessLogElement` types are discovered by ServiceLoader, so just add to your resources a file like this and inside list your `AccessLogElement` classes 65 | 66 | ```xml 67 | META-INF 68 | services 69 | com.romanpierson.vertx.web.accesslogger.configuration.element.AccessLogElement 70 | ``` 71 | 72 | As ServiceLoader SPI is intented to work with objects unfortunately your AccessLogElement implementation requires a parameter less constructor. 73 | 74 | In order that the pattern resolver is able to resolve your new element against a pattern you have to implement your resolving condition by implementing `findInRawPatternInternal(rawPattern)` method. 75 | 76 | To facility this and to avoid boilerplate code there are static helpers in PatternResolver that simplifies that a lot. 77 | 78 | Method | Resolves pattern | Remarks | Examples 79 | ----|------|--|---- 80 | extractBestPositionFromFixPatternIfApplicable | `` | Whatever value is matched and resolved | `%b` `cs-uri` 81 | extractBestPositionFromFixPatternsIfApplicable | `` | Like above but you can pass a list of values to be matched | `%b` `cs-uri` 82 | extractBestPositionFromPostfixPatternIfApplicable | `%{}POSTFIX` | Searches for a postfixed pattern and extracts a configuration string that is passed later to your defined function | `%{msec}t` 83 | extractBestPositionFromPostfixPatternAndAdditionalCheckIfApplicable | `%{}POSTFIX` | Like above but let you define an additional function that checks if the found configuration value is valid for your element to be handled | `%{msec}t` 84 | 85 | ### Redefine existing elements with your custom one 86 | 87 | By defining your custom element using same pattern as an existing one shipped with this library it will have preference over the predefined one. 88 | 89 | 90 | ## Appenders 91 | 92 | Appenders are basically a way to send the log data to one (or multiple) backends. This ships with a set of (hopefully) useful appenders but you can create your custom appenders in a very easy way. 93 | 94 | ### Available Appenders 95 | 96 | Appender | Description 97 | ----|------ 98 | Console Appender | Embedded - main purpose for testing 99 | EventBus Appender | Embedded - simple way to forward access events to a configurable address on the event bus 100 | [Logging Appender](https://github.com/romanpierson/vertx-web-accesslog/blob/master/LA_README.md) | Embedded - Using common logging functionality (logback, slf4j, etc) 101 | [ElasticSearch Appender](https://github.com/romanpierson/vertx-web-accesslog/blob/master/ES_README.md) | Embedded - Experimental appender that writes data to ElasticSearch (For usage eg in kibana) Requires [Vertx ElasticSearch Indexer](https://github.com/romanpierson/vertx-elasticsearch-indexer) 102 | 103 | 104 | 105 | ### Custom Appenders 106 | 107 | You can easily write your own appender doing 108 | 109 | Write your own CustomAppender class that must 110 | * implement `Appender` Interface 111 | * have a public constructor taking a `JsonObject` instance holding the configuration 112 | 113 | ## AccessLoggerProducerVerticle 114 | 115 | This verticle is responsible for receiving the raw data, formatting it based on the AccessLogElements configured and forwards the resulting data to the registered Appenders (by calling `Appender.push`). 116 | 117 | There is one worker instance of `AccessLoggerProducerVerticle` per vertx context started if you put configuration value `isAutoDeployProducerVerticle` to `true` (by default it is). If you prefer to manage the deployment of that verticle byself set the property to `false`. 118 | 119 | ## Usage 120 | 121 | This describes the basic usage. More specific info eg about the different appenders can be found on the links. 122 | 123 | ### Configure route 124 | 125 | Just put an instance of AccessLogHandler as first route handler. 126 | 127 | ```java 128 | Router router = Router.router(vertx); 129 | 130 | JsonObject config = .... load or create your configuration json 131 | 132 | router.route().handler(AccessLoggerHandler.create(config)); 133 | 134 | ``` 135 | 136 | As configuration is now done by plain JsonObject its very simple to use and inject configuration eg by yaml, see as an example `ServerSetupStarter` 137 | 138 | ```yaml 139 | configurations: 140 | - identifier: accesslog-formatted 141 | logPattern: '%{}t %D "cs-uri"' 142 | appenders: 143 | - appenderClassName : com.romanpierson.vertx.web.accesslogger.appender.console.impl.ConsoleAppender 144 | - identifier: accesslog-plain 145 | logPattern: "%{msec}t %D cs-uri" 146 | appenders: 147 | - appenderClassName : com.romanpierson.vertx.web.accesslogger.appender.console.impl.ConsoleAppender 148 | ``` 149 | 150 | ## Supported log elements 151 | 152 | Currently those elements are supported 153 | 154 | Element | Apache | W3C | Remarks 155 | ----|------|------------| -------- 156 | Method | %m | cs-method | | 157 | Status | %s | sc-status | | 158 | Duration s | %T | - | | 159 | Duration ms | %D | - | | 160 | Remote Host | %h | - | | 161 | Local Host | %v | - | | 162 | Local port | %p | - | | 163 | Bytes Written v1 | %B | - | Zero Bytes written as 0 | 164 | Bytes Written v2 | %b | - | Zero Bytes written as - | 165 | First line of request | %r | - | | 166 | URI path only | %U | cs-uri-stem | | 167 | Query only | %q | cs-uri-query | | 168 | URI path incl query | - | cs-uri | | 169 | Version / Protocol | %H | - | | 170 | Datetime Apache | %t | - | Logs by default the request timestamp using format 'EEE, dd MMM yyyy HH:mm:ss zzz', Locale English and Timezone GMT | 171 | Datetime Apache Timeunit | %t{msec} | - | Currently only milliseconds is supported | 172 | | Datetime Apache Configurable v1 | %{PATTERN}t | - | Specify the format pattern, by default it is used Locale English and Timezone GMT | 173 | | Datetime Apache Configurable v2 | %{PATTERN\|TIMEZONE\|LANGUAGE}t | - | Specify format pattern, timezone and language | 174 | Incoming Headers | %{IDENTIFIER}i | - | | 175 | Outgoing Response Headers | %{IDENTIFIER}o | - | | 176 | Cookie | %{IDENTIFIER}C | - | Request cookies only | 177 | Static value | %{IDENTIFIER}static | - | | 178 | Environment Variable value | %{IDENTIFIER}env | - | | 179 | 180 | ### Static values 181 | 182 | For static values you should prefer to use the %{value}static element. In case you have an appender like `ConsoleAppender` or `LoggingAppender` that writes its output via the resolved pattern you can also put such static values directly into the logpattern as it will just stay as non resolved. However for other appenders like `ElasticSearchAppender` one you need to explicitly define the element. 183 | 184 | ### Empty behavior 185 | 186 | The default way for elements where no actual value can be evaluated is to return a `NULL` value. This way the appender is able to translate this into an empty string or eg skip the value if we index towards a solution like ElasticSearch. 187 | 188 | ## Changelog 189 | 190 | Detailed changelog can be found [here](https://github.com/romanpierson/vertx-web-accesslog/blob/master/CHANGELOG.md). 191 | 192 | ## Demo Playground 193 | 194 | A sample project that shows usage of this (and other related features) can be found [here](https://github.com/romanpierson/vertx-logging-playground). 195 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | id 'maven-publish' 4 | id 'jacoco' 5 | id "org.sonarqube" version "6.1.0.5360" 6 | } 7 | 8 | repositories { 9 | mavenCentral() 10 | } 11 | 12 | ext{ 13 | vertxVersion = '5.0.0' 14 | jupiterVersion = '5.12.2' 15 | jupiterLauncherVersion = '1.12.1' 16 | slfApiVersion = '2.0.17' 17 | logbackVersion = '1.5.18' 18 | } 19 | 20 | dependencies { 21 | 22 | implementation "io.vertx:vertx-web:${vertxVersion}" 23 | 24 | testImplementation "io.vertx:vertx-config-yaml:${vertxVersion}" 25 | testImplementation "io.vertx:vertx-junit5:${vertxVersion}" 26 | testImplementation "io.vertx:vertx-web-client:${vertxVersion}" 27 | 28 | testImplementation "org.junit.jupiter:junit-jupiter-api:${jupiterVersion}" 29 | testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:${jupiterVersion}" 30 | testImplementation "org.junit.platform:junit-platform-launcher:${jupiterLauncherVersion}" 31 | 32 | testRuntimeOnly "org.slf4j:slf4j-api:${slfApiVersion}" 33 | testRuntimeOnly "ch.qos.logback:logback-classic:${logbackVersion}" 34 | } 35 | 36 | test { 37 | environment "envVar1", "envVal1" 38 | useJUnitPlatform() 39 | finalizedBy jacocoTestReport 40 | } 41 | 42 | 43 | 44 | jar.archiveFileName = "vertx-web-accesslog-2.7.0.jar" 45 | 46 | java { 47 | withSourcesJar() 48 | sourceCompatibility='11' 49 | targetCompatibility='11' 50 | } 51 | 52 | publishing { 53 | publications { 54 | mavenJava(MavenPublication) { 55 | 56 | groupId = 'com.romanpierson' 57 | artifactId = 'vertx-web-accesslog' 58 | version = '2.7.0' 59 | 60 | from components.java 61 | 62 | } 63 | } 64 | } 65 | 66 | sonar { 67 | properties { 68 | property "sonar.sources", "src/main" 69 | property "sonar.tests", "src/test" 70 | property "sonar.test.exclusions", "**/src/test/**" 71 | } 72 | } 73 | 74 | 75 | jacocoTestReport { 76 | reports { 77 | xml.required = true 78 | html.required = true 79 | } 80 | } 81 | 82 | wrapper() { 83 | gradleVersion = '8.14' 84 | } 85 | 86 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/romanpierson/vertx-web-accesslog/896807d488a9058022089319fdfc6f99f1be6795/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # SPDX-License-Identifier: Apache-2.0 19 | # 20 | 21 | ############################################################################## 22 | # 23 | # Gradle start up script for POSIX generated by Gradle. 24 | # 25 | # Important for running: 26 | # 27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 28 | # noncompliant, but you have some other compliant shell such as ksh or 29 | # bash, then to run this script, type that shell name before the whole 30 | # command line, like: 31 | # 32 | # ksh Gradle 33 | # 34 | # Busybox and similar reduced shells will NOT work, because this script 35 | # requires all of these POSIX shell features: 36 | # * functions; 37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 39 | # * compound commands having a testable exit status, especially «case»; 40 | # * various built-in commands including «command», «set», and «ulimit». 41 | # 42 | # Important for patching: 43 | # 44 | # (2) This script targets any POSIX shell, so it avoids extensions provided 45 | # by Bash, Ksh, etc; in particular arrays are avoided. 46 | # 47 | # The "traditional" practice of packing multiple parameters into a 48 | # space-separated string is a well documented source of bugs and security 49 | # problems, so this is (mostly) avoided, by progressively accumulating 50 | # options in "$@", and eventually passing that to Java. 51 | # 52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 54 | # see the in-line comments for details. 55 | # 56 | # There are tweaks for specific operating systems such as AIX, CygWin, 57 | # Darwin, MinGW, and NonStop. 58 | # 59 | # (3) This script is generated from the Groovy template 60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 61 | # within the Gradle project. 62 | # 63 | # You can find Gradle at https://github.com/gradle/gradle/. 64 | # 65 | ############################################################################## 66 | 67 | # Attempt to set APP_HOME 68 | 69 | # Resolve links: $0 may be a link 70 | app_path=$0 71 | 72 | # Need this for daisy-chained symlinks. 73 | while 74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 75 | [ -h "$app_path" ] 76 | do 77 | ls=$( ls -ld "$app_path" ) 78 | link=${ls#*' -> '} 79 | case $link in #( 80 | /*) app_path=$link ;; #( 81 | *) app_path=$APP_HOME$link ;; 82 | esac 83 | done 84 | 85 | # This is normally unused 86 | # shellcheck disable=SC2034 87 | APP_BASE_NAME=${0##*/} 88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | if ! command -v java >/dev/null 2>&1 137 | then 138 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 139 | 140 | Please set the JAVA_HOME variable in your environment to match the 141 | location of your Java installation." 142 | fi 143 | fi 144 | 145 | # Increase the maximum file descriptors if we can. 146 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 147 | case $MAX_FD in #( 148 | max*) 149 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 150 | # shellcheck disable=SC2039,SC3045 151 | MAX_FD=$( ulimit -H -n ) || 152 | warn "Could not query maximum file descriptor limit" 153 | esac 154 | case $MAX_FD in #( 155 | '' | soft) :;; #( 156 | *) 157 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 158 | # shellcheck disable=SC2039,SC3045 159 | ulimit -n "$MAX_FD" || 160 | warn "Could not set maximum file descriptor limit to $MAX_FD" 161 | esac 162 | fi 163 | 164 | # Collect all arguments for the java command, stacking in reverse order: 165 | # * args from the command line 166 | # * the main class name 167 | # * -classpath 168 | # * -D...appname settings 169 | # * --module-path (only if needed) 170 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 171 | 172 | # For Cygwin or MSYS, switch paths to Windows format before running java 173 | if "$cygwin" || "$msys" ; then 174 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 175 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 176 | 177 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 178 | 179 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 180 | for arg do 181 | if 182 | case $arg in #( 183 | -*) false ;; # don't mess with options #( 184 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 185 | [ -e "$t" ] ;; #( 186 | *) false ;; 187 | esac 188 | then 189 | arg=$( cygpath --path --ignore --mixed "$arg" ) 190 | fi 191 | # Roll the args list around exactly as many times as the number of 192 | # args, so each arg winds up back in the position where it started, but 193 | # possibly modified. 194 | # 195 | # NB: a `for` loop captures its iteration list before it begins, so 196 | # changing the positional parameters here affects neither the number of 197 | # iterations, nor the values presented in `arg`. 198 | shift # remove old arg 199 | set -- "$@" "$arg" # push replacement arg 200 | done 201 | fi 202 | 203 | 204 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 205 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 206 | 207 | # Collect all arguments for the java command: 208 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 209 | # and any embedded shellness will be escaped. 210 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 211 | # treated as '${Hostname}' itself on the command line. 212 | 213 | set -- \ 214 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 215 | -classpath "$CLASSPATH" \ 216 | org.gradle.wrapper.GradleWrapperMain \ 217 | "$@" 218 | 219 | # Stop when "xargs" is not available. 220 | if ! command -v xargs >/dev/null 2>&1 221 | then 222 | die "xargs is not available" 223 | fi 224 | 225 | # Use "xargs" to parse quoted args. 226 | # 227 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 228 | # 229 | # In Bash we could simply go: 230 | # 231 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 232 | # set -- "${ARGS[@]}" "$@" 233 | # 234 | # but POSIX shell has neither arrays nor command substitution, so instead we 235 | # post-process each arg (as a line of input to sed) to backslash-escape any 236 | # character that might be a shell metacharacter, then use eval to reverse 237 | # that process (while maintaining the separation between arguments), and wrap 238 | # the whole thing up as a single "set" statement. 239 | # 240 | # This will of course break if any of these variables contains a newline or 241 | # an unmatched quote. 242 | # 243 | 244 | eval "set -- $( 245 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 246 | xargs -n1 | 247 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 248 | tr '\n' ' ' 249 | )" '"$@"' 250 | 251 | exec "$JAVACMD" "$@" 252 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | -------------------------------------------------------------------------------- /src/main/java/com/romanpierson/vertx/web/accesslogger/AccessLoggerConstants.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016-2025 Roman Pierson 3 | * ------------------------------------------------------ 4 | * All rights reserved. This program and the accompanying materials 5 | * are made available under the terms of the Apache License v2.0 6 | * which accompanies this distribution. 7 | * 8 | * The Apache License v2.0 is available at 9 | * http://www.opensource.org/licenses/apache2.0.php 10 | * 11 | * You may elect to redistribute this code under either of these licenses. 12 | */ 13 | package com.romanpierson.vertx.web.accesslogger; 14 | 15 | public class AccessLoggerConstants { 16 | 17 | private AccessLoggerConstants() {} 18 | 19 | public static final String EVENTBUS_RAW_EVENT_NAME = "accesslog.event.raw"; 20 | public static final String EVENTBUS_APPENDER_EVENT_NAME = "accesslog.event.appender"; 21 | public static final String EVENTBUS_REGISTER_EVENT_NAME = "accesslog.event.register"; 22 | 23 | public static final String ZONE_UTC = "UTC"; 24 | 25 | public static class HandlerConfiguration{ 26 | 27 | private HandlerConfiguration() {} 28 | 29 | public static final String CONFIG_KEY_CONFIGURATIONS = "configurations"; 30 | public static final String CONFIG_KEY_IDENTIFIER = "identifier"; 31 | public static final String CONFIG_KEY_LOGPATTERN = "logPattern"; 32 | public static final String CONFIG_KEY_APPENDERS = "appenders"; 33 | public static final String CONFIG_KEY_APPENDER_CLASS_NAME = "appenderClassName"; 34 | public static final String CONFIG_KEY_IS_AUTO_DEPLOY_PRODUCER_VERTICLE = "isAutoDeployProducerVerticle"; 35 | } 36 | 37 | public static class InternalValues { 38 | 39 | private InternalValues() {} 40 | 41 | public static final String TIMESTAMP = "timestamp"; 42 | 43 | } 44 | 45 | public static class Messages { 46 | 47 | private Messages() {} 48 | 49 | public static class Registration { 50 | 51 | private Registration() {} 52 | 53 | public static class Request { 54 | 55 | private Request() {} 56 | 57 | public static final String IDENTIFIER = "identifier"; 58 | public static final String LOGPATTERN = "logPattern"; 59 | public static final String APPENDERS = "appenders"; 60 | public static final String APPENDER_CLASS_NAME = "appenderClassName"; 61 | public static final String APPENDER_CONFIG = "config"; 62 | 63 | } 64 | 65 | public static class Response { 66 | 67 | private Response() {} 68 | 69 | public static final String REQUIRES_COOKIES = "requiresCookies"; 70 | public static final String REQUIRES_INCOMING_HEADERS = "requiresIncomingHeaders"; 71 | public static final String REQUIRES_OUTGOING_HEADERS = "requiresOutgoingHeaders"; 72 | public static final String RESULT = "result"; 73 | public static final String RESULT_OK = "OK"; 74 | public static final String RESULT_FAILED = "FAILED"; 75 | } 76 | } 77 | 78 | public static class RawEvent { 79 | 80 | private RawEvent() {} 81 | 82 | public static class Request { 83 | 84 | private Request() {} 85 | 86 | public static final String IDENTIFIERS = "identifiers"; 87 | 88 | } 89 | 90 | } 91 | 92 | } 93 | 94 | public static final String CONFIG_KEY_RESOLVED_PATTERN = "resolvedPattern"; 95 | 96 | public static class Request{ 97 | 98 | private Request() {} 99 | 100 | public static class Data{ 101 | 102 | private Data() {} 103 | 104 | public enum Type{ 105 | 106 | REMOTE_HOST("remoteHost"), 107 | LOCAL_HOST("localHost"), 108 | LOCAL_PORT("localPort"), 109 | START_TS_MILLIS("startTSmillis"), 110 | END_TS_MILLIS("endTSmillis"), 111 | BYTES_SENT("bytesSent"), 112 | URI("uri"), 113 | STATUS("status"), 114 | METHOD("method"), 115 | VERSION("version"), 116 | QUERY("query"), 117 | REQUEST_HEADERS("requestHeaders"), 118 | RESPONSE_HEADERS("responseHeaders"), 119 | COOKIES("cookies"); 120 | 121 | private final String fieldName; 122 | 123 | private Type(String fieldName) { 124 | this.fieldName = fieldName; 125 | } 126 | 127 | public String getFieldName() { 128 | return this.fieldName; 129 | } 130 | } 131 | 132 | public static class Fields{ 133 | 134 | private Fields() {} 135 | 136 | public static final String COOKIE_NAME = "name"; 137 | public static final String COOKIE_VALUE = "value"; 138 | 139 | } 140 | 141 | } 142 | } 143 | 144 | public static class ElasticSearchAppenderConfig{ 145 | 146 | private ElasticSearchAppenderConfig() {} 147 | 148 | public static final String ELASTICSEARCH_INDEXER_EVENTBUS_EVENT_NAME = "es.indexer.event"; 149 | 150 | public static class Field{ 151 | 152 | private Field() {} 153 | 154 | public static final String TIMESTAMP = "timestamp"; 155 | public static final String INSTANCE_IDENTIFIER = "instance_identifier"; 156 | public static final String META = "meta"; 157 | public static final String MESSAGE = "message"; 158 | 159 | } 160 | } 161 | 162 | 163 | } 164 | -------------------------------------------------------------------------------- /src/main/java/com/romanpierson/vertx/web/accesslogger/AccessLoggerHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016-2025 Roman Pierson 3 | * ------------------------------------------------------ 4 | * All rights reserved. This program and the accompanying materials 5 | * are made available under the terms of the Apache License v2.0 6 | * which accompanies this distribution. 7 | * 8 | * The Apache License v2.0 is available at 9 | * http://www.opensource.org/licenses/apache2.0.php 10 | * 11 | * You may elect to redistribute this code under either of these licenses. 12 | */ 13 | package com.romanpierson.vertx.web.accesslogger; 14 | 15 | import com.romanpierson.vertx.web.accesslogger.impl.AccessLoggerHandlerImpl; 16 | 17 | import io.vertx.core.Handler; 18 | import io.vertx.core.json.JsonObject; 19 | import io.vertx.ext.web.RoutingContext; 20 | 21 | /** 22 | * 23 | * A handler that logs access information 24 | * 25 | * @author Roman Pierson 26 | * 27 | */ 28 | public interface AccessLoggerHandler extends Handler { 29 | 30 | /** 31 | * 32 | * Creates a logging handler by passing an {@link JsonObject} configuration 33 | * 34 | * @param config The access logger configuration 35 | * @return The logging handler 36 | */ 37 | static AccessLoggerHandler create(final JsonObject config){ 38 | 39 | return new AccessLoggerHandlerImpl(config); 40 | 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/com/romanpierson/vertx/web/accesslogger/appender/Appender.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016-2025 Roman Pierson 3 | * ------------------------------------------------------ 4 | * All rights reserved. This program and the accompanying materials 5 | * are made available under the terms of the Apache License v2.0 6 | * which accompanies this distribution. 7 | * 8 | * The Apache License v2.0 is available at 9 | * http://www.opensource.org/licenses/apache2.0.php 10 | * 11 | * You may elect to redistribute this code under either of these licenses. 12 | */ 13 | package com.romanpierson.vertx.web.accesslogger.appender; 14 | 15 | import java.util.List; 16 | import java.util.Map; 17 | 18 | /** 19 | * 20 | * An IF defining an appender that can handle Access Events 21 | * 22 | * @author Roman Pierson 23 | * 24 | */ 25 | public interface Appender { 26 | 27 | /** 28 | * 29 | * Push the access element values to the appender. 30 | * 31 | * Those values can contain native data types and null values 32 | * 33 | * Its the appenders responsibility to implement local storage 34 | * 35 | * @param rawAccessElementValues List of access events the appender should handle - those are no copies 36 | * @param internalValues A list of internal values that are sent independent from the configured access log elements 37 | */ 38 | void push(List rawAccessElementValues, Map internalValues); 39 | 40 | 41 | /** 42 | * Is called by the AccessLogger when the application is shutdown and gives the appender the chance to perform additional actions eg in case data is buffered etc 43 | */ 44 | default void notifyShutdown() { 45 | 46 | // Not forcing the implementations to implement if not required 47 | 48 | } 49 | 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/com/romanpierson/vertx/web/accesslogger/appender/console/impl/ConsoleAppender.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016-2025 Roman Pierson 3 | * ------------------------------------------------------ 4 | * All rights reserved. This program and the accompanying materials 5 | * are made available under the terms of the Apache License v2.0 6 | * which accompanies this distribution. 7 | * 8 | * The Apache License v2.0 is available at 9 | * http://www.opensource.org/licenses/apache2.0.php 10 | * 11 | * You may elect to redistribute this code under either of these licenses. 12 | */ 13 | package com.romanpierson.vertx.web.accesslogger.appender.console.impl; 14 | 15 | import static com.romanpierson.vertx.web.accesslogger.util.FormatUtility.getStringifiedParameterValues; 16 | 17 | import java.util.List; 18 | import java.util.Map; 19 | 20 | import com.romanpierson.vertx.web.accesslogger.AccessLoggerConstants; 21 | import com.romanpierson.vertx.web.accesslogger.appender.Appender; 22 | 23 | import io.vertx.core.internal.logging.Logger; 24 | import io.vertx.core.internal.logging.LoggerFactory; 25 | import io.vertx.core.json.JsonObject; 26 | 27 | /** 28 | * 29 | * An implementation of {@link Appender} that writes to System.out 30 | * 31 | * @author Roman Pierson 32 | * 33 | */ 34 | public class ConsoleAppender implements Appender { 35 | 36 | private final Logger logger = LoggerFactory.getLogger(this.getClass().getName()); 37 | 38 | private final String resolvedPattern; 39 | 40 | public ConsoleAppender(final JsonObject config){ 41 | 42 | if(config.getString(AccessLoggerConstants.CONFIG_KEY_RESOLVED_PATTERN, "").trim().length() == 0){ 43 | throw new IllegalArgumentException("resolvedPattern must not be empty"); 44 | } 45 | 46 | this.resolvedPattern = config.getString(AccessLoggerConstants.CONFIG_KEY_RESOLVED_PATTERN); 47 | 48 | logger.info("Created ConsoleAppender with resolvedLogPattern [" + this.resolvedPattern + "]"); 49 | 50 | } 51 | 52 | @Override 53 | public void push(List rawAccessElementValues, Map internalValues) { 54 | 55 | final String formattedString = String.format(this.resolvedPattern, getStringifiedParameterValues(rawAccessElementValues)); 56 | 57 | System.out.println(formattedString); 58 | 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/com/romanpierson/vertx/web/accesslogger/appender/elasticsearch/impl/ElasticSearchAppender.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016-2025 Roman Pierson 3 | * ------------------------------------------------------ 4 | * All rights reserved. This program and the accompanying materials 5 | * are made available under the terms of the Apache License v2.0 6 | * which accompanies this distribution. 7 | * 8 | * The Apache License v2.0 is available at 9 | * http://www.opensource.org/licenses/apache2.0.php 10 | * 11 | * You may elect to redistribute this code under either of these licenses. 12 | */ 13 | package com.romanpierson.vertx.web.accesslogger.appender.elasticsearch.impl; 14 | 15 | import java.util.Collection; 16 | import java.util.List; 17 | import java.util.Map; 18 | 19 | import com.romanpierson.vertx.web.accesslogger.AccessLoggerConstants; 20 | import com.romanpierson.vertx.web.accesslogger.AccessLoggerConstants.ElasticSearchAppenderConfig; 21 | import com.romanpierson.vertx.web.accesslogger.AccessLoggerConstants.ElasticSearchAppenderConfig.Field; 22 | import com.romanpierson.vertx.web.accesslogger.appender.Appender; 23 | 24 | import io.vertx.core.Vertx; 25 | import io.vertx.core.eventbus.EventBus; 26 | import io.vertx.core.internal.logging.Logger; 27 | import io.vertx.core.internal.logging.LoggerFactory; 28 | import io.vertx.core.json.JsonArray; 29 | import io.vertx.core.json.JsonObject; 30 | 31 | /** 32 | * 33 | * An implementation of {@link Appender} that writes to {ElasticSearchIndexerVerticle} 34 | * 35 | * see https://github.com/romanpierson/vertx-elasticsearch-indexer 36 | * 37 | * @author Roman Pierson 38 | * 39 | */ 40 | public class ElasticSearchAppender implements Appender { 41 | 42 | private final Logger logger = LoggerFactory.getLogger(this.getClass().getName()); 43 | 44 | private static final String CONFIG_KEY_INSTANCE_IDENTIFER = "instanceIdentifier"; 45 | private static final String CONFIG_KEY_FIELD_NAMES = "fieldNames"; 46 | 47 | private final EventBus vertxEventBus; 48 | 49 | private final String instanceIdentifier; 50 | private final Collection fieldNames; 51 | 52 | @SuppressWarnings("unchecked") 53 | public ElasticSearchAppender(final JsonObject config){ 54 | 55 | if (config.getString(CONFIG_KEY_INSTANCE_IDENTIFER, "").trim().length() == 0) { 56 | throw new IllegalArgumentException(CONFIG_KEY_INSTANCE_IDENTIFER + " must not be empty"); 57 | } else if (config.getJsonArray(CONFIG_KEY_FIELD_NAMES, new JsonArray()).isEmpty()) { 58 | throw new IllegalArgumentException(CONFIG_KEY_FIELD_NAMES + " must not be empty"); 59 | } 60 | 61 | this.vertxEventBus = Vertx.currentContext().owner().eventBus(); 62 | 63 | this.instanceIdentifier = config.getString(CONFIG_KEY_INSTANCE_IDENTIFER); 64 | this.fieldNames = config.getJsonArray(CONFIG_KEY_FIELD_NAMES).getList(); 65 | 66 | logger.info("Created ElasticSearchAppender with " + CONFIG_KEY_INSTANCE_IDENTIFER + " [" + this.instanceIdentifier + "], " + CONFIG_KEY_FIELD_NAMES + " " +this.fieldNames); 67 | } 68 | 69 | private Object[] getParameterValuesCopy(List rawAccessElementValues){ 70 | 71 | final Object[] parameterValues = new Object[rawAccessElementValues.size()]; 72 | 73 | int i = 0; 74 | for (final Object xValue : rawAccessElementValues) { 75 | parameterValues[i] = xValue; 76 | i++; 77 | } 78 | 79 | return parameterValues; 80 | 81 | } 82 | 83 | @Override 84 | public void push(final List rawAccessElementValues, Map internalValues) { 85 | 86 | // Just send the accesslog event to the indexer 87 | JsonObject jsonMeta = new JsonObject(); 88 | jsonMeta.put(Field.TIMESTAMP, (Long) internalValues.get(AccessLoggerConstants.InternalValues.TIMESTAMP)); 89 | jsonMeta.put(Field.INSTANCE_IDENTIFIER, this.instanceIdentifier); 90 | 91 | JsonObject jsonMessage = new JsonObject(); 92 | 93 | // Probably we d not need a copy here - but we are not copying the values themselves 94 | Object [] nativeParameterValues = getParameterValuesCopy(rawAccessElementValues); 95 | 96 | int i = 0; 97 | for(String fieldName : fieldNames) { 98 | 99 | if(nativeParameterValues[i] != null){ 100 | // Values having no value we dont send either 101 | jsonMessage.put(fieldName, nativeParameterValues[i]); 102 | } 103 | 104 | i++; 105 | } 106 | 107 | JsonObject json = new JsonObject() 108 | .put(Field.META, jsonMeta) 109 | .put(Field.MESSAGE, jsonMessage); 110 | 111 | this.vertxEventBus.send(ElasticSearchAppenderConfig.ELASTICSEARCH_INDEXER_EVENTBUS_EVENT_NAME, json); 112 | 113 | } 114 | 115 | } 116 | -------------------------------------------------------------------------------- /src/main/java/com/romanpierson/vertx/web/accesslogger/appender/eventbus/impl/EventBusAppender.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016-2025 Roman Pierson 3 | * ------------------------------------------------------ 4 | * All rights reserved. This program and the accompanying materials 5 | * are made available under the terms of the Apache License v2.0 6 | * which accompanies this distribution. 7 | * 8 | * The Apache License v2.0 is available at 9 | * http://www.opensource.org/licenses/apache2.0.php 10 | * 11 | * You may elect to redistribute this code under either of these licenses. 12 | */ 13 | package com.romanpierson.vertx.web.accesslogger.appender.eventbus.impl; 14 | 15 | import java.util.List; 16 | import java.util.Map; 17 | 18 | import com.romanpierson.vertx.web.accesslogger.appender.Appender; 19 | 20 | import io.vertx.core.Vertx; 21 | import io.vertx.core.eventbus.EventBus; 22 | import io.vertx.core.internal.logging.Logger; 23 | import io.vertx.core.internal.logging.LoggerFactory; 24 | import io.vertx.core.json.JsonArray; 25 | import io.vertx.core.json.JsonObject; 26 | 27 | /** 28 | * 29 | * An implementation of {@link Appender} that forwards the access event to the given event bus target address 30 | * 31 | * @author Roman Pierson 32 | * 33 | */ 34 | public class EventBusAppender implements Appender { 35 | 36 | private static final String CONFIG_KEY_TARGET_ADDRESS = "targetAddress"; 37 | 38 | private final Logger logger = LoggerFactory.getLogger(this.getClass().getName()); 39 | 40 | private final EventBus vertxEventBus; 41 | private final String eventBusTargetAddress; 42 | 43 | public EventBusAppender(final JsonObject config){ 44 | 45 | if(config.getString(CONFIG_KEY_TARGET_ADDRESS, "").trim().length() == 0){ 46 | throw new IllegalArgumentException(CONFIG_KEY_TARGET_ADDRESS + " must not be empty"); 47 | } 48 | 49 | this.vertxEventBus = Vertx.currentContext().owner().eventBus(); 50 | 51 | this.eventBusTargetAddress = config.getString(CONFIG_KEY_TARGET_ADDRESS); 52 | 53 | logger.info("Created EventBusAppender with eventBusTargetAddress [" + this.eventBusTargetAddress + "]"); 54 | 55 | } 56 | 57 | @Override 58 | public void push(final List rawAccessElementValues, Map internalValues) { 59 | 60 | // Maybe we should we make a copy here? 61 | 62 | vertxEventBus.send(this.eventBusTargetAddress, convertToJsonArray(rawAccessElementValues)); 63 | 64 | } 65 | 66 | private JsonArray convertToJsonArray(List rawAccessElementValues){ 67 | 68 | JsonArray s = new JsonArray(); 69 | 70 | rawAccessElementValues.forEach(x -> s.add(x)); 71 | 72 | return s; 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /src/main/java/com/romanpierson/vertx/web/accesslogger/appender/logging/impl/LoggingAppender.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016-2025 Roman Pierson 3 | * ------------------------------------------------------ 4 | * All rights reserved. This program and the accompanying materials 5 | * are made available under the terms of the Apache License v2.0 6 | * which accompanies this distribution. 7 | * 8 | * The Apache License v2.0 is available at 9 | * http://www.opensource.org/licenses/apache2.0.php 10 | * 11 | * You may elect to redistribute this code under either of these licenses. 12 | */ 13 | package com.romanpierson.vertx.web.accesslogger.appender.logging.impl; 14 | 15 | 16 | import com.romanpierson.vertx.web.accesslogger.AccessLoggerConstants; 17 | import com.romanpierson.vertx.web.accesslogger.appender.Appender; 18 | 19 | import io.vertx.core.internal.logging.Logger; 20 | import io.vertx.core.internal.logging.LoggerFactory; 21 | import io.vertx.core.json.JsonObject; 22 | 23 | import static com.romanpierson.vertx.web.accesslogger.util.FormatUtility.getStringifiedParameterValues; 24 | 25 | import java.util.List; 26 | import java.util.Map; 27 | 28 | /** 29 | * 30 | * An implementation of {@link Appender} that writes to standard log 31 | * 32 | * @author Roman Pierson 33 | * 34 | */ 35 | public class LoggingAppender implements Appender { 36 | 37 | private static final String CONFIG_KEY_LOGGER_NAME = "loggerName"; 38 | 39 | private String resolvedPattern; 40 | private final Logger logger; 41 | 42 | public LoggingAppender(final JsonObject config) { 43 | 44 | if (config.getString(AccessLoggerConstants.CONFIG_KEY_RESOLVED_PATTERN, "").trim().length() == 0) { 45 | throw new IllegalArgumentException("resolvedPattern must not be empty"); 46 | } else if (config.getString(CONFIG_KEY_LOGGER_NAME, "").trim().length() == 0) { 47 | throw new IllegalArgumentException("loggerName must not be empty"); 48 | } 49 | 50 | final String loggerName = config.getString(CONFIG_KEY_LOGGER_NAME); 51 | 52 | this.resolvedPattern = config.getString(AccessLoggerConstants.CONFIG_KEY_RESOLVED_PATTERN); 53 | this.logger = LoggerFactory.getLogger(loggerName); 54 | } 55 | 56 | @Override 57 | public void push(List rawAccessElementValues, Map internalValues) { 58 | 59 | final String formattedString = String.format(this.resolvedPattern, getStringifiedParameterValues(rawAccessElementValues)); 60 | 61 | this.logger.info(formattedString); 62 | 63 | } 64 | 65 | 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/com/romanpierson/vertx/web/accesslogger/configuration/element/AccessLogElement.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016-2025 Roman Pierson 3 | * ------------------------------------------------------ 4 | * All rights reserved. This program and the accompanying materials 5 | * are made available under the terms of the Apache License v2.0 6 | * which accompanies this distribution. 7 | * 8 | * The Apache License v2.0 is available at 9 | * http://www.opensource.org/licenses/apache2.0.php 10 | * 11 | * You may elect to redistribute this code under either of these licenses. 12 | */ 13 | package com.romanpierson.vertx.web.accesslogger.configuration.element; 14 | 15 | import java.util.Collection; 16 | import java.util.Collections; 17 | import java.util.Objects; 18 | import java.util.stream.Collectors; 19 | 20 | import com.romanpierson.vertx.web.accesslogger.AccessLoggerConstants; 21 | import com.romanpierson.vertx.web.accesslogger.configuration.pattern.ExtractedPosition; 22 | 23 | import io.vertx.core.json.JsonObject; 24 | 25 | public interface AccessLogElement { 26 | 27 | 28 | Collection findInRawPatternInternal(String rawPattern); 29 | 30 | /** 31 | * 32 | * 33 | * 34 | * @param rawPattern The pattern you want to search on 35 | * @return A list of matching extracted positions 36 | */ 37 | default Collection findInRawPattern(final String rawPattern){ 38 | 39 | final Collection foundPatterns = findInRawPatternInternal(rawPattern); 40 | 41 | for(final ExtractedPosition ep : foundPatterns) { 42 | if(ep == null || ep.getStart() == -1){ 43 | // at least one element is invalid for further usage - so we will clean the elements 44 | return foundPatterns.stream().filter(Objects::nonNull).filter(ep2 -> ep2.getStart() >= 0).collect(Collectors.toList()); 45 | } 46 | } 47 | 48 | // All elements clean when we get here 49 | return foundPatterns; 50 | 51 | } 52 | 53 | /** 54 | * 55 | * Allows to communicate what parts of the request are required for this element 56 | * 57 | * Only required for the more complex datasets - the basic data parts are always provided 58 | * 59 | * @return The list of explicitly required data parts 60 | */ 61 | default Collection claimDataParts(){ 62 | 63 | // Not enforcing anymore element implementations to explicitly define this lookup functionality 64 | return Collections.emptyList(); 65 | 66 | } 67 | 68 | Object getNativeValue(JsonObject values); 69 | 70 | 71 | } 72 | -------------------------------------------------------------------------------- /src/main/java/com/romanpierson/vertx/web/accesslogger/configuration/element/impl/BytesSentElement.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016-2025 Roman Pierson 3 | * ------------------------------------------------------ 4 | * All rights reserved. This program and the accompanying materials 5 | * are made available under the terms of the Apache License v2.0 6 | * which accompanies this distribution. 7 | * 8 | * The Apache License v2.0 is available at 9 | * http://www.opensource.org/licenses/apache2.0.php 10 | * 11 | * You may elect to redistribute this code under either of these licenses. 12 | */ 13 | package com.romanpierson.vertx.web.accesslogger.configuration.element.impl; 14 | 15 | import java.util.ArrayList; 16 | import java.util.Collection; 17 | 18 | import com.romanpierson.vertx.web.accesslogger.AccessLoggerConstants.Request.Data; 19 | import com.romanpierson.vertx.web.accesslogger.configuration.element.AccessLogElement; 20 | import com.romanpierson.vertx.web.accesslogger.configuration.pattern.ExtractedPosition; 21 | import static com.romanpierson.vertx.web.accesslogger.configuration.pattern.PatternResolver.extractBestPositionFromFixPatternIfApplicable; 22 | 23 | import io.vertx.core.json.JsonObject; 24 | 25 | public class BytesSentElement implements AccessLogElement{ 26 | 27 | private Mode mode; 28 | 29 | private static final Long DEFAULT_VALUE = Long.valueOf(0); 30 | 31 | private enum Mode{ 32 | 33 | NO_BYTES_NULL, 34 | NO_BYTES_DASH 35 | 36 | } 37 | 38 | public static BytesSentElement of(final Mode mode){ 39 | 40 | BytesSentElement element = new BytesSentElement(); 41 | element.mode = mode; 42 | 43 | return element; 44 | } 45 | 46 | @Override 47 | public Collection findInRawPatternInternal(final String rawPattern) { 48 | 49 | Collection foundPositions = new ArrayList<>(2); 50 | 51 | extractBestPositionFromFixPatternIfApplicable(rawPattern, "%b", () -> BytesSentElement.of(Mode.NO_BYTES_DASH)).ifPresent(foundPositions::add); 52 | extractBestPositionFromFixPatternIfApplicable(rawPattern, "%B", () -> BytesSentElement.of(Mode.NO_BYTES_NULL)).ifPresent(foundPositions::add); 53 | 54 | return foundPositions; 55 | 56 | } 57 | 58 | @Override 59 | public Object getNativeValue(final JsonObject values) { 60 | 61 | final long bytes = values.getLong(Data.Type.BYTES_SENT.getFieldName(), DEFAULT_VALUE); 62 | 63 | if(bytes > 0) { 64 | return Long.valueOf(bytes); 65 | } else { 66 | return Mode.NO_BYTES_DASH.equals(this.mode) ? "-" : Long.valueOf(0); 67 | } 68 | 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /src/main/java/com/romanpierson/vertx/web/accesslogger/configuration/element/impl/CookieElement.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016-2025 Roman Pierson 3 | * ------------------------------------------------------ 4 | * All rights reserved. This program and the accompanying materials 5 | * are made available under the terms of the Apache License v2.0 6 | * which accompanies this distribution. 7 | * 8 | * The Apache License v2.0 is available at 9 | * http://www.opensource.org/licenses/apache2.0.php 10 | * 11 | * You may elect to redistribute this code under either of these licenses. 12 | */ 13 | package com.romanpierson.vertx.web.accesslogger.configuration.element.impl; 14 | 15 | import static com.romanpierson.vertx.web.accesslogger.configuration.pattern.PatternResolver.extractBestPositionFromPostfixPatternIfApplicable; 16 | 17 | import java.util.ArrayList; 18 | import java.util.Collection; 19 | import java.util.Collections; 20 | 21 | import com.romanpierson.vertx.web.accesslogger.AccessLoggerConstants.Request.Data; 22 | import com.romanpierson.vertx.web.accesslogger.configuration.element.AccessLogElement; 23 | import com.romanpierson.vertx.web.accesslogger.configuration.pattern.ExtractedPosition; 24 | 25 | import io.vertx.core.json.JsonArray; 26 | import io.vertx.core.json.JsonObject; 27 | 28 | public class CookieElement implements AccessLogElement{ 29 | 30 | private String identifier; 31 | 32 | public static CookieElement of(final String identifier) { 33 | 34 | CookieElement element = new CookieElement(); 35 | element.identifier = identifier; 36 | 37 | return element; 38 | } 39 | 40 | @Override 41 | public Collection findInRawPatternInternal(final String rawPattern) { 42 | 43 | Collection foundPositions = new ArrayList<>(1); 44 | 45 | extractBestPositionFromPostfixPatternIfApplicable(rawPattern, "C", 46 | json -> CookieElement.of(json.getString("configuration"))) 47 | .ifPresent(foundPositions::add); 48 | 49 | return foundPositions; 50 | } 51 | 52 | @Override 53 | public Object getNativeValue(final JsonObject values) { 54 | 55 | if(!values.containsKey(Data.Type.COOKIES.getFieldName())){ 56 | return null; 57 | } 58 | 59 | final JsonArray cookies = values.getJsonArray(Data.Type.COOKIES.getFieldName()); 60 | 61 | for(int i = 0; i < cookies.size(); i++ ) { 62 | JsonObject cookie = cookies.getJsonObject(i); 63 | if(this.identifier.equals(cookie.getString(Data.Fields.COOKIE_NAME))){ 64 | return cookie.getString(Data.Fields.COOKIE_VALUE); 65 | } 66 | } 67 | 68 | return null; 69 | 70 | } 71 | 72 | @Override 73 | public Collection claimDataParts() { 74 | 75 | return Collections.singleton(Data.Type.COOKIES); 76 | 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /src/main/java/com/romanpierson/vertx/web/accesslogger/configuration/element/impl/DateTimeElement.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016-2025 Roman Pierson 3 | * ------------------------------------------------------ 4 | * All rights reserved. This program and the accompanying materials 5 | * are made available under the terms of the Apache License v2.0 6 | * which accompanies this distribution. 7 | * 8 | * The Apache License v2.0 is available at 9 | * http://www.opensource.org/licenses/apache2.0.php 10 | * 11 | * You may elect to redistribute this code under either of these licenses. 12 | */ 13 | package com.romanpierson.vertx.web.accesslogger.configuration.element.impl; 14 | 15 | import static com.romanpierson.vertx.web.accesslogger.configuration.pattern.PatternResolver.extractBestPositionFromFixPatternIfApplicable; 16 | import static com.romanpierson.vertx.web.accesslogger.configuration.pattern.PatternResolver.extractBestPositionFromPostfixPatternAndAdditionalCheckIfApplicable; 17 | 18 | import java.text.DateFormat; 19 | import java.text.SimpleDateFormat; 20 | import java.util.ArrayList; 21 | import java.util.Collection; 22 | import java.util.Locale; 23 | import java.util.TimeZone; 24 | 25 | import com.romanpierson.vertx.web.accesslogger.AccessLoggerConstants; 26 | import com.romanpierson.vertx.web.accesslogger.AccessLoggerConstants.Request.Data; 27 | import com.romanpierson.vertx.web.accesslogger.configuration.element.AccessLogElement; 28 | import com.romanpierson.vertx.web.accesslogger.configuration.pattern.ExtractedPosition; 29 | 30 | import io.vertx.core.json.JsonObject; 31 | 32 | public class DateTimeElement implements AccessLogElement{ 33 | 34 | private DateFormat dateFormat; 35 | 36 | public static DateTimeElement of(final String configurationString) { 37 | 38 | DateTimeElement element = new DateTimeElement(); 39 | element.dateFormat = deriveDateFormatFromConfigurationString(configurationString); 40 | 41 | return element; 42 | } 43 | 44 | public static DateTimeElement of(final DateFormat dateFormat) { 45 | 46 | DateTimeElement element = new DateTimeElement(); 47 | element.dateFormat = dateFormat; 48 | 49 | return element; 50 | } 51 | 52 | @Override 53 | public Collection findInRawPatternInternal(final String rawPattern) { 54 | 55 | Collection foundPositions = new ArrayList<>(2); 56 | 57 | extractBestPositionFromPostfixPatternAndAdditionalCheckIfApplicable(rawPattern, "t", 58 | json -> json.getString("configuration", "").length() > 0, // At least we expect some configuration even if at the end we would fallback to default 59 | json -> DateTimeElement.of(json.getString("configuration"))) 60 | .ifPresent(foundPositions::add); 61 | 62 | extractBestPositionFromFixPatternIfApplicable(rawPattern, "%t", () -> DateTimeElement.of(createRFC1123DateTimeFormatter())).ifPresent(foundPositions::add); 63 | 64 | return foundPositions; 65 | } 66 | 67 | protected static DateFormat deriveDateFormatFromConfigurationString(final String configurationString){ 68 | 69 | if(configurationString != null && configurationString.length() > 0){ 70 | 71 | if("msec".equalsIgnoreCase(configurationString)) { 72 | return null; 73 | } 74 | 75 | final String[] configurationTokens = configurationString.split("\\|"); 76 | 77 | if(configurationTokens != null && configurationTokens.length == 3){ 78 | 79 | // Assume that is a configuration including format, timezone and locale 80 | 81 | DateFormat dtf = new SimpleDateFormat(configurationTokens[0], Locale.forLanguageTag(configurationTokens[2])); 82 | dtf.setTimeZone(TimeZone.getTimeZone(configurationTokens[1])); 83 | 84 | return dtf; 85 | } 86 | else if(configurationTokens != null){ 87 | 88 | // Assume this is just a format configuration 89 | DateFormat dtf = new SimpleDateFormat(configurationTokens[0], Locale.ENGLISH); 90 | dtf.setTimeZone(TimeZone.getTimeZone(AccessLoggerConstants.ZONE_UTC)); 91 | 92 | return dtf; 93 | } 94 | } 95 | 96 | return createRFC1123DateTimeFormatter(); 97 | 98 | } 99 | 100 | @Override 101 | public Object getNativeValue(final JsonObject values) { 102 | 103 | if(this.dateFormat == null) { 104 | return values.getLong(Data.Type.START_TS_MILLIS.getFieldName()); 105 | } else { 106 | return this.dateFormat.format(values.getLong(Data.Type.START_TS_MILLIS.getFieldName())); 107 | } 108 | } 109 | 110 | private static DateFormat createRFC1123DateTimeFormatter() { 111 | 112 | final DateFormat dtf = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.ENGLISH); 113 | dtf.setTimeZone(TimeZone.getTimeZone("GMT")); 114 | return dtf; 115 | 116 | } 117 | 118 | } 119 | -------------------------------------------------------------------------------- /src/main/java/com/romanpierson/vertx/web/accesslogger/configuration/element/impl/DurationElement.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016-2025 Roman Pierson 3 | * ------------------------------------------------------ 4 | * All rights reserved. This program and the accompanying materials 5 | * are made available under the terms of the Apache License v2.0 6 | * which accompanies this distribution. 7 | * 8 | * The Apache License v2.0 is available at 9 | * http://www.opensource.org/licenses/apache2.0.php 10 | * 11 | * You may elect to redistribute this code under either of these licenses. 12 | */ 13 | package com.romanpierson.vertx.web.accesslogger.configuration.element.impl; 14 | 15 | import java.util.ArrayList; 16 | import java.util.Collection; 17 | 18 | import com.romanpierson.vertx.web.accesslogger.AccessLoggerConstants.Request.Data; 19 | import com.romanpierson.vertx.web.accesslogger.configuration.element.AccessLogElement; 20 | import com.romanpierson.vertx.web.accesslogger.configuration.pattern.ExtractedPosition; 21 | import static com.romanpierson.vertx.web.accesslogger.configuration.pattern.PatternResolver.extractBestPositionFromFixPatternIfApplicable; 22 | 23 | import io.vertx.core.json.JsonObject; 24 | 25 | public class DurationElement implements AccessLogElement{ 26 | 27 | private enum TimeUnit{ 28 | 29 | SECONDS, 30 | MILLISECONDS 31 | 32 | } 33 | 34 | private TimeUnit timeUnit; 35 | 36 | public static DurationElement of(final TimeUnit timeUnit){ 37 | 38 | DurationElement element = new DurationElement(); 39 | element.timeUnit = timeUnit; 40 | 41 | return element; 42 | } 43 | 44 | 45 | @Override 46 | public Collection findInRawPatternInternal(final String rawPattern) { 47 | 48 | Collection foundPositions = new ArrayList<>(2); 49 | 50 | extractBestPositionFromFixPatternIfApplicable(rawPattern, "%D", () -> DurationElement.of(TimeUnit.MILLISECONDS)).ifPresent(foundPositions::add); 51 | extractBestPositionFromFixPatternIfApplicable(rawPattern, "%T", () -> DurationElement.of(TimeUnit.SECONDS)).ifPresent(foundPositions::add); 52 | 53 | return foundPositions; 54 | } 55 | 56 | @Override 57 | public Object getNativeValue(final JsonObject values) { 58 | 59 | final Long startTS = values.getLong(Data.Type.START_TS_MILLIS.getFieldName()); 60 | final Long endTS = values.getLong(Data.Type.END_TS_MILLIS.getFieldName()); 61 | 62 | long duration = endTS.longValue() - startTS.longValue(); 63 | 64 | if(TimeUnit.SECONDS.equals(this.timeUnit)){ 65 | duration = duration / 1000; 66 | } 67 | 68 | return duration > 0 ? duration : Long.valueOf(0); 69 | } 70 | 71 | 72 | } 73 | -------------------------------------------------------------------------------- /src/main/java/com/romanpierson/vertx/web/accesslogger/configuration/element/impl/EnvironmentValueElement.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016-2025 Roman Pierson 3 | * ------------------------------------------------------ 4 | * All rights reserved. This program and the accompanying materials 5 | * are made available under the terms of the Apache License v2.0 6 | * which accompanies this distribution. 7 | * 8 | * The Apache License v2.0 is available at 9 | * http://www.opensource.org/licenses/apache2.0.php 10 | * 11 | * You may elect to redistribute this code under either of these licenses. 12 | */ 13 | package com.romanpierson.vertx.web.accesslogger.configuration.element.impl; 14 | 15 | 16 | import static com.romanpierson.vertx.web.accesslogger.configuration.pattern.PatternResolver.extractBestPositionFromPostfixPatternIfApplicable; 17 | 18 | import java.util.ArrayList; 19 | import java.util.Collection; 20 | 21 | import com.romanpierson.vertx.web.accesslogger.configuration.element.AccessLogElement; 22 | import com.romanpierson.vertx.web.accesslogger.configuration.pattern.ExtractedPosition; 23 | 24 | import io.vertx.core.json.JsonObject; 25 | 26 | public class EnvironmentValueElement implements AccessLogElement{ 27 | 28 | private String value; 29 | 30 | public static EnvironmentValueElement of(final String value) { 31 | 32 | EnvironmentValueElement element = new EnvironmentValueElement(); 33 | element.value = System.getenv(value); 34 | 35 | return element; 36 | } 37 | 38 | @Override 39 | public Collection findInRawPatternInternal(final String rawPattern) { 40 | 41 | Collection foundPositions = new ArrayList<>(1); 42 | 43 | extractBestPositionFromPostfixPatternIfApplicable(rawPattern, "env", 44 | json -> EnvironmentValueElement.of(json.getString("configuration"))) 45 | .ifPresent(foundPositions::add); 46 | 47 | return foundPositions; 48 | } 49 | 50 | @Override 51 | public Object getNativeValue(final JsonObject values) { 52 | 53 | return this.value; 54 | 55 | } 56 | 57 | 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/com/romanpierson/vertx/web/accesslogger/configuration/element/impl/HeaderElement.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016-2025 Roman Pierson 3 | * ------------------------------------------------------ 4 | * All rights reserved. This program and the accompanying materials 5 | * are made available under the terms of the Apache License v2.0 6 | * which accompanies this distribution. 7 | * 8 | * The Apache License v2.0 is available at 9 | * http://www.opensource.org/licenses/apache2.0.php 10 | * 11 | * You may elect to redistribute this code under either of these licenses. 12 | */ 13 | package com.romanpierson.vertx.web.accesslogger.configuration.element.impl; 14 | 15 | import java.util.ArrayList; 16 | import java.util.Collection; 17 | import java.util.Collections; 18 | 19 | import com.romanpierson.vertx.web.accesslogger.AccessLoggerConstants.Request.Data; 20 | import com.romanpierson.vertx.web.accesslogger.configuration.element.AccessLogElement; 21 | import com.romanpierson.vertx.web.accesslogger.configuration.pattern.ExtractedPosition; 22 | import static com.romanpierson.vertx.web.accesslogger.configuration.pattern.PatternResolver.extractBestPositionFromPostfixPatternIfApplicable; 23 | 24 | import io.vertx.core.json.JsonObject; 25 | 26 | public class HeaderElement implements AccessLogElement{ 27 | 28 | private Mode mode; 29 | private String identifier; 30 | 31 | private enum Mode{ 32 | 33 | INCOMING, 34 | OUTGOING 35 | 36 | } 37 | 38 | public static HeaderElement of(final Mode mode, final String identifier) { 39 | 40 | HeaderElement element = new HeaderElement(); 41 | element.mode = mode; 42 | element.identifier = identifier.toLowerCase(); 43 | 44 | return element; 45 | } 46 | 47 | @Override 48 | public Collection findInRawPatternInternal(final String rawPattern) { 49 | 50 | Collection foundPositions = new ArrayList<>(2); 51 | 52 | extractBestPositionFromPostfixPatternIfApplicable(rawPattern, "i", 53 | json -> HeaderElement.of(Mode.INCOMING, json.getString("configuration"))) 54 | .ifPresent(foundPositions::add); 55 | 56 | extractBestPositionFromPostfixPatternIfApplicable(rawPattern, "o", 57 | json -> HeaderElement.of(Mode.OUTGOING, json.getString("configuration"))) 58 | .ifPresent(foundPositions::add); 59 | 60 | return foundPositions; 61 | } 62 | 63 | @Override 64 | public Object getNativeValue(final JsonObject values) { 65 | 66 | final JsonObject headers = Mode.INCOMING.equals(this.mode) ? values.getJsonObject(Data.Type.REQUEST_HEADERS.getFieldName()) : values.getJsonObject(Data.Type.RESPONSE_HEADERS.getFieldName()); 67 | 68 | return headers.getString(this.identifier, null); 69 | 70 | } 71 | 72 | @Override 73 | public Collection claimDataParts() { 74 | 75 | return Mode.INCOMING.equals(this.mode) ? Collections.singleton(Data.Type.REQUEST_HEADERS) : Collections.singleton(Data.Type.RESPONSE_HEADERS); 76 | 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /src/main/java/com/romanpierson/vertx/web/accesslogger/configuration/element/impl/HostElement.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016-2025 Roman Pierson 3 | * ------------------------------------------------------ 4 | * All rights reserved. This program and the accompanying materials 5 | * are made available under the terms of the Apache License v2.0 6 | * which accompanies this distribution. 7 | * 8 | * The Apache License v2.0 is available at 9 | * http://www.opensource.org/licenses/apache2.0.php 10 | * 11 | * You may elect to redistribute this code under either of these licenses. 12 | */ 13 | package com.romanpierson.vertx.web.accesslogger.configuration.element.impl; 14 | 15 | import java.util.ArrayList; 16 | import java.util.Collection; 17 | import java.util.Objects; 18 | 19 | import com.romanpierson.vertx.web.accesslogger.AccessLoggerConstants.Request.Data; 20 | import com.romanpierson.vertx.web.accesslogger.configuration.element.AccessLogElement; 21 | import com.romanpierson.vertx.web.accesslogger.configuration.pattern.ExtractedPosition; 22 | import com.romanpierson.vertx.web.accesslogger.util.FormatUtility; 23 | 24 | import static com.romanpierson.vertx.web.accesslogger.configuration.pattern.PatternResolver.extractBestPositionFromFixPatternIfApplicable; 25 | 26 | import io.vertx.core.json.JsonObject; 27 | 28 | public class HostElement implements AccessLogElement{ 29 | 30 | private Mode mode; 31 | 32 | private enum Mode{ 33 | 34 | REMOTE_HOST, 35 | LOCAL_HOST, 36 | LOCAL_PORT 37 | 38 | } 39 | 40 | public static HostElement of(final Mode mode){ 41 | 42 | Objects.requireNonNull(mode, "mode must not be null"); 43 | 44 | HostElement element = new HostElement(); 45 | 46 | element.mode = mode; 47 | 48 | return element; 49 | 50 | } 51 | 52 | @Override 53 | public Collection findInRawPatternInternal(final String rawPattern) { 54 | 55 | Collection foundPositions = new ArrayList<>(3); 56 | 57 | extractBestPositionFromFixPatternIfApplicable(rawPattern, "%h", () -> HostElement.of(Mode.REMOTE_HOST)).ifPresent(foundPositions::add); 58 | extractBestPositionFromFixPatternIfApplicable(rawPattern, "%v", () -> HostElement.of(Mode.LOCAL_HOST)).ifPresent(foundPositions::add); 59 | extractBestPositionFromFixPatternIfApplicable(rawPattern, "%p", () -> HostElement.of(Mode.LOCAL_PORT)).ifPresent(foundPositions::add); 60 | 61 | return foundPositions; 62 | } 63 | 64 | @Override 65 | public Object getNativeValue(final JsonObject values) { 66 | 67 | switch (this.mode){ 68 | 69 | case LOCAL_HOST: 70 | return FormatUtility.getStringOrNull(values, Data.Type.LOCAL_HOST.getFieldName()); 71 | case LOCAL_PORT: 72 | return FormatUtility.getIntegerOrNull(values, Data.Type.LOCAL_PORT.getFieldName()); 73 | case REMOTE_HOST: 74 | return FormatUtility.getStringOrNull(values, Data.Type.REMOTE_HOST.getFieldName()); 75 | default: 76 | throw new IllegalStateException(String.format("mode %s not supported", this.mode.name())); 77 | 78 | } 79 | } 80 | 81 | 82 | } 83 | -------------------------------------------------------------------------------- /src/main/java/com/romanpierson/vertx/web/accesslogger/configuration/element/impl/MethodElement.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016-2025 Roman Pierson 3 | * ------------------------------------------------------ 4 | * All rights reserved. This program and the accompanying materials 5 | * are made available under the terms of the Apache License v2.0 6 | * which accompanies this distribution. 7 | * 8 | * The Apache License v2.0 is available at 9 | * http://www.opensource.org/licenses/apache2.0.php 10 | * 11 | * You may elect to redistribute this code under either of these licenses. 12 | */ 13 | package com.romanpierson.vertx.web.accesslogger.configuration.element.impl; 14 | 15 | import java.util.ArrayList; 16 | import java.util.Arrays; 17 | import java.util.Collection; 18 | 19 | import com.romanpierson.vertx.web.accesslogger.AccessLoggerConstants.Request.Data; 20 | import com.romanpierson.vertx.web.accesslogger.configuration.element.AccessLogElement; 21 | import com.romanpierson.vertx.web.accesslogger.configuration.pattern.ExtractedPosition; 22 | import com.romanpierson.vertx.web.accesslogger.util.FormatUtility; 23 | 24 | import static com.romanpierson.vertx.web.accesslogger.configuration.pattern.PatternResolver.extractBestPositionFromFixPatternsIfApplicable; 25 | 26 | import io.vertx.core.json.JsonObject; 27 | 28 | public class MethodElement implements AccessLogElement{ 29 | 30 | 31 | @Override 32 | public Collection findInRawPatternInternal(final String rawPattern) { 33 | 34 | Collection foundPositions = new ArrayList<>(2); 35 | 36 | extractBestPositionFromFixPatternsIfApplicable(rawPattern, Arrays.asList("cs-method", "%m"), MethodElement::new).ifPresent(foundPositions::addAll); 37 | 38 | return foundPositions; 39 | 40 | } 41 | 42 | @Override 43 | public Object getNativeValue(final JsonObject values) { 44 | 45 | return FormatUtility.getStringOrNull(values, Data.Type.METHOD.getFieldName()); 46 | 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/com/romanpierson/vertx/web/accesslogger/configuration/element/impl/RequestElement.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016-2025 Roman Pierson 3 | * ------------------------------------------------------ 4 | * All rights reserved. This program and the accompanying materials 5 | * are made available under the terms of the Apache License v2.0 6 | * which accompanies this distribution. 7 | * 8 | * The Apache License v2.0 is available at 9 | * http://www.opensource.org/licenses/apache2.0.php 10 | * 11 | * You may elect to redistribute this code under either of these licenses. 12 | */ 13 | package com.romanpierson.vertx.web.accesslogger.configuration.element.impl; 14 | 15 | import java.util.ArrayList; 16 | import java.util.Arrays; 17 | import java.util.Collection; 18 | 19 | import com.romanpierson.vertx.web.accesslogger.AccessLoggerConstants.Request.Data; 20 | import com.romanpierson.vertx.web.accesslogger.configuration.element.AccessLogElement; 21 | import com.romanpierson.vertx.web.accesslogger.configuration.pattern.ExtractedPosition; 22 | import static com.romanpierson.vertx.web.accesslogger.configuration.pattern.PatternResolver.extractBestPositionFromFixPatternsIfApplicable; 23 | import static com.romanpierson.vertx.web.accesslogger.configuration.pattern.PatternResolver.extractBestPositionFromFixPatternIfApplicable; 24 | import com.romanpierson.vertx.web.accesslogger.util.VersionUtility; 25 | 26 | import io.vertx.core.json.JsonObject; 27 | 28 | public class RequestElement implements AccessLogElement{ 29 | 30 | private RequestLogMode requestLogMode; 31 | 32 | private enum RequestLogMode{ 33 | 34 | APACHE_FIRST_REQUEST_LINE, // Identical to %m %U%q %H" will log the method, path, query-string, and protocol 35 | URI, 36 | QUERY_STRING, 37 | URI_QUERY 38 | 39 | } 40 | 41 | public static RequestElement of(final RequestLogMode requestLogMode) { 42 | 43 | RequestElement element = new RequestElement(); 44 | element.requestLogMode = requestLogMode; 45 | 46 | return element; 47 | } 48 | 49 | @Override 50 | public Collection findInRawPatternInternal(final String rawPattern) { 51 | 52 | Collection foundPositions = new ArrayList<>(6); 53 | 54 | extractBestPositionFromFixPatternIfApplicable(rawPattern, "%r", () -> RequestElement.of(RequestLogMode.APACHE_FIRST_REQUEST_LINE)).ifPresent(foundPositions::add); 55 | extractBestPositionFromFixPatternsIfApplicable(rawPattern, Arrays.asList("%U", "cs-uri-stem"), () -> RequestElement.of(RequestLogMode.URI)).ifPresent(foundPositions::addAll); 56 | extractBestPositionFromFixPatternsIfApplicable(rawPattern, Arrays.asList("%q", "cs-uri-query"), () -> RequestElement.of(RequestLogMode.QUERY_STRING)).ifPresent(foundPositions::addAll); 57 | extractBestPositionFromFixPatternIfApplicable(rawPattern, "cs-uri", () -> RequestElement.of(RequestLogMode.URI_QUERY)).ifPresent(foundPositions::add); 58 | 59 | return foundPositions; 60 | } 61 | 62 | 63 | @Override 64 | public Object getNativeValue(JsonObject values) { 65 | 66 | final StringBuilder sb = new StringBuilder(); 67 | 68 | if(RequestLogMode.APACHE_FIRST_REQUEST_LINE.equals(this.requestLogMode)){ 69 | 70 | sb.append(values.getString(Data.Type.METHOD.getFieldName())).append(' '); 71 | 72 | } 73 | 74 | if(!RequestLogMode.QUERY_STRING.equals(this.requestLogMode)){ 75 | sb.append(values.getString(Data.Type.URI.getFieldName())); 76 | } 77 | 78 | if(!RequestLogMode.URI.equals(this.requestLogMode) 79 | && values.getString(Data.Type.QUERY.getFieldName(), null) != null){ 80 | sb.append('?').append(values.getString(Data.Type.QUERY.getFieldName(), null)); 81 | } 82 | 83 | 84 | if(RequestLogMode.APACHE_FIRST_REQUEST_LINE.equals(this.requestLogMode)){ 85 | 86 | sb.append(' ').append(VersionUtility.getFormattedValue(values)); 87 | 88 | } 89 | 90 | return sb.toString(); 91 | } 92 | 93 | 94 | 95 | } 96 | -------------------------------------------------------------------------------- /src/main/java/com/romanpierson/vertx/web/accesslogger/configuration/element/impl/StaticValueElement.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016-2025 Roman Pierson 3 | * ------------------------------------------------------ 4 | * All rights reserved. This program and the accompanying materials 5 | * are made available under the terms of the Apache License v2.0 6 | * which accompanies this distribution. 7 | * 8 | * The Apache License v2.0 is available at 9 | * http://www.opensource.org/licenses/apache2.0.php 10 | * 11 | * You may elect to redistribute this code under either of these licenses. 12 | */ 13 | package com.romanpierson.vertx.web.accesslogger.configuration.element.impl; 14 | 15 | import static com.romanpierson.vertx.web.accesslogger.configuration.pattern.PatternResolver.extractBestPositionFromPostfixPatternIfApplicable; 16 | 17 | import java.util.ArrayList; 18 | import java.util.Collection; 19 | 20 | import com.romanpierson.vertx.web.accesslogger.configuration.element.AccessLogElement; 21 | import com.romanpierson.vertx.web.accesslogger.configuration.pattern.ExtractedPosition; 22 | 23 | import io.vertx.core.json.JsonObject; 24 | 25 | public class StaticValueElement implements AccessLogElement{ 26 | 27 | private String value; 28 | 29 | public static StaticValueElement of(final String value) { 30 | 31 | StaticValueElement element = new StaticValueElement(); 32 | element.value = value; 33 | 34 | return element; 35 | } 36 | 37 | @Override 38 | public Collection findInRawPatternInternal(final String rawPattern) { 39 | 40 | Collection foundPositions = new ArrayList<>(1); 41 | 42 | extractBestPositionFromPostfixPatternIfApplicable(rawPattern, "static", 43 | json -> StaticValueElement.of(json.getString("configuration"))) 44 | .ifPresent(foundPositions::add); 45 | 46 | return foundPositions; 47 | } 48 | 49 | @Override 50 | public Object getNativeValue(final JsonObject values) { 51 | 52 | return this.value; 53 | 54 | } 55 | 56 | 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/com/romanpierson/vertx/web/accesslogger/configuration/element/impl/StatusElement.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016-2025 Roman Pierson 3 | * ------------------------------------------------------ 4 | * All rights reserved. This program and the accompanying materials 5 | * are made available under the terms of the Apache License v2.0 6 | * which accompanies this distribution. 7 | * 8 | * The Apache License v2.0 is available at 9 | * http://www.opensource.org/licenses/apache2.0.php 10 | * 11 | * You may elect to redistribute this code under either of these licenses. 12 | */ 13 | package com.romanpierson.vertx.web.accesslogger.configuration.element.impl; 14 | 15 | import java.util.ArrayList; 16 | import java.util.Arrays; 17 | import java.util.Collection; 18 | 19 | import com.romanpierson.vertx.web.accesslogger.AccessLoggerConstants.Request.Data; 20 | import com.romanpierson.vertx.web.accesslogger.configuration.element.AccessLogElement; 21 | import com.romanpierson.vertx.web.accesslogger.configuration.pattern.ExtractedPosition; 22 | import static com.romanpierson.vertx.web.accesslogger.configuration.pattern.PatternResolver.extractBestPositionFromFixPatternsIfApplicable; 23 | 24 | import io.vertx.core.json.JsonObject; 25 | 26 | public class StatusElement implements AccessLogElement{ 27 | 28 | 29 | @Override 30 | public Collection findInRawPatternInternal(final String rawPattern) { 31 | 32 | Collection foundPositions = new ArrayList<>(2); 33 | 34 | extractBestPositionFromFixPatternsIfApplicable(rawPattern, Arrays.asList("sc-status", "%s"), StatusElement::new).ifPresent(foundPositions::addAll); 35 | 36 | return foundPositions; 37 | 38 | } 39 | 40 | @Override 41 | public Object getNativeValue(final JsonObject values) { 42 | 43 | return values.getInteger(Data.Type.STATUS.getFieldName()); 44 | 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/com/romanpierson/vertx/web/accesslogger/configuration/element/impl/VersionElement.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016-2025 Roman Pierson 3 | * ------------------------------------------------------ 4 | * All rights reserved. This program and the accompanying materials 5 | * are made available under the terms of the Apache License v2.0 6 | * which accompanies this distribution. 7 | * 8 | * The Apache License v2.0 is available at 9 | * http://www.opensource.org/licenses/apache2.0.php 10 | * 11 | * You may elect to redistribute this code under either of these licenses. 12 | */ 13 | package com.romanpierson.vertx.web.accesslogger.configuration.element.impl; 14 | 15 | import java.util.ArrayList; 16 | import java.util.Collection; 17 | 18 | import com.romanpierson.vertx.web.accesslogger.configuration.element.AccessLogElement; 19 | import com.romanpierson.vertx.web.accesslogger.configuration.pattern.ExtractedPosition; 20 | import static com.romanpierson.vertx.web.accesslogger.configuration.pattern.PatternResolver.extractBestPositionFromFixPatternIfApplicable; 21 | import com.romanpierson.vertx.web.accesslogger.util.VersionUtility; 22 | 23 | import io.vertx.core.json.JsonObject; 24 | 25 | public class VersionElement implements AccessLogElement{ 26 | 27 | @Override 28 | public Collection findInRawPatternInternal(final String rawPattern) { 29 | 30 | Collection foundPositions = new ArrayList<>(1); 31 | 32 | extractBestPositionFromFixPatternIfApplicable(rawPattern, "%H", VersionElement::new).ifPresent(foundPositions::add); 33 | 34 | return foundPositions; 35 | 36 | } 37 | 38 | @Override 39 | public Object getNativeValue(final JsonObject values) { 40 | 41 | return VersionUtility.getFormattedValue(values); 42 | 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/com/romanpierson/vertx/web/accesslogger/configuration/pattern/ExtractedPosition.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016-2025 Roman Pierson 3 | * ------------------------------------------------------ 4 | * All rights reserved. This program and the accompanying materials 5 | * are made available under the terms of the Apache License v2.0 6 | * which accompanies this distribution. 7 | * 8 | * The Apache License v2.0 is available at 9 | * http://www.opensource.org/licenses/apache2.0.php 10 | * 11 | * You may elect to redistribute this code under either of these licenses. 12 | */ 13 | package com.romanpierson.vertx.web.accesslogger.configuration.pattern; 14 | 15 | import java.util.function.Function; 16 | import java.util.function.Supplier; 17 | 18 | import com.romanpierson.vertx.web.accesslogger.configuration.element.AccessLogElement; 19 | 20 | import io.vertx.core.json.JsonObject; 21 | 22 | public class ExtractedPosition { 23 | 24 | private final int start; 25 | private final int offset; 26 | private final Supplier elementSupplier; 27 | private final Function elementFunction; 28 | private final JsonObject config; 29 | 30 | public static ExtractedPosition build(final int start, final int offset, 31 | final Supplier elementSupplier) 32 | { 33 | 34 | return new ExtractedPosition(start, offset, elementSupplier, null, null); 35 | 36 | } 37 | 38 | public static ExtractedPosition build(final int start, final int offset, 39 | final Function elementFunction, JsonObject config) 40 | { 41 | 42 | return new ExtractedPosition(start, offset, null, elementFunction, config); 43 | 44 | } 45 | 46 | private ExtractedPosition(final int start, final int offset, 47 | final Supplier elementSupplier, 48 | final Function elementFunction, 49 | JsonObject config) { 50 | 51 | super(); 52 | this.start = start; 53 | this.offset = offset; 54 | this.elementSupplier = elementSupplier; 55 | this.elementFunction = elementFunction; 56 | this.config = config; 57 | } 58 | 59 | public int getStart() { 60 | return start; 61 | } 62 | 63 | public int getOffset() { 64 | return offset; 65 | } 66 | 67 | public Supplier getElementSupplier() { 68 | return elementSupplier; 69 | } 70 | 71 | public Function getElementFunction(){ 72 | return elementFunction; 73 | } 74 | 75 | public JsonObject getConfig() { 76 | return config; 77 | } 78 | 79 | public boolean isCustom() { 80 | 81 | if(elementSupplier != null) { 82 | 83 | return !elementSupplier.get().getClass().getName().startsWith("com.romanpierson.vertx.web.accesslogger.configuration.element"); 84 | 85 | }else if(elementFunction != null && config != null) { 86 | 87 | return !elementFunction.apply(config).getClass().getName().startsWith("com.romanpierson.vertx.web.accesslogger.configuration.element"); 88 | 89 | }else { 90 | throw new IllegalStateException("Extracted Position must either have element supplier or function"); 91 | } 92 | 93 | } 94 | 95 | } 96 | -------------------------------------------------------------------------------- /src/main/java/com/romanpierson/vertx/web/accesslogger/configuration/pattern/PatternResolver.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016-2025 Roman Pierson 3 | * ------------------------------------------------------ 4 | * All rights reserved. This program and the accompanying materials 5 | * are made available under the terms of the Apache License v2.0 6 | * which accompanies this distribution. 7 | * 8 | * The Apache License v2.0 is available at 9 | * http://www.opensource.org/licenses/apache2.0.php 10 | * 11 | * You may elect to redistribute this code under either of these licenses. 12 | */ 13 | package com.romanpierson.vertx.web.accesslogger.configuration.pattern; 14 | 15 | import java.util.ArrayList; 16 | import java.util.Collection; 17 | import java.util.Optional; 18 | import java.util.ServiceLoader; 19 | import java.util.function.Function; 20 | import java.util.function.Supplier; 21 | 22 | import com.romanpierson.vertx.web.accesslogger.configuration.element.AccessLogElement; 23 | 24 | import io.vertx.core.internal.logging.Logger; 25 | import io.vertx.core.internal.logging.LoggerFactory; 26 | import io.vertx.core.json.JsonObject; 27 | 28 | public class PatternResolver { 29 | 30 | private final Logger logger = LoggerFactory.getLogger(this.getClass().getName()); 31 | 32 | final Collection availableElements = new ArrayList<>(); 33 | 34 | public PatternResolver() { 35 | 36 | ServiceLoader loader = ServiceLoader.load(AccessLogElement.class); 37 | 38 | loader.forEach(availableElements::add); 39 | 40 | } 41 | 42 | public ResolvedPatternResult resolvePattern(final String rawPattern){ 43 | 44 | String rawPatternInEvaluation = rawPattern; 45 | final StringBuilder sbEvaluatedPattern = new StringBuilder(); 46 | final Collection logElements = new ArrayList<>(); 47 | 48 | while(rawPatternInEvaluation != null && rawPatternInEvaluation.length() > 0){ 49 | 50 | ExtractedPosition currentBestMatchingElement = null; 51 | 52 | for(final AccessLogElement element : availableElements){ 53 | 54 | final Collection matchingElements = element.findInRawPattern(rawPatternInEvaluation); 55 | 56 | for(final ExtractedPosition matchingElement : matchingElements) { 57 | 58 | if (currentBestMatchingElement == null) { 59 | 60 | // First element that matches at all - take it 61 | currentBestMatchingElement = matchingElement; 62 | 63 | } else { 64 | // There is already a current best matching element so we need to check if this one is "better" 65 | if(matchingElement.getStart() < currentBestMatchingElement.getStart()) { 66 | 67 | // The new element starts earlier so should be a better match 68 | currentBestMatchingElement = matchingElement; 69 | 70 | } else if(matchingElement.getStart() == currentBestMatchingElement.getStart()) { 71 | 72 | // Both starts at same 73 | if(matchingElement.getOffset() > currentBestMatchingElement.getOffset()) { 74 | 75 | // Trying to retain some of the logic 76 | // If the start position is equal then we must have a duplicate pattern use the item with the shorter length 77 | // Basically we prefer shorter offset as it seems more precise 78 | 79 | } else if (matchingElement.getOffset() == currentBestMatchingElement.getOffset()) { 80 | 81 | // All identical - we give priority to non default elements to allow overwriting same pattern with custom elements 82 | if(matchingElement.isCustom() && !currentBestMatchingElement.isCustom()) { 83 | 84 | currentBestMatchingElement = matchingElement; 85 | 86 | } else if ((matchingElement.isCustom() && currentBestMatchingElement.isCustom()) || (!matchingElement.isCustom() && !currentBestMatchingElement.isCustom())) { 87 | 88 | // warn as both are same 89 | logger.warn("Found two elements that have identical match - first found will be used"); 90 | } 91 | 92 | // If we get here means we got a no custom element and already custom element is set - so we can ignore 93 | } 94 | } 95 | 96 | } 97 | } 98 | } 99 | 100 | if(currentBestMatchingElement != null){ 101 | 102 | if(currentBestMatchingElement.getStart() > 0){ 103 | // We need to take over some untranslatable part first 104 | sbEvaluatedPattern.append(rawPatternInEvaluation.substring(0, currentBestMatchingElement.getStart())); 105 | } 106 | 107 | // Shorten the raw pattern till where we found replacement 108 | rawPatternInEvaluation = rawPatternInEvaluation.substring(currentBestMatchingElement.getStart() + currentBestMatchingElement.getOffset()); 109 | 110 | // Add the placeholder - for now always type string 111 | sbEvaluatedPattern.append("%s"); 112 | 113 | // Add the log element 114 | if(currentBestMatchingElement.getElementFunction() != null) { 115 | logElements.add(currentBestMatchingElement.getElementFunction().apply(currentBestMatchingElement.getConfig())); 116 | }else { 117 | logElements.add(currentBestMatchingElement.getElementSupplier().get()); 118 | } 119 | 120 | } else { 121 | // Looks like no more that can be resolved 122 | 123 | if(rawPatternInEvaluation != null && rawPatternInEvaluation.length() > 0){ 124 | sbEvaluatedPattern.append(rawPatternInEvaluation); 125 | } 126 | 127 | break; 128 | } 129 | 130 | } 131 | 132 | return new ResolvedPatternResult(sbEvaluatedPattern.toString(), logElements); 133 | } 134 | 135 | public static Optional extractBestPositionFromPostfixPatternIfApplicable( 136 | final String rawPattern, 137 | final String postfixPattern, 138 | final Function logElementFunction) { 139 | 140 | return extractBestPositionFromPostfixPatternAndAdditionalCheckIfApplicable(rawPattern, postfixPattern, 141 | json -> Boolean.TRUE, 142 | logElementFunction); 143 | 144 | } 145 | 146 | public static Optional extractBestPositionFromPostfixPatternAndAdditionalCheckIfApplicable( 147 | final String rawPattern, 148 | final String postfixPattern, 149 | final Function matchElementFunction, 150 | final Function logElementFunction) { 151 | 152 | final int index = rawPattern.indexOf("%{"); 153 | 154 | if(index >= 0){ 155 | 156 | int indexEndConfiguration = rawPattern.indexOf('}'); 157 | 158 | if(indexEndConfiguration > index 159 | && rawPattern.length() > indexEndConfiguration 160 | && (rawPattern.substring(indexEndConfiguration + 1).startsWith(postfixPattern))) 161 | { 162 | String configurationString = rawPattern.substring(index + 2, indexEndConfiguration); 163 | 164 | final JsonObject configurationJson = new JsonObject(); 165 | configurationJson.put("configuration", configurationString); 166 | 167 | // Check if the potential element is able to support the provided configuration 168 | 169 | if(Boolean.TRUE.equals(matchElementFunction.apply(configurationJson))){ 170 | 171 | return Optional.of( 172 | ExtractedPosition.build( 173 | index, 174 | configurationString.length() + 3 + postfixPattern.length(), 175 | logElementFunction, 176 | configurationJson 177 | )); 178 | 179 | } 180 | } 181 | } 182 | 183 | return Optional.empty(); 184 | 185 | } 186 | 187 | public static Optional extractBestPositionFromFixPatternIfApplicable(final String rawPattern, final String fixPattern, Supplier logElementSupplier) { 188 | 189 | int index = rawPattern.indexOf(fixPattern); 190 | 191 | if(index >= 0){ 192 | return Optional.of(ExtractedPosition.build(index, fixPattern.length(), logElementSupplier)); 193 | } 194 | 195 | return Optional.empty(); 196 | 197 | } 198 | 199 | public static Optional> extractBestPositionFromFixPatternsIfApplicable(final String rawPattern, final Collection fixPatterns, Supplier logElementSupplier) { 200 | 201 | final Collection foundPositions = new ArrayList<>(fixPatterns.size()); 202 | 203 | fixPatterns.forEach(fixPattern -> { 204 | 205 | int index = rawPattern.indexOf(fixPattern); 206 | 207 | if(index >= 0){ 208 | foundPositions.add(ExtractedPosition.build(index, fixPattern.length(), logElementSupplier)); 209 | } 210 | }); 211 | 212 | return foundPositions.isEmpty() ? Optional.empty() : Optional.of(foundPositions); 213 | 214 | } 215 | 216 | } 217 | -------------------------------------------------------------------------------- /src/main/java/com/romanpierson/vertx/web/accesslogger/configuration/pattern/ResolvedPatternResult.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016-2025 Roman Pierson 3 | * ------------------------------------------------------ 4 | * All rights reserved. This program and the accompanying materials 5 | * are made available under the terms of the Apache License v2.0 6 | * which accompanies this distribution. 7 | * 8 | * The Apache License v2.0 is available at 9 | * http://www.opensource.org/licenses/apache2.0.php 10 | * 11 | * You may elect to redistribute this code under either of these licenses. 12 | */ 13 | package com.romanpierson.vertx.web.accesslogger.configuration.pattern; 14 | 15 | import java.util.Collection; 16 | 17 | import com.romanpierson.vertx.web.accesslogger.configuration.element.AccessLogElement; 18 | 19 | public class ResolvedPatternResult { 20 | 21 | private final String resolvedPattern; 22 | private final Collection logElements; 23 | 24 | public ResolvedPatternResult(String resolvedPattern, Collection logElements) { 25 | super(); 26 | this.resolvedPattern = resolvedPattern; 27 | this.logElements = logElements; 28 | } 29 | 30 | public String getResolvedPattern() { 31 | return resolvedPattern; 32 | } 33 | 34 | public Collection getLogElements() { 35 | return logElements; 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/com/romanpierson/vertx/web/accesslogger/exception/AccessLoggerException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016-2025 Roman Pierson 3 | * ------------------------------------------------------ 4 | * All rights reserved. This program and the accompanying materials 5 | * are made available under the terms of the Apache License v2.0 6 | * which accompanies this distribution. 7 | * 8 | * The Apache License v2.0 is available at 9 | * http://www.opensource.org/licenses/apache2.0.php 10 | * 11 | * You may elect to redistribute this code under either of these licenses. 12 | */ 13 | package com.romanpierson.vertx.web.accesslogger.exception; 14 | 15 | public class AccessLoggerException extends RuntimeException { 16 | 17 | private static final long serialVersionUID = 1L; 18 | 19 | public AccessLoggerException(String message, Throwable cause) { 20 | super(message, cause); 21 | } 22 | 23 | public AccessLoggerException(String message) { 24 | super(message); 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/romanpierson/vertx/web/accesslogger/impl/AccessLoggerHandlerImpl.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016-2025 Roman Pierson 3 | * ------------------------------------------------------ 4 | * All rights reserved. This program and the accompanying materials 5 | * are made available under the terms of the Apache License v2.0 6 | * which accompanies this distribution. 7 | * 8 | * The Apache License v2.0 is available at 9 | * http://www.opensource.org/licenses/apache2.0.php 10 | * 11 | * You may elect to redistribute this code under either of these licenses. 12 | */ 13 | package com.romanpierson.vertx.web.accesslogger.impl; 14 | 15 | import java.util.ArrayList; 16 | import java.util.Collection; 17 | import java.util.Set; 18 | 19 | import com.romanpierson.vertx.web.accesslogger.AccessLoggerConstants; 20 | import com.romanpierson.vertx.web.accesslogger.AccessLoggerHandler; 21 | import com.romanpierson.vertx.web.accesslogger.AccessLoggerConstants.HandlerConfiguration; 22 | import com.romanpierson.vertx.web.accesslogger.AccessLoggerConstants.Messages.RawEvent; 23 | import com.romanpierson.vertx.web.accesslogger.AccessLoggerConstants.Messages.Registration; 24 | import com.romanpierson.vertx.web.accesslogger.AccessLoggerConstants.Request.Data; 25 | import com.romanpierson.vertx.web.accesslogger.exception.AccessLoggerException; 26 | import com.romanpierson.vertx.web.accesslogger.verticle.AccessLoggerProducerVerticle; 27 | 28 | import io.vertx.core.DeploymentOptions; 29 | import io.vertx.core.MultiMap; 30 | import io.vertx.core.ThreadingModel; 31 | import io.vertx.core.Vertx; 32 | import io.vertx.core.eventbus.EventBus; 33 | import io.vertx.core.http.Cookie; 34 | import io.vertx.core.http.HttpServerRequest; 35 | import io.vertx.core.http.HttpServerResponse; 36 | import io.vertx.core.internal.logging.Logger; 37 | import io.vertx.core.internal.logging.LoggerFactory; 38 | import io.vertx.core.json.JsonArray; 39 | import io.vertx.core.json.JsonObject; 40 | import io.vertx.ext.web.RoutingContext; 41 | 42 | /** 43 | * 44 | * Access Logger for requests 45 | * 46 | * @author Roman Pierson 47 | * 48 | */ 49 | public class AccessLoggerHandlerImpl implements AccessLoggerHandler { 50 | 51 | private final Logger logger = LoggerFactory.getLogger(this.getClass().getName()); 52 | 53 | private final EventBus eventBus; 54 | 55 | private Collection registeredIdentifiers = new ArrayList<>(); 56 | private boolean allConfigurationsSuccessfullyRegistered = false; 57 | private final int requiredConfigurationsCounter; 58 | 59 | private boolean requiresIncomingHeaders; 60 | private boolean requiresOutgoingHeaders; 61 | private boolean requiresCookies; 62 | 63 | private static final Object lock = new Object(); 64 | private static boolean isProducerVerticleCreated = false; 65 | 66 | public AccessLoggerHandlerImpl(final JsonObject handlerConfiguration) { 67 | 68 | if(handlerConfiguration == null || handlerConfiguration.getJsonArray(HandlerConfiguration.CONFIG_KEY_CONFIGURATIONS, new JsonArray()).size() <= 0){ 69 | throw new IllegalArgumentException("must specify at least one valid configuration"); 70 | } 71 | 72 | this.requiredConfigurationsCounter = handlerConfiguration.getJsonArray(HandlerConfiguration.CONFIG_KEY_CONFIGURATIONS).size(); 73 | 74 | eventBus = Vertx.currentContext().owner().eventBus(); 75 | 76 | if(Boolean.TRUE.equals(handlerConfiguration.getBoolean(HandlerConfiguration.CONFIG_KEY_IS_AUTO_DEPLOY_PRODUCER_VERTICLE, true))) { 77 | 78 | synchronized(lock) { 79 | if(!isProducerVerticleCreated) { 80 | 81 | logger.info("Start creating singleton verticle"); 82 | 83 | Vertx.currentContext().owner().deployVerticle(AccessLoggerProducerVerticle.class.getName(), new DeploymentOptions().setThreadingModel(ThreadingModel.WORKER)) 84 | .onComplete(ar -> { 85 | 86 | if(ar.succeeded()) { 87 | isProducerVerticleCreated = true; 88 | registerHandlers(handlerConfiguration); 89 | } else { 90 | throw new AccessLoggerException("Unable to deploy AccessLoggerProducerVerticle", ar.cause()); 91 | } 92 | 93 | }); 94 | 95 | } 96 | } 97 | } 98 | else { 99 | registerHandlers(handlerConfiguration); 100 | } 101 | 102 | 103 | } 104 | 105 | private void registerHandlers (final JsonObject handlerConfiguration) { 106 | 107 | handlerConfiguration.getJsonArray(HandlerConfiguration.CONFIG_KEY_CONFIGURATIONS).forEach(xConfiguration -> { 108 | 109 | if(!(xConfiguration instanceof JsonObject)) { 110 | throw new IllegalArgumentException("must specify a valid configuration"); 111 | } 112 | 113 | final JsonObject configuration = (JsonObject) xConfiguration; 114 | 115 | eventBus 116 | .request(AccessLoggerConstants.EVENTBUS_REGISTER_EVENT_NAME, configuration) 117 | .onComplete(ar -> { 118 | 119 | final String configurationIdentifier = configuration.getString(Registration.Request.IDENTIFIER); 120 | 121 | if(ar.succeeded()) { 122 | JsonObject response = ar.result().body(); 123 | if(Registration.Response.RESULT_OK.equals(response.getString(Registration.Response.RESULT, null))){ 124 | 125 | this.requiresCookies = response.getBoolean(Registration.Response.REQUIRES_COOKIES, false) ? true : this.requiresCookies; 126 | this.requiresIncomingHeaders = response.getBoolean(Registration.Response.REQUIRES_INCOMING_HEADERS, false) ? true : this.requiresIncomingHeaders; 127 | this.requiresOutgoingHeaders = response.getBoolean(Registration.Response.REQUIRES_OUTGOING_HEADERS, false) ? true : this.requiresOutgoingHeaders; 128 | 129 | this.registeredIdentifiers.add(configurationIdentifier); 130 | 131 | if(this.requiredConfigurationsCounter == this.registeredIdentifiers.size()) { 132 | this.allConfigurationsSuccessfullyRegistered = true; 133 | 134 | logger.debug("Successfully registered all [" + this.requiredConfigurationsCounter + "] configurations with identifiers " + this.registeredIdentifiers); 135 | 136 | if(this.requiresCookies || this.requiresIncomingHeaders || this.requiresOutgoingHeaders) { 137 | 138 | logger.debug("Specific data required for cookies [" + this.requiresCookies + "], incoming headers [" + this.requiresIncomingHeaders + "], outgoing headers [" + this.requiresOutgoingHeaders + "]"); 139 | 140 | } else { 141 | logger.debug("No specific data required"); 142 | } 143 | } 144 | 145 | } else { 146 | throw new AccessLoggerException("Unable to register access log configuration for identifier [" + configurationIdentifier + "]"); 147 | } 148 | 149 | } else { 150 | throw new AccessLoggerException("Unable to register access log configuration [" + configurationIdentifier + "]", ar.cause()); 151 | } 152 | }); 153 | }); 154 | 155 | } 156 | 157 | @Override 158 | public void handle(final RoutingContext context) { 159 | 160 | if(!allConfigurationsSuccessfullyRegistered) { 161 | logger.error("Handler not ready to log due to missing registration(s)"); 162 | context.next(); 163 | } 164 | 165 | long startTSmillis = System.currentTimeMillis(); 166 | 167 | context.addBodyEndHandler(v -> log(context, startTSmillis)); 168 | 169 | context.next(); 170 | 171 | } 172 | 173 | private void log(final RoutingContext context, long startTSmillis){ 174 | 175 | final HttpServerRequest request = context.request(); 176 | final HttpServerResponse response = context.response(); 177 | 178 | JsonObject jsonValues = new JsonObject() 179 | .put(RawEvent.Request.IDENTIFIERS, this.registeredIdentifiers) 180 | .put(Data.Type.START_TS_MILLIS.getFieldName(), startTSmillis) 181 | .put(Data.Type.END_TS_MILLIS.getFieldName(), System.currentTimeMillis()) 182 | .put(Data.Type.STATUS.getFieldName(), response.getStatusCode()) 183 | .put(Data.Type.METHOD.getFieldName(), request.method().name()) 184 | .put(Data.Type.URI.getFieldName(), request.path()) 185 | .put(Data.Type.VERSION.getFieldName(), request.version()) 186 | .put(Data.Type.REMOTE_HOST.getFieldName(), request.remoteAddress().host()) 187 | .put(Data.Type.LOCAL_HOST.getFieldName(), request.authority() == null ? null : request.authority().host()) 188 | .put(Data.Type.LOCAL_PORT.getFieldName(), request.localAddress().port()); 189 | 190 | if(request.query() != null && !request.query().trim().isEmpty()){ 191 | jsonValues.put(Data.Type.QUERY.getFieldName(), request.query()); 192 | } 193 | 194 | jsonValues.put(Data.Type.BYTES_SENT.getFieldName(), response.bytesWritten()); 195 | 196 | if(requiresIncomingHeaders) { 197 | jsonValues.put(Data.Type.REQUEST_HEADERS.getFieldName(), extractHeaders(request.headers())); 198 | } 199 | 200 | if(requiresOutgoingHeaders) { 201 | jsonValues.put(Data.Type.RESPONSE_HEADERS.getFieldName(), extractHeaders(response.headers())); 202 | } 203 | 204 | if(requiresCookies) { 205 | jsonValues.put(Data.Type.COOKIES.getFieldName(), extractCookies(context.request().cookies())); 206 | } 207 | 208 | eventBus.send(AccessLoggerConstants.EVENTBUS_RAW_EVENT_NAME, jsonValues); 209 | 210 | } 211 | 212 | private JsonObject extractHeaders(final MultiMap headersMap){ 213 | 214 | JsonObject headers = new JsonObject(); 215 | headersMap.forEach(entry -> headers.put(entry.getKey().toLowerCase(), entry.getValue())); 216 | 217 | return headers; 218 | 219 | } 220 | 221 | private JsonArray extractCookies(final Set cookies) { 222 | 223 | JsonArray jsonArCookies = new JsonArray(); 224 | 225 | for(final Cookie cookie : cookies) { 226 | jsonArCookies.add(new JsonObject().put(Data.Fields.COOKIE_NAME, cookie.getName()).put(Data.Fields.COOKIE_VALUE, cookie.getValue())); 227 | } 228 | 229 | return jsonArCookies; 230 | 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /src/main/java/com/romanpierson/vertx/web/accesslogger/util/FormatUtility.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016-2025 Roman Pierson 3 | * ------------------------------------------------------ 4 | * All rights reserved. This program and the accompanying materials 5 | * are made available under the terms of the Apache License v2.0 6 | * which accompanies this distribution. 7 | * 8 | * The Apache License v2.0 is available at 9 | * http://www.opensource.org/licenses/apache2.0.php 10 | * 11 | * You may elect to redistribute this code under either of these licenses. 12 | */ 13 | package com.romanpierson.vertx.web.accesslogger.util; 14 | 15 | import io.vertx.core.json.JsonObject; 16 | 17 | import java.util.List; 18 | 19 | import io.vertx.core.internal.logging.Logger; 20 | import io.vertx.core.internal.logging.LoggerFactory; 21 | 22 | public class FormatUtility { 23 | 24 | private final static Logger logger = LoggerFactory.getLogger(FormatUtility.class.getName()); 25 | 26 | private FormatUtility() {} 27 | 28 | public static String getStringOrNull(final JsonObject values, final String fieldName) { 29 | 30 | return values.getString(fieldName) != null ? values.getString(fieldName) : null; 31 | 32 | } 33 | 34 | public static String getIntegerOrNull(final JsonObject values, final String fieldName) { 35 | 36 | return values.getInteger(fieldName) != null ? Integer.toString(values.getInteger(fieldName)) : null; 37 | 38 | } 39 | 40 | public static Object[] getStringifiedParameterValues(final List rawValues){ 41 | 42 | final String[] parameterValues = new String[rawValues.size()]; 43 | 44 | int i = 0; 45 | for (final Object xValue : rawValues) { 46 | 47 | if(xValue == null){ 48 | parameterValues[i] = ""; 49 | } 50 | else if (xValue instanceof Long) { 51 | parameterValues[i] = Long.toString((Long) xValue); 52 | } else if (xValue instanceof Integer) { 53 | parameterValues[i] = Integer.toString((Integer) xValue); 54 | }else if (xValue instanceof String) { 55 | parameterValues[i] = (String) xValue; 56 | } else { 57 | logger.error("Unrecognized type" + xValue.getClass()); 58 | } 59 | 60 | i++; 61 | } 62 | 63 | return parameterValues; 64 | 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/com/romanpierson/vertx/web/accesslogger/util/VersionUtility.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016-2025 Roman Pierson 3 | * ------------------------------------------------------ 4 | * All rights reserved. This program and the accompanying materials 5 | * are made available under the terms of the Apache License v2.0 6 | * which accompanies this distribution. 7 | * 8 | * The Apache License v2.0 is available at 9 | * http://www.opensource.org/licenses/apache2.0.php 10 | * 11 | * You may elect to redistribute this code under either of these licenses. 12 | */ 13 | package com.romanpierson.vertx.web.accesslogger.util; 14 | 15 | import com.romanpierson.vertx.web.accesslogger.AccessLoggerConstants.Request.Data; 16 | 17 | import io.vertx.core.json.JsonObject; 18 | 19 | public class VersionUtility { 20 | 21 | private VersionUtility() {} 22 | 23 | public static String getFormattedValue(final JsonObject values) { 24 | 25 | final String version = values.getString(Data.Type.VERSION.getFieldName(), null); 26 | 27 | return transform(version); 28 | 29 | } 30 | 31 | private static String transform(final String version){ 32 | 33 | if(version == null || version.trim().isEmpty()){ 34 | return null; 35 | } 36 | 37 | final String versionFormatted; 38 | 39 | switch (version){ 40 | case "HTTP_1_0": 41 | versionFormatted = "HTTP/1.0"; 42 | break; 43 | case "HTTP_1_1": 44 | versionFormatted = "HTTP/1.1"; 45 | break; 46 | case "HTTP_2": 47 | versionFormatted = "HTTP/2.0"; 48 | break; 49 | default: 50 | versionFormatted = version; 51 | break; 52 | } 53 | 54 | return versionFormatted; 55 | 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/com/romanpierson/vertx/web/accesslogger/verticle/AccessLoggerProducerVerticle.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016-2025 Roman Pierson 3 | * ------------------------------------------------------ 4 | * All rights reserved. This program and the accompanying materials 5 | * are made available under the terms of the Apache License v2.0 6 | * which accompanies this distribution. 7 | * 8 | * The Apache License v2.0 is available at 9 | * http://www.opensource.org/licenses/apache2.0.php 10 | * 11 | * You may elect to redistribute this code under either of these licenses. 12 | */ 13 | package com.romanpierson.vertx.web.accesslogger.verticle; 14 | 15 | import java.util.ArrayList; 16 | import java.util.Collection; 17 | import java.util.HashMap; 18 | import java.util.HashSet; 19 | import java.util.List; 20 | import java.util.Map; 21 | import java.util.Set; 22 | 23 | import com.romanpierson.vertx.web.accesslogger.AccessLoggerConstants; 24 | import com.romanpierson.vertx.web.accesslogger.AccessLoggerConstants.Messages.RawEvent; 25 | import com.romanpierson.vertx.web.accesslogger.AccessLoggerConstants.Messages.Registration; 26 | import com.romanpierson.vertx.web.accesslogger.AccessLoggerConstants.Request.Data; 27 | import com.romanpierson.vertx.web.accesslogger.appender.Appender; 28 | import com.romanpierson.vertx.web.accesslogger.configuration.element.AccessLogElement; 29 | import com.romanpierson.vertx.web.accesslogger.configuration.pattern.PatternResolver; 30 | import com.romanpierson.vertx.web.accesslogger.configuration.pattern.ResolvedPatternResult; 31 | import com.romanpierson.vertx.web.accesslogger.exception.AccessLoggerException; 32 | 33 | import io.vertx.core.AbstractVerticle; 34 | import io.vertx.core.internal.logging.Logger; 35 | import io.vertx.core.internal.logging.LoggerFactory; 36 | import io.vertx.core.json.JsonArray; 37 | import io.vertx.core.json.JsonObject; 38 | 39 | /** 40 | * 41 | * Verticle that is responsible for 42 | * 43 | * - Receiving and buffer access log meta data that arrives via the event bus 44 | * - Produce output as configured 45 | * 46 | * @author Roman Pierson 47 | * 48 | */ 49 | public class AccessLoggerProducerVerticle extends AbstractVerticle { 50 | 51 | private final Logger logger = LoggerFactory.getLogger(this.getClass().getName()); 52 | 53 | final PatternResolver patternResolver = new PatternResolver(); 54 | 55 | private Map resolvedLoggerConfigurations = new HashMap<>(); 56 | 57 | @Override 58 | public void start() throws Exception { 59 | 60 | super.start(); 61 | 62 | vertx.eventBus().consumer(AccessLoggerConstants.EVENTBUS_RAW_EVENT_NAME, event -> { 63 | 64 | JsonObject eventBody = event.body(); 65 | JsonArray identifiers = eventBody.getJsonArray(RawEvent.Request.IDENTIFIERS); 66 | 67 | for (Object x : identifiers.getList()) { 68 | String identifier = (String) x; 69 | if (resolvedLoggerConfigurations.containsKey(identifier)) { 70 | 71 | List nativeValues = getNativeValues(resolvedLoggerConfigurations.get(identifier).getResolvedLogElements(), event.body()); 72 | Map internalValues = getInternalValues(eventBody); 73 | 74 | for (Appender appender : resolvedLoggerConfigurations.get(identifier).getRawAppender()) { 75 | appender.push(nativeValues, internalValues); 76 | } 77 | } 78 | } 79 | 80 | }); 81 | 82 | vertx.eventBus().consumer(AccessLoggerConstants.EVENTBUS_REGISTER_EVENT_NAME, event -> 83 | 84 | event.reply(performRegistration(event.body())) 85 | 86 | ); 87 | 88 | } 89 | 90 | private Map getInternalValues(final JsonObject eventBody){ 91 | 92 | return Map.of(AccessLoggerConstants.InternalValues.TIMESTAMP, eventBody.getLong(Data.Type.START_TS_MILLIS.getFieldName())); 93 | 94 | } 95 | 96 | @SuppressWarnings({ "rawtypes", "unchecked" }) 97 | private synchronized JsonObject performRegistration(final JsonObject request) { 98 | 99 | String identifier = request.getString(Registration.Request.IDENTIFIER); 100 | String logPattern = request.getString(Registration.Request.LOGPATTERN); 101 | 102 | JsonObject response = new JsonObject(); 103 | 104 | if (!resolvedLoggerConfigurations.containsKey(identifier)) { 105 | 106 | final ResolvedPatternResult result = patternResolver.resolvePattern(logPattern); 107 | 108 | ResolvedLoggerConfiguration config = new ResolvedLoggerConfiguration(); 109 | config.setOriginalLogPattern(logPattern); 110 | config.setResolvedLogPattern(result.getResolvedPattern()); 111 | config.setResolvedLogElements(result.getLogElements()); 112 | 113 | final Set requiredTypes = determinateRequiredElementData(result.getLogElements()); 114 | 115 | config.setRequiresIncomingHeaders(requiredTypes.contains(Data.Type.REQUEST_HEADERS)); 116 | config.setRequiresOutgoingHeaders(requiredTypes.contains(Data.Type.RESPONSE_HEADERS)); 117 | config.setRequiresCookies(requiredTypes.contains(Data.Type.COOKIES)); 118 | 119 | JsonArray appenders = request.getJsonArray(Registration.Request.APPENDERS, new JsonArray()); 120 | 121 | appenders.forEach(appender -> { 122 | 123 | JsonObject appenderConfig = (JsonObject) appender; 124 | 125 | JsonObject appenderConstructoreConfig = appenderConfig 126 | .getJsonObject(Registration.Request.APPENDER_CONFIG, new JsonObject()); 127 | appenderConstructoreConfig.put(AccessLoggerConstants.CONFIG_KEY_RESOLVED_PATTERN, 128 | config.getResolvedLogPattern()); 129 | 130 | final String appenderClassName = appenderConfig.getString(Registration.Request.APPENDER_CLASS_NAME); 131 | try { 132 | Class clazz = this.getClass().getClassLoader().loadClass(appenderClassName); 133 | Appender appenderInstance = (Appender) clazz.getConstructor(JsonObject.class) 134 | .newInstance(appenderConstructoreConfig); 135 | config.getRawAppender().add(appenderInstance); 136 | 137 | } catch (Exception ex) { 138 | throw new AccessLoggerException("Failed to create appender with [" + appenderClassName + "]", ex); 139 | } 140 | 141 | }); 142 | 143 | resolvedLoggerConfigurations.put(identifier, config); 144 | 145 | logger.info("Successfully created config for [" + identifier + "]"); 146 | 147 | if (config.isRequiresCookies() || config.isRequiresIncomingHeaders() 148 | || config.isRequiresOutgoingHeaders()) { 149 | 150 | logger.info("Config [" + identifier + "] requires specific data for cookies [" + config.isRequiresCookies() + "], incoming headers [" + config.isRequiresIncomingHeaders() + "], outgoing headers [" + config.isRequiresOutgoingHeaders() + "]"); 151 | 152 | } else { 153 | 154 | logger.info("No specific data required for config [" + identifier + "]"); 155 | 156 | } 157 | 158 | populateResponse(response, config); 159 | 160 | } else if (resolvedLoggerConfigurations.get(identifier).getOriginalLogPattern().equals(logPattern)) { 161 | 162 | logger.info("Found and reused config for [" + identifier + "]"); 163 | 164 | populateResponse(response, resolvedLoggerConfigurations.get(identifier)); 165 | 166 | } else { 167 | response.put(Registration.Response.RESULT, Registration.Response.RESULT_FAILED); 168 | } 169 | 170 | return response; 171 | } 172 | 173 | private void populateResponse(final JsonObject response, final ResolvedLoggerConfiguration config) { 174 | 175 | response.put(Registration.Response.RESULT, Registration.Response.RESULT_OK); 176 | response.put(Registration.Response.REQUIRES_COOKIES, config.isRequiresCookies()); 177 | response.put(Registration.Response.REQUIRES_INCOMING_HEADERS, config.isRequiresIncomingHeaders()); 178 | response.put(Registration.Response.REQUIRES_OUTGOING_HEADERS, config.isRequiresOutgoingHeaders()); 179 | 180 | } 181 | 182 | 183 | private List getNativeValues(final Collection logElements, final JsonObject rawValue) { 184 | 185 | List values = new ArrayList<>(logElements.size()); 186 | 187 | for (final AccessLogElement alElement : logElements) { 188 | values.add(alElement.getNativeValue(rawValue)); 189 | } 190 | 191 | return values; 192 | 193 | } 194 | 195 | @Override 196 | public void stop() throws Exception { 197 | 198 | logger.info("Stopping AccessLoggerProducerVerticle"); 199 | 200 | logger.info("Notifying raw appenders about shutdown"); 201 | this.resolvedLoggerConfigurations.values().forEach(resolvedLoggerConfiguration -> 202 | resolvedLoggerConfiguration.getRawAppender().forEach(Appender::notifyShutdown) 203 | ); 204 | 205 | super.stop(); 206 | 207 | } 208 | 209 | Set determinateRequiredElementData(final Collection logElements) { 210 | 211 | final Set requiredTypes = new HashSet<>(); 212 | 213 | for (final AccessLogElement element : logElements) { 214 | requiredTypes.addAll(element.claimDataParts()); 215 | } 216 | 217 | return requiredTypes; 218 | 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /src/main/java/com/romanpierson/vertx/web/accesslogger/verticle/ResolvedLoggerConfiguration.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016-2025 Roman Pierson 3 | * ------------------------------------------------------ 4 | * All rights reserved. This program and the accompanying materials 5 | * are made available under the terms of the Apache License v2.0 6 | * which accompanies this distribution. 7 | * 8 | * The Apache License v2.0 is available at 9 | * http://www.opensource.org/licenses/apache2.0.php 10 | * 11 | * You may elect to redistribute this code under either of these licenses. 12 | */ 13 | package com.romanpierson.vertx.web.accesslogger.verticle; 14 | 15 | import java.util.ArrayList; 16 | import java.util.Collection; 17 | 18 | import com.romanpierson.vertx.web.accesslogger.appender.Appender; 19 | import com.romanpierson.vertx.web.accesslogger.configuration.element.AccessLogElement; 20 | 21 | public class ResolvedLoggerConfiguration { 22 | 23 | private String identifier; 24 | private String originalLogPattern; 25 | private String resolvedLogPattern; 26 | 27 | private boolean requiresIncomingHeaders; 28 | private boolean requiresOutgoingHeaders; 29 | private boolean requiresCookies; 30 | 31 | private Collection resolvedLogElements; 32 | 33 | private Collection rawAppender = new ArrayList<>(); 34 | 35 | public String getIdentifier() { 36 | return identifier; 37 | } 38 | 39 | public void setIdentifier(String identifier) { 40 | this.identifier = identifier; 41 | } 42 | 43 | public String getOriginalLogPattern() { 44 | return originalLogPattern; 45 | } 46 | 47 | public void setOriginalLogPattern(String originalLogPattern) { 48 | this.originalLogPattern = originalLogPattern; 49 | } 50 | 51 | public String getResolvedLogPattern() { 52 | return resolvedLogPattern; 53 | } 54 | 55 | public void setResolvedLogPattern(String resolvedLogPattern) { 56 | this.resolvedLogPattern = resolvedLogPattern; 57 | } 58 | 59 | public boolean isRequiresIncomingHeaders() { 60 | return requiresIncomingHeaders; 61 | } 62 | 63 | public void setRequiresIncomingHeaders(boolean requiresIncomingHeaders) { 64 | this.requiresIncomingHeaders = requiresIncomingHeaders; 65 | } 66 | 67 | public boolean isRequiresOutgoingHeaders() { 68 | return requiresOutgoingHeaders; 69 | } 70 | 71 | public void setRequiresOutgoingHeaders(boolean requiresOutgoingHeaders) { 72 | this.requiresOutgoingHeaders = requiresOutgoingHeaders; 73 | } 74 | 75 | public boolean isRequiresCookies() { 76 | return requiresCookies; 77 | } 78 | 79 | public void setRequiresCookies(boolean requiresCookies) { 80 | this.requiresCookies = requiresCookies; 81 | } 82 | 83 | public Collection getResolvedLogElements() { 84 | return resolvedLogElements; 85 | } 86 | 87 | public void setResolvedLogElements(Collection resolvedLogElements) { 88 | this.resolvedLogElements = resolvedLogElements; 89 | } 90 | 91 | public Collection getRawAppender() { 92 | return rawAppender; 93 | } 94 | 95 | public void setRawAppender(Collection rawAppender) { 96 | this.rawAppender = rawAppender; 97 | } 98 | 99 | 100 | } 101 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/services/com.romanpierson.vertx.web.accesslogger.configuration.element.AccessLogElement: -------------------------------------------------------------------------------- 1 | com.romanpierson.vertx.web.accesslogger.configuration.element.impl.BytesSentElement 2 | com.romanpierson.vertx.web.accesslogger.configuration.element.impl.CookieElement 3 | com.romanpierson.vertx.web.accesslogger.configuration.element.impl.DateTimeElement 4 | com.romanpierson.vertx.web.accesslogger.configuration.element.impl.DurationElement 5 | com.romanpierson.vertx.web.accesslogger.configuration.element.impl.HeaderElement 6 | com.romanpierson.vertx.web.accesslogger.configuration.element.impl.HostElement 7 | com.romanpierson.vertx.web.accesslogger.configuration.element.impl.MethodElement 8 | com.romanpierson.vertx.web.accesslogger.configuration.element.impl.RequestElement 9 | com.romanpierson.vertx.web.accesslogger.configuration.element.impl.StatusElement 10 | com.romanpierson.vertx.web.accesslogger.configuration.element.impl.VersionElement 11 | com.romanpierson.vertx.web.accesslogger.configuration.element.impl.StaticValueElement 12 | com.romanpierson.vertx.web.accesslogger.configuration.element.impl.EnvironmentValueElement -------------------------------------------------------------------------------- /src/test/java/com/romanpierson/custom/element/ShouldNotShowElement.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016-2025 Roman Pierson 3 | * ------------------------------------------------------ 4 | * All rights reserved. This program and the accompanying materials 5 | * are made available under the terms of the Apache License v2.0 6 | * which accompanies this distribution. 7 | * 8 | * The Apache License v2.0 is available at 9 | * http://www.opensource.org/licenses/apache2.0.php 10 | * 11 | * You may elect to redistribute this code under either of these licenses. 12 | */ 13 | package com.romanpierson.custom.element; 14 | 15 | import static com.romanpierson.vertx.web.accesslogger.configuration.pattern.PatternResolver.extractBestPositionFromFixPatternIfApplicable; 16 | 17 | import java.util.ArrayList; 18 | import java.util.Collection; 19 | 20 | import com.romanpierson.vertx.web.accesslogger.configuration.element.AccessLogElement; 21 | import com.romanpierson.vertx.web.accesslogger.configuration.pattern.ExtractedPosition; 22 | 23 | import io.vertx.core.json.JsonObject; 24 | 25 | public class ShouldNotShowElement implements AccessLogElement{ 26 | 27 | @Override 28 | public Collection findInRawPatternInternal(final String rawPattern) { 29 | 30 | Collection foundPositions = new ArrayList<>(1); 31 | 32 | extractBestPositionFromFixPatternIfApplicable(rawPattern, "%x", () -> new ShouldNotShowElement()).ifPresent(foundPositions::add); 33 | 34 | return foundPositions; 35 | 36 | } 37 | 38 | @Override 39 | public Object getNativeValue(final JsonObject values) { 40 | 41 | return "shouldNotShow"; 42 | 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/test/java/com/romanpierson/custom/element/ShouldShowElement.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016-2025 Roman Pierson 3 | * ------------------------------------------------------ 4 | * All rights reserved. This program and the accompanying materials 5 | * are made available under the terms of the Apache License v2.0 6 | * which accompanies this distribution. 7 | * 8 | * The Apache License v2.0 is available at 9 | * http://www.opensource.org/licenses/apache2.0.php 10 | * 11 | * You may elect to redistribute this code under either of these licenses. 12 | */ 13 | package com.romanpierson.custom.element; 14 | 15 | import static com.romanpierson.vertx.web.accesslogger.configuration.pattern.PatternResolver.extractBestPositionFromFixPatternIfApplicable; 16 | 17 | import java.util.ArrayList; 18 | import java.util.Collection; 19 | 20 | import com.romanpierson.vertx.web.accesslogger.configuration.element.AccessLogElement; 21 | import com.romanpierson.vertx.web.accesslogger.configuration.pattern.ExtractedPosition; 22 | 23 | import io.vertx.core.json.JsonObject; 24 | 25 | public class ShouldShowElement implements AccessLogElement{ 26 | 27 | @Override 28 | public Collection findInRawPatternInternal(final String rawPattern) { 29 | 30 | Collection foundPositions = new ArrayList<>(1); 31 | 32 | extractBestPositionFromFixPatternIfApplicable(rawPattern, "%x", () -> new ShouldShowElement()).ifPresent(foundPositions::add); 33 | 34 | return foundPositions; 35 | 36 | } 37 | 38 | @Override 39 | public Object getNativeValue(final JsonObject values) { 40 | 41 | return "shouldShow"; 42 | 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/test/java/com/romanpierson/vertx/test/verticle/SimpleJsonResponseVerticle.java: -------------------------------------------------------------------------------- 1 | package com.romanpierson.vertx.test.verticle; 2 | 3 | 4 | import com.romanpierson.vertx.web.accesslogger.AccessLoggerHandler; 5 | 6 | import io.vertx.config.ConfigRetriever; 7 | import io.vertx.config.ConfigRetrieverOptions; 8 | import io.vertx.config.ConfigStoreOptions; 9 | import io.vertx.core.AbstractVerticle; 10 | import io.vertx.core.Promise; 11 | import io.vertx.core.json.JsonObject; 12 | import io.vertx.ext.web.Router; 13 | import io.vertx.ext.web.handler.BodyHandler; 14 | 15 | public class SimpleJsonResponseVerticle extends AbstractVerticle { 16 | 17 | private final String configFile; 18 | 19 | public SimpleJsonResponseVerticle(String configFile) { 20 | this.configFile = configFile; 21 | } 22 | 23 | @Override 24 | public void start(Promise startPromise) throws Exception { 25 | 26 | final Router router = Router.router(vertx); 27 | 28 | ConfigStoreOptions store = new ConfigStoreOptions().setType("file").setFormat("yaml") 29 | .setConfig(new JsonObject().put("path", configFile)); 30 | 31 | ConfigRetriever retriever = ConfigRetriever.create(vertx, new ConfigRetrieverOptions().addStore(store)); 32 | 33 | retriever 34 | .getConfig() 35 | .onComplete(result -> { 36 | 37 | if(result.succeeded()) { 38 | router 39 | .route() 40 | .handler(BodyHandler.create()) 41 | .handler(AccessLoggerHandler.create(result.result()) 42 | ); 43 | 44 | router 45 | .post("/posttest") 46 | .handler(ctx -> { 47 | 48 | //System.out.println(ctx.body()); 49 | 50 | final JsonObject resultJson = new JsonObject(); 51 | 52 | resultJson.put("uri", ctx.request().uri()); 53 | 54 | ctx.response().putHeader("Content-Type", "application/json; charset=utf-8").end(resultJson.encodePrettily()); 55 | }); 56 | 57 | router 58 | .get("/empty") 59 | .handler(ctx -> { 60 | 61 | ctx.response().putHeader("Content-Type", "application/json; charset=utf-8").end(); 62 | }); 63 | 64 | router 65 | .get() 66 | .handler(ctx -> { 67 | 68 | final JsonObject resultJson = new JsonObject(); 69 | 70 | resultJson.put("uri", ctx.request().uri()); 71 | 72 | ctx.response().putHeader("Content-Type", "application/json; charset=utf-8").end(resultJson.encodePrettily()); 73 | }); 74 | 75 | vertx.createHttpServer().requestHandler(router).listen(8080); 76 | 77 | startPromise.complete(); 78 | } 79 | 80 | }); 81 | 82 | } 83 | 84 | @Override 85 | public void stop(Promise stopPromise) throws Exception { 86 | 87 | stopPromise.complete(); 88 | 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/test/java/com/romanpierson/vertx/web/accesslogger/AcessLogServerTest.java: -------------------------------------------------------------------------------- 1 | package com.romanpierson.vertx.web.accesslogger; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | import static org.junit.jupiter.api.Assertions.assertTrue; 5 | 6 | import org.junit.jupiter.api.MethodOrderer.OrderAnnotation; 7 | import org.junit.jupiter.api.Order; 8 | import org.junit.jupiter.api.Test; 9 | import org.junit.jupiter.api.TestMethodOrder; 10 | import org.junit.jupiter.api.extension.ExtendWith; 11 | 12 | import com.romanpierson.vertx.test.verticle.SimpleJsonResponseVerticle; 13 | import com.romanpierson.vertx.web.accesslogger.verticle.AccessLoggerProducerVerticle; 14 | 15 | import io.vertx.core.Vertx; 16 | import io.vertx.junit5.VertxExtension; 17 | import io.vertx.junit5.VertxTestContext; 18 | 19 | /** 20 | * 21 | * This class tests mostly generic issues with appenders 22 | * 23 | */ 24 | @ExtendWith(VertxExtension.class) 25 | @TestMethodOrder(OrderAnnotation.class) 26 | class AcessLogServerTest { 27 | 28 | @Test 29 | @Order(value = 1) 30 | void testInvalidConfig(Vertx vertx, VertxTestContext testContext) { 31 | 32 | vertx.exceptionHandler(throwable -> { 33 | throwable.printStackTrace(); 34 | assertTrue(throwable instanceof IllegalArgumentException); 35 | assertEquals("must specify at least one valid configuration", throwable.getMessage()); 36 | testContext.completeNow(); 37 | }); 38 | 39 | vertx 40 | .deployVerticle(new AccessLoggerProducerVerticle()) 41 | .onComplete(testContext.succeeding(deploymentId -> { 42 | vertx 43 | .deployVerticle(new SimpleJsonResponseVerticle("accesslog-config-invalid.yaml")) 44 | .onComplete(testContext.succeedingThenComplete()); 45 | })); 46 | 47 | 48 | } 49 | 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/test/java/com/romanpierson/vertx/web/accesslogger/ConsoleAppenderTest.java: -------------------------------------------------------------------------------- 1 | package com.romanpierson.vertx.web.accesslogger; 2 | 3 | 4 | import static org.junit.jupiter.api.Assertions.assertEquals; 5 | import static org.junit.jupiter.api.Assertions.assertTrue; 6 | 7 | import java.io.ByteArrayOutputStream; 8 | import java.io.PrintStream; 9 | 10 | import org.junit.jupiter.api.MethodOrderer.OrderAnnotation; 11 | import org.junit.jupiter.api.Order; 12 | import org.junit.jupiter.api.Test; 13 | import org.junit.jupiter.api.TestMethodOrder; 14 | import org.junit.jupiter.api.extension.ExtendWith; 15 | 16 | import com.romanpierson.vertx.test.verticle.SimpleJsonResponseVerticle; 17 | import com.romanpierson.vertx.web.accesslogger.exception.AccessLoggerException; 18 | import com.romanpierson.vertx.web.accesslogger.verticle.AccessLoggerProducerVerticle; 19 | 20 | import io.vertx.core.Vertx; 21 | import io.vertx.core.http.HttpClient; 22 | import io.vertx.core.http.HttpClientResponse; 23 | import io.vertx.core.http.HttpMethod; 24 | import io.vertx.junit5.VertxExtension; 25 | import io.vertx.junit5.VertxTestContext; 26 | 27 | @ExtendWith(VertxExtension.class) 28 | @TestMethodOrder(OrderAnnotation.class) 29 | class ConsoleAppenderTest { 30 | 31 | @Test 32 | @Order(value = 1) 33 | void testInvalidConsoleAppenderWithMissingResolvedPatttern(Vertx vertx, VertxTestContext testContext) { 34 | 35 | vertx.exceptionHandler(throwable -> { 36 | assertTrue(throwable instanceof AccessLoggerException); 37 | assertEquals("Failed to create appender with [com.romanpierson.vertx.web.accesslogger.appender.console.impl.ConsoleAppender]", throwable.getMessage()); 38 | Throwable internalCause = throwable.getCause().getCause(); 39 | assertTrue(internalCause instanceof IllegalArgumentException); 40 | assertEquals("resolvedPattern must not be empty", internalCause.getMessage()); 41 | testContext.completeNow(); 42 | }); 43 | 44 | vertx 45 | .deployVerticle(new AccessLoggerProducerVerticle()) 46 | .onComplete(testContext.succeeding(deploymentId -> { 47 | vertx 48 | .deployVerticle(new SimpleJsonResponseVerticle("accesslog-config-invalid-console-appender.yaml")) 49 | .onComplete(testContext.succeedingThenComplete()); 50 | })); 51 | 52 | 53 | } 54 | 55 | @Test 56 | @Order(value = 2) 57 | void testWithValidData(Vertx vertx, VertxTestContext testContext) { 58 | 59 | PrintStream originalStream = System.out; 60 | ByteArrayOutputStream catchingStream = new ByteArrayOutputStream(); 61 | 62 | vertx.exceptionHandler(throwable -> { 63 | testContext.failNow(throwable); 64 | }); 65 | 66 | vertx 67 | .deployVerticle(new AccessLoggerProducerVerticle()) 68 | .onComplete(testContext.succeeding(deploymentId -> { 69 | vertx 70 | .deployVerticle(new SimpleJsonResponseVerticle("accesslog-config-console-appender-valid.yaml")) 71 | .onComplete(testContext.succeeding(deploymentId2 -> { 72 | 73 | System.setOut(new PrintStream(catchingStream)); 74 | 75 | HttpClient client = vertx.createHttpClient(); 76 | 77 | // Just to fix github actions issue 78 | try { 79 | Thread.sleep(1000); 80 | } catch (InterruptedException e) { 81 | // we dont care 82 | } 83 | 84 | client 85 | .request(HttpMethod.GET, 8080, "localhost", "/test") 86 | .compose(req -> req.send().compose(HttpClientResponse::body)) 87 | .onComplete(testContext.succeeding(buffer -> testContext.verify(() -> { 88 | 89 | assertEquals("/test\n", catchingStream.toString()); // Has a newline at the end.... 90 | 91 | System.setOut(originalStream); 92 | 93 | testContext.completeNow(); 94 | 95 | }))); 96 | })); 97 | })); 98 | 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/test/java/com/romanpierson/vertx/web/accesslogger/ElasticSearchAppenderTest.java: -------------------------------------------------------------------------------- 1 | package com.romanpierson.vertx.web.accesslogger; 2 | 3 | 4 | import org.junit.jupiter.api.MethodOrderer.OrderAnnotation; 5 | 6 | import static org.junit.jupiter.api.Assertions.assertEquals; 7 | import static org.junit.jupiter.api.Assertions.assertTrue; 8 | 9 | import org.junit.jupiter.api.Test; 10 | import org.junit.jupiter.api.TestMethodOrder; 11 | import org.junit.jupiter.api.extension.ExtendWith; 12 | 13 | import com.romanpierson.vertx.test.verticle.SimpleJsonResponseVerticle; 14 | import com.romanpierson.vertx.web.accesslogger.AccessLoggerConstants.ElasticSearchAppenderConfig; 15 | import com.romanpierson.vertx.web.accesslogger.exception.AccessLoggerException; 16 | import com.romanpierson.vertx.web.accesslogger.verticle.AccessLoggerProducerVerticle; 17 | 18 | import io.vertx.core.Vertx; 19 | import io.vertx.core.http.HttpClient; 20 | import io.vertx.core.http.HttpClientResponse; 21 | import io.vertx.core.http.HttpMethod; 22 | import io.vertx.core.json.JsonObject; 23 | import io.vertx.junit5.VertxExtension; 24 | import io.vertx.junit5.VertxTestContext; 25 | 26 | @ExtendWith(VertxExtension.class) 27 | @TestMethodOrder(OrderAnnotation.class) 28 | class ElasticSearchAppenderTest { 29 | 30 | @Test 31 | void testInvalidElasticSearchAppenderWithMissingInstanceIdentifier(Vertx vertx, VertxTestContext testContext) { 32 | 33 | vertx.exceptionHandler(throwable -> { 34 | assertTrue(throwable instanceof AccessLoggerException); 35 | assertEquals("Failed to create appender with [com.romanpierson.vertx.web.accesslogger.appender.elasticsearch.impl.ElasticSearchAppender]", throwable.getMessage()); 36 | Throwable internalCause = throwable.getCause().getCause(); 37 | assertTrue(internalCause instanceof IllegalArgumentException); 38 | assertEquals("instanceIdentifier must not be empty", internalCause.getMessage()); 39 | testContext.completeNow(); 40 | }); 41 | 42 | vertx 43 | .deployVerticle(new AccessLoggerProducerVerticle()) 44 | .onComplete(testContext.succeeding(deploymentId -> { 45 | vertx 46 | .deployVerticle(new SimpleJsonResponseVerticle("accesslog-config-invalid-console-appender.yaml")) 47 | .onComplete(testContext.succeedingThenComplete()); 48 | })); 49 | } 50 | 51 | @Test 52 | void testInvalidElasticSearchAppenderWithMissingFieldNames(Vertx vertx, VertxTestContext testContext) { 53 | 54 | vertx.exceptionHandler(throwable -> { 55 | assertTrue(throwable instanceof AccessLoggerException); 56 | assertEquals("Failed to create appender with [com.romanpierson.vertx.web.accesslogger.appender.elasticsearch.impl.ElasticSearchAppender]", throwable.getMessage()); 57 | Throwable internalCause = throwable.getCause().getCause(); 58 | assertTrue(internalCause instanceof IllegalArgumentException); 59 | assertEquals("fieldNames must not be empty", internalCause.getMessage()); 60 | testContext.completeNow(); 61 | }); 62 | 63 | vertx 64 | .deployVerticle(new AccessLoggerProducerVerticle()) 65 | .onComplete(testContext.succeeding(deploymentId -> { 66 | vertx 67 | .deployVerticle(new SimpleJsonResponseVerticle("accesslog-config-elasticsearch-appender-invalid-fieldnames.yaml")) 68 | .onComplete(testContext.succeedingThenComplete()); 69 | })); 70 | } 71 | 72 | @Test 73 | void testValid(Vertx vertx, VertxTestContext testContext) { 74 | 75 | vertx.exceptionHandler(throwable -> { 76 | testContext.failNow(throwable); 77 | }); 78 | 79 | vertx.eventBus().consumer(ElasticSearchAppenderConfig.ELASTICSEARCH_INDEXER_EVENTBUS_EVENT_NAME, message -> { 80 | 81 | // Verify the meta data 82 | JsonObject logEntry = message.body(); 83 | 84 | // Verify the meta section 85 | JsonObject metaEntry = logEntry.getJsonObject("meta"); 86 | assertEquals("esInstance", metaEntry.getString("instance_identifier")); 87 | assertTrue(metaEntry.getLong("timestamp").longValue() > 0); 88 | 89 | 90 | // Verify the data section 91 | JsonObject messageEntry = logEntry.getJsonObject("message"); 92 | assertEquals("/test", messageEntry.getString("uri")); 93 | assertTrue(messageEntry.getLong("duration").longValue() >= 0); 94 | 95 | testContext.completeNow(); 96 | }); 97 | 98 | vertx 99 | .deployVerticle(new AccessLoggerProducerVerticle()) 100 | .onComplete(testContext.succeeding(deploymentId -> { 101 | vertx 102 | .deployVerticle(new SimpleJsonResponseVerticle("accesslog-config-elasticsearch-appender-valid.yaml")) 103 | .onComplete(testContext.succeeding(deploymentId2 -> { 104 | 105 | // Just to fix github actions issue 106 | try { 107 | Thread.sleep(1000); 108 | } catch (InterruptedException e) { 109 | // we dont care 110 | } 111 | 112 | HttpClient client = vertx.createHttpClient(); 113 | 114 | client 115 | .request(HttpMethod.GET, 8080, "localhost", "/test") 116 | .compose(req -> req.send().compose(HttpClientResponse::body)) 117 | .onComplete(testContext.succeeding(buffer -> testContext.verify(() -> { 118 | 119 | }))); 120 | 121 | })); 122 | })); 123 | 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/test/java/com/romanpierson/vertx/web/accesslogger/EventBusAppenderTest.java: -------------------------------------------------------------------------------- 1 | package com.romanpierson.vertx.web.accesslogger; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | import static org.junit.jupiter.api.Assertions.assertTrue; 5 | 6 | import org.junit.jupiter.api.MethodOrderer.OrderAnnotation; 7 | import org.junit.jupiter.api.Order; 8 | import org.junit.jupiter.api.Test; 9 | import org.junit.jupiter.api.TestMethodOrder; 10 | import org.junit.jupiter.api.extension.ExtendWith; 11 | 12 | import com.romanpierson.vertx.test.verticle.SimpleJsonResponseVerticle; 13 | import com.romanpierson.vertx.web.accesslogger.exception.AccessLoggerException; 14 | import com.romanpierson.vertx.web.accesslogger.verticle.AccessLoggerProducerVerticle; 15 | 16 | import io.vertx.core.Vertx; 17 | import io.vertx.core.http.HttpClient; 18 | import io.vertx.core.http.HttpClientResponse; 19 | import io.vertx.core.http.HttpMethod; 20 | import io.vertx.core.json.JsonArray; 21 | import io.vertx.junit5.VertxExtension; 22 | import io.vertx.junit5.VertxTestContext; 23 | 24 | @ExtendWith(VertxExtension.class) 25 | @TestMethodOrder(OrderAnnotation.class) 26 | class EventBusAppenderTest { 27 | 28 | @Test 29 | @Order(value = 1) 30 | void testInvalidConfig(Vertx vertx, VertxTestContext testContext) { 31 | 32 | vertx.exceptionHandler(throwable -> { 33 | assertTrue(throwable instanceof AccessLoggerException); 34 | assertEquals("Failed to create appender with [com.romanpierson.vertx.web.accesslogger.appender.eventbus.impl.EventBusAppender]", throwable.getMessage()); 35 | Throwable internalCause = throwable.getCause().getCause(); 36 | assertTrue(internalCause instanceof IllegalArgumentException); 37 | assertEquals("targetAddress must not be empty", internalCause.getMessage()); 38 | testContext.completeNow(); 39 | }); 40 | 41 | vertx 42 | .deployVerticle(new AccessLoggerProducerVerticle()) 43 | .onComplete(testContext.succeeding(deploymentId -> { 44 | vertx 45 | .deployVerticle(new SimpleJsonResponseVerticle("accesslog-config-eventbus-appender-invalid-config.yaml")) 46 | .onComplete(testContext.succeedingThenComplete()); 47 | })); 48 | 49 | } 50 | 51 | @Test 52 | @Order(value = 2) 53 | void testWithNotExistingAdress(Vertx vertx, VertxTestContext testContext) { 54 | 55 | vertx.exceptionHandler(throwable -> { 56 | testContext.failNow(throwable); 57 | }); 58 | 59 | vertx 60 | .deployVerticle(new AccessLoggerProducerVerticle()) 61 | .onComplete(testContext.succeeding(deploymentId -> { 62 | vertx 63 | .deployVerticle(new SimpleJsonResponseVerticle("accesslog-config-eventbus-appender-inexisting-address.yaml")) 64 | .onComplete(testContext.succeeding(deploymentId2 -> { 65 | 66 | HttpClient client = vertx.createHttpClient(); 67 | 68 | try { 69 | Thread.sleep(1000); 70 | } catch (InterruptedException e) { 71 | e.printStackTrace(); 72 | } 73 | 74 | client 75 | .request(HttpMethod.GET, 8080, "localhost", "/test") 76 | .compose(req -> req.send().compose(HttpClientResponse::body)) 77 | .onComplete(testContext.succeeding(buffer -> testContext.verify(() -> { 78 | testContext.completeNow(); 79 | }))); 80 | 81 | })); 82 | })); 83 | 84 | } 85 | 86 | @Test 87 | @Order(value = 3) 88 | void testWithExistingAdress(Vertx vertx, VertxTestContext testContext) { 89 | 90 | vertx.exceptionHandler(throwable -> { 91 | testContext.failNow(throwable); 92 | }); 93 | 94 | vertx.eventBus().consumer("shangriLa", message -> { 95 | assertEquals(1, message.body().size()); 96 | assertEquals("/test", message.body().getString(0)); 97 | testContext.completeNow(); 98 | }); 99 | 100 | vertx 101 | .deployVerticle(new AccessLoggerProducerVerticle()) 102 | .onComplete(testContext.succeeding(deploymentId -> { 103 | vertx 104 | .deployVerticle(new SimpleJsonResponseVerticle("accesslog-config-eventbus-appender-existing-address.yaml")) 105 | .onComplete(testContext.succeeding(deploymentId2 -> { 106 | 107 | HttpClient client = vertx.createHttpClient(); 108 | 109 | // Just to fix github actions issue 110 | try { 111 | Thread.sleep(1000); 112 | } catch (InterruptedException e) { 113 | // we dont care 114 | } 115 | 116 | client 117 | .request(HttpMethod.GET, 8080, "localhost", "/test") 118 | .compose(req -> req.send().compose(HttpClientResponse::body)) 119 | .onComplete(testContext.succeeding(buffer -> testContext.verify(() -> { 120 | 121 | }))); 122 | 123 | })); 124 | })); 125 | 126 | 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/test/java/com/romanpierson/vertx/web/accesslogger/LoggingAppenderTest.java: -------------------------------------------------------------------------------- 1 | package com.romanpierson.vertx.web.accesslogger; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | import static org.junit.jupiter.api.Assertions.assertTrue; 5 | 6 | import java.io.ByteArrayOutputStream; 7 | import java.io.PrintStream; 8 | 9 | import org.junit.jupiter.api.MethodOrderer.OrderAnnotation; 10 | import org.junit.jupiter.api.Test; 11 | import org.junit.jupiter.api.TestMethodOrder; 12 | import org.junit.jupiter.api.extension.ExtendWith; 13 | import org.opentest4j.AssertionFailedError; 14 | 15 | import com.romanpierson.vertx.test.verticle.SimpleJsonResponseVerticle; 16 | import com.romanpierson.vertx.web.accesslogger.exception.AccessLoggerException; 17 | import com.romanpierson.vertx.web.accesslogger.verticle.AccessLoggerProducerVerticle; 18 | 19 | import io.vertx.core.Vertx; 20 | import io.vertx.core.http.HttpClient; 21 | import io.vertx.core.http.HttpClientResponse; 22 | import io.vertx.core.http.HttpMethod; 23 | import io.vertx.junit5.VertxExtension; 24 | import io.vertx.junit5.VertxTestContext; 25 | 26 | @ExtendWith(VertxExtension.class) 27 | @TestMethodOrder(OrderAnnotation.class) 28 | class LoggingAppenderTest { 29 | 30 | @Test 31 | void testInvalidConfig(Vertx vertx, VertxTestContext testContext) { 32 | 33 | vertx.exceptionHandler(throwable -> { 34 | 35 | try { 36 | assertTrue(throwable instanceof AccessLoggerException); 37 | assertEquals("Failed to create appender with [com.romanpierson.vertx.web.accesslogger.appender.logging.impl.LoggingAppender]", throwable.getMessage()); 38 | Throwable internalCause = throwable.getCause().getCause(); 39 | assertTrue(internalCause instanceof IllegalArgumentException); 40 | assertEquals("loggerName must not be empty", internalCause.getMessage()); 41 | }catch(AssertionFailedError ex) { 42 | testContext.failNow(ex); 43 | } 44 | testContext.completeNow(); 45 | }); 46 | 47 | vertx 48 | .deployVerticle(new AccessLoggerProducerVerticle()) 49 | .onComplete(testContext.succeeding(deploymentId -> { 50 | vertx 51 | .deployVerticle(new SimpleJsonResponseVerticle("accesslog-config-logging-appender-invalid-loggername.yaml")) 52 | .onComplete(testContext.succeedingThenComplete()); 53 | })); 54 | } 55 | 56 | @Test 57 | void testInvalidLoggingAppenderWithMissingResolvedPatttern(Vertx vertx, VertxTestContext testContext) { 58 | 59 | vertx.exceptionHandler(throwable -> { 60 | 61 | try { 62 | assertTrue(throwable instanceof AccessLoggerException); 63 | assertEquals("Failed to create appender with [com.romanpierson.vertx.web.accesslogger.appender.logging.impl.LoggingAppender]", throwable.getMessage()); 64 | Throwable internalCause = throwable.getCause().getCause(); 65 | assertTrue(internalCause instanceof IllegalArgumentException); 66 | assertEquals("resolvedPattern must not be empty", internalCause.getMessage()); 67 | }catch(AssertionFailedError ex) { 68 | testContext.failNow(ex); 69 | } 70 | testContext.completeNow(); 71 | }); 72 | 73 | vertx 74 | .deployVerticle(new AccessLoggerProducerVerticle()) 75 | .onComplete(testContext.succeeding(deploymentId -> { 76 | vertx 77 | .deployVerticle(new SimpleJsonResponseVerticle("accesslog-config-logging-appender-invalid-logpattern.yaml")) 78 | .onComplete(testContext.succeedingThenComplete()); 79 | })); 80 | } 81 | 82 | // TODO this test needs to be fixed - need to check how the message written by logback can be read and verified 83 | @Test 84 | // This tests using slf4j/logback that at the end logs again to console so we can grab it 85 | void testWithValidData(Vertx vertx, VertxTestContext testContext) { 86 | 87 | PrintStream originalStream = System.out; 88 | ByteArrayOutputStream catchingStream = new ByteArrayOutputStream(); 89 | 90 | vertx.exceptionHandler(throwable -> { 91 | testContext.failNow(throwable); 92 | }); 93 | 94 | vertx 95 | .deployVerticle(new AccessLoggerProducerVerticle()) 96 | .onComplete(testContext.succeeding(deploymentId -> { 97 | vertx 98 | .deployVerticle(new SimpleJsonResponseVerticle("accesslog-config-logging-appender-valid.yaml")) 99 | .onComplete(testContext.succeeding(deploymentId2 -> { 100 | 101 | // Just to fix github actions issue 102 | try { 103 | Thread.sleep(1000); 104 | } catch (InterruptedException e) { 105 | // we dont care 106 | } 107 | 108 | System.setOut(new PrintStream(catchingStream)); 109 | 110 | HttpClient client = vertx.createHttpClient(); 111 | 112 | client 113 | .request(HttpMethod.GET, 8080, "localhost", "/test") 114 | .compose(req -> req.send().compose(HttpClientResponse::body)) 115 | .onComplete(testContext.succeeding(buffer -> testContext.verify(() -> { 116 | 117 | // Ensure it got logged by slf4j/logback 118 | Thread.sleep(1000); 119 | 120 | //assertEquals("/test\n", catchingStream.toString()); 121 | 122 | System.setOut(originalStream); 123 | 124 | 125 | //System.out.println(catchingStream); 126 | //assertEquals("/test", catchingStream.toString()); 127 | 128 | testContext.completeNow(); 129 | 130 | }))); 131 | 132 | })); 133 | })); 134 | 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/test/java/com/romanpierson/vertx/web/accesslogger/appender/MemoryAppender.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016-2025 Roman Pierson 3 | * ------------------------------------------------------ 4 | * All rights reserved. This program and the accompanying materials 5 | * are made available under the terms of the Apache License v2.0 6 | * which accompanies this distribution. 7 | * 8 | * The Apache License v2.0 is available at 9 | * http://www.opensource.org/licenses/apache2.0.php 10 | * 11 | * You may elect to redistribute this code under either of these licenses. 12 | */ 13 | package com.romanpierson.vertx.web.accesslogger.appender; 14 | 15 | import java.util.List; 16 | import java.util.Map; 17 | 18 | import com.romanpierson.vertx.web.accesslogger.AccessLoggerConstants; 19 | 20 | import io.vertx.core.internal.logging.Logger; 21 | import io.vertx.core.internal.logging.LoggerFactory; 22 | import io.vertx.core.json.JsonObject; 23 | 24 | import static com.romanpierson.vertx.web.accesslogger.util.FormatUtility.getStringifiedParameterValues; 25 | 26 | /** 27 | * 28 | * An implementation of {@link Appender} that writes to System.out 29 | * 30 | * @author Roman Pierson 31 | * 32 | */ 33 | public class MemoryAppender implements Appender { 34 | 35 | private final Logger logger = LoggerFactory.getLogger(this.getClass().getName()); 36 | 37 | private final String resolvedPattern; 38 | 39 | public MemoryAppender(final JsonObject config){ 40 | 41 | if(config == null || config.getString(AccessLoggerConstants.CONFIG_KEY_RESOLVED_PATTERN, "").trim().length() == 0){ 42 | throw new IllegalArgumentException("resolvedPattern must not be empty"); 43 | } 44 | 45 | this.resolvedPattern = config.getString(AccessLoggerConstants.CONFIG_KEY_RESOLVED_PATTERN); 46 | 47 | logger.info("Created ConsoleAppender with resolvedLogPattern [" + this.resolvedPattern + "]"); 48 | 49 | } 50 | 51 | @Override 52 | public void push(List rawAccessElementValues, Map internalValues) { 53 | 54 | final String formattedString = String.format(this.resolvedPattern, getStringifiedParameterValues(rawAccessElementValues)); 55 | 56 | System.out.println(formattedString); 57 | 58 | } 59 | 60 | 61 | } 62 | -------------------------------------------------------------------------------- /src/test/java/com/romanpierson/vertx/web/accesslogger/configuration/element/impl/AccessLogElementTest.java: -------------------------------------------------------------------------------- 1 | package com.romanpierson.vertx.web.accesslogger.configuration.element.impl; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | 5 | import java.util.Collection; 6 | 7 | import org.junit.jupiter.api.Test; 8 | 9 | import com.romanpierson.vertx.web.accesslogger.configuration.pattern.ExtractedPosition; 10 | 11 | public class AccessLogElementTest { 12 | 13 | @Test 14 | public void testInvalidElement1() { 15 | 16 | InvalidElement element = new InvalidElement(); 17 | Collection eps = element.findInRawPattern("foo"); 18 | 19 | assertEquals(0, eps.size()); 20 | 21 | } 22 | 23 | @Test 24 | public void testInvalidElement2() { 25 | 26 | InvalidElement2 element = new InvalidElement2(); 27 | Collection eps = element.findInRawPattern("foo"); 28 | 29 | assertEquals(0, eps.size()); 30 | 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/test/java/com/romanpierson/vertx/web/accesslogger/configuration/element/impl/InvalidElement.java: -------------------------------------------------------------------------------- 1 | package com.romanpierson.vertx.web.accesslogger.configuration.element.impl; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Collection; 5 | 6 | import com.romanpierson.vertx.web.accesslogger.configuration.element.AccessLogElement; 7 | import com.romanpierson.vertx.web.accesslogger.configuration.pattern.ExtractedPosition; 8 | 9 | import io.vertx.core.json.JsonObject; 10 | 11 | public class InvalidElement implements AccessLogElement { 12 | 13 | @Override 14 | public Collection findInRawPatternInternal(String rawPattern) { 15 | 16 | Collection eps = new ArrayList<>(); 17 | eps.add(null); 18 | 19 | return eps; 20 | 21 | } 22 | 23 | @Override 24 | public Object getNativeValue(JsonObject values) { 25 | 26 | return null; 27 | 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/test/java/com/romanpierson/vertx/web/accesslogger/configuration/element/impl/InvalidElement2.java: -------------------------------------------------------------------------------- 1 | package com.romanpierson.vertx.web.accesslogger.configuration.element.impl; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Collection; 5 | 6 | import com.romanpierson.vertx.web.accesslogger.configuration.element.AccessLogElement; 7 | import com.romanpierson.vertx.web.accesslogger.configuration.pattern.ExtractedPosition; 8 | 9 | import io.vertx.core.json.JsonObject; 10 | 11 | public class InvalidElement2 implements AccessLogElement { 12 | 13 | @Override 14 | public Collection findInRawPatternInternal(String rawPattern) { 15 | 16 | Collection eps = new ArrayList<>(); 17 | 18 | ExtractedPosition ep = ExtractedPosition.build(-1, 0, InvalidElement2::new); 19 | 20 | eps.add(ep); 21 | 22 | return eps; 23 | 24 | } 25 | 26 | @Override 27 | public Object getNativeValue(JsonObject values) { 28 | 29 | return null; 30 | 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/test/java/com/romanpierson/vertx/web/accesslogger/configuration/element/impl/ShouldNotShowElement.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016-2025 Roman Pierson 3 | * ------------------------------------------------------ 4 | * All rights reserved. This program and the accompanying materials 5 | * are made available under the terms of the Apache License v2.0 6 | * which accompanies this distribution. 7 | * 8 | * The Apache License v2.0 is available at 9 | * http://www.opensource.org/licenses/apache2.0.php 10 | * 11 | * You may elect to redistribute this code under either of these licenses. 12 | */ 13 | package com.romanpierson.vertx.web.accesslogger.configuration.element.impl; 14 | 15 | import static com.romanpierson.vertx.web.accesslogger.configuration.pattern.PatternResolver.extractBestPositionFromFixPatternIfApplicable; 16 | 17 | import java.util.ArrayList; 18 | import java.util.Collection; 19 | 20 | import com.romanpierson.vertx.web.accesslogger.configuration.element.AccessLogElement; 21 | import com.romanpierson.vertx.web.accesslogger.configuration.pattern.ExtractedPosition; 22 | 23 | import io.vertx.core.json.JsonObject; 24 | 25 | public class ShouldNotShowElement implements AccessLogElement{ 26 | 27 | @Override 28 | public Collection findInRawPatternInternal(final String rawPattern) { 29 | 30 | Collection foundPositions = new ArrayList<>(1); 31 | 32 | extractBestPositionFromFixPatternIfApplicable(rawPattern, "%x", () -> new ShouldNotShowElement()).ifPresent(foundPositions::add); 33 | 34 | return foundPositions; 35 | 36 | } 37 | 38 | @Override 39 | public Object getNativeValue(final JsonObject values) { 40 | 41 | return "shouldNotShow"; 42 | 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/test/java/com/romanpierson/vertx/web/accesslogger/configuration/pattern/DurationElementCustomTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016-2025 Roman Pierson 3 | * ------------------------------------------------------ 4 | * All rights reserved. This program and the accompanying materials 5 | * are made available under the terms of the Apache License v2.0 6 | * which accompanies this distribution. 7 | * 8 | * The Apache License v2.0 is available at 9 | * http://www.opensource.org/licenses/apache2.0.php 10 | * 11 | * You may elect to redistribute this code under either of these licenses. 12 | */ 13 | package com.romanpierson.vertx.web.accesslogger.configuration.pattern; 14 | 15 | import static org.junit.jupiter.api.Assertions.assertEquals; 16 | import static org.junit.jupiter.api.Assertions.assertNotNull; 17 | 18 | import java.util.HashMap; 19 | import java.util.Map; 20 | 21 | import com.romanpierson.vertx.web.accesslogger.AccessLoggerConstants.Request.Data; 22 | import com.romanpierson.vertx.web.accesslogger.configuration.element.impl.DurationElement; 23 | import com.romanpierson.vertx.web.accesslogger.configuration.element.impl.RequestElement; 24 | import com.romanpierson.vertx.web.accesslogger.util.VersionUtility; 25 | 26 | import io.vertx.core.json.JsonObject; 27 | 28 | /** 29 | * 30 | * Tests some special conditions for {@link DurationElement} 31 | * 32 | * @author Roman Pierson 33 | * 34 | */ 35 | public class DurationElementCustomTest { 36 | 37 | final String httpVersion = "HTTP_1_1"; 38 | final Map valuesWithQuery = getAllValues("uri-value", "method-value", "query-value", httpVersion); 39 | final Map valuesWithoutQuery = getAllValues("uri-value", "method-value", null, httpVersion); 40 | 41 | //@Test 42 | public void testFindInRawPatternApacheFirstRequestLine(){ 43 | 44 | //DurationElement.of(TimeUnit.SECONDS); 45 | 46 | 47 | final String expectedOutputWithQuery = "method-value uri-value?query-value " + VersionUtility.getFormattedValue(new JsonObject(valuesWithQuery)); 48 | final String expectedOutputWithoutQuery = "method-value uri-value " + VersionUtility.getFormattedValue(new JsonObject(valuesWithQuery)); 49 | 50 | ExtractedPosition ep = new RequestElement().findInRawPattern("%r").iterator().next(); 51 | assertNotNull(ep); 52 | assertEquals(0, ep.getStart()); 53 | assertEquals("%r".length(), ep.getOffset()); 54 | 55 | // Test if the output of the looked up element is correct 56 | assertEquals(expectedOutputWithQuery, ep.getElementSupplier().get().getNativeValue(new JsonObject(valuesWithQuery))); 57 | assertEquals(expectedOutputWithoutQuery, ep.getElementSupplier().get().getNativeValue(new JsonObject(valuesWithoutQuery))); 58 | } 59 | 60 | /** 61 | @Test 62 | public void testFindInRawPatternInvalidInputNull(){ 63 | 64 | assertThrows(IllegalArgumentException.class, 65 | ()->{ 66 | new RequestElement().findInRawPattern(null); 67 | }); 68 | 69 | 70 | } 71 | 72 | @Test 73 | public void testFindInRawPatternInvalidInputEmpty(){ 74 | 75 | assertThrows(IllegalArgumentException.class, 76 | ()->{ 77 | new RequestElement().findInRawPattern(""); 78 | }); 79 | 80 | } 81 | 82 | @Test 83 | public void testFindInRawPatternApacheFirstRequestLine(){ 84 | 85 | final String expectedOutputWithQuery = "method-value uri-value?query-value " + VersionUtility.getFormattedValue(new JsonObject(valuesWithQuery)); 86 | final String expectedOutputWithoutQuery = "method-value uri-value " + VersionUtility.getFormattedValue(new JsonObject(valuesWithQuery)); 87 | 88 | ExtractedPosition ep = new RequestElement().findInRawPattern("%r").iterator().next(); 89 | assertNotNull(ep); 90 | assertEquals(0, ep.getStart()); 91 | assertEquals("%r".length(), ep.getOffset()); 92 | 93 | // Test if the output of the looked up element is correct 94 | assertEquals(expectedOutputWithQuery, ep.getElementSupplier().get().getFormattedValue(new JsonObject(valuesWithQuery))); 95 | assertEquals(expectedOutputWithoutQuery, ep.getElementSupplier().get().getFormattedValue(new JsonObject(valuesWithoutQuery))); 96 | } 97 | 98 | @Test 99 | public void testFindInRawPatternURI(){ 100 | 101 | final String expectedOutput = "uri-value"; 102 | 103 | ExtractedPosition ep1 = new RequestElement().findInRawPattern("%U").iterator().next(); 104 | assertNotNull(ep1); 105 | assertEquals(0, ep1.getStart()); 106 | assertEquals("%U".length(), ep1.getOffset()); 107 | 108 | // Test if the output of the looked up element is correct 109 | assertEquals(expectedOutput, ep1.getElementSupplier().get().getFormattedValue(new JsonObject(valuesWithQuery))); 110 | 111 | ExtractedPosition ep2 = new RequestElement().findInRawPattern("cs-uri-stem").iterator().next(); 112 | assertNotNull(ep2); 113 | assertEquals(0, ep2.getStart()); 114 | assertEquals("cs-uri-stem".length(), ep2.getOffset()); 115 | 116 | // Test if the output of the looked up element is correct 117 | assertEquals(expectedOutput, ep2.getElementSupplier().get().getFormattedValue(new JsonObject(valuesWithQuery))); 118 | 119 | } 120 | **/ 121 | 122 | private Map getAllValues(final String uriValue, final String methodValue, final String queryValue, final String versionValue){ 123 | 124 | Map values = new HashMap<>(); 125 | 126 | values.put(Data.Type.URI.getFieldName(), uriValue); 127 | values.put(Data.Type.VERSION.getFieldName(), versionValue); 128 | values.put(Data.Type.QUERY.getFieldName(), queryValue); 129 | values.put(Data.Type.METHOD.getFieldName(), methodValue); 130 | 131 | return values; 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/test/java/com/romanpierson/vertx/web/accesslogger/configuration/pattern/PatternResolverTest.java: -------------------------------------------------------------------------------- 1 | package com.romanpierson.vertx.web.accesslogger.configuration.pattern; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | import static org.junit.jupiter.api.Assertions.assertTrue; 5 | 6 | import java.util.Arrays; 7 | import java.util.Collection; 8 | import java.util.Collections; 9 | import java.util.Iterator; 10 | 11 | import org.junit.jupiter.api.Test; 12 | 13 | import com.romanpierson.vertx.web.accesslogger.configuration.element.AccessLogElement; 14 | import com.romanpierson.vertx.web.accesslogger.configuration.element.impl.CookieElement; 15 | import com.romanpierson.vertx.web.accesslogger.configuration.element.impl.DateTimeElement; 16 | import com.romanpierson.vertx.web.accesslogger.configuration.element.impl.DurationElement; 17 | import com.romanpierson.vertx.web.accesslogger.configuration.element.impl.HeaderElement; 18 | import com.romanpierson.vertx.web.accesslogger.configuration.element.impl.MethodElement; 19 | import com.romanpierson.vertx.web.accesslogger.configuration.element.impl.RequestElement; 20 | import com.romanpierson.vertx.web.accesslogger.configuration.element.impl.StaticValueElement; 21 | import com.romanpierson.vertx.web.accesslogger.configuration.element.impl.StatusElement; 22 | 23 | public class PatternResolverTest { 24 | 25 | @Test 26 | public void resolvePattern_off_the_shelf_pattern() { 27 | PatternResolver patternResolver = new PatternResolver(); 28 | String logPattern = "server1 %{server1}static %{Accept-Encoding}i %t %D cs-uri %{foo}C"; 29 | Collection expected = Arrays 30 | .asList(StaticValueElement.class, HeaderElement.class, DateTimeElement.class, DurationElement.class, 31 | RequestElement.class, CookieElement.class); 32 | 33 | ResolvedPatternResult resolvedPatternResult = patternResolver.resolvePattern(logPattern); 34 | Collection actual = resolvedPatternResult.getLogElements(); 35 | 36 | assertResolvedPatterns(expected, actual); 37 | } 38 | 39 | @Test 40 | public void resolvePattern_single_pattern_element() { 41 | PatternResolver patternResolver = new PatternResolver(); 42 | String logPattern = "%m"; 43 | Collection expected = Collections.singletonList(MethodElement.class); 44 | 45 | ResolvedPatternResult resolvedPatternResult = patternResolver.resolvePattern(logPattern); 46 | Collection actual = resolvedPatternResult.getLogElements(); 47 | 48 | assertResolvedPatterns(expected, actual); 49 | } 50 | 51 | @Test 52 | public void resolvePattern_two_pattern_elements() { 53 | PatternResolver patternResolver = new PatternResolver(); 54 | String logPattern = "%D %m"; 55 | Collection expected = Arrays.asList(DurationElement.class, MethodElement.class); 56 | 57 | ResolvedPatternResult resolvedPatternResult = patternResolver.resolvePattern(logPattern); 58 | Collection actual = resolvedPatternResult.getLogElements(); 59 | 60 | assertResolvedPatterns(expected, actual); 61 | } 62 | 63 | @Test 64 | public void resolvePattern_two_pattern_elements_reversed_order() { 65 | PatternResolver patternResolver = new PatternResolver(); 66 | String logPattern = "%m %D"; 67 | Collection expected = Arrays.asList(MethodElement.class, DurationElement.class); 68 | 69 | ResolvedPatternResult resolvedPatternResult = patternResolver.resolvePattern(logPattern); 70 | Collection actual = resolvedPatternResult.getLogElements(); 71 | 72 | assertResolvedPatterns(expected, actual); 73 | } 74 | 75 | @Test 76 | public void resolvePattern_duplicate_pattern_elements() { 77 | PatternResolver patternResolver = new PatternResolver(); 78 | String logPattern = "%s %s %s"; 79 | Collection expected = Arrays.asList(StatusElement.class, StatusElement.class, StatusElement.class); 80 | 81 | ResolvedPatternResult resolvedPatternResult = patternResolver.resolvePattern(logPattern); 82 | Collection actual = resolvedPatternResult.getLogElements(); 83 | 84 | assertResolvedPatterns(expected, actual); 85 | } 86 | 87 | @Test 88 | public void resolvePattern_unresolvable_parts() { 89 | PatternResolver patternResolver = new PatternResolver(); 90 | String logPattern = "xx %m yy"; 91 | Collection expected = Arrays.asList(MethodElement.class); 92 | 93 | ResolvedPatternResult resolvedPatternResult = patternResolver.resolvePattern(logPattern); 94 | Collection actual = resolvedPatternResult.getLogElements(); 95 | 96 | assertResolvedPatterns(expected, actual); 97 | assertEquals("xx %s yy", resolvedPatternResult.getResolvedPattern()); 98 | } 99 | 100 | private void assertResolvedPatterns(Collection expectedElementClasses, Collection actual) { 101 | 102 | assertEquals(expectedElementClasses.size(), actual.size(), actual.toString()); 103 | 104 | Iterator actualIter = actual.iterator(); 105 | Iterator expectedElementClassesIterator = expectedElementClasses.iterator(); 106 | 107 | while (actualIter.hasNext()) { 108 | AccessLogElement actualElement = actualIter.next(); 109 | Class expectedElementClass = expectedElementClassesIterator.next(); 110 | assertTrue(actualElement.getClass().isAssignableFrom(expectedElementClass)); 111 | } 112 | } 113 | } -------------------------------------------------------------------------------- /src/test/java/com/romanpierson/vertx/web/accesslogger/configuration/pattern/RequestElementTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016-2025 Roman Pierson 3 | * ------------------------------------------------------ 4 | * All rights reserved. This program and the accompanying materials 5 | * are made available under the terms of the Apache License v2.0 6 | * which accompanies this distribution. 7 | * 8 | * The Apache License v2.0 is available at 9 | * http://www.opensource.org/licenses/apache2.0.php 10 | * 11 | * You may elect to redistribute this code under either of these licenses. 12 | */ 13 | package com.romanpierson.vertx.web.accesslogger.configuration.pattern; 14 | 15 | import static org.junit.jupiter.api.Assertions.assertEquals; 16 | import static org.junit.jupiter.api.Assertions.assertNotNull; 17 | import static org.junit.jupiter.api.Assertions.assertThrows; 18 | 19 | import java.util.HashMap; 20 | import java.util.Map; 21 | 22 | import org.junit.jupiter.api.Test; 23 | 24 | import com.romanpierson.vertx.web.accesslogger.AccessLoggerConstants.Request.Data; 25 | import com.romanpierson.vertx.web.accesslogger.configuration.element.impl.RequestElement; 26 | import com.romanpierson.vertx.web.accesslogger.util.VersionUtility; 27 | 28 | import io.vertx.core.json.JsonObject; 29 | 30 | /** 31 | * 32 | * Tests {@link RequestElement} 33 | * 34 | * @author Roman Pierson 35 | * 36 | */ 37 | public class RequestElementTest { 38 | 39 | final String httpVersion = "HTTP_1_1"; 40 | final Map valuesWithQuery = getAllValues("uri-value", "method-value", "query-value", httpVersion); 41 | final Map valuesWithoutQuery = getAllValues("uri-value", "method-value", null, httpVersion); 42 | 43 | /** 44 | @Test 45 | public void testFindInRawPatternInvalidInputNull(){ 46 | 47 | assertThrows(IllegalArgumentException.class, 48 | ()->{ 49 | new RequestElement().findInRawPattern(null); 50 | }); 51 | 52 | 53 | } 54 | 55 | @Test 56 | public void testFindInRawPatternInvalidInputEmpty(){ 57 | 58 | assertThrows(IllegalArgumentException.class, 59 | ()->{ 60 | new RequestElement().findInRawPattern(""); 61 | }); 62 | 63 | } 64 | 65 | @Test 66 | public void testFindInRawPatternApacheFirstRequestLine(){ 67 | 68 | final String expectedOutputWithQuery = "method-value uri-value?query-value " + VersionUtility.getFormattedValue(new JsonObject(valuesWithQuery)); 69 | final String expectedOutputWithoutQuery = "method-value uri-value " + VersionUtility.getFormattedValue(new JsonObject(valuesWithQuery)); 70 | 71 | ExtractedPosition ep = new RequestElement().findInRawPattern("%r").iterator().next(); 72 | assertNotNull(ep); 73 | assertEquals(0, ep.getStart()); 74 | assertEquals("%r".length(), ep.getOffset()); 75 | 76 | // Test if the output of the looked up element is correct 77 | assertEquals(expectedOutputWithQuery, ep.getElementSupplier().get().getFormattedValue(new JsonObject(valuesWithQuery))); 78 | assertEquals(expectedOutputWithoutQuery, ep.getElementSupplier().get().getFormattedValue(new JsonObject(valuesWithoutQuery))); 79 | } 80 | 81 | @Test 82 | public void testFindInRawPatternURI(){ 83 | 84 | final String expectedOutput = "uri-value"; 85 | 86 | ExtractedPosition ep1 = new RequestElement().findInRawPattern("%U").iterator().next(); 87 | assertNotNull(ep1); 88 | assertEquals(0, ep1.getStart()); 89 | assertEquals("%U".length(), ep1.getOffset()); 90 | 91 | // Test if the output of the looked up element is correct 92 | assertEquals(expectedOutput, ep1.getElementSupplier().get().getFormattedValue(new JsonObject(valuesWithQuery))); 93 | 94 | ExtractedPosition ep2 = new RequestElement().findInRawPattern("cs-uri-stem").iterator().next(); 95 | assertNotNull(ep2); 96 | assertEquals(0, ep2.getStart()); 97 | assertEquals("cs-uri-stem".length(), ep2.getOffset()); 98 | 99 | // Test if the output of the looked up element is correct 100 | assertEquals(expectedOutput, ep2.getElementSupplier().get().getFormattedValue(new JsonObject(valuesWithQuery))); 101 | 102 | } 103 | **/ 104 | 105 | private Map getAllValues(final String uriValue, final String methodValue, final String queryValue, final String versionValue){ 106 | 107 | Map values = new HashMap<>(); 108 | 109 | values.put(Data.Type.URI.getFieldName(), uriValue); 110 | values.put(Data.Type.VERSION.getFieldName(), versionValue); 111 | values.put(Data.Type.QUERY.getFieldName(), queryValue); 112 | values.put(Data.Type.METHOD.getFieldName(), methodValue); 113 | 114 | return values; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/test/java/com/romanpierson/vertx/web/accesslogger/element/ElementTest.java: -------------------------------------------------------------------------------- 1 | package com.romanpierson.vertx.web.accesslogger.element; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | import static org.junit.jupiter.api.Assertions.assertNotNull; 5 | import static org.junit.jupiter.api.Assertions.assertTrue; 6 | 7 | import java.io.PrintStream; 8 | 9 | import org.junit.jupiter.api.Test; 10 | import org.junit.jupiter.api.extension.ExtendWith; 11 | 12 | import com.romanpierson.vertx.test.verticle.SimpleJsonResponseVerticle; 13 | import com.romanpierson.vertx.web.accesslogger.verticle.AccessLoggerProducerVerticle; 14 | 15 | import io.vertx.core.Vertx; 16 | import io.vertx.core.http.HttpClient; 17 | import io.vertx.core.http.HttpClientResponse; 18 | import io.vertx.core.http.HttpMethod; 19 | import io.vertx.core.json.JsonArray; 20 | import io.vertx.core.json.JsonObject; 21 | import io.vertx.ext.web.client.WebClient; 22 | import io.vertx.junit5.VertxExtension; 23 | import io.vertx.junit5.VertxTestContext; 24 | 25 | @ExtendWith(VertxExtension.class) 26 | class ElementTest { 27 | 28 | /** 29 | * 30 | * Tests all out of the box elements if they create the expected values in the log when we have an empty response 31 | * 32 | * Using Event Bus Appender as its easiest to validate 33 | * 34 | * @param vertx 35 | * @param testContext 36 | */ 37 | 38 | @Test 39 | void testWithEmptyResponse(Vertx vertx, VertxTestContext testContext) { 40 | 41 | vertx.exceptionHandler(throwable -> { 42 | testContext.failNow(throwable); 43 | }); 44 | 45 | vertx.eventBus().consumer("target", message -> { 46 | 47 | assertEquals(2, message.body().size()); 48 | assertEquals("-", message.body().getString(0)); 49 | assertEquals(0, message.body().getLong(1)); 50 | 51 | testContext.completeNow(); 52 | }); 53 | 54 | vertx 55 | .deployVerticle(new AccessLoggerProducerVerticle()) 56 | .onComplete(testContext.succeeding(deploymentId -> { 57 | vertx 58 | .deployVerticle(new SimpleJsonResponseVerticle("accesslog-config-elements-empty-result.yaml")) 59 | .onComplete(testContext.succeeding(deploymentId2 -> { 60 | 61 | WebClient client = WebClient.create(vertx); 62 | 63 | // Just to fix github actions issue 64 | try { 65 | Thread.sleep(1000); 66 | } catch (InterruptedException e) { 67 | // we dont care 68 | } 69 | 70 | client 71 | .request(HttpMethod.GET, 8080, "localhost", "/empty") 72 | .send() 73 | .onComplete(testContext.succeeding(buffer -> testContext.verify(() -> { 74 | 75 | }))); 76 | 77 | })); 78 | })); 79 | 80 | 81 | } 82 | 83 | /** 84 | * 85 | * Tests all out of the box elements if they create the expected values in the log when we have a non empty response 86 | * 87 | * Using Event Bus Appender as its easiest to validate 88 | * 89 | * @param vertx 90 | * @param testContext 91 | */ 92 | @Test 93 | void testWithNonEmptyResponse(Vertx vertx, VertxTestContext testContext) { 94 | 95 | vertx.exceptionHandler(throwable -> { 96 | testContext.failNow(throwable); 97 | }); 98 | 99 | vertx.eventBus().consumer("target", message -> { 100 | 101 | assertEquals(32, message.body().size()); 102 | 103 | assertEquals(33, message.body().getLong(0)); 104 | assertEquals(33, message.body().getLong(1)); 105 | assertTrue(message.body().getLong(2) >= 0); 106 | assertTrue(message.body().getLong(3) >= 0); 107 | assertEquals(200, message.body().getInteger(4)); 108 | assertEquals(200, message.body().getInteger(5)); 109 | assertEquals("HTTP/1.1", message.body().getString(6)); 110 | assertNotNull(message.body().getString(7)); 111 | assertEquals("localhost", message.body().getString(8)); 112 | assertEquals("8080", message.body().getString(9)); 113 | assertEquals("GET", message.body().getString(10)); 114 | assertEquals("GET", message.body().getString(11)); 115 | assertEquals("GET /nonEmpty?foo=bar HTTP/1.1", message.body().getString(12)); 116 | assertEquals("/nonEmpty?foo=bar", message.body().getString(13)); 117 | assertEquals("/nonEmpty", message.body().getString(14)); 118 | assertEquals("/nonEmpty", message.body().getString(15)); 119 | assertEquals("?foo=bar", message.body().getString(16)); 120 | assertEquals("?foo=bar", message.body().getString(17)); 121 | // "Wed, 01 Feb 2023 20:19:14 GMT" 122 | // 858585858 123 | assertEquals("shouldShow", message.body().getString(20)); 124 | assertEquals("xy", message.body().getString(21)); 125 | assertEquals("envVal1", message.body().getString(22)); 126 | assertEquals("header1val", message.body().getString(23)); 127 | assertEquals("header1val", message.body().getString(24)); 128 | assertEquals(null, message.body().getString(25)); 129 | assertEquals("application/json; charset=utf-8", message.body().getString(26)); 130 | assertEquals("application/json; charset=utf-8", message.body().getString(27)); 131 | assertEquals(null, message.body().getString(28)); 132 | assertEquals("cookie1Value", message.body().getString(29)); 133 | assertEquals("cookie2Value", message.body().getString(30)); 134 | assertEquals(null, message.body().getString(31)); 135 | 136 | testContext.completeNow(); 137 | }); 138 | 139 | vertx 140 | .deployVerticle(new AccessLoggerProducerVerticle()) 141 | .onComplete(testContext.succeeding(deploymentId -> { 142 | vertx 143 | .deployVerticle(new SimpleJsonResponseVerticle("accesslog-config-elements-result.yaml")) 144 | .onComplete(testContext.succeeding(deploymentId2 -> { 145 | 146 | WebClient client = WebClient.create(vertx); 147 | 148 | // Just to fix github actions issue 149 | try { 150 | Thread.sleep(1000); 151 | } catch (InterruptedException e) { 152 | // we dont care 153 | } 154 | 155 | client 156 | .request(HttpMethod.GET, 8080, "localhost", "/nonEmpty?foo=bar") 157 | .putHeader("header1", "header1val") 158 | .putHeader("Cookie", "cookie1=cookie1Value; cookie2=cookie2Value") 159 | .send() 160 | .onComplete(testContext.succeeding(buffer -> testContext.verify(() -> { 161 | 162 | }))); 163 | 164 | })); 165 | })); 166 | 167 | } 168 | 169 | } 170 | -------------------------------------------------------------------------------- /src/test/java/com/romanpierson/vertx/web/accesslogger/util/FormatUtilityTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016-2025 Roman Pierson 3 | * ------------------------------------------------------ 4 | * All rights reserved. This program and the accompanying materials 5 | * are made available under the terms of the Apache License v2.0 6 | * which accompanies this distribution. 7 | * 8 | * The Apache License v2.0 is available at 9 | * http://www.opensource.org/licenses/apache2.0.php 10 | * 11 | * You may elect to redistribute this code under either of these licenses. 12 | */ 13 | package com.romanpierson.vertx.web.accesslogger.util; 14 | 15 | import static org.junit.jupiter.api.Assertions.assertEquals; 16 | import static org.junit.jupiter.api.Assertions.assertNull; 17 | 18 | import org.junit.jupiter.api.Test; 19 | 20 | import io.vertx.core.json.JsonObject; 21 | 22 | /** 23 | * 24 | * Tests {@link FormatUtility} 25 | * 26 | * @author Roman Pierson 27 | * 28 | */ 29 | public class FormatUtilityTest { 30 | 31 | @Test 32 | public void test() { 33 | 34 | JsonObject values = new JsonObject() 35 | .put("myInteger", Integer.valueOf(1)) 36 | .put("myString", "foo"); 37 | 38 | assertEquals("1", FormatUtility.getIntegerOrNull(values, "myInteger")); 39 | assertNull(FormatUtility.getIntegerOrNull(values, "myIntegerThatDoesNotExists")); 40 | assertEquals("foo", FormatUtility.getStringOrNull(values, "myString")); 41 | assertNull(FormatUtility.getStringOrNull(values, "myStringThatDoesNotExists")); 42 | 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/test/java/com/romanpierson/vertx/web/accesslogger/util/VersionUtilityTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016-2025 Roman Pierson 3 | * ------------------------------------------------------ 4 | * All rights reserved. This program and the accompanying materials 5 | * are made available under the terms of the Apache License v2.0 6 | * which accompanies this distribution. 7 | * 8 | * The Apache License v2.0 is available at 9 | * http://www.opensource.org/licenses/apache2.0.php 10 | * 11 | * You may elect to redistribute this code under either of these licenses. 12 | */ 13 | package com.romanpierson.vertx.web.accesslogger.util; 14 | 15 | import static org.junit.jupiter.api.Assertions.assertEquals; 16 | import static org.junit.jupiter.api.Assertions.assertNull; 17 | 18 | import org.junit.jupiter.api.Test; 19 | 20 | import io.vertx.core.json.JsonObject; 21 | 22 | /** 23 | * 24 | * Tests {@link VersionUtility} 25 | * 26 | * @author Roman Pierson 27 | * 28 | */ 29 | public class VersionUtilityTest { 30 | 31 | @Test 32 | public void testExtractsCorrectVersion() { 33 | 34 | final String version10 = "HTTP/1.0"; 35 | final String version11 = "HTTP/1.1"; 36 | final String version20 = "HTTP/2.0"; 37 | 38 | 39 | assertEquals(version10, VersionUtility.getFormattedValue(new JsonObject().put("version", "HTTP_1_0"))); 40 | assertEquals(version11, VersionUtility.getFormattedValue(new JsonObject().put("version", "HTTP_1_1"))); 41 | assertEquals(version20, VersionUtility.getFormattedValue(new JsonObject().put("version", "HTTP_2"))); 42 | assertEquals("foo", VersionUtility.getFormattedValue(new JsonObject().put("version", "foo"))); 43 | assertNull(VersionUtility.getFormattedValue(new JsonObject().put("version", ""))); 44 | assertNull(VersionUtility.getFormattedValue(new JsonObject().putNull("version"))); 45 | 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/test/resources/META-INF/services/com.romanpierson.vertx.web.accesslogger.configuration.element.AccessLogElement: -------------------------------------------------------------------------------- 1 | com.romanpierson.vertx.web.accesslogger.configuration.element.impl.BytesSentElement 2 | com.romanpierson.vertx.web.accesslogger.configuration.element.impl.CookieElement 3 | com.romanpierson.vertx.web.accesslogger.configuration.element.impl.DateTimeElement 4 | com.romanpierson.vertx.web.accesslogger.configuration.element.impl.DurationElement 5 | com.romanpierson.vertx.web.accesslogger.configuration.element.impl.HeaderElement 6 | com.romanpierson.vertx.web.accesslogger.configuration.element.impl.HostElement 7 | com.romanpierson.vertx.web.accesslogger.configuration.element.impl.MethodElement 8 | com.romanpierson.vertx.web.accesslogger.configuration.element.impl.RequestElement 9 | com.romanpierson.vertx.web.accesslogger.configuration.element.impl.StatusElement 10 | com.romanpierson.vertx.web.accesslogger.configuration.element.impl.VersionElement 11 | com.romanpierson.vertx.web.accesslogger.configuration.element.impl.StaticValueElement 12 | com.romanpierson.vertx.web.accesslogger.configuration.element.impl.EnvironmentValueElement 13 | com.romanpierson.vertx.web.accesslogger.configuration.element.impl.ShouldNotShowElement 14 | com.romanpierson.custom.element.ShouldShowElement 15 | com.romanpierson.custom.element.ShouldNotShowElement -------------------------------------------------------------------------------- /src/test/resources/accesslog-config-all-valid-console-appender-autocreation-verticle.yaml: -------------------------------------------------------------------------------- 1 | isAutoDeployProducerVerticle: true 2 | configurations: 3 | - identifier: accesslog-formatted 4 | logPattern: '%{}t %D %T "cs-uri" %h %v %p %b %B %{header-in}i %{header-out}o %{my-cookie}C %{my-static-value}static' 5 | appenders: 6 | - appenderClassName : com.romanpierson.vertx.web.accesslogger.appender.console.impl.ConsoleAppender -------------------------------------------------------------------------------- /src/test/resources/accesslog-config-all-valid-console-appender.yaml: -------------------------------------------------------------------------------- 1 | isAutoDeployProducerVerticle: false 2 | configurations: 3 | - identifier: accesslog-formatted 4 | logPattern: '%{}t %D %T "cs-uri" %h %v %p %b %B %{header-in}i %{header-out}o %{my-cookie}C %{my-static-value}static %{my-env}env' 5 | appenders: 6 | - appenderClassName : com.romanpierson.vertx.web.accesslogger.appender.console.impl.ConsoleAppender -------------------------------------------------------------------------------- /src/test/resources/accesslog-config-all-valid-memory-appender.yaml: -------------------------------------------------------------------------------- 1 | isAutoDeployProducerVerticle: false 2 | configurations: 3 | - identifier: accesslog-formatted 4 | logPattern: '%{}t %D %T "cs-uri" %s %h %v %p %b %B %{header-in}i %{header-out}o %{my-cookie}C %{my-static-value}static' 5 | appenders: 6 | - appenderClassName : com.romanpierson.vertx.web.accesslogger.appender.MemoryAppender -------------------------------------------------------------------------------- /src/test/resources/accesslog-config-console-appender-valid.yaml: -------------------------------------------------------------------------------- 1 | isAutoDeployProducerVerticle: false 2 | configurations: 3 | - identifier: accesslog-plain 4 | logPattern: "cs-uri" 5 | appenders: 6 | - appenderClassName : com.romanpierson.vertx.web.accesslogger.appender.console.impl.ConsoleAppender -------------------------------------------------------------------------------- /src/test/resources/accesslog-config-elasticsearch-appender-invalid-fieldnames.yaml: -------------------------------------------------------------------------------- 1 | isAutoDeployProducerVerticle: false 2 | configurations: 3 | - identifier: accesslog-formatted 4 | logPattern: '"%{msec}t cs-uri"' 5 | appenders: 6 | - appenderClassName : com.romanpierson.vertx.web.accesslogger.appender.elasticsearch.impl.ElasticSearchAppender 7 | config: 8 | instanceIdentifier: esInstance 9 | fieldNamesINVALID: 10 | - timestamp 11 | - uri -------------------------------------------------------------------------------- /src/test/resources/accesslog-config-elasticsearch-appender-invalid-instance.yaml: -------------------------------------------------------------------------------- 1 | isAutoDeployProducerVerticle: false 2 | configurations: 3 | - identifier: accesslog-formatted 4 | logPattern: '"%{msec}t cs-uri"' 5 | appenders: 6 | - appenderClassName : com.romanpierson.vertx.web.accesslogger.appender.elasticsearch.impl.ElasticSearchAppender 7 | config: 8 | instanceIdentifierINVALID: esInstance 9 | fieldNames: 10 | - timestamp 11 | - uri -------------------------------------------------------------------------------- /src/test/resources/accesslog-config-elasticsearch-appender-valid.yaml: -------------------------------------------------------------------------------- 1 | isAutoDeployProducerVerticle: false 2 | configurations: 3 | - identifier: accesslog-formatted 4 | logPattern: 'cs-uri %D' 5 | appenders: 6 | - appenderClassName : com.romanpierson.vertx.web.accesslogger.appender.elasticsearch.impl.ElasticSearchAppender 7 | config: 8 | instanceIdentifier: esInstance 9 | fieldNames: 10 | - uri 11 | - duration -------------------------------------------------------------------------------- /src/test/resources/accesslog-config-elements-empty-result.yaml: -------------------------------------------------------------------------------- 1 | isAutoDeployProducerVerticle: false 2 | configurations: 3 | - identifier: accesslog-formatted 4 | logPattern: '%b %B' 5 | appenders: 6 | - appenderClassName : com.romanpierson.vertx.web.accesslogger.appender.eventbus.impl.EventBusAppender 7 | config: 8 | targetAddress: target -------------------------------------------------------------------------------- /src/test/resources/accesslog-config-elements-result.yaml: -------------------------------------------------------------------------------- 1 | isAutoDeployProducerVerticle: false 2 | configurations: 3 | - identifier: accesslog-formatted 4 | logPattern: '%b %B %D %T sc-status %s %H %h %v %p cs-method %m %r cs-uri %U cs-uri-stem %q cs-uri-query %t %{msec}t %x %{xy}static %{envVar1}env %{header1}i %{heADeR1}i %{header2}i %{Content-Type}o %{CoNTent-tyPe}o %{header3}o %{cookie1}C %{cookie2}C %{cookieFoo}C' 5 | appenders: 6 | - appenderClassName : com.romanpierson.vertx.web.accesslogger.appender.eventbus.impl.EventBusAppender 7 | config: 8 | targetAddress: target -------------------------------------------------------------------------------- /src/test/resources/accesslog-config-eventbus-appender-existing-address.yaml: -------------------------------------------------------------------------------- 1 | isAutoDeployProducerVerticle: false 2 | configurations: 3 | - identifier: accesslog-formatted 4 | logPattern: '"cs-uri"' 5 | appenders: 6 | - appenderClassName : com.romanpierson.vertx.web.accesslogger.appender.eventbus.impl.EventBusAppender 7 | config: 8 | targetAddress: shangriLa -------------------------------------------------------------------------------- /src/test/resources/accesslog-config-eventbus-appender-inexisting-address.yaml: -------------------------------------------------------------------------------- 1 | isAutoDeployProducerVerticle: false 2 | configurations: 3 | - identifier: accesslog-formatted 4 | logPattern: '%{}t %D %T "cs-uri" %h %v %p %b %B %{header-in}i %{header-out}o %{my-cookie}C %{my-static-value}static' 5 | appenders: 6 | - appenderClassName : com.romanpierson.vertx.web.accesslogger.appender.eventbus.impl.EventBusAppender 7 | config: 8 | targetAddress: nirvana -------------------------------------------------------------------------------- /src/test/resources/accesslog-config-eventbus-appender-invalid-config.yaml: -------------------------------------------------------------------------------- 1 | isAutoDeployProducerVerticle: false 2 | configurations: 3 | - identifier: accesslog-formatted 4 | logPattern: '%{}t %D %T "cs-uri" %h %v %p %b %B %{header-in}i %{header-out}o %{my-cookie}C %{my-static-value}static' 5 | appenders: 6 | - appenderClassName : com.romanpierson.vertx.web.accesslogger.appender.eventbus.impl.EventBusAppender -------------------------------------------------------------------------------- /src/test/resources/accesslog-config-invalid-console-appender.yaml: -------------------------------------------------------------------------------- 1 | isAutoDeployProducerVerticle: false 2 | configurations: 3 | - identifier: accesslog-formatted 4 | logPatternINVALID: '%{}t %D %T "cs-uri" %h %v %p %b %B %{header-in}i %{header-out}o %{my-cookie}C %{my-static-value}static' 5 | appenders: 6 | - appenderClassName : com.romanpierson.vertx.web.accesslogger.appender.console.impl.ConsoleAppender -------------------------------------------------------------------------------- /src/test/resources/accesslog-config-invalid.yaml: -------------------------------------------------------------------------------- 1 | isAutoDeployProducerVerticle: false 2 | configurationsInvalid: 3 | - identifier: accesslog-formatted 4 | logPattern: '%{}t %D %T "cs-uri" %h %v %p %b %B %{header-in}i %{header-out}o %{my-cookie}C %{my-static-value}static' 5 | appenders: 6 | - appenderClassName : com.romanpierson.vertx.web.accesslogger.appender.console.impl.ConsoleAppender -------------------------------------------------------------------------------- /src/test/resources/accesslog-config-logging-appender-invalid-loggername.yaml: -------------------------------------------------------------------------------- 1 | isAutoDeployProducerVerticle: false 2 | configurations: 3 | - identifier: accesslog-plain 4 | logPattern: "cs-uri" 5 | appenders: 6 | - appenderClassName : com.romanpierson.vertx.web.accesslogger.appender.logging.impl.LoggingAppender -------------------------------------------------------------------------------- /src/test/resources/accesslog-config-logging-appender-invalid-logpattern.yaml: -------------------------------------------------------------------------------- 1 | isAutoDeployProducerVerticle: false 2 | configurations: 3 | - identifier: accesslog-plain 4 | logPatternINVALID: "cs-uri" 5 | appenders: 6 | - appenderClassName : com.romanpierson.vertx.web.accesslogger.appender.logging.impl.LoggingAppender -------------------------------------------------------------------------------- /src/test/resources/accesslog-config-logging-appender-valid.yaml: -------------------------------------------------------------------------------- 1 | isAutoDeployProducerVerticle: false 2 | configurations: 3 | - identifier: accesslog-plain 4 | logPattern: "cs-uri" 5 | appenders: 6 | - appenderClassName : com.romanpierson.vertx.web.accesslogger.appender.logging.impl.LoggingAppender 7 | config: 8 | loggerName: accesslog -------------------------------------------------------------------------------- /src/test/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | %msg 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | --------------------------------------------------------------------------------