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

[usm] service discovery, fetch only target environment variables from /proc #29574

Open
wants to merge 18 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
3039db8
[usm] service discovery, fetch only target environment variables from…
yuri-lipnesh Sep 25, 2024
8f6f4de
[usm] service discovery, improved event scanner performance
yuri-lipnesh Sep 27, 2024
09c8074
[usm] service discovery, use bufio.Scanner() to read /proc/<pid>/environ
yuri-lipnesh Sep 30, 2024
e34c68d
[usm] service discovery, comment exported variables to satisfy linter
yuri-lipnesh Sep 30, 2024
42b8cfe
[usm] service discovery, replace getEnvironPath() with kernel.HostPro…
yuri-lipnesh Oct 1, 2024
cdf477e
[usm] service discovery, add package targetenvs - source of truth for…
yuri-lipnesh Oct 3, 2024
5dcd755
[usm] service discovery, change name of const variable to satisfy linter
yuri-lipnesh Oct 3, 2024
b3acdb3
[usm] service discovery, set a build constraint 'go:build linux' in e…
yuri-lipnesh Oct 4, 2024
788c3b2
[usm] service discovery, rm ptracer.TextScannerIterator, do not impor…
yuri-lipnesh Oct 4, 2024
57dc567
[usm] service discovery, only non-linux in targetenvs pkg, add target…
yuri-lipnesh Oct 4, 2024
947e07a
[usm] service discovery, read env vars refactored, added EnvironmentV…
yuri-lipnesh Oct 8, 2024
b4b278c
[usm] service discovery, read env vars, fix linter error - remove unu…
yuri-lipnesh Oct 8, 2024
afcf84f
[usm] service discovery, read env vars, enhanced end of search in Env…
yuri-lipnesh Oct 9, 2024
6cafb58
[usm] service discovery, read env vars, remove optimization with stop…
yuri-lipnesh Oct 9, 2024
40e87c2
[usm] service discovery, read env vars, +envs/envs_testutils.go to se…
yuri-lipnesh Oct 10, 2024
44e65ff
[usm] service discovery, read env vars, pre-allocation of the map and…
yuri-lipnesh Oct 10, 2024
dcf19a7
[usm] service discovery, remove ret val in EnvReader.add(), +envs.Var…
yuri-lipnesh Oct 10, 2024
45cc90a
[usm] service discovery, read env vars, add comment to envs.Variables…
yuri-lipnesh Oct 10, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 12 additions & 11 deletions pkg/collector/corechecks/servicediscovery/apm/detect.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"strconv"
"strings"

"github.com/DataDog/datadog-agent/pkg/collector/corechecks/servicediscovery/envs"
"github.com/DataDog/datadog-agent/pkg/collector/corechecks/servicediscovery/language"
"github.com/DataDog/datadog-agent/pkg/collector/corechecks/servicediscovery/usm"
"github.com/DataDog/datadog-agent/pkg/network/go/bininspect"
Expand All @@ -37,7 +38,7 @@ const (
Injected Instrumentation = "injected"
)

type detector func(pid int, args []string, envs map[string]string, contextMap usm.DetectorContextMap) Instrumentation
type detector func(pid int, args []string, envs envs.EnvironmentVariables, contextMap usm.DetectorContextMap) Instrumentation

