Completed
Push — master ( 3dcd25...20576f )
by Nicolas
01:26
created

glances/plugins/glances_plugin.py (1 issue)

1
# -*- coding: utf-8 -*-
2
#
3
# This file is part of Glances.
4
#
5
# Copyright (C) 2015 Nicolargo <[email protected]>
6
#
7
# Glances is free software; you can redistribute it and/or modify
8
# it under the terms of the GNU Lesser General Public License as published by
9
# the Free Software Foundation, either version 3 of the License, or
10
# (at your option) any later version.
11
#
12
# Glances is distributed in the hope that it will be useful,
13
# but WITHOUT ANY WARRANTY; without even the implied warranty of
14
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
# GNU Lesser General Public License for more details.
16
#
17
# You should have received a copy of the GNU Lesser General Public License
18
# along with this program. If not, see <http://www.gnu.org/licenses/>.
19
20
"""
21
I am your father...
22
23
...for all Glances plugins.
24
"""
25
26
import re
27
import json
28
from operator import itemgetter
29
30
from glances.compat import iterkeys, itervalues, listkeys, map
31
from glances.actions import GlancesActions
32
from glances.history import GlancesHistory
33
from glances.logger import logger
34
from glances.logs import glances_logs
35
36
37
class GlancesPlugin(object):
38
39
    """Main class for Glances plugin."""
40
41
    def __init__(self, args=None, items_history_list=None):
42
        """Init the plugin of plugins class."""
43
        # Plugin name (= module name without glances_)
44
        self.plugin_name = self.__class__.__module__[len('glances_'):]
45
        # logger.debug("Init plugin %s" % self.plugin_name)
46
47
        # Init the args
48
        self.args = args
49
50
        # Init the default alignement (for curses)
51
        self._align = 'left'
52
53
        # Init the input method
54
        self._input_method = 'local'
55
        self._short_system_name = None
56
57
        # Init the stats list
58
        self.stats = None
59
60
        # Init the history list
61
        self.items_history_list = items_history_list
62
        self.stats_history = self.init_stats_history()
63
64
        # Init the limits dictionnary
65
        self._limits = dict()
66
67
        # Init the actions
68
        self.actions = GlancesActions(args=args)
69
70
        # Init the views
71
        self.views = dict()
72
73
    def exit(self):
74
        """Method to be called when Glances exit"""
75
        logger.debug("Stop the {} plugin".format(self.plugin_name))
76
77
    def __repr__(self):
78
        """Return the raw stats."""
79
        return self.stats
80
81
    def __str__(self):
82
        """Return the human-readable stats."""
83
        return str(self.stats)
84
85
    def get_key(self):
86
        """Return the key of the list."""
87
        return None
88
89
    def _json_dumps(self, d):
90
        """Return the object 'd' in a JSON format
91
        Manage the issue #815 for Windows OS"""
92
        try:
93
            return json.dumps(d)
94
        except UnicodeDecodeError:
95
            return json.dumps(d, ensure_ascii=False)
96
97
    def _history_enable(self):
98
        return self.args is not None and not self.args.disable_history and self.get_items_history_list() is not None
99
100
    def init_stats_history(self):
101
        """Init the stats history (dict of GlancesAttribute)."""
102
        if self._history_enable():
103
            init_list = [a['name'] for a in self.get_items_history_list()]
104
            logger.debug("Stats history activated for plugin {0} (items: {1})".format(self.plugin_name, init_list))
105
        return GlancesHistory()
106
107
    def reset_stats_history(self):
108
        """Reset the stats history (dict of GlancesAttribute)."""
109
        if self._history_enable():
110
            reset_list = [a['name'] for a in self.get_items_history_list()]
111
            logger.debug("Reset history for plugin {0} (items: {1})".format(self.plugin_name, reset_list))
112
            self.stats_history.reset()
113
114
    def update_stats_history(self, item_name=''):
115
        """Update stats history."""
116
        if self.stats and self._history_enable():
117
            for i in self.get_items_history_list():
118
                if isinstance(self.stats, list):
119
                    # Stats is a list of data
120
                    # Iter throught it (for exemple, iter throught network
121
                    # interface)
122
                    for l in self.stats:
123
                        self.stats_history.add(
124
                            l[item_name] + '_' + i['name'],
125
                            l[i['name']],
126
                            description=i['description'],
127
                            history_max_size=self._limits['history_size'])
