Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 77 additions & 12 deletions common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
} from "./errors";
import { setNotEnumerableProperty } from "./utils";
import { TelemetryAttribute, TelemetryAttributes } from "./telemetry/attributes";
import { TelemetryConfiguration } from "./telemetry/configuration";
import { TelemetryHistograms } from "./telemetry/histograms";

/**
Expand All @@ -28,8 +29,8 @@ export const DUMMY_BASE_URL = `https://${SdkConstants.SampleBaseDomain}`;
* @interface RequestArgs
*/
export interface RequestArgs {
url: string;
options: any;
url: string;
options: any;
}


Expand Down Expand Up @@ -240,30 +241,91 @@ export async function attemptHttpRequest<B, R>(
minWaitInMs: number;
},
axiosInstance: AxiosInstance,
telemetryConfig?: {
telemetry?: TelemetryConfiguration;
userAgent?: string;
},
): Promise<WrappedAxiosResponse<R> | undefined> {
let iterationCount = 0;
do {
iterationCount++;

// Track HTTP request duration for this specific call
const httpRequestStart = performance.now();
let response: AxiosResponse<R> | undefined;
let httpRequestError: any;

try {
const response = await axiosInstance(request);
response = await axiosInstance(request);
} catch (err: any) {
httpRequestError = err;
}

// Calculate duration for this individual HTTP call
const httpRequestDuration = Math.round(performance.now() - httpRequestStart);

// Emit per-HTTP-request metric if telemetry is configured
if (telemetryConfig?.telemetry?.metrics?.histogramHttpRequestDuration) {
const httpAttrs: Record<string, string | number> = {};

// Build attributes from the request
if (request.url) {
try {
const parsedUrl = new URL(request.url);
httpAttrs[TelemetryAttribute.HttpHost] = parsedUrl.hostname;
httpAttrs[TelemetryAttribute.UrlScheme] = parsedUrl.protocol.replace(":", "");
httpAttrs[TelemetryAttribute.UrlFull] = request.url;
} catch {
// URL parsing failed, still include the raw URL
httpAttrs[TelemetryAttribute.UrlFull] = request.url;
}
}
if (request.method) {
httpAttrs[TelemetryAttribute.HttpRequestMethod] = request.method.toUpperCase();
}
if (telemetryConfig.userAgent) {
httpAttrs[TelemetryAttribute.UserAgentOriginal] = telemetryConfig.userAgent;
}

// Add response status code if available
const responseStatus = response?.status || httpRequestError?.response?.status;
if (responseStatus != null) {
httpAttrs[TelemetryAttribute.HttpResponseStatusCode] = responseStatus;
}

telemetryConfig.telemetry.recorder.histogram(
TelemetryHistograms.httpRequestDuration,
httpRequestDuration,
TelemetryAttributes.prepare(
httpAttrs,
telemetryConfig.telemetry.metrics.histogramHttpRequestDuration.attributes
)
);
}

// Handle successful response
if (response && !httpRequestError) {
return {
response: response,
retries: iterationCount - 1,
};
} catch (err: any) {
const { retryable, error } = checkIfRetryableError(err, iterationCount, config.maxRetry);
}

// Handle error
if (httpRequestError) {
const { retryable, error } = checkIfRetryableError(httpRequestError, iterationCount, config.maxRetry);

if (!retryable) {
throw error;
}

const status = (err as any)?.response?.status;
const status = httpRequestError?.response?.status;
let retryDelayMs: number | undefined;

if ((status &&
(status === 429 || (status >= 500 && status !== 501))) &&
err.response?.headers) {
retryDelayMs = parseRetryAfterHeader(err.response.headers);
(status === 429 || (status >= 500 && status !== 501))) &&
httpRequestError.response?.headers) {
retryDelayMs = parseRetryAfterHeader(httpRequestError.response.headers);
}
if (!retryDelayMs) {
retryDelayMs = calculateExponentialBackoffWithJitter(iterationCount, config.minWaitInMs);
Expand All @@ -286,16 +348,19 @@ export const createRequestFunction = function (axiosArgs: RequestArgs, axiosInst

const start = performance.now();

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

const url = configuration.getBasePath() + axiosArgs.url;

const axiosRequestArgs = {...axiosArgs.options, url: url};
const axiosRequestArgs = { ...axiosArgs.options, url: url };
const wrappedResponse = await attemptHttpRequest(axiosRequestArgs, {
maxRetry,
minWaitInMs,
}, axios);
}, axios, {
telemetry: configuration.telemetry,
userAgent: configuration.baseOptions?.headers?.["User-Agent"],
});
const response = wrappedResponse?.response;
const data = typeof response?.data === "undefined" ? {} : response?.data;
const result: CallResult<any> = { ...data };
Expand Down
2 changes: 1 addition & 1 deletion telemetry/attributes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ export class TelemetryAttributes {
if (url) {
const parsedUrl = new URL(url);
attributes[TelemetryAttribute.HttpHost] = parsedUrl.hostname;
attributes[TelemetryAttribute.UrlScheme] = parsedUrl.protocol;
attributes[TelemetryAttribute.UrlScheme] = parsedUrl.protocol.replace(":", "");
Comment thread
SoulPancake marked this conversation as resolved.
attributes[TelemetryAttribute.UrlFull] = url;
}

Expand Down
9 changes: 9 additions & 0 deletions telemetry/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,12 +114,14 @@ export class TelemetryConfiguration implements TelemetryConfig {
[TelemetryMetric.CounterCredentialsRequest]: {attributes: TelemetryConfiguration.defaultAttributes},
[TelemetryMetric.HistogramRequestDuration]: {attributes: TelemetryConfiguration.defaultAttributes},
[TelemetryMetric.HistogramQueryDuration]: {attributes: TelemetryConfiguration.defaultAttributes},
[TelemetryMetric.HistogramHttpRequestDuration]: {attributes: TelemetryConfiguration.defaultAttributes},
};
} else {
this.metrics = {
[TelemetryMetric.CounterCredentialsRequest]: metrics[TelemetryMetric.CounterCredentialsRequest] || undefined,
[TelemetryMetric.HistogramRequestDuration]: metrics[TelemetryMetric.HistogramRequestDuration] || undefined,
[TelemetryMetric.HistogramQueryDuration]: metrics[TelemetryMetric.HistogramQueryDuration] || undefined,
[TelemetryMetric.HistogramHttpRequestDuration]: metrics[TelemetryMetric.HistogramHttpRequestDuration] || undefined,
};
}
}
Expand Down Expand Up @@ -152,5 +154,12 @@ export class TelemetryConfiguration implements TelemetryConfig {
throw new FgaValidationError(`Configuration.telemetry.metrics.histogramQueryDuration attribute '${histogramQueryDurationConfigAttr}' is not a valid attribute`);
}
});

