128 lines
3.6 KiB
Go
128 lines
3.6 KiB
Go
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)
|
|
}
|