diff --git a/README.md b/README.md index 714257faedc..8869a0a6ed3 100644 --- a/README.md +++ b/README.md @@ -560,7 +560,8 @@ It represents individual flag evaluations and is considered "full fidelity" even "key": "test-flag", "variation": "Default", "value": false, - "default": false + "default": false, + "source": "SERVER" } ``` The format of the data is [described in the documentation](https://gofeatureflag.org/docs/). diff --git a/cmd/relayproxy/config/config.go b/cmd/relayproxy/config/config.go index 225a95d3406..a6d1bd9ae3a 100644 --- a/cmd/relayproxy/config/config.go +++ b/cmd/relayproxy/config/config.go @@ -45,7 +45,7 @@ var DefaultExporter = struct { LogFormat: "[{{ .FormattedDate}}] user=\"{{ .UserKey}}\", flag=\"{{ .Key}}\", value=\"{{ .Value}}\"", FileName: "flag-variation-{{ .Hostname}}-{{ .Timestamp}}.{{ .Format}}", CsvFormat: "{{ .Kind}};{{ .ContextKind}};{{ .UserKey}};{{ .CreationDate}};{{ .Key}};{{ .Variation}};" + - "{{ .Value}};{{ .Default}}\n", + "{{ .Value}};{{ .Default}};{{ .Source}}\n", FlushInterval: 60000 * time.Millisecond, MaxEventInMemory: 100000, ParquetCompressionCodec: parquet.CompressionCodec_SNAPPY.String(), diff --git a/cmd/relayproxy/controller/collect_eval_data.go b/cmd/relayproxy/controller/collect_eval_data.go index a065157bfcb..fdc5a1e2b40 100644 --- a/cmd/relayproxy/controller/collect_eval_data.go +++ b/cmd/relayproxy/controller/collect_eval_data.go @@ -47,6 +47,9 @@ func (h *collectEvalData) Handler(c echo.Context) error { } for _, event := range reqBody.Events { + if event.Source == "" { + event.Source = "PROVIDER_CACHE" + } h.goFF.CollectEventData(event) } diff --git a/cmd/relayproxy/controller/collect_eval_data_test.go b/cmd/relayproxy/controller/collect_eval_data_test.go index 48ae0188329..4fff7c1af0f 100644 --- a/cmd/relayproxy/controller/collect_eval_data_test.go +++ b/cmd/relayproxy/controller/collect_eval_data_test.go @@ -2,14 +2,6 @@ package controller_test import ( "context" - "github.com/labstack/echo-contrib/prometheus" - "github.com/labstack/echo/v4" - "github.com/stretchr/testify/assert" - ffclient "github.com/thomaspoignant/go-feature-flag" - "github.com/thomaspoignant/go-feature-flag/cmd/relayproxy/controller" - "github.com/thomaspoignant/go-feature-flag/cmd/relayproxy/metric" - "github.com/thomaspoignant/go-feature-flag/exporter/fileexporter" - "github.com/thomaspoignant/go-feature-flag/retriever/fileretriever" "io" "log" "net/http" @@ -19,6 +11,15 @@ import ( "strings" "testing" "time" + + "github.com/labstack/echo-contrib/prometheus" + "github.com/labstack/echo/v4" + "github.com/stretchr/testify/assert" + ffclient "github.com/thomaspoignant/go-feature-flag" + "github.com/thomaspoignant/go-feature-flag/cmd/relayproxy/controller" + "github.com/thomaspoignant/go-feature-flag/cmd/relayproxy/metric" + "github.com/thomaspoignant/go-feature-flag/exporter/fileexporter" + "github.com/thomaspoignant/go-feature-flag/retriever/fileretriever" ) func Test_collect_eval_data_Handler(t *testing.T) { @@ -49,6 +50,17 @@ func Test_collect_eval_data_Handler(t *testing.T) { collectedDataFile: "../testdata/controller/collect_eval_data/valid_collected_data.json", }, }, + { + name: "valid with source field", + args: args{ + bodyFile: "../testdata/controller/collect_eval_data/request_with_source_field.json", + }, + want: want{ + httpCode: http.StatusOK, + bodyFile: "../testdata/controller/collect_eval_data/valid_response.json", + collectedDataFile: "../testdata/controller/collect_eval_data/collected_data_with_source_field.json", + }, + }, { name: "invalid json", args: args{ diff --git a/cmd/relayproxy/docs/docs.go b/cmd/relayproxy/docs/docs.go index 409ec08c9f2..89d917fe0a8 100644 --- a/cmd/relayproxy/docs/docs.go +++ b/cmd/relayproxy/docs/docs.go @@ -298,6 +298,11 @@ const docTemplate = `{ "type": "string", "example": "feature" }, + "source": { + "description": "Source indicates where the event was generated.\nThis is set to SERVER when the event was evaluated in the relay-proxy and PROVIDER_CACHE when it is evaluated from the cache.", + "type": "string", + "example": "SERVER" + }, "userKey": { "description": "UserKey The key of the user object used in a feature flag evaluation. Details for the user object used in a feature\nflag evaluation as reported by the \"feature\" event are transmitted periodically with a separate index event.", "type": "string", diff --git a/cmd/relayproxy/docs/swagger.json b/cmd/relayproxy/docs/swagger.json index a4cabb70a05..1544d4aef9d 100644 --- a/cmd/relayproxy/docs/swagger.json +++ b/cmd/relayproxy/docs/swagger.json @@ -290,6 +290,11 @@ "type": "string", "example": "feature" }, + "source": { + "description": "Source indicates where the event was generated.\nThis is set to SERVER when the event was evaluated in the relay-proxy and PROVIDER_CACHE when it is evaluated from the cache.", + "type": "string", + "example": "SERVER" + }, "userKey": { "description": "UserKey The key of the user object used in a feature flag evaluation. Details for the user object used in a feature\nflag evaluation as reported by the \"feature\" event are transmitted periodically with a separate index event.", "type": "string", diff --git a/cmd/relayproxy/docs/swagger.yaml b/cmd/relayproxy/docs/swagger.yaml index 16781352116..af871078a5c 100644 --- a/cmd/relayproxy/docs/swagger.yaml +++ b/cmd/relayproxy/docs/swagger.yaml @@ -29,6 +29,12 @@ definitions: A feature event will only be generated if the trackEvents attribute of the flag is set to true. example: feature type: string + source: + description: |- + Source indicates where the event was generated. + This is set to SERVER when the event was evaluated in the relay-proxy and PROVIDER_CACHE when it is evaluated from the cache. + example: SERVER + type: string userKey: description: |- UserKey The key of the user object used in a feature flag evaluation. Details for the user object used in a feature diff --git a/cmd/relayproxy/testdata/controller/collect_eval_data/collected_data_with_source_field.json b/cmd/relayproxy/testdata/controller/collect_eval_data/collected_data_with_source_field.json new file mode 100644 index 00000000000..6bc4280cd9b --- /dev/null +++ b/cmd/relayproxy/testdata/controller/collect_eval_data/collected_data_with_source_field.json @@ -0,0 +1 @@ +{"kind":"feature","contextKind":"user","userKey":"94a25909-20d8-40cc-8500-fee99b569345","creationDate":1680246000011,"key":"my-feature-flag","variation":"admin-variation","value":"string","default":false,"version":"v1.0.0","source":"EDGE"} diff --git a/cmd/relayproxy/testdata/controller/collect_eval_data/request_with_source_field.json b/cmd/relayproxy/testdata/controller/collect_eval_data/request_with_source_field.json new file mode 100644 index 00000000000..ad1a0ee66d5 --- /dev/null +++ b/cmd/relayproxy/testdata/controller/collect_eval_data/request_with_source_field.json @@ -0,0 +1,16 @@ +{ + "events": [ + { + "contextKind": "user", + "creationDate": 1680246000011, + "default": false, + "key": "my-feature-flag", + "kind": "feature", + "userKey": "94a25909-20d8-40cc-8500-fee99b569345", + "value": "string", + "variation": "admin-variation", + "version": "v1.0.0", + "source": "EDGE" + } + ] +} \ No newline at end of file diff --git a/cmd/relayproxy/testdata/controller/collect_eval_data/valid_collected_data.json b/cmd/relayproxy/testdata/controller/collect_eval_data/valid_collected_data.json index 7c98528f070..e5fb25016ea 100644 --- a/cmd/relayproxy/testdata/controller/collect_eval_data/valid_collected_data.json +++ b/cmd/relayproxy/testdata/controller/collect_eval_data/valid_collected_data.json @@ -1 +1 @@ -{"kind":"feature","contextKind":"user","userKey":"94a25909-20d8-40cc-8500-fee99b569345","creationDate":1680246000011,"key":"my-feature-flag","variation":"admin-variation","value":"string","default":false,"version":"v1.0.0"} +{"kind":"feature","contextKind":"user","userKey":"94a25909-20d8-40cc-8500-fee99b569345","creationDate":1680246000011,"key":"my-feature-flag","variation":"admin-variation","value":"string","default":false,"version":"v1.0.0","source":"PROVIDER_CACHE"} diff --git a/examples/data_export_file/main.go b/examples/data_export_file/main.go index 498c97e5f9e..dad7930783b 100644 --- a/examples/data_export_file/main.go +++ b/examples/data_export_file/main.go @@ -2,11 +2,12 @@ package main import ( "context" - "github.com/thomaspoignant/go-feature-flag/ffcontext" "log" "os" "time" + "github.com/thomaspoignant/go-feature-flag/ffcontext" + "github.com/thomaspoignant/go-feature-flag/exporter/fileexporter" "github.com/thomaspoignant/go-feature-flag/retriever/fileretriever" @@ -60,13 +61,13 @@ func main() { The output will be something like that: flag-variation-EXAMPLE-.json: - {"kind":"feature","contextKind":"anonymousUser","userKey":"aea2fdc1-b9a0-417a-b707-0c9083de68e3","creationDate":1618234129,"key":"new-admin-access","variation":"True","value":true,"default":false} - {"kind":"feature","contextKind":"user","userKey":"332460b9-a8aa-4f7a-bc5d-9cc33632df9a","creationDate":1618234129,"key":"new-admin-access","variation":"False","value":false,"default":false} - {"kind":"feature","contextKind":"anonymousUser","userKey":"aea2fdc1-b9a0-417a-b707-0c9083de68e3","creationDate":1618234129,"key":"unknown-flag","variation":"SdkDefault","value":"defaultValue","default":true} - {"kind":"feature","contextKind":"anonymousUser","userKey":"aea2fdc1-b9a0-417a-b707-0c9083de68e3","creationDate":1618234129,"key":"unknown-flag-2","variation":"SdkDefault","value":{"test":"toto"},"default":true} + {"kind":"feature","contextKind":"anonymousUser","userKey":"aea2fdc1-b9a0-417a-b707-0c9083de68e3","creationDate":1618234129,"key":"new-admin-access","variation":"True","value":true,"default":false,"source":"SERVER"} + {"kind":"feature","contextKind":"user","userKey":"332460b9-a8aa-4f7a-bc5d-9cc33632df9a","creationDate":1618234129,"key":"new-admin-access","variation":"False","value":false,"default":false,"source":"SERVER"} + {"kind":"feature","contextKind":"anonymousUser","userKey":"aea2fdc1-b9a0-417a-b707-0c9083de68e3","creationDate":1618234129,"key":"unknown-flag","variation":"SdkDefault","value":"defaultValue","default":true,"source":"SERVER"} + {"kind":"feature","contextKind":"anonymousUser","userKey":"aea2fdc1-b9a0-417a-b707-0c9083de68e3","creationDate":1618234129,"key":"unknown-flag-2","variation":"SdkDefault","value":{"test":"toto"},"default":true,"source":"SERVER"} ---- flag-variation-EXAMPLE-.json: - {"kind":"feature","contextKind":"anonymousUser","userKey":"aea2fdc1-b9a0-417a-b707-0c9083de68e3","creationDate":1618234131,"key":"new-admin-access","variation":"True","value":true,"default":false} - {"kind":"feature","contextKind":"user","userKey":"332460b9-a8aa-4f7a-bc5d-9cc33632df9a","creationDate":1618234131,"key":"new-admin-access","variation":"False","value":false,"default":false} + {"kind":"feature","contextKind":"anonymousUser","userKey":"aea2fdc1-b9a0-417a-b707-0c9083de68e3","creationDate":1618234131,"key":"new-admin-access","variation":"True","value":true,"default":false,"source":"SERVER"} + {"kind":"feature","contextKind":"user","userKey":"332460b9-a8aa-4f7a-bc5d-9cc33632df9a","creationDate":1618234131,"key":"new-admin-access","variation":"False","value":false,"default":false,"source":"SERVER"} */ } diff --git a/examples/data_export_googlecloudstorage/main.go b/examples/data_export_googlecloudstorage/main.go index ead016bc152..9157dee3bb4 100644 --- a/examples/data_export_googlecloudstorage/main.go +++ b/examples/data_export_googlecloudstorage/main.go @@ -2,11 +2,12 @@ package main import ( "context" - "github.com/thomaspoignant/go-feature-flag/ffcontext" "log" "os" "time" + "github.com/thomaspoignant/go-feature-flag/ffcontext" + "github.com/thomaspoignant/go-feature-flag/exporter/gcstorageexporter" "github.com/thomaspoignant/go-feature-flag/retriever/fileretriever" "google.golang.org/api/option" @@ -69,13 +70,13 @@ func main() { /* The content of those files should looks like: /go-feature-flag/variations/flag-variation-EXAMPLE-.json: - {"kind":"feature","contextKind":"anonymousUser","userKey":"aea2fdc1-b9a0-417a-b707-0c9083de68e3","creationDate":1618234129,"key":"new-admin-access","variation":"True","value":true,"default":false} - {"kind":"feature","contextKind":"user","userKey":"332460b9-a8aa-4f7a-bc5d-9cc33632df9a","creationDate":1618234129,"key":"new-admin-access","variation":"False","value":false,"default":false} - {"kind":"feature","contextKind":"anonymousUser","userKey":"aea2fdc1-b9a0-417a-b707-0c9083de68e3","creationDate":1618234129,"key":"unknown-flag","variation":"SdkDefault","value":"defaultValue","default":true} - {"kind":"feature","contextKind":"anonymousUser","userKey":"aea2fdc1-b9a0-417a-b707-0c9083de68e3","creationDate":1618234129,"key":"unknown-flag-2","variation":"SdkDefault","value":{"test":"toto"},"default":true} + {"kind":"feature","contextKind":"anonymousUser","userKey":"aea2fdc1-b9a0-417a-b707-0c9083de68e3","creationDate":1618234129,"key":"new-admin-access","variation":"True","value":true,"default":false,"source":"SERVER"} + {"kind":"feature","contextKind":"user","userKey":"332460b9-a8aa-4f7a-bc5d-9cc33632df9a","creationDate":1618234129,"key":"new-admin-access","variation":"False","value":false,"default":false,"source":"SERVER"} + {"kind":"feature","contextKind":"anonymousUser","userKey":"aea2fdc1-b9a0-417a-b707-0c9083de68e3","creationDate":1618234129,"key":"unknown-flag","variation":"SdkDefault","value":"defaultValue","default":true,"source":"SERVER"} + {"kind":"feature","contextKind":"anonymousUser","userKey":"aea2fdc1-b9a0-417a-b707-0c9083de68e3","creationDate":1618234129,"key":"unknown-flag-2","variation":"SdkDefault","value":{"test":"toto"},"default":true,"source":"SERVER"} ---- /go-feature-flag/variations/flag-variation-EXAMPLE-.json: - {"kind":"feature","contextKind":"anonymousUser","userKey":"aea2fdc1-b9a0-417a-b707-0c9083de68e3","creationDate":1618234131,"key":"new-admin-access","variation":"True","value":true,"default":false} - {"kind":"feature","contextKind":"user","userKey":"332460b9-a8aa-4f7a-bc5d-9cc33632df9a","creationDate":1618234131,"key":"new-admin-access","variation":"False","value":false,"default":false} + {"kind":"feature","contextKind":"anonymousUser","userKey":"aea2fdc1-b9a0-417a-b707-0c9083de68e3","creationDate":1618234131,"key":"new-admin-access","variation":"True","value":true,"default":false,"source":"SERVER"} + {"kind":"feature","contextKind":"user","userKey":"332460b9-a8aa-4f7a-bc5d-9cc33632df9a","creationDate":1618234131,"key":"new-admin-access","variation":"False","value":false,"default":false,"source":"SERVER"} */ } diff --git a/examples/data_export_s3/main.go b/examples/data_export_s3/main.go index df5293fa2bd..b92362fabcf 100644 --- a/examples/data_export_s3/main.go +++ b/examples/data_export_s3/main.go @@ -2,14 +2,15 @@ package main import ( "context" + "log" + "os" + "time" + "github.com/aws/aws-sdk-go-v2/config" ffclient "github.com/thomaspoignant/go-feature-flag" "github.com/thomaspoignant/go-feature-flag/exporter/s3exporterv2" "github.com/thomaspoignant/go-feature-flag/ffcontext" "github.com/thomaspoignant/go-feature-flag/retriever/fileretriever" - "log" - "os" - "time" ) func main() { @@ -68,13 +69,13 @@ func main() { /* The content of those files should looks like: /go-feature-flag/variations/flag-variation-EXAMPLE-.json: - {"kind":"feature","contextKind":"anonymousUser","userKey":"aea2fdc1-b9a0-417a-b707-0c9083de68e3","creationDate":1618234129,"key":"new-admin-access","variation":"True","value":true,"default":false} - {"kind":"feature","contextKind":"user","userKey":"332460b9-a8aa-4f7a-bc5d-9cc33632df9a","creationDate":1618234129,"key":"new-admin-access","variation":"False","value":false,"default":false} - {"kind":"feature","contextKind":"anonymousUser","userKey":"aea2fdc1-b9a0-417a-b707-0c9083de68e3","creationDate":1618234129,"key":"unknown-flag","variation":"SdkDefault","value":"defaultValue","default":true} - {"kind":"feature","contextKind":"anonymousUser","userKey":"aea2fdc1-b9a0-417a-b707-0c9083de68e3","creationDate":1618234129,"key":"unknown-flag-2","variation":"SdkDefault","value":{"test":"toto"},"default":true} + {"kind":"feature","contextKind":"anonymousUser","userKey":"aea2fdc1-b9a0-417a-b707-0c9083de68e3","creationDate":1618234129,"key":"new-admin-access","variation":"True","value":true,"default":false,"source":"SERVER"} + {"kind":"feature","contextKind":"user","userKey":"332460b9-a8aa-4f7a-bc5d-9cc33632df9a","creationDate":1618234129,"key":"new-admin-access","variation":"False","value":false,"default":false,"source":"SERVER"} + {"kind":"feature","contextKind":"anonymousUser","userKey":"aea2fdc1-b9a0-417a-b707-0c9083de68e3","creationDate":1618234129,"key":"unknown-flag","variation":"SdkDefault","value":"defaultValue","default":true,"source":"SERVER"} + {"kind":"feature","contextKind":"anonymousUser","userKey":"aea2fdc1-b9a0-417a-b707-0c9083de68e3","creationDate":1618234129,"key":"unknown-flag-2","variation":"SdkDefault","value":{"test":"toto"},"default":true,"source":"SERVER"} ---- /go-feature-flag/variations/flag-variation-EXAMPLE-.json: - {"kind":"feature","contextKind":"anonymousUser","userKey":"aea2fdc1-b9a0-417a-b707-0c9083de68e3","creationDate":1618234131,"key":"new-admin-access","variation":"True","value":true,"default":false} - {"kind":"feature","contextKind":"user","userKey":"332460b9-a8aa-4f7a-bc5d-9cc33632df9a","creationDate":1618234131,"key":"new-admin-access","variation":"False","value":false,"default":false} + {"kind":"feature","contextKind":"anonymousUser","userKey":"aea2fdc1-b9a0-417a-b707-0c9083de68e3","creationDate":1618234131,"key":"new-admin-access","variation":"True","value":true,"default":false,"source":"SERVER"} + {"kind":"feature","contextKind":"user","userKey":"332460b9-a8aa-4f7a-bc5d-9cc33632df9a","creationDate":1618234131,"key":"new-admin-access","variation":"False","value":false,"default":false,"source":"SERVER"} */ } diff --git a/exporter/common.go b/exporter/common.go index cdbece99bd1..152173f2c89 100644 --- a/exporter/common.go +++ b/exporter/common.go @@ -11,7 +11,7 @@ import ( ) const DefaultCsvTemplate = "{{ .Kind}};{{ .ContextKind}};{{ .UserKey}};{{ .CreationDate}};{{ .Key}};{{ .Variation}};" + - "{{ .Value}};{{ .Default}}\n" + "{{ .Value}};{{ .Default}};{{ .Source}}\n" const DefaultFilenameTemplate = "flag-variation-{{ .Hostname}}-{{ .Timestamp}}.{{ .Format}}" // ParseTemplate is parsing the template given by the config or use the default template diff --git a/exporter/common_test.go b/exporter/common_test.go index 7ff9cb7b65c..5ec0d759306 100644 --- a/exporter/common_test.go +++ b/exporter/common_test.go @@ -133,10 +133,10 @@ func TestFormatEventInCSV(t *testing.T) { csvTemplate: exporter.ParseTemplate("exporterExample", exporter.DefaultCsvTemplate, exporter.DefaultCsvTemplate), event: exporter.FeatureEvent{ Kind: "feature", ContextKind: "anonymousUser", UserKey: "ABCD", CreationDate: 1617970547, Key: "random-key", - Variation: "Default", Value: "YO", Default: false, + Variation: "Default", Value: "YO", Default: false, Source: "SERVER", }, }, - want: "feature;anonymousUser;ABCD;1617970547;random-key;Default;YO;false\n", + want: "feature;anonymousUser;ABCD;1617970547;random-key;Default;YO;false;SERVER\n", wantErr: assert.NoError, }, } @@ -165,9 +165,9 @@ func TestFormatEventInJSON(t *testing.T) { name: "valid", args: args{event: exporter.FeatureEvent{ Kind: "feature", ContextKind: "anonymousUser", UserKey: "ABCD", CreationDate: 1617970547, Key: "random-key", - Variation: "Default", Value: "YO", Default: false, + Variation: "Default", Value: "YO", Default: false, Source: "SERVER", }}, - want: "{\"kind\":\"feature\",\"contextKind\":\"anonymousUser\",\"userKey\":\"ABCD\",\"creationDate\":1617970547,\"key\":\"random-key\",\"variation\":\"Default\",\"value\":\"YO\",\"default\":false,\"version\":\"\"}\n", + want: "{\"kind\":\"feature\",\"contextKind\":\"anonymousUser\",\"userKey\":\"ABCD\",\"creationDate\":1617970547,\"key\":\"random-key\",\"variation\":\"Default\",\"value\":\"YO\",\"default\":false,\"version\":\"\",\"source\":\"SERVER\"}\n", wantErr: assert.NoError, }, } diff --git a/exporter/data_exporter_test.go b/exporter/data_exporter_test.go index a2be270d7ef..70288a3ee15 100644 --- a/exporter/data_exporter_test.go +++ b/exporter/data_exporter_test.go @@ -3,13 +3,14 @@ package exporter_test import ( "context" "errors" - "github.com/thomaspoignant/go-feature-flag/exporter" - "github.com/thomaspoignant/go-feature-flag/ffcontext" "log" "os" "testing" "time" + "github.com/thomaspoignant/go-feature-flag/exporter" + "github.com/thomaspoignant/go-feature-flag/ffcontext" + "github.com/stretchr/testify/assert" "github.com/thomaspoignant/go-feature-flag/testutils/mock" @@ -26,7 +27,7 @@ func TestDataExporterScheduler_flushWithTime(t *testing.T) { inputEvents := []exporter.FeatureEvent{ exporter.NewFeatureEvent( ffcontext.NewEvaluationContextBuilder("ABCD").AddCustom("anonymous", true).Build(), "random-key", - "YO", "defaultVar", false, ""), + "YO", "defaultVar", false, "", "SERVER"), } for _, event := range inputEvents { @@ -48,7 +49,7 @@ func TestDataExporterScheduler_flushWithNumberOfEvents(t *testing.T) { for i := 0; i <= 100; i++ { inputEvents = append(inputEvents, exporter.NewFeatureEvent( ffcontext.NewEvaluationContextBuilder("ABCD").AddCustom("anonymous", true).Build(), - "random-key", "YO", "defaultVar", false, "")) + "random-key", "YO", "defaultVar", false, "", "SERVER")) } for _, event := range inputEvents { dc.AddEvent(event) @@ -67,7 +68,7 @@ func TestDataExporterScheduler_defaultFlush(t *testing.T) { for i := 0; i <= 100000; i++ { inputEvents = append(inputEvents, exporter.NewFeatureEvent( ffcontext.NewEvaluationContextBuilder("ABCD").AddCustom("anonymous", true).Build(), - "random-key", "YO", "defaultVar", false, "")) + "random-key", "YO", "defaultVar", false, "", "SERVER")) } for _, event := range inputEvents { dc.AddEvent(event) @@ -92,7 +93,7 @@ func TestDataExporterScheduler_exporterReturnError(t *testing.T) { for i := 0; i <= 200; i++ { inputEvents = append(inputEvents, exporter.NewFeatureEvent( ffcontext.NewEvaluationContextBuilder("ABCD").AddCustom("anonymous", true).Build(), - "random-key", "YO", "defaultVar", false, "")) + "random-key", "YO", "defaultVar", false, "", "SERVER")) } for _, event := range inputEvents { dc.AddEvent(event) @@ -114,7 +115,7 @@ func TestDataExporterScheduler_nonBulkExporter(t *testing.T) { for i := 0; i < 100; i++ { inputEvents = append(inputEvents, exporter.NewFeatureEvent( ffcontext.NewEvaluationContextBuilder("ABCD").AddCustom("anonymous", true).Build(), - "random-key", "YO", "defaultVar", false, "")) + "random-key", "YO", "defaultVar", false, "", "SERVER")) } for _, event := range inputEvents { dc.AddEvent(event) diff --git a/exporter/feature_event.go b/exporter/feature_event.go index 2a073f15abf..08a68ed2524 100644 --- a/exporter/feature_event.go +++ b/exporter/feature_event.go @@ -2,8 +2,9 @@ package exporter import ( "encoding/json" - "github.com/thomaspoignant/go-feature-flag/ffcontext" "time" + + "github.com/thomaspoignant/go-feature-flag/ffcontext" ) func NewFeatureEvent( @@ -13,6 +14,7 @@ func NewFeatureEvent( variation string, failed bool, version string, + source string, ) FeatureEvent { contextKind := "user" if ctx.IsAnonymous() { @@ -29,6 +31,7 @@ func NewFeatureEvent( Value: value, Default: failed, Version: version, + Source: source, } } @@ -68,6 +71,10 @@ type FeatureEvent struct { // Version contains the version of the flag. If the field is omitted for the flag in the configuration file // the default version will be 0. Version string `json:"version" example:"v1.0.0" parquet:"name=version, type=BYTE_ARRAY, convertedtype=UTF8"` + + // Source indicates where the event was generated. + // This is set to SERVER when the event was evaluated in the relay-proxy and PROVIDER_CACHE when it is evaluated from the cache. + Source string `json:"source" example:"SERVER" parquet:"name=source, type=BYTE_ARRAY, convertedtype=UTF8"` } // MarshalInterface marshals all interface type fields in FeatureEvent into JSON-encoded string. diff --git a/exporter/feature_event_test.go b/exporter/feature_event_test.go index c10e50e762b..2732c08c8ea 100644 --- a/exporter/feature_event_test.go +++ b/exporter/feature_event_test.go @@ -1,10 +1,11 @@ package exporter_test import ( - "github.com/thomaspoignant/go-feature-flag/ffcontext" "testing" "time" + "github.com/thomaspoignant/go-feature-flag/ffcontext" + "github.com/stretchr/testify/assert" "github.com/thomaspoignant/go-feature-flag/exporter" ) @@ -17,6 +18,7 @@ func TestNewFeatureEvent(t *testing.T) { variation string failed bool version string + source string } tests := []struct { name string @@ -32,16 +34,17 @@ func TestNewFeatureEvent(t *testing.T) { variation: "Default", failed: false, version: "", + source: "SERVER", }, want: exporter.FeatureEvent{ Kind: "feature", ContextKind: "anonymousUser", UserKey: "ABCD", CreationDate: time.Now().Unix(), Key: "random-key", - Variation: "Default", Value: "YO", Default: false, + Variation: "Default", Value: "YO", Default: false, Source: "SERVER", }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assert.Equalf(t, tt.want, exporter.NewFeatureEvent(tt.args.user, tt.args.flagKey, tt.args.value, tt.args.variation, tt.args.failed, tt.args.version), "NewFeatureEvent(%v, %v, %v, %v, %v, %v)", tt.args.user, tt.args.flagKey, tt.args.value, tt.args.variation, tt.args.failed, tt.args.version) + assert.Equalf(t, tt.want, exporter.NewFeatureEvent(tt.args.user, tt.args.flagKey, tt.args.value, tt.args.variation, tt.args.failed, tt.args.version, tt.args.source), "NewFeatureEvent(%v, %v, %v, %v, %v, %v, %V)", tt.args.user, tt.args.flagKey, tt.args.value, tt.args.variation, tt.args.failed, tt.args.version, tt.args.source) }) } } diff --git a/exporter/fileexporter/exporter.go b/exporter/fileexporter/exporter.go index 973c44ad269..42feaf052d9 100644 --- a/exporter/fileexporter/exporter.go +++ b/exporter/fileexporter/exporter.go @@ -38,7 +38,8 @@ type Exporter struct { // You can decide which fields you want in your CSV line with a go-template syntax, // please check exporter/feature_event.go to see what are the fields available. // Default: - // {{ .Kind}};{{ .ContextKind}};{{ .UserKey}};{{ .CreationDate}};{{ .Key}};{{ .Variation}};{{ .Value}};{{ .Default}}\n + // {{ .Kind}};{{ .ContextKind}};{{ .UserKey}};{{ .CreationDate}};{{ .Key}};{{ .Variation}};{{ .Value}}; + // {{ .Default}};{{ .Source}}\n CsvTemplate string // ParquetCompressionCodec is the parquet compression codec for better space efficiency. diff --git a/exporter/fileexporter/exporter_test.go b/exporter/fileexporter/exporter_test.go index 4d9758dae96..f0cb2db346d 100644 --- a/exporter/fileexporter/exporter_test.go +++ b/exporter/fileexporter/exporter_test.go @@ -48,11 +48,11 @@ func TestFile_Export(t *testing.T) { featureEvents: []exporter.FeatureEvent{ { Kind: "feature", ContextKind: "anonymousUser", UserKey: "ABCD", CreationDate: 1617970547, Key: "random-key", - Variation: "Default", Value: "YO", Default: false, + Variation: "Default", Value: "YO", Default: false, Source: "SERVER", }, { Kind: "feature", ContextKind: "anonymousUser", UserKey: "EFGH", CreationDate: 1617970701, Key: "random-key", - Variation: "Default", Value: "YO2", Default: false, Version: "127", + Variation: "Default", Value: "YO2", Default: false, Version: "127", Source: "SERVER", }, }, }, @@ -71,11 +71,11 @@ func TestFile_Export(t *testing.T) { featureEvents: []exporter.FeatureEvent{ { Kind: "feature", ContextKind: "anonymousUser", UserKey: "ABCD", CreationDate: 1617970547, Key: "random-key", - Variation: "Default", Value: "YO", Default: false, + Variation: "Default", Value: "YO", Default: false, Source: "SERVER", }, { Kind: "feature", ContextKind: "anonymousUser", UserKey: "EFGH", CreationDate: 1617970701, Key: "random-key", - Variation: "Default", Value: "YO2", Default: false, + Variation: "Default", Value: "YO2", Default: false, Source: "SERVER", }, }, }, @@ -95,11 +95,11 @@ func TestFile_Export(t *testing.T) { featureEvents: []exporter.FeatureEvent{ { Kind: "feature", ContextKind: "anonymousUser", UserKey: "ABCD", CreationDate: 1617970547, Key: "random-key", - Variation: "Default", Value: "YO", Default: false, + Variation: "Default", Value: "YO", Default: false, Source: "SERVER", }, { Kind: "feature", ContextKind: "anonymousUser", UserKey: "EFGH", CreationDate: 1617970701, Key: "random-key", - Variation: "Default", Value: "YO2", Default: false, Version: "127", + Variation: "Default", Value: "YO2", Default: false, Version: "127", Source: "SERVER", }, }, }, @@ -108,11 +108,11 @@ func TestFile_Export(t *testing.T) { featureEvents: []exporter.FeatureEvent{ { Kind: "feature", ContextKind: "anonymousUser", UserKey: "ABCD", CreationDate: 1617970547, Key: "random-key", - Variation: "Default", Value: `"YO"`, Default: false, + Variation: "Default", Value: `"YO"`, Default: false, Source: "SERVER", }, { Kind: "feature", ContextKind: "anonymousUser", UserKey: "EFGH", CreationDate: 1617970701, Key: "random-key", - Variation: "Default", Value: `"YO2"`, Default: false, Version: "127", + Variation: "Default", Value: `"YO2"`, Default: false, Version: "127", Source: "SERVER", }, }, }, @@ -128,11 +128,11 @@ func TestFile_Export(t *testing.T) { featureEvents: []exporter.FeatureEvent{ { Kind: "feature", ContextKind: "anonymousUser", UserKey: "ABCD", CreationDate: 1617970547, Key: "random-key", - Variation: "Default", Value: "YO", Default: false, + Variation: "Default", Value: "YO", Default: false, Source: "SERVER", }, { Kind: "feature", ContextKind: "anonymousUser", UserKey: "EFGH", CreationDate: 1617970701, Key: "random-key", - Variation: "Default", Value: "YO2", Default: false, + Variation: "Default", Value: "YO2", Default: false, Source: "SERVER", }, }, }, @@ -164,6 +164,7 @@ func TestFile_Export(t *testing.T) { "int": 1, }, Default: false, + Source: "SERVER", }, }, }, @@ -179,6 +180,7 @@ func TestFile_Export(t *testing.T) { Variation: "Default", Value: `{"bool":true,"float":1.23,"int":1,"string":"string"}`, Default: false, + Source: "SERVER", }, }, }, @@ -194,11 +196,11 @@ func TestFile_Export(t *testing.T) { featureEvents: []exporter.FeatureEvent{ { Kind: "feature", ContextKind: "anonymousUser", UserKey: "ABCD", CreationDate: 1617970547, Key: "random-key", - Variation: "Default", Value: "YO", Default: false, + Variation: "Default", Value: "YO", Default: false, Source: "SERVER", }, { Kind: "feature", ContextKind: "anonymousUser", UserKey: "EFGH", CreationDate: 1617970701, Key: "random-key", - Variation: "Default", Value: "YO2", Default: false, + Variation: "Default", Value: "YO2", Default: false, Source: "SERVER", }, }, }, @@ -217,11 +219,11 @@ func TestFile_Export(t *testing.T) { featureEvents: []exporter.FeatureEvent{ { Kind: "feature", ContextKind: "anonymousUser", UserKey: "ABCD", CreationDate: 1617970547, Key: "random-key", - Variation: "Default", Value: "YO", Default: false, + Variation: "Default", Value: "YO", Default: false, Source: "SERVER", }, { Kind: "feature", ContextKind: "anonymousUser", UserKey: "EFGH", CreationDate: 1617970701, Key: "random-key", - Variation: "Default", Value: "YO2", Default: false, Version: "127", + Variation: "Default", Value: "YO2", Default: false, Version: "127", Source: "SERVER", }, }, }, @@ -240,11 +242,11 @@ func TestFile_Export(t *testing.T) { featureEvents: []exporter.FeatureEvent{ { Kind: "feature", ContextKind: "anonymousUser", UserKey: "ABCD", CreationDate: 1617970547, Key: "random-key", - Variation: "Default", Value: "YO", Default: false, + Variation: "Default", Value: "YO", Default: false, Source: "SERVER", }, { Kind: "feature", ContextKind: "anonymousUser", UserKey: "EFGH", CreationDate: 1617970701, Key: "random-key", - Variation: "Default", Value: "YO2", Default: false, + Variation: "Default", Value: "YO2", Default: false, Source: "SERVER", }, }, }, @@ -259,11 +261,11 @@ func TestFile_Export(t *testing.T) { featureEvents: []exporter.FeatureEvent{ { Kind: "feature", ContextKind: "anonymousUser", UserKey: "ABCD", CreationDate: 1617970547, Key: "random-key", - Variation: "Default", Value: "YO", Default: false, + Variation: "Default", Value: "YO", Default: false, Source: "SERVER", }, { Kind: "feature", ContextKind: "anonymousUser", UserKey: "EFGH", CreationDate: 1617970701, Key: "random-key", - Variation: "Default", Value: "YO2", Default: false, + Variation: "Default", Value: "YO2", Default: false, Source: "SERVER", }, }, }, @@ -279,11 +281,11 @@ func TestFile_Export(t *testing.T) { featureEvents: []exporter.FeatureEvent{ { Kind: "feature", ContextKind: "anonymousUser", UserKey: "ABCD", CreationDate: 1617970547, Key: "random-key", - Variation: "Default", Value: "YO", Default: false, + Variation: "Default", Value: "YO", Default: false, Source: "SERVER", }, { Kind: "feature", ContextKind: "anonymousUser", UserKey: "EFGH", CreationDate: 1617970701, Key: "random-key", - Variation: "Default", Value: "YO2", Default: false, + Variation: "Default", Value: "YO2", Default: false, Source: "SERVER", }, }, }, diff --git a/exporter/fileexporter/testdata/all_default.csv b/exporter/fileexporter/testdata/all_default.csv index 9541fdf0895..501ba4f8bad 100644 --- a/exporter/fileexporter/testdata/all_default.csv +++ b/exporter/fileexporter/testdata/all_default.csv @@ -1,2 +1,2 @@ -feature;anonymousUser;ABCD;1617970547;random-key;Default;YO;false -feature;anonymousUser;EFGH;1617970701;random-key;Default;YO2;false +feature;anonymousUser;ABCD;1617970547;random-key;Default;YO;false;SERVER +feature;anonymousUser;EFGH;1617970701;random-key;Default;YO2;false;SERVER diff --git a/exporter/fileexporter/testdata/all_default.json b/exporter/fileexporter/testdata/all_default.json index 99ab92ad5bd..aa837a30f1f 100644 --- a/exporter/fileexporter/testdata/all_default.json +++ b/exporter/fileexporter/testdata/all_default.json @@ -1,2 +1,2 @@ -{"kind":"feature","contextKind":"anonymousUser","userKey":"ABCD","creationDate":1617970547,"key":"random-key","variation":"Default","value":"YO","default":false,"version":""} -{"kind":"feature","contextKind":"anonymousUser","userKey":"EFGH","creationDate":1617970701,"key":"random-key","variation":"Default","value":"YO2","default":false,"version":"127"} +{"kind":"feature","contextKind":"anonymousUser","userKey":"ABCD","creationDate":1617970547,"key":"random-key","variation":"Default","value":"YO","default":false,"version":"","source":"SERVER"} +{"kind":"feature","contextKind":"anonymousUser","userKey":"EFGH","creationDate":1617970701,"key":"random-key","variation":"Default","value":"YO2","default":false,"version":"127","source":"SERVER"} diff --git a/exporter/fileexporter/testdata/custom_file_name.json b/exporter/fileexporter/testdata/custom_file_name.json index 891251dd748..fe6e945e07d 100644 --- a/exporter/fileexporter/testdata/custom_file_name.json +++ b/exporter/fileexporter/testdata/custom_file_name.json @@ -1,2 +1,2 @@ -{"kind":"feature","contextKind":"anonymousUser","userKey":"ABCD","creationDate":1617970547,"key":"random-key","variation":"Default","value":"YO","default":false,"version":""} -{"kind":"feature","contextKind":"anonymousUser","userKey":"EFGH","creationDate":1617970701,"key":"random-key","variation":"Default","value":"YO2","default":false,"version":""} +{"kind":"feature","contextKind":"anonymousUser","userKey":"ABCD","creationDate":1617970547,"key":"random-key","variation":"Default","value":"YO","default":false,"version":"","source":"SERVER"} +{"kind":"feature","contextKind":"anonymousUser","userKey":"EFGH","creationDate":1617970701,"key":"random-key","variation":"Default","value":"YO2","default":false,"version":"","source":"SERVER"} diff --git a/exporter/logsexporter/exporter.go b/exporter/logsexporter/exporter.go index fb0d8cc29ee..d4834e6bfe3 100644 --- a/exporter/logsexporter/exporter.go +++ b/exporter/logsexporter/exporter.go @@ -33,7 +33,7 @@ type Exporter struct { // Export is saving a collection of events in a file. func (f *Exporter) Export(_ context.Context, logger *log.Logger, featureEvents []exporter.FeatureEvent) error { f.initTemplates.Do(func() { - // Remove bellow after deprecation of Format + // Remove below after deprecation of Format if f.LogFormat == "" && f.Format != "" { f.LogFormat = f.Format } diff --git a/exporter/s3exporter/exporter_test.go b/exporter/s3exporter/exporter_test.go index f3985c270e7..5f887bfeaa6 100644 --- a/exporter/s3exporter/exporter_test.go +++ b/exporter/s3exporter/exporter_test.go @@ -41,7 +41,7 @@ func TestS3_Export(t *testing.T) { events: []exporter.FeatureEvent{ { Kind: "feature", ContextKind: "anonymousUser", UserKey: "ABCD", CreationDate: 1617970547, Key: "random-key", - Variation: "Default", Value: "YO", Default: false, + Variation: "Default", Value: "YO", Default: false, Source: "SERVER", }, }, expectedFile: "./testdata/all_default.json", @@ -56,7 +56,7 @@ func TestS3_Export(t *testing.T) { events: []exporter.FeatureEvent{ { Kind: "feature", ContextKind: "anonymousUser", UserKey: "ABCD", CreationDate: 1617970547, Key: "random-key", - Variation: "Default", Value: "YO", Default: false, + Variation: "Default", Value: "YO", Default: false, Source: "SERVER", }, }, expectedFile: "./testdata/all_default.json", @@ -71,7 +71,7 @@ func TestS3_Export(t *testing.T) { events: []exporter.FeatureEvent{ { Kind: "feature", ContextKind: "anonymousUser", UserKey: "ABCD", CreationDate: 1617970547, Key: "random-key", - Variation: "Default", Value: "YO", Default: false, + Variation: "Default", Value: "YO", Default: false, Source: "SERVER", }, }, expectedFile: "./testdata/all_default.csv", @@ -87,7 +87,7 @@ func TestS3_Export(t *testing.T) { events: []exporter.FeatureEvent{ { Kind: "feature", ContextKind: "anonymousUser", UserKey: "ABCD", CreationDate: 1617970547, Key: "random-key", - Variation: "Default", Value: "YO", Default: false, + Variation: "Default", Value: "YO", Default: false, Source: "SERVER", }, }, expectedFile: "./testdata/custom_csv_format.csv", @@ -103,7 +103,7 @@ func TestS3_Export(t *testing.T) { events: []exporter.FeatureEvent{ { Kind: "feature", ContextKind: "anonymousUser", UserKey: "ABCD", CreationDate: 1617970547, Key: "random-key", - Variation: "Default", Value: "YO", Default: false, + Variation: "Default", Value: "YO", Default: false, Source: "SERVER", }, }, expectedFile: "./testdata/all_default.json", @@ -118,7 +118,7 @@ func TestS3_Export(t *testing.T) { events: []exporter.FeatureEvent{ { Kind: "feature", ContextKind: "anonymousUser", UserKey: "ABCD", CreationDate: 1617970547, Key: "random-key", - Variation: "Default", Value: "YO", Default: false, + Variation: "Default", Value: "YO", Default: false, Source: "SERVER", }, }, expectedFile: "./testdata/all_default.json", @@ -132,7 +132,7 @@ func TestS3_Export(t *testing.T) { events: []exporter.FeatureEvent{ { Kind: "feature", ContextKind: "anonymousUser", UserKey: "ABCD", CreationDate: 1617970547, Key: "random-key", - Variation: "Default", Value: "YO", Default: false, + Variation: "Default", Value: "YO", Default: false, Source: "SERVER", }, }, wantErr: true, @@ -146,7 +146,7 @@ func TestS3_Export(t *testing.T) { events: []exporter.FeatureEvent{ { Kind: "feature", ContextKind: "anonymousUser", UserKey: "ABCD", CreationDate: 1617970547, Key: "random-key", - Variation: "Default", Value: "YO", Default: false, + Variation: "Default", Value: "YO", Default: false, Source: "SERVER", }, }, wantErr: true, @@ -160,7 +160,7 @@ func TestS3_Export(t *testing.T) { events: []exporter.FeatureEvent{ { Kind: "feature", ContextKind: "anonymousUser", UserKey: "ABCD", CreationDate: 1617970547, Key: "random-key", - Variation: "Default", Value: "YO", Default: false, + Variation: "Default", Value: "YO", Default: false, Source: "SERVER", }, }, wantErr: true, diff --git a/exporter/s3exporter/testdata/all_default.csv b/exporter/s3exporter/testdata/all_default.csv index db122b68d71..60f91161919 100644 --- a/exporter/s3exporter/testdata/all_default.csv +++ b/exporter/s3exporter/testdata/all_default.csv @@ -1 +1 @@ -feature;anonymousUser;ABCD;1617970547;random-key;Default;YO;false +feature;anonymousUser;ABCD;1617970547;random-key;Default;YO;false;SERVER diff --git a/exporter/s3exporter/testdata/all_default.json b/exporter/s3exporter/testdata/all_default.json index 9e702153e2e..851a24887d6 100644 --- a/exporter/s3exporter/testdata/all_default.json +++ b/exporter/s3exporter/testdata/all_default.json @@ -1 +1 @@ -{"kind":"feature","contextKind":"anonymousUser","userKey":"ABCD","creationDate":1617970547,"key":"random-key","variation":"Default","value":"YO","default":false,"version":""} +{"kind":"feature","contextKind":"anonymousUser","userKey":"ABCD","creationDate":1617970547,"key":"random-key","variation":"Default","value":"YO","default":false,"version":"","source":"SERVER"} diff --git a/exporter/s3exporterv2/exporter_test.go b/exporter/s3exporterv2/exporter_test.go index 608d968d92f..5bcb140e60f 100644 --- a/exporter/s3exporterv2/exporter_test.go +++ b/exporter/s3exporterv2/exporter_test.go @@ -2,11 +2,12 @@ package s3exporterv2 import ( "context" - "github.com/aws/aws-sdk-go-v2/aws" "log" "os" "testing" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/thomaspoignant/go-feature-flag/exporter" "github.com/stretchr/testify/assert" @@ -41,7 +42,7 @@ func TestS3_Export(t *testing.T) { events: []exporter.FeatureEvent{ { Kind: "feature", ContextKind: "anonymousUser", UserKey: "ABCD", CreationDate: 1617970547, Key: "random-key", - Variation: "Default", Value: "YO", Default: false, + Variation: "Default", Value: "YO", Default: false, Source: "SERVER", }, }, expectedFile: "./testdata/all_default.json", @@ -56,7 +57,7 @@ func TestS3_Export(t *testing.T) { events: []exporter.FeatureEvent{ { Kind: "feature", ContextKind: "anonymousUser", UserKey: "ABCD", CreationDate: 1617970547, Key: "random-key", - Variation: "Default", Value: "YO", Default: false, + Variation: "Default", Value: "YO", Default: false, Source: "SERVER", }, }, expectedFile: "./testdata/all_default.json", @@ -71,7 +72,7 @@ func TestS3_Export(t *testing.T) { events: []exporter.FeatureEvent{ { Kind: "feature", ContextKind: "anonymousUser", UserKey: "ABCD", CreationDate: 1617970547, Key: "random-key", - Variation: "Default", Value: "YO", Default: false, + Variation: "Default", Value: "YO", Default: false, Source: "SERVER", }, }, expectedFile: "./testdata/all_default.csv", @@ -87,7 +88,7 @@ func TestS3_Export(t *testing.T) { events: []exporter.FeatureEvent{ { Kind: "feature", ContextKind: "anonymousUser", UserKey: "ABCD", CreationDate: 1617970547, Key: "random-key", - Variation: "Default", Value: "YO", Default: false, + Variation: "Default", Value: "YO", Default: false, Source: "SERVER", }, }, expectedFile: "./testdata/custom_csv_format.csv", @@ -103,7 +104,7 @@ func TestS3_Export(t *testing.T) { events: []exporter.FeatureEvent{ { Kind: "feature", ContextKind: "anonymousUser", UserKey: "ABCD", CreationDate: 1617970547, Key: "random-key", - Variation: "Default", Value: "YO", Default: false, + Variation: "Default", Value: "YO", Default: false, Source: "SERVER", }, }, expectedFile: "./testdata/all_default.json", @@ -118,7 +119,7 @@ func TestS3_Export(t *testing.T) { events: []exporter.FeatureEvent{ { Kind: "feature", ContextKind: "anonymousUser", UserKey: "ABCD", CreationDate: 1617970547, Key: "random-key", - Variation: "Default", Value: "YO", Default: false, + Variation: "Default", Value: "YO", Default: false, Source: "SERVER", }, }, expectedFile: "./testdata/all_default.json", @@ -132,7 +133,7 @@ func TestS3_Export(t *testing.T) { events: []exporter.FeatureEvent{ { Kind: "feature", ContextKind: "anonymousUser", UserKey: "ABCD", CreationDate: 1617970547, Key: "random-key", - Variation: "Default", Value: "YO", Default: false, + Variation: "Default", Value: "YO", Default: false, Source: "SERVER", }, }, wantErr: true, @@ -146,7 +147,7 @@ func TestS3_Export(t *testing.T) { events: []exporter.FeatureEvent{ { Kind: "feature", ContextKind: "anonymousUser", UserKey: "ABCD", CreationDate: 1617970547, Key: "random-key", - Variation: "Default", Value: "YO", Default: false, + Variation: "Default", Value: "YO", Default: false, Source: "SERVER", }, }, wantErr: true, @@ -160,7 +161,7 @@ func TestS3_Export(t *testing.T) { events: []exporter.FeatureEvent{ { Kind: "feature", ContextKind: "anonymousUser", UserKey: "ABCD", CreationDate: 1617970547, Key: "random-key", - Variation: "Default", Value: "YO", Default: false, + Variation: "Default", Value: "YO", Default: false, Source: "SERVER", }, }, wantErr: true, diff --git a/exporter/s3exporterv2/testdata/all_default.csv b/exporter/s3exporterv2/testdata/all_default.csv index db122b68d71..60f91161919 100644 --- a/exporter/s3exporterv2/testdata/all_default.csv +++ b/exporter/s3exporterv2/testdata/all_default.csv @@ -1 +1 @@ -feature;anonymousUser;ABCD;1617970547;random-key;Default;YO;false +feature;anonymousUser;ABCD;1617970547;random-key;Default;YO;false;SERVER diff --git a/exporter/s3exporterv2/testdata/all_default.json b/exporter/s3exporterv2/testdata/all_default.json index 9e702153e2e..851a24887d6 100644 --- a/exporter/s3exporterv2/testdata/all_default.json +++ b/exporter/s3exporterv2/testdata/all_default.json @@ -1 +1 @@ -{"kind":"feature","contextKind":"anonymousUser","userKey":"ABCD","creationDate":1617970547,"key":"random-key","variation":"Default","value":"YO","default":false,"version":""} +{"kind":"feature","contextKind":"anonymousUser","userKey":"ABCD","creationDate":1617970547,"key":"random-key","variation":"Default","value":"YO","default":false,"version":"","source":"SERVER"} diff --git a/exporter/webhookexporter/exporter_test.go b/exporter/webhookexporter/exporter_test.go index c85d7cd46ac..330e98a6362 100644 --- a/exporter/webhookexporter/exporter_test.go +++ b/exporter/webhookexporter/exporter_test.go @@ -62,11 +62,11 @@ func TestWebhook_Export(t *testing.T) { featureEvents: []exporter.FeatureEvent{ { Kind: "feature", ContextKind: "anonymousUser", UserKey: "ABCD", CreationDate: 1617970547, Key: "random-key", - Variation: "Default", Value: "YO", Default: false, + Variation: "Default", Value: "YO", Default: false, Source: "SERVER", }, { Kind: "feature", ContextKind: "anonymousUser", UserKey: "EFGH", CreationDate: 1617970701, Key: "random-key", - Variation: "Default", Value: "YO2", Default: false, Version: "127", + Variation: "Default", Value: "YO2", Default: false, Version: "127", Source: "SERVER", }, }, }, @@ -89,17 +89,17 @@ func TestWebhook_Export(t *testing.T) { featureEvents: []exporter.FeatureEvent{ { Kind: "feature", ContextKind: "anonymousUser", UserKey: "ABCD", CreationDate: 1617970547, Key: "random-key", - Variation: "Default", Value: "YO", Default: false, + Variation: "Default", Value: "YO", Default: false, Source: "SERVER", }, { Kind: "feature", ContextKind: "anonymousUser", UserKey: "EFGH", CreationDate: 1617970701, Key: "random-key", - Variation: "Default", Value: "YO2", Default: false, Version: "127", + Variation: "Default", Value: "YO2", Default: false, Version: "127", Source: "SERVER", }, }, }, expected: expected{ bodyFilePath: "./testdata/valid_with_signature.json", - signHeader: "sha256=0c504fe37d423ff0a80e4dc29b93c18c2d1438a5387f36d8e6491e77fb5e70d4", + signHeader: "sha256=f1f9766836d4e513035986c035ee9f091b895709be47a802b5840467007e1ec0", }, wantErr: false, }, @@ -116,11 +116,11 @@ func TestWebhook_Export(t *testing.T) { featureEvents: []exporter.FeatureEvent{ { Kind: "feature", ContextKind: "anonymousUser", UserKey: "ABCD", CreationDate: 1617970547, Key: "random-key", - Variation: "Default", Value: "YO", Default: false, + Variation: "Default", Value: "YO", Default: false, Source: "SERVER", }, { Kind: "feature", ContextKind: "anonymousUser", UserKey: "EFGH", CreationDate: 1617970701, Key: "random-key", - Variation: "Default", Value: "YO2", Default: false, + Variation: "Default", Value: "YO2", Default: false, Source: "SERVER", }, }, }, @@ -139,11 +139,11 @@ func TestWebhook_Export(t *testing.T) { featureEvents: []exporter.FeatureEvent{ { Kind: "feature", ContextKind: "anonymousUser", UserKey: "ABCD", CreationDate: 1617970547, Key: "random-key", - Variation: "Default", Value: "YO", Default: false, + Variation: "Default", Value: "YO", Default: false, Source: "SERVER", }, { Kind: "feature", ContextKind: "anonymousUser", UserKey: "EFGH", CreationDate: 1617970701, Key: "random-key", - Variation: "Default", Value: "YO2", Default: false, + Variation: "Default", Value: "YO2", Default: false, Source: "SERVER", }, }, }, @@ -162,11 +162,11 @@ func TestWebhook_Export(t *testing.T) { featureEvents: []exporter.FeatureEvent{ { Kind: "feature", ContextKind: "anonymousUser", UserKey: "ABCD", CreationDate: 1617970547, Key: "random-key", - Variation: "Default", Value: "YO", Default: false, + Variation: "Default", Value: "YO", Default: false, Source: "SERVER", }, { Kind: "feature", ContextKind: "anonymousUser", UserKey: "EFGH", CreationDate: 1617970701, Key: "random-key", - Variation: "Default", Value: "YO2", Default: false, Version: "127", + Variation: "Default", Value: "YO2", Default: false, Version: "127", Source: "SERVER", }, }, }, diff --git a/exporter/webhookexporter/testdata/valid_with_signature.json b/exporter/webhookexporter/testdata/valid_with_signature.json index 6467446841e..cf706280a9b 100644 --- a/exporter/webhookexporter/testdata/valid_with_signature.json +++ b/exporter/webhookexporter/testdata/valid_with_signature.json @@ -12,7 +12,8 @@ "variation": "Default", "value": "YO", "default": false, - "version": "" + "version": "", + "source": "SERVER" }, { "kind": "feature", @@ -23,7 +24,8 @@ "variation": "Default", "value": "YO2", "default": false, - "version": "127" + "version": "127", + "source": "SERVER" } ] } diff --git a/exporter/webhookexporter/testdata/valid_without_signature.json b/exporter/webhookexporter/testdata/valid_without_signature.json index 6467446841e..cf706280a9b 100644 --- a/exporter/webhookexporter/testdata/valid_without_signature.json +++ b/exporter/webhookexporter/testdata/valid_without_signature.json @@ -12,7 +12,8 @@ "variation": "Default", "value": "YO", "default": false, - "version": "" + "version": "", + "source": "SERVER" }, { "kind": "feature", @@ -23,7 +24,8 @@ "variation": "Default", "value": "YO2", "default": false, - "version": "127" + "version": "127", + "source": "SERVER" } ] } diff --git a/feature_flag.go b/feature_flag.go index 61eb86a37b0..fc215243709 100644 --- a/feature_flag.go +++ b/feature_flag.go @@ -3,13 +3,14 @@ package ffclient import ( "context" "fmt" + "log" + "sync" + "time" + "github.com/thomaspoignant/go-feature-flag/exporter" "github.com/thomaspoignant/go-feature-flag/internal/dto" "github.com/thomaspoignant/go-feature-flag/retriever" "github.com/thomaspoignant/go-feature-flag/utils/fflog" - "log" - "sync" - "time" "github.com/thomaspoignant/go-feature-flag/notifier/logsnotifier" @@ -41,7 +42,7 @@ func Close() { } // GoFeatureFlag is the main object of the library -// it contains the cache, the config and the update. +// it contains the cache, the config, the updater and the exporter. type GoFeatureFlag struct { cache cache.Manager config Config diff --git a/variation.go b/variation.go index 327a97ad44c..22805c7060b 100644 --- a/variation.go +++ b/variation.go @@ -2,9 +2,10 @@ package ffclient import ( "fmt" + "time" + "github.com/thomaspoignant/go-feature-flag/exporter" "github.com/thomaspoignant/go-feature-flag/ffcontext" - "time" "github.com/thomaspoignant/go-feature-flag/internal/flag" "github.com/thomaspoignant/go-feature-flag/internal/flagstate" @@ -360,7 +361,8 @@ func notifyVariation[T model.JSONType]( result model.VariationResult[T], ) { if result.TrackEvents { - event := exporter.NewFeatureEvent(ctx, flagKey, result.Value, result.VariationType, result.Failed, result.Version) + event := exporter.NewFeatureEvent(ctx, flagKey, result.Value, result.VariationType, result.Failed, result.Version, + "SERVER") g.CollectEventData(event) } } diff --git a/website/README.md b/website/README.md index 6e08eb1b292..74491b97b32 100644 --- a/website/README.md +++ b/website/README.md @@ -10,16 +10,16 @@ You will need to have **nodejs** installed in your machine to work with the docu Your can start locally the website. 1. Open a terminal and go to the root project of this repository. -2. Launch the command bellow, it will install the dependencies and run the local server for the documentation. +2. Launch the command below, it will install the dependencies and run the local server for the documentation. ```shell make watch-doc ``` -3. You can now access to the documentation directly in your browser: [http://localhost:3000/](http://localhost:3000/). +3. You can now access the documentation directly in your browser: [http://localhost:3000/](http://localhost:3000/). ## Build the documentation 1. Open a terminal and go to the root project of this repository. -2. Launch the command bellow, it will install the dependencies and build the documentation. +2. Launch the command below, it will install the dependencies and build the documentation. ```shell make build-doc ``` diff --git a/website/docs/go_module/data_collection/file.md b/website/docs/go_module/data_collection/file.md index 9bcf78aeb1a..91216c49947 100644 --- a/website/docs/go_module/data_collection/file.md +++ b/website/docs/go_module/data_collection/file.md @@ -18,7 +18,7 @@ ffclient.Config{ OutputDir: "/output-data/", Format: "csv", FileName: "flag-variation-{{ .Hostname}}-{{ .Timestamp}}.{{ .Format}}", - CsvTemplate: "{{ .Kind}};{{ .ContextKind}};{{ .UserKey}};{{ .CreationDate}};{{ .Key}};{{ .Variation}};{{ .Value}};{{ .Default}}\n" + CsvTemplate: "{{ .Kind}};{{ .ContextKind}};{{ .UserKey}};{{ .CreationDate}};{{ .Key}};{{ .Variation}};{{ .Value}};{{ .Default}};{{ .Source}}\n" }, }, // ... diff --git a/website/docs/go_module/data_collection/google_cloud_storage.md b/website/docs/go_module/data_collection/google_cloud_storage.md index 0e1821453c8..14bacd54f85 100644 --- a/website/docs/go_module/data_collection/google_cloud_storage.md +++ b/website/docs/go_module/data_collection/google_cloud_storage.md @@ -36,7 +36,7 @@ ffclient.Config{ | Field | Description | |---------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `Bucket ` | Name of your Google Cloud Storage Bucket. | -| `CsvTemplate` | *(optional)* CsvTemplate is used if your output format is CSV. This field will be ignored if you are using another format than CSV. You can decide which fields you want in your CSV line with a go-template syntax, please check [internal/exporter/feature_event.go](https://github.com/thomaspoignant/go-feature-flag/blob/main/internal/exporter/feature_event.go) to see what are the fields available.
**Default:** `{{ .Kind}};{{ .ContextKind}};{{ .UserKey}};{{ .CreationDate}};{{ .Key}};{{ .Variation}};{{ .Value}};{{ .Default}}\n` | +| `CsvTemplate` | *(optional)* CsvTemplate is used if your output format is CSV. This field will be ignored if you are using another format than CSV. You can decide which fields you want in your CSV line with a go-template syntax, please check [internal/exporter/feature_event.go](https://github.com/thomaspoignant/go-feature-flag/blob/main/internal/exporter/feature_event.go) to see what are the fields available.
**Default:** `{{ .Kind}};{{ .ContextKind}};{{ .UserKey}};{{ .CreationDate}};{{ .Key}};{{ .Variation}};{{ .Value}};{{ .Default}};{{ .Source}}\n` | | `Filename` | *(optional)* Filename is the name of your output file. You can use a templated config to define the name of your exported files.
Available replacement are `{{ .Hostname}}`, `{{ .Timestamp}`} and `{{ .Format}}`
Default: `flag-variation-{{ .Hostname}}-{{ .Timestamp}}.{{ .Format}}` | | `Format` | *(optional)* Format is the output format you want in your exported file. Available format are **`JSON`**, **`CSV`**, **`Parquet`**. *(Default: `JSON`)* | | `Options` | *(optional)* An instance of `option.ClientOption` that configures your access to Google Cloud.
Check [this documentation for more info](https://cloud.google.com/docs/authentication). | diff --git a/website/docs/go_module/data_collection/index.md b/website/docs/go_module/data_collection/index.md index f2df3328876..e137f5fed79 100644 --- a/website/docs/go_module/data_collection/index.md +++ b/website/docs/go_module/data_collection/index.md @@ -31,7 +31,8 @@ It represents individual flag evaluations and are considered "full fidelity" eve "key": "test-flag", "variation": "Default", "value": false, - "default": false + "default": false, + "source": "SERVER" } ``` @@ -44,8 +45,9 @@ It represents individual flag evaluations and are considered "full fidelity" eve | **`userKey`** | The key of the user object used in a feature flag evaluation. | | **`creationDate`** | When the feature flag was requested at Unix epoch time in milliseconds. | | **`key`** | The key of the feature flag requested. | -| **`variation`** | The variation of the flag requested. Available values are:
**True**: if the flag was evaluated to True
**False**: if the flag was evaluated to False
**Dafault**: if the flag was evaluated to Default
**SdkDefault**: if something wrong happened and the SDK default value was used. | +| **`variation`** | The variation of the flag requested. Available values are:
**True**: if the flag was evaluated to True
**False**: if the flag was evaluated to False
**Default**: if the flag was evaluated to Default
**SdkDefault**: if something wrong happened and the SDK default value was used. | | **`value`** | The value of the feature flag returned by feature flag evaluation. | +| **`source`** | Where the event was generated. This is set to SERVER when the event was evaluated in the relay-proxy and PROVIDER_CACHE when it is evaluated from the cache. | **`default`** | (Optional) This value is set to true if feature flag evaluation failed, in which case the value returned was the default value passed to variation. | Events are collected and send in bulk to avoid spamming your exporter *(see details in [how to configure data export](#how-to-configure-data-export)*) diff --git a/website/docs/go_module/data_collection/s3.md b/website/docs/go_module/data_collection/s3.md index 0898a87ef7d..fddd3a6f102 100644 --- a/website/docs/go_module/data_collection/s3.md +++ b/website/docs/go_module/data_collection/s3.md @@ -27,7 +27,7 @@ ffclient.Config{ Exporter: &s3exporterv2.Exporter{ Format: "csv", FileName: "flag-variation-{{ .Hostname}}-{{ .Timestamp}}.{{ .Format}}", - CsvTemplate: "{{ .Kind}};{{ .ContextKind}};{{ .UserKey}};{{ .CreationDate}};{{ .Key}};{{ .Variation}};{{ .Value}};{{ .Default}}\n", + CsvTemplate: "{{ .Kind}};{{ .ContextKind}};{{ .UserKey}};{{ .CreationDate}};{{ .Key}};{{ .Variation}};{{ .Value}};{{ .Default}};{{ .Source}}\n", Bucket: "my-bucket", S3Path: "/go-feature-flag/variations/", Filename: "flag-variation-{{ .Timestamp}}.{{ .Format}}", @@ -43,8 +43,8 @@ ffclient.Config{ |---------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `Bucket ` | Name of your S3 Bucket. | | `AwsConfig ` | An instance of `aws.Config` that configure your access to AWS *(see [this documentation for more info](https://aws.github.io/aws-sdk-go-v2/docs/configuring-sdk/))*. | -| `CsvTemplate` | *(optional)* CsvTemplate is used if your output format is CSV. This field will be ignored if you are using another format than CSV. You can decide which fields you want in your CSV line with a go-template syntax, please check [internal/exporter/feature_event.go](https://github.com/thomaspoignant/go-feature-flag/blob/main/internal/exporter/feature_event.go) to see what are the fields available.
**Default:** `{{ .Kind}};{{ .ContextKind}};{{ .UserKey}};{{ .CreationDate}};{{ .Key}};{{ .Variation}};{{ .Value}};{{ .Default}}\n` | -| `Filename` | *(optional)* Filename is the name of your output file. You can use a templated config to define the name of your exported files.
Available replacement are `{{ .Hostname}}`, `{{ .Timestamp}`} and `{{ .Format}}`
Default: `flag-variation-{{ .Hostname}}-{{ .Timestamp}}.{{ .Format}}` | +| `CsvTemplate` | *(optional)* CsvTemplate is used if your output format is CSV. This field will be ignored if you are using another format than CSV. You can decide which fields you want in your CSV line with a go-template syntax, please check [internal/exporter/feature_event.go](https://github.com/thomaspoignant/go-feature-flag/blob/main/internal/exporter/feature_event.go) to see what are the fields available.
**Default:** `{{ .Kind}};{{ .ContextKind}};{{ .UserKey}};{{ .CreationDate}};{{ .Key}};{{ .Variation}};{{ .Value}};{{ .Default}};{{ .Source}}\n` | +| `Filename` | *(optional)* Filename is the name of your output file. You can use a templated config to define the name of your exported files.
Available replacement are `{{ .Hostname}}`, `{{ .Timestamp}}` and `{{ .Format}}`
Default: `flag-variation-{{ .Hostname}}-{{ .Timestamp}}.{{ .Format}}` | | `Format` | *(optional)* Format is the output format you want in your exported file. Available format are **`JSON`**, **`CSV`**, **`Parquet`**. *(Default: `JSON`)* | | `S3Path ` | *(optional)* The location of the directory in S3. | | `ParquetCompressionCodec` | *(optional)* ParquetCompressionCodec is the parquet compression codec for better space efficiency. [Available options](https://github.com/apache/parquet-format/blob/master/Compression.md) *(Default: `SNAPPY`)* |` diff --git a/website/docs/go_module/data_collection/webhook.md b/website/docs/go_module/data_collection/webhook.md index 4578f85712d..b6491c0b135 100644 --- a/website/docs/go_module/data_collection/webhook.md +++ b/website/docs/go_module/data_collection/webhook.md @@ -57,7 +57,8 @@ If you have configured a webhook, a `POST` request will be sent to the `Endpoint "key": "test-flag", "variation": "Default", "value": false, - "default": false + "default": false, + "source": "SERVER" }, // ... ] diff --git a/website/docs/relay_proxy/configure_relay_proxy.md b/website/docs/relay_proxy/configure_relay_proxy.md index fa7448b3b91..f95afa428a4 100644 --- a/website/docs/relay_proxy/configure_relay_proxy.md +++ b/website/docs/relay_proxy/configure_relay_proxy.md @@ -153,7 +153,7 @@ _Note that relay proxy is only supporting this while running inside the kubernet | `maxEventInMemory` | int | `100000` | If we hit that limit we will call the webhook. | | `format` | string | `JSON` | Format is the output format you want in your exported file. Available format: `JSON`, `CSV`, `Parquet`. | | `filename` | string | `flag-variation-{{ .Hostname}}-{{ .Timestamp}}.{{ .Format}}` | You can use a templated config to define the name of your exported files. Available replacement are `{{ .Hostname}}`, `{{ .Timestamp}}` and `{{ .Format}` | -| `csvTemplate` | string | `{{ .Kind}};{{ .ContextKind}};{{ .UserKey}};{{ .CreationDate}};{{ .Key}};{{ .Variation}};{{ .Value}};{{ .Default}}\n` | CsvTemplate is used if your output format is CSV.
This field will be ignored if you are using another format than CSV.
You can decide which fields you want in your CSV line with a go-template syntax, please check [`internal/exporter/feature_event.go`](https://github.com/thomaspoignant/go-feature-flag/blob/main/internal/exporter/feature_event.go) to see what are the fields available. |` +| `csvTemplate` | string | `{{ .Kind}};{{ .ContextKind}};{{ .UserKey}};{{ .CreationDate}};{{ .Key}};{{ .Variation}};{{ .Value}};{{ .Default}};{{ .Source}}\n` | CsvTemplate is used if your output format is CSV.
This field will be ignored if you are using another format than CSV.
You can decide which fields you want in your CSV line with a go-template syntax, please check [`internal/exporter/feature_event.go`](https://github.com/thomaspoignant/go-feature-flag/blob/main/internal/exporter/feature_event.go) to see what are the fields available. |` | `parquetCompressionCodec` | string | `SNAPPY` | ParquetCompressionCodec is the parquet compression codec for better space efficiency. [Available options](https://github.com/apache/parquet-format/blob/master/Compression.md) |` @@ -176,7 +176,7 @@ _Note that relay proxy is only supporting this while running inside the kubernet | `maxEventInMemory` | int | `100000` | If we hit that limit we will call the webhook. | | `format` | string | `JSON` | Format is the output format you want in your exported file. Available format: `JSON`, `CSV`, `Parquet`. | | `filename` | string | `flag-variation-{{ .Hostname}}-{{ .Timestamp}}.{{ .Format}}` | You can use a templated config to define the name of your exported files. Available replacement are `{{ .Hostname}}`, `{{ .Timestamp}}` and `{{ .Format}` | -| `csvTemplate` | string | `{{ .Kind}};{{ .ContextKind}};{{ .UserKey}};{{ .CreationDate}};{{ .Key}};{{ .Variation}};{{ .Value}};{{ .Default}}\n` | CsvTemplate is used if your output format is CSV.
This field will be ignored if you are using another format than CSV.
You can decide which fields you want in your CSV line with a go-template syntax, please check [`internal/exporter/feature_event.go`](https://github.com/thomaspoignant/go-feature-flag/blob/main/internal/exporter/feature_event.go) to see what are the fields available. |` +| `csvTemplate` | string | `{{ .Kind}};{{ .ContextKind}};{{ .UserKey}};{{ .CreationDate}};{{ .Key}};{{ .Variation}};{{ .Value}};{{ .Default}};{{ .Source}}\n` | CsvTemplate is used if your output format is CSV.
This field will be ignored if you are using another format than CSV.
You can decide which fields you want in your CSV line with a go-template syntax, please check [`internal/exporter/feature_event.go`](https://github.com/thomaspoignant/go-feature-flag/blob/main/internal/exporter/feature_event.go) to see what are the fields available. |` | `path` | string | **bucket root level** | The location of the directory in S3. | | `parquetCompressionCodec` | string | `SNAPPY` | ParquetCompressionCodec is the parquet compression codec for better space efficiency. [Available options](https://github.com/apache/parquet-format/blob/master/Compression.md) |` @@ -190,7 +190,7 @@ _Note that relay proxy is only supporting this while running inside the kubernet | `maxEventInMemory` | int | `100000` | If we hit that limit we will call the webhook. | | `format` | string | `JSON` | Format is the output format you want in your exported file. Available format: `JSON`, `CSV`, `Parquet`. | | `filename` | string | `flag-variation-{{ .Hostname}}-{{ .Timestamp}}.{{ .Format}}` | You can use a templated config to define the name of your exported files. Available replacement are `{{ .Hostname}}`, `{{ .Timestamp}}` and `{{ .Format}` | -| `csvTemplate` | string | `{{ .Kind}};{{ .ContextKind}};{{ .UserKey}};{{ .CreationDate}};{{ .Key}};{{ .Variation}};{{ .Value}};{{ .Default}}\n` | CsvTemplate is used if your output format is CSV.
This field will be ignored if you are using another format than CSV.
You can decide which fields you want in your CSV line with a go-template syntax, please check [`internal/exporter/feature_event.go`](https://github.com/thomaspoignant/go-feature-flag/blob/main/internal/exporter/feature_event.go) to see what are the fields available. |` +| `csvTemplate` | string | `{{ .Kind}};{{ .ContextKind}};{{ .UserKey}};{{ .CreationDate}};{{ .Key}};{{ .Variation}};{{ .Value}};{{ .Default}};{{ .Source}}\n` | CsvTemplate is used if your output format is CSV.
This field will be ignored if you are using another format than CSV.
You can decide which fields you want in your CSV line with a go-template syntax, please check [`internal/exporter/feature_event.go`](https://github.com/thomaspoignant/go-feature-flag/blob/main/internal/exporter/feature_event.go) to see what are the fields available. |` | `path` | string | **bucket root level** | The location of the directory in S3. | | `parquetCompressionCodec` | string | `SNAPPY` | ParquetCompressionCodec is the parquet compression codec for better space efficiency. [Available options](https://github.com/apache/parquet-format/blob/master/Compression.md) |`