Browse Source

Manifest resource detector (#10621)

Co-authored-by: Trask Stalnaker <trask.stalnaker@gmail.com>
Gregor Zeitlinger 1 year ago
parent
commit
5df8a5a0a0

+ 28 - 0
instrumentation/resources/library/src/main/java/io/opentelemetry/instrumentation/resources/AttributeProvider.java

@@ -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);
+  }
+}

+ 116 - 0
instrumentation/resources/library/src/main/java/io/opentelemetry/instrumentation/resources/AttributeResourceProvider.java

@@ -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;
+  }
+}

+ 112 - 0
instrumentation/resources/library/src/main/java/io/opentelemetry/instrumentation/resources/JarPathFinder.java

@@ -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;
+  }
+}

+ 14 - 74
instrumentation/resources/library/src/main/java/io/opentelemetry/instrumentation/resources/JarServiceNameDetector.java

@@ -14,16 +14,9 @@ import io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider;
 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.
@@ -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
@@ -75,52 +61,6 @@ public final class JarServiceNameDetector implements ConditionalResourceProvider
         && "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(".");

+ 83 - 0
instrumentation/resources/library/src/main/java/io/opentelemetry/instrumentation/resources/ManifestResourceProvider.java

@@ -0,0 +1,83 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.instrumentation.resources;
+
+import static java.util.logging.Level.WARNING;
+
+import com.google.auto.service.AutoService;
+import io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider;
+import io.opentelemetry.semconv.ResourceAttributes;
+import java.io.InputStream;
+import java.net.URL;
+import java.nio.file.Path;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.jar.Manifest;
+import java.util.logging.Logger;
+
+/**
+ * A {@link ResourceProvider} that will attempt to detect the <code>service.name</code> and <code>
+ * service.version</code> from META-INF/MANIFEST.MF.
+ */
+@AutoService(ResourceProvider.class)
+public final class ManifestResourceProvider extends AttributeResourceProvider<Manifest> {
+
+  private static final Logger logger = Logger.getLogger(ManifestResourceProvider.class.getName());
+
+  @SuppressWarnings("unused") // SPI
+  public ManifestResourceProvider() {
+    this(new JarPathFinder(), ManifestResourceProvider::readManifest);
+  }
+
+  // Visible for testing
+  ManifestResourceProvider(
+      JarPathFinder jarPathFinder, Function<Path, Optional<Manifest>> manifestReader) {
+    super(
+        new AttributeProvider<Manifest>() {
+          @Override
+          public Optional<Manifest> readData() {
+            return jarPathFinder.getJarPath().flatMap(manifestReader);
+          }
+
+          @Override
+          public void registerAttributes(Builder<Manifest> builder) {
+            builder
+                .add(
+                    ResourceAttributes.SERVICE_NAME,
+                    manifest -> {
+                      String serviceName =
+                          manifest.getMainAttributes().getValue("Implementation-Title");
+                      return Optional.ofNullable(serviceName);
+                    })
+                .add(
+                    ResourceAttributes.SERVICE_VERSION,
+                    manifest -> {
+                      String serviceVersion =
+                          manifest.getMainAttributes().getValue("Implementation-Version");
+                      return Optional.ofNullable(serviceVersion);
+                    });
+          }
+        });
+  }
+
+  private static Optional<Manifest> readManifest(Path jarPath) {
+    try (InputStream s =
+        new URL(String.format("jar:%s!/META-INF/MANIFEST.MF", jarPath.toUri())).openStream()) {
+      Manifest manifest = new Manifest();
+      manifest.read(s);
+      return Optional.of(manifest);
+    } catch (Exception e) {
+      logger.log(WARNING, "Error reading manifest", e);
+      return Optional.empty();
+    }
+  }
+
+  @Override
+  public int order() {
+    // make it run later than ManifestResourceProvider and SpringBootServiceNameDetector
+    return 300;
+  }
+}

+ 28 - 13
instrumentation/resources/library/src/test/java/io/opentelemetry/instrumentation/resources/JarServiceNameDetectorTest.java

@@ -16,6 +16,7 @@ import java.nio.file.Paths;
 import java.util.function.Function;
 import java.util.function.Predicate;
 import java.util.stream.Stream;
+import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
 import org.junit.jupiter.api.extension.ExtensionContext;
@@ -27,26 +28,39 @@ import org.mockito.Mock;
 import org.mockito.junit.jupiter.MockitoExtension;
 
 @ExtendWith(MockitoExtension.class)
