Skip to content

Commit

Permalink
feat: Azure Blob Storage Exporter
Browse files Browse the repository at this point in the history
  • Loading branch information
Abeeujah committed Nov 7, 2024
1 parent cba1570 commit 1cba9a4
Show file tree
Hide file tree
Showing 14 changed files with 380 additions and 30 deletions.
57 changes: 29 additions & 28 deletions README.md

Large diffs are not rendered by default.

10 changes: 9 additions & 1 deletion cmd/relayproxy/config/exporter.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ type ExporterConf struct {
Topic string `mapstructure:"topic" koanf:"topic"`
StreamArn string `mapstructure:"streamArn" koanf:"streamarn"`
StreamName string `mapstructure:"streamName" koanf:"streamname"`
AccountName string `mapstructure:"accountName" koanf:"accountname"`
AccountKey string `mapstructure:"accountKey" koanf:"accountkey"`
Container string `mapstructure:"container" koanf:"container"`
}

func (c *ExporterConf) IsValid() error {
Expand Down Expand Up @@ -65,6 +68,10 @@ func (c *ExporterConf) IsValid() error {
return fmt.Errorf("invalid exporter: \"projectID\" and \"topic\" are required for kind \"%s\"", c.Kind)
}

if c.Kind == AzureExporter && c.Container == "" {
return fmt.Errorf("invalid exporter: no \"container\" property found for kind \"%s\"", c.Kind)
}

return nil
}

Expand All @@ -80,13 +87,14 @@ const (
SQSExporter ExporterKind = "sqs"
KafkaExporter ExporterKind = "kafka"
PubSubExporter ExporterKind = "pubsub"
AzureExporter ExporterKind = "azureBlobStorage"
)

