Skip to content

Commit 4e7c2dd

Browse files
committed
[receiver/k8seventsreceiver] add extraction rules for labels and annotations
Inspired by k8sattributesprocessor introduces the similar configuration syntax to support labels and annotations extraction from event
1 parent ec1e506 commit 4e7c2dd

File tree

15 files changed

+728
-5
lines changed

15 files changed

+728
-5
lines changed
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Use this changelog template to create an entry for release notes.
2+
3+
# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
4+
change_type: enchancement
5+
6+
# The name of the component, or a single word describing the area of concern, (e.g. filelogreceiver)
7+
component: k8seventsreceiver
8+
9+
# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
10+
note: Add labels and annotations extraction from event
11+
12+
# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists.
13+
issues: []
14+
15+
# (Optional) One or more lines of additional information to render under the primary note.
16+
# These lines will be padded with 2 spaces and then inserted directly into the document.
17+
# Use pipe (|) for multiline entries.
18+
subtext:
19+
20+
# If your change doesn't affect end users or the exported elements of any package,
21+
# you should instead start your pull request title with [chore] or use the "Skip Changelog" label.
22+
# Optional: The change log or logs in which this entry should be included.
23+
# e.g. '[user]' or '[user, api]'
24+
# Include 'user' if the change is relevant to end users.
25+
# Include 'api' if there is a change to a library API.
26+
# Default: '[user]'
27+
change_logs: ["user"]

