Merge branch 'issue410' of github.com:nicolargo/glances into issue410

This commit is contained in:
nicolargo 2025-02-22 10:13:22 +01:00
commit c32d363897
11 changed files with 568 additions and 263 deletions

File diff suppressed because it is too large Load Diff

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" "Jan 04, 2025" "4.3.0.8" "Glances"
.TH "GLANCES" "1" "Jan 26, 2025" "4.3.1_dev09" "Glances"
.SH NAME
glances \- An eye on your system
.SH SYNOPSIS

View File

@ -29,7 +29,7 @@ from configparser import ConfigParser, NoOptionError, NoSectionError
from datetime import datetime
from operator import itemgetter, methodcaller
from statistics import mean
from typing import Any, Union
from typing import Any, Optional, Union
from urllib.error import HTTPError, URLError
from urllib.parse import urlparse
from urllib.request import Request, urlopen
@ -393,13 +393,22 @@ def dictlist(data, item):
return None
def json_dumps_dictlist(data, item):
def dictlist_json_dumps(data, item):
dl = dictlist(data, item)
if dl is None:
return None
return json_dumps(dl)
def dictlist_first_key_value(data: list[dict], key, value) -> Optional[dict]:
"""In a list of dict, return first item where key=value or none if not found."""
try:
ret = next(item for item in data if key in item and item[key] == value)
except StopIteration:
ret = None
return ret
def string_value_to_float(s):
"""Convert a string with a value and an unit to a float.
Example:

View File

@ -21,6 +21,7 @@ from glances.events_list import glances_events
from glances.globals import json_dumps
from glances.logger import logger
from glances.password import GlancesPassword
from glances.processes import glances_processes
from glances.servers_list import GlancesServersList
from glances.servers_list_dynamic import GlancesAutoDiscoverClient
from glances.stats import GlancesStats
@ -221,6 +222,9 @@ class GlancesRestfulApi:
# POST
router.add_api_route(f'{base_path}/events/clear/warning', self._events_clear_warning, methods=['POST'])
router.add_api_route(f'{base_path}/events/clear/all', self._events_clear_all, methods=['POST'])
router.add_api_route(
f'{base_path}/processes/extended/{{pid}}', self._api_set_extended_processes, methods=['POST']
)
# GET
route_mapping = {
@ -235,6 +239,8 @@ class GlancesRestfulApi:
f'{base_path}/all/views': self._api_all_views,
f'{base_path}/pluginslist': self._api_plugins,
f'{base_path}/serverslist': self._api_servers_list,
f'{base_path}/processes/extended': self._api_get_extended_processes,
f'{base_path}/processes/{{pid}}': self._api_get_processes,
f'{plugin_path}': self._api,
f'{plugin_path}/history': self._api_history,
f'{plugin_path}/history/{{nb}}': self._api_history,
@ -900,3 +906,52 @@ class GlancesRestfulApi:
raise HTTPException(status.HTTP_404_NOT_FOUND, f"Cannot get args item ({str(e)})")
return GlancesJSONResponse(args_json)
def _api_set_extended_processes(self, pid: str):
"""Glances API RESTful implementation.
Set the extended process stats for the given PID
HTTP/200 if OK
HTTP/400 if PID is not found
HTTP/404 if others error
"""
process_stats = glances_processes.get_stats(int(pid))
if not process_stats:
raise HTTPException(status.HTTP_404_NOT_FOUND, f"Unknown PID process {pid}")
glances_processes.extended_process = process_stats
return GlancesJSONResponse(True)
def _api_get_extended_processes(self):
"""Glances API RESTful implementation.
Get the extended process stats (if set before)
HTTP/200 if OK
HTTP/400 if PID is not found
HTTP/404 if others error
"""
process_stats = glances_processes.get_extended_stats()
if not process_stats:
process_stats = {}
print("Call _api_get_extended_processes")
return GlancesJSONResponse(process_stats)
def _api_get_processes(self, pid: str):
"""Glances API RESTful implementation.
Get the process stats for the given PID
HTTP/200 if OK
HTTP/400 if PID is not found
HTTP/404 if others error
"""
process_stats = glances_processes.get_stats(int(pid))
if not process_stats:
raise HTTPException(status.HTTP_404_NOT_FOUND, f"Unknown PID process {pid}")
return GlancesJSONResponse(process_stats)

View File

@ -225,7 +225,30 @@ def print_all():
print('Get all Glances stats::')
print('')
print(f' # curl {API_URL}/all')
print(' Return a very big dictionary (avoid using this request, performances will be poor)...')
print(' Return a very big dictionary with all stats')
print('')
print('Note: Update is done automatically every time /all or /<plugin> is called.')
print('')
def print_processes():
sub_title = 'GET stats of a specific process'
print(sub_title)
print('-' * len(sub_title))
print('')
print('Get stats for process with PID == 777::')
print('')
print(f' # curl {API_URL}/processes/777')
print(' Return stats for process (dict)')
print('')
print('Enable extended stats for process with PID == 777 (only one process at a time can be enabled)::')
print('')
print(f' # curl -X POST {API_URL}/processes/extended/777')
print(f' # curl {API_URL}/all')
print(f' # curl {API_URL}/processes/777')
print(' Return stats for process (dict)')
print('')
print('Note: Update *is not* done automatically when you call /processes/<pid>.')
print('')
@ -370,6 +393,9 @@ class GlancesStdoutApiDoc:
# Get all stats
print_all()
# Get process stats
print_processes()
# Get top stats (only for plugins with a list of items)
# Example for processlist plugin: get top 2 processes
print_top(stats)

View File

@ -398,6 +398,11 @@ body {
.table {
margin-bottom: 1em;
}
.table-hover tbody tr:hover td {
background: $glances-link-hover-color;
}
// Default column size
* > td:nth-child(-n+12) {
width: 5em;
@ -427,6 +432,7 @@ body {
}
}
#alerts {
span {
padding-left: 10px;

View File

@ -2,6 +2,7 @@
<!-- Display processes -->
<section class="plugin" id="processlist" v-if="!args.programs">
<div>PIN PROCESS: {{ extended_stat }} - {{ getPinProcess() }}</div>
<div class="table-responsive d-lg-none">
<table class="table table-sm table-borderless table-striped table-hover">
<thead>
@ -31,7 +32,8 @@
</tr>
</thead>
<tbody>
<tr v-for="(process, processId) in processes" :key="processId">
<tr v-for="(process, processId) in processes" :key="processId" @click="setPinProcess(process)"
style="cursor: pointer">
<td scope="row" :class="getCpuPercentAlert(process)"
v-show="!getDisableStats().includes('cpu_percent')">
{{ process.cpu_percent == -1 ? '?' : $filters.number(process.cpu_percent, 1) }}
@ -114,7 +116,8 @@
</tr>
</thead>
<tbody>
<tr v-for="(process, processId) in processes" :key="processId">
<tr v-for="(process, processId) in processes" :key="processId" @click="setPinProcess(process.pid)"
style="cursor: pointer">
<td scope="row" :class="getCpuPercentAlert(process)"
v-show="!getDisableStats().includes('cpu_percent')">
{{ process.cpu_percent == -1 ? '?' : $filters.number(process.cpu_percent, 1) }}
@ -354,9 +357,20 @@ export default {
},
data() {
return {
store
store,
extended_stat: undefined,
intervalId: null
};
},
mounted() {
// Refresh every second
this.intervalId = setInterval(() => {
this.getPinProcess()
}, 1000)
},
beforeUnmount() {
clearInterval(this.intervalId)
},
computed: {
args() {
return this.store.args || {};
@ -527,7 +541,7 @@ export default {
return this.config.outputs !== undefined
? this.config.outputs.max_processes_display
: undefined;
}
},
},
methods: {
getCpuPercentAlert(process) {
@ -538,6 +552,15 @@ export default {
},
getDisableStats() {
return GlancesHelper.getLimit('processlist', 'processlist_disable_stats') || [];
},
setPinProcess(pid) {
fetch('api/4/processes/extended/' + pid.toString(), { method: 'POST' })
.then((response) => response.json());
},
getPinProcess() {
fetch('api/4/processes/extended', { method: 'GET' })
.then((response) => response.json())
.then((response) => (this.extended_stat = response));
}
}
};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -17,7 +17,7 @@ import re
from glances.actions import GlancesActions
from glances.events_list import glances_events
from glances.globals import dictlist, iterkeys, itervalues, json_dumps, json_dumps_dictlist, listkeys, mean, nativestr
from glances.globals import dictlist, dictlist_json_dumps, iterkeys, itervalues, json_dumps, listkeys, mean, nativestr
from glances.history import GlancesHistory
from glances.logger import logger
from glances.outputs.glances_unicode import unicode_message
@ -244,7 +244,7 @@ class GlancesPluginModel:
if item is None:
return json_dumps(s)
return json_dumps_dictlist(s, item)
return dictlist_json_dumps(s, item)
def get_trend(self, item, nb=30):
"""Get the trend regarding to the last nb values.
@ -405,7 +405,7 @@ class GlancesPluginModel:
Stats should be a list of dict (processlist, network...)
"""
return json_dumps_dictlist(self.get_raw(), item)
return dictlist_json_dumps(self.get_raw(), item)
def get_raw_stats_value(self, item, value):
"""Return the stats object for a specific item=value.

