Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
79 changes: 72 additions & 7 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 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;
Comment thread
Abishek-Newar marked this conversation as resolved.
Outdated
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) {
Comment thread
Abishek-Newar marked this conversation as resolved.
Outdated
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);
httpRequestError.response?.headers) {
retryDelayMs = parseRetryAfterHeader(httpRequestError.response.headers);
}
if (!retryDelayMs) {
retryDelayMs = calculateExponentialBackoffWithJitter(iterationCount, config.minWaitInMs);
Expand Down Expand Up @@ -295,7 +357,10 @@ export const createRequestFunction = function (axiosArgs: RequestArgs, axiosInst
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
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."
Comment thread
Abishek-Newar marked this conversation as resolved.
Outdated
}
Comment thread
Abishek-Newar marked this conversation as resolved.
Outdated
}
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
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