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 name guesser / ResourceProvider #6516

Merged
merged 11 commits into from
Sep 7, 2022
Merged
Show file tree
Hide file tree
Changes from 9 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
@@ -0,0 +1,15 @@
plugins {
id("otel.library-instrumentation")
mateuszrzeszutek marked this conversation as resolved.
Show resolved Hide resolved
}

dependencies {
compileOnly("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure-spi")

annotationProcessor("com.google.auto.service:auto-service")
compileOnly("com.google.auto.service:auto-service-annotations")
testCompileOnly("com.google.auto.service:auto-service-annotations")

implementation("org.yaml:snakeyaml:1.31")

testImplementation("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure-spi")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,278 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.instrumentation.spring.resources;

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.resource.attributes.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;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import javax.annotation.Nullable;
import org.yaml.snakeyaml.Yaml;

/**
* A ResourceProvider that will attempt to guess the application name for a Spring Boot service.
* When successful, it will return a Resource that has the service name attribute populated with the
* name of the Spring Boot application. It uses the following strategies, and the first successful
* strategy wins:
*
* <ul>
* <li>Check for the SPRING_APPLICATION_NAME environment variable
* <li>Check for spring.application.name system property
* <li>Check for application.properties file on the classpath
* <li>Check for application.properties in the current working dir
* <li>Check for application.yml on the classpath
* <li>Check for application.yml in the current working dir
* <li>Check for --spring.application.name program argument (not jvm arg) via ProcessHandle
* <li>Check for --spring.application.name program argument via sun.java.command system property
* </ul>
*/
@AutoService(ResourceProvider.class)
public class SpringBootServiceNameGuesser implements ResourceProvider {

private static final Logger logger =
Logger.getLogger(SpringBootServiceNameGuesser.class.getName());
public static final String COMMANDLINE_ARG_PREFIX = "--spring.application.name=";
mateuszrzeszutek marked this conversation as resolved.
Show resolved Hide resolved
private static final Pattern COMMANDLINE_PATTERN =
Pattern.compile(".*--spring\\.application\\.name=([a-zA-Z.\\-_]+).*");
mateuszrzeszutek marked this conversation as resolved.
Show resolved Hide resolved
private final SystemHelper system;

public SpringBootServiceNameGuesser() {
this(new SystemHelper());
}

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

@Override
public Resource createResource(ConfigProperties config) {

logger.log(Level.FINER, "Performing Spring Boot service name auto-detection...");
// Note: The order should be consistent with the order of Spring matching, but noting
// that we have "first one wins" while Spring has "last one wins".
// The docs for Spring are here:
// https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.external-config
Stream<Supplier<String>> finders =
Stream.of(
this::findByCommandlineArgument,
this::findBySystemProperties,
this::findByEnvironmentVariable,
this::findByCurrentDirectoryApplicationProperties,
this::findByCurrentDirectoryApplicationYaml,
this::findByClasspathApplicationProperties,
this::findByClasspathApplicationYaml);
return finders
.map(Supplier::get)
.filter(Objects::nonNull)
.findFirst()
.map(
serviceName -> {
logger.log(Level.FINER, "Guessed Spring Boot service name: " + serviceName);
mateuszrzeszutek marked this conversation as resolved.
Show resolved Hide resolved
return Resource.builder().put(ResourceAttributes.SERVICE_NAME, serviceName).build();
})
.orElseGet(Resource::empty);
}

@Nullable
private String findByEnvironmentVariable() {
String result = system.getenv("SPRING_APPLICATION_NAME");
logger.log(Level.FINER, "Checking for SPRING_APPLICATION_NAME in env: " + result);
return result;
}

@Nullable
private String findBySystemProperties() {
String result = system.getProperty("spring.application.name");
logger.log(Level.FINER, "Checking for spring.application.name system property: " + result);
return result;
}

@Nullable
private String findByClasspathApplicationProperties() {
String result = readNameFromAppProperties();
logger.log(
Level.FINER,
"Checking for spring.application.name in application.properties file: " + result);
return result;
}

@Nullable
private String findByCurrentDirectoryApplicationProperties() {
String result = null;
try (InputStream in = system.openFile("application.properties")) {
result = getAppNamePropertyFromStream(in);
} catch (Exception e) {
// expected to fail sometimes
}
logger.log(Level.FINER, "Checking application.properties in current dir: " + result);
return result;
}

@Nullable
private String findByClasspathApplicationYaml() {
String result =
loadFromClasspath("application.yml", SpringBootServiceNameGuesser::parseNameFromYaml);
logger.log(Level.FINER, "Checking application.yml in classpath: " + result);
return result;
}

@Nullable
private String findByCurrentDirectoryApplicationYaml() {
String result = null;
try (InputStream in = system.openFile("application.yml")) {
result = parseNameFromYaml(in);
} catch (Exception e) {
// expected to fail sometimes
}
logger.log(Level.FINER, "Checking application.yml in current dir: " + result);
return result;
}

@Nullable
@SuppressWarnings("unchecked")
private static String parseNameFromYaml(InputStream in) {
Yaml yaml = new Yaml();
try {
Map<String, Object> data = yaml.load(in);
Map<String, Map<String, Object>> spring =
(Map<String, Map<String, Object>>) data.get("spring");
if (spring != null) {
Map<String, Object> app = spring.get("application");
if (app != null) {
Object name = app.get("name");
return (String) name;
}
}
} catch (RuntimeException e) {
// expected to fail sometimes
}
return null;
}

@Nullable
private String findByCommandlineArgument() {
String result = attemptProcessHandleReflection();
if (result == null) {
String javaCommand = system.getProperty("sun.java.command");
result = parseNameFromCommandLine(javaCommand);
}
logger.log(Level.FINER, "Checking application commandline args: " + result);
return result;
}

@Nullable
private String attemptProcessHandleReflection() {
try {
String[] args = system.attemptGetCommandLineArgsViaReflection();
return parseNameFromProcessArgs(args);
} catch (Exception e) {
return null;
}
}

@Nullable
private static String parseNameFromCommandLine(@Nullable String commandLine) {
if (commandLine == null) {
return null;
}
Matcher matcher = COMMANDLINE_PATTERN.matcher(commandLine);
if (matcher.find()) { // Required before group()
return matcher.group(1);
}
return null;
}

@Nullable
private static String parseNameFromProcessArgs(String[] args) {
return Stream.of(args)
.filter(arg -> arg.startsWith(COMMANDLINE_ARG_PREFIX))
.map(arg -> arg.substring(COMMANDLINE_ARG_PREFIX.length()))
.findFirst()
.orElse(null);
}

@Nullable
private String readNameFromAppProperties() {
return loadFromClasspath(
"application.properties", SpringBootServiceNameGuesser::getAppNamePropertyFromStream);
}

@Nullable
private static String getAppNamePropertyFromStream(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);
return properties.getProperty("spring.application.name");
} catch (IOException e) {
return null;
}
}

@Nullable
private String loadFromClasspath(String filename, Function<InputStream, String> parser) {
try (InputStream in = system.openClasspathResource(filename)) {
return parser.apply(in);
} catch (Exception e) {
return null;
}
}

// Exists for testing
static class SystemHelper {

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

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

InputStream openClasspathResource(String filename) {
return ClassLoader.getSystemClassLoader().getResourceAsStream(filename);
}

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. Will only
* succeed on java 9+.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

@trask trask Sep 6, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nm, I see this is for getting program args, maybe rename method to attemptGetProgramArgsViaReflection?

EDIT: oh, I see this is "full" commandline, so ignore my rename suggestion, maybe just a little more explicit javadoc to unconfuse those like me?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done 👍

*/
@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]);
}
}
}
Loading