diff --git a/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/constants.py b/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/constants.py index d2864c1076c..92d736c9181 100644 --- a/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/constants.py +++ b/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/constants.py @@ -4,3 +4,5 @@ USER_KEEP = 2 SAMPLE_RATE_METRIC_KEY = "_sample_rate" SAMPLING_PRIORITY_KEY = "_sampling_priority_v1" +ENV_KEY = "env" +VERSION_KEY = "version" diff --git a/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/exporter.py b/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/exporter.py index a1788b74a87..e11772d0d94 100644 --- a/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/exporter.py +++ b/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/exporter.py @@ -25,7 +25,7 @@ from opentelemetry.trace.status import StatusCanonicalCode # pylint:disable=relative-beyond-top-level -from .constants import DD_ORIGIN, SAMPLE_RATE_METRIC_KEY +from .constants import DD_ORIGIN, ENV_KEY, SAMPLE_RATE_METRIC_KEY, VERSION_KEY logger = logging.getLogger(__name__) @@ -56,16 +56,24 @@ class DatadogSpanExporter(SpanExporter): Args: agent_url: The url of the Datadog Agent or use ``DD_TRACE_AGENT_URL`` environment variable - service: The service to be used for the application or use ``DD_SERVICE`` environment variable + service: The service name to be used for the application or use ``DD_SERVICE`` environment variable + env: Set the application’s environment or use ``DD_ENV`` environment variable + version: Set the application’s version or use ``DD_VERSION`` environment variable + tags: A list of default tags to be added to every span or use ``DD_TAGS`` environment variable """ - def __init__(self, agent_url=None, service=None): + def __init__( + self, agent_url=None, service=None, env=None, version=None, tags=None + ): self.agent_url = ( agent_url if agent_url else os.environ.get("DD_TRACE_AGENT_URL", DEFAULT_AGENT_URL) ) - self.service = service if service else os.environ.get("DD_SERVICE") + self.service = service or os.environ.get("DD_SERVICE") + self.env = env or os.environ.get("DD_ENV") + self.version = version or os.environ.get("DD_VERSION") + self.tags = _parse_tags_str(tags or os.environ.get("DD_TAGS")) self._agent_writer = None @property @@ -133,6 +141,17 @@ def _translate_to_datadog(self, spans): datadog_span.set_tags(span.attributes) + # add configured env tag + if self.env is not None: + datadog_span.set_tag(ENV_KEY, self.env) + + # add configured application version tag to only root span + if self.version is not None and parent_id == 0: + datadog_span.set_tag(VERSION_KEY, self.version) + + # add configured global tags + datadog_span.set_tags(self.tags) + # add origin to root span origin = _get_origin(span) if origin and parent_id == 0: @@ -230,3 +249,35 @@ def _get_sampling_rate(span): and isinstance(span.sampler, trace_api.sampling.ProbabilitySampler) else None ) + + +def _parse_tags_str(tags_str): + """Parse a string of tags typically provided via environment variables. + + The expected string is of the form:: + "key1:value1,key2:value2" + + :param tags_str: A string of the above form to parse tags from. + :return: A dict containing the tags that were parsed. + """ + parsed_tags = {} + if not tags_str: + return parsed_tags + + for tag in tags_str.split(","): + try: + key, value = tag.split(":", 1) + + # Validate the tag + if key == "" or value == "" or value.endswith(":"): + raise ValueError + except ValueError: + logger.error( + "Malformed tag in tag pair '%s' from tag string '%s'.", + tag, + tags_str, + ) + else: + parsed_tags[key] = value + + return parsed_tags diff --git a/ext/opentelemetry-ext-datadog/tests/test_datadog_exporter.py b/ext/opentelemetry-ext-datadog/tests/test_datadog_exporter.py index d99f9db2c2a..5306d517b76 100644 --- a/ext/opentelemetry-ext-datadog/tests/test_datadog_exporter.py +++ b/ext/opentelemetry-ext-datadog/tests/test_datadog_exporter.py @@ -72,26 +72,73 @@ def test_constructor_explicit(self): """Test the constructor passing all the options.""" agent_url = "http://localhost:8126" exporter = datadog.DatadogSpanExporter( - agent_url=agent_url, service="explicit" + agent_url=agent_url, service="explicit", ) self.assertEqual(exporter.agent_url, agent_url) self.assertEqual(exporter.service, "explicit") - self.assertIsNotNone(exporter.agent_writer) + self.assertIsNone(exporter.env) + self.assertIsNone(exporter.version) + self.assertEqual(exporter.tags, {}) + + exporter = datadog.DatadogSpanExporter( + agent_url=agent_url, + service="explicit", + env="test", + version="0.0.1", + tags="", + ) + + self.assertEqual(exporter.agent_url, agent_url) + self.assertEqual(exporter.service, "explicit") + self.assertEqual(exporter.env, "test") + self.assertEqual(exporter.version, "0.0.1") + self.assertEqual(exporter.tags, {}) + + exporter = datadog.DatadogSpanExporter( + agent_url=agent_url, + service="explicit", + env="test", + version="0.0.1", + tags="team:testers,layer:app", + ) + + self.assertEqual(exporter.agent_url, agent_url) + self.assertEqual(exporter.service, "explicit") + self.assertEqual(exporter.env, "test") + self.assertEqual(exporter.version, "0.0.1") + self.assertEqual(exporter.tags, {"team": "testers", "layer": "app"}) @mock.patch.dict( "os.environ", - {"DD_TRACE_AGENT_URL": "http://agent:8126", "DD_SERVICE": "environ"}, + { + "DD_TRACE_AGENT_URL": "http://agent:8126", + "DD_SERVICE": "test-service", + "DD_ENV": "test", + "DD_VERSION": "0.0.1", + "DD_TAGS": "team:testers", + }, ) def test_constructor_environ(self): exporter = datadog.DatadogSpanExporter() self.assertEqual(exporter.agent_url, "http://agent:8126") - self.assertEqual(exporter.service, "environ") + self.assertEqual(exporter.service, "test-service") + self.assertEqual(exporter.env, "test") + self.assertEqual(exporter.version, "0.0.1") + self.assertEqual(exporter.tags, {"team": "testers"}) self.assertIsNotNone(exporter.agent_writer) # pylint: disable=too-many-locals - @mock.patch.dict("os.environ", {"DD_SERVICE": "test-service"}) + @mock.patch.dict( + "os.environ", + { + "DD_SERVICE": "test-service", + "DD_ENV": "test", + "DD_VERSION": "0.0.1", + "DD_TAGS": "team:testers", + }, + ) def test_translate_to_datadog(self): # pylint: disable=invalid-name self.maxDiff = None @@ -174,6 +221,7 @@ def test_translate_to_datadog(self): duration=durations[0], error=0, service="test-service", + meta={"env": "test", "team": "testers"}, ), dict( trace_id=trace_id_low, @@ -185,6 +233,7 @@ def test_translate_to_datadog(self): duration=durations[1], error=0, service="test-service", + meta={"env": "test", "team": "testers", "version": "0.0.1"}, ), dict( trace_id=trace_id_low, @@ -196,12 +245,12 @@ def test_translate_to_datadog(self): duration=durations[2], error=0, service="test-service", + meta={"env": "test", "team": "testers", "version": "0.0.1"}, ), ] self.assertEqual(datadog_spans, expected_spans) - @mock.patch.dict("os.environ", {"DD_SERVICE": "test-service"}) def test_export(self): """Test that agent and/or collector are invoked""" # create and save span to be used in tests