11"""Tracing setup and Celery tracing signal integration."""
22
3- import logging
43import os
54import socket
65from typing import Any
1615from opentelemetry .sdk .trace import TracerProvider
1716from opentelemetry .sdk .trace .export import BatchSpanProcessor , ConsoleSpanExporter
1817from opentelemetry .sdk .trace .sampling import ParentBased , TraceIdRatioBased
18+ from opentelemetry .trace import ProxyTracerProvider
1919
2020from django_o11y .config .setup import get_o11y_config
21+ from django_o11y .logging .celery import setup_celery_logging
2122from django_o11y .logging .utils import get_logger
2223from django_o11y .tracing .fork import register_post_fork_handler
2324from django_o11y .tracing .instrumentation import setup_instrumentation
2829from django_o11y .utils .process import get_process_identity
2930
3031logger = get_logger ()
31- provider_logger = logging .getLogger ("django_o11y.tracing" )
3232
3333# Track instrumentation per-process to remain fork-safe.
3434_instrumented_pid : int | None = None
3535_tracing_initialized_pid : int | None = None
3636
3737
38- def setup_tracing (config : dict [str , Any ]) -> TracerProvider :
38+ def setup_tracing (config : dict [str , Any ]) -> Any :
3939 """Set up OpenTelemetry tracing provider and span processors."""
40+ global _tracing_initialized_pid
41+
4042 service_name = config ["SERVICE_NAME" ]
4143 tracing_config = config ["TRACING" ]
4244
45+ existing_provider = trace .get_tracer_provider ()
46+ if not isinstance (existing_provider , ProxyTracerProvider ):
47+ _tracing_initialized_pid = os .getpid ()
48+ logger .debug (
49+ "Tracing provider already configured by %s.%s; skipping override [%s]" ,
50+ existing_provider .__class__ .__module__ ,
51+ existing_provider .__class__ .__name__ ,
52+ get_process_identity (),
53+ )
54+ return existing_provider
55+
4356 instance_id = config .get ("SERVICE_INSTANCE_ID" ) or (
4457 f"{ os .getenv ('HOSTNAME' , socket .gethostname ())} :{ os .getpid ()} "
4558 )
@@ -76,45 +89,49 @@ def setup_tracing(config: dict[str, Any]) -> TracerProvider:
7689 provider .add_span_processor (BatchSpanProcessor (console_exporter ))
7790
7891 trace .set_tracer_provider (provider )
79- global _tracing_initialized_pid
8092 _tracing_initialized_pid = os .getpid ()
81- provider_logger .info (
93+ logger .info (
8294 "Tracing configured for %s, sending to %s (%.0f%% sampling) [%s]" ,
8395 service_name ,
8496 tracing_config ["OTLP_ENDPOINT" ],
8597 tracing_config ["SAMPLE_RATE" ] * 100 ,
8698 get_process_identity (),
8799 )
88100
89- profiling_config = config ["PROFILING" ]
90- if profiling_config .get ("ENABLED" ):
91- is_prefork_parent = (
92- is_celery_prefork_pool () and not is_celery_fork_pool_worker ()
93- )
94- if is_prefork_parent :
95- provider_logger .warning (
96- "Skipping Pyroscope profile-trace correlation in Celery prefork "
97- "parent process [%s]. Correlation is initialized in worker "
98- "child processes post-fork." ,
99- get_process_identity (),
100- )
101- return provider
101+ _setup_pyroscope_correlation (provider , config )
102102
103- try :
104- from pyroscope .otel import PyroscopeSpanProcessor
103+ return provider
105104
106- provider .add_span_processor (PyroscopeSpanProcessor ())
107- provider_logger .info (
108- "Pyroscope span processor added for profile-to-trace correlation [%s]" ,
109- get_process_identity (),
110- )
111- except ImportError :
112- provider_logger .debug (
113- "django_o11y: pyroscope-otel not installed, skipping profile-trace "
114- "correlation. Install with: pip install django-o11y[profiling]"
115- )
116105
117- return provider
106+ def _setup_pyroscope_correlation (
107+ provider : TracerProvider , config : dict [str , Any ]
108+ ) -> None :
109+ """Attach Pyroscope span correlation when profiling is enabled."""
110+ if not config ["PROFILING" ].get ("ENABLED" ):
111+ return
112+
113+ if is_celery_prefork_pool () and not is_celery_fork_pool_worker ():
114+ logger .warning (
115+ "Skipping Pyroscope profile-trace correlation in Celery prefork "
116+ "parent process [%s]. Correlation is initialized in worker "
117+ "child processes post-fork." ,
118+ get_process_identity (),
119+ )
120+ return
121+
122+ try :
123+ from pyroscope .otel import PyroscopeSpanProcessor
124+
125+ provider .add_span_processor (PyroscopeSpanProcessor ())
126+ logger .info (
127+ "Pyroscope span processor added for profile-to-trace correlation [%s]" ,
128+ get_process_identity (),
129+ )
130+ except ImportError :
131+ logger .debug (
132+ "django_o11y: pyroscope-otel not installed, skipping profile-trace "
133+ "correlation. Install with: pip install django-o11y[profiling]"
134+ )
118135
119136
120137def setup_tracing_for_django (config : dict [str , Any ]) -> None :
@@ -151,7 +168,7 @@ def setup_tracing_for_django(config: dict[str, Any]) -> None:
151168
152169
153170def setup_celery_o11y (app : Any , config : dict [str , Any ] | None = None ) -> None :
154- """Set up tracing and worker defaults for Celery tasks ."""
171+ """Set up Celery worker logging and tracing bootstrap ."""
155172 global _instrumented_pid
156173
157174 if _instrumented_pid == os .getpid ():
@@ -164,13 +181,7 @@ def setup_celery_o11y(app: Any, config: dict[str, Any] | None = None) -> None:
164181 if not celery_config .get ("ENABLED" , False ):
165182 return
166183
167- # Keep Django/structlog logging ownership in workers.
168- app .conf .worker_hijack_root_logger = False
169- app .conf .worker_redirect_stdouts = False
170-
171- from django_structlog .celery .steps import DjangoStructLogInitStep
172-
173- app .steps ["worker" ].add (DjangoStructLogInitStep )
184+ setup_celery_logging (app )
174185
175186 if config .get ("TRACING" , {}).get ("ENABLED" ) and celery_config .get (
176187 "TRACING_ENABLED" , True
@@ -201,7 +212,7 @@ def setup_worker_metrics(celery_config: dict[str, Any]) -> None:
201212
202213 multiproc_dir = os .environ .get ("PROMETHEUS_MULTIPROC_DIR" )
203214 if not multiproc_dir :
204- provider_logger .warning (
215+ logger .warning (
205216 "PROMETHEUS_MULTIPROC_DIR is not set; "
206217 "Celery worker metrics server will not be started."
207218 )
@@ -217,7 +228,7 @@ def setup_worker_metrics(celery_config: dict[str, Any]) -> None:
217228 registry = CollectorRegistry ()
218229 MultiProcessCollector (registry )
219230 start_http_server (port , registry = registry )
220- provider_logger .info (
231+ logger .info (
221232 "Celery worker metrics server started on port %d (multiproc dir: %s)" ,
222233 port ,
223234 multiproc_dir ,
@@ -244,7 +255,7 @@ def _configure_celery_metrics_events(config: dict[str, Any]) -> None:
244255 app .conf .worker_send_task_events = True
245256 app .conf .task_send_sent_event = True
246257 except Exception : # pragma: no cover
247- provider_logger .debug (
258+ logger .debug (
248259 "Failed to enable Celery task events in Django/worker process" ,
249260 exc_info = True ,
250261 )
0 commit comments