karaoke-gen 0.90.1__py3-none-any.whl → 0.96.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.
- backend/.coveragerc +20 -0
- backend/.gitignore +37 -0
- backend/Dockerfile +43 -0
- backend/Dockerfile.base +74 -0
- backend/README.md +242 -0
- backend/__init__.py +0 -0
- backend/api/__init__.py +0 -0
- backend/api/dependencies.py +457 -0
- backend/api/routes/__init__.py +0 -0
- backend/api/routes/admin.py +742 -0
- backend/api/routes/audio_search.py +903 -0
- backend/api/routes/auth.py +348 -0
- backend/api/routes/file_upload.py +2076 -0
- backend/api/routes/health.py +344 -0
- backend/api/routes/internal.py +435 -0
- backend/api/routes/jobs.py +1610 -0
- backend/api/routes/review.py +652 -0
- backend/api/routes/themes.py +162 -0
- backend/api/routes/users.py +1014 -0
- backend/config.py +172 -0
- backend/main.py +133 -0
- backend/middleware/__init__.py +5 -0
- backend/middleware/audit_logging.py +124 -0
- backend/models/__init__.py +0 -0
- backend/models/job.py +519 -0
- backend/models/requests.py +123 -0
- backend/models/theme.py +153 -0
- backend/models/user.py +254 -0
- backend/models/worker_log.py +164 -0
- backend/pyproject.toml +29 -0
- backend/quick-check.sh +93 -0
- backend/requirements.txt +29 -0
- backend/run_tests.sh +60 -0
- backend/services/__init__.py +0 -0
- backend/services/audio_analysis_service.py +243 -0
- backend/services/audio_editing_service.py +278 -0
- backend/services/audio_search_service.py +702 -0
- backend/services/auth_service.py +630 -0
- backend/services/credential_manager.py +792 -0
- backend/services/discord_service.py +172 -0
- backend/services/dropbox_service.py +301 -0
- backend/services/email_service.py +1093 -0
- backend/services/encoding_interface.py +454 -0
- backend/services/encoding_service.py +405 -0
- backend/services/firestore_service.py +512 -0
- backend/services/flacfetch_client.py +573 -0
- backend/services/gce_encoding/README.md +72 -0
- backend/services/gce_encoding/__init__.py +22 -0
- backend/services/gce_encoding/main.py +589 -0
- backend/services/gce_encoding/requirements.txt +16 -0
- backend/services/gdrive_service.py +356 -0
- backend/services/job_logging.py +258 -0
- backend/services/job_manager.py +842 -0
- backend/services/job_notification_service.py +271 -0
- backend/services/local_encoding_service.py +590 -0
- backend/services/local_preview_encoding_service.py +407 -0
- backend/services/lyrics_cache_service.py +216 -0
- backend/services/metrics.py +413 -0
- backend/services/packaging_service.py +287 -0
- backend/services/rclone_service.py +106 -0
- backend/services/storage_service.py +209 -0
- backend/services/stripe_service.py +275 -0
- backend/services/structured_logging.py +254 -0
- backend/services/template_service.py +330 -0
- backend/services/theme_service.py +469 -0
- backend/services/tracing.py +543 -0
- backend/services/user_service.py +721 -0
- backend/services/worker_service.py +558 -0
- backend/services/youtube_service.py +112 -0
- backend/services/youtube_upload_service.py +445 -0
- backend/tests/__init__.py +4 -0
- backend/tests/conftest.py +224 -0
- backend/tests/emulator/__init__.py +7 -0
- backend/tests/emulator/conftest.py +88 -0
- backend/tests/emulator/test_e2e_cli_backend.py +1053 -0
- backend/tests/emulator/test_emulator_integration.py +356 -0
- backend/tests/emulator/test_style_loading_direct.py +436 -0
- backend/tests/emulator/test_worker_logs_direct.py +229 -0
- backend/tests/emulator/test_worker_logs_subcollection.py +443 -0
- backend/tests/requirements-test.txt +10 -0
- backend/tests/requirements.txt +6 -0
- backend/tests/test_admin_email_endpoints.py +411 -0
- backend/tests/test_api_integration.py +460 -0
- backend/tests/test_api_routes.py +93 -0
- backend/tests/test_audio_analysis_service.py +294 -0
- backend/tests/test_audio_editing_service.py +386 -0
- backend/tests/test_audio_search.py +1398 -0
- backend/tests/test_audio_services.py +378 -0
- backend/tests/test_auth_firestore.py +231 -0
- backend/tests/test_config_extended.py +68 -0
- backend/tests/test_credential_manager.py +377 -0
- backend/tests/test_dependencies.py +54 -0
- backend/tests/test_discord_service.py +244 -0
- backend/tests/test_distribution_services.py +820 -0
- backend/tests/test_dropbox_service.py +472 -0
- backend/tests/test_email_service.py +492 -0
- backend/tests/test_emulator_integration.py +322 -0
- backend/tests/test_encoding_interface.py +412 -0
- backend/tests/test_file_upload.py +1739 -0
- backend/tests/test_flacfetch_client.py +632 -0
- backend/tests/test_gdrive_service.py +524 -0
- backend/tests/test_instrumental_api.py +431 -0
- backend/tests/test_internal_api.py +343 -0
- backend/tests/test_job_creation_regression.py +583 -0
- backend/tests/test_job_manager.py +339 -0
- backend/tests/test_job_manager_notifications.py +329 -0
- backend/tests/test_job_notification_service.py +443 -0
- backend/tests/test_jobs_api.py +273 -0
- backend/tests/test_local_encoding_service.py +423 -0
- backend/tests/test_local_preview_encoding_service.py +567 -0
- backend/tests/test_main.py +87 -0
- backend/tests/test_models.py +918 -0
- backend/tests/test_packaging_service.py +382 -0
- backend/tests/test_requests.py +201 -0
- backend/tests/test_routes_jobs.py +282 -0
- backend/tests/test_routes_review.py +337 -0
- backend/tests/test_services.py +556 -0
- backend/tests/test_services_extended.py +112 -0
- backend/tests/test_storage_service.py +448 -0
- backend/tests/test_style_upload.py +261 -0
- backend/tests/test_template_service.py +295 -0
- backend/tests/test_theme_service.py +516 -0
- backend/tests/test_unicode_sanitization.py +522 -0
- backend/tests/test_upload_api.py +256 -0
- backend/tests/test_validate.py +156 -0
- backend/tests/test_video_worker_orchestrator.py +847 -0
- backend/tests/test_worker_log_subcollection.py +509 -0
- backend/tests/test_worker_logging.py +365 -0
- backend/tests/test_workers.py +1116 -0
- backend/tests/test_workers_extended.py +178 -0
- backend/tests/test_youtube_service.py +247 -0
- backend/tests/test_youtube_upload_service.py +568 -0
- backend/validate.py +173 -0
- backend/version.py +27 -0
- backend/workers/README.md +597 -0
- backend/workers/__init__.py +11 -0
- backend/workers/audio_worker.py +618 -0
- backend/workers/lyrics_worker.py +683 -0
- backend/workers/render_video_worker.py +483 -0
- backend/workers/screens_worker.py +525 -0
- backend/workers/style_helper.py +198 -0
- backend/workers/video_worker.py +1277 -0
- backend/workers/video_worker_orchestrator.py +701 -0
- backend/workers/worker_logging.py +278 -0
- karaoke_gen/instrumental_review/static/index.html +7 -4
- karaoke_gen/karaoke_finalise/karaoke_finalise.py +6 -1
- karaoke_gen/utils/__init__.py +163 -8
- karaoke_gen/video_background_processor.py +9 -4
- {karaoke_gen-0.90.1.dist-info → karaoke_gen-0.96.0.dist-info}/METADATA +1 -1
- {karaoke_gen-0.90.1.dist-info → karaoke_gen-0.96.0.dist-info}/RECORD +186 -41
- lyrics_transcriber/correction/agentic/providers/config.py +9 -5
- lyrics_transcriber/correction/agentic/providers/langchain_bridge.py +1 -51
- lyrics_transcriber/correction/corrector.py +192 -130
- lyrics_transcriber/correction/operations.py +24 -9
- lyrics_transcriber/frontend/package-lock.json +2 -2
- lyrics_transcriber/frontend/package.json +1 -1
- lyrics_transcriber/frontend/src/components/AIFeedbackModal.tsx +1 -1
- lyrics_transcriber/frontend/src/components/CorrectedWordWithActions.tsx +11 -7
- lyrics_transcriber/frontend/src/components/EditActionBar.tsx +31 -5
- lyrics_transcriber/frontend/src/components/EditModal.tsx +28 -10
- lyrics_transcriber/frontend/src/components/EditTimelineSection.tsx +123 -27
- lyrics_transcriber/frontend/src/components/EditWordList.tsx +112 -60
- lyrics_transcriber/frontend/src/components/Header.tsx +90 -76
- lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx +53 -31
- lyrics_transcriber/frontend/src/components/LyricsSynchronizer/SyncControls.tsx +44 -13
- lyrics_transcriber/frontend/src/components/LyricsSynchronizer/TimelineCanvas.tsx +66 -50
- lyrics_transcriber/frontend/src/components/LyricsSynchronizer/index.tsx +124 -30
- lyrics_transcriber/frontend/src/components/ReferenceView.tsx +1 -1
- lyrics_transcriber/frontend/src/components/TimelineEditor.tsx +12 -5
- lyrics_transcriber/frontend/src/components/TimingOffsetModal.tsx +3 -3
- lyrics_transcriber/frontend/src/components/TranscriptionView.tsx +1 -1
- lyrics_transcriber/frontend/src/components/WordDivider.tsx +11 -7
- lyrics_transcriber/frontend/src/components/shared/components/Word.tsx +4 -2
- lyrics_transcriber/frontend/src/hooks/useManualSync.ts +103 -1
- lyrics_transcriber/frontend/src/theme.ts +42 -15
- lyrics_transcriber/frontend/tsconfig.tsbuildinfo +1 -1
- lyrics_transcriber/frontend/vite.config.js +5 -0
- lyrics_transcriber/frontend/web_assets/assets/{index-BECn1o8Q.js → index-BSMgOq4Z.js} +6959 -5782
- lyrics_transcriber/frontend/web_assets/assets/index-BSMgOq4Z.js.map +1 -0
- lyrics_transcriber/frontend/web_assets/index.html +6 -2
- lyrics_transcriber/frontend/web_assets/nomad-karaoke-logo.svg +5 -0
- lyrics_transcriber/output/generator.py +17 -3
- lyrics_transcriber/output/video.py +60 -95
- lyrics_transcriber/frontend/web_assets/assets/index-BECn1o8Q.js.map +0 -1
- {karaoke_gen-0.90.1.dist-info → karaoke_gen-0.96.0.dist-info}/WHEEL +0 -0
- {karaoke_gen-0.90.1.dist-info → karaoke_gen-0.96.0.dist-info}/entry_points.txt +0 -0
- {karaoke_gen-0.90.1.dist-info → karaoke_gen-0.96.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OpenTelemetry tracing configuration for Google Cloud Trace.
|
|
3
|
+
|
|
4
|
+
This module sets up distributed tracing that exports to Google Cloud Trace,
|
|
5
|
+
providing visibility into request flows, latency, and errors across the
|
|
6
|
+
entire backend.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
# In main.py or app initialization
|
|
10
|
+
from backend.services.tracing import setup_tracing, instrument_app
|
|
11
|
+
|
|
12
|
+
setup_tracing(service_name="karaoke-backend")
|
|
13
|
+
app = FastAPI()
|
|
14
|
+
instrument_app(app)
|
|
15
|
+
|
|
16
|
+
# For custom spans in your code
|
|
17
|
+
from backend.services.tracing import tracer, create_span
|
|
18
|
+
|
|
19
|
+
with tracer.start_as_current_span("my-operation") as span:
|
|
20
|
+
span.set_attribute("job_id", job_id)
|
|
21
|
+
# ... do work ...
|
|
22
|
+
|
|
23
|
+
# For job-specific spans with automatic job_id attribute
|
|
24
|
+
from backend.services.tracing import job_span
|
|
25
|
+
|
|
26
|
+
with job_span("audio-worker", job_id) as span:
|
|
27
|
+
# job_id is automatically set as an attribute
|
|
28
|
+
span.set_attribute("stage", "separation")
|
|
29
|
+
# ... do work ...
|
|
30
|
+
|
|
31
|
+
# For propagating trace context through Cloud Tasks
|
|
32
|
+
from backend.services.tracing import inject_trace_context, extract_trace_context
|
|
33
|
+
|
|
34
|
+
# In worker_service.py (when creating task):
|
|
35
|
+
headers = inject_trace_context({})
|
|
36
|
+
|
|
37
|
+
# In internal.py (when receiving task):
|
|
38
|
+
context = extract_trace_context(request.headers)
|
|
39
|
+
with tracer.start_as_current_span("worker", context=context):
|
|
40
|
+
...
|
|
41
|
+
"""
|
|
42
|
+
import os
|
|
43
|
+
import logging
|
|
44
|
+
from typing import Optional, Any, Dict
|
|
45
|
+
from contextlib import contextmanager
|
|
46
|
+
from functools import wraps
|
|
47
|
+
|
|
48
|
+
# OpenTelemetry imports
|
|
49
|
+
from opentelemetry import trace
|
|
50
|
+
from opentelemetry.sdk.trace import TracerProvider
|
|
51
|
+
from opentelemetry.sdk.trace.export import BatchSpanProcessor, SimpleSpanProcessor
|
|
52
|
+
from opentelemetry.sdk.resources import Resource, SERVICE_NAME, SERVICE_VERSION
|
|
53
|
+
from opentelemetry.trace import Status, StatusCode, Span
|
|
54
|
+
from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator
|
|
55
|
+
|
|
56
|
+
# Google Cloud Trace exporter
|
|
57
|
+
try:
|
|
58
|
+
from opentelemetry.exporter.cloud_trace import CloudTraceSpanExporter
|
|
59
|
+
from opentelemetry.resourcedetector.gcp_resource_detector import GoogleCloudResourceDetector
|
|
60
|
+
GCP_AVAILABLE = True
|
|
61
|
+
except ImportError:
|
|
62
|
+
GCP_AVAILABLE = False
|
|
63
|
+
CloudTraceSpanExporter = None
|
|
64
|
+
GoogleCloudResourceDetector = None
|
|
65
|
+
|
|
66
|
+
# FastAPI instrumentation
|
|
67
|
+
try:
|
|
68
|
+
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
|
|
69
|
+
FASTAPI_INSTRUMENTATION_AVAILABLE = True
|
|
70
|
+
except ImportError:
|
|
71
|
+
FASTAPI_INSTRUMENTATION_AVAILABLE = False
|
|
72
|
+
FastAPIInstrumentor = None
|
|
73
|
+
|
|
74
|
+
# HTTPX instrumentation (for outgoing HTTP calls)
|
|
75
|
+
try:
|
|
76
|
+
from opentelemetry.instrumentation.httpx import HTTPXClientInstrumentation
|
|
77
|
+
HTTPX_INSTRUMENTATION_AVAILABLE = True
|
|
78
|
+
except ImportError:
|
|
79
|
+
HTTPX_INSTRUMENTATION_AVAILABLE = False
|
|
80
|
+
HTTPXClientInstrumentation = None
|
|
81
|
+
|
|
82
|
+
# Logging instrumentation
|
|
83
|
+
try:
|
|
84
|
+
from opentelemetry.instrumentation.logging import LoggingInstrumentor
|
|
85
|
+
LOGGING_INSTRUMENTATION_AVAILABLE = True
|
|
86
|
+
except ImportError:
|
|
87
|
+
LOGGING_INSTRUMENTATION_AVAILABLE = False
|
|
88
|
+
LoggingInstrumentor = None
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
logger = logging.getLogger(__name__)
|
|
92
|
+
|
|
93
|
+
# Global tracer instance
|
|
94
|
+
_tracer: Optional[trace.Tracer] = None
|
|
95
|
+
_initialized = False
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def setup_tracing(
|
|
99
|
+
service_name: str = "karaoke-backend",
|
|
100
|
+
service_version: str = "0.71.7",
|
|
101
|
+
enable_in_dev: bool = False,
|
|
102
|
+
) -> bool:
|
|
103
|
+
"""
|
|
104
|
+
Initialize OpenTelemetry tracing with Google Cloud Trace exporter.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
service_name: Name of the service (appears in Cloud Trace)
|
|
108
|
+
service_version: Version of the service
|
|
109
|
+
enable_in_dev: Whether to enable tracing in development (default False)
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
True if tracing was initialized, False if skipped
|
|
113
|
+
"""
|
|
114
|
+
global _tracer, _initialized
|
|
115
|
+
|
|
116
|
+
if _initialized:
|
|
117
|
+
logger.debug("Tracing already initialized")
|
|
118
|
+
return True
|
|
119
|
+
|
|
120
|
+
# Check if we should enable tracing
|
|
121
|
+
# In Cloud Run, GOOGLE_CLOUD_PROJECT is always set
|
|
122
|
+
is_cloud_run = os.environ.get("K_SERVICE") is not None
|
|
123
|
+
gcp_project = os.environ.get("GOOGLE_CLOUD_PROJECT") or os.environ.get("GCP_PROJECT")
|
|
124
|
+
|
|
125
|
+
if not is_cloud_run and not enable_in_dev:
|
|
126
|
+
logger.info("Tracing disabled in development (set enable_in_dev=True to enable)")
|
|
127
|
+
_tracer = trace.get_tracer(service_name)
|
|
128
|
+
return False
|
|
129
|
+
|
|
130
|
+
if not GCP_AVAILABLE:
|
|
131
|
+
logger.warning("Google Cloud Trace exporter not available, tracing disabled")
|
|
132
|
+
_tracer = trace.get_tracer(service_name)
|
|
133
|
+
return False
|
|
134
|
+
|
|
135
|
+
logger.info(f"Initializing OpenTelemetry tracing for {service_name} v{service_version}")
|
|
136
|
+
|
|
137
|
+
try:
|
|
138
|
+
# Create resource with service info and GCP resource detection
|
|
139
|
+
resource_attributes = {
|
|
140
|
+
SERVICE_NAME: service_name,
|
|
141
|
+
SERVICE_VERSION: service_version,
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
# Detect GCP resources (project, region, instance, etc.)
|
|
145
|
+
if GoogleCloudResourceDetector and is_cloud_run:
|
|
146
|
+
try:
|
|
147
|
+
gcp_resource = GoogleCloudResourceDetector().detect()
|
|
148
|
+
resource = Resource.create(resource_attributes).merge(gcp_resource)
|
|
149
|
+
logger.info("Detected GCP resources for tracing")
|
|
150
|
+
except Exception as e:
|
|
151
|
+
logger.warning(f"Could not detect GCP resources: {e}")
|
|
152
|
+
resource = Resource.create(resource_attributes)
|
|
153
|
+
else:
|
|
154
|
+
resource = Resource.create(resource_attributes)
|
|
155
|
+
|
|
156
|
+
# Create tracer provider
|
|
157
|
+
provider = TracerProvider(resource=resource)
|
|
158
|
+
|
|
159
|
+
# Add Cloud Trace exporter
|
|
160
|
+
if CloudTraceSpanExporter and gcp_project:
|
|
161
|
+
try:
|
|
162
|
+
exporter = CloudTraceSpanExporter(project_id=gcp_project)
|
|
163
|
+
# Use BatchSpanProcessor for production (better performance)
|
|
164
|
+
processor = BatchSpanProcessor(exporter)
|
|
165
|
+
provider.add_span_processor(processor)
|
|
166
|
+
logger.info(f"Cloud Trace exporter configured for project: {gcp_project}")
|
|
167
|
+
except Exception as e:
|
|
168
|
+
logger.warning(f"Could not configure Cloud Trace exporter: {e}")
|
|
169
|
+
|
|
170
|
+
# Set the global tracer provider
|
|
171
|
+
trace.set_tracer_provider(provider)
|
|
172
|
+
|
|
173
|
+
# Get tracer instance
|
|
174
|
+
_tracer = trace.get_tracer(service_name, service_version)
|
|
175
|
+
|
|
176
|
+
# Instrument logging to include trace context
|
|
177
|
+
if LOGGING_INSTRUMENTATION_AVAILABLE and LoggingInstrumentor:
|
|
178
|
+
try:
|
|
179
|
+
LoggingInstrumentor().instrument(set_logging_format=True)
|
|
180
|
+
logger.info("Logging instrumentation enabled")
|
|
181
|
+
except Exception as e:
|
|
182
|
+
logger.warning(f"Could not instrument logging: {e}")
|
|
183
|
+
|
|
184
|
+
# Instrument HTTPX for outgoing requests
|
|
185
|
+
if HTTPX_INSTRUMENTATION_AVAILABLE and HTTPXClientInstrumentation:
|
|
186
|
+
try:
|
|
187
|
+
HTTPXClientInstrumentation().instrument()
|
|
188
|
+
logger.info("HTTPX instrumentation enabled")
|
|
189
|
+
except Exception as e:
|
|
190
|
+
logger.warning(f"Could not instrument HTTPX: {e}")
|
|
191
|
+
|
|
192
|
+
_initialized = True
|
|
193
|
+
logger.info("OpenTelemetry tracing initialized successfully")
|
|
194
|
+
return True
|
|
195
|
+
|
|
196
|
+
except Exception as e:
|
|
197
|
+
logger.error(f"Failed to initialize tracing: {e}")
|
|
198
|
+
_tracer = trace.get_tracer(service_name)
|
|
199
|
+
return False
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def instrument_app(app) -> None:
|
|
203
|
+
"""
|
|
204
|
+
Instrument a FastAPI application with automatic tracing.
|
|
205
|
+
|
|
206
|
+
This adds spans for all incoming requests, including:
|
|
207
|
+
- Request method and path
|
|
208
|
+
- Response status code
|
|
209
|
+
- Request duration
|
|
210
|
+
- Error information
|
|
211
|
+
|
|
212
|
+
Args:
|
|
213
|
+
app: FastAPI application instance
|
|
214
|
+
"""
|
|
215
|
+
if not FASTAPI_INSTRUMENTATION_AVAILABLE or not FastAPIInstrumentor:
|
|
216
|
+
logger.warning("FastAPI instrumentation not available")
|
|
217
|
+
return
|
|
218
|
+
|
|
219
|
+
try:
|
|
220
|
+
FastAPIInstrumentor.instrument_app(
|
|
221
|
+
app,
|
|
222
|
+
excluded_urls="health,healthz,ready,readiness,ping",
|
|
223
|
+
)
|
|
224
|
+
logger.info("FastAPI instrumentation enabled")
|
|
225
|
+
except Exception as e:
|
|
226
|
+
logger.warning(f"Could not instrument FastAPI: {e}")
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def get_tracer() -> trace.Tracer:
|
|
230
|
+
"""
|
|
231
|
+
Get the global tracer instance.
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
Tracer instance (creates a no-op tracer if not initialized)
|
|
235
|
+
"""
|
|
236
|
+
global _tracer
|
|
237
|
+
if _tracer is None:
|
|
238
|
+
_tracer = trace.get_tracer("karaoke-backend")
|
|
239
|
+
return _tracer
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
# Convenience alias - lazily forwards attribute access to get_tracer()
|
|
243
|
+
class _TracerProxy:
|
|
244
|
+
"""Proxy that lazily returns the global tracer."""
|
|
245
|
+
def __getattr__(self, name):
|
|
246
|
+
return getattr(get_tracer(), name)
|
|
247
|
+
|
|
248
|
+
tracer = _TracerProxy()
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
@contextmanager
|
|
252
|
+
def create_span(
|
|
253
|
+
name: str,
|
|
254
|
+
attributes: Optional[Dict[str, Any]] = None,
|
|
255
|
+
kind: trace.SpanKind = trace.SpanKind.INTERNAL,
|
|
256
|
+
):
|
|
257
|
+
"""
|
|
258
|
+
Create a traced span as a context manager.
|
|
259
|
+
|
|
260
|
+
Usage:
|
|
261
|
+
with create_span("process-lyrics", {"job_id": job_id}) as span:
|
|
262
|
+
# ... do work ...
|
|
263
|
+
span.set_attribute("words_processed", 500)
|
|
264
|
+
|
|
265
|
+
Args:
|
|
266
|
+
name: Span name (appears in Cloud Trace)
|
|
267
|
+
attributes: Initial span attributes
|
|
268
|
+
kind: Span kind (INTERNAL, SERVER, CLIENT, PRODUCER, CONSUMER)
|
|
269
|
+
|
|
270
|
+
Yields:
|
|
271
|
+
The active span
|
|
272
|
+
"""
|
|
273
|
+
tracer = get_tracer()
|
|
274
|
+
with tracer.start_as_current_span(name, kind=kind) as span:
|
|
275
|
+
if attributes:
|
|
276
|
+
for key, value in attributes.items():
|
|
277
|
+
span.set_attribute(key, value)
|
|
278
|
+
try:
|
|
279
|
+
yield span
|
|
280
|
+
except Exception as e:
|
|
281
|
+
span.set_status(Status(StatusCode.ERROR, str(e)))
|
|
282
|
+
span.record_exception(e)
|
|
283
|
+
raise
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def traced(
|
|
287
|
+
name: Optional[str] = None,
|
|
288
|
+
attributes: Optional[Dict[str, Any]] = None,
|
|
289
|
+
):
|
|
290
|
+
"""
|
|
291
|
+
Decorator to trace a function.
|
|
292
|
+
|
|
293
|
+
Usage:
|
|
294
|
+
@traced("process-correction")
|
|
295
|
+
def process_correction(job_id: str):
|
|
296
|
+
...
|
|
297
|
+
|
|
298
|
+
@traced(attributes={"operation": "add-lyrics"})
|
|
299
|
+
async def add_lyrics(job_id: str, source: str):
|
|
300
|
+
...
|
|
301
|
+
|
|
302
|
+
Args:
|
|
303
|
+
name: Span name (defaults to function name)
|
|
304
|
+
attributes: Static attributes to add to span
|
|
305
|
+
"""
|
|
306
|
+
def decorator(func):
|
|
307
|
+
span_name = name or func.__name__
|
|
308
|
+
|
|
309
|
+
@wraps(func)
|
|
310
|
+
async def async_wrapper(*args, **kwargs):
|
|
311
|
+
with create_span(span_name, attributes) as span:
|
|
312
|
+
# Add function arguments as attributes
|
|
313
|
+
span.set_attribute("function", func.__name__)
|
|
314
|
+
return await func(*args, **kwargs)
|
|
315
|
+
|
|
316
|
+
@wraps(func)
|
|
317
|
+
def sync_wrapper(*args, **kwargs):
|
|
318
|
+
with create_span(span_name, attributes) as span:
|
|
319
|
+
span.set_attribute("function", func.__name__)
|
|
320
|
+
return func(*args, **kwargs)
|
|
321
|
+
|
|
322
|
+
# Return appropriate wrapper based on function type
|
|
323
|
+
import asyncio
|
|
324
|
+
if asyncio.iscoroutinefunction(func):
|
|
325
|
+
return async_wrapper
|
|
326
|
+
return sync_wrapper
|
|
327
|
+
|
|
328
|
+
return decorator
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def add_span_attribute(key: str, value: Any) -> None:
|
|
332
|
+
"""
|
|
333
|
+
Add an attribute to the current span.
|
|
334
|
+
|
|
335
|
+
Args:
|
|
336
|
+
key: Attribute name
|
|
337
|
+
value: Attribute value (must be string, int, float, bool, or list thereof)
|
|
338
|
+
"""
|
|
339
|
+
span = trace.get_current_span()
|
|
340
|
+
if span and span.is_recording():
|
|
341
|
+
span.set_attribute(key, value)
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def add_span_event(name: str, attributes: Optional[Dict[str, Any]] = None) -> None:
|
|
345
|
+
"""
|
|
346
|
+
Add an event to the current span.
|
|
347
|
+
|
|
348
|
+
Events are timestamped annotations that appear in the trace timeline.
|
|
349
|
+
|
|
350
|
+
Args:
|
|
351
|
+
name: Event name
|
|
352
|
+
attributes: Event attributes
|
|
353
|
+
"""
|
|
354
|
+
span = trace.get_current_span()
|
|
355
|
+
if span and span.is_recording():
|
|
356
|
+
span.add_event(name, attributes=attributes or {})
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def set_span_error(error: Exception) -> None:
|
|
360
|
+
"""
|
|
361
|
+
Mark the current span as errored.
|
|
362
|
+
|
|
363
|
+
Args:
|
|
364
|
+
error: The exception that occurred
|
|
365
|
+
"""
|
|
366
|
+
span = trace.get_current_span()
|
|
367
|
+
if span and span.is_recording():
|
|
368
|
+
span.set_status(Status(StatusCode.ERROR, str(error)))
|
|
369
|
+
span.record_exception(error)
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def get_current_trace_id() -> Optional[str]:
|
|
373
|
+
"""
|
|
374
|
+
Get the current trace ID for correlation.
|
|
375
|
+
|
|
376
|
+
This can be used to link logs to traces.
|
|
377
|
+
|
|
378
|
+
Returns:
|
|
379
|
+
Trace ID as hex string, or None if no active trace
|
|
380
|
+
"""
|
|
381
|
+
span = trace.get_current_span()
|
|
382
|
+
if span:
|
|
383
|
+
ctx = span.get_span_context()
|
|
384
|
+
if ctx.is_valid:
|
|
385
|
+
return format(ctx.trace_id, '032x')
|
|
386
|
+
return None
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def get_current_span_id() -> Optional[str]:
|
|
390
|
+
"""
|
|
391
|
+
Get the current span ID.
|
|
392
|
+
|
|
393
|
+
Returns:
|
|
394
|
+
Span ID as hex string, or None if no active span
|
|
395
|
+
"""
|
|
396
|
+
span = trace.get_current_span()
|
|
397
|
+
if span:
|
|
398
|
+
ctx = span.get_span_context()
|
|
399
|
+
if ctx.is_valid:
|
|
400
|
+
return format(ctx.span_id, '016x')
|
|
401
|
+
return None
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
# =====================================================
|
|
405
|
+
# Job-Aware Tracing Utilities
|
|
406
|
+
# =====================================================
|
|
407
|
+
|
|
408
|
+
@contextmanager
|
|
409
|
+
def job_span(
|
|
410
|
+
name: str,
|
|
411
|
+
job_id: str,
|
|
412
|
+
attributes: Optional[Dict[str, Any]] = None,
|
|
413
|
+
kind: trace.SpanKind = trace.SpanKind.INTERNAL,
|
|
414
|
+
):
|
|
415
|
+
"""
|
|
416
|
+
Create a traced span with job_id automatically set as an attribute.
|
|
417
|
+
|
|
418
|
+
This is the preferred way to create spans in job processing workers.
|
|
419
|
+
The job_id attribute enables filtering spans by job in Cloud Trace.
|
|
420
|
+
|
|
421
|
+
Usage:
|
|
422
|
+
with job_span("audio-separation", job_id) as span:
|
|
423
|
+
span.set_attribute("stage", "clean_instrumental")
|
|
424
|
+
# ... do work ...
|
|
425
|
+
|
|
426
|
+
Args:
|
|
427
|
+
name: Span name (appears in Cloud Trace)
|
|
428
|
+
job_id: Job ID to attach to the span
|
|
429
|
+
attributes: Additional span attributes
|
|
430
|
+
kind: Span kind (INTERNAL, SERVER, CLIENT, PRODUCER, CONSUMER)
|
|
431
|
+
|
|
432
|
+
Yields:
|
|
433
|
+
The active span
|
|
434
|
+
"""
|
|
435
|
+
all_attributes = {
|
|
436
|
+
"job_id": job_id,
|
|
437
|
+
"service.operation": name,
|
|
438
|
+
}
|
|
439
|
+
if attributes:
|
|
440
|
+
all_attributes.update(attributes)
|
|
441
|
+
|
|
442
|
+
with create_span(name, all_attributes, kind) as span:
|
|
443
|
+
yield span
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def inject_trace_context(headers: Dict[str, str]) -> Dict[str, str]:
|
|
447
|
+
"""
|
|
448
|
+
Inject current trace context into HTTP headers.
|
|
449
|
+
|
|
450
|
+
Use this when creating Cloud Tasks or making HTTP calls to propagate
|
|
451
|
+
the trace context so child spans link to the parent trace.
|
|
452
|
+
|
|
453
|
+
Usage:
|
|
454
|
+
headers = inject_trace_context({
|
|
455
|
+
"Content-Type": "application/json",
|
|
456
|
+
"Authorization": "Bearer token",
|
|
457
|
+
})
|
|
458
|
+
# Use headers in HTTP request or Cloud Task
|
|
459
|
+
|
|
460
|
+
Args:
|
|
461
|
+
headers: Existing headers dict to add trace context to
|
|
462
|
+
|
|
463
|
+
Returns:
|
|
464
|
+
Headers dict with traceparent/tracestate headers added
|
|
465
|
+
"""
|
|
466
|
+
propagator = TraceContextTextMapPropagator()
|
|
467
|
+
carrier = dict(headers) # Make a copy
|
|
468
|
+
propagator.inject(carrier)
|
|
469
|
+
return carrier
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
def extract_trace_context(headers: Dict[str, str]) -> Optional[trace.Context]:
|
|
473
|
+
"""
|
|
474
|
+
Extract trace context from HTTP headers.
|
|
475
|
+
|
|
476
|
+
Use this when receiving Cloud Tasks or HTTP requests to continue
|
|
477
|
+
the trace from the parent span.
|
|
478
|
+
|
|
479
|
+
Usage:
|
|
480
|
+
context = extract_trace_context(request.headers)
|
|
481
|
+
with tracer.start_as_current_span("worker", context=context):
|
|
482
|
+
# ... processing ...
|
|
483
|
+
|
|
484
|
+
Args:
|
|
485
|
+
headers: HTTP headers containing traceparent/tracestate
|
|
486
|
+
|
|
487
|
+
Returns:
|
|
488
|
+
Context object for use with start_as_current_span, or None
|
|
489
|
+
"""
|
|
490
|
+
propagator = TraceContextTextMapPropagator()
|
|
491
|
+
return propagator.extract(headers)
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
def start_span_with_context(
|
|
495
|
+
name: str,
|
|
496
|
+
context: Optional[trace.Context],
|
|
497
|
+
job_id: Optional[str] = None,
|
|
498
|
+
attributes: Optional[Dict[str, Any]] = None,
|
|
499
|
+
):
|
|
500
|
+
"""
|
|
501
|
+
Start a span as child of the given context (or current context if None).
|
|
502
|
+
|
|
503
|
+
This is useful for starting worker spans that should link to the
|
|
504
|
+
trace from the original API request.
|
|
505
|
+
|
|
506
|
+
Usage:
|
|
507
|
+
context = extract_trace_context(request.headers)
|
|
508
|
+
with start_span_with_context("audio-worker", context, job_id) as span:
|
|
509
|
+
# ... processing ...
|
|
510
|
+
|
|
511
|
+
Args:
|
|
512
|
+
name: Span name
|
|
513
|
+
context: Parent context (from extract_trace_context) or None
|
|
514
|
+
job_id: Optional job ID to set as attribute
|
|
515
|
+
attributes: Additional span attributes
|
|
516
|
+
|
|
517
|
+
Returns:
|
|
518
|
+
Context manager yielding the span
|
|
519
|
+
"""
|
|
520
|
+
tracer = get_tracer()
|
|
521
|
+
|
|
522
|
+
all_attributes = attributes or {}
|
|
523
|
+
if job_id:
|
|
524
|
+
all_attributes["job_id"] = job_id
|
|
525
|
+
all_attributes["service.operation"] = name
|
|
526
|
+
|
|
527
|
+
@contextmanager
|
|
528
|
+
def _span():
|
|
529
|
+
kwargs = {"kind": trace.SpanKind.INTERNAL}
|
|
530
|
+
if context is not None:
|
|
531
|
+
kwargs["context"] = context
|
|
532
|
+
|
|
533
|
+
with tracer.start_as_current_span(name, **kwargs) as span:
|
|
534
|
+
for key, value in all_attributes.items():
|
|
535
|
+
span.set_attribute(key, value)
|
|
536
|
+
try:
|
|
537
|
+
yield span
|
|
538
|
+
except Exception as e:
|
|
539
|
+
span.set_status(Status(StatusCode.ERROR, str(e)))
|
|
540
|
+
span.record_exception(e)
|
|
541
|
+
raise
|
|
542
|
+
|
|
543
|
+
return _span()
|