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.
+
+[](https://central.sonatype.com/artifact/io.kokuwa.micronaut/micronaut-logging)
+[](https://git.kokuwa.io/kokuwaio/micronaut-logging/src/branch/main/LICENSE)
+[](https://git.kokuwa.io/kokuwaio/micronaut-logging/issues)
+[](https://git.kokuwa.io/kokuwaio/micronaut-logging/pulls)
+[](https://ci.kokuwa.io/repos/kokuwaio/micronaut-logging/)
+
+For more documention see: [git.kokuwa.io/kokuwaio/micronaut-logging](https://git.kokuwa.io/kokuwaio/micronaut-logging)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
deleted file mode 100644
index dec1718..0000000
--- a/.github/workflows/ci.yml
+++ /dev/null
@@ -1,34 +0,0 @@
-name: PullRequest
-
-"on": [pull_request]
-
-jobs:
-
- lint-yaml:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v2
- - uses: ibiqlik/action-yamllint@v3
- with:
- format: colored
- strict: true
-
- lint-markdown:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v2
- - uses: docker://avtodev/markdown-lint:v1
- with:
- args: /github/workspace
-
- build:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v2
- - uses: actions/setup-java@v2
- with:
- distribution: temurin
- java-version: 11
- cache: maven
- - run: mvn -B dependency:go-offline -q
- - run: mvn -B verify
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
deleted file mode 100644
index 3f13993..0000000
--- a/.github/workflows/release.yml
+++ /dev/null
@@ -1,53 +0,0 @@
-name: Release
-
-"on":
- workflow_dispatch: {}
-
-jobs:
-
- lint-yaml:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v2
- - uses: ibiqlik/action-yamllint@v3
- with:
- format: colored
- strict: true
-
- lint-markdown:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v2
- - uses: docker://avtodev/markdown-lint:v1
- with:
- args: /github/workspace
-
- build:
- runs-on: ubuntu-latest
- needs:
- - lint-markdown
- - lint-yaml
- steps:
- - uses: actions/checkout@v2
- with:
- token: ${{ secrets.GIT_ACTION_TOKEN }}
- - name: git-configure
- run: |
- git config user.email "actions@github.com"
- git config user.name "GitHub Actions"
- - uses: actions/setup-java@v2
- with:
- distribution: temurin
- java-version: 11
- server-id: sonatype-nexus
- server-username: SERVER_USERNAME
- server-password: SERVER_PASSWORD
- gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }}
- gpg-passphrase: GPG_PASSPHRASE
- - run: mvn -B dependency:go-offline -q
- - run: mvn -B release:prepare
- - run: mvn -B release:perform -DreleaseProfiles=oss-release
- env:
- SERVER_USERNAME: ${{ secrets.SONATYPE_NEXUS_USERNAME }}
- SERVER_PASSWORD: ${{ secrets.SONATYPE_NEXUS_PASSWORD }}
- GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
diff --git a/.github/workflows/snapshot.yml b/.github/workflows/snapshot.yml
deleted file mode 100644
index ab9bf69..0000000
--- a/.github/workflows/snapshot.yml
+++ /dev/null
@@ -1,48 +0,0 @@
-name: Snapshot
-
-"on":
- workflow_dispatch: {}
- push:
- branches:
- - main
- - '*.x'
-
-jobs:
-
- lint-yaml:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v2
- - uses: ibiqlik/action-yamllint@v3
- with:
- format: colored
- strict: true
-
- lint-markdown:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v2
- - uses: docker://avtodev/markdown-lint:v1
- with:
- args: /github/workspace
-
- build:
- runs-on: ubuntu-latest
- needs:
- - lint-markdown
- - lint-yaml
- steps:
- - uses: actions/checkout@v2
- - uses: actions/setup-java@v2
- with:
- distribution: temurin
- java-version: 11
- cache: maven
- server-id: sonatype-nexus
- server-username: SERVER_USERNAME
- server-password: SERVER_PASSWORD
- - run: mvn -B dependency:go-offline -q
- - run: mvn -B deploy -Poss
- env:
- SERVER_USERNAME: ${{ secrets.SONATYPE_NEXUS_USERNAME }}
- SERVER_PASSWORD: ${{ secrets.SONATYPE_NEXUS_PASSWORD }}
diff --git a/.justfile b/.justfile
new file mode 100644
index 0000000..81f407b
--- /dev/null
+++ b/.justfile
@@ -0,0 +1,12 @@
+# https://just.systems/man/en/
+
+[private]
+@default:
+ just --list --unsorted
+
+# Run linter.
+@lint:
+ docker run --rm --read-only --volume=$(pwd):$(pwd):ro --workdir=$(pwd) kokuwaio/yamllint
+ docker run --rm --read-only --volume=$(pwd):$(pwd):rw --workdir=$(pwd) kokuwaio/markdownlint --fix
+ docker run --rm --read-only --volume=$(pwd):$(pwd):ro --workdir=$(pwd) kokuwaio/renovate-config-validator
+ docker run --rm --read-only --volume=$(pwd):$(pwd):ro --workdir=$(pwd) woodpeckerci/woodpecker-cli lint
diff --git a/.markdownlint.yaml b/.markdownlint.yaml
index dd23793..5f08047 100644
--- a/.markdownlint.yaml
+++ b/.markdownlint.yaml
@@ -1,6 +1,9 @@
# Default state for all rules
default: true
-# MD013/line-length - Line length
-MD013:
- line_length: 10000
+# MD009 - Trailing spaces
+MD009:
+ strict: true
+
+# MD013 - Line length
+MD013: false
diff --git a/.woodpecker/deploy.yaml b/.woodpecker/deploy.yaml
new file mode 100644
index 0000000..11f903e
--- /dev/null
+++ b/.woodpecker/deploy.yaml
@@ -0,0 +1,16 @@
+when:
+ instance: ci.kokuwa.io
+ repo: kokuwaio/micronaut-logging
+ event: [manual, push]
+ branch: main
+ path: [.woodpecker/deploy.yaml, pom.xml, src/main/**]
+
+steps:
+
+ maven:
+ image: maven:3.9.10-eclipse-temurin-17
+ commands: mvn deploy --settings=.woodpecker/maven/settings.xml
+ environment:
+ MAVEN_GPG_KEY: {from_secret: woodpecker_gpg_key}
+ SONATYPE_ORG_USERNAME: {from_secret: sonatype_org_username}
+ SONATYPE_ORG_PASSWORD: {from_secret: sonatype_org_password}
diff --git a/.woodpecker/lint.yaml b/.woodpecker/lint.yaml
new file mode 100644
index 0000000..74bb114
--- /dev/null
+++ b/.woodpecker/lint.yaml
@@ -0,0 +1,21 @@
+when:
+ event: [manual, pull_request, push]
+ branch: main
+ path: [.woodpecker/lint.yaml, renovate.json, "**/*.y*ml", "**/*.md"]
+
+steps:
+
+ renovate:
+ image: kokuwaio/renovate-config-validator
+ depends_on: []
+ when: [path: [.woodpecker/lint.yaml, renovate.json]]
+
+ yaml:
+ image: kokuwaio/yamllint
+ depends_on: []
+ when: [path: [.woodpecker/lint.yaml, .yamllint.yaml, "**/*.y*ml"]]
+
+ markdown:
+ image: kokuwaio/markdownlint
+ depends_on: []
+ when: [path: [.woodpecker/lint.yaml, .markdownlint.yaml, "**/*.md"]]
diff --git a/.woodpecker/maven/settings.xml b/.woodpecker/maven/settings.xml
new file mode 100644
index 0000000..57ad1cb
--- /dev/null
+++ b/.woodpecker/maven/settings.xml
@@ -0,0 +1,23 @@
+
+
+ false
+ /woodpecker/.m2
+
+
+ git.kokuwa.io
+ ${env.FORGEJO_USERNAME}
+ ${env.FORGEJO_PASSWORD}
+
+
+ sonatype.org
+ ${env.SONATYPE_ORG_USERNAME}
+ ${env.SONATYPE_ORG_PASSWORD}
+
+
+
+
+ http://mirror.woodpecker.svc.cluster.local/maven2
+ central
+
+
+
diff --git a/.woodpecker/release.yaml b/.woodpecker/release.yaml
new file mode 100644
index 0000000..21a190e
--- /dev/null
+++ b/.woodpecker/release.yaml
@@ -0,0 +1,29 @@
+when:
+ instance: ci.kokuwa.io
+ repo: kokuwaio/micronaut-logging
+ event: deployment
+ branch: main
+
+steps:
+
+ maven:
+ image: maven:3.9.10-eclipse-temurin-17
+ commands:
+ # setup git with ssk key signing
+ - git config user.email "$GIT_USER_EMAIL"
+ - git config user.name "$GIT_USER_NAME"
+ - git config commit.gpgsign true
+ - git config gpg.format ssh
+ - git config user.signingkey /run/secrets/sign.pub
+ - install -m 400 /dev/null /run/secrets/sign && echo "$GIT_SIGN_KEY" > /run/secrets/sign
+ - install -m 444 /dev/null /run/secrets/sign.pub && echo "$GIT_SIGN_PUB" > /run/secrets/sign.pub
+ # release
+ - mvn release:prepare release:perform --settings=.woodpecker/maven/settings.xml
+ environment:
+ MAVEN_GPG_KEY: {from_secret: woodpecker_gpg_key}
+ GIT_SIGN_KEY: {from_secret: woodpecker_sign_key}
+ GIT_SIGN_PUB: {from_secret: woodpecker_sign_pub}
+ FORGEJO_USERNAME: {from_secret: woodpecker_username}
+ FORGEJO_PASSWORD: {from_secret: woodpecker_password}
+ SONATYPE_ORG_USERNAME: {from_secret: sonatype_org_username}
+ SONATYPE_ORG_PASSWORD: {from_secret: sonatype_org_password}
diff --git a/.woodpecker/verify.yaml b/.woodpecker/verify.yaml
new file mode 100644
index 0000000..094b317
--- /dev/null
+++ b/.woodpecker/verify.yaml
@@ -0,0 +1,9 @@
+when:
+ event: [manual, pull_request]
+ path: [.woodpecker/verify.yaml, pom.xml, src/**]
+
+steps:
+
+ test:
+ image: maven:3.9.10-eclipse-temurin-17
+ commands: mvn verify --settings=.woodpecker/maven/settings.xml -P-deploy
diff --git a/.yamllint b/.yamllint.yaml
similarity index 64%
rename from .yamllint
rename to .yamllint.yaml
index 9d1b12e..21966f2 100644
--- a/.yamllint
+++ b/.yamllint.yaml
@@ -8,3 +8,8 @@ rules:
# line length is not important
line-length: disable
+
+ # force double quotes everywhere
+ quoted-strings:
+ quote-type: double
+ required: only-when-needed
diff --git a/LICENSE b/LICENSE
index 261eeb9..dacd3ae 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,201 +1,288 @@
- Apache License
- Version 2.0, January 2004
- http://www.apache.org/licenses/
- TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+ EUROPEAN UNION PUBLIC LICENCE v. 1.2
+ EUPL © the European Union 2007, 2016
- 1. Definitions.
+This European Union Public Licence (the ‘EUPL’) applies to the Work (as defined
+below) which is provided under the terms of this Licence. Any use of the Work,
+other than as authorised under this Licence is prohibited (to the extent such
+use is covered by a right of the copyright holder of the Work).
- "License" shall mean the terms and conditions for use, reproduction,
- and distribution as defined by Sections 1 through 9 of this document.
+The Work is provided under the terms of this Licence when the Licensor (as
+defined below) has placed the following notice immediately following the
+copyright notice for the Work:
- "Licensor" shall mean the copyright owner or entity authorized by
- the copyright owner that is granting the License.
+ Licensed under the EUPL
- "Legal Entity" shall mean the union of the acting entity and all
- other entities that control, are controlled by, or are under common
- control with that entity. For the purposes of this definition,
- "control" means (i) the power, direct or indirect, to cause the
- direction or management of such entity, whether by contract or
- otherwise, or (ii) ownership of fifty percent (50%) or more of the
- outstanding shares, or (iii) beneficial ownership of such entity.
+or has expressed by any other means his willingness to license under the EUPL.
- "You" (or "Your") shall mean an individual or Legal Entity
- exercising permissions granted by this License.
+1. Definitions
- "Source" form shall mean the preferred form for making modifications,
- including but not limited to software source code, documentation
- source, and configuration files.
+In this Licence, the following terms have the following meaning:
- "Object" form shall mean any form resulting from mechanical
- transformation or translation of a Source form, including but
- not limited to compiled object code, generated documentation,
- and conversions to other media types.
+- ‘The Licence’: this Licence.
- "Work" shall mean the work of authorship, whether in Source or
- Object form, made available under the License, as indicated by a
- copyright notice that is included in or attached to the work
- (an example is provided in the Appendix below).
+- ‘The Original Work’: the work or software distributed or communicated by the
+ Licensor under this Licence, available as Source Code and also as Executable
+ Code as the case may be.
- "Derivative Works" shall mean any work, whether in Source or Object
- form, that is based on (or derived from) the Work and for which the
- editorial revisions, annotations, elaborations, or other modifications
- represent, as a whole, an original work of authorship. For the purposes
- of this License, Derivative Works shall not include works that remain
- separable from, or merely link (or bind by name) to the interfaces of,
- the Work and Derivative Works thereof.
+- ‘Derivative Works’: the works or software that could be created by the
+ Licensee, based upon the Original Work or modifications thereof. This Licence
+ does not define the extent of modification or dependence on the Original Work
+ required in order to classify a work as a Derivative Work; this extent is
+ determined by copyright law applicable in the country mentioned in Article 15.
- "Contribution" shall mean any work of authorship, including
- the original version of the Work and any modifications or additions
- to that Work or Derivative Works thereof, that is intentionally
- submitted to Licensor for inclusion in the Work by the copyright owner
- or by an individual or Legal Entity authorized to submit on behalf of
- the copyright owner. For the purposes of this definition, "submitted"
- means any form of electronic, verbal, or written communication sent
- to the Licensor or its representatives, including but not limited to
- communication on electronic mailing lists, source code control systems,
- and issue tracking systems that are managed by, or on behalf of, the
- Licensor for the purpose of discussing and improving the Work, but
- excluding communication that is conspicuously marked or otherwise
- designated in writing by the copyright owner as "Not a Contribution."
+- ‘The Work’: the Original Work or its Derivative Works.
- "Contributor" shall mean Licensor and any individual or Legal Entity
- on behalf of whom a Contribution has been received by Licensor and
- subsequently incorporated within the Work.
+- ‘The Source Code’: the human-readable form of the Work which is the most
+ convenient for people to study and modify.
- 2. Grant of Copyright License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- copyright license to reproduce, prepare Derivative Works of,
- publicly display, publicly perform, sublicense, and distribute the
- Work and such Derivative Works in Source or Object form.
+- ‘The Executable Code’: any code which has generally been compiled and which is
+ meant to be interpreted by a computer as a program.
- 3. Grant of Patent License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- (except as stated in this section) patent license to make, have made,
- use, offer to sell, sell, import, and otherwise transfer the Work,
- where such license applies only to those patent claims licensable
- by such Contributor that are necessarily infringed by their
- Contribution(s) alone or by combination of their Contribution(s)
- with the Work to which such Contribution(s) was submitted. If You
- institute patent litigation against any entity (including a
- cross-claim or counterclaim in a lawsuit) alleging that the Work
- or a Contribution incorporated within the Work constitutes direct
- or contributory patent infringement, then any patent licenses
- granted to You under this License for that Work shall terminate
- as of the date such litigation is filed.
+- ‘The Licensor’: the natural or legal person that distributes or communicates
+ the Work under the Licence.
- 4. Redistribution. You may reproduce and distribute copies of the
- Work or Derivative Works thereof in any medium, with or without
- modifications, and in Source or Object form, provided that You
- meet the following conditions:
+- ‘Contributor(s)’: any natural or legal person who modifies the Work under the
+ Licence, or otherwise contributes to the creation of a Derivative Work.
- (a) You must give any other recipients of the Work or
- Derivative Works a copy of this License; and
+- ‘The Licensee’ or ‘You’: any natural or legal person who makes any usage of
+ the Work under the terms of the Licence.
- (b) You must cause any modified files to carry prominent notices
- stating that You changed the files; and
+- ‘Distribution’ or ‘Communication’: any act of selling, giving, lending,
+ renting, distributing, communicating, transmitting, or otherwise making
+ available, online or offline, copies of the Work or providing access to its
+ essential functionalities at the disposal of any other natural or legal
+ person.
- (c) You must retain, in the Source form of any Derivative Works
- that You distribute, all copyright, patent, trademark, and
- attribution notices from the Source form of the Work,
- excluding those notices that do not pertain to any part of
- the Derivative Works; and
+2. Scope of the rights granted by the Licence
- (d) If the Work includes a "NOTICE" text file as part of its
- distribution, then any Derivative Works that You distribute must
- include a readable copy of the attribution notices contained
- within such NOTICE file, excluding those notices that do not
- pertain to any part of the Derivative Works, in at least one
- of the following places: within a NOTICE text file distributed
- as part of the Derivative Works; within the Source form or
- documentation, if provided along with the Derivative Works; or,
- within a display generated by the Derivative Works, if and
- wherever such third-party notices normally appear. The contents
- of the NOTICE file are for informational purposes only and
- do not modify the License. You may add Your own attribution
- notices within Derivative Works that You distribute, alongside
- or as an addendum to the NOTICE text from the Work, provided
- that such additional attribution notices cannot be construed
- as modifying the License.
+The Licensor hereby grants You a worldwide, royalty-free, non-exclusive,
+sublicensable licence to do the following, for the duration of copyright vested
+in the Original Work:
- You may add Your own copyright statement to Your modifications and
- may provide additional or different license terms and conditions
- for use, reproduction, or distribution of Your modifications, or
- for any such Derivative Works as a whole, provided Your use,
- reproduction, and distribution of the Work otherwise complies with
- the conditions stated in this License.
+- use the Work in any circumstance and for all usage,
+- reproduce the Work,
+- modify the Work, and make Derivative Works based upon the Work,
+- communicate to the public, including the right to make available or display
+ the Work or copies thereof to the public and perform publicly, as the case may
+ be, the Work,
+- distribute the Work or copies thereof,
+- lend and rent the Work or copies thereof,
+- sublicense rights in the Work or copies thereof.
- 5. Submission of Contributions. Unless You explicitly state otherwise,
- any Contribution intentionally submitted for inclusion in the Work
- by You to the Licensor shall be under the terms and conditions of
- this License, without any additional terms or conditions.
- Notwithstanding the above, nothing herein shall supersede or modify
- the terms of any separate license agreement you may have executed
- with Licensor regarding such Contributions.
+Those rights can be exercised on any media, supports and formats, whether now
+known or later invented, as far as the applicable law permits so.
- 6. Trademarks. This License does not grant permission to use the trade
- names, trademarks, service marks, or product names of the Licensor,
- except as required for reasonable and customary use in describing the
- origin of the Work and reproducing the content of the NOTICE file.
+In the countries where moral rights apply, the Licensor waives his right to
+exercise his moral right to the extent allowed by law in order to make effective
+the licence of the economic rights here above listed.
- 7. Disclaimer of Warranty. Unless required by applicable law or
- agreed to in writing, Licensor provides the Work (and each
- Contributor provides its Contributions) on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
- implied, including, without limitation, any warranties or conditions
- of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
- PARTICULAR PURPOSE. You are solely responsible for determining the
- appropriateness of using or redistributing the Work and assume any
- risks associated with Your exercise of permissions under this License.
+The Licensor grants to the Licensee royalty-free, non-exclusive usage rights to
+any patents held by the Licensor, to the extent necessary to make use of the
+rights granted on the Work under this Licence.
- 8. Limitation of Liability. In no event and under no legal theory,
- whether in tort (including negligence), contract, or otherwise,
- unless required by applicable law (such as deliberate and grossly
- negligent acts) or agreed to in writing, shall any Contributor be
- liable to You for damages, including any direct, indirect, special,
- incidental, or consequential damages of any character arising as a
- result of this License or out of the use or inability to use the
- Work (including but not limited to damages for loss of goodwill,
- work stoppage, computer failure or malfunction, or any and all
- other commercial damages or losses), even if such Contributor
- has been advised of the possibility of such damages.
+3. Communication of the Source Code
- 9. Accepting Warranty or Additional Liability. While redistributing
- the Work or Derivative Works thereof, You may choose to offer,
- and charge a fee for, acceptance of support, warranty, indemnity,
- or other liability obligations and/or rights consistent with this
- License. However, in accepting such obligations, You may act only
- on Your own behalf and on Your sole responsibility, not on behalf
- of any other Contributor, and only if You agree to indemnify,
- defend, and hold each Contributor harmless for any liability
- incurred by, or claims asserted against, such Contributor by reason
- of your accepting any such warranty or additional liability.
+The Licensor may provide the Work either in its Source Code form, or as
+Executable Code. If the Work is provided as Executable Code, the Licensor
+provides in addition a machine-readable copy of the Source Code of the Work
+along with each copy of the Work that the Licensor distributes or indicates, in
+a notice following the copyright notice attached to the Work, a repository where
+the Source Code is easily and freely accessible for as long as the Licensor
+continues to distribute or communicate the Work.
- END OF TERMS AND CONDITIONS
+4. Limitations on copyright
- APPENDIX: How to apply the Apache License to your work.
+Nothing in this Licence is intended to deprive the Licensee of the benefits from
+any exception or limitation to the exclusive rights of the rights owners in the
+Work, of the exhaustion of those rights or of other applicable limitations
+thereto.
- To apply the Apache License to your work, attach the following
- boilerplate notice, with the fields enclosed by brackets "[]"
- replaced with your own identifying information. (Don't include
- the brackets!) The text should be enclosed in the appropriate
- comment syntax for the file format. We also recommend that a
- file or class name and description of purpose be included on the
- same "printed page" as the copyright notice for easier
- identification within third-party archives.
+5. Obligations of the Licensee
- Copyright [yyyy] [name of copyright owner]
+The grant of the rights mentioned above is subject to some restrictions and
+obligations imposed on the Licensee. Those obligations are the following:
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
+Attribution right: The Licensee shall keep intact all copyright, patent or
+trademarks notices and all notices that refer to the Licence and to the
+disclaimer of warranties. The Licensee must include a copy of such notices and a
+copy of the Licence with every copy of the Work he/she distributes or
+communicates. The Licensee must cause any Derivative Work to carry prominent
+notices stating that the Work has been modified and the date of modification.
- http://www.apache.org/licenses/LICENSE-2.0
+Copyleft clause: If the Licensee distributes or communicates copies of the
+Original Works or Derivative Works, this Distribution or Communication will be
+done under the terms of this Licence or of a later version of this Licence
+unless the Original Work is expressly distributed only under this version of the
+Licence — for example by communicating ‘EUPL v. 1.2 only’. The Licensee
+(becoming Licensor) cannot offer or impose any additional terms or conditions on
+the Work or Derivative Work that alter or restrict the terms of the Licence.
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
+Compatibility clause: If the Licensee Distributes or Communicates Derivative
+Works or copies thereof based upon both the Work and another work licensed under
+a Compatible Licence, this Distribution or Communication can be done under the
+terms of this Compatible Licence. For the sake of this clause, ‘Compatible
+Licence’ refers to the licences listed in the appendix attached to this Licence.
+Should the Licensee's obligations under the Compatible Licence conflict with
+his/her obligations under this Licence, the obligations of the Compatible
+Licence shall prevail.
+
+Provision of Source Code: When distributing or communicating copies of the Work,
+the Licensee will provide a machine-readable copy of the Source Code or indicate
+a repository where this Source will be easily and freely available for as long
+as the Licensee continues to distribute or communicate the Work.
+
+Legal Protection: This Licence does not grant permission to use the trade names,
+trademarks, service marks, or names of the Licensor, except as required for
+reasonable and customary use in describing the origin of the Work and
+reproducing the content of the copyright notice.
+
+6. Chain of Authorship
+
+The original Licensor warrants that the copyright in the Original Work granted
+hereunder is owned by him/her or licensed to him/her and that he/she has the
+power and authority to grant the Licence.
+
+Each Contributor warrants that the copyright in the modifications he/she brings
+to the Work are owned by him/her or licensed to him/her and that he/she has the
+power and authority to grant the Licence.
+
+Each time You accept the Licence, the original Licensor and subsequent
+Contributors grant You a licence to their contributions to the Work, under the
+terms of this Licence.
+
+7. Disclaimer of Warranty
+
+The Work is a work in progress, which is continuously improved by numerous
+Contributors. It is not a finished work and may therefore contain defects or
+‘bugs’ inherent to this type of development.
+
+For the above reason, the Work is provided under the Licence on an ‘as is’ basis
+and without warranties of any kind concerning the Work, including without
+limitation merchantability, fitness for a particular purpose, absence of defects
+or errors, accuracy, non-infringement of intellectual property rights other than
+copyright as stated in Article 6 of this Licence.
+
+This disclaimer of warranty is an essential part of the Licence and a condition
+for the grant of any rights to the Work.
+
+8. Disclaimer of Liability
+
+Except in the cases of wilful misconduct or damages directly caused to natural
+persons, the Licensor will in no event be liable for any direct or indirect,
+material or moral, damages of any kind, arising out of the Licence or of the use
+of the Work, including without limitation, damages for loss of goodwill, work
+stoppage, computer failure or malfunction, loss of data or any commercial
+damage, even if the Licensor has been advised of the possibility of such damage.
+However, the Licensor will be liable under statutory product liability laws as
+far such laws apply to the Work.
+
+9. Additional agreements
+
+While distributing the Work, You may choose to conclude an additional agreement,
+defining obligations or services consistent with this Licence. However, if
+accepting obligations, You may act only on your own behalf and on your sole
+responsibility, not on behalf of the original Licensor or any other Contributor,
+and only if You agree to indemnify, defend, and hold each Contributor harmless
+for any liability incurred by, or claims asserted against such Contributor by
+the fact You have accepted any warranty or additional liability.
+
+10. Acceptance of the Licence
+
+The provisions of this Licence can be accepted by clicking on an icon ‘I agree’
+placed under the bottom of a window displaying the text of this Licence or by
+affirming consent in any other similar way, in accordance with the rules of
+applicable law. Clicking on that icon indicates your clear and irrevocable
+acceptance of this Licence and all of its terms and conditions.
+
+Similarly, you irrevocably accept this Licence and all of its terms and
+conditions by exercising any rights granted to You by Article 2 of this Licence,
+such as the use of the Work, the creation by You of a Derivative Work or the
+Distribution or Communication by You of the Work or copies thereof.
+
+11. Information to the public
+
+In case of any Distribution or Communication of the Work by means of electronic
+communication by You (for example, by offering to download the Work from a
+remote location) the distribution channel or media (for example, a website) must
+at least provide to the public the information requested by the applicable law
+regarding the Licensor, the Licence and the way it may be accessible, concluded,
+stored and reproduced by the Licensee.
+
+12. Termination of the Licence
+
+The Licence and the rights granted hereunder will terminate automatically upon
+any breach by the Licensee of the terms of the Licence.
+
+Such a termination will not terminate the licences of any person who has
+received the Work from the Licensee under the Licence, provided such persons
+remain in full compliance with the Licence.
+
+13. Miscellaneous
+
+Without prejudice of Article 9 above, the Licence represents the complete
+agreement between the Parties as to the Work.
+
+If any provision of the Licence is invalid or unenforceable under applicable
+law, this will not affect the validity or enforceability of the Licence as a
+whole. Such provision will be construed or reformed so as necessary to make it
+valid and enforceable.
+
+The European Commission may publish other linguistic versions or new versions of
+this Licence or updated versions of the Appendix, so far this is required and
+reasonable, without reducing the scope of the rights granted by the Licence. New
+versions of the Licence will be published with a unique version number.
+
+All linguistic versions of this Licence, approved by the European Commission,
+have identical value. Parties can take advantage of the linguistic version of
+their choice.
+
+14. Jurisdiction
+
+Without prejudice to specific agreement between parties,
+
+- any litigation resulting from the interpretation of this License, arising
+ between the European Union institutions, bodies, offices or agencies, as a
+ Licensor, and any Licensee, will be subject to the jurisdiction of the Court
+ of Justice of the European Union, as laid down in article 272 of the Treaty on
+ the Functioning of the European Union,
+
+- any litigation arising between other parties and resulting from the
+ interpretation of this License, will be subject to the exclusive jurisdiction
+ of the competent court where the Licensor resides or conducts its primary
+ business.
+
+15. Applicable Law
+
+Without prejudice to specific agreement between parties,
+
+- this Licence shall be governed by the law of the European Union Member State
+ where the Licensor has his seat, resides or has his registered office,
+
+- this licence shall be governed by Belgian law if the Licensor has no seat,
+ residence or registered office inside a European Union Member State.
+
+Appendix
+
+‘Compatible Licences’ according to Article 5 EUPL are:
+
+- GNU General Public License (GPL) v. 2, v. 3
+- GNU Affero General Public License (AGPL) v. 3
+- Open Software License (OSL) v. 2.1, v. 3.0
+- Eclipse Public License (EPL) v. 1.0
+- CeCILL v. 2.0, v. 2.1
+- Mozilla Public Licence (MPL) v. 2
+- GNU Lesser General Public Licence (LGPL) v. 2.1, v. 3
+- Creative Commons Attribution-ShareAlike v. 3.0 Unported (CC BY-SA 3.0) for
+ works other than software
+- European Union Public Licence (EUPL) v. 1.1, v. 1.2
+- Québec Free and Open-Source Licence — Reciprocity (LiLiQ-R) or Strong
+ Reciprocity (LiLiQ-R+).
+
+The European Commission may update this Appendix to later versions of the above
+licences without producing a new version of the EUPL, as long as they provide
+the rights granted in Article 2 of this Licence and protect the covered Source
+Code from exclusive appropriation.
+
+All other changes or additions to this Appendix require the production of a new
+EUPL version.
diff --git a/README.md b/README.md
index 9cf902e..4a3a2ec 100644
--- a/README.md
+++ b/README.md
@@ -1,176 +1,41 @@
# Micronaut Logging support
-This branch is for Micronaut 2.x, for 3.x see wip branch [3.x](../../tree/3.x).
+Enhanced logging for Micronaut using MDC or request header.
+
+[](https://central.sonatype.com/artifact/io.kokuwa.micronaut/micronaut-logging)
+[](https://git.kokuwa.io/kokuwaio/micronaut-logging/src/branch/main/LICENSE)
+[](https://git.kokuwa.io/kokuwaio/micronaut-logging/issues)
+[](https://git.kokuwa.io/kokuwaio/micronaut-logging/pulls)
+[](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
-### 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
-
-
-
-
-
-
-
-
-```
-
-### Available Appender
-
-* console with jansi for developers
-* gcp logging format (with support for error reporting)
-* json
-
-### AutoSelect appender logback.xml
-
-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
-
-*IMPORTENT*: only works without custom `logback.xml`
-
-### Set log level based on MDC values
-
-Configuration:
-
-* *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*: passlist 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:
-
-```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
-```
-
-Example for omiting level and key:
-
-```yaml
-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:
-
-```yaml
-logger:
- levels:
- io.kokuwa: INFO
- mdc:
- user: {}
-```
-
-### Set log level based on HTTP request header
-
-Configuration for server filter (prefixed with *logger.request.filter*):
-
-* *enabled*: enable HTTP server filter (`true` is default)
-* *order*: order for [Ordered](https://github.com/micronaut-projects/micronaut-core/blob/v2.5.13/core/src/main/java/io/micronaut/core/order/Ordered.java) (highest is default)
-* *path*: filter path (`/**` is default)
-* *header*: name of HTTP header (`x-log-level` is default)
-
-Configuration for client filter for propagation (prefixed with *logger.request.propagation*):
-
-* *enabled*: enable HTTP client filter (`true` is default)
-* *order*: order for [Ordered](https://github.com/micronaut-projects/micronaut-core/blob/v2.5.13/core/src/main/java/io/micronaut/core/order/Ordered.java) (tracing is default)
-* *path*: filter path (`/**` is default)
-* *header*: name of HTTP header (server header is default)
-
-Example with default configuration:
-
-```yaml
-logger:
- request:
- filter:
- enabled: true
- order: -2147483648
- path: /**
- header: x-log-level
- propagation:
- enabled: true
- order: 19000
- path: /**
- header: ${logger.request.header.header-name}
-```
-
-### Add principal for request to MDC
-
-Configuration:
-
-* *enabled*: enable HTTP principal filter (`true` is default)
-* *order*: order for [Ordered](https://github.com/micronaut-projects/micronaut-core/blob/v2.5.13/core/src/main/java/io/micronaut/core/order/Ordered.java) ([ServerFilterPhase.SECURITY.after()](https://github.com/micronaut-projects/micronaut-core/blob/v2.5.13/http/src/main/java/io/micronaut/http/filter/ServerFilterPhase.java#L54) is default)
-* *path*: filter path (`/**` is default)
-* *key*: name of MDC header (`principal` is default)
-
-Example with default configuration:
-
-```yaml
-logger:
- request:
- principal:
- enabled: true
- order: 39250
- path: /**
- key: principal
-```
-
-## Build & Release
-
-### Dependency updates
-
-Display dependency updates:
-
-```sh
-mvn versions:display-property-updates -U
-```
-
-Update dependencies:
-
-```sh
-mvn versions:update-properties
-```
-
-### Release locally
-
-Run:
-
-```sh
-mvn release:prepare release:perform release:clean -B -DreleaseProfiles=oss-release
-```
+* 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
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/lombok.config b/lombok.config
deleted file mode 100644
index 4fc0545..0000000
--- a/lombok.config
+++ /dev/null
@@ -1,18 +0,0 @@
-config.stopBubbling = true
-
-lombok.addNullAnnotations = CUSTOM:io.micronaut.core.annotation.NonNull:io.micronaut.core.annotation.Nullable
-lombok.addJavaxGeneratedAnnotation = true
-lombok.addLombokGeneratedAnnotation = true
-
-# adjust model
-lombok.equalsAndHashCode.callSuper = call
-lombok.toString.callSuper = call
-lombok.accessors.chain = true
-
-# use slf4j
-lombok.log.apacheCommons.flagUsage = error
-lombok.log.javaUtilLogging.flagUsage = error
-lombok.log.jbosslog.flagUsage = error
-lombok.log.log4j.flagUsage = error
-lombok.log.log4j2.flagUsage = error
-lombok.log.xslf4j.flagUsage = error
diff --git a/pom.xml b/pom.xml
index 54a0b8f..22f50e6 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1,87 +1,73 @@
-
+
4.0.0
-
- io.kokuwa
- maven-parent
- 0.5.4
-
-
-
io.kokuwa.micronaut
micronaut-logging
- 0.1.3-SNAPSHOT
+ 5.0.1-SNAPSHOT
- Logging support for Micronaut
+ Logging Support for Micronaut
Enhanced logging using MDC or request header.
- https://github.com/kokuwaio/micronaut-logging
+ https://git.kokuwa.io/kokuwaio/micronaut-logging
2020
+
+ Kokuwa.io
+ http://kokuwa.io
+
- Apache License 2.0
- https://www.apache.org/licenses/LICENSE-2.0
+ EUPL-1.2
+ https://eupl.eu/1.2/en
+ repo
+ stephan.schnabel
Stephan Schnabel
- https://github.com/stephanschnabel
+ https://schnabel.org
+ stephan@schnabel.org
+ Europe/Berlin
- https://github.com/kokuwaio/micronaut-logging
- scm:git:https://github.com/kokuwaio/micronaut-logging.git
- scm:git:https://github.com/kokuwaio/micronaut-logging.git
+ 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.5.13
-
+ 2025-06-27T18:11:48Z
+ UTF-8
+ 17
-
-
- io.micronaut
- micronaut-bom
- ${version.io.micronaut}
+ io.micronaut.platform
+ micronaut-platform
+ 4.9.0
pom
import
-
-
-
- ch.qos.logback.contrib
- logback-json-classic
- ${version.ch.qos.logback.contrib}
-
-
- ch.qos.logback.contrib
- logback-json-core
- ${version.ch.qos.logback.contrib}
-
-
- ch.qos.logback.contrib
- logback-jackson
- ${version.ch.qos.logback.contrib}
-
-
@@ -89,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
@@ -112,46 +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
+ 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
index 1ebef70..3b1a51a 100644
--- a/src/main/java/io/kokuwa/micronaut/logging/configurator/DefaultConfigurator.java
+++ b/src/main/java/io/kokuwa/micronaut/logging/configurator/DefaultConfigurator.java
@@ -2,6 +2,7 @@ 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;
@@ -12,13 +13,19 @@ import ch.qos.logback.core.spi.ContextAwareBase;
*/
public class DefaultConfigurator extends ContextAwareBase implements Configurator {
+ @SuppressWarnings("deprecation")
@Override
- public void configure(LoggerContext loggerContext) {
+ public ExecutionStatus configure(LoggerContext loggerContext) {
+
+ if (new DefaultJoranConfigurator().findURLOfDefaultConfigurationFile(false) != null) {
+ // there is a default logback file, use this one instead of our default
+ return ExecutionStatus.INVOKE_NEXT_IF_ANY;
+ }
var base = DefaultConfigurator.class.getResource("/io/kokuwa/logback/logback-default.xml");
if (base == null) {
addError("Failed to find logback.xml from io.kokuwa:micronaut-logging");
- return;
+ return ExecutionStatus.NEUTRAL;
}
try {
@@ -28,6 +35,9 @@ public class DefaultConfigurator extends ContextAwareBase implements Configurato
configurator.doConfigure(base);
} catch (JoranException e) {
addError("Failed to load logback.xml from io.kokuwa:micronaut-logging", e);
+ return ExecutionStatus.NEUTRAL;
}
+
+ return ExecutionStatus.DO_NOT_INVOKE_NEXT_IF_ANY;
}
}
diff --git a/src/main/java/io/kokuwa/micronaut/logging/configurator/MicronautJoranConfigurator.java b/src/main/java/io/kokuwa/micronaut/logging/configurator/MicronautJoranConfigurator.java
index f89db16..a1010d7 100644
--- a/src/main/java/io/kokuwa/micronaut/logging/configurator/MicronautJoranConfigurator.java
+++ b/src/main/java/io/kokuwa/micronaut/logging/configurator/MicronautJoranConfigurator.java
@@ -12,8 +12,8 @@ import ch.qos.logback.core.joran.spi.RuleStore;
public class MicronautJoranConfigurator extends JoranConfigurator {
@Override
- public void addInstanceRules(RuleStore rs) {
- super.addInstanceRules(rs);
- rs.addRule(new ElementSelector("configuration/root/autoAppender"), new RootAutoSelectAppenderAction());
+ public void addElementSelectorAndActionAssociations(RuleStore rs) {
+ super.addElementSelectorAndActionAssociations(rs);
+ rs.addRule(new ElementSelector("configuration/root/autoAppender"), () -> new RootAutoSelectAppenderAction());
}
}
diff --git a/src/main/java/io/kokuwa/micronaut/logging/configurator/RootAutoSelectAppenderAction.java b/src/main/java/io/kokuwa/micronaut/logging/configurator/RootAutoSelectAppenderAction.java
index b490a35..1d0db03 100644
--- a/src/main/java/io/kokuwa/micronaut/logging/configurator/RootAutoSelectAppenderAction.java
+++ b/src/main/java/io/kokuwa/micronaut/logging/configurator/RootAutoSelectAppenderAction.java
@@ -1,16 +1,18 @@
package io.kokuwa.micronaut.logging.configurator;
-import java.util.Map;
-
-import org.xml.sax.Attributes;
+import java.util.Optional;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.LoggerContext;
+import ch.qos.logback.classic.PatternLayout;
import ch.qos.logback.classic.spi.ILoggingEvent;
-import ch.qos.logback.core.Appender;
+import ch.qos.logback.core.ConsoleAppender;
+import ch.qos.logback.core.Layout;
+import ch.qos.logback.core.encoder.LayoutWrappingEncoder;
import ch.qos.logback.core.joran.action.Action;
-import ch.qos.logback.core.joran.action.ActionConst;
-import ch.qos.logback.core.joran.spi.InterpretationContext;
+import ch.qos.logback.core.joran.spi.SaxEventInterpretationContext;
+import io.kokuwa.micronaut.logging.layout.GcpJsonLayout;
+import io.kokuwa.micronaut.logging.layout.JsonLayout;
import io.micronaut.core.util.StringUtils;
/**
@@ -27,9 +29,17 @@ public class RootAutoSelectAppenderAction extends Action {
private static final String APPENDER_JSON = "JSON";
private static final String APPENDER_GCP = "GCP";
private static final String LOGBACK_APPENDER = "LOGBACK_APPENDER";
+ private static final String LOGBACK_PATTERN = "LOGBACK_PATTERN";
+ private static final String LOGBACK_PATTERN_DEFAULT = """
+ %cyan(%d{HH:mm:ss.SSS}) \
+ %gray(%-6.6thread) \
+ %highlight(%-5level) \
+ %magenta(%32logger{32}) \
+ %mdc \
+ %msg%n""";
@Override
- public void begin(InterpretationContext ic, String name, Attributes attributes) {
+ public void begin(SaxEventInterpretationContext ic, String name, org.xml.sax.Attributes attributes) {
var rootLogger = LoggerContext.class.cast(context).getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME);
var rootLoggerAppenders = rootLogger.iteratorForAppenders();
@@ -38,36 +48,83 @@ public class RootAutoSelectAppenderAction extends Action {
return;
}
- var envAppender = System.getenv(LOGBACK_APPENDER);
- if (envAppender != null && setAppender(ic, rootLogger, envAppender)) {
+ var envAppender = env(LOGBACK_APPENDER, null);
+ if (envAppender != null) {
+ setAppender(rootLogger, envAppender);
return;
}
- if (IS_KUBERNETES && setAppender(ic, rootLogger, APPENDER_JSON)) {
+ if (IS_KUBERNETES) {
+ setAppender(rootLogger, APPENDER_JSON);
return;
}
- if (IS_GCP && setAppender(ic, rootLogger, APPENDER_GCP)) {
+ if (IS_GCP) {
+ setAppender(rootLogger, APPENDER_GCP);
return;
}
- setAppender(ic, rootLogger, APPENDER_CONSOLE);
+ setAppender(rootLogger, APPENDER_CONSOLE);
}
@Override
- public void end(InterpretationContext ic, String name) {}
-
- private boolean setAppender(InterpretationContext ic, Logger rootLogger, String appenderName) {
-
- @SuppressWarnings("unchecked")
- var appenderBag = (Map>) ic.getObjectMap().get(ActionConst.APPENDER_BAG);
- var appender = appenderBag.get(appenderName);
- if (appender == null) {
- addError("Could not find an appender named [" + appenderName
- + "]. Did you define it below instead of above in the configuration file?");
- return false;
- }
+ public void end(SaxEventInterpretationContext ic, String name) {}
+ private void setAppender(Logger rootLogger, String appenderName) {
addInfo("Use appender: " + appenderName);
+
+ var layout = switch (appenderName) {
+ case APPENDER_JSON -> json();
+ case APPENDER_GCP -> gcp();
+ case APPENDER_CONSOLE -> console();
+ default -> {
+ addError("Appender " + appenderName + " not found. Using console ...");
+ yield console();
+ }
+ };
+ layout.start();
+
+ var encoder = new LayoutWrappingEncoder();
+ encoder.setContext(context);
+ encoder.setLayout(layout);
+ encoder.start();
+
+ var appender = new ConsoleAppender();
+ appender.setContext(context);
+ appender.setName(appenderName);
+ appender.setEncoder(encoder);
+ appender.start();
+
rootLogger.addAppender(appender);
- return true;
+ }
+
+ private Layout console() {
+ var layout = new PatternLayout();
+ layout.setContext(context);
+ layout.setPattern(env(LOGBACK_PATTERN, LOGBACK_PATTERN_DEFAULT));
+ return layout;
+ }
+
+ private Layout json() {
+ var layout = new JsonLayout();
+ layout.setContext(context);
+ return layout;
+ }
+
+ private Layout gcp() {
+ var layout = new GcpJsonLayout();
+ layout.setContext(context);
+ layout.setServiceName(env("SERVICE_NAME", null));
+ layout.setServiceVersion(env("SERVICE_VERSION", null));
+ return layout;
+ }
+
+ private String env(String name, String defaultValue) {
+ var envValue = Optional.ofNullable(System.getenv(name)).map(String::trim).filter(StringUtils::isNotEmpty);
+ var finalValue = envValue.orElse(defaultValue);
+ if (envValue.isPresent()) {
+ addInfo("Use provided config: " + name + "=" + finalValue);
+ } else {
+ addInfo("Use default config: " + name + "=" + finalValue);
+ }
+ return finalValue;
}
}
diff --git a/src/main/java/io/kokuwa/micronaut/logging/http/AbstractMdcFilter.java b/src/main/java/io/kokuwa/micronaut/logging/http/AbstractMdcFilter.java
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/request/HeaderLoggingClientHttpFilter.java b/src/main/java/io/kokuwa/micronaut/logging/http/level/LogLevelClientFilter.java
similarity index 62%
rename from src/main/java/io/kokuwa/micronaut/logging/request/HeaderLoggingClientHttpFilter.java
rename to src/main/java/io/kokuwa/micronaut/logging/http/level/LogLevelClientFilter.java
index 463a070..511829d 100644
--- a/src/main/java/io/kokuwa/micronaut/logging/request/HeaderLoggingClientHttpFilter.java
+++ b/src/main/java/io/kokuwa/micronaut/logging/http/level/LogLevelClientFilter.java
@@ -1,4 +1,4 @@
-package io.kokuwa.micronaut.logging.request;
+package io.kokuwa.micronaut.logging.http.level;
import java.util.Optional;
@@ -13,30 +13,29 @@ import io.micronaut.http.annotation.Filter;
import io.micronaut.http.context.ServerRequestContext;
import io.micronaut.http.filter.ClientFilterChain;
import io.micronaut.http.filter.HttpClientFilter;
-import io.micronaut.http.filter.ServerFilterPhase;
/**
- * Http request logging filter.
+ * Propagates log-level from server request to client.
*
* @author Stephan Schnabel
*/
-@Requires(beans = HeaderLoggingServerHttpFilter.class)
-@Requires(property = HeaderLoggingClientHttpFilter.PREFIX + ".enabled", notEquals = StringUtils.FALSE)
-@Filter("${" + HeaderLoggingClientHttpFilter.PREFIX + ".path:/**}")
-public class HeaderLoggingClientHttpFilter implements HttpClientFilter {
+@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.request.propagation";
- public static final int DEFAULT_ORDER = ServerFilterPhase.TRACING.order();
+ 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 HeaderLoggingClientHttpFilter(
- @Value("${" + HeaderLoggingServerHttpFilter.PREFIX + ".header}") Optional serverHeader,
+ public LogLevelClientFilter(
+ @Value("${" + LogLevelServerFilter.PREFIX + ".header}") Optional serverHeader,
@Value("${" + PREFIX + ".header}") Optional propagationHeader,
@Value("${" + PREFIX + ".order}") Optional order) {
- this.serverHeader = serverHeader.orElse(HeaderLoggingServerHttpFilter.DEFAULT_HEADER);
+ this.serverHeader = serverHeader.orElse(LogLevelServerFilter.DEFAULT_HEADER);
this.propagationHeader = propagationHeader.orElse(this.serverHeader);
this.order = order.orElse(DEFAULT_ORDER);
}
diff --git a/src/main/java/io/kokuwa/micronaut/logging/request/HeaderLoggingServerHttpFilter.java b/src/main/java/io/kokuwa/micronaut/logging/http/level/LogLevelServerFilter.java
similarity index 50%
rename from src/main/java/io/kokuwa/micronaut/logging/request/HeaderLoggingServerHttpFilter.java
rename to src/main/java/io/kokuwa/micronaut/logging/http/level/LogLevelServerFilter.java
index f0afce3..6626fee 100644
--- a/src/main/java/io/kokuwa/micronaut/logging/request/HeaderLoggingServerHttpFilter.java
+++ b/src/main/java/io/kokuwa/micronaut/logging/http/level/LogLevelServerFilter.java
@@ -1,83 +1,68 @@
-package io.kokuwa.micronaut.logging.request;
+package io.kokuwa.micronaut.logging.http.level;
+import java.util.Map;
import java.util.Optional;
-import javax.annotation.PostConstruct;
-import javax.annotation.PreDestroy;
+import jakarta.annotation.PostConstruct;
+import jakarta.annotation.PreDestroy;
import org.reactivestreams.Publisher;
-import org.slf4j.MDC;
import ch.qos.logback.classic.turbo.TurboFilter;
import io.kokuwa.micronaut.logging.LogbackUtil;
+import io.kokuwa.micronaut.logging.http.AbstractMdcFilter;
import io.micronaut.context.annotation.Requires;
import io.micronaut.context.annotation.Value;
-import io.micronaut.core.async.publisher.Publishers;
import io.micronaut.core.util.StringUtils;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.MutableHttpResponse;
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;
+import io.micronaut.runtime.context.scope.Refreshable;
/**
* Http request logging filter.
*
* @author Stephan Schnabel
*/
-@Requires(beans = EmbeddedServer.class)
-@Requires(property = HeaderLoggingServerHttpFilter.PREFIX + ".enabled", notEquals = StringUtils.FALSE)
-@Filter("${" + HeaderLoggingServerHttpFilter.PREFIX + ".path:/**}")
-public class HeaderLoggingServerHttpFilter implements HttpServerFilter {
-
- public static final String PREFIX = "logger.request.filter";
- public static final String MDC_FILTER = PREFIX;
- public static final String MDC_KEY = "level";
+@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;
- private final int order;
- public HeaderLoggingServerHttpFilter(
+ 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);
- this.order = order.orElse(DEFAULT_ORDER);
}
@PostConstruct
void startTurbofilter() {
- logback.getTurboFilter(HeaderLoggingTurboFilter.class, MDC_FILTER, HeaderLoggingTurboFilter::new).start();
+ logback.getTurboFilter(LogLevelTurboFilter.class, MDC_FILTER, LogLevelTurboFilter::new).start();
}
@PreDestroy
void stopTurbofilter() {
- logback.getTurboFilter(HeaderLoggingTurboFilter.class, MDC_FILTER).ifPresent(TurboFilter::stop);
- }
-
- @Override
- public int getOrder() {
- return order;
+ logback.getTurboFilter(LogLevelTurboFilter.class, MDC_FILTER).ifPresent(TurboFilter::stop);
}
@Override
public Publisher> doFilter(HttpRequest> request, ServerFilterChain chain) {
- var level = request.getHeaders().getFirst(header);
- if (level.isPresent()) {
- MDC.put(MDC_KEY, level.get());
- return Publishers.map(chain.proceed(request), response -> {
- MDC.remove(MDC_KEY);
- return response;
- });
- } else {
- return chain.proceed(request);
- }
+ return request.getHeaders().getFirst(header)
+ .map(level -> doFilter(request, chain, Map.of(MDC_KEY, level)))
+ .orElseGet(() -> chain.proceed(request));
}
}
diff --git a/src/main/java/io/kokuwa/micronaut/logging/request/HeaderLoggingTurboFilter.java b/src/main/java/io/kokuwa/micronaut/logging/http/level/LogLevelTurboFilter.java
similarity index 79%
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 c1a8dc3..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(HeaderLoggingServerHttpFilter.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/layout/GcpJsonLayout.java b/src/main/java/io/kokuwa/micronaut/logging/layout/GcpJsonLayout.java
index 0cd52d8..524601c 100644
--- a/src/main/java/io/kokuwa/micronaut/logging/layout/GcpJsonLayout.java
+++ b/src/main/java/io/kokuwa/micronaut/logging/layout/GcpJsonLayout.java
@@ -1,15 +1,11 @@
package io.kokuwa.micronaut.logging.layout;
import java.time.Instant;
-import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import ch.qos.logback.classic.spi.ILoggingEvent;
-import ch.qos.logback.contrib.json.classic.JsonLayout;
import io.micronaut.core.util.StringUtils;
-import lombok.Setter;
-import lombok.experimental.Accessors;
/**
* GCP logging layout.
@@ -18,7 +14,6 @@ import lombok.experimental.Accessors;
* @see "https://cloud.google.com/logging/docs/agent/configuration#process-payload"
* @see "https://cloud.google.com/error-reporting/reference/rest/v1beta1/ServiceContext"
*/
-@Accessors(chain = false)
public class GcpJsonLayout extends JsonLayout {
private static final String UNDEFINED = "_IS_UNDEFINED";
@@ -26,9 +21,7 @@ public class GcpJsonLayout extends JsonLayout {
private static final String SEVERITY_ATTR_NAME = "severity";
private Map serviceContext;
- @Setter
private String serviceName;
- @Setter
private String serviceVersion;
@Override
@@ -49,7 +42,7 @@ public class GcpJsonLayout extends JsonLayout {
private void addServiceContext(Map map) {
if (serviceContext == null) {
- serviceContext = new HashMap<>(2);
+ serviceContext = new LinkedHashMap<>(2);
if (StringUtils.isNotEmpty(serviceName) && !serviceName.endsWith(UNDEFINED)) {
serviceContext.put("service", serviceName);
}
@@ -61,4 +54,12 @@ public class GcpJsonLayout extends JsonLayout {
map.put("serviceContext", serviceContext);
}
}
+
+ public void setServiceName(String serviceName) {
+ this.serviceName = serviceName;
+ }
+
+ public void setServiceVersion(String serviceVersion) {
+ this.serviceVersion = serviceVersion;
+ }
}
diff --git a/src/main/java/io/kokuwa/micronaut/logging/layout/JsonLayout.java b/src/main/java/io/kokuwa/micronaut/logging/layout/JsonLayout.java
new file mode 100644
index 0000000..4665494
--- /dev/null
+++ b/src/main/java/io/kokuwa/micronaut/logging/layout/JsonLayout.java
@@ -0,0 +1,197 @@
+package io.kokuwa.micronaut.logging.layout;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.TimeZone;
+
+import ch.qos.logback.classic.pattern.ThrowableHandlingConverter;
+import ch.qos.logback.classic.pattern.ThrowableProxyConverter;
+import ch.qos.logback.classic.spi.ILoggingEvent;
+import ch.qos.logback.core.CoreConstants;
+import ch.qos.logback.core.LayoutBase;
+import ch.qos.logback.core.status.OnConsoleStatusListener;
+import ch.qos.logback.core.status.StatusUtil;
+import ch.qos.logback.core.util.StatusListenerConfigHelper;
+import io.micronaut.http.MediaType;
+import io.micronaut.json.JsonMapper;
+
+public class JsonLayout extends LayoutBase {
+
+ public static final String TIMESTAMP_ATTR_NAME = "timestamp";
+ public static final String LEVEL_ATTR_NAME = "level";
+ public static final String THREAD_ATTR_NAME = "thread";
+ public static final String MDC_ATTR_NAME = "mdc";
+ public static final String LOGGER_ATTR_NAME = "logger";
+ public static final String FORMATTED_MESSAGE_ATTR_NAME = "message";
+ public static final String MESSAGE_ATTR_NAME = "raw-message";
+ public static final String EXCEPTION_ATTR_NAME = "exception";
+ public static final String CONTEXT_ATTR_NAME = "context";
+
+ protected boolean includeLevel = true;
+ protected boolean includeThreadName = true;
+ protected boolean includeMDC = true;
+ protected boolean includeLoggerName = true;
+ protected boolean includeFormattedMessage = true;
+ protected boolean includeMessage = true;
+ protected boolean includeException = true;
+ protected boolean includeContextName = false;
+ protected boolean includeTimestamp = true;
+ private String timestampFormat;
+ private String timestampFormatTimezoneId;
+ private ThrowableHandlingConverter throwableHandlingConverter = new ThrowableProxyConverter();
+ private JsonMapper mapper;
+
+ @Override
+ public String getContentType() {
+ return MediaType.APPLICATION_JSON;
+ }
+
+ @Override
+ public void start() {
+ this.throwableHandlingConverter.start();
+ super.start();
+ }
+
+ @Override
+ public void stop() {
+ super.stop();
+ this.throwableHandlingConverter.stop();
+ }
+
+ @Override
+ public String doLayout(ILoggingEvent event) {
+ var map = toJsonMap(event);
+
+ if (mapper == null) {
+ try {
+ mapper = JsonMapper.createDefault();
+ } catch (IllegalStateException e) {
+ if (!StatusUtil.contextHasStatusListener(context)) {
+ addError("Failed to get object mapper from micronaut, please check your classpath");
+ StatusListenerConfigHelper.addOnConsoleListenerInstance(context, new OnConsoleStatusListener());
+ }
+ return map.toString() + CoreConstants.LINE_SEPARATOR;
+ }
+ }
+
+ try {
+ return new String(mapper.writeValueAsBytes(map), StandardCharsets.UTF_8) + CoreConstants.LINE_SEPARATOR;
+ } catch (IOException e) {
+ addError("Failed to write json from event " + event + " and map " + map, e);
+ return null;
+ }
+ }
+
+ protected Map toJsonMap(ILoggingEvent event) {
+ var map = new LinkedHashMap();
+ addTimestamp(TIMESTAMP_ATTR_NAME, includeTimestamp, event.getTimeStamp(), map);
+ add(LEVEL_ATTR_NAME, includeLevel, String.valueOf(event.getLevel()), map);
+ add(THREAD_ATTR_NAME, includeThreadName, event.getThreadName(), map);
+ addMap(MDC_ATTR_NAME, includeMDC, event.getMDCPropertyMap(), map);
+ add(LOGGER_ATTR_NAME, includeLoggerName, event.getLoggerName(), map);
+ add(FORMATTED_MESSAGE_ATTR_NAME, includeFormattedMessage, event.getFormattedMessage(), map);
+ add(MESSAGE_ATTR_NAME, includeMessage, event.getMessage(), map);
+ add(CONTEXT_ATTR_NAME, includeContextName, event.getLoggerContextVO().getName(), map);
+ addThrowableInfo(EXCEPTION_ATTR_NAME, includeException, event, map);
+ return map;
+ }
+
+ protected void addThrowableInfo(String fieldName, boolean field, ILoggingEvent value, Map map) {
+ if (field && value != null) {
+ var throwableProxy = value.getThrowableProxy();
+ if (throwableProxy != null) {
+ var ex = throwableHandlingConverter.convert(value);
+ if (ex != null && !ex.isEmpty()) {
+ map.put(fieldName, ex);
+ }
+ }
+ }
+ }
+
+ protected void addMap(String key, boolean field, Map mapValue, Map map) {
+ if (field && mapValue != null && !mapValue.isEmpty()) {
+ map.put(key, mapValue);
+ }
+ }
+
+ protected void addTimestamp(String key, boolean field, long timeStamp, Map map) {
+ if (field) {
+ var formatted = formatTimestamp(timeStamp);
+ if (formatted != null) {
+ map.put(key, formatted);
+ }
+ }
+ }
+
+ protected void add(String fieldName, boolean field, String value, Map map) {
+ if (field && value != null) {
+ map.put(fieldName, value);
+ }
+ }
+
+ protected String formatTimestamp(long timestamp) {
+ if (timestampFormat == null || timestamp < 0) {
+ return String.valueOf(timestamp);
+ }
+ var date = new Date(timestamp);
+ var format = new SimpleDateFormat(timestampFormat);
+ if (timestampFormatTimezoneId != null) {
+ format.setTimeZone(TimeZone.getTimeZone(timestampFormatTimezoneId));
+ }
+ return format.format(date);
+ }
+
+ // setter
+
+ public void setIncludeLevel(boolean includeLevel) {
+ this.includeLevel = includeLevel;
+ }
+
+ public void setIncludeThreadName(boolean includeThreadName) {
+ this.includeThreadName = includeThreadName;
+ }
+
+ public void setIncludeMDC(boolean includeMDC) {
+ this.includeMDC = includeMDC;
+ }
+
+ public void setIncludeLoggerName(boolean includeLoggerName) {
+ this.includeLoggerName = includeLoggerName;
+ }
+
+ public void setIncludeFormattedMessage(boolean includeFormattedMessage) {
+ this.includeFormattedMessage = includeFormattedMessage;
+ }
+
+ public void setIncludeMessage(boolean includeMessage) {
+ this.includeMessage = includeMessage;
+ }
+
+ public void setIncludeException(boolean includeException) {
+ this.includeException = includeException;
+ }
+
+ public void setIncludeContextName(boolean includeContextName) {
+ this.includeContextName = includeContextName;
+ }
+
+ public void setIncludeTimestamp(boolean includeTimestamp) {
+ this.includeTimestamp = includeTimestamp;
+ }
+
+ public void setTimestampFormat(String timestampFormat) {
+ this.timestampFormat = timestampFormat;
+ }
+
+ public void setTimestampFormatTimezoneId(String timestampFormatTimezoneId) {
+ this.timestampFormatTimezoneId = timestampFormatTimezoneId;
+ }
+
+ public void setThrowableHandlingConverter(ThrowableHandlingConverter throwableHandlingConverter) {
+ this.throwableHandlingConverter = throwableHandlingConverter;
+ }
+}
diff --git a/src/main/java/io/kokuwa/micronaut/logging/mdc/MDCTurboFilter.java b/src/main/java/io/kokuwa/micronaut/logging/mdc/MDCTurboFilter.java
index c6f01b1..db4d805 100644
--- a/src/main/java/io/kokuwa/micronaut/logging/mdc/MDCTurboFilter.java
+++ b/src/main/java/io/kokuwa/micronaut/logging/mdc/MDCTurboFilter.java
@@ -50,7 +50,13 @@ public class MDCTurboFilter extends TurboFilter {
}
@Override
- public FilterReply decide(Marker marker, Logger logger, Level level, String format, Object[] params, Throwable t) {
+ public FilterReply decide(
+ Marker marker,
+ Logger logger,
+ Level eventLevel,
+ String format,
+ Object[] params,
+ Throwable t) {
if (logger == null || !isStarted()) {
return FilterReply.NEUTRAL;
@@ -67,6 +73,6 @@ public class MDCTurboFilter extends TurboFilter {
return FilterReply.NEUTRAL;
}
- return level.isGreaterOrEqual(this.level) ? FilterReply.ACCEPT : FilterReply.NEUTRAL;
+ return eventLevel.isGreaterOrEqual(this.level) ? FilterReply.ACCEPT : FilterReply.NEUTRAL;
}
}
diff --git a/src/main/java/io/kokuwa/micronaut/logging/mdc/MDCTurboFilterConfigurer.java b/src/main/java/io/kokuwa/micronaut/logging/mdc/MDCTurboFilterConfigurer.java
index e83217c..de87997 100644
--- a/src/main/java/io/kokuwa/micronaut/logging/mdc/MDCTurboFilterConfigurer.java
+++ b/src/main/java/io/kokuwa/micronaut/logging/mdc/MDCTurboFilterConfigurer.java
@@ -1,45 +1,71 @@
package io.kokuwa.micronaut.logging.mdc;
+import java.util.Collection;
import java.util.Set;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
import ch.qos.logback.classic.Level;
import io.kokuwa.micronaut.logging.LogbackUtil;
import io.micronaut.context.annotation.BootstrapContextCompatible;
import io.micronaut.context.annotation.Context;
import io.micronaut.context.annotation.Requires;
import io.micronaut.context.env.Environment;
-import io.micronaut.core.annotation.Internal;
import io.micronaut.core.type.Argument;
import io.micronaut.core.util.StringUtils;
-import lombok.extern.slf4j.Slf4j;
+import io.micronaut.logging.LogLevel;
+import io.micronaut.logging.LoggingSystem;
/**
* Configure MDC filter.
*
* @author Stephan Schnabel
*/
+@BootstrapContextCompatible
@Requires(beans = LogbackUtil.class)
@Requires(property = MDCTurboFilterConfigurer.PREFIX)
@Requires(property = MDCTurboFilterConfigurer.PREFIX + ".enabled", notEquals = StringUtils.FALSE)
-@BootstrapContextCompatible
@Context
-@Internal
-@Slf4j
-public class MDCTurboFilterConfigurer {
+public class MDCTurboFilterConfigurer implements LoggingSystem {
public static final String PREFIX = "logger.mdc";
+ private static final Logger log = LoggerFactory.getLogger(MDCTurboFilterConfigurer.class);
+
private final LogbackUtil logback;
private final Environment environment;
+ private Collection mdcs = Set.of();
+ private boolean initialized;
+
public MDCTurboFilterConfigurer(LogbackUtil logback, Environment environment) {
this.logback = logback;
this.environment = environment;
- configure();
+ this.refresh();
}
- public void configure() {
- for (var name : environment.getPropertyEntries(PREFIX)) {
+ @Override
+ public final void refresh() {
+
+ mdcs = environment.getPropertyEntries(PREFIX);
+ initialized = false;
+
+ if (environment.getProperties("logger.levels").isEmpty()) {
+ log.warn("MDCs are configured, but no levels are set. MDC may not work.");
+ }
+ }
+
+ @Override
+ public void setLogLevel(String name, LogLevel level) {
+ if (!initialized) {
+ configure();
+ initialized = true;
+ }
+ }
+
+ private void configure() {
+ for (var name : mdcs) {
var prefix = PREFIX + "." + name + ".";
var key = environment.getProperty(prefix + "key", String.class, name);
diff --git a/src/main/java/io/kokuwa/micronaut/logging/request/PrincipalHttpFilter.java b/src/main/java/io/kokuwa/micronaut/logging/request/PrincipalHttpFilter.java
deleted file mode 100644
index 0e7a14d..0000000
--- a/src/main/java/io/kokuwa/micronaut/logging/request/PrincipalHttpFilter.java
+++ /dev/null
@@ -1,63 +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.core.async.publisher.Publishers;
-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.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.PREFIX + ".enabled", notEquals = StringUtils.FALSE)
-@Filter("${" + PrincipalHttpFilter.PREFIX + ".path:/**}")
-public class PrincipalHttpFilter implements HttpServerFilter {
-
- public static final String PREFIX = "logger.request.principal";
-
- public static final String DEFAULT_KEY = "principal";
- 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) {
- var princial = request.getUserPrincipal();
- if (princial.isPresent()) {
- MDC.put(key, princial.get().getName());
- return Publishers.map(chain.proceed(request), response -> {
- MDC.remove(key);
- return response;
- });
- } else {
- 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/io/kokuwa/logback/appender-console.xml b/src/main/resources/io/kokuwa/logback/appender-console.xml
deleted file mode 100644
index e4bfb1d..0000000
--- a/src/main/resources/io/kokuwa/logback/appender-console.xml
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
- ${CONSOLE_LOG_JANSI:-true}
-
- ${CONSOLE_LOG_PATTERN:-%cyan(%d{HH:mm:ss.SSS}) %gray(%-6.6thread) %highlight(%-5level) %magenta(%32logger{32}) %mdc %msg%n}
- ${CONSOLE_LOG_CHARSET:-default}
-
-
-
-
diff --git a/src/main/resources/io/kokuwa/logback/appender-gcp.xml b/src/main/resources/io/kokuwa/logback/appender-gcp.xml
deleted file mode 100644
index e847d99..0000000
--- a/src/main/resources/io/kokuwa/logback/appender-gcp.xml
+++ /dev/null
@@ -1,17 +0,0 @@
-
-
-
-
-
-
- ${SERVICE_NAME}
- ${SERVICE_VERSION}
-
- true
- true
- false
-
-
-
-
-
diff --git a/src/main/resources/io/kokuwa/logback/appender-json.xml b/src/main/resources/io/kokuwa/logback/appender-json.xml
deleted file mode 100644
index b850ac7..0000000
--- a/src/main/resources/io/kokuwa/logback/appender-json.xml
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-
-
-
-
-
- true
- true
- false
-
-
-
-
-
diff --git a/src/main/resources/io/kokuwa/logback/base.xml b/src/main/resources/io/kokuwa/logback/base.xml
deleted file mode 100644
index 24ae946..0000000
--- a/src/main/resources/io/kokuwa/logback/base.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
-
-
-
-
diff --git a/src/main/resources/io/kokuwa/logback/logback-default.xml b/src/main/resources/io/kokuwa/logback/logback-default.xml
index 1092ea9..78fe3f0 100644
--- a/src/main/resources/io/kokuwa/logback/logback-default.xml
+++ b/src/main/resources/io/kokuwa/logback/logback-default.xml
@@ -1,8 +1,6 @@
-
-
diff --git a/src/main/resources/io/kokuwa/logback/logback-example.xml b/src/main/resources/io/kokuwa/logback/logback-example.xml
deleted file mode 100644
index 47deab2..0000000
--- a/src/main/resources/io/kokuwa/logback/logback-example.xml
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/test/java/io/kokuwa/micronaut/logging/AbstractTest.java b/src/test/java/io/kokuwa/micronaut/logging/AbstractTest.java
index 53960e4..ebcc5e7 100644
--- a/src/test/java/io/kokuwa/micronaut/logging/AbstractTest.java
+++ b/src/test/java/io/kokuwa/micronaut/logging/AbstractTest.java
@@ -2,7 +2,7 @@ package io.kokuwa.micronaut.logging;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.MethodOrderer.DisplayName;
+import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.TestMethodOrder;
import org.slf4j.MDC;
@@ -14,7 +14,7 @@ import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
* @author Stephan Schnabel
*/
@MicronautTest
-@TestMethodOrder(DisplayName.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 5fc80fa..186c2a3 100644
--- a/src/test/java/io/kokuwa/micronaut/logging/mdc/MDCTurboFilterTest.java
+++ b/src/test/java/io/kokuwa/micronaut/logging/mdc/MDCTurboFilterTest.java
@@ -17,7 +17,7 @@ import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
*
* @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 c0bd1d8..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.extensions.junit5.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 2b7ee19..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 HeaderLoggingServerHttpFilter}.
- *
- * @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 669e907..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 HeaderLoggingServerHttpFilter} 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(HeaderLoggingServerHttpFilter.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 d179f63..0000000
--- a/src/test/java/io/kokuwa/micronaut/logging/request/TestController.java
+++ /dev/null
@@ -1,54 +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 HeaderLoggingServerHttpFilter} 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;
- }
-
- log.info("Test log for MDC inclusion, expected: {}", principal);
-
- 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/application-test-composite.yaml b/src/test/resources/application-test-composite.yaml
deleted file mode 100644
index d15a885..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 c0fd2a2..69cbc89 100644
--- a/src/test/resources/application-test-mdc.yaml
+++ b/src/test/resources/application-test-mdc.yaml
@@ -1,4 +1,6 @@
logger:
+ levels:
+ io.micronaut.logging.PropertiesLoggingLevelsConfigurer: "OFF"
mdc:
key1:
key: key
diff --git a/src/test/resources/application-test.yaml b/src/test/resources/application-test.yaml
index 71beb2c..867b23e 100644
--- a/src/test/resources/application-test.yaml
+++ b/src/test/resources/application-test.yaml
@@ -7,6 +7,3 @@ micronaut:
generator:
secret: pleaseChangeThisSecretForANewOne
jws-algorithm: HS256
- http:
- client:
- logger-name: io.kokuwa.Test