Skip to content

Conversation

@pec1985
Copy link
Contributor

@pec1985 pec1985 commented Oct 27, 2025

  • Add UserOpenTelemetryConfig dataclass for parsing user OTEL config from AGENTUITY_USER_OTEL_CONF env var
  • Implement create_user_logger_provider() to create user OTLP logger providers
  • Add multi-delegate logging system that sends logs to both Agentuity and user endpoints
  • Enhance create_logger() to automatically add MultiDelegateHandler when user providers exist
  • Add proper shutdown handling for user logger providers with flush and cleanup
  • Implement graceful error handling that doesn't break existing functionality
  • Add comprehensive test coverage with 25+ new tests
  • Maintain full backward compatibility with existing OTEL functionality
  • Add ruff as dev dependency for code linting consistency

This implementation mirrors the functionality from the JavaScript SDK PR #176, allowing users to send agent logs to their own OTLP endpoints alongside the default Agentuity telemetry collection.

Amp-Thread-ID: https://ampcode.com/threads/T-1dd20dcd-70fb-4f19-9de2-1984bb3b9a05

Summary by CodeRabbit

  • New Features

    • User-configurable OpenTelemetry support: optional user-provided telemetry initialization, multi-delegate forwarding of logs to registered telemetry providers (retroactive attachment), and an exposed shutdown for graceful teardown.
    • Server now triggers telemetry shutdown on application stop.
  • Tests

    • Comprehensive tests for config parsing, provider lifecycle, emission behavior, severity mapping, error handling, and retroactive provider attachment.
  • Chores

    • Added a dev tooling dependency for code quality.

- Add UserOpenTelemetryConfig dataclass for parsing user OTEL config from AGENTUITY_USER_OTEL_CONF env var
- Implement create_user_logger_provider() to create user OTLP logger providers
- Add multi-delegate logging system that sends logs to both Agentuity and user endpoints
- Enhance create_logger() to automatically add MultiDelegateHandler when user providers exist
- Add proper shutdown handling for user logger providers with flush and cleanup
- Implement graceful error handling that doesn't break existing functionality
- Add comprehensive test coverage with 25+ new tests
- Maintain full backward compatibility with existing OTEL functionality
- Add ruff as dev dependency for code linting consistency

This implementation mirrors the functionality from the JavaScript SDK PR #176,
allowing users to send agent logs to their own OTLP endpoints alongside
the default Agentuity telemetry collection.

Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-1dd20dcd-70fb-4f19-9de2-1984bb3b9a05
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 27, 2025

Walkthrough

Adds optional user-provided OpenTelemetry support and a multi-delegate logging path: parse env config, create/register OTLP logger providers, forward Python LogRecords to user providers via a MultiDelegateHandler, integrate provider lifecycle into init() and server shutdown, add tests, and introduce a dev dependency group.

Changes

Cohort / File(s) Summary
User OTEL configuration & provider factory
agentuity/otel/__init__.py
Adds UserOpenTelemetryConfig dataclass, parse_user_otel_config(), create_user_logger_provider(), global _user_logger_provider, shutdown(), integrates parsing into init(), updates __all__, and adds init/shutdown logging.
Multi-delegate logging system
agentuity/otel/logger.py
Adds _user_logger_providers registry, add_user_logger_provider(), emit_to_user_providers(record), MultiDelegateHandler(logging.Handler), updates create_logger(...) to attach the handler and apply contextual attributes, and maps Python levels to OpenTelemetry SeverityNumber.
Server lifecycle integration
agentuity/server/__init__.py
Imports shutdown as otel_shutdown, adds shutdown_handler(app) to call OTEL shutdown on server shutdown, and registers it via app.on_shutdown.append.
Build metadata
pyproject.toml
Adds [dependency-groups] with dev = ["ruff>=0.11.11"].
User logger tests
tests/otel/test_user_logger.py
New tests for provider registry lifecycle, emit_to_user_providers() (no-op/success/error), severity mapping, attribute propagation/filtering, MultiDelegateHandler behavior, create_logger() handler attachment/duplication prevention, and retroactive attachment.
User OTEL config/provider tests
tests/otel/test_user_otel.py
New tests for parse_user_otel_config() variations, create_user_logger_provider() import-error and successful wiring, URL/resource merging, and shutdown() behavior across success, exceptions, and missing-method cases.
Test fixture adjustment
tests/otel/test_logger.py
Ensures child_logger.handlers exists on the mock child logger by adding an empty handlers list.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant App as Application
    participant Init as otel.init()
    participant Logger as Python Logger
    participant MDH as MultiDelegateHandler
    participant Emit as emit_to_user_providers()
    participant Provider as User OTLP Provider
    participant SDK as OpenTelemetry SDK

    Note over Init,Provider: init() may parse env and create provider
    App->>Init: start / init()
    Init->>Provider: create_user_logger_provider() [optional]
    Provider-->>Init: registered

    Logger->>MDH: create_logger() attaches MDH
    Logger->>Logger: logger.handle(LogRecord)
    Logger->>MDH: MDH.emit(record)
    MDH->>Emit: emit_to_user_providers(record)
    Emit->>Emit: map level -> SeverityNumber\nmerge attributes, timestamp (ns)
    Emit->>Provider: provider.get_logger(name) -> emit(event)
    Provider->>SDK: OTLP export
    SDK-->>Provider: ack / error

    alt Provider raises
        Provider--X Emit: exception
        Emit-->>Logger: warning logged, continue
    else Success
        Provider-->>Emit: success
    end

    App->>Init: shutdown
    Init->>Provider: shutdown()/force-close
    Provider-->>Init: cleared
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Focus areas:
    • emit_to_user_providers(): severity mapping, nanosecond timestamp handling, attribute merging/filtering.
    • create_user_logger_provider(): exporter/processor/provider wiring and graceful failure when OpenTelemetry is absent.
    • shutdown(): reverse-ordered cleanup, exception handling, and missing-method cases.
    • MultiDelegateHandler and create_logger(): retroactive attachment, deduplication, and ContextFilter-like attribute application.
    • Tests: ensure mocks simulate SDK absence/failures and validate side effects.

Poem

🐰
I stitched a trail of logs tonight,
Handlers hop in soft moonlight.
Providers listen, then take flight,
Cleanly closing when day is bright.
A rabbit’s patchwork: tidy, light.

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The PR title "feat: Add user-provided OpenTelemetry configuration support" directly and clearly aligns with the main objective of this pull request. The changeset introduces a UserOpenTelemetryConfig dataclass, parse_user_otel_config() function, create_user_logger_provider() factory, and supporting infrastructure to enable users to configure their own OpenTelemetry endpoints via the AGENTUITY_USER_OTEL_CONF environment variable. The title is concise, uses proper semantic versioning conventions, and accurately conveys the primary feature being added. A teammate reviewing the commit history would immediately understand that this PR introduces user-configurable OpenTelemetry support.
Docstring Coverage ✅ Passed Docstring coverage is 93.48% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch USER_OTEL

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (13)
pyproject.toml (1)

49-53: Avoid duplicate dev deps and align ruff versions.

You now declare ruff in two places: [project.optional-dependencies].dev ("ruff>=0.1.0") and [dependency-groups].dev ("ruff>=0.11.11"). This can drift and confuse tooling (uv vs pip extras). Pick one source of truth or keep both but pin the same spec.

Apply either:

  • Remove ruff from [project.optional-dependencies].dev and rely on [dependency-groups] for uv-only workflows; or
  • Keep extras.dev and drop [dependency-groups]; or
  • Keep both but use identical constraints.

Also, please verify that "0.11.11" is an intended version and actually exists for ruff.

agentuity/otel/__init__.py (3)

16-26: Dataclass defaults: prefer concrete dicts over Optionals when you set {} later.

parse_user_otel_config returns {} for resource_attributes and headers, while dataclass fields are Optional. Make them Dict[...] = field(default_factory=dict) to simplify consumers and types.

-@dataclass
-class UserOpenTelemetryConfig:
+@dataclass
+class UserOpenTelemetryConfig:
     """Configuration for user-provided OpenTelemetry setup."""
     endpoint: str
     service_name: str
-    resource_attributes: Optional[Dict[str, Any]] = None
-    headers: Optional[Dict[str, str]] = None
+    resource_attributes: Dict[str, Any] = field(default_factory=dict)
+    headers: Dict[str, str] = field(default_factory=dict)

28-56: Normalize and validate headers/attributes on parse.

User-provided headers/attributes can contain non-string/invalid types. Normalize header keys to lowercase and coerce non-primitive attribute values to strings to avoid exporter errors.

