scale-gp-beta 0.1.0a17__py3-none-any.whl → 0.1.0a18__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.
scale_gp_beta/_version.py CHANGED
@@ -1,4 +1,4 @@
1
1
  # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
2
2
 
3
3
  __title__ = "scale_gp_beta"
4
- __version__ = "0.1.0-alpha.17" # x-release-please-version
4
+ __version__ = "0.1.0-alpha.18" # x-release-please-version
@@ -0,0 +1,4 @@
1
+ from .tracing import create_span, flush_queue, create_trace, current_span, current_trace
2
+ from .trace_queue_manager import init
3
+
4
+ __all__ = ["init", "create_span", "create_trace", "current_trace", "current_span", "flush_queue"]
@@ -0,0 +1,5 @@
1
+ from scale_gp_beta._exceptions import SGPClientError
2
+
3
+
4
+ class ParamsCreationError(SGPClientError):
5
+ pass
@@ -0,0 +1,104 @@
1
+ import contextvars
2
+ from typing import TYPE_CHECKING, Optional
3
+
4
+ if TYPE_CHECKING:
5
+ from .span import BaseSpan
6
+ from .trace import BaseTrace
7
+
8
+
9
+ _current_span: contextvars.ContextVar["BaseSpan | None"] = contextvars.ContextVar("current_span", default=None)
10
+
11
+ _current_trace: contextvars.ContextVar["BaseTrace | None"] = contextvars.ContextVar("current_trace", default=None)
12
+
13
+
14
+ class Scope:
15
+ """
16
+ Manages the currently active span and trace within a context.
17
+
18
+ This class provides methods to get, set, and reset the current `BaseSpan`
19
+ and `BaseTrace` using `contextvars`. This allows for context-local
20
+ storage of the active span and trace.
21
+
22
+ Both traces and spans are managed in a way that allows for nesting.
23
+ While traces are not typically expected to be nested, this class handles
24
+ such scenarios gracefully by managing them with context variables, similar
25
+ to how spans are managed.
26
+ """
27
+
28
+ @classmethod
29
+ def get_current_span(cls) -> Optional["BaseSpan"]:
30
+ """
31
+ Retrieves the currently active span from the context.
32
+
33
+ Returns:
34
+ Optional["BaseSpan"]: The currently active span, or None if no
35
+ span is active in the current context.
36
+ """
37
+ return _current_span.get()
38
+
39
+ @classmethod
40
+ def set_current_span(cls, span: Optional["BaseSpan"]) -> contextvars.Token[Optional["BaseSpan"]]:
41
+ """
42
+ Sets the currently active span in the context.
43
+
44
+ Args:
45
+ span: The span to set as the current active span. Can be None
46
+ to indicate no active span.
47
+
48
+ Returns:
49
+ contextvars.Token[Optional["BaseSpan"]]: A token that can be used
50
+ to reset the context variable
51
+ to its previous state.
52
+ """
53
+ return _current_span.set(span)
54
+
55
+ @classmethod
56
+ def reset_current_span(cls, token: contextvars.Token[Optional["BaseSpan"]]) -> None:
57
+ """
58
+ Resets the current span in the context to its previous state.
59
+
60
+ Args:
61
+ token: The token returned by a previous call to `set_current_span`.
62
+ This token is used to restore the context variable to the
63
+ value it had before the `set` call that D_GENERATED the token.
64
+ """
65
+ _current_span.reset(token)
66
+
67
+ @classmethod
68
+ def get_current_trace(cls) -> Optional["BaseTrace"]:
69
+ """
70
+ Retrieves the currently active trace from the context.
71
+
72
+ Returns:
73
+ Optional["BaseTrace"]: The currently active trace, or None if no
74
+ trace is active in the current context.
75
+ """
76
+ return _current_trace.get()
77
+
78
+ @classmethod
79
+ def set_current_trace(cls, trace: Optional["BaseTrace"]) -> contextvars.Token[Optional["BaseTrace"]]:
80
+ """
81
+ Sets the currently active trace in the context.
82
+
83
+ Args:
84
+ trace: The trace to set as the current active trace. Can be None
85
+ to indicate no active trace.
86
+
87
+ Returns:
88
+ contextvars.Token[Optional["BaseTrace"]]: A token that can be used
89
+ to reset the context variable
90
+ to its previous state.
91
+ """
92
+ return _current_trace.set(trace)
93
+
94
+ @classmethod
95
+ def reset_current_trace(cls, token: contextvars.Token[Optional["BaseTrace"]]) -> None:
96
+ """
97
+ Resets the current trace in the context to its previous state.
98
+
99
+ Args:
100
+ token: The token returned by a previous call to `set_current_trace`.
101
+ This token is used to restore the context variable to the
102
+ value it had before the `set` call that D_GENERATED the token.
103
+ """
104
+ _current_trace.reset(token)
@@ -0,0 +1,217 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ from typing import TYPE_CHECKING, Any, Type, Optional
5
+ from typing_extensions import override
6
+
7
+ from scale_gp_beta.types.span_upsert_batch_params import Item as SpanCreateRequest
8
+
9
+ from .util import iso_timestamp, generate_span_id
10
+ from .scope import Scope
11
+ from .types import SpanTypeLiterals, SpanStatusLiterals
12
+ from .exceptions import ParamsCreationError
13
+
14
+ if TYPE_CHECKING:
15
+ import contextvars
16
+ from types import TracebackType
17
+
18
+ from .trace_queue_manager import TraceQueueManager
19
+
20
+ log: logging.Logger = logging.getLogger(__name__)
21
+
22
+
23
+ class BaseSpan:
24
+ """Base class for all span types, providing common attributes and context management.
25
+
26
+ A span represents a single unit of work or operation within a trace. This base
27
+ class defines the core interface and properties for spans, such as name, IDs,
28
+ timestamps, and methods for starting and ending the span's lifecycle.
29
+
30
+ It is intended to be subclassed by concrete span implementations like `Span`
31
+ (for active tracing) and `NoOpSpan` (for when tracing is disabled).
32
+
33
+ Attributes:
34
+ name (str): The human-readable name of the span.
35
+ trace_id (str): The ID of the trace this span belongs to.
36
+ span_id (str): The unique ID of this span.
37
+ parent_span_id (Optional[str]): The ID of the parent span, if this is a child span.
38
+ start_time (Optional[str]): ISO 8601 timestamp of when the span started.
39
+ Set by the `start()` method.
40
+ end_time (Optional[str]): ISO 8601 timestamp of when the span ended.
41
+ Set by the `end()` method.
42
+ input (Optional[dict[str, Any]]): Input data or parameters for the span's operation.
43
+ output (Optional[dict[str, Any]]): Output data or results from the span's operation.
44
+ metadata (Optional[dict[str, Any]]): Additional arbitrary key-value metadata for the span.
45
+ _contextvar_token (Optional[contextvars.Token]): Token for managing the span's presence
46
+ in the current execution context.
47
+ """
48
+
49
+ def __init__(
50
+ self,
51
+ name: str,
52
+ trace_id: Optional[str] = None,
53
+ queue_manager: Optional[TraceQueueManager] = None,
54
+ span_id: Optional[str] = None,
55
+ parent_span_id: Optional[str] = None,
56
+ input: Optional[dict[str, Any]] = None,
57
+ output: Optional[dict[str, Any]] = None,
58
+ metadata: Optional[dict[str, Any]] = None,
59
+ span_type: SpanTypeLiterals = "STANDALONE"
60
+ ):
61
+ self.name = name
62
+ self.trace_id = trace_id or "no_trace_id"
63
+ self.span_id: str = span_id or generate_span_id()
64
+ self.parent_span_id = parent_span_id
65
+ self.start_time: Optional[str] = None
66
+ self.end_time: Optional[str] = None
67
+ self.input = input
68
+ self.output = output
69
+ self.metadata = metadata
70
+ self.span_type: SpanTypeLiterals = span_type
71
+ self.status: SpanStatusLiterals = "SUCCESS"
72
+ self._queue_manager = queue_manager
73
+
74
+ self._contextvar_token: Optional[contextvars.Token[Optional[BaseSpan]]] = None
75
+
76
+ def start(self) -> None:
77
+ pass
78
+
79
+ def end(self) -> None:
80
+ pass
81
+
82
+ def __enter__(self) -> BaseSpan:
83
+ self.start()
84
+ return self
85
+
86
+ def __exit__(
87
+ self,
88
+ exc_type: Optional[Type[BaseException]],
89
+ exc_val: Optional[BaseException],
90
+ exc_tb: Optional[TracebackType]
91
+ ) -> None:
92
+ # Naively record details in metadata for now, note that error capture only supported in context manager
93
+ # TODO: support error observations when using direct span.start() and span.end()
94
+ if exc_type is not None:
95
+ if self.metadata is None:
96
+ self.metadata = {}
97
+ self.metadata["error"] = True
98
+ self.metadata["error.type"] = exc_type.__name__
99
+ self.metadata["error.message"] = str(exc_val)
100
+ self.status = "ERROR"
101
+ self.end()
102
+
103
+ def to_request_params(self) -> SpanCreateRequest:
104
+ if self.start_time is None:
105
+ raise ParamsCreationError("No start time specified")
106
+ if self.end_time is None:
107
+ # Can relax this check if we decide to allow span POSTs on creation.
108
+ raise ParamsCreationError("No end time specified")
109
+
110
+ request_data = SpanCreateRequest(
111
+ name=self.name,
112
+ id=self.span_id,
113
+ trace_id=self.trace_id,
114
+ start_timestamp=self.start_time,
115
+ end_timestamp=self.end_time,
116
+ input=self.input or {},
117
+ output=self.output or {},
118
+ metadata=self.metadata or {},
119
+ status=self.status,
120
+ type=self.span_type
121
+ )
122
+
123
+ # parent_span_id is optional (root spans)
124
+ if self.parent_span_id is not None:
125
+ request_data["parent_id"] = self.parent_span_id
126
+
127
+ return request_data
128
+
129
+
130
+ class NoOpSpan(BaseSpan):
131
+ @override
132
+ def start(self) -> None:
133
+ if self.start_time is not None:
134
+ log.warning(f"Span {self.name}: {self.span_id} has already started at {self.start_time}")
135
+ return
136
+
137
+ self.start_time = iso_timestamp()
138
+ self._contextvar_token = Scope.set_current_span(self)
139
+
140
+ @override
141
+ def end(self) -> None:
142
+ if self.end_time is not None:
143
+ log.warning(f"Span {self.name}: {self.span_id} has already ended at {self.end_time}")
144
+ return
145
+
146
+ if self._contextvar_token is None:
147
+ log.warning(f"Span {self.name}: {self.span_id} has not started yet.")
148
+ return
149
+
150
+ self.end_time = iso_timestamp()
151
+ Scope.reset_current_span(self._contextvar_token)
152
+ self._contextvar_token = None
153
+
154
+
155
+ class Span(BaseSpan):
156
+ """An operational span implementation that records and reports tracing data.
157
+
158
+ `Span` instances represent actual units of work that are part of an active trace.
159
+ They record timestamps, manage their context in `Scope`, and interact with the
160
+ `TraceQueueManager` to report their start and end events for later export.
161
+ """
162
+
163
+ def __init__(
164
+ self,
165
+ name: str,
166
+ trace_id: str,
167
+ queue_manager: TraceQueueManager,
168
+ span_id: Optional[str] = None,
169
+ parent_span_id: Optional[str] = None,
170
+ input: Optional[dict[str, Any]] = None,
171
+ output: Optional[dict[str, Any]] = None,
172
+ metadata: Optional[dict[str, Any]] = None,
173
+ span_type: SpanTypeLiterals = "STANDALONE",
174
+ ):
175
+ super().__init__(name, trace_id, queue_manager, span_id, parent_span_id, input, output, metadata, span_type)
176
+ self._queue_manager: TraceQueueManager = queue_manager
177
+ self.trace_id: str = trace_id
178
+
179
+ @override
180
+ def start(self) -> None:
181
+ """Starts the operational Span.
182
+
183
+ Sets the `start_time`, reports the span start to the `TraceQueueManager`
184
+ , and registers this span as the current span.
185
+ """
186
+ if self.start_time is not None:
187
+ log.warning(f"Span {self.name}: {self.span_id} has already started at {self.start_time}")
188
+ return
189
+
190
+ self.start_time = iso_timestamp()
191
+ self._queue_manager.report_span_start(self)
192
+ self._contextvar_token = Scope.set_current_span(self)
193
+
194
+ @override
195
+ def end(self) -> None:
196
+ """Ends the operational Span.
197
+
198
+ Sets the `end_time`, reports the span end (with its complete data) to the
199
+ `TraceQueueManager` for queuing and export, and resets this span from the
200
+ `Scope`.
201
+ """
202
+ if self.end_time is not None:
203
+ log.warning(f"Span {self.name}: {self.span_id} has already ended at {self.end_time}")
204
+ return
205
+ if self._contextvar_token is None:
206
+ log.warning(
207
+ (
208
+ f"Span {self.name}: {self.span_id} attempting to end without a valid context token. "
209
+ "Was start() called and completed successfully?"
210
+ )
211
+ )
212
+ return
213
+
214
+ self.end_time = iso_timestamp()
215
+ self._queue_manager.report_span_end(self)
216
+ Scope.reset_current_span(self._contextvar_token)
217
+ self._contextvar_token = None
@@ -0,0 +1,118 @@
1
+ import logging
2
+ import contextvars
3
+ from types import TracebackType
4
+ from typing import Dict, Type, Optional
5
+ from typing_extensions import override
6
+
7
+ from .span import Span, NoOpSpan
8
+ from .util import generate_trace_id
9
+ from .scope import Scope
10
+ from .trace_queue_manager import TraceQueueManager
11
+
12
+ log: logging.Logger = logging.getLogger(__name__)
13
+
14
+
15
+ class BaseTrace:
16
+ def __init__(self, queue_manager: Optional[TraceQueueManager], trace_id: Optional[str] = None):
17
+ self.trace_id = trace_id or generate_trace_id()
18
+ self.queue_manager = queue_manager
19
+
20
+ self._in_progress = False
21
+ self._contextvar_token: Optional[contextvars.Token[Optional[BaseTrace]]] = None
22
+
23
+ def start(self) -> None:
24
+ pass
25
+
26
+ def end(self) -> None:
27
+ pass
28
+
29
+ def __enter__(self) -> "BaseTrace":
30
+ self.start()
31
+ return self
32
+
33
+ def __exit__(
34
+ self,
35
+ exc_type: Optional[Type[BaseException]] = None,
36
+ exc_value: Optional[BaseException] = None,
37
+ traceback: Optional[TracebackType] = None,
38
+ ) -> None:
39
+ self.end()
40
+
41
+
42
+ class NoOpTrace(BaseTrace):
43
+ def __init__(
44
+ self,
45
+ name: str,
46
+ queue_manager: Optional[TraceQueueManager] = None,
47
+ trace_id: Optional[str] = None,
48
+ root_span_id: Optional[str] = None,
49
+ metadata: Optional[Dict[str, Optional[str]]] = None,
50
+ ):
51
+ super().__init__(queue_manager, trace_id)
52
+
53
+ self.root_span = NoOpSpan(
54
+ name=name,
55
+ span_id=root_span_id,
56
+ trace_id=self.trace_id,
57
+ queue_manager=queue_manager,
58
+ metadata=metadata,
59
+ span_type="TRACER"
60
+ )
61
+
62
+ @override
63
+ def start(self) -> None:
64
+ self.root_span.start()
65
+
66
+ @override
67
+ def end(self) -> None:
68
+ self.root_span.end()
69
+
70
+
71
+ class Trace(BaseTrace):
72
+ def __init__(
73
+ self,
74
+ name: str,
75
+ queue_manager: TraceQueueManager,
76
+ trace_id: Optional[str] = None,
77
+ root_span_id: Optional[str] = None,
78
+ metadata: Optional[Dict[str, Optional[str]]] = None,
79
+ ):
80
+ super().__init__(queue_manager, trace_id)
81
+ self.queue_manager: TraceQueueManager = queue_manager
82
+
83
+ self.root_span = Span(
84
+ name=name,
85
+ span_id=root_span_id,
86
+ trace_id=self.trace_id,
87
+ queue_manager=queue_manager,
88
+ metadata=metadata,
89
+ span_type="TRACER"
90
+ )
91
+
92
+ @override
93
+ def start(self) -> None:
94
+ if self._in_progress:
95
+ log.warning(f"Trace already started: {self.trace_id}")
96
+ return
97
+
98
+ self._in_progress = True
99
+ self.queue_manager.report_trace_start(self) # no-op
100
+ self._contextvar_token = Scope.set_current_trace(self)
101
+
102
+ self.root_span.start()
103
+
104
+ @override
105
+ def end(self) -> None:
106
+ if not self._in_progress:
107
+ log.warning(f"Ending trace which is not active: {self.trace_id}")
108
+ return
109
+ if self._contextvar_token is None:
110
+ log.warning(f"Ending trace which is not active: {self.trace_id}, contextvar_token not set")
111
+ return
112
+
113
+ self._in_progress = False
114
+ self.queue_manager.report_trace_end(self) # no-op
115
+ Scope.reset_current_trace(self._contextvar_token)
116
+ self._contextvar_token = None
117
+
118
+ self.root_span.end()
@@ -0,0 +1,88 @@
1
+ import time
2
+ import logging
3
+ from queue import Empty, Queue
4
+ from typing import TYPE_CHECKING, List
5
+ from typing_extensions import TypeAlias
6
+
7
+ from scale_gp_beta import SGPClient
8
+ from scale_gp_beta._exceptions import APIError
9
+ from scale_gp_beta.types.span_upsert_batch_params import Item as SpanCreateRequest
10
+
11
+ from .exceptions import ParamsCreationError
12
+
13
+ if TYPE_CHECKING:
14
+ from .tracing import Span
15
+
16
+ INITIAL_BACKOFF = 0.4
17
+ MAX_BACKOFF = 20
18
+
19
+ log: logging.Logger = logging.getLogger(__name__)
20
+
21
+ SpanRequestBatch: TypeAlias = List[SpanCreateRequest]
22
+
23
+
24
+ class TraceExporter:
25
+ def __init__(
26
+ self,
27
+ max_batch_size: int,
28
+ max_retries: int,
29
+ backoff: float = INITIAL_BACKOFF,
30
+ max_backoff: float = MAX_BACKOFF,
31
+ ):
32
+ self.max_batch_size = max_batch_size
33
+ self.max_retries = max_retries
34
+ self.backoff = backoff
35
+ self.max_backoff = max_backoff
36
+
37
+ def export(self, client: SGPClient, queue: "Queue[Span]") -> None:
38
+ # export finished spans, note we do a check to ensure spans are finished in spans.py #to_request_params()
39
+ # this is also thread safe, two threads can call this method with the same queue at once
40
+ # the ordering of the requests might be randomly split between the two threads, but they should all be picked up
41
+ batches: list[SpanRequestBatch] = self._create_batches(queue)
42
+
43
+ log.info(f"Exporting {len(batches)} span batches")
44
+
45
+ for batch in batches:
46
+ self._export_batch(batch, client)
47
+
48
+ def _export_batch(self, batch: SpanRequestBatch, client: SGPClient) -> None:
49
+ attempts_remaining = self.max_retries
50
+ backoff_delay = self.backoff
51
+ while attempts_remaining > 0:
52
+ attempts_remaining -= 1
53
+ try:
54
+ client.spans.upsert_batch(items=batch)
55
+ return
56
+ except APIError as e:
57
+ log.warning(
58
+ f"API error occurred while exporting batch: {e.message}, attempts remaining: {attempts_remaining}"
59
+ )
60
+ if attempts_remaining == 0:
61
+ continue
62
+ time.sleep(backoff_delay)
63
+ backoff_delay = min(backoff_delay * 2, self.max_backoff)
64
+
65
+ log.error(f"Failed to export span batch after {self.max_retries} attempts, dropping...")
66
+
67
+ def _create_batches(self, queue: "Queue[Span]") -> "list[SpanRequestBatch]":
68
+ """Drain the queue and return a list of batches"""
69
+ batches: list[SpanRequestBatch] = []
70
+
71
+ while True:
72
+ span_batch: SpanRequestBatch = []
73
+
74
+ while len(span_batch) < self.max_batch_size and queue.qsize() > 0:
75
+ try:
76
+ span: "Span" = queue.get_nowait()
77
+ span_request = span.to_request_params()
78
+ span_batch.append(span_request)
79
+ except Empty:
80
+ break
81
+ except ParamsCreationError as e:
82
+ log.warning(f"ParamsCreationError: {e}\ndropping...")
83
+
84
+ if not span_batch:
85
+ break
86
+ batches.append(span_batch)
87
+
88
+ return batches
@@ -0,0 +1,181 @@
1
+ import time
2
+ import queue
3
+ import atexit
4
+ import logging
5
+ import threading
6
+ from typing import TYPE_CHECKING, Optional
7
+
8
+ from scale_gp_beta import SGPClient, SGPClientError
9
+
10
+ from .util import configure, is_disabled
11
+ from .trace_exporter import TraceExporter
12
+
13
+ if TYPE_CHECKING:
14
+ from .span import Span
15
+ from .trace import Trace
16
+
17
+ # configurable by env vars?
18
+ DEFAULT_MAX_QUEUE_SIZE = 4_000
19
+ DEFAULT_TRIGGER_QUEUE_SIZE = 200
20
+ DEFAULT_TRIGGER_CADENCE = 4.0
21
+ DEFAULT_MAX_BATCH_SIZE = 50
22
+ DEFAULT_RETRIES = 4
23
+
24
+ WORKER_SLEEP_SECONDS = 0.1
25
+
26
+ log: logging.Logger = logging.getLogger(__name__)
27
+
28
+
29
+ class TraceQueueManager:
30
+ """Manage trace and spans queue
31
+ Store spans in-memory until the threshold has been reached then flush to server.
32
+
33
+ Optionally provide a client, if unprovided we will attempt to create a default client.
34
+ """
35
+
36
+ def __init__(
37
+ self,
38
+ client: Optional[SGPClient] = None,
39
+ max_queue_size: int = DEFAULT_MAX_QUEUE_SIZE,
40
+ max_batch_size: int = DEFAULT_MAX_BATCH_SIZE,
41
+ trigger_queue_size: int = DEFAULT_TRIGGER_QUEUE_SIZE,
42
+ trigger_cadence: float = DEFAULT_TRIGGER_CADENCE,
43
+ retries: int = DEFAULT_RETRIES,
44
+ ):
45
+ self._client = client
46
+ self._attempted_local_client_creation = False
47
+ self._trigger_queue_size = trigger_queue_size
48
+ self._trigger_cadence = trigger_cadence
49
+
50
+ self._reset_trigger_time()
51
+
52
+ self._exporter = TraceExporter(max_batch_size, retries)
53
+
54
+ self._shutdown_event = threading.Event()
55
+ self._queue: queue.Queue[Span] = queue.Queue(maxsize=max_queue_size)
56
+
57
+ if not is_disabled():
58
+ self._worker = threading.Thread(daemon=True, target=self._run)
59
+ self._worker.start()
60
+
61
+ # ensure the thread joins on exit
62
+ atexit.register(self.shutdown)
63
+
64
+ def register_client(self, client: SGPClient) -> None:
65
+ log.info("Registering client")
66
+ self._client = client
67
+
68
+ def shutdown(self, timeout: Optional[float] = None) -> None:
69
+ if is_disabled():
70
+ log.debug("No worker to shutdown")
71
+ return
72
+ log.info(f"Shutting down trace queue manager, joining worker thread with timeout {timeout}")
73
+ self._shutdown_event.set()
74
+ self._worker.join(timeout=timeout)
75
+ log.info("Shutdown complete")
76
+
77
+ def report_span_start(self, span: "Span") -> None:
78
+ # TODO: support making this optional. Current backend requires us to send span starts
79
+ try:
80
+ self._queue.put_nowait(span)
81
+ except queue.Full:
82
+ log.warning(f"Queue full, ignoring span {span.span_id}")
83
+
84
+ def report_span_end(self, span: "Span") -> None:
85
+ try:
86
+ self._queue.put_nowait(span)
87
+ except queue.Full:
88
+ log.warning(f"Queue full, ignoring span {span.span_id}")
89
+
90
+ def report_trace_start(self, trace: "Trace") -> None:
91
+ pass
92
+
93
+ def report_trace_end(self, trace: "Trace") -> None:
94
+ pass
95
+
96
+ def flush_queue(self) -> None:
97
+ self._export()
98
+
99
+ @property
100
+ def client(self) -> Optional[SGPClient]:
101
+ """
102
+ Use client provided client on init if available, otherwise attempt once to create a default one.
103
+ :return: SGPClient
104
+ """
105
+ if self._client is not None:
106
+ return self._client
107
+ if self._attempted_local_client_creation:
108
+ # Already tried and failed to create a client
109
+ return None
110
+
111
+ log.info("Tracing queue manager not initialized, attempting to create a default one")
112
+ try:
113
+ self.register_client(SGPClient())
114
+ except SGPClientError:
115
+ log.warning(
116
+ f"Failed to create SGPClient for tracing queue manager {self}, ignoring traces. Please initialize with a working client."
117
+ )
118
+ finally:
119
+ self._attempted_local_client_creation = True
120
+ return self._client
121
+
122
+ def _run(self) -> None:
123
+ # daemon worker loop
124
+ while not self._shutdown_event.is_set():
125
+ current_time = time.time()
126
+ queue_size = self._queue.qsize()
127
+
128
+ if queue_size >= self._trigger_queue_size or current_time >= self._next_trigger_time:
129
+ self._export()
130
+ self._reset_trigger_time()
131
+ continue
132
+
133
+ time.sleep(WORKER_SLEEP_SECONDS)
134
+
135
+ # flush all on shutdown
136
+ self._export()
137
+
138
+ def _export(self) -> None:
139
+ if self.client:
140
+ self._exporter.export(self.client, self._queue)
141
+
142
+ def _reset_trigger_time(self) -> None:
143
+ self._next_trigger_time = time.time() + self._trigger_cadence
144
+
145
+
146
+ _global_tracing_queue_manager: Optional[TraceQueueManager] = None
147
+ _queue_manager_lock = threading.Lock()
148
+
149
+
150
+ def init(client: Optional[SGPClient] = None, disabled: Optional[bool] = None) -> None:
151
+ """Initialize the tracing backend
152
+
153
+ Good practice to always include this method call with a valid client at your program entrypoint.
154
+
155
+ Tracing will attempt to generate a default client if unprovided.
156
+
157
+ :param client: SGPClient
158
+ :param disabled: Set to True to disable tracing. Overrides environment variable ``DISABLE_SCALE_TRACING``
159
+ """
160
+ if disabled is not None:
161
+ configure(disabled=disabled)
162
+
163
+ global _global_tracing_queue_manager
164
+ if _global_tracing_queue_manager is not None:
165
+ return
166
+
167
+ with _queue_manager_lock:
168
+ if _global_tracing_queue_manager is None:
169
+ _global_tracing_queue_manager = TraceQueueManager(client)
170
+
171
+
172
+ def tracing_queue_manager() -> TraceQueueManager:
173
+ global _global_tracing_queue_manager
174
+ if _global_tracing_queue_manager is None:
175
+ init(None)
176
+
177
+ if _global_tracing_queue_manager is None:
178
+ # should never happen... useful for linting
179
+ raise RuntimeError("Tracing queue manager failed to initialize.")
180
+
181
+ return _global_tracing_queue_manager
@@ -0,0 +1,180 @@
1
+ import logging
2
+ from typing import Any, Dict, Union, Optional
3
+
4
+ from .span import Span, BaseSpan, NoOpSpan
5
+ from .util import is_disabled
6
+ from .scope import Scope
7
+ from .trace import Trace, BaseTrace, NoOpTrace
8
+ from .trace_queue_manager import tracing_queue_manager
9
+
10
+ log: logging.Logger = logging.getLogger(__name__)
11
+
12
+
13
+ def current_span() -> Optional[BaseSpan]:
14
+ """Retrieves the currently active span from the execution context.
15
+
16
+ This function relies on `contextvars` to manage the active span in
17
+ a context-local manner, making it safe for concurrent execution
18
+ environments (e.g., threads, asyncio tasks).
19
+
20
+ Returns:
21
+ Optional[BaseSpan]: The current BaseSpan instance if one is active,
22
+ otherwise None. This could be a 'Span' or 'NoOpSpan'.
23
+ """
24
+ return Scope.get_current_span()
25
+
26
+
27
+ def current_trace() -> Optional[BaseTrace]:
28
+ """Retrieves the currently active trace from the execution context.
29
+
30
+ Similarly, to `current_span()`, this uses `contextvars` for context-local
31
+ trace management.
32
+
33
+ Returns:
34
+ Optional[BaseTrace]: The current BaseTrace instance if one is active,
35
+ otherwise None. This could be a 'Trace' or 'NoOpTrace'.
36
+ """
37
+ return Scope.get_current_trace()
38
+
39
+
40
+ def flush_queue() -> None:
41
+ """
42
+ Blocking flush of all requests in the queue.
43
+
44
+ Useful for distributed applications to ensure spans have been committed before continuing.
45
+ :return:
46
+ """
47
+ queue_manager = tracing_queue_manager()
48
+ queue_manager.flush_queue()
49
+
50
+
51
+ def create_trace(
52
+ name: str,
53
+ trace_id: Optional[str] = None,
54
+ root_span_id: Optional[str] = None,
55
+ metadata: Optional[Dict[str, Optional[str]]] = None,
56
+ ) -> BaseTrace:
57
+ """Creates a new trace and root span instance.
58
+
59
+ A trace represents a single, logical operation or workflow. It groups multiple
60
+ spans together. If tracing is disabled (via the 'DISABLE_SCALE_TRACING'
61
+ environment variable), a `NoOpTrace` instance is returned which performs no
62
+ actual tracing operations.
63
+
64
+ When a trace is started (e.g., by using it as a context manager or calling its
65
+ `start()` method), it becomes the `current_trace()` in the active scope.
66
+ Similarly, the root span instance becomes the `current_span()` in the active
67
+ scope.
68
+
69
+ Args:
70
+ name: The name of the trace.
71
+ trace_id (Optional[str]): An optional, user-defined ID for the trace.
72
+ If None, a unique trace ID will be generated.
73
+ root_span_id (Optional[str]): An optional, user-defined ID for the root span.
74
+ metadata (Optional[Dict[str, Optional[str]]]): An optional, user-defined metadata.
75
+
76
+ Returns:
77
+ BaseTrace: A `Trace` instance if tracing is enabled, or a `NoOpTrace`
78
+ instance if tracing is disabled.
79
+ """
80
+ if is_disabled():
81
+ log.debug(f"Tracing is disabled. Not creating a new trace.")
82
+ return NoOpTrace(name=name, trace_id=trace_id, root_span_id=root_span_id, metadata=metadata)
83
+
84
+ active_trace = current_trace()
85
+ if active_trace is not None:
86
+ log.warning(f"Trace with id {active_trace.trace_id} is already active. Creating a new trace anyways.")
87
+
88
+ trace = Trace(name=name, trace_id=trace_id, queue_manager=tracing_queue_manager(), metadata=metadata)
89
+ log.debug(f"Created new trace: {trace.trace_id}")
90
+
91
+ return trace
92
+
93
+
94
+ def create_span(
95
+ name: str,
96
+ span_id: Optional[str] = None,
97
+ input: Optional[Dict[str, Any]] = None,
98
+ output: Optional[Dict[str, Any]] = None,
99
+ metadata: Optional[Dict[str, Optional[str]]] = None,
100
+ trace: Optional[Union[BaseTrace, str]] = None,
101
+ parent_span: Optional[BaseSpan] = None,
102
+ ) -> BaseSpan:
103
+ """Creates a new span instance.
104
+
105
+ A span represents a single unit of work or operation within a trace. Spans
106
+ can be nested to form a hierarchy.
107
+
108
+ If tracing is disabled (via 'DISABLE_SCALE_TRACING' environment variable),
109
+ a `NoOpSpan` is returned. Additionally, if no explicit `parent` (Trace or Span)
110
+ is provided and there is no `current_trace()` active in the scope, a `NoOpSpan`
111
+ will also be returned to prevent orphaned spans.
112
+
113
+ When a span is started (e.g., via context manager or `start()`), it becomes
114
+ the `current_span()` in the active scope.
115
+
116
+ Args:
117
+ name (str): A descriptive name for the span (e.g., "database_query",
118
+ "http_request").
119
+ span_id (Optional[str]): An optional, user-defined ID for the span.
120
+ input (Optional[dict[str, Any]], optional): A dictionary containing
121
+ input data or parameters relevant to this span's operation. Defaults to None.
122
+ output (Optional[dict[str, Any]], optional): A dictionary containing
123
+ output data or results from this span's operation. Defaults to None.
124
+ metadata (Optional[dict[str, Union[str, int, float, bool, None]]], optional):
125
+ A dictionary for arbitrary key-value pairs providing additional
126
+ context or annotations for the span. Values should be simple types.
127
+ Defaults to None.
128
+ trace (Optional[Union[BaseTrace, str]], optional): A `Trace` instance
129
+ or a trace ID. Used for explicit control. Default to trace fetched
130
+ from the active scope.
131
+ parent_span (Optional[BaseSpan], optional): A `Span` instance. Like
132
+ trace, used for explicit control. Defaults to span fetched from the
133
+ active scope.
134
+
135
+ Returns:
136
+ BaseSpan: A `Span` instance if tracing is enabled and a valid trace context
137
+ exists, or a `NoOpSpan` otherwise.
138
+ """
139
+ queue_manager = tracing_queue_manager()
140
+ parent_span_id: Optional[str] = None
141
+
142
+ if parent_span is not None:
143
+ trace_id = parent_span.trace_id
144
+ parent_span_id = parent_span.span_id
145
+ elif trace is not None:
146
+ trace_id = trace if isinstance(trace, str) else trace.trace_id
147
+
148
+ parent_span = Scope.get_current_span()
149
+ parent_span_id = parent_span.span_id if parent_span else None
150
+ else:
151
+ trace = Scope.get_current_trace()
152
+ parent_span = Scope.get_current_span()
153
+
154
+ parent_span_id = parent_span.span_id if parent_span else None
155
+
156
+ if trace is None:
157
+ # need to think about default behavior here... do we create a trace, some other options?
158
+ # I am leaning towards setting it as an option as sometimes people might want to be succinct or when we
159
+ # build decorators we might want this functionality
160
+ log.debug(f"attempting to create a span with no trace")
161
+ return NoOpSpan(name=name, span_id=span_id, parent_span_id=parent_span_id)
162
+
163
+ trace_id = trace.trace_id
164
+
165
+ if is_disabled():
166
+ return NoOpSpan(name=name, span_id=span_id, parent_span_id=parent_span_id, trace_id=trace_id)
167
+
168
+ span = Span(
169
+ name=name,
170
+ span_id=span_id,
171
+ parent_span_id=parent_span_id,
172
+ trace_id=trace_id,
173
+ input=input or {},
174
+ output=output or {},
175
+ metadata=metadata or {},
176
+ queue_manager=queue_manager,
177
+ )
178
+ log.debug(f"Created new span: {span.span_id}")
179
+
180
+ return span
@@ -0,0 +1,40 @@
1
+ """
2
+ This is necessary, unfortunately. Stainless does not provide these as enums, only as type annotations.
3
+
4
+ For strict linting, we need to reference these enums.
5
+
6
+ NOTE: These will have to be manually updated to support updated span_types and status.
7
+ """
8
+
9
+ from typing_extensions import Literal
10
+
11
+ SpanStatusLiterals = Literal["SUCCESS", "ERROR"]
12
+
13
+ SpanTypeLiterals = Literal[
14
+ "TEXT_INPUT",
15
+ "TEXT_OUTPUT",
16
+ "COMPLETION_INPUT",
17
+ "COMPLETION",
18
+ "KB_RETRIEVAL",
19
+ "KB_INPUT",
20
+ "RERANKING",
21
+ "EXTERNAL_ENDPOINT",
22
+ "PROMPT_ENGINEERING",
23
+ "DOCUMENT_INPUT",
24
+ "MAP_REDUCE",
25
+ "DOCUMENT_SEARCH",
26
+ "DOCUMENT_PROMPT",
27
+ "CUSTOM",
28
+ "INPUT_GUARDRAIL",
29
+ "OUTPUT_GUARDRAIL",
30
+ "CODE_EXECUTION",
31
+ "DATA_MANIPULATION",
32
+ "EVALUATION",
33
+ "FILE_RETRIEVAL",
34
+ "KB_ADD_CHUNK",
35
+ "KB_MANAGEMENT",
36
+ "TRACER",
37
+ "AGENT_TRACER",
38
+ "AGENT_WORKFLOW",
39
+ "STANDALONE",
40
+ ]
@@ -0,0 +1,68 @@
1
+ import os
2
+ import uuid
3
+ from typing import Optional
4
+ from datetime import datetime, timezone
5
+ from functools import cache
6
+
7
+ _tracing_disabled: Optional[bool] = None
8
+
9
+
10
+ def generate_trace_id() -> str:
11
+ """
12
+ Generate a unique trace ID.
13
+
14
+ Returns:
15
+ str: A unique trace ID.
16
+ """
17
+ return str(uuid.uuid4())
18
+
19
+
20
+ def generate_span_id() -> str:
21
+ """
22
+ Generate a unique span ID.
23
+
24
+ Returns:
25
+ str: A unique span ID.
26
+ """
27
+ return str(uuid.uuid4())
28
+
29
+
30
+ def iso_timestamp() -> str:
31
+ """
32
+ Generate an ISO 8601 timestamp.
33
+
34
+ Returns:
35
+ str: The current time in ISO 8601 format.
36
+ """
37
+ return datetime.now(timezone.utc).isoformat()
38
+
39
+
40
+ def configure(disabled: bool) -> None:
41
+ """
42
+ Programmatically enable or disable tracing.
43
+
44
+ This setting takes precedence over the `DISABLE_SCALE_TRACING` environment
45
+ variable.
46
+
47
+ Args:
48
+ disabled (bool): Set to True to disable tracing, False to enable.
49
+ """
50
+ global _tracing_disabled
51
+ _tracing_disabled = disabled
52
+ is_disabled.cache_clear()
53
+
54
+
55
+ @cache
56
+ def is_disabled() -> bool:
57
+ """
58
+ Check if tracing is disabled, with programmatic control taking precedence.
59
+
60
+ Tracing is considered disabled if `configure(disabled=True)` has been called,
61
+ or if the `DISABLE_SCALE_TRACING` environment variable is set.
62
+
63
+ Returns:
64
+ bool: True if tracing is disabled, otherwise False.
65
+ """
66
+ if _tracing_disabled is not None:
67
+ return _tracing_disabled
68
+ return os.getenv("DISABLE_SCALE_TRACING") is not None
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: scale-gp-beta
3
- Version: 0.1.0a17
3
+ Version: 0.1.0a18
4
4
  Summary: The official Python library for the Scale GP API
