Prometheus exporter for Mikrotik routers and switches
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

206 lines
7.5 KiB

#!/usr/bin/env python
import asyncio
import os
from aio_api_ros.connection import ApiRosConnection
from sanic import Sanic, exceptions
app = Sanic("exporter")
pool = {}
PREFIX = os.getenv("PROMETHEUS_PREFIX", "mikrotik_")
PROMETHEUS_BEARER_TOKEN = os.getenv("PROMETHEUS_BEARER_TOKEN")
MIKROTIK_USER = os.getenv("MIKROTIK_USER")
MIKROTIK_PASSWORD = os.getenv("MIKROTIK_PASSWORD")
if not MIKROTIK_USER:
raise ValueError("MIKROTIK_USER not specified")
if not MIKROTIK_PASSWORD:
raise ValueError("MIKROTIK_PASSWORD not specified")
if not PROMETHEUS_BEARER_TOKEN:
raise ValueError("No PROMETHEUS_BEARER_TOKEN specified")
RATE_MAPPING = {
"40Gbps": 40 * 10 ** 9,
"10Gbps": 10 * 10 ** 9,
"1Gbps": 10 ** 9,
"100Mbps": 100 * 10 ** 6,
"10Mbps": 10 * 10 ** 6,
}
async def wrap(i):
metrics_seen = set()
async for name, tp, value, labels in i:
if name not in metrics_seen:
yield "# TYPE %s %s" % (PREFIX + name, tp)
metrics_seen.add(name)
yield "%s%s %s" % (
PREFIX + name,
("{%s}" % ",".join(["%s=\"%s\"" % j for j in labels.items()]) if labels else ""),
value)
async def scrape_mikrotik(mk):
async for obj in mk.query("/interface/print"):
labels = {"port": obj["name"], "type": obj.get("type", "null")}
yield "interface_rx_bytes", "counter", obj["rx-byte"], labels
yield "interface_tx_bytes", "counter", obj["tx-byte"], labels
yield "interface_rx_packets", "counter", obj["rx-packet"], labels
yield "interface_tx_packets", "counter", obj["tx-packet"], labels
try:
yield "interface_rx_errors", "counter", obj["rx-error"], labels
yield "interface_tx_errors", "counter", obj["tx-error"], labels
except KeyError:
pass
try:
yield "interface_rx_drops", "counter", obj["rx-drop"], labels
yield "interface_tx_drops", "counter", obj["tx-drop"], labels
except KeyError:
pass
yield "interface_running", "gauge", int(obj["running"]), labels
yield "interface_actual_mtu", "gauge", obj["actual-mtu"], labels
port_count = 0
res = mk.query("/interface/ethernet/print")
async for obj in res:
port_count += 1
ports = ",".join([str(j) for j in range(1, port_count)])
async for obj in mk.query("/interface/ethernet/monitor", "=once=", "=numbers=%s" % ports):
labels = {"port": obj["name"]}
try:
rate = obj["rate"]
except KeyError:
pass
else:
yield "interface_rate", "gauge", RATE_MAPPING[rate], labels
try:
labels["sfp_vendor_name"] = obj["sfp-vendor-name"]
except KeyError:
pass
try:
labels["sfp_vendor_part_number"] = obj["sfp-vendor-part-number"]
except KeyError:
pass
try:
yield "interface_sfp_temperature", "gauge", obj["sfp-temperature"], labels
yield "interface_sfp_tx_power", "gauge", obj["sfp-tx-power"], labels
yield "interface_sfp_rx_power", "gauge", obj["sfp-tx-power"], labels
except KeyError:
pass
labels["status"] = obj["status"]
try:
labels["sfp_module_present"] = int(obj["sfp-module-present"])
except KeyError:
pass
yield "interface_status", "gauge", 1, labels
poe_ports = set()
res = mk.query("/interface/ethernet/poe/print", optional=True)
async for obj in res:
poe_ports.add(int(obj[".id"][1:], 16) - 1)
if poe_ports:
res = mk.query("/interface/ethernet/poe/monitor", "=once=", "=numbers=%s" % ",".join([str(j) for j in poe_ports]))
async for obj in res:
labels = {"port": obj["name"]}
try:
yield "poe_out_voltage", "gauge", float(obj["poe-out-voltage"]), labels
yield "poe_out_current", "gauge", int(obj["poe-out-current"]) / 1000.0, labels
except KeyError:
pass
labels["status"] = obj["poe-out-status"]
yield "poe_out_status", "gauge", 1, labels
async for obj in mk.query("/system/resource/print"):
labels = {}
yield "system_write_sect_total", "counter", obj["write-sect-total"], labels
yield "system_free_memory", "gauge", obj["free-memory"], labels
try:
yield "system_bad_blocks", "counter", obj["bad-blocks"], labels
except KeyError:
pass
for key in ("version", "cpu", "cpu-count", "board-name", "architecture-name"):
labels[key.replace("-", "_")] = obj[key]
yield "system_version", "gauge", 1, labels
async for obj in mk.query("/system/health/print"):
# Normalize ROS6 vs ROS7 difference
if "name" in obj:
key = obj["name"]
value = obj["value"]
obj = {}
obj[key] = value
for key, value in obj.items():
if key.startswith("board-temperature"):
yield "system_health_temperature_celsius", "gauge", \
float(value), {"component": "board%s" % key[17:]}
elif key.endswith("temperature"):
yield "system_health_temperature_celsius", "gauge", \
float(value), {"component": key[:-12] or "system"}
elif key.startswith("fan") and key.endswith("-speed"):
yield "system_health_fan_speed_rpm", "gauge", \
float(value), {"component": key[:-6]}
elif key.startswith("psu") and key.endswith("-state"):
yield "system_health_power_supply_state", "gauge", \
1, {"state": value}
elif key.startswith("psu") and key.endswith("-voltage"):
yield "system_health_power_supply_voltage", "gauge", \
float(value), {"component": key[:-8]}
elif key.startswith("psu") and key.endswith("-current"):
yield "system_health_power_supply_current", "gauge", \
float(value), {"component": key[:-8]}
elif key == "power-consumption":
# Can be calculated from voltage*current
pass
elif key == "state" or key == "state-after-reboot":
# Seems disabled on x86
pass
elif key == "poe-out-consumption":
pass
else:
raise NotImplementedError("Don't know how to handle system health record %s" % repr(key))
@app.route("/metrics", stream=True)
async def view_export(request):
if request.token != PROMETHEUS_BEARER_TOKEN:
raise exceptions.Forbidden("Invalid bearer token")
target = request.args.get("target")
if not target:
raise exceptions.InvalidUsage("Invalid or no target specified")
if ":" in target:
target, port = target.split(":")
port = int(port)
else:
port = 8728
response = await request.respond(content_type="text/plain")
if target not in pool:
mk = ApiRosConnection(
mk_ip=target,
mk_port=port,
mk_user=MIKROTIK_USER,
mk_psw=MIKROTIK_PASSWORD,
)
await mk.connect()
await mk.login()
pool[target] = mk, asyncio.Lock()
mk, lock = pool[target]
async with lock:
try:
async for line in wrap(scrape_mikrotik(mk)):
await response.send(line + "\n")
except RuntimeError:
# Handle TCPTransport closed exception
pool.pop(target)
app.run(host="0.0.0.0", port=3001)