-        return UserOpenTelemetryConfig(
+        headers = {str(k).lower(): str(v) for k, v in (config_data.get("headers", {}) or {}).items()}
+        attrs = config_data.get("resourceAttributes", {}) or {}
+        safe_attrs = {str(k): (v if isinstance(v, (str, int, float, bool)) else str(v)) for k, v in attrs.items()}
+        return UserOpenTelemetryConfig(
             endpoint=config_data["endpoint"],
             service_name=config_data["serviceName"],
-            resource_attributes=config_data.get("resourceAttributes", {}),
-            headers=config_data.get("headers", {})
+            resource_attributes=safe_attrs,
+            headers=headers
         )

63-111: User logger provider creation looks solid; consider compression and timeouts.

Recommend enabling gzip and making timeouts configurable to harden network IO without changing defaults.

-        exporter = OTLPLogExporter(
+        exporter = OTLPLogExporter(
             endpoint=logs_url,
             headers=user_config.headers or {},
-            timeout=10,
+            timeout=int(os.environ.get("AGENTUITY_USER_OTEL_TIMEOUT", "10")),
+            compression=os.environ.get("AGENTUITY_USER_OTEL_COMPRESSION", "gzip"),
         )
agentuity/otel/logger.py (2)

9-14: Dedup and type the provider registry.

Prevent duplicate registrations and add a type for better tooling. Optional: guard with a lock or id(provider) set if concurrency matters.

-from typing import List
+from typing import List, Any, Set
+import threading
@@
-_user_logger_providers: List = []
+_user_logger_providers: List[Any] = []
+_user_provider_ids: Set[int] = set()
+_user_providers_lock = threading.Lock()
@@
 def add_user_logger_provider(provider):
     """Add a user logger provider to the multi-delegate system."""
-    global _user_logger_providers
-    _user_logger_providers.append(provider)
+    global _user_logger_providers, _user_provider_ids
+    with _user_providers_lock:
+        pid = id(provider)
+        if pid in _user_provider_ids:
+            return
+        _user_logger_providers.append(provider)
+        _user_provider_ids.add(pid)
     logging.info("Added user logger provider to multi-delegate system")

16-63: Make emission robust: snapshot providers, pre-import SeverityNumber, and sanitize attributes.

  • Snapshot providers to avoid concurrent modification during iteration.
  • Import SeverityNumber once at module scope.
  • Only pass OTEL-supported attribute types; coerce others to str to avoid exporter errors.
-from opentelemetry._logs import SeverityNumber
+# at top-level
+from opentelemetry._logs import SeverityNumber
+_SEVERITY_MAP = {
+    logging.DEBUG: SeverityNumber.DEBUG,
+    logging.INFO: SeverityNumber.INFO,
+    logging.WARNING: SeverityNumber.WARN,
+    logging.ERROR: SeverityNumber.ERROR,
+    logging.CRITICAL: SeverityNumber.FATAL,
+}
@@
-    for provider in _user_logger_providers:
+    # Snapshot to avoid mutation during iteration
+    providers = list(_user_logger_providers)
+    for provider in providers:
@@
-            # Convert log level to OpenTelemetry severity
-            from opentelemetry._logs import SeverityNumber
-            
-            severity_map = {
-                logging.DEBUG: SeverityNumber.DEBUG,
-                logging.INFO: SeverityNumber.INFO,
-                logging.WARNING: SeverityNumber.WARN,
-                logging.ERROR: SeverityNumber.ERROR,
-                logging.CRITICAL: SeverityNumber.FATAL,
-            }
-            
-            severity = severity_map.get(record.levelno, SeverityNumber.INFO)
+            severity = _SEVERITY_MAP.get(record.levelno, SeverityNumber.INFO)
@@
-            for key, value in record.__dict__.items():
+            for key, value in record.__dict__.items():
                 if not key.startswith('_') and key not in ['name', 'msg', 'args', 'levelname', 'levelno', 'pathname', 'filename', 'module', 'lineno', 'funcName', 'created', 'msecs', 'relativeCreated', 'thread', 'threadName', 'processName', 'process']:
-                    attributes[key] = value
+                    if isinstance(value, (str, int, float, bool)):
+                        attributes[key] = value
+                    else:
+                        attributes[key] = str(value)
tests/otel/test_user_otel.py (2)

125-138: Narrow the import-error patch to reduce blast radius.

Patching builtins.import can mask unrelated imports. Prefer targeting the exporter import path.

-with patch('builtins.__import__', side_effect=ImportError("No module")):
+with patch('opentelemetry.exporter.otlp.proto.http._log_exporter.OTLPLogExporter', side_effect=ImportError("No module")):

190-232: Add assertions for processor/exporter shutdown once implemented.

If you adopt flushing processor/exporter in shutdown, add asserts for both to be called, and keep provider shutdown assert.

-            mock_provider.force_flush.assert_called_once()
-            mock_provider.shutdown.assert_called_once()
+            mock_provider.shutdown.assert_called_once()
+            # New if implemented
+            result = agentuity.otel._user_logger_provider
+            # Example if you expose processor/exporter mocks:
+            # mock_processor.force_flush.assert_called_once()
+            # mock_exporter.shutdown.assert_called_once()
tests/otel/test_user_logger.py (5)

15-21: Consider using a fixture or public API instead of directly mutating global state.

The tests directly call .clear() on the module-level _user_logger_providers list. This creates tight coupling to internal implementation details and makes the tests fragile to refactoring. If _user_logger_providers is later changed to a different data structure or moved behind an API, all these tests will break.

Consider one of these approaches:

  1. Add a public clear_user_logger_providers() function to the module
  2. Use a pytest fixture that automatically resets state:
@pytest.fixture(autouse=True)
def reset_user_logger_providers():
    _user_logger_providers.clear()
    yield
    _user_logger_providers.clear()

This pattern repeats in all test classes (lines 182-187, 211-217), so fixing it once would improve all tests.


48-60: Inconsistent LogRecord creation across tests.

This test creates a LogRecord without explicitly setting module and funcName attributes, but other tests (e.g., lines 79-80) manually set these attributes after creation. This inconsistency could mask bugs if the code under test depends on these attributes being present.

Consider creating a helper function to create fully-populated test LogRecords:

def create_test_log_record(level=logging.INFO, msg="Test message", **extra_attrs):
    record = logging.LogRecord(
        name="test",
        level=level,
        pathname="/path/test.py",
        lineno=42,
        msg=msg,
        args=(),
        exc_info=None
    )
    record.module = "test"
    record.funcName = "test_function"
    for key, value in extra_attrs.items():
        setattr(record, key, value)
    return record

244-257: Clarify what the duplicate handler test is verifying.

The test creates a grandchild logger (child1child2) but only checks child2.handlers. Due to Python's logging hierarchy, this might not be testing what's intended. The test should clarify:

  1. Are you checking that child2 doesn't have duplicate handlers on itself?
  2. Or that the handler isn't added redundantly when creating nested loggers?

Consider making the test more explicit:

         # Create logger twice
         child1 = create_logger(parent_logger, "child", {"attr1": "value1"})
         child2 = create_logger(child1, "grandchild", {"attr2": "value2"})
         
-        # Should only have one MultiDelegateHandler
+        # child1 should have exactly one MultiDelegateHandler
+        multi_handlers_child1 = [h for h in child1.handlers if isinstance(h, MultiDelegateHandler)]
+        assert len(multi_handlers_child1) == 1
+        
+        # child2 should also have exactly one (not duplicate when nested)
         multi_handlers = [h for h in child2.handlers if isinstance(h, MultiDelegateHandler)]
         assert len(multi_handlers) == 1

259-282: Test doesn't verify attributes in actual logging integration.

The test manually applies filters to a LogRecord, simulating what the logging framework does. However, this doesn't test the actual integration—whether attributes correctly flow through when a real log message is emitted via the logger.

Consider adding an integration test that actually logs and verifies the full flow:

def test_create_logger_attributes_in_actual_logging(self):
    """Test that attributes are added when actually logging."""
    mock_otel_logger = MagicMock()
    mock_provider = MagicMock()
    mock_provider.get_logger.return_value = mock_otel_logger
    
    add_user_logger_provider(mock_provider)
    
    parent_logger = logging.getLogger("test_parent")
    child = create_logger(parent_logger, "child", {"custom_attr": "test_value"})
    
    # Actually log a message
    child.info("Test message")
    
    # Verify the custom attribute was passed through to OTEL
    mock_otel_logger.emit.assert_called()
    call_kwargs = mock_otel_logger.emit.call_args.kwargs
    assert 'custom_attr' in call_kwargs['attributes']
    assert call_kwargs['attributes']['custom_attr'] == 'test_value'

1-282: Consider adding tests for timestamp conversion and message formatting.

The test coverage is good for the main flows, but some details are untested:

  1. Timestamp conversion (line 44 in emit_to_user_providers): No test verifies that record.created is correctly converted to nanoseconds
  2. Message body: No test verifies the body parameter passed to emit() matches record.getMessage()
  3. End-to-end integration: No test actually calls logger.info(), logger.error(), etc., and verifies the complete flow through to user providers

Consider adding tests like:

def test_emit_timestamp_conversion(self):
    """Verify timestamp is converted to nanoseconds."""
    mock_otel_logger = MagicMock()
    mock_provider = MagicMock()
    mock_provider.get_logger.return_value = mock_otel_logger
    add_user_logger_provider(mock_provider)
    
    record = create_test_log_record()
    record.created = 1234567890.123456  # seconds with microseconds
    
    emit_to_user_providers(record)
    
    call_kwargs = mock_otel_logger.emit.call_args.kwargs
    expected_ns = int(1234567890.123456 * 1_000_000_000)
    assert call_kwargs['timestamp'] == expected_ns

def test_integration_actual_logging(self):
    """Test complete flow with actual logging calls."""
    # Set up user provider
    mock_otel_logger = MagicMock()
    mock_provider = MagicMock()
    mock_provider.get_logger.return_value = mock_otel_logger
    add_user_logger_provider(mock_provider)
    
    # Create logger and log message
    logger = logging.getLogger("integration_test")
    child = create_logger(logger, "test", {"session_id": "abc123"})
    child.warning("Integration test message")
    
    # Verify OTEL emission happened with correct parameters
    mock_otel_logger.emit.assert_called_once()
    call_kwargs = mock_otel_logger.emit.call_args.kwargs
    assert call_kwargs['body'] == "Integration test message"
    assert 'session_id' in call_kwargs['attributes']
    assert call_kwargs['attributes']['session_id'] == "abc123"
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7aafb06 and 81ceedc.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (6)
  • agentuity/otel/__init__.py (4 hunks)
  • agentuity/otel/logger.py (2 hunks)
  • agentuity/server/__init__.py (2 hunks)
  • pyproject.toml (1 hunks)
  • tests/otel/test_user_logger.py (1 hunks)
  • tests/otel/test_user_otel.py (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
agentuity/server/__init__.py (1)
agentuity/otel/__init__.py (2)
  • init (113-214)
  • shutdown (217-232)
agentuity/otel/__init__.py (1)
agentuity/otel/logger.py (1)
  • add_user_logger_provider (9-13)
tests/otel/test_user_otel.py (1)
agentuity/otel/__init__.py (4)
  • parse_user_otel_config (28-56)
  • UserOpenTelemetryConfig (17-25)
  • create_user_logger_provider (63-110)
  • shutdown (217-232)
tests/otel/test_user_logger.py (1)
agentuity/otel/logger.py (6)
  • add_user_logger_provider (9-13)
  • emit_to_user_providers (16-62)
  • MultiDelegateHandler (65-70)
  • create_logger (73-104)
  • emit (68-70)
  • filter (90-94)
🔇 Additional comments (2)
agentuity/server/__init__.py (2)

17-17: Import rename LGTM.

Clear alias for shutdown; no issues.


751-762: Graceful shutdown wiring LGTM.

on_shutdown callback is correct; errors are contained; keeps server resilient.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
agentuity/otel/__init__.py (1)

163-201: Critical: resource_attributes may be undefined when Traceloop import fails

resource_attributes is created inside the try block but used later (Lines 209-211). If the import of traceloop fails before assignment, init will raise UnboundLocalError. Define resource_attributes outside the try and reuse it for Traceloop.init.

@@
-    # Initialize traceloop for automatic instrumentation
-    try:
-        from traceloop.sdk import Traceloop
-
-        headers = {"Authorization": f"Bearer {bearer_token}"} if bearer_token else {}
-
-        resource_attributes = {
-            SERVICE_NAME: config.get(
-                "service_name",
-                app_name,
-            ),
-            SERVICE_VERSION: config.get(
-                "service_version",
-                app_version,
-            ),
-            "@agentuity/orgId": orgId,
-            "@agentuity/projectId": projectId,
-            "@agentuity/deploymentId": deploymentId,
-            "@agentuity/env": environment,
-            "@agentuity/devmode": devmode,
-            "@agentuity/sdkVersion": sdkVersion,
-            "@agentuity/cliVersion": cliVersion,
-            "@agentuity/language": "python",
-            "env": "dev" if devmode else "production",
-            "version": __version__,
-        }
+    # Build base resource attributes once; use for Traceloop and user OTEL
+    resource_attributes = {
+        SERVICE_NAME: config.get("service_name", app_name),
+        SERVICE_VERSION: config.get("service_version", app_version),
+        "@agentuity/orgId": orgId,
+        "@agentuity/projectId": projectId,
+        "@agentuity/deploymentId": deploymentId,
+        "@agentuity/env": environment,
+        "@agentuity/devmode": devmode,
+        "@agentuity/sdkVersion": sdkVersion,
+        "@agentuity/cliVersion": cliVersion,
+        "@agentuity/language": "python",
+        "env": "dev" if devmode else "production",
+        "version": __version__,
+    }
+
+    # Initialize traceloop for automatic instrumentation
+    try:
+        from traceloop.sdk import Traceloop
+        headers = {"Authorization": f"Bearer {bearer_token}"} if bearer_token else {}
@@
         Traceloop.init(
             app_name=app_name,
             api_endpoint=endpoint,
             headers=headers,
             disable_batch=devmode,
             resource_attributes=resource_attributes,
             telemetry_enabled=False
         )

Also applies to: 209-211

♻️ Duplicate comments (1)
agentuity/otel/__init__.py (1)

225-231: Shutdown order: flush processor/exporter before provider

Call force_flush/shutdown on the processor and exporter before the provider to avoid tearing down the pipeline prematurely.

-        # Shutdown components in reverse order: provider, processor, exporter
-        components = [
-            ("provider", _user_logger_provider.get("provider")),
-            ("processor", _user_logger_provider.get("processor")),
-            ("exporter", _user_logger_provider.get("exporter"))
-        ]
+        # Flush and shutdown components in safe order: processor -> exporter -> provider
+        components = [
+            ("processor", _user_logger_provider.get("processor")),
+            ("exporter", _user_logger_provider.get("exporter")),
+            ("provider", _user_logger_provider.get("provider")),
+        ]

Also applies to: 236-258

🧹 Nitpick comments (3)
agentuity/otel/__init__.py (3)

79-86: Normalize endpoint when user supplies full OTLP path

If endpoint already ends with /v1/logs (or /v1), appending /v1/logs creates invalid URLs. Normalize to accept both base and full paths.

-        base_url = user_config.endpoint.rstrip('/')
-        logs_url = f"{base_url}/v1/logs"
+        base_url = user_config.endpoint.rstrip("/")
+        if base_url.endswith("/v1/logs"):
+            logs_url = base_url
+        elif base_url.endswith("/v1"):
+            logs_url = f"{base_url}/logs"
+        else:
+            logs_url = f"{base_url}/v1/logs"

119-121: Robust truthy parsing for AGENTUITY_OTLP_DISABLED

Accept common truthy forms (TRUE, True, 1, yes).

-    if os.environ.get("AGENTUITY_OTLP_DISABLED", "false") == "true":
+    if os.environ.get("AGENTUITY_OTLP_DISABLED", "false").strip().lower() in {"true", "1", "yes"}:

28-50: Config parsing niceties: aliases and type guards

Minor DX polish: accept service_name/resource_attributes aliases and guard dict types.

-        if not config_data.get("serviceName"):
+        service_name = config_data.get("serviceName") or config_data.get("service_name")
+        if not service_name:
             logger.warning("User OTEL config missing required 'serviceName' field, ignoring")
             return None
@@
-        return UserOpenTelemetryConfig(
-            endpoint=config_data["endpoint"],
-            service_name=config_data["serviceName"],
-            resource_attributes=config_data.get("resourceAttributes", {}),
-            headers=config_data.get("headers", {})
-        )
+        ra = config_data.get("resourceAttributes") or config_data.get("resource_attributes") or {}
+        if ra is not None and not isinstance(ra, dict):
+            logger.warning("User OTEL config 'resourceAttributes' must be an object; ignoring value")
+            ra = {}
+        hdrs = config_data.get("headers") or {}
+        if hdrs is not None and not isinstance(hdrs, dict):
+            logger.warning("User OTEL config 'headers' must be an object; ignoring value")
+            hdrs = {}
+        return UserOpenTelemetryConfig(
+            endpoint=config_data["endpoint"],
+            service_name=service_name,
+            resource_attributes=ra,
+            headers=hdrs,
+        )
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5ebd772 and 33cabdd.

📒 Files selected for processing (2)
  • agentuity/otel/__init__.py (4 hunks)
  • tests/otel/test_user_otel.py (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/otel/test_user_otel.py
🧰 Additional context used
🧬 Code graph analysis (1)
agentuity/otel/__init__.py (1)
agentuity/otel/logger.py (1)
  • add_user_logger_provider (13-24)
🔇 Additional comments (3)
agentuity/otel/__init__.py (3)

16-26: LGTM on UserOpenTelemetryConfig

Dataclass shape is clear and minimal; sensible defaults.


205-216: Remove the suggested refactor—no code paths call init multiple times

Verification found init() is called exactly once at agentuity/server/init.py:705 during server startup. No re-initialization paths exist in the codebase, so the defensive shutdown/idempotency logic is unnecessary.


63-91: Remove the suggested import path change; current private import is correct for 1.31.x

The public import path opentelemetry.exporter.otlp.proto.http.log_exporter does not exist in OpenTelemetry Python 1.31.x. OTLPLogExporter is only provided under the private module path (opentelemetry.exporter.otlp.proto.http._log_exporter). The current code is correct and requires no changes.

The optional gzip compression suggestion remains valid if needed for optimization.

Likely an incorrect or invalid review comment.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
tests/otel/test_user_logger.py (1)

119-166: Previous review feedback addressed; consider explicit attribute setting for clarity.

The test now correctly verifies severity_number mapping (lines 161-164), addressing the prior review comment. However, the LogRecord created in lines 143-151 doesn't explicitly set module and funcName, which emit_to_user_providers accesses. While Python's LogRecord initializes these from pathname, explicitly setting them (as done in test_emit_to_user_providers_success lines 79-80) would improve test clarity and robustness.

Apply this diff to explicitly set missing attributes:

             record = logging.LogRecord(
                 name="test",
                 level=log_level,
                 pathname="test.py",
                 lineno=1,
                 msg="Test message",
                 args=(),
                 exc_info=None
             )
+            record.module = "test"
+            record.funcName = "test_func"
             
             # Call emit_to_user_providers
             emit_to_user_providers(record)
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 33cabdd and 48057e3.

📒 Files selected for processing (1)
  • tests/otel/test_user_logger.py (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
tests/otel/test_user_logger.py (1)
agentuity/otel/logger.py (6)
  • add_user_logger_provider (13-24)
  • emit_to_user_providers (27-73)
  • MultiDelegateHandler (76-83)
  • create_logger (86-120)
  • emit (79-83)
  • filter (103-107)
🔇 Additional comments (2)
tests/otel/test_user_logger.py (2)

167-209: LGTM! Previous review feedback fully addressed.

The test now correctly verifies that custom attributes are included (lines 202-205) and private attributes are filtered out (lines 207-208), addressing the prior review comment. The implementation is clear and thorough.


211-261: LGTM!

The TestMultiDelegateHandler class correctly validates both the active path (delegates to emit_to_user_providers when providers exist) and the no-op path (does nothing when no providers are registered). The tests are clear and comprehensive.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
tests/otel/test_user_logger.py (1)

362-388: Test still doesn't properly verify retroactive handler attachment.

The test clears both child.handlers and _logger_registry (line 376), then creates a new logger child2 (line 379). When add_user_logger_provider is called (line 384), the retroactive logic cannot attach a handler to child because it was removed from the registry. Meanwhile, child2 gets a handler immediately from create_logger, so there's nothing retroactive to test.

To properly test retroactive attachment, the test should:

  1. Create a logger that's in the registry but lacks a handler
  2. Add a provider (triggering retroactive attachment)
  3. Verify the handler was retroactively added to that logger

Based on learnings

Apply this diff to fix the test:

     def test_retroactive_handler_attachment(self):
         """Test that handlers are retroactively attached to existing loggers when providers are added."""
+        from agentuity.otel.logger import _logger_registry
+        
         parent_logger = logging.getLogger("test_retroactive")
         
         # Create logger before adding any providers
         child = create_logger(parent_logger, "child", {"attr1": "value1"})
         
         # Should have one handler (the MultiDelegateHandler)
         assert len(child.handlers) == 1
         assert isinstance(child.handlers[0], MultiDelegateHandler)
         
-        # Clear handler and logger registry, then recreate logger without providers
+        # Remove handler to simulate logger in old state (before provider was added)
         child.handlers.clear()
-        from agentuity.otel.logger import _logger_registry
-        _logger_registry.clear()
+        assert len(child.handlers) == 0
+        # child should still be in _logger_registry at this point
+        assert child in _logger_registry
         
-        # Create logger again without any providers
-        child2 = create_logger(parent_logger, "child2", {"attr2": "value2"})
-        assert len(child2.handlers) == 1  # Should still get handler
-        
-        # Now add a provider - should not add duplicate handlers
+        # Add provider - should trigger retroactive attachment to child
         mock_provider = MagicMock()
         add_user_logger_provider(mock_provider)
         
-        # Should still have only one handler (no duplicates)
-        assert len(child2.handlers) == 1
-        assert isinstance(child2.handlers[0], MultiDelegateHandler)
+        # Verify handler was retroactively attached to child
+        assert len(child.handlers) == 1
+        assert isinstance(child.handlers[0], MultiDelegateHandler)
🧹 Nitpick comments (1)
tests/otel/test_user_logger.py (1)

119-166: Past review issue resolved, but consider adding missing attributes for consistency.

The severity mapping verification has been properly fixed and now correctly asserts that the expected severity_number is passed to otel_logger.emit() for each logging level.

However, for consistency with other tests (e.g., lines 79-80), consider setting module and funcName on the LogRecord:

         for log_level, expected_severity in severity_mapping.items():
             # Reset mock calls for clean test
             mock_otel_logger.reset_mock()
             mock_provider.reset_mock()
             
             record = logging.LogRecord(
                 name="test",
                 level=log_level,
                 pathname="test.py",
                 lineno=1,
                 msg="Test message",
                 args=(),
                 exc_info=None
             )
+            record.module = "test"
+            record.funcName = "test_function"
             
             # Call emit_to_user_providers
             emit_to_user_providers(record)
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 48057e3 and 0c4a7f0.

📒 Files selected for processing (1)
  • tests/otel/test_user_logger.py (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
tests/otel/test_user_logger.py (1)
agentuity/otel/logger.py (6)
  • add_user_logger_provider (13-24)
  • emit_to_user_providers (27-73)
  • MultiDelegateHandler (76-83)
  • create_logger (86-120)
  • emit (79-83)
  • filter (103-107)
🔇 Additional comments (7)
tests/otel/test_user_logger.py (7)

12-44: LGTM! Clean test setup and basic provider management.

The setup/teardown pattern properly isolates tests by clearing the provider registry, and the basic provider addition tests are correct and straightforward.


46-117: LGTM! Emission tests cover key scenarios.

The tests properly cover the no-provider path, successful emission, and error handling with appropriate verification of mocked calls and warning logs.


167-209: LGTM! Past review issue resolved.

The test now properly verifies that custom attributes are included in the emitted attributes and that private attributes (those starting with _) are correctly filtered out.


211-261: LGTM! Handler tests correctly verify conditional behavior.

The tests properly verify that MultiDelegateHandler delegates to emit_to_user_providers when providers exist and no-ops when no providers are registered.


263-299: LGTM! Basic create_logger tests are comprehensive.

The tests correctly verify that create_logger adds a MultiDelegateHandler in both scenarios (with and without user providers), with proper assertions on handler count, type, and level.


300-336: LGTM! Past review issue resolved.

The test has been properly updated and now correctly verifies duplicate handler prevention. It simulates a scenario where a handler already exists and confirms that calling create_logger again doesn't add another handler (stays at 2, not 3).


337-361: LGTM! Attributes filter test is well-designed.

The test properly verifies that the ContextFilter created by create_logger correctly adds custom attributes to log records.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (2)
tests/otel/test_user_logger.py (2)

119-166: Consider setting module and funcName explicitly for consistency.

In this test, LogRecord is created without explicitly setting module and funcName (line 143-151). While Python derives module from pathname, funcName defaults to None. For consistency with other tests (e.g., lines 79-80) and clearer test intent, explicitly set these attributes.

Apply this diff to improve consistency:

         for log_level, expected_severity in severity_mapping.items():
             # Reset mock calls for clean test
             mock_otel_logger.reset_mock()
             mock_provider.reset_mock()
             
             record = logging.LogRecord(
                 name="test",
                 level=log_level,
                 pathname="test.py",
                 lineno=1,
                 msg="Test message",
                 args=(),
                 exc_info=None
             )
+            record.module = "test"
+            record.funcName = "test_func"
             
             # Call emit_to_user_providers
             emit_to_user_providers(record)

167-209: Consider setting module and funcName explicitly for consistency.

Similar to the severity mapping test, this test creates a LogRecord without explicitly setting module and funcName. While functional, explicitly setting these attributes improves consistency and test clarity.

Apply this diff:

         record = logging.LogRecord(
             name="test",
             level=logging.INFO,
             pathname="test.py",
             lineno=1,
             msg="Test message",
             args=(),
             exc_info=None
         )
+        record.module = "test"
+        record.funcName = "test_func"
         
         # Add custom attributes
         record.custom_attr = "custom_value"
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0c4a7f0 and 361456d.

📒 Files selected for processing (1)
  • tests/otel/test_user_logger.py (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
tests/otel/test_user_logger.py (1)
agentuity/otel/logger.py (6)
  • add_user_logger_provider (13-24)
  • emit_to_user_providers (27-73)
  • MultiDelegateHandler (76-83)
  • create_logger (86-120)
  • emit (79-83)
  • filter (103-107)
🔇 Additional comments (4)
tests/otel/test_user_logger.py (4)

1-21: LGTM! Proper test isolation with setup/teardown.

The imports are appropriate, and the setup/teardown methods correctly clear _user_logger_providers to ensure test isolation.


211-261: LGTM! Comprehensive coverage of MultiDelegateHandler behavior.

The tests correctly verify both scenarios:

  • Handler delegates to emit_to_user_providers when providers exist
  • Handler no-ops when no providers exist

300-336: LGTM! Duplicate prevention logic correctly tested.

The test properly verifies that create_logger doesn't add a duplicate MultiDelegateHandler when one already exists:

  1. Creates logger (1 handler)
  2. Manually adds another handler to simulate existing handlers (2 handlers)
  3. Calls create_logger again and verifies it doesn't add a 3rd handler (still 2)

The logic correctly addresses the previous review feedback.


362-384: LGTM! Retroactive attachment correctly tested.

The test properly verifies retroactive handler attachment:

  1. Creates logger with handler via create_logger (registers in _logger_registry)
  2. Removes handler to simulate a logger in old state
  3. Adds provider, triggering retroactive attachment logic in add_user_logger_provider
  4. Verifies handler was retroactively attached

The test correctly addresses the previous review feedback.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants