diff --git a/.gitignore b/.gitignore index 9154f4c..17ff6cd 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,5 @@ hs_err_pid* replay_pid* +.idea +.idea/** \ No newline at end of file diff --git a/README.md b/README.md index 7a5962c..101d18e 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,148 @@ -# OpenFeign-NormalizedLogger +# OpenFeign Normalized Logger -Normalized Logger for OpenFeign \ No newline at end of file +![](https://img.shields.io/maven-central/v/me.bvn13.openfeign.logger/feign-normalized-logger) + +Standard OpenFeign logger provides the only approach to log communications - +it logs every header in separated log entries, the body goes into another log entry. + +It is very inconvenient to deal with such logs in production especially in multithreaded systems. + +This 'Normalized Logger' is intended to combine all log entries related to one request-reply +communication into one log entry. + +## Old bad Logger + +All parts are separeted from each other: +1) Request: + 1) request headers - every header is put into separated entry + 2) requst body - at separated entry +2) Response: + 1) response headers - separately + 2) response body - at separated entry as well + +``` + +2022-07-25 14:12:43.572 DEBUG 1032530 --- [Executor] feign.Logger : [AuthApi#login] ---> POST https://example.com/api/v1/login HTTP/1.1 +2022-07-25 14:12:43.573 DEBUG 1032530 --- [Executor] feign.Logger : [AuthApi#login] Content-Length: 23 +2022-07-25 14:12:43.573 DEBUG 1032530 --- [Executor] feign.Logger : [AuthApi#login] Content-Type: application/json +2022-07-25 14:12:43.574 DEBUG 1032530 --- [Executor] feign.Logger : [AuthApi#login] user-agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4464.5 Safari/537.36 +2022-07-25 14:12:43.575 DEBUG 1032530 --- [Executor] feign.Logger : [AuthApi#login] +2022-07-25 14:12:43.576 DEBUG 1032530 --- [Executor] feign.Logger : [AuthApi#login] {"login":"123456789"} +2022-07-25 14:12:43.576 DEBUG 1032530 --- [Executor] feign.Logger : [AuthApi#login] ---> END HTTP (21-byte body) +2022-07-25 14:12:43.901 DEBUG 1032530 --- [Executor] feign.Logger : [AuthApi#login] <--- UNKNOWN 200 (324ms) +2022-07-25 14:12:43.901 DEBUG 1032530 --- [Executor] feign.Logger : [AuthApi#login] cache-control: no-cache +2022-07-25 14:12:43.901 DEBUG 1032530 --- [Executor] feign.Logger : [AuthApi#login] cf-cache-status: DYNAMIC +2022-07-25 14:12:43.901 DEBUG 1032530 --- [Executor] feign.Logger : [AuthApi#login] cf-ray: 730476518ea441ce-AMS +2022-07-25 14:12:43.901 DEBUG 1032530 --- [Executor] feign.Logger : [AuthApi#login] content-type: application/json +2022-07-25 14:12:43.901 DEBUG 1032530 --- [Executor] feign.Logger : [AuthApi#login] date: Mon, 25 Jul 2022 11:12:43 GMT +2022-07-25 14:12:43.901 DEBUG 1032530 --- [Executor] feign.Logger : [AuthApi#login] expect-ct: max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct" +2022-07-25 14:12:43.901 DEBUG 1032530 --- [Executor] feign.Logger : [AuthApi#login] feature-policy: accelerometer 'none'; camera 'none'; geolocation 'none'; gyroscope 'none'; magnetometer 'none'; microphone 'none'; payment *; usb 'none' +2022-07-25 14:12:43.901 DEBUG 1032530 --- [Executor] feign.Logger : [AuthApi#login] referrer-policy: strict-origin-when-cross-origin +2022-07-25 14:12:43.901 DEBUG 1032530 --- [Executor] feign.Logger : [AuthApi#login] server: cloudflare +2022-07-25 14:12:43.901 DEBUG 1032530 --- [Executor] feign.Logger : [AuthApi#login] set-cookie: ACCESS_TOKEN=eyJh9uygCUMA659bAZ54SHpSNy_KFXQ; Max-Age=1800; Domain=.example.com; Path=/; Secure; SameSite=None +2022-07-25 14:12:43.901 DEBUG 1032530 --- [Executor] feign.Logger : [AuthApi#login] strict-transport-security: max-age=31536000 +2022-07-25 14:12:43.901 DEBUG 1032530 --- [Executor] feign.Logger : [AuthApi#login] x-content-type-options: nosniff +2022-07-25 14:12:43.901 DEBUG 1032530 --- [Executor] feign.Logger : [AuthApi#login] x-frame-options: sameorigin +2022-07-25 14:12:43.901 DEBUG 1032530 --- [Executor] feign.Logger : [AuthApi#login] x-xss-protection: 1; mode=block +2022-07-25 14:12:43.901 DEBUG 1032530 --- [Executor] feign.Logger : [AuthApi#login] +2022-07-25 14:12:43.902 DEBUG 1032530 --- [Executor] feign.Logger : [AuthApi#login] {"status":{"code":"OK","message":"OK"},"body":{"id":"20826"}} +2022-07-25 14:12:43.902 DEBUG 1032530 --- [Executor] feign.Logger : [AuthApi#login] <--- END HTTP (61-byte body) + +``` + +## New Normalized Logger + +The whole communication (request and response parts) is combined into one log entry. + +``` + +2022-07-25 14:16:06.217 INFO 1057053 --- [Executor] com.pf.profee.api.NormalizedFeignLogger : normalized feign request {AuthApi#login(LoginRequestDto)=[AuthApi#login] }: [ +---> POST https://example.com/api/v1/login HTTP/1.1 +Content-Length: 23 +Content-Type: application/json +user-agent: bvn13 | Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4464.5 Safari/537.36 + +{"phone":"123456789"} +---> END HTTP (21-byte body) +] has response [ +<--- UNKNOWN 200 (411ms) +cache-control: no-cache +cf-cache-status: DYNAMIC +cf-ray: 73047b41ffd2fa4c-AMS +content-type: application/json +date: Mon, 25 Jul 2022 11:16:06 GMT +expect-ct: max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct" +feature-policy: accelerometer 'none'; camera 'none'; geolocation 'none'; gyroscope 'none'; magnetometer 'none'; microphone 'none'; payment *; usb 'none' +referrer-policy: strict-origin-when-cross-origin +server: cloudflare +set-cookie: QA_JWT_ACCESS_TOKEN=eyJhboft6rzD6Be16dXY5lgQNCzOZNFe4ra_NDIdmXlXi19hlvaQ; Max-Age=1800; Domain=.example.com; Path=/; Secure; SameSite=None +strict-transport-security: max-age=31536000 +x-content-type-options: nosniff +x-frame-options: sameorigin +x-xss-protection: 1; mode=block + +{"status":{"code":"OK","message":"OK"},"body":{"id":"20826"}} +<--- END HTTP (61-byte body) +] + +``` + +# How to use + +In order to user Normalized Logger into the application they must the following. + +## 0) Check the latest version + +at [Maven Central Repo](https://repo1.maven.org/maven2/me/bvn13/openfeign/logger) + +## 1) Add dependency + +for Maven + +```xml + + me.bvn13.openfeign.logger + feign-normalized-logger + 0.1.0 + +``` + +for Gradle + +```groovy +implementation 'me.bvn13.openfeign.logger:feign-normalized-logger:0.1.0' +``` + +## 2) Create Feign configuration and enable logger + +```java +import feign.Logger; + +public class MyFeignConfig { + + @Bean + public Logger logger() { + return new NormalizedFeignLogger(); + } + +} +``` + +### 3) Use this configuration into `@FeignClient` objects + +```java +@FeignClient(name = "auth", configuration = MyFeignConfig.class) +public interface AuthApi { +/*...methods...*/ +} +``` + +### 4) Adjust DEBUG level for Normalized Logger + +for Slf4J + Logback + +```yaml +logging: + level: + me.bvn13.openfeign.logger.NormalizedFeignLogger: DEBUG +``` \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..7cc9514 --- /dev/null +++ b/pom.xml @@ -0,0 +1,229 @@ + + + + 4.0.0 + + me.bvn13.openfeign.logger + feign-normalized-logger + 0.1.0-SNAPSHOT + + jar + + OpenFeign Normalized Logger + Normalized Logger for OpenFeign + https://github.com/bvn13/OpenFeign-NormalizedLogger + + + + Apache License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + + + + + + 1.8 + 1.8 + + + UTF-8 + UTF-8 + + + 11.9.1 + 1.7.9 + + + https://s01.oss.sonatype.org + 2.8.2 + 3.0.0-M7 + 1.6.13 + 1.6 + 1.18.0 + + + + + + internal.repo + Temporary Staging Repository + file://${project.build.directory}/mvn-repo + + + + + + io.github.openfeign + feign-core + ${feign.version} + provided + + + org.slf4j + slf4j-api + ${slf4j.version} + provided + + + + junit + junit + 4.12 + test + + + + + feign-normalized-logger + + + + + maven-source-plugin + + + attach-sources + + jar-no-fork + + + + + + maven-surefire-plugin + ${maven-surefire-plugin.version} + + + com.amashchenko.maven.plugin + gitflow-maven-plugin + ${gitflow-maven-plugin.version} + + false + + develop + + true + 2 + + + + + + + + + bvn13 + Vyacheslav Boyko + dev@bvn13.me + + Developer + + + + + + scm:git:git://github.com/bvn13/OpenFeign-NormalizedLogger.git + scm:git:ssh://git@github.com:bvn13/OpenFeign-NormalizedLogger.git + HEAD + https://github.com/bvn13/OpenFeign-NormalizedLogger.git + + + + + release + + + + org.apache.maven.plugins + maven-source-plugin + + + attach-sources + verify + + jar-no-fork + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + + + attach-javadocs + + jar + + + + + + org.apache.maven.plugins + maven-deploy-plugin + ${maven-deploy-plugin.version} + + true + + + + org.sonatype.plugins + nexus-staging-maven-plugin + ${nexus-staging-maven-plugin.version} + true + + + default-deploy + deploy + + deploy + + + + + ossrh + ${nexus.url} + true + + + + org.apache.maven.plugins + maven-gpg-plugin + ${maven-gpg-plugin.version} + + + sign-artifacts + verify + + sign + + + + + + + + + + ossrh + ${nexus.url}/content/repositories/snapshots + + + ossrh + ${nexus.url}/service/local/staging/deploy/maven2/ + + + + + + jcenter + JCenter + https://jcenter.bintray.com/ + + + + + + \ No newline at end of file diff --git a/src/main/java/me/bvn13/openfeign/logger/normalized/NormalizedFeignLogger.java b/src/main/java/me/bvn13/openfeign/logger/normalized/NormalizedFeignLogger.java new file mode 100644 index 0000000..d4d3a40 --- /dev/null +++ b/src/main/java/me/bvn13/openfeign/logger/normalized/NormalizedFeignLogger.java @@ -0,0 +1,122 @@ +package me.bvn13.openfeign.logger.normalized; + +import feign.Request; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * OpenFeign Logger + * combines request and response part into single log entry: + *

+ *
+ * {@code
+ *
+ * normalized feign request (HERE-IS-CLASS-AND-METHOD): [
+ *
+ * ] has response [
+ *
+ * ]
+ * }
+ * 
+ */ +public class NormalizedFeignLogger extends feign.Logger { + + private static final Logger log = LoggerFactory.getLogger(NormalizedFeignLogger.class); + + private final ThreadLocal> methodName; + private final ThreadLocal>> logsRequest; + private final ThreadLocal>> logsResponse; + private final ThreadLocal> isResponse; + + public NormalizedFeignLogger() { + methodName = new ThreadLocal<>(); + isResponse = new ThreadLocal<>(); + logsRequest = new ThreadLocal<>(); + logsResponse = new ThreadLocal<>(); + } + + @Override + protected void logRequest(String configKey, Level logLevel, Request request) { + init(); + super.logRequest(configKey, logLevel, request); + } + + @Override + protected void log(String configKey, String format, Object... args) { + if (format.startsWith("--->") && !format.startsWith("---> END")) { + // the very beginning + clean(configKey); + } + if (!isResponse.get().getOrDefault(configKey, false)) { + log(logsRequest, configKey, format, args); + } else { + log(logsResponse, configKey, format, args); + if (format.startsWith("<--- END")) { + showLogs(configKey); + } + } + if (format.startsWith("---> END")) { + isResponse.get().put(configKey, true); + } + } + + private void init() { + if (isResponse.get() == null) { + isResponse.set(new ConcurrentHashMap<>()); + } + if (methodName.get() == null) { + methodName.set(new ConcurrentHashMap<>()); + } + if (logsRequest.get() == null) { + logsRequest.set(new ConcurrentHashMap<>()); + } + if (logsResponse.get() == null) { + logsResponse.set(new ConcurrentHashMap<>()); + } + } + + private void clean(String configKey) { + isResponse.get().put(configKey, false); + methodName.get().put(configKey, methodTag(configKey)); + logsRequest.get().put(configKey, new LinkedList<>()); + logsResponse.get().put(configKey, new LinkedList<>()); + } + + private void log(ThreadLocal>> container, String configKey, String format, Object... args) { + extractList(container, configKey) + .add(String.format(format, args)); + } + + private void showLogs(String configKey) { + log.debug("normalized feign request " + methodName.get() + ": [\n" + + collectionToDelimitedString(logsRequest.get().getOrDefault(configKey, Collections.emptyList()), "\n") + + "\n] has response [\n" + + collectionToDelimitedString(logsResponse.get().getOrDefault(configKey, Collections.emptyList()), "\n") + + "\n]"); + } + + private List extractList(ThreadLocal>> container, String configKey) { + return container.get().get(configKey); + } + + private String collectionToDelimitedString(Collection collection, String delimeter) { + final StringBuilder sb = new StringBuilder(); + final Iterator iter = collection.iterator(); + int i = 0; + while (iter.hasNext()) { + if (i++ > 0) { + sb.append(delimeter); + } + sb.append(iter.next()); + } + return sb.toString(); + } +}