5
5
  Project-URL: Homepage, https://github.com/scaleapi/sgp-python-beta
6
6
  Project-URL: Repository, https://github.com/scaleapi/sgp-python-beta
@@ -11,7 +11,7 @@ scale_gp_beta/_resource.py,sha256=siZly_U6D0AOVLAzaOsqUdEFFzVMbWRj-ml30nvRp7E,11
11
11
  scale_gp_beta/_response.py,sha256=GemuybPk0uemovTlGHyHkj-ScYTTDJA0jqH5FQqIPwQ,28852
12
12
  scale_gp_beta/_streaming.py,sha256=fcCSGXslmi2SmmkM05g2SACXHk2Mj7k1X5uMBu6U5s8,10112
13
13
  scale_gp_beta/_types.py,sha256=0wSs40TefKMPBj2wQKenEeZ0lzedoHClNJeqrpAgkII,6204
14
- scale_gp_beta/_version.py,sha256=8X-aROY-qc-hXo3LtgtE__YX1vTzDaeOZIxZVj1_jfE,174
14
+ scale_gp_beta/_version.py,sha256=HRBrZAuyRmTcXMth7CO9Xnuo6QBBXCQICXY1m1KXiJk,174
15
15
  scale_gp_beta/pagination.py,sha256=t-U86PYxl20VRsz8VXOMJJDe7HxkX7ISFMvRNbBNy9s,4054
