mirascope 2.0.0a2__py3-none-any.whl → 2.0.0a3__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.
- mirascope/__init__.py +2 -2
- mirascope/api/__init__.py +6 -0
- mirascope/api/_generated/README.md +207 -0
- mirascope/api/_generated/__init__.py +85 -0
- mirascope/api/_generated/client.py +155 -0
- mirascope/api/_generated/core/__init__.py +52 -0
- mirascope/api/_generated/core/api_error.py +23 -0
- mirascope/api/_generated/core/client_wrapper.py +58 -0
- mirascope/api/_generated/core/datetime_utils.py +30 -0
- mirascope/api/_generated/core/file.py +70 -0
- mirascope/api/_generated/core/force_multipart.py +16 -0
- mirascope/api/_generated/core/http_client.py +619 -0
- mirascope/api/_generated/core/http_response.py +55 -0
- mirascope/api/_generated/core/jsonable_encoder.py +102 -0
- mirascope/api/_generated/core/pydantic_utilities.py +310 -0
- mirascope/api/_generated/core/query_encoder.py +60 -0
- mirascope/api/_generated/core/remove_none_from_dict.py +11 -0
- mirascope/api/_generated/core/request_options.py +35 -0
- mirascope/api/_generated/core/serialization.py +282 -0
- mirascope/api/_generated/docs/__init__.py +4 -0
- mirascope/api/_generated/docs/client.py +95 -0
- mirascope/api/_generated/docs/raw_client.py +132 -0
- mirascope/api/_generated/environment.py +9 -0
- mirascope/api/_generated/errors/__init__.py +7 -0
- mirascope/api/_generated/errors/bad_request_error.py +15 -0
- mirascope/api/_generated/health/__init__.py +7 -0
- mirascope/api/_generated/health/client.py +96 -0
- mirascope/api/_generated/health/raw_client.py +129 -0
- mirascope/api/_generated/health/types/__init__.py +8 -0
- mirascope/api/_generated/health/types/health_check_response.py +24 -0
- mirascope/api/_generated/health/types/health_check_response_status.py +5 -0
- mirascope/api/_generated/reference.md +167 -0
- mirascope/api/_generated/traces/__init__.py +55 -0
- mirascope/api/_generated/traces/client.py +162 -0
- mirascope/api/_generated/traces/raw_client.py +168 -0
- mirascope/api/_generated/traces/types/__init__.py +95 -0
- mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item.py +36 -0
- mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_resource.py +31 -0
- mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_resource_attributes_item.py +25 -0
- mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_resource_attributes_item_value.py +54 -0
- mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_resource_attributes_item_value_array_value.py +23 -0
- mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_resource_attributes_item_value_kvlist_value.py +28 -0
- mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_resource_attributes_item_value_kvlist_value_values_item.py +24 -0
- mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_scope_spans_item.py +35 -0
- mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_scope_spans_item_scope.py +35 -0
- mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_scope_spans_item_scope_attributes_item.py +27 -0
- mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_scope_spans_item_scope_attributes_item_value.py +54 -0
- mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_scope_spans_item_scope_attributes_item_value_array_value.py +23 -0
- mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_scope_spans_item_scope_attributes_item_value_kvlist_value.py +28 -0
- mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_scope_spans_item_scope_attributes_item_value_kvlist_value_values_item.py +24 -0
- mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_scope_spans_item_spans_item.py +60 -0
- mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_scope_spans_item_spans_item_attributes_item.py +29 -0
- mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_scope_spans_item_spans_item_attributes_item_value.py +54 -0
- mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_scope_spans_item_spans_item_attributes_item_value_array_value.py +23 -0
- mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_scope_spans_item_spans_item_attributes_item_value_kvlist_value.py +28 -0
- mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_scope_spans_item_spans_item_attributes_item_value_kvlist_value_values_item.py +24 -0
- mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_scope_spans_item_spans_item_status.py +24 -0
- mirascope/api/_generated/traces/types/traces_create_response.py +27 -0
- mirascope/api/_generated/traces/types/traces_create_response_partial_success.py +28 -0
- mirascope/api/_generated/types/__init__.py +21 -0
- mirascope/api/_generated/types/http_api_decode_error.py +31 -0
- mirascope/api/_generated/types/http_api_decode_error_tag.py +5 -0
- mirascope/api/_generated/types/issue.py +44 -0
- mirascope/api/_generated/types/issue_tag.py +17 -0
- mirascope/api/_generated/types/property_key.py +7 -0
- mirascope/api/_generated/types/property_key_tag.py +29 -0
- mirascope/api/_generated/types/property_key_tag_tag.py +5 -0
- mirascope/api/client.py +255 -0
- mirascope/api/settings.py +81 -0
- mirascope/llm/__init__.py +41 -11
- mirascope/llm/calls/calls.py +81 -57
- mirascope/llm/calls/decorator.py +121 -115
- mirascope/llm/content/__init__.py +3 -2
- mirascope/llm/context/_utils.py +19 -6
- mirascope/llm/exceptions.py +30 -16
- mirascope/llm/formatting/_utils.py +9 -5
- mirascope/llm/formatting/format.py +2 -2
- mirascope/llm/formatting/from_call_args.py +2 -2
- mirascope/llm/messages/message.py +13 -5
- mirascope/llm/models/__init__.py +2 -2
- mirascope/llm/models/models.py +189 -81
- mirascope/llm/prompts/__init__.py +13 -12
- mirascope/llm/prompts/_utils.py +27 -24
- mirascope/llm/prompts/decorator.py +133 -204
- mirascope/llm/prompts/prompts.py +424 -0
- mirascope/llm/prompts/protocols.py +25 -59
- mirascope/llm/providers/__init__.py +38 -0
- mirascope/llm/{clients → providers}/_missing_import_stubs.py +8 -6
- mirascope/llm/providers/anthropic/__init__.py +24 -0
- mirascope/llm/{clients → providers}/anthropic/_utils/decode.py +5 -4
- mirascope/llm/{clients → providers}/anthropic/_utils/encode.py +31 -10
- mirascope/llm/providers/anthropic/model_id.py +40 -0
- mirascope/llm/{clients/anthropic/clients.py → providers/anthropic/provider.py} +33 -418
- mirascope/llm/{clients → providers}/base/__init__.py +3 -3
- mirascope/llm/{clients → providers}/base/_utils.py +10 -7
- mirascope/llm/{clients/base/client.py → providers/base/base_provider.py} +255 -126
- mirascope/llm/providers/google/__init__.py +21 -0
- mirascope/llm/{clients → providers}/google/_utils/decode.py +6 -4
- mirascope/llm/{clients → providers}/google/_utils/encode.py +30 -24
- mirascope/llm/providers/google/model_id.py +28 -0
- mirascope/llm/providers/google/provider.py +438 -0
- mirascope/llm/providers/load_provider.py +48 -0
- mirascope/llm/providers/mlx/__init__.py +24 -0
- mirascope/llm/providers/mlx/_utils.py +107 -0
- mirascope/llm/providers/mlx/encoding/__init__.py +8 -0
- mirascope/llm/providers/mlx/encoding/base.py +69 -0
- mirascope/llm/providers/mlx/encoding/transformers.py +131 -0
- mirascope/llm/providers/mlx/mlx.py +237 -0
- mirascope/llm/providers/mlx/model_id.py +17 -0
- mirascope/llm/providers/mlx/provider.py +411 -0
- mirascope/llm/providers/model_id.py +16 -0
- mirascope/llm/providers/openai/__init__.py +6 -0
- mirascope/llm/providers/openai/completions/__init__.py +20 -0
- mirascope/llm/{clients/openai/responses → providers/openai/completions}/_utils/__init__.py +2 -0
- mirascope/llm/{clients → providers}/openai/completions/_utils/decode.py +5 -3
- mirascope/llm/{clients → providers}/openai/completions/_utils/encode.py +33 -23
- mirascope/llm/providers/openai/completions/provider.py +456 -0
- mirascope/llm/providers/openai/model_id.py +31 -0
- mirascope/llm/providers/openai/model_info.py +246 -0
- mirascope/llm/providers/openai/provider.py +386 -0
- mirascope/llm/providers/openai/responses/__init__.py +21 -0
- mirascope/llm/{clients → providers}/openai/responses/_utils/decode.py +5 -3
- mirascope/llm/{clients → providers}/openai/responses/_utils/encode.py +28 -17
- mirascope/llm/providers/openai/responses/provider.py +470 -0
- mirascope/llm/{clients → providers}/openai/shared/_utils.py +7 -3
- mirascope/llm/providers/provider_id.py +13 -0
- mirascope/llm/providers/provider_registry.py +167 -0
- mirascope/llm/responses/base_response.py +10 -5
- mirascope/llm/responses/base_stream_response.py +10 -5
- mirascope/llm/responses/response.py +24 -13
- mirascope/llm/responses/root_response.py +7 -12
- mirascope/llm/responses/stream_response.py +35 -23
- mirascope/llm/tools/__init__.py +9 -2
- mirascope/llm/tools/_utils.py +12 -3
- mirascope/llm/tools/protocols.py +4 -4
- mirascope/llm/tools/tool_schema.py +44 -9
- mirascope/llm/tools/tools.py +10 -9
- mirascope/ops/__init__.py +156 -0
- mirascope/ops/_internal/__init__.py +5 -0
- mirascope/ops/_internal/closure.py +1118 -0
- mirascope/ops/_internal/configuration.py +126 -0
- mirascope/ops/_internal/context.py +76 -0
- mirascope/ops/_internal/exporters/__init__.py +26 -0
- mirascope/ops/_internal/exporters/exporters.py +342 -0
- mirascope/ops/_internal/exporters/processors.py +104 -0
- mirascope/ops/_internal/exporters/types.py +165 -0
- mirascope/ops/_internal/exporters/utils.py +29 -0
- mirascope/ops/_internal/instrumentation/__init__.py +8 -0
- mirascope/ops/_internal/instrumentation/llm/__init__.py +8 -0
- mirascope/ops/_internal/instrumentation/llm/encode.py +238 -0
- mirascope/ops/_internal/instrumentation/llm/gen_ai_types/__init__.py +38 -0
- mirascope/ops/_internal/instrumentation/llm/gen_ai_types/gen_ai_input_messages.py +31 -0
- mirascope/ops/_internal/instrumentation/llm/gen_ai_types/gen_ai_output_messages.py +38 -0
- mirascope/ops/_internal/instrumentation/llm/gen_ai_types/gen_ai_system_instructions.py +18 -0
- mirascope/ops/_internal/instrumentation/llm/gen_ai_types/shared.py +100 -0
- mirascope/ops/_internal/instrumentation/llm/llm.py +1288 -0
- mirascope/ops/_internal/propagation.py +198 -0
- mirascope/ops/_internal/protocols.py +51 -0
- mirascope/ops/_internal/session.py +139 -0
- mirascope/ops/_internal/spans.py +232 -0
- mirascope/ops/_internal/traced_calls.py +371 -0
- mirascope/ops/_internal/traced_functions.py +394 -0
- mirascope/ops/_internal/tracing.py +276 -0
- mirascope/ops/_internal/types.py +13 -0
- mirascope/ops/_internal/utils.py +75 -0
- mirascope/ops/_internal/versioned_calls.py +512 -0
- mirascope/ops/_internal/versioned_functions.py +346 -0
- mirascope/ops/_internal/versioning.py +303 -0
- mirascope/ops/exceptions.py +21 -0
- {mirascope-2.0.0a2.dist-info → mirascope-2.0.0a3.dist-info}/METADATA +76 -1
- mirascope-2.0.0a3.dist-info/RECORD +206 -0
- {mirascope-2.0.0a2.dist-info → mirascope-2.0.0a3.dist-info}/WHEEL +1 -1
- mirascope/graphs/__init__.py +0 -22
- mirascope/graphs/finite_state_machine.py +0 -625
- mirascope/llm/agents/__init__.py +0 -15
- mirascope/llm/agents/agent.py +0 -97
- mirascope/llm/agents/agent_template.py +0 -45
- mirascope/llm/agents/decorator.py +0 -176
- mirascope/llm/calls/base_call.py +0 -33
- mirascope/llm/clients/__init__.py +0 -34
- mirascope/llm/clients/anthropic/__init__.py +0 -25
- mirascope/llm/clients/anthropic/model_ids.py +0 -8
- mirascope/llm/clients/google/__init__.py +0 -20
- mirascope/llm/clients/google/clients.py +0 -853
- mirascope/llm/clients/google/model_ids.py +0 -15
- mirascope/llm/clients/openai/__init__.py +0 -25
- mirascope/llm/clients/openai/completions/__init__.py +0 -28
- mirascope/llm/clients/openai/completions/_utils/model_features.py +0 -81
- mirascope/llm/clients/openai/completions/clients.py +0 -833
- mirascope/llm/clients/openai/completions/model_ids.py +0 -8
- mirascope/llm/clients/openai/responses/__init__.py +0 -26
- mirascope/llm/clients/openai/responses/_utils/model_features.py +0 -87
- mirascope/llm/clients/openai/responses/clients.py +0 -832
- mirascope/llm/clients/openai/responses/model_ids.py +0 -8
- mirascope/llm/clients/providers.py +0 -175
- mirascope-2.0.0a2.dist-info/RECORD +0 -102
- /mirascope/llm/{clients → providers}/anthropic/_utils/__init__.py +0 -0
- /mirascope/llm/{clients → providers}/base/kwargs.py +0 -0
- /mirascope/llm/{clients → providers}/base/params.py +0 -0
- /mirascope/llm/{clients → providers}/google/_utils/__init__.py +0 -0
- /mirascope/llm/{clients → providers}/google/message.py +0 -0
- /mirascope/llm/{clients/openai/completions → providers/openai/responses}/_utils/__init__.py +0 -0
- /mirascope/llm/{clients → providers}/openai/shared/__init__.py +0 -0
- {mirascope-2.0.0a2.dist-info → mirascope-2.0.0a3.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"""Context propagation utilities for distributed tracing."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
from collections.abc import Mapping, MutableMapping
|
|
8
|
+
from typing import Literal, TypeAlias
|
|
9
|
+
|
|
10
|
+
from opentelemetry import propagate
|
|
11
|
+
from opentelemetry.context import Context
|
|
12
|
+
from opentelemetry.propagators.b3 import B3MultiFormat, B3SingleFormat
|
|
13
|
+
from opentelemetry.propagators.composite import CompositePropagator
|
|
14
|
+
from opentelemetry.propagators.jaeger import JaegerPropagator
|
|
15
|
+
from opentelemetry.propagators.textmap import Setter, TextMapPropagator
|
|
16
|
+
from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator
|
|
17
|
+
|
|
18
|
+
from ..exceptions import ConfigurationError
|
|
19
|
+
from .session import SESSION_HEADER_NAME, current_session
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
PropagatorFormat: TypeAlias = Literal[
|
|
24
|
+
"tracecontext", "b3", "b3multi", "jaeger", "composite"
|
|
25
|
+
]
|
|
26
|
+
CarrierValue: TypeAlias = str | list[str]
|
|
27
|
+
|
|
28
|
+
# Environment variable names
|
|
29
|
+
ENV_PROPAGATOR_FORMAT = "MIRASCOPE_PROPAGATOR"
|
|
30
|
+
ENV_PROPAGATOR_SET_GLOBAL = "_MIRASCOPE_PROPAGATOR_SET_GLOBAL"
|
|
31
|
+
|
|
32
|
+
_PROPAGATOR: ContextPropagator | None = None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class _StrSetter(Setter[MutableMapping[str, str]]):
|
|
36
|
+
"""Setter that writes string header values into the carrier."""
|
|
37
|
+
|
|
38
|
+
def set(self, carrier: MutableMapping[str, str], key: str, value: str) -> None:
|
|
39
|
+
"""Set a single header value on the carrier."""
|
|
40
|
+
carrier[key] = value
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
_STR_SETTER = _StrSetter()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class ContextPropagator:
|
|
47
|
+
"""Manages OpenTelemetry context propagation across service boundaries."""
|
|
48
|
+
|
|
49
|
+
_propagator: TextMapPropagator
|
|
50
|
+
|
|
51
|
+
def __init__(self, *, set_global: bool = True) -> None:
|
|
52
|
+
"""Initialize propagator and optionally set as global.
|
|
53
|
+
|
|
54
|
+
Propagator format is determined by MIRASCOPE_PROPAGATOR environment variable.
|
|
55
|
+
Defaults to "tracecontext" if not set.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
set_global: Whether to set this propagator as the global textmap propagator.
|
|
59
|
+
|
|
60
|
+
Raises:
|
|
61
|
+
ConfigurationError: If the propagator format is invalid.
|
|
62
|
+
"""
|
|
63
|
+
env_format = os.environ.get(ENV_PROPAGATOR_FORMAT, "tracecontext")
|
|
64
|
+
propagator_name: PropagatorFormat
|
|
65
|
+
if env_format in ("tracecontext", "b3", "b3multi", "jaeger", "composite"):
|
|
66
|
+
propagator_name = env_format
|
|
67
|
+
else:
|
|
68
|
+
error_message = (
|
|
69
|
+
f"Invalid propagator format: {env_format}. "
|
|
70
|
+
f"Valid options: tracecontext, b3, b3multi, jaeger, composite"
|
|
71
|
+
)
|
|
72
|
+
logger.error(error_message)
|
|
73
|
+
raise ConfigurationError(error_message)
|
|
74
|
+
logger.debug(f"Initializing ContextPropagator with format: {propagator_name}")
|
|
75
|
+
|
|
76
|
+
match propagator_name:
|
|
77
|
+
case "tracecontext":
|
|
78
|
+
self._propagator = TraceContextTextMapPropagator()
|
|
79
|
+
case "b3":
|
|
80
|
+
self._propagator = B3SingleFormat()
|
|
81
|
+
case "b3multi":
|
|
82
|
+
self._propagator = B3MultiFormat()
|
|
83
|
+
case "jaeger":
|
|
84
|
+
self._propagator = JaegerPropagator()
|
|
85
|
+
case "composite":
|
|
86
|
+
propagators: list[TextMapPropagator] = [
|
|
87
|
+
TraceContextTextMapPropagator(),
|
|
88
|
+
B3SingleFormat(),
|
|
89
|
+
B3MultiFormat(),
|
|
90
|
+
JaegerPropagator(),
|
|
91
|
+
]
|
|
92
|
+
self._propagator = CompositePropagator(propagators)
|
|
93
|
+
|
|
94
|
+
if set_global:
|
|
95
|
+
should_set_global = os.environ.get(ENV_PROPAGATOR_SET_GLOBAL, "true")
|
|
96
|
+
if should_set_global.lower() == "true":
|
|
97
|
+
propagate.set_global_textmap(self._propagator)
|
|
98
|
+
logger.debug(f"Set {propagator_name} as global textmap propagator")
|
|
99
|
+
|
|
100
|
+
def extract_context(self, carrier: Mapping[str, CarrierValue]) -> Context:
|
|
101
|
+
"""Extract OTEL context from carrier headers.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
carrier: Dictionary containing HTTP headers or similar carrier data.
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
Extracted OpenTelemetry context. Returns empty context if extraction fails.
|
|
108
|
+
"""
|
|
109
|
+
try:
|
|
110
|
+
context = self._propagator.extract(carrier=carrier)
|
|
111
|
+
logger.debug("Successfully extracted context from carrier")
|
|
112
|
+
return context
|
|
113
|
+
except Exception as exception:
|
|
114
|
+
logger.debug(
|
|
115
|
+
f"Failed to extract context from carrier: {type(exception).__name__}: {exception}"
|
|
116
|
+
)
|
|
117
|
+
return Context()
|
|
118
|
+
|
|
119
|
+
def inject_context(
|
|
120
|
+
self,
|
|
121
|
+
carrier: MutableMapping[str, str],
|
|
122
|
+
context: Context | None = None,
|
|
123
|
+
) -> None:
|
|
124
|
+
"""Inject current OTEL context into carrier headers.
|
|
125
|
+
|
|
126
|
+
This method also injects session context if one is active, adding the
|
|
127
|
+
SESSION_HEADER_NAME header to the carrier.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
carrier: Mutable mapping (e.g., HTTP headers dict) to inject context into.
|
|
131
|
+
context: Optional specific context to inject. If None, uses current context.
|
|
132
|
+
"""
|
|
133
|
+
try:
|
|
134
|
+
self._propagator.inject(
|
|
135
|
+
carrier=carrier, context=context, setter=_STR_SETTER
|
|
136
|
+
)
|
|
137
|
+
logger.debug("Successfully injected context into carrier")
|
|
138
|
+
except Exception as exception:
|
|
139
|
+
logger.debug(
|
|
140
|
+
f"Failed to inject context into carrier: {type(exception).__name__}: {exception}"
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
session_ctx = current_session()
|
|
144
|
+
if session_ctx is not None:
|
|
145
|
+
carrier[SESSION_HEADER_NAME] = session_ctx.id
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def get_propagator() -> ContextPropagator:
|
|
149
|
+
"""Get or create the singleton ContextPropagator instance.
|
|
150
|
+
|
|
151
|
+
Reads propagator format from MIRASCOPE_PROPAGATOR environment variable.
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
The global ContextPropagator instance.
|
|
155
|
+
"""
|
|
156
|
+
global _PROPAGATOR
|
|
157
|
+
if _PROPAGATOR is None:
|
|
158
|
+
_PROPAGATOR = ContextPropagator()
|
|
159
|
+
return _PROPAGATOR
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def reset_propagator() -> None:
|
|
163
|
+
"""Reset the singleton ContextPropagator instance.
|
|
164
|
+
|
|
165
|
+
This is primarily useful for testing to ensure a clean state between tests.
|
|
166
|
+
The next call to get_propagator() will create a new instance.
|
|
167
|
+
"""
|
|
168
|
+
global _PROPAGATOR
|
|
169
|
+
_PROPAGATOR = None
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def extract_context(carrier: Mapping[str, CarrierValue]) -> Context:
|
|
173
|
+
"""Extract OTEL context from carrier headers using the global propagator.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
carrier: Dictionary containing HTTP headers or similar carrier data.
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
Extracted OpenTelemetry context.
|
|
180
|
+
"""
|
|
181
|
+
return get_propagator().extract_context(carrier)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def inject_context(
|
|
185
|
+
carrier: MutableMapping[str, str],
|
|
186
|
+
*,
|
|
187
|
+
context: Context | None = None,
|
|
188
|
+
) -> None:
|
|
189
|
+
"""Inject current OTEL context into carrier headers using the global propagator.
|
|
190
|
+
|
|
191
|
+
This function also injects session context if one is active, adding the
|
|
192
|
+
SESSION_HEADER_NAME header to the carrier.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
carrier: Mutable mapping (e.g., HTTP headers dict) to inject context into.
|
|
196
|
+
context: Optional specific context to inject. If None, uses current context.
|
|
197
|
+
"""
|
|
198
|
+
get_propagator().inject_context(carrier, context)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Call protocol helpers for ops.tracing."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import inspect
|
|
6
|
+
from typing import Protocol
|
|
7
|
+
from typing_extensions import TypeIs
|
|
8
|
+
|
|
9
|
+
from ...llm.context import Context, DepsT
|
|
10
|
+
from .types import P, R
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class SyncFunction(Protocol[P, R]):
|
|
14
|
+
"""Protocol for synchronous callable functions."""
|
|
15
|
+
|
|
16
|
+
def __call__(self, *args: P.args, **kwargs: P.kwargs) -> R:
|
|
17
|
+
"""The function required a synchronous call method."""
|
|
18
|
+
... # pragma: no cover
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class AsyncFunction(Protocol[P, R]):
|
|
22
|
+
"""Protocol for asynchronous callable functions."""
|
|
23
|
+
|
|
24
|
+
async def __call__(self, *args: P.args, **kwargs: P.kwargs) -> R:
|
|
25
|
+
"""The function's required asynchronous call method."""
|
|
26
|
+
... # pragma: no cover
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class SyncContextFunction(Protocol[P, DepsT, R]):
|
|
30
|
+
"""Protocol for synchronous callable functions with Context parameter."""
|
|
31
|
+
|
|
32
|
+
def __call__(self, ctx: Context[DepsT], *args: P.args, **kwargs: P.kwargs) -> R:
|
|
33
|
+
"""The function required a synchronous call method with context."""
|
|
34
|
+
... # pragma: no cover
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class AsyncContextFunction(Protocol[P, DepsT, R]):
|
|
38
|
+
"""Protocol for asynchronous callable functions with Context parameter."""
|
|
39
|
+
|
|
40
|
+
async def __call__(
|
|
41
|
+
self, ctx: Context[DepsT], *args: P.args, **kwargs: P.kwargs
|
|
42
|
+
) -> R:
|
|
43
|
+
"""The function's required asynchronous call method with context."""
|
|
44
|
+
... # pragma: no cover
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def fn_is_async(
|
|
48
|
+
fn: SyncFunction[P, R] | AsyncFunction[P, R],
|
|
49
|
+
) -> TypeIs[AsyncFunction[P, R]]:
|
|
50
|
+
"""Type check to determine if a given function is asynchronous."""
|
|
51
|
+
return inspect.iscoroutinefunction(fn) or inspect.isasyncgenfunction(fn)
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""Session context helpers for grouping traces."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import uuid
|
|
6
|
+
from collections.abc import Iterator, Mapping
|
|
7
|
+
from contextlib import contextmanager
|
|
8
|
+
from contextvars import ContextVar, Token
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
|
|
11
|
+
from mirascope.ops._internal.types import Jsonable
|
|
12
|
+
|
|
13
|
+
SESSION_HEADER_NAME = "Mirascope-Session-Id"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
SESSION_CONTEXT: ContextVar[SessionContext | None] = ContextVar(
|
|
17
|
+
"MIRASCOPE_SESSION_CONTEXT", default=None
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(slots=True)
|
|
22
|
+
class SessionContext:
|
|
23
|
+
"""Represents a session context for grouping related spans."""
|
|
24
|
+
|
|
25
|
+
id: str = field(default_factory=lambda: str(uuid.uuid4()))
|
|
26
|
+
"""Unique identifier for the session. Auto-generated if not provided."""
|
|
27
|
+
|
|
28
|
+
attributes: Mapping[str, Jsonable] | None = None
|
|
29
|
+
"""Optional JSON-serializable metadata associated with the session."""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@contextmanager
|
|
33
|
+
def session(
|
|
34
|
+
*, id: str | None = None, attributes: Mapping[str, Jsonable] | None = None
|
|
35
|
+
) -> Iterator[SessionContext]:
|
|
36
|
+
"""Context manager for setting session context.
|
|
37
|
+
|
|
38
|
+
Sessions are used to group related traces together. The session ID and
|
|
39
|
+
optional attributes are automatically propagated to all spans created
|
|
40
|
+
within the session context and are included in outgoing HTTP requests.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
id: Unique identifier for the session. If not provided, a UUID will be
|
|
44
|
+
automatically generated.
|
|
45
|
+
attributes: Optional dictionary of JSON-serializable attributes to
|
|
46
|
+
attach to the session.
|
|
47
|
+
|
|
48
|
+
Example:
|
|
49
|
+
```python
|
|
50
|
+
# With explicit ID
|
|
51
|
+
with mirascope.ops.session(id="user-123") as ctx:
|
|
52
|
+
print(ctx.id) # "user-123"
|
|
53
|
+
result = requests.get("https://api.example.com")
|
|
54
|
+
|
|
55
|
+
# With auto-generated ID
|
|
56
|
+
with mirascope.ops.session() as ctx:
|
|
57
|
+
print(ctx.id) # Auto-generated UUID
|
|
58
|
+
result = requests.get("https://api.example.com")
|
|
59
|
+
|
|
60
|
+
# Nested sessions override parent session
|
|
61
|
+
with mirascope.ops.session(id="1"):
|
|
62
|
+
# Session ID: 1
|
|
63
|
+
with mirascope.ops.session(id="2"):
|
|
64
|
+
# Session ID: 2
|
|
65
|
+
# Session ID: 1
|
|
66
|
+
```
|
|
67
|
+
"""
|
|
68
|
+
if id is not None:
|
|
69
|
+
session_ctx = SessionContext(id=id, attributes=attributes)
|
|
70
|
+
else:
|
|
71
|
+
session_ctx = SessionContext(attributes=attributes)
|
|
72
|
+
token: Token[SessionContext | None] = SESSION_CONTEXT.set(session_ctx)
|
|
73
|
+
try:
|
|
74
|
+
yield session_ctx
|
|
75
|
+
finally:
|
|
76
|
+
SESSION_CONTEXT.reset(token)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def current_session() -> SessionContext | None:
|
|
80
|
+
"""Get the current session context if one is active.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
The active SessionContext or None if no session is active.
|
|
84
|
+
|
|
85
|
+
Example:
|
|
86
|
+
```python
|
|
87
|
+
with mirascope.ops.session(id="user-123"):
|
|
88
|
+
ctx = mirascope.ops.current_session()
|
|
89
|
+
print(ctx.id) # "user-123"
|
|
90
|
+
|
|
91
|
+
ctx = mirascope.ops.current_session()
|
|
92
|
+
print(ctx) # None
|
|
93
|
+
```
|
|
94
|
+
"""
|
|
95
|
+
return SESSION_CONTEXT.get()
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def extract_session_id(headers: Mapping[str, str | list[str]]) -> str | None:
|
|
99
|
+
"""Extract session ID from carrier headers.
|
|
100
|
+
|
|
101
|
+
Performs case-insensitive header name matching and handles both string
|
|
102
|
+
and list[str] header values.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
headers: Dictionary of HTTP headers or similar carrier data.
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
The extracted session ID or None if not present.
|
|
109
|
+
|
|
110
|
+
Example:
|
|
111
|
+
```python
|
|
112
|
+
headers = {"mirascope-session-id": "session-123"}
|
|
113
|
+
session_id = extract_session_id(headers)
|
|
114
|
+
print(session_id) # "session-123"
|
|
115
|
+
```
|
|
116
|
+
"""
|
|
117
|
+
normalized_headers = {key.lower(): value for key, value in headers.items()}
|
|
118
|
+
header_name_lower = SESSION_HEADER_NAME.lower()
|
|
119
|
+
|
|
120
|
+
if header_name_lower not in normalized_headers:
|
|
121
|
+
return None
|
|
122
|
+
|
|
123
|
+
value = normalized_headers[header_name_lower]
|
|
124
|
+
|
|
125
|
+
if isinstance(value, list):
|
|
126
|
+
if len(value) > 0:
|
|
127
|
+
return value[0]
|
|
128
|
+
return None
|
|
129
|
+
|
|
130
|
+
return value if value else None
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
__all__ = [
|
|
134
|
+
"SESSION_HEADER_NAME",
|
|
135
|
+
"SessionContext",
|
|
136
|
+
"current_session",
|
|
137
|
+
"extract_session_id",
|
|
138
|
+
"session",
|
|
139
|
+
]
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
"""Explicit span management utilities for `mirascope.ops`."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from contextvars import Token
|
|
7
|
+
from typing import TYPE_CHECKING, Any
|
|
8
|
+
|
|
9
|
+
from opentelemetry import context as otel_context, trace as otel_trace
|
|
10
|
+
from opentelemetry.trace import (
|
|
11
|
+
SpanContext,
|
|
12
|
+
Status,
|
|
13
|
+
StatusCode,
|
|
14
|
+
format_span_id,
|
|
15
|
+
format_trace_id,
|
|
16
|
+
)
|
|
17
|
+
from opentelemetry.util.types import AttributeValue
|
|
18
|
+
|
|
19
|
+
from .session import current_session
|
|
20
|
+
from .utils import json_dumps
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from opentelemetry.context import Context
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger("mirascope.ops")
|
|
26
|
+
_warned_noop = False
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class Span:
|
|
30
|
+
"""Context-managed span for explicit tracing.
|
|
31
|
+
|
|
32
|
+
Creates a child span within the current trace context. Acts as a no-op
|
|
33
|
+
if tracing is not configured.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(self, name: str, **attributes: AttributeValue) -> None:
|
|
37
|
+
"""Initialize a new span with the given name.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
name: Name for the span.
|
|
41
|
+
**attributes: Initial attributes to set on the span.
|
|
42
|
+
"""
|
|
43
|
+
self._name = name
|
|
44
|
+
self._initial_attributes = attributes
|
|
45
|
+
self._span: otel_trace.Span | None = None
|
|
46
|
+
self._token: Token[Context] | None = None
|
|
47
|
+
self._is_noop = True
|
|
48
|
+
self._finished = False
|
|
49
|
+
|
|
50
|
+
def __enter__(self) -> Span:
|
|
51
|
+
"""Enter the span context.
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
This span instance for use within the context.
|
|
55
|
+
"""
|
|
56
|
+
tracer = otel_trace.get_tracer("mirascope.ops")
|
|
57
|
+
self._span = tracer.start_span(self._name)
|
|
58
|
+
|
|
59
|
+
if self._span.__class__.__name__ == "NonRecordingSpan":
|
|
60
|
+
self._is_noop = True
|
|
61
|
+
self._span = None
|
|
62
|
+
global _warned_noop
|
|
63
|
+
if not _warned_noop:
|
|
64
|
+
logger.warning(
|
|
65
|
+
f"mirascope tracing is not configured; Span('{self._name}') is a no-op."
|
|
66
|
+
)
|
|
67
|
+
_warned_noop = True
|
|
68
|
+
else:
|
|
69
|
+
self._is_noop = False
|
|
70
|
+
self._span.set_attribute("mirascope.type", "trace")
|
|
71
|
+
|
|
72
|
+
session_ctx = current_session()
|
|
73
|
+
if session_ctx is not None:
|
|
74
|
+
self._span.set_attribute("mirascope.ops.session.id", session_ctx.id)
|
|
75
|
+
if session_ctx.attributes is not None:
|
|
76
|
+
self._span.set_attribute(
|
|
77
|
+
"mirascope.ops.session.attributes",
|
|
78
|
+
json_dumps(session_ctx.attributes),
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
self._token = otel_context.attach(
|
|
82
|
+
otel_trace.set_span_in_context(self._span)
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
if self._initial_attributes:
|
|
86
|
+
self.set(**self._initial_attributes)
|
|
87
|
+
|
|
88
|
+
return self
|
|
89
|
+
|
|
90
|
+
def __exit__(
|
|
91
|
+
self,
|
|
92
|
+
exc_type: type[BaseException] | None,
|
|
93
|
+
exc: BaseException | None,
|
|
94
|
+
tb: Any, # noqa: ANN401
|
|
95
|
+
) -> None:
|
|
96
|
+
"""Exit the span context.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
exc_type: Exception type if an exception was raised.
|
|
100
|
+
exc: Exception instance if an exception was raised.
|
|
101
|
+
tb: Traceback if an exception was raised.
|
|
102
|
+
"""
|
|
103
|
+
if self._span and not self._finished:
|
|
104
|
+
if exc is not None:
|
|
105
|
+
self._span.record_exception(exc)
|
|
106
|
+
self._span.set_status(Status(StatusCode.ERROR))
|
|
107
|
+
self.finish()
|
|
108
|
+
|
|
109
|
+
def set(self, **attributes: AttributeValue) -> None:
|
|
110
|
+
"""Set attributes on the current span.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
**attributes: Key-value pairs to set as span attributes.
|
|
114
|
+
"""
|
|
115
|
+
if self._span and not self._finished:
|
|
116
|
+
for key, value in attributes.items():
|
|
117
|
+
self._span.set_attribute(key, value)
|
|
118
|
+
|
|
119
|
+
def event(self, name: str, **attributes: AttributeValue) -> None:
|
|
120
|
+
"""Record an event within the span.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
name: Name of the event.
|
|
124
|
+
**attributes: Event attributes as key-value pairs.
|
|
125
|
+
"""
|
|
126
|
+
if self._span and not self._finished:
|
|
127
|
+
self._span.add_event(name, attributes=attributes)
|
|
128
|
+
|
|
129
|
+
def debug(self, message: str, **additional_attributes: AttributeValue) -> None:
|
|
130
|
+
"""Log a debug message within the span.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
message: Debug message text.
|
|
134
|
+
**additional_attributes: Additional structured attributes for the log entry.
|
|
135
|
+
"""
|
|
136
|
+
self.event("debug", level="debug", message=message, **additional_attributes)
|
|
137
|
+
|
|
138
|
+
def info(self, message: str, **additional_attributes: AttributeValue) -> None:
|
|
139
|
+
"""Log an info message within the span.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
message: Info message text.
|
|
143
|
+
**additional_attributes: Additional structured attributes for the log entry.
|
|
144
|
+
"""
|
|
145
|
+
self.event("info", level="info", message=message, **additional_attributes)
|
|
146
|
+
|
|
147
|
+
def warning(self, message: str, **additional_attributes: AttributeValue) -> None:
|
|
148
|
+
"""Log a warning message within the span.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
message: Warning message text.
|
|
152
|
+
**additional_attributes: Additional structured attributes for the log entry.
|
|
153
|
+
"""
|
|
154
|
+
self.event("warning", level="warning", message=message, **additional_attributes)
|
|
155
|
+
|
|
156
|
+
def error(self, message: str, **additional_attributes: AttributeValue) -> None:
|
|
157
|
+
"""Log an error message within the span.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
message: Error message text.
|
|
161
|
+
**additional_attributes: Additional structured attributes for the log entry.
|
|
162
|
+
"""
|
|
163
|
+
self.event("error", level="error", message=message, **additional_attributes)
|
|
164
|
+
if self._span and not self._finished:
|
|
165
|
+
self._span.set_status(Status(StatusCode.ERROR))
|
|
166
|
+
|
|
167
|
+
def critical(self, message: str, **additional_attributes: AttributeValue) -> None:
|
|
168
|
+
"""Log a critical message within the span.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
message: Critical message text.
|
|
172
|
+
**additional_attributes: Additional structured attributes for the log entry.
|
|
173
|
+
"""
|
|
174
|
+
self.event("critical", level="error", message=message, **additional_attributes)
|
|
175
|
+
if self._span and not self._finished:
|
|
176
|
+
self._span.set_status(Status(StatusCode.ERROR))
|
|
177
|
+
|
|
178
|
+
def finish(self) -> None:
|
|
179
|
+
"""Explicitly finish the span."""
|
|
180
|
+
if not self._finished:
|
|
181
|
+
self._finished = True
|
|
182
|
+
if self._span:
|
|
183
|
+
self._span.end()
|
|
184
|
+
if self._token:
|
|
185
|
+
otel_context.detach(self._token)
|
|
186
|
+
self._token = None
|
|
187
|
+
|
|
188
|
+
@property
|
|
189
|
+
def span_id(self) -> str | None:
|
|
190
|
+
"""Get the span ID if available.
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
The span ID or None if not available.
|
|
194
|
+
"""
|
|
195
|
+
if span_context := self.span_context:
|
|
196
|
+
return format_span_id(span_context.span_id)
|
|
197
|
+
return None
|
|
198
|
+
|
|
199
|
+
@property
|
|
200
|
+
def trace_id(self) -> str | None:
|
|
201
|
+
if span_context := self.span_context:
|
|
202
|
+
return format_trace_id(span_context.trace_id)
|
|
203
|
+
return None
|
|
204
|
+
|
|
205
|
+
@property
|
|
206
|
+
def is_noop(self) -> bool:
|
|
207
|
+
"""Check if this is a no-op span.
|
|
208
|
+
|
|
209
|
+
Returns:
|
|
210
|
+
True if tracing is disabled, False otherwise.
|
|
211
|
+
"""
|
|
212
|
+
return self._is_noop
|
|
213
|
+
|
|
214
|
+
@property
|
|
215
|
+
def span_context(self) -> SpanContext | None:
|
|
216
|
+
"""Return the span context if available."""
|
|
217
|
+
if otel_span := self._span:
|
|
218
|
+
return otel_span.get_span_context()
|
|
219
|
+
return None
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def span(name: str, **attributes: AttributeValue) -> Span:
|
|
223
|
+
"""Create a new span context manager.
|
|
224
|
+
|
|
225
|
+
Args:
|
|
226
|
+
name: Name for the new span.
|
|
227
|
+
**attributes: Initial attributes to set on the span.
|
|
228
|
+
|
|
229
|
+
Returns:
|
|
230
|
+
A Span context manager that creates a child span when entered.
|
|
231
|
+
"""
|
|
232
|
+
return Span(name, **attributes)
|