Skip to content

Commit

Permalink
Add label checks
Browse files Browse the repository at this point in the history
- verify label key and value pairs
- limit number of labels to 64
  • Loading branch information
maguro committed Dec 19, 2024
1 parent b389eb4 commit 0bbcf21
Show file tree
Hide file tree
Showing 2 changed files with 172 additions and 11 deletions.
107 changes: 97 additions & 10 deletions labels.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,39 +16,125 @@ package gslog

import (
"context"
"log/slog"
"unicode"
)

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

func (lp LabelPair) LogValue() slog.Value {

Check failure on line 37 in labels.go

View workflow job for this annotation

GitHub Actions / run (1.22)

exported: exported method LabelPair.LogValue should have comment or be unexported
return slog.GroupValue(
slog.String("key", lp.key),
slog.String("value", lp.val))
}

// Label returns a new LabelPair from a key and a value.
// The key and value are not checked. Log records with invalid label pairs are
// not logged by Google Logging; use CheckedLabel if the pair need to be verified.
func Label(key, value string) LabelPair {
return LabelPair{valid: true, key: key, val: value}
return LabelPair{valid: true, ignore: false, key: key, val: value}
}

// CheckedLabel returns a new LabelPair from a key and a value.
// Invalid labels are ignored and not placed in the log record.
//
// The labels applied to a resource must meet the following requirements:
//
// - Each resource can have up to 64 labels.
// - Each label must be a key-value pair.
// - Keys have a minimum length of 1 character and a maximum length of 63
// characters, and cannot be empty. Values can be empty, and have a maximum
// length of 63 characters.
// - Keys and values can contain only lowercase letters, numeric characters,
// underscores, and dashes. All characters must use UTF-8 encoding, and
// international characters are allowed. Keys must start with a lowercase
// letter or international character.
func CheckedLabel(key, value string) LabelPair {
pair := LabelPair{valid: true, ignore: false, key: key, val: value}
if len(key) == 0 {

Check failure on line 66 in labels.go

View workflow job for this annotation

GitHub Actions / run (1.22)

if statements should only be cuddled with assignments used in the if statement itself
slog.Error("Label key is empty")
pair.ignore = true

Check failure on line 68 in labels.go

View workflow job for this annotation

GitHub Actions / run (1.22)

assignments should only be cuddled with other assignments
}
if len(key) > 63 {

Check failure on line 70 in labels.go

View workflow job for this annotation

GitHub Actions / run (1.22)

Magic number: 63, in <condition> detected

Check failure on line 70 in labels.go

View workflow job for this annotation

GitHub Actions / run (1.22)

if statements should only be cuddled with assignments
slog.Error("Label key greater than 63", "ignored", pair)
pair.ignore = true
}
if len(value) > 63 {

Check failure on line 74 in labels.go

View workflow job for this annotation

GitHub Actions / run (1.22)

Magic number: 63, in <condition> detected

Check failure on line 74 in labels.go

View workflow job for this annotation

GitHub Actions / run (1.22)

if statements should only be cuddled with assignments
slog.Error("Label value greater than 63", "ignored", pair)
pair.ignore = true
}
if !validLabel(key) {
slog.Error("Label key is not valid", "ignored", pair)
pair.ignore = true
}
if !validLabel(value) {
slog.Error("Label value is not valid", "ignored", pair)
pair.ignore = true
}

return pair
}

func validLabel(str string) bool {
// checked by other code
if len(str) == 0 {
return true
}

runes := []rune(str)
if !unicode.IsLower(runes[0]) {
return false
}
for _, c := range runes[1:] {
if !unicode.IsLower(c) &&
!unicode.IsDigit(c) &&
c != '_' &&
c != '-' {
return false
}
}
return true

Check failure on line 108 in labels.go

View workflow job for this annotation

GitHub Actions / run (1.22)

return with no blank line before
}

type labelsKey struct{}

type labeler func(ctx context.Context, labels map[string]string)
// labelClosure is a function closure that will insert labels into the map.
type labelClosure func(ctx context.Context, labels map[string]string)

