Skip to content

Commit 1a6bffb

Browse files
authored
feat: add opentelemetry metrics reporting (#117)
2 parents 0cebd3c + daf0206 commit 1a6bffb

File tree

6 files changed

+190
-22
lines changed

6 files changed

+190
-22
lines changed

api.ts

Lines changed: 62 additions & 17 deletions
Large diffs are not rendered by default.

common.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212

1313

1414
import { AxiosInstance, AxiosRequestConfig, AxiosResponse } from "axios";
15+
import { metrics } from "@opentelemetry/api";
16+
1517

1618
import { Configuration } from "./configuration";
1719
import type { Credentials } from "./credentials";
@@ -25,6 +27,17 @@ import {
2527
FgaError
2628
} from "./errors";
2729
import { setNotEnumerableProperty } from "./utils";
30+
import { buildAttributes } from "./telemetry";
31+
32+
const meter = metrics.getMeter("@openfga/sdk", "0.5.0");
33+
const durationHist = meter.createHistogram("fga-client.request.duration", {
34+
description: "The duration of requests",
35+
unit: "milliseconds",
36+
});
37+
const queryDurationHist = meter.createHistogram("fga-client.query.duration", {
38+
description: "The duration of queries on the FGA server",
39+
unit: "milliseconds",
40+
});
2841

2942
/**
3043
*
@@ -180,13 +193,15 @@ export async function attemptHttpRequest<B, R>(
180193
/**
181194
* creates an axios request function
182195
*/
183-
export const createRequestFunction = function (axiosArgs: RequestArgs, axiosInstance: AxiosInstance, configuration: Configuration, credentials: Credentials) {
196+
export const createRequestFunction = function (axiosArgs: RequestArgs, axiosInstance: AxiosInstance, configuration: Configuration, credentials: Credentials, methodAttributes: Record<string, unknown> = {}) {
184197
configuration.isValid();
185198

186199
const retryParams = axiosArgs.options?.retryParams ? axiosArgs.options?.retryParams : configuration.retryParams;
187200
const maxRetry:number = retryParams ? retryParams.maxRetry : 0;
188201
const minWaitInMs:number = retryParams ? retryParams.minWaitInMs : 0;
189202

203+
const start = Date.now();
204+
190205
return async (axios: AxiosInstance = axiosInstance) : PromiseResult<any> => {
191206
await setBearerAuthToObject(axiosArgs.options.headers, credentials!);
192207

@@ -195,9 +210,24 @@ export const createRequestFunction = function (axiosArgs: RequestArgs, axiosInst
195210
maxRetry,
196211
minWaitInMs,
197212
}, axios);
213+
const executionTime = Date.now() - start;
214+
198215
const data = typeof response?.data === "undefined" ? {} : response?.data;
199216
const result: CallResult<any> = { ...data };
200217
setNotEnumerableProperty(result, "$response", response);
218+
219+
const attributes = buildAttributes(response, configuration.credentials, methodAttributes);
220+
221+
if (response?.headers) {
222+
const duration = response.headers["fga-query-duration-ms"];
223+
if (duration !== undefined) {
224+
queryDurationHist.record(parseInt(duration, 10), attributes);
225+
}
226+
}
227+
228+
durationHist.record(executionTime, attributes);
229+
201230
return result;
202231
};
203232
};
233+

credentials/credentials.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,14 @@ import globalAxios, { AxiosInstance } from "axios";
1616
import { assertParamExists, isWellFormedUriString } from "../validation";
1717
import { FgaApiAuthenticationError, FgaApiError, FgaError, FgaValidationError } from "../errors";
1818
import { attemptHttpRequest } from "../common";
19+
import { buildAttributes } from "../telemetry";
1920
import { ApiTokenConfig, AuthCredentialsConfig, ClientCredentialsConfig, CredentialsMethod } from "./types";
21+
import { Counter, metrics } from "@opentelemetry/api";
2022

2123
export class Credentials {
2224
private accessToken?: string;
2325
private accessTokenExpiryDate?: Date;
26+
private tokenCounter?: Counter;
2427

2528
public static init(configuration: { credentials: AuthCredentialsConfig }): Credentials {
2629
return new Credentials(configuration.credentials);
@@ -48,7 +51,11 @@ export class Credentials {
4851
}
4952
}
5053
break;
51-
case CredentialsMethod.ClientCredentials:
54+
case CredentialsMethod.ClientCredentials: {
55+
const meter = metrics.getMeter("@openfga/sdk", "0.5.0");
56+
this.tokenCounter = meter.createCounter("fga-client.credentials.request");
57+
break;
58+
}
5259
case CredentialsMethod.None:
5360
default:
5461
break;
@@ -115,7 +122,6 @@ export class Credentials {
115122
if (this.accessToken && (!this.accessTokenExpiryDate || this.accessTokenExpiryDate > new Date())) {
116123
return this.accessToken;
117124
}
118-
119125
return this.refreshAccessToken();
120126
}
121127
}
@@ -126,7 +132,6 @@ export class Credentials {
126132
*/
127133
private async refreshAccessToken() {
128134
const clientCredentials = (this.authConfig as { method: CredentialsMethod.ClientCredentials; config: ClientCredentialsConfig })?.config;
129-
130135
try {
131136
const response = await attemptHttpRequest<{
132137
client_id: string,
@@ -157,7 +162,7 @@ export class Credentials {
157162
this.accessToken = response.data.access_token;
158163
this.accessTokenExpiryDate = new Date(Date.now() + response.data.expires_in * 1000);
159164
}
160-
165+
this.tokenCounter?.add(1, buildAttributes(response, this.authConfig));
161166
return this.accessToken;
162167
} catch (err: unknown) {
163168
if (err instanceof FgaApiError) {

package-lock.json

Lines changed: 28 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
"lint:fix": "eslint . --ext .ts --fix"
2323
},
2424
"dependencies": {
25+
"@opentelemetry/api": "^1.9.0",
26+
"@opentelemetry/semantic-conventions": "^1.25.0",
2527
"axios": "^1.6.8",
2628
"tiny-async-pool": "^2.1.0"
2729
},

telemetry.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { AxiosResponse } from "axios";
2+
import { Attributes } from "@opentelemetry/api";
3+
import { SEMATTRS_HTTP_HOST, SEMATTRS_HTTP_METHOD, SEMATTRS_HTTP_STATUS_CODE } from "@opentelemetry/semantic-conventions";
4+
import { AuthCredentialsConfig, CredentialsMethod } from "./credentials/types";
5+
6+
/**
7+
* Builds an object of attributes that can be used to report alongside an OpenTelemetry metric event.
8+
*
9+
* @param response - The Axios response object, used to add data like HTTP status, host, method, and headers.
10+
* @param credentials - The credentials object, used to add data like the ClientID when using ClientCredentials.
11+
* @param methodAttributes - Extra attributes that the method (i.e. check, listObjects) wishes to have included. Any custom attributes should use the common names.
12+
* @returns {Attributes}
13+
*/
14+
15+
export const buildAttributes = function buildAttributes(response: AxiosResponse<unknown, any> | undefined, credentials: AuthCredentialsConfig, methodAttributes: Record<string, any> = {}): Attributes {
16+
const attributes: Attributes = {
17+
...methodAttributes,
18+
};
19+
20+
if (response?.status) {
21+
attributes[SEMATTRS_HTTP_STATUS_CODE] = response.status;
22+
}
23+
24+
if (response?.request) {
25+
attributes[SEMATTRS_HTTP_METHOD] = response.request.method;
26+
attributes[SEMATTRS_HTTP_HOST] = response.request.host;
27+
}
28+
29+
if (response?.headers) {
30+
const modelId = response.headers["openfga-authorization-model-id"];
31+
if (modelId !== undefined) {
32+
attributes[attributeNames.responseModelId] = modelId;
33+
}
34+
}
35+
36+
if (credentials?.method === CredentialsMethod.ClientCredentials) {
37+
attributes[attributeNames.requestClientId] = credentials.config.clientId;
38+
}
39+
40+
return attributes;
41+
};
42+
/**
43+
* Common attribute names
44+
*/
45+
46+
export const attributeNames = {
47+
// Attributes associated with the request made
48+
requestModelId: "fga-client.request.model_id",
49+
requestMethod: "fga-client.request.method",
50+
requestStoreId: "fga-client.request.store_id",
51+
requestClientId: "fga-client.request.client_id",
52+
53+
// Attributes associated with the response
54+
responseModelId: "fga-client.response.model_id",
55+
56+
// Attributes associated with specific actions
57+
user: "fga-client.user"
58+
};

0 commit comments

Comments
 (0)