From 6112c7f8eeb1c4ebd76021bc0c44983ada9ea12a Mon Sep 17 00:00:00 2001 From: Mononaut Date: Thu, 12 Dec 2024 00:51:30 +0000 Subject: [PATCH] Add git hashes to monitoring --- backend/src/api/bitcoin/esplora-api.ts | 70 ++++++++++++++++++- .../server-health.component.html | 12 ++++ .../server-health.component.scss | 2 +- .../src/app/interfaces/websocket.interface.ts | 5 ++ 4 files changed, 85 insertions(+), 4 deletions(-) diff --git a/backend/src/api/bitcoin/esplora-api.ts b/backend/src/api/bitcoin/esplora-api.ts index 9a4b7706a..2aea8e73c 100644 --- a/backend/src/api/bitcoin/esplora-api.ts +++ b/backend/src/api/bitcoin/esplora-api.ts @@ -1,12 +1,12 @@ import config from '../../config'; -import axios, { AxiosResponse, isAxiosError } from 'axios'; +import axios, { isAxiosError } from 'axios'; import http from 'http'; import { AbstractBitcoinApi, HealthCheckHost } from './bitcoin-api-abstract-factory'; import { IEsploraApi } from './esplora-api.interface'; import logger from '../../logger'; import { Common } from '../common'; import { SubmitPackageResult, TestMempoolAcceptResult } from './bitcoin-api.interface'; - +import os from 'os'; interface FailoverHost { host: string, rtts: number[], @@ -20,6 +20,12 @@ interface FailoverHost { preferred?: boolean, checked: boolean, lastChecked?: number, + hashes: { + frontend?: string, + backend?: string, + electrs?: string, + lastUpdated: number, + } } class FailoverRouter { @@ -29,14 +35,21 @@ class FailoverRouter { maxHeight: number = 0; hosts: FailoverHost[]; multihost: boolean; - pollInterval: number = 60000; + gitHashInterval: number = 600000; // 10 minutes + pollInterval: number = 60000; // 1 minute pollTimer: NodeJS.Timeout | null = null; pollConnection = axios.create(); + localHostname: string = 'localhost'; requestConnection = axios.create({ httpAgent: new http.Agent({ keepAlive: true }) }); constructor() { + try { + this.localHostname = os.hostname(); + } catch (e) { + logger.warn('Failed to set local hostname, using "localhost"'); + } // setup list of hosts this.hosts = (config.ESPLORA.FALLBACK || []).map(domain => { return { @@ -45,6 +58,9 @@ class FailoverRouter { rtts: [], rtt: Infinity, failures: 0, + hashes: { + lastUpdated: 0, + }, }; }); this.activeHost = { @@ -55,6 +71,9 @@ class FailoverRouter { socket: !!config.ESPLORA.UNIX_SOCKET_PATH, preferred: true, checked: false, + hashes: { + lastUpdated: 0, + }, }; this.fallbackHost = this.activeHost; this.hosts.unshift(this.activeHost); @@ -106,6 +125,24 @@ class FailoverRouter { host.outOfSync = false; } host.unreachable = false; + + // update esplora git hash using the x-powered-by header from the height check + const poweredBy = result.headers['x-powered-by']; + if (poweredBy) { + const match = poweredBy.match(/([a-fA-F0-9]{5,40})/); + if (match && match[1]?.length) { + host.hashes.electrs = match[1]; + } + } + + // Check front and backend git hashes less often + if (Date.now() - host.hashes.lastUpdated > this.gitHashInterval) { + await Promise.all([ + this.$updateFrontendGitHash(host), + this.$updateBackendGitHash(host) + ]); + host.hashes.lastUpdated = Date.now(); + } } else { host.outOfSync = true; host.unreachable = true; @@ -202,6 +239,32 @@ class FailoverRouter { } } + // methods for retrieving git hashes by host + private async $updateFrontendGitHash(host: FailoverHost): Promise { + try { + const url = host.socket ? `http://${this.localHostname}/resources/config.js` : `${host.host.slice(0, -4)}/resources/config.js`; + const response = await this.pollConnection.get(url, { timeout: config.ESPLORA.FALLBACK_TIMEOUT }); + const match = response.data.match(/GIT_COMMIT_HASH\s*=\s*['"](.*?)['"]/); + if (match && match[1]?.length) { + host.hashes.frontend = match[1]; + } + } catch (e) { + // failed to get frontend build hash - do nothing + } + } + + private async $updateBackendGitHash(host: FailoverHost): Promise { + try { + const url = host.socket ? `http://${this.localHostname}/api/v1/backend-info` : `${host.host}/v1/backend-info`; + const response = await this.pollConnection.get(url, { timeout: config.ESPLORA.FALLBACK_TIMEOUT }); + if (response.data?.gitCommit) { + host.hashes.backend = response.data.gitCommit; + } + } catch (e) { + // failed to get backend build hash - do nothing + } + } + private async $query(method: 'get'| 'post', path, data: any, responseType = 'json', host = this.activeHost, retry: boolean = true): Promise { let axiosConfig; let url; @@ -381,6 +444,7 @@ class ElectrsApi implements AbstractBitcoinApi { unreachable: !!host.unreachable, checked: !!host.checked, lastChecked: host.lastChecked || 0, + hashes: host.hashes, })); } else { return []; diff --git a/frontend/src/app/components/server-health/server-health.component.html b/frontend/src/app/components/server-health/server-health.component.html index 6a0a905f9..a3a4a31e5 100644 --- a/frontend/src/app/components/server-health/server-health.component.html +++ b/frontend/src/app/components/server-health/server-health.component.html @@ -19,6 +19,9 @@ RTT RTT Height + Front + Back + Electrs {{ i + 1 }} @@ -28,6 +31,15 @@ {{ (host.rtt / 1000) | number : '1.1-1' }} {{ host.rtt == null ? '' : 's'}} {{ !host.checked ? '⏳' : (host.unreachable ? '🔥' : '✅') }} {{ host.rtt | number : '1.0-0' }} {{ host.rtt == null ? '' : 'ms'}} {{ !host.checked ? '⏳' : (host.unreachable ? '🔥' : '✅') }} {{ host.latestHeight }} {{ !host.checked ? '⏳' : (host.outOfSync ? '🚫' : (host.latestHeight && host.latestHeight < maxHeight ? '🟧' : '✅')) }} + + + @if (host.hashes?.[type]) { + {{ host.hashes[type].slice(0, 8) || '?' }} + } @else { + ? + } + + diff --git a/frontend/src/app/components/server-health/server-health.component.scss b/frontend/src/app/components/server-health/server-health.component.scss index ff4ec1384..4aa58732b 100644 --- a/frontend/src/app/components/server-health/server-health.component.scss +++ b/frontend/src/app/components/server-health/server-health.component.scss @@ -9,7 +9,7 @@ } .status-panel { - max-width: 720px; + max-width: 1000px; margin: auto; padding: 1em; background: var(--box-bg); diff --git a/frontend/src/app/interfaces/websocket.interface.ts b/frontend/src/app/interfaces/websocket.interface.ts index 89c8e3884..d61610a2e 100644 --- a/frontend/src/app/interfaces/websocket.interface.ts +++ b/frontend/src/app/interfaces/websocket.interface.ts @@ -144,4 +144,9 @@ export interface HealthCheckHost { link?: string; statusPage?: SafeResourceUrl; flag?: string; + hashes?: { + frontend?: string; + backend?: string; + electrs?: string; + } }