diff --git a/README.md b/README.md index afd15b9..a7683a0 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,7 @@ This is an autogenerated Go SDK for OpenFGA. It provides a wrapper around the [O - [Retries](#retries) - [API Endpoints](#api-endpoints) - [Models](#models) + - [OpenTelemetry](#opentelemetry) - [Contributing](#contributing) - [Issues](#issues) - [Pull Requests](#pull-requests) @@ -1035,6 +1036,10 @@ Class | Method | HTTP request | Description - [WriteRequestWrites](docs/WriteRequestWrites.md) +### OpenTelemetry + +This SDK supports producing metrics that can be consumed as part of an [OpenTelemetry](https://opentelemetry.io/) setup. For more information, please see [the documentation](https://github.com/openfga/java-sdk/blob/main/docs/OpenTelemetry.md) + ## Contributing ### Issues diff --git a/docs/OpenTelemetry.md b/docs/OpenTelemetry.md new file mode 100644 index 0000000..7afed5e --- /dev/null +++ b/docs/OpenTelemetry.md @@ -0,0 +1,43 @@ +# OpenTelemetry + +This SDK produces [metrics](https://opentelemetry.io/docs/concepts/signals/metrics/) using [OpenTelemetry](https://opentelemetry.io/) that allow you to view data such as request timings. These metrics also include attributes for the model and store ID, as well as the API called to allow you to build reporting. + +When an OpenTelemetry SDK instance is configured, the metrics will be exported and sent to the collector configured as part of your applications configuration. If you are not using OpenTelemetry, the metric functionality is a no-op and the events are never sent. + +In cases when metrics events are sent, they will not be viewable outside of infrastructure configured in your application, and are never available to the OpenFGA team or contributors. + +## Metrics + +### Supported Metrics + +| Metric Name | Type | Enabled by Default | Description | +| -------------------------------- | --------- | ------------------ | --------------------------------------------------------------------------------- | +| `fga-client.request.duration` | Histogram | Yes | Total request time for FGA requests, in milliseconds | +| `fga-client.query.duration` | Histogram | Yes | Time taken by the FGA server to process and evaluate the request, in milliseconds | +| `fga-client.credentials.request` | Counter | Yes | Total number of new token requests initiated using the Client Credentials flow | + +### Supported Attributes + +| Attribute Name | Type | Enabled by Default | Description | +| ------------------------------ | ------ | ------------------ | --------------------------------------------------------------------------------- | +| `fga-client.request.client_id` | string | Yes | Client ID associated with the request, if any | +| `fga-client.request.method` | string | Yes | FGA method/action that was performed (e.g., Check, ListObjects) in TitleCase | +| `fga-client.request.model_id` | string | Yes | Authorization model ID that was sent as part of the request, if any | +| `fga-client.request.store_id` | string | Yes | Store ID that was sent as part of the request | +| `fga-client.response.model_id` | string | Yes | Authorization model ID that the FGA server used | +| `fga-client.user` | string | No | User associated with the action of the request for check and list users | +| `http.client.request.duration` | int | No | Duration for the SDK to complete the request, in milliseconds | +| `http.host` | string | Yes | Host identifier of the origin the request was sent to | +| `http.request.method` | string | Yes | HTTP method for the request | +| `http.request.resend_count` | int | Yes | Number of retries attempted, if any | +| `http.response.status_code` | int | Yes | Status code of the response (e.g., `200` for success) | +| `http.server.request.duration` | int | No | Time taken by the FGA server to process and evaluate the request, in milliseconds | +| `url.scheme` | string | Yes | HTTP scheme of the request (`http`/`https`) | +| `url.full` | string | Yes | Full URL of the request | +| `user_agent.original` | string | Yes | User Agent used in the query | + +## Example + +You can find a basic example integration in the [examples/opentelemetry](../../examples/opentelemetry) directory, which demonstrates how to configure the OpenFGA SDK with OpenTelemetry. + +Please see [the OpenTelemetry documentation](https://opentelemetry.io/docs/languages/go/) for additional details on how to further configure their SDK for your applications. diff --git a/example/opentelemetry/go.mod b/example/opentelemetry/go.mod new file mode 100644 index 0000000..5cf93ad --- /dev/null +++ b/example/opentelemetry/go.mod @@ -0,0 +1,33 @@ +module openfga-opentelemetry-example + +go 1.21 + +replace github.com/openfga/go-sdk => ../.. + +require ( + github.com/joho/godotenv v1.5.1 + github.com/openfga/go-sdk v0.0.0-00010101000000-000000000000 + go.opentelemetry.io/otel v1.29.0 + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.29.0 + go.opentelemetry.io/otel/sdk v1.29.0 + go.opentelemetry.io/otel/sdk/metric v1.29.0 + google.golang.org/grpc v1.66.0 +) + +require ( + github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 // indirect + go.opentelemetry.io/otel/metric v1.29.0 // indirect + go.opentelemetry.io/otel/trace v1.29.0 // indirect + go.opentelemetry.io/proto/otlp v1.3.1 // indirect + golang.org/x/net v0.28.0 // indirect + golang.org/x/sync v0.8.0 // indirect + golang.org/x/sys v0.24.0 // indirect + golang.org/x/text v0.17.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240822170219-fc7c04adadcd // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240822170219-fc7c04adadcd // indirect + google.golang.org/protobuf v1.34.2 // indirect +) diff --git a/example/opentelemetry/go.sum b/example/opentelemetry/go.sum new file mode 100644 index 0000000..0c8ae80 --- /dev/null +++ b/example/opentelemetry/go.sum @@ -0,0 +1,55 @@ +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/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/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/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I= +github.com/jarcoal/httpmock v1.3.1 h1:iUx3whfZWVf3jT01hQTO/Eo5sAYtB2/rqaUuOtpInww= +github.com/jarcoal/httpmock v1.3.1/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +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.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw= +go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.29.0 h1:k6fQVDQexDE+3jG2SfCQjnHS7OamcP73YMoxEVq5B6k= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.29.0/go.mod h1:t4BrYLHU450Zo9fnydWlIuswB1bm7rM8havDpWOJeDo= +go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc= +go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8= +go.opentelemetry.io/otel/sdk v1.29.0 h1:vkqKjk7gwhS8VaWb0POZKmIEDimRCMsopNYnriHyryo= +go.opentelemetry.io/otel/sdk v1.29.0/go.mod h1:pM8Dx5WKnvxLCb+8lG1PRNIDxu9g9b9g59Qr7hfAAok= +go.opentelemetry.io/otel/sdk/metric v1.29.0 h1:K2CfmJohnRgvZ9UAj2/FhIf/okdWcNdBwe1m8xFXiSY= +go.opentelemetry.io/otel/sdk/metric v1.29.0/go.mod h1:6zZLdCl2fkauYoZIOn/soQIDSWFmNSRcICarHfuhNJQ= +go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4= +go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ= +go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= +go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= +golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= +golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= +golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +google.golang.org/genproto/googleapis/api v0.0.0-20240822170219-fc7c04adadcd h1:BBOTEWLuuEGQy9n1y9MhVJ9Qt0BDu21X8qZs71/uPZo= +google.golang.org/genproto/googleapis/api v0.0.0-20240822170219-fc7c04adadcd/go.mod h1:fO8wJzT2zbQbAjbIoos1285VfEIYKDDY+Dt+WpTkh6g= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240822170219-fc7c04adadcd h1:6TEm2ZxXoQmFWFlt1vNxvVOa1Q0dXFQD1m/rYjXmS0E= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240822170219-fc7c04adadcd/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= +google.golang.org/grpc v1.66.0 h1:DibZuoBznOxbDQxRINckZcUvnCEvrW9pcWIE2yF9r1c= +google.golang.org/grpc v1.66.0/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +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/example/opentelemetry/main.go b/example/opentelemetry/main.go new file mode 100644 index 0000000..0a4f44c --- /dev/null +++ b/example/opentelemetry/main.go @@ -0,0 +1,131 @@ +package main + +import ( + "context" + "fmt" + "log" + "os" + + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + + "github.com/joho/godotenv" + "github.com/openfga/go-sdk/client" + "github.com/openfga/go-sdk/credentials" + "github.com/openfga/go-sdk/telemetry" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc" + sdkmetric "go.opentelemetry.io/otel/sdk/metric" + "go.opentelemetry.io/otel/sdk/resource" + semconv "go.opentelemetry.io/otel/semconv/v1.4.0" +) + +var serviceName = semconv.ServiceNameKey.String("openfga-opentelemetry-example") + +func configureOpenTelemetry(ctx context.Context, res *resource.Resource, conn *grpc.ClientConn) (func(context.Context) error, error) { + metricExporter, _ := otlpmetricgrpc.New(ctx, otlpmetricgrpc.WithGRPCConn(conn)) + + meterProvider := sdkmetric.NewMeterProvider( + sdkmetric.WithReader(sdkmetric.NewPeriodicReader(metricExporter)), + sdkmetric.WithResource(res), + ) + otel.SetMeterProvider(meterProvider) + + return meterProvider.Shutdown, nil +} + +func configureOpenFga() (*client.OpenFgaClient, error) { + creds := credentials.Credentials{ + Method: credentials.CredentialsMethodClientCredentials, + Config: &credentials.Config{ + ClientCredentialsClientId: os.Getenv("FGA_CLIENT_ID"), + ClientCredentialsClientSecret: os.Getenv("FGA_CLIENT_SECRET"), + ClientCredentialsApiAudience: os.Getenv("FGA_API_AUDIENCE"), + ClientCredentialsApiTokenIssuer: os.Getenv("FGA_API_TOKEN_ISSUER"), + }, + } + + otel := telemetry.Configuration{ + Metrics: &telemetry.MetricsConfiguration{ + METRIC_COUNTER_CREDENTIALS_REQUEST: &telemetry.MetricConfiguration{ + ATTR_FGA_CLIENT_REQUEST_CLIENT_ID: &telemetry.AttributeConfiguration{Enabled: true}, + ATTR_HTTP_REQUEST_METHOD: &telemetry.AttributeConfiguration{Enabled: true}, + ATTR_FGA_CLIENT_REQUEST_MODEL_ID: &telemetry.AttributeConfiguration{Enabled: true}, + ATTR_FGA_CLIENT_REQUEST_STORE_ID: &telemetry.AttributeConfiguration{Enabled: true}, + ATTR_FGA_CLIENT_RESPONSE_MODEL_ID: &telemetry.AttributeConfiguration{Enabled: true}, + ATTR_HTTP_HOST: &telemetry.AttributeConfiguration{Enabled: true}, + ATTR_HTTP_REQUEST_RESEND_COUNT: &telemetry.AttributeConfiguration{Enabled: true}, + ATTR_HTTP_RESPONSE_STATUS_CODE: &telemetry.AttributeConfiguration{Enabled: true}, + ATTR_URL_FULL: &telemetry.AttributeConfiguration{Enabled: true}, + ATTR_URL_SCHEME: &telemetry.AttributeConfiguration{Enabled: true}, + ATTR_USER_AGENT_ORIGINAL: &telemetry.AttributeConfiguration{Enabled: true}, + }, + METRIC_HISTOGRAM_REQUEST_DURATION: &telemetry.MetricConfiguration{ + ATTR_FGA_CLIENT_REQUEST_CLIENT_ID: &telemetry.AttributeConfiguration{Enabled: true}, + ATTR_HTTP_REQUEST_METHOD: &telemetry.AttributeConfiguration{Enabled: true}, + ATTR_FGA_CLIENT_REQUEST_MODEL_ID: &telemetry.AttributeConfiguration{Enabled: true}, + ATTR_FGA_CLIENT_REQUEST_STORE_ID: &telemetry.AttributeConfiguration{Enabled: true}, + ATTR_FGA_CLIENT_RESPONSE_MODEL_ID: &telemetry.AttributeConfiguration{Enabled: true}, + ATTR_HTTP_HOST: &telemetry.AttributeConfiguration{Enabled: true}, + ATTR_HTTP_REQUEST_RESEND_COUNT: &telemetry.AttributeConfiguration{Enabled: true}, + ATTR_HTTP_RESPONSE_STATUS_CODE: &telemetry.AttributeConfiguration{Enabled: true}, + ATTR_URL_FULL: &telemetry.AttributeConfiguration{Enabled: true}, + ATTR_URL_SCHEME: &telemetry.AttributeConfiguration{Enabled: true}, + ATTR_USER_AGENT_ORIGINAL: &telemetry.AttributeConfiguration{Enabled: true}, + }, + METRIC_HISTOGRAM_QUERY_DURATION: &telemetry.MetricConfiguration{ + ATTR_FGA_CLIENT_REQUEST_CLIENT_ID: &telemetry.AttributeConfiguration{Enabled: true}, + ATTR_HTTP_REQUEST_METHOD: &telemetry.AttributeConfiguration{Enabled: true}, + ATTR_FGA_CLIENT_REQUEST_MODEL_ID: &telemetry.AttributeConfiguration{Enabled: true}, + ATTR_FGA_CLIENT_REQUEST_STORE_ID: &telemetry.AttributeConfiguration{Enabled: true}, + ATTR_FGA_CLIENT_RESPONSE_MODEL_ID: &telemetry.AttributeConfiguration{Enabled: true}, + ATTR_HTTP_HOST: &telemetry.AttributeConfiguration{Enabled: true}, + ATTR_HTTP_REQUEST_RESEND_COUNT: &telemetry.AttributeConfiguration{Enabled: true}, + ATTR_HTTP_RESPONSE_STATUS_CODE: &telemetry.AttributeConfiguration{Enabled: true}, + ATTR_URL_FULL: &telemetry.AttributeConfiguration{Enabled: true}, + ATTR_URL_SCHEME: &telemetry.AttributeConfiguration{Enabled: true}, + ATTR_USER_AGENT_ORIGINAL: &telemetry.AttributeConfiguration{Enabled: true}, + }, + }, + } + + fgaClient, err := client.NewSdkClient(&client.ClientConfiguration{ + ApiUrl: os.Getenv("FGA_API_URL"), + StoreId: os.Getenv("FGA_STORE_ID"), + AuthorizationModelId: os.Getenv("FGA_MODEL_ID"), + Credentials: &creds, + Telemetry: &otel, + }) + + return fgaClient, err +} + +func main() { + godotenv.Load() + + ctx := context.Background() + + conn, _ := grpc.NewClient("localhost:4317", + grpc.WithTransportCredentials(insecure.NewCredentials()), + ) + + res, _ := resource.New(ctx, + resource.WithAttributes( + serviceName, + ), + ) + + shutdownMeterProvider, _ := configureOpenTelemetry(ctx, res, conn) + + defer func() { + if err := shutdownMeterProvider(ctx); err != nil { + log.Fatalf("failed to shutdown MeterProvider: %s", err) + } + }() + + fgaClient, _ := configureOpenFga() + + fmt.Println("Read Authorization Models") + authModels, _ := fgaClient.ReadAuthorizationModels(ctx).Execute() + fmt.Printf("Authorization Models Count: %d\n", len(authModels.AuthorizationModels)) +}