mirascope 2.0.0a1__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/decorator.py +10 -10
- mirascope/llm/tools/protocols.py +4 -4
- mirascope/llm/tools/tool_schema.py +44 -9
- mirascope/llm/tools/tools.py +12 -11
- 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.0a1.dist-info → mirascope-2.0.0a3.dist-info}/METADATA +77 -1
- mirascope-2.0.0a3.dist-info/RECORD +206 -0
- {mirascope-2.0.0a1.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.0a1.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.0a1.dist-info → mirascope-2.0.0a3.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""Configuration utilities for Mirascope ops module initialization and setup."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterator
|
|
6
|
+
from contextlib import contextmanager
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from opentelemetry.sdk.trace import TracerProvider
|
|
11
|
+
from opentelemetry.trace import Tracer
|
|
12
|
+
else:
|
|
13
|
+
Tracer = None
|
|
14
|
+
|
|
15
|
+
DEFAULT_TRACER_NAME = "mirascope.llm"
|
|
16
|
+
|
|
17
|
+
try:
|
|
18
|
+
from opentelemetry import trace as otel_trace
|
|
19
|
+
except ImportError: # pragma: no cover
|
|
20
|
+
otel_trace = None
|
|
21
|
+
|
|
22
|
+
_tracer_provider: TracerProvider | None = None
|
|
23
|
+
_tracer_name: str = DEFAULT_TRACER_NAME
|
|
24
|
+
_tracer_version: str | None = None
|
|
25
|
+
_tracer: Tracer | None = None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def configure(
|
|
29
|
+
*,
|
|
30
|
+
tracer_provider: TracerProvider | None = None,
|
|
31
|
+
tracer_name: str = DEFAULT_TRACER_NAME,
|
|
32
|
+
tracer_version: str | None = None,
|
|
33
|
+
) -> None:
|
|
34
|
+
"""Configure the ops module defaults for tracing.
|
|
35
|
+
|
|
36
|
+
Sets up default tracer settings for the ops module. If a tracer_provider
|
|
37
|
+
is supplied, it will be installed as the global OpenTelemetry tracer provider.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
tracer_provider: Optional OpenTelemetry TracerProvider to use as default
|
|
41
|
+
and to install globally.
|
|
42
|
+
tracer_name: Tracer name to use when creating a tracer.
|
|
43
|
+
Defaults to "mirascope.llm".
|
|
44
|
+
tracer_version: Optional tracer version.
|
|
45
|
+
|
|
46
|
+
Example:
|
|
47
|
+
|
|
48
|
+
Configure custom tracer settings:
|
|
49
|
+
```python
|
|
50
|
+
from mirascope import ops
|
|
51
|
+
from opentelemetry.sdk.trace import TracerProvider
|
|
52
|
+
|
|
53
|
+
provider = TracerProvider()
|
|
54
|
+
ops.configure(tracer_provider=provider)
|
|
55
|
+
ops.instrument_llm()
|
|
56
|
+
```
|
|
57
|
+
"""
|
|
58
|
+
# TODO: refactor alongside other import error handling improvements
|
|
59
|
+
if otel_trace is None: # pragma: no cover
|
|
60
|
+
raise ImportError(
|
|
61
|
+
"OpenTelemetry is not installed. Run `pip install mirascope[otel]` "
|
|
62
|
+
"before calling `ops.configure(tracer_provider=...)`."
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
global _tracer_provider, _tracer_name, _tracer_version, _tracer
|
|
66
|
+
|
|
67
|
+
if tracer_provider is not None:
|
|
68
|
+
_tracer_provider = tracer_provider
|
|
69
|
+
otel_trace.set_tracer_provider(tracer_provider)
|
|
70
|
+
|
|
71
|
+
_tracer_name = tracer_name
|
|
72
|
+
_tracer_version = tracer_version
|
|
73
|
+
|
|
74
|
+
if otel_trace is not None:
|
|
75
|
+
provider = (
|
|
76
|
+
otel_trace.get_tracer_provider()
|
|
77
|
+
if _tracer_provider is None
|
|
78
|
+
else _tracer_provider
|
|
79
|
+
)
|
|
80
|
+
_tracer = provider.get_tracer(_tracer_name, _tracer_version)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def set_tracer(tracer: Tracer | None) -> None:
|
|
84
|
+
"""Set the configured tracer instance."""
|
|
85
|
+
global _tracer
|
|
86
|
+
_tracer = tracer
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def get_tracer() -> Tracer | None:
|
|
90
|
+
"""Return the configured tracer instance."""
|
|
91
|
+
return _tracer
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@contextmanager
|
|
95
|
+
def tracer_context(tracer: Tracer | None) -> Iterator[Tracer | None]:
|
|
96
|
+
"""Context manager for temporarily setting a tracer.
|
|
97
|
+
|
|
98
|
+
Temporarily sets the tracer for the duration of the context and restores
|
|
99
|
+
the previous tracer when the context exits.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
tracer: The tracer to use within the context.
|
|
103
|
+
|
|
104
|
+
Yields:
|
|
105
|
+
The tracer that was set.
|
|
106
|
+
|
|
107
|
+
Example:
|
|
108
|
+
```python
|
|
109
|
+
from mirascope import ops
|
|
110
|
+
from opentelemetry.sdk.trace import TracerProvider
|
|
111
|
+
|
|
112
|
+
provider = TracerProvider()
|
|
113
|
+
tracer = provider.get_tracer("my-tracer")
|
|
114
|
+
|
|
115
|
+
with ops.tracer_context(tracer):
|
|
116
|
+
# Use the tracer within this context
|
|
117
|
+
...
|
|
118
|
+
# Previous tracer is restored here
|
|
119
|
+
```
|
|
120
|
+
"""
|
|
121
|
+
previous_tracer = get_tracer()
|
|
122
|
+
set_tracer(tracer)
|
|
123
|
+
try:
|
|
124
|
+
yield tracer
|
|
125
|
+
finally:
|
|
126
|
+
set_tracer(previous_tracer)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Context management utilities for distributed tracing."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterator, Mapping
|
|
6
|
+
from contextlib import ExitStack, contextmanager
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from opentelemetry import context as otel_context
|
|
10
|
+
from opentelemetry.context import Context
|
|
11
|
+
|
|
12
|
+
from .propagation import extract_context
|
|
13
|
+
from .session import extract_session_id, session
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@contextmanager
|
|
17
|
+
def propagated_context(
|
|
18
|
+
*,
|
|
19
|
+
parent: Context | None = None,
|
|
20
|
+
extract_from: Mapping[str, Any] | None = None,
|
|
21
|
+
) -> Iterator[None]:
|
|
22
|
+
"""Attach a parent context or extract context from carrier headers.
|
|
23
|
+
|
|
24
|
+
This context manager is used to establish trace context continuity,
|
|
25
|
+
typically on the server side when receiving requests. It either extracts
|
|
26
|
+
context from incoming headers or attaches a pre-existing context.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
parent: Pre-existing OTEL context to attach. Mutually exclusive with extract_from.
|
|
30
|
+
extract_from: Dictionary of headers to extract context from (e.g., request.headers).
|
|
31
|
+
Mutually exclusive with parent.
|
|
32
|
+
|
|
33
|
+
Raises:
|
|
34
|
+
ValueError: If both parent and extract_from are provided, or if neither is provided.
|
|
35
|
+
|
|
36
|
+
Example:
|
|
37
|
+
Server-side context extraction from FastAPI request:
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
@app.post("/endpoint")
|
|
41
|
+
async def endpoint(request: Request):
|
|
42
|
+
with propagated_context(extract_from=dict(request.headers)):
|
|
43
|
+
result = process_request()
|
|
44
|
+
return result
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Using a pre-existing context:
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
with propagated_context(parent=existing_context):
|
|
51
|
+
do_work()
|
|
52
|
+
```
|
|
53
|
+
"""
|
|
54
|
+
if parent is not None and extract_from is not None:
|
|
55
|
+
raise ValueError("Cannot specify both 'parent' and 'extract_from' parameters")
|
|
56
|
+
|
|
57
|
+
if parent is None and extract_from is None:
|
|
58
|
+
raise ValueError("Must specify either 'parent' or 'extract_from' parameter")
|
|
59
|
+
|
|
60
|
+
if extract_from is not None:
|
|
61
|
+
with ExitStack() as stack:
|
|
62
|
+
session_id = extract_session_id(extract_from)
|
|
63
|
+
if session_id:
|
|
64
|
+
stack.enter_context(session(id=session_id))
|
|
65
|
+
|
|
66
|
+
extracted_context = extract_context(extract_from)
|
|
67
|
+
token = otel_context.attach(extracted_context)
|
|
68
|
+
stack.callback(otel_context.detach, token)
|
|
69
|
+
|
|
70
|
+
yield
|
|
71
|
+
elif parent is not None:
|
|
72
|
+
token = otel_context.attach(parent)
|
|
73
|
+
try:
|
|
74
|
+
yield
|
|
75
|
+
finally:
|
|
76
|
+
otel_context.detach(token)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Mirascope OpenTelemetry exporters for two-phase telemetry.
|
|
2
|
+
|
|
3
|
+
This package provides a two-phase export system for OpenTelemetry tracing:
|
|
4
|
+
1. Immediate start event transmission for real-time visibility
|
|
5
|
+
2. Batched end event transmission for efficiency
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .exporters import MirascopeOTLPExporter
|
|
9
|
+
from .processors import MirascopeSpanProcessor
|
|
10
|
+
from .types import (
|
|
11
|
+
Link,
|
|
12
|
+
SpanContextDict,
|
|
13
|
+
SpanEvent,
|
|
14
|
+
SpanEventType,
|
|
15
|
+
Status,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"Link",
|
|
20
|
+
"MirascopeOTLPExporter",
|
|
21
|
+
"MirascopeSpanProcessor",
|
|
22
|
+
"SpanContextDict",
|
|
23
|
+
"SpanEvent",
|
|
24
|
+
"SpanEventType",
|
|
25
|
+
"Status",
|
|
26
|
+
]
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
"""Exporter implementation for OpenTelemetry exporters.
|
|
2
|
+
|
|
3
|
+
This module provides the export layer for sending OpenTelemetry span
|
|
4
|
+
events to the Mirascope ingestion endpoint. It wraps the Fern-generated
|
|
5
|
+
Mirascope client to provide the interface needed by OpenTelemetry exporters.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
import time
|
|
12
|
+
from collections.abc import Sequence
|
|
13
|
+
|
|
14
|
+
from opentelemetry.sdk.trace import ReadableSpan
|
|
15
|
+
from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult
|
|
16
|
+
from opentelemetry.util.types import AttributeValue
|
|
17
|
+
|
|
18
|
+
from ....api._generated.traces.types import (
|
|
19
|
+
TracesCreateRequestResourceSpansItem,
|
|
20
|
+
TracesCreateRequestResourceSpansItemResource,
|
|
21
|
+
TracesCreateRequestResourceSpansItemResourceAttributesItem,
|
|
22
|
+
TracesCreateRequestResourceSpansItemResourceAttributesItemValue,
|
|
23
|
+
TracesCreateRequestResourceSpansItemScopeSpansItem,
|
|
24
|
+
TracesCreateRequestResourceSpansItemScopeSpansItemScope,
|
|
25
|
+
TracesCreateRequestResourceSpansItemScopeSpansItemSpansItem,
|
|
26
|
+
TracesCreateRequestResourceSpansItemScopeSpansItemSpansItemAttributesItem,
|
|
27
|
+
TracesCreateRequestResourceSpansItemScopeSpansItemSpansItemAttributesItemValue,
|
|
28
|
+
TracesCreateRequestResourceSpansItemScopeSpansItemSpansItemStatus,
|
|
29
|
+
)
|
|
30
|
+
from ....api.client import Mirascope
|
|
31
|
+
|
|
32
|
+
logger = logging.getLogger(__name__)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class MirascopeOTLPExporter(SpanExporter):
|
|
36
|
+
"""OTLP/HTTP exporter for completed spans.
|
|
37
|
+
|
|
38
|
+
This exporter implements the OpenTelemetry SpanExporter interface
|
|
39
|
+
for exporting completed spans in OTLP format over HTTP. It's
|
|
40
|
+
designed to work with BatchSpanProcessor for efficient batching.
|
|
41
|
+
|
|
42
|
+
This uses the Fern auto-generated client for sending converted spans.
|
|
43
|
+
|
|
44
|
+
Attributes:
|
|
45
|
+
exporter: Export client for sending events.
|
|
46
|
+
timeout: Request timeout in seconds.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def __init__(
|
|
50
|
+
self,
|
|
51
|
+
client: Mirascope,
|
|
52
|
+
timeout: float = 30.0,
|
|
53
|
+
max_retry_attempts: int = 3,
|
|
54
|
+
) -> None:
|
|
55
|
+
"""Initialize the telemetry exporter.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
client: The Fern-generated Mirascope client instance.
|
|
59
|
+
In the future, this will accept the enhanced client from
|
|
60
|
+
mirascope.api.client that provides error handling and caching
|
|
61
|
+
capabilities.
|
|
62
|
+
timeout: Request timeout in seconds for telemetry operations.
|
|
63
|
+
max_retry_attempts: Maximum number of retry attempts for failed exports.
|
|
64
|
+
"""
|
|
65
|
+
self.client = client
|
|
66
|
+
self.timeout = timeout
|
|
67
|
+
self.max_retry_attempts = max_retry_attempts
|
|
68
|
+
self._shutdown = False
|
|
69
|
+
|
|
70
|
+
def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult:
|
|
71
|
+
"""Export a batch of spans to the telemetry endpoint.
|
|
72
|
+
|
|
73
|
+
This is the standard OpenTelemetry export interface.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
spans: Sequence of ReadableSpan objects to export.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
SpanExportResult indicating success or failure.
|
|
80
|
+
"""
|
|
81
|
+
if self._shutdown:
|
|
82
|
+
return SpanExportResult.FAILURE
|
|
83
|
+
|
|
84
|
+
if not spans:
|
|
85
|
+
return SpanExportResult.SUCCESS
|
|
86
|
+
|
|
87
|
+
exceptions: list[Exception] = []
|
|
88
|
+
delay = 0.1
|
|
89
|
+
|
|
90
|
+
for i in range(self.max_retry_attempts):
|
|
91
|
+
if i > 0:
|
|
92
|
+
time.sleep(delay)
|
|
93
|
+
delay = min(delay * 2, 5.0)
|
|
94
|
+
|
|
95
|
+
try:
|
|
96
|
+
otlp_data = self._convert_spans_to_otlp(spans)
|
|
97
|
+
response = self.client.traces.create(resource_spans=otlp_data)
|
|
98
|
+
|
|
99
|
+
if (
|
|
100
|
+
response
|
|
101
|
+
and hasattr(response, "partial_success")
|
|
102
|
+
and response.partial_success
|
|
103
|
+
):
|
|
104
|
+
partial_success = response.partial_success
|
|
105
|
+
if hasattr(partial_success, "rejected_spans"):
|
|
106
|
+
rejected = partial_success.rejected_spans
|
|
107
|
+
if rejected is not None and rejected > 0:
|
|
108
|
+
return SpanExportResult.FAILURE
|
|
109
|
+
|
|
110
|
+
return SpanExportResult.SUCCESS
|
|
111
|
+
|
|
112
|
+
except Exception as e:
|
|
113
|
+
exceptions.append(e)
|
|
114
|
+
logger.warning(
|
|
115
|
+
f"Export attempt {i + 1} failed, retrying in {delay}s: {e}"
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
logger.error(
|
|
119
|
+
f"Failed to export spans after {self.max_retry_attempts} attempts: {exceptions}"
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
return SpanExportResult.FAILURE
|
|
123
|
+
|
|
124
|
+
def _convert_spans_to_otlp(
|
|
125
|
+
self, spans: Sequence[ReadableSpan]
|
|
126
|
+
) -> list[TracesCreateRequestResourceSpansItem]:
|
|
127
|
+
"""Convert OpenTelemetry spans to OTLP format.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
spans: Sequence of ReadableSpan objects.
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
List of ResourceSpans in OTLP format.
|
|
134
|
+
"""
|
|
135
|
+
resource_spans_map = {}
|
|
136
|
+
|
|
137
|
+
for span in spans:
|
|
138
|
+
try:
|
|
139
|
+
otlp_span = self._convert_span(span)
|
|
140
|
+
except ValueError as e:
|
|
141
|
+
logger.warning(f"Skipping span due to error: {e}")
|
|
142
|
+
continue
|
|
143
|
+
|
|
144
|
+
resource_key = id(span.resource) if span.resource else "default"
|
|
145
|
+
|
|
146
|
+
if resource_key not in resource_spans_map:
|
|
147
|
+
resource = None
|
|
148
|
+
if span.resource:
|
|
149
|
+
resource_attrs = []
|
|
150
|
+
for key, value in span.resource.attributes.items():
|
|
151
|
+
attr_value = self._convert_resource_attribute_value(value)
|
|
152
|
+
resource_attrs.append(
|
|
153
|
+
TracesCreateRequestResourceSpansItemResourceAttributesItem(
|
|
154
|
+
key=key,
|
|
155
|
+
value=attr_value,
|
|
156
|
+
)
|
|
157
|
+
)
|
|
158
|
+
resource = TracesCreateRequestResourceSpansItemResource(
|
|
159
|
+
attributes=resource_attrs
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
resource_spans_map[resource_key] = {
|
|
163
|
+
"resource": resource,
|
|
164
|
+
"scope_spans": {},
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
scope_key = (
|
|
168
|
+
span.instrumentation_scope.name
|
|
169
|
+
if span.instrumentation_scope
|
|
170
|
+
else "unknown"
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
if scope_key not in resource_spans_map[resource_key]["scope_spans"]:
|
|
174
|
+
scope = None
|
|
175
|
+
if span.instrumentation_scope:
|
|
176
|
+
scope = TracesCreateRequestResourceSpansItemScopeSpansItemScope(
|
|
177
|
+
name=span.instrumentation_scope.name,
|
|
178
|
+
version=span.instrumentation_scope.version,
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
resource_spans_map[resource_key]["scope_spans"][scope_key] = {
|
|
182
|
+
"scope": scope,
|
|
183
|
+
"spans": [],
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
resource_spans_map[resource_key]["scope_spans"][scope_key]["spans"].append(
|
|
187
|
+
otlp_span
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
result = []
|
|
191
|
+
for resource_data in resource_spans_map.values():
|
|
192
|
+
scope_spans = []
|
|
193
|
+
for scope_data in resource_data["scope_spans"].values():
|
|
194
|
+
scope_spans.append(
|
|
195
|
+
TracesCreateRequestResourceSpansItemScopeSpansItem(
|
|
196
|
+
scope=scope_data["scope"],
|
|
197
|
+
spans=scope_data["spans"],
|
|
198
|
+
)
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
result.append(
|
|
202
|
+
TracesCreateRequestResourceSpansItem(
|
|
203
|
+
resource=resource_data["resource"],
|
|
204
|
+
scope_spans=scope_spans,
|
|
205
|
+
)
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
return result
|
|
209
|
+
|
|
210
|
+
def _convert_span(
|
|
211
|
+
self, span: ReadableSpan
|
|
212
|
+
) -> TracesCreateRequestResourceSpansItemScopeSpansItemSpansItem:
|
|
213
|
+
"""Convert a single ReadableSpan to OTLP format."""
|
|
214
|
+
context = span.get_span_context()
|
|
215
|
+
if not context or not context.is_valid:
|
|
216
|
+
raise ValueError(f"Cannot export span without valid context: {span.name}")
|
|
217
|
+
|
|
218
|
+
attributes = []
|
|
219
|
+
if span.attributes:
|
|
220
|
+
for key, value in span.attributes.items():
|
|
221
|
+
attr_value = self._convert_attribute_value(value)
|
|
222
|
+
attributes.append(
|
|
223
|
+
TracesCreateRequestResourceSpansItemScopeSpansItemSpansItemAttributesItem(
|
|
224
|
+
key=key,
|
|
225
|
+
value=attr_value,
|
|
226
|
+
)
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
status = None
|
|
230
|
+
if span.status:
|
|
231
|
+
status = TracesCreateRequestResourceSpansItemScopeSpansItemSpansItemStatus(
|
|
232
|
+
code=span.status.status_code.value,
|
|
233
|
+
message=span.status.description or "",
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
trace_id = format(context.trace_id, "032x")
|
|
237
|
+
span_id = format(context.span_id, "016x")
|
|
238
|
+
|
|
239
|
+
return TracesCreateRequestResourceSpansItemScopeSpansItemSpansItem(
|
|
240
|
+
trace_id=trace_id,
|
|
241
|
+
span_id=span_id,
|
|
242
|
+
parent_span_id=(
|
|
243
|
+
format(span.parent.span_id, "016x")
|
|
244
|
+
if span.parent and span.parent.span_id
|
|
245
|
+
else None
|
|
246
|
+
),
|
|
247
|
+
name=span.name,
|
|
248
|
+
kind=span.kind.value if span.kind else 0,
|
|
249
|
+
start_time_unix_nano=str(span.start_time) if span.start_time else "0",
|
|
250
|
+
end_time_unix_nano=str(span.end_time) if span.end_time else "0",
|
|
251
|
+
attributes=attributes or None,
|
|
252
|
+
status=status,
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
def _convert_attribute_value(
|
|
256
|
+
self, value: AttributeValue
|
|
257
|
+
) -> TracesCreateRequestResourceSpansItemScopeSpansItemSpansItemAttributesItemValue:
|
|
258
|
+
"""Convert OpenTelemetry AttributeValue to Mirascope API's KeyValueValue.
|
|
259
|
+
|
|
260
|
+
This conversion is necessary because the Fern-generated API client
|
|
261
|
+
expects KeyValueValue objects, not OpenTelemetry's AttributeValue types.
|
|
262
|
+
|
|
263
|
+
Args:
|
|
264
|
+
value: An OpenTelemetry AttributeValue (bool, int, float, str, or Sequence)
|
|
265
|
+
|
|
266
|
+
Returns:
|
|
267
|
+
A KeyValueValue object for the Mirascope API
|
|
268
|
+
"""
|
|
269
|
+
match value:
|
|
270
|
+
case str():
|
|
271
|
+
return TracesCreateRequestResourceSpansItemScopeSpansItemSpansItemAttributesItemValue(
|
|
272
|
+
string_value=value
|
|
273
|
+
)
|
|
274
|
+
case bool():
|
|
275
|
+
return TracesCreateRequestResourceSpansItemScopeSpansItemSpansItemAttributesItemValue(
|
|
276
|
+
bool_value=value
|
|
277
|
+
)
|
|
278
|
+
case int():
|
|
279
|
+
return TracesCreateRequestResourceSpansItemScopeSpansItemSpansItemAttributesItemValue(
|
|
280
|
+
int_value=str(value)
|
|
281
|
+
)
|
|
282
|
+
case float():
|
|
283
|
+
return TracesCreateRequestResourceSpansItemScopeSpansItemSpansItemAttributesItemValue(
|
|
284
|
+
double_value=value
|
|
285
|
+
)
|
|
286
|
+
case _:
|
|
287
|
+
return TracesCreateRequestResourceSpansItemScopeSpansItemSpansItemAttributesItemValue(
|
|
288
|
+
string_value=str(list(value))
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
def _convert_resource_attribute_value(
|
|
292
|
+
self, value: AttributeValue
|
|
293
|
+
) -> TracesCreateRequestResourceSpansItemResourceAttributesItemValue:
|
|
294
|
+
"""Convert OpenTelemetry AttributeValue to Mirascope API's resource KeyValueValue.
|
|
295
|
+
|
|
296
|
+
This conversion is necessary because the Fern-generated API client
|
|
297
|
+
expects KeyValueValue objects, not OpenTelemetry's AttributeValue types.
|
|
298
|
+
|
|
299
|
+
Args:
|
|
300
|
+
value: An OpenTelemetry AttributeValue (bool, int, float, str, or Sequence)
|
|
301
|
+
|
|
302
|
+
Returns:
|
|
303
|
+
A KeyValueValue object for the Mirascope API resource attributes
|
|
304
|
+
"""
|
|
305
|
+
match value:
|
|
306
|
+
case str():
|
|
307
|
+
return TracesCreateRequestResourceSpansItemResourceAttributesItemValue(
|
|
308
|
+
string_value=value
|
|
309
|
+
)
|
|
310
|
+
case bool():
|
|
311
|
+
return TracesCreateRequestResourceSpansItemResourceAttributesItemValue(
|
|
312
|
+
bool_value=value
|
|
313
|
+
)
|
|
314
|
+
case int():
|
|
315
|
+
return TracesCreateRequestResourceSpansItemResourceAttributesItemValue(
|
|
316
|
+
int_value=str(value)
|
|
317
|
+
)
|
|
318
|
+
case float():
|
|
319
|
+
return TracesCreateRequestResourceSpansItemResourceAttributesItemValue(
|
|
320
|
+
double_value=value
|
|
321
|
+
)
|
|
322
|
+
case _:
|
|
323
|
+
return TracesCreateRequestResourceSpansItemResourceAttributesItemValue(
|
|
324
|
+
string_value=str(list(value))
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
def shutdown(self) -> None:
|
|
328
|
+
"""Shutdown the exporter. Subsequent exports will return FAILURE."""
|
|
329
|
+
self._shutdown = True
|
|
330
|
+
|
|
331
|
+
def force_flush(self, timeout_millis: int = 30000) -> bool:
|
|
332
|
+
"""Force flush any pending data.
|
|
333
|
+
|
|
334
|
+
No-op since this exporter does not buffer data internally.
|
|
335
|
+
|
|
336
|
+
Args:
|
|
337
|
+
timeout_millis: Maximum time to wait in milliseconds (unused).
|
|
338
|
+
|
|
339
|
+
Returns:
|
|
340
|
+
Always True since there is no internal buffer to flush.
|
|
341
|
+
"""
|
|
342
|
+
return True
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Span processors for two-phase export system.
|
|
2
|
+
|
|
3
|
+
This module implements a custom SpanProcessor that sends immediate
|
|
4
|
+
start events and batches end events for efficient export.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
8
|
+
|
|
9
|
+
from opentelemetry.context import Context
|
|
10
|
+
from opentelemetry.sdk.trace import ReadableSpan, SpanProcessor
|
|
11
|
+
from opentelemetry.sdk.trace.export import BatchSpanProcessor
|
|
12
|
+
|
|
13
|
+
from .exporters import MirascopeOTLPExporter
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class MirascopeSpanProcessor(SpanProcessor):
|
|
17
|
+
"""Two-phase span processor for Mirascope telemetry.
|
|
18
|
+
|
|
19
|
+
This processor implements a two-phase export strategy:
|
|
20
|
+
1. Immediate transmission of minimal start events for real-time visibility
|
|
21
|
+
2. Batched transmission of complete events for efficiency
|
|
22
|
+
|
|
23
|
+
The processor uses a thread pool to ensure start events don't block
|
|
24
|
+
the application while maintaining compatibility with OpenTelemetry's
|
|
25
|
+
synchronous SDK.
|
|
26
|
+
|
|
27
|
+
Attributes:
|
|
28
|
+
start_exporter: Exporter for immediate start events.
|
|
29
|
+
batch_processor: Standard batch processor for completed spans.
|
|
30
|
+
executor: Thread pool for non-blocking start event transmission.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
otlp_exporter: MirascopeOTLPExporter,
|
|
36
|
+
batch_processor: BatchSpanProcessor | None = None,
|
|
37
|
+
executor: ThreadPoolExecutor | None = None,
|
|
38
|
+
) -> None:
|
|
39
|
+
"""Initialize the two-phase processor.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
start_exporter: Exporter for immediate start events.
|
|
43
|
+
batch_processor: Optional batch processor for end events.
|
|
44
|
+
executor: Optional thread pool executor (creates default if None).
|
|
45
|
+
"""
|
|
46
|
+
self.otlp_exporter = otlp_exporter
|
|
47
|
+
self.batch_processor = batch_processor
|
|
48
|
+
self.executor = executor or ThreadPoolExecutor(
|
|
49
|
+
max_workers=2, thread_name_prefix="mirascope-span-processor"
|
|
50
|
+
)
|
|
51
|
+
self._shutdown = False
|
|
52
|
+
|
|
53
|
+
def on_start(
|
|
54
|
+
self, span: ReadableSpan, parent_context: Context | None = None
|
|
55
|
+
) -> None:
|
|
56
|
+
"""Handle span start by sending immediate start event.
|
|
57
|
+
|
|
58
|
+
This method extracts minimal span data and sends it immediately
|
|
59
|
+
via the start exporter in a non-blocking manner.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
span: The span that just started.
|
|
63
|
+
parent_context: Optional parent context for the span.
|
|
64
|
+
"""
|
|
65
|
+
if self._shutdown:
|
|
66
|
+
return
|
|
67
|
+
|
|
68
|
+
self.executor.submit(self.otlp_exporter.export, [span])
|
|
69
|
+
|
|
70
|
+
def on_end(self, span: ReadableSpan) -> None:
|
|
71
|
+
"""Handle span end by delegating to batch processor.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
span: The span that just ended.
|
|
75
|
+
"""
|
|
76
|
+
if self.batch_processor and not self._shutdown:
|
|
77
|
+
self.batch_processor.on_end(span)
|
|
78
|
+
|
|
79
|
+
def shutdown(self) -> None:
|
|
80
|
+
"""Gracefully shutdown the processor.
|
|
81
|
+
|
|
82
|
+
This ensures all pending start events are sent and the
|
|
83
|
+
batch processor is properly shutdown.
|
|
84
|
+
"""
|
|
85
|
+
self._shutdown = True
|
|
86
|
+
|
|
87
|
+
if self.batch_processor:
|
|
88
|
+
self.batch_processor.shutdown()
|
|
89
|
+
|
|
90
|
+
self.executor.shutdown(wait=True)
|
|
91
|
+
self.otlp_exporter.shutdown()
|
|
92
|
+
|
|
93
|
+
def force_flush(self, timeout_millis: int = 30000) -> bool:
|
|
94
|
+
"""Force flush all pending data.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
timeout_millis: Maximum time to wait in milliseconds.
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
True if flush completed successfully.
|
|
101
|
+
"""
|
|
102
|
+
if self.batch_processor:
|
|
103
|
+
return self.batch_processor.force_flush(timeout_millis)
|
|
104
|
+
return True
|