-
Notifications
You must be signed in to change notification settings - Fork 1.2k
/
service.go
479 lines (415 loc) · 13.9 KB
/
service.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
// Unless explicitly stated otherwise all files in this repository are licensed
// under the Apache License Version 2.0.
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2016-present Datadog, Inc.
package parser
import (
"os"
"path/filepath"
"runtime"
"slices"
"strings"
"unicode"
"github.com/Masterminds/semver"
"github.com/cihub/seelog"
"github.com/DataDog/datadog-agent/pkg/process/metadata"
javaparser "github.com/DataDog/datadog-agent/pkg/process/metadata/parser/java"
nodejsparser "github.com/DataDog/datadog-agent/pkg/process/metadata/parser/nodejs"
"github.com/DataDog/datadog-agent/pkg/process/procutil"
"github.com/DataDog/datadog-agent/pkg/util/log"
)
type serviceExtractorFn func(serviceExtractor *ServiceExtractor, process *procutil.Process, args []string) string
const (
javaJarFlag = "-jar"
javaJarExtension = ".jar"
javaModuleFlag = "--module"
javaModuleFlagShort = "-m"
javaSnapshotSuffix = "-SNAPSHOT"
javaApachePrefix = "org.apache."
dllSuffix = ".dll"
)
var (
javaAllowedFlags = []string{javaJarFlag, javaModuleFlag, javaModuleFlagShort}
)
// List of binaries that usually have additional process context of what's running
var binsWithContext = map[string]serviceExtractorFn{
"python": parseCommandContextPython,
"python2.7": parseCommandContextPython,
"python3": parseCommandContextPython,
"python3.7": parseCommandContextPython,
"ruby2.3": parseCommandContext,
"ruby": parseCommandContext,
"java": parseCommandContextJava,
"java.exe": parseCommandContextJava,
"sudo": parseCommandContext,
"node": parseCommandContextNodeJs,
"node.exe": parseCommandContextNodeJs,
"dotnet": parseCommandContextDotnet,
"dotnet.exe": parseCommandContextDotnet,
}
var _ metadata.Extractor = &ServiceExtractor{}
// ServiceExtractor infers a service tag by extracting it from a process
type ServiceExtractor struct {
enabled bool
useImprovedAlgorithm bool
useWindowsServiceName bool
serviceByPID map[int32]*serviceMetadata
scmReader *scmReader
}
type serviceMetadata struct {
cmdline []string
serviceContext string
}
// WindowsServiceInfo represents service data that is parsed from the SCM. On non-Windows platforms these fields should always be empty.
// On Windows, multiple services can be binpacked into a single `svchost.exe`, which is why `ServiceName` and `DisplayName` are slices.
type WindowsServiceInfo struct {
ServiceName []string
DisplayName []string
}
// NewServiceExtractor instantiates a new service discovery extractor
func NewServiceExtractor(enabled, useWindowsServiceName, useImprovedAlgorithm bool) *ServiceExtractor {
return &ServiceExtractor{
enabled: enabled,
useImprovedAlgorithm: useImprovedAlgorithm,
useWindowsServiceName: useWindowsServiceName,
serviceByPID: make(map[int32]*serviceMetadata),
scmReader: newSCMReader(),
}
}
//nolint:revive // TODO(PROC) Fix revive linter
func (d *ServiceExtractor) Extract(processes map[int32]*procutil.Process) {
if !d.enabled {
return
}
serviceByPID := make(map[int32]*serviceMetadata)
for _, proc := range processes {
if meta, seen := d.serviceByPID[proc.Pid]; seen {
// check the service metadata is for the same process
if len(proc.Cmdline) == len(meta.cmdline) {
if len(proc.Cmdline) == 0 || proc.Cmdline[0] == meta.cmdline[0] {
serviceByPID[proc.Pid] = meta
continue
}
}
}
meta := d.extractServiceMetadata(proc)
if meta != nil && log.ShouldLog(seelog.TraceLvl) {
log.Tracef("detected service metadata: %v", meta)
}
serviceByPID[proc.Pid] = meta
}
d.serviceByPID = serviceByPID
}
//nolint:revive // TODO(PROC) Fix revive linter
func (d *ServiceExtractor) GetServiceContext(pid int32) []string {
if !d.enabled {
return nil
}
if runtime.GOOS == "windows" && d.useWindowsServiceName {
tags, err := d.getWindowsServiceTags(pid)
if err != nil {
log.Warnf("Failed to get service data from SCM for pid %v:%v", pid, err.Error())
}
// Service tag was found from the SCM, return it.
if len(tags) > 0 {
if log.ShouldLog(seelog.TraceLvl) {
log.Tracef("Found process_context from SCM for pid:%v service tags:%v", pid, tags)
}
return tags
}
}
if meta, ok := d.serviceByPID[pid]; ok {
return []string{meta.serviceContext}
}
return nil
}
func (d *ServiceExtractor) extractServiceMetadata(process *procutil.Process) *serviceMetadata {
cmd := process.Cmdline
if len(cmd) == 0 || len(cmd[0]) == 0 {
return &serviceMetadata{
cmdline: cmd,
}
}
// check if all args are packed into the first argument
if len(cmd) == 1 {
cmd = strings.Split(cmd[0], " ")
}
cmdOrig := cmd
envs, cmd := extractEnvsFromCommand(cmd)
if len(envs) > 0 { // evaluate and skip the envs
svc, ok := chooseServiceNameFromEnvs(envs)
if ok {
return &serviceMetadata{
cmdline: cmdOrig,
serviceContext: "process_context:" + svc,
}
}
}
if len(cmd) == 0 || len(cmd[0]) == 0 {
return &serviceMetadata{
cmdline: cmdOrig,
}
}
exe := cmd[0]
// trim any quotes from the executable
exe = strings.Trim(exe, "\"")
// Extract executable from commandline args
exe = trimColonRight(removeFilePath(exe))
if !isRuneLetterAt(exe, 0) {
exe = parseExeStartWithSymbol(exe)
}
if contextFn, ok := binsWithContext[exe]; ok {
tag := contextFn(d, process, cmd[1:])
return &serviceMetadata{
cmdline: cmdOrig,
serviceContext: "process_context:" + tag,
}
}
// trim trailing file extensions
if i := strings.LastIndex(exe, "."); i > 0 {
exe = exe[:i]
}
return &serviceMetadata{
cmdline: cmdOrig,
serviceContext: "process_context:" + exe,
}
}
// GetWindowsServiceTags returns the process_context associated with a process by scraping the SCM.
// If the service name is not found in the scm, a nil slice is returned.
func (d *ServiceExtractor) getWindowsServiceTags(pid int32) ([]string, error) {
entry, err := d.scmReader.getServiceInfo(uint64(pid))
if err != nil {
return nil, err
}
if entry == nil {
return nil, nil
}
serviceTags := make([]string, 0, len(entry.ServiceName))
for _, serviceName := range entry.ServiceName {
serviceTags = append(serviceTags, "process_context:"+serviceName)
}
return serviceTags, nil
}
func removeFilePath(s string) string {
if s != "" {
return filepath.Base(s)
}
return s
}
// trimColonRight will remove any colon and it's associated value right of the string
func trimColonRight(s string) string {
if i := strings.Index(s, ":"); i > 0 {
return s[:i]
}
return s
}
func isRuneLetterAt(s string, position int) bool {
return len(s) > position && unicode.IsLetter(rune(s[position]))
}
// extractEnvsFromCommand separates the env var declaration from the command + args part
func extractEnvsFromCommand(cmd []string) ([]string, []string) {
pos := 0
for _, arg := range cmd {
if !strings.ContainsRune(arg, '=') {
break
}
pos++
}
return cmd[:pos], cmd[pos:]
}
// chooseServiceNameFromEnvs extracts the service name from usual tracer env variables (DD_SERVICE, DD_TAGS).
// returns the service name, true if found, otherwise "", false
func chooseServiceNameFromEnvs(envs []string) (string, bool) {
for _, env := range envs {
if strings.HasPrefix(env, "DD_SERVICE=") {
return strings.TrimPrefix(env, "DD_SERVICE="), true
}
if strings.HasPrefix(env, "DD_TAGS=") && strings.Contains(env, "service:") {
parts := strings.Split(strings.TrimPrefix(env, "DD_TAGS="), ",")
for _, p := range parts {
if strings.HasPrefix(p, "service:") {
return strings.TrimPrefix(p, "service:"), true
}
}
}
}
return "", false
}
// parseExeStartWithSymbol deals with exe that starts with special chars like "(", "-" or "["
func parseExeStartWithSymbol(exe string) string {
if exe == "" {
return exe
}
// drop the first character
result := exe[1:]
// if last character is also special character, also drop it
if result != "" && !isRuneLetterAt(result, len(result)-1) {
result = result[:len(result)-1]
}
return result
}
// In most cases, the best context is the first non-argument / environment variable, if it exists
func parseCommandContext(_ *ServiceExtractor, _ *procutil.Process, args []string) string {
var prevArgIsFlag bool
for _, a := range args {
hasFlagPrefix, isEnvVariable := strings.HasPrefix(a, "-"), strings.ContainsRune(a, '=')
shouldSkipArg := prevArgIsFlag || hasFlagPrefix || isEnvVariable
if !shouldSkipArg {
if c := trimColonRight(removeFilePath(a)); isRuneLetterAt(c, 0) {
return c
}
}
prevArgIsFlag = hasFlagPrefix
}
return ""
}
func parseCommandContextPython(se *ServiceExtractor, _ *procutil.Process, args []string) string {
var (
prevArgIsFlag bool
moduleFlag bool
)
for _, a := range args {
hasFlagPrefix, isEnvVariable := strings.HasPrefix(a, "-"), strings.ContainsRune(a, '=')
shouldSkipArg := prevArgIsFlag || hasFlagPrefix || isEnvVariable
if !shouldSkipArg || moduleFlag {
if c := trimColonRight(removeFilePath(a)); isRuneLetterAt(c, 0) {
if se.useImprovedAlgorithm && !moduleFlag {
return strings.TrimSuffix(c, filepath.Ext(c))
}
return c
}
}
if hasFlagPrefix && a == "-m" {
moduleFlag = true
}
prevArgIsFlag = hasFlagPrefix
}
return ""
}
func parseCommandContextJava(se *ServiceExtractor, process *procutil.Process, args []string) string {
prevArgIsFlag := false
// Look for dd.service
if index := slices.IndexFunc(args, func(arg string) bool { return strings.HasPrefix(arg, "-Ddd.service=") }); index != -1 {
return strings.TrimPrefix(args[index], "-Ddd.service=")
}
for _, a := range args {
hasFlagPrefix := strings.HasPrefix(a, "-")
includesAssignment := strings.ContainsRune(a, '=') ||
strings.HasPrefix(a, "-X") ||
strings.HasPrefix(a, "-javaagent:") ||
strings.HasPrefix(a, "-verbose:")
shouldSkipArg := prevArgIsFlag || hasFlagPrefix || includesAssignment
if !shouldSkipArg {
arg := removeFilePath(a)
if arg = trimColonRight(arg); isRuneLetterAt(arg, 0) {
if strings.HasSuffix(arg, javaJarExtension) {
value, ok := advancedGuessJavaServiceName(se, process, args, a)
if ok {
return value
}
// return the jar
jarName := arg[:len(arg)-len(javaJarExtension)]
if !strings.HasSuffix(jarName, javaSnapshotSuffix) {
return jarName
}
jarName = jarName[:len(jarName)-len(javaSnapshotSuffix)]
if idx := strings.LastIndex(jarName, "-"); idx != -1 {
if _, err := semver.NewVersion(jarName[idx+1:]); err == nil {
return jarName[:idx]
}
}
return jarName
}
if strings.HasPrefix(arg, javaApachePrefix) {
// take the project name after the package 'org.apache.' while stripping off the remaining package
// and class name
arg = arg[len(javaApachePrefix):]
if idx := strings.Index(arg, "."); idx != -1 {
return arg[:idx]
}
}
if idx := strings.LastIndex(arg, "."); idx != -1 && idx+1 < len(arg) {
// take just the class name without the package
return arg[idx+1:]
}
return arg
}
}
prevArgIsFlag = hasFlagPrefix && !includesAssignment && !slices.Contains(javaAllowedFlags, a)
}
return "java"
}
// advancedGuessJavaServiceName inspects a jvm process to extract framework specific metadata that could be used as service name
// if found the function will return the service name and true. Otherwise, "",false
func advancedGuessJavaServiceName(se *ServiceExtractor, process *procutil.Process, args []string, jarname string) (string, bool) {
if !se.useImprovedAlgorithm {
return "", false
}
// try to introspect the jar to get service name from spring application name
// TODO: pass process envs
springAppName, err := javaparser.GetSpringBootAppName(process.Cwd, jarname, args)
if err == nil {
return springAppName, true
}
log.Tracef("Error while trying to extract properties from potential spring boot application: %v", err)
return "", false
}
func parseCommandContextNodeJs(se *ServiceExtractor, process *procutil.Process, args []string) string {
if !se.useImprovedAlgorithm {
return "node"
}
skipNext := false
for _, a := range args {
if skipNext {
skipNext = false
continue
}
if strings.HasPrefix(a, "-") {
if a == "-r" || a == "--require" {
// next arg can be a js file but not the entry point. skip it
skipNext = !strings.ContainsRune(a, '=') // in this case the value is already in this arg
continue
}
} else if strings.HasSuffix(strings.ToLower(a), ".js") {
absFile := abs(filepath.Clean(a), process.Cwd)
if _, err := os.Stat(absFile); err == nil {
value, ok := nodejsparser.FindNameFromNearestPackageJSON(absFile)
if ok {
return value
}
break
}
}
}
return "node"
}
// abs returns the path itself if already absolute or the absolute path by joining cwd with path
// This is a variant of filepath.Abs since on windows it likely returns false when the drive/volume is missing
// hence, since we accept also paths, we test if the first char is a path separator
func abs(path string, cwd string) string {
if !(filepath.IsAbs(path) || path[0] == os.PathSeparator) && len(cwd) > 0 {
return filepath.Join(cwd, path)
}
return path
}
// parseCommandContextDotnet extracts metadata from a dotnet launcher command line
func parseCommandContextDotnet(se *ServiceExtractor, _ *procutil.Process, args []string) string {
if !se.useImprovedAlgorithm {
return "dotnet"
}
for _, a := range args {
if strings.HasPrefix(a, "-") {
continue
}
// when running assembly's dll, the cli must be executed without command
// https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-run#description
if strings.HasSuffix(strings.ToLower(a), dllSuffix) {
_, file := filepath.Split(a)
return file[:len(file)-len(dllSuffix)]
}
// dotnet cli syntax is something like `dotnet <cmd> <args> <dll> <prog args>`
// if the first non arg (`-v, --something, ...) is not a dll file, exit early since nothing is matching a dll execute case
break
}
return "dotnet"
}