Issues (49)

glances/plugins/quicklook/__init__.py (1 issue)

1
#
2
# This file is part of Glances.
3
#
4
# SPDX-FileCopyrightText: 2024 Nicolas Hennion <[email protected]>
5
#
6
# SPDX-License-Identifier: LGPL-3.0-only
7
#
8
9
"""Quicklook plugin."""
10
11
import psutil
12
13
from glances.cpu_percent import cpu_percent
14
from glances.logger import logger
15
from glances.outputs.glances_bars import Bar
16
from glances.outputs.glances_sparklines import Sparkline
17
from glances.plugins.load import get_load_average, get_nb_log_core, get_nb_phys_core
18
from glances.plugins.plugin.model import GlancesPluginModel
19
20
# Fields description
21
# description: human readable description
22
# short_name: shortname to use un UI
23
# unit: unit type
24
# rate: is it a rate ? If yes, // by time_since_update when displayed,
25
# min_symbol: Auto unit should be used if value > than 1 'X' (K, M, G)...
26
fields_description = {
27
    'cpu': {
28
        'description': 'CPU percent usage',
29
        'unit': 'percent',
30
    },
31
    'mem': {
32
        'description': 'MEM percent usage',
33
        'unit': 'percent',
34
    },
35
    'swap': {
36
        'description': 'SWAP percent usage',
37
        'unit': 'percent',
38
    },
39
    'load': {
40
        'description': 'LOAD percent usage',
41
        'unit': 'percent',
42
    },
43
    'cpu_log_core': {
44
        'description': 'Number of logical CPU core',
45
        'unit': 'number',
46
    },
47
    'cpu_phys_core': {
48
        'description': 'Number of physical CPU core',
49
        'unit': 'number',
50
    },
51
    'cpu_name': {
52
        'description': 'CPU name',
53
    },
54
    'cpu_hz_current': {
55
        'description': 'CPU current frequency',
56
        'unit': 'hertz',
57
    },
58
    'cpu_hz': {
59
        'description': 'CPU max frequency',
60
        'unit': 'hertz',
61
    },
62
}
63
64
# Define the history items list
65
# All items in this list will be historised if the --enable-history tag is set
66
items_history_list = [
67
    {'name': 'cpu', 'description': 'CPU percent usage', 'y_unit': '%'},
68
    {'name': 'percpu', 'description': 'PERCPU percent usage', 'y_unit': '%'},
69
    {'name': 'mem', 'description': 'MEM percent usage', 'y_unit': '%'},
70
    {'name': 'swap', 'description': 'SWAP percent usage', 'y_unit': '%'},
71
    {'name': 'load', 'description': 'LOAD percent usage', 'y_unit': '%'},
72
]
73
74
75
class PluginModel(GlancesPluginModel):
76
    """Glances quicklook plugin.
77
78
    'stats' is a dictionary.
79
    """
80
81
    AVAILABLE_STATS_LIST = ['cpu', 'mem', 'swap', 'load']
82
    DEFAULT_STATS_LIST = ['cpu', 'mem', 'load']
83
84
    def __init__(self, args=None, config=None):
85
        """Init the quicklook plugin."""
86
        super().__init__(
87
            args=args, config=config, items_history_list=items_history_list, fields_description=fields_description
88
        )
89
90
        # We want to display the stat in the curse interface
91
        self.display_curse = True
92
93
        # Manage the maximum number of CPU to display (related to enhancement request #2734)
94
        self.max_cpu_display = config.get_int_value('percpu', 'max_cpu_display', 4) if config else 4
95
96
        # Define the stats list
97
        self.stats_list = self.get_conf_value('list', default=self.DEFAULT_STATS_LIST)
98
        if not set(self.stats_list).issubset(self.AVAILABLE_STATS_LIST):
99
            logger.warning(f'Quicklook plugin: Invalid stats list: {self.stats_list}')
100
            self.stats_list = self.AVAILABLE_STATS_LIST
101
102
    @GlancesPluginModel._check_decorator
103
    @GlancesPluginModel._log_result_decorator
104
    def update(self):
105
        """Update quicklook stats using the input method."""
106
        # Init new stats
107
        stats = self.get_init_value()
108
109
        # Grab quicklook stats: CPU, MEM and SWAP
110
        if self.input_method == 'local':
111
            # Get system information
