Add new health
This commit is contained in:
114
src/Managing.Api/HealthChecks/CandleDataHealthCheck.cs
Normal file
114
src/Managing.Api/HealthChecks/CandleDataHealthCheck.cs
Normal 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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
97
src/Managing.Api/HealthChecks/GmxConnectivityHealthCheck.cs
Normal file
97
src/Managing.Api/HealthChecks/GmxConnectivityHealthCheck.cs
Normal 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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
187
src/Managing.Api/HealthChecks/Web3ProxyHealthCheck.cs
Normal file
187
src/Managing.Api/HealthChecks/Web3ProxyHealthCheck.cs
Normal 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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,12 +6,12 @@ import {GmxSdk} from '../../generated/gmxsdk/index.js'
|
|||||||
import {arbitrum} from 'viem/chains';
|
import {arbitrum} from 'viem/chains';
|
||||||
import {getTokenBySymbol} from '../../generated/gmxsdk/configs/tokens.js';
|
import {getTokenBySymbol} from '../../generated/gmxsdk/configs/tokens.js';
|
||||||
import {
|
import {
|
||||||
|
Position,
|
||||||
|
PositionStatus,
|
||||||
Trade,
|
Trade,
|
||||||
TradeDirection,
|
TradeDirection,
|
||||||
TradeStatus,
|
TradeStatus,
|
||||||
TradeType,
|
TradeType
|
||||||
Position,
|
|
||||||
PositionStatus
|
|
||||||
} from '../../generated/ManagingApiTypes.js';
|
} from '../../generated/ManagingApiTypes.js';
|
||||||
import {MarketInfo, MarketsInfoData} from '../../generated/gmxsdk/types/markets.js';
|
import {MarketInfo, MarketsInfoData} from '../../generated/gmxsdk/types/markets.js';
|
||||||
import {MarketConfig, MARKETS} from '../../generated/gmxsdk/configs/markets.js'
|
import {MarketConfig, MARKETS} from '../../generated/gmxsdk/configs/markets.js'
|
||||||
@@ -20,14 +20,10 @@ import {TokenData, TokensData} from '../../generated/gmxsdk/types/tokens.js';
|
|||||||
import {getByKey} from '../../generated/gmxsdk/utils/objects.js';
|
import {getByKey} from '../../generated/gmxsdk/utils/objects.js';
|
||||||
import {GmxSdkConfig} from '../../generated/gmxsdk/types/sdk.js';
|
import {GmxSdkConfig} from '../../generated/gmxsdk/types/sdk.js';
|
||||||
import {PositionIncreaseParams} from '../../generated/gmxsdk/modules/orders/helpers.js';
|
import {PositionIncreaseParams} from '../../generated/gmxsdk/modules/orders/helpers.js';
|
||||||
import {numberToBigint, PRECISION_DECIMALS} from '../../generated/gmxsdk/utils/numbers.js';
|
import {bigintToNumber, numberToBigint, PRECISION_DECIMALS} from '../../generated/gmxsdk/utils/numbers.js';
|
||||||
import {DecreasePositionSwapType, PositionOrderInfo} from '../../generated/gmxsdk/types/orders.js';
|
import {DecreasePositionSwapType, OrderType, PositionOrderInfo} from '../../generated/gmxsdk/types/orders.js';
|
||||||
import {OrderType} from '../../generated/gmxsdk/types/orders.js';
|
|
||||||
import {DecreasePositionAmounts} from '../../generated/gmxsdk/types/trade.js';
|
import {DecreasePositionAmounts} from '../../generated/gmxsdk/types/trade.js';
|
||||||
import {encodeReferralCode} from '../../generated/gmxsdk/utils/referrals.js';
|
import {encodeReferralCode} from '../../generated/gmxsdk/utils/referrals.js';
|
||||||
import {convertToUsd} from '../../generated/gmxsdk/utils/tokens.js';
|
|
||||||
import {toNumber} from 'ethers';
|
|
||||||
import {bigintToNumber} from '../../generated/gmxsdk/utils/numbers.js';
|
|
||||||
import {formatUsd} from '../../generated/gmxsdk/utils/numbers/formatting.js';
|
import {formatUsd} from '../../generated/gmxsdk/utils/numbers/formatting.js';
|
||||||
import {calculateDisplayDecimals} from '../../generated/gmxsdk/utils/numbers/index.js';
|
import {calculateDisplayDecimals} from '../../generated/gmxsdk/utils/numbers/index.js';
|
||||||
|
|
||||||
@@ -85,13 +81,12 @@ const cancelOrdersSchema = z.object({
|
|||||||
/**
|
/**
|
||||||
* Gets a GMX SDK client initialized for the given address
|
* Gets a GMX SDK client initialized for the given address
|
||||||
* If a walletId is provided, it will be used with Privy for signing
|
* If a walletId is provided, it will be used with Privy for signing
|
||||||
* @param this The FastifyRequest instance
|
|
||||||
* @param account The wallet address to use
|
* @param account The wallet address to use
|
||||||
* @returns An initialized GMX SDK client
|
* @returns An initialized GMX SDK client
|
||||||
*/
|
*/
|
||||||
export function getClientForAddress(
|
export async function getClientForAddress(
|
||||||
account: string,
|
account: string,
|
||||||
): GmxSdk {
|
): Promise<GmxSdk> {
|
||||||
try {
|
try {
|
||||||
// Create SDK instance
|
// Create SDK instance
|
||||||
const arbitrumSdkConfig: GmxSdkConfig = {
|
const arbitrumSdkConfig: GmxSdkConfig = {
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import {FastifyPluginAsyncTypebox, Type} from '@fastify/type-provider-typebox'
|
import {FastifyPluginAsyncTypebox, Type} from '@fastify/type-provider-typebox'
|
||||||
import {handleError} from '../utils/errorHandler.js'
|
import {handleError} from '../utils/errorHandler.js'
|
||||||
|
import {getClientForAddress} from '../plugins/custom/gmx.js'
|
||||||
|
import {getPrivyClient} from '../plugins/custom/privy.js'
|
||||||
|
|
||||||
const plugin: FastifyPluginAsyncTypebox = async (fastify) => {
|
const plugin: FastifyPluginAsyncTypebox = async (fastify) => {
|
||||||
fastify.get(
|
fastify.get(
|
||||||
@@ -27,16 +29,27 @@ const plugin: FastifyPluginAsyncTypebox = async (fastify) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
// Add health check endpoint
|
// Enhanced health check endpoint
|
||||||
fastify.get('/health', {
|
fastify.get('/health', {
|
||||||
schema: {
|
schema: {
|
||||||
tags: ['Health'],
|
tags: ['Health'],
|
||||||
description: 'Health check endpoint that confirms the API is operational',
|
description: 'Enhanced health check endpoint that verifies API, Privy, and GMX connectivity',
|
||||||
response: {
|
response: {
|
||||||
200: Type.Object({
|
200: Type.Object({
|
||||||
status: Type.String(),
|
status: Type.String(),
|
||||||
timestamp: Type.String(),
|
timestamp: Type.String(),
|
||||||
version: Type.String()
|
version: Type.String(),
|
||||||
|
checks: Type.Object({
|
||||||
|
privy: Type.Object({
|
||||||
|
status: Type.String(),
|
||||||
|
message: Type.String()
|
||||||
|
}),
|
||||||
|
gmx: Type.Object({
|
||||||
|
status: Type.String(),
|
||||||
|
message: Type.String(),
|
||||||
|
data: Type.Optional(Type.Any())
|
||||||
|
})
|
||||||
|
})
|
||||||
}),
|
}),
|
||||||
500: Type.Object({
|
500: Type.Object({
|
||||||
success: Type.Boolean(),
|
success: Type.Boolean(),
|
||||||
@@ -46,15 +59,139 @@ const plugin: FastifyPluginAsyncTypebox = async (fastify) => {
|
|||||||
}
|
}
|
||||||
}, async function (request, reply) {
|
}, async function (request, reply) {
|
||||||
try {
|
try {
|
||||||
|
const checks = {
|
||||||
|
privy: await checkPrivy(fastify),
|
||||||
|
gmx: await checkGmx()
|
||||||
|
}
|
||||||
|
|
||||||
|
// If any check failed, set status to degraded
|
||||||
|
const overallStatus = Object.values(checks).some(check => check.status !== 'healthy')
|
||||||
|
? 'degraded'
|
||||||
|
: 'healthy';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status: 'ok',
|
status: overallStatus,
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
version: process.env.npm_package_version || '1.0.0'
|
version: process.env.npm_package_version || '1.0.0',
|
||||||
|
checks
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return handleError(request, reply, error, 'health');
|
return handleError(request, reply, error, 'health');
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Helper function to check Privy connectivity and configuration
|
||||||
|
async function checkPrivy(fastify) {
|
||||||
|
try {
|
||||||
|
// Try to initialize the Privy client - this will validate env vars and connectivity
|
||||||
|
const privy = getPrivyClient(fastify);
|
||||||
|
|
||||||
|
// Check if the necessary configuration is available
|
||||||
|
const hasPrivyAppId = !!process.env.PRIVY_APP_ID;
|
||||||
|
const hasPrivySecret = !!process.env.PRIVY_APP_SECRET;
|
||||||
|
const hasPrivyAuthKey = !!process.env.PRIVY_AUTHORIZATION_KEY;
|
||||||
|
|
||||||
|
// Get the client status
|
||||||
|
const allConfigPresent = hasPrivyAppId && hasPrivySecret && hasPrivyAuthKey;
|
||||||
|
|
||||||
|
if (!allConfigPresent) {
|
||||||
|
return {
|
||||||
|
status: 'degraded',
|
||||||
|
message: 'Privy configuration incomplete: ' +
|
||||||
|
(!hasPrivyAppId ? 'PRIVY_APP_ID ' : '') +
|
||||||
|
(!hasPrivySecret ? 'PRIVY_APP_SECRET ' : '') +
|
||||||
|
(!hasPrivyAuthKey ? 'PRIVY_AUTHORIZATION_KEY ' : '')
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we got this far, the Privy client was successfully initialized
|
||||||
|
return {
|
||||||
|
status: 'healthy',
|
||||||
|
message: 'Privy client successfully initialized'
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
status: 'unhealthy',
|
||||||
|
message: `Privy client initialization failed: ${error instanceof Error ? error.message : 'Unknown error'}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to check GMX connectivity using the GMX SDK
|
||||||
|
async function checkGmx() {
|
||||||
|
try {
|
||||||
|
// Use a dummy zero address for the health check
|
||||||
|
const account = "0x0000000000000000000000000000000000000000";
|
||||||
|
|
||||||
|
// Use the existing getClientForAddress function to get a proper GMX SDK instance
|
||||||
|
const sdk = await getClientForAddress(account);
|
||||||
|
|
||||||
|
// Get markets info data
|
||||||
|
const startTime = Date.now();
|
||||||
|
const marketsInfo = await sdk.markets.getMarketsInfo();
|
||||||
|
const responseTime = Date.now() - startTime;
|
||||||
|
|
||||||
|
if (!marketsInfo.marketsInfoData || Object.keys(marketsInfo.marketsInfoData).length === 0) {
|
||||||
|
return {
|
||||||
|
status: 'degraded',
|
||||||
|
message: 'GMX SDK returned empty markets info data',
|
||||||
|
data: { responseTimeMs: responseTime }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check market data for ETH-USD
|
||||||
|
let foundEthMarket = false;
|
||||||
|
let marketInfoDetails = [];
|
||||||
|
|
||||||
|
// Collect information about all available markets
|
||||||
|
for (const [marketAddress, marketInfo] of Object.entries(marketsInfo.marketsInfoData)) {
|
||||||
|
const marketDetails = {
|
||||||
|
marketAddress,
|
||||||
|
indexToken: marketInfo.indexToken?.symbol,
|
||||||
|
longToken: marketInfo.longToken?.symbol,
|
||||||
|
shortToken: marketInfo.shortToken?.symbol
|
||||||
|
};
|
||||||
|
|
||||||
|
marketInfoDetails.push(marketDetails);
|
||||||
|
|
||||||
|
// Check if this is the ETH market
|
||||||
|
if (marketInfo.indexToken?.symbol === 'ETH' ||
|
||||||
|
marketInfo.indexToken?.symbol === 'WETH' ||
|
||||||
|
marketInfo.indexToken?.name?.includes('Ethereum')) {
|
||||||
|
foundEthMarket = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!foundEthMarket) {
|
||||||
|
return {
|
||||||
|
status: 'degraded',
|
||||||
|
message: 'ETH market not found in GMX markets data',
|
||||||
|
data: {
|
||||||
|
availableMarkets: marketInfoDetails,
|
||||||
|
responseTimeMs: responseTime
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: 'healthy',
|
||||||
|
message: `GMX SDK successfully retrieved markets data (${Object.keys(marketsInfo.marketsInfoData).length} markets)`,
|
||||||
|
data: {
|
||||||
|
marketCount: Object.keys(marketsInfo.marketsInfoData).length,
|
||||||
|
responseTimeMs: responseTime,
|
||||||
|
sampleMarkets: marketInfoDetails.slice(0, 3) // Just include first 3 markets for brevity
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
status: 'unhealthy',
|
||||||
|
message: `GMX SDK check failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||||
|
data: {
|
||||||
|
errorType: error instanceof Error ? error.constructor.name : 'Unknown'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default plugin
|
export default plugin
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ interface HealthCheckEntry {
|
|||||||
duration: string
|
duration: string
|
||||||
status: string
|
status: string
|
||||||
tags: string[]
|
tags: string[]
|
||||||
|
description?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface HealthCheckResponse {
|
interface HealthCheckResponse {
|
||||||
@@ -17,6 +18,23 @@ interface HealthCheckResponse {
|
|||||||
entries: Record<string, HealthCheckEntry>
|
entries: Record<string, HealthCheckEntry>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Interface for candle timeframe check data
|
||||||
|
interface CandleTimeframeCheck {
|
||||||
|
CheckedTicker: string
|
||||||
|
CheckedTimeframe: string
|
||||||
|
StartDate: string
|
||||||
|
LatestCandleDate?: string
|
||||||
|
TimeDifference?: string
|
||||||
|
Status: string
|
||||||
|
Message: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Web3ProxyHealthDetail {
|
||||||
|
status: string
|
||||||
|
message: string
|
||||||
|
data?: Record<string, any>
|
||||||
|
}
|
||||||
|
|
||||||
const HealthChecks: React.FC = () => {
|
const HealthChecks: React.FC = () => {
|
||||||
const { apiUrl, workerUrl } = useApiUrlStore()
|
const { apiUrl, workerUrl } = useApiUrlStore()
|
||||||
const [apiHealth, setApiHealth] = useState<HealthCheckResponse | null>(null)
|
const [apiHealth, setApiHealth] = useState<HealthCheckResponse | null>(null)
|
||||||
@@ -42,9 +60,8 @@ const HealthChecks: React.FC = () => {
|
|||||||
setWorkerHealth(data)
|
setWorkerHealth(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch Web3Proxy health check - assuming it's accessible via the API
|
// Fetch Web3Proxy health check - use the dedicated endpoint we created
|
||||||
// This might need adjustment based on your actual deployment
|
const web3Response = await fetch(`${apiUrl}/health/web3proxy`)
|
||||||
const web3Response = await fetch(`${apiUrl.replace(':5000', ':5002')}/health`)
|
|
||||||
if (web3Response.ok) {
|
if (web3Response.ok) {
|
||||||
const data = await web3Response.json()
|
const data = await web3Response.json()
|
||||||
setWeb3ProxyHealth(data)
|
setWeb3ProxyHealth(data)
|
||||||
@@ -72,18 +89,132 @@ const HealthChecks: React.FC = () => {
|
|||||||
status: 'Unreachable',
|
status: 'Unreachable',
|
||||||
duration: 'N/A',
|
duration: 'N/A',
|
||||||
tags: 'N/A',
|
tags: 'N/A',
|
||||||
|
description: 'Service unreachable',
|
||||||
|
details: null,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert entries to rows for the table
|
// Convert entries to rows for the table
|
||||||
return Object.entries(health.entries).map(([key, entry]) => ({
|
const results: any[] = [];
|
||||||
|
|
||||||
|
Object.entries(health.entries).forEach(([key, entry]) => {
|
||||||
|
// Basic health check entry
|
||||||
|
const baseEntry = {
|
||||||
service,
|
service,
|
||||||
component: key,
|
component: key,
|
||||||
status: entry.status,
|
status: entry.status,
|
||||||
duration: entry.duration,
|
duration: entry.duration,
|
||||||
tags: entry.tags.join(', '),
|
tags: entry.tags.join(', '),
|
||||||
}))
|
description: entry.description || '',
|
||||||
|
details: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add the base entry
|
||||||
|
results.push(baseEntry);
|
||||||
|
|
||||||
|
// Special handling for candle-data to expand timeframe checks
|
||||||
|
if (key === 'candle-data' && entry.data) {
|
||||||
|
// Extract timeframe checks
|
||||||
|
Object.entries(entry.data)
|
||||||
|
.filter(([dataKey]) => dataKey.startsWith('TimeframeCheck_'))
|
||||||
|
.forEach(([dataKey, timeframeData]) => {
|
||||||
|
const tfData = timeframeData as CandleTimeframeCheck;
|
||||||
|
results.push({
|
||||||
|
service,
|
||||||
|
component: `${key} - ${tfData.CheckedTimeframe}`,
|
||||||
|
status: tfData.Status,
|
||||||
|
duration: '',
|
||||||
|
tags: 'candles',
|
||||||
|
description: tfData.Message,
|
||||||
|
details: {
|
||||||
|
Ticker: tfData.CheckedTicker,
|
||||||
|
LatestCandle: tfData.LatestCandleDate,
|
||||||
|
TimeDifference: tfData.TimeDifference,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Special handling for Web3Proxy components
|
||||||
|
if (key === 'web3proxy' && entry.data) {
|
||||||
|
// Handle Privy check if present
|
||||||
|
if (entry.data.privy) {
|
||||||
|
const privyData = entry.data.privy as Web3ProxyHealthDetail;
|
||||||
|
results.push({
|
||||||
|
service,
|
||||||
|
component: `${key} - Privy`,
|
||||||
|
status: privyData.status,
|
||||||
|
duration: '',
|
||||||
|
tags: 'privy, external',
|
||||||
|
description: privyData.message || '',
|
||||||
|
details: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle GMX check if present
|
||||||
|
if (entry.data.gmx) {
|
||||||
|
const gmxData = entry.data.gmx as Web3ProxyHealthDetail;
|
||||||
|
const marketDetails: Record<string, any> = {};
|
||||||
|
|
||||||
|
// Add market count and response time if available
|
||||||
|
if (gmxData.data) {
|
||||||
|
if (gmxData.data.marketCount) {
|
||||||
|
marketDetails['Market Count'] = gmxData.data.marketCount;
|
||||||
|
}
|
||||||
|
if (gmxData.data.responseTimeMs) {
|
||||||
|
marketDetails['Response Time'] = `${gmxData.data.responseTimeMs}ms`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add sample markets info (just count for details section)
|
||||||
|
if (gmxData.data.sampleMarkets && Array.isArray(gmxData.data.sampleMarkets)) {
|
||||||
|
marketDetails['Sample Markets'] = gmxData.data.sampleMarkets.length;
|
||||||
|
|
||||||
|
// If there are sample markets, add the first one's details
|
||||||
|
if (gmxData.data.sampleMarkets.length > 0) {
|
||||||
|
const firstMarket = gmxData.data.sampleMarkets[0];
|
||||||
|
if (firstMarket.indexToken) {
|
||||||
|
marketDetails['Example Market'] = firstMarket.indexToken;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
results.push({
|
||||||
|
service,
|
||||||
|
component: `${key} - GMX`,
|
||||||
|
status: gmxData.status,
|
||||||
|
duration: '',
|
||||||
|
tags: 'gmx, external',
|
||||||
|
description: gmxData.message || '',
|
||||||
|
details: Object.keys(marketDetails).length > 0 ? marketDetails : null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add version info if available
|
||||||
|
if (entry.data.version) {
|
||||||
|
const versionDetails: Record<string, any> = {
|
||||||
|
Version: entry.data.version
|
||||||
|
};
|
||||||
|
|
||||||
|
if (entry.data.timestamp) {
|
||||||
|
versionDetails['Last Updated'] = entry.data.timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
results.push({
|
||||||
|
service,
|
||||||
|
component: `${key} - Version`,
|
||||||
|
status: entry.status,
|
||||||
|
duration: '',
|
||||||
|
tags: 'version',
|
||||||
|
description: `Web3Proxy Version: ${entry.data.version}`,
|
||||||
|
details: versionDetails,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Combine all health check data for display
|
// Combine all health check data for display
|
||||||
@@ -114,19 +245,41 @@ const HealthChecks: React.FC = () => {
|
|||||||
Cell: ({ value }: { value: string }) => (
|
Cell: ({ value }: { value: string }) => (
|
||||||
<span
|
<span
|
||||||
className={`badge ${
|
className={`badge ${
|
||||||
value === 'Healthy'
|
value === 'Healthy' || value === 'healthy'
|
||||||
? 'badge-success'
|
? 'badge-success'
|
||||||
: value === 'Unreachable'
|
: value === 'Unreachable' || value === 'unhealthy'
|
||||||
? 'badge-error'
|
? 'badge-error'
|
||||||
: 'badge-warning'
|
: 'badge-warning'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{value}
|
{value.charAt(0).toUpperCase() + value.slice(1)}
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
disableSortBy: true,
|
disableSortBy: true,
|
||||||
disableFilters: true,
|
disableFilters: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Header: 'Description',
|
||||||
|
accessor: 'description',
|
||||||
|
disableSortBy: true,
|
||||||
|
disableFilters: true,
|
||||||
|
Cell: ({ value, row }: any) => (
|
||||||
|
<div>
|
||||||
|
<div>{value}</div>
|
||||||
|
{row.original.details && (
|
||||||
|
<div className="text-xs mt-1 opacity-80">
|
||||||
|
{Object.entries(row.original.details).filter(([_, val]) => val !== undefined).map(
|
||||||
|
([key, val]) => (
|
||||||
|
<div key={key}>
|
||||||
|
<span className="font-semibold">{key}:</span> {String(val)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Header: 'Duration',
|
Header: 'Duration',
|
||||||
accessor: 'duration',
|
accessor: 'duration',
|
||||||
|
|||||||
Reference in New Issue
Block a user