googleapis / googleapis/google-cloud-java

[java-logging-logback] Structured Logging first class support

未关闭
#12,007 5 条评论 2 个 reaction 已指派 1 人 已被 @cindy-peng 认领 在 GitHub 查看
api: logging priority: p3
主要语言
Java
星标
2.1k
派生
1.2k
平均合并
1 天 23 小时
30 天内合并 PR
154

描述

**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...
```java
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 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.
```java
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 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 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 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();
}
}
}
}
}
```

贡献指南

打开贡献指南

评估

这个 Issue 还没有评估数据。

把新 issue 发到你的邮箱

精选适合新手参与的 GitHub issue 摘要。