Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions agentops/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
from agentops.client import Client
from agentops.sdk.core import TraceContext, tracer
from agentops.sdk.decorators import trace, session, agent, task, workflow, operation, tool, guardrail, track_endpoint
from agentops.sdk.decorators import error as error_decorator, log as log_decorator
from agentops.enums import TraceState, SUCCESS, ERROR, UNSET
from opentelemetry.trace.status import StatusCode

Expand Down Expand Up @@ -447,6 +448,46 @@ def extract_key_from_attr(attr_value: str) -> str:
return False


def error(name: str = "error", message: str = "", **attributes: Any) -> None:
"""Record an error event as a one-shot span.

Creates and immediately closes a span of kind ERROR with the given message.

Args:
name: Name of the error event.
message: Error message or description.
**attributes: Additional attributes to set on the span.
"""
from agentops.sdk.decorators.utility import _create_as_current_span

extra = {k: str(v) for k, v in attributes.items()}
if message:
extra["error.message"] = message
with _create_as_current_span(name, SpanKind.ERROR, attributes=extra) as span:
span.set_status(StatusCode.ERROR, message)


def log(name: str = "log", message: str = "", level: str = "INFO", **attributes: Any) -> None:
"""Record a log event as a one-shot span.

Creates and immediately closes a span of kind LOG with the given message and level.

Args:
name: Name of the log event.
message: Log message content.
level: Log level (e.g. DEBUG, INFO, WARNING, ERROR).
**attributes: Additional attributes to set on the span.
"""
from agentops.sdk.decorators.utility import _create_as_current_span

extra = {k: str(v) for k, v in attributes.items()}
if message:
extra["log.message"] = message
extra["log.level"] = level
with _create_as_current_span(name, SpanKind.LOG, attributes=extra):
pass


__all__ = [
# Legacy exports
"start_session",
Expand Down Expand Up @@ -476,6 +517,11 @@ def extract_key_from_attr(attr_value: str) -> str:
"tool",
"guardrail",
"track_endpoint",
"error_decorator",
"log_decorator",
# Standalone span functions
"error",
"log",
# Enums
"TraceState",
"SUCCESS",
Expand Down
4 changes: 4 additions & 0 deletions agentops/sdk/decorators/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
operation = task
guardrail = create_entity_decorator(SpanKind.GUARDRAIL)
track_endpoint = create_entity_decorator(SpanKind.HTTP)
error = create_entity_decorator(SpanKind.ERROR)
log = create_entity_decorator(SpanKind.LOG)


# For backward compatibility: @session decorator calls @trace decorator
Expand Down Expand Up @@ -48,4 +50,6 @@ def session(*args, **kwargs): # noqa: F811
"tool",
"guardrail",
"track_endpoint",
"error",
"log",
]
4 changes: 4 additions & 0 deletions agentops/semconv/span_kinds.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ class AgentOpsSpanKindValues(Enum):
TEXT = "text"
GUARDRAIL = "guardrail"
HTTP = "http"
ERROR = "error"
LOG = "log"
UNKNOWN = "unknown"


Expand Down Expand Up @@ -46,3 +48,5 @@ class SpanKind:
TEXT = AgentOpsSpanKindValues.TEXT.value
GUARDRAIL = AgentOpsSpanKindValues.GUARDRAIL.value
HTTP = AgentOpsSpanKindValues.HTTP.value
ERROR = AgentOpsSpanKindValues.ERROR.value
LOG = AgentOpsSpanKindValues.LOG.value
176 changes: 176 additions & 0 deletions tests/unit/sdk/test_error_log_spans.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
"""Tests for error and log span kinds β€” decorators and standalone functions."""

from agentops.sdk.decorators import error as error_decorator, log as log_decorator
from agentops.semconv import SpanKind
from agentops.semconv.span_attributes import SpanAttributes
from tests.unit.sdk.instrumentation_tester import InstrumentationTester


class TestErrorDecorator:
"""Tests for the @error decorator."""

def test_error_decorator_creates_span(self, instrumentation: InstrumentationTester):
@error_decorator
def failing_operation():
return "error occurred"

result = failing_operation()
assert result == "error occurred"

spans = instrumentation.get_finished_spans()
assert len(spans) == 1

span = spans[0]
assert span.attributes[SpanAttributes.AGENTOPS_SPAN_KIND] == SpanKind.ERROR

