modaltrace 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
modaltrace/__init__.py ADDED
@@ -0,0 +1,244 @@
1
+ """modaltrace: OpenTelemetry observability for real-time AI avatar and video pipelines.
2
+
3
+ Everything a user needs is importable from `modaltrace` directly:
4
+
5
+ import modaltrace
6
+
7
+ sdk = modaltrace.init(service_name="artalk-avatar")
8
+
9
+ @modaltrace.pipeline_stage("flame_inference")
10
+ async def run_model(...): ...
11
+
12
+ async with modaltrace.stage("render") as s:
13
+ s.record("vertex_count", 12345)
14
+
15
+ modaltrace.info("Pipeline started", target_fps=30)
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ from typing import Any
21
+
22
+ from modaltrace._version import __version__
23
+ from modaltrace.logging.api import (
24
+ debug,
25
+ error,
26
+ exception,
27
+ info,
28
+ notice,
29
+ warning,
30
+ )
31
+ from modaltrace.logging.api import (
32
+ trace_log as trace,
33
+ )
34
+ from modaltrace.tracing.pipeline import async_stage, pipeline_stage, stage
35
+
36
+ __all__ = [
37
+ "__version__",
38
+ "init",
39
+ "pipeline_stage",
40
+ "stage",
41
+ "async_stage",
42
+ "trace",
43
+ "debug",
44
+ "info",
45
+ "notice",
46
+ "warning",
47
+ "error",
48
+ "exception",
49
+ ]
50
+
51
+
52
+ class ModalTraceSDK:
53
+ """SDK handle returned by init(). Context-manager compatible."""
54
+
55
+ def __init__(
56
+ self,
57
+ config,
58
+ tracer_provider,
59
+ meter_provider,
60
+ logger_provider,
61
+ frame_aggregator,
62
+ av_tracker,
63
+ gpu_monitor=None,
64
+ pending_processor=None,
65
+ ):
66
+ self._config = config
67
+ self._tracer_provider = tracer_provider
68
+ self._meter_provider = meter_provider
69
+ self._logger_provider = logger_provider
70
+ self.frame_aggregator = frame_aggregator
71
+ self.av_tracker = av_tracker
72
+ self._gpu_monitor = gpu_monitor
73
+ self._pending_processor = pending_processor
74
+ self._stopped = False
75
+
76
+ def __enter__(self):
77
+ return self
78
+
79
+ def __exit__(self, *exc):
80
+ self.stop()
81
+
82
+ def flush(self) -> None:
83
+ """Force-flush all exporters."""
84
+ self._tracer_provider.force_flush()
85
+ self._meter_provider.force_flush()
86
+
87
+ def stop(self) -> None:
88
+ """Flush and shut down all components."""
89
+ if self._stopped:
90
+ return
91
+ self._stopped = True
92
+
93
+ self.frame_aggregator.stop()
94
+
95
+ if self._pending_processor is not None:
96
+ self._pending_processor.stop()
97
+
98
+ if self._gpu_monitor is not None:
99
+ self._gpu_monitor.stop()
100
+
101
+ from modaltrace.instrumentation.eventloop import uninstall_eventloop_monitor
102
+ from modaltrace.instrumentation.pytorch import uninstrument_pytorch
103
+ from modaltrace.tracing.propagation import unpatch_all
104
+
105
+ uninstrument_pytorch()
106
+ unpatch_all()
107
+ uninstall_eventloop_monitor()
108
+
109
+ self._tracer_provider.shutdown()
110
+ self._meter_provider.shutdown()
111
+ self._logger_provider.shutdown()
112
+
113
+
114
+ def init(**kwargs: Any) -> ModalTraceSDK:
115
+ """Initialize modaltrace. One-liner to get full observability.
116
+
117
+ All kwargs are passed to ModalTraceConfig (Pydantic Settings), which also
118
+ reads from MODALTRACE_* environment variables and .env files.
119
+
120
+ Returns an ModalTraceSDK handle with .frame_aggregator, .av_tracker,
121
+ .flush(), and .stop() methods. Also works as a context manager.
122
+ """
123
+ from modaltrace import _registry
124
+ from modaltrace.config import ModalTraceConfig
125
+ from modaltrace.exporters.setup import (
126
+ create_resource,
127
+ setup_logger_provider,
128
+ setup_meter_provider,
129
+ setup_tracer_provider,
130
+ )
131
+ from modaltrace.instrumentation.eventloop import install_eventloop_monitor
132
+ from modaltrace.instrumentation.gpu import GPUMonitor
133
+ from modaltrace.instrumentation.pytorch import instrument_pytorch
134
+ from modaltrace.logging.api import _init_logging
135
+ from modaltrace.logging.scrubber import ScrubbingSpanProcessor
136
+ from modaltrace.metrics.aggregator import FrameMetricsAggregator
137
+ from modaltrace.metrics.av_sync import AVSyncTracker
138
+ from modaltrace.metrics.instruments import MetricInstruments
139
+ from modaltrace.tracing.pending import PendingSpanProcessor
140
+ from modaltrace.tracing.propagation import patch_all
141
+ from modaltrace.tracing.sampler import AdaptiveSampler
142
+
143
+ config = ModalTraceConfig(**kwargs)
144
+ _registry._config = config
145
+
146
+ resource = create_resource(config)
147
+ tracer_provider = setup_tracer_provider(config, resource)
148
+ meter_provider = setup_meter_provider(config, resource)
149
+ logger_provider = setup_logger_provider(config, resource)
150
+
151
+ tracer = tracer_provider.get_tracer("modaltrace", __version__)
152
+ meter = meter_provider.get_meter("modaltrace", __version__)
153
+ _registry._tracer = tracer
154
+ _registry._meter = meter
155
+
156
+ instruments = MetricInstruments(meter)
157
+
158
+ if config.scrubbing_enabled:
159
+ scrubber = ScrubbingSpanProcessor(
160
+ extra_patterns=config.scrubbing_patterns,
161
+ callback=config.scrubbing_callback,
162
+ )
163
+ tracer_provider.add_span_processor(scrubber)
164
+
165
+ from modaltrace.exporters.setup import _create_span_exporter
166
+
167
+ pending_exporter = _create_span_exporter(config)
168
+ pending_processor = PendingSpanProcessor(
169
+ exporter=pending_exporter,
170
+ flush_interval_ms=config.pending_span_flush_interval_ms,
171
+ )
172
+ tracer_provider.add_span_processor(pending_processor)
173
+ pending_processor.start()
174
+
175
+ aggregator = FrameMetricsAggregator(
176
+ instruments=instruments,
177
+ buffer_size=config.ring_buffer_size,
178
+ flush_interval_ms=config.metrics_flush_interval_ms,
179
+ )
180
+ aggregator.start()
181
+
182
+ av_tracker = AVSyncTracker(
183
+ instruments=instruments,
184
+ drift_warning_ms=config.av_drift_warning_ms,
185
+ chunk_ttl_s=config.av_chunk_ttl_s,
186
+ jitter_window=config.av_jitter_window,
187
+ warning_callback=warning,
188
+ )
189
+
190
+ sampler = AdaptiveSampler(
191
+ window_s=config.span_window_s,
192
+ anomaly_threshold_ms=config.anomaly_threshold_ms,
193
+ )
194
+ _registry._sampler = sampler
195
+
196
+ gpu_monitor = None
197
+ if config.gpu_monitoring:
198
+ gpu_monitor = GPUMonitor(
199
+ poll_interval_s=config.gpu_poll_interval_s,
200
+ device_indices=config.gpu_device_indices,
201
+ )
202
+ if gpu_monitor.start():
203
+ gpu_monitor.register_gauges(meter)
204
+ else:
205
+ gpu_monitor = None
206
+
207
+ if config.pytorch_instrumentation:
208
+ instrument_pytorch(
209
+ tracer=tracer,
210
+ sample_rate=config.pytorch_sample_rate,
211
+ anomaly_threshold_ms=config.anomaly_threshold_ms,
212
+ track_memory=config.pytorch_track_memory,
213
+ track_shapes=config.pytorch_track_shapes,
214
+ aggregator=aggregator,
215
+ )
216
+
217
+ if config.threadpool_propagation:
218
+ patch_all()
219
+
220
+ if config.eventloop_monitoring:
221
+ install_eventloop_monitor(
222
+ threshold_ms=config.eventloop_lag_threshold_ms,
223
+ warning_callback=warning,
224
+ )
225
+
226
+ _init_logging(
227
+ logger_provider=logger_provider,
228
+ log_level=config.log_level,
229
+ log_console=config.log_console,
230
+ )
231
+
232
+ sdk = ModalTraceSDK(
233
+ config=config,
234
+ tracer_provider=tracer_provider,
235
+ meter_provider=meter_provider,
236
+ logger_provider=logger_provider,
237
+ frame_aggregator=aggregator,
238
+ av_tracker=av_tracker,
239
+ gpu_monitor=gpu_monitor,
240
+ pending_processor=pending_processor,
241
+ )
242
+ _registry._sdk = sdk
243
+
244
+ return sdk
@@ -0,0 +1,25 @@
1
+ """Module-level singletons for the modaltrace SDK.
2
+
3
+ Holds references to the active config, providers, and components
4
+ so that module-level functions (pipeline_stage, info, etc.) can
5
+ access them without requiring the user to pass the SDK instance.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import TYPE_CHECKING
11
+
12
+ if TYPE_CHECKING:
13
+ from modaltrace.config import ModalTraceConfig
14
+
15
+ _config: ModalTraceConfig | None = None
16
+ _tracer = None
17
+ _meter = None
18
+ _logger_provider = None
19
+ _sdk = None
20
+
21
+
22
+ def get_config() -> ModalTraceConfig:
23
+ if _config is None:
24
+ raise RuntimeError("modaltrace.init() has not been called yet")
25
+ return _config
modaltrace/_version.py ADDED
@@ -0,0 +1,2 @@
1
+ # This file is managed by hatch-vcs. Fallback for editable installs.
2
+ __version__ = "0.0.0"
modaltrace/config.py ADDED
@@ -0,0 +1,92 @@
1
+ """Pydantic Settings model — single source of truth for all configuration.
2
+
3
+ Users can configure via Python kwargs, environment variables (MODALTRACE_*), or .env files.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from collections.abc import Callable
9
+ from typing import Literal
10
+
11
+ from pydantic import AnyHttpUrl, Field, field_validator
12
+ from pydantic_settings import BaseSettings, SettingsConfigDict
13
+
14
+
15
+ class ModalTraceConfig(BaseSettings):
16
+ """Configuration for modaltrace SDK."""
17
+
18
+ # ── Identity ──────────────────────────────────────────────────────────
19
+ service_name: str = "modaltrace-pipeline"
20
+ service_version: str = "0.0.0"
21
+ deployment_environment: str = "development"
22
+
23
+ # ── OTLP Export ───────────────────────────────────────────────────────
24
+ otlp_endpoint: AnyHttpUrl = "http://localhost:4318" # type: ignore[assignment]
25
+ otlp_protocol: Literal["http", "grpc"] = "http"
26
+ otlp_headers: dict[str, str] = Field(default_factory=dict)
27
+ otlp_timeout_ms: int = 10_000
28
+
29
+ # ── Feature Flags ─────────────────────────────────────────────────────
30
+ pytorch_instrumentation: bool = True
31
+ gpu_monitoring: bool = True
32
+ webrtc_monitoring: bool = False
33
+ eventloop_monitoring: bool = True
34
+ threadpool_propagation: bool = True
35
+
36
+ # ── Frame Metrics Aggregator ──────────────────────────────────────────
37
+ metrics_flush_interval_ms: int = 1_000
38
+ ring_buffer_size: int = 512
39
+
40
+ # ── Adaptive Sampler ──────────────────────────────────────────────────
41
+ span_window_s: float = 1.0
42
+ anomaly_threshold_ms: float = 50.0
43
+ pytorch_sample_rate: float = 0.01
44
+
45
+ # ── Pending Spans ─────────────────────────────────────────────────────
46
+ pending_span_flush_interval_ms: int = 5_000
47
+
48
+ # ── A/V Sync ──────────────────────────────────────────────────────────
49
+ av_drift_warning_ms: float = 40.0
50
+ av_chunk_ttl_s: float = 5.0
51
+ av_jitter_window: int = 30
52
+
53
+ # ── GPU Monitor ───────────────────────────────────────────────────────
54
+ gpu_poll_interval_s: float = 1.0
55
+ gpu_device_indices: list[int] | None = None
56
+
57
+ # ── PyTorch Instrumentation ───────────────────────────────────────────
58
+ pytorch_track_memory: bool = True
59
+ pytorch_track_shapes: bool = False
60
+
61
+ # ── PII Scrubbing ─────────────────────────────────────────────────────
62
+ scrubbing_enabled: bool = True
63
+ scrubbing_patterns: list[str] = Field(default_factory=list)
64
+ scrubbing_callback: Callable | None = None
65
+
66
+ # ── Structured Logging ────────────────────────────────────────────────
67
+ log_level: str = "info"
68
+ log_console: bool = True
69
+
70
+ # ── Event Loop Monitor ────────────────────────────────────────────────
71
+ eventloop_lag_threshold_ms: float = 100.0
72
+
73
+ model_config = SettingsConfigDict(
74
+ env_prefix="MODALTRACE_",
75
+ env_file=".env",
76
+ env_file_encoding="utf-8",
77
+ arbitrary_types_allowed=True,
78
+ )
79
+
80
+ @field_validator("ring_buffer_size")
81
+ @classmethod
82
+ def must_be_power_of_two(cls, v: int) -> int:
83
+ if v & (v - 1) != 0:
84
+ raise ValueError(f"ring_buffer_size must be a power of 2, got {v}")
85
+ return v
86
+
87
+ @field_validator("pytorch_sample_rate")
88
+ @classmethod
89
+ def must_be_fraction(cls, v: float) -> float:
90
+ if not 0.0 <= v <= 1.0:
91
+ raise ValueError(f"pytorch_sample_rate must be between 0 and 1, got {v}")
92
+ return v
@@ -0,0 +1,19 @@
1
+ from modaltrace.conventions.attributes import (
2
+ AVSyncAttributes,
3
+ EventLoopAttributes,
4
+ GPUAttributes,
5
+ InferenceAttributes,
6
+ ModalAttributes,
7
+ PipelineAttributes,
8
+ TransportAttributes,
9
+ )
10
+
11
+ __all__ = [
12
+ "PipelineAttributes",
13
+ "InferenceAttributes",
14
+ "ModalAttributes",
15
+ "AVSyncAttributes",
16
+ "GPUAttributes",
17
+ "TransportAttributes",
18
+ "EventLoopAttributes",
19
+ ]
@@ -0,0 +1,66 @@
1
+ """Semantic convention string constants for modaltrace.
2
+
3
+ All attribute keys live here — zero magic strings in the rest of the codebase.
4
+ """
5
+
6
+
7
+ class PipelineAttributes:
8
+ ID = "modaltrace.pipeline.id"
9
+ SESSION_ID = "modaltrace.pipeline.session_id"
10
+ STAGE_NAME = "modaltrace.pipeline.stage.name"
11
+ STAGE_DURATION_MS = "modaltrace.pipeline.stage.duration_ms"
12
+ FRAME_SEQ = "modaltrace.pipeline.frame.sequence_number"
13
+ TARGET_FPS = "modaltrace.pipeline.target_fps"
14
+ SPAN_PENDING = "modaltrace.span.pending"
15
+
16
+
17
+ class InferenceAttributes:
18
+ MODEL_NAME = "modaltrace.inference.model_name"
19
+ FORWARD_PASS_MS = "modaltrace.inference.forward_pass_ms"
20
+ BATCH_SIZE = "modaltrace.inference.batch_size"
21
+ GPU_MEMORY_MB = "modaltrace.inference.gpu.memory_allocated_mb"
22
+ GPU_MEMORY_DELTA_MB = "modaltrace.inference.gpu.memory_delta_mb"
23
+ INPUT_SHAPES = "modaltrace.inference.input_shapes"
24
+ DEVICE = "modaltrace.inference.device"
25
+
26
+
27
+ class ModalAttributes:
28
+ FLAME_INFERENCE_MS = "modaltrace.flame.inference_ms"
29
+ FLAME_PARAM_COUNT = "modaltrace.flame.parameter_count"
30
+ RENDER_FRAME_MS = "modaltrace.render.frame_ms"
31
+ RENDER_RESOLUTION = "modaltrace.render.resolution"
32
+ MESH_VERTEX_COUNT = "modaltrace.mesh.vertex_count"
33
+ FRAME_SEQ = "modaltrace.frame.sequence_number"
34
+
35
+
36
+ class AVSyncAttributes:
37
+ DRIFT_MS = "modaltrace.av_sync.drift_ms"
38
+ JITTER_MS = "modaltrace.av_sync.jitter_ms"
39
+ THRESHOLD_MS = "modaltrace.av_sync.threshold_ms"
40
+ UNMATCHED_CHUNKS = "modaltrace.av_sync.unmatched_chunks"
41
+ CHUNK_ID = "modaltrace.av_sync.chunk_id"
42
+
43
+
44
+ class GPUAttributes:
45
+ DEVICE_INDEX = "modaltrace.gpu.device_index"
46
+ UTILIZATION_PCT = "modaltrace.gpu.utilization"
47
+ MEMORY_USED_MB = "modaltrace.gpu.memory.used"
48
+ MEMORY_FREE_MB = "modaltrace.gpu.memory.free"
49
+ TEMPERATURE_C = "modaltrace.gpu.temperature"
50
+ POWER_W = "modaltrace.gpu.power.draw"
51
+
52
+
53
+ class TransportAttributes:
54
+ PROTOCOL = "modaltrace.transport.protocol"
55
+ RTT_MS = "modaltrace.transport.rtt_ms"
56
+ JITTER_MS = "modaltrace.transport.jitter_ms"
57
+ PACKET_LOSS_PCT = "modaltrace.transport.packet_loss_percent"
58
+ BITRATE_KBPS = "modaltrace.transport.bitrate_kbps"
59
+ FRAME_RATE_ACTUAL = "modaltrace.transport.frame_rate_actual"
60
+ STREAM = "modaltrace.transport.stream"
61
+
62
+
63
+ class EventLoopAttributes:
64
+ ELAPSED_MS = "modaltrace.eventloop.blocked_ms"
65
+ THRESHOLD_MS = "modaltrace.eventloop.threshold_ms"
66
+ HANDLE_CALLBACK = "modaltrace.eventloop.handle_callback"
File without changes
@@ -0,0 +1,126 @@
1
+ """OTLP exporter and provider setup.
2
+
3
+ Configures TracerProvider, MeterProvider, and LoggerProvider with OTLP
4
+ HTTP or gRPC exporters based on ModalTraceConfig.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import TYPE_CHECKING
10
+
11
+ from opentelemetry import trace
12
+ from opentelemetry.sdk.resources import Resource
13
+ from opentelemetry.sdk.trace import TracerProvider
14
+ from opentelemetry.sdk.trace.export import BatchSpanProcessor
15
+
16
+ if TYPE_CHECKING:
17
+ from modaltrace.config import ModalTraceConfig
18
+
19
+
20
+ def create_resource(config: ModalTraceConfig) -> Resource:
21
+ return Resource.create(
22
+ {
23
+ "service.name": config.service_name,
24
+ "service.version": config.service_version,
25
+ "deployment.environment": config.deployment_environment,
26
+ }
27
+ )
28
+
29
+
30
+ def setup_tracer_provider(config: ModalTraceConfig, resource: Resource) -> TracerProvider:
31
+ provider = TracerProvider(resource=resource)
32
+ exporter = _create_span_exporter(config)
33
+ provider.add_span_processor(BatchSpanProcessor(exporter))
34
+ trace.set_tracer_provider(provider)
35
+ return provider
36
+
37
+
38
+ def setup_meter_provider(config: ModalTraceConfig, resource: Resource):
39
+ from opentelemetry import metrics
40
+ from opentelemetry.sdk.metrics import MeterProvider
41
+ from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
42
+
43
+ exporter = _create_metric_exporter(config)
44
+ reader = PeriodicExportingMetricReader(
45
+ exporter,
46
+ export_interval_millis=config.metrics_flush_interval_ms,
47
+ )
48
+ provider = MeterProvider(resource=resource, metric_readers=[reader])
49
+ metrics.set_meter_provider(provider)
50
+ return provider
51
+
52
+
53
+ def setup_logger_provider(config: ModalTraceConfig, resource: Resource):
54
+ from opentelemetry.sdk._logs import LoggerProvider
55
+ from opentelemetry.sdk._logs.export import BatchLogRecordProcessor
56
+
57
+ exporter = _create_log_exporter(config)
58
+ provider = LoggerProvider(resource=resource)
59
+ provider.add_log_record_processor(BatchLogRecordProcessor(exporter))
60
+ return provider
61
+
62
+
63
+ def _create_span_exporter(config: ModalTraceConfig):
64
+ endpoint = str(config.otlp_endpoint)
65
+ headers = config.otlp_headers
66
+ timeout = config.otlp_timeout_ms // 1000
67
+
68
+ if config.otlp_protocol == "grpc":
69
+ from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
70
+
71
+ return OTLPSpanExporter(
72
+ endpoint=endpoint,
73
+ headers=tuple(headers.items()) if headers else None,
74
+ timeout=timeout,
75
+ )
76
+ from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
77
+
78
+ return OTLPSpanExporter(
79
+ endpoint=f"{endpoint}/v1/traces",
80
+ headers=headers,
81
+ timeout=timeout,
82
+ )
83
+
84
+
85
+ def _create_metric_exporter(config: ModalTraceConfig):
86
+ endpoint = str(config.otlp_endpoint)
87
+ headers = config.otlp_headers
88
+ timeout = config.otlp_timeout_ms // 1000
89
+
90
+ if config.otlp_protocol == "grpc":
91
+ from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import OTLPMetricExporter
92
+
93
+ return OTLPMetricExporter(
94
+ endpoint=endpoint,
95
+ headers=tuple(headers.items()) if headers else None,
96
+ timeout=timeout,
97
+ )
98
+ from opentelemetry.exporter.otlp.proto.http.metric_exporter import OTLPMetricExporter
99
+
100
+ return OTLPMetricExporter(
101
+ endpoint=f"{endpoint}/v1/metrics",
102
+ headers=headers,
103
+ timeout=timeout,
104
+ )
105
+
106
+
107
+ def _create_log_exporter(config: ModalTraceConfig):
108
+ endpoint = str(config.otlp_endpoint)
109
+ headers = config.otlp_headers
110
+ timeout = config.otlp_timeout_ms // 1000
111
+
112
+ if config.otlp_protocol == "grpc":
113
+ from opentelemetry.exporter.otlp.proto.grpc._log_exporter import OTLPLogExporter
114
+
115
+ return OTLPLogExporter(
116
+ endpoint=endpoint,
117
+ headers=tuple(headers.items()) if headers else None,
118
+ timeout=timeout,
119
+ )
120
+ from opentelemetry.exporter.otlp.proto.http._log_exporter import OTLPLogExporter
121
+
122
+ return OTLPLogExporter(
123
+ endpoint=f"{endpoint}/v1/logs",
124
+ headers=headers,
125
+ timeout=timeout,
126
+ )
File without changes
@@ -0,0 +1,47 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import logging
5
+ import time
6
+
7
+ logger = logging.getLogger("modaltrace.eventloop")
8
+
9
+ _original_handle_run = None
10
+ _warning_callback = None
11
+
12
+
13
+ def install_eventloop_monitor(
14
+ threshold_ms: float = 100.0,
15
+ warning_callback=None,
16
+ ):
17
+ global _original_handle_run, _warning_callback
18
+ _original_handle_run = asyncio.events.Handle._run
19
+ _warning_callback = warning_callback
20
+
21
+ def patched_run(self):
22
+ start = time.perf_counter()
23
+ try:
24
+ _original_handle_run(self)
25
+ finally:
26
+ elapsed_ms = (time.perf_counter() - start) * 1000
27
+ if elapsed_ms > threshold_ms:
28
+ cb_name = getattr(self._callback, "__qualname__", str(self._callback))
29
+ if _warning_callback:
30
+ _warning_callback(
31
+ f"Event loop blocked for {elapsed_ms:.1f}ms",
32
+ elapsed_ms=elapsed_ms,
33
+ threshold_ms=threshold_ms,
34
+ handle_callback=cb_name,
35
+ )
36
+ else:
37
+ logger.warning("Event loop blocked for %.1fms by %s", elapsed_ms, cb_name)
38
+
39
+ asyncio.events.Handle._run = patched_run
40
+
41
+
42
+ def uninstall_eventloop_monitor():
43
+ global _original_handle_run, _warning_callback
44
+ if _original_handle_run is not None:
45
+ asyncio.events.Handle._run = _original_handle_run
46
+ _original_handle_run = None
47
+ _warning_callback = None