Create a Glances API in order to use Glances as a Python lib #3237

This commit is contained in:
nicolargo 2025-07-12 18:16:43 +02:00
parent 365a670c12
commit 3d263bc7d4
22 changed files with 14839 additions and 472 deletions

View File

@ -211,8 +211,9 @@ trivy: ## Run Trivy to find vulnerabilities in container images
# ===================================================================
docs: ## Create the documentation
$(PYTHON) -m glances -C $(CONF) --api-doc > ./docs/api/python.rst
$(PYTHON) ./generate_openapi.py
$(PYTHON) -m glances -C $(CONF) --api-doc > ./docs/api.rst
$(PYTHON) -m glances -C $(CONF) --api-restful-doc > ./docs/api/restful.rst
cd docs && ./build.sh && cd ..
docs-server: docs ## Start a Web server to serve the documentation

View File

@ -424,7 +424,7 @@ See release note in Wiki format: https://github.com/nicolargo/glances/wiki/Glanc
**BREAKING CHANGES:**
* The minimal Python version is 3.8
* The Glances API version 3 is replaced by the version 4. So Restfull API URL is now /api/4/ #2610
* The Glances API version 3 is replaced by the version 4. So Restful API URL is now /api/4/ #2610
* Alias definition change in the configuration file #1735
Glances version 3.x and lower:
@ -449,9 +449,9 @@ Minimal requirements for Glances version 4 are:
* packaging
* ujson
* pydantic
* fastapi (for WebUI / RestFull API)
* uvicorn (for WebUI / RestFull API)
* jinja2 (for WebUI / RestFull API)
* fastapi (for WebUI / RestFul API)
* uvicorn (for WebUI / RestFul API)
* jinja2 (for WebUI / RestFul API)
Majors changes between Glances version 3 and version 4:
@ -511,7 +511,7 @@ Bug corrected:
CI and documentation:
* New logo for Glances version 4.0 #2713
* Update api.rst documentation #2496
* Update api-restful.rst documentation #2496
* Change Renovate config #2729
* Docker compose password unrecognized arguments when applying docs #2698
* Docker includes OS Release Volume mount info #2473
@ -889,7 +889,7 @@ Bugs corrected:
* Threading.Event.isSet is deprecated in Python 3.10 #2017
* Fix code scanning alert - Clear-text logging of sensitive information security #2006
* The gpu temperature unit are displayed incorrectly in web ui bug #2002
* Doc for 'alert' Restfull/JSON API response documentation #1994
* Doc for 'alert' Restful/JSON API response documentation #1994
* Show the spinning state of a disk documentation #1993
* Web server status check endpoint enhancement #1988
* --time parameter being ignored for client/server mode bug #1978
@ -984,7 +984,7 @@ Bugs corrected:
* [3.2.0/3.2.1] keybinding not working anymore #1904
* InfluxDB/InfluxDB2 Export object has no attribute hostname #1899
Documentation: The "make docs" generate RestFull/API documentation file.
Documentation: The "make docs" generate RestFul/API documentation file.
===============
Version 3.2.1
@ -2011,7 +2011,7 @@ Version 2.1
* Add Glances log message (in the /tmp/glances.log file)
The default log level is INFO, you can switch to the DEBUG mode using the -d option on the command line.
* Add RESTful API to the Web server mode
RESTful API doc: https://github.com/nicolargo/glances/wiki/The-Glances-RESTFULL-JSON-API
RESTful API doc: https://github.com/nicolargo/glances/wiki/The-Glances-RESTFUL-JSON-API
* Improve SNMP fallback mode for Cisco IOS, VMware ESXi
* Add --theme-white feature to optimize display for white background
* Experimental history feature (--enable-history option on the command line)

View File

@ -49,7 +49,7 @@ history_size=1200
# You can download it in a specific folder
# thanks to https://github.com/nicolargo/glances/issues/2021
# then configure this folder with the webui_root_path key
# Default is folder where glances_restfull_api.py is hosted
# Default is folder where glances_restful_api.py is hosted
#webui_root_path=
# CORS options
# Comma separated list of origins that should be permitted to make cross-origin requests.

View File