View File

@ -11,7 +11,16 @@ import os
import psutil
from glances.filter import GlancesFilter, GlancesFilterList
from glances.globals import BSD, LINUX, MACOS, WINDOWS, iterkeys, list_of_namedtuple_to_list_of_dict, namedtuple_to_dict
from glances.globals import (
BSD,
LINUX,
MACOS,
WINDOWS,
dictlist_first_key_value,
iterkeys,
list_of_namedtuple_to_list_of_dict,
namedtuple_to_dict,
)
from glances.logger import logger
from glances.programs import processes_to_programs
from glances.timer import Timer, getTimeSinceLastUpdate
@ -303,8 +312,8 @@ class GlancesProcesses:
for k in self._max_values_list:
self._max_values[k] = 0.0
def get_extended_stats(self, proc):
"""Get the extended stats for the given PID."""
def set_extended_stats(self, proc):
"""Set the extended stats for the given PID."""
# - cpu_affinity (Linux, Windows, FreeBSD)
# - ionice (Linux and Windows > Vista)
# - num_ctx_switches (not available on Illumos/Solaris)
@ -350,6 +359,16 @@ class GlancesProcesses:
ret['extended_stats'] = True
return namedtuple_to_dict(ret)
def get_extended_stats(self):
"""Return the extended stats.
Return the process stat when extended_stats = True
"""
for p in self.processlist:
if p.get('extended_stats'):
return p
return None
def __get_min_max_mean(self, proc, prefix=['cpu', 'memory']):
"""Return the min/max/mean for the given process"""
ret = {}
@ -578,7 +597,7 @@ class GlancesProcesses:
# Grab extended stats only for the selected process (see issue #2225)
if self.extended_process is not None and proc['pid'] == self.extended_process['pid']:
proc.update(self.get_extended_stats(self.extended_process))
proc.update(self.set_extended_stats(self.extended_process))
self.extended_process = namedtuple_to_dict(proc)
# Meta data
@ -652,6 +671,10 @@ class GlancesProcesses:
"""Return the processlist for export."""
return self.processlist_export
def get_stats(self, pid):
"""Get stats for the given pid."""
return dictlist_first_key_value(self.processlist, 'pid', pid)
@property
def sort_key(self):
"""Get the current sort key."""