Skip to content

Commit

Permalink
feat: added smart-yard prometheus exporter (rosteleset#80)
Browse files Browse the repository at this point in the history
  • Loading branch information
kulakoff committed Nov 22, 2024
1 parent 971eebc commit 1ba7407
Show file tree
Hide file tree
Showing 21 changed files with 520 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ client/config/config.yml
!server/services/mqtt/package.json
!server/services/intercom_provision/package.json
!server/services/event/package.json
!server/services/sys_exporter/package.json

server/lib/PHPMailer/
server/lib/htmlpurifier/
Expand Down Expand Up @@ -82,6 +83,7 @@ server/services/push/assets/certificate-and-privatekey.pem
server/services/push/.env
server/services/push/.env_development
server/services/event/config.json
server/services/sys_exporter/.env
server/mobile/custom/*.php
server/mobile/address/custom/*.php
server/mobile/call/custom/*.php
Expand Down
7 changes: 7 additions & 0 deletions server/services/sys_exporter/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
docker
drafts
node_modules
docker-compose.yml
.example_env
.env
package-lock.json
7 changes: 7 additions & 0 deletions server/services/sys_exporter/.env_example
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
APP_NAME=SmartYard-Server/intercom
APP_PORT=9191
APP_HOST=127.0.0.1
SERVICE_PREFIX=sys_intercom
AUTH_ENABLED=false
AUTH_USER=username
AUTH_PASS=secure_password
17 changes: 17 additions & 0 deletions server/services/sys_exporter/app/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import 'dotenv/config'
export const NODE_ENV= process.env.NODE_ENV || "production";

export const APP_NAME = process.env.APP_NAME
export const APP_PORT = process.env.APP_PORT || 9191;
//TODO: refactor host usage
export const APP_HOST = process.env.APP_HOST || "localhost";
export const SERVICE_PREFIX = process.env.SERVICE_PREFIX || 'sys_intercom';
export const AUTH_ENABLED = process.env.AUTH_ENABLED || false;
export const AUTH_USER = process.env.AUTH_USER;
export const AUTH_PASS = process.env.AUTH_PASS;

// Intercom models
export const BEWARD_DKS = 'BEWARD DKS'
export const BEWARD_DS = 'BEWARD DS'
export const QTECH = 'QTECH'
export const AKUVOX = 'AKUVOX'
12 changes: 12 additions & 0 deletions server/services/sys_exporter/app/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import 'dotenv/config'
import express from 'express';
import { NODE_ENV, APP_HOST, APP_PORT } from './constants.js'
import { showTitle } from "./utils/showTitle.js";
import routes from "./routes/routes.js";

const app = express();
app.use("/", routes)
app.listen(APP_PORT, () => {
NODE_ENV !== "production" && showTitle();
console.log(`Exporter server is running on http://${APP_HOST}:${APP_PORT}`);
});
80 changes: 80 additions & 0 deletions server/services/sys_exporter/app/metrics/devices/akuvox.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import DigestFetch from "digest-fetch";

/**
* get Akuvox intercom metrics
* @param url
* @param username
* @param password
* @returns {Promise<{sipStatus: (number), uptimeSeconds: *}>}
*/
export const getAkuvoxMetrics = async (url, username, password) => {
console.log(`${new Date().toLocaleString("RU")} | getBewardMetrics: ${url}`);
const digestClient = new DigestFetch(username, password);
const BASE_URL = url + '/api';
const statusPayload = {
target: 'system',
action: 'status'
};
const infoPayload = {
target: 'system',
action: 'info'
};

class DigestClient {
constructor(client, baseUrl) {
this.client = client;
this.baseUrl = baseUrl;
}

async post(endpoint, payload, timeout = 5000) {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
reject(new Error(`Request timeout: ${timeout} ms`));
}, timeout);

this.client.fetch(this.baseUrl + endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
})
.then(response => {
clearTimeout(timer);
if (!response.ok) {
reject(new Error(`HTTP error! status: ${response.status}`));
} else {
resolve(response.json());
}
})
.catch(err => {
clearTimeout(timer);
reject(err);
});
});
}
}

