Skip to content

Commit

Permalink
Add opentelemetry for the relay proxy (#1312)
Browse files Browse the repository at this point in the history
* Add opentelemetry for the relay proxy

Signed-off-by: Thomas Poignant <[email protected]>

* Add doc

Signed-off-by: Thomas Poignant <[email protected]>

* Add configuration for openTelemetryOtlpEndpoint

Signed-off-by: Thomas Poignant <[email protected]>

---------

Signed-off-by: Thomas Poignant <[email protected]>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
  • Loading branch information
thomaspoignant and kodiakhq[bot] authored Dec 4, 2023
1 parent 4c6a9af commit ea12ff9
Show file tree
Hide file tree
Showing 12 changed files with 244 additions and 18 deletions.
71 changes: 71 additions & 0 deletions cmd/relayproxy/api/opentelemetry/otel.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package opentelemetry

import (
"context"
"github.com/thomaspoignant/go-feature-flag/cmd/relayproxy/config"
"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"
"net/url"
)

type OtelService struct {
otelTraceProvider *sdktrace.TracerProvider
otelExporter *otlptrace.Exporter
}

func NewOtelService() OtelService {
return 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)
if err != nil {
return err
}

var opts []otlptracehttp.Option
if u.Scheme == "http" {
opts = append(opts, otlptracehttp.WithInsecure())
}
opts = append(opts, otlptracehttp.WithEndpoint(u.Host))
client := otlptracehttp.NewClient(opts...)

s.otelExporter, err = otlptrace.New(ctx, client)
if err != nil {
return err
}

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),
)),
)
otel.SetTracerProvider(s.otelTraceProvider)
return nil
}

// Stop the OpenTelemetry service
func (s *OtelService) Stop() error {
if s.otelExporter != nil {
err := s.otelExporter.Shutdown(context.Background())
if err != nil {
return err
}
}
if s.otelTraceProvider != nil {
err := s.otelTraceProvider.Shutdown(context.Background())
if err != nil {
return err
}
}
return nil
}
29 changes: 24 additions & 5 deletions cmd/relayproxy/api/server.go
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
package api

import (
"context"
"fmt"
"github.com/labstack/echo-contrib/echoprometheus"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
echoSwagger "github.com/swaggo/echo-swagger"
custommiddleware "github.com/thomaspoignant/go-feature-flag/cmd/relayproxy/api/middleware"
"github.com/thomaspoignant/go-feature-flag/cmd/relayproxy/api/opentelemetry"
"github.com/thomaspoignant/go-feature-flag/cmd/relayproxy/config"
"github.com/thomaspoignant/go-feature-flag/cmd/relayproxy/controller"
"github.com/thomaspoignant/go-feature-flag/cmd/relayproxy/metric"
"github.com/thomaspoignant/go-feature-flag/cmd/relayproxy/service"
"go.opentelemetry.io/contrib/instrumentation/github.com/labstack/echo/otelecho"
"go.uber.org/zap"
"strings"
"time"
Expand All @@ -22,20 +25,22 @@ func New(config *config.Config,
zapLog *zap.Logger,
) Server {
s := Server{
config: config,
services: services,
zapLog: zapLog,
config: config,
services: services,
zapLog: zapLog,
otelService: opentelemetry.NewOtelService(),
}
s.init()
return s
}

// Server is the struct that represent the API server
// Server is the struct that represents the API server
type Server struct {
config *config.Config
echoInstance *echo.Echo
services service.Services
zapLog *zap.Logger
otelService opentelemetry.OtelService
}

// init initialize the configuration of our API server (using echo)
Expand All @@ -45,6 +50,14 @@ func (s *Server) init() {
s.echoInstance.HidePort = true
s.echoInstance.Debug = s.config.Debug

if s.config.OpenTelemetryOtlpEndpoint != "" {
err := s.otelService.Init(context.Background(), *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
}
}

// Global Middlewares
if s.services.Metrics != (metric.Metrics{}) {
s.echoInstance.Use(echoprometheus.NewMiddlewareWithConfig(echoprometheus.MiddlewareConfig{
Expand All @@ -54,6 +67,7 @@ func (s *Server) init() {
s.echoInstance.GET("/metrics", echoprometheus.NewHandlerWithConfig(
echoprometheus.HandlerConfig{Gatherer: s.services.Metrics.Registry}))
}
s.echoInstance.Use(otelecho.Middleware("go-feature-flag"))
s.echoInstance.Use(custommiddleware.ZapLogger(s.zapLog, s.config))
s.echoInstance.Use(middleware.CORSWithConfig(middleware.DefaultCORSConfig))
s.echoInstance.Use(middleware.Recover())
Expand Down Expand Up @@ -144,7 +158,12 @@ func (s *Server) StartAwsLambda() {

// Stop shutdown the API server
func (s *Server) Stop() {
err := s.echoInstance.Close()
err := s.otelService.Stop()
if err != nil {
s.zapLog.Error("impossible to stop otel", zap.Error(err))
}

err = s.echoInstance.Close()
if err != nil {
s.zapLog.Fatal("impossible to stop go-feature-flag relay proxy", zap.Error(err))
}
Expand Down
6 changes: 5 additions & 1 deletion cmd/relayproxy/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -183,9 +183,13 @@ type Config struct {
// Default: nil
EvaluationContextEnrichment map[string]interface{} `mapstructure:"evaluationContextEnrichment" koanf:"evaluationcontextenrichment"` //nolint: lll

// OpenTelemetryOtlpEndpoint (optional) is the endpoint of the OpenTelemetry collector
// Default: ""
OpenTelemetryOtlpEndpoint string `mapstructure:"openTelemetryOtlpEndpoint" koanf:"opentelemetryotlpendpoint"`

// ---- private fields

// apiKeySet is the internal representation of the list of api keys configured
// apiKeySet is the internal representation of an API keys list configured
// we store them in a set to be
apiKeysSet map[string]interface{}
}
Expand Down
3 changes: 3 additions & 0 deletions cmd/relayproxy/config/const.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package config

const OtelTracerName = "go-feature-flag"
11 changes: 10 additions & 1 deletion cmd/relayproxy/controller/all_flags.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package controller

import (
"github.com/thomaspoignant/go-feature-flag/cmd/relayproxy/config"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"net/http"

"github.com/labstack/echo/v4"
Expand Down Expand Up @@ -52,7 +55,13 @@ func (h *allFlags) Handler(c echo.Context) error {
if err != nil {
return err
}

tracer := otel.GetTracerProvider().Tracer(config.OtelTracerName)
_, span := tracer.Start(c.Request().Context(), "AllFlagsState")
defer span.End()
allFlags := h.goFF.AllFlagsState(evaluationCtx)
span.SetAttributes(
attribute.Bool("AllFlagsState.valid", allFlags.IsValid()),
attribute.Int("AllFlagsState.numberEvaluation", len(allFlags.GetFlags())),
)
return c.JSON(http.StatusOK, allFlags)
}
7 changes: 7 additions & 0 deletions cmd/relayproxy/controller/collect_eval_data.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ package controller

import (
"fmt"
"github.com/thomaspoignant/go-feature-flag/cmd/relayproxy/config"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"net/http"

"github.com/labstack/echo/v4"
Expand Down Expand Up @@ -48,6 +51,10 @@ func (h *collectEvalData) Handler(c echo.Context) error {
return echo.NewHTTPError(http.StatusBadRequest, "collectEvalData: invalid input data")
}

tracer := otel.GetTracerProvider().Tracer(config.OtelTracerName)
_, span := tracer.Start(c.Request().Context(), "collectEventData")
defer span.End()
span.SetAttributes(attribute.Int("collectEventData.eventCollectionSize", len(reqBody.Events)))
for _, event := range reqBody.Events {
if event.Source == "" {
event.Source = "PROVIDER_CACHE"
Expand Down
22 changes: 21 additions & 1 deletion cmd/relayproxy/controller/flag_eval.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ package controller

import (
"fmt"
"github.com/thomaspoignant/go-feature-flag/cmd/relayproxy/config"
"github.com/thomaspoignant/go-feature-flag/cmd/relayproxy/metric"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"net/http"

"github.com/labstack/echo/v4"
Expand Down Expand Up @@ -64,7 +67,24 @@ func (h *flagEval) Handler(c echo.Context) error {
return err
}

// get flag name from the URL
tracer := otel.GetTracerProvider().Tracer(config.OtelTracerName)
_, span := tracer.Start(c.Request().Context(), "flagEvaluation")
defer span.End()

flagValue, _ := h.goFF.RawVariation(flagKey, evaluationCtx, reqBody.DefaultValue)

span.SetAttributes(
attribute.String("flagEvaluation.flagName", flagKey),
attribute.Bool("flagEvaluation.trackEvents", flagValue.TrackEvents),
attribute.String("flagEvaluation.variant", flagValue.VariationType),
attribute.Bool("flagEvaluation.failed", flagValue.Failed),
attribute.String("flagEvaluation.version", flagValue.Version),
attribute.String("flagEvaluation.reason", flagValue.Reason),
attribute.String("flagEvaluation.errorCode", flagValue.ErrorCode),
attribute.Bool("flagEvaluation.cacheable", flagValue.Cacheable),
// we convert to string because there is no attribute for interface{}
attribute.String("flagEvaluation.value", fmt.Sprintf("%v", flagValue.Value)),
)

return c.JSON(http.StatusOK, flagValue)
}
32 changes: 32 additions & 0 deletions cmd/relayproxy/testdata/opentelemetry/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Test OpenTelemetry Tracing

**GO Feature Flag** is able to generate some trace if you use the OpenTelemetry.

If you want to test it locally, you can use the following command to start a local OpenTelemetry collector:

```bash
docker run --rm --name jaeger \
-e COLLECTOR_ZIPKIN_HOST_PORT=:9411 \
-e "COLLECTOR_OTLP_ENABLED=true" \
-p 6831:6831/udp \
-p 6832:6832/udp \
-p 5778:5778 \
-p 16686:16686 \
-p 4317:4317 \
-p 4318:4318 \
-p 14250:14250 \
-p 14268:14268 \
-p 14269:14269 \
-p 9411:9411 \
jaegertracing/all-in-one
```

When your collector is up, you can configure GO Feature Flag relay-proxy by setting the OTLP endpoint in your configuration file.
```yaml
#...
openTelemetryOtlpEndpoint: http://localhost:4318
```
You can connect to **`jaeger`** at this address: http://localhost:16686/search.

After a 1st call to the API you will see a service call `go-feature-flag` and you can check the traces.
13 changes: 12 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ require (
github.com/swaggo/swag v1.16.2
github.com/xitongsys/parquet-go v1.6.2
github.com/xitongsys/parquet-go-source v0.0.0-20230830030807-0dd610dbff1d
go.opentelemetry.io/contrib/instrumentation/github.com/labstack/echo/otelecho v0.46.1
go.opentelemetry.io/otel v1.21.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.21.0
go.opentelemetry.io/otel/sdk v1.21.0
go.uber.org/zap v1.26.0
golang.org/x/net v0.19.0
golang.org/x/oauth2 v0.15.0
Expand Down Expand Up @@ -81,14 +85,16 @@ require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/blang/semver v3.5.1+incompatible // indirect
github.com/buger/jsonparser v1.1.1 // indirect
github.com/cenkalti/backoff/v4 v4.2.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/emicklei/go-restful/v3 v3.9.0 // indirect
github.com/evanphx/json-patch v5.6.0+incompatible // indirect
github.com/felixge/httpsnoop v1.0.2 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/ghodss/yaml v1.0.0 // indirect
github.com/go-logr/logr v1.2.4 // indirect
github.com/go-logr/logr v1.3.0 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/jsonpointer v0.19.6 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect
github.com/go-openapi/spec v0.20.4 // indirect
Expand All @@ -106,6 +112,7 @@ require (
github.com/googleapis/gax-go/v2 v2.12.0 // indirect
github.com/gorilla/handlers v1.5.1 // indirect
github.com/gorilla/mux v1.8.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
Expand Down Expand Up @@ -137,6 +144,10 @@ require (
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
go.opencensus.io v0.24.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 // indirect
go.opentelemetry.io/otel/metric v1.21.0 // indirect
go.opentelemetry.io/otel/trace v1.21.0 // indirect
go.opentelemetry.io/proto/otlp v1.0.0 // indirect
go.uber.org/multierr v1.10.0 // indirect
golang.org/x/crypto v0.16.0 // indirect
golang.org/x/exp v0.0.0-20230807204917-050eac23e9de // indirect
Expand Down
Loading

0 comments on commit ea12ff9

Please sign in to comment.