Skip to content

Structured Logging first class support #1404

Open
@lukemauldinks

Description

@lukemauldinks

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();
        }
      }
    }
  }
}

Metadata

Metadata

Assignees

Labels

api: loggingIssues related to the googleapis/java-logging-logback API.priority: p3Desirable enhancement or fix. May not be included in next release.

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions