diff --git a/.github/workflows/proxy-ci.yml b/.github/workflows/proxy-ci.yml index 454eebf..6bdc5e5 100644 --- a/.github/workflows/proxy-ci.yml +++ b/.github/workflows/proxy-ci.yml @@ -19,15 +19,6 @@ jobs: test: runs-on: ubuntu-latest - services: - mongodb: - image: mongodb/mongodb-community-server - ports: - - 27017:27017 - dynamodb: - image: amazon/dynamodb-local - ports: - - 8000:8000 steps: - uses: actions/checkout@v5 - name: Setup Go @@ -47,15 +38,6 @@ jobs: coverage: runs-on: ubuntu-latest needs: test - services: - mongodb: - image: mongodb/mongodb-community-server - ports: - - 27017:27017 - dynamodb: - image: amazon/dynamodb-local - ports: - - 8000:8000 steps: - uses: actions/checkout@v5 with: diff --git a/.github/workflows/proxy-release.yml b/.github/workflows/proxy-release.yml index 46a8a1a..be35714 100644 --- a/.github/workflows/proxy-release.yml +++ b/.github/workflows/proxy-release.yml @@ -12,15 +12,6 @@ permissions: jobs: test: runs-on: ubuntu-latest - services: - mongodb: - image: mongodb/mongodb-community-server - ports: - - 27017:27017 - dynamodb: - image: amazon/dynamodb-local - ports: - - 8000:8000 steps: - uses: actions/checkout@v5 - name: Setup Go diff --git a/cache/cache.go b/cache/cache.go new file mode 100644 index 0000000..edc0432 --- /dev/null +++ b/cache/cache.go @@ -0,0 +1,51 @@ +package cache + +import ( + "context" + "time" + + "github.com/configcat/configcat-proxy/config" + "github.com/configcat/configcat-proxy/diag/telemetry" + "github.com/configcat/configcat-proxy/log" + configcat "github.com/configcat/go-sdk/v9" +) + +const ( + keyName = "key" + payloadName = "payload" +) + +type ReaderWriter = configcat.ConfigCache + +type External interface { + ReaderWriter + Shutdown() +} + +func SetupExternalCache(conf *config.CacheConfig, telemetryReporter telemetry.Reporter, log log.Logger) (External, error) { + cacheLog := log.WithPrefix("cache") + + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) // give 15 sec to spin up the cache connection + defer cancel() + + if conf.Redis.Enabled { + redis, err := newRedis(&conf.Redis, telemetryReporter, cacheLog) + if err != nil { + return nil, err + } + return redis, nil + } else if conf.MongoDb.Enabled { + mongoDb, err := newMongoDb(ctx, &conf.MongoDb, telemetryReporter, cacheLog) + if err != nil { + return nil, err + } + return mongoDb, nil + } else if conf.DynamoDb.Enabled { + dynamoDb, err := newDynamoDb(ctx, &conf.DynamoDb, telemetryReporter, cacheLog) + if err != nil { + return nil, err + } + return dynamoDb, nil + } + return nil, nil +} diff --git a/cache/cache_test.go b/cache/cache_test.go new file mode 100644 index 0000000..7cdf7b2 --- /dev/null +++ b/cache/cache_test.go @@ -0,0 +1,64 @@ +package cache + +import ( + "testing" + + "github.com/alicebob/miniredis/v2" + "github.com/configcat/configcat-proxy/config" + "github.com/configcat/configcat-proxy/diag/telemetry" + "github.com/configcat/configcat-proxy/log" + "github.com/stretchr/testify/assert" +) + +func TestSetupExternalCache_OnlyOneSelected(t *testing.T) { + s := miniredis.RunT(t) + store, err := SetupExternalCache(&config.CacheConfig{ + Redis: config.RedisConfig{Addresses: []string{s.Addr()}, Enabled: true}, + MongoDb: config.MongoDbConfig{ + Enabled: true, + Url: "mongodb://localhost:27017", + Database: "test_db", + Collection: "coll", + }, + }, telemetry.NewEmptyReporter(), log.NewNullLogger()) + assert.NoError(t, err) + defer store.Shutdown() + assert.IsType(t, &redisStore{}, store) +} + +func (s *mongoTestSuite) TestSetupExternalCache() { + store, err := SetupExternalCache(&config.CacheConfig{MongoDb: config.MongoDbConfig{ + Enabled: true, + Url: s.addr, + Database: "test_db", + Collection: "coll", + }}, telemetry.NewEmptyReporter(), log.NewNullLogger()) + assert.NoError(s.T(), err) + defer store.Shutdown() + assert.IsType(s.T(), &mongoDbStore{}, store) +} + +func (s *redisTestSuite) TestSetupExternalCache() { + store, err := SetupExternalCache(&config.CacheConfig{Redis: config.RedisConfig{Addresses: []string{"localhost:" + s.dbPort}, Enabled: true}}, telemetry.NewEmptyReporter(), log.NewNullLogger()) + assert.NoError(s.T(), err) + defer store.Shutdown() + assert.IsType(s.T(), &redisStore{}, store) +} + +func (s *valkeyTestSuite) TestSetupExternalCache() { + store, err := SetupExternalCache(&config.CacheConfig{Redis: config.RedisConfig{Addresses: []string{"localhost:" + s.dbPort}, Enabled: true}}, telemetry.NewEmptyReporter(), log.NewNullLogger()) + assert.NoError(s.T(), err) + defer store.Shutdown() + assert.IsType(s.T(), &redisStore{}, store) +} + +func (s *dynamoDbTestSuite) TestSetupExternalCache() { + store, err := SetupExternalCache(&config.CacheConfig{DynamoDb: config.DynamoDbConfig{ + Enabled: true, + Table: tableName, + Url: s.addr, + }}, telemetry.NewEmptyReporter(), log.NewNullLogger()) + assert.NoError(s.T(), err) + defer store.Shutdown() + assert.IsType(s.T(), &dynamoDbStore{}, store) +} diff --git a/sdk/store/cache/dynamodb.go b/cache/dynamodb.go similarity index 92% rename from sdk/store/cache/dynamodb.go rename to cache/dynamodb.go index 9ddc951..826ae8b 100644 --- a/sdk/store/cache/dynamodb.go +++ b/cache/dynamodb.go @@ -9,6 +9,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/dynamodb" "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" "github.com/configcat/configcat-proxy/config" + "github.com/configcat/configcat-proxy/diag/telemetry" "github.com/configcat/configcat-proxy/log" ) @@ -18,9 +19,10 @@ type dynamoDbStore struct { log log.Logger } -func newDynamoDb(ctx context.Context, conf *config.DynamoDbConfig, log log.Logger) (External, error) { +func newDynamoDb(ctx context.Context, conf *config.DynamoDbConfig, telemetryReporter telemetry.Reporter, log log.Logger) (External, error) { dynamoLog := log.WithPrefix("dynamodb") awsCtx, err := awsconfig.LoadDefaultConfig(ctx) + telemetryReporter.InstrumentAws(&awsCtx) if err != nil { dynamoLog.Errorf("couldn't read aws config for DynamoDB: %s", err) return nil, err diff --git a/cache/dynamodb_test.go b/cache/dynamodb_test.go new file mode 100644 index 0000000..587a289 --- /dev/null +++ b/cache/dynamodb_test.go @@ -0,0 +1,174 @@ +package cache + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + awsconfig "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/service/dynamodb" + "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" + "github.com/configcat/configcat-proxy/config" + "github.com/configcat/configcat-proxy/diag/telemetry" + "github.com/configcat/configcat-proxy/log" + "github.com/configcat/go-sdk/v9/configcatcache" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" + "github.com/testcontainers/testcontainers-go" + tcdynamodb "github.com/testcontainers/testcontainers-go/modules/dynamodb" +) + +const ( + tableName = "test-table" +) + +type dynamoDbTestSuite struct { + suite.Suite + + db *tcdynamodb.DynamoDBContainer + addr string +} + +func (s *dynamoDbTestSuite) SetupSuite() { + dynamoDbContainer, err := tcdynamodb.Run(s.T().Context(), "amazon/dynamodb-local") + if err != nil { + panic("failed to start container: " + err.Error() + "") + } + s.db = dynamoDbContainer + str, _ := s.db.ConnectionString(s.T().Context()) + s.addr = "http://" + str + + s.T().Setenv("AWS_ACCESS_KEY_ID", "key") + s.T().Setenv("AWS_SECRET_ACCESS_KEY", "secret") + s.T().Setenv("AWS_SESSION_TOKEN", "session") + s.T().Setenv("AWS_DEFAULT_REGION", "us-east-1") +} + +func (s *dynamoDbTestSuite) TearDownSuite() { + if err := testcontainers.TerminateContainer(s.db); err != nil { + panic("failed to terminate container: " + err.Error() + "") + } +} + +func TestRunDynamoDbSuite(t *testing.T) { + suite.Run(t, new(dynamoDbTestSuite)) +} + +func (s *dynamoDbTestSuite) TestDynamoDbStore() { + assert.NoError(s.T(), createTableIfNotExist(s.T().Context(), tableName, s.addr)) + + s.Run("ok", func() { + store, err := newDynamoDb(s.T().Context(), &config.DynamoDbConfig{ + Enabled: true, + Table: tableName, + Url: s.addr, + }, telemetry.NewEmptyReporter(), log.NewNullLogger()) + assert.NoError(s.T(), err) + defer store.Shutdown() + + cacheEntry := configcatcache.CacheSegmentsToBytes(time.Now(), "etag", []byte(`test`)) + + err = store.Set(s.T().Context(), "k1", cacheEntry) + assert.NoError(s.T(), err) + + res, err := store.Get(s.T().Context(), "k1") + assert.NoError(s.T(), err) + assert.Equal(s.T(), cacheEntry, res) + + cacheEntry = configcatcache.CacheSegmentsToBytes(time.Now(), "etag", []byte(`test2`)) + + err = store.Set(s.T().Context(), "k1", cacheEntry) + assert.NoError(s.T(), err) + + res, err = store.Get(s.T().Context(), "k1") + assert.NoError(s.T(), err) + assert.Equal(s.T(), cacheEntry, res) + }) + + s.Run("empty", func() { + store, err := newDynamoDb(s.T().Context(), &config.DynamoDbConfig{ + Enabled: true, + Table: tableName, + Url: s.addr, + }, telemetry.NewEmptyReporter(), log.NewNullLogger()) + assert.NoError(s.T(), err) + defer store.Shutdown() + + _, err = store.Get(s.T().Context(), "k2") + assert.Error(s.T(), err) + }) + + s.Run("no-table", func() { + store, err := newDynamoDb(s.T().Context(), &config.DynamoDbConfig{ + Enabled: true, + Table: "nonexisting", + Url: s.addr, + }, telemetry.NewEmptyReporter(), log.NewNullLogger()) + assert.NoError(s.T(), err) + defer store.Shutdown() + + _, err = store.Get(s.T().Context(), "k3") + assert.Error(s.T(), err) + }) +} + +func createTableIfNotExist(ctx context.Context, table string, addr string) error { + awsCtx, err := awsconfig.LoadDefaultConfig(ctx) + if err != nil { + return err + } + var opts []func(*dynamodb.Options) + opts = append(opts, func(options *dynamodb.Options) { + options.BaseEndpoint = aws.String(addr) + }) + + client := dynamodb.NewFromConfig(awsCtx, opts...) + + _, err = client.DescribeTable(ctx, &dynamodb.DescribeTableInput{ + TableName: aws.String(table), + }) + if err == nil { + return nil + } + _, err = client.CreateTable(ctx, &dynamodb.CreateTableInput{ + TableName: aws.String(table), + AttributeDefinitions: []types.AttributeDefinition{ + { + AttributeName: aws.String(keyName), + AttributeType: types.ScalarAttributeTypeS, + }, + }, + KeySchema: []types.KeySchemaElement{ + { + AttributeName: aws.String(keyName), + KeyType: types.KeyTypeHash, + }, + }, + ProvisionedThroughput: &types.ProvisionedThroughput{ + ReadCapacityUnits: aws.Int64(1), + WriteCapacityUnits: aws.Int64(1), + }, + }) + if err != nil { + return err + } + + timeout := time.After(5 * time.Second) + ticker := time.NewTicker(100 * time.Millisecond) + defer ticker.Stop() + for { + select { + case <-timeout: + return fmt.Errorf("table creation timed out") + case <-ticker.C: + res, err := client.DescribeTable(ctx, &dynamodb.DescribeTableInput{ + TableName: aws.String(table), + }) + if err == nil && res.Table.TableStatus == types.TableStatusActive { + return nil + } + } + } +} diff --git a/sdk/store/cache/mongodb.go b/cache/mongodb.go similarity index 84% rename from sdk/store/cache/mongodb.go rename to cache/mongodb.go index 399e06d..7e31f37 100644 --- a/sdk/store/cache/mongodb.go +++ b/cache/mongodb.go @@ -5,10 +5,11 @@ import ( "time" "github.com/configcat/configcat-proxy/config" + "github.com/configcat/configcat-proxy/diag/telemetry" "github.com/configcat/configcat-proxy/log" - "go.mongodb.org/mongo-driver/bson" - "go.mongodb.org/mongo-driver/mongo" - "go.mongodb.org/mongo-driver/mongo/options" + "go.mongodb.org/mongo-driver/v2/bson" + "go.mongodb.org/mongo-driver/v2/mongo" + "go.mongodb.org/mongo-driver/v2/mongo/options" ) type mongoDbStore struct { @@ -22,8 +23,9 @@ type entry struct { Payload []byte } -func newMongoDb(ctx context.Context, conf *config.MongoDbConfig, log log.Logger) (External, error) { +func newMongoDb(ctx context.Context, conf *config.MongoDbConfig, telemetryReporter telemetry.Reporter, log log.Logger) (External, error) { opts := options.Client().ApplyURI(conf.Url) + telemetryReporter.InstrumentMongoDb(opts) if conf.Tls.Enabled { t, err := conf.Tls.LoadTlsOptions() if err != nil { @@ -32,7 +34,7 @@ func newMongoDb(ctx context.Context, conf *config.MongoDbConfig, log log.Logger) } opts.SetTLSConfig(t) } - client, err := mongo.Connect(ctx, opts) + client, err := mongo.Connect(opts) if err != nil { log.Errorf("couldn't connect to MongoDB: %s", err) return nil, err diff --git a/cache/mongodb_test.go b/cache/mongodb_test.go new file mode 100644 index 0000000..0755f1b --- /dev/null +++ b/cache/mongodb_test.go @@ -0,0 +1,129 @@ +package cache + +import ( + "context" + "testing" + "time" + + "github.com/configcat/configcat-proxy/config" + "github.com/configcat/configcat-proxy/diag/telemetry" + "github.com/configcat/configcat-proxy/log" + "github.com/configcat/go-sdk/v9/configcatcache" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/modules/mongodb" +) + +type mongoTestSuite struct { + suite.Suite + + db *mongodb.MongoDBContainer + addr string +} + +func (s *mongoTestSuite) SetupSuite() { + mongodbContainer, err := mongodb.Run(s.T().Context(), "mongo") + if err != nil { + panic("failed to start container: " + err.Error() + "") + } + s.db = mongodbContainer + str, _ := s.db.ConnectionString(s.T().Context()) + s.addr = str +} + +func (s *mongoTestSuite) TearDownSuite() { + if err := testcontainers.TerminateContainer(s.db); err != nil { + panic("failed to terminate container: " + err.Error() + "") + } +} + +func TestRunMongoSuite(t *testing.T) { + suite.Run(t, new(mongoTestSuite)) +} + +func (s *mongoTestSuite) TestMongoDbStore() { + store, err := newMongoDb(s.T().Context(), &config.MongoDbConfig{ + Enabled: true, + Url: s.addr, + Database: "test_db", + Collection: "coll", + }, telemetry.NewEmptyReporter(), log.NewNullLogger()) + assert.NoError(s.T(), err) + defer store.Shutdown() + + cacheEntry := configcatcache.CacheSegmentsToBytes(time.Now(), "etag", []byte(`test`)) + + err = store.Set(s.T().Context(), "k1", cacheEntry) + assert.NoError(s.T(), err) + + res, err := store.Get(s.T().Context(), "k1") + assert.NoError(s.T(), err) + assert.Equal(s.T(), cacheEntry, res) + + cacheEntry = configcatcache.CacheSegmentsToBytes(time.Now(), "etag", []byte(`test2`)) + + err = store.Set(s.T().Context(), "k1", cacheEntry) + assert.NoError(s.T(), err) + + res, err = store.Get(s.T().Context(), "k1") + assert.NoError(s.T(), err) + assert.Equal(s.T(), cacheEntry, res) +} + +func (s *mongoTestSuite) TestMongoDbStore_Empty() { + store, err := newMongoDb(s.T().Context(), &config.MongoDbConfig{ + Enabled: true, + Url: s.addr, + Database: "test_db", + Collection: "coll", + }, telemetry.NewEmptyReporter(), log.NewNullLogger()) + assert.NoError(s.T(), err) + defer store.Shutdown() + + _, err = store.Get(s.T().Context(), "k2") + assert.Error(s.T(), err) +} + +func (s *mongoTestSuite) TestMongoDbStore_Invalid() { + _, err := newMongoDb(s.T().Context(), &config.MongoDbConfig{ + Enabled: true, + Url: "invalid", + Database: "test_db", + Collection: "coll", + }, telemetry.NewEmptyReporter(), log.NewNullLogger()) + + assert.Error(s.T(), err) +} + +func (s *mongoTestSuite) TestMongoDbStore_TLS_Invalid() { + store, err := newMongoDb(s.T().Context(), &config.MongoDbConfig{ + Enabled: true, + Url: s.addr, + Database: "test_db", + Collection: "coll", + Tls: config.TlsConfig{ + Enabled: true, + MinVersion: 1.1, + Certificates: []config.CertConfig{ + {Key: "nonexisting", Cert: "nonexisting"}, + }, + }, + }, telemetry.NewEmptyReporter(), log.NewNullLogger()) + assert.ErrorContains(s.T(), err, "failed to load certificate and key files") + assert.Nil(s.T(), store) +} + +func (s *mongoTestSuite) TestMongoDbStore_Connect_Fails() { + ctx, cancel := context.WithTimeout(s.T().Context(), 1*time.Second) + defer cancel() + + store, err := newMongoDb(ctx, &config.MongoDbConfig{ + Enabled: true, + Url: "mongodb://localhost:12345", + Database: "test_db", + Collection: "coll", + }, telemetry.NewEmptyReporter(), log.NewNullLogger()) + assert.ErrorContains(s.T(), err, "context deadline exceeded") + assert.Nil(s.T(), store) +} diff --git a/sdk/store/cache/redis.go b/cache/redis.go similarity index 80% rename from sdk/store/cache/redis.go rename to cache/redis.go index 67bd667..4a72752 100644 --- a/sdk/store/cache/redis.go +++ b/cache/redis.go @@ -4,6 +4,7 @@ import ( "context" "github.com/configcat/configcat-proxy/config" + "github.com/configcat/configcat-proxy/diag/telemetry" "github.com/configcat/configcat-proxy/log" "github.com/redis/go-redis/v9" ) @@ -13,7 +14,7 @@ type redisStore struct { log log.Logger } -func newRedis(conf *config.RedisConfig, log log.Logger) (External, error) { +func newRedis(conf *config.RedisConfig, telemetryReporter telemetry.Reporter, log log.Logger) (External, error) { opts := &redis.UniversalOptions{ Addrs: conf.Addresses, Password: conf.Password, @@ -30,9 +31,11 @@ func newRedis(conf *config.RedisConfig, log log.Logger) (External, error) { } opts.TLSConfig = t } + rdb := redis.NewUniversalClient(opts) + telemetryReporter.InstrumentRedis(rdb) log.Reportf("using Redis for cache storage") return &redisStore{ - redisDb: redis.NewUniversalClient(opts), + redisDb: rdb, log: log, }, nil } diff --git a/cache/redis_test.go b/cache/redis_test.go new file mode 100644 index 0000000..91ce85e --- /dev/null +++ b/cache/redis_test.go @@ -0,0 +1,150 @@ +package cache + +import ( + "bytes" + "crypto/x509" + "encoding/pem" + "testing" + "time" + + "github.com/configcat/configcat-proxy/config" + "github.com/configcat/configcat-proxy/diag/telemetry" + "github.com/configcat/configcat-proxy/internal/testutils" + "github.com/configcat/configcat-proxy/log" + "github.com/configcat/go-sdk/v9/configcatcache" + "github.com/docker/go-connections/nat" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/modules/redis" +) + +type redisTestSuite struct { + suite.Suite + + db *redis.RedisContainer + dbPort string +} + +func (s *redisTestSuite) SetupSuite() { + redisContainer, err := redis.Run(s.T().Context(), "redis") + if err != nil { + panic("failed to start container: " + err.Error() + "") + } + s.db = redisContainer + p, _ := nat.NewPort("tcp", "6379") + dbPort, _ := s.db.MappedPort(s.T().Context(), p) + s.dbPort = dbPort.Port() +} + +func (s *redisTestSuite) TearDownSuite() { + if err := testcontainers.TerminateContainer(s.db); err != nil { + panic("failed to terminate container: " + err.Error() + "") + } +} + +func TestRedisSuite(t *testing.T) { + suite.Run(t, new(redisTestSuite)) +} + +func (s *redisTestSuite) TestRedisStorage() { + store, err := newRedis(&config.RedisConfig{Addresses: []string{"localhost:" + s.dbPort}}, telemetry.NewEmptyReporter(), log.NewNullLogger()) + assert.NoError(s.T(), err) + srv := store.(*redisStore) + defer srv.Shutdown() + + cacheEntry := configcatcache.CacheSegmentsToBytes(time.Now(), "etag", []byte(`test`)) + err = srv.Set(s.T().Context(), "key", cacheEntry) + assert.NoError(s.T(), err) + res, err := srv.Get(s.T().Context(), "key") + assert.NoError(s.T(), err) + assert.Equal(s.T(), cacheEntry, res) + _, _, j, err := configcatcache.CacheSegmentsFromBytes(res) + assert.NoError(s.T(), err) + assert.Equal(s.T(), `test`, string(j)) +} + +func (s *redisTestSuite) TestRedisStorage_Unavailable() { + store, err := newRedis(&config.RedisConfig{Addresses: []string{"nonexisting"}}, telemetry.NewEmptyReporter(), log.NewNullLogger()) + assert.NoError(s.T(), err) + srv := store.(*redisStore) + defer srv.Shutdown() + + cacheEntry := configcatcache.CacheSegmentsToBytes(time.Now(), "etag", []byte(`test`)) + err = srv.Set(s.T().Context(), "", cacheEntry) + assert.Error(s.T(), err) + _, err = srv.Get(s.T().Context(), "") + assert.Error(s.T(), err) +} + +func TestRedisStorage_TLS(t *testing.T) { + ctx := t.Context() + + redisContainer, err := redis.Run(ctx, "redis", redis.WithTLS()) + defer func() { + if err := testcontainers.TerminateContainer(redisContainer); err != nil { + panic("failed to terminate container: " + err.Error() + "") + } + }() + if err != nil { + panic("failed to start container: " + err.Error() + "") + } + + p, _ := nat.NewPort("tcp", "6379") + dbPort, _ := redisContainer.MappedPort(t.Context(), p) + + tls := redisContainer.TLSConfig() + + cert := tls.Certificates[0] + + var pemCerts [][]byte + for _, derBytes := range cert.Certificate { + pemBlock := &pem.Block{ + Type: "CERTIFICATE", + Bytes: derBytes, + } + pemBytes := pem.EncodeToMemory(pemBlock) + pemCerts = append(pemCerts, pemBytes) + } + + pk, err := x509.MarshalPKCS8PrivateKey(cert.PrivateKey) + assert.NoError(t, err) + k := pem.EncodeToMemory(&pem.Block{ + Type: "PRIVATE KEY", + Bytes: pk, + }) + + t.Run("valid", func(t *testing.T) { + sep := []byte{'\n'} + testutils.UseTempFile(string(bytes.Join(pemCerts, sep)), func(cert string) { + testutils.UseTempFile(string(k), func(key string) { + store, err := newRedis(&config.RedisConfig{ + Addresses: []string{"localhost:" + dbPort.Port()}, + Tls: config.TlsConfig{ + Enabled: true, + MinVersion: 1.1, + Certificates: []config.CertConfig{ + {Key: key, Cert: cert}, + }, + }, + }, telemetry.NewEmptyReporter(), log.NewNullLogger()) + assert.NoError(t, err) + assert.NotNil(t, store) + }) + }) + }) + t.Run("invalid", func(t *testing.T) { + store, err := newRedis(&config.RedisConfig{ + Addresses: []string{"localhost:" + dbPort.Port()}, + Tls: config.TlsConfig{ + Enabled: true, + MinVersion: 1.1, + Certificates: []config.CertConfig{ + {Key: "nonexisting", Cert: "nonexisting"}, + }, + }, + }, telemetry.NewEmptyReporter(), log.NewNullLogger()) + assert.ErrorContains(t, err, "failed to load certificate and key files") + assert.Nil(t, store) + }) +} diff --git a/cache/valkey_test.go b/cache/valkey_test.go new file mode 100644 index 0000000..51a03fc --- /dev/null +++ b/cache/valkey_test.go @@ -0,0 +1,151 @@ +package cache + +import ( + "bytes" + "crypto/x509" + "encoding/pem" + "testing" + "time" + + "github.com/configcat/configcat-proxy/config" + "github.com/configcat/configcat-proxy/diag/telemetry" + "github.com/configcat/configcat-proxy/internal/testutils" + "github.com/configcat/configcat-proxy/log" + "github.com/configcat/go-sdk/v9/configcatcache" + "github.com/docker/go-connections/nat" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/modules/redis" + "github.com/testcontainers/testcontainers-go/modules/valkey" +) + +type valkeyTestSuite struct { + suite.Suite + + db *valkey.ValkeyContainer + dbPort string +} + +func (s *valkeyTestSuite) SetupSuite() { + valkeyContainer, err := valkey.Run(s.T().Context(), "valkey/valkey") + if err != nil { + panic("failed to start container: " + err.Error() + "") + } + s.db = valkeyContainer + p, _ := nat.NewPort("tcp", "6379") + dbPort, _ := s.db.MappedPort(s.T().Context(), p) + s.dbPort = dbPort.Port() +} + +func (s *valkeyTestSuite) TearDownSuite() { + if err := testcontainers.TerminateContainer(s.db); err != nil { + panic("failed to terminate container: " + err.Error() + "") + } +} + +func TestValkeySuite(t *testing.T) { + suite.Run(t, new(valkeyTestSuite)) +} + +func (s *valkeyTestSuite) TestValkeyStorage() { + store, err := newRedis(&config.RedisConfig{Addresses: []string{"localhost:" + s.dbPort}}, telemetry.NewEmptyReporter(), log.NewNullLogger()) + assert.NoError(s.T(), err) + srv := store.(*redisStore) + defer srv.Shutdown() + + cacheEntry := configcatcache.CacheSegmentsToBytes(time.Now(), "etag", []byte(`test`)) + err = srv.Set(s.T().Context(), "key", cacheEntry) + assert.NoError(s.T(), err) + res, err := srv.Get(s.T().Context(), "key") + assert.NoError(s.T(), err) + assert.Equal(s.T(), cacheEntry, res) + _, _, j, err := configcatcache.CacheSegmentsFromBytes(res) + assert.NoError(s.T(), err) + assert.Equal(s.T(), `test`, string(j)) +} + +func (s *valkeyTestSuite) TestValkeyStorage_Unavailable() { + store, err := newRedis(&config.RedisConfig{Addresses: []string{"nonexisting"}}, telemetry.NewEmptyReporter(), log.NewNullLogger()) + assert.NoError(s.T(), err) + srv := store.(*redisStore) + defer srv.Shutdown() + + cacheEntry := configcatcache.CacheSegmentsToBytes(time.Now(), "etag", []byte(`test`)) + err = srv.Set(s.T().Context(), "", cacheEntry) + assert.Error(s.T(), err) + _, err = srv.Get(s.T().Context(), "") + assert.Error(s.T(), err) +} + +func TestValkeyStorage_TLS(t *testing.T) { + ctx := t.Context() + + valkeyContainer, err := redis.Run(ctx, "redis", redis.WithTLS()) + defer func() { + if err := testcontainers.TerminateContainer(valkeyContainer); err != nil { + panic("failed to terminate container: " + err.Error() + "") + } + }() + if err != nil { + panic("failed to start container: " + err.Error() + "") + } + + p, _ := nat.NewPort("tcp", "6379") + dbPort, _ := valkeyContainer.MappedPort(t.Context(), p) + + tls := valkeyContainer.TLSConfig() + + cert := tls.Certificates[0] + + var pemCerts [][]byte + for _, derBytes := range cert.Certificate { + pemBlock := &pem.Block{ + Type: "CERTIFICATE", + Bytes: derBytes, + } + pemBytes := pem.EncodeToMemory(pemBlock) + pemCerts = append(pemCerts, pemBytes) + } + + pk, err := x509.MarshalPKCS8PrivateKey(cert.PrivateKey) + assert.NoError(t, err) + k := pem.EncodeToMemory(&pem.Block{ + Type: "PRIVATE KEY", + Bytes: pk, + }) + + t.Run("valid", func(t *testing.T) { + sep := []byte{'\n'} + testutils.UseTempFile(string(bytes.Join(pemCerts, sep)), func(cert string) { + testutils.UseTempFile(string(k), func(key string) { + store, err := newRedis(&config.RedisConfig{ + Addresses: []string{"localhost:" + dbPort.Port()}, + Tls: config.TlsConfig{ + Enabled: true, + MinVersion: 1.1, + Certificates: []config.CertConfig{ + {Key: key, Cert: cert}, + }, + }, + }, telemetry.NewEmptyReporter(), log.NewNullLogger()) + assert.NoError(t, err) + assert.NotNil(t, store) + }) + }) + }) + t.Run("invalid", func(t *testing.T) { + store, err := newRedis(&config.RedisConfig{ + Addresses: []string{"localhost:" + dbPort.Port()}, + Tls: config.TlsConfig{ + Enabled: true, + MinVersion: 1.1, + Certificates: []config.CertConfig{ + {Key: "nonexisting", Cert: "nonexisting"}, + }, + }, + }, telemetry.NewEmptyReporter(), log.NewNullLogger()) + assert.ErrorContains(t, err, "failed to load certificate and key files") + assert.Nil(t, store) + }) +} diff --git a/config/config.go b/config/config.go index 8dbdd5b..36be522 100644 --- a/config/config.go +++ b/config/config.go @@ -24,9 +24,9 @@ const ( DefaultWebhookSignatureValidFor = 300 - defaultSdkPollInterval = 60 - defaultCachePollInterval = 5 - defaultAutoSdkPollInterval = 300 + DefaultSdkPollInterval = 60 + DefaultCachePollInterval = 5 + DefaultAutoSdkPollInterval = 300 ) var allowedLogLevels = map[string]log.Level{ @@ -239,13 +239,31 @@ type DiagConfig struct { Port int `yaml:"port"` Enabled bool `yaml:"enabled"` Metrics MetricsConfig `yaml:"metrics"` + Traces TraceConfig `yaml:"traces"` Status StatusConfig `yaml:"status"` } type MetricsConfig struct { + Enabled bool `yaml:"enabled"` + Prometheus PrometheusExporterConfig `yaml:"prometheus"` + Otlp OtlpExporterConfig `yaml:"otlp"` +} + +type TraceConfig struct { + Enabled bool `yaml:"enabled"` + Otlp OtlpExporterConfig `yaml:"otlp"` +} + +type PrometheusExporterConfig struct { Enabled bool `yaml:"enabled"` } +type OtlpExporterConfig struct { + Enabled bool `yaml:"enabled"` + Protocol string + Endpoint string +} + type StatusConfig struct { Enabled bool `yaml:"enabled"` } @@ -322,9 +340,12 @@ func (c *Config) setDefaults() { c.Grpc.ServerReflectionEnabled = false c.Diag.Enabled = true + c.Diag.Port = 8051 c.Diag.Status.Enabled = true c.Diag.Metrics.Enabled = true - c.Diag.Port = 8051 + c.Diag.Metrics.Prometheus.Enabled = true + c.Diag.Metrics.Otlp.Protocol = "http" + c.Diag.Traces.Otlp.Protocol = "http" c.Http.Sse.Enabled = true c.Http.Sse.CORS.Enabled = true @@ -335,7 +356,7 @@ func (c *Config) setDefaults() { c.Http.Api.Enabled = true c.Http.Api.CORS.Enabled = true - c.Http.OFREP.Enabled = false + c.Http.OFREP.Enabled = true c.Http.OFREP.CORS.Enabled = true c.Http.Webhook.Enabled = true @@ -362,20 +383,20 @@ func (c *Config) fixupDefaults() { sdk.WebhookSignatureValidFor = DefaultWebhookSignatureValidFor } if sdk.PollInterval == 0 { - sdk.PollInterval = defaultSdkPollInterval + sdk.PollInterval = DefaultSdkPollInterval } if sdk.Offline.Local.PollInterval == 0 { - sdk.Offline.Local.PollInterval = defaultCachePollInterval + sdk.Offline.Local.PollInterval = DefaultCachePollInterval } if sdk.Offline.CachePollInterval == 0 { - sdk.Offline.CachePollInterval = defaultCachePollInterval + sdk.Offline.CachePollInterval = DefaultCachePollInterval } } if c.GlobalOfflineConfig.CachePollInterval == 0 { - c.GlobalOfflineConfig.CachePollInterval = defaultCachePollInterval + c.GlobalOfflineConfig.CachePollInterval = DefaultCachePollInterval } if c.Profile.PollInterval == 0 { - c.Profile.PollInterval = defaultAutoSdkPollInterval + c.Profile.PollInterval = DefaultAutoSdkPollInterval } if c.Profile.WebhookSignatureValidFor == 0 { c.Profile.WebhookSignatureValidFor = DefaultWebhookSignatureValidFor @@ -504,6 +525,26 @@ func (a *ProfileConfig) IsSet() bool { return a.Key != "" } +func (d *DiagConfig) IsMetricsEnabled() bool { + return d.Enabled && d.Metrics.Enabled +} + +func (d *DiagConfig) IsTracesEnabled() bool { + return d.Enabled && d.Traces.Enabled +} + +func (d *DiagConfig) IsStatusEnabled() bool { + return d.Enabled && d.Status.Enabled +} + +func (d *DiagConfig) IsPrometheusExporterEnabled() bool { + return d.IsMetricsEnabled() && d.Metrics.Prometheus.Enabled +} + +func (d *DiagConfig) ShouldRunDiagServer() bool { + return d.Enabled && (d.IsPrometheusExporterEnabled() || d.Status.Enabled) +} + func (t *TlsConfig) LoadTlsOptions() (*tls.Config, error) { conf := &tls.Config{ MinVersion: t.GetVersion(), diff --git a/config/config_test.go b/config/config_test.go index a016ef0..8771592 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -27,6 +27,10 @@ func TestConfig_Defaults(t *testing.T) { assert.True(t, conf.Diag.Enabled) assert.True(t, conf.Diag.Status.Enabled) assert.True(t, conf.Diag.Metrics.Enabled) + assert.True(t, conf.Diag.Metrics.Prometheus.Enabled) + assert.Equal(t, "http", conf.Diag.Metrics.Otlp.Protocol) + assert.False(t, conf.Diag.Traces.Enabled) + assert.Equal(t, "http", conf.Diag.Traces.Otlp.Protocol) assert.True(t, conf.Http.Sse.Enabled) assert.True(t, conf.Http.Sse.CORS.Enabled) @@ -37,6 +41,9 @@ func TestConfig_Defaults(t *testing.T) { assert.True(t, conf.Http.Api.Enabled) assert.True(t, conf.Http.Api.CORS.Enabled) + assert.True(t, conf.Http.OFREP.Enabled) + assert.True(t, conf.Http.OFREP.CORS.Enabled) + assert.True(t, conf.Http.Webhook.Enabled) assert.False(t, conf.Http.Status.Enabled) @@ -487,6 +494,19 @@ diag: enabled: false metrics: enabled: false + prometheus: + enabled: false + otlp: + enabled: true + endpoint: "http://localhost:4317" + protocol: "grpc" + traces: + enabled: false + otlp: + enabled: true + endpoint: "http://localhost:4317" + protocol: "grpc" + `, func(file string) { conf, err := LoadConfigFromFileAndEnvironment(file) require.NoError(t, err) @@ -495,6 +515,14 @@ diag: assert.Equal(t, 8091, conf.Diag.Port) assert.False(t, conf.Diag.Status.Enabled) assert.False(t, conf.Diag.Metrics.Enabled) + assert.False(t, conf.Diag.Metrics.Prometheus.Enabled) + assert.True(t, conf.Diag.Metrics.Otlp.Enabled) + assert.Equal(t, "grpc", conf.Diag.Metrics.Otlp.Protocol) + assert.Equal(t, "http://localhost:4317", conf.Diag.Metrics.Otlp.Endpoint) + assert.False(t, conf.Diag.Traces.Enabled) + assert.True(t, conf.Diag.Traces.Otlp.Enabled) + assert.Equal(t, "grpc", conf.Diag.Traces.Otlp.Protocol) + assert.Equal(t, "http://localhost:4317", conf.Diag.Traces.Otlp.Endpoint) }) } @@ -838,3 +866,36 @@ MK4Li/LGWcksyoF+hbPNXMFCIA== assert.Nil(t, tlsConf) }) } + +func TestDiagConfig(t *testing.T) { + tests := []struct { + conf *DiagConfig + expM bool + expP bool + expD bool + expS bool + }{ + { + conf: &DiagConfig{Enabled: true}, expM: false, expP: false, expD: false, expS: false, + }, + { + conf: &DiagConfig{Enabled: true, Status: StatusConfig{Enabled: true}}, expM: false, expP: false, expD: true, expS: true, + }, + { + conf: &DiagConfig{Enabled: true, Metrics: MetricsConfig{Prometheus: PrometheusExporterConfig{Enabled: true}}}, expM: false, expP: false, expD: false, expS: false, + }, + { + conf: &DiagConfig{Enabled: true, Metrics: MetricsConfig{Enabled: true, Prometheus: PrometheusExporterConfig{Enabled: true}}}, expM: true, expP: true, expD: true, expS: false, + }, + { + conf: &DiagConfig{Enabled: true, Metrics: MetricsConfig{Enabled: true}}, expM: true, expP: false, expD: false, expS: false, + }, + } + + for _, test := range tests { + assert.Equal(t, test.expM, test.conf.IsMetricsEnabled()) + assert.Equal(t, test.expP, test.conf.IsPrometheusExporterEnabled()) + assert.Equal(t, test.expD, test.conf.ShouldRunDiagServer()) + assert.Equal(t, test.expS, test.conf.IsStatusEnabled()) + } +} diff --git a/config/env.go b/config/env.go index 26ba8af..b809dde 100644 --- a/config/env.go +++ b/config/env.go @@ -424,6 +424,9 @@ func (d *DiagConfig) loadEnv(prefix string) error { if err := d.Metrics.loadEnv(prefix); err != nil { return err } + if err := d.Traces.loadEnv(prefix); err != nil { + return err + } return nil } @@ -432,6 +435,41 @@ func (m *MetricsConfig) loadEnv(prefix string) error { if err := readEnv(prefix, "ENABLED", &m.Enabled, toBool); err != nil { return err } + if err := m.Prometheus.loadEnv(prefix); err != nil { + return err + } + if err := m.Otlp.loadEnv(prefix); err != nil { + return err + } + return nil +} + +func (m *TraceConfig) loadEnv(prefix string) error { + prefix = concatPrefix(prefix, "TRACES") + if err := readEnv(prefix, "ENABLED", &m.Enabled, toBool); err != nil { + return err + } + if err := m.Otlp.loadEnv(prefix); err != nil { + return err + } + return nil +} + +func (p *PrometheusExporterConfig) loadEnv(prefix string) error { + prefix = concatPrefix(prefix, "PROMETHEUS") + if err := readEnv(prefix, "ENABLED", &p.Enabled, toBool); err != nil { + return err + } + return nil +} + +func (o *OtlpExporterConfig) loadEnv(prefix string) error { + prefix = concatPrefix(prefix, "OTLP") + readEnvString(prefix, "PROTOCOL", &o.Protocol) + readEnvString(prefix, "ENDPOINT", &o.Endpoint) + if err := readEnv(prefix, "ENABLED", &o.Enabled, toBool); err != nil { + return err + } return nil } diff --git a/config/env_test.go b/config/env_test.go index 5db522a..98f78a0 100644 --- a/config/env_test.go +++ b/config/env_test.go @@ -206,6 +206,14 @@ func TestDiagConfig_ENV(t *testing.T) { t.Setenv("CONFIGCAT_DIAG_PORT", "8091") t.Setenv("CONFIGCAT_DIAG_METRICS_ENABLED", "false") t.Setenv("CONFIGCAT_DIAG_STATUS_ENABLED", "false") + t.Setenv("CONFIGCAT_DIAG_METRICS_PROMETHEUS_ENABLED", "false") + t.Setenv("CONFIGCAT_DIAG_METRICS_OTLP_ENABLED", "true") + t.Setenv("CONFIGCAT_DIAG_METRICS_OTLP_PROTOCOL", "grpc") + t.Setenv("CONFIGCAT_DIAG_METRICS_OTLP_ENDPOINT", "http://localhost:4317") + t.Setenv("CONFIGCAT_DIAG_TRACES_ENABLED", "false") + t.Setenv("CONFIGCAT_DIAG_TRACES_OTLP_ENABLED", "true") + t.Setenv("CONFIGCAT_DIAG_TRACES_OTLP_PROTOCOL", "grpc") + t.Setenv("CONFIGCAT_DIAG_TRACES_OTLP_ENDPOINT", "http://localhost:4317") conf, err := LoadConfigFromFileAndEnvironment("") require.NoError(t, err) @@ -214,6 +222,14 @@ func TestDiagConfig_ENV(t *testing.T) { assert.Equal(t, 8091, conf.Diag.Port) assert.False(t, conf.Diag.Status.Enabled) assert.False(t, conf.Diag.Metrics.Enabled) + assert.False(t, conf.Diag.Metrics.Prometheus.Enabled) + assert.True(t, conf.Diag.Metrics.Otlp.Enabled) + assert.Equal(t, "grpc", conf.Diag.Metrics.Otlp.Protocol) + assert.Equal(t, "http://localhost:4317", conf.Diag.Metrics.Otlp.Endpoint) + assert.False(t, conf.Diag.Traces.Enabled) + assert.True(t, conf.Diag.Traces.Otlp.Enabled) + assert.Equal(t, "grpc", conf.Diag.Traces.Otlp.Protocol) + assert.Equal(t, "http://localhost:4317", conf.Diag.Traces.Otlp.Endpoint) } func TestGlobalOfflineConfig_ENV(t *testing.T) { diff --git a/config/validate.go b/config/validate.go index 15dfc7b..39e0e55 100644 --- a/config/validate.go +++ b/config/validate.go @@ -198,6 +198,23 @@ func (d *DiagConfig) validate() error { if d.Port < 1 || d.Port > 65535 { return fmt.Errorf("diag: invalid port %d", d.Port) } + if d.IsMetricsEnabled() && d.Metrics.Otlp.Enabled { + if err := d.Metrics.Otlp.validate(); err != nil { + return err + } + } + if d.IsTracesEnabled() && d.Traces.Otlp.Enabled { + if err := d.Traces.Otlp.validate(); err != nil { + return err + } + } + return nil +} + +func (o *OtlpExporterConfig) validate() error { + if o.Protocol != "http" && o.Protocol != "https" && o.Protocol != "grpc" { + return fmt.Errorf("diag: invalid otlp protocol %s (only 'http', 'https', or 'grpc' allowed)", o.Protocol) + } return nil } diff --git a/config/validate_test.go b/config/validate_test.go index a65f618..d21b4f0 100644 --- a/config/validate_test.go +++ b/config/validate_test.go @@ -159,4 +159,14 @@ func TestConfig_Validate(t *testing.T) { conf.setDefaults() require.ErrorContains(t, conf.Validate(), "cors: the 'if no watch' field is required when allowed origins regex is set") }) + t.Run("otlp", func(t *testing.T) { + t.Run("metrics protocol", func(t *testing.T) { + conf := Config{SDKs: map[string]*SDKConfig{"env1": {Key: "Key"}}, Diag: DiagConfig{Port: 80, Enabled: true, Metrics: MetricsConfig{Enabled: true, Otlp: OtlpExporterConfig{Enabled: true, Protocol: "test"}}}, Http: HttpConfig{Port: 80}} + require.ErrorContains(t, conf.Validate(), "diag: invalid otlp protocol test (only 'http', 'https', or 'grpc' allowed)") + }) + t.Run("traces protocol", func(t *testing.T) { + conf := Config{SDKs: map[string]*SDKConfig{"env1": {Key: "Key"}}, Diag: DiagConfig{Port: 80, Enabled: true, Traces: TraceConfig{Enabled: true, Otlp: OtlpExporterConfig{Enabled: true, Protocol: "test"}}}, Http: HttpConfig{Port: 80}} + require.ErrorContains(t, conf.Validate(), "diag: invalid otlp protocol test (only 'http', 'https', or 'grpc' allowed)") + }) + }) } diff --git a/diag/metrics/metrics.go b/diag/metrics/metrics.go deleted file mode 100644 index 71b51d5..0000000 --- a/diag/metrics/metrics.go +++ /dev/null @@ -1,108 +0,0 @@ -package metrics - -import ( - "net/http" - - "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/client_golang/prometheus/collectors" - "github.com/prometheus/client_golang/prometheus/promhttp" -) - -type Reporter interface { - IncrementConnection(sdkId string, streamType string, flag string) - DecrementConnection(sdkId string, streamType string, flag string) - AddSentMessageCount(count int, sdkId string, streamType string, flag string) - - HttpHandler() http.Handler -} - -type reporter struct { - registry *prometheus.Registry - httpResponseTime *prometheus.HistogramVec - grpcResponseTime *prometheus.HistogramVec - sdkResponseTime *prometheus.HistogramVec - profileResponseTime *prometheus.HistogramVec - connections *prometheus.GaugeVec - streamMessageSent *prometheus.CounterVec -} - -func NewReporter() Reporter { - reg := prometheus.NewRegistry() - - httpRespTime := prometheus.NewHistogramVec(prometheus.HistogramOpts{ - Namespace: "configcat", - Name: "http_request_duration_seconds", - Help: "Histogram of Proxy HTTP response time in seconds.", - Buckets: prometheus.DefBuckets, - }, []string{"route", "method", "status"}) - - grpcRespTime := prometheus.NewHistogramVec(prometheus.HistogramOpts{ - Namespace: "configcat", - Name: "grpc_rpc_duration_seconds", - Help: "Histogram of RPC response latency in seconds.", - Buckets: prometheus.DefBuckets, - }, []string{"method", "code"}) - - sdkRespTime := prometheus.NewHistogramVec(prometheus.HistogramOpts{ - Namespace: "configcat", - Name: "sdk_http_request_duration_seconds", - Help: "Histogram of ConfigCat CDN HTTP response time in seconds.", - Buckets: prometheus.DefBuckets, - }, []string{"sdk", "route", "status"}) - - profileResponseTime := prometheus.NewHistogramVec(prometheus.HistogramOpts{ - Namespace: "configcat", - Name: "profile_http_request_duration_seconds", - Help: "Histogram of Proxy profile HTTP response time in seconds.", - Buckets: prometheus.DefBuckets, - }, []string{"key", "route", "status"}) - - connections := prometheus.NewGaugeVec(prometheus.GaugeOpts{ - Namespace: "configcat", - Name: "stream_connections", - Help: "Number of active client connections per stream.", - }, []string{"sdk", "type", "flag"}) - - streamMessageSent := prometheus.NewCounterVec(prometheus.CounterOpts{ - Namespace: "configcat", - Name: "stream_msg_sent_total", - Help: "Total number of stream messages sent by the server.", - }, []string{"sdk", "type", "flag"}) - - reg.MustRegister( - collectors.NewGoCollector(), - collectors.NewProcessCollector(collectors.ProcessCollectorOpts{}), - httpRespTime, - grpcRespTime, - sdkRespTime, - profileResponseTime, - connections, - streamMessageSent, - ) - - return &reporter{ - registry: reg, - httpResponseTime: httpRespTime, - grpcResponseTime: grpcRespTime, - sdkResponseTime: sdkRespTime, - profileResponseTime: profileResponseTime, - connections: connections, - streamMessageSent: streamMessageSent, - } -} - -func (r *reporter) HttpHandler() http.Handler { - return promhttp.HandlerFor(r.registry, promhttp.HandlerOpts{Registry: r.registry}) -} - -func (r *reporter) IncrementConnection(sdkId string, streamType string, flag string) { - r.connections.WithLabelValues(sdkId, streamType, flag).Inc() -} - -func (r *reporter) DecrementConnection(sdkId string, streamType string, flag string) { - r.connections.WithLabelValues(sdkId, streamType, flag).Dec() -} - -func (r *reporter) AddSentMessageCount(count int, sdkId string, streamType string, flag string) { - r.streamMessageSent.WithLabelValues(sdkId, streamType, flag).Add(float64(count)) -} diff --git a/diag/metrics/metrics_test.go b/diag/metrics/metrics_test.go deleted file mode 100644 index a99eb6c..0000000 --- a/diag/metrics/metrics_test.go +++ /dev/null @@ -1,37 +0,0 @@ -package metrics - -import ( - "testing" - - "github.com/prometheus/client_golang/prometheus/testutil" - "github.com/stretchr/testify/assert" -) - -func TestConnection(t *testing.T) { - handler := NewReporter().(*reporter) - - handler.IncrementConnection("test", "t1", "n1") - handler.IncrementConnection("test", "t1", "n1") - handler.IncrementConnection("test", "t2", "n1") - handler.IncrementConnection("test", "t2", "n1") - handler.IncrementConnection("test", "t2", "n1") - - handler.AddSentMessageCount(1, "test", "t1", "n1") - handler.AddSentMessageCount(4, "test", "t2", "n1") - - assert.Equal(t, 2, testutil.CollectAndCount(handler.connections)) - assert.Equal(t, 2, testutil.CollectAndCount(handler.streamMessageSent)) - - assert.Equal(t, float64(2), testutil.ToFloat64(handler.connections.WithLabelValues("test", "t1", "n1"))) - assert.Equal(t, float64(3), testutil.ToFloat64(handler.connections.WithLabelValues("test", "t2", "n1"))) - - assert.Equal(t, float64(1), testutil.ToFloat64(handler.streamMessageSent.WithLabelValues("test", "t1", "n1"))) - assert.Equal(t, float64(4), testutil.ToFloat64(handler.streamMessageSent.WithLabelValues("test", "t2", "n1"))) - - handler.DecrementConnection("test", "t1", "n1") - handler.DecrementConnection("test", "t2", "n1") - handler.DecrementConnection("test", "t2", "n1") - - assert.Equal(t, float64(1), testutil.ToFloat64(handler.connections.WithLabelValues("test", "t1", "n1"))) - assert.Equal(t, float64(1), testutil.ToFloat64(handler.connections.WithLabelValues("test", "t2", "n1"))) -} diff --git a/diag/metrics/mware.go b/diag/metrics/mware.go deleted file mode 100644 index f803b1a..0000000 --- a/diag/metrics/mware.go +++ /dev/null @@ -1,100 +0,0 @@ -package metrics - -import ( - "context" - "net/http" - "strconv" - "time" - - "google.golang.org/grpc" - "google.golang.org/grpc/status" -) - -type httpRequestInterceptor struct { - http.ResponseWriter - - statusCode int -} - -func (r *httpRequestInterceptor) WriteHeader(statusCode int) { - r.statusCode = statusCode - r.ResponseWriter.WriteHeader(statusCode) -} - -func Measure(metricsReporter Reporter, next http.HandlerFunc) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - start := time.Now() - interceptor := httpRequestInterceptor{w, http.StatusOK} - - next(&interceptor, r) - - duration := time.Since(start) - metricsReporter.(*reporter).httpResponseTime.WithLabelValues(r.URL.Path, r.Method, strconv.Itoa(interceptor.statusCode)).Observe(duration.Seconds()) - } -} - -func GrpcUnaryInterceptor(metricsReporter Reporter) grpc.UnaryServerInterceptor { - return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) { - start := time.Now() - - resp, err := handler(ctx, req) - - stat, ok := status.FromError(err) - if !ok { - stat = status.FromContextError(err) - } - duration := time.Since(start) - metricsReporter.(*reporter).grpcResponseTime.WithLabelValues(info.FullMethod, stat.Code().String()).Observe(duration.Seconds()) - return resp, err - } -} - -type clientInterceptor struct { - http.RoundTripper - - metricsHandler Reporter - sdkId string -} - -func InterceptSdk(sdkId string, metricsHandler Reporter, transport http.RoundTripper) http.RoundTripper { - return &clientInterceptor{metricsHandler: metricsHandler, RoundTripper: transport, sdkId: sdkId} -} - -func (i *clientInterceptor) RoundTrip(r *http.Request) (*http.Response, error) { - start := time.Now() - resp, err := i.RoundTripper.RoundTrip(r) - duration := time.Since(start) - var stat string - if err != nil { - stat = err.Error() - } else { - stat = resp.Status - } - i.metricsHandler.(*reporter).sdkResponseTime.WithLabelValues(i.sdkId, r.URL.String(), stat).Observe(duration.Seconds()) - return resp, err -} - -type profileInterceptor struct { - http.RoundTripper - - metricsHandler Reporter - key string -} - -func InterceptProxyProfile(key string, metricsHandler Reporter, transport http.RoundTripper) http.RoundTripper { - return &profileInterceptor{metricsHandler: metricsHandler, RoundTripper: transport, key: key} -} - -func (p *profileInterceptor) RoundTrip(r *http.Request) (*http.Response, error) { - start := time.Now() - resp, err := p.RoundTripper.RoundTrip(r) - duration := time.Since(start) - var stat string - if err != nil { - stat = err.Error() - } else { - stat = resp.Status - } - p.metricsHandler.(*reporter).profileResponseTime.WithLabelValues(p.key, r.URL.String(), stat).Observe(duration.Seconds()) - return resp, err -} diff --git a/diag/metrics/mware_test.go b/diag/metrics/mware_test.go deleted file mode 100644 index f416eab..0000000 --- a/diag/metrics/mware_test.go +++ /dev/null @@ -1,125 +0,0 @@ -package metrics - -import ( - "context" - "io" - "net/http" - "net/http/httptest" - "testing" - - "github.com/prometheus/client_golang/prometheus/testutil" - "github.com/stretchr/testify/assert" - "google.golang.org/grpc" -) - -func TestMeasure(t *testing.T) { - handler := NewReporter().(*reporter) - h := Measure(handler, func(writer http.ResponseWriter, request *http.Request) { - writer.WriteHeader(http.StatusOK) - }) - srv := httptest.NewServer(h) - client := http.Client{} - req, _ := http.NewRequest(http.MethodGet, srv.URL, http.NoBody) - _, _ = client.Do(req) - - assert.Equal(t, 1, testutil.CollectAndCount(handler.httpResponseTime)) - - mSrv := httptest.NewServer(handler.HttpHandler()) - client = http.Client{} - req, _ = http.NewRequest(http.MethodGet, mSrv.URL, http.NoBody) - resp, _ := client.Do(req) - body, _ := io.ReadAll(resp.Body) - _ = resp.Body.Close() - - assert.Contains(t, string(body), "configcat_http_request_duration_seconds_bucket{method=\"GET\",route=\"/\",status=\"200\",le=\"0.005\"} 1") -} - -func TestMeasure_Non_Success(t *testing.T) { - handler := NewReporter().(*reporter) - h := Measure(handler, func(writer http.ResponseWriter, request *http.Request) { - writer.WriteHeader(http.StatusBadRequest) - }) - srv := httptest.NewServer(h) - client := http.Client{} - req, _ := http.NewRequest(http.MethodGet, srv.URL, http.NoBody) - _, _ = client.Do(req) - - assert.Equal(t, 1, testutil.CollectAndCount(handler.httpResponseTime)) - - mSrv := httptest.NewServer(handler.HttpHandler()) - client = http.Client{} - req, _ = http.NewRequest(http.MethodGet, mSrv.URL, http.NoBody) - resp, _ := client.Do(req) - body, _ := io.ReadAll(resp.Body) - _ = resp.Body.Close() - - assert.Contains(t, string(body), "configcat_http_request_duration_seconds_bucket{method=\"GET\",route=\"/\",status=\"400\",le=\"0.005\"} 1") -} - -func TestIntercept(t *testing.T) { - handler := NewReporter().(*reporter) - h := http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { - writer.WriteHeader(http.StatusOK) - }) - srv := httptest.NewServer(h) - client := http.Client{} - client.Transport = InterceptSdk("test", handler, http.DefaultTransport) - req, _ := http.NewRequest(http.MethodGet, srv.URL, http.NoBody) - _, _ = client.Do(req) - - assert.Equal(t, 1, testutil.CollectAndCount(handler.sdkResponseTime)) - - mSrv := httptest.NewServer(handler.HttpHandler()) - client = http.Client{} - req, _ = http.NewRequest(http.MethodGet, mSrv.URL, http.NoBody) - resp, _ := client.Do(req) - body, _ := io.ReadAll(resp.Body) - _ = resp.Body.Close() - - assert.Contains(t, string(body), "configcat_sdk_http_request_duration_seconds_bucket{route=\""+srv.URL+"\",sdk=\"test\",status=\"200 OK\",le=\"0.005\"} 1") -} - -func TestProfileIntercept(t *testing.T) { - handler := NewReporter().(*reporter) - h := http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { - writer.WriteHeader(http.StatusOK) - }) - srv := httptest.NewServer(h) - client := http.Client{} - client.Transport = InterceptProxyProfile("test", handler, http.DefaultTransport) - req, _ := http.NewRequest(http.MethodGet, srv.URL, http.NoBody) - _, _ = client.Do(req) - - assert.Equal(t, 1, testutil.CollectAndCount(handler.profileResponseTime)) - - mSrv := httptest.NewServer(handler.HttpHandler()) - client = http.Client{} - req, _ = http.NewRequest(http.MethodGet, mSrv.URL, http.NoBody) - resp, _ := client.Do(req) - body, _ := io.ReadAll(resp.Body) - _ = resp.Body.Close() - - assert.Contains(t, string(body), "configcat_profile_http_request_duration_seconds_bucket{key=\"test\",route=\""+srv.URL+"\",status=\"200 OK\",le=\"0.005\"} 1") -} - -func TestUnaryInterceptor(t *testing.T) { - handler := func(ctx context.Context, req interface{}) (i interface{}, e error) { - return nil, nil - } - - rep := NewReporter().(*reporter) - i := GrpcUnaryInterceptor(rep) - _, err := i(context.Background(), "test-req", &grpc.UnaryServerInfo{FullMethod: "test-method"}, handler) - - assert.NoError(t, err) - assert.Equal(t, 1, testutil.CollectAndCount(rep.grpcResponseTime)) - - mSrv := httptest.NewServer(rep.HttpHandler()) - client := http.Client{} - req, _ := http.NewRequest(http.MethodGet, mSrv.URL, http.NoBody) - resp, _ := client.Do(req) - body, _ := io.ReadAll(resp.Body) - _ = resp.Body.Close() - - assert.Contains(t, string(body), "configcat_grpc_rpc_duration_seconds_bucket{code=\"OK\",method=\"test-method\",le=\"0.005\"} 1") -} diff --git a/diag/server.go b/diag/server.go index c6f5bfd..de40014 100644 --- a/diag/server.go +++ b/diag/server.go @@ -9,30 +9,27 @@ import ( "time" "github.com/configcat/configcat-proxy/config" - "github.com/configcat/configcat-proxy/diag/metrics" "github.com/configcat/configcat-proxy/diag/status" + "github.com/configcat/configcat-proxy/diag/telemetry" "github.com/configcat/configcat-proxy/log" ) type Server struct { - httpServer *http.Server - log log.Logger - conf *config.DiagConfig - errorChannel chan error - statusReporter status.Reporter - metricsReporter metrics.Reporter + httpServer *http.Server + log log.Logger + conf *config.DiagConfig + errorChannel chan error } -func NewServer(conf *config.DiagConfig, statusReporter status.Reporter, metricsReporter metrics.Reporter, log log.Logger, errorChan chan error) *Server { +func NewServer(conf *config.DiagConfig, telemetryReporter telemetry.Reporter, statusReporter status.Reporter, log log.Logger, errorChan chan error) *Server { diagLog := log.WithPrefix("diag") mux := http.NewServeMux() - if metricsReporter != nil && conf.Metrics.Enabled { - mux.Handle("/metrics", metricsReporter.HttpHandler()) - diagLog.Reportf("metrics enabled, accepting requests on path: /metrics") + if conf.IsMetricsEnabled() && conf.IsPrometheusExporterEnabled() { + mux.Handle("/metrics", telemetryReporter.GetPrometheusHttpHandler()) } - if statusReporter != nil && conf.Status.Enabled { + if conf.IsStatusEnabled() { mux.Handle("/status", statusReporter.HttpHandler()) diagLog.Reportf("status enabled, accepting requests on path: /status") } @@ -40,17 +37,18 @@ func NewServer(conf *config.DiagConfig, statusReporter status.Reporter, metricsR setupDebugEndpoints(mux) httpServer := &http.Server{ - Addr: ":" + strconv.Itoa(conf.Port), - Handler: mux, + Addr: ":" + strconv.Itoa(conf.Port), + Handler: mux, + ReadTimeout: 5 * time.Second, + WriteTimeout: 10 * time.Second, + IdleTimeout: 120 * time.Second, } return &Server{ - log: diagLog, - httpServer: httpServer, - conf: conf, - errorChannel: errorChan, - statusReporter: statusReporter, - metricsReporter: metricsReporter, + log: diagLog, + httpServer: httpServer, + conf: conf, + errorChannel: errorChan, } } @@ -84,5 +82,6 @@ func (s *Server) Shutdown() { if err != nil { s.log.Errorf("shutdown error: %s", err) } + s.log.Reportf("server shutdown complete") } diff --git a/diag/server_test.go b/diag/server_test.go index 1ab0b0a..c45d3da 100644 --- a/diag/server_test.go +++ b/diag/server_test.go @@ -6,8 +6,8 @@ import ( "time" "github.com/configcat/configcat-proxy/config" - "github.com/configcat/configcat-proxy/diag/metrics" "github.com/configcat/configcat-proxy/diag/status" + "github.com/configcat/configcat-proxy/diag/telemetry" "github.com/configcat/configcat-proxy/log" "github.com/stretchr/testify/assert" ) @@ -18,12 +18,12 @@ func TestNewServer(t *testing.T) { Port: 5051, Enabled: true, Status: config.StatusConfig{Enabled: true}, - Metrics: config.MetricsConfig{Enabled: true}, + Metrics: config.MetricsConfig{Enabled: true, Prometheus: config.PrometheusExporterConfig{Enabled: true}}, } reporter := status.NewEmptyReporter() reporter.RegisterSdk("test", &config.SDKConfig{Key: "key"}) - srv := NewServer(&conf, reporter, metrics.NewReporter(), log.NewNullLogger(), errChan) + srv := NewServer(&conf, telemetry.NewReporter(&conf, "0.1.0", log.NewNullLogger()), reporter, log.NewNullLogger(), errChan) srv.Listen() time.Sleep(500 * time.Millisecond) @@ -48,12 +48,12 @@ func TestNewServer_NotEnabled(t *testing.T) { Port: 5052, Enabled: true, Status: config.StatusConfig{Enabled: false}, - Metrics: config.MetricsConfig{Enabled: false}, + Metrics: config.MetricsConfig{Enabled: false, Prometheus: config.PrometheusExporterConfig{Enabled: true}}, } reporter := status.NewEmptyReporter() reporter.RegisterSdk("test", &config.SDKConfig{Key: "key"}) - srv := NewServer(&conf, reporter, metrics.NewReporter(), log.NewNullLogger(), errChan) + srv := NewServer(&conf, telemetry.NewReporter(&conf, "0.1.0", log.NewNullLogger()), reporter, log.NewNullLogger(), errChan) srv.Listen() time.Sleep(500 * time.Millisecond) @@ -72,33 +72,6 @@ func TestNewServer_NotEnabled(t *testing.T) { assert.Nil(t, readFromErrChan(errChan)) } -func TestNewServer_NilReporters(t *testing.T) { - errChan := make(chan error) - conf := config.DiagConfig{ - Port: 5053, - Enabled: true, - Status: config.StatusConfig{Enabled: true}, - Metrics: config.MetricsConfig{Enabled: true}, - } - srv := NewServer(&conf, nil, nil, log.NewNullLogger(), errChan) - srv.Listen() - time.Sleep(500 * time.Millisecond) - - req, _ := http.NewRequest(http.MethodGet, "http://localhost:5053/status", http.NoBody) - resp, err := http.DefaultClient.Do(req) - assert.NoError(t, err) - assert.Equal(t, http.StatusNotFound, resp.StatusCode) - - req, _ = http.NewRequest(http.MethodGet, "http://localhost:5053/metrics", http.NoBody) - resp, err = http.DefaultClient.Do(req) - assert.NoError(t, err) - assert.Equal(t, http.StatusNotFound, resp.StatusCode) - - srv.Shutdown() - - assert.Nil(t, readFromErrChan(errChan)) -} - func readFromErrChan(ch chan error) error { select { case val, ok := <-ch: diff --git a/diag/telemetry/metrics.go b/diag/telemetry/metrics.go new file mode 100644 index 0000000..9afc9ee --- /dev/null +++ b/diag/telemetry/metrics.go @@ -0,0 +1,159 @@ +package telemetry + +import ( + "context" + "time" + + "github.com/configcat/configcat-proxy/config" + "github.com/configcat/configcat-proxy/log" + "github.com/prometheus/otlptranslator" + otelhost "go.opentelemetry.io/contrib/instrumentation/host" + otelruntime "go.opentelemetry.io/contrib/instrumentation/runtime" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc" + "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp" + promexporter "go.opentelemetry.io/otel/exporters/prometheus" + otelmetric "go.opentelemetry.io/otel/metric" + "go.opentelemetry.io/otel/sdk/metric" + "go.opentelemetry.io/otel/sdk/resource" +) + +type metricsHandler struct { + connections otelmetric.Int64Gauge + streamMessageSent otelmetric.Int64Counter + provider *metric.MeterProvider + log log.Logger + + ctx context.Context + ctxCancel func() +} + +const ( + meterName = "github.com/configcat/configcat-proxy" +) + +func newMetricsHandler(ctx context.Context, resource *resource.Resource, conf *config.MetricsConfig, log log.Logger) *metricsHandler { + if !conf.Prometheus.Enabled && !conf.Otlp.Enabled { + return nil + } + logger := log.WithPrefix("metrics") + providerOpts := []metric.Option{metric.WithResource(resource)} + if conf.Prometheus.Enabled { + exporter, err := promexporter.New( + promexporter.WithNamespace("configcat"), + promexporter.WithTranslationStrategy(otlptranslator.UnderscoreEscapingWithSuffixes)) + if err != nil { + logger.Errorf("failed to configure Prometheus exporter: %s", err) + return nil + } + providerOpts = append(providerOpts, metric.WithReader(exporter)) + logger.Reportf("prometheus exporter enabled on /metrics") + } + if conf.Otlp.Enabled { + switch conf.Otlp.Protocol { + case "grpc": + var opts []otlpmetricgrpc.Option + if conf.Otlp.Endpoint != "" { + opts = append(opts, otlpmetricgrpc.WithEndpoint(conf.Otlp.Endpoint)) + } + opts = append(opts, otlpmetricgrpc.WithInsecure()) + r, err := otlpmetricgrpc.New(ctx, opts...) + if err != nil { + logger.Errorf("failed to configure OTLP gRPC exporter: %s", err) + return nil + } + providerOpts = append(providerOpts, metric.WithReader(metric.NewPeriodicReader(r))) + case "http": + fallthrough + case "https": + var opts []otlpmetrichttp.Option + if conf.Otlp.Endpoint != "" { + opts = append(opts, otlpmetrichttp.WithEndpoint(conf.Otlp.Endpoint)) + } + if conf.Otlp.Protocol == "http" { + opts = append(opts, otlpmetrichttp.WithInsecure()) + } + r, err := otlpmetrichttp.New(ctx, opts...) + if err != nil { + logger.Errorf("failed to configure OTLP HTTP exporter: %s", err) + return nil + } + providerOpts = append(providerOpts, metric.WithReader(metric.NewPeriodicReader(r))) + } + var ep string + if conf.Otlp.Endpoint != "" { + ep = " to " + conf.Otlp.Endpoint + "" + } + logger.Reportf("otlp exporter enabled over %s%s", conf.Otlp.Protocol, ep) + } + return newMetricsHandlerWithOpts(providerOpts, logger) +} + +func newMetricsHandlerWithOpts(opts []metric.Option, logger log.Logger) *metricsHandler { + provider := metric.NewMeterProvider(opts...) + meter := provider.Meter(meterName) + + err := otelruntime.Start(otelruntime.WithMeterProvider(provider)) + if err != nil { + logger.Errorf("failed to start runtime metrics: %s", err) + } + err = otelhost.Start(otelhost.WithMeterProvider(provider)) + if err != nil { + logger.Errorf("failed to start host metrics: %s", err) + } + + connections, err := meter.Int64Gauge("stream.connections", + otelmetric.WithDescription("Number of active client connections per stream.")) + if err != nil { + logger.Errorf("failed to configure connections gauge: %s", err) + return nil + } + + streamMessageSent, err := meter.Int64Counter("stream.msg.sent.total", + otelmetric.WithDescription("Total number of stream messages sent by the server.")) + if err != nil { + logger.Errorf("failed to configure stream message sent counter: %s", err) + return nil + } + + ctx, ctxCancel := context.WithCancel(context.Background()) + + return &metricsHandler{ + connections: connections, + streamMessageSent: streamMessageSent, + provider: provider, + log: logger, + ctx: ctx, + ctxCancel: ctxCancel, + } +} + +func (r *metricsHandler) recordConnections(count int64, sdkId string, streamType string, flag string) { + r.connections.Record(r.ctx, count, otelmetric.WithAttributes( + attribute.Key("sdk").String(sdkId), + attribute.Key("type").String(streamType), + attribute.Key("flag").String(flag), + )) +} + +func (r *metricsHandler) addSentMessageCount(count int, sdkId string, streamType string, flag string) { + r.streamMessageSent.Add(r.ctx, int64(count), otelmetric.WithAttributes( + attribute.Key("sdk").String(sdkId), + attribute.Key("type").String(streamType), + attribute.Key("flag").String(flag), + )) +} + +func (r *metricsHandler) shutdown() { + r.log.Reportf("initiating server shutdown") + r.ctxCancel() + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + err := r.provider.Shutdown(ctx) + if err != nil { + r.log.Errorf("shutdown error: %s", err) + } + r.log.Reportf("server shutdown complete") +} diff --git a/diag/telemetry/metrics_test.go b/diag/telemetry/metrics_test.go new file mode 100644 index 0000000..70ee7bb --- /dev/null +++ b/diag/telemetry/metrics_test.go @@ -0,0 +1,225 @@ +package telemetry + +import ( + "context" + "net/http/httptest" + "testing" + + "github.com/configcat/configcat-proxy/config" + "github.com/configcat/configcat-proxy/log" + "github.com/stretchr/testify/assert" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/sdk/metric" + "go.opentelemetry.io/otel/sdk/metric/metricdata" + "go.opentelemetry.io/otel/sdk/metric/metricdata/metricdatatest" + otlpmpb "go.opentelemetry.io/proto/otlp/collector/metrics/v1" + mpb "go.opentelemetry.io/proto/otlp/metrics/v1" +) + +func TestConnection(t *testing.T) { + reader := metric.NewManualReader() + handler := newMetricsHandlerWithOpts([]metric.Option{metric.WithReader(reader)}, log.NewNullLogger()) + defer handler.shutdown() + + handler.recordConnections(1, "test", "t1", "n1") + handler.recordConnections(2, "test", "t1", "n1") + handler.recordConnections(1, "test", "t2", "n1") + + handler.addSentMessageCount(1, "test", "t1", "n1") + handler.addSentMessageCount(4, "test", "t2", "n1") + + rm := metricdata.ResourceMetrics{} + err := reader.Collect(t.Context(), &rm) + assert.NoError(t, err) + + var sm metricdata.ScopeMetrics + for _, s := range rm.ScopeMetrics { + if s.Scope.Name == meterName { + sm = s + } + } + + m1 := sm.Metrics[0] + m2 := sm.Metrics[1] + + assert.Equal(t, "stream.connections", m1.Name) + assert.Equal(t, "stream.msg.sent.total", m2.Name) + + metricdatatest.AssertEqual(t, metricdata.Metrics{ + Name: "stream.connections", + Description: "Number of active client connections per stream.", + Data: metricdata.Gauge[int64]{ + DataPoints: []metricdata.DataPoint[int64]{ + { + Value: 2, + Attributes: attribute.NewSet(attribute.Key("sdk").String("test"), + attribute.Key("type").String("t1"), + attribute.Key("flag").String("n1")), + }, + { + Value: 1, + Attributes: attribute.NewSet(attribute.Key("sdk").String("test"), + attribute.Key("type").String("t2"), + attribute.Key("flag").String("n1")), + }, + }, + }}, m1, metricdatatest.IgnoreTimestamp()) + + metricdatatest.AssertEqual(t, metricdata.Metrics{ + Name: "stream.msg.sent.total", + Description: "Total number of stream messages sent by the server.", + Data: metricdata.Sum[int64]{ + Temporality: metricdata.CumulativeTemporality, + IsMonotonic: true, + DataPoints: []metricdata.DataPoint[int64]{ + { + Value: 1, + Attributes: attribute.NewSet(attribute.Key("sdk").String("test"), + attribute.Key("type").String("t1"), + attribute.Key("flag").String("n1")), + }, + { + Value: 4, + Attributes: attribute.NewSet(attribute.Key("sdk").String("test"), + attribute.Key("type").String("t2"), + attribute.Key("flag").String("n1")), + }, + }, + }}, m2, metricdatatest.IgnoreTimestamp()) + + handler.recordConnections(1, "test", "t1", "n1") + + rm = metricdata.ResourceMetrics{} + err = reader.Collect(t.Context(), &rm) + assert.NoError(t, err) + + for _, s := range rm.ScopeMetrics { + if s.Scope.Name == meterName { + sm = s + } + } + + m1 = sm.Metrics[0] + + metricdatatest.AssertEqual(t, metricdata.Metrics{ + Name: "stream.connections", + Description: "Number of active client connections per stream.", + Data: metricdata.Gauge[int64]{ + DataPoints: []metricdata.DataPoint[int64]{ + { + Value: 1, + Attributes: attribute.NewSet(attribute.Key("sdk").String("test"), + attribute.Key("type").String("t1"), + attribute.Key("flag").String("n1")), + }, + { + Value: 1, + Attributes: attribute.NewSet(attribute.Key("sdk").String("test"), + attribute.Key("type").String("t2"), + attribute.Key("flag").String("n1")), + }, + }, + }}, m1, metricdatatest.IgnoreTimestamp()) +} + +func TestOtlpMetricsExporterGrpc(t *testing.T) { + collector, err := newInMemoryMetricGrpcCollector() + assert.NoError(t, err) + defer collector.Shutdown() + + handler := newMetricsHandler(t.Context(), buildResource("0.1.0"), + &config.MetricsConfig{Enabled: true, Otlp: config.OtlpExporterConfig{Enabled: true, Protocol: "grpc", Endpoint: collector.Addr()}}, log.NewNullLogger()) + assert.NotNil(t, handler) + + handler.recordConnections(1, "test", "t1", "n1") + _ = handler.provider.ForceFlush(t.Context()) + + assert.True(t, collector.hasMetric("stream.connections")) +} + +func TestOtlpMetricsExporterHttp(t *testing.T) { + collector := newInMemoryMetricHttpCollector() + defer collector.Shutdown() + + handler := newMetricsHandler(t.Context(), buildResource("0.1.0"), + &config.MetricsConfig{Enabled: true, Otlp: config.OtlpExporterConfig{Enabled: true, Protocol: "http", Endpoint: collector.Addr()}}, log.NewNullLogger()) + assert.NotNil(t, handler) + + handler.recordConnections(1, "test", "t1", "n1") + _ = handler.provider.ForceFlush(t.Context()) + + assert.True(t, hasMetric(collector, "stream.connections")) +} + +type inMemoryMetricGrpcCollector struct { + otlpmpb.UnimplementedMetricsServiceServer + *grpcCollector + + metrics []*mpb.ResourceMetrics +} + +func newInMemoryMetricGrpcCollector() (*inMemoryMetricGrpcCollector, error) { + gc, err := newGrpcCollector() + if err != nil { + return nil, err + } + c := &inMemoryMetricGrpcCollector{ + grpcCollector: gc, + metrics: make([]*mpb.ResourceMetrics, 0), + } + otlpmpb.RegisterMetricsServiceServer(c.srv, c) + go func() { _ = c.srv.Serve(c.listener) }() + + return c, nil +} + +func (c *inMemoryMetricGrpcCollector) Export(_ context.Context, req *otlpmpb.ExportMetricsServiceRequest) (*otlpmpb.ExportMetricsServiceResponse, error) { + c.mu.Lock() + defer c.mu.Unlock() + + c.metrics = append(c.metrics, req.ResourceMetrics...) + + return &otlpmpb.ExportMetricsServiceResponse{}, nil +} + +func (c *inMemoryMetricGrpcCollector) hasMetric(name string) bool { + c.mu.RLock() + defer c.mu.RUnlock() + + for _, t := range c.metrics { + for _, span := range t.ScopeMetrics { + for _, s := range span.Metrics { + if s.Name == name { + return true + } + } + } + } + return false +} + +func newInMemoryMetricHttpCollector() *inMemoryHttpCollector[*otlpmpb.ExportMetricsServiceRequest] { + c := &inMemoryHttpCollector[*otlpmpb.ExportMetricsServiceRequest]{ + records: make([]*otlpmpb.ExportMetricsServiceRequest, 0), + } + c.srv = httptest.NewServer(c) + return c +} + +func hasMetric(c *inMemoryHttpCollector[*otlpmpb.ExportMetricsServiceRequest], name string) bool { + c.mu.RLock() + defer c.mu.RUnlock() + + for _, r := range c.records { + for _, t := range r.ResourceMetrics { + for _, span := range t.ScopeMetrics { + for _, s := range span.Metrics { + if s.Name == name { + return true + } + } + } + } + } + return false +} diff --git a/diag/telemetry/reporter.go b/diag/telemetry/reporter.go new file mode 100644 index 0000000..905dd1f --- /dev/null +++ b/diag/telemetry/reporter.go @@ -0,0 +1,255 @@ +package telemetry + +import ( + "context" + "net/http" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/configcat/configcat-proxy/config" + "github.com/configcat/configcat-proxy/log" + "github.com/prometheus/client_golang/prometheus/promhttp" + "github.com/redis/go-redis/extra/redisotel/v9" + "github.com/redis/go-redis/v9" + "go.mongodb.org/mongo-driver/v2/mongo/options" + "go.opentelemetry.io/contrib/instrumentation/github.com/aws/aws-sdk-go-v2/otelaws" + "go.opentelemetry.io/contrib/instrumentation/go.mongodb.org/mongo-driver/v2/mongo/otelmongo" + "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/sdk/resource" + semconv "go.opentelemetry.io/otel/semconv/v1.37.0" + "go.opentelemetry.io/otel/trace" + "go.opentelemetry.io/otel/trace/noop" + "google.golang.org/grpc" +) + +type K string +type V string + +type KV struct { + Key K + Value V +} + +func (k K) V(val string) KV { + return KV{ + Key: k, + Value: V(val), + } +} + +type HttpClientType int + +type Reporter interface { + GetPrometheusHttpHandler() http.Handler + + RecordConnections(count int64, sdkId string, streamType string, flag string) + AddSentMessageCount(count int, sdkId string, streamType string, flag string) + + StartSpan(ctx context.Context, name string, attributes ...KV) (context.Context, trace.Span) + ForceFlush(ctx context.Context) + + InstrumentHttp(operation string, method string, handler http.HandlerFunc) http.HandlerFunc + InstrumentHttpClient(handler http.RoundTripper, attributes ...KV) http.RoundTripper + InstrumentGrpc(opts []grpc.ServerOption) []grpc.ServerOption + + InstrumentRedis(rdb redis.UniversalClient) + InstrumentMongoDb(opts *options.ClientOptions) + InstrumentAws(opts *aws.Config) + + Shutdown() +} + +const ( + traceName = "github.com/configcat/configcat-proxy" +) + +type reporter struct { + conf *config.DiagConfig + metricsHandler *metricsHandler + traceHandler *traceHandler + tracer trace.Tracer + log log.Logger +} + +func NewReporter(conf *config.DiagConfig, version string, log log.Logger) Reporter { + logger := log.WithPrefix("telemetry") + res := buildResource(version) + + var mh *metricsHandler + var th *traceHandler + var tracer trace.Tracer + if conf.IsMetricsEnabled() { + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) + defer cancel() + + mh = newMetricsHandler(ctx, res, &conf.Metrics, logger) + } + if conf.IsTracesEnabled() { + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) + defer cancel() + + th = newTraceHandler(ctx, res, &conf.Traces, logger) + if th != nil { + tracer = th.provider.Tracer(traceName) + } + } + + return &reporter{ + conf: conf, + metricsHandler: mh, + traceHandler: th, + tracer: tracer, + log: logger, + } +} + +func NewEmptyReporter() Reporter { + return &reporter{conf: &config.DiagConfig{}} +} + +func (r *reporter) GetPrometheusHttpHandler() http.Handler { + return promhttp.Handler() +} + +func (r *reporter) ForceFlush(ctx context.Context) { + if r.metricsHandler != nil { + err := r.metricsHandler.provider.ForceFlush(ctx) + if err != nil { + r.log.Errorf("failed to force flush metrics: %v", err) + } + } + if r.traceHandler != nil { + err := r.traceHandler.provider.ForceFlush(ctx) + if err != nil { + r.log.Errorf("failed to force flush traces: %v", err) + } + } +} + +func (r *reporter) RecordConnections(count int64, sdkId string, streamType string, flag string) { + if r.metricsHandler == nil { + return + } + r.metricsHandler.recordConnections(count, sdkId, streamType, flag) +} + +func (r *reporter) AddSentMessageCount(count int, sdkId string, streamType string, flag string) { + if r.metricsHandler == nil { + return + } + r.metricsHandler.addSentMessageCount(count, sdkId, streamType, flag) +} + +func (r *reporter) StartSpan(ctx context.Context, name string, attributes ...KV) (context.Context, trace.Span) { + if r.tracer == nil { + return noop.NewTracerProvider().Tracer("noop").Start(ctx, "noop", trace.WithAttributes(toAttributeArray(attributes...)...)) + } + return r.tracer.Start(ctx, name, trace.WithAttributes(toAttributeArray(attributes...)...), trace.WithSpanKind(trace.SpanKindInternal)) +} + +func (r *reporter) InstrumentHttp(operation string, method string, handler http.HandlerFunc) http.HandlerFunc { + var otelOpts []otelhttp.Option + if r.metricsHandler != nil { + otelOpts = append(otelOpts, otelhttp.WithMeterProvider(r.metricsHandler.provider), otelhttp.WithMetricAttributesFn(func(r *http.Request) []attribute.KeyValue { + return []attribute.KeyValue{semconv.HTTPRoute(r.URL.Path)} + })) + } + if r.traceHandler != nil { + otelOpts = append(otelOpts, otelhttp.WithTracerProvider(r.traceHandler.provider)) + } + if len(otelOpts) > 0 { + return otelhttp.NewHandler(handler, "HTTP "+method+" "+operation, otelOpts...).ServeHTTP + } + return handler +} + +func (r *reporter) InstrumentHttpClient(handler http.RoundTripper, attributes ...KV) http.RoundTripper { + var otelOpts []otelhttp.Option + if r.metricsHandler != nil { + otelOpts = append(otelOpts, otelhttp.WithMeterProvider(r.metricsHandler.provider)) + if len(attributes) > 0 { + arr := toAttributeArray(attributes...) + otelOpts = append(otelOpts, otelhttp.WithMetricAttributesFn(func(r *http.Request) []attribute.KeyValue { + return append(arr, semconv.HTTPRoute(r.URL.Path)) + })) + } + } + if r.traceHandler != nil { + otelOpts = append(otelOpts, otelhttp.WithTracerProvider(r.traceHandler.provider)) + } + if len(otelOpts) > 0 { + return otelhttp.NewTransport(handler, otelOpts...) + } + return handler +} + +func (r *reporter) InstrumentGrpc(opts []grpc.ServerOption) []grpc.ServerOption { + var otelOpts []otelgrpc.Option + if r.metricsHandler != nil { + otelOpts = append(otelOpts, otelgrpc.WithMeterProvider(r.metricsHandler.provider)) + } + if r.traceHandler != nil { + otelOpts = append(otelOpts, otelgrpc.WithTracerProvider(r.traceHandler.provider)) + } + if len(otelOpts) > 0 { + return append(opts, grpc.StatsHandler(otelgrpc.NewServerHandler(otelOpts...))) + } + return opts +} + +func (r *reporter) InstrumentRedis(rdb redis.UniversalClient) { + if r.metricsHandler != nil { + err := redisotel.InstrumentMetrics(rdb, redisotel.WithMeterProvider(r.metricsHandler.provider)) + if err != nil { + r.log.Errorf("failed to instrument redis: %v", err) + } + } + if r.traceHandler != nil { + err := redisotel.InstrumentTracing(rdb, redisotel.WithTracerProvider(r.traceHandler.provider)) + if err != nil { + r.log.Errorf("failed to instrument redis: %v", err) + } + } +} + +func (r *reporter) InstrumentMongoDb(opts *options.ClientOptions) { + if r.traceHandler != nil { + opts.Monitor = otelmongo.NewMonitor(otelmongo.WithTracerProvider(r.traceHandler.provider)) + } +} + +func (r *reporter) InstrumentAws(opts *aws.Config) { + if r.traceHandler != nil { + otelaws.AppendMiddlewares(&opts.APIOptions, otelaws.WithTracerProvider(r.traceHandler.provider)) + } +} + +func (r *reporter) Shutdown() { + if r.metricsHandler != nil { + r.metricsHandler.shutdown() + } + if r.traceHandler != nil { + r.traceHandler.Shutdown() + } +} + +func buildResource(version string) *resource.Resource { + res, _ := resource.Merge( + resource.Default(), + resource.NewWithAttributes( + semconv.SchemaURL, + semconv.ServiceName("configcat-proxy"), + semconv.ServiceVersion(version), + )) + return res +} + +func toAttributeArray(attributes ...KV) []attribute.KeyValue { + var result []attribute.KeyValue + for _, attr := range attributes { + result = append(result, attribute.String(string(attr.Key), string(attr.Value))) + } + return result +} diff --git a/diag/telemetry/reporter_test.go b/diag/telemetry/reporter_test.go new file mode 100644 index 0000000..ef07ba6 --- /dev/null +++ b/diag/telemetry/reporter_test.go @@ -0,0 +1,331 @@ +package telemetry + +import ( + "io" + "net/http" + "net/http/httptest" + "testing" + + "github.com/alicebob/miniredis/v2" + "github.com/aws/aws-sdk-go-v2/aws" + awsconfig "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/service/dynamodb" + "github.com/configcat/configcat-proxy/config" + "github.com/configcat/configcat-proxy/log" + "github.com/redis/go-redis/v9" + "github.com/stretchr/testify/assert" + "github.com/testcontainers/testcontainers-go" + tcdynamodb "github.com/testcontainers/testcontainers-go/modules/dynamodb" + "github.com/testcontainers/testcontainers-go/modules/mongodb" + "go.mongodb.org/mongo-driver/v2/mongo" + "go.mongodb.org/mongo-driver/v2/mongo/options" + "go.opentelemetry.io/otel/trace/noop" + "google.golang.org/grpc" +) + +func TestHandler_Metrics_Prometheus_Export(t *testing.T) { + conf := config.DiagConfig{ + Port: 5052, + Enabled: true, + Metrics: config.MetricsConfig{Enabled: true, Prometheus: config.PrometheusExporterConfig{Enabled: true}}, + } + + handler := NewReporter(&conf, "0.1.0", log.NewNullLogger()).(*reporter) + defer handler.Shutdown() + + mSrv := httptest.NewServer(handler.GetPrometheusHttpHandler()) + defer mSrv.Close() + + t.Run("http", func(t *testing.T) { + h := handler.InstrumentHttp("t", http.MethodGet, func(writer http.ResponseWriter, request *http.Request) { + writer.WriteHeader(http.StatusOK) + }) + srv := httptest.NewServer(h) + defer srv.Close() + req, _ := http.NewRequest(http.MethodGet, srv.URL, http.NoBody) + _, _ = http.DefaultClient.Do(req) + + req, _ = http.NewRequest(http.MethodGet, mSrv.URL, http.NoBody) + resp, _ := http.DefaultClient.Do(req) + body, _ := io.ReadAll(resp.Body) + _ = resp.Body.Close() + + assert.Contains(t, string(body), "configcat_http_server_request_duration_seconds_bucket{http_request_method=\"GET\",http_response_status_code=\"200\",http_route=\"/\",network_protocol_name=\"http\",network_protocol_version=\"1.1\"") + }) + t.Run("http bad", func(t *testing.T) { + h := handler.InstrumentHttp("t", http.MethodGet, func(writer http.ResponseWriter, request *http.Request) { + writer.WriteHeader(http.StatusBadRequest) + }) + srv := httptest.NewServer(h) + defer srv.Close() + req, _ := http.NewRequest(http.MethodGet, srv.URL, http.NoBody) + _, _ = http.DefaultClient.Do(req) + + req, _ = http.NewRequest(http.MethodGet, mSrv.URL, http.NoBody) + resp, _ := http.DefaultClient.Do(req) + body, _ := io.ReadAll(resp.Body) + _ = resp.Body.Close() + + assert.Contains(t, string(body), "configcat_http_server_request_duration_seconds_bucket{http_request_method=\"GET\",http_response_status_code=\"400\",http_route=\"/\",network_protocol_name=\"http\",network_protocol_version=\"1.1\"") + }) + t.Run("http client", func(t *testing.T) { + h := http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { + writer.WriteHeader(http.StatusOK) + }) + srv := httptest.NewServer(h) + defer srv.Close() + client := http.Client{} + client.Transport = handler.InstrumentHttpClient(http.DefaultTransport, K("sdk").V("test")) + req, _ := http.NewRequest(http.MethodGet, srv.URL, http.NoBody) + _, _ = client.Do(req) + + req, _ = http.NewRequest(http.MethodGet, mSrv.URL, http.NoBody) + resp, _ := http.DefaultClient.Do(req) + body, _ := io.ReadAll(resp.Body) + _ = resp.Body.Close() + + assert.Contains(t, string(body), "configcat_http_client_request_duration_seconds_bucket{http_request_method=\"GET\",http_response_status_code=\"200\",http_route=\"\",network_protocol_name=\"http\"") + }) + t.Run("grpc", func(t *testing.T) { + opts := handler.InstrumentGrpc([]grpc.ServerOption{}) + assert.Equal(t, 1, len(opts)) + }) + t.Run("conn", func(t *testing.T) { + handler.RecordConnections(5, "sdk", "grpc", "flag") + + req, _ := http.NewRequest(http.MethodGet, mSrv.URL, http.NoBody) + resp, _ := http.DefaultClient.Do(req) + body, _ := io.ReadAll(resp.Body) + _ = resp.Body.Close() + + assert.Contains(t, string(body), "configcat_stream_connections{flag=\"flag\"") + }) + t.Run("sent msgs", func(t *testing.T) { + handler.AddSentMessageCount(5, "sdk", "grpc", "flag") + + req, _ := http.NewRequest(http.MethodGet, mSrv.URL, http.NoBody) + resp, _ := http.DefaultClient.Do(req) + body, _ := io.ReadAll(resp.Body) + _ = resp.Body.Close() + + assert.Contains(t, string(body), "configcat_stream_msg_sent_total{flag=\"flag\"") + }) + t.Run("redis", func(t *testing.T) { + r := miniredis.RunT(t) + opts := &redis.UniversalOptions{Addrs: []string{r.Addr()}} + rdb := redis.NewUniversalClient(opts) + handler.InstrumentRedis(rdb) + + req, _ := http.NewRequest(http.MethodGet, mSrv.URL, http.NoBody) + resp, _ := http.DefaultClient.Do(req) + body, _ := io.ReadAll(resp.Body) + _ = resp.Body.Close() + + assert.Contains(t, string(body), "configcat_db_client_connections_waits{db_system=\"redis\"") + }) +} + +func TestHandler_Metrics_Otlp_Export(t *testing.T) { + collector, err := newInMemoryMetricGrpcCollector() + assert.NoError(t, err) + defer collector.Shutdown() + conf := config.DiagConfig{ + Port: 5052, + Enabled: true, + Metrics: config.MetricsConfig{Enabled: true, Otlp: config.OtlpExporterConfig{Enabled: true, Protocol: "grpc", Endpoint: collector.Addr()}}, + } + + handler := NewReporter(&conf, "0.1.0", log.NewNullLogger()).(*reporter) + defer handler.Shutdown() + + t.Run("http", func(t *testing.T) { + h := handler.InstrumentHttp("t", http.MethodGet, func(writer http.ResponseWriter, request *http.Request) { + writer.WriteHeader(http.StatusOK) + }) + srv := httptest.NewServer(h) + defer srv.Close() + req, _ := http.NewRequest(http.MethodGet, srv.URL, http.NoBody) + _, _ = http.DefaultClient.Do(req) + + handler.ForceFlush(t.Context()) + + assert.True(t, collector.hasMetric("http.server.request.body.size")) + }) + t.Run("http client", func(t *testing.T) { + h := http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { + writer.WriteHeader(http.StatusOK) + }) + srv := httptest.NewServer(h) + defer srv.Close() + client := http.Client{} + client.Transport = handler.InstrumentHttpClient(http.DefaultTransport, K("sdk").V("test")) + req, _ := http.NewRequest(http.MethodGet, srv.URL, http.NoBody) + _, _ = client.Do(req) + + handler.ForceFlush(t.Context()) + + assert.True(t, collector.hasMetric("http.client.request.body.size")) + }) + t.Run("conn", func(t *testing.T) { + handler.RecordConnections(5, "sdk", "grpc", "flag") + + handler.ForceFlush(t.Context()) + + assert.True(t, collector.hasMetric("stream.connections")) + }) + t.Run("sent msgs", func(t *testing.T) { + handler.AddSentMessageCount(5, "sdk", "grpc", "flag") + + handler.ForceFlush(t.Context()) + + assert.True(t, collector.hasMetric("stream.msg.sent.total")) + }) + t.Run("redis", func(t *testing.T) { + r := miniredis.RunT(t) + opts := &redis.UniversalOptions{Addrs: []string{r.Addr()}} + rdb := redis.NewUniversalClient(opts) + defer func() { _ = rdb.Close() }() + handler.InstrumentRedis(rdb) + + handler.ForceFlush(t.Context()) + + assert.True(t, collector.hasMetric("db.client.connections.idle.max")) + }) +} + +func TestHandler_Traces_Otlp_Export(t *testing.T) { + collector, err := newInMemoryTraceGrpcCollector() + assert.NoError(t, err) + defer collector.Shutdown() + assert.NoError(t, err) + conf := config.DiagConfig{ + Port: 5052, + Enabled: true, + Traces: config.TraceConfig{Enabled: true, Otlp: config.OtlpExporterConfig{Enabled: true, Protocol: "grpc", Endpoint: collector.Addr()}}, + } + + handler := NewReporter(&conf, "0.1.0", log.NewDebugLogger()) + defer handler.Shutdown() + + t.Run("http", func(t *testing.T) { + h := handler.InstrumentHttp("test", http.MethodGet, func(writer http.ResponseWriter, request *http.Request) { + writer.WriteHeader(http.StatusOK) + }) + srv := httptest.NewServer(h) + defer srv.Close() + req, _ := http.NewRequest(http.MethodGet, srv.URL, http.NoBody) + _, _ = http.DefaultClient.Do(req) + + handler.ForceFlush(t.Context()) + + assert.True(t, collector.hasTrace("HTTP GET test")) + }) + t.Run("http client", func(t *testing.T) { + h := http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { + writer.WriteHeader(http.StatusOK) + }) + srv := httptest.NewServer(h) + defer srv.Close() + client := http.Client{} + client.Transport = handler.InstrumentHttpClient(http.DefaultTransport) + ctx, span := handler.StartSpan(t.Context(), "test") + req, _ := http.NewRequestWithContext(ctx, http.MethodGet, srv.URL, http.NoBody) + _, _ = client.Do(req) + span.End() + + handler.ForceFlush(t.Context()) + + assert.True(t, collector.hasTrace("test")) + }) + t.Run("redis", func(t *testing.T) { + r := miniredis.RunT(t) + opts := &redis.UniversalOptions{Addrs: []string{r.Addr()}} + rdb := redis.NewUniversalClient(opts) + defer func() { _ = rdb.Close() }() + handler.InstrumentRedis(rdb) + rdb.Set(t.Context(), "test", "test", 0) + + handler.ForceFlush(t.Context()) + + assert.True(t, collector.hasTrace("redis.dial")) + }) + t.Run("mongo", func(t *testing.T) { + mongodbContainer, err := mongodb.Run(t.Context(), "mongo") + if err != nil { + panic("failed to start container: " + err.Error() + "") + } + defer func() { + if err := testcontainers.TerminateContainer(mongodbContainer); err != nil { + panic("failed to terminate container: " + err.Error() + "") + } + }() + str, _ := mongodbContainer.ConnectionString(t.Context()) + opts := options.Client().ApplyURI(str) + handler.InstrumentMongoDb(opts) + client, _ := mongo.Connect(opts) + collection := client.Database("db").Collection("coll") + _, _ = collection.InsertOne(t.Context(), map[string]interface{}{"test": "test"}) + + handler.ForceFlush(t.Context()) + + assert.True(t, collector.hasTrace("coll.insert")) + }) + t.Run("dynamodb", func(t *testing.T) { + dynamoDbContainer, err := tcdynamodb.Run(t.Context(), "amazon/dynamodb-local") + if err != nil { + panic("failed to start container: " + err.Error() + "") + } + defer func() { + if err := testcontainers.TerminateContainer(dynamoDbContainer); err != nil { + panic("failed to terminate container: " + err.Error() + "") + } + }() + + t.Setenv("AWS_ACCESS_KEY_ID", "key") + t.Setenv("AWS_SECRET_ACCESS_KEY", "secret") + t.Setenv("AWS_SESSION_TOKEN", "session") + t.Setenv("AWS_DEFAULT_REGION", "us-east-1") + + awsCtx, err := awsconfig.LoadDefaultConfig(t.Context()) + handler.InstrumentAws(&awsCtx) + str, _ := dynamoDbContainer.ConnectionString(t.Context()) + opts := []func(*dynamodb.Options){ + func(options *dynamodb.Options) { + options.BaseEndpoint = aws.String("http://" + str) + }, + } + db := dynamodb.NewFromConfig(awsCtx, opts...) + _, _ = db.DescribeTable(t.Context(), &dynamodb.DescribeTableInput{ + TableName: aws.String("test"), + }) + + handler.ForceFlush(t.Context()) + + assert.True(t, collector.hasTrace("DynamoDB.DescribeTable")) + }) +} + +func Test_Empty_Instrument(t *testing.T) { + handler := NewEmptyReporter() + + assert.Nil(t, handler.InstrumentHttp("t", http.MethodGet, nil)) + assert.Equal(t, http.DefaultTransport, handler.InstrumentHttpClient(http.DefaultTransport)) + assert.Empty(t, handler.InstrumentGrpc([]grpc.ServerOption{})) + _, span := handler.StartSpan(t.Context(), "t") + assert.Equal(t, noop.Span{}, span) + + handler.ForceFlush(t.Context()) + handler.RecordConnections(5, "sdk", "grpc", "flag") + handler.AddSentMessageCount(5, "sdk", "grpc", "flag") + handler.InstrumentRedis(nil) + + opts := &options.ClientOptions{} + handler.InstrumentMongoDb(opts) + assert.Nil(t, opts.Monitor) + + conf := &aws.Config{} + handler.InstrumentAws(conf) + assert.Empty(t, conf.APIOptions) + + handler.Shutdown() +} diff --git a/diag/telemetry/traces.go b/diag/telemetry/traces.go new file mode 100644 index 0000000..1b94055 --- /dev/null +++ b/diag/telemetry/traces.go @@ -0,0 +1,86 @@ +package telemetry + +import ( + "context" + "time" + + "github.com/configcat/configcat-proxy/config" + "github.com/configcat/configcat-proxy/log" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp" + "go.opentelemetry.io/otel/sdk/resource" + "go.opentelemetry.io/otel/sdk/trace" +) + +var ( + SdkId = K("configcat.sdk.id") + Source = K("configcat.source") +) + +type traceHandler struct { + provider *trace.TracerProvider + log log.Logger +} + +func newTraceHandler(ctx context.Context, resource *resource.Resource, conf *config.TraceConfig, log log.Logger) *traceHandler { + if !conf.Otlp.Enabled { + return nil + } + logger := log.WithPrefix("traces") + providerOpts := []trace.TracerProviderOption{trace.WithResource(resource)} + if conf.Otlp.Enabled { + switch conf.Otlp.Protocol { + case "grpc": + var opts []otlptracegrpc.Option + if conf.Otlp.Endpoint != "" { + opts = append(opts, otlptracegrpc.WithEndpoint(conf.Otlp.Endpoint)) + } + opts = append(opts, otlptracegrpc.WithInsecure()) + r, err := otlptracegrpc.New(ctx, opts...) + if err != nil { + logger.Errorf("failed to configure OTLP gRPC exporter: %s", err) + return nil + } + providerOpts = append(providerOpts, trace.WithBatcher(r)) + case "http": + fallthrough + case "https": + var opts []otlptracehttp.Option + if conf.Otlp.Endpoint != "" { + opts = append(opts, otlptracehttp.WithEndpoint(conf.Otlp.Endpoint)) + } + if conf.Otlp.Protocol == "http" { + opts = append(opts, otlptracehttp.WithInsecure()) + } + r, err := otlptracehttp.New(ctx, opts...) + if err != nil { + logger.Errorf("failed to configure OTLP HTTP exporter: %s", err) + return nil + } + providerOpts = append(providerOpts, trace.WithBatcher(r)) + } + + var ep string + if conf.Otlp.Endpoint != "" { + ep = " to " + conf.Otlp.Endpoint + "" + } + logger.Reportf("otlp exporter enabled over %s%s", conf.Otlp.Protocol, ep) + } + return &traceHandler{ + provider: trace.NewTracerProvider(providerOpts...), + log: logger, + } +} + +func (r *traceHandler) Shutdown() { + r.log.Reportf("initiating server shutdown") + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + err := r.provider.Shutdown(ctx) + if err != nil { + r.log.Errorf("shutdown error: %s", err) + } + r.log.Reportf("server shutdown complete") +} diff --git a/diag/telemetry/traces_test.go b/diag/telemetry/traces_test.go new file mode 100644 index 0000000..a41ebd0 --- /dev/null +++ b/diag/telemetry/traces_test.go @@ -0,0 +1,210 @@ +package telemetry + +import ( + "compress/gzip" + "context" + "fmt" + "io" + "net" + "net/http" + "net/http/httptest" + "reflect" + "sync" + "testing" + + "github.com/configcat/configcat-proxy/config" + "github.com/configcat/configcat-proxy/log" + "github.com/stretchr/testify/assert" + codes2 "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/trace" + otlptpb "go.opentelemetry.io/proto/otlp/collector/trace/v1" + tpb "go.opentelemetry.io/proto/otlp/trace/v1" + "google.golang.org/grpc" + "google.golang.org/protobuf/proto" +) + +func TestOtlpTracesExporterGrpc(t *testing.T) { + collector, err := newInMemoryTraceGrpcCollector() + assert.NoError(t, err) + defer collector.Shutdown() + + handler := newTraceHandler(t.Context(), buildResource("0.1.0"), + &config.TraceConfig{Enabled: true, Otlp: config.OtlpExporterConfig{Enabled: true, Protocol: "grpc", Endpoint: collector.Addr()}}, log.NewNullLogger()) + assert.NotNil(t, handler) + defer handler.Shutdown() + + _, s := handler.provider.Tracer("t").Start(t.Context(), "span", trace.WithSpanKind(trace.SpanKindClient)) + s.SetStatus(codes2.Ok, "OK") + s.End() + _ = handler.provider.ForceFlush(t.Context()) + + assert.True(t, collector.hasTrace("span")) +} + +func TestOtlpTracesExporterHttp(t *testing.T) { + collector := newInMemoryTraceHttpCollector() + defer collector.Shutdown() + + handler := newTraceHandler(t.Context(), buildResource("0.1.0"), + &config.TraceConfig{Enabled: true, Otlp: config.OtlpExporterConfig{Enabled: true, Protocol: "http", Endpoint: collector.Addr()}}, log.NewNullLogger()) + assert.NotNil(t, handler) + defer handler.Shutdown() + + _, s := handler.provider.Tracer("t").Start(t.Context(), "span", trace.WithSpanKind(trace.SpanKindClient)) + s.SetStatus(codes2.Ok, "OK") + s.End() + _ = handler.provider.ForceFlush(t.Context()) + + assert.True(t, hasTrace(collector, "span")) +} + +type grpcCollector struct { + listener net.Listener + srv *grpc.Server + mu sync.RWMutex +} + +type inMemoryTraceGrpcCollector struct { + otlptpb.UnimplementedTraceServiceServer + *grpcCollector + + traces []*tpb.ResourceSpans +} + +func newGrpcCollector() (*grpcCollector, error) { + listener, err := net.Listen("tcp", "localhost:0") + if err != nil { + return nil, err + } + srv := grpc.NewServer() + return &grpcCollector{ + listener: listener, + srv: srv, + }, nil +} + +func newInMemoryTraceGrpcCollector() (*inMemoryTraceGrpcCollector, error) { + gc, err := newGrpcCollector() + if err != nil { + return nil, err + } + c := &inMemoryTraceGrpcCollector{ + grpcCollector: gc, + traces: make([]*tpb.ResourceSpans, 0), + } + otlptpb.RegisterTraceServiceServer(c.srv, c) + go func() { _ = c.srv.Serve(c.listener) }() + + return c, nil +} + +func (c *grpcCollector) Addr() string { + return c.listener.Addr().String() +} + +func (c *grpcCollector) Shutdown() { + c.srv.Stop() +} + +func (c *inMemoryTraceGrpcCollector) hasTrace(name string) bool { + c.mu.RLock() + defer c.mu.RUnlock() + + for _, t := range c.traces { + for _, span := range t.ScopeSpans { + for _, s := range span.Spans { + if s.Name == name { + return true + } + } + } + } + return false +} + +func (c *inMemoryTraceGrpcCollector) Export(_ context.Context, req *otlptpb.ExportTraceServiceRequest) (*otlptpb.ExportTraceServiceResponse, error) { + c.mu.Lock() + defer c.mu.Unlock() + + c.traces = append(c.traces, req.ResourceSpans...) + + return &otlptpb.ExportTraceServiceResponse{}, nil +} + +func newInMemoryTraceHttpCollector() *inMemoryHttpCollector[*otlptpb.ExportTraceServiceRequest] { + c := &inMemoryHttpCollector[*otlptpb.ExportTraceServiceRequest]{ + records: make([]*otlptpb.ExportTraceServiceRequest, 0), + } + c.srv = httptest.NewServer(c) + return c +} + +type inMemoryHttpCollector[T proto.Message] struct { + srv *httptest.Server + mu sync.RWMutex + records []T +} + +func hasTrace(c *inMemoryHttpCollector[*otlptpb.ExportTraceServiceRequest], name string) bool { + c.mu.RLock() + defer c.mu.RUnlock() + + for _, r := range c.records { + for _, t := range r.ResourceSpans { + for _, span := range t.ScopeSpans { + for _, s := range span.Spans { + if s.Name == name { + return true + } + } + } + } + } + return false +} + +func (c *inMemoryHttpCollector[T]) Addr() string { + return c.srv.Listener.Addr().String() +} + +func (c *inMemoryHttpCollector[T]) ServeHTTP(w http.ResponseWriter, r *http.Request) { + var reader io.ReadCloser + var err error + switch r.Header.Get("Content-Encoding") { + case "gzip": + reader, err = gzip.NewReader(r.Body) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + _, _ = fmt.Fprintln(w, err.Error()) + return + } + default: + reader = r.Body + } + defer func() { _ = reader.Close() }() + body, err := io.ReadAll(reader) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + _, _ = fmt.Fprintln(w, err.Error()) + return + } + + var req T + msgType := reflect.TypeOf(req).Elem() + req = reflect.New(msgType).Interface().(T) + err = proto.Unmarshal(body, req) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + _, _ = fmt.Fprintln(w, err.Error()) + return + } + c.mu.Lock() + defer c.mu.Unlock() + c.records = append(c.records, req) + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte("OK")) +} + +func (c *inMemoryHttpCollector[msg]) Shutdown() { + c.srv.Close() +} diff --git a/go.mod b/go.mod index 663eaea..4417530 100644 --- a/go.mod +++ b/go.mod @@ -4,61 +4,130 @@ go 1.25 require ( github.com/alicebob/miniredis/v2 v2.35.0 - github.com/aws/aws-sdk-go-v2 v1.38.1 - github.com/aws/aws-sdk-go-v2/config v1.31.3 - github.com/aws/aws-sdk-go-v2/service/dynamodb v1.49.1 + github.com/aws/aws-sdk-go-v2 v1.39.6 + github.com/aws/aws-sdk-go-v2/config v1.31.19 + github.com/aws/aws-sdk-go-v2/service/dynamodb v1.52.5 github.com/cespare/xxhash/v2 v2.3.0 - github.com/configcat/go-sdk/v9 v9.0.7 + github.com/configcat/go-sdk/v9 v9.1.0 + github.com/docker/go-connections v0.6.0 github.com/fsnotify/fsnotify v1.9.0 - github.com/open-feature/go-sdk v1.15.1 + github.com/open-feature/go-sdk v1.17.0 github.com/open-feature/go-sdk-contrib/providers/ofrep v0.1.6 - github.com/prometheus/client_golang v1.23.0 + github.com/prometheus/client_golang v1.23.2 + github.com/prometheus/otlptranslator v1.0.0 github.com/puzpuzpuz/xsync/v3 v3.5.1 - github.com/redis/go-redis/v9 v9.12.1 + github.com/redis/go-redis/extra/redisotel/v9 v9.16.0 + github.com/redis/go-redis/v9 v9.16.0 github.com/stretchr/testify v1.11.1 - go.mongodb.org/mongo-driver v1.17.4 - google.golang.org/grpc v1.75.0 - google.golang.org/protobuf v1.36.8 + github.com/testcontainers/testcontainers-go v0.40.0 + github.com/testcontainers/testcontainers-go/modules/dynamodb v0.40.0 + github.com/testcontainers/testcontainers-go/modules/mongodb v0.40.0 + github.com/testcontainers/testcontainers-go/modules/redis v0.40.0 + github.com/testcontainers/testcontainers-go/modules/valkey v0.40.0 + go.mongodb.org/mongo-driver/v2 v2.4.0 + go.opentelemetry.io/contrib/instrumentation/github.com/aws/aws-sdk-go-v2/otelaws v0.63.0 + go.opentelemetry.io/contrib/instrumentation/go.mongodb.org/mongo-driver/v2/mongo/otelmongo v0.0.0-20251112104402-3caebdc613c8 + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 + go.opentelemetry.io/contrib/instrumentation/host v0.63.0 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 + go.opentelemetry.io/contrib/instrumentation/runtime v0.63.0 + go.opentelemetry.io/otel v1.38.0 + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.38.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 + go.opentelemetry.io/otel/exporters/prometheus v0.60.0 + go.opentelemetry.io/otel/metric v1.38.0 + go.opentelemetry.io/otel/sdk v1.38.0 + go.opentelemetry.io/otel/sdk/metric v1.38.0 + go.opentelemetry.io/otel/trace v1.38.0 + go.opentelemetry.io/proto/otlp v1.9.0 + google.golang.org/grpc v1.76.0 + google.golang.org/protobuf v1.36.10 gopkg.in/yaml.v3 v3.0.1 ) require ( - github.com/aws/aws-sdk-go-v2/credentials v1.18.7 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.4 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.4 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.4 // indirect - github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.0 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.11.4 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.4 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.28.2 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.34.0 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.38.0 // indirect - github.com/aws/smithy-go v1.23.0 // indirect + dario.cat/mergo v1.0.2 // indirect + github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.18.23 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.13 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.13 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.11.13 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13 // indirect + github.com/aws/aws-sdk-go-v2/service/sns v1.39.4 // indirect + github.com/aws/aws-sdk-go-v2/service/sqs v1.42.14 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.30.2 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.6 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.40.1 // indirect + github.com/aws/smithy-go v1.23.2 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/blang/semver/v4 v4.0.0 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/cenkalti/backoff/v5 v5.0.3 // indirect + github.com/containerd/errdefs v1.0.0 // indirect + github.com/containerd/errdefs/pkg v0.3.0 // indirect + github.com/containerd/log v0.1.0 // indirect + github.com/containerd/platforms v0.2.1 // indirect + github.com/cpuguy83/dockercfg v0.3.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/distribution/reference v0.6.0 // indirect + github.com/docker/docker v28.5.2+incompatible // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/ebitengine/purego v0.9.1 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect github.com/golang/snappy v1.0.0 // indirect - github.com/klauspost/compress v1.18.0 // indirect - github.com/kylelemons/godebug v1.1.0 // indirect - github.com/montanaflynn/stats v0.7.1 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect + github.com/klauspost/compress v1.18.1 // indirect + github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect + github.com/magiconair/properties v1.8.10 // indirect + github.com/mdelapenya/tlscert v0.2.0 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/go-archive v0.1.0 // indirect + github.com/moby/patternmatcher v0.6.0 // indirect + github.com/moby/sys/sequential v0.6.0 // indirect + github.com/moby/sys/user v0.4.0 // indirect + github.com/moby/sys/userns v0.1.0 // indirect + github.com/moby/term v0.5.2 // indirect + github.com/morikuni/aec v1.0.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.1 // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.65.0 // indirect - github.com/prometheus/procfs v0.17.0 // indirect + github.com/prometheus/common v0.67.2 // indirect + github.com/prometheus/procfs v0.19.2 // indirect + github.com/redis/go-redis/extra/rediscmd/v9 v9.16.0 // indirect + github.com/shirou/gopsutil/v4 v4.25.10 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/tklauser/go-sysconf v0.3.15 // indirect + github.com/tklauser/numcpus v0.11.0 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect github.com/xdg-go/scram v1.1.2 // indirect github.com/xdg-go/stringprep v1.0.4 // indirect github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect github.com/yuin/gopher-lua v1.1.1 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect go.uber.org/mock v0.6.0 // indirect - golang.org/x/crypto v0.41.0 // indirect - golang.org/x/net v0.43.0 // indirect - golang.org/x/sync v0.16.0 // indirect - golang.org/x/sys v0.35.0 // indirect - golang.org/x/text v0.28.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250826171959-ef028d996bc1 // indirect + go.yaml.in/yaml/v2 v2.4.3 // indirect + golang.org/x/crypto v0.44.0 // indirect + golang.org/x/net v0.47.0 // indirect + golang.org/x/sync v0.18.0 // indirect + golang.org/x/sys v0.38.0 // indirect + golang.org/x/text v0.31.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba // indirect ) diff --git a/go.sum b/go.sum index 63de787..63eacb3 100644 --- a/go.sum +++ b/go.sum @@ -1,35 +1,49 @@ +dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= +dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/alicebob/miniredis/v2 v2.35.0 h1:QwLphYqCEAo1eu1TqPRN2jgVMPBweeQcR21jeqDCONI= github.com/alicebob/miniredis/v2 v2.35.0/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM= -github.com/aws/aws-sdk-go-v2 v1.38.1 h1:j7sc33amE74Rz0M/PoCpsZQ6OunLqys/m5antM0J+Z8= -github.com/aws/aws-sdk-go-v2 v1.38.1/go.mod h1:9Q0OoGQoboYIAJyslFyF1f5K1Ryddop8gqMhWx/n4Wg= -github.com/aws/aws-sdk-go-v2/config v1.31.3 h1:RIb3yr/+PZ18YYNe6MDiG/3jVoJrPmdoCARwNkMGvco= -github.com/aws/aws-sdk-go-v2/config v1.31.3/go.mod h1:jjgx1n7x0FAKl6TnakqrpkHWWKcX3xfWtdnIJs5K9CE= -github.com/aws/aws-sdk-go-v2/credentials v1.18.7 h1:zqg4OMrKj+t5HlswDApgvAHjxKtlduKS7KicXB+7RLg= -github.com/aws/aws-sdk-go-v2/credentials v1.18.7/go.mod h1:/4M5OidTskkgkv+nCIfC9/tbiQ/c8qTox9QcUDV0cgc= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.4 h1:lpdMwTzmuDLkgW7086jE94HweHCqG+uOJwHf3LZs7T0= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.4/go.mod h1:9xzb8/SV62W6gHQGC/8rrvgNXU6ZoYM3sAIJCIrXJxY= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.4 h1:IdCLsiiIj5YJ3AFevsewURCPV+YWUlOW8JiPhoAy8vg= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.4/go.mod h1:l4bdfCD7XyyZA9BolKBo1eLqgaJxl0/x91PL4Yqe0ao= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.4 h1:j7vjtr1YIssWQOMeOWRbh3z8g2oY/xPjnZH2gLY4sGw= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.4/go.mod h1:yDmJgqOiH4EA8Hndnv4KwAo8jCGTSnM5ASG1nBI+toA= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo= -github.com/aws/aws-sdk-go-v2/service/dynamodb v1.49.1 h1:0RqS5X7EodJzOenoY4V3LUSp9PirELO2ZOpOZbMldco= -github.com/aws/aws-sdk-go-v2/service/dynamodb v1.49.1/go.mod h1:VRp/OeQolnQD9GfNgdSf3kU5vbg708PF6oPHh2bq3hc= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.0 h1:6+lZi2JeGKtCraAj1rpoZfKqnQ9SptseRZioejfUOLM= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.0/go.mod h1:eb3gfbVIxIoGgJsi9pGne19dhCBpK6opTYpQqAmdy44= -github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.11.4 h1:upi++G3fQCAUBXQe58TbjXmdVPwrqMnRQMThOAIz7KM= -github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.11.4/go.mod h1:swb+GqWXTZMOyVV9rVePAUu5L80+X5a+Lui1RNOyUFo= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.4 h1:ueB2Te0NacDMnaC+68za9jLwkjzxGWm0KB5HTUHjLTI= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.4/go.mod h1:nLEfLnVMmLvyIG58/6gsSA03F1voKGaCfHV7+lR8S7s= -github.com/aws/aws-sdk-go-v2/service/sso v1.28.2 h1:ve9dYBB8CfJGTFqcQ3ZLAAb/KXWgYlgu/2R2TZL2Ko0= -github.com/aws/aws-sdk-go-v2/service/sso v1.28.2/go.mod h1:n9bTZFZcBa9hGGqVz3i/a6+NG0zmZgtkB9qVVFDqPA8= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.34.0 h1:Bnr+fXrlrPEoR1MAFrHVsge3M/WoK4n23VNhRM7TPHI= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.34.0/go.mod h1:eknndR9rU8UpE/OmFpqU78V1EcXPKFTTm5l/buZYgvM= -github.com/aws/aws-sdk-go-v2/service/sts v1.38.0 h1:iV1Ko4Em/lkJIsoKyGfc0nQySi+v0Udxr6Igq+y9JZc= -github.com/aws/aws-sdk-go-v2/service/sts v1.38.0/go.mod h1:bEPcjW7IbolPfK67G1nilqWyoxYMSPrDiIQ3RdIdKgo= -github.com/aws/smithy-go v1.23.0 h1:8n6I3gXzWJB2DxBDnfxgBaSX6oe0d/t10qGz7OKqMCE= -github.com/aws/smithy-go v1.23.0/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI= +github.com/aws/aws-sdk-go-v2 v1.39.6 h1:2JrPCVgWJm7bm83BDwY5z8ietmeJUbh3O2ACnn+Xsqk= +github.com/aws/aws-sdk-go-v2 v1.39.6/go.mod h1:c9pm7VwuW0UPxAEYGyTmyurVcNrbF6Rt/wixFqDhcjE= +github.com/aws/aws-sdk-go-v2/config v1.31.19 h1:qdUtOw4JhZr2YcKO3g0ho/IcFXfXrrb8xlX05Y6EvSw= +github.com/aws/aws-sdk-go-v2/config v1.31.19/go.mod h1:tMJ8bur01t8eEm0atLadkIIFA154OJ4JCKZeQ+o+R7k= +github.com/aws/aws-sdk-go-v2/credentials v1.18.23 h1:IQILcxVgMO2BVLaJ2aAv21dKWvE1MduNrbvuK43XL2Q= +github.com/aws/aws-sdk-go-v2/credentials v1.18.23/go.mod h1:JRodHszhVdh5TPUknxDzJzrMiznG+M+FfR3WSWKgCI8= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13 h1:T1brd5dR3/fzNFAQch/iBKeX07/ffu/cLu+q+RuzEWk= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13/go.mod h1:Peg/GBAQ6JDt+RoBf4meB1wylmAipb7Kg2ZFakZTlwk= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.13 h1:a+8/MLcWlIxo1lF9xaGt3J/u3yOZx+CdSveSNwjhD40= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.13/go.mod h1:oGnKwIYZ4XttyU2JWxFrwvhF6YKiK/9/wmE3v3Iu9K8= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.13 h1:HBSI2kDkMdWz4ZM7FjwE7e/pWDEZ+nR95x8Ztet1ooY= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.13/go.mod h1:YE94ZoDArI7awZqJzBAZ3PDD2zSfuP7w6P2knOzIn8M= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc= +github.com/aws/aws-sdk-go-v2/service/dynamodb v1.52.5 h1:/TXo+DTOlDiZ/RyH+96ymvtfPT5ervOlg9j+42IMXA0= +github.com/aws/aws-sdk-go-v2/service/dynamodb v1.52.5/go.mod h1:6eUUnWOJ8sucL5Uk8rPkFo8FYioM0CTNGHga8hwzXVc= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3 h1:x2Ibm/Af8Fi+BH+Hsn9TXGdT+hKbDd5XOTZxTMxDk7o= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3/go.mod h1:IW1jwyrQgMdhisceG8fQLmQIydcT/jWY21rFhzgaKwo= +github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.11.13 h1:FScsqdRyKFkw3u2ysLeWC0dbaz9I+g0xJ1JlQpH6bPo= +github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.11.13/go.mod h1:wkhwIaGltEuG4SRwNzPiJmf/tDp+yL5ym55Lt4bheno= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13 h1:kDqdFvMY4AtKoACfzIGD8A0+hbT41KTKF//gq7jITfM= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13/go.mod h1:lmKuogqSU3HzQCwZ9ZtcqOc5XGMqtDK7OIc2+DxiUEg= +github.com/aws/aws-sdk-go-v2/service/route53 v1.57.2 h1:S3UZycqIGdXUDZkHQ/dTo99mFaHATfCJEVcYrnT24o4= +github.com/aws/aws-sdk-go-v2/service/route53 v1.57.2/go.mod h1:j4q6vBiAJvH9oxFyFtZoV739zxVMsSn26XNFvFlorfU= +github.com/aws/aws-sdk-go-v2/service/sns v1.39.4 h1:xbR5avT2W3v4tHh8HqeqqJHR/ge5kJgMNy9SyI4HJ3M= +github.com/aws/aws-sdk-go-v2/service/sns v1.39.4/go.mod h1:1LvRsmADXI6174y66InuSDQiEztkQgCLbcw62VLC0FQ= +github.com/aws/aws-sdk-go-v2/service/sqs v1.42.14 h1:VB/VRA5FLpYqUMR9jHyihkg2qTk2u7MIkwKFKf2870Y= +github.com/aws/aws-sdk-go-v2/service/sqs v1.42.14/go.mod h1:ZS67woOy/ftzvKK2+P53u2NPqImAPTWz+hBn+tchP7k= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.2 h1:/p6MxkbQoCzaGQT3WO0JwG0FlQyG9RD8VmdmoKc5xqU= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.2/go.mod h1:fKvyjJcz63iL/ftA6RaM8sRCtN4r4zl4tjL3qw5ec7k= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.6 h1:0dES42T2dhICCbVB3JSTTn7+Bz93wfJEK1b7jksZIyQ= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.6/go.mod h1:klO+ejMvYsB4QATfEOIXk8WAEwN4N0aBfJpvC+5SZBo= +github.com/aws/aws-sdk-go-v2/service/sts v1.40.1 h1:5sbIM57lHLaEaNWdIx23JH30LNBsSDkjN/QXGcRLAFc= +github.com/aws/aws-sdk-go-v2/service/sts v1.40.1/go.mod h1:E19xDjpzPZC7LS2knI9E6BaRFDK43Eul7vd6rSq2HWk= +github.com/aws/smithy-go v1.23.2 h1:Crv0eatJUQhaManss33hS5r40CG3ZFH+21XSkqMrIUM= +github.com/aws/smithy-go v1.23.2/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= @@ -38,22 +52,55 @@ github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= +github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/configcat/go-sdk/v9 v9.0.7 h1:BGa2lzpkH8i/f5BI8TcNp6sAtV36aKxejV7nSFSwtu4= -github.com/configcat/go-sdk/v9 v9.0.7/go.mod h1:RXzI3PW8zBvycGwSEqH+/mPQx/SF+XSwf/ANaSSI+ag= +github.com/configcat/go-sdk/v9 v9.1.0 h1:S0bMUEjPaFs6lG3oNOdMmTuLVEMLERIygUl3P4vhaE4= +github.com/configcat/go-sdk/v9 v9.1.0/go.mod h1:HLEJ5Rl75yy+XSQL3+iXhZcvebtCCZp2VzU3Aua5ItU= +github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= +github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= +github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= +github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= +github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= +github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +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/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM= +github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= +github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A= +github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= @@ -62,40 +109,102 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= -github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4= +github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co= +github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0= 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/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= -github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE= -github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= +github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 h1:PwQumkgq4/acIiZhtifTV5OUqqiP82UAl0h87xj/l9k= +github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= +github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= +github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mdelapenya/tlscert v0.2.0 h1:7H81W6Z/4weDvZBNOfQte5GpIMo0lGYEeWbkGp5LJHI= +github.com/mdelapenya/tlscert v0.2.0/go.mod h1:O4njj3ELLnJjGdkN7M/vIVCpZ+Cf0L6muqOG4tLSl8o= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ= +github.com/moby/go-archive v0.1.0/go.mod h1:G9B+YoujNohJmrIYFBpSd54GTUB4lt9S+xVQvsJyFuo= +github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= +github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= +github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw= +github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs= +github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= +github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= +github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs= +github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= +github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= +github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= +github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= +github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/open-feature/go-sdk v1.15.1 h1:TC3FtHtOKlGlIbSf3SEpxXVhgTd/bCbuc39XHIyltkw= -github.com/open-feature/go-sdk v1.15.1/go.mod h1:2WAFYzt8rLYavcubpCoiym3iSCXiHdPB6DxtMkv2wyo= +github.com/open-feature/go-sdk v1.17.0 h1:/OUBBw5d9D61JaNZZxb2Nnr5/EJrEpjtKCTY3rspJQk= +github.com/open-feature/go-sdk v1.17.0/go.mod h1:lPxPSu1UnZ4E3dCxZi5gV3et2ACi8O8P+zsTGVsDZUw= github.com/open-feature/go-sdk-contrib/providers/ofrep v0.1.6 h1:WinefYxeVx5rV0uQmuWbxQf8iACu/JiRubo5w0saToc= github.com/open-feature/go-sdk-contrib/providers/ofrep v0.1.6/go.mod h1:Dwcaoma6lZVqYwyfVlY7eB6RXbG+Ju3b9cnpTlUN+Hc= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v1.23.0 h1:ust4zpdl9r4trLY/gSjlm07PuiBq2ynaXXlptpfy8Uc= -github.com/prometheus/client_golang v1.23.0/go.mod h1:i/o0R9ByOnHX0McrTMTyhYvKE4haaf2mW08I+jGAjEE= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE= -github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= -github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0= -github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw= +github.com/prometheus/common v0.67.2 h1:PcBAckGFTIHt2+L3I33uNRTlKTplNzFctXcWhPyAEN8= +github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko= +github.com/prometheus/otlptranslator v1.0.0 h1:s0LJW/iN9dkIH+EnhiD3BlkkP5QVIUVEoIwkU+A6qos= +github.com/prometheus/otlptranslator v1.0.0/go.mod h1:vRYWnXvI6aWGpsdY/mOT/cbeVRBlPWtBNDb7kGR3uKM= +github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= +github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg= github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= -github.com/redis/go-redis/v9 v9.12.1 h1:k5iquqv27aBtnTm2tIkROUDp8JBXhXZIVu1InSgvovg= -github.com/redis/go-redis/v9 v9.12.1/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw= -github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= -github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/redis/go-redis/extra/rediscmd/v9 v9.16.0 h1:zAFQyFxJ3QDwpPUY/CKn22LI5+B8m/lUyffzq2+8ENs= +github.com/redis/go-redis/extra/rediscmd/v9 v9.16.0/go.mod h1:ouOc8ujB2wdUG6o0RrqaPl2tI6cenExC0KkJQ+PHXmw= +github.com/redis/go-redis/extra/redisotel/v9 v9.16.0 h1:+a9h9qxFXdf3gX0FXnDcz7X44ZBFUPq58Gblq7aMU4s= +github.com/redis/go-redis/extra/redisotel/v9 v9.16.0/go.mod h1:EtTTC7vnKWgznfG6kBgl9ySLqd7NckRCFUBzVXdeHeI= +github.com/redis/go-redis/v9 v9.16.0 h1:OotgqgLSRCmzfqChbQyG1PHC3tLNR89DG4jdOERSEP4= +github.com/redis/go-redis/v9 v9.16.0/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/shirou/gopsutil/v4 v4.25.10 h1:at8lk/5T1OgtuCp+AwrDofFRjnvosn0nkN2OLQ6g8tA= +github.com/shirou/gopsutil/v4 v4.25.10/go.mod h1:+kSwyC8DRUD9XXEHCAFjK+0nuArFJM0lva+StQAcskM= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/testcontainers/testcontainers-go v0.40.0 h1:pSdJYLOVgLE8YdUY2FHQ1Fxu+aMnb6JfVz1mxk7OeMU= +github.com/testcontainers/testcontainers-go v0.40.0/go.mod h1:FSXV5KQtX2HAMlm7U3APNyLkkap35zNLxukw9oBi/MY= +github.com/testcontainers/testcontainers-go/modules/dynamodb v0.40.0 h1:2MHpOb5FOSHk61BxZvUJU5JmQaV0cMSDvNFy48UpX9o= +github.com/testcontainers/testcontainers-go/modules/dynamodb v0.40.0/go.mod h1:7/xTZTRLo6rmjrtO7x7D4e0Jn6OVAFfQtAR/9a1NvxQ= +github.com/testcontainers/testcontainers-go/modules/mongodb v0.40.0 h1:z/1qHeliTLDKNaJ7uOHOx1FjwghbcbYfga4dTFkF0hU= +github.com/testcontainers/testcontainers-go/modules/mongodb v0.40.0/go.mod h1:GaunAWwMXLtsMKG3xn2HYIBDbKddGArfcGsF2Aog81E= +github.com/testcontainers/testcontainers-go/modules/redis v0.40.0 h1:OG4qwcxp2O0re7V7M9lY9w0v6wWgWf7j7rtkpAnGMd0= +github.com/testcontainers/testcontainers-go/modules/redis v0.40.0/go.mod h1:Bc+EDhKMo5zI5V5zdBkHiMVzeAXbtI4n5isS/nzf6zw= +github.com/testcontainers/testcontainers-go/modules/valkey v0.40.0 h1:V0zwJVnN8fOT++ySwo/P5cwd3pmXI7O4VdA7kQ+5OiM= +github.com/testcontainers/testcontainers-go/modules/valkey v0.40.0/go.mod h1:z+ndszow9abHiSnpO/hOvCgUMv80FldiKZHSpMwd80s= +github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4= +github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4= +github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw= +github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ= +github.com/valkey-io/valkey-go v1.0.59 h1:W67Z0UY+Qqk3k8NKkFCFlM3X4yQUniixl7dSJAch2Qo= +github.com/valkey-io/valkey-go v1.0.59/go.mod h1:bHmwjIEOrGq/ubOJfh5uMRs7Xj6mV3mQ/ZXUbmqpjqY= github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= @@ -107,67 +216,111 @@ github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfS github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M= github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= -go.mongodb.org/mongo-driver v1.17.4 h1:jUorfmVzljjr0FLzYQsGP8cgN/qzzxlY9Vh0C9KFXVw= -go.mongodb.org/mongo-driver v1.17.4/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= -go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= -go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= -go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= -go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= -go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= -go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc= -go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps= -go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= -go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +go.mongodb.org/mongo-driver/v2 v2.4.0 h1:Oq6BmUAAFTzMeh6AonuDlgZMuAuEiUxoAD1koK5MuFo= +go.mongodb.org/mongo-driver/v2 v2.4.0/go.mod h1:jHeEDJHJq7tm6ZF45Issun9dbogjfnPySb1vXA7EeAI= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/instrumentation/github.com/aws/aws-sdk-go-v2/otelaws v0.63.0 h1:0W0GZvzQe514c3igO063tR0cFVStoABt1agKqlYToL8= +go.opentelemetry.io/contrib/instrumentation/github.com/aws/aws-sdk-go-v2/otelaws v0.63.0/go.mod h1:wIvTiRUU7Pbfqas/5JVjGZcftBeSAGSYVMOHWzWG0qE= +go.opentelemetry.io/contrib/instrumentation/go.mongodb.org/mongo-driver/v2/mongo/otelmongo v0.0.0-20251112104402-3caebdc613c8 h1:FLlTQ+hODfAJliipMZY4+ILGkcDiLl+iSQMXCTBPhVk= +go.opentelemetry.io/contrib/instrumentation/go.mongodb.org/mongo-driver/v2/mongo/otelmongo v0.0.0-20251112104402-3caebdc613c8/go.mod h1:cKZXVt6JOnhcJe6z5iw54Jtmr2Lw3v5B30FI8ojDa7c= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 h1:YH4g8lQroajqUwWbq/tr2QX1JFmEXaDLgG+ew9bLMWo= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ= +go.opentelemetry.io/contrib/instrumentation/host v0.63.0 h1:zsaUrWypCf0NtYSUby+/BS6QqhXVNxMQD5w4dLczKCQ= +go.opentelemetry.io/contrib/instrumentation/host v0.63.0/go.mod h1:Ru+kuFO+ToZqBKwI59rCStOhW6LWrbGisYrFaX61bJk= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg= +go.opentelemetry.io/contrib/instrumentation/runtime v0.63.0 h1:PeBoRj6af6xMI7qCupwFvTbbnd49V7n5YpG6pg8iDYQ= +go.opentelemetry.io/contrib/instrumentation/runtime v0.63.0/go.mod h1:ingqBCtMCe8I4vpz/UVzCW6sxoqgZB37nao91mLQ3Bw= +go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= +go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 h1:vl9obrcoWVKp/lwl8tRE33853I8Xru9HFbw/skNeLs8= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0/go.mod h1:GAXRxmLJcVM3u22IjTg74zWBrRCKq8BnOqUVLodpcpw= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.38.0 h1:Oe2z/BCg5q7k4iXC3cqJxKYg0ieRiOqF0cecFYdPTwk= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.38.0/go.mod h1:ZQM5lAJpOsKnYagGg/zV2krVqTtaVdYdDkhMoX6Oalg= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 h1:lwI4Dc5leUqENgGuQImwLo4WnuXFPetmPpkLi2IrX54= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0/go.mod h1:Kz/oCE7z5wuyhPxsXDuaPteSWqjSBD5YaSdbxZYGbGk= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 h1:aTL7F04bJHUlztTsNGJ2l+6he8c+y/b//eR0jjjemT4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0/go.mod h1:kldtb7jDTeol0l3ewcmd8SDvx3EmIE7lyvqbasU3QC4= +go.opentelemetry.io/otel/exporters/prometheus v0.60.0 h1:cGtQxGvZbnrWdC2GyjZi0PDKVSLWP/Jocix3QWfXtbo= +go.opentelemetry.io/otel/exporters/prometheus v0.60.0/go.mod h1:hkd1EekxNo69PTV4OWFGZcKQiIqg0RfuWExcPKFvepk= +go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= +go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= +go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= +go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= +go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= +go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= +go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= +go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= +go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= +go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= +go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= -golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= +golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU= +golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= -golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= +golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= +golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= -golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= +golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= -golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= +golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= -golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= +golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= +golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44= +golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250826171959-ef028d996bc1 h1:pmJpJEvT846VzausCQ5d7KreSROcDqmO388w5YbnltA= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250826171959-ef028d996bc1/go.mod h1:GmFNa4BdJZ2a8G+wCe9Bg3wwThLrJun751XstdJt5Og= -google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4= -google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= -google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= -google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= +google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba h1:B14OtaXuMaCQsl2deSvNkyPKIzq3BjfxQp8d00QyWx4= +google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:G5IanEx8/PgI9w6CFcYQf7jMtHQhZruvfM1i3qOqk5U= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba h1:UKgtfRM7Yh93Sya0Fo8ZzhDP4qBckrrxEr2oF5UIVb8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= +google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= 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/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +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= +gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= +gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= diff --git a/grpc/flag_service.go b/grpc/flag_service.go index b23a4d7..7539deb 100644 --- a/grpc/flag_service.go +++ b/grpc/flag_service.go @@ -4,7 +4,7 @@ import ( "context" "errors" - "github.com/configcat/configcat-proxy/diag/metrics" + "github.com/configcat/configcat-proxy/diag/telemetry" "github.com/configcat/configcat-proxy/grpc/proto" "github.com/configcat/configcat-proxy/log" "github.com/configcat/configcat-proxy/model" @@ -24,9 +24,9 @@ type flagService struct { closed chan struct{} } -func newFlagService(sdkRegistrar sdk.Registrar, metrics metrics.Reporter, log log.Logger) *flagService { +func newFlagService(sdkRegistrar sdk.Registrar, telemetryReporter telemetry.Reporter, log log.Logger) *flagService { return &flagService{ - streamServer: stream.NewServer(sdkRegistrar, metrics, log, "grpc"), + streamServer: stream.NewServer(sdkRegistrar, telemetryReporter, log, "grpc"), log: log, sdkRegistrar: sdkRegistrar, closed: make(chan struct{}), @@ -133,33 +133,47 @@ func (s *flagService) EvalAllFlags(_ context.Context, req *proto.EvalRequest) (* } func (s *flagService) GetKeys(_ context.Context, req *proto.KeysRequest) (*proto.KeysResponse, error) { - if req.GetSdkId() == "" { - return nil, status.Error(codes.InvalidArgument, "sdk id parameter missing") + sdkId, sdkKey := identifyTarget(req.GetTarget(), req.GetSdkId()) + if sdkId == "" && sdkKey == "" { + return nil, status.Error(codes.InvalidArgument, "either the sdk id or the sdk key parameter must be set") + } + + var sdkClient sdk.Client + if sdkId != "" { + sdkClient = s.sdkRegistrar.GetSdkOrNil(sdkId) + } else { + sdkClient = s.sdkRegistrar.GetSdkByKeyOrNil(sdkKey) } - sdkClient := s.sdkRegistrar.GetSdkOrNil(req.GetSdkId()) if sdkClient == nil { - return nil, status.Error(codes.InvalidArgument, "sdk not found for identifier: '"+req.GetSdkId()+"'") + return nil, status.Error(codes.InvalidArgument, "could not identify a configured SDK") } if !sdkClient.IsInValidState() { - return nil, status.Error(codes.Internal, "sdk with identifier '"+req.GetSdkId()+"' is in an invalid state; please check the logs for more details") + return nil, status.Error(codes.Internal, "requested SDK is in an invalid state; please check the logs for more details") } keys := sdkClient.Keys() return &proto.KeysResponse{Keys: keys}, nil } -func (s *flagService) Refresh(_ context.Context, req *proto.RefreshRequest) (*emptypb.Empty, error) { - if req.GetSdkId() == "" { - return nil, status.Error(codes.InvalidArgument, "sdk id parameter missing") +func (s *flagService) Refresh(ctx context.Context, req *proto.RefreshRequest) (*emptypb.Empty, error) { + sdkId, sdkKey := identifyTarget(req.GetTarget(), req.GetSdkId()) + if sdkId == "" && sdkKey == "" { + return nil, status.Error(codes.InvalidArgument, "either the sdk id or the sdk key parameter must be set") + } + + var sdkClient sdk.Client + if sdkId != "" { + sdkClient = s.sdkRegistrar.GetSdkOrNil(sdkId) + } else { + sdkClient = s.sdkRegistrar.GetSdkByKeyOrNil(sdkKey) } - sdkClient := s.sdkRegistrar.GetSdkOrNil(req.GetSdkId()) if sdkClient == nil { - return nil, status.Error(codes.InvalidArgument, "sdk not found for identifier: '"+req.GetSdkId()+"'") + return nil, status.Error(codes.InvalidArgument, "could not identify a configured SDK") } - if err := sdkClient.Refresh(); err != nil { + if err := sdkClient.Refresh(ctx); err != nil { return nil, err } @@ -188,23 +202,29 @@ func (s *flagService) Close() { } func (s *flagService) parseEvalStreamRequest(req *proto.EvalRequest, user *model.UserAttrs, checkKey bool) (stream.Stream, error) { - if req.GetSdkId() == "" { - return nil, status.Error(codes.InvalidArgument, "sdk id parameter missing") + sdkId, sdkKey := identifyTarget(req.GetTarget(), req.GetSdkId()) + if sdkId == "" && sdkKey == "" { + return nil, status.Error(codes.InvalidArgument, "either the sdk id or the sdk key parameter must be set") } if checkKey && req.GetKey() == "" { return nil, status.Error(codes.InvalidArgument, "key request parameter missing") } - if req.GetUser() != nil { *user = getUserAttrs(req.GetUser()) } - str := s.streamServer.GetStreamOrNil(req.GetSdkId()) + var str stream.Stream + if sdkId != "" { + str = s.streamServer.GetStreamOrNil(sdkId) + } else { + str = s.streamServer.GetStreamBySdkKeyOrNil(sdkKey) + } + if str == nil { - return nil, status.Error(codes.InvalidArgument, "sdk not found for identifier: '"+req.GetSdkId()+"'") + return nil, status.Error(codes.InvalidArgument, "could not identify a configured SDK") } if !str.IsInValidState() { - return nil, status.Error(codes.Internal, "sdk with identifier '"+req.GetSdkId()+"' is in an invalid state; please check the logs for more details") + return nil, status.Error(codes.Internal, "requested SDK is in an invalid state; please check the logs for more details") } if checkKey && !str.CanEval(req.GetKey()) { return nil, status.Error(codes.InvalidArgument, "feature flag or setting with key '"+req.GetKey()+"' not found") @@ -213,27 +233,40 @@ func (s *flagService) parseEvalStreamRequest(req *proto.EvalRequest, user *model } func (s *flagService) parseEvalRequest(req *proto.EvalRequest, user *model.UserAttrs, checkKey bool) (sdk.Client, error) { - if req.GetSdkId() == "" { - return nil, status.Error(codes.InvalidArgument, "sdk id parameter missing") + sdkId, sdkKey := identifyTarget(req.GetTarget(), req.GetSdkId()) + if sdkId == "" && sdkKey == "" { + return nil, status.Error(codes.InvalidArgument, "either the sdk id or the sdk key parameter must be set") } if checkKey && req.GetKey() == "" { return nil, status.Error(codes.InvalidArgument, "key request parameter missing") } - if req.GetUser() != nil { *user = getUserAttrs(req.GetUser()) } - sdkClient := s.sdkRegistrar.GetSdkOrNil(req.GetSdkId()) + var sdkClient sdk.Client + if sdkId != "" { + sdkClient = s.sdkRegistrar.GetSdkOrNil(sdkId) + } else { + sdkClient = s.sdkRegistrar.GetSdkByKeyOrNil(sdkKey) + } + if sdkClient == nil { - return nil, status.Error(codes.InvalidArgument, "sdk not found for identifier: '"+req.GetSdkId()+"'") + return nil, status.Error(codes.InvalidArgument, "could not identify a configured SDK") } if !sdkClient.IsInValidState() { - return nil, status.Error(codes.Internal, "sdk with identifier '"+req.GetSdkId()+"' is in an invalid state; please check the logs for more details") + return nil, status.Error(codes.Internal, "requested SDK is in an invalid state; please check the logs for more details") } return sdkClient, nil } +func identifyTarget(target *proto.Target, sdkId string) (string, string) { + if target == nil { + return sdkId, "" + } + return target.GetSdkId(), target.GetSdkKey() +} + func getUserAttrs(attrs map[string]*proto.UserValue) model.UserAttrs { res := make(map[string]interface{}, len(attrs)) for k, v := range attrs { diff --git a/grpc/flag_service_test.go b/grpc/flag_service_test.go index fef7094..a6ae4b2 100644 --- a/grpc/flag_service_test.go +++ b/grpc/flag_service_test.go @@ -8,6 +8,7 @@ import ( "time" "github.com/configcat/configcat-proxy/config" + "github.com/configcat/configcat-proxy/diag/telemetry" "github.com/configcat/configcat-proxy/grpc/proto" "github.com/configcat/configcat-proxy/internal/testutils" "github.com/configcat/configcat-proxy/log" @@ -20,43 +21,58 @@ import ( ) func TestGrpc_EvalFlagStream(t *testing.T) { - key := configcattest.RandomSDKKey() - var h configcattest.Handler - _ = h.SetFlags(key, map[string]*configcattest.Flag{ + h, key, url := newFlagServer(t, map[string]*configcattest.Flag{ "flag": { Default: "test1", }, }) - sdkSrv := httptest.NewServer(&h) - defer sdkSrv.Close() - - reg := sdk.NewTestRegistrar(&config.SDKConfig{BaseUrl: sdkSrv.URL, Key: key, PollInterval: 1}, nil) - defer reg.Close() - flagSrv := newFlagService(reg, nil, log.NewNullLogger()) + conn := createFlagServiceConnWithManualRegistrar(t, url, key) + defer func() { + _ = conn.Close() + }() - lis := bufconn.Listen(1024 * 1024) + client := proto.NewFlagServiceClient(conn) + cl, err := client.EvalFlagStream(t.Context(), &proto.EvalRequest{Key: "flag", Target: &proto.Target{Identifier: &proto.Target_SdkId{SdkId: "test"}}, User: map[string]*proto.UserValue{"id": {Value: &proto.UserValue_StringValue{StringValue: "u1"}}}}) - srv := grpc.NewServer() - defer srv.GracefulStop() + assert.NoError(t, err) - proto.RegisterFlagServiceServer(srv, flagSrv) - go func() { - _ = srv.Serve(lis) - }() + var payload *proto.EvalResponse + testutils.WithTimeout(2*time.Second, func() { + payload, err = cl.Recv() + assert.NoError(t, err) + }) + assert.Equal(t, "test1", payload.GetStringValue()) - conn, err := grpc.NewClient("passthrough://bufnet", - grpc.WithTransportCredentials(insecure.NewCredentials()), - grpc.WithContextDialer(func(ctx context.Context, s string) (net.Conn, error) { - return lis.Dial() - })) + _ = h.SetFlags(key, map[string]*configcattest.Flag{ + "flag": { + Default: "test2", + }, + }) + _, err = client.Refresh(t.Context(), &proto.RefreshRequest{Target: &proto.Target{Identifier: &proto.Target_SdkId{SdkId: "test"}}}) assert.NoError(t, err) + + testutils.WithTimeout(2*time.Second, func() { + payload, err = cl.Recv() + assert.NoError(t, err) + }) + assert.Equal(t, "test2", payload.GetStringValue()) +} + +func TestGrpc_EvalFlagStream_With_Sdk_Key(t *testing.T) { + h, key, url := newFlagServer(t, map[string]*configcattest.Flag{ + "flag": { + Default: "test1", + }, + }) + conn := createFlagServiceConnWithManualRegistrar(t, url, key) defer func() { _ = conn.Close() }() client := proto.NewFlagServiceClient(conn) - cl, err := client.EvalFlagStream(context.Background(), &proto.EvalRequest{Key: "flag", SdkId: "test", User: map[string]*proto.UserValue{"id": {Value: &proto.UserValue_StringValue{StringValue: "u1"}}}}) + cl, err := client.EvalFlagStream(t.Context(), &proto.EvalRequest{Key: "flag", Target: &proto.Target{Identifier: &proto.Target_SdkKey{SdkKey: key}}, User: map[string]*proto.UserValue{"id": {Value: &proto.UserValue_StringValue{StringValue: "u1"}}}}) + assert.NoError(t, err) var payload *proto.EvalResponse @@ -72,7 +88,7 @@ func TestGrpc_EvalFlagStream(t *testing.T) { }, }) - _, err = client.Refresh(context.Background(), &proto.RefreshRequest{SdkId: "test"}) + _, err = client.Refresh(t.Context(), &proto.RefreshRequest{Target: &proto.Target{Identifier: &proto.Target_SdkKey{SdkKey: key}}}) assert.NoError(t, err) testutils.WithTimeout(2*time.Second, func() { @@ -83,32 +99,13 @@ func TestGrpc_EvalFlagStream(t *testing.T) { } func TestGrpc_EvalFlagStream_SdkRemoved(t *testing.T) { - reg, h, _ := sdk.NewTestAutoRegistrarWithAutoConfig(t, config.ProfileConfig{PollInterval: 60}, log.NewNullLogger()) - flagSrv := newFlagService(reg, nil, log.NewNullLogger()) - - lis := bufconn.Listen(1024 * 1024) - - srv := grpc.NewServer() - defer srv.GracefulStop() - - proto.RegisterFlagServiceServer(srv, flagSrv) - go func() { - _ = srv.Serve(lis) - }() - - conn, err := grpc.NewClient("passthrough://bufnet", - grpc.WithTransportCredentials(insecure.NewCredentials()), - grpc.WithContextDialer(func(ctx context.Context, s string) (net.Conn, error) { - return lis.Dial() - })) - - assert.NoError(t, err) + reg, conn, h := createFlagServiceConnWithAutoRegistrar(t) defer func() { _ = conn.Close() }() client := proto.NewFlagServiceClient(conn) - cl, err := client.EvalFlagStream(context.Background(), &proto.EvalRequest{Key: "flag", SdkId: "test", User: map[string]*proto.UserValue{"id": {Value: &proto.UserValue_StringValue{StringValue: "u1"}}}}) + cl, err := client.EvalFlagStream(t.Context(), &proto.EvalRequest{Key: "flag", Target: &proto.Target{Identifier: &proto.Target_SdkId{SdkId: "test"}}, User: map[string]*proto.UserValue{"id": {Value: &proto.UserValue_StringValue{StringValue: "u1"}}}}) assert.NoError(t, err) var payload *proto.EvalResponse @@ -127,9 +124,7 @@ func TestGrpc_EvalFlagStream_SdkRemoved(t *testing.T) { } func TestGrpc_EvalAllFlagsStream(t *testing.T) { - key := configcattest.RandomSDKKey() - var h configcattest.Handler - _ = h.SetFlags(key, map[string]*configcattest.Flag{ + h, key, url := newFlagServer(t, map[string]*configcattest.Flag{ "flag1": { Default: "test1", }, @@ -137,36 +132,13 @@ func TestGrpc_EvalAllFlagsStream(t *testing.T) { Default: "test2", }, }) - sdkSrv := httptest.NewServer(&h) - defer sdkSrv.Close() - - reg := sdk.NewTestRegistrar(&config.SDKConfig{BaseUrl: sdkSrv.URL, Key: key, PollInterval: 1}, nil) - defer reg.Close() - flagSrv := newFlagService(reg, nil, log.NewNullLogger()) - - lis := bufconn.Listen(1024 * 1024) - - srv := grpc.NewServer() - defer srv.GracefulStop() - - proto.RegisterFlagServiceServer(srv, flagSrv) - go func() { - _ = srv.Serve(lis) - }() - - conn, err := grpc.NewClient("passthrough://bufnet", - grpc.WithTransportCredentials(insecure.NewCredentials()), - grpc.WithContextDialer(func(ctx context.Context, s string) (net.Conn, error) { - return lis.Dial() - })) - - assert.NoError(t, err) + conn := createFlagServiceConnWithManualRegistrar(t, url, key) defer func() { _ = conn.Close() }() client := proto.NewFlagServiceClient(conn) - cl, err := client.EvalAllFlagsStream(context.Background(), &proto.EvalRequest{Key: "flag", SdkId: "test", User: map[string]*proto.UserValue{"id": {Value: &proto.UserValue_StringValue{StringValue: "u1"}}}}) + cl, err := client.EvalAllFlagsStream(t.Context(), &proto.EvalRequest{Target: &proto.Target{Identifier: &proto.Target_SdkId{SdkId: "test"}}, User: map[string]*proto.UserValue{"id": {Value: &proto.UserValue_StringValue{StringValue: "u1"}}}}) assert.NoError(t, err) var payload *proto.EvalAllResponse @@ -190,7 +162,7 @@ func TestGrpc_EvalAllFlagsStream(t *testing.T) { }, }) - _, err = client.Refresh(context.Background(), &proto.RefreshRequest{SdkId: "test"}) + _, err = client.Refresh(t.Context(), &proto.RefreshRequest{Target: &proto.Target{Identifier: &proto.Target_SdkId{SdkId: "test"}}}) assert.NoError(t, err) testutils.WithTimeout(2*time.Second, func() { @@ -204,32 +176,14 @@ func TestGrpc_EvalAllFlagsStream(t *testing.T) { } func TestGrpc_EvalAllFlagsStream_SdkRemoved(t *testing.T) { - reg, h, _ := sdk.NewTestAutoRegistrarWithAutoConfig(t, config.ProfileConfig{PollInterval: 60}, log.NewNullLogger()) - flagSrv := newFlagService(reg, nil, log.NewNullLogger()) - - lis := bufconn.Listen(1024 * 1024) - - srv := grpc.NewServer() - defer srv.GracefulStop() - - proto.RegisterFlagServiceServer(srv, flagSrv) - go func() { - _ = srv.Serve(lis) - }() - - conn, err := grpc.NewClient("passthrough://bufnet", - grpc.WithTransportCredentials(insecure.NewCredentials()), - grpc.WithContextDialer(func(ctx context.Context, s string) (net.Conn, error) { - return lis.Dial() - })) - - assert.NoError(t, err) + reg, conn, h := createFlagServiceConnWithAutoRegistrar(t) defer func() { _ = conn.Close() }() client := proto.NewFlagServiceClient(conn) - cl, err := client.EvalAllFlagsStream(context.Background(), &proto.EvalRequest{Key: "flag", SdkId: "test", User: map[string]*proto.UserValue{"id": {Value: &proto.UserValue_StringValue{StringValue: "u1"}}}}) + + cl, err := client.EvalAllFlagsStream(t.Context(), &proto.EvalRequest{Key: "flag", Target: &proto.Target{Identifier: &proto.Target_SdkId{SdkId: "test"}}, User: map[string]*proto.UserValue{"id": {Value: &proto.UserValue_StringValue{StringValue: "u1"}}}}) assert.NoError(t, err) var payload *proto.EvalAllResponse @@ -249,48 +203,58 @@ func TestGrpc_EvalAllFlagsStream_SdkRemoved(t *testing.T) { } func TestGrpc_EvalFlag(t *testing.T) { - key := configcattest.RandomSDKKey() - var h configcattest.Handler - _ = h.SetFlags(key, map[string]*configcattest.Flag{ + h, key, url := newFlagServer(t, map[string]*configcattest.Flag{ "flag": { Default: "test1", }, }) - sdkSrv := httptest.NewServer(&h) - defer sdkSrv.Close() + conn := createFlagServiceConnWithManualRegistrar(t, url, key) + defer func() { + _ = conn.Close() + }() - reg := sdk.NewTestRegistrar(&config.SDKConfig{BaseUrl: sdkSrv.URL, Key: key, PollInterval: 1}, nil) - defer reg.Close() - flagSrv := newFlagService(reg, nil, log.NewNullLogger()) + client := proto.NewFlagServiceClient(conn) + resp, err := client.EvalFlag(t.Context(), &proto.EvalRequest{Key: "flag", Target: &proto.Target{Identifier: &proto.Target_SdkId{SdkId: "test"}}, User: map[string]*proto.UserValue{"id": {Value: &proto.UserValue_StringValue{StringValue: "u1"}}}}) + assert.NoError(t, err) - lis := bufconn.Listen(1024 * 1024) + assert.Equal(t, "test1", resp.GetStringValue()) - srv := grpc.NewServer() - defer srv.GracefulStop() + _, err = client.EvalFlag(t.Context(), &proto.EvalRequest{Key: "non-existing", Target: &proto.Target{Identifier: &proto.Target_SdkId{SdkId: "test"}}, User: map[string]*proto.UserValue{"id": {Value: &proto.UserValue_StringValue{StringValue: "u1"}}}}) + assert.Error(t, err) - proto.RegisterFlagServiceServer(srv, flagSrv) - go func() { - _ = srv.Serve(lis) - }() + _ = h.SetFlags(key, map[string]*configcattest.Flag{ + "flag": { + Default: "test2", + }, + }) - conn, err := grpc.NewClient("passthrough://bufnet", - grpc.WithTransportCredentials(insecure.NewCredentials()), - grpc.WithContextDialer(func(ctx context.Context, s string) (net.Conn, error) { - return lis.Dial() - })) + _, err = client.Refresh(t.Context(), &proto.RefreshRequest{Target: &proto.Target{Identifier: &proto.Target_SdkId{SdkId: "test"}}}) + assert.NoError(t, err) + resp, err = client.EvalFlag(t.Context(), &proto.EvalRequest{Key: "flag", Target: &proto.Target{Identifier: &proto.Target_SdkId{SdkId: "test"}}, User: map[string]*proto.UserValue{"id": {Value: &proto.UserValue_StringValue{StringValue: "u1"}}}}) assert.NoError(t, err) + + assert.Equal(t, "test2", resp.GetStringValue()) +} + +func TestGrpc_EvalFlag_Old(t *testing.T) { + h, key, url := newFlagServer(t, map[string]*configcattest.Flag{ + "flag": { + Default: "test1", + }, + }) + conn := createFlagServiceConnWithManualRegistrar(t, url, key) defer func() { _ = conn.Close() }() client := proto.NewFlagServiceClient(conn) - resp, err := client.EvalFlag(context.Background(), &proto.EvalRequest{Key: "flag", SdkId: "test", User: map[string]*proto.UserValue{"id": {Value: &proto.UserValue_StringValue{StringValue: "u1"}}}}) + resp, err := client.EvalFlag(t.Context(), &proto.EvalRequest{Key: "flag", SdkId: "test", User: map[string]*proto.UserValue{"id": {Value: &proto.UserValue_StringValue{StringValue: "u1"}}}}) assert.NoError(t, err) assert.Equal(t, "test1", resp.GetStringValue()) - _, err = client.EvalFlag(context.Background(), &proto.EvalRequest{Key: "non-existing", SdkId: "test", User: map[string]*proto.UserValue{"id": {Value: &proto.UserValue_StringValue{StringValue: "u1"}}}}) + _, err = client.EvalFlag(t.Context(), &proto.EvalRequest{Key: "non-existing", SdkId: "test", User: map[string]*proto.UserValue{"id": {Value: &proto.UserValue_StringValue{StringValue: "u1"}}}}) assert.Error(t, err) _ = h.SetFlags(key, map[string]*configcattest.Flag{ @@ -299,162 +263,174 @@ func TestGrpc_EvalFlag(t *testing.T) { }, }) - _, err = client.Refresh(context.Background(), &proto.RefreshRequest{SdkId: "test"}) + _, err = client.Refresh(t.Context(), &proto.RefreshRequest{SdkId: "test"}) assert.NoError(t, err) - resp, err = client.EvalFlag(context.Background(), &proto.EvalRequest{Key: "flag", SdkId: "test", User: map[string]*proto.UserValue{"id": {Value: &proto.UserValue_StringValue{StringValue: "u1"}}}}) + resp, err = client.EvalFlag(t.Context(), &proto.EvalRequest{Key: "flag", SdkId: "test", User: map[string]*proto.UserValue{"id": {Value: &proto.UserValue_StringValue{StringValue: "u1"}}}}) assert.NoError(t, err) assert.Equal(t, "test2", resp.GetStringValue()) } -func TestGrpc_SDK_InvalidState(t *testing.T) { - reg := sdk.NewTestRegistrar(&config.SDKConfig{BaseUrl: "http://localhost", Key: configcattest.RandomSDKKey()}, nil) - defer reg.Close() - flagSrv := newFlagService(reg, nil, log.NewNullLogger()) +func TestGrpc_EvalFlag_With_Sdk_Key(t *testing.T) { + h, key, url := newFlagServer(t, map[string]*configcattest.Flag{ + "flag": { + Default: "test1", + }, + }) + conn := createFlagServiceConnWithManualRegistrar(t, url, key) + defer func() { + _ = conn.Close() + }() - lis := bufconn.Listen(1024 * 1024) + client := proto.NewFlagServiceClient(conn) - srv := grpc.NewServer() - defer srv.GracefulStop() + resp, err := client.EvalFlag(t.Context(), &proto.EvalRequest{Key: "flag", Target: &proto.Target{Identifier: &proto.Target_SdkKey{SdkKey: key}}, User: map[string]*proto.UserValue{"id": {Value: &proto.UserValue_StringValue{StringValue: "u1"}}}}) + assert.NoError(t, err) - proto.RegisterFlagServiceServer(srv, flagSrv) - go func() { - _ = srv.Serve(lis) - }() + assert.Equal(t, "test1", resp.GetStringValue()) - conn, err := grpc.NewClient("passthrough://bufnet", - grpc.WithTransportCredentials(insecure.NewCredentials()), - grpc.WithContextDialer(func(ctx context.Context, s string) (net.Conn, error) { - return lis.Dial() - })) + _, err = client.EvalFlag(t.Context(), &proto.EvalRequest{Key: "non-existing", Target: &proto.Target{Identifier: &proto.Target_SdkKey{SdkKey: key}}, User: map[string]*proto.UserValue{"id": {Value: &proto.UserValue_StringValue{StringValue: "u1"}}}}) + assert.Error(t, err) + + _ = h.SetFlags(key, map[string]*configcattest.Flag{ + "flag": { + Default: "test2", + }, + }) + _, err = client.Refresh(t.Context(), &proto.RefreshRequest{Target: &proto.Target{Identifier: &proto.Target_SdkKey{SdkKey: key}}}) assert.NoError(t, err) + + resp, err = client.EvalFlag(t.Context(), &proto.EvalRequest{Key: "flag", Target: &proto.Target{Identifier: &proto.Target_SdkKey{SdkKey: key}}, User: map[string]*proto.UserValue{"id": {Value: &proto.UserValue_StringValue{StringValue: "u1"}}}}) + assert.NoError(t, err) + + assert.Equal(t, "test2", resp.GetStringValue()) +} + +func TestGrpc_SDK_InvalidState(t *testing.T) { + conn := createFlagServiceConnWithManualRegistrar(t, "http://localhost", configcattest.RandomSDKKey()) defer func() { _ = conn.Close() }() client := proto.NewFlagServiceClient(conn) - _, err = client.EvalFlag(context.Background(), &proto.EvalRequest{Key: "flag", SdkId: "test"}) - assert.ErrorContains(t, err, "sdk with identifier 'test' is in an invalid state; please check the logs for more details") + _, err := client.EvalFlag(t.Context(), &proto.EvalRequest{Key: "flag", Target: &proto.Target{Identifier: &proto.Target_SdkId{SdkId: "test"}}}) + assert.ErrorContains(t, err, "requested SDK is in an invalid state; please check the logs for more details") - _, err = client.EvalAllFlags(context.Background(), &proto.EvalRequest{Key: "flag", SdkId: "test"}) - assert.ErrorContains(t, err, "sdk with identifier 'test' is in an invalid state; please check the logs for more details") + _, err = client.EvalAllFlags(t.Context(), &proto.EvalRequest{Key: "flag", Target: &proto.Target{Identifier: &proto.Target_SdkId{SdkId: "test"}}}) + assert.ErrorContains(t, err, "requested SDK is in an invalid state; please check the logs for more details") - cl, err := client.EvalFlagStream(context.Background(), &proto.EvalRequest{Key: "flag", SdkId: "test"}) + cl, err := client.EvalFlagStream(t.Context(), &proto.EvalRequest{Key: "flag", Target: &proto.Target{Identifier: &proto.Target_SdkId{SdkId: "test"}}}) testutils.WithTimeout(2*time.Second, func() { _, err = cl.Recv() - assert.ErrorContains(t, err, "sdk with identifier 'test' is in an invalid state; please check the logs for more details") + assert.ErrorContains(t, err, "requested SDK is in an invalid state; please check the logs for more details") }) - cl1, err := client.EvalAllFlagsStream(context.Background(), &proto.EvalRequest{Key: "flag", SdkId: "test"}) + cl1, err := client.EvalAllFlagsStream(t.Context(), &proto.EvalRequest{Key: "flag", Target: &proto.Target{Identifier: &proto.Target_SdkId{SdkId: "test"}}}) testutils.WithTimeout(2*time.Second, func() { _, err = cl1.Recv() - assert.ErrorContains(t, err, "sdk with identifier 'test' is in an invalid state; please check the logs for more details") + assert.ErrorContains(t, err, "requested SDK is in an invalid state; please check the logs for more details") }) - _, err = client.GetKeys(context.Background(), &proto.KeysRequest{SdkId: "test"}) - assert.ErrorContains(t, err, "sdk with identifier 'test' is in an invalid state; please check the logs for more details") + _, err = client.GetKeys(t.Context(), &proto.KeysRequest{Target: &proto.Target{Identifier: &proto.Target_SdkId{SdkId: "test"}}}) + assert.ErrorContains(t, err, "requested SDK is in an invalid state; please check the logs for more details") } func TestGrpc_Invalid_SdkKey(t *testing.T) { - key := configcattest.RandomSDKKey() - var h configcattest.Handler - _ = h.SetFlags(key, map[string]*configcattest.Flag{ + _, key, url := newFlagServer(t, map[string]*configcattest.Flag{ "flag1": { Default: "test1", }, }) - sdkSrv := httptest.NewServer(&h) - defer sdkSrv.Close() - reg := sdk.NewTestRegistrar(&config.SDKConfig{BaseUrl: sdkSrv.URL, Key: key}, nil) - defer reg.Close() - flagSrv := newFlagService(reg, nil, log.NewNullLogger()) - lis := bufconn.Listen(1024 * 1024) - srv := grpc.NewServer() - defer srv.GracefulStop() - proto.RegisterFlagServiceServer(srv, flagSrv) - go func() { - _ = srv.Serve(lis) + conn := createFlagServiceConnWithManualRegistrar(t, url, key) + defer func() { + _ = conn.Close() }() - conn, err := grpc.NewClient("passthrough://bufnet", - grpc.WithTransportCredentials(insecure.NewCredentials()), - grpc.WithContextDialer(func(ctx context.Context, s string) (net.Conn, error) { - return lis.Dial() - })) + client := proto.NewFlagServiceClient(conn) - assert.NoError(t, err) + _, err := client.EvalFlag(t.Context(), &proto.EvalRequest{Key: "flag", Target: &proto.Target{Identifier: &proto.Target_SdkId{SdkId: "non-existing"}}}) + assert.ErrorContains(t, err, "could not identify a configured SDK") + + _, err = client.EvalAllFlags(t.Context(), &proto.EvalRequest{Key: "flag", Target: &proto.Target{Identifier: &proto.Target_SdkId{SdkId: "non-existing"}}}) + assert.ErrorContains(t, err, "could not identify a configured SDK") + + cl, err := client.EvalFlagStream(t.Context(), &proto.EvalRequest{Key: "flag", Target: &proto.Target{Identifier: &proto.Target_SdkId{SdkId: "non-existing"}}}) + testutils.WithTimeout(2*time.Second, func() { + _, err = cl.Recv() + assert.ErrorContains(t, err, "could not identify a configured SDK") + }) + + cl1, err := client.EvalAllFlagsStream(t.Context(), &proto.EvalRequest{Key: "flag", Target: &proto.Target{Identifier: &proto.Target_SdkId{SdkId: "non-existing"}}}) + testutils.WithTimeout(2*time.Second, func() { + _, err = cl1.Recv() + assert.ErrorContains(t, err, "could not identify a configured SDK") + }) + + _, err = client.GetKeys(t.Context(), &proto.KeysRequest{Target: &proto.Target{Identifier: &proto.Target_SdkId{SdkId: "non-existing"}}}) + assert.ErrorContains(t, err, "could not identify a configured SDK") + + _, err = client.Refresh(t.Context(), &proto.RefreshRequest{Target: &proto.Target{Identifier: &proto.Target_SdkId{SdkId: "non-existing"}}}) + assert.ErrorContains(t, err, "could not identify a configured SDK") +} + +func TestGrpc_Invalid_Target(t *testing.T) { + _, key, url := newFlagServer(t, map[string]*configcattest.Flag{ + "flag1": { + Default: "test1", + }, + }) + conn := createFlagServiceConnWithManualRegistrar(t, url, key) defer func() { _ = conn.Close() }() client := proto.NewFlagServiceClient(conn) - _, err = client.EvalFlag(context.Background(), &proto.EvalRequest{Key: "flag", SdkId: "non-existing"}) - assert.ErrorContains(t, err, "sdk not found for identifier: 'non-existing'") + _, err := client.EvalFlag(t.Context(), &proto.EvalRequest{Key: "flag"}) + assert.ErrorContains(t, err, "either the sdk id or the sdk key parameter must be set") - _, err = client.EvalAllFlags(context.Background(), &proto.EvalRequest{Key: "flag", SdkId: "non-existing"}) - assert.ErrorContains(t, err, "sdk not found for identifier: 'non-existing'") + _, err = client.EvalAllFlags(t.Context(), &proto.EvalRequest{Key: "flag", Target: &proto.Target{}}) + assert.ErrorContains(t, err, "either the sdk id or the sdk key parameter must be set") - cl, err := client.EvalFlagStream(context.Background(), &proto.EvalRequest{Key: "flag", SdkId: "non-existing"}) + cl, err := client.EvalFlagStream(t.Context(), &proto.EvalRequest{Key: "flag", Target: &proto.Target{}}) testutils.WithTimeout(2*time.Second, func() { _, err = cl.Recv() - assert.ErrorContains(t, err, "sdk not found for identifier: 'non-existing'") + assert.ErrorContains(t, err, "either the sdk id or the sdk key parameter must be set") }) - cl1, err := client.EvalAllFlagsStream(context.Background(), &proto.EvalRequest{Key: "flag", SdkId: "non-existing"}) + cl1, err := client.EvalAllFlagsStream(t.Context(), &proto.EvalRequest{Key: "flag", Target: &proto.Target{}}) testutils.WithTimeout(2*time.Second, func() { _, err = cl1.Recv() - assert.ErrorContains(t, err, "sdk not found for identifier: 'non-existing'") + assert.ErrorContains(t, err, "either the sdk id or the sdk key parameter must be set") }) - _, err = client.GetKeys(context.Background(), &proto.KeysRequest{SdkId: "non-existing"}) - assert.ErrorContains(t, err, "sdk not found for identifier: 'non-existing'") + _, err = client.GetKeys(t.Context(), &proto.KeysRequest{Target: &proto.Target{}}) + assert.ErrorContains(t, err, "either the sdk id or the sdk key parameter must be set") - _, err = client.Refresh(context.Background(), &proto.RefreshRequest{SdkId: "non-existing"}) - assert.ErrorContains(t, err, "sdk not found for identifier: 'non-existing'") + _, err = client.Refresh(t.Context(), &proto.RefreshRequest{Target: &proto.Target{}}) + assert.ErrorContains(t, err, "either the sdk id or the sdk key parameter must be set") } func TestGrpc_Invalid_FlagKey(t *testing.T) { - key := configcattest.RandomSDKKey() - var h configcattest.Handler - _ = h.SetFlags(key, map[string]*configcattest.Flag{ + _, key, url := newFlagServer(t, map[string]*configcattest.Flag{ "flag1": { Default: "test1", }, }) - sdkSrv := httptest.NewServer(&h) - defer sdkSrv.Close() - reg := sdk.NewTestRegistrar(&config.SDKConfig{BaseUrl: sdkSrv.URL, Key: key}, nil) - defer reg.Close() - flagSrv := newFlagService(reg, nil, log.NewNullLogger()) - lis := bufconn.Listen(1024 * 1024) - srv := grpc.NewServer() - defer srv.GracefulStop() - proto.RegisterFlagServiceServer(srv, flagSrv) - go func() { - _ = srv.Serve(lis) - }() - - conn, err := grpc.NewClient("passthrough://bufnet", - grpc.WithTransportCredentials(insecure.NewCredentials()), - grpc.WithContextDialer(func(ctx context.Context, s string) (net.Conn, error) { - return lis.Dial() - })) - - assert.NoError(t, err) + conn := createFlagServiceConnWithManualRegistrar(t, url, key) defer func() { _ = conn.Close() }() client := proto.NewFlagServiceClient(conn) - _, err = client.EvalFlag(context.Background(), &proto.EvalRequest{Key: "flag", SdkId: "test"}) + _, err := client.EvalFlag(t.Context(), &proto.EvalRequest{Key: "flag", Target: &proto.Target{Identifier: &proto.Target_SdkId{SdkId: "test"}}}) assert.ErrorContains(t, err, "feature flag or setting with key 'flag' not found") - cl, err := client.EvalFlagStream(context.Background(), &proto.EvalRequest{Key: "flag", SdkId: "test"}) + cl, err := client.EvalFlagStream(t.Context(), &proto.EvalRequest{Key: "flag", Target: &proto.Target{Identifier: &proto.Target_SdkId{SdkId: "test"}}}) testutils.WithTimeout(2*time.Second, func() { _, err = cl.Recv() assert.ErrorContains(t, err, "feature flag or setting with key 'flag' not found") @@ -462,9 +438,7 @@ func TestGrpc_Invalid_FlagKey(t *testing.T) { } func TestGrpc_EvalAllFlags(t *testing.T) { - key := configcattest.RandomSDKKey() - var h configcattest.Handler - _ = h.SetFlags(key, map[string]*configcattest.Flag{ + h, key, url := newFlagServer(t, map[string]*configcattest.Flag{ "flag1": { Default: "test1", }, @@ -472,36 +446,13 @@ func TestGrpc_EvalAllFlags(t *testing.T) { Default: "test2", }, }) - sdkSrv := httptest.NewServer(&h) - defer sdkSrv.Close() - - reg := sdk.NewTestRegistrar(&config.SDKConfig{BaseUrl: sdkSrv.URL, Key: key, PollInterval: 1}, nil) - defer reg.Close() - flagSrv := newFlagService(reg, nil, log.NewNullLogger()) - - lis := bufconn.Listen(1024 * 1024) - - srv := grpc.NewServer() - defer srv.GracefulStop() - - proto.RegisterFlagServiceServer(srv, flagSrv) - go func() { - _ = srv.Serve(lis) - }() - - conn, err := grpc.NewClient("passthrough://bufnet", - grpc.WithTransportCredentials(insecure.NewCredentials()), - grpc.WithContextDialer(func(ctx context.Context, s string) (net.Conn, error) { - return lis.Dial() - })) - - assert.NoError(t, err) + conn := createFlagServiceConnWithManualRegistrar(t, url, key) defer func() { _ = conn.Close() }() client := proto.NewFlagServiceClient(conn) - resp, err := client.EvalAllFlags(context.Background(), &proto.EvalRequest{Key: "flag", SdkId: "test", User: map[string]*proto.UserValue{"id": {Value: &proto.UserValue_StringValue{StringValue: "u1"}}}}) + resp, err := client.EvalAllFlags(t.Context(), &proto.EvalRequest{Key: "flag", Target: &proto.Target{Identifier: &proto.Target_SdkId{SdkId: "test"}}, User: map[string]*proto.UserValue{"id": {Value: &proto.UserValue_StringValue{StringValue: "u1"}}}}) assert.NoError(t, err) assert.Equal(t, 2, len(resp.GetValues())) @@ -520,10 +471,10 @@ func TestGrpc_EvalAllFlags(t *testing.T) { }, }) - _, err = client.Refresh(context.Background(), &proto.RefreshRequest{SdkId: "test"}) + _, err = client.Refresh(t.Context(), &proto.RefreshRequest{Target: &proto.Target{Identifier: &proto.Target_SdkId{SdkId: "test"}}}) assert.NoError(t, err) - resp, err = client.EvalAllFlags(context.Background(), &proto.EvalRequest{Key: "flag", SdkId: "test", User: map[string]*proto.UserValue{"id": {Value: &proto.UserValue_StringValue{StringValue: "u1"}}}}) + resp, err = client.EvalAllFlags(t.Context(), &proto.EvalRequest{Key: "flag", Target: &proto.Target{Identifier: &proto.Target_SdkId{SdkId: "test"}}, User: map[string]*proto.UserValue{"id": {Value: &proto.UserValue_StringValue{StringValue: "u1"}}}}) assert.NoError(t, err) assert.Equal(t, 3, len(resp.GetValues())) @@ -533,9 +484,7 @@ func TestGrpc_EvalAllFlags(t *testing.T) { } func TestGrpc_GetKeys(t *testing.T) { - key := configcattest.RandomSDKKey() - var h configcattest.Handler - _ = h.SetFlags(key, map[string]*configcattest.Flag{ + h, key, url := newFlagServer(t, map[string]*configcattest.Flag{ "flag1": { Default: "test1", }, @@ -543,36 +492,59 @@ func TestGrpc_GetKeys(t *testing.T) { Default: "test2", }, }) - sdkSrv := httptest.NewServer(&h) - defer sdkSrv.Close() - - reg := sdk.NewTestRegistrar(&config.SDKConfig{BaseUrl: sdkSrv.URL, Key: key, PollInterval: 1}, nil) - defer reg.Close() - flagSrv := newFlagService(reg, nil, log.NewNullLogger()) + conn := createFlagServiceConnWithManualRegistrar(t, url, key) + defer func() { + _ = conn.Close() + }() - lis := bufconn.Listen(1024 * 1024) + client := proto.NewFlagServiceClient(conn) + resp, err := client.GetKeys(t.Context(), &proto.KeysRequest{Target: &proto.Target{Identifier: &proto.Target_SdkId{SdkId: "test"}}}) + assert.NoError(t, err) - srv := grpc.NewServer() - defer srv.GracefulStop() + assert.Equal(t, 2, len(resp.GetKeys())) + assert.Equal(t, "flag1", resp.GetKeys()[0]) + assert.Equal(t, "flag2", resp.GetKeys()[1]) - proto.RegisterFlagServiceServer(srv, flagSrv) - go func() { - _ = srv.Serve(lis) - }() + _ = h.SetFlags(key, map[string]*configcattest.Flag{ + "flag1": { + Default: "test1", + }, + "flag2": { + Default: "test2", + }, + "flag3": { + Default: "test3", + }, + }) - conn, err := grpc.NewClient("passthrough://bufnet", - grpc.WithTransportCredentials(insecure.NewCredentials()), - grpc.WithContextDialer(func(ctx context.Context, s string) (net.Conn, error) { - return lis.Dial() - })) + _, err = client.Refresh(t.Context(), &proto.RefreshRequest{Target: &proto.Target{Identifier: &proto.Target_SdkId{SdkId: "test"}}}) + assert.NoError(t, err) + resp, err = client.GetKeys(t.Context(), &proto.KeysRequest{Target: &proto.Target{Identifier: &proto.Target_SdkId{SdkId: "test"}}}) assert.NoError(t, err) + + assert.Equal(t, 3, len(resp.GetKeys())) + assert.Equal(t, "flag1", resp.GetKeys()[0]) + assert.Equal(t, "flag2", resp.GetKeys()[1]) + assert.Equal(t, "flag3", resp.GetKeys()[2]) +} + +func TestGrpc_GetKeys_Old(t *testing.T) { + h, key, url := newFlagServer(t, map[string]*configcattest.Flag{ + "flag1": { + Default: "test1", + }, + "flag2": { + Default: "test2", + }, + }) + conn := createFlagServiceConnWithManualRegistrar(t, url, key) defer func() { _ = conn.Close() }() client := proto.NewFlagServiceClient(conn) - resp, err := client.GetKeys(context.Background(), &proto.KeysRequest{SdkId: "test"}) + resp, err := client.GetKeys(t.Context(), &proto.KeysRequest{SdkId: "test"}) assert.NoError(t, err) assert.Equal(t, 2, len(resp.GetKeys())) @@ -591,10 +563,10 @@ func TestGrpc_GetKeys(t *testing.T) { }, }) - _, err = client.Refresh(context.Background(), &proto.RefreshRequest{SdkId: "test"}) + _, err = client.Refresh(t.Context(), &proto.RefreshRequest{SdkId: "test"}) assert.NoError(t, err) - resp, err = client.GetKeys(context.Background(), &proto.KeysRequest{SdkId: "test"}) + resp, err = client.GetKeys(t.Context(), &proto.KeysRequest{SdkId: "test"}) assert.NoError(t, err) assert.Equal(t, 3, len(resp.GetKeys())) @@ -602,3 +574,50 @@ func TestGrpc_GetKeys(t *testing.T) { assert.Equal(t, "flag2", resp.GetKeys()[1]) assert.Equal(t, "flag3", resp.GetKeys()[2]) } + +func newFlagServer(t *testing.T, flags map[string]*configcattest.Flag) (h *configcattest.Handler, sdkKey string, srvUrl string) { + key := configcattest.RandomSDKKey() + var ha configcattest.Handler + _ = ha.SetFlags(key, flags) + sdkSrv := httptest.NewServer(&ha) + t.Cleanup(func() { + sdkSrv.Close() + }) + return &ha, key, sdkSrv.URL +} + +func createFlagServiceConnWithManualRegistrar(t *testing.T, url string, key string) *grpc.ClientConn { + reg := sdk.NewTestRegistrar(&config.SDKConfig{BaseUrl: url, Key: key, PollInterval: 1}, nil) + t.Cleanup(func() { + reg.Close() + }) + return createFlagServiceConn(t, reg) +} + +func createFlagServiceConnWithAutoRegistrar(t *testing.T) (sdk.AutoRegistrar, *grpc.ClientConn, *sdk.TestSdkRegistrarHandler) { + reg, h, _ := sdk.NewTestAutoRegistrarWithAutoConfig(t, config.ProfileConfig{PollInterval: 60}, log.NewNullLogger()) + return reg, createFlagServiceConn(t, reg), h +} + +func createFlagServiceConn(t *testing.T, registrar sdk.Registrar) *grpc.ClientConn { + flagSrv := newFlagService(registrar, telemetry.NewEmptyReporter(), log.NewNullLogger()) + lis := bufconn.Listen(1024 * 1024) + srv := grpc.NewServer() + + proto.RegisterFlagServiceServer(srv, flagSrv) + go func() { + _ = srv.Serve(lis) + }() + + conn, _ := grpc.NewClient("passthrough://bufnet", + grpc.WithTransportCredentials(insecure.NewCredentials()), + grpc.WithContextDialer(func(ctx context.Context, s string) (net.Conn, error) { + return lis.Dial() + })) + + t.Cleanup(func() { + srv.GracefulStop() + }) + + return conn +} diff --git a/grpc/mware_test.go b/grpc/mware_test.go index 24c2684..62e064f 100644 --- a/grpc/mware_test.go +++ b/grpc/mware_test.go @@ -18,7 +18,7 @@ func TestDebug_UnaryInterceptor(t *testing.T) { l := log.NewLogger(&errBuf, &out, log.Debug) addr := net.IPNet{IP: net.IPv4(127, 0, 0, 1), Mask: net.IPv4Mask(255, 255, 255, 255)} - ctx := peer.NewContext(context.Background(), &peer.Peer{Addr: &addr}) + ctx := peer.NewContext(t.Context(), &peer.Peer{Addr: &addr}) md := metadata.Pairs("user-agent", "test-agent") ctx = metadata.NewIncomingContext(ctx, md) @@ -41,7 +41,7 @@ func TestDebug_StreamInterceptor(t *testing.T) { l := log.NewLogger(&errBuf, &out, log.Debug) addr := net.IPNet{IP: net.IPv4(127, 0, 0, 1), Mask: net.IPv4Mask(255, 255, 255, 255)} - ctx := peer.NewContext(context.Background(), &peer.Peer{Addr: &addr}) + ctx := peer.NewContext(t.Context(), &peer.Peer{Addr: &addr}) md := metadata.Pairs("user-agent", "test-agent") ctx = metadata.NewIncomingContext(ctx, md) diff --git a/grpc/proto/flag_service.pb.go b/grpc/proto/flag_service.pb.go index 24b4818..af2cf0d 100644 --- a/grpc/proto/flag_service.pb.go +++ b/grpc/proto/flag_service.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.32.0 -// protoc v4.25.1 +// protoc v5.26.0 // source: flag_service.proto package proto @@ -28,12 +28,16 @@ type EvalRequest struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - // The SDK identifier. + // The SDK identifier. Deprecated, the field `target` should be used instead for SDK identification. + // + // Deprecated: Marked as deprecated in flag_service.proto. SdkId string `protobuf:"bytes,1,opt,name=sdk_id,json=sdkId,proto3" json:"sdk_id,omitempty"` // The feature flag's key to evaluate. Key string `protobuf:"bytes,2,opt,name=key,proto3" json:"key,omitempty"` // The user object. User map[string]*UserValue `protobuf:"bytes,3,rep,name=user,proto3" json:"user,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` + // The evaluation request's target. + Target *Target `protobuf:"bytes,4,opt,name=target,proto3" json:"target,omitempty"` } func (x *EvalRequest) Reset() { @@ -68,6 +72,7 @@ func (*EvalRequest) Descriptor() ([]byte, []int) { return file_flag_service_proto_rawDescGZIP(), []int{0} } +// Deprecated: Marked as deprecated in flag_service.proto. func (x *EvalRequest) GetSdkId() string { if x != nil { return x.SdkId @@ -89,6 +94,13 @@ func (x *EvalRequest) GetUser() map[string]*UserValue { return nil } +func (x *EvalRequest) GetTarget() *Target { + if x != nil { + return x.Target + } + return nil +} + // Feature flag evaluation response message. type EvalResponse struct { state protoimpl.MessageState @@ -265,8 +277,12 @@ type KeysRequest struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - // The SDK identifier. + // The SDK identifier. Deprecated, the field `target` should be used instead for SDK identification. + // + // Deprecated: Marked as deprecated in flag_service.proto. SdkId string `protobuf:"bytes,1,opt,name=sdk_id,json=sdkId,proto3" json:"sdk_id,omitempty"` + // The request's target. + Target *Target `protobuf:"bytes,2,opt,name=target,proto3" json:"target,omitempty"` } func (x *KeysRequest) Reset() { @@ -301,6 +317,7 @@ func (*KeysRequest) Descriptor() ([]byte, []int) { return file_flag_service_proto_rawDescGZIP(), []int{3} } +// Deprecated: Marked as deprecated in flag_service.proto. func (x *KeysRequest) GetSdkId() string { if x != nil { return x.SdkId @@ -308,6 +325,13 @@ func (x *KeysRequest) GetSdkId() string { return "" } +func (x *KeysRequest) GetTarget() *Target { + if x != nil { + return x.Target + } + return nil +} + // Response message that contains each available feature flag's key. type KeysResponse struct { state protoimpl.MessageState @@ -363,8 +387,12 @@ type RefreshRequest struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - // The SDK identifier. + // The SDK identifier. Deprecated, the field `target` should be used instead for SDK identification. + // + // Deprecated: Marked as deprecated in flag_service.proto. SdkId string `protobuf:"bytes,1,opt,name=sdk_id,json=sdkId,proto3" json:"sdk_id,omitempty"` + // The request's target. + Target *Target `protobuf:"bytes,2,opt,name=target,proto3" json:"target,omitempty"` } func (x *RefreshRequest) Reset() { @@ -399,6 +427,7 @@ func (*RefreshRequest) Descriptor() ([]byte, []int) { return file_flag_service_proto_rawDescGZIP(), []int{5} } +// Deprecated: Marked as deprecated in flag_service.proto. func (x *RefreshRequest) GetSdkId() string { if x != nil { return x.SdkId @@ -406,6 +435,13 @@ func (x *RefreshRequest) GetSdkId() string { return "" } +func (x *RefreshRequest) GetTarget() *Target { + if x != nil { + return x.Target + } + return nil +} + // Defines the possible values that can be set in the `user` map. type UserValue struct { state protoimpl.MessageState @@ -516,6 +552,90 @@ func (*UserValue_TimeValue) isUserValue_Value() {} func (*UserValue_StringListValue) isUserValue_Value() {} +// Identifies the target of evaluation requests by either an SDK Id or an SDK Key. +type Target struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Types that are assignable to Identifier: + // + // *Target_SdkId + // *Target_SdkKey + Identifier isTarget_Identifier `protobuf_oneof:"identifier"` +} + +func (x *Target) Reset() { + *x = Target{} + if protoimpl.UnsafeEnabled { + mi := &file_flag_service_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Target) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Target) ProtoMessage() {} + +func (x *Target) ProtoReflect() protoreflect.Message { + mi := &file_flag_service_proto_msgTypes[7] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Target.ProtoReflect.Descriptor instead. +func (*Target) Descriptor() ([]byte, []int) { + return file_flag_service_proto_rawDescGZIP(), []int{7} +} + +func (m *Target) GetIdentifier() isTarget_Identifier { + if m != nil { + return m.Identifier + } + return nil +} + +func (x *Target) GetSdkId() string { + if x, ok := x.GetIdentifier().(*Target_SdkId); ok { + return x.SdkId + } + return "" +} + +func (x *Target) GetSdkKey() string { + if x, ok := x.GetIdentifier().(*Target_SdkKey); ok { + return x.SdkKey + } + return "" +} + +type isTarget_Identifier interface { + isTarget_Identifier() +} + +type Target_SdkId struct { + // The SDK Id. + SdkId string `protobuf:"bytes,1,opt,name=sdk_id,json=sdkId,proto3,oneof"` +} + +type Target_SdkKey struct { + // The SDK key. + SdkKey string `protobuf:"bytes,2,opt,name=sdk_key,json=sdkKey,proto3,oneof"` +} + +func (*Target_SdkId) isTarget_Identifier() {} + +func (*Target_SdkKey) isTarget_Identifier() {} + // Represents a list of strings. type StringList struct { state protoimpl.MessageState @@ -528,7 +648,7 @@ type StringList struct { func (x *StringList) Reset() { *x = StringList{} if protoimpl.UnsafeEnabled { - mi := &file_flag_service_proto_msgTypes[7] + mi := &file_flag_service_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -541,7 +661,7 @@ func (x *StringList) String() string { func (*StringList) ProtoMessage() {} func (x *StringList) ProtoReflect() protoreflect.Message { - mi := &file_flag_service_proto_msgTypes[7] + mi := &file_flag_service_proto_msgTypes[8] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -554,7 +674,7 @@ func (x *StringList) ProtoReflect() protoreflect.Message { // Deprecated: Use StringList.ProtoReflect.Descriptor instead. func (*StringList) Descriptor() ([]byte, []int) { - return file_flag_service_proto_rawDescGZIP(), []int{7} + return file_flag_service_proto_rawDescGZIP(), []int{8} } func (x *StringList) GetValues() []string { @@ -572,96 +692,110 @@ var file_flag_service_proto_rawDesc = []byte{ 0x1b, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x65, 0x6d, 0x70, 0x74, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, - 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xbb, 0x01, - 0x0a, 0x0b, 0x45, 0x76, 0x61, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x15, 0x0a, - 0x06, 0x73, 0x64, 0x6b, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x73, - 0x64, 0x6b, 0x49, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x34, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x03, - 0x20, 0x03, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x63, 0x61, 0x74, - 0x2e, 0x45, 0x76, 0x61, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x55, 0x73, 0x65, - 0x72, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x1a, 0x4d, 0x0a, 0x09, - 0x55, 0x73, 0x65, 0x72, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x2a, 0x0a, 0x05, 0x76, - 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x63, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x63, 0x61, 0x74, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x56, 0x61, 0x6c, 0x75, 0x65, - 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0xc4, 0x01, 0x0a, 0x0c, - 0x45, 0x76, 0x61, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1d, 0x0a, 0x09, - 0x69, 0x6e, 0x74, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x48, - 0x00, 0x52, 0x08, 0x69, 0x6e, 0x74, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x23, 0x0a, 0x0c, 0x64, - 0x6f, 0x75, 0x62, 0x6c, 0x65, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x01, 0x48, 0x00, 0x52, 0x0b, 0x64, 0x6f, 0x75, 0x62, 0x6c, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, - 0x12, 0x23, 0x0a, 0x0c, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x0b, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, - 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x1f, 0x0a, 0x0a, 0x62, 0x6f, 0x6f, 0x6c, 0x5f, 0x76, 0x61, - 0x6c, 0x75, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x09, 0x62, 0x6f, 0x6f, - 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x76, 0x61, 0x72, 0x69, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x76, 0x61, - 0x72, 0x69, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x42, 0x07, 0x0a, 0x05, 0x76, 0x61, 0x6c, - 0x75, 0x65, 0x22, 0xa5, 0x01, 0x0a, 0x0f, 0x45, 0x76, 0x61, 0x6c, 0x41, 0x6c, 0x6c, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3e, 0x0a, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, - 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x63, - 0x61, 0x74, 0x2e, 0x45, 0x76, 0x61, 0x6c, 0x41, 0x6c, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x2e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x06, - 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x1a, 0x52, 0x0a, 0x0b, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, - 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x2d, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x63, - 0x61, 0x74, 0x2e, 0x45, 0x76, 0x61, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x52, - 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x24, 0x0a, 0x0b, 0x4b, 0x65, - 0x79, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x15, 0x0a, 0x06, 0x73, 0x64, 0x6b, - 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x73, 0x64, 0x6b, 0x49, 0x64, - 0x22, 0x22, 0x0a, 0x0c, 0x4b, 0x65, 0x79, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x12, 0x12, 0x0a, 0x04, 0x6b, 0x65, 0x79, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x04, - 0x6b, 0x65, 0x79, 0x73, 0x22, 0x27, 0x0a, 0x0e, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x15, 0x0a, 0x06, 0x73, 0x64, 0x6b, 0x5f, 0x69, 0x64, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x73, 0x64, 0x6b, 0x49, 0x64, 0x22, 0xe0, 0x01, - 0x0a, 0x09, 0x55, 0x73, 0x65, 0x72, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x23, 0x0a, 0x0c, 0x6e, - 0x75, 0x6d, 0x62, 0x65, 0x72, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x01, 0x48, 0x00, 0x52, 0x0b, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x56, 0x61, 0x6c, 0x75, 0x65, - 0x12, 0x23, 0x0a, 0x0c, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x0b, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, - 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x3b, 0x0a, 0x0a, 0x74, 0x69, 0x6d, 0x65, 0x5f, 0x76, 0x61, - 0x6c, 0x75, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, - 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, - 0x73, 0x74, 0x61, 0x6d, 0x70, 0x48, 0x00, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x56, 0x61, 0x6c, - 0x75, 0x65, 0x12, 0x43, 0x0a, 0x11, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x5f, 0x6c, 0x69, 0x73, - 0x74, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, - 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x63, 0x61, 0x74, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, - 0x4c, 0x69, 0x73, 0x74, 0x48, 0x00, 0x52, 0x0f, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x4c, 0x69, - 0x73, 0x74, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x07, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, - 0x22, 0x24, 0x0a, 0x0a, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x16, - 0x0a, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, - 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x32, 0xa5, 0x03, 0x0a, 0x0b, 0x46, 0x6c, 0x61, 0x67, 0x53, - 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x45, 0x0a, 0x0e, 0x45, 0x76, 0x61, 0x6c, 0x46, 0x6c, - 0x61, 0x67, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x12, 0x16, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, + 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xea, 0x01, + 0x0a, 0x0b, 0x45, 0x76, 0x61, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x19, 0x0a, + 0x06, 0x73, 0x64, 0x6b, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x02, 0x18, + 0x01, 0x52, 0x05, 0x73, 0x64, 0x6b, 0x49, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x34, 0x0a, 0x04, 0x75, 0x73, + 0x65, 0x72, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x63, 0x61, 0x74, 0x2e, 0x45, 0x76, 0x61, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x17, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x63, 0x61, 0x74, 0x2e, 0x45, 0x76, 0x61, - 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x30, 0x01, 0x12, 0x4c, 0x0a, - 0x12, 0x45, 0x76, 0x61, 0x6c, 0x41, 0x6c, 0x6c, 0x46, 0x6c, 0x61, 0x67, 0x73, 0x53, 0x74, 0x72, - 0x65, 0x61, 0x6d, 0x12, 0x16, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x63, 0x61, 0x74, 0x2e, + 0x2e, 0x55, 0x73, 0x65, 0x72, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, + 0x12, 0x29, 0x0a, 0x06, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x11, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x63, 0x61, 0x74, 0x2e, 0x54, 0x61, 0x72, + 0x67, 0x65, 0x74, 0x52, 0x06, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x1a, 0x4d, 0x0a, 0x09, 0x55, + 0x73, 0x65, 0x72, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x2a, 0x0a, 0x05, 0x76, 0x61, + 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x63, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x63, 0x61, 0x74, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, + 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0xc4, 0x01, 0x0a, 0x0c, 0x45, + 0x76, 0x61, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1d, 0x0a, 0x09, 0x69, + 0x6e, 0x74, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x48, 0x00, + 0x52, 0x08, 0x69, 0x6e, 0x74, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x23, 0x0a, 0x0c, 0x64, 0x6f, + 0x75, 0x62, 0x6c, 0x65, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x01, + 0x48, 0x00, 0x52, 0x0b, 0x64, 0x6f, 0x75, 0x62, 0x6c, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, + 0x23, 0x0a, 0x0c, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x0b, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, + 0x61, 0x6c, 0x75, 0x65, 0x12, 0x1f, 0x0a, 0x0a, 0x62, 0x6f, 0x6f, 0x6c, 0x5f, 0x76, 0x61, 0x6c, + 0x75, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x09, 0x62, 0x6f, 0x6f, 0x6c, + 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x76, 0x61, 0x72, 0x69, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x76, 0x61, 0x72, + 0x69, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x42, 0x07, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x22, 0xa5, 0x01, 0x0a, 0x0f, 0x45, 0x76, 0x61, 0x6c, 0x41, 0x6c, 0x6c, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3e, 0x0a, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, + 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x63, 0x61, + 0x74, 0x2e, 0x45, 0x76, 0x61, 0x6c, 0x41, 0x6c, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x2e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x06, 0x76, + 0x61, 0x6c, 0x75, 0x65, 0x73, 0x1a, 0x52, 0x0a, 0x0b, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x45, + 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x2d, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x63, 0x61, + 0x74, 0x2e, 0x45, 0x76, 0x61, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x52, 0x05, + 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x53, 0x0a, 0x0b, 0x4b, 0x65, 0x79, + 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x19, 0x0a, 0x06, 0x73, 0x64, 0x6b, 0x5f, + 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x02, 0x18, 0x01, 0x52, 0x05, 0x73, 0x64, + 0x6b, 0x49, 0x64, 0x12, 0x29, 0x0a, 0x06, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x63, 0x61, 0x74, 0x2e, + 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x52, 0x06, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x22, 0x22, + 0x0a, 0x0c, 0x4b, 0x65, 0x79, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x12, + 0x0a, 0x04, 0x6b, 0x65, 0x79, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x04, 0x6b, 0x65, + 0x79, 0x73, 0x22, 0x56, 0x0a, 0x0e, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x19, 0x0a, 0x06, 0x73, 0x64, 0x6b, 0x5f, 0x69, 0x64, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x42, 0x02, 0x18, 0x01, 0x52, 0x05, 0x73, 0x64, 0x6b, 0x49, 0x64, 0x12, + 0x29, 0x0a, 0x06, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x11, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x63, 0x61, 0x74, 0x2e, 0x54, 0x61, 0x72, 0x67, + 0x65, 0x74, 0x52, 0x06, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x22, 0xe0, 0x01, 0x0a, 0x09, 0x55, + 0x73, 0x65, 0x72, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x23, 0x0a, 0x0c, 0x6e, 0x75, 0x6d, 0x62, + 0x65, 0x72, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x01, 0x48, 0x00, + 0x52, 0x0b, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x23, 0x0a, + 0x0c, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x0b, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, + 0x75, 0x65, 0x12, 0x3b, 0x0a, 0x0a, 0x74, 0x69, 0x6d, 0x65, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, + 0x6d, 0x70, 0x48, 0x00, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, + 0x43, 0x0a, 0x11, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x5f, 0x6c, 0x69, 0x73, 0x74, 0x5f, 0x76, + 0x61, 0x6c, 0x75, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x63, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x63, 0x61, 0x74, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x4c, 0x69, 0x73, + 0x74, 0x48, 0x00, 0x52, 0x0f, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x4c, 0x69, 0x73, 0x74, 0x56, + 0x61, 0x6c, 0x75, 0x65, 0x42, 0x07, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x4a, 0x0a, + 0x06, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x12, 0x17, 0x0a, 0x06, 0x73, 0x64, 0x6b, 0x5f, 0x69, + 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x05, 0x73, 0x64, 0x6b, 0x49, 0x64, + 0x12, 0x19, 0x0a, 0x07, 0x73, 0x64, 0x6b, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x09, 0x48, 0x00, 0x52, 0x06, 0x73, 0x64, 0x6b, 0x4b, 0x65, 0x79, 0x42, 0x0c, 0x0a, 0x0a, 0x69, + 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x22, 0x24, 0x0a, 0x0a, 0x53, 0x74, 0x72, + 0x69, 0x6e, 0x67, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, + 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x32, + 0xa5, 0x03, 0x0a, 0x0b, 0x46, 0x6c, 0x61, 0x67, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, + 0x45, 0x0a, 0x0e, 0x45, 0x76, 0x61, 0x6c, 0x46, 0x6c, 0x61, 0x67, 0x53, 0x74, 0x72, 0x65, 0x61, + 0x6d, 0x12, 0x16, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x63, 0x61, 0x74, 0x2e, 0x45, 0x76, + 0x61, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x17, 0x2e, 0x63, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x63, 0x61, 0x74, 0x2e, 0x45, 0x76, 0x61, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x22, 0x00, 0x30, 0x01, 0x12, 0x4c, 0x0a, 0x12, 0x45, 0x76, 0x61, 0x6c, 0x41, 0x6c, + 0x6c, 0x46, 0x6c, 0x61, 0x67, 0x73, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x12, 0x16, 0x2e, 0x63, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x63, 0x61, 0x74, 0x2e, 0x45, 0x76, 0x61, 0x6c, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x63, 0x61, 0x74, + 0x2e, 0x45, 0x76, 0x61, 0x6c, 0x41, 0x6c, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x22, 0x00, 0x30, 0x01, 0x12, 0x3d, 0x0a, 0x08, 0x45, 0x76, 0x61, 0x6c, 0x46, 0x6c, 0x61, 0x67, + 0x12, 0x16, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x63, 0x61, 0x74, 0x2e, 0x45, 0x76, 0x61, + 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x17, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x63, 0x61, 0x74, 0x2e, 0x45, 0x76, 0x61, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x22, 0x00, 0x12, 0x44, 0x0a, 0x0c, 0x45, 0x76, 0x61, 0x6c, 0x41, 0x6c, 0x6c, 0x46, 0x6c, + 0x61, 0x67, 0x73, 0x12, 0x16, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x63, 0x61, 0x74, 0x2e, 0x45, 0x76, 0x61, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x63, 0x61, 0x74, 0x2e, 0x45, 0x76, 0x61, 0x6c, 0x41, 0x6c, 0x6c, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x30, 0x01, 0x12, 0x3d, 0x0a, 0x08, 0x45, - 0x76, 0x61, 0x6c, 0x46, 0x6c, 0x61, 0x67, 0x12, 0x16, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, - 0x63, 0x61, 0x74, 0x2e, 0x45, 0x76, 0x61, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x17, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x63, 0x61, 0x74, 0x2e, 0x45, 0x76, 0x61, 0x6c, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x44, 0x0a, 0x0c, 0x45, 0x76, - 0x61, 0x6c, 0x41, 0x6c, 0x6c, 0x46, 0x6c, 0x61, 0x67, 0x73, 0x12, 0x16, 0x2e, 0x63, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x63, 0x61, 0x74, 0x2e, 0x45, 0x76, 0x61, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x63, 0x61, 0x74, 0x2e, 0x45, - 0x76, 0x61, 0x6c, 0x41, 0x6c, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, - 0x12, 0x3c, 0x0a, 0x07, 0x47, 0x65, 0x74, 0x4b, 0x65, 0x79, 0x73, 0x12, 0x16, 0x2e, 0x63, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x63, 0x61, 0x74, 0x2e, 0x4b, 0x65, 0x79, 0x73, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x17, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x63, 0x61, 0x74, 0x2e, - 0x4b, 0x65, 0x79, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x3e, - 0x0a, 0x07, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x12, 0x19, 0x2e, 0x63, 0x6f, 0x6e, 0x66, - 0x69, 0x67, 0x63, 0x61, 0x74, 0x2e, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x42, 0x31, - 0x5a, 0x2f, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x63, 0x61, 0x74, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x63, 0x61, 0x74, - 0x2d, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2f, 0x67, 0x72, 0x70, 0x63, 0x2f, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x3c, 0x0a, 0x07, 0x47, 0x65, 0x74, + 0x4b, 0x65, 0x79, 0x73, 0x12, 0x16, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x63, 0x61, 0x74, + 0x2e, 0x4b, 0x65, 0x79, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x17, 0x2e, 0x63, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x63, 0x61, 0x74, 0x2e, 0x4b, 0x65, 0x79, 0x73, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x3e, 0x0a, 0x07, 0x52, 0x65, 0x66, 0x72, 0x65, + 0x73, 0x68, 0x12, 0x19, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x63, 0x61, 0x74, 0x2e, 0x52, + 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, + 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, + 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x42, 0x31, 0x5a, 0x2f, 0x67, 0x69, 0x74, 0x68, 0x75, + 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x63, 0x61, 0x74, 0x2f, + 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x63, 0x61, 0x74, 0x2d, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2f, + 0x67, 0x72, 0x70, 0x63, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x33, } var ( @@ -676,7 +810,7 @@ func file_flag_service_proto_rawDescGZIP() []byte { return file_flag_service_proto_rawDescData } -var file_flag_service_proto_msgTypes = make([]protoimpl.MessageInfo, 10) +var file_flag_service_proto_msgTypes = make([]protoimpl.MessageInfo, 11) var file_flag_service_proto_goTypes = []interface{}{ (*EvalRequest)(nil), // 0: configcat.EvalRequest (*EvalResponse)(nil), // 1: configcat.EvalResponse @@ -685,36 +819,40 @@ var file_flag_service_proto_goTypes = []interface{}{ (*KeysResponse)(nil), // 4: configcat.KeysResponse (*RefreshRequest)(nil), // 5: configcat.RefreshRequest (*UserValue)(nil), // 6: configcat.UserValue - (*StringList)(nil), // 7: configcat.StringList - nil, // 8: configcat.EvalRequest.UserEntry - nil, // 9: configcat.EvalAllResponse.ValuesEntry - (*timestamppb.Timestamp)(nil), // 10: google.protobuf.Timestamp - (*emptypb.Empty)(nil), // 11: google.protobuf.Empty + (*Target)(nil), // 7: configcat.Target + (*StringList)(nil), // 8: configcat.StringList + nil, // 9: configcat.EvalRequest.UserEntry + nil, // 10: configcat.EvalAllResponse.ValuesEntry + (*timestamppb.Timestamp)(nil), // 11: google.protobuf.Timestamp + (*emptypb.Empty)(nil), // 12: google.protobuf.Empty } var file_flag_service_proto_depIdxs = []int32{ - 8, // 0: configcat.EvalRequest.user:type_name -> configcat.EvalRequest.UserEntry - 9, // 1: configcat.EvalAllResponse.values:type_name -> configcat.EvalAllResponse.ValuesEntry - 10, // 2: configcat.UserValue.time_value:type_name -> google.protobuf.Timestamp - 7, // 3: configcat.UserValue.string_list_value:type_name -> configcat.StringList - 6, // 4: configcat.EvalRequest.UserEntry.value:type_name -> configcat.UserValue - 1, // 5: configcat.EvalAllResponse.ValuesEntry.value:type_name -> configcat.EvalResponse - 0, // 6: configcat.FlagService.EvalFlagStream:input_type -> configcat.EvalRequest - 0, // 7: configcat.FlagService.EvalAllFlagsStream:input_type -> configcat.EvalRequest - 0, // 8: configcat.FlagService.EvalFlag:input_type -> configcat.EvalRequest - 0, // 9: configcat.FlagService.EvalAllFlags:input_type -> configcat.EvalRequest - 3, // 10: configcat.FlagService.GetKeys:input_type -> configcat.KeysRequest - 5, // 11: configcat.FlagService.Refresh:input_type -> configcat.RefreshRequest - 1, // 12: configcat.FlagService.EvalFlagStream:output_type -> configcat.EvalResponse - 2, // 13: configcat.FlagService.EvalAllFlagsStream:output_type -> configcat.EvalAllResponse - 1, // 14: configcat.FlagService.EvalFlag:output_type -> configcat.EvalResponse - 2, // 15: configcat.FlagService.EvalAllFlags:output_type -> configcat.EvalAllResponse - 4, // 16: configcat.FlagService.GetKeys:output_type -> configcat.KeysResponse - 11, // 17: configcat.FlagService.Refresh:output_type -> google.protobuf.Empty - 12, // [12:18] is the sub-list for method output_type - 6, // [6:12] is the sub-list for method input_type - 6, // [6:6] is the sub-list for extension type_name - 6, // [6:6] is the sub-list for extension extendee - 0, // [0:6] is the sub-list for field type_name + 9, // 0: configcat.EvalRequest.user:type_name -> configcat.EvalRequest.UserEntry + 7, // 1: configcat.EvalRequest.target:type_name -> configcat.Target + 10, // 2: configcat.EvalAllResponse.values:type_name -> configcat.EvalAllResponse.ValuesEntry + 7, // 3: configcat.KeysRequest.target:type_name -> configcat.Target + 7, // 4: configcat.RefreshRequest.target:type_name -> configcat.Target + 11, // 5: configcat.UserValue.time_value:type_name -> google.protobuf.Timestamp + 8, // 6: configcat.UserValue.string_list_value:type_name -> configcat.StringList + 6, // 7: configcat.EvalRequest.UserEntry.value:type_name -> configcat.UserValue + 1, // 8: configcat.EvalAllResponse.ValuesEntry.value:type_name -> configcat.EvalResponse + 0, // 9: configcat.FlagService.EvalFlagStream:input_type -> configcat.EvalRequest + 0, // 10: configcat.FlagService.EvalAllFlagsStream:input_type -> configcat.EvalRequest + 0, // 11: configcat.FlagService.EvalFlag:input_type -> configcat.EvalRequest + 0, // 12: configcat.FlagService.EvalAllFlags:input_type -> configcat.EvalRequest + 3, // 13: configcat.FlagService.GetKeys:input_type -> configcat.KeysRequest + 5, // 14: configcat.FlagService.Refresh:input_type -> configcat.RefreshRequest + 1, // 15: configcat.FlagService.EvalFlagStream:output_type -> configcat.EvalResponse + 2, // 16: configcat.FlagService.EvalAllFlagsStream:output_type -> configcat.EvalAllResponse + 1, // 17: configcat.FlagService.EvalFlag:output_type -> configcat.EvalResponse + 2, // 18: configcat.FlagService.EvalAllFlags:output_type -> configcat.EvalAllResponse + 4, // 19: configcat.FlagService.GetKeys:output_type -> configcat.KeysResponse + 12, // 20: configcat.FlagService.Refresh:output_type -> google.protobuf.Empty + 15, // [15:21] is the sub-list for method output_type + 9, // [9:15] is the sub-list for method input_type + 9, // [9:9] is the sub-list for extension type_name + 9, // [9:9] is the sub-list for extension extendee + 0, // [0:9] is the sub-list for field type_name } func init() { file_flag_service_proto_init() } @@ -808,6 +946,18 @@ func file_flag_service_proto_init() { } } file_flag_service_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Target); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_flag_service_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*StringList); i { case 0: return &v.state @@ -832,13 +982,17 @@ func file_flag_service_proto_init() { (*UserValue_TimeValue)(nil), (*UserValue_StringListValue)(nil), } + file_flag_service_proto_msgTypes[7].OneofWrappers = []interface{}{ + (*Target_SdkId)(nil), + (*Target_SdkKey)(nil), + } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_flag_service_proto_rawDesc, NumEnums: 0, - NumMessages: 10, + NumMessages: 11, NumExtensions: 0, NumServices: 1, }, diff --git a/grpc/proto/flag_service.proto b/grpc/proto/flag_service.proto index 7c959f7..c979a21 100644 --- a/grpc/proto/flag_service.proto +++ b/grpc/proto/flag_service.proto @@ -26,12 +26,14 @@ service FlagService { // Feature flag evaluation request message. message EvalRequest { - // The SDK identifier. - string sdk_id = 1; + // The SDK identifier. Deprecated, the field `target` should be used instead for SDK identification. + string sdk_id = 1 [ deprecated = true ]; // The feature flag's key to evaluate. string key = 2; // The user object. map user = 3; + // The evaluation request's target. + Target target = 4; } // Feature flag evaluation response message. @@ -55,8 +57,10 @@ message EvalAllResponse { // Request message for getting each available feature flag's key. message KeysRequest { - // The SDK identifier. - string sdk_id = 1; + // The SDK identifier. Deprecated, the field `target` should be used instead for SDK identification. + string sdk_id = 1 [ deprecated = true ]; + // The request's target. + Target target = 2; } // Response message that contains each available feature flag's key. @@ -67,8 +71,10 @@ message KeysResponse { // Request message for the given SDK to refresh its evaluation data. message RefreshRequest { - // The SDK identifier. - string sdk_id = 1; + // The SDK identifier. Deprecated, the field `target` should be used instead for SDK identification. + string sdk_id = 1 [ deprecated = true ]; + // The request's target. + Target target = 2; } // Defines the possible values that can be set in the `user` map. @@ -81,6 +87,16 @@ message UserValue { } } +// Identifies the target of evaluation requests by either an SDK Id or an SDK Key. +message Target { + oneof identifier { + // The SDK Id. + string sdk_id = 1; + // The SDK key. + string sdk_key = 2; + } +} + // Represents a list of strings. message StringList { repeated string values = 1; diff --git a/grpc/proto/flag_service_grpc.pb.go b/grpc/proto/flag_service_grpc.pb.go index 1455f6f..f8eeeb1 100644 --- a/grpc/proto/flag_service_grpc.pb.go +++ b/grpc/proto/flag_service_grpc.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.3.0 -// - protoc v4.25.1 +// - protoc v5.26.0 // source: flag_service.proto package proto diff --git a/grpc/server.go b/grpc/server.go index 59e5198..deb93ef 100644 --- a/grpc/server.go +++ b/grpc/server.go @@ -7,8 +7,8 @@ import ( "time" "github.com/configcat/configcat-proxy/config" - "github.com/configcat/configcat-proxy/diag/metrics" "github.com/configcat/configcat-proxy/diag/status" + "github.com/configcat/configcat-proxy/diag/telemetry" "github.com/configcat/configcat-proxy/grpc/proto" "github.com/configcat/configcat-proxy/log" "github.com/configcat/configcat-proxy/sdk" @@ -33,7 +33,7 @@ type Server struct { errorChannel chan error } -func NewServer(sdkRegistrar sdk.Registrar, metricsReporter metrics.Reporter, statusReporter status.Reporter, conf *config.Config, logger log.Logger, errorChan chan error) (*Server, error) { +func NewServer(sdkRegistrar sdk.Registrar, telemetryReporter telemetry.Reporter, statusReporter status.Reporter, conf *config.Config, logger log.Logger, errorChan chan error) (*Server, error) { grpcLog := logger.WithLevel(conf.Grpc.Log.GetLevel()).WithPrefix("grpc") opts := make([]grpc.ServerOption, 0) if conf.Tls.Enabled { @@ -48,9 +48,6 @@ func NewServer(sdkRegistrar sdk.Registrar, metricsReporter metrics.Reporter, sta unaryInterceptors := make([]grpc.UnaryServerInterceptor, 0) streamInterceptors := make([]grpc.StreamServerInterceptor, 0) - if metricsReporter != nil { - unaryInterceptors = append(unaryInterceptors, metrics.GrpcUnaryInterceptor(metricsReporter)) - } if grpcLog.Level() == log.Debug { unaryInterceptors = append(unaryInterceptors, DebugLogUnaryInterceptor(grpcLog)) streamInterceptors = append(streamInterceptors, DebugLogStreamInterceptor(grpcLog)) @@ -65,8 +62,9 @@ func NewServer(sdkRegistrar sdk.Registrar, metricsReporter metrics.Reporter, sta opts = append(opts, grpc.KeepaliveParams(params)) } - flagService := newFlagService(sdkRegistrar, metricsReporter, grpcLog) + flagService := newFlagService(sdkRegistrar, telemetryReporter, grpcLog) + opts = telemetryReporter.InstrumentGrpc(opts) grpcServer := grpc.NewServer(opts...) proto.RegisterFlagServiceServer(grpcServer, flagService) var healthServer *health.Server diff --git a/grpc/server_test.go b/grpc/server_test.go index f7df484..4ed4f14 100644 --- a/grpc/server_test.go +++ b/grpc/server_test.go @@ -8,8 +8,8 @@ import ( "time" "github.com/configcat/configcat-proxy/config" - "github.com/configcat/configcat-proxy/diag/metrics" "github.com/configcat/configcat-proxy/diag/status" + "github.com/configcat/configcat-proxy/diag/telemetry" "github.com/configcat/configcat-proxy/internal/testutils" "github.com/configcat/configcat-proxy/log" "github.com/configcat/configcat-proxy/sdk" @@ -35,7 +35,7 @@ func TestNewServer(t *testing.T) { conf := config.Config{Grpc: config.GrpcConfig{Port: 5061, HealthCheckEnabled: true, ServerReflectionEnabled: true, KeepAlive: config.KeepAliveConfig{Timeout: 10}}, SDKs: map[string]*config.SDKConfig{key: &sdkConf}} defer reg.Close() - srv, _ := NewServer(reg, metrics.NewReporter(), status.NewReporter(&conf.Cache), &conf, log.NewDebugLogger(), errChan) + srv, _ := NewServer(reg, telemetry.NewEmptyReporter(), status.NewReporter(&conf.Cache), &conf, log.NewDebugLogger(), errChan) wg := sync.WaitGroup{} wg.Add(1) @@ -121,7 +121,7 @@ MK4Li/LGWcksyoF+hbPNXMFCIA== conf := config.Config{Grpc: config.GrpcConfig{Port: 5062}, Tls: tlsConf, SDKs: map[string]*config.SDKConfig{key: &sdkConf}} defer reg.Close() - srv, _ := NewServer(reg, nil, status.NewReporter(&conf.Cache), &conf, log.NewNullLogger(), errChan) + srv, _ := NewServer(reg, telemetry.NewEmptyReporter(), status.NewReporter(&conf.Cache), &conf, log.NewNullLogger(), errChan) wg := sync.WaitGroup{} wg.Add(1) @@ -162,7 +162,7 @@ func TestNewServer_TLS_Missing_Cert(t *testing.T) { conf := config.Config{Grpc: config.GrpcConfig{Port: 5063}, Tls: tlsConf, SDKs: map[string]*config.SDKConfig{key: &sdkConf}} defer reg.Close() - _, err := NewServer(reg, nil, status.NewReporter(&conf.Cache), &conf, log.NewNullLogger(), errChan) + _, err := NewServer(reg, telemetry.NewEmptyReporter(), status.NewReporter(&conf.Cache), &conf, log.NewNullLogger(), errChan) assert.Error(t, err) } diff --git a/internal/resources/docker-compose-test.yml b/internal/resources/docker-compose-test.yml index 0bdb5cb..e862f42 100644 --- a/internal/resources/docker-compose-test.yml +++ b/internal/resources/docker-compose-test.yml @@ -12,7 +12,7 @@ configs: file: monitor/grafana_ds.yml grafana_dashboards_2: file: monitor/dashboards.yml - grafana_dashboard_12: + grafana_dashboard_13: file: monitor/proxy_dashboard.json services: @@ -27,14 +27,23 @@ services: - CONFIGCAT_GRPC_KEEP_ALIVE_MAX_CONNECTION_IDLE=15 - CONFIGCAT_GRPC_SERVER_REFLECTION_ENABLED=true - CONFIGCAT_PROFILE_KEY=08ddb657-51e9-4b6a-848e-017939e290fe - - CONFIGCAT_PROFILE_SECRET=configcat_pst_2i2czO3+T5GxyosW9aiXg/CsCGD4TaIJe7nXgT45riU= + - CONFIGCAT_PROFILE_SECRET=configcat_pst_k+ImIcT3T08iPdLyBx9aZWVRiv4QAynj9DJ+SXcWPxU= - CONFIGCAT_PROFILE_BASE_URL=https://test-api.configcat.com - CONFIGCAT_PROFILE_SDKS_BASE_URL=https://test-cdn-global.configcat.com - CONFIGCAT_PROFILE_POLL_INTERVAL=60 - CONFIGCAT_PROFILE_LOG_LEVEL=debug - - CONFIGCAT_PROFILE_WEBHOOK_SIGNING_KEY=configcat_whsk_6R0Dt8/dA+1Bvlrpp0IHAWLr0x3RQ5jQ+YlG87CHjtM= + - CONFIGCAT_PROFILE_WEBHOOK_SIGNING_KEY=configcat_whsk_TCkS/w0Ob0/uDyq0QkLl0JZRSMfOzimxBz48afhP45I= - CONFIGCAT_SDKS={"sdk-676":""} - CONFIGCAT_SDK_676_LOG_LEVEL=debug + - CONFIGCAT_DIAG_METRICS_OTLP_ENABLED=true + - CONFIGCAT_DIAG_METRICS_OTLP_PROTOCOL=grpc + - CONFIGCAT_DIAG_METRICS_OTLP_ENDPOINT=aspire-dashboard:18889 + - CONFIGCAT_DIAG_TRACES_ENABLED=true + - CONFIGCAT_DIAG_TRACES_OTLP_ENABLED=true + - CONFIGCAT_DIAG_TRACES_OTLP_PROTOCOL=grpc + - CONFIGCAT_DIAG_TRACES_OTLP_ENDPOINT=aspire-dashboard:18889 + - OTEL_EXPORTER_OTLP_HEADERS=x-otlp-api-key=test +# - CONFIGCAT_OFFLINE_ENABLED=true # - CONFIGCAT_SDKS={"sdk1":"XxPbCKmzIUGORk4vsufpzw/iC_KABprDEueeQs3yovVnQ", "sdk2":"XxPbCKmzIUGORk4vsufpzw/6ft7XQudcEuIXY49grZM9w"} # - CONFIGCAT_SDK1_BASE_URL=https://test-cdn-global.configcat.com # - CONFIGCAT_SDK1_LOG_LEVEL=error @@ -89,26 +98,37 @@ services: target: /etc/grafana/provisioning/datasources/prometheus.yml - source: grafana_dashboards_2 target: /etc/grafana/provisioning/dashboards/dashboards.yml - - source: grafana_dashboard_12 + - source: grafana_dashboard_13 target: /etc/grafana/provisioning/dashboards/proxy/proxy_dashboard.json depends_on: - configcat_proxy - prometheus redis: - image: redis:7.0.8-alpine3.17 + image: redis ports: - "6379:6379" - mongodb: - image: mongodb/mongodb-community-server - ports: - - "27017:27017" +# mongodb: +# image: mongodb/mongodb-community-server +# ports: +# - "27017:27017" +# +# dynamodb: +# image: amazon/dynamodb-local +# ports: +# - "8000:8000" - dynamodb: - image: amazon/dynamodb-local + aspire-dashboard: + image: mcr.microsoft.com/dotnet/aspire-dashboard ports: - - "8000:8000" + - "4317:18889" + - "4318:18890" + - "18888:18888" + environment: + - Dashboard__Frontend__AuthMode=Unsecured + - Dashboard__Otlp__AuthMode=ApiKey + - Dashboard__Otlp__PrimaryApiKey=test squid: image: ubuntu/squid diff --git a/internal/resources/monitor/proxy_dashboard.json b/internal/resources/monitor/proxy_dashboard.json index 9a1d3d5..e01af23 100644 --- a/internal/resources/monitor/proxy_dashboard.json +++ b/internal/resources/monitor/proxy_dashboard.json @@ -61,6 +61,7 @@ "type": "linear" }, "showPoints": "auto", + "showValues": false, "spanNulls": false, "stacking": { "group": "A", @@ -75,7 +76,8 @@ "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": 0 }, { "color": "red", @@ -107,7 +109,7 @@ "sort": "none" } }, - "pluginVersion": "12.0.2", + "pluginVersion": "12.2.0", "targets": [ { "datasource": { @@ -158,6 +160,7 @@ "type": "linear" }, "showPoints": "auto", + "showValues": false, "spanNulls": false, "stacking": { "group": "A", @@ -172,7 +175,8 @@ "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": 0 }, { "color": "red", @@ -203,7 +207,7 @@ "sort": "none" } }, - "pluginVersion": "12.0.2", + "pluginVersion": "12.2.0", "targets": [ { "datasource": { @@ -255,6 +259,7 @@ "type": "linear" }, "showPoints": "auto", + "showValues": false, "spanNulls": false, "stacking": { "group": "A", @@ -270,7 +275,8 @@ "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": 0 }, { "color": "red", @@ -302,7 +308,7 @@ "sort": "none" } }, - "pluginVersion": "12.0.2", + "pluginVersion": "12.2.0", "targets": [ { "datasource": { @@ -334,7 +340,8 @@ "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": 0 } ] }, @@ -366,7 +373,7 @@ "textMode": "auto", "wideLayout": true }, - "pluginVersion": "12.0.2", + "pluginVersion": "12.2.0", "targets": [ { "datasource": { @@ -417,6 +424,7 @@ "type": "linear" }, "showPoints": "auto", + "showValues": false, "spanNulls": false, "stacking": { "group": "A", @@ -432,7 +440,8 @@ "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": 0 }, { "color": "red", @@ -464,7 +473,7 @@ "sort": "none" } }, - "pluginVersion": "12.0.2", + "pluginVersion": "12.2.0", "targets": [ { "datasource": { @@ -516,6 +525,7 @@ "type": "linear" }, "showPoints": "auto", + "showValues": false, "spanNulls": false, "stacking": { "group": "A", @@ -531,7 +541,8 @@ "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": 0 }, { "color": "red", @@ -563,7 +574,7 @@ "sort": "none" } }, - "pluginVersion": "12.0.2", + "pluginVersion": "12.2.0", "targets": [ { "datasource": { @@ -615,6 +626,7 @@ "type": "linear" }, "showPoints": "auto", + "showValues": false, "spanNulls": false, "stacking": { "group": "A", @@ -630,7 +642,8 @@ "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": 0 }, { "color": "red", @@ -662,7 +675,7 @@ "sort": "none" } }, - "pluginVersion": "12.0.2", + "pluginVersion": "12.2.0", "targets": [ { "datasource": { @@ -714,6 +727,7 @@ "type": "linear" }, "showPoints": "auto", + "showValues": false, "spanNulls": false, "stacking": { "group": "A", @@ -729,7 +743,8 @@ "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": 0 }, { "color": "red", @@ -739,7 +754,32 @@ }, "unit": "none" }, - "overrides": [] + "overrides": [ + { + "__systemRef": "hideSeriesFrom", + "matcher": { + "id": "byNames", + "options": { + "mode": "exclude", + "names": [ + "/api/sdk-342/keys - GET - 200" + ], + "prefix": "All except:", + "readOnly": true + } + }, + "properties": [ + { + "id": "custom.hideFrom", + "value": { + "legend": false, + "tooltip": true, + "viz": true + } + } + ] + } + ] }, "gridPos": { "h": 6, @@ -762,16 +802,16 @@ "sort": "none" } }, - "pluginVersion": "12.0.2", + "pluginVersion": "12.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "prometheus" }, - "editorMode": "code", - "expr": "sum(increase(configcat_http_request_duration_seconds_count{instance=\"configcat_proxy:8051\"}[$__rate_interval])) by (route, method, status) > 0", - "legendFormat": "{{route}} - {{method}} - {{status}}", + "editorMode": "builder", + "expr": "sum by(http_route, http_request_method, http_response_status_code) (increase(configcat_http_server_request_duration_seconds_count{instance=\"configcat_proxy:8051\"}[$__rate_interval])) > 0", + "legendFormat": "{{http_route}} - {{http_request_method}} - {{http_response_status_code}}", "range": true, "refId": "A" } @@ -813,6 +853,7 @@ "type": "linear" }, "showPoints": "auto", + "showValues": false, "spanNulls": false, "stacking": { "group": "A", @@ -827,7 +868,8 @@ "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": 0 }, { "color": "red", @@ -859,15 +901,15 @@ "sort": "none" } }, - "pluginVersion": "12.0.2", + "pluginVersion": "12.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "prometheus" }, - "editorMode": "code", - "expr": "histogram_quantile(0.99, sum by(le, route) (rate(configcat_http_request_duration_seconds_bucket{instance=\"configcat_proxy:8051\"}[$__rate_interval]))) > 0", + "editorMode": "builder", + "expr": "histogram_quantile(0.99, sum by(le, http_route) (rate(configcat_http_server_request_duration_seconds_bucket{instance=\"configcat_proxy:8051\"}[$__rate_interval]))) > 0", "legendFormat": "__auto", "range": true, "refId": "A" @@ -910,6 +952,7 @@ "type": "linear" }, "showPoints": "auto", + "showValues": false, "spanNulls": false, "stacking": { "group": "A", @@ -925,7 +968,8 @@ "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": 0 }, { "color": "red", @@ -958,16 +1002,16 @@ "sort": "none" } }, - "pluginVersion": "12.0.2", + "pluginVersion": "12.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "prometheus" }, - "editorMode": "code", - "expr": "sum(increase(configcat_grpc_rpc_duration_seconds_count{instance=\"configcat_proxy:8051\"}[$__rate_interval])) by (method, code) > 0", - "legendFormat": "{{method}} - {{code}}", + "editorMode": "builder", + "expr": "sum by(rpc_method, rpc_grpc_status_code) (increase(configcat_rpc_server_duration_milliseconds_count{instance=\"configcat_proxy:8051\"}[$__rate_interval])) > 0", + "legendFormat": "{{rpc_method}} - {{rpc_grpc_status_code}}", "range": true, "refId": "A" } @@ -1009,6 +1053,7 @@ "type": "linear" }, "showPoints": "auto", + "showValues": false, "spanNulls": false, "stacking": { "group": "A", @@ -1023,7 +1068,8 @@ "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": 0 }, { "color": "red", @@ -1031,7 +1077,7 @@ } ] }, - "unit": "s" + "unit": "ms" }, "overrides": [] }, @@ -1055,15 +1101,15 @@ "sort": "none" } }, - "pluginVersion": "12.0.2", + "pluginVersion": "12.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "prometheus" }, - "editorMode": "code", - "expr": "histogram_quantile(0.99, sum by(le, method) (rate(configcat_grpc_rpc_duration_seconds_bucket{instance=\"configcat_proxy:8051\"}[$__rate_interval]))) > 0", + "editorMode": "builder", + "expr": "histogram_quantile(0.99, sum by(le, rpc_method) (rate(configcat_rpc_server_duration_milliseconds_bucket{instance=\"configcat_proxy:8051\"}[$__rate_interval]))) > 0", "legendFormat": "__auto", "range": true, "refId": "A" @@ -1106,6 +1152,7 @@ "type": "linear" }, "showPoints": "auto", + "showValues": false, "spanNulls": false, "stacking": { "group": "A", @@ -1121,7 +1168,8 @@ "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": 0 }, { "color": "red", @@ -1154,17 +1202,17 @@ "sort": "none" } }, - "pluginVersion": "12.0.2", + "pluginVersion": "12.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "prometheus" }, - "editorMode": "code", - "expr": "sum(increase(configcat_sdk_http_request_duration_seconds_count{instance=\"configcat_proxy:8051\"}[$__rate_interval])) by (sdk, status)", + "editorMode": "builder", + "expr": "sum by(configcat_sdk_id, http_response_status_code) (increase(configcat_http_client_request_duration_seconds_count{instance=\"configcat_proxy:8051\", configcat_source=\"sdk\"}[$__rate_interval]))", "interval": "", - "legendFormat": "{{sdk}} - {{status}}", + "legendFormat": "{{configcat_sdk_id}} - {{http_response_status_code}}", "range": true, "refId": "A" } @@ -1206,6 +1254,7 @@ "type": "linear" }, "showPoints": "auto", + "showValues": false, "spanNulls": false, "stacking": { "group": "A", @@ -1220,7 +1269,8 @@ "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": 0 }, { "color": "red", @@ -1253,15 +1303,15 @@ "sort": "none" } }, - "pluginVersion": "12.0.2", + "pluginVersion": "12.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "prometheus" }, - "editorMode": "code", - "expr": "histogram_quantile(0.99, sum by(le, sdk) (rate(configcat_sdk_http_request_duration_seconds_bucket{instance=\"configcat_proxy:8051\"}[$__rate_interval])))", + "editorMode": "builder", + "expr": "histogram_quantile(0.99, sum by(le, configcat_sdk_id) (rate(configcat_http_client_request_duration_seconds_bucket{instance=\"configcat_proxy:8051\", configcat_source=\"sdk\"}[$__rate_interval])))", "legendFormat": "__auto", "range": true, "refId": "A" @@ -1304,6 +1354,7 @@ "type": "linear" }, "showPoints": "auto", + "showValues": false, "spanNulls": false, "stacking": { "group": "A", @@ -1319,7 +1370,8 @@ "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": 0 }, { "color": "red", @@ -1352,17 +1404,17 @@ "sort": "none" } }, - "pluginVersion": "12.0.2", + "pluginVersion": "12.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "prometheus" }, - "editorMode": "code", - "expr": "sum(increase(configcat_profile_http_request_duration_seconds_count{instance=\"configcat_proxy:8051\"}[$__rate_interval])) by (key, status)", + "editorMode": "builder", + "expr": "sum by(http_route, http_response_status_code) (increase(configcat_http_client_request_duration_seconds_count{instance=\"configcat_proxy:8051\", configcat_source=\"profile\"}[$__rate_interval]))", "interval": "", - "legendFormat": "{{key}} - {{status}}", + "legendFormat": "{{http_route}} - {{http_response_status_code}}", "range": true, "refId": "A" } @@ -1404,6 +1456,7 @@ "type": "linear" }, "showPoints": "auto", + "showValues": false, "spanNulls": false, "stacking": { "group": "A", @@ -1418,7 +1471,8 @@ "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": 0 }, { "color": "red", @@ -1451,16 +1505,16 @@ "sort": "none" } }, - "pluginVersion": "12.0.2", + "pluginVersion": "12.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "prometheus" }, - "editorMode": "code", - "expr": "histogram_quantile(0.99, sum by(le, key) (rate(configcat_profile_http_request_duration_seconds_bucket{instance=\"configcat_proxy:8051\"}[$__rate_interval])))", - "legendFormat": "__auto", + "editorMode": "builder", + "expr": "histogram_quantile(0.99, sum by(le, http_route, http_response_status_code) (rate(configcat_http_client_request_duration_seconds_bucket{instance=\"configcat_proxy:8051\", configcat_source=\"profile\"}[$__rate_interval])))", + "legendFormat": "{{http_route}} - {{http_response_status_code}}", "range": true, "refId": "A" } @@ -1502,6 +1556,7 @@ "type": "linear" }, "showPoints": "auto", + "showValues": false, "spanNulls": false, "stacking": { "group": "A", @@ -1516,7 +1571,8 @@ "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": 0 }, { "color": "red", @@ -1548,7 +1604,7 @@ "sort": "none" } }, - "pluginVersion": "12.0.2", + "pluginVersion": "12.2.0", "targets": [ { "datasource": { @@ -1599,6 +1655,7 @@ "type": "linear" }, "showPoints": "auto", + "showValues": false, "spanNulls": false, "stacking": { "group": "A", @@ -1613,7 +1670,8 @@ "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": 0 }, { "color": "red", @@ -1646,7 +1704,7 @@ "sort": "none" } }, - "pluginVersion": "12.0.2", + "pluginVersion": "12.2.0", "targets": [ { "datasource": { @@ -1697,6 +1755,7 @@ "type": "linear" }, "showPoints": "auto", + "showValues": false, "spanNulls": false, "stacking": { "group": "A", @@ -1711,7 +1770,8 @@ "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": 0 }, { "color": "red", @@ -1743,7 +1803,7 @@ "sort": "none" } }, - "pluginVersion": "12.0.2", + "pluginVersion": "12.2.0", "targets": [ { "datasource": { @@ -1763,7 +1823,7 @@ ], "preload": false, "refresh": "5s", - "schemaVersion": 41, + "schemaVersion": 42, "tags": [], "templating": { "list": [] @@ -1776,5 +1836,5 @@ "timezone": "", "title": "Proxy", "uid": "s3Vj4B14k", - "version": 2 + "version": 1 } \ No newline at end of file diff --git a/internal/resources/sdk.html b/internal/resources/sdk.html index 09c378c..e5f3b50 100644 --- a/internal/resources/sdk.html +++ b/internal/resources/sdk.html @@ -8,14 +8,12 @@