Repository: DataDog/dd-trace-py

Issue:
[Feature] Support the DD_SERVICE_MAPPING environment variable
The Java tracer can rename services in traces using the DD_SERVICE_MAPPING environment variable. It would be nice to have the Python tracer support it too (or a similar mechanism).


Discussion:
@jcassee thanks for the feature request.

How would you intend to use this feature in Python? Is there a service name you are currently unable to control the name of?
Yes, we currently have a couple of Java, Python and Node applications that access a Postgres database. All of the Datadog trace libraries use a different name for the Postgres service:
* Java calls it "postgresql"
* Python calls it "postgres"
* Node calls it "[application]-postgres"

I would like to consolidate them.

In the Python application we use SQLAlchemy, and the only way to change to service name it to set `Pin.override(engine, service='NAME')` in code. I would like not to have to change the code in this way. (Also, we use flask-sqlalchemy so it is not obvious to me where to place it.)
Just to add a use case here, we have a Django app which calls a postgres db 'defaultdb', and a Flask app which calls it 'postgres'. It'd be really handy to have everything under the same service.
Also interested in this. Dev teams have asked me quite a few times what the difference is between `postgres`, `defaultdb`... 
We use Python and indeed, having resource called "postgres" in APM is misleading, as we run multiple databases and application. Having a control over the name or at least defaulting to $APP-postgres would be truly appreciated!
Same in my team. One project example, we use django and multiple database connections

```python
DATABASES = {
    'default': env.db('POSTGRESQL_URL'),
    'locations-storage': env.db('LOCATIONS_DB_URL'),
}
```

```python
service_name = env.str('DD_SERVICE_NAME', default='core-api')
config.django['database_service_name_prefix'] = f'{service_name}-'
```

This is how trace looks like
![image](https://user-images.githubusercontent.com/598479/102324350-efcbcb00-3fab-11eb-8263-695e9ede7d29.png)

> Same in my team. One project example, we use django and multiple database connections
> 

Check #1887, maybe it helps you too?

Resolving patch:
diff --git a/ddtrace/settings/config.py b/ddtrace/settings/config.py
--- a/ddtrace/settings/config.py
+++ b/ddtrace/settings/config.py
@@ -62,6 +62,8 @@ def __init__(self):
         self.version = os.getenv("DD_VERSION") or self.tags.get("version")
         self.http_server.error_statuses = "500-599"
 
+        self.service_mapping = parse_tags_str(get_env("service", "mapping", default=""))
+
         # The service tag corresponds to span.service and should not be
         # included in the global tags.
         if self.service and "service" in self.tags:
diff --git a/ddtrace/tracer.py b/ddtrace/tracer.py
--- a/ddtrace/tracer.py
+++ b/ddtrace/tracer.py
@@ -433,6 +433,8 @@ def start_span(self, name, child_of=None, service=None, resource=None, span_type
             else:
                 service = config.service
 
+        mapped_service = config.service_mapping.get(service, service)
+
         if trace_id:
             # child_of a non-empty context, so either a local child span or from a remote context
             span = Span(
@@ -440,7 +442,7 @@ def start_span(self, name, child_of=None, service=None, resource=None, span_type
                 name,
                 trace_id=trace_id,
                 parent_id=parent_span_id,
-                service=service,
+                service=mapped_service,
                 resource=resource,
                 span_type=span_type,
                 _check_pid=False,
@@ -456,7 +458,7 @@ def start_span(self, name, child_of=None, service=None, resource=None, span_type
             span = Span(
                 self,
                 name,
-                service=service,
+                service=mapped_service,
                 resource=resource,
                 span_type=span_type,
                 _check_pid=False,


Test patch:
diff --git a/tests/tracer/test_global_config.py b/tests/tracer/test_global_config.py
--- a/tests/tracer/test_global_config.py
+++ b/tests/tracer/test_global_config.py
@@ -274,3 +274,11 @@ def test_dd_env(self):
         with override_env(dict(DATADOG_ENV="prod", DD_ENV="prod-staging")):
             c = Config()
             assert c.env == "prod-staging"
+
+    def test_dd_service_mapping(self):
+        c = Config()
+        assert c.service_mapping == {}
+
+        with override_env(dict(DD_SERVICE_MAPPING="foobar:bar,snafu:foo")):
+            c = Config()
+            assert c.service_mapping == {"foobar": "bar", "snafu": "foo"}
diff --git a/tests/tracer/test_tracer.py b/tests/tracer/test_tracer.py
--- a/tests/tracer/test_tracer.py
+++ b/tests/tracer/test_tracer.py
@@ -26,6 +26,7 @@
 from ddtrace.ext import system
 from ddtrace.internal.writer import AgentWriter
 from ddtrace.internal.writer import LogWriter
+from ddtrace.settings import Config
 from ddtrace.tracer import Tracer
 from ddtrace.vendor import six
 from tests import DummyTracer
@@ -34,6 +35,8 @@
 from tests import override_global_config
 from tests.subprocesstest import run_in_subprocess
 
+from .. import override_env
+
 
 def get_dummy_tracer():
     return DummyTracer()
@@ -1526,3 +1529,32 @@ def test_get_report_hostname_default(get_hostname):
     child = spans[1]
     assert root.get_tag(HOSTNAME_KEY) is None
     assert child.get_tag(HOSTNAME_KEY) is None
+
+
+def test_service_mapping():
+    @contextlib.contextmanager
+    def override_service_mapping(service_mapping):
+        with override_env(dict(DD_SERVICE_MAPPING=service_mapping)):
+            assert ddtrace.config.service_mapping == {}
+            ddtrace.config.service_mapping = Config().service_mapping
+            yield
+            ddtrace.config.service_mapping = {}
+
+    # Test single mapping
+    with override_service_mapping("foo:bar"), ddtrace.Tracer().trace("renaming", service="foo") as span:
+        assert span.service == "bar"
+
+    # Test multiple mappings
+    with override_service_mapping("foo:bar,sna:fu"), ddtrace.Tracer().trace("renaming", service="sna") as span:
+        assert span.service == "fu"
+
+    # Test colliding mappings
+    with override_service_mapping("foo:bar,foo:foobar"), ddtrace.Tracer().trace("renaming", service="foo") as span:
+        assert span.service == "foobar"
+
+    # Test invalid service mapping
+    with override_service_mapping("foo;bar,sna:fu"):
+        with ddtrace.Tracer().trace("passthru", service="foo") as _:
+            assert _.service == "foo"
+        with ddtrace.Tracer().trace("renaming", "sna") as _:
+            assert _.service == "fu"