Skip to content

Commit aa3f549

Browse files
dot-agiCopilot
andauthored
feat: Context management for using with that manages trace(s) automatically (#1019)
* add context management to use `with` with `start_trace` * set to `Success` when no error encountered * add examples * add tests for context management * add docs * add `TraceState` for handling status codes and handle thread management for client * modify to use updated status codes * heckin' ruff * gh copilot said do it so i did * global tracer everywher * should fix linting * Convert end_state to string using helper function Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Revert "Convert end_state to string using helper function" --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 3f71e28 commit aa3f549

File tree

21 files changed

+2427
-68
lines changed

21 files changed

+2427
-68
lines changed

agentops/__init__.py

Lines changed: 51 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,28 @@
1515
from typing import List, Optional, Union, Dict, Any
1616
from agentops.client import Client
1717
from agentops.sdk.core import TraceContext, tracer
18-
from agentops.sdk.decorators import trace, session, agent, task, workflow, operation
18+
from agentops.sdk.decorators import trace, session, agent, task, workflow, operation, tool
19+
from agentops.enums import TraceState, SUCCESS, ERROR, UNSET
20+
from opentelemetry.trace.status import StatusCode
1921

2022
from agentops.logging.config import logger
23+
import threading
2124

22-
# Client global instance; one per process runtime
23-
_client = Client()
25+
# Thread-safe client management
26+
_client_lock = threading.Lock()
27+
_client = None
2428

2529

2630
def get_client() -> Client:
27-
"""Get the singleton client instance"""
31+
"""Get the singleton client instance in a thread-safe manner"""
2832
global _client
2933

34+
# Double-checked locking pattern for thread safety
35+
if _client is None:
36+
with _client_lock:
37+
if _client is None:
38+
_client = Client()
39+
3040
return _client
3141

3242

@@ -106,24 +116,31 @@ def init(
106116
elif default_tags:
107117
merged_tags = default_tags
108118

109-
return _client.init(
110-
api_key=api_key,
111-
endpoint=endpoint,
112-
app_url=app_url,
113-
max_wait_time=max_wait_time,
114-
max_queue_size=max_queue_size,
115-
default_tags=merged_tags,
116-
trace_name=trace_name,
117-
instrument_llm_calls=instrument_llm_calls,
118-
auto_start_session=auto_start_session,
119-
auto_init=auto_init,
120-
skip_auto_end_session=skip_auto_end_session,
121-
env_data_opt_out=env_data_opt_out,
122-
log_level=log_level,
123-
fail_safe=fail_safe,
124-
exporter_endpoint=exporter_endpoint,
119+
# Prepare initialization arguments
120+
init_kwargs = {
121+
"api_key": api_key,
122+
"endpoint": endpoint,
123+
"app_url": app_url,
124+
"max_wait_time": max_wait_time,
125+
"max_queue_size": max_queue_size,
126+
"default_tags": merged_tags,
127+
"trace_name": trace_name,
128+
"instrument_llm_calls": instrument_llm_calls,
129+
"auto_start_session": auto_start_session,
130+
"auto_init": auto_init,
131+
"skip_auto_end_session": skip_auto_end_session,
132+
"env_data_opt_out": env_data_opt_out,
133+
"log_level": log_level,
134+
"fail_safe": fail_safe,
135+
"exporter_endpoint": exporter_endpoint,
125136
**kwargs,
126-
)
137+
}
138+
139+
# Get the current client instance (creates new one if needed)
140+
client = get_client()
141+
142+
# Initialize the client directly
143+
return client.init(**init_kwargs)
127144

128145

129146
def configure(**kwargs):
@@ -173,7 +190,8 @@ def configure(**kwargs):
173190
if invalid_params:
174191
logger.warning(f"Invalid configuration parameters: {invalid_params}")
175192

176-
_client.configure(**kwargs)
193+
client = get_client()
194+
client.configure(**kwargs)
177195

178196

179197
def start_trace(
@@ -207,7 +225,9 @@ def start_trace(
207225
return tracer.start_trace(trace_name=trace_name, tags=tags)
208226

209227

210-
def end_trace(trace_context: Optional[TraceContext] = None, end_state: str = "Success") -> None:
228+
def end_trace(
229+
trace_context: Optional[TraceContext] = None, end_state: Union[TraceState, StatusCode, str] = TraceState.SUCCESS
230+
) -> None:
211231
"""
212232
Ends a trace (its root span) and finalizes it.
213233
If no trace_context is provided, ends all active session spans.
@@ -246,4 +266,12 @@ def end_trace(trace_context: Optional[TraceContext] = None, end_state: str = "Su
246266
"workflow",
247267
"operation",
248268
"tracer",
269+
"tool",
270+
# Trace state enums
271+
"TraceState",
272+
"SUCCESS",
273+
"ERROR",
274+
"UNSET",
275+
# OpenTelemetry status codes (for advanced users)
276+
"StatusCode",
249277
]

agentops/client/client.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ def _end_init_trace_atexit():
2424
if _client_init_trace_context is not None:
2525
logger.debug("Auto-ending client's init trace during shutdown.")
2626
try:
27-
# Use TracingCore to end the trace directly
27+
# Use global tracer to end the trace directly
2828
if tracer.initialized and _client_init_trace_context.span.is_recording():
2929
tracer.end_trace(_client_init_trace_context, end_state="Shutdown")
3030
except Exception as e:
@@ -104,7 +104,7 @@ def init(self, **kwargs: Any) -> None: # Return type updated to None
104104
try:
105105
response = self.api.v3.fetch_auth_token(self.config.api_key)
106106
if response is None:
107-
# If auth fails, we cannot proceed with TracingCore initialization that depends on project_id
107+
# If auth fails, we cannot proceed with tracer initialization that depends on project_id
108108
logger.error("Failed to fetch auth token. AgentOps SDK will not be initialized.")
109109
return None # Explicitly return None if auth fails
110110
except Exception as e:
@@ -161,8 +161,8 @@ def init(self, **kwargs: Any) -> None: # Return type updated to None
161161

162162
else:
163163
logger.error("Failed to start the auto-init trace.")
164-
# Even if auto-start fails, core services up to TracingCore might be initialized.
165-
# Set self.initialized to True if TracingCore is up, but return None.
164+
# Even if auto-start fails, core services up to the tracer might be initialized.
165+
# Set self.initialized to True if tracer is up, but return None.
166166
self._initialized = tracer.initialized
167167
return None # Failed to start trace
168168

agentops/enums.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
"""
2+
AgentOps enums for user-friendly API.
3+
4+
This module provides simple enums that users can import from agentops
5+
without needing to know about OpenTelemetry internals.
6+
"""
7+
8+
from enum import Enum
9+
from opentelemetry.trace.status import StatusCode
10+
11+
12+
class TraceState(Enum):
13+
"""
14+
Enum for trace end states.
15+
16+
This provides a user-friendly interface that maps to OpenTelemetry StatusCode internally.
17+
Users can simply use agentops.TraceState.SUCCESS instead of importing OpenTelemetry.
18+
"""
19+
20+
SUCCESS = StatusCode.OK
21+
ERROR = StatusCode.ERROR
22+
UNSET = StatusCode.UNSET
23+
24+
def __str__(self) -> str:
25+
"""Return the name for string representation."""
26+
return self.name
27+
28+
def to_status_code(self) -> StatusCode:
29+
"""Convert to OpenTelemetry StatusCode."""
30+
return self.value
31+
32+
33+
# For backward compatibility, also provide these as module-level constants
34+
SUCCESS = TraceState.SUCCESS
35+
ERROR = TraceState.ERROR
36+
UNSET = TraceState.UNSET

agentops/instrumentation/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -444,7 +444,7 @@ def instrument_one(loader: InstrumentorLoader) -> Optional[BaseInstrumentor]:
444444

445445
instrumentor = loader.get_instance()
446446
try:
447-
instrumentor.instrument(tracer_provider=TracingCore.get_instance()._provider)
447+
instrumentor.instrument(tracer_provider=tracer._provider)
448448
logger.info(
449449
f"AgentOps: Successfully instrumented '{loader.class_name}' for package '{loader.package_name or loader.module_name}'."
450450
)

agentops/legacy/__init__.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ def start_session(
6565
) -> Session:
6666
"""
6767
@deprecated Use agentops.start_trace() instead.
68-
Starts a legacy AgentOps session. Calls TracingCore.start_trace internally.
68+
Starts a legacy AgentOps session. Calls tracer.start_trace internally.
6969
"""
7070
global _current_session, _current_trace_context
7171

@@ -89,7 +89,7 @@ def start_session(
8989

9090
trace_context = tracer.start_trace(trace_name="session", tags=tags)
9191
if trace_context is None:
92-
logger.error("Failed to start trace via TracingCore. Returning dummy session.")
92+
logger.error("Failed to start trace via global tracer. Returning dummy session.")
9393
dummy_session = Session(None)
9494
_current_session = dummy_session
9595
_current_trace_context = None
@@ -124,13 +124,13 @@ def _set_span_attributes(span: Any, attributes: Dict[str, Any]) -> None:
124124
def end_session(session_or_status: Any = None, **kwargs: Any) -> None:
125125
"""
126126
@deprecated Use agentops.end_trace() instead.
127-
Ends a legacy AgentOps session. Calls TracingCore.end_trace internally.
127+
Ends a legacy AgentOps session. Calls tracer.end_trace internally.
128128
Supports multiple calling patterns for backward compatibility.
129129
"""
130130
global _current_session, _current_trace_context
131131

132132
if not tracer.initialized:
133-
logger.debug("Ignoring end_session: TracingCore not initialized.")
133+
logger.debug("Ignoring end_session: global tracer not initialized.")
134134
return
135135

136136
target_trace_context: Optional[TraceContext] = None
@@ -189,7 +189,7 @@ def end_session(session_or_status: Any = None, **kwargs: Any) -> None:
189189
def end_all_sessions() -> None:
190190
"""@deprecated Ends all active sessions/traces."""
191191
if not tracer.initialized:
192-
logger.debug("Ignoring end_all_sessions: TracingCore not initialized.")
192+
logger.debug("Ignoring end_all_sessions: global tracer not initialized.")
193193
return
194194

195195
# Use the new end_trace functionality to end all active traces

agentops/sdk/README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -176,11 +176,11 @@ flowchart TD
176176
## Example Usage
177177

178178
```python
179-
from agentops import Session, agent, tool
180-
from agentops.sdk import TracingCore, TracingConfig
179+
from agentops import Session, agent, tool, tracer
180+
from agentops.sdk import TracingConfig
181181

182-
# Initialize the tracing core with a dedicated configuration
183-
TracingCore.get_instance().initialize(
182+
# Initialize the global tracer with a dedicated configuration
183+
tracer.initialize(
184184
service_name="my-service",
185185
exporter_endpoint="https://my-exporter-endpoint.com",
186186
max_queue_size=1000,

agentops/sdk/__init__.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,23 @@
55
for different types of operations in AI agent workflows.
66
"""
77

8-
# Import core components
9-
from agentops.sdk.core import TracingCore
10-
118
# Import decorators
129
from agentops.sdk.decorators import agent, operation, session, task, workflow
1310

1411
# from agentops.sdk.traced import TracedObject # Merged into TracedObject
1512
from agentops.sdk.types import TracingConfig
1613

14+
from opentelemetry.trace.status import StatusCode
15+
1716
__all__ = [
1817
# Core components
19-
"TracingCore",
2018
"TracingConfig",
2119
# Decorators
2220
"session",
2321
"operation",
2422
"agent",
2523
"task",
2624
"workflow",
25+
# OpenTelemetry status codes
26+
"StatusCode",
2727
]

0 commit comments

Comments
 (0)