From c1c2389a1f43d0581e03c796a74c9eb670ebcf8a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Oct 2024 15:15:27 +0000 Subject: [PATCH 1/3] chore: bump @vitejs/plugin-react (#2537) Bumps [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/tree/HEAD/packages/plugin-react) from 4.3.2 to 4.3.3. - [Release notes](https://github.com/vitejs/vite-plugin-react/releases) - [Changelog](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite-plugin-react/commits/v4.3.3/packages/plugin-react) --- updated-dependencies: - dependency-name: "@vitejs/plugin-react" dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- examples/openfeature_react/react-app/package-lock.json | 8 ++++---- examples/openfeature_react/react-app/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/openfeature_react/react-app/package-lock.json b/examples/openfeature_react/react-app/package-lock.json index 99d8497c421..da8de281c54 100644 --- a/examples/openfeature_react/react-app/package-lock.json +++ b/examples/openfeature_react/react-app/package-lock.json @@ -20,7 +20,7 @@ "@types/react-dom": "^18.3.1", "@typescript-eslint/eslint-plugin": "^8.10.0", "@typescript-eslint/parser": "^8.10.0", - "@vitejs/plugin-react": "^4.3.2", + "@vitejs/plugin-react": "^4.3.3", "autoprefixer": "^10.4.20", "eslint": "^9.13.0", "eslint-plugin-react-hooks": "^5.0.0", @@ -1496,9 +1496,9 @@ } }, "node_modules/@vitejs/plugin-react": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.2.tgz", - "integrity": "sha512-hieu+o05v4glEBucTcKMK3dlES0OeJlD9YVOAPraVMOInBCwzumaIFiUjr4bHK7NPgnAHgiskUoceKercrN8vg==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.3.tgz", + "integrity": "sha512-NooDe9GpHGqNns1i8XDERg0Vsg5SSYRhRxxyTGogUdkdNt47jal+fbuYi+Yfq6pzRCKXyoPcWisfxE6RIM3GKA==", "dev": true, "dependencies": { "@babel/core": "^7.25.2", diff --git a/examples/openfeature_react/react-app/package.json b/examples/openfeature_react/react-app/package.json index 52662f3f024..91dfae33c13 100644 --- a/examples/openfeature_react/react-app/package.json +++ b/examples/openfeature_react/react-app/package.json @@ -22,7 +22,7 @@ "@types/react-dom": "^18.3.1", "@typescript-eslint/eslint-plugin": "^8.10.0", "@typescript-eslint/parser": "^8.10.0", - "@vitejs/plugin-react": "^4.3.2", + "@vitejs/plugin-react": "^4.3.3", "autoprefixer": "^10.4.20", "eslint": "^9.13.0", "eslint-plugin-react-hooks": "^5.0.0", From ad84c27c069236a678bed68ba0b33316531a152b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 22 Oct 2024 15:06:24 +0000 Subject: [PATCH 2/3] chore: bump org.junit.jupiter:junit-jupiter-engine (#2555) Bumps [org.junit.jupiter:junit-jupiter-engine](https://github.com/junit-team/junit5) from 5.11.2 to 5.11.3. - [Release notes](https://github.com/junit-team/junit5/releases) - [Commits](https://github.com/junit-team/junit5/compare/r5.11.2...r5.11.3) --- updated-dependencies: - dependency-name: org.junit.jupiter:junit-jupiter-engine dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- openfeature/provider_tests/java-integration-tests/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openfeature/provider_tests/java-integration-tests/pom.xml b/openfeature/provider_tests/java-integration-tests/pom.xml index 4e6cf9fab3b..ffd05a94eef 100644 --- a/openfeature/provider_tests/java-integration-tests/pom.xml +++ b/openfeature/provider_tests/java-integration-tests/pom.xml @@ -18,7 +18,7 @@ org.junit.jupiter junit-jupiter-engine - 5.11.2 + 5.11.3 test From f085397cc1c5b0c0b4c77c71111e61fd4f905488 Mon Sep 17 00:00:00 2001 From: Dave Henderson Date: Wed, 23 Oct 2024 15:37:07 -0400 Subject: [PATCH 3/3] feat(tracing): Misc. OTel tracing improvements (#2485) * feat(tracing): Misc. OTel tracing improvements Signed-off-by: Dave Henderson * chore: Use koanf instead of the direct usage of env variables --------- Signed-off-by: Dave Henderson Co-authored-by: Thomas Poignant --- cmd/relayproxy/api/opentelemetry/otel.go | 164 +++++++++++-- cmd/relayproxy/api/opentelemetry/otel_test.go | 230 ++++++++++++++++++ cmd/relayproxy/api/routes_monitoring_test.go | 3 +- cmd/relayproxy/api/server.go | 6 +- cmd/relayproxy/api/server_test.go | 7 +- cmd/relayproxy/config/config.go | 48 ++++ cmd/relayproxy/main.go | 8 +- go.mod | 24 +- go.sum | 36 ++- .../docs/relay_proxy/monitor_relay_proxy.md | 17 +- 10 files changed, 494 insertions(+), 49 deletions(-) create mode 100644 cmd/relayproxy/api/opentelemetry/otel_test.go diff --git a/cmd/relayproxy/api/opentelemetry/otel.go b/cmd/relayproxy/api/opentelemetry/otel.go index 6844b00300b..4afd25f69f6 100644 --- a/cmd/relayproxy/api/opentelemetry/otel.go +++ b/cmd/relayproxy/api/opentelemetry/otel.go @@ -2,20 +2,24 @@ package opentelemetry import ( "context" - "net/url" + "fmt" + "os" + "time" "github.com/thomaspoignant/go-feature-flag/cmd/relayproxy/config" + "go.opentelemetry.io/contrib/exporters/autoexport" + "go.opentelemetry.io/contrib/samplers/jaegerremote" "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/exporters/otlp/otlptrace" - "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp" "go.opentelemetry.io/otel/sdk/resource" sdktrace "go.opentelemetry.io/otel/sdk/trace" + semconv "go.opentelemetry.io/otel/semconv/v1.26.0" + "go.opentelemetry.io/otel/trace/noop" + "go.uber.org/zap" ) type OtelService struct { otelTraceProvider *sdktrace.TracerProvider - otelExporter *otlptrace.Exporter + otelExporter sdktrace.SpanExporter } func NewOtelService() OtelService { @@ -23,47 +27,157 @@ func NewOtelService() OtelService { } // Init the OpenTelemetry service -func (s *OtelService) Init(ctx context.Context, config config.Config) error { - // parsing the OpenTelemetry endpoint - u, err := url.Parse(config.OpenTelemetryOtlpEndpoint) +func (s *OtelService) Init(ctx context.Context, zapLog *zap.Logger, config config.Config) error { + // OTEL_SDK_DISABLED is not supported by the Go SDK, but is a standard env + // var defined by the OTel spec. We'll use it to disable the trace provider. + if config.OtelConfig.SDK.Disabled { + otel.SetTracerProvider(noop.NewTracerProvider()) + return nil + } + + // support the openTelemetryOtlpEndpoint config element + if config.OpenTelemetryOtlpEndpoint != "" && + config.OtelConfig.Exporter.Otlp.Endpoint == "" { + config.OtelConfig.Exporter.Otlp.Endpoint = config.OpenTelemetryOtlpEndpoint + _ = os.Setenv("OTEL_EXPORTER_OTLP_ENDPOINT", config.OpenTelemetryOtlpEndpoint) + } + + exporter, err := autoexport.NewSpanExporter(ctx) if err != nil { - return err + return fmt.Errorf("initializing OTel exporter: %w", err) + } + + serviceName := "go-feature-flag" + if v := config.OtelConfig.Service.Name; v != "" { + serviceName = v } - var opts []otlptracehttp.Option - if u.Scheme == "http" { - opts = append(opts, otlptracehttp.WithInsecure()) + sampler, err := initSampler(serviceName, config) + if err != nil { + return fmt.Errorf("initializing OTel sampler: %w", err) } - opts = append(opts, otlptracehttp.WithEndpoint(u.Host)) - client := otlptracehttp.NewClient(opts...) - s.otelExporter, err = otlptrace.New(ctx, client) + resource, err := initResource(ctx, serviceName, config.Version) if err != nil { - return err + return fmt.Errorf("initializing OTel resources: %w", err) } + s.otelExporter = exporter s.otelTraceProvider = sdktrace.NewTracerProvider( - sdktrace.WithSampler(sdktrace.AlwaysSample()), - sdktrace.WithBatcher(s.otelExporter), - sdktrace.WithResource(resource.NewSchemaless( - attribute.String("service.name", "go-feature-flag"), - attribute.String("service.version", config.Version), - )), + sdktrace.WithBatcher(exporter), + sdktrace.WithSampler(sampler), + sdktrace.WithResource(resource), ) + otel.SetTracerProvider(s.otelTraceProvider) + + // log OTel errors to zap rather than the default log package + otel.SetErrorHandler(otelErrHandler(func(err error) { + zapLog.Error("OTel error", zap.Error(err)) + })) + return nil } +type otelErrHandler func(err error) + +func (o otelErrHandler) Handle(err error) { + o(err) +} + +var _ otel.ErrorHandler = otelErrHandler(nil) + +func initResource(ctx context.Context, serviceName string, version string) (*resource.Resource, error) { + return resource.New(ctx, + resource.WithFromEnv(), + resource.WithProcessPID(), + resource.WithProcessExecutableName(), + resource.WithProcessExecutablePath(), + resource.WithProcessOwner(), + resource.WithProcessRuntimeName(), + resource.WithProcessRuntimeVersion(), + resource.WithProcessRuntimeDescription(), + resource.WithHost(), + resource.WithTelemetrySDK(), + resource.WithOS(), + resource.WithContainer(), + resource.WithAttributes( + semconv.ServiceNameKey.String(serviceName), + semconv.ServiceVersionKey.String(version), + ), + ) +} + +// initSampler determines which sampling strategy to use. If OTEL_TRACES_SAMPLER +// is unset, we'll always sample. +// If it's set to jaeger_remote, we'll use the Jaeger sampling server (supports +// JAEGER_SAMPLER_MANAGER_HOST_PORT, JAEGER_SAMPLER_REFRESH_INTERVAL, and +// JAEGER_SAMPLER_MAX_OPERATIONS). +// If it's set to any other value, we return nil and sdktrace.NewTracerProvider +// will set up the initSampler from the environment. +func initSampler(serviceName string, conf config.Config) (sdktrace.Sampler, error) { + sampler := conf.OtelConfig.Traces.Sampler + if sampler == "" { + return sdktrace.AlwaysSample(), nil + } + + if sampler != "jaeger_remote" { + return nil, nil + } + + samplerURL, samplerRefreshInterval, maxOperations, err := jaegerRemoteSamplerOpts(conf) + if err != nil { + return nil, err + } + + return jaegerremote.New( + serviceName, + jaegerremote.WithSamplingServerURL(samplerURL), + jaegerremote.WithSamplingRefreshInterval(samplerRefreshInterval), + jaegerremote.WithMaxOperations(maxOperations), + jaegerremote.WithInitialSampler(sdktrace.AlwaysSample()), + ), nil +} + +const ( + defaultSamplerURL = "http://localhost:5778/sampling" + defaultSamplingRefreshInterval = 1 * time.Minute + defaultSamplingMaxOperations = 256 +) + +func jaegerRemoteSamplerOpts(conf config.Config) (string, time.Duration, int, error) { + samplerURL := defaultSamplerURL + if host := conf.JaegerConfig.Sampler.Manager.Host.Port; host != "" { + samplerURL = host + } + + samplerRefreshInterval := defaultSamplingRefreshInterval + if v := conf.JaegerConfig.Sampler.Refresh.Interval; v != "" { + d, err := time.ParseDuration(v) + if err != nil { + return "", 0, 0, fmt.Errorf("parsing JAEGER_SAMPLER_REFRESH_INTERVAL: %w", err) + } + samplerRefreshInterval = d + } + + maxOperations := defaultSamplingMaxOperations + if v := conf.JaegerConfig.Sampler.Max.Operations; v != 0 { + maxOperations = v + } + return samplerURL, samplerRefreshInterval, maxOperations, nil +} + // Stop the OpenTelemetry service -func (s *OtelService) Stop() error { +func (s *OtelService) Stop(ctx context.Context) error { if s.otelExporter != nil { - err := s.otelExporter.Shutdown(context.Background()) + err := s.otelExporter.Shutdown(ctx) if err != nil { return err } } + if s.otelTraceProvider != nil { - err := s.otelTraceProvider.Shutdown(context.Background()) + err := s.otelTraceProvider.Shutdown(ctx) if err != nil { return err } diff --git a/cmd/relayproxy/api/opentelemetry/otel_test.go b/cmd/relayproxy/api/opentelemetry/otel_test.go new file mode 100644 index 00000000000..da1b99bfcfe --- /dev/null +++ b/cmd/relayproxy/api/opentelemetry/otel_test.go @@ -0,0 +1,230 @@ +package opentelemetry + +import ( + "context" + "errors" + "os" + "strconv" + "testing" + "time" + + "github.com/spf13/pflag" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/thomaspoignant/go-feature-flag/cmd/relayproxy/config" + "github.com/thomaspoignant/go-feature-flag/cmd/relayproxy/log" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + "go.opentelemetry.io/otel/trace/noop" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + "go.uber.org/zap/zaptest/observer" +) + +func TestInitSampler(t *testing.T) { + t.Run("OTEL_TRACES_SAMPLER unset", func(t *testing.T) { + f := pflag.NewFlagSet("config", pflag.ContinueOnError) + c, errC := config.New(f, zap.L(), "1.X.X") + require.NoError(t, errC) + + sampler, err := initSampler("test", *c) + require.NoError(t, err) + assert.Equal(t, sdktrace.AlwaysSample(), sampler) + }) + + t.Run("OTEL_TRACES_SAMPLER set to non-jaeger_remote", func(t *testing.T) { + t.Setenv("OTEL_TRACES_SAMPLER", "always_on") + f := pflag.NewFlagSet("config", pflag.ContinueOnError) + c, errC := config.New(f, zap.L(), "1.X.X") + require.NoError(t, errC) + + sampler, err := initSampler("test", *c) + require.NoError(t, err) + assert.Nil(t, sampler) + }) + + t.Run("OTEL_TRACES_SAMPLER set to jaeger_remote", func(t *testing.T) { + t.Setenv("OTEL_TRACES_SAMPLER", "jaeger_remote") + f := pflag.NewFlagSet("config", pflag.ContinueOnError) + c, errC := config.New(f, zap.L(), "1.X.X") + require.NoError(t, errC) + + sampler, err := initSampler("test", *c) + require.NoError(t, err) + + // not really any way to assert on the sampler other than calling + // Description()... + assert.Equal(t, "JaegerRemoteSampler{}", sampler.Description()) + }) +} + +func TestJaegerRemoteSamplerOpts(t *testing.T) { + t.Run("defaults", func(t *testing.T) { + f := pflag.NewFlagSet("config", pflag.ContinueOnError) + c, errC := config.New(f, zap.L(), "1.X.X") + require.NoError(t, errC) + + url, refreshInterval, maxOperations, err := jaegerRemoteSamplerOpts(*c) + require.NoError(t, err) + assert.Equal(t, defaultSamplerURL, url) + assert.Equal(t, defaultSamplingRefreshInterval, refreshInterval) + assert.Equal(t, defaultSamplingMaxOperations, maxOperations) + }) + + t.Run("JAEGER_SAMPLER_MANAGER_HOST_PORT set", func(t *testing.T) { + expected := "http://example.com:1234" + t.Setenv("JAEGER_SAMPLER_MANAGER_HOST_PORT", expected) + + f := pflag.NewFlagSet("config", pflag.ContinueOnError) + c, errC := config.New(f, zap.L(), "1.X.X") + require.NoError(t, errC) + + url, _, _, err := jaegerRemoteSamplerOpts(*c) + require.NoError(t, err) + assert.Equal(t, expected, url) + }) + + t.Run("JAEGER_SAMPLER_REFRESH_INTERVAL set", func(t *testing.T) { + expected := 42 * time.Second + t.Setenv("JAEGER_SAMPLER_REFRESH_INTERVAL", expected.String()) + + f := pflag.NewFlagSet("config", pflag.ContinueOnError) + c, errC := config.New(f, zap.L(), "1.X.X") + require.NoError(t, errC) + + _, refreshInterval, _, err := jaegerRemoteSamplerOpts(*c) + require.NoError(t, err) + assert.Equal(t, expected, refreshInterval) + }) + + t.Run("JAEGER_SAMPLER_MAX_OPERATIONS set", func(t *testing.T) { + expected := 42 + t.Setenv("JAEGER_SAMPLER_MAX_OPERATIONS", strconv.Itoa(expected)) + + f := pflag.NewFlagSet("config", pflag.ContinueOnError) + c, errC := config.New(f, zap.L(), "1.X.X") + require.NoError(t, errC) + + _, _, maxOperations, err := jaegerRemoteSamplerOpts(*c) + require.NoError(t, err) + assert.Equal(t, expected, maxOperations) + }) + + t.Run("invalid JAEGER_SAMPLER_REFRESH_INTERVAL", func(t *testing.T) { + t.Setenv("JAEGER_SAMPLER_REFRESH_INTERVAL", "bogus") + + f := pflag.NewFlagSet("config", pflag.ContinueOnError) + c, errC := config.New(f, zap.L(), "1.X.X") + require.NoError(t, errC) + + _, _, _, err := jaegerRemoteSamplerOpts(*c) + require.Error(t, err) + }) + + t.Run("invalid JAEGER_SAMPLER_MAX_OPERATIONS", func(t *testing.T) { + t.Setenv("JAEGER_SAMPLER_MAX_OPERATIONS", "bogus") + + f := pflag.NewFlagSet("config", pflag.ContinueOnError) + _, errC := config.New(f, zap.L(), "1.X.X") + require.Error(t, errC) + }) +} + +func TestInitResource(t *testing.T) { + t.Run("defaults, no env", func(t *testing.T) { + res, err := initResource(context.Background(), "test", "1.2.3") + require.NoError(t, err) + + rmap := map[string]attribute.Value{} + for _, attr := range res.Attributes() { + rmap[string(attr.Key)] = attr.Value + } + + // just spot-check a few things + assert.Equal(t, "test", rmap["service.name"].AsString()) + assert.Equal(t, "1.2.3", rmap["service.version"].AsString()) + assert.Equal(t, "go", rmap["process.runtime.name"].AsString()) + }) + + t.Run("with OTEL_RESOURCE_ATTRIBUTES set", func(t *testing.T) { + t.Setenv("OTEL_RESOURCE_ATTRIBUTES", "key1=val1,key2=val2") + + res, err := initResource(context.Background(), "test", "1.2.3") + require.NoError(t, err) + + rmap := map[string]attribute.Value{} + for _, attr := range res.Attributes() { + rmap[string(attr.Key)] = attr.Value + } + + assert.Equal(t, "val1", rmap["key1"].AsString()) + assert.Equal(t, "val2", rmap["key2"].AsString()) + }) +} + +func TestInit(t *testing.T) { + logger := log.InitLogger().ZapLogger + + svc := NewOtelService() + + t.Run("no config", func(t *testing.T) { + err := svc.Init(context.Background(), logger, config.Config{}) + require.NoError(t, err) + defer func() { _ = svc.Stop(context.Background()) }() + assert.NotNil(t, otel.GetTracerProvider()) + }) + + t.Run("disabled", func(t *testing.T) { + t.Setenv("OTEL_SDK_DISABLED", "true") + f := pflag.NewFlagSet("config", pflag.ContinueOnError) + c, errC := config.New(f, zap.L(), "1.X.X") + require.NoError(t, errC) + err := svc.Init(context.Background(), logger, *c) + require.NoError(t, err) + defer func() { _ = svc.Stop(context.Background()) }() + assert.Equal(t, noop.NewTracerProvider(), otel.GetTracerProvider()) + }) + + t.Run("support openTelemetryOtlpEndpoint", func(t *testing.T) { + err := svc.Init(context.Background(), logger, config.Config{ + OpenTelemetryOtlpEndpoint: "https://example.com:4318", + }) + require.NoError(t, err) + defer func() { _ = svc.Stop(context.Background()) }() + assert.Equal(t, "https://example.com:4318", os.Getenv("OTEL_EXPORTER_OTLP_ENDPOINT")) + }) + + t.Run("OTEL_EXPORTER_OTLP_ENDPOINT takes precedence", func(t *testing.T) { + t.Setenv("OTEL_EXPORTER_OTLP_ENDPOINT", "https://example.com:4318") + f := pflag.NewFlagSet("config", pflag.ContinueOnError) + c, errC := config.New(f, zap.L(), "1.X.X") + require.NoError(t, errC) + c.OpenTelemetryOtlpEndpoint = "https://bogus.com:4317" + err := svc.Init(context.Background(), logger, *c) + require.NoError(t, err) + defer func() { _ = svc.Stop(context.Background()) }() + assert.Equal(t, "https://example.com:4318", os.Getenv("OTEL_EXPORTER_OTLP_ENDPOINT")) + }) + + t.Run("error handler logs to zap", func(t *testing.T) { + obs, logs := observer.New(zap.InfoLevel) + testLogger := zap.New(obs) + + expectedErr := errors.New("test error") + + err := svc.Init(context.Background(), testLogger, config.Config{}) + require.NoError(t, err) + defer func() { _ = svc.Stop(context.Background()) }() + otel.GetErrorHandler().Handle(expectedErr) + + require.Len(t, logs.All(), 1) + + want := []observer.LoggedEntry{{ + Entry: zapcore.Entry{Level: zap.ErrorLevel, Message: "OTel error"}, + Context: []zapcore.Field{zap.Error(expectedErr)}, + }} + + assert.Equal(t, want, logs.AllUntimed()) + }) +} diff --git a/cmd/relayproxy/api/routes_monitoring_test.go b/cmd/relayproxy/api/routes_monitoring_test.go index 032f02c263d..4144a1c3ec7 100644 --- a/cmd/relayproxy/api/routes_monitoring_test.go +++ b/cmd/relayproxy/api/routes_monitoring_test.go @@ -1,6 +1,7 @@ package api_test import ( + "context" "fmt" "net/http" "testing" @@ -70,7 +71,7 @@ func TestPprofEndpointsStarts(t *testing.T) { } go apiServer.Start() - defer apiServer.Stop() + defer apiServer.Stop(context.Background()) resp, err := http.Get(fmt.Sprintf("http://localhost:%d/debug/pprof/heap", portToCheck)) require.NoError(t, err) require.Equal(t, tt.expectedStatusCode, resp.StatusCode) diff --git a/cmd/relayproxy/api/server.go b/cmd/relayproxy/api/server.go index ebce3b557ce..8054561cdd8 100644 --- a/cmd/relayproxy/api/server.go +++ b/cmd/relayproxy/api/server.go @@ -121,7 +121,7 @@ func (s *Server) Start() { // start the OpenTelemetry tracing service if s.config.OpenTelemetryOtlpEndpoint != "" { - err := s.otelService.Init(context.Background(), *s.config) + err := s.otelService.Init(context.Background(), s.zapLog, *s.config) if err != nil { s.zapLog.Error("error while initializing Otel", zap.Error(err)) // we can continue because otel is not mandatory to start the server @@ -155,8 +155,8 @@ func (s *Server) getLambdaHandler() interface{} { } // Stop shutdown the API server -func (s *Server) Stop() { - err := s.otelService.Stop() +func (s *Server) Stop(ctx context.Context) { + err := s.otelService.Stop(ctx) if err != nil { s.zapLog.Error("impossible to stop otel", zap.Error(err)) } diff --git a/cmd/relayproxy/api/server_test.go b/cmd/relayproxy/api/server_test.go index 1d3ceb9b8cd..266fa399ef5 100644 --- a/cmd/relayproxy/api/server_test.go +++ b/cmd/relayproxy/api/server_test.go @@ -1,6 +1,7 @@ package api_test import ( + "context" "net/http" "strings" "testing" @@ -52,7 +53,7 @@ func Test_Starting_RelayProxy_with_monitoring_on_same_port(t *testing.T) { s := api.New(proxyConf, services, log.ZapLogger) go func() { s.Start() }() - defer s.Stop() + defer s.Stop(context.Background()) time.Sleep(10 * time.Millisecond) @@ -106,7 +107,7 @@ func Test_Starting_RelayProxy_with_monitoring_on_different_port(t *testing.T) { s := api.New(proxyConf, services, log.ZapLogger) go func() { s.Start() }() - defer s.Stop() + defer s.Stop(context.Background()) time.Sleep(10 * time.Millisecond) @@ -175,7 +176,7 @@ func Test_CheckOFREPAPIExists(t *testing.T) { s := api.New(proxyConf, services, log.ZapLogger) go func() { s.Start() }() - defer s.Stop() + defer s.Stop(context.Background()) time.Sleep(10 * time.Millisecond) diff --git a/cmd/relayproxy/config/config.go b/cmd/relayproxy/config/config.go index 33ab485fcdc..a11cc7a0c90 100644 --- a/cmd/relayproxy/config/config.go +++ b/cmd/relayproxy/config/config.go @@ -247,6 +247,12 @@ type Config struct { // you ensure that GO Feature Flag will always start with a configuration but which can be out-dated. PersistentFlagConfigurationFile string `mapstructure:"persistentFlagConfigurationFile" koanf:"persistentflagconfigurationfile"` //nolint: lll + // OtelConfig is the configuration for the OpenTelemetry part of the relay proxy + OtelConfig OpenTelemetryConfiguration `mapstructure:"otel" koanf:"otel"` + + // JaegerConfig is the configuration for the Jaeger sampling of the relay proxy + JaegerConfig JaegerSamplerConfiguration `mapstructure:"jaeger" koanf:"jaeger"` + // ---- private fields // apiKeySet is the internal representation of an API keys list configured @@ -258,6 +264,48 @@ type Config struct { adminAPIKeySet map[string]interface{} } +// OpenTelemetryConfiguration is the configuration for the OpenTelemetry part of the relay proxy +// It is used to configure the OpenTelemetry SDK and the OpenTelemetry Exporter +// Most of the time this configuration is set using environment variables. +type OpenTelemetryConfiguration struct { + SDK struct { + Disabled bool `mapstructure:"disabled" koanf:"disabled"` + } `mapstructure:"sdk" koanf:"sdk"` + Exporter struct { + Otlp struct { + Endpoint string `mapstructure:"endpoint" koanf:"endpoint"` + Protocol string `mapstructure:"protocol" koanf:"protocol"` + } `mapstructure:"otlp" koanf:"otlp"` + } `mapstructure:"exporter" koanf:"exporter"` + Service struct { + Name string `mapstructure:"name" koanf:"name"` + } `mapstructure:"service" koanf:"service"` + Traces struct { + Sampler string `mapstructure:"sampler" koanf:"sampler"` + } `mapstructure:"traces" koanf:"traces"` + Resource struct { + Attributes map[string]string `mapstructure:"attributes" koanf:"attributes"` + } `mapstructure:"resource" koanf:"resource"` +} + +// JaegerSamplerConfiguration is the configuration object to configure the sampling. +// Most of the time this configuration is set using environment variables. +type JaegerSamplerConfiguration struct { + Sampler struct { + Manager struct { + Host struct { + Port string `mapstructure:"port" koanf:"port"` + } `mapstructure:"host" koanf:"host"` + } `mapstructure:"manager" koanf:"manager"` + Refresh struct { + Interval string `mapstructure:"interval" koanf:"interval"` + } `mapstructure:"refresh" koanf:"refresh"` + Max struct { + Operations int `mapstructure:"operations" koanf:"operations"` + } `mapstructure:"max" koanf:"max"` + } `mapstructure:"sampler" koanf:"sampler"` +} + // APIKeysAdminExists is checking if an admin API Key exist in the relay proxy configuration func (c *Config) APIKeysAdminExists(apiKey string) bool { if c.adminAPIKeySet == nil { diff --git a/cmd/relayproxy/main.go b/cmd/relayproxy/main.go index 52a31f5984e..b4be86d4b2a 100644 --- a/cmd/relayproxy/main.go +++ b/cmd/relayproxy/main.go @@ -1,8 +1,10 @@ package main import ( + "context" "fmt" "os" + "time" "github.com/spf13/pflag" "github.com/thomaspoignant/go-feature-flag/cmd/relayproxy/api" @@ -100,7 +102,11 @@ func main() { if proxyConf.StartAsAwsLambda { apiServer.StartAwsLambda() } else { + defer func() { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + apiServer.Stop(ctx) + }() apiServer.Start() - defer func() { _ = apiServer.Stop }() } } diff --git a/go.mod b/go.mod index f3cd931b6bb..66e30989aa4 100644 --- a/go.mod +++ b/go.mod @@ -50,11 +50,12 @@ require ( github.com/xitongsys/parquet-go v1.6.2 github.com/xitongsys/parquet-go-source v0.0.0-20230830030807-0dd610dbff1d go.mongodb.org/mongo-driver v1.17.1 + go.opentelemetry.io/contrib/exporters/autoexport v0.56.0 go.opentelemetry.io/contrib/instrumentation/github.com/labstack/echo/otelecho v0.56.0 + go.opentelemetry.io/contrib/samplers/jaegerremote v0.24.0 go.opentelemetry.io/otel v1.31.0 - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.31.0 - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.31.0 go.opentelemetry.io/otel/sdk v1.31.0 + go.opentelemetry.io/otel/trace v1.31.0 go.uber.org/zap v1.27.0 golang.org/x/net v0.30.0 golang.org/x/oauth2 v0.23.0 @@ -191,7 +192,7 @@ require ( github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/prometheus/client_model v0.6.1 // indirect - github.com/prometheus/common v0.55.0 // indirect + github.com/prometheus/common v0.60.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect github.com/samber/lo v1.44.0 // indirect @@ -215,12 +216,25 @@ require ( github.com/yusufpapurcu/wmi v1.2.3 // indirect go.einride.tech/aip v0.68.0 // indirect go.opencensus.io v0.24.0 // indirect + go.opentelemetry.io/contrib/bridges/prometheus v0.56.0 // indirect go.opentelemetry.io/contrib/detectors/gcp v1.29.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.7.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.7.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.31.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.31.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.31.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.31.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.31.0 // indirect + go.opentelemetry.io/otel/exporters/prometheus v0.53.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.7.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.31.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.31.0 // indirect + go.opentelemetry.io/otel/log v0.7.0 // indirect go.opentelemetry.io/otel/metric v1.31.0 // indirect - go.opentelemetry.io/otel/sdk/metric v1.29.0 // indirect - go.opentelemetry.io/otel/trace v1.31.0 // indirect + go.opentelemetry.io/otel/sdk/log v0.7.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.31.0 // indirect go.opentelemetry.io/proto/otlp v1.3.1 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/crypto v0.28.0 // indirect diff --git a/go.sum b/go.sum index da1fcfd24a9..382a5223b2b 100644 --- a/go.sum +++ b/go.sum @@ -779,8 +779,8 @@ github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/j github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= -github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= -github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= +github.com/prometheus/common v0.60.0 h1:+V9PAREWNvJMAuJ1x1BaWl9dewMW4YrHZQbx0sJNllA= +github.com/prometheus/common v0.60.0/go.mod h1:h0LYf1R1deLSKtD4Vdg8gy4RuOvENW2J/h19V5NADQw= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/r3labs/diff/v3 v3.0.1 h1:CBKqf3XmNRHXKmdU7mZP1w7TV0pDyVCis1AUHtA4Xtg= @@ -912,8 +912,12 @@ go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.opentelemetry.io/contrib/bridges/prometheus v0.56.0 h1:ax2MzrA26l3LTS2NRnagkbeKDrW4SM8VcAubasnpYqs= +go.opentelemetry.io/contrib/bridges/prometheus v0.56.0/go.mod h1:+aiuB6jaKqSb5xaY7sOpGZEMIgjL0sxXfIW1PQmp5d0= go.opentelemetry.io/contrib/detectors/gcp v1.29.0 h1:TiaiXB4DpGD3sdzNlYQxruQngn5Apwzi1X0DRhuGvDQ= go.opentelemetry.io/contrib/detectors/gcp v1.29.0/go.mod h1:GW2aWZNwR2ZxDLdv8OyC2G8zkRoQBuURgV7RPQgcPoU= +go.opentelemetry.io/contrib/exporters/autoexport v0.56.0 h1:2k73WaZ+jHYcK3lLAC3CJ8viT/LqkIcDDUWpbbYbZK0= +go.opentelemetry.io/contrib/exporters/autoexport v0.56.0/go.mod h1:RAHAFqVEQ+iKEAPgm6z+Gnsi0Fd5MDuqnD5T3Ms6Kg4= go.opentelemetry.io/contrib/instrumentation/github.com/labstack/echo/otelecho v0.56.0 h1:INy+gB4Y1rE0gJNfjTgZBFVD4RuTV5NpRnafbwoeROU= go.opentelemetry.io/contrib/instrumentation/github.com/labstack/echo/otelecho v0.56.0/go.mod h1:ZXC8RPcIIJTidnOto6PE5w5vPwSg6XngjBLiWlX4n2Q= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 h1:r6I7RJCN86bpD/FQwedZ0vSixDpwuWREjW9oRMsmqDc= @@ -922,18 +926,42 @@ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+n go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8= go.opentelemetry.io/contrib/propagators/b3 v1.31.0 h1:PQPXYscmwbCp76QDvO4hMngF2j8Bx/OTV86laEl8uqo= go.opentelemetry.io/contrib/propagators/b3 v1.31.0/go.mod h1:jbqfV8wDdqSDrAYxVpXQnpM0XFMq2FtDesblJ7blOwQ= +go.opentelemetry.io/contrib/samplers/jaegerremote v0.24.0 h1:LcynmmCFYwa0AySFI+yLEeZpwQi2yQdEkv5+zA3TwcI= +go.opentelemetry.io/contrib/samplers/jaegerremote v0.24.0/go.mod h1:oPzwx3Gp7+u62wvez470Yq/Yu0O7hpVl5SFBRGI4964= go.opentelemetry.io/otel v1.31.0 h1:NsJcKPIW0D0H3NgzPDHmo0WW6SptzPdqg/L1zsIm2hY= go.opentelemetry.io/otel v1.31.0/go.mod h1:O0C14Yl9FgkjqcCZAsE053C13OaddMYr/hz6clDkEJE= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.7.0 h1:iNba3cIZTDPB2+IAbVY/3TUN+pCCLrNYo2GaGtsKBak= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.7.0/go.mod h1:l5BDPiZ9FbeejzWTAX6BowMzQOM/GeaUQ6lr3sOcSkc= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.7.0 h1:mMOmtYie9Fx6TSVzw4W+NTpvoaS1JWWga37oI1a/4qQ= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.7.0/go.mod h1:yy7nDsMMBUkD+jeekJ36ur5f3jJIrmCwUrY67VFhNpA= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.31.0 h1:FZ6ei8GFW7kyPYdxJaV2rgI6M+4tvZzhYsQ2wgyVC08= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.31.0/go.mod h1:MdEu/mC6j3D+tTEfvI15b5Ci2Fn7NneJ71YMoiS3tpI= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.31.0 h1:ZsXq73BERAiNuuFXYqP4MR5hBrjXfMGSO+Cx7qoOZiM= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.31.0/go.mod h1:hg1zaDMpyZJuUzjFxFsRYBoccE86tM9Uf4IqNMUxvrY= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.31.0 h1:K0XaT3DwHAcV4nKLzcQvwAgSyisUghWoY20I7huthMk= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.31.0/go.mod h1:B5Ki776z/MBnVha1Nzwp5arlzBbE3+1jk+pGmaP5HME= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.31.0 h1:FFeLy03iVTXP6ffeN2iXrxfGsZGCjVx0/4KlizjyBwU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.31.0/go.mod h1:TMu73/k1CP8nBUpDLc71Wj/Kf7ZS9FK5b53VapRsP9o= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.31.0 h1:lUsI2TYsQw2r1IASwoROaCnjdj2cvC2+Jbxvk6nHnWU= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.31.0/go.mod h1:2HpZxxQurfGxJlJDblybejHB6RX6pmExPNe517hREw4= +go.opentelemetry.io/otel/exporters/prometheus v0.53.0 h1:QXobPHrwiGLM4ufrY3EOmDPJpo2P90UuFau4CDPJA/I= +go.opentelemetry.io/otel/exporters/prometheus v0.53.0/go.mod h1:WOAXGr3D00CfzmFxtTV1eR0GpoHuPEu+HJT8UWW2SIU= +go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.7.0 h1:TwmL3O3fRR80m8EshBrd8YydEZMcUCsZXzOUlnFohwM= +go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.7.0/go.mod h1:tH98dDv5KPmPThswbXA0fr0Lwfs+OhK8HgaCo7PjRrk= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.31.0 h1:HZgBIps9wH0RDrwjrmNa3DVbNRW60HEhdzqZFyAp3fI= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.31.0/go.mod h1:RDRhvt6TDG0eIXmonAx5bd9IcwpqCkziwkOClzWKwAQ= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.31.0 h1:UGZ1QwZWY67Z6BmckTU+9Rxn04m2bD3gD6Mk0OIOCPk= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.31.0/go.mod h1:fcwWuDuaObkkChiDlhEpSq9+X1C0omv+s5mBtToAQ64= +go.opentelemetry.io/otel/log v0.7.0 h1:d1abJc0b1QQZADKvfe9JqqrfmPYQCz2tUSO+0XZmuV4= +go.opentelemetry.io/otel/log v0.7.0/go.mod h1:2jf2z7uVfnzDNknKTO9G+ahcOAyWcp1fJmk/wJjULRo= go.opentelemetry.io/otel/metric v1.31.0 h1:FSErL0ATQAmYHUIzSezZibnyVlft1ybhy4ozRPcF2fE= go.opentelemetry.io/otel/metric v1.31.0/go.mod h1:C3dEloVbLuYoX41KpmAhOqNriGbA+qqH6PQ5E5mUfnY= go.opentelemetry.io/otel/sdk v1.31.0 h1:xLY3abVHYZ5HSfOg3l2E5LUj2Cwva5Y7yGxnSW9H5Gk= go.opentelemetry.io/otel/sdk v1.31.0/go.mod h1:TfRbMdhvxIIr/B2N2LQW2S5v9m3gOQ/08KsbbO5BPT0= -go.opentelemetry.io/otel/sdk/metric v1.29.0 h1:K2CfmJohnRgvZ9UAj2/FhIf/okdWcNdBwe1m8xFXiSY= -go.opentelemetry.io/otel/sdk/metric v1.29.0/go.mod h1:6zZLdCl2fkauYoZIOn/soQIDSWFmNSRcICarHfuhNJQ= +go.opentelemetry.io/otel/sdk/log v0.7.0 h1:dXkeI2S0MLc5g0/AwxTZv6EUEjctiH8aG14Am56NTmQ= +go.opentelemetry.io/otel/sdk/log v0.7.0/go.mod h1:oIRXpW+WD6M8BuGj5rtS0aRu/86cbDV/dAfNaZBIjYM= +go.opentelemetry.io/otel/sdk/metric v1.31.0 h1:i9hxxLJF/9kkvfHppyLL55aW7iIJz4JjxTeYusH7zMc= +go.opentelemetry.io/otel/sdk/metric v1.31.0/go.mod h1:CRInTMVvNhUKgSAMbKyTMxqOBC0zgyxzW55lZzX43Y8= go.opentelemetry.io/otel/trace v1.31.0 h1:ffjsj1aRouKewfr85U2aGagJ46+MvodynlQ1HYdmJys= go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= diff --git a/website/docs/relay_proxy/monitor_relay_proxy.md b/website/docs/relay_proxy/monitor_relay_proxy.md index 5d13662fa2d..740c3e1df0f 100644 --- a/website/docs/relay_proxy/monitor_relay_proxy.md +++ b/website/docs/relay_proxy/monitor_relay_proxy.md @@ -9,18 +9,21 @@ description: Monitoring and Tracing of the relay proxy. The **relay proxy** is able to trace the requests it is handling. This is done by using OpenTelemetry. ### Configuration -To configure the tracing, you need to set in the configuration the endpoint to your OTLP collector. -```yaml -# ... -openTelemetryOtlpEndpoint: http://localhost:4318 -# ... -``` + +By default, the relay proxy will attempt to send traces to an OpenTelemetry +collector or compatible agent running at `http://localhost:4318` using the +`http/protobuf` protocol. +To override the endpoint, set the `OTEL_EXPORTER_OTLP_ENDPOINT` environment variable. +To override the protocol, set the `OTEL_EXPORTER_OTLP_PROTOCOL` environment variable. +See [the OpenTelemetry documentation](https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/) for more information. All your requests will be traced and sent to the collector with the service name **`go-feature-flag`**. +To disable tracing, set the `OTEL_SDK_DISABLED` environment variable to `true`. + :::note If you want to try the OpenTelemetry integration locally, follow this [README](https://github.com/thomaspoignant/go-feature-flag/tree/main/cmd/relayproxy/testdata/opentelemetry) -to setup Jaeger and see your traces. +to setup Jaeger and see your traces. ::: ## Monitoring