Skip to content

Commit

Permalink
Add label checks
Browse files Browse the repository at this point in the history
- limit number of labels to 64
  • Loading branch information
maguro committed Dec 20, 2024
1 parent b389eb4 commit ae758ff
Show file tree
Hide file tree
Showing 4 changed files with 95 additions and 37 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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 {
Expand Down
73 changes: 49 additions & 24 deletions labels.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,59 +16,84 @@ 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
}
}),
)
}

// 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)

return 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
}
Expand Down
53 changes: 42 additions & 11 deletions labels_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"fmt"
"testing"

"cloud.google.com/go/logging"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"

Expand All @@ -38,22 +39,26 @@ var _ = Describe("gslog labels", func() {

It("should panic when extracting from the context", func() {
Ω(func() {
gslog.ExtractLabels(ctx)
gslog.LabelsEntryAugmentorFrom(ctx)(ctx, &logging.Entry{}, nil)
}).Should(PanicWith("invalid label passed to WithLabels()"))
})
})

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() {
labels := gslog.ExtractLabels(ctx)
entry := &logging.Entry{}
gslog.LabelsEntryAugmentorFrom(ctx)(ctx, entry, nil)

Ω(labels).Should(HaveLen(2))
Ω(labels).Should(HaveKeyWithValue("how", "now"))
Ω(labels).Should(HaveKeyWithValue("brown", "cow"))
Ω(entry.Labels).Should(HaveLen(2))
Ω(entry.Labels).Should(HaveKeyWithValue("how", "now"))
Ω(entry.Labels).Should(HaveKeyWithValue("brown", "cow"))
})

Context("and a label overridden", func() {
Expand All @@ -62,14 +67,40 @@ var _ = Describe("gslog labels", func() {
})

It("the overrides can be extracted from the context", func() {
labels := gslog.ExtractLabels(ctx)
entry := &logging.Entry{}
gslog.LabelsEntryAugmentorFrom(ctx)(ctx, entry, nil)

Ω(labels).Should(HaveLen(2))
Ω(labels).Should(HaveKeyWithValue("how", "now"))
Ω(labels).Should(HaveKeyWithValue("brown", "cat"))
Ω(entry.Labels).Should(HaveLen(2))
Ω(entry.Labels).Should(HaveKeyWithValue("how", "now"))
Ω(entry.Labels).Should(HaveKeyWithValue("brown", "cat"))
})
})
})

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() {
entry := &logging.Entry{}
gslog.LabelsEntryAugmentorFrom(ctx)(ctx, entry, nil)

Ω(entry.Labels).Should(HaveLen(64))
Ω(entry.Labels).Should(HaveKeyWithValue("how", "now"))
Ω(entry.Labels).Should(HaveKeyWithValue("brown", "cow"))
})
})
})

const (
Expand Down Expand Up @@ -112,5 +143,5 @@ func init() {
}

func BenchmarkExtractLabels(b *testing.B) {
gslog.ExtractLabels(ctx)
gslog.LabelsEntryAugmentorFrom(ctx)(ctx, &logging.Entry{}, nil)
}

0 comments on commit ae758ff

Please sign in to comment.