Repository: pinpoint-apm/pinpoint
Issue: Collect service information for Spring Boot applications
Labels: enhancement

Service information (loaded libraries) are not collected for Spring Boot applications except for cases where they are packaged as _war_ files and are loaded on a container (such as Tomcat).

A lot of Spring Boot applications are run as an executable jar (or war), so it would be useful to collect these information in these cases as well.


Issue comments:
+1, nice feature. as many projects prefer spring boot for integration.


Pull request review comments:

Patch that resolved the issue:
diff --git a/bootstrap-core/src/main/java/com/navercorp/pinpoint/bootstrap/context/ServerMetaDataHolder.java b/bootstrap-core/src/main/java/com/navercorp/pinpoint/bootstrap/context/ServerMetaDataHolder.java
index 9dd936122cbd..ced882dfec78 100644
--- a/bootstrap-core/src/main/java/com/navercorp/pinpoint/bootstrap/context/ServerMetaDataHolder.java
+++ b/bootstrap-core/src/main/java/com/navercorp/pinpoint/bootstrap/context/ServerMetaDataHolder.java
@@ -33,7 +33,7 @@ public interface ServerMetaDataHolder {
     
     void removeListener(ServerMetaDataListener listener);
     
-    void publishServerMetaData();
+    void notifyListeners();
     
     interface ServerMetaDataListener {
         
diff --git a/bootstrap-core/src/main/java/com/navercorp/pinpoint/bootstrap/resolver/condition/MainClassCondition.java b/bootstrap-core/src/main/java/com/navercorp/pinpoint/bootstrap/resolver/condition/MainClassCondition.java
index ac9c8b4a3410..71343f464083 100644
--- a/bootstrap-core/src/main/java/com/navercorp/pinpoint/bootstrap/resolver/condition/MainClassCondition.java
+++ b/bootstrap-core/src/main/java/com/navercorp/pinpoint/bootstrap/resolver/condition/MainClassCondition.java
@@ -90,8 +90,9 @@ private String getMainClassName(SimpleProperty property) {
             logger.warn("Error retrieving main class from [{}]", property.getClass().getName());
             return NOT_FOUND;
         } else {
+            JarFile executableArchive = null;
             try {
-                JarFile executableArchive = new JarFile(javaCommand);
+                executableArchive = new JarFile(javaCommand);
                 return extractMainClassFromArchive(executableArchive);
             } catch (IOException e) {
                 // If it's not a valid java archive, VM shouldn't start in the first place.
@@ -101,6 +102,14 @@ private String getMainClassName(SimpleProperty property) {
                 // fail-safe, application shouldn't not start because of this
                 logger.warn("Error retrieving main class from java command : [{}]", javaCommand, e);
                 return NOT_FOUND;
+            } finally {
+                if (executableArchive != null) {
+                    try {
+                        executableArchive.close();
+                    } catch (IOException e) {
+                        logger.warn("Error closing jarFile : [{}]", executableArchive.getName(), e);
+                    }
+                }
             }
         }
     }
diff --git a/plugins/spring-boot/src/main/java/com/navercorp/pinpoint/plugin/spring/boot/SpringBootConstants.java b/plugins/spring-boot/src/main/java/com/navercorp/pinpoint/plugin/spring/boot/SpringBootConstants.java
index 89da3cd0205c..83c66de9ee4c 100644
--- a/plugins/spring-boot/src/main/java/com/navercorp/pinpoint/plugin/spring/boot/SpringBootConstants.java
+++ b/plugins/spring-boot/src/main/java/com/navercorp/pinpoint/plugin/spring/boot/SpringBootConstants.java
@@ -32,4 +32,6 @@ private SpringBootConstants() {
     public static final ServiceType SERVICE_TYPE = ServiceTypeFactory.of(1210, NAME, RECORD_STATISTICS);
 
     public static final String BOOTSTRAP_MAIN_PREFIX = "org.springframework.boot.loader.";
+
+    public static final String ROOT_CONTEXT_KEY = "Spring Boot";
 }
diff --git a/plugins/spring-boot/src/main/java/com/navercorp/pinpoint/plugin/spring/boot/SpringBootPlugin.java b/plugins/spring-boot/src/main/java/com/navercorp/pinpoint/plugin/spring/boot/SpringBootPlugin.java
index 852a369aced6..d4c109edf520 100644
--- a/plugins/spring-boot/src/main/java/com/navercorp/pinpoint/plugin/spring/boot/SpringBootPlugin.java
+++ b/plugins/spring-boot/src/main/java/com/navercorp/pinpoint/plugin/spring/boot/SpringBootPlugin.java
@@ -16,17 +16,54 @@
 
 package com.navercorp.pinpoint.plugin.spring.boot;
 
+import com.navercorp.pinpoint.bootstrap.instrument.InstrumentClass;
+import com.navercorp.pinpoint.bootstrap.instrument.InstrumentException;
+import com.navercorp.pinpoint.bootstrap.instrument.InstrumentMethod;
+import com.navercorp.pinpoint.bootstrap.instrument.Instrumentor;
+import com.navercorp.pinpoint.bootstrap.instrument.transformer.TransformCallback;
+import com.navercorp.pinpoint.bootstrap.instrument.transformer.TransformTemplate;
+import com.navercorp.pinpoint.bootstrap.instrument.transformer.TransformTemplateAware;
 import com.navercorp.pinpoint.bootstrap.plugin.ProfilerPlugin;
 import com.navercorp.pinpoint.bootstrap.plugin.ProfilerPluginSetupContext;
 
+import java.security.ProtectionDomain;
+
 /**
  * @author HyunGil Jeong
  */
-public class SpringBootPlugin implements ProfilerPlugin {
+public class SpringBootPlugin implements ProfilerPlugin, TransformTemplateAware {
+
+    private TransformTemplate transformTemplate;
 
     @Override
     public void setup(ProfilerPluginSetupContext context) {
         context.addApplicationTypeDetector(new SpringBootDetector());
+
+        addLauncherEditor();
+    }
+
+    @Override
+    public void setTransformTemplate(TransformTemplate transformTemplate) {
+        this.transformTemplate = transformTemplate;
     }
 
+    private void addLauncherEditor() {
+        transformTemplate.transform("org.springframework.boot.loader.Launcher", new TransformCallback() {
+
+            @Override
+            public byte[] doInTransform(Instrumentor instrumentor, ClassLoader classLoader, String className,
+                                        Class<?> classBeingRedefined, ProtectionDomain protectionDomain,
+                                        byte[] classfileBuffer) throws InstrumentException {
+
+                InstrumentClass target = instrumentor.getInstrumentClass(classLoader, className, classfileBuffer);
+                InstrumentMethod method = target.getDeclaredMethod("launch", "java.lang.String[]", "java.lang.String", "java.lang.ClassLoader");
+                if (method != null) {
+                    method.addInterceptor("com.navercorp.pinpoint.plugin.spring.boot.interceptor.LauncherLaunchInterceptor");
+                }
+
+                return target.toBytecode();
+            }
+
+        });
+    }
 }
diff --git a/plugins/spring-boot/src/main/java/com/navercorp/pinpoint/plugin/spring/boot/interceptor/LauncherLaunchInterceptor.java b/plugins/spring-boot/src/main/java/com/navercorp/pinpoint/plugin/spring/boot/interceptor/LauncherLaunchInterceptor.java
new file mode 100644
index 000000000000..852d7637f379
--- /dev/null
+++ b/plugins/spring-boot/src/main/java/com/navercorp/pinpoint/plugin/spring/boot/interceptor/LauncherLaunchInterceptor.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright 2016 NAVER Corp.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.navercorp.pinpoint.plugin.spring.boot.interceptor;
+
+import com.navercorp.pinpoint.bootstrap.context.ServerMetaDataHolder;
+import com.navercorp.pinpoint.bootstrap.context.TraceContext;
+import com.navercorp.pinpoint.bootstrap.interceptor.AroundInterceptor;
+import com.navercorp.pinpoint.bootstrap.logging.PLogger;
+import com.navercorp.pinpoint.bootstrap.logging.PLoggerFactory;
+import com.navercorp.pinpoint.plugin.spring.boot.SpringBootConstants;
+
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.TreeSet;
+
+/**
+ * @author HyunGil Jeong
+ */
+public class LauncherLaunchInterceptor implements AroundInterceptor {
+
+    private final static String PATH_SEPARATOR = "/";
+    private final static String JAR_SEPARATOR = "!";
+
+    private final PLogger logger = PLoggerFactory.getLogger(this.getClass());
+    private final boolean isDebug = logger.isDebugEnabled();
+
+    private final TraceContext traceContext;
+
+    public LauncherLaunchInterceptor(TraceContext traceContext) {
+        this.traceContext = traceContext;
+    }
+
+    @Override
+    public void before(Object target, Object[] args) {
+        if (isDebug) {
+            logger.beforeInterceptor(target, args);
+        }
+        if (!validate(target, args)) {
+            return;
+        }
+        String serviceName = createServiceName(target);
+        URLClassLoader classLoader = (URLClassLoader) args[2];
+        URL[] urls = classLoader.getURLs();
+        List<String> loadedJarNames = new ArrayList<String>(extractLibJarNamesFromURLs(urls));
+        ServerMetaDataHolder holder = this.traceContext.getServerMetaDataHolder();
+        holder.addServiceInfo(serviceName, loadedJarNames);
+        holder.notifyListeners();
+    }
+
+    @Override
+    public void after(Object target, Object[] args, Object result, Throwable throwable) {
+        // Do nothing
+    }
+
+    private String createServiceName(Object target) {
+        StringBuilder sb = new StringBuilder(SpringBootConstants.ROOT_CONTEXT_KEY);
+        sb.append(" (").append(target.getClass().getSimpleName()).append(")");
+        return sb.toString();
+    }
+
+    private boolean validate(Object target, Object[] args) {
+        if (target == null || args == null) {
+            return false;
+        }
+        if (args.length < 3) {
+            return false;
+        }
+        if (!(args[2] instanceof URLClassLoader)) {
+            return false;
+        }
+        return true;
+    }
+
+    private Set<String> extractLibJarNamesFromURLs(URL[] urls) {
+        if (urls == null) {
+            return Collections.emptySet();
+        }
+        Set<String> libJarNames = new TreeSet<String>(String.CASE_INSENSITIVE_ORDER);
+        for (URL url : urls) {
+            try {
+                String libJarName = extractLibJarName(url);
+                if (libJarName.length() > 0) {
+                    libJarNames.add(libJarName);
+                }
+            } catch (Exception e) {
+                // safety net
+                logger.warn("Error extracting library name", e);
+            }
+        }
+        return libJarNames;
+    }
+
+    private String extractLibJarName(URL url) {
+        String protocol = url.getProtocol();
+        String path = url.getPath().replaceAll("\\\\", PATH_SEPARATOR);
+        if (protocol.equals("file")) {
+            return extractNameFromFile(path);
+        } else if (protocol.equals("jar")) {
+            return extractNameFromJar(path);
+        } else {
+            return "";
+        }
+    }
+
+    private String extractNameFromFile(String fileUri) {
+        int lastIndexOfSeparator = fileUri.lastIndexOf(PATH_SEPARATOR);
+        if (lastIndexOfSeparator < 0) {
+            return fileUri;
+        } else {
+            return fileUri.substring(lastIndexOfSeparator + 1);
+        }
+    }
+
+    private String extractNameFromJar(String jarUri) {
+        String uri = jarUri.substring(0, jarUri.lastIndexOf(JAR_SEPARATOR));
+        int rootJarEndIndex = uri.indexOf(JAR_SEPARATOR);
+        if (rootJarEndIndex < 0) {
+            return extractNameFromFile(uri);
+        }
+        String rootJarFileUri = extractNameFromFile(uri.substring(0, rootJarEndIndex));
+        StringBuilder sb = new StringBuilder(rootJarFileUri);
+        sb.append(uri.substring(rootJarEndIndex));
+        return sb.toString();
+    }
+
+}
diff --git a/plugins/tomcat/src/main/java/com/navercorp/pinpoint/plugin/tomcat/TomcatPlugin.java b/plugins/tomcat/src/main/java/com/navercorp/pinpoint/plugin/tomcat/TomcatPlugin.java
index 0917e1c0d732..81766e7fe549 100644
--- a/plugins/tomcat/src/main/java/com/navercorp/pinpoint/plugin/tomcat/TomcatPlugin.java
+++ b/plugins/tomcat/src/main/java/com/navercorp/pinpoint/plugin/tomcat/TomcatPlugin.java
@@ -177,16 +177,17 @@ private void addWebappLoaderEditor() {
             public byte[] doInTransform(Instrumentor instrumentor, ClassLoader classLoader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws InstrumentException {
                 InstrumentClass target = instrumentor.getInstrumentClass(classLoader, className, classfileBuffer);
 
-                // Tomcat 6 - org.apache.catalina.loader.WebappLoader.start()
-                InstrumentMethod startEditor = target.getDeclaredMethod("start");
-                if (startEditor != null) {
-                    startEditor.addInterceptor("com.navercorp.pinpoint.plugin.tomcat.interceptor.WebappLoaderStartInterceptor");
+                InstrumentMethod startMethod = null;
+                if (target.hasDeclaredMethod("start")) {
+                    // Tomcat 6 - org.apache.catalina.loader.WebappLoader.start()
+                    startMethod = target.getDeclaredMethod("start");
+                } else if (target.hasDeclaredMethod("startInternal")) {
+                    // Tomcat 7, 8 - org.apache.catalina.loader.WebappLoader.startInternal()
+                    startMethod = target.getDeclaredMethod("startInternal");
                 }
 
-                // Tomcat 7, 8 - org.apache.catalina.loader.WebappLoader.startInternal()
-                InstrumentMethod startInternalEditor = target.getDeclaredMethod("startInternal");
-                if (startInternalEditor != null) {
-                    startInternalEditor.addInterceptor("com.navercorp.pinpoint.plugin.tomcat.interceptor.WebappLoaderStartInterceptor");
+                if (startMethod != null) {
+                    startMethod.addInterceptor("com.navercorp.pinpoint.plugin.tomcat.interceptor.WebappLoaderStartInterceptor");
                 }
 
                 return target.toBytecode();
diff --git a/plugins/tomcat/src/main/java/com/navercorp/pinpoint/plugin/tomcat/interceptor/ConnectorInitializeInterceptor.java b/plugins/tomcat/src/main/java/com/navercorp/pinpoint/plugin/tomcat/interceptor/ConnectorInitializeInterceptor.java
index 9e961e25efd9..26e90855be09 100644
--- a/plugins/tomcat/src/main/java/com/navercorp/pinpoint/plugin/tomcat/interceptor/ConnectorInitializeInterceptor.java
+++ b/plugins/tomcat/src/main/java/com/navercorp/pinpoint/plugin/tomcat/interceptor/ConnectorInitializeInterceptor.java
@@ -16,6 +16,7 @@
 
 package com.navercorp.pinpoint.plugin.tomcat.interceptor;
 
+import com.navercorp.pinpoint.bootstrap.context.ServerMetaDataHolder;
 import org.apache.catalina.connector.Connector;
 
 import com.navercorp.pinpoint.bootstrap.context.TraceContext;
@@ -49,7 +50,9 @@ public void after(Object target, Object[] args, Object result, Throwable throwab
         }
         if (target instanceof Connector) {
             final Connector connector = (Connector) target;
-            this.traceContext.getServerMetaDataHolder().addConnector(connector.getProtocol(), connector.getPort());
+            ServerMetaDataHolder holder = this.traceContext.getServerMetaDataHolder();
+            holder.addConnector(connector.getProtocol(), connector.getPort());
+            holder.notifyListeners();
         }
     }
 }
diff --git a/plugins/tomcat/src/main/java/com/navercorp/pinpoint/plugin/tomcat/interceptor/StandardServiceStartInterceptor.java b/plugins/tomcat/src/main/java/com/navercorp/pinpoint/plugin/tomcat/interceptor/StandardServiceStartInterceptor.java
index ecc3bfbb6fc2..b0cb2dab25db 100644
--- a/plugins/tomcat/src/main/java/com/navercorp/pinpoint/plugin/tomcat/interceptor/StandardServiceStartInterceptor.java
+++ b/plugins/tomcat/src/main/java/com/navercorp/pinpoint/plugin/tomcat/interceptor/StandardServiceStartInterceptor.java
@@ -16,6 +16,7 @@
 
 package com.navercorp.pinpoint.plugin.tomcat.interceptor;
 
+import com.navercorp.pinpoint.bootstrap.context.ServerMetaDataHolder;
 import org.apache.catalina.util.ServerInfo;
 
 import com.navercorp.pinpoint.bootstrap.context.TraceContext;
@@ -50,8 +51,8 @@ public void after(Object target, Object[] args, Object result, Throwable throwab
         }
 
         String serverInfo = ServerInfo.getServerInfo();
-        this.traceContext.getServerMetaDataHolder().setServerName(serverInfo);
-        // Service started. Publish server meta data.
-        this.traceContext.getServerMetaDataHolder().publishServerMetaData();
+        ServerMetaDataHolder holder = this.traceContext.getServerMetaDataHolder();
+        holder.setServerName(serverInfo);
+        holder.notifyListeners();
     }
 }
diff --git a/plugins/tomcat/src/main/java/com/navercorp/pinpoint/plugin/tomcat/interceptor/WebappLoaderStartInterceptor.java b/plugins/tomcat/src/main/java/com/navercorp/pinpoint/plugin/tomcat/interceptor/WebappLoaderStartInterceptor.java
index a980fefba7e6..0b663c228d6b 100644
--- a/plugins/tomcat/src/main/java/com/navercorp/pinpoint/plugin/tomcat/interceptor/WebappLoaderStartInterceptor.java
+++ b/plugins/tomcat/src/main/java/com/navercorp/pinpoint/plugin/tomcat/interceptor/WebappLoaderStartInterceptor.java
@@ -18,12 +18,15 @@
 
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
+import java.net.URI;
+import java.net.URISyntaxException;
 import java.net.URL;
 import java.net.URLClassLoader;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 
+import com.navercorp.pinpoint.bootstrap.context.ServerMetaDataHolder;
 import org.apache.catalina.Container;
 import org.apache.catalina.Context;
 import org.apache.catalina.Engine;
@@ -139,19 +142,24 @@ private List<String> extractLibJarNamesFromURLs(URL[] urls) {
         }
         List<String> libJarNames = new ArrayList<String>(urls.length);
         for (URL url : urls) {
-            String libJarName = extractLibJarName(url);
-            if (libJarName.length() > 0) {
-                libJarNames.add(libJarName);
+            try {
+                URI uri =  url.toURI();
+                String libJarName = extractLibJarName(uri);
+                if (libJarName.length() > 0) {
+                    libJarNames.add(libJarName);
+                }
+            } catch (URISyntaxException e) {
+                // ignore invalid formats
+                logger.warn("Invalid library url found : [{}]", url, e);
+            } catch (Exception e) {
+                logger.warn("Error extracting library name", e);
             }
         }
         return libJarNames;
     }
     
-    private String extractLibJarName(URL url) {
-        if (url == null) {
-            return "";
-        }
-        String jarName = url.toString();
+    private String extractLibJarName(URI uri) {
+        String jarName = uri.toString();
         if (jarName == null) {
             return "";
         }
@@ -164,7 +172,9 @@ private String extractLibJarName(URL url) {
     }
     
     private void dispatchLibJars(String contextKey, List<String> libJars) {
-        this.traceContext.getServerMetaDataHolder().addServiceInfo(contextKey, libJars);
+        ServerMetaDataHolder holder = this.traceContext.getServerMetaDataHolder();
+        holder.addServiceInfo(contextKey, libJars);
+        holder.notifyListeners();
     }
 
 }
diff --git a/profiler/src/main/java/com/navercorp/pinpoint/profiler/context/DefaultServerMetaDataHolder.java b/profiler/src/main/java/com/navercorp/pinpoint/profiler/context/DefaultServerMetaDataHolder.java
index 0b9f34830219..40bf8971a8d0 100644
--- a/profiler/src/main/java/com/navercorp/pinpoint/profiler/context/DefaultServerMetaDataHolder.java
+++ b/profiler/src/main/java/com/navercorp/pinpoint/profiler/context/DefaultServerMetaDataHolder.java
@@ -73,7 +73,7 @@ public void removeListener(ServerMetaDataListener listener) {
     }
 
     @Override
-    public void publishServerMetaData() {
+    public void notifyListeners() {
         final ServerMetaData serverMetaData = createServerMetaData();
         for (ServerMetaDataListener listener : this.listeners) {
             listener.publishServerMetaData(serverMetaData);


Test patch:
diff --git a/profiler/src/test/java/com/navercorp/pinpoint/profiler/AgentInfoSenderTest.java b/profiler/src/test/java/com/navercorp/pinpoint/profiler/AgentInfoSenderTest.java
index 487f1b70eeb3..451b671b814c 100644
--- a/profiler/src/test/java/com/navercorp/pinpoint/profiler/AgentInfoSenderTest.java
+++ b/profiler/src/test/java/com/navercorp/pinpoint/profiler/AgentInfoSenderTest.java
@@ -359,13 +359,15 @@ public void serverMetaDataCouldBePublishedFromMultipleThreads() throws Interrupt
         metaDataContext.addListener(agentInfoSender);
         // When
         for (int i = 0; i < threadCount; ++i) {
+            final String serviceName = "/name" + i;
             executorService.submit(new Callable<Void>() {
                 @Override
                 public Void call() throws Exception {
                     initLatch.countDown();
                     try {
                         startLatch.await();
-                        metaDataContext.publishServerMetaData();
+                        metaDataContext.addServiceInfo(serviceName, Collections.<String>emptyList());
+                        metaDataContext.notifyListeners();
                     } catch (final Throwable t) {
                         exceptions.add(t);
                     } finally {
diff --git a/profiler/src/test/java/com/navercorp/pinpoint/profiler/context/DefaultServerMetaDataHolderTest.java b/profiler/src/test/java/com/navercorp/pinpoint/profiler/context/DefaultServerMetaDataHolderTest.java
index 2d51b4582380..bced0b1ee73b 100644
--- a/profiler/src/test/java/com/navercorp/pinpoint/profiler/context/DefaultServerMetaDataHolderTest.java
+++ b/profiler/src/test/java/com/navercorp/pinpoint/profiler/context/DefaultServerMetaDataHolderTest.java
@@ -35,7 +35,6 @@
 
 import com.navercorp.pinpoint.bootstrap.context.ServerMetaData;
 import com.navercorp.pinpoint.bootstrap.context.ServerMetaDataHolder;
-import com.navercorp.pinpoint.profiler.context.DefaultServerMetaDataHolder;
 import com.navercorp.pinpoint.test.TestableServerMetaDataListener;
 
 /**
@@ -83,7 +82,7 @@ public Void call() throws Exception {
                     try {
                         startLatch.await();
                         metaDataContext.addServiceInfo(serviceName, serviceLibs);
-                        metaDataContext.publishServerMetaData();
+                        metaDataContext.notifyListeners();
                     } catch (final Throwable t) {
                         exceptions.add(t);
                     } finally {
@@ -129,7 +128,7 @@ public Void call() throws Exception {
                         try {
                             startLatch.await();
                             metaDataContext.addServiceInfo(serviceName, serviceLibs);
-                            metaDataContext.publishServerMetaData();
+                            metaDataContext.notifyListeners();
                         } catch (Throwable t) {
                             exceptions.add(t);
                         } finally {