Repository: pinpoint-apm/pinpoint
Issue: Add APIs for managing registered agents
Labels: enhancement

There is currently not a lot of ways to manage agent ids once they're mapped to an application. These data tend to linger around until the TTL has passed and ends up clogging the application index.
There needs to be APIs for doing 2 things:
1. Detect inactive agents (those that were registered, but are no longer in use) and provide a way to delete them (and their application name mapping)
2. Identify duplicate agent ids that are mapped to more than 1 application name.


Issue comments:
~~Reopening to add service type to request parameter.~~
Reopening to modify response for `/getInactiveAgents`.

Reopening to add API for removing all inactive agents from index.


Pull request review comments:

Patch that resolved the issue:
diff --git a/web/src/main/java/com/navercorp/pinpoint/web/controller/AdminController.java b/web/src/main/java/com/navercorp/pinpoint/web/controller/AdminController.java
index fedae0275299..5aae7f08b41a 100644
--- a/web/src/main/java/com/navercorp/pinpoint/web/controller/AdminController.java
+++ b/web/src/main/java/com/navercorp/pinpoint/web/controller/AdminController.java
@@ -26,6 +26,9 @@
 
 import com.navercorp.pinpoint.web.service.AdminService;
 
+import java.util.List;
+import java.util.Map;
+
 /**
  * @author netspider
  * @author HyunGil Jeong
@@ -67,4 +70,25 @@ public String removeAgentId(
         }
     }
 
+    @RequestMapping(value = "/agentIdMap")
+    @ResponseBody
+    public Map<String, List<String>> agentIdMap() {
+        return this.adminService.getAgentIdMap();
+    }
+
+    @RequestMapping(value = "/duplicateAgentIdMap")
+    @ResponseBody
+    public Map<String, List<String>> duplicateAgentIdMap() {
+        return this.adminService.getDuplicateAgentIdMap();
+    }
+
+    @RequestMapping(value = "/getInactiveAgents")
+    @ResponseBody
+    public Map<String, List<String>> getInactiveAgents(
+            @RequestParam(value = "applicationName", required = true) String applicationName,
+            @RequestParam(value = "durationDays", defaultValue = "30") int durationDays) {
+        logger.info("get inactive agents - applicationName: [{}], duration: {} days.", applicationName, durationDays);
+        return this.adminService.getInactiveAgents(applicationName, durationDays);
+    }
+
 }
\ No newline at end of file
diff --git a/web/src/main/java/com/navercorp/pinpoint/web/dao/AgentStatDao.java b/web/src/main/java/com/navercorp/pinpoint/web/dao/AgentStatDao.java
index 0a7bcd616284..00a989e87867 100644
--- a/web/src/main/java/com/navercorp/pinpoint/web/dao/AgentStatDao.java
+++ b/web/src/main/java/com/navercorp/pinpoint/web/dao/AgentStatDao.java
@@ -28,4 +28,6 @@ public interface AgentStatDao {
 
     List<AgentStat> scanAgentStatList(String agentId, Range range);
 
+    boolean agentStatExists(String agentId, Range range);
+
 }
diff --git a/web/src/main/java/com/navercorp/pinpoint/web/dao/hbase/HbaseAgentStatDao.java b/web/src/main/java/com/navercorp/pinpoint/web/dao/hbase/HbaseAgentStatDao.java
index 61d7a106e453..12eaaa777824 100644
--- a/web/src/main/java/com/navercorp/pinpoint/web/dao/hbase/HbaseAgentStatDao.java
+++ b/web/src/main/java/com/navercorp/pinpoint/web/dao/hbase/HbaseAgentStatDao.java
@@ -30,11 +30,14 @@
 import com.navercorp.pinpoint.web.vo.AgentStat;
 import com.navercorp.pinpoint.web.vo.Range;
 
+import org.apache.hadoop.hbase.client.Result;
+import org.apache.hadoop.hbase.client.ResultScanner;
 import org.apache.hadoop.hbase.client.Scan;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.data.hadoop.hbase.ResultsExtractor;
 import org.springframework.data.hadoop.hbase.RowMapper;
 import org.springframework.stereotype.Repository;
 
@@ -66,6 +69,7 @@ public void setScanCacheSize(int scanCacheSize) {
         this.scanCacheSize = scanCacheSize;
     }
 
+    @Override
     public List<AgentStat> scanAgentStatList(String agentId, Range range) {
         if (agentId == null) {
             throw new NullPointerException("agentId must not be null");
@@ -84,16 +88,61 @@ public List<AgentStat> scanAgentStatList(String agentId, Range range) {
 
         List<List<AgentStat>> intermediate = hbaseOperations2.find(HBaseTables.AGENT_STAT, scan, rowKeyDistributor, agentStatMapper);
 
-        int expectedSize = (int)(range.getRange() / 5000); // data for 5 seconds
+        int expectedSize = (int) (range.getRange() / 5000); // data for 5 seconds
         List<AgentStat> merged = new ArrayList<>(expectedSize);
 
-        for(List<AgentStat> each : intermediate) {
+        for (List<AgentStat> each : intermediate) {
             merged.addAll(each);
         }
 
         return merged;
     }
 
+    @Override
+    public boolean agentStatExists(String agentId, Range range) {
+        if (agentId == null) {
+            throw new NullPointerException("agentId must not be null");
+        }
+        if (range == null) {
+            throw new NullPointerException("range must not be null");
+        }
+
+        if (logger.isDebugEnabled()) {
+            logger.debug("checking for stat data existence : agentId={}, {}", agentId, range);
+        }
+
+        Scan scan = createScan(agentId, range);
+        scan.setCaching(1);
+        scan.addFamily(HBaseTables.AGENT_STAT_CF_STATISTICS);
+
+        return hbaseOperations2.find(HBaseTables.AGENT_STAT, scan, rowKeyDistributor, new AgentStatDataExistsResultsExtractor(this.agentStatMapper));
+    }
+
+    private class AgentStatDataExistsResultsExtractor implements ResultsExtractor<Boolean> {
+
+        private final RowMapper<List<AgentStat>> agentStatMapper;
+
+        private AgentStatDataExistsResultsExtractor(RowMapper<List<AgentStat>> agentStatMapper) {
+            this.agentStatMapper = agentStatMapper;
+        }
+
+        @Override
+        public Boolean extractData(ResultScanner results) throws Exception {
+            int matchCnt = 0;
+            for (Result result : results) {
+                if (!result.isEmpty()) {
+                    if (logger.isDebugEnabled()) {
+                        logger.debug("stat data exists, most recent data : {}", this.agentStatMapper.mapRow(result, matchCnt++));
+                    }
+                    return true;
+                } else {
+                    return false;
+                }
+            }
+            return false;
+        }
+    }
+
     /**
      * make a row key based on timestamp
      * FIXME there is the same duplicate code at collector's dao module
diff --git a/web/src/main/java/com/navercorp/pinpoint/web/service/AdminService.java b/web/src/main/java/com/navercorp/pinpoint/web/service/AdminService.java
index e22ed83964c7..2e7e51948ece 100644
--- a/web/src/main/java/com/navercorp/pinpoint/web/service/AdminService.java
+++ b/web/src/main/java/com/navercorp/pinpoint/web/service/AdminService.java
@@ -16,6 +16,9 @@
 
 package com.navercorp.pinpoint.web.service;
 
+import java.util.List;
+import java.util.Map;
+
 /**
  * @author netspider
  * @author HyunGil Jeong
@@ -25,5 +28,11 @@ public interface AdminService {
     void removeApplicationName(String applicationName);
 
     void removeAgentId(String applicationName, String agentId);
-    
+
+    Map<String, List<String>> getAgentIdMap();
+
+    Map<String, List<String>> getDuplicateAgentIdMap();
+
+    Map<String, List<String>> getInactiveAgents(String applicationName, int durationDays);
+
 }
diff --git a/web/src/main/java/com/navercorp/pinpoint/web/service/AdminServiceImpl.java b/web/src/main/java/com/navercorp/pinpoint/web/service/AdminServiceImpl.java
index 279ffb0dcff8..f55f5a3c2792 100644
--- a/web/src/main/java/com/navercorp/pinpoint/web/service/AdminServiceImpl.java
+++ b/web/src/main/java/com/navercorp/pinpoint/web/service/AdminServiceImpl.java
@@ -16,11 +16,23 @@
 
 package com.navercorp.pinpoint.web.service;
 
+import com.google.common.collect.Ordering;
+import com.navercorp.pinpoint.web.dao.AgentStatDao;
+import com.navercorp.pinpoint.web.vo.Application;
+import com.navercorp.pinpoint.web.vo.Range;
+import org.apache.commons.collections.CollectionUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
 import com.navercorp.pinpoint.web.dao.ApplicationIndexDao;
 
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+
 /**
  * @author netspider
  * @author HyunGil Jeong
@@ -31,6 +43,9 @@ public class AdminServiceImpl implements AdminService {
     @Autowired
     private ApplicationIndexDao applicationIndexDao;
 
+    @Autowired
+    private AgentStatDao agentStatDao;
+
     @Override
     public void removeApplicationName(String applicationName) {
         applicationIndexDao.deleteApplicationName(applicationName);
@@ -41,4 +56,69 @@ public void removeAgentId(String applicationName, String agentId) {
         applicationIndexDao.deleteAgentId(applicationName, agentId);
     }
 
+    @Override
+    public Map<String, List<String>> getAgentIdMap() {
+        Map<String, List<String>> agentIdMap = new TreeMap<>(Ordering.usingToString());
+        List<Application> applications = this.applicationIndexDao.selectAllApplicationNames();
+        for (Application application : applications) {
+            List<String> agentIds = this.applicationIndexDao.selectAgentIds(application.getName());
+            for (String agentId : agentIds) {
+                if (!agentIdMap.containsKey(agentId)) {
+                    agentIdMap.put(agentId, new ArrayList<String>());
+                }
+                agentIdMap.get(agentId).add(application.toString());
+            }
+        }
+        return agentIdMap;
+    }
+
+    @Override
+    public Map<String, List<String>> getDuplicateAgentIdMap() {
+        Map<String, List<String>> duplicateAgentIdMap = new TreeMap<>(Ordering.usingToString());
+        Map<String, List<String>> agentIdMap = this.getAgentIdMap();
+        for (Map.Entry<String, List<String>> entry : agentIdMap.entrySet()) {
+            String agentId = entry.getKey();
+            List<String> applications = entry.getValue();
+            if (applications.size() > 1) {
+                duplicateAgentIdMap.put(agentId, applications);
+            }
+        }
+        return duplicateAgentIdMap;
+    }
+
+    @Override
+    public Map<String, List<String>> getInactiveAgents(String applicationName, int durationDays) {
+        if (applicationName == null) {
+            throw new NullPointerException("applicationName must not be null");
+        }
+        if (durationDays < 30) {
+            throw new IllegalArgumentException("duration may not be less than 30 days");
+        }
+        if (durationDays > 180) {
+            throw new IllegalArgumentException("duration may not be greater than 180 days");
+        }
+        List<String> agentIds = this.applicationIndexDao.selectAgentIds(applicationName);
+        if (CollectionUtils.isEmpty(agentIds)) {
+            return Collections.emptyMap();
+        }
+
+        final long toTimestamp = System.currentTimeMillis();
+        Calendar cal = Calendar.getInstance();
+        cal.add(Calendar.DATE, durationDays * -1);
+        final long fromTimestamp = cal.getTimeInMillis();
+        Range queryRange = new Range(fromTimestamp, toTimestamp);
+
+        Map<String, List<String>> agentIdMap = this.getAgentIdMap();
+
+        Map<String, List<String>> inactiveAgentMap = new TreeMap<>(Ordering.usingToString());
+        for (String agentId : agentIds) {
+            boolean dataExists = this.agentStatDao.agentStatExists(agentId, queryRange);
+            if (!dataExists) {
+                List<String> applications = agentIdMap.get(agentId);
+                inactiveAgentMap.put(agentId, applications);
+            }
+        }
+        return inactiveAgentMap;
+    }
+
 }


Test patch:
diff --git a/web/src/test/java/com/navercorp/pinpoint/web/alarm/AlarmPartitionerTest.java b/web/src/test/java/com/navercorp/pinpoint/web/alarm/AlarmPartitionerTest.java
index 0375b105a0f4..ed0f1e1fffd8 100644
--- a/web/src/test/java/com/navercorp/pinpoint/web/alarm/AlarmPartitionerTest.java
+++ b/web/src/test/java/com/navercorp/pinpoint/web/alarm/AlarmPartitionerTest.java
@@ -26,7 +26,6 @@
 import org.springframework.batch.item.ExecutionContext;
 
 import com.navercorp.pinpoint.common.trace.ServiceType;
-import com.navercorp.pinpoint.web.alarm.AlarmPartitioner;
 import com.navercorp.pinpoint.web.dao.ApplicationIndexDao;
 import com.navercorp.pinpoint.web.vo.Application;
 
@@ -55,7 +54,7 @@ public List<Application> selectAllApplicationNames() {
                 
                 return apps;
             }
-            
+
             @Override
             public List<String> selectAgentIds(String applicationName) {
                 return null;
diff --git a/web/src/test/java/com/navercorp/pinpoint/web/alarm/checker/GcCountCheckerTest.java b/web/src/test/java/com/navercorp/pinpoint/web/alarm/checker/GcCountCheckerTest.java
index b3890aae2973..50a5ca025d25 100644
--- a/web/src/test/java/com/navercorp/pinpoint/web/alarm/checker/GcCountCheckerTest.java
+++ b/web/src/test/java/com/navercorp/pinpoint/web/alarm/checker/GcCountCheckerTest.java
@@ -53,6 +53,11 @@ public List<AgentStat> scanAgentStatList(String agentId, Range range) {
                 
                 return agentStatList;
             }
+
+            @Override
+            public boolean agentStatExists(String agentId, Range range) {
+                return true;
+            }
         };
         
         applicationIndexDao = new ApplicationIndexDao() {
diff --git a/web/src/test/java/com/navercorp/pinpoint/web/alarm/checker/HeapUsageRateCheckerTest.java b/web/src/test/java/com/navercorp/pinpoint/web/alarm/checker/HeapUsageRateCheckerTest.java
index 64c9a96a3290..a95897a3a3c5 100644
--- a/web/src/test/java/com/navercorp/pinpoint/web/alarm/checker/HeapUsageRateCheckerTest.java
+++ b/web/src/test/java/com/navercorp/pinpoint/web/alarm/checker/HeapUsageRateCheckerTest.java
@@ -29,8 +29,6 @@
 import com.navercorp.pinpoint.web.alarm.CheckerCategory;
 import com.navercorp.pinpoint.web.alarm.DataCollectorFactory;
 import com.navercorp.pinpoint.web.alarm.DataCollectorFactory.DataCollectorCategory;
-import com.navercorp.pinpoint.web.alarm.checker.AgentChecker;
-import com.navercorp.pinpoint.web.alarm.checker.HeapUsageRateChecker;
 import com.navercorp.pinpoint.web.alarm.collector.AgentStatDataCollector;
 import com.navercorp.pinpoint.web.alarm.vo.Rule;
 import com.navercorp.pinpoint.web.dao.AgentStatDao;
@@ -66,6 +64,11 @@ public List<AgentStat> scanAgentStatList(String agentId, Range range) {
                 
                 return agentStatList;
             }
+
+            @Override
+            public boolean agentStatExists(String agentId, Range range) {
+                return true;
+            }
         };
         
         applicationIndexDao = new ApplicationIndexDao() {
diff --git a/web/src/test/java/com/navercorp/pinpoint/web/alarm/checker/JvmCpuUsageRateCheckerTest.java b/web/src/test/java/com/navercorp/pinpoint/web/alarm/checker/JvmCpuUsageRateCheckerTest.java
index 318aeb1bc136..2819bd30edd9 100644
--- a/web/src/test/java/com/navercorp/pinpoint/web/alarm/checker/JvmCpuUsageRateCheckerTest.java
+++ b/web/src/test/java/com/navercorp/pinpoint/web/alarm/checker/JvmCpuUsageRateCheckerTest.java
@@ -28,8 +28,6 @@
 import com.navercorp.pinpoint.web.alarm.CheckerCategory;
 import com.navercorp.pinpoint.web.alarm.DataCollectorFactory;
 import com.navercorp.pinpoint.web.alarm.DataCollectorFactory.DataCollectorCategory;
-import com.navercorp.pinpoint.web.alarm.checker.AgentChecker;
-import com.navercorp.pinpoint.web.alarm.checker.JvmCpuUsageRateChecker;
 import com.navercorp.pinpoint.web.alarm.collector.AgentStatDataCollector;
 import com.navercorp.pinpoint.web.alarm.vo.Rule;
 import com.navercorp.pinpoint.web.dao.AgentStatDao;
@@ -63,6 +61,11 @@ public List<AgentStat> scanAgentStatList(String agentId, Range range) {
                 
                 return agentStatList;
             }
+
+            @Override
+            public boolean agentStatExists(String agentId, Range range) {
+                return true;
+            }
         };
         
         applicationIndexDao = new ApplicationIndexDao() {