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 {
    clientStats;
    remoteServiceUrl;
    remoteServiceHost;
    options;
    register;
    logger;
    collectDataMetric;
    sendDataMetric;
    status = Status.Started;
    initialDelayTimeout;
    monitoringInterval;
    fetchAbortController;
    pendingRequest;
    constructor(client, options, { register, logger }) {
        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 and wait for any pending request to complete
     */
    async 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);
            await this.pendingRequest;
        }
    }
    /**
     * 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