Add new health

This commit is contained in:
2025-04-25 13:34:59 +07:00
parent 2e6afe3869
commit 5844d89175
6 changed files with 716 additions and 33 deletions

View File

@@ -0,0 +1,114 @@
using Managing.Application.Abstractions.Services;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using static Managing.Common.Enums;
namespace Managing.Api.HealthChecks
{
public class CandleDataHealthCheck : IHealthCheck
{
private readonly IExchangeService _exchangeService;
private readonly Ticker _tickerToCheck = Ticker.ETH;
private readonly TradingExchanges _exchangeToCheck = TradingExchanges.Evm;
public CandleDataHealthCheck(IExchangeService exchangeService)
{
_exchangeService = exchangeService;
}
public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
{
try
{
var now = DateTime.UtcNow;
// Define timeframes to check with their appropriate start dates and expected freshness
var timeframeChecks = new[]
{
(timeframe: Timeframe.FifteenMinutes, startDate: now.AddDays(-1), maxAgeMins: 30.0),
(timeframe: Timeframe.OneHour, startDate: now.AddDays(-3), maxAgeMins: 120.0),
(timeframe: Timeframe.OneDay, startDate: now.AddDays(-30), maxAgeMins: 1440.0)
};
var results = new List<Dictionary<string, object>>();
var isHealthy = true;
foreach (var (timeframe, startDate, maxAgeMins) in timeframeChecks)
{
// Using configured exchange and ticker
var candles = await _exchangeService.GetCandlesInflux(
_exchangeToCheck,
_tickerToCheck,
startDate,
timeframe);
var checkResult = new Dictionary<string, object>
{
["CheckedTicker"] = _tickerToCheck.ToString(),
["CheckedTimeframe"] = timeframe.ToString(),
["StartDate"] = startDate
};
if (candles == null || !candles.Any())
{
checkResult["Status"] = "Degraded";
checkResult["Message"] = $"No candle data found for {timeframe}";
isHealthy = false;
}
else
{
var latestCandle = candles.OrderByDescending(c => c.Date).FirstOrDefault();
var timeDiff = now - latestCandle.Date;
checkResult["LatestCandleDate"] = latestCandle.Date;
checkResult["TimeDifference"] = $"{timeDiff.TotalMinutes:F2} minutes";
if (timeDiff.TotalMinutes > maxAgeMins)
{
checkResult["Status"] = "Degraded";
checkResult["Message"] = $"Data for {timeframe} is outdated. Latest candle is from {latestCandle.Date:yyyy-MM-dd HH:mm:ss} UTC";
isHealthy = false;
}
else
{
checkResult["Status"] = "Healthy";
checkResult["Message"] = $"Data for {timeframe} is up-to-date. Latest candle is from {latestCandle.Date:yyyy-MM-dd HH:mm:ss} UTC";
}
}
results.Add(checkResult);
}
// Combine all results into a summary
var resultsDictionary = new Dictionary<string, object>();
for (int i = 0; i < results.Count; i++)
{
resultsDictionary[$"TimeframeCheck_{i+1}"] = results[i];
}
if (isHealthy)
{
return HealthCheckResult.Healthy(
"All candle timeframes are up-to-date",
data: resultsDictionary);
}
else
{
return HealthCheckResult.Degraded(
"One or more candle timeframes are outdated or missing",
data: resultsDictionary);
}
}
catch (Exception ex)
{
return HealthCheckResult.Unhealthy(
"Error checking candle data health",
ex,
data: new Dictionary<string, object>
{
["ErrorMessage"] = ex.Message,
["ErrorType"] = ex.GetType().Name
});
}
}
}
}

View File

