Open
Description
Is your feature request related to a problem? Please describe.
I would like for structured logging with slf4j facade to have first class support.
Describe the solution you'd like
I would like code like this to be able to send jsonPayload
with the fields orderId
, amount
, etc...
package com.example;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import net.logstash.logback.argument.StructuredArguments;
import net.logstash.logback.marker.Markers;
import java.util.HashMap;
import java.util.Map;
/** Sample REST Controller to demonstrate Stackdriver Logging. */
@RestController
public class ExampleController {
private static final Logger logger = LoggerFactory.getLogger(ExampleController.class);
@GetMapping("/log")
public String log() {
// Method 1: Using StructuredArguments
logger.info("Order processed",
StructuredArguments.kv("orderId", "12345"),
StructuredArguments.kv("amount", 99.99),
StructuredArguments.kv("currency", "USD")
);
// Method 2: Using Markers for JSON fields
Map<String, Object> fields = new HashMap<>();
fields.put("customerId", "CUS-789");
fields.put("region", "US-WEST");
fields.put("priority", 1);
logger.info(Markers.appendFields(fields), "Customer order details logged");
return "Structured logs written successfully";
}
}
Describe alternatives you've considered
I have written a helper class to work-around this but it makes my applications not be able to use the slf4j.Logger
interface.
package util
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import org.slf4j.event.Level;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
/**
* Thread-safe structured logging utility that integrates with Google Cloud Logging.
* Provides a builder pattern API for constructing log entries with MDC context.
*
* @author Luke Mauldin (luke.mauldin@kidstrong.com) - KidStrong, Inc.
*/
public class StructuredLogger {
private final Logger logger;
/**
* Creates a new StructuredLogger instance for the specified class.
*
* @param clazz The class to create the logger for
* @throws IllegalArgumentException if clazz is null
*/
public StructuredLogger(@NonNull Class<?> clazz) {
Objects.requireNonNull(clazz, "Class cannot be null");
this.logger = LoggerFactory.getLogger(clazz);
}
/**
* Creates a new log entry builder.
*
* @return A new LogBuilder instance
*/
public LogBuilder log() {
return new LogBuilder();
}
/**
* Builder class for constructing structured log entries.
*/
public class LogBuilder {
private final Map<String, String> fields;
private Level level;
private String message;
private Throwable throwable;
private LogBuilder() {
this.fields = new LinkedHashMap<>();
this.level = Level.INFO;
}
/**
* Sets the log level to INFO.
*
* @return this builder
*/
public LogBuilder info() {
this.level = Level.INFO;
return this;
}
/**
* Sets the log level to ERROR.
*
* @return this builder
*/
public LogBuilder error() {
this.level = Level.ERROR;
return this;
}
/**
* Sets the log level to WARN.
*
* @return this builder
*/
public LogBuilder warn() {
this.level = Level.WARN;
return this;
}
/**
* Sets the log level to DEBUG.
*
* @return this builder
*/
public LogBuilder debug() {
this.level = Level.DEBUG;
return this;
}
/**
* Adds a field to the log entry.
*
* @param key The field key
* @param value The field value
* @return this builder
* @throws IllegalArgumentException if key is null
*/
public LogBuilder addField(@NonNull String key, @Nullable Object value) {
Objects.requireNonNull(key, "Field key cannot be null");
fields.put(key, value != null ? value.toString() : "null");
return this;
}
/**
* Adds multiple fields to the log entry.
*
* @param fields Map of fields to add
* @return this builder
* @throws IllegalArgumentException if fields is null
*/
public LogBuilder addFields(@NonNull Map<String, Object> fields) {
Objects.requireNonNull(fields, "Fields map cannot be null");
fields.forEach(this::addField);
return this;
}
/**
* Sets the log message.
*
* @param message The log message
* @return this builder
*/
public LogBuilder message(@Nullable String message) {
this.message = message;
return this;
}
/**
* Adds an exception to the log entry.
*
* @param throwable The exception to log
* @return this builder
*/
public LogBuilder exception(@Nullable Throwable throwable) {
this.throwable = throwable;
if (throwable != null) {
addField("error_type", throwable.getClass().getName());
addField("error_message", throwable.getMessage());
}
return this;
}
/**
* Builds and writes the log entry.
*/
public void write() {
Map<String, String> oldContext = null;
try {
// Backup existing MDC context
oldContext = MDC.getCopyOfContextMap();
// Add all fields to MDC
fields.forEach(MDC::put);
// Log at the appropriate level
switch (level) {
case ERROR -> logger.error(message, throwable);
case WARN -> logger.warn(message, throwable);
case DEBUG -> logger.debug(message, throwable);
default -> logger.info(message);
}
} finally {
// Clean up MDC
fields.keySet().forEach(MDC::remove);
// Restore original MDC context
if (oldContext != null) {
MDC.setContextMap(oldContext);
} else {
MDC.clear();
}
}
}
}
}