diff --git a/README.md b/README.md index 362e29b..f4887f4 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,8 @@ The GCL Handler's options include a number of ways to include information from "outside" frameworks: - Labels attached to the context, via `gslog.WithLabels(ctx, ...labels)`, which - are added to the GCL entry, `logging.Entry`, `Labels` field. + are added to the GCL entry, `logging.Entry`, `Labels` field. The number of + labels is limited to 64. - [OpenTelemetry baggage](https://opentelemetry.io/docs/concepts/signals/baggage/) attached to the context which are added as attributes, `slog.Attr`, to the logging record, `slog.Record`. The baggage keys are prefixed diff --git a/handler.go b/handler.go index 97de541..49fc7d3 100644 --- a/handler.go +++ b/handler.go @@ -138,7 +138,6 @@ func (h *GcpHandler) Handle(ctx context.Context, record slog.Record) error { entry.Payload = payload2 entry.Timestamp = record.Time.UTC() entry.Severity = level.ToSeverity(record.Level) - entry.Labels = ExtractLabels(ctx) if h.addSource { addSourceLocation(&entry, &record) @@ -148,6 +147,8 @@ func (h *GcpHandler) Handle(ctx context.Context, record slog.Record) error { b(ctx, &entry, h.groups) } + labelsEntryAugmentorFrom(ctx)(ctx, &entry, h.groups) + if entry.Severity >= logging.Critical { err := h.log.LogSync(ctx, entry) if err != nil { diff --git a/labels.go b/labels.go index 93cef73..4c88fb6 100644 --- a/labels.go +++ b/labels.go @@ -16,41 +16,76 @@ package gslog import ( "context" + "log/slog" + + "cloud.google.com/go/logging" + + "m4o.io/gslog/internal/options" +) + +const ( + maxLabels = 64 ) // LabelPair represents a key-value string pair. type LabelPair struct { - valid bool - key string - val string + valid bool + ignore bool + key string + val string +} + +// IsIgnored indicates if there's something wrong with the label pair and that it +// will not be passed in the logging record. +func (lp LabelPair) IsIgnored() bool { + return lp.ignore +} + +// LogValue returns the slog.Value of the label pair. +func (lp LabelPair) LogValue() slog.Value { + return slog.GroupValue( + slog.String("key", lp.key), + slog.String("value", lp.val)) } // Label returns a new LabelPair from a key and a value. func Label(key, value string) LabelPair { - return LabelPair{valid: true, key: key, val: value} + return LabelPair{valid: true, ignore: false, key: key, val: value} } type labelsKey struct{} -type labeler func(ctx context.Context, labels map[string]string) - -func doNothing(context.Context, map[string]string) {} +func doNothing(context.Context, *logging.Entry, []string) {} // WithLabels returns a new Context with labels to be used in the GCP log // entries produced using that context. func WithLabels(ctx context.Context, labelPairs ...LabelPair) context.Context { - parent := labelsFrom(ctx) + parentLabelClosure := labelsEntryAugmentorFrom(ctx) return context.WithValue(ctx, labelsKey{}, - labeler(func(ctx context.Context, labels map[string]string) { - parent(ctx, labels) + options.EntryAugmentor(func(ctx context.Context, entry *logging.Entry, groups []string) { + parentLabelClosure(ctx, entry, groups) + + if entry.Labels == nil { + entry.Labels = make(map[string]string) + } for _, labelPair := range labelPairs { + if labelPair.ignore { + continue + } + if !labelPair.valid { panic("invalid label passed to WithLabels()") } - labels[labelPair.key] = labelPair.val + if len(entry.Labels) >= maxLabels { + slog.Error("Too many labels", "ignored", labelPair) + + continue + } + + entry.Labels[labelPair.key] = labelPair.val } }), ) @@ -59,16 +94,16 @@ func WithLabels(ctx context.Context, labelPairs ...LabelPair) context.Context { // ExtractLabels extracts labels from the ctx. These labels were associated // with the context using WithLabels. func ExtractLabels(ctx context.Context) map[string]string { - labels := make(map[string]string) - - labeler := labelsFrom(ctx) - labeler(ctx, labels) + //nolint:exhaustruct + entry := &logging.Entry{} + labelsEntryAugmentorFrom(ctx)(ctx, entry, nil) - return labels + return entry.Labels } -func labelsFrom(ctx context.Context) labeler { - v, ok := ctx.Value(labelsKey{}).(labeler) +// labelsEntryAugmentorFrom extracts the latest labelClosure from the context. +func labelsEntryAugmentorFrom(ctx context.Context) options.EntryAugmentor { + v, ok := ctx.Value(labelsKey{}).(options.EntryAugmentor) if !ok { return doNothing } diff --git a/labels_test.go b/labels_test.go index e8141ec..4d54b2f 100644 --- a/labels_test.go +++ b/labels_test.go @@ -45,7 +45,10 @@ var _ = Describe("gslog labels", func() { When("context is initialized with several labels", func() { BeforeEach(func() { - ctx = gslog.WithLabels(ctx, gslog.Label("how", "now"), gslog.Label("brown", "cow")) + ctx = gslog.WithLabels(ctx, + gslog.Label("how", "now"), + gslog.Label("brown", "cow"), + ) }) It("they can be extracted from the context", func() { @@ -70,6 +73,30 @@ var _ = Describe("gslog labels", func() { }) }) }) + + When("context is initialized with too many labels", func() { + BeforeEach(func() { + ctx = gslog.WithLabels(ctx, + gslog.Label("how", "now"), + gslog.Label("brown", "cow"), + ) + for i := 0; i < 64; i++ { + key := fmt.Sprintf("key_%06d", i) + value := fmt.Sprintf("val_%06d", i) + ctx = gslog.WithLabels(ctx, + gslog.Label(key, value), + ) + } + }) + + It("only 64 labels can be obtained from the context", func() { + labels := gslog.ExtractLabels(ctx) + + Ω(labels).Should(HaveLen(64)) + Ω(labels).Should(HaveKeyWithValue("how", "now")) + Ω(labels).Should(HaveKeyWithValue("brown", "cow")) + }) + }) }) const (