Files
peardata/server/services/collectors/sockets.js
T
Raven Scott a639b3c953
CI / test (push) Successful in 1m15s
Release rolling / release (push) Successful in 7m6s
Updates
2026-07-18 19:49:57 -04:00

156 lines
3.8 KiB
JavaScript

/**
* TCP/UDP socket state timeseries from /proc/net/tcp{,6} and udp.
* Always-on on Linux (cheap enough); disable with PEARDATA_SOCKETS=0.
*/
import fs from 'fs'
import os from 'os'
import { EventEmitter } from 'events'
import { SAMPLE_INTERVAL_MS, registerChart } from '../../../shared/metrics.js'
import logger from '../../utils/logger.js'
const log = logger.child('sockets')
const TCP_STATES = {
'01': 'established',
'02': 'syn_sent',
'03': 'syn_recv',
'04': 'fin_wait1',
'05': 'fin_wait2',
'06': 'time_wait',
'07': 'close',
'08': 'close_wait',
'09': 'last_ack',
'0A': 'listen',
'0B': 'closing',
}
export function isSocketsEnabled() {
const v = process.env.PEARDATA_SOCKETS
if (v === '0' || v === 'off' || v === 'false') return false
if (v === '1' || v === 'on' || v === 'true') return true
return os.platform() === 'linux'
}
function countTcp(file) {
/** @type {Record<string, number>} */
const counts = {
established: 0,
listen: 0,
time_wait: 0,
close_wait: 0,
syn_sent: 0,
syn_recv: 0,
other: 0,
}
let raw
try {
raw = fs.readFileSync(file, 'utf8')
} catch {
return counts
}
for (const line of raw.split('\n').slice(1)) {
const parts = line.trim().split(/\s+/)
if (parts.length < 4) continue
const st = parts[3]
const name = TCP_STATES[st]
if (name && name in counts) counts[name]++
else counts.other++
}
return counts
}
function countUdp(file) {
try {
return Math.max(0, fs.readFileSync(file, 'utf8').trim().split('\n').length - 1)
} catch {
return 0
}
}
export class SocketsCollector extends EventEmitter {
constructor(opts = {}) {
super()
this.intervalMs =
opts.intervalMs ?? (Number(process.env.PEARDATA_SAMPLE_MS) || SAMPLE_INTERVAL_MS)
this.timer = null
this.running = false
}
start() {
if (this.running) return
this.running = true
registerChart({
id: 'net.socket_states',
name: 'net.socket_states',
context: 'net.socket_states',
title: 'TCP socket states',
units: 'sockets',
family: 'sockets',
chartType: 'stacked',
priority: 775,
plugin: 'sockets',
dimensions: [
'established',
'listen',
'time_wait',
'close_wait',
'syn_sent',
'syn_recv',
'other',
].map((id) => ({ id, name: id, algorithm: 'absolute' })),
})
registerChart({
id: 'net.udp_sockets',
name: 'net.udp_sockets',
context: 'net.udp_sockets',
title: 'UDP sockets',
units: 'sockets',
family: 'sockets',
chartType: 'line',
priority: 776,
plugin: 'sockets',
dimensions: [
{ id: 'udp', name: 'udp', algorithm: 'absolute' },
{ id: 'udp6', name: 'udp6', algorithm: 'absolute' },
],
})
this._tick()
this.timer = setInterval(() => this._tick(), this.intervalMs)
if (this.timer.unref) this.timer.unref()
log.info('Sockets collector started')
}
stop() {
this.running = false
if (this.timer) clearInterval(this.timer)
this.timer = null
}
_tick() {
const ts = Date.now()
const v4 = countTcp('/proc/net/tcp')
const v6 = countTcp('/proc/net/tcp6')
/** @type {Record<string, number>} */
const merged = {}
for (const k of Object.keys(v4)) merged[k] = (v4[k] || 0) + (v6[k] || 0)
this.emit('samples', [
{ chart: 'net.socket_states', context: 'net.socket_states', ts, values: merged },
{
chart: 'net.udp_sockets',
context: 'net.udp_sockets',
ts,
values: {
udp: countUdp('/proc/net/udp'),
udp6: countUdp('/proc/net/udp6'),
},
},
])
}
}
let singleton = null
export function getSocketsCollector() {
if (!singleton) singleton = new SocketsCollector()
return singleton
}