Files
peardata/server/services/collectors/nginx.js
T
Raven Scott 1d46b7b3ad
CI / test (push) Failing after 6s
Release rolling / release (push) Has been cancelled
updates
2026-07-18 19:11:32 -04:00

180 lines
4.9 KiB
JavaScript

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