receiver/k8seventsreceiver/README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,26 @@ data:
8181
EOF
8282
```
8383

84+
## Extracting attributes from event labels and annotations
85+
86+
The k8seventsreceiver can also set resource attributes from k8s labels and annotations of event.
87+
This config represents a list of annotations/labels that are extracted from event and added to log.
88+
Each item is specified as a config of an optional tag_name (representing the tag name to tag the spans with),
89+
key (representing the key used to extract value) or key_regex (representing the key regex used to extract the value).
90+
91+
A few examples to use this config are as follows:
92+
93+
```yaml
94+
extract:
95+
annotations: # same can be applied for labels
96+
- tag_name: a1 # extracts value of annotation from event with key `annotation-one` and inserts it as a tag with key `a1`
97+
key: annotation-one
98+
- tag_name: k8s.event.annotations.$1 # extracts value of annotation from event with key matching the regex
99+
key_regex: topology.kubernetes.io/(.*)
100+
labels:
101+
- key: label1 # extracts value of label from event with key `label1` and inserts it as a tag with key `k8s.event.labels.label1`
102+
```
103+
84104
### Service Account
85105
86106
Create a service account that the collector should use.

receiver/k8seventsreceiver/config.go

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
package k8seventsreceiver // import "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/k8seventsreceiver"
55

66
import (
7+
"fmt"
8+
"regexp"
9+
710
k8s "k8s.io/client-go/kubernetes"
811

912
"github.com/open-telemetry/opentelemetry-collector-contrib/internal/k8sconfig"
@@ -16,12 +19,33 @@ type Config struct {
1619
// List of ‘namespaces’ to collect events from.
1720
Namespaces []string `mapstructure:"namespaces"`
1821

22+
// Extract section allows specifying extraction rules to extract
23+
// data from k8s event specs
24+
Extract ExtractConfig `mapstructure:"extract"`
25+
1926
// For mocking
2027
makeClient func(apiConf k8sconfig.APIConfig) (k8s.Interface, error)
2128
}
2229

2330
func (cfg *Config) Validate() error {
24-
return cfg.APIConfig.Validate()
31+
if err := cfg.APIConfig.Validate(); err != nil {
32+
return err
33+
}
34+
35+
for _, f := range append(cfg.Extract.Labels, cfg.Extract.Annotations...) {
36+
if f.Key != "" && f.KeyRegex != "" {
37+
return fmt.Errorf("Out of Key or KeyRegex only one option is expected to be configured at a time, currently Key:%s and KeyRegex:%s", f.Key, f.KeyRegex)
38+
}
39+
40+
if f.KeyRegex != "" {
41+
_, err := regexp.Compile("^(?:" + f.KeyRegex + ")$")
42+
if err != nil {
43+
return err
44+
}
45+
}
46+
}
47+
48+
return nil
2549
}
2650

2751
func (cfg *Config) getK8sClient() (k8s.Interface, error) {
@@ -30,3 +54,52 @@ func (cfg *Config) getK8sClient() (k8s.Interface, error) {
3054
}
3155
return cfg.makeClient(cfg.APIConfig)
3256
}
57+
58+
// ExtractConfig section allows specifying extraction rules to extract
59+
// data from k8s event specs.
60+
type ExtractConfig struct {
61+
// Annotations allows extracting data from event annotations and record it
62+
// as resource attributes.
63+
// It is a list of FieldExtractConfig type. See FieldExtractConfig
64+
// documentation for more details.
65+
Annotations []FieldExtractConfig `mapstructure:"annotations"`
66+
67+
// Labels allows extracting data from event labels and record it
68+
// as resource attributes.
69+
// It is a list of FieldExtractConfig type. See FieldExtractConfig
70+
// documentation for more details.
71+
Labels []FieldExtractConfig `mapstructure:"labels"`
72+
}
73+
74+
// FieldExtractConfig allows specifying an extraction rule to extract a resource attribute from event
75+
// annotations (or labels).
76+
type FieldExtractConfig struct {
77+
// TagName represents the name of the resource attribute that will be added to logs, metrics or spans.
78+
// When not specified, a default tag name will be used of the format:
79+
// - k8s.event.annotations.<annotation key>
80+
// - k8s.event.labels.<label key>
81+
// For example, if tag_name is not specified and the key is git_sha,
82+
// then the attribute name will be `k8s.event.annotations.git_sha`.
83+
// When key_regex is present, tag_name supports back reference to both named capturing and positioned capturing.
84+
// For example, if your event spec contains the following labels,
85+
//
86+
// app.kubernetes.io/component: mysql
87+
// app.kubernetes.io/version: 5.7.21
88+
//
89+
// and you'd like to add tags for all labels with prefix app.kubernetes.io/ and also trim the prefix,
90+
// then you can specify the following extraction rules:
91+
//
92+
// extract:
93+
// labels:
94+
// - tag_name: $$1
95+
// key_regex: kubernetes.io/(.*)
96+
//
97+
// this will add the `component` and `version` tags to the spans or metrics.
98+
TagName string `mapstructure:"tag_name"`
99+
100+
// Key represents the annotation (or label) name. This must exactly match an annotation (or label) name.
101+
Key string `mapstructure:"key"`
102+
// KeyRegex is a regular expression used to extract a Key that matches the regex.
103+
// Out of Key or KeyRegex, only one option is expected to be configured at a time.
104+
KeyRegex string `mapstructure:"key_regex"`
105+
}

receiver/k8seventsreceiver/config_test.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,16 @@ func TestLoadConfig(t *testing.T) {
3939
APIConfig: k8sconfig.APIConfig{
4040
AuthType: k8sconfig.AuthTypeServiceAccount,
4141
},
42+
Extract: ExtractConfig{
43+
Annotations: []FieldExtractConfig{
44+
{TagName: "a1", Key: "annotation-one"},
45+
{TagName: "a2", KeyRegex: "annotation-two"},
46+
},
47+
Labels: []FieldExtractConfig{
48+
{TagName: "l1", Key: "label1"},
49+
{TagName: "l2", KeyRegex: "label2"},
50+
},
51+
},
4252
},
4353
},
4454
}

receiver/k8seventsreceiver/factory.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,14 @@ func createLogsReceiver(
4040

4141
return newReceiver(params, rCfg, consumer)
4242
}
43+
44+
func createReceiverOpts(cfg component.Config) []option {
45+
oCfg := cfg.(*Config)
46+
var opts []option
47+
48+
// extraction rules
49+
opts = append(opts, withExtractLabels(oCfg.Extract.Labels...))
50+
opts = append(opts, withExtractAnnotations(oCfg.Extract.Annotations...))
51+
52+
return opts
53+
}

receiver/k8seventsreceiver/go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ require (
66
github.com/open-telemetry/opentelemetry-collector-contrib/internal/k8sconfig v0.124.1
77
github.com/stretchr/testify v1.10.0
88
go.opentelemetry.io/collector/component v1.30.0
9+
go.opentelemetry.io/collector/component/componentstatus v0.124.0
910
go.opentelemetry.io/collector/component/componenttest v0.124.0
1011
go.opentelemetry.io/collector/confmap v1.30.0
1112
go.opentelemetry.io/collector/confmap/xconfmap v0.124.0

receiver/k8seventsreceiver/go.sum

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
// Copyright The OpenTelemetry Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package kube // import "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/k8seventsreceiver/internal/kube"
5+
6+
import (
7+
"fmt"
8+
"regexp"
9+
)
10+
11+
// ExtractionRules is used to specify the information that needs to be extracted
12+
// from event and added to the spans as tags.
13+
type ExtractionRules struct {
14+
Annotations []FieldExtractionRule
15+
Labels []FieldExtractionRule
16+
}
17+
18+
// FieldExtractionRule is used to specify which fields to extract from event fields
19+
// and inject into spans as attributes.
20+
type FieldExtractionRule struct {
21+
// Name is used to as the Span tag name.
22+
Name string
23+
// Key is used to lookup k8s event fields.
24+
Key string
25+
// KeyRegex is a regular expression(full length match) used to extract a Key that matches the regex.
26+
KeyRegex *regexp.Regexp
27+
HasKeyRegexReference bool
28+
// Regex is a regular expression used to extract a sub-part of a field value.
29+
// Full value is extracted when no regexp is provided.
30+
Regex *regexp.Regexp
31+
}
32+
33+
func (r *FieldExtractionRule) ExtractTagsFromMetadata(metadata map[string]string, formatter string) map[string]string {
34+
tags := make(map[string]string)
35+
if r.KeyRegex != nil {
36+
for k, v := range metadata {
37+
if r.KeyRegex.MatchString(k) && v != "" {
38+
var name string
39+
if r.HasKeyRegexReference {
40+
var result []byte
41+
name = string(r.KeyRegex.ExpandString(result, r.Name, k, r.KeyRegex.FindStringSubmatchIndex(k)))
42+
} else {
43+
name = fmt.Sprintf(formatter, k)
44+
}
45+
tags[name] = v
46+
}
47+
}
48+
} else if v, ok := metadata[r.Key]; ok {
49+
tags[r.Name] = r.extractField(v)
50+
}
51+
return tags
52+
}
53+
54+
func (r *FieldExtractionRule) extractField(v string) string {
55+
// Check if a subset of the field should be extracted with a regular expression
56+
// instead of the whole field.
57+
if r.Regex == nil {
58+
return v
59+
}
60+
61+
matches := r.Regex.FindStringSubmatch(v)
62+
if len(matches) == 2 {
63+
return matches[1]
64+
}
65+
return ""
66+
}

receiver/k8seventsreceiver/k8s_event_to_logdata.go

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import (
1111
semconv "go.opentelemetry.io/collector/semconv/v1.27.0"
1212
"go.uber.org/zap"
1313
corev1 "k8s.io/api/core/v1"
14+
15+
"github.com/open-telemetry/opentelemetry-collector-contrib/receiver/k8seventsreceiver/internal/kube"
1416
)
1517

1618
const (
@@ -29,7 +31,7 @@ var severityMap = map[string]plog.SeverityNumber{
2931
}
3032

3133
// k8sEventToLogRecord converts Kubernetes event to plog.LogRecordSlice and adds the resource attributes.
32-
func k8sEventToLogData(logger *zap.Logger, ev *corev1.Event) plog.Logs {
34+
func k8sEventToLogData(logger *zap.Logger, ev *corev1.Event, extractionRules ...*kube.ExtractionRules) plog.Logs {
3335
ld := plog.NewLogs()
3436
rl := ld.ResourceLogs().AppendEmpty()
3537
sl := rl.ScopeLogs().AppendEmpty()
@@ -73,6 +75,19 @@ func k8sEventToLogData(logger *zap.Logger, ev *corev1.Event) plog.Logs {
7375
attrs.PutStr("k8s.event.uid", string(ev.UID))
7476
attrs.PutStr(semconv.AttributeK8SNamespaceName, ev.InvolvedObject.Namespace)
7577

78+
for _, rules := range extractionRules {
79+
for _, r := range rules.Labels {
80+
for k, v := range r.ExtractTagsFromMetadata(ev.Labels, "k8s.event.labels.%s") {
81+
attrs.PutStr(k, v)
82+
}
83+
}
84+
for _, r := range rules.Annotations {
85+
for k, v := range r.ExtractTagsFromMetadata(ev.Annotations, "k8s.event.annotations.%s") {
86+
attrs.PutStr(k, v)
87+
}
88+
}
89+
}
90+
7691
// "Count" field of k8s event will be '0' in case it is
7792
// not present in the collected event from k8s.
7893
if ev.Count != 0 {

receiver/k8seventsreceiver/k8s_event_to_logdata_test.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import (
99
"github.com/stretchr/testify/assert"
1010
"go.opentelemetry.io/collector/pdata/plog"
1111
"go.uber.org/zap"
12+
13+
"github.com/open-telemetry/opentelemetry-collector-contrib/receiver/k8seventsreceiver/internal/kube"
1214
)
1315

1416
func TestK8sEventToLogData(t *testing.T) {
@@ -51,6 +53,68 @@ func TestK8sEventToLogDataWithApiAndResourceVersion(t *testing.T) {
5153
assert.Equal(t, "7387066320", attr.AsString())
5254
}
5355

56+
func TestK8sEventToLogDataWithExtractionRules(t *testing.T) {
57+
k8sEvent := getEvent()
58+
k8sEvent.Labels["testlabel"] = "test-key-label"
59+
k8sEvent.Labels["app.kubernetes.io/by-key"] = "test-key-label"
60+
k8sEvent.Labels["app.kubernetes.io/by-regex"] = "test-regex-label"
61+
k8sEvent.Annotations["topology.kubernetes.io/by-key"] = "test-key-annotation"
62+
k8sEvent.Annotations["topology.kubernetes.io/by-regex"] = "test-regex-annotation"
63+
64+
labelsRules, err := ExtractFieldRules("labels",
65+
FieldExtractConfig{
66+
Key: "testlabel",
67+
},
68+
FieldExtractConfig{
69+
TagName: "k8s.event.labels.label_by_key",
70+
Key: "app.kubernetes.io/by-key",
71+
},
72+
FieldExtractConfig{
73+
TagName: "k8s.event.labels.$1",
74+
KeyRegex: "app.kubernetes.io/(.*)",
75+
})
76+
assert.NoError(t, err)
77+
78+
annotationsRules, err := ExtractFieldRules("annotations", FieldExtractConfig{
79+
TagName: "k8s.event.annotations.annotation_by_key",
80+
Key: "topology.kubernetes.io/by-key",
81+
}, FieldExtractConfig{
82+
TagName: "k8s.event.annotations.$1",
83+
KeyRegex: "topology.kubernetes.io/(.*)",
84+
})
85+
assert.NoError(t, err)
86+
87+
ld := k8sEventToLogData(zap.NewNop(), k8sEvent, &kube.ExtractionRules{
88+
Labels: labelsRules,
89+
Annotations: annotationsRules,
90+
})
91+
assert.NoError(t, err)
92+
93+
rl := ld.ResourceLogs().At(0)
94+
lr := rl.ScopeLogs().At(0)
95+
attrs := lr.LogRecords().At(0).Attributes()
96+
97+
attr, ok := attrs.Get("k8s.event.labels.testlabel")
98+
assert.True(t, ok)
99+
assert.Equal(t, "test-key-label", attr.AsString())
100+
101+
attr, ok = attrs.Get("k8s.event.labels.label_by_key")
102+
assert.True(t, ok)
103+
assert.Equal(t, "test-key-label", attr.AsString())
104+
105+
attr, ok = attrs.Get("k8s.event.labels.by-regex")
106+
assert.True(t, ok)
107+
assert.Equal(t, "test-regex-label", attr.AsString())
108+
109+
attr, ok = attrs.Get("k8s.event.annotations.annotation_by_key")
110+
assert.True(t, ok)
111+
assert.Equal(t, "test-key-annotation", attr.AsString())
112+
113+
attr, ok = attrs.Get("k8s.event.annotations.by-regex")
114+
assert.True(t, ok)
115+
assert.Equal(t, "test-regex-annotation", attr.AsString())
116+
}
117+
54118
func TestUnknownSeverity(t *testing.T) {
55119
k8sEvent := getEvent()
56120
k8sEvent.Type = "Unknown"

0 commit comments

Comments
 (0)