fix and improve manifest resource provider (#10857)

Co-authored-by: Lauri Tulmin <ltulmin@splunk.com>
This commit is contained in:
Gregor Zeitlinger 2024-03-14 15:03:56 +01:00 committed by GitHub
parent 3a508d98f4
commit 17b6cad16f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 98 additions and 87 deletions

View File

@ -17,7 +17,9 @@ import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.Set;
import java.util.function.Function; import java.util.function.Function;
import java.util.stream.Collectors;
/** /**
* An easier alternative to {@link io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider}, which * An easier alternative to {@link io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider}, which
@ -25,7 +27,6 @@ import java.util.function.Function;
* *
* <p>An example of how to use this interface can be found in {@link ManifestResourceProvider}. * <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 { public abstract class AttributeResourceProvider<D> implements ConditionalResourceProvider {
private final AttributeProvider<D> attributeProvider; private final AttributeProvider<D> attributeProvider;
@ -36,29 +37,31 @@ public abstract class AttributeResourceProvider<D> implements ConditionalResourc
@CanIgnoreReturnValue @CanIgnoreReturnValue
@Override @Override
@SuppressWarnings({"unchecked", "rawtypes"})
public <T> AttributeBuilder add(AttributeKey<T> key, Function<D, Optional<T>> getter) { public <T> AttributeBuilder add(AttributeKey<T> key, Function<D, Optional<T>> getter) {
attributeGetters.put((AttributeKey) key, Objects.requireNonNull((Function) getter)); attributeGetters.put((AttributeKey) key, Objects.requireNonNull((Function) getter));
return this; return this;
} }
} }
private static final ThreadLocal<Resource> existingResource = new ThreadLocal<>(); private Set<AttributeKey<?>> filteredKeys;
private final Map<AttributeKey<Object>, Function<D, Optional<?>>> attributeGetters = private final Map<AttributeKey<Object>, Function<D, Optional<?>>> attributeGetters =
new HashMap<>(); new HashMap<>();
public AttributeResourceProvider(AttributeProvider<D> attributeProvider) { AttributeResourceProvider(AttributeProvider<D> attributeProvider) {
this.attributeProvider = attributeProvider; this.attributeProvider = attributeProvider;
attributeProvider.registerAttributes(new AttributeBuilder()); attributeProvider.registerAttributes(new AttributeBuilder());
} }
@Override @Override
public final boolean shouldApply(ConfigProperties config, Resource existing) { public final boolean shouldApply(ConfigProperties config, Resource existing) {
existingResource.set(existing); Map<String, String> resourceAttributes = config.getMap("otel.resource.attributes");
filteredKeys =
Map<String, String> resourceAttributes = getResourceAttributes(config); attributeGetters.keySet().stream()
return attributeGetters.keySet().stream() .filter(key -> shouldUpdate(config, existing, key, resourceAttributes))
.allMatch(key -> shouldUpdate(config, existing, key, resourceAttributes)); .collect(Collectors.toSet());
return !filteredKeys.isEmpty();
} }
@Override @Override
@ -67,17 +70,12 @@ public abstract class AttributeResourceProvider<D> implements ConditionalResourc
.readData() .readData()
.map( .map(
data -> { data -> {
// what should we do here? if (filteredKeys == null) {
// we don't have access to the existing resource throw new IllegalStateException("shouldApply should be called first");
// 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(); AttributesBuilder builder = Attributes.builder();
attributeGetters.entrySet().stream() attributeGetters.entrySet().stream()
.filter(e -> shouldUpdate(config, existing, e.getKey(), resourceAttributes)) .filter(e -> filteredKeys.contains(e.getKey()))
.forEach( .forEach(
e -> e ->
e.getValue() e.getValue()
@ -92,10 +90,6 @@ public abstract class AttributeResourceProvider<D> implements ConditionalResourc
builder.put(key, value); builder.put(key, value);
} }
private static Map<String, String> getResourceAttributes(ConfigProperties config) {
return config.getMap("otel.resource.attributes");
}
private static boolean shouldUpdate( private static boolean shouldUpdate(
ConfigProperties config, ConfigProperties config,
Resource existing, Resource existing,

View File

@ -16,6 +16,8 @@ import io.opentelemetry.sdk.resources.Resource;
import io.opentelemetry.semconv.ResourceAttributes; import io.opentelemetry.semconv.ResourceAttributes;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.Map; import java.util.Map;
import java.util.Optional;
import java.util.function.Supplier;
import java.util.logging.Logger; import java.util.logging.Logger;
/** /**
@ -26,22 +28,26 @@ public final class JarServiceNameDetector implements ConditionalResourceProvider
private static final Logger logger = Logger.getLogger(JarServiceNameDetector.class.getName()); private static final Logger logger = Logger.getLogger(JarServiceNameDetector.class.getName());
private final JarPathFinder jarPathFinder; private final Supplier<Optional<Path>> jarPathSupplier;
@SuppressWarnings("unused") // SPI @SuppressWarnings("unused") // SPI
public JarServiceNameDetector() { public JarServiceNameDetector() {
this(new JarPathFinder()); this(MainJarPathHolder::getJarPath);
}
private JarServiceNameDetector(Supplier<Optional<Path>> jarPathSupplier) {
this.jarPathSupplier = jarPathSupplier;
} }
// visible for tests // visible for tests
JarServiceNameDetector(JarPathFinder jarPathFinder) { JarServiceNameDetector(MainJarPathFinder jarPathFinder) {
this.jarPathFinder = jarPathFinder; this(() -> Optional.ofNullable(jarPathFinder.detectJarPath()));
} }
@Override @Override
public Resource createResource(ConfigProperties config) { public Resource createResource(ConfigProperties config) {
return jarPathFinder return jarPathSupplier
.getJarPath() .get()
.map( .map(
jarPath -> { jarPath -> {
String serviceName = getServiceName(jarPath); String serviceName = getServiceName(jarPath);

View File

@ -9,33 +9,22 @@ import java.nio.file.Files;
import java.nio.file.InvalidPathException; import java.nio.file.InvalidPathException;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.util.Optional;
import java.util.function.Function; import java.util.function.Function;
import java.util.function.Predicate; import java.util.function.Predicate;
import java.util.function.Supplier; import java.util.function.Supplier;
import javax.annotation.Nullable; import javax.annotation.Nullable;
class JarPathFinder { class MainJarPathFinder {
private final Supplier<String[]> getProcessHandleArguments; private final Supplier<String[]> getProcessHandleArguments;
private final Function<String, String> getSystemProperty; private final Function<String, String> getSystemProperty;
private final Predicate<Path> fileExists; private final Predicate<Path> fileExists;
private static class DetectionResult { public MainJarPathFinder() {
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); this(ProcessArguments::getProcessArguments, System::getProperty, Files::isRegularFile);
} }
// visible for tests // visible for tests
JarPathFinder( MainJarPathFinder(
Supplier<String[]> getProcessHandleArguments, Supplier<String[]> getProcessHandleArguments,
Function<String, String> getSystemProperty, Function<String, String> getSystemProperty,
Predicate<Path> fileExists) { Predicate<Path> fileExists) {
@ -44,19 +33,7 @@ class JarPathFinder {
this.fileExists = fileExists; this.fileExists = fileExists;
} }
// visible for testing Path detectJarPath() {
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(); Path jarPath = getJarPathFromProcessHandle();
if (jarPath != null) { if (jarPath != null) {
return jarPath; return jarPath;

View File

@ -0,0 +1,20 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.resources;
import java.nio.file.Path;
import java.util.Optional;
class MainJarPathHolder {
private static final Optional<Path> jarPath =
Optional.ofNullable(new MainJarPathFinder().detectJarPath());
static Optional<Path> getJarPath() {
return jarPath;
}
private MainJarPathHolder() {}
}

View File

@ -10,11 +10,12 @@ import static java.util.logging.Level.WARNING;
import com.google.auto.service.AutoService; import com.google.auto.service.AutoService;
import io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider; import io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider;
import io.opentelemetry.semconv.ResourceAttributes; import io.opentelemetry.semconv.ResourceAttributes;
import java.io.InputStream; import java.io.IOException;
import java.net.URL;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.Optional; import java.util.Optional;
import java.util.function.Function; import java.util.function.Function;
import java.util.function.Supplier;
import java.util.jar.JarFile;
import java.util.jar.Manifest; import java.util.jar.Manifest;
import java.util.logging.Logger; import java.util.logging.Logger;
@ -29,17 +30,16 @@ public final class ManifestResourceProvider extends AttributeResourceProvider<Ma
@SuppressWarnings("unused") // SPI @SuppressWarnings("unused") // SPI
public ManifestResourceProvider() { public ManifestResourceProvider() {
this(new JarPathFinder(), ManifestResourceProvider::readManifest); this(MainJarPathHolder::getJarPath, ManifestResourceProvider::readManifest);
} }
// Visible for testing private ManifestResourceProvider(
ManifestResourceProvider( Supplier<Optional<Path>> jarPathSupplier, Function<Path, Optional<Manifest>> manifestReader) {
JarPathFinder jarPathFinder, Function<Path, Optional<Manifest>> manifestReader) {
super( super(
new AttributeProvider<Manifest>() { new AttributeProvider<Manifest>() {
@Override @Override
public Optional<Manifest> readData() { public Optional<Manifest> readData() {
return jarPathFinder.getJarPath().flatMap(manifestReader); return jarPathSupplier.get().flatMap(manifestReader);
} }
@Override @Override
@ -63,14 +63,17 @@ public final class ManifestResourceProvider extends AttributeResourceProvider<Ma
}); });
} }
// Visible for testing
ManifestResourceProvider(
MainJarPathFinder jarPathFinder, Function<Path, Optional<Manifest>> manifestReader) {
this(() -> Optional.ofNullable(jarPathFinder.detectJarPath()), manifestReader);
}
private static Optional<Manifest> readManifest(Path jarPath) { private static Optional<Manifest> readManifest(Path jarPath) {
try (InputStream s = try (JarFile jarFile = new JarFile(jarPath.toFile(), false)) {
new URL(String.format("jar:%s!/META-INF/MANIFEST.MF", jarPath.toUri())).openStream()) { return Optional.of(jarFile.getManifest());
Manifest manifest = new Manifest(); } catch (IOException exception) {
manifest.read(s); logger.log(WARNING, "Error reading manifest", exception);
return Optional.of(manifest);
} catch (Exception e) {
logger.log(WARNING, "Error reading manifest", e);
return Optional.empty(); return Optional.empty();
} }
} }

View File

@ -16,7 +16,6 @@ import java.nio.file.Paths;
import java.util.function.Function; import java.util.function.Function;
import java.util.function.Predicate; import java.util.function.Predicate;
import java.util.stream.Stream; import java.util.stream.Stream;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.api.extension.ExtensionContext;
@ -33,11 +32,6 @@ class JarServiceNameDetectorTest {
@Mock ConfigProperties config; @Mock ConfigProperties config;
@BeforeEach
void setUp() {
JarPathFinder.resetForTest();
}
@Test @Test
void createResource_empty() { void createResource_empty() {
String[] processArgs = new String[0]; String[] processArgs = new String[0];
@ -53,7 +47,7 @@ class JarServiceNameDetectorTest {
private static JarServiceNameDetector getDetector( private static JarServiceNameDetector getDetector(
String[] processArgs, Function<String, String> getProperty, Predicate<Path> fileExists) { String[] processArgs, Function<String, String> getProperty, Predicate<Path> fileExists) {
return new JarServiceNameDetector( return new JarServiceNameDetector(
new JarPathFinder(() -> processArgs, getProperty, fileExists)); new MainJarPathFinder(() -> processArgs, getProperty, fileExists));
} }
@Test @Test

View File

@ -9,6 +9,7 @@ import static io.opentelemetry.semconv.ResourceAttributes.SERVICE_NAME;
import static io.opentelemetry.semconv.ResourceAttributes.SERVICE_VERSION; import static io.opentelemetry.semconv.ResourceAttributes.SERVICE_VERSION;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;
import io.opentelemetry.sdk.autoconfigure.spi.internal.DefaultConfigProperties; import io.opentelemetry.sdk.autoconfigure.spi.internal.DefaultConfigProperties;
import io.opentelemetry.sdk.resources.Resource; import io.opentelemetry.sdk.resources.Resource;
@ -19,28 +20,29 @@ import java.util.Optional;
import java.util.jar.Manifest; import java.util.jar.Manifest;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DynamicTest; import org.junit.jupiter.api.DynamicTest;
import org.junit.jupiter.api.TestFactory; import org.junit.jupiter.api.TestFactory;
class ManifestResourceProviderTest { class ManifestResourceProviderTest {
@BeforeEach
void setUp() {
JarPathFinder.resetForTest();
}
private static class TestCase { private static class TestCase {
private final String name; private final String name;
private final String expectedName; private final String expectedName;
private final String expectedVersion; private final String expectedVersion;
private final InputStream input; private final InputStream input;
private final Resource existing;
public TestCase(String name, String expectedName, String expectedVersion, InputStream input) { public TestCase(
String name,
String expectedName,
String expectedVersion,
InputStream input,
Resource existing) {
this.name = name; this.name = name;
this.expectedName = expectedName; this.expectedName = expectedName;
this.expectedVersion = expectedVersion; this.expectedVersion = expectedVersion;
this.input = input; this.input = input;
this.existing = existing;
} }
} }
@ -49,10 +51,25 @@ class ManifestResourceProviderTest {
ConfigProperties config = DefaultConfigProperties.createFromMap(Collections.emptyMap()); ConfigProperties config = DefaultConfigProperties.createFromMap(Collections.emptyMap());
return Stream.of( return Stream.of(
new TestCase("name ok", "demo", "0.0.1-SNAPSHOT", openClasspathResource("MANIFEST.MF")),
new TestCase("name - no resource", null, null, null),
new TestCase( new TestCase(
"name - empty resource", null, null, openClasspathResource("empty-MANIFEST.MF"))) "name ok",
"demo",
"0.0.1-SNAPSHOT",
openClasspathResource("MANIFEST.MF"),
Resource.getDefault()),
new TestCase("name - no resource", null, null, null, Resource.getDefault()),
new TestCase(
"name - empty resource",
null,
null,
openClasspathResource("empty-MANIFEST.MF"),
Resource.getDefault()),
new TestCase(
"name already detected",
null,
"0.0.1-SNAPSHOT",
openClasspathResource("MANIFEST.MF"),
Resource.create(Attributes.of(SERVICE_NAME, "old"))))
.map( .map(
t -> t ->
DynamicTest.dynamicTest( DynamicTest.dynamicTest(
@ -60,7 +77,7 @@ class ManifestResourceProviderTest {
() -> { () -> {
ManifestResourceProvider provider = ManifestResourceProvider provider =
new ManifestResourceProvider( new ManifestResourceProvider(
new JarPathFinder( new MainJarPathFinder(
() -> JarServiceNameDetectorTest.getArgs("app.jar"), () -> JarServiceNameDetectorTest.getArgs("app.jar"),
prop -> null, prop -> null,
JarServiceNameDetectorTest::failPath), JarServiceNameDetectorTest::failPath),
@ -73,7 +90,7 @@ class ManifestResourceProviderTest {
return Optional.empty(); return Optional.empty();
} }
}); });
provider.shouldApply(config, Resource.getDefault()); provider.shouldApply(config, t.existing);
Resource resource = provider.createResource(config); Resource resource = provider.createResource(config);
assertThat(resource.getAttribute(SERVICE_NAME)).isEqualTo(t.expectedName); assertThat(resource.getAttribute(SERVICE_NAME)).isEqualTo(t.expectedName);