@@ -0,0 +1,97 @@
using System.Text.Json;
using Microsoft.Extensions.Diagnostics.HealthChecks;
namespace Managing.Api.HealthChecks
{
public class GmxConnectivityHealthCheck : IHealthCheck
{
private readonly HttpClient _httpClient;
private readonly string _gmxCandlesEndpoint = "https://arbitrum-api.gmxinfra.io/prices/candles?tokenSymbol=ETH&period=1m&limit=2";
public GmxConnectivityHealthCheck(IHttpClientFactory httpClientFactory)
{
_httpClient = httpClientFactory.CreateClient("GmxHealthCheck");
}
public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
{
try
{
var response = await _httpClient.GetAsync(_gmxCandlesEndpoint, cancellationToken);
if (!response.IsSuccessStatusCode)
{
return HealthCheckResult.Degraded(
$"GMX API returned non-success status code: {response.StatusCode}",
data: new Dictionary<string, object>
{
["StatusCode"] = (int)response.StatusCode,
["Endpoint"] = _gmxCandlesEndpoint
});
}
var content = await response.Content.ReadAsStringAsync(cancellationToken);
// Parse the JSON to verify it's valid and has the expected structure
using (JsonDocument document = JsonDocument.Parse(content))
{
var root = document.RootElement;
// Check if the response has the expected structure
if (!root.TryGetProperty("candles", out var candlesElement) ||
candlesElement.ValueKind != JsonValueKind.Array ||
candlesElement.GetArrayLength() == 0)
{
return HealthCheckResult.Degraded(
"GMX API returned an invalid or empty response",
data: new Dictionary<string, object>
{
["Endpoint"] = _gmxCandlesEndpoint,
["Response"] = content
});
}
// Get the first candle timestamp to verify freshness
var firstCandle = candlesElement[0];
if (firstCandle.ValueKind != JsonValueKind.Array || firstCandle.GetArrayLength() < 1)
{
return HealthCheckResult.Degraded(
"GMX API returned invalid candle data format",
data: new Dictionary<string, object>
{
["Endpoint"] = _gmxCandlesEndpoint,
["Response"] = content
});
}
// Extract timestamp from the first element of the candle array
var timestamp = firstCandle[0].GetInt64();
var candleTime = DateTimeOffset.FromUnixTimeSeconds(timestamp).UtcDateTime;
var timeDiff = DateTime.UtcNow - candleTime;
return HealthCheckResult.Healthy(
"GMX API is responding with valid candle data",
data: new Dictionary<string, object>
{
["Endpoint"] = _gmxCandlesEndpoint,
["LatestCandleTimestamp"] = candleTime,
["TimeDifference"] = $"{timeDiff.TotalMinutes:F2} minutes",
["CandleCount"] = candlesElement.GetArrayLength()
});
}
}
catch (Exception ex)
{
return HealthCheckResult.Unhealthy(
"Failed to connect to GMX API",
ex,
data: new Dictionary<string, object>
{
["Endpoint"] = _gmxCandlesEndpoint,
["ErrorMessage"] = ex.Message,
["ErrorType"] = ex.GetType().Name
});
}
}
}
}

View File

