|
4 | 4 | """ |
5 | 5 |
|
6 | 6 | import asyncio |
| 7 | +import json |
7 | 8 | import logging |
8 | 9 | import os |
9 | 10 | import re |
|
27 | 28 |
|
28 | 29 | import backoff |
29 | 30 | import httpx |
30 | | -from opentelemetry import trace |
31 | | -from opentelemetry import trace as otel_trace_api |
| 31 | +from opentelemetry import ( |
| 32 | + trace as otel_trace_api, |
| 33 | + baggage as otel_baggage_api, |
| 34 | + context as otel_context_api, |
| 35 | +) |
32 | 36 | from opentelemetry.sdk.trace import TracerProvider |
33 | 37 | from opentelemetry.sdk.trace.id_generator import RandomIdGenerator |
34 | 38 | from opentelemetry.util._decorator import ( |
|
111 | 115 | ) |
112 | 116 | from langfuse.types import MaskFunction, ScoreDataType, SpanLevel, TraceContext |
113 | 117 |
|
| 118 | +# Context key constants for Langfuse context propagation |
| 119 | +LANGFUSE_CTX_USER_ID = "langfuse.ctx.user.id" |
| 120 | +LANGFUSE_CTX_SESSION_ID = "langfuse.ctx.session.id" |
| 121 | +LANGFUSE_CTX_METADATA = "langfuse.ctx.metadata" |
| 122 | + |
114 | 123 |
|
115 | 124 | class Langfuse: |
116 | 125 | """Main client for Langfuse tracing and platform features. |
@@ -1667,6 +1676,11 @@ def update_current_trace( |
1667 | 1676 | span.update(output=response) |
1668 | 1677 | ``` |
1669 | 1678 | """ |
| 1679 | + warnings.warn( |
| 1680 | + "update_current_trace is deprecated and will be removed in a future version. ", |
| 1681 | + DeprecationWarning, |
| 1682 | + stacklevel=2, |
| 1683 | + ) |
1670 | 1684 | if not self._tracing_enabled: |
1671 | 1685 | langfuse_logger.debug( |
1672 | 1686 | "Operation skipped: update_current_trace - Tracing is disabled or client is in no-op mode." |
@@ -1811,7 +1825,7 @@ def _create_remote_parent_span( |
1811 | 1825 | is_remote=False, |
1812 | 1826 | ) |
1813 | 1827 |
|
1814 | | - return trace.NonRecordingSpan(span_context) |
| 1828 | + return otel_trace_api.NonRecordingSpan(span_context) |
1815 | 1829 |
|
1816 | 1830 | def _is_valid_trace_id(self, trace_id: str) -> bool: |
1817 | 1831 | pattern = r"^[0-9a-f]{32}$" |
@@ -3450,3 +3464,186 @@ def clear_prompt_cache(self) -> None: |
3450 | 3464 | """ |
3451 | 3465 | if self._resources is not None: |
3452 | 3466 | self._resources.prompt_cache.clear() |
| 3467 | + |
| 3468 | + @_agnosticcontextmanager |
| 3469 | + def session(self, id: str, *, as_baggage: bool = False) -> _AgnosticContextManager: |
| 3470 | + """Create a session context manager that propagates session_id to all child spans. |
| 3471 | +
|
| 3472 | + Args: |
| 3473 | + id (str): The session identifier to propagate to child spans. |
| 3474 | + as_baggage (bool, optional): If True, stores the session_id in OpenTelemetry baggage |
| 3475 | + for cross-service propagation. If False, stores only in local context for |
| 3476 | + current-service propagation. Defaults to False. |
| 3477 | +
|
| 3478 | + Returns: |
| 3479 | + Context manager that sets session_id on all spans created within its scope. |
| 3480 | +
|
| 3481 | + Warning: |
| 3482 | + When as_baggage=True, the session_id will be included in HTTP headers of any |
| 3483 | + outbound requests made within this context. Only use this for non-sensitive |
| 3484 | + identifiers that are safe to transmit across service boundaries. |
| 3485 | +
|
| 3486 | + Example: |
| 3487 | + ```python |
| 3488 | + # Local context only (default) |
| 3489 | + with langfuse.session(id="session_123"): |
| 3490 | + with langfuse.start_as_current_span(name="process-request") as span: |
| 3491 | + # This span and all its children will have session_id="session_123" |
| 3492 | + child_span = langfuse.start_span(name="child-operation") |
| 3493 | +
|
| 3494 | + # Cross-service propagation (use with caution) |
| 3495 | + with langfuse.session(id="session_123", as_baggage=True): |
| 3496 | + # session_id will be propagated to external service calls |
| 3497 | + response = requests.get("https://api.example.com/data") |
| 3498 | + ``` |
| 3499 | + """ |
| 3500 | + # Set context variable |
| 3501 | + new_context = otel_context_api.set_value(LANGFUSE_CTX_SESSION_ID, id) |
| 3502 | + token = otel_context_api.attach(new_context) |
| 3503 | + |
| 3504 | + # Set baggage if requested |
| 3505 | + baggage_token = None |
| 3506 | + if as_baggage: |
| 3507 | + new_baggage = otel_baggage_api.set_baggage("session.id", id) |
| 3508 | + baggage_token = otel_context_api.attach(new_baggage) |
| 3509 | + |
| 3510 | + try: |
| 3511 | + yield |
| 3512 | + finally: |
| 3513 | + # Always detach context token |
| 3514 | + otel_context_api.detach(token) |
| 3515 | + |
| 3516 | + # Detach baggage token if it was set |
| 3517 | + if baggage_token is not None: |
| 3518 | + otel_context_api.detach(baggage_token) |
| 3519 | + |
| 3520 | + @_agnosticcontextmanager |
| 3521 | + def user(self, id: str, *, as_baggage: bool = False) -> _AgnosticContextManager: |
| 3522 | + """Create a user context manager that propagates user_id to all child spans. |
| 3523 | +
|
| 3524 | + Args: |
| 3525 | + id (str): The user identifier to propagate to child spans. |
| 3526 | + as_baggage (bool, optional): If True, stores the user_id in OpenTelemetry baggage |
| 3527 | + for cross-service propagation. If False, stores only in local context for |
| 3528 | + current-service propagation. Defaults to False. |
| 3529 | +
|
| 3530 | + Returns: |
| 3531 | + Context manager that sets user_id on all spans created within its scope. |
| 3532 | +
|
| 3533 | + Warning: |
| 3534 | + When as_baggage=True, the user_id will be included in HTTP headers of any |
| 3535 | + outbound requests made within this context. This may leak sensitive user |
| 3536 | + information to external services. Use with extreme caution. |
| 3537 | +
|
| 3538 | + Example: |
| 3539 | + ```python |
| 3540 | + # Local context only (default, recommended for user IDs) |
| 3541 | + with langfuse.user(id="user_456"): |
| 3542 | + with langfuse.start_as_current_span(name="user-action") as span: |
| 3543 | + # This span and all its children will have user_id="user_456" |
| 3544 | + pass |
| 3545 | +
|
| 3546 | + # Cross-service propagation (NOT recommended for sensitive user IDs) |
| 3547 | + with langfuse.user(id="public_user_456", as_baggage=True): |
| 3548 | + # user_id will be propagated to external service calls |
| 3549 | + response = requests.get("https://api.example.com/data") |
| 3550 | + ``` |
| 3551 | + """ |
| 3552 | + # Set context variable |
| 3553 | + new_context = otel_context_api.set_value(LANGFUSE_CTX_USER_ID, id) |
| 3554 | + token = otel_context_api.attach(new_context) |
| 3555 | + |
| 3556 | + # Set baggage if requested |
| 3557 | + baggage_token = None |
| 3558 | + if as_baggage: |
| 3559 | + new_baggage = otel_baggage_api.set_baggage("user.id", id) |
| 3560 | + baggage_token = otel_context_api.attach(new_baggage) |
| 3561 | + |
| 3562 | + try: |
| 3563 | + yield |
| 3564 | + finally: |
| 3565 | + # Always detach context token |
| 3566 | + otel_context_api.detach(token) |
| 3567 | + |
| 3568 | + # Detach baggage token if it was set |
| 3569 | + if baggage_token is not None: |
| 3570 | + otel_context_api.detach(baggage_token) |
| 3571 | + |
| 3572 | + @_agnosticcontextmanager |
| 3573 | + def metadata( |
| 3574 | + self, *, as_baggage: bool = False, **kwargs |
| 3575 | + ) -> _AgnosticContextManager: |
| 3576 | + """Create a metadata context manager that propagates metadata to all child spans. |
| 3577 | +
|
| 3578 | + Args: |
| 3579 | + as_baggage (bool, optional): If True, stores the metadata in OpenTelemetry baggage |
| 3580 | + for cross-service propagation. If False, stores only in local context for |
| 3581 | + current-service propagation. Defaults to False. |
| 3582 | + **kwargs: Metadata key-value pairs. Values should not exceed 200 characters. |
| 3583 | +
|
| 3584 | + Returns: |
| 3585 | + Context manager that sets metadata on all spans created within its scope. |
| 3586 | +
|
| 3587 | + Warning: |
| 3588 | + When as_baggage=True, all metadata key-value pairs will be included in HTTP |
| 3589 | + headers of any outbound requests made within this context. Ensure no sensitive |
| 3590 | + information is included in the metadata when using cross-service propagation. |
| 3591 | +
|
| 3592 | + Example: |
| 3593 | + ```python |
| 3594 | + # Local context only (default) |
| 3595 | + with langfuse.metadata(experiment="A/B", version="1.2.3"): |
| 3596 | + with langfuse.start_as_current_span(name="experiment-run") as span: |
| 3597 | + # This span and all its children will have the metadata |
| 3598 | + pass |
| 3599 | +
|
| 3600 | + # Cross-service propagation (use with caution) |
| 3601 | + with langfuse.metadata(as_baggage=True, experiment="A/B", service="api"): |
| 3602 | + # metadata will be propagated to external service calls |
| 3603 | + response = requests.get("https://api.example.com/data") |
| 3604 | + ``` |
| 3605 | + """ |
| 3606 | + if not kwargs: |
| 3607 | + # No metadata to set, just yield |
| 3608 | + yield |
| 3609 | + return |
| 3610 | + |
| 3611 | + # Convert metadata dict to JSON string for context storage |
| 3612 | + metadata_json = json.dumps(kwargs) |
| 3613 | + |
| 3614 | + # Set context variable |
| 3615 | + new_context = otel_context_api.set_value(LANGFUSE_CTX_METADATA, metadata_json) |
| 3616 | + token = otel_context_api.attach(new_context) |
| 3617 | + |
| 3618 | + # Set baggage if requested |
| 3619 | + baggage_tokens = [] |
| 3620 | + if as_baggage: |
| 3621 | + current_baggage = otel_baggage_api.get_all() |
| 3622 | + new_baggage = current_baggage |
| 3623 | + |
| 3624 | + # Add each metadata key-value pair to baggage |
| 3625 | + for key, value in kwargs.items(): |
| 3626 | + # Convert value to string and truncate if needed for baggage |
| 3627 | + str_value = str(value) |
| 3628 | + if len(str_value) > 200: |
| 3629 | + str_value = str_value[:200] |
| 3630 | + |
| 3631 | + baggage_key = f"metadata.{key}" |
| 3632 | + new_baggage = otel_baggage_api.set_baggage( |
| 3633 | + baggage_key, str_value, new_baggage |
| 3634 | + ) |
| 3635 | + |
| 3636 | + # Attach the new baggage context |
| 3637 | + if new_baggage != current_baggage: |
| 3638 | + baggage_token = otel_context_api.attach(new_baggage) |
| 3639 | + baggage_tokens.append(baggage_token) |
| 3640 | + |
| 3641 | + try: |
| 3642 | + yield |
| 3643 | + finally: |
| 3644 | + # Always detach context token |
| 3645 | + otel_context_api.detach(token) |
| 3646 | + |
| 3647 | + # Detach all baggage tokens if they were set |
| 3648 | + for baggage_token in baggage_tokens: |
| 3649 | + otel_context_api.detach(baggage_token) |
0 commit comments