diff --git a/.github/workflows/build_deploy.yml b/.github/workflows/build_deploy.yml index 179ed367141..47584911df4 100644 --- a/.github/workflows/build_deploy.yml +++ b/.github/workflows/build_deploy.yml @@ -27,7 +27,7 @@ jobs: build_wheels: uses: ./.github/workflows/build_python_3.yml with: - cibw_build: 'cp37* cp38* cp39* cp310* cp311* cp312* cp313*' + cibw_build: 'cp38* cp39* cp310* cp311* cp312* cp313*' build_sdist: name: Build source distribution diff --git a/benchmarks/bm/utils.py b/benchmarks/bm/utils.py index dd7b4991c57..13e99e8be74 100644 --- a/benchmarks/bm/utils.py +++ b/benchmarks/bm/utils.py @@ -65,7 +65,7 @@ def process_trace(self, trace): def drop_traces(tracer): - tracer.configure(settings={"FILTERS": [_DropTraces()]}) + tracer.configure(trace_processors=[_DropTraces()]) def drop_telemetry_events(): diff --git a/benchmarks/rate_limiter/scenario.py b/benchmarks/rate_limiter/scenario.py index 5210647ef89..3388af1cfb8 100644 --- a/benchmarks/rate_limiter/scenario.py +++ b/benchmarks/rate_limiter/scenario.py @@ -23,8 +23,8 @@ def _(loops): windows = [start + (i * self.time_window) for i in range(self.num_windows)] per_window = math.floor(loops / self.num_windows) - for window in windows: + for _ in windows: for _ in range(per_window): - rate_limiter.is_allowed(window) + rate_limiter.is_allowed() yield _ diff --git a/ddtrace/_trace/sampling_rule.py b/ddtrace/_trace/sampling_rule.py index 532a0b71f51..482a95d403a 100644 --- a/ddtrace/_trace/sampling_rule.py +++ b/ddtrace/_trace/sampling_rule.py @@ -8,8 +8,6 @@ from ddtrace.internal.glob_matching import GlobMatcher from ddtrace.internal.logger import get_logger from ddtrace.internal.utils.cache import cachedmethod -from ddtrace.internal.utils.deprecations import DDTraceDeprecationWarning -from ddtrace.vendor.debtcollector import deprecate if TYPE_CHECKING: # pragma: no cover @@ -210,14 +208,12 @@ def choose_matcher(self, prop): # We currently support the ability to pass in a function, a regular expression, or a string # If a string is passed in we create a GlobMatcher to handle the matching if callable(prop) or isinstance(prop, pattern_type): - # deprecated: passing a function or a regular expression' - deprecate( - "Using methods or regular expressions for SamplingRule matching is deprecated. ", - message="Please move to passing in a string for Glob matching.", - removal_version="3.0.0", - category=DDTraceDeprecationWarning, + log.error( + "Using methods or regular expressions for SamplingRule matching is not supported: %s ." + "Please move to passing in a string for Glob matching.", + str(prop), ) - return prop + return "None" # Name and Resource will never be None, but service can be, since we str() # whatever we pass into the GlobMatcher, we can just use its matching elif prop is None: diff --git a/ddtrace/_trace/span.py b/ddtrace/_trace/span.py index 446239a8091..c6eb4d4b72a 100644 --- a/ddtrace/_trace/span.py +++ b/ddtrace/_trace/span.py @@ -52,8 +52,6 @@ from ddtrace.internal.logger import get_logger from ddtrace.internal.sampling import SamplingMechanism from ddtrace.internal.sampling import set_sampling_decision_maker -from ddtrace.internal.utils.deprecations import DDTraceDeprecationWarning -from ddtrace.vendor.debtcollector import deprecate _NUMERIC_TAGS = (_ANALYTICS_SAMPLE_RATE_KEY,) @@ -279,29 +277,6 @@ def duration(self) -> Optional[float]: def duration(self, value: float) -> None: self.duration_ns = int(value * 1e9) - @property - def sampled(self) -> Optional[bool]: - deprecate( - "span.sampled is deprecated and will be removed in a future version of the tracer.", - message="""span.sampled references the state of span.context.sampling_priority. - Please use span.context.sampling_priority instead to check if a span is sampled.""", - category=DDTraceDeprecationWarning, - ) - if self.context.sampling_priority is None: - # this maintains original span.sampled behavior, where all spans would start - # with span.sampled = True until sampling runs - return True - return self.context.sampling_priority > 0 - - @sampled.setter - def sampled(self, value: bool) -> None: - deprecate( - "span.sampled is deprecated and will be removed in a future version of the tracer.", - message="""span.sampled has a no-op setter. - Please use span.set_tag('manual.keep'/'manual.drop') to keep or drop spans.""", - category=DDTraceDeprecationWarning, - ) - def finish(self, finish_time: Optional[float] = None) -> None: """Mark the end time of the span and submit it to the tracer. If the span has already been finished don't do anything. diff --git a/ddtrace/_trace/trace_handlers.py b/ddtrace/_trace/trace_handlers.py index e0d99c0d020..9ddfe93baa0 100644 --- a/ddtrace/_trace/trace_handlers.py +++ b/ddtrace/_trace/trace_handlers.py @@ -595,8 +595,10 @@ def _on_botocore_patched_bedrock_api_call_success(ctx, reqid, latency, input_tok span = ctx.span span.set_tag_str("bedrock.response.id", reqid) span.set_tag_str("bedrock.response.duration", latency) - span.set_tag_str("bedrock.usage.prompt_tokens", input_token_count) - span.set_tag_str("bedrock.usage.completion_tokens", output_token_count) + if input_token_count: + span.set_metric("bedrock.response.usage.prompt_tokens", int(input_token_count)) + if output_token_count: + span.set_metric("bedrock.response.usage.completion_tokens", int(output_token_count)) def _propagate_context(ctx, headers): @@ -640,7 +642,10 @@ def _on_botocore_bedrock_process_response( integration = ctx["bedrock_integration"] if metadata is not None: for k, v in metadata.items(): - span.set_tag_str("bedrock.{}".format(k), str(v)) + if k in ["usage.completion_tokens", "usage.prompt_tokens"] and v: + span.set_metric("bedrock.response{}".format(k), int(v)) + else: + span.set_tag_str("bedrock.response{}".format(k), str(v)) if "embed" in model_name: span.set_metric("bedrock.response.embedding_length", len(formatted_response["text"][0])) span.finish() diff --git a/ddtrace/_trace/tracer.py b/ddtrace/_trace/tracer.py index 87f312bb18c..7030ec823d6 100644 --- a/ddtrace/_trace/tracer.py +++ b/ddtrace/_trace/tracer.py @@ -24,6 +24,7 @@ from ddtrace._trace.processor import TraceProcessor from ddtrace._trace.processor import TraceSamplingProcessor from ddtrace._trace.processor import TraceTagsProcessor +from ddtrace._trace.provider import BaseContextProvider from ddtrace._trace.provider import DefaultContextProvider from ddtrace._trace.sampler import BasePrioritySampler from ddtrace._trace.sampler import BaseSampler @@ -200,7 +201,7 @@ def __init__( self, url: Optional[str] = None, dogstatsd_url: Optional[str] = None, - context_provider: Optional[DefaultContextProvider] = None, + context_provider: Optional[BaseContextProvider] = None, ) -> None: """ Create a new ``Tracer`` instance. A global tracer is already initialized @@ -328,28 +329,6 @@ def sample(self, span): else: log.error("No sampler available to sample span") - @property - def sampler(self): - deprecate( - "tracer.sampler is deprecated and will be removed.", - message="To manually sample call tracer.sample(span) instead.", - category=DDTraceDeprecationWarning, - ) - return self._sampler - - @sampler.setter - def sampler(self, value): - deprecate( - "Setting a custom sampler is deprecated and will be removed.", - message="""Please use DD_TRACE_SAMPLING_RULES to configure the sampler instead: - https://ddtrace.readthedocs.io/en/stable/configuration.html#DD_TRACE_SAMPLING_RULES""", - category=DDTraceDeprecationWarning, - ) - if asm_config._apm_opt_out: - log.warning("Cannot set a custom sampler with Standalone ASM mode") - return - self._sampler = value - def on_start_span(self, func: Callable) -> Callable: """Register a function to execute when a span start. @@ -441,21 +420,7 @@ def get_log_correlation_context(self, active: Optional[Union[Context, Span]] = N def configure( self, - enabled: Optional[bool] = None, - hostname: Optional[str] = None, - port: Optional[int] = None, - uds_path: Optional[str] = None, - https: Optional[bool] = None, - sampler: Optional[BaseSampler] = None, - context_provider: Optional[DefaultContextProvider] = None, - wrap_executor: Optional[Callable] = None, - priority_sampling: Optional[bool] = None, - settings: Optional[Dict[str, Any]] = None, - dogstatsd_url: Optional[str] = None, - writer: Optional[TraceWriter] = None, - partial_flush_enabled: Optional[bool] = None, - partial_flush_min_spans: Optional[int] = None, - api_version: Optional[str] = None, + context_provider: Optional[BaseContextProvider] = None, compute_stats_enabled: Optional[bool] = None, appsec_enabled: Optional[bool] = None, iast_enabled: Optional[bool] = None, @@ -472,58 +437,14 @@ def configure( :param bool appsec_standalone_enabled: When tracing is disabled ensures ASM support is still enabled. :param List[TraceProcessor] trace_processors: This parameter sets TraceProcessor (ex: TraceFilters). Trace processors are used to modify and filter traces based on certain criteria. - - :param bool enabled: If True, finished traces will be submitted to the API, else they'll be dropped. - This parameter is deprecated and will be removed. - :param str hostname: Hostname running the Trace Agent. This parameter is deprecated and will be removed. - :param int port: Port of the Trace Agent. This parameter is deprecated and will be removed. - :param str uds_path: The Unix Domain Socket path of the agent. This parameter is deprecated and will be removed. - :param bool https: Whether to use HTTPS or HTTP. This parameter is deprecated and will be removed. - :param object sampler: A custom Sampler instance, locally deciding to totally drop the trace or not. - This parameter is deprecated and will be removed. - :param object wrap_executor: callable that is used when a function is decorated with - ``Tracer.wrap()``. This is an advanced option that usually doesn't need to be changed - from the default value. This parameter is deprecated and will be removed. - :param priority_sampling: This parameter is deprecated and will be removed in a future version. - :param bool settings: This parameter is deprecated and will be removed. - :param str dogstatsd_url: URL for UDP or Unix socket connection to DogStatsD - This parameter is deprecated and will be removed. - :param TraceWriter writer: This parameter is deprecated and will be removed. - :param bool partial_flush_enabled: This parameter is deprecated and will be removed. - :param bool partial_flush_min_spans: This parameter is deprecated and will be removed. - :param str api_version: This parameter is deprecated and will be removed. - :param bool compute_stats_enabled: This parameter is deprecated and will be removed. """ - if settings is not None: - deprecate( - "Support for ``tracer.configure(...)`` with the settings parameter is deprecated", - message="Please use the trace_processors parameter instead of settings['FILTERS'].", - version="3.0.0", - category=DDTraceDeprecationWarning, - ) - trace_processors = (trace_processors or []) + (settings.get("FILTERS") or []) - return self._configure( - enabled, - hostname, - port, - uds_path, - https, - sampler, - context_provider, - wrap_executor, - priority_sampling, - trace_processors, - dogstatsd_url, - writer, - partial_flush_enabled, - partial_flush_min_spans, - api_version, - compute_stats_enabled, - appsec_enabled, - iast_enabled, - appsec_standalone_enabled, - True, + context_provider=context_provider, + trace_processors=trace_processors, + compute_stats_enabled=compute_stats_enabled, + appsec_enabled=appsec_enabled, + iast_enabled=iast_enabled, + appsec_standalone_enabled=appsec_standalone_enabled, ) def _configure( @@ -534,7 +455,7 @@ def _configure( uds_path: Optional[str] = None, https: Optional[bool] = None, sampler: Optional[BaseSampler] = None, - context_provider: Optional[DefaultContextProvider] = None, + context_provider: Optional[BaseContextProvider] = None, wrap_executor: Optional[Callable] = None, priority_sampling: Optional[bool] = None, trace_processors: Optional[List[TraceProcessor]] = None, @@ -547,48 +468,18 @@ def _configure( appsec_enabled: Optional[bool] = None, iast_enabled: Optional[bool] = None, appsec_standalone_enabled: Optional[bool] = None, - log_deprecations: bool = False, ) -> None: if enabled is not None: self.enabled = enabled - if log_deprecations: - deprecate( - "Enabling/Disabling tracing after application start is deprecated", - message="Please use DD_TRACE_ENABLED instead.", - version="3.0.0", - category=DDTraceDeprecationWarning, - ) - - if priority_sampling is not None and log_deprecations: - deprecate( - "Disabling priority sampling is deprecated", - message="Calling `tracer.configure(priority_sampling=....) has no effect", - version="3.0.0", - category=DDTraceDeprecationWarning, - ) if trace_processors is not None: self._user_trace_processors = trace_processors if partial_flush_enabled is not None: self._partial_flush_enabled = partial_flush_enabled - if log_deprecations: - deprecate( - "Configuring partial flushing after application start is deprecated", - message="Please use DD_TRACE_PARTIAL_FLUSH_ENABLED to enable/disable the partial flushing instead.", - version="3.0.0", - category=DDTraceDeprecationWarning, - ) if partial_flush_min_spans is not None: self._partial_flush_min_spans = partial_flush_min_spans - if log_deprecations: - deprecate( - "Configuring partial flushing after application start is deprecated", - message="Please use DD_TRACE_PARTIAL_FLUSH_MIN_SPANS to set the flushing threshold instead.", - version="3.0.0", - category=DDTraceDeprecationWarning, - ) if appsec_enabled is not None: asm_config._asm_enabled = appsec_enabled @@ -620,33 +511,11 @@ def _configure( if sampler is not None: self._sampler = sampler self._user_sampler = self._sampler - if log_deprecations: - deprecate( - "Configuring custom samplers is deprecated", - message="Please use DD_TRACE_SAMPLING_RULES to configure the sample rates instead", - category=DDTraceDeprecationWarning, - removal_version="3.0.0", - ) if dogstatsd_url is not None: - if log_deprecations: - deprecate( - "Configuring dogstatsd_url after application start is deprecated", - message="Please use DD_DOGSTATSD_URL instead.", - version="3.0.0", - category=DDTraceDeprecationWarning, - ) self._dogstatsd_url = dogstatsd_url if any(x is not None for x in [hostname, port, uds_path, https]): - if log_deprecations: - deprecate( - "Configuring tracer agent connection after application start is deprecated", - message="Please use DD_TRACE_AGENT_URL instead.", - version="3.0.0", - category=DDTraceDeprecationWarning, - ) - # If any of the parts of the URL have updated, merge them with # the previous writer values. prev_url_parsed = compat.parse.urlparse(self._agent_url) @@ -670,13 +539,6 @@ def _configure( new_url = None if compute_stats_enabled is not None: - if log_deprecations: - deprecate( - "Configuring tracer stats computation after application start is deprecated", - message="Please use DD_TRACE_STATS_COMPUTATION_ENABLED instead.", - version="3.0.0", - category=DDTraceDeprecationWarning, - ) self._compute_stats = compute_stats_enabled try: @@ -685,14 +547,6 @@ def _configure( # It's possible the writer never got started pass - if api_version is not None and log_deprecations: - deprecate( - "Configuring Tracer API version after application start is deprecated", - message="Please use DD_TRACE_API_VERSION instead.", - version="3.0.0", - category=DDTraceDeprecationWarning, - ) - if writer is not None: self._writer = writer elif any(x is not None for x in [new_url, api_version, sampler, dogstatsd_url, appsec_enabled]): @@ -754,12 +608,6 @@ def _configure( if wrap_executor is not None: self._wrap_executor = wrap_executor - if log_deprecations: - deprecate( - "Support for tracer.configure(...) with the wrap_executor parameter is deprecated", - version="3.0.0", - category=DDTraceDeprecationWarning, - ) self._generate_diagnostic_logs() @@ -1344,7 +1192,7 @@ def _handle_sampler_update(self, cfg: Config) -> None: and self._user_sampler ): # if we get empty configs from rc for both sample rate and rules, we should revert to the user sampler - self.sampler = self._user_sampler + self._sampler = self._user_sampler return if cfg._get_source("_trace_sample_rate") != "remote_config" and self._user_sampler: diff --git a/ddtrace/appsec/_iast/_handlers.py b/ddtrace/appsec/_iast/_handlers.py index cf60fc610be..bcd913085f4 100644 --- a/ddtrace/appsec/_iast/_handlers.py +++ b/ddtrace/appsec/_iast/_handlers.py @@ -82,23 +82,28 @@ def _on_flask_patch(flask_version): "Headers.items", functools.partial(if_iast_taint_yield_tuple_for, (OriginType.HEADER_NAME, OriginType.HEADER)), ) - _set_metric_iast_instrumented_source(OriginType.HEADER_NAME) - _set_metric_iast_instrumented_source(OriginType.HEADER) try_wrap_function_wrapper( "werkzeug.datastructures", - "ImmutableMultiDict.__getitem__", - functools.partial(if_iast_taint_returned_object_for, OriginType.PARAMETER), + "EnvironHeaders.__getitem__", + functools.partial(if_iast_taint_returned_object_for, OriginType.HEADER), ) - _set_metric_iast_instrumented_source(OriginType.PARAMETER) - + # Since werkzeug 3.1.0 get doesn't call to __getitem__ try_wrap_function_wrapper( "werkzeug.datastructures", - "EnvironHeaders.__getitem__", + "EnvironHeaders.get", functools.partial(if_iast_taint_returned_object_for, OriginType.HEADER), ) + _set_metric_iast_instrumented_source(OriginType.HEADER_NAME) _set_metric_iast_instrumented_source(OriginType.HEADER) + try_wrap_function_wrapper( + "werkzeug.datastructures", + "ImmutableMultiDict.__getitem__", + functools.partial(if_iast_taint_returned_object_for, OriginType.PARAMETER), + ) + _set_metric_iast_instrumented_source(OriginType.PARAMETER) + if flask_version >= (2, 0, 0): # instance.query_string: raising an error on werkzeug/_internal.py "AttributeError: read only property" try_wrap_function_wrapper("werkzeug.wrappers.request", "Request.__init__", _on_request_init) diff --git a/ddtrace/appsec/_iast/taint_sinks/xss.py b/ddtrace/appsec/_iast/taint_sinks/xss.py index 425affac77a..6f7d263f7c2 100644 --- a/ddtrace/appsec/_iast/taint_sinks/xss.py +++ b/ddtrace/appsec/_iast/taint_sinks/xss.py @@ -52,6 +52,18 @@ def patch(): _iast_django_xss, ) + try_wrap_function_wrapper( + "jinja2.filters", + "do_mark_safe", + _iast_jinja2_xss, + ) + try_wrap_function_wrapper( + "flask", + "render_template_string", + _iast_jinja2_xss, + ) + + _set_metric_iast_instrumented_sink(VULN_XSS) _set_metric_iast_instrumented_sink(VULN_XSS) @@ -70,6 +82,12 @@ def _iast_django_xss(wrapped, instance, args, kwargs): return wrapped(*args, **kwargs) +def _iast_jinja2_xss(wrapped, instance, args, kwargs): + if args and len(args) >= 1: + _iast_report_xss(args[0]) + return wrapped(*args, **kwargs) + + def _iast_report_xss(code_string: Text): increment_iast_span_metric(IAST_SPAN_TAGS.TELEMETRY_EXECUTED_SINK, XSS.vulnerability_type) _set_metric_iast_executed_sink(XSS.vulnerability_type) diff --git a/ddtrace/appsec/_python_info/stdlib/__init__.py b/ddtrace/appsec/_python_info/stdlib/__init__.py index e745c392f55..8b220b0af85 100644 --- a/ddtrace/appsec/_python_info/stdlib/__init__.py +++ b/ddtrace/appsec/_python_info/stdlib/__init__.py @@ -3,11 +3,7 @@ from sys import version_info -if version_info < (3, 7, 0): - from .module_names_py36 import STDLIB_MODULE_NAMES -elif version_info < (3, 8, 0): - from .module_names_py37 import STDLIB_MODULE_NAMES -elif version_info < (3, 9, 0): +if version_info < (3, 9, 0): from .module_names_py38 import STDLIB_MODULE_NAMES elif version_info < (3, 10, 0): from .module_names_py39 import STDLIB_MODULE_NAMES diff --git a/ddtrace/contrib/_langchain.py b/ddtrace/contrib/_langchain.py index d36cd76f3f1..4d419cc5d5c 100644 --- a/ddtrace/contrib/_langchain.py +++ b/ddtrace/contrib/_langchain.py @@ -1,9 +1,8 @@ """ -The LangChain integration instruments the LangChain Python library to emit metrics, -traces, and logs (logs are disabled by default) for requests made to the LLMs, +The LangChain integration instruments the LangChain Python library to emit traces for requests made to the LLMs, chat models, embeddings, chains, and vector store interfaces. -All metrics, logs, and traces submitted from the LangChain integration are tagged by: +All traces submitted from the LangChain integration are tagged by: - ``service``, ``env``, ``version``: see the `Unified Service Tagging docs `_. - ``langchain.request.provider``: LLM provider used in the request. @@ -26,58 +25,6 @@ - Total cost metrics for OpenAI requests -Metrics -~~~~~~~ - -The following metrics are collected by default by the LangChain integration. - -.. important:: - If the Agent is configured to use a non-default Statsd hostname or port, use ``DD_DOGSTATSD_URL`` to configure - ``ddtrace`` to use it. - - -.. py:data:: langchain.request.duration - - The duration of the LangChain request in seconds. - - Type: ``distribution`` - - -.. py:data:: langchain.request.error - - The number of errors from requests made with LangChain. - - Type: ``count`` - - -.. py:data:: langchain.tokens.prompt - - The number of tokens used in the prompt of a LangChain request. - - Type: ``distribution`` - - -.. py:data:: langchain.tokens.completion - - The number of tokens used in the completion of a LangChain response. - - Type: ``distribution`` - - -.. py:data:: langchain.tokens.total - - The total number of tokens used in the prompt and completion of a LangChain request/response. - - Type: ``distribution`` - - -.. py:data:: langchain.tokens.total_cost - - The estimated cost in USD based on token usage. - - Type: ``count`` - - (beta) Prompt and Completion Sampling ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -89,18 +36,6 @@ - Prompt inputs, chain inputs, and outputs for the ``Chain`` interface. - Query inputs and document outputs for the ``VectorStore`` interface. -Prompt and message inputs and completions can also be emitted as log data. -Logs are **not** emitted by default. When logs are enabled they are sampled at ``0.1``. - -Read the **Global Configuration** section for information about enabling logs and configuring sampling -rates. - -.. important:: - - To submit logs, you must set the ``DD_API_KEY`` environment variable. - - Set ``DD_SITE`` to send logs to a Datadog site such as ``datadoghq.eu``. The default is ``datadoghq.com``. - Enabling ~~~~~~~~ @@ -143,32 +78,6 @@ Default: ``DD_SERVICE`` -.. py:data:: ddtrace.config.langchain["logs_enabled"] - - Enable collection of prompts and completions as logs. You can adjust the rate of prompts and completions collected - using the sample rate configuration described below. - - Alternatively, you can set this option with the ``DD_LANGCHAIN_LOGS_ENABLED`` environment - variable. - - Note that you must set the ``DD_API_KEY`` environment variable to enable sending logs. - - Default: ``False`` - - -.. py:data:: ddtrace.config.langchain["metrics_enabled"] - - Enable collection of LangChain metrics. - - If the Datadog Agent is configured to use a non-default Statsd hostname - or port, use ``DD_DOGSTATSD_URL`` to configure ``ddtrace`` to use it. - - Alternatively, you can set this option with the ``DD_LANGCHAIN_METRICS_ENABLED`` environment - variable. - - Default: ``True`` - - .. py:data:: (beta) ddtrace.config.langchain["span_char_limit"] Configure the maximum number of characters for the following data within span tags: @@ -195,14 +104,4 @@ Default: ``1.0`` - -.. py:data:: (beta) ddtrace.config.langchain["log_prompt_completion_sample_rate"] - - Configure the sample rate for the collection of prompts and completions as logs. - - Alternatively, you can set this option with the ``DD_LANGCHAIN_LOG_PROMPT_COMPLETION_SAMPLE_RATE`` environment - variable. - - Default: ``0.1`` - """ # noqa: E501 diff --git a/ddtrace/contrib/_openai.py b/ddtrace/contrib/_openai.py index 8e2eb87aeb5..0642bbb0881 100644 --- a/ddtrace/contrib/_openai.py +++ b/ddtrace/contrib/_openai.py @@ -1,10 +1,8 @@ """ -The OpenAI integration instruments the OpenAI Python library to emit metrics, -traces, and logs (logs are disabled by default) for requests made to the models, -completions, chat completions, edits, images, embeddings, audio, files, fine-tunes, -and moderations endpoints. +The OpenAI integration instruments the OpenAI Python library to emit traces for requests made to the models, +completions, chat completions, images, embeddings, audio, files, and moderations endpoints. -All metrics, logs, and traces submitted from the OpenAI integration are tagged by: +All traces submitted from the OpenAI integration are tagged by: - ``service``, ``env``, ``version``: see the `Unified Service Tagging docs `_. - ``openai.request.endpoint``: OpenAI API endpoint used in the request. @@ -15,84 +13,6 @@ - ``openai.user.api_key``: OpenAI API key used to make the request (obfuscated to match the OpenAI UI representation ``sk-...XXXX`` where ``XXXX`` is the last 4 digits of the key). -Metrics -~~~~~~~ - -The following metrics are collected by default by the OpenAI integration. - -.. important:: - If the Agent is configured to use a non-default Statsd hostname or port, use ``DD_DOGSTATSD_URL`` to configure - ``ddtrace`` to use it. - - -.. important:: - Ratelimit and token metrics only reflect usage of the supported completions, chat completions, and embedding - endpoints. Usage of other OpenAI endpoints will not be recorded as they are not provided. - - -.. py:data:: openai.request.duration - - The duration of the OpenAI request in seconds. - - Type: ``distribution`` - - -.. py:data:: openai.request.error - - The number of errors from requests made to OpenAI. - - Type: ``count`` - - -.. py:data:: openai.ratelimit.requests - - The maximum number of OpenAI requests permitted before exhausting the rate limit. - - Type: ``gauge`` - - -.. py:data:: openai.ratelimit.tokens - - The maximum number of OpenAI tokens permitted before exhausting the rate limit. - - Type: ``gauge`` - - -.. py:data:: openai.ratelimit.remaining.requests - - The remaining number of OpenAI requests permitted before exhausting the rate limit. - - Type: ``gauge`` - - -.. py:data:: openai.ratelimit.remaining.tokens - - The remaining number of OpenAI tokens permitted before exhausting the rate limit. - - Type: ``gauge`` - - -.. py:data:: openai.tokens.prompt - - The number of tokens used in the prompt of an OpenAI request. - - Type: ``distribution`` - - -.. py:data:: openai.tokens.completion - - The number of tokens used in the completion of a OpenAI response. - - Type: ``distribution`` - - -.. py:data:: openai.tokens.total - - The total number of tokens used in the prompt and completion of a OpenAI request/response. - - Type: ``distribution`` - - (beta) Prompt and Completion Sampling ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -101,22 +21,9 @@ - Prompt inputs and completions for the ``completions`` endpoint. - Message inputs and completions for the ``chat.completions`` endpoint. - Embedding inputs for the ``embeddings`` endpoint. -- Edit inputs, instructions, and completions for the ``edits`` endpoint. - Image input filenames and completion URLs for the ``images`` endpoint. - Audio input filenames and completions for the ``audio`` endpoint. -Prompt and message inputs and completions can also be emitted as log data. -Logs are **not** emitted by default. When logs are enabled they are sampled at ``0.1``. - -Read the **Global Configuration** section for information about enabling logs and configuring sampling -rates. - -.. important:: - - To submit logs, you must set the ``DD_API_KEY`` environment variable. - - Set ``DD_SITE`` to send logs to a Datadog site such as ``datadoghq.eu``. The default is ``datadoghq.com``. - (beta) Streamed Responses Support ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -172,32 +79,6 @@ Default: ``DD_SERVICE`` -.. py:data:: ddtrace.config.openai["logs_enabled"] - - Enable collection of prompts and completions as logs. You can adjust the rate of prompts and completions collected - using the sample rate configuration described below. - - Alternatively, you can set this option with the ``DD_OPENAI_LOGS_ENABLED`` environment - variable. - - Note that you must set the ``DD_API_KEY`` environment variable to enable sending logs. - - Default: ``False`` - - -.. py:data:: ddtrace.config.openai["metrics_enabled"] - - Enable collection of OpenAI metrics. - - If the Datadog Agent is configured to use a non-default Statsd hostname - or port, use ``DD_DOGSTATSD_URL`` to configure ``ddtrace`` to use it. - - Alternatively, you can set this option with the ``DD_OPENAI_METRICS_ENABLED`` environment - variable. - - Default: ``True`` - - .. py:data:: (beta) ddtrace.config.openai["span_char_limit"] Configure the maximum number of characters for the following data within span tags: @@ -225,16 +106,6 @@ Default: ``1.0`` -.. py:data:: (beta) ddtrace.config.openai["log_prompt_completion_sample_rate"] - - Configure the sample rate for the collection of prompts and completions as logs. - - Alternatively, you can set this option with the ``DD_OPENAI_LOG_PROMPT_COMPLETION_SAMPLE_RATE`` environment - variable. - - Default: ``0.1`` - - Instance Configuration ~~~~~~~~~~~~~~~~~~~~~~ diff --git a/ddtrace/contrib/aiohttp.py b/ddtrace/contrib/aiohttp.py index dbb5def90d1..d001139dde8 100644 --- a/ddtrace/contrib/aiohttp.py +++ b/ddtrace/contrib/aiohttp.py @@ -36,6 +36,13 @@ Default: ``False`` +.. py:data:: ddtrace.config.aiohttp['disable_stream_timing_for_mem_leak'] + + Whether or not to to address a potential memory leak in the aiohttp integration. + When set to ``True``, this flag may cause streamed response span timing to be inaccurate. + + Default: ``False`` + Server ****** diff --git a/ddtrace/contrib/internal/aiohttp/middlewares.py b/ddtrace/contrib/internal/aiohttp/middlewares.py index b3dde240d44..c1a5b8e4f3b 100644 --- a/ddtrace/contrib/internal/aiohttp/middlewares.py +++ b/ddtrace/contrib/internal/aiohttp/middlewares.py @@ -59,8 +59,9 @@ async def attach_context(request): request[REQUEST_CONFIG_KEY] = app[CONFIG_KEY] try: response = await handler(request) - if isinstance(response, web.StreamResponse): - request.task.add_done_callback(lambda _: finish_request_span(request, response)) + if not config.aiohttp["disable_stream_timing_for_mem_leak"]: + if isinstance(response, web.StreamResponse): + request.task.add_done_callback(lambda _: finish_request_span(request, response)) return response except Exception: req_span.set_traceback() @@ -134,9 +135,13 @@ async def on_prepare(request, response): the trace middleware execution. """ # NB isinstance is not appropriate here because StreamResponse is a parent of the other - # aiohttp response types - if type(response) is web.StreamResponse and not response.task.done(): - return + # aiohttp response types. However in some cases this can also lead to missing the closing of + # spans, leading to a memory leak, which is why we have this flag. + # todo: this is a temporary fix for a memory leak in aiohttp. We should find a way to + # consistently close spans with the correct timing. + if not config.aiohttp["disable_stream_timing_for_mem_leak"]: + if type(response) is web.StreamResponse and not response.task.done(): + return finish_request_span(request, response) diff --git a/ddtrace/contrib/internal/aiohttp/patch.py b/ddtrace/contrib/internal/aiohttp/patch.py index 900a8d26e41..4643ba2ae43 100644 --- a/ddtrace/contrib/internal/aiohttp/patch.py +++ b/ddtrace/contrib/internal/aiohttp/patch.py @@ -22,6 +22,7 @@ from ddtrace.internal.utils import get_argument_value from ddtrace.internal.utils.formats import asbool from ddtrace.propagation.http import HTTPPropagator +from ddtrace.settings._core import get_config as _get_config from ddtrace.trace import Pin @@ -31,7 +32,12 @@ # Server config config._add( "aiohttp", - dict(distributed_tracing=True), + dict( + distributed_tracing=True, + disable_stream_timing_for_mem_leak=asbool( + _get_config("DD_AIOHTTP_CLIENT_DISABLE_STREAM_TIMING_FOR_MEM_LEAK", default=False) + ), + ), ) config._add( diff --git a/ddtrace/contrib/internal/langchain/patch.py b/ddtrace/contrib/internal/langchain/patch.py index 9badbf22d87..f9681dd1302 100644 --- a/ddtrace/contrib/internal/langchain/patch.py +++ b/ddtrace/contrib/internal/langchain/patch.py @@ -56,7 +56,6 @@ from ddtrace.internal.logger import get_logger from ddtrace.internal.utils import ArgumentError from ddtrace.internal.utils import get_argument_value -from ddtrace.internal.utils.formats import asbool from ddtrace.internal.utils.formats import deep_getattr from ddtrace.internal.utils.version import parse_version from ddtrace.llmobs._integrations import LangChainIntegration @@ -76,10 +75,7 @@ def get_version(): config._add( "langchain", { - "logs_enabled": asbool(os.getenv("DD_LANGCHAIN_LOGS_ENABLED", False)), - "metrics_enabled": asbool(os.getenv("DD_LANGCHAIN_METRICS_ENABLED", True)), "span_prompt_completion_sample_rate": float(os.getenv("DD_LANGCHAIN_SPAN_PROMPT_COMPLETION_SAMPLE_RATE", 1.0)), - "log_prompt_completion_sample_rate": float(os.getenv("DD_LANGCHAIN_LOG_PROMPT_COMPLETION_SAMPLE_RATE", 0.1)), "span_char_limit": int(os.getenv("DD_LANGCHAIN_SPAN_CHAR_LIMIT", 128)), }, ) @@ -221,7 +217,6 @@ def traced_llm_generate(langchain, pin, func, instance, args, kwargs): completions = func(*args, **kwargs) if _is_openai_llm_instance(instance): _tag_openai_token_usage(span, completions.llm_output) - integration.record_usage(span, completions.llm_output) for idx, completion in enumerate(completions.generations): if integration.is_pc_sampled_span(span): @@ -237,28 +232,10 @@ def traced_llm_generate(langchain, pin, func, instance, args, kwargs): ) except Exception: span.set_exc_info(*sys.exc_info()) - integration.metric(span, "incr", "request.error", 1) raise finally: integration.llmobs_set_tags(span, args=args, kwargs=kwargs, response=completions, operation="llm") span.finish() - integration.metric(span, "dist", "request.duration", span.duration_ns) - if integration.is_pc_sampled_log(span): - if completions is None: - log_completions = [] - else: - log_completions = [ - [{"text": completion.text} for completion in completions] for completions in completions.generations - ] - integration.log( - span, - "info" if span.error == 0 else "error", - "sampled %s.%s" % (instance.__module__, instance.__class__.__name__), - attrs={ - "prompts": prompts, - "choices": log_completions, - }, - ) return completions @@ -292,7 +269,6 @@ async def traced_llm_agenerate(langchain, pin, func, instance, args, kwargs): completions = await func(*args, **kwargs) if _is_openai_llm_instance(instance): _tag_openai_token_usage(span, completions.llm_output) - integration.record_usage(span, completions.llm_output) for idx, completion in enumerate(completions.generations): if integration.is_pc_sampled_span(span): @@ -308,28 +284,10 @@ async def traced_llm_agenerate(langchain, pin, func, instance, args, kwargs): ) except Exception: span.set_exc_info(*sys.exc_info()) - integration.metric(span, "incr", "request.error", 1) raise finally: integration.llmobs_set_tags(span, args=args, kwargs=kwargs, response=completions, operation="llm") span.finish() - integration.metric(span, "dist", "request.duration", span.duration_ns) - if integration.is_pc_sampled_log(span): - if completions is None: - log_completions = [] - else: - log_completions = [ - [{"text": completion.text} for completion in completions] for completions in completions.generations - ] - integration.log( - span, - "info" if span.error == 0 else "error", - "sampled %s.%s" % (instance.__module__, instance.__class__.__name__), - attrs={ - "prompts": prompts, - "choices": log_completions, - }, - ) return completions @@ -376,7 +334,6 @@ def traced_chat_model_generate(langchain, pin, func, instance, args, kwargs): chat_completions = func(*args, **kwargs) if _is_openai_chat_instance(instance): _tag_openai_token_usage(span, chat_completions.llm_output) - integration.record_usage(span, chat_completions.llm_output) for message_set_idx, message_set in enumerate(chat_completions.generations): for idx, chat_completion in enumerate(message_set): @@ -417,45 +374,10 @@ def traced_chat_model_generate(langchain, pin, func, instance, args, kwargs): ) except Exception: span.set_exc_info(*sys.exc_info()) - integration.metric(span, "incr", "request.error", 1) raise finally: integration.llmobs_set_tags(span, args=args, kwargs=kwargs, response=chat_completions, operation="chat") span.finish() - integration.metric(span, "dist", "request.duration", span.duration_ns) - if integration.is_pc_sampled_log(span): - if chat_completions is None: - log_chat_completions = [] - else: - log_chat_completions = [ - [ - {"content": message.text, "message_type": message.message.__class__.__name__} - for message in messages - ] - for messages in chat_completions.generations - ] - integration.log( - span, - "info" if span.error == 0 else "error", - "sampled %s.%s" % (instance.__module__, instance.__class__.__name__), - attrs={ - "messages": [ - [ - { - "content": ( - message.get("content", "") - if isinstance(message, dict) - else str(getattr(message, "content", "")) - ), - "message_type": message.__class__.__name__, - } - for message in messages - ] - for messages in chat_messages - ], - "choices": log_chat_completions, - }, - ) return chat_completions @@ -502,7 +424,6 @@ async def traced_chat_model_agenerate(langchain, pin, func, instance, args, kwar chat_completions = await func(*args, **kwargs) if _is_openai_chat_instance(instance): _tag_openai_token_usage(span, chat_completions.llm_output) - integration.record_usage(span, chat_completions.llm_output) for message_set_idx, message_set in enumerate(chat_completions.generations): for idx, chat_completion in enumerate(message_set): @@ -542,45 +463,10 @@ async def traced_chat_model_agenerate(langchain, pin, func, instance, args, kwar ) except Exception: span.set_exc_info(*sys.exc_info()) - integration.metric(span, "incr", "request.error", 1) raise finally: integration.llmobs_set_tags(span, args=args, kwargs=kwargs, response=chat_completions, operation="chat") span.finish() - integration.metric(span, "dist", "request.duration", span.duration_ns) - if integration.is_pc_sampled_log(span): - if chat_completions is None: - log_chat_completions = [] - else: - log_chat_completions = [ - [ - {"content": message.text, "message_type": message.message.__class__.__name__} - for message in messages - ] - for messages in chat_completions.generations - ] - integration.log( - span, - "info" if span.error == 0 else "error", - "sampled %s.%s" % (instance.__module__, instance.__class__.__name__), - attrs={ - "messages": [ - [ - { - "content": ( - message.get("content", "") - if isinstance(message, dict) - else str(getattr(message, "content", "")) - ), - "message_type": message.__class__.__name__, - } - for message in messages - ] - for messages in chat_messages - ], - "choices": log_chat_completions, - }, - ) return chat_completions @@ -627,19 +513,10 @@ def traced_embedding(langchain, pin, func, instance, args, kwargs): span.set_metric("langchain.response.outputs.embedding_length", len(embeddings)) except Exception: span.set_exc_info(*sys.exc_info()) - integration.metric(span, "incr", "request.error", 1) raise finally: integration.llmobs_set_tags(span, args=args, kwargs=kwargs, response=embeddings, operation="embedding") span.finish() - integration.metric(span, "dist", "request.duration", span.duration_ns) - if integration.is_pc_sampled_log(span): - integration.log( - span, - "info" if span.error == 0 else "error", - "sampled %s.%s" % (instance.__module__, instance.__class__.__name__), - attrs={"inputs": [input_texts] if isinstance(input_texts, str) else input_texts}, - ) return embeddings @@ -689,12 +566,10 @@ def traced_lcel_runnable_sequence(langchain, pin, func, instance, args, kwargs): span.set_tag_str("langchain.response.outputs.%d" % idx, integration.trunc(str(output))) except Exception: span.set_exc_info(*sys.exc_info()) - integration.metric(span, "incr", "request.error", 1) raise finally: integration.llmobs_set_tags(span, args=[], kwargs=inputs, response=final_output, operation="chain") span.finish() - integration.metric(span, "dist", "request.duration", span.duration_ns) return final_output @@ -735,12 +610,10 @@ async def traced_lcel_runnable_sequence_async(langchain, pin, func, instance, ar span.set_tag_str("langchain.response.outputs.%d" % idx, integration.trunc(str(output))) except Exception: span.set_exc_info(*sys.exc_info()) - integration.metric(span, "incr", "request.error", 1) raise finally: integration.llmobs_set_tags(span, args=[], kwargs=inputs, response=final_output, operation="chain") span.finish() - integration.metric(span, "dist", "request.duration", span.duration_ns) return final_output @@ -793,25 +666,10 @@ def traced_similarity_search(langchain, pin, func, instance, args, kwargs): ) except Exception: span.set_exc_info(*sys.exc_info()) - integration.metric(span, "incr", "request.error", 1) raise finally: integration.llmobs_set_tags(span, args=args, kwargs=kwargs, response=documents, operation="retrieval") span.finish() - integration.metric(span, "dist", "request.duration", span.duration_ns) - if integration.is_pc_sampled_log(span): - integration.log( - span, - "info" if span.error == 0 else "error", - "sampled %s.%s" % (instance.__module__, instance.__class__.__name__), - attrs={ - "query": query, - "k": k or "", - "documents": [ - {"page_content": document.page_content, "metadata": document.metadata} for document in documents - ], - }, - ) return documents diff --git a/ddtrace/contrib/internal/openai/_endpoint_hooks.py b/ddtrace/contrib/internal/openai/_endpoint_hooks.py index 00ee44aef4b..786bb67f919 100644 --- a/ddtrace/contrib/internal/openai/_endpoint_hooks.py +++ b/ddtrace/contrib/internal/openai/_endpoint_hooks.py @@ -112,7 +112,6 @@ def shared_gen(): _process_finished_stream(integration, span, kwargs, streamed_chunks, is_completion=is_completion) finally: span.finish() - integration.metric(span, "dist", "request.duration", span.duration_ns) if _is_async_generator(resp): @@ -199,16 +198,6 @@ def _record_response(self, pin, integration, span, args, kwargs, resp, error): resp = super()._record_response(pin, integration, span, args, kwargs, resp, error) if kwargs.get("stream") and error is None: return self._handle_streamed_response(integration, span, kwargs, resp, is_completion=True) - if integration.is_pc_sampled_log(span): - attrs_dict = {"prompt": kwargs.get("prompt", "")} - if error is None: - log_choices = resp.choices - if hasattr(resp.choices[0], "model_dump"): - log_choices = [choice.model_dump() for choice in resp.choices] - attrs_dict.update({"choices": log_choices}) - integration.log( - span, "info" if error is None else "error", "sampled %s" % self.OPERATION_ID, attrs=attrs_dict - ) integration.llmobs_set_tags(span, args=[], kwargs=kwargs, response=resp, operation="completion") if not resp: return @@ -268,14 +257,6 @@ def _record_response(self, pin, integration, span, args, kwargs, resp, error): resp = super()._record_response(pin, integration, span, args, kwargs, resp, error) if kwargs.get("stream") and error is None: return self._handle_streamed_response(integration, span, kwargs, resp, is_completion=False) - if integration.is_pc_sampled_log(span): - log_choices = resp.choices - if hasattr(resp.choices[0], "model_dump"): - log_choices = [choice.model_dump() for choice in resp.choices] - attrs_dict = {"messages": kwargs.get("messages", []), "completion": log_choices} - integration.log( - span, "info" if error is None else "error", "sampled %s" % self.OPERATION_ID, attrs=attrs_dict - ) integration.llmobs_set_tags(span, args=[], kwargs=kwargs, response=resp, operation="chat") if not resp: return @@ -518,26 +499,6 @@ def _record_request(self, pin, integration, instance, span, args, kwargs): def _record_response(self, pin, integration, span, args, kwargs, resp, error): resp = super()._record_response(pin, integration, span, args, kwargs, resp, error) - if integration.is_pc_sampled_log(span): - attrs_dict = {} - if kwargs.get("response_format", "") == "b64_json": - attrs_dict.update({"choices": [{"b64_json": "returned"} for _ in resp.data]}) - else: - log_choices = resp.data - if hasattr(resp.data[0], "model_dump"): - log_choices = [choice.model_dump() for choice in resp.data] - attrs_dict.update({"choices": log_choices}) - if "prompt" in self._request_kwarg_params: - attrs_dict.update({"prompt": kwargs.get("prompt", "")}) - if "image" in self._request_kwarg_params: - image = args[0] if len(args) >= 1 else kwargs.get("image", "") - attrs_dict.update({"image": image.name.split("/")[-1]}) - if "mask" in self._request_kwarg_params: - mask = args[1] if len(args) >= 2 else kwargs.get("mask", "") - attrs_dict.update({"mask": mask.name.split("/")[-1]}) - integration.log( - span, "info" if error is None else "error", "sampled %s" % self.OPERATION_ID, attrs=attrs_dict - ) if not resp: return choices = resp.data @@ -629,19 +590,6 @@ def _record_response(self, pin, integration, span, args, kwargs, resp, error): span.set_metric("openai.response.segments_count", len(resp_to_tag.get("segments"))) if integration.is_pc_sampled_span(span): span.set_tag_str("openai.response.text", integration.trunc(text)) - if integration.is_pc_sampled_log(span): - file_input = args[1] if len(args) >= 2 else kwargs.get("file", "") - integration.log( - span, - "info" if error is None else "error", - "sampled %s" % self.OPERATION_ID, - attrs={ - "file": getattr(file_input, "name", "").split("/")[-1], - "prompt": kwargs.get("prompt", ""), - "language": kwargs.get("language", ""), - "text": text, - }, - ) return resp diff --git a/ddtrace/contrib/internal/openai/patch.py b/ddtrace/contrib/internal/openai/patch.py index 3696314acc4..812c786dfc4 100644 --- a/ddtrace/contrib/internal/openai/patch.py +++ b/ddtrace/contrib/internal/openai/patch.py @@ -10,7 +10,6 @@ from ddtrace.contrib.trace_utils import with_traced_module from ddtrace.contrib.trace_utils import wrap from ddtrace.internal.logger import get_logger -from ddtrace.internal.utils.formats import asbool from ddtrace.internal.utils.formats import deep_getattr from ddtrace.internal.utils.version import parse_version from ddtrace.llmobs._integrations import OpenAIIntegration @@ -23,10 +22,7 @@ config._add( "openai", { - "logs_enabled": asbool(os.getenv("DD_OPENAI_LOGS_ENABLED", False)), - "metrics_enabled": asbool(os.getenv("DD_OPENAI_METRICS_ENABLED", True)), "span_prompt_completion_sample_rate": float(os.getenv("DD_OPENAI_SPAN_PROMPT_COMPLETION_SAMPLE_RATE", 1.0)), - "log_prompt_completion_sample_rate": float(os.getenv("DD_OPENAI_LOG_PROMPT_COMPLETION_SAMPLE_RATE", 0.1)), "span_char_limit": int(os.getenv("DD_OPENAI_SPAN_CHAR_LIMIT", 128)), }, ) @@ -183,7 +179,6 @@ def _traced_endpoint(endpoint_hook, integration, instance, pin, args, kwargs): # Record any error information if err is not None: span.set_exc_info(*sys.exc_info()) - integration.metric(span, "incr", "request.error", 1) # Pass the response and the error to the hook try: @@ -196,7 +191,6 @@ def _traced_endpoint(endpoint_hook, integration, instance, pin, args, kwargs): # Streamed responses with error will need to be finished manually as well. if not kwargs.get("stream") or err is not None: span.finish() - integration.metric(span, "dist", "request.duration", span.duration_ns) def _patched_endpoint(openai, patch_hook): @@ -256,7 +250,6 @@ async def patched_endpoint(openai, pin, func, instance, args, kwargs): @with_traced_module def patched_convert(openai, pin, func, instance, args, kwargs): """Patch convert captures header information in the openai response""" - integration = openai._datadog_integration span = pin.tracer.current_span() if not span: return func(*args, **kwargs) @@ -281,23 +274,19 @@ def patched_convert(openai, pin, func, instance, args, kwargs): if headers.get("x-ratelimit-limit-requests"): v = headers.get("x-ratelimit-limit-requests") if v is not None: - integration.metric(span, "gauge", "ratelimit.requests", int(v)) span.set_metric("openai.organization.ratelimit.requests.limit", int(v)) if headers.get("x-ratelimit-limit-tokens"): v = headers.get("x-ratelimit-limit-tokens") if v is not None: - integration.metric(span, "gauge", "ratelimit.tokens", int(v)) span.set_metric("openai.organization.ratelimit.tokens.limit", int(v)) # Gauge and set span info for remaining requests and tokens if headers.get("x-ratelimit-remaining-requests"): v = headers.get("x-ratelimit-remaining-requests") if v is not None: - integration.metric(span, "gauge", "ratelimit.remaining.requests", int(v)) span.set_metric("openai.organization.ratelimit.requests.remaining", int(v)) if headers.get("x-ratelimit-remaining-tokens"): v = headers.get("x-ratelimit-remaining-tokens") if v is not None: - integration.metric(span, "gauge", "ratelimit.remaining.tokens", int(v)) span.set_metric("openai.organization.ratelimit.tokens.remaining", int(v)) return func(*args, **kwargs) diff --git a/ddtrace/internal/rate_limiter.py b/ddtrace/internal/rate_limiter.py index 0a97a6a7abc..9b514e5ff32 100644 --- a/ddtrace/internal/rate_limiter.py +++ b/ddtrace/internal/rate_limiter.py @@ -9,9 +9,6 @@ from typing import Callable # noqa:F401 from typing import Optional # noqa:F401 -from ddtrace.internal.utils.deprecations import DDTraceDeprecationWarning -from ddtrace.vendor.debtcollector import deprecate - class RateLimiter(object): """ @@ -57,26 +54,18 @@ def __init__(self, rate_limit: int, time_window: float = 1e9): self._lock = threading.Lock() - def is_allowed(self, timestamp_ns: Optional[int] = None) -> bool: + def is_allowed(self) -> bool: """ Check whether the current request is allowed or not This method will also reduce the number of available tokens by 1 - :param int timestamp_ns: timestamp in nanoseconds for the current request. :returns: Whether the current request is allowed or not :rtype: :obj:`bool` """ - if timestamp_ns is not None: - deprecate( - "The `timestamp_ns` parameter is deprecated and will be removed in a future version." - "Ratelimiter will use the current time.", - category=DDTraceDeprecationWarning, - ) - # rate limits are tested and mocked in pytest so we need to compute the timestamp here # (or move the unit tests to rust) - timestamp_ns = timestamp_ns or time.monotonic_ns() + timestamp_ns = time.monotonic_ns() allowed = self._is_allowed(timestamp_ns) # Update counts used to determine effective rate self._update_rate_counts(allowed, timestamp_ns) diff --git a/ddtrace/internal/remoteconfig/worker.py b/ddtrace/internal/remoteconfig/worker.py index 5429e599e74..08650bd8507 100644 --- a/ddtrace/internal/remoteconfig/worker.py +++ b/ddtrace/internal/remoteconfig/worker.py @@ -2,7 +2,6 @@ from typing import List # noqa:F401 from ddtrace.internal import agent -from ddtrace.internal import atexit from ddtrace.internal import forksafe from ddtrace.internal import periodic from ddtrace.internal.logger import get_logger @@ -132,9 +131,6 @@ def disable(self, join=False): if self.status == ServiceStatus.STOPPED: return - forksafe.unregister(self.reset_at_fork) - atexit.unregister(self.disable) - self.stop(join=join) def _stop_service(self, *args, **kwargs): diff --git a/ddtrace/internal/tracemethods.py b/ddtrace/internal/tracemethods.py index 5328797c09f..456cca597e1 100644 --- a/ddtrace/internal/tracemethods.py +++ b/ddtrace/internal/tracemethods.py @@ -4,8 +4,6 @@ import wrapt from ddtrace.internal.logger import get_logger -from ddtrace.internal.utils.deprecations import DDTraceDeprecationWarning -from ddtrace.vendor.debtcollector import deprecate log = get_logger(__name__) @@ -65,102 +63,10 @@ def _parse_trace_methods(raw_dd_trace_methods: str) -> List[Tuple[str, str]]: return dd_trace_methods -def _parse_legacy_trace_methods(raw_dd_trace_methods: str) -> List[str]: - """ - Return a list of method names to trace based on the specification of - DD_TRACE_METHODS. - - Note that support for wildcard methods with [*] is not implemented. - - This square bracket notation will be deprecated in favor of the new ':' notation - TODO: This method can be deleted once the legacy syntax is officially deprecated - """ - if not raw_dd_trace_methods: - return [] - dd_trace_methods = [] - for qualified_methods in raw_dd_trace_methods.split(";"): - # Validate that methods are specified - if "[" not in qualified_methods or "]" not in qualified_methods: - log.warning( - ( - "Invalid DD_TRACE_METHODS: %s. " - "Methods must be specified in square brackets following the fully qualified module or class name." - ), - qualified_methods, - ) - return [] - - # Store the prefix of the qualified method name (eg. for "foo.bar.baz[qux,quux]", this is "foo.bar.baz") - qualified_method_prefix = qualified_methods.split("[")[0] - - if qualified_method_prefix == "__main__": - # __main__ cannot be used since the __main__ that exists now is not the same as the __main__ that the user - # application will have. __main__ when sitecustomize module is run is the builtin __main__. - log.warning( - "Invalid DD_TRACE_METHODS: %s. Methods cannot be traced on the __main__ module.", qualified_methods - ) - return [] - - # Get the class or module name of the method (eg. for "foo.bar.baz[qux,quux]", this is "baz[qux,quux]") - class_or_module_with_methods = qualified_methods.split(".")[-1] - - # Strip off the leading 'moduleOrClass[' and trailing ']' - methods = class_or_module_with_methods.split("[")[1] - methods = methods[:-1] - - # Add the methods to the list of methods to trace - for method in methods.split(","): - if not str.isidentifier(method): - log.warning( - "Invalid method name: %r. %s", - method, - ( - "You might have a trailing comma." - if method == "" - else "Method names must be valid Python identifiers." - ), - ) - return [] - dd_trace_methods.append("%s.%s" % (qualified_method_prefix, method)) - return dd_trace_methods - - def _install_trace_methods(raw_dd_trace_methods: str) -> None: """Install tracing on the given methods.""" - if "[" in raw_dd_trace_methods: - deprecate( - "Using DD_TRACE_METHODS with the '[]' notation is deprecated", - message="Please use DD_TRACE_METHODS with the new ':' notation instead", - removal_version="3.0.0", - category=DDTraceDeprecationWarning, - ) - # Using legacy syntax - for qualified_method in _parse_legacy_trace_methods(raw_dd_trace_methods): - # We don't know if the method is a class method or a module method, so we need to assume it's a module - # and if the import fails then go a level up and try again. - base_module_guess = ".".join(qualified_method.split(".")[:-1]) - method_name = qualified_method.split(".")[-1] - module = None - - while base_module_guess: - try: - module = __import__(base_module_guess) - except ImportError: - # Add the class to the method name - method_name = "%s.%s" % (base_module_guess.split(".")[-1], method_name) - base_module_guess = ".".join(base_module_guess.split(".")[:-1]) - else: - break - - if module is None: - log.warning("Could not import module for %r", qualified_method) - continue - - trace_method(base_module_guess, method_name) - else: - # Using updated syntax, no need to try to import - for module_name, method_name in _parse_trace_methods(raw_dd_trace_methods): - trace_method(module_name, method_name) + for module_name, method_name in _parse_trace_methods(raw_dd_trace_methods): + trace_method(module_name, method_name) def trace_method(module, method_name): diff --git a/ddtrace/internal/wrapping/asyncs.py b/ddtrace/internal/wrapping/asyncs.py index 855578f9db2..d0ed131e962 100644 --- a/ddtrace/internal/wrapping/asyncs.py +++ b/ddtrace/internal/wrapping/asyncs.py @@ -537,96 +537,6 @@ """ ) -elif PY >= (3, 7): - COROUTINE_ASSEMBLY.parse( - r""" - get_awaitable - load_const None - yield_from - """ - ) - - ASYNC_GEN_ASSEMBLY.parse( - r""" - setup_except @stopiter - dup_top - store_fast $__ddgen - load_attr $asend - store_fast $__ddgensend - load_fast $__ddgen - load_attr $__anext__ - call_function 0 - - loop: - get_awaitable - load_const None - yield_from - - yield: - setup_except @genexit - yield_value - pop_block - load_fast $__ddgensend - rot_two - call_function 1 - jump_absolute @loop - - genexit: - dup_top - load_const GeneratorExit - compare_op asm.Compare.EXC_MATCH - pop_jump_if_false @exc - pop_top - pop_top - pop_top - pop_top - load_fast $__ddgen - load_attr $aclose - call_function 0 - get_awaitable - load_const None - yield_from - pop_except - return_value - - exc: - pop_top - pop_top - pop_top - pop_top - load_fast $__ddgen - load_attr $athrow - load_const sys.exc_info - call_function 0 - call_function_ex 0 - get_awaitable - load_const None - yield_from - store_fast $__value - pop_except - load_fast $__value - jump_absolute @yield - - stopiter: - dup_top - load_const StopAsyncIteration - compare_op asm.Compare.EXC_MATCH - pop_jump_if_false @propagate - pop_top - pop_top - pop_top - pop_except - load_const None - return_value - - propagate: - end_finally - load_const None - return_value - """ - ) - - else: msg = "No async wrapping support for Python %d.%d" % PY[:2] raise RuntimeError(msg) diff --git a/ddtrace/internal/wrapping/context.py b/ddtrace/internal/wrapping/context.py index cf36a93011b..393bd097da5 100644 --- a/ddtrace/internal/wrapping/context.py +++ b/ddtrace/internal/wrapping/context.py @@ -274,7 +274,7 @@ ) -elif sys.version_info >= (3, 7): +elif sys.version_info >= (3, 8): CONTEXT_HEAD.parse( r""" load_const {context} diff --git a/ddtrace/internal/wrapping/generators.py b/ddtrace/internal/wrapping/generators.py index f2a98b42a18..9ec5a654556 100644 --- a/ddtrace/internal/wrapping/generators.py +++ b/ddtrace/internal/wrapping/generators.py @@ -383,77 +383,6 @@ """ ) - -elif PY >= (3, 7): - GENERATOR_ASSEMBLY.parse( - r""" - setup_except @stopiter - dup_top - store_fast $__ddgen - load_attr $send - store_fast $__ddgensend - load_const next - load_fast $__ddgen - - loop: - call_function 1 - - yield: - setup_except @genexit - yield_value - pop_block - load_fast $__ddgensend - rot_two - jump_absolute @loop - - genexit: - dup_top - load_const GeneratorExit - compare_op asm.Compare.EXC_MATCH - pop_jump_if_false @exc - pop_top - pop_top - pop_top - pop_top - load_fast $__ddgen - load_attr $close - call_function 0 - return_value - - exc: - pop_top - pop_top - pop_top - pop_top - load_fast $__ddgen - load_attr $throw - load_const sys.exc_info - call_function 0 - call_function_ex 0 - store_fast $__value - pop_except - load_fast $__value - jump_absolute @yield - - stopiter: - dup_top - load_const StopIteration - compare_op asm.Compare.EXC_MATCH - pop_jump_if_false @propagate - pop_top - pop_top - pop_top - pop_except - load_const None - return_value - - propagate: - end_finally - load_const None - return_value - """ - ) - else: msg = "No generator wrapping support for Python %d.%d" % PY[:2] raise RuntimeError(msg) diff --git a/ddtrace/llmobs/_integrations/anthropic.py b/ddtrace/llmobs/_integrations/anthropic.py index a3224a083cd..bb4f96e7814 100644 --- a/ddtrace/llmobs/_integrations/anthropic.py +++ b/ddtrace/llmobs/_integrations/anthropic.py @@ -7,16 +7,14 @@ from ddtrace.internal.logger import get_logger from ddtrace.llmobs._constants import INPUT_MESSAGES -from ddtrace.llmobs._constants import INPUT_TOKENS_METRIC_KEY from ddtrace.llmobs._constants import METADATA from ddtrace.llmobs._constants import METRICS from ddtrace.llmobs._constants import MODEL_NAME from ddtrace.llmobs._constants import MODEL_PROVIDER from ddtrace.llmobs._constants import OUTPUT_MESSAGES -from ddtrace.llmobs._constants import OUTPUT_TOKENS_METRIC_KEY from ddtrace.llmobs._constants import SPAN_KIND -from ddtrace.llmobs._constants import TOTAL_TOKENS_METRIC_KEY from ddtrace.llmobs._integrations.base import BaseLLMIntegration +from ddtrace.llmobs._integrations.utils import get_llmobs_metrics_tags from ddtrace.llmobs._utils import _get_attr from ddtrace.trace import Span @@ -77,7 +75,7 @@ def _llmobs_set_tags( INPUT_MESSAGES: input_messages, METADATA: parameters, OUTPUT_MESSAGES: output_messages, - METRICS: self._get_llmobs_metrics_tags(span), + METRICS: get_llmobs_metrics_tags("anthropic", span), } ) @@ -188,18 +186,3 @@ def record_usage(self, span: Span, usage: Dict[str, Any]) -> None: span.set_metric("anthropic.response.usage.output_tokens", output_tokens) if input_tokens is not None and output_tokens is not None: span.set_metric("anthropic.response.usage.total_tokens", input_tokens + output_tokens) - - @staticmethod - def _get_llmobs_metrics_tags(span): - usage = {} - input_tokens = span.get_metric("anthropic.response.usage.input_tokens") - output_tokens = span.get_metric("anthropic.response.usage.output_tokens") - total_tokens = span.get_metric("anthropic.response.usage.total_tokens") - - if input_tokens is not None: - usage[INPUT_TOKENS_METRIC_KEY] = input_tokens - if output_tokens is not None: - usage[OUTPUT_TOKENS_METRIC_KEY] = output_tokens - if total_tokens is not None: - usage[TOTAL_TOKENS_METRIC_KEY] = total_tokens - return usage diff --git a/ddtrace/llmobs/_integrations/bedrock.py b/ddtrace/llmobs/_integrations/bedrock.py index ac6092cbe1a..cbc1456fc24 100644 --- a/ddtrace/llmobs/_integrations/bedrock.py +++ b/ddtrace/llmobs/_integrations/bedrock.py @@ -5,18 +5,16 @@ from ddtrace.internal.logger import get_logger from ddtrace.llmobs._constants import INPUT_MESSAGES -from ddtrace.llmobs._constants import INPUT_TOKENS_METRIC_KEY from ddtrace.llmobs._constants import METADATA from ddtrace.llmobs._constants import METRICS from ddtrace.llmobs._constants import MODEL_NAME from ddtrace.llmobs._constants import MODEL_PROVIDER from ddtrace.llmobs._constants import OUTPUT_MESSAGES -from ddtrace.llmobs._constants import OUTPUT_TOKENS_METRIC_KEY from ddtrace.llmobs._constants import PARENT_ID_KEY from ddtrace.llmobs._constants import PROPAGATED_PARENT_ID_KEY from ddtrace.llmobs._constants import SPAN_KIND -from ddtrace.llmobs._constants import TOTAL_TOKENS_METRIC_KEY from ddtrace.llmobs._integrations import BaseLLMIntegration +from ddtrace.llmobs._integrations.utils import get_llmobs_metrics_tags from ddtrace.llmobs._utils import _get_llmobs_parent_id from ddtrace.trace import Span @@ -57,22 +55,11 @@ def _llmobs_set_tags( MODEL_PROVIDER: span.get_tag("bedrock.request.model_provider") or "", INPUT_MESSAGES: input_messages, METADATA: parameters, - METRICS: self._llmobs_metrics(span, response), + METRICS: get_llmobs_metrics_tags("bedrock", span), OUTPUT_MESSAGES: output_messages, } ) - @staticmethod - def _llmobs_metrics(span: Span, response: Optional[Dict[str, Any]]) -> Dict[str, Any]: - metrics = {} - if response and response.get("text"): - prompt_tokens = int(span.get_tag("bedrock.usage.prompt_tokens") or 0) - completion_tokens = int(span.get_tag("bedrock.usage.completion_tokens") or 0) - metrics[INPUT_TOKENS_METRIC_KEY] = prompt_tokens - metrics[OUTPUT_TOKENS_METRIC_KEY] = completion_tokens - metrics[TOTAL_TOKENS_METRIC_KEY] = prompt_tokens + completion_tokens - return metrics - @staticmethod def _extract_input_message(prompt): """Extract input messages from the stored prompt. diff --git a/ddtrace/llmobs/_integrations/gemini.py b/ddtrace/llmobs/_integrations/gemini.py index ecec71e0645..0407ec7188b 100644 --- a/ddtrace/llmobs/_integrations/gemini.py +++ b/ddtrace/llmobs/_integrations/gemini.py @@ -14,7 +14,7 @@ from ddtrace.llmobs._constants import SPAN_KIND from ddtrace.llmobs._integrations.base import BaseLLMIntegration from ddtrace.llmobs._integrations.utils import extract_message_from_part_google -from ddtrace.llmobs._integrations.utils import get_llmobs_metrics_tags_google +from ddtrace.llmobs._integrations.utils import get_llmobs_metrics_tags from ddtrace.llmobs._integrations.utils import get_system_instructions_from_google_model from ddtrace.llmobs._integrations.utils import llmobs_get_metadata_google from ddtrace.llmobs._utils import _get_attr @@ -59,7 +59,7 @@ def _llmobs_set_tags( METADATA: metadata, INPUT_MESSAGES: input_messages, OUTPUT_MESSAGES: output_messages, - METRICS: get_llmobs_metrics_tags_google("google_generativeai", span), + METRICS: get_llmobs_metrics_tags("google_generativeai", span), } ) diff --git a/ddtrace/llmobs/_integrations/langchain.py b/ddtrace/llmobs/_integrations/langchain.py index c6a77fad3bc..d380c6ab7a8 100644 --- a/ddtrace/llmobs/_integrations/langchain.py +++ b/ddtrace/llmobs/_integrations/langchain.py @@ -6,8 +6,6 @@ from typing import Optional from typing import Union -from ddtrace import config -from ddtrace.constants import ERROR_TYPE from ddtrace.internal.logger import get_logger from ddtrace.internal.utils import ArgumentError from ddtrace.internal.utils import get_argument_value @@ -454,54 +452,6 @@ def _set_base_span_tags( # type: ignore[override] else: span.set_tag_str(API_KEY, api_key) - @classmethod - def _logs_tags(cls, span: Span) -> str: - api_key = span.get_tag(API_KEY) or "" - tags = "env:%s,version:%s,%s:%s,%s:%s,%s:%s,%s:%s" % ( # noqa: E501 - (config.env or ""), - (config.version or ""), - PROVIDER, - (span.get_tag(PROVIDER) or ""), - MODEL, - (span.get_tag(MODEL) or ""), - TYPE, - (span.get_tag(TYPE) or ""), - API_KEY, - api_key, - ) - return tags - - @classmethod - def _metrics_tags(cls, span: Span) -> List[str]: - provider = span.get_tag(PROVIDER) or "" - api_key = span.get_tag(API_KEY) or "" - tags = [ - "version:%s" % (config.version or ""), - "env:%s" % (config.env or ""), - "service:%s" % (span.service or ""), - "%s:%s" % (PROVIDER, provider), - "%s:%s" % (MODEL, span.get_tag(MODEL) or ""), - "%s:%s" % (TYPE, span.get_tag(TYPE) or ""), - "%s:%s" % (API_KEY, api_key), - "error:%d" % span.error, - ] - err_type = span.get_tag(ERROR_TYPE) - if err_type: - tags.append("%s:%s" % (ERROR_TYPE, err_type)) - return tags - - def record_usage(self, span: Span, usage: Dict[str, Any]) -> None: - if not usage or self.metrics_enabled is False: - return - for token_type in ("prompt", "completion", "total"): - num_tokens = usage.get("token_usage", {}).get(token_type + "_tokens") - if not num_tokens: - continue - self.metric(span, "dist", "tokens.%s" % token_type, num_tokens) - total_cost = span.get_metric(TOTAL_COST) - if total_cost: - self.metric(span, "incr", "tokens.total_cost", total_cost) - def check_token_usage_chat_or_llm_result(self, result): """Checks for token usage on the top-level ChatResult or LLMResult object""" llm_output = getattr(result, "llm_output", {}) diff --git a/ddtrace/llmobs/_integrations/openai.py b/ddtrace/llmobs/_integrations/openai.py index 7ed3aace08a..eb01a679191 100644 --- a/ddtrace/llmobs/_integrations/openai.py +++ b/ddtrace/llmobs/_integrations/openai.py @@ -5,7 +5,6 @@ from typing import Optional from typing import Tuple -from ddtrace import config from ddtrace.internal.constants import COMPONENT from ddtrace.internal.utils.version import parse_version from ddtrace.llmobs._constants import INPUT_DOCUMENTS @@ -21,6 +20,7 @@ from ddtrace.llmobs._constants import SPAN_KIND from ddtrace.llmobs._constants import TOTAL_TOKENS_METRIC_KEY from ddtrace.llmobs._integrations.base import BaseLLMIntegration +from ddtrace.llmobs._integrations.utils import get_llmobs_metrics_tags from ddtrace.llmobs._utils import _get_attr from ddtrace.llmobs.utils import Document from ddtrace.trace import Pin @@ -88,54 +88,14 @@ def _is_azure_openai(span): return False return "azure" in base_url.lower() - @classmethod - def _logs_tags(cls, span: Span) -> str: - tags = ( - "env:%s,version:%s,openai.request.endpoint:%s,openai.request.method:%s,openai.request.model:%s,openai.organization.name:%s," - "openai.user.api_key:%s" - % ( # noqa: E501 - (config.env or ""), - (config.version or ""), - (span.get_tag("openai.request.endpoint") or ""), - (span.get_tag("openai.request.method") or ""), - (span.get_tag("openai.request.model") or ""), - (span.get_tag("openai.organization.name") or ""), - (span.get_tag("openai.user.api_key") or ""), - ) - ) - return tags - - @classmethod - def _metrics_tags(cls, span: Span) -> List[str]: - model_name = span.get_tag("openai.request.model") or "" - tags = [ - "version:%s" % (config.version or ""), - "env:%s" % (config.env or ""), - "service:%s" % (span.service or ""), - "openai.request.model:%s" % model_name, - "model:%s" % model_name, - "openai.request.endpoint:%s" % (span.get_tag("openai.request.endpoint") or ""), - "openai.request.method:%s" % (span.get_tag("openai.request.method") or ""), - "openai.organization.id:%s" % (span.get_tag("openai.organization.id") or ""), - "openai.organization.name:%s" % (span.get_tag("openai.organization.name") or ""), - "openai.user.api_key:%s" % (span.get_tag("openai.user.api_key") or ""), - "error:%d" % span.error, - ] - err_type = span.get_tag("error.type") - if err_type: - tags.append("error_type:%s" % err_type) - return tags - def record_usage(self, span: Span, usage: Dict[str, Any]) -> None: - if not usage or not self.metrics_enabled: + if not usage: return - tags = ["openai.estimated:false"] for token_type in ("prompt", "completion", "total"): num_tokens = getattr(usage, token_type + "_tokens", None) if not num_tokens: continue span.set_metric("openai.response.usage.%s_tokens" % token_type, num_tokens) - self.metric(span, "dist", "tokens.%s" % token_type, num_tokens, tags=tags) def _llmobs_set_tags( self, @@ -275,12 +235,4 @@ def _extract_llmobs_metrics_tags(span: Span, resp: Any) -> Dict[str, Any]: OUTPUT_TOKENS_METRIC_KEY: completion_tokens, TOTAL_TOKENS_METRIC_KEY: prompt_tokens + completion_tokens, } - prompt_tokens = span.get_metric("openai.response.usage.prompt_tokens") - completion_tokens = span.get_metric("openai.response.usage.completion_tokens") - if prompt_tokens is None or completion_tokens is None: - return {} - return { - INPUT_TOKENS_METRIC_KEY: prompt_tokens, - OUTPUT_TOKENS_METRIC_KEY: completion_tokens, - TOTAL_TOKENS_METRIC_KEY: prompt_tokens + completion_tokens, - } + return get_llmobs_metrics_tags("openai", span) diff --git a/ddtrace/llmobs/_integrations/utils.py b/ddtrace/llmobs/_integrations/utils.py index f180e0c1820..fb1dbf3a1fd 100644 --- a/ddtrace/llmobs/_integrations/utils.py +++ b/ddtrace/llmobs/_integrations/utils.py @@ -118,10 +118,16 @@ def extract_message_from_part_google(part, role=None): return message -def get_llmobs_metrics_tags_google(integration_name, span): +def get_llmobs_metrics_tags(integration_name, span): usage = {} - input_tokens = span.get_metric("%s.response.usage.prompt_tokens" % integration_name) - output_tokens = span.get_metric("%s.response.usage.completion_tokens" % integration_name) + + # check for both prompt / completion or input / output tokens + input_tokens = span.get_metric("%s.response.usage.prompt_tokens" % integration_name) or span.get_metric( + "%s.response.usage.input_tokens" % integration_name + ) + output_tokens = span.get_metric("%s.response.usage.completion_tokens" % integration_name) or span.get_metric( + "%s.response.usage.output_tokens" % integration_name + ) total_tokens = span.get_metric("%s.response.usage.total_tokens" % integration_name) if input_tokens is not None: diff --git a/ddtrace/llmobs/_integrations/vertexai.py b/ddtrace/llmobs/_integrations/vertexai.py index 88d38f1975e..db40ac15b19 100644 --- a/ddtrace/llmobs/_integrations/vertexai.py +++ b/ddtrace/llmobs/_integrations/vertexai.py @@ -15,7 +15,7 @@ from ddtrace.llmobs._constants import SPAN_KIND from ddtrace.llmobs._integrations.base import BaseLLMIntegration from ddtrace.llmobs._integrations.utils import extract_message_from_part_google -from ddtrace.llmobs._integrations.utils import get_llmobs_metrics_tags_google +from ddtrace.llmobs._integrations.utils import get_llmobs_metrics_tags from ddtrace.llmobs._integrations.utils import get_system_instructions_from_google_model from ddtrace.llmobs._integrations.utils import llmobs_get_metadata_google from ddtrace.llmobs._utils import _get_attr @@ -65,7 +65,7 @@ def _llmobs_set_tags( METADATA: metadata, INPUT_MESSAGES: input_messages, OUTPUT_MESSAGES: output_messages, - METRICS: get_llmobs_metrics_tags_google("vertexai", span), + METRICS: get_llmobs_metrics_tags("vertexai", span), } ) diff --git a/ddtrace/opentracer/tracer.py b/ddtrace/opentracer/tracer.py index ca10cb8125a..65d1b95b314 100644 --- a/ddtrace/opentracer/tracer.py +++ b/ddtrace/opentracer/tracer.py @@ -18,7 +18,6 @@ from ddtrace.trace import Context as DatadogContext # noqa:F401 from ddtrace.trace import Span as DatadogSpan from ddtrace.trace import Tracer as DatadogTracer -from ddtrace.vendor.debtcollector import deprecate from ..internal.logger import get_logger from .propagation import HTTPPropagator @@ -55,7 +54,7 @@ def __init__( service_name: Optional[str] = None, config: Optional[Dict[str, Any]] = None, scope_manager: Optional[ScopeManager] = None, - dd_tracer: Optional[DatadogTracer] = None, + _dd_tracer: Optional[DatadogTracer] = None, ) -> None: """Initialize a new Datadog opentracer. @@ -70,9 +69,6 @@ def __init__( here: https://github.com/opentracing/opentracing-python#scope-managers. If ``None`` is provided, defaults to :class:`opentracing.scope_managers.ThreadLocalScopeManager`. - :param dd_tracer: (optional) the Datadog tracer for this tracer to use. This - parameter is deprecated and will be removed in v3.0.0. The - to the global tracer (``ddtrace.tracer``) should always be used. """ # Merge the given config with the default into a new dict self._config = DEFAULT_CONFIG.copy() @@ -100,14 +96,7 @@ def __init__( self._scope_manager = scope_manager or ThreadLocalScopeManager() dd_context_provider = get_context_provider_for_scope_manager(self._scope_manager) - if dd_tracer is not None: - deprecate( - "The ``dd_tracer`` parameter is deprecated", - message="The global tracer (``ddtrace.tracer``) will be used instead.", - removal_version="3.0.0", - ) - - self._dd_tracer = dd_tracer or ddtrace.tracer + self._dd_tracer = _dd_tracer or ddtrace.tracer self._dd_tracer.set_tags(self._config.get(keys.GLOBAL_TAGS)) # type: ignore[arg-type] trace_processors = None if keys.SETTINGS in self._config: @@ -121,7 +110,7 @@ def __init__( trace_processors=trace_processors, priority_sampling=self._config.get(keys.PRIORITY_SAMPLING), uds_path=self._config.get(keys.UDS_PATH), - context_provider=dd_context_provider, # type: ignore[arg-type] + context_provider=dd_context_provider, ) self._propagators = { Format.HTTP_HEADERS: HTTPPropagator, diff --git a/ddtrace/settings/_config.py b/ddtrace/settings/_config.py index 35d2849884d..0072986286e 100644 --- a/ddtrace/settings/_config.py +++ b/ddtrace/settings/_config.py @@ -16,8 +16,6 @@ from ddtrace.internal.serverless import in_gcp_function from ddtrace.internal.telemetry import telemetry_writer from ddtrace.internal.utils.cache import cachedmethod -from ddtrace.internal.utils.deprecations import DDTraceDeprecationWarning -from ddtrace.vendor.debtcollector import deprecate from .._trace.pin import Pin from ..internal import gitmetadata @@ -264,9 +262,11 @@ def _parse_global_tags(s): def _default_config() -> Dict[str, _ConfigItem]: return { + # Remove the _trace_sample_rate property, _trace_sampling_rules should be the source of truth "_trace_sample_rate": _ConfigItem( default=1.0, - envs=[("DD_TRACE_SAMPLE_RATE", float)], + # trace_sample_rate is placeholder, this code will be removed up after v3.0 + envs=[("trace_sample_rate", float)], ), "_trace_sampling_rules": _ConfigItem( default=lambda: "", @@ -352,14 +352,6 @@ def __init__(self): self._from_endpoint = ENDPOINT_FETCHED_CONFIG self._config = _default_config() - sample_rate = os.getenv("DD_TRACE_SAMPLE_RATE") - if sample_rate is not None: - deprecate( - "DD_TRACE_SAMPLE_RATE is deprecated", - message="Please use DD_TRACE_SAMPLING_RULES instead.", - removal_version="3.0.0", - ) - # Use a dict as underlying storing mechanism for integration configs self._integration_configs = {} @@ -368,9 +360,6 @@ def __init__(self): rate_limit = os.getenv("DD_TRACE_RATE_LIMIT") if rate_limit is not None and self._trace_sampling_rules in ("", "[]"): - # This warning will be logged when DD_TRACE_SAMPLE_RATE is set. This is intentional. - # Even though DD_TRACE_SAMPLE_RATE is treated as a global trace sampling rule, this configuration - # is deprecated. We should always encourage users to set DD_TRACE_SAMPLING_RULES instead. log.warning( "DD_TRACE_RATE_LIMIT is set to %s and DD_TRACE_SAMPLING_RULES is not set. " "Tracer rate limiting is only applied to spans that match tracer sampling rules. " @@ -388,13 +377,9 @@ def __init__(self): ) self._trace_api = _get_config("DD_TRACE_API_VERSION") if self._trace_api == "v0.3": - deprecate( - "DD_TRACE_API_VERSION=v0.3 is deprecated", - message="Traces will be submitted to the v0.4/traces agent endpoint instead.", - removal_version="3.0.0", - category=DDTraceDeprecationWarning, + log.error( + "Setting DD_TRACE_API_VERSION to ``v0.3`` is not supported. The default ``v0.5`` format will be used.", ) - self._trace_api = "v0.4" self._trace_writer_buffer_size = _get_config("DD_TRACE_WRITER_BUFFER_SIZE_BYTES", DEFAULT_BUFFER_SIZE, int) self._trace_writer_payload_size = _get_config( "DD_TRACE_WRITER_MAX_PAYLOAD_SIZE_BYTES", DEFAULT_MAX_PAYLOAD_SIZE, int @@ -418,18 +403,8 @@ def __init__(self): self._span_traceback_max_size = _get_config("DD_TRACE_SPAN_TRACEBACK_MAX_SIZE", 30, int) - # Master switch for turning on and off trace search by default - # this weird invocation of getenv is meant to read the DD_ANALYTICS_ENABLED - # legacy environment variable. It should be removed in the future - self._analytics_enabled = _get_config(["DD_TRACE_ANALYTICS_ENABLED", "DD_ANALYTICS_ENABLED"], False, asbool) - if self._analytics_enabled: - deprecate( - "Datadog App Analytics is deprecated and will be removed in a future version. " - "App Analytics can be enabled via DD_TRACE_ANALYTICS_ENABLED and DD_ANALYTICS_ENABLED " - "environment variables and ddtrace.config.analytics_enabled configuration. " - "These configurations will also be removed.", - category=DDTraceDeprecationWarning, - ) + # DD_ANALYTICS_ENABLED is not longer supported, remove this functionatiy from all integrations in the future + self._analytics_enabled = False self._client_ip_header = _get_config("DD_TRACE_CLIENT_IP_HEADER") self._retrieve_client_ip = _get_config("DD_TRACE_CLIENT_IP_ENABLED", False, asbool) @@ -477,14 +452,6 @@ def __init__(self): self._128_bit_trace_id_enabled = _get_config("DD_TRACE_128_BIT_TRACEID_GENERATION_ENABLED", True, asbool) self._128_bit_trace_id_logging_enabled = _get_config("DD_TRACE_128_BIT_TRACEID_LOGGING_ENABLED", False, asbool) - if self._128_bit_trace_id_logging_enabled: - deprecate( - "Using DD_TRACE_128_BIT_TRACEID_LOGGING_ENABLED is deprecated.", - message="Log injection format is now configured automatically.", - removal_version="3.0.0", - category=DDTraceDeprecationWarning, - ) - self._sampling_rules = _get_config("DD_SPAN_SAMPLING_RULES") self._sampling_rules_file = _get_config("DD_SPAN_SAMPLING_RULES_FILE") @@ -536,18 +503,7 @@ def __init__(self): ["DD_TRACE_COMPUTE_STATS", "DD_TRACE_STATS_COMPUTATION_ENABLED"], trace_compute_stats_default, asbool ) self._data_streams_enabled = _get_config("DD_DATA_STREAMS_ENABLED", False, asbool) - - legacy_client_tag_enabled = _get_config("DD_HTTP_CLIENT_TAG_QUERY_STRING") - if legacy_client_tag_enabled is None: - self._http_client_tag_query_string = _get_config("DD_TRACE_HTTP_CLIENT_TAG_QUERY_STRING", "true") - else: - deprecate( - "DD_HTTP_CLIENT_TAG_QUERY_STRING is deprecated", - message="Please use DD_TRACE_HTTP_CLIENT_TAG_QUERY_STRING instead.", - removal_version="3.0.0", - category=DDTraceDeprecationWarning, - ) - self._http_client_tag_query_string = legacy_client_tag_enabled.lower() + self._http_client_tag_query_string = _get_config("DD_TRACE_HTTP_CLIENT_TAG_QUERY_STRING", "true") dd_trace_obfuscation_query_string_regexp = _get_config( "DD_TRACE_OBFUSCATION_QUERY_STRING_REGEXP", DD_TRACE_OBFUSCATION_QUERY_STRING_REGEXP_DEFAULT @@ -577,15 +533,8 @@ def __init__(self): # https://github.com/open-telemetry/opentelemetry-python/blob/v1.16.0/opentelemetry-api/src/opentelemetry/context/__init__.py#L53 os.environ["OTEL_PYTHON_CONTEXT"] = "ddcontextvars_context" self._subscriptions = [] # type: List[Tuple[List[str], Callable[[Config, List[str]], None]]] - self._span_aggregator_rlock = _get_config("DD_TRACE_SPAN_AGGREGATOR_RLOCK", True, asbool) - if self._span_aggregator_rlock is False: - deprecate( - "DD_TRACE_SPAN_AGGREGATOR_RLOCK is deprecated", - message="Soon the ddtrace library will only support using threading.Rlock to " - "aggregate and encode span data. If you need to disable the re-entrant lock and " - "revert to using threading.Lock, please contact Datadog support.", - removal_version="3.0.0", - ) + # Disabled Span Aggregator Rlock is not supported. Remove this configuration in the future + self._span_aggregator_rlock = True self._trace_methods = _get_config("DD_TRACE_METHODS") diff --git a/ddtrace/settings/_otel_remapper.py b/ddtrace/settings/_otel_remapper.py index ec238e8a3cb..e495f783cd3 100644 --- a/ddtrace/settings/_otel_remapper.py +++ b/ddtrace/settings/_otel_remapper.py @@ -52,12 +52,16 @@ def _remap_traces_sampler(otel_value: str) -> Optional[str]: otel_value, ) otel_value = f"parentbased_{otel_value}" + rate = None if otel_value == "parentbased_always_on": - return "1.0" + rate = "1.0" elif otel_value == "parentbased_always_off": - return "0.0" + rate = "0.0" elif otel_value == "parentbased_traceidratio": - return os.environ.get("OTEL_TRACES_SAMPLER_ARG", "1") + rate = os.environ.get("OTEL_TRACES_SAMPLER_ARG", "1") + + if rate is not None: + return f'[{{"sample_rate":{rate}}}]' return None @@ -130,7 +134,7 @@ def _remap_default(otel_value: str) -> Optional[str]: "OTEL_SERVICE_NAME": ("DD_SERVICE", _remap_default), "OTEL_LOG_LEVEL": ("DD_TRACE_DEBUG", _remap_otel_log_level), "OTEL_PROPAGATORS": ("DD_TRACE_PROPAGATION_STYLE", _remap_otel_propagators), - "OTEL_TRACES_SAMPLER": ("DD_TRACE_SAMPLE_RATE", _remap_traces_sampler), + "OTEL_TRACES_SAMPLER": ("DD_TRACE_SAMPLING_RULES", _remap_traces_sampler), "OTEL_TRACES_EXPORTER": ("DD_TRACE_ENABLED", _remap_traces_exporter), "OTEL_METRICS_EXPORTER": ("DD_RUNTIME_METRICS_ENABLED", _remap_metrics_exporter), "OTEL_LOGS_EXPORTER": ("", _validate_logs_exporter), # Does not set a DDTRACE environment variable. diff --git a/ddtrace/settings/config.py b/ddtrace/settings/config.py deleted file mode 100644 index 00c0ee9917c..00000000000 --- a/ddtrace/settings/config.py +++ /dev/null @@ -1,11 +0,0 @@ -from ddtrace.internal.utils.deprecations import DDTraceDeprecationWarning -from ddtrace.settings._config import * # noqa: F403 -from ddtrace.vendor.debtcollector import deprecate - - -deprecate( - "The ddtrace.settings.config module is deprecated", - message="Access the global configuration using ``ddtrace.config``.", - category=DDTraceDeprecationWarning, - removal_version="3.0.0", -) diff --git a/ddtrace/settings/integration.py b/ddtrace/settings/integration.py index 354e99f7625..eef7f5c81c6 100644 --- a/ddtrace/settings/integration.py +++ b/ddtrace/settings/integration.py @@ -1,13 +1,8 @@ import os from typing import Optional # noqa:F401 -from typing import Tuple # noqa:F401 - -from ddtrace.internal.utils.deprecations import DDTraceDeprecationWarning -from ddtrace.vendor.debtcollector import deprecate from .._hooks import Hooks from ..internal.utils.attrdict import AttrDict -from ..internal.utils.formats import asbool from .http import HttpConfig @@ -43,9 +38,10 @@ def __init__(self, global_config, name, *args, **kwargs): object.__setattr__(self, "hooks", Hooks()) object.__setattr__(self, "http", HttpConfig()) - analytics_enabled, analytics_sample_rate = self._get_analytics_settings() - self.setdefault("analytics_enabled", analytics_enabled) - self.setdefault("analytics_sample_rate", float(analytics_sample_rate)) + # Trace Analytics was removed in v3.0.0 + # TODO(munir): Remove all references to analytics_enabled and analytics_sample_rate + self.setdefault("analytics_enabled", False) + self.setdefault("analytics_sample_rate", 1.0) service = os.getenv( "DD_%s_SERVICE" % name.upper(), default=os.getenv( @@ -65,33 +61,6 @@ def __init__(self, global_config, name, *args, **kwargs): self.get_http_tag_query_string(getattr(self, "default_http_tag_query_string", None)), ) - def _get_analytics_settings(self): - # type: () -> Tuple[Optional[bool], float] - # Set default analytics configuration, default is disabled - # DEV: Default to `None` which means do not set this key - # Inject environment variables for integration - env = "DD_TRACE_%s_ANALYTICS_ENABLED" % self.integration_name.upper() - legacy_env = "DD_%s_ANALYTICS_ENABLED" % self.integration_name.upper() - analytics_enabled = asbool(os.getenv(env, os.getenv(legacy_env, default=None))) - - if analytics_enabled: - deprecate( - "Datadog App Analytics is deprecated. " - f"App Analytics can be enabled via {env} and {legacy_env} " - f"environment variables and the ddtrace.config.{self.integration_name}.analytics_enabled configuration." - " This feature and its associated configurations will be removed in a future release.", - category=DDTraceDeprecationWarning, - ) - - analytics_sample_rate = float( - os.getenv( - "DD_TRACE_%s_ANALYTICS_SAMPLE_RATE" % self.integration_name.upper(), - os.getenv("DD_%s_ANALYTICS_SAMPLE_RATE" % self.integration_name.upper(), default=1.0), - ) - ) - - return analytics_enabled, analytics_sample_rate - def get_http_tag_query_string(self, value): if self.global_config._http_tag_query_string: dd_http_server_tag_query_string = value if value else os.getenv("DD_HTTP_SERVER_TAG_QUERY_STRING", "true") diff --git a/docs/advanced_usage.rst b/docs/advanced_usage.rst index 5fe21321680..e6ead60c1f5 100644 --- a/docs/advanced_usage.rst +++ b/docs/advanced_usage.rst @@ -1,28 +1,6 @@ Advanced Usage ============== -.. _agentconfiguration: - -Agent Configuration -------------------- - -If the Datadog Agent is on a separate host from your application, you can modify -the default ``ddtrace.tracer`` object to utilize another hostname and port. Here -is a small example showcasing this:: - - from ddtrace.trace import tracer - - tracer.configure(hostname=, port=, https=) - -By default, these will be set to ``localhost``, ``8126``, and ``False`` respectively. - -You can also use a Unix Domain Socket to connect to the agent:: - - from ddtrace.trace import tracer - - tracer.configure(uds_path="/path/to/socket") - - .. _context: @@ -223,7 +201,7 @@ provider can be used. It must implement the :class:`ddtrace.trace.BaseContextProvider` interface and can be configured with:: - tracer.configure(context_provider=MyContextProvider) + tracer.configure(context_provider=MyContextProvider()) .. _disttracing: diff --git a/docs/configuration.rst b/docs/configuration.rst index f45ac992582..6f5c87a945e 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -817,6 +817,7 @@ Sampling version_added: v0.33.0: v2.15.0: Only applied when DD_TRACE_SAMPLE_RATE, DD_TRACE_SAMPLING_RULES, or DD_SPAN_SAMPLING_RULE are set. + v3.0.0: Only applied when DD_TRACE_SAMPLING_RULES or DD_SPAN_SAMPLING_RULE are set. DD_TRACE_SAMPLING_RULES: type: JSON array diff --git a/hatch.toml b/hatch.toml index 3e80f24a5e7..74dcba41602 100644 --- a/hatch.toml +++ b/hatch.toml @@ -399,6 +399,13 @@ flask = ["~=2.2"] python = ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] flask = ["~=3.0"] +[[envs.appsec_integrations_flask.matrix]] +# werkzeug 3.1 drops support for py3.8 +python = ["3.11", "3.12", "3.13"] +flask = ["~=3.1"] +werkzeug = ["~=3.1"] + +## ASM appsec_integrations_fastapi [envs.appsec_integrations_fastapi] template = "appsec_integrations_fastapi" diff --git a/lib-injection/sources/sitecustomize.py b/lib-injection/sources/sitecustomize.py index 32ab1c31ff3..0daa9c2413a 100644 --- a/lib-injection/sources/sitecustomize.py +++ b/lib-injection/sources/sitecustomize.py @@ -35,7 +35,7 @@ def parse_version(version): SCRIPT_DIR = os.path.dirname(__file__) RUNTIMES_ALLOW_LIST = { "cpython": { - "min": Version(version=(3, 7), constraint=""), + "min": Version(version=(3, 8), constraint=""), "max": Version(version=(3, 13), constraint=""), } } diff --git a/releasenotes/notes/add_aiohttp_memory_leak_flag-66005f987dbfbd47.yaml b/releasenotes/notes/add_aiohttp_memory_leak_flag-66005f987dbfbd47.yaml new file mode 100644 index 00000000000..67ef6980a36 --- /dev/null +++ b/releasenotes/notes/add_aiohttp_memory_leak_flag-66005f987dbfbd47.yaml @@ -0,0 +1,5 @@ +--- + +fixes: + - | + aiohttp: Adds the environment variable ``DD_AIOHTTP_CLIENT_DISABLE_STREAM_TIMING_FOR_MEM_LEAK`` to address a potential memory leak in the aiohttp integration. When set to true, this flag may cause streamed response span timing to be inaccurate. The flag defaults to false. \ No newline at end of file diff --git a/releasenotes/notes/fix-bedrock-token-location-36c21044eecce688.yaml b/releasenotes/notes/fix-bedrock-token-location-36c21044eecce688.yaml new file mode 100644 index 00000000000..cddc0fffa49 --- /dev/null +++ b/releasenotes/notes/fix-bedrock-token-location-36c21044eecce688.yaml @@ -0,0 +1,5 @@ +--- +fixes: + - | + botocore: This fix moves bedrock token usage metrics on APM spans from the meta field under `bedrock.usage.{prompt/completion}_tokens` + to the metrics field under `bedrock.response.usage.{prompt/completion}_tokens`. diff --git a/releasenotes/notes/langchain-drop-logs-metrics-a997e8059886b20a.yaml b/releasenotes/notes/langchain-drop-logs-metrics-a997e8059886b20a.yaml new file mode 100644 index 00000000000..e7099dd1a77 --- /dev/null +++ b/releasenotes/notes/langchain-drop-logs-metrics-a997e8059886b20a.yaml @@ -0,0 +1,8 @@ +--- +upgrade: + - | + langchain: Removes prompt-completion log sampling from the LangChain integration. To continue logging prompt completions, + enable LLM Observability. + - | + langchain: Removes integration metrics from the LangChain integration. To continue tracking operational metrics from the + OpenAI integration, enable LLM Observability or use trace metrics instead. diff --git a/releasenotes/notes/remove-deprecated-tracing-configs-c6711b57037576f6.yaml b/releasenotes/notes/remove-deprecated-tracing-configs-c6711b57037576f6.yaml new file mode 100644 index 00000000000..b47613d504e --- /dev/null +++ b/releasenotes/notes/remove-deprecated-tracing-configs-c6711b57037576f6.yaml @@ -0,0 +1,10 @@ +--- +upgrade: + - | + configurations: Drops support for deprecated tracing configurations. The following configurations are no longer supported: + - DD_TRACE_SAMPLE_RATE, use DD_TRACE_SAMPLING_RULES instead. + - DD_TRACE_API_VERSION=v0.3, the default ``v0.5`` version is used instead. + - DD_ANALYTICS_ENABLED, Datadog Analytics is no longer supported. + - DD_TRACE_ANALYTICS_ENABLED, Datadog Analytics is no longer supported. + - DD_HTTP_CLIENT_TAG_QUERY_STRING, DD_TRACE_HTTP_CLIENT_TAG_QUERY_STRING should be used instead. + - DD_TRACE_SPAN_AGGREGATOR_RLOCK, disabling the span aggregator rlock is no longer supported. diff --git a/releasenotes/notes/remove-openai-metrics-logs-656c6ba8e2e07ea3.yaml b/releasenotes/notes/remove-openai-metrics-logs-656c6ba8e2e07ea3.yaml new file mode 100644 index 00000000000..63153702d16 --- /dev/null +++ b/releasenotes/notes/remove-openai-metrics-logs-656c6ba8e2e07ea3.yaml @@ -0,0 +1,8 @@ +--- +upgrade: + - | + openai: Removes prompt-completion log sampling from the OpenAI integration. To continue logging prompt completions, + enable LLM Observability. + - | + openai: Removes integration metrics from the OpenAI integration. To continue tracking operational metrics from the + OpenAI integration, enable LLM Observability or use trace metrics instead. diff --git a/releasenotes/notes/remove-tracing-attrs-3-0-5743fa668289d5bc.yaml b/releasenotes/notes/remove-tracing-attrs-3-0-5743fa668289d5bc.yaml new file mode 100644 index 00000000000..35ee9378801 --- /dev/null +++ b/releasenotes/notes/remove-tracing-attrs-3-0-5743fa668289d5bc.yaml @@ -0,0 +1,15 @@ +--- +upgrade: + - | + tracer: Removes deprecated parameters from ``Tracer.configure(...)`` method and removes the ``Tracer.sampler`` attribute. + - | + tracing: Drops support for multiple tracer instances, ``ddtrace.trace.Tracer`` can not be reinitialized. + - | + span: Removes the deprecated ``Span.sampled`` property + - | + sampling: Drops support for configuring sampling rules using functions and regex in the ``ddtrace.tracer.sampler.rules[].choose_matcher(...)`` method + and removes the ``timestamp_ns`` parameter from ``ddtrace.internal.rate_limiter.RateLimiter.is_allowed()``. + - | + configurations: Drops support for configuring ``DD_TRACE_METHODS`` with the '[]' notation. Ensure DD_TRACE_METHODS use the ':' notation instead". + - | + opentracing: Removes the deprecated ``ddtracer`` parameter from ``ddtrace.opentracer.tracer.Tracer()``. \ No newline at end of file diff --git a/releasenotes/notes/remove_unneeded_unregister-ad20120201768a7e.yaml b/releasenotes/notes/remove_unneeded_unregister-ad20120201768a7e.yaml new file mode 100644 index 00000000000..1a5dc451340 --- /dev/null +++ b/releasenotes/notes/remove_unneeded_unregister-ad20120201768a7e.yaml @@ -0,0 +1,5 @@ +--- +fixes: + - | + logging: Resolves an an unneeded info log being logged on process exit + due to a forksafe hook being unregistered that was never registered to begin with. diff --git a/tests/appsec/iast/test_processor.py b/tests/appsec/iast/test_processor.py index 3bb5eaa5015..4f9b912ffc2 100644 --- a/tests/appsec/iast/test_processor.py +++ b/tests/appsec/iast/test_processor.py @@ -51,7 +51,7 @@ def test_appsec_iast_processor_ensure_span_is_manual_keep(iast_context_defaults, test_appsec_iast_processor_ensure_span_is_manual_keep. This test throws 'finished span not connected to a trace' log error """ - with override_env(dict(DD_TRACE_SAMPLE_RATE=sampling_rate)): + with override_env({"DD_TRACE_SAMPLING_RULES": '[{"sample_rate":%s]"}]' % (sampling_rate,)}): oce.reconfigure() tracer = DummyTracer(iast_enabled=True) @@ -59,7 +59,6 @@ def test_appsec_iast_processor_ensure_span_is_manual_keep(iast_context_defaults, tracer._on_span_finish(span) result = span.get_tag(IAST.JSON) - assert len(json.loads(result)["vulnerabilities"]) == 1 assert span.get_metric(_SAMPLING_PRIORITY_KEY) is USER_KEEP diff --git a/tests/appsec/iast_packages/test_packages.py b/tests/appsec/iast_packages/test_packages.py index 83e53ae92c9..d65cf2ea709 100644 --- a/tests/appsec/iast_packages/test_packages.py +++ b/tests/appsec/iast_packages/test_packages.py @@ -626,7 +626,7 @@ def uninstall(self, python_cmd): "", import_module_to_validate="soupsieve.css_match", extras=[("beautifulsoup4", "4.12.3")], - skip_python_version=[(3, 6), (3, 7), (3, 8)], + skip_python_version=[(3, 8)], test_propagation=True, fixme_propagation_fails=True, ), @@ -638,7 +638,7 @@ def uninstall(self, python_cmd): # "Original password: your-password\nHashed password: replaced_hashed\nPassword match: True", # "", # import_module_to_validate="werkzeug.http", - # skip_python_version=[(3, 6), (3, 7), (3, 8)], + # skip_python_version=[(3, 8)], # ), PackageForTesting( "yarl", @@ -648,7 +648,7 @@ def uninstall(self, python_cmd): + " example.com\nPath: /path\nQuery: \n", "", import_module_to_validate="yarl._url", - skip_python_version=[(3, 6), (3, 7), (3, 8)], + skip_python_version=[(3, 8)], test_propagation=True, fixme_propagation_fails=True, ), @@ -659,7 +659,7 @@ def uninstall(self, python_cmd): # "example.zip", # "Contents of example.zip: ['example.zip/example.txt']", # "", - # skip_python_version=[(3, 6), (3, 7), (3, 8)], + # skip_python_version=[(3, 8)], # ), ## Skip due to typing-extensions added to the denylist # PackageForTesting( @@ -670,7 +670,7 @@ def uninstall(self, python_cmd): # "", # import_name="typing_extensions", # test_e2e=False, - # skip_python_version=[(3, 6), (3, 7), (3, 8)], + # skip_python_version=[(3, 8)], # ), PackageForTesting( "six", @@ -678,7 +678,7 @@ def uninstall(self, python_cmd): "", "We're in Python 3", "", - skip_python_version=[(3, 6), (3, 7), (3, 8)], + skip_python_version=[(3, 8)], ), ## Skip due to pillow added to the denylist # PackageForTesting( @@ -688,7 +688,7 @@ def uninstall(self, python_cmd): # "Image correctly generated", # "", # import_name="PIL.Image", - # skip_python_version=[(3, 6), (3, 7), (3, 8)], + # skip_python_version=[(3, 8)], # ), PackageForTesting( "aiobotocore", "2.13.0", "", "", "", test_e2e=False, test_import=False, import_name="aiobotocore.session" @@ -853,7 +853,7 @@ def uninstall(self, python_cmd): "Processed value: 15", "", import_name="annotated_types", - skip_python_version=[(3, 6), (3, 7), (3, 8)], + skip_python_version=[(3, 8)], ), ] diff --git a/tests/appsec/integrations/fastapi_tests/test_fastapi_appsec_iast.py b/tests/appsec/integrations/fastapi_tests/test_fastapi_appsec_iast.py index 81702f3c8d9..07d8e1c9dc6 100644 --- a/tests/appsec/integrations/fastapi_tests/test_fastapi_appsec_iast.py +++ b/tests/appsec/integrations/fastapi_tests/test_fastapi_appsec_iast.py @@ -27,6 +27,7 @@ from ddtrace.appsec._iast.constants import VULN_NO_SAMESITE_COOKIE from ddtrace.appsec._iast.constants import VULN_SQL_INJECTION from ddtrace.appsec._iast.constants import VULN_STACKTRACE_LEAK +from ddtrace.appsec._iast.constants import VULN_XSS from ddtrace.contrib.internal.fastapi.patch import patch as patch_fastapi from ddtrace.contrib.internal.sqlite3.patch import patch as patch_sqlite_sqli from tests.appsec.iast.iast_utils import get_line_and_hash @@ -987,3 +988,38 @@ async def stacktrace_leak_inline_response(request: Request): assert len(loaded["vulnerabilities"]) == 1 vulnerability = loaded["vulnerabilities"][0] assert vulnerability["type"] == VULN_STACKTRACE_LEAK + + +def test_fastapi_xss(fastapi_application, client, tracer, test_spans): + @fastapi_application.get("/index.html") + async def test_route(request: Request): + from fastapi.responses import HTMLResponse + from jinja2 import Template + + query_params = request.query_params.get("iast_queryparam") + template = Template("

