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.
Files changed (94) hide show
  1. agentops/__init__.py +488 -0
  2. agentops/client/__init__.py +5 -0
  3. agentops/client/api/__init__.py +71 -0
  4. agentops/client/api/base.py +162 -0
  5. agentops/client/api/types.py +21 -0
  6. agentops/client/api/versions/__init__.py +10 -0
  7. agentops/client/api/versions/v3.py +65 -0
  8. agentops/client/api/versions/v4.py +104 -0
  9. agentops/client/client.py +211 -0
  10. agentops/client/http/__init__.py +0 -0
  11. agentops/client/http/http_adapter.py +116 -0
  12. agentops/client/http/http_client.py +215 -0
  13. agentops/config.py +268 -0
  14. agentops/enums.py +36 -0
  15. agentops/exceptions.py +38 -0
  16. agentops/helpers/__init__.py +44 -0
  17. agentops/helpers/dashboard.py +54 -0
  18. agentops/helpers/deprecation.py +50 -0
  19. agentops/helpers/env.py +52 -0
  20. agentops/helpers/serialization.py +137 -0
  21. agentops/helpers/system.py +178 -0
  22. agentops/helpers/time.py +11 -0
  23. agentops/helpers/version.py +36 -0
  24. agentops/instrumentation/__init__.py +598 -0
  25. agentops/instrumentation/common/__init__.py +82 -0
  26. agentops/instrumentation/common/attributes.py +278 -0
  27. agentops/instrumentation/common/instrumentor.py +147 -0
  28. agentops/instrumentation/common/metrics.py +100 -0
  29. agentops/instrumentation/common/objects.py +26 -0
  30. agentops/instrumentation/common/span_management.py +176 -0
  31. agentops/instrumentation/common/streaming.py +218 -0
  32. agentops/instrumentation/common/token_counting.py +177 -0
  33. agentops/instrumentation/common/version.py +71 -0
  34. agentops/instrumentation/common/wrappers.py +235 -0
  35. agentops/legacy/__init__.py +277 -0
  36. agentops/legacy/event.py +156 -0
  37. agentops/logging/__init__.py +4 -0
  38. agentops/logging/config.py +86 -0
  39. agentops/logging/formatters.py +34 -0
  40. agentops/logging/instrument_logging.py +91 -0
  41. agentops/sdk/__init__.py +27 -0
  42. agentops/sdk/attributes.py +151 -0
  43. agentops/sdk/core.py +607 -0
  44. agentops/sdk/decorators/__init__.py +51 -0
  45. agentops/sdk/decorators/factory.py +486 -0
  46. agentops/sdk/decorators/utility.py +216 -0
  47. agentops/sdk/exporters.py +87 -0
  48. agentops/sdk/processors.py +71 -0
  49. agentops/sdk/types.py +21 -0
  50. agentops/semconv/__init__.py +36 -0
  51. agentops/semconv/agent.py +29 -0
  52. agentops/semconv/core.py +19 -0
  53. agentops/semconv/enum.py +11 -0
  54. agentops/semconv/instrumentation.py +13 -0
  55. agentops/semconv/langchain.py +63 -0
  56. agentops/semconv/message.py +61 -0
  57. agentops/semconv/meters.py +24 -0
  58. agentops/semconv/resource.py +52 -0
  59. agentops/semconv/span_attributes.py +118 -0
  60. agentops/semconv/span_kinds.py +50 -0
  61. agentops/semconv/status.py +11 -0
  62. agentops/semconv/tool.py +15 -0
  63. agentops/semconv/workflow.py +69 -0
  64. agentops/validation.py +357 -0
  65. mseep_agentops-0.4.18.dist-info/METADATA +49 -0
  66. mseep_agentops-0.4.18.dist-info/RECORD +94 -0
  67. mseep_agentops-0.4.18.dist-info/WHEEL +5 -0
  68. mseep_agentops-0.4.18.dist-info/licenses/LICENSE +21 -0
  69. mseep_agentops-0.4.18.dist-info/top_level.txt +2 -0
  70. tests/__init__.py +0 -0
  71. tests/conftest.py +10 -0
  72. tests/unit/__init__.py +0 -0
  73. tests/unit/client/__init__.py +1 -0
  74. tests/unit/client/test_http_adapter.py +221 -0
  75. tests/unit/client/test_http_client.py +206 -0
  76. tests/unit/conftest.py +54 -0
  77. tests/unit/sdk/__init__.py +1 -0
  78. tests/unit/sdk/instrumentation_tester.py +207 -0
  79. tests/unit/sdk/test_attributes.py +392 -0
  80. tests/unit/sdk/test_concurrent_instrumentation.py +468 -0
  81. tests/unit/sdk/test_decorators.py +763 -0
  82. tests/unit/sdk/test_exporters.py +241 -0
  83. tests/unit/sdk/test_factory.py +1188 -0
  84. tests/unit/sdk/test_internal_span_processor.py +397 -0
  85. tests/unit/sdk/test_resource_attributes.py +35 -0
  86. tests/unit/test_config.py +82 -0
  87. tests/unit/test_context_manager.py +777 -0
  88. tests/unit/test_events.py +27 -0
  89. tests/unit/test_host_env.py +54 -0
  90. tests/unit/test_init_py.py +501 -0
  91. tests/unit/test_serialization.py +433 -0
  92. tests/unit/test_session.py +676 -0
  93. tests/unit/test_user_agent.py +34 -0
  94. 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
+ ]