From 976b2d27a5be0982080e9cc4d3b0bd265cd71b4b Mon Sep 17 00:00:00 2001 From: Stephan Schnabel Date: Wed, 5 Aug 2020 21:38:05 +0200 Subject: [PATCH] Initial commit --- LICENSE | 201 ++++++++++++++++++ README.md | 52 +++++ pom.xml | 130 +++++++++++ .../micronaut/logging/MDCTurboFilter.java | 74 +++++++ .../logging/MDCTurboFilterConfigurer.java | 86 ++++++++ .../logging/StackdriverJsonLayout.java | 72 +++++++ src/main/resources/logback.xml | 25 +++ 7 files changed, 640 insertions(+) create mode 100644 LICENSE create mode 100644 README.md create mode 100644 pom.xml create mode 100644 src/main/java/io/kokuwa/micronaut/logging/MDCTurboFilter.java create mode 100644 src/main/java/io/kokuwa/micronaut/logging/MDCTurboFilterConfigurer.java create mode 100644 src/main/java/io/kokuwa/micronaut/logging/StackdriverJsonLayout.java create mode 100644 src/main/resources/logback.xml diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "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. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "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. + + "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). + + "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. + + "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." + + "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. + + 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. + + 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. + + 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: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (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 + + (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. + + 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. + + 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. + + 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. + + 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. + + 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. + + 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. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + 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. + + Copyright [yyyy] [name of copyright owner] + + 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 + + http://www.apache.org/licenses/LICENSE-2.0 + + 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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..f83fd31 --- /dev/null +++ b/README.md @@ -0,0 +1,52 @@ +# Micronaut Logging support + +## Features + + * Buildin appender: + * console format + * stackdriver format (with support for error reporting) + * logback filter for log-levels per mdc + +## Usage + +MDC example: +``` +logger: + levels: + io.kokuwa: INFO + mdc: + gateway: + level: DEBUG + loggers: + - io.kokuwa + values: + - 6a1bae7f-eb6c-4c81-af9d-dc15396584e2 + - fb3318f1-2c73-48e9-acd4-a2be3c9f9256 +``` + +## Build & Release + +### Dependency updates + +Display dependency updates: +``` +mvn versions:display-property-updates -U +``` + +Update dependencies: +``` +mvn versions:update-properties +``` + +### Release locally + +Run: +``` +mvn release:prepare release:perform release:clean -B +``` + +## Open Topics + + * tests + * configure mdc on refresh event + * examples and documentation \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..77bd9c0 --- /dev/null +++ b/pom.xml @@ -0,0 +1,130 @@ + + + 4.0.0 + + + io.kokuwa + maven-parent + 0.1.0 + + + + io.kokuwa.micronaut + micronaut-logging + 0.0.1-SNAPSHOT + + Micronaut Logging Support + TODO + https://github.com/kokuwaio/micronaut-logging + 2020 + + + Apache License 2.0 + https://www.apache.org/licenses/LICENSE-2.0 + + + + + https://github.com/kokuwaio/micronaut-logging + scm:git:https://github.com/kokuwaio/micronaut-logging.git + scm:git:https://github.com/kokuwaio/micronaut-logging.git + HEAD + + + github + https://github.com/kokuwaio/micronaut-logging/issues + + + + + + + + + 0.1.5 + 2.0.1 + + + + + + + + + io.micronaut + micronaut-bom + ${version.io.micronaut} + pom + import + + + + + ch.qos.logback.contrib + logback-json-classic + ${version.ch.qos.logback.contrib} + + + ch.qos.logback.contrib + logback-json-core + ${version.ch.qos.logback.contrib} + + + ch.qos.logback.contrib + logback-jackson + ${version.ch.qos.logback.contrib} + + + + + + + + + io.micronaut + micronaut-runtime + provided + + + + + ch.qos.logback + logback-classic + + + ch.qos.logback.contrib + logback-jackson + + + ch.qos.logback.contrib + logback-json-classic + + + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + org.projectlombok + lombok + ${version.org.projectlombok} + + + io.micronaut + micronaut-inject-java + ${version.io.micronaut} + + + + + + + + diff --git a/src/main/java/io/kokuwa/micronaut/logging/MDCTurboFilter.java b/src/main/java/io/kokuwa/micronaut/logging/MDCTurboFilter.java new file mode 100644 index 0000000..1f00fd6 --- /dev/null +++ b/src/main/java/io/kokuwa/micronaut/logging/MDCTurboFilter.java @@ -0,0 +1,74 @@ +package io.kokuwa.micronaut.logging; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.slf4j.MDC; +import org.slf4j.Marker; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.turbo.TurboFilter; +import ch.qos.logback.core.Context; +import ch.qos.logback.core.spi.FilterReply; + +/** + * Filter for log levels based on mdc. + * + * @author Stephan Schnabel + */ +public class MDCTurboFilter extends TurboFilter { + + private final String key; + private final Map cache = new HashMap<>(); + private final Set loggers = new HashSet<>(); + private final Set values = new HashSet<>(); + private Level level; + + public MDCTurboFilter(String name, String key, Context context) { + this.key = key; + this.level = Level.TRACE; + this.setName(name); + this.setContext(context); + } + + public MDCTurboFilter setLoggers(Set loggers) { + this.cache.clear(); + this.loggers.clear(); + this.loggers.addAll(loggers); + return this; + } + + public MDCTurboFilter setValues(Set values) { + this.values.clear(); + this.values.addAll(values); + return this; + } + + public MDCTurboFilter setLevel(Level level) { + this.level = level; + return this; + } + + @Override + public FilterReply decide(Marker marker, Logger logger, Level level, String format, Object[] params, Throwable t) { + + if (logger == null || !isStarted() || values.isEmpty() || loggers.isEmpty()) { + return FilterReply.NEUTRAL; + } + + var value = MDC.get(key); + if (value == null || !values.contains(value)) { + return FilterReply.NEUTRAL; + } + + var isLoggerIncluded = !cache.computeIfAbsent(logger.getName(), k -> loggers.stream().anyMatch(k::startsWith)); + if (isLoggerIncluded) { + return FilterReply.NEUTRAL; + } + + return level.isGreaterOrEqual(this.level) ? FilterReply.ACCEPT : FilterReply.NEUTRAL; + } +} diff --git a/src/main/java/io/kokuwa/micronaut/logging/MDCTurboFilterConfigurer.java b/src/main/java/io/kokuwa/micronaut/logging/MDCTurboFilterConfigurer.java new file mode 100644 index 0000000..4468643 --- /dev/null +++ b/src/main/java/io/kokuwa/micronaut/logging/MDCTurboFilterConfigurer.java @@ -0,0 +1,86 @@ +package io.kokuwa.micronaut.logging; + +import java.util.Objects; +import java.util.Set; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.LoggerContext; +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.context.event.ApplicationEventListener; +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.type.Argument; +import io.micronaut.runtime.context.scope.refresh.RefreshEvent; + +/** + * Configure mdc filter. + * + * @author Stephan Schnabel + */ +@BootstrapContextCompatible +@Context +@Requires(classes = LoggerContext.class) +@Requires(property = MDCTurboFilterConfigurer.LOGGER_MDCS_PROPERTY_PREFIX) +@Internal +public class MDCTurboFilterConfigurer implements ApplicationEventListener { + + public static final String LOGGER_MDCS_PROPERTY_PREFIX = "logger.mdc"; + + private static final Logger log = LoggerFactory.getLogger(MDCTurboFilterConfigurer.class); + private final LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory(); + private final Environment environment; + + public MDCTurboFilterConfigurer(Environment environment) { + this.environment = environment; + configure(); + } + + @Override + public void onApplicationEvent(RefreshEvent event) { + if (event.getSource().keySet().stream().anyMatch(key -> key.startsWith(LOGGER_MDCS_PROPERTY_PREFIX))) { + configure(); + } + } + + private void configure() { + for (var name : environment.getPropertyEntries(LOGGER_MDCS_PROPERTY_PREFIX)) { + + var prefix = LOGGER_MDCS_PROPERTY_PREFIX + "." + name + "."; + var key = environment.getProperty(prefix + "key", String.class, name); + var loggers = environment.getProperty(prefix + "loggers", Argument.setOf(String.class)).orElseGet(Set::of); + var values = environment.getProperty(prefix + "values", Argument.setOf(String.class)).orElseGet(Set::of); + var level = Level.valueOf(environment.getProperty(prefix + "level", String.class, Level.TRACE.toString())); + + getFilter(name, key).setLoggers(loggers).setValues(values).setLevel(level); + + log.info("Configured MDC filter {} for key {} with level {}.", name, key, level); + } + } + + private MDCTurboFilter getFilter(String name, String key) { + + // get filter + + var filterName = LOGGER_MDCS_PROPERTY_PREFIX + "." + key; + var filterOptional = context.getTurboFilterList().stream() + .filter(f -> Objects.equals(filterName, f.getName())) + .filter(MDCTurboFilter.class::isInstance) + .map(MDCTurboFilter.class::cast) + .findAny(); + if (filterOptional.isPresent()) { + return filterOptional.get(); + } + + // add filter + + var filter = new MDCTurboFilter(name, key, context); + filter.start(); + context.addTurboFilter(filter); + return filter; + } +} diff --git a/src/main/java/io/kokuwa/micronaut/logging/StackdriverJsonLayout.java b/src/main/java/io/kokuwa/micronaut/logging/StackdriverJsonLayout.java new file mode 100644 index 0000000..a609ca1 --- /dev/null +++ b/src/main/java/io/kokuwa/micronaut/logging/StackdriverJsonLayout.java @@ -0,0 +1,72 @@ +package io.kokuwa.micronaut.logging; + +import java.time.Instant; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; + +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.contrib.jackson.JacksonJsonFormatter; +import ch.qos.logback.contrib.json.classic.JsonLayout; +import io.micronaut.core.util.StringUtils; +import lombok.Getter; +import lombok.Setter; + +/** + * Stackdriver layout. + * + * @author Stephan Schnabel + * @see "https://cloud.google.com/logging/docs/agent/configuration#process-payload" + * @see "https://cloud.google.com/error-reporting/reference/rest/v1beta1/ServiceContext" + */ +@Getter +@Setter +public class StackdriverJsonLayout extends JsonLayout { + + private static final String TIMESTAMP_ATTR_NAME = "time"; + private static final String SEVERITY_ATTR_NAME = "severity"; + + private Map serviceContext; + private String serviceName; + private String serviceVersion; + private boolean includeExceptionInMessage; + + public StackdriverJsonLayout() { + appendLineSeparator = true; + includeContextName = false; + includeMessage = true; + includeExceptionInMessage = true; + setJsonFormatter(new JacksonJsonFormatter()); + } + + @Override + protected Map toJsonMap(ILoggingEvent event) { + var map = new LinkedHashMap(); + add(TIMESTAMP_ATTR_NAME, includeTimestamp, Instant.ofEpochMilli(event.getTimeStamp()).toString(), map); + add(SEVERITY_ATTR_NAME, includeLevel, String.valueOf(event.getLevel()), map); + add(THREAD_ATTR_NAME, includeThreadName, event.getThreadName(), map); + add(CONTEXT_ATTR_NAME, includeContextName, event.getLoggerContextVO().getName(), map); + add(LOGGER_ATTR_NAME, includeLoggerName, event.getLoggerName(), map); + addMap(MDC_ATTR_NAME, includeMDC, event.getMDCPropertyMap(), map); + add(FORMATTED_MESSAGE_ATTR_NAME, includeFormattedMessage, event.getFormattedMessage(), map); + add(MESSAGE_ATTR_NAME, includeMessage, event.getMessage(), map); + addThrowableInfo(JsonLayout.EXCEPTION_ATTR_NAME, includeException, event, map); + addServiceContext(map); + return map; + } + + private void addServiceContext(Map map) { + if (serviceContext == null) { + serviceContext = new HashMap<>(2); + if (StringUtils.isNotEmpty(serviceName)) { + serviceContext.put("service", serviceName); + } + if (StringUtils.isNotEmpty(serviceVersion)) { + serviceContext.put("version", serviceVersion); + } + } + if (!serviceContext.isEmpty()) { + map.put("serviceContext", serviceContext); + } + } +} diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml new file mode 100644 index 0000000..42a34f7 --- /dev/null +++ b/src/main/resources/logback.xml @@ -0,0 +1,25 @@ + + + + + + + true + + %cyan(%d{HH:mm:ss.SSS}) %gray(%-6.6thread) %highlight(%-5level) %magenta(%32logger{32}) %mdc %msg%n + + + + + + ${serviceName} + ${serviceVersion} + + + + + + + + +