@cryptotaxi247 / netdata-1 / commits / 0f7ccc49d

feat(diskspace.plugin): smart ZFS dataset deduplication (#21643)

* feat(diskspace.plugin): smart ZFS dataset deduplication Replace unconditional ZFS dataset exclusion with a capacity-based heuristic that: - Excludes datasets that mirror pool capacity (no quota set) - Keeps datasets with quotas (different capacity than pool) - Keeps all datasets when pool itself is not mounted - Keeps all mounts inside LXC containers - Never excludes root filesystem New configuration options in [plugin:proc:diskspace]: - "zfs datasets heuristic" (default: yes) - enable smart heuristic - "exclude zfs datasets on paths" - manual pattern exclusion when heuristic is disabled Also exports is_lxcfs_proc_mounted() from proc.plugin for use by other collectors that need LXC container detection. * perf(diskspace.plugin): cache ZFS pool capacities with 5-min recheck Avoid calling statvfs() for ZFS mounts on every collection cycle. Pool capacities and exclusion decisions are now cached and only rechecked every 5 minutes (ZFS_DATASET_RECHECK_SECONDS). This eliminates the performance overhead of the two-pass heuristic while still adapting to quota changes within a reasonable timeframe. * fix(diskspace.plugin): address review comments for ZFS deduplication - Fix LXC detection to run unconditionally (once at startup) - Use "!*" as default pattern (explicitly exclude nothing) - Create dictionary/pattern only when needed for the active mode - Clarify tolerance comparison comments - Add mount_source to basic_mountinfo struct - Apply ZFS exclusion check to slow path (prevents duplicates) * fix(proc.plugin): clean up is_lxcfs_proc_mounted function Remove pointless conditional check (local variable was always NULL) and simplify control flow with single exit point for procfile_close(). * fix(diskspace.plugin): prevent crash on NULL mount_source and add defensive checks - Handle NULL mount_source in basic_mountinfo_create_and_copy() to prevent strdupz(NULL) crash when mountinfo parsing encounters errors - Add defensive NULL check after dictionary_set() in zfs_collect_pool_capacities() * fix(diskspace.plugin): correct misleading comment about LXC detection timing * fix(diskspace.plugin): prevent use-after-free race in ZFS pool info lookup Use dictionary_get_and_acquire_item() to hold a reference while reading pool info values. This prevents a race where the main thread could flush the dictionary (freeing values) while the slow worker thread is reading from a pointer returned by dictionary_get(). * fix(diskspace.plugin): use pool's own capacity for ZFS dataset comparison The previous logic tracked max_capacity across all ZFS mounts, but in ZFS statvfs returns total = used + available. This means datasets with data show larger "total" than empty datasets or the pool itself. For example: - rpool (pool): 1.7T total (empty) - rpool/ROOT/pve-1: 1.9T total (222G used + 1.7T available) Using max (1.9T) made empty datasets (1.7T) look like they had quotas. Fix: Track pool_capacity from the pool mount only, not datasets. Datasets with capacity >= pool capacity have no quota and should be excluded. Datasets with capacity < pool capacity have quotas and should be kept. * refactor(diskspace.plugin): always create ZFS dataset exclusion pattern Move zfs_exclude_datasets_pattern initialization alongside other exclusion patterns in do_disk_space_stats(). Create it unconditionally so the config option always appears in netdata.conf, allowing users to customize it even when starting with heuristic enabled and later switching to pattern mode. Renamed option to "exclude space metrics on zfs datasets" for consistency with other exclusion options. * Fix ZFS exclusion stopping after 5 minutes Bug: ZFS dataset exclusion worked for the first 5 minutes, then stopped. Datasets that should be excluded suddenly started being collected. Root cause: dictionary_flush() cleared all pool info entries every 5 minutes. If subsequent statvfs() calls failed (e.g., mount temporarily busy), the dictionary remained empty and should_exclude_zfs() couldn't find pool info, so it returned false (keep all datasets). Fix: Remove dictionary_flush() and update entries in place. The dictionary_set() overwrites existing entries when successful, but if statvfs fails, the previous valid pool info is preserved. This ensures stable exclusion behavior. Additional simplification: Moved statvfs() call before dictionary operations - only add/update pool info when we successfully get capacity data. * Refactor ZFS exclusion to cache dataset decisions Combine pool capacity and dataset exclusion into single cache dictionary. Previously we only cached pool capacities and checked every dataset on every collection cycle. Now we also cache the exclusion decision per dataset, avoiding statvfs() syscalls for excluded datasets. Changes: - Replace zfs_pool_info_dict with unified zfs_cache - Add zfs_dataset_cached_excluded() check before statvfs() - Per-entry timestamps instead of global zfs_last_checked - Add debug logs (to be removed after testing) Performance: For systems with hundreds of ZFS datasets, this reduces statvfs() calls from hundreds per second to just a few (only pools and datasets needing recheck). * Fix stale pool cache causing incorrect dataset exclusion If a ZFS pool was previously mounted and then unmounted, its cache entry persisted indefinitely. Datasets would continue to be excluded based on stale pool capacity, even though the design requires keeping all datasets when the pool is not mounted (so at least one mount reports pool capacity). Fix: Add freshness check before using pool_entry->pool_capacity. If the pool cache entry is older than ZFS_DATASET_RECHECK_SECONDS, treat the pool as unmounted and keep the dataset. * Use 2x interval for pool cache staleness check Using exactly ZFS_DATASET_RECHECK_SECONDS could cause race conditions where an entry is considered stale right as it's being refreshed. Using 2x interval means the entry must have missed at least one full refresh cycle before being considered stale, which reliably indicates the pool is no longer mounted. * Remove debug logs from ZFS dataset exclusion * Fix incorrect comment about ZFS dataset capacity

Ilya Mashchenko committed Jan 27, 2026 at 03:59 UTC 0f7ccc49db8eec3dbc636efb6f23bb3dfd29912d
3 files changed +294 -35
src/collectors/diskspace.plugin/plugin_diskspace.c
+276 -16
@@ -20,6 +20,32 @@ static struct mountinfo *disk_mountinfo_root = NULL;
20 static int check_for_new_mountpoints_every = 15;
21 static int cleanup_mount_points = 1;
22
23 +// ----------------------------------------------------------------------------
24 +// ZFS dataset deduplication
25 +//
26 +// ZFS datasets without quotas report the same capacity as their parent pool,
27 +// causing duplicate metrics and alert floods. This heuristic excludes datasets
28 +// that mirror pool capacity while keeping datasets with quotas.
29 +//
30 +// To avoid calling statvfs() on every dataset on every collection cycle,
31 +// we cache both pool capacities and dataset exclusion decisions.
32 +// Cache entries are refreshed every ZFS_DATASET_RECHECK_SECONDS.
33 +
34 +#define ZFS_DATASET_RECHECK_SECONDS 300 // recheck every 5 minutes
35 +
36 +// Combined cache for ZFS pools and datasets
37 +// Key: mount_source (e.g., "tank" for pool, "tank/home" for dataset)
38 +struct zfs_cache_entry {
39 + time_t last_checked; // when this entry was last checked
40 + bool is_pool; // true = pool, false = dataset
41 + uint64_t pool_capacity; // only for pools: capacity in bytes
42 + bool excluded; // only for datasets: true if excluded from monitoring
43 +};
44 +
45 +static DICTIONARY *zfs_cache = NULL;
46 +static SIMPLE_PATTERN *excluded_zfs_datasets_pattern = NULL;
47 +static int zfs_datasets_heuristic = CONFIG_BOOLEAN_YES;
48 +
49 static inline void mountinfo_reload(int force) {
50 static time_t last_loaded = 0;
51 time_t now = now_realtime_sec();
@@ -113,11 +139,12 @@ void mountpoint_delete_cb(const DICTIONARY_ITEM *item __maybe_unused, void *entr
139
140 // a copy of basic mountinfo fields
141 struct basic_mountinfo {
116 - char *persistent_id;
117 - char *root;
142 + char *persistent_id;
143 + char *root;
144 char *mount_point_stat_path;
119 - char *mount_point;
120 - char *filesystem;
145 + char *mount_point;
146 + char *mount_source;
147 + char *filesystem;
148
149 struct basic_mountinfo *next;
150 };
@@ -134,6 +161,7 @@ static struct basic_mountinfo *basic_mountinfo_create_and_copy(struct mountinfo*
161 bmi->root = strdupz(mi->root);
162 bmi->mount_point_stat_path = strdupz(mi->mount_point_stat_path);
163 bmi->mount_point = strdupz(mi->mount_point);
164 + bmi->mount_source = mi->mount_source ? strdupz(mi->mount_source) : NULL;
165 bmi->filesystem = strdupz(mi->filesystem);
166 }
167
@@ -158,6 +186,7 @@ static void free_basic_mountinfo(struct basic_mountinfo *bmi)
186 freez(bmi->root);
187 freez(bmi->mount_point_stat_path);
188 freez(bmi->mount_point);
189 + freez(bmi->mount_source);
190 freez(bmi->filesystem);
191
192 freez(bmi);
@@ -299,26 +328,212 @@ static void calculate_values_and_show_charts(
328 m->collected++;
329 }
330
331 +// ----------------------------------------------------------------------------
332 +// ZFS helper functions
333 +
334 +// Extract pool name from ZFS mount_source
335 +// "tank" → "tank", "tank/data/set" → "tank"
336 +static const char *extract_zfs_pool_name(const char *mount_source, char *buf, size_t buf_size) {
337 + if (!mount_source || !mount_source[0] || buf_size == 0)
338 + return NULL;
339 +
340 + const char *slash = strchr(mount_source, '/');
341 + if (slash) {
342 + size_t len = slash - mount_source;
343 + if (len >= buf_size)
344 + len = buf_size - 1;
345 + memcpy(buf, mount_source, len);
346 + buf[len] = '\0';
347 + } else {
348 + strncpyz(buf, mount_source, buf_size - 1);
349 + }
350 +
351 + return buf;
352 +}
353 +
354 +static inline bool is_zfs_filesystem(struct mountinfo *mi) {
355 + return mi && mi->filesystem && strcmp(mi->filesystem, "zfs") == 0;
356 +}
357 +
358 // Check if a ZFS filesystem entry is a dataset (not a pool)
359 +// Dataset = has '/' in mount_source (e.g., "tank/home")
360 +// Pool = no '/' in mount_source (e.g., "tank")
361 static inline bool is_zfs_dataset(struct mountinfo *mi) {
304 - if(!mi || !mi->filesystem || !mi->mount_source || !mi->mount_source[0])
305 - return false;
362 + return is_zfs_filesystem(mi) &&
363 + mi->mount_source &&
364 + mi->mount_source[0] &&
365 + strchr(mi->mount_source, '/') != NULL;
366 +}
367
307 - if(strcmp(mi->filesystem, "zfs") != 0)
368 +// Cached LXC detection result (checked once at first call, can't change at runtime)
369 +static bool zfs_inside_lxc_container = false;
370 +
371 +// Collect ZFS pool capacities for the heuristic
372 +// Called on every collection cycle but only updates pools (quick operation)
373 +static void zfs_collect_pool_capacities(void) {
374 + // LXC detection - only once (can't change at runtime)
375 + static bool lxc_checked = false;
376 + if (!lxc_checked) {
377 + zfs_inside_lxc_container = is_lxcfs_proc_mounted();
378 + lxc_checked = true;
379 + }
380 +
381 + if (!zfs_datasets_heuristic || !zfs_cache)
382 + return;
383 +
384 + time_t now = now_realtime_sec();
385 +
386 + for (struct mountinfo *mi = disk_mountinfo_root; mi; mi = mi->next) {
387 + if (!is_zfs_filesystem(mi))
388 + continue;
389 +
390 + if (!mi->mount_source || !mi->mount_source[0])
391 + continue;
392 +
393 + // Only process pool mounts (no '/' in mount_source), not datasets
394 + if (strchr(mi->mount_source, '/'))
395 + continue;
396 +
397 + // Check if this pool entry needs refresh
398 + const DICTIONARY_ITEM *existing = dictionary_get_and_acquire_item(zfs_cache, mi->mount_source);
399 + if (existing) {
400 + struct zfs_cache_entry *entry = dictionary_acquired_item_value(existing);
401 + if (entry->is_pool && now - entry->last_checked < ZFS_DATASET_RECHECK_SECONDS) {
402 + dictionary_acquired_item_release(zfs_cache, existing);
403 + continue; // still fresh
404 + }
405 + dictionary_acquired_item_release(zfs_cache, existing);
406 + }
407 +
408 + // Get capacity from the pool mount
409 + struct statvfs buff;
410 + if (statvfs(mi->mount_point_stat_path, &buff) != 0)
411 + continue;
412 +
413 + unsigned long bsize = buff.f_frsize ? buff.f_frsize : buff.f_bsize;
414 + uint64_t capacity = (uint64_t)buff.f_blocks * bsize;
415 +
416 + struct zfs_cache_entry new_entry = {
417 + .last_checked = now,
418 + .is_pool = true,
419 + .pool_capacity = capacity,
420 + .excluded = false
421 + };
422 + dictionary_set(zfs_cache, mi->mount_source, &new_entry, sizeof(new_entry));
423 + }
424 +}
425 +
426 +// Check if a ZFS dataset has a cached exclusion decision that's still valid
427 +// Returns: true if cached as excluded (skip statvfs), false otherwise
428 +static bool zfs_dataset_cached_excluded(const char *filesystem, const char *mount_point, const char *mount_source) {
429 + // Quick checks that don't need cache
430 + if (!filesystem || strcmp(filesystem, "zfs") != 0)
431 return false;
309 -
310 - // For ZFS, the mount_source contains the dataset name (e.g., "tank" or "tank/install")
311 - // Pools have no slash, datasets have at least one slash
312 - return strchr(mi->mount_source, '/') != NULL;
432 + if (!zfs_datasets_heuristic || !zfs_cache)
433 + return false;
434 + if (zfs_inside_lxc_container)
435 + return false;
436 + if (mount_point && strcmp(mount_point, "/") == 0)
437 + return false;
438 + if (!mount_source || !mount_source[0] || !strchr(mount_source, '/'))
439 + return false; // not a dataset
440 +
441 + const DICTIONARY_ITEM *item = dictionary_get_and_acquire_item(zfs_cache, mount_source);
442 + if (!item)
443 + return false; // not in cache
444 +
445 + struct zfs_cache_entry *entry = dictionary_acquired_item_value(item);
446 +
447 + // Only use cache for datasets (not pools), and only if still fresh
448 + time_t now = now_realtime_sec();
449 + bool cached_excluded = !entry->is_pool &&
450 + entry->excluded &&
451 + (now - entry->last_checked < ZFS_DATASET_RECHECK_SECONDS);
452 +
453 + dictionary_acquired_item_release(zfs_cache, item);
454 +
455 + return cached_excluded;
456 }
457
315 -static inline void do_disk_space_stats(struct mountinfo *mi, int update_every) {
316 - // Skip ZFS datasets, only monitor ZFS pools
317 - // This prevents alert floods when a pool fills up
318 - if (is_zfs_dataset(mi)) {
319 - return;
458 +// Determine if a ZFS mount should be excluded and cache the decision
459 +// Returns: true if should exclude, false if should keep
460 +static bool should_exclude_zfs(const char *filesystem, const char *mount_point, const char *mount_source, struct statvfs *buff) {
461 + // 1. Not ZFS → keep
462 + if (!filesystem || strcmp(filesystem, "zfs") != 0)
463 + return false;
464 +
465 + // 2. Root mount → always keep
466 + if (mount_point && strcmp(mount_point, "/") == 0)
467 + return false;
468 +
469 + // 3. Inside LXC container → keep all (container sees virtualized mounts)
470 + if (zfs_inside_lxc_container)
471 + return false;
472 +
473 + // 4. Not a dataset (it's a pool) → keep
474 + // Dataset = has '/' in mount_source, Pool = no '/'
475 + if (!mount_source || !mount_source[0] || !strchr(mount_source, '/'))
476 + return false;
477 +
478 + // 5. Heuristic disabled → use pattern matching (default "!*" excludes nothing)
479 + if (!zfs_datasets_heuristic)
480 + return excluded_zfs_datasets_pattern && simple_pattern_matches(excluded_zfs_datasets_pattern, mount_point);
481 +
482 + if (!zfs_cache)
483 + return false;
484 +
485 + // 6. Heuristic enabled - use capacity logic
486 + char pool_name_buf[256];
487 + const char *pool_name = extract_zfs_pool_name(mount_source, pool_name_buf, sizeof(pool_name_buf));
488 + if (!pool_name || !pool_name[0])
489 + return false; // can't determine pool → keep
490 +
491 + // Get pool info from cache
492 + const DICTIONARY_ITEM *pool_item = dictionary_get_and_acquire_item(zfs_cache, pool_name);
493 + if (!pool_item)
494 + return false; // unknown pool → keep
495 +
496 + struct zfs_cache_entry *pool_entry = dictionary_acquired_item_value(pool_item);
497 +
498 + // 7. Verify it's a pool entry with valid capacity
499 + if (!pool_entry->is_pool || !pool_entry->pool_capacity) {
500 + dictionary_acquired_item_release(zfs_cache, pool_item);
501 + return false;
502 }
503
504 + // 8. Check if pool cache entry is still fresh (pool may have been unmounted)
505 + // Use 2x interval: if entry missed one refresh cycle, pool is likely unmounted
506 + time_t now = now_realtime_sec();
507 + if (now - pool_entry->last_checked >= ZFS_DATASET_RECHECK_SECONDS * 2) {
508 + dictionary_acquired_item_release(zfs_cache, pool_item);
509 + return false;
510 + }
511 +
512 + uint64_t pool_capacity = pool_entry->pool_capacity;
513 + dictionary_acquired_item_release(zfs_cache, pool_item);
514 +
515 + // 9. Calculate this dataset's capacity
516 + unsigned long bsize = buff->f_frsize ? buff->f_frsize : buff->f_bsize;
517 + uint64_t dataset_capacity = (uint64_t)buff->f_blocks * bsize;
518 +
519 + // 10. Determine exclusion: dataset capacity >= pool capacity → no quota → exclude
520 + // Datasets without quotas report same capacity as pool.
521 + // Datasets with quotas report smaller capacity (capped by quota).
522 + bool excluded = (dataset_capacity >= pool_capacity);
523 +
524 + // 11. Cache the decision for this dataset
525 + struct zfs_cache_entry dataset_entry = {
526 + .last_checked = now,
527 + .is_pool = false,
528 + .pool_capacity = 0,
529 + .excluded = excluded
530 + };
531 + dictionary_set(zfs_cache, mount_source, &dataset_entry, sizeof(dataset_entry));
532 +
533 + return excluded;
534 +}
535 +
536 +static inline void do_disk_space_stats(struct mountinfo *mi, int update_every) {
537 const char *disk = mi->persistent_id;
538
539 static SIMPLE_PATTERN *excluded_mountpoints = NULL;
@@ -355,6 +570,14 @@ static inline void do_disk_space_stats(struct mountinfo *mi, int update_every) {
570 SIMPLE_PATTERN_EXACT,
571 true);
572
573 + // ZFS dataset exclusion pattern (used when heuristic is disabled)
574 + // Always create so the option appears in config for users to customize
575 + excluded_zfs_datasets_pattern = simple_pattern_create(
576 + inicfg_get(&netdata_config, CONFIG_SECTION_DISKSPACE, "exclude zfs datasets on paths", "!*"),
577 + NULL,
578 + SIMPLE_PATTERN_EXACT,
579 + true);
580 +
581 dict_mountpoints = dictionary_create_advanced(DICT_OPTION_FIXED_SIZE, &dictionary_stats_category_collectors, sizeof(struct mount_point_metadata));
582 dictionary_register_delete_callback(dict_mountpoints, mountpoint_delete_cb, NULL);
583 }
@@ -474,6 +697,10 @@ static inline void do_disk_space_stats(struct mountinfo *mi, int update_every) {
697 goto cleanup;
698 }
699
700 + // Check if this ZFS dataset has a cached exclusion decision (skip statvfs entirely)
701 + if (zfs_dataset_cached_excluded(mi->filesystem, mi->mount_point, mi->mount_source))
702 + goto cleanup;
703 +
704 usec_t start_time = now_monotonic_high_precision_usec();
705 struct statvfs buff_statvfs;
706
@@ -493,6 +720,10 @@ static inline void do_disk_space_stats(struct mountinfo *mi, int update_every) {
720 if ((now_monotonic_high_precision_usec() - start_time) > slow_timeout)
721 m->slow = true;
722
723 + // Check if this ZFS mount should be excluded (capacity-based heuristic)
724 + if (should_exclude_zfs(mi->filesystem, mi->mount_point, mi->mount_source, &buff_statvfs))
725 + goto cleanup;
726 +
727 m->shown_error = false;
728
729 struct basic_mountinfo bmi;
@@ -529,6 +760,10 @@ static inline void do_slow_disk_space_stats(struct basic_mountinfo *mi, int upda
760 }
761 m->shown_error = false;
762
763 + // Check if this ZFS mount should be excluded (same logic as fast path)
764 + if (should_exclude_zfs(mi->filesystem, mi->mount_point, mi->mount_source, &buff_statvfs))
765 + goto cleanup;
766 +
767 calculate_values_and_show_charts(mi, m, &buff_statvfs, update_every);
768
769 cleanup:
@@ -635,6 +870,13 @@ static void diskspace_main_cleanup(void *ptr) {
870 dictionary_destroy(dict_mountpoints);
871 dict_mountpoints = NULL;
872
873 + // Free ZFS deduplication resources
874 + dictionary_destroy(zfs_cache);
875 + zfs_cache = NULL;
876 +
877 + simple_pattern_free(excluded_zfs_datasets_pattern);
878 + excluded_zfs_datasets_pattern = NULL;
879 +
880 rrd_collector_finished();
881 worker_unregister();
882
@@ -884,6 +1126,21 @@ void diskspace_main(void *ptr) {
1126 if(check_for_new_mountpoints_every < update_every)
1127 check_for_new_mountpoints_every = update_every;
1128
1129 + // ZFS dataset deduplication configuration
1130 + zfs_datasets_heuristic = inicfg_get_boolean(
1131 + &netdata_config,
1132 + CONFIG_SECTION_DISKSPACE,
1133 + "zfs datasets heuristic",
1134 + CONFIG_BOOLEAN_YES);
1135 +
1136 + if (zfs_datasets_heuristic) {
1137 + // Heuristic mode: create cache for pool capacities and dataset exclusion decisions
1138 + zfs_cache = dictionary_create_advanced(
1139 + DICT_OPTION_FIXED_SIZE,
1140 + &dictionary_stats_category_collectors,
1141 + sizeof(struct zfs_cache_entry));
1142 + }
1143 +
1144 netdata_mutex_init(&slow_mountinfo_mutex);
1145
1146 struct slow_worker_data slow_worker_data = { .update_every = update_every };
@@ -915,6 +1172,9 @@ void diskspace_main(void *ptr) {
1172 free_basic_mountinfo_list(slow_mountinfo_tmp_root);
1173 slow_mountinfo_tmp_root = NULL;
1174
1175 + // Collect ZFS pool capacities for the heuristic (Pass 1)
1176 + zfs_collect_pool_capacities();
1177 +
1178 struct mountinfo *mi;
1179 for(mi = disk_mountinfo_root; mi; mi = mi->next) {
1180 if(unlikely(mi->flags & (MOUNTINFO_IS_DUMMY | MOUNTINFO_IS_BIND)))
src/collectors/proc.plugin/plugin_proc.c
+17 -19
@@ -113,37 +113,35 @@ bool is_mem_swap_enabled = false;
113 bool is_mem_zswap_enabled = false;
114 bool is_mem_ksm_enabled = false;
115
116 -static bool is_lxcfs_proc_mounted() {
117 - procfile *ff = NULL;
118 -
119 - if (unlikely(!ff)) {
120 - char filename[FILENAME_MAX + 1];
121 - snprintfz(filename, FILENAME_MAX, "/proc/self/mounts");
122 - ff = procfile_open(filename, " \t", PROCFILE_FLAG_DEFAULT);
123 - if (unlikely(!ff))
124 - return false;
125 - }
116 +bool is_lxcfs_proc_mounted(void) {
117 + char filename[FILENAME_MAX + 1];
118 + snprintfz(filename, FILENAME_MAX, "/proc/self/mounts");
119 +
120 + procfile *ff = procfile_open(filename, " \t", PROCFILE_FLAG_DEFAULT);
121 + if (unlikely(!ff))
122 + return false;
123
124 ff = procfile_readall(ff);
125 if (unlikely(!ff))
126 return false;
127
131 - unsigned long l, lines = procfile_lines(ff);
128 + bool found = false;
129 + unsigned long lines = procfile_lines(ff);
130
133 - for (l = 0; l < lines; l++) {
131 + for (unsigned long l = 0; l < lines; l++) {
132 size_t words = procfile_linewords(ff, l);
135 - if (words < 2) {
133 + if (words < 2)
134 continue;
135 +
136 + if (!strcmp(procfile_lineword(ff, l, 0), "lxcfs") &&
137 + !strncmp(procfile_lineword(ff, l, 1), "/proc", 5)) {
138 + found = true;
139 + break;
140 }
138 - if (!strcmp(procfile_lineword(ff, l, 0), "lxcfs") && !strncmp(procfile_lineword(ff, l, 1), "/proc", 5)) {
139 - procfile_close(ff);
140 - return true;
141 - }
141 }
142
143 procfile_close(ff);
145 -
146 - return false;
144 + return found;
145 }
146
147 static bool is_ksm_enabled() {
src/collectors/proc.plugin/plugin_proc.h
+1
@@ -62,6 +62,7 @@ void pci_aer_plugin_cleanup(void);
62 // metrics that need to be shared among data collectors
63 extern unsigned long long zfs_arcstats_shrinkable_cache_size_bytes;
64 extern bool inside_lxc_container;
65 +bool is_lxcfs_proc_mounted(void);
66
67 extern bool is_mem_swap_enabled;
68 extern bool is_mem_zswap_enabled;