// IsValid is checking if the value is part of the enum
func (r ExporterKind) IsValid() error {
switch r {
case FileExporter, WebhookExporter, LogExporter, S3Exporter, GoogleStorageExporter, SQSExporter, KafkaExporter,
PubSubExporter, KinesisExporter:
PubSubExporter, KinesisExporter, AzureExporter:
return nil
}
return fmt.Errorf("invalid exporter: kind \"%s\" is not supported", r)
Expand Down
9 changes: 9 additions & 0 deletions cmd/relayproxy/config/exporter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ func TestExporterConf_IsValid(t *testing.T) {
ProjectID string
Topic string
StreamName string
Container string
}
tests := []struct {
name string
Expand Down Expand Up @@ -77,6 +78,14 @@ func TestExporterConf_IsValid(t *testing.T) {
wantErr: true,
errValue: "invalid exporter: no \"bucket\" property found for kind \"googleStorage\"",
},
{
name: "kind azureBlobStorage without container",
fields: fields{
Kind: "azureBlobStorage",
},
wantErr: true,
errValue: "invalid exporter: no \"container\" property found for kind \"azureBlobStorage\"",
},
{
name: "kind webhook without bucket",
fields: fields{
Expand Down
10 changes: 10 additions & 0 deletions cmd/relayproxy/service/gofeatureflag.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
ffclient "github.com/thomaspoignant/go-feature-flag"
"github.com/thomaspoignant/go-feature-flag/cmd/relayproxy/config"
"github.com/thomaspoignant/go-feature-flag/exporter"
"github.com/thomaspoignant/go-feature-flag/exporter/azureexporter"
"github.com/thomaspoignant/go-feature-flag/exporter/fileexporter"
"github.com/thomaspoignant/go-feature-flag/exporter/gcstorageexporter"
"github.com/thomaspoignant/go-feature-flag/exporter/kafkaexporter"
Expand Down Expand Up @@ -303,6 +304,15 @@ func createExporter(c *config.ExporterConf) (exporter.CommonExporter, error) {
ProjectID: c.ProjectID,
Topic: c.Topic,
}, nil
case config.AzureExporter:
return &azureexporter.Exporter{
Container: c.Container,
Format: format,
Path: c.Path,
Filename: filename,
CsvTemplate: csvTemplate,
ParquetCompressionCodec: parquetCompressionCodec,
}, nil
default:
return nil, fmt.Errorf("invalid exporter: kind \"%s\" is not supported", c.Kind)
}
Expand Down
24 changes: 24 additions & 0 deletions cmd/relayproxy/service/gofeatureflag_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
ffclient "github.com/thomaspoignant/go-feature-flag"
"github.com/thomaspoignant/go-feature-flag/cmd/relayproxy/config"
"github.com/thomaspoignant/go-feature-flag/exporter"
"github.com/thomaspoignant/go-feature-flag/exporter/azureexporter"
"github.com/thomaspoignant/go-feature-flag/exporter/fileexporter"
"github.com/thomaspoignant/go-feature-flag/exporter/gcstorageexporter"
"github.com/thomaspoignant/go-feature-flag/exporter/kafkaexporter"
Expand Down Expand Up @@ -393,6 +394,29 @@ func Test_initExporter(t *testing.T) {
wantType: &kinesisexporter.Exporter{},
skipCompleteValidation: true,
},
{
name: "Azure Blob Storage Exporter",
wantErr: assert.NoError,
conf: &config.ExporterConf{
Kind: "azureBlobStorage",
Container: "my-container",
Path: "/my-path/",
MaxEventInMemory: 1990,
},
want: ffclient.DataExporter{
FlushInterval: config.DefaultExporter.FlushInterval,
MaxEventInMemory: 1990,
Exporter: &azureexporter.Exporter{
Container: "my-container",
Format: config.DefaultExporter.Format,
Path: "/my-path/",
Filename: config.DefaultExporter.FileName,
CsvTemplate: config.DefaultExporter.CsvFormat,
ParquetCompressionCodec: config.DefaultExporter.ParquetCompressionCodec,
},
},
wantType: &azureexporter.Exporter{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
Expand Down
130 changes: 130 additions & 0 deletions exporter/azureexporter/exporter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
package azureexporter

import (
"context"
"fmt"
"log/slog"
"os"

"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"
"github.com/thomaspoignant/go-feature-flag/exporter"
"github.com/thomaspoignant/go-feature-flag/exporter/fileexporter"
"github.com/thomaspoignant/go-feature-flag/utils/fflog"
)

type Exporter struct {
// Container is the name of your Azure Blob Storage Container similar to Buckets in S3.
Container string

// Storage Account Name and Key
AccountName string
AccountKey string

// Format is the output format you want in your exported file.
// Available format are JSON, CSV, and Parquet.
// Default: JSON
Format string

// Path allows you to specify in which directory you want to export your data.
Path string

// Filename is the name of your output file
// You can use a templated config to define the name of your export files.
// Available replacement are {{ .Hostname}}, {{ .Timestamp}} and {{ .Format}}
// Default: "flag-variation-{{ .Hostname}}-{{ .Timestamp}}.{{ .Format}}"
Filename string

// 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 exporter/feature_event.go to see what are the fields available.
// Default:
// {{ .Kind}};{{ .ContextKind}};{{ .UserKey}};{{ .CreationDate}};{{ .Key}};{{ .Variation}};{{ .Value}};{{ .Default}}\n
CsvTemplate string

// ParquetCompressionCodec is the parquet compression codec for better space efficiency.
// Available options https://github.com/apache/parquet-format/blob/master/Compression.md
// Default: SNAPPY
ParquetCompressionCodec string
}

func (f *Exporter) initializeAzureClient() (*azblob.Client, error) {
url := fmt.Sprintf("https://%s.blob.core.windows.net/", f.AccountName)

if f.AccountKey == "" {
cred, err := azidentity.NewDefaultAzureCredential(nil)
if err != nil {
return nil, err
}

Check warning on line 59 in exporter/azureexporter/exporter.go

View check run for this annotation

Codecov / codecov/patch

exporter/azureexporter/exporter.go#L58-L59

Added lines #L58 - L59 were not covered by tests
return azblob.NewClient(url, cred, nil)
}
cred, err := azblob.NewSharedKeyCredential(f.AccountName, f.AccountKey)
if err != nil {
return nil, err
}
return azblob.NewClientWithSharedKeyCredential(url, cred, nil)

Check warning on line 66 in exporter/azureexporter/exporter.go

View check run for this annotation

Codecov / codecov/patch

exporter/azureexporter/exporter.go#L62-L66

Added lines #L62 - L66 were not covered by tests
}

func (f *Exporter) Export(ctx context.Context, logger *fflog.FFLogger, featureEvents []exporter.FeatureEvent) error {
client, err := f.initializeAzureClient()
if err != nil {
return err
}

Check warning on line 73 in exporter/azureexporter/exporter.go

View check run for this annotation

Codecov / codecov/patch

exporter/azureexporter/exporter.go#L72-L73

Added lines #L72 - L73 were not covered by tests

if f.Container == "" {
return fmt.Errorf("you should specify a container. %v is invalid", f.Container)
}

Check warning on line 77 in exporter/azureexporter/exporter.go

View check run for this annotation

Codecov / codecov/patch

exporter/azureexporter/exporter.go#L76-L77

Added lines #L76 - L77 were not covered by tests

outputDir, err := os.MkdirTemp("", "go_feature_flag_AzureBlobStorage_export")
if err != nil {
return err
}

Check warning on line 82 in exporter/azureexporter/exporter.go

View check run for this annotation

Codecov / codecov/patch

exporter/azureexporter/exporter.go#L81-L82

Added lines #L81 - L82 were not covered by tests
defer func() { _ = os.Remove(outputDir) }()

fileExporter := fileexporter.Exporter{
Format: f.Format,
OutputDir: outputDir,
Filename: f.Filename,
CsvTemplate: f.CsvTemplate,
ParquetCompressionCodec: f.ParquetCompressionCodec,
}
err = fileExporter.Export(ctx, logger, featureEvents)
if err != nil {
return err
}

Check warning on line 95 in exporter/azureexporter/exporter.go

View check run for this annotation

Codecov / codecov/patch

exporter/azureexporter/exporter.go#L94-L95

Added lines #L94 - L95 were not covered by tests

files, err := os.ReadDir(outputDir)
if err != nil {
return err
}

Check warning on line 100 in exporter/azureexporter/exporter.go

View check run for this annotation

Codecov / codecov/patch

exporter/azureexporter/exporter.go#L99-L100

Added lines #L99 - L100 were not covered by tests

for _, file := range files {
fileName := file.Name()
of, err := os.Open(outputDir + fileName)
if err != nil {
logger.Error("[Azure Exporter] impossible to open file", slog.String("path", outputDir+"/"+fileName))
continue
}
defer func() { _ = of.Close() }()

Check warning on line 109 in exporter/azureexporter/exporter.go

View check run for this annotation

Codecov / codecov/patch

exporter/azureexporter/exporter.go#L109

Added line #L109 was not covered by tests

// prepend the path
source := fileName
if f.Path != "" {
source = f.Path + "/" + fileName
}

Check warning on line 115 in exporter/azureexporter/exporter.go

View check run for this annotation

Codecov / codecov/patch

exporter/azureexporter/exporter.go#L112-L115

Added lines #L112 - L115 were not covered by tests

_, err = client.UploadFile(context.Background(), f.Container, source, of, nil)
if err != nil {
logger.Error("[Azure Exporter] failed to upload file", slog.String("path", outputDir+"/"+fileName))
return err
}

Check warning on line 121 in exporter/azureexporter/exporter.go

View check run for this annotation

Codecov / codecov/patch

exporter/azureexporter/exporter.go#L117-L121

Added lines #L117 - L121 were not covered by tests

logger.Info("[Azure Exporter] file uploaded.", slog.String("location", f.Container+"/"+fileName))

Check warning on line 123 in exporter/azureexporter/exporter.go

View check run for this annotation

Codecov / codecov/patch

exporter/azureexporter/exporter.go#L123

Added line #L123 was not covered by tests
}
return nil
}

func (f *Exporter) IsBulk() bool {
return true
}
74 changes: 74 additions & 0 deletions exporter/azureexporter/exporter_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package azureexporter_test

import (
"context"
"log/slog"
"os"
"testing"

"github.com/stretchr/testify/assert"
"github.com/thomaspoignant/go-feature-flag/exporter"
"github.com/thomaspoignant/go-feature-flag/exporter/azureexporter"
"github.com/thomaspoignant/go-feature-flag/utils/fflog"
)

func TestAzureBlobStorage_Export(t *testing.T) {
hostname, _ := os.Hostname()
type fields struct {
Container string
AccountName string
AccountKey string
Format string
Path string
Filename string
CsvTemplate string
}

tests := []struct {
name string
fields fields
events []exporter.FeatureEvent
wantErr bool
expectedName string
}{
{
name: "All default test",
fields: fields{
Container: "test",
},
events: []exporter.FeatureEvent{
{
Kind: "feature", ContextKind: "anonymousUser", UserKey: "ABCD", CreationDate: 1617970547, Key: "random-key",
Variation: "Default", Value: "YO", Default: false,
},
},
expectedName: "^flag-variation-" + hostname + "-[0-9]*\\.json$",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f := azureexporter.Exporter{
Container: tt.fields.Container,
AccountName: tt.fields.AccountName,
AccountKey: tt.fields.AccountKey,
Format: tt.fields.Format,
Path: tt.fields.Path,
Filename: tt.fields.Filename,
CsvTemplate: tt.fields.CsvTemplate,
}

err := f.Export(context.Background(), &fflog.FFLogger{LeveledLogger: slog.Default()}, tt.events)
if tt.wantErr {
assert.Error(t, err, "Export should error")
return
}
assert.NoError(t, err, "Export should not error")
})
}
}

func TestAzureBlobStorage_IsBulk(t *testing.T) {
exporter := azureexporter.Exporter{}
assert.True(t, exporter.IsBulk(), "exporter is a bulk exporter")
}
7 changes: 7 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ go 1.22.8
require (
cloud.google.com/go/pubsub v1.45.1
cloud.google.com/go/storage v1.46.0
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.1.0
github.com/BurntSushi/toml v1.4.0
github.com/IBM/sarama v1.43.3
github.com/aws/aws-lambda-go v1.47.0
Expand Down Expand Up @@ -79,7 +81,10 @@ require (
cloud.google.com/go/iam v1.2.1 // indirect
cloud.google.com/go/monitoring v1.21.1 // indirect
dario.cat/mergo v1.0.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.6.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0 // indirect
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
github.com/AzureAD/microsoft-authentication-library-for-go v1.0.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.24.1 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.48.1 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.48.1 // indirect
Expand Down Expand Up @@ -139,6 +144,7 @@ require (
github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/golang/snappy v0.0.4 // indirect
Expand Down Expand Up @@ -187,6 +193,7 @@ require (
github.com/opencontainers/image-spec v1.1.0 // indirect
github.com/pelletier/go-toml v1.9.5 // indirect
github.com/pierrec/lz4/v4 v4.1.21 // indirect
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pkg/xattr v0.4.10 // indirect
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
Expand Down
Loading

0 comments on commit 1cba9a4

Please sign in to comment.