读取和写入 Cloud Run functions 函数日志

写入运行时日志

默认情况下,Cloud Run functions 包含简单的运行时日志记录功能。写入 stdoutstderr 的日志将自动显示在 Google Cloud 控制台中。如需了解更高级的日志记录功能,请使用 Cloud Logging 客户端库

默认情况下,日志载荷是一个简单的文本字符串,如以下代码段所示。该字符串存储在日志条目的 textPayload 字段中。

Node.js

const functions = require('@google-cloud/functions-framework');

functions.http('helloWorld', (req, res) => {
  console.log('I am a log entry!');
  console.error('I am an error!');
  res.end();
});
大多数日志条目没有关联的日志级别。其中包括:

  • 使用 console.log()console.info()console.warn()console.error() 发出的日志
  • 直接写入 stdoutstderr 的日志

内部系统消息的日志级别为 DEBUG

Python

def hello_world(data, context):
    """Background Cloud Function.
    Args:
         data (dict): The dictionary with data specific to the given event.
         context (google.cloud.functions.Context): The event metadata.
    """
    print("Hello, stdout!")

  • 标准输出或标准错误日志没有关联的日志级别。
  • 内部系统消息的日志级别为 DEBUG

Go


// Package helloworld provides a set of Cloud Functions samples.
package helloworld

import (
	"fmt"
	"log"
	"net/http"

	"github.com/GoogleCloudPlatform/functions-framework-go/functions"
)

func init() {
	functions.HTTP("HelloLogging", HelloLogging)
}

// HelloLogging logs messages.
func HelloLogging(w http.ResponseWriter, r *http.Request) {
	log.Println("This is stderr")
	fmt.Println("This is stdout")

	// Structured logging can be used to set severity levels.
	// See https://cloud.google.com/logging/docs/structured-logging.
	fmt.Println(`{"message": "This has ERROR severity", "severity": "error"}`)

	// cloud.google.com/go/logging can optionally be used for additional options.
}
  • 写入 stdoutstderr 的日志没有关联的日志级别。
  • 内部系统消息的日志级别为 DEBUG

Java


import com.google.cloud.functions.HttpFunction;
import com.google.cloud.functions.HttpRequest;
import com.google.cloud.functions.HttpResponse;
import java.io.BufferedWriter;
import java.io.IOException;
import java.util.logging.Logger;

public class LogHelloWorld implements HttpFunction {

  private static final Logger logger = Logger.getLogger(LogHelloWorld.class.getName());

  @Override
  public void service(HttpRequest request, HttpResponse response)
      throws IOException {
    System.out.println("I am a log to stdout!");
    System.err.println("I am a log to stderr!");

    logger.info("I am an info log!");
    logger.warning("I am a warning log!");

    BufferedWriter writer = response.getWriter();
    writer.write("Messages successfully logged!");
  }
}
  • 写入 stdoutstderr 的日志没有关联的日志级别。
  • 内部系统消息的日志级别为 DEBUG

C#

using Google.Cloud.Functions.Framework;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using System;
using System.Threading.Tasks;

namespace LogHelloWorld;

public class Function : IHttpFunction
{
    private readonly ILogger _logger;

    public Function(ILogger<Function> logger) =>
        _logger = logger;

    public async Task HandleAsync(HttpContext context)
    {
        Console.WriteLine("I am a log to stdout!");
        Console.Error.WriteLine("I am a log to stderr!");

        _logger.LogInformation("I am an info log!");
        _logger.LogWarning("I am a warning log!");

        await context.Response.WriteAsync("Messages successfully logged!", context.RequestAborted);
    }
}
  • 写入 stdout(例如,通过 Console.WriteLine)和 stderr(例如,通过 Console.Error.WriteLine)的文本没有日志级别。
  • ASP.NET Core 日志记录级别会映射到 Cloud Logging 级别,如下所示:
    • LogLevel.TraceLogLevel.Debug map 映射到 Cloud Logging DEBUG
    • LogLevel.Information 映射到 Cloud Logging INFO
    • LogLevel.Warning 映射到 Cloud Logging WARNING
    • LogLevel.Error 映射到 Cloud Logging ERROR
    • LogLevel.Critical 映射到 Cloud Logging CRITICAL

Ruby

require "functions_framework"

FunctionsFramework.http "log-helloworld" do |_request|
  # Any output sent to either stdout or stderr will be captured and written to
  # the function's logs.
  puts "Hello, stdout!"
  warn "Hello, stderr!"

  # Return the response body as a string.
  "Hello, world!"
end

日志条目没有关联的日志级别。

PHP


use Psr\Http\Message\ServerRequestInterface;

function helloLogging(ServerRequestInterface $request): string
{
    // Code running in Google Cloud Functions itself writes log entries to
    // Cloud Logging. (Default log severity level is INFO.)
    $log = fopen('php://stderr', 'wb');
    fwrite($log, "Log entry from fwrite().\n");

    // You can also specify a severity level explicitly using structured logs.
    // See this page for a list of log severity values:
    //   https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry#LogSeverity
    fwrite($log, json_encode([
      'message' => 'Structured log with error severity',
      'severity' => 'error'
    ]) . PHP_EOL);

    // This will log to standard error, which will appear in Cloud Logging
    error_log('error_log logs in Cloud Functions!');

    // This will log an error message and immediately terminate the function execution
    // trigger_error('fatal errors are logged!');

    // For HTTP functions, this is added to the HTTP response
    // For CloudEvent functions, this does nothing
    var_dump('var_dump goes to HTTP response for HTTP functions');

    // You can also dump variables using var_export() and forward
    // the resulting string to Cloud Logging via an fwrite() call.
    $entry = var_export('var_export output can be captured.', true);
    fwrite($log, $entry);

    // Functions must return a String or PSR-7 Response object
    return '';
}

写入结构化日志

上述默认文本日志没有关联的日志级别。如果您要在日志条目中包含日志级别或其他特定字段,您可以以单行序列化 JSON 的形式将日志写入 stdoutstderr。此行由 Cloud Run functions 函数获取并解析,然后放入 jsonPayload 字段而不是 textPayload。以下代码段演示了如何写入此类结构化日志。

Node.js


// Uncomment and populate this variable in your code:
// const project = 'The project ID of your function or Cloud Run service';

// Build structured log messages as an object.
const globalLogFields = {};

// Add log correlation to nest all log messages beneath request log in Log Viewer.
// (This only works for HTTP-based invocations where `req` is defined.)
if (typeof req !== 'undefined') {
  const traceHeader = req.header('X-Cloud-Trace-Context');
  if (traceHeader && project) {
    const [trace] = traceHeader.split('/');
    globalLogFields['logging.googleapis.com/trace'] =
      `projects/${project}/traces/${trace}`;
  }
}

// Complete a structured log entry.
const entry = Object.assign(
  {
    severity: 'NOTICE',
    message: 'This is the default display field.',
    // Log viewer accesses 'component' as 'jsonPayload.component'.
    component: 'arbitrary-property',
  },
  globalLogFields
);

// Serialize to a JSON string and output.
console.log(JSON.stringify(entry));

Python

Python 3.8 及更高版本提供结构化日志记录支持。

# Uncomment and populate this variable in your code:
# PROJECT = 'The project ID of your Cloud Run service';

# Build structured log messages as an object.
global_log_fields = {}

# Add log correlation to nest all log messages.
# This is only relevant in HTTP-based contexts, and is ignored elsewhere.
# (In particular, non-HTTP-based Cloud Functions.)
request_is_defined = "request" in globals() or "request" in locals()
if request_is_defined and request:
    trace_header = request.headers.get("X-Cloud-Trace-Context")

    if trace_header and PROJECT:
        trace = trace_header.split("/")
        global_log_fields[
            "logging.googleapis.com/trace"
        ] = f"projects/{PROJECT}/traces/{trace[0]}"

# Complete a structured log entry.
entry = dict(
    severity="NOTICE",
    message="This is the default display field.",
    # Log viewer accesses 'component' as jsonPayload.component'.
    component="arbitrary-property",
    **global_log_fields,
)

print(json.dumps(entry))

Go

每个日志条目的结构由 Entry 类型提供:


// Entry defines a log entry.
type Entry struct {
	Message  string `json:"message"`
	Severity string `json:"severity,omitempty"`
	Trace    string `json:"logging.googleapis.com/trace,omitempty"`

	// Logs Explorer allows filtering and display of this as `jsonPayload.component`.
	Component string `json:"component,omitempty"`
}

// String renders an entry structure to the JSON format expected by Cloud Logging.
func (e Entry) String() string {
	if e.Severity == "" {
		e.Severity = "INFO"
	}
	out, err := json.Marshal(e)
	if err != nil {
		log.Printf("json.Marshal: %v", err)
	}
	return string(out)
}

记录 Entry struct 时,调用 String 方法以将其编组为 Cloud Logging 预期的 JSON 格式:


func init() {
	// Disable log prefixes such as the default timestamp.
	// Prefix text prevents the message from being parsed as JSON.
	// A timestamp is added when shipping logs to Cloud Logging.
	log.SetFlags(0)
}

func indexHandler(w http.ResponseWriter, r *http.Request) {
	// Uncomment and populate this variable in your code:
	// projectID = "The project ID of your Cloud Run service"

	// Derive the traceID associated with the current request.
	var trace string
	if projectID != "" {
		traceHeader := r.Header.Get("X-Cloud-Trace-Context")
		traceParts := strings.Split(traceHeader, "/")
		if len(traceParts) > 0 && len(traceParts[0]) > 0 {
			trace = fmt.Sprintf("projects/%s/traces/%s", projectID, traceParts[0])
		}
	}

	log.Println(Entry{
		Severity:  "NOTICE",
		Message:   "This is the default display field.",
		Component: "arbitrary-property",
		Trace:     trace,
	})

	fmt.Fprintln(w, "Hello Logger!")
}

Java

通过在 logback.xml 配置中启用 Logstash JSON 编码器,以使用 LogbackSLF4J 启用 JSON 日志记录。

// Build structured log messages as an object.
Object globalLogFields = null;

// Add log correlation to nest all log messages beneath request log in Log Viewer.
// TODO(developer): delete this code if you're creating a Cloud
//                  Function and it is *NOT* triggered by HTTP.
String traceHeader = req.headers("x-cloud-trace-context");
if (traceHeader != null && project != null) {
  String trace = traceHeader.split("/")[0];
  globalLogFields =
      kv(
          "logging.googleapis.com/trace",
          String.format("projects/%s/traces/%s", project, trace));
}
// -- End log correlation code --

// Create a structured log entry using key value pairs.
// For instantiating the "logger" variable, see
// https://cloud.google.com/run/docs/logging#run_manual_logging-java
logger.error(
    "This is the default display field.",
    kv("component", "arbitrary-property"),
    kv("severity", "NOTICE"),
    globalLogFields);
<configuration>
  <appender name="jsonConsoleAppender" class="ch.qos.logback.core.ConsoleAppender">
    <encoder class="net.logstash.logback.encoder.LogstashEncoder">
      <!-- Ignore default logging fields -->
      <fieldNames>
        <timestamp>[ignore]</timestamp>
        <version>[ignore]</version>
        <logger>[ignore]</logger>
        <thread>[ignore]</thread>
        <level>[ignore]</level>
        <levelValue>[ignore]</levelValue>
      </fieldNames>
    </encoder>
  </appender>
  <root level="INFO">
    <appender-ref ref="jsonConsoleAppender"/>
  </root>
</configuration>

处理消息中的特殊 JSON 字段

如果您以 JSON 字典形式提供结构化数据,则系统会从 jsonPayload 中删除某些特殊字段,并将这些字段写入所生成 LogEntry 中的相应字段(具体说明请参阅特殊字段文档)。

例如,如果您的 JSON 包含 severity 属性,则系统会将该属性从 jsonPayload 中移除,并将其显示为日志条目的 severitymessage 属性用作日志条目的主显示文本(如果存在)。

使用客户端库写入日志

Cloud Logging 客户端库提供了写入日志的其他方法。借助这些库,您可以使用编程语言的标准日志记录机制,并与各种支持的日志记录框架集成。客户端库还会自动捕获一些信息并提供接口来适当填充字段,从而简化特殊 JSON 字段的填充过程。

您可以通过客户端库以同步或异步方式使用 Cloud Logging API 写入日志。某些客户端库还支持直接将结构化日志写入 stdoutstderr。请注意,如果您以异步方式写入日志,则意外的函数终止可能会导致日志条目丢失。另请注意,使用 Logging API 进行同步日志记录会增加函数执行时间,因为它需要等待 API 调用完成。

查看运行时日志

使用命令行工具

您可以在 Cloud Logging 界面中查看 Cloud Run functions 的日志,也可以通过 Google Cloud CLI 查看。

如需使用 gcloud CLI 查看日志,请使用 gcloud functions logs read 命令:

gcloud functions logs read --gen2

如需查看特定函数的日志,请以参数形式提供函数名称:

gcloud functions logs read FUNCTION_NAME --gen2

对于其他语言中的函数,可以使用 x-cloud-trace-context 请求标头来关联来自同一次函数执行的日志。

如需了解全部日志查看选项,请参阅 gcloud functions logs read 的文档。

使用 Logging 信息中心

您还可以在 Google Cloud 控制台中查看 Cloud Run functions 的运行时日志。

使用 Logging API

您还可以通过 Cloud Logging API 写入和检索运行时日志。Cloud Logging 客户端库提供了 Logging API 的惯用接口:

Node.js

如需了解详情,请参阅 Node.js 客户端库参考
// Imports the Google Cloud client library
const {Logging} = require('@google-cloud/logging');

// Creates a client
const logging = new Logging();

/**
 * TODO(developer): Uncomment the following line to run the code.
 */
// const logName = 'Name of the log from which to list entries, e.g. my-log';

const log = logging.log(logName);

async function printEntryMetadata() {
  // List the most recent entries for a given log
  // See https://googleapis.dev/nodejs/logging/latest/Logging.html#getEntries
  const [entries] = await log.getEntries();
  console.log('Logs:');
  entries.forEach(entry => {
    const metadata = entry.metadata;
    console.log(`${metadata.timestamp}:`, metadata[metadata.payload]);
  });
}
printEntryMetadata();

Python

如需了解详情,请参阅 Python 客户端库参考
def list_entries(logger_name):
    """Lists the most recent entries for a given logger."""
    logging_client = logging.Client()
    logger = logging_client.logger(logger_name)

    print("Listing entries for logger {}:".format(logger.name))

    for entry in logger.list_entries():
        timestamp = entry.timestamp.isoformat()
        print("* {}: {}".format(timestamp, entry.payload))

Go

如需了解详情,请参阅 Go 客户端库参考
import (
	"context"
	"fmt"
	"log"
	"time"

	"cloud.google.com/go/logging"
	"cloud.google.com/go/logging/logadmin"
	"google.golang.org/api/iterator"
)

func getEntries(projectID string) ([]*logging.Entry, error) {
	ctx := context.Background()
	adminClient, err := logadmin.NewClient(ctx, projectID)
	if err != nil {
		log.Fatalf("Failed to create logadmin client: %v", err)
	}
	defer adminClient.Close()

	var entries []*logging.Entry
	const name = "log-example"
	lastHour := time.Now().Add(-1 * time.Hour).Format(time.RFC3339)

	iter := adminClient.Entries(ctx,
		// Only get entries from the "log-example" log within the last hour.
		logadmin.Filter(fmt.Sprintf(`logName = "projects/%s/logs/%s" AND timestamp > "%s"`, projectID, name, lastHour)),
		// Get most recent entries first.
		logadmin.NewestFirst(),
	)

	// Fetch the most recent 20 entries.
	for len(entries) < 20 {
		entry, err := iter.Next()
		if err == iterator.Done {
			return entries, nil
		}
		if err != nil {
			return nil, err
		}
		entries = append(entries, entry)
	}
	return entries, nil
}

Java

如需了解详情,请参阅 Java 客户端库参考
import com.google.api.gax.paging.Page;
import com.google.cloud.logging.LogEntry;
import com.google.cloud.logging.Logging;
import com.google.cloud.logging.Logging.EntryListOption;
import com.google.cloud.logging.LoggingOptions;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.TimeZone;

public class ListLogEntries {

