@cryptotaxi247 / netdata-1 / commits / 753ea0a2c

apps.plugin: add per-Windows-service process tree grouping (#21925)

* apps.plugin: add per-Windows-service process tree grouping Add a new grouping dimension to apps.plugin on Windows that groups processes by their owning Windows Service. Service root PIDs are tagged via EnumServicesStatusEx (already called for process naming), then untagged processes walk up the tree to inherit their ancestor's service, with "Other" as fallback for non-service processes. Charts use type "service" and label "service" to naturally group with the existing windows.plugin service state charts. Enabled by default; disable with --without-services. Files changed: - apps_plugin.h: PROCESSES_HAVE_SERVICE flag, TARGET_TYPE_SERVICE, service_name/service_target fields, externs - apps_targets.c: services_root_target, get_service_target() - apps_os_windows.c: set p->service_name in GetServiceNames() - apps_aggregations.c: assign_service_to_all_processes() with 3-phase walk-up, zero/aggregate blocks - apps_plugin.c: enable_services_charts, CLI flag, chart output - apps_pid.c: cleanup service_name on process deletion - metadata.yaml: new services module with Windows-only metrics * apps.plugin: rename service fallback group from "Other" to "not-services" "Other" was misleading as it implied other services not individually monitored. "not-services" clearly indicates processes that do not belong to any Windows service. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(apps.plugin): detect Windows PID reuse to prevent bogus CPU spikes When Windows recycles a PID, the new process has less accumulated CPU time than the old one. The unsigned ULONGLONG subtraction in perflib_cpu_utilization() and perflib_rate() would underflow, producing massive values (e.g., 184725% CPU). Fix: detect PID reuse by comparing process creation times (Elapsed Time counter). When a change is detected, reset all counter history and re-read the process identity (comm, ppid, cmdline, SID, service). Also add safety guards (data1 >= data0) in rate calculations. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(apps.plugin): update metadata to match code changes - Change 'Other' to 'not-services' in method_description - Fix 'Zero additional overhead' to 'No additional SCM query overhead' - Document both CLI flag forms (without-services / no-services) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

Costa Tsaousis committed Mar 30, 2026 at 15:10 UTC 753ea0a2c1258838580f076a3263d9841f4d294a
7 files changed +349 -2
src/collectors/apps.plugin/apps_aggregations.c
+65
@@ -180,8 +180,54 @@ static void assign_a_target_to_all_processes(void) {
180 }
181 }
182
183 +#if (PROCESSES_HAVE_SERVICE == 1)
184 +static STRING *other_service_name = NULL;
185 +
186 +static void assign_service_to_all_processes(void) {
187 + if(!other_service_name)
188 + other_service_name = string_strdupz("not-services");
189 +
190 + // Clear walk-up assigned service_names from previous iteration.
191 + // Direct matches (got_service) are kept — they persist per process lifetime.
192 + for(struct pid_stat *p = root_of_pids(); p ; p = p->next) {
193 + if(!p->got_service && p->service_name) {
194 + string_freez(p->service_name);
195 + p->service_name = NULL;
196 + }
197 + }
198 +
199 + // Phase 1: direct match — GetServiceNames() already tagged service root PIDs
200 + // with p->service_name during data collection.
201 +
202 + // Phase 2: untagged processes walk up to find a service-tagged ancestor
203 + for(struct pid_stat *p = root_of_pids(); p ; p = p->next) {
204 + if(!p->service_name) {
205 + if(!p->is_manager) {
206 + for(struct pid_stat *pp = p->parent; pp ; pp = pp->parent) {
207 + if(pp->is_manager) break;
208 +
209 + if(pp->service_name) {
210 + p->service_name = string_dup(pp->service_name);
211 + break;
212 + }
213 + }
214 + }
215 +
216 + // Phase 3: fallback — no service ancestor found
217 + if(!p->service_name)
218 + p->service_name = string_dup(other_service_name);
219 + }
220 + }
221 +}
222 +#endif
223 +
224 void aggregate_processes_to_targets(void) {
225 assign_a_target_to_all_processes();
226 +
227 +#if (PROCESSES_HAVE_SERVICE == 1)
228 + if(enable_services_charts)
229 + assign_service_to_all_processes();
230 +#endif
231 apps_groups_targets_count = zero_all_targets(apps_groups_root_target);
232
233 #if (PROCESSES_HAVE_UID == 1)
@@ -193,6 +239,10 @@ void aggregate_processes_to_targets(void) {
239 #if (PROCESSES_HAVE_SID == 1)
240 zero_all_targets(sids_root_target);
241 #endif
242 +#if (PROCESSES_HAVE_SERVICE == 1)
243 + if(enable_services_charts)
244 + zero_all_targets(services_root_target);
245 +#endif
246
247 // this has to be done, before the cleanup
248 struct target *w = NULL, *o = NULL;
@@ -268,6 +318,21 @@ void aggregate_processes_to_targets(void) {
318 aggregate_pid_on_target(w, p, o);
319 #endif
320
321 + // --------------------------------------------------------------------
322 + // service target
323 +
324 +#if (PROCESSES_HAVE_SERVICE == 1)
325 + if(enable_services_charts) {
326 + o = p->service_target;
327 + if(likely(p->service_target && p->service_target->service_name == p->service_name))
328 + w = p->service_target;
329 + else
330 + w = p->service_target = get_service_target(p->service_name);
331 +
332 + aggregate_pid_on_target(w, p, o);
333 + }
334 +#endif
335 +
336 // --------------------------------------------------------------------
337 // aggregate all file descriptors
338
src/collectors/apps.plugin/apps_os_windows.c
+69 -2
@@ -630,6 +630,10 @@ static void GetServiceNames(void) {
630 sanitize_apps_plugin_chart_meta(name);
631 string_freez(p->name);
632 p->name = string_strdupz(name);
633 +#if (PROCESSES_HAVE_SERVICE == 1)
634 + string_freez(p->service_name);
635 + p->service_name = string_strdupz(name);
636 +#endif
637 }
638 }
639 }
@@ -819,7 +823,7 @@ static inline kernel_uint_t perflib_cpu_utilization(COUNTER_DATA *d) {
823 */
824
825 LONGLONG dt = time1 - time0;
822 - if(dt > 0)
826 + if(dt > 0 && data1 >= data0)
827 return NSEC_PER_SEC * (data1 - data0) / dt;
828 else
829 return 0;
@@ -832,7 +836,7 @@ static inline kernel_uint_t perflib_rate(COUNTER_DATA *d) {
836 LONGLONG time0 = d->previous.Time;
837
838 LONGLONG dt = (time1 - time0);
835 - if(dt > 0)
839 + if(dt > 0 && data1 >= data0)
840 return (RATES_DETAIL * (data1 - data0)) / dt;
841 else
842 return 0;
@@ -947,6 +951,69 @@ bool apps_os_collect_all_pids_windows(void) {
951 continue;
952 }
953
954 + // Detect PID reuse: if the process creation time changed, the PID was recycled
955 + // by a different process. Without this check, the unsigned subtraction in
956 + // perflib_cpu_utilization() and perflib_rate() would underflow, producing
957 + // massive bogus values (e.g., 184725% CPU).
958 + if(p->perflib[PDF_UPTIME].previous.Data != 0 &&
959 + p->perflib[PDF_UPTIME].current.Data != p->perflib[PDF_UPTIME].previous.Data) {
960 +
961 + nd_log(NDLS_COLLECTORS, NDLP_WARNING,
962 + "APPS: PID %d (%s) creation time changed "
963 + "(0x%" PRIx64 " -> 0x%" PRIx64 "), "
964 + "PID reuse detected, resetting counters",
965 + (int)p->pid, pid_stat_comm(p),
966 + (uint64_t)p->perflib[PDF_UPTIME].previous.Data,
967 + (uint64_t)p->perflib[PDF_UPTIME].current.Data);
968 +
969 + // Reset all counter history to prevent unsigned underflow in rate calculations
970 + for(PID_FIELD f = 0; f < PDF_MAX; f++) {
971 + if(p->perflib[f].key)
972 + p->perflib[f].previous = RAW_DATA_EMPTY;
973 + }
974 +
975 + // Reset process identity so GetAllProcessesInfo() re-reads everything
976 + p->got_info = false;
977 + p->got_service = false;
978 +
979 + string_freez(p->sid_name);
980 + p->sid_name = NULL;
981 +
982 + string_freez(p->service_name);
983 + p->service_name = NULL;
984 +
985 + string_freez(p->name);
986 + p->name = NULL;
987 +
988 + string_freez(p->cmdline);
989 + p->cmdline = NULL;
990 +
991 + // Re-read comm name from the perflib instance
992 + {
993 + char reuse_comm[MAX_PATH];
994 + if(getInstanceName(d.pDataBlock, d.pObjectType, d.pi, reuse_comm, sizeof(reuse_comm)))
995 + fix_windows_comm(p, reuse_comm);
996 + else
997 + strncpyz(reuse_comm, "unknown", sizeof(reuse_comm) - 1);
998 +
999 + update_pid_comm(p, reuse_comm);
1000 + }
1001 +
1002 + // Update parent PID
1003 + {
1004 + COUNTER_DATA ppid = {.key = "Creating Process ID"};
1005 + perflibGetInstanceCounter(d.pDataBlock, d.pObjectType, d.pi, &ppid);
1006 + p->ppid = (pid_t)ppid.current.Data;
1007 + }
1008 +
1009 + // Reset target assignment — the new process may belong to a different group
1010 + p->target = NULL;
1011 + p->matched_by_config = false;
1012 +
1013 + // Trigger GetAllProcessesInfo() to re-read cmdline, name, SID, service
1014 + added++;
1015 + }
1016 +
1017 // CPU time
1018 p->values[PDF_UTIME] = perflib_cpu_utilization(&p->perflib[PDF_UTIME]);
1019 p->values[PDF_STIME] = perflib_cpu_utilization(&p->perflib[PDF_STIME]);
src/collectors/apps.plugin/apps_pid.c
+4
@@ -151,6 +151,10 @@ void del_pid_entry(pid_t pid) {
151 string_freez(p->sid_name);
152 #endif
153
154 +#if (PROCESSES_HAVE_SERVICE == 1)
155 + string_freez(p->service_name);
156 +#endif
157 +
158 string_freez(p->comm_orig);
159 string_freez(p->comm);
160 string_freez(p->cmdline);
src/collectors/apps.plugin/apps_plugin.c
+21
@@ -26,6 +26,9 @@ bool debug_enabled = false;
26 bool enable_detailed_uptime_charts = false;
27 bool enable_users_charts = true;
28 bool enable_groups_charts = true;
29 +#if (PROCESSES_HAVE_SERVICE == 1)
30 +bool enable_services_charts = true;
31 +#endif
32 bool include_exited_childs = true;
33 bool proc_pid_cmdline_is_needed = true; // true when we need to read /proc/cmdline
34
@@ -518,6 +521,13 @@ static void parse_args(int argc, char **argv)
521 }
522 #endif
523
524 +#if (PROCESSES_HAVE_SERVICE == 1)
525 + if(strcmp("no-services", argv[i]) == 0 || strcmp("without-services", argv[i]) == 0) {
526 + enable_services_charts = false;
527 + continue;
528 + }
529 +#endif
530 +
531 if(strcmp("with-detailed-uptime", argv[i]) == 0) {
532 enable_detailed_uptime_charts = 1;
533 continue;
@@ -574,6 +584,10 @@ static void parse_args(int argc, char **argv)
584 #if (PROCESSES_HAVE_GID == 1)
585 " without-groups disable reporting per user group charts\n"
586 "\n"
587 +#endif
588 +#if (PROCESSES_HAVE_SERVICE == 1)
589 + " without-services disable reporting per Windows service charts\n"
590 + "\n"
591 #endif
592 " with-detailed-uptime enable reporting min/avg/max uptime charts\n"
593 "\n"
@@ -877,6 +891,13 @@ int main(int argc, char **argv) {
891 }
892 #endif
893
894 +#if (PROCESSES_HAVE_SERVICE == 1)
895 + if (enable_services_charts) {
896 + send_charts_updates_to_netdata(services_root_target, "service", "service", "Windows Service");
897 + send_collected_data_to_netdata(services_root_target, "service", dt);
898 + }
899 +#endif
900 +
901 fflush(stdout);
902
903 debug_log("done Loop No %zu", global_iterations_counter);
src/collectors/apps.plugin/apps_plugin.h
+19
@@ -101,6 +101,7 @@ struct pid_info {
101 #define PROCESSES_HAVE_UID 0
102 #define PROCESSES_HAVE_GID 0
103 #define PROCESSES_HAVE_SID 1
104 +#define PROCESSES_HAVE_SERVICE 1
105 #define PROCESSES_HAVE_MAJFLT 0
106 #define PROCESSES_HAVE_CHILDREN_FLTS 0
107 #define PROCESSES_HAVE_VMSWAP 1
@@ -293,6 +294,9 @@ typedef enum __attribute__((packed)) {
294 #endif
295 #if (PROCESSES_HAVE_SID == 1)
296 TARGET_TYPE_SID,
297 +#endif
298 +#if (PROCESSES_HAVE_SERVICE == 1)
299 + TARGET_TYPE_SERVICE,
300 #endif
301 TARGET_TYPE_TREE,
302 } TARGET_TYPE;
@@ -407,6 +411,9 @@ struct target {
411 #if (PROCESSES_HAVE_SID == 1)
412 STRING *sid_name;
413 #endif
414 +#if (PROCESSES_HAVE_SERVICE == 1)
415 + STRING *service_name;
416 +#endif
417
418 kernel_uint_t values[PDF_MAX];
419
@@ -516,6 +523,9 @@ struct pid_stat {
523 #if (PROCESSES_HAVE_SID == 1)
524 struct target *sid_target; // sid based targets
525 #endif
526 +#if (PROCESSES_HAVE_SERVICE == 1)
527 + struct target *service_target; // service based targets
528 +#endif
529
530 STRING *comm_orig; // the command, as-collected
531 STRING *comm; // the command, sanitized
@@ -539,6 +549,9 @@ struct pid_stat {
549 #if (PROCESSES_HAVE_SID == 1)
550 STRING *sid_name;
551 #endif
552 +#if (PROCESSES_HAVE_SERVICE == 1)
553 + STRING *service_name;
554 +#endif
555
556 #if (ALL_PIDS_ARE_READ_INSTANTLY == 0)
557 uint32_t sortlist; // higher numbers = top on the process tree
@@ -732,6 +745,12 @@ extern struct target *sids_root_target;
745 struct target *get_sid_target(STRING *sid_name);
746 #endif
747
748 +#if (PROCESSES_HAVE_SERVICE == 1)
749 +extern bool enable_services_charts;
750 +extern struct target *services_root_target;
751 +struct target *get_service_target(STRING *service_name);
752 +#endif
753 +
754 extern struct target *apps_groups_root_target;
755 int read_apps_groups_conf(const char *path, const char *file);
756
src/collectors/apps.plugin/apps_targets.c
+27
@@ -301,6 +301,33 @@ struct target *get_sid_target(STRING *sid_name) {
301 }
302 #endif
303
304 +// --------------------------------------------------------------------------------------------------------------------
305 +// Service
306 +
307 +#if (PROCESSES_HAVE_SERVICE == 1)
308 +struct target *services_root_target = NULL;
309 +
310 +struct target *get_service_target(STRING *service_name) {
311 + struct target *w;
312 + for(w = services_root_target ; w ; w = w->next)
313 + if(w->service_name == service_name) return w;
314 +
315 + w = callocz(sizeof(struct target), 1);
316 + w->type = TARGET_TYPE_SERVICE;
317 + w->service_name = string_dup(service_name);
318 + w->id = string_dup(service_name);
319 + w->name = string_dup(service_name);
320 + w->clean_name = get_clean_name(w->name);
321 +
322 + w->next = services_root_target;
323 + services_root_target = w;
324 +
325 + debug_log("added service '%s' target", string2str(w->service_name));
326 +
327 + return w;
328 +}
329 +#endif
330 +
331 // --------------------------------------------------------------------------------------------------------------------
332 // apps_groups.conf
333
src/collectors/apps.plugin/metadata.yaml
+144
@@ -567,3 +567,147 @@ modules:
567 - name: min
568 - name: avg
569 - name: max
570 + - meta:
571 + plugin_name: apps.plugin
572 + module_name: services
573 + monitored_instance:
574 + name: Windows Services
575 + link: ""
576 + categories:
577 + - data-collection.operating-systems
578 + icon_filename: "windows.svg"
579 + related_resources:
580 + integrations:
581 + list: []
582 + info_provided_to_referring_integrations:
583 + description: ""
584 + keywords:
585 + - windows
586 + - services
587 + - processes
588 + - os
589 + - host monitoring
590 + overview:
591 + data_collection:
592 + metrics_description: "This integration monitors resource utilization grouped by Windows Service."
593 + method_description: "It auto-discovers running Windows services via the Service Control Manager and groups each service's process tree to aggregate CPU, memory, I/O, and other metrics per service. Processes not belonging to any service are grouped under 'not-services'."
594 + supported_platforms:
595 + include:
596 + - windows
597 + exclude: []
598 + multi_instance: true
599 + additional_permissions:
600 + description: ""
601 + default_behavior:
602 + auto_detection:
603 + description: "All running Windows services are automatically discovered. No configuration is needed."
604 + limits:
605 + description: ""
606 + performance_impact:
607 + description: "No additional SCM query overhead. The service list is already queried by apps.plugin for process naming. Per-iteration work involves walking parent chains to assign service names."
608 + setup:
609 + prerequisites:
610 + list: []
611 + configuration:
612 + file:
613 + name: ""
614 + description: ""
615 + options:
616 + description: "Service charts are enabled by default. Use the `without-services` (or `no-services`) command line option to disable them."
617 + folding:
618 + title: ""
619 + enabled: true
620 + list: []
621 + examples:
622 + folding:
623 + enabled: true
624 + title: ""
625 + list: []
626 + troubleshooting:
627 + problems:
628 + list: []
629 + alerts: []
630 + metrics:
631 + folding:
632 + title: Metrics
633 + enabled: false
634 + description: ""
635 + availability:
636 + - windows
637 + scopes:
638 + - name: windows service
639 + description: These metrics refer to the Windows Service.
640 + labels:
641 + - name: service
642 + description: The display name of the Windows service.
643 + metrics:
644 + - name: service.cpu_utilization
645 + description: Windows Service CPU utilization (100% = 1 core)
646 + unit: percentage
647 + chart_type: stacked
648 + dimensions:
649 + - name: user
650 + - name: system
651 + - name: service.mem_usage
652 + description: Windows Service memory RSS usage
653 + unit: MiB
654 + chart_type: area
655 + dimensions:
656 + - name: rss
657 + - name: service.vmem_usage
658 + description: Windows Service virtual memory size
659 + unit: MiB
660 + chart_type: line
661 + dimensions:
662 + - name: vmem
663 + - name: service.mem_page_faults
664 + description: Windows Service memory page faults
665 + unit: pgfaults/s
666 + chart_type: stacked
667 + dimensions:
668 + - name: minor
669 + - name: service.swap_usage
670 + description: Windows Service swap usage
671 + unit: MiB
672 + chart_type: area
673 + dimensions:
674 + - name: swap
675 + - name: service.disk_logical_io
676 + description: Windows Service disk logical IO
677 + unit: KiB/s
678 + chart_type: area
679 + dimensions:
680 + - name: reads
681 + - name: writes
682 + - name: service.fds_open
683 + description: Windows Service open handles
684 + unit: fds
685 + chart_type: stacked
686 + dimensions:
687 + - name: handles
688 + - name: service.processes
689 + description: Windows Service processes
690 + unit: processes
691 + chart_type: line
692 + dimensions:
693 + - name: processes
694 + - name: service.threads
695 + description: Windows Service threads
696 + unit: threads
697 + chart_type: line
698 + dimensions:
699 + - name: threads
700 + - name: service.uptime
701 + description: Windows Service uptime
702 + unit: seconds
703 + chart_type: line
704 + dimensions:
705 + - name: uptime
706 + - name: service.uptime_summary
707 + description: Windows Service uptime summary
708 + unit: seconds
709 + chart_type: area
710 + dimensions:
711 + - name: min
712 + - name: avg
713 + - name: max