diff --git a/glances/plugins/fs/zfs.py b/glances/plugins/fs/zfs.py new file mode 100644 index 00000000..6eb44658 --- /dev/null +++ b/glances/plugins/fs/zfs.py @@ -0,0 +1,34 @@ +# +# This file is part of Glances. +# +# SPDX-FileCopyrightText: 2025 Nicolas Hennion +# +# SPDX-License-Identifier: LGPL-3.0-only +# + +# For the moment, thoses functions are only used in the MEM plugin (see #3979) + +import os + +from glances.logger import logger + + +def zfs_enable(zfs_stats_path='/proc/spl/kstat/zfs'): + """Check if ZFS is enabled on this system.""" + return os.path.isdir(zfs_stats_path) + + +def zfs_stats(zfs_stats_files=['/proc/spl/kstat/zfs/arcstats']): + """Get ZFS stats from /proc/spl/kstat/zfs files.""" + stats = {} + for zfs_stats_file in zfs_stats_files: + try: + with open(zfs_stats_file) as f: + lines = f.readlines() + namespace = os.path.basename(zfs_stats_file) + for line in lines[2:]: # Skip the first two header lines + parts = line.split() + stats[namespace + '.' + parts[0]] = int(parts[2]) + except Exception as e: + logger.error(f"Error reading ZFS stats in {zfs_stats_file}: {e}") + return stats diff --git a/glances/plugins/mem/__init__.py b/glances/plugins/mem/__init__.py index 7eea9694..97a194ba 100644 --- a/glances/plugins/mem/__init__.py +++ b/glances/plugins/mem/__init__.py @@ -1,7 +1,7 @@ # # This file is part of Glances. # -# SPDX-FileCopyrightText: 2022 Nicolas Hennion +# SPDX-FileCopyrightText: 2025 Nicolas Hennion # # SPDX-License-Identifier: LGPL-3.0-only # @@ -10,6 +10,7 @@ import psutil +from glances.plugins.fs.zfs import zfs_enable, zfs_stats from glances.plugins.plugin.model import GlancesPluginModel # Fields description @@ -124,6 +125,9 @@ class MemPlugin(GlancesPluginModel): args=args, config=config, items_history_list=items_history_list, fields_description=fields_description ) + # ZFS + self.zfs_enabled = zfs_enable() + # We want to display the stat in the curse interface self.display_curse = True @@ -167,6 +171,26 @@ class MemPlugin(GlancesPluginModel): if hasattr(vm_stats, mem): stats[mem] = getattr(vm_stats, mem) + # Manage ZFS cache (see #3979 for details) + if self.zfs_enabled: + zfs_shrink = 0 + zfs_cache_stats = zfs_stats() + # Uncomment the following line to use the test data + # zfs_cache_stats = zfs_stats(['./tests-data/plugins/fs/zfs/arcstats']) + if 'arcstats.size' in zfs_cache_stats: + zfs_size = zfs_cache_stats['arcstats.size'] + if 'arcstats.c_min' in zfs_cache_stats: + zfs_cmin = zfs_cache_stats['arcstats.c_min'] + else: + zfs_cmin = 0 + if zfs_size > zfs_cmin: + zfs_shrink = zfs_size - zfs_cmin + # Add the ZFS cache to the 'cached' memory + if 'cached' in stats: + stats['cached'] += zfs_shrink + else: + stats['cached'] = zfs_shrink + # Use the 'free'/htop calculation # free=available+buffer+cached stats['free'] = stats['available'] diff --git a/pyproject.toml b/pyproject.toml index c8414407..d63e4444 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -247,3 +247,6 @@ log_cli = false log_level = "INFO" #log_cli_format = "%(asctime)s [%(levelname)8s] %(message)s (%(filename)s:%(lineno)s)" #log_cli_date_format = "%Y-%m-%d %H:%M:%S" + +[tool.flake8] +max-line-length = 120 diff --git a/tests-data/plugins/fs/zfs/arcstats b/tests-data/plugins/fs/zfs/arcstats new file mode 100644 index 00000000..cbbb066d --- /dev/null +++ b/tests-data/plugins/fs/zfs/arcstats @@ -0,0 +1,149 @@ +9 1 0x01 147 39984 22179924650 77805722884984 +name type data +hits 4 163573507 +iohits 4 1112419 +misses 4 16368761 +demand_data_hits 4 72451565 +demand_data_iohits 4 803830 +demand_data_misses 4 2130504 +demand_metadata_hits 4 90322004 +demand_metadata_iohits 4 33395 +demand_metadata_misses 4 1259616 +prefetch_data_hits 4 5100 +prefetch_data_iohits 4 54 +prefetch_data_misses 4 12846585 +prefetch_metadata_hits 4 794838 +prefetch_metadata_iohits 4 275140 +prefetch_metadata_misses 4 132056 +mru_hits 4 40587444 +mru_ghost_hits 4 119612 +mfu_hits 4 122986063 +mfu_ghost_hits 4 792 +uncached_hits 4 0 +deleted 4 240714336 +mutex_miss 4 29651 +access_skip 4 17 +evict_skip 4 3 +evict_not_enough 4 0 +evict_l2_cached 4 779348272640 +evict_l2_eligible 4 3032617077760 +evict_l2_eligible_mfu 4 64470016 +evict_l2_eligible_mru 4 3032552607744 +evict_l2_ineligible 4 112752640 +evict_l2_skip 4 0 +hash_elements 4 4708410 +hash_elements_max 4 9041764 +hash_collisions 4 80682806 +hash_chains 4 549398 +hash_chain_max 4 8 +meta 4 1120493253 +pd 4 2140686966 +pm 4 2225035009 +c 4 60312499360 +c_min 4 2637352832 +c_max 4 69793218560 +size 4 41321273080 +compressed_size 4 35897732096 +uncompressed_size 4 65567515648 +overhead_size 4 2501788672 +hdr_size 4 1134386336 +data_size 4 32250192896 +metadata_size 4 6149327872 +dbuf_size 4 450035208 +dnode_size 4 984383760 +bonus_size 4 308172480 +anon_size 4 20157952 +anon_data 4 19607552 +anon_metadata 4 550400 +anon_evictable_data 4 0 +anon_evictable_metadata 4 0 +mru_size 4 35167577600 +mru_data 4 29692877824 +mru_metadata 4 5474699776 +mru_evictable_data 4 27496793600 +mru_evictable_metadata 4 4834513920 +mru_ghost_size 4 21166751744 +mru_ghost_data 4 2506145792 +mru_ghost_metadata 4 18660605952 +mru_ghost_evictable_data 4 2506145792 +mru_ghost_evictable_metadata 4 18660605952 +mfu_size 4 3211785216 +mfu_data 4 2537707520 +mfu_metadata 4 674077696 +mfu_evictable_data 4 1617436160 +mfu_evictable_metadata 4 204706304 +mfu_ghost_size 4 76094464 +mfu_ghost_data 4 76094464 +mfu_ghost_metadata 4 0 +mfu_ghost_evictable_data 4 76094464 +mfu_ghost_evictable_metadata 4 0 +uncached_size 4 0 +uncached_data 4 0 +uncached_metadata 4 0 +uncached_evictable_data 4 0 +uncached_evictable_metadata 4 0 +l2_hits 4 143402 +l2_misses 4 12645465 +l2_prefetch_asize 4 6069248 +l2_mru_asize 4 361556480 +l2_mfu_asize 4 1765280768 +l2_bufc_data_asize 4 2019670016 +l2_bufc_metadata_asize 4 113236480 +l2_feeds 4 127414 +l2_rw_clash 4 0 +l2_read_bytes 4 1315843584 +l2_write_bytes 4 763235489792 +l2_writes_sent 4 90085 +l2_writes_done 4 90085 +l2_writes_error 4 0 +l2_writes_lock_retry 4 256 +l2_evict_lock_retry 4 93 +l2_evict_reading 4 0 +l2_evict_l1cached 4 12729 +l2_free_on_write 4 76388 +l2_abort_lowmem 4 18 +l2_cksum_bad 4 0 +l2_io_error 4 0 +l2_size 4 3095654400 +l2_asize 4 2132906496 +l2_hdr_size 4 852096 +l2_log_blk_writes 4 6134 +l2_log_blk_avg_asize 4 16195 +l2_log_blk_asize 4 60463616 +l2_log_blk_count 4 4242 +l2_data_to_meta_ratio 4 220 +l2_rebuild_success 4 0 +l2_rebuild_unsupported 4 1 +l2_rebuild_io_errors 4 0 +l2_rebuild_dh_errors 4 0 +l2_rebuild_cksum_lb_errors 4 0 +l2_rebuild_lowmem 4 0 +l2_rebuild_size 4 0 +l2_rebuild_asize 4 0 +l2_rebuild_bufs 4 0 +l2_rebuild_bufs_precached 4 0 +l2_rebuild_log_blks 4 0 +memory_throttle_count 4 0 +memory_direct_count 4 0 +memory_indirect_count 4 2690 +memory_all_bytes 4 84395290624 +memory_free_bytes 4 26664001536 +memory_available_bytes 3 23747772544 +arc_no_grow 4 0 +arc_tempreserve 4 0 +arc_loaned_bytes 4 0 +arc_prune 4 0 +arc_meta_used 4 9027157752 +arc_dnode_limit 4 6979321856 +async_upgrade_sync 4 804060 +predictive_prefetch 4 14053359 +demand_hit_predictive_prefetch 4 12183015 +demand_iohit_predictive_prefetch 4 828711 +prescient_prefetch 4 414 +demand_hit_prescient_prefetch 4 333 +demand_iohit_prescient_prefetch 4 81 +arc_need_free 4 0 +arc_sys_free 4 2916228992 +arc_raw_size 4 0 +cached_only_in_progress 4 0 +abd_chunk_waste_size 4 43922432 \ No newline at end of file diff --git a/tests/test_core.py b/tests/test_core.py index 4cccaee8..8a62bd72 100755 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -29,6 +29,7 @@ from glances.filter import GlancesFilter, GlancesFilterList from glances.globals import LINUX, WINDOWS, pretty_date, string_value_to_float, subsample from glances.main import GlancesMain from glances.outputs.glances_bars import Bar +from glances.plugins.fs.zfs import zfs_enable, zfs_stats from glances.plugins.plugin.model import GlancesPluginModel from glances.stats import GlancesStats from glances.thresholds import ( @@ -671,6 +672,17 @@ class TestGlances(unittest.TestCase): print('INFO: [TEST_107] Test fs plugin methods') self._common_plugin_tests('fs') + def test_108_fs_zfs_(self): + """Test zfs functions""" + print('INFO: [TEST_108] Test zfs functions') + self.assertTrue(zfs_enable('./tests-data/plugins/fs/zfs')) + stats = zfs_stats(['./tests-data/plugins/fs/zfs/arcstats']) + self.assertTrue(isinstance(stats, dict)) + self.assertTrue('arcstats.c_min' in stats) + self.assertEqual(stats['arcstats.c_min'], 2637352832) + self.assertTrue('arcstats.size' in stats) + self.assertEqual(stats['arcstats.size'], 41321273080) + def test_200_views_hidden(self): """Test hide feature""" print('INFO: [TEST_200] Test views hidden feature')