{{ user_input|safe }}

") + html = template.render(user_input=query_params) + return HTMLResponse(html) + + with override_global_config(dict(_iast_enabled=True, _iast_request_sampling=100.0)): + patch_iast({"xss": True}) + from jinja2.filters import FILTERS + from jinja2.filters import do_mark_safe + + FILTERS["safe"] = do_mark_safe + _aux_appsec_prepare_tracer(tracer) + resp = client.get( + "/index.html?iast_queryparam=test1234", + headers={"Content-Type": "application/json"}, + ) + assert resp.status_code == 200 + + span = test_spans.pop_traces()[0][0] + assert span.get_metric(IAST.ENABLED) == 1.0 + + iast_tag = span.get_tag(IAST.JSON) + assert iast_tag is not None + loaded = json.loads(iast_tag) + assert len(loaded["vulnerabilities"]) == 1 + vulnerability = loaded["vulnerabilities"][0] + assert vulnerability["type"] == VULN_XSS diff --git a/tests/appsec/integrations/flask_tests/test_iast_flask.py b/tests/appsec/integrations/flask_tests/test_iast_flask.py index be45e6bb82f..0d8f7c5b4ad 100644 --- a/tests/appsec/integrations/flask_tests/test_iast_flask.py +++ b/tests/appsec/integrations/flask_tests/test_iast_flask.py @@ -16,7 +16,9 @@ from ddtrace.appsec._iast.constants import VULN_NO_SAMESITE_COOKIE from ddtrace.appsec._iast.constants import VULN_SQL_INJECTION from ddtrace.appsec._iast.constants import VULN_STACKTRACE_LEAK +from ddtrace.appsec._iast.constants import VULN_XSS from ddtrace.appsec._iast.taint_sinks.header_injection import patch as patch_header_injection +from ddtrace.appsec._iast.taint_sinks.xss import patch as patch_xss_injection from ddtrace.contrib.internal.sqlite3.patch import patch as patch_sqlite_sqli from ddtrace.settings.asm import config as asm_config from tests.appsec.iast.iast_utils import get_line_and_hash @@ -45,11 +47,15 @@ def setUp(self): _iast_request_sampling=100.0, ) ): - super(FlaskAppSecIASTEnabledTestCase, self).setUp() patch_sqlite_sqli() patch_header_injection() + patch_xss_injection() patch_json() + from jinja2.filters import FILTERS + from jinja2.filters import do_mark_safe + FILTERS["safe"] = do_mark_safe + super(FlaskAppSecIASTEnabledTestCase, self).setUp() self.tracer._configure(api_version="v0.4", appsec_enabled=True, iast_enabled=True) oce.reconfigure() @@ -59,7 +65,6 @@ def test_flask_full_sqli_iast_http_request_path_parameter(self): def sqli_1(param_str): import sqlite3 - from ddtrace.appsec._iast._taint_tracking._taint_objects import is_pyobject_tainted from ddtrace.appsec._iast._taint_tracking.aspects import add_aspect assert is_pyobject_tainted(param_str) @@ -161,6 +166,62 @@ def sqli_2(param_str): assert vulnerability["location"]["path"] == TEST_FILE_PATH assert vulnerability["hash"] == hash_value + @pytest.mark.skipif(not asm_config._iast_supported, reason="Python version not supported by IAST") + def test_flask_iast_enabled_http_request_header_get(self): + @self.app.route("/sqli//", methods=["GET", "POST"]) + def sqli_2(param_str): + import sqlite3 + + from flask import request + + from ddtrace.appsec._iast._taint_tracking.aspects import add_aspect + + con = sqlite3.connect(":memory:") + cur = con.cursor() + # label test_flask_iast_enabled_http_request_header_get + cur.execute(add_aspect("SELECT 1 FROM ", request.headers.get("User-Agent"))) + + return "OK", 200 + + with override_global_config( + dict( + _iast_enabled=True, + _iast_deduplication_enabled=False, + ) + ): + resp = self.client.post( + "/sqli/sqlite_master/", data={"name": "test"}, headers={"User-Agent": "sqlite_master"} + ) + assert resp.status_code == 200 + + root_span = self.pop_spans()[0] + assert root_span.get_metric(IAST.ENABLED) == 1.0 + + loaded = json.loads(root_span.get_tag(IAST.JSON)) + assert loaded["sources"] == [ + {"origin": "http.request.header", "name": "User-Agent", "value": "sqlite_master"} + ] + + line, hash_value = get_line_and_hash( + "test_flask_iast_enabled_http_request_header_get", + VULN_SQL_INJECTION, + filename=TEST_FILE_PATH, + ) + vulnerability = loaded["vulnerabilities"][0] + + assert vulnerability["type"] == VULN_SQL_INJECTION + assert vulnerability["evidence"] == { + "valueParts": [ + {"value": "SELECT "}, + {"redacted": True}, + {"value": " FROM "}, + {"value": "sqlite_master", "source": 0}, + ] + } + assert vulnerability["location"]["line"] == line + assert vulnerability["location"]["path"] == TEST_FILE_PATH + assert vulnerability["hash"] == hash_value + @pytest.mark.skipif(not asm_config._iast_supported, reason="Python version not supported by IAST") def test_flask_full_sqli_iast_enabled_http_request_header_name_keys(self): @self.app.route("/sqli//", methods=["GET", "POST"]) @@ -274,7 +335,6 @@ def sqli_5(param_str, param_int): from ddtrace.appsec._iast._taint_tracking import OriginType from ddtrace.appsec._iast._taint_tracking._taint_objects import get_tainted_ranges - from ddtrace.appsec._iast._taint_tracking._taint_objects import is_pyobject_tainted header_ranges = get_tainted_ranges(request.headers["User-Agent"]) assert header_ranges @@ -324,8 +384,6 @@ def test_flask_simple_iast_path_header_and_querystring_tainted_request_sampling_ def sqli_6(param_str): from flask import request - from ddtrace.appsec._iast._taint_tracking._taint_objects import is_pyobject_tainted - # Note: these are not tainted because of request sampling at 0% assert not is_pyobject_tainted(request.headers["User-Agent"]) assert not is_pyobject_tainted(request.query_string) @@ -535,7 +593,6 @@ def test_flask_full_sqli_iast_http_request_parameter_name_post(self): def sqli_13(): import sqlite3 - from ddtrace.appsec._iast._taint_tracking._taint_objects import is_pyobject_tainted from ddtrace.appsec._iast._taint_tracking.aspects import add_aspect for i in request.form.keys(): @@ -593,7 +650,6 @@ def test_flask_full_sqli_iast_http_request_parameter_name_get(self): def sqli_14(): import sqlite3 - from ddtrace.appsec._iast._taint_tracking._taint_objects import is_pyobject_tainted from ddtrace.appsec._iast._taint_tracking.aspects import add_aspect for i in request.args.keys(): @@ -654,7 +710,6 @@ def sqli_10(): from flask import request - from ddtrace.appsec._iast._taint_tracking._taint_objects import is_pyobject_tainted from ddtrace.appsec._iast._taint_tracking.aspects import add_aspect con = sqlite3.connect(":memory:") @@ -719,7 +774,6 @@ def sqli_11(): from flask import request - from ddtrace.appsec._iast._taint_tracking._taint_objects import is_pyobject_tainted from ddtrace.appsec._iast._taint_tracking.aspects import add_aspect con = sqlite3.connect(":memory:") @@ -784,7 +838,6 @@ def sqli_11(): from flask import request - from ddtrace.appsec._iast._taint_tracking._taint_objects import is_pyobject_tainted from ddtrace.appsec._iast._taint_tracking.aspects import add_aspect con = sqlite3.connect(":memory:") @@ -849,7 +902,6 @@ def sqli_11(): from flask import request - from ddtrace.appsec._iast._taint_tracking._taint_objects import is_pyobject_tainted from ddtrace.appsec._iast._taint_tracking.aspects import add_aspect con = sqlite3.connect(":memory:") @@ -916,7 +968,6 @@ def sqli_11(): from flask import request - from ddtrace.appsec._iast._taint_tracking._taint_objects import is_pyobject_tainted from ddtrace.appsec._iast._taint_tracking.aspects import add_aspect def iterate_json(data, parent_key=""): @@ -1057,7 +1108,6 @@ def sqli_10(): from flask import request - from ddtrace.appsec._iast._taint_tracking._taint_objects import is_pyobject_tainted from ddtrace.appsec._iast._taint_tracking.aspects import add_aspect con = sqlite3.connect(":memory:") @@ -1160,8 +1210,6 @@ def header_injection(): from flask import Response from flask import request - from ddtrace.appsec._iast._taint_tracking._taint_objects import is_pyobject_tainted - tainted_string = request.form.get("name") assert is_pyobject_tainted(tainted_string) resp = Response("OK") @@ -1194,14 +1242,12 @@ def header_injection(): # TODO: vulnerability path is flaky, it points to "tests/contrib/flask/__init__.py" @pytest.mark.skipif(not asm_config._iast_supported, reason="Python version not supported by IAST") - def test_flask_header_injection_exlusions_location(self): + def test_flask_header_injection_exclusions_location(self): @self.app.route("/header_injection/", methods=["GET", "POST"]) def header_injection(): from flask import Response from flask import request - from ddtrace.appsec._iast._taint_tracking._taint_objects import is_pyobject_tainted - tainted_string = request.form.get("name") assert is_pyobject_tainted(tainted_string) resp = Response("OK") @@ -1223,14 +1269,12 @@ def header_injection(): assert root_span.get_tag(IAST.JSON) is None @pytest.mark.skipif(not asm_config._iast_supported, reason="Python version not supported by IAST") - def test_flask_header_injection_exlusions_access_control(self): + def test_flask_header_injection_exclusions_access_control(self): @self.app.route("/header_injection/", methods=["GET", "POST"]) def header_injection(): from flask import Response from flask import request - from ddtrace.appsec._iast._taint_tracking._taint_objects import is_pyobject_tainted - tainted_string = request.form.get("name") assert is_pyobject_tainted(tainted_string) resp = Response("OK") @@ -1258,8 +1302,6 @@ def insecure_cookie(): from flask import Response from flask import request - from ddtrace.appsec._iast._taint_tracking._taint_objects import is_pyobject_tainted - tainted_string = request.form.get("name") assert is_pyobject_tainted(tainted_string) resp = Response("OK") @@ -1296,8 +1338,6 @@ def insecure_cookie_empty(): from flask import Response from flask import request - from ddtrace.appsec._iast._taint_tracking._taint_objects import is_pyobject_tainted - tainted_string = request.form.get("name") assert is_pyobject_tainted(tainted_string) resp = Response("OK") @@ -1326,8 +1366,6 @@ def no_http_only_cookie(): from flask import Response from flask import request - from ddtrace.appsec._iast._taint_tracking._taint_objects import is_pyobject_tainted - tainted_string = request.form.get("name") assert is_pyobject_tainted(tainted_string) resp = Response("OK") @@ -1364,8 +1402,6 @@ def no_http_only_cookie_empty(): from flask import Response from flask import request - from ddtrace.appsec._iast._taint_tracking._taint_objects import is_pyobject_tainted - tainted_string = request.form.get("name") assert is_pyobject_tainted(tainted_string) resp = Response("OK") @@ -1395,8 +1431,6 @@ def no_samesite_cookie(): from flask import Response from flask import request - from ddtrace.appsec._iast._taint_tracking._taint_objects import is_pyobject_tainted - tainted_string = request.form.get("name") assert is_pyobject_tainted(tainted_string) resp = Response("OK") @@ -1433,8 +1467,6 @@ def no_samesite_cookie_empty(): from flask import Response from flask import request - from ddtrace.appsec._iast._taint_tracking._taint_objects import is_pyobject_tainted - tainted_string = request.form.get("name") assert is_pyobject_tainted(tainted_string) resp = Response("OK") @@ -1461,8 +1493,6 @@ def cookie_secure(): from flask import Response from flask import request - from ddtrace.appsec._iast._taint_tracking._taint_objects import is_pyobject_tainted - tainted_string = request.form.get("name") assert is_pyobject_tainted(tainted_string) resp = Response("OK") @@ -1587,6 +1617,159 @@ def stacktrace_leak(): ) assert "Exception: ValueError" in vulnerability["evidence"]["valueParts"][0]["value"] + def test_flask_xss(self): + @self.app.route("/xss/", methods=["GET"]) + def xss_view(): + from flask import render_template_string + from flask import request + + user_input = request.args.get("input", "") + + # label test_flask_xss + return render_template_string("

