Cloud Run Functions 로그 보기 및 쓰기

런타임 로그 작성

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입니다.

자바


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);
    }
}
  • 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으로 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!")
}

자바

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에서 삭제되고 대신 로그 항목의 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 클라이언트 라이브러리 참조를 확인하세요.
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);
    }
}

자바의 추가 로깅 옵션은 자바 로깅을 참조하세요.

런타임 로그에 응답

로그를 Cloud Run Functions로 전달하여 Cloud Logging 이벤트에 응답할 수 있습니다. 자세한 내용은 Cloud Logging으로 2차 트리거 페이지를 참조하세요.

빌드 이미지 로그 보기

배포 프로세스에서 빌드 이미지 단계의 로그를 확인할 수도 있습니다. 자세한 내용을 보려면 링크를 클릭하세요.