128
                else:
129
                    # Stats is not a list
130
                    # Add the item to the history directly
131
                    self.stats_history.add(i['name'],
132
                                           self.stats[i['name']],
133
                                           description=i['description'],
134
                                           history_max_size=self._limits['history_size'])
135
136
    def get_items_history_list(self):
137
        """Return the items history list."""
138
        return self.items_history_list
139
140
    def get_raw_history(self, item=None):
141
        """Return
142
        - the stats history (dict of list) if item is None
143
        - the stats history for the given item (list) instead
144
        - None if item did not exist in the history"""
145
        s = self.stats_history.get()
146
        if item is None:
147
            return s
148
        else:
149
            if item in s:
150
                return s[item]
151
            else:
152
                return None
153
154
    def get_json_history(self, item=None, nb=0):
155
        """Return:
156
        - the stats history (dict of list) if item is None
157
        - the stats history for the given item (list) instead
158
        - None if item did not exist in the history
159
        Limit to lasts nb items (all if nb=0)"""
160
        s = self.stats_history.get_json(nb=nb)
161
        if item is None:
162
            return s
163
        else:
164
            if item in s:
165
                return s[item]
166
            else:
167
                return None
168
169
    def get_export_history(self, item=None):
170
        """Return the stats history object to export.
171
        See get_raw_history for a full description"""
172
        return self.get_raw_history(item=item)
173
174 View Code Duplication
    def get_stats_history(self, item=None, nb=0):
175
        """Return the stats history as a JSON object (dict or None).
176
        Limit to lasts nb items (all if nb=0)"""
177
        s = self.get_json_history(nb=nb)
178
179
        if item is None:
180
            return self._json_dumps(s)
181
182
        if isinstance(s, dict):
183
            try:
184
                return self._json_dumps({item: s[item]})
185
            except KeyError as e:
186
                logger.error("Cannot get item history {0} ({1})".format(item, e))
187
                return None
188
        elif isinstance(s, list):
189
            try:
190
                # Source:
191
                # http://stackoverflow.com/questions/4573875/python-get-index-of-dictionary-item-in-list
192
                return self._json_dumps({item: map(itemgetter(item), s)})
193
            except (KeyError, ValueError) as e:
194
                logger.error("Cannot get item history {0} ({1})".format(item, e))
195
                return None
196
        else:
197
            return None
198
199
    @property
200
    def input_method(self):
201
        """Get the input method."""
202
        return self._input_method
203
204
    @input_method.setter
205
    def input_method(self, input_method):
206
        """Set the input method.
207
208
        * local: system local grab (psutil or direct access)
209
        * snmp: Client server mode via SNMP
210
        * glances: Client server mode via Glances API
211
        """
212
        self._input_method = input_method
213
214
    @property
215
    def short_system_name(self):
216
        """Get the short detected OS name (SNMP)."""
217
        return self._short_system_name
218
219
    @short_system_name.setter
220
    def short_system_name(self, short_name):
221
        """Set the short detected OS name (SNMP)."""
222
        self._short_system_name = short_name
223
224
    def set_stats(self, input_stats):
225
        """Set the stats to input_stats."""
226
        self.stats = input_stats
227
228
    def get_stats_snmp(self, bulk=False, snmp_oid=None):
229
        """Update stats using SNMP.
230
231
        If bulk=True, use a bulk request instead of a get request.
232
        """
233
        snmp_oid = snmp_oid or {}
234
235
        from glances.snmp import GlancesSNMPClient
236
237
        # Init the SNMP request
238
        clientsnmp = GlancesSNMPClient(host=self.args.client,
239
                                       port=self.args.snmp_port,
240
                                       version=self.args.snmp_version,
241
                                       community=self.args.snmp_community)
242
243
        # Process the SNMP request
244
        ret = {}
245
        if bulk:
246
            # Bulk request
247
            snmpresult = clientsnmp.getbulk_by_oid(0, 10, itervalues(*snmp_oid))
248
249
            if len(snmp_oid) == 1:
250
                # Bulk command for only one OID
251
                # Note: key is the item indexed but the OID result
252
                for item in snmpresult:
253
                    if iterkeys(item)[0].startswith(itervalues(snmp_oid)[0]):
254
                        ret[iterkeys(snmp_oid)[0] + iterkeys(item)
255
                            [0].split(itervalues(snmp_oid)[0])[1]] = itervalues(item)[0]
