From b50bc0f812d90d2d8bc1f822e977cc86831d7769 Mon Sep 17 00:00:00 2001 From: Kyle Verhoog Date: Wed, 14 Dec 2022 16:37:17 -0500 Subject: [PATCH 1/4] chore(logging): fix noisy rq patching log (#4779) The patching logs generated a scary looking message for rq: ``` DEBUG:ddtrace._monkey:failed to patch rq Traceback (most recent call last): File "/Users/kverhoog/dev/dd-trace-py/ddtrace/_monkey.py", line 249, in _patch_module return _attempt_patch_module(module, patch_modules_prefix=patch_modules_prefix) File "/Users/kverhoog/dev/dd-trace-py/ddtrace/_monkey.py", line 304, in _attempt_patch_module imported_module.patch() File "/Users/kverhoog/dev/dd-trace-py/ddtrace/contrib/rq/__init__.py", line 207, in patch import rq ModuleNotFoundError: No module named 'rq' ``` because the rq integration imports the `rq` module on `patch()` and doesn't declare required_modules. Nor does it dynamically expose the patch and unpatch methods. By making the rq integration a patch-on-import integration the issue is avoided. This can be done because the rq integration was written to be patch-on-import compatible (no top-level imports of the library). --- ddtrace/_monkey.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ddtrace/_monkey.py b/ddtrace/_monkey.py index 7dabcac74c3..3de16392fe8 100644 --- a/ddtrace/_monkey.py +++ b/ddtrace/_monkey.py @@ -110,6 +110,7 @@ "elasticsearch7", ), "pynamodb": ("pynamodb",), + "rq": ("rq",), } IAST_PATCH = { From b30669faf3fa2c91affc5b479cd640ed5ed9ab7e Mon Sep 17 00:00:00 2001 From: "Tahir H. Butt" Date: Thu, 15 Dec 2022 10:10:09 -0500 Subject: [PATCH 2/4] fix(aws): limit api params as span tags (#4781) ## Description We are reducing the number of API parameters that the boto, botocore and aiobotocore integrations collect as span tags by default. This change limits span tags to a narrow set of parameters for specific AWS APIs using standard tag names. Users can opt out and collect no parameters. This is a functional breaking change. Users can configure the integrations to retain the existing deprecated behavior of collecting all parameters checked against an exclusion list. ## Checklist - [X] Followed the [library release note guidelines](https://ddtrace.readthedocs.io/en/stable/contributing.html#Release-Note-Guidelines) when writing a release note. - [X] Add additional sections for `feat` and `fix` pull requests. - [X] [Library documentation](https://github.com/DataDog/dd-trace-py/tree/1.x/docs) and/or [Datadog's documentation site](https://github.com/DataDog/documentation/) is updated. Link to doc PR in description. ## Relevant issue(s) Discussed in #4710 ## Testing strategy Test cases have been added for new behavior as well as for the deprecated behavior. ## Reviewer Checklist - [ ] Title is accurate. - [ ] Description motivates each change. - [ ] No unnecessary changes were introduced in this PR. - [ ] Avoid breaking [API](https://ddtrace.readthedocs.io/en/stable/versioning.html#interfaces) changes unless absolutely necessary. - [ ] Tests provided or description of manual testing performed is included in the code or PR. - [ ] Release note has been added and follows the [library release note guidelines](https://ddtrace.readthedocs.io/en/stable/contributing.html#Release-Note-Guidelines), or else `changelog/no-changelog` label added. - [ ] All relevant GitHub issues are correctly linked. - [ ] Backports are identified and tagged with Mergifyio. --- ddtrace/contrib/aiobotocore/__init__.py | 44 +- ddtrace/contrib/aiobotocore/patch.py | 29 +- ddtrace/contrib/boto/__init__.py | 46 +- ddtrace/contrib/boto/patch.py | 30 +- ddtrace/contrib/botocore/__init__.py | 26 + ddtrace/contrib/botocore/patch.py | 17 +- ddtrace/ext/aws.py | 33 + docs/spelling_wordlist.txt | 1 + .../fix-aws-tag-params-0e1488513a0ae5c7.yaml | 7 + tests/contrib/aiobotocore/test.py | 34 +- tests/contrib/boto/test.py | 43 +- tests/contrib/botocore/test.py | 1364 ++++++++++------- 12 files changed, 1052 insertions(+), 622 deletions(-) create mode 100755 releasenotes/notes/fix-aws-tag-params-0e1488513a0ae5c7.yaml diff --git a/ddtrace/contrib/aiobotocore/__init__.py b/ddtrace/contrib/aiobotocore/__init__.py index 125c126df25..731c5e0c8f3 100644 --- a/ddtrace/contrib/aiobotocore/__init__.py +++ b/ddtrace/contrib/aiobotocore/__init__.py @@ -1,22 +1,44 @@ """ The aiobotocore integration will trace all AWS calls made with the ``aiobotocore`` -library. This integration isn't enabled when applying the default patching. -To enable it, you must run ``patch_all(aiobotocore=True)`` +library. This integration is not enabled by default. -:: +Enabling +~~~~~~~~ - import aiobotocore.session - from ddtrace import patch +The aiobotocore integration is not enabled by default. Use +:func:`patch()` to enable the integration:: - # If not patched yet, you can patch botocore specifically + from ddtrace import patch patch(aiobotocore=True) - # This will report spans with the default instrumentation - aiobotocore.session.get_session() - lambda_client = session.create_client('lambda', region_name='us-east-1') +Configuration +~~~~~~~~~~~~~ + +.. py:data:: ddtrace.config.aiobotocore['tag_no_params'] + + This opts out of the default behavior of adding span tags for a narrow set of API parameters. + + To not collect any API parameters, ``ddtrace.config.aiobotocore.tag_no_params = True`` or by setting the environment + variable ``DD_AWS_TAG_NO_PARAMS=true``. + + + Default: ``False`` + +.. py:data:: ddtrace.config.aiobotocore['tag_all_params'] + + **Deprecated**: This retains the deprecated behavior of adding span tags for + all API parameters that are not explicitly excluded by the integration. + These deprecated span tags will be added along with the API parameters + enabled by default. + + This configuration is ignored if ``tag_no_parms`` (``DD_AWS_TAG_NO_PARAMS``) + is set to ``True``. + + To collect all API parameters, ``ddtrace.config.botocore.tag_all_params = + True`` or by setting the environment variable ``DD_AWS_TAG_ALL_PARAMS=true``. + - # This query generates a trace - lambda_client.list_functions() + Default: ``False`` """ from ...internal.utils.importlib import require_modules diff --git a/ddtrace/contrib/aiobotocore/patch.py b/ddtrace/contrib/aiobotocore/patch.py index d0a8326ae19..ab9e1143dcb 100644 --- a/ddtrace/contrib/aiobotocore/patch.py +++ b/ddtrace/contrib/aiobotocore/patch.py @@ -1,7 +1,10 @@ +import os + import aiobotocore.client from ddtrace import config from ddtrace.internal.utils.version import parse_version +from ddtrace.vendor import debtcollector from ddtrace.vendor import wrapt from ...constants import ANALYTICS_SAMPLE_RATE_KEY @@ -12,6 +15,7 @@ from ...internal.compat import PYTHON_VERSION_INFO from ...internal.utils import ArgumentError from ...internal.utils import get_argument_value +from ...internal.utils.formats import asbool from ...internal.utils.formats import deep_getattr from ...pin import Pin from ..trace_utils import unwrap @@ -31,6 +35,22 @@ TRACED_ARGS = {"params", "path", "verb"} +if os.getenv("DD_AWS_TAG_ALL_PARAMS") is not None: + debtcollector.deprecate( + "Using environment variable 'DD_AWS_TAG_ALL_PARAMS' is deprecated", + message="The aiobotocore integration no longer includes all API parameters by default.", + removal_version="2.0.0", + ) + +config._add( + "aiobotocore", + { + "tag_no_params": asbool(os.getenv("DD_AWS_TAG_NO_PARAMS", default=False)), + "tag_all_params": asbool(os.getenv("DD_AWS_TAG_ALL_PARAMS", default=False)), + }, +) + + def patch(): if getattr(aiobotocore.client, "_datadog_patch", False): return @@ -94,13 +114,20 @@ async def _wrapped_api_call(original_func, instance, args, kwargs): span.set_tag(SPAN_MEASURED_KEY) try: + operation = get_argument_value(args, kwargs, 0, "operation_name") + params = get_argument_value(args, kwargs, 1, "params") + span.resource = "{}.{}".format(endpoint_name, operation.lower()) + + if params and not config.aiobotocore["tag_no_params"]: + aws._add_api_param_span_tags(span, endpoint_name, params) except ArgumentError: operation = None span.resource = endpoint_name - aws.add_span_arg_tags(span, endpoint_name, args, ARGS_NAME, TRACED_ARGS) + if not config.aiobotocore["tag_no_params"] and config.aiobotocore["tag_all_params"]: + aws.add_span_arg_tags(span, endpoint_name, args, ARGS_NAME, TRACED_ARGS) region_name = deep_getattr(instance, "meta.region_name") diff --git a/ddtrace/contrib/boto/__init__.py b/ddtrace/contrib/boto/__init__.py index 1615b0f6cbf..dc760ab8e8d 100644 --- a/ddtrace/contrib/boto/__init__.py +++ b/ddtrace/contrib/boto/__init__.py @@ -1,17 +1,47 @@ """ Boto integration will trace all AWS calls made via boto2. -This integration is automatically patched when using ``patch_all()``:: - import boto.ec2 - from ddtrace import patch +Enabling +~~~~~~~~ + +The boto integration is enabled automatically when using +:ref:`ddtrace-run` or :func:`patch_all()`. - # If not patched yet, you can patch boto specifically +Or use :func:`patch()` to manually enable the integration:: + + from ddtrace import patch patch(boto=True) - # This will report spans with the default instrumentation - ec2 = boto.ec2.connect_to_region("us-west-2") - # Example of instrumented query - ec2.get_all_instances() +Configuration +~~~~~~~~~~~~~ + +.. py:data:: ddtrace.config.boto['tag_no_params'] + + This opts out of the default behavior of collecting a narrow set of API + parameters as span tags. + + To not collect any API parameters, ``ddtrace.config.boto.tag_no_params = + True`` or by setting the environment variable ``DD_AWS_TAG_NO_PARAMS=true``. + + + Default: ``False`` + +.. py:data:: ddtrace.config.boto['tag_all_params'] + + **Deprecated**: This retains the deprecated behavior of adding span tags for + all API parameters that are not explicitly excluded by the integration. + These deprecated span tags will be added along with the API parameters + enabled by default. + + This configuration is ignored if ``tag_no_parms`` (``DD_AWS_TAG_NO_PARAMS``) + is set to ``True``. + + To collect all API parameters, ``ddtrace.config.botocore.tag_all_params = + True`` or by setting the environment variable ``DD_AWS_TAG_ALL_PARAMS=true``. + + + Default: ``False`` + """ from ...internal.utils.importlib import require_modules diff --git a/ddtrace/contrib/boto/patch.py b/ddtrace/contrib/boto/patch.py index baf602f23b3..a2d9aa5919c 100644 --- a/ddtrace/contrib/boto/patch.py +++ b/ddtrace/contrib/boto/patch.py @@ -1,4 +1,5 @@ import inspect +import os import boto.connection @@ -10,9 +11,11 @@ from ddtrace.ext import http from ddtrace.internal.utils.wrappers import unwrap from ddtrace.pin import Pin +from ddtrace.vendor import debtcollector from ddtrace.vendor import wrapt from ...internal.utils import get_argument_value +from ...internal.utils.formats import asbool # Original boto client class @@ -32,6 +35,22 @@ AWS_AUTH_TRACED_ARGS = {"path", "data", "host"} +if os.getenv("DD_AWS_TAG_ALL_PARAMS") is not None: + debtcollector.deprecate( + "Using environment variable 'DD_AWS_TAG_ALL_PARAMS' is deprecated", + message="The boto integration no longer includes all API parameters by default.", + removal_version="2.0.0", + ) + +config._add( + "boto", + { + "tag_no_params": asbool(os.getenv("DD_AWS_TAG_NO_PARAMS", default=False)), + "tag_all_params": asbool(os.getenv("DD_AWS_TAG_ALL_PARAMS", default=False)), + }, +) + + def patch(): if getattr(boto.connection, "_datadog_patch", False): return @@ -72,11 +91,17 @@ def patched_query_request(original_func, instance, args, kwargs): operation_name = None if args: operation_name = get_argument_value(args, kwargs, 0, "action") + params = get_argument_value(args, kwargs, 1, "params") + span.resource = "%s.%s" % (endpoint_name, operation_name.lower()) + + if params and not config.boto["tag_no_params"]: + aws._add_api_param_span_tags(span, endpoint_name, params) else: span.resource = endpoint_name - aws.add_span_arg_tags(span, endpoint_name, args, AWS_QUERY_ARGS_NAME, AWS_QUERY_TRACED_ARGS) + if not config.boto["tag_no_params"] and config.boto["tag_all_params"]: + aws.add_span_arg_tags(span, endpoint_name, args, AWS_QUERY_ARGS_NAME, AWS_QUERY_TRACED_ARGS) # Obtaining region name region_name = _get_instance_region_name(instance) @@ -140,7 +165,8 @@ def patched_auth_request(original_func, instance, args, kwargs): else: span.resource = endpoint_name - aws.add_span_arg_tags(span, endpoint_name, args, AWS_AUTH_ARGS_NAME, AWS_AUTH_TRACED_ARGS) + if not config.boto["tag_no_params"] and config.boto["tag_all_params"]: + aws.add_span_arg_tags(span, endpoint_name, args, AWS_AUTH_ARGS_NAME, AWS_AUTH_TRACED_ARGS) # Obtaining region name region_name = _get_instance_region_name(instance) diff --git a/ddtrace/contrib/botocore/__init__.py b/ddtrace/contrib/botocore/__init__.py index 8c0d91eb349..36dce206fc5 100644 --- a/ddtrace/contrib/botocore/__init__.py +++ b/ddtrace/contrib/botocore/__init__.py @@ -52,6 +52,32 @@ See :ref:`HTTP - Custom Error Codes` documentation for more examples. +.. py:data:: ddtrace.config.botocore['tag_no_params'] + + This opts out of the default behavior of collecting a narrow set of API parameters as span tags. + + To not collect any API parameters, ``ddtrace.config.botocore.tag_no_params = True`` or by setting the environment + variable ``DD_AWS_TAG_NO_PARAMS=true``. + + + Default: ``False`` + +.. py:data:: ddtrace.config.botocore['tag_all_params'] + + **Deprecated**: This retains the deprecated behavior of adding span tags for + all API parameters that are not explicitly excluded by the integration. + These deprecated span tags will be added along with the API parameters + enabled by default. + + This configuration is ignored if ``tag_no_parms`` (``DD_AWS_TAG_NO_PARAMS``) + is set to ``True``. + + To collect all API parameters, ``ddtrace.config.botocore.tag_all_params = + True`` or by setting the environment variable ``DD_AWS_TAG_ALL_PARAMS=true``. + + + Default: ``False`` + Example:: diff --git a/ddtrace/contrib/botocore/patch.py b/ddtrace/contrib/botocore/patch.py index 7f879cef582..d0074db0875 100644 --- a/ddtrace/contrib/botocore/patch.py +++ b/ddtrace/contrib/botocore/patch.py @@ -16,6 +16,7 @@ from ddtrace import config from ddtrace.settings.config import Config +from ddtrace.vendor import debtcollector from ddtrace.vendor import wrapt from ...constants import ANALYTICS_SAMPLE_RATE_KEY @@ -47,6 +48,14 @@ log = get_logger(__name__) +if os.getenv("DD_AWS_TAG_ALL_PARAMS") is not None: + debtcollector.deprecate( + "Using environment variable 'DD_AWS_TAG_ALL_PARAMS' is deprecated", + message="The botocore integration no longer includes all API parameters by default.", + removal_version="2.0.0", + ) + + # Botocore default settings config._add( "botocore", @@ -54,6 +63,8 @@ "distributed_tracing": asbool(os.getenv("DD_BOTOCORE_DISTRIBUTED_TRACING", default=True)), "invoke_with_legacy_context": asbool(os.getenv("DD_BOTOCORE_INVOKE_WITH_LEGACY_CONTEXT", default=False)), "operations": collections.defaultdict(Config._HTTPServerConfig), + "tag_no_params": asbool(os.getenv("DD_AWS_TAG_NO_PARAMS", default=False)), + "tag_all_params": asbool(os.getenv("DD_AWS_TAG_ALL_PARAMS", default=False)), }, ) @@ -325,10 +336,14 @@ def patched_api_call(original_func, instance, args, kwargs): except Exception: log.warning("Unable to inject trace context", exc_info=True) + if params and not config.botocore["tag_no_params"]: + aws._add_api_param_span_tags(span, endpoint_name, params) + else: span.resource = endpoint_name - aws.add_span_arg_tags(span, endpoint_name, args, ARGS_NAME, TRACED_ARGS) + if not config.botocore["tag_no_params"] and config.botocore["tag_all_params"]: + aws.add_span_arg_tags(span, endpoint_name, args, ARGS_NAME, TRACED_ARGS) region_name = deep_getattr(instance, "meta.region_name") diff --git a/ddtrace/ext/aws.py b/ddtrace/ext/aws.py index ac93cdf6e97..1968fcf7395 100644 --- a/ddtrace/ext/aws.py +++ b/ddtrace/ext/aws.py @@ -1,4 +1,5 @@ from typing import Any +from typing import Dict from typing import FrozenSet from typing import Set from typing import TYPE_CHECKING @@ -47,6 +48,38 @@ def add_span_arg_tags( ) +def _add_api_param_span_tags(span, endpoint_name, params): + # type: (Span, str, Dict[str, Any]) -> None + if endpoint_name == "cloudwatch": + log_group_name = params.get("logGroupName") + if log_group_name: + span.set_tag_str("aws.cloudwatch.logs.log_group_name", log_group_name) + elif endpoint_name == "dynamodb": + table_name = params.get("TableName") + if table_name: + span.set_tag_str("aws.dynamodb.table_name", table_name) + elif endpoint_name == "kinesis": + stream_name = params.get("StreamName") + if stream_name: + span.set_tag_str("aws.kinesis.stream_name", stream_name) + elif endpoint_name == "redshift": + cluster_identifier = params.get("ClusterIdentifier") + if cluster_identifier: + span.set_tag_str("aws.redshift.cluster_identifier", cluster_identifier) + elif endpoint_name == "s3": + bucket_name = params.get("Bucket") + if bucket_name: + span.set_tag_str("aws.s3.bucket_name", bucket_name) + elif endpoint_name == "sns": + topic_arn = params.get("TopicArn") + if topic_arn: + span.set_tag_str("aws.sns.topic_arn", topic_arn) + elif endpoint_name == "sqs": + queue_name = params.get("QueueName") or params.get("QueueUrl") + if queue_name: + span.set_tag_str("aws.sqs.queue_name", queue_name) + + REGION = "aws.region" AGENT = "aws.agent" OPERATION = "aws.operation" diff --git a/docs/spelling_wordlist.txt b/docs/spelling_wordlist.txt index 1f28358e13f..6a146ad9368 100644 --- a/docs/spelling_wordlist.txt +++ b/docs/spelling_wordlist.txt @@ -32,6 +32,7 @@ asyncpg attrs autodetected autopatching +aws backend backends backport diff --git a/releasenotes/notes/fix-aws-tag-params-0e1488513a0ae5c7.yaml b/releasenotes/notes/fix-aws-tag-params-0e1488513a0ae5c7.yaml new file mode 100755 index 00000000000..c4027b7ad94 --- /dev/null +++ b/releasenotes/notes/fix-aws-tag-params-0e1488513a0ae5c7.yaml @@ -0,0 +1,7 @@ +--- +deprecations: + - | + aws: The boto, botocore and aiobotocore integrations no longer include all API parameters by default. To retain the deprecated behavior, set the environment variable ``DD_AWS_TAG_ALL_PARAMS=1``. The deprecated behavior and environment variable will be removed in v2.0.0. +fixes: + - | + aws: We are reducing the number of API parameters that the boto, botocore and aiobotocore integrations collect as span tags by default. This change limits span tags to a narrow set of parameters for specific AWS APIs using standard tag names. To opt out of the new default behavior and collect no API parameters, set the environment variable ``DD_AWS_TAG_NO_PARAMS=1``. To retain the deprecated behavior and collect all API parameters, set the environment variable ``DD_AWS_TAG_ALL_PARAMS=1``. diff --git a/tests/contrib/aiobotocore/test.py b/tests/contrib/aiobotocore/test.py index fba033c78c2..36b958d14d1 100644 --- a/tests/contrib/aiobotocore/test.py +++ b/tests/contrib/aiobotocore/test.py @@ -6,7 +6,6 @@ from ddtrace.constants import ERROR_MSG from ddtrace.contrib.aiobotocore.patch import patch from ddtrace.contrib.aiobotocore.patch import unpatch -from ddtrace.internal.compat import stringify from tests.utils import assert_is_measured from tests.utils import assert_span_http_status_code from tests.utils import override_config @@ -76,8 +75,7 @@ async def test_s3_client(tracer): assert span.name == "s3.command" -@pytest.mark.asyncio -async def test_s3_put(tracer): +async def _test_s3_put(tracer): params = dict(Key="foo", Bucket="mybucket", Body=b"bar") async with aiobotocore_client("s3", tracer) as s3: @@ -97,9 +95,33 @@ async def test_s3_put(tracer): assert_is_measured(spans[1]) assert spans[1].get_tag("aws.operation") == "PutObject" assert spans[1].resource == "s3.putobject" - assert spans[1].get_tag("params.Key") == stringify(params["Key"]) - assert spans[1].get_tag("params.Bucket") == stringify(params["Bucket"]) - assert spans[1].get_tag("params.Body") is None + + return spans[1] + + +@pytest.mark.asyncio +async def test_s3_put(tracer): + span = await _test_s3_put(tracer) + assert span.get_tag("aws.s3.bucket_name") == "mybucket" + + +@pytest.mark.asyncio +async def test_s3_put_no_params(tracer): + with override_config("aiobotocore", dict(tag_no_params=True)): + span = await _test_s3_put(tracer) + assert span.get_tag("aws.s3.bucket_name") is None + assert span.get_tag("params.Key") is None + assert span.get_tag("params.Bucket") is None + assert span.get_tag("params.Body") is None + + +@pytest.mark.asyncio +async def test_s3_put_all_params(tracer): + with override_config("aiobotocore", dict(tag_all_params=True)): + span = await _test_s3_put(tracer) + assert span.get_tag("params.Key") == "foo" + assert span.get_tag("params.Bucket") == "mybucket" + assert span.get_tag("params.Body") is None @pytest.mark.asyncio diff --git a/tests/contrib/boto/test.py b/tests/contrib/boto/test.py index 6cd93451a91..15e338655e7 100644 --- a/tests/contrib/boto/test.py +++ b/tests/contrib/boto/test.py @@ -93,8 +93,8 @@ def test_analytics_enabled_without_rate(self): span = spans[0] self.assertEqual(span.get_metric(ANALYTICS_SAMPLE_RATE_KEY), 1.0) - @mock_s3 - def test_s3_client(self): + def _test_s3_client(self): + # DEV: To test tag params check create bucket's span s3 = boto.s3.connect_to_region("us-east-1") Pin(service=self.TEST_SERVICE, tracer=self.tracer).onto(s3) @@ -113,12 +113,11 @@ def test_s3_client(self): spans = self.pop_spans() assert spans self.assertEqual(len(spans), 1) - span = spans[0] - assert_is_measured(span) - assert_span_http_status_code(span, 200) - self.assertEqual(span.get_tag(http.METHOD), "PUT") - self.assertEqual(span.get_tag("path"), "/") - self.assertEqual(span.get_tag("aws.operation"), "create_bucket") + create_span = spans[0] + assert_is_measured(create_span) + assert_span_http_status_code(create_span, 200) + self.assertEqual(create_span.get_tag(http.METHOD), "PUT") + self.assertEqual(create_span.get_tag("aws.operation"), "create_bucket") # Get the created bucket s3.get_bucket("cheese") @@ -143,6 +142,34 @@ def test_s3_client(self): span = spans[0] self.assertEqual(span.resource, "s3.head") + return create_span + + @mock_s3 + def test_s3_client(self): + span = self._test_s3_client() + # DEV: Not currently supported + self.assertIsNone(span.get_tag("aws.s3.bucket_name")) + + @mock_s3 + def test_s3_client_no_params(self): + with self.override_config("boto", dict(tag_no_params=True)): + span = self._test_s3_client() + self.assertIsNone(span.get_tag("aws.s3.bucket_name")) + + @mock_s3 + def test_s3_client_all_params(self): + with self.override_config("boto", dict(tag_all_params=True)): + span = self._test_s3_client() + self.assertEqual(span.get_tag("path"), "/") + + @mock_s3 + def test_s3_client_no_params_all_params(self): + # DEV: Test no params overrides all params + with self.override_config("boto", dict(tag_no_params=True, tag_all_params=True)): + span = self._test_s3_client() + self.assertIsNone(span.get_tag("aws.s3.bucket_name")) + self.assertIsNone(span.get_tag("path")) + @mock_s3 def test_s3_put(self): s3 = boto.s3.connect_to_region("us-east-1") diff --git a/tests/contrib/botocore/test.py b/tests/contrib/botocore/test.py index fabc3b2e2e3..41fb67825ec 100644 --- a/tests/contrib/botocore/test.py +++ b/tests/contrib/botocore/test.py @@ -33,7 +33,6 @@ from ddtrace.contrib.botocore.patch import patch from ddtrace.contrib.botocore.patch import unpatch from ddtrace.internal.compat import PY2 -from ddtrace.internal.compat import stringify from ddtrace.internal.utils.version import parse_version from ddtrace.propagation.http import HTTP_HEADER_PARENT_ID from ddtrace.propagation.http import HTTP_HEADER_TRACE_ID @@ -204,8 +203,7 @@ def test_s3_head_404_as_errors(self): for t in (ERROR_MSG, ERROR_STACK, ERROR_TYPE): assert head_object.get_tag(t) is not None - @mock_s3 - def test_s3_put(self): + def _test_s3_put(self): s3 = self.session.create_client("s3", region_name="us-west-2") Pin(service=self.TEST_SERVICE, tracer=self.tracer).onto(s3) params = { @@ -229,63 +227,113 @@ def test_s3_put(self): assert span.resource == "s3.createbucket" assert spans[1].get_tag("aws.operation") == "PutObject" assert spans[1].resource == "s3.putobject" - assert spans[1].get_tag("params.Key") == stringify(params["Key"]) - assert spans[1].get_tag("params.Bucket") == stringify(params["Bucket"]) - # confirm blacklisted - assert spans[1].get_tag("params.Body") is None + return spans[1] - @mock_sqs - def test_sqs_client(self): + @mock_s3 + def test_s3_put(self): + span = self._test_s3_put() + assert span.get_tag("aws.s3.bucket_name") == "mybucket" + + @mock_s3 + def test_s3_put_no_params(self): + with self.override_config("botocore", dict(tag_no_params=True)): + span = self._test_s3_put() + assert span.get_tag("aws.s3.bucket_name") is None + assert span.get_tag("params.Key") is None + assert span.get_tag("params.Bucket") is None + assert span.get_tag("params.Body") is None + + @mock_s3 + def test_s3_put_all_params(self): + with self.override_config("botocore", dict(tag_all_params=True)): + span = self._test_s3_put() + assert span.get_tag("params.Key") == "foo" + assert span.get_tag("params.Bucket") == "mybucket" + # confirm blacklisted + assert span.get_tag("params.Body") is None + + @mock_s3 + def test_s3_put_no_params_all_params(self): + # DEV: Test no params overrides all params + with self.override_config("botocore", dict(tag_no_params=True, tag_all_params=True)): + span = self._test_s3_put() + assert span.get_tag("aws.s3.bucket_name") is None + assert span.get_tag("params.Key") is None + assert span.get_tag("params.Bucket") is None + assert span.get_tag("params.Body") is None + + def _test_sqs_client(self): sqs = self.session.create_client("sqs", region_name="us-east-1", endpoint_url="http://localhost:4566") Pin(service=self.TEST_SERVICE, tracer=self.tracer).onto(sqs) - sqs.list_queues() + sqs.create_queue(QueueName="test") spans = self.get_spans() assert spans span = spans[0] assert len(spans) == 1 assert span.get_tag("aws.region") == "us-east-1" - assert span.get_tag("aws.operation") == "ListQueues" - assert span.get_tag("params.MessageBody") is None + assert span.get_tag("aws.operation") == "CreateQueue" assert_is_measured(span) assert_span_http_status_code(span, 200) assert span.service == "test-botocore-tracing.sqs" - assert span.resource == "sqs.listqueues" + assert span.resource == "sqs.createqueue" + return span + + @mock_sqs + def test_sqs_client(self): + span = self._test_sqs_client() + assert span.get_tag("aws.sqs.queue_name") == "test" + + @mock_sqs + def test_sqs_client_no_params(self): + with self.override_config("botocore", dict(tag_no_params=True)): + span = self._test_sqs_client() + assert span.get_tag("aws.sqs.queue_name") is None + assert span.get_tag("params.MessageBody") is None + + @mock_sqs + def test_sqs_client_all_params(self): + with self.override_config("botocore", dict(tag_all_params=True)): + span = self._test_sqs_client() + assert span.get_tag("params.MessageBody") is None @mock_sqs def test_sqs_send_message_trace_injection_with_no_message_attributes(self): - sqs = self.session.create_client("sqs", region_name="us-east-1", endpoint_url="http://localhost:4566") - queue = sqs.create_queue(QueueName="test") - Pin(service=self.TEST_SERVICE, tracer=self.tracer).onto(sqs) + # DEV: Only test deprecated behavior because this inspect span tags for MessageAttributes + with self.override_config("botocore", dict(tag_all_params=True)): + sqs = self.session.create_client("sqs", region_name="us-east-1", endpoint_url="http://localhost:4566") + queue = sqs.create_queue(QueueName="test") + Pin(service=self.TEST_SERVICE, tracer=self.tracer).onto(sqs) - sqs.send_message(QueueUrl=queue["QueueUrl"], MessageBody="world") - spans = self.get_spans() - assert spans - span = spans[0] - assert len(spans) == 1 - assert span.get_tag("aws.region") == "us-east-1" - assert span.get_tag("aws.operation") == "SendMessage" - assert span.get_tag("params.MessageBody") is None - assert_is_measured(span) - assert_span_http_status_code(span, 200) - assert span.service == "test-botocore-tracing.sqs" - assert span.resource == "sqs.sendmessage" - trace_json = span.get_tag("params.MessageAttributes._datadog.StringValue") - trace_data_injected = json.loads(trace_json) - assert trace_data_injected[HTTP_HEADER_TRACE_ID] == str(span.trace_id) - assert trace_data_injected[HTTP_HEADER_PARENT_ID] == str(span.span_id) - response = sqs.receive_message(QueueUrl=queue["QueueUrl"], MessageAttributeNames=["_datadog"]) - assert len(response["Messages"]) == 1 - trace_json_message = response["Messages"][0]["MessageAttributes"]["_datadog"]["StringValue"] - sqs.delete_queue(QueueUrl=queue["QueueUrl"]) - trace_data_in_message = json.loads(trace_json_message) - assert trace_data_in_message[HTTP_HEADER_TRACE_ID] == str(span.trace_id) - assert trace_data_in_message[HTTP_HEADER_PARENT_ID] == str(span.span_id) + sqs.send_message(QueueUrl=queue["QueueUrl"], MessageBody="world") + spans = self.get_spans() + assert spans + span = spans[0] + assert len(spans) == 1 + assert span.get_tag("aws.region") == "us-east-1" + assert span.get_tag("aws.operation") == "SendMessage" + assert span.get_tag("params.MessageBody") is None + assert_is_measured(span) + assert_span_http_status_code(span, 200) + assert span.service == "test-botocore-tracing.sqs" + assert span.resource == "sqs.sendmessage" + trace_json = span.get_tag("params.MessageAttributes._datadog.StringValue") + trace_data_injected = json.loads(trace_json) + assert trace_data_injected[HTTP_HEADER_TRACE_ID] == str(span.trace_id) + assert trace_data_injected[HTTP_HEADER_PARENT_ID] == str(span.span_id) + response = sqs.receive_message(QueueUrl=queue["QueueUrl"], MessageAttributeNames=["_datadog"]) + assert len(response["Messages"]) == 1 + trace_json_message = response["Messages"][0]["MessageAttributes"]["_datadog"]["StringValue"] + sqs.delete_queue(QueueUrl=queue["QueueUrl"]) + trace_data_in_message = json.loads(trace_json_message) + assert trace_data_in_message[HTTP_HEADER_TRACE_ID] == str(span.trace_id) + assert trace_data_in_message[HTTP_HEADER_PARENT_ID] == str(span.span_id) @mock_sqs def test_sqs_send_message_distributed_tracing_off(self): - with self.override_config("botocore", dict(distributed_tracing=False)): + # DEV: Only test deprecated behavior because this inspect span tags for MessageAttributes + with self.override_config("botocore", dict(distributed_tracing=False, tag_all_params=True)): sqs = self.session.create_client("sqs", region_name="us-east-1", endpoint_url="http://localhost:4566") queue = sqs.create_queue(QueueName="test") Pin(service=self.TEST_SERVICE, tracer=self.tracer).onto(sqs) @@ -312,154 +360,162 @@ def test_sqs_send_message_distributed_tracing_off(self): @mock_sqs def test_sqs_send_message_trace_injection_with_message_attributes(self): - sqs = self.session.create_client("sqs", region_name="us-east-1", endpoint_url="http://localhost:4566") - queue = sqs.create_queue(QueueName="test") - Pin(service=self.TEST_SERVICE, tracer=self.tracer).onto(sqs) - message_attributes = { - "one": {"DataType": "String", "StringValue": "one"}, - "two": {"DataType": "String", "StringValue": "two"}, - "three": {"DataType": "String", "StringValue": "three"}, - "four": {"DataType": "String", "StringValue": "four"}, - "five": {"DataType": "String", "StringValue": "five"}, - "six": {"DataType": "String", "StringValue": "six"}, - "seven": {"DataType": "String", "StringValue": "seven"}, - "eight": {"DataType": "String", "StringValue": "eight"}, - "nine": {"DataType": "String", "StringValue": "nine"}, - } - sqs.send_message(QueueUrl=queue["QueueUrl"], MessageBody="world", MessageAttributes=message_attributes) - spans = self.get_spans() - assert spans - span = spans[0] - assert len(spans) == 1 - assert span.get_tag("aws.region") == "us-east-1" - assert span.get_tag("aws.operation") == "SendMessage" - assert span.get_tag("params.MessageBody") is None - assert_is_measured(span) - assert_span_http_status_code(span, 200) - assert span.service == "test-botocore-tracing.sqs" - assert span.resource == "sqs.sendmessage" - trace_json = span.get_tag("params.MessageAttributes._datadog.StringValue") - trace_data_injected = json.loads(trace_json) - assert trace_data_injected[HTTP_HEADER_TRACE_ID] == str(span.trace_id) - assert trace_data_injected[HTTP_HEADER_PARENT_ID] == str(span.span_id) - response = sqs.receive_message(QueueUrl=queue["QueueUrl"], MessageAttributeNames=["_datadog"]) - assert len(response["Messages"]) == 1 - trace_json_message = response["Messages"][0]["MessageAttributes"]["_datadog"]["StringValue"] - trace_data_in_message = json.loads(trace_json_message) - assert trace_data_in_message[HTTP_HEADER_TRACE_ID] == str(span.trace_id) - assert trace_data_in_message[HTTP_HEADER_PARENT_ID] == str(span.span_id) - sqs.delete_queue(QueueUrl=queue["QueueUrl"]) + # DEV: Only test deprecated behavior because this inspect span tags for MessageAttributes + with self.override_config("botocore", dict(tag_all_params=True)): + sqs = self.session.create_client("sqs", region_name="us-east-1", endpoint_url="http://localhost:4566") + queue = sqs.create_queue(QueueName="test") + Pin(service=self.TEST_SERVICE, tracer=self.tracer).onto(sqs) + message_attributes = { + "one": {"DataType": "String", "StringValue": "one"}, + "two": {"DataType": "String", "StringValue": "two"}, + "three": {"DataType": "String", "StringValue": "three"}, + "four": {"DataType": "String", "StringValue": "four"}, + "five": {"DataType": "String", "StringValue": "five"}, + "six": {"DataType": "String", "StringValue": "six"}, + "seven": {"DataType": "String", "StringValue": "seven"}, + "eight": {"DataType": "String", "StringValue": "eight"}, + "nine": {"DataType": "String", "StringValue": "nine"}, + } + sqs.send_message(QueueUrl=queue["QueueUrl"], MessageBody="world", MessageAttributes=message_attributes) + spans = self.get_spans() + assert spans + span = spans[0] + assert len(spans) == 1 + assert span.get_tag("aws.region") == "us-east-1" + assert span.get_tag("aws.operation") == "SendMessage" + assert span.get_tag("params.MessageBody") is None + assert_is_measured(span) + assert_span_http_status_code(span, 200) + assert span.service == "test-botocore-tracing.sqs" + assert span.resource == "sqs.sendmessage" + trace_json = span.get_tag("params.MessageAttributes._datadog.StringValue") + trace_data_injected = json.loads(trace_json) + assert trace_data_injected[HTTP_HEADER_TRACE_ID] == str(span.trace_id) + assert trace_data_injected[HTTP_HEADER_PARENT_ID] == str(span.span_id) + response = sqs.receive_message(QueueUrl=queue["QueueUrl"], MessageAttributeNames=["_datadog"]) + assert len(response["Messages"]) == 1 + trace_json_message = response["Messages"][0]["MessageAttributes"]["_datadog"]["StringValue"] + trace_data_in_message = json.loads(trace_json_message) + assert trace_data_in_message[HTTP_HEADER_TRACE_ID] == str(span.trace_id) + assert trace_data_in_message[HTTP_HEADER_PARENT_ID] == str(span.span_id) + sqs.delete_queue(QueueUrl=queue["QueueUrl"]) @mock_sqs def test_sqs_send_message_trace_injection_with_max_message_attributes(self): - sqs = self.session.create_client("sqs", region_name="us-east-1", endpoint_url="http://localhost:4566") - queue = sqs.create_queue(QueueName="test") - Pin(service=self.TEST_SERVICE, tracer=self.tracer).onto(sqs) - message_attributes = { - "one": {"DataType": "String", "StringValue": "one"}, - "two": {"DataType": "String", "StringValue": "two"}, - "three": {"DataType": "String", "StringValue": "three"}, - "four": {"DataType": "String", "StringValue": "four"}, - "five": {"DataType": "String", "StringValue": "five"}, - "six": {"DataType": "String", "StringValue": "six"}, - "seven": {"DataType": "String", "StringValue": "seven"}, - "eight": {"DataType": "String", "StringValue": "eight"}, - "nine": {"DataType": "String", "StringValue": "nine"}, - "ten": {"DataType": "String", "StringValue": "ten"}, - } - sqs.send_message(QueueUrl=queue["QueueUrl"], MessageBody="world", MessageAttributes=message_attributes) - spans = self.get_spans() - assert spans - span = spans[0] - assert len(spans) == 1 - assert span.get_tag("aws.region") == "us-east-1" - assert span.get_tag("aws.operation") == "SendMessage" - assert span.get_tag("params.MessageBody") is None - assert_is_measured(span) - assert_span_http_status_code(span, 200) - assert span.service == "test-botocore-tracing.sqs" - assert span.resource == "sqs.sendmessage" - trace_json = span.get_tag("params.MessageAttributes._datadog.StringValue") - assert trace_json is None - response = sqs.receive_message(QueueUrl=queue["QueueUrl"], MessageAttributeNames=["_datadog"]) - assert len(response["Messages"]) == 1 - trace_in_message = "MessageAttributes" in response["Messages"][0] - assert trace_in_message is False - sqs.delete_queue(QueueUrl=queue["QueueUrl"]) + # DEV: Only test deprecated behavior where MessageBody would be excluded + with self.override_config("botocore", dict(tag_all_params=True)): + sqs = self.session.create_client("sqs", region_name="us-east-1", endpoint_url="http://localhost:4566") + queue = sqs.create_queue(QueueName="test") + Pin(service=self.TEST_SERVICE, tracer=self.tracer).onto(sqs) + message_attributes = { + "one": {"DataType": "String", "StringValue": "one"}, + "two": {"DataType": "String", "StringValue": "two"}, + "three": {"DataType": "String", "StringValue": "three"}, + "four": {"DataType": "String", "StringValue": "four"}, + "five": {"DataType": "String", "StringValue": "five"}, + "six": {"DataType": "String", "StringValue": "six"}, + "seven": {"DataType": "String", "StringValue": "seven"}, + "eight": {"DataType": "String", "StringValue": "eight"}, + "nine": {"DataType": "String", "StringValue": "nine"}, + "ten": {"DataType": "String", "StringValue": "ten"}, + } + sqs.send_message(QueueUrl=queue["QueueUrl"], MessageBody="world", MessageAttributes=message_attributes) + spans = self.get_spans() + assert spans + span = spans[0] + assert len(spans) == 1 + assert span.get_tag("aws.region") == "us-east-1" + assert span.get_tag("aws.operation") == "SendMessage" + assert span.get_tag("params.MessageBody") is None + assert_is_measured(span) + assert_span_http_status_code(span, 200) + assert span.service == "test-botocore-tracing.sqs" + assert span.resource == "sqs.sendmessage" + trace_json = span.get_tag("params.MessageAttributes._datadog.StringValue") + assert trace_json is None + response = sqs.receive_message(QueueUrl=queue["QueueUrl"], MessageAttributeNames=["_datadog"]) + assert len(response["Messages"]) == 1 + trace_in_message = "MessageAttributes" in response["Messages"][0] + assert trace_in_message is False + sqs.delete_queue(QueueUrl=queue["QueueUrl"]) @mock_sqs def test_sqs_send_message_batch_trace_injection_with_no_message_attributes(self): - sqs = self.session.create_client("sqs", region_name="us-east-1", endpoint_url="http://localhost:4566") - queue = sqs.create_queue(QueueName="test") - Pin(service=self.TEST_SERVICE, tracer=self.tracer).onto(sqs) - entries = [ - { - "Id": "1", - "MessageBody": "ironmaiden", - } - ] - sqs.send_message_batch(QueueUrl=queue["QueueUrl"], Entries=entries) - spans = self.get_spans() - assert spans - span = spans[0] - assert len(spans) == 1 - assert span.get_tag("aws.region") == "us-east-1" - assert span.get_tag("aws.operation") == "SendMessageBatch" - assert span.get_tag("params.MessageBody") is None - assert_is_measured(span) - assert_span_http_status_code(span, 200) - assert span.service == "test-botocore-tracing.sqs" - assert span.resource == "sqs.sendmessagebatch" - response = sqs.receive_message(QueueUrl=queue["QueueUrl"], MessageAttributeNames=["_datadog"]) - assert len(response["Messages"]) == 1 - trace_json_message = response["Messages"][0]["MessageAttributes"]["_datadog"]["StringValue"] - trace_data_in_message = json.loads(trace_json_message) - assert trace_data_in_message[HTTP_HEADER_TRACE_ID] == str(span.trace_id) - assert trace_data_in_message[HTTP_HEADER_PARENT_ID] == str(span.span_id) - sqs.delete_queue(QueueUrl=queue["QueueUrl"]) + # DEV: Only test deprecated behavior where MessageBody would be excluded + with self.override_config("botocore", dict(tag_all_params=True)): + sqs = self.session.create_client("sqs", region_name="us-east-1", endpoint_url="http://localhost:4566") + queue = sqs.create_queue(QueueName="test") + Pin(service=self.TEST_SERVICE, tracer=self.tracer).onto(sqs) + entries = [ + { + "Id": "1", + "MessageBody": "ironmaiden", + } + ] + sqs.send_message_batch(QueueUrl=queue["QueueUrl"], Entries=entries) + spans = self.get_spans() + assert spans + span = spans[0] + assert len(spans) == 1 + assert span.get_tag("aws.region") == "us-east-1" + assert span.get_tag("aws.operation") == "SendMessageBatch" + assert span.get_tag("params.MessageBody") is None + assert_is_measured(span) + assert_span_http_status_code(span, 200) + assert span.service == "test-botocore-tracing.sqs" + assert span.resource == "sqs.sendmessagebatch" + response = sqs.receive_message(QueueUrl=queue["QueueUrl"], MessageAttributeNames=["_datadog"]) + assert len(response["Messages"]) == 1 + trace_json_message = response["Messages"][0]["MessageAttributes"]["_datadog"]["StringValue"] + trace_data_in_message = json.loads(trace_json_message) + assert trace_data_in_message[HTTP_HEADER_TRACE_ID] == str(span.trace_id) + assert trace_data_in_message[HTTP_HEADER_PARENT_ID] == str(span.span_id) + sqs.delete_queue(QueueUrl=queue["QueueUrl"]) @mock_sqs def test_sqs_send_message_batch_trace_injection_with_message_attributes(self): - sqs = self.session.create_client("sqs", region_name="us-east-1", endpoint_url="http://localhost:4566") - queue = sqs.create_queue(QueueName="test") - Pin(service=self.TEST_SERVICE, tracer=self.tracer).onto(sqs) - entries = [ - { - "Id": "1", - "MessageBody": "ironmaiden", - "MessageAttributes": { - "one": {"DataType": "String", "StringValue": "one"}, - "two": {"DataType": "String", "StringValue": "two"}, - "three": {"DataType": "String", "StringValue": "three"}, - "four": {"DataType": "String", "StringValue": "four"}, - "five": {"DataType": "String", "StringValue": "five"}, - "six": {"DataType": "String", "StringValue": "six"}, - "seven": {"DataType": "String", "StringValue": "seven"}, - "eight": {"DataType": "String", "StringValue": "eight"}, - "nine": {"DataType": "String", "StringValue": "nine"}, - }, - } - ] - - sqs.send_message_batch(QueueUrl=queue["QueueUrl"], Entries=entries) - spans = self.get_spans() - assert spans - span = spans[0] - assert len(spans) == 1 - assert span.get_tag("aws.region") == "us-east-1" - assert span.get_tag("aws.operation") == "SendMessageBatch" - assert span.get_tag("params.MessageBody") is None - assert_is_measured(span) - assert_span_http_status_code(span, 200) - assert span.service == "test-botocore-tracing.sqs" - assert span.resource == "sqs.sendmessagebatch" - response = sqs.receive_message(QueueUrl=queue["QueueUrl"], MessageAttributeNames=["_datadog"]) - assert len(response["Messages"]) == 1 - trace_json_message = response["Messages"][0]["MessageAttributes"]["_datadog"]["StringValue"] - trace_data_in_message = json.loads(trace_json_message) - assert trace_data_in_message[HTTP_HEADER_TRACE_ID] == str(span.trace_id) - assert trace_data_in_message[HTTP_HEADER_PARENT_ID] == str(span.span_id) - sqs.delete_queue(QueueUrl=queue["QueueUrl"]) + # DEV: Only test deprecated behavior where MessageBody would be excluded + with self.override_config("botocore", dict(tag_all_params=True)): + sqs = self.session.create_client("sqs", region_name="us-east-1", endpoint_url="http://localhost:4566") + queue = sqs.create_queue(QueueName="test") + Pin(service=self.TEST_SERVICE, tracer=self.tracer).onto(sqs) + entries = [ + { + "Id": "1", + "MessageBody": "ironmaiden", + "MessageAttributes": { + "one": {"DataType": "String", "StringValue": "one"}, + "two": {"DataType": "String", "StringValue": "two"}, + "three": {"DataType": "String", "StringValue": "three"}, + "four": {"DataType": "String", "StringValue": "four"}, + "five": {"DataType": "String", "StringValue": "five"}, + "six": {"DataType": "String", "StringValue": "six"}, + "seven": {"DataType": "String", "StringValue": "seven"}, + "eight": {"DataType": "String", "StringValue": "eight"}, + "nine": {"DataType": "String", "StringValue": "nine"}, + }, + } + ] + + sqs.send_message_batch(QueueUrl=queue["QueueUrl"], Entries=entries) + spans = self.get_spans() + assert spans + span = spans[0] + assert len(spans) == 1 + assert span.get_tag("aws.region") == "us-east-1" + assert span.get_tag("aws.operation") == "SendMessageBatch" + assert span.get_tag("params.MessageBody") is None + assert_is_measured(span) + assert_span_http_status_code(span, 200) + assert span.service == "test-botocore-tracing.sqs" + assert span.resource == "sqs.sendmessagebatch" + response = sqs.receive_message(QueueUrl=queue["QueueUrl"], MessageAttributeNames=["_datadog"]) + assert len(response["Messages"]) == 1 + trace_json_message = response["Messages"][0]["MessageAttributes"]["_datadog"]["StringValue"] + trace_data_in_message = json.loads(trace_json_message) + assert trace_data_in_message[HTTP_HEADER_TRACE_ID] == str(span.trace_id) + assert trace_data_in_message[HTTP_HEADER_PARENT_ID] == str(span.span_id) + sqs.delete_queue(QueueUrl=queue["QueueUrl"]) @mock_sqs def test_sqs_send_message_batch_trace_injection_with_max_message_attributes(self): @@ -503,23 +559,50 @@ def test_sqs_send_message_batch_trace_injection_with_max_message_attributes(self assert trace_in_message is False sqs.delete_queue(QueueUrl=queue["QueueUrl"]) - @mock_kinesis - def test_kinesis_client(self): - kinesis = self.session.create_client("kinesis", region_name="us-east-1") - Pin(service=self.TEST_SERVICE, tracer=self.tracer).onto(kinesis) + def _test_kinesis_client(self): + client = self.session.create_client("kinesis", region_name="us-east-1") + stream_name = "test" + client.create_stream(StreamName=stream_name, ShardCount=1) - kinesis.list_streams() + partition_key = "1234" + data = [ + {"Data": json.dumps({"Hello": "World"}), "PartitionKey": partition_key}, + {"Data": json.dumps({"foo": "bar"}), "PartitionKey": partition_key}, + ] + Pin(service=self.TEST_SERVICE, tracer=self.tracer).onto(client) + client.put_records(StreamName=stream_name, Records=data) spans = self.get_spans() assert spans span = spans[0] assert len(spans) == 1 assert span.get_tag("aws.region") == "us-east-1" - assert span.get_tag("aws.operation") == "ListStreams" + assert span.get_tag("aws.operation") == "PutRecords" assert_is_measured(span) assert_span_http_status_code(span, 200) assert span.service == "test-botocore-tracing.kinesis" - assert span.resource == "kinesis.liststreams" + assert span.resource == "kinesis.putrecords" + return span + + @mock_kinesis + def test_kinesis_client(self): + span = self._test_kinesis_client() + assert span.get_tag("aws.kinesis.stream_name") == "test" + + @mock_kinesis + def test_kinesis_client_no_params(self): + with self.override_config("botocore", dict(tag_no_params=True)): + span = self._test_kinesis_client() + assert span.get_tag("aws.kinesis.stream_name") is None + assert span.get_tag("params.Records") is None + + @mock_kinesis + def test_kinesis_client_all_params(self): + with self.override_config("botocore", dict(tag_no_params=True)): + span = self._test_kinesis_client() + assert span.get_tag("params.Records") is None + assert span.get_tag("params.Data") is None + assert span.get_tag("params.MessageBody") is None @mock_kinesis def test_unpatch(self): @@ -548,6 +631,7 @@ def test_double_patch(self): @mock_lambda def test_lambda_client(self): + # DEV: No lambda params tagged so we only check no ClientContext lamb = self.session.create_client("lambda", region_name="us-west-2") Pin(service=self.TEST_SERVICE, tracer=self.tracer).onto(lamb) @@ -563,53 +647,57 @@ def test_lambda_client(self): assert_span_http_status_code(span, 200) assert span.service == "test-botocore-tracing.lambda" assert span.resource == "lambda.listfunctions" + assert span.get_tag("params.ClientContext") is None @mock_lambda def test_lambda_invoke_no_context_client(self): - lamb = self.session.create_client("lambda", region_name="us-west-2", endpoint_url="http://localhost:4566") - lamb.create_function( - FunctionName="ironmaiden", - Runtime="python3.7", - Role="test-iam-role", - Handler="lambda_function.lambda_handler", - Code={ - "ZipFile": get_zip_lambda(), - }, - Publish=True, - Timeout=30, - MemorySize=128, - ) + # DEV: Test only deprecated behavior as we need to inspect span tags for ClientContext + with self.override_config("botocore", dict(tag_all_params=True)): + lamb = self.session.create_client("lambda", region_name="us-west-2", endpoint_url="http://localhost:4566") + lamb.create_function( + FunctionName="ironmaiden", + Runtime="python3.7", + Role="test-iam-role", + Handler="lambda_function.lambda_handler", + Code={ + "ZipFile": get_zip_lambda(), + }, + Publish=True, + Timeout=30, + MemorySize=128, + ) - Pin(service=self.TEST_SERVICE, tracer=self.tracer).onto(lamb) + Pin(service=self.TEST_SERVICE, tracer=self.tracer).onto(lamb) - lamb.invoke( - FunctionName="ironmaiden", - Payload=json.dumps({}), - ) + lamb.invoke( + FunctionName="ironmaiden", + Payload=json.dumps({}), + ) - spans = self.get_spans() - assert spans - span = spans[0] + spans = self.get_spans() + assert spans + span = spans[0] - assert len(spans) == 1 - assert span.get_tag("aws.region") == "us-west-2" - assert span.get_tag("aws.operation") == "Invoke" - assert_is_measured(span) - assert_span_http_status_code(span, 200) - assert span.service == "test-botocore-tracing.lambda" - assert span.resource == "lambda.invoke" - context_b64 = span.get_tag("params.ClientContext") - context_json = base64.b64decode(context_b64.encode()).decode() - context_obj = json.loads(context_json) + assert len(spans) == 1 + assert span.get_tag("aws.region") == "us-west-2" + assert span.get_tag("aws.operation") == "Invoke" + assert_is_measured(span) + assert_span_http_status_code(span, 200) + assert span.service == "test-botocore-tracing.lambda" + assert span.resource == "lambda.invoke" + context_b64 = span.get_tag("params.ClientContext") + context_json = base64.b64decode(context_b64.encode()).decode() + context_obj = json.loads(context_json) - assert context_obj["custom"][HTTP_HEADER_TRACE_ID] == str(span.trace_id) - assert context_obj["custom"][HTTP_HEADER_PARENT_ID] == str(span.span_id) + assert context_obj["custom"][HTTP_HEADER_TRACE_ID] == str(span.trace_id) + assert context_obj["custom"][HTTP_HEADER_PARENT_ID] == str(span.span_id) - lamb.delete_function(FunctionName="ironmaiden") + lamb.delete_function(FunctionName="ironmaiden") @mock_lambda def test_lambda_invoke_with_old_style_trace_propagation(self): - with self.override_config("botocore", dict(invoke_with_legacy_context=True)): + # DEV: Test only deprecated behavior as we need to inspect span tags for ClientContext + with self.override_config("botocore", dict(invoke_with_legacy_context=True, tag_all_params=True)): lamb = self.session.create_client("lambda", region_name="us-west-2", endpoint_url="http://localhost:4566") lamb.create_function( FunctionName="ironmaiden", @@ -652,11 +740,51 @@ def test_lambda_invoke_with_old_style_trace_propagation(self): lamb.delete_function(FunctionName="ironmaiden") @mock_lambda - def test_lambda_invoke_distributed_tracing_off(self): - with self.override_config("botocore", dict(distributed_tracing=False)): + def test_lambda_invoke_distributed_tracing_off(self): + # DEV: Test only deprecated behavior as we need to inspect span tags for ClientContext + with self.override_config("botocore", dict(distributed_tracing=False, tag_all_params=True)): + lamb = self.session.create_client("lambda", region_name="us-west-2", endpoint_url="http://localhost:4566") + lamb.create_function( + FunctionName="ironmaiden", + Runtime="python3.7", + Role="test-iam-role", + Handler="lambda_function.lambda_handler", + Code={ + "ZipFile": get_zip_lambda(), + }, + Publish=True, + Timeout=30, + MemorySize=128, + ) + + Pin(service=self.TEST_SERVICE, tracer=self.tracer).onto(lamb) + + lamb.invoke( + FunctionName="ironmaiden", + Payload=json.dumps({}), + ) + + spans = self.get_spans() + assert spans + span = spans[0] + + assert len(spans) == 1 + assert span.get_tag("aws.region") == "us-west-2" + assert span.get_tag("aws.operation") == "Invoke" + assert_is_measured(span) + assert_span_http_status_code(span, 200) + assert span.service == "test-botocore-tracing.lambda" + assert span.resource == "lambda.invoke" + assert span.get_tag("params.ClientContext") is None + lamb.delete_function(FunctionName="ironmaiden") + + @mock_lambda + def test_lambda_invoke_with_context_client(self): + # DEV: Test only deprecated behavior as we need to inspect span tags for ClientContext + with self.override_config("botocore", dict(tag_all_params=True)): lamb = self.session.create_client("lambda", region_name="us-west-2", endpoint_url="http://localhost:4566") lamb.create_function( - FunctionName="ironmaiden", + FunctionName="megadeth", Runtime="python3.7", Role="test-iam-role", Handler="lambda_function.lambda_handler", @@ -667,11 +795,13 @@ def test_lambda_invoke_distributed_tracing_off(self): Timeout=30, MemorySize=128, ) + client_context = base64.b64encode(json.dumps({"custom": {"foo": "bar"}}).encode()).decode() Pin(service=self.TEST_SERVICE, tracer=self.tracer).onto(lamb) lamb.invoke( - FunctionName="ironmaiden", + FunctionName="megadeth", + ClientContext=client_context, Payload=json.dumps({}), ) @@ -686,54 +816,15 @@ def test_lambda_invoke_distributed_tracing_off(self): assert_span_http_status_code(span, 200) assert span.service == "test-botocore-tracing.lambda" assert span.resource == "lambda.invoke" - assert span.get_tag("params.ClientContext") is None - lamb.delete_function(FunctionName="ironmaiden") - - @mock_lambda - def test_lambda_invoke_with_context_client(self): - lamb = self.session.create_client("lambda", region_name="us-west-2", endpoint_url="http://localhost:4566") - lamb.create_function( - FunctionName="megadeth", - Runtime="python3.7", - Role="test-iam-role", - Handler="lambda_function.lambda_handler", - Code={ - "ZipFile": get_zip_lambda(), - }, - Publish=True, - Timeout=30, - MemorySize=128, - ) - client_context = base64.b64encode(json.dumps({"custom": {"foo": "bar"}}).encode()).decode() - - Pin(service=self.TEST_SERVICE, tracer=self.tracer).onto(lamb) - - lamb.invoke( - FunctionName="megadeth", - ClientContext=client_context, - Payload=json.dumps({}), - ) - - spans = self.get_spans() - assert spans - span = spans[0] - - assert len(spans) == 1 - assert span.get_tag("aws.region") == "us-west-2" - assert span.get_tag("aws.operation") == "Invoke" - assert_is_measured(span) - assert_span_http_status_code(span, 200) - assert span.service == "test-botocore-tracing.lambda" - assert span.resource == "lambda.invoke" - context_b64 = span.get_tag("params.ClientContext") - context_json = base64.b64decode(context_b64.encode()).decode() - context_obj = json.loads(context_json) + context_b64 = span.get_tag("params.ClientContext") + context_json = base64.b64decode(context_b64.encode()).decode() + context_obj = json.loads(context_json) - assert context_obj["custom"]["foo"] == "bar" - assert context_obj["custom"][HTTP_HEADER_TRACE_ID] == str(span.trace_id) - assert context_obj["custom"][HTTP_HEADER_PARENT_ID] == str(span.span_id) + assert context_obj["custom"]["foo"] == "bar" + assert context_obj["custom"][HTTP_HEADER_TRACE_ID] == str(span.trace_id) + assert context_obj["custom"][HTTP_HEADER_PARENT_ID] == str(span.span_id) - lamb.delete_function(FunctionName="megadeth") + lamb.delete_function(FunctionName="megadeth") @mock_lambda def test_lambda_invoke_bad_context_client(self): @@ -770,152 +861,158 @@ def test_lambda_invoke_bad_context_client(self): @mock_events def test_eventbridge_single_entry_trace_injection(self): - bridge = self.session.create_client("events", region_name="us-east-1", endpoint_url="http://localhost:4566") - bridge.create_event_bus(Name="a-test-bus") - - entries = [ - { - "Source": "some-event-source", - "DetailType": "some-event-detail-type", - "Detail": json.dumps({"foo": "bar"}), - "EventBusName": "a-test-bus", - } - ] - bridge.put_rule( - Name="a-test-bus-rule", - EventBusName="a-test-bus", - EventPattern="""{"source": [{"prefix": ""}]}""", - State="ENABLED", - ) + # DEV: Only check deprecated all params behavior + with self.override_config("botocore", dict(tag_all_params=True)): + bridge = self.session.create_client("events", region_name="us-east-1", endpoint_url="http://localhost:4566") + bridge.create_event_bus(Name="a-test-bus") + + entries = [ + { + "Source": "some-event-source", + "DetailType": "some-event-detail-type", + "Detail": json.dumps({"foo": "bar"}), + "EventBusName": "a-test-bus", + } + ] + bridge.put_rule( + Name="a-test-bus-rule", + EventBusName="a-test-bus", + EventPattern="""{"source": [{"prefix": ""}]}""", + State="ENABLED", + ) - bridge.list_rules() - sqs = self.session.create_client("sqs", region_name="us-east-1", endpoint_url="http://localhost:4566") - queue = sqs.create_queue(QueueName="test") - queue_url = queue["QueueUrl"] - bridge.put_targets( - Rule="a-test-bus-rule", - Targets=[{"Id": "a-test-bus-rule-target", "Arn": "arn:aws:sqs:us-east-1:000000000000:test"}], - ) + bridge.list_rules() + sqs = self.session.create_client("sqs", region_name="us-east-1", endpoint_url="http://localhost:4566") + queue = sqs.create_queue(QueueName="test") + queue_url = queue["QueueUrl"] + bridge.put_targets( + Rule="a-test-bus-rule", + Targets=[{"Id": "a-test-bus-rule-target", "Arn": "arn:aws:sqs:us-east-1:000000000000:test"}], + ) - Pin(service=self.TEST_SERVICE, tracer=self.tracer).onto(bridge) - bridge.put_events(Entries=entries) + Pin(service=self.TEST_SERVICE, tracer=self.tracer).onto(bridge) + bridge.put_events(Entries=entries) - messages = sqs.receive_message(QueueUrl=queue_url, WaitTimeSeconds=2) + messages = sqs.receive_message(QueueUrl=queue_url, WaitTimeSeconds=2) - bridge.delete_event_bus(Name="a-test-bus") - sqs.delete_queue(QueueUrl=queue["QueueUrl"]) + bridge.delete_event_bus(Name="a-test-bus") + sqs.delete_queue(QueueUrl=queue["QueueUrl"]) - spans = self.get_spans() - assert spans - assert len(spans) == 2 - span = spans[0] - str_entries = span.get_tag("params.Entries") - assert str_entries is None - - message = messages["Messages"][0] - body = message.get("Body") - assert body is not None - # body_obj = ast.literal_eval(body) - body_obj = json.loads(body) - detail = body_obj.get("detail") - headers = detail.get("_datadog") - assert headers is not None - assert headers[HTTP_HEADER_TRACE_ID] == str(span.trace_id) - assert headers[HTTP_HEADER_PARENT_ID] == str(span.span_id) + spans = self.get_spans() + assert spans + assert len(spans) == 2 + span = spans[0] + str_entries = span.get_tag("params.Entries") + assert str_entries is None + + message = messages["Messages"][0] + body = message.get("Body") + assert body is not None + # body_obj = ast.literal_eval(body) + body_obj = json.loads(body) + detail = body_obj.get("detail") + headers = detail.get("_datadog") + assert headers is not None + assert headers[HTTP_HEADER_TRACE_ID] == str(span.trace_id) + assert headers[HTTP_HEADER_PARENT_ID] == str(span.span_id) @mock_events def test_eventbridge_muliple_entries_trace_injection(self): - bridge = self.session.create_client("events", region_name="us-east-1", endpoint_url="http://localhost:4566") - bridge.create_event_bus(Name="a-test-bus") - - entries = [ - { - "Source": "another-event-source", - "DetailType": "a-different-event-detail-type", - "Detail": json.dumps({"abc": "xyz"}), - "EventBusName": "a-test-bus", - }, - { - "Source": "some-event-source", - "DetailType": "some-event-detail-type", - "Detail": json.dumps({"foo": "bar"}), - "EventBusName": "a-test-bus", - }, - ] - bridge.put_rule( - Name="a-test-bus-rule", - EventBusName="a-test-bus", - EventPattern="""{"source": [{"prefix": ""}]}""", - State="ENABLED", - ) - - bridge.list_rules() - sqs = self.session.create_client("sqs", region_name="us-east-1", endpoint_url="http://localhost:4566") - queue = sqs.create_queue(QueueName="test") - queue_url = queue["QueueUrl"] - bridge.put_targets( - Rule="a-test-bus-rule", - Targets=[{"Id": "a-test-bus-rule-target", "Arn": "arn:aws:sqs:us-east-1:000000000000:test"}], - ) - - Pin(service=self.TEST_SERVICE, tracer=self.tracer).onto(bridge) - bridge.put_events(Entries=entries) + # DEV: Only check deprecated all params behavior + with self.override_config("botocore", dict(tag_all_params=True)): + bridge = self.session.create_client("events", region_name="us-east-1", endpoint_url="http://localhost:4566") + bridge.create_event_bus(Name="a-test-bus") + + entries = [ + { + "Source": "another-event-source", + "DetailType": "a-different-event-detail-type", + "Detail": json.dumps({"abc": "xyz"}), + "EventBusName": "a-test-bus", + }, + { + "Source": "some-event-source", + "DetailType": "some-event-detail-type", + "Detail": json.dumps({"foo": "bar"}), + "EventBusName": "a-test-bus", + }, + ] + bridge.put_rule( + Name="a-test-bus-rule", + EventBusName="a-test-bus", + EventPattern="""{"source": [{"prefix": ""}]}""", + State="ENABLED", + ) - messages = sqs.receive_message(QueueUrl=queue_url, WaitTimeSeconds=2) + bridge.list_rules() + sqs = self.session.create_client("sqs", region_name="us-east-1", endpoint_url="http://localhost:4566") + queue = sqs.create_queue(QueueName="test") + queue_url = queue["QueueUrl"] + bridge.put_targets( + Rule="a-test-bus-rule", + Targets=[{"Id": "a-test-bus-rule-target", "Arn": "arn:aws:sqs:us-east-1:000000000000:test"}], + ) - bridge.delete_event_bus(Name="a-test-bus") - sqs.delete_queue(QueueUrl=queue["QueueUrl"]) + Pin(service=self.TEST_SERVICE, tracer=self.tracer).onto(bridge) + bridge.put_events(Entries=entries) - spans = self.get_spans() - assert spans - assert len(spans) == 2 - span = spans[0] - str_entries = span.get_tag("params.Entries") - assert str_entries is None - - message = messages["Messages"][0] - body = message.get("Body") - assert body is not None - body_obj = json.loads(body) - detail = body_obj.get("detail") - headers = detail.get("_datadog") - assert headers is not None - assert headers[HTTP_HEADER_TRACE_ID] == str(span.trace_id) - assert headers[HTTP_HEADER_PARENT_ID] == str(span.span_id) + messages = sqs.receive_message(QueueUrl=queue_url, WaitTimeSeconds=2) - # the following doesn't work due to an issue in moto/localstack where - # an SQS message is generated per put_events rather than per event sent + bridge.delete_event_bus(Name="a-test-bus") + sqs.delete_queue(QueueUrl=queue["QueueUrl"]) - # message = messages["Messages"][1] - # body = message.get("Body") - # assert body is not None - # body_obj = json.loads(body) - # detail = body_obj.get("detail") - # headers = detail.get("_datadog") - # assert headers is not None - # assert headers[HTTP_HEADER_TRACE_ID] == str(span.trace_id) - # assert headers[HTTP_HEADER_PARENT_ID] == str(span.span_id) + spans = self.get_spans() + assert spans + assert len(spans) == 2 + span = spans[0] + str_entries = span.get_tag("params.Entries") + assert str_entries is None + + message = messages["Messages"][0] + body = message.get("Body") + assert body is not None + body_obj = json.loads(body) + detail = body_obj.get("detail") + headers = detail.get("_datadog") + assert headers is not None + assert headers[HTTP_HEADER_TRACE_ID] == str(span.trace_id) + assert headers[HTTP_HEADER_PARENT_ID] == str(span.span_id) + + # the following doesn't work due to an issue in moto/localstack where + # an SQS message is generated per put_events rather than per event sent + + # message = messages["Messages"][1] + # body = message.get("Body") + # assert body is not None + # body_obj = json.loads(body) + # detail = body_obj.get("detail") + # headers = detail.get("_datadog") + # assert headers is not None + # assert headers[HTTP_HEADER_TRACE_ID] == str(span.trace_id) + # assert headers[HTTP_HEADER_PARENT_ID] == str(span.span_id) @mock_kms def test_kms_client(self): - kms = self.session.create_client("kms", region_name="us-east-1") - Pin(service=self.TEST_SERVICE, tracer=self.tracer).onto(kms) + # DEV: We can ignore the params tags as none currently exists. Test all params for deprecated exclusion. + with self.override_config("botocore", dict(tag_all_params=True)): + kms = self.session.create_client("kms", region_name="us-east-1") + Pin(service=self.TEST_SERVICE, tracer=self.tracer).onto(kms) - kms.list_keys(Limit=21) + kms.list_keys(Limit=21) - spans = self.get_spans() - assert spans - span = spans[0] - assert len(spans) == 1 - assert span.get_tag("aws.region") == "us-east-1" - assert span.get_tag("aws.operation") == "ListKeys" - assert_is_measured(span) - assert_span_http_status_code(span, 200) - assert span.service == "test-botocore-tracing.kms" - assert span.resource == "kms.listkeys" + spans = self.get_spans() + assert spans + span = spans[0] + assert len(spans) == 1 + assert span.get_tag("aws.region") == "us-east-1" + assert span.get_tag("aws.operation") == "ListKeys" + assert_is_measured(span) + assert_span_http_status_code(span, 200) + assert span.service == "test-botocore-tracing.kms" + assert span.resource == "kms.listkeys" - # checking for protection on sts against security leak - assert span.get_tag("params") is None + # checking for protection on sts against security leak + assert span.get_tag("params") is None @mock_ec2 def test_traced_client_ot(self): @@ -967,48 +1064,50 @@ def test_stubber_no_response_metadata(self): @mock_firehose def test_firehose_no_records_arg(self): - firehose = self.session.create_client("firehose", region_name="us-west-2") - Pin(service=self.TEST_SERVICE, tracer=self.tracer).onto(firehose) - - stream_name = "test-stream" - account_id = "test-account" - - firehose.create_delivery_stream( - DeliveryStreamName=stream_name, - RedshiftDestinationConfiguration={ - "RoleARN": "arn:aws:iam::{}:role/firehose_delivery_role".format(account_id), - "ClusterJDBCURL": "jdbc:redshift://host.amazonaws.com:5439/database", - "CopyCommand": { - "DataTableName": "outputTable", - "CopyOptions": "CSV DELIMITER ',' NULL '\\0'", - }, - "Username": "username", - "Password": "password", - "S3Configuration": { + # DEV: This test only applies for deprecated all params + with self.override_config("botocore", dict(tag_all_params=True)): + firehose = self.session.create_client("firehose", region_name="us-west-2") + Pin(service=self.TEST_SERVICE, tracer=self.tracer).onto(firehose) + + stream_name = "test-stream" + account_id = "test-account" + + firehose.create_delivery_stream( + DeliveryStreamName=stream_name, + RedshiftDestinationConfiguration={ "RoleARN": "arn:aws:iam::{}:role/firehose_delivery_role".format(account_id), - "BucketARN": "arn:aws:s3:::kinesis-test", - "Prefix": "myFolder/", - "BufferingHints": {"SizeInMBs": 123, "IntervalInSeconds": 124}, - "CompressionFormat": "UNCOMPRESSED", + "ClusterJDBCURL": "jdbc:redshift://host.amazonaws.com:5439/database", + "CopyCommand": { + "DataTableName": "outputTable", + "CopyOptions": "CSV DELIMITER ',' NULL '\\0'", + }, + "Username": "username", + "Password": "password", + "S3Configuration": { + "RoleARN": "arn:aws:iam::{}:role/firehose_delivery_role".format(account_id), + "BucketARN": "arn:aws:s3:::kinesis-test", + "Prefix": "myFolder/", + "BufferingHints": {"SizeInMBs": 123, "IntervalInSeconds": 124}, + "CompressionFormat": "UNCOMPRESSED", + }, }, - }, - ) + ) - firehose.put_record_batch( - DeliveryStreamName=stream_name, - Records=[{"Data": "some data"}], - ) + firehose.put_record_batch( + DeliveryStreamName=stream_name, + Records=[{"Data": "some data"}], + ) - spans = self.get_spans() + spans = self.get_spans() - assert spans - assert len(spans) == 2 - assert all(span.name == "firehose.command" for span in spans) + assert spans + assert len(spans) == 2 + assert all(span.name == "firehose.command" for span in spans) - delivery_stream_span, put_record_batch_span = spans - assert delivery_stream_span.get_tag("aws.operation") == "CreateDeliveryStream" - assert put_record_batch_span.get_tag("aws.operation") == "PutRecordBatch" - assert put_record_batch_span.get_tag("params.Records") is None + delivery_stream_span, put_record_batch_span = spans + assert delivery_stream_span.get_tag("aws.operation") == "CreateDeliveryStream" + assert put_record_batch_span.get_tag("aws.operation") == "PutRecordBatch" + assert put_record_batch_span.get_tag("params.Records") is None @TracerTestCase.run_in_subprocess(env_overrides=dict(DD_BOTOCORE_DISTRIBUTED_TRACING="true")) def test_distributed_tracing_env_override(self): @@ -1022,9 +1121,7 @@ def test_distributed_tracing_env_override_false(self): def test_invoke_legacy_context_env_override(self): assert config.botocore.invoke_with_legacy_context is True - @mock_sns - @mock_sqs - def test_sns_send_message_trace_injection_with_no_message_attributes(self): + def _test_sns(self): sns = self.session.create_client("sns", region_name="us-east-1", endpoint_url="http://localhost:4566") sqs = self.session.create_client("sqs", region_name="us-east-1", endpoint_url="http://localhost:4566") @@ -1041,171 +1138,229 @@ def test_sns_send_message_trace_injection_with_no_message_attributes(self): spans = self.get_spans() # get SNS messages via SQS - response = sqs.receive_message(QueueUrl=queue["QueueUrl"], WaitTimeSeconds=2) + _ = sqs.receive_message(QueueUrl=queue["QueueUrl"], WaitTimeSeconds=2) # clean up resources sqs.delete_queue(QueueUrl=sqs_url) sns.delete_topic(TopicArn=topic_arn) # check if the appropriate span was generated - assert spans - span = spans[0] assert len(spans) == 2 - assert span.get_tag("aws.region") == "us-east-1" - assert span.get_tag("aws.operation") == "Publish" - assert span.get_tag("params.MessageBody") is None - assert_is_measured(span) - assert_span_http_status_code(span, 200) - assert span.service == "test-botocore-tracing.sns" - assert span.resource == "sns.publish" - trace_json = span.get_tag("params.MessageAttributes._datadog.StringValue") - assert trace_json is None + return spans[0] - # receive message using SQS and ensure headers are present - assert len(response["Messages"]) == 1 - msg = response["Messages"][0] - assert msg is not None - msg_body = json.loads(msg["Body"]) - msg_str = msg_body["Message"] - assert msg_str == "test" - msg_attr = msg_body["MessageAttributes"] - assert msg_attr.get("_datadog") is not None - assert msg_attr["_datadog"]["Type"] == "Binary" - datadog_value_decoded = base64.b64decode(msg_attr["_datadog"]["Value"]) - headers = json.loads(datadog_value_decoded.decode()) - assert headers is not None - assert headers[HTTP_HEADER_TRACE_ID] == str(span.trace_id) - assert headers[HTTP_HEADER_PARENT_ID] == str(span.span_id) + @mock_sns + @mock_sqs + def test_sns(self): + span = self._test_sns() + assert span.get_tag("aws.sns.topic_arn") == "arn:aws:sns:us-east-1:000000000000:testTopic" @mock_sns @mock_sqs - @pytest.mark.xfail(strict=False) # FIXME: flaky test - def test_sns_send_message_trace_injection_with_message_attributes(self): - sns = self.session.create_client("sns", region_name="us-east-1", endpoint_url="http://localhost:4566") - sqs = self.session.create_client("sqs", region_name="us-east-1", endpoint_url="http://localhost:4566") + def test_sns_no_params(self): + with self.override_config("botocore", dict(tag_no_params=True)): + span = self._test_sns() + assert span.get_tag("aws.sns.topic_arn") is None - topic = sns.create_topic(Name="testTopic") - queue = sqs.create_queue(QueueName="test") + @mock_sns + @mock_sqs + def test_sns_all_params(self): + with self.override_config("botocore", dict(tag_all_params=True)): + span = self._test_sns() + assert span.get_tag("params.MessageBody") is None - topic_arn = topic["TopicArn"] - sqs_url = queue["QueueUrl"] - sns.subscribe(TopicArn=topic_arn, Protocol="sqs", Endpoint=sqs_url) + @mock_sns + @mock_sqs + def test_sns_send_message_trace_injection_with_no_message_attributes(self): + # DEV: This test expects MessageAttributes to be included as span tags which has been deprecated. + # TODO: Move away from inspecting MessageAttributes using span tag + with self.override_config("botocore", dict(tag_all_params=True)): + sns = self.session.create_client("sns", region_name="us-east-1", endpoint_url="http://localhost:4566") + sqs = self.session.create_client("sqs", region_name="us-east-1", endpoint_url="http://localhost:4566") - Pin(service=self.TEST_SERVICE, tracer=self.tracer).onto(sns) + topic = sns.create_topic(Name="testTopic") + queue = sqs.create_queue(QueueName="test") - message_attributes = { - "one": {"DataType": "String", "StringValue": "one"}, - "two": {"DataType": "String", "StringValue": "two"}, - "three": {"DataType": "String", "StringValue": "three"}, - "four": {"DataType": "String", "StringValue": "four"}, - "five": {"DataType": "String", "StringValue": "five"}, - "six": {"DataType": "String", "StringValue": "six"}, - "seven": {"DataType": "String", "StringValue": "seven"}, - "eight": {"DataType": "String", "StringValue": "eight"}, - "nine": {"DataType": "String", "StringValue": "nine"}, - } + topic_arn = topic["TopicArn"] + sqs_url = queue["QueueUrl"] + sns.subscribe(TopicArn=topic_arn, Protocol="sqs", Endpoint=sqs_url) - sns.publish(TopicArn=topic_arn, Message="test", MessageAttributes=message_attributes) - spans = self.get_spans() + Pin(service=self.TEST_SERVICE, tracer=self.tracer).onto(sns) - # get SNS messages via SQS - response = sqs.receive_message(QueueUrl=queue["QueueUrl"], MessageAttributeNames=["_datadog"]) + sns.publish(TopicArn=topic_arn, Message="test") + spans = self.get_spans() - # clean up resources - sqs.delete_queue(QueueUrl=sqs_url) - sns.delete_topic(TopicArn=topic_arn) + # get SNS messages via SQS + response = sqs.receive_message(QueueUrl=queue["QueueUrl"], WaitTimeSeconds=2) - # check if the appropriate span was generated - assert spans - span = spans[0] - assert len(spans) == 2 - assert span.get_tag("aws.region") == "us-east-1" - assert span.get_tag("aws.operation") == "Publish" - assert span.get_tag("params.MessageBody") is None - assert_is_measured(span) - assert_span_http_status_code(span, 200) - assert span.service == "test-botocore-tracing.sns" - assert span.resource == "sns.publish" - trace_json = span.get_tag("params.MessageAttributes._datadog.StringValue") - assert trace_json is None + # clean up resources + sqs.delete_queue(QueueUrl=sqs_url) + sns.delete_topic(TopicArn=topic_arn) - # receive message using SQS and ensure headers are present - assert len(response["Messages"]) == 1 - msg = response["Messages"][0] - assert msg is not None - msg_body = json.loads(msg["Body"]) - msg_str = msg_body["Message"] - assert msg_str == "test" - msg_attr = msg_body["MessageAttributes"] - assert msg_attr.get("_datadog") is not None - assert msg_attr["_datadog"]["Type"] == "Binary" - datadog_value_decoded = base64.b64decode(msg_attr["_datadog"]["Value"]) - headers = json.loads(datadog_value_decoded.decode()) - assert headers is not None - assert headers[HTTP_HEADER_TRACE_ID] == str(span.trace_id) - assert headers[HTTP_HEADER_PARENT_ID] == str(span.span_id) + # check if the appropriate span was generated + assert spans + span = spans[0] + assert len(spans) == 2 + assert span.get_tag("aws.region") == "us-east-1" + assert span.get_tag("aws.operation") == "Publish" + assert span.get_tag("params.MessageBody") is None + assert_is_measured(span) + assert_span_http_status_code(span, 200) + assert span.service == "test-botocore-tracing.sns" + assert span.resource == "sns.publish" + trace_json = span.get_tag("params.MessageAttributes._datadog.StringValue") + assert trace_json is None + + # receive message using SQS and ensure headers are present + assert len(response["Messages"]) == 1 + msg = response["Messages"][0] + assert msg is not None + msg_body = json.loads(msg["Body"]) + msg_str = msg_body["Message"] + assert msg_str == "test" + msg_attr = msg_body["MessageAttributes"] + assert msg_attr.get("_datadog") is not None + assert msg_attr["_datadog"]["Type"] == "Binary" + datadog_value_decoded = base64.b64decode(msg_attr["_datadog"]["Value"]) + headers = json.loads(datadog_value_decoded.decode()) + assert headers is not None + assert headers[HTTP_HEADER_TRACE_ID] == str(span.trace_id) + assert headers[HTTP_HEADER_PARENT_ID] == str(span.span_id) @mock_sns @mock_sqs - def test_sns_send_message_trace_injection_with_max_message_attributes(self): - sns = self.session.create_client("sns", region_name="us-east-1", endpoint_url="http://localhost:4566") - sqs = self.session.create_client("sqs", region_name="us-east-1", endpoint_url="http://localhost:4566") + @pytest.mark.xfail(strict=False) # FIXME: flaky test + def test_sns_send_message_trace_injection_with_message_attributes(self): + # DEV: This test expects MessageAttributes to be included as span tags which has been deprecated. + # TODO: Move away from inspecting MessageAttributes using span tag + with self.override_config("botocore", dict(tag_all_params=True)): + sns = self.session.create_client("sns", region_name="us-east-1", endpoint_url="http://localhost:4566") + sqs = self.session.create_client("sqs", region_name="us-east-1", endpoint_url="http://localhost:4566") - topic = sns.create_topic(Name="testTopic") - queue = sqs.create_queue(QueueName="test") + topic = sns.create_topic(Name="testTopic") + queue = sqs.create_queue(QueueName="test") - topic_arn = topic["TopicArn"] - sqs_url = queue["QueueUrl"] - sns.subscribe(TopicArn=topic_arn, Protocol="sqs", Endpoint=sqs_url) + topic_arn = topic["TopicArn"] + sqs_url = queue["QueueUrl"] + sns.subscribe(TopicArn=topic_arn, Protocol="sqs", Endpoint=sqs_url) + + Pin(service=self.TEST_SERVICE, tracer=self.tracer).onto(sns) + + message_attributes = { + "one": {"DataType": "String", "StringValue": "one"}, + "two": {"DataType": "String", "StringValue": "two"}, + "three": {"DataType": "String", "StringValue": "three"}, + "four": {"DataType": "String", "StringValue": "four"}, + "five": {"DataType": "String", "StringValue": "five"}, + "six": {"DataType": "String", "StringValue": "six"}, + "seven": {"DataType": "String", "StringValue": "seven"}, + "eight": {"DataType": "String", "StringValue": "eight"}, + "nine": {"DataType": "String", "StringValue": "nine"}, + } - Pin(service=self.TEST_SERVICE, tracer=self.tracer).onto(sns) + sns.publish(TopicArn=topic_arn, Message="test", MessageAttributes=message_attributes) + spans = self.get_spans() - message_attributes = { - "one": {"DataType": "String", "StringValue": "one"}, - "two": {"DataType": "String", "StringValue": "two"}, - "three": {"DataType": "String", "StringValue": "three"}, - "four": {"DataType": "String", "StringValue": "four"}, - "five": {"DataType": "String", "StringValue": "five"}, - "six": {"DataType": "String", "StringValue": "six"}, - "seven": {"DataType": "String", "StringValue": "seven"}, - "eight": {"DataType": "String", "StringValue": "eight"}, - "nine": {"DataType": "String", "StringValue": "nine"}, - "ten": {"DataType": "String", "StringValue": "ten"}, - } + # get SNS messages via SQS + response = sqs.receive_message(QueueUrl=queue["QueueUrl"], MessageAttributeNames=["_datadog"]) - sns.publish(TopicArn=topic_arn, Message="test", MessageAttributes=message_attributes) - spans = self.get_spans() + # clean up resources + sqs.delete_queue(QueueUrl=sqs_url) + sns.delete_topic(TopicArn=topic_arn) - # get SNS messages via SQS - response = sqs.receive_message(QueueUrl=queue["QueueUrl"], WaitTimeSeconds=2) + # check if the appropriate span was generated + assert spans + span = spans[0] + assert len(spans) == 2 + assert span.get_tag("aws.region") == "us-east-1" + assert span.get_tag("aws.operation") == "Publish" + assert span.get_tag("params.MessageBody") is None + assert_is_measured(span) + assert_span_http_status_code(span, 200) + assert span.service == "test-botocore-tracing.sns" + assert span.resource == "sns.publish" + trace_json = span.get_tag("params.MessageAttributes._datadog.StringValue") + assert trace_json is None - # clean up resources - sqs.delete_queue(QueueUrl=sqs_url) - sns.delete_topic(TopicArn=topic_arn) + # receive message using SQS and ensure headers are present + assert len(response["Messages"]) == 1 + msg = response["Messages"][0] + assert msg is not None + msg_body = json.loads(msg["Body"]) + msg_str = msg_body["Message"] + assert msg_str == "test" + msg_attr = msg_body["MessageAttributes"] + assert msg_attr.get("_datadog") is not None + assert msg_attr["_datadog"]["Type"] == "Binary" + datadog_value_decoded = base64.b64decode(msg_attr["_datadog"]["Value"]) + headers = json.loads(datadog_value_decoded.decode()) + assert headers is not None + assert headers[HTTP_HEADER_TRACE_ID] == str(span.trace_id) + assert headers[HTTP_HEADER_PARENT_ID] == str(span.span_id) - # check if the appropriate span was generated - assert spans - span = spans[0] - assert len(spans) == 2 - assert span.get_tag("aws.region") == "us-east-1" - assert span.get_tag("aws.operation") == "Publish" - assert span.get_tag("params.MessageBody") is None - assert_is_measured(span) - assert_span_http_status_code(span, 200) - assert span.service == "test-botocore-tracing.sns" - assert span.resource == "sns.publish" - trace_json = span.get_tag("params.MessageAttributes._datadog.StringValue") - assert trace_json is None + @mock_sns + @mock_sqs + def test_sns_send_message_trace_injection_with_max_message_attributes(self): + # DEV: This test expects MessageAttributes to be included as span tags which has been deprecated. + # TODO: Move away from inspecting MessageAttributes using span tag + with self.override_config("botocore", dict(tag_all_params=True)): + sns = self.session.create_client("sns", region_name="us-east-1", endpoint_url="http://localhost:4566") + sqs = self.session.create_client("sqs", region_name="us-east-1", endpoint_url="http://localhost:4566") - # receive message using SQS and ensure headers are present - assert len(response["Messages"]) == 1 - msg = response["Messages"][0] - assert msg is not None - msg_body = json.loads(msg["Body"]) - msg_str = msg_body["Message"] - assert msg_str == "test" - msg_attr = msg_body["MessageAttributes"] - assert msg_attr.get("_datadog") is None + topic = sns.create_topic(Name="testTopic") + queue = sqs.create_queue(QueueName="test") + + topic_arn = topic["TopicArn"] + sqs_url = queue["QueueUrl"] + sns.subscribe(TopicArn=topic_arn, Protocol="sqs", Endpoint=sqs_url) + + Pin(service=self.TEST_SERVICE, tracer=self.tracer).onto(sns) + + message_attributes = { + "one": {"DataType": "String", "StringValue": "one"}, + "two": {"DataType": "String", "StringValue": "two"}, + "three": {"DataType": "String", "StringValue": "three"}, + "four": {"DataType": "String", "StringValue": "four"}, + "five": {"DataType": "String", "StringValue": "five"}, + "six": {"DataType": "String", "StringValue": "six"}, + "seven": {"DataType": "String", "StringValue": "seven"}, + "eight": {"DataType": "String", "StringValue": "eight"}, + "nine": {"DataType": "String", "StringValue": "nine"}, + "ten": {"DataType": "String", "StringValue": "ten"}, + } + + sns.publish(TopicArn=topic_arn, Message="test", MessageAttributes=message_attributes) + spans = self.get_spans() + + # get SNS messages via SQS + response = sqs.receive_message(QueueUrl=queue["QueueUrl"], WaitTimeSeconds=2) + + # clean up resources + sqs.delete_queue(QueueUrl=sqs_url) + sns.delete_topic(TopicArn=topic_arn) + + # check if the appropriate span was generated + assert spans + span = spans[0] + assert len(spans) == 2 + assert span.get_tag("aws.region") == "us-east-1" + assert span.get_tag("aws.operation") == "Publish" + assert span.get_tag("params.MessageBody") is None + assert_is_measured(span) + assert_span_http_status_code(span, 200) + assert span.service == "test-botocore-tracing.sns" + assert span.resource == "sns.publish" + trace_json = span.get_tag("params.MessageAttributes._datadog.StringValue") + assert trace_json is None + + # receive message using SQS and ensure headers are present + assert len(response["Messages"]) == 1 + msg = response["Messages"][0] + assert msg is not None + msg_body = json.loads(msg["Body"]) + msg_str = msg_body["Message"] + assert msg_str == "test" + msg_attr = msg_body["MessageAttributes"] + assert msg_attr.get("_datadog") is None # NOTE: commenting out the tests below because localstack has a bug where messages # published to SNS via publish_batch and retrieved via SQS are missing MessageAttributes @@ -1424,13 +1579,10 @@ def test_kinesis_put_record_json_trace_injection(self): assert len(spans) == 1 assert span.get_tag("aws.region") == "us-east-1" assert span.get_tag("aws.operation") == "PutRecord" - assert span.get_tag("params.MessageBody") is None assert_is_measured(span) assert_span_http_status_code(span, 200) assert span.service == "test-botocore-tracing.kinesis" assert span.resource == "kinesis.putrecord" - trace_json = span.get_tag("params.Data") - assert trace_json is None resp = client.get_shard_iterator(StreamName=stream_name, ShardId=shard_id, ShardIteratorType="TRIM_HORIZON") shard_iterator = resp["ShardIterator"] @@ -1474,13 +1626,10 @@ def test_kinesis_put_record_base64_trace_injection(self): assert len(spans) == 1 assert span.get_tag("aws.region") == "us-east-1" assert span.get_tag("aws.operation") == "PutRecord" - assert span.get_tag("params.MessageBody") is None assert_is_measured(span) assert_span_http_status_code(span, 200) assert span.service == "test-botocore-tracing.kinesis" assert span.resource == "kinesis.putrecord" - trace_json = span.get_tag("params.Data") - assert trace_json is None resp = client.get_shard_iterator(StreamName=stream_name, ShardId=shard_id, ShardIteratorType="TRIM_HORIZON") shard_iterator = resp["ShardIterator"] @@ -1529,8 +1678,6 @@ def test_kinesis_put_record_base64_max_size(self): assert_span_http_status_code(span, 200) assert span.service == "test-botocore-tracing.kinesis" assert span.resource == "kinesis.putrecord" - trace_json = span.get_tag("params.Data") - assert trace_json is None resp = client.get_shard_iterator(StreamName=stream_name, ShardId=shard_id, ShardIteratorType="TRIM_HORIZON") shard_iterator = resp["ShardIterator"] @@ -1575,8 +1722,6 @@ def test_kinesis_put_records_json_trace_injection(self): assert_span_http_status_code(span, 200) assert span.service == "test-botocore-tracing.kinesis" assert span.resource == "kinesis.putrecords" - records = span.get_tag("params.Records") - assert records is None resp = client.get_shard_iterator(StreamName=stream_name, ShardId=shard_id, ShardIteratorType="TRIM_HORIZON") shard_iterator = resp["ShardIterator"] @@ -1626,13 +1771,10 @@ def test_kinesis_put_records_base64_trace_injection(self): assert len(spans) == 1 assert span.get_tag("aws.region") == "us-east-1" assert span.get_tag("aws.operation") == "PutRecords" - assert span.get_tag("params.MessageBody") is None assert_is_measured(span) assert_span_http_status_code(span, 200) assert span.service == "test-botocore-tracing.kinesis" assert span.resource == "kinesis.putrecords" - records = span.get_tag("params.Records") - assert records is None resp = client.get_shard_iterator(StreamName=stream_name, ShardId=shard_id, ShardIteratorType="TRIM_HORIZON") shard_iterator = resp["ShardIterator"] @@ -1670,7 +1812,7 @@ def test_secretsmanager(self): assert span.name == "secretsmanager.command" assert span.resource == "secretsmanager.createsecret" - assert span.get_tag("params.Name") == "/my/secrets" + assert span.get_tag("params.Name") is None assert span.get_tag("aws.operation") == "CreateSecret" assert span.get_tag("aws.region") == "us-east-1" assert span.get_tag("aws.agent") == "botocore" @@ -1678,6 +1820,32 @@ def test_secretsmanager(self): assert span.get_tag("params.SecretString") is None assert span.get_tag("params.SecretBinary") is None + @unittest.skipIf(PY2, "Skipping for Python 2.7 since older moto doesn't support secretsmanager") + def test_secretsmanager_all_params(self): + with self.override_config("botocore", dict(tag_all_params=True)): + from moto import mock_secretsmanager + + with mock_secretsmanager(): + client = self.session.create_client("secretsmanager", region_name="us-east-1") + Pin(service=self.TEST_SERVICE, tracer=self.tracer).onto(client) + + resp = client.create_secret(Name="/my/secrets", SecretString="supersecret-string") + assert resp["ResponseMetadata"]["HTTPStatusCode"] == 200 + + spans = self.get_spans() + assert len(spans) == 1 + span = spans[0] + + assert span.name == "secretsmanager.command" + assert span.resource == "secretsmanager.createsecret" + assert span.get_tag("params.Name") == "/my/secrets" + assert span.get_tag("aws.operation") == "CreateSecret" + assert span.get_tag("aws.region") == "us-east-1" + assert span.get_tag("aws.agent") == "botocore" + assert span.get_tag("http.status_code") == "200" + assert span.get_tag("params.SecretString") is None + assert span.get_tag("params.SecretBinary") is None + @unittest.skipIf(PY2, "Skipping for Python 2.7 since older moto doesn't support secretsmanager") def test_secretsmanager_binary(self): from moto import mock_secretsmanager @@ -1695,10 +1863,36 @@ def test_secretsmanager_binary(self): assert span.name == "secretsmanager.command" assert span.resource == "secretsmanager.createsecret" - assert span.get_tag("params.Name") == "/my/secrets" + assert span.get_tag("params.Name") is None assert span.get_tag("aws.operation") == "CreateSecret" assert span.get_tag("aws.region") == "us-east-1" assert span.get_tag("aws.agent") == "botocore" assert span.get_tag("http.status_code") == "200" assert span.get_tag("params.SecretString") is None assert span.get_tag("params.SecretBinary") is None + + @unittest.skipIf(PY2, "Skipping for Python 2.7 since older moto doesn't support secretsmanager") + def test_secretsmanager_binary_all_params(self): + with self.override_config("botocore", dict(tag_all_params=True)): + from moto import mock_secretsmanager + + with mock_secretsmanager(): + client = self.session.create_client("secretsmanager", region_name="us-east-1") + Pin(service=self.TEST_SERVICE, tracer=self.tracer).onto(client) + + resp = client.create_secret(Name="/my/secrets", SecretBinary=b"supersecret-binary") + assert resp["ResponseMetadata"]["HTTPStatusCode"] == 200 + + spans = self.get_spans() + assert len(spans) == 1 + span = spans[0] + + assert span.name == "secretsmanager.command" + assert span.resource == "secretsmanager.createsecret" + assert span.get_tag("params.Name") == "/my/secrets" + assert span.get_tag("aws.operation") == "CreateSecret" + assert span.get_tag("aws.region") == "us-east-1" + assert span.get_tag("aws.agent") == "botocore" + assert span.get_tag("http.status_code") == "200" + assert span.get_tag("params.SecretString") is None + assert span.get_tag("params.SecretBinary") is None From 2e07a34997f2809a0942747cfc686b53b4903c47 Mon Sep 17 00:00:00 2001 From: Munir Abdinur Date: Thu, 15 Dec 2022 11:26:31 -0500 Subject: [PATCH 3/4] feat(w3c): enable w3c tracecontext propagation by default (#4783) ## Description Updates default propagation mode to include w3c tracecontext propagation. Note - This change adds the following headers to all traced http connections: - traceparent - tracestate To revert to the previous behavior set the following configuration: `DD_TRACE_PROPAGATION_STYLE=datadog`. ## Reviewer Checklist - [ ] Title is accurate. - [ ] Description motivates each change. - [ ] No unnecessary changes were introduced in this PR. - [ ] Avoid breaking [API](https://ddtrace.readthedocs.io/en/stable/versioning.html#interfaces) changes unless absolutely necessary. - [ ] Tests provided or description of manual testing performed is included in the code or PR. - [ ] Release note has been added and follows the [library release note guidelines](https://ddtrace.readthedocs.io/en/stable/contributing.html#Release-Note-Guidelines), or else `changelog/no-changelog` label added. - [ ] All relevant GitHub issues are correctly linked. - [ ] Backports are identified and tagged with Mergifyio. Co-authored-by: Tahir H. Butt Co-authored-by: Brett Langdon --- ddtrace/internal/constants.py | 1 + ddtrace/settings/config.py | 6 +-- docs/spelling_wordlist.txt | 1 + ...cecontext_by_default-03cc948a3a466803.yaml | 7 ++++ tests/contrib/rq/test_rq.py | 2 +- tests/contrib/urllib3/test_urllib3.py | 2 + tests/tracer/test_propagation.py | 38 +++++++++++++------ 7 files changed, 41 insertions(+), 16 deletions(-) create mode 100644 releasenotes/notes/enable_tracecontext_by_default-03cc948a3a466803.yaml diff --git a/ddtrace/internal/constants.py b/ddtrace/internal/constants.py index 1ad85fcc9b4..c3175f88c7d 100644 --- a/ddtrace/internal/constants.py +++ b/ddtrace/internal/constants.py @@ -3,6 +3,7 @@ PROPAGATION_STYLE_B3_SINGLE_HEADER = "b3 single header" _PROPAGATION_STYLE_W3C_TRACECONTEXT = "tracecontext" _PROPAGATION_STYLE_NONE = "none" +_PROPAGATION_STYLE_DEFAULT = "tracecontext,datadog" PROPAGATION_STYLE_ALL = ( _PROPAGATION_STYLE_W3C_TRACECONTEXT, PROPAGATION_STYLE_DATADOG, diff --git a/ddtrace/settings/config.py b/ddtrace/settings/config.py index 69a574cda6c..1f4ec30f958 100644 --- a/ddtrace/settings/config.py +++ b/ddtrace/settings/config.py @@ -13,7 +13,7 @@ from ..internal.constants import PROPAGATION_STYLE_ALL from ..internal.constants import PROPAGATION_STYLE_B3 -from ..internal.constants import PROPAGATION_STYLE_DATADOG +from ..internal.constants import _PROPAGATION_STYLE_DEFAULT from ..internal.logger import get_logger from ..internal.utils.formats import asbool from ..internal.utils.formats import parse_tags_str @@ -54,7 +54,7 @@ def _parse_propagation_styles(name, default): - "none" - The default value is ``"datadog"``. + The default value is ``"tracecontext,datadog"``. Examples:: @@ -222,7 +222,7 @@ def __init__(self): # Propagation styles self._propagation_style_extract = self._propagation_style_inject = _parse_propagation_styles( - "DD_TRACE_PROPAGATION_STYLE", default=PROPAGATION_STYLE_DATADOG + "DD_TRACE_PROPAGATION_STYLE", default=_PROPAGATION_STYLE_DEFAULT ) # DD_TRACE_PROPAGATION_STYLE_EXTRACT and DD_TRACE_PROPAGATION_STYLE_INJECT # take precedence over DD_TRACE_PROPAGATION_STYLE diff --git a/docs/spelling_wordlist.txt b/docs/spelling_wordlist.txt index 6a146ad9368..15f51e80c61 100644 --- a/docs/spelling_wordlist.txt +++ b/docs/spelling_wordlist.txt @@ -190,6 +190,7 @@ username uvicorn versioned vertica +w3c whitelist workflow wsgi diff --git a/releasenotes/notes/enable_tracecontext_by_default-03cc948a3a466803.yaml b/releasenotes/notes/enable_tracecontext_by_default-03cc948a3a466803.yaml new file mode 100644 index 00000000000..abe111c4a03 --- /dev/null +++ b/releasenotes/notes/enable_tracecontext_by_default-03cc948a3a466803.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + tracing: Adds support for W3C Trace Context propagation style for distributed tracing. The ``traceparent`` and ``tracestate`` HTTP headers are enabled by default for all incoming and outgoing HTTP request headers. The Datadog propagation style continue to be enabled by default. +upgrade: + - | + The default propagation style configuration changes to ``DD_TRACE_PROPAGATION_STYLE=tracecontext,datadog``. To only support Datadog propagation and retain the existing default behavior, set ``DD_TRACE_PROPAGATION_STYLE=datadog``. diff --git a/tests/contrib/rq/test_rq.py b/tests/contrib/rq/test_rq.py index 370ff3da41a..5496faa1a3f 100644 --- a/tests/contrib/rq/test_rq.py +++ b/tests/contrib/rq/test_rq.py @@ -20,7 +20,7 @@ # Span data which isn't static to ignore in the snapshots. -snapshot_ignores = ["meta.job.id", "meta.error.stack"] +snapshot_ignores = ["meta.job.id", "meta.error.stack", "meta.traceparent", "meta.tracestate"] rq_version = tuple(int(x) for x in rq.__version__.split(".")[:3]) diff --git a/tests/contrib/urllib3/test_urllib3.py b/tests/contrib/urllib3/test_urllib3.py index 79a1e5baebc..747f19f8bd6 100644 --- a/tests/contrib/urllib3/test_urllib3.py +++ b/tests/contrib/urllib3/test_urllib3.py @@ -372,6 +372,8 @@ def test_distributed_tracing_enabled(self): "x-datadog-parent-id": str(s.span_id), "x-datadog-sampling-priority": "1", "x-datadog-tags": "_dd.p.dm=-0", + "traceparent": s.context._traceparent, + "tracestate": s.context._tracestate, } m_make_request.assert_called_with( mock.ANY, "GET", "/status/200", body=None, chunked=mock.ANY, headers=expected_headers, timeout=mock.ANY diff --git a/tests/tracer/test_propagation.py b/tests/tracer/test_propagation.py index 108574146e8..3946a3b5fea 100644 --- a/tests/tracer/test_propagation.py +++ b/tests/tracer/test_propagation.py @@ -68,16 +68,22 @@ def test_inject_tags_unicode(tracer): def test_inject_tags_bytes(tracer): """We properly encode when the meta key as long as it is just ascii characters""" # Context._meta allows str and bytes for keys - meta = {u"_dd.p.test": b"bytes"} - ctx = Context(trace_id=1234, sampling_priority=2, dd_origin="synthetics", meta=meta) - tracer.context_provider.activate(ctx) - with tracer.trace("global_root_span") as span: - headers = {} - HTTPPropagator.inject(span.context, headers) + # FIXME: W3C does not support byte headers + overrides = { + "_propagation_style_extract": [PROPAGATION_STYLE_DATADOG], + "_propagation_style_inject": [PROPAGATION_STYLE_DATADOG], + } + with override_global_config(overrides): + meta = {u"_dd.p.test": b"bytes"} + ctx = Context(trace_id=1234, sampling_priority=2, dd_origin="synthetics", meta=meta) + tracer.context_provider.activate(ctx) + with tracer.trace("global_root_span") as span: + headers = {} + HTTPPropagator.inject(span.context, headers) - # The ordering is non-deterministic, so compare as a list of tags - tags = set(headers[_HTTP_HEADER_TAGS].split(",")) - assert tags == set(["_dd.p.test=bytes"]) + # The ordering is non-deterministic, so compare as a list of tags + tags = set(headers[_HTTP_HEADER_TAGS].split(",")) + assert tags == set(["_dd.p.test=bytes"]) def test_inject_tags_unicode_error(tracer): @@ -1408,6 +1414,8 @@ def test_DD_TRACE_PROPAGATION_STYLE_EXTRACT_overrides_DD_TRACE_PROPAGATION_STYLE "sampling_priority": 1, "dd_origin": "synthetics", } + + VALID_USER_KEEP_CONTEXT = { "trace_id": 13088165645273925489, "span_id": 8185124618007618416, @@ -1467,6 +1475,8 @@ def test_DD_TRACE_PROPAGATION_STYLE_EXTRACT_overrides_DD_TRACE_PROPAGATION_STYLE HTTP_HEADER_PARENT_ID: "8185124618007618416", HTTP_HEADER_SAMPLING_PRIORITY: "1", HTTP_HEADER_ORIGIN: "synthetics", + _HTTP_HEADER_TRACESTATE: "dd=s~1;o~synthetics", + _HTTP_HEADER_TRACEPARENT: "00-0000000000000000b5a2814f70060771-7197677932a62370-01", }, ), ( @@ -1477,6 +1487,8 @@ def test_DD_TRACE_PROPAGATION_STYLE_EXTRACT_overrides_DD_TRACE_PROPAGATION_STYLE HTTP_HEADER_TRACE_ID: "13088165645273925489", HTTP_HEADER_PARENT_ID: "8185124618007618416", HTTP_HEADER_SAMPLING_PRIORITY: "2", + _HTTP_HEADER_TRACESTATE: "dd=s~2", + _HTTP_HEADER_TRACEPARENT: "00-0000000000000000b5a2814f70060771-7197677932a62370-01", }, ), ( @@ -1487,6 +1499,8 @@ def test_DD_TRACE_PROPAGATION_STYLE_EXTRACT_overrides_DD_TRACE_PROPAGATION_STYLE HTTP_HEADER_TRACE_ID: "13088165645273925489", HTTP_HEADER_PARENT_ID: "8185124618007618416", HTTP_HEADER_SAMPLING_PRIORITY: "0", + _HTTP_HEADER_TRACESTATE: "dd=s~0", + _HTTP_HEADER_TRACEPARENT: "00-0000000000000000b5a2814f70060771-7197677932a62370-00", }, ), ( @@ -1502,7 +1516,7 @@ def test_DD_TRACE_PROPAGATION_STYLE_EXTRACT_overrides_DD_TRACE_PROPAGATION_STYLE ), ( "valid_datadog_style_user_keep", - None, + [PROPAGATION_STYLE_DATADOG], VALID_USER_KEEP_CONTEXT, { HTTP_HEADER_TRACE_ID: "13088165645273925489", @@ -1512,7 +1526,7 @@ def test_DD_TRACE_PROPAGATION_STYLE_EXTRACT_overrides_DD_TRACE_PROPAGATION_STYLE ), ( "valid_datadog_style_auto_reject", - None, + [PROPAGATION_STYLE_DATADOG], VALID_AUTO_REJECT_CONTEXT, { HTTP_HEADER_TRACE_ID: "13088165645273925489", @@ -1522,7 +1536,7 @@ def test_DD_TRACE_PROPAGATION_STYLE_EXTRACT_overrides_DD_TRACE_PROPAGATION_STYLE ), ( "valid_datadog_style_no_sampling_priority", - None, + [PROPAGATION_STYLE_DATADOG], { "trace_id": VALID_DATADOG_CONTEXT["trace_id"], "span_id": VALID_DATADOG_CONTEXT["span_id"], From 2f586cc582d2148f051d22c5e75c0dc85b36af5b Mon Sep 17 00:00:00 2001 From: Idan Shatz Date: Thu, 15 Dec 2022 11:00:26 -0700 Subject: [PATCH 4/4] chore(debugger): support for all metric kinds with value expression (#4699) ## Description Effort to make feature parity with dd-trace-java metric implementation. To gain parity python debugger need to support: 1. GAUGE and HISTOGRAM metrics. 2. Be able to evaluate the metric value expression 3. Support Method Metric Probe with duration We also decided to support DISTRIBUTION metric support as well. This PR address the two main features. future PR would add support for function level Metric Probe ## Reviewer Checklist - [ ] Title is accurate. - [ ] Description motivates each change. - [ ] No unnecessary changes were introduced in this PR. - [ ] Avoid breaking [API](https://ddtrace.readthedocs.io/en/stable/versioning.html#interfaces) changes unless absolutely necessary. - [ ] Tests provided or description of manual testing performed is included in the code or PR. - [ ] Release note has been added and follows the [library release note guidelines](https://ddtrace.readthedocs.io/en/stable/contributing.html#Release-Note-Guidelines), or else `changelog/no-changelog` label added. - [ ] All relevant GitHub issues are correctly linked. - [ ] Backports are identified and tagged with Mergifyio. Co-authored-by: Brett Langdon Co-authored-by: Gabriele N. Tornetta --- ddtrace/debugging/_debugger.py | 15 ++++-- ddtrace/debugging/_probe/model.py | 1 + ddtrace/debugging/_probe/remoteconfig.py | 17 +++---- ddtrace/internal/metrics.py | 18 ++++++++ tests/debugging/test_debugger.py | 59 ++++++++++++++++++------ 5 files changed, 86 insertions(+), 24 deletions(-) diff --git a/ddtrace/debugging/_debugger.py b/ddtrace/debugging/_debugger.py index 00d430ec9a3..4e22c6c34f0 100644 --- a/ddtrace/debugging/_debugger.py +++ b/ddtrace/debugging/_debugger.py @@ -254,10 +254,19 @@ def _dd_debugger_hook(self, probe): if isinstance(probe, MetricProbe): # TODO: Handle value expressions assert probe.kind is not None and probe.name is not None + + value = float(probe.value(sys._getframe(1).f_locals)) if probe.value is not None else 1 + + # TODO[perf]: We know the tags in advance so we can avoid the + # list comprehension. if probe.kind == MetricProbeKind.COUNTER: - # TODO[perf]: We know the tags in advance so we can avoid the - # list comprehension. - self._probe_meter.increment(probe.name, tags=probe.tags) + self._probe_meter.increment(probe.name, value, probe.tags) + elif probe.kind == MetricProbeKind.GAUGE: + self._probe_meter.gauge(probe.name, value, probe.tags) + elif probe.kind == MetricProbeKind.HISTOGRAM: + self._probe_meter.histogram(probe.name, value, probe.tags) + elif probe.kind == MetricProbeKind.DISTRIBUTION: + self._probe_meter.distribution(probe.name, value, probe.tags) return diff --git a/ddtrace/debugging/_probe/model.py b/ddtrace/debugging/_probe/model.py index 50469a403b3..13f72a23bf8 100644 --- a/ddtrace/debugging/_probe/model.py +++ b/ddtrace/debugging/_probe/model.py @@ -107,3 +107,4 @@ class MetricProbeKind(object): class MetricProbe(LineProbe): kind = attr.ib(type=Optional[str], default=None) name = attr.ib(type=Optional[str], default=None) + value = attr.ib(type=Optional[Callable[[Dict[str, Any]], Any]], default=None) diff --git a/ddtrace/debugging/_probe/remoteconfig.py b/ddtrace/debugging/_probe/remoteconfig.py index f9322c6dc8b..0299a90c417 100644 --- a/ddtrace/debugging/_probe/remoteconfig.py +++ b/ddtrace/debugging/_probe/remoteconfig.py @@ -25,8 +25,8 @@ _EXPRESSION_CACHE = LFUCache() -def _invalid_condition(_): - """Forces probes with invalid conditions to never trigger. +def _invalid_expression(_): + """Forces probes with invalid expression/conditions to never trigger. Any signs of invalid conditions in logs is an indication of a problem with the expression compiler. @@ -34,12 +34,12 @@ def _invalid_condition(_): return False -INVALID_CONDITION = _invalid_condition +INVALID_EXPRESSION = _invalid_expression -def _compile_condition(when): +def _compile_expression(when): # type: (Optional[Dict[str, Any]]) -> Optional[Callable[[Dict[str, Any]], Any]] - global _EXPRESSION_CACHE, INVALID_CONDITION + global _EXPRESSION_CACHE, INVALID_EXPRESSION if when is None: return None @@ -52,13 +52,13 @@ def compile_or_invalid(expr): return dd_compile(ast) except Exception: log.error("Cannot compile expression: %s", expr, exc_info=True) - return INVALID_CONDITION + return INVALID_EXPRESSION expr = when["dsl"] compiled = _EXPRESSION_CACHE.get(expr, compile_or_invalid) # type: Callable[[Dict[str, Any]], Any] - if compiled is INVALID_CONDITION: + if compiled is INVALID_EXPRESSION: log.error("Cannot compile expression: %s", expr, exc_info=True) return compiled @@ -91,7 +91,7 @@ def probe(_id, _type, attribs): if _type == "snapshotProbes": args = dict( probe_id=_id, - condition=_compile_condition(attribs.get("when")), + condition=_compile_expression(attribs.get("when")), active=attribs["active"], tags=dict(_.split(":", 1) for _ in attribs.get("tags", [])), ) @@ -116,6 +116,7 @@ def probe(_id, _type, attribs): line=int(attribs["where"]["lines"][0]), name=attribs["metricName"], kind=attribs["kind"], + value=_compile_expression(attribs.get("value")), ) raise ValueError("Unknown probe type: %s" % _type) diff --git a/ddtrace/internal/metrics.py b/ddtrace/internal/metrics.py index a37f6f58dc1..0edd18dc959 100644 --- a/ddtrace/internal/metrics.py +++ b/ddtrace/internal/metrics.py @@ -47,6 +47,24 @@ def increment(self, name, value=1.0, tags=None): ".".join((self.name, name)), value, [":".join(_) for _ in tags.items()] if tags else None ) + def gauge(self, name, value=1.0, tags=None): + # type: (str, float, Optional[Dict[str, str]]) -> None + if not self.metrics.enabled: + return None + + self.metrics._client.gauge( + ".".join((self.name, name)), value, [":".join(_) for _ in tags.items()] if tags else None + ) + + def histogram(self, name, value=1.0, tags=None): + # type: (str, float, Optional[Dict[str, str]]) -> None + if not self.metrics.enabled: + return None + + self.metrics._client.histogram( + ".".join((self.name, name)), value, [":".join(_) for _ in tags.items()] if tags else None + ) + def distribution(self, name, value=1.0, tags=None): # type: (str, float, Optional[Dict[str, str]]) -> None if not self.metrics.enabled: diff --git a/tests/debugging/test_debugger.py b/tests/debugging/test_debugger.py index aa4dca82f73..fff936746a1 100644 --- a/tests/debugging/test_debugger.py +++ b/tests/debugging/test_debugger.py @@ -481,25 +481,58 @@ def mock_metrics(): _probe_metrics._client = old_client -def test_debugger_metric_probe(mock_metrics): +def create_line_metric_probe(kind, value=None): + return MetricProbe( + probe_id="metric-probe-test", + source_file="tests/submod/stuff.py", + line=36, + kind=kind, + name="test.counter", + tags={"foo": "bar"}, + value=value, + ) + + +def test_debugger_metric_probe_simple_count(mock_metrics): with debugger() as d: - d.add_probes( - MetricProbe( - probe_id="metric-probe-test", - source_file="tests/submod/stuff.py", - line=36, - kind=MetricProbeKind.COUNTER, - name="test.counter", - tags={"foo": "bar"}, - ), - ) + d.add_probes(create_line_metric_probe(MetricProbeKind.COUNTER)) sleep(0.5) - Stuff().instancestuff() - assert call("probe.test.counter", 1.0, ["foo:bar"]) in mock_metrics.increment.mock_calls +def test_debugger_metric_probe_count_value(mock_metrics): + with debugger() as d: + d.add_probes(create_line_metric_probe(MetricProbeKind.COUNTER, dd_compile("#bar"))) + sleep(0.5) + Stuff().instancestuff(40) + assert call("probe.test.counter", 40.0, ["foo:bar"]) in mock_metrics.increment.mock_calls + + +def test_debugger_metric_probe_guage_value(mock_metrics): + with debugger() as d: + d.add_probes(create_line_metric_probe(MetricProbeKind.GAUGE, dd_compile("#bar"))) + sleep(0.5) + Stuff().instancestuff(41) + assert call("probe.test.counter", 41.0, ["foo:bar"]) in mock_metrics.gauge.mock_calls + + +def test_debugger_metric_probe_histogram_value(mock_metrics): + with debugger() as d: + d.add_probes(create_line_metric_probe(MetricProbeKind.HISTOGRAM, dd_compile("#bar"))) + sleep(0.5) + Stuff().instancestuff(42) + assert call("probe.test.counter", 42.0, ["foo:bar"]) in mock_metrics.histogram.mock_calls + + +def test_debugger_metric_probe_distribution_value(mock_metrics): + with debugger() as d: + d.add_probes(create_line_metric_probe(MetricProbeKind.DISTRIBUTION, dd_compile("#bar"))) + sleep(0.5) + Stuff().instancestuff(43) + assert call("probe.test.counter", 43.0, ["foo:bar"]) in mock_metrics.distribution.mock_calls + + def test_debugger_multiple_function_probes_on_same_function(): global Stuff