const histogramHttpRequestDurationConfigAttrs = this.metrics?.histogramHttpRequestDuration?.attributes || new Set<TelemetryAttribute>();
histogramHttpRequestDurationConfigAttrs.forEach(histogramHttpRequestDurationAttr => {
if (!validAttrs.has(histogramHttpRequestDurationAttr)) {
throw new FgaValidationError(`Configuration.telemetry.metrics.histogramHttpRequestDuration attribute '${histogramHttpRequestDurationAttr}' is not a valid attribute`);
}
});
}
}
6 changes: 6 additions & 0 deletions telemetry/histograms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,10 @@ export class TelemetryHistograms {
unit: "milliseconds",
description: "How long it took to perform a query request.",
};

static httpRequestDuration: TelemetryHistogram = {
name: "fga-client.http_request.duration",
unit: "milliseconds",
description: "The time (in milliseconds) for a single HTTP request to complete.",
};
}
1 change: 1 addition & 0 deletions telemetry/metrics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export enum TelemetryMetric {
CounterCredentialsRequest = "counterCredentialsRequest",
HistogramRequestDuration = "histogramRequestDuration",
HistogramQueryDuration = "histogramQueryDuration",
HistogramHttpRequestDuration = "histogramHttpRequestDuration",
}

export class MetricRecorder {
Expand Down
2 changes: 1 addition & 1 deletion tests/telemetry/attributes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ describe("TelemetryAttributes", () => {
expect(result["fga-client.request.method"]).toEqual("GET");
expect(result["http.request.method"]).toEqual("POST");
expect(result["http.host"]).toEqual("example.com");
expect(result["url.scheme"]).toEqual("https:");
expect(result["url.scheme"]).toEqual("https");
});

test("should create attributes from response correctly", () => {
Expand Down
6 changes: 6 additions & 0 deletions tests/telemetry/histograms.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,10 @@ describe("TelemetryHistograms", () => {
expect(TelemetryHistograms.queryDuration.unit).toBe("milliseconds");
expect(TelemetryHistograms.queryDuration.description).toBe("How long it took to perform a query request.");
});

test("should have correct histogram details for http request duration", () => {
expect(TelemetryHistograms.httpRequestDuration.name).toBe("fga-client.http_request.duration");
expect(TelemetryHistograms.httpRequestDuration.unit).toBe("milliseconds");
expect(TelemetryHistograms.httpRequestDuration.description).toBe("The time (in milliseconds) for a single HTTP request to complete.");
});
});
Loading