Fix WAS to WAS filtering when destination nodes have receiving spans missing
#### Description
When filtering on WAS to WAS links, the destination node may very well have receiving spans missing due to packet losses, or simply because agents were only installed partially.
The latter case would be for example, when there is node A invoking RPC calls to node B (which has multiple instances) on a single address, but node B does not have agents installed on all of it's instances.

When filtering on WAS to WAS links, the calling span's spanId and the receiving span's parentSpanId are checked. But in the cases above, there is no receiving span so we must check using only the calling span to decide if a particular transaction should be included or not.
`RpcHint`s were introduced to aid in such cases but was reliable.

This issue attempts to fix this.
commons/src/main/java/com/navercorp/pinpoint/common/trace/ServiceType.java web/src/main/java/com/navercorp/pinpoint/web/filter/DefaultFilterBuilder.java web/src/main/java/com/navercorp/pinpoint/web/filter/FilterHint.java web/src/main/java/com/navercorp/pinpoint/web/filter/LinkFilter.java web/src/main/java/com/navercorp/pinpoint/web/filter/RpcURLPatternFilter.java
commons/src/main/java/com/navercorp/pinpoint/common/trace/ServiceType.java web/src/main/java/com/navercorp/pinpoint/web/filter/DefaultFilterBuilder.java web/src/main/java/com/navercorp/pinpoint/web/filter/FilterHint.java web/src/main/java/com/navercorp/pinpoint/web/filter/LinkFilter.java web/src/main/java/com/navercorp/pinpoint/web/filter/RpcURLPatternFilter.java
web/src/test/java/com/navercorp/pinpoint/web/filter/LinkFilterTest.java web/src/test/java/com/navercorp/pinpoint/web/filter/RpcURLPatternFilterTest.java web/src/test/java/com/navercorp/pinpoint/web/util/ServiceTypeRegistryMockFactory.java
diff --git a/commons/src/main/java/com/navercorp/pinpoint/common/trace/ServiceType.java b/commons/src/main/java/com/navercorp/pinpoint/common/trace/ServiceType.java
index 5144585c76da..4a99bc785a84 100644
--- a/commons/src/main/java/com/navercorp/pinpoint/common/trace/ServiceType.java
+++ b/commons/src/main/java/com/navercorp/pinpoint/common/trace/ServiceType.java
@@ -121,6 +121,8 @@
  * <tr><td>8200</td><td>REDIS</td></tr>
  * <tr><td>8250</td><td><i>RESERVED</i></td></tr>
  * <tr><td>8251</td><td><i>RESERVED</i></td></tr>
+ * <tr><td>8310</td><td><i>ACTIVEMQ_CLIENT</i></td></tr>
+ * <tr><td>8311</td><td><i>ACTIVEMQ_CLIENT_INTERNAL</i></td></tr>
  * </table>
  * <h3>Cache Library Sandbox (8900 ~ 8999) Histogram type: Fast </h3>
  * 
diff --git a/web/src/main/java/com/navercorp/pinpoint/web/filter/DefaultFilterBuilder.java b/web/src/main/java/com/navercorp/pinpoint/web/filter/DefaultFilterBuilder.java
index 28c3d43dce1b..c6ceb65f4005 100644
--- a/web/src/main/java/com/navercorp/pinpoint/web/filter/DefaultFilterBuilder.java
+++ b/web/src/main/java/com/navercorp/pinpoint/web/filter/DefaultFilterBuilder.java
@@ -23,6 +23,7 @@
 import java.util.ArrayList;
 import java.util.List;
 
+import com.navercorp.pinpoint.common.service.AnnotationKeyRegistryService;
 import com.navercorp.pinpoint.common.service.ServiceTypeRegistryService;
 import org.apache.commons.lang3.StringUtils;
 