@ -49,7 +49,7 @@ max_processes_display=25
# You can download it in a specific folder
# thanks to https://github.com/nicolargo/glances/issues/2021
# then configure this folder with the webui_root_path key
# Default is folder where glances_restfull_api.py is hosted
# Default is folder where glances_restful_api.py is hosted
#webui_root_path=
# CORS options
# Comma separated list of origins that should be permitted to make cross-origin requests.

View File

@ -63,7 +63,7 @@ Within ``/etc/glances/actions.d/fs-critical.py``:
.. note::
You can use all the stats for the current plugin. See
https://github.com/nicolargo/glances/wiki/The-Glances-RESTFULL-JSON-API
https://github.com/nicolargo/glances/wiki/The-Glances-RESTFUL-JSON-API
for the stats list.
It is also possible to repeat action until the end of the alert.

14183
docs/api/python.rst Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -80,7 +80,7 @@ than a second one concerning the user interface:
# You can download it in a specific folder
# thanks to https://github.com/nicolargo/glances/issues/2021
# then configure this folder with the webui_root_path key
# Default is folder where glances_restfull_api.py is hosted
# Default is folder where glances_restful_api.py is hosted
#webui_root_path=
# CORS options
# Comma separated list of origins that should be permitted to make cross-origin requests.

View File

@ -30,7 +30,8 @@ Table of Contents
config
aoa/index
gw/index
api
api/python
api/restful
docker
faq
support

View File

@ -28,7 +28,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
..
.TH "GLANCES" "1" "Jul 09, 2025" "4.3.3" "Glances"
.TH "GLANCES" "1" "Jul 12, 2025" "4.4.0_dev1" "Glances"
.SH NAME
glances \- An eye on your system
.SH SYNOPSIS
@ -666,7 +666,7 @@ max_processes_display=25
# You can download it in a specific folder
# thanks to https://github.com/nicolargo/glances/issues/2021
# then configure this folder with the webui_root_path key
# Default is folder where glances_restfull_api.py is hosted
# Default is folder where glances_restful_api.py is hosted
#webui_root_path=
# CORS options
# Comma separated list of origins that should be permitted to make cross\-origin requests.

View File

