diff --git a/.github/README.md b/.github/README.md new file mode 100644 index 0000000..8875cce --- /dev/null +++ b/.github/README.md @@ -0,0 +1,11 @@ +# Micronaut Logging support + +Enhanced logging for Micronaut using MDC or request header. + +[![maven](https://img.shields.io/maven-central/v/io.kokuwa.micronaut/micronaut-logging.svg?label=maven)](https://central.sonatype.com/artifact/io.kokuwa.micronaut/micronaut-logging) +[![license](https://img.shields.io/badge/license-EUPL%201.2-blue)](https://git.kokuwa.io/kokuwaio/micronaut-logging/src/branch/main/LICENSE) +[![issues](https://img.shields.io/gitea/issues/open/kokuwaio/micronaut-logging?gitea_url=https%3A%2F%2Fgit.kokuwa.io)](https://git.kokuwa.io/kokuwaio/micronaut-logging/issues) +[![prs](https://img.shields.io/gitea/pull-requests/open/kokuwaio/micronaut-logging?gitea_url=https%3A%2F%2Fgit.kokuwa.io)](https://git.kokuwa.io/kokuwaio/micronaut-logging/pulls) +[![build](https://ci.kokuwa.io/api/badges/kokuwaio/micronaut-logging/status.svg)](https://ci.kokuwa.io/repos/kokuwaio/micronaut-logging/) + +For more documention see: [git.kokuwa.io/kokuwaio/micronaut-logging](https://git.kokuwa.io/kokuwaio/micronaut-logging) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index dec1718..0000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,34 +0,0 @@ -name: PullRequest - -"on": [pull_request] - -jobs: - - lint-yaml: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: ibiqlik/action-yamllint@v3 - with: - format: colored - strict: true - - lint-markdown: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: docker://avtodev/markdown-lint:v1 - with: - args: /github/workspace - - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: actions/setup-java@v2 - with: - distribution: temurin - java-version: 11 - cache: maven - - run: mvn -B dependency:go-offline -q - - run: mvn -B verify diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index 3f13993..0000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,53 +0,0 @@ -name: Release - -"on": - workflow_dispatch: {} - -jobs: - - lint-yaml: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: ibiqlik/action-yamllint@v3 - with: - format: colored - strict: true - - lint-markdown: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: docker://avtodev/markdown-lint:v1 - with: - args: /github/workspace - - build: - runs-on: ubuntu-latest - needs: - - lint-markdown - - lint-yaml - steps: - - uses: actions/checkout@v2 - with: - token: ${{ secrets.GIT_ACTION_TOKEN }} - - name: git-configure - run: | - git config user.email "actions@github.com" - git config user.name "GitHub Actions" - - uses: actions/setup-java@v2 - with: - distribution: temurin - java-version: 11 - server-id: sonatype-nexus - server-username: SERVER_USERNAME - server-password: SERVER_PASSWORD - gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }} - gpg-passphrase: GPG_PASSPHRASE - - run: mvn -B dependency:go-offline -q - - run: mvn -B release:prepare - - run: mvn -B release:perform -DreleaseProfiles=oss-release - env: - SERVER_USERNAME: ${{ secrets.SONATYPE_NEXUS_USERNAME }} - SERVER_PASSWORD: ${{ secrets.SONATYPE_NEXUS_PASSWORD }} - GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} diff --git a/.github/workflows/snapshot.yml b/.github/workflows/snapshot.yml deleted file mode 100644 index ab9bf69..0000000 --- a/.github/workflows/snapshot.yml +++ /dev/null @@ -1,48 +0,0 @@ -name: Snapshot - -"on": - workflow_dispatch: {} - push: - branches: - - main - - '*.x' - -jobs: - - lint-yaml: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: ibiqlik/action-yamllint@v3 - with: - format: colored - strict: true - - lint-markdown: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: docker://avtodev/markdown-lint:v1 - with: - args: /github/workspace - - build: - runs-on: ubuntu-latest - needs: - - lint-markdown - - lint-yaml - steps: - - uses: actions/checkout@v2 - - uses: actions/setup-java@v2 - with: - distribution: temurin - java-version: 11 - cache: maven - server-id: sonatype-nexus - server-username: SERVER_USERNAME - server-password: SERVER_PASSWORD - - run: mvn -B dependency:go-offline -q - - run: mvn -B deploy -Poss - env: - SERVER_USERNAME: ${{ secrets.SONATYPE_NEXUS_USERNAME }} - SERVER_PASSWORD: ${{ secrets.SONATYPE_NEXUS_PASSWORD }} diff --git a/.justfile b/.justfile new file mode 100644 index 0000000..81f407b --- /dev/null +++ b/.justfile @@ -0,0 +1,12 @@ +# https://just.systems/man/en/ + +[private] +@default: + just --list --unsorted + +# Run linter. +@lint: + docker run --rm --read-only --volume=$(pwd):$(pwd):ro --workdir=$(pwd) kokuwaio/yamllint + docker run --rm --read-only --volume=$(pwd):$(pwd):rw --workdir=$(pwd) kokuwaio/markdownlint --fix + docker run --rm --read-only --volume=$(pwd):$(pwd):ro --workdir=$(pwd) kokuwaio/renovate-config-validator + docker run --rm --read-only --volume=$(pwd):$(pwd):ro --workdir=$(pwd) woodpeckerci/woodpecker-cli lint diff --git a/.markdownlint.yaml b/.markdownlint.yaml index dd23793..5f08047 100644 --- a/.markdownlint.yaml +++ b/.markdownlint.yaml @@ -1,6 +1,9 @@ # Default state for all rules default: true -# MD013/line-length - Line length -MD013: - line_length: 10000 +# MD009 - Trailing spaces +MD009: + strict: true + +# MD013 - Line length +MD013: false diff --git a/.woodpecker/deploy.yaml b/.woodpecker/deploy.yaml new file mode 100644 index 0000000..11f903e --- /dev/null +++ b/.woodpecker/deploy.yaml @@ -0,0 +1,16 @@ +when: + instance: ci.kokuwa.io + repo: kokuwaio/micronaut-logging + event: [manual, push] + branch: main + path: [.woodpecker/deploy.yaml, pom.xml, src/main/**] + +steps: + + maven: + image: maven:3.9.10-eclipse-temurin-17 + commands: mvn deploy --settings=.woodpecker/maven/settings.xml + environment: + MAVEN_GPG_KEY: {from_secret: woodpecker_gpg_key} + SONATYPE_ORG_USERNAME: {from_secret: sonatype_org_username} + SONATYPE_ORG_PASSWORD: {from_secret: sonatype_org_password} diff --git a/.woodpecker/lint.yaml b/.woodpecker/lint.yaml new file mode 100644 index 0000000..74bb114 --- /dev/null +++ b/.woodpecker/lint.yaml @@ -0,0 +1,21 @@ +when: + event: [manual, pull_request, push] + branch: main + path: [.woodpecker/lint.yaml, renovate.json, "**/*.y*ml", "**/*.md"] + +steps: + + renovate: + image: kokuwaio/renovate-config-validator + depends_on: [] + when: [path: [.woodpecker/lint.yaml, renovate.json]] + + yaml: + image: kokuwaio/yamllint + depends_on: [] + when: [path: [.woodpecker/lint.yaml, .yamllint.yaml, "**/*.y*ml"]] + + markdown: + image: kokuwaio/markdownlint + depends_on: [] + when: [path: [.woodpecker/lint.yaml, .markdownlint.yaml, "**/*.md"]] diff --git a/.woodpecker/maven/settings.xml b/.woodpecker/maven/settings.xml new file mode 100644 index 0000000..57ad1cb --- /dev/null +++ b/.woodpecker/maven/settings.xml @@ -0,0 +1,23 @@ + + + false + /woodpecker/.m2 + + + git.kokuwa.io + ${env.FORGEJO_USERNAME} + ${env.FORGEJO_PASSWORD} + + + sonatype.org + ${env.SONATYPE_ORG_USERNAME} + ${env.SONATYPE_ORG_PASSWORD} + + + + + http://mirror.woodpecker.svc.cluster.local/maven2 + central + + + diff --git a/.woodpecker/release.yaml b/.woodpecker/release.yaml new file mode 100644 index 0000000..21a190e --- /dev/null +++ b/.woodpecker/release.yaml @@ -0,0 +1,29 @@ +when: + instance: ci.kokuwa.io + repo: kokuwaio/micronaut-logging + event: deployment + branch: main + +steps: + + maven: + image: maven:3.9.10-eclipse-temurin-17 + commands: + # setup git with ssk key signing + - git config user.email "$GIT_USER_EMAIL" + - git config user.name "$GIT_USER_NAME" + - git config commit.gpgsign true + - git config gpg.format ssh + - git config user.signingkey /run/secrets/sign.pub + - install -m 400 /dev/null /run/secrets/sign && echo "$GIT_SIGN_KEY" > /run/secrets/sign + - install -m 444 /dev/null /run/secrets/sign.pub && echo "$GIT_SIGN_PUB" > /run/secrets/sign.pub + # release + - mvn release:prepare release:perform --settings=.woodpecker/maven/settings.xml + environment: + MAVEN_GPG_KEY: {from_secret: woodpecker_gpg_key} + GIT_SIGN_KEY: {from_secret: woodpecker_sign_key} + GIT_SIGN_PUB: {from_secret: woodpecker_sign_pub} + FORGEJO_USERNAME: {from_secret: woodpecker_username} + FORGEJO_PASSWORD: {from_secret: woodpecker_password} + SONATYPE_ORG_USERNAME: {from_secret: sonatype_org_username} + SONATYPE_ORG_PASSWORD: {from_secret: sonatype_org_password} diff --git a/.woodpecker/verify.yaml b/.woodpecker/verify.yaml new file mode 100644 index 0000000..094b317 --- /dev/null +++ b/.woodpecker/verify.yaml @@ -0,0 +1,9 @@ +when: + event: [manual, pull_request] + path: [.woodpecker/verify.yaml, pom.xml, src/**] + +steps: + + test: + image: maven:3.9.10-eclipse-temurin-17 + commands: mvn verify --settings=.woodpecker/maven/settings.xml -P-deploy diff --git a/.yamllint b/.yamllint deleted file mode 100644 index c9f12ef..0000000 --- a/.yamllint +++ /dev/null @@ -1,7 +0,0 @@ -extends: default - -## see https://yamllint.readthedocs.io/en/stable/rules.html -rules: - - # no need for document start - document-start: disable diff --git a/.yamllint.yaml b/.yamllint.yaml new file mode 100644 index 0000000..21966f2 --- /dev/null +++ b/.yamllint.yaml @@ -0,0 +1,15 @@ +extends: default + +## see https://yamllint.readthedocs.io/en/stable/rules.html +rules: + + # no need for document start + document-start: disable + + # line length is not important + line-length: disable + + # force double quotes everywhere + quoted-strings: + quote-type: double + required: only-when-needed diff --git a/LICENSE b/LICENSE index 261eeb9..dacd3ae 100644 --- a/LICENSE +++ b/LICENSE @@ -1,201 +1,288 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + EUROPEAN UNION PUBLIC LICENCE v. 1.2 + EUPL © the European Union 2007, 2016 - 1. Definitions. +This European Union Public Licence (the ‘EUPL’) applies to the Work (as defined +below) which is provided under the terms of this Licence. Any use of the Work, +other than as authorised under this Licence is prohibited (to the extent such +use is covered by a right of the copyright holder of the Work). - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. +The Work is provided under the terms of this Licence when the Licensor (as +defined below) has placed the following notice immediately following the +copyright notice for the Work: - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. + Licensed under the EUPL - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. +or has expressed by any other means his willingness to license under the EUPL. - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. +1. Definitions - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. +In this Licence, the following terms have the following meaning: - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. +- ‘The Licence’: this Licence. - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). +- ‘The Original Work’: the work or software distributed or communicated by the + Licensor under this Licence, available as Source Code and also as Executable + Code as the case may be. - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. +- ‘Derivative Works’: the works or software that could be created by the + Licensee, based upon the Original Work or modifications thereof. This Licence + does not define the extent of modification or dependence on the Original Work + required in order to classify a work as a Derivative Work; this extent is + determined by copyright law applicable in the country mentioned in Article 15. - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." +- ‘The Work’: the Original Work or its Derivative Works. - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. +- ‘The Source Code’: the human-readable form of the Work which is the most + convenient for people to study and modify. - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. +- ‘The Executable Code’: any code which has generally been compiled and which is + meant to be interpreted by a computer as a program. - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. +- ‘The Licensor’: the natural or legal person that distributes or communicates + the Work under the Licence. - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: +- ‘Contributor(s)’: any natural or legal person who modifies the Work under the + Licence, or otherwise contributes to the creation of a Derivative Work. - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and +- ‘The Licensee’ or ‘You’: any natural or legal person who makes any usage of + the Work under the terms of the Licence. - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and +- ‘Distribution’ or ‘Communication’: any act of selling, giving, lending, + renting, distributing, communicating, transmitting, or otherwise making + available, online or offline, copies of the Work or providing access to its + essential functionalities at the disposal of any other natural or legal + person. - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and +2. Scope of the rights granted by the Licence - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. +The Licensor hereby grants You a worldwide, royalty-free, non-exclusive, +sublicensable licence to do the following, for the duration of copyright vested +in the Original Work: - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. +- use the Work in any circumstance and for all usage, +- reproduce the Work, +- modify the Work, and make Derivative Works based upon the Work, +- communicate to the public, including the right to make available or display + the Work or copies thereof to the public and perform publicly, as the case may + be, the Work, +- distribute the Work or copies thereof, +- lend and rent the Work or copies thereof, +- sublicense rights in the Work or copies thereof. - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. +Those rights can be exercised on any media, supports and formats, whether now +known or later invented, as far as the applicable law permits so. - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. +In the countries where moral rights apply, the Licensor waives his right to +exercise his moral right to the extent allowed by law in order to make effective +the licence of the economic rights here above listed. - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. +The Licensor grants to the Licensee royalty-free, non-exclusive usage rights to +any patents held by the Licensor, to the extent necessary to make use of the +rights granted on the Work under this Licence. - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. +3. Communication of the Source Code - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. +The Licensor may provide the Work either in its Source Code form, or as +Executable Code. If the Work is provided as Executable Code, the Licensor +provides in addition a machine-readable copy of the Source Code of the Work +along with each copy of the Work that the Licensor distributes or indicates, in +a notice following the copyright notice attached to the Work, a repository where +the Source Code is easily and freely accessible for as long as the Licensor +continues to distribute or communicate the Work. - END OF TERMS AND CONDITIONS +4. Limitations on copyright - APPENDIX: How to apply the Apache License to your work. +Nothing in this Licence is intended to deprive the Licensee of the benefits from +any exception or limitation to the exclusive rights of the rights owners in the +Work, of the exhaustion of those rights or of other applicable limitations +thereto. - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. +5. Obligations of the Licensee - Copyright [yyyy] [name of copyright owner] +The grant of the rights mentioned above is subject to some restrictions and +obligations imposed on the Licensee. Those obligations are the following: - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at +Attribution right: The Licensee shall keep intact all copyright, patent or +trademarks notices and all notices that refer to the Licence and to the +disclaimer of warranties. The Licensee must include a copy of such notices and a +copy of the Licence with every copy of the Work he/she distributes or +communicates. The Licensee must cause any Derivative Work to carry prominent +notices stating that the Work has been modified and the date of modification. - http://www.apache.org/licenses/LICENSE-2.0 +Copyleft clause: If the Licensee distributes or communicates copies of the +Original Works or Derivative Works, this Distribution or Communication will be +done under the terms of this Licence or of a later version of this Licence +unless the Original Work is expressly distributed only under this version of the +Licence — for example by communicating ‘EUPL v. 1.2 only’. The Licensee +(becoming Licensor) cannot offer or impose any additional terms or conditions on +the Work or Derivative Work that alter or restrict the terms of the Licence. - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. +Compatibility clause: If the Licensee Distributes or Communicates Derivative +Works or copies thereof based upon both the Work and another work licensed under +a Compatible Licence, this Distribution or Communication can be done under the +terms of this Compatible Licence. For the sake of this clause, ‘Compatible +Licence’ refers to the licences listed in the appendix attached to this Licence. +Should the Licensee's obligations under the Compatible Licence conflict with +his/her obligations under this Licence, the obligations of the Compatible +Licence shall prevail. + +Provision of Source Code: When distributing or communicating copies of the Work, +the Licensee will provide a machine-readable copy of the Source Code or indicate +a repository where this Source will be easily and freely available for as long +as the Licensee continues to distribute or communicate the Work. + +Legal Protection: This Licence does not grant permission to use the trade names, +trademarks, service marks, or names of the Licensor, except as required for +reasonable and customary use in describing the origin of the Work and +reproducing the content of the copyright notice. + +6. Chain of Authorship + +The original Licensor warrants that the copyright in the Original Work granted +hereunder is owned by him/her or licensed to him/her and that he/she has the +power and authority to grant the Licence. + +Each Contributor warrants that the copyright in the modifications he/she brings +to the Work are owned by him/her or licensed to him/her and that he/she has the +power and authority to grant the Licence. + +Each time You accept the Licence, the original Licensor and subsequent +Contributors grant You a licence to their contributions to the Work, under the +terms of this Licence. + +7. Disclaimer of Warranty + +The Work is a work in progress, which is continuously improved by numerous +Contributors. It is not a finished work and may therefore contain defects or +‘bugs’ inherent to this type of development. + +For the above reason, the Work is provided under the Licence on an ‘as is’ basis +and without warranties of any kind concerning the Work, including without +limitation merchantability, fitness for a particular purpose, absence of defects +or errors, accuracy, non-infringement of intellectual property rights other than +copyright as stated in Article 6 of this Licence. + +This disclaimer of warranty is an essential part of the Licence and a condition +for the grant of any rights to the Work. + +8. Disclaimer of Liability + +Except in the cases of wilful misconduct or damages directly caused to natural +persons, the Licensor will in no event be liable for any direct or indirect, +material or moral, damages of any kind, arising out of the Licence or of the use +of the Work, including without limitation, damages for loss of goodwill, work +stoppage, computer failure or malfunction, loss of data or any commercial +damage, even if the Licensor has been advised of the possibility of such damage. +However, the Licensor will be liable under statutory product liability laws as +far such laws apply to the Work. + +9. Additional agreements + +While distributing the Work, You may choose to conclude an additional agreement, +defining obligations or services consistent with this Licence. However, if +accepting obligations, You may act only on your own behalf and on your sole +responsibility, not on behalf of the original Licensor or any other Contributor, +and only if You agree to indemnify, defend, and hold each Contributor harmless +for any liability incurred by, or claims asserted against such Contributor by +the fact You have accepted any warranty or additional liability. + +10. Acceptance of the Licence + +The provisions of this Licence can be accepted by clicking on an icon ‘I agree’ +placed under the bottom of a window displaying the text of this Licence or by +affirming consent in any other similar way, in accordance with the rules of +applicable law. Clicking on that icon indicates your clear and irrevocable +acceptance of this Licence and all of its terms and conditions. + +Similarly, you irrevocably accept this Licence and all of its terms and +conditions by exercising any rights granted to You by Article 2 of this Licence, +such as the use of the Work, the creation by You of a Derivative Work or the +Distribution or Communication by You of the Work or copies thereof. + +11. Information to the public + +In case of any Distribution or Communication of the Work by means of electronic +communication by You (for example, by offering to download the Work from a +remote location) the distribution channel or media (for example, a website) must +at least provide to the public the information requested by the applicable law +regarding the Licensor, the Licence and the way it may be accessible, concluded, +stored and reproduced by the Licensee. + +12. Termination of the Licence + +The Licence and the rights granted hereunder will terminate automatically upon +any breach by the Licensee of the terms of the Licence. + +Such a termination will not terminate the licences of any person who has +received the Work from the Licensee under the Licence, provided such persons +remain in full compliance with the Licence. + +13. Miscellaneous + +Without prejudice of Article 9 above, the Licence represents the complete +agreement between the Parties as to the Work. + +If any provision of the Licence is invalid or unenforceable under applicable +law, this will not affect the validity or enforceability of the Licence as a +whole. Such provision will be construed or reformed so as necessary to make it +valid and enforceable. + +The European Commission may publish other linguistic versions or new versions of +this Licence or updated versions of the Appendix, so far this is required and +reasonable, without reducing the scope of the rights granted by the Licence. New +versions of the Licence will be published with a unique version number. + +All linguistic versions of this Licence, approved by the European Commission, +have identical value. Parties can take advantage of the linguistic version of +their choice. + +14. Jurisdiction + +Without prejudice to specific agreement between parties, + +- any litigation resulting from the interpretation of this License, arising + between the European Union institutions, bodies, offices or agencies, as a + Licensor, and any Licensee, will be subject to the jurisdiction of the Court + of Justice of the European Union, as laid down in article 272 of the Treaty on + the Functioning of the European Union, + +- any litigation arising between other parties and resulting from the + interpretation of this License, will be subject to the exclusive jurisdiction + of the competent court where the Licensor resides or conducts its primary + business. + +15. Applicable Law + +Without prejudice to specific agreement between parties, + +- this Licence shall be governed by the law of the European Union Member State + where the Licensor has his seat, resides or has his registered office, + +- this licence shall be governed by Belgian law if the Licensor has no seat, + residence or registered office inside a European Union Member State. + +Appendix + +‘Compatible Licences’ according to Article 5 EUPL are: + +- GNU General Public License (GPL) v. 2, v. 3 +- GNU Affero General Public License (AGPL) v. 3 +- Open Software License (OSL) v. 2.1, v. 3.0 +- Eclipse Public License (EPL) v. 1.0 +- CeCILL v. 2.0, v. 2.1 +- Mozilla Public Licence (MPL) v. 2 +- GNU Lesser General Public Licence (LGPL) v. 2.1, v. 3 +- Creative Commons Attribution-ShareAlike v. 3.0 Unported (CC BY-SA 3.0) for + works other than software +- European Union Public Licence (EUPL) v. 1.1, v. 1.2 +- Québec Free and Open-Source Licence — Reciprocity (LiLiQ-R) or Strong + Reciprocity (LiLiQ-R+). + +The European Commission may update this Appendix to later versions of the above +licences without producing a new version of the EUPL, as long as they provide +the rights granted in Article 2 of this Licence and protect the covered Source +Code from exclusive appropriation. + +All other changes or additions to this Appendix require the production of a new +EUPL version. diff --git a/README.md b/README.md index 620914e..4a3a2ec 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,42 @@ # Micronaut Logging support +Enhanced logging for Micronaut using MDC or request header. + +[![maven](https://img.shields.io/maven-central/v/io.kokuwa.micronaut/micronaut-logging.svg?label=maven)](https://central.sonatype.com/artifact/io.kokuwa.micronaut/micronaut-logging) +[![license](https://img.shields.io/badge/license-EUPL%201.2-blue)](https://git.kokuwa.io/kokuwaio/micronaut-logging/src/branch/main/LICENSE) +[![issues](https://img.shields.io/gitea/issues/open/kokuwaio/micronaut-logging?gitea_url=https%3A%2F%2Fgit.kokuwa.io)](https://git.kokuwa.io/kokuwaio/micronaut-logging/issues) +[![prs](https://img.shields.io/gitea/pull-requests/open/kokuwaio/micronaut-logging?gitea_url=https%3A%2F%2Fgit.kokuwa.io)](https://git.kokuwa.io/kokuwaio/micronaut-logging/pulls) +[![build](https://ci.kokuwa.io/api/badges/kokuwaio/micronaut-logging/status.svg)](https://ci.kokuwa.io/repos/kokuwaio/micronaut-logging/) + +Include in your `pom.xml`: + +```xml + + io.kokuwa.micronaut + micronaut-logging + ${version.io.kokuwa.micronaut.logging} + runtime + + + + io.micronaut.serde + micronaut-serde-jsonp + runtime + +``` + ## Features +* Version [3.x](https://github.com/kokuwaio/micronaut-logging/tree/3.x) is based on SLF4J 1.7 & Logback 1.2 & Micronaut 3.x +* Version [4.x](https://github.com/kokuwaio/micronaut-logging/tree/main) is based on SLF4J 2.0 & Logback 1.4 & Micronaut 4.x * [set log level based on MDC values](docs/features/logback_mdc_level.md) * [add default xml](docs/features/logback_default.md) * [preconfigured appender for different environments](docs/features/logback_appender.md) * [set log level based on HTTP request header](docs/features/http_log_level.md) -* [add HTTP headers to MDC](docs/features/http_mdc_headers.md) +* [add HTTP path parts to MDC](docs/features/http_mdc_path.md) +* [add HTTP header to MDC](docs/features/http_mdc_header.md) * [add authentication information from HTTP request to MDC](docs/features/http_mdc_authentication.md) -## Development - -* [build](docs/build.md) - ## Open Topics * configure mdc on refresh event diff --git a/docs/build.md b/docs/build.md deleted file mode 100644 index 4d5c72e..0000000 --- a/docs/build.md +++ /dev/null @@ -1,23 +0,0 @@ -# Build & Release - -## Dependency updates - -Display dependency updates: - -```sh -mvn versions:display-parent-updates versions:display-property-updates -U -``` - -Update dependencies: - -```sh -mvn versions:update-parent versions:update-properties -``` - -## Release locally - -Run: - -```sh -mvn release:prepare release:perform release:clean -B -DreleaseProfiles=oss-release -``` diff --git a/docs/features/http_mdc_header.md b/docs/features/http_mdc_header.md new file mode 100644 index 0000000..8a8e9d4 --- /dev/null +++ b/docs/features/http_mdc_header.md @@ -0,0 +1,28 @@ +# Add HTTP headers to MDC + +## Properties + +Property | Description | Default +-------- | ----------- | ------- +`logger.http.header.enabled` | filter enabled? | `true` +`logger.http.header.path` | filter path | `/**` +`logger.http.header.order` | order for [Ordered](https://github.com/micronaut-projects/micronaut-core/blob/v3.2.0/core/src/main/java/io/micronaut/core/order/Ordered.java) | [ServerFilterPhase.FIRST.before()](https://github.com/micronaut-projects/micronaut-core/blob/v3.2.0/http/src/main/java/io/micronaut/http/filter/ServerFilterPhase.java#L34) +`logger.http.header.prefix` | prefix to MDC key | `` +`logger.http.header.names` | http header names to add to MDC | `[]` + +## Examples + +Configuration for b3-propagation: + +```yaml +logger: + http: + header: + prefix: header. + names: + - x-request-id + - x-b3-traceId + - x-b3-parentspanid + - x-b3-spanid + - x-b3-sampled +``` diff --git a/docs/features/http_mdc_headers.md b/docs/features/http_mdc_headers.md deleted file mode 100644 index b6c031a..0000000 --- a/docs/features/http_mdc_headers.md +++ /dev/null @@ -1,28 +0,0 @@ -# Add HTTP headers to MDC - -## Properties - -Property | Description | Default --------- | ----------- | ------- -`logger.http.headers.enabled` | filter enabled? | `true` -`logger.http.headers.path` | filter path | `/**` -`logger.http.headers.order` | order for [Ordered](https://github.com/micronaut-projects/micronaut-core/blob/v3.2.0/core/src/main/java/io/micronaut/core/order/Ordered.java) | [ServerFilterPhase.FIRST.before()](https://github.com/micronaut-projects/micronaut-core/blob/v3.2.0/http/src/main/java/io/micronaut/http/filter/ServerFilterPhase.java#L34) -`logger.http.headers.prefix` | prefix to MDC key | `` -`logger.http.headers.names` | http header names to add to MDC | `[]` - -## Examples - -Configuration for b3-propagation: - -```yaml -logger: - http: - headers: - prefix: header. - names: - - x-request-id - - x-b3-traceId - - x-b3-parentspanid - - x-b3-spanid - - x-b3-sampled -``` diff --git a/docs/features/http_mdc_path.md b/docs/features/http_mdc_path.md new file mode 100644 index 0000000..b5f9f73 --- /dev/null +++ b/docs/features/http_mdc_path.md @@ -0,0 +1,25 @@ +# Add HTTP path parts to MDC + +## Properties + +Property | Description | Default +-------- | ----------- | ------- +`logger.http.path.enabled` | filter enabled? | `true` +`logger.http.path.path` | filter path | `/**` +`logger.http.path.order` | order for [Ordered](https://github.com/micronaut-projects/micronaut-core/blob/v3.2.0/core/src/main/java/io/micronaut/core/order/Ordered.java) | [ServerFilterPhase.FIRST.before()](https://github.com/micronaut-projects/micronaut-core/blob/v3.2.0/http/src/main/java/io/micronaut/http/filter/ServerFilterPhase.java#L34) +`logger.http.path.prefix` | prefix to MDC key | `` +`logger.http.path.patterns` | patterns with groups to add to MDC | `[]` + +## Examples + +Configuration for adding ids: + +```yaml +logger: + http: + path: + prefix: path. + patterns: + - \/gateway\/(?[a-f0-9\-]{36}) + - \/gateway\/(?[a-f0-9\-]{36})\/configuration\/(?[a-z]+) +``` diff --git a/docs/features/logback_appender.md b/docs/features/logback_appender.md index 1975e8b..d71885b 100644 --- a/docs/features/logback_appender.md +++ b/docs/features/logback_appender.md @@ -12,5 +12,3 @@ 2. if GCP is detected gcp appender will be used 3. if Kubernetes is detected json appender will be used 4. console appender else - -*IMPORTENT*: only works without custom `logback.xml` diff --git a/docs/features/logback_default.md b/docs/features/logback_default.md index 98ccbc5..5b34f1a 100644 --- a/docs/features/logback_default.md +++ b/docs/features/logback_default.md @@ -4,15 +4,13 @@ If no `logback.xml` by user is provided a default [logback.xml](../../src/main/r ```xml - + - + - - - - - + + + ``` diff --git a/docs/features/logback_mdc_level.md b/docs/features/logback_mdc_level.md index 53887d5..ee1e853 100644 --- a/docs/features/logback_mdc_level.md +++ b/docs/features/logback_mdc_level.md @@ -7,7 +7,7 @@ This can be used to change the log level based on MDC valus. E.g. change log lev Property | Description | Default -------- | ----------- | ------- `logger.mdc.enabled` | MDC enabled? | `true` -`logger.mdc.` | MDC key to use | +`logger.mdc.` | MDC key to use | `` `logger.mdc..key` | MDC key override, see complex example below for usage | `` `logger.mdc..level` | log level to use | `TRACE` `logger.mdc..loggers` | passlist of logger names, matches all loggers if empty | `[]` diff --git a/lombok.config b/lombok.config deleted file mode 100644 index 4fc0545..0000000 --- a/lombok.config +++ /dev/null @@ -1,18 +0,0 @@ -config.stopBubbling = true - -lombok.addNullAnnotations = CUSTOM:io.micronaut.core.annotation.NonNull:io.micronaut.core.annotation.Nullable -lombok.addJavaxGeneratedAnnotation = true -lombok.addLombokGeneratedAnnotation = true - -# adjust model -lombok.equalsAndHashCode.callSuper = call -lombok.toString.callSuper = call -lombok.accessors.chain = true - -# use slf4j -lombok.log.apacheCommons.flagUsage = error -lombok.log.javaUtilLogging.flagUsage = error -lombok.log.jbosslog.flagUsage = error -lombok.log.log4j.flagUsage = error -lombok.log.log4j2.flagUsage = error -lombok.log.xslf4j.flagUsage = error diff --git a/pom.xml b/pom.xml index 9149222..22f50e6 100644 --- a/pom.xml +++ b/pom.xml @@ -1,87 +1,73 @@ - + 4.0.0 - - io.kokuwa - maven-parent - 0.5.5 - - - io.kokuwa.micronaut micronaut-logging - 3.0.0-M2 + 5.0.1-SNAPSHOT - Logging support for Micronaut + Logging Support for Micronaut Enhanced logging using MDC or request header. - https://github.com/kokuwaio/micronaut-logging + https://git.kokuwa.io/kokuwaio/micronaut-logging 2020 + + Kokuwa.io + http://kokuwa.io + - Apache License 2.0 - https://www.apache.org/licenses/LICENSE-2.0 + EUPL-1.2 + https://eupl.eu/1.2/en + repo + stephan.schnabel Stephan Schnabel - https://github.com/stephanschnabel + https://schnabel.org + stephan@schnabel.org + Europe/Berlin - https://github.com/kokuwaio/micronaut-logging - scm:git:https://github.com/kokuwaio/micronaut-logging.git - scm:git:https://github.com/kokuwaio/micronaut-logging.git - 3.0.0-M2 + https://git.kokuwa.io/kokuwaio/micronaut-logging + scm:git:https://git.kokuwa.io/kokuwaio/micronaut-logging.git + scm:git:https://git.kokuwa.io/kokuwaio/micronaut-logging.git + HEAD - github - https://github.com/kokuwaio/micronaut-logging/issues + forgejo + https://git.kokuwa.io/kokuwaio/micronaut-logging/issues + + woodpecker + https://ci.kokuwa.io/repos/kokuwaio/micronaut-logging + + + + sonatype.org + https://central.sonatype.com/repository/maven-snapshots/ + + - - - - - - 0.1.5 - 3.2.0 - + 2025-06-27T18:11:48Z + UTF-8 + 17 - - - io.micronaut - micronaut-bom - ${version.io.micronaut} + io.micronaut.platform + micronaut-platform + 4.9.0 pom import - - - - ch.qos.logback.contrib - logback-json-classic - ${version.ch.qos.logback.contrib} - - - ch.qos.logback.contrib - logback-json-core - ${version.ch.qos.logback.contrib} - - - ch.qos.logback.contrib - logback-jackson - ${version.ch.qos.logback.contrib} - - @@ -89,7 +75,7 @@ io.micronaut - micronaut-runtime + micronaut-http provided @@ -97,6 +83,10 @@ micronaut-security provided + + io.micronaut.serde + micronaut-serde-api + io.micronaut.test micronaut-test-junit5 @@ -117,53 +107,360 @@ micronaut-security-jwt test - - - com.google.code.findbugs - jsr305 - provided + io.micronaut.serde + micronaut-serde-jackson + test + + + org.yaml + snakeyaml + test - + + + com.fasterxml.jackson.core + jackson-databind + + + + + org.slf4j + slf4j-api + ch.qos.logback logback-classic - ch.qos.logback.contrib - logback-jackson - - - ch.qos.logback.contrib - logback-json-classic + ch.qos.logback + logback-core + verify + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.14.0 + + class + true + true + true + -Xlint:all,-processing + + + io.micronaut + micronaut-inject-java + + + + + + org.apache.maven.plugins + maven-deploy-plugin + 3.1.4 + + + org.apache.maven.plugins + maven-gpg-plugin + 3.2.7 + + + org.apache.maven.plugins + maven-install-plugin + 3.1.4 + + + org.apache.maven.plugins + maven-invoker-plugin + 3.9.1 + + + org.apache.maven.plugins + maven-jar-plugin + 3.4.2 + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.11.2 + + + org.apache.maven.plugins + maven-release-plugin + 3.1.1 + + verify + check + deploy + deploy,release + true + @{prefix} prepare release @{releaseLabel} [CI SKIP] + @{project.version} + + + + org.apache.maven.plugins + maven-resources-plugin + 3.3.1 + + ISO-8859-1 + + + + org.apache.maven.plugins + maven-source-plugin + 3.3.1 + + + org.apache.maven.plugins + maven-surefire-plugin + 3.5.3 + + + org.codehaus.mojo + tidy-maven-plugin + 1.4.0 + + + org.sonatype.central + central-publishing-maven-plugin + 0.8.0 + + + net.revelc.code.formatter + formatter-maven-plugin + 2.27.0 + + ${project.basedir}/src/eclipse/formatter.xml + + + + net.revelc.code + impsort-maven-plugin + 1.12.0 + + true + java.,javax.,jakarta.,org. + + + + - + org.apache.maven.plugins - maven-compiler-plugin - - - - org.projectlombok - lombok - ${version.org.projectlombok} - - - io.micronaut - micronaut-inject-java - ${version.io.micronaut} - - - + maven-invoker-plugin + + + + install + integration-test + verify + + + ${project.build.directory}/its + true + test + false + true + + + + + + + + org.apache.maven.plugins + maven-install-plugin + + + default-install + + + + + + + dev + + + !env.CI + + + + true + + + + + org.codehaus.mojo + tidy-maven-plugin + + + validate + + pom + + + + + + net.revelc.code + impsort-maven-plugin + + + validate + + sort + + + + + + net.revelc.code.formatter + formatter-maven-plugin + + + validate + + format + + + + + + + + + check + + + env.CI + + + + + + org.codehaus.mojo + tidy-maven-plugin + + + validate + + check + + + + + + net.revelc.code + impsort-maven-plugin + + + validate + + check + + + + + + net.revelc.code.formatter + formatter-maven-plugin + + + validate + + validate + + + + + + + + + deploy + + + env.CI + + + + + + + + org.apache.maven.plugins + maven-source-plugin + + + + jar + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + + + + jar + + + + + + + + org.apache.maven.plugins + maven-gpg-plugin + + + + sign + + + bc + + + + + + + + + + release + + + + org.sonatype.central + central-publishing-maven-plugin + true + + sonatype.org + true + published + + + + + + diff --git a/pom.xml.versionsBackup b/pom.xml.versionsBackup deleted file mode 100644 index 5a30895..0000000 --- a/pom.xml.versionsBackup +++ /dev/null @@ -1,169 +0,0 @@ - - - 4.0.0 - - - io.kokuwa - maven-parent - 0.5.4 - - - - io.kokuwa.micronaut - micronaut-logging - 3.0.0-SNAPSHOT - - Logging support for Micronaut - Enhanced logging using MDC or request header. - https://github.com/kokuwaio/micronaut-logging - 2020 - - - Apache License 2.0 - https://www.apache.org/licenses/LICENSE-2.0 - - - - - - Stephan Schnabel - https://github.com/stephanschnabel - - - - - https://github.com/kokuwaio/micronaut-logging - scm:git:https://github.com/kokuwaio/micronaut-logging.git - scm:git:https://github.com/kokuwaio/micronaut-logging.git - HEAD - - - github - https://github.com/kokuwaio/micronaut-logging/issues - - - - - - - - - 0.1.5 - 3.2.0 - - - - - - - - - io.micronaut - micronaut-bom - ${version.io.micronaut} - pom - import - - - - - ch.qos.logback.contrib - logback-json-classic - ${version.ch.qos.logback.contrib} - - - ch.qos.logback.contrib - logback-json-core - ${version.ch.qos.logback.contrib} - - - ch.qos.logback.contrib - logback-jackson - ${version.ch.qos.logback.contrib} - - - - - - - - - io.micronaut - micronaut-runtime - provided - - - io.micronaut.security - micronaut-security - provided - - - io.micronaut.test - micronaut-test-junit5 - test - - - io.micronaut - micronaut-http-client - test - - - io.micronaut - micronaut-http-server-netty - test - - - io.micronaut.security - micronaut-security-jwt - test - - - - - com.google.code.findbugs - jsr305 - provided - - - - - ch.qos.logback - logback-classic - - - ch.qos.logback.contrib - logback-jackson - - - ch.qos.logback.contrib - logback-json-classic - - - - - - - - - - org.apache.maven.plugins - maven-compiler-plugin - - - - org.projectlombok - lombok - ${version.org.projectlombok} - - - io.micronaut - micronaut-inject-java - ${version.io.micronaut} - - - - - - - - diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000..4bf0e44 --- /dev/null +++ b/renovate.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": ["local>kokuwaio/renovate-config", ":reviewer(stephan.schnabel)"], + "packageRules": [{ + "matchPackageNames": ["io.kokuwa.micronaut:mirconaut-logging-it"], + "enabled": false + }] +} diff --git a/src/eclipse/formatter.xml b/src/eclipse/formatter.xml new file mode 100644 index 0000000..61186a2 --- /dev/null +++ b/src/eclipse/formatter.xml @@ -0,0 +1,404 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/it/invoker.properties b/src/it/invoker.properties new file mode 100644 index 0000000..7920ff9 --- /dev/null +++ b/src/it/invoker.properties @@ -0,0 +1,3 @@ +invoker.environmentVariables.KUBERNETES_SERVICE_HOST= +invoker.environmentVariables.LOGBACK_APPENDER= +invoker.environmentVariables.GOOGLE_CLOUD_PROJECT= diff --git a/src/it/level-from-micronaut/invoker.properties b/src/it/level-from-micronaut/invoker.properties new file mode 100644 index 0000000..46bbf8c --- /dev/null +++ b/src/it/level-from-micronaut/invoker.properties @@ -0,0 +1 @@ +invoker.environmentVariables.LOGGER_LEVELS_IO_KOKUWA_MICRONAUT_LOGGING=DEBUG diff --git a/src/it/level-from-micronaut/pom.xml b/src/it/level-from-micronaut/pom.xml new file mode 100644 index 0000000..cf36117 --- /dev/null +++ b/src/it/level-from-micronaut/pom.xml @@ -0,0 +1,12 @@ + + + 4.0.0 + + + io.kokuwa.micronaut + mirconaut-logging-it + LOCAL-SNAPSHOT + + + mirconaut-logging-it-level-from-micronaut + diff --git a/src/it/level-from-micronaut/postbuild.bsh b/src/it/level-from-micronaut/postbuild.bsh new file mode 100644 index 0000000..026881f --- /dev/null +++ b/src/it/level-from-micronaut/postbuild.bsh @@ -0,0 +1,22 @@ +// verify log + +String expected = "^[0-9]{2}:[0-9]{2}:[0-9]{2}.[0-9]{0,3} main DEBUG i.k.m.logging.LoggingTest test-output-marker$"; +String[] logs = org.codehaus.plexus.util.FileUtils.fileRead(basedir + "/build.log").split("\n"); + +for (String log : logs) { + if (!log.contains("test-output-marker")) { + continue; + } + if (java.util.regex.Pattern.matches(expected, log.replaceAll("\u001B\\[[;\\d]*m", ""))) { + return true; + } else { + System.out.println("marker found, but formatting invalid:"); + System.out.println("[EXPECTED] " + expected); + System.out.println("[ACTUAL] " + log); + System.out.println("[BASE64] " + Base64.getEncoder().encodeToString(log.getBytes())); + return false; + } +} + +System.out.println("marker not found"); +return false; diff --git a/src/it/log-gcp-from-env/invoker.properties b/src/it/log-gcp-from-env/invoker.properties new file mode 100644 index 0000000..f9f63a8 --- /dev/null +++ b/src/it/log-gcp-from-env/invoker.properties @@ -0,0 +1 @@ +invoker.environmentVariables.LOGBACK_APPENDER=GCP diff --git a/src/it/log-gcp-from-env/pom.xml b/src/it/log-gcp-from-env/pom.xml new file mode 100644 index 0000000..11f4a92 --- /dev/null +++ b/src/it/log-gcp-from-env/pom.xml @@ -0,0 +1,19 @@ + + + 4.0.0 + + + io.kokuwa.micronaut + mirconaut-logging-it + LOCAL-SNAPSHOT + + + mirconaut-logging-it-log-gcp-from-env + + + + io.micronaut.serde + micronaut-serde-jsonp + + + diff --git a/src/it/log-gcp-from-env/postbuild.bsh b/src/it/log-gcp-from-env/postbuild.bsh new file mode 100644 index 0000000..d9e0657 --- /dev/null +++ b/src/it/log-gcp-from-env/postbuild.bsh @@ -0,0 +1,21 @@ +// verify log + +String expected = "^\\{\"time\":\"202[3-9]-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}.[0-9]{0,3}Z\",\"severity\":\"INFO\",\"thread\":\"main\",\"logger\":\"io.kokuwa.micronaut.logging.LoggingTest\",\"message\":\"test-output-marker\",\"raw-message\":\"test-output-marker\"}$"; +String[] logs = org.codehaus.plexus.util.FileUtils.fileRead(basedir + "/build.log").split("\n"); + +for (String log : logs) { + if (!log.contains("test-output-marker")) { + continue; + } + if (java.util.regex.Pattern.matches(expected, log)) { + return true; + } else { + System.out.println("marker found, but formatting invalid:"); + System.out.println("[EXPECTED] " + expected); + System.out.println("[ACTUAL] " + log); + return false; + } +} + +System.out.println("marker not found"); +return false; diff --git a/src/it/log-gcp-from-gcloud/invoker.properties b/src/it/log-gcp-from-gcloud/invoker.properties new file mode 100644 index 0000000..ec347b6 --- /dev/null +++ b/src/it/log-gcp-from-gcloud/invoker.properties @@ -0,0 +1 @@ +invoker.environmentVariables.GOOGLE_CLOUD_PROJECT=value diff --git a/src/it/log-gcp-from-gcloud/pom.xml b/src/it/log-gcp-from-gcloud/pom.xml new file mode 100644 index 0000000..0bf33a2 --- /dev/null +++ b/src/it/log-gcp-from-gcloud/pom.xml @@ -0,0 +1,19 @@ + + + 4.0.0 + + + io.kokuwa.micronaut + mirconaut-logging-it + LOCAL-SNAPSHOT + + + mirconaut-logging-it-log-gcp-from-gcloud + + + + io.micronaut.serde + micronaut-serde-jsonp + + + diff --git a/src/it/log-gcp-from-gcloud/postbuild.bsh b/src/it/log-gcp-from-gcloud/postbuild.bsh new file mode 100644 index 0000000..d9e0657 --- /dev/null +++ b/src/it/log-gcp-from-gcloud/postbuild.bsh @@ -0,0 +1,21 @@ +// verify log + +String expected = "^\\{\"time\":\"202[3-9]-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}.[0-9]{0,3}Z\",\"severity\":\"INFO\",\"thread\":\"main\",\"logger\":\"io.kokuwa.micronaut.logging.LoggingTest\",\"message\":\"test-output-marker\",\"raw-message\":\"test-output-marker\"}$"; +String[] logs = org.codehaus.plexus.util.FileUtils.fileRead(basedir + "/build.log").split("\n"); + +for (String log : logs) { + if (!log.contains("test-output-marker")) { + continue; + } + if (java.util.regex.Pattern.matches(expected, log)) { + return true; + } else { + System.out.println("marker found, but formatting invalid:"); + System.out.println("[EXPECTED] " + expected); + System.out.println("[ACTUAL] " + log); + return false; + } +} + +System.out.println("marker not found"); +return false; diff --git a/src/it/log-gcp-with-service/invoker.properties b/src/it/log-gcp-with-service/invoker.properties new file mode 100644 index 0000000..887fd80 --- /dev/null +++ b/src/it/log-gcp-with-service/invoker.properties @@ -0,0 +1,3 @@ +invoker.environmentVariables.LOGBACK_APPENDER=GCP +invoker.environmentVariables.SERVICE_NAME=test-service +invoker.environmentVariables.SERVICE_VERSION=0.1.2 diff --git a/src/it/log-gcp-with-service/pom.xml b/src/it/log-gcp-with-service/pom.xml new file mode 100644 index 0000000..0f81f40 --- /dev/null +++ b/src/it/log-gcp-with-service/pom.xml @@ -0,0 +1,19 @@ + + + 4.0.0 + + + io.kokuwa.micronaut + mirconaut-logging-it + LOCAL-SNAPSHOT + + + mirconaut-logging-it-log-gcp-with-service + + + + io.micronaut.serde + micronaut-serde-jsonp + + + diff --git a/src/it/log-gcp-with-service/postbuild.bsh b/src/it/log-gcp-with-service/postbuild.bsh new file mode 100644 index 0000000..6264c10 --- /dev/null +++ b/src/it/log-gcp-with-service/postbuild.bsh @@ -0,0 +1,21 @@ +// verify log + +String expected = "^\\{\"time\":\"202[3-9]-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}.[0-9]{0,3}Z\",\"severity\":\"INFO\",\"thread\":\"main\",\"logger\":\"io.kokuwa.micronaut.logging.LoggingTest\",\"message\":\"test-output-marker\",\"raw-message\":\"test-output-marker\",\"serviceContext\":\\{\"service\":\"test-service\",\"version\":\"0.1.2\"}}$"; +String[] logs = org.codehaus.plexus.util.FileUtils.fileRead(basedir + "/build.log").split("\n"); + +for (String log : logs) { + if (!log.contains("test-output-marker")) { + continue; + } + if (java.util.regex.Pattern.matches(expected, log)) { + return true; + } else { + System.out.println("marker found, but formatting invalid:"); + System.out.println("[EXPECTED] " + expected); + System.out.println("[ACTUAL] " + log); + return false; + } +} + +System.out.println("marker not found"); +return false; diff --git a/src/it/log-json-from-env-serde-jackson/invoker.properties b/src/it/log-json-from-env-serde-jackson/invoker.properties new file mode 100644 index 0000000..08de0de --- /dev/null +++ b/src/it/log-json-from-env-serde-jackson/invoker.properties @@ -0,0 +1 @@ +invoker.environmentVariables.LOGBACK_APPENDER=JSON diff --git a/src/it/log-json-from-env-serde-jackson/pom.xml b/src/it/log-json-from-env-serde-jackson/pom.xml new file mode 100644 index 0000000..87283dd --- /dev/null +++ b/src/it/log-json-from-env-serde-jackson/pom.xml @@ -0,0 +1,19 @@ + + + 4.0.0 + + + io.kokuwa.micronaut + mirconaut-logging-it + LOCAL-SNAPSHOT + + + mirconaut-logging-it-log-json-from-env-serde-jackson + + + + io.micronaut.serde + micronaut-serde-jackson + + + diff --git a/src/it/log-json-from-env-serde-jackson/postbuild.bsh b/src/it/log-json-from-env-serde-jackson/postbuild.bsh new file mode 100644 index 0000000..0693fe0 --- /dev/null +++ b/src/it/log-json-from-env-serde-jackson/postbuild.bsh @@ -0,0 +1,21 @@ +// verify log + +String expected = "^\\{\"timestamp\":\"[0-9]{13}\",\"level\":\"INFO\",\"thread\":\"main\",\"logger\":\"io.kokuwa.micronaut.logging.LoggingTest\",\"message\":\"test-output-marker\",\"raw-message\":\"test-output-marker\"}$"; +String[] logs = org.codehaus.plexus.util.FileUtils.fileRead(basedir + "/build.log").split("\n"); + +for (String log : logs) { + if (!log.contains("test-output-marker")) { + continue; + } + if (java.util.regex.Pattern.matches(expected, log)) { + return true; + } else { + System.out.println("marker found, but formatting invalid:"); + System.out.println("[EXPECTED] " + expected); + System.out.println("[ACTUAL] " + log); + return false; + } +} + +System.out.println("marker not found"); +return false; diff --git a/src/it/log-json-from-env-serde-jsonp/invoker.properties b/src/it/log-json-from-env-serde-jsonp/invoker.properties new file mode 100644 index 0000000..08de0de --- /dev/null +++ b/src/it/log-json-from-env-serde-jsonp/invoker.properties @@ -0,0 +1 @@ +invoker.environmentVariables.LOGBACK_APPENDER=JSON diff --git a/src/it/log-json-from-env-serde-jsonp/pom.xml b/src/it/log-json-from-env-serde-jsonp/pom.xml new file mode 100644 index 0000000..2d0d0ad --- /dev/null +++ b/src/it/log-json-from-env-serde-jsonp/pom.xml @@ -0,0 +1,19 @@ + + + 4.0.0 + + + io.kokuwa.micronaut + mirconaut-logging-it + LOCAL-SNAPSHOT + + + mirconaut-logging-it-log-json-from-env-serde-jsonp + + + + io.micronaut.serde + micronaut-serde-jsonp + + + diff --git a/src/it/log-json-from-env-serde-jsonp/postbuild.bsh b/src/it/log-json-from-env-serde-jsonp/postbuild.bsh new file mode 100644 index 0000000..0693fe0 --- /dev/null +++ b/src/it/log-json-from-env-serde-jsonp/postbuild.bsh @@ -0,0 +1,21 @@ +// verify log + +String expected = "^\\{\"timestamp\":\"[0-9]{13}\",\"level\":\"INFO\",\"thread\":\"main\",\"logger\":\"io.kokuwa.micronaut.logging.LoggingTest\",\"message\":\"test-output-marker\",\"raw-message\":\"test-output-marker\"}$"; +String[] logs = org.codehaus.plexus.util.FileUtils.fileRead(basedir + "/build.log").split("\n"); + +for (String log : logs) { + if (!log.contains("test-output-marker")) { + continue; + } + if (java.util.regex.Pattern.matches(expected, log)) { + return true; + } else { + System.out.println("marker found, but formatting invalid:"); + System.out.println("[EXPECTED] " + expected); + System.out.println("[ACTUAL] " + log); + return false; + } +} + +System.out.println("marker not found"); +return false; diff --git a/src/it/log-json-from-env-serde-missing/invoker.properties b/src/it/log-json-from-env-serde-missing/invoker.properties new file mode 100644 index 0000000..08de0de --- /dev/null +++ b/src/it/log-json-from-env-serde-missing/invoker.properties @@ -0,0 +1 @@ +invoker.environmentVariables.LOGBACK_APPENDER=JSON diff --git a/src/it/log-json-from-env-serde-missing/pom.xml b/src/it/log-json-from-env-serde-missing/pom.xml new file mode 100644 index 0000000..fcf06e0 --- /dev/null +++ b/src/it/log-json-from-env-serde-missing/pom.xml @@ -0,0 +1,12 @@ + + + 4.0.0 + + + io.kokuwa.micronaut + mirconaut-logging-it + LOCAL-SNAPSHOT + + + mirconaut-logging-it-log-json-from-env-serde-missing + diff --git a/src/it/log-json-from-env-serde-missing/postbuild.bsh b/src/it/log-json-from-env-serde-missing/postbuild.bsh new file mode 100644 index 0000000..465b88c --- /dev/null +++ b/src/it/log-json-from-env-serde-missing/postbuild.bsh @@ -0,0 +1,5 @@ +// verify log + +return org.codehaus.plexus.util.FileUtils + .fileRead(basedir + "/build.log") + .contains("Failed to get object mapper from micronaut, please check your classpath"); diff --git a/src/it/log-json-from-kubernetes/invoker.properties b/src/it/log-json-from-kubernetes/invoker.properties new file mode 100644 index 0000000..5bba112 --- /dev/null +++ b/src/it/log-json-from-kubernetes/invoker.properties @@ -0,0 +1 @@ +invoker.environmentVariables.KUBERNETES_SERVICE_HOST=value diff --git a/src/it/log-json-from-kubernetes/pom.xml b/src/it/log-json-from-kubernetes/pom.xml new file mode 100644 index 0000000..20d412c --- /dev/null +++ b/src/it/log-json-from-kubernetes/pom.xml @@ -0,0 +1,19 @@ + + + 4.0.0 + + + io.kokuwa.micronaut + mirconaut-logging-it + LOCAL-SNAPSHOT + + + mirconaut-logging-it-log-json-from-kubernetes + + + + io.micronaut.serde + micronaut-serde-jsonp + + + diff --git a/src/it/log-json-from-kubernetes/postbuild.bsh b/src/it/log-json-from-kubernetes/postbuild.bsh new file mode 100644 index 0000000..0693fe0 --- /dev/null +++ b/src/it/log-json-from-kubernetes/postbuild.bsh @@ -0,0 +1,21 @@ +// verify log + +String expected = "^\\{\"timestamp\":\"[0-9]{13}\",\"level\":\"INFO\",\"thread\":\"main\",\"logger\":\"io.kokuwa.micronaut.logging.LoggingTest\",\"message\":\"test-output-marker\",\"raw-message\":\"test-output-marker\"}$"; +String[] logs = org.codehaus.plexus.util.FileUtils.fileRead(basedir + "/build.log").split("\n"); + +for (String log : logs) { + if (!log.contains("test-output-marker")) { + continue; + } + if (java.util.regex.Pattern.matches(expected, log)) { + return true; + } else { + System.out.println("marker found, but formatting invalid:"); + System.out.println("[EXPECTED] " + expected); + System.out.println("[ACTUAL] " + log); + return false; + } +} + +System.out.println("marker not found"); +return false; diff --git a/src/it/log-text/pom.xml b/src/it/log-text/pom.xml new file mode 100644 index 0000000..f89c081 --- /dev/null +++ b/src/it/log-text/pom.xml @@ -0,0 +1,12 @@ + + + 4.0.0 + + + io.kokuwa.micronaut + mirconaut-logging-it + LOCAL-SNAPSHOT + + + mirconaut-logging-it-log-text + diff --git a/src/it/log-text/postbuild.bsh b/src/it/log-text/postbuild.bsh new file mode 100644 index 0000000..40e2f91 --- /dev/null +++ b/src/it/log-text/postbuild.bsh @@ -0,0 +1,22 @@ +// verify log + +String expected = "^[0-9]{2}:[0-9]{2}:[0-9]{2}.[0-9]{0,3} main INFO i.k.m.logging.LoggingTest test-output-marker$"; +String[] logs = org.codehaus.plexus.util.FileUtils.fileRead(basedir + "/build.log").split("\n"); + +for (String log : logs) { + if (!log.contains("test-output-marker")) { + continue; + } + if (java.util.regex.Pattern.matches(expected, log.replaceAll("\u001B\\[[;\\d]*m", ""))) { + return true; + } else { + System.out.println("marker found, but formatting invalid:"); + System.out.println("[EXPECTED] " + expected); + System.out.println("[ACTUAL] " + log); + System.out.println("[BASE64] " + Base64.getEncoder().encodeToString(log.getBytes())); + return false; + } +} + +System.out.println("marker not found"); +return false; diff --git a/src/it/logback-xml-custom/pom.xml b/src/it/logback-xml-custom/pom.xml new file mode 100644 index 0000000..f89c081 --- /dev/null +++ b/src/it/logback-xml-custom/pom.xml @@ -0,0 +1,12 @@ + + + 4.0.0 + + + io.kokuwa.micronaut + mirconaut-logging-it + LOCAL-SNAPSHOT + + + mirconaut-logging-it-log-text + diff --git a/src/it/logback-xml-custom/postbuild.bsh b/src/it/logback-xml-custom/postbuild.bsh new file mode 100644 index 0000000..c6a3d94 --- /dev/null +++ b/src/it/logback-xml-custom/postbuild.bsh @@ -0,0 +1,21 @@ +// verify log + +String expected = "^TRACE io.kokuwa.micronaut.logging.LoggingTest test-output-marker$"; +String[] logs = org.codehaus.plexus.util.FileUtils.fileRead(basedir + "/build.log").split("\n"); + +for (String log : logs) { + if (!log.contains("test-output-marker")) { + continue; + } + if (java.util.regex.Pattern.matches(expected, log)) { + return true; + } else { + System.out.println("marker found, but formatting invalid:"); + System.out.println("[EXPECTED] " + expected); + System.out.println("[ACTUAL] " + log); + return false; + } +} + +System.out.println("marker not found"); +return false; diff --git a/src/it/logback-xml-custom/src/test/resources/logback.xml b/src/it/logback-xml-custom/src/test/resources/logback.xml new file mode 100644 index 0000000..a704605 --- /dev/null +++ b/src/it/logback-xml-custom/src/test/resources/logback.xml @@ -0,0 +1,13 @@ + + + + + %-5level %logger{40} %msg%n + + + + + + + + diff --git a/src/it/pom.xml b/src/it/pom.xml new file mode 100644 index 0000000..d08c441 --- /dev/null +++ b/src/it/pom.xml @@ -0,0 +1,122 @@ + + + 4.0.0 + + io.kokuwa.micronaut + mirconaut-logging-it + LOCAL-SNAPSHOT + pom + + + log-text + log-json-from-env + log-json-from-kubernetes + log-gcp-from-env + log-gcp-from-gcloud + level-from-micronaut + logback-xml-custom + + + + 2025-06-27T00:00:00Z + UTF-8 + 17 + + + + + + io.kokuwa.micronaut + micronaut-logging + @project.version@ + + + io.micronaut.platform + micronaut-platform + 4.9.0 + pom + import + + + + + + + + io.micronaut + micronaut-runtime + + + io.micronaut.test + micronaut-test-junit5 + test + + + io.kokuwa.micronaut + micronaut-logging + runtime + + + + + + ${project.basedir}/../src/test/java + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.14.0 + + class + true + true + true + -Xlint:all,-processing + + + io.micronaut + micronaut-inject-java + + + + + + org.apache.maven.plugins + maven-resources-plugin + 3.3.1 + + + org.apache.maven.plugins + maven-surefire-plugin + 3.5.3 + + + + + + + + org.apache.maven.plugins + maven-resources-plugin + + + default-resources + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + default-compile + + + + + + + + diff --git a/src/it/src/test/java/io/kokuwa/micronaut/logging/LoggingTest.java b/src/it/src/test/java/io/kokuwa/micronaut/logging/LoggingTest.java new file mode 100644 index 0000000..2d8463a --- /dev/null +++ b/src/it/src/test/java/io/kokuwa/micronaut/logging/LoggingTest.java @@ -0,0 +1,13 @@ +package io.kokuwa.micronaut.logging; + +@io.micronaut.test.extensions.junit5.annotation.MicronautTest +public class LoggingTest { + + @org.junit.jupiter.api.Test + void log() { + var log = org.slf4j.LoggerFactory.getLogger(LoggingTest.class); + log.trace("test-output-marker"); + log.debug("test-output-marker"); + log.info("test-output-marker"); + } +} diff --git a/src/main/java/io/kokuwa/micronaut/logging/LogbackUtil.java b/src/main/java/io/kokuwa/micronaut/logging/LogbackUtil.java index df1d6ab..bb21701 100644 --- a/src/main/java/io/kokuwa/micronaut/logging/LogbackUtil.java +++ b/src/main/java/io/kokuwa/micronaut/logging/LogbackUtil.java @@ -4,14 +4,14 @@ import java.util.Objects; import java.util.Optional; import java.util.function.Supplier; +import jakarta.inject.Singleton; + import org.slf4j.LoggerFactory; import ch.qos.logback.classic.LoggerContext; import ch.qos.logback.classic.turbo.TurboFilter; import io.micronaut.context.annotation.BootstrapContextCompatible; import io.micronaut.context.annotation.Requires; -import io.micronaut.core.annotation.Internal; -import jakarta.inject.Singleton; /** * Utility class for Logback operations. @@ -21,7 +21,6 @@ import jakarta.inject.Singleton; @Requires(classes = LoggerContext.class) @BootstrapContextCompatible @Singleton -@Internal public class LogbackUtil { private final LoggerContext context; diff --git a/src/main/java/io/kokuwa/micronaut/logging/configurator/DefaultConfigurator.java b/src/main/java/io/kokuwa/micronaut/logging/configurator/DefaultConfigurator.java index 1d808a9..3b1a51a 100644 --- a/src/main/java/io/kokuwa/micronaut/logging/configurator/DefaultConfigurator.java +++ b/src/main/java/io/kokuwa/micronaut/logging/configurator/DefaultConfigurator.java @@ -1,8 +1,8 @@ package io.kokuwa.micronaut.logging.configurator; -import ch.qos.logback.classic.Level; import ch.qos.logback.classic.LoggerContext; import ch.qos.logback.classic.spi.Configurator; +import ch.qos.logback.classic.util.DefaultJoranConfigurator; import ch.qos.logback.core.joran.spi.JoranException; import ch.qos.logback.core.spi.ContextAwareBase; @@ -13,13 +13,19 @@ import ch.qos.logback.core.spi.ContextAwareBase; */ public class DefaultConfigurator extends ContextAwareBase implements Configurator { + @SuppressWarnings("deprecation") @Override - public void configure(LoggerContext loggerContext) { + public ExecutionStatus configure(LoggerContext loggerContext) { + + if (new DefaultJoranConfigurator().findURLOfDefaultConfigurationFile(false) != null) { + // there is a default logback file, use this one instead of our default + return ExecutionStatus.INVOKE_NEXT_IF_ANY; + } var base = DefaultConfigurator.class.getResource("/io/kokuwa/logback/logback-default.xml"); if (base == null) { addError("Failed to find logback.xml from io.kokuwa:micronaut-logging"); - return; + return ExecutionStatus.NEUTRAL; } try { @@ -29,8 +35,9 @@ public class DefaultConfigurator extends ContextAwareBase implements Configurato configurator.doConfigure(base); } catch (JoranException e) { addError("Failed to load logback.xml from io.kokuwa:micronaut-logging", e); + return ExecutionStatus.NEUTRAL; } - loggerContext.getLogger("io.micronaut.logging.PropertiesLoggingLevelsConfigurer").setLevel(Level.WARN); + return ExecutionStatus.DO_NOT_INVOKE_NEXT_IF_ANY; } } diff --git a/src/main/java/io/kokuwa/micronaut/logging/configurator/MicronautJoranConfigurator.java b/src/main/java/io/kokuwa/micronaut/logging/configurator/MicronautJoranConfigurator.java index f89db16..a1010d7 100644 --- a/src/main/java/io/kokuwa/micronaut/logging/configurator/MicronautJoranConfigurator.java +++ b/src/main/java/io/kokuwa/micronaut/logging/configurator/MicronautJoranConfigurator.java @@ -12,8 +12,8 @@ import ch.qos.logback.core.joran.spi.RuleStore; public class MicronautJoranConfigurator extends JoranConfigurator { @Override - public void addInstanceRules(RuleStore rs) { - super.addInstanceRules(rs); - rs.addRule(new ElementSelector("configuration/root/autoAppender"), new RootAutoSelectAppenderAction()); + public void addElementSelectorAndActionAssociations(RuleStore rs) { + super.addElementSelectorAndActionAssociations(rs); + rs.addRule(new ElementSelector("configuration/root/autoAppender"), () -> new RootAutoSelectAppenderAction()); } } diff --git a/src/main/java/io/kokuwa/micronaut/logging/configurator/RootAutoSelectAppenderAction.java b/src/main/java/io/kokuwa/micronaut/logging/configurator/RootAutoSelectAppenderAction.java index b490a35..1d0db03 100644 --- a/src/main/java/io/kokuwa/micronaut/logging/configurator/RootAutoSelectAppenderAction.java +++ b/src/main/java/io/kokuwa/micronaut/logging/configurator/RootAutoSelectAppenderAction.java @@ -1,16 +1,18 @@ package io.kokuwa.micronaut.logging.configurator; -import java.util.Map; - -import org.xml.sax.Attributes; +import java.util.Optional; import ch.qos.logback.classic.Logger; import ch.qos.logback.classic.LoggerContext; +import ch.qos.logback.classic.PatternLayout; import ch.qos.logback.classic.spi.ILoggingEvent; -import ch.qos.logback.core.Appender; +import ch.qos.logback.core.ConsoleAppender; +import ch.qos.logback.core.Layout; +import ch.qos.logback.core.encoder.LayoutWrappingEncoder; import ch.qos.logback.core.joran.action.Action; -import ch.qos.logback.core.joran.action.ActionConst; -import ch.qos.logback.core.joran.spi.InterpretationContext; +import ch.qos.logback.core.joran.spi.SaxEventInterpretationContext; +import io.kokuwa.micronaut.logging.layout.GcpJsonLayout; +import io.kokuwa.micronaut.logging.layout.JsonLayout; import io.micronaut.core.util.StringUtils; /** @@ -27,9 +29,17 @@ public class RootAutoSelectAppenderAction extends Action { private static final String APPENDER_JSON = "JSON"; private static final String APPENDER_GCP = "GCP"; private static final String LOGBACK_APPENDER = "LOGBACK_APPENDER"; + private static final String LOGBACK_PATTERN = "LOGBACK_PATTERN"; + private static final String LOGBACK_PATTERN_DEFAULT = """ + %cyan(%d{HH:mm:ss.SSS}) \ + %gray(%-6.6thread) \ + %highlight(%-5level) \ + %magenta(%32logger{32}) \ + %mdc \ + %msg%n"""; @Override - public void begin(InterpretationContext ic, String name, Attributes attributes) { + public void begin(SaxEventInterpretationContext ic, String name, org.xml.sax.Attributes attributes) { var rootLogger = LoggerContext.class.cast(context).getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME); var rootLoggerAppenders = rootLogger.iteratorForAppenders(); @@ -38,36 +48,83 @@ public class RootAutoSelectAppenderAction extends Action { return; } - var envAppender = System.getenv(LOGBACK_APPENDER); - if (envAppender != null && setAppender(ic, rootLogger, envAppender)) { + var envAppender = env(LOGBACK_APPENDER, null); + if (envAppender != null) { + setAppender(rootLogger, envAppender); return; } - if (IS_KUBERNETES && setAppender(ic, rootLogger, APPENDER_JSON)) { + if (IS_KUBERNETES) { + setAppender(rootLogger, APPENDER_JSON); return; } - if (IS_GCP && setAppender(ic, rootLogger, APPENDER_GCP)) { + if (IS_GCP) { + setAppender(rootLogger, APPENDER_GCP); return; } - setAppender(ic, rootLogger, APPENDER_CONSOLE); + setAppender(rootLogger, APPENDER_CONSOLE); } @Override - public void end(InterpretationContext ic, String name) {} - - private boolean setAppender(InterpretationContext ic, Logger rootLogger, String appenderName) { - - @SuppressWarnings("unchecked") - var appenderBag = (Map>) ic.getObjectMap().get(ActionConst.APPENDER_BAG); - var appender = appenderBag.get(appenderName); - if (appender == null) { - addError("Could not find an appender named [" + appenderName - + "]. Did you define it below instead of above in the configuration file?"); - return false; - } + public void end(SaxEventInterpretationContext ic, String name) {} + private void setAppender(Logger rootLogger, String appenderName) { addInfo("Use appender: " + appenderName); + + var layout = switch (appenderName) { + case APPENDER_JSON -> json(); + case APPENDER_GCP -> gcp(); + case APPENDER_CONSOLE -> console(); + default -> { + addError("Appender " + appenderName + " not found. Using console ..."); + yield console(); + } + }; + layout.start(); + + var encoder = new LayoutWrappingEncoder(); + encoder.setContext(context); + encoder.setLayout(layout); + encoder.start(); + + var appender = new ConsoleAppender(); + appender.setContext(context); + appender.setName(appenderName); + appender.setEncoder(encoder); + appender.start(); + rootLogger.addAppender(appender); - return true; + } + + private Layout console() { + var layout = new PatternLayout(); + layout.setContext(context); + layout.setPattern(env(LOGBACK_PATTERN, LOGBACK_PATTERN_DEFAULT)); + return layout; + } + + private Layout json() { + var layout = new JsonLayout(); + layout.setContext(context); + return layout; + } + + private Layout gcp() { + var layout = new GcpJsonLayout(); + layout.setContext(context); + layout.setServiceName(env("SERVICE_NAME", null)); + layout.setServiceVersion(env("SERVICE_VERSION", null)); + return layout; + } + + private String env(String name, String defaultValue) { + var envValue = Optional.ofNullable(System.getenv(name)).map(String::trim).filter(StringUtils::isNotEmpty); + var finalValue = envValue.orElse(defaultValue); + if (envValue.isPresent()) { + addInfo("Use provided config: " + name + "=" + finalValue); + } else { + addInfo("Use default config: " + name + "=" + finalValue); + } + return finalValue; } } diff --git a/src/main/java/io/kokuwa/micronaut/logging/http/AbstractMdcFilter.java b/src/main/java/io/kokuwa/micronaut/logging/http/AbstractMdcFilter.java index 777d363..b4634be 100644 --- a/src/main/java/io/kokuwa/micronaut/logging/http/AbstractMdcFilter.java +++ b/src/main/java/io/kokuwa/micronaut/logging/http/AbstractMdcFilter.java @@ -3,6 +3,8 @@ package io.kokuwa.micronaut.logging.http; import java.util.Map; import org.reactivestreams.Publisher; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.slf4j.MDC; import io.micronaut.core.async.publisher.Publishers; @@ -18,10 +20,13 @@ import io.micronaut.http.filter.ServerFilterChain; */ public abstract class AbstractMdcFilter implements HttpServerFilter { - private final int order; + protected final Logger log = LoggerFactory.getLogger(getClass()); + protected final int order; + protected final String prefix; - public AbstractMdcFilter(Integer order) { + protected AbstractMdcFilter(Integer order, String prefix) { this.order = order; + this.prefix = prefix; } @Override @@ -38,10 +43,14 @@ public abstract class AbstractMdcFilter implements HttpServerFilter { return chain.proceed(request); } - mdc.forEach(MDC::put); + mdc.forEach((key, value) -> MDC.put(addPrefix(key), value)); return Publishers.map(chain.proceed(request), response -> { - mdc.keySet().forEach(MDC::remove); + mdc.keySet().forEach(key -> MDC.remove(addPrefix(key))); return response; }); } + + private String addPrefix(String key) { + return prefix == null ? key : prefix + key; + } } diff --git a/src/main/java/io/kokuwa/micronaut/logging/http/level/LogLevelServerFilter.java b/src/main/java/io/kokuwa/micronaut/logging/http/level/LogLevelServerFilter.java index 67357ed..6626fee 100644 --- a/src/main/java/io/kokuwa/micronaut/logging/http/level/LogLevelServerFilter.java +++ b/src/main/java/io/kokuwa/micronaut/logging/http/level/LogLevelServerFilter.java @@ -1,19 +1,18 @@ package io.kokuwa.micronaut.logging.http.level; +import java.util.Map; import java.util.Optional; -import javax.annotation.PostConstruct; -import javax.annotation.PreDestroy; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; import org.reactivestreams.Publisher; -import org.slf4j.MDC; import ch.qos.logback.classic.turbo.TurboFilter; import io.kokuwa.micronaut.logging.LogbackUtil; import io.kokuwa.micronaut.logging.http.AbstractMdcFilter; import io.micronaut.context.annotation.Requires; import io.micronaut.context.annotation.Value; -import io.micronaut.core.async.publisher.Publishers; import io.micronaut.core.util.StringUtils; import io.micronaut.http.HttpRequest; import io.micronaut.http.MutableHttpResponse; @@ -45,7 +44,7 @@ public class LogLevelServerFilter extends AbstractMdcFilter { LogbackUtil logback, @Value("${" + PREFIX + ".header}") Optional header, @Value("${" + PREFIX + ".order}") Optional order) { - super(order.orElse(DEFAULT_ORDER)); + super(order.orElse(DEFAULT_ORDER), null); this.logback = logback; this.header = header.orElse(DEFAULT_HEADER); } @@ -62,15 +61,8 @@ public class LogLevelServerFilter extends AbstractMdcFilter { @Override public Publisher> doFilter(HttpRequest request, ServerFilterChain chain) { - var level = request.getHeaders().getFirst(header); - if (level.isPresent()) { - MDC.put(MDC_KEY, level.get()); - return Publishers.map(chain.proceed(request), response -> { - MDC.remove(MDC_KEY); - return response; - }); - } else { - return chain.proceed(request); - } + return request.getHeaders().getFirst(header) + .map(level -> doFilter(request, chain, Map.of(MDC_KEY, level))) + .orElseGet(() -> chain.proceed(request)); } } diff --git a/src/main/java/io/kokuwa/micronaut/logging/http/mdc/AuthenticationMdcFilter.java b/src/main/java/io/kokuwa/micronaut/logging/http/mdc/AuthenticationMdcFilter.java index f1c4a14..adc2946 100644 --- a/src/main/java/io/kokuwa/micronaut/logging/http/mdc/AuthenticationMdcFilter.java +++ b/src/main/java/io/kokuwa/micronaut/logging/http/mdc/AuthenticationMdcFilter.java @@ -1,11 +1,10 @@ package io.kokuwa.micronaut.logging.http.mdc; import java.util.HashMap; -import java.util.List; import java.util.Optional; +import java.util.Set; import org.reactivestreams.Publisher; -import org.slf4j.MDC; import io.kokuwa.micronaut.logging.http.AbstractMdcFilter; import io.micronaut.context.annotation.Requires; @@ -35,18 +34,19 @@ public class AuthenticationMdcFilter extends AbstractMdcFilter { public static final int DEFAULT_ORDER = ServerFilterPhase.SECURITY.after(); private final String name; - private final List attributes; - private final String prefix; + private final Set attributes; public AuthenticationMdcFilter( - @Value("${" + PREFIX + ".name:principal}") Optional name, - @Value("${" + PREFIX + ".attributes:[]}") List attributes, + @Value("${" + PREFIX + ".name}") Optional name, + @Value("${" + PREFIX + ".attributes}") Optional> attributes, @Value("${" + PREFIX + ".prefix}") Optional prefix, @Value("${" + PREFIX + ".order}") Optional order) { - super(order.orElse(DEFAULT_ORDER)); + super(order.orElse(DEFAULT_ORDER), prefix.orElse(null)); this.name = name.orElse(DEFAULT_NAME); - this.prefix = prefix.orElse(null); - this.attributes = attributes; + this.attributes = attributes.orElseGet(Set::of); + if (name.isPresent() || !this.attributes.isEmpty()) { + log.info("Configured with name {} and attributes {}", this.name, this.attributes); + } } @Override @@ -54,21 +54,21 @@ public class AuthenticationMdcFilter extends AbstractMdcFilter { // get authentication - var optional = request.getUserPrincipal(Authentication.class); - if (optional.isEmpty()) { + var authenticationOptional = request.getUserPrincipal(Authentication.class); + if (authenticationOptional.isEmpty()) { return chain.proceed(request); } - var authentication = optional.get(); + var authentication = authenticationOptional.get(); var authenticationAttributes = authentication.getAttributes(); // add mdc var mdc = new HashMap(); - MDC.put(prefix == null ? name : prefix + name, authentication.getName()); - for (var header : attributes) { - var value = authenticationAttributes.get(header); - if (value != null) { - mdc.put(prefix == null ? header : prefix + header, String.valueOf(value)); + mdc.put(name, authentication.getName()); + for (var attibuteName : attributes) { + var attibuteValue = authenticationAttributes.get(attibuteName); + if (attibuteValue != null) { + mdc.put(attibuteName, String.valueOf(attibuteValue)); } } diff --git a/src/main/java/io/kokuwa/micronaut/logging/http/mdc/HttpHeadersMdcFilter.java b/src/main/java/io/kokuwa/micronaut/logging/http/mdc/HeaderMdcFilter.java similarity index 60% rename from src/main/java/io/kokuwa/micronaut/logging/http/mdc/HttpHeadersMdcFilter.java rename to src/main/java/io/kokuwa/micronaut/logging/http/mdc/HeaderMdcFilter.java index efca100..a5a8b85 100644 --- a/src/main/java/io/kokuwa/micronaut/logging/http/mdc/HttpHeadersMdcFilter.java +++ b/src/main/java/io/kokuwa/micronaut/logging/http/mdc/HeaderMdcFilter.java @@ -3,8 +3,6 @@ package io.kokuwa.micronaut.logging.http.mdc; import java.util.HashMap; import java.util.List; import java.util.Optional; -import java.util.Set; -import java.util.stream.Collectors; import org.reactivestreams.Publisher; @@ -25,33 +23,30 @@ import io.micronaut.runtime.context.scope.Refreshable; * @author Stephan Schnabel */ @Refreshable -@Requires(property = HttpHeadersMdcFilter.PREFIX + ".enabled", notEquals = StringUtils.FALSE) -@Requires(property = HttpHeadersMdcFilter.PREFIX + ".names") -@Filter("${" + HttpHeadersMdcFilter.PREFIX + ".path:/**}") -public class HttpHeadersMdcFilter extends AbstractMdcFilter { +@Requires(property = HeaderMdcFilter.PREFIX + ".enabled", notEquals = StringUtils.FALSE) +@Requires(property = HeaderMdcFilter.PREFIX + ".names") +@Filter("${" + HeaderMdcFilter.PREFIX + ".path:/**}") +public class HeaderMdcFilter extends AbstractMdcFilter { - public static final String PREFIX = "logger.http.headers"; + public static final String PREFIX = "logger.http.header"; public static final int DEFAULT_ORDER = ServerFilterPhase.FIRST.before(); - private final Set headers; - private final String prefix; + private final List headers; - public HttpHeadersMdcFilter( + public HeaderMdcFilter( @Value("${" + PREFIX + ".names}") List headers, @Value("${" + PREFIX + ".prefix}") Optional prefix, @Value("${" + PREFIX + ".order}") Optional order) { - super(order.orElse(DEFAULT_ORDER)); - this.prefix = prefix.orElse(null); - this.headers = headers.stream().map(String::toLowerCase).collect(Collectors.toSet()); + super(order.orElse(DEFAULT_ORDER), prefix.orElse(null)); + this.headers = headers.stream().map(String::toLowerCase).toList(); + log.info("Configured with header names {}", headers); } @Override public Publisher> doFilter(HttpRequest request, ServerFilterChain chain) { var mdc = new HashMap(); for (var header : headers) { - request.getHeaders() - .getFirst(header) - .ifPresent(value -> mdc.put(prefix == null ? header : prefix + header, String.valueOf(value))); + request.getHeaders().getFirst(header).ifPresent(value -> mdc.put(header, String.valueOf(value))); } return doFilter(request, chain, mdc); } diff --git a/src/main/java/io/kokuwa/micronaut/logging/http/mdc/PathMdcFilter.java b/src/main/java/io/kokuwa/micronaut/logging/http/mdc/PathMdcFilter.java new file mode 100644 index 0000000..848e34d --- /dev/null +++ b/src/main/java/io/kokuwa/micronaut/logging/http/mdc/PathMdcFilter.java @@ -0,0 +1,86 @@ +package io.kokuwa.micronaut.logging.http.mdc; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +import org.reactivestreams.Publisher; + +import io.kokuwa.micronaut.logging.http.AbstractMdcFilter; +import io.micronaut.context.annotation.Requires; +import io.micronaut.context.annotation.Value; +import io.micronaut.core.util.StringUtils; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.MutableHttpResponse; +import io.micronaut.http.annotation.Filter; +import io.micronaut.http.filter.ServerFilterChain; +import io.micronaut.http.filter.ServerFilterPhase; +import io.micronaut.runtime.context.scope.Refreshable; + +/** + * Filter to add request path parts to MDC. + * + * @author Stephan Schnabel + */ +@Refreshable +@Requires(property = PathMdcFilter.PREFIX + ".enabled", notEquals = StringUtils.FALSE) +@Requires(property = PathMdcFilter.PREFIX + ".patterns") +@Filter("${" + PathMdcFilter.PREFIX + ".path:/**}") +public class PathMdcFilter extends AbstractMdcFilter { + + public static final String PREFIX = "logger.http.path"; + public static final int DEFAULT_ORDER = ServerFilterPhase.FIRST.before(); + public static final Pattern PATTERN_GROUPS = Pattern.compile("\\(\\?<([a-zA-Z][a-zA-Z0-9]+)>"); + + private final Map> patternsWithGroups; + + public PathMdcFilter( + @Value("${" + PREFIX + ".patterns}") List patterns, + @Value("${" + PREFIX + ".prefix}") Optional prefix, + @Value("${" + PREFIX + ".order}") Optional order) { + super(order.orElse(DEFAULT_ORDER), prefix.orElse(null)); + this.patternsWithGroups = new HashMap<>(); + for (var patternString : patterns) { + try { + var pattern = Pattern.compile(patternString); + var groupMatcher = PATTERN_GROUPS.matcher(pattern.toString()); + var groups = new HashSet(); + while (groupMatcher.find()) { + groups.add(groupMatcher.group(1)); + } + + if (groups.isEmpty()) { + log.warn("Path {} is missing groups.", patternString); + } else { + log.info("Added path {} with groups {}.", patternString, groups); + patternsWithGroups.put(pattern, groups); + } + } catch (PatternSyntaxException e) { + log.warn("Path {} is invalid.", patternString); + } + } + } + + @Override + public Publisher> doFilter(HttpRequest request, ServerFilterChain chain) { + + var mdc = new HashMap(); + var path = request.getPath(); + + for (var patternWithGroup : patternsWithGroups.entrySet()) { + var matcher = patternWithGroup.getKey().matcher(path); + if (matcher.matches()) { + for (var group : patternWithGroup.getValue()) { + mdc.put(group, matcher.group(group)); + } + } + } + + return doFilter(request, chain, mdc); + } +} diff --git a/src/main/java/io/kokuwa/micronaut/logging/layout/GcpJsonLayout.java b/src/main/java/io/kokuwa/micronaut/logging/layout/GcpJsonLayout.java index 0cd52d8..524601c 100644 --- a/src/main/java/io/kokuwa/micronaut/logging/layout/GcpJsonLayout.java +++ b/src/main/java/io/kokuwa/micronaut/logging/layout/GcpJsonLayout.java @@ -1,15 +1,11 @@ package io.kokuwa.micronaut.logging.layout; import java.time.Instant; -import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; import ch.qos.logback.classic.spi.ILoggingEvent; -import ch.qos.logback.contrib.json.classic.JsonLayout; import io.micronaut.core.util.StringUtils; -import lombok.Setter; -import lombok.experimental.Accessors; /** * GCP logging layout. @@ -18,7 +14,6 @@ import lombok.experimental.Accessors; * @see "https://cloud.google.com/logging/docs/agent/configuration#process-payload" * @see "https://cloud.google.com/error-reporting/reference/rest/v1beta1/ServiceContext" */ -@Accessors(chain = false) public class GcpJsonLayout extends JsonLayout { private static final String UNDEFINED = "_IS_UNDEFINED"; @@ -26,9 +21,7 @@ public class GcpJsonLayout extends JsonLayout { private static final String SEVERITY_ATTR_NAME = "severity"; private Map serviceContext; - @Setter private String serviceName; - @Setter private String serviceVersion; @Override @@ -49,7 +42,7 @@ public class GcpJsonLayout extends JsonLayout { private void addServiceContext(Map map) { if (serviceContext == null) { - serviceContext = new HashMap<>(2); + serviceContext = new LinkedHashMap<>(2); if (StringUtils.isNotEmpty(serviceName) && !serviceName.endsWith(UNDEFINED)) { serviceContext.put("service", serviceName); } @@ -61,4 +54,12 @@ public class GcpJsonLayout extends JsonLayout { map.put("serviceContext", serviceContext); } } + + public void setServiceName(String serviceName) { + this.serviceName = serviceName; + } + + public void setServiceVersion(String serviceVersion) { + this.serviceVersion = serviceVersion; + } } diff --git a/src/main/java/io/kokuwa/micronaut/logging/layout/JsonLayout.java b/src/main/java/io/kokuwa/micronaut/logging/layout/JsonLayout.java new file mode 100644 index 0000000..4665494 --- /dev/null +++ b/src/main/java/io/kokuwa/micronaut/logging/layout/JsonLayout.java @@ -0,0 +1,197 @@ +package io.kokuwa.micronaut.logging.layout; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.TimeZone; + +import ch.qos.logback.classic.pattern.ThrowableHandlingConverter; +import ch.qos.logback.classic.pattern.ThrowableProxyConverter; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.CoreConstants; +import ch.qos.logback.core.LayoutBase; +import ch.qos.logback.core.status.OnConsoleStatusListener; +import ch.qos.logback.core.status.StatusUtil; +import ch.qos.logback.core.util.StatusListenerConfigHelper; +import io.micronaut.http.MediaType; +import io.micronaut.json.JsonMapper; + +public class JsonLayout extends LayoutBase { + + public static final String TIMESTAMP_ATTR_NAME = "timestamp"; + public static final String LEVEL_ATTR_NAME = "level"; + public static final String THREAD_ATTR_NAME = "thread"; + public static final String MDC_ATTR_NAME = "mdc"; + public static final String LOGGER_ATTR_NAME = "logger"; + public static final String FORMATTED_MESSAGE_ATTR_NAME = "message"; + public static final String MESSAGE_ATTR_NAME = "raw-message"; + public static final String EXCEPTION_ATTR_NAME = "exception"; + public static final String CONTEXT_ATTR_NAME = "context"; + + protected boolean includeLevel = true; + protected boolean includeThreadName = true; + protected boolean includeMDC = true; + protected boolean includeLoggerName = true; + protected boolean includeFormattedMessage = true; + protected boolean includeMessage = true; + protected boolean includeException = true; + protected boolean includeContextName = false; + protected boolean includeTimestamp = true; + private String timestampFormat; + private String timestampFormatTimezoneId; + private ThrowableHandlingConverter throwableHandlingConverter = new ThrowableProxyConverter(); + private JsonMapper mapper; + + @Override + public String getContentType() { + return MediaType.APPLICATION_JSON; + } + + @Override + public void start() { + this.throwableHandlingConverter.start(); + super.start(); + } + + @Override + public void stop() { + super.stop(); + this.throwableHandlingConverter.stop(); + } + + @Override + public String doLayout(ILoggingEvent event) { + var map = toJsonMap(event); + + if (mapper == null) { + try { + mapper = JsonMapper.createDefault(); + } catch (IllegalStateException e) { + if (!StatusUtil.contextHasStatusListener(context)) { + addError("Failed to get object mapper from micronaut, please check your classpath"); + StatusListenerConfigHelper.addOnConsoleListenerInstance(context, new OnConsoleStatusListener()); + } + return map.toString() + CoreConstants.LINE_SEPARATOR; + } + } + + try { + return new String(mapper.writeValueAsBytes(map), StandardCharsets.UTF_8) + CoreConstants.LINE_SEPARATOR; + } catch (IOException e) { + addError("Failed to write json from event " + event + " and map " + map, e); + return null; + } + } + + protected Map toJsonMap(ILoggingEvent event) { + var map = new LinkedHashMap(); + addTimestamp(TIMESTAMP_ATTR_NAME, includeTimestamp, event.getTimeStamp(), map); + add(LEVEL_ATTR_NAME, includeLevel, String.valueOf(event.getLevel()), map); + add(THREAD_ATTR_NAME, includeThreadName, event.getThreadName(), map); + addMap(MDC_ATTR_NAME, includeMDC, event.getMDCPropertyMap(), map); + add(LOGGER_ATTR_NAME, includeLoggerName, event.getLoggerName(), map); + add(FORMATTED_MESSAGE_ATTR_NAME, includeFormattedMessage, event.getFormattedMessage(), map); + add(MESSAGE_ATTR_NAME, includeMessage, event.getMessage(), map); + add(CONTEXT_ATTR_NAME, includeContextName, event.getLoggerContextVO().getName(), map); + addThrowableInfo(EXCEPTION_ATTR_NAME, includeException, event, map); + return map; + } + + protected void addThrowableInfo(String fieldName, boolean field, ILoggingEvent value, Map map) { + if (field && value != null) { + var throwableProxy = value.getThrowableProxy(); + if (throwableProxy != null) { + var ex = throwableHandlingConverter.convert(value); + if (ex != null && !ex.isEmpty()) { + map.put(fieldName, ex); + } + } + } + } + + protected void addMap(String key, boolean field, Map mapValue, Map map) { + if (field && mapValue != null && !mapValue.isEmpty()) { + map.put(key, mapValue); + } + } + + protected void addTimestamp(String key, boolean field, long timeStamp, Map map) { + if (field) { + var formatted = formatTimestamp(timeStamp); + if (formatted != null) { + map.put(key, formatted); + } + } + } + + protected void add(String fieldName, boolean field, String value, Map map) { + if (field && value != null) { + map.put(fieldName, value); + } + } + + protected String formatTimestamp(long timestamp) { + if (timestampFormat == null || timestamp < 0) { + return String.valueOf(timestamp); + } + var date = new Date(timestamp); + var format = new SimpleDateFormat(timestampFormat); + if (timestampFormatTimezoneId != null) { + format.setTimeZone(TimeZone.getTimeZone(timestampFormatTimezoneId)); + } + return format.format(date); + } + + // setter + + public void setIncludeLevel(boolean includeLevel) { + this.includeLevel = includeLevel; + } + + public void setIncludeThreadName(boolean includeThreadName) { + this.includeThreadName = includeThreadName; + } + + public void setIncludeMDC(boolean includeMDC) { + this.includeMDC = includeMDC; + } + + public void setIncludeLoggerName(boolean includeLoggerName) { + this.includeLoggerName = includeLoggerName; + } + + public void setIncludeFormattedMessage(boolean includeFormattedMessage) { + this.includeFormattedMessage = includeFormattedMessage; + } + + public void setIncludeMessage(boolean includeMessage) { + this.includeMessage = includeMessage; + } + + public void setIncludeException(boolean includeException) { + this.includeException = includeException; + } + + public void setIncludeContextName(boolean includeContextName) { + this.includeContextName = includeContextName; + } + + public void setIncludeTimestamp(boolean includeTimestamp) { + this.includeTimestamp = includeTimestamp; + } + + public void setTimestampFormat(String timestampFormat) { + this.timestampFormat = timestampFormat; + } + + public void setTimestampFormatTimezoneId(String timestampFormatTimezoneId) { + this.timestampFormatTimezoneId = timestampFormatTimezoneId; + } + + public void setThrowableHandlingConverter(ThrowableHandlingConverter throwableHandlingConverter) { + this.throwableHandlingConverter = throwableHandlingConverter; + } +} diff --git a/src/main/java/io/kokuwa/micronaut/logging/mdc/MDCTurboFilter.java b/src/main/java/io/kokuwa/micronaut/logging/mdc/MDCTurboFilter.java index c6f01b1..db4d805 100644 --- a/src/main/java/io/kokuwa/micronaut/logging/mdc/MDCTurboFilter.java +++ b/src/main/java/io/kokuwa/micronaut/logging/mdc/MDCTurboFilter.java @@ -50,7 +50,13 @@ public class MDCTurboFilter extends TurboFilter { } @Override - public FilterReply decide(Marker marker, Logger logger, Level level, String format, Object[] params, Throwable t) { + public FilterReply decide( + Marker marker, + Logger logger, + Level eventLevel, + String format, + Object[] params, + Throwable t) { if (logger == null || !isStarted()) { return FilterReply.NEUTRAL; @@ -67,6 +73,6 @@ public class MDCTurboFilter extends TurboFilter { return FilterReply.NEUTRAL; } - return level.isGreaterOrEqual(this.level) ? FilterReply.ACCEPT : FilterReply.NEUTRAL; + return eventLevel.isGreaterOrEqual(this.level) ? FilterReply.ACCEPT : FilterReply.NEUTRAL; } } diff --git a/src/main/java/io/kokuwa/micronaut/logging/mdc/MDCTurboFilterConfigurer.java b/src/main/java/io/kokuwa/micronaut/logging/mdc/MDCTurboFilterConfigurer.java index e83217c..de87997 100644 --- a/src/main/java/io/kokuwa/micronaut/logging/mdc/MDCTurboFilterConfigurer.java +++ b/src/main/java/io/kokuwa/micronaut/logging/mdc/MDCTurboFilterConfigurer.java @@ -1,45 +1,71 @@ package io.kokuwa.micronaut.logging.mdc; +import java.util.Collection; import java.util.Set; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import ch.qos.logback.classic.Level; import io.kokuwa.micronaut.logging.LogbackUtil; import io.micronaut.context.annotation.BootstrapContextCompatible; import io.micronaut.context.annotation.Context; import io.micronaut.context.annotation.Requires; import io.micronaut.context.env.Environment; -import io.micronaut.core.annotation.Internal; import io.micronaut.core.type.Argument; import io.micronaut.core.util.StringUtils; -import lombok.extern.slf4j.Slf4j; +import io.micronaut.logging.LogLevel; +import io.micronaut.logging.LoggingSystem; /** * Configure MDC filter. * * @author Stephan Schnabel */ +@BootstrapContextCompatible @Requires(beans = LogbackUtil.class) @Requires(property = MDCTurboFilterConfigurer.PREFIX) @Requires(property = MDCTurboFilterConfigurer.PREFIX + ".enabled", notEquals = StringUtils.FALSE) -@BootstrapContextCompatible @Context -@Internal -@Slf4j -public class MDCTurboFilterConfigurer { +public class MDCTurboFilterConfigurer implements LoggingSystem { public static final String PREFIX = "logger.mdc"; + private static final Logger log = LoggerFactory.getLogger(MDCTurboFilterConfigurer.class); + private final LogbackUtil logback; private final Environment environment; + private Collection mdcs = Set.of(); + private boolean initialized; + public MDCTurboFilterConfigurer(LogbackUtil logback, Environment environment) { this.logback = logback; this.environment = environment; - configure(); + this.refresh(); } - public void configure() { - for (var name : environment.getPropertyEntries(PREFIX)) { + @Override + public final void refresh() { + + mdcs = environment.getPropertyEntries(PREFIX); + initialized = false; + + if (environment.getProperties("logger.levels").isEmpty()) { + log.warn("MDCs are configured, but no levels are set. MDC may not work."); + } + } + + @Override + public void setLogLevel(String name, LogLevel level) { + if (!initialized) { + configure(); + initialized = true; + } + } + + private void configure() { + for (var name : mdcs) { var prefix = PREFIX + "." + name + "."; var key = environment.getProperty(prefix + "key", String.class, name); diff --git a/src/main/resources/META-INF/native-image/io.kokuwa.micronaut/micronaut-logging/native-image.properties b/src/main/resources/META-INF/native-image/io.kokuwa.micronaut/micronaut-logging/native-image.properties new file mode 100644 index 0000000..e1e3f1f --- /dev/null +++ b/src/main/resources/META-INF/native-image/io.kokuwa.micronaut/micronaut-logging/native-image.properties @@ -0,0 +1,2 @@ +Args = --initialize-at-build-time=io.kokuwa.micronaut.logging.configurator.RootAutoSelectAppenderAction + diff --git a/src/main/resources/META-INF/native-image/io.kokuwa.micronaut/micronaut-logging/resource-config.json b/src/main/resources/META-INF/native-image/io.kokuwa.micronaut/micronaut-logging/resource-config.json new file mode 100644 index 0000000..3a97089 --- /dev/null +++ b/src/main/resources/META-INF/native-image/io.kokuwa.micronaut/micronaut-logging/resource-config.json @@ -0,0 +1,12 @@ +{ + "resources": { + "includes": [ + { + "pattern": "\\Qio/kokuwa/logback/logback-default.xml\\E" + }, + { + "pattern": "\\QMETA-INF/services/ch.qos.logback.classic.spi.Configurator\\E" + } + ] + } +} \ No newline at end of file diff --git a/src/main/resources/io/kokuwa/logback/appender-console.xml b/src/main/resources/io/kokuwa/logback/appender-console.xml deleted file mode 100644 index e4bfb1d..0000000 --- a/src/main/resources/io/kokuwa/logback/appender-console.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - ${CONSOLE_LOG_JANSI:-true} - - ${CONSOLE_LOG_PATTERN:-%cyan(%d{HH:mm:ss.SSS}) %gray(%-6.6thread) %highlight(%-5level) %magenta(%32logger{32}) %mdc %msg%n} - ${CONSOLE_LOG_CHARSET:-default} - - - - diff --git a/src/main/resources/io/kokuwa/logback/appender-gcp.xml b/src/main/resources/io/kokuwa/logback/appender-gcp.xml deleted file mode 100644 index e847d99..0000000 --- a/src/main/resources/io/kokuwa/logback/appender-gcp.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - ${SERVICE_NAME} - ${SERVICE_VERSION} - - true - true - false - - - - - diff --git a/src/main/resources/io/kokuwa/logback/appender-json.xml b/src/main/resources/io/kokuwa/logback/appender-json.xml deleted file mode 100644 index b850ac7..0000000 --- a/src/main/resources/io/kokuwa/logback/appender-json.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - true - true - false - - - - - diff --git a/src/main/resources/io/kokuwa/logback/base.xml b/src/main/resources/io/kokuwa/logback/base.xml deleted file mode 100644 index 24ae946..0000000 --- a/src/main/resources/io/kokuwa/logback/base.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/src/main/resources/io/kokuwa/logback/logback-default.xml b/src/main/resources/io/kokuwa/logback/logback-default.xml index 1092ea9..78fe3f0 100644 --- a/src/main/resources/io/kokuwa/logback/logback-default.xml +++ b/src/main/resources/io/kokuwa/logback/logback-default.xml @@ -1,8 +1,6 @@ - - diff --git a/src/main/resources/io/kokuwa/logback/logback-example.xml b/src/main/resources/io/kokuwa/logback/logback-example.xml deleted file mode 100644 index 47deab2..0000000 --- a/src/main/resources/io/kokuwa/logback/logback-example.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/src/test/java/io/kokuwa/micronaut/logging/AbstractTest.java b/src/test/java/io/kokuwa/micronaut/logging/AbstractTest.java index 366ba0c..ebcc5e7 100644 --- a/src/test/java/io/kokuwa/micronaut/logging/AbstractTest.java +++ b/src/test/java/io/kokuwa/micronaut/logging/AbstractTest.java @@ -2,9 +2,7 @@ package io.kokuwa.micronaut.logging; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.ClassOrderer; import org.junit.jupiter.api.MethodOrderer; -import org.junit.jupiter.api.TestClassOrder; import org.junit.jupiter.api.TestMethodOrder; import org.slf4j.MDC; @@ -16,7 +14,6 @@ import io.micronaut.test.extensions.junit5.annotation.MicronautTest; * @author Stephan Schnabel */ @MicronautTest -@TestClassOrder(ClassOrderer.DisplayName.class) @TestMethodOrder(MethodOrderer.DisplayName.class) public abstract class AbstractTest { diff --git a/src/test/java/io/kokuwa/micronaut/logging/http/AbstractFilterTest.java b/src/test/java/io/kokuwa/micronaut/logging/http/AbstractFilterTest.java index 11d6676..4587dcb 100644 --- a/src/test/java/io/kokuwa/micronaut/logging/http/AbstractFilterTest.java +++ b/src/test/java/io/kokuwa/micronaut/logging/http/AbstractFilterTest.java @@ -2,25 +2,33 @@ package io.kokuwa.micronaut.logging.http; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; import java.util.Map; import java.util.function.Consumer; -import org.junit.jupiter.api.BeforeEach; +import jakarta.inject.Inject; + import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.slf4j.MDC; -import com.nimbusds.jwt.JWTClaimsSet; - import ch.qos.logback.classic.Level; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jwt.JWTClaimsSet; import io.kokuwa.micronaut.logging.AbstractTest; +import io.micronaut.core.annotation.Nullable; import io.micronaut.core.util.CollectionUtils; import io.micronaut.http.HttpHeaderValues; import io.micronaut.http.HttpRequest; import io.micronaut.http.HttpStatus; import io.micronaut.http.annotation.Controller; import io.micronaut.http.annotation.Get; +import io.micronaut.http.annotation.PathVariable; import io.micronaut.http.client.DefaultHttpClientConfiguration; import io.micronaut.http.client.HttpClient; import io.micronaut.http.filter.HttpServerFilter; @@ -28,13 +36,8 @@ import io.micronaut.runtime.server.EmbeddedServer; import io.micronaut.security.annotation.Secured; import io.micronaut.security.rules.SecurityRule; import io.micronaut.security.token.jwt.signature.SignatureGeneratorConfiguration; +import io.micronaut.serde.annotation.Serdeable; import io.micronaut.test.extensions.junit5.annotation.MicronautTest; -import jakarta.inject.Inject; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.SneakyThrows; -import lombok.extern.slf4j.Slf4j; /** * Test for {@link HttpServerFilter}. @@ -44,24 +47,14 @@ import lombok.extern.slf4j.Slf4j; @MicronautTest(rebuildContext = true) public abstract class AbstractFilterTest extends AbstractTest { - private static boolean INIT = false; - @Inject SignatureGeneratorConfiguration signature; @Inject EmbeddedServer embeddedServer; - @DisplayName("0 init") + @DisplayName("0 - trigger rebuild of context") @Test - @BeforeEach - void refresh() { - // https://github.com/micronaut-projects/micronaut-core/issues/5453#issuecomment-864594741 - if (INIT) { - embeddedServer.refresh(); - } else { - INIT = true; - } - } + void rebuild() {} // security @@ -69,19 +62,22 @@ public abstract class AbstractFilterTest extends AbstractTest { return token(subject, claims -> {}); } - @SneakyThrows public String token(String subject, Consumer manipulator) { var claims = new JWTClaimsSet.Builder().subject(subject); manipulator.accept(claims); - return HttpHeaderValues.AUTHORIZATION_PREFIX_BEARER + " " + signature.sign(claims.build()).serialize(); + try { + return HttpHeaderValues.AUTHORIZATION_PREFIX_BEARER + " " + signature.sign(claims.build()).serialize(); + } catch (JOSEException e) { + fail(e); + return null; + } } // request - @SneakyThrows - public TestResponse get(Map headers) { + public TestResponse get(String path, Map headers) { - var request = HttpRequest.GET("/"); + var request = HttpRequest.GET(path); headers.forEach((name, value) -> request.header(name, value)); var configuration = new DefaultHttpClientConfiguration(); configuration.setLoggerName("io.kokuwa.TestClient"); @@ -97,11 +93,12 @@ public abstract class AbstractFilterTest extends AbstractTest { @Secured({ SecurityRule.IS_ANONYMOUS, SecurityRule.IS_AUTHENTICATED }) @Controller - @Slf4j public static class TestController { - @Get("/") - TestResponse run() { + private static final Logger log = LoggerFactory.getLogger(TestController.class); + + @Get("/{+path}") + TestResponse run(@PathVariable String path) { var level = Level.OFF; if (log.isTraceEnabled()) { @@ -119,15 +116,37 @@ public abstract class AbstractFilterTest extends AbstractTest { var mdc = MDC.getCopyOfContextMap(); log.info("Found MDC: {}", mdc); - return new TestResponse(level.toString(), mdc == null ? Map.of() : mdc); + return new TestResponse(path, level.toString(), mdc); } } - @Data - @NoArgsConstructor - @AllArgsConstructor + @Serdeable public static class TestResponse { - private String level; - private Map context = Map.of(); + + private final String path; + private final String level; + private final Map context; + + @JsonCreator + public TestResponse( + @JsonProperty("path") String path, + @JsonProperty("level") String level, + @Nullable @JsonProperty("context") Map context) { + this.path = path; + this.level = level; + this.context = context == null ? Map.of() : context; + } + + public String getPath() { + return path; + } + + public String getLevel() { + return level; + } + + public Map getContext() { + return context; + } } } diff --git a/src/test/java/io/kokuwa/micronaut/logging/http/level/LogLevelServerFilterTest.java b/src/test/java/io/kokuwa/micronaut/logging/http/level/LogLevelServerFilterTest.java index b25a6af..c4db1ef 100644 --- a/src/test/java/io/kokuwa/micronaut/logging/http/level/LogLevelServerFilterTest.java +++ b/src/test/java/io/kokuwa/micronaut/logging/http/level/LogLevelServerFilterTest.java @@ -75,6 +75,6 @@ public class LogLevelServerFilterTest extends AbstractFilterTest { private void assertLevel(Level expectedLevel, String name, String value) { var headers = value == null ? Map.of() : Map.of(name, value); - assertEquals(expectedLevel.toString(), get(headers).getLevel()); + assertEquals(expectedLevel.toString(), get("/level", headers).getLevel()); } } diff --git a/src/test/java/io/kokuwa/micronaut/logging/http/mdc/AuthenticationMdcFilterTest.java b/src/test/java/io/kokuwa/micronaut/logging/http/mdc/AuthenticationMdcFilterTest.java index 0492139..d10b673 100644 --- a/src/test/java/io/kokuwa/micronaut/logging/http/mdc/AuthenticationMdcFilterTest.java +++ b/src/test/java/io/kokuwa/micronaut/logging/http/mdc/AuthenticationMdcFilterTest.java @@ -63,7 +63,7 @@ public class AuthenticationMdcFilterTest extends AbstractFilterTest { } private Map getContext(boolean token) { - return get(token + return get("/security", token ? Map.of(HttpHeaders.AUTHORIZATION, token("mySubject", claims -> claims .issuer("nope") .claim("azp", "myAzp") diff --git a/src/test/java/io/kokuwa/micronaut/logging/http/mdc/HttpHeadersMdcFilterTest.java b/src/test/java/io/kokuwa/micronaut/logging/http/mdc/HeaderMdcFilterTest.java similarity index 68% rename from src/test/java/io/kokuwa/micronaut/logging/http/mdc/HttpHeadersMdcFilterTest.java rename to src/test/java/io/kokuwa/micronaut/logging/http/mdc/HeaderMdcFilterTest.java index f551bd9..489870f 100644 --- a/src/test/java/io/kokuwa/micronaut/logging/http/mdc/HttpHeadersMdcFilterTest.java +++ b/src/test/java/io/kokuwa/micronaut/logging/http/mdc/HeaderMdcFilterTest.java @@ -11,12 +11,12 @@ import io.kokuwa.micronaut.logging.http.AbstractFilterTest; import io.micronaut.context.annotation.Property; /** - * Test for {@link HttpHeadersMdcFilter}. + * Test for {@link HeaderMdcFilter}. * * @author Stephan Schnabel */ @DisplayName("http: mdc from headers") -public class HttpHeadersMdcFilterTest extends AbstractFilterTest { +public class HeaderMdcFilterTest extends AbstractFilterTest { @DisplayName("noop: empty configuration") @Test @@ -26,35 +26,35 @@ public class HttpHeadersMdcFilterTest extends AbstractFilterTest { @DisplayName("noop: disabled") @Test - @Property(name = "logger.http.headers.enabled", value = "false") - @Property(name = "logger.http.headers.names", value = "foo") + @Property(name = "logger.http.header.enabled", value = "false") + @Property(name = "logger.http.header.names", value = "foo") void noopDisabled() { assertContext(Map.of(), Map.of("foo", "bar")); } @DisplayName("mdc: mismatch") @Test - @Property(name = "logger.http.headers.names", value = "foo") + @Property(name = "logger.http.header.names", value = "foo") void mdcMismatch() { assertContext(Map.of(), Map.of("nope", "bar")); } @DisplayName("mdc: match without prefix") @Test - @Property(name = "logger.http.headers.names", value = "foo") + @Property(name = "logger.http.header.names", value = "foo") void mdcMatchWithoutPrefix() { assertContext(Map.of("foo", "bar"), Map.of("foo", "bar", "nope", "bar")); } @DisplayName("mdc: match with prefix") @Test - @Property(name = "logger.http.headers.names", value = "foo") - @Property(name = "logger.http.headers.prefix", value = "header.") + @Property(name = "logger.http.header.names", value = "foo") + @Property(name = "logger.http.header.prefix", value = "header.") void mdcMatchWithPrefix() { assertContext(Map.of("header.foo", "bar"), Map.of("foo", "bar", "nope", "bar")); } private void assertContext(Map expectedMdcs, Map headers) { - assertEquals(expectedMdcs, get(headers).getContext()); + assertEquals(expectedMdcs, get("/header", headers).getContext()); } } diff --git a/src/test/java/io/kokuwa/micronaut/logging/http/mdc/PathMdcFilterTest.java b/src/test/java/io/kokuwa/micronaut/logging/http/mdc/PathMdcFilterTest.java new file mode 100644 index 0000000..3b7ac78 --- /dev/null +++ b/src/test/java/io/kokuwa/micronaut/logging/http/mdc/PathMdcFilterTest.java @@ -0,0 +1,104 @@ +package io.kokuwa.micronaut.logging.http.mdc; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Map; +import java.util.UUID; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import io.kokuwa.micronaut.logging.http.AbstractFilterTest; +import io.micronaut.context.annotation.Property; + +/** + * Test for {@link PathMdcFilter}. + * + * @author Stephan Schnabel + */ +@DisplayName("http: mdc from path") +public class PathMdcFilterTest extends AbstractFilterTest { + + @DisplayName("noop: empty configuration") + @Test + void noopEmptyConfiguration() { + assertContext(Map.of(), "/foo/bar"); + } + + @DisplayName("noop: disabled") + @Test + @Property(name = "logger.http.path.enabled", value = "false") + @Property(name = "logger.http.path.patterns", value = "\\/foo\\/(?[0-9]+)") + void noopDisabled() { + assertContext(Map.of(), "/foo/123"); + } + + @DisplayName("noop: misconfigured") + @Test + @Property(name = "logger.http.path.patterns", value = "\\A{") + void noopMisconfigured() { + assertContext(Map.of(), "/foo/123"); + } + + @DisplayName("noop: no group") + @Test + @Property(name = "logger.http.path.patterns", value = "\\/foo/[0-9]+") + void noopGroups() { + assertContext(Map.of(), "/foo/123"); + } + + @DisplayName("mdc: mismatch") + @Test + @Property(name = "logger.http.path.patterns", value = "\\/foo\\/(?[0-9]+)") + void mdcMismatch() { + assertContext(Map.of(), "/nope"); + assertContext(Map.of(), "/foo/abc"); + } + + @DisplayName("mdc: match with single group") + @Test + @Property(name = "logger.http.path.patterns", value = "\\/foo\\/(?[0-9]+)") + void mdcMatchWithSingleGroup() { + assertContext(Map.of("foo", "123"), "/foo/123"); + } + + @DisplayName("mdc: match with single group and prefix") + @Test + @Property(name = "logger.http.path.names", value = "foo") + @Property(name = "logger.http.path.patterns", value = "\\/foo\\/(?[0-9]+)") + @Property(name = "logger.http.path.prefix", value = "path.") + void mdcMatchWithSingleGroupAndPrefix() { + assertContext(Map.of("path.foo", "123"), "/foo/123"); + } + + @DisplayName("mdc: match with single group and misconfigured") + @Test + @Property(name = "logger.http.path.names", value = "foo") + @Property(name = "logger.http.path.patterns", value = "\\/foo\\/(?[0-9]+),\\A{") + @Property(name = "logger.http.path.prefix", value = "path.") + void mdcMatchWithSingleGroupAndMisconfigured() { + assertContext(Map.of("path.foo", "123"), "/foo/123"); + } + + @DisplayName("mdc: match with multiple group") + @Test + @Property(name = "logger.http.path.patterns", value = "/foo/(?[0-9]+)/bar/(?[0-9]+)") + void mdcMatchWithmultipleGroup() { + assertContext(Map.of("foo", "123", "bar", "456"), "/foo/123/bar/456"); + } + + @DisplayName("mdc: test for documentation example") + @Test + @Property(name = "logger.http.path.patterns", value = """ + \\/gateway\\/(?[a-f0-9\\-]{36}),\ + \\/gateway\\/(?[a-f0-9\\-]{36})\\/configuration\\/(?[a-z]+)""") + void mdcMatchExample() { + var uuid = UUID.randomUUID().toString(); + assertContext(Map.of("gatewayId", uuid), "/gateway/" + uuid); + assertContext(Map.of("gatewayId", uuid, "config", "abc"), "/gateway/" + uuid + "/configuration/abc"); + } + + private void assertContext(Map expectedMdcs, String path) { + assertEquals(expectedMdcs, get(path, Map.of()).getContext()); + } +} diff --git a/src/test/resources/application-test-mdc.yaml b/src/test/resources/application-test-mdc.yaml index c0fd2a2..69cbc89 100644 --- a/src/test/resources/application-test-mdc.yaml +++ b/src/test/resources/application-test-mdc.yaml @@ -1,4 +1,6 @@ logger: + levels: + io.micronaut.logging.PropertiesLoggingLevelsConfigurer: "OFF" mdc: key1: key: key