package gcp import ( "context" "fmt" "log/slog" "github.com/go-logr/logr" "go.opentelemetry.io/otel/trace" ) // wireLogMaxFieldBytes is the elision threshold for string fields in wire // payloads: GCP responses embed multi-KB blobs (Shielded-VM UEFI dbx // databases, licenses) that swamp the log line without diagnostic value. const wireLogMaxFieldBytes = 1024 // WireLogOptions controls the V(5) HTTP wire logging of the GCP SDK. type WireLogOptions struct { // FullPayloads disables field elision and logs payloads verbatim. FullPayloads bool } // 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. // // Debug records other than the compute client's api request/response are // dropped entirely: the same logger propagates into the auth library, // whose token-exchange records contain the signed JWT assertion and the // bearer access token. Warnings and errors pass through. func wireLogger(base logr.Logger, opts WireLogOptions) *slog.Logger { return slog.New(&wireFilterHandler{ inner: logr.ToSlogHandler(base.V(1)), fullPayloads: opts.FullPayloads, }) } type wireFilterHandler struct { inner slog.Handler fullPayloads bool } func (h *wireFilterHandler) Enabled(ctx context.Context, level slog.Level) bool { return h.inner.Enabled(ctx, level) } func (h *wireFilterHandler) Handle(ctx context.Context, rec slog.Record) error { if rec.Level <= slog.LevelDebug && rec.Message != "api request" && rec.Message != "api response" { return nil } // The SDK logs with the request ctx, so wire records can carry the // surrounding provider span — the one place slog's ctx-aware handlers // beat logr, and the same keys the logr enrichment uses. if sc := trace.SpanContextFromContext(ctx); sc.IsValid() { rec = rec.Clone() rec.AddAttrs( slog.String("traceID", sc.TraceID().String()), slog.String("spanID", sc.SpanID().String())) } if h.fullPayloads { return h.inner.Handle(ctx, rec) } elided := slog.NewRecord(rec.Time, rec.Level, rec.Message, rec.PC) rec.Attrs(func(a slog.Attr) bool { elided.AddAttrs(slog.Attr{Key: a.Key, Value: elideValue(a.Value)}) return true }) return h.inner.Handle(ctx, elided) } func (h *wireFilterHandler) WithAttrs(attrs []slog.Attr) slog.Handler { return &wireFilterHandler{inner: h.inner.WithAttrs(attrs), fullPayloads: h.fullPayloads} } func (h *wireFilterHandler) WithGroup(name string) slog.Handler { return &wireFilterHandler{inner: h.inner.WithGroup(name), fullPayloads: h.fullPayloads} } func elideValue(v slog.Value) slog.Value { v = v.Resolve() switch v.Kind() { case slog.KindString: if s := v.String(); len(s) > wireLogMaxFieldBytes { return slog.StringValue(elisionMarker(len(s))) } return v case slog.KindGroup: attrs := v.Group() out := make([]slog.Attr, 0, len(attrs)) for _, a := range attrs { out = append(out, slog.Attr{Key: a.Key, Value: elideValue(a.Value)}) } return slog.GroupValue(out...) case slog.KindAny: return slog.AnyValue(elideAny(v.Any())) default: return v } } func elideAny(v any) any { switch t := v.(type) { case string: if len(t) > wireLogMaxFieldBytes { return elisionMarker(len(t)) } return t case map[string]any: out := make(map[string]any, len(t)) for k, val := range t { out[k] = elideAny(val) } return out case []any: out := make([]any, len(t)) for i, val := range t { out[i] = elideAny(val) } return out default: return v } } func elisionMarker(size int) string { return fmt.Sprintf("[elided %d bytes]", size) }