Skip to content

Commit

Permalink
Add support for dynamic MDC with jakarta (modern javax)
Browse files Browse the repository at this point in the history
  • Loading branch information
jhannes committed Sep 2, 2023
1 parent 994a617 commit d6d9fdd
Show file tree
Hide file tree
Showing 11 changed files with 1,235 additions and 0 deletions.
6 changes: 6 additions & 0 deletions logevents/pom.xml
Expand Up @@ -54,6 +54,12 @@
<version>3.1.0</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<version>6.0.0</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.microsoft.azure</groupId>
<artifactId>applicationinsights-core</artifactId>
Expand Down
@@ -0,0 +1,63 @@
package org.logevents.optional.jakarta;

import org.logevents.config.MdcFilter;
import org.logevents.formatters.exceptions.ExceptionFormatter;
import org.logevents.mdc.DynamicMDC;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.util.Map;
import java.util.function.Supplier;

/**
* Populates MDC or JSON format according to <a href="https://www.elastic.co/guide/en/ecs/current/">Elastic
* Common Schema</a> guidelines:
*
* <ul>
* <li><strong>url.original:</strong> The URL as the user entered it</li>
* <li><strong>http.request.method:</strong> GET, PUT, POST, DELETE etc</li>
* <li><strong>user.name:</strong> {@link HttpServletRequest#getRemoteUser()}</li>
* <li><strong>client.address:</strong> {@link HttpServletRequest#getRemoteAddr()}</li>
* <li><strong>event.time:</strong> The number of seconds since the request started</li>
* <li>
* <strong>error.{class, message, stack_trace} (only JSON, not MDC)</strong>:
* The exception in <code>"javax.servlet.error.exception"</code> (if any)
* </li>
* <li><strong>http.response.status_code</strong>: {@link HttpServletResponse#getStatus()}</li>
* <li><strong>http.response.mime_type</strong>: {@link HttpServletResponse#getContentType()}</li>
* <li><strong>http.response.redirect</strong>: "Location" response header</li>
* </ul>
*/
public class HttpServletMDC implements DynamicMDC {

private final HttpServletRequest request;
private final HttpServletResponse response;
private final long duration;

public HttpServletMDC(HttpServletRequest request, HttpServletResponse response, long duration) {
this.request = request;
this.response = response;
this.duration = duration;
}

public static Supplier<DynamicMDC> supplier(HttpServletRequest request, HttpServletResponse response) {
long start = System.currentTimeMillis();
return () -> new HttpServletMDC(request, response, System.currentTimeMillis() - start);
}

@Override
public Iterable<? extends Map.Entry<String, String>> entrySet() {
Map<String, String> result = new java.util.LinkedHashMap<>();
HttpServletResponseMDC.addMdcVariables(result, response);
HttpServletRequestMDC.addMdcVariables(result, request);
result.put("event.time", String.format("%.04f", duration / 1000.0));
return result.entrySet();
}

@Override
public void populateJsonEvent(Map<String, Object> jsonPayload, MdcFilter mdcFilter, ExceptionFormatter exceptionFormatter) {
HttpServletResponseMDC.populateJson(jsonPayload, response);
HttpServletRequestMDC.populateJson(jsonPayload, exceptionFormatter, request);
jsonPayload.put("event.time", String.format("%.04f", duration / 1000.0));
}
}
@@ -0,0 +1,87 @@
package org.logevents.optional.jakarta;

import org.logevents.config.MdcFilter;
import org.logevents.formatters.JsonLogEventFormatter;
import org.logevents.formatters.exceptions.ExceptionFormatter;
import org.logevents.mdc.DynamicMDC;
import org.logevents.mdc.DynamicMDCAdapter;
import org.logevents.util.JsonUtil;

import jakarta.servlet.ServletRequest;
import jakarta.servlet.http.HttpServletRequest;
import java.util.Map;
import java.util.function.Supplier;

/**
* Populates MDC or JSON format according to <a href="https://www.elastic.co/guide/en/ecs/current/">Elastic
* Common Schema</a> guidelines:
*
* <ul>
* <li><strong>url.original:</strong> The URL as the user entered it</li>
* <li><strong>http.request.method:</strong> GET, PUT, POST, DELETE etc</li>
* <li><strong>user.name:</strong> {@link HttpServletRequest#getRemoteUser()}</li>
* <li><strong>client.address:</strong> {@link HttpServletRequest#getRemoteAddr()}</li>
* <li><strong>event.time:</strong> The number of seconds since the request started</li>
* <li>
* <strong>error.{class, message, stack_trace} (only JSON, not MDC)</strong>:
* The exception in <code>"javax.servlet.error.exception"</code> (if any)
* </li>
* </ul>
*/
public class HttpServletRequestMDC implements DynamicMDC {

public static void addMdcVariables(Map<String, String> result, HttpServletRequest request) {
result.put("http.request.method", request.getMethod());
result.put("url.original", request.getRequestURL().toString());
result.put("user.name", request.getRemoteUser());
result.put("client.address", request.getRemoteHost());
}

public static void populateJson(Map<String, Object> jsonPayload, ExceptionFormatter exceptionFormatter, HttpServletRequest request) {
jsonPayload.put("url.original", request.getRequestURL().toString());
jsonPayload.put("user.name", request.getRemoteUser());
jsonPayload.put("client.address", request.getRemoteHost());

if (jsonPayload.containsKey("http")) {
JsonUtil.getObject(jsonPayload, "http").put("request.method", request.getMethod());
} else {
jsonPayload.put("http.request.method", request.getMethod());
}

if (request.getAttribute("javax.servlet.error.exception") instanceof Throwable) {
Throwable exception = (Throwable) request.getAttribute("javax.servlet.error.exception");
jsonPayload.put("error", JsonLogEventFormatter.toJsonObject(exception, exceptionFormatter));
}
}

private final HttpServletRequest request;
private final long duration;

private HttpServletRequestMDC(HttpServletRequest request, long duration) {
this.request = request;
this.duration = duration;
}

public static Supplier<DynamicMDC> supplier(HttpServletRequest request) {
long start = System.currentTimeMillis();
return () -> new HttpServletRequestMDC(request, System.currentTimeMillis() - start);
}

public static DynamicMDCAdapter.Cleanup put(ServletRequest request) {
return DynamicMDC.putDynamic("request", supplier((HttpServletRequest) request));
}

@Override
public Iterable<? extends Map.Entry<String, String>> entrySet() {
Map<String, String> result = new java.util.LinkedHashMap<>();
addMdcVariables(result, request);
result.put("event.time", String.format("%.04f", duration / 1000.0));
return result.entrySet();
}

@Override
public void populateJsonEvent(Map<String, Object> jsonPayload, MdcFilter mdcFilter, ExceptionFormatter exceptionFormatter) {
populateJson(jsonPayload, exceptionFormatter, request);
jsonPayload.put("event.time", String.format("%.04f", duration / 1000.0));
}
}
@@ -0,0 +1,63 @@
package org.logevents.optional.jakarta;

