Skip to content
This repository was archived by the owner on Nov 7, 2022. It is now read-only.

Commit 5fb7397

Browse files
author
Paulo Janotti
authored
Add span attribute key mapping support (#9) (#536)
* Add span attribute key mapping support (#9) Many libraries and services didn't implement consistent naming for their span attribute keys, this feature provides the capability of making the attribute keys consistent for different libraries and services without the need to change their code. * PR Feedback * PR feedback * type on test Fix nil return and avoid lookup if overwritting Better syntax to avoid lookup when overwritting Support same replacement for multiple keys * Simplify handling of attribute related config
1 parent 5228f66 commit 5fb7397

File tree

9 files changed

+684
-12
lines changed

9 files changed

+684
-12
lines changed

README.md

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -277,9 +277,13 @@ agent/client health information/inventory metadata to downstream exporters.
277277

278278
### <a name="global-attributes"></a> Global Attributes
279279

280-
The collector also takes some global configurations that modify its behavior for all receivers / exporters. One of the configurations
281-
available is to add Attributes or Tags to all spans passing through this collector. These additional attributes can be configured to either overwrite
282-
attributes if they already exists on the span, or respect the original values. An example of this is provided below.
280+
The collector also takes some global configurations that modify its behavior for all receivers / exporters.
281+
282+
1. Add Attributes to all spans passing through this collector. These additional attributes can be configured to either overwrite existing keys if they already exist on the span, or respect the original values.
283+
2. The key of each attribute can also be mapped to different strings using the `key-mapping` configuration. The key matching is case sensitive.
284+
285+
An example using these configurations of this is provided below.
286+
283287
```yaml
284288
global:
285289
attributes:
@@ -290,6 +294,14 @@ global:
290294
some_int: 1234
291295
some_float: 3.14159
292296
some_bool: false
297+
key-mapping:
298+
# key-mapping is used to replace the attribute key with different keys
299+
- key: servertracer.http.responsecode
300+
replacement: http.status_code
301+
- key: servertracer.http.responsephrase
302+
replacement: http.message
303+
overwrite: true # replace attribute key even if the replacement string is already a key on the span attributes
304+
keep: true # keep the attribute with the original key
293305
```
294306

295307
### <a name="tail-sampling"></a>Intelligent Sampling

cmd/occollector/app/builder/processor_builder.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import (
1818
"time"
1919

2020
"github.com/spf13/viper"
21+
22+
"github.com/census-instrumentation/opencensus-service/processor/attributekeyprocessor"
2123
)
2224

2325
// SenderType indicates the type of sender
@@ -126,8 +128,9 @@ type QueuedSpanProcessorCfg struct {
126128
// AttributesCfg holds configuration for attributes that can be added to all spans
127129
// going through a processor.
128130
type AttributesCfg struct {
129-
Overwrite bool `mapstructure:"overwrite"`
130-
Values map[string]interface{} `mapstructure:"values"`
131+
Overwrite bool `mapstructure:"overwrite"`
132+
Values map[string]interface{} `mapstructure:"values"`
133+
KeyReplacements []attributekeyprocessor.KeyReplacement `mapstructure:"key-mapping,omitempty"`
131134
}
132135

133136
// GlobalProcessorCfg holds global configuration values that apply to all processors
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
// Copyright 2019, OpenCensus Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package builder
16+
17+
import (
18+
"testing"
19+
20+
"github.com/google/go-cmp/cmp"
21+
22+
"github.com/census-instrumentation/opencensus-service/processor/attributekeyprocessor"
23+
)
24+
25+
func TestGlobalProcessorCfg_InitFromViper(t *testing.T) {
26+
tests := []struct {
27+
name string
28+
file string
29+
want *AttributesCfg
30+
}{
31+
{
32+
name: "key_mapping",
33+
file: "./testdata/global_attributes_key_mapping.yaml",
34+
want: &AttributesCfg{
35+
KeyReplacements: []attributekeyprocessor.KeyReplacement{
36+
{
37+
Key: "servertracer.http.responsecode",
38+
NewKey: "http.status_code",
39+
},
40+
{
41+
Key: "servertracer.http.responsephrase",
42+
NewKey: "http.message",
43+
Overwrite: true,
44+
KeepOriginal: true,
45+
},
46+
},
47+
},
48+
},
49+
{
50+
name: "all_settings",
51+
file: "./testdata/global_attributes_all.yaml",
52+
want: &AttributesCfg{
53+
Overwrite: true,
54+
Values: map[string]interface{}{"some_string": "hello world"},
55+
KeyReplacements: []attributekeyprocessor.KeyReplacement{
56+
{
57+
Key: "servertracer.http.responsecode",
58+
NewKey: "http.status_code",
59+
},
60+
{
61+
Key: "servertracer.http.responsephrase",
62+
NewKey: "http.message",
63+
Overwrite: true,
64+
KeepOriginal: true,
65+
},
66+
},
67+
},
68+
},
69+
}
70+
for _, tt := range tests {
71+
t.Run(tt.name, func(t *testing.T) {
72+
v, err := loadViperFromFile(tt.file)
73+
if err != nil {
74+
t.Fatalf("Failed to load viper from test file: %v", err)
75+
}
76+
77+
cfg := NewDefaultMultiSpanProcessorCfg().InitFromViper(v)
78+
79+
got := cfg.Global.Attributes
80+
if got == nil {
81+
t.Fatalf("got nil, want non-nil")
82+
}
83+
84+
if diff := cmp.Diff(got, tt.want); diff != "" {
85+
t.Errorf("Mismatched global configuration\n-Got +Want:\n\t%s", diff)
86+
}
87+
})
88+
}
89+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
global:
2+
attributes:
3+
overwrite: true
4+
values:
5+
some_string: "hello world"
6+
key-mapping:
7+
- key: servertracer.http.responsecode
8+
replacement: http.status_code
9+
- key: servertracer.http.responsephrase
10+
replacement: http.message
11+
overwrite: true
12+
keep: true
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
global:
2+
attributes:
3+
key-mapping:
4+
- key: servertracer.http.responsecode
5+
replacement: http.status_code
6+
- key: servertracer.http.responsephrase
7+
replacement: http.message
8+
overwrite: true
9+
keep: true

cmd/occollector/app/collector/processors.go

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import (
3333
"github.com/census-instrumentation/opencensus-service/internal/collector/sampling"
3434
"github.com/census-instrumentation/opencensus-service/internal/config"
3535
"github.com/census-instrumentation/opencensus-service/processor/addattributesprocessor"
36+
"github.com/census-instrumentation/opencensus-service/processor/attributekeyprocessor"
3637
"github.com/census-instrumentation/opencensus-service/processor/multiconsumer"
3738
)
3839

@@ -310,18 +311,25 @@ func startProcessor(v *viper.Viper, logger *zap.Logger) (consumer.TraceConsumer,
310311
}
311312

312313
// Wraps processors in a single one to be connected to all enabled receivers.
314+
tp := multiconsumer.NewTraceProcessor(traceConsumers)
313315
if multiProcessorCfg.Global != nil && multiProcessorCfg.Global.Attributes != nil {
314316
logger.Info(
315317
"Found global attributes config",
316318
zap.Bool("overwrite", multiProcessorCfg.Global.Attributes.Overwrite),
317319
zap.Any("values", multiProcessorCfg.Global.Attributes.Values),
320+
zap.Any("key-mapping", multiProcessorCfg.Global.Attributes.KeyReplacements),
318321
)
319-
tp, _ := addattributesprocessor.NewTraceProcessor(
320-
multiconsumer.NewTraceProcessor(traceConsumers),
321-
addattributesprocessor.WithAttributes(multiProcessorCfg.Global.Attributes.Values),
322-
addattributesprocessor.WithOverwrite(multiProcessorCfg.Global.Attributes.Overwrite),
323-
)
324-
return tp, closeFns
322+
323+
if len(multiProcessorCfg.Global.Attributes.Values) > 0 {
324+
tp, _ = addattributesprocessor.NewTraceProcessor(
325+
tp,
326+
addattributesprocessor.WithAttributes(multiProcessorCfg.Global.Attributes.Values),
327+
addattributesprocessor.WithOverwrite(multiProcessorCfg.Global.Attributes.Overwrite),
328+
)
329+
}
330+
if len(multiProcessorCfg.Global.Attributes.KeyReplacements) > 0 {
331+
tp, _ = attributekeyprocessor.NewTraceProcessor(tp, multiProcessorCfg.Global.Attributes.KeyReplacements...)
332+
}
325333
}
326-
return multiconsumer.NewTraceProcessor(traceConsumers), closeFns
334+
return tp, closeFns
327335
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
// Copyright 2019, OpenCensus Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package collector
16+
17+
import (
18+
"reflect"
19+
"testing"
20+
21+
"github.com/spf13/viper"
22+
"go.uber.org/zap"
23+
24+
"github.com/census-instrumentation/opencensus-service/processor/addattributesprocessor"
25+
"github.com/census-instrumentation/opencensus-service/processor/attributekeyprocessor"
26+
"github.com/census-instrumentation/opencensus-service/processor/multiconsumer"
27+
"github.com/census-instrumentation/opencensus-service/processor/processortest"
28+
)
29+
30+
func Test_startProcessor(t *testing.T) {
31+
tests := []struct {
32+
name string
33+
setupViperCfg func() *viper.Viper
34+
wantExamplar func(t *testing.T) interface{}
35+
}{
36+
{
37+
name: "incomplete_global_attrib_config",
38+
setupViperCfg: func() *viper.Viper {
39+
v := viper.New()
40+
v.Set("logging-exporter", true)
41+
v.Set("global.attributes.overwrite", true)
42+
return v
43+
},
44+
wantExamplar: func(t *testing.T) interface{} {
45+
return multiconsumer.NewTraceProcessor(nil)
46+
},
47+
},
48+
{
49+
name: "global_attrib_config_values",
50+
setupViperCfg: func() *viper.Viper {
51+
v := viper.New()
52+
v.Set("logging-exporter", true)
53+
v.Set("global.attributes.values", map[string]interface{}{"foo": "bar"})
54+
return v
55+
},
56+
wantExamplar: func(t *testing.T) interface{} {
57+
nopProcessor := processortest.NewNopTraceProcessor(nil)
58+
addAttributesProcessor, err := addattributesprocessor.NewTraceProcessor(nopProcessor)
59+
if err != nil {
60+
t.Fatalf("addattributesprocessor.NewTraceProcessor() = %v", err)
61+
}
62+
return addAttributesProcessor
63+
},
64+
},
65+
{
66+
name: "global_attrib_config_key_mapping",
67+
setupViperCfg: func() *viper.Viper {
68+
v := viper.New()
69+
v.Set("logging-exporter", true)
70+
v.Set("global.attributes.key-mapping",
71+
[]map[string]interface{}{
72+
{
73+
"key": "foo",
74+
"replacement": "bar",
75+
},
76+
})
77+
return v
78+
},
79+
wantExamplar: func(t *testing.T) interface{} {
80+
nopProcessor := processortest.NewNopTraceProcessor(nil)
81+
attributeKeyProcessor, err := attributekeyprocessor.NewTraceProcessor(nopProcessor)
82+
if err != nil {
83+
t.Fatalf("attributekeyprocessor.NewTraceProcessor() = %v", err)
84+
}
85+
return attributeKeyProcessor
86+
},
87+
},
88+
}
89+
for _, tt := range tests {
90+
t.Run(tt.name, func(t *testing.T) {
91+
consumer, closeFns := startProcessor(tt.setupViperCfg(), zap.NewNop())
92+
if consumer == nil {
93+
t.Errorf("startProcessor() got nil consumer")
94+
}
95+
consumerExamplar := tt.wantExamplar(t)
96+
if reflect.TypeOf(consumer) != reflect.TypeOf(consumerExamplar) {
97+
t.Errorf("startProcessor() got consumer type %q want %q",
98+
reflect.TypeOf(consumer),
99+
reflect.TypeOf(consumerExamplar))
100+
}
101+
for _, closeFn := range closeFns {
102+
closeFn()
103+
}
104+
})
105+
}
106+
}

0 commit comments

Comments
 (0)