112
            cpu_info = cpu_percent.get_info()
113
            stats['cpu_name'] = cpu_info['cpu_name']
114
            stats['cpu_hz_current'] = (
115
                self._mhz_to_hz(cpu_info['cpu_hz_current']) if cpu_info['cpu_hz_current'] is not None else None
116
            )
117
            stats['cpu_hz'] = self._mhz_to_hz(cpu_info['cpu_hz']) if cpu_info['cpu_hz'] is not None else None
118
119
            # Get the CPU percent value (global and per core)
120
            # Stats is shared across all plugins
121
            stats['cpu'] = cpu_percent.get()
122
            stats['percpu'] = cpu_percent.get(percpu=True)
123
124
            # Get the virtual and swap memory
125
            stats['mem'] = psutil.virtual_memory().percent
126
            try:
127
                stats['swap'] = psutil.swap_memory().percent
128
            except RuntimeError:
129
                # Correct issue in Illumos OS (see #1767)
130
                stats['swap'] = None
131
132
            # Get load
133
            stats['cpu_log_core'] = get_nb_log_core()
134
            stats['cpu_phys_core'] = get_nb_phys_core()
135
            try:
136
                # Load average is a tuple (1 min, 5 min, 15 min)
137
                # Process only the 15 min value (index 2)
138
                stats['load'] = get_load_average(percent=True)[2]
139
            except (TypeError, IndexError):
140
                stats['load'] = None
141
142
        elif self.input_method == 'snmp':
143
            # Not available
144
            pass
145
146
        # Update the stats
147
        self.stats = stats
148
149
        return self.stats
150
151
    def update_views(self):
152
        """Update stats views."""
153
        # Call the father's method
154
        super().update_views()
155
156
        # Alert for CPU, MEM and SWAP
157
        for key in self.stats_list:
158
            if key in self.stats:
159
                self.views[key]['decoration'] = self.get_alert(self.stats[key], header=key)
160
161
        # Alert for LOAD
162
        self.views['load']['decoration'] = self.get_alert(self.stats['load'], header='load')
163
164
        # Define the list of stats to display
165
        self.views['list'] = self.stats_list
166
167
    def msg_curse(self, args=None, max_width=10):
168
        """Return the list to display in the UI."""
169
        # Init the return message
170
        ret = []
171
172
        # Only process if stats exist...
173
        if not self.stats or self.is_disabled():
174
            return ret
175
176
        if not max_width:
177
            # No max_width defined, return an empty message
178
            logger.debug(f"No max_width defined for the {self.plugin_name} plugin, it will not be displayed.")
179
            return ret
180
181
        # Define the data: Bar (default behavior) or Sparkline
182
        data = {}
183
        for key in self.stats_list:
184
            if self.args.sparkline and self.history_enable() and not self.args.client:
185
                data[key] = Sparkline(max_width)
186
            else:
187
                # Fallback to bar if Sparkline module is not installed
188
                data[key] = Bar(max_width, bar_char=self.get_conf_value('bar_char', default=['|'])[0])
189
190
        # Build the string message
191
        ##########################
192
193
        # System information
194
        if 'cpu_hz_current' in self.stats and 'cpu_hz' in self.stats:
195
            msg_freq = ' {:.2f}/{:.2f}GHz'.format(
196
                self._hz_to_ghz(self.stats['cpu_hz_current']), self._hz_to_ghz(self.stats['cpu_hz'])
197
            )
198
        else:
199
            msg_freq = ''
200
201
        if 'cpu_name' in self.stats and (max_width - len(msg_freq) + 7) > 0:
202
            msg_name = '{:{width}}'.format(self.stats['cpu_name'], width=max_width - len(msg_freq) + 7)
203
        else:
204
            msg_name = '' if msg_freq == '' else 'Frequency'
205
206
        ret.append(self.curse_add_line(msg_name))
207
        ret.append(self.curse_add_line(msg_freq))
208
        ret.append(self.curse_new_line())
209
210
        # Loop over CPU, MEM and LOAD
211
        for key in self.stats_list:
212
            if key == 'cpu' and args.percpu:
213
                ret.extend(self._msg_per_cpu(data, key, max_width))
214
            else:
215
                if type(data[key]).__name__ == 'Sparkline':
216
                    # Sparkline display an history