@ -14,7 +14,7 @@ test_args = core.get_args()
app = GlancesRestfulApi(config=test_config, args=test_args)._app
with open('./docs/openapi.json', 'w') as f:
with open('./docs/api/openapi.json', 'w') as f:
json.dump(
get_openapi(
title=app.title,

View File

@ -95,8 +95,8 @@ def check_memleak(args, mode):
def setup_server_mode(args, mode):
if args.stdout_issue or args.stdout_apidoc:
# Serve once for issue/test mode
if args.stdout_issue or args.stdout_api_restful_doc or args.stdout_api_doc:
# Serve once for issue and API documentation modes
mode.serve_issue()
else:
# Serve forever

View File

@ -7,18 +7,45 @@
#
from glances import __version__ as glances_version
from glances.globals import weak_lru_cache
from glances.main import GlancesMain
from glances.stats import GlancesStats
plugin_dependencies_tree = {
'processlist': ['processcount'],
}
class GlancesAPI:
def __init__(self):
ttl = 2.0 # Default cache TTL in seconds
def __init__(self, config=None, args=None, args_begin_at=1):
self.__version__ = glances_version.split('.')[0] # Get the major version
core = GlancesMain(args_begin_at=2)
self._stats = GlancesStats(config=core.get_config(), args=core.get_args())
core = GlancesMain(args_begin_at)
self.args = args if args is not None else core.get_args()
self.config = config if config is not None else core.get_config()
self._stats = GlancesStats(config=self.config, args=self.args)
for p in self._stats.getPluginsList():
plugin = self._stats.get_plugin(p)
if plugin is not None:
setattr(self, p, plugin)
# Set the cache TTL for the API
self.ttl = self.args.time if self.args.time is not None else self.ttl
# Init the stats of all plugins in order to ensure that rate are computed
self._stats.update()
@weak_lru_cache(maxsize=1, ttl=ttl)
def __getattr__(self, item):
"""Fallback to the stats object for any missing attributes."""
if item in self._stats.getPluginsList():
if item in plugin_dependencies_tree:
# Ensure dependencies are updated before accessing the plugin
for dependency in plugin_dependencies_tree[item]:
self._stats.get_plugin(dependency).update()
# Update the plugin stats
self._stats.get_plugin(item).update()
return self._stats.get_plugin(item)
raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{item}'")
def plugins(self):
"""Return the list of available plugins."""
return self._stats.getPluginsList()

View File

@ -550,7 +550,18 @@ Examples of use:
help='test memory leak (python 3.4 or higher needed)',
)
parser.add_argument(
'--api-doc', default=None, action='store_true', dest='stdout_apidoc', help='display fields descriptions'
'--api-doc',
default=None,
action='store_true',
dest='stdout_api_doc',
help='display Python API documentation',
)
parser.add_argument(
'--api-restful-doc',
default=None,
action='store_true',
dest='stdout_api_restful_doc',
help='display Restful API documentation',
)
if not WINDOWS:
parser.add_argument(

View File

@ -6,7 +6,7 @@
# SPDX-License-Identifier: LGPL-3.0-only
#
"""RestFull API interface class."""
"""RestFul API interface class."""
import os
import socket

View File

@ -0,0 +1,175 @@
#
# This file is part of Glances.
#
# SPDX-FileCopyrightText: 2025 Nicolas Hennion <nicolas@nicolargo.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""Generate Glances Python API documentation."""
from pprint import pformat
from glances import api
APIDOC_HEADER = """\
.. _api:
Python API documentation
========================
This documentation describes the Glances Python API.
Note: This API is only available in Glances 4.4.0 or higher.
"""
def printtab(s, indent=' '):
print(indent + s.replace('\n', '\n' + indent))
def print_tldr(gl):
"""Print the TL;DR section of the API documentation."""
sub_title = 'TL;DR'
print(sub_title)
print('-' * len(sub_title))
print('')
print('You can access the Glances API by importing the `glances.api` module and creating an')
print('instance of the `GlancesAPI` class. This instance provides access to all Glances plugins')
print('and their fields. For example, to access the CPU plugin and its total field, you can')
print('use the following code:')
print('')
print('.. code-block:: python')
print('')
printtab('>>> from glances import api')
printtab('>>> gl = api.GlancesAPI()')
printtab('>>> gl.cpu')
printtab(f'{pformat(gl.cpu.stats)}')
printtab('>>> gl.cpu["total"]')
printtab(f'{gl.cpu["total"]}')
print('')
print('If the stats return a list of items (like network interfaces or processes), you can')
print('access them by their name:')
print('')
print('.. code-block:: python')
print('')
printtab(f'{gl.network.keys()}')
printtab(f'>>> gl.network["{gl.network.keys()[0]}"]')
printtab(f'{pformat(gl.network[gl.network.keys()[0]])}')
print('')
def print_init_api(gl):
sub_title = 'Init Glances Python API'
print(sub_title)
print('-' * len(sub_title))
print('')
print('Init the Glances API:')
print('')
print('.. code-block:: python')
print('')
printtab('>>> from glances import api')
printtab('>>> gl = api.GlancesAPI()')
print('')
def print_plugins_list(gl):
sub_title = 'Get Glances plugins list'
print(sub_title)
print('-' * len(sub_title))
print('')
print('Get the plugins list:')
print('')
print('.. code-block:: python')
print('')
printtab('>>> gl.plugins()')
printtab(f'{gl.plugins()}')
print('```')
print('')
def print_plugin(gl, plugin):
"""Print the details of a single plugin."""
sub_title = f'Glances {plugin}'
print(sub_title)
print('-' * len(sub_title))
print('')
stats_obj = gl.__getattr__(plugin)
print(f'{plugin.capitalize()} stats:')
print('')
print('.. code-block:: python')
print('')
printtab(f'>>> gl.{plugin}')
printtab(f'Return a {type(stats_obj)} object')
if len(stats_obj.keys()) > 0 and isinstance(stats_obj[stats_obj.keys()[0]], dict):
printtab(f'>>> gl.{plugin}')
printtab(f'Return a dict of dict with key=<{stats_obj[stats_obj.keys()[0]]["key"]}>')
printtab(f'>>> gl.{plugin}.keys()')
printtab(f'{stats_obj.keys()}')
printtab(f'>>> gl.{plugin}["{stats_obj.keys()[0]}"]')
printtab(f'{pformat(stats_obj[stats_obj.keys()[0]])}')
else:
printtab(f'>>> gl.{plugin}')
printtab(f'{pformat(stats_obj.stats)}')
if len(stats_obj.keys()) > 0:
printtab(f'>>> gl.{plugin}.keys()')
printtab(f'{stats_obj.keys()}')
printtab(f'>>> gl.{plugin}["{stats_obj.keys()[0]}"]')
printtab(f'{pformat(stats_obj[stats_obj.keys()[0]])}')
print('')
if stats_obj.fields_description is not None:
print(f'{plugin.capitalize()} fields description:')
print('')
for field, description in stats_obj.fields_description.items():
print(f'* {field}: {description["description"]}')
print('')
print(f'{plugin.capitalize()} limits:')
print('')
print('.. code-block:: python')
print('')
printtab(f'>>> gl.{plugin}.limits')
printtab(f'{pformat(gl.__getattr__(plugin).limits)}')
print('')
def print_plugins(gl):
"""Print the details of all plugins."""
for plugin in gl.plugins():
print_plugin(gl, plugin)
class GlancesStdoutApiDoc:
"""This class manages the fields description display."""
def __init__(self, config=None, args=None):
# Init
self.gl = api.GlancesAPI()
def end(self):
pass
def update(self, stats, duration=1):
"""Display issue"""
# Display header
print(APIDOC_HEADER)
# Display TL;DR section
print_tldr(self.gl)
# Init the API
print_init_api(self.gl)
# Display plugins list
print_plugins_list(self.gl)
# Loop over plugins
print_plugins(self.gl)
# Return True to exit directly (no refresh)
return True

View File

@ -6,7 +6,7 @@
# SPDX-License-Identifier: LGPL-3.0-only
#
"""Fields description interface class."""
"""Generate Glances Restful API documentation."""
import json
import time
@ -19,20 +19,20 @@ from glances.logger import logger
API_URL = f"http://localhost:61208/api/{__apiversion__}"
APIDOC_HEADER = f"""\
.. _api:
.. _api_restful:
API (Restfull/JSON) documentation
=================================
Restful/JSON API documentation
==============================
This documentation describes the Glances API version {__apiversion__} (Restfull/JSON) interface.
This documentation describes the Glances API version {__apiversion__} (Restful/JSON) interface.
An OpenAPI specification file is available at:
``https://raw.githubusercontent.com/nicolargo/glances/refs/heads/develop/docs/openapi.json``
``https://raw.githubusercontent.com/nicolargo/glances/refs/heads/develop/docs/api/openapi.json``
Run the Glances API server
--------------------------
The Glances Restfull/API server could be ran using the following command line:
The Glances Restful/API server could be ran using the following command line:
.. code-block:: bash
@ -354,7 +354,7 @@ def print_plugin_post_events():
print('')
class GlancesStdoutApiDoc:
class GlancesStdoutApiRestfulDoc:
"""This class manages the fields description display."""
def __init__(self, config=None, args=None):

View File

@ -150,14 +150,13 @@ class GlancesPluginModel:
def __getitem__(self, item):
"""Return the stats item."""
if self.stats is not None:
# Stats is a dict try, to return the item
if isinstance(self.stats, dict) and item in self.stats:
return self.stats[item]
if isinstance(self.stats, list):
ltd = list_to_dict(self.stats)
if item in ltd:
return ltd[item]
if isinstance(self.stats, dict) and item in self.stats:
return self.stats[item]
if isinstance(self.stats, list):
ltd = list_to_dict(self.stats)
if item in ltd:
return ltd[item]
raise KeyError(f"'{self.__class__.__name__}' object has no key '{item}'")

View File

@ -16,7 +16,8 @@ from glances.logger import logger
from glances.outdated import Outdated
from glances.outputs.glances_curses import GlancesCursesStandalone
from glances.outputs.glances_stdout import GlancesStdout
from glances.outputs.glances_stdout_apidoc import GlancesStdoutApiDoc
from glances.outputs.glances_stdout_api_doc import GlancesStdoutApiDoc
from glances.outputs.glances_stdout_api_restful_doc import GlancesStdoutApiRestfulDoc
from glances.outputs.glances_stdout_csv import GlancesStdoutCsv
from glances.outputs.glances_stdout_issue import GlancesStdoutIssue
from glances.outputs.glances_stdout_json import GlancesStdoutJson
@ -85,10 +86,14 @@ class GlancesStandalone:
logger.info("Issue mode is ON")
# Init screen
self.screen = GlancesStdoutIssue(config=config, args=args)
elif args.stdout_apidoc:
logger.info("Fields descriptions mode is ON")
elif args.stdout_api_doc:
logger.info("Restful Python documentation mode is ON")
# Init screen
self.screen = GlancesStdoutApiDoc(config=config, args=args)
elif args.stdout_api_restful_doc:
logger.info("Restful API documentation mode is ON")
# Init screen
self.screen = GlancesStdoutApiRestfulDoc(config=config, args=args)
elif args.stdout:
logger.info(f"Stdout mode is ON, following stats will be displayed: {args.stdout}")
# Init screen

View File

@ -266,8 +266,10 @@ please rename it to "{plugin_path.capitalize()}Plugin"'
for p in self.getPluginsList(enable=False):
self._plugins[p].load_limits(config)
# It's a weak cache to avoid updating the same plugin too often
# Note: the function always return None
@weak_lru_cache(maxsize=1, ttl=1)
def __update_plugin(self, p):
def update_plugin(self, p):
"""Update stats, history and views for the given plugin name p"""
self._plugins[p].update()
self._plugins[p].update_views()
@ -280,7 +282,7 @@ please rename it to "{plugin_path.capitalize()}Plugin"'
"""
# Start update of all enable plugins
for p in self.getPluginsList(enable=True):
self.__update_plugin(p)
self.update_plugin(p)
def export(self, input_stats=None):
"""Export all the stats.

View File

@ -17,7 +17,7 @@ from glances import __version__, api
# Init Glances API
# test_config = core.get_config()
# test_args = core.get_args()
gl = api.GlancesAPI()
gl = api.GlancesAPI(args_begin_at=2)
# Pytest functions to test the Glances API version
@ -25,11 +25,22 @@ def test_glances_api_version():
assert gl.__version__ == __version__.split('.')[0]
def test_glances_api_plugins():
# Check that the plugins list is not empty
assert len(gl.plugins()) > 0
# Check that the cpu plugin is in the list of plugins
assert 'cpu' in gl.plugins()
# Check that the network plugin is in the list of plugins
assert 'network' in gl.plugins()
# Check that the processcount plugin is in the list of plugins
assert 'processcount' in gl.plugins()
# Check that the processlist plugin is in the list of plugins
assert 'processlist' in gl.plugins()
def test_glances_api_plugin_cpu():
# Check that the cpu plugin is available
assert gl.cpu is not None
# Update stats
gl.cpu.update()
# Get list of keys (cpu stat fields)
keys = gl.cpu.keys()
# Check that the keys are not empty
@ -45,8 +56,6 @@ def test_glances_api_plugin_cpu():
def test_glances_api_plugin_network():
# Check that the network plugin is available
assert gl.network is not None
# Update stats
gl.network.update()
# Get list of keys (interfaces)
keys = gl.network.keys()
# Check that the keys are not empty
@ -56,7 +65,6 @@ def test_glances_api_plugin_network():
def test_glances_api_plugin_process():
gl.processcount.update()
# Get list of keys (processes)
keys = gl.processcount.keys()
# Check that the keys are not empty
@ -64,11 +72,13 @@ def test_glances_api_plugin_process():
# Check that processcount total is > 0
assert gl.processcount['total'] > 0
# Note should be done after processcount update
gl.processlist.update()
# Get list of keys (processes)
keys = gl.processlist.keys()
# Check that first key is an integer (PID)
assert isinstance(keys[0], int)
# Check that the first item is a dictionary
assert isinstance(gl.processlist[keys[0]], dict)
def test_glances_api_limits():
assert isinstance(gl.cpu.limits, dict)