/** * Apache mod_status collector (service plugin). * * Enable: PEARDATA_APACHE=1 * URL: PEARDATA_APACHE_URL=http://127.0.0.1/server-status?auto * * Charts: apache.workers, apache.accesses, apache.traffic */ import http from 'http' import https from 'https' import { CollectorPlugin } from './plugin.js' import { registerChart } from '../../../shared/metrics.js' import logger from '../../utils/logger.js' const log = logger.child('apache') const CHART_WORKERS = { id: 'apache.workers', name: 'apache.workers', context: 'apache.workers', title: 'Apache workers', units: 'workers', family: 'apache', chartType: 'line', priority: 8400, plugin: 'apache', dimensions: [ { id: 'BusyWorkers', name: 'BusyWorkers', algorithm: 'absolute' }, { id: 'IdleWorkers', name: 'IdleWorkers', algorithm: 'absolute' }, ], } const CHART_ACCESSES = { id: 'apache.accesses', name: 'apache.accesses', context: 'apache.accesses', title: 'Apache total accesses', units: 'accesses/s', family: 'apache', chartType: 'line', priority: 8410, plugin: 'apache', dimensions: [{ id: 'Total_Accesses', name: 'Total Accesses', algorithm: 'absolute' }], } const CHART_TRAFFIC = { id: 'apache.traffic', name: 'apache.traffic', context: 'apache.traffic', title: 'Apache traffic', units: 'KiB/s', family: 'apache', chartType: 'line', priority: 8420, plugin: 'apache', dimensions: [{ id: 'Total_kBytes', name: 'Total kBytes', algorithm: 'absolute' }], } export function isApacheEnabled() { const v = process.env.PEARDATA_APACHE return v === '1' || v === 'on' || v === 'true' } /** * Parse Apache server-status?auto body. * @param {string} body * @returns {{ BusyWorkers: number, IdleWorkers: number, TotalAccesses: number, TotalKBytes: number }|null} */ export function parseApacheServerStatus(body) { const text = String(body || '') const busy = text.match(/BusyWorkers:\s*(\d+)/i) const idle = text.match(/IdleWorkers:\s*(\d+)/i) const accesses = text.match(/Total Accesses:\s*(\d+)/i) const kbytes = text.match(/Total kBytes:\s*(\d+)/i) if (!busy || !idle) return null return { BusyWorkers: Number(busy[1]), IdleWorkers: Number(idle[1]), TotalAccesses: accesses ? Number(accesses[1]) : 0, TotalKBytes: kbytes ? Number(kbytes[1]) : 0, } } function fetchText(url, timeoutMs = 3000) { return new Promise((resolve, reject) => { const mod = String(url).startsWith('https') ? https : http const req = mod.get(url, (res) => { let body = '' res.on('data', (c) => { body += c }) res.on('end', () => { if (res.statusCode && res.statusCode >= 400) { reject(new Error(`HTTP ${res.statusCode}`)) return } resolve(body) }) }) req.setTimeout(timeoutMs, () => { req.destroy() reject(new Error('timeout')) }) req.on('error', reject) }) } export class ApacheCollector extends CollectorPlugin { constructor(opts = {}) { super({ name: 'apache', intervalMs: opts.intervalMs }) this.url = opts.url || process.env.PEARDATA_APACHE_URL || 'http://127.0.0.1/server-status?auto' /** @type {{ accesses: number, kbytes: number, wallMs: number }|null} */ this._prev = null } isEnabled() { return isApacheEnabled() } start() { if (!this.isEnabled()) return registerChart(CHART_WORKERS) registerChart(CHART_ACCESSES) registerChart(CHART_TRAFFIC) log.info('Apache collector started', { url: this.url }) super.start() } async collect() { const body = await fetchText(this.url) const parsed = parseApacheServerStatus(body) if (!parsed) throw new Error('unrecognized server-status body') const ts = Date.now() let accessRate = 0 let trafficRate = 0 if (this._prev && ts > this._prev.wallMs) { const dt = (ts - this._prev.wallMs) / 1000 if (dt > 0) { accessRate = Math.max(0, (parsed.TotalAccesses - this._prev.accesses) / dt) trafficRate = Math.max(0, (parsed.TotalKBytes - this._prev.kbytes) / dt) } } this._prev = { accesses: parsed.TotalAccesses, kbytes: parsed.TotalKBytes, wallMs: ts, } return [ { chart: 'apache.workers', context: 'apache.workers', ts, values: { BusyWorkers: parsed.BusyWorkers, IdleWorkers: parsed.IdleWorkers, }, }, { chart: 'apache.accesses', context: 'apache.accesses', ts, values: { Total_Accesses: accessRate }, }, { chart: 'apache.traffic', context: 'apache.traffic', ts, values: { Total_kBytes: trafficRate }, }, ] } } /** @type {ApacheCollector|null} */ let singleton = null export function getApacheCollector() { if (!singleton) singleton = new ApacheCollector() return singleton }