@@ -0,0 +1,187 @@
using System.Text.Json;
using Microsoft.Extensions.Diagnostics.HealthChecks;
namespace Managing.Api.HealthChecks
{
public class Web3ProxyHealthCheck : IHealthCheck
{
private readonly HttpClient _httpClient;
private readonly string _web3ProxyUrl;
public Web3ProxyHealthCheck(IHttpClientFactory httpClientFactory, string web3ProxyUrl)
{
_httpClient = httpClientFactory.CreateClient("Web3ProxyHealthCheck");
_web3ProxyUrl = web3ProxyUrl;
}
public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
{
try
{
var response = await _httpClient.GetAsync($"{_web3ProxyUrl}/health", cancellationToken);
if (!response.IsSuccessStatusCode)
{
return HealthCheckResult.Degraded(
$"Web3Proxy health check failed with status code: {response.StatusCode}",
data: new Dictionary<string, object>
{
["StatusCode"] = (int)response.StatusCode,
["Endpoint"] = $"{_web3ProxyUrl}/health"
});
}
var content = await response.Content.ReadAsStringAsync(cancellationToken);
// Parse the JSON response to extract the detailed data
using (JsonDocument document = JsonDocument.Parse(content))
{
var root = document.RootElement;
string status = "healthy";
string message = "Web3Proxy is healthy";
if (root.TryGetProperty("status", out var statusElement))
{
status = statusElement.GetString();
}
// Extract the detailed data from the Web3Proxy response
var data = new Dictionary<string, object>();
// Parse timestamp if available
if (root.TryGetProperty("timestamp", out var timestampElement))
{
data["timestamp"] = timestampElement.GetString();
}
// Parse version if available
if (root.TryGetProperty("version", out var versionElement))
{
data["version"] = versionElement.GetString();
}
// Parse checks if available
if (root.TryGetProperty("checks", out var checksElement))
{
// Extract Privy check
if (checksElement.TryGetProperty("privy", out var privyElement))
{
var privyData = new Dictionary<string, object>();
if (privyElement.TryGetProperty("status", out var privyStatusElement))
{
privyData["status"] = privyStatusElement.GetString();
}
if (privyElement.TryGetProperty("message", out var privyMessageElement))
{
privyData["message"] = privyMessageElement.GetString();
}
data["privy"] = privyData;
}
// Extract GMX check
if (checksElement.TryGetProperty("gmx", out var gmxElement))
{
var gmxData = new Dictionary<string, object>();
if (gmxElement.TryGetProperty("status", out var gmxStatusElement))
{
gmxData["status"] = gmxStatusElement.GetString();
}
if (gmxElement.TryGetProperty("message", out var gmxMessageElement))
{
gmxData["message"] = gmxMessageElement.GetString();
}
// Extract GMX market data
if (gmxElement.TryGetProperty("data", out var gmxDataElement))
{
if (gmxDataElement.TryGetProperty("marketCount", out var marketCountElement))
{
gmxData["marketCount"] = marketCountElement.GetInt32();
}
if (gmxDataElement.TryGetProperty("responseTimeMs", out var responseTimeElement))
{
gmxData["responseTimeMs"] = responseTimeElement.GetInt32();
}
if (gmxDataElement.TryGetProperty("sampleMarkets", out var sampleMarketsElement))
{
var sampleMarkets = new List<Dictionary<string, string>>();
for (int i = 0; i < sampleMarketsElement.GetArrayLength(); i++)
{
var marketElement = sampleMarketsElement[i];
var market = new Dictionary<string, string>();
if (marketElement.TryGetProperty("marketAddress", out var addressElement))
{
market["marketAddress"] = addressElement.GetString();
}
if (marketElement.TryGetProperty("indexToken", out var indexTokenElement))
{
market["indexToken"] = indexTokenElement.GetString();
}
if (marketElement.TryGetProperty("longToken", out var longTokenElement))
{
market["longToken"] = longTokenElement.GetString();
}
if (marketElement.TryGetProperty("shortToken", out var shortTokenElement))
{
market["shortToken"] = shortTokenElement.GetString();
}
sampleMarkets.Add(market);
}
gmxData["sampleMarkets"] = sampleMarkets;
}
}
data["gmx"] = gmxData;
}
}
// Determine overall health result based on status
if (status.ToLower() == "healthy")
{
return HealthCheckResult.Healthy(
"Web3Proxy is healthy",
data: data);
}
else if (status.ToLower() == "degraded")
{
return HealthCheckResult.Degraded(
"Web3Proxy is degraded",
data: data);
}
else
{
return HealthCheckResult.Unhealthy(
"Web3Proxy is unhealthy",
data: data);
}
}
}
catch (Exception ex)
{
return HealthCheckResult.Unhealthy(
"Failed to connect to Web3Proxy",
ex,
data: new Dictionary<string, object>
{
["Endpoint"] = $"{_web3ProxyUrl}/health",
["ErrorMessage"] = ex.Message,
["ErrorType"] = ex.GetType().Name
});
}
}
}
}