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
tests/unit/conftest.py ADDED
@@ -0,0 +1,54 @@
1
+ import uuid
2
+
3
+ import pytest
4
+ import requests_mock
5
+
6
+ from agentops.config import Config
7
+ from tests.fixtures.client import * # noqa
8
+ from tests.unit.sdk.instrumentation_tester import InstrumentationTester
9
+
10
+
11
+ @pytest.fixture
12
+ def api_key() -> str:
13
+ """Standard API key for testing"""
14
+ return "test-api-key"
15
+
16
+
17
+ @pytest.fixture
18
+ def endpoint() -> str:
19
+ """Base API URL"""
20
+ return Config().endpoint
21
+
22
+
23
+ @pytest.fixture(autouse=True)
24
+ def mock_req(endpoint, api_key):
25
+ """
26
+ Mocks AgentOps backend API requests.
27
+ """
28
+ with requests_mock.Mocker(real_http=False) as m:
29
+ # Map session IDs to their JWTs
30
+ m.post(
31
+ endpoint + "/v3/auth/token",
32
+ json={"token": str(uuid.uuid4()), "project_id": "test-project-id", "api_key": api_key},
33
+ )
34
+ yield m
35
+
36
+
37
+ @pytest.fixture
38
+ def noinstrument():
39
+ # Tells the client to not instrument LLM calls
40
+ yield
41
+
42
+
43
+ @pytest.fixture
44
+ def mock_config(mocker):
45
+ """Mock the Client.configure method"""
46
+ return mocker.patch("agentops.client.Client.configure")
47
+
48
+
49
+ @pytest.fixture
50
+ def instrumentation():
51
+ """Fixture for the instrumentation tester."""
52
+ tester = InstrumentationTester()
53
+ yield tester
54
+ tester.reset()
@@ -0,0 +1 @@
1
+ # Test package for the SDK
@@ -0,0 +1,207 @@
1
+ from typing import Any, Dict, List, Protocol, Tuple, Union
2
+ import importlib
3
+ import unittest.mock as mock
4
+
5
+ from opentelemetry import trace as trace_api
6
+ from opentelemetry.sdk.trace import ReadableSpan, Span, TracerProvider
7
+ from opentelemetry.sdk.trace.export import SimpleSpanProcessor
8
+ from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter
9
+ from opentelemetry.util.types import Attributes
10
+
11
+ from agentops.sdk.core import tracer
12
+
13
+
14
+ def create_tracer_provider(
15
+ **kwargs,
16
+ ) -> Tuple[TracerProvider, InMemorySpanExporter, SimpleSpanProcessor]:
17
+ """Helper to create a configured tracer provider.
18
+
19
+ Creates and configures a `TracerProvider` with a
20
+ `SimpleSpanProcessor` and a `InMemorySpanExporter`.
21
+ All the parameters passed are forwarded to the TracerProvider
22
+ constructor.
23
+
24
+ Returns:
25
+ A tuple with the tracer provider, memory exporter, and span processor.
26
+ """
27
+ tracer_provider = TracerProvider(**kwargs)
28
+ memory_exporter = InMemorySpanExporter()
29
+
30
+ # Use SimpleSpanProcessor instead of both processors to avoid duplication
31
+ span_processor = SimpleSpanProcessor(memory_exporter)
32
+ tracer_provider.add_span_processor(span_processor)
33
+
34
+ return tracer_provider, memory_exporter, span_processor
35
+
36
+
37
+ def reset_trace_globals():
38
+ """Reset the global trace state to avoid conflicts."""
39
+ # Reset tracer provider
40
+ trace_api._TRACER_PROVIDER = None
41
+
42
+ # Reload the trace module to clear warning state
43
+ importlib.reload(trace_api)
44
+
45
+
46
+ class HasAttributesViaProperty(Protocol):
47
+ @property
48
+ def attributes(self) -> Attributes:
49
+ ...
50
+
51
+
52
+ class HasAttributesViaAttr(Protocol):
53
+ attributes: Attributes
54
+
55
+
56
+ HasAttributes = Union[HasAttributesViaProperty, HasAttributesViaAttr]
57
+
58
+
59
+ class InstrumentationTester:
60
+ """
61
+ A utility class for testing instrumentation in the AgentOps SDK.
62
+
63
+ This class provides methods for setting up a test environment with
64
+ in-memory span exporters, and for asserting properties of spans.
65
+ """
66
+
67
+ tracer_provider: TracerProvider
68
+ memory_exporter: InMemorySpanExporter
69
+ span_processor: SimpleSpanProcessor
70
+
71
+ def __init__(self):
72
+ """Initialize the instrumentation tester."""
73
+ # Reset any global state first
74
+ reset_trace_globals()
75
+
76
+ # Shut down any existing tracing core
77
+ # self._shutdown_core()
78
+
79
+ # Create a new tracer provider and memory exporter
80
+ (
81
+ self.tracer_provider,
82
+ self.memory_exporter,
83
+ self.span_processor,
84
+ ) = create_tracer_provider()
85
+
86
+ # Set the tracer provider
87
+ trace_api.set_tracer_provider(self.tracer_provider)
88
+
89
+ # Create a mock for the meter provider
90
+ self.mock_meter_provider = mock.MagicMock()
91
+
92
+ # Patch the setup_telemetry function to return our test providers
93
+ self.setup_telemetry_patcher = mock.patch(
94
+ "agentops.sdk.core.setup_telemetry", return_value=(self.tracer_provider, self.mock_meter_provider)
95
+ )
96
+ self.mock_setup_telemetry = self.setup_telemetry_patcher.start()
97
+
98
+ # Reset the tracing core to force reinitialization
99
+ core = tracer
100
+ core._initialized = False
101
+ core._provider = None
102
+
103
+ # Initialize the core, which will now use our mocked setup_telemetry
104
+ core.initialize()
105
+
106
+ self.clear_spans()
107
+
108
+ def _shutdown_core(self):
109
+ """Safely shut down the tracing core."""
110
+ try:
111
+ tracer.shutdown()
112
+ except Exception as e:
113
+ print(f"Warning: Error shutting down tracing core: {e}")
114
+
115
+ def clear_spans(self):
116
+ """Clear all spans from the memory exporter."""
117
+ # Force flush spans
118
+ self.span_processor.force_flush()
119
+
120
+ # Then clear the memory
121
+ self.memory_exporter.clear()
122
+ print("Cleared all spans from memory exporter")
123
+
124
+ def reset(self):
125
+ """Reset the instrumentation tester."""
126
+ # Force flush any pending spans
127
+ self.span_processor.force_flush()
128
+
129
+ # Clear any existing spans
130
+ self.clear_spans()
131
+
132
+ # Reset global trace state
133
+ reset_trace_globals()
134
+
135
+ # Set our tracer provider again
136
+ trace_api.set_tracer_provider(self.tracer_provider)
137
+
138
+ # Shut down and re-initialize the tracing core
139
+ self._shutdown_core()
140
+
141
+ # Reset the mock setup_telemetry function
142
+ self.mock_setup_telemetry.reset_mock()
143
+
144
+ # Reset the tracing core to force reinitialization
145
+ core = tracer
146
+ core._initialized = False
147
+ core._provider = None
148
+
149
+ # Initialize the core, which will now use our mocked setup_telemetry
150
+ core.initialize()
151
+
152
+ def __del__(self):
153
+ """Clean up resources when the tester is garbage collected."""
154
+ try:
155
+ # Stop the patcher when the tester is deleted
156
+ self.setup_telemetry_patcher.stop()
157
+ except Exception:
158
+ pass
159
+
160
+ def get_finished_spans(self) -> List[ReadableSpan]:
161
+ """Get all finished spans."""
162
+ # Force flush any pending spans
163
+ self.span_processor.force_flush()
164
+
165
+ # Get the spans
166
+ spans = list(self.memory_exporter.get_finished_spans())
167
+ print(f"Instrumentation Tester: Found {len(spans)} finished spans")
168
+ for i, span in enumerate(spans):
169
+ print(f"Span {i}: name={span.name}, attributes={span.attributes}")
170
+ return spans
171
+
172
+ def get_spans_by_name(self, name: str) -> List[ReadableSpan]:
173
+ """Get all spans with the given name."""
174
+ return [span for span in self.get_finished_spans() if span.name == name]
175
+
176
+ def get_spans_by_kind(self, kind: str) -> List[ReadableSpan]:
177
+ """Get all spans with the given kind."""
178
+ return [
179
+ span for span in self.get_finished_spans() if span.attributes and span.attributes.get("span.kind") == kind
180
+ ]
181
+
182
+ @staticmethod
183
+ def assert_has_attributes(obj: HasAttributes, attributes: Dict[str, Any]):
184
+ """Assert that an object has the given attributes."""
185
+ import json
186
+
187
+ assert obj.attributes is not None
188
+ for key, val in attributes.items():
189
+ assert key in obj.attributes, f"Key {key!r} not found in attributes"
190
+
191
+ actual_val = obj.attributes[key]
192
+
193
+ # Try to handle JSON-serialized values
194
+ if isinstance(actual_val, str) and isinstance(val, (list, dict)):
195
+ try:
196
+ actual_val = json.loads(actual_val)
197
+ except json.JSONDecodeError:
198
+ pass
199
+
200
+ assert actual_val == val, f"Value for key {key!r} does not match: {actual_val} != {val}"
201
+
202
+ @staticmethod
203
+ def assert_span_instrumented_for(span: Union[Span, ReadableSpan], module):
204
+ """Assert that a span is instrumented for the given module."""
205
+ assert span.instrumentation_scope is not None
206
+ assert span.instrumentation_scope.name == module.__name__
207
+ assert span.instrumentation_scope.version == module.__version__
@@ -0,0 +1,392 @@
1
+ """
2
+ Tests for agentops.sdk.attributes module.
3
+
4
+ This module tests all attribute management functions for telemetry contexts.
5
+ """
6
+
7
+ import platform
8
+ from unittest.mock import Mock, patch
9
+
10
+ import pytest
11
+
12
+ from agentops.sdk.attributes import (
13
+ get_system_resource_attributes,
14
+ get_global_resource_attributes,
15
+ get_trace_attributes,
16
+ get_span_attributes,
17
+ get_session_end_attributes,
18
+ )
19
+ from agentops.semconv import ResourceAttributes, SpanAttributes, CoreAttributes
20
+
21
+
22
+ class TestGetSystemResourceAttributes:
23
+ """Test get_system_resource_attributes function."""
24
+
25
+ def test_basic_system_attributes(self):
26
+ """Test that basic system attributes are included."""
27
+ attributes = get_system_resource_attributes()
28
+
29
+ # Check that all basic platform attributes are present
30
+ assert ResourceAttributes.HOST_MACHINE in attributes
31
+ assert ResourceAttributes.HOST_NAME in attributes
32
+ assert ResourceAttributes.HOST_NODE in attributes
33
+ assert ResourceAttributes.HOST_PROCESSOR in attributes
34
+ assert ResourceAttributes.HOST_SYSTEM in attributes
35
+ assert ResourceAttributes.HOST_VERSION in attributes
36
+ assert ResourceAttributes.HOST_OS_RELEASE in attributes
37
+
38
+ # Check that values match platform module
39
+ assert attributes[ResourceAttributes.HOST_MACHINE] == platform.machine()
40
+ assert attributes[ResourceAttributes.HOST_NAME] == platform.node()
41
+ assert attributes[ResourceAttributes.HOST_NODE] == platform.node()
42
+ assert attributes[ResourceAttributes.HOST_PROCESSOR] == platform.processor()
43
+ assert attributes[ResourceAttributes.HOST_SYSTEM] == platform.system()
44
+ assert attributes[ResourceAttributes.HOST_VERSION] == platform.version()
45
+ assert attributes[ResourceAttributes.HOST_OS_RELEASE] == platform.release()
46
+
47
+ @patch("agentops.sdk.attributes.os.cpu_count")
48
+ @patch("agentops.sdk.attributes.psutil.cpu_percent")
49
+ def test_cpu_stats_success(self, mock_cpu_percent, mock_cpu_count):
50
+ """Test CPU stats when successfully retrieved."""
51
+ mock_cpu_count.return_value = 8
52
+ mock_cpu_percent.return_value = 25.5
53
+
54
+ attributes = get_system_resource_attributes()
55
+
56
+ assert ResourceAttributes.CPU_COUNT in attributes
57
+ assert ResourceAttributes.CPU_PERCENT in attributes
58
+ assert attributes[ResourceAttributes.CPU_COUNT] == 8
59
+ assert attributes[ResourceAttributes.CPU_PERCENT] == 25.5
60
+
61
+ @patch("agentops.sdk.attributes.os.cpu_count")
62
+ @patch("agentops.sdk.attributes.psutil.cpu_percent")
63
+ def test_cpu_stats_cpu_count_none(self, mock_cpu_percent, mock_cpu_count):
64
+ """Test CPU stats when cpu_count returns None."""
65
+ mock_cpu_count.return_value = None
66
+ mock_cpu_percent.return_value = 25.5
67
+
68
+ attributes = get_system_resource_attributes()
69
+
70
+ assert ResourceAttributes.CPU_COUNT in attributes
71
+ assert attributes[ResourceAttributes.CPU_COUNT] == 0
72
+
73
+ @patch("agentops.sdk.attributes.os.cpu_count")
74
+ @patch("agentops.sdk.attributes.psutil.cpu_percent")
75
+ def test_cpu_stats_exception(self, mock_cpu_percent, mock_cpu_count):
76
+ """Test CPU stats when exception occurs."""
77
+ mock_cpu_count.side_effect = Exception("CPU count error")
78
+ mock_cpu_percent.side_effect = Exception("CPU percent error")
79
+
80
+ attributes = get_system_resource_attributes()
81
+
82
+ # Should not include CPU attributes when exception occurs
83
+ assert ResourceAttributes.CPU_COUNT not in attributes
84
+ assert ResourceAttributes.CPU_PERCENT not in attributes
85
+
86
+ @patch("agentops.sdk.attributes.psutil.virtual_memory")
87
+ def test_memory_stats_success(self, mock_virtual_memory):
88
+ """Test memory stats when successfully retrieved."""
89
+ mock_memory = Mock()
90
+ mock_memory.total = 8589934592 # 8GB
91
+ mock_memory.available = 4294967296 # 4GB
92
+ mock_memory.used = 4294967296 # 4GB
93
+ mock_memory.percent = 50.0
94
+ mock_virtual_memory.return_value = mock_memory
95
+
96
+ attributes = get_system_resource_attributes()
97
+
98
+ assert ResourceAttributes.MEMORY_TOTAL in attributes
99
+ assert ResourceAttributes.MEMORY_AVAILABLE in attributes
100
+ assert ResourceAttributes.MEMORY_USED in attributes
101
+ assert ResourceAttributes.MEMORY_PERCENT in attributes
102
+ assert attributes[ResourceAttributes.MEMORY_TOTAL] == 8589934592
103
+ assert attributes[ResourceAttributes.MEMORY_AVAILABLE] == 4294967296
104
+ assert attributes[ResourceAttributes.MEMORY_USED] == 4294967296
105
+ assert attributes[ResourceAttributes.MEMORY_PERCENT] == 50.0
106
+
107
+ @patch("agentops.sdk.attributes.psutil.virtual_memory")
108
+ def test_memory_stats_exception(self, mock_virtual_memory):
109
+ """Test memory stats when exception occurs."""
110
+ mock_virtual_memory.side_effect = Exception("Memory error")
111
+
112
+ attributes = get_system_resource_attributes()
113
+
114
+ # Should not include memory attributes when exception occurs
115
+ assert ResourceAttributes.MEMORY_TOTAL not in attributes
116
+ assert ResourceAttributes.MEMORY_AVAILABLE not in attributes
117
+ assert ResourceAttributes.MEMORY_USED not in attributes
118
+ assert ResourceAttributes.MEMORY_PERCENT not in attributes
119
+
120
+
121
+ class TestGetGlobalResourceAttributes:
122
+ """Test get_global_resource_attributes function."""
123
+
124
+ @patch("agentops.sdk.attributes.get_imported_libraries")
125
+ def test_basic_attributes_with_project_id(self, mock_get_libs):
126
+ """Test basic attributes with project ID."""
127
+ mock_get_libs.return_value = ["requests", "pandas"]
128
+
129
+ attributes = get_global_resource_attributes("test-service", project_id="test-project")
130
+
131
+ assert ResourceAttributes.SERVICE_NAME in attributes
132
+ assert ResourceAttributes.PROJECT_ID in attributes
133
+ assert ResourceAttributes.IMPORTED_LIBRARIES in attributes
134
+ assert attributes[ResourceAttributes.SERVICE_NAME] == "test-service"
135
+ assert attributes[ResourceAttributes.PROJECT_ID] == "test-project"
136
+ assert attributes[ResourceAttributes.IMPORTED_LIBRARIES] == ["requests", "pandas"]
137
+
138
+ @patch("agentops.sdk.attributes.get_imported_libraries")
139
+ def test_basic_attributes_without_project_id(self, mock_get_libs):
140
+ """Test basic attributes without project ID."""
141
+ mock_get_libs.return_value = ["requests", "pandas"]
142
+
143
+ attributes = get_global_resource_attributes("test-service")
144
+
145
+ assert ResourceAttributes.SERVICE_NAME in attributes
146
+ assert ResourceAttributes.PROJECT_ID not in attributes
147
+ assert ResourceAttributes.IMPORTED_LIBRARIES in attributes
148
+ assert attributes[ResourceAttributes.SERVICE_NAME] == "test-service"
149
+ assert attributes[ResourceAttributes.IMPORTED_LIBRARIES] == ["requests", "pandas"]
150
+
151
+ @patch("agentops.sdk.attributes.get_imported_libraries")
152
+ def test_no_imported_libraries(self, mock_get_libs):
153
+ """Test when no imported libraries are found."""
154
+ mock_get_libs.return_value = None
155
+
156
+ attributes = get_global_resource_attributes("test-service", project_id="test-project")
157
+
158
+ assert ResourceAttributes.SERVICE_NAME in attributes
159
+ assert ResourceAttributes.PROJECT_ID in attributes
160
+ assert ResourceAttributes.IMPORTED_LIBRARIES not in attributes
161
+ assert attributes[ResourceAttributes.SERVICE_NAME] == "test-service"
162
+ assert attributes[ResourceAttributes.PROJECT_ID] == "test-project"
163
+
164
+ @patch("agentops.sdk.attributes.get_imported_libraries")
165
+ def test_empty_imported_libraries(self, mock_get_libs):
166
+ """Test when imported libraries list is empty."""
167
+ mock_get_libs.return_value = []
168
+
169
+ attributes = get_global_resource_attributes("test-service", project_id="test-project")
170
+
171
+ assert ResourceAttributes.SERVICE_NAME in attributes
172
+ assert ResourceAttributes.PROJECT_ID in attributes
173
+ assert ResourceAttributes.IMPORTED_LIBRARIES not in attributes
174
+ assert attributes[ResourceAttributes.SERVICE_NAME] == "test-service"
175
+ assert attributes[ResourceAttributes.PROJECT_ID] == "test-project"
176
+
177
+
178
+ class TestGetTraceAttributes:
179
+ """Test get_trace_attributes function."""
180
+
181
+ def test_no_tags(self):
182
+ """Test when no tags are provided."""
183
+ attributes = get_trace_attributes()
184
+
185
+ assert attributes == {}
186
+
187
+ def test_list_tags(self):
188
+ """Test with list of tags."""
189
+ tags = ["tag1", "tag2", "tag3"]
190
+ attributes = get_trace_attributes(tags)
191
+
192
+ assert CoreAttributes.TAGS in attributes
193
+ assert attributes[CoreAttributes.TAGS] == ["tag1", "tag2", "tag3"]
194
+
195
+ def test_dict_tags(self):
196
+ """Test with dictionary of tags."""
197
+ tags = {"key1": "value1", "key2": "value2"}
198
+ attributes = get_trace_attributes(tags)
199
+
200
+ assert "key1" in attributes
201
+ assert "key2" in attributes
202
+ assert attributes["key1"] == "value1"
203
+ assert attributes["key2"] == "value2"
204
+
205
+ def test_mixed_dict_tags(self):
206
+ """Test with dictionary containing various value types."""
207
+ tags = {
208
+ "string_key": "string_value",
209
+ "int_key": 42,
210
+ "float_key": 3.14,
211
+ "bool_key": True,
212
+ "list_key": [1, 2, 3],
213
+ }
214
+ attributes = get_trace_attributes(tags)
215
+
216
+ assert attributes["string_key"] == "string_value"
217
+ assert attributes["int_key"] == 42
218
+ assert attributes["float_key"] == 3.14
219
+ assert attributes["bool_key"] is True
220
+ assert attributes["list_key"] == [1, 2, 3]
221
+
222
+ def test_invalid_tags_type(self):
223
+ """Test with invalid tags type."""
224
+ with patch("agentops.sdk.attributes.logger") as mock_logger:
225
+ attributes = get_trace_attributes("invalid_tags")
226
+
227
+ assert attributes == {}
228
+ mock_logger.warning.assert_called_once()
229
+
230
+ def test_none_tags(self):
231
+ """Test with None tags."""
232
+ attributes = get_trace_attributes(None)
233
+
234
+ assert attributes == {}
235
+
236
+
237
+ class TestGetSpanAttributes:
238
+ """Test get_span_attributes function."""
239
+
240
+ def test_basic_span_attributes(self):
241
+ """Test basic span attributes."""
242
+ attributes = get_span_attributes("test-operation", "test-kind")
243
+
244
+ assert SpanAttributes.AGENTOPS_SPAN_KIND in attributes
245
+ assert SpanAttributes.OPERATION_NAME in attributes
246
+ assert attributes[SpanAttributes.AGENTOPS_SPAN_KIND] == "test-kind"
247
+ assert attributes[SpanAttributes.OPERATION_NAME] == "test-operation"
248
+ assert SpanAttributes.OPERATION_VERSION not in attributes
249
+
250
+ def test_span_attributes_with_version(self):
251
+ """Test span attributes with version."""
252
+ attributes = get_span_attributes("test-operation", "test-kind", version=1)
253
+
254
+ assert SpanAttributes.AGENTOPS_SPAN_KIND in attributes
255
+ assert SpanAttributes.OPERATION_NAME in attributes
256
+ assert SpanAttributes.OPERATION_VERSION in attributes
257
+ assert attributes[SpanAttributes.AGENTOPS_SPAN_KIND] == "test-kind"
258
+ assert attributes[SpanAttributes.OPERATION_NAME] == "test-operation"
259
+ assert attributes[SpanAttributes.OPERATION_VERSION] == 1
260
+
261
+ def test_span_attributes_with_version_zero(self):
262
+ """Test span attributes with version zero."""
263
+ attributes = get_span_attributes("test-operation", "test-kind", version=0)
264
+
265
+ assert SpanAttributes.OPERATION_VERSION in attributes
266
+ assert attributes[SpanAttributes.OPERATION_VERSION] == 0
267
+
268
+ def test_span_attributes_with_additional_kwargs(self):
269
+ """Test span attributes with additional keyword arguments."""
270
+ attributes = get_span_attributes(
271
+ "test-operation",
272
+ "test-kind",
273
+ version=1,
274
+ custom_key="custom_value",
275
+ another_key=42,
276
+ )
277
+
278
+ assert SpanAttributes.AGENTOPS_SPAN_KIND in attributes
279
+ assert SpanAttributes.OPERATION_NAME in attributes
280
+ assert SpanAttributes.OPERATION_VERSION in attributes
281
+ assert "custom_key" in attributes
282
+ assert "another_key" in attributes
283
+ assert attributes["custom_key"] == "custom_value"
284
+ assert attributes["another_key"] == 42
285
+
286
+ def test_span_attributes_overwrite_kwargs(self):
287
+ """Test that kwargs can overwrite default attributes."""
288
+ attributes = get_span_attributes(
289
+ "test-operation",
290
+ "test-kind",
291
+ version=1,
292
+ custom_operation_name="overwritten-name",
293
+ custom_span_kind="overwritten-kind",
294
+ )
295
+
296
+ # kwargs should overwrite the default values
297
+ assert attributes["custom_operation_name"] == "overwritten-name"
298
+ assert attributes["custom_span_kind"] == "overwritten-kind"
299
+ # The original positional arguments should still be set
300
+ assert attributes[SpanAttributes.OPERATION_NAME] == "test-operation"
301
+ assert attributes[SpanAttributes.AGENTOPS_SPAN_KIND] == "test-kind"
302
+
303
+
304
+ class TestGetSessionEndAttributes:
305
+ """Test get_session_end_attributes function."""
306
+
307
+ def test_session_end_attributes_success(self):
308
+ """Test session end attributes with success state."""
309
+ attributes = get_session_end_attributes("Success")
310
+
311
+ assert SpanAttributes.AGENTOPS_SESSION_END_STATE in attributes
312
+ assert attributes[SpanAttributes.AGENTOPS_SESSION_END_STATE] == "Success"
313
+
314
+ def test_session_end_attributes_failure(self):
315
+ """Test session end attributes with failure state."""
316
+ attributes = get_session_end_attributes("Failure")
317
+
318
+ assert SpanAttributes.AGENTOPS_SESSION_END_STATE in attributes
319
+ assert attributes[SpanAttributes.AGENTOPS_SESSION_END_STATE] == "Failure"
320
+
321
+ def test_session_end_attributes_custom_state(self):
322
+ """Test session end attributes with custom state."""
323
+ attributes = get_session_end_attributes("CustomState")
324
+
325
+ assert SpanAttributes.AGENTOPS_SESSION_END_STATE in attributes
326
+ assert attributes[SpanAttributes.AGENTOPS_SESSION_END_STATE] == "CustomState"
327
+
328
+ def test_session_end_attributes_empty_string(self):
329
+ """Test session end attributes with empty string."""
330
+ attributes = get_session_end_attributes("")
331
+
332
+ assert SpanAttributes.AGENTOPS_SESSION_END_STATE in attributes
333
+ assert attributes[SpanAttributes.AGENTOPS_SESSION_END_STATE] == ""
334
+
335
+
336
+ class TestAttributesIntegration:
337
+ """Integration tests for attributes module."""
338
+
339
+ def test_all_functions_work_together(self):
340
+ """Test that all attribute functions work together without conflicts."""
341
+ # Get system attributes
342
+ system_attrs = get_system_resource_attributes()
343
+ assert isinstance(system_attrs, dict)
344
+
345
+ # Get global attributes
346
+ global_attrs = get_global_resource_attributes("test-service", project_id="test-project")
347
+ assert isinstance(global_attrs, dict)
348
+
349
+ # Get trace attributes
350
+ trace_attrs = get_trace_attributes(["tag1", "tag2"])
351
+ assert isinstance(trace_attrs, dict)
352
+
353
+ # Get span attributes
354
+ span_attrs = get_span_attributes("test-operation", "test-kind", version=1)
355
+ assert isinstance(span_attrs, dict)
356
+
357
+ # Get session end attributes
358
+ session_attrs = get_session_end_attributes("Success")
359
+ assert isinstance(session_attrs, dict)
360
+
361
+ # Verify no key conflicts between different attribute types
362
+ all_keys = (
363
+ set(system_attrs.keys())
364
+ | set(global_attrs.keys())
365
+ | set(trace_attrs.keys())
366
+ | set(span_attrs.keys())
367
+ | set(session_attrs.keys())
368
+ )
369
+ assert len(all_keys) == len(system_attrs) + len(global_attrs) + len(trace_attrs) + len(span_attrs) + len(
370
+ session_attrs
371
+ )
372
+
373
+ def test_attribute_types_consistency(self):
374
+ """Test that all attributes return consistent types."""
375
+ # All functions should return dictionaries
376
+ assert isinstance(get_system_resource_attributes(), dict)
377
+ assert isinstance(get_global_resource_attributes("test"), dict)
378
+ assert isinstance(get_trace_attributes(), dict)
379
+ assert isinstance(get_span_attributes("test", "test"), dict)
380
+ assert isinstance(get_session_end_attributes("test"), dict)
381
+
382
+ # All dictionary values should be serializable
383
+ import json
384
+
385
+ try:
386
+ json.dumps(get_system_resource_attributes())
387
+ json.dumps(get_global_resource_attributes("test"))
388
+ json.dumps(get_trace_attributes())
389
+ json.dumps(get_span_attributes("test", "test"))
390
+ json.dumps(get_session_end_attributes("test"))
391
+ except (TypeError, ValueError) as e:
392
+ pytest.fail(f"Attributes are not JSON serializable: {e}")