const instance = new DigestClient(digestClient, BASE_URL);

try {
const [statusResponse, infoResponse] = await Promise.all([
instance.post('', statusPayload).then(({data}) => data),
instance.post('', infoPayload).then(({data}) => data)
]);

const parseUptime = (data) => {
return data.UpTime ?? 0;
};

const parseSipStatus = (data) => {
return data.Account1.Status === "2" ? 1 : 0;
};

const sipStatus = parseSipStatus(infoResponse);
const uptimeSeconds = parseUptime(statusResponse);

return { sipStatus, uptimeSeconds };
} catch (err) {
console.error(`${new Date().toLocaleString("RU")} | Error fetching metrics from device ${url}: ${err.message}`);
throw new Error('Failed to fetch metrics from intercom');
}
};
60 changes: 60 additions & 0 deletions server/services/sys_exporter/app/metrics/devices/beward.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import axios from "axios";

export const getBewardMetrics = async (url, username = 'admin', password) => {
console.log(`${new Date().toLocaleString("RU")} | getBewardMetrics: ${url}`);
const BASE_URL = url + '/cgi-bin';
const PATH_SIP_STATUS = '/sip_cgi?action=regstatus&AccountReg';
const PATH_SYSINFO = '/systeminfo_cgi?action=get';

const instance = axios.create({
baseURL: BASE_URL,
timeout: 1000,
auth: {
username: username,
password: password
}
});

/**
* Extract value of AccountReg1
* @param data
* @returns {number|number}
*/
const parseSipStatus = (data) => {
const match = data.match(/AccountReg1=(\d+)/);
return match ? parseInt(match[1], 10) : 0;
};

/**
* Extract value of UpTime and convert to seconds
* @param data
* @returns {number}
* @example "UpTime=20:22:31", "UpTime=11.18:44:44"
*/
const parseUptimeMatch = (data) => {
const match = data.match(/UpTime=(?<days>\d+\.)?(?<hours>\d+\:)(?<minutes>\d+\:)(?<seconds>\d+)/);
if (!match || !match.groups) {
return 0;
}
const { days = 0, hours, minutes, seconds } = match.groups;
return (parseInt(days, 10) * 24 * 3600)
+ (parseInt(hours, 10) * 3600)
+ (parseInt(minutes, 10) * 60)
+ parseInt(seconds, 10);
}

try {
const [sipStatusData, sysInfoData] = await Promise.all([
instance.get(PATH_SIP_STATUS).then(({data}) => data),
instance.get(PATH_SYSINFO).then(({data}) => data)
]);

const sipStatus = parseSipStatus(sipStatusData);
const uptimeSeconds = parseUptimeMatch(sysInfoData);

return { sipStatus, uptimeSeconds };
} catch (err){
console.error(`${new Date().toLocaleString("RU")} | Error fetching metrics from device ${url}: ${err.message}`);
throw new Error('Failed to fetch metrics from intercom');
}
}
67 changes: 67 additions & 0 deletions server/services/sys_exporter/app/metrics/devices/qtech.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import axios from "axios";

