From 0ee43a622cd0b186c3fc61843511a47d1f059c52 Mon Sep 17 00:00:00 2001 From: emeroad Date: Tue, 17 Mar 2026 17:39:45 +0900 Subject: [PATCH 1/2] [#noissue] Add serviceName field to ApplicationForm and update related classes --- .../controller/FilteredMapController.java | 1 + .../controller/form/ApplicationForm.java | 14 +- .../web/applicationmap/nodes/NodeName.java | 11 +- .../applicationmap/nodes/ServiceNodeName.java | 19 +- .../nodes/ServiceNodeNameParser.java | 64 +++++++ .../web/applicationmap/view/LinkView.java | 11 +- .../web/applicationmap/view/NodeView.java | 3 +- .../web/filter/ApplicationFilter.java | 5 +- .../pinpoint/web/filter/FilterDescriptor.java | 27 ++- .../pinpoint/web/filter/LinkFilter.java | 25 +-- .../web/filter/transaction/LinkContext.java | 11 +- .../web/filter/transaction/NodeContext.java | 9 +- .../web/filter/transaction/SpanContext.java | 4 +- .../nodes/ServiceNodeNameParserTest.java | 88 ++++++++++ .../nodes/ServiceNodeNameTest.java | 164 ++++++++++++++++++ .../web/filter/FilterDescriptorTest.java | 42 ++--- .../pinpoint/web/filter/LinkFilterTest.java | 62 +++---- 17 files changed, 469 insertions(+), 91 deletions(-) create mode 100644 web/src/main/java/com/navercorp/pinpoint/web/applicationmap/nodes/ServiceNodeNameParser.java create mode 100644 web/src/test/java/com/navercorp/pinpoint/web/applicationmap/nodes/ServiceNodeNameParserTest.java create mode 100644 web/src/test/java/com/navercorp/pinpoint/web/applicationmap/nodes/ServiceNodeNameTest.java diff --git a/web/src/main/java/com/navercorp/pinpoint/web/applicationmap/controller/FilteredMapController.java b/web/src/main/java/com/navercorp/pinpoint/web/applicationmap/controller/FilteredMapController.java index b79b668179cd..0a2e85809e12 100644 --- a/web/src/main/java/com/navercorp/pinpoint/web/applicationmap/controller/FilteredMapController.java +++ b/web/src/main/java/com/navercorp/pinpoint/web/applicationmap/controller/FilteredMapController.java @@ -108,6 +108,7 @@ public FilterMapViewV3 getFilterServer( @RequestParam(value = "useStatisticsAgentState", defaultValue = "true", required = false) boolean useStatisticsAgentState, @RequestParam(value = "traceIndexReadV2", required = false) Optional traceIndexReadV2) { + final String serviceName = appForm.getServiceName(); final String applicationName = appForm.getApplicationName(); final int limit = Math.min(limitParam, LimitUtils.MAX); diff --git a/web/src/main/java/com/navercorp/pinpoint/web/applicationmap/controller/form/ApplicationForm.java b/web/src/main/java/com/navercorp/pinpoint/web/applicationmap/controller/form/ApplicationForm.java index fb0a173b6d67..499c6add603c 100644 --- a/web/src/main/java/com/navercorp/pinpoint/web/applicationmap/controller/form/ApplicationForm.java +++ b/web/src/main/java/com/navercorp/pinpoint/web/applicationmap/controller/form/ApplicationForm.java @@ -1,12 +1,16 @@ package com.navercorp.pinpoint.web.applicationmap.controller.form; import com.navercorp.pinpoint.common.trace.ServiceType; +import com.navercorp.pinpoint.web.vo.Service; import jakarta.validation.constraints.NotBlank; public class ApplicationForm { public static final int UNDEFINED = ServiceType.UNDEFINED.getCode(); + + private String serviceName = Service.DEFAULT.getServiceName(); + @NotBlank private String applicationName; private int serviceTypeCode = UNDEFINED; @@ -15,6 +19,13 @@ public class ApplicationForm { public ApplicationForm() { } + public String getServiceName() { + return serviceName; + } + + public void setServiceName(String serviceName) { + this.serviceName = serviceName; + } public void setApplicationName(String applicationName) { this.applicationName = applicationName; @@ -43,7 +54,8 @@ public String getServiceTypeName() { @Override public String toString() { return "ApplicationForm{" + - "applicationName='" + applicationName + '\'' + + "serviceName='" + serviceName + '\'' + + ", applicationName='" + applicationName + '\'' + ", serviceTypeCode=" + serviceTypeCode + ", serviceTypeName='" + serviceTypeName + '\'' + '}'; diff --git a/web/src/main/java/com/navercorp/pinpoint/web/applicationmap/nodes/NodeName.java b/web/src/main/java/com/navercorp/pinpoint/web/applicationmap/nodes/NodeName.java index e63904019bac..1dee954261eb 100644 --- a/web/src/main/java/com/navercorp/pinpoint/web/applicationmap/nodes/NodeName.java +++ b/web/src/main/java/com/navercorp/pinpoint/web/applicationmap/nodes/NodeName.java @@ -10,7 +10,8 @@ * @author Woonduk Kang(emeroad) */ public class NodeName { - public static final String NODE_DELIMITER = "^"; + public static final char NODE_DELIMITER_CHAR = '^'; + public static final String NODE_DELIMITER = "" + NODE_DELIMITER_CHAR; private final String name; private final ServiceType serviceType; @@ -31,11 +32,15 @@ public String getName() { } public static String toNodeName(String name, ServiceType serviceType) { - return name + NODE_DELIMITER + serviceType.getDesc(); + return newNodeKey(name, serviceType.getDesc()); } public static String toNodeKey(String name, ServiceType serviceType) { - return name + NODE_DELIMITER + serviceType.getName(); + return newNodeKey(name, serviceType.getName()); + } + + private static String newNodeKey(String name, String serviceType) { + return name + NODE_DELIMITER_CHAR + serviceType; } @Override diff --git a/web/src/main/java/com/navercorp/pinpoint/web/applicationmap/nodes/ServiceNodeName.java b/web/src/main/java/com/navercorp/pinpoint/web/applicationmap/nodes/ServiceNodeName.java index 83c60fcfda59..662fe97fb177 100644 --- a/web/src/main/java/com/navercorp/pinpoint/web/applicationmap/nodes/ServiceNodeName.java +++ b/web/src/main/java/com/navercorp/pinpoint/web/applicationmap/nodes/ServiceNodeName.java @@ -9,8 +9,9 @@ * @author Woonduk Kang(emeroad) */ public class ServiceNodeName { - public static final String SERVICE_DELIMITER = ":"; + public static final String SERVICE_DELIMITER = NodeName.NODE_DELIMITER; public static final String NODE_DELIMITER = NodeName.NODE_DELIMITER; + public static final String NODE_ESCAPED_DELIMITER = "\\" + NodeName.NODE_DELIMITER; private final String serviceName; private final String applicationName; @@ -32,11 +33,23 @@ public String getName() { } public static String toServiceNodeName(String serviceName, String applicationName, ServiceType serviceType) { - return serviceName + SERVICE_DELIMITER + applicationName + NODE_DELIMITER + serviceType.getDesc(); + return newServiceNodeKey(serviceName, applicationName, serviceType.getDesc()); } public static String toServiceNodeKey(String serviceName, String applicationName, ServiceType serviceType) { - return serviceName + SERVICE_DELIMITER + applicationName + NODE_DELIMITER + serviceType.getName(); + return newServiceNodeKey(serviceName, applicationName, serviceType.getName()); + } + + public static String newServiceNodeKey(String serviceName, String applicationName, String serviceType) { + return serviceName + SERVICE_DELIMITER + escapeApplicationName(applicationName) + NODE_DELIMITER + serviceType; + } + + public static String escapeApplicationName(String applicationName) { + return applicationName.replace(NODE_DELIMITER, NODE_ESCAPED_DELIMITER); + } + + public static String unescapeApplicationName(String escapedApplicationName) { + return escapedApplicationName.replace(ServiceNodeName.NODE_ESCAPED_DELIMITER, ServiceNodeName.NODE_DELIMITER); } @Override diff --git a/web/src/main/java/com/navercorp/pinpoint/web/applicationmap/nodes/ServiceNodeNameParser.java b/web/src/main/java/com/navercorp/pinpoint/web/applicationmap/nodes/ServiceNodeNameParser.java new file mode 100644 index 000000000000..2b9abb201f81 --- /dev/null +++ b/web/src/main/java/com/navercorp/pinpoint/web/applicationmap/nodes/ServiceNodeNameParser.java @@ -0,0 +1,64 @@ +package com.navercorp.pinpoint.web.applicationmap.nodes; + +import com.navercorp.pinpoint.common.trace.ServiceType; +import com.navercorp.pinpoint.loader.service.ServiceTypeRegistryService; +import com.navercorp.pinpoint.web.vo.Service; + +import java.util.Objects; + +/** + * @author Woonduk Kang(emeroad) + */ +public class ServiceNodeNameParser { + + private final ServiceTypeRegistryService serviceTypeRegistryService; + + public ServiceNodeNameParser(ServiceTypeRegistryService serviceTypeRegistryService) { + this.serviceTypeRegistryService = Objects.requireNonNull(serviceTypeRegistryService, "serviceTypeRegistryService"); + } + + public ServiceNodeName parse(String serviceNodeName) { + Objects.requireNonNull(serviceNodeName, "serviceNodeName"); + Objects.requireNonNull(serviceTypeRegistryService, "serviceTypeRegistryService"); + + int lastDelimiter = serviceNodeName.lastIndexOf(NodeName.NODE_DELIMITER_CHAR); + if (lastDelimiter == -1) { + throw new IllegalArgumentException("ServiceType not found :" + serviceNodeName); + } + String serviceTypeName = serviceNodeName.substring(lastDelimiter + 1); + + + String serviceNameAndApp = serviceNodeName.substring(0, lastDelimiter); + + String serviceName; + int firstDelimiter = findServiceDelimiter(serviceNameAndApp, lastDelimiter); +// int firstDelimiter = serviceNameAndApp.indexOf(NodeName.NODE_DELIMITER_CHAR); + if (firstDelimiter == -1) { + serviceName = Service.DEFAULT.getServiceName(); + } else { + serviceName = serviceNodeName.substring(0, firstDelimiter); + } + + String escapedApplicationName = serviceNameAndApp.substring(firstDelimiter + 1); + String applicationName = ServiceNodeName.unescapeApplicationName(escapedApplicationName); + + ServiceType serviceType = serviceTypeRegistryService.findServiceTypeByName(serviceTypeName); + if (serviceType == null) { + throw new IllegalArgumentException("Unknown serviceType :" + serviceType); + } + + return new ServiceNodeName(serviceName, applicationName, serviceType); + } + + private static int findServiceDelimiter(String serviceNameAndApp, int lastDelimiter) { + for (int i = 0; i < lastDelimiter; i++) { + if (serviceNameAndApp.charAt(i) == NodeName.NODE_DELIMITER_CHAR) { + if (i == 0 || serviceNameAndApp.charAt(i - 1) != '\\') { + return i; + } + } + } + return -1; + } + +} diff --git a/web/src/main/java/com/navercorp/pinpoint/web/applicationmap/view/LinkView.java b/web/src/main/java/com/navercorp/pinpoint/web/applicationmap/view/LinkView.java index 12f4c16bda6b..e50dcac168c3 100644 --- a/web/src/main/java/com/navercorp/pinpoint/web/applicationmap/view/LinkView.java +++ b/web/src/main/java/com/navercorp/pinpoint/web/applicationmap/view/LinkView.java @@ -77,12 +77,17 @@ public void serialize(LinkView linkView, JsonGenerator jgen, SerializerProvider Link link = linkView.getLink(); jgen.writeStartObject(); - jgen.writeObjectField("key", link.getLinkName()); // for servermap + jgen.writeObjectField("key", link.getServiceLinkName().toString()); // for servermap +// jgen.writeObjectField("key", link.getLinkName()); // for servermap + jgen.writeStringField("linkKey", link.getLinkNameKey()); jgen.writeObjectField("serviceKey", link.getServiceLinkName().toString()); - jgen.writeObjectField("from", link.getFrom().getNodeName()); // necessary for go.js - jgen.writeObjectField("to", link.getTo().getNodeName()); // necessary for go.js +// jgen.writeObjectField("from", link.getFrom().getNodeName()); // necessary for servermap +// jgen.writeObjectField("to", link.getTo().getNodeName()); // necessary for servermap + + jgen.writeObjectField("from", link.getFrom().getServiceNodeName().toString()); // necessary for servermap + jgen.writeObjectField("to", link.getTo().getServiceNodeName().toString()); // necessary for servermap // for FilterWizard, to agent mapping data writeAgents("fromAgents", link.getFrom(), jgen); diff --git a/web/src/main/java/com/navercorp/pinpoint/web/applicationmap/view/NodeView.java b/web/src/main/java/com/navercorp/pinpoint/web/applicationmap/view/NodeView.java index 150fab25afad..6da23f60b39b 100644 --- a/web/src/main/java/com/navercorp/pinpoint/web/applicationmap/view/NodeView.java +++ b/web/src/main/java/com/navercorp/pinpoint/web/applicationmap/view/NodeView.java @@ -101,7 +101,8 @@ public void serialize(NodeView nodeView, JsonGenerator jgen, SerializerProvider jgen.writeStartObject(); // jgen.writeStringField("id", node.getNodeName()); serverInstanceList - jgen.writeObjectField("key", node.getNodeName()); // necessary for go.js + jgen.writeObjectField("key", node.getServiceNodeName().toString()); // necessary for servermap +// jgen.writeObjectField("key", node.getNodeName()); // necessary for servermap jgen.writeStringField("nodeKey", node.getNodeKey()); jgen.writeObjectField("serviceKey", node.getServiceNodeName().toString()); diff --git a/web/src/main/java/com/navercorp/pinpoint/web/filter/ApplicationFilter.java b/web/src/main/java/com/navercorp/pinpoint/web/filter/ApplicationFilter.java index 1564fa713ef4..4ff3b5083fba 100644 --- a/web/src/main/java/com/navercorp/pinpoint/web/filter/ApplicationFilter.java +++ b/web/src/main/java/com/navercorp/pinpoint/web/filter/ApplicationFilter.java @@ -18,7 +18,6 @@ import com.navercorp.pinpoint.common.server.bo.SpanBo; import com.navercorp.pinpoint.common.trace.ServiceType; - import com.navercorp.pinpoint.common.util.StringUtils; import com.navercorp.pinpoint.loader.service.ServiceTypeRegistryService; import com.navercorp.pinpoint.web.filter.agent.AgentFilter; @@ -30,8 +29,8 @@ import com.navercorp.pinpoint.web.filter.responsetime.SpanResponseConditionFilter; import com.navercorp.pinpoint.web.filter.transaction.NodeContext; import com.navercorp.pinpoint.web.filter.transaction.SpanContext; -import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import java.util.List; import java.util.Objects; @@ -92,7 +91,7 @@ private List findDesc(String serviceType, String typeName) { @Override public boolean include(List spanBoList) { SpanContext spanContext = new SpanContext(spanBoList, serviceTypeRegistryService); - NodeContext nodeContext = new NodeContext(spanContext, selfNode.getApplicationName(), serviceDescList, agentFilter); + NodeContext nodeContext = new NodeContext(spanContext, selfNode.getServiceName(), selfNode.getApplicationName(), serviceDescList, agentFilter); com.navercorp.pinpoint.web.filter.transaction.ApplicationFilter filter = new com.navercorp.pinpoint.web.filter.transaction.ApplicationFilter(spanResponseConditionFilter, acceptURLFilter); return filter.include(nodeContext); diff --git a/web/src/main/java/com/navercorp/pinpoint/web/filter/FilterDescriptor.java b/web/src/main/java/com/navercorp/pinpoint/web/filter/FilterDescriptor.java index a793b83807d8..af615eb85d0e 100644 --- a/web/src/main/java/com/navercorp/pinpoint/web/filter/FilterDescriptor.java +++ b/web/src/main/java/com/navercorp/pinpoint/web/filter/FilterDescriptor.java @@ -26,6 +26,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.navercorp.pinpoint.common.util.StringUtils; +import com.navercorp.pinpoint.web.vo.Service; import java.io.IOException; import java.nio.charset.StandardCharsets; @@ -77,16 +78,22 @@ public FilterDescriptor(FromNode fromNode, ToNode toNode, SelfNode selfNode, Res @JsonIgnoreProperties(ignoreUnknown=true) public static class Node { + private final String serviceName; private final String applicationName; private final String serviceType ; private final String agentId; - public Node(String applicationName, String serviceType, String agentId) { + public Node(String serviceName, String applicationName, String serviceType, String agentId) { + this.serviceName = Objects.requireNonNullElse(serviceName, Service.DEFAULT.getServiceName()); this.applicationName = applicationName; this.serviceType = serviceType; this.agentId = agentId; } + public String getServiceName() { + return serviceName; + } + public String getApplicationName() { return applicationName; } @@ -100,12 +107,13 @@ public String getAgentId() { } public boolean isValid() { - return StringUtils.hasLength(applicationName) && StringUtils.hasLength(serviceType); + return StringUtils.hasLength(serviceName) & StringUtils.hasLength(applicationName) && StringUtils.hasLength(serviceType); } @Override public String toString() { return this.getClass().getSimpleName() + "{" + + "serviceName='" + serviceName + '\'' + "applicationName='" + applicationName + '\'' + ", serviceType='" + serviceType + '\'' + ", agentId='" + agentId + '\'' + @@ -115,10 +123,11 @@ public String toString() { public static class FromNode extends Node { @JsonCreator - public FromNode(@JsonProperty("fa") String applicationName, + public FromNode(@JsonProperty("fs") String serviceName, + @JsonProperty("fa") String applicationName, @JsonProperty("fst") String serviceType, @JsonProperty("fan") String agentId) { - super(applicationName, serviceType, agentId); + super(serviceName, applicationName, serviceType, agentId); } } @@ -127,10 +136,11 @@ public static class ToNode extends Node { * to application */ @JsonCreator - public ToNode(@JsonProperty("ta") String applicationName, + public ToNode(@JsonProperty("ts") String serviceName, + @JsonProperty("ta") String applicationName, @JsonProperty("tst") String serviceType, @JsonProperty("tan") String agentId) { - super(applicationName, serviceType, agentId); + super(serviceName, applicationName, serviceType, agentId); } } @@ -139,10 +149,11 @@ public static class SelfNode extends Node { * self application */ @JsonCreator - public SelfNode(@JsonProperty("a") String applicationName, + public SelfNode(@JsonProperty("s") String serviceName, + @JsonProperty("a") String applicationName, @JsonProperty("st") String serviceType, @JsonProperty("an") String agentId) { - super(applicationName, serviceType, agentId); + super(serviceName, applicationName, serviceType, agentId); } } 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 7a0bba7d3708..158a8f6ede7f 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 @@ -16,16 +16,12 @@ package com.navercorp.pinpoint.web.filter; -import java.util.List; -import java.util.Objects; - - import com.navercorp.pinpoint.common.server.bo.SpanBo; import com.navercorp.pinpoint.common.server.bo.SpanEventBo; +import com.navercorp.pinpoint.common.trace.ServiceType; import com.navercorp.pinpoint.common.util.StringUtils; import com.navercorp.pinpoint.loader.service.AnnotationKeyRegistryService; import com.navercorp.pinpoint.loader.service.ServiceTypeRegistryService; -import com.navercorp.pinpoint.common.trace.ServiceType; import com.navercorp.pinpoint.web.filter.agent.AgentFilter; import com.navercorp.pinpoint.web.filter.agent.AgentFilterFactory; import com.navercorp.pinpoint.web.filter.responsetime.DefaultExecutionTypeFilter; @@ -42,9 +38,11 @@ import com.navercorp.pinpoint.web.filter.transaction.WasToQueueFilter; import com.navercorp.pinpoint.web.filter.transaction.WasToUnknownFilter; import com.navercorp.pinpoint.web.filter.transaction.WasToWasFilter; - -import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.util.List; +import java.util.Objects; /** * @author netspider @@ -96,10 +94,10 @@ public LinkFilter(FilterDescriptor filterDescriptor, FilterHint filterHint, Serv this.filterHint = Objects.requireNonNull(filterHint, "filterHint"); - final String fromAgentName = fromNode.getAgentId(); - final String toAgentName = toNode.getAgentId(); + final String fromAgentId = fromNode.getAgentId(); + final String toAgentId = toNode.getAgentId(); - this.agentFilterFactory = new AgentFilterFactory(fromAgentName, toAgentName); + this.agentFilterFactory = new AgentFilterFactory(fromAgentId, toAgentId); logger.debug("agentFilterFactory:{}", agentFilterFactory); this.fromAgentFilter = agentFilterFactory.createFromAgentFilter(); this.toAgentFilter = agentFilterFactory.createToAgentFilter(); @@ -189,12 +187,15 @@ private Filter resolveLinkFilter() { public boolean include(List transaction) { SpanContext spanContext = new SpanContext(transaction, serviceTypeRegistryService); + final String fromServiceName = fromNode.getServiceName(); final String fromApplicationName = fromNode.getApplicationName(); + + final String toServiceName = toNode.getServiceName(); final String toApplicationName = toNode.getApplicationName(); LinkContext linkContext = new LinkContext(spanContext, - fromApplicationName, fromServiceDescList, fromAgentFilter, - toApplicationName, toServiceDescList, toAgentFilter); + fromServiceName, fromApplicationName, fromServiceDescList, fromAgentFilter, + toServiceName, toApplicationName, toServiceDescList, toAgentFilter); return filter.include(linkContext); } diff --git a/web/src/main/java/com/navercorp/pinpoint/web/filter/transaction/LinkContext.java b/web/src/main/java/com/navercorp/pinpoint/web/filter/transaction/LinkContext.java index e33eb32fdc0d..f73dbbfc2f52 100644 --- a/web/src/main/java/com/navercorp/pinpoint/web/filter/transaction/LinkContext.java +++ b/web/src/main/java/com/navercorp/pinpoint/web/filter/transaction/LinkContext.java @@ -32,30 +32,35 @@ public class LinkContext { private final SpanContext spanContext; + private final String fromServiceName; private final String fromApplicationName; private final List fromServiceDescList; private final AgentFilter fromAgentFilter; + private final String toServiceName; private final String toApplicationName; private final List toServiceDescList; private final AgentFilter toAgentFilter; public LinkContext(SpanContext spanContext, + String fromServiceName, String fromApplicationName, List fromServiceDescList, AgentFilter fromAgentFilter, + String toServiceName, String toApplicationName, List toServiceDescList, AgentFilter toAgentFilter) { this.spanContext = Objects.requireNonNull(spanContext, "spanContext"); - + this.fromServiceName = Objects.requireNonNull(fromServiceName, "fromServiceName"); this.fromApplicationName = Objects.requireNonNull(fromApplicationName, "fromApplicationName"); this.fromServiceDescList = Objects.requireNonNull(fromServiceDescList, "fromServiceDescList"); this.fromAgentFilter = Objects.requireNonNull(fromAgentFilter, "fromAgentFilter"); + this.toServiceName = Objects.requireNonNull(toServiceName, "toServiceName"); this.toApplicationName = Objects.requireNonNull(toApplicationName, "toApplicationName"); this.toServiceDescList = Objects.requireNonNull(toServiceDescList, "toServiceDescList"); this.toAgentFilter = Objects.requireNonNull(toAgentFilter, "toAgentFilter"); @@ -64,7 +69,7 @@ public LinkContext(SpanContext spanContext, public List findFromNode() { - return spanContext.findNode(fromApplicationName, fromServiceDescList, fromAgentFilter); + return spanContext.findNode(fromServiceName, fromApplicationName, fromServiceDescList, fromAgentFilter); } // public List findToNode() { @@ -72,7 +77,7 @@ public List findFromNode() { // } public List findToNode(URLPatternFilter acceptURLFilter) { - final List node = spanContext.findNode(toApplicationName, toServiceDescList, toAgentFilter); + final List node = spanContext.findNode(toServiceName, toApplicationName, toServiceDescList, toAgentFilter); if (acceptURLFilter == null) { return node; } diff --git a/web/src/main/java/com/navercorp/pinpoint/web/filter/transaction/NodeContext.java b/web/src/main/java/com/navercorp/pinpoint/web/filter/transaction/NodeContext.java index c67bffa9f09d..72b8df9ad909 100644 --- a/web/src/main/java/com/navercorp/pinpoint/web/filter/transaction/NodeContext.java +++ b/web/src/main/java/com/navercorp/pinpoint/web/filter/transaction/NodeContext.java @@ -30,21 +30,26 @@ public class NodeContext { private final SpanContext spanContext; + private final String serviceName; private final String applicationName; private final List serviceDescList; private final AgentFilter agentFilter; - public NodeContext(SpanContext spanContext, String applicationName, List serviceDescList, AgentFilter agentFilter){ + public NodeContext(SpanContext spanContext, String serviceName, String applicationName, List serviceDescList, AgentFilter agentFilter){ this.spanContext = Objects.requireNonNull(spanContext, "spanContext"); + this.serviceName = Objects.requireNonNull(serviceName, "serviceName"); this.applicationName = Objects.requireNonNull(applicationName, "applicationName"); this.serviceDescList = Objects.requireNonNull(serviceDescList, "serviceDescList"); this.agentFilter = Objects.requireNonNull(agentFilter, "agentFilter"); } public List findApplicationNode() { - return spanContext.findNode(applicationName, serviceDescList, agentFilter); + return spanContext.findNode(serviceName, applicationName, serviceDescList, agentFilter); } + public String getServiceName() { + return serviceName; + } public String getApplicationName() { return applicationName; diff --git a/web/src/main/java/com/navercorp/pinpoint/web/filter/transaction/SpanContext.java b/web/src/main/java/com/navercorp/pinpoint/web/filter/transaction/SpanContext.java index 5b96698d07de..959219a7d627 100644 --- a/web/src/main/java/com/navercorp/pinpoint/web/filter/transaction/SpanContext.java +++ b/web/src/main/java/com/navercorp/pinpoint/web/filter/transaction/SpanContext.java @@ -40,11 +40,11 @@ public SpanContext(List nodeList, this.serviceTypeRegistryService = Objects.requireNonNull(serviceTypeRegistryService, "serviceTypeRegistryService"); } - public List findNode(String findApplicationName, List findServiceCode, AgentFilter agentFilter) { + public List findNode(String findServiceName, String findApplicationName, List findServiceCode, AgentFilter agentFilter) { List findList = null; for (SpanBo span : nodeList) { final ServiceType applicationServiceType = serviceTypeRegistryService.findServiceType(span.getApplicationServiceType()); - if (findApplicationName.equals(span.getApplicationName()) && includeServiceType(findServiceCode, applicationServiceType)) { + if (findServiceName.equals(span.getServiceName()) && findApplicationName.equals(span.getApplicationName()) && includeServiceType(findServiceCode, applicationServiceType)) { // apply preAgentFilter if (agentFilter.accept(span.getAgentId())) { if (findList == null) { diff --git a/web/src/test/java/com/navercorp/pinpoint/web/applicationmap/nodes/ServiceNodeNameParserTest.java b/web/src/test/java/com/navercorp/pinpoint/web/applicationmap/nodes/ServiceNodeNameParserTest.java new file mode 100644 index 000000000000..786f24a4eefb --- /dev/null +++ b/web/src/test/java/com/navercorp/pinpoint/web/applicationmap/nodes/ServiceNodeNameParserTest.java @@ -0,0 +1,88 @@ +package com.navercorp.pinpoint.web.applicationmap.nodes; + +import com.navercorp.pinpoint.common.trace.ServiceType; +import com.navercorp.pinpoint.loader.service.ServiceTypeRegistryService; +import com.navercorp.pinpoint.web.util.ServiceTypeRegistryMockFactory; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class ServiceNodeNameParserTest { + + private ServiceTypeRegistryService newServiceTypeRegistry() { + ServiceTypeRegistryMockFactory factory = new ServiceTypeRegistryMockFactory(); + factory.addServiceTypeMock(ServiceType.STAND_ALONE); + factory.addServiceTypeMock(ServiceType.UNKNOWN); + return factory.createMockServiceTypeRegistryService(); + } + + @Test + void parse() { + ServiceTypeRegistryService registry = newServiceTypeRegistry(); + ServiceNodeName parsed = new ServiceNodeNameParser(registry).parse("myService^myApp^STAND_ALONE"); + assertEquals(new ServiceNodeName("myService", "myApp", ServiceType.STAND_ALONE), parsed); + } + + @Test + void parse_withEscapedApplicationName() { + ServiceTypeRegistryService registry = newServiceTypeRegistry(); + ServiceNodeName parsed = new ServiceNodeNameParser(registry).parse("svc^my\\^app^UNKNOWN"); + assertEquals(new ServiceNodeName("svc", "my^app", ServiceType.UNKNOWN), parsed); + } + + @Test + void parse_roundTrip() { + ServiceTypeRegistryService registry = newServiceTypeRegistry(); + ServiceNodeName original = new ServiceNodeName("svc", "my^app", ServiceType.STAND_ALONE); + ServiceNodeName parsed = new ServiceNodeNameParser(registry).parse(original.getName()); + assertEquals(original, parsed); + } + + @Test + void parse_nullServiceNodeName() { + ServiceTypeRegistryService registry = newServiceTypeRegistry(); + assertThrows(NullPointerException.class, () -> new ServiceNodeNameParser(registry).parse(null)); + } + + @Test + void parse_nullServiceTypeRegistry() { + assertThrows(NullPointerException.class, () -> new ServiceNodeNameParser(null)); + } + + @Test + void parse_noDelimiter() { + ServiceTypeRegistryService registry = newServiceTypeRegistry(); + assertThrows(IllegalArgumentException.class, () -> new ServiceNodeNameParser(registry).parse("invalidString")); + } + + @Test + void parse_unknownServiceType() { + ServiceTypeRegistryService registry = newServiceTypeRegistry(); + assertThrows(IllegalArgumentException.class, () -> new ServiceNodeNameParser(registry).parse("svc^app^NONEXISTENT")); + } + + @Test + void parse_roundTrip_withBackslashAndCaret() { + ServiceTypeRegistryService registry = newServiceTypeRegistry(); + ServiceNodeName original = new ServiceNodeName("svc", "a\\^b", ServiceType.STAND_ALONE); + ServiceNodeName parsed = new ServiceNodeNameParser(registry).parse(original.getName()); + assertEquals(original, parsed); + } + + @Test + void parse_roundTrip_withTrailingBackslash() { + ServiceTypeRegistryService registry = newServiceTypeRegistry(); + ServiceNodeName original = new ServiceNodeName("svc", "app\\", ServiceType.UNKNOWN); + ServiceNodeName parsed = new ServiceNodeNameParser(registry).parse(original.getName()); + assertEquals(original, parsed); + } + + @Test + void parse_roundTrip_multipleEscapeChars() { + ServiceTypeRegistryService registry = newServiceTypeRegistry(); + ServiceNodeName original = new ServiceNodeName("svc", "a^b\\^c^d", ServiceType.STAND_ALONE); + ServiceNodeName parsed = new ServiceNodeNameParser(registry).parse(original.getName()); + assertEquals(original, parsed); + } +} diff --git a/web/src/test/java/com/navercorp/pinpoint/web/applicationmap/nodes/ServiceNodeNameTest.java b/web/src/test/java/com/navercorp/pinpoint/web/applicationmap/nodes/ServiceNodeNameTest.java new file mode 100644 index 000000000000..ef1b5c01d9aa --- /dev/null +++ b/web/src/test/java/com/navercorp/pinpoint/web/applicationmap/nodes/ServiceNodeNameTest.java @@ -0,0 +1,164 @@ +package com.navercorp.pinpoint.web.applicationmap.nodes; + +import com.navercorp.pinpoint.common.trace.ServiceType; +import com.navercorp.pinpoint.web.vo.Application; +import com.navercorp.pinpoint.web.vo.Service; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class ServiceNodeNameTest { + + @Test + void constructor() { + ServiceNodeName nodeName = new ServiceNodeName("myService", "myApp", ServiceType.STAND_ALONE); + assertEquals("myService^myApp^STAND_ALONE", nodeName.getName()); + } + + @Test + void constructor_nullServiceName() { + assertThrows(NullPointerException.class, + () -> new ServiceNodeName(null, "myApp", ServiceType.STAND_ALONE)); + } + + @Test + void constructor_nullApplicationName() { + assertThrows(NullPointerException.class, + () -> new ServiceNodeName("myService", null, ServiceType.STAND_ALONE)); + } + + @Test + void constructor_nullServiceType() { + assertThrows(NullPointerException.class, + () -> new ServiceNodeName("myService", "myApp", null)); + } + + @Test + void of() { + Service service = new Service("myService", 1); + Application application = new Application(service, "myApp", ServiceType.STAND_ALONE); + + ServiceNodeName nodeName = ServiceNodeName.of(application); + assertEquals("myService^myApp^STAND_ALONE", nodeName.getName()); + } + + @Test + void of_nullApplication() { + assertThrows(NullPointerException.class, () -> ServiceNodeName.of(null)); + } + + @Test + void getName() { + ServiceNodeName nodeName = new ServiceNodeName("svc", "app", ServiceType.UNKNOWN); + String expected = "svc^app^UNKNOWN"; + assertEquals(expected, nodeName.getName()); + } + + @Test + void toServiceNodeName() { + String result = ServiceNodeName.toServiceNodeName("svc", "app", ServiceType.STAND_ALONE); + assertEquals("svc^app^STAND_ALONE", result); + } + + @Test + void toServiceNodeKey() { + String result = ServiceNodeName.toServiceNodeKey("svc", "app", ServiceType.STAND_ALONE); + // toServiceNodeKey uses serviceType.getName() instead of getDesc() + assertEquals("svc^app^STAND_ALONE", result); + } + + @Test + void escapeApplicationName() { + // Application name containing the NODE_DELIMITER "^" should be escaped + String escaped = ServiceNodeName.escapeApplicationName("my^app"); + assertEquals("my\\^app", escaped); + } + + @Test + void escapeApplicationName_noDelimiter() { + String escaped = ServiceNodeName.escapeApplicationName("myapp"); + assertEquals("myapp", escaped); + } + + @Test + void escapeApplicationName_multipleDelimiters() { + String escaped = ServiceNodeName.escapeApplicationName("a^b^c"); + assertEquals("a\\^b\\^c", escaped); + } + + @Test + void toServiceNodeName_withEscape() { + String result = ServiceNodeName.toServiceNodeName("svc", "my^app", ServiceType.UNKNOWN); + assertEquals("svc^my\\^app^UNKNOWN", result); + } + + @Test + void toServiceNodeKey_withEscape() { + String result = ServiceNodeName.toServiceNodeKey("svc", "my^app", ServiceType.UNKNOWN); + assertEquals("svc^my\\^app^UNKNOWN", result); + } + + @Test + void equals_same() { + ServiceNodeName name1 = new ServiceNodeName("svc", "app", ServiceType.STAND_ALONE); + ServiceNodeName name2 = new ServiceNodeName("svc", "app", ServiceType.STAND_ALONE); + assertEquals(name1, name2); + assertEquals(name1.hashCode(), name2.hashCode()); + } + + @Test + void equals_differentServiceName() { + ServiceNodeName name1 = new ServiceNodeName("svc1", "app", ServiceType.STAND_ALONE); + ServiceNodeName name2 = new ServiceNodeName("svc2", "app", ServiceType.STAND_ALONE); + assertNotEquals(name1, name2); + } + + @Test + void equals_differentApplicationName() { + ServiceNodeName name1 = new ServiceNodeName("svc", "app1", ServiceType.STAND_ALONE); + ServiceNodeName name2 = new ServiceNodeName("svc", "app2", ServiceType.STAND_ALONE); + assertNotEquals(name1, name2); + } + + @Test + void equals_differentServiceType() { + ServiceNodeName name1 = new ServiceNodeName("svc", "app", ServiceType.STAND_ALONE); + ServiceNodeName name2 = new ServiceNodeName("svc", "app", ServiceType.UNKNOWN); + assertNotEquals(name1, name2); + } + + @Test + void equals_null() { + ServiceNodeName name1 = new ServiceNodeName("svc", "app", ServiceType.STAND_ALONE); + assertNotEquals(null, name1); + } + + @Test + void equals_differentClass() { + ServiceNodeName name1 = new ServiceNodeName("svc", "app", ServiceType.STAND_ALONE); + assertNotEquals("string", name1); + } + + @Test + void testToString() { + ServiceNodeName nodeName = new ServiceNodeName("svc", "app", ServiceType.UNKNOWN); + assertEquals(nodeName.getName(), nodeName.toString()); + } + + @Test + void escapeApplicationName_withBackslash() { + // applicationName containing "\" should be preserved as-is (only "^" gets escaped) + String escaped = ServiceNodeName.escapeApplicationName("my\\app"); + assertEquals("my\\app", escaped); + } + + @Test + void escapeApplicationName_withBackslashAndCaret() { + // applicationName = "a\^b" (backslash + caret): "^" gets escaped to "\^", so "\^" becomes "\\^" + String escaped = ServiceNodeName.escapeApplicationName("a\\^b"); + assertEquals("a\\\\^b", escaped); + } +} + diff --git a/web/src/test/java/com/navercorp/pinpoint/web/filter/FilterDescriptorTest.java b/web/src/test/java/com/navercorp/pinpoint/web/filter/FilterDescriptorTest.java index 40b85c51da87..cec31de545fd 100644 --- a/web/src/test/java/com/navercorp/pinpoint/web/filter/FilterDescriptorTest.java +++ b/web/src/test/java/com/navercorp/pinpoint/web/filter/FilterDescriptorTest.java @@ -68,30 +68,32 @@ public void convert() throws IOException { private String writeJsonString() throws IOException { StringWriter writer = new StringWriter(); - JsonGenerator json = mapper.getFactory().createGenerator(writer); + try (JsonGenerator json = mapper.getFactory().createGenerator(writer)) { // json.writeStartArray(); - json.writeStartObject(); - json.writeStringField("fa", "FROM_APPLICATION"); - json.writeStringField("fst", "FROM_SERVICE_TYPE"); - json.writeStringField("fan", "FROM_AGENT_ID"); - // fromResponseTime - json.writeNumberField("rf", 0); - - json.writeStringField("ta", "TO_APPLICATION"); - json.writeStringField("tst", "TO_SERVICE_TYPE"); - json.writeStringField("tan", "TO_AGENT_ID"); - // toResponseTime - json.writeNumberField("rt", 1000); - - json.writeNumberField("ie", 1); - - json.writeStringField("url", encodeBase64("/**")); - json.writeEndObject(); + json.writeStartObject(); + json.writeStringField("fs", "FROM_SERVICE_NAME"); + json.writeStringField("fa", "FROM_APPLICATION"); + json.writeStringField("fst", "FROM_SERVICE_TYPE"); + json.writeStringField("fan", "FROM_AGENT_ID"); + // fromResponseTime + json.writeNumberField("rf", 0); + + json.writeStringField("ts", "TO_SERVICE_NAME"); + json.writeStringField("ta", "TO_APPLICATION"); + json.writeStringField("tst", "TO_SERVICE_TYPE"); + json.writeStringField("tan", "TO_AGENT_ID"); + // toResponseTime + json.writeNumberField("rt", 1000); + + json.writeNumberField("ie", 1); + + json.writeStringField("url", encodeBase64("/**")); + json.writeEndObject(); // json.writeEndArray(); - json.flush(); - json.close(); + json.flush(); + } String jsonString = writer.toString(); logger.debug("json:{}", jsonString); 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 f1e8da29b8ed..a8f2c05b3790 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 @@ -9,6 +9,7 @@ import com.navercorp.pinpoint.loader.service.AnnotationKeyRegistryService; import com.navercorp.pinpoint.loader.service.ServiceTypeRegistryService; import com.navercorp.pinpoint.web.TestTraceUtils; +import com.navercorp.pinpoint.web.vo.Service; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.junit.jupiter.api.Assertions; @@ -37,6 +38,7 @@ */ public class LinkFilterTest { + private static final String DEFAULT_SERVICE = Service.DEFAULT.getServiceName(); private static final int RPC_ANNOTATION_CODE = -1; private static final String RPC_ANNOTATION_NAME = "rpc.url"; @@ -61,9 +63,9 @@ public void fromToFilterTest() { ServiceType tomcat = serviceTypeRegistryService.findServiceTypeByName(TOMCAT_TYPE_NAME); final int tomcatServiceType = tomcat.getCode(); - FilterDescriptor.FromNode fromNode = new FilterDescriptor.FromNode("APP_A", tomcat.getName(), null); - FilterDescriptor.ToNode toNode = new FilterDescriptor.ToNode("APP_B", tomcat.getName(), null); - FilterDescriptor.SelfNode selfNode = new FilterDescriptor.SelfNode(null, null, null); + FilterDescriptor.FromNode fromNode = new FilterDescriptor.FromNode(DEFAULT_SERVICE, "APP_A", tomcat.getName(), null); + FilterDescriptor.ToNode toNode = new FilterDescriptor.ToNode(DEFAULT_SERVICE, "APP_B", tomcat.getName(), null); + FilterDescriptor.SelfNode selfNode = new FilterDescriptor.SelfNode(null, null, null, null); FilterDescriptor.ResponseTime responseTime = new FilterDescriptor.ResponseTime(null, null); FilterDescriptor.Option option = new FilterDescriptor.Option(null, null); FilterDescriptor descriptor = new FilterDescriptor(fromNode, toNode, selfNode, responseTime, option); @@ -102,9 +104,9 @@ public void fromToFilterAgentTest() { final ServiceType tomcat = serviceTypeRegistryService.findServiceTypeByName(TOMCAT_TYPE_NAME); final int tomcatServiceType = tomcat.getCode(); - FilterDescriptor.FromNode fromNode = new FilterDescriptor.FromNode("APP_A", tomcat.getName(), "AGENT_A"); - FilterDescriptor.ToNode toNode = new FilterDescriptor.ToNode("APP_B", tomcat.getName(), "AGENT_B"); - FilterDescriptor.SelfNode selfNode = new FilterDescriptor.SelfNode(null, null, null); + FilterDescriptor.FromNode fromNode = new FilterDescriptor.FromNode(DEFAULT_SERVICE, "APP_A", tomcat.getName(), "AGENT_A"); + FilterDescriptor.ToNode toNode = new FilterDescriptor.ToNode(DEFAULT_SERVICE, "APP_B", tomcat.getName(), "AGENT_B"); + FilterDescriptor.SelfNode selfNode = new FilterDescriptor.SelfNode(null, null, null, null); FilterDescriptor.ResponseTime responseTime = new FilterDescriptor.ResponseTime(null, null); FilterDescriptor.Option option = new FilterDescriptor.Option(null, null); FilterDescriptor descriptor = new FilterDescriptor(fromNode, toNode, selfNode, responseTime, option); @@ -141,9 +143,9 @@ public void userToWasFilter() { final ServiceType user = serviceTypeRegistryService.findServiceTypeByName(USER_TYPE_NAME); final ServiceType tomcat = serviceTypeRegistryService.findServiceTypeByName(TOMCAT_TYPE_NAME); - FilterDescriptor.FromNode fromNode = new FilterDescriptor.FromNode("USER", user.getName(), null); - FilterDescriptor.ToNode toNode = new FilterDescriptor.ToNode("APP_A", tomcat.getName(), null); - FilterDescriptor.SelfNode selfNode = new FilterDescriptor.SelfNode(null, null, null); + FilterDescriptor.FromNode fromNode = new FilterDescriptor.FromNode(DEFAULT_SERVICE,"USER", user.getName(), null); + FilterDescriptor.ToNode toNode = new FilterDescriptor.ToNode(DEFAULT_SERVICE,"APP_A", tomcat.getName(), null); + FilterDescriptor.SelfNode selfNode = new FilterDescriptor.SelfNode(null, null, null, null); FilterDescriptor.Option option = new FilterDescriptor.Option(null, null); FilterDescriptor.ResponseTime responseTime = new FilterDescriptor.ResponseTime(null, null); FilterDescriptor descriptor = new FilterDescriptor(fromNode, toNode, selfNode, responseTime, option); @@ -188,9 +190,9 @@ public void wasToUnknownFilter() { final String rpcUrl = "http://" + rpcHost + "/some/test/path"; final String urlPattern = "/some/test/**"; - FilterDescriptor.FromNode fromNode = new FilterDescriptor.FromNode("APP_A", tomcat.getName(), null); - FilterDescriptor.ToNode toNode = new FilterDescriptor.ToNode(rpcHost, unknown.getName(), null); - FilterDescriptor.SelfNode selfNode = new FilterDescriptor.SelfNode(null, null, null); + FilterDescriptor.FromNode fromNode = new FilterDescriptor.FromNode(DEFAULT_SERVICE,"APP_A", tomcat.getName(), null); + FilterDescriptor.ToNode toNode = new FilterDescriptor.ToNode(DEFAULT_SERVICE,rpcHost, unknown.getName(), null); + FilterDescriptor.SelfNode selfNode = new FilterDescriptor.SelfNode(null, null, null, null); FilterDescriptor.Option option = new FilterDescriptor.Option(encodeUrl(urlPattern), null); FilterDescriptor.ResponseTime responseTime = new FilterDescriptor.ResponseTime(null, null); FilterDescriptor descriptor = new FilterDescriptor(fromNode, toNode, selfNode, responseTime, option); @@ -222,9 +224,9 @@ public void wasToUnknownFilter() { public void wasToWasFilter_perfectMatch() { final ServiceType tomcat = serviceTypeRegistryService.findServiceTypeByName(TOMCAT_TYPE_NAME); - FilterDescriptor.FromNode fromNode = new FilterDescriptor.FromNode("APP_A", tomcat.getName(), null); - FilterDescriptor.ToNode toNode = new FilterDescriptor.ToNode("APP_B", tomcat.getName(), null); - FilterDescriptor.SelfNode selfNode = new FilterDescriptor.SelfNode(null, null, null); + FilterDescriptor.FromNode fromNode = new FilterDescriptor.FromNode(DEFAULT_SERVICE,"APP_A", tomcat.getName(), null); + FilterDescriptor.ToNode toNode = new FilterDescriptor.ToNode(DEFAULT_SERVICE,"APP_B", tomcat.getName(), null); + FilterDescriptor.SelfNode selfNode = new FilterDescriptor.SelfNode(null, null, null, null); FilterDescriptor.ResponseTime responseTime = new FilterDescriptor.ResponseTime(null, null); FilterDescriptor.Option option = new FilterDescriptor.Option(null, null); FilterDescriptor descriptor = new FilterDescriptor(fromNode, toNode, selfNode, responseTime, option); @@ -252,9 +254,9 @@ public void wasToWasFilter_perfectMatch() { public void wasToWasFilter_noMatch() { final ServiceType tomcat = serviceTypeRegistryService.findServiceTypeByName(TOMCAT_TYPE_NAME); - FilterDescriptor.FromNode fromNode = new FilterDescriptor.FromNode("APP_A", tomcat.getName(), null); - FilterDescriptor.ToNode toNode = new FilterDescriptor.ToNode("APP_B", tomcat.getName(), null); - FilterDescriptor.SelfNode selfNode = new FilterDescriptor.SelfNode(null, null, null); + FilterDescriptor.FromNode fromNode = new FilterDescriptor.FromNode(DEFAULT_SERVICE,"APP_A", tomcat.getName(), null); + FilterDescriptor.ToNode toNode = new FilterDescriptor.ToNode(DEFAULT_SERVICE,"APP_B", tomcat.getName(), null); + FilterDescriptor.SelfNode selfNode = new FilterDescriptor.SelfNode(null, null, null, null); FilterDescriptor.ResponseTime responseTime = new FilterDescriptor.ResponseTime(null, null); FilterDescriptor.Option option = new FilterDescriptor.Option(null, null); FilterDescriptor descriptor = new FilterDescriptor(fromNode, toNode, selfNode, responseTime, option); @@ -298,9 +300,9 @@ public void wasToWasFilter_noMatch_missingReceivingSpan() { final String rpcHost = "some.domain.name"; final String rpcUrl = "http://" + rpcHost + "/some/test/path"; - FilterDescriptor.FromNode fromNode = new FilterDescriptor.FromNode("APP_A", tomcat.getName(), null); - FilterDescriptor.ToNode toNode = new FilterDescriptor.ToNode("APP_B", tomcat.getName(), null); - FilterDescriptor.SelfNode selfNode = new FilterDescriptor.SelfNode(null, null, null); + FilterDescriptor.FromNode fromNode = new FilterDescriptor.FromNode(DEFAULT_SERVICE,"APP_A", tomcat.getName(), null); + FilterDescriptor.ToNode toNode = new FilterDescriptor.ToNode(DEFAULT_SERVICE,"APP_B", tomcat.getName(), null); + FilterDescriptor.SelfNode selfNode = new FilterDescriptor.SelfNode(null, null, null, null); FilterDescriptor.ResponseTime responseTime = new FilterDescriptor.ResponseTime(null, null); FilterDescriptor.Option option = mock(FilterDescriptor.Option.class); FilterDescriptor descriptor = new FilterDescriptor(fromNode, toNode, selfNode, responseTime, option); @@ -360,9 +362,9 @@ public void wasToBackendFilter() { final String destinationA = "BACKEND_A"; final String destinationB = "BACKEND_B"; - FilterDescriptor.FromNode fromNode = new FilterDescriptor.FromNode("APP_A", tomcat.getName(), null); - FilterDescriptor.ToNode toNode = new FilterDescriptor.ToNode(destinationA, backend.getName(), null); - FilterDescriptor.SelfNode selfNode = new FilterDescriptor.SelfNode(null, null, null); + FilterDescriptor.FromNode fromNode = new FilterDescriptor.FromNode(DEFAULT_SERVICE, "APP_A", tomcat.getName(), null); + FilterDescriptor.ToNode toNode = new FilterDescriptor.ToNode(DEFAULT_SERVICE, destinationA, backend.getName(), null); + FilterDescriptor.SelfNode selfNode = new FilterDescriptor.SelfNode(null, null, null, null); FilterDescriptor.ResponseTime responseTime = new FilterDescriptor.ResponseTime(null, null); FilterDescriptor.Option option = mock(FilterDescriptor.Option.class); FilterDescriptor descriptor = new FilterDescriptor(fromNode, toNode, selfNode, responseTime, option); @@ -407,9 +409,9 @@ public void wasToQueueFilter() { final String messageQueueA = "QUEUE_A"; final String messageQueueB = "QUEUE_B"; - FilterDescriptor.FromNode fromNode = new FilterDescriptor.FromNode("APP_A", tomcat.getName(), null); - FilterDescriptor.ToNode toNode = new FilterDescriptor.ToNode(messageQueueA, messageQueue.getName(), null); - FilterDescriptor.SelfNode selfNode = new FilterDescriptor.SelfNode(null, null, null); + FilterDescriptor.FromNode fromNode = new FilterDescriptor.FromNode(DEFAULT_SERVICE, "APP_A", tomcat.getName(), null); + FilterDescriptor.ToNode toNode = new FilterDescriptor.ToNode(DEFAULT_SERVICE, messageQueueA, messageQueue.getName(), null); + FilterDescriptor.SelfNode selfNode = new FilterDescriptor.SelfNode(null, null, null, null); FilterDescriptor.ResponseTime responseTime = new FilterDescriptor.ResponseTime(null, null); FilterDescriptor.Option option = mock(FilterDescriptor.Option.class); FilterDescriptor descriptor = new FilterDescriptor(fromNode, toNode, selfNode, responseTime, option); @@ -454,9 +456,9 @@ public void queueToWasFilter() { final String messageQueueA = "QUEUE_A"; final String messageQueueB = "QUEUE_B"; - FilterDescriptor.FromNode fromNode = new FilterDescriptor.FromNode(messageQueueA, messageQueue.getName(), null); - FilterDescriptor.ToNode toNode = new FilterDescriptor.ToNode("APP_A", tomcat.getName(), null); - FilterDescriptor.SelfNode selfNode = new FilterDescriptor.SelfNode(null, null, null); + FilterDescriptor.FromNode fromNode = new FilterDescriptor.FromNode(DEFAULT_SERVICE, messageQueueA, messageQueue.getName(), null); + FilterDescriptor.ToNode toNode = new FilterDescriptor.ToNode(DEFAULT_SERVICE, "APP_A", tomcat.getName(), null); + FilterDescriptor.SelfNode selfNode = new FilterDescriptor.SelfNode(null, null, null, null); FilterDescriptor.ResponseTime responseTime = new FilterDescriptor.ResponseTime(null, null); FilterDescriptor.Option option = mock(FilterDescriptor.Option.class); FilterDescriptor descriptor = new FilterDescriptor(fromNode, toNode, selfNode, responseTime, option); From 880987126c9c7584fd94012360843bd8fc0264a7 Mon Sep 17 00:00:00 2001 From: "jihea.park" Date: Thu, 19 Mar 2026 16:11:05 +0900 Subject: [PATCH 2/2] [#noissue] Integrate serviceKey into ServerMap and FilteredMap flows Co-Authored-By: Claude Sonnet 4.6 --- .../v3/packages/ui/src/atoms/serverMap.ts | 5 +- .../ApplicationCombinedListForCommon.tsx | 8 +- .../ChartsBoard/ChartsBoardHeader.tsx | 19 +++- .../src/components/FilterMap/FilterWizard.tsx | 27 +++-- .../FilterMap/FilteredMapFetcher.tsx | 13 ++- .../components/ServerMap/ServerMapFetcher.tsx | 18 ++- .../ui/src/constants/types/Application.ts | 1 + .../ui/src/constants/types/FilteredMap.ts | 4 + .../useFilteredMapParameters.ts | 4 +- .../serverMap/useFilterWizardOnClickApply.ts | 6 +- .../v3/packages/ui/src/pages/FilteredMap.tsx | 22 +++- .../v3/packages/ui/src/pages/ServerMap.tsx | 4 +- .../ui/src/utils/helper/application.test.ts | 80 ++++++++++++-- .../ui/src/utils/helper/application.ts | 52 ++++++++- .../ui/src/utils/helper/route.test.ts | 34 +++++- .../v3/packages/ui/src/utils/helper/route.ts | 9 +- .../ui/src/utils/helper/serverMap.test.ts | 103 +++++++++--------- .../packages/ui/src/utils/helper/serverMap.ts | 26 ++++- 18 files changed, 319 insertions(+), 116 deletions(-) diff --git a/web-frontend/src/main/v3/packages/ui/src/atoms/serverMap.ts b/web-frontend/src/main/v3/packages/ui/src/atoms/serverMap.ts index ba636ba3cea5..040dab97c00e 100644 --- a/web-frontend/src/main/v3/packages/ui/src/atoms/serverMap.ts +++ b/web-frontend/src/main/v3/packages/ui/src/atoms/serverMap.ts @@ -44,9 +44,10 @@ export const serverMapCurrentTargetDataAtom = atom((get) => { ); } else if (currentTarget?.type === 'node') { return (serverMapData?.applicationMapData?.nodeDataArray as ServerMapNodeDataArray)?.find( - ({ key }) => + ({ key, applicationName, serviceType }) => key === currentTarget?.id || - key === `${currentTarget?.applicationName}^${currentTarget?.serviceType}`, + (applicationName === currentTarget?.applicationName && + serviceType === currentTarget?.serviceType), ); } else if (currentTarget?.type === 'edge') { return (serverMapData?.applicationMapData?.linkDataArray as ServerMapNodeLinkArray)?.find( diff --git a/web-frontend/src/main/v3/packages/ui/src/components/Application/ApplicationCombinedListForCommon.tsx b/web-frontend/src/main/v3/packages/ui/src/components/Application/ApplicationCombinedListForCommon.tsx index 16c2c5b99739..0251590af21d 100644 --- a/web-frontend/src/main/v3/packages/ui/src/components/Application/ApplicationCombinedListForCommon.tsx +++ b/web-frontend/src/main/v3/packages/ui/src/components/Application/ApplicationCombinedListForCommon.tsx @@ -24,6 +24,7 @@ export interface ApplicationCombinedListForCommonProps { addFavoriteMessage?: string; removeFavoriteMessage?: string; onClickApplication?: (application: ApplicationType) => void; + formatApplicationName?: (application: ApplicationType) => string; } export const ApplicationCombinedListForCommon = ({ @@ -36,6 +37,7 @@ export const ApplicationCombinedListForCommon = ({ disabled, selectedApplication, onClickApplication, + formatApplicationName, }: ApplicationCombinedListForCommonProps) => { const [isOpen, setIsOpen] = React.useState(open); const popoverContentRef = React.useRef(null); @@ -248,7 +250,11 @@ export const ApplicationCombinedListForCommon = ({ {selectedApplication ? (
-
{selectedApplication.applicationName}
+
+ {formatApplicationName + ? formatApplicationName(selectedApplication) + : selectedApplication.applicationName} +
handleClickFavorite(e, selectedApplication, { disableToast: true })} diff --git a/web-frontend/src/main/v3/packages/ui/src/components/ChartsBoard/ChartsBoardHeader.tsx b/web-frontend/src/main/v3/packages/ui/src/components/ChartsBoard/ChartsBoardHeader.tsx index 9cc52747f2ea..1e4a759148dc 100644 --- a/web-frontend/src/main/v3/packages/ui/src/components/ChartsBoard/ChartsBoardHeader.tsx +++ b/web-frontend/src/main/v3/packages/ui/src/components/ChartsBoard/ChartsBoardHeader.tsx @@ -1,5 +1,5 @@ import { HiOutlineArrowRight } from 'react-icons/hi'; -import { getApplicationTypeAndName } from '@pinpoint-fe/ui/src/utils'; +import { parseServiceKey, getDisplayApplicationName } from '@pinpoint-fe/ui/src/utils'; import { Edge } from '@pinpoint-fe/server-map'; import { ServerIcon } from '../Application/ServerIcon'; @@ -25,14 +25,23 @@ export const ChartsBoardHeader = ({ currentTarget }: ChartsBoardHeaderProps) => ) : ( (() => { - const sourceApp = getApplicationTypeAndName(currentTarget?.source)!; + const parsedSource = parseServiceKey(currentTarget?.source ?? ''); + const sourceApp = { + applicationName: getDisplayApplicationName(parsedSource.applicationName), + serviceType: parsedSource.serviceType, + }; const targetApp = currentTarget.edges ? { applicationName: `total: ${currentTarget.edges.length}`, - serviceType: getApplicationTypeAndName(currentTarget.edges[0].target)! - .serviceType, + serviceType: parseServiceKey(currentTarget.edges[0].target).serviceType, } - : getApplicationTypeAndName(currentTarget?.target)!; + : (() => { + const parsedTarget = parseServiceKey(currentTarget?.target ?? ''); + return { + applicationName: getDisplayApplicationName(parsedTarget.applicationName), + serviceType: parsedTarget.serviceType, + }; + })(); return (
diff --git a/web-frontend/src/main/v3/packages/ui/src/components/FilterMap/FilterWizard.tsx b/web-frontend/src/main/v3/packages/ui/src/components/FilterMap/FilterWizard.tsx index 3e6ec385f058..0dee2f51549c 100644 --- a/web-frontend/src/main/v3/packages/ui/src/components/FilterMap/FilterWizard.tsx +++ b/web-frontend/src/main/v3/packages/ui/src/components/FilterMap/FilterWizard.tsx @@ -19,7 +19,7 @@ import { PiDotOutlineLight } from 'react-icons/pi'; import { GoDot } from 'react-icons/go'; import { FilterStatus } from './FilterStatus'; import { FilteredMapType as FilteredMap } from '@pinpoint-fe/ui/src/constants'; -import { addCommas, getApplicationTypeAndName } from '@pinpoint-fe/ui/src/utils'; +import { addCommas, parseServiceKey, getDisplayApplicationName } from '@pinpoint-fe/ui/src/utils'; import { Edge, Node } from '@pinpoint-fe/ui/src/utils/helper/serverMap'; import { PopoverArrow } from '@radix-ui/react-popover'; @@ -56,14 +56,16 @@ export const getDefaultFilters = ( ) => { if ('source' in data) { const edgeData = data as Edge; - // edge - const from = getApplicationTypeAndName(edgeData.source); - const to = getApplicationTypeAndName(edgeData.target); + // edge — source/target are keys: serviceName^applicationName^serviceType + const from = parseServiceKey(edgeData.source); + const to = parseServiceKey(edgeData.target); return { - fromApplication: from?.applicationName, - fromServiceType: from?.serviceType, - toApplication: to?.applicationName, - toServiceType: to?.serviceType, + fromApplication: getDisplayApplicationName(from.applicationName), + fromServiceType: from.serviceType, + fromServiceName: from.serviceName, + toApplication: getDisplayApplicationName(to.applicationName), + toServiceType: to.serviceType, + toServiceName: to.serviceName, transactionResult: null, applicationName: '', serviceType: '', @@ -79,17 +81,18 @@ export const getDefaultFilters = ( hint, }; } else if ('type' in data) { - // node + // node — id is key: serviceName^applicationName^serviceType const nodeData = data as Node; - const app = getApplicationTypeAndName(nodeData.id); + const { serviceName, applicationName, serviceType } = parseServiceKey(nodeData.id); return { fromApplication: '', fromServiceType: '', toApplication: '', toServiceType: '', transactionResult: null, - applicationName: app?.applicationName, - serviceType: app?.serviceType, + applicationName: getDisplayApplicationName(applicationName), + serviceType, + serviceName, agentName: '', responseFrom: MIN_RESPONSE_TIME, responseTo: 'max', diff --git a/web-frontend/src/main/v3/packages/ui/src/components/FilterMap/FilteredMapFetcher.tsx b/web-frontend/src/main/v3/packages/ui/src/components/FilterMap/FilteredMapFetcher.tsx index 2916e21abe4c..d9e9ab3c04b8 100644 --- a/web-frontend/src/main/v3/packages/ui/src/components/FilterMap/FilteredMapFetcher.tsx +++ b/web-frontend/src/main/v3/packages/ui/src/components/FilterMap/FilteredMapFetcher.tsx @@ -12,6 +12,8 @@ import { mergeFilteredMapLinkData, getServerImagePath, getBaseNodeId, + parseServiceKey, + getDisplayApplicationName, } from '@pinpoint-fe/ui/src/utils'; import { MergedNode, MergedEdge, Node, Edge } from '@pinpoint-fe/server-map'; import { useFilteredMapParameters, useGetFilteredServerMapData } from '@pinpoint-fe/ui/src/hooks'; @@ -155,10 +157,13 @@ export const FilteredMapFetcher = ({ }; const handleMergeStateChange = () => { - const [applicationName, serviceType] = getBaseNodeId({ - application, - applicationMapData: data?.applicationMapData, - }).split('^'); + const { applicationName: parsedAppName, serviceType } = parseServiceKey( + getBaseNodeId({ + application, + applicationMapData: data?.applicationMapData, + }), + ); + const applicationName = getDisplayApplicationName(parsedAppName); setServerMapCurrentTarget({ applicationName, diff --git a/web-frontend/src/main/v3/packages/ui/src/components/ServerMap/ServerMapFetcher.tsx b/web-frontend/src/main/v3/packages/ui/src/components/ServerMap/ServerMapFetcher.tsx index 90f22d95152a..53c3c2475e8a 100644 --- a/web-frontend/src/main/v3/packages/ui/src/components/ServerMap/ServerMapFetcher.tsx +++ b/web-frontend/src/main/v3/packages/ui/src/components/ServerMap/ServerMapFetcher.tsx @@ -12,7 +12,12 @@ import { useServerMapSearchParameters, } from '@pinpoint-fe/ui/src/hooks'; import { useTranslation } from 'react-i18next'; -import { getBaseNodeId, getServerImagePath } from '@pinpoint-fe/ui/src/utils'; +import { + getBaseNodeId, + getServerImagePath, + parseServiceKey, + getDisplayApplicationName, +} from '@pinpoint-fe/ui/src/utils'; import { ServerMapCore, ServerMapCoreProps } from './ServerMapCore'; export interface ServerMapFetcherProps extends Pick< @@ -71,10 +76,13 @@ export const ServerMapFetcher = ({ shouldPoll, ...props }: ServerMapFetcherProps const handleMergeStateChange = () => { if (data) { - const [applicationName, serviceType] = getBaseNodeId({ - application, - applicationMapData: data?.applicationMapData, - }).split('^'); + const { applicationName: parsedAppName, serviceType } = parseServiceKey( + getBaseNodeId({ + application, + applicationMapData: data?.applicationMapData, + }), + ); + const applicationName = getDisplayApplicationName(parsedAppName); setServerMapCurrentTarget({ applicationName, diff --git a/web-frontend/src/main/v3/packages/ui/src/constants/types/Application.ts b/web-frontend/src/main/v3/packages/ui/src/constants/types/Application.ts index 31e36798da04..70f58feafe7a 100644 --- a/web-frontend/src/main/v3/packages/ui/src/constants/types/Application.ts +++ b/web-frontend/src/main/v3/packages/ui/src/constants/types/Application.ts @@ -2,4 +2,5 @@ export type ApplicationType = { code?: number; serviceType?: string; applicationName?: string; + serviceName?: string; }; diff --git a/web-frontend/src/main/v3/packages/ui/src/constants/types/FilteredMap.ts b/web-frontend/src/main/v3/packages/ui/src/constants/types/FilteredMap.ts index c9d541ea2dd4..a771d38582b5 100644 --- a/web-frontend/src/main/v3/packages/ui/src/constants/types/FilteredMap.ts +++ b/web-frontend/src/main/v3/packages/ui/src/constants/types/FilteredMap.ts @@ -10,14 +10,17 @@ export namespace FilteredMapType { export interface FilterState { fromApplication?: SearchParameters['fa']; // from applicationName fromServiceType?: SearchParameters['fst']; // from serviceType + fromServiceName?: string; // from serviceName fromAgents?: string[]; toApplication?: SearchParameters['ta']; // to applicationName toServiceType?: SearchParameters['tst']; // to serviceType + toServiceName?: string; // to serviceName toAgents?: string[]; transactionResult: SearchParameters['ie']; // include exception (X) // if single node applicationName?: SearchParameters['a']; // applicationName serviceType?: SearchParameters['st']; // serviceType + serviceName?: string; // serviceName agentName?: SearchParameters['an']; // agentName agents?: string[]; // settings by user @@ -207,6 +210,7 @@ export namespace FilteredMapType { export interface NodeData { key: string; + serviceName?: string; applicationName: string; serviceType: string; serviceTypeCode: number; diff --git a/web-frontend/src/main/v3/packages/ui/src/hooks/searchParameters/useFilteredMapParameters.ts b/web-frontend/src/main/v3/packages/ui/src/hooks/searchParameters/useFilteredMapParameters.ts index b0bc12752b15..3aa4ff02d272 100644 --- a/web-frontend/src/main/v3/packages/ui/src/hooks/searchParameters/useFilteredMapParameters.ts +++ b/web-frontend/src/main/v3/packages/ui/src/hooks/searchParameters/useFilteredMapParameters.ts @@ -1,6 +1,6 @@ import { useLocation } from 'react-router-dom'; import { - getApplicationTypeAndName, + getApplicationTypeAndNameFromServicePath, parseFilterStateFromQueryString, } from '@pinpoint-fe/ui/src/utils'; import { getSearchParameters, getDateRange } from './utils'; @@ -8,7 +8,7 @@ import { getSearchParameters, getDateRange } from './utils'; export const useFilteredMapParameters = () => { const { search, pathname } = useLocation(); const searchParameters = getSearchParameters(search); - const application = getApplicationTypeAndName(pathname); + const application = getApplicationTypeAndNameFromServicePath(pathname); const dateRange = getDateRange(search, false); const parsedFilters = parseFilterStateFromQueryString(searchParameters.filter); const parsedHint = (() => { diff --git a/web-frontend/src/main/v3/packages/ui/src/hooks/serverMap/useFilterWizardOnClickApply.ts b/web-frontend/src/main/v3/packages/ui/src/hooks/serverMap/useFilterWizardOnClickApply.ts index 81b6be7fe560..a5d67bf98715 100644 --- a/web-frontend/src/main/v3/packages/ui/src/hooks/serverMap/useFilterWizardOnClickApply.ts +++ b/web-frontend/src/main/v3/packages/ui/src/hooks/serverMap/useFilterWizardOnClickApply.ts @@ -29,8 +29,10 @@ export function useFilterWizardOnClickApply< if (!filterState?.applicationName) { const link = (serverMapData?.applicationMapData?.linkDataArray as T[])?.find( (l) => - l?.key === - `${filterState?.fromApplication}^${filterState?.fromServiceType}~${filterState?.toApplication}^${filterState?.toServiceType}`, + l?.sourceInfo?.applicationName === filterState?.fromApplication && + l?.sourceInfo?.serviceType === filterState?.fromServiceType && + l?.targetInfo?.applicationName === filterState?.toApplication && + l?.targetInfo?.serviceType === filterState?.toServiceType, ); if (link) { soureIsWas = link?.sourceInfo?.nodeCategory === GetServerMap.NodeCategory.SERVER; diff --git a/web-frontend/src/main/v3/packages/ui/src/pages/FilteredMap.tsx b/web-frontend/src/main/v3/packages/ui/src/pages/FilteredMap.tsx index 1ab08deaef84..f653c9ff8a1a 100644 --- a/web-frontend/src/main/v3/packages/ui/src/pages/FilteredMap.tsx +++ b/web-frontend/src/main/v3/packages/ui/src/pages/FilteredMap.tsx @@ -5,7 +5,6 @@ import { convertParamsToQueryString, getServerImagePath, getFilteredMapPath, - getApplicationKey, getServerMapPath, } from '@pinpoint-fe/ui/src/utils'; import { useFilteredMapParameters } from '@pinpoint-fe/ui/src/hooks'; @@ -87,7 +86,11 @@ export const FilteredMapPage = ({ return { ...prevFilter, agents: (serverMapData?.applicationMapData.nodeDataArray as FilteredMap.NodeData[]) - ?.find((n) => n.key === `${prevFilter.applicationName}^${prevFilter.serviceType}`) + ?.find( + (n) => + n.applicationName === prevFilter.applicationName && + n.serviceType === prevFilter.serviceType, + ) ?.agents?.map((agent) => agent.id), }; } else if ( @@ -100,8 +103,10 @@ export const FilteredMapPage = ({ serverMapData?.applicationMapData.linkDataArray as FilteredMap.LinkData[] )?.find( (n) => - n.key === - `${prevFilter.fromApplication}^${prevFilter.fromServiceType}~${prevFilter.toApplication}^${prevFilter.toServiceType}`, + n.sourceInfo.applicationName === prevFilter.fromApplication && + n.sourceInfo.serviceType === prevFilter.fromServiceType && + n.targetInfo.applicationName === prevFilter.toApplication && + n.targetInfo.serviceType === prevFilter.toServiceType, ); return { @@ -137,7 +142,8 @@ export const FilteredMapPage = ({ serverMapData.applicationMapData.nodeDataArray as GetServerMap.NodeData[] ).find((node) => { return ( - getApplicationKey(application!) === node.key || + (node.applicationName === application?.applicationName && + node.serviceType === application?.serviceType) || (node.applicationName === application?.applicationName && node.serviceType === 'UNAUTHORIZED') ); @@ -213,7 +219,11 @@ export const FilteredMapPage = ({
} > - + `${app.serviceName ?? 'DEFAULT'}^${app.applicationName}`} + />
{application && ( { return ( - getApplicationKey(application!) === node.key || + (node.applicationName === application?.applicationName && + node.serviceType === application?.serviceType) || (node.applicationName === application?.applicationName && node.serviceType === 'UNAUTHORIZED') ); diff --git a/web-frontend/src/main/v3/packages/ui/src/utils/helper/application.test.ts b/web-frontend/src/main/v3/packages/ui/src/utils/helper/application.test.ts index d2066bb025b3..33295f1ac0c9 100644 --- a/web-frontend/src/main/v3/packages/ui/src/utils/helper/application.test.ts +++ b/web-frontend/src/main/v3/packages/ui/src/utils/helper/application.test.ts @@ -1,4 +1,9 @@ -import { getApplicationTypeAndName, getApplicationKey } from './application'; +import { + getApplicationTypeAndName, + getApplicationKey, + parseServiceKey, + getDisplayApplicationName, +} from './application'; import { ApplicationType } from '@pinpoint-fe/ui/src/constants'; describe('Test application helper utils', () => { @@ -59,35 +64,86 @@ describe('Test application helper utils', () => { }); describe('Test "getApplicationKey"', () => { - test('Return application key from application object', () => { + test('Return key format (serviceName^applicationName^serviceType)', () => { const application: ApplicationType = { applicationName: 'appName', serviceType: 'serviceType', + serviceName: 'myService', }; const result = getApplicationKey(application); - expect(result).toBe('appName^serviceType'); + expect(result).toBe('myService^appName^serviceType'); }); - test('Return application key with empty strings when application is undefined', () => { - const result = getApplicationKey(undefined); - expect(result).toBe('undefined^undefined'); + test('Return key with empty serviceName when serviceName is not provided', () => { + const application: ApplicationType = { + applicationName: 'appName', + serviceType: 'serviceType', + }; + const result = getApplicationKey(application); + expect(result).toBe('^appName^serviceType'); }); - test('Return application key with partial data', () => { - const application: Partial = { - applicationName: 'appName', + test('Escape ^ in applicationName', () => { + const application: ApplicationType = { + applicationName: 'app^name', + serviceType: 'serviceType', + serviceName: 'myService', }; - const result = getApplicationKey(application as ApplicationType); - expect(result).toBe('appName^undefined'); + const result = getApplicationKey(application); + expect(result).toBe('myService^app\\^name^serviceType'); }); test('Handle application with special characters', () => { const application: ApplicationType = { applicationName: 'app-name', serviceType: 'service_type', + serviceName: 'myService', }; const result = getApplicationKey(application); - expect(result).toBe('app-name^service_type'); + expect(result).toBe('myService^app-name^service_type'); + }); + }); + + describe('Test "parseServiceKey"', () => { + test('Parse basic serviceKey', () => { + const result = parseServiceKey('myService^myApp^myType'); + expect(result).toEqual({ + serviceName: 'myService', + applicationName: 'myApp', + serviceType: 'myType', + }); + }); + + test('Parse serviceKey with escaped ^ in applicationName', () => { + const result = parseServiceKey('myService^myAppli\\^cation^myType'); + expect(result).toEqual({ + serviceName: 'myService', + applicationName: 'myAppli\\^cation', + serviceType: 'myType', + }); + }); + + test('Parse serviceKey with empty serviceName', () => { + const result = parseServiceKey('^myApp^myType'); + expect(result).toEqual({ + serviceName: '', + applicationName: 'myApp', + serviceType: 'myType', + }); + }); + }); + + describe('Test "getDisplayApplicationName"', () => { + test('Replace escaped ^ with ^ for display', () => { + expect(getDisplayApplicationName('myAppli\\^cation')).toBe('myAppli^cation'); + }); + + test('Leave applicationName unchanged when no escaped ^', () => { + expect(getDisplayApplicationName('myApp')).toBe('myApp'); + }); + + test('Replace multiple escaped ^ ', () => { + expect(getDisplayApplicationName('a\\^b\\^c')).toBe('a^b^c'); }); }); }); diff --git a/web-frontend/src/main/v3/packages/ui/src/utils/helper/application.ts b/web-frontend/src/main/v3/packages/ui/src/utils/helper/application.ts index b2155965658e..6aa9ffc93b5a 100644 --- a/web-frontend/src/main/v3/packages/ui/src/utils/helper/application.ts +++ b/web-frontend/src/main/v3/packages/ui/src/utils/helper/application.ts @@ -12,6 +12,56 @@ export const getApplicationTypeAndName = (path = '') => { return null; }; +/** + * Parses the FilteredMap URL path segment with the format: + * serviceName@applicationName@serviceType + * + * Falls back to the 2-part format (applicationName@serviceType) for backward compatibility. + */ +export const getApplicationTypeAndNameFromServicePath = (path = '') => { + const lastSegment = path.match(/\/([^/]+)$/)?.[1]; + if (!lastSegment) return null; + + const parts = lastSegment.split('@'); + if (parts.length >= 3) { + const serviceName = parts[0]; + const serviceType = parts[parts.length - 1]; + const applicationName = parts.slice(1, -1).join('@'); + return { serviceName, applicationName, serviceType }; + } else if (parts.length === 2) { + return { applicationName: parts[0], serviceType: parts[1] }; + } + + return null; +}; + +/** + * Parses a serviceKey of the format `serviceName^applicationName^serviceType`. + * The applicationName may contain escaped carets (`\^`), which are treated as literal `^` + * rather than delimiters. + * + * Example: `myService^myAppli\^cation^myType` + * → { serviceName: 'myService', applicationName: 'myAppli\^cation', serviceType: 'myType' } + */ +export const parseServiceKey = (serviceKey: string) => { + // Split on ^ that is NOT preceded by backslash + const parts = serviceKey.split(/(? { + return applicationName.replace(/\\\^/g, '^'); +}; + export const getApplicationKey = (application?: ApplicationType) => { - return `${application?.applicationName}^${application?.serviceType}`; + const escapedApplicationName = (application?.applicationName ?? '').replace(/\^/g, '\\^'); + return `${application?.serviceName ?? ''}^${escapedApplicationName}^${application?.serviceType}`; }; diff --git a/web-frontend/src/main/v3/packages/ui/src/utils/helper/route.test.ts b/web-frontend/src/main/v3/packages/ui/src/utils/helper/route.test.ts index 2c48e39abd2d..644ec1427b88 100644 --- a/web-frontend/src/main/v3/packages/ui/src/utils/helper/route.test.ts +++ b/web-frontend/src/main/v3/packages/ui/src/utils/helper/route.test.ts @@ -122,7 +122,31 @@ describe('Test route helper utils', () => { }); describe('Test "getFilteredMapPath"', () => { - test('Return FilterMap path: using toApplication and toServiceType when applicationName is not exist and sourceIsWas is false', () => { + test('Return FilterMap path with serviceName prefix when applicationName is present', () => { + const filterState = { + fromApplication: '', + fromServiceType: '', + toApplication: '', + toServiceType: '', + transactionResult: null, + applicationName: 'applicationName', + serviceType: 'serviceType', + serviceName: 'myService', + agentName: '', + responseFrom: 0, + responseTo: 'max', + url: '', + fromAgentName: '', + toAgentName: '', + agents: ['agent'], + }; + const sourceIsWas = false; + + const result = getFilteredMapPath(filterState, sourceIsWas); + expect(result).toEqual('/filteredMap/myService@applicationName@serviceType'); + }); + + test('Use DEFAULT as serviceName when serviceName is not provided and applicationName is present', () => { const filterState = { fromApplication: '', fromServiceType: '', @@ -142,13 +166,14 @@ describe('Test route helper utils', () => { const sourceIsWas = false; const result = getFilteredMapPath(filterState, sourceIsWas); - expect(result).toEqual('/filteredMap/applicationName@serviceType'); + expect(result).toEqual('/filteredMap/DEFAULT@applicationName@serviceType'); }); test('Return FilterMap path: using fromApplication and fromServiceType when applicationName is not exist and sourceIsWas is true', () => { const filterState = { fromApplication: 'fromApplication', fromServiceType: 'fromServiceType', + fromServiceName: 'fromService', toApplication: 'toApplication', toServiceType: 'toServiceType', transactionResult: null, @@ -165,7 +190,7 @@ describe('Test route helper utils', () => { const sourceIsWas = true; const result = getFilteredMapPath(filterState, sourceIsWas); - expect(result).toEqual('/filteredMap/fromApplication@fromServiceType'); + expect(result).toEqual('/filteredMap/fromService@fromApplication@fromServiceType'); }); test('Return FilterMap path: using toApplication and toServiceType when applicationName is not exist and sourceIsWas is false', () => { @@ -174,6 +199,7 @@ describe('Test route helper utils', () => { fromServiceType: 'fromServiceType', toApplication: 'toApplication', toServiceType: 'toServiceType', + toServiceName: 'toService', transactionResult: null, applicationName: '', serviceType: '', @@ -188,7 +214,7 @@ describe('Test route helper utils', () => { const sourceIsWas = false; const result = getFilteredMapPath(filterState, sourceIsWas); - expect(result).toEqual('/filteredMap/toApplication@toServiceType'); + expect(result).toEqual('/filteredMap/toService@toApplication@toServiceType'); }); }); }); diff --git a/web-frontend/src/main/v3/packages/ui/src/utils/helper/route.ts b/web-frontend/src/main/v3/packages/ui/src/utils/helper/route.ts index 532cf784e9f5..3ef33d92a98b 100644 --- a/web-frontend/src/main/v3/packages/ui/src/utils/helper/route.ts +++ b/web-frontend/src/main/v3/packages/ui/src/utils/helper/route.ts @@ -83,12 +83,15 @@ export const getHeatmapFullScreenRealtimePath = getApplicationPath( export const getFilteredMapPath = (filterState: FilteredMap.FilterState, soureIsWas?: boolean) => { let applicationNameAndType = ''; if (filterState?.applicationName) { - applicationNameAndType = `${filterState?.applicationName}@${filterState.serviceType}`; + const serviceName = filterState?.serviceName ?? 'DEFAULT'; + applicationNameAndType = `${serviceName}@${filterState?.applicationName}@${filterState.serviceType}`; } else { if (soureIsWas) { - applicationNameAndType = `${filterState?.fromApplication}@${filterState.fromServiceType}`; + const serviceName = filterState?.fromServiceName ?? 'DEFAULT'; + applicationNameAndType = `${serviceName}@${filterState?.fromApplication}@${filterState.fromServiceType}`; } else { - applicationNameAndType = `${filterState?.toApplication}@${filterState.toServiceType}`; + const serviceName = filterState?.toServiceName ?? 'DEFAULT'; + applicationNameAndType = `${serviceName}@${filterState?.toApplication}@${filterState.toServiceType}`; } } return `${APP_PATH.FILTERED_MAP}/${applicationNameAndType}`; diff --git a/web-frontend/src/main/v3/packages/ui/src/utils/helper/serverMap.test.ts b/web-frontend/src/main/v3/packages/ui/src/utils/helper/serverMap.test.ts index eaaaed6e1a9c..d954ebecded0 100644 --- a/web-frontend/src/main/v3/packages/ui/src/utils/helper/serverMap.test.ts +++ b/web-frontend/src/main/v3/packages/ui/src/utils/helper/serverMap.test.ts @@ -7,81 +7,81 @@ import { describe('Test serverMap helper utils', () => { describe('Test "getBaseNodeId"', () => { - test('Return base node ID when node list is empty', () => { + test('Return fallback key (serviceName^applicationName^serviceType) when node list is empty', () => { const application: ApplicationType = { applicationName: 'test-app', serviceType: 'TOMCAT', + serviceName: 'myService', }; const applicationMapData: GetServerMap.ApplicationMapData = { - range: { - from: 0, - to: 0, - fromDateTime: '', - toDateTime: '', - }, + range: { from: 0, to: 0, fromDateTime: '', toDateTime: '' }, timestamp: [], nodeDataArray: [], linkDataArray: [], }; const result = getBaseNodeId({ application, applicationMapData }); - expect(result).toBe('test-app^TOMCAT'); + expect(result).toBe('myService^test-app^TOMCAT'); }); - test('Return base node ID when node exists in node list', () => { + test('Return node key when matching node exists', () => { const application: ApplicationType = { applicationName: 'test-app', serviceType: 'TOMCAT', }; const applicationMapData: GetServerMap.ApplicationMapData = { - range: { - from: 0, - to: 0, - fromDateTime: '', - toDateTime: '', - }, + range: { from: 0, to: 0, fromDateTime: '', toDateTime: '' }, timestamp: [], nodeDataArray: [ - { key: 'test-app^TOMCAT' } as GetServerMap.NodeData, - { key: 'other-app^JETTY' } as GetServerMap.NodeData, + { + key: 'myService^test-app^TOMCAT', + applicationName: 'test-app', + serviceType: 'TOMCAT', + } as GetServerMap.NodeData, + { + key: 'myService^other-app^JETTY', + applicationName: 'other-app', + serviceType: 'JETTY', + } as GetServerMap.NodeData, ], linkDataArray: [], }; const result = getBaseNodeId({ application, applicationMapData }); - expect(result).toBe('test-app^TOMCAT'); + expect(result).toBe('myService^test-app^TOMCAT'); }); - test('Return UNAUTHORIZED node ID when node does not exist in node list', () => { + test('Return UNAUTHORIZED node key when node does not match but UNAUTHORIZED node exists', () => { const application: ApplicationType = { applicationName: 'test-app', serviceType: 'TOMCAT', }; const applicationMapData: GetServerMap.ApplicationMapData = { - range: { - from: 0, - to: 0, - fromDateTime: '', - toDateTime: '', - }, + range: { from: 0, to: 0, fromDateTime: '', toDateTime: '' }, timestamp: [], - nodeDataArray: [{ key: 'other-app^JETTY' } as GetServerMap.NodeData], + nodeDataArray: [ + { + key: 'myService^test-app^UNAUTHORIZED', + applicationName: 'test-app', + serviceType: 'UNAUTHORIZED', + } as GetServerMap.NodeData, + { + key: 'myService^other-app^JETTY', + applicationName: 'other-app', + serviceType: 'JETTY', + } as GetServerMap.NodeData, + ], linkDataArray: [], }; const result = getBaseNodeId({ application, applicationMapData }); - expect(result).toBe('test-app^UNAUTHORIZED'); + expect(result).toBe('myService^test-app^UNAUTHORIZED'); }); test('Return empty string when application is null', () => { const application = null; const applicationMapData: GetServerMap.ApplicationMapData = { - range: { - from: 0, - to: 0, - fromDateTime: '', - toDateTime: '', - }, + range: { from: 0, to: 0, fromDateTime: '', toDateTime: '' }, timestamp: [], nodeDataArray: [], linkDataArray: [], @@ -107,42 +107,43 @@ describe('Test serverMap helper utils', () => { serviceType: 'TOMCAT', }; const applicationMapData: FilteredMap.ApplicationMapData = { - range: { - from: 0, - to: 0, - fromDateTime: '', - toDateTime: '', - }, + range: { from: 0, to: 0, fromDateTime: '', toDateTime: '' }, timestamp: [], - nodeDataArray: [{ key: 'test-app^TOMCAT' } as FilteredMap.NodeData], + nodeDataArray: [ + { + key: 'myService^test-app^TOMCAT', + applicationName: 'test-app', + serviceType: 'TOMCAT', + } as FilteredMap.NodeData, + ], linkDataArray: [], }; const result = getBaseNodeId({ application, applicationMapData }); - expect(result).toBe('test-app^TOMCAT'); + expect(result).toBe('myService^test-app^TOMCAT'); }); - test('Handle case-insensitive matching for UNAUTHORIZED replacement', () => { + test('Return fallback key when no matching node and no UNAUTHORIZED node', () => { const application: ApplicationType = { applicationName: 'test-app', - serviceType: 'tomcat', // lowercase + serviceType: 'TOMCAT', + serviceName: 'myService', }; const applicationMapData: GetServerMap.ApplicationMapData = { - range: { - from: 0, - to: 0, - fromDateTime: '', - toDateTime: '', - }, + range: { from: 0, to: 0, fromDateTime: '', toDateTime: '' }, timestamp: [], nodeDataArray: [ - { key: 'test-app^TOMCAT' } as GetServerMap.NodeData, // uppercase + { + key: 'myService^other-app^JETTY', + applicationName: 'other-app', + serviceType: 'JETTY', + } as GetServerMap.NodeData, ], linkDataArray: [], }; const result = getBaseNodeId({ application, applicationMapData }); - expect(result).toBe('test-app^UNAUTHORIZED'); + expect(result).toBe('myService^test-app^TOMCAT'); }); }); }); diff --git a/web-frontend/src/main/v3/packages/ui/src/utils/helper/serverMap.ts b/web-frontend/src/main/v3/packages/ui/src/utils/helper/serverMap.ts index 2591e9587f77..a58ea387dcad 100644 --- a/web-frontend/src/main/v3/packages/ui/src/utils/helper/serverMap.ts +++ b/web-frontend/src/main/v3/packages/ui/src/utils/helper/serverMap.ts @@ -4,6 +4,7 @@ import { GetServerMap, } from '@pinpoint-fe/ui/src/constants'; import { Edge as ServerMapEdge, Node as ServerMapNode } from '@pinpoint-fe/server-map'; +import { getApplicationKey } from './application'; export type Edge = ServerMapEdge; export type Node = ServerMapNode; @@ -17,11 +18,28 @@ export const getBaseNodeId = ({ }) => { if (application && applicationMapData) { const nodeList = applicationMapData.nodeDataArray; - const baseNodeId = `${application?.applicationName}^${application?.serviceType}`; - return nodeList.length === 0 || nodeList.some(({ key }: { key: string }) => key === baseNodeId) - ? baseNodeId - : baseNodeId.replace(/(.*)\^(.*)/i, '$1^UNAUTHORIZED'); + if (nodeList.length === 0) { + return getApplicationKey(application); + } + + const matchedNode = nodeList.find( + (node) => + node.applicationName === application.applicationName && + node.serviceType === application.serviceType, + ); + + if (matchedNode) { + return matchedNode.key; + } + + const unauthorizedNode = nodeList.find( + (node) => + node.applicationName === application.applicationName && + node.serviceType === 'UNAUTHORIZED', + ); + + return unauthorizedNode?.key ?? getApplicationKey(application); } return ''; };