UNPKG

@lodestar/beacon-node

Version:

A Typescript implementation of the beacon chain

176 lines 6.83 kB
import { ErrorAborted, TimeoutError, fetch } from "@lodestar/utils"; import { createClientStats } from "./clientStats.js"; import { defaultMonitoringOptions } from "./options.js"; import system from "./system.js"; var FetchAbortReason; (function (FetchAbortReason) { FetchAbortReason["Close"] = "close"; FetchAbortReason["Timeout"] = "timeout"; })(FetchAbortReason || (FetchAbortReason = {})); var Status; (function (Status) { Status["Started"] = "started"; Status["Closed"] = "closed"; })(Status || (Status = {})); var SendDataStatus; (function (SendDataStatus) { SendDataStatus["Success"] = "success"; SendDataStatus["Error"] = "error"; })(SendDataStatus || (SendDataStatus = {})); /** * Service for sending clients stats to a remote service (e.g. beaconcha.in) */ export class MonitoringService { constructor(client, options, { register, logger }) { this.status = Status.Started; this.options = { ...defaultMonitoringOptions, ...options }; this.logger = logger; this.register = register; this.remoteServiceUrl = this.parseMonitoringEndpoint(this.options.endpoint); this.remoteServiceHost = this.remoteServiceUrl.host; this.clientStats = createClientStats(client, this.options.collectSystemStats); this.collectDataMetric = register.histogram({ name: "lodestar_monitoring_collect_data_seconds", help: "Time spent to collect monitoring data in seconds", buckets: [0.001, 0.01, 0.1, 1, 5], }); this.sendDataMetric = register.histogram({ name: "lodestar_monitoring_send_data_seconds", help: "Time spent to send monitoring data to remote service in seconds", labelNames: ["status"], buckets: [0.3, 0.5, 1, Math.floor(this.options.requestTimeout / 1000)], }); this.initialDelayTimeout = setTimeout(async () => { await this.send(); this.nextMonitoringInterval(); }, this.options.initialDelay); this.logger.info("Started monitoring service", { // do not log full URL as it may contain secrets remote: this.remoteServiceHost, machine: this.remoteServiceUrl.searchParams.get("machine"), interval: this.options.interval, }); } /** * Stop sending client stats */ close() { if (this.status === Status.Closed) return; this.status = Status.Closed; if (this.initialDelayTimeout) { clearTimeout(this.initialDelayTimeout); } if (this.monitoringInterval) { clearTimeout(this.monitoringInterval); } if (this.pendingRequest) { this.fetchAbortController?.abort(FetchAbortReason.Close); } } /** * Collect and send client stats */ async send() { if (!this.pendingRequest) { this.pendingRequest = (async () => { try { const data = await this.collectData(); const res = await this.sendData(data); if (!res.ok) { const error = (await res.json()); throw new Error(error.status); } this.logger.debug(`Sent client stats to ${this.remoteServiceHost}`, { data: JSON.stringify(data) }); } catch (e) { this.logger.warn(`Failed to send client stats to ${this.remoteServiceHost}`, { reason: e.message }); } finally { this.pendingRequest = undefined; } })(); } await this.pendingRequest; } async collectData() { const timer = this.collectDataMetric.startTimer(); const data = []; const recordPromises = []; if (this.options.collectSystemStats) { await system.collectData(this.logger); } for (const [i, s] of this.clientStats.entries()) { data[i] = {}; recordPromises.push(...Object.values(s).map(async (property) => { const record = await property.getRecord(this.register); data[i][record.key] = record.value; })); } await Promise.all(recordPromises).finally(timer); return data; } async sendData(data) { const timer = this.sendDataMetric.startTimer(); this.fetchAbortController = new AbortController(); const timeout = setTimeout(() => this.fetchAbortController?.abort(FetchAbortReason.Timeout), this.options.requestTimeout); let res; try { res = await fetch(this.remoteServiceUrl, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(data), signal: this.fetchAbortController.signal, }); return res; } catch (e) { const { signal } = this.fetchAbortController; if (!signal.aborted) { // error was thrown by fetch throw e; } // error was thrown by abort signal if (signal.reason === FetchAbortReason.Close) { throw new ErrorAborted("request"); } if (signal.reason === FetchAbortReason.Timeout) { throw new TimeoutError("request"); } throw e; } finally { timer({ status: res?.ok ? SendDataStatus.Success : SendDataStatus.Error }); clearTimeout(timeout); } } nextMonitoringInterval() { if (this.status === Status.Closed) return; // ensure next interval only starts after previous request has finished // else we might send next request too early and run into rate limit errors this.monitoringInterval = setTimeout(async () => { await this.send(); this.nextMonitoringInterval(); }, this.options.interval); } parseMonitoringEndpoint(endpoint) { if (!endpoint) { throw new Error(`Monitoring endpoint is empty or undefined: ${endpoint}`); } try { const url = new URL(endpoint); if (url.protocol === "http:") { this.logger.warn("Insecure monitoring endpoint, please make sure to always use a HTTPS connection in production"); } else if (url.protocol !== "https:") { throw new Error(); } return url; } catch (_e) { throw new Error(`Monitoring endpoint must be a valid URL: ${endpoint}`); } } } //# sourceMappingURL=service.js.map