Cloud Run 関数ログの表示と書き込み

ランタイムログの書き込み

Cloud Run functions には、デフォルトで簡単なランタイムのロギング機能が含まれています。stdout または stderr に書き込まれたログは、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() を使用して出力されたログ
  • stdout または stderr に直接書き込まれたログ

内部システム メッセージのログレベルは 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.
}
  • stdout または stderr に出力されるログにはログレベルが関連付けられていません。
  • 内部システム メッセージのログレベルは 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!");
  }
}
  • stdout または stderr に出力されるログにはログレベルが関連付けられていません。
  • 内部システム メッセージのログレベルは 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);
    }
}
  • stdoutConsole.WriteLine など)や stderrConsole.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 形式の 1 行でログを stdout または stderr に書き込むことができます。この行は Cloud Run functions によって取得され、解析された後、textPayload ではなく jsonPayload フィールドに配置されます。以下のスニペットは、このような構造化ログを書き込む方法を示しています。

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 構造体がログに記録されると、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 Encoder を有効にして、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 から削除され、代わりにログエントリの severity として表示されます。message プロパティが存在する場合は、ログエントリのメイン表示テキストとして使用されます。

クライアント ライブラリを使用したログの書き込み

Cloud Logging クライアント ライブラリを使用すると、別の方法でログを書き込むことができます。これらのライブラリを使用すると、プログラミング言語の標準ロギング メカニズムを使用して、サポートされているさまざまなロギング フレームワークと統合できます。また、クライアント ライブラリでは、一部の情報を自動的に取得し、フィールドに入力するためのインターフェースが提供されるため、特別な JSON フィールドを簡単に入力できます。

クライアント ライブラリを使用すると、Cloud Logging API によって同期または非同期でログを書き込むことができます。一部のクライアント ライブラリでは、構造化ログを stdout または stderr に直接書き込むことができます。ログを非同期で書き込むと、関数が予期せず終了し、ログエントリが失われる可能性があります。また、Logging API を使用した同期ロギングでは、API 呼び出しの完了を待つ必要があるため、関数の実行時間が長くなります。

ランタイムログの表示

コマンドライン ツールの使用

Cloud Run functions のログは、Cloud Logging UI または 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 関数に転送します。詳細については、Cloud Logging でのセカンド パーティ トリガーページをご覧ください。

ビルドイメージのログの表示

デプロイ プロセスのビルドイメージのステップのログも確認できます。リンクをクリックして詳細をご確認ください。