From 4a0db07ca10dedca78cdb3e2ec42e59218bf8e5f Mon Sep 17 00:00:00 2001 From: Matthieu MOREL Date: Thu, 27 Jul 2023 10:00:29 +0000 Subject: [PATCH] Peer Service Mapping Signed-off-by: Matthieu MOREL --- .../net/PeerServiceAttributesExtractor.java | 24 ++- .../instrumenter/net/PeerServiceResolver.java | 137 +++++++++++++++++ .../net/internal/PeerServiceResolver.java | 141 ++++++++++++++++++ .../net/PeerServiceResolverTest.java | 42 ++++++ .../net/internal/PeerServiceResolverTest.java | 39 +++++ 5 files changed, 375 insertions(+), 8 deletions(-) create mode 100644 instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/net/PeerServiceResolver.java create mode 100644 instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/net/internal/PeerServiceResolver.java create mode 100644 instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/net/PeerServiceResolverTest.java create mode 100644 instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/net/internal/PeerServiceResolverTest.java diff --git a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/net/PeerServiceAttributesExtractor.java b/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/net/PeerServiceAttributesExtractor.java index 8744b5f1845e..adb93ad70f36 100644 --- a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/net/PeerServiceAttributesExtractor.java +++ b/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/net/PeerServiceAttributesExtractor.java @@ -22,14 +22,14 @@ public final class PeerServiceAttributesExtractor implements AttributesExtractor { private final ServerAttributesGetter attributesGetter; - private final Map peerServiceMapping; + private final PeerServiceResolver peerServiceResolver; // visible for tests PeerServiceAttributesExtractor( ServerAttributesGetter attributesGetter, Map peerServiceMapping) { this.attributesGetter = attributesGetter; - this.peerServiceMapping = peerServiceMapping; + this.peerServiceResolver = new PeerServiceResolver(peerServiceMapping); } /** @@ -53,16 +53,20 @@ public void onEnd( @Nullable RESPONSE response, @Nullable Throwable error) { - if (peerServiceMapping.isEmpty()) { + if (peerServiceResolver.isEmpty()) { // optimization for common case return; } String serverAddress = attributesGetter.getServerAddress(request); - String peerService = mapToPeerService(serverAddress); + Integer serverPort = attributesGetter.getServerPort(request); + // TODO wire protocol and path + String peerService = mapToPeerService(null, serverAddress, serverPort, null); if (peerService == null) { String serverSocketDomain = attributesGetter.getServerSocketDomain(request, response); - peerService = mapToPeerService(serverSocketDomain); + Integer serverSocketPort = attributesGetter.getServerSocketPort(request, response); + // TODO wire protocol and path + peerService = mapToPeerService(null, serverSocketDomain, serverSocketPort, null); } if (peerService != null) { attributes.put(SemanticAttributes.PEER_SERVICE, peerService); @@ -70,10 +74,14 @@ public void onEnd( } @Nullable - private String mapToPeerService(@Nullable String endpoint) { - if (endpoint == null) { + private String mapToPeerService( + @Nullable String protocol, + @Nullable String host, + @Nullable Integer port, + @Nullable String path) { + if (host == null) { return null; } - return peerServiceMapping.get(endpoint); + return peerServiceResolver.resolveService(protocol, host, port, path); } } diff --git a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/net/PeerServiceResolver.java b/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/net/PeerServiceResolver.java new file mode 100644 index 000000000000..018a271a7309 --- /dev/null +++ b/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/net/PeerServiceResolver.java @@ -0,0 +1,137 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.instrumenter.net; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Comparator; +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +public class PeerServiceResolver { + + private static final Map> MAPPING = new ConcurrentHashMap<>(); + + public PeerServiceResolver(Map peerServiceMapping) { + peerServiceMapping.forEach( + (key, serviceName) -> { + try { + String protocol = null; + URI uri; + if (key.contains("://")) { + uri = new URI(key); + protocol = uri.getScheme(); + } else { + uri = new URI("http://" + key); + } + String host = uri.getHost(); + Integer port = uri.getPort() != -1 ? Integer.valueOf(uri.getPort()) : null; + Set matchers = MAPPING.get(host); + ServiceMatcher serviceMatcher = + new ServiceMatcher(serviceName, protocol, port, uri.getPath()); + if (matchers == null) { + matchers = new HashSet<>(); + matchers.add(serviceMatcher); + MAPPING.put(host, matchers); + } else { + matchers.add(serviceMatcher); + } + } catch (URISyntaxException use) { + // TODO wire logging + } + }); + } + + public boolean isEmpty() { + return MAPPING.isEmpty(); + } + + public String resolveService(String protocol, String host, Integer port, String path) { + Set matchers = MAPPING.get(host); + if (matchers == null) { + return null; + } else { + return matchers.stream() + .filter(serviceMatcher -> serviceMatcher.matches(protocol, port, path)) + .max(Comparator.comparingInt(ServiceMatcher::getPathLength)) + .map(ServiceMatcher::getServiceName) + .orElse(null); + } + } + + private static class ServiceMatcher { + + private final String serviceName; + private final String protocol; + private final Integer port; + private final String path; + + public ServiceMatcher(String serviceName, String protocol, Integer port, String path) { + this.serviceName = serviceName; + this.protocol = protocol; + this.port = port; + this.path = path; + } + + public String getServiceName() { + return serviceName; + } + + public int getPathLength() { + if (this.path == null) { + return 0; + } + return this.path.length(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof ServiceMatcher)) { + return false; + } + ServiceMatcher that = (ServiceMatcher) o; + return Objects.equals(protocol, that.protocol) + && Objects.equals(port, that.port) + && Objects.equals(path, that.path); + } + + @Override + public int hashCode() { + return Objects.hash(protocol, port, path); + } + + public boolean matches(String protocol, Integer port, String path) { + if (this.protocol != null) { + if (!this.protocol.equals(protocol)) { + return false; + } + } + if (this.port != null) { + if (!this.port.equals(port)) { + return false; + } + } + if (this.path != null) { + if (path == null) { + return false; + } + if (!path.startsWith(this.path)) { + return false; + } + if (port != null) { + return port.equals(this.port); + } + } + return true; + } + } +} diff --git a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/net/internal/PeerServiceResolver.java b/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/net/internal/PeerServiceResolver.java new file mode 100644 index 000000000000..462e73a2068f --- /dev/null +++ b/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/net/internal/PeerServiceResolver.java @@ -0,0 +1,141 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.instrumenter.net.internal; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Comparator; +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +public class PeerServiceResolver { + + private static final Map> MAPPING = new ConcurrentHashMap<>(); + + public PeerServiceResolver(Map peerServiceMapping) { + peerServiceMapping.forEach( + (key, serviceName) -> { + try { + String protocol = null; + URI uri; + if (key.contains("://")) { + uri = new URI(key); + protocol = uri.getScheme(); + } else { + uri = new URI("http://" + key); + } + String host = uri.getHost(); + Integer port = uri.getPort() != -1 ? Integer.valueOf(uri.getPort()) : null; + Set matchers = MAPPING.get(host); + ServiceMatcher serviceMatcher = + new ServiceMatcher(serviceName, protocol, port, uri.getPath()); + if (matchers == null) { + matchers = new HashSet<>(); + matchers.add(serviceMatcher); + MAPPING.put(host, matchers); + } else { + matchers.add(serviceMatcher); + } + } catch (URISyntaxException use) { + // TODO wire logging + } + }); + } + + public boolean isEmpty() { + return MAPPING.isEmpty(); + } + + public String resolveService(String protocol, String host, Integer port, String path) { + Set matchers = MAPPING.get(host); + if (matchers == null) { + return null; + } else { + return matchers.stream() + .filter(serviceMatcher -> serviceMatcher.matches(protocol, port, path)) + .max(Comparator.comparingInt(ServiceMatcher::getPathLength)) + .map(ServiceMatcher::getServiceName) + .orElse(null); + } + } + + private static class ServiceMatcher { + + private final String serviceName; + private final String protocol; + private final Integer port; + private final String path; + + public ServiceMatcher(String serviceName, String protocol, Integer port, String path) { + this.serviceName = serviceName; + this.protocol = protocol; + this.port = port; + this.path = path; + } + + public String getServiceName() { + return serviceName; + } + + public int getPathLength() { + if (this.path == null) { + return 0; + } + return this.path.length(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof ServiceMatcher)) { + return false; + } + ServiceMatcher that = (ServiceMatcher) o; + return Objects.equals(protocol, that.protocol) + && Objects.equals(port, that.port) + && Objects.equals(path, that.path); + } + + @Override + public int hashCode() { + return Objects.hash(protocol, port, path); + } + + public boolean matches(String protocol, Integer port, String path) { + if (this.protocol != null) { + if (!this.protocol.equals(protocol)) { + return false; + } + } + if (this.port != null) { + if (!this.port.equals(port)) { + return false; + } + } + if (this.path != null) { + if (path == null) { + return false; + } + if (!path.startsWith(this.path)) { + return false; + } + if (port != null) { + return port.equals(this.port); + } + } + return true; + } + } +} diff --git a/instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/net/PeerServiceResolverTest.java b/instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/net/PeerServiceResolverTest.java new file mode 100644 index 000000000000..0c9dfb2e52c8 --- /dev/null +++ b/instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/net/PeerServiceResolverTest.java @@ -0,0 +1,42 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.instrumenter.net; + +import java.util.HashMap; +import java.util.Map; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class PeerServiceResolverTest { + + @Test + public void test() { + Map peerServiceMapping = new HashMap<>(); + peerServiceMapping.put("tcp://example.com:8080", "myTcpService"); + peerServiceMapping.put("example.com:8080", "myService"); + peerServiceMapping.put("example.com", "myServiceBase"); + peerServiceMapping.put("1.2.3.4", "someOtherService"); + peerServiceMapping.put("1.2.3.4:8080/api", "someOtherService8080"); + peerServiceMapping.put("1.2.3.4/api", "someOtherServiceAPI"); + + PeerServiceResolver peerServiceResolver = new PeerServiceResolver(peerServiceMapping); + + Assertions.assertEquals( + "myServiceBase", peerServiceResolver.resolveService(null, "example.com", null, null)); + Assertions.assertEquals( + "myService", peerServiceResolver.resolveService("http", "example.com", 8080, "/")); + Assertions.assertEquals( + "myTcpService", peerServiceResolver.resolveService("tcp", "example.com", 8080, "/api")); + Assertions.assertEquals( + "someOtherService8080", peerServiceResolver.resolveService(null, "1.2.3.4", 8080, "/api")); + Assertions.assertEquals( + "someOtherService", peerServiceResolver.resolveService(null, "1.2.3.4", 9000, "/api")); + Assertions.assertEquals( + "someOtherService", peerServiceResolver.resolveService(null, "1.2.3.4", 8080, null)); + Assertions.assertEquals( + "someOtherServiceAPI", peerServiceResolver.resolveService(null, "1.2.3.4", null, "/api")); + } +} diff --git a/instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/net/internal/PeerServiceResolverTest.java b/instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/net/internal/PeerServiceResolverTest.java new file mode 100644 index 000000000000..692a2d76a379 --- /dev/null +++ b/instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/net/internal/PeerServiceResolverTest.java @@ -0,0 +1,39 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.instrumenter.net.internal; + +import org.junit.jupiter.api.Assertions; + +class PeerServiceResolverTest { + + @Test + public void test() { + Map peerServiceMapping = new HashMap<>(); + peerServiceMapping.put("tcp://example.com:8080", "myTcpService"); + peerServiceMapping.put("example.com:8080", "myService"); + peerServiceMapping.put("example.com", "myServiceBase"); + peerServiceMapping.put("1.2.3.4", "someOtherService"); + peerServiceMapping.put("1.2.3.4:8080/api", "someOtherService8080"); + peerServiceMapping.put("1.2.3.4/api", "someOtherServiceAPI"); + + PeerServiceResolver peerServiceResolver = new PeerServiceResolver(peerServiceMapping); + + Assertions.assertEquals( + "myServiceBase", peerServiceResolver.resolveService(null, "example.com", null, null)); + Assertions.assertEquals( + "myService", peerServiceResolver.resolveService("http", "example.com", 8080, "/")); + Assertions.assertEquals( + "myTcpService", peerServiceResolver.resolveService("tcp", "example.com", 8080, "/api")); + Assertions.assertEquals( + "someOtherService8080", peerServiceResolver.resolveService(null, "1.2.3.4", 8080, "/api")); + Assertions.assertEquals( + "someOtherService", peerServiceResolver.resolveService(null, "1.2.3.4", 9000, "/api")); + Assertions.assertEquals( + "someOtherService", peerServiceResolver.resolveService(null, "1.2.3.4", 8080, null)); + Assertions.assertEquals( + "someOtherServiceAPI", peerServiceResolver.resolveService(null, "1.2.3.4", null, "/api")); + } +}