updates
This commit is contained in:
@@ -0,0 +1,227 @@
|
||||
/**
|
||||
* Redis INFO collector (service plugin).
|
||||
*
|
||||
* Enable: PEARDATA_REDIS=1
|
||||
* Addr: PEARDATA_REDIS_URL=redis://127.0.0.1:6379 (or host:port)
|
||||
*
|
||||
* Charts: redis.memory, redis.clients, redis.stats
|
||||
*/
|
||||
import net from 'net'
|
||||
import { CollectorPlugin } from './plugin.js'
|
||||
import { registerChart } from '../../../shared/metrics.js'
|
||||
import logger from '../../utils/logger.js'
|
||||
|
||||
const log = logger.child('redis')
|
||||
|
||||
const CHART_MEMORY = {
|
||||
id: 'redis.memory',
|
||||
name: 'redis.memory',
|
||||
context: 'redis.memory',
|
||||
title: 'Redis memory',
|
||||
units: 'MiB',
|
||||
family: 'redis',
|
||||
chartType: 'area',
|
||||
priority: 8100,
|
||||
plugin: 'redis',
|
||||
dimensions: [
|
||||
{ id: 'used', name: 'used', algorithm: 'absolute' },
|
||||
{ id: 'peak', name: 'peak', algorithm: 'absolute' },
|
||||
{ id: 'rss', name: 'rss', algorithm: 'absolute' },
|
||||
],
|
||||
}
|
||||
|
||||
const CHART_CLIENTS = {
|
||||
id: 'redis.clients',
|
||||
name: 'redis.clients',
|
||||
context: 'redis.clients',
|
||||
title: 'Redis clients',
|
||||
units: 'clients',
|
||||
family: 'redis',
|
||||
chartType: 'line',
|
||||
priority: 8110,
|
||||
plugin: 'redis',
|
||||
dimensions: [
|
||||
{ id: 'connected', name: 'connected', algorithm: 'absolute' },
|
||||
{ id: 'blocked', name: 'blocked', algorithm: 'absolute' },
|
||||
],
|
||||
}
|
||||
|
||||
const CHART_STATS = {
|
||||
id: 'redis.stats',
|
||||
name: 'redis.stats',
|
||||
context: 'redis.stats',
|
||||
title: 'Redis ops',
|
||||
units: 'ops/s',
|
||||
family: 'redis',
|
||||
chartType: 'line',
|
||||
priority: 8120,
|
||||
plugin: 'redis',
|
||||
dimensions: [
|
||||
{ id: 'ops', name: 'ops', algorithm: 'absolute' },
|
||||
{ id: 'hit_rate', name: 'hit_rate', algorithm: 'absolute' },
|
||||
],
|
||||
}
|
||||
|
||||
export function isRedisEnabled() {
|
||||
const v = process.env.PEARDATA_REDIS
|
||||
return v === '1' || v === 'on' || v === 'true'
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} urlOrHost
|
||||
* @returns {{ host: string, port: number }}
|
||||
*/
|
||||
export function parseRedisAddr(urlOrHost) {
|
||||
const raw = String(urlOrHost || '127.0.0.1:6379').trim()
|
||||
if (raw.includes('://')) {
|
||||
try {
|
||||
const u = new URL(raw)
|
||||
return {
|
||||
host: u.hostname || '127.0.0.1',
|
||||
port: Number(u.port) || 6379,
|
||||
}
|
||||
} catch {
|
||||
// fall through
|
||||
}
|
||||
}
|
||||
const [host, port] = raw.split(':')
|
||||
return { host: host || '127.0.0.1', port: Number(port) || 6379 }
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} body Redis INFO text
|
||||
* @returns {Record<string, string>}
|
||||
*/
|
||||
export function parseRedisInfo(body) {
|
||||
/** @type {Record<string, string>} */
|
||||
const out = {}
|
||||
for (const line of String(body || '').split(/\r?\n/)) {
|
||||
if (!line || line.startsWith('#')) continue
|
||||
const i = line.indexOf(':')
|
||||
if (i < 0) continue
|
||||
out[line.slice(0, i)] = line.slice(i + 1).trim()
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
function bytesToMiB(n) {
|
||||
return n / (1024 * 1024)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{ host: string, port: number }} addr
|
||||
* @param {number} [timeoutMs]
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
export function fetchRedisInfo(addr, timeoutMs = 3000) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const socket = net.createConnection({ host: addr.host, port: addr.port })
|
||||
let buf = ''
|
||||
let settled = false
|
||||
const done = (err, data) => {
|
||||
if (settled) return
|
||||
settled = true
|
||||
clearTimeout(timer)
|
||||
try {
|
||||
socket.destroy()
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
if (err) reject(err)
|
||||
else resolve(data)
|
||||
}
|
||||
const timer = setTimeout(() => done(new Error('redis timeout')), timeoutMs)
|
||||
socket.on('connect', () => {
|
||||
socket.write('INFO\r\n')
|
||||
})
|
||||
socket.on('data', (chunk) => {
|
||||
buf += chunk.toString('utf8')
|
||||
// RESP bulk: $<len>\r\n<body>\r\n or plain INFO dump
|
||||
if (buf.includes('redis_version:') || buf.includes('used_memory:')) {
|
||||
const idx = buf.indexOf('$')
|
||||
if (idx === 0) {
|
||||
const nl = buf.indexOf('\r\n')
|
||||
if (nl > 0) {
|
||||
const body = buf.slice(nl + 2)
|
||||
if (body.includes('redis_version:') || body.length > 200) done(null, body)
|
||||
}
|
||||
} else {
|
||||
done(null, buf)
|
||||
}
|
||||
}
|
||||
})
|
||||
socket.on('error', (err) => done(err))
|
||||
socket.on('end', () => {
|
||||
if (buf) done(null, buf)
|
||||
else done(new Error('redis closed with no data'))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export class RedisCollector extends CollectorPlugin {
|
||||
constructor(opts = {}) {
|
||||
super({ name: 'redis', intervalMs: opts.intervalMs })
|
||||
this.addr = parseRedisAddr(opts.url || process.env.PEARDATA_REDIS_URL || '127.0.0.1:6379')
|
||||
}
|
||||
|
||||
isEnabled() {
|
||||
return isRedisEnabled()
|
||||
}
|
||||
|
||||
start() {
|
||||
if (!this.isEnabled()) return
|
||||
registerChart(CHART_MEMORY)
|
||||
registerChart(CHART_CLIENTS)
|
||||
registerChart(CHART_STATS)
|
||||
log.info('Redis collector started', this.addr)
|
||||
super.start()
|
||||
}
|
||||
|
||||
async collect() {
|
||||
const raw = await fetchRedisInfo(this.addr)
|
||||
const info = parseRedisInfo(raw)
|
||||
const ts = Date.now()
|
||||
const hits = Number(info.keyspace_hits) || 0
|
||||
const misses = Number(info.keyspace_misses) || 0
|
||||
const denom = hits + misses
|
||||
const hitRate = denom > 0 ? (hits / denom) * 100 : 0
|
||||
return [
|
||||
{
|
||||
chart: 'redis.memory',
|
||||
context: 'redis.memory',
|
||||
ts,
|
||||
values: {
|
||||
used: bytesToMiB(Number(info.used_memory) || 0),
|
||||
peak: bytesToMiB(Number(info.used_memory_peak) || 0),
|
||||
rss: bytesToMiB(Number(info.used_memory_rss) || 0),
|
||||
},
|
||||
},
|
||||
{
|
||||
chart: 'redis.clients',
|
||||
context: 'redis.clients',
|
||||
ts,
|
||||
values: {
|
||||
connected: Number(info.connected_clients) || 0,
|
||||
blocked: Number(info.blocked_clients) || 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
chart: 'redis.stats',
|
||||
context: 'redis.stats',
|
||||
ts,
|
||||
values: {
|
||||
ops: Number(info.instantaneous_ops_per_sec) || 0,
|
||||
hit_rate: hitRate,
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
/** @type {RedisCollector|null} */
|
||||
let singleton = null
|
||||
|
||||
export function getRedisCollector() {
|
||||
if (!singleton) singleton = new RedisCollector()
|
||||
return singleton
|
||||
}
|
||||
Reference in New Issue
Block a user