Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Structured Logging first class support #1404

Open
lukemauldinks opened this issue Nov 2, 2024 · 0 comments
Open

Structured Logging first class support #1404

lukemauldinks opened this issue Nov 2, 2024 · 0 comments
Assignees
Labels
api: logging Issues related to the googleapis/java-logging-logback API.

Comments

@lukemauldinks
Copy link

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 ([email protected]) - 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();
        }
      }
    }
  }
}
@product-auto-label product-auto-label bot added the api: logging Issues related to the googleapis/java-logging-logback API. label Nov 2, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
api: logging Issues related to the googleapis/java-logging-logback API.
Projects
None yet
Development

No branches or pull requests

2 participants