Repository: DataDog/dd-trace-py

Issue:
Django - set_tag on span require now ALLOWED_HOSTS in version 0.34.0
### Which version of dd-trace-py are you using?
The bug apply to 0.34.0

### Which version of the libraries are you using?
django==2.2.0

You can copy/paste the output of `pip freeze` here.

### How can we reproduce your problem?
Yay, it will require a bit of work, but my case is as follows:

* I have healtcheck view configured in the application which I am using with AWS Load Balancer healtcheck (then LB knows that targets are up and healthy)
* The ALB target do not set any particular HOST headers (also that is not configurable)
* I implemented the view in the way that django ALLOWED_HOST is not needed to make it successful
* However, the ddtrace==0.34.0 due to changes in the implementation of how the tag is set on the span - broke this; 

So how can you reproduce it? Simply create a view that passes ALLOWED_HOST check but it is expected to return something. This can be done - eg. using the Middleware. 

### What is the result that you get?

I am getting 500 django error (which is totally valid) but I am not sure if this is desired behaviour. Setting up the healtcheck in any internal network is pretty hard to be done in django using ALLOWED_HOSTS (as you can not set wildcards there) and workaround are not super nice. 

### What is result that you expected?

I would expect it to pass nicely (the healtcheck)

### Where the bug is?

So in 0.34.0 this was introduced in the ddtrace/contrib/django/patch.py (it starts in line 404)

```
span.set_tag(
    http.URL,
    parse.urlunparse(
        parse.ParseResult(
            scheme=request.scheme,
            netloc=request.get_host(),  # this will include `host:port`
            path=request.path,
            params="",
            query="",
            fragment="",
        )
    ),
)
```

The biggest issue is `request.get_host()` as this fire the ALLOWED_HOST check in django:

```
    def get_host(self):
        """Return the HTTP host using the environment or request headers."""
        host = self._get_raw_host()
        # Allow variants of localhost if ALLOWED_HOSTS is empty and DEBUG=True.
        allowed_hosts = settings.ALLOWED_HOSTS
        if settings.DEBUG and not allowed_hosts:
            allowed_hosts = ['localhost', '127.0.0.1', '[::1]']
        domain, port = split_domain_port(host)
        if domain and validate_host(domain, allowed_hosts):
            return host
        else:
            msg = "Invalid HTTP_HOST header: %r." % host
            if domain:
                msg += " You may need to add %r to ALLOWED_HOSTS." % domain
            else:
                msg += " The domain name provided is not valid according to RFC 1034/1035."
            raise DisallowedHost(msg)
```

Of course raises DisallowedHost exception.


We can argue about my 'unorthodox' solution, but my bet would be that ddtrace should not rely here on django implementation - but rather introduce own host and port extractor. 

Cause - even not attached ot my particular use case - any misconfiguration in ALLOWED_HOST will not reach the datadog agent (cause 500 will be raised) and execution will be stopped (or I am wrong here?).


Discussion:
Btw. the workaround is obviously to use 0.33.0 of ddtrace now :) 

Resolving patch:
diff --git a/ddtrace/contrib/django/patch.py b/ddtrace/contrib/django/patch.py
--- a/ddtrace/contrib/django/patch.py
+++ b/ddtrace/contrib/django/patch.py
@@ -13,7 +13,6 @@
 
 from ddtrace import config, Pin
 from ddtrace.vendor import debtcollector, wrapt
-from ddtrace.compat import parse
 from ddtrace.constants import ANALYTICS_SAMPLE_RATE_KEY
 from ddtrace.contrib import func_name, dbapi
 from ddtrace.ext import http, sql as sqlx, SpanTypes
@@ -394,21 +393,7 @@ def traced_get_response(django, pin, func, instance, args, kwargs):
                 span.set_tag("http.route", route)
 
             # Set HTTP Request tags
-            # Build `http.url` tag value from request info
-            # DEV: We are explicitly omitting query strings since they may contain sensitive information
-            span.set_tag(
-                http.URL,
-                parse.urlunparse(
-                    parse.ParseResult(
-                        scheme=request.scheme,
-                        netloc=request.get_host(),  # this will include `host:port`
-                        path=request.path,
-                        params="",
-                        query="",
-                        fragment="",
-                    )
-                ),
-            )
+            span.set_tag(http.URL, utils.get_request_uri(request))
 
             response = func(*args, **kwargs)
 
diff --git a/ddtrace/contrib/django/utils.py b/ddtrace/contrib/django/utils.py
--- a/ddtrace/contrib/django/utils.py
+++ b/ddtrace/contrib/django/utils.py
@@ -1,3 +1,4 @@
+from ...compat import parse
 from ...internal.logger import get_logger
 
 
@@ -57,3 +58,36 @@ def set_tag_array(span, prefix, value):
     else:
         for i, v in enumerate(value, start=0):
             span.set_tag("{0}.{1}".format(prefix, i), v)
+
+
+def get_request_uri(request):
+    """
+    Helper to rebuild the original request url
+
+    query string or fragments are not included.
+    """
+    # DEV: Use django.http.request.HttpRequest._get_raw_host() when available
+    # otherwise back-off to PEP 333 as done in django 1.8.x
+    if hasattr(request, "_get_raw_host"):
+        host = request._get_raw_host()
+    else:
+        try:
+            # Try to build host how Django would have
+            # https://github.com/django/django/blob/e8d0d2a5efc8012dcc8bf1809dec065ebde64c81/django/http/request.py#L85-L102
+            if "HTTP_HOST" in request.META:
+                host = request.META["HTTP_HOST"]
+            else:
+                host = request.META["SERVER_NAME"]
+                port = str(request.META["SERVER_PORT"])
+                if port != ("443" if request.is_secure() else "80"):
+                    host = "{0}:{1}".format(host, port)
+        except Exception:
+            # This really shouldn't ever happen, but lets guard here just in case
+            log.debug("Failed to build Django request host", exc_info=True)
+            host = "unknown"
+
+    # Build request url from the information available
+    # DEV: We are explicitly omitting query strings since they may contain sensitive information
+    return parse.urlunparse(
+        parse.ParseResult(scheme=request.scheme, netloc=host, path=request.path, params="", query="", fragment="",)
+    )


Test patch:
diff --git a/tests/contrib/django/test_django.py b/tests/contrib/django/test_django.py
--- a/tests/contrib/django/test_django.py
+++ b/tests/contrib/django/test_django.py
@@ -1,5 +1,5 @@
 import django
-from django.test import modify_settings
+from django.test import modify_settings, override_settings
 import os
 import pytest
 
@@ -118,6 +118,17 @@ def test_v1XX_middleware(client, test_spans):
     assert span_resources == expected_resources
 
 
+def test_disallowed_host(client, test_spans):
+    with override_settings(ALLOWED_HOSTS="not-testserver"):
+        resp = client.get("/")
+        assert resp.status_code == 400
+        assert b"Bad Request (400)" in resp.content
+
+    root_span = test_spans.get_root_span()
+    assert root_span.get_tag("http.status_code") == "400"
+    assert root_span.get_tag("http.url") == "http://testserver/"
+
+
 """
 Middleware tests
 """