256
            else:
257
                # Build the internal dict with the SNMP result
258
                # Note: key is the first item in the snmp_oid
259
                index = 1
260
                for item in snmpresult:
261
                    item_stats = {}
262
                    item_key = None
263
                    for key in iterkeys(snmp_oid):
264
                        oid = snmp_oid[key] + '.' + str(index)
265
                        if oid in item:
266
                            if item_key is None:
267
                                item_key = item[oid]
268
                            else:
269
                                item_stats[key] = item[oid]
270
                    if item_stats:
271
                        ret[item_key] = item_stats
272
                    index += 1
273
        else:
274
            # Simple get request
275
            snmpresult = clientsnmp.get_by_oid(itervalues(*snmp_oid))
276
277
            # Build the internal dict with the SNMP result
278
            for key in iterkeys(snmp_oid):
279
                ret[key] = snmpresult[snmp_oid[key]]
280
281
        return ret
282
283
    def get_raw(self):
284
        """Return the stats object."""
285
        return self.stats
286
287
    def get_export(self):
288
        """Return the stats object to export."""
289
        return self.get_raw()
290
291
    def get_stats(self):
292
        """Return the stats object in JSON format."""
293
        return self._json_dumps(self.stats)
294
295 View Code Duplication
    def get_stats_item(self, item):
0 ignored issues
show
This code seems to be duplicated in your project.
Loading history...
296
        """Return the stats object for a specific item in JSON format.
297
298
        Stats should be a list of dict (processlist, network...)
299
        """
300
        if isinstance(self.stats, dict):
301
            try:
302
                return self._json_dumps({item: self.stats[item]})
303
            except KeyError as e:
304
                logger.error("Cannot get item {} ({})".format(item, e))
305
                return None
306
        elif isinstance(self.stats, list):
307
            try:
308
                # Source:
309
                # http://stackoverflow.com/questions/4573875/python-get-index-of-dictionary-item-in-list
310
                return self._json_dumps({item: map(itemgetter(item), self.stats)})
311
            except (KeyError, ValueError) as e:
312
                logger.error("Cannot get item {} ({})".format(item, e))
313
                return None
314
        else:
315
            return None
316
317
    def get_stats_value(self, item, value):
318
        """Return the stats object for a specific item=value in JSON format.
319
320
        Stats should be a list of dict (processlist, network...)
321
        """
322
        if not isinstance(self.stats, list):
323
            return None
324
        else:
325
            if value.isdigit():
326
                value = int(value)
327
            try:
328
                return self._json_dumps({value: [i for i in self.stats if i[item] == value]})
329
            except (KeyError, ValueError) as e:
330
                logger.error(
331
                    "Cannot get item({})=value({}) ({})".format(item, value, e))
332
                return None
333
334
    def update_views(self):
335
        """Default builder fo the stats views.
336
337
        The V of MVC
338
        A dict of dict with the needed information to display the stats.
339
        Example for the stat xxx:
340
        'xxx': {'decoration': 'DEFAULT',
341
                'optional': False,
342
                'additional': False,
343
                'splittable': False}
344
        """
345
        ret = {}
346
347
        if (isinstance(self.get_raw(), list) and
348
                self.get_raw() is not None and
349
                self.get_key() is not None):
350
            # Stats are stored in a list of dict (ex: NETWORK, FS...)
351
            for i in self.get_raw():
352
                ret[i[self.get_key()]] = {}
353
                for key in listkeys(i):
354
                    value = {'decoration': 'DEFAULT',
355
                             'optional': False,
356
                             'additional': False,
357
                             'splittable': False}
358
                    ret[i[self.get_key()]][key] = value
359
        elif isinstance(self.get_raw(), dict) and self.get_raw() is not None:
360
            # Stats are stored in a dict (ex: CPU, LOAD...)
361
            for key in listkeys(self.get_raw()):
362
                value = {'decoration': 'DEFAULT',
363
                         'optional': False,
364
                         'additional': False,
365
                         'splittable': False}
366
                ret[key] = value
367
368
        self.views = ret
369
370
        return self.views
371
372
    def set_views(self, input_views):
373
        """Set the views to input_views."""
374
        self.views = input_views
375
376
    def get_views(self, item=None, key=None, option=None):
