From 5844d89175558d008bdea53e9b9899ce5846a607 Mon Sep 17 00:00:00 2001 From: cryptooda Date: Fri, 25 Apr 2025 13:34:59 +0700 Subject: [PATCH] Add new health --- .../HealthChecks/CandleDataHealthCheck.cs | 114 +++++++++++ .../GmxConnectivityHealthCheck.cs | 97 +++++++++ .../HealthChecks/Web3ProxyHealthCheck.cs | 187 ++++++++++++++++++ .../src/plugins/custom/gmx.ts | 19 +- src/Managing.Web3Proxy/src/routes/home.ts | 149 +++++++++++++- .../healthchecks/healthChecks.tsx | 183 +++++++++++++++-- 6 files changed, 716 insertions(+), 33 deletions(-) create mode 100644 src/Managing.Api/HealthChecks/CandleDataHealthCheck.cs create mode 100644 src/Managing.Api/HealthChecks/GmxConnectivityHealthCheck.cs create mode 100644 src/Managing.Api/HealthChecks/Web3ProxyHealthCheck.cs diff --git a/src/Managing.Api/HealthChecks/CandleDataHealthCheck.cs b/src/Managing.Api/HealthChecks/CandleDataHealthCheck.cs new file mode 100644 index 0000000..1b1c740 --- /dev/null +++ b/src/Managing.Api/HealthChecks/CandleDataHealthCheck.cs @@ -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 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>(); + 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 + { + ["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(); + 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 + { + ["ErrorMessage"] = ex.Message, + ["ErrorType"] = ex.GetType().Name + }); + } + } + } +} \ No newline at end of file diff --git a/src/Managing.Api/HealthChecks/GmxConnectivityHealthCheck.cs b/src/Managing.Api/HealthChecks/GmxConnectivityHealthCheck.cs new file mode 100644 index 0000000..96f3cf9 --- /dev/null +++ b/src/Managing.Api/HealthChecks/GmxConnectivityHealthCheck.cs @@ -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 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 + { + ["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 + { + ["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 + { + ["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 + { + ["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 + { + ["Endpoint"] = _gmxCandlesEndpoint, + ["ErrorMessage"] = ex.Message, + ["ErrorType"] = ex.GetType().Name + }); + } + } + } +} \ No newline at end of file diff --git a/src/Managing.Api/HealthChecks/Web3ProxyHealthCheck.cs b/src/Managing.Api/HealthChecks/Web3ProxyHealthCheck.cs new file mode 100644 index 0000000..ed7cbdf --- /dev/null +++ b/src/Managing.Api/HealthChecks/Web3ProxyHealthCheck.cs @@ -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 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 + { + ["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(); + + // 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(); + + 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(); + + 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>(); + + for (int i = 0; i < sampleMarketsElement.GetArrayLength(); i++) + { + var marketElement = sampleMarketsElement[i]; + var market = new Dictionary(); + + 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 + { + ["Endpoint"] = $"{_web3ProxyUrl}/health", + ["ErrorMessage"] = ex.Message, + ["ErrorType"] = ex.GetType().Name + }); + } + } + } +} \ No newline at end of file diff --git a/src/Managing.Web3Proxy/src/plugins/custom/gmx.ts b/src/Managing.Web3Proxy/src/plugins/custom/gmx.ts index 34a2916..11424a2 100644 --- a/src/Managing.Web3Proxy/src/plugins/custom/gmx.ts +++ b/src/Managing.Web3Proxy/src/plugins/custom/gmx.ts @@ -6,12 +6,12 @@ import {GmxSdk} from '../../generated/gmxsdk/index.js' import {arbitrum} from 'viem/chains'; import {getTokenBySymbol} from '../../generated/gmxsdk/configs/tokens.js'; import { + Position, + PositionStatus, Trade, TradeDirection, TradeStatus, - TradeType, - Position, - PositionStatus + TradeType } from '../../generated/ManagingApiTypes.js'; import {MarketInfo, MarketsInfoData} from '../../generated/gmxsdk/types/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 {GmxSdkConfig} from '../../generated/gmxsdk/types/sdk.js'; import {PositionIncreaseParams} from '../../generated/gmxsdk/modules/orders/helpers.js'; -import {numberToBigint, PRECISION_DECIMALS} from '../../generated/gmxsdk/utils/numbers.js'; -import {DecreasePositionSwapType, PositionOrderInfo} from '../../generated/gmxsdk/types/orders.js'; -import {OrderType} from '../../generated/gmxsdk/types/orders.js'; +import {bigintToNumber, numberToBigint, PRECISION_DECIMALS} from '../../generated/gmxsdk/utils/numbers.js'; +import {DecreasePositionSwapType, OrderType, PositionOrderInfo} from '../../generated/gmxsdk/types/orders.js'; import {DecreasePositionAmounts} from '../../generated/gmxsdk/types/trade.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 {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 * 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 * @returns An initialized GMX SDK client */ -export function getClientForAddress( +export async function getClientForAddress( account: string, -): GmxSdk { +): Promise { try { // Create SDK instance const arbitrumSdkConfig: GmxSdkConfig = { diff --git a/src/Managing.Web3Proxy/src/routes/home.ts b/src/Managing.Web3Proxy/src/routes/home.ts index f47968b..3ec08f8 100644 --- a/src/Managing.Web3Proxy/src/routes/home.ts +++ b/src/Managing.Web3Proxy/src/routes/home.ts @@ -1,5 +1,7 @@ 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) => { fastify.get( @@ -27,16 +29,27 @@ const plugin: FastifyPluginAsyncTypebox = async (fastify) => { } } ) - // Add health check endpoint + // Enhanced health check endpoint fastify.get('/health', { schema: { 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: { 200: Type.Object({ status: 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({ success: Type.Boolean(), @@ -46,15 +59,139 @@ const plugin: FastifyPluginAsyncTypebox = async (fastify) => { } }, async function (request, reply) { 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 { - status: 'ok', + status: overallStatus, 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) { 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 diff --git a/src/Managing.WebApp/src/pages/settingsPage/healthchecks/healthChecks.tsx b/src/Managing.WebApp/src/pages/settingsPage/healthchecks/healthChecks.tsx index 51adb4e..27743f1 100644 --- a/src/Managing.WebApp/src/pages/settingsPage/healthchecks/healthChecks.tsx +++ b/src/Managing.WebApp/src/pages/settingsPage/healthchecks/healthChecks.tsx @@ -1,7 +1,7 @@ -import React, { useEffect, useState } from 'react' +import React, {useEffect, useState} from 'react' import useApiUrlStore from '../../../app/store/apiStore' -import { Table } from '../../../components/mollecules' +import {Table} from '../../../components/mollecules' // Define health check response interface based on the provided example interface HealthCheckEntry { @@ -9,6 +9,7 @@ interface HealthCheckEntry { duration: string status: string tags: string[] + description?: string } interface HealthCheckResponse { @@ -17,6 +18,23 @@ interface HealthCheckResponse { entries: Record } +// 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 +} + const HealthChecks: React.FC = () => { const { apiUrl, workerUrl } = useApiUrlStore() const [apiHealth, setApiHealth] = useState(null) @@ -42,9 +60,8 @@ const HealthChecks: React.FC = () => { setWorkerHealth(data) } - // Fetch Web3Proxy health check - assuming it's accessible via the API - // This might need adjustment based on your actual deployment - const web3Response = await fetch(`${apiUrl.replace(':5000', ':5002')}/health`) + // Fetch Web3Proxy health check - use the dedicated endpoint we created + const web3Response = await fetch(`${apiUrl}/health/web3proxy`) if (web3Response.ok) { const data = await web3Response.json() setWeb3ProxyHealth(data) @@ -72,18 +89,132 @@ const HealthChecks: React.FC = () => { status: 'Unreachable', duration: 'N/A', tags: 'N/A', + description: 'Service unreachable', + details: null, }, ] } // Convert entries to rows for the table - return Object.entries(health.entries).map(([key, entry]) => ({ - service, - component: key, - status: entry.status, - duration: entry.duration, - tags: entry.tags.join(', '), - })) + const results: any[] = []; + + Object.entries(health.entries).forEach(([key, entry]) => { + // Basic health check entry + const baseEntry = { + service, + component: key, + status: entry.status, + duration: entry.duration, + 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 = {}; + + // 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 = { + 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 @@ -114,19 +245,41 @@ const HealthChecks: React.FC = () => { Cell: ({ value }: { value: string }) => ( - {value} + {value.charAt(0).toUpperCase() + value.slice(1)} ), disableSortBy: true, disableFilters: true, }, + { + Header: 'Description', + accessor: 'description', + disableSortBy: true, + disableFilters: true, + Cell: ({ value, row }: any) => ( +
+
{value}
+ {row.original.details && ( +
+ {Object.entries(row.original.details).filter(([_, val]) => val !== undefined).map( + ([key, val]) => ( +
+ {key}: {String(val)} +
+ ) + )} +
+ )} +
+ ), + }, { Header: 'Duration', accessor: 'duration',