/** * Nginx stub_status collector (service plugin spike). * * Enable: PEARDATA_NGINX=1 * URL: PEARDATA_NGINX_URL=http://127.0.0.1/nginx_status * * Charts: nginx.connections, nginx.requests */ 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('nginx') const CHART_CONNECTIONS = { id: 'nginx.connections', name: 'nginx.connections', context: 'nginx.connections', title: 'Nginx connections', units: 'connections', family: 'nginx', chartType: 'line', priority: 8000, plugin: 'nginx', dimensions: [ { id: 'active', name: 'active', algorithm: 'absolute' }, { id: 'reading', name: 'reading', algorithm: 'absolute' }, { id: 'writing', name: 'writing', algorithm: 'absolute' }, { id: 'waiting', name: 'waiting', algorithm: 'absolute' }, ], } const CHART_REQUESTS = { id: 'nginx.requests', name: 'nginx.requests', context: 'nginx.requests', title: 'Nginx requests', units: 'requests/s', family: 'nginx', chartType: 'line', priority: 8010, plugin: 'nginx', dimensions: [ { id: 'accepts', name: 'accepts', algorithm: 'incremental' }, { id: 'handled', name: 'handled', algorithm: 'incremental' }, { id: 'requests', name: 'requests', algorithm: 'incremental' }, ], } export function isNginxEnabled() { const v = process.env.PEARDATA_NGINX return v === '1' || v === 'on' || v === 'true' } /** * Parse nginx stub_status body. * @param {string} body * @returns {{ active: number, accepts: number, handled: number, requests: number, reading: number, writing: number, waiting: number }|null} */ export function parseNginxStubStatus(body) { const text = String(body || '') const active = text.match(/Active connections:\s*(\d+)/i) const counters = text.match(/^\s*(\d+)\s+(\d+)\s+(\d+)\s*$/m) const states = text.match(/Reading:\s*(\d+)\s+Writing:\s*(\d+)\s+Waiting:\s*(\d+)/i) if (!active || !counters || !states) return null return { active: Number(active[1]), accepts: Number(counters[1]), handled: Number(counters[2]), requests: Number(counters[3]), reading: Number(states[1]), writing: Number(states[2]), waiting: Number(states[3]), } } 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 NginxCollector extends CollectorPlugin { constructor(opts = {}) { super({ name: 'nginx', intervalMs: opts.intervalMs }) this.url = opts.url || process.env.PEARDATA_NGINX_URL || 'http://127.0.0.1/nginx_status' /** @type {{ accepts: number, handled: number, requests: number, wallMs: number }|null} */ this._prev = null } isEnabled() { return isNginxEnabled() } start() { if (!this.isEnabled()) return registerChart(CHART_CONNECTIONS) registerChart(CHART_REQUESTS) log.info('Nginx collector started', { url: this.url }) super.start() } async collect() { const body = await fetchText(this.url) const parsed = parseNginxStubStatus(body) if (!parsed) throw new Error('unrecognized stub_status body') const ts = Date.now() let acceptsRate = 0 let handledRate = 0 let requestsRate = 0 if (this._prev && ts > this._prev.wallMs) { const dt = (ts - this._prev.wallMs) / 1000 if (dt > 0) { acceptsRate = Math.max(0, (parsed.accepts - this._prev.accepts) / dt) handledRate = Math.max(0, (parsed.handled - this._prev.handled) / dt) requestsRate = Math.max(0, (parsed.requests - this._prev.requests) / dt) } } this._prev = { accepts: parsed.accepts, handled: parsed.handled, requests: parsed.requests, wallMs: ts, } return [ { chart: 'nginx.connections', context: 'nginx.connections', ts, values: { active: parsed.active, reading: parsed.reading, writing: parsed.writing, waiting: parsed.waiting, }, }, { chart: 'nginx.requests', context: 'nginx.requests', ts, values: { accepts: acceptsRate, handled: handledRate, requests: requestsRate, }, }, ] } } /** @type {NginxCollector|null} */ let singleton = null export function getNginxCollector() { if (!singleton) singleton = new NginxCollector() return singleton }