progress-observability 1.1.4__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.
@@ -0,0 +1,22 @@
1
+ """
2
+ Progress Observability - Zero-intrusion AI agent telemetry
3
+
4
+ Provides granular control over AI agent tracing with zero code changes required
5
+ to existing agent implementations.
6
+ """
7
+
8
+ from .sdk import Observability
9
+ from .decorators import task, workflow, agent, tool
10
+ from .enums import ObservabilityInstruments
11
+ from .constants import OBSERVABILITY_ENV_VARS
12
+ from .helpers import clear_sdk_env_vars
13
+
14
+ __all__ = [
15
+ 'Observability',
16
+ 'ObservabilityInstruments',
17
+ 'OBSERVABILITY_ENV_VARS',
18
+ 'task',
19
+ 'workflow',
20
+ 'agent',
21
+ 'tool',
22
+ ]
@@ -0,0 +1,31 @@
1
+ """
2
+ Constants for Progress Observability Instrumentation
3
+
4
+ Contains environment variables and configuration constants used by the
5
+ Progress Observability instrumentation system.
6
+ """
7
+
8
+ # Traceloop SDK environment variables that should be cleared to prevent conflicts
9
+ SDK_ENV_VARS = [
10
+ "TRACELOOP_TELEMETRY",
11
+ "TRACELOOP_BASE_URL",
12
+ "TRACELOOP_API_KEY",
13
+ "TRACELOOP_HEADERS",
14
+ "TRACELOOP_METRICS_ENDPOINT",
15
+ "TRACELOOP_METRICS_HEADERS",
16
+ "TRACELOOP_LOGGING_ENDPOINT",
17
+ "TRACELOOP_LOGGING_HEADERS"
18
+ ]
19
+
20
+ # Observability environment variables
21
+ OBSERVABILITY_ENV_VARS = [
22
+ "OBSERVABILITY_API_KEY",
23
+ "OBSERVABILITY_ENDPOINT",
24
+ "OBSERVABILITY_APP_NAME",
25
+ "OBSERVABILITY_TRACE_CONTENT"
26
+ ]
27
+
28
+ # Default configuration values
29
+ DEFAULTS = {
30
+ "ENDPOINT": "https://collector.observability.progress.com:443"
31
+ }
@@ -0,0 +1,288 @@
1
+ """
2
+ Progress Observability Decorators - Unified telemetry decorators for AI agents
3
+
4
+ Provides convenient decorators for instrumenting agent functions, workflows,
5
+ tasks, and tools with Progress Observability telemetry.
6
+ """
7
+
8
+ import functools
9
+ import inspect
10
+ from typing import Optional, TypeVar, Callable, Awaitable, Union, Dict, Any
11
+
12
+ from opentelemetry import trace
13
+ from opentelemetry.trace import SpanKind, Status, StatusCode
14
+
15
+
16
+ class ObservabilitySpanKind:
17
+ """
18
+ Observability span kind values for distinguishing different operation types.
19
+
20
+ These custom span kinds complement OpenTelemetry standard semantic conventions
21
+ to provide domain-specific categorization for AI agent operations.
22
+ """
23
+ TASK = "task"
24
+ WORKFLOW = "workflow"
25
+ AGENT = "agent"
26
+ TOOL = "tool"
27
+
28
+
29
+ R = TypeVar("R")
30
+ F = TypeVar("F", bound=Callable[..., Union[R, Awaitable[R]]])
31
+
32
+ # Get tracer instance
33
+ tracer = trace.get_tracer("progress.observability")
34
+
35
+
36
+ def _create_span_wrapper(
37
+ func: F,
38
+ span_name: Optional[str],
39
+ span_kind: str,
40
+ version: Optional[int],
41
+ additional_attributes: Optional[Dict[str, Any]] = None,
42
+ ) -> F:
43
+ """
44
+ Create a span wrapper for a function with OpenTelemetry standard semantic conventions.
45
+
46
+ Args:
47
+ func: Function to wrap
48
+ span_name: Name for the span (if None, derived from function)
49
+ span_kind: Observability span kind value
50
+ version: Optional version number
51
+ additional_attributes: Optional additional attributes to add
52
+
53
+ Returns:
54
+ Wrapped function with telemetry
55
+ """
56
+ # Get function name and namespace
57
+ func_name = func.__name__
58
+ namespace = None
59
+
60
+ # Try to get class name if this is a method
61
+ if hasattr(func, '__qualname__') and '.' in func.__qualname__:
62
+ parts = func.__qualname__.rsplit('.', 1)
63
+ if len(parts) == 2:
64
+ namespace = parts[0]
65
+
66
+ # Determine span name
67
+ if span_name is None:
68
+ span_name = f"{namespace}.{func_name}" if namespace else func_name
69
+
70
+ # Build base attributes using OpenTelemetry standard semantic conventions
71
+ base_attributes = {
72
+ 'code.function': func_name,
73
+ 'observability.span.kind': span_kind,
74
+ }
75
+
76
+ if namespace:
77
+ base_attributes['code.namespace'] = namespace
78
+
79
+ if version is not None:
80
+ base_attributes['service.version'] = str(version)
81
+
82
+ if additional_attributes:
83
+ base_attributes.update(additional_attributes)
84
+
85
+ # Handle async functions
86
+ if inspect.iscoroutinefunction(func):
87
+ @functools.wraps(func)
88
+ async def async_wrapper(*args: Any, **kwargs: Any) -> Any:
89
+ with tracer.start_as_current_span(
90
+ span_name,
91
+ kind=SpanKind.INTERNAL,
92
+ attributes=base_attributes,
93
+ ) as span:
94
+ try:
95
+ result = await func(*args, **kwargs)
96
+ span.set_status(Status(StatusCode.OK))
97
+ return result
98
+ except Exception as e:
99
+ span.set_status(Status(StatusCode.ERROR, str(e)))
100
+ span.record_exception(e)
101
+ raise
102
+ return async_wrapper # type: ignore
103
+
104
+ # Handle sync functions
105
+ @functools.wraps(func)
106
+ def sync_wrapper(*args: Any, **kwargs: Any) -> Any:
107
+ with tracer.start_as_current_span(
108
+ span_name,
109
+ kind=SpanKind.INTERNAL,
110
+ attributes=base_attributes,
111
+ ) as span:
112
+ try:
113
+ result = func(*args, **kwargs)
114
+ span.set_status(Status(StatusCode.OK))
115
+ return result
116
+ except Exception as e:
117
+ span.set_status(Status(StatusCode.ERROR, str(e)))
118
+ span.record_exception(e)
119
+ raise
120
+ return sync_wrapper # type: ignore
121
+
122
+
123
+ def task(
124
+ name: Optional[str] = None,
125
+ version: Optional[int] = None,
126
+ attributes: Optional[Dict[str, Any]] = None,
127
+ ) -> Callable[[F], F]:
128
+ """
129
+ Decorator for instrumenting task functions with Progress Observability telemetry.
130
+
131
+ Uses OpenTelemetry standard semantic conventions for attributes:
132
+ - code.function: Function name
133
+ - code.namespace: Class name (if method)
134
+ - service.version: Version number
135
+ - observability.span.kind: Set to "task"
136
+
137
+ Args:
138
+ name: Optional name override for the task span
139
+ version: Optional version number for the task
140
+ attributes: Optional additional attributes to add to the span
141
+
142
+ Returns:
143
+ Decorated function with telemetry instrumentation
144
+
145
+ Examples:
146
+ @task()
147
+ def my_task():
148
+ pass
149
+
150
+ @task(name="custom_task", version=1)
151
+ async def async_task():
152
+ pass
153
+ """
154
+ def decorator(func: F) -> F:
155
+ return _create_span_wrapper(
156
+ func,
157
+ name,
158
+ ObservabilitySpanKind.TASK,
159
+ version,
160
+ attributes,
161
+ )
162
+ return decorator
163
+
164
+
165
+ def workflow(
166
+ name: Optional[str] = None,
167
+ version: Optional[int] = None,
168
+ attributes: Optional[Dict[str, Any]] = None,
169
+ ) -> Callable[[F], F]:
170
+ """
171
+ Decorator for instrumenting workflow functions with Progress Observability telemetry.
172
+
173
+ Uses OpenTelemetry standard semantic conventions for attributes:
174
+ - code.function: Function name
175
+ - code.namespace: Class name (if method)
176
+ - service.version: Version number
177
+ - observability.span.kind: Set to "workflow"
178
+
179
+ Args:
180
+ name: Optional name override for the workflow span
181
+ version: Optional version number for the workflow
182
+ attributes: Optional additional attributes to add to the span
183
+
184
+ Returns:
185
+ Decorated function with telemetry instrumentation
186
+
187
+ Examples:
188
+ @workflow()
189
+ def my_workflow():
190
+ pass
191
+
192
+ @workflow(name="data_processing", version=2)
193
+ async def process_data():
194
+ pass
195
+ """
196
+ def decorator(func: F) -> F:
197
+ return _create_span_wrapper(
198
+ func,
199
+ name,
200
+ ObservabilitySpanKind.WORKFLOW,
201
+ version,
202
+ attributes,
203
+ )
204
+ return decorator
205
+
206
+
207
+ def agent(
208
+ name: Optional[str] = None,
209
+ version: Optional[int] = None,
210
+ attributes: Optional[Dict[str, Any]] = None,
211
+ ) -> Callable[[F], F]:
212
+ """
213
+ Decorator for instrumenting agent functions with Progress Observability telemetry.
214
+
215
+ Uses OpenTelemetry standard semantic conventions for attributes:
216
+ - code.function: Function name
217
+ - code.namespace: Class name (if method)
218
+ - service.version: Version number
219
+ - observability.span.kind: Set to "agent"
220
+
221
+ Args:
222
+ name: Optional name override for the agent span
223
+ version: Optional version number for the agent
224
+ attributes: Optional additional attributes to add to the span
225
+
226
+ Returns:
227
+ Decorated function with telemetry instrumentation
228
+
229
+ Examples:
230
+ @agent()
231
+ def my_agent():
232
+ pass
233
+
234
+ @agent(name="chat_agent", version=1)
235
+ async def chat_with_user():
236
+ pass
237
+ """
238
+ def decorator(func: F) -> F:
239
+ return _create_span_wrapper(
240
+ func,
241
+ name,
242
+ ObservabilitySpanKind.AGENT,
243
+ version,
244
+ attributes,
245
+ )
246
+ return decorator
247
+
248
+
249
+ def tool(
250
+ name: Optional[str] = None,
251
+ version: Optional[int] = None,
252
+ attributes: Optional[Dict[str, Any]] = None,
253
+ ) -> Callable[[F], F]:
254
+ """
255
+ Decorator for instrumenting tool functions with Progress Observability telemetry.
256
+
257
+ Uses OpenTelemetry standard semantic conventions for attributes:
258
+ - code.function: Function name
259
+ - code.namespace: Class name (if method)
260
+ - service.version: Version number
261
+ - observability.span.kind: Set to "tool"
262
+
263
+ Args:
264
+ name: Optional name override for the tool span
265
+ version: Optional version number for the tool
266
+ attributes: Optional additional attributes to add to the span
267
+
268
+ Returns:
269
+ Decorated function with telemetry instrumentation
270
+
271
+ Examples:
272
+ @tool()
273
+ def my_tool():
274
+ pass
275
+
276
+ @tool(name="web_search", version=1)
277
+ async def search_web(query: str):
278
+ pass
279
+ """
280
+ def decorator(func: F) -> F:
281
+ return _create_span_wrapper(
282
+ func,
283
+ name,
284
+ ObservabilitySpanKind.TOOL,
285
+ version,
286
+ attributes,
287
+ )
288
+ return decorator
@@ -0,0 +1,51 @@
1
+ """
2
+ Enums for Progress Observability Instrumentation
3
+
4
+ Provides granular control over AI agent tracing.
5
+ """
6
+
7
+ from enum import Enum
8
+ from traceloop.sdk.instruments import Instruments
9
+
10
+
11
+ class ObservabilityInstruments(Enum):
12
+ """Progress Observability instruments enum that maps to Traceloop instruments"""
13
+ # LLM Providers
14
+ OPENAI = Instruments.OPENAI
15
+ ANTHROPIC = Instruments.ANTHROPIC
16
+ COHERE = Instruments.COHERE
17
+ BEDROCK = Instruments.BEDROCK
18
+ VERTEXAI = Instruments.VERTEXAI
19
+ SAGEMAKER = Instruments.SAGEMAKER
20
+ OLLAMA = Instruments.OLLAMA
21
+ GROQ = Instruments.GROQ
22
+ MISTRAL = Instruments.MISTRAL
23
+ TOGETHER = Instruments.TOGETHER
24
+ REPLICATE = Instruments.REPLICATE
25
+ ALEPHALPHA = Instruments.ALEPHALPHA
26
+ GOOGLE_GENERATIVEAI = Instruments.GOOGLE_GENERATIVEAI
27
+ TRANSFORMERS = Instruments.TRANSFORMERS
28
+ WATSONX = Instruments.WATSONX
29
+
30
+ # Agent and Chain Frameworks
31
+ LANGCHAIN = Instruments.LANGCHAIN
32
+ LLAMA_INDEX = Instruments.LLAMA_INDEX
33
+ CREW = Instruments.CREW
34
+ HAYSTACK = Instruments.HAYSTACK
35
+ OPENAI_AGENTS = Instruments.OPENAI_AGENTS
36
+ MCP = Instruments.MCP
37
+
38
+ # Vector Databases
39
+ PINECONE = Instruments.PINECONE
40
+ CHROMA = Instruments.CHROMA
41
+ WEAVIATE = Instruments.WEAVIATE
42
+ QDRANT = Instruments.QDRANT
43
+ MILVUS = Instruments.MILVUS
44
+ LANCEDB = Instruments.LANCEDB
45
+ MARQO = Instruments.MARQO
46
+ REDIS = Instruments.REDIS
47
+ PYMYSQL = Instruments.PYMYSQL
48
+
49
+ # Tools and Infrastructure
50
+ REQUESTS = Instruments.REQUESTS
51
+ URLLIB3 = Instruments.URLLIB3
@@ -0,0 +1,61 @@
1
+ """
2
+ Observability custom exceptions for validation and error handling.
3
+ """
4
+
5
+
6
+ class ObservabilityError(Exception):
7
+ """Base exception class for Progress Observability instrumentation errors."""
8
+ pass
9
+
10
+
11
+ class EndpointValidationError(ObservabilityError):
12
+ """Raised when an invalid endpoint is provided."""
13
+ pass
14
+
15
+
16
+ class InvalidPortError(EndpointValidationError):
17
+ """Raised when an invalid port number is provided in the endpoint."""
18
+ def __init__(self, port: str, message: str = None):
19
+ if message is None:
20
+ message = f"Invalid port number '{port}'. Port must be between 1 and 65535."
21
+ super().__init__(message)
22
+
23
+
24
+ class MissingHostError(EndpointValidationError):
25
+ """Raised when the host is missing from the endpoint URL."""
26
+ def __init__(self, endpoint: str, message: str = None):
27
+ if message is None:
28
+ message = f"Missing host in endpoint '{endpoint}'. Expected format: http://hostname:port"
29
+ super().__init__(message)
30
+
31
+
32
+ class MissingPortError(EndpointValidationError):
33
+ """Raised when the port is missing from the endpoint URL."""
34
+ def __init__(self, endpoint: str, message: str = None):
35
+ if message is None:
36
+ message = f"Missing port in endpoint '{endpoint}'. Expected format: http://hostname:port"
37
+ super().__init__(message)
38
+
39
+
40
+ class NonNumericPortError(EndpointValidationError):
41
+ """Raised when the port is not a valid numeric value."""
42
+ def __init__(self, port: str, message: str = None):
43
+ if message is None:
44
+ message = f"Port '{port}' must be a numeric value between 1 and 65535."
45
+ super().__init__(message)
46
+
47
+
48
+ class UnsupportedSchemeError(EndpointValidationError):
49
+ """Raised when an unsupported URL scheme is used (only http/https are supported)."""
50
+ def __init__(self, scheme: str, message: str = None):
51
+ if message is None:
52
+ message = f"Unsupported URL scheme '{scheme}'. Only 'http' and 'https' are supported."
53
+ super().__init__(message)
54
+
55
+
56
+ class InvalidHostError(EndpointValidationError):
57
+ """Raised when the host contains invalid characters (e.g., spaces)."""
58
+ def __init__(self, host: str, message: str = None):
59
+ if message is None:
60
+ message = f"Invalid host '{host}'. Host cannot contain spaces or invalid characters."
61
+ super().__init__(message)
@@ -0,0 +1,143 @@
1
+ """
2
+ Helper functions for Progress Observability instrumentation
3
+ """
4
+
5
+ import os
6
+ import sys
7
+ from typing import Optional, Dict, Any
8
+ from .constants import SDK_ENV_VARS
9
+
10
+ class ObservabilityTelemetry:
11
+ """Helper class for Progress Observability"""
12
+
13
+ def __new__(cls):
14
+ if not hasattr(cls, "instance"):
15
+ cls.instance = super(ObservabilityTelemetry, cls).__new__(cls)
16
+ return cls.instance
17
+
18
+ def __init__(self):
19
+ self._telemetry_enabled = False
20
+ self._posthog = None
21
+ self._curr_anon_id = None
22
+
23
+ def _anon_id(self) -> str:
24
+ return "disabled"
25
+
26
+ def _context(self) -> Dict[str, Any]:
27
+ return {}
28
+
29
+ def capture(self, event: str, event_properties: Dict[str, Any] = {}) -> None:
30
+ pass
31
+
32
+ def log_exception(self, exception: Exception) -> None:
33
+ pass
34
+
35
+ def feature_enabled(self, key: str) -> bool:
36
+ return False
37
+
38
+
39
+ def patch_traceloop_modules() -> None:
40
+ """Patch Traceloop modules with Observability implementations"""
41
+ sys.modules['traceloop.sdk.telemetry'] = type(sys)('telemetry')
42
+ sys.modules['traceloop.sdk.telemetry'].Telemetry = ObservabilityTelemetry
43
+
44
+ # Patch Traceloop with Progress Observability's default span processor
45
+ from traceloop.sdk import Traceloop
46
+ Traceloop.get_default_span_processor = staticmethod(observability_get_default_span_processor)
47
+
48
+
49
+ def clear_sdk_env_vars() -> None:
50
+ """Clear all sdk* environment variables to prevent conflicts"""
51
+ for var in SDK_ENV_VARS:
52
+ if var in os.environ:
53
+ del os.environ[var]
54
+
55
+
56
+ def is_http_endpoint(endpoint: Optional[str]) -> bool:
57
+ """Check if endpoint is HTTP/HTTPS (vs gRPC)"""
58
+ return bool(endpoint) and (
59
+ endpoint.startswith("http://") or endpoint.startswith("https://")
60
+ )
61
+
62
+
63
+ def observability_get_default_span_processor(
64
+ disable_batch: bool = False,
65
+ api_endpoint: Optional[str] = None,
66
+ api_key: Optional[str] = None,
67
+ headers: Optional[Dict[str, Any]] = None,
68
+ exporter: Optional[Any] = None
69
+ ) -> Any:
70
+ """Observability version of get_default_span_processor with dual auth headers"""
71
+ from traceloop.sdk.tracing.tracing import get_default_span_processor
72
+
73
+ if headers is None:
74
+ if api_key is None:
75
+ api_key = os.getenv("OBSERVABILITY_API_KEY")
76
+
77
+ # Only add headers for HTTP endpoints
78
+ if is_http_endpoint(api_endpoint):
79
+ headers = {
80
+ "Authorization": f"Bearer {api_key}",
81
+ "X-Api-Key": api_key
82
+ }
83
+ else:
84
+ headers = {} # No headers for gRPC endpoints
85
+
86
+ if api_endpoint is None:
87
+ api_endpoint = os.getenv("OBSERVABILITY_ENDPOINT")
88
+
89
+ return get_default_span_processor(api_key, disable_batch, api_endpoint, headers, exporter)
90
+
91
+
92
+ def init_environment(app_name: str, endpoint: Optional[str], api_key: Optional[str], trace_content: Optional[bool]) -> tuple[str, Optional[str], Optional[str]]:
93
+ """Initialize environment variables with Observability overrides"""
94
+ endpoint = os.getenv("OBSERVABILITY_ENDPOINT") or endpoint
95
+ api_key = os.getenv("OBSERVABILITY_API_KEY") or api_key
96
+ app_name = os.getenv("OBSERVABILITY_APP_NAME") or app_name
97
+
98
+ # Handle trace_content from environment variable or parameter
99
+ env_trace_content = os.getenv("OBSERVABILITY_TRACE_CONTENT")
100
+ if env_trace_content is not None:
101
+ trace_content_value = env_trace_content.lower() in ('true', '1', 'yes')
102
+ elif trace_content is not None:
103
+ trace_content_value = trace_content
104
+ else:
105
+ trace_content_value = True
106
+
107
+ # Set TRACELOOP_TRACE_CONTENT for the underlying SDK only if false (default is true)
108
+ if not trace_content_value:
109
+ os.environ['TRACELOOP_TRACE_CONTENT'] = 'false'
110
+
111
+ import logging
112
+
113
+ # Disable metrics exporter - OBSERVABILITY focuses on spans/traces only
114
+ if 'OTEL_METRICS_EXPORTER' not in os.environ:
115
+ os.environ['OTEL_METRICS_EXPORTER'] = 'none'
116
+
117
+ # Suppress metrics exporter 404 errors (underlying SDK doesn't fully respect the env var)
118
+ logger = logging.getLogger("opentelemetry.exporter.otlp.proto.http.metric_exporter")
119
+ logger.disabled = True
120
+
121
+ # Validate endpoint format if provided
122
+ if endpoint and not (endpoint.startswith("http://") or endpoint.startswith("https://") or ":" in endpoint):
123
+ raise ValueError(f"Invalid endpoint format: {endpoint}")
124
+
125
+ return app_name, endpoint, api_key
126
+ def setup_api_key_headers(api_key: str, endpoint: Optional[str], init_kwargs: Dict[str, Any], kwargs: Dict[str, Any]) -> None:
127
+ """Setup API key validation and authentication headers"""
128
+ # Validate API key format
129
+ if not isinstance(api_key, str) or not api_key.strip():
130
+ raise ValueError("API key must be a non-empty string")
131
+
132
+ # Only add headers for HTTP endpoints
133
+ if is_http_endpoint(endpoint):
134
+ headers = kwargs.get("headers", {})
135
+ if isinstance(headers, dict):
136
+ headers["Authorization"] = f"Bearer {api_key}"
137
+ headers["X-Api-Key"] = api_key
138
+ else:
139
+ headers = {
140
+ "Authorization": f"Bearer {api_key}",
141
+ "X-Api-Key": api_key
142
+ }
143
+ init_kwargs["headers"] = headers