All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.cloud.functions.invoker.gcf.JsonLogHandler Maven / Gradle / Ivy

package com.google.cloud.functions.invoker.gcf;

import java.io.PrintStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.LogRecord;

/**
 * A log handler that publishes log messages in a json format. This is StackDriver's
 * "structured
 * logging" format.
 */
public final class JsonLogHandler extends Handler {
  private static final String SOURCE_LOCATION_KEY = "\"logging.googleapis.com/sourceLocation\": ";

  private static final String DEBUG = "DEBUG";
  private static final String INFO = "INFO";
  private static final String WARNING = "WARNING";
  private static final String ERROR = "ERROR";
  private static final String DEFAULT = "DEFAULT";

  private final PrintStream out;
  private final boolean closePrintStreamOnClose;

  public JsonLogHandler(PrintStream out, boolean closePrintStreamOnClose) {
    this.out = out;
    this.closePrintStreamOnClose = closePrintStreamOnClose;
  }

  @Override
  public void publish(LogRecord record) {
    // We avoid String.format and String.join even though they would simplify the code.
    // Logging code often shows up in profiling so we want to make this fast and StringBuilder is
    // more performant.
    StringBuilder json = new StringBuilder("{");
    appendSeverity(json, record);
    appendSourceLocation(json, record);
    appendMessage(json, record); // must be last, see appendMessage
    json.append("}");
    // We must output the log all at once (should only call println once per call to publish)
    out.println(json);
  }

  private static void appendMessage(StringBuilder json, LogRecord record) {
    // This must be the last item in the JSON object, because it has no trailing comma. JSON is
    // unforgiving about commas and you can't have one just before }.
    json.append("\"message\": \"").append(escapeString(record.getMessage()));
    if (record.getThrown() != null) {
      json.append("\\n")
          .append(escapeString(getStackTraceAsString(record.getThrown())));
    }
    json.append("\"");
  }

  private static void appendSeverity(StringBuilder json, LogRecord record) {
    json.append("\"severity\": \"").append(levelToSeverity(record.getLevel())).append("\", ");
  }

  private static String levelToSeverity(Level level) {
    int intLevel = (level == null) ? 0 : level.intValue();
    switch (intLevel) {
      case 300: // FINEST
      case 400: // FINER
      case 500: // FINE
        return DEBUG;
      case 700: // CONFIG
      case 800: // INFO
        // Java's CONFIG is lower than its INFO, while Stackdriver's NOTICE is greater than its
        // INFO. So despite the similarity, we don't try to use NOTICE for CONFIG.
        return INFO;
      case 900: // WARNING
        return WARNING;
      case 1000: // SEVERE
        return ERROR;
      default:
        return DEFAULT;
    }
  }

  private static void appendSourceLocation(StringBuilder json, LogRecord record) {
    if (record.getSourceClassName() == null && record.getSourceMethodName() == null) {
      return;
    }
    List entries = new ArrayList<>();
    if (record.getSourceClassName() != null) {
      // TODO: Handle nested classes. If the source class name is com.example.Foo$Bar then the
      // source file is com/example/Foo.java, not com/example/Foo$Bar.java.
      String fileName = record.getSourceClassName().replace('.', '/') + ".java";
      entries.add("\"file\": \"" + escapeString(fileName) + "\"");
    }
    if (record.getSourceMethodName() != null) {
      entries.add("\"method\": \"" + escapeString(record.getSourceMethodName()) + "\"");
    }
    json.append(SOURCE_LOCATION_KEY).append("{").append(String.join(", ", entries)).append("}, ");
  }

  private static String escapeString(String s) {
    return s.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n").replace("\r", "\\r");
  }

  private static String getStackTraceAsString(Throwable t) {
    StringWriter stringWriter = new StringWriter();
    t.printStackTrace(new PrintWriter(stringWriter));
    return stringWriter.toString();
  }

  @Override
  public void flush() {
    out.flush();
  }

  @Override
  public void close() throws SecurityException {
    if (closePrintStreamOnClose) {
      out.close();
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy