Skip to content

Commit efacc7c

Browse files
committed
Enhance tracing functionality by introducing new decorators and improving session handling. Added trace, session, agent, task, workflow, and operation decorators for better instrumentation. Updated log_trace_url to include titles for improved logging context. Refactored Client initialization trace name and adjusted end trace state handling. Improved error handling during trace logging in TracingCore and removed deprecated session decorator usage.
1 parent 6fa9dd5 commit efacc7c

7 files changed

Lines changed: 155 additions & 21 deletions

File tree

agentops/__init__.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from typing import List, Optional, Union, Dict, Any
1616
from agentops.client import Client
1717
from agentops.sdk.core import TracingCore, TraceContext
18+
from agentops.sdk.decorators import trace, session, agent, task, workflow, operation
1819

1920
from agentops.logging.config import logger
2021

@@ -207,6 +208,12 @@ def end_trace(trace_context: TraceContext, end_state: str = "Success") -> None:
207208
"record",
208209
"start_trace",
209210
"end_trace",
211+
"trace",
212+
"session",
213+
"agent",
214+
"task",
215+
"workflow",
216+
"operation",
210217
"start_session",
211218
"end_session",
212219
"track_agent",

agentops/client/client.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ def _end_init_trace_atexit():
2727
# Use TracingCore to end the trace directly
2828
tracing_core = TracingCore.get_instance()
2929
if tracing_core.initialized and _client_init_trace_context.span.is_recording():
30-
tracing_core.end_trace(_client_init_trace_context, end_state="Aborted_AtExit")
30+
tracing_core.end_trace(_client_init_trace_context, end_state="Shutdown")
3131
except Exception as e:
3232
logger.warning(f"Error ending client's init trace during shutdown: {e}")
3333
finally:
@@ -129,7 +129,7 @@ def init(self, **kwargs) -> None: # Return type updated to None
129129
if self._init_trace_context is None or not self._init_trace_context.span.is_recording():
130130
logger.debug("Auto-starting init trace.")
131131
self._init_trace_context = tracing_core.start_trace(
132-
trace_name="agentops_init_trace",
132+
trace_name="default",
133133
tags=list(self.config.default_tags) if self.config.default_tags else None,
134134
is_init_trace=True,
135135
)

agentops/helpers/dashboard.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
Helpers for interacting with the AgentOps dashboard.
33
"""
44

5-
from typing import Union
5+
from typing import Union, Optional
66
from termcolor import colored
77
from opentelemetry.sdk.trace import Span, ReadableSpan
88
from agentops.logging import logger
@@ -33,12 +33,12 @@ def get_trace_url(span: Union[Span, ReadableSpan]) -> str:
3333
return f"{app_url}/sessions?trace_id={trace_id}"
3434

3535

36-
def log_trace_url(span: Union[Span, ReadableSpan]) -> None:
36+
def log_trace_url(span: Union[Span, ReadableSpan], title: Optional[str] = None) -> None:
3737
"""
3838
Log the trace URL for the AgentOps dashboard.
3939
4040
Args:
4141
span: The span to log the URL for.
4242
"""
4343
session_url = get_trace_url(span)
44-
logger.info(colored(f"\x1b[34mSession Replay: {session_url}\x1b[0m", "blue"))
44+
logger.info(colored(f"\x1b[34mSession Replay for {title} trace: {session_url}\x1b[0m", "blue"))

agentops/sdk/core.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from agentops.sdk.processors import InternalSpanProcessor
2424
from agentops.sdk.types import TracingConfig
2525
from agentops.semconv import ResourceAttributes, SpanKind, SpanAttributes
26+
from agentops.helpers.dashboard import log_trace_url
2627

2728
# No need to create shortcuts since we're using our own ResourceAttributes class now
2829

@@ -431,6 +432,13 @@ def start_trace(
431432
# It returns: span, context_object, context_token
432433
span, _, context_token = _make_span(trace_name, span_kind=SpanKind.SESSION, attributes=attributes)
433434
logger.debug(f"Trace '{trace_name}' started with span ID: {span.get_span_context().span_id}")
435+
436+
# Log the session replay URL for this new trace
437+
try:
438+
log_trace_url(span, title=trace_name)
439+
except Exception as e:
440+
logger.warning(f"Failed to log trace URL for '{trace_name}': {e}")
441+
434442
return TraceContext(span, token=context_token, is_init_trace=is_init_trace)
435443

436444
def end_trace(self, trace_context: TraceContext, end_state: str = "Success") -> None:
@@ -458,9 +466,17 @@ def end_trace(self, trace_context: TraceContext, end_state: str = "Success") ->
458466

459467
try:
460468
span.set_attribute(SpanAttributes.AGENTOPS_SESSION_END_STATE, end_state)
461-
# _finalize_span ends the span and detaches it from context if a token is provided
462469
_finalize_span(span, token=token)
463470
# For root spans (traces), we might want an immediate flush after they end.
464471
self._flush_span_processors()
472+
473+
# Log the session replay URL again after the trace has ended
474+
# The span object should still contain the necessary context (trace_id)
475+
try:
476+
# Use span.name as the title, which should reflect the original trace_name
477+
log_trace_url(span, title=span.name)
478+
except Exception as e:
479+
logger.warning(f"Failed to log trace URL after ending trace '{span.name}': {e}")
480+
465481
except Exception as e:
466482
logger.error(f"Error ending trace: {e}", exc_info=True)

agentops/sdk/decorators/__init__.py

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,38 @@
55
and methods with appropriate span kinds. Decorators can be used with or without parentheses.
66
"""
77

8+
import functools
9+
from termcolor import colored
10+
11+
from agentops.logging import logger
812
from agentops.sdk.decorators.factory import create_entity_decorator
913
from agentops.semconv.span_kinds import SpanKind
1014

1115
# Create decorators for specific entity types using the factory
1216
agent = create_entity_decorator(SpanKind.AGENT)
1317
task = create_entity_decorator(SpanKind.TASK)
14-
operation = create_entity_decorator(SpanKind.OPERATION)
18+
operation_decorator = create_entity_decorator(SpanKind.OPERATION)
1519
workflow = create_entity_decorator(SpanKind.WORKFLOW)
16-
session = create_entity_decorator(SpanKind.SESSION)
20+
trace = create_entity_decorator(SpanKind.SESSION)
21+
22+
23+
# For backward compatibility: @session decorator calls @trace decorator
24+
@functools.wraps(trace)
25+
def session(*args, **kwargs):
26+
# yellow color
27+
logger.info(colored("@agentops.session decorator is deprecated. Please use @agentops.trace instead.", "yellow"))
28+
# If called as @session or @session(...)
29+
if not args or not callable(args[0]): # called with kwargs like @session(name=...)
30+
return trace(*args, **kwargs)
31+
else: # called as @session directly on a function
32+
return trace(args[0], **kwargs) # args[0] is the wrapped function
33+
34+
35+
# Note: The original `operation = task` was potentially problematic if `operation` was meant to be distinct.
36+
# Using operation_decorator for clarity if a distinct OPERATION kind decorator is needed.
37+
# For now, keeping the alias as it was, assuming it was intentional for `operation` to be `task`.
1738
operation = task
1839

19-
__all__ = ["agent", "task", "workflow", "session", "operation"]
40+
__all__ = ["agent", "task", "workflow", "trace", "session", "operation"]
2041

2142
# Create decorators task, workflow, session, agent

agentops/sdk/decorators/factory.py

Lines changed: 102 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
from agentops.logging import logger
88
from agentops.sdk.core import TracingCore
9+
from agentops.semconv.span_kinds import SpanKind
910