377
        """Return the views object.
378
379
        If key is None, return all the view for the current plugin
380
        else if option is None return the view for the specific key (all option)
381
        else return the view fo the specific key/option
382
383
        Specify item if the stats are stored in a dict of dict (ex: NETWORK, FS...)
384
        """
385
        if item is None:
386
            item_views = self.views
387
        else:
388
            item_views = self.views[item]
389
390
        if key is None:
391
            return item_views
392
        else:
393
            if option is None:
394
                return item_views[key]
395
            else:
396
                return item_views[key][option]
397
398
    def load_limits(self, config):
399
        """Load limits from the configuration file, if it exists."""
400
401
        # By default set the history length to 3 points per second during one day
402
        self._limits['history_size'] = 28800
403
404
        if not hasattr(config, 'has_section'):
405
            return False
406
407
        # Read the global section
408
        if config.has_section('global'):
409
            self._limits['history_size'] = config.get_float_value('global', 'history_size', default=28800)
410
            logger.debug("Load configuration key: {0} = {1}".format('history_size', self._limits['history_size']))
411
412
        # Read the plugin specific section
413
        if config.has_section(self.plugin_name):
414
            for level, _ in config.items(self.plugin_name):
415
                # Read limits
416
                limit = '_'.join([self.plugin_name, level])
417
                try:
418
                    self._limits[limit] = config.get_float_value(self.plugin_name, level)
419
                except ValueError:
420
                    self._limits[limit] = config.get_value(self.plugin_name, level).split(",")
421
                logger.debug("Load limit: {} = {}".format(limit, self._limits[limit]))
422
423
        return True
424
425
    @property
426
    def limits(self):
427
        """Return the limits object."""
428
        return self._limits
429
430
    @limits.setter
431
    def limits(self, input_limits):
432
        """Set the limits to input_limits."""
433
        self._limits = input_limits
434
435
    def get_alert(self, current=0, minimum=0, maximum=100, header="", log=False):
436
        """Return the alert status relative to a current value.
437
438
        Use this function for minor stats.
439
440
        If current < CAREFUL of max then alert = OK
441
        If current > CAREFUL of max then alert = CAREFUL
442
        If current > WARNING of max then alert = WARNING
443
        If current > CRITICAL of max then alert = CRITICAL
444
445
        If defined 'header' is added between the plugin name and the status.
446
        Only useful for stats with several alert status.
447
448
        If log=True than add log if necessary
449
        elif log=False than do not log
450
        elig log=None than apply the config given in the conf file
451
        """
452
        # Compute the %
453
        try:
454
            value = (current * 100) / maximum
455
        except ZeroDivisionError:
456
            return 'DEFAULT'
457
        except TypeError:
458
            return 'DEFAULT'
459
460
        # Build the stat_name = plugin_name + header
461
        if header == "":
462
            stat_name = self.plugin_name
463
        else:
464
            stat_name = self.plugin_name + '_' + header
465
466
        # Manage limits
467
        ret = 'OK'
468
        try:
469
            if value > self.__get_limit('critical', stat_name=stat_name):
470
                ret = 'CRITICAL'
471
            elif value > self.__get_limit('warning', stat_name=stat_name):
472
                ret = 'WARNING'
473
            elif value > self.__get_limit('careful', stat_name=stat_name):
474
                ret = 'CAREFUL'
475
            elif current < minimum:
476
                ret = 'CAREFUL'
477
        except KeyError:
478
            return 'DEFAULT'
479
480
        # Manage log
481
        log_str = ""
482
        if self.__get_limit_log(stat_name=stat_name, default_action=log):
483
            # Add _LOG to the return string
484
            # So stats will be highlited with a specific color
485
            log_str = "_LOG"
486
            # Add the log to the list
487
            glances_logs.add(ret, stat_name.upper(), value)
488
489
        # Manage action
490
        # Here is a command line for the current trigger ?
491
        try:
492
            command = self.__get_limit_action(ret.lower(), stat_name=stat_name)
493
        except KeyError:
494
            # Reset the trigger
495
            self.actions.set(stat_name, ret.lower())
496
        else:
497
            # A command line is available for the current alert, run it
498
            # Build the {{mustache}} dictionnary
499
            if isinstance(self.stats, list):
500
                # If the stats are stored in a list of dict (fs plugin for exemple)
501
                # Return the dict for the current header
502
                mustache_dict = {}
503
                for item in self.stats:
504
                    if item[self.get_key()] == header:
