Skip to content

Add client-side telemetry hooks with OpenTelemetry integration and ad-hoc capture#133

Draft
saurabhrb wants to merge 13 commits intomainfrom
feature/capture-telemetry
Draft

Add client-side telemetry hooks with OpenTelemetry integration and ad-hoc capture#133
saurabhrb wants to merge 13 commits intomainfrom
feature/capture-telemetry

Conversation

@saurabhrb
Copy link
Contributor

@saurabhrb saurabhrb commented Mar 10, 2026

Summary

Adds a complete, opt-in telemetry infrastructure that lets users hook into the SDK's HTTP request lifecycle, integrate with OpenTelemetry for tracing and metrics, and use Python's standard logging -- all with zero overhead when disabled.

New Public API

Type Name Description
Dataclass TelemetryConfig Frozen config for enabling signals (tracing, metrics, logging) and registering hooks
Class TelemetryHook Concrete base class for custom telemetry integrations (on_request_start, on_request_end, on_request_error, get_additional_headers)
Dataclass RequestContext Typed request data passed to hooks (method, URL, table, operation)
Dataclass ResponseContext Typed response data passed to hooks (status code, duration, headers, service request ID)
Context Manager client.capture_telemetry() Lightweight ad-hoc capture of HTTP request details without requiring full telemetry setup
Dataclass CapturedRequest Request details captured by capture_telemetry()

Internal Components

  • TelemetryManager / NoOpTelemetryManager -- instrumentation engine with factory; returns a zero-overhead no-op when telemetry is disabled
  • _operation_scope -- ContextVar-based mechanism to propagate operation names (e.g., records.create) from namespace methods to _request()
  • _TrackedRequest -- internal span tracking (not exposed to hooks)

Key Design Decisions

  • Zero overhead when disabled: NoOpTelemetryManager yields None for true zero-allocation overhead
  • Hook safety: hook-provided headers use case-insensitive comparison to prevent overwriting SDK-managed headers (Authorization, OData, correlation)
  • No global state mutation: internal _log_level threshold filters log emissions instead of calling logger.setLevel()
  • OpenTelemetry is optional: installed via pip install PowerPlatform-Dataverse-Client[telemetry]; hooks work standalone without OTel

Operation Instrumentation

All operation namespaces are instrumented with _operation_scope:

  • records (create, read, update, delete, upsert)
  • query (sql, fetch_page)
  • tables (get_columns, create_one_to_many, create_many_to_many)
  • files (upload, download)

Changes

  • New: src/PowerPlatform/Dataverse/core/telemetry.py -- core telemetry engine (617 lines)
  • New: examples/telemetry_demo.py -- runnable demo with console/Jaeger exporters
  • New: tests/unit/core/test_telemetry.py -- comprehensive test suite (758 lines)
  • Modified: client.py -- adds capture_telemetry() method, wires telemetry manager
  • Modified: data/_odata.py -- instruments _request() with trace/record/hook dispatch
  • Modified: operations/*.py -- wraps operations with _operation_scope
  • Modified: core/config.py -- adds telemetry field to DataverseConfig
  • Modified: common/constants.py -- adds telemetry-related constants
  • Modified: pyproject.toml -- adds [telemetry] optional dependency group
  • Modified: README.md -- adds telemetry & observability section
  • Modified: SKILL.md (sdk-use) -- adds telemetry usage docs

tpellissier and others added 13 commits February 25, 2026 08:31
Adds a complete telemetry infrastructure that lets users hook into the
SDK's HTTP request lifecycle, integrate with OpenTelemetry for
tracing/metrics, and use Python's standard logging -- all opt-in with
zero overhead when disabled.

Key components:
- TelemetryConfig: frozen dataclass for configuring signals and hooks
- TelemetryHook: concrete base class for custom telemetry integrations
- TelemetryManager / NoOpTelemetryManager: instrumentation engine with
  factory that returns a zero-overhead no-op when telemetry is disabled
- RequestContext / ResponseContext: typed data passed to hooks
- _operation_scope: ContextVar-based mechanism to propagate operation
  names (e.g. "records.create") from namespace methods to _request()

Design improvements over the initial draft:
- TelemetryHook as concrete base class (not broken @runtime_checkable Protocol)
- _span removed from user-facing RequestContext (kept internal via _TrackedRequest)
- No except block in trace_request() -- eliminates double error dispatch
- Per-subsystem try/except in record_response() for exception safety
- NoOp yields None for true zero allocation overhead
- Explicit span status (OK/ERROR) on every response
- Safe log_level parsing with fallback

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Resolve conflicts in operations layer: combine telemetry
_operation_scope wrapping (branch) with typed return models
Record/TableInfo/RelationshipInfo (main). Both features now work
together - operations are instrumented AND return typed objects.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Remove unused imports in test_telemetry.py (time, Mock)
- Use env vars for org URL/tenant ID in telemetry_demo.py
- Normalize None header values to empty string in _odata.py
- Include network exceptions (error is not None) in error count metric
- Scope _operation_scope per page fetch, not across yield points
- Dispatch on_request_error hook for network exceptions
- Update SKILL.md docs to match on_request_error behavior

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add skipUnless(_OTEL_AVAILABLE) to 3 tests that require opentelemetry
- Clarify on_request_error docstring: covers both HTTP and network errors
- Fix SKILL.md: separate request vs response fields in hook data docs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add table_name arg to create_one_to_many (referencing_entity) and
create_many_to_many (entity1_logical_name) so telemetry traces and
metrics can be filtered by table.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Hook-provided headers now only set headers that aren't already present,
preventing hooks from silently overwriting Authorization, OData, or
correlation headers.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Hook header merge now uses case-insensitive comparison to prevent
  hooks from bypassing SDK headers via different casing
- Remove redundant 'pip install opentelemetry-sdk' from demo
  (already included in [telemetry] extra)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add DataverseClient import to README and SKILL.md telemetry snippets
- Stop calling logger.setLevel() which mutates global logging state;
  use internal _log_level threshold to filter emissions instead
- Update test to check _log_level instead of logger.level

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Guard against Optional[Dict] type by assigning headers = ... or {}
before iterating, preventing potential None iteration if the type
annotation is ever accurate.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds client.capture_telemetry() -- a lightweight context manager that
captures HTTP request details without requiring full telemetry hook
setup. Works even when no TelemetryConfig is set on the client.

Usage:
    with client.capture_telemetry() as t:
        record_id = client.records.create("account", {"name": "Contoso"})
    print(t.requests[-1].service_request_id)

New types: TelemetryCapture (hook subclass), CapturedRequest (dataclass).
When telemetry is disabled (NoOp), temporarily swaps in a real manager
for the duration of the capture block, then restores NoOp on exit.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@saurabhrb saurabhrb changed the title [DRAFT] Feature/capture telemetry Add client-side telemetry hooks with OpenTelemetry integration and ad-hoc capture Mar 20, 2026
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