forked from elastic/apm-agent-java
-
Notifications
You must be signed in to change notification settings - Fork 0
/
SystemInfo.java
584 lines (516 loc) · 21.7 KB
/
SystemInfo.java
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
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package co.elastic.apm.agent.impl.metadata;
import co.elastic.apm.agent.common.util.ProcessExecutionUtil;
import co.elastic.apm.agent.configuration.ServerlessConfiguration;
import co.elastic.apm.agent.sdk.logging.Logger;
import co.elastic.apm.agent.sdk.logging.LoggerFactory;
import javax.annotation.Nullable;
import java.net.InetAddress;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static co.elastic.apm.agent.common.util.ProcessExecutionUtil.cmdAsString;
/**
* Information about the system the agent is running on.
*/
public class SystemInfo {
private static final Logger logger = LoggerFactory.getLogger(SystemInfo.class);
private static final String CONTAINER_REGEX_64 = "[0-9a-fA-F]{64}";
private static final String CONTAINER_UID_REGEX = "^" + CONTAINER_REGEX_64 + "$";
private static final String SHORTENED_UUID_PATTERN = "^[0-9a-fA-F]{8}\\-[0-9a-fA-F]{4}\\-[0-9a-fA-F]{4}\\-[0-9a-fA-F]{4}\\-[0-9a-fA-F]{4,}";
private static final String AWS_FARGATE_UID_REGEX = "^[0-9a-fA-F]{32}\\-[0-9]{10}$";
private static final String POD_REGEX = "(?:^/kubepods[\\S]*/pod([^/]+)$)|(?:kubepods[^/]*-pod([^/]+)\\.slice)";
private static final String CGROUPV2_HOSTNAME_FILE = "/etc/hostname";
private static final Pattern CGROUPV2_CONTAINER_PATTERN = Pattern.compile("^.*(" + CONTAINER_REGEX_64 + ").*$");
private static final String SELF_CGROUP = "/proc/self/cgroup";
private static final String SELF_MOUNTINFO = "/proc/self/mountinfo";
/**
* Architecture of the system the agent is running on.
*/
private final String architecture;
/**
* Hostname configured manually through {@link co.elastic.apm.agent.configuration.CoreConfiguration#hostname}.
*/
@SuppressWarnings("JavadocReference")
@Nullable
private final String configuredHostname;
/**
* Hostname detected automatically.
*/
@Nullable
private final String detectedHostname;
/**
* Name of the system platform the agent is running on.
*/
private final String platform;
/**
* Info about the container the agent is running on, where applies
*/
@Nullable
private Container container;
/**
* Info about the Kubernetes pod/node the agent is running on, where applies
*/
@Nullable
private Kubernetes kubernetes;
public SystemInfo(String architecture, @Nullable String configuredHostname, @Nullable String detectedHostname, String platform) {
this(architecture, configuredHostname, detectedHostname, platform, null, null);
}
SystemInfo(String architecture, @Nullable String configuredHostname, @Nullable String detectedHostname,
String platform, @Nullable Container container, @Nullable Kubernetes kubernetes) {
this.architecture = architecture;
this.configuredHostname = configuredHostname;
this.detectedHostname = detectedHostname;
this.platform = platform;
this.container = container;
this.kubernetes = kubernetes;
}
/**
* Creates a {@link SystemInfo} containing auto-discovered info about the system.
* This method may block on reading files and executing external processes.
*
* @param configuredHostname hostname configured through the {@link co.elastic.apm.agent.configuration.CoreConfiguration#hostname} config
* @param timeoutMillis enables to limit the execution of the system discovery task
* @param serverlessConfiguration serverless config
* @return a future from which this system's info can be obtained
*/
@SuppressWarnings("JavadocReference")
public static SystemInfo create(final @Nullable String configuredHostname, final long timeoutMillis, ServerlessConfiguration serverlessConfiguration) {
final String osName = System.getProperty("os.name");
final String osArch = System.getProperty("os.arch");
if (serverlessConfiguration.runsOnAwsLambda()) {
return new SystemInfo(osArch, null, null, osName);
}
SystemInfo systemInfo;
if (configuredHostname != null && !configuredHostname.isEmpty()) {
systemInfo = new SystemInfo(osArch, configuredHostname, null, osName);
} else {
// this call is invoking external commands
String detectedHostname = discoverHostname(isWindows(osName), timeoutMillis);
systemInfo = new SystemInfo(osArch, configuredHostname, detectedHostname, osName);
}
// this call reads and parses files
systemInfo.findContainerDetails();
return systemInfo;
}
static boolean isWindows(String osName) {
return osName.startsWith("Windows");
}
/**
* Discover the current host's name. This method separates operating systems only to Windows and non-Windows,
* both in the executed hostname-discovery-command and the fallback environment variables.
* It always starts with execution of a command on an external process, so it may block up to the specified timeout.
*
* @param isWindows used to decide how hostname discovery should be executed
* @param timeoutMillis limits the time this method may block on executing external commands
* @return the discovered hostname
*/
@Nullable
static String discoverHostname(boolean isWindows, long timeoutMillis) {
String hostname = discoverHostnameThroughCommand(isWindows, timeoutMillis);
if (hostname == null || hostname.isEmpty()) {
hostname = fallbackHostnameDiscovery(isWindows);
}
if (hostname == null || hostname.isEmpty()) {
logger.warn("Unable to discover hostname, set log_level to debug for more details");
}
return hostname;
}
@Nullable
static String fallbackHostnameDiscovery(boolean isWindows) {
String hostname = discoverHostnameThroughEnv(isWindows);
if (hostname == null || hostname.isEmpty()) {
try {
hostname = InetAddress.getLocalHost().getHostName();
} catch (Exception e) {
logger.warn("Last fallback for hostname discovery of localhost failed", e);
}
}
return hostname;
}
@Nullable
static String discoverHostnameThroughCommand(boolean isWindows, long timeoutMillis) {
String hostname;
if (isWindows) {
hostname = executeHostnameDiscoveryCommand(Arrays.asList("powershell.exe", "[System.Net.Dns]::GetHostEntry($env:computerName).HostName"), timeoutMillis);
if (hostname == null || hostname.isEmpty()) {
hostname = executeHostnameDiscoveryCommand(Arrays.asList("cmd.exe", "/c", "hostname"), timeoutMillis);
}
} else {
hostname = executeHostnameDiscoveryCommand(Arrays.asList("hostname", "-f"), timeoutMillis);
}
return hostname;
}
/**
* Tries to discover the current host name by executing the provided command in a spawned process.
* This method may block up to the specified timeout, waiting for the spawned process to terminate.
*
* @param cmd the hostname discovery command
* @param timeoutMillis maximum time to allow to the provided command to execute
* @return the discovered hostname
*/
@Nullable
private static String executeHostnameDiscoveryCommand(List<String> cmd, long timeoutMillis) {
String hostname = null;
ProcessExecutionUtil.CommandOutput commandOutput = ProcessExecutionUtil.executeCommand(cmd, timeoutMillis);
if (commandOutput.exitedNormally()) {
hostname = commandOutput.getOutput().toString().trim();
if (logger.isDebugEnabled()) {
logger.debug("hostname obtained by executing command {}: {}", cmdAsString(cmd), hostname);
}
} else {
logger.info("Failed to execute command {} with exit code {}", cmdAsString(cmd), commandOutput.getExitCode());
logger.debug("Command execution error", commandOutput.getExceptionThrown());
}
if(hostname != null) {
hostname = hostname.toLowerCase(Locale.ROOT);
}
return hostname;
}
/**
* Returns the hostname from environment variables.
* <br/>
* Note for Windows: the Windows implementation relies on the COMPUTERNAME environment variable that does not
* 100% matches the computer name: the returned value is the "netbios name" in upper-case and limited to the first
* 15 characters of the complete computer name returned by {@code hostname} command.
*
* @param isWindows {@literal true} for Windows
* @return computer/host name from environment variables.
*/
@Nullable
static String discoverHostnameThroughEnv(boolean isWindows) {
String hostname;
if (isWindows) {
// Windows implementation will always return an upper-case name
// limited to the 15 first characters of the actual computer name
hostname = System.getenv("COMPUTERNAME");
} else {
hostname = System.getenv("HOSTNAME");
if (hostname == null || hostname.isEmpty()) {
hostname = System.getenv("HOST");
}
}
if (hostname != null) {
hostname = hostname.toLowerCase(Locale.ROOT);
}
return hostname;
}
/**
* Finding the container ID based on the {@code /proc/self/cgroup} file.
* Each line in this file represents a control group hierarchy of the form
* <p>
* {@code \d+:([^:,]+(?:,[^:,]+)?):(/.*)}
* <p>
* with the first field representing the hierarchy ID, the second field representing a comma-separated list of the subsystems bound to
* the hierarchy, and the last field representing the control group.
*
* @return container ID parsed from {@code /proc/self/cgroup} file lines, or {@code null} if can't find/read/parse file lines
*/
SystemInfo findContainerDetails() {
parseCgroupsFile(FileSystems.getDefault().getPath(SELF_CGROUP));
if (container == null) {
parseMountInfo(FileSystems.getDefault().getPath(SELF_MOUNTINFO));
}
try {
// Kubernetes Downward API enables setting environment variables. We are looking for the relevant ones to this discovery
String podUid = System.getenv("KUBERNETES_POD_UID");
String podName = System.getenv("KUBERNETES_POD_NAME");
String nodeName = System.getenv("KUBERNETES_NODE_NAME");
String namespace = System.getenv("KUBERNETES_NAMESPACE");
if (podUid != null || podName != null || nodeName != null || namespace != null) {
// avoid overriding valid info with invalid info
if (kubernetes != null) {
if (kubernetes.getPod() != null) {
podUid = (podUid != null) ? podUid : kubernetes.getPod().getUid();
podName = (podName != null) ? podName : kubernetes.getPod().getName();
}
}
kubernetes = new Kubernetes(podName, nodeName, namespace, podUid);
}
} catch (Throwable e) {
logger.warn("Failed to read environment variables for Kubernetes Downward API discovery", e);
}
logger.debug("container ID is {}", container != null ? container.getId() : null);
return this;
}
@Nullable
private void parseMountInfo(Path path) {
if (!Files.isRegularFile(path)) {
logger.debug("Could not parse container ID from '{}'", path);
return;
}
try {
List<String> lines = Files.readAllLines(path, StandardCharsets.UTF_8);
parseCgroupsV2ContainerId(lines);
if (container != null) {
return;
}
logger.debug("Could not parse container ID from '{}' lines: {}", path, lines);
} catch (Throwable e) {
logger.warn(String.format("Failed to read/parse container ID from '%s'", path), e);
}
}
@Nullable
private void parseCgroupsFile(Path path) {
if(!Files.isRegularFile(path)){
logger.debug("Could not parse container ID from '{}'", path);
return;
}
try {
List<String> lines = Files.readAllLines(path, StandardCharsets.UTF_8);
for (String line : lines) {
parseCgroupsLine(line);
if (container != null) {
return;
}
}
} catch (Throwable e) {
logger.warn(String.format("Failed to read/parse container ID from '%s'", path), e);
}
}
/**
* The virtual file /proc/self/cgroup lists the control groups that the process is a member of. Each line contains
* three colon-separated fields of the form hierarchy-ID:subsystem-list:cgroup-path.
* <p>
* Depending on the filesystem driver used for cgroup management, the cgroup-path will have
* one of the following formats in a Docker container:
* </p>
* <pre>
* systemd: /system.slice/docker-<container-ID>.scope
* cgroupfs: /docker/<container-ID>
* </pre>
* In a Kubernetes pod, the cgroup path will look like:
* <pre>
* systemd: /kubepods.slice/kubepods-<QoS-class>.slice/kubepods-<QoS-class>-pod<pod-UID>.slice/<container-iD>.scope
* cgroupfs: /kubepods/<QoS-class>/pod<pod-UID>/<container-iD>
* </pre
*
* @param line a line from the /proc/self/cgroup file
* @return this SystemInfo object after parsing
*/
SystemInfo parseCgroupsLine(String line) {
final String[] fields = line.split(":", 3);
if (fields.length == 3) {
String cGroupPath = fields[2];
// Looking whether the cgroup path part is delimited with `:`, e.g. in containerd cri
int indexOfIdSeparator = cGroupPath.lastIndexOf(':');
if (indexOfIdSeparator < 0) {
indexOfIdSeparator = cGroupPath.lastIndexOf('/');
}
if (indexOfIdSeparator >= 0) {
String idPart = cGroupPath.substring(indexOfIdSeparator + 1);
// Legacy, e.g.: /system.slice/docker-<CID>.scope
if (idPart.endsWith(".scope")) {
idPart = idPart.substring(0, idPart.length() - ".scope".length()).substring(idPart.indexOf("-") + 1);
}
// Looking for kubernetes info
String dir = cGroupPath.substring(0, indexOfIdSeparator);
if (dir.length() > 0) {
final Pattern pattern = Pattern.compile(POD_REGEX);
final Matcher matcher = pattern.matcher(dir);
if (matcher.find()) {
for (int i = 1; i <= matcher.groupCount(); i++) {
String podUid = matcher.group(i);
if (podUid != null && !podUid.isEmpty()) {
// systemd cgroup driver is being used, so we need to unescape '_' back to '-'.
podUid = podUid.replace('_', '-');
logger.debug("Found Kubernetes pod UID: {}", podUid);
// By default, Kubernetes will set the hostname of the pod containers to the pod name. Users that override
// the name should use the Downward API to override the pod name or override the hostname through the hostname config.
kubernetes = new Kubernetes(getHostname(), null, null, podUid);
break;
}
}
}
}
// If the line matched the one of the kubernetes patterns, we assume that the last part is always the container ID.
// Otherwise we validate that it is a 64-length hex string
if (kubernetes != null ||
idPart.matches(CONTAINER_UID_REGEX) ||
idPart.matches(SHORTENED_UUID_PATTERN) ||
idPart.matches(AWS_FARGATE_UID_REGEX)) {
container = new Container(idPart);
}
}
}
if (container == null) {
logger.debug("Could not parse container ID from line: {}", line);
}
return this;
}
/**
* @param lines lines from the /proc/self/mountinfo file
* @return this SystemInfo object after parsing
*/
SystemInfo parseCgroupsV2ContainerId(List<String> lines) {
for (String line : lines) {
int index = line.indexOf(CGROUPV2_HOSTNAME_FILE);
if (index > 0) {
String[] parts = line.split(" ");
if (parts.length > 3) {
Matcher matcher = CGROUPV2_CONTAINER_PATTERN.matcher(parts[3]);
if (matcher.matches() && matcher.groupCount() == 1) {
container = new Container(matcher.group(1));
}
}
}
}
return this;
}
/**
* Architecture of the system the agent is running on.
*/
public String getArchitecture() {
return architecture;
}
/**
* Returns the hostname. If a non-empty hostname was configured manually, it will be returned.
* Otherwise, the automatically discovered hostname will be returned.
* If both are null or empty, this method returns {@code <unknown>}.
*
* @deprecated should only be used when communicating to APM Server of version lower than 7.4
*/
@Deprecated
public String getHostname() {
if (configuredHostname != null && !configuredHostname.isEmpty()) {
return configuredHostname;
}
if (detectedHostname != null && !detectedHostname.isEmpty()) {
return detectedHostname;
}
return "<unknown>";
}
/**
* The hostname manually configured through {@link co.elastic.apm.agent.configuration.CoreConfiguration#hostname}
*
* @return the manually configured hostname
*/
@SuppressWarnings("JavadocReference")
@Nullable
public String getConfiguredHostname() {
return configuredHostname;
}
/**
* @return the automatically discovered hostname
*/
@Nullable
public String getDetectedHostname() {
return detectedHostname;
}
/**
* Name of the system platform the agent is running on.
*/
public String getPlatform() {
return platform;
}
/**
* Info about the container this agent is running on, where applies
*
* @return container info
*/
@Nullable
public Container getContainerInfo() {
return container;
}
/**
* Info about the kubernetes Pod and Node this agent is running on, where applies
*
* @return container info
*/
@Nullable
public Kubernetes getKubernetesInfo() {
return kubernetes;
}
public static class Container {
private final String id;
Container(String id) {
this.id = id;
}
public String getId() {
return id;
}
}
public static class Kubernetes {
@Nullable
Pod pod;
@Nullable
Node node;
@Nullable
private String namespace;
Kubernetes(@Nullable String podName, @Nullable String nodeName, @Nullable String namespace, @Nullable String podUid) {
if (podName != null || podUid != null) {
pod = new Pod(podName, podUid);
}
if (nodeName != null) {
node = new Node(nodeName);
}
this.namespace = namespace;
}
@Nullable
public Pod getPod() {
return pod;
}
@Nullable
public Node getNode() {
return node;
}
@Nullable
public String getNamespace() {
return namespace;
}
public boolean hasContent() {
return pod != null || node != null || namespace != null;
}
public static class Pod {
@Nullable
private String name;
@Nullable
private String uid;
Pod(@Nullable String name, @Nullable String uid) {
this.name = name;
this.uid = uid;
}
@Nullable
public String getName() {
return name;
}
@Nullable
public String getUid() {
return uid;
}
}
public static class Node {
private String name;
Node(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
}
}