mirror of https://github.com/nicolargo/glances.git
First version ok. Log message should be removed. Code should be tested.
This commit is contained in:
parent
ba932e72bd
commit
b7c6cce373
|
|
@ -242,7 +242,7 @@ Glances can export stats to:
|
||||||
|
|
||||||
- files: ``CSV`` and ``JSON``
|
- files: ``CSV`` and ``JSON``
|
||||||
- databases: ``InfluxDB``, ``ElasticSearch``, ``PostgreSQL/TimeScale``, ``Cassandra``, ``CouchDB``, ``OpenTSDB``, ``Prometheus``, ``StatsD``, ``Riemann`` and ``Graphite``
|
- databases: ``InfluxDB``, ``ElasticSearch``, ``PostgreSQL/TimeScale``, ``Cassandra``, ``CouchDB``, ``OpenTSDB``, ``Prometheus``, ``StatsD``, ``Riemann`` and ``Graphite``
|
||||||
- brokers: ``RabbitMQ/ActiveMQ``, ``ZeroMQ`` and ``Kafka``
|
- brokers: ``RabbitMQ/ActiveMQ``, ``NATS``, ``ZeroMQ`` and ``Kafka``
|
||||||
- others: ``RESTful`` endpoint
|
- others: ``RESTful`` endpoint
|
||||||
|
|
||||||
Installation 🚀
|
Installation 🚀
|
||||||
|
|
@ -574,6 +574,7 @@ Extra dependencies:
|
||||||
- ``influxdb`` (for the InfluxDB version 1 export module)
|
- ``influxdb`` (for the InfluxDB version 1 export module)
|
||||||
- ``influxdb-client`` (for the InfluxDB version 2 export module)
|
- ``influxdb-client`` (for the InfluxDB version 2 export module)
|
||||||
- ``kafka-python`` (for the Kafka export module)
|
- ``kafka-python`` (for the Kafka export module)
|
||||||
|
- ``nats-py`` (for the NATS export module)
|
||||||
- ``netifaces2`` (for the IP plugin)
|
- ``netifaces2`` (for the IP plugin)
|
||||||
- ``nvidia-ml-py`` (for the GPU plugin)
|
- ``nvidia-ml-py`` (for the GPU plugin)
|
||||||
- ``pycouchdb`` (for the CouchDB export module)
|
- ``pycouchdb`` (for the CouchDB export module)
|
||||||
|
|
|
||||||
|
|
@ -890,6 +890,14 @@ password=password
|
||||||
# Most of the time, you should not overwrite this value
|
# Most of the time, you should not overwrite this value
|
||||||
#hostname=mycomputer
|
#hostname=mycomputer
|
||||||
|
|
||||||
|
[nats]
|
||||||
|
# Configuration for the --export nats option
|
||||||
|
# https://nats.io/
|
||||||
|
# Host is a separated list of NATS nodes
|
||||||
|
host=nats://localhost:4222
|
||||||
|
# Prefix for the subjects (default is 'glances')
|
||||||
|
prefix=glances
|
||||||
|
|
||||||
##############################################################################
|
##############################################################################
|
||||||
# AMPS
|
# AMPS
|
||||||
# * enable: Enable (true) or disable (false) the AMP
|
# * enable: Enable (true) or disable (false) the AMP
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ This section describes the available exporters and how to configure them:
|
||||||
kafka
|
kafka
|
||||||
mqtt
|
mqtt
|
||||||
mongodb
|
mongodb
|
||||||
|
nats
|
||||||
opentsdb
|
opentsdb
|
||||||
prometheus
|
prometheus
|
||||||
rabbitmq
|
rabbitmq
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
.. _nats:
|
||||||
|
|
||||||
|
NATS
|
||||||
|
====
|
||||||
|
|
||||||
|
NATS is a message broker.
|
||||||
|
|
||||||
|
You can export statistics to a ``NATS`` server.
|
||||||
|
|
||||||
|
The connection should be defined in the Glances configuration file as
|
||||||
|
following:
|
||||||
|
|
||||||
|
.. code-block:: ini
|
||||||
|
|
||||||
|
[nats]
|
||||||
|
host=nats://localhost:4222
|
||||||
|
prefix=glances
|
||||||
|
|
||||||
|
and run Glances with:
|
||||||
|
|
||||||
|
.. code-block:: console
|
||||||
|
|
||||||
|
$ glances --export nats
|
||||||
|
|
||||||
|
Data model
|
||||||
|
-----------
|
||||||
|
|
||||||
|
Glances stats are published as JSON messagesto the following subjects:
|
||||||
|
|
||||||
|
<prefix>.<plugin>
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
CPU stats are published to glances.cpu
|
||||||
|
|
||||||
|
So a simple Python client will subscribe to this subject with:
|
||||||
|
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
import nats
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
nc = nats.NATS()
|
||||||
|
|
||||||
|
await nc.connect(servers=["nats://localhost:4222"])
|
||||||
|
|
||||||
|
future = asyncio.Future()
|
||||||
|
|
||||||
|
async def cb(msg):
|
||||||
|
nonlocal future
|
||||||
|
future.set_result(msg)
|
||||||
|
|
||||||
|
await nc.subscribe("glances.cpu", cb=cb)
|
||||||
|
|
||||||
|
# Wait for message to come in
|
||||||
|
print("Waiting (max 30 seconds) for a message on 'glances' subject...")
|
||||||
|
msg = await asyncio.wait_for(future, 30)
|
||||||
|
print(msg.subject, msg.data)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
asyncio.run(main())
|
||||||
|
|
||||||
|
To subscribe to all Glannces stats use wildcard:
|
||||||
|
|
||||||
|
await nc.subscribe("glances.*", cb=cb)
|
||||||
|
|
||||||
|
|
@ -0,0 +1,221 @@
|
||||||
|
"""NATS interface class."""
|
||||||
|
import asyncio
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
|
||||||
|
from nats.aio.client import Client as NATS
|
||||||
|
from nats.errors import ConnectionClosedError
|
||||||
|
from nats.errors import TimeoutError as NatsTimeoutError
|
||||||
|
|
||||||
|
from glances.exports.export import GlancesExport
|
||||||
|
from glances.globals import json_dumps
|
||||||
|
from glances.logger import logger
|
||||||
|
|
||||||
|
|
||||||
|
class Export(GlancesExport):
|
||||||
|
"""This class manages the NATS export module."""
|
||||||
|
|
||||||
|
def __init__(self, config=None, args=None):
|
||||||
|
"""Init the NATS export IF."""
|
||||||
|
super().__init__(config=config, args=args)
|
||||||
|
|
||||||
|
# Load the NATS configuration file
|
||||||
|
self.export_enable = self.load_conf(
|
||||||
|
'nats',
|
||||||
|
mandatories=['host'],
|
||||||
|
options=['prefix'],
|
||||||
|
)
|
||||||
|
if not self.export_enable:
|
||||||
|
exit('Missing NATS config')
|
||||||
|
|
||||||
|
self.prefix = self.prefix or 'glances'
|
||||||
|
self.hosts = self.host
|
||||||
|
|
||||||
|
logger.info(f"Initializing NATS export with prefix '{self.prefix}' to {self.hosts}")
|
||||||
|
|
||||||
|
# Create a persistent event loop in a background thread
|
||||||
|
self.loop = None
|
||||||
|
self.client = None
|
||||||
|
self._connected = False
|
||||||
|
self._shutdown = False
|
||||||
|
self._loop_ready = threading.Event()
|
||||||
|
self._loop_exception = None
|
||||||
|
self._publish_count = 0
|
||||||
|
|
||||||
|
self._loop_thread = threading.Thread(target=self._run_event_loop, daemon=True)
|
||||||
|
self._loop_thread.start()
|
||||||
|
|
||||||
|
# Wait for the loop to be ready
|
||||||
|
if not self._loop_ready.wait(timeout=10):
|
||||||
|
exit("NATS event loop failed to start within timeout")
|
||||||
|
|
||||||
|
if self._loop_exception:
|
||||||
|
exit(f"NATS event loop creation failed: {self._loop_exception}")
|
||||||
|
|
||||||
|
if self.loop is None:
|
||||||
|
exit("NATS event loop is None after initialization")
|
||||||
|
|
||||||
|
# Initial connection attempt
|
||||||
|
future = asyncio.run_coroutine_threadsafe(self._connect(), self.loop)
|
||||||
|
try:
|
||||||
|
future.result(timeout=10)
|
||||||
|
logger.info("NATS export initialized successfully")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Initial NATS connection failed: {e}. Will retry in background.")
|
||||||
|
|
||||||
|
def _run_event_loop(self):
|
||||||
|
"""Run event loop in background thread."""
|
||||||
|
try:
|
||||||
|
self.loop = asyncio.new_event_loop()
|
||||||
|
asyncio.set_event_loop(self.loop)
|
||||||
|
self._loop_ready.set()
|
||||||
|
logger.info("NATS event loop started")
|
||||||
|
self.loop.run_forever()
|
||||||
|
except Exception as e:
|
||||||
|
self._loop_exception = e
|
||||||
|
self._loop_ready.set()
|
||||||
|
logger.error(f"Event loop thread error: {e}")
|
||||||
|
finally:
|
||||||
|
# Clean up any pending tasks
|
||||||
|
pending = asyncio.all_tasks(self.loop)
|
||||||
|
for task in pending:
|
||||||
|
task.cancel()
|
||||||
|
if pending:
|
||||||
|
self.loop.run_until_complete(asyncio.gather(*pending, return_exceptions=True))
|
||||||
|
self.loop.close()
|
||||||
|
logger.info("NATS event loop stopped")
|
||||||
|
|
||||||
|
async def _connect(self):
|
||||||
|
"""Connect to NATS with error handling."""
|
||||||
|
try:
|
||||||
|
if self.client:
|
||||||
|
try:
|
||||||
|
await self.client.close()
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"Error closing existing NATS client: {e}")
|
||||||
|
|
||||||
|
self.client = NATS()
|
||||||
|
|
||||||
|
logger.info(f"Connecting to NATS servers: {self.hosts}")
|
||||||
|
|
||||||
|
# Configure with reconnection callbacks
|
||||||
|
await self.client.connect(
|
||||||
|
servers=[s.strip() for s in self.hosts.split(',')],
|
||||||
|
reconnect_time_wait=2,
|
||||||
|
max_reconnect_attempts=60,
|
||||||
|
error_cb=self._error_callback,
|
||||||
|
disconnected_cb=self._disconnected_callback,
|
||||||
|
reconnected_cb=self._reconnected_callback,
|
||||||
|
)
|
||||||
|
|
||||||
|
self._connected = True
|
||||||
|
logger.info(f"Successfully connected to NATS: {self.hosts}")
|
||||||
|
except Exception as e:
|
||||||
|
self._connected = False
|
||||||
|
logger.error(f"NATS connection error: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
async def _error_callback(self, e):
|
||||||
|
"""Called when NATS client encounters an error."""
|
||||||
|
logger.error(f"NATS error callback: {e}")
|
||||||
|
|
||||||
|
async def _disconnected_callback(self):
|
||||||
|
"""Called when disconnected from NATS."""
|
||||||
|
self._connected = False
|
||||||
|
logger.warning("NATS disconnected callback")
|
||||||
|
|
||||||
|
async def _reconnected_callback(self):
|
||||||
|
"""Called when reconnected to NATS."""
|
||||||
|
self._connected = True
|
||||||
|
logger.info("NATS reconnected callback")
|
||||||
|
|
||||||
|
def exit(self):
|
||||||
|
"""Close the NATS connection."""
|
||||||
|
super().exit()
|
||||||
|
self._shutdown = True
|
||||||
|
logger.info("NATS export shutting down")
|
||||||
|
|
||||||
|
if self.loop and self.client:
|
||||||
|
future = asyncio.run_coroutine_threadsafe(self._disconnect(), self.loop)
|
||||||
|
try:
|
||||||
|
future.result(timeout=5)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error disconnecting from NATS: {e}")
|
||||||
|
|
||||||
|
if self.loop:
|
||||||
|
self.loop.call_soon_threadsafe(self.loop.stop)
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
logger.info(f"NATS export shutdown complete. Total messages published: {self._publish_count}")
|
||||||
|
|
||||||
|
async def _disconnect(self):
|
||||||
|
"""Disconnect from NATS."""
|
||||||
|
try:
|
||||||
|
if self.client and self._connected:
|
||||||
|
await self.client.drain()
|
||||||
|
await self.client.close()
|
||||||
|
self._connected = False
|
||||||
|
logger.info("NATS disconnected cleanly")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in disconnect: {e}")
|
||||||
|
|
||||||
|
def export(self, name, columns, points):
|
||||||
|
"""Write the points in NATS."""
|
||||||
|
logger.info(f"Export called: name={name}, columns={columns}, connected={self._connected}")
|
||||||
|
|
||||||
|
if self._shutdown:
|
||||||
|
logger.warning("Export called during shutdown, skipping")
|
||||||
|
return
|
||||||
|
|
||||||
|
if not self.loop or not self.loop.is_running():
|
||||||
|
logger.error("NATS event loop is not running")
|
||||||
|
return
|
||||||
|
|
||||||
|
if not self._connected:
|
||||||
|
logger.warning("NATS not connected, skipping export")
|
||||||
|
return
|
||||||
|
|
||||||
|
subject_name = f"{self.prefix}.{name}"
|
||||||
|
subject_data = dict(zip(columns, points))
|
||||||
|
|
||||||
|
logger.info(f"Publishing to subject: {subject_name}")
|
||||||
|
|
||||||
|
# Submit the publish operation to the background event loop
|
||||||
|
try:
|
||||||
|
future = asyncio.run_coroutine_threadsafe(
|
||||||
|
self._publish(subject_name, json_dumps(subject_data)),
|
||||||
|
self.loop
|
||||||
|
)
|
||||||
|
# Don't block forever - use a short timeout
|
||||||
|
future.result(timeout=1)
|
||||||
|
self._publish_count += 1
|
||||||
|
logger.info(f"Successfully published message #{self._publish_count} to {subject_name}")
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
logger.warning(f"NATS publish timeout for {subject_name}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"NATS publish error for {subject_name}: {e}", exc_info=True)
|
||||||
|
|
||||||
|
async def _publish(self, subject, data):
|
||||||
|
"""Publish data to NATS."""
|
||||||
|
try:
|
||||||
|
logger.info(f"_publish called: subject={subject}, data_length={len(data)}")
|
||||||
|
|
||||||
|
if not self._connected:
|
||||||
|
raise ConnectionClosedError("Not connected to NATS")
|
||||||
|
|
||||||
|
logger.info(f"Calling client.publish for {subject}")
|
||||||
|
await self.client.publish(subject, data)
|
||||||
|
|
||||||
|
logger.info(f"Calling client.flush for {subject}")
|
||||||
|
await asyncio.wait_for(self.client.flush(), timeout=2.0)
|
||||||
|
|
||||||
|
logger.info(f"Successfully published and flushed to '{subject}'")
|
||||||
|
except (ConnectionClosedError, NatsTimeoutError) as e:
|
||||||
|
self._connected = False
|
||||||
|
logger.error(f"NATS publish failed: {e}")
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Unexpected error in _publish: {e}", exc_info=True)
|
||||||
|
raise
|
||||||
|
|
||||||
|
# End of glances/exports/glances_nats/__init__.py
|
||||||
|
|
@ -83,6 +83,7 @@ export = [
|
||||||
"influxdb>=1.0.0",
|
"influxdb>=1.0.0",
|
||||||
"influxdb3-python",
|
"influxdb3-python",
|
||||||
"kafka-python",
|
"kafka-python",
|
||||||
|
"nats-py",
|
||||||
"paho-mqtt",
|
"paho-mqtt",
|
||||||
"pika",
|
"pika",
|
||||||
"potsdb",
|
"potsdb",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
import nats
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
nc = nats.NATS()
|
||||||
|
|
||||||
|
await nc.connect(servers=["nats://localhost:4222"])
|
||||||
|
|
||||||
|
await nc.publish("glances.test", b'A test')
|
||||||
|
await nc.flush()
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
asyncio.run(main())
|
||||||
|
|
||||||
|
# To run this test script, make sure you have a NATS server running locally.
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
import nats
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
nc = nats.NATS()
|
||||||
|
|
||||||
|
await nc.connect(servers=["nats://localhost:4222"])
|
||||||
|
|
||||||
|
future = asyncio.Future()
|
||||||
|
|
||||||
|
async def cb(msg):
|
||||||
|
nonlocal future
|
||||||
|
future.set_result(msg)
|
||||||
|
|
||||||
|
await nc.subscribe("glances.*", cb=cb)
|
||||||
|
|
||||||
|
# Wait for message to come in
|
||||||
|
print("Waiting (max 30s) for a message on 'glances' subject...")
|
||||||
|
msg = await asyncio.wait_for(future, 30)
|
||||||
|
print(msg.subject, msg.data)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
asyncio.run(main())
|
||||||
|
|
||||||
|
# To run this test script, make sure you have a NATS server running locally.
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# Pre-requisites:
|
||||||
|
# - docker
|
||||||
|
# - jq
|
||||||
|
|
||||||
|
# Exit on error
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "Stop previous nats container..."
|
||||||
|
docker stop nats-for-glances || true
|
||||||
|
docker rm nats-for-glances || true
|
||||||
|
|
||||||
|
echo "Starting nats container..."
|
||||||
|
docker run -d \
|
||||||
|
--name nats-for-glances \
|
||||||
|
-p 4222:4222 \
|
||||||
|
-p 8222:8222 \
|
||||||
|
-p 6222:6222 \
|
||||||
|
nats:latest
|
||||||
|
|
||||||
|
# Wait for InfluxDB to be ready (5 seconds)
|
||||||
|
echo "Waiting for nats to start (~ 5 seconds)..."
|
||||||
|
sleep 5
|
||||||
|
|
||||||
|
# Run glances with export to nats, stopping after 10 writes
|
||||||
|
# This will run synchronously now since we're using --stop-after
|
||||||
|
echo "Glances to export system stats to nats (duration: ~ 20 seconds)"
|
||||||
|
.venv/bin/python -m glances --config ./conf/glances.conf --export nats --stop-after 10 --quiet
|
||||||
|
|
||||||
|
# Stop and remove the nats container
|
||||||
|
echo "Stopping and removing nats container..."
|
||||||
|
docker stop nats-for-glances && docker rm nats-for-glances
|
||||||
|
|
||||||
|
echo "Script completed successfully!"
|
||||||
|
|
@ -38,6 +38,6 @@ docker exec timescaledb-for-glances psql -d "postgres://postgres:password@localh
|
||||||
|
|
||||||
# Stop and remove the TimescaleDB container
|
# Stop and remove the TimescaleDB container
|
||||||
echo "Stopping and removing TimescaleDB container..."
|
echo "Stopping and removing TimescaleDB container..."
|
||||||
# docker stop timescaledb-for-glances && docker rm timescaledb-for-glances
|
docker stop timescaledb-for-glances && docker rm timescaledb-for-glances
|
||||||
|
|
||||||
echo "Script completed successfully!"
|
echo "Script completed successfully!"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue