Repository: DataDog/dd-trace-py

Issue:
Jinja2: Handle non-string template names as span resources
Jinja2 template names may not be strings, but need to be serialized as strings prior to being set as a span resource. Problem areas can be found in `ddtrace/contrib/jinja2/patch.py` on lines 63, 81 and 97 where the value from Jinja2 is trusted to be a string, but may not be.

https://github.com/DataDog/dd-trace-py/blob/fb8dfa2f33fff37d21df9728d8386c0260df9744/ddtrace/contrib/jinja2/patch.py#L51-L100


Here is an example of the exception we are seeing when `span.finish` is called:

```
  File "/opt/venv/lib/python3.8/site-packages/ddtrace/span.py", line 237, in finish
    cb(self)
  File "/opt/venv/lib/python3.8/site-packages/ddtrace/tracer.py", line 712, in _on_span_finish
    p.on_span_finish(span)
  File "/opt/venv/lib/python3.8/site-packages/ddtrace/internal/processor/trace.py", line 208, in on_span_finish
    self._writer.write(spans)
  File "/opt/venv/lib/python3.8/site-packages/ddtrace/internal/writer.py", line 518, in write
    self._encoder.put(spans)
  File "ddtrace/internal/_encoding.pyx", line 456, in ddtrace.internal._encoding.MsgpackEncoderBase.put
  File "ddtrace/internal/_encoding.pyx", line 460, in ddtrace.internal._encoding.MsgpackEncoderBase.put
  File "ddtrace/internal/_encoding.pyx", line 483, in ddtrace.internal._encoding.MsgpackEncoderBase.put
  File "ddtrace/internal/_encoding.pyx", line 464, in ddtrace.internal._encoding.MsgpackEncoderBase.put
  File "ddtrace/internal/_encoding.pyx", line 451, in ddtrace.internal._encoding.MsgpackEncoderBase._pack_trace
  File "ddtrace/internal/_encoding.pyx", line 600, in ddtrace.internal._encoding.MsgpackEncoderV03.pack_span
  File "ddtrace/internal/_encoding.pyx", line 142, in ddtrace.internal._encoding.pack_text
TypeError: Unhandled text type: <class 'int'>
```

Relevant pip dependencies:
datadogpy == 0.44.0
dd-trace-py == 1.2.3
jinja2 == 3.0.3



Discussion:
hey @TimothyLoyer, thanks for reaching out.

Do you have a reproduction/example of when the template name is not a string? I had not seen that before, and want to make sure your specific use-case is covered by any fix
> Do you have a reproduction/example of when the template name is not a string? I had not seen that before, and want to make sure your specific use-case is covered by any fix

In this case, it was caused by moto's usage of Jinja2, where they set the template name as the memory id (i.e. using Python's `id(obj)` builtin function). I have opened a ticket for moto as well, as Jinja2 has made it clear in their documentation (after v3.0) and code-hinting that this value ought to be a string.

We are using `moto.mock_s3` in some unit tests. We are patching a boto3 s3 client with the mock from moto, and then creating a fake bucket to store data during testing.
@TimothyLoyer thanks! that is helpful

Resolving patch:
diff --git a/ddtrace/contrib/jinja2/patch.py b/ddtrace/contrib/jinja2/patch.py
--- a/ddtrace/contrib/jinja2/patch.py
+++ b/ddtrace/contrib/jinja2/patch.py
@@ -7,6 +7,7 @@
 
 from ...constants import SPAN_MEASURED_KEY
 from ...ext import SpanTypes
+from ...internal.compat import stringify
 from ...internal.utils import ArgumentError
 from ...internal.utils import get_argument_value
 from ...pin import Pin
@@ -54,7 +55,7 @@ def _wrap_render(wrapped, instance, args, kwargs):
     if not pin or not pin.enabled():
         return wrapped(*args, **kwargs)
 
-    template_name = instance.name or DEFAULT_TEMPLATE_NAME
+    template_name = stringify(instance.name or DEFAULT_TEMPLATE_NAME)
     with pin.tracer.trace("jinja2.render", pin.service, span_type=SpanTypes.TEMPLATE) as span:
         span.set_tag(SPAN_MEASURED_KEY)
         try:


Test patch:
diff --git a/tests/contrib/jinja2/test_jinja2.py b/tests/contrib/jinja2/test_jinja2.py
--- a/tests/contrib/jinja2/test_jinja2.py
+++ b/tests/contrib/jinja2/test_jinja2.py
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
 import os.path
 
 # 3rd party
@@ -68,6 +69,32 @@ def test_generate_inline_template(self):
         assert spans[1].name == "jinja2.render"
         assert_is_measured(spans[1])
 
+    def test_custom_template_name(self):
+        """
+        Regression test for #4008
+
+        https://github.com/DataDog/dd-trace-py/issues/4008
+        """
+        # pytest.mark.parametrize is not supported for unittest test methods
+        cases = [
+            ("custom-name", "custom-name"),
+            (1, "1"),
+            (u"😐", u"😐"),
+        ]
+        for template_name, expected in cases:
+            t = jinja2.environment.Template("Hello {{name}}!")
+            t.name = template_name
+            assert "".join(t.generate(name="Jinja")) == "Hello Jinja!"
+
+            # tests
+            spans = self.pop_spans()
+            assert len(spans) == 2
+
+            render_span = spans[1]
+            assert render_span.name == "jinja2.render"
+            assert render_span.get_tag("jinja2.template_name") == expected
+            assert render_span.resource == expected
+
     def test_file_template(self):
         loader = jinja2.loaders.FileSystemLoader(TMPL_DIR)
         env = jinja2.Environment(loader=loader)