1011
from .utility import (
1112
_create_as_current_span,
@@ -28,10 +29,10 @@ def create_entity_decorator(entity_kind: str):
2829
A decorator with optional arguments for name and version
2930
"""
3031

31-
def decorator(wrapped=None, *, name=None, version=None):
32+
def decorator(wrapped=None, *, name=None, version=None, tags=None):
3233
# Handle case where decorator is called with parameters
3334
if wrapped is None:
34-
return functools.partial(decorator, name=name, version=version)
35+
return functools.partial(decorator, name=name, version=version, tags=tags)
3536

3637
# Handle class decoration
3738
if inspect.isclass(wrapped):
@@ -91,7 +92,7 @@ async def __aexit__(self, exc_type, exc_val, exc_tb):
9192
@wrapt.decorator
9293
def wrapper(wrapped, instance, args, kwargs):
9394
# Skip instrumentation if tracer not initialized
94-
if not TracingCore.get_instance()._initialized:
95+
if not TracingCore.get_instance().initialized:
9596
return wrapped(*args, **kwargs)
9697

9798
# Use provided name or function name
@@ -102,8 +103,100 @@ def wrapper(wrapped, instance, args, kwargs):
102103
is_generator = inspect.isgeneratorfunction(wrapped)
103104
is_async_generator = inspect.isasyncgenfunction(wrapped)
104105

106+
# If it's a SESSION kind, we use start_trace/end_trace
107+
if entity_kind == SpanKind.SESSION:
108+
if is_generator or is_async_generator:
109+
# Using start_trace/end_trace for generators decorated with @session might be complex
110+
# due to the nature of yielding. For now, log a warning and fall back to existing generator handling OR disallow.
111+
# Let's keep existing generator handling for now, which creates a single span.
112+
# A true "session per generator invocation" would require more complex handling.
113+
logger.warning(
114+
f"@agentops.session decorator used on a generator function '{operation_name}'. \
115+
This will create a single span for the generator's instantiation, not a long-running trace for its entire execution."
116+
)
117+
# Fallthrough to existing generator logic below for non-session spans
118+
pass # Explicitly fall through
119+
120+
elif is_async:
121+
122+
async def _wrapped_session_async():
123+
trace_context = None
124+
try:
125+
trace_context = TracingCore.get_instance().start_trace(trace_name=operation_name, tags=tags)
126+
if not trace_context:
127+
logger.error(
128+
f"Failed to start trace for @session '{operation_name}'. Executing function without AgentOps trace."
129+
)
130+
return await wrapped(*args, **kwargs)
131+
132+
# Record input if possible (span is in trace_context.span)
133+
try:
134+
_record_entity_input(trace_context.span, args, kwargs)
135+
except Exception as e:
136+
logger.warning(f"Failed to record entity input for @session '{operation_name}': {e}")
137+
138+
result = await wrapped(*args, **kwargs)
139+
140+
try:
141+
_record_entity_output(trace_context.span, result)
142+
except Exception as e:
143+
logger.warning(f"Failed to record entity output for @session '{operation_name}': {e}")
144+
145+
TracingCore.get_instance().end_trace(trace_context, "Success")
146+
return result
147+
except Exception:
148+
if trace_context:
149+
TracingCore.get_instance().end_trace(trace_context, "Failure")
150+
# record_exception on trace_context.span might be an option too
151+
# trace_context.span.record_exception(e) # If we want it on the span directly
152+
raise
153+
finally:
154+
# Ensure trace is ended if not already (e.g. early exit without exception but before success end_trace)
155+
if trace_context and trace_context.span.is_recording():
156+
logger.warning(
157+
f"Trace for @session '{operation_name}' was not explicitly ended. Ending as 'Unknown'."
158+
)
159+
TracingCore.get_instance().end_trace(trace_context, "Unknown")
160+
161+
return _wrapped_session_async()
162+
else: # Sync function for SpanKind.SESSION
163+
trace_context = None
164+
try:
165+
trace_context = TracingCore.get_instance().start_trace(trace_name=operation_name, tags=tags)
166+
if not trace_context:
167+
logger.error(
168+
f"Failed to start trace for @session '{operation_name}'. Executing function without AgentOps trace."
169+
)
170+
return wrapped(*args, **kwargs)
171+
172+
try:
173+
_record_entity_input(trace_context.span, args, kwargs)
174+
except Exception as e:
175+
logger.warning(f"Failed to record entity input for @session '{operation_name}': {e}")
176+
177+
result = wrapped(*args, **kwargs)
178+
179+
try:
180+
_record_entity_output(trace_context.span, result)
181+
except Exception as e:
182+
logger.warning(f"Failed to record entity output for @session '{operation_name}': {e}")
183+
184+
TracingCore.get_instance().end_trace(trace_context, "Success")
185+
return result
186+
except Exception:
187+
if trace_context:
188+
TracingCore.get_instance().end_trace(trace_context, "Failure")
189+
raise
190+
finally:
191+
if trace_context and trace_context.span.is_recording():
192+
logger.warning(
193+
f"Trace for @session '{operation_name}' was not explicitly ended. Ending as 'Unknown'."
194+
)
195+
TracingCore.get_instance().end_trace(trace_context, "Unknown")
196+
197+
# Existing logic for non-SESSION kinds or generators under @session (as per above warning)
105198
# Handle generator functions
106-
if is_generator:
199+
if is_generator: # This 'if' will also catch generators decorated with @session due to fallthrough
107200
# Use the old approach for generators
108201
span, ctx, token = _make_span(operation_name, entity_kind, version)
109202
try:
@@ -115,7 +208,7 @@ def wrapper(wrapped, instance, args, kwargs):
115208
return _process_sync_generator(span, result)
116209

117210
# Handle async generator functions
118-
elif is_async_generator:
211+
elif is_async_generator: # This 'elif' will also catch async generators decorated with @session
119212
# Use the old approach for async generators
120213
span, ctx, token = _make_span(operation_name, entity_kind, version)
121214
try:
@@ -126,8 +219,8 @@ def wrapper(wrapped, instance, args, kwargs):
126219
result = wrapped(*args, **kwargs)
127220
return _process_async_generator(span, token, result)
128221

129-
# Handle async functions
130-
elif is_async:
222+
# Handle async functions (non-SESSION)
223+
elif is_async: # This is for entity_kind != SpanKind.SESSION
131224

132225
async def _wrapped_async():
133226
with _create_as_current_span(operation_name, entity_kind, version) as span:
@@ -149,8 +242,8 @@ async def _wrapped_async():
149242

150243
return _wrapped_async()
151244

152-
# Handle sync functions
153-
else:
245+
# Handle sync functions (non-SESSION)
246+
else: # This is for entity_kind != SpanKind.SESSION
154247
with _create_as_current_span(operation_name, entity_kind, version) as span:
155248
try:
156249
_record_entity_input(span, args, kwargs)

agentops/sdk/processors.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
from opentelemetry.sdk.trace.export import SpanExporter
1414

1515
from agentops.logging import logger
16-
from agentops.helpers.dashboard import log_trace_url
1716
from agentops.semconv.core import CoreAttributes
1817
from agentops.logging import upload_logfile
1918

@@ -108,7 +107,6 @@ def on_start(self, span: Span, parent_context: Optional[Context] = None) -> None
108107
if not self._root_span_id:
109108
self._root_span_id = span.context.span_id
110109
logger.debug(f"[agentops.InternalSpanProcessor] Found root span: {span.name}")
111-
log_trace_url(span)
112110

113111
def on_end(self, span: ReadableSpan) -> None:
114112
"""
@@ -123,7 +121,6 @@ def on_end(self, span: ReadableSpan) -> None:
123121

124122
if self._root_span_id and (span.context.span_id is self._root_span_id):
125123
logger.debug(f"[agentops.InternalSpanProcessor] Ending root span: {span.name}")
126-
log_trace_url(span)
127124
try:
128125
upload_logfile(span.context.trace_id)
129126
except Exception as e:

0 commit comments

Comments
 (0)