Files
peardata/server/services/collectors/apache.js
T
Raven Scott 2e1a3e9b06
CI / test (push) Successful in 1m24s
Release rolling / release (push) Has been cancelled
Updates
2026-07-18 19:44:32 -04:00

184 lines
4.8 KiB
JavaScript

/**
* 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
}