217
                    data[key].percents = [i[1] for i in self.get_raw_history(item=key, nb=data[key].size)]
218
                    # A simple padding in order to align metrics to the right
219
                    data[key].percents += [None] * (data[key].size - len(data[key].percents))
220
                else:
221
                    # Bar only the last value
222
                    data[key].percent = self.stats[key]
223
                msg = f'{key.upper():4} '
224
                ret.extend(self._msg_create_line(msg, data[key], key))
225
                ret.append(self.curse_new_line())
226
227
        # Remove the last new line
228
        ret.pop()
229
230
        # Return the message with decoration
231
        return ret
232
233
    def _msg_per_cpu(self, data, key, max_width):
234
        """Create per-cpu view"""
235
        ret = []
236
237
        # Get history (only used with the sparkline option)
238
        if type(data[key]).__name__ == 'Sparkline':
239
            raw_cpu = self.get_raw_history(item='percpu', nb=data[key].size)
240
241
        # Manage the maximum number of CPU to display (related to enhancement request #2734)
242
        if len(self.stats['percpu']) > self.max_cpu_display:
243
            # If the number of CPU is > max_cpu_display then sort and display top 'n'
244
            percpu_list = sorted(self.stats['percpu'], key=lambda x: x['total'], reverse=True)
245
        else:
246
            percpu_list = self.stats['percpu']
247
248
        # Display the first max_cpu_display CPU
249
        for cpu in percpu_list[0 : self.max_cpu_display]:
250
            cpu_id = cpu[cpu['key']]
251
            if type(data[key]).__name__ == 'Sparkline':
252
                # Sparkline display an history
253
                data[key].percents = [i[1][cpu_id]['total'] for i in raw_cpu]
0 ignored issues
show
The variable raw_cpu does not seem to be defined in case type(SubscriptNode).__name__ == 'Sparkline' on line 238 is False. Are you sure this can never be the case?
Loading history...
254
                # A simple padding in order to align metrics to the right
255
                data[key].percents += [None] * (data[key].size - len(data[key].percents))
256
            else:
257
                # Bar will only display the last value
258
                data[key].percent = cpu['total']
259
            if cpu_id < 10:
260
                msg = f'{key.upper():3}{cpu_id} '
261
            else:
262
                msg = f'{cpu_id:4} '
263
            ret.extend(self._msg_create_line(msg, data[key], key))
264
            ret.append(self.curse_new_line())
265
266
        # Add a new line with sum of all others CPU
267
        if len(self.stats['percpu']) > self.max_cpu_display:
268
            if type(data[key]).__name__ == 'Sparkline':
269
                sum_other = Sparkline(max_width)
270
                # Sparkline display an history
271
                sum_other.percents = [
272
                    sum([i['total'] for i in r[1] if i[i['key']] >= self.max_cpu_display])
273
                    / len([i['total'] for i in r[1] if i[i['key']] >= self.max_cpu_display])
274
                    for r in raw_cpu
275
                ]
276
                # A simple padding in order to align metrics to the right
277
                sum_other.percents += [None] * (sum_other.size - len(sum_other.percents))
278
            else:
279
                # Bar will only display the last value
280
                sum_other = Bar(max_width, bar_char=self.get_conf_value('bar_char', default=['|'])[0])
281
                sum_other.percent = sum([i['total'] for i in percpu_list[self.max_cpu_display :]]) / len(
282
                    percpu_list[self.max_cpu_display :]
283
                )
284
            msg = msg = f'{key.upper():3}* '
285
            ret.extend(self._msg_create_line(msg, sum_other, key))
286
            ret.append(self.curse_new_line())
287
288
        return ret
289
290
    def _msg_create_line(self, msg, data, key):
291
        """Create a new line to the Quick view."""
292
        return [
293
            self.curse_add_line(msg),
294
            self.curse_add_line(data.pre_char, decoration='BOLD'),
295
            self.curse_add_line(data.get(), self.get_views(key=key, option='decoration')),
296
            self.curse_add_line(data.post_char, decoration='BOLD'),
297
            self.curse_add_line('  '),
298
        ]
299
300
    def _hz_to_ghz(self, hz):
301
        """Convert Hz to Ghz."""
302
        return hz / 1000000000.0
303
304
    def _mhz_to_hz(self, hz):
305
        """Convert Mhz to Hz."""
306
        return hz * 1000000.0
307