Repository: open-telemetry/opentelemetry-python

Issue:
Tracing decorator for functions
**Is your feature request related to a problem?**
Tracing every use of a function is prone to error. It is very hard to do in some contexts, such as functions from a third party library that don't integrate with OTel, without lots of manual span management.

**Describe the solution you'd like**
I would like to have a first party tracing decorator.

**Describe alternatives you've considered**
I have rolled my own for now:
```python
import functools
from contextlib import nullcontext
from typing import Callable

from opentelemetry import trace
from opentelemetry.trace import NonRecordingSpan


def traced(name: str) -> Callable:
    """Wrap a function in an OTel span."""

    def decorator(func: Callable):
        @functools.wraps(func)
        def wraps(*args, **kwargs):
            if isinstance(trace.get_current_span(), NonRecordingSpan):
                span = nullcontext()
            else:
                span = trace.get_tracer(func.__module__).start_as_current_span(name)
            
            with span:
                return func(*args, **kwargs)

        return wraps

    return decorator
```

**Additional context**
Honeycomb does something similar in their beeline module with the traced decorator.



Discussion:
You could achieve same with this `@tracer.start_as_current_span(name)`.

```python
@tracer.start_as_current_span("name")
def say_hello():
  print("Hello!")

if __name__ == "__main__":
  hello()
```
We should probably clarify this in our documentation, at least in the API doc

https://github.com/open-telemetry/opentelemetry-python/blob/faf8868f7a3d7f64ea3214cf6f201bb95d8ec513/opentelemetry-api/src/opentelemetry/trace/__init__.py#L343
@aabmass What is @tracer here? Is `tracer` the return from `trace.get_tracer(...)`. 

A convenience might be better around this, as it seems like the one-line decorator here would be 
`@trace.get_tracer(__name__).start_as_current_span("some_name")` which is very verbose for something that seems like it should be a common use case
> Is tracer the return from trace.get_tracer(...).

Yes. Typically you would hold the tracer as a package, module, or class level variable and use that throughout your code. We don't want to make any assumptions about what the tracer's name is.
I had the same need so I created this extension package for OpenTelemetry:
https://pypi.org/project/otel-extensions/

Very much open to feedback but would love to equivalent features integrated into the `opentelemetry-api` or `opentelemetry-sdk` library.

Resolving patch:
diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py
--- a/opentelemetry-api/src/opentelemetry/trace/__init__.py
+++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py
@@ -371,6 +371,14 @@ def start_as_current_span(
             with opentelemetry.trace.use_span(span, end_on_exit=True):
                 do_work()
 
+        This can also be used as a decorator::
+
+            @tracer.start_as_current_span("name"):
+            def function():
+                ...
+
+            function()
+
         Args:
             name: The name of the span to be created.
             context: An optional Context containing the span's parent. Defaults to the


Test patch:
diff --git a/opentelemetry-api/tests/trace/test_tracer.py b/opentelemetry-api/tests/trace/test_tracer.py
--- a/opentelemetry-api/tests/trace/test_tracer.py
+++ b/opentelemetry-api/tests/trace/test_tracer.py
@@ -12,25 +12,59 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-import unittest
 
-from opentelemetry import trace
+from contextlib import contextmanager
+from unittest import TestCase
+from unittest.mock import Mock
 
+from opentelemetry.trace import (
+    INVALID_SPAN,
+    NoOpTracer,
+    Span,
+    Tracer,
+    get_current_span,
+)
 
-class TestTracer(unittest.TestCase):
+
+class TestTracer(TestCase):
     def setUp(self):
-        self.tracer = trace.NoOpTracer()
+        self.tracer = NoOpTracer()
 
     def test_start_span(self):
         with self.tracer.start_span("") as span:
-            self.assertIsInstance(span, trace.Span)
+            self.assertIsInstance(span, Span)
 
-    def test_start_as_current_span(self):
+    def test_start_as_current_span_context_manager(self):
         with self.tracer.start_as_current_span("") as span:
-            self.assertIsInstance(span, trace.Span)
+            self.assertIsInstance(span, Span)
+
+    def test_start_as_current_span_decorator(self):
+
+        mock_call = Mock()
+
+        class MockTracer(Tracer):
+            def start_span(self, *args, **kwargs):
+                return INVALID_SPAN
+
+            @contextmanager
+            def start_as_current_span(self, *args, **kwargs):  # type: ignore
+                mock_call()
+                yield INVALID_SPAN
+
+        mock_tracer = MockTracer()
+
+        @mock_tracer.start_as_current_span("name")
+        def function():  # type: ignore
+            pass
+
+        function()  # type: ignore
+        function()  # type: ignore
+        function()  # type: ignore
+
+        self.assertEqual(mock_call.call_count, 3)
 
     def test_get_current_span(self):
         with self.tracer.start_as_current_span("test") as span:
-            trace.get_current_span().set_attribute("test", "test")
-            self.assertEqual(span, trace.INVALID_SPAN)
+            get_current_span().set_attribute("test", "test")
+            self.assertEqual(span, INVALID_SPAN)
             self.assertFalse(hasattr("span", "attributes"))