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 +244 -0
- modaltrace/_registry.py +25 -0
- modaltrace/_version.py +2 -0
- modaltrace/config.py +92 -0
- modaltrace/conventions/__init__.py +19 -0
- modaltrace/conventions/attributes.py +66 -0
- modaltrace/exporters/__init__.py +0 -0
- modaltrace/exporters/setup.py +126 -0
- modaltrace/instrumentation/__init__.py +0 -0
- modaltrace/instrumentation/eventloop.py +47 -0
- modaltrace/instrumentation/gpu.py +170 -0
- modaltrace/instrumentation/pytorch.py +153 -0
- modaltrace/instrumentation/transport.py +82 -0
- modaltrace/logging/__init__.py +0 -0
- modaltrace/logging/api.py +217 -0
- modaltrace/logging/scrubber.py +107 -0
- modaltrace/metrics/__init__.py +0 -0
- modaltrace/metrics/aggregator.py +122 -0
- modaltrace/metrics/av_sync.py +108 -0
- modaltrace/metrics/instruments.py +67 -0
- modaltrace/tracing/__init__.py +0 -0
- modaltrace/tracing/pending.py +124 -0
- modaltrace/tracing/pipeline.py +210 -0
- modaltrace/tracing/propagation.py +132 -0
- modaltrace/tracing/sampler.py +57 -0
- modaltrace-0.1.0.dist-info/METADATA +32 -0
- modaltrace-0.1.0.dist-info/RECORD +29 -0
- modaltrace-0.1.0.dist-info/WHEEL +4 -0
- modaltrace-0.1.0.dist-info/licenses/LICENSE +15 -0
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
|
modaltrace/_registry.py
ADDED
|
@@ -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
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
|