Skip to content

Commit

Permalink
Merge branch 'main' into feat-otlp-support-grpc
Browse files Browse the repository at this point in the history
  • Loading branch information
thomaspoignant authored Oct 12, 2024
2 parents bd6c73c + c1bab4f commit d2f9e25
Show file tree
Hide file tree
Showing 109 changed files with 6,223 additions and 232 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ coverage: ## Run the tests of the project and export the coverage
&& cat coverage.cov.tmp | grep -v "/examples/" > coverage.cov

bench: ## Launch the benchmark test
$(GOTEST) -bench Benchmark -cpu 2 -run=^$$
$(GOTEST) -tags=bench -bench Benchmark -cpu 2 -run=^$$

## Lint:
lint: ## Use golintci-lint on your project
Expand Down
43 changes: 33 additions & 10 deletions cmd/relayproxy/api/lambda_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,54 @@ package api

import (
"context"
"strings"

"github.com/aws/aws-lambda-go/events"
"github.com/aws/aws-lambda-go/lambda"
echoadapter "github.com/awslabs/aws-lambda-go-api-proxy/echo"
"github.com/labstack/echo/v4"
)

// newAwsLambdaHandler is creating a new awsLambdaHandler struct with the echoadapter
// newAwsLambdaHandlerManager is creating a new awsLambdaHandler struct with the echoadapter
// to proxy all lambda event to echo.
func newAwsLambdaHandler(echoInstance *echo.Echo) awsLambdaHandler {
func newAwsLambdaHandlerManager(echoInstance *echo.Echo) awsLambdaHandler {
return awsLambdaHandler{
adapter: echoadapter.NewV2(echoInstance),
adapterAPIGtwV2: echoadapter.NewV2(echoInstance),
adapterALB: echoadapter.NewALB(echoInstance),
adapterAPIGtwV1: echoadapter.New(echoInstance),
}
}

type awsLambdaHandler struct {
adapter *echoadapter.EchoLambdaV2
adapterAPIGtwV2 *echoadapter.EchoLambdaV2
adapterAPIGtwV1 *echoadapter.EchoLambda
adapterALB *echoadapter.EchoLambdaALB
}

func (h *awsLambdaHandler) Start() {
lambda.Start(h.Handler)
func (h *awsLambdaHandler) GetAdapter(mode string) interface{} {
switch strings.ToUpper(mode) {
case "APIGATEWAYV1":
return h.HandlerAPIGatewayV1
case "ALB":
return h.HandlerALB
default:
return h.HandlerAPIGatewayV2
}
}

// Handler is the function that proxy the lambda events to echo calls.
func (h *awsLambdaHandler) Handler(ctx context.Context, req events.APIGatewayV2HTTPRequest) (
// HandlerAPIGatewayV2 is the function that proxy the lambda events to echo calls for API Gateway V2.
func (h *awsLambdaHandler) HandlerAPIGatewayV2(ctx context.Context, req events.APIGatewayV2HTTPRequest) (
events.APIGatewayV2HTTPResponse, error) {
return h.adapter.ProxyWithContext(ctx, req)
return h.adapterAPIGtwV2.ProxyWithContext(ctx, req)
}

// HandlerAPIGatewayV1 is the function that proxy the lambda events to echo calls for API Gateway V1.
func (h *awsLambdaHandler) HandlerAPIGatewayV1(ctx context.Context, req events.APIGatewayProxyRequest) (
events.APIGatewayProxyResponse, error) {
return h.adapterAPIGtwV1.ProxyWithContext(ctx, req)
}

// HandlerALB is the function that proxy the lambda events to echo calls for API Gateway V1.
func (h *awsLambdaHandler) HandlerALB(ctx context.Context, req events.ALBTargetGroupRequest) (
events.ALBTargetGroupResponse, error) {
return h.adapterALB.ProxyWithContext(ctx, req)
}
125 changes: 125 additions & 0 deletions cmd/relayproxy/api/lambda_handler_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
package api

import (
"context"
"encoding/json"
"strings"
"testing"

"github.com/aws/aws-lambda-go/events"
"github.com/aws/aws-lambda-go/lambda"
"github.com/stretchr/testify/require"
"github.com/thomaspoignant/go-feature-flag/cmd/relayproxy/config"
"github.com/thomaspoignant/go-feature-flag/cmd/relayproxy/metric"
"github.com/thomaspoignant/go-feature-flag/cmd/relayproxy/service"
"github.com/thomaspoignant/go-feature-flag/notifier"
"go.uber.org/zap"
)

func TestAwsLambdaHandler_GetAdapter(t *testing.T) {
type test struct {
name string
mode string
request interface{}
}

tests := []test{
{
name: "APIGatewayV2 event handler",
mode: "APIGatewayV2",
request: events.APIGatewayV2HTTPRequest{
RequestContext: events.APIGatewayV2HTTPRequestContext{
HTTP: events.APIGatewayV2HTTPRequestContextHTTPDescription{
Method: "GET",
Path: "/health",
},
},
Headers: map[string]string{
"Content-Type": "application/json",
},
Body: "",
},
},
{
name: "APIGatewayV1 event handler",
mode: "APIGatewayV1",
request: events.APIGatewayProxyRequest{
HTTPMethod: "GET",
Path: "/health",
RequestContext: events.APIGatewayProxyRequestContext{
Path: "/health",
HTTPMethod: "GET",
},
Headers: map[string]string{
"Content-Type": "application/json",
},
Body: "",
},
},
{
name: "ALB event handler",
mode: "ALB",
request: events.ALBTargetGroupRequest{
HTTPMethod: "GET",
Path: "/health",
Headers: map[string]string{
"Content-Type": "application/json",
},
Body: "",
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
z, err := zap.NewProduction()
require.NoError(t, err)
c := &config.Config{
StartAsAwsLambda: true,
AwsLambdaAdapter: tt.mode,
Retriever: &config.RetrieverConf{
Kind: "file",
Path: "../../../testdata/flag-config.yaml",
},
}
goff, err := service.NewGoFeatureFlagClient(c, z, []notifier.Notifier{})
require.NoError(t, err)
apiServer := New(c, service.Services{
MonitoringService: service.NewMonitoring(goff),
WebsocketService: service.NewWebsocketService(),
GOFeatureFlagService: goff,
Metrics: metric.Metrics{},
}, z)

reqJSON, err := json.Marshal(tt.request)
require.NoError(t, err)

// Create a Lambda handler
handler := lambda.NewHandler(apiServer.getLambdaHandler())

// Invoke the handler with the mock event
response, err := handler.Invoke(context.Background(), reqJSON)
require.NoError(t, err)

switch strings.ToLower(tt.mode) {
case "apigatewayv2":
var res events.APIGatewayV2HTTPResponse
err = json.Unmarshal(response, &res)
require.NoError(t, err)
require.Equal(t, 200, res.StatusCode)
case "apigatewayv1":
var res events.APIGatewayProxyResponse
err = json.Unmarshal(response, &res)
require.NoError(t, err)
require.Equal(t, 200, res.StatusCode)
case "alb":
var res events.ALBTargetGroupResponse
err = json.Unmarshal(response, &res)
require.NoError(t, err)
require.Equal(t, 200, res.StatusCode)
default:
require.Fail(t, "not implemented")
}
})
}
}
5 changes: 5 additions & 0 deletions cmd/relayproxy/api/routes_monitoring.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package api

import (
"github.com/labstack/echo-contrib/echoprometheus"
"github.com/labstack/echo-contrib/pprof"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
custommiddleware "github.com/thomaspoignant/go-feature-flag/cmd/relayproxy/api/middleware"
Expand Down Expand Up @@ -39,4 +40,8 @@ func (s *Server) initMonitoringEndpoint(echoInstance *echo.Echo) {
// health Routes
echoInstance.GET("/health", cHealth.Handler)
echoInstance.GET("/info", cInfo.Handler)

if s.config.Debug {
pprof.Register(echoInstance)
}
}
79 changes: 79 additions & 0 deletions cmd/relayproxy/api/routes_monitoring_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package api_test

import (
"fmt"
"net/http"
"testing"

"github.com/stretchr/testify/require"
"github.com/thomaspoignant/go-feature-flag/cmd/relayproxy/api"

Check failure on line 9 in cmd/relayproxy/api/routes_monitoring_test.go

View workflow job for this annotation

GitHub Actions / Lint

could not import github.com/thomaspoignant/go-feature-flag/cmd/relayproxy/api (-: # github.com/thomaspoignant/go-feature-flag/cmd/relayproxy/api [github.com/thomaspoignant/go-feature-flag/cmd/relayproxy/api.test]
"github.com/thomaspoignant/go-feature-flag/cmd/relayproxy/config"
"github.com/thomaspoignant/go-feature-flag/cmd/relayproxy/metric"
"github.com/thomaspoignant/go-feature-flag/cmd/relayproxy/service"
"github.com/thomaspoignant/go-feature-flag/notifier"
"go.uber.org/zap"
)

func TestPprofEndpointsStarts(t *testing.T) {
type test struct {
name string
MonitoringPort int
Debug bool
expectedStatusCode int
}
tests := []test{
{
name: "pprof available in proxy port",
Debug: true,
expectedStatusCode: http.StatusOK,
},
{
name: "pprof available in monitoring port",
Debug: true,
MonitoringPort: 1032,
expectedStatusCode: http.StatusOK,
},
{
name: "pprof not available ii debug not enabled",
Debug: false,
MonitoringPort: 1032,
expectedStatusCode: http.StatusNotFound,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
z, err := zap.NewProduction()
require.NoError(t, err)
c := &config.Config{
Retriever: &config.RetrieverConf{
Kind: "file",
Path: "../../../testdata/flag-config.yaml",
},
MonitoringPort: tt.MonitoringPort,
ListenPort: 1031,
Debug: tt.Debug,
}

goff, err := service.NewGoFeatureFlagClient(c, z, []notifier.Notifier{})
require.NoError(t, err)
apiServer := api.New(c, service.Services{
MonitoringService: service.NewMonitoring(goff),
WebsocketService: service.NewWebsocketService(),
GOFeatureFlagService: goff,
Metrics: metric.Metrics{},
}, z)

portToCheck := c.ListenPort
if tt.MonitoringPort != 0 {
portToCheck = tt.MonitoringPort
}

go apiServer.Start()
defer apiServer.Stop()
resp, err := http.Get(fmt.Sprintf("http://localhost:%d/debug/pprof/heap", portToCheck))
require.NoError(t, err)
require.Equal(t, tt.expectedStatusCode, resp.StatusCode)
})
}
}
21 changes: 14 additions & 7 deletions cmd/relayproxy/api/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"strings"
"time"

"github.com/aws/aws-lambda-go/lambda"
"github.com/labstack/echo-contrib/echoprometheus"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
Expand Down Expand Up @@ -119,10 +120,12 @@ func (s *Server) Start() {
}

// start the OpenTelemetry tracing service
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
if s.config.OpenTelemetryOtlpEndpoint != "" {
err := s.otelService.Init(context.Background(), *s.config)

Check failure on line 124 in cmd/relayproxy/api/server.go

View workflow job for this annotation

GitHub Actions / Coverage

not enough arguments in call to s.otelService.Init

Check failure on line 124 in cmd/relayproxy/api/server.go

View workflow job for this annotation

GitHub Actions / Build

not enough arguments in call to s.otelService.Init

Check failure on line 124 in cmd/relayproxy/api/server.go

View workflow job for this annotation

GitHub Actions / Test

not enough arguments in call to s.otelService.Init

Check failure on line 124 in cmd/relayproxy/api/server.go

View workflow job for this annotation

GitHub Actions / Integration Tests

not enough arguments in call to s.otelService.Init

Check failure on line 124 in cmd/relayproxy/api/server.go

View workflow job for this annotation

GitHub Actions / Lint

not enough arguments in call to s.otelService.Init

Check failure on line 124 in cmd/relayproxy/api/server.go

View workflow job for this annotation

GitHub Actions / Lint

not enough arguments in call to s.otelService.Init

Check failure on line 124 in cmd/relayproxy/api/server.go

View workflow job for this annotation

GitHub Actions / Lint

not enough arguments in call to s.otelService.Init
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
}
}

// starting the main application
Expand All @@ -135,16 +138,20 @@ func (s *Server) Start() {
zap.String("address", address),
zap.String("version", s.config.Version))

err = s.apiEcho.Start(address)
err := s.apiEcho.Start(address)
if err != nil && !errors.Is(err, http.ErrServerClosed) {
s.zapLog.Fatal("Error starting relay proxy", zap.Error(err))
}
}

// StartAwsLambda is starting the relay proxy as an AWS Lambda
func (s *Server) StartAwsLambda() {
adapter := newAwsLambdaHandler(s.apiEcho)
adapter.Start()
lambda.Start(s.getLambdaHandler())
}

func (s *Server) getLambdaHandler() interface{} {
handlerMngr := newAwsLambdaHandlerManager(s.apiEcho)
return handlerMngr.GetAdapter(s.config.AwsLambdaAdapter)
}

// Stop shutdown the API server
Expand Down
9 changes: 8 additions & 1 deletion cmd/relayproxy/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,9 @@ type Config struct {
// HideBanner (optional) if true, we don't display the go-feature-flag relay proxy banner
HideBanner bool `mapstructure:"hideBanner" koanf:"hidebanner"`

// Debug (optional) if true, go-feature-flag relay proxy will run on debug mode, with more logs and custom responses
// Debug (optional) if true, go-feature-flag relay proxy will run on debug mode, with more logs and custom responses.
// It will also start the pprof endpoints on the same port as the monitoring.
// Default: false
Debug bool `mapstructure:"debug" koanf:"debug"`

// EnableSwagger (optional) to have access to the swagger
Expand Down Expand Up @@ -216,6 +218,11 @@ type Config struct {
// StartAsAwsLambda (optional) if true, the relay proxy will start ready to be launched as AWS Lambda
StartAsAwsLambda bool `mapstructure:"startAsAwsLambda" koanf:"startasawslambda"`

// AwsLambdaAdapter (optional) is the adapter to use when the relay proxy is started as an AWS Lambda.
// Possible values are "APIGatewayV1", "APIGatewayV2" and "ALB"
// Default: "APIGatewayV2"
AwsLambdaAdapter string `mapstructure:"awsLambdaAdapter" koanf:"awslambdaadapter"`

// EvaluationContextEnrichment (optional) will be merged with the evaluation context sent during the evaluation.
// It is useful to add common attributes to all the evaluations, such as a server version, environment, ...
//
Expand Down
12 changes: 10 additions & 2 deletions cmd/relayproxy/controller/flag_change.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ func NewAPIFlagChange(goFF *ffclient.GoFeatureFlag, metrics metric.Metrics) Cont
}

type FlagChangeResponse struct {
Hash uint32 `json:"hash"`
Hash uint32 `json:"hash"`
Flags map[string]uint32 `json:"flags"`
}

// Handler is the endpoint to poll if you want to know if there is a configuration change in the flags
Expand All @@ -48,5 +49,12 @@ func (h *FlagChangeAPICtrl) Handler(c echo.Context) error {
if err != nil {
return c.JSON(http.StatusInternalServerError, err)
}
return c.JSON(http.StatusOK, FlagChangeResponse{Hash: utils.Hash(string(res))})

flagHashes := map[string]uint32{}
for key, flag := range flags {
jsonFlag, _ := json.Marshal(flag)
flagHashes[key] = utils.Hash(string(jsonFlag))
}

return c.JSON(http.StatusOK, FlagChangeResponse{Hash: utils.Hash(string(res)), Flags: flagHashes})
}
Loading

0 comments on commit d2f9e25

Please sign in to comment.