func doNothing(context.Context, map[string]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 := labelsFrom(ctx)

return context.WithValue(ctx, labelsKey{},
labeler(func(ctx context.Context, labels map[string]string) {
parent(ctx, labels)
labelClosure(func(ctx context.Context, labels map[string]string) {
parentLabelClosure(ctx, labels)

for _, labelPair := range labelPairs {
if labelPair.ignore {
continue
}
if !labelPair.valid {
panic("invalid label passed to WithLabels()")
}
if len(labels) >= 64 {

Check failure on line 134 in labels.go

View workflow job for this annotation

GitHub Actions / run (1.22)

Magic number: 64, in <condition> detected
slog.Error("Too many labels", "ignored", labelPair)
continue

Check failure on line 136 in labels.go

View workflow job for this annotation

GitHub Actions / run (1.22)

continue with no blank line before
}

labels[labelPair.key] = labelPair.val
}
Expand All @@ -67,8 +153,9 @@ func ExtractLabels(ctx context.Context) map[string]string {
return labels
}

func labelsFrom(ctx context.Context) labeler {
v, ok := ctx.Value(labelsKey{}).(labeler)
// labelsFrom extracts the latest labelClosure from the context.
func labelsFrom(ctx context.Context) labelClosure {
v, ok := ctx.Value(labelsKey{}).(labelClosure)
if !ok {
return doNothing
}
Expand Down
76 changes: 75 additions & 1 deletion labels_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ import (
"m4o.io/gslog"
)

const (
isInvalid = true
isValid = false
tooLongValue = "a1234567890123456789012345678901234567890123456789012345678901234567890"
)

var _ = Describe("gslog labels", func() {
var ctx context.Context
BeforeEach(func() {
Expand All @@ -45,7 +51,12 @@ 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"),
gslog.CheckedLabel("", "invalid"),
gslog.CheckedLabel("Invalid", "Label"),
)
})

It("they can be extracted from the context", func() {
Expand All @@ -70,6 +81,69 @@ 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"))
})
})

DescribeTable("Passed various label pairs",
func(key, value string, isIgnored bool) {
checked := gslog.CheckedLabel(key, value)
Expect(checked.IsIgnored()).To(Equal(isIgnored))

notChecked := gslog.Label(key, value)
Expect(notChecked.IsIgnored()).To(BeFalse())
},
Entry("When a label has a key and value", "foo", "bar", isValid),
Entry("When a label has a key and value with numbers, etc.", "foo-123_456", "bar-123_456", isValid),
Entry("When a label has a one character key and value", "a", "b", isValid),
Entry("When a label has a key that starts with a lower case unicode and value", "über", "car", isValid),
Entry("When a label has a key and a value that starts with a lower case unicode", "car", "über", isValid),
Entry("When a label has a key and an empty value", "foo", "", isValid),

Entry("When a label has an empty key and value", "", "ouch", isInvalid),

Entry("When a label has key with an invalid value and value", "f*", "bar", isInvalid),
Entry("When a label has key and value with an invalid value", "foo", "b*", isInvalid),

Entry("When a label has mixed case key and value", "Foo", "bar", isInvalid),
Entry("When a label has key and mixed case value", "foo", "Bar", isInvalid),
Entry("When a label has mixed case for both key and value", "Foo", "Bar", isInvalid),

Entry("When a label has too long string key and value", tooLongValue, "bar", isInvalid),
Entry("When a label has key and a too long string value", "foo", tooLongValue, isInvalid),
Entry("When a label has too long value for both key and value", "foo", tooLongValue, isInvalid),

Entry("When a label has key that does not start with lower case and value", "Foo", "bar", isInvalid),
Entry("When a label has key that does not start with lower case and value", "3oo", "bar", isInvalid),
Entry("When a label has key that does not start with lower case and value", "_foo", "bar", isInvalid),
Entry("When a label has key that does not start with lower case and value", "-foo", "bar", isInvalid),

Entry("When a label has key and value that does not start with lower case", "foo", "Bar", isInvalid),
Entry("When a label has key and value that does not start with lower case", "foo", "8ar", isInvalid),
Entry("When a label has key and value that does not start with lower case", "foo", "_bar", isInvalid),
Entry("When a label has key and value that does not start with lower case", "foo", "-bar", isInvalid),
)
})

const (
Expand Down

0 comments on commit 0bbcf21

Please sign in to comment.