Skip to content

Commit

Permalink
Discover service names from process env vars (#1195)
Browse files Browse the repository at this point in the history
  • Loading branch information
grcevski authored Sep 30, 2024
1 parent 79d6d2b commit 47b960e
Show file tree
Hide file tree
Showing 12 changed files with 162 additions and 14 deletions.
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")
})
}
}

0 comments on commit 47b960e

Please sign in to comment.