@@ -50,6 +51,9 @@ public class DefaultFilterBuilder implements FilterBuilder {
     @Autowired
     private ServiceTypeRegistryService serviceTypeRegistryService;
 
+    @Autowired
+    private AnnotationKeyRegistryService annotationKeyRegistryService;
+
     @Override
     public Filter build(String filterText) {
         if (StringUtils.isEmpty(filterText)) {
@@ -122,7 +126,7 @@ private List<LinkFilter> createLinkFilter(String jsonFilterText, List<FilterDesc
             }
 
             logger.debug("FilterDescriptor={}", descriptor);
-            final LinkFilter linkFilter = new LinkFilter(descriptor, hint, serviceTypeRegistryService);
+            final LinkFilter linkFilter = new LinkFilter(descriptor, hint, serviceTypeRegistryService, annotationKeyRegistryService);
             result.add(linkFilter);
         }
         return result;
diff --git a/web/src/main/java/com/navercorp/pinpoint/web/filter/FilterHint.java b/web/src/main/java/com/navercorp/pinpoint/web/filter/FilterHint.java
index 27afac63730c..b21497dea399 100644
--- a/web/src/main/java/com/navercorp/pinpoint/web/filter/FilterHint.java
+++ b/web/src/main/java/com/navercorp/pinpoint/web/filter/FilterHint.java
@@ -45,14 +45,14 @@ public FilterHint(List<RpcHint> rpcHintList) {
     }
 
 
-    public List<RpcHint> getRpcHintList(String sourceApplicationName) {
-        if (sourceApplicationName == null) {
-            throw new NullPointerException("sourceApplicationName must not be null");
+    public List<RpcHint> getRpcHintList(String targetApplicationName) {
+        if (targetApplicationName == null) {
+            throw new NullPointerException("targetApplicationName must not be null");
         }
         final List<RpcHint> findRpcHintList = new ArrayList<>();
         for (RpcHint rpcHint : rpcHintList) {
             // TODO miss serviceType
-            if (rpcHint.getApplicationName().equals(sourceApplicationName)) {
+            if (rpcHint.getApplicationName().equals(targetApplicationName)) {
                 findRpcHintList.add(rpcHint);
             }
         }
diff --git a/web/src/main/java/com/navercorp/pinpoint/web/filter/LinkFilter.java b/web/src/main/java/com/navercorp/pinpoint/web/filter/LinkFilter.java
index 44d045c3e7a1..fdd193de09ce 100644
--- a/web/src/main/java/com/navercorp/pinpoint/web/filter/LinkFilter.java
+++ b/web/src/main/java/com/navercorp/pinpoint/web/filter/LinkFilter.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2014 NAVER Corp.
+ * Copyright 2017 NAVER Corp.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -23,6 +23,7 @@
 
 import com.navercorp.pinpoint.common.server.bo.SpanBo;
 import com.navercorp.pinpoint.common.server.bo.SpanEventBo;
+import com.navercorp.pinpoint.common.service.AnnotationKeyRegistryService;
 import com.navercorp.pinpoint.common.service.ServiceTypeRegistryService;
 import com.navercorp.pinpoint.common.trace.ServiceType;
 import com.navercorp.pinpoint.web.filter.agent.*;
@@ -31,6 +32,7 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
 
 /**
  *
@@ -61,18 +63,28 @@ public class LinkFilter implements Filter {
 
     private final List<RpcHint> rpcHintList;
 
-    private final URLPatternFilter acceptURLFilter;
-
     private final ServiceTypeRegistryService serviceTypeRegistryService;
+    private final AnnotationKeyRegistryService annotationKeyRegistryService;
+
+    private final URLPatternFilter acceptURLFilter;
+    private final URLPatternFilter rpcUrlFilter;
 
-    public LinkFilter(FilterDescriptor filterDescriptor, FilterHint filterHint, ServiceTypeRegistryService serviceTypeRegistryService) {
+    public LinkFilter(FilterDescriptor filterDescriptor, FilterHint filterHint, ServiceTypeRegistryService serviceTypeRegistryService, AnnotationKeyRegistryService annotationKeyRegistryService) {
         if (filterDescriptor == null) {
             throw new NullPointerException("filter descriptor must not be null");
         }
         if (filterHint == null) {
             throw new NullPointerException("filterHint must not be null");
         }
+        if (serviceTypeRegistryService == null) {
+            throw new NullPointerException("serviceTypeRegistryService must not be null");
+        }
+        if (annotationKeyRegistryService == null) {
+            throw new NullPointerException("annotationKeyRegistryService must not be null");
+        }
+
         this.serviceTypeRegistryService = serviceTypeRegistryService;
+        this.annotationKeyRegistryService = annotationKeyRegistryService;
 
         final String fromServiceType = filterDescriptor.getFromServiceType();
         this.fromServiceDescList = serviceTypeRegistryService.findDesc(fromServiceType);
@@ -110,20 +122,29 @@ public LinkFilter(FilterDescriptor filterDescriptor, FilterHint filterHint, Serv
         this.filterType = getFilterType();
         logger.info("filterType:{}", filterType);
 
-        this.rpcHintList = this.filterHint.getRpcHintList(fromApplicationName);
+        this.rpcHintList = this.filterHint.getRpcHintList(toApplicationName);
+
         // TODO fix : fromSpan base rpccall filter
-        this.acceptURLFilter = createPatternFilter(filterDescriptor);
+        this.acceptURLFilter = createAcceptUrlFilter(filterDescriptor);
+        this.rpcUrlFilter = createRpcUrlFilter(filterDescriptor);
         logger.info("acceptURLFilter:{}", acceptURLFilter);
     }
 
-    private URLPatternFilter createPatternFilter(FilterDescriptor filterDescriptor) {
-        if (filterDescriptor.getUrlPattern() == null) {
+    private URLPatternFilter createAcceptUrlFilter(FilterDescriptor filterDescriptor) {
+        if (StringUtils.isEmpty(filterDescriptor.getUrlPattern())) {
             return new BypassURLPatternFilter();
         }
         // TODO remove decode
         return new AcceptUrlFilter(filterDescriptor.getUrlPattern());
     }
 
+    private URLPatternFilter createRpcUrlFilter(FilterDescriptor filterDescriptor) {
+        if (StringUtils.isEmpty(filterDescriptor.getUrlPattern())) {
+            return new BypassURLPatternFilter();
+        }
+        return new RpcURLPatternFilter(filterDescriptor.getUrlPattern(), serviceTypeRegistryService, annotationKeyRegistryService);
+    }
+
     private ResponseTimeFilter createResponseTimeFilter(FilterDescriptor filterDescriptor) {
         final ResponseTimeFilterFactory factory = new ResponseTimeFilterFactory(filterDescriptor.getFromResponseTime(), filterDescriptor.getResponseTo());
         return factory.createFilter();
@@ -161,7 +182,7 @@ private FilterType getFilterType() {
         if (includeWas(fromServiceDescList) && includeWas(toServiceDescList)) {
             return FilterType.WAS_TO_WAS;
         }
-        if (includeServiceType(fromServiceDescList, ServiceType.USER) && includeWas(toServiceDescList)) {
+        if (includeUser(fromServiceDescList) && includeWas(toServiceDescList)) {
             return FilterType.USER_TO_WAS;
         }
         if (includeWas(fromServiceDescList) && includeUnknown(toServiceDescList)) {
@@ -263,6 +284,9 @@ private boolean wasToUnknownFilter(List<SpanBo> transaction) {
          * WAS -> UNKNOWN
          */
         final List<SpanBo> fromNode = findFromNode(transaction);
+        if (!rpcUrlFilter.accept(fromNode)) {
+            return false;
+        }
         for (SpanBo span : fromNode) {
             final List<SpanEventBo> eventBoList = span.getSpanEventBoList();
             if (eventBoList == null) {
@@ -312,7 +336,7 @@ private boolean wasToBackendFilter(List<SpanBo> transaction) {
     private boolean wasToWasFilter(List<SpanBo> transaction) {
         /*
          * WAS -> WAS
-         * if destination is a "WAS", the span of src and dest may exists. need to check if be circular or not.
+         * if destination is a "WAS", the span of src and dest may exist. need to check if be circular or not.
          * find src first. span (from, to) may exist more than one. so (spanId == parentSpanID) should be checked.
          */
 
@@ -336,9 +360,11 @@ private boolean wasToWasFilter(List<SpanBo> transaction) {
             return false;
         }
 
-        if (!rpcHintList.isEmpty()) {
+        // Check for url pattern should now be done on the caller side (from spans) as to spans are missing at this point
+        if (!rpcUrlFilter.accept(fromSpanList)) {
             return false;
         }
+
         // if agent filter is FromAgentFilter or AcceptAgentFilter(agent filter is not selected), url filtering is available.
         return fromBaseFilter(fromSpanList);
     }
@@ -372,6 +398,10 @@ private boolean fromBaseFilter(List<SpanBo> fromSpanList) {
         // exceptional case
         // 1. remote call fail
         // 2. span packet lost.
+        if (rpcHintList.isEmpty()) {
+            // fast skip. There is nothing more we can do if rpcHintList is empty.
+            return false;
+        }
         for (SpanBo fromSpan : fromSpanList) {
             final List<SpanEventBo> eventBoList = fromSpan.getSpanEventBoList();
             if (eventBoList == null) {
@@ -379,13 +409,16 @@ private boolean fromBaseFilter(List<SpanBo> fromSpanList) {
             }
             for (SpanEventBo event : eventBoList) {
                 final ServiceType eventServiceType = serviceTypeRegistryService.findServiceType(event.getServiceType());
-                if (!eventServiceType.isRpcClient() || !eventServiceType.isQueue()) {
+                if (!eventServiceType.isRpcClient() && !eventServiceType.isQueue()) {
                     continue;
                 }
                 if (!eventServiceType.isRecordStatistics()) {
                     continue;
                 }
                 // check rpc call fail
+                // There are also cases where multiple applications receiving the same request from the caller node
+                // but not all of them have agents installed. RpcHint is used for such cases as acceptUrlFilter will
+                // reject these transactions.
                 for (RpcHint rpcHint : rpcHintList) {
                     for (RpcType rpcType : rpcHint.getRpcTypeList()) {
                         if (rpcType.isMatched(event.getDestinationId(), eventServiceType.getCode())) {
@@ -445,16 +478,16 @@ private List<SpanBo> findToNode(List<SpanBo> transaction) {
     private List<SpanBo> findNode(List<SpanBo> nodeList, String findApplicationName, List<ServiceType> findServiceCode, AgentFilter agentFilter) {
         List<SpanBo> findList = null;
         for (SpanBo span : nodeList) {
-                final ServiceType applicationServiceType = serviceTypeRegistryService.findServiceType(span.getApplicationServiceType());
-                if (findApplicationName.equals(span.getApplicationId()) && includeServiceType(findServiceCode, applicationServiceType)) {
-                    // apply preAgentFilter
-                    if (agentFilter.accept(span.getAgentId())) {
-                        if (findList == null) {
-                            findList = new ArrayList<>();
-                        }
-                        findList.add(span);
+            final ServiceType applicationServiceType = serviceTypeRegistryService.findServiceType(span.getApplicationServiceType());
+            if (findApplicationName.equals(span.getApplicationId()) && includeServiceType(findServiceCode, applicationServiceType)) {
+                // apply preAgentFilter
+                if (agentFilter.accept(span.getAgentId())) {
+                    if (findList == null) {
+                        findList = new ArrayList<>();
                     }
+                    findList.add(span);
                 }
+            }
         }
         if (findList == null) {
             return Collections.emptyList();
@@ -468,6 +501,15 @@ private boolean isToNode(String applicationId, ServiceType serviceType) {
         return this.toApplicationName.equals(applicationId) && includeServiceType(this.toServiceDescList, serviceType);
     }
 
+    private boolean includeUser(List<ServiceType> serviceTypeList) {
+        for (ServiceType serviceType : serviceTypeList) {
+            if (serviceType.isUser()) {
+                return true;
+            }
+        }
+        return false;
+    }
+
     private boolean includeUnknown(List<ServiceType> serviceTypeList) {
         for (ServiceType serviceType : serviceTypeList) {
             if (serviceType.isUnknown()) {
diff --git a/web/src/main/java/com/navercorp/pinpoint/web/filter/RpcURLPatternFilter.java b/web/src/main/java/com/navercorp/pinpoint/web/filter/RpcURLPatternFilter.java
index 5e06570a2d25..88422925bc03 100644
--- a/web/src/main/java/com/navercorp/pinpoint/web/filter/RpcURLPatternFilter.java
+++ b/web/src/main/java/com/navercorp/pinpoint/web/filter/RpcURLPatternFilter.java
@@ -16,6 +16,7 @@
 
 package com.navercorp.pinpoint.web.filter;
 
+import com.google.common.collect.ImmutableSet;
 import com.navercorp.pinpoint.common.server.bo.AnnotationBo;
 import com.navercorp.pinpoint.common.server.bo.SpanBo;
 import com.navercorp.pinpoint.common.server.bo.SpanEventBo;
@@ -28,7 +29,10 @@
 
 import java.nio.charset.Charset;
 import java.nio.charset.StandardCharsets;
+import java.util.HashSet;
 import java.util.List;
+import java.util.NoSuchElementException;
+import java.util.Set;
 
 /**
  * @author emeroad
@@ -42,21 +46,46 @@ public class RpcURLPatternFilter implements URLPatternFilter {
     private final ServiceTypeRegistryService serviceTypeRegistryService;
     private final AnnotationKeyRegistryService annotationKeyRegistryService;
 
-    // TODO remove. hard coded annotation for compatibility
-    @Deprecated
-    private final AnnotationKey npcUrl;
+    // TODO remove. hard coded annotation for compatibility, need a better to group rpc url annotations
+    private final Set<Integer> rpcEndpointAnnotationCodes;
 
     public RpcURLPatternFilter(String urlPattern, ServiceTypeRegistryService serviceTypeRegistryService, AnnotationKeyRegistryService annotationKeyRegistryService) {
         if (urlPattern == null) {
             throw new NullPointerException("urlPattern must not be null");
         }
+        if (serviceTypeRegistryService == null) {
+            throw new NullPointerException("serviceTypeRegistryService must not be null");
+        }
+        if (annotationKeyRegistryService == null) {
+            throw new NullPointerException("annotationKeyRegistryService must not be null");
+        }
         // TODO remove decode
         this.urlPattern = new String(Base64.decodeBase64(urlPattern), UTF8);
         // TODO serviceType rpctype
 
         this.serviceTypeRegistryService = serviceTypeRegistryService;
         this.annotationKeyRegistryService = annotationKeyRegistryService;
-        this.npcUrl = this.annotationKeyRegistryService.findAnnotationKeyByName("npc.url");
+        // TODO remove. hard coded annotation for compatibility, need a better to group rpc url annotations
+        this.rpcEndpointAnnotationCodes = initRpcEndpointAnnotations(
+                AnnotationKey.HTTP_URL.getName(), AnnotationKey.MESSAGE_QUEUE_URI.getName(),
+                "thrift.url", "npc.url", "nimm.url"
+        );
+    }
+
+    private Set<Integer> initRpcEndpointAnnotations(String... annotationKeyNames) {
+        Set<Integer> rpcEndPointAnnotationCodes = new HashSet<>();
+        for (String annotationKeyName : annotationKeyNames) {
+            AnnotationKey pluginRpcEndpointAnnotationKey = null;
+            try {
+                pluginRpcEndpointAnnotationKey = annotationKeyRegistryService.findAnnotationKeyByName(annotationKeyName);
+            } catch (NoSuchElementException e) {
+                // ignore
+            }
+            if (pluginRpcEndpointAnnotationKey != null) {
+                rpcEndPointAnnotationCodes.add(pluginRpcEndpointAnnotationKey.getCode());
+            }
+        }
+        return ImmutableSet.copyOf(rpcEndPointAnnotationCodes);
     }
 
     @Override
@@ -101,7 +130,7 @@ public boolean accept(List<SpanBo> fromSpanList) {
     }
 
     private boolean isURL(int key) {
-        return key == AnnotationKey.HTTP_URL.getCode() || key == npcUrl.getCode();
+        return rpcEndpointAnnotationCodes.contains(key);
     }
 
     private String getPath(String endPoint) {
@@ -109,15 +138,16 @@ private String getPath(String endPoint) {
             return  null;
         }
         // is URI format
-        final int authorityIndex = endPoint.indexOf("://");
+        final String authoritySeparator = "://";
+        final int authorityIndex = endPoint.indexOf(authoritySeparator);
         if (authorityIndex == -1) {
             return endPoint;
         }
-        final int pathIndex = endPoint.indexOf('/', authorityIndex + 1);
+        final int pathIndex = endPoint.indexOf('/', authorityIndex + authoritySeparator.length());
         if (pathIndex == -1) {
 //            ???
             return endPoint;
         }
-        return endPoint.substring(pathIndex+1);
+        return endPoint.substring(pathIndex);
     }
 }

diff --git a/web/src/test/java/com/navercorp/pinpoint/web/filter/LinkFilterTest.java b/web/src/test/java/com/navercorp/pinpoint/web/filter/LinkFilterTest.java
index a96b51f49e6e..a9e060627063 100644
--- a/web/src/test/java/com/navercorp/pinpoint/web/filter/LinkFilterTest.java
+++ b/web/src/test/java/com/navercorp/pinpoint/web/filter/LinkFilterTest.java
@@ -1,38 +1,88 @@
 package com.navercorp.pinpoint.web.filter;
 
+import com.navercorp.pinpoint.common.server.bo.AnnotationBo;
+import com.navercorp.pinpoint.common.server.bo.SpanEventBo;
+import com.navercorp.pinpoint.common.service.AnnotationKeyRegistryService;
 import com.navercorp.pinpoint.common.service.ServiceTypeRegistryService;
+import com.navercorp.pinpoint.common.trace.AnnotationKey;
+import com.navercorp.pinpoint.common.trace.AnnotationKeyFactory;
 import com.navercorp.pinpoint.common.trace.ServiceType;
 import com.navercorp.pinpoint.common.server.bo.SpanBo;
 import com.navercorp.pinpoint.web.util.ServiceTypeRegistryMockFactory;
+import org.apache.hadoop.hbase.util.Base64;
 import org.junit.Assert;
 import org.junit.Test;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.nio.charset.StandardCharsets;
 import java.util.Arrays;
 import java.util.Collections;
 
+import static com.navercorp.pinpoint.common.trace.ServiceTypeProperty.*;
+
 /**
  * @author emeroad
  */
 public class LinkFilterTest {
+
+    private static final short UNKNOWN_TYPE_CODE = ServiceType.UNKNOWN.getCode();
+    private static final String UNKNOWN_TYPE_NAME = ServiceType.UNKNOWN.getName();
+    private static final short USER_TYPE_CODE = ServiceType.USER.getCode();
+    private static final String USER_TYPE_NAME = ServiceType.USER.getName();
+    private static final short TOMCAT_TYPE_CODE = 1010;
+    private static final String TOMCAT_TYPE_NAME = "TOMCAT";
+    private static final short RPC_TYPE_CODE = 9999;
+    private static final String RPC_TYPE_NAME = "RPC";
+    private static final short BACKEND_TYPE_CODE = 2100;
+    private static final String BACKEND_TYPE_NAME = "BACKEND";
+    private static final short MESSAGE_QUEUE_TYPE_CODE = 8310;
+    private static final String MESSAGE_QUEUE_TYPE_NAME = "MESSAGE_QUEUE";
+
+    private static final int RPC_ANNOTATION_CODE = -1;
+    private static final String RPC_ANNOTATION_NAME = "rpc.url";
+
     private final Logger logger = LoggerFactory.getLogger(this.getClass());
     private final ServiceTypeRegistryService serviceTypeRegistryService = mockServiceTypeRegistryService();
+    private final AnnotationKeyRegistryService annotationKeyRegistryService = mockAnnotationKeyRegistryService();
 
     private ServiceTypeRegistryService mockServiceTypeRegistryService() {
 
-        final short tomcatTypeCode = 1010;
-        final String tomcatTypeName = "TOMCAT";
         ServiceTypeRegistryMockFactory mockFactory = new ServiceTypeRegistryMockFactory();
-        mockFactory.addServiceTypeMock(tomcatTypeCode, tomcatTypeName);
+        mockFactory.addServiceTypeMock(UNKNOWN_TYPE_CODE, UNKNOWN_TYPE_NAME, RECORD_STATISTICS);
+        mockFactory.addServiceTypeMock(USER_TYPE_CODE, USER_TYPE_NAME, RECORD_STATISTICS);
+        mockFactory.addServiceTypeMock(TOMCAT_TYPE_CODE, TOMCAT_TYPE_NAME, RECORD_STATISTICS);
+        mockFactory.addServiceTypeMock(RPC_TYPE_CODE, RPC_TYPE_NAME, RECORD_STATISTICS);
+        mockFactory.addServiceTypeMock(BACKEND_TYPE_CODE, BACKEND_TYPE_NAME, TERMINAL, INCLUDE_DESTINATION_ID);
+        mockFactory.addServiceTypeMock(MESSAGE_QUEUE_TYPE_CODE, MESSAGE_QUEUE_TYPE_NAME, QUEUE, RECORD_STATISTICS);
 
         return mockFactory.createMockServiceTypeRegistryService();
     }
 
+    private AnnotationKeyRegistryService mockAnnotationKeyRegistryService() {
+        final AnnotationKey rpcUrlAnnotationKey = AnnotationKeyFactory.of(RPC_ANNOTATION_CODE, RPC_ANNOTATION_NAME);
+        return new AnnotationKeyRegistryService() {
+            @Override
+            public AnnotationKey findAnnotationKey(int annotationCode) {
+                return rpcUrlAnnotationKey;
+            }
+
+            @Override
+            public AnnotationKey findAnnotationKeyByName(String keyName) {
+                return rpcUrlAnnotationKey;
+            }
+
+            @Override
+            public AnnotationKey findApiErrorCode(int annotationCode) {
+                return rpcUrlAnnotationKey;
+            }
+        };
+    }
+
 
     @Test
     public void fromToFilterTest() {
-        ServiceType tomcat = serviceTypeRegistryService.findServiceTypeByName("TOMCAT");
+        ServiceType tomcat = serviceTypeRegistryService.findServiceTypeByName(TOMCAT_TYPE_NAME);
         final short tomcatServiceType = tomcat.getCode();
 
         FilterDescriptor descriptor = new FilterDescriptor();
@@ -44,9 +94,9 @@ public void fromToFilterTest() {
         descriptor.setToServiceType(tomcat.getName());
 //        descriptor.setToAgentId("AGENT_B");
 
-        FilterHint hint = new FilterHint(Collections.<RpcHint>emptyList());
+        FilterHint hint = new FilterHint(Collections.emptyList());
 
-        LinkFilter linkFilter = new LinkFilter(descriptor, hint, serviceTypeRegistryService);
+        LinkFilter linkFilter = new LinkFilter(descriptor, hint, serviceTypeRegistryService, annotationKeyRegistryService);
         logger.debug(linkFilter.toString());
 
         SpanBo fromSpanBo = new SpanBo();
@@ -74,7 +124,7 @@ public void fromToFilterTest() {
 
     @Test
     public void fromToFilterAgentTest() {
-        final ServiceType tomcat = serviceTypeRegistryService.findServiceTypeByName("TOMCAT");
+        final ServiceType tomcat = serviceTypeRegistryService.findServiceTypeByName(TOMCAT_TYPE_NAME);
         final short tomcatServiceType = tomcat.getCode();
 
         FilterDescriptor descriptor = new FilterDescriptor();
@@ -86,9 +136,9 @@ public void fromToFilterAgentTest() {
         descriptor.setToServiceType(tomcat.getName());
         descriptor.setToAgentId("AGENT_B");
 
-        FilterHint hint = new FilterHint(Collections.<RpcHint>emptyList());
+        FilterHint hint = new FilterHint(Collections.emptyList());
 
-        LinkFilter linkFilter = new LinkFilter(descriptor, hint, serviceTypeRegistryService);
+        LinkFilter linkFilter = new LinkFilter(descriptor, hint, serviceTypeRegistryService, annotationKeyRegistryService);
         logger.debug(linkFilter.toString());
 
         SpanBo fromSpanBo = new SpanBo();
@@ -111,9 +161,345 @@ public void fromToFilterAgentTest() {
 
         Assert.assertTrue(linkFilter.include(Arrays.asList(fromSpanBo, toSpanBO)));
         Assert.assertFalse(linkFilter.include(Arrays.asList(fromSpanBo, spanBoC)));
+    }
+
+    @Test
+    public void userToWasFilter() {
+        final ServiceType user = serviceTypeRegistryService.findServiceTypeByName(USER_TYPE_NAME);
+        final ServiceType tomcat = serviceTypeRegistryService.findServiceTypeByName(TOMCAT_TYPE_NAME);
+
+        FilterDescriptor descriptor = new FilterDescriptor();
+        descriptor.setFromApplicationName("USER");
+        descriptor.setFromServiceType(user.getName());
+        descriptor.setToApplicationName("APP_A");
+        descriptor.setToServiceType(tomcat.getName());
+
+        FilterHint hint = new FilterHint(Collections.emptyList());
+
+        LinkFilter linkFilter = new LinkFilter(descriptor, hint, serviceTypeRegistryService, annotationKeyRegistryService);
+        logger.debug(linkFilter.toString());
+
+        SpanBo user_appA = new SpanBo();
+        user_appA.setSpanId(1);
+        user_appA.setParentSpanId(-1);
+        user_appA.setApplicationId("APP_A");
+        user_appA.setApplicationServiceType(tomcat.getCode());
+        SpanBo appA_appB = new SpanBo();
+        appA_appB.setSpanId(2);
+        appA_appB.setParentSpanId(1);
+        appA_appB.setApplicationId("APP_B");
+        appA_appB.setApplicationServiceType(tomcat.getCode());
+        SpanBo appB_appA = new SpanBo();
+        appB_appA.setSpanId(3);
+        appB_appA.setParentSpanId(2);
+        appB_appA.setApplicationId("APP_A");
+        appB_appA.setApplicationServiceType(tomcat.getCode());
+
+        Assert.assertTrue(linkFilter.include(Collections.singletonList(user_appA)));
+        Assert.assertFalse(linkFilter.include(Collections.singletonList(appA_appB)));
+        Assert.assertFalse(linkFilter.include(Collections.singletonList(appB_appA)));
+        Assert.assertTrue(linkFilter.include(Arrays.asList(user_appA, appA_appB, appB_appA)));
+    }
+
+    @Test
+    public void wasToUnknownFilter() {
+        final ServiceType tomcat = serviceTypeRegistryService.findServiceTypeByName(TOMCAT_TYPE_NAME);
+        final ServiceType unknown = serviceTypeRegistryService.findServiceTypeByName(UNKNOWN_TYPE_NAME);
+
+        final String rpcHost = "some.domain.name";
+        final String rpcUrl = "http://" + rpcHost + "/some/test/path";
+        final String urlPattern = "/some/test/**";
+
+        FilterDescriptor descriptor = new FilterDescriptor();
+        descriptor.setFromApplicationName("APP_A");
+        descriptor.setFromServiceType(tomcat.getName());
+        descriptor.setToApplicationName(rpcHost);
+        descriptor.setToServiceType(unknown.getName());
+        descriptor.setUrl(encodeUrl(urlPattern));
+
+        FilterHint hint = new FilterHint(Collections.emptyList());
+
+        LinkFilter linkFilter = new LinkFilter(descriptor, hint, serviceTypeRegistryService, annotationKeyRegistryService);
+        logger.debug(linkFilter.toString());
+
+        // Reject - no rpc span event
+        SpanBo spanBo = new SpanBo();
+        spanBo.setSpanId(1);
+        spanBo.setParentSpanId(-1);
+        spanBo.setApplicationId("APP_A");
+        spanBo.setApplicationServiceType(tomcat.getCode());
+        Assert.assertFalse(linkFilter.include(Collections.singletonList(spanBo)));
+
+        // Accept - has matching rpc span event
+        AnnotationBo rpcAnnotation = new AnnotationBo();
+        rpcAnnotation.setKey(RPC_ANNOTATION_CODE);
+        rpcAnnotation.setValue(rpcUrl);
+        SpanEventBo rpcSpanEvent = new SpanEventBo();
+        rpcSpanEvent.setServiceType(RPC_TYPE_CODE);
+        rpcSpanEvent.setDestinationId(rpcHost);
+        rpcSpanEvent.setAnnotationBoList(Collections.singletonList(rpcAnnotation));
+        spanBo.addSpanEvent(rpcSpanEvent);
+        Assert.assertTrue(linkFilter.include(Collections.singletonList(spanBo)));
+    }
+
+    @Test
+    public void wasToWasFilter_perfectMatch() {
+        final ServiceType tomcat = serviceTypeRegistryService.findServiceTypeByName(TOMCAT_TYPE_NAME);
+
+        FilterDescriptor descriptor = new FilterDescriptor();
+        descriptor.setFromApplicationName("APP_A");
+        descriptor.setFromServiceType(tomcat.getName());
+        descriptor.setToApplicationName("APP_B");
+        descriptor.setToServiceType(tomcat.getName());
+
+        FilterHint hint = new FilterHint(Collections.emptyList());
+
+        LinkFilter linkFilter = new LinkFilter(descriptor, hint, serviceTypeRegistryService, annotationKeyRegistryService);
+        logger.debug(linkFilter.toString());
+
+        // Accept - perfect match
+        SpanBo user_appA = new SpanBo();
+        user_appA.setSpanId(1);
+        user_appA.setParentSpanId(-1);
+        user_appA.setApplicationId("APP_A");
+        user_appA.setApplicationServiceType(tomcat.getCode());
+        SpanBo appA_appB = new SpanBo();
+        appA_appB.setSpanId(2);
+        appA_appB.setParentSpanId(1);
+        appA_appB.setApplicationId("APP_B");
+        appA_appB.setApplicationServiceType(tomcat.getCode());
+        Assert.assertTrue(linkFilter.include(Arrays.asList(user_appA, appA_appB)));
+    }
+
+    @Test
+    public void wasToWasFilter_noMatch() {
+        final ServiceType tomcat = serviceTypeRegistryService.findServiceTypeByName(TOMCAT_TYPE_NAME);
+
+        FilterDescriptor descriptor = new FilterDescriptor();
+        descriptor.setFromApplicationName("APP_A");
+        descriptor.setFromServiceType(tomcat.getName());
+        descriptor.setToApplicationName("APP_B");
+        descriptor.setToServiceType(tomcat.getName());
 
+        FilterHint hint = new FilterHint(Collections.emptyList());
+
+        LinkFilter linkFilter = new LinkFilter(descriptor, hint, serviceTypeRegistryService, annotationKeyRegistryService);
+        logger.debug(linkFilter.toString());
+
+        // Reject - fromNode different
+        SpanBo user_appC = new SpanBo();
+        user_appC.setSpanId(1);
+        user_appC.setParentSpanId(-1);
+        user_appC.setApplicationId("APP_C");
+        user_appC.setApplicationServiceType(tomcat.getCode());
+        SpanBo appC_appB = new SpanBo();
+        appC_appB.setSpanId(2);
+        appC_appB.setParentSpanId(1);
+        appC_appB.setApplicationId("APP_B");
+        appC_appB.setApplicationServiceType(tomcat.getCode());
+        Assert.assertFalse(linkFilter.include(Arrays.asList(user_appC, appC_appB)));
+
+        // Reject - toNode different
+        SpanBo user_appA = new SpanBo();
+        user_appA.setSpanId(1);
+        user_appA.setParentSpanId(-1);
+        user_appA.setApplicationId("APP_A");
+        user_appA.setApplicationServiceType(tomcat.getCode());
+        SpanBo appA_appC = new SpanBo();
+        appA_appC.setSpanId(2);
+        appA_appC.setParentSpanId(1);
+        appA_appC.setApplicationId("APP_C");
+        appA_appC.setApplicationServiceType(tomcat.getCode());
+        Assert.assertFalse(linkFilter.include(Arrays.asList(user_appA, appA_appC)));
     }
 
+    @Test
+    public void wasToWasFilter_noMatch_missingReceivingSpan() {
+        final ServiceType tomcat = serviceTypeRegistryService.findServiceTypeByName(TOMCAT_TYPE_NAME);
+
+        final String rpcHost = "some.domain.name";
+        final String rpcUrl = "http://" + rpcHost + "/some/test/path";
+
+        FilterDescriptor descriptor = new FilterDescriptor();
+        descriptor.setFromApplicationName("APP_A");
+        descriptor.setFromServiceType(tomcat.getName());
+        descriptor.setToApplicationName("APP_B");
+        descriptor.setToServiceType(tomcat.getName());
+
+        FilterHint emptyHint = new FilterHint(Collections.emptyList());
+        FilterHint unmatchingHint = new FilterHint(Collections.singletonList(
+                new RpcHint("APP_B", Collections.singletonList(
+                        new RpcType("different.domain.name", RPC_TYPE_CODE)))));
+        FilterHint matchingHint = new FilterHint(Collections.singletonList(
+                new RpcHint("APP_B", Collections.singletonList(
+                        new RpcType(rpcHost, RPC_TYPE_CODE)))));
+
+        LinkFilter emptyHintLinkFilter = new LinkFilter(descriptor, emptyHint, serviceTypeRegistryService, annotationKeyRegistryService);
+        LinkFilter unmatchingHintLinkFilter = new LinkFilter(descriptor, unmatchingHint, serviceTypeRegistryService, annotationKeyRegistryService);
+        LinkFilter matchingHintLinkFilter = new LinkFilter(descriptor, matchingHint, serviceTypeRegistryService, annotationKeyRegistryService);
+        logger.debug("emptyHintLinkFilter : {}", emptyHintLinkFilter.toString());
+        logger.debug("unmatchingHintLinkFilter : {}", unmatchingHintLinkFilter.toString());
+        logger.debug("matchingHintLinkFilter : {}", matchingHintLinkFilter.toString());
+
+        SpanBo fromSpan = new SpanBo();
+        fromSpan.setSpanId(1);
+        fromSpan.setParentSpanId(-1);
+        fromSpan.setApplicationId("APP_A");
+        fromSpan.setApplicationServiceType(tomcat.getCode());
+        AnnotationBo rpcAnnotation = new AnnotationBo();
+        rpcAnnotation.setKey(RPC_ANNOTATION_CODE);
+        rpcAnnotation.setValue(rpcUrl);
+        SpanEventBo rpcSpanEvent = new SpanEventBo();
+        rpcSpanEvent.setServiceType(RPC_TYPE_CODE);
+        rpcSpanEvent.setDestinationId(rpcHost);
+        rpcSpanEvent.setAnnotationBoList(Collections.singletonList(rpcAnnotation));
+        fromSpan.addSpanEvent(rpcSpanEvent);
+        // Reject - filter hint empty
+        Assert.assertFalse(emptyHintLinkFilter.include(Collections.singletonList(fromSpan)));
+        // Reject - filter hint does not match
+        Assert.assertFalse(unmatchingHintLinkFilter.include(Collections.singletonList(fromSpan)));
+        // Accept - filter hint matches
+        Assert.assertTrue(matchingHintLinkFilter.include(Collections.singletonList(fromSpan)));
+
+        // Check rpc url as well
+        final String unmatchingUrlPattern = "/other/test/**";
+        final String matchingUrlPattern = "/some/test/**";
+        // Reject - url pattern does not match
+        descriptor.setUrl(unmatchingUrlPattern);
+        LinkFilter matchingHintLinkFilterWithUnmatchingUrlPattern = new LinkFilter(descriptor, matchingHint, serviceTypeRegistryService, annotationKeyRegistryService);
+        Assert.assertFalse(matchingHintLinkFilterWithUnmatchingUrlPattern.include(Collections.singletonList(fromSpan)));
+        // Accept - url pattern matches
+        descriptor.setUrl(encodeUrl(matchingUrlPattern));
+        LinkFilter matchingHintLinkFilterWithMatchingUrlPattern = new LinkFilter(descriptor, matchingHint, serviceTypeRegistryService, annotationKeyRegistryService);
+        Assert.assertTrue(matchingHintLinkFilterWithMatchingUrlPattern.include(Collections.singletonList(fromSpan)));
+    }
+
+    @Test
+    public void wasToBackendFilter() {
+        final ServiceType tomcat = serviceTypeRegistryService.findServiceTypeByName(TOMCAT_TYPE_NAME);
+        final ServiceType backend = serviceTypeRegistryService.findServiceTypeByName(BACKEND_TYPE_NAME);
+
+        final String destinationA = "BACKEND_A";
+        final String destinationB = "BACKEND_B";
+
+        FilterDescriptor descriptor = new FilterDescriptor();
+        descriptor.setFromApplicationName("APP_A");
+        descriptor.setFromServiceType(tomcat.getName());
+        descriptor.setToApplicationName(destinationA);
+        descriptor.setToServiceType(backend.getName());
+
+        FilterHint hint = new FilterHint(Collections.emptyList());
+
+        LinkFilter linkFilter = new LinkFilter(descriptor, hint, serviceTypeRegistryService, annotationKeyRegistryService);
+        logger.debug(linkFilter.toString());
+
+        SpanBo matchingSpan = new SpanBo();
+        matchingSpan.setApplicationId("APP_A");
+        matchingSpan.setApplicationServiceType(tomcat.getCode());
+        SpanEventBo spanEventDestinationA = new SpanEventBo();
+        spanEventDestinationA.setDestinationId(destinationA);
+        spanEventDestinationA.setServiceType(BACKEND_TYPE_CODE);
+        matchingSpan.addSpanEvent(spanEventDestinationA);
+        Assert.assertTrue(linkFilter.include(Collections.singletonList(matchingSpan)));
+
+        SpanBo unmatchingSpan = new SpanBo();
+        unmatchingSpan.setApplicationId("APP_A");
+        unmatchingSpan.setApplicationServiceType(tomcat.getCode());
+        SpanEventBo spanEventDestinationB = new SpanEventBo();
+        spanEventDestinationB.setDestinationId(destinationB);
+        spanEventDestinationB.setServiceType(BACKEND_TYPE_CODE);
+        unmatchingSpan.addSpanEvent(spanEventDestinationB);
+        Assert.assertFalse(linkFilter.include(Collections.singletonList(unmatchingSpan)));
+
+        Assert.assertTrue(linkFilter.include(Arrays.asList(matchingSpan, unmatchingSpan)));
+
+        SpanBo bothSpan = new SpanBo();
+        bothSpan.setApplicationId("APP_A");
+        bothSpan.setApplicationServiceType(tomcat.getCode());
+        bothSpan.addSpanEventBoList(Arrays.asList(spanEventDestinationA, spanEventDestinationB));
+        Assert.assertTrue(linkFilter.include(Collections.singletonList(bothSpan)));
+    }
 
+    @Test
+    public void wasToQueueFilter() {
+        final ServiceType tomcat = serviceTypeRegistryService.findServiceTypeByName(TOMCAT_TYPE_NAME);
+        final ServiceType messageQueue = serviceTypeRegistryService.findServiceTypeByName(MESSAGE_QUEUE_TYPE_NAME);
+
+        final String messageQueueA = "QUEUE_A";
+        final String messageQueueB = "QUEUE_B";
+
+        FilterDescriptor descriptor = new FilterDescriptor();
+        descriptor.setFromApplicationName("APP_A");
+        descriptor.setFromServiceType(tomcat.getName());
+        descriptor.setToApplicationName(messageQueueA);
+        descriptor.setToServiceType(messageQueue.getName());
+
+        FilterHint hint = new FilterHint(Collections.emptyList());
+
+        LinkFilter linkFilter = new LinkFilter(descriptor, hint, serviceTypeRegistryService, annotationKeyRegistryService);
+        logger.debug(linkFilter.toString());
+
+        SpanBo matchingSpan = new SpanBo();
+        matchingSpan.setApplicationId("APP_A");
+        matchingSpan.setApplicationServiceType(tomcat.getCode());
+        SpanEventBo spanEventDestinationA = new SpanEventBo();
+        spanEventDestinationA.setDestinationId(messageQueueA);
+        spanEventDestinationA.setServiceType(MESSAGE_QUEUE_TYPE_CODE);
+        matchingSpan.addSpanEvent(spanEventDestinationA);
+        Assert.assertTrue(linkFilter.include(Collections.singletonList(matchingSpan)));
+
+        SpanBo unmatchingSpan = new SpanBo();
+        unmatchingSpan.setApplicationId("APP_A");
+        unmatchingSpan.setApplicationServiceType(tomcat.getCode());
+        SpanEventBo spanEventDestinationB = new SpanEventBo();
+        spanEventDestinationB.setDestinationId(messageQueueB);
+        spanEventDestinationB.setServiceType(MESSAGE_QUEUE_TYPE_CODE);
+        unmatchingSpan.addSpanEvent(spanEventDestinationB);
+        Assert.assertFalse(linkFilter.include(Collections.singletonList(unmatchingSpan)));
+
+        Assert.assertTrue(linkFilter.include(Arrays.asList(matchingSpan, unmatchingSpan)));
+
+        SpanBo bothSpan = new SpanBo();
+        bothSpan.setApplicationId("APP_A");
+        bothSpan.setApplicationServiceType(tomcat.getCode());
+        bothSpan.addSpanEventBoList(Arrays.asList(spanEventDestinationA, spanEventDestinationB));
+        Assert.assertTrue(linkFilter.include(Collections.singletonList(bothSpan)));
+    }
+
+    @Test
+    public void queueToWasFilter() {
+        final ServiceType tomcat = serviceTypeRegistryService.findServiceTypeByName(TOMCAT_TYPE_NAME);
+        final ServiceType messageQueue = serviceTypeRegistryService.findServiceTypeByName(MESSAGE_QUEUE_TYPE_NAME);
+
+        final String messageQueueA = "QUEUE_A";
+        final String messageQueueB = "QUEUE_B";
+
+        FilterDescriptor descriptor = new FilterDescriptor();
+        descriptor.setFromApplicationName(messageQueueA);
+        descriptor.setFromServiceType(messageQueue.getName());
+        descriptor.setToApplicationName("APP_A");
+        descriptor.setToServiceType(tomcat.getName());
+
+        FilterHint hint = new FilterHint(Collections.emptyList());
+
+        LinkFilter linkFilter = new LinkFilter(descriptor, hint, serviceTypeRegistryService, annotationKeyRegistryService);
+        logger.debug(linkFilter.toString());
+
+        SpanBo matchingSpan = new SpanBo();
+        matchingSpan.setApplicationId("APP_A");
+        matchingSpan.setApplicationServiceType(tomcat.getCode());
+        matchingSpan.setAcceptorHost(messageQueueA);
+        Assert.assertTrue(linkFilter.include(Collections.singletonList(matchingSpan)));
+
+        SpanBo unmatchingSpan = new SpanBo();
+        unmatchingSpan.setApplicationId("APP_A");
+        unmatchingSpan.setApplicationServiceType(tomcat.getCode());
+        unmatchingSpan.setAcceptorHost(messageQueueB);
+        Assert.assertFalse(linkFilter.include(Collections.singletonList(unmatchingSpan)));
+    }
+
+    private String encodeUrl(String value) {
+        return Base64.encodeBytes(value.getBytes(StandardCharsets.UTF_8));
+    }
 
 }
\ No newline at end of file
diff --git a/web/src/test/java/com/navercorp/pinpoint/web/filter/RpcURLPatternFilterTest.java b/web/src/test/java/com/navercorp/pinpoint/web/filter/RpcURLPatternFilterTest.java
new file mode 100644
index 000000000000..25bcd1b9eb33
--- /dev/null
+++ b/web/src/test/java/com/navercorp/pinpoint/web/filter/RpcURLPatternFilterTest.java
@@ -0,0 +1,181 @@
+/*
+ * Copyright 2017 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.web.filter;
+
+import com.navercorp.pinpoint.common.server.bo.AnnotationBo;
+import com.navercorp.pinpoint.common.server.bo.SpanBo;
+import com.navercorp.pinpoint.common.server.bo.SpanEventBo;
+import com.navercorp.pinpoint.common.service.AnnotationKeyRegistryService;
+import com.navercorp.pinpoint.common.service.ServiceTypeRegistryService;
+import com.navercorp.pinpoint.common.trace.AnnotationKey;
+import com.navercorp.pinpoint.common.trace.AnnotationKeyFactory;
+import com.navercorp.pinpoint.common.trace.ServiceType;
+import com.navercorp.pinpoint.common.trace.ServiceTypeFactory;
+import com.navercorp.pinpoint.common.trace.ServiceTypeProperty;
+import org.apache.hadoop.hbase.util.Base64;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * @author HyunGil Jeong
+ */
+public class RpcURLPatternFilterTest {
+
+    private final AnnotationKey TEST_RPC_URL_ANNOTATION_KEY = AnnotationKeyFactory.of(-1, "rpc.url");
+    private final short TEST_RPC_SERVICE_TYPE_CODE = 9999;
+    private final String TEST_RPC_SERVICE_TYPE_NAME = "TEST_RPC";
+    private final ServiceType TEST_RPC_SERVICE_TYPE = ServiceTypeFactory.of(TEST_RPC_SERVICE_TYPE_CODE, TEST_RPC_SERVICE_TYPE_NAME, ServiceTypeProperty.RECORD_STATISTICS);
+
+    private ServiceTypeRegistryService serviceTypeRegistryService;
+
+    private AnnotationKeyRegistryService annotationKeyRegistryService;
+
+    @Before
+    public void setUp() {
+        serviceTypeRegistryService = new ServiceTypeRegistryService() {
+            @Override
+            public ServiceType findServiceType(short serviceType) {
+                return TEST_RPC_SERVICE_TYPE;
+            }
+
+            @Override
+            public ServiceType findServiceTypeByName(String typeName) {
+                throw new UnsupportedOperationException();
+            }
+
+            @Override
+            public List<ServiceType> findDesc(String desc) {
+                throw new UnsupportedOperationException();
+            }
+        };
+        annotationKeyRegistryService = new AnnotationKeyRegistryService() {
+            @Override
+            public AnnotationKey findAnnotationKey(int annotationCode) {
+                throw new UnsupportedOperationException();
+            }
+
+            @Override
+            public AnnotationKey findAnnotationKeyByName(String keyName) {
+                return TEST_RPC_URL_ANNOTATION_KEY;
+            }
+
+            @Override
+            public AnnotationKey findApiErrorCode(int annotationCode) {
+                throw new UnsupportedOperationException();
+            }
+        };
+    }
+
+    @Test
+    public void emptyPatternShouldReject() {
+        // Given
+        final String urlPattern = "";
+        final String rpcUrl = "http://a.b.c";
+        final RpcURLPatternFilter rpcURLPatternFilter = new RpcURLPatternFilter(encode(urlPattern), serviceTypeRegistryService, annotationKeyRegistryService);
+        // When
+        boolean accept = rpcURLPatternFilter.accept(createTestRpcSpans(rpcUrl));
+        // Then
+        Assert.assertFalse(accept);
+    }
+
+    @Test
+    public void testPath() {
+        // Given
+        final String urlPattern = "/test/**";
+        final String rpcUrl = "/test/rpc/path";
+        final RpcURLPatternFilter rpcURLPatternFilter = new RpcURLPatternFilter(encode(urlPattern), serviceTypeRegistryService, annotationKeyRegistryService);
+        // When
+        boolean accept = rpcURLPatternFilter.accept(createTestRpcSpans(rpcUrl));
+        // Then
+        Assert.assertTrue(accept);
+    }
+
+    @Test
+    public void testFullUrl() {
+        // Given
+        final String urlPattern = "/test/**";
+        final String rpcUrl = "http://some.test.domain:8080/test/rpc/path";
+        final RpcURLPatternFilter rpcURLPatternFilter = new RpcURLPatternFilter(encode(urlPattern), serviceTypeRegistryService, annotationKeyRegistryService);
+        // When
+        boolean accept = rpcURLPatternFilter.accept(createTestRpcSpans(rpcUrl));
+        // Then
+        Assert.assertTrue(accept);
+    }
+
+    @Test
+    public void testDomainAndPath() {
+        // Given
+        final String urlPattern = "some.test.domain/test/rpc/**";
+        final String rpcUrl = "some.test.domain/test/rpc/test?value=11";
+        final RpcURLPatternFilter rpcURLPatternFilter = new RpcURLPatternFilter(encode(urlPattern), serviceTypeRegistryService, annotationKeyRegistryService);
+        // When
+        boolean accept = rpcURLPatternFilter.accept(createTestRpcSpans(rpcUrl));
+        // Then
+        Assert.assertTrue(accept);
+    }
+
+    @Test
+    public void testString() {
+        // Given
+        final String urlPattern = "some*";
+        final String rpcUrl = "someName";
+        final RpcURLPatternFilter rpcURLPatternFilter = new RpcURLPatternFilter(encode(urlPattern), serviceTypeRegistryService, annotationKeyRegistryService);
+        // When
+        boolean accept = rpcURLPatternFilter.accept(createTestRpcSpans(rpcUrl));
+        // Then
+        Assert.assertTrue(accept);
+    }
+
+    @Test
+    public void testWeirdPath() {
+        // Given
+        final String urlPattern = ":/**";
+        final String rpcUrl = ":/invalid/uri";
+        final RpcURLPatternFilter rpcURLPatternFilter = new RpcURLPatternFilter(encode(urlPattern), serviceTypeRegistryService, annotationKeyRegistryService);
+        // When
+        boolean accept = rpcURLPatternFilter.accept(createTestRpcSpans(rpcUrl));
+        // Then
+        Assert.assertTrue(accept);
+    }
+
+    private String encode(String value) {
+        return Base64.encodeBytes(value.getBytes(StandardCharsets.UTF_8));
+    }
+
+    private List<SpanBo> createTestRpcSpans(String... rpcUrls) {
+        List<SpanBo> spanBos = new ArrayList<>();
+        for (String rpcUrl : rpcUrls) {
+            SpanEventBo testRpcSpanEvent = new SpanEventBo();
+            testRpcSpanEvent.setServiceType(TEST_RPC_SERVICE_TYPE_CODE);
+            AnnotationBo testRpcAnnotationBo = new AnnotationBo();
+            testRpcAnnotationBo.setKey(TEST_RPC_URL_ANNOTATION_KEY.getCode());
+            testRpcAnnotationBo.setValue(rpcUrl);
+            testRpcSpanEvent.setAnnotationBoList(Collections.singletonList(testRpcAnnotationBo));
+            SpanBo spanBo = new SpanBo();
+            spanBo.addSpanEvent(testRpcSpanEvent);
+            spanBos.add(spanBo);
+        }
+        return spanBos;
+    }
+}
diff --git a/web/src/test/java/com/navercorp/pinpoint/web/util/ServiceTypeRegistryMockFactory.java b/web/src/test/java/com/navercorp/pinpoint/web/util/ServiceTypeRegistryMockFactory.java
index 5a8143f32b0e..52e183e38786 100644
--- a/web/src/test/java/com/navercorp/pinpoint/web/util/ServiceTypeRegistryMockFactory.java
+++ b/web/src/test/java/com/navercorp/pinpoint/web/util/ServiceTypeRegistryMockFactory.java
@@ -19,6 +19,7 @@
 import com.navercorp.pinpoint.common.service.ServiceTypeRegistryService;
 import com.navercorp.pinpoint.common.trace.ServiceType;
 import com.navercorp.pinpoint.common.trace.ServiceTypeCategory;
+import com.navercorp.pinpoint.common.trace.ServiceTypeProperty;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -38,16 +39,43 @@ public class ServiceTypeRegistryMockFactory {
 
     private final Map<Short, ServiceType> serviceTypeMap = new HashMap<>();
 
-    public void addServiceTypeMock(short typeCode, String typeName) {
+    public void addServiceTypeMock(short typeCode, String typeName, ServiceTypeProperty... serviceTypeProperties) {
         // setup serviceType
         ServiceType mockServiceType = mock(ServiceType.class);
         when(mockServiceType.getCode()).thenReturn(typeCode);
         when(mockServiceType.getName()).thenReturn(typeName);
 
+        if (ServiceType.USER.getName().equals(typeName)) {
+            when(mockServiceType.isUser()).thenReturn(true);
+        }
+        if (ServiceType.UNKNOWN.getName().equals(typeName)) {
+            when(mockServiceType.isUnknown()).thenReturn(true);
+        }
         if (ServiceTypeCategory.SERVER.contains(typeCode)) {
-            logger.debug("mark isWas() {}/{}", mockServiceType.getName(), mockServiceType.getCode());
             when(mockServiceType.isWas()).thenReturn(true);
         }
+        if (ServiceTypeCategory.RPC.contains(typeCode)) {
+            when(mockServiceType.isRpcClient()).thenReturn(true);
+        }
+
+        for (ServiceTypeProperty serviceTypeProperty : serviceTypeProperties) {
+            switch (serviceTypeProperty) {
+                case TERMINAL:
+                    when(mockServiceType.isTerminal()).thenReturn(true);
+                    break;
+                case QUEUE:
+                    when(mockServiceType.isQueue()).thenReturn(true);
+                    break;
+                case RECORD_STATISTICS:
+                    when(mockServiceType.isRecordStatistics()).thenReturn(true);
+                    break;
+                case INCLUDE_DESTINATION_ID:
+                    when(mockServiceType.isIncludeDestinationId()).thenReturn(true);
+                    break;
+                default:
+                    throw new IllegalArgumentException("Unknown serviceTypeProperty : " + serviceTypeProperty);
+            }
+        }
 
         this.serviceTypeMap.put(typeCode, mockServiceType);
     }
