progress-observability 1.5.1__tar.gz → 1.5.3__tar.gz

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 (19) hide show
  1. {progress_observability-1.5.1 → progress_observability-1.5.3}/PKG-INFO +6 -3
  2. {progress_observability-1.5.1 → progress_observability-1.5.3}/README.md +3 -1
  3. {progress_observability-1.5.1 → progress_observability-1.5.3}/pyproject.toml +3 -2
  4. {progress_observability-1.5.1 → progress_observability-1.5.3}/src/progress/observability/genkit_span_processor.py +25 -0
  5. {progress_observability-1.5.1 → progress_observability-1.5.3}/src/progress/observability/sdk.py +125 -8
  6. {progress_observability-1.5.1 → progress_observability-1.5.3}/uv.lock +4 -2
  7. {progress_observability-1.5.1 → progress_observability-1.5.3}/.gitignore +0 -0
  8. {progress_observability-1.5.1 → progress_observability-1.5.3}/.python-version +0 -0
  9. {progress_observability-1.5.1 → progress_observability-1.5.3}/LICENSE +0 -0
  10. {progress_observability-1.5.1 → progress_observability-1.5.3}/notices.txt +0 -0
  11. {progress_observability-1.5.1 → progress_observability-1.5.3}/src/progress/observability/__init__.py +0 -0
  12. {progress_observability-1.5.1 → progress_observability-1.5.3}/src/progress/observability/constants.py +0 -0
  13. {progress_observability-1.5.1 → progress_observability-1.5.3}/src/progress/observability/decorators.py +0 -0
  14. {progress_observability-1.5.1 → progress_observability-1.5.3}/src/progress/observability/enums.py +0 -0
  15. {progress_observability-1.5.1 → progress_observability-1.5.3}/src/progress/observability/exceptions.py +0 -0
  16. {progress_observability-1.5.1 → progress_observability-1.5.3}/src/progress/observability/genai_message_processor.py +0 -0
  17. {progress_observability-1.5.1 → progress_observability-1.5.3}/src/progress/observability/helpers.py +0 -0
  18. {progress_observability-1.5.1 → progress_observability-1.5.3}/src/progress/observability/model_fix_processor.py +0 -0
  19. {progress_observability-1.5.1 → progress_observability-1.5.3}/src/progress/observability/tags.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: progress-observability
3
- Version: 1.5.1
3
+ Version: 1.5.3
4
4
  Summary: Progress Observability instrumentation for Python agents
5
5
  Project-URL: Homepage, https://github.com/telerik/agentclarity
6
6
  Project-URL: Repository, https://github.com/telerik/agentclarity
@@ -10,7 +10,8 @@ License: MIT
10
10
  License-File: LICENSE
11
11
  License-File: notices.txt
12
12
  Requires-Python: <4.0.0,>=3.10.0
13
- Requires-Dist: httpx>=0.23.0
13
+ Requires-Dist: httpx==0.28.1
14
+ Requires-Dist: requests==2.34.2
14
15
  Requires-Dist: traceloop-sdk==0.62.3
15
16
  Description-Content-Type: text/markdown
16
17
 
@@ -50,7 +51,7 @@ Common options (for `Observability.instrument`):
50
51
  - `app_name` - Application name for telemetry
51
52
  - `endpoint` - OpenTelemetry collector endpoint
52
53
  - `api_key` - Authentication key
53
- - `debug` - Enable debug logging
54
+ - `debug` - Show sanitized OTLP trace-delivery failures on standard error (default: `False`)
54
55
  - `instruments` / `block_instruments` - Control what gets traced via `ObservabilityInstruments`
55
56
  - `trace_content` - when set to `False` will stop sending LLM prompt/responses as part of the telemetry data
56
57
  - `additional_tags` - custom tags (see below)
@@ -61,8 +62,10 @@ Environment overrides (optional):
61
62
  - `OBSERVABILITY_ENDPOINT` sets a custom endpoint for collecting data (e.g. "https://collector.observability.progress.com:443")
62
63
  - `OBSERVABILITY_API_KEY` sets the authentication key to use when sending data
63
64
  - `OBSERVABILITY_TRACE_CONTENT` when set to `false` will stop sending LLM prompt/responses as part of the telemetry data
