Skip to content

Commit d6752fe

Browse files
zeitlingerdragonlord93
authored andcommitted
service resource attributes (open-telemetry#39335)
second part of open-telemetry#37114
1 parent e60e167 commit d6752fe

18 files changed

+558
-67
lines changed
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
change_type: enhancement
2+
3+
component: k8sattributesprocessor
4+
5+
note: Add option to configure automatic service resource attributes
6+
7+
issues: [37114]
8+
9+
subtext: |
10+
Implements [Service Attributes](https://opentelemetry.io/docs/specs/semconv/non-normative/k8s-attributes/#service-attributes).
11+
12+
If you are using the file log receiver, you can now create the same resource attributes as traces (via OTLP) received
13+
from an application instrumented with the OpenTelemetry Operator -
14+
simply by adding the
15+
`extract: { metadata: ["service.namespace", "service.name", "service.version", "service.instance.id"] }`
16+
configuration to the `k8sattributesprocessor` processor.
17+
See the [documentation](https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/main/processor/k8sattributesprocessor/README.md#configuring-recommended-resource-attributes) for more details.

processor/k8sattributesprocessor/README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,10 @@ are then also available for the use within association rules. Available attribut
9393
- k8s.job.name
9494
- k8s.node.name
9595
- k8s.cluster.uid
96+
- [service.namespace](https://opentelemetry.io/docs/specs/semconv/non-normative/k8s-attributes/#how-servicenamespace-should-be-calculated)
97+
- [service.name](https://opentelemetry.io/docs/specs/semconv/non-normative/k8s-attributes/#how-servicename-should-be-calculated)
98+
- [service.version](https://opentelemetry.io/docs/specs/semconv/non-normative/k8s-attributes/#how-serviceversion-should-be-calculated)
99+
- [service.instance.id](https://opentelemetry.io/docs/specs/semconv/non-normative/k8s-attributes/#how-serviceinstanceid-should-be-calculated)
96100
- Any tags extracted from the pod labels and annotations, as described in [extracting attributes from pod labels and annotations](#extracting-attributes-from-pod-labels-and-annotations)
97101

98102

@@ -108,11 +112,15 @@ correctly associate the matching container to the resource:
108112
- container.image.name
109113
- container.image.tag
110114
- container.image.repo_digests (if k8s CRI populates [repository digest field](https://github.com/open-telemetry/semantic-conventions/blob/v1.26.0/model/registry/container.yaml#L60-L71))
115+
- service.version
116+
- service.instance.id
111117
2. If the `k8s.container.name` resource attribute is provided, the following additional attributes will be available:
112118
- container.id (if the `k8s.container.restart_count` resource attribute is not provided, it's not guaranteed to get the right container ID.)
113119
- container.image.name
114120
- container.image.tag
115121
- container.image.repo_digests (if k8s CRI populates [repository digest field](https://github.com/open-telemetry/semantic-conventions/blob/v1.26.0/model/registry/container.yaml#L60-L71))
122+
- service.version
123+
- service.instance.id
116124
3. If the `k8s.container.restart_count` resource attribute is provided, it can be used to associate with a particular container
117125
instance. If it's not set, the latest container instance will be used:
118126
- container.id (not added by default, has to be specified in `metadata`)
@@ -262,6 +270,11 @@ The processor can be configured to set the
262270
```yaml
263271
extract:
264272
otel_annotations: true
273+
metadata:
274+
- service.namespace
275+
- service.name
276+
- service.version
277+
- service.instance.id
265278
```
266279

267280
### Config example
@@ -283,6 +296,10 @@ k8sattributes/2:
283296
- k8s.namespace.name
284297
- k8s.node.name
285298
- k8s.pod.start_time
299+
- service.namespace
300+
- service.name
301+
- service.version
302+
- service.instance.id
286303
labels:
287304
# This label extraction rule takes the value 'app.kubernetes.io/component' label and maps it to the 'app.label.component' attribute which will be added to the associated resources
288305
- tag_name: app.label.component

processor/k8sattributesprocessor/config.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,8 @@ func (cfg *Config) Validate() error {
101101
string(conventions.K8SNodeNameKey), string(conventions.K8SNodeUIDKey),
102102
string(conventions.K8SContainerNameKey), string(conventions.ContainerIDKey),
103103
string(conventions.ContainerImageNameKey), string(conventions.ContainerImageTagKey),
104+
string(conventions.ServiceNamespaceKey), string(conventions.ServiceNameKey),
105+
string(conventions.ServiceVersionKey), string(conventions.ServiceInstanceIDKey),
104106
containerImageRepoDigests, clusterUID:
105107
default:
106108
return fmt.Errorf("\"%s\" is not a supported metadata field", field)

processor/k8sattributesprocessor/documentation.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@
3131
| k8s.replicaset.uid | The UID of the ReplicaSet. | Any Str | false |
3232
| k8s.statefulset.name | The name of the StatefulSet. | Any Str | false |
3333
| k8s.statefulset.uid | The UID of the StatefulSet. | Any Str | false |
34+
| service.instance.id | The instance ID of the service. | Any Str | false |
35+
| service.name | The name of the service. | Any Str | false |
36+
| service.namespace | The namespace of the service. | Any Str | false |
37+
| service.version | The version of the service. | Any Str | false |
3438

3539
## Internal Telemetry
3640

processor/k8sattributesprocessor/go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ module github.com/open-telemetry/opentelemetry-collector-contrib/processor/k8sat
33
go 1.23.0
44

55
require (
6+
github.com/distribution/reference v0.6.0
67
github.com/google/go-cmp v0.7.0
78
github.com/google/uuid v1.6.0
89
github.com/open-telemetry/opentelemetry-collector-contrib/internal/common v0.126.0
@@ -48,7 +49,6 @@ require (
4849
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
4950
github.com/Microsoft/go-winio v0.6.2 // indirect
5051
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
51-
github.com/distribution/reference v0.6.0 // indirect
5252
github.com/docker/docker v28.1.1+incompatible // indirect
5353
github.com/docker/go-connections v0.5.0 // indirect
5454
github.com/docker/go-units v0.5.0 // indirect

processor/k8sattributesprocessor/internal/kube/client.go

Lines changed: 116 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ import (
1212
"sync"
1313
"time"
1414

15+
"github.com/distribution/reference"
1516
"go.opentelemetry.io/collector/component"
17+
"go.opentelemetry.io/otel/attribute"
1618
conventions "go.opentelemetry.io/otel/semconv/v1.6.1"
1719
"go.uber.org/zap"
1820
apps_v1 "k8s.io/api/apps/v1"
@@ -77,6 +79,8 @@ var rRegex = regexp.MustCompile(`^(.*)-[0-9a-zA-Z]+$`)
7779
// format: [cronjob-name]-[time-hash-int]
7880
var cronJobRegex = regexp.MustCompile(`^(.*)-[0-9]+$`)
7981

82+
var errCannotRetrieveImage = errors.New("cannot retrieve image name")
83+
8084
// New initializes a new k8s Client.
8185
func New(
8286
set component.TelemetrySettings,
@@ -453,6 +457,9 @@ func (c *WatchClient) extractPodAttributes(pod *api_v1.Pod) map[string]string {
453457
if c.Rules.PodName {
454458
tags[string(conventions.K8SPodNameKey)] = pod.Name
455459
}
460+
if c.Rules.ServiceName {
461+
tags[string(conventions.ServiceNameKey)] = pod.Name
462+
}
456463

457464
if c.Rules.PodHostName {
458465
tags[tagHostName] = pod.Spec.Hostname
@@ -487,7 +494,7 @@ func (c *WatchClient) extractPodAttributes(pod *api_v1.Pod) map[string]string {
487494
c.Rules.JobUID || c.Rules.JobName ||
488495
c.Rules.StatefulSetUID || c.Rules.StatefulSetName ||
489496
c.Rules.DeploymentName || c.Rules.DeploymentUID ||
490-
c.Rules.CronJobName {
497+
c.Rules.CronJobName || c.Rules.ServiceName {
491498
for _, ref := range pod.OwnerReferences {
492499
switch ref.Kind {
493500
case "ReplicaSet":
@@ -497,10 +504,20 @@ func (c *WatchClient) extractPodAttributes(pod *api_v1.Pod) map[string]string {
497504
if c.Rules.ReplicaSetName {
498505
tags[string(conventions.K8SReplicaSetNameKey)] = ref.Name
499506
}
500-
if c.Rules.DeploymentName {
507+
if c.Rules.ServiceName {
508+
tags[string(conventions.ServiceNameKey)] = ref.Name
509+
}
510+
if c.Rules.DeploymentName || c.Rules.ServiceName {
501511
if replicaset, ok := c.getReplicaSet(string(ref.UID)); ok {
502-
if replicaset.Deployment.Name != "" {
503-
tags[string(conventions.K8SDeploymentNameKey)] = replicaset.Deployment.Name
512+
name := replicaset.Deployment.Name
513+
if name != "" {
514+
if c.Rules.DeploymentName {
515+
tags[string(conventions.K8SDeploymentNameKey)] = name
516+
}
517+
if c.Rules.ServiceName {
518+
// deployment name wins over replicaset name
519+
tags[string(conventions.ServiceNameKey)] = name
520+
}
504521
}
505522
}
506523
}
@@ -518,26 +535,42 @@ func (c *WatchClient) extractPodAttributes(pod *api_v1.Pod) map[string]string {
518535
if c.Rules.DaemonSetName {
519536
tags[string(conventions.K8SDaemonSetNameKey)] = ref.Name
520537
}
538+
if c.Rules.ServiceName {
539+
tags[string(conventions.ServiceNameKey)] = ref.Name
540+
}
521541
case "StatefulSet":
522542
if c.Rules.StatefulSetUID {
523543
tags[string(conventions.K8SStatefulSetUIDKey)] = string(ref.UID)
524544
}
525545
if c.Rules.StatefulSetName {
526546
tags[string(conventions.K8SStatefulSetNameKey)] = ref.Name
527547
}
528-
case "Job":
529-
if c.Rules.CronJobName {
530-
parts := c.cronJobRegex.FindStringSubmatch(ref.Name)
531-
if len(parts) == 2 {
532-
tags[string(conventions.K8SCronJobNameKey)] = parts[1]
533-
}
548+
if c.Rules.ServiceName {
549+
tags[string(conventions.ServiceNameKey)] = ref.Name
534550
}
551+
case "Job":
535552
if c.Rules.JobUID {
536553
tags[string(conventions.K8SJobUIDKey)] = string(ref.UID)
537554
}
538555
if c.Rules.JobName {
539556
tags[string(conventions.K8SJobNameKey)] = ref.Name
540557
}
558+
if c.Rules.ServiceName {
559+
tags[string(conventions.ServiceNameKey)] = ref.Name
560+
}
561+
if c.Rules.CronJobName || c.Rules.ServiceName {
562+
parts := c.cronJobRegex.FindStringSubmatch(ref.Name)
563+
if len(parts) == 2 {
564+
name := parts[1]
565+
if c.Rules.CronJobName {
566+
tags[string(conventions.K8SCronJobNameKey)] = name
567+
}
568+
if c.Rules.ServiceName {
569+
// cronjob name wins over job name
570+
tags[string(conventions.ServiceNameKey)] = name
571+
}
572+
}
573+
}
541574
}
542575
}
543576
}
@@ -558,12 +591,28 @@ func (c *WatchClient) extractPodAttributes(pod *api_v1.Pod) map[string]string {
558591
r.extractFromPodMetadata(pod.Labels, tags, "k8s.pod.labels.%s")
559592
}
560593

594+
if c.Rules.ServiceName {
595+
copyLabel(pod, tags, "app.kubernetes.io/name", conventions.ServiceNameKey)
596+
// app.kubernetes.io/instance has a higher precedence than app.kubernetes.io/name
597+
copyLabel(pod, tags, "app.kubernetes.io/instance", conventions.ServiceNameKey)
598+
}
599+
600+
if c.Rules.ServiceVersion {
601+
copyLabel(pod, tags, "app.kubernetes.io/version", conventions.ServiceVersionKey)
602+
}
603+
561604
for _, r := range c.Rules.Annotations {
562605
r.extractFromPodMetadata(pod.Annotations, tags, "k8s.pod.annotations.%s")
563606
}
564607
return tags
565608
}
566609

610+
func copyLabel(pod *api_v1.Pod, tags map[string]string, labelKey string, key attribute.Key) {
611+
if val, ok := pod.Labels[labelKey]; ok {
612+
tags[string(key)] = val
613+
}
614+
}
615+
567616
// This function removes all data from the Pod except what is required by extraction rules and pod association
568617
func removeUnnecessaryPodData(pod *api_v1.Pod, rules ExtractionRules) *api_v1.Pod {
569618
// name, namespace, uid, start time and ip are needed for identifying Pods
@@ -626,7 +675,7 @@ func removeUnnecessaryPodData(pod *api_v1.Pod, rules ExtractionRules) *api_v1.Po
626675
removeUnnecessaryContainerData := func(c api_v1.Container) api_v1.Container {
627676
transformedContainer := api_v1.Container{}
628677
transformedContainer.Name = c.Name // we always need the name, it's used for identification
629-
if rules.ContainerImageName || rules.ContainerImageTag {
678+
if rules.ContainerImageName || rules.ContainerImageTag || rules.ServiceVersion {
630679
transformedContainer.Image = c.Image
631680
}
632681
return transformedContainer
@@ -644,7 +693,7 @@ func removeUnnecessaryPodData(pod *api_v1.Pod, rules ExtractionRules) *api_v1.Po
644693
}
645694
}
646695

647-
if len(rules.Labels) > 0 {
696+
if len(rules.Labels) > 0 || rules.ServiceName || rules.ServiceVersion {
648697
transformedPod.Labels = pod.Labels
649698
}
650699

@@ -659,6 +708,38 @@ func removeUnnecessaryPodData(pod *api_v1.Pod, rules ExtractionRules) *api_v1.Po
659708
return &transformedPod
660709
}
661710

711+
// parseServiceVersionFromImage parses the service version for differently-formatted image names
712+
// according to https://github.com/open-telemetry/semantic-conventions/blob/main/docs/non-normative/k8s-attributes.md#how-serviceversion-should-be-calculated
713+
func parseServiceVersionFromImage(image string) (string, error) {
714+
ref, err := reference.Parse(image)
715+
if err != nil {
716+
return "", err
717+
}
718+
719+
namedRef, ok := ref.(reference.Named)
720+
if !ok {
721+
return "", errCannotRetrieveImage
722+
}
723+
var tag, digest string
724+
if taggedRef, ok := namedRef.(reference.Tagged); ok {
725+
tag = taggedRef.Tag()
726+
}
727+
if digestedRef, ok := namedRef.(reference.Digested); ok {
728+
digest = digestedRef.Digest().String()
729+
}
730+
if digest != "" {
731+
if tag != "" {
732+
return fmt.Sprintf("%s@%s", tag, digest), nil
733+
}
734+
return digest, nil
735+
}
736+
if tag != "" {
737+
return tag, nil
738+
}
739+
740+
return "", errCannotRetrieveImage
741+
}
742+
662743
func (c *WatchClient) extractPodContainersAttributes(pod *api_v1.Pod) PodContainers {
663744
containers := PodContainers{
664745
ByID: map[string]*Container{},
@@ -667,7 +748,8 @@ func (c *WatchClient) extractPodContainersAttributes(pod *api_v1.Pod) PodContain
667748
if !needContainerAttributes(c.Rules) {
668749
return containers
669750
}
670-
if c.Rules.ContainerImageName || c.Rules.ContainerImageTag {
751+
if c.Rules.ContainerImageName || c.Rules.ContainerImageTag ||
752+
c.Rules.ServiceVersion || c.Rules.ServiceInstanceID {
671753
for _, spec := range append(pod.Spec.Containers, pod.Spec.InitContainers...) {
672754
container := &Container{}
673755
imageRef, err := dcommon.ParseImageName(spec.Image)
@@ -678,18 +760,28 @@ func (c *WatchClient) extractPodContainersAttributes(pod *api_v1.Pod) PodContain
678760
if c.Rules.ContainerImageTag {
679761
container.ImageTag = imageRef.Tag
680762
}
763+
if c.Rules.ServiceVersion {
764+
serviceVersion, err := parseServiceVersionFromImage(spec.Image)
765+
if err == nil {
766+
container.ServiceVersion = serviceVersion
767+
}
768+
}
681769
}
682770
containers.ByName[spec.Name] = container
683771
}
684772
}
685773
for _, apiStatus := range append(pod.Status.ContainerStatuses, pod.Status.InitContainerStatuses...) {
686-
container, ok := containers.ByName[apiStatus.Name]
774+
containerName := apiStatus.Name
775+
container, ok := containers.ByName[containerName]
687776
if !ok {
688777
container = &Container{}
689-
containers.ByName[apiStatus.Name] = container
778+
containers.ByName[containerName] = container
690779
}
691780
if c.Rules.ContainerName {
692-
container.Name = apiStatus.Name
781+
container.Name = containerName
782+
}
783+
if c.Rules.ServiceInstanceID {
784+
container.ServiceInstanceID = automaticServiceInstanceID(pod, containerName)
693785
}
694786
containerID := apiStatus.ContainerID
695787
// Remove container runtime prefix
@@ -1022,7 +1114,9 @@ func needContainerAttributes(rules ExtractionRules) bool {
10221114
rules.ContainerName ||
10231115
rules.ContainerImageTag ||
10241116
rules.ContainerImageRepoDigests ||
1025-
rules.ContainerID
1117+
rules.ContainerID ||
1118+
rules.ServiceVersion ||
1119+
rules.ServiceInstanceID
10261120
}
10271121

10281122
func (c *WatchClient) handleReplicaSetAdd(obj any) {
@@ -1127,3 +1221,8 @@ func ignoreDeletedFinalStateUnknown(obj any) any {
11271221
}
11281222
return obj
11291223
}
1224+
1225+
func automaticServiceInstanceID(pod *api_v1.Pod, containerName string) string {
1226+
resNames := []string{pod.Namespace, pod.Name, containerName}
1227+
return strings.Join(resNames, ".")
1228+
}

0 commit comments

Comments
 (0)