Afficher et écrire des journaux Cloud Run Functions

Écrire des journaux d'exécution

Par défaut, Cloud Run Functions inclut la journalisation d'exécution simple. Les journaux écrits dans stdout ou stderr s'affichent automatiquement dans la console Google Cloud. Pour une journalisation plus avancée, utilisez les bibliothèques clientes Cloud Logging.

Par défaut, la charge utile du journal est une simple chaîne de texte, comme illustré dans les extraits ci-dessous. La chaîne est stockée dans le champ textPayload de l'entrée de journal.

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();
});
La plupart des entrées de journal ne sont pas associées à un niveau de journalisation. Exemples :

  • Les journaux générés avec console.log(), console.info(), console.warn() ou console.error()
  • Les journaux générés directement dans stdout ou stderr

Les messages système internes ont le niveau de journalisation 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!")

  • Les journaux de sortie standard ou d'erreur standard n'ont pas de niveau de journalisation associé.
  • Les messages système internes ont le niveau de journalisation 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.
}
  • Les journaux de stdout ou stderr n'ont pas de niveau de journalisation associé.
  • Les messages système internes ont le niveau de journalisation 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!");
  }
}
  • Les journaux de stdout ou stderr n'ont pas de niveau de journalisation associé.
  • Les messages système internes ont le niveau de journalisation 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);
    }
}
  • Le texte écrit dans stdout (par exemple, via Console.WriteLine) et stderr (par exemple, via Console.Error.WriteLine) n'a pas de niveau de journalisation.
  • Les niveaux de journalisation ASP.NET Core sont mappés avec les niveaux Cloud Logging comme suit :
    • LogLevel.Trace et LogLevel.Debug map sont mappés avec DEBUG de Cloud Logging.
    • LogLevel.Information est mappé avec INFO de Cloud Logging.
    • LogLevel.Warning est mappé avec WARNING de Cloud Logging.
    • LogLevel.Error est mappé avec ERROR de Cloud Logging.
    • LogLevel.Critical est mappé avec CRITICAL de Cloud Logging.

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

Les entrées de journal ne sont pas associées à un niveau de journalisation.

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 '';
}

Écrire des journaux structurés

Les journaux texte par défaut décrits ci-dessus ne sont associés à aucun niveau de journalisation. Si vous souhaitez inclure des niveaux de journalisation ou d'autres champs spécifiques dans vos entrées de journal, vous pouvez écrire des journaux dans stdout ou stderr sous la forme d'une seule ligne de données JSON sérialisées. Cette ligne est récupérée et analysée par Cloud Run Functions, puis placée dans le champ jsonPayload au lieu de textPayload. Les extraits ci-dessous illustrent l'écriture de ces journaux structurés.

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

La journalisation structurée est disponible dans Python 3.8 et versions ultérieures.

# 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

La structure de chaque entrée de journal est fournie par un type 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)
}

Lorsqu'une structure Entry est journalisée, la méthode String est appelée pour la convertir au format JSON attendu par Cloud Logging :


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

Activez la journalisation JSON avec Logback et SLF4J en activant l'encodeur JSON Logstash dans votre configuration logback.xml.

// 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>

Traiter des champs JSON spéciaux dans des messages

Lorsque vous fournissez des données structurées sous forme de dictionnaire JSON, certains champs spéciaux sont supprimés de jsonPayload et écrits dans le champ correspondant de l'entrée LogEntry générée, comme décrit dans la documentation sur les champs spéciaux.

Par exemple, si vos données JSON incluent une propriété severity, celle-ci est supprimée de jsonPayload et apparaît en tant que propriété severity de l'entrée de journal. La propriété message est utilisée comme texte d'affichage principal de l'entrée de journal, le cas échéant.

Écrire des journaux à l'aide de bibliothèques clientes

Les bibliothèques clientes Cloud Logging offrent un autre moyen d'écrire des journaux. Ces bibliothèques vous permettent d'utiliser les mécanismes de journalisation standards de votre langage de programmation et de les intégrer à divers frameworks de journalisation compatibles. Les bibliothèques clientes simplifient également la population des champs JSON spéciaux en capturant automatiquement certaines informations et en fournissant des interfaces pour remplir les champs de manière appropriée.

Vous pouvez utiliser des bibliothèques clientes pour écrire des journaux avec l'API Cloud Logging de manière synchrone ou asynchrone. Certaines bibliothèques clientes sont également compatibles avec l'écriture de journaux structurés directement dans stdout ou stderr. Notez que si vous écrivez des journaux de manière asynchrone, l'arrêt inattendu de la fonction peut entraîner la perte d'entrées de journal. Notez également que la journalisation synchrone avec l'API Logging augmente le temps d'exécution des fonctions, car elle nécessite l'attente des appels d'API.

Afficher les journaux d'exécution

Utiliser l'outil de ligne de commande

Les journaux Cloud Run Functions sont visibles dans l'UI de Cloud Logging et via Google Cloud CLI.

Pour afficher les journaux à l'aide de la gcloud CLI, exécutez la commande gcloud functions logs read :

gcloud functions logs read --gen2

Pour afficher les journaux d'une fonction spécifique, indiquez le nom de la fonction en tant qu'argument :

gcloud functions logs read FUNCTION_NAME --gen2

Pour les fonctions dans d'autres langages, les journaux peuvent être mis en corrélation à partir de la même exécution de fonction à l'aide de l'en-tête de requête x-cloud-trace-context.

Pour obtenir la liste complète des options d'affichage des journaux, consultez la documentation sur gcloud functions logs read.

Utiliser le tableau de bord Logging

Vous pouvez également afficher les journaux d'exécution de Cloud Run Functions dans la console Google Cloud.

Utiliser l'API Logging

Les journaux d'exécution peuvent également être écrits et récupérés via l'API Cloud Logging. Les bibliothèques clientes de Cloud Logging fournissent une interface idiomatique à l'API Logging :

Node.js

Pour plus d'informations, consultez la documentation de référence sur la bibliothèque cliente 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

Pour en savoir plus, consultez la documentation de référence sur la bibliothèque cliente 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

Pour en savoir plus, consultez la documentation de référence sur la bibliothèque cliente 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

Pour en savoir plus, consultez la documentation de référence sur la bibliothèque cliente 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);
    }
}

Pour connaître les autres options de journalisation pour Java, consultez la page Journalisation Java.

Comprendre les journaux de mise à l'échelle des instances

Lorsque de nouvelles instances sont démarrées pour votre fonction, Cloud Logging inclut des entrées de journal sous le nom de journal varlog/system pour indiquer pourquoi chaque instance a été créée. L'entrée de journal suit le format suivant :

Starting new instance. Reason: REASON - DESCRIPTION

Le tableau suivant fournit une répartition des descriptions d'instance :

Motif Description
CUSTOMER_MIN_INSTANCE Instance minimale configurée par le client pour la fonction.
SCHEDULED L'instance a démarré en raison des facteurs de scaling configurés (par exemple, utilisation du processeur, débit des requêtes, etc.) et de leurs cibles.
OVERFLOW L'instance a démarré, car aucune capacité n'a été trouvée pour le trafic actuel.

Répondre aux journaux d'exécution

Vous pouvez répondre aux événements Cloud Logging en transférant leurs journaux vers une fonction Cloud Run. Pour en savoir plus, consultez la page Déclencheurs secondaires avec Cloud Logging.

Afficher les journaux des images de compilation

Vous pouvez également consulter les journaux de l'étape image de compilation du processus de déploiement. Cliquez sur le lien pour obtenir plus d'informations.