XSS: {{ user_input|safe }}

", user_input=user_input) + + with override_global_config( + dict( + _iast_enabled=True, + _iast_deduplication_enabled=False, + _iast_request_sampling=100.0, + ) + ): + resp = self.client.get("/xss/?input=") + assert resp.status_code == 200 + assert resp.data == b"

XSS:

" + + root_span = self.pop_spans()[0] + assert root_span.get_metric(IAST.ENABLED) == 1.0 + + loaded = json.loads(root_span.get_tag(IAST.JSON)) + assert loaded["sources"] == [ + {"origin": "http.request.parameter", "name": "input", "value": ""} + ] + + line, hash_value = get_line_and_hash("test_flask_xss", VULN_SQL_INJECTION, filename=TEST_FILE_PATH) + vulnerability = loaded["vulnerabilities"][0] + assert vulnerability["type"] == VULN_XSS + assert vulnerability["evidence"] == { + "valueParts": [ + {"value": "", "source": 0}, + ] + } + assert vulnerability["location"]["line"] == line + assert vulnerability["location"]["path"] == TEST_FILE_PATH + + def test_flask_xss_concat(self): + @self.app.route("/xss/concat/", methods=["GET"]) + def xss_view(): + from flask import render_template_string + from flask import request + + from ddtrace.appsec._iast._taint_tracking.aspects import add_aspect + + user_input = request.args.get("input", "") + + # label test_flask_xss_concat + return render_template_string(add_aspect(add_aspect("

