Skip to content

Commit

Permalink
Remove Resilience and just hand roll
Browse files Browse the repository at this point in the history
  • Loading branch information
DrEsteban committed Nov 22, 2024
1 parent 94f8744 commit 14a9ca1
Show file tree
Hide file tree
Showing 4 changed files with 32 additions and 50 deletions.
3 changes: 1 addition & 2 deletions src/Services/EnphaseMetricsService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@ namespace SolarGateway_PrometheusProxy.Services;
/// </remarks>
public class EnphaseMetricsService(
HttpClient httpClient,
ILogger<EnphaseMetricsService> logger,
ILoggerFactory loggerFactory) : MetricsServiceBase(httpClient, logger, loggerFactory)
ILogger<EnphaseMetricsService> logger) : MetricsServiceBase(httpClient, logger)
{
protected override string MetricCategory => "enphase";

Expand Down
70 changes: 28 additions & 42 deletions src/Services/MetricsServiceBase.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
using System.Net;
using System.Net.Http.Headers;
using System.Text.Json;
using Polly;
using Polly.Retry;
using Prometheus;
using SolarGateway_PrometheusProxy.Exceptions;
using SolarGateway_PrometheusProxy.Models;
Expand All @@ -12,55 +10,22 @@ namespace SolarGateway_PrometheusProxy.Services;
/// <summary>
/// Base class that provides common functionality for brands that use a REST API to collect metrics.
/// </summary>
public abstract class MetricsServiceBase : IMetricsService
public abstract class MetricsServiceBase(HttpClient client, ILogger logger) : IMetricsService
{
/// <summary>
/// The HTTP client used to call the brand's API.
/// </summary>
protected readonly HttpClient _client;
protected readonly HttpClient _client = client;

/// <summary>
/// The logger used to log messages.
/// </summary>
protected readonly ILogger _logger;

/// <summary>
/// The resilience pipeline used to call the brand's API.
/// </summary>
protected readonly ResiliencePipeline<HttpResponseMessage> _resiliencePipeline;
protected readonly ILogger _logger = logger;

private readonly SemaphoreSlim _authTokenRefreshLock = new(1, 1);
private bool _authTokenRefreshed;
private AuthenticationHeaderValue? _authenticationHeader;

public MetricsServiceBase(HttpClient client, ILogger logger, ILoggerFactory factory)
{
this._client = client;
this._logger = logger;
this._resiliencePipeline = new ResiliencePipelineBuilder<HttpResponseMessage>()
.ConfigureTelemetry(factory)
.AddRetry(
new RetryStrategyOptions<HttpResponseMessage>
{
ShouldHandle = new PredicateBuilder<HttpResponseMessage>()
.HandleResult(result => this.UsesAuthToken && result.StatusCode is HttpStatusCode.Unauthorized or HttpStatusCode.Forbidden),
MaxRetryAttempts = 1,
Delay = TimeSpan.Zero,
OnRetry = args => this.RefreshTokenAsync(args.Context.CancellationToken)
})
.AddRetry(
new RetryStrategyOptions<HttpResponseMessage>
{
ShouldHandle = new PredicateBuilder<HttpResponseMessage>()
.HandleResult(result => result.StatusCode is HttpStatusCode.TooManyRequests),
MaxRetryAttempts = 3,
BackoffType = DelayBackoffType.Exponential,
UseJitter = true
})
.AddTimeout(TimeSpan.FromSeconds(5))
.Build();
}

/// <summary>
/// Main method to collect metrics from the brand's device(s) and save them to the Prometheus <see cref="CollectorRegistry"/>.
/// </summary>
Expand Down Expand Up @@ -129,20 +94,31 @@ protected void SetRequestDurationMetric(CollectorRegistry registry, TimeSpan dur
/// </remarks>
protected async Task<JsonDocument?> CallMetricEndpointAsync(string path, CancellationToken cancellationToken)
{
HttpResponseMessage? response = null;
try
{
if (this.UsesAuthToken && this._authenticationHeader == null)
{
await this.RefreshTokenAsync(cancellationToken);
}

using var response = await _resiliencePipeline.ExecuteAsync(async token =>
async Task<HttpResponseMessage> SendRequestAsync()
{
using var request = new HttpRequestMessage(HttpMethod.Get, path);
request.Headers.Authorization = this._authenticationHeader;

return await this._client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, token);
}, cancellationToken);
return await this._client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
}

response = await SendRequestAsync();

if (this.UsesAuthToken && response.StatusCode is HttpStatusCode.Unauthorized or HttpStatusCode.Forbidden)
{
// Try refresh token and issue again
await this.RefreshTokenAsync(cancellationToken);
response.Dispose();
response = await SendRequestAsync();
}

if (!response.IsSuccessStatusCode)
{
Expand All @@ -157,12 +133,22 @@ protected void SetRequestDurationMetric(CollectorRegistry registry, TimeSpan dur
}
catch (Exception ex)
{
throw new MetricRequestFailedException(ex.Message, ex);
this._logger.LogError(ex, "Failed to call '{Path}'", path);
return null;
}
finally
{
response?.Dispose();
}
}

private async ValueTask RefreshTokenAsync(CancellationToken cancellationToken)
{
if (!this.UsesAuthToken || this._authTokenRefreshed)
{
return;
}

await _authTokenRefreshLock.WaitAsync(cancellationToken);
try
{
Expand Down
7 changes: 3 additions & 4 deletions src/Services/TeslaGatewayMetricsService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,7 @@ public partial class TeslaGatewayMetricsService(
HttpClient httpClient,
IOptions<TeslaConfiguration> configuration,
IOptions<TeslaLoginRequest> loginRequest,
ILogger<TeslaGatewayMetricsService> logger,
ILoggerFactory loggerFactory) : MetricsServiceBase(httpClient, logger, loggerFactory)
ILogger<TeslaGatewayMetricsService> logger) : MetricsServiceBase(httpClient, logger)
{
private readonly TeslaLoginRequest _loginRequest = loginRequest.Value;
private readonly TimeSpan _loginCacheLength = TimeSpan.FromMinutes(configuration.Value.LoginCacheMinutes);
Expand Down Expand Up @@ -68,12 +67,12 @@ public override async Task CollectMetricsAsync(CollectorRegistry collectorRegist
this.PullSiteInfo(collectorRegistry, cancellationToken),
this.PullStatus(collectorRegistry, cancellationToken),
this.PullOperation(collectorRegistry, cancellationToken));

base.SetRequestDurationMetric(collectorRegistry, sw.Elapsed);
if (!results.All(r => r))
{
throw new MetricRequestFailedException($"Failed to pull {results.Count(r => !r)}/{results.Length} endpoints on Tesla gateway");
}

base.SetRequestDurationMetric(collectorRegistry, sw.Elapsed);
}

private async Task<bool> PullMeterAggregates(CollectorRegistry registry, CancellationToken cancellationToken)
Expand Down
2 changes: 0 additions & 2 deletions src/SolarGateway_PrometheusProxy.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@

<ItemGroup>
<PackageReference Include="Azure.Monitor.OpenTelemetry.AspNetCore" Version="1.3.0-beta.2" />
<PackageReference Include="Microsoft.Extensions.Resilience" Version="9.0.0" />
<PackageReference Include="OpenTelemetry" Version="1.10.0" />
<PackageReference Include="OpenTelemetry.Api" Version="1.10.0" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.10.0" />
Expand All @@ -31,7 +30,6 @@
<PackageReference Include="OpenTelemetry.Resources.OperatingSystem" Version="0.1.0-alpha.4" />
<PackageReference Include="OpenTelemetry.Resources.Process" Version="0.1.0-beta.3" />
<PackageReference Include="OpenTelemetry.Resources.ProcessRuntime" Version="0.1.0-beta.2" />
<!--<PackageReference Include="Polly" Version="8.5.0" />-->
<PackageReference Include="prometheus-net" Version="8.2.1" />
<PackageReference Include="System.Text.Json" Version="9.0.0" />
</ItemGroup>
Expand Down

0 comments on commit 14a9ca1

Please sign in to comment.