Repository: DataDog/dd-trace-py

Issue:
werkzeug.exceptions.NotFound logged as error
Just recently started using ddtrace for APM, and perhaps I missed some configuration. If so, I apologize.

### Which version of dd-trace-py are you using?

0.46.0

### Which version of pip are you using?

21.0.1

### Which version of the libraries are you using?

datadog==0.41.0
ddtrace==0.46.0
Flask==1.1.2
Flask-RESTful==0.3.8
itsdangerous==1.1.0
Jinja2==2.11.3
MarkupSafe==1.1.1
Werkzeug==1.0.1

### How can we reproduce your problem?

Make a request to an endpoint that isn't handled by any Flask route.

### What is the result that you get?

An error logged to the trace for the `Flask.dispatch_request` operation like so:

```
werkzeug.exceptions.NotFound: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.
Traceback (most recent call last):
File "/usr/lib/python3.8/site-packages/ddtrace/contrib/flask/patch.py", line 490, in _traced_request
return wrapped(*args, **kwargs)
File "/usr/lib/python3.8/site-packages/flask/app.py", line 1926, in dispatch_request
self.raise_routing_exception(req)
File "/usr/lib/python3.8/site-packages/flask/app.py", line 1908, in raise_routing_exception
raise request.routing_exception
File "/usr/lib/python3.8/site-packages/flask/ctx.py", line 350, in match_request
result = self.url_adapter.match(return_rule=True)
File "/usr/lib/python3.8/site-packages/werkzeug/routing.py", line 1945, in match
raise NotFound()
werkzeug.exceptions.NotFound: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.
```

### What is the result that you expected?

For 404 requests not to result in a server error trace log. The `flask.request` operation is not logged as an error because at that point it knows the status code, but in `flask.dispatch_request` the `werkzeug.excpetions.NotFound` is raised and ddtrace thinks it is a server error.

Similarly, I would hope that other 4xx level werkzeug exceptions are also not logged as errors.



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
@@ -485,7 +485,10 @@ def _traced_request(pin, wrapped, instance, args, kwargs):
         except Exception:
             log.debug('failed to set tags for "flask.request" span', exc_info=True)
 
-        with pin.tracer.trace(".".join(("flask", name)), service=trace_utils.int_service(pin, config.flask, pin)):
+        with pin.tracer.trace(
+            ".".join(("flask", name)), service=trace_utils.int_service(pin, config.flask, pin)
+        ) as request_span:
+            request_span._ignore_exception(werkzeug.exceptions.NotFound)
             return wrapped(*args, **kwargs)
 
     return _traced_request


Test patch:
diff --git a/tests/contrib/flask/test_errorhandler.py b/tests/contrib/flask/test_errorhandler.py
--- a/tests/contrib/flask/test_errorhandler.py
+++ b/tests/contrib/flask/test_errorhandler.py
@@ -30,13 +30,10 @@ def test_default_404_handler(self):
         self.assertIsNone(req_span.get_tag("flask.url_rule"))
 
         # flask.dispatch_request span
-        self.assertEqual(dispatch_span.error, 1)
-        error_msg = dispatch_span.get_tag("error.msg")
-        self.assertTrue(error_msg.startswith("404 Not Found"))
-        error_stack = dispatch_span.get_tag("error.stack")
-        self.assertTrue(error_stack.startswith("Traceback (most recent call last):"))
-        error_type = dispatch_span.get_tag("error.type")
-        self.assertEqual(error_type, "werkzeug.exceptions.NotFound")
+        self.assertEqual(dispatch_span.error, 0)
+        self.assertIsNone(dispatch_span.get_tag("error.msg"))
+        self.assertIsNone(dispatch_span.get_tag("error.stack"))
+        self.assertIsNone(dispatch_span.get_tag("error.type"))
 
         # flask.handle_user_exception span
         self.assertEqual(user_ex_span.meta, dict())
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
@@ -463,10 +463,10 @@ def test_request_404(self):
         self.assertEqual(dispatch_span.service, "flask")
         self.assertEqual(dispatch_span.name, "flask.dispatch_request")
         self.assertEqual(dispatch_span.resource, "flask.dispatch_request")
-        self.assertEqual(dispatch_span.error, 1)
-        self.assertTrue(dispatch_span.get_tag("error.msg").startswith("404 Not Found"))
-        self.assertTrue(dispatch_span.get_tag("error.stack").startswith("Traceback"))
-        self.assertEqual(dispatch_span.get_tag("error.type"), "werkzeug.exceptions.NotFound")
+        self.assertEqual(dispatch_span.error, 0)
+        self.assertIsNone(dispatch_span.get_tag("error.msg"))
+        self.assertIsNone(dispatch_span.get_tag("error.stack"))
+        self.assertIsNone(dispatch_span.get_tag("error.type"))
 
     def test_request_abort_404(self):
         """
@@ -529,10 +529,10 @@ def not_found():
         self.assertEqual(dispatch_span.service, "flask")
         self.assertEqual(dispatch_span.name, "flask.dispatch_request")
         self.assertEqual(dispatch_span.resource, "flask.dispatch_request")
-        self.assertEqual(dispatch_span.error, 1)
-        self.assertTrue(dispatch_span.get_tag("error.msg").startswith("404 Not Found"))
-        self.assertTrue(dispatch_span.get_tag("error.stack").startswith("Traceback"))
-        self.assertEqual(dispatch_span.get_tag("error.type"), "werkzeug.exceptions.NotFound")
+        self.assertEqual(dispatch_span.error, 0)
+        self.assertIsNone(dispatch_span.get_tag("error.msg"))
+        self.assertIsNone(dispatch_span.get_tag("error.stack"))
+        self.assertIsNone(dispatch_span.get_tag("error.type"))
 
         # Handler span
         handler_span = spans[4]