diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index 82db6a7..0000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,13 +0,0 @@ -version: 2 -updates: - - package-ecosystem: "github-actions" - directory: "/" - schedule: - interval: "weekly" - - package-ecosystem: gomod - directory: "/" - schedule: - interval: "weekly" - reviewers: - - "devcyclehq/engineering" - versioning-strategy: increase diff --git a/.idea/local-bucketing-proxy.iml b/.idea/local-bucketing-proxy.iml index 5e764c4..c1e61a1 100644 --- a/.idea/local-bucketing-proxy.iml +++ b/.idea/local-bucketing-proxy.iml @@ -1,6 +1,14 @@ - + + + + + diff --git a/README.md b/README.md index dc4d2ba..344e2e9 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ A simple healthcheck for each proxy instance can be performed by sending a GET r We recommend setting the file permissions for the unix socket to be as restrictive as possible. However, as a workaround for deployment issues, you can set the permissions to your own custom mask via the -`DVC_LB_PROXY_UNIX_SOCKET_PERMISSIONS` environment variable, or the unixSocketPermissions option in the config file. The +`DEVCYCLE_PROXY_UNIX_SOCKET_PERMISSIONS` environment variable, or the unixSocketPermissions option in the config file. The default is 0755 ### Command Line Arguments @@ -48,27 +48,27 @@ default is 0755 ### Environment Variables -| KEY | TYPE | DEFAULT | REQUIRED | DESCRIPTION | -|--------------------------------------------------------|---------------|---------|----------|---------------------------------------------------------------------------------| -| DVC_LB_PROXY_CONFIG | String | | | The path to a JSON configuration file. | -| DVC_LB_PROXY_UNIX_SOCKET_PATH | String | | | The path to the Unix socket. | -| DVC_LB_PROXY_HTTP_PORT | Integer | 8080 | | The port to listen on for HTTP requests. Defaults to 8080. | -| DVC_LB_PROXY_UNIX_SOCKET_ENABLED | True or False | false | | Whether to enable the Unix socket. Defaults to false. | -| DVC_LB_PROXY_UNIX_SOCKET_PERMISSIONS | String | 0755 | | The permissions to set on the Unix socket. Defaults to 0755 | -| DVC_LB_PROXY_HTTP_ENABLED | True or False | true | | Whether to enable the HTTP server. Defaults to true. | -| DVC_LB_PROXY_SDK_KEY | String | | true | The Server SDK key to use for this instance. | -| DVC_LB_PROXY_PLATFORMDATA_SDKTYPE | String | | | | -| DVC_LB_PROXY_PLATFORMDATA_SDKVERSION | String | | | | -| DVC_LB_PROXY_PLATFORMDATA_PLATFORMVERSION | String | | | | -| DVC_LB_PROXY_PLATFORMDATA_DEVICEMODEL | String | | | | -| DVC_LB_PROXY_PLATFORMDATA_PLATFORM | String | | | | -| DVC_LB_PROXY_PLATFORMDATA_HOSTNAME | String | | | | -| DVC_LB_PROXY_SDKCONFIG_EVENT_FLUSH_INTERVAL_MS | Duration | | | The interval at which events are flushed to the events api in milliseconds. | -| DVC_LB_PROXY_SDKCONFIG_CONFIG_POLLING_INTERVAL_MS | Duration | | | The interval at which the SDK polls the config CDN for updates in milliseconds. | -| DVC_LB_PROXY_SDKCONFIG_REQUEST_TIMEOUT | Duration | | | The timeout for requests to the config CDN and events API in milliseconds. | -| DVC_LB_PROXY_SDKCONFIG_DISABLE_AUTOMATIC_EVENT_LOGGING | True or False | false | | Whether to disable automatic event logging. Defaults to false. | -| DVC_LB_PROXY_SDKCONFIG_DISABLE_CUSTOM_EVENT_LOGGING | True or False | false | | Whether to disable custom event logging. Defaults to false. | -| DVC_LB_PROXY_SDKCONFIG_MAX_EVENT_QUEUE_SIZE | Integer | | | The maximum number of events to be in the queue before dropping events. | -| DVC_LB_PROXY_SDKCONFIG_FLUSH_EVENT_QUEUE_SIZE | Integer | | | The minimum number of events to be in the queue before flushing events. | -| DVC_LB_PROXY_SDKCONFIG_CONFIG_CDN_URI | String | | | The URI of the Config CDN - leave unspecified if not needing an outbound proxy. | -| DVC_LB_PROXY_SDKCONFIG_EVENTSAPIURI | String | | | The URI of the Events API - leave unspecified if not needing an outbound proxy. | \ No newline at end of file +| KEY | TYPE | DEFAULT | REQUIRED | DESCRIPTION | +|----------------------------------------------------------|---------------|---------|----------|---------------------------------------------------------------------------------| +| DEVCYCLE_PROXY_CONFIG | String | | | The path to a JSON configuration file. | +| DEVCYCLE_PROXY_UNIX_SOCKET_PATH | String | | | The path to the Unix socket. | +| DEVCYCLE_PROXY_HTTP_PORT | Integer | 8080 | | The port to listen on for HTTP requests. Defaults to 8080. | +| DEVCYCLE_PROXY_UNIX_SOCKET_ENABLED | True or False | false | | Whether to enable the Unix socket. Defaults to false. | +| DEVCYCLE_PROXY_UNIX_SOCKET_PERMISSIONS | String | 0755 | | The permissions to set on the Unix socket. Defaults to 0755 | +| DEVCYCLE_PROXY_HTTP_ENABLED | True or False | true | | Whether to enable the HTTP server. Defaults to true. | +| DEVCYCLE_PROXY_SDK_KEY | String | | true | The Server SDK key to use for this instance. | +| DEVCYCLE_PROXY_PLATFORMDATA_SDKTYPE | String | | | | +| DEVCYCLE_PROXY_PLATFORMDATA_SDKVERSION | String | | | | +| DEVCYCLE_PROXY_PLATFORMDATA_PLATFORMVERSION | String | | | | +| DEVCYCLE_PROXY_PLATFORMDATA_DEVICEMODEL | String | | | | +| DEVCYCLE_PROXY_PLATFORMDATA_PLATFORM | String | | | | +| DEVCYCLE_PROXY_PLATFORMDATA_HOSTNAME | String | | | | +| DEVCYCLE_PROXY_SDKCONFIG_EVENT_FLUSH_INTERVAL_MS | Duration | | | The interval at which events are flushed to the events api in milliseconds. | +| DEVCYCLE_PROXY_SDKCONFIG_CONFIG_POLLING_INTERVAL_MS | Duration | | | The interval at which the SDK polls the config CDN for updates in milliseconds. | +| DEVCYCLE_PROXY_SDKCONFIG_REQUEST_TIMEOUT | Duration | | | The timeout for requests to the config CDN and events API in milliseconds. | +| DEVCYCLE_PROXY_SDKCONFIG_DISABLE_AUTOMATIC_EVENT_LOGGING | True or False | false | | Whether to disable automatic event logging. Defaults to false. | +| DEVCYCLE_PROXY_SDKCONFIG_DISABLE_CUSTOM_EVENT_LOGGING | True or False | false | | Whether to disable custom event logging. Defaults to false. | +| DEVCYCLE_PROXY_SDKCONFIG_MAX_EVENT_QUEUE_SIZE | Integer | | | The maximum number of events to be in the queue before dropping events. | +| DEVCYCLE_PROXY_SDKCONFIG_FLUSH_EVENT_QUEUE_SIZE | Integer | | | The minimum number of events to be in the queue before flushing events. | +| DEVCYCLE_PROXY_SDKCONFIG_CONFIG_CDN_URI | String | | | The URI of the Config CDN - leave unspecified if not needing an outbound proxy. | +| DEVCYCLE_PROXY_SDKCONFIG_EVENTSAPIURI | String | | | The URI of the Events API - leave unspecified if not needing an outbound proxy. | \ No newline at end of file diff --git a/go.mod b/go.mod index 0146cbe..6d36026 100644 --- a/go.mod +++ b/go.mod @@ -3,10 +3,11 @@ module github.com/devcyclehq/sdk-proxy go 1.20 require ( - github.com/devcyclehq/go-server-sdk/v2 v2.15.0 + github.com/devcyclehq/go-server-sdk/v2 v2.16.0 github.com/gin-gonic/gin v1.10.0 github.com/kelseyhightower/envconfig v1.4.0 github.com/kr/pretty v0.3.1 + github.com/launchdarkly/eventsource v1.7.1 github.com/stretchr/testify v1.9.0 ) @@ -24,7 +25,6 @@ require ( github.com/go-playground/validator/v10 v10.20.0 // indirect github.com/goccy/go-json v0.10.2 // indirect github.com/google/uuid v1.3.0 // indirect - github.com/jarcoal/httpmock v1.2.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.2.7 // indirect github.com/kr/text v0.2.0 // indirect diff --git a/go.sum b/go.sum index 9377aa0..6d5dd57 100644 --- a/go.sum +++ b/go.sum @@ -11,8 +11,8 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/devcyclehq/go-server-sdk/v2 v2.15.0 h1:bK2Z9OTooACeETPvSq0h3JOoOwYUekcbxKb9gqgzfig= -github.com/devcyclehq/go-server-sdk/v2 v2.15.0/go.mod h1:/F805DZbz6ghCPCUf+au7UycoIEYlW62Kh8JN1da0+k= +github.com/devcyclehq/go-server-sdk/v2 v2.16.0 h1:8yefgJISXZgSIlXcCNMK+RGM6X9kuuB1nZAs3kzSExQ= +github.com/devcyclehq/go-server-sdk/v2 v2.16.0/go.mod h1:DzKrJ4s2apfphFwB/Aq8YDf7brB+NDr6IxX0TNi2c24= github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= @@ -36,7 +36,6 @@ github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/ github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/jarcoal/httpmock v1.2.0 h1:gSvTxxFR/MEMfsGrvRbdfpRUMBStovlSRLw0Ep1bwwc= -github.com/jarcoal/httpmock v1.2.0/go.mod h1:oCoTsnAz4+UoOUIf5lJOWV2QQIW5UoeUI6aM2YnWAZk= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= @@ -49,13 +48,16 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/launchdarkly/eventsource v1.7.1 h1:StoRQeiPyrcQIXjlQ7b5jWMzHW4p+GGczN2r2oBhujg= +github.com/launchdarkly/eventsource v1.7.1/go.mod h1:LHxSeb4OnqznNZxCSXbFghxS/CjIQfzHovNoAqbO/Wk= +github.com/launchdarkly/go-test-helpers/v2 v2.2.0 h1:L3kGILP/6ewikhzhdNkHy1b5y4zs50LueWenVF0sBbs= +github.com/launchdarkly/go-test-helpers/v2 v2.2.0/go.mod h1:L7+th5govYp5oKU9iN7To5PgznBuIjBPn+ejqKR0avw= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/matryer/try v0.0.0-20161228173917-9ac251b645a2 h1:JAEbJn3j/FrhdWA9jW8B5ajsLIjeuEHLi8xE4fk997o= github.com/matryer/try v0.0.0-20161228173917-9ac251b645a2/go.mod h1:0KeJpeMD6o+O4hW7qJOT7vyQPKrWmj26uf5wMc/IiIs= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/maxatome/go-testdeep v1.11.0 h1:Tgh5efyCYyJFGUYiT0qxBSIDeXw0F5zSoatlou685kk= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -75,6 +77,8 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= @@ -107,6 +111,8 @@ google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFW google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/http_endpoints.go b/http_endpoints.go index 7b0242c..d3ce2f5 100644 --- a/http_endpoints.go +++ b/http_endpoints.go @@ -8,6 +8,7 @@ import ( "strings" devcycle "github.com/devcyclehq/go-server-sdk/v2" + devcycle_api "github.com/devcyclehq/go-server-sdk/v2/api" "github.com/gin-gonic/gin" ) @@ -15,8 +16,9 @@ func Health(c *gin.Context) { c.Status(200) } -func Variable(client *devcycle.Client) gin.HandlerFunc { +func Variable() gin.HandlerFunc { return func(c *gin.Context) { + client := c.Value("devcycle").(*devcycle.Client) user := getUserFromBody(c) if user == nil { return @@ -50,8 +52,10 @@ func Variable(client *devcycle.Client) gin.HandlerFunc { } } -func Feature(client *devcycle.Client) gin.HandlerFunc { +func Feature() gin.HandlerFunc { return func(c *gin.Context) { + client := c.Value("devcycle").(*devcycle.Client) + user := getUserFromBody(c) if user == nil { return @@ -65,8 +69,10 @@ func Feature(client *devcycle.Client) gin.HandlerFunc { } } -func Track(client *devcycle.Client) gin.HandlerFunc { +func Track() gin.HandlerFunc { return func(c *gin.Context) { + client := c.Value("devcycle").(*devcycle.Client) + event := getEventFromBody(c) for _, e := range event.Events { _, err := client.Track(event.User.User, e) @@ -77,8 +83,10 @@ func Track(client *devcycle.Client) gin.HandlerFunc { } } -func BatchEvents(client *devcycle.Client) gin.HandlerFunc { +func BatchEvents() gin.HandlerFunc { return func(c *gin.Context) { + client := c.Value("devcycle").(*devcycle.Client) + // Passthrough proxy to the configured events api endpoint. httpC := http.DefaultClient req, err := http.NewRequest("POST", client.DevCycleOptions.EventsAPIURI+"/v1/events/batch", c.Request.Body) @@ -103,20 +111,61 @@ func BatchEvents(client *devcycle.Client) gin.HandlerFunc { } } -func GetConfig(client *devcycle.Client) gin.HandlerFunc { +func GetConfig() gin.HandlerFunc { return func(c *gin.Context) { + instance := c.Value("instance").(*ProxyInstance) + client := c.Value("devcycle").(*devcycle.Client) + if c.Param("sdkKey") == "" || !strings.HasSuffix(c.Param("sdkKey"), ".json") { c.AbortWithStatus(http.StatusForbidden) return } - + var ret []byte rawConfig, etag, err := client.GetRawConfig() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{}) + return + } + if instance.SSEEnabled { + config := map[string]interface{}{} + err = json.Unmarshal(rawConfig, &config) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{}) + return + } + hostname := fmt.Sprintf("http://%s:%d", instance.SSEHostname, instance.HTTPPort) + // This is the only indicator that a unix socket request was made + if c.Request.RemoteAddr == "" { + hostname = fmt.Sprintf("unix:%s", instance.UnixSocketPath) + } + fmt.Println(c.Request) + if val, ok := config["sse"]; ok { + path := val.(map[string]interface{})["path"].(string) + + config["sse"] = devcycle_api.SSEHost{ + Hostname: hostname, + Path: path, + } + } + + ret, err = json.Marshal(config) + } else { + ret = rawConfig + } + if err != nil { c.JSON(http.StatusInternalServerError, gin.H{}) return } c.Header("ETag", etag) - c.Data(http.StatusOK, "application/json", rawConfig) + c.Data(http.StatusOK, "application/json", ret) + } +} + +func SSE() gin.HandlerFunc { + return func(c *gin.Context) { + instance := c.Value("instance").(*ProxyInstance) + instance.sseServer.Handler(instance.SDKKey).ServeHTTP(c.Writer, c.Request) } } diff --git a/options.go b/options.go index 17481b0..b893f71 100644 --- a/options.go +++ b/options.go @@ -3,6 +3,8 @@ package local_bucketing_proxy import ( "encoding/json" "fmt" + "github.com/devcyclehq/go-server-sdk/v2/api" + "github.com/launchdarkly/eventsource" "log" "os" "runtime" @@ -14,7 +16,7 @@ import ( ) const ( - EnvVarPrefix = "DVC_LB_PROXY" + EnvVarPrefix = "DEVCYCLE_PROXY" ) type ProxyConfig struct { @@ -27,11 +29,15 @@ type ProxyInstance struct { UnixSocketEnabled bool `json:"unixSocketEnabled" envconfig:"UNIX_SOCKET_ENABLED" default:"false" desc:"Whether to enable the Unix socket. Defaults to false."` HTTPPort int `json:"httpPort" envconfig:"HTTP_PORT" default:"8080" desc:"The port to listen on for HTTP requests. Defaults to 8080."` HTTPEnabled bool `json:"httpEnabled" envconfig:"HTTP_ENABLED" default:"true" desc:"Whether to enable the HTTP server. Defaults to true."` + SSEEnabled bool `json:"sseEnabled" envconfig:"SSE_ENABLED" default:"false" desc:"Whether to enable the SSE server. Requires setting sseHostname param too. Defaults to false."` + SSEHostname string `json:"sseHostname" envconfig:"SSE_HOSTNAME" desc:"The hostname to provide to clients to connect to for SSE requests. This must be reachable from the clients and can be either a DNS hostname or a raw IP address."` SDKKey string `json:"sdkKey" required:"true" envconfig:"SDK_KEY" desc:"The Server SDK key to use for this instance."` LogFile string `json:"logFile" default:"/var/log/devcycle.log" envconfig:"LOG_FILE" desc:"The path to the log file. Defaults to /var/log/devcycle.log"` PlatformData devcycle.PlatformData `json:"platformData" required:"true"` SDKConfig SDKConfig `json:"sdkConfig" required:"true"` dvcClient *devcycle.Client + sseServer *eventsource.Server + sseEvents chan api.ClientEvent } type SDKConfig struct { @@ -63,15 +69,25 @@ func (i *ProxyInstance) BuildDevCycleOptions() *devcycle.Options { FlushEventQueueSize: i.SDKConfig.FlushEventQueueSize, ConfigCDNURI: i.SDKConfig.ConfigCDNURI, EventsAPIURI: i.SDKConfig.EventsAPIURI, - Logger: nil, + EnableBetaRealtimeUpdates: i.SSEEnabled, AdvancedOptions: devcycle.AdvancedOptions{ OverridePlatformData: &i.PlatformData, }, + ClientEventHandler: i.sseEvents, } options.CheckDefaults() return &options } +func (i *ProxyInstance) EventRebroadcaster() { + for event := range i.sseEvents { + if event.EventType == api.ClientEventType_RealtimeUpdates { + i.sseServer.Publish([]string{i.SDKKey}, event.EventData.(eventsource.Event)) + log.Printf("Rebroadcasting SSE event: %s\n", event.EventData.(eventsource.Event).Data()) + } + } +} + func (i *ProxyInstance) Default() { i.SDKConfig.Default() if i.HTTPEnabled && i.HTTPPort == 0 { @@ -88,6 +104,14 @@ func (i *ProxyInstance) Default() { i.UnixSocketPermissions = "0755" } } + if i.SSEEnabled && i.SSEHostname == "" { + hostname, err := os.Hostname() + if err != nil { + i.SSEHostname = "localhost" + } else { + i.SSEHostname = hostname + } + } } func (c *ProxyConfig) Default() { for i := range c.Instances { diff --git a/options_test.go b/options_test.go index 3b1512b..8151501 100644 --- a/options_test.go +++ b/options_test.go @@ -164,7 +164,7 @@ func TestParseConfig(t *testing.T) { HTTPEnabled: true, SDKKey: "dvc_YOUR_KEY_HERE", LogFile: "/var/log/devcycle.log", - + SSEEnabled: false, PlatformData: api.PlatformData{ SdkType: "server", SdkVersion: "2.10.2", diff --git a/proxy.go b/proxy.go index c84d8e8..c14c36e 100644 --- a/proxy.go +++ b/proxy.go @@ -2,6 +2,8 @@ package local_bucketing_proxy import ( "fmt" + "github.com/devcyclehq/go-server-sdk/v2/api" + "github.com/launchdarkly/eventsource" "io" "log" "os" @@ -22,31 +24,20 @@ func NewBucketingProxyInstance(instance *ProxyInstance) (*ProxyInstance, error) mw := io.MultiWriter(os.Stdout, logFile) log.SetOutput(mw) gin.DefaultWriter = mw + if instance.SSEEnabled { + instance.sseEvents = make(chan api.ClientEvent, 100) + instance.sseServer = eventsource.NewServer() + instance.sseServer.ReplayAll = false + eventsource.NewSliceRepository() + go instance.EventRebroadcaster() + log.Printf("Initialized SSE server at %s", instance.SSEHostname) + } options := instance.BuildDevCycleOptions() client, err := devcycle.NewClient(instance.SDKKey, options) instance.dvcClient = client - r := gin.New() - r.Use(gin.Logger()) - r.Use(gin.Recovery()) - r.GET("/healthz", Health) - v1 := r.Group("/v1") - v1.Use(DevCycleAuthRequired()) - { - // Bucketing API - v1.POST("/variables/:key", Variable(client)) - v1.POST("/variables", Variable(client)) - v1.POST("/features", Feature(client)) - v1.POST("/track", Track(client)) - // Events API - v1.POST("/events", Track(client)) - v1.POST("/events/batch", BatchEvents(client)) - } - configCDNv1 := r.Group("/config/v1") - { - configCDNv1.GET("/server/:sdkKey", GetConfig(client)) - } + r := newRouter(client, instance) if instance.HTTPEnabled { if instance.HTTPPort == 0 { @@ -87,3 +78,48 @@ func NewBucketingProxyInstance(instance *ProxyInstance) (*ProxyInstance, error) } return instance, err } + +// Add the DevCycle client to the request context +func devCycleMiddleware(client *devcycle.Client) gin.HandlerFunc { + return func(c *gin.Context) { + c.Set("devcycle", client) + c.Next() + } +} + +func sdkProxyMiddleware(instance *ProxyInstance) gin.HandlerFunc { + return func(c *gin.Context) { + c.Set("instance", instance) + c.Next() + } +} + +func newRouter(client *devcycle.Client, instance *ProxyInstance) *gin.Engine { + r := gin.New() + + r.Use(gin.Logger()) + r.Use(gin.Recovery()) + r.Use(devCycleMiddleware(client)) + r.Use(sdkProxyMiddleware(instance)) + r.GET("/healthz", Health) + v1 := r.Group("/v1") + v1.Use(DevCycleAuthRequired()) + { + // Bucketing API + v1.POST("/variables/:key", Variable()) + v1.POST("/variables", Variable()) + v1.POST("/features", Feature()) + v1.POST("/track", Track()) + // Events API + v1.POST("/events", Track()) + v1.POST("/events/batch", BatchEvents()) + } + configCDNv1 := r.Group("/config/v1") + { + configCDNv1.GET("/server/:sdkKey", GetConfig()) + } + + r.GET("/event-stream", SSE()) + + return r +}