Repository: DataDog/dd-trace-py

Issue:
requests traces show up under django service as an operation instead of as its own service

We have ddtrace set up using ddtrace-run and service name set up using DD_SERVICE_NAME(?).
Each service is given an unique color for better visualisation and understanding.

<img width="482" alt="Screenshot 2021-03-23 at 2 50 10 PM" src="https://user-images.githubusercontent.com/18142532/112127010-04db7a00-8beb-11eb-8549-019610df8049.png">

Now there are external requests happening using the requests library.
Currently it shows up as service django and operation requests.request
However we want it to show up as requests and operation requests.request
so that we can color requests differently and understand how much time is spent inside django and how much in external requests from django.

<img width="1069" alt="Screenshot 2021-03-23 at 3 11 26 PM" src="https://user-images.githubusercontent.com/18142532/112127147-23da0c00-8beb-11eb-8c9f-bd76f3341499.png">

@codebreach


Discussion:
To add to this, the challenge is that the `requests` calls are showing as an operation under the service `django` however we want it to show up as its own service so that we can color it and all.

To do this we added the following code. However it doesnt seem to be doing anything

```python
from ddtrace import config

# make requests use a different service name so its easier to see in the UI
config.requests["service_name"] = "requests"
```

Resolving patch:
diff --git a/ddtrace/contrib/requests/__init__.py b/ddtrace/contrib/requests/__init__.py
--- a/ddtrace/contrib/requests/__init__.py
+++ b/ddtrace/contrib/requests/__init__.py
@@ -1,35 +1,75 @@
 """
-The ``requests`` integration traces all HTTP calls to internal or external services.
-Auto instrumentation is available using the ``patch`` function that **must be called
-before** importing the ``requests`` library. The following is an example::
+The ``requests`` integration traces all HTTP requests made with the ``requests``
+library.
+
+The default service name used is `requests` but it can be configured to match
+the services that the specific requests are made to.
+
+Enabling
+~~~~~~~~
+
+The requests integration is enabled automatically when using
+:ref:`ddtrace-run<ddtracerun>` or :ref:`patch_all()<patch_all>`.
+
+Or use :ref:`patch()<patch>` to manually enable the integration::
 
     from ddtrace import patch
     patch(requests=True)
 
-    import requests
-    requests.get("https://www.datadoghq.com")
+    # use requests like usual
 
-If you would prefer finer grained control, use a ``TracedSession`` object as you would a
-``requests.Session``::
 
-    from ddtrace.contrib.requests import TracedSession
+Global Configuration
+~~~~~~~~~~~~~~~~~~~~
 
-    session = TracedSession()
-    session.get("https://www.datadoghq.com")
+.. py:data:: ddtrace.config.requests['service']
 
-The library can be configured globally and per instance, using the Configuration API::
+   The service name reported by default for requests queries. This value will
+   be overridden by an instance override or if the split_by_domain setting is
+   enabled.
 
-    from ddtrace import config
+   This option can also be set with the ``DD_REQUESTS_SERVICE`` environment
+   variable.
+
+   Default: ``"requests"``
+
+
+.. py:data:: ddtrace.config.requests['distributed_tracing']
+
+   Whether or not to parse distributed tracing headers.
+
+   Default: ``True``
+
+
+.. py:data:: ddtrace.config.requests['trace_query_string']
 
-    # disable distributed tracing globally
-    config.requests['distributed_tracing'] = False
+   Whether or not to include the query string as a tag.
+
+   Default: ``False``
+
+
+.. py:data:: ddtrace.config.requests['split_by_domain']
+
+   Whether or not to use the domain name of requests as the service name. This
+   setting can be overridden with session overrides (described in the Instance
+   Configuration section).
+
+   Default: ``False``
+
+
+Instance Configuration
+~~~~~~~~~~~~~~~~~~~~~~
+
+To set configuration options for all requests made with a ``requests.Session`` object
+use the config API::
+
+    from ddtrace import config
+    from requests import Session
 
-    # change the service name/distributed tracing only for this session
     session = Session()
     cfg = config.get_from(session)
     cfg['service_name'] = 'auth-api'
-
-:ref:`Headers tracing <http-headers-tracing>` is supported for this integration.
+    cfg['distributed_tracing'] = False
 """
 from ...utils.importlib import require_modules
 
diff --git a/ddtrace/contrib/requests/connection.py b/ddtrace/contrib/requests/connection.py
--- a/ddtrace/contrib/requests/connection.py
+++ b/ddtrace/contrib/requests/connection.py
@@ -8,37 +8,11 @@
 from ...ext import SpanTypes
 from ...internal.logger import get_logger
 from ...propagation.http import HTTPPropagator
-from .constants import DEFAULT_SERVICE
 
 
 log = get_logger(__name__)
 
 
-def _extract_service_name(session, span, hostname=None):
-    """Extracts the right service name based on the following logic:
-    - `requests` is the default service name
-    - users can change it via `session.service_name = 'clients'`
-    - if the Span doesn't have a parent, use the set service name or fallback to the default
-    - if the Span has a parent, use the set service name or the
-    parent service value if the set service name is the default
-    - if `split_by_domain` is used, always override users settings
-    and use the network location as a service name
-
-    The priority can be represented as:
-    Updated service name > parent service name > default to `requests`.
-    """
-    cfg = config.get_from(session)
-    if cfg["split_by_domain"] and hostname:
-        return hostname
-
-    service_name = cfg["service_name"]
-    if service_name is None and span._parent is not None and span._parent.service is not None:
-        service_name = span._parent.service
-    elif service_name is None:
-        service_name = DEFAULT_SERVICE
-    return service_name
-
-
 def _wrap_send(func, instance, args, kwargs):
     """Trace the `Session.send` instance method"""
     # TODO[manu]: we already offer a way to provide the Global Tracer
@@ -60,10 +34,19 @@ def _wrap_send(func, instance, args, kwargs):
     if parsed_uri.port:
         hostname = "{}:{}".format(hostname, parsed_uri.port)
 
-    with tracer.trace("requests.request", span_type=SpanTypes.HTTP) as span:
+    cfg = config.get_from(instance)
+    service = None
+    if cfg["split_by_domain"] and hostname:
+        service = hostname
+    if service is None:
+        service = cfg.get("service", None)
+    if service is None:
+        service = cfg.get("service_name", None)
+    if service is None:
+        service = trace_utils.ext_service(None, config.requests)
+
+    with tracer.trace("requests.request", service=service, span_type=SpanTypes.HTTP) as span:
         span.set_tag(SPAN_MEASURED_KEY)
-        # update the span service name before doing any action
-        span.service = _extract_service_name(instance, span, hostname=hostname)
 
         # Configure trace search sample rate
         # DEV: analytics enabled on per-session basis
diff --git a/ddtrace/contrib/requests/patch.py b/ddtrace/contrib/requests/patch.py
--- a/ddtrace/contrib/requests/patch.py
+++ b/ddtrace/contrib/requests/patch.py
@@ -18,6 +18,7 @@
     {
         "distributed_tracing": asbool(get_env("requests", "distributed_tracing", default=True)),
         "split_by_domain": asbool(get_env("requests", "split_by_domain", default=False)),
+        "_default_service": "requests",
     },
 )
 


Test patch:
diff --git a/tests/contrib/requests/test_requests.py b/tests/contrib/requests/test_requests.py
--- a/tests/contrib/requests/test_requests.py
+++ b/tests/contrib/requests/test_requests.py
@@ -237,16 +237,25 @@ def test_user_set_service_name(self):
         cfg["service_name"] = "clients"
         out = self.session.get(URL_200)
         assert out.status_code == 200
-
         spans = self.pop_spans()
         assert len(spans) == 1
         s = spans[0]
+        assert s.service == "clients"
 
+    def test_user_set_service(self):
+        # ensure a service name set by the user has precedence
+        cfg = config.get_from(self.session)
+        cfg["service"] = "clients"
+        out = self.session.get(URL_200)
+        assert out.status_code == 200
+        spans = self.pop_spans()
+        assert len(spans) == 1
+        s = spans[0]
         assert s.service == "clients"
 
     def test_parent_service_name_precedence(self):
-        # ensure the parent service name has precedence if the value
-        # is not set by the user
+        # span should not inherit the parent's service name
+        # as all outbound requests should go to a difference service
         with self.tracer.trace("parent.span", service="web"):
             out = self.session.get(URL_200)
             assert out.status_code == 200
@@ -256,7 +265,7 @@ def test_parent_service_name_precedence(self):
         s = spans[1]
 
         assert s.name == "requests.request"
-        assert s.service == "web"
+        assert s.service == "requests"
 
     def test_parent_without_service_name(self):
         # ensure the default value is used if the parent
@@ -367,6 +376,34 @@ def test_split_by_domain_includes_port_path(self):
 
         assert s.service == "httpbin.org:80"
 
+    @TracerTestCase.run_in_subprocess(env_overrides=dict(DD_REQUESTS_SERVICE="override"))
+    def test_global_config_service_env_precedence(self):
+        out = self.session.get(URL_200)
+        assert out.status_code == 200
+        spans = self.pop_spans()
+        assert spans[0].service == "override"
+
+        cfg = config.get_from(self.session)
+        cfg["service"] = "override2"
+        out = self.session.get(URL_200)
+        assert out.status_code == 200
+        spans = self.pop_spans()
+        assert spans[0].service == "override2"
+
+    @TracerTestCase.run_in_subprocess(env_overrides=dict(DD_REQUESTS_SERVICE="override"))
+    def test_global_config_service_env(self):
+        out = self.session.get(URL_200)
+        assert out.status_code == 200
+        spans = self.pop_spans()
+        assert spans[0].service == "override"
+
+    def test_global_config_service(self):
+        with self.override_config("requests", dict(service="override")):
+            out = self.session.get(URL_200)
+        assert out.status_code == 200
+        spans = self.pop_spans()
+        assert spans[0].service == "override"
+
     def test_200_ot(self):
         """OpenTracing version of test_200."""