Skip to content

Commit 14ab0b4

Browse files
committed
Create a utility to simplify recording request attributes.
1 parent 9e4de00 commit 14ab0b4

File tree

2 files changed

+228
-0
lines changed
  • instrumentation-genai/opentelemetry-instrumentation-google-genai

2 files changed

+228
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
# Copyright The OpenTelemetry Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
16+
from typing import Dict, Optional, Set, Any, Callable, TypeAlias, Union, List
17+
18+
import json
19+
20+
Primitive: TypeAlias = Union[bool, str, int, float]
21+
22+
BoolList: TypeAlias = list[bool]
23+
StringList: TypeAlias = list[str]
24+
IntList: TypeAlias = list[int]
25+
FloatList: TypeAlias = list[float]
26+
HomogenousPrimitiveList: TypeAlias = Union[BoolList, StringList, IntList, FloatList]
27+
28+
FlattenedValue: TypeAlias = Union[Primitive, HomogenousPrimitiveList]
29+
FlattenedDict: TypeAlias = Dict[str, FlattenedValue]
30+
31+
32+
def _concat_key(prefix: Optional[str], suffix: str):
33+
if not prefix:
34+
return suffix
35+
return f"{prefix}.{suffix}"
36+
37+
38+
def _is_primitive(v):
39+
for t in [str, bool, int, float]:
40+
if isinstance(v, t):
41+
return True
42+
return False
43+
44+
45+
def _is_homogenous_primitive_list(v):
46+
if not isinstance(v, list):
47+
return False
48+
if len(v) == 0:
49+
return True
50+
if not _is_primitive(v[0]):
51+
return False
52+
t = type(v[0])
53+
for entry in v[1:]:
54+
if not isinstance(entry, t):
55+
return False
56+
return True
57+
58+
59+
def _get_flatten_func(
60+
flatten_functions: Dict[str, Callable],
61+
key_names: set[str]):
62+
for key in key_names:
63+
flatten_func = flatten_functions.get(key)
64+
if flatten_func is not None:
65+
return flatten_func
66+
return None
67+
68+
69+
def _flatten_value(
70+
key: str,
71+
value: Any,
72+
exclude_keys: Set[str],
73+
rename_keys: Dict[str, str],
74+
flatten_functions: Dict[str, Callable],
75+
_from_json=False) -> FlattenedDict:
76+
key_names = set([key])
77+
renamed_key = rename_keys.get(key)
78+
if renamed_key is not None:
79+
key_names.add(renamed_key)
80+
key = renamed_key
81+
if key_names & exclude_keys:
82+
return {}
83+
if _is_primitive(value):
84+
return {key: value}
85+
flatten_func = _get_flatten_func(flatten_functions, key_names)
86+
if flatten_func is not None:
87+
return flatten_func(key, value, exclude_keys=exclude_keys, rename_keys=rename_keys, flatten_functions=flatten_functions)
88+
if isinstance(value, dict):
89+
return _flatten_dict(value, key_prefix=key, exclude_keys=exclude_keys, rename_keys=rename_keys, flatten_functions=flatten_functions)
90+
if isinstance(value, list):
91+
if _is_homogenous_primitive_list(value):
92+
return {key: value}
93+
return _flatten_list(value, key_prefix=key, exclude_keys=exclude_keys, rename_keys=rename_keys, flatten_functions=flatten_functions)
94+
if hasattr(value, "model_dump"):
95+
d = value.model_dump()
96+
return _flatten_dict(d, key_prefix=key, exclude_keys=exclude_keys, rename_keys=rename_keys, flatten_functions=flatten_functions)
97+
if _from_json:
98+
raise ValueError(f"Cannot flatten value with key {key}; value: {value}")
99+
json_value = json.loads(json.dumps(value))
100+
return _flatten_value(key, json_value, exclude_keys=exclude_keys, rename_keys=rename_keys, flatten_functions=flatten_functions, _from_json=True)
101+
102+
103+
def _flatten_dict(
104+
d: Dict[str, Any],
105+
key_prefix: str,
106+
exclude_keys: Set[str],
107+
rename_keys: Dict[str, str],
108+
flatten_functions: Dict[str, Callable]) -> FlattenedDict:
109+
result = {}
110+
for key, value in d.items():
111+
full_key = _concat_key(key_prefix, key)
112+
flattened = _flatten_value(full_key, value, exclude_keys=exclude_keys, rename_keys=rename_keys, flatten_functions=flatten_functions)
113+
result.update(flattened)
114+
return result
115+
116+
117+
118+
def _flatten_list(
119+
lst: list[Any],
120+
key_prefix: str,
121+
exclude_keys: Set[str],
122+
rename_keys: Dict[str, str],
123+
flatten_functions: Dict[str, Callable]) -> FlattenedDict:
124+
result = {}
125+
result[_concat_key(key_prefix, "length")] = len(lst)
126+
for index, value in enumerate(lst):
127+
full_key = f"{key_prefix}[{index}]"
128+
flattened = _flatten_value(full_key, value, exclude_keys=exclude_keys, rename_keys=rename_keys, flatten_functions=flatten_functions)
129+
result.update(flattened)
130+
return result
131+
132+
133+
134+
def flatten_dict(
135+
d: Dict[str, Any],
136+
key_prefix: Optional[str] = None,
137+
exclude_keys: Optional[Union[List[str]|Set[str]]] = None,
138+
rename_keys: Optional[Dict[str, str]] = None,
139+
flatten_functions: Optional[Dict[str, Callable]] = None):
140+
key_prefix = key_prefix or ""
141+
if exclude_keys is None:
142+
exclude_keys = set()
143+
elif isinstance(exclude_keys, list):
144+
exclude_keys = set(exclude_keys)
145+
rename_keys = rename_keys or {}
146+
return _flatten_dict(d, key_prefix=key_prefix, exclude_keys=exclude_keys, rename_keys=rename_keys, flatten_functions=flatten_functions)
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
# Copyright The OpenTelemetry Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
16+
from opentelemetry.instrumentation.google_genai import dict_util
17+
18+
19+
def test_flatten_empty_dict():
20+
d = {}
21+
assert dict_util.flatten_dict(d) == d
22+
23+
def test_flatten_simple_dict():
24+
d = {
25+
"int_key": 1,
26+
"string_key": "somevalue",
27+
"float_key": 3.14,
28+
"bool_key": True
29+
}
30+
assert dict_util.flatten_dict(d) == d
31+
32+
33+
def test_flatten_with_key_exclusion():
34+
d = {
35+
"int_key": 1,
36+
"string_key": "somevalue",
37+
"float_key": 3.14,
38+
"bool_key": True
39+
}
40+
output = dict_util.flatten_dict(d, exclude_keys=["int_key"])
41+
assert "int_key" not in output
42+
assert output == {
43+
"string_key": "somevalue",
44+
"float_key": 3.14,
45+
"bool_key": True
46+
}
47+
48+
49+
def test_flatten_with_renaming():
50+
d = {
51+
"int_key": 1,
52+
"string_key": "somevalue",
53+
"float_key": 3.14,
54+
"bool_key": True
55+
}
56+
output = dict_util.flatten_dict(
57+
d,
58+
rename_keys={"float_key": "math_key"})
59+
assert "float_key" not in output
60+
assert "math_key" in output
61+
assert output == {
62+
"int_key": 1,
63+
"string_key": "somevalue",
64+
"math_key": 3.14,
65+
"bool_key": True
66+
}
67+
68+
69+
def test_flatten_with_prefixing():
70+
d = {
71+
"int_key": 1,
72+
"string_key": "somevalue",
73+
"float_key": 3.14,
74+
"bool_key": True
75+
}
76+
output = dict_util.flatten_dict(d, key_prefix="someprefix")
77+
assert output == {
78+
"someprefix.int_key": 1,
79+
"someprefix.string_key": "somevalue",
80+
"someprefix.float_key": 3.14,
81+
"someprefix.bool_key": True
82+
}

0 commit comments

Comments
 (0)