16
16
  scale_gp_beta/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
17
  scale_gp_beta/_utils/__init__.py,sha256=PNZ_QJuzZEgyYXqkO1HVhGkj5IU9bglVUcw7H-Knjzw,2062
@@ -25,6 +25,16 @@ scale_gp_beta/_utils/_transform.py,sha256=n7kskEWz6o__aoNvhFoGVyDoalNe6mJwp-g7BW
25
25
  scale_gp_beta/_utils/_typing.py,sha256=D0DbbNu8GnYQTSICnTSHDGsYXj8TcAKyhejb0XcnjtY,4602
26
26
  scale_gp_beta/_utils/_utils.py,sha256=ts4CiiuNpFiGB6YMdkQRh2SZvYvsl7mAF-JWHCcLDf4,12312
27
27
  scale_gp_beta/lib/.keep,sha256=wuNrz-5SXo3jJaJOJgz4vFHM41YH_g20F5cRQo0vLes,224
28
+ scale_gp_beta/lib/tracing/__init__.py,sha256=UgyExbqAA2ljDEF4X4YFhtbBZuoQJ2IF4hkGs_xQEc0,226
29
+ scale_gp_beta/lib/tracing/exceptions.py,sha256=vL2_GAfWEy8EfLhrBkDClLYTasOLnL-5zUpdCQnSzcs,107
30
+ scale_gp_beta/lib/tracing/scope.py,sha256=kHrd0his8L2K_KXn2E6J9d565PliEdFoKRQ1d5ALTyk,3901
31
+ scale_gp_beta/lib/tracing/span.py,sha256=kLo7rCAqENSnTfPbeldg1MqdSl7k9tH-1LwhWa0GhFs,8265
32
+ scale_gp_beta/lib/tracing/trace.py,sha256=jDZeT5-cE70FcsjC18CEGBdbDqYX36LqlnLIavK98RI,3382
33
+ scale_gp_beta/lib/tracing/trace_exporter.py,sha256=XLr4S8_FYww0dQToLyj56E5Vhhq04JDSkdgTml-fnOM,3146
34
+ scale_gp_beta/lib/tracing/trace_queue_manager.py,sha256=DCR239JuOTr76Lo5jBPDhi8ZCqxCdH9GklXKuOyzUqE,5880
35
+ scale_gp_beta/lib/tracing/tracing.py,sha256=xht7oAzxbgIE3fgH_botEDxBDKBr_8r5jrtFwtaVmiQ,7193
36
+ scale_gp_beta/lib/tracing/types.py,sha256=CKdOHYIEZGQXWFSrn10iC-cyRw6_Lido9jC8pdSc3P0,1010
37
+ scale_gp_beta/lib/tracing/util.py,sha256=8Oq4wLXRNOzh3CC1zRaBEr0h_WdXLrk536BUNKRddVE,1527
28
38
  scale_gp_beta/resources/__init__.py,sha256=Fyo05_2_pc5orfyTSIpxa3btmBTd45VasgibwSqbbKo,4942
29
39
  scale_gp_beta/resources/completions.py,sha256=4esj9lGTJAxt6wFvON126DvEGkMIChRZ6uZBOf56Aac,31868
30
40
  scale_gp_beta/resources/dataset_items.py,sha256=2d7O5zmqVEafJTxVwgbRz9yq-4T81dPPfFuPDRAaWqU,22510
@@ -107,7 +117,7 @@ scale_gp_beta/types/chat/completion_models_params.py,sha256=ETxafJIUx4tTvkiR-ZCr
107
117
  scale_gp_beta/types/chat/completion_models_response.py,sha256=Ctgj6o-QWPSdjBKzG9J4Id0-DjXu4UGGw1NR6-840Ec,403
108
118
  scale_gp_beta/types/chat/model_definition.py,sha256=NNgopTm900GD0Zs2YHkcvoW67uKaWUKVyPbhKBHvKdQ,817
109
119
  scale_gp_beta/types/files/__init__.py,sha256=OKfJYcKb4NObdiRObqJV_dOyDQ8feXekDUge2o_4pXQ,122
110
- scale_gp_beta-0.1.0a17.dist-info/METADATA,sha256=ZXduP_l4D_AXtqcwASCXrowWY5Af3_hdJFzmjysq9hk,16881
111
- scale_gp_beta-0.1.0a17.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
112
- scale_gp_beta-0.1.0a17.dist-info/licenses/LICENSE,sha256=x49Bj8r_ZpqfzThbmfHyZ_bE88XvHdIMI_ANyLHFFRE,11338
113
- scale_gp_beta-0.1.0a17.dist-info/RECORD,,
120
+ scale_gp_beta-0.1.0a18.dist-info/METADATA,sha256=KLOOZkz6tbD1xiUdgpc_sPQ0w9Mvj4m17WGxQ4sPDPc,16881
121
+ scale_gp_beta-0.1.0a18.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
122
+ scale_gp_beta-0.1.0a18.dist-info/licenses/LICENSE,sha256=x49Bj8r_ZpqfzThbmfHyZ_bE88XvHdIMI_ANyLHFFRE,11338
123
+ scale_gp_beta-0.1.0a18.dist-info/RECORD,,