65
+ - `OBSERVABILITY_DEBUG` enables sanitized trace-delivery errors when set to `1`, `true`, or `yes`
64
66
 
65
67
  Auth headers are added automatically for HTTP(S) endpoints when api_key is provided.
68
+ Invalid endpoints and missing or empty API keys raise during initialization. Delivery failures do not terminate the application and remain silent unless debug logging is enabled. Debug messages never include API keys, authorization headers, or telemetry payloads.
66
69
 
67
70
  ## Custom Tags
68
71
 
@@ -34,7 +34,7 @@ Common options (for `Observability.instrument`):
34
34
  - `app_name` - Application name for telemetry
35
35
  - `endpoint` - OpenTelemetry collector endpoint
36
36
  - `api_key` - Authentication key
37
- - `debug` - Enable debug logging
37
+ - `debug` - Show sanitized OTLP trace-delivery failures on standard error (default: `False`)
38
38
  - `instruments` / `block_instruments` - Control what gets traced via `ObservabilityInstruments`
39
39
  - `trace_content` - when set to `False` will stop sending LLM prompt/responses as part of the telemetry data
40
40
  - `additional_tags` - custom tags (see below)
@@ -45,8 +45,10 @@ Environment overrides (optional):
45
45
  - `OBSERVABILITY_ENDPOINT` sets a custom endpoint for collecting data (e.g. "https://collector.observability.progress.com:443")
46
46
  - `OBSERVABILITY_API_KEY` sets the authentication key to use when sending data
47
47
  - `OBSERVABILITY_TRACE_CONTENT` when set to `false` will stop sending LLM prompt/responses as part of the telemetry data
48
+ - `OBSERVABILITY_DEBUG` enables sanitized trace-delivery errors when set to `1`, `true`, or `yes`
48
49
 
49
50
  Auth headers are added automatically for HTTP(S) endpoints when api_key is provided.
51
+ Invalid endpoints and missing or empty API keys raise during initialization. Delivery failures do not terminate the application and remain silent unless debug logging is enabled. Debug messages never include API keys, authorization headers, or telemetry payloads.
50
52
 
51
53
  ## Custom Tags
52
54
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "progress-observability"
7
- version = "1.5.1"
7
+ version = "1.5.3"
8
8
  description = "Progress Observability instrumentation for Python agents"
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -12,7 +12,8 @@ license-files = ["LICENSE", "notices.txt"]
12
12
  requires-python = ">=3.10.0,<4.0.0"
13
13
  dependencies = [
14
14
  "traceloop-sdk==0.62.3",
15
- "httpx>=0.23.0",
15
+ "httpx==0.28.1",
16
+ "requests==2.34.2",
16
17
  ]
17
18
  [project.urls]
18
19
  Homepage = "https://github.com/telerik/agentclarity"
@@ -122,6 +122,31 @@ class GenkitSpanProcessor:
122
122
  attributes.setdefault("gen_ai.tool.call.result", tool_output)
123
123
  return
124
124
  if subtype != "model":
125
+ # The root span of ai.generate()/ai.generate_stream() calls made outside of an
126
+ # explicit flow isn't tagged with a subtype (it's registered as a "util"
127
+ # action), so it falls through every branch above and is left without a span
128
+ # kind or provider. Since genkit:input carries the requested model name, derive
129
+ # both from it the same way the model span does. Genkit's built-in generate
130
+ # action always reports subtype "util" (or omits it for nested turns); guard
131
+ # on that so a user-defined action merely named "generate" with a different
132
+ # subtype (e.g. "prompt") isn't misclassified.
133
+ if attributes.get("genkit:name") == "generate" and subtype in (None, "util"):
134
+ attributes.setdefault("traceloop.span.kind", "agent")
135
+ input_data = _parse_json(attributes.get("genkit:input"))
136
+ model_name = input_data.get("model") if input_data else None
137
+ if isinstance(model_name, str) and model_name:
138
+ separator = model_name.find("/")
139
+ if separator >= 0:
140
+ plugin = model_name[:separator].lower()
141
+ model = model_name[separator + 1 :]
142
+ if "gen_ai.provider.name" not in attributes and plugin:
143
+ attributes["gen_ai.provider.name"] = _PROVIDERS.get(
144
+ plugin, plugin[:1].upper() + plugin[1:]
145
+ )
146
+ else:
147
+ model = model_name
148
+ attributes.setdefault("gen_ai.request.model", model)
149
+ attributes.setdefault("gen_ai.response.model", model)
125
150
  return
