174 lines
5.0 KiB
JavaScript
174 lines
5.0 KiB
JavaScript
/**
|
|
* Snapshot export + Prometheus text push (Pushgateway-compatible).
|
|
*
|
|
* Jobs:
|
|
* exportSnapshot — build JSON snapshot (optional write to PEARDATA_EXPORT_DIR)
|
|
* prometheusPush — POST exposition text to PEARDATA_PUSHGATEWAY_URL
|
|
*
|
|
* Env:
|
|
* PEARDATA_EXPORT_DIR — directory for snapshot-*.json
|
|
* PEARDATA_PUSHGATEWAY_URL — e.g. http://127.0.0.1:9091/metrics/job/peardata
|
|
*/
|
|
import fs from 'fs'
|
|
import path from 'path'
|
|
import http from 'http'
|
|
import https from 'https'
|
|
import os from 'os'
|
|
import b4a from 'b4a'
|
|
import { getStore } from './store.js'
|
|
import { getCollector } from './collector.js'
|
|
import { getAnomalyEngine } from './anomaly.js'
|
|
import { listAlerts } from './alerts.js'
|
|
import { getServerPublicKeyHex } from '../core/auth-keys.js'
|
|
import { APP_NAME, APP_VERSION } from '../../shared/protocol.js'
|
|
import { CHART_BY_ID } from '../../shared/metrics.js'
|
|
import logger from '../utils/logger.js'
|
|
|
|
const log = logger.child('export')
|
|
|
|
/**
|
|
* @returns {object}
|
|
*/
|
|
export function buildSnapshot() {
|
|
const store = getStore()
|
|
const collector = getCollector()
|
|
const pk = (() => {
|
|
try {
|
|
return getServerPublicKeyHex()
|
|
} catch {
|
|
return null
|
|
}
|
|
})()
|
|
return {
|
|
success: true,
|
|
app: APP_NAME,
|
|
version: APP_VERSION,
|
|
hostname: os.hostname(),
|
|
publicKeyHex: pk,
|
|
node: collector.getNodeInfo(pk, APP_VERSION),
|
|
latest: store.latestValues(),
|
|
health: getAnomalyEngine().getHealth(),
|
|
alerts: listAlerts(),
|
|
ts: Date.now(),
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Convert latest values to Prometheus exposition format.
|
|
* @param {Record<string, { ts: number, values: Record<string, number|null> }>} [latest]
|
|
*/
|
|
export function toPrometheusText(latest) {
|
|
const data = latest || getStore().latestValues()
|
|
const lines = [
|
|
`# HELP peardata_info PearData agent info`,
|
|
`# TYPE peardata_info gauge`,
|
|
`peardata_info{version="${APP_VERSION}",hostname="${os.hostname()}"} 1`,
|
|
]
|
|
for (const [chart, point] of Object.entries(data)) {
|
|
const metric = `peardata_${chart.replace(/[^a-zA-Z0-9_]/g, '_')}`
|
|
const ctx = CHART_BY_ID.get(chart)?.context || chart
|
|
for (const [dim, val] of Object.entries(point.values || {})) {
|
|
if (val == null || Number.isNaN(val)) continue
|
|
lines.push(
|
|
`${metric}{dimension="${dim}",context="${ctx}"} ${val}`
|
|
)
|
|
}
|
|
}
|
|
return lines.join('\n') + '\n'
|
|
}
|
|
|
|
/**
|
|
* @param {object} snapshot
|
|
* @returns {{ path: string|null, bytes: number }}
|
|
*/
|
|
export function writeSnapshotFile(snapshot) {
|
|
const dir = process.env.PEARDATA_EXPORT_DIR
|
|
if (!dir) return { path: null, bytes: 0 }
|
|
fs.mkdirSync(dir, { recursive: true })
|
|
const file = path.join(dir, `snapshot-${snapshot.ts || Date.now()}.json`)
|
|
const body = JSON.stringify(snapshot, null, 2)
|
|
fs.writeFileSync(file, body)
|
|
return { path: file, bytes: body.length }
|
|
}
|
|
|
|
/**
|
|
* POST Prometheus text to Pushgateway (or any text receiver).
|
|
* @param {string} url
|
|
* @param {string} body
|
|
* @param {number} [timeoutMs]
|
|
*/
|
|
export function pushPrometheusText(url, body, timeoutMs = 10_000) {
|
|
return new Promise((resolve, reject) => {
|
|
const u = new URL(url)
|
|
const mod = u.protocol === 'https:' ? https : http
|
|
const req = mod.request(
|
|
{
|
|
hostname: u.hostname,
|
|
port: u.port || (u.protocol === 'https:' ? 443 : 80),
|
|
path: u.pathname + u.search,
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'text/plain; version=0.0.4',
|
|
'Content-Length': b4a.byteLength(body),
|
|
},
|
|
timeout: timeoutMs,
|
|
},
|
|
(res) => {
|
|
let data = ''
|
|
res.on('data', (c) => {
|
|
data += c
|
|
})
|
|
res.on('end', () => {
|
|
if (res.statusCode && res.statusCode >= 400) {
|
|
reject(new Error(`push failed HTTP ${res.statusCode}: ${data.slice(0, 200)}`))
|
|
return
|
|
}
|
|
resolve({ status: res.statusCode || 200, bytes: body.length })
|
|
})
|
|
}
|
|
)
|
|
req.on('timeout', () => {
|
|
req.destroy()
|
|
reject(new Error('push timeout'))
|
|
})
|
|
req.on('error', reject)
|
|
req.write(body)
|
|
req.end()
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Job handler: build + optional file write.
|
|
*/
|
|
export async function jobExportSnapshot() {
|
|
const snapshot = buildSnapshot()
|
|
const written = writeSnapshotFile(snapshot)
|
|
if (written.path) log.info('Wrote snapshot', written)
|
|
return {
|
|
ok: true,
|
|
ts: snapshot.ts,
|
|
charts: Object.keys(snapshot.latest || {}).length,
|
|
file: written.path,
|
|
bytes: written.bytes,
|
|
snapshot: written.path ? undefined : snapshot,
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Job handler: push Prometheus text to gateway.
|
|
* @param {{ url?: string }} [args]
|
|
*/
|
|
export async function jobPrometheusPush(args = {}) {
|
|
const url = args.url || process.env.PEARDATA_PUSHGATEWAY_URL
|
|
if (!url) {
|
|
return {
|
|
ok: false,
|
|
error: 'PEARDATA_PUSHGATEWAY_URL (or args.url) required',
|
|
}
|
|
}
|
|
const body = toPrometheusText()
|
|
const res = await pushPrometheusText(url, body)
|
|
log.info('Prometheus push ok', { url, ...res })
|
|
return { ok: true, ...res, url, lines: body.split('\n').length }
|
|
}
|