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/release.yaml b/.github/workflows/release.yaml deleted file mode 100644 index 35cff56..0000000 --- a/.github/workflows/release.yaml +++ /dev/null @@ -1,32 +0,0 @@ -name: Release - -"on": - workflow_dispatch: {} - -jobs: - build: - runs-on: ubuntu-latest - 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 - env: - SERVER_USERNAME: ${{ secrets.SONATYPE_NEXUS_USERNAME }} - SERVER_PASSWORD: ${{ secrets.SONATYPE_NEXUS_PASSWORD }} - GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} diff --git a/.github/workflows/snapshot.yaml b/.github/workflows/snapshot.yaml deleted file mode 100644 index 4414d1c..0000000 --- a/.github/workflows/snapshot.yaml +++ /dev/null @@ -1,54 +0,0 @@ -name: Build - -"on": - workflow_dispatch: {} - push: {} - -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 - server-id: sonatype-nexus - server-username: SERVER_USERNAME - server-password: SERVER_PASSWORD - - run: mvn -B dependency:go-offline -q - - run: mvn -B verify - if: github.ref != 'refs/heads/main' - - run: mvn -B deploy - if: github.ref == 'refs/heads/main' - env: - SERVER_USERNAME: ${{ secrets.SONATYPE_NEXUS_USERNAME }} - SERVER_PASSWORD: ${{ secrets.SONATYPE_NEXUS_PASSWORD }} - - uses: dorny/test-reporter@v1 - if: failure() - with: - name: surefire - path: '*/target/surefire-reports/*.xml' - reporter: java-junit - list-suites: failed - list-tests: failed - fail-on-error: false 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.yaml similarity index 64% rename from .yamllint rename to .yamllint.yaml index 9d1b12e..21966f2 100644 --- a/.yamllint +++ b/.yamllint.yaml @@ -8,3 +8,8 @@ rules: # 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 28fab9b..4a3a2ec 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,34 @@ # Micronaut Logging support -[![Maven Central](https://img.shields.io/maven-central/v/io.kokuwa.micronaut/micronaut-logging.svg?label=Maven%20Central)](https://search.maven.org/search?q=g:%22io.kokuwa.micronaut%22%20AND%20a:%22micronaut-logging%22) [![CI](https://github.com/kokuwaio/micronaut-logging/actions/workflows/snapshot.yaml/badge.svg)](https://github.com/kokuwaio/micronaut-logging/actions/workflows/snapshot.yaml) +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) @@ -12,13 +37,8 @@ * [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 * read **serviceName** and **serviceVersion** from yaml * support auto select appender with custom `logback.xml` -* add maven site with jacoco / dependency updates for snapshot build diff --git a/docs/build.md b/docs/build.md deleted file mode 100644 index 0d2c727..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 -``` diff --git a/docs/features/http_mdc_header.md b/docs/features/http_mdc_header.md index db91c1e..8a8e9d4 100644 --- a/docs/features/http_mdc_header.md +++ b/docs/features/http_mdc_header.md @@ -21,8 +21,8 @@ logger: prefix: header. names: - x-request-id - - x-b3-traceId + - x-b3-traceId - x-b3-parentspanid - x-b3-spanid - - x-b3-sampled + - x-b3-sampled ``` 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/pom.xml b/pom.xml index 9459387..22f50e6 100644 --- a/pom.xml +++ b/pom.xml @@ -1,14 +1,14 @@ - + 4.0.0 io.kokuwa.micronaut micronaut-logging - 3.0.2 + 5.0.1-SNAPSHOT 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 @@ -16,125 +16,58 @@ - Apache License 2.0 - https://www.apache.org/licenses/LICENSE-2.0 + EUPL-1.2 + https://eupl.eu/1.2/en + repo - stephanschnabel + stephan.schnabel Stephan Schnabel - https://github.com/stephanschnabel - stephan@grayc.de - GrayC GmbH - http://grayc.de + 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.2 + 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 - github - https://github.com/kokuwaio/micronaut-logging/actions + woodpecker + https://ci.kokuwa.io/repos/kokuwaio/micronaut-logging - sonatype-nexus - https://oss.sonatype.org/content/repositories/snapshots + sonatype.org + https://central.sonatype.com/repository/maven-snapshots/ - - sonatype-nexus - https://oss.sonatype.org/service/local/staging/deploy/maven2 - - - - - - + 2025-06-27T18:11:48Z UTF-8 - ISO-8859-1 - UTF-8 - - 11 - 11 - true - true - true - - - - - - - - 1.2.10 - 0.1.5 - 3.2.3 - - - - 3.1.0 - 3.8.1 - 3.0.0-M1 - 3.0.1 - 3.0.0-M1 - 3.3.1 - 3.2.0 - 3.0.0-M4 - 3.2.0 - 3.2.1 - 3.0.0-M5 - 1.1.0 - 1.6.8 - + 17 - - - io.micronaut - micronaut-bom - ${version.io.micronaut} + io.micronaut.platform + micronaut-platform + 4.9.0 pom import - - - - ch.qos.logback - logback-classic - ${version.ch.qos.logback} - - - 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} - - @@ -142,7 +75,7 @@ io.micronaut - micronaut-runtime + micronaut-http provided @@ -150,6 +83,10 @@ micronaut-security provided + + io.micronaut.serde + micronaut-serde-api + io.micronaut.test micronaut-test-junit5 @@ -170,42 +107,57 @@ micronaut-security-jwt test + + 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-json-classic - - - ch.qos.logback.contrib - logback-jackson - runtime + ch.qos.logback + logback-core + verify - - org.apache.maven.plugins - maven-clean-plugin - ${version.org.apache.maven.plugins.clean} - org.apache.maven.plugins maven-compiler-plugin - ${version.org.apache.maven.plugins.compiler} + 3.14.0 + class + true + true + true + -Xlint:all,-processing io.micronaut micronaut-inject-java - ${version.io.micronaut} @@ -213,93 +165,126 @@ org.apache.maven.plugins maven-deploy-plugin - ${version.org.apache.maven.plugins.deploy} + 3.1.4 org.apache.maven.plugins maven-gpg-plugin - ${version.org.apache.maven.plugins.gpg} + 3.2.7 org.apache.maven.plugins maven-install-plugin - ${version.org.apache.maven.plugins.install} + 3.1.4 org.apache.maven.plugins - maven-javadoc-plugin - ${version.org.apache.maven.plugins.javadoc} + maven-invoker-plugin + 3.9.1 org.apache.maven.plugins maven-jar-plugin - ${version.org.apache.maven.plugins.jar} + 3.4.2 + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.11.2 org.apache.maven.plugins maven-release-plugin - ${version.org.apache.maven.plugins.release} + 3.1.1 + verify + check + deploy + deploy,release + true + @{prefix} prepare release @{releaseLabel} [CI SKIP] @{project.version} - release - true org.apache.maven.plugins maven-resources-plugin - ${version.org.apache.maven.plugins.resources} + 3.3.1 + + ISO-8859-1 + org.apache.maven.plugins maven-source-plugin - ${version.org.apache.maven.plugins.source} + 3.3.1 org.apache.maven.plugins maven-surefire-plugin - ${version.org.apache.maven.plugins.surefire} - - true - true - + 3.5.3 org.codehaus.mojo tidy-maven-plugin - ${version.org.codehaus.mojo.tidy} + 1.4.0 - org.sonatype.plugins - nexus-staging-maven-plugin - ${version.org.sonatype.plugins.nexus-staging} + 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-source-plugin + maven-invoker-plugin - jar-no-fork + install + integration-test + verify + + ${project.build.directory}/its + true + test + false + true + - + - org.codehaus.mojo - tidy-maven-plugin + org.apache.maven.plugins + maven-install-plugin - - check - + default-install + @@ -309,11 +294,126 @@ - release + 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 @@ -326,7 +426,7 @@ - + org.apache.maven.plugins maven-gpg-plugin @@ -336,28 +436,29 @@ sign - - - --pinentry-mode - loopback - + bc - + + + + + release + + - org.sonatype.plugins - nexus-staging-maven-plugin + org.sonatype.central + central-publishing-maven-plugin true - sonatype-nexus - https://oss.sonatype.org/ - true + sonatype.org + true + published - 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/level/LogLevelServerFilter.java b/src/main/java/io/kokuwa/micronaut/logging/http/level/LogLevelServerFilter.java index 0e5d07a..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 @@ -3,8 +3,8 @@ 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; 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 aea7557..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 @@ -45,7 +45,7 @@ public class AuthenticationMdcFilter extends AbstractMdcFilter { this.name = name.orElse(DEFAULT_NAME); this.attributes = attributes.orElseGet(Set::of); if (name.isPresent() || !this.attributes.isEmpty()) { - log.info("Configured with name {} and attributes {}", name, attributes); + log.info("Configured with name {} and attributes {}", this.name, this.attributes); } } diff --git a/src/main/java/io/kokuwa/micronaut/logging/http/mdc/HeaderMdcFilter.java b/src/main/java/io/kokuwa/micronaut/logging/http/mdc/HeaderMdcFilter.java index a59b425..a5a8b85 100644 --- a/src/main/java/io/kokuwa/micronaut/logging/http/mdc/HeaderMdcFilter.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; @@ -33,14 +31,14 @@ public class HeaderMdcFilter extends AbstractMdcFilter { public static final String PREFIX = "logger.http.header"; public static final int DEFAULT_ORDER = ServerFilterPhase.FIRST.before(); - private final Set headers; + private final List headers; public HeaderMdcFilter( @Value("${" + PREFIX + ".names}") List headers, @Value("${" + PREFIX + ".prefix}") Optional prefix, @Value("${" + PREFIX + ".order}") Optional order) { super(order.orElse(DEFAULT_ORDER), prefix.orElse(null)); - this.headers = headers.stream().map(String::toLowerCase).collect(Collectors.toSet()); + this.headers = headers.stream().map(String::toLowerCase).toList(); log.info("Configured with header names {}", headers); } @@ -48,9 +46,7 @@ public class HeaderMdcFilter extends AbstractMdcFilter { public Publisher> doFilter(HttpRequest request, ServerFilterChain chain) { var mdc = new HashMap(); for (var header : headers) { - request.getHeaders() - .getFirst(header) - .ifPresent(value -> mdc.put(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/layout/GcpJsonLayout.java b/src/main/java/io/kokuwa/micronaut/logging/layout/GcpJsonLayout.java index 139607c..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,12 +1,10 @@ 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; /** @@ -44,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); } 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/MDCTurboFilterConfigurer.java b/src/main/java/io/kokuwa/micronaut/logging/mdc/MDCTurboFilterConfigurer.java index e3abfb4..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,5 +1,6 @@ package io.kokuwa.micronaut.logging.mdc; +import java.util.Collection; import java.util.Set; import org.slf4j.Logger; @@ -11,37 +12,60 @@ 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 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 -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/http/AbstractFilterTest.java b/src/test/java/io/kokuwa/micronaut/logging/http/AbstractFilterTest.java index 56b68d0..4587dcb 100644 --- a/src/test/java/io/kokuwa/micronaut/logging/http/AbstractFilterTest.java +++ b/src/test/java/io/kokuwa/micronaut/logging/http/AbstractFilterTest.java @@ -7,20 +7,21 @@ 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 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 ch.qos.logback.classic.Level; 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; @@ -35,8 +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; /** * Test for {@link HttpServerFilter}. @@ -46,24 +47,14 @@ import jakarta.inject.Inject; @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 @@ -129,6 +120,7 @@ public abstract class AbstractFilterTest extends AbstractTest { } } + @Serdeable public static class TestResponse { private final String path; @@ -139,7 +131,7 @@ public abstract class AbstractFilterTest extends AbstractTest { public TestResponse( @JsonProperty("path") String path, @JsonProperty("level") String level, - @JsonProperty("context") Map context) { + @Nullable @JsonProperty("context") Map context) { this.path = path; this.level = level; this.context = context == null ? Map.of() : context; 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 index 3654d4d..3b7ac78 100644 --- a/src/test/java/io/kokuwa/micronaut/logging/http/mdc/PathMdcFilterTest.java +++ b/src/test/java/io/kokuwa/micronaut/logging/http/mdc/PathMdcFilterTest.java @@ -89,9 +89,9 @@ public class PathMdcFilterTest extends AbstractFilterTest { @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]+)") + @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); 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