Skip to content

Commit 0e7086d

Browse files
Add Dynamic-Log-Level feature
- New package called com.sap.hcp.cf.logging.servlet.dynlog in project cf-java-logging-support-servlet contains the classes for dynamic logging. - DynamicLogLevelProcessor is called in RequestLoggingFilter and adds log level to MDC (if available in request-header) - Class DynamicLogLevelHelper in core-project provides the same mdc key for all classes of all subprojects that need to access the thread-specific log level threshold in the mdc. - Log level is provided in header field via RSA signed JWT token - The Public Key for JWT token verification has to be provided via environment variable to the application - The HTTP header name for the JWT token can be changed via environment variable - The new threshold for log levels is considered via TurboFilters when using Logback and via DynamicThresholdFilters when using log4j2 - A detailed description about how to use this feature can be found in DynamicLogLevels.md which is also referenced in the main README.md Change-Id: I8eee88d4cba7374424e040290fe67c845b5db88e
1 parent c2ba4ab commit 0e7086d

File tree

20 files changed

+709
-11
lines changed

20 files changed

+709
-11
lines changed

DynamicLogLevels.md

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# Dynamic Log Levels
2+
3+
## Summary
4+
5+
Dynamic Log Levels enable application developers to set a specific log level for
6+
just one thread executed by their application. This feature allows to understand
7+
misbehavior of a live-system in detail for a specific request, without flooding
8+
the system with an enormous number of DEBUG level log messages emitted during
9+
the processing of all other requests. This approach has the advantage that no
10+
unnecessary log messages need to be processed that might lead to quota
11+
exceedance. To avoid abuse of this feature, the dynamic log level has to be
12+
provided within an RSA-signed JWT token that also contains an expiry date.
13+
14+
## App-Configuration
15+
16+
This feature is easy to use and requires only little extra configuration of the
17+
application. Depending on the logging framework in use, few additions have to be
18+
made to the logback.xml or the log4j2.xml file as described below. Regardless of
19+
the framework, the public RSA key required to verify the JWT token's signature
20+
must be provided in the application's environment variables. Furthermore an
21+
individually chosen name for the HTTPS header field can also be specified here.
22+
23+
### Environment Variables
24+
25+
- `DYN_LOG_HEADER`: a specific header name for the log level token can be
26+
defined here. If not specified, the default value (SAP-LOG-LEVEL) is used.
27+
28+
- `DYN_LOG_LEVEL_KEY`: a public key which can be used to verify the JWT
29+
tokens that contain the dynamic log level.
30+
31+
### Logback Specific Configuration
32+
33+
In the logback.xml file, a turbofilter has to be defined by adding the following
34+
line to the configuration element:
35+
36+
```xml
37+
<turboFilter class="com.sap.hcp.cf.logback.filter.CustomLoggingTurboFilter" />
38+
```
39+
40+
### Log4j2 Specific Configuration
41+
42+
In the log4j2.xml file, add the following to the configuration element:
43+
44+
```xml
45+
<DynamicThresholdFilter key="dynamic_log_level"
46+
defaultThreshold="ERROR" onMatch="ACCEPT" onMismatch="DENY">
47+
<KeyValuePair key="TRACE" value="TRACE" />
48+
<KeyValuePair key="DEBUG" value="DEBUG" />
49+
<KeyValuePair key="INFO" value="INFO" />
50+
<KeyValuePair key="WARN" value="WARN" />
51+
<KeyValuePair key="ERROR" value="ERROR" />
52+
</DynamicThresholdFilter>
53+
```
54+
55+
## Usage
56+
57+
### What should a valid token look like
58+
59+
A valid JWT token should be signed with RS256. Its payload should contain the
60+
email of the issuer, the desired log-level, a timestamp for the time at which
61+
the token has been generated and a timestamp that represents the expiry date.
62+
The Java class [TokenCreator] can be used to create valid tokens.
63+
64+
[TokenCreator]: ./cf-java-logging-support-servlet/src/main/java/com/sap/hcp/cf/logging/servlet/dynlog/TokenCreator.java
65+
66+
#### Header
67+
68+
```json
69+
{
70+
"alg": "RS256",
71+
"typ": "JWT"
72+
}
73+
```
74+
75+
#### Payload
76+
77+
```json
78+
{
79+
"issuer": "[email protected]",
80+
"level": "TRACE",
81+
"iat": 1506016127,
82+
"exp": 1506188927
83+
}
84+
```

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,13 @@ Here are sort of the minimal configurations you'd need:
143143
</Configuration>
144144
```
145145

146+
## Dynamic Log Levels
147+
148+
This library provides the possibility to change the log-level threshold for a
149+
single thread by adding a token in the header of a request. A detailed
150+
description about how to apply this feature can be found
151+
[here](./DynamicLogLevels.md).
152+
146153
## Sample Application
147154

148155
In order to illustrate how the different features are used, this repository includes a simple application in the [./sample folder](./sample).
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.sap.hcp.cf.logging.common.helper;
2+
3+
public class DynamicLogLevelHelper {
4+
5+
/**
6+
* This is the mdc key used to set and retrieve the dynamic log level to and
7+
* from mdc
8+
*/
9+
public static final String MDC_DYNAMIC_LOG_LEVEL_KEY = "dynamic_log_level";
10+
11+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package com.sap.hcp.cf.logback.filter;
2+
3+
import org.slf4j.MDC;
4+
import org.slf4j.Marker;
5+
6+
import com.sap.hcp.cf.logging.common.helper.DynamicLogLevelHelper;
7+
8+
import ch.qos.logback.classic.Level;
9+
import ch.qos.logback.classic.Logger;
10+
import ch.qos.logback.classic.turbo.TurboFilter;
11+
import ch.qos.logback.core.spi.FilterReply;
12+
13+
public class CustomLoggingTurboFilter extends TurboFilter {
14+
15+
@Override
16+
public FilterReply decide(Marker marker, Logger logger, Level level, String format, Object[] params, Throwable t) {
17+
String logLevel = MDC.get(DynamicLogLevelHelper.MDC_DYNAMIC_LOG_LEVEL_KEY);
18+
if (logLevel == null) {
19+
return FilterReply.NEUTRAL;
20+
}
21+
if (level.isGreaterOrEqual(Level.toLevel(logLevel))) {
22+
return FilterReply.ACCEPT;
23+
}
24+
return FilterReply.DENY;
25+
}
26+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package com.sap.hcp.cf.logback.filter;
2+
3+
import static org.junit.Assert.assertEquals;
4+
import static org.mockito.Mockito.verifyZeroInteractions;
5+
6+
import org.junit.After;
7+
import org.junit.Before;
8+
import org.junit.Test;
9+
import org.junit.runner.RunWith;
10+
import org.mockito.Mock;
11+
import org.mockito.runners.MockitoJUnitRunner;
12+
import org.slf4j.MDC;
13+
import org.slf4j.Marker;
14+
15+
import com.sap.hcp.cf.logging.common.helper.DynamicLogLevelHelper;
16+
17+
import ch.qos.logback.classic.Level;
18+
import ch.qos.logback.classic.Logger;
19+
import ch.qos.logback.core.spi.FilterReply;
20+
21+
@RunWith(MockitoJUnitRunner.class)
22+
public class CustomLoggingTurboFilterTest {
23+
24+
private CustomLoggingTurboFilter filter;
25+
26+
@Mock
27+
private Marker marker;
28+
private Logger logger;
29+
private String format;
30+
@Mock
31+
private Object param;
32+
private Object[] params;
33+
@Mock
34+
private Throwable t;
35+
36+
@Before
37+
public void setup() {
38+
filter = new CustomLoggingTurboFilter();
39+
params = new Object[] { param };
40+
format = "format";
41+
}
42+
43+
@After
44+
public void teardown() {
45+
verifyZeroInteractions(marker, param, t);
46+
MDC.clear();
47+
}
48+
49+
@Test
50+
public void testNeutralCondition() {
51+
assertEquals(FilterReply.NEUTRAL, filter.decide(marker, logger, Level.TRACE, format, params, t));
52+
assertEquals(FilterReply.NEUTRAL, filter.decide(marker, logger, Level.DEBUG, format, params, t));
53+
assertEquals(FilterReply.NEUTRAL, filter.decide(marker, logger, Level.INFO, format, params, t));
54+
assertEquals(FilterReply.NEUTRAL, filter.decide(marker, logger, Level.WARN, format, params, t));
55+
assertEquals(FilterReply.NEUTRAL, filter.decide(marker, logger, Level.ERROR, format, params, t));
56+
}
57+
58+
@Test
59+
public void testAcceptCondition() {
60+
assertEquals(FilterReply.NEUTRAL, filter.decide(marker, logger, Level.ERROR, format, params, t));
61+
62+
MDC.put(DynamicLogLevelHelper.MDC_DYNAMIC_LOG_LEVEL_KEY, "TRACE");
63+
64+
assertEquals(FilterReply.ACCEPT, filter.decide(marker, logger, Level.TRACE, format, params, t));
65+
assertEquals(FilterReply.ACCEPT, filter.decide(marker, logger, Level.DEBUG, format, params, t));
66+
assertEquals(FilterReply.ACCEPT, filter.decide(marker, logger, Level.INFO, format, params, t));
67+
assertEquals(FilterReply.ACCEPT, filter.decide(marker, logger, Level.WARN, format, params, t));
68+
assertEquals(FilterReply.ACCEPT, filter.decide(marker, logger, Level.ERROR, format, params, t));
69+
}
70+
71+
@Test
72+
public void testDenyCondition() {
73+
assertEquals(FilterReply.NEUTRAL, filter.decide(marker, logger, Level.DEBUG, format, params, t));
74+
75+
MDC.put(DynamicLogLevelHelper.MDC_DYNAMIC_LOG_LEVEL_KEY, "INFO");
76+
assertEquals(FilterReply.DENY, filter.decide(marker, logger, Level.TRACE, format, params, t));
77+
assertEquals(FilterReply.DENY, filter.decide(marker, logger, Level.DEBUG, format, params, t));
78+
assertEquals(FilterReply.ACCEPT, filter.decide(marker, logger, Level.INFO, format, params, t));
79+
assertEquals(FilterReply.ACCEPT, filter.decide(marker, logger, Level.WARN, format, params, t));
80+
assertEquals(FilterReply.ACCEPT, filter.decide(marker, logger, Level.ERROR, format, params, t));
81+
}
82+
}

cf-java-logging-support-servlet/pom.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,5 +43,11 @@
4343
<version>${logback.version}</version>
4444
<scope>test</scope>
4545
</dependency>
46+
<!-- Library for token signing/verification -->
47+
<dependency>
48+
<groupId>com.auth0</groupId>
49+
<artifactId>java-jwt</artifactId>
50+
<version>3.2.0</version>
51+
</dependency>
4652
</dependencies>
4753
</project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package com.sap.hcp.cf.logging.servlet.dynlog;
2+
3+
import java.io.IOException;
4+
import java.security.NoSuchAlgorithmException;
5+
import java.security.interfaces.RSAPublicKey;
6+
import java.security.spec.InvalidKeySpecException;
7+
8+
import org.slf4j.Logger;
9+
import org.slf4j.LoggerFactory;
10+
11+
public class DynLogEnvironment {
12+
13+
private static final Logger LOGGER = LoggerFactory.getLogger(DynLogEnvironment.class);
14+
private final RSAPublicKey rsaPublicKey;
15+
private final String dynLogHeaderKey;
16+
17+
public DynLogEnvironment() {
18+
this(new Environment());
19+
}
20+
21+
DynLogEnvironment(Environment environment) {
22+
String header = environment.getVariable("DYN_LOG_HEADER");
23+
if (header != null) {
24+
dynLogHeaderKey = header;
25+
LOGGER.info("The header key used to retrieve the dynamic log level token has been set to {}", header);
26+
} else {
27+
dynLogHeaderKey = "SAP-LOG-LEVEL";
28+
LOGGER.info("The header key used to retrieve the dynamic log level token has been set to the default value: {}",
29+
dynLogHeaderKey);
30+
}
31+
PublicKeyReader publicKeyReader = new PublicKeyReader();
32+
33+
RSAPublicKey tempKey = null;
34+
try {
35+
tempKey = publicKeyReader.readPublicKey(environment);
36+
} catch (NoSuchAlgorithmException e) {
37+
LOGGER.error("Could not read RSAPublicKey from environment", e);
38+
} catch (InvalidKeySpecException e) {
39+
LOGGER.error("Could not read RSAPublicKey from environment", e);
40+
} catch (IOException e) {
41+
LOGGER.error("Could not read RSAPublicKey from environment", e);
42+
} finally {
43+
rsaPublicKey = tempKey;
44+
}
45+
}
46+
47+
public RSAPublicKey getRsaPublicKey() {
48+
return rsaPublicKey;
49+
}
50+
51+
public String getDynLogHeaderKey() {
52+
return dynLogHeaderKey;
53+
}
54+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package com.sap.hcp.cf.logging.servlet.dynlog;
2+
3+
import javax.servlet.ServletException;
4+
5+
import com.auth0.jwt.exceptions.JWTVerificationException;
6+
7+
public class DynamicLogLevelException extends ServletException {
8+
9+
private static final long serialVersionUID = 1L;
10+
11+
public DynamicLogLevelException(String message, JWTVerificationException cause) {
12+
super(message, cause);
13+
}
14+
15+
public DynamicLogLevelException(String message) {
16+
super(message);
17+
}
18+
19+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package com.sap.hcp.cf.logging.servlet.dynlog;
2+
3+
import java.util.Arrays;
4+
import java.util.List;
5+
6+
import javax.servlet.http.HttpServletRequest;
7+
8+
import org.slf4j.Logger;
9+
import org.slf4j.LoggerFactory;
10+
import org.slf4j.MDC;
11+
12+
import com.auth0.jwt.interfaces.DecodedJWT;
13+
import com.sap.hcp.cf.logging.common.helper.DynamicLogLevelHelper;
14+
15+
/**
16+
* This class provides a mechanism that reads a token from an
17+
* HTTP-request-header. If this token is provided and does contain a correct
18+
* signature, valid timestamps and a log-level-value, the log-level for the
19+
* thread triggered by this request will be changed to the provided value.
20+
*/
21+
public class DynamicLogLevelProcessor {
22+
23+
private final static Logger LOGGER = LoggerFactory.getLogger(DynamicLogLevelProcessor.class);
24+
private static final List<String> ALLOWED_DYNAMIC_LOGLEVELS = Arrays.asList("TRACE", "DEBUG", "INFO", "WARN",
25+
"ERROR");
26+
private final TokenDecoder tokenDecoder;
27+
private final DynLogEnvironment dynLogEnvironment;
28+
29+
public DynamicLogLevelProcessor(DynLogEnvironment dynLogEnvironment) {
30+
this.dynLogEnvironment = dynLogEnvironment;
31+
this.tokenDecoder = new TokenDecoder(dynLogEnvironment.getRsaPublicKey());
32+
}
33+
34+
public void copyDynamicLogLevelToMDC(HttpServletRequest httpRequest) {
35+
String logLevelToken = httpRequest.getHeader(dynLogEnvironment.getDynLogHeaderKey());
36+
if (logLevelToken == null) {
37+
return;
38+
} else {
39+
try {
40+
DecodedJWT jwt = tokenDecoder.validateAndDecodeToken(logLevelToken);
41+
String dynamicLogLevel = jwt.getClaim("level").asString();
42+
if (ALLOWED_DYNAMIC_LOGLEVELS.contains(dynamicLogLevel)) {
43+
MDC.put(DynamicLogLevelHelper.MDC_DYNAMIC_LOG_LEVEL_KEY, dynamicLogLevel);
44+
} else {
45+
throw new DynamicLogLevelException("Dynamic Log-Level [" + dynamicLogLevel +
46+
"] provided in header is not valid. Allowed Values are " +
47+
ALLOWED_DYNAMIC_LOGLEVELS.toString());
48+
}
49+
} catch (DynamicLogLevelException e) {
50+
LOGGER.error("DynamicLogLevelProcessor could not write dynamic log level to MDC", e);
51+
}
52+
}
53+
}
54+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package com.sap.hcp.cf.logging.servlet.dynlog;
2+
3+
public class Environment {
4+
5+
public String getVariable(String name) {
6+
return System.getenv(name);
7+
}
8+
}

0 commit comments

Comments
 (0)