Repository: open-telemetry/opentelemetry-python

Issue:
Batch Span Processor does not work with uwsgi
Recently support for forking web servers landed in Batch Span Processor in https://github.com/open-telemetry/opentelemetry-python/pull/2242

I tested this and it works with gunicorn but does not work with uwsgi. 


Given a file called `myapp.py`:


```python
from flask import Flask

app = Flask(__name__)

@app.route("/")
def hello():
    return "Hello World"
```

The following command works reports spans
```
opentelemetry-instrument gunicorn -b 127.0.0.1:8000 --threads 2 --workers 4 myapp:app
```

While as this does not
```
opentelemetry-instrument uwsgi --http 127.0.0.1:8000 --wsgi-file myapp.py --callable app --master --processes 2 --enable-threads
```

I added some debug statements to the fork hook and it seems the hook is called with gunicorn but not with uwsgi. 

We should look into how uwsgi works and confirm whether the solution work with it or not.


Discussion:
//cc @lonewolf3739 
Also tested with Celery and it does not work when worker's `--pool` CLI option is set to `threads` or `gevent`. 
That change wasn't effective for uwsgi since it's worker proc management implementation is in C. I might have overlooked it when verifying, my apologies. We can probably make use of pid at the time of instantiation and processing to reinitialise. Can you share the more details about how you tested celery? I tried to set the option to given values but was able to see the spans emitted.
No worries at all. I just wanted to document what works and what doesn't somewhere so we can track it and patch it at some point.
On celery:

#### `tasks.py`
```python
from celery import Celery

app = Celery('tasks', broker='redis://localhost/1')

@app.task
def add(x, y):
    return x + y
```

#### `producer.py`
```python
from tasks import add

add.delay(4, 4)
```

Producing jobs always works obviously:

```
opentelemetry-instrument python producer.py
```

Consuming messages works with the following command:

```
opentelemetry-instrument celery -A tasks worker --loglevel=INFO -c 4
```

But does not work with either of the following:

```
opentelemetry-instrument celery -A tasks worker --loglevel=INFO -c 4 --pool gevent
```

```
opentelemetry-instrument celery -A tasks worker --loglevel=INFO -c 4 --pool threads
```
@lonewolf3739 My bad. Celery actually works but only if I remove this special case from the sitecustomize file: https://github.com/open-telemetry/opentelemetry-python-contrib/blob/main/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/sitecustomize.py#L128-L141 

Resolving patch:
diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/export/__init__.py
--- a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/export/__init__.py
+++ b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/export/__init__.py
@@ -24,6 +24,7 @@
 
 from opentelemetry.context import attach, detach, set_value
 from opentelemetry.sdk._logs import LogData, LogProcessor, LogRecord
+from opentelemetry.util._once import Once
 from opentelemetry.util._time import _time_ns
 
 _logger = logging.getLogger(__name__)
@@ -129,6 +130,9 @@ def __init__(self):
         self.num_log_records = 0
 
 
+_BSP_RESET_ONCE = Once()
+
+
 class BatchLogProcessor(LogProcessor):
     """This is an implementation of LogProcessor which creates batches of
     received logs in the export-friendly LogData representation and
@@ -164,6 +168,7 @@ def __init__(
             os.register_at_fork(
                 after_in_child=self._at_fork_reinit
             )  # pylint: disable=protected-access
+        self._pid = os.getpid()
 
     def _at_fork_reinit(self):
         self._condition = threading.Condition(threading.Lock())
@@ -174,6 +179,7 @@ def _at_fork_reinit(self):
             daemon=True,
         )
         self._worker_thread.start()
+        self._pid = os.getpid()
 
     def worker(self):
         timeout = self._schedule_delay_millis / 1e3
@@ -293,6 +299,9 @@ def emit(self, log_data: LogData) -> None:
         """
         if self._shutdown:
             return
+        if self._pid != os.getpid():
+            _BSP_RESET_ONCE.do_once(self._at_fork_reinit)
+
         self._queue.appendleft(log_data)
         if len(self._queue) >= self._max_export_batch_size:
             with self._condition:
diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py
--- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py
+++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py
@@ -36,6 +36,7 @@
     OTEL_BSP_SCHEDULE_DELAY,
 )
 from opentelemetry.sdk.trace import ReadableSpan, Span, SpanProcessor
+from opentelemetry.util._once import Once
 from opentelemetry.util._time import _time_ns
 
 logger = logging.getLogger(__name__)
@@ -119,6 +120,9 @@ def __init__(self):
         self.num_spans = 0
 
 
+_BSP_RESET_ONCE = Once()
+
+
 class BatchSpanProcessor(SpanProcessor):
     """Batch span processor implementation.
 
@@ -203,6 +207,7 @@ def __init__(
             os.register_at_fork(
                 after_in_child=self._at_fork_reinit
             )  # pylint: disable=protected-access
+        self._pid = os.getpid()
 
     def on_start(
         self, span: Span, parent_context: typing.Optional[Context] = None
@@ -215,6 +220,9 @@ def on_end(self, span: ReadableSpan) -> None:
             return
         if not span.context.trace_flags.sampled:
             return
+        if self._pid != os.getpid():
+            _BSP_RESET_ONCE.do_once(self._at_fork_reinit)
+
         if len(self.queue) == self.max_queue_size:
             if not self._spans_dropped:
                 logger.warning("Queue is full, likely spans will be dropped.")
@@ -236,6 +244,7 @@ def _at_fork_reinit(self):
             name="OtelBatchSpanProcessor", target=self.worker, daemon=True
         )
         self.worker_thread.start()
+        self._pid = os.getpid()
 
     def worker(self):
         timeout = self.schedule_delay_millis / 1e3


Test patch:
diff --git a/opentelemetry-sdk/tests/logs/test_export.py b/opentelemetry-sdk/tests/logs/test_export.py
--- a/opentelemetry-sdk/tests/logs/test_export.py
+++ b/opentelemetry-sdk/tests/logs/test_export.py
@@ -16,7 +16,6 @@
 import logging
 import multiprocessing
 import os
-import sys
 import time
 import unittest
 from concurrent.futures import ThreadPoolExecutor
@@ -44,8 +43,6 @@
 from opentelemetry.trace import TraceFlags
 from opentelemetry.trace.span import INVALID_SPAN_CONTEXT
 
-supports_register_at_fork = hasattr(os, "fork") and sys.version_info >= (3, 7)
-
 
 class TestSimpleLogProcessor(unittest.TestCase):
     def test_simple_log_processor_default_level(self):
@@ -274,9 +271,9 @@ def bulk_log_and_flush(num_logs):
         finished_logs = exporter.get_finished_logs()
         self.assertEqual(len(finished_logs), 2415)
 
-    @unittest.skipIf(
-        not supports_register_at_fork,
-        "needs *nix and minor version 7 or later",
+    @unittest.skipUnless(
+        hasattr(os, "fork"),
+        "needs *nix",
     )
     def test_batch_log_processor_fork(self):
         # pylint: disable=invalid-name
diff --git a/opentelemetry-sdk/tests/trace/export/test_export.py b/opentelemetry-sdk/tests/trace/export/test_export.py
--- a/opentelemetry-sdk/tests/trace/export/test_export.py
+++ b/opentelemetry-sdk/tests/trace/export/test_export.py
@@ -14,7 +14,6 @@
 
 import multiprocessing
 import os
-import sys
 import threading
 import time
 import unittest
@@ -369,8 +368,8 @@ def _check_fork_trace(self, exporter, expected):
             self.assertIn(span.name, expected)
 
     @unittest.skipUnless(
-        hasattr(os, "fork") and sys.version_info >= (3, 7),
-        "needs *nix and minor version 7 or later",
+        hasattr(os, "fork"),
+        "needs *nix",
     )
     def test_batch_span_processor_fork(self):
         # pylint: disable=invalid-name