mseep-agentops 0.4.18__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.
- agentops/__init__.py +488 -0
- agentops/client/__init__.py +5 -0
- agentops/client/api/__init__.py +71 -0
- agentops/client/api/base.py +162 -0
- agentops/client/api/types.py +21 -0
- agentops/client/api/versions/__init__.py +10 -0
- agentops/client/api/versions/v3.py +65 -0
- agentops/client/api/versions/v4.py +104 -0
- agentops/client/client.py +211 -0
- agentops/client/http/__init__.py +0 -0
- agentops/client/http/http_adapter.py +116 -0
- agentops/client/http/http_client.py +215 -0
- agentops/config.py +268 -0
- agentops/enums.py +36 -0
- agentops/exceptions.py +38 -0
- agentops/helpers/__init__.py +44 -0
- agentops/helpers/dashboard.py +54 -0
- agentops/helpers/deprecation.py +50 -0
- agentops/helpers/env.py +52 -0
- agentops/helpers/serialization.py +137 -0
- agentops/helpers/system.py +178 -0
- agentops/helpers/time.py +11 -0
- agentops/helpers/version.py +36 -0
- agentops/instrumentation/__init__.py +598 -0
- agentops/instrumentation/common/__init__.py +82 -0
- agentops/instrumentation/common/attributes.py +278 -0
- agentops/instrumentation/common/instrumentor.py +147 -0
- agentops/instrumentation/common/metrics.py +100 -0
- agentops/instrumentation/common/objects.py +26 -0
- agentops/instrumentation/common/span_management.py +176 -0
- agentops/instrumentation/common/streaming.py +218 -0
- agentops/instrumentation/common/token_counting.py +177 -0
- agentops/instrumentation/common/version.py +71 -0
- agentops/instrumentation/common/wrappers.py +235 -0
- agentops/legacy/__init__.py +277 -0
- agentops/legacy/event.py +156 -0
- agentops/logging/__init__.py +4 -0
- agentops/logging/config.py +86 -0
- agentops/logging/formatters.py +34 -0
- agentops/logging/instrument_logging.py +91 -0
- agentops/sdk/__init__.py +27 -0
- agentops/sdk/attributes.py +151 -0
- agentops/sdk/core.py +607 -0
- agentops/sdk/decorators/__init__.py +51 -0
- agentops/sdk/decorators/factory.py +486 -0
- agentops/sdk/decorators/utility.py +216 -0
- agentops/sdk/exporters.py +87 -0
- agentops/sdk/processors.py +71 -0
- agentops/sdk/types.py +21 -0
- agentops/semconv/__init__.py +36 -0
- agentops/semconv/agent.py +29 -0
- agentops/semconv/core.py +19 -0
- agentops/semconv/enum.py +11 -0
- agentops/semconv/instrumentation.py +13 -0
- agentops/semconv/langchain.py +63 -0
- agentops/semconv/message.py +61 -0
- agentops/semconv/meters.py +24 -0
- agentops/semconv/resource.py +52 -0
- agentops/semconv/span_attributes.py +118 -0
- agentops/semconv/span_kinds.py +50 -0
- agentops/semconv/status.py +11 -0
- agentops/semconv/tool.py +15 -0
- agentops/semconv/workflow.py +69 -0
- agentops/validation.py +357 -0
- mseep_agentops-0.4.18.dist-info/METADATA +49 -0
- mseep_agentops-0.4.18.dist-info/RECORD +94 -0
- mseep_agentops-0.4.18.dist-info/WHEEL +5 -0
- mseep_agentops-0.4.18.dist-info/licenses/LICENSE +21 -0
- mseep_agentops-0.4.18.dist-info/top_level.txt +2 -0
- tests/__init__.py +0 -0
- tests/conftest.py +10 -0
- tests/unit/__init__.py +0 -0
- tests/unit/client/__init__.py +1 -0
- tests/unit/client/test_http_adapter.py +221 -0
- tests/unit/client/test_http_client.py +206 -0
- tests/unit/conftest.py +54 -0
- tests/unit/sdk/__init__.py +1 -0
- tests/unit/sdk/instrumentation_tester.py +207 -0
- tests/unit/sdk/test_attributes.py +392 -0
- tests/unit/sdk/test_concurrent_instrumentation.py +468 -0
- tests/unit/sdk/test_decorators.py +763 -0
- tests/unit/sdk/test_exporters.py +241 -0
- tests/unit/sdk/test_factory.py +1188 -0
- tests/unit/sdk/test_internal_span_processor.py +397 -0
- tests/unit/sdk/test_resource_attributes.py +35 -0
- tests/unit/test_config.py +82 -0
- tests/unit/test_context_manager.py +777 -0
- tests/unit/test_events.py +27 -0
- tests/unit/test_host_env.py +54 -0
- tests/unit/test_init_py.py +501 -0
- tests/unit/test_serialization.py +433 -0
- tests/unit/test_session.py +676 -0
- tests/unit/test_user_agent.py +34 -0
- tests/unit/test_validation.py +405 -0
agentops/sdk/core.py
ADDED
@@ -0,0 +1,607 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import atexit
|
4
|
+
import threading
|
5
|
+
from typing import Optional, Any, Dict, Union
|
6
|
+
|
7
|
+
from opentelemetry import metrics, trace
|
8
|
+
from opentelemetry.exporter.otlp.proto.http.metric_exporter import OTLPMetricExporter
|
9
|
+
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
|
10
|
+
from opentelemetry.sdk.metrics import MeterProvider
|
11
|
+
from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
|
12
|
+
from opentelemetry.sdk.resources import Resource
|
13
|
+
from opentelemetry.sdk.trace import TracerProvider, Span
|
14
|
+
from opentelemetry.sdk.trace.export import BatchSpanProcessor
|
15
|
+
from opentelemetry import context as context_api
|
16
|
+
|
17
|
+
from agentops.exceptions import AgentOpsClientNotInitializedException
|
18
|
+
from agentops.logging import logger, setup_print_logger
|
19
|
+
from agentops.sdk.processors import InternalSpanProcessor
|
20
|
+
from agentops.sdk.types import TracingConfig
|
21
|
+
from agentops.sdk.attributes import (
|
22
|
+
get_global_resource_attributes,
|
23
|
+
get_trace_attributes,
|
24
|
+
get_span_attributes,
|
25
|
+
get_session_end_attributes,
|
26
|
+
get_system_resource_attributes,
|
27
|
+
)
|
28
|
+
from agentops.semconv import SpanKind
|
29
|
+
from agentops.helpers.dashboard import log_trace_url
|
30
|
+
from opentelemetry.trace.status import StatusCode
|
31
|
+
|
32
|
+
# No need to create shortcuts since we're using our own ResourceAttributes class now
|
33
|
+
|
34
|
+
|
35
|
+
# Define TraceContext to hold span and token
|
36
|
+
class TraceContext:
|
37
|
+
def __init__(self, span: Span, token: Optional[context_api.Token] = None, is_init_trace: bool = False):
|
38
|
+
self.span = span
|
39
|
+
self.token = token
|
40
|
+
self.is_init_trace = is_init_trace # Flag to identify the auto-started trace
|
41
|
+
self._end_state = StatusCode.UNSET # Default end state because we don't know yet
|
42
|
+
|
43
|
+
def __enter__(self) -> "TraceContext":
|
44
|
+
"""Enter the trace context."""
|
45
|
+
return self
|
46
|
+
|
47
|
+
def __exit__(self, exc_type: Optional[type], exc_val: Optional[Exception], exc_tb: Optional[Any]) -> bool:
|
48
|
+
"""Exit the trace context and end the trace.
|
49
|
+
|
50
|
+
Automatically sets the trace status based on whether an exception occurred:
|
51
|
+
- If an exception is present, sets status to ERROR
|
52
|
+
- If no exception occurred, sets status to OK
|
53
|
+
|
54
|
+
Returns:
|
55
|
+
False: Always returns False to propagate any exceptions that occurred
|
56
|
+
within the context manager block, following Python's
|
57
|
+
context manager protocol for proper exception handling.
|
58
|
+
"""
|
59
|
+
if exc_type is not None:
|
60
|
+
self._end_state = StatusCode.ERROR
|
61
|
+
if exc_val:
|
62
|
+
logger.debug(f"Trace exiting with exception: {exc_val}")
|
63
|
+
else:
|
64
|
+
# No exception occurred, set to OK
|
65
|
+
self._end_state = StatusCode.OK
|
66
|
+
|
67
|
+
try:
|
68
|
+
tracer.end_trace(self, self._end_state)
|
69
|
+
except Exception as e:
|
70
|
+
logger.error(f"Error ending trace in context manager: {e}")
|
71
|
+
|
72
|
+
return False
|
73
|
+
|
74
|
+
|
75
|
+
# get_imported_libraries moved to agentops.helpers.system
|
76
|
+
|
77
|
+
|
78
|
+
def setup_telemetry(
|
79
|
+
service_name: str = "agentops",
|
80
|
+
project_id: Optional[str] = None,
|
81
|
+
exporter_endpoint: str = "https://otlp.agentops.ai/v1/traces",
|
82
|
+
metrics_endpoint: str = "https://otlp.agentops.ai/v1/metrics",
|
83
|
+
max_queue_size: int = 512,
|
84
|
+
max_wait_time: int = 5000,
|
85
|
+
export_flush_interval: int = 1000,
|
86
|
+
jwt: Optional[str] = None,
|
87
|
+
) -> tuple[TracerProvider, MeterProvider]:
|
88
|
+
"""
|
89
|
+
Setup the telemetry system.
|
90
|
+
|
91
|
+
Args:
|
92
|
+
service_name: Name of the OpenTelemetry service
|
93
|
+
project_id: Project ID to include in resource attributes
|
94
|
+
exporter_endpoint: Endpoint for the span exporter
|
95
|
+
metrics_endpoint: Endpoint for the metrics exporter
|
96
|
+
max_queue_size: Maximum number of spans to queue before forcing a flush
|
97
|
+
max_wait_time: Maximum time in milliseconds to wait before flushing
|
98
|
+
export_flush_interval: Time interval in milliseconds between automatic exports of telemetry data
|
99
|
+
jwt: JWT token for authentication
|
100
|
+
|
101
|
+
Returns:
|
102
|
+
Tuple of (TracerProvider, MeterProvider)
|
103
|
+
"""
|
104
|
+
# Build resource attributes
|
105
|
+
resource_attrs = get_global_resource_attributes(
|
106
|
+
service_name=service_name,
|
107
|
+
project_id=project_id,
|
108
|
+
)
|
109
|
+
|
110
|
+
resource = Resource(resource_attrs)
|
111
|
+
provider = TracerProvider(resource=resource)
|
112
|
+
|
113
|
+
# Set as global provider
|
114
|
+
trace.set_tracer_provider(provider)
|
115
|
+
|
116
|
+
# Create exporter with authentication
|
117
|
+
exporter = OTLPSpanExporter(endpoint=exporter_endpoint, headers={"Authorization": f"Bearer {jwt}"} if jwt else {})
|
118
|
+
|
119
|
+
# Regular processor for normal spans and immediate export
|
120
|
+
processor = BatchSpanProcessor(
|
121
|
+
exporter,
|
122
|
+
max_export_batch_size=max_queue_size,
|
123
|
+
schedule_delay_millis=export_flush_interval,
|
124
|
+
)
|
125
|
+
provider.add_span_processor(processor)
|
126
|
+
internal_processor = InternalSpanProcessor() # Catches spans for AgentOps on-terminal printing
|
127
|
+
provider.add_span_processor(internal_processor)
|
128
|
+
|
129
|
+
# Setup metrics
|
130
|
+
metric_exporter = OTLPMetricExporter(
|
131
|
+
endpoint=metrics_endpoint, headers={"Authorization": f"Bearer {jwt}"} if jwt else {}
|
132
|
+
)
|
133
|
+
metric_reader = PeriodicExportingMetricReader(metric_exporter)
|
134
|
+
meter_provider = MeterProvider(resource=resource, metric_readers=[metric_reader])
|
135
|
+
metrics.set_meter_provider(meter_provider)
|
136
|
+
|
137
|
+
### Logging
|
138
|
+
setup_print_logger()
|
139
|
+
|
140
|
+
# Initialize root context
|
141
|
+
# context_api.get_current() # It's better to manage context explicitly with traces
|
142
|
+
|
143
|
+
logger.debug("Telemetry system initialized")
|
144
|
+
|
145
|
+
return provider, meter_provider
|
146
|
+
|
147
|
+
|
148
|
+
class TracingCore:
|
149
|
+
"""
|
150
|
+
Central component for tracing in AgentOps.
|
151
|
+
|
152
|
+
This class manages the creation, processing, and export of spans.
|
153
|
+
It handles provider management, span creation, and context propagation.
|
154
|
+
"""
|
155
|
+
|
156
|
+
def __init__(self) -> None:
|
157
|
+
"""Initialize the tracing core."""
|
158
|
+
self.provider: Optional[TracerProvider] = None
|
159
|
+
self._meter_provider: Optional[MeterProvider] = None
|
160
|
+
self._initialized = False
|
161
|
+
self._config: Optional[TracingConfig] = None
|
162
|
+
self._span_processors: list = []
|
163
|
+
self._active_traces: dict = {}
|
164
|
+
self._traces_lock = threading.Lock()
|
165
|
+
|
166
|
+
# Register shutdown handler
|
167
|
+
atexit.register(self.shutdown)
|
168
|
+
|
169
|
+
def initialize(self, jwt: Optional[str] = None, **kwargs: Any) -> None:
|
170
|
+
"""
|
171
|
+
Initialize the tracing core with the given configuration.
|
172
|
+
|
173
|
+
Args:
|
174
|
+
jwt: JWT token for authentication
|
175
|
+
**kwargs: Configuration parameters for tracing
|
176
|
+
service_name: Name of the service
|
177
|
+
exporter: Custom span exporter
|
178
|
+
processor: Custom span processor
|
179
|
+
exporter_endpoint: Endpoint for the span exporter
|
180
|
+
max_queue_size: Maximum number of spans to queue before forcing a flush
|
181
|
+
max_wait_time: Maximum time in milliseconds to wait before flushing
|
182
|
+
api_key: API key for authentication (required for authenticated exporter)
|
183
|
+
project_id: Project ID to include in resource attributes
|
184
|
+
"""
|
185
|
+
if self._initialized:
|
186
|
+
return
|
187
|
+
|
188
|
+
# Set default values for required fields
|
189
|
+
kwargs.setdefault("service_name", "agentops")
|
190
|
+
kwargs.setdefault("exporter_endpoint", "https://otlp.agentops.ai/v1/traces")
|
191
|
+
kwargs.setdefault("metrics_endpoint", "https://otlp.agentops.ai/v1/metrics")
|
192
|
+
kwargs.setdefault("max_queue_size", 512)
|
193
|
+
kwargs.setdefault("max_wait_time", 5000)
|
194
|
+
kwargs.setdefault("export_flush_interval", 1000)
|
195
|
+
|
196
|
+
# Create a TracingConfig from kwargs with proper defaults
|
197
|
+
config: TracingConfig = {
|
198
|
+
"service_name": kwargs["service_name"],
|
199
|
+
"exporter_endpoint": kwargs["exporter_endpoint"],
|
200
|
+
"metrics_endpoint": kwargs["metrics_endpoint"],
|
201
|
+
"max_queue_size": kwargs["max_queue_size"],
|
202
|
+
"max_wait_time": kwargs["max_wait_time"],
|
203
|
+
"export_flush_interval": kwargs["export_flush_interval"],
|
204
|
+
"api_key": kwargs.get("api_key"),
|
205
|
+
"project_id": kwargs.get("project_id"),
|
206
|
+
}
|
207
|
+
|
208
|
+
self._config = config
|
209
|
+
|
210
|
+
# Setup telemetry using the extracted configuration
|
211
|
+
provider, meter_provider = setup_telemetry(
|
212
|
+
service_name=config["service_name"] or "",
|
213
|
+
project_id=config.get("project_id"),
|
214
|
+
exporter_endpoint=config["exporter_endpoint"],
|
215
|
+
metrics_endpoint=config["metrics_endpoint"],
|
216
|
+
max_queue_size=config["max_queue_size"],
|
217
|
+
max_wait_time=config["max_wait_time"],
|
218
|
+
export_flush_interval=config["export_flush_interval"],
|
219
|
+
jwt=jwt,
|
220
|
+
)
|
221
|
+
|
222
|
+
self.provider = provider
|
223
|
+
self._meter_provider = meter_provider
|
224
|
+
|
225
|
+
self._initialized = True
|
226
|
+
logger.debug("Tracing core initialized")
|
227
|
+
|
228
|
+
@property
|
229
|
+
def initialized(self) -> bool:
|
230
|
+
"""Check if the tracing core is initialized."""
|
231
|
+
return self._initialized
|
232
|
+
|
233
|
+
@property
|
234
|
+
def config(self) -> TracingConfig:
|
235
|
+
"""Get the tracing configuration."""
|
236
|
+
if self._config is None:
|
237
|
+
# This case should ideally not be reached if initialized properly
|
238
|
+
raise AgentOpsClientNotInitializedException("Tracer config accessed before initialization.")
|
239
|
+
return self._config
|
240
|
+
|
241
|
+
def shutdown(self) -> None:
|
242
|
+
"""Shutdown the tracing core."""
|
243
|
+
|
244
|
+
if not self._initialized or not self.provider:
|
245
|
+
return
|
246
|
+
|
247
|
+
logger.debug("Attempting to flush span processors during shutdown...")
|
248
|
+
self._flush_span_processors()
|
249
|
+
|
250
|
+
# Shutdown provider
|
251
|
+
try:
|
252
|
+
self.provider.shutdown()
|
253
|
+
except Exception as e:
|
254
|
+
logger.warning(f"Error shutting down provider: {e}")
|
255
|
+
|
256
|
+
# Shutdown meter_provider
|
257
|
+
if hasattr(self, "_meter_provider") and self._meter_provider:
|
258
|
+
try:
|
259
|
+
self._meter_provider.shutdown()
|
260
|
+
except Exception as e:
|
261
|
+
logger.warning(f"Error shutting down meter provider: {e}")
|
262
|
+
|
263
|
+
self._initialized = False
|
264
|
+
logger.debug("Tracing core shut down")
|
265
|
+
|
266
|
+
def _flush_span_processors(self) -> None:
|
267
|
+
"""Helper to force flush all span processors."""
|
268
|
+
if not self.provider or not hasattr(self.provider, "force_flush"):
|
269
|
+
logger.debug("No provider or provider cannot force_flush.")
|
270
|
+
return
|
271
|
+
|
272
|
+
try:
|
273
|
+
self.provider.force_flush() # type: ignore
|
274
|
+
logger.debug("Provider force_flush completed.")
|
275
|
+
except Exception as e:
|
276
|
+
logger.warning(f"Failed to force flush provider's span processors: {e}", exc_info=True)
|
277
|
+
|
278
|
+
def get_tracer(self, name: str = "agentops") -> trace.Tracer:
|
279
|
+
"""
|
280
|
+
Get a tracer with the given name.
|
281
|
+
|
282
|
+
Args:
|
283
|
+
name: Name of the tracer
|
284
|
+
|
285
|
+
Returns:
|
286
|
+
A tracer with the given name
|
287
|
+
"""
|
288
|
+
if not self._initialized:
|
289
|
+
raise AgentOpsClientNotInitializedException
|
290
|
+
|
291
|
+
return trace.get_tracer(name)
|
292
|
+
|
293
|
+
@classmethod
|
294
|
+
def initialize_from_config(cls, config_obj: Any, **kwargs: Any) -> None:
|
295
|
+
"""
|
296
|
+
Initialize the tracing core from a configuration object.
|
297
|
+
|
298
|
+
Args:
|
299
|
+
config: Configuration object (dict or object with dict method)
|
300
|
+
**kwargs: Additional keyword arguments to pass to initialize
|
301
|
+
"""
|
302
|
+
# Use the global tracer instance instead of getting singleton
|
303
|
+
instance = tracer
|
304
|
+
|
305
|
+
# Extract tracing-specific configuration
|
306
|
+
# For TracingConfig, we can directly pass it to initialize
|
307
|
+
if isinstance(config_obj, dict):
|
308
|
+
# If it's already a dict (TracingConfig), use it directly
|
309
|
+
tracing_kwargs = config_obj.copy()
|
310
|
+
else:
|
311
|
+
# For backward compatibility with old Config object
|
312
|
+
# Extract tracing-specific configuration from the Config object
|
313
|
+
# Use getattr with default values to ensure we don't pass None for required fields
|
314
|
+
tracing_kwargs = {
|
315
|
+
k: v
|
316
|
+
for k, v in {
|
317
|
+
"exporter": getattr(config_obj, "exporter", None),
|
318
|
+
"processor": getattr(config_obj, "processor", None),
|
319
|
+
"exporter_endpoint": getattr(config_obj, "exporter_endpoint", None),
|
320
|
+
"max_queue_size": getattr(config_obj, "max_queue_size", 512),
|
321
|
+
"max_wait_time": getattr(config_obj, "max_wait_time", 5000),
|
322
|
+
"export_flush_interval": getattr(config_obj, "export_flush_interval", 1000),
|
323
|
+
"api_key": getattr(config_obj, "api_key", None),
|
324
|
+
"project_id": getattr(config_obj, "project_id", None),
|
325
|
+
"endpoint": getattr(config_obj, "endpoint", None),
|
326
|
+
}.items()
|
327
|
+
if v is not None
|
328
|
+
}
|
329
|
+
# Update with any additional kwargs
|
330
|
+
tracing_kwargs.update(kwargs)
|
331
|
+
|
332
|
+
# Initialize with the extracted configuration
|
333
|
+
instance.initialize(**tracing_kwargs)
|
334
|
+
|
335
|
+
# Span types are registered in the constructor
|
336
|
+
# No need to register them here anymore
|
337
|
+
|
338
|
+
def start_trace(
|
339
|
+
self, trace_name: str = "session", tags: Optional[dict | list] = None, is_init_trace: bool = False
|
340
|
+
) -> Optional[TraceContext]:
|
341
|
+
"""
|
342
|
+
Starts a new trace (root span) and returns its context.
|
343
|
+
|
344
|
+
Args:
|
345
|
+
trace_name: Name for the trace (e.g., "session", "my_custom_trace").
|
346
|
+
tags: Optional tags to attach to the trace span.
|
347
|
+
is_init_trace: Internal flag to mark if this is the automatically started init trace.
|
348
|
+
|
349
|
+
Returns:
|
350
|
+
A TraceContext object containing the span and context token, or None if not initialized.
|
351
|
+
"""
|
352
|
+
if not self.initialized:
|
353
|
+
logger.warning("Global tracer not initialized. Cannot start trace.")
|
354
|
+
return None
|
355
|
+
|
356
|
+
# Build trace attributes
|
357
|
+
attributes = get_trace_attributes(tags=tags)
|
358
|
+
# Include system metadata only for the default session trace
|
359
|
+
if trace_name == "session":
|
360
|
+
attributes.update(get_system_resource_attributes())
|
361
|
+
|
362
|
+
# make_span creates and starts the span, and activates it in the current context
|
363
|
+
# It returns: span, context_object, context_token
|
364
|
+
span, _, context_token = self.make_span(trace_name, span_kind=SpanKind.SESSION, attributes=attributes)
|
365
|
+
logger.debug(f"Trace '{trace_name}' started with span ID: {span.get_span_context().span_id}")
|
366
|
+
|
367
|
+
# Log the session replay URL for this new trace
|
368
|
+
try:
|
369
|
+
log_trace_url(span, title=trace_name)
|
370
|
+
except Exception as e:
|
371
|
+
logger.warning(f"Failed to log trace URL for '{trace_name}': {e}")
|
372
|
+
|
373
|
+
trace_context = TraceContext(span, token=context_token, is_init_trace=is_init_trace)
|
374
|
+
|
375
|
+
# Track the active trace
|
376
|
+
with self._traces_lock:
|
377
|
+
try:
|
378
|
+
trace_id = f"{span.get_span_context().trace_id:x}"
|
379
|
+
except (TypeError, ValueError):
|
380
|
+
# Handle case where span is mocked or trace_id is not a valid integer
|
381
|
+
trace_id = str(span.get_span_context().trace_id)
|
382
|
+
self._active_traces[trace_id] = trace_context
|
383
|
+
logger.debug(f"Added trace {trace_id} to active traces. Total active: {len(self._active_traces)}")
|
384
|
+
|
385
|
+
return trace_context
|
386
|
+
|
387
|
+
def end_trace(
|
388
|
+
self, trace_context: Optional[TraceContext] = None, end_state: Union[Any, StatusCode, str] = None
|
389
|
+
) -> None:
|
390
|
+
"""
|
391
|
+
Ends a trace (its root span) and finalizes it.
|
392
|
+
If no trace_context is provided, ends all active session spans.
|
393
|
+
|
394
|
+
Args:
|
395
|
+
trace_context: The TraceContext object returned by start_trace. If None, ends all active traces.
|
396
|
+
end_state: The final state of the trace (e.g., "Success", "Indeterminate", "Error").
|
397
|
+
"""
|
398
|
+
if not self.initialized:
|
399
|
+
logger.warning("Global tracer not initialized. Cannot end trace.")
|
400
|
+
return
|
401
|
+
|
402
|
+
# Set default if not provided
|
403
|
+
if end_state is None:
|
404
|
+
from agentops.enums import TraceState
|
405
|
+
|
406
|
+
end_state = TraceState.SUCCESS
|
407
|
+
|
408
|
+
# If no specific trace_context provided, end all active traces
|
409
|
+
if trace_context is None:
|
410
|
+
with self._traces_lock:
|
411
|
+
active_traces = list(self._active_traces.values())
|
412
|
+
logger.debug(f"Ending all {len(active_traces)} active traces with state: {end_state}")
|
413
|
+
|
414
|
+
for active_trace in active_traces:
|
415
|
+
self._end_single_trace(active_trace, end_state)
|
416
|
+
return
|
417
|
+
|
418
|
+
# End specific trace
|
419
|
+
self._end_single_trace(trace_context, end_state)
|
420
|
+
|
421
|
+
def _end_single_trace(self, trace_context: TraceContext, end_state: Union[Any, StatusCode, str]) -> None:
|
422
|
+
"""
|
423
|
+
Internal method to end a single trace.
|
424
|
+
|
425
|
+
Args:
|
426
|
+
trace_context: The TraceContext object to end.
|
427
|
+
end_state: The final state of the trace.
|
428
|
+
"""
|
429
|
+
if not trace_context or not trace_context.span:
|
430
|
+
logger.warning("Invalid TraceContext or span provided to end trace.")
|
431
|
+
return
|
432
|
+
|
433
|
+
span = trace_context.span
|
434
|
+
token = trace_context.token
|
435
|
+
try:
|
436
|
+
trace_id = f"{span.get_span_context().trace_id:x}"
|
437
|
+
except (TypeError, ValueError):
|
438
|
+
# Handle case where span is mocked or trace_id is not a valid integer
|
439
|
+
trace_id = str(span.get_span_context().trace_id)
|
440
|
+
|
441
|
+
# Convert TraceState enum to StatusCode if needed
|
442
|
+
from agentops.enums import TraceState
|
443
|
+
|
444
|
+
if isinstance(end_state, TraceState):
|
445
|
+
# It's a TraceState enum
|
446
|
+
state_str = str(end_state)
|
447
|
+
elif isinstance(end_state, StatusCode):
|
448
|
+
# It's already a StatusCode
|
449
|
+
state_str = str(end_state)
|
450
|
+
else:
|
451
|
+
# It's a string (legacy)
|
452
|
+
state_str = str(end_state)
|
453
|
+
|
454
|
+
logger.debug(f"Ending trace with span ID: {span.get_span_context().span_id}, end_state: {state_str}")
|
455
|
+
|
456
|
+
try:
|
457
|
+
# Build and set session end attributes
|
458
|
+
end_attributes = get_session_end_attributes(end_state)
|
459
|
+
for key, value in end_attributes.items():
|
460
|
+
span.set_attribute(key, value)
|
461
|
+
self.finalize_span(span, token=token)
|
462
|
+
|
463
|
+
# Remove from active traces
|
464
|
+
with self._traces_lock:
|
465
|
+
if trace_id in self._active_traces:
|
466
|
+
del self._active_traces[trace_id]
|
467
|
+
logger.debug(f"Removed trace {trace_id} from active traces. Remaining: {len(self._active_traces)}")
|
468
|
+
|
469
|
+
# For root spans (traces), we might want an immediate flush after they end.
|
470
|
+
self._flush_span_processors()
|
471
|
+
|
472
|
+
# Log the session replay URL again after the trace has ended
|
473
|
+
# The span object should still contain the necessary context (trace_id)
|
474
|
+
try:
|
475
|
+
# Use span.name as the title, which should reflect the original trace_name
|
476
|
+
log_trace_url(span, title=span.name)
|
477
|
+
except Exception as e:
|
478
|
+
logger.warning(f"Failed to log trace URL after ending trace '{span.name}': {e}")
|
479
|
+
|
480
|
+
except Exception as e:
|
481
|
+
logger.error(f"Error ending trace: {e}", exc_info=True)
|
482
|
+
|
483
|
+
def make_span(
|
484
|
+
self,
|
485
|
+
operation_name: str,
|
486
|
+
span_kind: str,
|
487
|
+
version: Optional[int] = None,
|
488
|
+
attributes: Optional[Dict[str, Any]] = None,
|
489
|
+
) -> tuple:
|
490
|
+
"""
|
491
|
+
Create a span without context management for manual span lifecycle control.
|
492
|
+
|
493
|
+
This function creates a span that will be properly nested within any parent span
|
494
|
+
based on the current execution context, but requires manual ending via finalize_span.
|
495
|
+
|
496
|
+
Args:
|
497
|
+
operation_name: Name of the operation being traced
|
498
|
+
span_kind: Type of operation (from SpanKind)
|
499
|
+
version: Optional version identifier for the operation
|
500
|
+
attributes: Optional dictionary of attributes to set on the span
|
501
|
+
|
502
|
+
Returns:
|
503
|
+
A tuple of (span, context, token) where:
|
504
|
+
- span is the created span
|
505
|
+
- context is the span context
|
506
|
+
- token is the context token needed for detaching
|
507
|
+
"""
|
508
|
+
# Create span with proper naming convention
|
509
|
+
span_name = f"{operation_name}.{span_kind}"
|
510
|
+
|
511
|
+
# Get tracer
|
512
|
+
tracer = self.get_tracer()
|
513
|
+
|
514
|
+
# Build span attributes using the attribute helper
|
515
|
+
attributes = get_span_attributes(
|
516
|
+
operation_name=operation_name,
|
517
|
+
span_kind=span_kind,
|
518
|
+
version=version,
|
519
|
+
**(attributes or {}),
|
520
|
+
)
|
521
|
+
|
522
|
+
current_context = context_api.get_current()
|
523
|
+
|
524
|
+
# Create the span with proper context management
|
525
|
+
if span_kind == SpanKind.SESSION:
|
526
|
+
# For session spans, create as a root span
|
527
|
+
span = tracer.start_span(span_name, attributes=attributes)
|
528
|
+
else:
|
529
|
+
# For other spans, use the current context
|
530
|
+
span = tracer.start_span(span_name, context=current_context, attributes=attributes)
|
531
|
+
|
532
|
+
# Set as current context and get token for detachment
|
533
|
+
ctx = trace.set_span_in_context(span)
|
534
|
+
token = context_api.attach(ctx)
|
535
|
+
|
536
|
+
return span, ctx, token
|
537
|
+
|
538
|
+
def finalize_span(self, span: trace.Span, token: Any) -> None:
|
539
|
+
"""
|
540
|
+
Finalizes a span and cleans up its context.
|
541
|
+
|
542
|
+
This function performs three critical tasks needed for proper span lifecycle management:
|
543
|
+
1. Ends the span to mark it complete and calculate its duration
|
544
|
+
2. Detaches the context token to prevent memory leaks and maintain proper context hierarchy
|
545
|
+
3. Forces immediate span export rather than waiting for batch processing
|
546
|
+
|
547
|
+
Use cases:
|
548
|
+
- Session span termination: Ensures root spans are properly ended and exported
|
549
|
+
- Shutdown handling: Ensures spans are flushed during application termination
|
550
|
+
- Async operations: Finalizes spans from asynchronous execution contexts
|
551
|
+
|
552
|
+
Without proper finalization, spans may not trigger on_end events in processors,
|
553
|
+
potentially resulting in missing or incomplete telemetry data.
|
554
|
+
|
555
|
+
Args:
|
556
|
+
span: The span to finalize
|
557
|
+
token: The context token to detach
|
558
|
+
"""
|
559
|
+
# End the span
|
560
|
+
if span:
|
561
|
+
try:
|
562
|
+
span.end()
|
563
|
+
except Exception as e:
|
564
|
+
logger.warning(f"Error ending span: {e}")
|
565
|
+
|
566
|
+
# Detach context token if provided
|
567
|
+
if token:
|
568
|
+
try:
|
569
|
+
context_api.detach(token)
|
570
|
+
except Exception:
|
571
|
+
pass
|
572
|
+
|
573
|
+
# Try to flush span processors
|
574
|
+
# Note: force_flush() might not be available in certain scenarios:
|
575
|
+
# - During application shutdown when the provider may be partially destroyed
|
576
|
+
# We use try/except to gracefully handle these cases while ensuring spans are
|
577
|
+
# flushed when possible, which is especially critical for session spans.
|
578
|
+
try:
|
579
|
+
if self.provider:
|
580
|
+
self.provider.force_flush()
|
581
|
+
except (AttributeError, Exception):
|
582
|
+
# Either force_flush doesn't exist or there was an error calling it
|
583
|
+
pass
|
584
|
+
|
585
|
+
def get_active_traces(self) -> Dict[str, TraceContext]:
|
586
|
+
"""
|
587
|
+
Get a copy of currently active traces.
|
588
|
+
|
589
|
+
Returns:
|
590
|
+
Dictionary mapping trace IDs to TraceContext objects.
|
591
|
+
"""
|
592
|
+
with self._traces_lock:
|
593
|
+
return self._active_traces.copy()
|
594
|
+
|
595
|
+
def get_active_trace_count(self) -> int:
|
596
|
+
"""
|
597
|
+
Get the number of currently active traces.
|
598
|
+
|
599
|
+
Returns:
|
600
|
+
Number of active traces.
|
601
|
+
"""
|
602
|
+
with self._traces_lock:
|
603
|
+
return len(self._active_traces)
|
604
|
+
|
605
|
+
|
606
|
+
# Global tracer instance; one per process runtime
|
607
|
+
tracer = TracingCore()
|
@@ -0,0 +1,51 @@
|
|
1
|
+
"""
|
2
|
+
Decorators for instrumenting code with AgentOps.
|
3
|
+
Provides @trace for creating trace-level spans (sessions) and other decorators for nested spans.
|
4
|
+
"""
|
5
|
+
|
6
|
+
from agentops.helpers.deprecation import deprecated
|
7
|
+
from agentops.sdk.decorators.factory import create_entity_decorator
|
8
|
+
from agentops.semconv.span_kinds import SpanKind
|
9
|
+
|
10
|
+
# Create decorators for specific entity types using the factory
|
11
|
+
agent = create_entity_decorator(SpanKind.AGENT)
|
12
|
+
task = create_entity_decorator(SpanKind.TASK)
|
13
|
+
operation_decorator = create_entity_decorator(SpanKind.OPERATION)
|
14
|
+
workflow = create_entity_decorator(SpanKind.WORKFLOW)
|
15
|
+
trace = create_entity_decorator(SpanKind.SESSION)
|
16
|
+
tool = create_entity_decorator(SpanKind.TOOL)
|
17
|
+
operation = task
|
18
|
+
guardrail = create_entity_decorator(SpanKind.GUARDRAIL)
|
19
|
+
track_endpoint = create_entity_decorator(SpanKind.HTTP)
|
20
|
+
|
21
|
+
|
22
|
+
# For backward compatibility: @session decorator calls @trace decorator
|
23
|
+
def session(*args, **kwargs): # noqa: F811
|
24
|
+
"""@deprecated Use @agentops.trace instead. Wraps the @trace decorator for backward compatibility."""
|
25
|
+
# If called as @session or @session(...)
|
26
|
+
if not args or not callable(args[0]): # called with kwargs like @session(name=...)
|
27
|
+
return trace(*args, **kwargs)
|
28
|
+
else: # called as @session directly on a function
|
29
|
+
return trace(args[0], **kwargs) # args[0] is the wrapped function
|
30
|
+
|
31
|
+
|
32
|
+
# Apply deprecation decorator to session function
|
33
|
+
session = deprecated("Use @trace decorator instead.")(session)
|
34
|
+
|
35
|
+
|
36
|
+
# Note: The original `operation = task` was potentially problematic if `operation` was meant to be distinct.
|
37
|
+
# Using operation_decorator for clarity if a distinct OPERATION kind decorator is needed.
|
38
|
+
# For now, keeping the alias as it was, assuming it was intentional for `operation` to be `task`.
|
39
|
+
operation = task
|
40
|
+
|
41
|
+
__all__ = [
|
42
|
+
"agent",
|
43
|
+
"task",
|
44
|
+
"workflow",
|
45
|
+
"trace",
|
46
|
+
"session",
|
47
|
+
"operation",
|
48
|
+
"tool",
|
49
|
+
"guardrail",
|
50
|
+
"track_endpoint",
|
51
|
+
]
|