import org.logevents.config.MdcFilter;
import org.logevents.formatters.exceptions.ExceptionFormatter;
import org.logevents.mdc.DynamicMDC;

import jakarta.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Supplier;

/**
* Populates MDC or JSON format according to <a href="https://www.elastic.co/guide/en/ecs/current/">Elastic
* Common Schema</a> guidelines:
*
* <ul>
* <li><strong>http.response.status_code</strong>: {@link HttpServletResponse#getStatus()}</li>
* <li><strong>http.response.mime_type</strong>: {@link HttpServletResponse#getContentType()}</li>
* <li><strong>http.response.redirect</strong>: "Location" response header</li>
* </ul>
*/
public class HttpServletResponseMDC implements DynamicMDC {

public static void populateJson(Map<String, Object> jsonPayload, HttpServletResponse response) {
Map<String, Object> http = new HashMap<>();

Map<String, Object> httpResponse = new HashMap<>();
httpResponse.put("status_code", response.getStatus());
httpResponse.put("mime_type", response.getContentType());
httpResponse.put("redirect", response.getHeader("Location"));

http.put("response", httpResponse);
jsonPayload.put("http", http);
}

public static void addMdcVariables(Map<String, String> result, HttpServletResponse response) {
result.put("http.response.status_code", String.valueOf(response.getStatus()));
result.put("http.response.mime_type", response.getContentType());
result.put("http.response.redirect", response.getHeader("Location"));
}

private final HttpServletResponse response;

public HttpServletResponseMDC(HttpServletResponse response) {
this.response = response;
}

public static Supplier<DynamicMDC> supplier(HttpServletResponse response) {
return () -> new HttpServletResponseMDC(response);
}

@Override
public Iterable<? extends Map.Entry<String, String>> entrySet() {
Map<String, String> result = new java.util.LinkedHashMap<>();
addMdcVariables(result, response);
return result.entrySet();
}

@Override
public void populateJsonEvent(Map<String, Object> jsonPayload, MdcFilter mdcFilter, ExceptionFormatter exceptionFormatter) {
populateJson(jsonPayload, response);
}
}
@@ -0,0 +1,71 @@
package org.logevents.optional.jakarta;

import org.logevents.LogEventFactory;
import org.logevents.LogEventLogger;
import org.logevents.core.LogEventFilter;
import org.logevents.util.JsonUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.event.Level;

import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

public class LogEventsConfigurationServlet extends HttpServlet {

private static final Logger logger = LoggerFactory.getLogger(LogEventsConfigurationServlet.class);

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
Map<String, Object> configuration = logConfigurationToJson(LogEventFactory.getInstance());
resp.setContentType("application/json");
resp.getWriter().println(JsonUtil.toIndentedJson(configuration));
}

Map<String, Object> logConfigurationToJson(LogEventFactory logEventFactory) {
Map<String, LogEventLogger> loggers = logEventFactory.getLoggers();

List<String> loggerNames = new ArrayList<>(loggers.keySet());
Collections.sort(loggerNames);

Map<String, String> logLevels = new LinkedHashMap<>();
// TODO: null check?
logLevels.put("/", logEventFactory.getRootLogger().getOwnFilter().toString());
for (String loggerName : loggerNames) {
LogEventFilter threshold = logEventFactory.getLogger(loggerName).getOwnFilter();
logLevels.put(loggerName, threshold != null ? threshold.toString() : "<inherited>");
}

Map<String, String> observers = new LinkedHashMap<>();
observers.put("/", logEventFactory.getRootLogger().getObserver());
for (String loggerName : loggerNames) {
observers.put(loggerName, logEventFactory.getLogger(loggerName).getObserver());
}

Map<String, Object> configuration = new LinkedHashMap<>();
configuration.put("logLevels", logLevels);
configuration.put("observers", observers);
return configuration;
}

@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
setLogLevel(req.getParameter("loggerName"), req.getParameter("level"));
resp.sendRedirect(req.getContextPath() + req.getServletPath() + req.getPathInfo());
}

void setLogLevel(String loggerName, String levelName) {
Level level = levelName == null || levelName.equals("null") ? null : Level.valueOf(levelName);
logger.info("Changing log level for {} to {}", loggerName, level);

LogEventFactory.getInstance().setLevel(loggerName, level);
}

}

0 comments on commit d6d9fdd

Please sign in to comment.