This is an automated email from the ASF dual-hosted git repository. arnold pushed a commit to branch develop in repository https://gitbox.apache.org/repos/asf/fineract.git
commit 8a55d5807ac0c1e73a28e020836bee9872fb9406 Author: Vic Romero <[email protected]> AuthorDate: Wed Jun 29 03:53:36 2022 -0500 FINERACT-1656 Correlation ID propagation and configuration --- docker-compose-postgresql.yml | 3 + docker-compose.yml | 4 ++ .../core/config/CorrelationIdConfig.java | 50 ++++++++++++++ .../core/config/FineractProperties.java | 10 +++ .../core/filters/CorrelationHeaderFilter.java | 77 +++++++++++++++++++++ .../service/MdcAdapter.java} | 39 +++++++++-- .../security/utils/LogParameterEscapeUtil.java | 4 ++ .../src/main/resources/application.properties | 6 ++ .../src/main/resources/logback-spring.xml | 1 + .../FineractCorrelationIdApiFilterTest.java | 80 ++++++++++++++++++++++ .../src/test/resources/application-test.properties | 3 + fineract-war/setenv.sh | 2 + 12 files changed, 274 insertions(+), 5 deletions(-) diff --git a/docker-compose-postgresql.yml b/docker-compose-postgresql.yml index f5da1f263..217d77f17 100644 --- a/docker-compose-postgresql.yml +++ b/docker-compose-postgresql.yml @@ -94,4 +94,7 @@ services: - FINERACT_DEFAULT_TENANTDB_IDENTIFIER=default - FINERACT_DEFAULT_TENANTDB_NAME=fineract_default - FINERACT_DEFAULT_TENANTDB_DESCRIPTION=Default Demo Tenant + - FINERACT_LOGGING_HTTP_CORRELATION_ID_ENABLED=false + - FINERACT_LOGGING_HTTP_CORRELATION_ID_HEADER_NAME=X-Correlation-ID + - CONSOLE_LOG_PATTERN=%d{yyyy-MM-dd HH:mm:ss.SSS} %thread %replace([%X{correlationId}]){'\[\]', ''} [%-5level] %class{0} - %msg%n - JAVA_TOOL_OPTIONS="-Xmx1G" diff --git a/docker-compose.yml b/docker-compose.yml index 08e0cc6b5..398e0b0d2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -90,4 +90,8 @@ services: - FINERACT_DEFAULT_TENANTDB_IDENTIFIER=default - FINERACT_DEFAULT_TENANTDB_NAME=fineract_default - FINERACT_DEFAULT_TENANTDB_DESCRIPTION=Default Demo Tenant + - FINERACT_LOGGING_HTTP_CORRELATION_ID_ENABLED=false + - FINERACT_LOGGING_HTTP_CORRELATION_ID_HEADER_NAME=X-Correlation-ID + - CONSOLE_LOG_PATTERN=%d{yyyy-MM-dd HH:mm:ss.SSS} %thread %replace([%X{correlationId}]){'\[\]', ''} [%-5level] %class{0} - %msg%n + - FINERACT_LOGGING_LEVEL=warn - JAVA_TOOL_OPTIONS="-Xmx1G" diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/CorrelationIdConfig.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/CorrelationIdConfig.java new file mode 100644 index 000000000..d1ef8f354 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/CorrelationIdConfig.java @@ -0,0 +1,50 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +package org.apache.fineract.infrastructure.core.config; + +import java.util.Arrays; +import org.apache.fineract.infrastructure.core.filters.CorrelationHeaderFilter; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.EnvironmentAware; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; + +@Configuration +@ConditionalOnProperty("fineract.logging.http.correlation-id.enabled") +public class CorrelationIdConfig implements EnvironmentAware { + + private Environment environment; + + @Override + public void setEnvironment(Environment environment) { + this.environment = environment; + } + + @Bean + public FilterRegistrationBean<CorrelationHeaderFilter> correlationHeaderFilter() { + FilterRegistrationBean<CorrelationHeaderFilter> filterRegBean = new FilterRegistrationBean<CorrelationHeaderFilter>(); + filterRegBean.setFilter(new CorrelationHeaderFilter(environment)); + filterRegBean.setUrlPatterns(Arrays.asList("/*")); + return filterRegBean; + } + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/FineractProperties.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/FineractProperties.java index 10c447990..fb888cb89 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/FineractProperties.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/FineractProperties.java @@ -34,6 +34,8 @@ public class FineractProperties { private FineractModeProperties mode; + private FineractCorrelationProperties correlation; + @Getter @Setter public static class FineractTenantProperties { @@ -62,4 +64,12 @@ public class FineractProperties { return readEnabled && !writeEnabled && !batchWorkerEnabled && !batchManagerEnabled; } } + + @Getter + @Setter + public static class FineractCorrelationProperties { + + private boolean enabled; + private String headerName; + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/filters/CorrelationHeaderFilter.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/filters/CorrelationHeaderFilter.java new file mode 100644 index 000000000..495f872a1 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/filters/CorrelationHeaderFilter.java @@ -0,0 +1,77 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +package org.apache.fineract.infrastructure.core.filters; + +import java.io.IOException; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.infrastructure.security.utils.LogParameterEscapeUtil; +import org.slf4j.MDC; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.env.Environment; +import org.springframework.web.filter.OncePerRequestFilter; + +@RequiredArgsConstructor +@Slf4j +public class CorrelationHeaderFilter extends OncePerRequestFilter { + + private String correlationIdHeader; + + public static final String correlationIdKey = "correlationId"; + + @Autowired + public CorrelationHeaderFilter(Environment env) { + correlationIdHeader = env.getRequiredProperty("fineract.logging.http.correlation-id.header-name"); + } + + @Override + protected void doFilterInternal(final HttpServletRequest request, final HttpServletResponse response, final FilterChain filterChain) + throws IOException, ServletException { + + try { + final HttpServletRequest httpServletRequest = (HttpServletRequest) request; + String currentCorrId = httpServletRequest.getHeader(correlationIdHeader); + log.debug("Found correlationId in Header : {}", LogParameterEscapeUtil.escapeLogMDCParameter(currentCorrId)); + MDC.put(correlationIdKey, currentCorrId); + filterChain.doFilter(request, response); + } finally { + MDC.remove(correlationIdKey); + } + } + + public static String getCurrentValue() { + return MDC.get(correlationIdKey); + } + + @Override + protected boolean isAsyncDispatch(final HttpServletRequest request) { + return false; + } + + @Override + protected boolean shouldNotFilterErrorDispatch() { + return false; + } + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/utils/LogParameterEscapeUtil.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/service/MdcAdapter.java similarity index 52% copy from fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/utils/LogParameterEscapeUtil.java copy to fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/service/MdcAdapter.java index 1e249f4f8..241aefdac 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/utils/LogParameterEscapeUtil.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/service/MdcAdapter.java @@ -16,13 +16,42 @@ * specific language governing permissions and limitations * under the License. */ -package org.apache.fineract.infrastructure.security.utils; -public final class LogParameterEscapeUtil { +package org.apache.fineract.infrastructure.core.service; - private LogParameterEscapeUtil() {} +import java.util.Map; +import org.slf4j.MDC; +import org.slf4j.spi.MDCAdapter; - public static String escapeLogParameter(String logParameter) { - return logParameter.replaceAll("[\n\r\t]", "_"); +public class MdcAdapter implements MDCAdapter { + + @Override + public void put(String key, String val) { + MDC.put(key, val); + } + + @Override + public String get(String key) { + return MDC.get(key); + } + + @Override + public void remove(String key) { + MDC.remove(key); + } + + @Override + public void clear() { + MDC.clear(); + } + + @Override + public Map<String, String> getCopyOfContextMap() { + return MDC.getCopyOfContextMap(); + } + + @Override + public void setContextMap(Map<String, String> map) { + MDC.setContextMap(map); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/utils/LogParameterEscapeUtil.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/utils/LogParameterEscapeUtil.java index 1e249f4f8..8eb145f80 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/utils/LogParameterEscapeUtil.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/utils/LogParameterEscapeUtil.java @@ -25,4 +25,8 @@ public final class LogParameterEscapeUtil { public static String escapeLogParameter(String logParameter) { return logParameter.replaceAll("[\n\r\t]", "_"); } + + public static String escapeLogMDCParameter(String logParameter) { + return logParameter.replaceAll("[\r\n]", ""); + } } diff --git a/fineract-provider/src/main/resources/application.properties b/fineract-provider/src/main/resources/application.properties index e939a8642..3bf753e0a 100644 --- a/fineract-provider/src/main/resources/application.properties +++ b/fineract-provider/src/main/resources/application.properties @@ -40,6 +40,12 @@ fineract.mode.write-enabled=${FINERACT_MODE_WRITE_ENABLED:true} fineract.mode.batch-worker-enabled=${FINERACT_MODE_BATCH_WORKER_ENABLED:true} fineract.mode.batch-manager-enabled=${FINERACT_MODE_BATCH_MANAGER_ENABLED:true} +fineract.logging.http.correlation-id.enabled=${FINERACT_LOGGING_HTTP_CORRELATION_ID_ENABLED:false} +fineract.logging.http.correlation-id.header-name=${FINERACT_LOGGING_HTTP_CORRELATION_ID_HEADER_NAME:X-Correlation-ID} + +# Logging pattern for the console +logging.pattern.console=${CONSOLE_LOG_PATTERN:%d{yyyy-MM-dd HH\:mm\:ss.SSS} %thread %replace([%X{correlationId}]){'\\[\\]', ''} [%-5level] %class{0} - %msg%n} + management.health.jms.enabled=false # FINERACT 1296 diff --git a/fineract-provider/src/main/resources/logback-spring.xml b/fineract-provider/src/main/resources/logback-spring.xml index a24143495..8ede0e3e3 100644 --- a/fineract-provider/src/main/resources/logback-spring.xml +++ b/fineract-provider/src/main/resources/logback-spring.xml @@ -39,5 +39,6 @@ <!-- But these three INFO are still handy ;-) just to see when it's up and running --> <logger name="org.springframework.boot.web.embedded.tomcat.TomcatWebServer" level="info" /> <logger name="org.apache.fineract.ServerApplication" level="info" /> + <logger name="org.apache.fineract" level="${FINERACT_LOGGING_LEVEL:-INFO}" /> <logger name="liquibase" level="info" /> </configuration> diff --git a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/core/filters/FineractCorrelationIdApiFilterTest.java b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/core/filters/FineractCorrelationIdApiFilterTest.java new file mode 100644 index 000000000..b042b5928 --- /dev/null +++ b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/core/filters/FineractCorrelationIdApiFilterTest.java @@ -0,0 +1,80 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +package org.apache.fineract.infrastructure.core.filters; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.verify; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.util.UUID; +import org.apache.fineract.infrastructure.core.service.MdcAdapter; +import org.junit.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; +import org.slf4j.MDC; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.SpyBean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.web.servlet.MockMvc; + +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@ContextConfiguration(classes = { Configuration.class }) +@WebMvcTest +class FineractCorrelationIdApiFilterTest { + + @SpyBean + private MdcAdapter mdc; + + @Autowired + private MockMvc mockMvc; + + @ParameterizedTest + @ValueSource(strings = { "/fineract-provider/api/v1/loans", "/fineract-provider/api/v1/loans" }) + void shouldGet200IfXCorrelationIdHeaderIsPresentAndRequestIsForV1Path(String url) throws Exception { + String correlationId = UUID.randomUUID().toString(); + mockMvc.perform(get(url).header(CorrelationHeaderFilter.correlationIdKey, correlationId)).andExpect(status().isOk()) + .andExpect(header().string(CorrelationHeaderFilter.correlationIdKey, correlationId)); + + verify(mdc).remove(CorrelationHeaderFilter.correlationIdKey); + } + + @ParameterizedTest + @ValueSource(strings = { "/fineract-provider/api/v1/loans", "/fineract-provider/api/v1/loans" }) + void shouldGet400IfXCorrelationIdHeaderIsNotPresentAndRequestIsForV1Path(String url) throws Exception { + mockMvc.perform(get(url)).andExpect(status().isBadRequest()) + .andExpect(header().doesNotExist(CorrelationHeaderFilter.correlationIdKey)); + } + + @Test + void shouldReturnCurrentCorrelationIdFromMDC() { + MDC.put(CorrelationHeaderFilter.correlationIdKey, "1"); + assertThat(CorrelationHeaderFilter.getCurrentValue()).isEqualTo("1"); + } + +} diff --git a/fineract-provider/src/test/resources/application-test.properties b/fineract-provider/src/test/resources/application-test.properties index 58981db96..54deaac7d 100644 --- a/fineract-provider/src/test/resources/application-test.properties +++ b/fineract-provider/src/test/resources/application-test.properties @@ -37,6 +37,9 @@ fineract.mode.read-enabled=true fineract.mode.write-enabled=true fineract.mode.batch-enabled=true +fineract.logging.http.correlation-id.enabled=false +fineract.logging.http.correlation-id.header-name=X-Correlation-ID + management.health.jms.enabled=false # FINERACT 1296 diff --git a/fineract-war/setenv.sh b/fineract-war/setenv.sh index 5d69b5aba..531936710 100644 --- a/fineract-war/setenv.sh +++ b/fineract-war/setenv.sh @@ -53,3 +53,5 @@ export FINERACT_DEFAULT_TENANTDB_TIMEZONE="Asia/Kolkata" export FINERACT_DEFAULT_TENANTDB_IDENTIFIER="default" export FINERACT_DEFAULT_TENANTDB_NAME="fineract_default" export FINERACT_DEFAULT_TENANTDB_DESCRIPTION="Default Demo Tenant" +export FINERACT_LOGGING_HTTP_CORRELATION_ID_ENABLED="false" +export FINERACT_LOGGING_HTTP_CORRELATION_ID_HEADER_NAME="X-Correlation-ID"
