Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions cmd/tesseract/gcp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
110 changes: 83 additions & 27 deletions cmd/tesseract/gcp/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import (
"syscall"
"time"

"cloud.google.com/go/compute/metadata"
"cloud.google.com/go/logging"
"k8s.io/klog/v2"

Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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()
}
}
}
2 changes: 2 additions & 0 deletions deployment/modules/gcp/gce/tesseract/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -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}",
Expand Down
Loading