180 lines
4.9 KiB
JavaScript
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
|
|
}
|