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

Discover service names from process env vars #1195

Merged
merged 8 commits into from
Sep 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion pkg/export/alloy/traces.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,14 @@ func (tr *tracesReceiver) provideLoop() (pipe.FinalFunc[[]request.Span], error)
if err != nil {
slog.Error("error fetching user defined attributes", "error", err)
}
envResourceAttrs := otel.ResourceAttrsFromEnv()

for spans := range in {
for i := range spans {
span := &spans[i]
if tr.spanDiscarded(span) {
continue
}
envResourceAttrs := otel.ResourceAttrsFromEnv(&span.ServiceID)

for _, tc := range tr.cfg.Traces {
traces := otel.GenerateTraces(span, tr.hostID, traceAttrs, envResourceAttrs)
Expand Down
19 changes: 14 additions & 5 deletions pkg/export/otel/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -340,7 +340,7 @@ func headersFromEnv(varName string) map[string]string {
headers[k] = v
}

parseOTELEnvVar(varName, addToMap)
parseOTELEnvVar(nil, varName, addToMap)

return headers
}
Expand All @@ -352,8 +352,17 @@ type varHandler func(k string, v string)
// OTEL_RESOURCE_ATTRIBUTES, i.e. a comma-separated list of
// key=values. For example: api-key=key,other-config-value=value
// The values are passed as parameters to the handler function
func parseOTELEnvVar(varName string, handler varHandler) {
envVar, ok := os.LookupEnv(varName)
func parseOTELEnvVar(svc *svc.ID, varName string, handler varHandler) {
var envVar string
ok := false

if svc != nil && svc.EnvVars != nil {
envVar, ok = svc.EnvVars[varName]
}

if !ok {
envVar, ok = os.LookupEnv(varName)
}

if !ok {
return
Expand All @@ -379,12 +388,12 @@ func parseOTELEnvVar(varName string, handler varHandler) {
}
}

func ResourceAttrsFromEnv() []attribute.KeyValue {
func ResourceAttrsFromEnv(svc *svc.ID) []attribute.KeyValue {
var otelResourceAttrs []attribute.KeyValue
apply := func(k string, v string) {
otelResourceAttrs = append(otelResourceAttrs, attribute.String(k, v))
}

parseOTELEnvVar(envResourceAttrs, apply)
parseOTELEnvVar(svc, envResourceAttrs, apply)
return otelResourceAttrs
}
45 changes: 43 additions & 2 deletions pkg/export/otel/common_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (
"testing"

"github.com/stretchr/testify/assert"

"github.com/grafana/beyla/pkg/internal/svc"
)

func TestOtlpOptions_AsMetricHTTP(t *testing.T) {
Expand Down Expand Up @@ -123,7 +125,7 @@ func TestParseOTELEnvVar(t *testing.T) {

assert.NoError(t, err)

parseOTELEnvVar(dummyVar, apply)
parseOTELEnvVar(nil, dummyVar, apply)

assert.True(t, reflect.DeepEqual(actual, tc.expected))

Expand All @@ -134,14 +136,53 @@ func TestParseOTELEnvVar(t *testing.T) {
}
}

func TestParseOTELEnvVarPerService(t *testing.T) {
type testCase struct {
envVar string
expected map[string]string
}

testCases := []testCase{
{envVar: "foo=bar", expected: map[string]string{"foo": "bar"}},
{envVar: "foo=bar,", expected: map[string]string{"foo": "bar"}},
{envVar: "foo=bar,baz", expected: map[string]string{"foo": "bar"}},
{envVar: "foo=bar,baz=baz", expected: map[string]string{"foo": "bar", "baz": "baz"}},
{envVar: "foo=bar,baz=baz ", expected: map[string]string{"foo": "bar", "baz": "baz"}},
{envVar: " foo=bar, baz=baz ", expected: map[string]string{"foo": "bar", "baz": "baz"}},
{envVar: " foo = bar , baz =baz ", expected: map[string]string{"foo": "bar", "baz": "baz"}},
{envVar: " foo = bar , baz =baz= ", expected: map[string]string{"foo": "bar", "baz": "baz="}},
{envVar: ",a=b , c=d,=", expected: map[string]string{"a": "b", "c": "d"}},
{envVar: "=", expected: map[string]string{}},
{envVar: "====", expected: map[string]string{}},
{envVar: "a====b", expected: map[string]string{"a": "===b"}},
{envVar: "", expected: map[string]string{}},
}

const dummyVar = "foo"

for _, tc := range testCases {
t.Run(fmt.Sprint(tc), func(t *testing.T) {
actual := map[string]string{}

apply := func(k string, v string) {
actual[k] = v
}

parseOTELEnvVar(&svc.ID{EnvVars: map[string]string{dummyVar: tc.envVar}}, dummyVar, apply)

assert.True(t, reflect.DeepEqual(actual, tc.expected))
})
}
}

func TestParseOTELEnvVar_nil(t *testing.T) {
actual := map[string]string{}

apply := func(k string, v string) {
actual[k] = v
}

parseOTELEnvVar("NOT_SET_VAR", apply)
parseOTELEnvVar(nil, "NOT_SET_VAR", apply)

assert.True(t, reflect.DeepEqual(actual, map[string]string{}))
}
2 changes: 1 addition & 1 deletion pkg/export/otel/metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -545,7 +545,7 @@ func (mr *MetricsReporter) setupGraphMeters(m *Metrics, meter instrument.Meter)
func (mr *MetricsReporter) newMetricSet(service *svc.ID) (*Metrics, error) {
mlog := mlog().With("service", service)
mlog.Debug("creating new Metrics reporter")
resourceAttributes := append(getAppResourceAttrs(mr.hostID, service), ResourceAttrsFromEnv()...)
resourceAttributes := append(getAppResourceAttrs(mr.hostID, service), ResourceAttrsFromEnv(service)...)
resources := resource.NewWithAttributes(semconv.SchemaURL, resourceAttributes...)

opts := []metric.Option{
Expand Down
3 changes: 1 addition & 2 deletions pkg/export/otel/traces.go
Original file line number Diff line number Diff line change
Expand Up @@ -198,14 +198,13 @@ func (tr *tracesOTELReceiver) provideLoop() (pipe.FinalFunc[[]request.Span], err
return
}

envResourceAttrs := ResourceAttrsFromEnv()

for spans := range in {
for i := range spans {
span := &spans[i]
if tr.spanDiscarded(span) {
continue
}
envResourceAttrs := ResourceAttrsFromEnv(&span.ServiceID)
traces := GenerateTraces(span, tr.ctxInfo.HostID, traceAttrs, envResourceAttrs)
err := exp.ConsumeTraces(tr.ctx, traces)
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion pkg/export/otel/traces_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -590,7 +590,7 @@ func TestGenerateTracesAttributes(t *testing.T) {
defer restoreEnvAfterExecution()()
require.NoError(t, os.Setenv(envResourceAttrs, "deployment.environment=productions,source.upstream=beyla"))
span := request.Span{Type: request.EventTypeHTTP, Method: "GET", Route: "/test", Status: 200}
traces := GenerateTraces(&span, "host-id", map[attr.Name]struct{}{}, ResourceAttrsFromEnv())
traces := GenerateTraces(&span, "host-id", map[attr.Name]struct{}{}, ResourceAttrsFromEnv(&span.ServiceID))

assert.Equal(t, 1, traces.ResourceSpans().Len())
rs := traces.ResourceSpans().At(0)
Expand Down
16 changes: 16 additions & 0 deletions pkg/internal/exec/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ type FileInfo struct {
Ns uint32
}

const (
envServiceName = "OTEL_SERVICE_NAME"
)

func (fi *FileInfo) ExecutableName() string {
parts := strings.Split(fi.CmdExePath, "/")
return parts[len(parts)-1]
Expand Down Expand Up @@ -59,5 +63,17 @@ func FindExecELF(p *services.ProcessInfo, svcID svc.ID) (*FileInfo, error) {
} else {
return nil, err
}

envVars, err := EnvVars(p.Pid)

if err != nil {
return nil, err
}

file.Service.EnvVars = envVars
if svcName, ok := file.Service.EnvVars[envServiceName]; ok {
file.Service.Name = svcName
}

return &file, nil
}
42 changes: 42 additions & 0 deletions pkg/internal/exec/procenv.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package exec

import (
"strings"

"github.com/prometheus/procfs"
)

func envStrsToMap(varsStr []string) map[string]string {
vars := make(map[string]string, len(varsStr))

for _, s := range varsStr {
keyVal := strings.SplitN(s, "=", 2)
if len(keyVal) < 2 {
continue
}
key := strings.TrimSpace(keyVal[0])
val := strings.TrimSpace(keyVal[1])

if key != "" && val != "" {
vars[key] = val
}
}

return vars
}

func EnvVars(pid int32) (map[string]string, error) {
proc, err := procfs.NewProc(int(pid))

if err != nil {
return nil, err
}

varsStr, err := proc.Environ()

if err != nil {
return nil, err
}

return envStrsToMap(varsStr), nil
}
24 changes: 24 additions & 0 deletions pkg/internal/exec/procenv_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package exec

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestEnvStrParsing(t *testing.T) {
strs := []string{
"ok=\"= =\"",
"nothing",
"=wrong",
"something=somethingelse",
"something_empty=",
"something= else",
"weird== =",
"resources=a=b,c=d,e= fg",
"",
}

res := envStrsToMap(strs)
assert.Equal(t, map[string]string{"something": "else", "ok": "\"= =\"", "weird": "= =", "resources": "a=b,c=d,e= fg"}, res)
}
2 changes: 2 additions & 0 deletions pkg/internal/svc/svc.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ type ID struct {
// by other metadata if available (e.g., Pod Name, Node Name, etc...)
HostName string

EnvVars map[string]string

flags idFlags
}

Expand Down
3 changes: 3 additions & 0 deletions test/integration/docker-compose-ruby.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ services:
image: ghcr.io/grafana/beyla-test/greeting-rails${TESTSERVER_IMAGE_SUFFIX}/0.0.1
ports:
- "${TEST_SERVICE_PORTS}"
environment:
OTEL_SERVICE_NAME: "my-ruby-app"
OTEL_RESOURCE_ATTRIBUTES: "data-center=ca,deployment-zone=to"
depends_on:
otelcol:
condition: service_started
Expand Down
16 changes: 14 additions & 2 deletions test/integration/red_test_ruby.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,18 @@ func testREDMetricsForRubyHTTPLibrary(t *testing.T, url string, comm string) {
}
})

// check that the resource attributes we passed made it for the service
test.Eventually(t, testTimeout, func(t require.TestingT) {
var err error
results, err = pq.Query(`target_info{` +
`data_center="ca",` +
`deployment_zone="to"}`)
require.NoError(t, err)
enoughPromResults(t, results)
val := totalPromCount(t, results)
assert.LessOrEqual(t, 1, val)
})

// Call 4 times the instrumented service, forcing it to:
// - process multiple calls in a row with, one more than we might need
// - returning a 200 code
Expand Down Expand Up @@ -83,7 +95,7 @@ func testREDMetricsRailsHTTP(t *testing.T) {
} {
t.Run(testCaseURL, func(t *testing.T) {
waitForRubyTestComponents(t, testCaseURL)
testREDMetricsForRubyHTTPLibrary(t, testCaseURL, "ruby")
testREDMetricsForRubyHTTPLibrary(t, testCaseURL, "my-ruby-app")
})
}
}
Expand All @@ -94,7 +106,7 @@ func testREDMetricsRailsHTTPS(t *testing.T) {
} {
t.Run(testCaseURL, func(t *testing.T) {
waitForRubyTestComponents(t, testCaseURL)
testREDMetricsForRubyHTTPLibrary(t, testCaseURL, "ruby")
testREDMetricsForRubyHTTPLibrary(t, testCaseURL, "my-ruby-app")
})
}
}
Loading