  public static void main(String[] args) throws Exception {
    // TODO(developer): Replace the variable value with valid log name before running the sample
    // or provide it as an argument.
    String logName = args.length > 0 ? args[0] : "test-log";

    try (Logging logging = LoggingOptions.getDefaultInstance().getService()) {

      // When composing a filter, using indexed fields, such as timestamp, resource.type, logName
      // and
      // others can help accelerate the results
      // Full list of indexed fields here:
      // https://cloud.google.com/logging/docs/view/advanced-queries#finding-quickly
      // This sample restrict the results to only last minute to minimize number of API calls
      Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
      calendar.add(Calendar.MINUTE, -1);
      DateFormat rfc3339 = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
      String logFilter =
          "logName=projects/"
              + logging.getOptions().getProjectId()
              + "/logs/"
              + logName
              + " AND timestamp>=\""
              + rfc3339.format(calendar.getTime())
              + "\"";

      // List all log entries
      Page<LogEntry> entries = logging.listLogEntries(EntryListOption.filter(logFilter));
      while (entries != null) {
        for (LogEntry logEntry : entries.iterateAll()) {
          System.out.println(logEntry);
        }
        entries = entries.getNextPage();
      }
    }
  }
}

C#

private void ListLogEntries(string logId)
{
    var client = LoggingServiceV2Client.Create();
    LogName logName = new LogName(s_projectId, logId);
    ProjectName projectName = new ProjectName(s_projectId);
    var results = client.ListLogEntries(Enumerable.Repeat(projectName, 1), $"logName={logName.ToString()}",
        "timestamp desc", callSettings: _retryAWhile);
    foreach (var row in results)
    {
        Console.WriteLine($"{row.TextPayload.Trim()}");
    }
}

Ruby

require "google/cloud/logging"

# log_name = "my_log_name"
logging = Google::Cloud::Logging.new
entries = logging.entries filter: "logName:#{log_name}",
                          max:    1000,
                          order:  "timestamp desc"

entries.each do |entry|
  puts "[#{entry.timestamp}] #{entry.log_name} #{entry.payload.inspect}"
end

PHP

use Google\Cloud\Logging\LoggingClient;

/**
 * Print the timestamp and entry for the project and logger.
 *
 * @param string $projectId The Google project ID.
 * @param string $loggerName The name of the logger.
 */
function list_entries($projectId, $loggerName)
{
    $logging = new LoggingClient(['projectId' => $projectId]);
    $loggerFullName = sprintf('projects/%s/logs/%s', $projectId, $loggerName);
    $oneDayAgo = date(\DateTime::RFC3339, strtotime('-24 hours'));
    $filter = sprintf(
        'logName = "%s" AND timestamp >= "%s"',
        $loggerFullName,
        $oneDayAgo
    );
    $options = [
        'filter' => $filter,
    ];
    $entries = $logging->entries($options);

    // Print the entries
    foreach ($entries as $entry) {
        /* @var $entry \Google\Cloud\Logging\Entry */
        $entryInfo = $entry->info();
        if (isset($entryInfo['textPayload'])) {
            $entryText = $entryInfo['textPayload'];
        } else {
            $entryPayload = [];
            foreach ($entryInfo['jsonPayload'] as $key => $value) {
                $entryPayload[] = "$key: $value";
            }
            $entryText = '{' . implode(', ', $entryPayload) . '}';
        }
        printf('%s : %s' . PHP_EOL, $entryInfo['timestamp'], $entryText);
    }
}

如需了解 Java 的额外日志记录选项,请参阅 Java 日志记录

了解实例扩缩日志

为您的函数启动新实例时,Cloud Logging 会在 varlog/system 日志名称下添加日志条目,以反映创建每个实例的原因。日志条目遵循以下格式:

Starting new instance. Reason: REASON - DESCRIPTION

下表详细介绍了实例说明:

原因 说明
CUSTOMER_MIN_INSTANCE 客户为函数配置的实例数下限
SCHEDULED 由于配置的扩缩因子(例如 CPU 利用率、请求吞吐量等)及其目标而启动实例。
OVERFLOW 由于未找到适合当前流量的现有容量,因此启动了实例。

响应运行时日志

如需响应 Cloud Logging 事件,您可以将其日志转发到 Cloud Run functions 函数。如需了解详情,请参阅使用 Cloud Logging 的第二方触发器页面。

查看构建映像日志

您还可以查看部署过程的构建映像步骤的日志。请点击链接了解详情。