Skip to content

Commit 8e7dcf6

Browse files
committed
peer service mapping support glob
1 parent f930598 commit 8e7dcf6

File tree

6 files changed

+635
-30
lines changed

6 files changed

+635
-30
lines changed

instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/net/PeerServiceResolverImpl.java

+71-18
Original file line numberDiff line numberDiff line change
@@ -10,55 +10,108 @@
1010
import static java.util.Comparator.nullsFirst;
1111

1212
import com.google.auto.value.AutoValue;
13+
import io.opentelemetry.instrumentation.api.incubator.semconv.net.internal.GlobUrlParser;
1314
import io.opentelemetry.instrumentation.api.incubator.semconv.net.internal.UrlParser;
15+
import io.opentelemetry.instrumentation.api.incubator.semconv.util.internal.GlobUtil;
1416
import java.util.Comparator;
1517
import java.util.HashMap;
18+
import java.util.LinkedHashMap;
1619
import java.util.Map;
20+
import java.util.TreeMap;
21+
import java.util.function.Predicate;
1722
import java.util.function.Supplier;
1823
import javax.annotation.Nullable;
1924

2025
class PeerServiceResolverImpl implements PeerServiceResolver {
2126

2227
private static final Comparator<ServiceMatcher> matcherComparator =
2328
nullsFirst(
24-
comparing(ServiceMatcher::getPort, nullsFirst(naturalOrder()))
25-
.thenComparing(comparing(ServiceMatcher::getPath, nullsFirst(naturalOrder()))));
29+
comparing(ServiceMatcher::getPort, nullsFirst(naturalOrder()))
30+
.thenComparing(ServiceMatcher::getPath, nullsFirst(naturalOrder())))
31+
.reversed();
2632

2733
private final Map<String, Map<ServiceMatcher, String>> mapping = new HashMap<>();
34+
private final Map<Predicate<String>, Map<ServiceMatcher, String>> globMapping =
35+
new LinkedHashMap<>();
2836

2937
PeerServiceResolverImpl(Map<String, String> peerServiceMapping) {
30-
peerServiceMapping.forEach(
31-
(key, serviceName) -> {
32-
String url = "https://" + key;
33-
String host = UrlParser.getHost(url);
34-
Integer port = UrlParser.getPort(url);
35-
String path = UrlParser.getPath(url);
36-
Map<ServiceMatcher, String> matchers =
37-
mapping.computeIfAbsent(host, x -> new HashMap<>());
38-
matchers.putIfAbsent(ServiceMatcher.create(port, path), serviceName);
39-
});
38+
Map<String, Predicate<String>> globHostPredicates = new HashMap<>();
39+
40+
peerServiceMapping.entrySet().stream()
41+
.sorted((o1, o2) -> Comparator.<String>naturalOrder().compare(o2.getKey(), o1.getKey()))
42+
.forEach(
43+
(entry) -> {
44+
String url = "https://" + entry.getKey();
45+
String serviceName = entry.getValue();
46+
47+
String host = GlobUrlParser.getHost(url);
48+
if (host == null) {
49+
return;
50+
}
51+
52+
if (host.contains("*") || host.contains("?")) {
53+
ServiceMatcher serviceMatcher =
54+
ServiceMatcher.create(GlobUrlParser.getPort(url), GlobUrlParser.getPath(url));
55+
globMapping
56+
.computeIfAbsent(
57+
globHostPredicates.computeIfAbsent(host, GlobUtil::toGlobPatternPredicate),
58+
x -> new TreeMap<>(matcherComparator))
59+
.putIfAbsent(serviceMatcher, serviceName);
60+
} else {
61+
ServiceMatcher serviceMatcher =
62+
ServiceMatcher.create(UrlParser.getPort(url), UrlParser.getPath(url));
63+
mapping
64+
.computeIfAbsent(host, x -> new TreeMap<>(matcherComparator))
65+
.putIfAbsent(serviceMatcher, serviceName);
66+
}
67+
});
4068
}
4169

4270
@Override
4371
public boolean isEmpty() {
44-
return mapping.isEmpty();
72+
return mapping.isEmpty() && globMapping.isEmpty();
4573
}
4674

