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 e4088676608e..4685fe939faf 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 @@ -33,9 +33,11 @@ * 3UNKNOWN_GROUP * 5TEST * 7COLLECTOR + * 10SERVICE * 100ASYNC * 500SDK * 510SDK_ASYNC + * * * * @@ -373,6 +375,8 @@ public interface ServiceType { ServiceType COLLECTOR = of(7, "COLLECTOR"); + ServiceType SERVICE = of(10, "SERVICE"); + ServiceType ASYNC = of(100, "ASYNC"); // Java applications, WAS diff --git a/web/src/main/java/com/navercorp/pinpoint/web/applicationmap/config/MapControllerConfiguration.java b/web/src/main/java/com/navercorp/pinpoint/web/applicationmap/config/MapControllerConfiguration.java index 0e85cd5876ad..7b9f4e61c675 100644 --- a/web/src/main/java/com/navercorp/pinpoint/web/applicationmap/config/MapControllerConfiguration.java +++ b/web/src/main/java/com/navercorp/pinpoint/web/applicationmap/config/MapControllerConfiguration.java @@ -29,6 +29,8 @@ import com.navercorp.pinpoint.web.applicationmap.service.TraceIndexService; import com.navercorp.pinpoint.web.applicationmap.servicemap.ServiceMappingProperties; import com.navercorp.pinpoint.web.applicationmap.servicemap.ServiceResolver; +import com.navercorp.pinpoint.web.applicationmap.virtualapplication.VirtualApplicationProperties; +import com.navercorp.pinpoint.web.applicationmap.virtualapplication.VirtualApplicationResolver; import com.navercorp.pinpoint.web.component.ApplicationFactory; import com.navercorp.pinpoint.web.config.ConfigProperties; import com.navercorp.pinpoint.web.filter.FilterBuilder; @@ -43,7 +45,7 @@ import java.util.List; @Configuration -@EnableConfigurationProperties(ServiceMappingProperties.class) +@EnableConfigurationProperties({ServiceMappingProperties.class, VirtualApplicationProperties.class}) public class MapControllerConfiguration { @Bean @@ -54,13 +56,23 @@ public ServiceResolver serviceResolver(ServiceMappingProperties serviceMappingPr return new ServiceResolver(serviceMappingProperties.getMockMappings()); } + @Bean + public VirtualApplicationResolver virtualApplicationResolver(VirtualApplicationProperties properties, + ApplicationValidator applicationValidator) { + if (!properties.isMockEnabled()) { + return VirtualApplicationResolver.emptyResolver(); + } + return VirtualApplicationResolver.of(properties.getMappings(), applicationValidator); + } + @Bean public MapController mapController(MapProperties mapProperties, MapService mapService, ApplicationValidator applicationValidator, + VirtualApplicationResolver virtualApplicationResolver, ConfigProperties configProperties) { Duration maxPeriod = Duration.ofDays(configProperties.getServerMapPeriodMax()); - return new MapController(mapProperties, mapService, applicationValidator, maxPeriod); + return new MapController(mapProperties, mapService, applicationValidator, virtualApplicationResolver, maxPeriod); } @Bean diff --git a/web/src/main/java/com/navercorp/pinpoint/web/applicationmap/controller/MapController.java b/web/src/main/java/com/navercorp/pinpoint/web/applicationmap/controller/MapController.java index 02dcf12ec483..4029dbcc09b9 100644 --- a/web/src/main/java/com/navercorp/pinpoint/web/applicationmap/controller/MapController.java +++ b/web/src/main/java/com/navercorp/pinpoint/web/applicationmap/controller/MapController.java @@ -35,6 +35,7 @@ import com.navercorp.pinpoint.web.applicationmap.servicemap.ServiceMapViewBuilder; import com.navercorp.pinpoint.web.applicationmap.view.LinkRender; import com.navercorp.pinpoint.web.applicationmap.view.NodeRender; +import com.navercorp.pinpoint.web.applicationmap.virtualapplication.VirtualApplicationResolver; import com.navercorp.pinpoint.web.util.ApplicationValidator; import com.navercorp.pinpoint.web.vo.Application; import com.navercorp.pinpoint.web.vo.SearchOption; @@ -71,6 +72,7 @@ public class MapController { private final MapService mapService; private final RangeValidator rangeValidator; private final ApplicationValidator applicationValidator; + private final VirtualApplicationResolver virtualApplicationResolver; private static final int DEFAULT_MAX_SEARCH_DEPTH = 4; @@ -78,10 +80,12 @@ public MapController( MapProperties mapProperties, MapService mapService, ApplicationValidator applicationValidator, + VirtualApplicationResolver virtualApplicationResolver, Duration limitDay) { this.mapProperties = Objects.requireNonNull(mapProperties, "mapProperties"); this.mapService = Objects.requireNonNull(mapService, "mapService"); this.applicationValidator = Objects.requireNonNull(applicationValidator, "applicationValidator"); + this.virtualApplicationResolver = Objects.requireNonNull(virtualApplicationResolver, "virtualApplicationResolver"); this.rangeValidator = new ForwardRangeValidator(Objects.requireNonNull(limitDay, "limitDay")); } @@ -113,9 +117,10 @@ public MapWrap getServerMapData( .build(searchForm.getCallerRange(), searchForm.getCalleeRange(), searchForm.isBidirectional(), searchForm.isWasOnly()); final Application application = getApplication(appForm); + final List applications = virtualApplicationResolver.resolve(application); final MapServiceOption option = new MapServiceOption - .Builder(application, timeWindow, searchOption) + .Builder(applications, timeWindow, searchOption) .setUseStatisticsAgentState(useStatisticsAgentState) .build(); @@ -154,9 +159,10 @@ public MapWrap getServiceMapData( final Set keepServices = getKeepServices(keepServiceNames); final Application application = getApplication(appForm); + final List applications = virtualApplicationResolver.resolve(application); final MapServiceOption option = new MapServiceOption - .Builder(application, timeWindow, searchOption) + .Builder(applications, timeWindow, searchOption) .setUseStatisticsAgentState(useStatisticsAgentState) .build(); diff --git a/web/src/main/java/com/navercorp/pinpoint/web/applicationmap/virtualapplication/VirtualApplicationProperties.java b/web/src/main/java/com/navercorp/pinpoint/web/applicationmap/virtualapplication/VirtualApplicationProperties.java new file mode 100644 index 000000000000..d8020d10e5dd --- /dev/null +++ b/web/src/main/java/com/navercorp/pinpoint/web/applicationmap/virtualapplication/VirtualApplicationProperties.java @@ -0,0 +1,87 @@ +/* + * Copyright 2026 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.applicationmap.virtualapplication; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.util.ArrayList; +import java.util.List; + +@ConfigurationProperties(prefix = "web.virtual-service") +public class VirtualApplicationProperties { + + private boolean mockEnabled = false; + private List mappings = new ArrayList<>(); + + public boolean isMockEnabled() { + return mockEnabled; + } + + public void setMockEnabled(boolean mockEnabled) { + this.mockEnabled = mockEnabled; + } + + public List getMappings() { + return mappings; + } + + public void setMappings(List mappings) { + this.mappings = mappings; + } + + public static class VirtualApplicationRule { + private String virtualServiceName; + private List members = new ArrayList<>(); + + public String getVirtualServiceName() { + return virtualServiceName; + } + + public void setVirtualServiceName(String virtualServiceName) { + this.virtualServiceName = virtualServiceName; + } + + public List getMembers() { + return members; + } + + public void setMembers(List members) { + this.members = members; + } + } + + public static class ApplicationRef { + private String name; + private String serviceType; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getServiceType() { + return serviceType; + } + + public void setServiceType(String serviceType) { + this.serviceType = serviceType; + } + } +} \ No newline at end of file diff --git a/web/src/main/java/com/navercorp/pinpoint/web/applicationmap/virtualapplication/VirtualApplicationResolver.java b/web/src/main/java/com/navercorp/pinpoint/web/applicationmap/virtualapplication/VirtualApplicationResolver.java new file mode 100644 index 000000000000..be672e1960c5 --- /dev/null +++ b/web/src/main/java/com/navercorp/pinpoint/web/applicationmap/virtualapplication/VirtualApplicationResolver.java @@ -0,0 +1,82 @@ +/* + * Copyright 2026 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.applicationmap.virtualapplication; + +import com.navercorp.pinpoint.common.trace.ServiceType; +import com.navercorp.pinpoint.common.util.CollectionUtils; +import com.navercorp.pinpoint.web.applicationmap.virtualapplication.VirtualApplicationProperties.ApplicationRef; +import com.navercorp.pinpoint.web.applicationmap.virtualapplication.VirtualApplicationProperties.VirtualApplicationRule; +import com.navercorp.pinpoint.web.util.ApplicationValidator; +import com.navercorp.pinpoint.web.vo.Application; +import org.springframework.util.StringUtils; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +public class VirtualApplicationResolver { + + private static final String SERVICE_TYPE_NAME = ServiceType.SERVICE.getName(); + private static final VirtualApplicationResolver EMPTY = new VirtualApplicationResolver(Map.of()); + + private final Map> table; + + VirtualApplicationResolver(Map> table) { + this.table = Objects.requireNonNull(table, "table"); + } + + public static VirtualApplicationResolver emptyResolver() { + return EMPTY; + } + + public static VirtualApplicationResolver of(List rules, ApplicationValidator validator) { + Objects.requireNonNull(rules, "rules"); + Objects.requireNonNull(validator, "validator"); + + Map> table = new LinkedHashMap<>(); + for (VirtualApplicationRule rule : rules) { + String virtualServiceName = rule.getVirtualServiceName(); + if (!StringUtils.hasLength(virtualServiceName)) { + throw new IllegalArgumentException("virtualServiceName must not be empty"); + } + List members = rule.getMembers(); + if (CollectionUtils.isEmpty(members)) { + throw new IllegalArgumentException("members must not be empty for virtualServiceName=" + virtualServiceName); + } + List applications = members.stream() + .map(m -> validator.newApplication(m.getName(), m.getServiceType())) + .toList(); + if (table.put(virtualServiceName, applications) != null) { + throw new IllegalArgumentException("duplicate virtualServiceName=" + virtualServiceName); + } + } + return new VirtualApplicationResolver(Map.copyOf(table)); + } + + public List resolve(Application input) { + Objects.requireNonNull(input, "input"); + if (!SERVICE_TYPE_NAME.equals(input.getServiceType().getName())) { + return List.of(input); + } + List expanded = table.get(input.getApplicationName()); + if (expanded != null) { + return expanded; + } + return List.of(input); + } +} \ No newline at end of file diff --git a/web/src/main/resources/application.yml b/web/src/main/resources/application.yml index 664a0a82fdf4..4cac3cc096f1 100644 --- a/web/src/main/resources/application.yml +++ b/web/src/main/resources/application.yml @@ -111,6 +111,29 @@ pinpoint.web.websocket: max-binary-message-buffer-size: max-text-message-buffer-size: +# Service map application-to-service mock mapping rules +#web.servicemap: +# mock-enabled: false +# mock-mappings: +# - prefix: +# - "BLOG-API" +# - "BLOG-MAIN" +# serviceName: "BLOG" +# serviceUid: 100 +# - prefix: +# - "CAFE-NOTICE" +# - "CAFE-MANAGE-API" +# serviceName: "CAFE" +# serviceUid: 200 + +## Service map virtual-service mock mapping rules +#web.virtual-service: +# mock-enabled: true +# mappings: +# - virtual-service-name: CAFE-SERVICE +# members: +# - { name: CAFE.MAIN, service-type: SPRING_BOOT } +# - { name: CAFE.API, service-type: SPRING_BOOT } --- spring.config.activate.on-profile: local diff --git a/web/src/test/java/com/navercorp/pinpoint/web/applicationmap/controller/MapControllerTest.java b/web/src/test/java/com/navercorp/pinpoint/web/applicationmap/controller/MapControllerTest.java index 01c1d1c8910e..cbcfeeca9c60 100644 --- a/web/src/test/java/com/navercorp/pinpoint/web/applicationmap/controller/MapControllerTest.java +++ b/web/src/test/java/com/navercorp/pinpoint/web/applicationmap/controller/MapControllerTest.java @@ -20,6 +20,7 @@ import com.navercorp.pinpoint.web.applicationmap.ApplicationMap; import com.navercorp.pinpoint.web.applicationmap.config.MapProperties; import com.navercorp.pinpoint.web.applicationmap.service.MapService; +import com.navercorp.pinpoint.web.applicationmap.virtualapplication.VirtualApplicationResolver; import com.navercorp.pinpoint.web.util.ApplicationValidator; import com.navercorp.pinpoint.web.vo.Application; import org.apache.logging.log4j.LogManager; @@ -60,7 +61,8 @@ public void setup() { MapProperties mapProperties = new MapProperties(); Duration duration = Duration.ofMinutes(1); - MapController controller = new MapController(mapProperties, mapService, applicationValidator, duration); + VirtualApplicationResolver virtualApplicationResolver = VirtualApplicationResolver.emptyResolver(); + MapController controller = new MapController(mapProperties, mapService, applicationValidator, virtualApplicationResolver, duration); when(applicationValidator.newApplication(any(), anyInt(), any())) .thenReturn(new Application("test", ServiceType.STAND_ALONE)); diff --git a/web/src/test/java/com/navercorp/pinpoint/web/applicationmap/virtualapplication/VirtualApplicationResolverTest.java b/web/src/test/java/com/navercorp/pinpoint/web/applicationmap/virtualapplication/VirtualApplicationResolverTest.java new file mode 100644 index 000000000000..48776055fd82 --- /dev/null +++ b/web/src/test/java/com/navercorp/pinpoint/web/applicationmap/virtualapplication/VirtualApplicationResolverTest.java @@ -0,0 +1,161 @@ +/* + * Copyright 2026 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.applicationmap.virtualapplication; + +import com.navercorp.pinpoint.common.trace.ServiceType; +import com.navercorp.pinpoint.web.applicationmap.virtualapplication.VirtualApplicationProperties.ApplicationRef; +import com.navercorp.pinpoint.web.applicationmap.virtualapplication.VirtualApplicationProperties.VirtualApplicationRule; +import com.navercorp.pinpoint.web.util.ApplicationValidator; +import com.navercorp.pinpoint.web.vo.Application; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class VirtualApplicationResolverTest { + + @Test + void resolve_match_expandsToConfiguredMembers() { + Application memberA = new Application("CAFE.A", ServiceType.STAND_ALONE); + Application memberB = new Application("CAFE.B", ServiceType.STAND_ALONE); + + ApplicationValidator validator = mock(ApplicationValidator.class); + when(validator.newApplication("CAFE.A", "SPRING_BOOT")).thenReturn(memberA); + when(validator.newApplication("CAFE.B", "TOMCAT")).thenReturn(memberB); + + VirtualApplicationRule rule = newRule("CAFESERVICE", + ref("CAFE.A", "SPRING_BOOT"), + ref("CAFE.B", "TOMCAT")); + + VirtualApplicationResolver resolver = VirtualApplicationResolver.of(List.of(rule), validator); + + Application input = new Application("CAFESERVICE", serviceTypeNamed("SERVICE")); + List result = resolver.resolve(input); + + assertThat(result).containsExactly(memberA, memberB); + } + + @Test + void resolve_noMatch_returnsSingletonOfInput() { + ApplicationValidator validator = mock(ApplicationValidator.class); + when(validator.newApplication("CAFE.A", "SPRING_BOOT")) + .thenReturn(new Application("CAFE.A", ServiceType.STAND_ALONE)); + + VirtualApplicationResolver resolver = VirtualApplicationResolver.of( + List.of(newRule("CAFESERVICE", ref("CAFE.A", "SPRING_BOOT"))), + validator); + + Application input = new Application("UNKNOWN_APP", serviceTypeNamed("SERVICE")); + List result = resolver.resolve(input); + + assertThat(result).containsExactly(input); + } + + @Test + void resolve_nonServiceType_returnsSingletonOfInput() { + ApplicationValidator validator = mock(ApplicationValidator.class); + when(validator.newApplication("CAFE.A", "SPRING_BOOT")) + .thenReturn(new Application("CAFE.A", ServiceType.STAND_ALONE)); + + VirtualApplicationResolver resolver = VirtualApplicationResolver.of( + List.of(newRule("CAFESERVICE", ref("CAFE.A", "SPRING_BOOT"))), + validator); + + Application input = new Application("CAFESERVICE", serviceTypeNamed("SPRING_BOOT")); + assertThat(resolver.resolve(input)).containsExactly(input); + } + + @Test + void emptyResolver_alwaysReturnsSingletonOfInput() { + VirtualApplicationResolver resolver = VirtualApplicationResolver.emptyResolver(); + + Application input = new Application("ANY", ServiceType.STAND_ALONE); + assertThat(resolver.resolve(input)).containsExactly(input); + } + + @Test + void of_validatesMembers_atBootTime() { + ApplicationValidator validator = mock(ApplicationValidator.class); + Application memberA = new Application("CAFE.A", ServiceType.STAND_ALONE); + when(validator.newApplication("CAFE.A", "SPRING_BOOT")).thenReturn(memberA); + + VirtualApplicationResolver.of( + List.of(newRule("CAFESERVICE", ref("CAFE.A", "SPRING_BOOT"))), + validator); + + verify(validator).newApplication("CAFE.A", "SPRING_BOOT"); + } + + @Test + void of_rejects_emptyMembers() { + ApplicationValidator validator = mock(ApplicationValidator.class); + VirtualApplicationRule rule = newRule("CAFESERVICE"); + + assertThatThrownBy(() -> VirtualApplicationResolver.of(List.of(rule), validator)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("members must not be empty"); + } + + @Test + void of_rejects_emptyVirtualServiceName() { + ApplicationValidator validator = mock(ApplicationValidator.class); + VirtualApplicationRule rule = newRule("", ref("CAFE.A", "SPRING_BOOT")); + + assertThatThrownBy(() -> VirtualApplicationResolver.of(List.of(rule), validator)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("virtualServiceName must not be empty"); + } + + @Test + void of_rejects_duplicateVirtualServiceName() { + ApplicationValidator validator = mock(ApplicationValidator.class); + when(validator.newApplication("CAFE.A", "SPRING_BOOT")) + .thenReturn(new Application("CAFE.A", ServiceType.STAND_ALONE)); + + VirtualApplicationRule first = newRule("DUP", ref("CAFE.A", "SPRING_BOOT")); + VirtualApplicationRule second = newRule("DUP", ref("CAFE.A", "SPRING_BOOT")); + + assertThatThrownBy(() -> VirtualApplicationResolver.of(List.of(first, second), validator)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("duplicate virtualServiceName"); + } + + private static VirtualApplicationRule newRule(String virtualServiceName, ApplicationRef... members) { + VirtualApplicationRule rule = new VirtualApplicationRule(); + rule.setVirtualServiceName(virtualServiceName); + rule.setMembers(List.of(members)); + return rule; + } + + private static ApplicationRef ref(String name, String serviceType) { + ApplicationRef ref = new ApplicationRef(); + ref.setName(name); + ref.setServiceType(serviceType); + return ref; + } + + private static ServiceType serviceTypeNamed(String name) { + ServiceType serviceType = mock(ServiceType.class); + when(serviceType.getName()).thenReturn(name); + return serviceType; + } +} \ No newline at end of file