Skip to content

Commit 2d288fd

Browse files
WIP custom attributes metrics labeler
1 parent aa0579d commit 2d288fd

File tree

4 files changed

+442
-0
lines changed

4 files changed

+442
-0
lines changed
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
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+
Labeler that supports per-request custom attribute addition to web framework
17+
instrumentor-originating OpenTelemetry metrics.
18+
19+
This was inspired by OpenTelemetry Go's net/http instrumentation Labeler
20+
https://github.com/open-telemetry/opentelemetry-go-contrib/pull/306
21+
"""
22+
23+
from opentelemetry.instrumentation._labeler._internal import (
24+
Labeler,
25+
get_labeler,
26+
set_labeler,
27+
clear_labeler,
28+
get_labeler_attributes,
29+
enhance_metric_attributes,
30+
)
31+
32+
__all__ = [
33+
"Labeler",
34+
"get_labeler",
35+
"set_labeler",
36+
"clear_labeler",
37+
"get_labeler_attributes",
38+
"enhance_metric_attributes",
39+
]
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
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+
import threading
16+
from typing import Dict, Union, Optional, Any
17+
import contextvars
18+
19+
# Context variable to store the current labeler
20+
_labeler_context: contextvars.ContextVar[Optional["Labeler"]] = contextvars.ContextVar(
21+
"otel_labeler", default=None
22+
)
23+
24+
25+
class Labeler:
26+
"""
27+
Labeler is used to allow instrumented web applications to add custom attributes
28+
to the metrics recorded by OpenTelemetry instrumentations.
29+
30+
This class is thread-safe and can be used to accumulate custom attributes
31+
that will be included in OpenTelemetry metrics for the current request.
32+
"""
33+
34+
def __init__(self):
35+
self._lock = threading.Lock()
36+
self._attributes: Dict[str, Union[str, int, float, bool]] = {}
37+
38+
def add(self, key: str, value: Union[str, int, float, bool]) -> None:
39+
"""
40+
Add a single attribute to the labeler.
41+
42+
Args:
43+
key: The attribute key
44+
value: The attribute value (must be a primitive type)
45+
"""
46+
if not isinstance(value, (str, int, float, bool)):
47+
raise ValueError(f"Attribute value must be str, int, float, or bool, got {type(value)}")
48+
49+
with self._lock:
50+
self._attributes[key] = value
51+
52+
def add_attributes(self, attributes: Dict[str, Union[str, int, float, bool]]) -> None:
53+
"""
54+
Add multiple attributes to the labeler.
55+
56+
Args:
57+
attributes: Dictionary of attributes to add
58+
"""
59+
for key, value in attributes.items():
60+
if not isinstance(value, (str, int, float, bool)):
61+
raise ValueError(f"Attribute value for '{key}' must be str, int, float, or bool, got {type(value)}")
62+
63+
with self._lock:
64+
self._attributes.update(attributes)
65+
66+
def get_attributes(self) -> Dict[str, Union[str, int, float, bool]]:
67+
"""
68+
Returns a copy of all attributes added to the labeler.
69+
"""
70+
with self._lock:
71+
return self._attributes.copy()
72+
73+
def clear(self) -> None:
74+
with self._lock:
75+
self._attributes.clear()
76+
77+
def __len__(self) -> int:
78+
with self._lock:
79+
return len(self._attributes)
80+
81+
82+
def get_labeler() -> Labeler:
83+
"""
84+
Get the Labeler instance for the current request context.
85+
86+
If no Labeler exists in the current context, a new one is created
87+
and stored in the context.
88+
89+
Returns:
90+
Labeler instance for the current request, or a new empty Labeler
91+
if not in a request context
92+
"""
93+
labeler = _labeler_context.get()
94+
if labeler is None:
95+
labeler = Labeler()
96+
_labeler_context.set(labeler)
97+
return labeler
98+
99+
100+
def set_labeler(labeler: Labeler) -> None:
101+
"""
102+
Set the Labeler instance for the current request context.
103+
104+
Args:
105+
labeler: The Labeler instance to set
106+
"""
107+
_labeler_context.set(labeler)
108+
109+
110+
def clear_labeler() -> None:
111+
"""
112+
Clear the Labeler instance from the current request context.
113+
"""
114+
_labeler_context.set(None)
115+
116+
117+
def get_labeler_attributes() -> Dict[str, Union[str, int, float, bool]]:
118+
"""
119+
Get attributes from the current labeler, if any.
120+
121+
Returns:
122+
Dictionary of custom attributes, or empty dict if no labeler exists
123+
"""
124+
labeler = _labeler_context.get()
125+
if labeler is None:
126+
return {}
127+
return labeler.get_attributes()
128+
129+
130+
def enhance_metric_attributes(
131+
base_attributes: Dict[str, Any],
132+
include_custom: bool = True,
133+
max_custom_attrs: int = 20,
134+
max_attr_value_length: int = 100
135+
) -> Dict[str, Any]:
136+
"""
137+
Enhance metric attributes with custom labeler attributes.
138+
139+
This function combines base metric attributes with custom attributes
140+
from the current labeler.
141+
142+
Args:
143+
base_attributes: The base attributes for the metric
144+
include_custom: Whether to include custom labeler attributes
145+
max_custom_attrs: Maximum number of custom attributes to include
146+
max_attr_value_length: Maximum length for string attribute values
147+
148+
Returns:
149+
Enhanced attributes dictionary combining base and custom attributes
150+
"""
151+
if not include_custom:
152+
return base_attributes.copy()
153+
154+
# Get custom attributes from labeler
155+
custom_attributes = get_labeler_attributes()
156+
if not custom_attributes:
157+
return base_attributes.copy()
158+
159+
# Create enhanced attributes dict
160+
enhanced_attributes = base_attributes.copy()
161+
162+
# Filter and add custom attributes with safety checks
163+
added_count = 0
164+
for key, value in custom_attributes.items():
165+
if added_count >= max_custom_attrs:
166+
break
167+
168+
# Skip attributes that would override base attributes
169+
if key in base_attributes:
170+
continue
171+
172+
# Apply value length limit for strings
173+
if isinstance(value, str) and len(value) > max_attr_value_length:
174+
value = value[:max_attr_value_length]
175+
176+
# Only include safe attribute types
177+
if isinstance(value, (str, int, float, bool)):
178+
enhanced_attributes[key] = value
179+
added_count += 1
180+
181+
return enhanced_attributes
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
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+
Example Flask application demonstrating how to use Labeler for adding
16+
custom attributes to metrics generated by the Flask instrumentor.
17+
"""
18+
19+
from flask import Flask
20+
from opentelemetry.instrumentation.flask import FlaskInstrumentor
21+
from opentelemetry.instrumentation._labeler import get_labeler
22+
23+
app = Flask(__name__)
24+
FlaskInstrumentor().instrument_app(app)
25+
26+
27+
@app.route("/healthcheck")
28+
def healthcheck():
29+
# Get the labeler for the current request
30+
labeler = get_labeler()
31+
32+
labeler.add_attributes({
33+
"endpoint_type": "healthcheck",
34+
"internal_request": True,
35+
})
36+
return "OK"
37+
38+
39+
@app.route("/user/<user_id>")
40+
def user_profile(user_id):
41+
labeler = get_labeler()
42+
43+
# Can add individual attributes or multiple at once
44+
labeler.add("user_id", user_id)
45+
labeler.add_attributes({
46+
"has_premium": user_id in ["123", "456"],
47+
"experiment_group": "control",
48+
"feature_enabled": True,
49+
"user_segment": "active"
50+
})
51+
52+
return f"Got user profile for {user_id}"
53+
54+
55+
if __name__ == "__main__":
56+
app.run(debug=True, port=5000, host='0.0.0.0')

0 commit comments

Comments
 (0)