From 47b960ec55fd701b90f5c5005e824b26cc5c7bb6 Mon Sep 17 00:00:00 2001 From: Nikola Grcevski <6207777+grcevski@users.noreply.github.com> Date: Mon, 30 Sep 2024 08:55:23 -0400 Subject: [PATCH] Discover service names from process env vars (#1195) --- pkg/export/alloy/traces.go | 2 +- pkg/export/otel/common.go | 19 +++++++--- pkg/export/otel/common_test.go | 45 ++++++++++++++++++++++-- pkg/export/otel/metrics.go | 2 +- pkg/export/otel/traces.go | 3 +- pkg/export/otel/traces_test.go | 2 +- pkg/internal/exec/file.go | 16 +++++++++ pkg/internal/exec/procenv.go | 42 ++++++++++++++++++++++ pkg/internal/exec/procenv_test.go | 24 +++++++++++++ pkg/internal/svc/svc.go | 2 ++ test/integration/docker-compose-ruby.yml | 3 ++ test/integration/red_test_ruby.go | 16 +++++++-- 12 files changed, 162 insertions(+), 14 deletions(-) create mode 100644 pkg/internal/exec/procenv.go create mode 100644 pkg/internal/exec/procenv_test.go diff --git a/pkg/export/alloy/traces.go b/pkg/export/alloy/traces.go index 92d2dc4db..55ff700d0 100644 --- a/pkg/export/alloy/traces.go +++ b/pkg/export/alloy/traces.go @@ -44,7 +44,6 @@ 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 { @@ -52,6 +51,7 @@ func (tr *tracesReceiver) provideLoop() (pipe.FinalFunc[[]request.Span], error) if tr.spanDiscarded(span) { continue } + envResourceAttrs := otel.ResourceAttrsFromEnv(&span.ServiceID) for _, tc := range tr.cfg.Traces { traces := otel.GenerateTraces(span, tr.hostID, traceAttrs, envResourceAttrs) diff --git a/pkg/export/otel/common.go b/pkg/export/otel/common.go index 0549788e4..6e7e57297 100644 --- a/pkg/export/otel/common.go +++ b/pkg/export/otel/common.go @@ -340,7 +340,7 @@ func headersFromEnv(varName string) map[string]string { headers[k] = v } - parseOTELEnvVar(varName, addToMap) + parseOTELEnvVar(nil, varName, addToMap) return headers } @@ -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 @@ -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 } diff --git a/pkg/export/otel/common_test.go b/pkg/export/otel/common_test.go index 4a969c2f3..1a32caacf 100644 --- a/pkg/export/otel/common_test.go +++ b/pkg/export/otel/common_test.go @@ -7,6 +7,8 @@ import ( "testing" "github.com/stretchr/testify/assert" + + "github.com/grafana/beyla/pkg/internal/svc" ) func TestOtlpOptions_AsMetricHTTP(t *testing.T) { @@ -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)) @@ -134,6 +136,45 @@ 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{} @@ -141,7 +182,7 @@ func TestParseOTELEnvVar_nil(t *testing.T) { actual[k] = v } - parseOTELEnvVar("NOT_SET_VAR", apply) + parseOTELEnvVar(nil, "NOT_SET_VAR", apply) assert.True(t, reflect.DeepEqual(actual, map[string]string{})) } diff --git a/pkg/export/otel/metrics.go b/pkg/export/otel/metrics.go index 22c6e9661..5529623f6 100644 --- a/pkg/export/otel/metrics.go +++ b/pkg/export/otel/metrics.go @@ -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{ diff --git a/pkg/export/otel/traces.go b/pkg/export/otel/traces.go index 0f2a4e807..1ac591c78 100644 --- a/pkg/export/otel/traces.go +++ b/pkg/export/otel/traces.go @@ -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 { diff --git a/pkg/export/otel/traces_test.go b/pkg/export/otel/traces_test.go index 877ce4aba..60e3c4fc8 100644 --- a/pkg/export/otel/traces_test.go +++ b/pkg/export/otel/traces_test.go @@ -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) diff --git a/pkg/internal/exec/file.go b/pkg/internal/exec/file.go index 91f42277f..564e08b58 100644 --- a/pkg/internal/exec/file.go +++ b/pkg/internal/exec/file.go @@ -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] @@ -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 } diff --git a/pkg/internal/exec/procenv.go b/pkg/internal/exec/procenv.go new file mode 100644 index 000000000..ed230cb46 --- /dev/null +++ b/pkg/internal/exec/procenv.go @@ -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 +} diff --git a/pkg/internal/exec/procenv_test.go b/pkg/internal/exec/procenv_test.go new file mode 100644 index 000000000..ba711a28b --- /dev/null +++ b/pkg/internal/exec/procenv_test.go @@ -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) +} diff --git a/pkg/internal/svc/svc.go b/pkg/internal/svc/svc.go index b9cd55c33..bb8b71c95 100644 --- a/pkg/internal/svc/svc.go +++ b/pkg/internal/svc/svc.go @@ -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 } diff --git a/test/integration/docker-compose-ruby.yml b/test/integration/docker-compose-ruby.yml index b01fb83d5..a81501757 100644 --- a/test/integration/docker-compose-ruby.yml +++ b/test/integration/docker-compose-ruby.yml @@ -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 diff --git a/test/integration/red_test_ruby.go b/test/integration/red_test_ruby.go index 3ceeafd94..29e88a736 100644 --- a/test/integration/red_test_ruby.go +++ b/test/integration/red_test_ruby.go @@ -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 @@ -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") }) } } @@ -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") }) } }