Skip to content

Commit 5df8a5a

Browse files
zeitlingertrask
andauthored
Manifest resource detector (#10621)
Co-authored-by: Trask Stalnaker <[email protected]>
1 parent 2701c4d commit 5df8a5a

File tree

10 files changed

+488
-95
lines changed

10 files changed

+488
-95
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.instrumentation.resources;
7+
8+
import com.google.errorprone.annotations.CanIgnoreReturnValue;
9+
import io.opentelemetry.api.common.AttributeKey;
10+
import java.util.Optional;
11+
import java.util.function.Function;
12+
13+
/**
14+
* An easier alternative to {@link io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider}, which
15+
* avoids some common pitfalls and boilerplate.
16+
*
17+
* <p>An example of how to use this interface can be found in {@link ManifestResourceProvider}.
18+
*/
19+
interface AttributeProvider<D> {
20+
Optional<D> readData();
21+
22+
void registerAttributes(Builder<D> builder);
23+
24+
interface Builder<D> {
25+
@CanIgnoreReturnValue
26+
<T> Builder<D> add(AttributeKey<T> key, Function<D, Optional<T>> getter);
27+
}
28+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.instrumentation.resources;
7+
8+
import com.google.errorprone.annotations.CanIgnoreReturnValue;
9+
import io.opentelemetry.api.common.AttributeKey;
10+
import io.opentelemetry.api.common.Attributes;
11+
import io.opentelemetry.api.common.AttributesBuilder;
12+
import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;
13+
import io.opentelemetry.sdk.autoconfigure.spi.internal.ConditionalResourceProvider;
14+
import io.opentelemetry.sdk.resources.Resource;
15+
import io.opentelemetry.semconv.ResourceAttributes;
16+
import java.util.HashMap;
17+
import java.util.Map;
18+
import java.util.Objects;
19+
import java.util.Optional;
20+
import java.util.function.Function;
21+
22+
/**
23+
* An easier alternative to {@link io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider}, which
24+
* avoids some common pitfalls and boilerplate.
25+
*
26+
* <p>An example of how to use this interface can be found in {@link ManifestResourceProvider}.
27+
*/
28+
@SuppressWarnings({"unchecked", "rawtypes"})
29+
public abstract class AttributeResourceProvider<D> implements ConditionalResourceProvider {
30+
31+
private final AttributeProvider<D> attributeProvider;
32+
33+
public class AttributeBuilder implements AttributeProvider.Builder<D> {
34+
35+
private AttributeBuilder() {}
36+
37+
@CanIgnoreReturnValue
38+
@Override
39+
public <T> AttributeBuilder add(AttributeKey<T> key, Function<D, Optional<T>> getter) {
40+
attributeGetters.put((AttributeKey) key, Objects.requireNonNull((Function) getter));
41+
return this;
42+
}
43+
}
44+
45+
private static final ThreadLocal<Resource> existingResource = new ThreadLocal<>();
46+
47+
private final Map<AttributeKey<Object>, Function<D, Optional<?>>> attributeGetters =
48+
new HashMap<>();
49+
50+
public AttributeResourceProvider(AttributeProvider<D> attributeProvider) {
51+
this.attributeProvider = attributeProvider;
52+
attributeProvider.registerAttributes(new AttributeBuilder());
53+
}
54+
55+
@Override
56+
public final boolean shouldApply(ConfigProperties config, Resource existing) {
57+
existingResource.set(existing);
58+
59+
Map<String, String> resourceAttributes = getResourceAttributes(config);
60+
return attributeGetters.keySet().stream()
61+
.allMatch(key -> shouldUpdate(config, existing, key, resourceAttributes));
62+
}
63+
64+
@Override
65+
public final Resource createResource(ConfigProperties config) {
66+
return attributeProvider
67+
.readData()
68+
.map(
69+
data -> {
70+
// what should we do here?
71+
// we don't have access to the existing resource
72+
// if the resource provider produces a single key, we can rely on shouldApply
73+
// i.e. this method won't be called if the key is already present
74+
// the thread local is a hack to work around this
75+
Resource existing =
76+
Objects.requireNonNull(existingResource.get(), "call shouldApply first");
77+
Map<String, String> resourceAttributes = getResourceAttributes(config);
78+
AttributesBuilder builder = Attributes.builder();
79+
attributeGetters.entrySet().stream()
80+
.filter(e -> shouldUpdate(config, existing, e.getKey(), resourceAttributes))
81+
.forEach(
82+
e ->
83+
e.getValue()
84+
.apply(data)
85+
.ifPresent(value -> putAttribute(builder, e.getKey(), value)));
86+
return Resource.create(builder.build());
87+
})
88+
.orElse(Resource.empty());
89+
}
90+
91+
private static <T> void putAttribute(AttributesBuilder builder, AttributeKey<T> key, T value) {
92+
builder.put(key, value);
93+
}
94+
95+
private static Map<String, String> getResourceAttributes(ConfigProperties config) {
96+
return config.getMap("otel.resource.attributes");
97+
}
98+
99+
private static boolean shouldUpdate(
100+
ConfigProperties config,
101+
Resource existing,
102+
AttributeKey<?> key,
103+
Map<String, String> resourceAttributes) {
104+
if (resourceAttributes.containsKey(key.getKey())) {
105+
return false;
106+
}
107+
108+
Object value = existing.getAttribute(key);
109+
110+
if (key.equals(ResourceAttributes.SERVICE_NAME)) {
111+
return config.getString("otel.service.name") == null && "unknown_service:java".equals(value);
112+
}
113+
114+
return value == null;
115+
}
116+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.instrumentation.resources;
7+
8+
import java.nio.file.Files;
9+
import java.nio.file.InvalidPathException;
10+
import java.nio.file.Path;
11+
import java.nio.file.Paths;
12+
import java.util.Optional;
13+
import java.util.function.Function;
14+
import java.util.function.Predicate;
15+
import java.util.function.Supplier;
16+
import javax.annotation.Nullable;
17+
18+
class JarPathFinder {
19+
private final Supplier<String[]> getProcessHandleArguments;
20+
private final Function<String, String> getSystemProperty;
21+
private final Predicate<Path> fileExists;
22+
23+
private static class DetectionResult {
24+
private final Optional<Path> jarPath;
25+
26+
private DetectionResult(Optional<Path> jarPath) {
27+
this.jarPath = jarPath;
28+
}
29+
}
30+
31+
private static Optional<DetectionResult> detectionResult = Optional.empty();
32+
33+
public JarPathFinder() {
34+
this(ProcessArguments::getProcessArguments, System::getProperty, Files::isRegularFile);
35+
}
36+
37+
// visible for tests
38+
JarPathFinder(
39+
Supplier<String[]> getProcessHandleArguments,
40+
Function<String, String> getSystemProperty,
41+
Predicate<Path> fileExists) {
42+
this.getProcessHandleArguments = getProcessHandleArguments;
43+
this.getSystemProperty = getSystemProperty;
44+
this.fileExists = fileExists;
45+
}
46+
47+
// visible for testing
48+
static void resetForTest() {
49+
detectionResult = Optional.empty();
50+
}
51+
52+
Optional<Path> getJarPath() {
53+
if (!detectionResult.isPresent()) {
54+
detectionResult = Optional.of(new DetectionResult(Optional.ofNullable(detectJarPath())));
55+
}
56+
return detectionResult.get().jarPath;
57+
}
58+
59+
private Path detectJarPath() {
60+
Path jarPath = getJarPathFromProcessHandle();
61+
if (jarPath != null) {
62+
return jarPath;
63+
}
64+
return getJarPathFromSunCommandLine();
65+
}
66+
67+
@Nullable
68+
private Path getJarPathFromProcessHandle() {
69+
String[] javaArgs = getProcessHandleArguments.get();
70+
for (int i = 0; i < javaArgs.length; ++i) {
71+
if ("-jar".equals(javaArgs[i]) && (i < javaArgs.length - 1)) {
72+
return Paths.get(javaArgs[i + 1]);
73+
}
74+
}
75+
return null;
76+
}
77+
78+
@Nullable
79+
private Path getJarPathFromSunCommandLine() {
80+
// the jar file is the first argument in the command line string
81+
String programArguments = getSystemProperty.apply("sun.java.command");
82+
if (programArguments == null) {
83+
return null;
84+
}
85+
86+
// Take the path until the first space. If the path doesn't exist extend it up to the next
87+
// space. Repeat until a path that exists is found or input runs out.
88+
int next = 0;
89+
while (true) {
90+
int nextSpace = programArguments.indexOf(' ', next);
91+
if (nextSpace == -1) {
92+
return pathIfExists(programArguments);
93+
}
94+
Path path = pathIfExists(programArguments.substring(0, nextSpace));
95+
next = nextSpace + 1;
96+
if (path != null) {
97+
return path;
98+
}
99+
}
100+
}
101+
102+
@Nullable
103+
private Path pathIfExists(String programArguments) {
104+
Path candidate;
105+
try {
106+
candidate = Paths.get(programArguments);
107+
} catch (InvalidPathException e) {
108+
return null;
109+
}
110+
return fileExists.test(candidate) ? candidate : null;
111+
}
112+
}

instrumentation/resources/library/src/main/java/io/opentelemetry/instrumentation/resources/JarServiceNameDetector.java

Lines changed: 14 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,9 @@
1414
import io.opentelemetry.sdk.autoconfigure.spi.internal.ConditionalResourceProvider;
1515
import io.opentelemetry.sdk.resources.Resource;
1616
import io.opentelemetry.semconv.ResourceAttributes;
17-
import java.nio.file.Files;
18-
import java.nio.file.InvalidPathException;
1917
import java.nio.file.Path;
20-
import java.nio.file.Paths;
2118
import java.util.Map;
22-
import java.util.function.Function;
23-
import java.util.function.Predicate;
24-
import java.util.function.Supplier;
2519
import java.util.logging.Logger;
26-
import javax.annotation.Nullable;
2720

2821
/**
2922
* A {@link ResourceProvider} that will attempt to detect the application name from the jar name.
@@ -33,37 +26,30 @@ public final class JarServiceNameDetector implements ConditionalResourceProvider
3326

3427
private static final Logger logger = Logger.getLogger(JarServiceNameDetector.class.getName());
3528

36-
private final Supplier<String[]> getProcessHandleArguments;
37-
private final Function<String, String> getSystemProperty;
38-
private final Predicate<Path> fileExists;
29+
private final JarPathFinder jarPathFinder;
3930

4031
@SuppressWarnings("unused") // SPI
4132
public JarServiceNameDetector() {
42-
this(ProcessArguments::getProcessArguments, System::getProperty, Files::isRegularFile);
33+
this(new JarPathFinder());
4334
}
4435

4536
// visible for tests
46-
JarServiceNameDetector(
47-
Supplier<String[]> getProcessHandleArguments,
48-
Function<String, String> getSystemProperty,
49-
Predicate<Path> fileExists) {
50-
this.getProcessHandleArguments = getProcessHandleArguments;
51-
this.getSystemProperty = getSystemProperty;
52-
this.fileExists = fileExists;
37+
JarServiceNameDetector(JarPathFinder jarPathFinder) {
38+
this.jarPathFinder = jarPathFinder;
5339
}
5440

5541
@Override
5642
public Resource createResource(ConfigProperties config) {
57-
Path jarPath = getJarPathFromProcessHandle();
58-
if (jarPath == null) {
59-
jarPath = getJarPathFromSunCommandLine();
60-
}
61-
if (jarPath == null) {
62-
return Resource.empty();
63-
}
64-
String serviceName = getServiceName(jarPath);
65-
logger.log(FINE, "Auto-detected service name from the jar file name: {0}", serviceName);
66-
return Resource.create(Attributes.of(ResourceAttributes.SERVICE_NAME, serviceName));
43+
return jarPathFinder
44+
.getJarPath()
45+
.map(
46+
jarPath -> {
47+
String serviceName = getServiceName(jarPath);
48+
logger.log(
49+
FINE, "Auto-detected service name from the jar file name: {0}", serviceName);
50+
return Resource.create(Attributes.of(ResourceAttributes.SERVICE_NAME, serviceName));
51+
})
52+
.orElseGet(Resource::empty);
6753
}
6854

6955
@Override
@@ -75,52 +61,6 @@ public boolean shouldApply(ConfigProperties config, Resource existing) {
7561
&& "unknown_service:java".equals(existing.getAttribute(ResourceAttributes.SERVICE_NAME));
7662
}
7763

78-
@Nullable
79-
private Path getJarPathFromProcessHandle() {
80-
String[] javaArgs = getProcessHandleArguments.get();
81-
for (int i = 0; i < javaArgs.length; ++i) {
82-
if ("-jar".equals(javaArgs[i]) && (i < javaArgs.length - 1)) {
83-
return Paths.get(javaArgs[i + 1]);
84-
}
85-
}
86-
return null;
87-
}
88-
89-
@Nullable
90-
private Path getJarPathFromSunCommandLine() {
91-
// the jar file is the first argument in the command line string
92-
String programArguments = getSystemProperty.apply("sun.java.command");
93-
if (programArguments == null) {
94-
return null;
95-
}
96-
97-
// Take the path until the first space. If the path doesn't exist extend it up to the next
98-
// space. Repeat until a path that exists is found or input runs out.
99-
int next = 0;
100-
while (true) {
101-
int nextSpace = programArguments.indexOf(' ', next);
102-
if (nextSpace == -1) {
103-
return pathIfExists(programArguments);
104-
}
105-
Path path = pathIfExists(programArguments.substring(0, nextSpace));
106-
next = nextSpace + 1;
107-
if (path != null) {
108-
return path;
109-
}
110-
}
111-
}
112-
113-
@Nullable
114-
private Path pathIfExists(String programArguments) {
115-
Path candidate;
116-
try {
117-
candidate = Paths.get(programArguments);
118-
} catch (InvalidPathException e) {
119-
return null;
120-
}
121-
return fileExists.test(candidate) ? candidate : null;
122-
}
123-
12464
private static String getServiceName(Path jarPath) {
12565
String jarName = jarPath.getFileName().toString();
12666
int dotIndex = jarName.lastIndexOf(".");

0 commit comments

Comments
 (0)