Skip to content

Commit c4a7d12

Browse files
committed
Add support for more wildcards in the allowlist.
1 parent d6fe6a7 commit c4a7d12

File tree

2 files changed

+82
-14
lines changed

2 files changed

+82
-14
lines changed

instrumentation-genai/opentelemetry-instrumentation-google-genai/src/opentelemetry/instrumentation/google_genai/allowlist_util.py

Lines changed: 45 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
import re
1516
import os
1617
from typing import Callable, List, Optional, Set, Union
1718

@@ -29,27 +30,58 @@ def _parse_env_list(s: str) -> Set[str]:
2930
return result
3031

3132

33+
class _CompoundMatcher:
34+
35+
def __init__(self, entries: Set[str]):
36+
self._match_all = '*' in entries
37+
self._entries = entries
38+
self._regex_matcher = None
39+
regex_entries = []
40+
for entry in entries:
41+
if "*" not in entry:
42+
continue
43+
if entry == "*":
44+
continue
45+
entry = entry.replace("[", "\\[")
46+
entry = entry.replace("]", "\\]")
47+
entry = entry.replace(".", "\\.")
48+
entry = entry.replace("*", ".*")
49+
regex_entries.append(f"({entry})")
50+
if regex_entries:
51+
joined_regex = '|'.join(regex_entries)
52+
regex_str = f"^({joined_regex})$"
53+
self._regex_matcher = re.compile(regex_str)
54+
55+
@property
56+
def match_all(self):
57+
return self._match_all
58+
59+
def matches(self, x):
60+
if self._match_all:
61+
return True
62+
if x in self._entries:
63+
return True
64+
if (self._regex_matcher is not None) and (self._regex_matcher.fullmatch(x)):
65+
return True
66+
return False
67+
68+
3269
class AllowList:
3370
def __init__(
3471
self,
3572
includes: Optional[Union[Set[str], List[str]]] = None,
3673
excludes: Optional[Union[Set[str], List[str]]] = None,
37-
if_none_match: Optional[Callable[str, bool]] = None,
3874
):
39-
self._includes = set(includes or [])
40-
self._excludes = set(excludes or [])
41-
self._include_all = "*" in self._includes
42-
self._exclude_all = "*" in self._excludes
43-
assert (not self._include_all) or (
44-
not self._exclude_all
45-
), "Can't have '*' in both includes and excludes."
75+
self._includes = _CompoundMatcher(set(includes or []))
76+
self._excludes = _CompoundMatcher(set(excludes or []))
77+
assert ((not self._includes.match_all) or (not self._excludes.match_all)), "Can't have '*' in both includes and excludes."
4678

4779
def allowed(self, x: str):
48-
if self._exclude_all:
49-
return x in self._includes
50-
if self._include_all:
51-
return x not in self._excludes
52-
return (x in self._includes) and (x not in self._excludes)
80+
if self._excludes.match_all:
81+
return self._includes.matches(x)
82+
if self._includes.match_all:
83+
return not self._excludes.matches(x)
84+
return self._includes.matches(x) and not self._excludes.matches(x)
5385

5486
@staticmethod
5587
def from_env(

instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/utils/test_allowlist_util.py

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,19 +30,55 @@ def test_empty_allowlist_allows_nothing():
3030
def test_simple_include_allow_list():
3131
allow_list = AllowList(includes=["abc", "xyz"])
3232
assert allow_list.allowed("abc")
33+
assert not allow_list.allowed("abc.xyz")
3334
assert allow_list.allowed("xyz")
3435
assert not allow_list.allowed("blah")
3536
assert not allow_list.allowed("other value not in includes")
3637

3738

38-
def test_includes_and_exclues():
39+
def test_allow_list_with_prefix_matching():
40+
allow_list = AllowList(includes=["abc.*", "xyz"])
41+
assert not allow_list.allowed("abc")
42+
assert allow_list.allowed("abc.foo")
43+
assert allow_list.allowed("abc.bar")
44+
assert allow_list.allowed("xyz")
45+
assert not allow_list.allowed("blah")
46+
assert not allow_list.allowed("other value not in includes")
47+
48+
49+
def test_allow_list_with_array_wildcard_matching():
50+
allow_list = AllowList(includes=["abc[*].foo", "xyz[*].*"])
51+
assert not allow_list.allowed("abc")
52+
assert allow_list.allowed("abc[0].foo")
53+
assert not allow_list.allowed("abc[0].bar")
54+
assert allow_list.allowed("abc[1].foo")
55+
assert allow_list.allowed("xyz[0].blah")
56+
assert allow_list.allowed("xyz[1].yadayada")
57+
assert not allow_list.allowed("blah")
58+
assert not allow_list.allowed("other value not in includes")
59+
60+
61+
def test_includes_and_excludes():
3962
allow_list = AllowList(includes=["abc", "xyz"], excludes=["xyz"])
4063
assert allow_list.allowed("abc")
4164
assert not allow_list.allowed("xyz")
4265
assert not allow_list.allowed("blah")
4366
assert not allow_list.allowed("other value not in includes")
4467

4568

69+
def test_includes_and_excludes_with_wildcards():
70+
allow_list = AllowList(includes=["abc", "xyz", "xyz.*"], excludes=["xyz.foo", "xyz.foo.*"])
71+
assert allow_list.allowed("abc")
72+
assert allow_list.allowed("xyz")
73+
assert not allow_list.allowed("xyz.foo")
74+
assert not allow_list.allowed("xyz.foo.bar")
75+
assert not allow_list.allowed("xyz.foo.baz")
76+
assert allow_list.allowed("xyz.not_foo")
77+
assert allow_list.allowed("xyz.blah")
78+
assert not allow_list.allowed("blah")
79+
assert not allow_list.allowed("other value not in includes")
80+
81+
4682
def test_default_include_with_excludes():
4783
allow_list = AllowList(includes=["*"], excludes=["foo", "bar"])
4884
assert not allow_list.allowed("foo")

0 commit comments

Comments
 (0)