XSS: ", user_input), "

")) + + with override_global_config( + dict( + _iast_enabled=True, + _iast_deduplication_enabled=False, + _iast_request_sampling=100.0, + ) + ): + resp = self.client.get("/xss/concat/?input=") + assert resp.status_code == 200 + assert resp.data == b"

XSS:

" + + root_span = self.pop_spans()[0] + assert root_span.get_metric(IAST.ENABLED) == 1.0 + + loaded = json.loads(root_span.get_tag(IAST.JSON)) + assert loaded["sources"] == [ + {"origin": "http.request.parameter", "name": "input", "value": ""} + ] + + line, hash_value = get_line_and_hash("test_flask_xss_concat", VULN_SQL_INJECTION, filename=TEST_FILE_PATH) + vulnerability = loaded["vulnerabilities"][0] + assert vulnerability["type"] == VULN_XSS + assert vulnerability["evidence"] == { + "valueParts": [ + {"value": "

XSS: "}, + {"source": 0, "value": ""}, + {"value": "

"}, + ] + } + assert vulnerability["location"]["line"] == line + assert vulnerability["location"]["path"] == TEST_FILE_PATH + + def test_flask_xss_template_secure(self): + @self.app.route("/xss/template/secure/", methods=["GET"]) + def xss_view_template(): + from flask import render_template + from flask import request + + user_input = request.args.get("input", "") + + # label test_flask_xss_template + return render_template("test.html", world=user_input) + + with override_global_config( + dict( + _iast_enabled=True, + _iast_deduplication_enabled=False, + _iast_request_sampling=100.0, + ) + ): + resp = self.client.get("/xss/template/secure/?input=") + assert resp.status_code == 200 + assert resp.data == b"hello <script>alert('XSS')</script>" + + root_span = self.pop_spans()[0] + assert root_span.get_metric(IAST.ENABLED) == 1.0 + + assert root_span.get_tag(IAST.JSON) is None + + def test_flask_xss_template(self): + @self.app.route("/xss/template/", methods=["GET"]) + def xss_view_template(): + from flask import render_template + from flask import request + + user_input = request.args.get("input", "") + + # label test_flask_xss_template + return render_template("test_insecure.html", world=user_input) + + with override_global_config( + dict( + _iast_enabled=True, + _iast_deduplication_enabled=False, + _iast_request_sampling=100.0, + ) + ): + resp = self.client.get("/xss/template/?input=") + assert resp.status_code == 200 + assert resp.data == b"hello " + + root_span = self.pop_spans()[0] + assert root_span.get_metric(IAST.ENABLED) == 1.0 + + loaded = json.loads(root_span.get_tag(IAST.JSON)) + assert loaded["sources"] == [ + {"origin": "http.request.parameter", "name": "input", "value": ""} + ] + + line, hash_value = get_line_and_hash("test_flask_xss", VULN_SQL_INJECTION, filename=TEST_FILE_PATH) + vulnerability = loaded["vulnerabilities"][0] + assert vulnerability["type"] == VULN_XSS + assert vulnerability["evidence"] == { + "valueParts": [ + {"value": "", "source": 0}, + ] + } + assert vulnerability["location"]["path"] == "tests/contrib/flask/test_templates/test_insecure.html" + class FlaskAppSecIASTDisabledTestCase(BaseFlaskTestCase): @pytest.fixture(autouse=True) diff --git a/tests/contrib/aiohttp/test_request.py b/tests/contrib/aiohttp/test_request.py index 36e9d8a399a..056eda09c4b 100644 --- a/tests/contrib/aiohttp/test_request.py +++ b/tests/contrib/aiohttp/test_request.py @@ -31,6 +31,33 @@ async def test_full_request(patched_app_tracer, aiohttp_client, loop): assert "GET /" == request_span.resource +async def test_full_request_w_mem_leak_prevention_flag(patched_app_tracer, aiohttp_client, loop): + config.aiohttp.disable_stream_timing_for_mem_leak = True + try: + app, tracer = patched_app_tracer + client = await aiohttp_client(app) + # it should create a root span when there is a handler hit + # with the proper tags + request = await client.request("GET", "/") + assert 200 == request.status + await request.text() + # the trace is created + traces = tracer.pop_traces() + assert 1 == len(traces) + assert 1 == len(traces[0]) + request_span = traces[0][0] + assert_is_measured(request_span) + + # request + assert "aiohttp-web" == request_span.service + assert "aiohttp.request" == request_span.name + assert "GET /" == request_span.resource + except Exception: + raise + finally: + config.aiohttp.disable_stream_timing_for_mem_leak = False + + async def test_stream_request(patched_app_tracer, aiohttp_client, loop): app, tracer = patched_app_tracer async with await aiohttp_client(app) as client: diff --git a/tests/contrib/botocore/test_bedrock_llmobs.py b/tests/contrib/botocore/test_bedrock_llmobs.py index 790b86f0704..9efac64b196 100644 --- a/tests/contrib/botocore/test_bedrock_llmobs.py +++ b/tests/contrib/botocore/test_bedrock_llmobs.py @@ -80,8 +80,16 @@ def mock_llmobs_span_writer(): class TestLLMObsBedrock: @staticmethod def expected_llmobs_span_event(span, n_output, message=False): - prompt_tokens = int(span.get_tag("bedrock.usage.prompt_tokens")) - completion_tokens = int(span.get_tag("bedrock.usage.completion_tokens")) + prompt_tokens = span.get_metric("bedrock.response.usage.prompt_tokens") + completion_tokens = span.get_metric("bedrock.response.usage.completion_tokens") + token_metrics = {} + if prompt_tokens is not None: + token_metrics["input_tokens"] = prompt_tokens + if completion_tokens is not None: + token_metrics["output_tokens"] = completion_tokens + if prompt_tokens is not None and completion_tokens is not None: + token_metrics["total_tokens"] = prompt_tokens + completion_tokens + expected_parameters = {"temperature": float(span.get_tag("bedrock.request.temperature"))} if span.get_tag("bedrock.request.max_tokens"): expected_parameters["max_tokens"] = int(span.get_tag("bedrock.request.max_tokens")) @@ -95,11 +103,7 @@ def expected_llmobs_span_event(span, n_output, message=False): input_messages=expected_input, output_messages=[{"content": mock.ANY} for _ in range(n_output)], metadata=expected_parameters, - token_metrics={ - "input_tokens": prompt_tokens, - "output_tokens": completion_tokens, - "total_tokens": prompt_tokens + completion_tokens, - }, + token_metrics=token_metrics, tags={"service": "aws.bedrock-runtime", "ml_app": ""}, ) diff --git a/tests/contrib/flask/test_templates/test_insecure.html b/tests/contrib/flask/test_templates/test_insecure.html new file mode 100644 index 00000000000..a1921295a57 --- /dev/null +++ b/tests/contrib/flask/test_templates/test_insecure.html @@ -0,0 +1 @@ +hello {{world|safe}} diff --git a/tests/contrib/openai/conftest.py b/tests/contrib/openai/conftest.py index 615a4e773b1..47f258b3ed0 100644 --- a/tests/contrib/openai/conftest.py +++ b/tests/contrib/openai/conftest.py @@ -92,34 +92,6 @@ def process_trace(self, trace): return trace -@pytest.fixture(scope="session") -def mock_metrics(): - patcher = mock.patch("ddtrace.llmobs._integrations.base.get_dogstatsd_client") - try: - DogStatsdMock = patcher.start() - m = mock.MagicMock() - DogStatsdMock.return_value = m - yield m - finally: - patcher.stop() - - -@pytest.fixture(scope="session") -def mock_logs(): - """ - Note that this fixture must be ordered BEFORE mock_tracer as it needs to patch the log writer - before it is instantiated. - """ - patcher = mock.patch("ddtrace.llmobs._integrations.base.V2LogWriter") - try: - V2LogWriterMock = patcher.start() - m = mock.MagicMock() - V2LogWriterMock.return_value = m - yield m - finally: - patcher.stop() - - @pytest.fixture() def mock_llmobs_writer(): patcher = mock.patch("ddtrace.llmobs._llmobs.LLMObsSpanWriter") @@ -163,18 +135,15 @@ def patch_openai(ddtrace_global_config, ddtrace_config_openai, openai_api_key, o @pytest.fixture -def snapshot_tracer(openai, patch_openai, mock_logs, mock_metrics): +def snapshot_tracer(openai, patch_openai): pin = Pin.get_from(openai) pin.tracer._configure(trace_processors=[FilterOrg()]) yield pin.tracer - mock_logs.reset_mock() - mock_metrics.reset_mock() - @pytest.fixture -def mock_tracer(ddtrace_global_config, openai, patch_openai, mock_logs, mock_metrics): +def mock_tracer(ddtrace_global_config, openai, patch_openai): pin = Pin.get_from(openai) mock_tracer = DummyTracer(writer=DummyWriter(trace_flush_enabled=False)) pin.override(openai, tracer=mock_tracer) @@ -187,6 +156,4 @@ def mock_tracer(ddtrace_global_config, openai, patch_openai, mock_logs, mock_met yield mock_tracer - mock_logs.reset_mock() - mock_metrics.reset_mock() LLMObs.disable() diff --git a/tests/contrib/openai/test_openai_llmobs.py b/tests/contrib/openai/test_openai_llmobs.py index 70adff39ef5..91e454c1673 100644 --- a/tests/contrib/openai/test_openai_llmobs.py +++ b/tests/contrib/openai/test_openai_llmobs.py @@ -602,9 +602,7 @@ def test_embedding_string_base64(self, openai, ddtrace_global_config, mock_llmob [dict(_llmobs_enabled=True, _llmobs_ml_app="", _llmobs_agentless_enabled=True)], ) @pytest.mark.skipif(parse_version(openai_module.version.VERSION) < (1, 0), reason="These tests are for openai >= 1.0") -def test_agentless_enabled_does_not_submit_metrics( - openai, ddtrace_global_config, mock_llmobs_writer, mock_tracer, mock_metrics -): +def test_agentless_enabled_does_not_submit_metrics(openai, ddtrace_global_config, mock_llmobs_writer, mock_tracer): """Ensure openai metrics are not emitted when agentless mode is enabled.""" with get_openai_vcr(subdirectory_name="v1").use_cassette("completion.yaml"): model = "ada" @@ -619,7 +617,3 @@ def test_agentless_enabled_does_not_submit_metrics( user="ddtrace-test", ) assert mock_llmobs_writer.enqueue.call_count == 1 - mock_metrics.assert_not_called() - assert mock_metrics.increment.call_count == 0 - assert mock_metrics.distribution.call_count == 0 - assert mock_metrics.gauge.call_count == 0 diff --git a/tests/contrib/openai/test_openai_v1.py b/tests/contrib/openai/test_openai_v1.py index a081583b4d1..468f4b03606 100644 --- a/tests/contrib/openai/test_openai_v1.py +++ b/tests/contrib/openai/test_openai_v1.py @@ -25,18 +25,8 @@ def openai_vcr(): yield get_openai_vcr(subdirectory_name="v1") -@pytest.mark.parametrize("ddtrace_config_openai", [dict(metrics_enabled=True), dict(metrics_enabled=False)]) -def test_config(ddtrace_config_openai, mock_tracer, openai): - # Ensure that the module state is reloaded for each test run - assert not hasattr(openai, "_test") - openai._test = 1 - - # Ensure overriding the config works - assert ddtrace.config.openai.metrics_enabled is ddtrace_config_openai["metrics_enabled"] - - @pytest.mark.parametrize("api_key_in_env", [True, False]) -def test_model_list(api_key_in_env, request_api_key, openai, openai_vcr, mock_metrics, snapshot_tracer): +def test_model_list(api_key_in_env, request_api_key, openai, openai_vcr, snapshot_tracer): with snapshot_context( token="tests.contrib.openai.test_openai.test_model_list", ignores=["meta.http.useragent", "meta.openai.api_type", "meta.openai.api_base", "meta.openai.request.user"], @@ -47,7 +37,7 @@ def test_model_list(api_key_in_env, request_api_key, openai, openai_vcr, mock_me @pytest.mark.parametrize("api_key_in_env", [True, False]) -async def test_model_alist(api_key_in_env, request_api_key, openai, openai_vcr, mock_metrics, snapshot_tracer): +async def test_model_alist(api_key_in_env, request_api_key, openai, openai_vcr, snapshot_tracer): with snapshot_context( token="tests.contrib.openai.test_openai.test_model_list", ignores=["meta.http.useragent", "meta.openai.api_type", "meta.openai.api_base", "meta.openai.request.user"], @@ -58,7 +48,7 @@ async def test_model_alist(api_key_in_env, request_api_key, openai, openai_vcr, @pytest.mark.parametrize("api_key_in_env", [True, False]) -def test_model_retrieve(api_key_in_env, request_api_key, openai, openai_vcr, mock_metrics, snapshot_tracer): +def test_model_retrieve(api_key_in_env, request_api_key, openai, openai_vcr, snapshot_tracer): with snapshot_context( token="tests.contrib.openai.test_openai.test_model_retrieve", ignores=["meta.http.useragent", "meta.openai.api_type", "meta.openai.api_base", "meta.openai.request.user"], @@ -69,7 +59,7 @@ def test_model_retrieve(api_key_in_env, request_api_key, openai, openai_vcr, moc @pytest.mark.parametrize("api_key_in_env", [True, False]) -async def test_model_aretrieve(api_key_in_env, request_api_key, openai, openai_vcr, mock_metrics, snapshot_tracer): +async def test_model_aretrieve(api_key_in_env, request_api_key, openai, openai_vcr, snapshot_tracer): with snapshot_context( token="tests.contrib.openai.test_openai.test_model_retrieve", ignores=["meta.http.useragent", "meta.openai.api_type", "meta.openai.api_base", "meta.openai.request.user"], @@ -80,9 +70,7 @@ async def test_model_aretrieve(api_key_in_env, request_api_key, openai, openai_v @pytest.mark.parametrize("api_key_in_env", [True, False]) -def test_completion( - api_key_in_env, request_api_key, openai, openai_vcr, mock_metrics, mock_logs, mock_llmobs_writer, snapshot_tracer -): +def test_completion(api_key_in_env, request_api_key, openai, openai_vcr, mock_llmobs_writer, snapshot_tracer): with snapshot_context( token="tests.contrib.openai.test_openai.test_completion", ignores=["meta.http.useragent", "meta.openai.api_type", "meta.openai.api_base"], @@ -111,42 +99,12 @@ def test_completion( assert choice.logprobs == expected_choices[idx]["logprobs"] assert choice.text == expected_choices[idx]["text"] - expected_tags = [ - "version:", - "env:", - "service:tests.contrib.openai", - "openai.request.model:ada", - "model:ada", - "openai.request.endpoint:/v1/completions", - "openai.request.method:POST", - "openai.organization.id:", - "openai.organization.name:datadog-4", - "openai.user.api_key:sk-...key>", - "error:0", - ] - mock_metrics.assert_has_calls( - [ - mock.call.distribution("tokens.prompt", 2, tags=expected_tags + ["openai.estimated:false"]), - mock.call.distribution("tokens.completion", 12, tags=expected_tags + ["openai.estimated:false"]), - mock.call.distribution("tokens.total", 14, tags=expected_tags + ["openai.estimated:false"]), - mock.call.distribution("request.duration", mock.ANY, tags=expected_tags), - mock.call.gauge("ratelimit.remaining.requests", mock.ANY, tags=expected_tags), - mock.call.gauge("ratelimit.requests", mock.ANY, tags=expected_tags), - mock.call.gauge("ratelimit.remaining.tokens", mock.ANY, tags=expected_tags), - mock.call.gauge("ratelimit.tokens", mock.ANY, tags=expected_tags), - ], - any_order=True, - ) - mock_logs.start.assert_not_called() - mock_logs.enqueue.assert_not_called() mock_llmobs_writer.start.assert_not_called() mock_llmobs_writer.enqueue.assert_not_called() @pytest.mark.parametrize("api_key_in_env", [True, False]) -async def test_acompletion( - api_key_in_env, request_api_key, openai, openai_vcr, mock_metrics, mock_logs, mock_llmobs_writer, snapshot_tracer -): +async def test_acompletion(api_key_in_env, request_api_key, openai, openai_vcr, mock_llmobs_writer, snapshot_tracer): with snapshot_context( token="tests.contrib.openai.test_openai.test_acompletion", ignores=["meta.http.useragent", "meta.openai.api_type", "meta.openai.api_base"], @@ -181,88 +139,11 @@ async def test_acompletion( for key, value in expected_choices.items(): assert getattr(resp.choices[0], key, None) == value - expected_tags = [ - "version:", - "env:", - "service:tests.contrib.openai", - "openai.request.model:curie", - "model:curie", - "openai.request.endpoint:/v1/completions", - "openai.request.method:POST", - "openai.organization.id:", - "openai.organization.name:datadog-4", - "openai.user.api_key:sk-...key>", - "error:0", - ] - mock_metrics.assert_has_calls( - [ - mock.call.distribution("tokens.prompt", 10, tags=expected_tags + ["openai.estimated:false"]), - mock.call.distribution("tokens.completion", 150, tags=expected_tags + ["openai.estimated:false"]), - mock.call.distribution("tokens.total", 160, tags=expected_tags + ["openai.estimated:false"]), - mock.call.distribution("request.duration", mock.ANY, tags=expected_tags), - mock.call.gauge("ratelimit.remaining.requests", mock.ANY, tags=expected_tags), - mock.call.gauge("ratelimit.requests", mock.ANY, tags=expected_tags), - mock.call.gauge("ratelimit.remaining.tokens", mock.ANY, tags=expected_tags), - mock.call.gauge("ratelimit.tokens", mock.ANY, tags=expected_tags), - ], - any_order=True, - ) - mock_logs.start.assert_not_called() - mock_logs.enqueue.assert_not_called() mock_llmobs_writer.start.assert_not_called() mock_llmobs_writer.enqueue.assert_not_called() -@pytest.mark.xfail(reason="An API key is required when logs are enabled") -@pytest.mark.parametrize( - "ddtrace_global_config,ddtrace_config_openai", - [(dict(_dd_api_key=""), dict(logs_enabled=True))], -) -def test_logs_no_api_key(openai, ddtrace_global_config, ddtrace_config_openai, mock_tracer): - """When no DD_API_KEY is set, the patching fails""" - pass - - -@pytest.mark.parametrize("ddtrace_config_openai", [dict(logs_enabled=True, log_prompt_completion_sample_rate=1.0)]) -def test_logs_completions(openai_vcr, openai, ddtrace_config_openai, mock_logs, mock_tracer): - """Ensure logs are emitted for completion endpoints when configured. - - Also ensure the logs have the correct tagging including the trace-logs correlation tagging. - """ - with openai_vcr.use_cassette("completion.yaml"): - client = openai.OpenAI() - client.completions.create( - model="ada", prompt="Hello world", temperature=0.8, n=2, stop=".", max_tokens=10, user="ddtrace-test" - ) - - span = mock_tracer.pop_traces()[0][0] - trace_id, span_id = span.trace_id, span.span_id - - assert mock_logs.enqueue.call_count == 1 - mock_logs.assert_has_calls( - [ - mock.call.start(), - mock.call.enqueue( - { - "timestamp": mock.ANY, - "message": mock.ANY, - "hostname": mock.ANY, - "ddsource": "openai", - "service": "tests.contrib.openai", - "status": "info", - "ddtags": "env:,version:,openai.request.endpoint:/v1/completions,openai.request.method:POST,openai.request.model:ada,openai.organization.name:datadog-4,openai.user.api_key:sk-...key>", # noqa: E501 - "dd.trace_id": "{:x}".format(trace_id), - "dd.span_id": str(span_id), - "prompt": "Hello world", - "choices": mock.ANY, - } - ), - ] - ) - - -@pytest.mark.parametrize("ddtrace_config_openai", [dict(logs_enabled=True, log_prompt_completion_sample_rate=1.0)]) -def test_global_tags(openai_vcr, ddtrace_config_openai, openai, mock_metrics, mock_logs, mock_tracer): +def test_global_tags(openai_vcr, openai, mock_tracer): """ When the global config UST tags are set The service name should be used for all data @@ -288,32 +169,6 @@ def test_global_tags(openai_vcr, ddtrace_config_openai, openai, mock_metrics, mo assert span.get_tag("openai.organization.name") == "datadog-4" assert span.get_tag("openai.user.api_key") == "sk-...key>" - for _, _args, kwargs in mock_metrics.mock_calls: - expected_metrics = [ - "service:test-svc", - "env:staging", - "version:1234", - "openai.request.model:ada", - "model:ada", - "openai.request.endpoint:/v1/completions", - "openai.request.method:POST", - "openai.organization.name:datadog-4", - "openai.user.api_key:sk-...key>", - ] - actual_tags = kwargs.get("tags") - for m in expected_metrics: - assert m in actual_tags - - for call, args, _kwargs in mock_logs.mock_calls: - if call != "enqueue": - continue - log = args[0] - assert log["service"] == "test-svc" - assert ( - log["ddtags"] - == "env:staging,version:1234,openai.request.endpoint:/v1/completions,openai.request.method:POST,openai.request.model:ada,openai.organization.name:datadog-4,openai.user.api_key:sk-...key>" # noqa: E501 - ) - def test_completion_raw_response(openai, openai_vcr, snapshot_tracer): with snapshot_context( @@ -440,20 +295,6 @@ def test_chat_completion_raw_response(openai, openai_vcr, snapshot_tracer): ) -@pytest.mark.parametrize("ddtrace_config_openai", [dict(metrics_enabled=b) for b in [True, False]]) -def test_enable_metrics(openai, openai_vcr, ddtrace_config_openai, mock_metrics, mock_tracer): - """Ensure the metrics_enabled configuration works.""" - with openai_vcr.use_cassette("completion.yaml"): - client = openai.OpenAI() - client.completions.create( - model="ada", prompt="Hello world", temperature=0.8, n=2, stop=".", max_tokens=10, user="ddtrace-test" - ) - if ddtrace_config_openai["metrics_enabled"]: - assert mock_metrics.mock_calls - else: - assert not mock_metrics.mock_calls - - @pytest.mark.parametrize("api_key_in_env", [True, False]) async def test_achat_completion(api_key_in_env, request_api_key, openai, openai_vcr, snapshot_tracer): with snapshot_context( @@ -510,47 +351,6 @@ async def test_image_acreate(api_key_in_env, request_api_key, openai, openai_vcr ) -@pytest.mark.parametrize("ddtrace_config_openai", [dict(logs_enabled=True, log_prompt_completion_sample_rate=1.0)]) -def test_logs_image_create(openai_vcr, openai, ddtrace_config_openai, mock_logs, mock_tracer): - """Ensure logs are emitted for image endpoints when configured. - - Also ensure the logs have the correct tagging including the trace-logs correlation tagging. - """ - with openai_vcr.use_cassette("image_create.yaml"): - client = openai.OpenAI() - client.images.generate( - prompt="sleepy capybara with monkey on top", - n=1, - size="256x256", - response_format="url", - user="ddtrace-test", - ) - span = mock_tracer.pop_traces()[0][0] - trace_id, span_id = span.trace_id, span.span_id - - assert mock_logs.enqueue.call_count == 1 - mock_logs.assert_has_calls( - [ - mock.call.start(), - mock.call.enqueue( - { - "timestamp": mock.ANY, - "message": mock.ANY, - "hostname": mock.ANY, - "ddsource": "openai", - "service": "tests.contrib.openai", - "status": "info", - "ddtags": "env:,version:,openai.request.endpoint:/v1/images/generations,openai.request.method:POST,openai.request.model:dall-e,openai.organization.name:datadog-4,openai.user.api_key:sk-...key>", # noqa: E501 - "dd.trace_id": "{:x}".format(trace_id), - "dd.span_id": str(span_id), - "prompt": "sleepy capybara with monkey on top", - "choices": mock.ANY, - } - ), - ] - ) - - # TODO: Note that vcr tests for image edit/variation don't work as they error out when recording the vcr request, # during the payload decoding. We'll need to migrate those tests over once we can address this. @pytest.mark.snapshot( @@ -871,7 +671,7 @@ def test_span_finish_on_stream_error(openai, openai_vcr, snapshot_tracer): @pytest.mark.snapshot @pytest.mark.skipif(TIKTOKEN_AVAILABLE, reason="This test estimates token counts") -def test_completion_stream_est_tokens(openai, openai_vcr, mock_metrics, snapshot_tracer): +def test_completion_stream_est_tokens(openai, openai_vcr, snapshot_tracer): with openai_vcr.use_cassette("completion_streamed.yaml"): with mock.patch("ddtrace.contrib.internal.openai.utils.encoding_for_model", create=True) as mock_encoding: mock_encoding.return_value.encode.side_effect = lambda x: [1, 2] @@ -882,7 +682,7 @@ def test_completion_stream_est_tokens(openai, openai_vcr, mock_metrics, snapshot @pytest.mark.skipif(not TIKTOKEN_AVAILABLE, reason="This test computes token counts using tiktoken") @pytest.mark.snapshot(token="tests.contrib.openai.test_openai.test_completion_stream") -def test_completion_stream(openai, openai_vcr, mock_metrics, snapshot_tracer): +def test_completion_stream(openai, openai_vcr, snapshot_tracer): with openai_vcr.use_cassette("completion_streamed.yaml"): with mock.patch("ddtrace.contrib.internal.openai.utils.encoding_for_model", create=True) as mock_encoding: mock_encoding.return_value.encode.side_effect = lambda x: [1, 2] @@ -893,7 +693,7 @@ def test_completion_stream(openai, openai_vcr, mock_metrics, snapshot_tracer): @pytest.mark.skipif(not TIKTOKEN_AVAILABLE, reason="This test computes token counts using tiktoken") @pytest.mark.snapshot(token="tests.contrib.openai.test_openai.test_completion_stream") -async def test_completion_async_stream(openai, openai_vcr, mock_metrics, snapshot_tracer): +async def test_completion_async_stream(openai, openai_vcr, snapshot_tracer): with openai_vcr.use_cassette("completion_streamed.yaml"): with mock.patch("ddtrace.contrib.internal.openai.utils.encoding_for_model", create=True) as mock_encoding: mock_encoding.return_value.encode.side_effect = lambda x: [1, 2] @@ -907,7 +707,7 @@ async def test_completion_async_stream(openai, openai_vcr, mock_metrics, snapsho reason="Streamed response context managers are only available v1.6.0+", ) @pytest.mark.snapshot(token="tests.contrib.openai.test_openai.test_completion_stream") -def test_completion_stream_context_manager(openai, openai_vcr, mock_metrics, snapshot_tracer): +def test_completion_stream_context_manager(openai, openai_vcr, snapshot_tracer): with openai_vcr.use_cassette("completion_streamed.yaml"): with mock.patch("ddtrace.contrib.internal.openai.utils.encoding_for_model", create=True) as mock_encoding: mock_encoding.return_value.encode.side_effect = lambda x: [1, 2] @@ -920,7 +720,7 @@ def test_completion_stream_context_manager(openai, openai_vcr, mock_metrics, sna parse_version(openai_module.version.VERSION) < (1, 26), reason="Stream options only available openai >= 1.26" ) @pytest.mark.snapshot(token="tests.contrib.openai.test_openai.test_chat_completion_stream") -def test_chat_completion_stream(openai, openai_vcr, mock_metrics, snapshot_tracer): +def test_chat_completion_stream(openai, openai_vcr, snapshot_tracer): """Assert that streamed token chunk extraction logic works automatically.""" with openai_vcr.use_cassette("chat_completion_streamed_tokens.yaml"): with mock.patch("ddtrace.contrib.internal.openai.utils.encoding_for_model", create=True) as mock_encoding: @@ -939,7 +739,7 @@ def test_chat_completion_stream(openai, openai_vcr, mock_metrics, snapshot_trace @pytest.mark.skipif( parse_version(openai_module.version.VERSION) < (1, 26), reason="Stream options only available openai >= 1.26" ) -def test_chat_completion_stream_explicit_no_tokens(openai, openai_vcr, mock_metrics, snapshot_tracer): +def test_chat_completion_stream_explicit_no_tokens(openai, openai_vcr, mock_tracer): """Assert that streamed token chunk extraction logic is avoided if explicitly set to False by the user.""" with openai_vcr.use_cassette("chat_completion_streamed.yaml"): with mock.patch("ddtrace.contrib.internal.openai.utils.encoding_for_model", create=True) as mock_encoding: @@ -956,41 +756,22 @@ def test_chat_completion_stream_explicit_no_tokens(openai, openai_vcr, mock_metr user="ddtrace-test", n=None, ) - span = snapshot_tracer.current_span() chunks = [c for c in resp] assert len(chunks) == 15 completion = "".join([c.choices[0].delta.content for c in chunks if c.choices[0].delta.content is not None]) assert completion == expected_completion - expected_tags = [ - "version:", - "env:", - "service:tests.contrib.openai", - "openai.request.model:gpt-3.5-turbo", - "model:gpt-3.5-turbo", - "openai.request.endpoint:/v1/chat/completions", - "openai.request.method:POST", - "openai.organization.id:", - "openai.organization.name:datadog-4", - "openai.user.api_key:sk-...key>", - "error:0", - ] - assert mock.call.distribution("request.duration", span.duration_ns, tags=expected_tags) in mock_metrics.mock_calls - assert mock.call.gauge("ratelimit.requests", 3000, tags=expected_tags) in mock_metrics.mock_calls - assert mock.call.gauge("ratelimit.remaining.requests", 2999, tags=expected_tags) in mock_metrics.mock_calls - expected_tags += ["openai.estimated:true"] - if TIKTOKEN_AVAILABLE: - expected_tags = expected_tags[:-1] - assert mock.call.distribution("tokens.prompt", 8, tags=expected_tags) in mock_metrics.mock_calls - assert mock.call.distribution("tokens.completion", mock.ANY, tags=expected_tags) in mock_metrics.mock_calls - assert mock.call.distribution("tokens.total", mock.ANY, tags=expected_tags) in mock_metrics.mock_calls + span = mock_tracer.pop_traces()[0][0] + assert span.get_metric("openai.response.usage.prompt_tokens") == 8 + assert span.get_metric("openai.response.usage.completion_tokens") is not None + assert span.get_metric("openai.response.usage.total_tokens") is not None @pytest.mark.skipif( parse_version(openai_module.version.VERSION) < (1, 26, 0), reason="Streamed tokens available in 1.26.0+" ) @pytest.mark.snapshot(token="tests.contrib.openai.test_openai.test_chat_completion_stream") -async def test_chat_completion_async_stream(openai, openai_vcr, mock_metrics, snapshot_tracer): +async def test_chat_completion_async_stream(openai, openai_vcr, snapshot_tracer): with openai_vcr.use_cassette("chat_completion_streamed_tokens.yaml"): with mock.patch("ddtrace.contrib.internal.openai.utils.encoding_for_model", create=True) as mock_encoding: mock_encoding.return_value.encode.side_effect = lambda x: [1, 2, 3, 4, 5, 6, 7, 8] @@ -1012,7 +793,7 @@ async def test_chat_completion_async_stream(openai, openai_vcr, mock_metrics, sn reason="Streamed response context managers are only available v1.6.0+, tokens available 1.26.0+", ) @pytest.mark.snapshot(token="tests.contrib.openai.test_openai.test_chat_completion_stream") -async def test_chat_completion_async_stream_context_manager(openai, openai_vcr, mock_metrics, snapshot_tracer): +async def test_chat_completion_async_stream_context_manager(openai, openai_vcr, snapshot_tracer): with openai_vcr.use_cassette("chat_completion_streamed_tokens.yaml"): with mock.patch("ddtrace.contrib.internal.openai.utils.encoding_for_model", create=True) as mock_encoding: mock_encoding.return_value.encode.side_effect = lambda x: [1, 2, 3, 4, 5, 6, 7, 8] @@ -1045,14 +826,7 @@ def test_integration_sync(openai_api_key, ddtrace_run_python_code_in_subprocess) pypath = [os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__))))] if "PYTHONPATH" in env: pypath.append(env["PYTHONPATH"]) - env.update( - { - "OPENAI_API_KEY": openai_api_key, - "PYTHONPATH": ":".join(pypath), - # Disable metrics because the test agent doesn't support metrics - "DD_OPENAI_METRICS_ENABLED": "false", - } - ) + env.update({"OPENAI_API_KEY": openai_api_key, "PYTHONPATH": ":".join(pypath)}) out, err, status, pid = ddtrace_run_python_code_in_subprocess( """ import openai @@ -1092,14 +866,7 @@ def test_integration_async(openai_api_key, ddtrace_run_python_code_in_subprocess pypath = [os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__))))] if "PYTHONPATH" in env: pypath.append(env["PYTHONPATH"]) - env.update( - { - "OPENAI_API_KEY": openai_api_key, - "PYTHONPATH": ":".join(pypath), - # Disable metrics because the test agent doesn't support metrics - "DD_OPENAI_METRICS_ENABLED": "false", - } - ) + env.update({"OPENAI_API_KEY": openai_api_key, "PYTHONPATH": ":".join(pypath)}) out, err, status, pid = ddtrace_run_python_code_in_subprocess( """ import asyncio @@ -1247,36 +1014,13 @@ def test_completion_truncation(openai, openai_vcr, mock_tracer, ddtrace_config_o @pytest.mark.parametrize("ddtrace_config_openai", [dict(span_prompt_completion_sample_rate=0)]) -def test_embedding_unsampled_prompt_completion(openai, openai_vcr, ddtrace_config_openai, mock_logs, mock_tracer): +def test_embedding_unsampled_prompt_completion(openai, openai_vcr, ddtrace_config_openai, mock_tracer): with openai_vcr.use_cassette("embedding.yaml"): client = openai.OpenAI() client.embeddings.create(input="hello world", model="text-embedding-ada-002") - logs = mock_logs.enqueue.call_count traces = mock_tracer.pop_traces() assert len(traces) == 1 assert traces[0][0].get_tag("openai.request.input") is None - assert logs == 0 - - -@pytest.mark.parametrize( - "ddtrace_config_openai", - [dict(logs_enabled=True, log_prompt_completion_sample_rate=r) for r in [0, 0.25, 0.75, 1]], -) -def test_logs_sample_rate(openai, openai_vcr, ddtrace_config_openai, mock_logs, mock_tracer): - total_calls = 200 - for _ in range(total_calls): - with openai_vcr.use_cassette("completion.yaml"): - client = openai.OpenAI() - client.completions.create(model="ada", prompt="Hello world", temperature=0.8, n=2, stop=".", max_tokens=10) - - logs = mock_logs.enqueue.call_count - if ddtrace.config.openai["log_prompt_completion_sample_rate"] == 0: - assert logs == 0 - elif ddtrace.config.openai["log_prompt_completion_sample_rate"] == 1: - assert logs == total_calls - else: - rate = ddtrace.config.openai["log_prompt_completion_sample_rate"] * total_calls - assert (rate - 30) < logs < (rate + 30) def test_est_tokens(): @@ -1489,14 +1233,7 @@ def test_integration_service_name(openai_api_key, ddtrace_run_python_code_in_sub pypath = [os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__))))] if "PYTHONPATH" in env: pypath.append(env["PYTHONPATH"]) - env.update( - { - "OPENAI_API_KEY": openai_api_key, - "PYTHONPATH": ":".join(pypath), - # Disable metrics because the test agent doesn't support metrics - "DD_OPENAI_METRICS_ENABLED": "false", - } - ) + env.update({"OPENAI_API_KEY": openai_api_key, "PYTHONPATH": ":".join(pypath)}) if schema_version: env["DD_TRACE_SPAN_ATTRIBUTE_SCHEMA"] = schema_version if service_name: diff --git a/tests/integration/test_debug.py b/tests/integration/test_debug.py index f5453f353fe..97c95507668 100644 --- a/tests/integration/test_debug.py +++ b/tests/integration/test_debug.py @@ -36,7 +36,6 @@ def __eq__(self, other): @pytest.mark.subprocess() def test_standard_tags(): from datetime import datetime - import sys import ddtrace from ddtrace.internal import debug @@ -75,14 +74,6 @@ def test_standard_tags(): in_venv = f.get("in_virtual_env") assert in_venv is True - lang_version = f.get("lang_version") - if sys.version_info == (3, 7, 0): - assert "3.7" in lang_version - elif sys.version_info == (3, 6, 0): - assert "3.6" in lang_version - elif sys.version_info == (2, 7, 0): - assert "2.7" in lang_version - agent_url = f.get("agent_url") assert agent_url == "http://localhost:8126" diff --git a/tests/integration/test_settings.py b/tests/integration/test_settings.py index 249b0211bb4..ba9bcd66f37 100644 --- a/tests/integration/test_settings.py +++ b/tests/integration/test_settings.py @@ -20,7 +20,7 @@ def test_setting_origin_environment(test_agent_session, run_python_code_in_subpr env = os.environ.copy() env.update( { - "DD_TRACE_SAMPLE_RATE": "0.1", + "DD_TRACE_SAMPLING_RULES": '[{"sample_rate":0.1}]', "DD_LOGS_INJECTION": "true", "DD_TRACE_HEADER_TAGS": "X-Header-Tag-1:header_tag_1,X-Header-Tag-2:header_tag_2", "DD_TAGS": "team:apm,component:web", @@ -39,11 +39,11 @@ def test_setting_origin_environment(test_agent_session, run_python_code_in_subpr assert status == 0, err events = test_agent_session.get_events(subprocess=True) - events_trace_sample_rate = _get_telemetry_config_items(events, "DD_TRACE_SAMPLE_RATE") + events_trace_sample_rate = _get_telemetry_config_items(events, "DD_TRACE_SAMPLING_RULES") assert { - "name": "DD_TRACE_SAMPLE_RATE", - "value": 0.1, + "name": "DD_TRACE_SAMPLING_RULES", + "value": '[{"sample_rate":0.1}]', "origin": "env_var", } in events_trace_sample_rate @@ -69,7 +69,6 @@ def test_setting_origin_code(test_agent_session, run_python_code_in_subprocess): env = os.environ.copy() env.update( { - "DD_TRACE_SAMPLE_RATE": "0.1", "DD_LOGS_INJECTION": "true", "DD_TRACE_HEADER_TAGS": "X-Header-Tag-1:header_tag_1,X-Header-Tag-2:header_tag_2", "DD_TAGS": "team:apm,component:web", @@ -81,7 +80,6 @@ def test_setting_origin_code(test_agent_session, run_python_code_in_subprocess): """ from ddtrace import config, tracer -config._trace_sample_rate = 0.2 config._logs_injection = False config._trace_http_header_tags = {"header": "value"} config.tags = {"header": "value"} @@ -96,12 +94,6 @@ def test_setting_origin_code(test_agent_session, run_python_code_in_subprocess): assert status == 0, err events = test_agent_session.get_events(subprocess=True) - events_trace_sample_rate = _get_telemetry_config_items(events, "DD_TRACE_SAMPLE_RATE") - assert { - "name": "DD_TRACE_SAMPLE_RATE", - "value": 0.2, - "origin": "code", - } in events_trace_sample_rate events_logs_injection_enabled = _get_telemetry_config_items(events, "DD_LOGS_INJECTION") assert { @@ -174,8 +166,8 @@ def test_remoteconfig_sampling_rate_default(test_agent_session, run_python_code_ assert status == 0, err events = test_agent_session.get_events(subprocess=True) - events_trace_sample_rate = _get_telemetry_config_items(events, "DD_TRACE_SAMPLE_RATE") - assert {"name": "DD_TRACE_SAMPLE_RATE", "value": 1.0, "origin": "default"} in events_trace_sample_rate + events_trace_sample_rate = _get_telemetry_config_items(events, "trace_sample_rate") + assert {"name": "trace_sample_rate", "value": 1.0, "origin": "default"} in events_trace_sample_rate @pytest.mark.skipif(AGENT_VERSION != "testagent", reason="Tests only compatible with a testagent") @@ -191,7 +183,22 @@ def test_remoteconfig_sampling_rate_telemetry(test_agent_session, run_python_cod from ddtrace import config, tracer from tests.internal.test_settings import _base_rc_config -config._handle_remoteconfig(_base_rc_config({"tracing_sampling_rate": 0.5})) +config._handle_remoteconfig( + _base_rc_config( + { + "tracing_sampling_rules": [ + { + "sample_rate": "0.5", + "service": "*", + "name": "*", + "resource": "*", + "tags": {}, + "provenance": "customer", + } + ] + } + ) +) with tracer.trace("test") as span: pass assert span.get_metric("_dd.rule_psr") == 0.5 @@ -201,8 +208,13 @@ def test_remoteconfig_sampling_rate_telemetry(test_agent_session, run_python_cod assert status == 0, err events = test_agent_session.get_events(subprocess=True) - events_trace_sample_rate = _get_telemetry_config_items(events, "DD_TRACE_SAMPLE_RATE") - assert {"name": "DD_TRACE_SAMPLE_RATE", "value": 0.5, "origin": "remote_config"} in events_trace_sample_rate + events_trace_sample_rate = _get_telemetry_config_items(events, "DD_TRACE_SAMPLING_RULES") + assert { + "name": "DD_TRACE_SAMPLING_RULES", + "origin": "remote_config", + "value": '[{"sample_rate": "0.5", "service": "*", "name": "*", "resource": "*", ' + '"tags": {}, "provenance": "customer"}]', + } in events_trace_sample_rate @pytest.mark.skipif(AGENT_VERSION != "testagent", reason="Tests only compatible with a testagent") @@ -226,9 +238,11 @@ def test_remoteconfig_header_tags_telemetry(test_agent_session, run_python_code_ {"header": "used-with-default", "tag_name":""}] })) with tracer.trace("test") as span: - trace_utils.set_http_meta(span, - config.falcon, # randomly chosen http integration config - request_headers={"used": "foobarbanana", "used-with-default": "defaultname"}) + trace_utils.set_http_meta( + span, + config.falcon, # randomly chosen http integration config + request_headers={"used": "foobarbanana", "used-with-default": "defaultname"}, + ) assert span.get_tag("header_tag_69") == "foobarbanana" assert span.get_tag("header_tag_70") is None assert span.get_tag("http.request.headers.used-with-default") == "defaultname" diff --git a/tests/integration/test_tracemethods.py b/tests/integration/test_tracemethods.py index 15129c56161..7353c12182a 100644 --- a/tests/integration/test_tracemethods.py +++ b/tests/integration/test_tracemethods.py @@ -27,14 +27,10 @@ "mod.mod2.mod3:Class.test_method,Class.test_method2", [("mod.mod2.mod3", "Class.test_method"), ("mod.mod2.mod3", "Class.test_method2")], ), - ("module[method1, method2]", []), ("module", []), ("module.", []), ("module.method", []), - ("module.method[m1,m2,]", []), ("module.method;module.method", []), - ("module.method[m1];module.method[m1,m2,]", []), - ("module.method[[m1]", []), ], ) def test_trace_methods_parse(dd_trace_methods: str, expected_output: List[Tuple[str, str]]): @@ -43,37 +39,6 @@ def test_trace_methods_parse(dd_trace_methods: str, expected_output: List[Tuple[ assert _parse_trace_methods(dd_trace_methods) == expected_output -def test_legacy_trace_methods_parse(): - from ddtrace.internal.tracemethods import _parse_legacy_trace_methods - - assert _parse_legacy_trace_methods("") == [] - assert _parse_legacy_trace_methods("module[method1]") == ["module.method1"] - assert _parse_legacy_trace_methods("module[method1,method2]") == ["module.method1", "module.method2"] - assert _parse_legacy_trace_methods("module[method1,method2];mod2[m1,m2]") == [ - "module.method1", - "module.method2", - "mod2.m1", - "mod2.m2", - ] - assert _parse_legacy_trace_methods("mod.submod[m1,m2,m3]") == ["mod.submod.m1", "mod.submod.m2", "mod.submod.m3"] - assert _parse_legacy_trace_methods("mod.submod.subsubmod[m1,m2]") == [ - "mod.submod.subsubmod.m1", - "mod.submod.subsubmod.m2", - ] - assert _parse_legacy_trace_methods("mod.mod2.mod3.Class[test_method,test_method2]") == [ - "mod.mod2.mod3.Class.test_method", - "mod.mod2.mod3.Class.test_method2", - ] - assert _parse_legacy_trace_methods("module[method1, method2]") == [] - assert _parse_legacy_trace_methods("module") == [] - assert _parse_legacy_trace_methods("module.") == [] - assert _parse_legacy_trace_methods("module.method") == [] - assert _parse_legacy_trace_methods("module.method[m1,m2,]") == [] - assert _parse_legacy_trace_methods("module.method;module.method") == [] - assert _parse_legacy_trace_methods("module.method[m1];module.method[m1,m2,]") == [] - assert _parse_legacy_trace_methods("module.method[[m1]") == [] - - def _test_method(): pass @@ -105,9 +70,9 @@ def test_method(self): ddtrace_run=True, env=dict( DD_TRACE_METHODS=( - "tests.integration.test_tracemethods[_test_method,_test_method2];" - "tests.integration.test_tracemethods._Class[test_method,test_method2];" - "tests.integration.test_tracemethods._Class.NestedClass[test_method]" + "tests.integration.test_tracemethods:_test_method,_test_method2;" + "tests.integration.test_tracemethods:_Class.test_method,_Class.test_method2;" + "tests.integration.test_tracemethods:_Class.NestedClass.test_method" ) ), ) @@ -139,8 +104,8 @@ async def _async_test_method2(): def test_ddtrace_run_trace_methods_async(ddtrace_run_python_code_in_subprocess): env = os.environ.copy() env["DD_TRACE_METHODS"] = ( - "tests.integration.test_tracemethods[_async_test_method,_async_test_method2];" - "tests.integration.test_tracemethods._Class[async_test_method]" + "tests.integration.test_tracemethods:_async_test_method,_async_test_method2;" + "tests.integration.test_tracemethods:_Class.async_test_method" ) tests_dir = os.path.dirname(os.path.dirname(__file__)) env["PYTHONPATH"] = os.pathsep.join([tests_dir, env.get("PYTHONPATH", "")]) diff --git a/tests/internal/test_settings.py b/tests/internal/test_settings.py index 2ff1843690e..a26d692eea4 100644 --- a/tests/internal/test_settings.py +++ b/tests/internal/test_settings.py @@ -62,22 +62,36 @@ def _deleted_rc_config(): }, }, { - "env": {"DD_TRACE_SAMPLE_RATE": "0.9"}, - "expected": {"_trace_sample_rate": 0.9}, - "expected_source": {"_trace_sample_rate": "env_var"}, + "env": {"DD_TRACE_SAMPLING_RULES": '[{"sample_rate":0.91}]'}, + "expected": {"_trace_sampling_rules": '[{"sample_rate":0.91}]'}, + "expected_source": {"_trace_sampling_rules": "env_var"}, }, { - "env": {"DD_TRACE_SAMPLE_RATE": "0.9"}, - "code": {"_trace_sample_rate": 0.8}, - "expected": {"_trace_sample_rate": 0.8}, - "expected_source": {"_trace_sample_rate": "code"}, + "env": {"DD_TRACE_SAMPLING_RULES": '[{"sample_rate":0.92}]'}, + "code": {"_trace_sampling_rules": '[{"sample_rate":0.82}]'}, + "expected": {"_trace_sampling_rules": '[{"sample_rate":0.82}]'}, + "expected_source": {"_trace_sampling_rules": "code"}, }, { - "env": {"DD_TRACE_SAMPLE_RATE": "0.9"}, - "code": {"_trace_sample_rate": 0.8}, - "rc": {"tracing_sampling_rate": 0.7}, - "expected": {"_trace_sample_rate": 0.7}, - "expected_source": {"_trace_sample_rate": "remote_config"}, + "env": {"DD_TRACE_SAMPLING_RULES": '[{"sample_rate":0.93}]'}, + "code": {"_trace_sampling_rules": '[{"sample_rate":0.83}]'}, + "rc": { + "tracing_sampling_rules": [ + { + "sample_rate": "0.73", + "service": "*", + "name": "*", + "resource": "*", + "tags": [], + "provenance": "customer", + } + ] + }, + "expected": { + "_trace_sampling_rules": '[{"sample_rate": "0.73", "service": "*", "name": "*", ' + '"resource": "*", "tags": [], "provenance": "customer"}]', + }, + "expected_source": {"_trace_sampling_rules": "remote_config"}, }, { "env": {"DD_LOGS_INJECTION": "true"}, @@ -227,60 +241,6 @@ def test_config_subscription(config): _handler.assert_called_once_with(config, [s]) -def test_remoteconfig_sampling_rate_user(run_python_code_in_subprocess): - env = os.environ.copy() - env.update({"DD_TRACE_SAMPLE_RATE": "0.1"}) - out, err, status, _ = run_python_code_in_subprocess( - """ -from ddtrace import config, tracer -from ddtrace._trace.sampler import DatadogSampler -from tests.internal.test_settings import _base_rc_config, _deleted_rc_config - -with tracer.trace("test") as span: - pass -assert span.get_metric("_dd.rule_psr") == 0.1 - -config._handle_remoteconfig(_base_rc_config({"tracing_sampling_rate": 0.2})) -with tracer.trace("test") as span: - pass -assert span.get_metric("_dd.rule_psr") == 0.2 - -config._handle_remoteconfig(_base_rc_config({})) -with tracer.trace("test") as span: - pass -assert span.get_metric("_dd.rule_psr") == 0.1 - -custom_sampler = DatadogSampler(default_sample_rate=0.3) -tracer._configure(sampler=custom_sampler) -with tracer.trace("test") as span: - pass -assert span.get_metric("_dd.rule_psr") == 0.3 - -config._handle_remoteconfig(_base_rc_config({"tracing_sampling_rate": 0.4})) -with tracer.trace("test") as span: - pass -assert span.get_metric("_dd.rule_psr") == 0.4 - -config._handle_remoteconfig(_base_rc_config({})) -with tracer.trace("test") as span: - pass -assert span.get_metric("_dd.rule_psr") == 0.3 - -config._handle_remoteconfig(_base_rc_config({"tracing_sampling_rate": 0.4})) -with tracer.trace("test") as span: - pass -assert span.get_metric("_dd.rule_psr") == 0.4 - -config._handle_remoteconfig(_deleted_rc_config()) -with tracer.trace("test") as span: - pass -assert span.get_metric("_dd.rule_psr") == 0.3 - """, - env=env, - ) - assert status == 0, err.decode("utf-8") - - def test_remoteconfig_sampling_rules(run_python_code_in_subprocess): env = os.environ.copy() env.update({"DD_TRACE_SAMPLING_RULES": '[{"sample_rate":0.1, "name":"test"}]'}) @@ -368,13 +328,12 @@ def test_remoteconfig_sampling_rules(run_python_code_in_subprocess): assert status == 0, err.decode("utf-8") -def test_remoteconfig_sample_rate_and_rules(run_python_code_in_subprocess): +def test_remoteconfig_global_sample_rate_and_rules(run_python_code_in_subprocess): """There is complex logic regarding the interaction between setting new sample rates and rules with remote config. """ env = os.environ.copy() - env.update({"DD_TRACE_SAMPLING_RULES": '[{"sample_rate":0.9, "name":"rules"}]'}) - env.update({"DD_TRACE_SAMPLE_RATE": "0.8"}) + env.update({"DD_TRACE_SAMPLING_RULES": '[{"sample_rate":0.9, "name":"rules"}, {"sample_rate":0.8}]'}) out, err, status, _ = run_python_code_in_subprocess( """ @@ -410,8 +369,9 @@ def test_remoteconfig_sample_rate_and_rules(run_python_code_in_subprocess): with tracer.trace("sample_rate") as span: pass -assert span.get_metric("_dd.rule_psr") == 0.8 -assert span.get_tag("_dd.p.dm") == "-3" +# Global sampling rule was overwritten +assert span.get_metric("_dd.rule_psr") is None +assert span.get_tag("_dd.p.dm") == "-0" config._handle_remoteconfig(_base_rc_config({"tracing_sampling_rate": 0.2})) @@ -482,8 +442,8 @@ def test_remoteconfig_sample_rate_and_rules(run_python_code_in_subprocess): with tracer.trace("sample_rate") as span: pass -assert span.get_metric("_dd.rule_psr") == 0.8 -assert span.get_tag("_dd.p.dm") == "-3" +assert span.get_metric("_dd.rule_psr") is None +assert span.get_tag("_dd.p.dm") == "-0" """, env=env, diff --git a/tests/opentelemetry/test_config.py b/tests/opentelemetry/test_config.py index 39a43128e9e..d5e9bf570fd 100644 --- a/tests/opentelemetry/test_config.py +++ b/tests/opentelemetry/test_config.py @@ -1,6 +1,24 @@ import pytest +def _global_sampling_rule(): + from ddtrace._trace.sampling_rule import SamplingRule + from ddtrace.trace import tracer + + assert hasattr(tracer._sampler, "rules") + + for rule in tracer._sampler.rules: + if ( + rule.service == SamplingRule.NO_RULE + and rule.name == SamplingRule.NO_RULE + and rule.resource == SamplingRule.NO_RULE + and rule.tags == SamplingRule.NO_RULE + and rule.provenance == "default" + ): + return rule + assert False, "Rule not found" + + @pytest.mark.subprocess( env={ "OTEL_SERVICE_NAME": "Test", @@ -10,7 +28,7 @@ "OTEL_PROPAGATORS": "jaegar, tracecontext, b3", "DD_TRACE_PROPAGATION_STYLE": "b3", "OTEL_TRACES_SAMPLER": "always_off", - "DD_TRACE_SAMPLE_RATE": "1.0", + "DD_TRACE_SAMPLING_RULES": '[{"sample_rate":0.1}]', "OTEL_TRACES_EXPORTER": "True", "DD_TRACE_ENABLED": "True", "OTEL_METRICS_EXPORTER": "none", @@ -26,11 +44,12 @@ ) def test_dd_otel_mixed_env_configuration(): from ddtrace import config + from tests.opentelemetry.test_config import _global_sampling_rule assert config.service == "DD_service_test", config.service assert config._debug_mode is False, config._debug_mode assert config._propagation_style_extract == ["b3"], config._propagation_style_extract - assert config._trace_sample_rate == 1.0, config._trace_sample_rate + assert _global_sampling_rule().sample_rate == 0.1 assert config._tracing_enabled is True, config._tracing_enabled assert config._runtime_metrics_enabled is True, config._runtime_metrics_enabled assert config._otel_enabled is True, config._otel_enabled @@ -45,7 +64,7 @@ def test_dd_otel_mixed_env_configuration(): "OTEL_LOG_LEVEL": "debug", "OTEL_PROPAGATORS": "jaegar, tracecontext, b3", "OTEL_TRACES_SAMPLER": "always_off", - "DD_TRACE_SAMPLE_RATE": "1.0", + "DD_TRACE_SAMPLING_RULES": '[{"sample_rate":0.9}]', "OTEL_TRACES_EXPORTER": "OTLP", "OTEL_METRICS_EXPORTER": "none", "OTEL_LOGS_EXPORTER": "warning", @@ -59,13 +78,14 @@ def test_dd_otel_mixed_env_configuration(): ) def test_dd_otel_missing_dd_env_configuration(): from ddtrace import config + from tests.opentelemetry.test_config import _global_sampling_rule assert config.service == "Test", config.service assert config.version == "1.0" assert config._otel_enabled is True, config._otel_enabled assert config._debug_mode is True, config._debug_mode assert config._propagation_style_extract == ["tracecontext", "b3"], config._propagation_style_extract - assert config._trace_sample_rate == 1.0, config._trace_sample_rate + assert _global_sampling_rule().sample_rate == 0.9 assert config._tracing_enabled is True, config._tracing_enabled assert config._runtime_metrics_enabled is False, config._runtime_metrics_enabled assert config.tags == { @@ -133,8 +153,9 @@ def test_otel_propagation_style_configuration_unsupportedwarning(): ) def test_otel_traces_sampler_configuration_alwayson(): from ddtrace import config + from tests.opentelemetry.test_config import _global_sampling_rule - assert config._trace_sample_rate == 1.0, config._trace_sample_rate + assert _global_sampling_rule().sample_rate == 1.0, config._trace_sample_rate @pytest.mark.subprocess( @@ -143,8 +164,9 @@ def test_otel_traces_sampler_configuration_alwayson(): ) def test_otel_traces_sampler_configuration_ignore_parent(): from ddtrace import config + from tests.opentelemetry.test_config import _global_sampling_rule - assert config._trace_sample_rate == 1.0, config._trace_sample_rate + assert _global_sampling_rule().sample_rate == 1.0, config._trace_sample_rate @pytest.mark.subprocess( @@ -153,8 +175,9 @@ def test_otel_traces_sampler_configuration_ignore_parent(): ) def test_otel_traces_sampler_configuration_alwaysoff(): from ddtrace import config + from tests.opentelemetry.test_config import _global_sampling_rule - assert config._trace_sample_rate == 0.0, config._trace_sample_rate + assert _global_sampling_rule().sample_rate == 0.0, config._trace_sample_rate @pytest.mark.subprocess( @@ -167,8 +190,9 @@ def test_otel_traces_sampler_configuration_alwaysoff(): ) def test_otel_traces_sampler_configuration_traceidratio(): from ddtrace import config + from tests.opentelemetry.test_config import _global_sampling_rule - assert config._trace_sample_rate == 0.5, config._trace_sample_rate + assert _global_sampling_rule().sample_rate == 0.5, config._trace_sample_rate @pytest.mark.subprocess(env={"OTEL_TRACES_EXPORTER": "none"}) diff --git a/tests/opentracer/core/test_dd_compatibility.py b/tests/opentracer/core/test_dd_compatibility.py index 4ba14b0618f..c68b5ca6d6c 100644 --- a/tests/opentracer/core/test_dd_compatibility.py +++ b/tests/opentracer/core/test_dd_compatibility.py @@ -15,14 +15,6 @@ def test_ottracer_uses_global_ddtracer(self): tracer = ddtrace.opentracer.Tracer() assert tracer._dd_tracer is ddtrace.tracer - def test_custom_ddtracer(self): - """A user should be able to specify their own Datadog tracer instance if - they wish. - """ - custom_dd_tracer = ddtrace.trace.Tracer() - tracer = ddtrace.opentracer.Tracer(dd_tracer=custom_dd_tracer) - assert tracer._dd_tracer is custom_dd_tracer - def test_ot_dd_global_tracers(self, global_tracer): """Ensure our test function opentracer_init() prep""" ot_tracer = global_tracer diff --git a/tests/opentracer/core/test_tracer.py b/tests/opentracer/core/test_tracer.py index a0a18ff0dd8..f5534c8f1b0 100644 --- a/tests/opentracer/core/test_tracer.py +++ b/tests/opentracer/core/test_tracer.py @@ -15,8 +15,6 @@ from ddtrace.opentracer.span_context import SpanContext from ddtrace.propagation.http import HTTP_HEADER_TRACE_ID from ddtrace.settings import ConfigException -from ddtrace.trace import Tracer as DDTracer -from tests.utils import override_global_config class TestTracerConfig(object): @@ -69,12 +67,6 @@ def test_invalid_config_key(self): assert ["enabeld", "setttings"] in str(ce_info) # codespell:ignore assert tracer is not None - def test_ddtrace_fallback_config(self): - """Ensure datadog configuration is used by default.""" - with override_global_config(dict(_tracing_enabled=False)): - tracer = Tracer(dd_tracer=DDTracer()) - assert tracer._dd_tracer.enabled is False - def test_global_tags(self): """Global tags should be passed from the opentracer to the tracer.""" config = { diff --git a/tests/opentracer/utils.py b/tests/opentracer/utils.py index 6a34052a385..85b84865ad8 100644 --- a/tests/opentracer/utils.py +++ b/tests/opentracer/utils.py @@ -7,5 +7,5 @@ def init_tracer(service_name, dd_tracer, scope_manager=None): It accepts a Datadog tracer that should be the same one used for testing. """ - ot_tracer = Tracer(service_name, dd_tracer=dd_tracer, scope_manager=scope_manager) + ot_tracer = Tracer(service_name, scope_manager=scope_manager, _dd_tracer=dd_tracer) return ot_tracer diff --git a/tests/snapshots/tests.contrib.botocore.test_bedrock.test_ai21_invoke.json b/tests/snapshots/tests.contrib.botocore.test_bedrock.test_ai21_invoke.json index c9e51d357fc..2234a250bc7 100644 --- a/tests/snapshots/tests.contrib.botocore.test_bedrock.test_ai21_invoke.json +++ b/tests/snapshots/tests.contrib.botocore.test_bedrock.test_ai21_invoke.json @@ -23,8 +23,6 @@ "bedrock.response.choices.0.text": "\\nA neural network is like a secret recipe that a computer uses to learn how to", "bedrock.response.duration": "319", "bedrock.response.id": "1de3312e-48d1-4d7f-8694-733c1c1ea20f", - "bedrock.usage.completion_tokens": "10", - "bedrock.usage.prompt_tokens": "10", "language": "python", "runtime-id": "3dd17f1c810946349e47a84acb56402a" }, @@ -32,6 +30,8 @@ "_dd.top_level": 1, "_dd.tracer_kr": 1.0, "_sampling_priority_v1": 1, + "bedrock.response.usage.completion_tokens": 10, + "bedrock.response.usage.prompt_tokens": 10, "process_id": 7458 }, "duration": 2112000, diff --git a/tests/snapshots/tests.contrib.botocore.test_bedrock.test_amazon_embedding.json b/tests/snapshots/tests.contrib.botocore.test_bedrock.test_amazon_embedding.json index 52fe47a79d8..11a968f244b 100644 --- a/tests/snapshots/tests.contrib.botocore.test_bedrock.test_amazon_embedding.json +++ b/tests/snapshots/tests.contrib.botocore.test_bedrock.test_amazon_embedding.json @@ -17,8 +17,6 @@ "bedrock.request.prompt": "Hello World!", "bedrock.response.duration": "311", "bedrock.response.id": "1fd884e0-c9e8-44fa-b736-d31e2f607d54", - "bedrock.usage.completion_tokens": "", - "bedrock.usage.prompt_tokens": "3", "language": "python", "runtime-id": "a7bb6456241740dea419398d37aa13d2" }, @@ -27,6 +25,7 @@ "_dd.tracer_kr": 1.0, "_sampling_priority_v1": 1, "bedrock.response.embedding_length": 1536, + "bedrock.response.usage.prompt_tokens": 3, "process_id": 60939 }, "duration": 6739000, diff --git a/tests/snapshots/tests.contrib.botocore.test_bedrock.test_amazon_invoke.json b/tests/snapshots/tests.contrib.botocore.test_bedrock.test_amazon_invoke.json index 8a97aa3a5f6..bfa129a3c79 100644 --- a/tests/snapshots/tests.contrib.botocore.test_bedrock.test_amazon_invoke.json +++ b/tests/snapshots/tests.contrib.botocore.test_bedrock.test_amazon_invoke.json @@ -23,8 +23,6 @@ "bedrock.response.choices.0.text": "\\nDatadog is a monitoring and analytics platform that helps businesses track and optimize their digital infrastructure. It provi...", "bedrock.response.duration": "1835", "bedrock.response.id": "758fa023-a298-48d0-89e6-0208306cb76d", - "bedrock.usage.completion_tokens": "50", - "bedrock.usage.prompt_tokens": "18", "language": "python", "runtime-id": "13983704e2404c4a911b0cc558662a8f" }, @@ -32,6 +30,8 @@ "_dd.top_level": 1, "_dd.tracer_kr": 1.0, "_sampling_priority_v1": 1, + "bedrock.response.usage.completion_tokens": 50, + "bedrock.response.usage.prompt_tokens": 18, "process_id": 14088 }, "duration": 2147082000, diff --git a/tests/snapshots/tests.contrib.botocore.test_bedrock.test_amazon_invoke_stream.json b/tests/snapshots/tests.contrib.botocore.test_bedrock.test_amazon_invoke_stream.json index 25c96fbfa8e..21c1c1117f5 100644 --- a/tests/snapshots/tests.contrib.botocore.test_bedrock.test_amazon_invoke_stream.json +++ b/tests/snapshots/tests.contrib.botocore.test_bedrock.test_amazon_invoke_stream.json @@ -23,8 +23,6 @@ "bedrock.response.choices.0.text": "\\nDatadog is a monitoring and analytics platform that helps businesses track and optimize their digital infrastructure. It provi...", "bedrock.response.duration": "1804", "bedrock.response.id": "a4b0a846-cd84-41b5-a567-1bf5e341c30c", - "bedrock.usage.completion_tokens": "51", - "bedrock.usage.prompt_tokens": "18", "language": "python", "runtime-id": "13983704e2404c4a911b0cc558662a8f" }, @@ -32,6 +30,8 @@ "_dd.top_level": 1, "_dd.tracer_kr": 1.0, "_sampling_priority_v1": 1, + "bedrock.response.usage.completion_tokens": 51, + "bedrock.response.usage.prompt_tokens": 18, "process_id": 14088 }, "duration": 2185710000, diff --git a/tests/snapshots/tests.contrib.botocore.test_bedrock.test_anthropic_invoke.json b/tests/snapshots/tests.contrib.botocore.test_bedrock.test_anthropic_invoke.json index d611c648f01..802d685c441 100644 --- a/tests/snapshots/tests.contrib.botocore.test_bedrock.test_anthropic_invoke.json +++ b/tests/snapshots/tests.contrib.botocore.test_bedrock.test_anthropic_invoke.json @@ -24,8 +24,6 @@ "bedrock.response.choices.0.text": " I'm an AI assistant created by Anthropic to be helpful, harmless, and honest. I don't compare myself to other systems.", "bedrock.response.duration": "556", "bedrock.response.id": "47ef2ae8-23da-4600-8cda-cbbf4df7bbb6", - "bedrock.usage.completion_tokens": "32", - "bedrock.usage.prompt_tokens": "23", "language": "python", "runtime-id": "75edb3ee6bee404fa05694de91dd42a3" }, @@ -33,6 +31,8 @@ "_dd.top_level": 1, "_dd.tracer_kr": 1.0, "_sampling_priority_v1": 1, + "bedrock.response.usage.completion_tokens": 32, + "bedrock.response.usage.prompt_tokens": 23, "process_id": 7272 }, "duration": 2434000, diff --git a/tests/snapshots/tests.contrib.botocore.test_bedrock.test_anthropic_invoke_stream.json b/tests/snapshots/tests.contrib.botocore.test_bedrock.test_anthropic_invoke_stream.json index c833a69ac4a..cf08e21c6dd 100644 --- a/tests/snapshots/tests.contrib.botocore.test_bedrock.test_anthropic_invoke_stream.json +++ b/tests/snapshots/tests.contrib.botocore.test_bedrock.test_anthropic_invoke_stream.json @@ -24,8 +24,6 @@ "bedrock.response.choices.0.text": "", "bedrock.response.duration": "266", "bedrock.response.id": "710b14d5-164f-460d-bfc6-d3b31e794691", - "bedrock.usage.completion_tokens": "4", - "bedrock.usage.prompt_tokens": "25", "language": "python", "runtime-id": "5e8e1855ea8f4eefaa631b5691eb5e62" }, @@ -33,6 +31,8 @@ "_dd.top_level": 1, "_dd.tracer_kr": 1.0, "_sampling_priority_v1": 1, + "bedrock.response.usage.completion_tokens": 4, + "bedrock.response.usage.prompt_tokens": 25, "process_id": 13707 }, "duration": 624710000, diff --git a/tests/snapshots/tests.contrib.botocore.test_bedrock.test_anthropic_message_invoke.json b/tests/snapshots/tests.contrib.botocore.test_bedrock.test_anthropic_message_invoke.json index 1bbbe5d05a7..72e952dacca 100644 --- a/tests/snapshots/tests.contrib.botocore.test_bedrock.test_anthropic_message_invoke.json +++ b/tests/snapshots/tests.contrib.botocore.test_bedrock.test_anthropic_message_invoke.json @@ -24,8 +24,6 @@ "bedrock.response.choices.0.text": "{'type': 'text', 'text': 'Hobbits embark on epic quest to destroy powerful ring, battling dark forces.'}", "bedrock.response.duration": "886", "bedrock.response.id": "cfb79f63-67cf-45b5-8a3c-9f5bf02064f5", - "bedrock.usage.completion_tokens": "22", - "bedrock.usage.prompt_tokens": "21", "language": "python", "runtime-id": "d2a21cfa56b94b50bae2ca63f83c50f9" }, @@ -33,6 +31,8 @@ "_dd.top_level": 1, "_dd.tracer_kr": 1.0, "_sampling_priority_v1": 1, + "bedrock.response.usage.completion_tokens": 22, + "bedrock.response.usage.prompt_tokens": 21, "process_id": 40705 }, "duration": 2160000, diff --git a/tests/snapshots/tests.contrib.botocore.test_bedrock.test_anthropic_message_invoke_stream.json b/tests/snapshots/tests.contrib.botocore.test_bedrock.test_anthropic_message_invoke_stream.json index b1aed2025fb..7159b8f16a2 100644 --- a/tests/snapshots/tests.contrib.botocore.test_bedrock.test_anthropic_message_invoke_stream.json +++ b/tests/snapshots/tests.contrib.botocore.test_bedrock.test_anthropic_message_invoke_stream.json @@ -24,8 +24,6 @@ "bedrock.response.choices.0.text": "Hobbits embark on epic quest to destroy powerful ring, battling dark forces.", "bedrock.response.duration": "2033", "bedrock.response.id": "1708c260-6ca9-46ce-8799-1ce6c03cae0d", - "bedrock.usage.completion_tokens": "22", - "bedrock.usage.prompt_tokens": "21", "language": "python", "runtime-id": "7cf2c8f7bcae407886c63c657123594f" }, @@ -33,6 +31,8 @@ "_dd.top_level": 1, "_dd.tracer_kr": 1.0, "_sampling_priority_v1": 1, + "bedrock.response.usage.completion_tokens": 22, + "bedrock.response.usage.prompt_tokens": 21, "process_id": 40896 }, "duration": 2950000, diff --git a/tests/snapshots/tests.contrib.botocore.test_bedrock.test_cohere_embedding.json b/tests/snapshots/tests.contrib.botocore.test_bedrock.test_cohere_embedding.json index f6deaba74ae..b27dbc32a17 100644 --- a/tests/snapshots/tests.contrib.botocore.test_bedrock.test_cohere_embedding.json +++ b/tests/snapshots/tests.contrib.botocore.test_bedrock.test_cohere_embedding.json @@ -19,8 +19,6 @@ "bedrock.request.truncate": "", "bedrock.response.duration": "271", "bedrock.response.id": "0e9cb5ab-1fef-46eb-8e2c-773f0f60f39d", - "bedrock.usage.completion_tokens": "", - "bedrock.usage.prompt_tokens": "7", "language": "python", "runtime-id": "c02c555fdac14227bee7b37a0c304534" }, @@ -29,6 +27,7 @@ "_dd.tracer_kr": 1.0, "_sampling_priority_v1": 1, "bedrock.response.embedding_length": 1024, + "bedrock.response.usage.prompt_tokens": 7, "process_id": 61336 }, "duration": 630192000, diff --git a/tests/snapshots/tests.contrib.botocore.test_bedrock.test_cohere_invoke_multi_output.json b/tests/snapshots/tests.contrib.botocore.test_bedrock.test_cohere_invoke_multi_output.json index 302c3b02957..f819efdfaf8 100644 --- a/tests/snapshots/tests.contrib.botocore.test_bedrock.test_cohere_invoke_multi_output.json +++ b/tests/snapshots/tests.contrib.botocore.test_bedrock.test_cohere_invoke_multi_output.json @@ -30,8 +30,6 @@ "bedrock.response.choices.1.text": " Sure! A Long Short-Term Memory (L", "bedrock.response.duration": "346", "bedrock.response.id": "c158207e-8168-4e9f-927c-971b9e2d9d38", - "bedrock.usage.completion_tokens": "20", - "bedrock.usage.prompt_tokens": "40", "language": "python", "runtime-id": "88ca69f6cc694697b1289399e130da4e" }, @@ -39,6 +37,8 @@ "_dd.top_level": 1, "_dd.tracer_kr": 1.0, "_sampling_priority_v1": 1, + "bedrock.response.usage.completion_tokens": 20, + "bedrock.response.usage.prompt_tokens": 40, "process_id": 3568 }, "duration": 810213000, diff --git a/tests/snapshots/tests.contrib.botocore.test_bedrock.test_cohere_invoke_single_output.json b/tests/snapshots/tests.contrib.botocore.test_bedrock.test_cohere_invoke_single_output.json index de967d47296..bb81b11283b 100644 --- a/tests/snapshots/tests.contrib.botocore.test_bedrock.test_cohere_invoke_single_output.json +++ b/tests/snapshots/tests.contrib.botocore.test_bedrock.test_cohere_invoke_single_output.json @@ -27,8 +27,6 @@ "bedrock.response.choices.0.text": " A Large Language Model (LLM) chain refers", "bedrock.response.duration": "277", "bedrock.response.id": "909434da-e64a-4ccd-8c11-c97793dc3746", - "bedrock.usage.completion_tokens": "10", - "bedrock.usage.prompt_tokens": "20", "language": "python", "runtime-id": "5fae717bb41e4e75bb78b4443e234992" }, @@ -36,6 +34,8 @@ "_dd.top_level": 1, "_dd.tracer_kr": 1.0, "_sampling_priority_v1": 1, + "bedrock.response.usage.completion_tokens": 10, + "bedrock.response.usage.prompt_tokens": 20, "process_id": 13549 }, "duration": 659370000, diff --git a/tests/snapshots/tests.contrib.botocore.test_bedrock.test_cohere_invoke_stream_multi_output.json b/tests/snapshots/tests.contrib.botocore.test_bedrock.test_cohere_invoke_stream_multi_output.json index 506f32bb0ab..5482491249d 100644 --- a/tests/snapshots/tests.contrib.botocore.test_bedrock.test_cohere_invoke_stream_multi_output.json +++ b/tests/snapshots/tests.contrib.botocore.test_bedrock.test_cohere_invoke_stream_multi_output.json @@ -28,8 +28,6 @@ "bedrock.response.choices.1.text": " A large language model (LLM) chain refers", "bedrock.response.duration": "597", "bedrock.response.id": "2501c28f-0a40-49ec-9b59-5d58313c05f3", - "bedrock.usage.completion_tokens": "20", - "bedrock.usage.prompt_tokens": "40", "language": "python", "runtime-id": "e2468c635ce44f8788acce3e9e569237" }, @@ -37,6 +35,8 @@ "_dd.top_level": 1, "_dd.tracer_kr": 1.0, "_sampling_priority_v1": 1, + "bedrock.response.usage.completion_tokens": 20, + "bedrock.response.usage.prompt_tokens": 40, "process_id": 21816 }, "duration": 980170000, diff --git a/tests/snapshots/tests.contrib.botocore.test_bedrock.test_cohere_invoke_stream_single_output.json b/tests/snapshots/tests.contrib.botocore.test_bedrock.test_cohere_invoke_stream_single_output.json index f6c889bcffb..e5dcc4f0e00 100644 --- a/tests/snapshots/tests.contrib.botocore.test_bedrock.test_cohere_invoke_stream_single_output.json +++ b/tests/snapshots/tests.contrib.botocore.test_bedrock.test_cohere_invoke_stream_single_output.json @@ -26,8 +26,6 @@ "bedrock.response.choices.0.text": " I am very happy to assist you! A Long", "bedrock.response.duration": "347", "bedrock.response.id": "ff340d3e-475b-4774-92fa-56ee3d4702c8", - "bedrock.usage.completion_tokens": "10", - "bedrock.usage.prompt_tokens": "20", "language": "python", "runtime-id": "e2468c635ce44f8788acce3e9e569237" }, @@ -35,6 +33,8 @@ "_dd.top_level": 1, "_dd.tracer_kr": 1.0, "_sampling_priority_v1": 1, + "bedrock.response.usage.completion_tokens": 10, + "bedrock.response.usage.prompt_tokens": 20, "process_id": 21816 }, "duration": 630536000, diff --git a/tests/snapshots/tests.contrib.botocore.test_bedrock.test_invoke_model_using_aws_arn_model_id.json b/tests/snapshots/tests.contrib.botocore.test_bedrock.test_invoke_model_using_aws_arn_model_id.json index 0da1e335083..d98344bef5f 100644 --- a/tests/snapshots/tests.contrib.botocore.test_bedrock.test_invoke_model_using_aws_arn_model_id.json +++ b/tests/snapshots/tests.contrib.botocore.test_bedrock.test_invoke_model_using_aws_arn_model_id.json @@ -23,8 +23,6 @@ "bedrock.response.choices.0.text": "\\n\\nDatadog is a monitoring and analytics platform for IT operations, DevOps, and software development teams. It provides real-t...", "bedrock.response.duration": "2646", "bedrock.response.id": "b2d0fd44-c29a-4cd4-a97a-6901a48f6264", - "bedrock.usage.completion_tokens": "50", - "bedrock.usage.prompt_tokens": "18", "language": "python", "runtime-id": "cf8ef38d3504475ba71634071f15d00f" }, @@ -32,6 +30,8 @@ "_dd.top_level": 1, "_dd.tracer_kr": 1.0, "_sampling_priority_v1": 1, + "bedrock.response.usage.completion_tokens": 50, + "bedrock.response.usage.prompt_tokens": 18, "process_id": 96028 }, "duration": 2318000, diff --git a/tests/snapshots/tests.contrib.botocore.test_bedrock.test_meta_invoke.json b/tests/snapshots/tests.contrib.botocore.test_bedrock.test_meta_invoke.json index ade15ee69a4..4b530365443 100644 --- a/tests/snapshots/tests.contrib.botocore.test_bedrock.test_meta_invoke.json +++ b/tests/snapshots/tests.contrib.botocore.test_bedrock.test_meta_invoke.json @@ -22,8 +22,6 @@ "bedrock.response.choices.0.text": "\\n\\n\"Lorem ipsum\" is a commonly used phrase in the graphic design and publishing industries. It refers to a piece of pretend tex...", "bedrock.response.duration": "1686", "bedrock.response.id": "491ad1a3-45d8-4d84-b92c-eb8e79cc79d3", - "bedrock.usage.completion_tokens": "60", - "bedrock.usage.prompt_tokens": "10", "language": "python", "runtime-id": "63a32fa9ed86471383f1355fba7356d8" }, @@ -31,6 +29,8 @@ "_dd.top_level": 1, "_dd.tracer_kr": 1.0, "_sampling_priority_v1": 1, + "bedrock.response.usage.completion_tokens": 60, + "bedrock.response.usage.prompt_tokens": 10, "process_id": 10703 }, "duration": 2120703000, diff --git a/tests/snapshots/tests.contrib.botocore.test_bedrock.test_meta_invoke_stream.json b/tests/snapshots/tests.contrib.botocore.test_bedrock.test_meta_invoke_stream.json index c163928cb62..8a55ff7bd77 100644 --- a/tests/snapshots/tests.contrib.botocore.test_bedrock.test_meta_invoke_stream.json +++ b/tests/snapshots/tests.contrib.botocore.test_bedrock.test_meta_invoke_stream.json @@ -22,8 +22,6 @@ "bedrock.response.choices.0.text": "\\n\\nThe phrase \"lorem ipsum\" is a commonly used dummy text in the graphic design and publishing industries. The text is derived ...", "bedrock.response.duration": "1686", "bedrock.response.id": "4dcc90b7-81b8-4983-b2d3-9989798b0db1", - "bedrock.usage.completion_tokens": "60", - "bedrock.usage.prompt_tokens": "10", "language": "python", "runtime-id": "60ce339e546c4812962fb496314b8dab" }, @@ -31,6 +29,8 @@ "_dd.top_level": 1, "_dd.tracer_kr": 1.0, "_sampling_priority_v1": 1, + "bedrock.response.usage.completion_tokens": 60, + "bedrock.response.usage.prompt_tokens": 10, "process_id": 2664 }, "duration": 3795000, diff --git a/tests/snapshots/tests.contrib.botocore.test_bedrock.test_read_error.json b/tests/snapshots/tests.contrib.botocore.test_bedrock.test_read_error.json index a99c84fa59f..f2a37ba6f87 100644 --- a/tests/snapshots/tests.contrib.botocore.test_bedrock.test_read_error.json +++ b/tests/snapshots/tests.contrib.botocore.test_bedrock.test_read_error.json @@ -20,8 +20,6 @@ "bedrock.request.top_p": "1.0", "bedrock.response.duration": "1686", "bedrock.response.id": "491ad1a3-45d8-4d84-b92c-eb8e79cc79d3", - "bedrock.usage.completion_tokens": "60", - "bedrock.usage.prompt_tokens": "10", "error.message": "test", "error.stack": "Traceback (most recent call last):\n File \"/Users/yun.kim/go/src/github.com/DataDog/dd-trace-py/ddtrace/contrib/botocore/services/bedrock.py\", line 37, in read\n formatted_response = _extract_response(self._datadog_span, self._body[0])\n File \"/Users/yun.kim/go/src/github.com/DataDog/dd-trace-py/.riot/venv_py3105_mock_pytest_pytest-mock_coverage_pytest-cov_opentracing_hypothesis6451_moto[all]_botocore_pytest-randomly_vcrpy/lib/python3.10/site-packages/mock/mock.py\", line 1178, in __call__\n return _mock_self._mock_call(*args, **kwargs)\n File \"/Users/yun.kim/go/src/github.com/DataDog/dd-trace-py/.riot/venv_py3105_mock_pytest_pytest-mock_coverage_pytest-cov_opentracing_hypothesis6451_moto[all]_botocore_pytest-randomly_vcrpy/lib/python3.10/site-packages/mock/mock.py\", line 1182, in _mock_call\n return _mock_self._execute_mock_call(*args, **kwargs)\n File \"/Users/yun.kim/go/src/github.com/DataDog/dd-trace-py/.riot/venv_py3105_mock_pytest_pytest-mock_coverage_pytest-cov_opentracing_hypothesis6451_moto[all]_botocore_pytest-randomly_vcrpy/lib/python3.10/site-packages/mock/mock.py\", line 1239, in _execute_mock_call\n raise effect\nException: test\n", "error.type": "builtins.Exception", @@ -32,6 +30,8 @@ "_dd.top_level": 1, "_dd.tracer_kr": 1.0, "_sampling_priority_v1": 1, + "bedrock.response.usage.completion_tokens": 60, + "bedrock.response.usage.prompt_tokens": 10, "process_id": 42139 }, "duration": 2505000, diff --git a/tests/snapshots/tests.contrib.botocore.test_bedrock.test_readlines_error.json b/tests/snapshots/tests.contrib.botocore.test_bedrock.test_readlines_error.json index 3090d3b72e6..11648ab5b94 100644 --- a/tests/snapshots/tests.contrib.botocore.test_bedrock.test_readlines_error.json +++ b/tests/snapshots/tests.contrib.botocore.test_bedrock.test_readlines_error.json @@ -20,8 +20,6 @@ "bedrock.request.top_p": "1.0", "bedrock.response.duration": "1686", "bedrock.response.id": "491ad1a3-45d8-4d84-b92c-eb8e79cc79d3", - "bedrock.usage.completion_tokens": "60", - "bedrock.usage.prompt_tokens": "10", "error.message": "test", "error.stack": "Traceback (most recent call last):\n File \"/Users/yun.kim/go/src/github.com/DataDog/dd-trace-py/ddtrace/contrib/botocore/services/bedrock.py\", line 51, in readlines\n formatted_response = _extract_response(self._datadog_span, self._body[0])\n File \"/Users/yun.kim/go/src/github.com/DataDog/dd-trace-py/.riot/venv_py3105_mock_pytest_pytest-mock_coverage_pytest-cov_opentracing_hypothesis6451_moto[all]_botocore_pytest-randomly_vcrpy/lib/python3.10/site-packages/mock/mock.py\", line 1178, in __call__\n return _mock_self._mock_call(*args, **kwargs)\n File \"/Users/yun.kim/go/src/github.com/DataDog/dd-trace-py/.riot/venv_py3105_mock_pytest_pytest-mock_coverage_pytest-cov_opentracing_hypothesis6451_moto[all]_botocore_pytest-randomly_vcrpy/lib/python3.10/site-packages/mock/mock.py\", line 1182, in _mock_call\n return _mock_self._execute_mock_call(*args, **kwargs)\n File \"/Users/yun.kim/go/src/github.com/DataDog/dd-trace-py/.riot/venv_py3105_mock_pytest_pytest-mock_coverage_pytest-cov_opentracing_hypothesis6451_moto[all]_botocore_pytest-randomly_vcrpy/lib/python3.10/site-packages/mock/mock.py\", line 1239, in _execute_mock_call\n raise effect\nException: test\n", "error.type": "builtins.Exception", @@ -32,6 +30,8 @@ "_dd.top_level": 1, "_dd.tracer_kr": 1.0, "_sampling_priority_v1": 1, + "bedrock.response.usage.completion_tokens": 60, + "bedrock.response.usage.prompt_tokens": 10, "process_id": 42139 }, "duration": 3064000, diff --git a/tests/snapshots/tests.contrib.openai.test_openai_v0.test_integration_service_name[None-None].json b/tests/snapshots/tests.contrib.openai.test_openai_v0.test_integration_service_name[None-None].json deleted file mode 100644 index 65eec00d960..00000000000 --- a/tests/snapshots/tests.contrib.openai.test_openai_v0.test_integration_service_name[None-None].json +++ /dev/null @@ -1,77 +0,0 @@ -[[ - { - "name": "openai.request", - "service": "ddtrace_subprocess_dir", - "resource": "createCompletion", - "trace_id": 0, - "span_id": 1, - "parent_id": 0, - "type": "", - "error": 0, - "meta": { - "_dd.p.dm": "-0", - "_dd.p.tid": "65674d7100000000", - "component": "openai", - "language": "python", - "openai.api_base": "https://api.openai.com/v1", - "openai.api_type": "open_ai", - "openai.organization.name": "datadog-4", - "openai.request.client": "OpenAI", - "openai.request.endpoint": "/v1/completions", - "openai.request.max_tokens": "10", - "openai.request.method": "POST", - "openai.request.model": "ada", - "openai.request.n": "2", - "openai.request.prompt.0": "Hello world", - "openai.request.stop": ".", - "openai.request.temperature": "0.8", - "openai.response.choices.0.finish_reason": "length", - "openai.response.choices.0.text": ", relax!\u201d I said to my laptop", - "openai.response.choices.1.finish_reason": "stop", - "openai.response.choices.1.text": " (1", - "openai.response.created": "1681852797", - "openai.response.id": "cmpl-76n1xLvRKv3mfjx7hJ41UHrHy9ar6", - "openai.response.model": "ada", - "openai.user.api_key": "sk-...key>", - "runtime-id": "8c92d3e850d9413593bf481d805039d1" - }, - "metrics": { - "_dd.measured": 1, - "_dd.top_level": 1, - "_dd.tracer_kr": 1.0, - "_sampling_priority_v1": 1, - "openai.organization.ratelimit.requests.limit": 3000, - "openai.organization.ratelimit.requests.remaining": 2999, - "openai.organization.ratelimit.tokens.limit": 250000, - "openai.organization.ratelimit.tokens.remaining": 249979, - "process_id": 20673 - }, - "duration": 21745000, - "start": 1701268849462298000 - }, - { - "name": "requests.request", - "service": "openai", - "resource": "POST /v1/completions", - "trace_id": 0, - "span_id": 2, - "parent_id": 1, - "type": "http", - "error": 0, - "meta": { - "_dd.base_service": "ddtrace_subprocess_dir", - "component": "requests", - "http.method": "POST", - "http.status_code": "200", - "http.url": "https://api.openai.com/v1/completions", - "http.useragent": "OpenAI/v1 PythonBindings/0.27.2", - "out.host": "api.openai.com", - "span.kind": "client" - }, - "metrics": { - "_dd.measured": 1, - "_dd.top_level": 1 - }, - "duration": 2999000, - "start": 1701268849479960000 - }]] diff --git a/tests/snapshots/tests.contrib.openai.test_openai_v0.test_integration_service_name[None-v0].json b/tests/snapshots/tests.contrib.openai.test_openai_v0.test_integration_service_name[None-v0].json deleted file mode 100644 index d6417fb5667..00000000000 --- a/tests/snapshots/tests.contrib.openai.test_openai_v0.test_integration_service_name[None-v0].json +++ /dev/null @@ -1,77 +0,0 @@ -[[ - { - "name": "openai.request", - "service": "ddtrace_subprocess_dir", - "resource": "createCompletion", - "trace_id": 0, - "span_id": 1, - "parent_id": 0, - "type": "", - "error": 0, - "meta": { - "_dd.p.dm": "-0", - "_dd.p.tid": "65674d7200000000", - "component": "openai", - "language": "python", - "openai.api_base": "https://api.openai.com/v1", - "openai.api_type": "open_ai", - "openai.organization.name": "datadog-4", - "openai.request.client": "OpenAI", - "openai.request.endpoint": "/v1/completions", - "openai.request.max_tokens": "10", - "openai.request.method": "POST", - "openai.request.model": "ada", - "openai.request.n": "2", - "openai.request.prompt.0": "Hello world", - "openai.request.stop": ".", - "openai.request.temperature": "0.8", - "openai.response.choices.0.finish_reason": "length", - "openai.response.choices.0.text": ", relax!\u201d I said to my laptop", - "openai.response.choices.1.finish_reason": "stop", - "openai.response.choices.1.text": " (1", - "openai.response.created": "1681852797", - "openai.response.id": "cmpl-76n1xLvRKv3mfjx7hJ41UHrHy9ar6", - "openai.response.model": "ada", - "openai.user.api_key": "sk-...key>", - "runtime-id": "675032183b244929ba8c3a0a1c0021e5" - }, - "metrics": { - "_dd.measured": 1, - "_dd.top_level": 1, - "_dd.tracer_kr": 1.0, - "_sampling_priority_v1": 1, - "openai.organization.ratelimit.requests.limit": 3000, - "openai.organization.ratelimit.requests.remaining": 2999, - "openai.organization.ratelimit.tokens.limit": 250000, - "openai.organization.ratelimit.tokens.remaining": 249979, - "process_id": 20696 - }, - "duration": 20412000, - "start": 1701268850764763000 - }, - { - "name": "requests.request", - "service": "openai", - "resource": "POST /v1/completions", - "trace_id": 0, - "span_id": 2, - "parent_id": 1, - "type": "http", - "error": 0, - "meta": { - "_dd.base_service": "ddtrace_subprocess_dir", - "component": "requests", - "http.method": "POST", - "http.status_code": "200", - "http.url": "https://api.openai.com/v1/completions", - "http.useragent": "OpenAI/v1 PythonBindings/0.27.2", - "out.host": "api.openai.com", - "span.kind": "client" - }, - "metrics": { - "_dd.measured": 1, - "_dd.top_level": 1 - }, - "duration": 3134000, - "start": 1701268850780901000 - }]] diff --git a/tests/snapshots/tests.contrib.openai.test_openai_v0.test_integration_service_name[None-v1].json b/tests/snapshots/tests.contrib.openai.test_openai_v0.test_integration_service_name[None-v1].json deleted file mode 100644 index 979ea768ef5..00000000000 --- a/tests/snapshots/tests.contrib.openai.test_openai_v0.test_integration_service_name[None-v1].json +++ /dev/null @@ -1,77 +0,0 @@ -[[ - { - "name": "openai.request", - "service": "ddtrace_subprocess_dir", - "resource": "createCompletion", - "trace_id": 0, - "span_id": 1, - "parent_id": 0, - "type": "", - "error": 0, - "meta": { - "_dd.p.dm": "-0", - "_dd.p.tid": "65674d7400000000", - "component": "openai", - "language": "python", - "openai.api_base": "https://api.openai.com/v1", - "openai.api_type": "open_ai", - "openai.organization.name": "datadog-4", - "openai.request.client": "OpenAI", - "openai.request.endpoint": "/v1/completions", - "openai.request.max_tokens": "10", - "openai.request.method": "POST", - "openai.request.model": "ada", - "openai.request.n": "2", - "openai.request.prompt.0": "Hello world", - "openai.request.stop": ".", - "openai.request.temperature": "0.8", - "openai.response.choices.0.finish_reason": "length", - "openai.response.choices.0.text": ", relax!\u201d I said to my laptop", - "openai.response.choices.1.finish_reason": "stop", - "openai.response.choices.1.text": " (1", - "openai.response.created": "1681852797", - "openai.response.id": "cmpl-76n1xLvRKv3mfjx7hJ41UHrHy9ar6", - "openai.response.model": "ada", - "openai.user.api_key": "sk-...key>", - "runtime-id": "1f3499a720954236be60cf0fece4246c" - }, - "metrics": { - "_dd.measured": 1, - "_dd.top_level": 1, - "_dd.tracer_kr": 1.0, - "_sampling_priority_v1": 1, - "openai.organization.ratelimit.requests.limit": 3000, - "openai.organization.ratelimit.requests.remaining": 2999, - "openai.organization.ratelimit.tokens.limit": 250000, - "openai.organization.ratelimit.tokens.remaining": 249979, - "process_id": 20714 - }, - "duration": 19970000, - "start": 1701268852029562000 - }, - { - "name": "http.client.request", - "service": "ddtrace_subprocess_dir", - "resource": "POST /v1/completions", - "trace_id": 0, - "span_id": 2, - "parent_id": 1, - "type": "http", - "error": 0, - "meta": { - "_dd.peer.service.source": "out.host", - "component": "requests", - "http.method": "POST", - "http.status_code": "200", - "http.url": "https://api.openai.com/v1/completions", - "http.useragent": "OpenAI/v1 PythonBindings/0.27.2", - "out.host": "api.openai.com", - "peer.service": "api.openai.com", - "span.kind": "client" - }, - "metrics": { - "_dd.measured": 1 - }, - "duration": 2897000, - "start": 1701268852045569000 - }]] diff --git a/tests/snapshots/tests.contrib.openai.test_openai_v0.test_integration_service_name[mysvc-None].json b/tests/snapshots/tests.contrib.openai.test_openai_v0.test_integration_service_name[mysvc-None].json deleted file mode 100644 index a80c1218caf..00000000000 --- a/tests/snapshots/tests.contrib.openai.test_openai_v0.test_integration_service_name[mysvc-None].json +++ /dev/null @@ -1,77 +0,0 @@ -[[ - { - "name": "openai.request", - "service": "mysvc", - "resource": "createCompletion", - "trace_id": 0, - "span_id": 1, - "parent_id": 0, - "type": "", - "error": 0, - "meta": { - "_dd.p.dm": "-0", - "_dd.p.tid": "65674d7500000000", - "component": "openai", - "language": "python", - "openai.api_base": "https://api.openai.com/v1", - "openai.api_type": "open_ai", - "openai.organization.name": "datadog-4", - "openai.request.client": "OpenAI", - "openai.request.endpoint": "/v1/completions", - "openai.request.max_tokens": "10", - "openai.request.method": "POST", - "openai.request.model": "ada", - "openai.request.n": "2", - "openai.request.prompt.0": "Hello world", - "openai.request.stop": ".", - "openai.request.temperature": "0.8", - "openai.response.choices.0.finish_reason": "length", - "openai.response.choices.0.text": ", relax!\u201d I said to my laptop", - "openai.response.choices.1.finish_reason": "stop", - "openai.response.choices.1.text": " (1", - "openai.response.created": "1681852797", - "openai.response.id": "cmpl-76n1xLvRKv3mfjx7hJ41UHrHy9ar6", - "openai.response.model": "ada", - "openai.user.api_key": "sk-...key>", - "runtime-id": "1244eea37568412fb5bdedf9c37ed48a" - }, - "metrics": { - "_dd.measured": 1, - "_dd.top_level": 1, - "_dd.tracer_kr": 1.0, - "_sampling_priority_v1": 1, - "openai.organization.ratelimit.requests.limit": 3000, - "openai.organization.ratelimit.requests.remaining": 2999, - "openai.organization.ratelimit.tokens.limit": 250000, - "openai.organization.ratelimit.tokens.remaining": 249979, - "process_id": 20736 - }, - "duration": 19953000, - "start": 1701268853284736000 - }, - { - "name": "requests.request", - "service": "openai", - "resource": "POST /v1/completions", - "trace_id": 0, - "span_id": 2, - "parent_id": 1, - "type": "http", - "error": 0, - "meta": { - "_dd.base_service": "mysvc", - "component": "requests", - "http.method": "POST", - "http.status_code": "200", - "http.url": "https://api.openai.com/v1/completions", - "http.useragent": "OpenAI/v1 PythonBindings/0.27.2", - "out.host": "api.openai.com", - "span.kind": "client" - }, - "metrics": { - "_dd.measured": 1, - "_dd.top_level": 1 - }, - "duration": 2837000, - "start": 1701268853300833000 - }]] diff --git a/tests/snapshots/tests.contrib.openai.test_openai_v0.test_integration_service_name[mysvc-v0].json b/tests/snapshots/tests.contrib.openai.test_openai_v0.test_integration_service_name[mysvc-v0].json deleted file mode 100644 index f3f9c57f768..00000000000 --- a/tests/snapshots/tests.contrib.openai.test_openai_v0.test_integration_service_name[mysvc-v0].json +++ /dev/null @@ -1,77 +0,0 @@ -[[ - { - "name": "openai.request", - "service": "mysvc", - "resource": "createCompletion", - "trace_id": 0, - "span_id": 1, - "parent_id": 0, - "type": "", - "error": 0, - "meta": { - "_dd.p.dm": "-0", - "_dd.p.tid": "65674d7600000000", - "component": "openai", - "language": "python", - "openai.api_base": "https://api.openai.com/v1", - "openai.api_type": "open_ai", - "openai.organization.name": "datadog-4", - "openai.request.client": "OpenAI", - "openai.request.endpoint": "/v1/completions", - "openai.request.max_tokens": "10", - "openai.request.method": "POST", - "openai.request.model": "ada", - "openai.request.n": "2", - "openai.request.prompt.0": "Hello world", - "openai.request.stop": ".", - "openai.request.temperature": "0.8", - "openai.response.choices.0.finish_reason": "length", - "openai.response.choices.0.text": ", relax!\u201d I said to my laptop", - "openai.response.choices.1.finish_reason": "stop", - "openai.response.choices.1.text": " (1", - "openai.response.created": "1681852797", - "openai.response.id": "cmpl-76n1xLvRKv3mfjx7hJ41UHrHy9ar6", - "openai.response.model": "ada", - "openai.user.api_key": "sk-...key>", - "runtime-id": "12b4a711854c44f681695957b545dcf5" - }, - "metrics": { - "_dd.measured": 1, - "_dd.top_level": 1, - "_dd.tracer_kr": 1.0, - "_sampling_priority_v1": 1, - "openai.organization.ratelimit.requests.limit": 3000, - "openai.organization.ratelimit.requests.remaining": 2999, - "openai.organization.ratelimit.tokens.limit": 250000, - "openai.organization.ratelimit.tokens.remaining": 249979, - "process_id": 20750 - }, - "duration": 25352000, - "start": 1701268854568669000 - }, - { - "name": "requests.request", - "service": "openai", - "resource": "POST /v1/completions", - "trace_id": 0, - "span_id": 2, - "parent_id": 1, - "type": "http", - "error": 0, - "meta": { - "_dd.base_service": "mysvc", - "component": "requests", - "http.method": "POST", - "http.status_code": "200", - "http.url": "https://api.openai.com/v1/completions", - "http.useragent": "OpenAI/v1 PythonBindings/0.27.2", - "out.host": "api.openai.com", - "span.kind": "client" - }, - "metrics": { - "_dd.measured": 1, - "_dd.top_level": 1 - }, - "duration": 3922000, - "start": 1701268854588758000 - }]] diff --git a/tests/snapshots/tests.contrib.openai.test_openai_v0.test_integration_service_name[mysvc-v1].json b/tests/snapshots/tests.contrib.openai.test_openai_v0.test_integration_service_name[mysvc-v1].json deleted file mode 100644 index 0696ae54454..00000000000 --- a/tests/snapshots/tests.contrib.openai.test_openai_v0.test_integration_service_name[mysvc-v1].json +++ /dev/null @@ -1,77 +0,0 @@ -[[ - { - "name": "openai.request", - "service": "mysvc", - "resource": "createCompletion", - "trace_id": 0, - "span_id": 1, - "parent_id": 0, - "type": "", - "error": 0, - "meta": { - "_dd.p.dm": "-0", - "_dd.p.tid": "65674d7700000000", - "component": "openai", - "language": "python", - "openai.api_base": "https://api.openai.com/v1", - "openai.api_type": "open_ai", - "openai.organization.name": "datadog-4", - "openai.request.client": "OpenAI", - "openai.request.endpoint": "/v1/completions", - "openai.request.max_tokens": "10", - "openai.request.method": "POST", - "openai.request.model": "ada", - "openai.request.n": "2", - "openai.request.prompt.0": "Hello world", - "openai.request.stop": ".", - "openai.request.temperature": "0.8", - "openai.response.choices.0.finish_reason": "length", - "openai.response.choices.0.text": ", relax!\u201d I said to my laptop", - "openai.response.choices.1.finish_reason": "stop", - "openai.response.choices.1.text": " (1", - "openai.response.created": "1681852797", - "openai.response.id": "cmpl-76n1xLvRKv3mfjx7hJ41UHrHy9ar6", - "openai.response.model": "ada", - "openai.user.api_key": "sk-...key>", - "runtime-id": "03e7664126ea4fe99e0aefec4efd003c" - }, - "metrics": { - "_dd.measured": 1, - "_dd.top_level": 1, - "_dd.tracer_kr": 1.0, - "_sampling_priority_v1": 1, - "openai.organization.ratelimit.requests.limit": 3000, - "openai.organization.ratelimit.requests.remaining": 2999, - "openai.organization.ratelimit.tokens.limit": 250000, - "openai.organization.ratelimit.tokens.remaining": 249979, - "process_id": 20772 - }, - "duration": 19966000, - "start": 1701268855885252000 - }, - { - "name": "http.client.request", - "service": "mysvc", - "resource": "POST /v1/completions", - "trace_id": 0, - "span_id": 2, - "parent_id": 1, - "type": "http", - "error": 0, - "meta": { - "_dd.peer.service.source": "out.host", - "component": "requests", - "http.method": "POST", - "http.status_code": "200", - "http.url": "https://api.openai.com/v1/completions", - "http.useragent": "OpenAI/v1 PythonBindings/0.27.2", - "out.host": "api.openai.com", - "peer.service": "api.openai.com", - "span.kind": "client" - }, - "metrics": { - "_dd.measured": 1 - }, - "duration": 2849000, - "start": 1701268855901267000 - }]] diff --git a/tests/snapshots/tests.contrib.openai.test_openai_v1.test_integration_async.json b/tests/snapshots/tests.contrib.openai.test_openai_v1.test_integration_async.json index 9cfd3a107cd..ab2f74aa60b 100644 --- a/tests/snapshots/tests.contrib.openai.test_openai_v1.test_integration_async.json +++ b/tests/snapshots/tests.contrib.openai.test_openai_v1.test_integration_async.json @@ -44,6 +44,9 @@ "openai.organization.ratelimit.requests.remaining": 2999, "openai.organization.ratelimit.tokens.limit": 250000, "openai.organization.ratelimit.tokens.remaining": 249979, + "openai.response.usage.completion_tokens": 12, + "openai.response.usage.prompt_tokens": 2, + "openai.response.usage.total_tokens": 14, "process_id": 24448 }, "duration": 17466000, diff --git a/tests/snapshots/tests.contrib.openai.test_openai_v1.test_integration_service_name[None-None].json b/tests/snapshots/tests.contrib.openai.test_openai_v1.test_integration_service_name[None-None].json index 3361ea38b5c..d7faa3f22e2 100644 --- a/tests/snapshots/tests.contrib.openai.test_openai_v1.test_integration_service_name[None-None].json +++ b/tests/snapshots/tests.contrib.openai.test_openai_v1.test_integration_service_name[None-None].json @@ -39,6 +39,9 @@ "openai.organization.ratelimit.requests.remaining": 2999, "openai.organization.ratelimit.tokens.limit": 250000, "openai.organization.ratelimit.tokens.remaining": 249979, + "openai.response.usage.completion_tokens": 12, + "openai.response.usage.prompt_tokens": 2, + "openai.response.usage.total_tokens": 14, "process_id": 20806 }, "duration": 16421000, diff --git a/tests/snapshots/tests.contrib.openai.test_openai_v1.test_integration_service_name[None-v0].json b/tests/snapshots/tests.contrib.openai.test_openai_v1.test_integration_service_name[None-v0].json index 9815b378221..3af343273f6 100644 --- a/tests/snapshots/tests.contrib.openai.test_openai_v1.test_integration_service_name[None-v0].json +++ b/tests/snapshots/tests.contrib.openai.test_openai_v1.test_integration_service_name[None-v0].json @@ -39,6 +39,9 @@ "openai.organization.ratelimit.requests.remaining": 2999, "openai.organization.ratelimit.tokens.limit": 250000, "openai.organization.ratelimit.tokens.remaining": 249979, + "openai.response.usage.completion_tokens": 12, + "openai.response.usage.prompt_tokens": 2, + "openai.response.usage.total_tokens": 14, "process_id": 20827 }, "duration": 17257000, diff --git a/tests/snapshots/tests.contrib.openai.test_openai_v1.test_integration_service_name[None-v1].json b/tests/snapshots/tests.contrib.openai.test_openai_v1.test_integration_service_name[None-v1].json index 3c9e6612d78..7f51ec196a6 100644 --- a/tests/snapshots/tests.contrib.openai.test_openai_v1.test_integration_service_name[None-v1].json +++ b/tests/snapshots/tests.contrib.openai.test_openai_v1.test_integration_service_name[None-v1].json @@ -39,6 +39,9 @@ "openai.organization.ratelimit.requests.remaining": 2999, "openai.organization.ratelimit.tokens.limit": 250000, "openai.organization.ratelimit.tokens.remaining": 249979, + "openai.response.usage.completion_tokens": 12, + "openai.response.usage.prompt_tokens": 2, + "openai.response.usage.total_tokens": 14, "process_id": 20839 }, "duration": 17259000, diff --git a/tests/snapshots/tests.contrib.openai.test_openai_v1.test_integration_service_name[mysvc-None].json b/tests/snapshots/tests.contrib.openai.test_openai_v1.test_integration_service_name[mysvc-None].json index fb11e4200a0..35268ec5092 100644 --- a/tests/snapshots/tests.contrib.openai.test_openai_v1.test_integration_service_name[mysvc-None].json +++ b/tests/snapshots/tests.contrib.openai.test_openai_v1.test_integration_service_name[mysvc-None].json @@ -39,6 +39,9 @@ "openai.organization.ratelimit.requests.remaining": 2999, "openai.organization.ratelimit.tokens.limit": 250000, "openai.organization.ratelimit.tokens.remaining": 249979, + "openai.response.usage.completion_tokens": 12, + "openai.response.usage.prompt_tokens": 2, + "openai.response.usage.total_tokens": 14, "process_id": 20848 }, "duration": 17004000, diff --git a/tests/snapshots/tests.contrib.openai.test_openai_v1.test_integration_service_name[mysvc-v0].json b/tests/snapshots/tests.contrib.openai.test_openai_v1.test_integration_service_name[mysvc-v0].json index 63341870faa..999dbb7529c 100644 --- a/tests/snapshots/tests.contrib.openai.test_openai_v1.test_integration_service_name[mysvc-v0].json +++ b/tests/snapshots/tests.contrib.openai.test_openai_v1.test_integration_service_name[mysvc-v0].json @@ -39,6 +39,9 @@ "openai.organization.ratelimit.requests.remaining": 2999, "openai.organization.ratelimit.tokens.limit": 250000, "openai.organization.ratelimit.tokens.remaining": 249979, + "openai.response.usage.completion_tokens": 12, + "openai.response.usage.prompt_tokens": 2, + "openai.response.usage.total_tokens": 14, "process_id": 20864 }, "duration": 17872000, diff --git a/tests/snapshots/tests.contrib.openai.test_openai_v1.test_integration_service_name[mysvc-v1].json b/tests/snapshots/tests.contrib.openai.test_openai_v1.test_integration_service_name[mysvc-v1].json index 4ff254b053c..76d352a3f59 100644 --- a/tests/snapshots/tests.contrib.openai.test_openai_v1.test_integration_service_name[mysvc-v1].json +++ b/tests/snapshots/tests.contrib.openai.test_openai_v1.test_integration_service_name[mysvc-v1].json @@ -39,6 +39,9 @@ "openai.organization.ratelimit.requests.remaining": 2999, "openai.organization.ratelimit.tokens.limit": 250000, "openai.organization.ratelimit.tokens.remaining": 249979, + "openai.response.usage.completion_tokens": 12, + "openai.response.usage.prompt_tokens": 2, + "openai.response.usage.total_tokens": 14, "process_id": 20888 }, "duration": 16629000, diff --git a/tests/snapshots/tests.contrib.openai.test_openai_v1.test_integration_sync.json b/tests/snapshots/tests.contrib.openai.test_openai_v1.test_integration_sync.json index 9cfd3a107cd..ab2f74aa60b 100644 --- a/tests/snapshots/tests.contrib.openai.test_openai_v1.test_integration_sync.json +++ b/tests/snapshots/tests.contrib.openai.test_openai_v1.test_integration_sync.json @@ -44,6 +44,9 @@ "openai.organization.ratelimit.requests.remaining": 2999, "openai.organization.ratelimit.tokens.limit": 250000, "openai.organization.ratelimit.tokens.remaining": 249979, + "openai.response.usage.completion_tokens": 12, + "openai.response.usage.prompt_tokens": 2, + "openai.response.usage.total_tokens": 14, "process_id": 24448 }, "duration": 17466000, diff --git a/tests/suitespec.yml b/tests/suitespec.yml index d9da18df66d..69c6b19e8d8 100644 --- a/tests/suitespec.yml +++ b/tests/suitespec.yml @@ -76,7 +76,7 @@ components: - ddtrace/__init__.py - ddtrace/py.typed - ddtrace/version.py - - ddtrace/settings/config.py + - ddtrace/settings/_config.py - src/native/* datastreams: - ddtrace/internal/datastreams/* @@ -117,7 +117,7 @@ components: - ddtrace/trace/* - ddtrace/constants.py - ddtrace/settings/__init__.py - - ddtrace/settings/config.py + - ddtrace/settings/_config.py - ddtrace/settings/http.py - ddtrace/settings/exceptions.py - ddtrace/settings/integration.py diff --git a/tests/telemetry/test_writer.py b/tests/telemetry/test_writer.py index 39d672a1c01..2de23c9fba3 100644 --- a/tests/telemetry/test_writer.py +++ b/tests/telemetry/test_writer.py @@ -118,7 +118,6 @@ def test_app_started_event(telemetry_writer, test_agent_session, mock_time): {"name": "DD_SPAN_SAMPLING_RULES_FILE", "origin": "unknown", "value": None}, {"name": "DD_TRACE_128_BIT_TRACEID_GENERATION_ENABLED", "origin": "unknown", "value": True}, {"name": "DD_TRACE_AGENT_TIMEOUT_SECONDS", "origin": "unknown", "value": 2.0}, - {"name": "DD_TRACE_ANALYTICS_ENABLED", "origin": "unknown", "value": False}, {"name": "DD_TRACE_API_VERSION", "origin": "unknown", "value": None}, {"name": "DD_TRACE_CLIENT_IP_ENABLED", "origin": "unknown", "value": None}, {"name": "DD_TRACE_COMPUTE_STATS", "origin": "unknown", "value": False}, @@ -225,7 +224,6 @@ def test_app_started_event_configuration_override(test_agent_session, run_python env["DD_RUNTIME_METRICS_ENABLED"] = "True" env["DD_SERVICE_MAPPING"] = "default_dd_service:remapped_dd_service" env["DD_TRACE_128_BIT_TRACEID_GENERATION_ENABLED"] = "True" - env["DD_TRACE_ANALYTICS_ENABLED"] = "True" env["DD_TRACE_CLIENT_IP_ENABLED"] = "True" env["DD_TRACE_COMPUTE_STATS"] = "True" env["DD_TRACE_DEBUG"] = "True" @@ -237,7 +235,6 @@ def test_app_started_event_configuration_override(test_agent_session, run_python env["DD_TRACE_PROPAGATION_STYLE_INJECT"] = "tracecontext" env["DD_REMOTE_CONFIGURATION_ENABLED"] = "True" env["DD_REMOTE_CONFIG_POLL_INTERVAL_SECONDS"] = "1" - env["DD_TRACE_SAMPLE_RATE"] = "0.5" env["DD_TRACE_RATE_LIMIT"] = "50" env["DD_TRACE_SAMPLING_RULES"] = '[{"sample_rate":1.0,"service":"xyz","name":"abc"}]' env["DD_PROFILING_ENABLED"] = "True" @@ -356,7 +353,6 @@ def test_app_started_event_configuration_override(test_agent_session, run_python {"name": "DD_EXCEPTION_REPLAY_CAPTURE_MAX_FRAMES", "origin": "default", "value": 8}, {"name": "DD_EXCEPTION_REPLAY_ENABLED", "origin": "env_var", "value": True}, {"name": "DD_EXPERIMENTAL_APPSEC_STANDALONE_ENABLED", "origin": "default", "value": False}, - {"name": "DD_HTTP_CLIENT_TAG_QUERY_STRING", "origin": "default", "value": None}, {"name": "DD_IAST_DEDUPLICATION_ENABLED", "origin": "default", "value": True}, {"name": "DD_IAST_ENABLED", "origin": "default", "value": False}, {"name": "DD_IAST_MAX_CONCURRENT_REQUESTS", "origin": "default", "value": 2}, @@ -433,7 +429,6 @@ def test_app_started_event_configuration_override(test_agent_session, run_python {"name": "DD_TRACE_128_BIT_TRACEID_GENERATION_ENABLED", "origin": "env_var", "value": True}, {"name": "DD_TRACE_128_BIT_TRACEID_LOGGING_ENABLED", "origin": "default", "value": False}, {"name": "DD_TRACE_AGENT_TIMEOUT_SECONDS", "origin": "default", "value": 2.0}, - {"name": "DD_TRACE_ANALYTICS_ENABLED", "origin": "env_var", "value": True}, {"name": "DD_TRACE_API_VERSION", "origin": "env_var", "value": "v0.5"}, {"name": "DD_TRACE_CLIENT_IP_ENABLED", "origin": "env_var", "value": True}, {"name": "DD_TRACE_CLIENT_IP_HEADER", "origin": "default", "value": None}, @@ -456,13 +451,11 @@ def test_app_started_event_configuration_override(test_agent_session, run_python {"name": "DD_TRACE_PROPAGATION_STYLE_INJECT", "origin": "env_var", "value": "tracecontext"}, {"name": "DD_TRACE_RATE_LIMIT", "origin": "env_var", "value": 50}, {"name": "DD_TRACE_REPORT_HOSTNAME", "origin": "default", "value": False}, - {"name": "DD_TRACE_SAMPLE_RATE", "origin": "env_var", "value": 0.5}, { "name": "DD_TRACE_SAMPLING_RULES", "origin": "env_var", "value": '[{"sample_rate":1.0,"service":"xyz","name":"abc"}]', }, - {"name": "DD_TRACE_SPAN_AGGREGATOR_RLOCK", "origin": "default", "value": True}, {"name": "DD_TRACE_SPAN_TRACEBACK_MAX_SIZE", "origin": "default", "value": 30}, {"name": "DD_TRACE_STARTUP_LOGS", "origin": "env_var", "value": True}, {"name": "DD_TRACE_WRITER_BUFFER_SIZE_BYTES", "origin": "env_var", "value": 1000}, @@ -483,6 +476,7 @@ def test_app_started_event_configuration_override(test_agent_session, run_python {"name": "python_build_gnu_type", "origin": "unknown", "value": sysconfig.get_config_var("BUILD_GNU_TYPE")}, {"name": "python_host_gnu_type", "origin": "unknown", "value": sysconfig.get_config_var("HOST_GNU_TYPE")}, {"name": "python_soabi", "origin": "unknown", "value": sysconfig.get_config_var("SOABI")}, + {"name": "trace_sample_rate", "origin": "default", "value": 1.0}, ] assert configurations == expected, configurations diff --git a/tests/tracer/test_encoders.py b/tests/tracer/test_encoders.py index 7006bc6b95d..4fe48a2a838 100644 --- a/tests/tracer/test_encoders.py +++ b/tests/tracer/test_encoders.py @@ -869,19 +869,3 @@ def test_json_encoder_traces_bytes(): assert "\\x80span.a" == span_a["name"] assert "\x80span.b" == span_b["name"] assert "\x80span.b" == span_c["name"] - - -@pytest.mark.subprocess(env={"DD_TRACE_API_VERSION": "v0.3"}) -def test_v03_trace_api_deprecation(): - import warnings - - with warnings.catch_warnings(record=True) as warns: - warnings.simplefilter("always") - from ddtrace.trace import tracer - - assert tracer._writer._api_version == "v0.4" - assert len(warns) == 1, warns - assert ( - warns[0].message.args[0] == "DD_TRACE_API_VERSION=v0.3 is deprecated and will be " - "removed in version '3.0.0': Traces will be submitted to the v0.4/traces agent endpoint instead." - ), warns[0].message diff --git a/tests/tracer/test_sampler.py b/tests/tracer/test_sampler.py index 813dc1be439..f54c7de55da 100644 --- a/tests/tracer/test_sampler.py +++ b/tests/tracer/test_sampler.py @@ -1,6 +1,5 @@ from __future__ import division -import re import unittest import mock @@ -250,7 +249,7 @@ def test_sampling_rule_init_defaults(): def test_sampling_rule_init(): - a_regex = re.compile(r"\.request$") + a_regex = "*request" a_string = "my-service" rule = SamplingRule( @@ -261,7 +260,7 @@ def test_sampling_rule_init(): assert rule.sample_rate == 0.0, "SamplingRule should store the rate it's initialized with" assert rule.service.pattern == a_string, "SamplingRule should store the service it's initialized with" - assert rule.name == a_regex, "SamplingRule should store the name regex it's initialized with" + assert rule.name.pattern == a_regex, "SamplingRule should store the name regex it's initialized with" @pytest.mark.parametrize( @@ -272,38 +271,13 @@ def test_sampling_rule_init(): (SamplingRule(sample_rate=0.0), SamplingRule(sample_rate=0.0), True), (SamplingRule(sample_rate=0.5), SamplingRule(sample_rate=1.0), False), (SamplingRule(sample_rate=1.0, service="my-svc"), SamplingRule(sample_rate=1.0, service="my-svc"), True), - ( - SamplingRule(sample_rate=1.0, service=re.compile("my-svc")), - SamplingRule(sample_rate=1.0, service=re.compile("my-svc")), - True, - ), (SamplingRule(sample_rate=1.0, service="my-svc"), SamplingRule(sample_rate=1.0, service="other-svc"), False), (SamplingRule(sample_rate=1.0, service="my-svc"), SamplingRule(sample_rate=0.5, service="my-svc"), False), - ( - SamplingRule(sample_rate=1.0, service=re.compile("my-svc")), - SamplingRule(sample_rate=0.5, service=re.compile("my-svc")), - False, - ), - ( - SamplingRule(sample_rate=1.0, service=re.compile("my-svc")), - SamplingRule(sample_rate=1.0, service=re.compile("other")), - False, - ), ( SamplingRule(sample_rate=1.0, name="span.name"), SamplingRule(sample_rate=1.0, name="span.name"), True, ), - ( - SamplingRule(sample_rate=1.0, name=re.compile("span.name")), - SamplingRule(sample_rate=1.0, name=re.compile("span.name")), - True, - ), - ( - SamplingRule(sample_rate=1.0, name=re.compile("span.name")), - SamplingRule(sample_rate=1.0, name=re.compile("span.other")), - False, - ), ( SamplingRule(sample_rate=1.0, name="span.name"), SamplingRule(sample_rate=0.5, name="span.name"), @@ -316,16 +290,6 @@ def test_sampling_rule_init(): SamplingRule(sample_rate=1.0, service="my-svc", name="span.name"), True, ), - ( - SamplingRule(sample_rate=1.0, service="my-svc", name=re.compile("span.name")), - SamplingRule(sample_rate=1.0, service="my-svc", name=re.compile("span.name")), - True, - ), - ( - SamplingRule(sample_rate=1.0, service=re.compile("my-svc"), name=re.compile("span.name")), - SamplingRule(sample_rate=1.0, service=re.compile("my-svc"), name=re.compile("span.name")), - True, - ), ( SamplingRule(sample_rate=1.0, service="my-svc", name="span.name"), SamplingRule(sample_rate=0.5, service="my-svc", name="span.name"), @@ -491,15 +455,6 @@ def test_sampling_rule_init_via_env(): ("test.span", None, False), ("test.span", "test.span", True), ("test.span", "test_span", False), - ("test.span", re.compile(r"^test\.span$"), True), - ("test_span", re.compile(r"^test.span$"), True), - ("test.span", re.compile(r"^test_span$"), False), - ("test.span", re.compile(r"test"), True), - ("test.span", re.compile(r"test\.span|another\.span"), True), - ("another.span", re.compile(r"test\.span|another\.span"), True), - ("test.span", lambda name: "span" in name, True), - ("test.span", lambda name: "span" not in name, False), - ("test.span", lambda name: 1 / 0, False), ] ], ) @@ -518,20 +473,8 @@ def test_sampling_rule_matches_name(span, rule, span_expected_to_match_rule): ("my-service", None, False), (None, "tests.tracer", True), ("tests.tracer", "my-service", False), - ("tests.tracer", re.compile(r"my-service"), False), - ("tests.tracer", lambda service: "service" in service, False), ("my-service", "my-service", True), ("my-service", "my_service", False), - ("my-service", re.compile(r"^my-"), True), - ("my_service", re.compile(r"^my[_-]"), True), - ("my-service", re.compile(r"^my_"), False), - ("my-service", re.compile(r"my-service"), True), - ("my-service", re.compile(r"my"), True), - ("my-service", re.compile(r"my-service|another-service"), True), - ("another-service", re.compile(r"my-service|another-service"), True), - ("my-service", lambda service: "service" in service, True), - ("my-service", lambda service: "service" not in service, False), - ("my-service", lambda service: 1 / 0, False), ] ], ) @@ -553,7 +496,7 @@ def test_sampling_rule_matches_service(span, rule, span_expected_to_match_rule): SamplingRule( sample_rate=1, name="test.span", - service=re.compile(r"^my-"), + service="my-*", ), True, ), @@ -567,7 +510,7 @@ def test_sampling_rule_matches_service(span, rule, span_expected_to_match_rule): SamplingRule( sample_rate=0, name="test.span", - service=re.compile(r"^my-"), + service="my-*", ), True, ), @@ -580,7 +523,7 @@ def test_sampling_rule_matches_service(span, rule, span_expected_to_match_rule): SamplingRule( sample_rate=1, name="test_span", - service=re.compile(r"^my-"), + service="my-*", ), False, ), @@ -593,7 +536,7 @@ def test_sampling_rule_matches_service(span, rule, span_expected_to_match_rule): SamplingRule( sample_rate=1, name="test.span", - service=re.compile(r"^service-"), + service="service-", ), False, ), @@ -605,26 +548,6 @@ def test_sampling_rule_matches(span, rule, span_expected_to_match_rule): ) -def test_sampling_rule_matches_exception(): - def pattern(prop): - raise Exception("an error occurred") - - rule = SamplingRule(sample_rate=1.0, name=pattern) - span = create_span(name="test.span") - - with mock.patch("ddtrace._trace.sampling_rule.log") as mock_log: - assert ( - rule.matches(span) is False - ), "SamplingRule should not match when its name pattern function throws an exception" - mock_log.warning.assert_called_once_with( - "%r pattern %r failed with %r", - rule, - pattern, - "test.span", - exc_info=True, - ) - - @pytest.mark.subprocess( parametrize={"DD_TRACE_128_BIT_TRACEID_GENERATION_ENABLED": ["true", "false"]}, ) @@ -645,21 +568,6 @@ def test_sampling_rule_sample(): ) -@pytest.mark.subprocess(env={"DD_TRACE_SAMPLE_RATE": "0.2"}) -def test_sampling_rate_config_deprecated(): - import warnings - - with warnings.catch_warnings(record=True) as ws: - warnings.simplefilter("always") - - from ddtrace import config - - assert config._trace_sample_rate == 0.2 - - assert len(ws) >= 1 - assert any(w for w in ws if "DD_TRACE_SAMPLE_RATE is deprecated" in str(w.message)), [w.message for w in ws] - - def test_sampling_rule_sample_rate_1(): rule = SamplingRule(sample_rate=1) @@ -727,15 +635,6 @@ def test_datadog_sampler_init(): SamplingRule(sample_rate=0.5) ], "DatadogSampler initialized with no arguments and envvars set should hold a sample_rate from the envvar" - with override_global_config(dict(_trace_sample_rate=0)): - sampler = DatadogSampler() - assert ( - sampler.limiter.rate_limit == DatadogSampler.DEFAULT_RATE_LIMIT - ), "DatadogSampler initialized with DD_TRACE_SAMPLE_RATE=0 envvar should hold the default rate limit" - assert sampler.rules == [ - SamplingRule(sample_rate=0) - ], "DatadogSampler initialized with DD_TRACE_SAMPLE_RATE=0 envvar should hold sample_rate=0" - with override_global_config(dict(_trace_sample_rate="asdf")): with pytest.raises(ValueError): DatadogSampler()