Files
peardata/server/services/export.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

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