Skip to content

refactor(plugins): decouple plugin framework from gateway observability service#2827

Open
araujof wants to merge 5 commits intomainfrom
refactor/plugins_observability
Open

refactor(plugins): decouple plugin framework from gateway observability service#2827
araujof wants to merge 5 commits intomainfrom
refactor/plugins_observability

Conversation

@araujof
Copy link
Member

@araujof araujof commented Feb 11, 2026

Description

Removes the plugin framework's direct dependency on mcpgateway.db.SessionLocal and mcpgateway.services.observability_service by introducing a protocol-based observability abstraction with dependency injection.

Closes: #2828

Problem

The plugin framework's _execute_with_timeout method imported SessionLocal and ObservabilityService directly from the gateway, creating a tight coupling that prevented the framework from operating standalone. This also required the plugin framework to manage database sessions for tracing — a concern that belongs to the host application.

Solution

  • Introduced ObservabilityProvider protocol and NullObservability no-op default in a new observability.py module
  • Added a current_trace_id ContextVar owned by the plugin framework (replacing the one imported from the gateway's observability service)
  • PluginExecutor and PluginManager now accept an optional observability parameter via constructor injection
  • get_plugin_manager() forwards the observability provider to the manager
  • The host application (mcpgateway) can inject its own implementation at startup; the framework operates without tracing when none is provided

Dependency direction (before → after)

# Before: circular
plugin framework ──imports──> mcpgateway.db, mcpgateway.services.observability_service

# After: clean inversion
mcpgateway ──injects──> plugin framework (ObservabilityProvider)

Changes

File Change
mcpgateway/plugins/framework/observability.py NewObservabilityProvider protocol, NullObservability default, current_trace_id ContextVar
mcpgateway/plugins/framework/manager.py PluginExecutor and PluginManager accept observability param; _execute_with_timeout uses injected provider instead of direct gateway imports
mcpgateway/plugins/framework/__init__.py get_plugin_manager() accepts observability param; ObservabilityProvider added to __all__
tests/unit/.../test_observability.py New — 5 tests: DI injection with tracing, no trace_id, no provider, NullObservability, executor injection
tests/unit/.../test_manager_coverage.py Updated TestExecuteWithTimeout to test against injected provider instead of mocking mcpgateway.db/mcpgateway.services

Additional Changes

A few tests in tests/unit/../test_http_auth_integration.py were failing on dev environment when running the coverage target. Solution: Reset Starlette's cached middleware_stack after injecting the mock plugin manager so the middleware chain is rebuilt with the mock, and restore it afterward to avoid side effects on other tests.

Test plan

  • All unit tests pass
  • All doctests pass
  • New observability tests verify: span creation/completion with injected provider, graceful no-op without trace_id, graceful no-op without provider, provider failure fallback

@araujof araujof marked this pull request as draft February 11, 2026 02:17
@araujof araujof added enhancement New feature or request plugins SHOULD P2: Important but not vital; high-value items that are not crucial for the immediate release labels Feb 11, 2026
@araujof araujof force-pushed the refactor/plugins_observability branch from 086f205 to bb1bb88 Compare February 11, 2026 06:32
@araujof araujof marked this pull request as ready for review February 11, 2026 07:06
@crivetimihai crivetimihai added this to the Release 1.0.0-GA milestone Feb 11, 2026
@araujof araujof requested a review from terylt February 13, 2026 02:19
Copy link
Member

@crivetimihai crivetimihai left a comment

Choose a reason for hiding this comment

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

Thanks for this clean decoupling work, @araujof. The Protocol + DI approach is the right pattern. Two critical issues:

1. Two independent current_trace_id ContextVars (silent tracing breakage)
mcpgateway/plugins/framework/observability.py:15 creates a new ContextVar("current_trace_id"), but the gateway's observability middleware sets the original one at mcpgateway/services/observability_service.py:53. These are two distinct Python objects despite sharing the same name string. After this PR, _execute_with_timeout imports from the plugin framework's copy, which the middleware never populates — trace_id will always be None at runtime and plugin spans silently stop recording.

2. No gateway-side adapter wiring
There is no class implementing ObservabilityProvider that bridges to ObservabilityService. And main.py:187 still calls PluginManager(_config_file) without passing observability=. So even after merge, self.observability will be None on the executor, making this a no-op. The PR needs a concrete adapter and it must be passed at the main.py call site.

Minor: The Borg singleton PluginManager.__init__ only sets _executor.observability inside the not self._initialized block, so a second call with a different provider silently drops it.

@araujof
Copy link
Member Author

araujof commented Feb 13, 2026

Thanks for this clean decoupling work, @araujof. The Protocol + DI approach is the right pattern. Two critical issues:

1. Two independent current_trace_id ContextVars (silent tracing breakage) mcpgateway/plugins/framework/observability.py:15 creates a new ContextVar("current_trace_id"), but the gateway's observability middleware sets the original one at mcpgateway/services/observability_service.py:53. These are two distinct Python objects despite sharing the same name string. After this PR, _execute_with_timeout imports from the plugin framework's copy, which the middleware never populates — trace_id will always be None at runtime and plugin spans silently stop recording.

2. No gateway-side adapter wiring There is no class implementing ObservabilityProvider that bridges to ObservabilityService. And main.py:187 still calls PluginManager(_config_file) without passing observability=. So even after merge, self.observability will be None on the executor, making this a no-op. The PR needs a concrete adapter and it must be passed at the main.py call site.

Minor: The Borg singleton PluginManager.__init__ only sets _executor.observability inside the not self._initialized block, so a second call with a different provider silently drops it.

Thanks, working on it.

@araujof araujof force-pushed the refactor/plugins_observability branch from 3a9742b to 005d3ce Compare February 13, 2026 18:28
@araujof araujof requested a review from crivetimihai February 14, 2026 00:11
…ion for observability service

Signed-off-by: Frederico Araujo <frederico.araujo@ibm.com>
Signed-off-by: Frederico Araujo <frederico.araujo@ibm.com>
Signed-off-by: Frederico Araujo <frederico.araujo@ibm.com>
Signed-off-by: Frederico Araujo <frederico.araujo@ibm.com>
@araujof araujof force-pushed the refactor/plugins_observability branch from 961137b to 2033885 Compare February 14, 2026 00:29
Signed-off-by: Frederico Araujo <frederico.araujo@ibm.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request plugins SHOULD P2: Important but not vital; high-value items that are not crucial for the immediate release

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEATURE]: Remove observability service dependency from plugin framework

3 participants