Repository: open-telemetry/opentelemetry-go-contrib (go)

Issue:
Support Environment Variable Installation and Configuration of Jaeger Remote Sampler
### Problem Statement
The jaeger remote sampler does not currently support auto installation or configuration using the standard `OTEL_TRACES_SAMPLER` and `OTEL_TRACES_SAMPLER_ARG` environment variables (see [Environment Variable Specification](https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/)). Although this is not explicitly required by Otel specifications, it is a common practice that allows an application's tracing to be configured for different contexts without recompilation.

### Proposed Solution

The OTel golang SDK should automatically use the jaeger remote sampler when `OTEL_TRACES_SAMPLER` equals `jaeger_remote`.

The installed Jaeger Remote Sampler should use configuration values drawn by parsing `OTEL_TRACES_SAMPLER_ARG` as a comma separated list of `key=value` pairs that include keys `endpoint`, `pollingIntervalMs`, and `initialSamplingRate`.

> For jaeger_remote and parentbased_jaeger_remote: The value [of OTEL_TRACES_SAMPLER_ARG] is a comma separated list:
> - endpoint: the endpoint in form of scheme://host:port of gRPC server that serves the sampling strategy for the service ([sampling.proto](https://github.com/jaegertracing/jaeger-idl/blob/master/proto/api_v2/sampling.proto)).
> - pollingIntervalMs: in milliseconds indicating how often the sampler will poll the backend for updates to sampling strategy.
> - initialSamplingRate: in the [0..1] range, which is used as the sampling probability when the backend cannot be reached to retrieve a sampling strategy. This value stops having an effect once a sampling strategy is retrieved successfully, as the remote strategy will be used until a new update is retrieved.
>
> Example: endpoint=http://localhost:14250,pollingIntervalMs=5000,initialSamplingRate=0.25

[Reference: Docs - Specs - OTel 1.38.0 - Configuration - Env var](https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/)



Pull request:
Support Environment Variable for Jaeger Remote Sampler
## Which problem is this PR solving?
Resolves #6256 

## Description of the changes
- Added capability to configure jaeger remote sampler using `OTEL_TRACES_SAMPLER_ARG`
- Environment variables would be applied before the code values for the corresponding keys.
- If the corresponding values exist in code, we take those.

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 29e95a2c4..bfcacfc5e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,10 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
 
 ## [Unreleased]
 
+### Added
+
+- Added support for providing `endpoint`, `pollingIntervalMs` and `initialSamplingRate` using environment variable `OTEL_TRACES_SAMPLER_ARG` in `go.opentelemetry.io/contrib/samples/jaegerremote`. (#6310)
+
 <!-- Released section -->
 <!-- Don't change this section unless doing release -->
 
diff --git a/samplers/jaegerremote/sampler_remote_options.go b/samplers/jaegerremote/sampler_remote_options.go
index ffb260570..2922ec648 100644
--- a/samplers/jaegerremote/sampler_remote_options.go
+++ b/samplers/jaegerremote/sampler_remote_options.go
@@ -19,6 +19,10 @@
 package jaegerremote // import "go.opentelemetry.io/contrib/samplers/jaegerremote"
 
 import (
+	"fmt"
+	"os"
+	"strconv"
+	"strings"
 	"time"
 
 	"github.com/go-logr/logr"
@@ -37,6 +41,45 @@ type config struct {
 	logger                  logr.Logger
 }
 
+func getEnvOptions() ([]Option, []error) {
+	var options []Option
+	// list of errors which will be logged once logger is set by the user
+	var errs []error
+
+	args := strings.Split(os.Getenv("OTEL_TRACES_SAMPLER_ARG"), ",")
+	for _, arg := range args {
+		keyValue := strings.Split(arg, "=")
+		if len(keyValue) != 2 {
+			errs = append(errs, fmt.Errorf("argument %s is not of type '<key>=<value>'", arg))
+			continue
+		}
+		key := strings.Trim(keyValue[0], " ")
+		value := strings.Trim(keyValue[1], " ")
+
+		switch key {
+		case "endpoint":
+			options = append(options, WithSamplingServerURL(value))
+		case "pollingIntervalMs":
+			intervalMs, err := strconv.Atoi(value)
+			if err != nil {
+				errs = append(errs, fmt.Errorf("%s parsing failed with :%w", key, err))
+				continue
+			}
+			options = append(options, WithSamplingRefreshInterval(time.Duration(intervalMs)*time.Millisecond))
+		case "initialSamplingRate":
+			samplingRate, err := strconv.ParseFloat(value, 64)
+			if err != nil {
+				errs = append(errs, fmt.Errorf("%s parsing failed with :%w", key, err))
+				continue
+			}
+			options = append(options, WithInitialSampler(trace.TraceIDRatioBased(samplingRate)))
+		default:
+			errs = append(errs, fmt.Errorf("invalid argument %s in OTEL_TRACE_SAMPLER_ARG", key))
+		}
+	}
+	return options, errs
+}
+
 // newConfig returns an appropriately configured config.
 func newConfig(options ...Option) config {
 	c := config{
@@ -55,9 +98,20 @@ func newConfig(options ...Option) config {
 		},
 		logger: logr.Discard(),
 	}
+
+	envOptions, errs := getEnvOptions()
+	for _, option := range envOptions {
+		option.apply(&c)
+	}
+
 	for _, option := range options {
 		option.apply(&c)
 	}
+
+	for _, err := range errs {
+		c.logger.Error(err, "env variable parsing failure")
+	}
+
 	c.updaters = append([]samplerUpdater{&perOperationSamplerUpdater{
 		MaxOperations:            c.posParams.MaxOperations,
 		OperationNameLateBinding: c.posParams.OperationNameLateBinding,


Test patch:
diff --git a/samplers/jaegerremote/sampler_remote_test.go b/samplers/jaegerremote/sampler_remote_test.go
index 79d711a5f..93b16da46 100644
--- a/samplers/jaegerremote/sampler_remote_test.go
+++ b/samplers/jaegerremote/sampler_remote_test.go
@@ -595,3 +595,62 @@ func TestDefaultSamplingStrategyFetcher_Timeout(t *testing.T) {
 	fetcher := newHTTPSamplingStrategyFetcher("")
 	assert.Equal(t, defaultRemoteSamplingTimeout, fetcher.httpClient.Timeout)
 }
+
+func TestEnvVarSettingForNewTracer(t *testing.T) {
+	type testConfig struct {
+		samplingServerURL       string
+		samplingRefreshInterval time.Duration
+	}
+
+	tests := []struct {
+		otelTraceSamplerArgs string
+		expErrs              []string
+		codeOptions          []Option
+		expConfig            testConfig
+	}{
+		{
+			otelTraceSamplerArgs: "endpoint=http://localhost:14250,pollingIntervalMs=5000,initialSamplingRate=0.25",
+			expErrs:              []string{},
+		},
+		{
+			otelTraceSamplerArgs: "endpointhttp://localhost:14250,pollingIntervalMs=5x000,initialSamplingRate=0.xyz25,invalidKey=invalidValue",
+			expErrs: []string{
+				"argument endpointhttp://localhost:14250 is not of type '<key>=<value>'",
+				"pollingIntervalMs parsing failed",
+				"initialSamplingRate parsing failed",
+				"invalid argument invalidKey in OTEL_TRACE_SAMPLER_ARG",
+			},
+		},
+		{
+			// Make sure we don't override values provided in code
+			otelTraceSamplerArgs: "endpoint=http://localhost:14250,pollingIntervalMs=5000,initialSamplingRate=0.25",
+			expErrs:              []string{},
+			codeOptions: []Option{
+				WithSamplingServerURL("http://localhost:5778"),
+			},
+			expConfig: testConfig{
+				samplingServerURL:       "http://localhost:5778",
+				samplingRefreshInterval: time.Millisecond * 5000,
+			},
+		},
+	}
+
+	for _, test := range tests {
+		t.Run("", func(t *testing.T) {
+			t.Setenv("OTEL_TRACES_SAMPLER_ARG", test.otelTraceSamplerArgs)
+
+			_, errs := getEnvOptions()
+			require.Equal(t, len(test.expErrs), len(errs))
+
+			for i := range len(errs) {
+				require.ErrorContains(t, errs[i], test.expErrs[i])
+			}
+
+			if test.codeOptions != nil {
+				cfg := newConfig(test.codeOptions...)
+				require.Equal(t, test.expConfig.samplingServerURL, cfg.samplingServerURL)
+				require.Equal(t, test.expConfig.samplingRefreshInterval, cfg.samplingRefreshInterval)
+			}
+		})
+	}
+}