505
                        mustache_dict = item
506
                        break
507
            else:
508
                # Use the stats dict
509
                mustache_dict = self.stats
510
            # Run the action
511
            self.actions.run(
512
                stat_name, ret.lower(), command, mustache_dict=mustache_dict)
513
514
        # Default is ok
515
        return ret + log_str
516
517
    def get_alert_log(self, current=0, minimum=0, maximum=100, header=""):
518
        """Get the alert log."""
519
        return self.get_alert(current, minimum, maximum, header, log=True)
520
521
    def __get_limit(self, criticity, stat_name=""):
522
        """Return the limit value for the alert."""
523
        # Get the limit for stat + header
524
        # Exemple: network_wlan0_rx_careful
525
        try:
526
            limit = self._limits[stat_name + '_' + criticity]
527
        except KeyError:
528
            # Try fallback to plugin default limit
529
            # Exemple: network_careful
530
            limit = self._limits[self.plugin_name + '_' + criticity]
531
532
        # logger.debug("{} {} value is {}".format(stat_name, criticity, limit))
533
534
        # Return the limit
535
        return limit
536
537
    def __get_limit_action(self, criticity, stat_name=""):
538
        """Return the action for the alert."""
539
        # Get the action for stat + header
540
        # Exemple: network_wlan0_rx_careful_action
541
        try:
542
            ret = self._limits[stat_name + '_' + criticity + '_action']
543
        except KeyError:
544
            # Try fallback to plugin default limit
545
            # Exemple: network_careful_action
546
            ret = self._limits[self.plugin_name + '_' + criticity + '_action']
547
548
        # Return the action list
549
        return ret
550
551
    def __get_limit_log(self, stat_name, default_action=False):
552
        """Return the log tag for the alert."""
553
        # Get the log tag for stat + header
554
        # Exemple: network_wlan0_rx_log
555
        try:
556
            log_tag = self._limits[stat_name + '_log']
557
        except KeyError:
558
            # Try fallback to plugin default log
559
            # Exemple: network_log
560
            try:
561
                log_tag = self._limits[self.plugin_name + '_log']
562
            except KeyError:
563
                # By defaukt, log are disabled
564
                return default_action
565
566
        # Return the action list
567
        return log_tag[0].lower() == 'true'
568
569
    def get_conf_value(self, value, header="", plugin_name=None):
570
        """Return the configuration (header_) value for the current plugin.
571
572
        ...or the one given by the plugin_name var.
573
        """
574
        if plugin_name is None:
575
            # If not default use the current plugin name
576
            plugin_name = self.plugin_name
577
578
        if header != "":
579
            # Add the header
580
            plugin_name = plugin_name + '_' + header
581
582
        try:
583
            return self._limits[plugin_name + '_' + value]
584
        except KeyError:
585
            return []
586
587
    def is_hide(self, value, header=""):
588
        """
589
        Return True if the value is in the hide configuration list.
590
        The hide configuration list is defined in the glances.conf file.
591
        It is a comma separed list of regexp.
592
        Example for diskio:
593
        hide=sda2,sda5,loop.*
594
        """
595
        # TODO: possible optimisation: create a re.compile list
596
        return not all(j is None for j in [re.match(i, value) for i in self.get_conf_value('hide', header=header)])
597
598
    def has_alias(self, header):
599
        """Return the alias name for the relative header or None if nonexist."""
600
        try:
601
            return self._limits[self.plugin_name + '_' + header + '_' + 'alias'][0]
602
        except (KeyError, IndexError):
603
            return None
604
605
    def msg_curse(self, args=None, max_width=None):
606
        """Return default string to display in the curse interface."""
607
        return [self.curse_add_line(str(self.stats))]
608
609
    def get_stats_display(self, args=None, max_width=None):
610
        """Return a dict with all the information needed to display the stat.
611
612
        key     | description
613
        ----------------------------
614
        display | Display the stat (True or False)
615
        msgdict | Message to display (list of dict [{ 'msg': msg, 'decoration': decoration } ... ])
616
        align   | Message position
617
        """
618
        display_curse = False
619
620
        if hasattr(self, 'display_curse'):
621
            display_curse = self.display_curse
622
        if hasattr(self, 'align'):
623
            align_curse = self._align
624
625
        if max_width is not None:
626
            ret = {'display': display_curse,
627
                   'msgdict': self.msg_curse(args, max_width=max_width),
628
                   'align': align_curse}
