Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 6 additions & 8 deletions ddtrace/_trace/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@
from typing import Tuple

from ddtrace._trace._span_link import SpanLink
from ddtrace._trace.types import _MetaDictType
from ddtrace._trace.types import _MetricDictType
from ddtrace.constants import _ORIGIN_KEY
from ddtrace.constants import _SAMPLING_PRIORITY_KEY
from ddtrace.constants import _USER_ID_KEY
Expand All @@ -25,8 +23,8 @@
_ContextState = Tuple[
Optional[int], # trace_id
Optional[int], # span_id
_MetaDictType, # _meta
_MetricDictType, # _metrics
Dict[str, str], # _meta
Dict[str, NumericType], # _metrics
List[SpanLink], # span_links
Dict[str, Any], # baggage
bool, # is_remote
Expand Down Expand Up @@ -63,15 +61,15 @@ def __init__(
span_id: Optional[int] = None,
dd_origin: Optional[str] = None,
sampling_priority: Optional[float] = None,
meta: Optional[_MetaDictType] = None,
metrics: Optional[_MetricDictType] = None,
meta: Optional[Dict[str, str]] = None,
metrics: Optional[Dict[str, NumericType]] = None,
lock: Optional[threading.RLock] = None,
span_links: Optional[List[SpanLink]] = None,
baggage: Optional[Dict[str, Any]] = None,
is_remote: bool = True,
):
self._meta: _MetaDictType = meta if meta is not None else {}
self._metrics: _MetricDictType = metrics if metrics is not None else {}
self._meta: Dict[str, str] = meta if meta is not None else {}
self._metrics: Dict[str, NumericType] = metrics if metrics is not None else {}
self._baggage: Dict[str, Any] = baggage if baggage is not None else {}

self.trace_id: Optional[int] = trace_id
Expand Down
127 changes: 32 additions & 95 deletions ddtrace/_trace/span.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,6 @@
from ddtrace._trace._span_pointer import _SpanPointerDirection
from ddtrace._trace.context import Context
from ddtrace._trace.types import _AttributeValueType
from ddtrace._trace.types import _MetaDictType
from ddtrace._trace.types import _MetricDictType
from ddtrace._trace.types import _TagNameType
from ddtrace.constants import _SAMPLING_AGENT_DECISION
from ddtrace.constants import _SAMPLING_LIMIT_DECISION
from ddtrace.constants import _SAMPLING_RULE_DECISION
Expand All @@ -37,14 +34,10 @@
from ddtrace.constants import USER_KEEP
from ddtrace.constants import USER_REJECT
from ddtrace.constants import VERSION_KEY
from ddtrace.ext import http
from ddtrace.ext import net
from ddtrace.internal import core
from ddtrace.internal._rand import rand64bits as _rand64bits
from ddtrace.internal._rand import rand128bits as _rand128bits
from ddtrace.internal.compat import NumericType
from ddtrace.internal.compat import ensure_text
from ddtrace.internal.compat import is_integer
from ddtrace.internal.constants import MAX_INT_64BITS as _MAX_INT_64BITS
from ddtrace.internal.constants import MAX_UINT_64BITS as _MAX_UINT_64BITS
from ddtrace.internal.constants import MIN_INT_64BITS as _MIN_INT_64BITS
Expand Down Expand Up @@ -191,9 +184,9 @@ def __init__(
self.span_type = span_type
self._span_api = span_api

self._meta: _MetaDictType = {}
self._meta: Dict[str, str] = {}
self.error = 0
self._metrics: _MetricDictType = {}
self._metrics: Dict[str, NumericType] = {}

self._meta_struct: Dict[str, Dict[str, Any]] = {}

Expand Down Expand Up @@ -335,51 +328,17 @@ def _set_sampling_decision_maker(
self.context._meta[SAMPLING_DECISION_TRACE_TAG_KEY] = value
return value

def set_tag(self, key: _TagNameType, value: Any = None) -> None:
def set_tag(self, key: str, value: Optional[str] = None) -> None:
"""Set a tag key/value pair on the span.

Keys must be strings, values must be ``str``-able.
Keys must be strings, values must be ``str``.

:param key: Key to use for the tag
:type key: str
:type key: ``str``
:param value: Value to assign for the tag
:type value: ``str``-able value
:type value: ``str`` | `None``
"""

if not isinstance(key, str):
log.warning("Ignoring tag pair %s:%s. Key must be a string.", key, value)
return

# Special case, force `http.status_code` as a string
# DEV: `http.status_code` *has* to be in `meta` for metrics
# calculated in the trace agent
if key == http.STATUS_CODE:
value = str(value)

# Determine once up front
val_is_an_int = is_integer(value)

# Explicitly try to convert expected integers to `int`
# DEV: Some integrations parse these values from strings, but don't call `int(value)` themselves
INT_TYPES = (net.TARGET_PORT,)
if key in INT_TYPES and not val_is_an_int:
try:
value = int(value)
val_is_an_int = True
except (ValueError, TypeError):
pass

# Set integers that are less than equal to 2^53 as metrics
if value is not None and val_is_an_int and abs(value) <= 2**53:
self.set_metric(key, value)
return

# All floats should be set as a metric
elif isinstance(value, float):
self.set_metric(key, value)
return

elif key == MANUAL_KEEP_KEY:
if key == MANUAL_KEEP_KEY:
self._override_sampling_decision(USER_KEEP)
return
elif key == MANUAL_DROP_KEY:
Expand All @@ -390,21 +349,17 @@ def set_tag(self, key: _TagNameType, value: Any = None) -> None:
elif key == SERVICE_VERSION_KEY:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we handle these tags in a trace processor or in Logs backend? It seems unnecessary to do this check every time set_tag is called.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good question... probably, but this change is started to get bloated a bit anyways, so maybe better to handle as a follow up.

it is an internal implementation detail, so... 🤷🏻

# Also set the `version` tag to the same value
# DEV: Note that we do no return, we want to set both
self.set_tag(VERSION_KEY, value)
elif key == _SPAN_MEASURED_KEY:
# Set `_dd.measured` tag as a metric
# DEV: `set_metric` will ensure it is an integer 0 or 1
if value is None:
value = 1
self.set_metric(key, value)
return
if value:
self._meta[VERSION_KEY] = value
else:
self._meta.pop(VERSION_KEY, None)

try:
self._meta[key] = str(value)
if key in self._metrics:
del self._metrics[key]
except Exception:
log.warning("error setting tag %s, ignoring it", key, exc_info=True)
if value is not None:
self._meta[key] = value
else:
self._meta.pop(key, None)
# Ensure we do not have the same key in both meta and metrics
self._metrics.pop(key, None)

def set_struct_tag(self, key: str, value: Dict[str, Any]) -> None:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this method be public or is this functionality internal to the ddtrace library

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there is another PR to deprecate and internalize this

"""
Expand All @@ -417,35 +372,29 @@ def get_struct_tag(self, key: str) -> Optional[Dict[str, Any]]:
"""Return the given struct or None if it doesn't exist."""
return self._meta_struct.get(key, None)

def set_tag_str(self, key: _TagNameType, value: Text) -> None:
def set_tag_str(self, key: str, value: str) -> None:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we deprecate/internalize this method? Ideally we should expose one public API for setting tags. If the overhead of handling sampling and service naming logic is significant we can move this logic out of set tags in v5.0

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have another PR to deprecate and internalize it. With the changes to set_tag we might be able to just get rid of it entirely.

"""Set a value for a tag. Values are coerced to unicode in Python 2 and
str in Python 3, with decoding errors in conversion being replaced with
U+FFFD.
"""
try:
self._meta[key] = ensure_text(value, errors="replace")
except Exception as e:
if config._raise:
raise e
log.warning("Failed to set text tag '%s'", key, exc_info=True)
self._meta[key] = value

def get_tag(self, key: _TagNameType) -> Optional[Text]:
def get_tag(self, key: str) -> Optional[str]:
"""Return the given tag or None if it doesn't exist."""
return self._meta.get(key, None)

def get_tags(self) -> _MetaDictType:
def get_tags(self) -> Dict[str, str]:
"""Return all tags."""
return self._meta.copy()

def set_tags(self, tags: Dict[_TagNameType, Any]) -> None:
def set_tags(self, tags: Dict[str, str]) -> None:
"""Set a dictionary of tags on the given span. Keys and values
must be strings (or stringable)
"""
if tags:
for k, v in iter(tags.items()):
self.set_tag(k, v)
for k, v in tags.items():
self.set_tag(k, v)

def set_metric(self, key: _TagNameType, value: NumericType) -> None:
def set_metric(self, key: str, value: NumericType) -> None:
"""This method sets a numeric tag value for the given key."""
# Enforce a specific constant for `_dd.measured`
if key == _SPAN_MEASURED_KEY:
Expand All @@ -455,35 +404,23 @@ def set_metric(self, key: _TagNameType, value: NumericType) -> None:
log.warning("failed to convert %r tag to an integer from %r", key, value)
return

# FIXME[matt] we could push this check to serialization time as well.
# only permit types that are commonly serializable (don't use
# isinstance so that we convert unserializable types like numpy
# numbers)
if not isinstance(value, (int, float)):
try:
value = float(value)
except (ValueError, TypeError):
log.debug("ignoring not number metric %s:%s", key, value)
return

# don't allow nan or inf
if math.isnan(value) or math.isinf(value):
log.debug("ignoring not real metric %s:%s", key, value)
return

if key in self._meta:
del self._meta[key]
self._metrics[key] = value
# Ensure we do not have the same key in both meta and metrics
self._meta.pop(key, None)

def set_metrics(self, metrics: _MetricDictType) -> None:
def set_metrics(self, metrics: Dict[str, NumericType]) -> None:
"""Set a dictionary of metrics on the given span. Keys must be
must be strings (or stringable). Values must be numeric.
"""
if metrics:
for k, v in metrics.items():
self.set_metric(k, v)
for k, v in metrics.items():
self.set_metric(k, v)

def get_metric(self, key: _TagNameType) -> Optional[NumericType]:
def get_metric(self, key: str) -> Optional[NumericType]:
"""Return the given metric or None if it doesn't exist."""
return self._metrics.get(key)

Expand All @@ -496,7 +433,7 @@ def _add_on_finish_exception_callback(self, callback: Callable[["Span"], None]):
"""Add an errortracking related callback to the on_finish_callback array"""
self._on_finish_callbacks.insert(0, callback)

def get_metrics(self) -> _MetricDictType:
def get_metrics(self) -> Dict[str, NumericType]:
"""Return all metrics."""
return self._metrics.copy()

Expand Down
7 changes: 0 additions & 7 deletions ddtrace/_trace/types.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,7 @@
from typing import Dict
from typing import Sequence
from typing import Text
from typing import Union

from ddtrace.internal.compat import NumericType


_TagNameType = Union[Text, bytes]
_MetaDictType = Dict[_TagNameType, Text]
_MetricDictType = Dict[_TagNameType, NumericType]
_AttributeValueType = Union[
str,
bool,
Expand Down
2 changes: 1 addition & 1 deletion ddtrace/_trace/utils_botocore/aws_payload_tagging.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ def _tag_object(self, span: Span, key: str, obj: Any, depth: int = 0) -> None:
"""
# if we've hit the maximum allowed tags, mark the expansion as incomplete
if self.current_tag_count >= config.botocore.get("payload_tagging_max_tags"):
span.set_tag(self._INCOMPLETE_TAG, True)
span.set_tag(self._INCOMPLETE_TAG, str(True))
return
if obj is None:
self.current_tag_count += 1
Expand Down
11 changes: 6 additions & 5 deletions ddtrace/appsec/ai_guard/_api_client.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""AI Guard client for security evaluation of agentic AI workflows."""

import json
from typing import Any
from typing import Dict
Expand Down Expand Up @@ -104,11 +105,11 @@ def evaluate_tool(
tool_name: str,
tool_args: Dict[Union[Text, bytes], Any],
output: Optional[str] = None,
tags: Optional[Dict[Union[Text, bytes], Any]] = None,
tags: Optional[Dict[str, str]] = None,
) -> bool:
return self._client.evaluate_tool(tool_name, tool_args, output=output, history=self._history, tags=tags)

def evaluate_prompt(self, role: str, content: str, tags: Optional[Dict[Union[Text, bytes], Any]] = None) -> bool:
def evaluate_prompt(self, role: str, content: str, tags: Optional[Dict[str, str]] = None) -> bool:
return self._client.evaluate_prompt(role, content, history=self._history, tags=tags)


Expand Down Expand Up @@ -149,7 +150,7 @@ def evaluate_tool(
tool_args: Dict[Union[Text, bytes], Any],
output: Optional[str] = None,
history: Optional[List[Evaluation]] = None,
tags: Optional[Dict[Union[Text, bytes], Any]] = None,
tags: Optional[Dict[str, str]] = None,
) -> bool:
"""Evaluate if a tool call is safe to execute.

Expand Down Expand Up @@ -187,7 +188,7 @@ def evaluate_prompt(
content: str,
output: Optional[str] = None,
history: Optional[List[Evaluation]] = None,
tags: Optional[Dict[Union[Text, bytes], Any]] = None,
tags: Optional[Dict[str, str]] = None,
) -> bool:
"""Evaluate if a prompt is safe to execute.

Expand Down Expand Up @@ -269,7 +270,7 @@ def truncate_content(evaluation: Evaluation) -> Evaluation:
TELEMETRY_NAMESPACE.APPSEC, AI_GUARD.TRUNCATED_METRIC, 1, (("type", "content"),)
)

def _evaluate(self, current: Evaluation, history: List[Evaluation], tags: Dict[Union[Text, bytes], Any]) -> bool:
def _evaluate(self, current: Evaluation, history: List[Evaluation], tags: Dict[str, str]) -> bool:
"""Send evaluation request to AI Guard service."""
with self._tracer.trace(AI_GUARD.RESOURCE_TYPE) as span:
if tags is not None:
Expand Down
8 changes: 7 additions & 1 deletion ddtrace/contrib/internal/trace_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -617,7 +617,13 @@ def set_flattened_tags(
# type: (...) -> None
for prefix, value in items:
for tag, v in _flatten(value, sep, prefix, exclude_policy):
span.set_tag(tag, processor(v) if processor is not None else v)
v = processor(v) if processor is not None else v
if isinstance(v, str):
span.set_tag(tag, v)
elif isinstance(v, (int, float)):
span.set_metric(tag, v)
else:
span.set_tag(tag, str(v))


def extract_netloc_and_query_info_from_url(url):
Expand Down
11 changes: 9 additions & 2 deletions ddtrace/contrib/internal/trace_utils_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,15 +142,22 @@ def set_user(
)


def _set_url_tag(integration_config: IntegrationConfig, span: Span, url: str, query: str) -> None:
def _set_url_tag(integration_config: IntegrationConfig, span: Span, url: str, query: Optional[str]) -> None:
if not integration_config.http_tag_query_string:
span.set_tag_str(http.URL, strip_query_string(url))
return
elif config._global_query_string_obfuscation_disabled:
# TODO(munir): This case exists for backwards compatibility. To remove query strings from URLs,
# users should set ``DD_TRACE_HTTP_CLIENT_TAG_QUERY_STRING=False``. This case should be
# removed when config.global_query_string_obfuscation_disabled is removed (v3.0).
span.set_tag_str(http.URL, url)
elif getattr(config._obfuscation_query_string_pattern, "pattern", None) == b"":
return

if config._obfuscation_query_string_pattern is None:
span.set_tag_str(http.URL, strip_query_string(url))
return

if config._obfuscation_query_string_pattern.pattern == "":
# obfuscation is disabled when DD_TRACE_OBFUSCATION_QUERY_STRING_REGEXP=""
span.set_tag_str(http.URL, strip_query_string(url))
else:
Expand Down
2 changes: 1 addition & 1 deletion ddtrace/debugging/_signal/tracing.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ def enter(self, scope: t.Mapping[str, t.Any]) -> None:
)
span = self._span_cm.__enter__()

span.set_tags(probe.tags) # type: ignore[arg-type]
span.set_tags(probe.tags)
span.set_tag_str(PROBE_ID_TAG_NAME, probe.probe_id)
span.set_tag_str(_ORIGIN_KEY, "di")

Expand Down
2 changes: 1 addition & 1 deletion ddtrace/internal/ci_visibility/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

class TraceCiVisibilityFilter(TraceFilter):
def __init__(self, tags, service):
# type: (Dict[Union[str, bytes], str], str) -> None
# type: (Dict[str, str], str) -> None
self._tags = tags
self._service = service

Expand Down
Loading
Loading