Skip to content

Commit 3814f3b

Browse files
committed
feat: Add scope.set_attribute
1 parent d2cbf74 commit 3814f3b

File tree

4 files changed

+143
-1
lines changed

4 files changed

+143
-1
lines changed

sentry_sdk/scope.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
disable_capture_event,
4747
event_from_exception,
4848
exc_info_from_error,
49+
format_attribute,
4950
logger,
5051
has_logs_enabled,
5152
has_metrics_enabled,
@@ -73,6 +74,8 @@
7374
from typing_extensions import Unpack
7475

7576
from sentry_sdk._types import (
77+
Attributes,
78+
AttributeValue,
7679
Breadcrumb,
7780
BreadcrumbHint,
7881
ErrorProcessor,
@@ -230,6 +233,7 @@ class Scope:
230233
"_type",
231234
"_last_event_id",
232235
"_flags",
236+
"_attributes",
233237
)
234238

235239
def __init__(
@@ -296,6 +300,8 @@ def __copy__(self) -> "Scope":
296300

297301
rv._flags = deepcopy(self._flags)
298302

303+
rv._attributes = self._attributes.copy()
304+
299305
return rv
300306

301307
@classmethod
@@ -684,6 +690,8 @@ def clear(self) -> None:
684690
self._last_event_id: "Optional[str]" = None
685691
self._flags: "Optional[FlagBuffer]" = None
686692

693+
self._attributes: "Attributes" = {}
694+
687695
@_attr_setter
688696
def level(self, value: "LogLevelStr") -> None:
689697
"""
@@ -1487,6 +1495,14 @@ def _apply_global_attributes_to_telemetry(
14871495
if release is not None and "sentry.release" not in attributes:
14881496
attributes["sentry.release"] = release
14891497

1498+
def _apply_scope_attributes_to_telemetry(
1499+
self, telemetry: "Union[Log, Metric]"
1500+
) -> None:
1501+
attributes = telemetry["attributes"]
1502+
1503+
for attribute, value in self._attributes.items():
1504+
attributes[attribute] = value
1505+
14901506
def _apply_user_attributes_to_telemetry(
14911507
self, telemetry: "Union[Log, Metric]"
14921508
) -> None:
@@ -1622,6 +1638,7 @@ def apply_to_telemetry(self, telemetry: "Union[Log, Metric]") -> None:
16221638
telemetry["span_id"] = span_id
16231639

16241640
self._apply_global_attributes_to_telemetry(telemetry)
1641+
self._apply_scope_attributes_to_telemetry(telemetry)
16251642
self._apply_user_attributes_to_telemetry(telemetry)
16261643

16271644
def update_from_scope(self, scope: "Scope") -> None:
@@ -1668,6 +1685,8 @@ def update_from_scope(self, scope: "Scope") -> None:
16681685
else:
16691686
for flag in scope._flags.get():
16701687
self._flags.set(flag["flag"], flag["result"])
1688+
if scope._attributes:
1689+
self._attributes.update(scope._attributes)
16711690

16721691
def update_from_kwargs(
16731692
self,
@@ -1677,6 +1696,7 @@ def update_from_kwargs(
16771696
contexts: "Optional[Dict[str, Dict[str, Any]]]" = None,
16781697
tags: "Optional[Dict[str, str]]" = None,
16791698
fingerprint: "Optional[List[str]]" = None,
1699+
attributes: "Optional[Attributes]" = None,
16801700
) -> None:
16811701
"""Update the scope's attributes."""
16821702
if level is not None:
@@ -1691,6 +1711,8 @@ def update_from_kwargs(
16911711
self._tags.update(tags)
16921712
if fingerprint is not None:
16931713
self._fingerprint = fingerprint
1714+
if attributes is not None:
1715+
self._attributes.update(attributes)
16941716

16951717
def __repr__(self) -> str:
16961718
return "<%s id=%s name=%s type=%s>" % (
@@ -1710,6 +1732,15 @@ def flags(self) -> "FlagBuffer":
17101732
self._flags = FlagBuffer(capacity=max_flags)
17111733
return self._flags
17121734

1735+
def set_attribute(self, attribute: str, value: "AttributeValue") -> None:
1736+
self._attributes[attribute] = format_attribute(value)
1737+
1738+
def remove_attribute(self, attribute: str) -> None:
1739+
try:
1740+
del self._attributes[attribute]
1741+
except KeyError:
1742+
pass
1743+
17131744

17141745
@contextmanager
17151746
def new_scope() -> "Generator[Scope, None, None]":

sentry_sdk/utils.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2047,7 +2047,25 @@ def get_before_send_metric(
20472047
)
20482048

20492049

2050+
def format_attribute(val: "Any") -> "AttributeValue":
2051+
"""
2052+
Turn unsupported attribute value types into an AttributeValue.
2053+
2054+
We do this as soon as a user-provided attribute is set, to prevent spans,
2055+
logs, ßmetrics and similar from having live references to various ßobjects.
2056+
2057+
Note: This is not the final attribute value format. Before they're sent,
2058+
they're serialized further into the actual format the protocol expects:
2059+
https://develop.sentry.dev/sdk/telemetry/attributes/
2060+
"""
2061+
if isinstance(val, (bool, int, float, str)):
2062+
return val
2063+
2064+
return safe_repr(val)
2065+
2066+
20502067
def serialize_attribute(val: "AttributeValue") -> "SerializedAttributeValue":
2068+
"""Serialize attribute value to the transport format."""
20512069
if isinstance(val, bool):
20522070
return {"value": val, "type": "boolean"}
20532071
if isinstance(val, int):
@@ -2057,5 +2075,6 @@ def serialize_attribute(val: "AttributeValue") -> "SerializedAttributeValue":
20572075
if isinstance(val, str):
20582076
return {"value": val, "type": "string"}
20592077

2060-
# Coerce to string if we don't know what to do with the value
2078+
# Coerce to string if we don't know what to do with the value. This should
2079+
# never happen as we pre-format early in format_attribute, but let's be safe.
20612080
return {"value": safe_repr(val), "type": "string"}

tests/test_logs.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -548,3 +548,50 @@ def record_lost_event(reason, data_category=None, item=None, *, quantity=1):
548548
}
549549
]
550550
}
551+
552+
553+
def test_log_gets_attributes_from_scopes(sentry_init, capture_envelopes):
554+
sentry_init(enable_logs=True)
555+
556+
envelopes = capture_envelopes()
557+
558+
global_scope = sentry_sdk.get_global_scope()
559+
global_scope.set_attribute("global.attribute", "value")
560+
561+
with sentry_sdk.new_scope() as scope:
562+
scope.set_attribute("current.attribute", "value")
563+
python_logger = logging.Logger("test-logger")
564+
python_logger.warning("Hello, world!")
565+
566+
python_logger.warning("Hello, world!")
567+
568+
get_client().flush()
569+
570+
logs = envelopes_to_logs(envelopes)
571+
(log1, log2) = logs
572+
573+
assert log1["attributes"]["global.attribute"] == "value"
574+
assert log1["attributes"]["current.attribute"] == "value"
575+
576+
assert log2["attributes"]["temp.attribute"] == "value"
577+
assert "current.attribute" not in log2["attributes"]
578+
579+
580+
def test_log_attributes_override_scope_attributes(sentry_init, capture_envelopes):
581+
sentry_init(enable_logs=True)
582+
583+
envelopes = capture_envelopes()
584+
585+
with sentry_sdk.new_scope() as scope:
586+
scope.set_attribute("durable.attribute", "value1")
587+
scope.set_attribute("temp.attribute", "value1")
588+
python_logger = logging.Logger("test-logger")
589+
python_logger.warning("Hello, world!", attributes={"temp.attribute": "value2"})
590+
591+
get_client().flush()
592+
593+
logs = envelopes_to_logs(envelopes)
594+
(log,) = logs
595+
596+
assert log["attributes"]["durable.attribute"] == "value1"
597+
assert log["attributes"]["temp.attribute"] == "value2"