126
151
 
127
152
  name = attributes.get("genkit:name")
@@ -19,6 +19,7 @@ Quick Start:
19
19
 
20
20
  """
21
21
 
22
+ import re
22
23
  import sys
23
24
  import os
24
25
  from contextlib import redirect_stdout, redirect_stderr
@@ -63,6 +64,88 @@ class Observability:
63
64
  _tags_processor = None # TagsSpanProcessor instance for tag propagation
64
65
  SDK = Traceloop
65
66
 
67
+ _EXPORTER_LOGGER_NAME = "opentelemetry.exporter.otlp.proto.http.trace_exporter"
68
+ _EXPORTER_HANDLER_MARKER = "_progress_observability_handler"
69
+ _EXPORTER_FILTER_MARKER = "_progress_observability_filter"
70
+
71
+ @staticmethod
72
+ def _sanitize_exporter_message(message: str) -> str:
73
+ """Map exporter failures to useful messages without exposing request data."""
74
+ normalized = message.lower()
75
+
76
+ status_match = re.search(r"\b(401|403)\b", normalized)
77
+ if status_match:
78
+ status_messages = {
79
+ "401": "HTTP 401 Unauthorized.",
80
+ "403": "HTTP 403 Forbidden.",
81
+ }
82
+ return status_messages[status_match.group(1)]
83
+ if "timed out" in normalized or "timeout" in normalized:
84
+ return "request timed out."
85
+ if any(
86
+ term in normalized for term in ("name resolution", "resolve host", "dns")
87
+ ):
88
+ return "collector host could not be resolved."
89
+ if any(
90
+ term in normalized
91
+ for term in (
92
+ "connection refused",
93
+ "unreachable",
94
+ "failed to establish a new connection",
95
+ )
96
+ ):
97
+ return "collector is unreachable."
98
+ return "failed to export traces."
99
+
100
+ @classmethod
101
+ def _configure_exporter_logging(cls, debug_enabled: bool) -> None:
102
+ exporter_logger = logging.getLogger(cls._EXPORTER_LOGGER_NAME)
103
+
104
+ for handler in list(exporter_logger.handlers):
105
+ if getattr(handler, cls._EXPORTER_HANDLER_MARKER, False):
106
+ exporter_logger.removeHandler(handler)
107
+ handler.close()
108
+ for logger_filter in list(exporter_logger.filters):
109
+ if getattr(logger_filter, cls._EXPORTER_FILTER_MARKER, False):
110
+ exporter_logger.removeFilter(logger_filter)
111
+
112
+ exporter_logger.propagate = False
113
+ exporter_logger.disabled = not debug_enabled
114
+
115
+ if not debug_enabled:
116
+ return
117
+
118
+ class SanitizingFilter(logging.Filter):
119
+ _progress_observability_filter = True
120
+
121
+ def filter(self, record: logging.LogRecord) -> bool:
122
+ record.msg = cls._sanitize_exporter_message(record.getMessage())
123
+ record.args = ()
124
+ record.exc_info = None
125
+ record.exc_text = None
126
+ record.stack_info = None
127
+ return True
128
+
129
+ exporter_logger.setLevel(logging.ERROR)
130
+ handler = logging.StreamHandler(sys.stderr)
131
+ handler.setLevel(logging.ERROR)
132
+ handler.setFormatter(
133
+ logging.Formatter(
134
+ "[Observability] Failed to export traces: %(message)s"
135
+ )
136
+ )
137
+ setattr(handler, cls._EXPORTER_HANDLER_MARKER, True)
138
+ exporter_logger.addFilter(SanitizingFilter())
139
+ exporter_logger.addHandler(handler)
140
+
141
+ @staticmethod
142
+ def _report_configuration_failure(debug_enabled: bool, message: str) -> None:
143
+ if debug_enabled:
144
+ print(
145
+ f"[Observability] Configuration error: {message}",
146
+ file=sys.stderr,
147
+ )
148
+
66
149
  @staticmethod
67
150
  def _validate_endpoint(endpoint: Optional[str]) -> str:
68
151
  """
