Skip to content

Commit

Permalink
feat(dotnet): export opentelemetry metrics (#398)
Browse files Browse the repository at this point in the history
  • Loading branch information
ewanharris authored Nov 26, 2024
2 parents 5999ea3 + 5347626 commit 3661919
Show file tree
Hide file tree
Showing 29 changed files with 1,672 additions and 250 deletions.
5 changes: 5 additions & 0 deletions config/clients/dotnet/CHANGELOG.md.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

## [Unreleased](https://github.com/openfga/dotnet-sdk/compare/v{{packageVersion}}...HEAD)

## v0.5.1

### [0.5.1](https://{{gitHost}}/{{gitUserId}}/{{gitRepoId}}/compare/v0.5.0...v0.5.1) (2024-09-09)
- feat: export OpenTelemetry metrics. Refer to the [https://{{gitHost}}/{{gitUserId}}/{{gitRepoId}}/blob/main/OpenTelemetry.md](documentation) for more.

## v0.5.0

### [0.5.0](https://{{gitHost}}/{{gitUserId}}/{{gitRepoId}}/compare/v0.4.0...v0.5.0) (2024-08-28)
Expand Down
34 changes: 33 additions & 1 deletion 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.5.0",
"packageVersion": "0.5.1",
"licenseUrl": "https://github.com/openfga/dotnet-sdk/blob/main/LICENSE",
"fossaComplianceNoticeId": "f8ac2ec4-84fc-44f4-a617-5800cd3d180e",
"termsOfService": "",
Expand All @@ -33,6 +33,7 @@
"enablePostProcessFile": true,
"hashCodeBasePrimeNumber": 9661,
"hashCodeMultiplierPrimeNumber": 9923,
"supportsOpenTelemetry": true,
"files": {
"Client_OAuth2Client.mustache": {
"destinationFilename": "src/OpenFga.Sdk/ApiClient/OAuth2Client.cs",
Expand Down Expand Up @@ -226,10 +227,34 @@
"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/Meters.cs.mustache": {
"destinationFilename": "src/OpenFga.Sdk/Telemetry/Meters.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"
},
"Configuration_TelemetryConfig.mustache": {
"destinationFilename": "src/OpenFga.Sdk/Configuration/TelemetryConfig.cs",
"templateType": "SupportingFiles"
},
"Exceptions_Parsers_ApiErrorParser.mustache": {
"destinationFilename": "src/OpenFga.Sdk/Exceptions/Parsers/ApiErrorParser.cs",
"templateType": "SupportingFiles"
Expand Down Expand Up @@ -294,10 +319,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
2 changes: 1 addition & 1 deletion config/clients/dotnet/template/Client/Client.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public class {{appShortName}}Client : IDisposable {
ClientConfiguration configuration,
HttpClient? httpClient = null
) {
configuration.IsValid();
configuration.EnsureValid();
_configuration = configuration;
api = new {{appShortName}}Api(_configuration, httpClient);
}
Expand Down
48 changes: 37 additions & 11 deletions config/clients/dotnet/template/Client/ClientConfiguration.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -7,50 +7,76 @@ using {{packageName}}.Exceptions;

namespace {{packageName}}.Client;

/// <summary>
/// Class for managing telemetry settings.
/// </summary>
public class Telemetry {
}

/// <summary>
/// Configuration class for the {{packageName}} client.
/// </summary>
public class ClientConfiguration : Configuration.Configuration {
/// <summary>
/// Initializes a new instance of the <see cref="ClientConfiguration"/> class with the specified configuration.
/// </summary>
/// <param name="config">The base configuration to copy settings from.</param>
public ClientConfiguration(Configuration.Configuration config) {
ApiScheme = config.ApiScheme;
ApiHost = config.ApiHost;
ApiUrl = config.ApiUrl;
UserAgent = config.UserAgent;
Credentials = config.Credentials;
DefaultHeaders = config.DefaultHeaders;
Telemetry = config.Telemetry;
RetryParams = new RetryParams {MaxRetry = config.MaxRetry, MinWaitInMs = config.MinWaitInMs};
}

/// <summary>
/// Initializes a new instance of the <see cref="ClientConfiguration"/> class.
/// </summary>
public ClientConfiguration() { }

/// <summary>
/// Gets or sets the Store ID.
/// Gets or sets the Store ID.
/// </summary>
/// <value>Store ID.</value>
/// <value>The Store ID.</value>
public string? StoreId { get; set; }

/// <summary>
/// Gets or sets the Authorization Model ID.
/// Gets or sets the Authorization Model ID.
/// </summary>
/// <value>Authorization Model ID.</value>
/// <value>The Authorization Model ID.</value>
public string? AuthorizationModelId { get; set; }

/// <summary>
/// Gets or sets the retry parameters.
/// </summary>
/// <value>The retry parameters.</value>
public RetryParams? RetryParams { get; set; } = new();

public new void IsValid() {
base.IsValid();
/// <summary>
/// Ensures the configuration is valid, otherwise throws an error.
/// </summary>
/// <exception cref="FgaValidationError">Thrown when the Store ID or Authorization Model ID is not in a valid ULID format.</exception>
public new void EnsureValid() {
base.EnsureValid();
if (StoreId != null && !IsWellFormedUlidString(StoreId)) {
throw new FgaValidationError("StoreId is not in a valid ulid format");
}

if (AuthorizationModelId != null && AuthorizationModelId != "" && !IsWellFormedUlidString(AuthorizationModelId)) {
throw new FgaValidationError("AuthorizationModelId is not in a valid ulid format");
if (!string.IsNullOrEmpty(AuthorizationModelId) &&
!IsWellFormedUlidString(AuthorizationModelId)) {
throw new FgaValidationError("AuthorizationModelId is not in a valid ulid format");
}
}

/// <summary>
/// Ensures that a string is in valid [ULID](https://github.com/ulid/spec) format
/// Ensures that a string is in valid [ULID](https://github.com/ulid/spec) format.
/// </summary>
/// <param name="ulid"></param>
/// <returns></returns>
/// <param name="ulid">The string to validate as a ULID.</param>
/// <returns>True if the string is a valid ULID, otherwise false.</returns>
public static bool IsWellFormedUlidString(string ulid) {
var regex = new Regex("^[0-7][0-9A-HJKMNP-TV-Z]{25}$");
return regex.IsMatch(ulid);
Expand Down
117 changes: 60 additions & 57 deletions config/clients/dotnet/template/Client_ApiClient.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -3,39 +3,46 @@
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;
/// <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>
public ApiClient(Configuration.Configuration configuration, HttpClient? userHttpClient = null) {
configuration.IsValid();
configuration.EnsureValid();
_configuration = configuration;
_baseClient = new BaseClient(configuration, userHttpClient);
metrics = new Metrics(_configuration);
if (_configuration.Credentials == null) {
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 },
metrics);
break;
case CredentialsMethod.None:
default:
Expand All @@ -44,119 +51,115 @@ 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, 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, 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

0 comments on commit 3661919

Please sign in to comment.