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]