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.
Files changed (187) hide show
  1. backend/.coveragerc +20 -0
  2. backend/.gitignore +37 -0
  3. backend/Dockerfile +43 -0
  4. backend/Dockerfile.base +74 -0
  5. backend/README.md +242 -0
  6. backend/__init__.py +0 -0
  7. backend/api/__init__.py +0 -0
  8. backend/api/dependencies.py +457 -0
  9. backend/api/routes/__init__.py +0 -0
  10. backend/api/routes/admin.py +742 -0
  11. backend/api/routes/audio_search.py +903 -0
  12. backend/api/routes/auth.py +348 -0
  13. backend/api/routes/file_upload.py +2076 -0
  14. backend/api/routes/health.py +344 -0
  15. backend/api/routes/internal.py +435 -0
  16. backend/api/routes/jobs.py +1610 -0
  17. backend/api/routes/review.py +652 -0
  18. backend/api/routes/themes.py +162 -0
  19. backend/api/routes/users.py +1014 -0
  20. backend/config.py +172 -0
  21. backend/main.py +133 -0
  22. backend/middleware/__init__.py +5 -0
  23. backend/middleware/audit_logging.py +124 -0
  24. backend/models/__init__.py +0 -0
  25. backend/models/job.py +519 -0
  26. backend/models/requests.py +123 -0
  27. backend/models/theme.py +153 -0
  28. backend/models/user.py +254 -0
  29. backend/models/worker_log.py +164 -0
  30. backend/pyproject.toml +29 -0
  31. backend/quick-check.sh +93 -0
  32. backend/requirements.txt +29 -0
  33. backend/run_tests.sh +60 -0
  34. backend/services/__init__.py +0 -0
  35. backend/services/audio_analysis_service.py +243 -0
  36. backend/services/audio_editing_service.py +278 -0
  37. backend/services/audio_search_service.py +702 -0
  38. backend/services/auth_service.py +630 -0
  39. backend/services/credential_manager.py +792 -0
  40. backend/services/discord_service.py +172 -0
  41. backend/services/dropbox_service.py +301 -0
  42. backend/services/email_service.py +1093 -0
  43. backend/services/encoding_interface.py +454 -0
  44. backend/services/encoding_service.py +405 -0
  45. backend/services/firestore_service.py +512 -0
  46. backend/services/flacfetch_client.py +573 -0
  47. backend/services/gce_encoding/README.md +72 -0
  48. backend/services/gce_encoding/__init__.py +22 -0
  49. backend/services/gce_encoding/main.py +589 -0
  50. backend/services/gce_encoding/requirements.txt +16 -0
  51. backend/services/gdrive_service.py +356 -0
  52. backend/services/job_logging.py +258 -0
  53. backend/services/job_manager.py +842 -0
  54. backend/services/job_notification_service.py +271 -0
  55. backend/services/local_encoding_service.py +590 -0
  56. backend/services/local_preview_encoding_service.py +407 -0
  57. backend/services/lyrics_cache_service.py +216 -0
  58. backend/services/metrics.py +413 -0
  59. backend/services/packaging_service.py +287 -0
  60. backend/services/rclone_service.py +106 -0
  61. backend/services/storage_service.py +209 -0
  62. backend/services/stripe_service.py +275 -0
  63. backend/services/structured_logging.py +254 -0
  64. backend/services/template_service.py +330 -0
  65. backend/services/theme_service.py +469 -0
  66. backend/services/tracing.py +543 -0
  67. backend/services/user_service.py +721 -0
  68. backend/services/worker_service.py +558 -0
  69. backend/services/youtube_service.py +112 -0
  70. backend/services/youtube_upload_service.py +445 -0
  71. backend/tests/__init__.py +4 -0
  72. backend/tests/conftest.py +224 -0
  73. backend/tests/emulator/__init__.py +7 -0
  74. backend/tests/emulator/conftest.py +88 -0
  75. backend/tests/emulator/test_e2e_cli_backend.py +1053 -0
  76. backend/tests/emulator/test_emulator_integration.py +356 -0
  77. backend/tests/emulator/test_style_loading_direct.py +436 -0
  78. backend/tests/emulator/test_worker_logs_direct.py +229 -0
  79. backend/tests/emulator/test_worker_logs_subcollection.py +443 -0
  80. backend/tests/requirements-test.txt +10 -0
  81. backend/tests/requirements.txt +6 -0
  82. backend/tests/test_admin_email_endpoints.py +411 -0
  83. backend/tests/test_api_integration.py +460 -0
  84. backend/tests/test_api_routes.py +93 -0
  85. backend/tests/test_audio_analysis_service.py +294 -0
  86. backend/tests/test_audio_editing_service.py +386 -0
  87. backend/tests/test_audio_search.py +1398 -0
  88. backend/tests/test_audio_services.py +378 -0
  89. backend/tests/test_auth_firestore.py +231 -0
  90. backend/tests/test_config_extended.py +68 -0
  91. backend/tests/test_credential_manager.py +377 -0
  92. backend/tests/test_dependencies.py +54 -0
  93. backend/tests/test_discord_service.py +244 -0
  94. backend/tests/test_distribution_services.py +820 -0
  95. backend/tests/test_dropbox_service.py +472 -0
  96. backend/tests/test_email_service.py +492 -0
  97. backend/tests/test_emulator_integration.py +322 -0
  98. backend/tests/test_encoding_interface.py +412 -0
  99. backend/tests/test_file_upload.py +1739 -0
  100. backend/tests/test_flacfetch_client.py +632 -0
  101. backend/tests/test_gdrive_service.py +524 -0
  102. backend/tests/test_instrumental_api.py +431 -0
  103. backend/tests/test_internal_api.py +343 -0
  104. backend/tests/test_job_creation_regression.py +583 -0
  105. backend/tests/test_job_manager.py +339 -0
  106. backend/tests/test_job_manager_notifications.py +329 -0
  107. backend/tests/test_job_notification_service.py +443 -0
  108. backend/tests/test_jobs_api.py +273 -0
  109. backend/tests/test_local_encoding_service.py +423 -0
  110. backend/tests/test_local_preview_encoding_service.py +567 -0
  111. backend/tests/test_main.py +87 -0
  112. backend/tests/test_models.py +918 -0
  113. backend/tests/test_packaging_service.py +382 -0
  114. backend/tests/test_requests.py +201 -0
  115. backend/tests/test_routes_jobs.py +282 -0
  116. backend/tests/test_routes_review.py +337 -0
  117. backend/tests/test_services.py +556 -0
  118. backend/tests/test_services_extended.py +112 -0
  119. backend/tests/test_storage_service.py +448 -0
  120. backend/tests/test_style_upload.py +261 -0
  121. backend/tests/test_template_service.py +295 -0
  122. backend/tests/test_theme_service.py +516 -0
  123. backend/tests/test_unicode_sanitization.py +522 -0
  124. backend/tests/test_upload_api.py +256 -0
  125. backend/tests/test_validate.py +156 -0
  126. backend/tests/test_video_worker_orchestrator.py +847 -0
  127. backend/tests/test_worker_log_subcollection.py +509 -0
  128. backend/tests/test_worker_logging.py +365 -0
  129. backend/tests/test_workers.py +1116 -0
  130. backend/tests/test_workers_extended.py +178 -0
  131. backend/tests/test_youtube_service.py +247 -0
  132. backend/tests/test_youtube_upload_service.py +568 -0
  133. backend/validate.py +173 -0
  134. backend/version.py +27 -0
  135. backend/workers/README.md +597 -0
  136. backend/workers/__init__.py +11 -0
  137. backend/workers/audio_worker.py +618 -0
  138. backend/workers/lyrics_worker.py +683 -0
  139. backend/workers/render_video_worker.py +483 -0
  140. backend/workers/screens_worker.py +525 -0
  141. backend/workers/style_helper.py +198 -0
  142. backend/workers/video_worker.py +1277 -0
  143. backend/workers/video_worker_orchestrator.py +701 -0
  144. backend/workers/worker_logging.py +278 -0
  145. karaoke_gen/instrumental_review/static/index.html +7 -4
  146. karaoke_gen/karaoke_finalise/karaoke_finalise.py +6 -1
  147. karaoke_gen/utils/__init__.py +163 -8
  148. karaoke_gen/video_background_processor.py +9 -4
  149. {karaoke_gen-0.90.1.dist-info → karaoke_gen-0.96.0.dist-info}/METADATA +1 -1
  150. {karaoke_gen-0.90.1.dist-info → karaoke_gen-0.96.0.dist-info}/RECORD +186 -41
  151. lyrics_transcriber/correction/agentic/providers/config.py +9 -5
  152. lyrics_transcriber/correction/agentic/providers/langchain_bridge.py +1 -51
  153. lyrics_transcriber/correction/corrector.py +192 -130
  154. lyrics_transcriber/correction/operations.py +24 -9
  155. lyrics_transcriber/frontend/package-lock.json +2 -2
  156. lyrics_transcriber/frontend/package.json +1 -1
  157. lyrics_transcriber/frontend/src/components/AIFeedbackModal.tsx +1 -1
  158. lyrics_transcriber/frontend/src/components/CorrectedWordWithActions.tsx +11 -7
  159. lyrics_transcriber/frontend/src/components/EditActionBar.tsx +31 -5
  160. lyrics_transcriber/frontend/src/components/EditModal.tsx +28 -10
  161. lyrics_transcriber/frontend/src/components/EditTimelineSection.tsx +123 -27
  162. lyrics_transcriber/frontend/src/components/EditWordList.tsx +112 -60
  163. lyrics_transcriber/frontend/src/components/Header.tsx +90 -76
  164. lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx +53 -31
  165. lyrics_transcriber/frontend/src/components/LyricsSynchronizer/SyncControls.tsx +44 -13
  166. lyrics_transcriber/frontend/src/components/LyricsSynchronizer/TimelineCanvas.tsx +66 -50
  167. lyrics_transcriber/frontend/src/components/LyricsSynchronizer/index.tsx +124 -30
  168. lyrics_transcriber/frontend/src/components/ReferenceView.tsx +1 -1
  169. lyrics_transcriber/frontend/src/components/TimelineEditor.tsx +12 -5
  170. lyrics_transcriber/frontend/src/components/TimingOffsetModal.tsx +3 -3
  171. lyrics_transcriber/frontend/src/components/TranscriptionView.tsx +1 -1
  172. lyrics_transcriber/frontend/src/components/WordDivider.tsx +11 -7
  173. lyrics_transcriber/frontend/src/components/shared/components/Word.tsx +4 -2
  174. lyrics_transcriber/frontend/src/hooks/useManualSync.ts +103 -1
  175. lyrics_transcriber/frontend/src/theme.ts +42 -15
  176. lyrics_transcriber/frontend/tsconfig.tsbuildinfo +1 -1
  177. lyrics_transcriber/frontend/vite.config.js +5 -0
  178. lyrics_transcriber/frontend/web_assets/assets/{index-BECn1o8Q.js → index-BSMgOq4Z.js} +6959 -5782
  179. lyrics_transcriber/frontend/web_assets/assets/index-BSMgOq4Z.js.map +1 -0
  180. lyrics_transcriber/frontend/web_assets/index.html +6 -2
  181. lyrics_transcriber/frontend/web_assets/nomad-karaoke-logo.svg +5 -0
  182. lyrics_transcriber/output/generator.py +17 -3
  183. lyrics_transcriber/output/video.py +60 -95
  184. lyrics_transcriber/frontend/web_assets/assets/index-BECn1o8Q.js.map +0 -1
  185. {karaoke_gen-0.90.1.dist-info → karaoke_gen-0.96.0.dist-info}/WHEEL +0 -0
  186. {karaoke_gen-0.90.1.dist-info → karaoke_gen-0.96.0.dist-info}/entry_points.txt +0 -0
  187. {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()