export const getQtechMetrics = async (url, username, password) => {
console.log(`${new Date().toLocaleString("RU")} | getQtechMetrics: ${url}`);
const BASE_URL = url + '/api';
const uptimePayload = {
"target": "firmware",
"action": "status",
"data": {},
}
const sipStatusPayload = {
"target": "sip",
"action": "get",
"data": {
"AccountId": 0
},
}
const instance = axios.create({
baseURL: BASE_URL,
timeout: 1000,
auth: {
username: username,
password: password
}
});
const parseSipStatus = (data) => {
return data?.SipAccount?.AccountStatus === "2" ? 1 : 0
}
const parseUptime = (data) => {
if (!data.UpTime){
return 0
}

const upTime = data.UpTime
// Регулярное выражение для поиска дней
const dayMatch = upTime.match(/day:(\d+)/);

// Регулярное выражение для поиска часов, минут и секунд, игнорируя лишние пробелы
const timeMatch = upTime.match(/(\d+):\s*(\d+):\s*(\d+)/);

if (dayMatch && timeMatch) {
const days = parseInt(dayMatch[1], 10);
const hours = parseInt(timeMatch[1].trim(), 10);
const minutes = parseInt(timeMatch[2].trim(), 10);
const seconds = parseInt(timeMatch[3].trim(), 10);

return days * 86400 + hours * 3600 + minutes * 60 + seconds;
} else {
throw new Error(`Invalid UpTime format: ${upTime}`);
}
}

try {
const [ sysInfoData, sipStatusData, ] = await Promise.all([
instance.post('', JSON.stringify(uptimePayload)).then((res) => res.data.data),
instance.post('', JSON.stringify(sipStatusPayload)).then((res) => res.data.data)
])

const sipStatus = parseSipStatus(sipStatusData);
const uptimeSeconds = parseUptime(sysInfoData);

return { sipStatus, uptimeSeconds };
} catch (err){
console.error(`${new Date().toLocaleString("RU")} | Error fetching metrics from device ${url}: ${err.message}`);
throw new Error('Failed to fetch metrics from intercom');
}
}
4 changes: 4 additions & 0 deletions server/services/sys_exporter/app/metrics/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// device specific metrics
export { getBewardMetrics } from './devices/beward.js'
export { getQtechMetrics } from './devices/qtech.js'
export { getAkuvoxMetrics } from './devices/akuvox.js'
31 changes: 31 additions & 0 deletions server/services/sys_exporter/app/metrics/metricsFactory.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Gauge } from 'prom-client';
import { SERVICE_PREFIX } from '../constants.js'

export const createMetrics = (registers, isGlobal = false) => {
const sipStatusGauge = new Gauge({
name: `${SERVICE_PREFIX}_sip_status`,
help: 'SIP status of the intercom. 0 = offline; 1 = online',
labelNames: ['url'],
registers: registers,
});

const uptimeGauge = new Gauge({
name: `${SERVICE_PREFIX}_uptime_seconds`,
help: 'Uptime of the intercom in seconds',
labelNames: ['url'],
registers: registers,
});

const metrics = { sipStatusGauge, uptimeGauge };

if (!isGlobal) {
metrics.probeSuccess = new Gauge({
name: 'probe_success',
help: 'Displays whether or not the probe was a success',
labelNames: ['url'],
registers: registers,
});
}

return metrics;
};
16 changes: 16 additions & 0 deletions server/services/sys_exporter/app/metrics/registry.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Registry } from 'prom-client';
import { APP_NAME } from "../constants.js";
import { createMetrics } from "./metricsFactory.js";

// Create a global registry for all metrics
export const globalRegistry = new Registry();

export const {
sipStatusGauge: globalSipStatusGauge,
uptimeGauge: globalUptimeGauge,
} = createMetrics([globalRegistry], true);

// Set default metrics
globalRegistry.setDefaultLabels({
app: APP_NAME
});
16 changes: 16 additions & 0 deletions server/services/sys_exporter/app/middleware/auth.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import basicAuth from "express-basic-auth";
import { AUTH_PASS, AUTH_USER } from "../constants.js";

const logUnauthorized = (req) => {
console.log(`Failed auth: ${req.ip}`)
}
const basicAuthMiddleware =
basicAuth({
users: {[AUTH_USER]: AUTH_PASS},
challenge: true,
unauthorizedResponse: (req) => {
logUnauthorized(req);
return 'Failed auth';
}
})
export default basicAuthMiddleware
11 changes: 11 additions & 0 deletions server/services/sys_exporter/app/routes/metrics.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import express from "express";
import { globalRegistry } from "../metrics/registry.js";

const router = express.Router();

router.get("/", async (req, res) => {
res.set('Content-Type', globalRegistry.contentType);
res.end(await globalRegistry.metrics());
})

export default router;
Loading

0 comments on commit 1ba7407

Please sign in to comment.