Skip to content

Commit

Permalink
Manifest resource detector (#10621)
Browse files Browse the repository at this point in the history
Co-authored-by: Trask Stalnaker <trask.stalnaker@gmail.com>
  • Loading branch information
zeitlinger and trask committed Mar 13, 2024
1 parent 2701c4d commit 5df8a5a
Show file tree
Hide file tree
Showing 10 changed files with 488 additions and 95 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.instrumentation.resources;

import com.google.errorprone.annotations.CanIgnoreReturnValue;
import io.opentelemetry.api.common.AttributeKey;
import java.util.Optional;
import java.util.function.Function;

/**
* An easier alternative to {@link io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider}, which
* avoids some common pitfalls and boilerplate.
*
* <p>An example of how to use this interface can be found in {@link ManifestResourceProvider}.
*/
interface AttributeProvider<D> {
Optional<D> readData();

void registerAttributes(Builder<D> builder);

interface Builder<D> {
@CanIgnoreReturnValue
<T> Builder<D> add(AttributeKey<T> key, Function<D, Optional<T>> getter);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.instrumentation.resources;

import com.google.errorprone.annotations.CanIgnoreReturnValue;
import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.common.AttributesBuilder;
import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;
import io.opentelemetry.sdk.autoconfigure.spi.internal.ConditionalResourceProvider;
import io.opentelemetry.sdk.resources.Resource;
import io.opentelemetry.semconv.ResourceAttributes;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Function;

/**
* An easier alternative to {@link io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider}, which
* avoids some common pitfalls and boilerplate.
*
* <p>An example of how to use this interface can be found in {@link ManifestResourceProvider}.
*/
@SuppressWarnings({"unchecked", "rawtypes"})
public abstract class AttributeResourceProvider<D> implements ConditionalResourceProvider {

private final AttributeProvider<D> attributeProvider;

public class AttributeBuilder implements AttributeProvider.Builder<D> {

private AttributeBuilder() {}

@CanIgnoreReturnValue
@Override
public <T> AttributeBuilder add(AttributeKey<T> key, Function<D, Optional<T>> getter) {
attributeGetters.put((AttributeKey) key, Objects.requireNonNull((Function) getter));
return this;
}
}

private static final ThreadLocal<Resource> existingResource = new ThreadLocal<>();

private final Map<AttributeKey<Object>, Function<D, Optional<?>>> attributeGetters =
new HashMap<>();

public AttributeResourceProvider(AttributeProvider<D> attributeProvider) {
this.attributeProvider = attributeProvider;
attributeProvider.registerAttributes(new AttributeBuilder());
}

@Override
public final boolean shouldApply(ConfigProperties config, Resource existing) {
existingResource.set(existing);

Map<String, String> resourceAttributes = getResourceAttributes(config);
return attributeGetters.keySet().stream()
.allMatch(key -> shouldUpdate(config, existing, key, resourceAttributes));
}

@Override
public final Resource createResource(ConfigProperties config) {
return attributeProvider
.readData()
.map(
data -> {
// what should we do here?
// we don't have access to the existing resource
// if the resource provider produces a single key, we can rely on shouldApply
// i.e. this method won't be called if the key is already present
// the thread local is a hack to work around this
Resource existing =
Objects.requireNonNull(existingResource.get(), "call shouldApply first");
Map<String, String> resourceAttributes = getResourceAttributes(config);
AttributesBuilder builder = Attributes.builder();
attributeGetters.entrySet().stream()
.filter(e -> shouldUpdate(config, existing, e.getKey(), resourceAttributes))
.forEach(
e ->
e.getValue()
.apply(data)
.ifPresent(value -> putAttribute(builder, e.getKey(), value)));
return Resource.create(builder.build());
})
.orElse(Resource.empty());
}

private static <T> void putAttribute(AttributesBuilder builder, AttributeKey<T> key, T value) {
builder.put(key, value);
}

private static Map<String, String> getResourceAttributes(ConfigProperties config) {
return config.getMap("otel.resource.attributes");
}

private static boolean shouldUpdate(
ConfigProperties config,
Resource existing,
AttributeKey<?> key,
Map<String, String> resourceAttributes) {
if (resourceAttributes.containsKey(key.getKey())) {
return false;
}

Object value = existing.getAttribute(key);

if (key.equals(ResourceAttributes.SERVICE_NAME)) {
return config.getString("otel.service.name") == null && "unknown_service:java".equals(value);
}

return value == null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.instrumentation.resources;

import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Optional;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
import javax.annotation.Nullable;

class JarPathFinder {
private final Supplier<String[]> getProcessHandleArguments;
private final Function<String, String> getSystemProperty;
private final Predicate<Path> fileExists;

private static class DetectionResult {
private final Optional<Path> jarPath;

private DetectionResult(Optional<Path> jarPath) {
this.jarPath = jarPath;
}
}

private static Optional<DetectionResult> detectionResult = Optional.empty();

public JarPathFinder() {
this(ProcessArguments::getProcessArguments, System::getProperty, Files::isRegularFile);
}

// visible for tests
JarPathFinder(
Supplier<String[]> getProcessHandleArguments,
Function<String, String> getSystemProperty,
Predicate<Path> fileExists) {
this.getProcessHandleArguments = getProcessHandleArguments;
this.getSystemProperty = getSystemProperty;
this.fileExists = fileExists;
}

// visible for testing
static void resetForTest() {
detectionResult = Optional.empty();
}

Optional<Path> getJarPath() {
if (!detectionResult.isPresent()) {
detectionResult = Optional.of(new DetectionResult(Optional.ofNullable(detectJarPath())));
}
return detectionResult.get().jarPath;
}

private Path detectJarPath() {
Path jarPath = getJarPathFromProcessHandle();
if (jarPath != null) {
return jarPath;
}
return getJarPathFromSunCommandLine();
}

@Nullable
private Path getJarPathFromProcessHandle() {
String[] javaArgs = getProcessHandleArguments.get();
for (int i = 0; i < javaArgs.length; ++i) {
if ("-jar".equals(javaArgs[i]) && (i < javaArgs.length - 1)) {
return Paths.get(javaArgs[i + 1]);
}
}
return null;
}

@Nullable
private Path getJarPathFromSunCommandLine() {
// the jar file is the first argument in the command line string
String programArguments = getSystemProperty.apply("sun.java.command");
if (programArguments == null) {
return null;
}

// Take the path until the first space. If the path doesn't exist extend it up to the next
// space. Repeat until a path that exists is found or input runs out.
int next = 0;
while (true) {
int nextSpace = programArguments.indexOf(' ', next);
if (nextSpace == -1) {
return pathIfExists(programArguments);
}
Path path = pathIfExists(programArguments.substring(0, nextSpace));
next = nextSpace + 1;
if (path != null) {
return path;
}
}
}

@Nullable
private Path pathIfExists(String programArguments) {
Path candidate;
try {
candidate = Paths.get(programArguments);
} catch (InvalidPathException e) {
return null;
}
return fileExists.test(candidate) ? candidate : null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,9 @@
import io.opentelemetry.sdk.autoconfigure.spi.internal.ConditionalResourceProvider;
import io.opentelemetry.sdk.resources.Resource;
import io.opentelemetry.semconv.ResourceAttributes;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Map;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.logging.Logger;
import javax.annotation.Nullable;

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

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

private final Supplier<String[]> getProcessHandleArguments;
private final Function<String, String> getSystemProperty;
private final Predicate<Path> fileExists;
private final JarPathFinder jarPathFinder;

@SuppressWarnings("unused") // SPI
public JarServiceNameDetector() {
this(ProcessArguments::getProcessArguments, System::getProperty, Files::isRegularFile);
this(new JarPathFinder());
}

// visible for tests
JarServiceNameDetector(
Supplier<String[]> getProcessHandleArguments,
Function<String, String> getSystemProperty,
Predicate<Path> fileExists) {
this.getProcessHandleArguments = getProcessHandleArguments;
this.getSystemProperty = getSystemProperty;
this.fileExists = fileExists;
JarServiceNameDetector(JarPathFinder jarPathFinder) {
this.jarPathFinder = jarPathFinder;
}

@Override
public Resource createResource(ConfigProperties config) {
Path jarPath = getJarPathFromProcessHandle();
if (jarPath == null) {
jarPath = getJarPathFromSunCommandLine();
}
if (jarPath == null) {
return Resource.empty();
}
String serviceName = getServiceName(jarPath);
logger.log(FINE, "Auto-detected service name from the jar file name: {0}", serviceName);
return Resource.create(Attributes.of(ResourceAttributes.SERVICE_NAME, serviceName));
return jarPathFinder
.getJarPath()
.map(
jarPath -> {
String serviceName = getServiceName(jarPath);
logger.log(
FINE, "Auto-detected service name from the jar file name: {0}", serviceName);
return Resource.create(Attributes.of(ResourceAttributes.SERVICE_NAME, serviceName));
})
.orElseGet(Resource::empty);
}

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

@Nullable
private Path getJarPathFromProcessHandle() {
String[] javaArgs = getProcessHandleArguments.get();
for (int i = 0; i < javaArgs.length; ++i) {
if ("-jar".equals(javaArgs[i]) && (i < javaArgs.length - 1)) {
return Paths.get(javaArgs[i + 1]);
}
}
return null;
}

@Nullable
private Path getJarPathFromSunCommandLine() {
// the jar file is the first argument in the command line string
String programArguments = getSystemProperty.apply("sun.java.command");
if (programArguments == null) {
return null;
}

// Take the path until the first space. If the path doesn't exist extend it up to the next
// space. Repeat until a path that exists is found or input runs out.
int next = 0;
while (true) {
int nextSpace = programArguments.indexOf(' ', next);
if (nextSpace == -1) {
return pathIfExists(programArguments);
}
Path path = pathIfExists(programArguments.substring(0, nextSpace));
next = nextSpace + 1;
if (path != null) {
return path;
}
}
}

@Nullable
private Path pathIfExists(String programArguments) {
Path candidate;
try {
candidate = Paths.get(programArguments);
} catch (InvalidPathException e) {
return null;
}
return fileExists.test(candidate) ? candidate : null;
}

private static String getServiceName(Path jarPath) {
String jarName = jarPath.getFileName().toString();
int dotIndex = jarName.lastIndexOf(".");
Expand Down
Loading

0 comments on commit 5df8a5a

Please sign in to comment.