diff --git a/api_client.go b/api_client.go index b32321d..dc43126 100644 --- a/api_client.go +++ b/api_client.go @@ -30,6 +30,8 @@ import ( "strings" "time" "unicode/utf8" + + "github.com/openfga/go-sdk/internal/telemetry" ) var ( @@ -67,6 +69,8 @@ func NewAPIClient(cfg *Configuration) *APIClient { if cfg.Credentials == nil { cfg.HTTPClient = http.DefaultClient } else { + cfg.Credentials.Context = context.Background() + telemetry.Bind(cfg.Credentials.Context, telemetry.Get(telemetry.TelemetryFactoryParameters{Configuration: cfg.Telemetry})) var httpClient, headers = cfg.Credentials.GetHttpClientAndHeaderOverrides() if len(headers) > 0 { for idx := range headers { diff --git a/api_open_fga.go b/api_open_fga.go index d4f916c..8ee8b17 100644 --- a/api_open_fga.go +++ b/api_open_fga.go @@ -21,7 +21,8 @@ import ( "strings" "time" - "github.com/openfga/go-sdk/internal/utils" + telemetry "github.com/openfga/go-sdk/internal/telemetry" + internalutils "github.com/openfga/go-sdk/internal/utils" ) // Linger please @@ -864,6 +865,7 @@ func (a *OpenFgaApiService) Check(ctx _context.Context, storeId string) ApiCheck func (a *OpenFgaApiService) CheckExecute(r ApiCheckRequest) (CheckResponse, *_nethttp.Response, error) { var maxRetry int var minWaitInMs int + var requestStarted time.Time = time.Now() if a.RetryParams != nil { maxRetry = a.RetryParams.MinWaitInMs @@ -1083,6 +1085,28 @@ func (a *OpenFgaApiService) CheckExecute(r ApiCheckRequest) (CheckResponse, *_ne return localVarReturnValue, localVarHTTPResponse, newErr } + metrics := telemetry.GetMetrics(telemetry.TelemetryFactoryParameters{Configuration: a.client.cfg.Telemetry}) + + var attrs, queryDuration, requestDuration, _ = metrics.BuildTelemetryAttributes( + "Check", + map[string]interface{}{ + "storeId": r.storeId, + "body": localVarPostBody, + }, + req, + localVarHTTPResponse, + requestStarted, + i, + ) + + if requestDuration > 0 { + metrics.RequestDuration(requestDuration, attrs) + } + + if queryDuration > 0 { + metrics.QueryDuration(queryDuration, attrs) + } + return localVarReturnValue, localVarHTTPResponse, nil } // should never have reached this @@ -1125,6 +1149,7 @@ func (a *OpenFgaApiService) CreateStore(ctx _context.Context) ApiCreateStoreRequ func (a *OpenFgaApiService) CreateStoreExecute(r ApiCreateStoreRequest) (CreateStoreResponse, *_nethttp.Response, error) { var maxRetry int var minWaitInMs int + var requestStarted time.Time = time.Now() if a.RetryParams != nil { maxRetry = a.RetryParams.MinWaitInMs @@ -1333,6 +1358,27 @@ func (a *OpenFgaApiService) CreateStoreExecute(r ApiCreateStoreRequest) (CreateS return localVarReturnValue, localVarHTTPResponse, newErr } + metrics := telemetry.GetMetrics(telemetry.TelemetryFactoryParameters{Configuration: a.client.cfg.Telemetry}) + + var attrs, queryDuration, requestDuration, _ = metrics.BuildTelemetryAttributes( + "CreateStore", + map[string]interface{}{ + "body": localVarPostBody, + }, + req, + localVarHTTPResponse, + requestStarted, + i, + ) + + if requestDuration > 0 { + metrics.RequestDuration(requestDuration, attrs) + } + + if queryDuration > 0 { + metrics.QueryDuration(queryDuration, attrs) + } + return localVarReturnValue, localVarHTTPResponse, nil } // should never have reached this @@ -1371,6 +1417,7 @@ func (a *OpenFgaApiService) DeleteStore(ctx _context.Context, storeId string) Ap func (a *OpenFgaApiService) DeleteStoreExecute(r ApiDeleteStoreRequest) (*_nethttp.Response, error) { var maxRetry int var minWaitInMs int + var requestStarted time.Time = time.Now() if a.RetryParams != nil { maxRetry = a.RetryParams.MinWaitInMs @@ -1575,6 +1622,28 @@ func (a *OpenFgaApiService) DeleteStoreExecute(r ApiDeleteStoreRequest) (*_netht return localVarHTTPResponse, newErr } + metrics := telemetry.GetMetrics(telemetry.TelemetryFactoryParameters{Configuration: a.client.cfg.Telemetry}) + + var attrs, queryDuration, requestDuration, _ = metrics.BuildTelemetryAttributes( + "DeleteStore", + map[string]interface{}{ + "storeId": r.storeId, + "body": localVarPostBody, + }, + req, + localVarHTTPResponse, + requestStarted, + i, + ) + + if requestDuration > 0 { + metrics.RequestDuration(requestDuration, attrs) + } + + if queryDuration > 0 { + metrics.QueryDuration(queryDuration, attrs) + } + return localVarHTTPResponse, nil } // should never have reached this @@ -1672,6 +1741,7 @@ func (a *OpenFgaApiService) Expand(ctx _context.Context, storeId string) ApiExpa func (a *OpenFgaApiService) ExpandExecute(r ApiExpandRequest) (ExpandResponse, *_nethttp.Response, error) { var maxRetry int var minWaitInMs int + var requestStarted time.Time = time.Now() if a.RetryParams != nil { maxRetry = a.RetryParams.MinWaitInMs @@ -1891,6 +1961,28 @@ func (a *OpenFgaApiService) ExpandExecute(r ApiExpandRequest) (ExpandResponse, * return localVarReturnValue, localVarHTTPResponse, newErr } + metrics := telemetry.GetMetrics(telemetry.TelemetryFactoryParameters{Configuration: a.client.cfg.Telemetry}) + + var attrs, queryDuration, requestDuration, _ = metrics.BuildTelemetryAttributes( + "Expand", + map[string]interface{}{ + "storeId": r.storeId, + "body": localVarPostBody, + }, + req, + localVarHTTPResponse, + requestStarted, + i, + ) + + if requestDuration > 0 { + metrics.RequestDuration(requestDuration, attrs) + } + + if queryDuration > 0 { + metrics.QueryDuration(queryDuration, attrs) + } + return localVarReturnValue, localVarHTTPResponse, nil } // should never have reached this @@ -1930,6 +2022,7 @@ func (a *OpenFgaApiService) GetStore(ctx _context.Context, storeId string) ApiGe func (a *OpenFgaApiService) GetStoreExecute(r ApiGetStoreRequest) (GetStoreResponse, *_nethttp.Response, error) { var maxRetry int var minWaitInMs int + var requestStarted time.Time = time.Now() if a.RetryParams != nil { maxRetry = a.RetryParams.MinWaitInMs @@ -2144,6 +2237,28 @@ func (a *OpenFgaApiService) GetStoreExecute(r ApiGetStoreRequest) (GetStoreRespo return localVarReturnValue, localVarHTTPResponse, newErr } + metrics := telemetry.GetMetrics(telemetry.TelemetryFactoryParameters{Configuration: a.client.cfg.Telemetry}) + + var attrs, queryDuration, requestDuration, _ = metrics.BuildTelemetryAttributes( + "GetStore", + map[string]interface{}{ + "storeId": r.storeId, + "body": localVarPostBody, + }, + req, + localVarHTTPResponse, + requestStarted, + i, + ) + + if requestDuration > 0 { + metrics.RequestDuration(requestDuration, attrs) + } + + if queryDuration > 0 { + metrics.QueryDuration(queryDuration, attrs) + } + return localVarReturnValue, localVarHTTPResponse, nil } // should never have reached this @@ -2197,6 +2312,7 @@ func (a *OpenFgaApiService) ListObjects(ctx _context.Context, storeId string) Ap func (a *OpenFgaApiService) ListObjectsExecute(r ApiListObjectsRequest) (ListObjectsResponse, *_nethttp.Response, error) { var maxRetry int var minWaitInMs int + var requestStarted time.Time = time.Now() if a.RetryParams != nil { maxRetry = a.RetryParams.MinWaitInMs @@ -2416,6 +2532,28 @@ func (a *OpenFgaApiService) ListObjectsExecute(r ApiListObjectsRequest) (ListObj return localVarReturnValue, localVarHTTPResponse, newErr } + metrics := telemetry.GetMetrics(telemetry.TelemetryFactoryParameters{Configuration: a.client.cfg.Telemetry}) + + var attrs, queryDuration, requestDuration, _ = metrics.BuildTelemetryAttributes( + "ListObjects", + map[string]interface{}{ + "storeId": r.storeId, + "body": localVarPostBody, + }, + req, + localVarHTTPResponse, + requestStarted, + i, + ) + + if requestDuration > 0 { + metrics.RequestDuration(requestDuration, attrs) + } + + if queryDuration > 0 { + metrics.QueryDuration(queryDuration, attrs) + } + return localVarReturnValue, localVarHTTPResponse, nil } // should never have reached this @@ -2466,6 +2604,7 @@ func (a *OpenFgaApiService) ListStores(ctx _context.Context) ApiListStoresReques func (a *OpenFgaApiService) ListStoresExecute(r ApiListStoresRequest) (ListStoresResponse, *_nethttp.Response, error) { var maxRetry int var minWaitInMs int + var requestStarted time.Time = time.Now() if a.RetryParams != nil { maxRetry = a.RetryParams.MinWaitInMs @@ -2675,6 +2814,27 @@ func (a *OpenFgaApiService) ListStoresExecute(r ApiListStoresRequest) (ListStore return localVarReturnValue, localVarHTTPResponse, newErr } + metrics := telemetry.GetMetrics(telemetry.TelemetryFactoryParameters{Configuration: a.client.cfg.Telemetry}) + + var attrs, queryDuration, requestDuration, _ = metrics.BuildTelemetryAttributes( + "ListStores", + map[string]interface{}{ + "body": localVarPostBody, + }, + req, + localVarHTTPResponse, + requestStarted, + i, + ) + + if requestDuration > 0 { + metrics.RequestDuration(requestDuration, attrs) + } + + if queryDuration > 0 { + metrics.QueryDuration(queryDuration, attrs) + } + return localVarReturnValue, localVarHTTPResponse, nil } // should never have reached this @@ -2730,6 +2890,7 @@ func (a *OpenFgaApiService) ListUsers(ctx _context.Context, storeId string) ApiL func (a *OpenFgaApiService) ListUsersExecute(r ApiListUsersRequest) (ListUsersResponse, *_nethttp.Response, error) { var maxRetry int var minWaitInMs int + var requestStarted time.Time = time.Now() if a.RetryParams != nil { maxRetry = a.RetryParams.MinWaitInMs @@ -2949,6 +3110,28 @@ func (a *OpenFgaApiService) ListUsersExecute(r ApiListUsersRequest) (ListUsersRe return localVarReturnValue, localVarHTTPResponse, newErr } + metrics := telemetry.GetMetrics(telemetry.TelemetryFactoryParameters{Configuration: a.client.cfg.Telemetry}) + + var attrs, queryDuration, requestDuration, _ = metrics.BuildTelemetryAttributes( + "ListUsers", + map[string]interface{}{ + "storeId": r.storeId, + "body": localVarPostBody, + }, + req, + localVarHTTPResponse, + requestStarted, + i, + ) + + if requestDuration > 0 { + metrics.RequestDuration(requestDuration, attrs) + } + + if queryDuration > 0 { + metrics.QueryDuration(queryDuration, attrs) + } + return localVarReturnValue, localVarHTTPResponse, nil } // should never have reached this @@ -3105,6 +3288,7 @@ func (a *OpenFgaApiService) Read(ctx _context.Context, storeId string) ApiReadRe func (a *OpenFgaApiService) ReadExecute(r ApiReadRequest) (ReadResponse, *_nethttp.Response, error) { var maxRetry int var minWaitInMs int + var requestStarted time.Time = time.Now() if a.RetryParams != nil { maxRetry = a.RetryParams.MinWaitInMs @@ -3324,6 +3508,28 @@ func (a *OpenFgaApiService) ReadExecute(r ApiReadRequest) (ReadResponse, *_netht return localVarReturnValue, localVarHTTPResponse, newErr } + metrics := telemetry.GetMetrics(telemetry.TelemetryFactoryParameters{Configuration: a.client.cfg.Telemetry}) + + var attrs, queryDuration, requestDuration, _ = metrics.BuildTelemetryAttributes( + "Read", + map[string]interface{}{ + "storeId": r.storeId, + "body": localVarPostBody, + }, + req, + localVarHTTPResponse, + requestStarted, + i, + ) + + if requestDuration > 0 { + metrics.RequestDuration(requestDuration, attrs) + } + + if queryDuration > 0 { + metrics.QueryDuration(queryDuration, attrs) + } + return localVarReturnValue, localVarHTTPResponse, nil } // should never have reached this @@ -3366,6 +3572,7 @@ func (a *OpenFgaApiService) ReadAssertions(ctx _context.Context, storeId string, func (a *OpenFgaApiService) ReadAssertionsExecute(r ApiReadAssertionsRequest) (ReadAssertionsResponse, *_nethttp.Response, error) { var maxRetry int var minWaitInMs int + var requestStarted time.Time = time.Now() if a.RetryParams != nil { maxRetry = a.RetryParams.MinWaitInMs @@ -3585,6 +3792,28 @@ func (a *OpenFgaApiService) ReadAssertionsExecute(r ApiReadAssertionsRequest) (R return localVarReturnValue, localVarHTTPResponse, newErr } + metrics := telemetry.GetMetrics(telemetry.TelemetryFactoryParameters{Configuration: a.client.cfg.Telemetry}) + + var attrs, queryDuration, requestDuration, _ = metrics.BuildTelemetryAttributes( + "ReadAssertions", + map[string]interface{}{ + "storeId": r.storeId, + "body": localVarPostBody, + }, + req, + localVarHTTPResponse, + requestStarted, + i, + ) + + if requestDuration > 0 { + metrics.RequestDuration(requestDuration, attrs) + } + + if queryDuration > 0 { + metrics.QueryDuration(queryDuration, attrs) + } + return localVarReturnValue, localVarHTTPResponse, nil } // should never have reached this @@ -3670,6 +3899,7 @@ func (a *OpenFgaApiService) ReadAuthorizationModel(ctx _context.Context, storeId func (a *OpenFgaApiService) ReadAuthorizationModelExecute(r ApiReadAuthorizationModelRequest) (ReadAuthorizationModelResponse, *_nethttp.Response, error) { var maxRetry int var minWaitInMs int + var requestStarted time.Time = time.Now() if a.RetryParams != nil { maxRetry = a.RetryParams.MinWaitInMs @@ -3889,6 +4119,28 @@ func (a *OpenFgaApiService) ReadAuthorizationModelExecute(r ApiReadAuthorization return localVarReturnValue, localVarHTTPResponse, newErr } + metrics := telemetry.GetMetrics(telemetry.TelemetryFactoryParameters{Configuration: a.client.cfg.Telemetry}) + + var attrs, queryDuration, requestDuration, _ = metrics.BuildTelemetryAttributes( + "ReadAuthorizationModel", + map[string]interface{}{ + "storeId": r.storeId, + "body": localVarPostBody, + }, + req, + localVarHTTPResponse, + requestStarted, + i, + ) + + if requestDuration > 0 { + metrics.RequestDuration(requestDuration, attrs) + } + + if queryDuration > 0 { + metrics.QueryDuration(queryDuration, attrs) + } + return localVarReturnValue, localVarHTTPResponse, nil } // should never have reached this @@ -3980,6 +4232,7 @@ func (a *OpenFgaApiService) ReadAuthorizationModels(ctx _context.Context, storeI func (a *OpenFgaApiService) ReadAuthorizationModelsExecute(r ApiReadAuthorizationModelsRequest) (ReadAuthorizationModelsResponse, *_nethttp.Response, error) { var maxRetry int var minWaitInMs int + var requestStarted time.Time = time.Now() if a.RetryParams != nil { maxRetry = a.RetryParams.MinWaitInMs @@ -4200,6 +4453,28 @@ func (a *OpenFgaApiService) ReadAuthorizationModelsExecute(r ApiReadAuthorizatio return localVarReturnValue, localVarHTTPResponse, newErr } + metrics := telemetry.GetMetrics(telemetry.TelemetryFactoryParameters{Configuration: a.client.cfg.Telemetry}) + + var attrs, queryDuration, requestDuration, _ = metrics.BuildTelemetryAttributes( + "ReadAuthorizationModels", + map[string]interface{}{ + "storeId": r.storeId, + "body": localVarPostBody, + }, + req, + localVarHTTPResponse, + requestStarted, + i, + ) + + if requestDuration > 0 { + metrics.RequestDuration(requestDuration, attrs) + } + + if queryDuration > 0 { + metrics.QueryDuration(queryDuration, attrs) + } + return localVarReturnValue, localVarHTTPResponse, nil } // should never have reached this @@ -4260,6 +4535,7 @@ func (a *OpenFgaApiService) ReadChanges(ctx _context.Context, storeId string) Ap func (a *OpenFgaApiService) ReadChangesExecute(r ApiReadChangesRequest) (ReadChangesResponse, *_nethttp.Response, error) { var maxRetry int var minWaitInMs int + var requestStarted time.Time = time.Now() if a.RetryParams != nil { maxRetry = a.RetryParams.MinWaitInMs @@ -4483,6 +4759,28 @@ func (a *OpenFgaApiService) ReadChangesExecute(r ApiReadChangesRequest) (ReadCha return localVarReturnValue, localVarHTTPResponse, newErr } + metrics := telemetry.GetMetrics(telemetry.TelemetryFactoryParameters{Configuration: a.client.cfg.Telemetry}) + + var attrs, queryDuration, requestDuration, _ = metrics.BuildTelemetryAttributes( + "ReadChanges", + map[string]interface{}{ + "storeId": r.storeId, + "body": localVarPostBody, + }, + req, + localVarHTTPResponse, + requestStarted, + i, + ) + + if requestDuration > 0 { + metrics.RequestDuration(requestDuration, attrs) + } + + if queryDuration > 0 { + metrics.QueryDuration(queryDuration, attrs) + } + return localVarReturnValue, localVarHTTPResponse, nil } // should never have reached this @@ -4570,6 +4868,7 @@ func (a *OpenFgaApiService) Write(ctx _context.Context, storeId string) ApiWrite func (a *OpenFgaApiService) WriteExecute(r ApiWriteRequest) (map[string]interface{}, *_nethttp.Response, error) { var maxRetry int var minWaitInMs int + var requestStarted time.Time = time.Now() if a.RetryParams != nil { maxRetry = a.RetryParams.MinWaitInMs @@ -4789,6 +5088,28 @@ func (a *OpenFgaApiService) WriteExecute(r ApiWriteRequest) (map[string]interfac return localVarReturnValue, localVarHTTPResponse, newErr } + metrics := telemetry.GetMetrics(telemetry.TelemetryFactoryParameters{Configuration: a.client.cfg.Telemetry}) + + var attrs, queryDuration, requestDuration, _ = metrics.BuildTelemetryAttributes( + "Write", + map[string]interface{}{ + "storeId": r.storeId, + "body": localVarPostBody, + }, + req, + localVarHTTPResponse, + requestStarted, + i, + ) + + if requestDuration > 0 { + metrics.RequestDuration(requestDuration, attrs) + } + + if queryDuration > 0 { + metrics.QueryDuration(queryDuration, attrs) + } + return localVarReturnValue, localVarHTTPResponse, nil } // should never have reached this @@ -4836,6 +5157,7 @@ func (a *OpenFgaApiService) WriteAssertions(ctx _context.Context, storeId string func (a *OpenFgaApiService) WriteAssertionsExecute(r ApiWriteAssertionsRequest) (*_nethttp.Response, error) { var maxRetry int var minWaitInMs int + var requestStarted time.Time = time.Now() if a.RetryParams != nil { maxRetry = a.RetryParams.MinWaitInMs @@ -5050,6 +5372,28 @@ func (a *OpenFgaApiService) WriteAssertionsExecute(r ApiWriteAssertionsRequest) return localVarHTTPResponse, newErr } + metrics := telemetry.GetMetrics(telemetry.TelemetryFactoryParameters{Configuration: a.client.cfg.Telemetry}) + + var attrs, queryDuration, requestDuration, _ = metrics.BuildTelemetryAttributes( + "WriteAssertions", + map[string]interface{}{ + "storeId": r.storeId, + "body": localVarPostBody, + }, + req, + localVarHTTPResponse, + requestStarted, + i, + ) + + if requestDuration > 0 { + metrics.RequestDuration(requestDuration, attrs) + } + + if queryDuration > 0 { + metrics.QueryDuration(queryDuration, attrs) + } + return localVarHTTPResponse, nil } // should never have reached this @@ -5139,6 +5483,7 @@ func (a *OpenFgaApiService) WriteAuthorizationModel(ctx _context.Context, storeI func (a *OpenFgaApiService) WriteAuthorizationModelExecute(r ApiWriteAuthorizationModelRequest) (WriteAuthorizationModelResponse, *_nethttp.Response, error) { var maxRetry int var minWaitInMs int + var requestStarted time.Time = time.Now() if a.RetryParams != nil { maxRetry = a.RetryParams.MinWaitInMs @@ -5358,6 +5703,28 @@ func (a *OpenFgaApiService) WriteAuthorizationModelExecute(r ApiWriteAuthorizati return localVarReturnValue, localVarHTTPResponse, newErr } + metrics := telemetry.GetMetrics(telemetry.TelemetryFactoryParameters{Configuration: a.client.cfg.Telemetry}) + + var attrs, queryDuration, requestDuration, _ = metrics.BuildTelemetryAttributes( + "WriteAuthorizationModel", + map[string]interface{}{ + "storeId": r.storeId, + "body": localVarPostBody, + }, + req, + localVarHTTPResponse, + requestStarted, + i, + ) + + if requestDuration > 0 { + metrics.RequestDuration(requestDuration, attrs) + } + + if queryDuration > 0 { + metrics.QueryDuration(queryDuration, attrs) + } + return localVarReturnValue, localVarHTTPResponse, nil } // should never have reached this diff --git a/client/client.go b/client/client.go index c922aa8..c047f9a 100644 --- a/client/client.go +++ b/client/client.go @@ -21,6 +21,7 @@ import ( fgaSdk "github.com/openfga/go-sdk" "github.com/openfga/go-sdk/credentials" + "github.com/openfga/go-sdk/internal/telemetry" internalutils "github.com/openfga/go-sdk/internal/utils" "golang.org/x/sync/errgroup" ) @@ -50,6 +51,7 @@ type ClientConfiguration struct { Debug bool `json:"debug"` HTTPClient *_nethttp.Client RetryParams *fgaSdk.RetryParams + Telemetry *telemetry.Configuration `json:"telemetry,omitempty"` } func newClientConfiguration(cfg *fgaSdk.Configuration) ClientConfiguration { @@ -63,6 +65,7 @@ func newClientConfiguration(cfg *fgaSdk.Configuration) ClientConfiguration { Debug: cfg.Debug, HTTPClient: cfg.HTTPClient, RetryParams: cfg.RetryParams, + Telemetry: cfg.Telemetry, } } @@ -83,6 +86,7 @@ func NewSdkClient(cfg *ClientConfiguration) (*OpenFgaClient, error) { Debug: cfg.Debug, HTTPClient: cfg.HTTPClient, RetryParams: cfg.RetryParams, + Telemetry: cfg.Telemetry, }) if err != nil { diff --git a/configuration.go b/configuration.go index 2b97a95..48603da 100644 --- a/configuration.go +++ b/configuration.go @@ -16,6 +16,7 @@ import ( "net/http" "github.com/openfga/go-sdk/credentials" + "github.com/openfga/go-sdk/internal/telemetry" ) const ( @@ -45,6 +46,7 @@ type Configuration struct { Debug bool `json:"debug,omitempty"` HTTPClient *http.Client RetryParams *RetryParams + Telemetry *telemetry.Configuration `json:"telemetry,omitempty"` } // DefaultRetryParams returns the default retry parameters @@ -81,6 +83,7 @@ func NewConfiguration(config Configuration) (*Configuration, error) { Debug: config.Debug, HTTPClient: config.HTTPClient, RetryParams: config.RetryParams, + Telemetry: config.Telemetry, } if cfg.UserAgent == "" { @@ -91,6 +94,10 @@ func NewConfiguration(config Configuration) (*Configuration, error) { cfg.DefaultHeaders = make(map[string]string) } + if cfg.Telemetry == nil { + cfg.Telemetry = telemetry.DefaultTelemetryConfiguration() + } + err := cfg.ValidateConfig() if err != nil { diff --git a/credentials/credentials.go b/credentials/credentials.go index 89a9733..94fc896 100644 --- a/credentials/credentials.go +++ b/credentials/credentials.go @@ -35,8 +35,9 @@ type Config struct { } type Credentials struct { - Method CredentialsMethod `json:"method,omitempty"` - Config *Config `json:"config,omitempty"` + Method CredentialsMethod `json:"method,omitempty"` + Config *Config `json:"config,omitempty"` + Context context.Context } func NewCredentials(config Credentials) (*Credentials, error) { @@ -117,7 +118,10 @@ func (c *Credentials) GetHttpClientAndHeaderOverrides() (*http.Client, []*Header scopes := strings.Split(strings.TrimSpace(c.Config.ClientCredentialsScopes), " ") ccConfig.Scopes = append(ccConfig.Scopes, scopes...) } - client = ccConfig.Client(context.Background()) + if c.Context == nil { + c.Context = context.Background() + } + client = ccConfig.Client(c.Context) case CredentialsMethodApiToken: var header = c.GetApiTokenHeader() if header != nil { diff --git a/go.mod b/go.mod index 5328cd5..94e8a0c 100644 --- a/go.mod +++ b/go.mod @@ -4,5 +4,13 @@ go 1.21 require ( github.com/jarcoal/httpmock v1.3.1 + go.opentelemetry.io/otel v1.28.0 + go.opentelemetry.io/otel/metric v1.28.0 golang.org/x/sync v0.8.0 ) + +require ( + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + go.opentelemetry.io/otel/trace v1.28.0 // indirect +) diff --git a/go.sum b/go.sum index 4826c8e..edd693c 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,27 @@ 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/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/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/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/jarcoal/httpmock v1.3.1 h1:iUx3whfZWVf3jT01hQTO/Eo5sAYtB2/rqaUuOtpInww= github.com/jarcoal/httpmock v1.3.1/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg= github.com/maxatome/go-testdeep v1.12.0 h1:Ql7Go8Tg0C1D/uMMX59LAoYK7LffeJQ6X2T04nTH68g= github.com/maxatome/go-testdeep v1.12.0/go.mod h1:lPZc/HAcJMP92l7yI6TRz1aZN5URwUBUAfUNvrclaNM= +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/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo= +go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4= +go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q= +go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s= +go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g= +go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI= golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/telemetry/attribute.go b/internal/telemetry/attribute.go new file mode 100644 index 0000000..4ec1f35 --- /dev/null +++ b/internal/telemetry/attribute.go @@ -0,0 +1,5 @@ +package telemetry + +type Attribute struct { + Name string +} diff --git a/internal/telemetry/attribute_test.go b/internal/telemetry/attribute_test.go new file mode 100644 index 0000000..87e38d3 --- /dev/null +++ b/internal/telemetry/attribute_test.go @@ -0,0 +1,22 @@ +package telemetry + +import ( + "testing" +) + +func TestAttributeCreation(t *testing.T) { + attrName := "test-attribute" + attr := &Attribute{Name: attrName} + + if attr.Name != attrName { + t.Errorf("Expected Attribute Name to be '%s', but got '%s'", attrName, attr.Name) + } +} + +func TestEmptyAttributeCreation(t *testing.T) { + attr := &Attribute{} + + if attr.Name != "" { + t.Errorf("Expected Attribute Name to be empty, but got '%s'", attr.Name) + } +} diff --git a/internal/telemetry/attributes.go b/internal/telemetry/attributes.go new file mode 100644 index 0000000..2a4e3f8 --- /dev/null +++ b/internal/telemetry/attributes.go @@ -0,0 +1,260 @@ +package telemetry + +import ( + "fmt" + "net/http" + "strconv" + "time" + + "go.opentelemetry.io/otel/attribute" +) + +const ( + ATTR_FGA_CLIENT_REQUEST_CLIENT_ID = "fga-client.request.client_id" + ATTR_FGA_CLIENT_REQUEST_METHOD = "fga-client.request.method" + ATTR_FGA_CLIENT_REQUEST_MODEL_ID = "fga-client.request.model_id" + ATTR_FGA_CLIENT_REQUEST_STORE_ID = "fga-client.request.store_id" + ATTR_FGA_CLIENT_RESPONSE_MODEL_ID = "fga-client.response.model_id" + ATTR_FGA_CLIENT_USER = "fga-client.user" + ATTR_HTTP_CLIENT_REQUEST_DURATION = "http.client.request.duration" + ATTR_HTTP_HOST = "http.host" + ATTR_HTTP_REQUEST_METHOD = "http.request.method" + ATTR_HTTP_REQUEST_RESEND_COUNT = "http.request.resend_count" + ATTR_HTTP_RESPONSE_STATUS_CODE = "http.response.status_code" + ATTR_HTTP_SERVER_REQUEST_DURATION = "http.server.request.duration" + ATTR_URL_SCHEME = "url.scheme" + ATTR_URL_FULL = "url.full" + ATTR_USER_AGENT_ORIGINAL = "user_agent.original" +) + +var ( + FGAClientRequestClientID = &Attribute{Name: ATTR_FGA_CLIENT_REQUEST_CLIENT_ID} + FGAClientRequestMethod = &Attribute{Name: ATTR_FGA_CLIENT_REQUEST_METHOD} + FGAClientRequestModelID = &Attribute{Name: ATTR_FGA_CLIENT_REQUEST_MODEL_ID} + FGAClientRequestStoreID = &Attribute{Name: ATTR_FGA_CLIENT_REQUEST_STORE_ID} + FGAClientResponseModelID = &Attribute{Name: ATTR_FGA_CLIENT_RESPONSE_MODEL_ID} + FGAClientUser = &Attribute{Name: ATTR_FGA_CLIENT_USER} + HTTPClientRequestDuration = &Attribute{Name: ATTR_HTTP_CLIENT_REQUEST_DURATION} + HTTPHost = &Attribute{Name: ATTR_HTTP_HOST} + HTTPRequestMethod = &Attribute{Name: ATTR_HTTP_REQUEST_METHOD} + HTTPRequestResendCount = &Attribute{Name: ATTR_HTTP_REQUEST_RESEND_COUNT} + HTTPResponseStatusCode = &Attribute{Name: ATTR_HTTP_RESPONSE_STATUS_CODE} + HTTPServerRequestDuration = &Attribute{Name: ATTR_HTTP_SERVER_REQUEST_DURATION} + URLScheme = &Attribute{Name: ATTR_URL_SCHEME} + URLFull = &Attribute{Name: ATTR_URL_FULL} + UserAgent = &Attribute{Name: ATTR_USER_AGENT_ORIGINAL} +) + +func (m *Metrics) PrepareAttributes(metric MetricInterface, attrs map[*Attribute]string, config *MetricsConfiguration) (attribute.Set, error) { + var prepared []attribute.KeyValue + var allowed *MetricConfiguration + + if config == nil || metric == nil || attrs == nil { + return *attribute.EmptySet(), nil + } + + switch metric.GetName() { + case METRIC_COUNTER_CREDENTIALS_REQUEST: + if config.METRIC_COUNTER_CREDENTIALS_REQUEST == nil { + return *attribute.EmptySet(), nil + } + + allowed = config.METRIC_COUNTER_CREDENTIALS_REQUEST + case METRIC_HISTOGRAM_REQUEST_DURATION: + if config.METRIC_HISTOGRAM_REQUEST_DURATION == nil { + return *attribute.EmptySet(), nil + } + + allowed = config.METRIC_HISTOGRAM_REQUEST_DURATION + case METRIC_HISTOGRAM_QUERY_DURATION: + if config.METRIC_HISTOGRAM_QUERY_DURATION == nil { + return *attribute.EmptySet(), nil + } + + allowed = config.METRIC_HISTOGRAM_QUERY_DURATION + } + + if allowed == nil { + return *attribute.EmptySet(), nil + } + + for attr, value := range attrs { + if attr == nil { + continue + } + + switch attr { + case FGAClientRequestClientID: + if allowed.ATTR_FGA_CLIENT_REQUEST_CLIENT_ID == nil || !allowed.ATTR_FGA_CLIENT_REQUEST_CLIENT_ID.Enabled { + continue + } + case FGAClientRequestMethod: + if allowed.ATTR_HTTP_REQUEST_METHOD == nil || !allowed.ATTR_HTTP_REQUEST_METHOD.Enabled { + continue + } + case FGAClientRequestModelID: + if allowed.ATTR_FGA_CLIENT_REQUEST_MODEL_ID == nil || !allowed.ATTR_FGA_CLIENT_REQUEST_MODEL_ID.Enabled { + continue + } + case FGAClientRequestStoreID: + if allowed.ATTR_FGA_CLIENT_REQUEST_STORE_ID == nil || !allowed.ATTR_FGA_CLIENT_REQUEST_STORE_ID.Enabled { + continue + } + case FGAClientResponseModelID: + if allowed.ATTR_FGA_CLIENT_RESPONSE_MODEL_ID == nil || !allowed.ATTR_FGA_CLIENT_RESPONSE_MODEL_ID.Enabled { + continue + } + case FGAClientUser: + if allowed.ATTR_FGA_CLIENT_USER == nil || !allowed.ATTR_FGA_CLIENT_USER.Enabled { + continue + } + case HTTPClientRequestDuration: + if allowed.ATTR_HTTP_CLIENT_REQUEST_DURATION == nil || !allowed.ATTR_HTTP_CLIENT_REQUEST_DURATION.Enabled { + continue + } + case HTTPHost: + if allowed.ATTR_HTTP_HOST == nil || !allowed.ATTR_HTTP_HOST.Enabled { + continue + } + case HTTPRequestMethod: + if allowed.ATTR_HTTP_REQUEST_METHOD == nil || !allowed.ATTR_HTTP_REQUEST_METHOD.Enabled { + continue + } + case HTTPRequestResendCount: + if allowed.ATTR_HTTP_REQUEST_RESEND_COUNT == nil || !allowed.ATTR_HTTP_REQUEST_RESEND_COUNT.Enabled { + continue + } + case HTTPResponseStatusCode: + if allowed.ATTR_HTTP_RESPONSE_STATUS_CODE == nil || !allowed.ATTR_HTTP_RESPONSE_STATUS_CODE.Enabled { + continue + } + case HTTPServerRequestDuration: + if allowed.ATTR_HTTP_SERVER_REQUEST_DURATION == nil || !allowed.ATTR_HTTP_SERVER_REQUEST_DURATION.Enabled { + continue + } + case URLScheme: + if allowed.ATTR_URL_SCHEME == nil || !allowed.ATTR_URL_SCHEME.Enabled { + continue + } + case URLFull: + if allowed.ATTR_URL_FULL == nil || !allowed.ATTR_URL_FULL.Enabled { + continue + } + case UserAgent: + if allowed.ATTR_USER_AGENT_ORIGINAL == nil || !allowed.ATTR_USER_AGENT_ORIGINAL.Enabled { + continue + } + } + + prepared = append(prepared, attribute.String(attr.Name, value)) + } + + return attribute.NewSet(prepared...), nil +} + +func (m *Metrics) AttributesFromRequest(req *http.Request, params map[string]interface{}) (map[*Attribute]string, error) { + var request = map[*Attribute]string{ + HTTPHost: req.URL.Host, + HTTPRequestMethod: req.Method, + URLFull: req.URL.String(), + URLScheme: req.URL.Scheme, + UserAgent: req.UserAgent(), + } + + if storeId, ok := params["storeId"].(string); ok && storeId != "" { + request[FGAClientRequestStoreID] = storeId + } + + if authorizationModelId, ok := params["authorizationModelId"].(string); ok && authorizationModelId != "" { + request[FGAClientRequestModelID] = authorizationModelId + } + + if body, ok := params["body"]; ok { + requestType := fmt.Sprintf("%T", body) + + switch requestType { + case "*openfga.CheckRequest": + if req, ok := body.(CheckRequestInterface); ok { + if tupleKey := req.GetTupleKey(); tupleKey != nil { + if user := tupleKey.GetUser(); user != nil { + request[FGAClientUser] = *user + } + } + + if modelId := req.GetAuthorizationModelId(); modelId != nil { + request[FGAClientRequestModelID] = *modelId + } + } + case "*openfga.ExpandRequest": + case "*openfga.ListObjectsRequest": + case "*openfga.ListUsersRequest": + case "*openfga.WriteRequest": + if req, ok := body.(RequestAuthorizationModelIdInterface); ok { + if modelId := req.GetAuthorizationModelId(); modelId != nil { + request[FGAClientRequestModelID] = *modelId + } + } + } + } + + return request, nil +} + +func (m *Metrics) AttributesFromResponse(res *http.Response, attrs map[*Attribute]string) (map[*Attribute]string, error) { + attrs[HTTPResponseStatusCode] = strconv.Itoa(res.StatusCode) + + if res.Header.Get("openfga-authorization-model-id") != "" { + attrs[FGAClientResponseModelID] = res.Header.Get("openfga-authorization-model-id") + } + + if res.Header.Get("fga-query-duration-ms") != "" { + attrs[HTTPServerRequestDuration] = res.Header.Get("fga-query-duration-ms") + } + + return attrs, nil +} + +func (m *Metrics) AttributesFromRequestDuration(requestStarted time.Time, attrs map[*Attribute]string) (float64, map[*Attribute]string, error) { + requestDurationFloat := time.Since(requestStarted).Seconds() * 1000 + attrs[HTTPClientRequestDuration] = strconv.FormatFloat(requestDurationFloat, 'f', -1, 64) + + return requestDurationFloat, attrs, nil +} + +func (m *Metrics) AttributesFromQueryDuration(attrs map[*Attribute]string) (float64, map[*Attribute]string, error) { + if attrs[HTTPServerRequestDuration] == "" { + return 0, attrs, nil + } + + queryDurationFloat, queryDurationFloatErr := strconv.ParseFloat(attrs[HTTPServerRequestDuration], 64) + + if queryDurationFloatErr != nil { + return 0, attrs, queryDurationFloatErr + } + + return queryDurationFloat, attrs, nil +} + +func (m *Metrics) AttributesFromResendCount(resendCount int, attrs map[*Attribute]string) (map[*Attribute]string, error) { + if resendCount > 0 { + attrs[HTTPRequestResendCount] = strconv.Itoa(resendCount) + } + + return attrs, nil +} + +func (m *Metrics) BuildTelemetryAttributes(requestMethod string, methodParameters map[string]interface{}, req *http.Request, res *http.Response, requestStarted time.Time, resendCount int) (map[*Attribute]string, float64, float64, error) { + var attrs = make(map[*Attribute]string) + + attrs, _ = m.AttributesFromRequest(req, methodParameters) + attrs, _ = m.AttributesFromResponse(res, attrs) + attrs, _ = m.AttributesFromResendCount(resendCount, attrs) + + var requestDuration, queryDuration float64 + queryDuration, attrs, _ = m.AttributesFromQueryDuration(attrs) + requestDuration, attrs, _ = m.AttributesFromRequestDuration(requestStarted, attrs) + + attrs[FGAClientRequestMethod] = requestMethod + + return attrs, queryDuration, requestDuration, nil +} diff --git a/internal/telemetry/attributes_test.go b/internal/telemetry/attributes_test.go new file mode 100644 index 0000000..7b40b94 --- /dev/null +++ b/internal/telemetry/attributes_test.go @@ -0,0 +1,69 @@ +package telemetry + +import ( + "net/http" + "net/http/httptest" + "testing" + "time" +) + +func TestBuildTelemetryAttributes(t *testing.T) { + metrics := &Metrics{} + + req := httptest.NewRequest(http.MethodGet, "http://example.com", nil) + req.Header.Set("User-Agent", "test-agent") + + res := &http.Response{ + StatusCode: http.StatusOK, + Header: make(http.Header), + } + res.Header.Set("openfga-authorization-model-id", "test-model-id") + res.Header.Set("fga-query-duration-ms", "123") + + methodParameters := map[string]interface{}{ + "storeId": "test-store-id", + "authorizationModelId": "test-model-id", + } + + requestStarted := time.Now().Add(-500 * time.Millisecond) + + resendCount := 2 + + attrs, queryDuration, requestDuration, err := metrics.BuildTelemetryAttributes("TestMethod", methodParameters, req, res, requestStarted, resendCount) + + if err != nil { + t.Errorf("Expected no error from BuildTelemetryAttributes, but got %v", err) + } + + if attrs[FGAClientRequestMethod] != "TestMethod" { + t.Errorf("Expected method to be 'TestMethod', but got %v", attrs[FGAClientRequestMethod]) + } + + if attrs[FGAClientRequestStoreID] != "test-store-id" { + t.Errorf("Expected store ID to be 'test-store-id', but got %v", attrs[FGAClientRequestStoreID]) + } + + if attrs[FGAClientRequestModelID] != "test-model-id" { + t.Errorf("Expected model ID to be 'test-model-id', but got %v", attrs[FGAClientRequestModelID]) + } + + if attrs[FGAClientResponseModelID] != "test-model-id" { + t.Errorf("Expected model ID in response to be 'test-model-id', but got %v", attrs[FGAClientResponseModelID]) + } + + if attrs[HTTPServerRequestDuration] != "123" { + t.Errorf("Expected query duration to be '123', but got %v", attrs[HTTPServerRequestDuration]) + } + + if attrs[HTTPRequestResendCount] != "2" { + t.Errorf("Expected resend count to be '2', but got %v", attrs[HTTPRequestResendCount]) + } + + if requestDuration <= 0 { + t.Errorf("Expected positive request duration, but got %v", requestDuration) + } + + if queryDuration != 123.0 { + t.Errorf("Expected query duration to be 123.0, but got %v", queryDuration) + } +} diff --git a/internal/telemetry/configuration.go b/internal/telemetry/configuration.go new file mode 100644 index 0000000..cbe75d4 --- /dev/null +++ b/internal/telemetry/configuration.go @@ -0,0 +1,79 @@ +package telemetry + +type AttributeConfiguration struct { + Enabled bool `json:"enabled,omitempty"` +} + +type MetricConfiguration struct { + ATTR_FGA_CLIENT_REQUEST_CLIENT_ID *AttributeConfiguration `json:"fga_client_request_client_id,omitempty"` + ATTR_FGA_CLIENT_REQUEST_METHOD *AttributeConfiguration `json:"fga_client_request_method,omitempty"` + ATTR_FGA_CLIENT_REQUEST_MODEL_ID *AttributeConfiguration `json:"fga_client_request_model_id,omitempty"` + ATTR_FGA_CLIENT_REQUEST_STORE_ID *AttributeConfiguration `json:"fga_client_request_store_id,omitempty"` + ATTR_FGA_CLIENT_RESPONSE_MODEL_ID *AttributeConfiguration `json:"fga_client_response_model_id,omitempty"` + ATTR_FGA_CLIENT_USER *AttributeConfiguration `json:"fga_client_user,omitempty"` + ATTR_HTTP_CLIENT_REQUEST_DURATION *AttributeConfiguration `json:"http_client_request_duration,omitempty"` + ATTR_HTTP_HOST *AttributeConfiguration `json:"http_host,omitempty"` + ATTR_HTTP_REQUEST_METHOD *AttributeConfiguration `json:"http_request_method,omitempty"` + ATTR_HTTP_REQUEST_RESEND_COUNT *AttributeConfiguration `json:"http_request_resend_count,omitempty"` + ATTR_HTTP_RESPONSE_STATUS_CODE *AttributeConfiguration `json:"http_response_status_code,omitempty"` + ATTR_HTTP_SERVER_REQUEST_DURATION *AttributeConfiguration `json:"http_server_request_duration,omitempty"` + ATTR_URL_SCHEME *AttributeConfiguration `json:"url_scheme,omitempty"` + ATTR_URL_FULL *AttributeConfiguration `json:"url_full,omitempty"` + ATTR_USER_AGENT_ORIGINAL *AttributeConfiguration `json:"user_agent_original,omitempty"` +} + +type MetricsConfiguration struct { + METRIC_COUNTER_CREDENTIALS_REQUEST *MetricConfiguration `json:"fga_client_credentials_request,omitempty"` + METRIC_HISTOGRAM_REQUEST_DURATION *MetricConfiguration `json:"fga_client_request_duration,omitempty"` + METRIC_HISTOGRAM_QUERY_DURATION *MetricConfiguration `json:"fga_client_query_duration,omitempty"` +} + +type Configuration struct { + Metrics *MetricsConfiguration `json:"metrics,omitempty"` +} + +func DefaultTelemetryConfiguration() *Configuration { + return &Configuration{ + Metrics: &MetricsConfiguration{ + METRIC_COUNTER_CREDENTIALS_REQUEST: &MetricConfiguration{ + ATTR_FGA_CLIENT_REQUEST_CLIENT_ID: &AttributeConfiguration{Enabled: true}, + ATTR_HTTP_REQUEST_METHOD: &AttributeConfiguration{Enabled: true}, + ATTR_FGA_CLIENT_REQUEST_MODEL_ID: &AttributeConfiguration{Enabled: true}, + ATTR_FGA_CLIENT_REQUEST_STORE_ID: &AttributeConfiguration{Enabled: true}, + ATTR_FGA_CLIENT_RESPONSE_MODEL_ID: &AttributeConfiguration{Enabled: true}, + ATTR_HTTP_HOST: &AttributeConfiguration{Enabled: true}, + ATTR_HTTP_REQUEST_RESEND_COUNT: &AttributeConfiguration{Enabled: true}, + ATTR_HTTP_RESPONSE_STATUS_CODE: &AttributeConfiguration{Enabled: true}, + ATTR_URL_FULL: &AttributeConfiguration{Enabled: true}, + ATTR_URL_SCHEME: &AttributeConfiguration{Enabled: true}, + ATTR_USER_AGENT_ORIGINAL: &AttributeConfiguration{Enabled: true}, + }, + METRIC_HISTOGRAM_REQUEST_DURATION: &MetricConfiguration{ + ATTR_FGA_CLIENT_REQUEST_CLIENT_ID: &AttributeConfiguration{Enabled: true}, + ATTR_HTTP_REQUEST_METHOD: &AttributeConfiguration{Enabled: true}, + ATTR_FGA_CLIENT_REQUEST_MODEL_ID: &AttributeConfiguration{Enabled: true}, + ATTR_FGA_CLIENT_REQUEST_STORE_ID: &AttributeConfiguration{Enabled: true}, + ATTR_FGA_CLIENT_RESPONSE_MODEL_ID: &AttributeConfiguration{Enabled: true}, + ATTR_HTTP_HOST: &AttributeConfiguration{Enabled: true}, + ATTR_HTTP_REQUEST_RESEND_COUNT: &AttributeConfiguration{Enabled: true}, + ATTR_HTTP_RESPONSE_STATUS_CODE: &AttributeConfiguration{Enabled: true}, + ATTR_URL_FULL: &AttributeConfiguration{Enabled: true}, + ATTR_URL_SCHEME: &AttributeConfiguration{Enabled: true}, + ATTR_USER_AGENT_ORIGINAL: &AttributeConfiguration{Enabled: true}, + }, + METRIC_HISTOGRAM_QUERY_DURATION: &MetricConfiguration{ + ATTR_FGA_CLIENT_REQUEST_CLIENT_ID: &AttributeConfiguration{Enabled: true}, + ATTR_HTTP_REQUEST_METHOD: &AttributeConfiguration{Enabled: true}, + ATTR_FGA_CLIENT_REQUEST_MODEL_ID: &AttributeConfiguration{Enabled: true}, + ATTR_FGA_CLIENT_REQUEST_STORE_ID: &AttributeConfiguration{Enabled: true}, + ATTR_FGA_CLIENT_RESPONSE_MODEL_ID: &AttributeConfiguration{Enabled: true}, + ATTR_HTTP_HOST: &AttributeConfiguration{Enabled: true}, + ATTR_HTTP_REQUEST_RESEND_COUNT: &AttributeConfiguration{Enabled: true}, + ATTR_HTTP_RESPONSE_STATUS_CODE: &AttributeConfiguration{Enabled: true}, + ATTR_URL_FULL: &AttributeConfiguration{Enabled: true}, + ATTR_URL_SCHEME: &AttributeConfiguration{Enabled: true}, + ATTR_USER_AGENT_ORIGINAL: &AttributeConfiguration{Enabled: true}, + }, + }, + } +} diff --git a/internal/telemetry/configuration_test.go b/internal/telemetry/configuration_test.go new file mode 100644 index 0000000..ff1ab96 --- /dev/null +++ b/internal/telemetry/configuration_test.go @@ -0,0 +1,61 @@ +package telemetry + +import ( + "testing" +) + +func TestDefaultTelemetryConfiguration(t *testing.T) { + config := DefaultTelemetryConfiguration() + + if config == nil { + t.Fatalf("Expected non-nil configuration, but got nil") + } + + if config.Metrics == nil { + t.Fatalf("Expected non-nil Metrics configuration, but got nil") + } + + testMetricConfiguration := func(metricConfig *MetricConfiguration, metricName string) { + if metricConfig == nil { + t.Fatalf("Expected non-nil MetricConfiguration for %s, but got nil", metricName) + } + + if !metricConfig.ATTR_FGA_CLIENT_REQUEST_CLIENT_ID.Enabled { + t.Errorf("Expected %s.ATTR_FGA_CLIENT_REQUEST_CLIENT_ID to be enabled, but it was not", metricName) + } + if !metricConfig.ATTR_HTTP_REQUEST_METHOD.Enabled { + t.Errorf("Expected %s.ATTR_HTTP_REQUEST_METHOD to be enabled, but it was not", metricName) + } + if !metricConfig.ATTR_FGA_CLIENT_REQUEST_MODEL_ID.Enabled { + t.Errorf("Expected %s.ATTR_FGA_CLIENT_REQUEST_MODEL_ID to be enabled, but it was not", metricName) + } + if !metricConfig.ATTR_FGA_CLIENT_REQUEST_STORE_ID.Enabled { + t.Errorf("Expected %s.ATTR_FGA_CLIENT_REQUEST_STORE_ID to be enabled, but it was not", metricName) + } + if !metricConfig.ATTR_FGA_CLIENT_RESPONSE_MODEL_ID.Enabled { + t.Errorf("Expected %s.ATTR_FGA_CLIENT_RESPONSE_MODEL_ID to be enabled, but it was not", metricName) + } + if !metricConfig.ATTR_HTTP_HOST.Enabled { + t.Errorf("Expected %s.ATTR_HTTP_HOST to be enabled, but it was not", metricName) + } + if !metricConfig.ATTR_HTTP_REQUEST_RESEND_COUNT.Enabled { + t.Errorf("Expected %s.ATTR_HTTP_REQUEST_RESEND_COUNT to be enabled, but it was not", metricName) + } + if !metricConfig.ATTR_HTTP_RESPONSE_STATUS_CODE.Enabled { + t.Errorf("Expected %s.ATTR_HTTP_RESPONSE_STATUS_CODE to be enabled, but it was not", metricName) + } + if !metricConfig.ATTR_URL_FULL.Enabled { + t.Errorf("Expected %s.ATTR_URL_FULL to be enabled, but it was not", metricName) + } + if !metricConfig.ATTR_URL_SCHEME.Enabled { + t.Errorf("Expected %s.ATTR_URL_SCHEME to be enabled, but it was not", metricName) + } + if !metricConfig.ATTR_USER_AGENT_ORIGINAL.Enabled { + t.Errorf("Expected %s.ATTR_USER_AGENT_ORIGINAL to be enabled, but it was not", metricName) + } + } + + testMetricConfiguration(config.Metrics.METRIC_COUNTER_CREDENTIALS_REQUEST, "METRIC_COUNTER_CREDENTIALS_REQUEST") + testMetricConfiguration(config.Metrics.METRIC_HISTOGRAM_REQUEST_DURATION, "METRIC_HISTOGRAM_REQUEST_DURATION") + testMetricConfiguration(config.Metrics.METRIC_HISTOGRAM_QUERY_DURATION, "METRIC_HISTOGRAM_QUERY_DURATION") +} diff --git a/internal/telemetry/counter.go b/internal/telemetry/counter.go new file mode 100644 index 0000000..fc75220 --- /dev/null +++ b/internal/telemetry/counter.go @@ -0,0 +1,14 @@ +package telemetry + +type Counter struct { + Name string + Description string +} + +func (m *Counter) GetName() string { + return m.Name +} + +func (m *Counter) GetDescription() string { + return m.Description +} diff --git a/internal/telemetry/counter_test.go b/internal/telemetry/counter_test.go new file mode 100644 index 0000000..2902bd9 --- /dev/null +++ b/internal/telemetry/counter_test.go @@ -0,0 +1,89 @@ +package telemetry + +import ( + "testing" +) + +func TestCounterCreation(t *testing.T) { + counterName := "test-counter" + counterDescription := "This is a test counter." + + counter := &Counter{ + Name: counterName, + Description: counterDescription, + } + + if counter.GetName() != counterName { + t.Errorf("Expected Counter Name to be '%s', but got '%s'", counterName, counter.GetName()) + } + + if counter.GetDescription() != counterDescription { + t.Errorf("Expected Counter Description to be '%s', but got '%s'", counterDescription, counter.GetDescription()) + } +} + +func TestEmptyCounterCreation(t *testing.T) { + counter := &Counter{} + + if counter.GetName() != "" { + t.Errorf("Expected Counter Name to be empty, but got '%s'", counter.GetName()) + } + + if counter.GetDescription() != "" { + t.Errorf("Expected Counter Description to be empty, but got '%s'", counter.GetDescription()) + } +} + +func TestCounterWithWhitespaceName(t *testing.T) { + counterName := " " + counterDescription := "Counter with whitespace name." + + counter := &Counter{ + Name: counterName, + Description: counterDescription, + } + + if counter.GetName() != counterName { + t.Errorf("Expected Counter Name to be '%s', but got '%s'", counterName, counter.GetName()) + } + + if counter.GetDescription() != counterDescription { + t.Errorf("Expected Counter Description to be '%s', but got '%s'", counterDescription, counter.GetDescription()) + } +} + +func TestCounterWithSpecialCharacters(t *testing.T) { + counterName := "!@#$%^&*()_+{}|:\"<>?" + counterDescription := "Description with special characters: !@#$%^&*()_+{}|:\"<>?" + + counter := &Counter{ + Name: counterName, + Description: counterDescription, + } + + if counter.GetName() != counterName { + t.Errorf("Expected Counter Name to be '%s', but got '%s'", counterName, counter.GetName()) + } + + if counter.GetDescription() != counterDescription { + t.Errorf("Expected Counter Description to be '%s', but got '%s'", counterDescription, counter.GetDescription()) + } +} + +func TestCounterWithLongNameAndDescription(t *testing.T) { + counterName := "ThisIsAVeryLongCounterNameToTestEdgeCasesInTheTelemetryModule" + counterDescription := "This is a very long description to test how the Counter struct handles long strings." + + counter := &Counter{ + Name: counterName, + Description: counterDescription, + } + + if counter.GetName() != counterName { + t.Errorf("Expected Counter Name to be '%s', but got '%s'", counterName, counter.GetName()) + } + + if counter.GetDescription() != counterDescription { + t.Errorf("Expected Counter Description to be '%s', but got '%s'", counterDescription, counter.GetDescription()) + } +} diff --git a/internal/telemetry/counters.go b/internal/telemetry/counters.go new file mode 100644 index 0000000..d45babe --- /dev/null +++ b/internal/telemetry/counters.go @@ -0,0 +1,10 @@ +package telemetry + +const ( + METRIC_COUNTER_CREDENTIALS_REQUEST string = "fga-client.credentials.request" +) + +var CredentialsRequest = &Counter{ + Name: METRIC_COUNTER_CREDENTIALS_REQUEST, + Description: "The total number of times new access tokens have been requested using ClientCredentials.", +} diff --git a/internal/telemetry/counters_test.go b/internal/telemetry/counters_test.go new file mode 100644 index 0000000..dd95863 --- /dev/null +++ b/internal/telemetry/counters_test.go @@ -0,0 +1,22 @@ +package telemetry + +import ( + "testing" +) + +func TestCredentialsRequestCounter(t *testing.T) { + expectedName := METRIC_COUNTER_CREDENTIALS_REQUEST + expectedDescription := "The total number of times new access tokens have been requested using ClientCredentials." + + if CredentialsRequest == nil { + t.Fatalf("Expected CredentialsRequest to be initialized, but got nil") + } + + if CredentialsRequest.GetName() != expectedName { + t.Errorf("Expected Name to be '%s', but got '%s'", expectedName, CredentialsRequest.GetName()) + } + + if CredentialsRequest.GetDescription() != expectedDescription { + t.Errorf("Expected Description to be '%s', but got '%s'", expectedDescription, CredentialsRequest.GetDescription()) + } +} diff --git a/internal/telemetry/histogram.go b/internal/telemetry/histogram.go new file mode 100644 index 0000000..962456c --- /dev/null +++ b/internal/telemetry/histogram.go @@ -0,0 +1,19 @@ +package telemetry + +type Histogram struct { + Name string + Unit string + Description string +} + +func (m *Histogram) GetName() string { + return m.Name +} + +func (m *Histogram) GetDescription() string { + return m.Description +} + +func (m *Histogram) GetUnit() string { + return m.Unit +} diff --git a/internal/telemetry/histogram_test.go b/internal/telemetry/histogram_test.go new file mode 100644 index 0000000..ff32ccc --- /dev/null +++ b/internal/telemetry/histogram_test.go @@ -0,0 +1,93 @@ +package telemetry + +import ( + "testing" +) + +func TestHistogramCreation(t *testing.T) { + histogramName := "request_duration" + histogramUnit := "milliseconds" + histogramDescription := "The duration of client requests." + + histogram := &Histogram{ + Name: histogramName, + Unit: histogramUnit, + Description: histogramDescription, + } + + if histogram.GetName() != histogramName { + t.Errorf("Expected Histogram Name to be '%s', but got '%s'", histogramName, histogram.GetName()) + } + + if histogram.GetUnit() != histogramUnit { + t.Errorf("Expected Histogram Unit to be '%s', but got '%s'", histogramUnit, histogram.GetUnit()) + } + + if histogram.GetDescription() != histogramDescription { + t.Errorf("Expected Histogram Description to be '%s', but got '%s'", histogramDescription, histogram.GetDescription()) + } +} + +func TestEmptyHistogramCreation(t *testing.T) { + histogram := &Histogram{} + + if histogram.GetName() != "" { + t.Errorf("Expected Histogram Name to be empty, but got '%s'", histogram.GetName()) + } + + if histogram.GetUnit() != "" { + t.Errorf("Expected Histogram Unit to be empty, but got '%s'", histogram.GetUnit()) + } + + if histogram.GetDescription() != "" { + t.Errorf("Expected Histogram Description to be empty, but got '%s'", histogram.GetDescription()) + } +} + +func TestHistogramWithSpecialCharacters(t *testing.T) { + histogramName := "request_duration!@#$%" + histogramUnit := "ms!@#$%" + histogramDescription := "The duration of client requests!@#$%." + + histogram := &Histogram{ + Name: histogramName, + Unit: histogramUnit, + Description: histogramDescription, + } + + if histogram.GetName() != histogramName { + t.Errorf("Expected Histogram Name to be '%s', but got '%s'", histogramName, histogram.GetName()) + } + + if histogram.GetUnit() != histogramUnit { + t.Errorf("Expected Histogram Unit to be '%s', but got '%s'", histogramUnit, histogram.GetUnit()) + } + + if histogram.GetDescription() != histogramDescription { + t.Errorf("Expected Histogram Description to be '%s', but got '%s'", histogramDescription, histogram.GetDescription()) + } +} + +func TestHistogramWithLongStrings(t *testing.T) { + histogramName := "this_is_a_very_long_histogram_name_to_test_edge_cases_in_the_telemetry_module" + histogramUnit := "milliseconds_with_long_unit_name" + histogramDescription := "This is a very long description to test how the Histogram struct handles long strings in the telemetry module." + + histogram := &Histogram{ + Name: histogramName, + Unit: histogramUnit, + Description: histogramDescription, + } + + if histogram.GetName() != histogramName { + t.Errorf("Expected Histogram Name to be '%s', but got '%s'", histogramName, histogram.GetName()) + } + + if histogram.GetUnit() != histogramUnit { + t.Errorf("Expected Histogram Unit to be '%s', but got '%s'", histogramUnit, histogram.GetUnit()) + } + + if histogram.GetDescription() != histogramDescription { + t.Errorf("Expected Histogram Description to be '%s', but got '%s'", histogramDescription, histogram.GetDescription()) + } +} diff --git a/internal/telemetry/histograms.go b/internal/telemetry/histograms.go new file mode 100644 index 0000000..7788047 --- /dev/null +++ b/internal/telemetry/histograms.go @@ -0,0 +1,20 @@ +package telemetry + +const ( + METRIC_HISTOGRAM_REQUEST_DURATION string = "fga-client.request.duration" + METRIC_HISTOGRAM_QUERY_DURATION string = "fga-client.query.duration" +) + +var ( + RequestDuration = &Histogram{ + Name: METRIC_HISTOGRAM_REQUEST_DURATION, + Unit: "milliseconds", + Description: "The total time (in milliseconds) it took for the request to complete, including the time it took to send the request and receive the response.", + } + + QueryDuration = &Histogram{ + Name: METRIC_HISTOGRAM_QUERY_DURATION, + Unit: "milliseconds", + Description: "The total time it took (in milliseconds) for the FGA server to process and evaluate the request.", + } +) diff --git a/internal/telemetry/histograms_test.go b/internal/telemetry/histograms_test.go new file mode 100644 index 0000000..af93f8d --- /dev/null +++ b/internal/telemetry/histograms_test.go @@ -0,0 +1,49 @@ +package telemetry + +import ( + "testing" +) + +func TestRequestDurationHistogram(t *testing.T) { + expectedName := METRIC_HISTOGRAM_REQUEST_DURATION + expectedUnit := "milliseconds" + expectedDescription := "The total time (in milliseconds) it took for the request to complete, including the time it took to send the request and receive the response." + + if RequestDuration == nil { + t.Fatalf("Expected RequestDuration to be initialized, but got nil") + } + + if RequestDuration.GetName() != expectedName { + t.Errorf("Expected RequestDuration Name to be '%s', but got '%s'", expectedName, RequestDuration.GetName()) + } + + if RequestDuration.GetUnit() != expectedUnit { + t.Errorf("Expected RequestDuration Unit to be '%s', but got '%s'", expectedUnit, RequestDuration.GetUnit()) + } + + if RequestDuration.GetDescription() != expectedDescription { + t.Errorf("Expected RequestDuration Description to be '%s', but got '%s'", expectedDescription, RequestDuration.GetDescription()) + } +} + +func TestQueryDurationHistogram(t *testing.T) { + expectedName := METRIC_HISTOGRAM_QUERY_DURATION + expectedUnit := "milliseconds" + expectedDescription := "The total time it took (in milliseconds) for the FGA server to process and evaluate the request." + + if QueryDuration == nil { + t.Fatalf("Expected QueryDuration to be initialized, but got nil") + } + + if QueryDuration.GetName() != expectedName { + t.Errorf("Expected QueryDuration Name to be '%s', but got '%s'", expectedName, QueryDuration.GetName()) + } + + if QueryDuration.GetUnit() != expectedUnit { + t.Errorf("Expected QueryDuration Unit to be '%s', but got '%s'", expectedUnit, QueryDuration.GetUnit()) + } + + if QueryDuration.GetDescription() != expectedDescription { + t.Errorf("Expected QueryDuration Description to be '%s', but got '%s'", expectedDescription, QueryDuration.GetDescription()) + } +} diff --git a/internal/telemetry/interfaces.go b/internal/telemetry/interfaces.go new file mode 100644 index 0000000..c580ed7 --- /dev/null +++ b/internal/telemetry/interfaces.go @@ -0,0 +1,23 @@ +package telemetry + +/* +CheckRequestTupleKeyInterface is a simplified interface that defines the methods the CheckRequestTupleKey struct implements, relevant to the context of the telemetry package. +*/ +type CheckRequestTupleKeyInterface interface { + GetUser() *string +} + +/* +CheckRequestInterface is a simplified interface that defines the methods the CheckRequest struct implements, relevant to the context of the telemetry package. +*/ +type CheckRequestInterface interface { + GetTupleKey() CheckRequestTupleKeyInterface + RequestAuthorizationModelIdInterface +} + +/* +RequestAuthorizationModelIdInterface is a generic interface that defines the GetAuthorizationModelId() method a Request struct implements, relevant to the context of the telemetry package. +*/ +type RequestAuthorizationModelIdInterface interface { + GetAuthorizationModelId() *string +} diff --git a/internal/telemetry/interfaces_test.go b/internal/telemetry/interfaces_test.go new file mode 100644 index 0000000..573bec8 --- /dev/null +++ b/internal/telemetry/interfaces_test.go @@ -0,0 +1,60 @@ +package telemetry + +import ( + "testing" +) + +type MockCheckRequestTupleKey struct { + user *string +} + +func (m *MockCheckRequestTupleKey) GetUser() *string { + return m.user +} + +type MockRequestAuthorizationModelId struct { + authorizationModelId *string +} + +func (m *MockRequestAuthorizationModelId) GetAuthorizationModelId() *string { + return m.authorizationModelId +} + +type MockCheckRequest struct { + MockCheckRequestTupleKey + MockRequestAuthorizationModelId +} + +func (m *MockCheckRequest) GetTupleKey() *MockCheckRequestTupleKey { + return &m.MockCheckRequestTupleKey +} + +func TestCheckRequestInterfaceImplementation(t *testing.T) { + user := "test-user" + modelId := "test-model-id" + + mockCheckRequest := &MockCheckRequest{ + MockCheckRequestTupleKey: MockCheckRequestTupleKey{user: &user}, + MockRequestAuthorizationModelId: MockRequestAuthorizationModelId{authorizationModelId: &modelId}, + } + + if mockCheckRequest.GetTupleKey().GetUser() != &user { + t.Errorf("Expected GetUser to return '%s', but got '%s'", user, *mockCheckRequest.GetTupleKey().GetUser()) + } + + if mockCheckRequest.GetAuthorizationModelId() != &modelId { + t.Errorf("Expected GetAuthorizationModelId to return '%s', but got '%s'", modelId, *mockCheckRequest.GetAuthorizationModelId()) + } +} + +func TestCheckRequestInterfaceNilValues(t *testing.T) { + mockCheckRequest := &MockCheckRequest{} + + if mockCheckRequest.GetTupleKey().GetUser() != nil { + t.Errorf("Expected GetUser to return nil, but got '%s'", *mockCheckRequest.GetTupleKey().GetUser()) + } + + if mockCheckRequest.GetAuthorizationModelId() != nil { + t.Errorf("Expected GetAuthorizationModelId to return nil, but got '%s'", *mockCheckRequest.GetAuthorizationModelId()) + } +} diff --git a/internal/telemetry/metric.go b/internal/telemetry/metric.go new file mode 100644 index 0000000..cecac74 --- /dev/null +++ b/internal/telemetry/metric.go @@ -0,0 +1,20 @@ +package telemetry + +type Metric struct { + Name string + Description string + MetricInterface +} + +type MetricInterface interface { + GetName() string + GetDescription() string +} + +func (m *Metric) GetName() string { + return m.Name +} + +func (m *Metric) GetDescription() string { + return m.Description +} diff --git a/internal/telemetry/metric_test.go b/internal/telemetry/metric_test.go new file mode 100644 index 0000000..ba452fe --- /dev/null +++ b/internal/telemetry/metric_test.go @@ -0,0 +1,76 @@ +package telemetry + +import ( + "testing" +) + +type MockMetric struct { + Name string + Description string +} + +func (m *MockMetric) GetName() string { + return m.Name +} + +func (m *MockMetric) GetDescription() string { + return m.Description +} + +func TestMetric_GetName(t *testing.T) { + metricName := "test-metric" + metricDescription := "This is a test metric." + + metric := &Metric{ + Name: metricName, + Description: metricDescription, + MetricInterface: &MockMetric{ + Name: metricName, + Description: metricDescription, + }, + } + + if metric.GetName() != metricName { + t.Errorf("Expected Metric Name to be '%s', but got '%s'", metricName, metric.GetName()) + } + + if metric.GetDescription() != metricDescription { + t.Errorf("Expected Metric Description to be '%s', but got '%s'", metricDescription, metric.GetDescription()) + } +} + +func TestMetricInterfaceImplementation(t *testing.T) { + metricName := "interface-metric" + metricDescription := "Metric implemented using MetricInterface." + + mockMetric := &MockMetric{ + Name: metricName, + Description: metricDescription, + } + + metric := &Metric{ + Name: metricName, + Description: metricDescription, + MetricInterface: mockMetric, + } + + if metric.MetricInterface.GetName() != metricName { + t.Errorf("Expected MetricInterface Name to be '%s', but got '%s'", metricName, metric.MetricInterface.GetName()) + } + + if metric.MetricInterface.GetDescription() != metricDescription { + t.Errorf("Expected MetricInterface Description to be '%s', but got '%s'", metricDescription, metric.MetricInterface.GetDescription()) + } +} + +func TestEmptyMetric(t *testing.T) { + metric := &Metric{} + + if metric.GetName() != "" { + t.Errorf("Expected Metric Name to be empty, but got '%s'", metric.GetName()) + } + + if metric.GetDescription() != "" { + t.Errorf("Expected Metric Description to be empty, but got '%s'", metric.GetDescription()) + } +} diff --git a/internal/telemetry/metrics.go b/internal/telemetry/metrics.go new file mode 100644 index 0000000..25e99cb --- /dev/null +++ b/internal/telemetry/metrics.go @@ -0,0 +1,87 @@ +package telemetry + +import ( + "context" + "net/http" + "time" + + "go.opentelemetry.io/otel/metric" +) + +type Metrics struct { + Meter metric.Meter + Counters map[string]metric.Int64Counter + Histograms map[string]metric.Float64Histogram + Configuration *MetricsConfiguration +} + +type MetricsInterface interface { + GetCounter(name string, description string) (metric.Int64Counter, error) + GetHistogram(name string, description string, unit string) (metric.Float64Histogram, error) + CredentialsRequest(value int64, attrs map[*Attribute]string) (metric.Int64Counter, error) + RequestDuration(value float64, attrs map[*Attribute]string) (metric.Float64Histogram, error) + QueryDuration(value float64, attrs map[*Attribute]string) (metric.Float64Histogram, error) + BuildTelemetryAttributes(requestMethod string, methodParameters map[string]interface{}, req *http.Request, res *http.Response, requestStarted time.Time, resendCount int) (map[*Attribute]string, float64, float64, error) +} + +func (m *Metrics) GetCounter(name string, description string) (metric.Int64Counter, error) { + if counter, exists := m.Counters[name]; exists { + return counter, nil + } + counter, _ := m.Meter.Int64Counter(name, metric.WithDescription(description)) + m.Counters[name] = counter + return counter, nil +} + +func (m *Metrics) GetHistogram(name string, description string, unit string) (metric.Float64Histogram, error) { + if histogram, exists := m.Histograms[name]; exists { + return histogram, nil + } + + histogram, _ := m.Meter.Float64Histogram(name, metric.WithDescription(description), metric.WithUnit(unit)) + m.Histograms[name] = histogram + + return histogram, nil +} + +func (m *Metrics) CredentialsRequest(value int64, attrs map[*Attribute]string) (metric.Int64Counter, error) { + var counter, err = m.GetCounter(CredentialsRequest.Name, CredentialsRequest.Description) + + if err == nil { + attrs, err := m.PrepareAttributes(CredentialsRequest, attrs, m.Configuration) + + if err == nil { + counter.Add(context.Background(), value, metric.WithAttributeSet(attrs)) + } + } + + return counter, err +} + +func (m *Metrics) RequestDuration(value float64, attrs map[*Attribute]string) (metric.Float64Histogram, error) { + var histogram, err = m.GetHistogram(RequestDuration.Name, RequestDuration.Description, RequestDuration.Unit) + + if err == nil { + attrs, err := m.PrepareAttributes(RequestDuration, attrs, m.Configuration) + + if err == nil { + histogram.Record(context.Background(), value, metric.WithAttributeSet(attrs)) + } + } + + return histogram, err +} + +func (m *Metrics) QueryDuration(value float64, attrs map[*Attribute]string) (metric.Float64Histogram, error) { + var histogram, err = m.GetHistogram(QueryDuration.Name, QueryDuration.Description, QueryDuration.Unit) + + if err == nil { + attrs, err := m.PrepareAttributes(QueryDuration, attrs, m.Configuration) + + if err == nil { + histogram.Record(context.Background(), value, metric.WithAttributeSet(attrs)) + } + } + + return histogram, err +} diff --git a/internal/telemetry/metrics_test.go b/internal/telemetry/metrics_test.go new file mode 100644 index 0000000..b6f0388 --- /dev/null +++ b/internal/telemetry/metrics_test.go @@ -0,0 +1,192 @@ +package telemetry + +import ( + "context" + "testing" + + "go.opentelemetry.io/otel/metric" +) + +// Mock Int64Counter implementation +type MockInt64Counter struct { + metric.Int64Counter + addCalled bool +} + +func (m *MockInt64Counter) Add(ctx context.Context, value int64, opts ...metric.AddOption) { + m.addCalled = true +} + +// Mock Float64Histogram implementation +type MockFloat64Histogram struct { + metric.Float64Histogram + recordCalled bool +} + +func (m *MockFloat64Histogram) Record(ctx context.Context, value float64, opts ...metric.RecordOption) { + m.recordCalled = true +} + +// Mock Meter implementation +type MockMeter struct { + metric.Meter + counters map[string]metric.Int64Counter + histograms map[string]metric.Float64Histogram +} + +func (m *MockMeter) Int64Counter(name string, opts ...metric.Int64CounterOption) (metric.Int64Counter, error) { + if counter, exists := m.counters[name]; exists { + return counter, nil + } + counter := &MockInt64Counter{} + m.counters[name] = counter + return counter, nil +} + +func (m *MockMeter) Float64Histogram(name string, opts ...metric.Float64HistogramOption) (metric.Float64Histogram, error) { + if histogram, exists := m.histograms[name]; exists { + return histogram, nil + } + histogram := &MockFloat64Histogram{} + m.histograms[name] = histogram + return histogram, nil +} + +func TestGetCounter(t *testing.T) { + mockMeter := &MockMeter{ + counters: make(map[string]metric.Int64Counter), + histograms: make(map[string]metric.Float64Histogram), + } + metrics := &Metrics{ + Meter: mockMeter, + Counters: make(map[string]metric.Int64Counter), + } + + counter, err := metrics.GetCounter("test_counter", "A test counter") + if err != nil { + t.Fatalf("Expected no error, but got %v", err) + } + + if counter == nil { + t.Fatalf("Expected a non-nil counter, but got nil") + } + + counter2, err := metrics.GetCounter("test_counter", "A test counter") + if err != nil { + t.Fatalf("Expected no error, but got %v", err) + } + + if counter != counter2 { + t.Fatalf("Expected the same counter instance to be returned") + } +} + +func TestGetHistogram(t *testing.T) { + mockMeter := &MockMeter{ + counters: make(map[string]metric.Int64Counter), + histograms: make(map[string]metric.Float64Histogram), + } + metrics := &Metrics{ + Meter: mockMeter, + Histograms: make(map[string]metric.Float64Histogram), + } + + histogram, err := metrics.GetHistogram("test_histogram", "A test histogram", "ms") + if err != nil { + t.Fatalf("Expected no error, but got %v", err) + } + + if histogram == nil { + t.Fatalf("Expected a non-nil histogram, but got nil") + } + + histogram2, err := metrics.GetHistogram("test_histogram", "A test histogram", "ms") + if err != nil { + t.Fatalf("Expected no error, but got %v", err) + } + + if histogram != histogram2 { + t.Fatalf("Expected the same histogram instance to be returned") + } +} + +func TestCredentialsRequest(t *testing.T) { + mockMeter := &MockMeter{ + counters: make(map[string]metric.Int64Counter), + histograms: make(map[string]metric.Float64Histogram), + } + metrics := &Metrics{ + Meter: mockMeter, + Counters: make(map[string]metric.Int64Counter), + } + + attrs := make(map[*Attribute]string) + + counter, err := metrics.CredentialsRequest(1, attrs) + if err != nil { + t.Fatalf("Expected no error, but got %v", err) + } + + if counter == nil { + t.Fatalf("Expected a non-nil counter, but got nil") + } + + mockCounter, ok := counter.(*MockInt64Counter) + if !ok || !mockCounter.addCalled { + t.Fatalf("Expected Add method to be called on counter") + } +} + +func TestRequestDuration(t *testing.T) { + mockMeter := &MockMeter{ + counters: make(map[string]metric.Int64Counter), + histograms: make(map[string]metric.Float64Histogram), + } + metrics := &Metrics{ + Meter: mockMeter, + Histograms: make(map[string]metric.Float64Histogram), + } + + attrs := make(map[*Attribute]string) + + histogram, err := metrics.RequestDuration(1.0, attrs) + if err != nil { + t.Fatalf("Expected no error, but got %v", err) + } + + if histogram == nil { + t.Fatalf("Expected a non-nil histogram, but got nil") + } + + mockHistogram, ok := histogram.(*MockFloat64Histogram) + if !ok || !mockHistogram.recordCalled { + t.Fatalf("Expected Record method to be called on histogram") + } +} + +func TestQueryDuration(t *testing.T) { + mockMeter := &MockMeter{ + counters: make(map[string]metric.Int64Counter), + histograms: make(map[string]metric.Float64Histogram), + } + metrics := &Metrics{ + Meter: mockMeter, + Histograms: make(map[string]metric.Float64Histogram), + } + + attrs := make(map[*Attribute]string) + + histogram, err := metrics.QueryDuration(1.0, attrs) + if err != nil { + t.Fatalf("Expected no error, but got %v", err) + } + + if histogram == nil { + t.Fatalf("Expected a non-nil histogram, but got nil") + } + + mockHistogram, ok := histogram.(*MockFloat64Histogram) + if !ok || !mockHistogram.recordCalled { + t.Fatalf("Expected Record method to be called on histogram") + } +} diff --git a/internal/telemetry/telemetry.go b/internal/telemetry/telemetry.go new file mode 100644 index 0000000..e1d02d2 --- /dev/null +++ b/internal/telemetry/telemetry.go @@ -0,0 +1,114 @@ +package telemetry + +import ( + "context" + + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/metric" +) + +type TelemetryInterface interface { + Configure(configuration *Configuration) (*Telemetry, error) + Get(configuration *Configuration) *Telemetry +} + +type Telemetry struct { + Metrics MetricsInterface + Configuration *Configuration +} + +type TelemetryFactoryParameters struct { + Configuration *Configuration +} + +type CredentialsRequestMetricParameters struct { + Value int64 + Attrs map[*Attribute]string + TelemetryFactoryParameters +} + +type RequestDurationMetricParameters struct { + Value float64 + Attrs map[*Attribute]string + TelemetryFactoryParameters +} + +type QueryDurationMetricParameters struct { + Value float64 + Attrs map[*Attribute]string + TelemetryFactoryParameters +} + +type TelemetryContextKey struct{} + +var ( + TelemetryInstances map[*Configuration]*Telemetry + TelemetryContext TelemetryContextKey +) + +func Configure(configuration *Configuration) (*Telemetry, error) { + return &Telemetry{ + Metrics: &Metrics{ + Meter: otel.Meter("openfga-sdk/0.5.0"), + Counters: make(map[string]metric.Int64Counter), + Histograms: make(map[string]metric.Float64Histogram), + Configuration: configuration.Metrics, + }, + Configuration: configuration, + }, nil +} + +func Get(factory TelemetryFactoryParameters) *Telemetry { + configuration := factory.Configuration + + if configuration == nil { + configuration = DefaultTelemetryConfiguration() + } + + if TelemetryInstances == nil { + TelemetryInstances = make(map[*Configuration]*Telemetry) + } + + if _, exists := TelemetryInstances[configuration]; !exists { + telemetry, _ := Configure(configuration) + TelemetryInstances[configuration] = telemetry + } + + return TelemetryInstances[configuration] +} + +func Bind(ctx context.Context, instance *Telemetry) context.Context { + return context.WithValue(ctx, TelemetryContext, instance) +} + +func Unbind(ctx context.Context) context.Context { + return context.WithValue(ctx, TelemetryContext, nil) +} + +func Extract(ctx context.Context) *Telemetry { + if ctx == nil { + return nil + } + + if instance, ok := ctx.Value(TelemetryContext).(*Telemetry); ok { + return instance + } + + return nil +} + +func GetMetrics(factory TelemetryFactoryParameters) MetricsInterface { + return Get(factory).Metrics +} + +func CredentialsRequestMetric(factory CredentialsRequestMetricParameters) (metric.Int64Counter, error) { + return GetMetrics(TelemetryFactoryParameters{Configuration: factory.Configuration}).CredentialsRequest(factory.Value, factory.Attrs) +} + +func RequestDurationMetric(factory RequestDurationMetricParameters) (metric.Float64Histogram, error) { + return GetMetrics(TelemetryFactoryParameters{Configuration: factory.Configuration}).RequestDuration(factory.Value, factory.Attrs) +} + +func QueryDurationMetric(factory QueryDurationMetricParameters) (metric.Float64Histogram, error) { + return GetMetrics(TelemetryFactoryParameters{Configuration: factory.Configuration}).QueryDuration(factory.Value, factory.Attrs) +} diff --git a/internal/telemetry/telemetry_test.go b/internal/telemetry/telemetry_test.go new file mode 100644 index 0000000..50446e3 --- /dev/null +++ b/internal/telemetry/telemetry_test.go @@ -0,0 +1,227 @@ +package telemetry + +import ( + "context" + "net/http" + "testing" + "time" + + "go.opentelemetry.io/otel/metric" +) + +// Mock implementation of MetricsInterface +type MockMetrics struct { + counters map[string]metric.Int64Counter + histograms map[string]metric.Float64Histogram +} + +func (m *MockMetrics) GetCounter(name string, description string) (metric.Int64Counter, error) { + if counter, exists := m.counters[name]; exists { + return counter, nil + } + counter := &MockInt64Counter{} + m.counters[name] = counter + return counter, nil +} + +func (m *MockMetrics) GetHistogram(name string, description string, unit string) (metric.Float64Histogram, error) { + if histogram, exists := m.histograms[name]; exists { + return histogram, nil + } + histogram := &MockFloat64Histogram{} + m.histograms[name] = histogram + return histogram, nil +} + +func (m *MockMetrics) CredentialsRequest(value int64, attrs map[*Attribute]string) (metric.Int64Counter, error) { + counter, _ := m.GetCounter("credentials_request", "A credentials request") + return counter, nil +} + +func (m *MockMetrics) RequestDuration(value float64, attrs map[*Attribute]string) (metric.Float64Histogram, error) { + histogram, _ := m.GetHistogram("request_duration", "A request duration", "ms") + return histogram, nil +} + +func (m *MockMetrics) QueryDuration(value float64, attrs map[*Attribute]string) (metric.Float64Histogram, error) { + histogram, _ := m.GetHistogram("query_duration", "A query duration", "ms") + return histogram, nil +} + +func (m *MockMetrics) BuildTelemetryAttributes(requestMethod string, methodParameters map[string]interface{}, req *http.Request, res *http.Response, requestStarted time.Time, resendCount int) (map[*Attribute]string, float64, float64, error) { + attrs := map[*Attribute]string{ + HTTPRequestMethod: requestMethod, + } + + requestDuration := float64(100) + queryDuration := float64(50) + + return attrs, queryDuration, requestDuration, nil +} + +func TestConfigure(t *testing.T) { + config := &Configuration{} + telemetry, err := Configure(config) + + if err != nil { + t.Fatalf("Expected no error, but got %v", err) + } + + if telemetry == nil { + t.Fatalf("Expected telemetry to be non-nil") + } + + if telemetry.Configuration != config { + t.Fatalf("Expected configuration to be set correctly") + } +} + +func TestGet(t *testing.T) { + config := &Configuration{} + factoryParams := TelemetryFactoryParameters{Configuration: config} + + telemetry := Get(factoryParams) + + if telemetry == nil { + t.Fatalf("Expected telemetry to be non-nil") + } + + if telemetry.Configuration != config { + t.Fatalf("Expected configuration to be set correctly") + } +} + +func TestBindAndExtract(t *testing.T) { + config := &Configuration{} + telemetry := &Telemetry{Configuration: config} + + ctx := Bind(context.Background(), telemetry) + extractedTelemetry := Extract(ctx) + + if extractedTelemetry == nil { + t.Fatalf("Expected extracted telemetry to be non-nil") + } + + if extractedTelemetry.Configuration != config { + t.Fatalf("Expected extracted telemetry configuration to be set correctly") + } +} + +func TestUnbind(t *testing.T) { + config := &Configuration{} + telemetry := &Telemetry{Configuration: config} + + ctx := Bind(context.Background(), telemetry) + ctx = Unbind(ctx) + extractedTelemetry := Extract(ctx) + + if extractedTelemetry != nil { + t.Fatalf("Expected extracted telemetry to be nil after unbinding") + } +} + +func TestCredentialsRequestMetric(t *testing.T) { + config := &Configuration{} + metrics := &MockMetrics{ + counters: make(map[string]metric.Int64Counter), + histograms: make(map[string]metric.Float64Histogram), + } + telemetry := &Telemetry{Metrics: metrics, Configuration: config} + Bind(context.Background(), telemetry) + + factoryParams := CredentialsRequestMetricParameters{ + Value: 1, + Attrs: make(map[*Attribute]string), + TelemetryFactoryParameters: TelemetryFactoryParameters{Configuration: config}, + } + + counter, err := CredentialsRequestMetric(factoryParams) + if err != nil { + t.Fatalf("Expected no error, but got %v", err) + } + + if counter == nil { + t.Fatalf("Expected counter to be non-nil") + } +} + +func TestRequestDurationMetric(t *testing.T) { + config := &Configuration{} + metrics := &MockMetrics{ + counters: make(map[string]metric.Int64Counter), + histograms: make(map[string]metric.Float64Histogram), + } + telemetry := &Telemetry{Metrics: metrics, Configuration: config} + Bind(context.Background(), telemetry) + + factoryParams := RequestDurationMetricParameters{ + Value: 1.0, + Attrs: make(map[*Attribute]string), + TelemetryFactoryParameters: TelemetryFactoryParameters{Configuration: config}, + } + + histogram, err := RequestDurationMetric(factoryParams) + if err != nil { + t.Fatalf("Expected no error, but got %v", err) + } + + if histogram == nil { + t.Fatalf("Expected histogram to be non-nil") + } +} + +func TestQueryDurationMetric(t *testing.T) { + config := &Configuration{} + metrics := &MockMetrics{ + counters: make(map[string]metric.Int64Counter), + histograms: make(map[string]metric.Float64Histogram), + } + telemetry := &Telemetry{Metrics: metrics, Configuration: config} + Bind(context.Background(), telemetry) + + factoryParams := QueryDurationMetricParameters{ + Value: 1.0, + Attrs: make(map[*Attribute]string), + TelemetryFactoryParameters: TelemetryFactoryParameters{Configuration: config}, + } + + histogram, err := QueryDurationMetric(factoryParams) + if err != nil { + t.Fatalf("Expected no error, but got %v", err) + } + + if histogram == nil { + t.Fatalf("Expected histogram to be non-nil") + } +} + +func TestBuildTelemetryAttributesMethod(t *testing.T) { + config := &Configuration{} + metrics := &MockMetrics{ + counters: make(map[string]metric.Int64Counter), + histograms: make(map[string]metric.Float64Histogram), + } + telemetry := &Telemetry{Metrics: metrics, Configuration: config} + Bind(context.Background(), telemetry) + + req := &http.Request{} + res := &http.Response{} + requestStarted := time.Now() + + attrs, queryDuration, requestDuration, err := metrics.BuildTelemetryAttributes("GET", make(map[string]interface{}), req, res, requestStarted, 0) + if err != nil { + t.Fatalf("Expected no error, but got %v", err) + } + + if len(attrs) == 0 { + t.Fatalf("Expected non-empty attributes") + } + + if queryDuration != 50 { + t.Fatalf("Expected queryDuration to be 50, but got %v", queryDuration) + } + + if requestDuration != 100 { + t.Fatalf("Expected requestDuration to be 100, but got %v", requestDuration) + } +} diff --git a/oauth2/clientcredentials/clientcredentials.go b/oauth2/clientcredentials/clientcredentials.go index 6b2e486..be87191 100644 --- a/oauth2/clientcredentials/clientcredentials.go +++ b/oauth2/clientcredentials/clientcredentials.go @@ -16,11 +16,12 @@ package clientcredentials import ( "context" "fmt" - "github.com/openfga/go-sdk/oauth2" - "github.com/openfga/go-sdk/oauth2/internal" "net/http" "net/url" "strings" + + "github.com/openfga/go-sdk/oauth2" + "github.com/openfga/go-sdk/oauth2/internal" ) // Config describes a 2-legged OAuth2 flow, with both the diff --git a/oauth2/internal/token.go b/oauth2/internal/token.go index bd38600..44f1ca2 100644 --- a/oauth2/internal/token.go +++ b/oauth2/internal/token.go @@ -20,6 +20,7 @@ import ( "sync" "time" + "github.com/openfga/go-sdk/internal/telemetry" internalutils "github.com/openfga/go-sdk/internal/utils" ) @@ -295,6 +296,11 @@ func doTokenRoundTrip(ctx context.Context, req *http.Request) (*Token, error) { token, err = singleTokenRoundTrip(ctx, req) if err == nil { + if otel := telemetry.Extract(ctx); otel != nil { + attrs := make(map[*telemetry.Attribute]string) + attrs[telemetry.HTTPRequestResendCount] = strconv.Itoa(i) + otel.Metrics.CredentialsRequest(1, attrs) + } return token, err }