Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Manifest resource detector #10621

Merged
merged 14 commits into from
Mar 13, 2024
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"})
zeitlinger marked this conversation as resolved.
Show resolved Hide resolved
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<>();
zeitlinger marked this conversation as resolved.
Show resolved Hide resolved

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 {
zeitlinger marked this conversation as resolved.
Show resolved Hide resolved
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
Loading