Files
egress-proxies-operator/docs/plans/2026-08-11-1838-gcp-http-wire-logging-v5.md
2026-08-11 18:38:16 +02:00

3.9 KiB

Plan: GCP HTTP wire logging at V(5)

Created: 2026-08-11 18:38

Context

The GCP provider logs curated call summaries at V(1)/V(2), but when debugging against the real API the user wants ground truth: the actual HTTP requests and responses ("gory details") — visible at --zap-log-level=5, in the same log stream as everything else. The compute SDK already produces exactly this: cloud.google.com/go/compute@v1.65.0/apiv1/helpers.go:60,70 logs "api request"/"api response" (method, URL, headers, full JSON payloads, lazily via internallog.HTTPRequest/HTTPResponse) to an injectable *slog.Logger at slog Debug level. We inject one bridged to the operator's zap sink, level-shifted so those Debug records surface only at V(5).

Level scheme after this change: V(1) call outcomes, V(2) curated detail, V(5) raw HTTP traffic. V(3)/V(4) reserved.

Mechanism (verified in module sources)

  • option.WithLogger(*slog.Logger) exists in google.golang.org/api@v0.292.0 (option.go:529) and takes precedence over GOOGLE_SDK_GO_LOGGING_LEVEL — after this change, V(5) is the single knob for this client; document that.
  • logr.ToSlogHandler (go-logr/logr v1.4.3, already a direct dep) maps slog Debug → logr V(4), plus the base logger's V-bias. logr's own docs (sloghandler.go:180-184): slog.New(ToSlogHandler(logrV2)).Debug() ≈ V(6). So a base of .V(1) lands Debug at exactly V(5).
  • Gating is cheap: the slog handler's Enabled() consults the zap sink, so below level 5 the SDK's lazy LogValuers are never evaluated.

Implementation

internal/provider/gcp/gcp.go (only production file):

  1. New pure function:
    // wireLogger returns the slog logger handed to the SDK: its Debug-level
    // "api request"/"api response" records (slog Debug = +4 on the logr
    // scale) land at V(5) on top of the base's V(1) shift.
    func wireLogger(base logr.Logger) *slog.Logger {
        return slog.New(logr.ToSlogHandler(base.V(1)))
    }
    
  2. In New (gcp.go:96): pass it to the client — compute.NewInstancesRESTClient(ctx, option.WithLogger(wireLogger(logf.Log.WithName("gcp").WithName("http")))). Base is the process-root logf.Log (client is built once at startup; ctrl.SetLogger runs before registry.Build in cmd/main.go, so it resolves to the real zap logger).
  3. One-time notice in New: if logf.Log.V(5).Enabled(), log at Info: "GCP HTTP wire logging active — request payloads include cloud-init user-data" (the secret-leak warning our curated V(2) logging exists to avoid; at V(5) the user has explicitly opted into raw payloads).
  4. New imports: log/slog, google.golang.org/api/option (module already in go.mod as a direct dep; option package is a first-time import in the repo).

internal/provider/gcp/gcp_test.go:

  • TestWireLogger_gatesAtV5: table over funcr sink verbosities (funcr.Options{Verbosity: N}, pattern already used by captureContext): at 5 a Debug("api request", ...) through wireLogger emits (message and attrs present); at 4 it emits nothing; an Info record through the same logger lands at V(1) (sanity-check of the shift).
  • New itself stays untested by design (dials real Google endpoints — existing convention, gcp.go:86-87).

No changes to manifests, Makefile, other providers, or the reconciler. CHANGELOG entry after the user confirms it works (house convention) — this plus the two earlier pending entries (GCP V-logging, version stamp).

Verification

go test -race ./internal/provider/gcp/
go build ./... && go test ./...

Live (the real proof, needs the cluster):

# rebuild + load image, set --zap-log-level=5, restart, then:
kubectl -n egress-proxies-operator-system logs deploy/egress-proxies-operator-controller-manager -f \
  | grep -m2 'api request\|api response'   # full URL/headers/payload visible
# and at --zap-log-level=2: the same grep stays silent while V(2) lines still appear