@cryptotaxi247 / netdata-1 / commits / 940ce2e21

apps.plugin documentation and grouping matches improvements (#20386)

updated documentation for apps.plugin; matches are now case insensitive; windows processes names are now part of the matching

Costa Tsaousis committed Jun 1, 2025 at 19:19 UTC 940ce2e211e012862509c5addd256d13a9cacfba
5 files changed +223 -30
src/collectors/apps.plugin/README.md
+106 -12
@@ -122,31 +122,125 @@ interpreters: process1 process2 process3
122
123 ### Matching processes
124
125 -The following methods are used for matching against the specified patterns:
125 +`apps.plugin` uses different fields for process matching depending on the operating system:
126
127 -| Method | Description |
128 -|---------|----------------------------------------------------------------------|
129 -| comm | Process name as reported by `ps -e` or `cat /proc/{PID}/comm` |
130 -| cmdline | The complete command line (`cat /proc/{PID}/cmdline \| tr '\0' ' '`) |
127 +#### Unix-like systems (Linux, FreeBSD, macOS)
128
132 -> On Linux, the **comm** field is limited to 15 characters.
129 +| Field | Description | Example |
130 +|----------|----------------------------------------------------------------------|----------------------|
131 +| comm | Process name (command) | `chrome` |
132 +| cmdline | Full command line with arguments | `/usr/bin/chrome --enable-features=...` |
133 +
134 +> **Note:** On Linux specifically, the **comm** field is limited to 15 characters from `/proc/{PID}/comm`.
135 > `apps.plugin` attempts to obtain the full process name by searching for it in the **cmdline**.
136 > If successful, the entire process name is used; otherwise, the shortened version is used.
137
138 +#### Windows process fields
139 +
140 +| Field | Description | Example |
141 +|----------|----------------------------------------------------------------------|----------------------|
142 +| comm | Performance Monitor instance name (may include instance numbers) | `chrome#12` |
143 +| cmdline | Full path to the executable (without command line arguments) | `C:\Program Files\Google\Chrome\Application\chrome.exe` |
144 +| name | Friendly name from file description or service display name | `Google Chrome` |
145 +
146 +> On Windows:
147 +> - All pattern types (exact, prefix, suffix, substring) also match against the **name** field
148 +> - The **name** field is preferred for default grouping when no pattern matches
149 +> - Instance numbers (e.g., `#1`, `#2`) are automatically stripped from the **comm** field
150 +> - The `.exe` extension is automatically removed for cleaner display
151 +> - For services (especially `svchost.exe`), the service display name is resolved
152 +
153 +#### Pattern matching
154 +
155 You can use asterisks (`*`) to create patterns:
156
138 -| Mode | Pattern | Description |
139 -|-----------|----------|------------------------------------------|
140 -| prefix | `name*` | Matches a **comm** that begins with name |
141 -| suffix | `*name` | Matches a **comm** that ends with name |
142 -| substring | `*name*` | Searches for name within the **cmdline** |
157 +> **Version differences:**
158 +> - **Netdata v2.5.2 and earlier**: Pattern matching is case sensitive
159 +> - **Netdata v2.5.3 and later**: Pattern matching is case insensitive
160 +> - **Netdata v2.5.2 and earlier**: Windows patterns match against `comm` and `cmdline` fields
161 +> - **Netdata v2.5.3 and later**: Windows patterns match against `comm`, `cmdline`, and `name` (friendly name) fields
162 +
163 +| Mode | Pattern | Description | Unix-like | Windows |
164 +|-----------|-------------|---------------------------------------------|-----------|---------|
165 +| exact | `firefox` | Matches **comm** exactly | ✓ Yes | ✓ Yes |
166 +| prefix | `firefox*` | Matches **comm** starting with firefox | ✓ Yes | ✓ Yes |
167 +| suffix | `*fox` | Matches **comm** ending with fox | ✓ Yes | ✓ Yes |
168 +| substring | `*firefox*` | Searches within **cmdline** | ✓ Yes (full command line) | ✓ Yes (full path) |
169
144 -- Asterisks can be placed anywhere within name (e.g., `na*me`) without affecting the matching criteria (**comm** or **cmdline**).
170 +**Note on substring matching (`*pattern*`):**
171 +- On Unix-like systems: Searches within the full command line including arguments
172 +- On Windows: Searches within the full executable path (e.g., `C:\Program Files\Mozilla Firefox\firefox.exe`)
173 +
174 +- Asterisks can be placed anywhere within pattern (e.g., `fi*fox`) without affecting the matching criteria (**comm** or **cmdline**).
175 - To include process names with spaces, enclose them in quotes (single or double), like this: `'Plex Media Serv'` or `"my other process"`.
176 - To include processes with single quotes, enclose them in double quotes: `"process with this ' single quote"`.
177 - To include processes with double quotes, enclose them in single quotes: `'process with this " double quote'`.
178 - The order of the entries in the configuration list is crucial. The first matching entry will be used, so it's important to follow a top-down hierarchy. Processes that don't match any entry will inherit the group from their parent processes.
179
180 +#### Windows default grouping behavior
181 +
182 +On Windows, when a process doesn't match any pattern in `apps_groups.conf`:
183 +- The **name** field (friendly name from file description or service display name) is used as the default group/category if available
184 +- If no **name** field exists, the **comm** field is used
185 +- This provides better default grouping for Windows services and applications with descriptive names
186 +
187 +For example, a process might have:
188 +- **comm**: `svchost`
189 +- **name**: `Windows Update`
190 +- **Default category**: `Windows Update` (uses the friendly name)
191 +
192 +### Windows path handling
193 +
194 +When configuring `apps_groups.conf` on Windows systems:
195 +
196 +1. **No backslash escaping needed** - Windows paths with backslashes are handled as literal strings:
197 + ```text
198 + sqlserver: "C:\Program Files\Microsoft SQL Server\MSSQL15.MSSQLSERVER\MSSQL\Binn\sqlservr.exe"
199 + ```
200 +
201 +2. **Use quotes for paths with spaces**:
202 + ```text
203 + office: "Microsoft Word" "Microsoft Excel"
204 + browsers: chrome firefox msedge
205 + ```
206 +
207 +3. **Prefer process names over full paths** - This is more portable and easier to maintain:
208 + ```text
209 + # Recommended - matches all SQL Server processes regardless of version/instance
210 + sqlserver: sqlservr
211 +
212 + # Also works but less flexible
213 + sqlserver: "C:\Program Files\Microsoft SQL Server\MSSQL15.MSSQLSERVER\MSSQL\Binn\sqlservr.exe"
214 + ```
215 +
216 +4. **Use wildcards for flexible path matching**:
217 + ```text
218 + # Match anything from Program Files
219 + programfiles: "*Program Files*"
220 +
221 + # Match SQL Server components across versions
222 + mssql: "*\Microsoft SQL Server\*"
223 +
224 + # Match enterprise backup solutions
225 + backup: "*\Veeam\*" "*\Veritas\*" "*\CommVault\*"
226 + ```
227 +
228 +### Verifying your configuration
229 +
230 +You can use the Netdata `processes` function to verify that your `apps_groups.conf` configuration is working correctly:
231 +
232 +1. **Access the processes function** through Netdata Cloud (required for security reasons)
233 +2. **Review the output** to see:
234 + - Current running processes with their `comm`, `cmdline`, and (on Windows) `name` fields
235 + - The **Category** column shows which group from `apps_groups.conf` each process has been assigned to
236 + - Resource utilization for each process
237 +
238 +3. **Troubleshooting tips**:
239 + - If a process shows the wrong Category, check the exact process name in the function output
240 + - On Windows, remember that the `name` field is used for default categories but NOT for pattern matching
241 + - Remember that the first matching pattern wins - check your pattern order
242 + - For inherited groups, verify the parent process has the correct Category
243 +
244 There are a few command line options you can pass to `apps.plugin`. The list of available options can be acquired with the `--help` flag.
245 The options can be set in the `netdata.conf` using the [`edit-config` script](/docs/netdata-agent/configuration/README.md).
246
src/collectors/apps.plugin/apps_groups.conf
+16
@@ -40,6 +40,22 @@
40 ## Processes of interest
41 ## Grouping and/or rename individual processes.
42 ## (there is no internal default for this section)
43 +##
44 +## Pattern matching rules:
45 +## - All matches are CASE INSENSITIVE (case sensitive in v2.5.2 and earlier)
46 +## - Use quotes for process names with spaces: "Process Name"
47 +## - Patterns work on both Unix-like systems and Windows:
48 +## - Exact match: firefox (matches process named firefox)
49 +## - Prefix match: firefox* (matches firefox, firefox.exe, firefox-bin, etc.)
50 +## - Suffix match: *fox (matches firefox, icefox, etc.)
51 +## - Substring match: *firefox* (searches in full command line/path)
52 +##
53 +## Windows-specific notes:
54 +## - Process names work without .exe extension (automatically handled)
55 +## - Substring patterns (*pattern*) search within the full executable path
56 +## - No backslash escaping needed for Windows paths
57 +## - The 'name' field (friendly name) is used for default categories when no pattern matches
58 +## - v2.5.3+: Patterns also match against the 'name' field (friendly name)
59
60 ## NETDATA processes accounting
61 netdata: netdata
src/collectors/apps.plugin/apps_pid_match.c
+57 -13
@@ -4,46 +4,90 @@
4
5 bool pid_match_check(struct pid_stat *p, APPS_MATCH *match) {
6 if(!match->starts_with && !match->ends_with) {
7 + // Exact match
8 if(match->pattern) {
9 if(simple_pattern_matches_string(match->pattern, p->comm))
10 return true;
11 +#if (PROCESSES_HAVE_COMM_AND_NAME == 1)
12 + if(p->name && simple_pattern_matches_string(match->pattern, p->name))
13 + return true;
14 +#endif
15 }
16 else {
12 - if(match->compare == p->comm || match->compare == p->comm_orig)
17 + if(string_equals_string_nocase(match->compare, p->comm) ||
18 + string_equals_string_nocase(match->compare, p->comm_orig))
19 + return true;
20 +#if (PROCESSES_HAVE_COMM_AND_NAME == 1)
21 + if(p->name && string_equals_string_nocase(match->compare, p->name))
22 return true;
23 +#endif
24 }
25 }
26 else if(match->starts_with && !match->ends_with) {
27 + // Prefix match
28 if(match->pattern) {
29 if(simple_pattern_matches_string(match->pattern, p->comm))
30 return true;
31 +#if (PROCESSES_HAVE_COMM_AND_NAME == 1)
32 + if(p->name && simple_pattern_matches_string(match->pattern, p->name))
33 + return true;
34 +#endif
35 }
36 else {
22 - if(string_starts_with_string(p->comm, match->compare) ||
23 - (p->comm != p->comm_orig && string_starts_with_string(p->comm, match->compare)))
37 + if(string_starts_with_string_nocase(p->comm, match->compare) ||
38 + (p->comm != p->comm_orig && string_starts_with_string_nocase(p->comm_orig, match->compare)))
39 return true;
40 +#if (PROCESSES_HAVE_COMM_AND_NAME == 1)
41 + if(p->name && string_starts_with_string_nocase(p->name, match->compare))
42 + return true;
43 +#endif
44 }
45 }
46 else if(!match->starts_with && match->ends_with) {
47 + // Suffix match
48 if(match->pattern) {
49 if(simple_pattern_matches_string(match->pattern, p->comm))
50 return true;
51 +#if (PROCESSES_HAVE_COMM_AND_NAME == 1)
52 + if(p->name && simple_pattern_matches_string(match->pattern, p->name))
53 + return true;
54 +#endif
55 }
56 else {
33 - if(string_ends_with_string(p->comm, match->compare) ||
34 - (p->comm != p->comm_orig && string_ends_with_string(p->comm, match->compare)))
57 + if(string_ends_with_string_nocase(p->comm, match->compare) ||
58 + (p->comm != p->comm_orig && string_ends_with_string_nocase(p->comm_orig, match->compare)))
59 return true;
60 +#if (PROCESSES_HAVE_COMM_AND_NAME == 1)
61 + if(p->name && string_ends_with_string_nocase(p->name, match->compare))
62 + return true;
63 +#endif
64 }
65 }
38 - else if(match->starts_with && match->ends_with && p->cmdline) {
39 - if(match->pattern) {
40 - if(simple_pattern_matches_string(match->pattern, p->cmdline))
41 - return true;
66 + else if(match->starts_with && match->ends_with) {
67 + // Substring match - search in cmdline and on Windows also in name
68 + if(p->cmdline) {
69 + if(match->pattern) {
70 + if(simple_pattern_matches_string(match->pattern, p->cmdline))
71 + return true;
72 + }
73 + else {
74 + if(strcasestr(string2str(p->cmdline), string2str(match->compare)))
75 + return true;
76 + }
77 }
43 - else {
44 - if(strstr(string2str(p->cmdline), string2str(match->compare)))
45 - return true;
78 +#if (PROCESSES_HAVE_COMM_AND_NAME == 1)
79 + // On Windows, also search in the name field for substring patterns
80 + if(p->name) {
81 + if(match->pattern) {
82 + if(simple_pattern_matches_string(match->pattern, p->name))
83 + return true;
84 + }
85 + else {
86 + if(strcasestr(string2str(p->name), string2str(match->compare)))
87 + return true;
88 + }
89 }
90 +#endif
91 }
92
93 return false;
@@ -78,7 +122,7 @@ APPS_MATCH pid_match_create(const char *comm) {
122 m.compare = string_strdupz(nid);
123
124 if(strchr(nid, '*'))
81 - m.pattern = simple_pattern_create(comm, SIMPLE_PATTERN_NO_SEPARATORS, SIMPLE_PATTERN_EXACT, true);
125 + m.pattern = simple_pattern_create(comm, SIMPLE_PATTERN_NO_SEPARATORS, SIMPLE_PATTERN_EXACT, false);
126
127 return m;
128 }
src/libnetdata/string/string.c
+41 -5
@@ -111,6 +111,7 @@ static inline bool string_entry_check_and_acquire(STRING *se) {
111 return true;
112 }
113
114 +ALWAYS_INLINE
115 STRING *string_dup(STRING *string) {
116 if(unlikely(!string)) return NULL;
117
@@ -133,7 +134,7 @@ STRING *string_dup(STRING *string) {
134 }
135
136 // Search the index and return an ACQUIRED string entry, or NULL
136 -static inline STRING *string_index_search(const char *str, size_t length) {
137 +static STRING *string_index_search(const char *str, size_t length) {
138 STRING *string;
139
140 uint8_t partition = string_partition_str(str);
@@ -176,7 +177,7 @@ static inline STRING *string_index_search(const char *str, size_t length) {
177 // The returned entry is ACQUIRED, and it can either be:
178 // 1. a new item inserted, or
179 // 2. an item found in the index that is not currently deleted
179 -static inline STRING *string_index_insert(const char *str, size_t length) {
180 +static STRING *string_index_insert(const char *str, size_t length) {
181 STRING *string;
182
183 uint8_t partition = string_partition_str(str);
@@ -253,7 +254,7 @@ static inline STRING *string_index_insert(const char *str, size_t length) {
254 }
255
256 // delete an entry from the index
256 -static inline void string_index_delete(STRING *string) {
257 +static void string_index_delete(STRING *string) {
258 uint8_t partition = string_partition(string);
259
260 rw_spinlock_write_lock(&string_base[partition].spinlock);
@@ -301,6 +302,7 @@ static inline void string_index_delete(STRING *string) {
302 rw_spinlock_write_unlock(&string_base[partition].spinlock);
303 }
304
305 +ALWAYS_INLINE
306 STRING *string_strdupz(const char *str) {
307 if(unlikely(!str || !*str)) return NULL;
308
@@ -330,6 +332,7 @@ STRING *string_strdupz(const char *str) {
332 return string;
333 }
334
335 +ALWAYS_INLINE
336 STRING *string_strndupz(const char *str, size_t len) {
337 if(unlikely(!str || !*str || !len)) return NULL;
338
@@ -355,6 +358,7 @@ STRING *string_strndupz(const char *str, size_t len) {
358 return string;
359 }
360
361 +ALWAYS_INLINE
362 void string_freez(STRING *string) {
363 if(unlikely(!string)) return;
364
@@ -370,16 +374,19 @@ void string_freez(STRING *string) {
374 string_stats_atomic_increment(partition, releases);
375 }
376
373 -inline size_t string_strlen(const STRING *string) {
377 +ALWAYS_INLINE
378 +size_t string_strlen(const STRING *string) {
379 if(unlikely(!string)) return 0;
380 return string->length - 1;
381 }
382
378 -inline const char *string2str(const STRING *string) {
383 +ALWAYS_INLINE
384 +const char *string2str(const STRING *string) {
385 if(unlikely(!string)) return "";
386 return string->str;
387 }
388
389 +ALWAYS_INLINE
390 bool string_ends_with_string(const STRING *whole, const STRING *end) {
391 if(whole == end) return true;
392 if(!whole || !end) return false;
@@ -390,6 +397,18 @@ bool string_ends_with_string(const STRING *whole, const STRING *end) {
397 return strncmp(we, end->str, string_strlen(end)) == 0;
398 }
399
400 +ALWAYS_INLINE
401 +bool string_ends_with_string_nocase(const STRING *whole, const STRING *end) {
402 + if(whole == end) return true;
403 + if(!whole || !end) return false;
404 + if(end->length > whole->length) return false;
405 + if(end->length == whole->length) return strcasecmp(string2str(whole), string2str(end)) == 0;
406 + const char *we = string2str(whole);
407 + we = &we[string_strlen(whole) - string_strlen(end)];
408 + return strncasecmp(we, end->str, string_strlen(end)) == 0;
409 +}
410 +
411 +ALWAYS_INLINE
412 bool string_starts_with_string(const STRING *whole, const STRING *end) {
413 if(whole == end) return true;
414 if(!whole || !end) return false;
@@ -398,6 +417,23 @@ bool string_starts_with_string(const STRING *whole, const STRING *end) {
417 return strncmp(string2str(whole), string2str(end), string_strlen(end)) == 0;
418 }
419
420 +ALWAYS_INLINE
421 +bool string_starts_with_string_nocase(const STRING *whole, const STRING *prefix) {
422 + if(whole == prefix) return true;
423 + if(!whole || !prefix) return false;
424 + if(prefix->length > whole->length) return false;
425 + if(prefix->length == whole->length) return strcasecmp(string2str(whole), string2str(prefix)) == 0;
426 + return strncasecmp(string2str(whole), string2str(prefix), string_strlen(prefix)) == 0;
427 +}
428 +
429 +ALWAYS_INLINE
430 +bool string_equals_string_nocase(const STRING *a, const STRING *b) {
431 + if(a == b) return true;
432 + if(!a || !b) return false;
433 + if(a->length != b->length) return false;
434 + return strcasecmp(string2str(a), string2str(b)) == 0;
435 +}
436 +
437 // Static X used by string_2way_merge
438 static STRING *string_2way_merge_X = NULL;
439
src/libnetdata/string/string.h
+3
@@ -18,6 +18,9 @@ size_t string_strlen(const STRING *string);
18 const char *string2str(const STRING *string) NEVERNULL;
19 bool string_ends_with_string(const STRING *whole, const STRING *end);
20 bool string_starts_with_string(const STRING *whole, const STRING *end);
21 +bool string_ends_with_string_nocase(const STRING *whole, const STRING *end);
22 +bool string_starts_with_string_nocase(const STRING *whole, const STRING *prefix);
23 +bool string_equals_string_nocase(const STRING *a, const STRING *b);
24 size_t string_destroy(void);
25
26 // keep common prefix/suffix and replace everything else with [x]