diff --git a/cmd/tesseract/gcp/README.md b/cmd/tesseract/gcp/README.md index 0a0faa145..b89902f30 100644 --- a/cmd/tesseract/gcp/README.md +++ b/cmd/tesseract/gcp/README.md @@ -45,6 +45,28 @@ provided via the `--additional_signer` flag. The witness policy file is expected to contain a text-based description of the policy in the format described by https://git.glasklar.is/sigsum/core/sigsum-go/-/blob/main/doc/policy.md +## Logging + +TesseraCT on GCP uses two logging systems as it transitions to structured logging: + +### Standard `klog` (Legacy) +Many internal libraries and older code use `klog`. +- **Routing**: `klog` is configured to write to `stderr` (`-logtostderr`). When running in GCE/COS, the Docker daemon intercepts `stderr` because it is configured with `--log-driver=gcplogs`. +- **Expected Fields**: Because Docker's `gcplogs` driver handles the transmission, it automatically decorates logs with: + - `container`: name, id, image name, etc. + - `instance`: VM name, id, zone. + +### Structured `slog` (Recommended) +Newer code and the main server logs use `log/slog`. +- **Routing**: By default, `slog` bakes OpenTelemetry trace context and exports logs **directly to the Cloud Logging API** (bypassing `stderr`). +- **Expected Fields**: Because it bypasses `stderr` and the Docker `gcplogs` driver, it does not get automatic container/instance decoration by Docker. Instead, at startup, TesseraCT queries the GCE Metadata Server and reads flags to manually bake these fields into the default `slog` logger. You can expect: + - `message` + - `severity` + - `timestamp` + - `logging.googleapis.com/trace`, `logging.googleapis.com/spanId` (if OpenTelemetry span present) + - `container`: `name`, `imageName` (passed via Terraform flags) + - `instance`: `name`, `id`, `zone` (queried from GCE metadata server) + ## GCE VMs Custom monitoring settings need to be applied when running on GCE VMs, these are diff --git a/cmd/tesseract/gcp/main.go b/cmd/tesseract/gcp/main.go index 0600f1fee..ee21e0581 100644 --- a/cmd/tesseract/gcp/main.go +++ b/cmd/tesseract/gcp/main.go @@ -31,6 +31,7 @@ import ( "syscall" "time" + "cloud.google.com/go/compute/metadata" "cloud.google.com/go/logging" "k8s.io/klog/v2" @@ -114,6 +115,8 @@ var ( slogLevel = flag.Int("slog_level", 0, "The cut-off threshold for structured logging. Default is INFO. See https://pkg.go.dev/log/slog#Level.") slogToCloudAPI = flag.Bool("slog_to_cloud_api", true, "Export logs directly to Cloud Logging API. Required --otel_project_id to be set.") slogToStdOut = flag.Bool("slog_to_stdout", false, "Export logs to stdout.") + containerName = flag.String("container_name", "", "Name of the running container. Only used to decorate slog events.") + imageName = flag.String("image_name", "", "Name of the cached docker image. Only used to decorate slog events.") ) // nolint:staticcheck @@ -122,33 +125,8 @@ func main() { flag.Parse() ctx := context.Background() - var loggingHandlers []slog.Handler - if *slogToStdOut { - loggingHandlers = append(loggingHandlers, slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ - ReplaceAttr: logger.GCPReplaceAttr, - Level: slog.Level(*slogLevel), - })) - } - if *slogToCloudAPI { - if *otelProjectID == "" { - klog.Exitf("--otel_project_id is required when --slog_to_cloud_api is true") - } - var err error - loggingClient, err := logging.NewClient(ctx, "projects/"+*otelProjectID) - if err != nil { - klog.Exitf("Failed to create Cloud Logging client: %v", err) - } - defer func() { - if err := loggingClient.Close(); err != nil { - klog.Errorf("Failed to close Cloud Logging client: %v", err) - } - }() - loggingHandlers = append(loggingHandlers, logger.NewExporter(loggingClient.Logger("tesseract"), slog.Level(*slogLevel))) - } - - if len(loggingHandlers) > 0 { - slog.SetDefault(slog.New(logger.NewEnricher(logger.NewMultiHandler(loggingHandlers...), *otelProjectID))) - } + cleanup := initLogging(ctx) + defer cleanup() shutdownOTel := initOTel(ctx, *traceFraction, *origin, *otelProjectID) defer shutdownOTel(ctx) @@ -408,3 +386,81 @@ func notBeforeRLFromFlags() *tesseract.NotBeforeRL { } return &tesseract.NotBeforeRL{AgeThreshold: a, RateLimit: l} } + +func initLogging(ctx context.Context) func() { + var staticAttrs []any + containerMap := map[string]string{} + if *containerName != "" { + containerMap["name"] = *containerName + } + if *imageName != "" { + containerMap["imageName"] = *imageName + } + if len(containerMap) > 0 { + staticAttrs = append(staticAttrs, slog.Any("container", containerMap)) + } + + if metadata.OnGCE() { + id, err := metadata.InstanceIDWithContext(ctx) + if err != nil { + id = "unknown" + } + name, err := metadata.InstanceNameWithContext(ctx) + if err != nil { + name = "unknown" + } + zone, err := metadata.ZoneWithContext(ctx) + if err != nil { + zone = "unknown" + } + // Zone from metadata server is full path like projects/.../zones/europe-west3-c. We just want the basename. + if idx := strings.LastIndex(zone, "/"); idx != -1 { + zone = zone[idx+1:] + } + staticAttrs = append(staticAttrs, slog.Any("instance", map[string]string{ + "id": id, + "name": name, + "zone": zone, + })) + } + + var loggingHandlers []slog.Handler + if *slogToStdOut { + loggingHandlers = append(loggingHandlers, slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ + ReplaceAttr: logger.GCPReplaceAttr, + Level: slog.Level(*slogLevel), + })) + } + + var cleanup []func() + if *slogToCloudAPI { + if *otelProjectID == "" { + klog.Exitf("--otel_project_id is required when --slog_to_cloud_api is true") + } + var err error + loggingClient, err := logging.NewClient(ctx, "projects/"+*otelProjectID) + if err != nil { + klog.Exitf("Failed to create Cloud Logging client: %v", err) + } + cleanup = append(cleanup, func() { + if err := loggingClient.Close(); err != nil { + klog.Errorf("Failed to close Cloud Logging client: %v", err) + } + }) + loggingHandlers = append(loggingHandlers, logger.NewExporter(loggingClient.Logger("tesseract"), slog.Level(*slogLevel))) + } + + if len(loggingHandlers) > 0 { + l := slog.New(logger.NewEnricher(logger.NewMultiHandler(loggingHandlers...), *otelProjectID)) + if len(staticAttrs) > 0 { + l = l.With(staticAttrs...) + } + slog.SetDefault(l) + } + + return func() { + for _, f := range cleanup { + f() + } + } +} diff --git a/deployment/modules/gcp/gce/tesseract/main.tf b/deployment/modules/gcp/gce/tesseract/main.tf index c15b084a5..854547283 100644 --- a/deployment/modules/gcp/gce/tesseract/main.tf +++ b/deployment/modules/gcp/gce/tesseract/main.tf @@ -44,6 +44,8 @@ locals { "-slog_level=-4", "-slog_to_cloud_api=true", "-otel_project_id=${var.project_id}", + "-container_name=${local.container_name}", + "-image_name=${local.cached_docker_image}", "-http_endpoint=:80", "-bucket=${var.bucket}", "-spanner_db_path=${local.spanner_log_db_path}",