47-
@Override
4875
@Nullable
49-
public String resolveService(
50-
String host, @Nullable Integer port, @Nullable Supplier<String> pathSupplier) {
51-
Map<ServiceMatcher, String> matchers = mapping.get(host);
76+
static String matchService(
77+
@Nullable Map<ServiceMatcher, String> matchers,
78+
@Nullable Integer port,
79+
@Nullable Supplier<String> pathSupplier) {
5280
if (matchers == null) {
5381
return null;
5482
}
83+
5584
return matchers.entrySet().stream()
5685
.filter(entry -> entry.getKey().matches(port, pathSupplier))
57-
.max((o1, o2) -> matcherComparator.compare(o1.getKey(), o2.getKey()))
86+
.findFirst()
5887
.map(Map.Entry::getValue)
5988
.orElse(null);
6089
}
6190

91+
@Override
92+
@Nullable
93+
public String resolveService(
94+
String host, @Nullable Integer port, @Nullable Supplier<String> pathSupplier) {
95+
96+
String service = matchService(mapping.get(host), port, pathSupplier);
97+
if (service != null) {
98+
return service;
99+
}
100+
101+
for (Map.Entry<Predicate<String>, Map<ServiceMatcher, String>> entry : globMapping.entrySet()) {
102+
if (!entry.getKey().test(host)) {
103+
continue;
104+
}
105+
106+
service = matchService(entry.getValue(), port, pathSupplier);
107+
if (service != null) {
108+
return service;
109+
}
110+
}
111+
112+
return null;
113+
}
114+
62115
@AutoValue
63116
abstract static class ServiceMatcher {
64117

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.instrumentation.api.incubator.semconv.net.internal;
7+
8+
import java.util.function.Predicate;
9+
import javax.annotation.Nullable;
10+
11+
/**
12+
* This class is internal and is hence not for public use. Its APIs are unstable and can change at
13+
* any time.
14+
*/
15+
public final class GlobUrlParser {
16+
17+
@Nullable
18+
public static String getHost(String url) {
19+
20+
int startIndex = getHostStartIndex(url);
21+
if (startIndex == -1) {
22+
return null;
23+
}
24+
25+
int endIndexExclusive = getHostEndIndexExclusive(url, startIndex);
26+
if (endIndexExclusive == startIndex) {
27+
return null;
28+
}
29+
30+
return url.substring(startIndex, endIndexExclusive);
31+
}
32+
33+
@Nullable
34+
public static Integer getPort(String url) {
35+
36+
int hostStartIndex = getHostStartIndex(url);
37+
if (hostStartIndex == -1) {
38+
return null;
39+
}
40+
41+
int hostEndIndexExclusive = getHostEndIndexExclusive(url, hostStartIndex);
42+
if (hostEndIndexExclusive == hostStartIndex) {
43+
return null;
44+
}
45+
46+
if (hostEndIndexExclusive < url.length() && url.charAt(hostEndIndexExclusive) != ':') {
47+
return null;
48+
}
49+
50+
int portStartIndex = hostEndIndexExclusive + 1;
51+
52+
int portEndIndexExclusive = getPortEndIndexExclusive(url, portStartIndex);
53+
if (portEndIndexExclusive == portStartIndex) {
54+
return null;
55+
}
56+
57+
return safeParse(url.substring(portStartIndex, portEndIndexExclusive));
58+
}
59+
60+
@Nullable
61+
public static String getPath(String url) {
62+
63+
int hostStartIndex = getHostStartIndex(url);
64+
if (hostStartIndex == -1) {
65+
return null;
66+
}
67+
68+
int hostEndIndexExclusive = getHostEndIndexExclusive(url, hostStartIndex);
69+
if (hostEndIndexExclusive == hostStartIndex) {
70+
return null;
71+
}
72+
73+
int pathStartIndex = url.indexOf('/', hostEndIndexExclusive);
74+
if (pathStartIndex == -1) {
75+
return null;
76+
}
77+
78+
int pathEndIndexExclusive = getPathEndIndexExclusive(url, pathStartIndex);
79+
if (pathEndIndexExclusive == pathStartIndex) {
80+
return null;
81+
}
82+
83+
return url.substring(pathStartIndex, pathEndIndexExclusive);
84+
}
85+
86+
private static int getHostStartIndex(String url) {
87+
88+
int schemeEndIndex = url.indexOf(':');
89+
if (schemeEndIndex == -1) {
90+
// not a valid url
91+
return -1;
92+
}
93+
94+
int len = url.length();
95+
if (len <= schemeEndIndex + 2
96+
|| url.charAt(schemeEndIndex + 1) != '/'
97+
|| url.charAt(schemeEndIndex + 2) != '/') {
98+
// has no authority component
99+
return -1;
100+
}
101+
102+
return schemeEndIndex + 3;
103+
}
104+
105+
private static int getHostEndIndexExclusive(String url, int startIndex) {
106+
// look for the end of the host:
107+
// ':' ==> start of port, or
108+
// '/', '?', '#' ==> start of path
109+
return getEndIndexExclusive(url, startIndex, c -> (c == ':' || c == '/' || c == '#'));
110+
}
111+
112+
private static int getPortEndIndexExclusive(String url, int startIndex) {
113+
// look for the end of the port:
114+
// '/', '?', '#' ==> start of path
115+
return getEndIndexExclusive(url, startIndex, c -> (c == '/' || c == '?' || c == '#'));
116+
}
117+
118+
private static int getPathEndIndexExclusive(String url, int startIndex) {
119+
// look for the end of the path:
120+
// '?', '#' ==> end of path
121+
return getEndIndexExclusive(url, startIndex, c -> (c == '?' || c == '#'));
122+
}
123+
124+
private static int getEndIndexExclusive(
125+
String url, int startIndex, Predicate<Character> predicate) {
126+
int index;
127+
int len = url.length();
128+
for (index = startIndex; index < len; index++) {
129+
char c = url.charAt(index);
130+
if (predicate.test(c)) {
131+
break;
132+
}
133+
}
134+
return index;
135+
}
136+
137+
@Nullable
138+
private static Integer safeParse(String port) {
139+
try {
140+
return Integer.valueOf(port);
141+
} catch (NumberFormatException e) {
142+
return null;
143+
}
144+
}
145+
146+
private GlobUrlParser() {}
147+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.instrumentation.api.incubator.semconv.util.internal;
7+
8+
import java.util.function.Predicate;
9+
import java.util.regex.Pattern;
10+
11+
/**
12+
* Utilities for glob pattern matching.
13+
*
14+
* <p>This class is internal and is hence not for public use. Its APIs are unstable and can change
15+
* at any time.
16+
*/
17+
public final class GlobUtil {
18+
19+
private GlobUtil() {}
20+
21+
/**
22+
* Return a predicate that returns {@code true} if a string matches the {@code globPattern}.
23+
*
24+
* <p>{@code globPattern} may contain the wildcard characters {@code *} and {@code ?} with the
25+
* following matching criteria:
26+
*
27+
* <ul>
28+
* <li>{@code *} matches 0 or more instances of any character
29+
* <li>{@code ?} matches exactly one instance of any character
30+
* </ul>
31+
*/
32+
public static Predicate<String> toGlobPatternPredicate(String globPattern) {
33+
// Match all
34+
if (globPattern.equals("*")) {
35+
return unused -> true;
36+
}
37+
38+
// If globPattern contains '*' or '?', convert it to a regex and return corresponding predicate
39+
for (int i = 0; i < globPattern.length(); i++) {
40+
char c = globPattern.charAt(i);
41+
if (c == '*' || c == '?') {
42+
Pattern pattern = toRegexPattern(globPattern);
43+
return string -> pattern.matcher(string).matches();
44+
}
45+
}
46+
47+
// Exact match, ignoring case
48+
return globPattern::equalsIgnoreCase;
49+
}
50+
51+
/**
52+
* Transform the {@code globPattern} to a regex by converting {@code *} to {@code .*}, {@code ?}
53+
* to {@code .}, and escaping other regex special characters.
54+
*/
55+
private static Pattern toRegexPattern(String globPattern) {
56+
int tokenStart = -1;
57+
StringBuilder patternBuilder = new StringBuilder();
58+
for (int i = 0; i < globPattern.length(); i++) {
59+
char c = globPattern.charAt(i);
60+
if (c == '*' || c == '?') {
61+
if (tokenStart != -1) {
62+
patternBuilder.append(Pattern.quote(globPattern.substring(tokenStart, i)));
63+
tokenStart = -1;
64+
}
65+
if (c == '*') {
66+
patternBuilder.append(".*");
67+
} else {
68+
// c == '?'
69+
patternBuilder.append(".");
70+
}
71+
} else {
72+
if (tokenStart == -1) {
73+
tokenStart = i;
74+
}
75+
}
76+
}
77+
if (tokenStart != -1) {
78+
patternBuilder.append(Pattern.quote(globPattern.substring(tokenStart)));
79+
}
80+
return Pattern.compile(patternBuilder.toString());
81+
}
82+
}

0 commit comments

Comments
 (0)