From 4054a100d67240c5c7a384598916cfbec47e3ca0 Mon Sep 17 00:00:00 2001 From: Jesse Gumz Date: Fri, 6 Mar 2020 17:10:34 -0500 Subject: [PATCH] Add support for DD_ENV, DD_SERVICE, and DD_VERSION to set global tags for env, service, and version --- datadog/dogstatsd/base.py | 41 +++++++++++++++----- tests/unit/dogstatsd/test_statsd.py | 58 ++++++++++++++++++++++++++++- 2 files changed, 88 insertions(+), 11 deletions(-) diff --git a/datadog/dogstatsd/base.py b/datadog/dogstatsd/base.py index 8441105d8..13d058b72 100644 --- a/datadog/dogstatsd/base.py +++ b/datadog/dogstatsd/base.py @@ -32,6 +32,14 @@ # Tag name of entity_id ENTITY_ID_TAG_NAME = "dd.internal.entity_id" +# Mapping of each "DD_" prefixed environment variable to a specific tag name +DD_ENV_TAGS_MAPPING = { + 'DD_ENTITY_ID': ENTITY_ID_TAG_NAME, + 'DD_ENV': 'env', + 'DD_SERVICE': 'service', + 'DD_VERSION': 'version', +} + # Telemetry minimum flush interval in seconds DEFAULT_TELEMETRY_MIN_FLUSH_INTERVAL = 10 @@ -56,6 +64,24 @@ def __init__(self, host=DEFAULT_HOST, port=DEFAULT_PORT, max_buffer_size=50, nam If set, it overrides default value. :type DD_DOGSTATSD_PORT: integer + :envvar DATADOG_TAGS: Tags to attach to every metric reported by dogstatsd client. + :type DATADOG_TAGS: comma-delimited string + + :envvar DD_ENTITY_ID: Tag to identify the client entity. + :type DD_ENTITY_ID: string + + :envvar DD_ENV: the env of the service running the dogstatsd client. + If set, it is appended to the constant (global) tags of the statsd client. + :type DD_ENV: string + + :envvar DD_SERVICE: the name of the service running the dogstatsd client. + If set, it is appended to the constant (global) tags of the statsd client. + :type DD_SERVICE: string + + :envvar DD_VERSION: the version of the service running the dogstatsd client. + If set, it is appended to the constant (global) tags of the statsd client. + :type DD_VERSION: string + :param host: the host of the DogStatsd server. :type host: string @@ -75,12 +101,6 @@ def __init__(self, host=DEFAULT_HOST, port=DEFAULT_PORT, max_buffer_size=50, nam :param use_ms: Report timed values in milliseconds instead of seconds (default False) :type use_ms: boolean - :envvar DATADOG_TAGS: Tags to attach to every metric reported by dogstatsd client - :type DATADOG_TAGS: list of strings - - :envvar DD_ENTITY_ID: Tag to identify the client entity. - :type DD_ENTITY_ID: string - :param use_default_route: Dynamically set the DogStatsd host to the default route (Useful when running the client in a container) (Linux only) :type use_default_route: boolean @@ -128,13 +148,14 @@ def __init__(self, host=DEFAULT_HOST, port=DEFAULT_PORT, max_buffer_size=50, nam # Options env_tags = [tag for tag in os.environ.get('DATADOG_TAGS', '').split(',') if tag] + # Inject values of DD_* environment variables as global tags. + for var, tag_name in DD_ENV_TAGS_MAPPING.items(): + value = os.environ.get(var, '') + if value: + env_tags.append('{name}:{value}'.format(name=tag_name, value=value)) if constant_tags is None: constant_tags = [] self.constant_tags = constant_tags + env_tags - entity_id = os.environ.get('DD_ENTITY_ID') - if entity_id: - entity_tag = '{name}:{value}'.format(name=ENTITY_ID_TAG_NAME, value=entity_id) - self.constant_tags.append(entity_tag) if namespace is not None: namespace = text(namespace) self.namespace = namespace diff --git a/tests/unit/dogstatsd/test_statsd.py b/tests/unit/dogstatsd/test_statsd.py index 8ae748915..6db7ae43b 100644 --- a/tests/unit/dogstatsd/test_statsd.py +++ b/tests/unit/dogstatsd/test_statsd.py @@ -27,7 +27,7 @@ from datadog.dogstatsd.context import TimedContextManagerDecorator from datadog.util.compat import is_higher_py35, is_p3k from datadog.util.config import get_version -from tests.util.contextmanagers import preserve_environment_variable +from tests.util.contextmanagers import preserve_environment_variable, EnvVars from tests.unit.dogstatsd.fixtures import load_fixtures @@ -767,6 +767,62 @@ def test_entity_tag_and_tags_from_environment_and_constant(self): tags = "country:canada,red,country:china,age:45,blue,dd.internal.entity_id:04652bb7-19b7-11e9-9cc6-42010a9c016d" assert_equal_telemetry('gt:123.4|g|#'+tags, statsd.socket.recv(), telemetry=telemetry_metrics(tags=tags)) + def test_dogstatsd_initialization_with_dd_env_service_version(self): + """ + Dogstatsd should automatically use DD_ENV, DD_SERVICE, and DD_VERSION (if present) + to set {env, service, version} as global tags for all metrics emitted. + """ + cases = [ + # Test various permutations of setting DD_* env vars, as well as other global tag configuration. + # An empty string signifies that the env var either isn't set or that it is explicitly set to empty string. + ('', '', '', '', [], []), + ('prod', '', '', '', [], ['env:prod']), + ('prod', 'dog', '', '', [], ['env:prod', 'service:dog']), + ('prod', 'dog', 'abc123', '', [], ['env:prod', 'service:dog', 'version:abc123']), + ('prod', 'dog', 'abc123', 'env:prod,type:app', [], ['env:prod', 'env:prod', 'service:dog', 'type:app', 'version:abc123']), + ('prod', 'dog', 'abc123', 'env:prod2,type:app', [], ['env:prod', 'env:prod2', 'service:dog', 'type:app', 'version:abc123']), + ('prod', 'dog', 'abc123', '', ['env:prod', 'type:app'], ['env:prod', 'env:prod', 'service:dog', 'type:app', 'version:abc123']), + ('prod', 'dog', 'abc123', '', ['env:prod2', 'type:app'], ['env:prod', 'env:prod2', 'service:dog', 'type:app', 'version:abc123']), + ('prod', 'dog', 'abc123', 'env:prod3,custom_tag:cat', ['env:prod2', 'type:app'], ['custom_tag:cat', 'env:prod', 'env:prod2', 'env:prod3', 'service:dog', 'type:app', 'version:abc123']), + ] + for c in cases: + dd_env, dd_service, dd_version, datadog_tags, constant_tags, global_tags = c + with EnvVars( + env_vars={ + 'DATADOG_TAGS': datadog_tags, + 'DD_ENV': dd_env, + 'DD_SERVICE': dd_service, + 'DD_VERSION': dd_version, + } + ): + statsd = DogStatsd(constant_tags=constant_tags, telemetry_min_flush_interval=0) + statsd.socket = FakeSocket() + + # Guarantee consistent ordering, regardless of insertion order. + statsd.constant_tags.sort() + assert global_tags == statsd.constant_tags + + # Make call with no tags passed; only the globally configured tags will be used. + global_tags_str = ','.join([t for t in global_tags]) + statsd.gauge('gt', 123.4) + assert_equal_telemetry( + # Protect against the no tags case. + 'gt:123.4|g|#{}'.format(global_tags_str) if global_tags_str else 'gt:123.4|g', + statsd.socket.recv(), + telemetry=telemetry_metrics(tags=global_tags_str) + ) + statsd._reset_telementry() + + # Make another call with local tags passed. + passed_tags = ['env:prod', 'version:def456', 'custom_tag:toad'] + all_tags_str = ','.join([t for t in passed_tags + global_tags]) + statsd.gauge('gt', 123.4, tags=passed_tags) + assert_equal_telemetry( + 'gt:123.4|g|#{}'.format(all_tags_str), + statsd.socket.recv(), + telemetry=telemetry_metrics(tags=global_tags_str) + ) + def test_gauge_doesnt_send_None(self): self.statsd.gauge('metric', None) assert self.recv() is None