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/.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 new file mode 100644 index 0000000..5f08047 --- /dev/null +++ b/.markdownlint.yaml @@ -0,0 +1,9 @@ +# Default state for all rules +default: true + +# 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.yaml b/.yamllint.yaml new file mode 100644 index 0000000..21966f2 --- /dev/null +++ b/.yamllint.yaml @@ -0,0 +1,15 @@ +extends: default + +## see https://yamllint.readthedocs.io/en/stable/rules.html +rules: + + # no need for document start + document-start: disable + + # line length is not important + line-length: disable + + # force double quotes everywhere + quoted-strings: + quote-type: double + required: only-when-needed diff --git a/LICENSE b/LICENSE index 261eeb9..dacd3ae 100644 --- a/LICENSE +++ b/LICENSE @@ -1,201 +1,288 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + EUROPEAN UNION PUBLIC LICENCE v. 1.2 + EUPL © the European Union 2007, 2016 - 1. Definitions. +This European Union Public Licence (the ‘EUPL’) applies to the Work (as defined +below) which is provided under the terms of this Licence. Any use of the Work, +other than as authorised under this Licence is prohibited (to the extent such +use is covered by a right of the copyright holder of the Work). - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. +The Work is provided under the terms of this Licence when the Licensor (as +defined below) has placed the following notice immediately following the +copyright notice for the Work: - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. + Licensed under the EUPL - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. +or has expressed by any other means his willingness to license under the EUPL. - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. +1. Definitions - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. +In this Licence, the following terms have the following meaning: - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. +- ‘The Licence’: this Licence. - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). +- ‘The Original Work’: the work or software distributed or communicated by the + Licensor under this Licence, available as Source Code and also as Executable + Code as the case may be. - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. +- ‘Derivative Works’: the works or software that could be created by the + Licensee, based upon the Original Work or modifications thereof. This Licence + does not define the extent of modification or dependence on the Original Work + required in order to classify a work as a Derivative Work; this extent is + determined by copyright law applicable in the country mentioned in Article 15. - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." +- ‘The Work’: the Original Work or its Derivative Works. - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. +- ‘The Source Code’: the human-readable form of the Work which is the most + convenient for people to study and modify. - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. +- ‘The Executable Code’: any code which has generally been compiled and which is + meant to be interpreted by a computer as a program. - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. +- ‘The Licensor’: the natural or legal person that distributes or communicates + the Work under the Licence. - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: +- ‘Contributor(s)’: any natural or legal person who modifies the Work under the + Licence, or otherwise contributes to the creation of a Derivative Work. - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and +- ‘The Licensee’ or ‘You’: any natural or legal person who makes any usage of + the Work under the terms of the Licence. - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and +- ‘Distribution’ or ‘Communication’: any act of selling, giving, lending, + renting, distributing, communicating, transmitting, or otherwise making + available, online or offline, copies of the Work or providing access to its + essential functionalities at the disposal of any other natural or legal + person. - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and +2. Scope of the rights granted by the Licence - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. +The Licensor hereby grants You a worldwide, royalty-free, non-exclusive, +sublicensable licence to do the following, for the duration of copyright vested +in the Original Work: - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. +- use the Work in any circumstance and for all usage, +- reproduce the Work, +- modify the Work, and make Derivative Works based upon the Work, +- communicate to the public, including the right to make available or display + the Work or copies thereof to the public and perform publicly, as the case may + be, the Work, +- distribute the Work or copies thereof, +- lend and rent the Work or copies thereof, +- sublicense rights in the Work or copies thereof. - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. +Those rights can be exercised on any media, supports and formats, whether now +known or later invented, as far as the applicable law permits so. - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. +In the countries where moral rights apply, the Licensor waives his right to +exercise his moral right to the extent allowed by law in order to make effective +the licence of the economic rights here above listed. - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. +The Licensor grants to the Licensee royalty-free, non-exclusive usage rights to +any patents held by the Licensor, to the extent necessary to make use of the +rights granted on the Work under this Licence. - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. +3. Communication of the Source Code - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. +The Licensor may provide the Work either in its Source Code form, or as +Executable Code. If the Work is provided as Executable Code, the Licensor +provides in addition a machine-readable copy of the Source Code of the Work +along with each copy of the Work that the Licensor distributes or indicates, in +a notice following the copyright notice attached to the Work, a repository where +the Source Code is easily and freely accessible for as long as the Licensor +continues to distribute or communicate the Work. - END OF TERMS AND CONDITIONS +4. Limitations on copyright - APPENDIX: How to apply the Apache License to your work. +Nothing in this Licence is intended to deprive the Licensee of the benefits from +any exception or limitation to the exclusive rights of the rights owners in the +Work, of the exhaustion of those rights or of other applicable limitations +thereto. - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. +5. Obligations of the Licensee - Copyright [yyyy] [name of copyright owner] +The grant of the rights mentioned above is subject to some restrictions and +obligations imposed on the Licensee. Those obligations are the following: - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at +Attribution right: The Licensee shall keep intact all copyright, patent or +trademarks notices and all notices that refer to the Licence and to the +disclaimer of warranties. The Licensee must include a copy of such notices and a +copy of the Licence with every copy of the Work he/she distributes or +communicates. The Licensee must cause any Derivative Work to carry prominent +notices stating that the Work has been modified and the date of modification. - http://www.apache.org/licenses/LICENSE-2.0 +Copyleft clause: If the Licensee distributes or communicates copies of the +Original Works or Derivative Works, this Distribution or Communication will be +done under the terms of this Licence or of a later version of this Licence +unless the Original Work is expressly distributed only under this version of the +Licence — for example by communicating ‘EUPL v. 1.2 only’. The Licensee +(becoming Licensor) cannot offer or impose any additional terms or conditions on +the Work or Derivative Work that alter or restrict the terms of the Licence. - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. +Compatibility clause: If the Licensee Distributes or Communicates Derivative +Works or copies thereof based upon both the Work and another work licensed under +a Compatible Licence, this Distribution or Communication can be done under the +terms of this Compatible Licence. For the sake of this clause, ‘Compatible +Licence’ refers to the licences listed in the appendix attached to this Licence. +Should the Licensee's obligations under the Compatible Licence conflict with +his/her obligations under this Licence, the obligations of the Compatible +Licence shall prevail. + +Provision of Source Code: When distributing or communicating copies of the Work, +the Licensee will provide a machine-readable copy of the Source Code or indicate +a repository where this Source will be easily and freely available for as long +as the Licensee continues to distribute or communicate the Work. + +Legal Protection: This Licence does not grant permission to use the trade names, +trademarks, service marks, or names of the Licensor, except as required for +reasonable and customary use in describing the origin of the Work and +reproducing the content of the copyright notice. + +6. Chain of Authorship + +The original Licensor warrants that the copyright in the Original Work granted +hereunder is owned by him/her or licensed to him/her and that he/she has the +power and authority to grant the Licence. + +Each Contributor warrants that the copyright in the modifications he/she brings +to the Work are owned by him/her or licensed to him/her and that he/she has the +power and authority to grant the Licence. + +Each time You accept the Licence, the original Licensor and subsequent +Contributors grant You a licence to their contributions to the Work, under the +terms of this Licence. + +7. Disclaimer of Warranty + +The Work is a work in progress, which is continuously improved by numerous +Contributors. It is not a finished work and may therefore contain defects or +‘bugs’ inherent to this type of development. + +For the above reason, the Work is provided under the Licence on an ‘as is’ basis +and without warranties of any kind concerning the Work, including without +limitation merchantability, fitness for a particular purpose, absence of defects +or errors, accuracy, non-infringement of intellectual property rights other than +copyright as stated in Article 6 of this Licence. + +This disclaimer of warranty is an essential part of the Licence and a condition +for the grant of any rights to the Work. + +8. Disclaimer of Liability + +Except in the cases of wilful misconduct or damages directly caused to natural +persons, the Licensor will in no event be liable for any direct or indirect, +material or moral, damages of any kind, arising out of the Licence or of the use +of the Work, including without limitation, damages for loss of goodwill, work +stoppage, computer failure or malfunction, loss of data or any commercial +damage, even if the Licensor has been advised of the possibility of such damage. +However, the Licensor will be liable under statutory product liability laws as +far such laws apply to the Work. + +9. Additional agreements + +While distributing the Work, You may choose to conclude an additional agreement, +defining obligations or services consistent with this Licence. However, if +accepting obligations, You may act only on your own behalf and on your sole +responsibility, not on behalf of the original Licensor or any other Contributor, +and only if You agree to indemnify, defend, and hold each Contributor harmless +for any liability incurred by, or claims asserted against such Contributor by +the fact You have accepted any warranty or additional liability. + +10. Acceptance of the Licence + +The provisions of this Licence can be accepted by clicking on an icon ‘I agree’ +placed under the bottom of a window displaying the text of this Licence or by +affirming consent in any other similar way, in accordance with the rules of +applicable law. Clicking on that icon indicates your clear and irrevocable +acceptance of this Licence and all of its terms and conditions. + +Similarly, you irrevocably accept this Licence and all of its terms and +conditions by exercising any rights granted to You by Article 2 of this Licence, +such as the use of the Work, the creation by You of a Derivative Work or the +Distribution or Communication by You of the Work or copies thereof. + +11. Information to the public + +In case of any Distribution or Communication of the Work by means of electronic +communication by You (for example, by offering to download the Work from a +remote location) the distribution channel or media (for example, a website) must +at least provide to the public the information requested by the applicable law +regarding the Licensor, the Licence and the way it may be accessible, concluded, +stored and reproduced by the Licensee. + +12. Termination of the Licence + +The Licence and the rights granted hereunder will terminate automatically upon +any breach by the Licensee of the terms of the Licence. + +Such a termination will not terminate the licences of any person who has +received the Work from the Licensee under the Licence, provided such persons +remain in full compliance with the Licence. + +13. Miscellaneous + +Without prejudice of Article 9 above, the Licence represents the complete +agreement between the Parties as to the Work. + +If any provision of the Licence is invalid or unenforceable under applicable +law, this will not affect the validity or enforceability of the Licence as a +whole. Such provision will be construed or reformed so as necessary to make it +valid and enforceable. + +The European Commission may publish other linguistic versions or new versions of +this Licence or updated versions of the Appendix, so far this is required and +reasonable, without reducing the scope of the rights granted by the Licence. New +versions of the Licence will be published with a unique version number. + +All linguistic versions of this Licence, approved by the European Commission, +have identical value. Parties can take advantage of the linguistic version of +their choice. + +14. Jurisdiction + +Without prejudice to specific agreement between parties, + +- any litigation resulting from the interpretation of this License, arising + between the European Union institutions, bodies, offices or agencies, as a + Licensor, and any Licensee, will be subject to the jurisdiction of the Court + of Justice of the European Union, as laid down in article 272 of the Treaty on + the Functioning of the European Union, + +- any litigation arising between other parties and resulting from the + interpretation of this License, will be subject to the exclusive jurisdiction + of the competent court where the Licensor resides or conducts its primary + business. + +15. Applicable Law + +Without prejudice to specific agreement between parties, + +- this Licence shall be governed by the law of the European Union Member State + where the Licensor has his seat, resides or has his registered office, + +- this licence shall be governed by Belgian law if the Licensor has no seat, + residence or registered office inside a European Union Member State. + +Appendix + +‘Compatible Licences’ according to Article 5 EUPL are: + +- GNU General Public License (GPL) v. 2, v. 3 +- GNU Affero General Public License (AGPL) v. 3 +- Open Software License (OSL) v. 2.1, v. 3.0 +- Eclipse Public License (EPL) v. 1.0 +- CeCILL v. 2.0, v. 2.1 +- Mozilla Public Licence (MPL) v. 2 +- GNU Lesser General Public Licence (LGPL) v. 2.1, v. 3 +- Creative Commons Attribution-ShareAlike v. 3.0 Unported (CC BY-SA 3.0) for + works other than software +- European Union Public Licence (EUPL) v. 1.1, v. 1.2 +- Québec Free and Open-Source Licence — Reciprocity (LiLiQ-R) or Strong + Reciprocity (LiLiQ-R+). + +The European Commission may update this Appendix to later versions of the above +licences without producing a new version of the EUPL, as long as they provide +the rights granted in Article 2 of this Licence and protect the covered Source +Code from exclusive appropriation. + +All other changes or additions to this Appendix require the production of a new +EUPL version. diff --git a/README.md b/README.md index 378e985..4a3a2ec 100644 --- a/README.md +++ b/README.md @@ -1,131 +1,44 @@ # Micronaut Logging support +Enhanced logging for Micronaut using MDC or request header. + +[![maven](https://img.shields.io/maven-central/v/io.kokuwa.micronaut/micronaut-logging.svg?label=maven)](https://central.sonatype.com/artifact/io.kokuwa.micronaut/micronaut-logging) +[![license](https://img.shields.io/badge/license-EUPL%201.2-blue)](https://git.kokuwa.io/kokuwaio/micronaut-logging/src/branch/main/LICENSE) +[![issues](https://img.shields.io/gitea/issues/open/kokuwaio/micronaut-logging?gitea_url=https%3A%2F%2Fgit.kokuwa.io)](https://git.kokuwa.io/kokuwaio/micronaut-logging/issues) +[![prs](https://img.shields.io/gitea/pull-requests/open/kokuwaio/micronaut-logging?gitea_url=https%3A%2F%2Fgit.kokuwa.io)](https://git.kokuwa.io/kokuwaio/micronaut-logging/pulls) +[![build](https://ci.kokuwa.io/api/badges/kokuwaio/micronaut-logging/status.svg)](https://ci.kokuwa.io/repos/kokuwaio/micronaut-logging/) + +Include in your `pom.xml`: + +```xml + + io.kokuwa.micronaut + micronaut-logging + ${version.io.kokuwa.micronaut.logging} + runtime + + + + io.micronaut.serde + micronaut-serde-jsonp + runtime + +``` + ## Features -### Preconfigured Appender - -Buildin appender: - * console format - * stackdriver format (with support for error reporting) - -### Set log level based on MDC values - -Confguration: - * *enabled*: enable MDC filter (`true` is default) - * *key*: MDC key, is optional (will use name instead, see example `user` below) - * *level*: log level to use (`TRACE` is default) - * *loggers*: whitelist of logger names, matches all loggers if empty - * *values*: values for matching MDC key, matches all values if empty - -Example for setting different values for different values/logger: -``` -logger: - levels: - io.kokuwa: INFO - mdc: - gateway-debug: - key: gateway - level: DEBUG - loggers: - - io.kokuwa - values: - - 6a1bae7f-eb6c-4c81-af9d-dc15396584e2 - - fb3318f1-2c73-48e9-acd4-a2be3c9f9256 - gateway-trace: - key: gateway - level: TRACE - loggers: - - io.kokuwa - - io.micronaut - values: - - 257802b2-22fe-4dcc-bb99-c1db2a47861f -``` - -Example for omiting level and key: -``` -logger: - levels: - io.kokuwa: INFO - mdc: - gateway: - loggers: - - io.kokuwa - values: - - 257802b2-22fe-4dcc-bb99-c1db2a47861f - - 0a44738b-0c3a-4798-8210-2495485f10b2 -``` - -Example for minimal configuration: -``` -logger: - levels: - io.kokuwa: INFO - mdc: - user: {} -``` - -### Set log level based on HTTP request header - -Confguration: - * *enabled*: enable HTTP request filter (`true` is default) - * *order*: order for [Ordered](https://github.com/micronaut-projects/micronaut-core/blob/master/core/src/main/java/io/micronaut/core/order/Ordered.java) (highest is default) - * *pattern*: filter pattern (`/**` is default) - * *header*: name of HTTP header (`x-log-level` is default) - -Example with default configuration: -``` -logger: - request: - header: - enabled: true - order: -2147483648 - pattern: /** - header: x-log-level -``` - -### Add princial for request to MDC - -Confguration: - * *enabled*: enable HTTP principal filter (`true` is default) - * *order*: order for [Ordered](https://github.com/micronaut-projects/micronaut-core/blob/master/core/src/main/java/io/micronaut/core/order/Ordered.java) ([ServerFilterPhase.SECURITY.after()](https://github.com/micronaut-projects/micronaut-core/blob/v2.0.1/http/src/main/java/io/micronaut/http/filter/ServerFilterPhase.java#L54) is default) - * *pattern*: filter pattern (`/**` is default) - * *key*: name of MDC header (`principal` is default) - -Example with default configuration: -``` -logger: - request: - principal: - enabled: true - order: 39250 - pattern: /** - key: principal -``` - -## Build & Release - -### Dependency updates - -Display dependency updates: -``` -mvn versions:display-property-updates -U -``` - -Update dependencies: -``` -mvn versions:update-properties -``` - -### Release locally - -Run: -``` -mvn release:prepare release:perform release:clean -B -``` +* Version [3.x](https://github.com/kokuwaio/micronaut-logging/tree/3.x) is based on SLF4J 1.7 & Logback 1.2 & Micronaut 3.x +* Version [4.x](https://github.com/kokuwaio/micronaut-logging/tree/main) is based on SLF4J 2.0 & Logback 1.4 & Micronaut 4.x +* [set log level based on MDC values](docs/features/logback_mdc_level.md) +* [add default xml](docs/features/logback_default.md) +* [preconfigured appender for different environments](docs/features/logback_appender.md) +* [set log level based on HTTP request header](docs/features/http_log_level.md) +* [add HTTP path parts to MDC](docs/features/http_mdc_path.md) +* [add HTTP header to MDC](docs/features/http_mdc_header.md) +* [add authentication information from HTTP request to MDC](docs/features/http_mdc_authentication.md) ## Open Topics - * configure mdc on refresh event - * add stackdriver per configuration - * add fluent per configuration - * read **serviceName** and **serviceVersion** from yaml +* configure mdc on refresh event +* read **serviceName** and **serviceVersion** from yaml +* support auto select appender with custom `logback.xml` diff --git a/docs/features/http_log_level.md b/docs/features/http_log_level.md new file mode 100644 index 0000000..59b960d --- /dev/null +++ b/docs/features/http_log_level.md @@ -0,0 +1,35 @@ +# Set log level based on HTTP request header + +With this features it is possible to set the log level while processing a request by adding the http header `x-log-level` with value `TRACE`. This log level is propagated to HTTP client requests. + +## Properties + +Property | Description | Default +-------- | ----------- | ------- +`logger.http.level.enabled` | filter enabled? | `true` +`logger.http.level.path` | filter path | `/**` +`logger.http.level.order` | order for [Ordered](https://github.com/micronaut-projects/micronaut-core/blob/v3.2.0/core/src/main/java/io/micronaut/core/order/Ordered.java) | [ServerFilterPhase.FIRST.before()](https://github.com/micronaut-projects/micronaut-core/blob/v3.2.0/http/src/main/java/io/micronaut/http/filter/ServerFilterPhase.java#L34) +`logger.http.level.header` | name of HTTP header | `x-log-level` +`logger.http.level.propagation.enabled` | propagation enabled? | `true` +`logger.http.level.propagation.path` | propagation path | `/**` +`logger.http.level.propagation.order` | order for [Ordered](https://github.com/micronaut-projects/micronaut-core/blob/v3.2.0/core/src/main/java/io/micronaut/core/order/Ordered.java) | [Order.HIGHEST_PRECEDENCE](https://github.com/micronaut-projects/micronaut-core/blob/v3.2.0/core/src/main/java/io/micronaut/core/order/Ordered.java#L30) +`logger.http.level.propagation.header` | name of HTTP header | see `logger.http.level.header` + +## Examples + +Default configuration: + +```yaml +logger: + http: + level: + enabled: true + order: -1000 + path: /** + header: x-log-level + propagation: + enabled: true + order: 2147483648 + path: /** + header: ${logger.http.level.header} +``` diff --git a/docs/features/http_mdc_authentication.md b/docs/features/http_mdc_authentication.md new file mode 100644 index 0000000..ff4c4e9 --- /dev/null +++ b/docs/features/http_mdc_authentication.md @@ -0,0 +1,29 @@ +# Add authentication information to MDC + +This only applies to HTTP requests with successful security authentication. + +## Properties + +Property | Description | Default +-------- | ----------- | ------- +`logger.http.authentication.enabled` | filter enabled? | `true` +`logger.http.authentication.path` | filter path | `/**` +`logger.http.authentication.order` | order for [Ordered](https://github.com/micronaut-projects/micronaut-core/blob/v3.2.0/core/src/main/java/io/micronaut/core/order/Ordered.java) | [ServerFilterPhase.SECURITY.after()](https://github.com/micronaut-projects/micronaut-core/blob/v3.2.0/http/src/main/java/io/micronaut/http/filter/ServerFilterPhase.java#L54) +`logger.http.authentication.prefix` | prefix to MDC key | `` +`logger.http.authentication.name` | MDC key of authentication name | `principal` +`logger.http.authentication.attributes` | authentication attributes to add to MDC, | `[]` + +## Examples + +Configuration for adding some jwt claims: + +```yaml +logger: + http: + authentication: + prefix: jwt. + name: sub + attributes: + - aud + - azp +``` diff --git a/docs/features/http_mdc_header.md b/docs/features/http_mdc_header.md new file mode 100644 index 0000000..8a8e9d4 --- /dev/null +++ b/docs/features/http_mdc_header.md @@ -0,0 +1,28 @@ +# Add HTTP headers to MDC + +## Properties + +Property | Description | Default +-------- | ----------- | ------- +`logger.http.header.enabled` | filter enabled? | `true` +`logger.http.header.path` | filter path | `/**` +`logger.http.header.order` | order for [Ordered](https://github.com/micronaut-projects/micronaut-core/blob/v3.2.0/core/src/main/java/io/micronaut/core/order/Ordered.java) | [ServerFilterPhase.FIRST.before()](https://github.com/micronaut-projects/micronaut-core/blob/v3.2.0/http/src/main/java/io/micronaut/http/filter/ServerFilterPhase.java#L34) +`logger.http.header.prefix` | prefix to MDC key | `` +`logger.http.header.names` | http header names to add to MDC | `[]` + +## Examples + +Configuration for b3-propagation: + +```yaml +logger: + http: + header: + prefix: header. + names: + - x-request-id + - x-b3-traceId + - x-b3-parentspanid + - x-b3-spanid + - x-b3-sampled +``` diff --git a/docs/features/http_mdc_path.md b/docs/features/http_mdc_path.md new file mode 100644 index 0000000..b5f9f73 --- /dev/null +++ b/docs/features/http_mdc_path.md @@ -0,0 +1,25 @@ +# Add HTTP path parts to MDC + +## Properties + +Property | Description | Default +-------- | ----------- | ------- +`logger.http.path.enabled` | filter enabled? | `true` +`logger.http.path.path` | filter path | `/**` +`logger.http.path.order` | order for [Ordered](https://github.com/micronaut-projects/micronaut-core/blob/v3.2.0/core/src/main/java/io/micronaut/core/order/Ordered.java) | [ServerFilterPhase.FIRST.before()](https://github.com/micronaut-projects/micronaut-core/blob/v3.2.0/http/src/main/java/io/micronaut/http/filter/ServerFilterPhase.java#L34) +`logger.http.path.prefix` | prefix to MDC key | `` +`logger.http.path.patterns` | patterns with groups to add to MDC | `[]` + +## Examples + +Configuration for adding ids: + +```yaml +logger: + http: + path: + prefix: path. + patterns: + - \/gateway\/(?[a-f0-9\-]{36}) + - \/gateway\/(?[a-f0-9\-]{36})\/configuration\/(?[a-z]+) +``` diff --git a/docs/features/logback_appender.md b/docs/features/logback_appender.md new file mode 100644 index 0000000..d71885b --- /dev/null +++ b/docs/features/logback_appender.md @@ -0,0 +1,14 @@ +# Appender + +## Available Appender + +* console with jansi for developers +* gcp logging format (with support for error reporting) +* json + +## AutoSelect appender + +1. if `LOGBACK_APPENDER` is set this appender will be used +2. if GCP is detected gcp appender will be used +3. if Kubernetes is detected json appender will be used +4. console appender else diff --git a/docs/features/logback_default.md b/docs/features/logback_default.md new file mode 100644 index 0000000..5b34f1a --- /dev/null +++ b/docs/features/logback_default.md @@ -0,0 +1,16 @@ +# Add default logback.xml + +If no `logback.xml` by user is provided a default [logback.xml](../../src/main/resources/io/kokuwa/logback/logback-default.xml) is loaded. Otherwise use custom [logback.xml](../../src/main/resources/io/kokuwa/logback/logback-example.xml): + +```xml + + + + + + + + + + +``` diff --git a/docs/features/logback_mdc_level.md b/docs/features/logback_mdc_level.md new file mode 100644 index 0000000..ee1e853 --- /dev/null +++ b/docs/features/logback_mdc_level.md @@ -0,0 +1,66 @@ +# Set log level based on MDC values + +This can be used to change the log level based on MDC valus. E.g. change log levels for specific users/services etc. + +## Properties + +Property | Description | Default +-------- | ----------- | ------- +`logger.mdc.enabled` | MDC enabled? | `true` +`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 | `[]` +`logger.mdc..values` | values for matching MDC key, matches all values if empty | `[]` + +## Examples + +Minimal configuration that logs everything with `TRACE` if MDC `principal` is present: + +```yaml +logger: + levels: + io.kokuwa: INFO + mdc: + principal: {} +``` + +Configuration that logs everything with `TRACE` for logger `io.kokuwa` if MDC `gateway` matches one value: + +```yaml +logger: + levels: + io.kokuwa: INFO + mdc: + gateway: + loggers: + - io.kokuwa + values: + - 257802b2-22fe-4dcc-bb99-c1db2a47861f + - 0a44738b-0c3a-4798-8210-2495485f10b2 +``` + +Complex example with setting different values for different values/logger: + +```yaml +logger: + levels: + io.kokuwa: INFO + mdc: + gateway-debug: + key: gateway + level: DEBUG + loggers: + - io.kokuwa + values: + - 6a1bae7f-eb6c-4c81-af9d-dc15396584e2 + - fb3318f1-2c73-48e9-acd4-a2be3c9f9256 + gateway-trace: + key: gateway + level: TRACE + loggers: + - io.kokuwa + - io.micronaut + values: + - 257802b2-22fe-4dcc-bb99-c1db2a47861f +``` diff --git a/pom.xml b/pom.xml index f4b63d6..22f50e6 100644 --- a/pom.xml +++ b/pom.xml @@ -1,80 +1,73 @@ - + 4.0.0 - - io.kokuwa - maven-parent - 0.1.0 - - - io.kokuwa.micronaut micronaut-logging - 0.0.2 + 5.0.1-SNAPSHOT - Micronaut Logging Support - TODO - https://github.com/kokuwaio/micronaut-logging + Logging Support for Micronaut + Enhanced logging using MDC or request header. + https://git.kokuwa.io/kokuwaio/micronaut-logging 2020 + + Kokuwa.io + http://kokuwa.io + - Apache License 2.0 - https://www.apache.org/licenses/LICENSE-2.0 + EUPL-1.2 + https://eupl.eu/1.2/en + repo + + + stephan.schnabel + Stephan Schnabel + https://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 - 0.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 + + woodpecker + https://ci.kokuwa.io/repos/kokuwaio/micronaut-logging + + + + sonatype.org + https://central.sonatype.com/repository/maven-snapshots/ + + - - - - - - 0.1.5 - 2.0.1 - + 2025-06-27T18:11:48Z + UTF-8 + 17 - - - io.micronaut - micronaut-bom - ${version.io.micronaut} + io.micronaut.platform + micronaut-platform + 4.9.0 pom import - - - - ch.qos.logback.contrib - logback-json-classic - ${version.ch.qos.logback.contrib} - - - ch.qos.logback.contrib - logback-json-core - ${version.ch.qos.logback.contrib} - - - ch.qos.logback.contrib - logback-jackson - ${version.ch.qos.logback.contrib} - - @@ -82,9 +75,18 @@ io.micronaut - micronaut-runtime + micronaut-http provided + + io.micronaut.security + micronaut-security + provided + + + io.micronaut.serde + micronaut-serde-api + io.micronaut.test micronaut-test-junit5 @@ -105,52 +107,360 @@ 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-jackson - - - ch.qos.logback.contrib - logback-json-classic + ch.qos.logback + logback-core - - - src/test/resources - true - - + verify + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.14.0 + + class + true + true + true + -Xlint:all,-processing + + + io.micronaut + micronaut-inject-java + + + + + + org.apache.maven.plugins + maven-deploy-plugin + 3.1.4 + + + org.apache.maven.plugins + maven-gpg-plugin + 3.2.7 + + + org.apache.maven.plugins + maven-install-plugin + 3.1.4 + + + org.apache.maven.plugins + maven-invoker-plugin + 3.9.1 + + + org.apache.maven.plugins + maven-jar-plugin + 3.4.2 + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.11.2 + + + org.apache.maven.plugins + maven-release-plugin + 3.1.1 + + verify + check + deploy + deploy,release + true + @{prefix} prepare release @{releaseLabel} [CI SKIP] + @{project.version} + + + + org.apache.maven.plugins + maven-resources-plugin + 3.3.1 + + ISO-8859-1 + + + + org.apache.maven.plugins + maven-source-plugin + 3.3.1 + + + org.apache.maven.plugins + maven-surefire-plugin + 3.5.3 + + + org.codehaus.mojo + tidy-maven-plugin + 1.4.0 + + + org.sonatype.central + central-publishing-maven-plugin + 0.8.0 + + + net.revelc.code.formatter + formatter-maven-plugin + 2.27.0 + + ${project.basedir}/src/eclipse/formatter.xml + + + + net.revelc.code + impsort-maven-plugin + 1.12.0 + + true + java.,javax.,jakarta.,org. + + + + - + org.apache.maven.plugins - maven-compiler-plugin - - - - org.projectlombok - lombok - ${version.org.projectlombok} - - - io.micronaut - micronaut-inject-java - ${version.io.micronaut} - - - + maven-invoker-plugin + + + + install + integration-test + verify + + + ${project.build.directory}/its + true + test + false + true + + + + + + + + org.apache.maven.plugins + maven-install-plugin + + + default-install + + + + + + + dev + + + !env.CI + + + + true + + + + + org.codehaus.mojo + tidy-maven-plugin + + + validate + + pom + + + + + + net.revelc.code + impsort-maven-plugin + + + validate + + sort + + + + + + net.revelc.code.formatter + formatter-maven-plugin + + + validate + + format + + + + + + + + + check + + + env.CI + + + + + + org.codehaus.mojo + tidy-maven-plugin + + + validate + + check + + + + + + net.revelc.code + impsort-maven-plugin + + + validate + + check + + + + + + net.revelc.code.formatter + formatter-maven-plugin + + + validate + + validate + + + + + + + + + deploy + + + env.CI + + + + + + + + org.apache.maven.plugins + maven-source-plugin + + + + jar + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + + + + jar + + + + + + + + org.apache.maven.plugins + maven-gpg-plugin + + + + sign + + + bc + + + + + + + + + + release + + + + org.sonatype.central + central-publishing-maven-plugin + true + + sonatype.org + true + published + + + + + + diff --git a/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 625191e..bb21701 100644 --- a/src/main/java/io/kokuwa/micronaut/logging/LogbackUtil.java +++ b/src/main/java/io/kokuwa/micronaut/logging/LogbackUtil.java @@ -4,7 +4,7 @@ import java.util.Objects; import java.util.Optional; import java.util.function.Supplier; -import javax.inject.Singleton; +import jakarta.inject.Singleton; import org.slf4j.LoggerFactory; @@ -12,7 +12,6 @@ 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; /** * Utility class for Logback operations. @@ -22,7 +21,6 @@ import io.micronaut.core.annotation.Internal; @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 new file mode 100644 index 0000000..3b1a51a --- /dev/null +++ b/src/main/java/io/kokuwa/micronaut/logging/configurator/DefaultConfigurator.java @@ -0,0 +1,43 @@ +package io.kokuwa.micronaut.logging.configurator; + +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; + +/** + * Use logback-default.xml if no configuration is provided by user. + * + * @author Stephan Schnabel + */ +public class DefaultConfigurator extends ContextAwareBase implements Configurator { + + @SuppressWarnings("deprecation") + @Override + 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 ExecutionStatus.NEUTRAL; + } + + try { + addInfo("Use logback.xml from io.kokuwa:micronaut-logging"); + var configurator = new MicronautJoranConfigurator(); + configurator.setContext(loggerContext); + configurator.doConfigure(base); + } catch (JoranException e) { + addError("Failed to load logback.xml from io.kokuwa:micronaut-logging", e); + return ExecutionStatus.NEUTRAL; + } + + 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 new file mode 100644 index 0000000..a1010d7 --- /dev/null +++ b/src/main/java/io/kokuwa/micronaut/logging/configurator/MicronautJoranConfigurator.java @@ -0,0 +1,19 @@ +package io.kokuwa.micronaut.logging.configurator; + +import ch.qos.logback.classic.joran.JoranConfigurator; +import ch.qos.logback.core.joran.spi.ElementSelector; +import ch.qos.logback.core.joran.spi.RuleStore; + +/** + * Add custom actions. + * + * @author Stephan Schnabel + */ +public class MicronautJoranConfigurator extends JoranConfigurator { + + @Override + 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 new file mode 100644 index 0000000..1d0db03 --- /dev/null +++ b/src/main/java/io/kokuwa/micronaut/logging/configurator/RootAutoSelectAppenderAction.java @@ -0,0 +1,130 @@ +package io.kokuwa.micronaut.logging.configurator; + +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.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.spi.SaxEventInterpretationContext; +import io.kokuwa.micronaut.logging.layout.GcpJsonLayout; +import io.kokuwa.micronaut.logging.layout.JsonLayout; +import io.micronaut.core.util.StringUtils; + +/** + * Auto select appender by environment. + * + * @author Stephan Schnabel + */ +public class RootAutoSelectAppenderAction extends Action { + + private static final boolean IS_KUBERNETES = StringUtils.isNotEmpty(System.getenv("KUBERNETES_SERVICE_HOST")); + private static final boolean IS_GCP = StringUtils.isNotEmpty(System.getenv("GOOGLE_CLOUD_PROJECT")); + + private static final String APPENDER_CONSOLE = "CONSOLE"; + 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(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(); + if (rootLoggerAppenders.hasNext()) { + addWarn("Skip because appender already found: " + rootLoggerAppenders.next().getName()); + return; + } + + var envAppender = env(LOGBACK_APPENDER, null); + if (envAppender != null) { + setAppender(rootLogger, envAppender); + return; + } + + if (IS_KUBERNETES) { + setAppender(rootLogger, APPENDER_JSON); + return; + } + if (IS_GCP) { + setAppender(rootLogger, APPENDER_GCP); + return; + } + setAppender(rootLogger, APPENDER_CONSOLE); + } + + @Override + 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); + } + + private Layout console() { + var layout = new PatternLayout(); + layout.setContext(context); + layout.setPattern(env(LOGBACK_PATTERN, LOGBACK_PATTERN_DEFAULT)); + return layout; + } + + private Layout json() { + var layout = new JsonLayout(); + layout.setContext(context); + return layout; + } + + private Layout gcp() { + var layout = new GcpJsonLayout(); + layout.setContext(context); + layout.setServiceName(env("SERVICE_NAME", null)); + layout.setServiceVersion(env("SERVICE_VERSION", null)); + return layout; + } + + private String env(String name, String defaultValue) { + var envValue = Optional.ofNullable(System.getenv(name)).map(String::trim).filter(StringUtils::isNotEmpty); + var finalValue = envValue.orElse(defaultValue); + if (envValue.isPresent()) { + addInfo("Use provided config: " + name + "=" + finalValue); + } else { + addInfo("Use default config: " + name + "=" + finalValue); + } + return finalValue; + } +} diff --git a/src/main/java/io/kokuwa/micronaut/logging/http/AbstractMdcFilter.java b/src/main/java/io/kokuwa/micronaut/logging/http/AbstractMdcFilter.java new file mode 100644 index 0000000..b4634be --- /dev/null +++ b/src/main/java/io/kokuwa/micronaut/logging/http/AbstractMdcFilter.java @@ -0,0 +1,56 @@ +package io.kokuwa.micronaut.logging.http; + +import java.util.Map; + +import org.reactivestreams.Publisher; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.slf4j.MDC; + +import io.micronaut.core.async.publisher.Publishers; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.MutableHttpResponse; +import io.micronaut.http.filter.HttpServerFilter; +import io.micronaut.http.filter.ServerFilterChain; + +/** + * Base for all MDC related http filters. + * + * @author Stephan Schnabel + */ +public abstract class AbstractMdcFilter implements HttpServerFilter { + + protected final Logger log = LoggerFactory.getLogger(getClass()); + protected final int order; + protected final String prefix; + + protected AbstractMdcFilter(Integer order, String prefix) { + this.order = order; + this.prefix = prefix; + } + + @Override + public int getOrder() { + return order; + } + + protected Publisher> doFilter( + HttpRequest request, + ServerFilterChain chain, + Map mdc) { + + if (mdc.isEmpty()) { + return chain.proceed(request); + } + + mdc.forEach((key, value) -> MDC.put(addPrefix(key), value)); + return Publishers.map(chain.proceed(request), response -> { + mdc.keySet().forEach(key -> MDC.remove(addPrefix(key))); + return response; + }); + } + + private String addPrefix(String key) { + return prefix == null ? key : prefix + key; + } +} diff --git a/src/main/java/io/kokuwa/micronaut/logging/http/level/LogLevelClientFilter.java b/src/main/java/io/kokuwa/micronaut/logging/http/level/LogLevelClientFilter.java new file mode 100644 index 0000000..511829d --- /dev/null +++ b/src/main/java/io/kokuwa/micronaut/logging/http/level/LogLevelClientFilter.java @@ -0,0 +1,55 @@ +package io.kokuwa.micronaut.logging.http.level; + +import java.util.Optional; + +import org.reactivestreams.Publisher; + +import io.micronaut.context.annotation.Requires; +import io.micronaut.context.annotation.Value; +import io.micronaut.core.util.StringUtils; +import io.micronaut.http.HttpResponse; +import io.micronaut.http.MutableHttpRequest; +import io.micronaut.http.annotation.Filter; +import io.micronaut.http.context.ServerRequestContext; +import io.micronaut.http.filter.ClientFilterChain; +import io.micronaut.http.filter.HttpClientFilter; + +/** + * Propagates log-level from server request to client. + * + * @author Stephan Schnabel + */ +@Requires(beans = LogLevelServerFilter.class) +@Requires(property = LogLevelClientFilter.PREFIX + ".enabled", notEquals = StringUtils.FALSE) +@Filter("${" + LogLevelClientFilter.PREFIX + ".path:/**}") +public class LogLevelClientFilter implements HttpClientFilter { + + public static final String PREFIX = "logger.http.level.propagation"; + public static final int DEFAULT_ORDER = HIGHEST_PRECEDENCE; + + private final String serverHeader; + private final String propagationHeader; + private final int order; + + public LogLevelClientFilter( + @Value("${" + LogLevelServerFilter.PREFIX + ".header}") Optional serverHeader, + @Value("${" + PREFIX + ".header}") Optional propagationHeader, + @Value("${" + PREFIX + ".order}") Optional order) { + this.serverHeader = serverHeader.orElse(LogLevelServerFilter.DEFAULT_HEADER); + this.propagationHeader = propagationHeader.orElse(this.serverHeader); + this.order = order.orElse(DEFAULT_ORDER); + } + + @Override + public int getOrder() { + return order; + } + + @Override + public Publisher> doFilter(MutableHttpRequest targetRequest, ClientFilterChain chain) { + ServerRequestContext.currentRequest() + .flatMap(currentRequest -> currentRequest.getHeaders().getFirst(serverHeader)) + .ifPresent(level -> targetRequest.getHeaders().add(propagationHeader, level)); + return chain.proceed(targetRequest); + } +} 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 new file mode 100644 index 0000000..6626fee --- /dev/null +++ b/src/main/java/io/kokuwa/micronaut/logging/http/level/LogLevelServerFilter.java @@ -0,0 +1,68 @@ +package io.kokuwa.micronaut.logging.http.level; + +import java.util.Map; +import java.util.Optional; + +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; + +import org.reactivestreams.Publisher; + +import ch.qos.logback.classic.turbo.TurboFilter; +import io.kokuwa.micronaut.logging.LogbackUtil; +import io.kokuwa.micronaut.logging.http.AbstractMdcFilter; +import io.micronaut.context.annotation.Requires; +import io.micronaut.context.annotation.Value; +import io.micronaut.core.util.StringUtils; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.MutableHttpResponse; +import io.micronaut.http.annotation.Filter; +import io.micronaut.http.filter.ServerFilterChain; +import io.micronaut.http.filter.ServerFilterPhase; +import io.micronaut.runtime.context.scope.Refreshable; + +/** + * Http request logging filter. + * + * @author Stephan Schnabel + */ +@Refreshable +@Requires(property = LogLevelServerFilter.PREFIX + ".enabled", notEquals = StringUtils.FALSE) +@Filter("${" + LogLevelServerFilter.PREFIX + ".path:/**}") +public class LogLevelServerFilter extends AbstractMdcFilter { + + public static final String PREFIX = "logger.http.level"; + public static final String DEFAULT_HEADER = "x-log-level"; + public static final int DEFAULT_ORDER = ServerFilterPhase.FIRST.before(); + public static final String MDC_KEY = "level"; + public static final String MDC_FILTER = PREFIX; + + private final LogbackUtil logback; + private final String header; + + public LogLevelServerFilter( + LogbackUtil logback, + @Value("${" + PREFIX + ".header}") Optional header, + @Value("${" + PREFIX + ".order}") Optional order) { + super(order.orElse(DEFAULT_ORDER), null); + this.logback = logback; + this.header = header.orElse(DEFAULT_HEADER); + } + + @PostConstruct + void startTurbofilter() { + logback.getTurboFilter(LogLevelTurboFilter.class, MDC_FILTER, LogLevelTurboFilter::new).start(); + } + + @PreDestroy + void stopTurbofilter() { + logback.getTurboFilter(LogLevelTurboFilter.class, MDC_FILTER).ifPresent(TurboFilter::stop); + } + + @Override + public Publisher> doFilter(HttpRequest request, ServerFilterChain chain) { + return request.getHeaders().getFirst(header) + .map(level -> doFilter(request, chain, Map.of(MDC_KEY, level))) + .orElseGet(() -> chain.proceed(request)); + } +} diff --git a/src/main/java/io/kokuwa/micronaut/logging/request/HeaderLoggingTurboFilter.java b/src/main/java/io/kokuwa/micronaut/logging/http/level/LogLevelTurboFilter.java similarity index 80% rename from src/main/java/io/kokuwa/micronaut/logging/request/HeaderLoggingTurboFilter.java rename to src/main/java/io/kokuwa/micronaut/logging/http/level/LogLevelTurboFilter.java index 17e42ea..57df67f 100644 --- a/src/main/java/io/kokuwa/micronaut/logging/request/HeaderLoggingTurboFilter.java +++ b/src/main/java/io/kokuwa/micronaut/logging/http/level/LogLevelTurboFilter.java @@ -1,4 +1,4 @@ -package io.kokuwa.micronaut.logging.request; +package io.kokuwa.micronaut.logging.http.level; import org.slf4j.MDC; import org.slf4j.Marker; @@ -13,7 +13,7 @@ import ch.qos.logback.core.spi.FilterReply; * * @author Stephan Schnabel */ -public class HeaderLoggingTurboFilter extends TurboFilter { +public class LogLevelTurboFilter extends TurboFilter { @Override public FilterReply decide(Marker marker, Logger logger, Level level, String format, Object[] params, Throwable t) { @@ -22,7 +22,7 @@ public class HeaderLoggingTurboFilter extends TurboFilter { return FilterReply.NEUTRAL; } - var value = MDC.get(HeaderLoggingHttpFilter.MDC_KEY); + var value = MDC.get(LogLevelServerFilter.MDC_KEY); if (value == null) { return FilterReply.NEUTRAL; } 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 new file mode 100644 index 0000000..adc2946 --- /dev/null +++ b/src/main/java/io/kokuwa/micronaut/logging/http/mdc/AuthenticationMdcFilter.java @@ -0,0 +1,77 @@ +package io.kokuwa.micronaut.logging.http.mdc; + +import java.util.HashMap; +import java.util.Optional; +import java.util.Set; + +import org.reactivestreams.Publisher; + +import io.kokuwa.micronaut.logging.http.AbstractMdcFilter; +import io.micronaut.context.annotation.Requires; +import io.micronaut.context.annotation.Value; +import io.micronaut.core.util.StringUtils; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.MutableHttpResponse; +import io.micronaut.http.annotation.Filter; +import io.micronaut.http.filter.ServerFilterChain; +import io.micronaut.http.filter.ServerFilterPhase; +import io.micronaut.runtime.context.scope.Refreshable; +import io.micronaut.security.authentication.Authentication; + +/** + * Filter to add claims from authentication to MDC. + * + * @author Stephan Schnabel + */ +@Refreshable +@Requires(classes = Authentication.class) +@Requires(property = AuthenticationMdcFilter.PREFIX + ".enabled", notEquals = StringUtils.FALSE) +@Filter("${" + AuthenticationMdcFilter.PREFIX + ".path:/**}") +public class AuthenticationMdcFilter extends AbstractMdcFilter { + + public static final String PREFIX = "logger.http.authentication"; + public static final String DEFAULT_NAME = "principal"; + public static final int DEFAULT_ORDER = ServerFilterPhase.SECURITY.after(); + + private final String name; + private final Set attributes; + + public AuthenticationMdcFilter( + @Value("${" + PREFIX + ".name}") Optional name, + @Value("${" + PREFIX + ".attributes}") Optional> attributes, + @Value("${" + PREFIX + ".prefix}") Optional prefix, + @Value("${" + PREFIX + ".order}") Optional order) { + super(order.orElse(DEFAULT_ORDER), prefix.orElse(null)); + 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 {}", this.name, this.attributes); + } + } + + @Override + public Publisher> doFilter(HttpRequest request, ServerFilterChain chain) { + + // get authentication + + var authenticationOptional = request.getUserPrincipal(Authentication.class); + if (authenticationOptional.isEmpty()) { + return chain.proceed(request); + } + var authentication = authenticationOptional.get(); + var authenticationAttributes = authentication.getAttributes(); + + // add mdc + + var mdc = new HashMap(); + mdc.put(name, authentication.getName()); + for (var attibuteName : attributes) { + var attibuteValue = authenticationAttributes.get(attibuteName); + if (attibuteValue != null) { + mdc.put(attibuteName, String.valueOf(attibuteValue)); + } + } + + return doFilter(request, chain, mdc); + } +} 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 new file mode 100644 index 0000000..a5a8b85 --- /dev/null +++ b/src/main/java/io/kokuwa/micronaut/logging/http/mdc/HeaderMdcFilter.java @@ -0,0 +1,53 @@ +package io.kokuwa.micronaut.logging.http.mdc; + +import java.util.HashMap; +import java.util.List; +import java.util.Optional; + +import org.reactivestreams.Publisher; + +import io.kokuwa.micronaut.logging.http.AbstractMdcFilter; +import io.micronaut.context.annotation.Requires; +import io.micronaut.context.annotation.Value; +import io.micronaut.core.util.StringUtils; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.MutableHttpResponse; +import io.micronaut.http.annotation.Filter; +import io.micronaut.http.filter.ServerFilterChain; +import io.micronaut.http.filter.ServerFilterPhase; +import io.micronaut.runtime.context.scope.Refreshable; + +/** + * Filter to add http headers to MDC. + * + * @author Stephan Schnabel + */ +@Refreshable +@Requires(property = HeaderMdcFilter.PREFIX + ".enabled", notEquals = StringUtils.FALSE) +@Requires(property = HeaderMdcFilter.PREFIX + ".names") +@Filter("${" + HeaderMdcFilter.PREFIX + ".path:/**}") +public class HeaderMdcFilter extends AbstractMdcFilter { + + public static final String PREFIX = "logger.http.header"; + public static final int DEFAULT_ORDER = ServerFilterPhase.FIRST.before(); + + 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).toList(); + log.info("Configured with header names {}", headers); + } + + @Override + public Publisher> doFilter(HttpRequest request, ServerFilterChain chain) { + var mdc = new HashMap(); + for (var header : headers) { + request.getHeaders().getFirst(header).ifPresent(value -> mdc.put(header, String.valueOf(value))); + } + return doFilter(request, chain, mdc); + } +} diff --git a/src/main/java/io/kokuwa/micronaut/logging/http/mdc/PathMdcFilter.java b/src/main/java/io/kokuwa/micronaut/logging/http/mdc/PathMdcFilter.java new file mode 100644 index 0000000..848e34d --- /dev/null +++ b/src/main/java/io/kokuwa/micronaut/logging/http/mdc/PathMdcFilter.java @@ -0,0 +1,86 @@ +package io.kokuwa.micronaut.logging.http.mdc; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +import org.reactivestreams.Publisher; + +import io.kokuwa.micronaut.logging.http.AbstractMdcFilter; +import io.micronaut.context.annotation.Requires; +import io.micronaut.context.annotation.Value; +import io.micronaut.core.util.StringUtils; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.MutableHttpResponse; +import io.micronaut.http.annotation.Filter; +import io.micronaut.http.filter.ServerFilterChain; +import io.micronaut.http.filter.ServerFilterPhase; +import io.micronaut.runtime.context.scope.Refreshable; + +/** + * Filter to add request path parts to MDC. + * + * @author Stephan Schnabel + */ +@Refreshable +@Requires(property = PathMdcFilter.PREFIX + ".enabled", notEquals = StringUtils.FALSE) +@Requires(property = PathMdcFilter.PREFIX + ".patterns") +@Filter("${" + PathMdcFilter.PREFIX + ".path:/**}") +public class PathMdcFilter extends AbstractMdcFilter { + + public static final String PREFIX = "logger.http.path"; + public static final int DEFAULT_ORDER = ServerFilterPhase.FIRST.before(); + public static final Pattern PATTERN_GROUPS = Pattern.compile("\\(\\?<([a-zA-Z][a-zA-Z0-9]+)>"); + + private final Map> patternsWithGroups; + + public PathMdcFilter( + @Value("${" + PREFIX + ".patterns}") List patterns, + @Value("${" + PREFIX + ".prefix}") Optional prefix, + @Value("${" + PREFIX + ".order}") Optional order) { + super(order.orElse(DEFAULT_ORDER), prefix.orElse(null)); + this.patternsWithGroups = new HashMap<>(); + for (var patternString : patterns) { + try { + var pattern = Pattern.compile(patternString); + var groupMatcher = PATTERN_GROUPS.matcher(pattern.toString()); + var groups = new HashSet(); + while (groupMatcher.find()) { + groups.add(groupMatcher.group(1)); + } + + if (groups.isEmpty()) { + log.warn("Path {} is missing groups.", patternString); + } else { + log.info("Added path {} with groups {}.", patternString, groups); + patternsWithGroups.put(pattern, groups); + } + } catch (PatternSyntaxException e) { + log.warn("Path {} is invalid.", patternString); + } + } + } + + @Override + public Publisher> doFilter(HttpRequest request, ServerFilterChain chain) { + + var mdc = new HashMap(); + var path = request.getPath(); + + for (var patternWithGroup : patternsWithGroups.entrySet()) { + var matcher = patternWithGroup.getKey().matcher(path); + if (matcher.matches()) { + for (var group : patternWithGroup.getValue()) { + mdc.put(group, matcher.group(group)); + } + } + } + + return doFilter(request, chain, mdc); + } +} diff --git a/src/main/java/io/kokuwa/micronaut/logging/StackdriverJsonLayout.java b/src/main/java/io/kokuwa/micronaut/logging/layout/GcpJsonLayout.java similarity index 62% rename from src/main/java/io/kokuwa/micronaut/logging/StackdriverJsonLayout.java rename to src/main/java/io/kokuwa/micronaut/logging/layout/GcpJsonLayout.java index a609ca1..524601c 100644 --- a/src/main/java/io/kokuwa/micronaut/logging/StackdriverJsonLayout.java +++ b/src/main/java/io/kokuwa/micronaut/logging/layout/GcpJsonLayout.java @@ -1,48 +1,33 @@ -package io.kokuwa.micronaut.logging; +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.jackson.JacksonJsonFormatter; -import ch.qos.logback.contrib.json.classic.JsonLayout; import io.micronaut.core.util.StringUtils; -import lombok.Getter; -import lombok.Setter; /** - * Stackdriver layout. + * GCP logging layout. * * @author Stephan Schnabel * @see "https://cloud.google.com/logging/docs/agent/configuration#process-payload" * @see "https://cloud.google.com/error-reporting/reference/rest/v1beta1/ServiceContext" */ -@Getter -@Setter -public class StackdriverJsonLayout extends JsonLayout { +public class GcpJsonLayout extends JsonLayout { - private static final String TIMESTAMP_ATTR_NAME = "time"; + private static final String UNDEFINED = "_IS_UNDEFINED"; + private static final String TIME_ATTR_NAME = "time"; private static final String SEVERITY_ATTR_NAME = "severity"; private Map serviceContext; private String serviceName; private String serviceVersion; - private boolean includeExceptionInMessage; - - public StackdriverJsonLayout() { - appendLineSeparator = true; - includeContextName = false; - includeMessage = true; - includeExceptionInMessage = true; - setJsonFormatter(new JacksonJsonFormatter()); - } @Override protected Map toJsonMap(ILoggingEvent event) { var map = new LinkedHashMap(); - add(TIMESTAMP_ATTR_NAME, includeTimestamp, Instant.ofEpochMilli(event.getTimeStamp()).toString(), map); + add(TIME_ATTR_NAME, includeTimestamp, Instant.ofEpochMilli(event.getTimeStamp()).toString(), map); add(SEVERITY_ATTR_NAME, includeLevel, String.valueOf(event.getLevel()), map); add(THREAD_ATTR_NAME, includeThreadName, event.getThreadName(), map); add(CONTEXT_ATTR_NAME, includeContextName, event.getLoggerContextVO().getName(), map); @@ -50,18 +35,18 @@ public class StackdriverJsonLayout extends JsonLayout { addMap(MDC_ATTR_NAME, includeMDC, event.getMDCPropertyMap(), map); add(FORMATTED_MESSAGE_ATTR_NAME, includeFormattedMessage, event.getFormattedMessage(), map); add(MESSAGE_ATTR_NAME, includeMessage, event.getMessage(), map); - addThrowableInfo(JsonLayout.EXCEPTION_ATTR_NAME, includeException, event, map); + addThrowableInfo(EXCEPTION_ATTR_NAME, includeException, event, map); addServiceContext(map); return map; } private void addServiceContext(Map map) { if (serviceContext == null) { - serviceContext = new HashMap<>(2); - if (StringUtils.isNotEmpty(serviceName)) { + serviceContext = new LinkedHashMap<>(2); + if (StringUtils.isNotEmpty(serviceName) && !serviceName.endsWith(UNDEFINED)) { serviceContext.put("service", serviceName); } - if (StringUtils.isNotEmpty(serviceVersion)) { + if (StringUtils.isNotEmpty(serviceVersion) && !serviceVersion.endsWith(UNDEFINED)) { serviceContext.put("version", serviceVersion); } } @@ -69,4 +54,12 @@ public class StackdriverJsonLayout extends JsonLayout { map.put("serviceContext", serviceContext); } } + + public void setServiceName(String serviceName) { + this.serviceName = serviceName; + } + + public void setServiceVersion(String serviceVersion) { + this.serviceVersion = serviceVersion; + } } diff --git a/src/main/java/io/kokuwa/micronaut/logging/layout/JsonLayout.java b/src/main/java/io/kokuwa/micronaut/logging/layout/JsonLayout.java new file mode 100644 index 0000000..4665494 --- /dev/null +++ b/src/main/java/io/kokuwa/micronaut/logging/layout/JsonLayout.java @@ -0,0 +1,197 @@ +package io.kokuwa.micronaut.logging.layout; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.TimeZone; + +import ch.qos.logback.classic.pattern.ThrowableHandlingConverter; +import ch.qos.logback.classic.pattern.ThrowableProxyConverter; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.CoreConstants; +import ch.qos.logback.core.LayoutBase; +import ch.qos.logback.core.status.OnConsoleStatusListener; +import ch.qos.logback.core.status.StatusUtil; +import ch.qos.logback.core.util.StatusListenerConfigHelper; +import io.micronaut.http.MediaType; +import io.micronaut.json.JsonMapper; + +public class JsonLayout extends LayoutBase { + + public static final String TIMESTAMP_ATTR_NAME = "timestamp"; + public static final String LEVEL_ATTR_NAME = "level"; + public static final String THREAD_ATTR_NAME = "thread"; + public static final String MDC_ATTR_NAME = "mdc"; + public static final String LOGGER_ATTR_NAME = "logger"; + public static final String FORMATTED_MESSAGE_ATTR_NAME = "message"; + public static final String MESSAGE_ATTR_NAME = "raw-message"; + public static final String EXCEPTION_ATTR_NAME = "exception"; + public static final String CONTEXT_ATTR_NAME = "context"; + + protected boolean includeLevel = true; + protected boolean includeThreadName = true; + protected boolean includeMDC = true; + protected boolean includeLoggerName = true; + protected boolean includeFormattedMessage = true; + protected boolean includeMessage = true; + protected boolean includeException = true; + protected boolean includeContextName = false; + protected boolean includeTimestamp = true; + private String timestampFormat; + private String timestampFormatTimezoneId; + private ThrowableHandlingConverter throwableHandlingConverter = new ThrowableProxyConverter(); + private JsonMapper mapper; + + @Override + public String getContentType() { + return MediaType.APPLICATION_JSON; + } + + @Override + public void start() { + this.throwableHandlingConverter.start(); + super.start(); + } + + @Override + public void stop() { + super.stop(); + this.throwableHandlingConverter.stop(); + } + + @Override + public String doLayout(ILoggingEvent event) { + var map = toJsonMap(event); + + if (mapper == null) { + try { + mapper = JsonMapper.createDefault(); + } catch (IllegalStateException e) { + if (!StatusUtil.contextHasStatusListener(context)) { + addError("Failed to get object mapper from micronaut, please check your classpath"); + StatusListenerConfigHelper.addOnConsoleListenerInstance(context, new OnConsoleStatusListener()); + } + return map.toString() + CoreConstants.LINE_SEPARATOR; + } + } + + try { + return new String(mapper.writeValueAsBytes(map), StandardCharsets.UTF_8) + CoreConstants.LINE_SEPARATOR; + } catch (IOException e) { + addError("Failed to write json from event " + event + " and map " + map, e); + return null; + } + } + + protected Map toJsonMap(ILoggingEvent event) { + var map = new LinkedHashMap(); + addTimestamp(TIMESTAMP_ATTR_NAME, includeTimestamp, event.getTimeStamp(), map); + add(LEVEL_ATTR_NAME, includeLevel, String.valueOf(event.getLevel()), map); + add(THREAD_ATTR_NAME, includeThreadName, event.getThreadName(), map); + addMap(MDC_ATTR_NAME, includeMDC, event.getMDCPropertyMap(), map); + add(LOGGER_ATTR_NAME, includeLoggerName, event.getLoggerName(), map); + add(FORMATTED_MESSAGE_ATTR_NAME, includeFormattedMessage, event.getFormattedMessage(), map); + add(MESSAGE_ATTR_NAME, includeMessage, event.getMessage(), map); + add(CONTEXT_ATTR_NAME, includeContextName, event.getLoggerContextVO().getName(), map); + addThrowableInfo(EXCEPTION_ATTR_NAME, includeException, event, map); + return map; + } + + protected void addThrowableInfo(String fieldName, boolean field, ILoggingEvent value, Map map) { + if (field && value != null) { + var throwableProxy = value.getThrowableProxy(); + if (throwableProxy != null) { + var ex = throwableHandlingConverter.convert(value); + if (ex != null && !ex.isEmpty()) { + map.put(fieldName, ex); + } + } + } + } + + protected void addMap(String key, boolean field, Map mapValue, Map map) { + if (field && mapValue != null && !mapValue.isEmpty()) { + map.put(key, mapValue); + } + } + + protected void addTimestamp(String key, boolean field, long timeStamp, Map map) { + if (field) { + var formatted = formatTimestamp(timeStamp); + if (formatted != null) { + map.put(key, formatted); + } + } + } + + protected void add(String fieldName, boolean field, String value, Map map) { + if (field && value != null) { + map.put(fieldName, value); + } + } + + protected String formatTimestamp(long timestamp) { + if (timestampFormat == null || timestamp < 0) { + return String.valueOf(timestamp); + } + var date = new Date(timestamp); + var format = new SimpleDateFormat(timestampFormat); + if (timestampFormatTimezoneId != null) { + format.setTimeZone(TimeZone.getTimeZone(timestampFormatTimezoneId)); + } + return format.format(date); + } + + // setter + + public void setIncludeLevel(boolean includeLevel) { + this.includeLevel = includeLevel; + } + + public void setIncludeThreadName(boolean includeThreadName) { + this.includeThreadName = includeThreadName; + } + + public void setIncludeMDC(boolean includeMDC) { + this.includeMDC = includeMDC; + } + + public void setIncludeLoggerName(boolean includeLoggerName) { + this.includeLoggerName = includeLoggerName; + } + + public void setIncludeFormattedMessage(boolean includeFormattedMessage) { + this.includeFormattedMessage = includeFormattedMessage; + } + + public void setIncludeMessage(boolean includeMessage) { + this.includeMessage = includeMessage; + } + + public void setIncludeException(boolean includeException) { + this.includeException = includeException; + } + + public void setIncludeContextName(boolean includeContextName) { + this.includeContextName = includeContextName; + } + + public void setIncludeTimestamp(boolean includeTimestamp) { + this.includeTimestamp = includeTimestamp; + } + + public void setTimestampFormat(String timestampFormat) { + this.timestampFormat = timestampFormat; + } + + public void setTimestampFormatTimezoneId(String timestampFormatTimezoneId) { + this.timestampFormatTimezoneId = timestampFormatTimezoneId; + } + + public void setThrowableHandlingConverter(ThrowableHandlingConverter throwableHandlingConverter) { + this.throwableHandlingConverter = throwableHandlingConverter; + } +} diff --git a/src/main/java/io/kokuwa/micronaut/logging/mdc/MDCTurboFilter.java b/src/main/java/io/kokuwa/micronaut/logging/mdc/MDCTurboFilter.java index c6f01b1..db4d805 100644 --- a/src/main/java/io/kokuwa/micronaut/logging/mdc/MDCTurboFilter.java +++ b/src/main/java/io/kokuwa/micronaut/logging/mdc/MDCTurboFilter.java @@ -50,7 +50,13 @@ public class MDCTurboFilter extends TurboFilter { } @Override - public FilterReply decide(Marker marker, Logger logger, Level level, String format, Object[] params, Throwable t) { + public FilterReply decide( + Marker marker, + Logger logger, + Level eventLevel, + String format, + Object[] params, + Throwable t) { if (logger == null || !isStarted()) { return FilterReply.NEUTRAL; @@ -67,6 +73,6 @@ public class MDCTurboFilter extends TurboFilter { return FilterReply.NEUTRAL; } - return level.isGreaterOrEqual(this.level) ? FilterReply.ACCEPT : FilterReply.NEUTRAL; + return eventLevel.isGreaterOrEqual(this.level) ? FilterReply.ACCEPT : FilterReply.NEUTRAL; } } diff --git a/src/main/java/io/kokuwa/micronaut/logging/mdc/MDCTurboFilterConfigurer.java b/src/main/java/io/kokuwa/micronaut/logging/mdc/MDCTurboFilterConfigurer.java index 300b57e..de87997 100644 --- a/src/main/java/io/kokuwa/micronaut/logging/mdc/MDCTurboFilterConfigurer.java +++ b/src/main/java/io/kokuwa/micronaut/logging/mdc/MDCTurboFilterConfigurer.java @@ -1,45 +1,71 @@ package io.kokuwa.micronaut.logging.mdc; +import java.util.Collection; import java.util.Set; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import ch.qos.logback.classic.Level; import io.kokuwa.micronaut.logging.LogbackUtil; import io.micronaut.context.annotation.BootstrapContextCompatible; import io.micronaut.context.annotation.Context; import io.micronaut.context.annotation.Requires; import io.micronaut.context.env.Environment; -import io.micronaut.core.annotation.Internal; import io.micronaut.core.type.Argument; -import lombok.extern.slf4j.Slf4j; +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.ENABLED, notEquals = "false") -@BootstrapContextCompatible +@Requires(property = MDCTurboFilterConfigurer.PREFIX + ".enabled", notEquals = StringUtils.FALSE) @Context -@Internal -@Slf4j -public class MDCTurboFilterConfigurer { +public class MDCTurboFilterConfigurer implements LoggingSystem { public static final String PREFIX = "logger.mdc"; - public static final String ENABLED = PREFIX + ".enabled"; + + 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/java/io/kokuwa/micronaut/logging/request/HeaderLoggingHttpFilter.java b/src/main/java/io/kokuwa/micronaut/logging/request/HeaderLoggingHttpFilter.java deleted file mode 100644 index bed5fae..0000000 --- a/src/main/java/io/kokuwa/micronaut/logging/request/HeaderLoggingHttpFilter.java +++ /dev/null @@ -1,75 +0,0 @@ -package io.kokuwa.micronaut.logging.request; - -import java.util.Optional; - -import javax.annotation.PostConstruct; -import javax.annotation.PreDestroy; - -import org.reactivestreams.Publisher; -import org.slf4j.MDC; - -import ch.qos.logback.classic.turbo.TurboFilter; -import io.kokuwa.micronaut.logging.LogbackUtil; -import io.micronaut.context.annotation.Requires; -import io.micronaut.context.annotation.Value; -import io.micronaut.http.HttpRequest; -import io.micronaut.http.MutableHttpResponse; -import io.micronaut.http.annotation.Filter; -import io.micronaut.http.filter.HttpServerFilter; -import io.micronaut.http.filter.ServerFilterChain; -import io.micronaut.http.filter.ServerFilterPhase; -import io.micronaut.runtime.server.EmbeddedServer; - -/** - * Http request logging filter. - * - * @author Stephan Schnabel - */ -@Requires(beans = EmbeddedServer.class) -@Requires(property = HeaderLoggingHttpFilter.ENABLED, notEquals = "false") -@Filter("${" + HeaderLoggingHttpFilter.PREFIX + ".pattern:" + HeaderLoggingHttpFilter.DEFAULT_PATTERN + ":/**}") -public class HeaderLoggingHttpFilter implements HttpServerFilter { - - public static final String PREFIX = "logger.request.header"; - public static final String ENABLED = PREFIX + ".enabled"; - public static final String MDC_FILTER = PREFIX + ".filter"; - public static final String MDC_KEY = "level"; - - public static final String DEFAULT_HEADER = "x-log-level"; - public static final String DEFAULT_PATTERN = "/**"; - public static final int DEFAULT_ORDER = ServerFilterPhase.FIRST.before(); - - private final LogbackUtil logback; - private final String header; - private final int order; - - public HeaderLoggingHttpFilter( - LogbackUtil logback, - @Value("${" + PREFIX + ".header:" + DEFAULT_HEADER + "}") String header, - @Value("${" + PREFIX + ".order}") Optional order) { - this.logback = logback; - this.header = header; - this.order = order.orElse(DEFAULT_ORDER); - } - - @PostConstruct - void startTurbofilter() { - logback.getTurboFilter(HeaderLoggingTurboFilter.class, MDC_FILTER, HeaderLoggingTurboFilter::new).start(); - } - - @PreDestroy - void stopTurbofilter() { - logback.getTurboFilter(HeaderLoggingTurboFilter.class, MDC_FILTER).ifPresent(TurboFilter::stop); - } - - @Override - public int getOrder() { - return order; - } - - @Override - public Publisher> doFilter(HttpRequest request, ServerFilterChain chain) { - request.getHeaders().getFirst(header).ifPresent(level -> MDC.put(MDC_KEY, level)); - return chain.proceed(request); - } -} diff --git a/src/main/java/io/kokuwa/micronaut/logging/request/PrincipalHttpFilter.java b/src/main/java/io/kokuwa/micronaut/logging/request/PrincipalHttpFilter.java deleted file mode 100644 index 80848e2..0000000 --- a/src/main/java/io/kokuwa/micronaut/logging/request/PrincipalHttpFilter.java +++ /dev/null @@ -1,55 +0,0 @@ -package io.kokuwa.micronaut.logging.request; - -import java.util.Optional; - -import org.reactivestreams.Publisher; -import org.slf4j.MDC; - -import io.micronaut.context.annotation.Requires; -import io.micronaut.context.annotation.Value; -import io.micronaut.http.HttpRequest; -import io.micronaut.http.MutableHttpResponse; -import io.micronaut.http.annotation.Filter; -import io.micronaut.http.filter.HttpServerFilter; -import io.micronaut.http.filter.ServerFilterChain; -import io.micronaut.http.filter.ServerFilterPhase; -import io.micronaut.runtime.server.EmbeddedServer; - -/** - * Http request principal filter. - * - * @author Stephan Schnabel - */ -@Requires(beans = EmbeddedServer.class) -@Requires(property = PrincipalHttpFilter.ENABLED, notEquals = "false") -@Filter("${" + PrincipalHttpFilter.PREFIX + ".pattern:" + PrincipalHttpFilter.DEFAULT_PATTERN + ":/**}") -public class PrincipalHttpFilter implements HttpServerFilter { - - public static final String PREFIX = "logger.request.principal"; - public static final String ENABLED = PREFIX + ".enabled"; - - public static final String DEFAULT_KEY = "principal"; - public static final String DEFAULT_PATTERN = "/**"; - public static final int DEFAULT_ORDER = ServerFilterPhase.SECURITY.after(); - - private final String key; - private final int order; - - public PrincipalHttpFilter( - @Value("${" + PREFIX + ".key:" + DEFAULT_KEY + "}") String key, - @Value("${" + PREFIX + ".order}") Optional order) { - this.key = key; - this.order = order.orElse(DEFAULT_ORDER); - } - - @Override - public int getOrder() { - return order; - } - - @Override - public Publisher> doFilter(HttpRequest request, ServerFilterChain chain) { - request.getUserPrincipal().ifPresent(princial -> MDC.put(key, princial.getName())); - return chain.proceed(request); - } -} 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/META-INF/services/ch.qos.logback.classic.spi.Configurator b/src/main/resources/META-INF/services/ch.qos.logback.classic.spi.Configurator new file mode 100644 index 0000000..1f9f2da --- /dev/null +++ b/src/main/resources/META-INF/services/ch.qos.logback.classic.spi.Configurator @@ -0,0 +1 @@ +io.kokuwa.micronaut.logging.configurator.DefaultConfigurator diff --git a/src/main/resources/io/kokuwa/logback/logback-default.xml b/src/main/resources/io/kokuwa/logback/logback-default.xml new file mode 100644 index 0000000..78fe3f0 --- /dev/null +++ b/src/main/resources/io/kokuwa/logback/logback-default.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml deleted file mode 100644 index 42a34f7..0000000 --- a/src/main/resources/logback.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - true - - %cyan(%d{HH:mm:ss.SSS}) %gray(%-6.6thread) %highlight(%-5level) %magenta(%32logger{32}) %mdc %msg%n - - - - - - ${serviceName} - ${serviceVersion} - - - - - - - - - diff --git a/src/test/java/io/kokuwa/micronaut/logging/AbstractTest.java b/src/test/java/io/kokuwa/micronaut/logging/AbstractTest.java index f63e4c2..ebcc5e7 100644 --- a/src/test/java/io/kokuwa/micronaut/logging/AbstractTest.java +++ b/src/test/java/io/kokuwa/micronaut/logging/AbstractTest.java @@ -2,11 +2,11 @@ package io.kokuwa.micronaut.logging; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.MethodOrderer.Alphanumeric; +import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.TestMethodOrder; import org.slf4j.MDC; -import io.micronaut.test.annotation.MicronautTest; +import io.micronaut.test.extensions.junit5.annotation.MicronautTest; /** * Base for tests regarding logging. @@ -14,7 +14,7 @@ import io.micronaut.test.annotation.MicronautTest; * @author Stephan Schnabel */ @MicronautTest -@TestMethodOrder(Alphanumeric.class) +@TestMethodOrder(MethodOrderer.DisplayName.class) public abstract class AbstractTest { @BeforeEach diff --git a/src/test/java/io/kokuwa/micronaut/logging/http/AbstractFilterTest.java b/src/test/java/io/kokuwa/micronaut/logging/http/AbstractFilterTest.java new file mode 100644 index 0000000..4587dcb --- /dev/null +++ b/src/test/java/io/kokuwa/micronaut/logging/http/AbstractFilterTest.java @@ -0,0 +1,152 @@ +package io.kokuwa.micronaut.logging.http; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import java.util.Map; +import java.util.function.Consumer; + +import 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 io.kokuwa.micronaut.logging.AbstractTest; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.util.CollectionUtils; +import io.micronaut.http.HttpHeaderValues; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.HttpStatus; +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Get; +import io.micronaut.http.annotation.PathVariable; +import io.micronaut.http.client.DefaultHttpClientConfiguration; +import io.micronaut.http.client.HttpClient; +import io.micronaut.http.filter.HttpServerFilter; +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; + +/** + * Test for {@link HttpServerFilter}. + * + * @author Stephan Schnabel + */ +@MicronautTest(rebuildContext = true) +public abstract class AbstractFilterTest extends AbstractTest { + + @Inject + SignatureGeneratorConfiguration signature; + @Inject + EmbeddedServer embeddedServer; + + @DisplayName("0 - trigger rebuild of context") + @Test + void rebuild() {} + + // security + + public String token(String subject) { + return token(subject, claims -> {}); + } + + public String token(String subject, Consumer manipulator) { + var claims = new JWTClaimsSet.Builder().subject(subject); + manipulator.accept(claims); + try { + return HttpHeaderValues.AUTHORIZATION_PREFIX_BEARER + " " + signature.sign(claims.build()).serialize(); + } catch (JOSEException e) { + fail(e); + return null; + } + } + + // request + + public TestResponse get(String path, Map headers) { + + var request = HttpRequest.GET(path); + headers.forEach((name, value) -> request.header(name, value)); + var configuration = new DefaultHttpClientConfiguration(); + configuration.setLoggerName("io.kokuwa.TestClient"); + var response = HttpClient + .create(embeddedServer.getURL(), configuration) + .toBlocking().exchange(request, TestResponse.class); + assertEquals(HttpStatus.OK, response.getStatus(), "status"); + assertTrue(response.getBody().isPresent(), "body"); + assertTrue(CollectionUtils.isEmpty(MDC.getCopyOfContextMap()), "mdc leaked: " + MDC.getCopyOfContextMap()); + + return response.body(); + } + + @Secured({ SecurityRule.IS_ANONYMOUS, SecurityRule.IS_AUTHENTICATED }) + @Controller + public static class TestController { + + private static final Logger log = LoggerFactory.getLogger(TestController.class); + + @Get("/{+path}") + TestResponse run(@PathVariable String path) { + + var level = Level.OFF; + if (log.isTraceEnabled()) { + level = Level.TRACE; + } else if (log.isDebugEnabled()) { + level = Level.DEBUG; + } else if (log.isInfoEnabled()) { + level = Level.INFO; + } else if (log.isWarnEnabled()) { + level = Level.WARN; + } else if (log.isErrorEnabled()) { + level = Level.ERROR; + } + + var mdc = MDC.getCopyOfContextMap(); + log.info("Found MDC: {}", mdc); + + return new TestResponse(path, level.toString(), mdc); + } + } + + @Serdeable + public static class TestResponse { + + private final String path; + private final String level; + private final Map context; + + @JsonCreator + public TestResponse( + @JsonProperty("path") String path, + @JsonProperty("level") String level, + @Nullable @JsonProperty("context") Map context) { + this.path = path; + this.level = level; + this.context = context == null ? Map.of() : context; + } + + public String getPath() { + return path; + } + + public String getLevel() { + return level; + } + + public Map getContext() { + return context; + } + } +} diff --git a/src/test/java/io/kokuwa/micronaut/logging/http/level/LogLevelServerFilterTest.java b/src/test/java/io/kokuwa/micronaut/logging/http/level/LogLevelServerFilterTest.java new file mode 100644 index 0000000..c4db1ef --- /dev/null +++ b/src/test/java/io/kokuwa/micronaut/logging/http/level/LogLevelServerFilterTest.java @@ -0,0 +1,80 @@ +package io.kokuwa.micronaut.logging.http.level; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Map; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import ch.qos.logback.classic.Level; +import io.kokuwa.micronaut.logging.http.AbstractFilterTest; +import io.micronaut.context.annotation.Property; + +/** + * Test for {@link LogLevelServerFilter}. + * + * @author Stephan Schnabel + */ +@DisplayName("http: set log level via http request") +public class LogLevelServerFilterTest extends AbstractFilterTest { + + @DisplayName("noop: disabled") + @Test + @Property(name = "logger.http.level.enabled", value = "false") + void noopDisabled() { + assertLevel(Level.INFO, "TRACE"); + } + + @DisplayName("noop: header missing") + @Test + void noopHeaderMissing() { + assertLevel(Level.INFO, null); + } + + @DisplayName("noop: header invalid, use DEBUG as default from logback") + @Test + void noopHeaderInvalid() { + assertLevel(Level.DEBUG, "TRCE"); + } + + @DisplayName("level: trace (below default)") + @Test + void levelTrace() { + assertLevel(Level.TRACE, "TRACE"); + } + + @DisplayName("level: debug (below default)") + @Test + void levelDebug() { + assertLevel(Level.DEBUG, "DEBUG"); + } + + @DisplayName("level: info (is default)") + @Test + void levelInfo() { + assertLevel(Level.INFO, "INFO"); + } + + @DisplayName("level: warn (above default)") + @Test + void levelWarn() { + assertLevel(Level.INFO, "WARN"); + } + + @DisplayName("config: custom header name") + @Test + @Property(name = "logger.http.level.header", value = "FOO") + void configHeaderWarn() { + assertLevel(Level.TRACE, "FOO", "TRACE"); + } + + private void assertLevel(Level expectedLevel, String value) { + assertLevel(expectedLevel, LogLevelServerFilter.DEFAULT_HEADER, value); + } + + private void assertLevel(Level expectedLevel, String name, String value) { + var headers = value == null ? Map.of() : Map.of(name, value); + assertEquals(expectedLevel.toString(), get("/level", headers).getLevel()); + } +} diff --git a/src/test/java/io/kokuwa/micronaut/logging/http/mdc/AuthenticationMdcFilterTest.java b/src/test/java/io/kokuwa/micronaut/logging/http/mdc/AuthenticationMdcFilterTest.java new file mode 100644 index 0000000..d10b673 --- /dev/null +++ b/src/test/java/io/kokuwa/micronaut/logging/http/mdc/AuthenticationMdcFilterTest.java @@ -0,0 +1,73 @@ +package io.kokuwa.micronaut.logging.http.mdc; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import io.kokuwa.micronaut.logging.http.AbstractFilterTest; +import io.micronaut.context.annotation.Property; +import io.micronaut.http.HttpHeaders; + +/** + * Test for {@link AuthenticationMdcFilter}. + * + * @author Stephan Schnabel + */ +@DisplayName("http: mdc from authentication") +public class AuthenticationMdcFilterTest extends AbstractFilterTest { + + @DisplayName("noop: disabled") + @Test + @Property(name = "logger.http.authentication.enabled", value = "false") + void noopDisabled() { + assertEquals(Map.of(), getContext(true)); + } + + @DisplayName("noop: token missing") + @Test + void noopTokenMissing() { + assertEquals(Map.of(), getContext(false)); + } + + @DisplayName("mdc: default config") + @Test + void mdcWithDefault() { + assertEquals(Map.of("principal", "mySubject"), getContext(true)); + } + + @DisplayName("mdc: with name") + @Test + @Property(name = "logger.http.authentication.name", value = "sub") + void mdcWithName() { + assertEquals(Map.of("sub", "mySubject"), getContext(true)); + } + + @DisplayName("mdc: with attribute keys") + @Test + @Property(name = "logger.http.authentication.attributes", value = "azp,aud") + void mdcWithAttributes() { + assertEquals(Map.of("principal", "mySubject", "aud", "[a, b]", "azp", "myAzp"), getContext(true)); + } + + @DisplayName("mdc: with prefix") + @Test + @Property(name = "logger.http.authentication.name", value = "sub") + @Property(name = "logger.http.authentication.attributes", value = "azp") + @Property(name = "logger.http.authentication.prefix", value = "auth.") + void mdcWithPrefix() { + assertEquals(Map.of("auth.sub", "mySubject", "auth.azp", "myAzp"), getContext(true)); + } + + private Map getContext(boolean token) { + return get("/security", token + ? Map.of(HttpHeaders.AUTHORIZATION, token("mySubject", claims -> claims + .issuer("nope") + .claim("azp", "myAzp") + .audience(List.of("a", "b")))) + : Map.of()).getContext(); + } +} diff --git a/src/test/java/io/kokuwa/micronaut/logging/http/mdc/HeaderMdcFilterTest.java b/src/test/java/io/kokuwa/micronaut/logging/http/mdc/HeaderMdcFilterTest.java new file mode 100644 index 0000000..489870f --- /dev/null +++ b/src/test/java/io/kokuwa/micronaut/logging/http/mdc/HeaderMdcFilterTest.java @@ -0,0 +1,60 @@ +package io.kokuwa.micronaut.logging.http.mdc; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Map; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import io.kokuwa.micronaut.logging.http.AbstractFilterTest; +import io.micronaut.context.annotation.Property; + +/** + * Test for {@link HeaderMdcFilter}. + * + * @author Stephan Schnabel + */ +@DisplayName("http: mdc from headers") +public class HeaderMdcFilterTest extends AbstractFilterTest { + + @DisplayName("noop: empty configuration") + @Test + void noopEmptyConfiguration() { + assertContext(Map.of(), Map.of("foo", "bar")); + } + + @DisplayName("noop: disabled") + @Test + @Property(name = "logger.http.header.enabled", value = "false") + @Property(name = "logger.http.header.names", value = "foo") + void noopDisabled() { + assertContext(Map.of(), Map.of("foo", "bar")); + } + + @DisplayName("mdc: mismatch") + @Test + @Property(name = "logger.http.header.names", value = "foo") + void mdcMismatch() { + assertContext(Map.of(), Map.of("nope", "bar")); + } + + @DisplayName("mdc: match without prefix") + @Test + @Property(name = "logger.http.header.names", value = "foo") + void mdcMatchWithoutPrefix() { + assertContext(Map.of("foo", "bar"), Map.of("foo", "bar", "nope", "bar")); + } + + @DisplayName("mdc: match with prefix") + @Test + @Property(name = "logger.http.header.names", value = "foo") + @Property(name = "logger.http.header.prefix", value = "header.") + void mdcMatchWithPrefix() { + assertContext(Map.of("header.foo", "bar"), Map.of("foo", "bar", "nope", "bar")); + } + + private void assertContext(Map expectedMdcs, Map headers) { + assertEquals(expectedMdcs, get("/header", headers).getContext()); + } +} diff --git a/src/test/java/io/kokuwa/micronaut/logging/http/mdc/PathMdcFilterTest.java b/src/test/java/io/kokuwa/micronaut/logging/http/mdc/PathMdcFilterTest.java new file mode 100644 index 0000000..3b7ac78 --- /dev/null +++ b/src/test/java/io/kokuwa/micronaut/logging/http/mdc/PathMdcFilterTest.java @@ -0,0 +1,104 @@ +package io.kokuwa.micronaut.logging.http.mdc; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Map; +import java.util.UUID; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import io.kokuwa.micronaut.logging.http.AbstractFilterTest; +import io.micronaut.context.annotation.Property; + +/** + * Test for {@link PathMdcFilter}. + * + * @author Stephan Schnabel + */ +@DisplayName("http: mdc from path") +public class PathMdcFilterTest extends AbstractFilterTest { + + @DisplayName("noop: empty configuration") + @Test + void noopEmptyConfiguration() { + assertContext(Map.of(), "/foo/bar"); + } + + @DisplayName("noop: disabled") + @Test + @Property(name = "logger.http.path.enabled", value = "false") + @Property(name = "logger.http.path.patterns", value = "\\/foo\\/(?[0-9]+)") + void noopDisabled() { + assertContext(Map.of(), "/foo/123"); + } + + @DisplayName("noop: misconfigured") + @Test + @Property(name = "logger.http.path.patterns", value = "\\A{") + void noopMisconfigured() { + assertContext(Map.of(), "/foo/123"); + } + + @DisplayName("noop: no group") + @Test + @Property(name = "logger.http.path.patterns", value = "\\/foo/[0-9]+") + void noopGroups() { + assertContext(Map.of(), "/foo/123"); + } + + @DisplayName("mdc: mismatch") + @Test + @Property(name = "logger.http.path.patterns", value = "\\/foo\\/(?[0-9]+)") + void mdcMismatch() { + assertContext(Map.of(), "/nope"); + assertContext(Map.of(), "/foo/abc"); + } + + @DisplayName("mdc: match with single group") + @Test + @Property(name = "logger.http.path.patterns", value = "\\/foo\\/(?[0-9]+)") + void mdcMatchWithSingleGroup() { + assertContext(Map.of("foo", "123"), "/foo/123"); + } + + @DisplayName("mdc: match with single group and prefix") + @Test + @Property(name = "logger.http.path.names", value = "foo") + @Property(name = "logger.http.path.patterns", value = "\\/foo\\/(?[0-9]+)") + @Property(name = "logger.http.path.prefix", value = "path.") + void mdcMatchWithSingleGroupAndPrefix() { + assertContext(Map.of("path.foo", "123"), "/foo/123"); + } + + @DisplayName("mdc: match with single group and misconfigured") + @Test + @Property(name = "logger.http.path.names", value = "foo") + @Property(name = "logger.http.path.patterns", value = "\\/foo\\/(?[0-9]+),\\A{") + @Property(name = "logger.http.path.prefix", value = "path.") + void mdcMatchWithSingleGroupAndMisconfigured() { + assertContext(Map.of("path.foo", "123"), "/foo/123"); + } + + @DisplayName("mdc: match with multiple group") + @Test + @Property(name = "logger.http.path.patterns", value = "/foo/(?[0-9]+)/bar/(?[0-9]+)") + void mdcMatchWithmultipleGroup() { + assertContext(Map.of("foo", "123", "bar", "456"), "/foo/123/bar/456"); + } + + @DisplayName("mdc: test for documentation example") + @Test + @Property(name = "logger.http.path.patterns", value = """ + \\/gateway\\/(?[a-f0-9\\-]{36}),\ + \\/gateway\\/(?[a-f0-9\\-]{36})\\/configuration\\/(?[a-z]+)""") + void mdcMatchExample() { + var uuid = UUID.randomUUID().toString(); + assertContext(Map.of("gatewayId", uuid), "/gateway/" + uuid); + assertContext(Map.of("gatewayId", uuid, "config", "abc"), "/gateway/" + uuid + "/configuration/abc"); + } + + private void assertContext(Map expectedMdcs, String path) { + assertEquals(expectedMdcs, get(path, Map.of()).getContext()); + } +} diff --git a/src/test/java/io/kokuwa/micronaut/logging/mdc/MDCTurboFilterTest.java b/src/test/java/io/kokuwa/micronaut/logging/mdc/MDCTurboFilterTest.java index e964d2b..186c2a3 100644 --- a/src/test/java/io/kokuwa/micronaut/logging/mdc/MDCTurboFilterTest.java +++ b/src/test/java/io/kokuwa/micronaut/logging/mdc/MDCTurboFilterTest.java @@ -10,14 +10,14 @@ import org.slf4j.LoggerFactory; import org.slf4j.MDC; import io.kokuwa.micronaut.logging.AbstractTest; -import io.micronaut.test.annotation.MicronautTest; +import io.micronaut.test.extensions.junit5.annotation.MicronautTest; /** * Test for {@link MDCTurboFilterConfigurer}. * * @author Stephan Schnabel */ -@DisplayName("mdc") +@DisplayName("mdc based log levels") @MicronautTest(environments = "test-mdc") public class MDCTurboFilterTest extends AbstractTest { diff --git a/src/test/java/io/kokuwa/micronaut/logging/request/CompositeTest.java b/src/test/java/io/kokuwa/micronaut/logging/request/CompositeTest.java deleted file mode 100644 index 26e766d..0000000 --- a/src/test/java/io/kokuwa/micronaut/logging/request/CompositeTest.java +++ /dev/null @@ -1,41 +0,0 @@ -package io.kokuwa.micronaut.logging.request; - -import javax.inject.Inject; - -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -import ch.qos.logback.classic.Level; -import io.kokuwa.micronaut.logging.AbstractTest; -import io.micronaut.test.annotation.MicronautTest; - -/** - * Test for MDC and request filter combined. - * - * @author Stephan Schnabel - */ -@DisplayName("request-composite") -@MicronautTest(environments = "test-composite") -public class CompositeTest extends AbstractTest { - - @Inject - TestClient client; - - @DisplayName("default level") - @Test - void defaultLogging() { - client.assertLevel(Level.INFO, client.token("somebody"), null); - } - - @DisplayName("level set by mdc") - @Test - void headerFromMdc() { - client.assertLevel(Level.DEBUG, client.token("horst"), null); - } - - @DisplayName("level set by header (overriding mdc)") - @Test - void headerFromHeader() { - client.assertLevel(Level.TRACE, client.token("horst"), "TRACE"); - } -} diff --git a/src/test/java/io/kokuwa/micronaut/logging/request/RequestHeaderTest.java b/src/test/java/io/kokuwa/micronaut/logging/request/RequestHeaderTest.java deleted file mode 100644 index b939b95..0000000 --- a/src/test/java/io/kokuwa/micronaut/logging/request/RequestHeaderTest.java +++ /dev/null @@ -1,57 +0,0 @@ -package io.kokuwa.micronaut.logging.request; - -import javax.inject.Inject; - -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -import ch.qos.logback.classic.Level; -import io.kokuwa.micronaut.logging.AbstractTest; - -/** - * Test for {@link HeaderLoggingHttpFilter}. - * - * @author Stephan Schnabel - */ -@DisplayName("request-header") -public class RequestHeaderTest extends AbstractTest { - - @Inject - TestClient client; - - @DisplayName("header missing") - @Test - void headerMissing() { - client.assertLevel(Level.INFO, null, null); - } - - @DisplayName("header invalid, use DEBUG as default from logback") - @Test - void headerInvalid() { - client.assertLevel(Level.DEBUG, null, "TRCE"); - } - - @DisplayName("level trace (below default)") - @Test - void headerLevelTrace() { - client.assertLevel(Level.TRACE, null, "TRACE"); - } - - @DisplayName("level debug (below default)") - @Test - void headerLevelDebug() { - client.assertLevel(Level.DEBUG, null, "DEBUG"); - } - - @DisplayName("level info (is default)") - @Test - void headerLevelInfo() { - client.assertLevel(Level.INFO, null, "INFO"); - } - - @DisplayName("level warn (above default)") - @Test - void headerLevelWarn() { - client.assertLevel(Level.INFO, null, "WARN"); - } -} diff --git a/src/test/java/io/kokuwa/micronaut/logging/request/RequestPrincipalTest.java b/src/test/java/io/kokuwa/micronaut/logging/request/RequestPrincipalTest.java deleted file mode 100644 index f8f1fcc..0000000 --- a/src/test/java/io/kokuwa/micronaut/logging/request/RequestPrincipalTest.java +++ /dev/null @@ -1,44 +0,0 @@ -package io.kokuwa.micronaut.logging.request; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import javax.inject.Inject; - -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -import io.kokuwa.micronaut.logging.AbstractTest; - -/** - * Test for {@link PrincipalHttpFilter}. - * - * @author Stephan Schnabel - */ -@DisplayName("request-principal") -public class RequestPrincipalTest extends AbstractTest { - - @Inject - TestClient client; - - @DisplayName("token missing") - @Test - void tokenMissing() { - assertPrincipal(null, null); - } - - @DisplayName("token invalid") - @Test - void tokenInvalid() { - assertPrincipal(null, "meh"); - } - - @DisplayName("token valid") - @Test - void tokenValid() { - assertPrincipal("meh", client.token("meh")); - } - - private void assertPrincipal(String expectedPrincipal, String actualTokenValue) { - assertEquals(expectedPrincipal, client.get(actualTokenValue, null).getPrincipal()); - } -} diff --git a/src/test/java/io/kokuwa/micronaut/logging/request/TestClient.java b/src/test/java/io/kokuwa/micronaut/logging/request/TestClient.java deleted file mode 100644 index b44495a..0000000 --- a/src/test/java/io/kokuwa/micronaut/logging/request/TestClient.java +++ /dev/null @@ -1,64 +0,0 @@ -package io.kokuwa.micronaut.logging.request; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assertions.fail; - -import javax.inject.Inject; -import javax.inject.Singleton; - -import com.nimbusds.jose.JOSEException; -import com.nimbusds.jwt.JWTClaimsSet; - -import ch.qos.logback.classic.Level; -import io.kokuwa.micronaut.logging.request.TestController.TestResponse; -import io.micronaut.http.HttpRequest; -import io.micronaut.http.HttpStatus; -import io.micronaut.http.client.HttpClient; -import io.micronaut.http.client.annotation.Client; -import io.micronaut.security.token.jwt.signature.SignatureGeneratorConfiguration; - -/** - * Contoller for testing {@link HeaderLoggingHttpFilter} and {@link PrincipalHttpFilter}. - * - * @author Stephan Schnabel - */ -@Singleton -public class TestClient { - - @Inject - @Client("/") - HttpClient client; - @Inject - SignatureGeneratorConfiguration signature; - - String token(String subject) { - try { - return signature.sign(new JWTClaimsSet.Builder().subject(subject).build()).serialize(); - } catch (JOSEException e) { - fail("failed to create token"); - return null; - } - } - - TestResponse get(String token, String header) { - - var request = HttpRequest.GET("/"); - if (token != null) { - request.bearerAuth(token); - } - if (header != null) { - request.getHeaders().add(HeaderLoggingHttpFilter.DEFAULT_HEADER, header); - } - - var response = client.toBlocking().exchange(request, TestResponse.class); - assertEquals(HttpStatus.OK, response.getStatus(), "status"); - assertTrue(response.getBody().isPresent(), "body"); - - return response.body(); - } - - void assertLevel(Level expectedLevel, String actualTokenValue, String actualHeaderValue) { - assertEquals(expectedLevel.toString(), get(actualTokenValue, actualHeaderValue).getLevel()); - } -} diff --git a/src/test/java/io/kokuwa/micronaut/logging/request/TestController.java b/src/test/java/io/kokuwa/micronaut/logging/request/TestController.java deleted file mode 100644 index 3d3d6c9..0000000 --- a/src/test/java/io/kokuwa/micronaut/logging/request/TestController.java +++ /dev/null @@ -1,52 +0,0 @@ -package io.kokuwa.micronaut.logging.request; - -import org.slf4j.MDC; - -import ch.qos.logback.classic.Level; -import io.micronaut.http.annotation.Controller; -import io.micronaut.http.annotation.Get; -import io.micronaut.security.annotation.Secured; -import io.micronaut.security.rules.SecurityRule; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.extern.slf4j.Slf4j; - -/** - * Controller for testing {@link HeaderLoggingHttpFilter} and {@link PrincipalHttpFilter}. - * - * @author Stephan Schnabel - */ -@Secured({ SecurityRule.IS_ANONYMOUS, SecurityRule.IS_AUTHENTICATED }) -@Controller -@Slf4j -public class TestController { - - @Get("/") - TestResponse run() { - - var principal = MDC.get(PrincipalHttpFilter.DEFAULT_KEY); - var level = Level.OFF; - if (log.isTraceEnabled()) { - level = Level.TRACE; - } else if (log.isDebugEnabled()) { - level = Level.DEBUG; - } else if (log.isInfoEnabled()) { - level = Level.INFO; - } else if (log.isWarnEnabled()) { - level = Level.WARN; - } else if (log.isErrorEnabled()) { - level = Level.ERROR; - } - - return new TestResponse(level.toString(), principal); - } - - @Data - @NoArgsConstructor - @AllArgsConstructor - public static class TestResponse { - private String level; - private String principal; - } -} diff --git a/src/test/resources/META-INF/build-info.properties b/src/test/resources/META-INF/build-info.properties deleted file mode 100644 index 5401a6c..0000000 --- a/src/test/resources/META-INF/build-info.properties +++ /dev/null @@ -1,2 +0,0 @@ -serviceName: ${project.artifactId} -serviceVersion: ${project.version} diff --git a/src/test/resources/application-test-composite.yaml b/src/test/resources/application-test-composite.yaml deleted file mode 100644 index 7ba34db..0000000 --- a/src/test/resources/application-test-composite.yaml +++ /dev/null @@ -1,8 +0,0 @@ -logger: - mdc: - principal: - level: DEBUG - loggers: - - io.kokuwa - values: - - horst diff --git a/src/test/resources/application-test-mdc.yaml b/src/test/resources/application-test-mdc.yaml index ef6bfc5..69cbc89 100644 --- a/src/test/resources/application-test-mdc.yaml +++ b/src/test/resources/application-test-mdc.yaml @@ -1,26 +1,28 @@ logger: + levels: + io.micronaut.logging.PropertiesLoggingLevelsConfigurer: "OFF" mdc: key1: key: key level: DEBUG loggers: - - io.kokuwa.a - - io.kokuwa.b + - io.kokuwa.a + - io.kokuwa.b values: - - value-1 - - value-2 + - value-1 + - value-2 key2: key: key level: TRACE loggers: - - io.kokuwa.b - - io.kokuwa.c + - io.kokuwa.b + - io.kokuwa.c values: - - value-2 + - value-2 key: level: TRACE loggers: - - io.kokuwa + - io.kokuwa values: - - value-3 + - value-3 user: {} diff --git a/src/test/resources/application-test.yaml b/src/test/resources/application-test.yaml index 42aa67e..867b23e 100644 --- a/src/test/resources/application-test.yaml +++ b/src/test/resources/application-test.yaml @@ -7,10 +7,3 @@ micronaut: generator: secret: pleaseChangeThisSecretForANewOne jws-algorithm: HS256 - http: - client: - logger-name: io.kokuwa.Test - -logger: - levels: - io.kokuwa.Test: TRACE