var (
detectorMap = map[language.Language]detector{
Expand All @@ -52,7 +53,7 @@ var (
)

// Detect attempts to detect the type of APM instrumentation for the given service.
func Detect(pid int, args []string, envs map[string]string, lang language.Language, contextMap usm.DetectorContextMap) Instrumentation {
func Detect(pid int, args []string, envs envs.EnvironmentVariables, lang language.Language, contextMap usm.DetectorContextMap) Instrumentation {
// first check to see if the DD_INJECTION_ENABLED is set to tracer
if isInjected(envs) {
return Injected
Expand All @@ -66,8 +67,8 @@ func Detect(pid int, args []string, envs map[string]string, lang language.Langua
return None
}

func isInjected(envs map[string]string) bool {
if val, ok := envs["DD_INJECTION_ENABLED"]; ok {
func isInjected(envs envs.EnvironmentVariables) bool {
if val, ok := envs.Get("DD_INJECTION_ENABLED"); ok {
parts := strings.Split(val, ",")
for _, v := range parts {
if v == "tracer" {
Expand Down Expand Up @@ -96,7 +97,7 @@ const (
// goDetector detects APM instrumentation for Go binaries by checking for
// the presence of the dd-trace-go symbols in the ELF. This only works for
// unstripped binaries.
func goDetector(pid int, _ []string, _ map[string]string, _ usm.DetectorContextMap) Instrumentation {
func goDetector(pid int, _ []string, _ envs.EnvironmentVariables, _ usm.DetectorContextMap) Instrumentation {
exePath := kernel.HostProc(strconv.Itoa(pid), "exe")

elfFile, err := elf.Open(exePath)
Expand Down Expand Up @@ -142,7 +143,7 @@ func pythonDetectorFromMapsReader(reader io.Reader) Instrumentation {
// For example:
// 7aef453fc000-7aef453ff000 rw-p 0004c000 fc:06 7895473 /home/foo/.local/lib/python3.10/site-packages/ddtrace/internal/_encoding.cpython-310-x86_64-linux-gnu.so
// 7aef45400000-7aef45459000 r--p 00000000 fc:06 7895588 /home/foo/.local/lib/python3.10/site-packages/ddtrace/internal/datadog/profiling/libdd_wrapper.so
func pythonDetector(pid int, _ []string, _ map[string]string, _ usm.DetectorContextMap) Instrumentation {
func pythonDetector(pid int, _ []string, _ envs.EnvironmentVariables, _ usm.DetectorContextMap) Instrumentation {
mapsPath := kernel.HostProc(strconv.Itoa(pid), "maps")
mapsFile, err := os.Open(mapsPath)
if err != nil {
Expand Down Expand Up @@ -172,7 +173,7 @@ func isNodeInstrumented(f fs.File) bool {
// To check for APM instrumentation, we try to find a package.json in
// the parent directories of the service. If found, we then check for a
// `dd-trace` entry to be present.
func nodeDetector(_ int, _ []string, _ map[string]string, contextMap usm.DetectorContextMap) Instrumentation {
func nodeDetector(_ int, _ []string, _ envs.EnvironmentVariables, contextMap usm.DetectorContextMap) Instrumentation {
pkgJSONPath, ok := contextMap[usm.NodePackageJSONPath]
if !ok {
log.Debugf("could not get package.json path from context map")
Expand All @@ -199,7 +200,7 @@ func nodeDetector(_ int, _ []string, _ map[string]string, contextMap usm.Detecto
return None
}

func javaDetector(_ int, args []string, envs map[string]string, _ usm.DetectorContextMap) Instrumentation {
func javaDetector(_ int, args []string, envs envs.EnvironmentVariables, _ usm.DetectorContextMap) Instrumentation {
ignoreArgs := map[string]bool{
"-version": true,
"-Xshare:dump": true,
Expand Down Expand Up @@ -229,7 +230,7 @@ func javaDetector(_ int, args []string, envs map[string]string, _ usm.DetectorCo
"JDPA_OPTS",
}
for _, name := range toolOptionEnvs {
if val, ok := envs[name]; ok {
if val, ok := envs.Get(name); ok {
if strings.Contains(val, "-javaagent:") && strings.Contains(val, "dd-java-agent.jar") {
return Provided
}
Expand Down Expand Up @@ -265,8 +266,8 @@ func dotNetDetectorFromMapsReader(reader io.Reader) Instrumentation {
// maps file. Note that this does not work for single-file deployments.
//
// 785c8a400000-785c8aaeb000 r--s 00000000 fc:06 12762267 /home/foo/.../publish/Datadog.Trace.dll
func dotNetDetector(pid int, _ []string, envs map[string]string, _ usm.DetectorContextMap) Instrumentation {
if val, ok := envs["CORECLR_ENABLE_PROFILING"]; ok && val == "1" {
func dotNetDetector(pid int, _ []string, envs envs.EnvironmentVariables, _ usm.DetectorContextMap) Instrumentation {
if val, ok := envs.Get("CORECLR_ENABLE_PROFILING"); ok && val == "1" {
return Provided
}

Expand Down
23 changes: 12 additions & 11 deletions pkg/collector/corechecks/servicediscovery/apm/detect_nix_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/DataDog/datadog-agent/pkg/collector/corechecks/servicediscovery/envs"
"github.com/DataDog/datadog-agent/pkg/collector/corechecks/servicediscovery/usm"
"github.com/DataDog/datadog-agent/pkg/network/protocols/http/testutil"
usmtestutil "github.com/DataDog/datadog-agent/pkg/network/usm/testutil"
Expand Down Expand Up @@ -54,7 +55,7 @@ func TestInjected(t *testing.T) {
}
for _, d := range data {
t.Run(d.name, func(t *testing.T) {
result := isInjected(d.envs)
result := isInjected(envs.NewEnvironmentVariables(d.envs))
assert.Equal(t, d.result, result)
})
}
Expand Down Expand Up @@ -93,7 +94,7 @@ func Test_javaDetector(t *testing.T) {
}
for _, d := range data {
t.Run(d.name, func(t *testing.T) {
result := javaDetector(0, d.args, d.envs, nil)
result := javaDetector(0, d.args, envs.NewEnvironmentVariables(d.envs), nil)
if result != d.result {
t.Errorf("expected %s got %s", d.result, result)
}
Expand Down Expand Up @@ -130,7 +131,7 @@ func Test_nodeDetector(t *testing.T) {

for _, d := range data {
t.Run(d.name, func(t *testing.T) {
result := nodeDetector(0, nil, nil, d.contextMap)
result := nodeDetector(0, nil, envs.NewEnvironmentVariables(nil), d.contextMap)
assert.Equal(t, d.result, result)
})
}
Expand Down Expand Up @@ -176,7 +177,7 @@ func Test_pythonDetector(t *testing.T) {
func TestDotNetDetector(t *testing.T) {
for _, test := range []struct {
name string
env map[string]string
envs map[string]string
maps string
result Instrumentation
}{
Expand All @@ -186,14 +187,14 @@ func TestDotNetDetector(t *testing.T) {
},
{
name: "profiling disabled",
env: map[string]string{
envs: map[string]string{
"CORECLR_ENABLE_PROFILING": "0",
},
result: None,
},
{
name: "profiling enabled",
env: map[string]string{
envs: map[string]string{
"CORECLR_ENABLE_PROFILING": "1",
},
result: Provided,
Expand All @@ -218,7 +219,7 @@ func TestDotNetDetector(t *testing.T) {
},
{
name: "in maps, env misleading",
env: map[string]string{
envs: map[string]string{
"CORECLR_ENABLE_PROFILING": "0",
},
maps: `
Expand All @@ -230,7 +231,7 @@ func TestDotNetDetector(t *testing.T) {
t.Run(test.name, func(t *testing.T) {
var result Instrumentation
if test.maps == "" {
result = dotNetDetector(0, nil, test.env, nil)
result = dotNetDetector(0, nil, envs.NewEnvironmentVariables(test.envs), nil)
} else {
result = dotNetDetectorFromMapsReader(strings.NewReader(test.maps))
}
Expand Down Expand Up @@ -259,12 +260,12 @@ func TestGoDetector(t *testing.T) {
_ = cmdWithoutSymbols.Process.Kill()
})

result := goDetector(os.Getpid(), nil, nil, nil)
result := goDetector(os.Getpid(), nil, envs.NewEnvironmentVariables(nil), nil)
require.Equal(t, None, result)

result = goDetector(cmdWithSymbols.Process.Pid, nil, nil, nil)
result = goDetector(cmdWithSymbols.Process.Pid, nil, envs.NewEnvironmentVariables(nil), nil)
require.Equal(t, Provided, result)

result = goDetector(cmdWithoutSymbols.Process.Pid, nil, nil, nil)
result = goDetector(cmdWithoutSymbols.Process.Pid, nil, envs.NewEnvironmentVariables(nil), nil)
require.Equal(t, Provided, result)
}
86 changes: 86 additions & 0 deletions pkg/collector/corechecks/servicediscovery/envs/envs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// Unless explicitly stated otherwise all files in this repository are licensed
// under the Apache License Version 2.0.
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2024-present Datadog, Inc.

// Package envs provides target environment variables of interest.
package envs

import (
"fmt"
)

// targets is a collection of environment variables of interest.
var targets = map[string]struct{}{
"PWD": {},
"DD_INJECTION_ENABLED": {},
"DD_SERVICE": {},
"DD_TAGS": {},
"DD_DISCOVERY_ENABLED": {},
"GUNICORN_CMD_ARGS": {},
"WSGI_APP": {},
"CORECLR_ENABLE_PROFILING": {},
"CATALINA_OPTS": {},
"JAVA_TOOL_OPTIONS": {},
"_JAVA_OPTIONS": {},
"JDK_JAVA_OPTIONS": {},
"JAVA_OPTIONS": {},
"JDPA_OPTS": {},
"SPRING_APPLICATION_NAME": {},
"SPRING_CONFIG_LOCATIONS": {},
"SPRING_CONFIG_NAME": {},
"SPRING_PROFILES_ACTIVE": {},
}

// GetExpectedEnvs - return list of expected env. variables for testing.
func GetExpectedEnvs() ([]string, map[string]string) {
yuri-lipnesh marked this conversation as resolved.
Show resolved Hide resolved
var expectedEnvs []string
var expectedMap = make(map[string]string)
yuri-lipnesh marked this conversation as resolved.
Show resolved Hide resolved

for env := range targets {
expectedEnvs = append(expectedEnvs, fmt.Sprintf("%s=true", env))
expectedMap[env] = "true"
}
return expectedEnvs, expectedMap
}

// EnvironmentVariables - collected of targeted environment variables.
type EnvironmentVariables struct {
yuri-lipnesh marked this conversation as resolved.
Show resolved Hide resolved
vars map[string]string
}

// NewEnvironmentVariables returns a new [EnvironmentVariables] to collect env. variables.
func NewEnvironmentVariables(vars map[string]string) EnvironmentVariables {
return EnvironmentVariables{
vars: vars,
}
}

// GetVars returns the collected environment variables
func (ev *EnvironmentVariables) GetVars() map[string]string {
return ev.vars
}
yuri-lipnesh marked this conversation as resolved.
Show resolved Hide resolved

// Get returns an environment variable if it is present in the collection
func (ev *EnvironmentVariables) Get(name string) (string, bool) {
if _, ok := targets[name]; !ok {
return "", false
}

val, ok := ev.vars[name]
return val, ok
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

imho, vars should reflect only the allowed environment variables
no need to store (and waste memory) unneeded envs + currently you always pay for 2 map accesses per "supported" environment variable instead of only one map access

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If vars is guaranteed to have only supported environment variables, then no need to check targets first

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is where unnecessary environment variables are filtered out and ultimately only the requested variables are included in the list.

}

// Set saves the environment variable if it is targeted.
// returns true if env variable matches the target
yuri-lipnesh marked this conversation as resolved.
Show resolved Hide resolved
func (ev *EnvironmentVariables) Set(name string, val string) bool {
yuri-lipnesh marked this conversation as resolved.
Show resolved Hide resolved
if _, ok := targets[name]; !ok {
return false
}
if ev.vars == nil {
ev.vars = make(map[string]string)
}
ev.vars[name] = val

return false
yuri-lipnesh marked this conversation as resolved.
Show resolved Hide resolved
}
88 changes: 87 additions & 1 deletion pkg/collector/corechecks/servicediscovery/module/envs.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,18 @@
package module

import (
"bufio"
"io"
"os"
"path/filepath"
"strconv"
"strings"

"github.com/shirou/gopsutil/v3/process"

"github.com/DataDog/datadog-agent/pkg/collector/corechecks/servicediscovery/envs"
"github.com/DataDog/datadog-agent/pkg/util/kernel"
"github.com/DataDog/datadog-agent/pkg/util/log"
"github.com/shirou/gopsutil/v3/process"
)

const (
Expand Down Expand Up @@ -153,3 +156,86 @@ func getEnvs(proc *process.Process) (map[string]string, error) {
}
return envs, nil
}

// EnvReader reads the environment variables from /proc/<pid>/environ file.
type EnvReader struct {
file *os.File // open pointer to environment variables file
scanner *bufio.Scanner // iterator to read strings from text file
envs envs.EnvironmentVariables // collected environment variables
}

func zeroSplitter(data []byte, atEOF bool) (advance int, token []byte, err error) {
for i := 0; i < len(data); i++ {
if data[i] == '\x00' {
return i + 1, data[:i], nil
}
}
if !atEOF {
return 0, nil, nil
}
return 0, data, bufio.ErrFinalToken
}

// newEnvReader returns a new [EnvReader] to read from path, it reads null terminated strings.
func newEnvReader(proc *process.Process) (*EnvReader, error) {
envPath := kernel.HostProc(strconv.Itoa(int(proc.Pid)), "environ")
file, err := os.Open(envPath)
if err != nil {
return nil, err
}

scanner := bufio.NewScanner(file)
scanner.Split(zeroSplitter)

return &EnvReader{
file: file,
scanner: scanner,
envs: envs.NewEnvironmentVariables(nil),
}, nil
}

// finish closes an open file.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

neat: rename to Close

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

func (er *EnvReader) finish() {
if er.file != nil {
er.file.Close()
}
}

// add adds env. variable to the map of environment variables,
// returns true if reading should be stopped.
yuri-lipnesh marked this conversation as resolved.
Show resolved Hide resolved
func (er *EnvReader) add() bool {
env := er.scanner.Text()
name, val, found := strings.Cut(env, "=")
if found {
return er.envs.Set(name, val)
}
return false
}

// getTargetEnvs reads the environment variables of interest from the /proc/<pid>/environ file.
func getTargetEnvs(proc *process.Process) (envs.EnvironmentVariables, error) {
er, err := newEnvReader(proc)
defer er.finish()
yuri-lipnesh marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return envs.NewEnvironmentVariables(nil), err
}

for er.scanner.Scan() {
stop := er.add()
if stop {
break
}
}

injectionMeta, ok := getInjectionMeta(proc)
if !ok {
return er.envs, nil
}
for _, env := range injectionMeta.InjectedEnv {
name, val, found := strings.Cut(string(env), "=")
if found {
er.envs.Set(name, val)
}
}
return er.envs, nil
}
Loading
Loading