Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(dotnet): export opentelemetry metrics #398

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Open
5 changes: 5 additions & 0 deletions config/clients/dotnet/CHANGELOG.md.mustache
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Changelog

## v0.4.1

### [0.4.1](https://{{gitHost}}/{{gitUserId}}/{{gitRepoId}}/compare/v0.4.0...v0.4.1) (2024-07-30)
- feat: export OpenTelemetry metrics. Refer to the [https://{{gitHost}}/{{gitUserId}}/{{gitRepoId}}/blob/main/OpenTelemetry.md](documentation) for more.

## v0.4.0

### [0.4.0](https://{{gitHost}}/{{gitUserId}}/{{gitRepoId}}/compare/v0.3.2...v0.4.0) (2024-06-14)
Expand Down
30 changes: 25 additions & 5 deletions config/clients/dotnet/config.overrides.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"packageGuid": "b8d9e3e9-0156-4948-9de7-5e0d3f9c4d9e",
"testPackageGuid": "d119dfae-509a-4eba-a973-645b739356fc",
"packageName": "OpenFga.Sdk",
"packageVersion": "0.4.0",
"packageVersion": "0.4.1",
"licenseUrl": "https://github.com/openfga/dotnet-sdk/blob/main/LICENSE",
"fossaComplianceNoticeId": "f8ac2ec4-84fc-44f4-a617-5800cd3d180e",
"termsOfService": "",
Expand All @@ -33,11 +33,8 @@
"enablePostProcessFile": true,
"hashCodeBasePrimeNumber": 9661,
"hashCodeMultiplierPrimeNumber": 9923,
"supportsOpenTelemetry": true,
"files": {
".github/workflows/main.yaml.mustache": {
"destinationFilename": ".github/workflows/main.yaml",
"templateType": "SupportingFiles"
},
"Client_OAuth2Client.mustache": {
"destinationFilename": "src/OpenFga.Sdk/ApiClient/OAuth2Client.cs",
"templateType": "SupportingFiles"
Expand Down Expand Up @@ -214,6 +211,22 @@
"destinationFilename": "src/OpenFga.Sdk/Client/Model/StoreIdOptions.cs",
"templateType": "SupportingFiles"
},
"Telemetry/Attributes.cs.mustache": {
"destinationFilename": "src/OpenFga.Sdk/Telemetry/Attributes.cs",
"templateType": "SupportingFiles"
},
"Telemetry/Counters.cs.mustache": {
"destinationFilename": "src/OpenFga.Sdk/Telemetry/Counters.cs",
"templateType": "SupportingFiles"
},
"Telemetry/Histograms.cs.mustache": {
"destinationFilename": "src/OpenFga.Sdk/Telemetry/Histograms.cs",
"templateType": "SupportingFiles"
},
"Telemetry/Metrics.cs.mustache": {
"destinationFilename": "src/OpenFga.Sdk/Telemetry/Metrics.cs",
"templateType": "SupportingFiles"
},
"Configuration_Configuration.mustache": {
"destinationFilename": "src/OpenFga.Sdk/Configuration/Configuration.cs",
"templateType": "SupportingFiles"
Expand Down Expand Up @@ -282,10 +295,17 @@
"destinationFilename": ".fossa.yml",
"templateType": "SupportingFiles"
},
"OpenTelemetry.md.mustache": {
"destinationFilename": "OpenTelemetry.md",
"templateType": "SupportingFiles"
},
"example/Makefile": {},
"example/README.md": {},
"example/Example1/Example1.cs": {},
"example/Example1/Example1.csproj": {},
"example/OpenTelemetryExample/.env.example": {},
"example/OpenTelemetryExample/OpenTelemetryExample.cs": {},
"example/OpenTelemetryExample/OpenTelemetryExample.csproj": {},
"assets/FGAIcon.png": {},
".editorconfig": {}
}
Expand Down
111 changes: 55 additions & 56 deletions config/clients/dotnet/template/Client_ApiClient.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,22 @@
using {{packageName}}.Client.Model;
using {{packageName}}.Configuration;
using {{packageName}}.Exceptions;
using {{packageName}}.Telemetry;
using System.Diagnostics;

namespace {{packageName}}.ApiClient;

/// <summary>
/// API Client - used by all the API related methods to call the API. Handles token exchange and retries.
/// API Client - used by all the API related methods to call the API. Handles token exchange and retries.
/// </summary>
public class ApiClient : IDisposable {
private readonly BaseClient _baseClient;
private readonly OAuth2Client? _oauth2Client;
private readonly Configuration.Configuration _configuration;
private readonly OAuth2Client? _oauth2Client;
private readonly Metrics metrics = new();

/// <summary>
/// Initializes a new instance of the <see cref="ApiClient"/> class.
/// Initializes a new instance of the <see cref="ApiClient" /> class.
/// </summary>
/// <param name="configuration">Client Configuration</param>
/// <param name="userHttpClient">User Http Client - Allows Http Client reuse</param>
Expand All @@ -28,14 +31,15 @@ public class ApiClient : IDisposable {
return;
}

switch (_configuration.Credentials.Method)
{
switch (_configuration.Credentials.Method) {
case CredentialsMethod.ApiToken:
_configuration.DefaultHeaders["Authorization"] = $"Bearer {_configuration.Credentials.Config!.ApiToken}";
_configuration.DefaultHeaders["Authorization"] =
$"Bearer {_configuration.Credentials.Config!.ApiToken}";
_baseClient = new BaseClient(_configuration, userHttpClient);
break;
case CredentialsMethod.ClientCredentials:
_oauth2Client = new OAuth2Client(_configuration.Credentials, _baseClient, new RetryParams { MaxRetry = _configuration.MaxRetry, MinWaitInMs = _configuration.MinWaitInMs});
_oauth2Client = new OAuth2Client(_configuration.Credentials, _baseClient,
new RetryParams { MaxRetry = _configuration.MaxRetry, MinWaitInMs = _configuration.MinWaitInMs });
break;
case CredentialsMethod.None:
default:
Expand All @@ -44,119 +48,114 @@ public class ApiClient : IDisposable {
}

/// <summary>
/// Handles getting the access token, calling the API and potentially retrying
/// Based on: https://github.com/auth0/auth0.net/blob/595ae80ccad8aa7764b80d26d2ef12f8b35bbeff/src/Auth0.ManagementApi/HttpClientManagementConnection.cs#L67
/// Handles getting the access token, calling the API and potentially retrying
/// Based on:
/// https://github.com/auth0/auth0.net/blob/595ae80ccad8aa7764b80d26d2ef12f8b35bbeff/src/Auth0.ManagementApi/HttpClientManagementConnection.cs#L67
/// </summary>
/// <param name="requestBuilder"></param>
/// <param name="apiName"></param>
/// <param name="cancellationToken"></param>
/// <typeparam name="T">Response Type</typeparam>
/// <returns></returns>
/// <exception cref="FgaApiAuthenticationError"></exception>
public async Task<T> SendRequestAsync<T>(RequestBuilder requestBuilder, string apiName,
public async Task<TRes> SendRequestAsync<TReq, TRes>(RequestBuilder<TReq> requestBuilder, string apiName,
CancellationToken cancellationToken = default) {
IDictionary<string, string> additionalHeaders = new Dictionary<string, string>();

var sw = Stopwatch.StartNew();
if (_oauth2Client != null) {
try {
var token = await _oauth2Client.GetAccessTokenAsync();

if (!string.IsNullOrEmpty(token)) {
additionalHeaders["Authorization"] = $"Bearer {token}";
}
} catch (ApiException e) {
}
catch (ApiException e) {
throw new FgaApiAuthenticationError("Invalid Client Credentials", apiName, e);
}
}

return await Retry(async () => await _baseClient.SendRequestAsync<T>(requestBuilder, additionalHeaders, apiName, cancellationToken));
var response = await Retry(async () =>
await _baseClient.SendRequestAsync<TReq, TRes>(requestBuilder, additionalHeaders, apiName,
cancellationToken));

sw.Stop();
metrics.buildForResponse(apiName, response.rawResponse, requestBuilder, _configuration.Credentials, sw,
response.retryCount);

return response.responseContent;
}

/// <summary>
/// Handles getting the access token, calling the API and potentially retrying (use for requests that return no content)
/// Handles getting the access token, calling the API and potentially retrying (use for requests that return no
/// content)
/// </summary>
/// <param name="requestBuilder"></param>
/// <param name="apiName"></param>
/// <param name="cancellationToken"></param>
/// <exception cref="FgaApiAuthenticationError"></exception>
public async Task SendRequestAsync(RequestBuilder requestBuilder, string apiName,
public async Task SendRequestAsync<TReq>(RequestBuilder<TReq> requestBuilder, string apiName,
CancellationToken cancellationToken = default) {
IDictionary<string, string> additionalHeaders = new Dictionary<string, string>();

var sw = Stopwatch.StartNew();
if (_oauth2Client != null) {
try {
var token = await _oauth2Client.GetAccessTokenAsync();

if (!string.IsNullOrEmpty(token)) {
additionalHeaders["Authorization"] = $"Bearer {token}";
}
} catch (ApiException e) {
}
catch (ApiException e) {
throw new FgaApiAuthenticationError("Invalid Client Credentials", apiName, e);
}
}

await Retry(async () => await _baseClient.SendRequestAsync(requestBuilder, additionalHeaders, apiName, cancellationToken));
var response = await Retry(async () =>
await _baseClient.SendRequestAsync<TReq, object>(requestBuilder, additionalHeaders, apiName,
cancellationToken));

sw.Stop();
metrics.buildForResponse(apiName, response.rawResponse, requestBuilder, _configuration.Credentials, sw,
response.retryCount);
}

private async Task<TResult> Retry<TResult>(Func<Task<TResult>> retryable) {
var numRetries = 0;
private async Task<ResponseWrapper<TResult>> Retry<TResult>(Func<Task<ResponseWrapper<TResult>>> retryable) {
var requestCount = 0;
while (true) {
try {
numRetries++;
requestCount++;

return await retryable();
} catch (FgaApiRateLimitExceededError err) {
if (numRetries > _configuration.MaxRetry) {
throw;
}
var waitInMs = (int) ((err.ResetInMs == null || err.ResetInMs < _configuration.MinWaitInMs)
? _configuration.MinWaitInMs
: err.ResetInMs);
var response = await retryable();

await Task.Delay(waitInMs);
}
catch (FgaApiError err) {
if (!err.ShouldRetry || numRetries > _configuration.MaxRetry) {
throw;
}
var waitInMs = (int)(_configuration.MinWaitInMs);
response.retryCount = requestCount - 1; // OTEL spec specifies that the original request is not included in the count

await Task.Delay(waitInMs);
return response;
}
}
}

private async Task Retry(Func<Task> retryable) {
var numRetries = 0;
while (true) {
try {
numRetries++;

await retryable();

return;
} catch (FgaApiRateLimitExceededError err) {
if (numRetries > _configuration.MaxRetry) {
catch (FgaApiRateLimitExceededError err) {
if (requestCount > _configuration.MaxRetry) {
throw;
}
var waitInMs = (int) ((err.ResetInMs == null || err.ResetInMs < _configuration.MinWaitInMs)

var waitInMs = (int)(err.ResetInMs == null || err.ResetInMs < _configuration.MinWaitInMs
? _configuration.MinWaitInMs
: err.ResetInMs);

await Task.Delay(waitInMs);
}
catch (FgaApiError err) {
if (!err.ShouldRetry || numRetries > _configuration.MaxRetry) {
if (!err.ShouldRetry || requestCount > _configuration.MaxRetry) {
throw;
}
var waitInMs = (int)(_configuration.MinWaitInMs);

var waitInMs = _configuration.MinWaitInMs;

await Task.Delay(waitInMs);
}
}
}

public void Dispose() {
_baseClient.Dispose();
}
}
public void Dispose() => _baseClient.Dispose();
}
Loading
Loading