"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
    if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.PrometheusMetricsService = void 0;
const node_fs_1 = require("node:fs");
const config_1 = require("@n8n/config");
const constants_1 = require("@n8n/constants");
const db_1 = require("@n8n/db");
const decorators_1 = require("@n8n/decorators");
const di_1 = require("@n8n/di");
const express_prom_bundle_1 = __importDefault(require("express-prom-bundle"));
const luxon_1 = require("luxon");
const n8n_core_1 = require("n8n-core");
const n8n_workflow_1 = require("n8n-workflow");
const prom_client_1 = __importDefault(require("prom-client"));
const parse_1 = __importDefault(require("semver/functions/parse"));
const constants_2 = require("../constants");
const message_event_bus_1 = require("../eventbus/message-event-bus/message-event-bus");
const event_service_1 = require("../events/event.service");
const cache_service_1 = require("../services/cache/cache.service");
let PrometheusMetricsService = class PrometheusMetricsService {
    constructor(cacheService, eventBus, globalConfig, eventService, instanceSettings, workflowRepository, licenseMetricsRepository) {
        this.cacheService = cacheService;
        this.eventBus = eventBus;
        this.globalConfig = globalConfig;
        this.eventService = eventService;
        this.instanceSettings = instanceSettings;
        this.workflowRepository = workflowRepository;
        this.licenseMetricsRepository = licenseMetricsRepository;
        this.counters = {};
        this.tokenExchangeListenersRegistered = false;
        this.gauges = {};
        this.histograms = {};
        this.prefix = this.globalConfig.endpoints.metrics.prefix;
        this.includes = {
            metrics: {
                default: this.globalConfig.endpoints.metrics.includeDefaultMetrics,
                routes: this.globalConfig.endpoints.metrics.includeApiEndpoints,
                cache: this.globalConfig.endpoints.metrics.includeCacheMetrics,
                logs: this.globalConfig.endpoints.metrics.includeMessageEventBusMetrics,
                queue: this.globalConfig.endpoints.metrics.includeQueueMetrics,
                workflowExecutionDuration: this.globalConfig.endpoints.metrics.includeWorkflowExecutionDuration,
                workflowStatistics: this.globalConfig.endpoints.metrics.includeWorkflowStatistics,
            },
            labels: {
                credentialsType: this.globalConfig.endpoints.metrics.includeCredentialTypeLabel,
                nodeType: this.globalConfig.endpoints.metrics.includeNodeTypeLabel,
                workflowId: this.globalConfig.endpoints.metrics.includeWorkflowIdLabel,
                apiPath: this.globalConfig.endpoints.metrics.includeApiPathLabel,
                apiMethod: this.globalConfig.endpoints.metrics.includeApiMethodLabel,
                apiStatusCode: this.globalConfig.endpoints.metrics.includeApiStatusCodeLabel,
                workflowName: this.globalConfig.endpoints.metrics.includeWorkflowNameLabel,
            },
        };
        this.workflowStatisticsCache = null;
    }
    async init(app) {
        prom_client_1.default.register.clear();
        this.initDefaultMetrics();
        this.initPssMetric();
        this.initN8nVersionMetric();
        if (this.instanceSettings.instanceType === 'main')
            this.initInstanceRoleMetric();
        this.initCacheMetrics();
        this.initEventBusMetrics();
        this.initRouteMetrics(app);
        this.initQueueMetrics();
        this.initWorkflowExecutionDurationMetric();
        this.initActiveWorkflowCountMetric();
        this.initWorkflowStatisticsMetrics();
        this.initTokenExchangeMetrics();
        this.mountMetricsEndpoint(app);
    }
    enableMetric(metric) {
        this.includes.metrics[metric] = true;
    }
    disableMetric(metric) {
        this.includes.metrics[metric] = false;
    }
    disableAllMetrics() {
        for (const metric in this.includes.metrics) {
            this.includes.metrics[metric] = false;
        }
    }
    enableLabels(labels) {
        for (const label of labels) {
            this.includes.labels[label] = true;
        }
    }
    disableAllLabels() {
        for (const label in this.includes.labels) {
            this.includes.labels[label] = false;
        }
    }
    initN8nVersionMetric() {
        const n8nVersion = (0, parse_1.default)(constants_2.N8N_VERSION ?? '0.0.0');
        if (!n8nVersion)
            return;
        const versionGauge = new prom_client_1.default.Gauge({
            name: this.prefix + 'version_info',
            help: 'n8n version info.',
            labelNames: ['version', 'major', 'minor', 'patch'],
        });
        const { version, major, minor, patch } = n8nVersion;
        versionGauge.set({ version: 'v' + version, major, minor, patch }, 1);
    }
    initInstanceRoleMetric() {
        this.gauges.instanceRoleLeader = new prom_client_1.default.Gauge({
            name: this.prefix + 'instance_role_leader',
            help: 'Whether this main instance is the leader (1) or not (0).',
        });
        this.gauges.instanceRoleLeader.set(this.instanceSettings.isLeader ? 1 : 0);
    }
    updateOnLeaderTakeover() {
        this.gauges.instanceRoleLeader?.set(1);
    }
    updateOnLeaderStepdown() {
        this.gauges.instanceRoleLeader?.set(0);
    }
    initDefaultMetrics() {
        if (!this.includes.metrics.default)
            return;
        prom_client_1.default.collectDefaultMetrics({ prefix: this.globalConfig.endpoints.metrics.prefix });
    }
    initPssMetric() {
        if (!this.includes.metrics.default)
            return;
        let pssAvailable = true;
        try {
            (0, node_fs_1.readFileSync)('/proc/self/smaps_rollup', 'utf8');
        }
        catch {
            pssAvailable = false;
        }
        if (!pssAvailable)
            return;
        const prefix = this.prefix;
        new prom_client_1.default.Gauge({
            name: prefix + 'process_pss_bytes',
            help: 'Proportional Set Size of the process in bytes.',
            collect() {
                try {
                    const content = (0, node_fs_1.readFileSync)('/proc/self/smaps_rollup', 'utf8');
                    const match = content.match(/^Pss:\s+(\d+)\s+kB$/m);
                    if (match) {
                        this.set(parseInt(match[1], 10) * 1024);
                    }
                }
                catch {
                }
            },
        });
    }
    initRouteMetrics(app) {
        if (!this.includes.metrics.routes)
            return;
        const metricsMiddleware = (0, express_prom_bundle_1.default)({
            autoregister: false,
            includeUp: false,
            includePath: this.includes.labels.apiPath,
            includeMethod: this.includes.labels.apiMethod,
            includeStatusCode: this.includes.labels.apiStatusCode,
            httpDurationMetricName: this.prefix + 'http_request_duration_seconds',
        });
        const activityGauge = new prom_client_1.default.Gauge({
            name: this.prefix + 'last_activity',
            help: 'last instance activity (backend request) in Unix time (seconds).',
        });
        activityGauge.set(luxon_1.DateTime.now().toUnixInteger());
        app.use([
            '/api/',
            `/${this.globalConfig.endpoints.rest}/`,
            `/${this.globalConfig.endpoints.webhook}/`,
            `/${this.globalConfig.endpoints.webhookWaiting}/`,
            `/${this.globalConfig.endpoints.webhookTest}/`,
            `/${this.globalConfig.endpoints.form}/`,
            `/${this.globalConfig.endpoints.formWaiting}/`,
            `/${this.globalConfig.endpoints.formTest}/`,
        ], async (req, res, next) => {
            activityGauge.set(luxon_1.DateTime.now().toUnixInteger());
            await metricsMiddleware(req, res, next);
        });
    }
    mountMetricsEndpoint(app) {
        app.get('/metrics', async (_req, res) => {
            const metrics = await prom_client_1.default.register.metrics();
            res.setHeader('Content-Type', prom_client_1.default.register.contentType);
            res.send(metrics).end();
        });
    }
    initCacheMetrics() {
        if (!this.includes.metrics.cache)
            return;
        const [hitsConfig, missesConfig, updatesConfig] = ['hits', 'misses', 'updates'].map((kind) => ({
            name: this.prefix + 'cache_' + kind + '_total',
            help: `Total number of cache ${kind}.`,
            labelNames: ['cache'],
        }));
        this.counters.cacheHitsTotal = new prom_client_1.default.Counter(hitsConfig);
        this.counters.cacheHitsTotal.inc(0);
        this.cacheService.on('metrics.cache.hit', () => this.counters.cacheHitsTotal?.inc(1));
        this.counters.cacheMissesTotal = new prom_client_1.default.Counter(missesConfig);
        this.counters.cacheMissesTotal.inc(0);
        this.cacheService.on('metrics.cache.miss', () => this.counters.cacheMissesTotal?.inc(1));
        this.counters.cacheUpdatesTotal = new prom_client_1.default.Counter(updatesConfig);
        this.counters.cacheUpdatesTotal.inc(0);
        this.cacheService.on('metrics.cache.update', () => this.counters.cacheUpdatesTotal?.inc(1));
    }
    toCounter(event) {
        const { eventName } = event;
        if (!this.counters[eventName]) {
            const metricName = this.prefix + eventName.replace('n8n.', '').replace(/\./g, '_') + '_total';
            if (!prom_client_1.default.validateMetricName(metricName)) {
                this.counters[eventName] = null;
                return null;
            }
            const labels = this.toLabels(event);
            const counter = new prom_client_1.default.Counter({
                name: metricName,
                help: `Total number of ${eventName} events.`,
                labelNames: Object.keys(labels),
            });
            this.counters[eventName] = counter;
        }
        return this.counters[eventName];
    }
    initEventBusMetrics() {
        if (!this.includes.metrics.logs)
            return;
        this.eventBus.on('metrics.eventBus.event', (event) => {
            const counter = this.toCounter(event);
            if (!counter)
                return;
            const labels = this.toLabels(event);
            counter.inc(labels, 1);
        });
    }
    initQueueMetrics() {
        if (!this.includes.metrics.queue ||
            this.globalConfig.executions.mode !== 'queue' ||
            this.instanceSettings.instanceType !== 'main') {
            return;
        }
        this.gauges.waiting = new prom_client_1.default.Gauge({
            name: this.prefix + 'scaling_mode_queue_jobs_waiting',
            help: 'Current number of enqueued jobs waiting for pickup in scaling mode.',
        });
        this.gauges.active = new prom_client_1.default.Gauge({
            name: this.prefix + 'scaling_mode_queue_jobs_active',
            help: 'Current number of jobs being processed across all workers in scaling mode.',
        });
        this.counters.completed = new prom_client_1.default.Counter({
            name: this.prefix + 'scaling_mode_queue_jobs_completed',
            help: 'Total number of jobs completed across all workers in scaling mode since instance start.',
        });
        this.counters.failed = new prom_client_1.default.Counter({
            name: this.prefix + 'scaling_mode_queue_jobs_failed',
            help: 'Total number of jobs failed across all workers in scaling mode since instance start.',
        });
        this.gauges.waiting.set(0);
        this.gauges.active.set(0);
        this.counters.completed.inc(0);
        this.counters.failed.inc(0);
        this.eventService.on('job-counts-updated', (jobCounts) => {
            this.gauges.waiting.set(jobCounts.waiting);
            this.gauges.active.set(jobCounts.active);
            this.counters.completed?.inc(jobCounts.completed);
            this.counters.failed?.inc(jobCounts.failed);
        });
    }
    initWorkflowExecutionDurationMetric() {
        if (!this.includes.metrics.workflowExecutionDuration)
            return;
        const labelNames = ['status', 'mode'];
        if (this.includes.labels.workflowId)
            labelNames.push('workflow_id');
        this.histograms.workflowExecutionDuration = new prom_client_1.default.Histogram({
            name: this.prefix + 'workflow_execution_duration_seconds',
            help: 'Workflow execution duration in seconds.',
            labelNames,
            buckets: [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10, 30, 60, 120, 300, 600],
        });
        this.eventService.on('workflow-post-execute', ({ runData, workflow }) => {
            if (!runData?.stoppedAt)
                return;
            const durationSeconds = (runData.stoppedAt.getTime() - runData.startedAt.getTime()) / 1000;
            const labels = {
                status: runData.status === 'success' ? 'success' : 'failed',
                mode: runData.mode,
            };
            if (this.includes.labels.workflowId) {
                labels.workflow_id = String(workflow.id ?? 'unknown');
            }
            this.histograms.workflowExecutionDuration?.observe(labels, durationSeconds);
        });
    }
    initActiveWorkflowCountMetric() {
        const workflowRepository = this.workflowRepository;
        const cacheService = this.cacheService;
        const cacheKey = 'metrics:active-workflow-count';
        const cacheTtl = this.globalConfig.endpoints.metrics.activeWorkflowCountInterval * constants_1.Time.seconds.toMilliseconds;
        new prom_client_1.default.Gauge({
            name: this.prefix + 'active_workflow_count',
            help: 'Total number of active workflows.',
            async collect() {
                const value = await cacheService.get(cacheKey);
                const numericValue = value !== undefined ? parseInt(value, 10) : undefined;
                if (numericValue !== undefined && Number.isFinite(numericValue)) {
                    this.set(numericValue);
                }
                else {
                    const activeWorkflowCount = await workflowRepository.getActiveCount();
                    await cacheService.set(cacheKey, activeWorkflowCount.toString(), cacheTtl);
                    this.set(activeWorkflowCount);
                }
            },
        });
    }
    toLabels(event) {
        const { __type, eventName, payload } = event;
        switch (__type) {
            case "$$EventMessageAudit":
                if (eventName.startsWith('n8n.audit.user.credentials')) {
                    return this.includes.labels.credentialsType
                        ? {
                            credential_type: String((event.payload.credentialType ?? 'unknown').replace(/\./g, '_')),
                        }
                        : {};
                }
                if (eventName.startsWith('n8n.audit.workflow')) {
                    return this.buildWorkflowLabels(payload);
                }
                break;
            case "$$EventMessageNode":
                const nodeLabels = this.buildWorkflowLabels(payload);
                if (this.includes.labels.nodeType) {
                    nodeLabels.node_type = String((payload.nodeType ?? 'unknown').replace('n8n-nodes-', '').replace(/\./g, '_'));
                }
                return nodeLabels;
            case "$$EventMessageWorkflow":
                return this.buildWorkflowLabels(payload);
        }
        return {};
    }
    buildWorkflowLabels(payload) {
        const labels = {};
        if (this.includes.labels.workflowId) {
            labels.workflow_id = String(payload.workflowId ?? 'unknown');
        }
        if (this.includes.labels.workflowName) {
            labels.workflow_name = String(payload.workflowName ?? 'unknown');
        }
        return labels;
    }
    async getWorkflowStatistics(cacheService, licenseMetricsRepository, cacheTtl) {
        const now = Date.now();
        if (this.workflowStatisticsCache &&
            now - this.workflowStatisticsCache.timestamp < this.workflowStatisticsCache.ttl) {
            return this.workflowStatisticsCache.data;
        }
        const fullCacheKey = 'metrics:workflow-statistics:shared';
        const cachedValue = await cacheService.get(fullCacheKey);
        if (cachedValue !== undefined) {
            const parsedValue = (0, n8n_workflow_1.jsonParse)(String(cachedValue), { fallbackValue: undefined });
            if (parsedValue !== undefined) {
                this.workflowStatisticsCache = {
                    data: parsedValue,
                    timestamp: now,
                    ttl: 1000,
                };
                return parsedValue;
            }
        }
        const metrics = await licenseMetricsRepository.getLicenseRenewalMetrics();
        await cacheService.set(fullCacheKey, JSON.stringify(metrics), cacheTtl);
        this.workflowStatisticsCache = {
            data: metrics,
            timestamp: now,
            ttl: 1000,
        };
        return metrics;
    }
    createWorkflowStatisticsGauge(metricName, help, getMetricValue, cacheService, licenseMetricsRepository, cacheTtl) {
        const getWorkflowStatistics = this.getWorkflowStatistics.bind(this);
        return new prom_client_1.default.Gauge({
            name: this.prefix + metricName,
            help,
            async collect() {
                const metrics = await getWorkflowStatistics(cacheService, licenseMetricsRepository, cacheTtl);
                const value = getMetricValue(metrics);
                this.set(value);
            },
        });
    }
    initWorkflowStatisticsMetrics() {
        if (!this.includes.metrics.workflowStatistics)
            return;
        const licenseMetricsRepository = this.licenseMetricsRepository;
        const cacheService = this.cacheService;
        const cacheTtl = this.globalConfig.endpoints.metrics.workflowStatisticsInterval * constants_1.Time.seconds.toMilliseconds;
        const metricsConfig = [
            {
                name: 'production_executions',
                help: 'Total number of production workflow executions (success + error).',
                getValue: (metrics) => Number(metrics.productionExecutions) || 0,
            },
            {
                name: 'production_root_executions',
                help: 'Total number of production root workflow executions (excludes sub-workflows).',
                getValue: (metrics) => Number(metrics.productionRootExecutions) || 0,
            },
            {
                name: 'manual_executions',
                help: 'Total number of manual workflow executions (success + error).',
                getValue: (metrics) => Number(metrics.manualExecutions) || 0,
            },
            {
                name: 'enabled_users',
                help: 'Total number of enabled users.',
                getValue: (metrics) => Number(metrics.enabledUsers) || 0,
            },
            {
                name: 'users',
                help: 'Total number of users.',
                getValue: (metrics) => Number(metrics.totalUsers) || 0,
            },
            {
                name: 'workflows',
                help: 'Total number of workflows.',
                getValue: (metrics) => Number(metrics.totalWorkflows) || 0,
            },
            {
                name: 'credentials',
                help: 'Total number of credentials.',
                getValue: (metrics) => Number(metrics.totalCredentials) || 0,
            },
        ];
        metricsConfig.forEach((config) => {
            this.createWorkflowStatisticsGauge(config.name, config.help, config.getValue, cacheService, licenseMetricsRepository, cacheTtl);
        });
    }
    initTokenExchangeMetrics() {
        this.counters.tokenExchangeRequestsTotal = new prom_client_1.default.Counter({
            name: this.prefix + 'token_exchange_requests_total',
            help: 'Total number of token exchange requests.',
            labelNames: ['result'],
        });
        this.counters.tokenExchangeRequestsTotal.inc({ result: 'success' }, 0);
        this.counters.tokenExchangeRequestsTotal.inc({ result: 'failure' }, 0);
        this.counters.tokenExchangeFailuresTotal = new prom_client_1.default.Counter({
            name: this.prefix + 'token_exchange_failures_total',
            help: 'Total number of token exchange failures broken down by reason.',
            labelNames: ['reason'],
        });
        this.counters.embedLoginRequestsTotal = new prom_client_1.default.Counter({
            name: this.prefix + 'embed_login_requests_total',
            help: 'Total number of embed login requests.',
            labelNames: ['result'],
        });
        this.counters.embedLoginRequestsTotal.inc({ result: 'success' }, 0);
        this.counters.embedLoginRequestsTotal.inc({ result: 'failure' }, 0);
        this.counters.embedLoginFailuresTotal = new prom_client_1.default.Counter({
            name: this.prefix + 'embed_login_failures_total',
            help: 'Total number of embed login failures broken down by reason.',
            labelNames: ['reason'],
        });
        this.counters.tokenExchangeJitProvisioningTotal = new prom_client_1.default.Counter({
            name: this.prefix + 'token_exchange_jit_provisioning_total',
            help: 'Total number of users JIT-provisioned via token exchange.',
        });
        this.counters.tokenExchangeJitProvisioningTotal.inc(0);
        this.counters.tokenExchangeIdentityLinkedTotal = new prom_client_1.default.Counter({
            name: this.prefix + 'token_exchange_identity_linked_total',
            help: 'Total number of external identities linked to existing users via token exchange.',
        });
        this.counters.tokenExchangeIdentityLinkedTotal.inc(0);
        if (this.tokenExchangeListenersRegistered)
            return;
        this.tokenExchangeListenersRegistered = true;
        this.eventService.on('token-exchange-succeeded', () => {
            this.counters.tokenExchangeRequestsTotal?.inc({ result: 'success' }, 1);
        });
        this.eventService.on('token-exchange-failed', ({ failureReason }) => {
            this.counters.tokenExchangeRequestsTotal?.inc({ result: 'failure' }, 1);
            this.counters.tokenExchangeFailuresTotal?.inc({ reason: failureReason }, 1);
        });
        this.eventService.on('embed-login', () => {
            this.counters.embedLoginRequestsTotal?.inc({ result: 'success' }, 1);
        });
        this.eventService.on('embed-login-failed', ({ failureReason }) => {
            this.counters.embedLoginRequestsTotal?.inc({ result: 'failure' }, 1);
            this.counters.embedLoginFailuresTotal?.inc({ reason: failureReason }, 1);
        });
        this.eventService.on('token-exchange-user-provisioned', () => {
            this.counters.tokenExchangeJitProvisioningTotal?.inc(1);
        });
        this.eventService.on('token-exchange-identity-linked', () => {
            this.counters.tokenExchangeIdentityLinkedTotal?.inc(1);
        });
    }
};
exports.PrometheusMetricsService = PrometheusMetricsService;
__decorate([
    (0, decorators_1.OnLeaderTakeover)(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", []),
    __metadata("design:returntype", void 0)
], PrometheusMetricsService.prototype, "updateOnLeaderTakeover", null);
__decorate([
    (0, decorators_1.OnLeaderStepdown)(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", []),
    __metadata("design:returntype", void 0)
], PrometheusMetricsService.prototype, "updateOnLeaderStepdown", null);
exports.PrometheusMetricsService = PrometheusMetricsService = __decorate([
    (0, di_1.Service)(),
    __metadata("design:paramtypes", [cache_service_1.CacheService,
        message_event_bus_1.MessageEventBus,
        config_1.GlobalConfig,
        event_service_1.EventService,
        n8n_core_1.InstanceSettings,
        db_1.WorkflowRepository,
        db_1.LicenseMetricsRepository])
], PrometheusMetricsService);
//# sourceMappingURL=prometheus-metrics.service.js.map