@lodestar/beacon-node
Version:
A Typescript implementation of the beacon chain
176 lines • 6.83 kB
JavaScript
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