Skip to content

Commit

Permalink
processor/k8sattributesprocessor: map containers by ID (open-telemetr…
Browse files Browse the repository at this point in the history
…y#20340)

This change allows the user (or SDK) sent [`container.id`](https://github.com/open-telemetry/opentelemetry-go/blob/sdk/v1.14.0/sdk/resource/config.go#L188-L201) to be picked up by the processor and used to identify container attributes. As a result, it is now possible to obtain the `k8s.container.name` attribute (along with the `container.image.{name,tag}` pair) more easily and in line with the SDK API.
  • Loading branch information
gbbr committed Apr 11, 2023
1 parent fa04914 commit 21591e1
Show file tree
Hide file tree
Showing 9 changed files with 305 additions and 104 deletions.
16 changes: 16 additions & 0 deletions .chloggen/gbbr_container-id.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
change_type: enhancement

# The name of the component, or a single word describing the area of concern, (e.g. filelogreceiver)
component: k8sattributesprocessor

# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
note: Allow getting k8s.container.name, container.image.name and container.image.tag by container.id.

# One or more tracking issues related to the change
issues: [19468]

# (Optional) One or more lines of additional information to render under the primary note.
# These lines will be padded with 2 spaces and then inserted directly into the document.
# Use pipe (|) for multiline entries.
subtext: The container.id resource attribute can be set automatically in most SDKs by means of API.
17 changes: 10 additions & 7 deletions processor/k8sattributesprocessor/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,15 +59,18 @@ You can change this list with `metadata` configuration.
Not all the attributes are guaranteed to be added. Only attribute names from `metadata` should be used for
pod_association's `resource_attribute`, because empty or non-existing values will be ignored.

The following container level attributes require additional attributes to identify a particular container in a pod:
1. Container spec attributes - will be set only if container identifying attribute `k8s.container.name` is set
as a resource attribute (similar to all other attributes, pod has to be identified as well):
Additional container level attributes can be extracted provided that certain resource attributes are provided:

1. If the `container.id` resource attribute is provided, the following additional attributes will be available:
- k8s.container.name
- container.image.name
- container.image.tag
2. If the `k8s.container.name` resource attribute is provided, the following additional attributes will be available:
- container.image.name
- container.image.tag
2. Container attributes - in addition to pod identifier and `k8s.container.name` attribute, `k8s.container.restart_count`
resource attribute is needed to get association with a particular container instance, but not required.
If `k8s.container.restart_count` is not set, the latest container instance will be used:
- container.id (not added by default, have to be specified in `metadata`)
3. If the `k8s.container.restart_count` resource attribute is provided, it can be used to associate with a particular container
instance. If it's not set, the latest container instance will be used:
- container.id (not added by default, has to be specified in `metadata`)

The k8sattributesprocessor can also set resource attributes from k8s labels and annotations of pods and namespaces.
The config for associating the data passing through the processor (spans, metrics and logs) with specific Pod/Namespace annotations/labels is configured via "annotations" and "labels" keys.
Expand Down
9 changes: 5 additions & 4 deletions processor/k8sattributesprocessor/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,8 @@ type ExtractConfig struct {
// k8s.daemonset.name, k8s.daemonset.uid,
// k8s.job.name, k8s.job.uid, k8s.cronjob.name,
// k8s.statefulset.name, k8s.statefulset.uid,
// container.image.name, container.image.tag,
// container.id
// k8s.container.name, container.image.name,
// container.image.tag, container.id
//
// Specifying anything other than these values will result in an error.
// By default, the following fields are extracted and added to spans, metrics and logs as attributes:
Expand All @@ -86,8 +86,9 @@ type ExtractConfig struct {
// - k8s.namespace.name
// - k8s.node.name
// - k8s.deployment.name (if the pod is controlled by a deployment)
// - container.image.name (requires an additional attribute to be set: k8s.container.name)
// - container.image.tag (requires an additional attribute to be set: k8s.container.name)
// - k8s.container.name (requires an additional attribute to be set: container.id)
// - container.image.name (requires one of the following additional attributes to be set: container.id or k8s.container.name)
// - container.image.tag (requires one of the following additional attributes to be set: container.id or k8s.container.name)
Metadata []string `mapstructure:"metadata"`

// Annotations allows extracting data from pod annotations and record it
Expand Down
52 changes: 30 additions & 22 deletions processor/k8sattributesprocessor/internal/kube/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -374,9 +374,14 @@ func (c *WatchClient) extractPodAttributes(pod *api_v1.Pod) map[string]string {
return tags
}

func (c *WatchClient) extractPodContainersAttributes(pod *api_v1.Pod) map[string]*Container {
containers := map[string]*Container{}

func (c *WatchClient) extractPodContainersAttributes(pod *api_v1.Pod) PodContainers {
containers := PodContainers{
ByID: map[string]*Container{},
ByName: map[string]*Container{},
}
if !needContainerAttributes(c.Rules) {
return containers
}
if c.Rules.ContainerImageName || c.Rules.ContainerImageTag {
for _, spec := range append(pod.Spec.Containers, pod.Spec.InitContainers...) {
container := &Container{}
Expand All @@ -391,29 +396,29 @@ func (c *WatchClient) extractPodContainersAttributes(pod *api_v1.Pod) map[string
if c.Rules.ContainerImageTag && nameTagSep > 0 {
container.ImageTag = spec.Image[nameTagSep+1:]
}
containers[spec.Name] = container
containers.ByName[spec.Name] = container
}
}

if c.Rules.ContainerID {
for _, apiStatus := range append(pod.Status.ContainerStatuses, pod.Status.InitContainerStatuses...) {
container, ok := containers[apiStatus.Name]
if !ok {
container = &Container{}
containers[apiStatus.Name] = container
}
for _, apiStatus := range append(pod.Status.ContainerStatuses, pod.Status.InitContainerStatuses...) {
container, ok := containers.ByName[apiStatus.Name]
if !ok {
container = &Container{}
containers.ByName[apiStatus.Name] = container
}
if c.Rules.ContainerName {
container.Name = apiStatus.Name
}
containerID := apiStatus.ContainerID
// Remove container runtime prefix
parts := strings.Split(containerID, ":https://")
if len(parts) == 2 {
containerID = parts[1]
}
containers.ByID[containerID] = container
if c.Rules.ContainerID {
if container.Statuses == nil {
container.Statuses = map[int]ContainerStatus{}
}

containerID := apiStatus.ContainerID

// Remove container runtime prefix
idParts := strings.Split(containerID, ":https://")
if len(idParts) == 2 {
containerID = idParts[1]
}

container.Statuses[int(apiStatus.RestartCount)] = ContainerStatus{containerID}
}
}
Expand Down Expand Up @@ -651,5 +656,8 @@ func (c *WatchClient) extractNamespaceLabelsAnnotations() bool {
}

func needContainerAttributes(rules ExtractionRules) bool {
return rules.ContainerImageName || rules.ContainerImageTag || rules.ContainerID
return rules.ContainerImageName ||
rules.ContainerName ||
rules.ContainerImageTag ||
rules.ContainerID
}
128 changes: 91 additions & 37 deletions processor/k8sattributesprocessor/internal/kube/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1076,7 +1076,7 @@ func Test_extractPodContainersAttributes(t *testing.T) {
name string
rules ExtractionRules
pod api_v1.Pod
want map[string]*Container
want PodContainers
}{
{
name: "no-data",
Expand All @@ -1086,24 +1086,31 @@ func Test_extractPodContainersAttributes(t *testing.T) {
ContainerID: true,
},
pod: api_v1.Pod{},
want: map[string]*Container{},
want: PodContainers{ByID: map[string]*Container{}, ByName: map[string]*Container{}},
},
{
name: "no-rules",
rules: ExtractionRules{},
pod: pod,
want: map[string]*Container{},
want: PodContainers{ByID: map[string]*Container{}, ByName: map[string]*Container{}},
},
{
name: "image-name-only",
rules: ExtractionRules{
ContainerImageName: true,
},
pod: pod,
want: map[string]*Container{
"container1": {ImageName: "test/image1"},
"container2": {ImageName: "example.com:port1/image2"},
"init_container": {ImageName: "test/init-image"},
want: PodContainers{
ByID: map[string]*Container{
"container1-id-123": {ImageName: "test/image1"},
"container2-id-456": {ImageName: "example.com:port1/image2"},
"init-container-id-123": {ImageName: "test/init-image"},
},
ByName: map[string]*Container{
"container1": {ImageName: "test/image1"},
"container2": {ImageName: "example.com:port1/image2"},
"init_container": {ImageName: "test/init-image"},
},
},
},
{
Expand All @@ -1121,8 +1128,11 @@ func Test_extractPodContainersAttributes(t *testing.T) {
},
},
},
want: map[string]*Container{
"test-container": {ImageName: "test/image"},
want: PodContainers{
ByID: map[string]*Container{},
ByName: map[string]*Container{
"test-container": {ImageName: "test/image"},
},
},
},
{
Expand All @@ -1131,20 +1141,39 @@ func Test_extractPodContainersAttributes(t *testing.T) {
ContainerID: true,
},
pod: pod,
want: map[string]*Container{
"container1": {
Statuses: map[int]ContainerStatus{
0: {ContainerID: "container1-id-123"},
want: PodContainers{
ByID: map[string]*Container{
"container1-id-123": {
Statuses: map[int]ContainerStatus{
0: {ContainerID: "container1-id-123"},
},
},
},
"container2": {
Statuses: map[int]ContainerStatus{
2: {ContainerID: "container2-id-456"},
"container2-id-456": {
Statuses: map[int]ContainerStatus{
2: {ContainerID: "container2-id-456"},
},
},
"init-container-id-123": {
Statuses: map[int]ContainerStatus{
0: {ContainerID: "init-container-id-123"},
},
},
},
"init_container": {
Statuses: map[int]ContainerStatus{
0: {ContainerID: "init-container-id-123"},
ByName: map[string]*Container{
"container1": {
Statuses: map[int]ContainerStatus{
0: {ContainerID: "container1-id-123"},
},
},
"container2": {
Statuses: map[int]ContainerStatus{
2: {ContainerID: "container2-id-456"},
},
},
"init_container": {
Statuses: map[int]ContainerStatus{
0: {ContainerID: "init-container-id-123"},
},
},
},
},
Expand All @@ -1157,26 +1186,51 @@ func Test_extractPodContainersAttributes(t *testing.T) {
ContainerID: true,
},
pod: pod,
want: map[string]*Container{
"container1": {
ImageName: "test/image1",
ImageTag: "0.1.0",
Statuses: map[int]ContainerStatus{
0: {ContainerID: "container1-id-123"},
want: PodContainers{
ByID: map[string]*Container{
"container1-id-123": {
ImageName: "test/image1",
ImageTag: "0.1.0",
Statuses: map[int]ContainerStatus{
0: {ContainerID: "container1-id-123"},
},
},
},
"container2": {
ImageName: "example.com:port1/image2",
ImageTag: "0.2.0",
Statuses: map[int]ContainerStatus{
2: {ContainerID: "container2-id-456"},
"container2-id-456": {
ImageName: "example.com:port1/image2",
ImageTag: "0.2.0",
Statuses: map[int]ContainerStatus{
2: {ContainerID: "container2-id-456"},
},
},
"init-container-id-123": {
ImageName: "test/init-image",
ImageTag: "1.0.2",
Statuses: map[int]ContainerStatus{
0: {ContainerID: "init-container-id-123"},
},
},
},
"init_container": {
ImageName: "test/init-image",
ImageTag: "1.0.2",
Statuses: map[int]ContainerStatus{
0: {ContainerID: "init-container-id-123"},
ByName: map[string]*Container{
"container1": {
ImageName: "test/image1",
ImageTag: "0.1.0",
Statuses: map[int]ContainerStatus{
0: {ContainerID: "container1-id-123"},
},
},
"container2": {
ImageName: "example.com:port1/image2",
ImageTag: "0.2.0",
Statuses: map[int]ContainerStatus{
2: {ContainerID: "container2-id-456"},
},
},
"init_container": {
ImageName: "test/init-image",
ImageTag: "1.0.2",
Statuses: map[int]ContainerStatus{
0: {ContainerID: "init-container-id-123"},
},
},
},
},
Expand Down
14 changes: 12 additions & 2 deletions processor/k8sattributesprocessor/internal/kube/kube.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,14 +120,23 @@ type Pod struct {
Namespace string
HostNetwork bool

// Containers is a map of container name to Container struct.
Containers map[string]*Container
// Containers specifies all containers in this pod.
Containers PodContainers

DeletedAt time.Time
}

// PodContainers specifies a list of pod containers. It is not safe for concurrent use.
type PodContainers struct {
// ByID specifies all containers in a pod by container ID.
ByID map[string]*Container
// ByName specifies all containers in a pod by container name (k8s.container.name).
ByName map[string]*Container
}

// Container stores resource attributes for a specific container defined by k8s pod spec.
type Container struct {
Name string
ImageName string
ImageTag string

Expand Down Expand Up @@ -200,6 +209,7 @@ type ExtractionRules struct {
StatefulSetName bool
Node bool
StartTime bool
ContainerName bool
ContainerID bool
ContainerImageName bool
ContainerImageTag bool
Expand Down
2 changes: 2 additions & 0 deletions processor/k8sattributesprocessor/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ func withExtractMetadata(fields ...string) option {
p.rules.StatefulSetName = true
case conventions.AttributeK8SStatefulSetUID:
p.rules.StatefulSetUID = true
case conventions.AttributeK8SContainerName:
p.rules.ContainerName = true
case conventions.AttributeK8SJobName:
p.rules.JobName = true
case conventions.AttributeK8SJobUID:
Expand Down
Loading

0 comments on commit 21591e1

Please sign in to comment.