+// todo split JarFileDetectorTest and JarServiceNameDetectorTest
 class JarServiceNameDetectorTest {
 
   @Mock ConfigProperties config;
 
+  @BeforeEach
+  void setUp() {
+    JarPathFinder.resetForTest();
+  }
+
   @Test
   void createResource_empty() {
-    JarServiceNameDetector serviceNameProvider =
-        new JarServiceNameDetector(
-            () -> new String[0], prop -> null, JarServiceNameDetectorTest::failPath);
+    String[] processArgs = new String[0];
+    Function<String, String> getProperty = prop -> null;
+    Predicate<Path> fileExists = JarServiceNameDetectorTest::failPath;
+    JarServiceNameDetector serviceNameProvider = getDetector(processArgs, getProperty, fileExists);
 
     Resource resource = serviceNameProvider.createResource(config);
 
     assertThat(resource.getAttributes()).isEmpty();
   }
 
+  private static JarServiceNameDetector getDetector(
+      String[] processArgs, Function<String, String> getProperty, Predicate<Path> fileExists) {
+    return new JarServiceNameDetector(
+        new JarPathFinder(() -> processArgs, getProperty, fileExists));
+  }
+
   @Test
   void createResource_noJarFileInArgs() {
     String[] args = new String[] {"-Dtest=42", "-Xmx666m", "-jar"};
     JarServiceNameDetector serviceNameProvider =
-        new JarServiceNameDetector(() -> args, prop -> null, JarServiceNameDetectorTest::failPath);
+        getDetector(args, prop -> null, JarServiceNameDetectorTest::failPath);
 
     Resource resource = serviceNameProvider.createResource(config);
 
@@ -55,10 +69,8 @@ class JarServiceNameDetectorTest {
 
   @Test
   void createResource_processHandleJar() {
-    String path = Paths.get("path", "to", "app", "my-service.jar").toString();
-    String[] args = new String[] {"-Dtest=42", "-Xmx666m", "-jar", path, "abc", "def"};
     JarServiceNameDetector serviceNameProvider =
-        new JarServiceNameDetector(() -> args, prop -> null, JarServiceNameDetectorTest::failPath);
+        getDetector(getArgs("my-service.jar"), prop -> null, JarServiceNameDetectorTest::failPath);
 
     Resource resource = serviceNameProvider.createResource(config);
 
@@ -69,10 +81,8 @@ class JarServiceNameDetectorTest {
 
   @Test
   void createResource_processHandleJarWithoutExtension() {
-    String path = Paths.get("path", "to", "app", "my-service.jar").toString();
-    String[] args = new String[] {"-Dtest=42", "-Xmx666m", "-jar", path};
     JarServiceNameDetector serviceNameProvider =
-        new JarServiceNameDetector(() -> args, prop -> null, JarServiceNameDetectorTest::failPath);
+        getDetector(getArgs("my-service"), prop -> null, JarServiceNameDetectorTest::failPath);
 
     Resource resource = serviceNameProvider.createResource(config);
 
@@ -81,6 +91,11 @@ class JarServiceNameDetectorTest {
         .containsEntry(ResourceAttributes.SERVICE_NAME, "my-service");
   }
 
+  static String[] getArgs(String jarName) {
+    String path = Paths.get("path", "to", "app", jarName).toString();
+    return new String[] {"-Dtest=42", "-Xmx666m", "-jar", path, "abc", "def"};
+  }
+
   @ParameterizedTest
   @ArgumentsSource(SunCommandLineProvider.class)
   void createResource_sunCommandLine(String commandLine, Path jarPath) {
@@ -89,7 +104,7 @@ class JarServiceNameDetectorTest {
     Predicate<Path> fileExists = jarPath::equals;
 
     JarServiceNameDetector serviceNameProvider =
-        new JarServiceNameDetector(() -> new String[0], getProperty, fileExists);
+        getDetector(new String[0], getProperty, fileExists);
 
     Resource resource = serviceNameProvider.createResource(config);
 
@@ -107,7 +122,7 @@ class JarServiceNameDetectorTest {
     Predicate<Path> fileExists = path -> false;
 
     JarServiceNameDetector serviceNameProvider =
-        new JarServiceNameDetector(() -> new String[0], getProperty, fileExists);
+        getDetector(new String[0], getProperty, fileExists);
 
     Resource resource = serviceNameProvider.createResource(config);
 
@@ -128,7 +143,7 @@ class JarServiceNameDetectorTest {
     }
   }
 
-  private static boolean failPath(Path file) {
+  static boolean failPath(Path file) {
     throw new AssertionError("Unexpected call to Files.isRegularFile()");
   }
 }

+ 89 - 0
instrumentation/resources/library/src/test/java/io/opentelemetry/instrumentation/resources/ManifestResourceProviderTest.java

@@ -0,0 +1,89 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.instrumentation.resources;
+
+import static io.opentelemetry.semconv.ResourceAttributes.SERVICE_NAME;
+import static io.opentelemetry.semconv.ResourceAttributes.SERVICE_VERSION;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;
+import io.opentelemetry.sdk.autoconfigure.spi.internal.DefaultConfigProperties;
+import io.opentelemetry.sdk.resources.Resource;
+import java.io.InputStream;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Optional;
+import java.util.jar.Manifest;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DynamicTest;
+import org.junit.jupiter.api.TestFactory;
+
+class ManifestResourceProviderTest {
+
+  @BeforeEach
+  void setUp() {
+    JarPathFinder.resetForTest();
+  }
+
+  private static class TestCase {
+    private final String name;
+    private final String expectedName;
+    private final String expectedVersion;
+    private final InputStream input;
+
+    public TestCase(String name, String expectedName, String expectedVersion, InputStream input) {
+      this.name = name;
+      this.expectedName = expectedName;
+      this.expectedVersion = expectedVersion;
+      this.input = input;
+    }
+  }
+
+  @TestFactory
+  Collection<DynamicTest> createResource() {
+    ConfigProperties config = DefaultConfigProperties.createFromMap(Collections.emptyMap());
+
+    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(
+                "name - empty resource", null, null, openClasspathResource("empty-MANIFEST.MF")))
+        .map(
+            t ->
+                DynamicTest.dynamicTest(
+                    t.name,
+                    () -> {
+                      ManifestResourceProvider provider =
+                          new ManifestResourceProvider(
+                              new JarPathFinder(
+                                  () -> JarServiceNameDetectorTest.getArgs("app.jar"),
+                                  prop -> null,
+                                  JarServiceNameDetectorTest::failPath),
+                              p -> {
+                                try {
+                                  Manifest manifest = new Manifest();
+                                  manifest.read(t.input);
+                                  return Optional.of(manifest);
+                                } catch (Exception e) {
+                                  return Optional.empty();
+                                }
+                              });
+                      provider.shouldApply(config, Resource.getDefault());
+
+                      Resource resource = provider.createResource(config);
+                      assertThat(resource.getAttribute(SERVICE_NAME)).isEqualTo(t.expectedName);
+                      assertThat(resource.getAttribute(SERVICE_VERSION))
+                          .isEqualTo(t.expectedVersion);
+                    }))
+        .collect(Collectors.toList());
+  }
+
+  private static InputStream openClasspathResource(String resource) {
+    return ManifestResourceProviderTest.class.getClassLoader().getResourceAsStream(resource);
+  }
+}

+ 3 - 0
instrumentation/resources/library/src/test/resources/MANIFEST.MF

@@ -0,0 +1,3 @@
+Manifest-Version: 1.0
+Implementation-Title: demo
+Implementation-Version: 0.0.1-SNAPSHOT

+ 1 - 0
instrumentation/resources/library/src/test/resources/empty-MANIFEST.MF

@@ -0,0 +1 @@
+Manifest-Version: 1.0

+ 14 - 8
smoke-tests/src/test/groovy/io/opentelemetry/smoketest/QuarkusSmokeTest.groovy

@@ -5,7 +5,7 @@
 
 package io.opentelemetry.smoketest
 
-import io.opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest
+import io.opentelemetry.semconv.ResourceAttributes
 import spock.lang.IgnoreIf
 import spock.lang.Unroll
 
@@ -14,7 +14,6 @@ import java.util.jar.Attributes
 import java.util.jar.JarFile
 
 import static io.opentelemetry.smoketest.TestContainerManager.useWindowsContainers
-import static java.util.stream.Collectors.toSet
 
 @IgnoreIf({ useWindowsContainers() })
 class QuarkusSmokeTest extends SmokeTest {
@@ -28,6 +27,11 @@ class QuarkusSmokeTest extends SmokeTest {
     return new TargetWaitStrategy.Log(Duration.ofMinutes(1), ".*Listening on.*")
   }
 
+  @Override
+  protected boolean getSetServiceName() {
+    return false
+  }
+
   @Unroll
   def "quarkus smoke test on JDK #jdk"(int jdk) {
     setup:
@@ -37,14 +41,16 @@ class QuarkusSmokeTest extends SmokeTest {
 
     when:
     client().get("/hello").aggregate().join()
-    Collection<ExportTraceServiceRequest> traces = waitForTraces()
+    TraceInspector traces = new TraceInspector(waitForTraces())
+
+    then: "Expected span names"
+    traces.countSpansByName('GET /hello') == 1
 
-    then:
-    countSpansByName(traces, 'GET /hello') == 1
+    and: "telemetry.distro.version is set"
+    traces.countFilteredResourceAttributes("telemetry.distro.version", currentAgentVersion) == 1
 
-      [currentAgentVersion] as Set == findResourceAttribute(traces, "telemetry.distro.version")
-      .map { it.stringValue }
-      .collect(toSet())
+    and: "service.name is detected from manifest"
+    traces.countFilteredResourceAttributes(ResourceAttributes.SERVICE_NAME.key, "smoke-test-quarkus-images") == 1
 
     cleanup:
     stopTarget()