ddtrace 3.11.0rc1__cp313-cp313-musllinux_1_2_aarch64.whl → 3.11.0rc3__cp313-cp313-musllinux_1_2_aarch64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of ddtrace might be problematic. Click here for more details.
- ddtrace/_logger.py +5 -6
- ddtrace/_trace/product.py +1 -1
- ddtrace/_trace/sampling_rule.py +25 -33
- ddtrace/_trace/trace_handlers.py +12 -50
- ddtrace/_trace/utils_botocore/span_tags.py +48 -0
- ddtrace/_version.py +2 -2
- ddtrace/appsec/_asm_request_context.py +3 -1
- ddtrace/appsec/_constants.py +7 -0
- ddtrace/appsec/_handlers.py +11 -0
- ddtrace/appsec/_iast/_listener.py +12 -2
- ddtrace/appsec/_processor.py +1 -1
- ddtrace/contrib/integration_registry/registry.yaml +10 -0
- ddtrace/contrib/internal/aiobotocore/patch.py +8 -0
- ddtrace/contrib/internal/avro/__init__.py +17 -0
- ddtrace/contrib/internal/azure_functions/patch.py +23 -12
- ddtrace/contrib/internal/azure_functions/utils.py +14 -0
- ddtrace/contrib/internal/boto/patch.py +14 -0
- ddtrace/contrib/internal/botocore/__init__.py +153 -0
- ddtrace/contrib/internal/botocore/services/bedrock.py +3 -27
- ddtrace/contrib/internal/django/patch.py +31 -8
- ddtrace/contrib/{_freezegun.py → internal/freezegun/__init__.py} +1 -1
- ddtrace/contrib/internal/google_genai/_utils.py +2 -2
- ddtrace/contrib/internal/google_genai/patch.py +7 -7
- ddtrace/contrib/internal/google_generativeai/patch.py +7 -5
- ddtrace/contrib/internal/langchain/patch.py +11 -443
- ddtrace/contrib/internal/langchain/utils.py +0 -26
- ddtrace/contrib/internal/logbook/patch.py +1 -2
- ddtrace/contrib/internal/logging/patch.py +4 -7
- ddtrace/contrib/internal/loguru/patch.py +1 -3
- ddtrace/contrib/internal/openai_agents/patch.py +44 -1
- ddtrace/contrib/internal/protobuf/__init__.py +17 -0
- ddtrace/contrib/internal/pytest/__init__.py +62 -0
- ddtrace/contrib/internal/pytest/_plugin_v2.py +13 -4
- ddtrace/contrib/internal/pytest_bdd/__init__.py +23 -0
- ddtrace/contrib/internal/pytest_benchmark/__init__.py +3 -0
- ddtrace/contrib/internal/structlog/patch.py +2 -4
- ddtrace/contrib/internal/unittest/__init__.py +36 -0
- ddtrace/contrib/internal/vertexai/patch.py +7 -5
- ddtrace/ext/ci.py +20 -0
- ddtrace/ext/git.py +66 -11
- ddtrace/internal/_encoding.cpython-313-aarch64-linux-musl.so +0 -0
- ddtrace/internal/_encoding.pyi +1 -1
- ddtrace/internal/ci_visibility/encoder.py +126 -49
- ddtrace/internal/ci_visibility/utils.py +4 -4
- ddtrace/internal/core/__init__.py +5 -2
- ddtrace/internal/endpoints.py +76 -0
- ddtrace/internal/schema/processor.py +6 -2
- ddtrace/internal/telemetry/writer.py +18 -0
- ddtrace/internal/test_visibility/coverage_lines.py +4 -4
- ddtrace/internal/writer/writer.py +24 -11
- ddtrace/llmobs/_constants.py +3 -0
- ddtrace/llmobs/_experiment.py +75 -10
- ddtrace/llmobs/_integrations/bedrock.py +4 -0
- ddtrace/llmobs/_integrations/bedrock_agents.py +5 -1
- ddtrace/llmobs/_integrations/crewai.py +52 -3
- ddtrace/llmobs/_integrations/gemini.py +7 -7
- ddtrace/llmobs/_integrations/google_genai.py +10 -10
- ddtrace/llmobs/_integrations/{google_genai_utils.py → google_utils.py} +103 -7
- ddtrace/llmobs/_integrations/langchain.py +29 -20
- ddtrace/llmobs/_integrations/openai_agents.py +145 -0
- ddtrace/llmobs/_integrations/pydantic_ai.py +67 -26
- ddtrace/llmobs/_integrations/utils.py +68 -158
- ddtrace/llmobs/_integrations/vertexai.py +8 -8
- ddtrace/llmobs/_llmobs.py +83 -14
- ddtrace/llmobs/_telemetry.py +20 -5
- ddtrace/llmobs/_utils.py +27 -0
- ddtrace/settings/_config.py +1 -2
- ddtrace/settings/asm.py +9 -2
- ddtrace/settings/profiling.py +0 -9
- {ddtrace-3.11.0rc1.dist-info → ddtrace-3.11.0rc3.dist-info}/METADATA +1 -1
- {ddtrace-3.11.0rc1.dist-info → ddtrace-3.11.0rc3.dist-info}/RECORD +154 -160
- ddtrace/contrib/_avro.py +0 -17
- ddtrace/contrib/_botocore.py +0 -153
- ddtrace/contrib/_protobuf.py +0 -17
- ddtrace/contrib/_pytest.py +0 -62
- ddtrace/contrib/_pytest_bdd.py +0 -23
- ddtrace/contrib/_pytest_benchmark.py +0 -3
- ddtrace/contrib/_unittest.py +0 -36
- /ddtrace/contrib/{_aiobotocore.py → internal/aiobotocore/__init__.py} +0 -0
- /ddtrace/contrib/{_aiohttp_jinja2.py → internal/aiohttp_jinja2/__init__.py} +0 -0
- /ddtrace/contrib/{_aiomysql.py → internal/aiomysql/__init__.py} +0 -0
- /ddtrace/contrib/{_aiopg.py → internal/aiopg/__init__.py} +0 -0
- /ddtrace/contrib/{_aioredis.py → internal/aioredis/__init__.py} +0 -0
- /ddtrace/contrib/{_algoliasearch.py → internal/algoliasearch/__init__.py} +0 -0
- /ddtrace/contrib/{_anthropic.py → internal/anthropic/__init__.py} +0 -0
- /ddtrace/contrib/{_aredis.py → internal/aredis/__init__.py} +0 -0
- /ddtrace/contrib/{_asyncio.py → internal/asyncio/__init__.py} +0 -0
- /ddtrace/contrib/{_asyncpg.py → internal/asyncpg/__init__.py} +0 -0
- /ddtrace/contrib/{_aws_lambda.py → internal/aws_lambda/__init__.py} +0 -0
- /ddtrace/contrib/{_azure_functions.py → internal/azure_functions/__init__.py} +0 -0
- /ddtrace/contrib/{_azure_servicebus.py → internal/azure_servicebus/__init__.py} +0 -0
- /ddtrace/contrib/{_boto.py → internal/boto/__init__.py} +0 -0
- /ddtrace/contrib/{_cassandra.py → internal/cassandra/__init__.py} +0 -0
- /ddtrace/contrib/{_consul.py → internal/consul/__init__.py} +0 -0
- /ddtrace/contrib/{_coverage.py → internal/coverage/__init__.py} +0 -0
- /ddtrace/contrib/{_crewai.py → internal/crewai/__init__.py} +0 -0
- /ddtrace/contrib/{_django.py → internal/django/__init__.py} +0 -0
- /ddtrace/contrib/{_dogpile_cache.py → internal/dogpile_cache/__init__.py} +0 -0
- /ddtrace/contrib/{_dramatiq.py → internal/dramatiq/__init__.py} +0 -0
- /ddtrace/contrib/{_elasticsearch.py → internal/elasticsearch/__init__.py} +0 -0
- /ddtrace/contrib/{_fastapi.py → internal/fastapi/__init__.py} +0 -0
- /ddtrace/contrib/{_flask.py → internal/flask/__init__.py} +0 -0
- /ddtrace/contrib/{_futures.py → internal/futures/__init__.py} +0 -0
- /ddtrace/contrib/{_gevent.py → internal/gevent/__init__.py} +0 -0
- /ddtrace/contrib/{_google_genai.py → internal/google_genai/__init__.py} +0 -0
- /ddtrace/contrib/{_google_generativeai.py → internal/google_generativeai/__init__.py} +0 -0
- /ddtrace/contrib/{_graphql.py → internal/graphql/__init__.py} +0 -0
- /ddtrace/contrib/{_grpc.py → internal/grpc/__init__.py} +0 -0
- /ddtrace/contrib/{_gunicorn.py → internal/gunicorn/__init__.py} +0 -0
- /ddtrace/contrib/{_httplib.py → internal/httplib/__init__.py} +0 -0
- /ddtrace/contrib/{_httpx.py → internal/httpx/__init__.py} +0 -0
- /ddtrace/contrib/{_jinja2.py → internal/jinja2/__init__.py} +0 -0
- /ddtrace/contrib/{_kafka.py → internal/kafka/__init__.py} +0 -0
- /ddtrace/contrib/{_kombu.py → internal/kombu/__init__.py} +0 -0
- /ddtrace/contrib/{_langchain.py → internal/langchain/__init__.py} +0 -0
- /ddtrace/contrib/{_langgraph.py → internal/langgraph/__init__.py} +0 -0
- /ddtrace/contrib/{_litellm.py → internal/litellm/__init__.py} +0 -0
- /ddtrace/contrib/{_logbook.py → internal/logbook/__init__.py} +0 -0
- /ddtrace/contrib/{_logging.py → internal/logging/__init__.py} +0 -0
- /ddtrace/contrib/{_loguru.py → internal/loguru/__init__.py} +0 -0
- /ddtrace/contrib/{_mako.py → internal/mako/__init__.py} +0 -0
- /ddtrace/contrib/{_mariadb.py → internal/mariadb/__init__.py} +0 -0
- /ddtrace/contrib/{_mcp.py → internal/mcp/__init__.py} +0 -0
- /ddtrace/contrib/{_molten.py → internal/molten/__init__.py} +0 -0
- /ddtrace/contrib/{_mongoengine.py → internal/mongoengine/__init__.py} +0 -0
- /ddtrace/contrib/{_mysql.py → internal/mysql/__init__.py} +0 -0
- /ddtrace/contrib/{_mysqldb.py → internal/mysqldb/__init__.py} +0 -0
- /ddtrace/contrib/{_openai.py → internal/openai/__init__.py} +0 -0
- /ddtrace/contrib/{_openai_agents.py → internal/openai_agents/__init__.py} +0 -0
- /ddtrace/contrib/{_psycopg.py → internal/psycopg/__init__.py} +0 -0
- /ddtrace/contrib/{_pydantic_ai.py → internal/pydantic_ai/__init__.py} +0 -0
- /ddtrace/contrib/{_pymemcache.py → internal/pymemcache/__init__.py} +0 -0
- /ddtrace/contrib/{_pymongo.py → internal/pymongo/__init__.py} +0 -0
- /ddtrace/contrib/{_pymysql.py → internal/pymysql/__init__.py} +0 -0
- /ddtrace/contrib/{_pynamodb.py → internal/pynamodb/__init__.py} +0 -0
- /ddtrace/contrib/{_pyodbc.py → internal/pyodbc/__init__.py} +0 -0
- /ddtrace/contrib/{_redis.py → internal/redis/__init__.py} +0 -0
- /ddtrace/contrib/{_rediscluster.py → internal/rediscluster/__init__.py} +0 -0
- /ddtrace/contrib/{_rq.py → internal/rq/__init__.py} +0 -0
- /ddtrace/contrib/{_sanic.py → internal/sanic/__init__.py} +0 -0
- /ddtrace/contrib/{_selenium.py → internal/selenium/__init__.py} +0 -0
- /ddtrace/contrib/{_snowflake.py → internal/snowflake/__init__.py} +0 -0
- /ddtrace/contrib/{_sqlite3.py → internal/sqlite3/__init__.py} +0 -0
- /ddtrace/contrib/{_starlette.py → internal/starlette/__init__.py} +0 -0
- /ddtrace/contrib/{_structlog.py → internal/structlog/__init__.py} +0 -0
- /ddtrace/contrib/{_subprocess.py → internal/subprocess/__init__.py} +0 -0
- /ddtrace/contrib/{_urllib.py → internal/urllib/__init__.py} +0 -0
- /ddtrace/contrib/{_urllib3.py → internal/urllib3/__init__.py} +0 -0
- /ddtrace/contrib/{_vertexai.py → internal/vertexai/__init__.py} +0 -0
- /ddtrace/contrib/{_vertica.py → internal/vertica/__init__.py} +0 -0
- /ddtrace/contrib/{_webbrowser.py → internal/webbrowser/__init__.py} +0 -0
- /ddtrace/contrib/{_yaaredis.py → internal/yaaredis/__init__.py} +0 -0
- {ddtrace-3.11.0rc1.dist-info → ddtrace-3.11.0rc3.dist-info}/WHEEL +0 -0
- {ddtrace-3.11.0rc1.dist-info → ddtrace-3.11.0rc3.dist-info}/entry_points.txt +0 -0
- {ddtrace-3.11.0rc1.dist-info → ddtrace-3.11.0rc3.dist-info}/licenses/LICENSE +0 -0
- {ddtrace-3.11.0rc1.dist-info → ddtrace-3.11.0rc3.dist-info}/licenses/LICENSE.Apache +0 -0
- {ddtrace-3.11.0rc1.dist-info → ddtrace-3.11.0rc3.dist-info}/licenses/LICENSE.BSD3 +0 -0
- {ddtrace-3.11.0rc1.dist-info → ddtrace-3.11.0rc3.dist-info}/licenses/NOTICE +0 -0
- {ddtrace-3.11.0rc1.dist-info → ddtrace-3.11.0rc3.dist-info}/top_level.txt +0 -0
ddtrace/_logger.py
CHANGED
@@ -20,6 +20,7 @@ class LogInjectionState(object):
|
|
20
20
|
# Log injection is enabled, but not yet configured
|
21
21
|
ENABLED = "true"
|
22
22
|
# Log injection is enabled and configured for structured logging
|
23
|
+
# This value is deprecated, but kept for backwards compatibility
|
23
24
|
STRUCTURED = "structured"
|
24
25
|
|
25
26
|
|
@@ -108,17 +109,15 @@ def set_log_formatting():
|
|
108
109
|
handler.setFormatter(logging.Formatter(DD_LOG_FORMAT))
|
109
110
|
|
110
111
|
|
111
|
-
def get_log_injection_state(raw_config: Optional[str]) ->
|
112
|
+
def get_log_injection_state(raw_config: Optional[str]) -> bool:
|
112
113
|
"""Returns the current log injection state."""
|
113
114
|
if raw_config:
|
114
115
|
normalized = raw_config.lower().strip()
|
115
|
-
if normalized == LogInjectionState.STRUCTURED:
|
116
|
-
return
|
117
|
-
elif normalized in ("true", "1"):
|
118
|
-
return LogInjectionState.ENABLED
|
116
|
+
if normalized == LogInjectionState.STRUCTURED or normalized in ("true", "1"):
|
117
|
+
return True
|
119
118
|
elif normalized not in ("false", "0"):
|
120
119
|
logging.warning(
|
121
120
|
"Invalid log injection state '%s'. Expected 'true', 'false', or 'structured'. Defaulting to 'false'.",
|
122
121
|
normalized,
|
123
122
|
)
|
124
|
-
return
|
123
|
+
return False
|
ddtrace/_trace/product.py
CHANGED
@@ -215,7 +215,7 @@ def apm_tracing_rc(lib_config, dd_config):
|
|
215
215
|
new_rc_configs["_trace_sampling_rules"] = trace_sampling_rules
|
216
216
|
|
217
217
|
if "log_injection_enabled" in lib_config:
|
218
|
-
new_rc_configs["_logs_injection"] =
|
218
|
+
new_rc_configs["_logs_injection"] = lib_config["log_injection_enabled"]
|
219
219
|
|
220
220
|
if "tracing_tags" in lib_config:
|
221
221
|
tags = lib_config["tracing_tags"]
|
ddtrace/_trace/sampling_rule.py
CHANGED
@@ -60,7 +60,7 @@ class SamplingRule(object):
|
|
60
60
|
number of characters, and "?" meaning any one character. If all tags specified in a SamplingRule are
|
61
61
|
matches with a given span, that span is considered to have matching tags with the rule.
|
62
62
|
"""
|
63
|
-
self.sample_rate =
|
63
|
+
self.sample_rate = min(1.0, max(0.0, float(sample_rate)))
|
64
64
|
# since span.py converts None to 'None' for tags, and does not accept 'None' for metrics
|
65
65
|
# we can just create a GlobMatcher for 'None' and it will match properly
|
66
66
|
self._tag_value_matchers = (
|
@@ -111,46 +111,38 @@ class SamplingRule(object):
|
|
111
111
|
:returns: Whether this span matches or not
|
112
112
|
:rtype: :obj:`bool`
|
113
113
|
"""
|
114
|
-
tags_match
|
115
|
-
return tags_match and self._matches((span.service, span.name, span.resource))
|
114
|
+
return self.tags_match(span) and self._matches((span.service, span.name, span.resource))
|
116
115
|
|
117
116
|
def tags_match(self, span: Span) -> bool:
|
118
|
-
|
119
|
-
|
120
|
-
tag_match = self.check_tags(span.get_tags(), span.get_metrics())
|
121
|
-
return tag_match
|
117
|
+
if not self._tag_value_matchers:
|
118
|
+
return True
|
122
119
|
|
123
|
-
|
124
|
-
|
120
|
+
meta = span._meta or {}
|
121
|
+
metrics = span._metrics or {}
|
122
|
+
if not meta and not metrics:
|
125
123
|
return False
|
126
124
|
|
127
|
-
|
128
|
-
|
129
|
-
value = meta.get(tag_key)
|
130
|
-
# it's because we're not checking metrics first before continuing
|
125
|
+
for tag_key, pattern in self._tag_value_matchers.items():
|
126
|
+
value = meta.get(tag_key, metrics.get(tag_key))
|
131
127
|
if value is None:
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
#
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
tag_match = self._tag_value_matchers[tag_key].match(str(value))
|
149
|
-
# if we don't match with all specified tags for a rule, it's not a match
|
150
|
-
if tag_match is False:
|
128
|
+
# If the tag is not present, we failed the match
|
129
|
+
# (Metrics and meta do not support the value None)
|
130
|
+
return False
|
131
|
+
|
132
|
+
if isinstance(value, float):
|
133
|
+
# Floats: Convert floats that represent integers to int for matching. This is because
|
134
|
+
# SamplingRules only support integers for matfching or glob patterns.
|
135
|
+
if value.is_integer():
|
136
|
+
value = int(value)
|
137
|
+
elif set(pattern.pattern) - {"?", "*"}:
|
138
|
+
# Only match floats to patterns that only contain wildcards (ex: * or ?*)
|
139
|
+
# This is because we do not want to match floats to patterns like `23.*`.
|
140
|
+
return False
|
141
|
+
|
142
|
+
if not pattern.match(str(value)):
|
151
143
|
return False
|
152
144
|
|
153
|
-
return
|
145
|
+
return True
|
154
146
|
|
155
147
|
def sample(self, span):
|
156
148
|
"""
|
ddtrace/_trace/trace_handlers.py
CHANGED
@@ -661,13 +661,9 @@ def _on_botocore_patched_bedrock_api_call_started(ctx, request_params):
|
|
661
661
|
|
662
662
|
span.set_tag_str("bedrock.request.model_provider", ctx["model_provider"])
|
663
663
|
span.set_tag_str("bedrock.request.model", ctx["model_name"])
|
664
|
-
|
665
|
-
|
666
|
-
|
667
|
-
v = integration.trunc(str(v))
|
668
|
-
span.set_tag_str("bedrock.request.{}".format(k), str(v))
|
669
|
-
if k == "n":
|
670
|
-
ctx.set_item("num_generations", str(v))
|
664
|
+
|
665
|
+
if "n" in request_params:
|
666
|
+
ctx.set_item("num_generations", str(request_params["n"]))
|
671
667
|
|
672
668
|
|
673
669
|
def _on_botocore_patched_bedrock_api_call_exception(ctx, exc_info):
|
@@ -680,16 +676,6 @@ def _on_botocore_patched_bedrock_api_call_exception(ctx, exc_info):
|
|
680
676
|
span.finish()
|
681
677
|
|
682
678
|
|
683
|
-
def _on_botocore_patched_bedrock_api_call_success(ctx, reqid, latency, input_token_count, output_token_count):
|
684
|
-
span = ctx.span
|
685
|
-
span.set_tag_str("bedrock.response.id", reqid)
|
686
|
-
span.set_tag_str("bedrock.response.duration", latency)
|
687
|
-
if input_token_count:
|
688
|
-
span.set_metric("bedrock.response.usage.prompt_tokens", int(input_token_count))
|
689
|
-
if output_token_count:
|
690
|
-
span.set_metric("bedrock.response.usage.completion_tokens", int(output_token_count))
|
691
|
-
|
692
|
-
|
693
679
|
def _propagate_context(ctx, headers):
|
694
680
|
distributed_tracing_enabled = ctx["integration_config"].distributed_tracing_enabled
|
695
681
|
span = ctx.span
|
@@ -731,38 +717,13 @@ def _on_botocore_bedrock_process_response_converse(
|
|
731
717
|
def _on_botocore_bedrock_process_response(
|
732
718
|
ctx: core.ExecutionContext,
|
733
719
|
formatted_response: Dict[str, Any],
|
734
|
-
metadata: Dict[str, Any],
|
735
|
-
body: Dict[str, List[Dict]],
|
736
|
-
should_set_choice_ids: bool,
|
737
720
|
) -> None:
|
738
|
-
|
739
|
-
|
740
|
-
|
741
|
-
|
742
|
-
|
743
|
-
|
744
|
-
integration = ctx["bedrock_integration"]
|
745
|
-
if metadata is not None:
|
746
|
-
for k, v in metadata.items():
|
747
|
-
if k in ["usage.completion_tokens", "usage.prompt_tokens"] and v:
|
748
|
-
span.set_metric("bedrock.response.{}".format(k), int(v))
|
749
|
-
else:
|
750
|
-
span.set_tag_str("bedrock.{}".format(k), str(v))
|
751
|
-
if "embed" in model_name:
|
752
|
-
span.set_metric("bedrock.response.embedding_length", len(formatted_response["text"][0]))
|
753
|
-
span.finish()
|
754
|
-
return
|
755
|
-
for i in range(len(formatted_response["text"])):
|
756
|
-
if integration.is_pc_sampled_span(span):
|
757
|
-
span.set_tag_str(
|
758
|
-
"bedrock.response.choices.{}.text".format(i),
|
759
|
-
integration.trunc(str(formatted_response["text"][i])),
|
760
|
-
)
|
761
|
-
span.set_tag_str(
|
762
|
-
"bedrock.response.choices.{}.finish_reason".format(i), str(formatted_response["finish_reason"][i])
|
763
|
-
)
|
764
|
-
integration.llmobs_set_tags(span, args=[ctx], kwargs={}, response=formatted_response)
|
765
|
-
span.finish()
|
721
|
+
with ctx.span as span:
|
722
|
+
model_name = ctx["model_name"]
|
723
|
+
integration = ctx["bedrock_integration"]
|
724
|
+
if "embed" in model_name:
|
725
|
+
return
|
726
|
+
integration.llmobs_set_tags(span, args=[ctx], kwargs={}, response=formatted_response)
|
766
727
|
|
767
728
|
|
768
729
|
def _on_botocore_sqs_recvmessage_post(
|
@@ -872,10 +833,12 @@ def _on_azure_functions_service_bus_trigger_span_modifier(
|
|
872
833
|
span = ctx.span
|
873
834
|
_set_azure_function_tags(span, azure_functions_config, function_name, trigger, span_kind)
|
874
835
|
span.set_tag_str(MESSAGING_DESTINATION_NAME, entity_name)
|
875
|
-
span.set_tag_str(MESSAGING_MESSAGE_ID, message_id)
|
876
836
|
span.set_tag_str(MESSAGING_OPERATION, "receive")
|
877
837
|
span.set_tag_str(MESSAGING_SYSTEM, azure_servicebusx.SERVICE)
|
878
838
|
|
839
|
+
if message_id is not None:
|
840
|
+
span.set_tag_str(MESSAGING_MESSAGE_ID, message_id)
|
841
|
+
|
879
842
|
|
880
843
|
def _on_azure_servicebus_send_message_modifier(ctx, azure_servicebus_config, entity_name, fully_qualified_namespace):
|
881
844
|
span = ctx.span
|
@@ -929,7 +892,6 @@ def listen():
|
|
929
892
|
core.on("botocore.client_context.update_messages", _on_botocore_update_messages)
|
930
893
|
core.on("botocore.patched_bedrock_api_call.started", _on_botocore_patched_bedrock_api_call_started)
|
931
894
|
core.on("botocore.patched_bedrock_api_call.exception", _on_botocore_patched_bedrock_api_call_exception)
|
932
|
-
core.on("botocore.patched_bedrock_api_call.success", _on_botocore_patched_bedrock_api_call_success)
|
933
895
|
core.on("botocore.bedrock.process_response", _on_botocore_bedrock_process_response)
|
934
896
|
core.on("botocore.bedrock.process_response_converse", _on_botocore_bedrock_process_response_converse)
|
935
897
|
core.on("botocore.sqs.ReceiveMessage.post", _on_botocore_sqs_recvmessage_post)
|
@@ -12,11 +12,52 @@ from ddtrace.ext import SpanKind
|
|
12
12
|
from ddtrace.ext import aws
|
13
13
|
from ddtrace.ext import http
|
14
14
|
from ddtrace.internal.constants import COMPONENT
|
15
|
+
from ddtrace.internal.serverless import in_aws_lambda
|
15
16
|
from ddtrace.internal.utils.formats import deep_getattr
|
16
17
|
|
17
18
|
|
18
19
|
_PAYLOAD_TAGGER = AWSPayloadTagging()
|
19
20
|
|
21
|
+
SERVICE_MAP = {
|
22
|
+
"eventbridge": "events",
|
23
|
+
"events": "events",
|
24
|
+
"sqs": "sqs",
|
25
|
+
"sns": "sns",
|
26
|
+
"kinesis": "kinesis",
|
27
|
+
"dynamodb": "dynamodb",
|
28
|
+
"dynamodbdocument": "dynamodb",
|
29
|
+
}
|
30
|
+
|
31
|
+
|
32
|
+
# Helper to build AWS hostname from service, region and parameters
|
33
|
+
def _derive_peer_hostname(service: str, region: str, params: Optional[Dict[str, Any]] = None) -> Optional[str]:
|
34
|
+
"""Return hostname for given AWS service according to Datadog peer hostname rules.
|
35
|
+
|
36
|
+
Only returns hostnames for specific AWS services:
|
37
|
+
- eventbridge/events -> events.<region>.amazonaws.com
|
38
|
+
- sqs -> sqs.<region>.amazonaws.com
|
39
|
+
- sns -> sns.<region>.amazonaws.com
|
40
|
+
- kinesis -> kinesis.<region>.amazonaws.com
|
41
|
+
- dynamodb -> dynamodb.<region>.amazonaws.com
|
42
|
+
- s3 -> <bucket>.s3.<region>.amazonaws.com (if Bucket param present)
|
43
|
+
s3.<region>.amazonaws.com (otherwise)
|
44
|
+
|
45
|
+
Other services return ``None``.
|
46
|
+
"""
|
47
|
+
|
48
|
+
if not region:
|
49
|
+
return None
|
50
|
+
|
51
|
+
aws_service = service.lower()
|
52
|
+
|
53
|
+
if aws_service == "s3":
|
54
|
+
bucket = params.get("Bucket") if params else None
|
55
|
+
return f"{bucket}.s3.{region}.amazonaws.com" if bucket else f"s3.{region}.amazonaws.com"
|
56
|
+
|
57
|
+
mapped = SERVICE_MAP.get(aws_service)
|
58
|
+
|
59
|
+
return f"{mapped}.{region}.amazonaws.com" if mapped else None
|
60
|
+
|
20
61
|
|
21
62
|
def set_botocore_patched_api_call_span_tags(span: Span, instance, args, params, endpoint_name, operation):
|
22
63
|
span.set_tag_str(COMPONENT, config.botocore.integration_name)
|
@@ -51,6 +92,13 @@ def set_botocore_patched_api_call_span_tags(span: Span, instance, args, params,
|
|
51
92
|
span.set_tag_str("aws.region", region_name)
|
52
93
|
span.set_tag_str("region", region_name)
|
53
94
|
|
95
|
+
# Derive peer hostname only in serverless environments to avoid
|
96
|
+
# unnecessary tag noise in traditional hosts/containers.
|
97
|
+
if in_aws_lambda():
|
98
|
+
hostname = _derive_peer_hostname(endpoint_name, region_name, params)
|
99
|
+
if hostname:
|
100
|
+
span.set_tag_str("peer.service", hostname)
|
101
|
+
|
54
102
|
|
55
103
|
def set_botocore_response_metadata_tags(
|
56
104
|
span: Span, result: Dict[str, Any], is_error_code_fn: Optional[Callable] = None
|
ddtrace/_version.py
CHANGED
@@ -17,5 +17,5 @@ __version__: str
|
|
17
17
|
__version_tuple__: VERSION_TUPLE
|
18
18
|
version_tuple: VERSION_TUPLE
|
19
19
|
|
20
|
-
__version__ = version = '3.11.
|
21
|
-
__version_tuple__ = version_tuple = (3, 11, 0, '
|
20
|
+
__version__ = version = '3.11.0rc3'
|
21
|
+
__version_tuple__ = version_tuple = (3, 11, 0, 'rc3')
|
@@ -1,6 +1,7 @@
|
|
1
1
|
import functools
|
2
2
|
import json
|
3
3
|
import re
|
4
|
+
from types import TracebackType
|
4
5
|
from typing import Any
|
5
6
|
from typing import Callable
|
6
7
|
from typing import Dict
|
@@ -8,6 +9,7 @@ from typing import List
|
|
8
9
|
from typing import Literal
|
9
10
|
from typing import Optional
|
10
11
|
from typing import Set
|
12
|
+
from typing import Tuple
|
11
13
|
from typing import Union
|
12
14
|
from urllib import parse
|
13
15
|
|
@@ -534,7 +536,7 @@ def end_context(span: Span):
|
|
534
536
|
finalize_asm_env(env)
|
535
537
|
|
536
538
|
|
537
|
-
def _on_context_ended(ctx):
|
539
|
+
def _on_context_ended(ctx, _exc_info: Tuple[Optional[type], Optional[BaseException], Optional[TracebackType]]):
|
538
540
|
env = ctx.get_local_item(_ASM_CONTEXT)
|
539
541
|
if env is not None:
|
540
542
|
finalize_asm_env(env)
|
ddtrace/appsec/_constants.py
CHANGED
@@ -275,6 +275,12 @@ class API_SECURITY(metaclass=Constant_Class):
|
|
275
275
|
SAMPLE_RATE: Literal["DD_API_SECURITY_REQUEST_SAMPLE_RATE"] = "DD_API_SECURITY_REQUEST_SAMPLE_RATE"
|
276
276
|
SAMPLE_DELAY: Literal["DD_API_SECURITY_SAMPLE_DELAY"] = "DD_API_SECURITY_SAMPLE_DELAY"
|
277
277
|
MAX_PAYLOAD_SIZE: Literal[0x1000000] = 0x1000000 # 16MB maximum size
|
278
|
+
ENDPOINT_COLLECTION: Literal[
|
279
|
+
"DD_API_SECURITY_ENDPOINT_COLLECTION_ENABLED"
|
280
|
+
] = "DD_API_SECURITY_ENDPOINT_COLLECTION_ENABLED"
|
281
|
+
ENDPOINT_COLLECTION_LIMIT: Literal[
|
282
|
+
"DD_API_SECURITY_ENDPOINT_COLLECTION_MESSAGE_LIMIT"
|
283
|
+
] = "DD_API_SECURITY_ENDPOINT_COLLECTION_MESSAGE_LIMIT"
|
278
284
|
|
279
285
|
|
280
286
|
class WAF_CONTEXT_NAMES(metaclass=Constant_Class):
|
@@ -348,6 +354,7 @@ class DEFAULT(metaclass=Constant_Class):
|
|
348
354
|
r"|ey[I-L][\w=-]+\.(ey[I-L][\w=-]+(?:\.[\w.+\/=-]+)?)|[\-]{5}BEGIN[a-z\s]+PRIVATE\sKEY[\-]{5}([^\-]+)[\-]"
|
349
355
|
r"{5}END[a-z\s]+PRIVATE\sKEY|ssh-rsa\s*([a-z0-9\/\.+]{100,})"
|
350
356
|
)
|
357
|
+
ENDPOINT_COLLECTION_LIMIT = 300
|
351
358
|
|
352
359
|
|
353
360
|
class EXPLOIT_PREVENTION(metaclass=Constant_Class):
|
ddtrace/appsec/_handlers.py
CHANGED
@@ -4,6 +4,7 @@ import json
|
|
4
4
|
from typing import Any
|
5
5
|
from typing import Dict
|
6
6
|
from typing import Optional
|
7
|
+
from typing import Union
|
7
8
|
|
8
9
|
import xmltodict
|
9
10
|
|
@@ -11,6 +12,7 @@ from ddtrace._trace.span import Span
|
|
11
12
|
from ddtrace.appsec._asm_request_context import _call_waf
|
12
13
|
from ddtrace.appsec._asm_request_context import _call_waf_first
|
13
14
|
from ddtrace.appsec._asm_request_context import get_blocked
|
15
|
+
from ddtrace.appsec._asm_request_context import set_body_response
|
14
16
|
from ddtrace.appsec._constants import SPAN_DATA_NAMES
|
15
17
|
from ddtrace.appsec._http_utils import extract_cookies_from_headers
|
16
18
|
from ddtrace.appsec._http_utils import normalize_headers
|
@@ -157,6 +159,14 @@ def _on_lambda_start_response(
|
|
157
159
|
_call_waf(("aws_lambda",))
|
158
160
|
|
159
161
|
|
162
|
+
def _on_lambda_parse_body(
|
163
|
+
response_body: Optional[Union[str, Dict[str, Any]]],
|
164
|
+
):
|
165
|
+
if asm_config._api_security_feature_active:
|
166
|
+
if response_body:
|
167
|
+
set_body_response(response_body)
|
168
|
+
|
169
|
+
|
160
170
|
# ASGI
|
161
171
|
|
162
172
|
|
@@ -408,6 +418,7 @@ def listen():
|
|
408
418
|
|
409
419
|
core.on("aws_lambda.start_request", _on_lambda_start_request)
|
410
420
|
core.on("aws_lambda.start_response", _on_lambda_start_response)
|
421
|
+
core.on("aws_lambda.parse_body", _on_lambda_parse_body)
|
411
422
|
|
412
423
|
core.on("grpc.server.response.message", _on_grpc_server_response)
|
413
424
|
core.on("grpc.server.data", _on_grpc_server_data)
|
@@ -1,3 +1,7 @@
|
|
1
|
+
from types import TracebackType
|
2
|
+
from typing import Optional
|
3
|
+
from typing import Tuple
|
4
|
+
|
1
5
|
from ddtrace.appsec._iast._handlers import _iast_on_wrapped_view
|
2
6
|
from ddtrace.appsec._iast._handlers import _on_asgi_finalize_response
|
3
7
|
from ddtrace.appsec._iast._handlers import _on_django_finalize_response_pre
|
@@ -19,6 +23,12 @@ from ddtrace.internal import core
|
|
19
23
|
|
20
24
|
|
21
25
|
def iast_listen():
|
26
|
+
def _iast_context_end(
|
27
|
+
ctx: core.ExecutionContext,
|
28
|
+
_exc_info: Tuple[Optional[type], Optional[BaseException], Optional[TracebackType]],
|
29
|
+
):
|
30
|
+
_iast_end_request(ctx)
|
31
|
+
|
22
32
|
core.on("grpc.client.response.message", _on_grpc_response)
|
23
33
|
core.on("grpc.server.response.message", _on_grpc_server_response)
|
24
34
|
|
@@ -37,8 +47,8 @@ def iast_listen():
|
|
37
47
|
core.on("flask.finalize_request.post", _on_flask_finalize_request_post)
|
38
48
|
core.on("werkzeug.render_debugger_html", _on_werkzeug_render_debugger_html)
|
39
49
|
|
40
|
-
core.on("context.ended.wsgi.__call__",
|
41
|
-
core.on("context.ended.asgi.__call__",
|
50
|
+
core.on("context.ended.wsgi.__call__", _iast_context_end)
|
51
|
+
core.on("context.ended.asgi.__call__", _iast_context_end)
|
42
52
|
|
43
53
|
# Sink points
|
44
54
|
core.on("db_query_check", _on_report_sqli)
|
ddtrace/appsec/_processor.py
CHANGED
@@ -189,7 +189,7 @@ class AppSecSpanProcessor(SpanProcessor):
|
|
189
189
|
if skip_event:
|
190
190
|
core.discard_item("appsec_skip_next_lambda_event")
|
191
191
|
log.debug(
|
192
|
-
"appsec: ignoring unsupported
|
192
|
+
"appsec: ignoring unsupported lambda event",
|
193
193
|
)
|
194
194
|
span.set_metric(APPSEC.UNSUPPORTED_EVENT_TYPE, 1.0)
|
195
195
|
return
|
@@ -393,6 +393,16 @@ integrations:
|
|
393
393
|
min: 20.12.1
|
394
394
|
max: 24.11.1
|
395
395
|
|
396
|
+
- integration_name: gunicorn
|
397
|
+
is_external_package: true
|
398
|
+
is_tested: true
|
399
|
+
dependency_names:
|
400
|
+
- gunicorn
|
401
|
+
tested_versions_by_dependency:
|
402
|
+
gunicorn:
|
403
|
+
min: 20.0.4
|
404
|
+
max: 23.0.0
|
405
|
+
|
396
406
|
- integration_name: google_genai
|
397
407
|
is_external_package: true
|
398
408
|
is_tested: true
|
@@ -5,6 +5,7 @@ import aiobotocore.client
|
|
5
5
|
import wrapt
|
6
6
|
|
7
7
|
from ddtrace import config
|
8
|
+
from ddtrace._trace.utils_botocore.span_tags import _derive_peer_hostname
|
8
9
|
from ddtrace.constants import _SPAN_MEASURED_KEY
|
9
10
|
from ddtrace.constants import SPAN_KIND
|
10
11
|
from ddtrace.contrib.internal.trace_utils import ext_service
|
@@ -16,6 +17,7 @@ from ddtrace.ext import http
|
|
16
17
|
from ddtrace.internal.constants import COMPONENT
|
17
18
|
from ddtrace.internal.schema import schematize_cloud_api_operation
|
18
19
|
from ddtrace.internal.schema import schematize_service_name
|
20
|
+
from ddtrace.internal.serverless import in_aws_lambda
|
19
21
|
from ddtrace.internal.utils import ArgumentError
|
20
22
|
from ddtrace.internal.utils import get_argument_value
|
21
23
|
from ddtrace.internal.utils.formats import asbool
|
@@ -145,6 +147,12 @@ async def _wrapped_api_call(original_func, instance, args, kwargs):
|
|
145
147
|
|
146
148
|
region_name = deep_getattr(instance, "meta.region_name")
|
147
149
|
|
150
|
+
if in_aws_lambda():
|
151
|
+
# Derive the peer hostname now that we have both service and region.
|
152
|
+
hostname = _derive_peer_hostname(endpoint_name, region_name, params)
|
153
|
+
if hostname:
|
154
|
+
span.set_tag_str("peer.service", hostname)
|
155
|
+
|
148
156
|
meta = {
|
149
157
|
"aws.agent": "aiobotocore",
|
150
158
|
"aws.operation": operation,
|
@@ -0,0 +1,17 @@
|
|
1
|
+
"""
|
2
|
+
The Avro integration will trace all Avro read / write calls made with the ``avro``
|
3
|
+
library. This integration is enabled by default.
|
4
|
+
|
5
|
+
Enabling
|
6
|
+
~~~~~~~~
|
7
|
+
|
8
|
+
The avro integration is enabled by default. Use
|
9
|
+
:func:`patch()<ddtrace.patch>` to enable the integration::
|
10
|
+
|
11
|
+
from ddtrace import patch
|
12
|
+
patch(avro=True)
|
13
|
+
|
14
|
+
Configuration
|
15
|
+
~~~~~~~~~~~~~
|
16
|
+
|
17
|
+
"""
|
@@ -12,6 +12,7 @@ from ddtrace.internal.utils.formats import asbool
|
|
12
12
|
from ddtrace.trace import Pin
|
13
13
|
|
14
14
|
from .utils import create_context
|
15
|
+
from .utils import message_list_has_single_context
|
15
16
|
from .utils import wrap_function_with_tracing
|
16
17
|
|
17
18
|
|
@@ -97,24 +98,34 @@ def _wrap_service_bus_trigger(pin, func, function_name, trigger_arg_name, trigge
|
|
97
98
|
def context_factory(kwargs):
|
98
99
|
resource_name = f"{trigger_type} {function_name}"
|
99
100
|
msg = kwargs.get(trigger_arg_name)
|
100
|
-
|
101
|
-
|
102
|
-
)
|
101
|
+
|
102
|
+
# Reparent trace if single message or list of messages all with same context
|
103
|
+
if isinstance(msg, azure_functions.ServiceBusMessage):
|
104
|
+
application_properties = msg.application_properties
|
105
|
+
elif (
|
106
|
+
isinstance(msg, list)
|
107
|
+
and msg
|
108
|
+
and isinstance(msg[0], azure_functions.ServiceBusMessage)
|
109
|
+
and message_list_has_single_context(msg)
|
110
|
+
):
|
111
|
+
application_properties = msg[0].application_properties
|
112
|
+
else:
|
113
|
+
application_properties = None
|
114
|
+
|
115
|
+
return create_context("azure.functions.patched_service_bus", pin, resource_name, headers=application_properties)
|
103
116
|
|
104
117
|
def pre_dispatch(ctx, kwargs):
|
105
118
|
msg = kwargs.get(trigger_arg_name)
|
119
|
+
|
120
|
+
if isinstance(msg, azure_functions.ServiceBusMessage):
|
121
|
+
message_id = msg.message_id
|
122
|
+
else:
|
123
|
+
message_id = None
|
124
|
+
|
106
125
|
entity_name = trigger_details.get("topicName") or trigger_details.get("queueName")
|
107
126
|
return (
|
108
127
|
"azure.functions.service_bus_trigger_modifier",
|
109
|
-
(
|
110
|
-
ctx,
|
111
|
-
config.azure_functions,
|
112
|
-
function_name,
|
113
|
-
trigger_type,
|
114
|
-
SpanKind.CONSUMER,
|
115
|
-
entity_name,
|
116
|
-
msg.message_id,
|
117
|
-
),
|
128
|
+
(ctx, config.azure_functions, function_name, trigger_type, SpanKind.CONSUMER, entity_name, message_id),
|
118
129
|
)
|
119
130
|
|
120
131
|
return wrap_function_with_tracing(func, context_factory, pre_dispatch=pre_dispatch)
|
@@ -1,11 +1,15 @@
|
|
1
1
|
import functools
|
2
2
|
import inspect
|
3
|
+
from typing import List
|
4
|
+
|
5
|
+
import azure.functions as azure_functions
|
3
6
|
|
4
7
|
from ddtrace import config
|
5
8
|
from ddtrace.contrib.internal.trace_utils import int_service
|
6
9
|
from ddtrace.ext import SpanTypes
|
7
10
|
from ddtrace.internal import core
|
8
11
|
from ddtrace.internal.schema import schematize_cloud_faas_operation
|
12
|
+
from ddtrace.propagation.http import HTTPPropagator
|
9
13
|
|
10
14
|
|
11
15
|
def create_context(context_name, pin, resource=None, headers=None):
|
@@ -59,3 +63,13 @@ def wrap_function_with_tracing(func, context_factory, pre_dispatch=None, post_di
|
|
59
63
|
core.dispatch(*post_dispatch(ctx, res))
|
60
64
|
|
61
65
|
return wrapper
|
66
|
+
|
67
|
+
|
68
|
+
def message_list_has_single_context(msg_list: List[azure_functions.ServiceBusMessage]):
|
69
|
+
first_context = HTTPPropagator.extract(msg_list[0].application_properties)
|
70
|
+
for message in msg_list[1:]:
|
71
|
+
context = HTTPPropagator.extract(message.application_properties)
|
72
|
+
if first_context != context:
|
73
|
+
return False
|
74
|
+
|
75
|
+
return True
|
@@ -7,6 +7,7 @@ import boto.connection
|
|
7
7
|
import wrapt
|
8
8
|
|
9
9
|
from ddtrace import config
|
10
|
+
from ddtrace._trace.utils_botocore.span_tags import _derive_peer_hostname
|
10
11
|
from ddtrace.constants import _SPAN_MEASURED_KEY
|
11
12
|
from ddtrace.constants import SPAN_KIND
|
12
13
|
from ddtrace.ext import SpanKind
|
@@ -16,6 +17,7 @@ from ddtrace.ext import http
|
|
16
17
|
from ddtrace.internal.constants import COMPONENT
|
17
18
|
from ddtrace.internal.schema import schematize_cloud_api_operation
|
18
19
|
from ddtrace.internal.schema import schematize_service_name
|
20
|
+
from ddtrace.internal.serverless import in_aws_lambda
|
19
21
|
from ddtrace.internal.utils import get_argument_value
|
20
22
|
from ddtrace.internal.utils.formats import asbool
|
21
23
|
from ddtrace.internal.utils.wrappers import unwrap
|
@@ -122,6 +124,12 @@ def patched_query_request(original_func, instance, args, kwargs):
|
|
122
124
|
meta[aws.REGION] = region_name
|
123
125
|
meta[aws.AWSREGION] = region_name
|
124
126
|
|
127
|
+
if in_aws_lambda():
|
128
|
+
# Derive the peer hostname now that we have both service and region.
|
129
|
+
hostname = _derive_peer_hostname(endpoint_name, region_name, params)
|
130
|
+
if hostname:
|
131
|
+
meta["peer.service"] = hostname
|
132
|
+
|
125
133
|
span.set_tags(meta)
|
126
134
|
|
127
135
|
# Original func returns a boto.connection.HTTPResponse object
|
@@ -183,6 +191,12 @@ def patched_auth_request(original_func, instance, args, kwargs):
|
|
183
191
|
meta[aws.REGION] = region_name
|
184
192
|
meta[aws.AWSREGION] = region_name
|
185
193
|
|
194
|
+
if in_aws_lambda():
|
195
|
+
# Derive the peer hostname
|
196
|
+
hostname = _derive_peer_hostname(endpoint_name, region_name, None)
|
197
|
+
if hostname:
|
198
|
+
meta["peer.service"] = hostname
|
199
|
+
|
186
200
|
span.set_tags(meta)
|
187
201
|
|
188
202
|
# Original func returns a boto.connection.HTTPResponse object
|