@@ -131,7 +214,7 @@ class Observability:
131
214
  return endpoint
132
215
 
133
216
  @staticmethod
134
- def _validate_api_key(api_key: Optional[str]) -> None:
217
+ def _validate_api_key(api_key: Optional[str]) -> str:
135
218
  """Validate the API key is provided via parameter or environment variable.
136
219
  Args:
137
220
  api_key: The API key parameter value to validate
@@ -258,17 +341,49 @@ class Observability:
258
341
  # Always allow re-initialization to support different instrument configurations
259
342
  # The underlying traceloop SDK will handle duplicate initialization safely
260
343
 
261
- # Validate and resolve endpoint and API key before processing
262
- endpoint = cls._validate_endpoint(endpoint)
263
- api_key = cls._validate_api_key(api_key)
264
-
265
- app_name = init_environment(app_name, trace_content)
266
-
267
344
  # Ensure debug logs are visible when explicitly requested.
268
345
  # Honor both the debug parameter and the OBSERVABILITY_DEBUG env var.
269
346
  # Only raise the progress.observability logger - not the root logger,
270
347
  # which would make every third-party library verbose.
271
- debug_enabled = debug or os.getenv("OBSERVABILITY_DEBUG", "").lower() in ("1", "true", "yes")
348
+ debug_enabled = debug or os.getenv("OBSERVABILITY_DEBUG", "").lower() in (
349
+ "1",
350
+ "true",
351
+ "yes",
352
+ )
353
+ cls._configure_exporter_logging(debug_enabled)
354
+
355
+ endpoint_error: Optional[Exception] = None
356
+ api_key_error: Optional[Exception] = None
357
+
358
+ try:
359
+ endpoint = cls._validate_endpoint(endpoint)
360
+ except EndpointValidationError as exc:
361
+ endpoint_error = exc
362
+ cls._report_configuration_failure(
363
+ debug_enabled,
364
+ "endpoint is invalid. Use a valid http/https URL with host and port.",
365
+ )
366
+
367
+ try:
368
+ api_key = cls._validate_api_key(api_key)
369
+ except (TypeError, ValueError) as exc:
370
+ api_key_error = exc
371
+ cls._report_configuration_failure(
372
+ debug_enabled,
373
+ "API key is missing or empty.",
374
+ )
375
+
376
+ if endpoint_error and api_key_error:
377
+ raise ValueError(
378
+ "Invalid configuration: endpoint is invalid and API key is missing or empty."
379
+ ) from api_key_error
380
+ if endpoint_error:
381
+ raise endpoint_error
382
+ if api_key_error:
383
+ raise api_key_error
384
+
385
+ app_name = init_environment(app_name, trace_content)
386
+
272
387
  if debug_enabled:
273
388
  pkg_logger = logging.getLogger("progress.observability")
274
389
  pkg_logger.setLevel(logging.DEBUG)
@@ -380,6 +495,8 @@ class Observability:
380
495
  After shutdown, subsequent calls to instrument() will reinitialize
381
496
  the instrumentation.
382
497
  """
498
+ cls._configure_exporter_logging(False)
499
+
383
500
  if not cls._initialized:
384
501
  return True
385
502
 
@@ -1544,16 +1544,18 @@ wheels = [
1544
1544
 
1545
1545
  [[package]]
1546
1546
  name = "progress-observability"
1547
- version = "1.5.1"
1547
+ version = "1.5.3"
1548
1548
  source = { editable = "." }
1549
1549
  dependencies = [
1550
1550
  { name = "httpx" },
1551
+ { name = "requests" },
1551
1552
  { name = "traceloop-sdk" },
1552
1553
  ]
1553
1554
 
1554
1555
  [package.metadata]
1555
1556
  requires-dist = [
1556
- { name = "httpx", specifier = ">=0.23.0" },
1557
+ { name = "httpx", specifier = "==0.28.1" },
1558
+ { name = "requests", specifier = "==2.34.2" },
1557
1559
  { name = "traceloop-sdk", specifier = "==0.62.3" },
1558
1560
  ]
1559
1561