Repository: DataDog/dd-trace-py

Issue:
flask view_args doesnt support non-string arguments
flask view_args doesnt support non-string arguments even though flask allows you to create non-string url parameters.
The code here: https://github.com/DataDog/dd-trace-py/blob/master/ddtrace/contrib/flask/patch.py#L484 assumes all `view_args` will be strings.

### Which version of dd-trace-py are you using?
0.51.1

### Which version of pip are you using?
21

### Which version of the libraries are you using?
flask 1.1.4

### How can we reproduce your problem?
Create a route in a flask app that doesnt use string parameters. more documentation here: https://flask.palletsprojects.com/en/1.1.x/api/#url-route-registrations

### What is the result that you get?
The parameter is defined as follows:
`@app.route("/wallets/<int:container_type_value>")`
```
Aug 09 11:59:31 coin-tracker-stefan-ct--gpvg7l app/web.1 2021-08-09 18:59:31,173 WARNING [ddtrace.span] [span.py:326] [dd.trace_id=1555487579007622952 dd.span_id=3425534705218378124] - Failed to set text tag 'flask.view_args.container_type_value', 1 additional messages skipped
Aug 09 11:59:31 coin-tracker-stefan-ct--gpvg7l app/web.1 Traceback (most recent call last):
Aug 09 11:59:31 coin-tracker-stefan-ct--gpvg7l app/web.1   File "/app/.heroku/python/lib/python3.7/site-packages/ddtrace/span.py", line 322, in _set_str_tag
Aug 09 11:59:31 coin-tracker-stefan-ct--gpvg7l app/web.1     self.meta[key] = ensure_text(value, errors="replace")
Aug 09 11:59:31 coin-tracker-stefan-ct--gpvg7l app/web.1   File "/app/.heroku/python/lib/python3.7/site-packages/six.py", line 955, in ensure_text
Aug 09 11:59:31 coin-tracker-stefan-ct--gpvg7l app/web.1     raise TypeError("not expecting type '%s'" % type(s))
Aug 09 11:59:31 coin-tracker-stefan-ct--gpvg7l app/web.1 TypeError: not expecting type '<class 'int'>'
```
### What is the result that you expected?
No exceptions and no warning log.

Thanks
flask view_args doesnt support non-string arguments
flask view_args doesnt support non-string arguments even though flask allows you to create non-string url parameters.
The code here: https://github.com/DataDog/dd-trace-py/blob/master/ddtrace/contrib/flask/patch.py#L484 assumes all `view_args` will be strings.

### Which version of dd-trace-py are you using?
0.51.1

### Which version of pip are you using?
21

### Which version of the libraries are you using?
flask 1.1.4

### How can we reproduce your problem?
Create a route in a flask app that doesnt use string parameters. more documentation here: https://flask.palletsprojects.com/en/1.1.x/api/#url-route-registrations

### What is the result that you get?
The parameter is defined as follows:
`@app.route("/wallets/<int:container_type_value>")`
```
Aug 09 11:59:31 coin-tracker-stefan-ct--gpvg7l app/web.1 2021-08-09 18:59:31,173 WARNING [ddtrace.span] [span.py:326] [dd.trace_id=1555487579007622952 dd.span_id=3425534705218378124] - Failed to set text tag 'flask.view_args.container_type_value', 1 additional messages skipped
Aug 09 11:59:31 coin-tracker-stefan-ct--gpvg7l app/web.1 Traceback (most recent call last):
Aug 09 11:59:31 coin-tracker-stefan-ct--gpvg7l app/web.1   File "/app/.heroku/python/lib/python3.7/site-packages/ddtrace/span.py", line 322, in _set_str_tag
Aug 09 11:59:31 coin-tracker-stefan-ct--gpvg7l app/web.1     self.meta[key] = ensure_text(value, errors="replace")
Aug 09 11:59:31 coin-tracker-stefan-ct--gpvg7l app/web.1   File "/app/.heroku/python/lib/python3.7/site-packages/six.py", line 955, in ensure_text
Aug 09 11:59:31 coin-tracker-stefan-ct--gpvg7l app/web.1     raise TypeError("not expecting type '%s'" % type(s))
Aug 09 11:59:31 coin-tracker-stefan-ct--gpvg7l app/web.1 TypeError: not expecting type '<class 'int'>'
```
### What is the result that you expected?
No exceptions and no warning log.

Thanks


Discussion:

Resolving patch:
diff --git a/ddtrace/contrib/flask/patch.py b/ddtrace/contrib/flask/patch.py
--- a/ddtrace/contrib/flask/patch.py
+++ b/ddtrace/contrib/flask/patch.py
@@ -481,7 +481,9 @@ def _traced_request(pin, wrapped, instance, args, kwargs):
 
             if not span.get_tag(FLASK_VIEW_ARGS) and request.view_args and config.flask.get("collect_view_args"):
                 for k, v in request.view_args.items():
-                    span._set_str_tag(u".".join((FLASK_VIEW_ARGS, k)), v)
+                    # DEV: Do not use `_set_str_tag` here since view args can be string/int/float/path/uuid/etc
+                    #      https://flask.palletsprojects.com/en/1.1.x/api/#url-route-registrations
+                    span.set_tag(u".".join((FLASK_VIEW_ARGS, k)), v)
         except Exception:
             log.debug('failed to set tags for "flask.request" span', exc_info=True)
 


Test patch:
diff --git a/tests/contrib/flask/test_request.py b/tests/contrib/flask/test_request.py
--- a/tests/contrib/flask/test_request.py
+++ b/tests/contrib/flask/test_request.py
@@ -1,6 +1,9 @@
 # -*- coding: utf-8 -*-
+import json
+
 import flask
 from flask import abort
+from flask import jsonify
 from flask import make_response
 
 from ddtrace.constants import ANALYTICS_SAMPLE_RATE_KEY
@@ -82,6 +85,50 @@ def index():
         self.assertEqual(handler_span.resource, "/")
         self.assertEqual(req_span.error, 0)
 
+    def test_route_params_request(self):
+        """
+        When making a request to an endpoint with non-string url params
+            We create the expected spans
+        """
+
+        @self.app.route("/route_params/<first>/<int:second>/<float:third>/<path:fourth>")
+        def route_params(first, second, third, fourth):
+            return jsonify(
+                {
+                    "first": first,
+                    "second": second,
+                    "third": third,
+                    "fourth": fourth,
+                }
+            )
+
+        res = self.client.get("/route_params/test/100/5.5/some/sub/path")
+        self.assertEqual(res.status_code, 200)
+        if isinstance(res.data, bytes):
+            data = json.loads(res.data.decode())
+        else:
+            data = json.loads(res.data)
+
+        assert data == {
+            "first": "test",
+            "second": 100,
+            "third": 5.5,
+            "fourth": "some/sub/path",
+        }
+
+        spans = self.get_spans()
+        self.assertEqual(len(spans), 8)
+
+        root = spans[0]
+        assert root.name == "flask.request"
+        assert root.get_tag(http.URL) == "http://localhost/route_params/test/100/5.5/some/sub/path"
+        assert root.get_tag("flask.endpoint") == "route_params"
+        assert root.get_tag("flask.url_rule") == "/route_params/<first>/<int:second>/<float:third>/<path:fourth>"
+        assert root.get_tag("flask.view_args.first") == "test"
+        assert root.get_metric("flask.view_args.second") == 100.0
+        assert root.get_metric("flask.view_args.third") == 5.5
+        assert root.get_tag("flask.view_args.fourth") == "some/sub/path"
+
     def test_request_query_string_trace(self):
         """Make sure when making a request that we create the expected spans and capture the query string."""