Skip to content

Commit

Permalink
feat: add opentelemetry metrics reporting (#117)
Browse files Browse the repository at this point in the history
  • Loading branch information
ewanharris authored Jun 28, 2024
2 parents 0cebd3c + daf0206 commit 1a6bffb
Show file tree
Hide file tree
Showing 6 changed files with 190 additions and 22 deletions.
79 changes: 62 additions & 17 deletions api.ts

Large diffs are not rendered by default.

32 changes: 31 additions & 1 deletion common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@


import { AxiosInstance, AxiosRequestConfig, AxiosResponse } from "axios";
import { metrics } from "@opentelemetry/api";


import { Configuration } from "./configuration";
import type { Credentials } from "./credentials";
Expand All @@ -25,6 +27,17 @@ import {
FgaError
} from "./errors";
import { setNotEnumerableProperty } from "./utils";
import { buildAttributes } from "./telemetry";

const meter = metrics.getMeter("@openfga/sdk", "0.5.0");
const durationHist = meter.createHistogram("fga-client.request.duration", {
description: "The duration of requests",
unit: "milliseconds",
});
const queryDurationHist = meter.createHistogram("fga-client.query.duration", {
description: "The duration of queries on the FGA server",
unit: "milliseconds",
});

/**
*
Expand Down Expand Up @@ -180,13 +193,15 @@ export async function attemptHttpRequest<B, R>(
/**
* creates an axios request function
*/
export const createRequestFunction = function (axiosArgs: RequestArgs, axiosInstance: AxiosInstance, configuration: Configuration, credentials: Credentials) {
export const createRequestFunction = function (axiosArgs: RequestArgs, axiosInstance: AxiosInstance, configuration: Configuration, credentials: Credentials, methodAttributes: Record<string, unknown> = {}) {
configuration.isValid();

const retryParams = axiosArgs.options?.retryParams ? axiosArgs.options?.retryParams : configuration.retryParams;
const maxRetry:number = retryParams ? retryParams.maxRetry : 0;
const minWaitInMs:number = retryParams ? retryParams.minWaitInMs : 0;

const start = Date.now();

return async (axios: AxiosInstance = axiosInstance) : PromiseResult<any> => {
await setBearerAuthToObject(axiosArgs.options.headers, credentials!);

Expand All @@ -195,9 +210,24 @@ export const createRequestFunction = function (axiosArgs: RequestArgs, axiosInst
maxRetry,
minWaitInMs,
}, axios);
const executionTime = Date.now() - start;

const data = typeof response?.data === "undefined" ? {} : response?.data;
const result: CallResult<any> = { ...data };
setNotEnumerableProperty(result, "$response", response);

const attributes = buildAttributes(response, configuration.credentials, methodAttributes);

if (response?.headers) {
const duration = response.headers["fga-query-duration-ms"];
if (duration !== undefined) {
queryDurationHist.record(parseInt(duration, 10), attributes);
}
}

durationHist.record(executionTime, attributes);

return result;
};
};

13 changes: 9 additions & 4 deletions credentials/credentials.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,14 @@ import globalAxios, { AxiosInstance } from "axios";
import { assertParamExists, isWellFormedUriString } from "../validation";
import { FgaApiAuthenticationError, FgaApiError, FgaError, FgaValidationError } from "../errors";
import { attemptHttpRequest } from "../common";
import { buildAttributes } from "../telemetry";
import { ApiTokenConfig, AuthCredentialsConfig, ClientCredentialsConfig, CredentialsMethod } from "./types";
import { Counter, metrics } from "@opentelemetry/api";

export class Credentials {
private accessToken?: string;
private accessTokenExpiryDate?: Date;
private tokenCounter?: Counter;

public static init(configuration: { credentials: AuthCredentialsConfig }): Credentials {
return new Credentials(configuration.credentials);
Expand Down Expand Up @@ -48,7 +51,11 @@ export class Credentials {
}
}
break;
case CredentialsMethod.ClientCredentials:
case CredentialsMethod.ClientCredentials: {
const meter = metrics.getMeter("@openfga/sdk", "0.5.0");
this.tokenCounter = meter.createCounter("fga-client.credentials.request");
break;
}
case CredentialsMethod.None:
default:
break;
Expand Down Expand Up @@ -115,7 +122,6 @@ export class Credentials {
if (this.accessToken && (!this.accessTokenExpiryDate || this.accessTokenExpiryDate > new Date())) {
return this.accessToken;
}

return this.refreshAccessToken();
}
}
Expand All @@ -126,7 +132,6 @@ 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,
Expand Down Expand Up @@ -157,7 +162,7 @@ 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) {
Expand Down
28 changes: 28 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
"lint:fix": "eslint . --ext .ts --fix"
},
"dependencies": {
"@opentelemetry/api": "^1.9.0",
"@opentelemetry/semantic-conventions": "^1.25.0",
"axios": "^1.6.8",
"tiny-async-pool": "^2.1.0"
},
Expand Down
58 changes: 58 additions & 0 deletions telemetry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { AxiosResponse } from "axios";
import { Attributes } from "@opentelemetry/api";
import { SEMATTRS_HTTP_HOST, SEMATTRS_HTTP_METHOD, SEMATTRS_HTTP_STATUS_CODE } from "@opentelemetry/semantic-conventions";
import { AuthCredentialsConfig, CredentialsMethod } from "./credentials/types";

/**
* Builds an object of attributes that can be used to report alongside an OpenTelemetry metric event.
*
* @param response - The Axios response object, used to add data like HTTP status, host, method, and headers.
* @param credentials - The credentials object, used to add data like the ClientID when using ClientCredentials.
* @param methodAttributes - Extra attributes that the method (i.e. check, listObjects) wishes to have included. Any custom attributes should use the common names.
* @returns {Attributes}
*/

export const buildAttributes = function buildAttributes(response: AxiosResponse<unknown, any> | undefined, credentials: AuthCredentialsConfig, methodAttributes: Record<string, any> = {}): Attributes {
const attributes: Attributes = {
...methodAttributes,
};

if (response?.status) {
attributes[SEMATTRS_HTTP_STATUS_CODE] = response.status;
}

if (response?.request) {
attributes[SEMATTRS_HTTP_METHOD] = response.request.method;
attributes[SEMATTRS_HTTP_HOST] = response.request.host;
}

if (response?.headers) {
const modelId = response.headers["openfga-authorization-model-id"];
if (modelId !== undefined) {
attributes[attributeNames.responseModelId] = modelId;
}
}

if (credentials?.method === CredentialsMethod.ClientCredentials) {
attributes[attributeNames.requestClientId] = credentials.config.clientId;
}

return attributes;
};
/**
* Common attribute names
*/

export const attributeNames = {
// Attributes associated with the request made
requestModelId: "fga-client.request.model_id",
requestMethod: "fga-client.request.method",
requestStoreId: "fga-client.request.store_id",
requestClientId: "fga-client.request.client_id",

// Attributes associated with the response
responseModelId: "fga-client.response.model_id",

// Attributes associated with specific actions
user: "fga-client.user"
};

0 comments on commit 1a6bffb

Please sign in to comment.