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

com.google.apphosting.runtime.JsonLogHandler Maven / Gradle / Ivy

There is a newer version: 2.0.31
Show newest version
/*
 * Copyright 2021 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.apphosting.runtime;

import static com.google.common.base.Preconditions.checkNotNull;

import com.google.apphosting.api.ApiProxy;
import com.google.apphosting.api.ApiProxy.Environment;
import com.google.common.base.Strings;
import com.google.common.base.Throwables;
import com.google.common.escape.Escaper;
import com.google.common.escape.Escapers;
import java.io.PrintStream;
import java.util.logging.Formatter;
import java.util.logging.Level;
import java.util.logging.LogRecord;
import javax.annotation.Nullable;

/** A log handler that publishes log messages in a json format. */
public final class JsonLogHandler extends LogHandler {
  private static final String TRACE_KEY = "\"logging.googleapis.com/trace\": ";
  private static final String SOURCE_LOCATION_KEY = "\"logging.googleapis.com/sourceLocation\": ";
  private static final String SPAN_KEY = "\"logging.googleapis.com/spanId\": ";
  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";

  static final Escaper ESCAPER =
      Escapers.builder()
          .addEscape('"', "\\\"")
          .addEscape('\\', "\\\\")
          .addEscape('\n', "\\n")
          .addEscape('\r', "\\r")
          .addEscape('\t', "\\t")
          .build();

  private final PrintStream out;
  private final boolean closePrintStreamOnClose;
  @Nullable private final String projectId;
  private final Formatter formatter;

  public JsonLogHandler(
      PrintStream out,
      boolean closePrintStreamOnClose,
      @Nullable String projectId,
      Formatter formatter) {
    this.out = out;
    this.closePrintStreamOnClose = closePrintStreamOnClose;
    this.projectId = projectId;
    this.formatter = checkNotNull(formatter);
  }

  @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("{");
    appendTraceId(json);
    appendSpanId(json);
    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.toString());
  }

  private static void appendSpanId(StringBuilder json) {
    Environment environment = ApiProxy.getCurrentEnvironment();
    if (environment instanceof ApiProxy.EnvironmentWithTrace) {
      ApiProxy.EnvironmentWithTrace environmentWithTrace =
          (ApiProxy.EnvironmentWithTrace) environment;
      environmentWithTrace
          .getSpanId()
          .ifPresent(id -> json.append(SPAN_KEY).append("\"").append(id).append("\", "));
    }
  }

  private void appendMessage(StringBuilder json, LogRecord record) {
    String message = formatter.formatMessage(record);
    if (message == null) {
      message = "";
    }

    // 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(ESCAPER.escape(message));
    if (record.getThrown() != null) {
      json.append("\\n")
          .append(ESCAPER.escape(Throwables.getStackTraceAsString(record.getThrown())));
    }
    json.append("\"");
  }

  private void appendTraceId(StringBuilder json) {
    if (Strings.isNullOrEmpty(projectId)) {
      return;
    }

    Environment environment = ApiProxy.getCurrentEnvironment();
    if (environment instanceof ApiProxy.EnvironmentWithTrace) {
      ApiProxy.EnvironmentWithTrace environmentWithTrace =
          (ApiProxy.EnvironmentWithTrace) environment;
      environmentWithTrace
          .getTraceId()
          .ifPresent(
              id ->
                  json.append(TRACE_KEY)
                      .append("\"projects/")
                      .append(projectId)
                      .append("/traces/")
                      .append(id)
                      .append("\", "));
    }
  }

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

  private static void appendSourceLocation(StringBuilder json, LogRecord record) {
    if (record.getSourceClassName() != null && record.getSourceMethodName() != null) {
      StackTraceElement stackFrame =
          AppLogsWriter.findStackFrame(
              record.getSourceClassName(), record.getSourceMethodName(), new Throwable());
      String function = '"' + stackFrame.getClassName() + "." + stackFrame.getMethodName() + '"';
      json.append(SOURCE_LOCATION_KEY)
          .append('{')
          .append("\"function\": ")
          .append(function)
          .append(", \"file\": \"")
          .append(stackFrame.getFileName())
          .append("\", \"line\": \"")
          .append(stackFrame.getLineNumber())
          .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;
    }
  }

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

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




© 2015 - 2025 Weber Informatics LLC | Privacy Policy