def test_error_decorator_with_name(self, instrumentation: InstrumentationTester):
@error_decorator(name="custom_error")
def failing_operation():
return "fail"

failing_operation()

spans = instrumentation.get_finished_spans()
assert len(spans) == 1
assert spans[0].attributes[SpanAttributes.OPERATION_NAME] == "custom_error"

def test_error_decorator_async(self, instrumentation: InstrumentationTester):
@error_decorator
async def async_failing():
return "async error"

import asyncio

loop = asyncio.new_event_loop()
try:
result = loop.run_until_complete(async_failing())
finally:
loop.close()
assert result == "async error"

spans = instrumentation.get_finished_spans()
assert len(spans) == 1
assert spans[0].attributes[SpanAttributes.AGENTOPS_SPAN_KIND] == SpanKind.ERROR


class TestLogDecorator:
"""Tests for the @log decorator."""

def test_log_decorator_creates_span(self, instrumentation: InstrumentationTester):
@log_decorator
def log_operation():
return "logged"

result = log_operation()
assert result == "logged"

spans = instrumentation.get_finished_spans()
assert len(spans) == 1
assert spans[0].attributes[SpanAttributes.AGENTOPS_SPAN_KIND] == SpanKind.LOG

def test_log_decorator_with_name(self, instrumentation: InstrumentationTester):
@log_decorator(name="custom_log")
def log_operation():
return "log"

log_operation()

spans = instrumentation.get_finished_spans()
assert len(spans) == 1
assert spans[0].attributes[SpanAttributes.OPERATION_NAME] == "custom_log"


class TestStandaloneError:
"""Tests for the standalone agentops.error() function."""

def test_error_creates_span(self, instrumentation: InstrumentationTester):
import agentops

agentops.error(name="test_error", message="something went wrong")

spans = instrumentation.get_finished_spans()
assert len(spans) == 1

span = spans[0]
assert span.attributes[SpanAttributes.AGENTOPS_SPAN_KIND] == SpanKind.ERROR
assert span.attributes["error.message"] == "something went wrong"

def test_error_sets_error_status(self, instrumentation: InstrumentationTester):
import agentops
from opentelemetry.trace import StatusCode

agentops.error(name="status_error", message="bad request")

spans = instrumentation.get_finished_spans()
assert len(spans) == 1
assert spans[0].status.status_code == StatusCode.ERROR

def test_error_with_extra_attributes(self, instrumentation: InstrumentationTester):
import agentops

agentops.error(name="attr_error", message="fail", code="E001")

spans = instrumentation.get_finished_spans()
assert len(spans) == 1
assert spans[0].attributes["code"] == "E001"

def test_error_default_name(self, instrumentation: InstrumentationTester):
import agentops

agentops.error(message="default name test")

spans = instrumentation.get_finished_spans()
assert len(spans) == 1
assert spans[0].attributes[SpanAttributes.OPERATION_NAME] == "error"


class TestStandaloneLog:
"""Tests for the standalone agentops.log() function."""

def test_log_creates_span(self, instrumentation: InstrumentationTester):
import agentops

agentops.log(name="test_log", message="hello world")

spans = instrumentation.get_finished_spans()
assert len(spans) == 1

span = spans[0]
assert span.attributes[SpanAttributes.AGENTOPS_SPAN_KIND] == SpanKind.LOG
assert span.attributes["log.message"] == "hello world"

def test_log_with_level(self, instrumentation: InstrumentationTester):
import agentops

agentops.log(name="level_log", message="debug info", level="DEBUG")

spans = instrumentation.get_finished_spans()
assert len(spans) == 1
assert spans[0].attributes["log.level"] == "DEBUG"

def test_log_default_level_is_info(self, instrumentation: InstrumentationTester):
import agentops

agentops.log(name="info_log", message="info message")

spans = instrumentation.get_finished_spans()
assert len(spans) == 1
assert spans[0].attributes["log.level"] == "INFO"

def test_log_with_extra_attributes(self, instrumentation: InstrumentationTester):
import agentops

agentops.log(name="extra_log", message="data", source="test")

spans = instrumentation.get_finished_spans()
assert len(spans) == 1
assert spans[0].attributes["source"] == "test"

def test_log_default_name(self, instrumentation: InstrumentationTester):
import agentops

agentops.log(message="default")

spans = instrumentation.get_finished_spans()
assert len(spans) == 1
assert spans[0].attributes[SpanAttributes.OPERATION_NAME] == "log"