diff --git a/.openapi-generator/FILES b/.openapi-generator/FILES index 7ddd706..9a1a0af 100644 --- a/.openapi-generator/FILES +++ b/.openapi-generator/FILES @@ -28,6 +28,7 @@ configuration.ts credentials/credentials.ts credentials/index.ts credentials/types.ts +docs/opentelemetry.md errors.ts example/Makefile example/README.md @@ -36,6 +37,7 @@ example/example1/package.json git_push.sh index.ts package.json +telemetry.ts tests/client.test.ts tests/helpers/default-config.ts tests/helpers/index.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 7877b0a..a9bc521 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## v0.6.0 + +### [0.6.0](https://github.com/openfga/js-sdk/compare/v0.5.0...v0.6.0) (2024-06-28) +- feat: add opentelemetry metrics reporting (#117) + ## v0.5.0 ### [0.5.0](https://github.com/openfga/js-sdk/compare/v0.4.0...v0.5.0) (2024-06-14) diff --git a/README.md b/README.md index e3a35eb..afe29b3 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,7 @@ This is an autogenerated JavaScript SDK for OpenFGA. It provides a wrapper aroun - [Retries](#retries) - [API Endpoints](#api-endpoints) - [Models](#models) + - [OpenTelemetry](#opentelemetry) - [Contributing](#contributing) - [Issues](#issues) - [Pull Requests](#pull-requests) @@ -711,6 +712,10 @@ const fgaClient = new OpenFgaClient({ [Models](https://github.com/openfga/js-sdk/blob/main/apiModel.ts) +### 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/js-sdk/blob/main/docs/opentelemetry.md) + ## Contributing ### Issues diff --git a/api.ts b/api.ts index d4acbf6..260e0bc 100644 --- a/api.ts +++ b/api.ts @@ -22,7 +22,8 @@ import { createRequestFunction, RequestArgs, CallResult, - PromiseResult} from "./common"; + PromiseResult +} from "./common"; import { attributeNames } from "./telemetry"; import { Configuration } from "./configuration"; import { Credentials } from "./credentials"; @@ -757,8 +758,8 @@ export const OpenFgaApiFp = function(configuration: Configuration, credentials: async check(storeId: string, body: CheckRequest, options?: any): Promise<(axios?: AxiosInstance) => PromiseResult> { const localVarAxiosArgs = await localVarAxiosParamCreator.check(storeId, body, options); return createRequestFunction(localVarAxiosArgs, globalAxios, configuration, credentials, { - [attributeNames.requestStoreId]: storeId, [attributeNames.requestMethod]: "check", + [attributeNames.requestStoreId]: storeId, [attributeNames.user]: body.tuple_key.user }); }, @@ -771,8 +772,8 @@ export const OpenFgaApiFp = function(configuration: Configuration, credentials: */ async createStore(body: CreateStoreRequest, options?: any): Promise<(axios?: AxiosInstance) => PromiseResult> { const localVarAxiosArgs = await localVarAxiosParamCreator.createStore(body, options); - return createRequestFunction(localVarAxiosArgs, globalAxios, configuration, credentials, { - [attributeNames.requestMethod]: "createStore" + return createRequestFunction(localVarAxiosArgs, globalAxios, configuration, credentials, { + [attributeNames.requestMethod]: "createStore", }); }, /** @@ -784,7 +785,10 @@ export const OpenFgaApiFp = function(configuration: Configuration, credentials: */ async deleteStore(storeId: string, options?: any): Promise<(axios?: AxiosInstance) => PromiseResult> { const localVarAxiosArgs = await localVarAxiosParamCreator.deleteStore(storeId, options); - return createRequestFunction(localVarAxiosArgs, globalAxios, configuration, credentials); + return createRequestFunction(localVarAxiosArgs, globalAxios, configuration, credentials, { + [attributeNames.requestMethod]: "deleteStore", + [attributeNames.requestStoreId]: storeId, + }); }, /** * The Expand API will return all users and usersets that have certain relationship with an object in a certain store. This is different from the `/stores/{store_id}/read` API in that both users and computed usersets are returned. Body parameters `tuple_key.object` and `tuple_key.relation` are all required. The response will return a tree whose leaves are the specific users and usersets. Union, intersection and difference operator are located in the intermediate nodes. ## Example To expand all users that have the `reader` relationship with object `document:2021-budget`, use the Expand API with the following request body ```json { \"tuple_key\": { \"object\": \"document:2021-budget\", \"relation\": \"reader\" }, \"authorization_model_id\": \"01G50QVV17PECNVAHX1GG4Y5NC\" } ``` OpenFGA\'s response will be a userset tree of the users and usersets that have read access to the document. ```json { \"tree\":{ \"root\":{ \"type\":\"document:2021-budget#reader\", \"union\":{ \"nodes\":[ { \"type\":\"document:2021-budget#reader\", \"leaf\":{ \"users\":{ \"users\":[ \"user:bob\" ] } } }, { \"type\":\"document:2021-budget#reader\", \"leaf\":{ \"computed\":{ \"userset\":\"document:2021-budget#writer\" } } } ] } } } } ``` The caller can then call expand API for the `writer` relationship for the `document:2021-budget`. @@ -797,8 +801,8 @@ export const OpenFgaApiFp = function(configuration: Configuration, credentials: async expand(storeId: string, body: ExpandRequest, options?: any): Promise<(axios?: AxiosInstance) => PromiseResult> { const localVarAxiosArgs = await localVarAxiosParamCreator.expand(storeId, body, options); return createRequestFunction(localVarAxiosArgs, globalAxios, configuration, credentials, { + [attributeNames.requestMethod]: "expand", [attributeNames.requestStoreId]: storeId, - [attributeNames.requestMethod]: "expand" }); }, /** @@ -811,8 +815,8 @@ export const OpenFgaApiFp = function(configuration: Configuration, credentials: async getStore(storeId: string, options?: any): Promise<(axios?: AxiosInstance) => PromiseResult> { const localVarAxiosArgs = await localVarAxiosParamCreator.getStore(storeId, options); return createRequestFunction(localVarAxiosArgs, globalAxios, configuration, credentials, { - [attributeNames.requestStoreId]: storeId, - [attributeNames.requestMethod]: "getStore" + [attributeNames.requestMethod]: "getStore", + [attributeNames.requestStoreId]: storeId, }); }, /** @@ -826,8 +830,8 @@ export const OpenFgaApiFp = function(configuration: Configuration, credentials: async listObjects(storeId: string, body: ListObjectsRequest, options?: any): Promise<(axios?: AxiosInstance) => PromiseResult> { const localVarAxiosArgs = await localVarAxiosParamCreator.listObjects(storeId, body, options); return createRequestFunction(localVarAxiosArgs, globalAxios, configuration, credentials, { - [attributeNames.requestStoreId]: storeId, [attributeNames.requestMethod]: "listObjects", + [attributeNames.requestStoreId]: storeId, [attributeNames.user]: body.user }); }, @@ -841,8 +845,8 @@ export const OpenFgaApiFp = function(configuration: Configuration, credentials: */ async listStores(pageSize?: number, continuationToken?: string, options?: any): Promise<(axios?: AxiosInstance) => PromiseResult> { const localVarAxiosArgs = await localVarAxiosParamCreator.listStores(pageSize, continuationToken, options); - return createRequestFunction(localVarAxiosArgs, globalAxios, configuration, credentials, { - [attributeNames.requestMethod]: "listStores" + return createRequestFunction(localVarAxiosArgs, globalAxios, configuration, credentials, { + [attributeNames.requestMethod]: "listStores", }); }, /** @@ -856,8 +860,8 @@ export const OpenFgaApiFp = function(configuration: Configuration, credentials: async listUsers(storeId: string, body: ListUsersRequest, options?: any): Promise<(axios?: AxiosInstance) => PromiseResult> { const localVarAxiosArgs = await localVarAxiosParamCreator.listUsers(storeId, body, options); return createRequestFunction(localVarAxiosArgs, globalAxios, configuration, credentials, { - [attributeNames.requestStoreId]: storeId, - [attributeNames.requestMethod]: "listUsers" + [attributeNames.requestMethod]: "listUsers", + [attributeNames.requestStoreId]: storeId, }); }, /** @@ -871,8 +875,8 @@ export const OpenFgaApiFp = function(configuration: Configuration, credentials: async read(storeId: string, body: ReadRequest, options?: any): Promise<(axios?: AxiosInstance) => PromiseResult> { const localVarAxiosArgs = await localVarAxiosParamCreator.read(storeId, body, options); return createRequestFunction(localVarAxiosArgs, globalAxios, configuration, credentials, { - [attributeNames.requestStoreId]: storeId, - [attributeNames.requestMethod]: "read" + [attributeNames.requestMethod]: "read", + [attributeNames.requestStoreId]: storeId, }); }, /** @@ -886,8 +890,8 @@ export const OpenFgaApiFp = function(configuration: Configuration, credentials: async readAssertions(storeId: string, authorizationModelId: string, options?: any): Promise<(axios?: AxiosInstance) => PromiseResult> { const localVarAxiosArgs = await localVarAxiosParamCreator.readAssertions(storeId, authorizationModelId, options); return createRequestFunction(localVarAxiosArgs, globalAxios, configuration, credentials, { - [attributeNames.requestStoreId]: storeId, - [attributeNames.requestMethod]: "readAssertions" + [attributeNames.requestMethod]: "readAssertions", + [attributeNames.requestStoreId]: storeId, }); }, /** @@ -901,8 +905,8 @@ export const OpenFgaApiFp = function(configuration: Configuration, credentials: async readAuthorizationModel(storeId: string, id: string, options?: any): Promise<(axios?: AxiosInstance) => PromiseResult> { const localVarAxiosArgs = await localVarAxiosParamCreator.readAuthorizationModel(storeId, id, options); return createRequestFunction(localVarAxiosArgs, globalAxios, configuration, credentials, { - [attributeNames.requestStoreId]: storeId, - [attributeNames.requestMethod]: "readAuthorizationModel" + [attributeNames.requestMethod]: "readAuthorizationModel", + [attributeNames.requestStoreId]: storeId, }); }, /** @@ -917,8 +921,8 @@ export const OpenFgaApiFp = function(configuration: Configuration, credentials: async readAuthorizationModels(storeId: string, pageSize?: number, continuationToken?: string, options?: any): Promise<(axios?: AxiosInstance) => PromiseResult> { const localVarAxiosArgs = await localVarAxiosParamCreator.readAuthorizationModels(storeId, pageSize, continuationToken, options); return createRequestFunction(localVarAxiosArgs, globalAxios, configuration, credentials, { - [attributeNames.requestStoreId]: storeId, - [attributeNames.requestMethod]: "readAuthorizationModels" + [attributeNames.requestMethod]: "readAuthorizationModels", + [attributeNames.requestStoreId]: storeId, }); }, /** @@ -934,8 +938,8 @@ export const OpenFgaApiFp = function(configuration: Configuration, credentials: async readChanges(storeId: string, type?: string, pageSize?: number, continuationToken?: string, options?: any): Promise<(axios?: AxiosInstance) => PromiseResult> { const localVarAxiosArgs = await localVarAxiosParamCreator.readChanges(storeId, type, pageSize, continuationToken, options); return createRequestFunction(localVarAxiosArgs, globalAxios, configuration, credentials, { - [attributeNames.requestStoreId]: storeId, - [attributeNames.requestMethod]: "readChanges" + [attributeNames.requestMethod]: "readChanges", + [attributeNames.requestStoreId]: storeId, }); }, /** @@ -949,8 +953,8 @@ export const OpenFgaApiFp = function(configuration: Configuration, credentials: async write(storeId: string, body: WriteRequest, options?: any): Promise<(axios?: AxiosInstance) => PromiseResult> { const localVarAxiosArgs = await localVarAxiosParamCreator.write(storeId, body, options); return createRequestFunction(localVarAxiosArgs, globalAxios, configuration, credentials, { - [attributeNames.requestStoreId]: storeId, - [attributeNames.requestMethod]: "write" + [attributeNames.requestMethod]: "write", + [attributeNames.requestStoreId]: storeId, }); }, /** @@ -965,8 +969,8 @@ export const OpenFgaApiFp = function(configuration: Configuration, credentials: async writeAssertions(storeId: string, authorizationModelId: string, body: WriteAssertionsRequest, options?: any): Promise<(axios?: AxiosInstance) => PromiseResult> { const localVarAxiosArgs = await localVarAxiosParamCreator.writeAssertions(storeId, authorizationModelId, body, options); return createRequestFunction(localVarAxiosArgs, globalAxios, configuration, credentials, { - [attributeNames.requestStoreId]: storeId, - [attributeNames.requestMethod]: "writeAssertions" + [attributeNames.requestMethod]: "writeAssertions", + [attributeNames.requestStoreId]: storeId, }); }, /** @@ -980,8 +984,8 @@ export const OpenFgaApiFp = function(configuration: Configuration, credentials: async writeAuthorizationModel(storeId: string, body: WriteAuthorizationModelRequest, options?: any): Promise<(axios?: AxiosInstance) => PromiseResult> { const localVarAxiosArgs = await localVarAxiosParamCreator.writeAuthorizationModel(storeId, body, options); return createRequestFunction(localVarAxiosArgs, globalAxios, configuration, credentials, { - [attributeNames.requestStoreId]: storeId, - [attributeNames.requestMethod]: "writeAuthorizationModel" + [attributeNames.requestMethod]: "writeAuthorizationModel", + [attributeNames.requestStoreId]: storeId, }); }, }; diff --git a/common.ts b/common.ts index f5b7c2a..8ca15c1 100644 --- a/common.ts +++ b/common.ts @@ -14,7 +14,6 @@ import { AxiosInstance, AxiosRequestConfig, AxiosResponse } from "axios"; import { metrics } from "@opentelemetry/api"; - import { Configuration } from "./configuration"; import type { Credentials } from "./credentials"; import { @@ -29,7 +28,7 @@ import { import { setNotEnumerableProperty } from "./utils"; import { buildAttributes } from "./telemetry"; -const meter = metrics.getMeter("@openfga/sdk", "0.5.0"); +const meter = metrics.getMeter("@openfga/sdk", "0.6.0"); const durationHist = meter.createHistogram("fga-client.request.duration", { description: "The duration of requests", unit: "milliseconds", @@ -230,4 +229,3 @@ export const createRequestFunction = function (axiosArgs: RequestArgs, axiosInst return result; }; }; - diff --git a/configuration.ts b/configuration.ts index b5e8c99..de71d85 100644 --- a/configuration.ts +++ b/configuration.ts @@ -21,7 +21,7 @@ const DEFAULT_MAX_RETRY = 15; // default minimum wait period in retry - but will backoff exponentially const DEFAULT_MIN_WAIT_MS = 100; -const DEFAULT_USER_AGENT = "openfga-sdk js/0.5.0"; +const DEFAULT_USER_AGENT = "openfga-sdk js/0.6.0"; export interface RetryParams { maxRetry?: number; @@ -73,7 +73,7 @@ export class Configuration { * @type {string} * @memberof Configuration */ - private static sdkVersion = "0.5.0"; + private static sdkVersion = "0.6.0"; /** * provide the full api URL (e.g. `https://api.fga.example`) diff --git a/credentials/credentials.ts b/credentials/credentials.ts index 8bd4ba6..ec89da3 100644 --- a/credentials/credentials.ts +++ b/credentials/credentials.ts @@ -52,7 +52,7 @@ export class Credentials { } break; case CredentialsMethod.ClientCredentials: { - const meter = metrics.getMeter("@openfga/sdk", "0.5.0"); + const meter = metrics.getMeter("@openfga/sdk", "0.6.0"); this.tokenCounter = meter.createCounter("fga-client.credentials.request"); break; } @@ -122,6 +122,7 @@ export class Credentials { if (this.accessToken && (!this.accessTokenExpiryDate || this.accessTokenExpiryDate > new Date())) { return this.accessToken; } + return this.refreshAccessToken(); } } @@ -132,6 +133,7 @@ export class Credentials { */ private async refreshAccessToken() { const clientCredentials = (this.authConfig as { method: CredentialsMethod.ClientCredentials; config: ClientCredentialsConfig })?.config; + try { const response = await attemptHttpRequest<{ client_id: string, @@ -162,7 +164,9 @@ export class Credentials { this.accessToken = response.data.access_token; this.accessTokenExpiryDate = new Date(Date.now() + response.data.expires_in * 1000); } + this.tokenCounter?.add(1, buildAttributes(response, this.authConfig)); + return this.accessToken; } catch (err: unknown) { if (err instanceof FgaApiError) { diff --git a/docs/opentelemetry.md b/docs/opentelemetry.md new file mode 100644 index 0000000..5b9dbd5 --- /dev/null +++ b/docs/opentelemetry.md @@ -0,0 +1,31 @@ +# 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 | Description | +|---------------------------------|-----------|---------------------------------------------------------------------------------| +| `fga-client.request.duration` | Histogram | The total request time for FGA requests | +| `fga-client.query.duration` | Histogram | The amount of time the FGA server took to process the request | +|` fga-client.credentials.request`| Counter | The total number of times a new token was requested when using ClientCredentials| + +### Supported attributes + +| Attribute Name | Type | Description | +|--------------------------------|----------|-------------------------------------------------------------------------------------| +| `fga-client.response.model_id` | `string` | The authorization model ID that the FGA server used | +| `fga-client.request.method` | `string` | The FGA method/action that was performed | +| `fga-client.request.store_id` | `string` | The store ID that was sent as part of the request | +| `fga-client.request.model_id` | `string` | The authorization model ID that was sent as part of the request, if any | +| `fga-client.request.client_id` | `string` | The client ID associated with the request, if any | +| `fga-client.user` | `string` | The user that is associated with the action of the request for check and list users | +| `http.status_code ` | `int` | The status code of the response | +| `http.method` | `string` | The HTTP method for the request | +| `http.host` | `string` | Host identifier of the origin the request was sent to | \ No newline at end of file diff --git a/example/README.md b/example/README.md index ab6f011..504c0cc 100644 --- a/example/README.md +++ b/example/README.md @@ -28,7 +28,7 @@ Steps 2. In the Example `package.json` change the `@openfga/sdk` dependency from a semver range like below ```json "dependencies": { - "@openfga/sdk": "^0.5.0" + "@openfga/sdk": "^0.6.0" } ``` to a `file:` reference like below diff --git a/example/example1/package.json b/example/example1/package.json index 7debf8f..96e3f20 100644 --- a/example/example1/package.json +++ b/example/example1/package.json @@ -9,7 +9,7 @@ "start": "node example1.mjs" }, "dependencies": { - "@openfga/sdk": "^0.5.0" + "@openfga/sdk": "^0.6.0" }, "engines": { "node": ">=16.13.0" diff --git a/package-lock.json b/package-lock.json index ef4e451..1eccb37 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@openfga/sdk", - "version": "0.5.0", + "version": "0.6.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@openfga/sdk", - "version": "0.5.0", + "version": "0.6.0", "license": "Apache-2.0", "dependencies": { "@opentelemetry/api": "^1.9.0", diff --git a/package.json b/package.json index 4de67fc..69d502c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@openfga/sdk", - "version": "0.5.0", + "version": "0.6.0", "description": "JavaScript and Node.js SDK for OpenFGA", "author": "OpenFGA", "keywords": [ diff --git a/telemetry.ts b/telemetry.ts index 625e6a6..3c34e08 100644 --- a/telemetry.ts +++ b/telemetry.ts @@ -1,3 +1,16 @@ +/** + * JavaScript and Node.js SDK for OpenFGA + * + * API version: 1.x + * Website: https://openfga.dev + * Documentation: https://openfga.dev/docs + * Support: https://openfga.dev/community + * License: [Apache-2.0](https://github.com/openfga/js-sdk/blob/main/LICENSE) + * + * NOTE: This file was auto generated by OpenAPI Generator (https://openapi-generator.tech). DO NOT EDIT. + */ + + import { AxiosResponse } from "axios"; import { Attributes } from "@opentelemetry/api"; import { SEMATTRS_HTTP_HOST, SEMATTRS_HTTP_METHOD, SEMATTRS_HTTP_STATUS_CODE } from "@opentelemetry/semantic-conventions";