tests/test_metrics.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,3 +290,48 @@ def record_lost_event(reason, data_category, quantity):
290290
assert len(lost_event_calls) == 5
291291
for lost_event_call in lost_event_calls:
292292
assert lost_event_call == ("queue_overflow", "trace_metric", 1)
293+
294+
295+
def test_metric_gets_attributes_from_scopes(sentry_init, capture_envelopes):
296+
sentry_init()
297+
298+
envelopes = capture_envelopes()
299+
300+
global_scope = sentry_sdk.get_global_scope()
301+
global_scope.set_attribute("global.attribute", "value")
302+
303+
with sentry_sdk.new_scope() as scope:
304+
scope.set_attribute("current.attribute", "value")
305+
sentry_sdk.metrics.count("test", 1)
306+
307+
sentry_sdk.metrics.count("test", 1)
308+
309+
get_client().flush()
310+
311+
metrics = envelopes_to_metrics(envelopes)
312+
(metric1, metric2) = metrics
313+
314+
assert metric1["attributes"]["global.attribute"] == "value"
315+
assert metric1["attributes"]["current.attribute"] == "value"
316+
317+
assert metric2["attributes"]["temp.attribute"] == "value"
318+
assert "current.attribute" not in metric2["attributes"]
319+
320+
321+
def test_metric_attributes_override_scope_attributes(sentry_init, capture_envelopes):
322+
sentry_init()
323+
324+
envelopes = capture_envelopes()
325+
326+
with sentry_sdk.new_scope() as scope:
327+
scope.set_attribute("durable.attribute", "value1")
328+
scope.set_attribute("temp.attribute", "value1")
329+
sentry_sdk.metrics.count("test", 1)
330+
331+
get_client().flush()
332+
333+
metrics = envelopes_to_metrics(envelopes)
334+
(metric,) = metrics
335+
336+
assert metric["attributes"]["durable.attribute"] == "value1"
337+
assert metric["attributes"]["temp.attribute"] == "value2"

0 commit comments

Comments
 (0)