629
        else:
630
            ret = {'display': display_curse,
631
                   'msgdict': self.msg_curse(args),
632
                   'align': align_curse}
633
634
        return ret
635
636
    def curse_add_line(self, msg, decoration="DEFAULT",
637
                       optional=False, additional=False,
638
                       splittable=False):
639
        """Return a dict with.
640
641
        Where:
642
            msg: string
643
            decoration:
644
                DEFAULT: no decoration
645
                UNDERLINE: underline
646
                BOLD: bold
647
                TITLE: for stat title
648
                PROCESS: for process name
649
                STATUS: for process status
650
                NICE: for process niceness
651
                CPU_TIME: for process cpu time
652
                OK: Value is OK and non logged
653
                OK_LOG: Value is OK and logged
654
                CAREFUL: Value is CAREFUL and non logged
655
                CAREFUL_LOG: Value is CAREFUL and logged
656
                WARNING: Value is WARINING and non logged
657
                WARNING_LOG: Value is WARINING and logged
658
                CRITICAL: Value is CRITICAL and non logged
659
                CRITICAL_LOG: Value is CRITICAL and logged
660
            optional: True if the stat is optional (display only if space is available)
661
            additional: True if the stat is additional (display only if space is available after optional)
662
            spittable: Line can be splitted to fit on the screen (default is not)
663
        """
664
        return {'msg': msg, 'decoration': decoration, 'optional': optional, 'additional': additional, 'splittable': splittable}
665
666
    def curse_new_line(self):
667
        """Go to a new line."""
668
        return self.curse_add_line('\n')
669
670
    @property
671
    def align(self):
672
        """Get the curse align."""
673
        return self._align
674
675
    @align.setter
676
    def align(self, value):
677
        """Set the curse align.
678
679
        value: left, right, bottom.
680
        """
681
        self._align = value
682
683
    def auto_unit(self, number, low_precision=False):
684
        """Make a nice human-readable string out of number.
685
686
        Number of decimal places increases as quantity approaches 1.
687
688
        examples:
689
        CASE: 613421788        RESULT:       585M low_precision:       585M
690
        CASE: 5307033647       RESULT:      4.94G low_precision:       4.9G
691
        CASE: 44968414685      RESULT:      41.9G low_precision:      41.9G
692
        CASE: 838471403472     RESULT:       781G low_precision:       781G
693
        CASE: 9683209690677    RESULT:      8.81T low_precision:       8.8T
694
        CASE: 1073741824       RESULT:      1024M low_precision:      1024M
695
        CASE: 1181116006       RESULT:      1.10G low_precision:       1.1G
696
697
        'low_precision=True' returns less decimal places potentially
698
        sacrificing precision for more readability.
699
        """
700
        symbols = ('K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y')
701
        prefix = {
702
            'Y': 1208925819614629174706176,
703
            'Z': 1180591620717411303424,
704
            'E': 1152921504606846976,
705
            'P': 1125899906842624,
706
            'T': 1099511627776,
707
            'G': 1073741824,
708
            'M': 1048576,
709
            'K': 1024
710
        }
711
712
        for symbol in reversed(symbols):
713
            value = float(number) / prefix[symbol]
714
            if value > 1:
715
                decimal_precision = 0
716
                if value < 10:
717
                    decimal_precision = 2
718
                elif value < 100:
719
                    decimal_precision = 1
720
                if low_precision:
721
                    if symbol in 'MK':
722
                        decimal_precision = 0
723
                    else:
724
                        decimal_precision = min(1, decimal_precision)
725
                elif symbol in 'K':
726
                    decimal_precision = 0
727
                return '{:.{decimal}f}{symbol}'.format(
728
                    value, decimal=decimal_precision, symbol=symbol)
729
        return '{!s}'.format(number)
730
731
    def _log_result_decorator(fct):
732
        """Log (DEBUG) the result of the function fct."""
733
        def wrapper(*args, **kw):
734
            ret = fct(*args, **kw)
735
            logger.debug("%s %s %s return %s" % (
736
                args[0].__class__.__name__,
737
                args[0].__class__.__module__[len('glances_'):],
738
                fct.__name__, ret))
739
            return ret
740
        return wrapper
741
742
    # Mandatory to call the decorator in childs' classes
743
    _log_result_decorator = staticmethod(_log_result_decorator)
744