Skip to content

Commit 0fe65e6

Browse files
MrAliasXSAM
andauthored
Comply with OpenTelemetry attributes specification (#1703)
* Add Valid method to KeyValue * Use KeyValue.Valid in attribute add on Span * Resource StringDetector errors for invalid attribute * Ignore invalid attr in NewWithAttributes The OpenTelemetry specification requires attributes conform to a standard evaluated and returned by attribute.KeyValue.Valid. To comply with the specification, Resources created from NewWithAttributes need to only contain valid attributes. This adds a check to ensure this and drops invalid attributes passed as arguments. * Add changes to changelog * Add nolint comment The attribute.Set is (possibly overly) optimized to avoid allocations. The returned value from the constructor is a value of a Set, not a pointer to the Set. A Set contains a lock value and pointer methods so passing the Set value raises the copylock go vet error. This copies the same nolint comment from the `NewSet` method this used to use. * Apply suggestions from code review Co-authored-by: Sam Xie <[email protected]> Co-authored-by: Sam Xie <[email protected]>
1 parent 8888435 commit 0fe65e6

File tree

8 files changed

+138
-26
lines changed

8 files changed

+138
-26
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
1414
- A `ForceFlush` method to the `"go.opentelemetry.io/otel/sdk/trace".TracerProvider` to flush all registered `SpanProcessor`s. (#1608)
1515
- Added `WithDefaultSampler` and `WithSpanLimits` to tracer provider. (#1633)
1616
- Jaeger exporter falls back to `resource.Default`'s `service.name` if the exported Span does not have one. (#1673)
17+
- A `Valid` method to the `"go.opentelemetry.io/otel/attribute".KeyValue` type. (#1703)
1718

1819
### Changed
1920

@@ -27,6 +28,8 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
2728
- `trace.NewSpanContext()` can be used in conjunction with the `trace.SpanContextConfig` struct to initialize a new `SpanContext` where all values are known.
2829
- Renamed the `LabelSet` method of `"go.opentelemetry.io/otel/sdk/resource".Resource` to `Set`. (#1692)
2930
- Jaeger exporter populates Jaeger's Span Process from Resource. (#1673)
31+
- `"go.opentelemetry.io/otel/sdk/resource".NewWithAttributes` will now drop any invalid attributes passed. (#1703)
32+
- `"go.opentelemetry.io/otel/sdk/resource".StringDetector` will now error if the produced attribute is invalid. (#1703)
3033

3134
### Removed
3235

attribute/kv.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@ type KeyValue struct {
2626
Value Value
2727
}
2828

29+
// Valid returns if kv is a valid OpenTelemetry attribute.
30+
func (kv KeyValue) Valid() bool {
31+
return kv.Key != "" && kv.Value.Type() != INVALID
32+
}
33+
2934
// Bool creates a new key-value pair with a passed name and a bool
3035
// value.
3136
func Bool(k string, v bool) KeyValue {

attribute/kv_test.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,3 +160,62 @@ func TestAny(t *testing.T) {
160160
}
161161
}
162162
}
163+
164+
func TestKeyValueValid(t *testing.T) {
165+
tests := []struct {
166+
desc string
167+
valid bool
168+
kv attribute.KeyValue
169+
}{
170+
{
171+
desc: "uninitialized KeyValue should be invalid",
172+
valid: false,
173+
kv: attribute.KeyValue{},
174+
},
175+
{
176+
desc: "empty key value should be invalid",
177+
valid: false,
178+
kv: attribute.Key("").Bool(true),
179+
},
180+
{
181+
desc: "INVALID value type should be invalid",
182+
valid: false,
183+
kv: attribute.KeyValue{
184+
Key: attribute.Key("valid key"),
185+
// Default type is INVALID.
186+
Value: attribute.Value{},
187+
},
188+
},
189+
{
190+
desc: "non-empty key with BOOL type Value should be valid",
191+
valid: true,
192+
kv: attribute.Bool("bool", true),
193+
},
194+
{
195+
desc: "non-empty key with INT64 type Value should be valid",
196+
valid: true,
197+
kv: attribute.Int64("int64", 0),
198+
},
199+
{
200+
desc: "non-empty key with FLOAT64 type Value should be valid",
201+
valid: true,
202+
kv: attribute.Float64("float64", 0),
203+
},
204+
{
205+
desc: "non-empty key with STRING type Value should be valid",
206+
valid: true,
207+
kv: attribute.String("string", ""),
208+
},
209+
{
210+
desc: "non-empty key with ARRAY type Value should be valid",
211+
valid: true,
212+
kv: attribute.Array("array", []int{}),
213+
},
214+
}
215+
216+
for _, test := range tests {
217+
if got, want := test.kv.Valid(), test.valid; got != want {
218+
t.Error(test.desc)
219+
}
220+
}
221+
}

sdk/resource/builtin.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,10 @@ func (sd stringDetector) Detect(ctx context.Context) (*Resource, error) {
8181
if err != nil {
8282
return nil, fmt.Errorf("%s: %w", string(sd.K), err)
8383
}
84+
a := sd.K.String(value)
85+
if !a.Valid() {
86+
return nil, fmt.Errorf("invalid attribute: %q -> %q", a.Key, a.Value.Emit())
87+
}
8488
return NewWithAttributes(sd.K.String(value)), nil
8589
}
8690

sdk/resource/builtin_test.go

Lines changed: 39 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -36,24 +36,44 @@ func TestBuiltinStringDetector(t *testing.T) {
3636
require.Nil(t, res)
3737
}
3838

39-
func TestBuiltinStringConfig(t *testing.T) {
40-
res, err := resource.New(
41-
context.Background(),
42-
resource.WithoutBuiltin(),
43-
resource.WithAttributes(attribute.String("A", "B")),
44-
resource.WithDetectors(resource.StringDetector(attribute.Key("K"), func() (string, error) {
45-
return "", fmt.Errorf("K-IS-MISSING")
46-
})),
47-
)
48-
require.Error(t, err)
49-
require.Contains(t, err.Error(), "K-IS-MISSING")
50-
require.NotNil(t, res)
51-
52-
m := map[string]string{}
53-
for _, kv := range res.Attributes() {
54-
m[string(kv.Key)] = kv.Value.Emit()
39+
func TestStringDetectorErrors(t *testing.T) {
40+
tests := []struct {
41+
desc string
42+
s resource.Detector
43+
errContains string
44+
}{
45+
{
46+
desc: "explicit error from func should be returned",
47+
s: resource.StringDetector(attribute.Key("K"), func() (string, error) {
48+
return "", fmt.Errorf("K-IS-MISSING")
49+
}),
50+
errContains: "K-IS-MISSING",
51+
},
52+
{
53+
desc: "empty key is an invalid",
54+
s: resource.StringDetector(attribute.Key(""), func() (string, error) {
55+
return "not-empty", nil
56+
}),
57+
errContains: "invalid attribute: \"\" -> \"not-empty\"",
58+
},
5559
}
56-
require.EqualValues(t, map[string]string{
57-
"A": "B",
58-
}, m)
60+
61+
for _, test := range tests {
62+
res, err := resource.New(
63+
context.Background(),
64+
resource.WithoutBuiltin(),
65+
resource.WithAttributes(attribute.String("A", "B")),
66+
resource.WithDetectors(test.s),
67+
)
68+
require.Error(t, err, test.desc)
69+
require.Contains(t, err.Error(), test.errContains)
70+
require.NotNil(t, res, "resource contains remaining valid entries")
71+
72+
m := map[string]string{}
73+
for _, kv := range res.Attributes() {
74+
m[string(kv.Key)] = kv.Value.Emit()
75+
}
76+
require.EqualValues(t, map[string]string{"A": "B"}, m)
77+
}
78+
5979
}

sdk/resource/resource.go

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,26 @@ var (
4343
}(Detect(context.Background(), defaultServiceNameDetector{}, TelemetrySDK{}))
4444
)
4545

46-
// NewWithAttributes creates a resource from a set of attributes. If there are
47-
// duplicate keys present in the list of attributes, then the last
48-
// value found for the key is preserved.
49-
func NewWithAttributes(kvs ...attribute.KeyValue) *Resource {
50-
return &Resource{
51-
attrs: attribute.NewSet(kvs...),
46+
// NewWithAttributes creates a resource from attrs. If attrs contains
47+
// duplicate keys, the last value will be used. If attrs contains any invalid
48+
// items those items will be dropped.
49+
func NewWithAttributes(attrs ...attribute.KeyValue) *Resource {
50+
if len(attrs) == 0 {
51+
return &emptyResource
5252
}
53+
54+
// Ensure attributes comply with the specification:
55+
// https://github.com/open-telemetry/opentelemetry-specification/blob/v1.0.1/specification/common/common.md#attributes
56+
s, _ := attribute.NewSetWithFiltered(attrs, func(kv attribute.KeyValue) bool {
57+
return kv.Valid()
58+
})
59+
60+
// If attrs only contains invalid entries do not allocate a new resource.
61+
if s.Len() == 0 {
62+
return &emptyResource
63+
}
64+
65+
return &Resource{s} //nolint
5366
}
5467

5568
// String implements the Stringer interface and provides a

sdk/resource/resource_test.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,14 @@ func TestString(t *testing.T) {
237237
kvs: []attribute.KeyValue{attribute.String(`A=a\,B`, `b`)},
238238
want: `A\=a\\\,B=b`,
239239
},
240+
{
241+
kvs: []attribute.KeyValue{attribute.String("", "invalid")},
242+
want: "",
243+
},
244+
{
245+
kvs: []attribute.KeyValue{attribute.String("", "invalid"), attribute.String("B", "b")},
246+
want: "B=b",
247+
},
240248
} {
241249
if got := resource.NewWithAttributes(test.kvs...).String(); got != test.want {
242250
t.Errorf("Resource(%v).String() = %q, want %q", test.kvs, got, test.want)

sdk/trace/span.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -497,7 +497,7 @@ func (s *span) copyToCappedAttributes(attributes ...attribute.KeyValue) {
497497
for _, a := range attributes {
498498
// Ensure attributes conform to the specification:
499499
// https://github.com/open-telemetry/opentelemetry-specification/blob/v1.0.1/specification/common/common.md#attributes
500-
if a.Value.Type() != attribute.INVALID && a.Key != "" {
500+
if a.Valid() {
501501
s.attributes.add(a)
502502
}
503503
}

0 commit comments

Comments
 (0)