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.
- progress/observability/__init__.py +22 -0
- progress/observability/constants.py +31 -0
- progress/observability/decorators.py +288 -0
- progress/observability/enums.py +51 -0
- progress/observability/exceptions.py +61 -0
- progress/observability/helpers.py +143 -0
- progress/observability/model_fix_processor.py +804 -0
- progress/observability/sdk.py +296 -0
- progress_observability-1.1.4.dist-info/METADATA +68 -0
- progress_observability-1.1.4.dist-info/RECORD +13 -0
- progress_observability-1.1.4.dist-info/WHEEL +4 -0
- progress_observability-1.1.4.dist-info/licenses/LICENSE +7 -0
- progress_observability-1.1.4.dist-info/licenses/notices.txt +1424 -0
|
@@ -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
|