Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add resource attributes to collector sidecar #832

Merged
merged 10 commits into from
Apr 28, 2022
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,8 @@ spec:
EOF
```

When using sidecar mode the OpenTelemetry collector container will have the environment variable `OTEL_RESOURCE_ATTRIBUTES`set with Kubernetes resource attributes, ready to be consumed by the [resourcedetection](https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/processor/resourcedetectionprocessor) processor.

rubenvp8510 marked this conversation as resolved.
Show resolved Hide resolved
### OpenTelemetry auto-instrumentation injection

The operator can inject and configure OpenTelemetry auto-instrumentation libraries. Currently Java, NodeJS and Python are supported.
Expand Down
28 changes: 28 additions & 0 deletions pkg/constants/env.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Copyright The OpenTelemetry Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package constants

const (
EnvOTELServiceName = "OTEL_SERVICE_NAME"
EnvOTELExporterOTLPEndpoint = "OTEL_EXPORTER_OTLP_ENDPOINT"
EnvOTELResourceAttrs = "OTEL_RESOURCE_ATTRIBUTES"
EnvOTELPropagators = "OTEL_PROPAGATORS"
EnvOTELTracesSampler = "OTEL_TRACES_SAMPLER"
EnvOTELTracesSamplerArg = "OTEL_TRACES_SAMPLER_ARG"

EnvPodName = "OTEL_RESOURCE_ATTRIBUTES_POD_NAME"
EnvPodUID = "OTEL_RESOURCE_ATTRIBUTES_POD_UID"
EnvNodeName = "OTEL_RESOURCE_ATTRIBUTES_NODE_NAME"
)
50 changes: 20 additions & 30 deletions pkg/instrumentation/sdk.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,22 +31,12 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client"

"github.com/open-telemetry/opentelemetry-operator/apis/v1alpha1"
"github.com/open-telemetry/opentelemetry-operator/pkg/constants"
)

const (
volumeName = "opentelemetry-auto-instrumentation"
initContainerName = "opentelemetry-auto-instrumentation"

envOTELServiceName = "OTEL_SERVICE_NAME"
envOTELExporterOTLPEndpoint = "OTEL_EXPORTER_OTLP_ENDPOINT"
envOTELResourceAttrs = "OTEL_RESOURCE_ATTRIBUTES"
envOTELPropagators = "OTEL_PROPAGATORS"
envOTELTracesSampler = "OTEL_TRACES_SAMPLER"
envOTELTracesSamplerArg = "OTEL_TRACES_SAMPLER_ARG"

envPodName = "OTEL_RESOURCE_ATTRIBUTES_POD_NAME"
envPodUID = "OTEL_RESOURCE_ATTRIBUTES_POD_UID"
envNodeName = "OTEL_RESOURCE_ATTRIBUTES_NODE_NAME"
)

// inject a new sidecar container to the given pod, based on the given OpenTelemetryCollector.
Expand Down Expand Up @@ -101,18 +91,18 @@ func (i *sdkInjector) injectCommonEnvVar(otelinst v1alpha1.Instrumentation, pod
func (i *sdkInjector) injectCommonSDKConfig(ctx context.Context, otelinst v1alpha1.Instrumentation, ns corev1.Namespace, pod corev1.Pod) corev1.Pod {
container := &pod.Spec.Containers[0]
resourceMap := i.createResourceMap(ctx, otelinst, ns, pod)
idx := getIndexOfEnv(container.Env, envOTELServiceName)
idx := getIndexOfEnv(container.Env, constants.EnvOTELServiceName)
if idx == -1 {
container.Env = append(container.Env, corev1.EnvVar{
Name: envOTELServiceName,
Name: constants.EnvOTELServiceName,
Value: chooseServiceName(pod, resourceMap),
})
}
if otelinst.Spec.Exporter.Endpoint != "" {
idx = getIndexOfEnv(container.Env, envOTELExporterOTLPEndpoint)
idx = getIndexOfEnv(container.Env, constants.EnvOTELExporterOTLPEndpoint)
if idx == -1 {
container.Env = append(container.Env, corev1.EnvVar{
Name: envOTELExporterOTLPEndpoint,
Name: constants.EnvOTELExporterOTLPEndpoint,
Value: otelinst.Spec.Endpoint,
})
}
Expand All @@ -121,45 +111,45 @@ func (i *sdkInjector) injectCommonSDKConfig(ctx context.Context, otelinst v1alph
// Some attributes might be empty, we should get them via k8s downward API
if resourceMap[string(semconv.K8SPodNameKey)] == "" {
container.Env = append(container.Env, corev1.EnvVar{
Name: envPodName,
Name: constants.EnvPodName,
ValueFrom: &corev1.EnvVarSource{
FieldRef: &corev1.ObjectFieldSelector{
FieldPath: "metadata.name",
},
},
})
resourceMap[string(semconv.K8SPodNameKey)] = fmt.Sprintf("$(%s)", envPodName)
resourceMap[string(semconv.K8SPodNameKey)] = fmt.Sprintf("$(%s)", constants.EnvPodName)
}
if otelinst.Spec.Resource.AddK8sUIDAttributes {
if resourceMap[string(semconv.K8SPodUIDKey)] == "" {
container.Env = append(container.Env, corev1.EnvVar{
Name: envPodUID,
Name: constants.EnvPodUID,
ValueFrom: &corev1.EnvVarSource{
FieldRef: &corev1.ObjectFieldSelector{
FieldPath: "metadata.uid",
},
},
})
resourceMap[string(semconv.K8SPodUIDKey)] = fmt.Sprintf("$(%s)", envPodUID)
resourceMap[string(semconv.K8SPodUIDKey)] = fmt.Sprintf("$(%s)", constants.EnvPodUID)
}
}
if resourceMap[string(semconv.K8SNodeNameKey)] == "" {
container.Env = append(container.Env, corev1.EnvVar{
Name: envNodeName,
Name: constants.EnvNodeName,
ValueFrom: &corev1.EnvVarSource{
FieldRef: &corev1.ObjectFieldSelector{
FieldPath: "spec.nodeName",
},
},
})
resourceMap[string(semconv.K8SNodeNameKey)] = fmt.Sprintf("$(%s)", envNodeName)
resourceMap[string(semconv.K8SNodeNameKey)] = fmt.Sprintf("$(%s)", constants.EnvNodeName)
}

idx = getIndexOfEnv(container.Env, envOTELResourceAttrs)
idx = getIndexOfEnv(container.Env, constants.EnvOTELResourceAttrs)
resStr := resourceMapToStr(resourceMap)
if idx == -1 {
container.Env = append(container.Env, corev1.EnvVar{
Name: envOTELResourceAttrs,
Name: constants.EnvOTELResourceAttrs,
Value: resStr,
})
} else {
Expand All @@ -169,27 +159,27 @@ func (i *sdkInjector) injectCommonSDKConfig(ctx context.Context, otelinst v1alph
container.Env[idx].Value += resStr
}

idx = getIndexOfEnv(container.Env, envOTELPropagators)
idx = getIndexOfEnv(container.Env, constants.EnvOTELPropagators)
if idx == -1 && len(otelinst.Spec.Propagators) > 0 {
propagators := *(*[]string)((unsafe.Pointer(&otelinst.Spec.Propagators)))
container.Env = append(container.Env, corev1.EnvVar{
Name: envOTELPropagators,
Name: constants.EnvOTELPropagators,
Value: strings.Join(propagators, ","),
})
}

idx = getIndexOfEnv(container.Env, envOTELTracesSampler)
idx = getIndexOfEnv(container.Env, constants.EnvOTELTracesSampler)
// configure sampler only if it is configured in the CR
if idx == -1 && otelinst.Spec.Sampler.Type != "" {
idxSamplerArg := getIndexOfEnv(container.Env, envOTELTracesSamplerArg)
idxSamplerArg := getIndexOfEnv(container.Env, constants.EnvOTELTracesSamplerArg)
if idxSamplerArg == -1 {
container.Env = append(container.Env, corev1.EnvVar{
Name: envOTELTracesSampler,
Name: constants.EnvOTELTracesSampler,
Value: string(otelinst.Spec.Sampler.Type),
})
if otelinst.Spec.Sampler.Argument != "" {
container.Env = append(container.Env, corev1.EnvVar{
Name: envOTELTracesSamplerArg,
Name: constants.EnvOTELTracesSamplerArg,
Value: otelinst.Spec.Sampler.Argument,
})
}
Expand Down Expand Up @@ -223,7 +213,7 @@ func chooseServiceName(pod corev1.Pod, resources map[string]string) string {
func (i *sdkInjector) createResourceMap(ctx context.Context, otelinst v1alpha1.Instrumentation, ns corev1.Namespace, pod corev1.Pod) map[string]string {
// get existing resources env var and parse it into a map
existingRes := map[string]bool{}
existingResourceEnvIdx := getIndexOfEnv(pod.Spec.Containers[0].Env, envOTELResourceAttrs)
existingResourceEnvIdx := getIndexOfEnv(pod.Spec.Containers[0].Env, constants.EnvOTELResourceAttrs)
if existingResourceEnvIdx > -1 {
existingResArr := strings.Split(pod.Spec.Containers[0].Env[existingResourceEnvIdx].Value, ",")
for _, kv := range existingResArr {
Expand Down
120 changes: 120 additions & 0 deletions pkg/sidecar/attributes.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
// Copyright The OpenTelemetry Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Package sidecar contains operations related to sidecar manipulation (Add, update, remove).
package sidecar

import (
"fmt"
"sort"
"strings"

"go.opentelemetry.io/otel/attribute"
semconv "go.opentelemetry.io/otel/semconv/v1.7.0"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"

"github.com/open-telemetry/opentelemetry-operator/pkg/constants"
)

const resourceAttributesEnvName = "OTEL_RESOURCE_ATTRIBUTES"

type podReferences struct {
replicaset *appsv1.ReplicaSet
deployment *appsv1.Deployment
}

// getResourceAttributesEnv returns a list of environment variables. The list contains OTEL_RESOURCE_ATTRIBUTES and additional environment variables that use Kubernetes downward API to read pod specification.
// see: https://kubernetes.io/docs/tasks/inject-data-application/environment-variable-expose-pod-information/
func getResourceAttributesEnv(ns corev1.Namespace, podReferences podReferences) []corev1.EnvVar {

var envvars []corev1.EnvVar

attributes := map[attribute.Key]string{
semconv.K8SPodNameKey: fmt.Sprintf("$(%s)", constants.EnvPodName),
semconv.K8SPodUIDKey: fmt.Sprintf("$(%s)", constants.EnvPodUID),
semconv.K8SNodeNameKey: fmt.Sprintf("$(%s)", constants.EnvNodeName),
semconv.K8SNamespaceNameKey: ns.Name,
}

if podReferences.deployment != nil {
attributes[semconv.K8SDeploymentUIDKey] = string(podReferences.deployment.UID)
attributes[semconv.K8SDeploymentNameKey] = string(podReferences.deployment.Name)
}

if podReferences.replicaset != nil {
attributes[semconv.K8SReplicaSetUIDKey] = string(podReferences.replicaset.UID)
attributes[semconv.K8SReplicaSetNameKey] = string(podReferences.replicaset.Name)
}

envvars = append(envvars, corev1.EnvVar{
Name: constants.EnvPodName,
ValueFrom: &corev1.EnvVarSource{
FieldRef: &corev1.ObjectFieldSelector{
FieldPath: "metadata.name",
},
},
})

envvars = append(envvars, corev1.EnvVar{
Name: constants.EnvPodUID,
ValueFrom: &corev1.EnvVarSource{
FieldRef: &corev1.ObjectFieldSelector{
FieldPath: "metadata.uid",
},
},
})

envvars = append(envvars, corev1.EnvVar{
Name: constants.EnvNodeName,
ValueFrom: &corev1.EnvVarSource{
FieldRef: &corev1.ObjectFieldSelector{
FieldPath: "spec.nodeName",
},
},
})

envvars = append(envvars, corev1.EnvVar{
Name: resourceAttributesEnvName,
Value: mapToValue(attributes),
})

return envvars
}

func mapToValue(attributesMap map[attribute.Key]string) string {
var parts []string

// Sort it to make it predictable
keys := make([]string, 0, len(attributesMap))
for k := range attributesMap {
keys = append(keys, string(k))
}
sort.Strings(keys)

for _, key := range keys {
parts = append(parts, fmt.Sprintf("%s=%s", key, attributesMap[attribute.Key(key)]))
}
return strings.Join(parts, ",")
}

// check if container doesn't have already the OTEL_RESOURCE_ATTRIBUTES, we don't want to override it if it's already specified.
func hasResourceAttributeEnvVar(envvars []corev1.EnvVar) bool {
for _, env := range envvars {
if env.Name == resourceAttributesEnvName {
return true
}
}
return false
}
Loading