open-telemetry / open-telemetry/opentelemetry-python

opentelemetry python sdk does not work well under fork

Open
#4,215 3 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug
Dominant language
Python
Stars
2.6k
Forks
1k
Avg merge
4d 15h
Merged PRs (30d)
19

Description

Describe your environment

OS: (Ubuntu 22.04)
Python version: (Python 3.12)
SDK version: (1.26.0)
API version: (1.26.0)

What happened?

i wrote the code below:

import os
from opentelemetry import  trace

from opentelemetry.exporter.otlp.proto.http.trace_exporter import (
    OTLPSpanExporter,
)
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor, SimpleSpanProcessor
from functools import wraps
from multiprocessing import Process
import multiprocessing as mp

def init_tracer():
    resource = Resource.create(
        attributes={
            "service.name": "api-service",
            # If workers are not distinguished within attributes, traces and
            # metrics exported from each worker will be indistinguishable. While
            # not necessarily an issue for traces, it is confusing for almost
            # all metric types. A built-in way to identify a worker is by PID
            # but this may lead to high label cardinality. An alternative
            # workaround and additional discussion are available here:
            # https://github.com/benoitc/gunicorn/issues/1352
            "worker": os.getpid(),
        }
    )

    trace.set_tracer_provider(TracerProvider(resource=resource))
    # This uses insecure connection for the purpose of example. Please see the
    # OTLP Exporter documentation for other options.
    span_processor = BatchSpanProcessor(
            OTLPSpanExporter(endpoint="http://tempo.mycompany.cn:4318/v1/traces")
    )

    trace.get_tracer_provider().add_span_processor(span_processor)


# def post_fork(func):
#     @wraps(func)
#     def wrapper(*args, **kwargs):
#         init_tracer()
#         res = func(*args, **kwargs)
#         return res
#     return wrapper

#@post_fork
import time
def worker_func(name):
    with trace.get_tracer(__name__).start_as_current_span('multi-span') as span:
        span.set_attribute("pid", os.getpid())
        print(f"worker_func {name} running, span context : {trace.get_current_span().get_span_context()}")




if __name__ == '__main__':
    init_tracer()
    print('----------------------')

    mp.set_start_method('fork')
    p = Process(target=worker_func, args=('bob',))
    p.start()
    print("main process will wait child process")


 

i want to see the trace which is generated in the child process to be exported to the tempo. but it does not.

Steps to Reproduce
import os
from opentelemetry import  trace

from opentelemetry.exporter.otlp.proto.http.trace_exporter import (
    OTLPSpanExporter,
)
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor, SimpleSpanProcessor
from functools import wraps
from multiprocessing import Process
import multiprocessing as mp

def init_tracer():
    resource = Resource.create(
        attributes={
            "service.name": "api-service",
            # If workers are not distinguished within attributes, traces and
            # metrics exported from each worker will be indistinguishable. While
            # not necessarily an issue for traces, it is confusing for almost
            # all metric types. A built-in way to identify a worker is by PID
            # but this may lead to high label cardinality. An alternative
            # workaround and additional discussion are available here:
            # https://github.com/benoitc/gunicorn/issues/1352
            "worker": os.getpid(),
        }
    )

    trace.set_tracer_provider(TracerProvider(resource=resource))
    # This uses insecure connection for the purpose of example. Please see the
    # OTLP Exporter documentation for other options.
    span_processor = BatchSpanProcessor(
            OTLPSpanExporter(endpoint="http://tempo.mycopany.ac.cn:4318/v1/traces")
    )

    trace.get_tracer_provider().add_span_processor(span_processor)


# def post_fork(func):
#     @wraps(func)
#     def wrapper(*args, **kwargs):
#         init_tracer()
#         res = func(*args, **kwargs)
#         return res
#     return wrapper

#@post_fork
import time
def worker_func(name):
    with trace.get_tracer(__name__).start_as_current_span('multi-span') as span:
        span.set_attribute("pid", os.getpid())
        print(f"worker_func {name} running, span context : {trace.get_current_span().get_span_context()}")




if __name__ == '__main__':
    init_tracer()
    print('----------------------')

    mp.set_start_method('fork')
    p = Process(target=worker_func, args=('bob',))
    p.start()
    print("main process will wait child process")


    
 

Expected Result

i want to see the trace which is generated in the child process to be exported to the tempo.

and could you offer some function to reinitialize the TracerProvider object after i fork a process, and i can use it as a total new object. and the all of state is right.

Actual Result

the readable span info do not be exported in the BatchSpanProcessor.worker() function.

Additional context
  • i init the TracerProvider in the main process.
  • and i fork a process, and execute some logic.
  • in my worker_func, i generate a span.
  • i add some log in the opentelemetry-sdk lib.
  • it seems that when my child process exit, the TracerProvider object in child process does not call the shutdown function, so the SynchronousMultiSpanProcessor.shutdown() can not be called, so the BatchSpanProcessor.worker() can not be waited. when i span info be send to queue, and just then, when the worker wait the condition(timeout), the worker thread killed when the children process exit.
image

could you offer some function to reinitialize the TracerProvider object after i fork a process, and i can use it as a total new object. and the all of state is right.

Would you like to implement a fix?

Yes

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the init_tracer and worker_func entry points in the reproduction, then inspect TracerProvider, BatchSpanProcessor.worker(), and shutdown behavior across multiprocessing fork. Reproduce the child-process exit case and determine how a child provider should be reinitialized; done means child spans are exported to Tempo reliably, with coverage for the fork scenario.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
observability
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
38/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.