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

Add Spring Boot service version finder / ResourceProvider #9480

Merged
merged 6 commits into from
Sep 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,8 @@
import io.opentelemetry.semconv.ResourceAttributes;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Properties;
import java.util.function.Function;
import java.util.function.Supplier;
Expand Down Expand Up @@ -286,54 +282,4 @@ private String loadFromClasspath(String filename, Function<InputStream, String>
return null;
}
}

// Exists for testing
static class SystemHelper {
private final ClassLoader classLoader;
private final boolean addBootInfPrefix;

SystemHelper() {
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
classLoader =
contextClassLoader != null ? contextClassLoader : ClassLoader.getSystemClassLoader();
addBootInfPrefix = classLoader.getResource("BOOT-INF/classes/") != null;
if (addBootInfPrefix) {
logger.log(FINER, "Detected presence of BOOT-INF/classes/");
}
}

String getenv(String name) {
return System.getenv(name);
}

String getProperty(String key) {
return System.getProperty(key);
}

InputStream openClasspathResource(String filename) {
String path = addBootInfPrefix ? "BOOT-INF/classes/" + filename : filename;
return classLoader.getResourceAsStream(path);
}

InputStream openFile(String filename) throws Exception {
return Files.newInputStream(Paths.get(filename));
}

/**
* Attempts to use ProcessHandle to get the full commandline of the current process (including
* the main method arguments). Will only succeed on java 9+.
*/
@SuppressWarnings("unchecked")
String[] attemptGetCommandLineArgsViaReflection() throws Exception {
Class<?> clazz = Class.forName("java.lang.ProcessHandle");
Method currentMethod = clazz.getDeclaredMethod("current");
Method infoMethod = clazz.getDeclaredMethod("info");
Object currentInstance = currentMethod.invoke(null);
Object info = infoMethod.invoke(currentInstance);
Class<?> infoClass = Class.forName("java.lang.ProcessHandle$Info");
Method argumentsMethod = infoClass.getMethod("arguments");
Optional<String[]> optionalArgs = (Optional<String[]>) argumentsMethod.invoke(info);
return optionalArgs.orElse(new String[0]);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.instrumentation.spring.resources;

import static java.util.logging.Level.FINE;

import com.google.auto.service.AutoService;
import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;
import io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider;
import io.opentelemetry.sdk.resources.Resource;
import io.opentelemetry.semconv.ResourceAttributes;
import java.io.IOException;
import java.io.InputStream;
import java.util.Optional;
import java.util.Properties;
import java.util.logging.Logger;

@AutoService(ResourceProvider.class)
public class SpringBootServiceVersionDetector implements ResourceProvider {

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

private final SystemHelper system;

public SpringBootServiceVersionDetector() {
this.system = new SystemHelper();
}

// Exists for testing
SpringBootServiceVersionDetector(SystemHelper system) {
this.system = system;
}

@Override
public Resource createResource(ConfigProperties config) {
return getServiceVersionFromBuildInfo()
.map(
version -> {
logger.log(FINE, "Auto-detected Spring Boot service version: {0}", version);
return Resource.builder().put(ResourceAttributes.SERVICE_VERSION, version).build();
})
.orElseGet(Resource::empty);
}

private Optional<String> getServiceVersionFromBuildInfo() {
try (InputStream in = system.openClasspathResource("META-INF", "build-info.properties")) {
return in != null ? getServiceVersionPropertyFromStream(in) : Optional.empty();
} catch (Exception e) {
return Optional.empty();
}
}

private static Optional<String> getServiceVersionPropertyFromStream(InputStream in) {
Properties properties = new Properties();
try {
// Note: load() uses ISO 8859-1 encoding, same as spring uses by default for property files
properties.load(in);
laurit marked this conversation as resolved.
Show resolved Hide resolved
return Optional.ofNullable(properties.getProperty("build.version"));
} catch (IOException e) {
return Optional.empty();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.instrumentation.spring.resources;

import java.io.InputStream;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Optional;
import java.util.logging.Level;
import java.util.logging.Logger;

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

private final ClassLoader classLoader;
private final boolean addBootInfPrefix;

SystemHelper() {
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
classLoader =
contextClassLoader != null ? contextClassLoader : ClassLoader.getSystemClassLoader();
addBootInfPrefix = classLoader.getResource("BOOT-INF/classes/") != null;
if (addBootInfPrefix) {
logger.log(Level.FINER, "Detected presence of BOOT-INF/classes/");
}
}

String getenv(String name) {
return System.getenv(name);
}

String getProperty(String key) {
return System.getProperty(key);
}

InputStream openClasspathResource(String filename) {
String path = addBootInfPrefix ? "BOOT-INF/classes/" + filename : filename;
return classLoader.getResourceAsStream(path);
}

InputStream openClasspathResource(String directory, String filename) {
String path = directory + "/" + filename;
return classLoader.getResourceAsStream(path);
}

InputStream openFile(String filename) throws Exception {
return Files.newInputStream(Paths.get(filename));
}

/**
* Attempts to use ProcessHandle to get the full commandline of the current process (including the
* main method arguments). Will only succeed on java 9+.
*/
@SuppressWarnings("unchecked")
String[] attemptGetCommandLineArgsViaReflection() throws Exception {
Class<?> clazz = Class.forName("java.lang.ProcessHandle");
Method currentMethod = clazz.getDeclaredMethod("current");
Method infoMethod = clazz.getDeclaredMethod("info");
Object currentInstance = currentMethod.invoke(null);
Object info = infoMethod.invoke(currentInstance);
Class<?> infoClass = Class.forName("java.lang.ProcessHandle$Info");
Method argumentsMethod = infoClass.getMethod("arguments");
Optional<String[]> optionalArgs = (Optional<String[]>) argumentsMethod.invoke(info);
return optionalArgs.orElse(new String[0]);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ class SpringBootServiceNameDetectorTest {
static final String PROPS = "application.properties";
static final String APPLICATION_YML = "application.yml";
@Mock ConfigProperties config;
@Mock SpringBootServiceNameDetector.SystemHelper system;
@Mock SystemHelper system;

@Test
void findByEnvVar() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.instrumentation.spring.resources;

import static io.opentelemetry.semconv.ResourceAttributes.SERVICE_VERSION;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.when;

import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;
import io.opentelemetry.sdk.resources.Resource;
import java.io.InputStream;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

@ExtendWith(MockitoExtension.class)
class SpringBootServiceVersionDetectorTest {

static final String BUILD_PROPS = "build-info.properties";
static final String META_INFO = "META-INF";

@Mock ConfigProperties config;
@Mock SystemHelper system;

@Test
void givenBuildVersionIsPresentInBuildInfProperties_thenReturnBuildVersion() {
when(system.openClasspathResource(META_INFO, BUILD_PROPS))
.thenReturn(openClasspathResource(META_INFO + "/" + BUILD_PROPS));

SpringBootServiceVersionDetector guesser = new SpringBootServiceVersionDetector(system);
Resource result = guesser.createResource(config);
assertThat(result.getAttribute(SERVICE_VERSION)).isEqualTo("0.0.2");
}

@Test
void givenBuildVersionFileNotPresent_thenReturnEmptyResource() {
when(system.openClasspathResource(META_INFO, BUILD_PROPS)).thenReturn(null);

SpringBootServiceVersionDetector guesser = new SpringBootServiceVersionDetector(system);
Resource result = guesser.createResource(config);
assertThat(result).isEqualTo(Resource.empty());
}

@Test
void givenBuildVersionFileIsPresentButBuildVersionPropertyNotPresent_thenReturnEmptyResource() {
when(system.openClasspathResource(META_INFO, BUILD_PROPS))
.thenReturn(openClasspathResource(BUILD_PROPS));

SpringBootServiceVersionDetector guesser = new SpringBootServiceVersionDetector(system);
Resource result = guesser.createResource(config);
assertThat(result).isEqualTo(Resource.empty());
}

private InputStream openClasspathResource(String resource) {
return getClass().getClassLoader().getResourceAsStream(resource);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
build.artifact=something
build.name=some-name
build.version=0.0.2
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
build.artifact=something
build.name=some-name
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import static java.util.stream.Collectors.toSet
class SpringBootSmokeTest extends SmokeTest {

protected String getTargetImage(String jdk) {
"ghcr.io/open-telemetry/opentelemetry-java-instrumentation/smoke-test-spring-boot:jdk$jdk-20230321.4484174638"
"ghcr.io/open-telemetry/opentelemetry-java-instrumentation/smoke-test-spring-boot:jdk$jdk-20230920.6251727205"
}

@Override
Expand Down Expand Up @@ -94,6 +94,13 @@ class SpringBootSmokeTest extends SmokeTest {
serviceName.isPresent()
serviceName.get() == "otel-spring-test-app"

then: "service version is autodetected"
def serviceVersion = findResourceAttribute(traces, "service.version")
.map { it.stringValue }
.findAny()
serviceVersion.isPresent()
serviceVersion.get() == "1.31.0-alpha-SNAPSHOT"

cleanup:
stopTarget()

Expand Down
Loading