From 2bd5ce5703c4b7e6eafbfb9ac8e80389c21b70d9 Mon Sep 17 00:00:00 2001 From: Bar Haim Date: Tue, 2 Dec 2025 13:13:22 +0200 Subject: [PATCH 01/10] Add Tools Telemetry Exporter plugin Signed-off-by: Bar Haim --- plugins/config.yaml | 16 ++ plugins/tools_telemetry_exporter/__init__.py | 3 + .../plugin-manifest.yaml | 9 + .../telemetry_exporter.py | 178 ++++++++++++++++++ 4 files changed, 206 insertions(+) create mode 100644 plugins/tools_telemetry_exporter/__init__.py create mode 100644 plugins/tools_telemetry_exporter/plugin-manifest.yaml create mode 100644 plugins/tools_telemetry_exporter/telemetry_exporter.py diff --git a/plugins/config.yaml b/plugins/config.yaml index 7c821daf6..b0de682ed 100644 --- a/plugins/config.yaml +++ b/plugins/config.yaml @@ -899,3 +899,19 @@ plugins: api_key: "" # optional, can define ANTHROPIC_API_KEY instead model_id: "ibm/granite-3-3-8b-instruct" # note that this changes depending on provider length_threshold: 100000 + + # Tools Telemetry Exporter - export tool invocation telemetry to OpenTelemetry + - name: "ToolsTelemetryExporter" + kind: "plugins.tools_telemetry_exporter.telemetry_exporter.ToolsTelemetryExporterPlugin" + description: "Export comprehensive tool invocation telemetry to OpenTelemetry" + version: "0.1.0" + author: "Bar Haim" + hooks: ["tool_pre_invoke", "tool_post_invoke"] + tags: ["telemetry", "observability", "opentelemetry", "monitoring"] + mode: "permissive" # enforce | permissive | disabled + priority: 200 # Run late to capture all context + conditions: [] # Apply to all tools + config: + export_full_payload: true + max_payload_bytes_size: 10000 + diff --git a/plugins/tools_telemetry_exporter/__init__.py b/plugins/tools_telemetry_exporter/__init__.py new file mode 100644 index 000000000..7ece0b2a2 --- /dev/null +++ b/plugins/tools_telemetry_exporter/__init__.py @@ -0,0 +1,3 @@ +from plugins.tools_telemetry_exporter.telemetry_exporter import ToolsTelemetryExporterPlugin + +__all__ = ["ToolsTelemetryExporterPlugin"] diff --git a/plugins/tools_telemetry_exporter/plugin-manifest.yaml b/plugins/tools_telemetry_exporter/plugin-manifest.yaml new file mode 100644 index 000000000..a7fde4055 --- /dev/null +++ b/plugins/tools_telemetry_exporter/plugin-manifest.yaml @@ -0,0 +1,9 @@ +description: "Export comprehensive tool invocation telemetry to OpenTelemetry" +author: "Bar Haim" +version: "0.1.0" +available_hooks: + - "tool_pre_invoke" + - "tool_post_invoke" +default_configs: + export_full_payload: true + max_payload_bytes_size: 10000 # (10 KB default) diff --git a/plugins/tools_telemetry_exporter/telemetry_exporter.py b/plugins/tools_telemetry_exporter/telemetry_exporter.py new file mode 100644 index 000000000..3a306ae7b --- /dev/null +++ b/plugins/tools_telemetry_exporter/telemetry_exporter.py @@ -0,0 +1,178 @@ +# -*- coding: utf-8 -*- +"""Location: ./plugins/tools_telemetry_exporter/telemetry_exporter.py +Copyright 2025 +SPDX-License-Identifier: Apache-2.0 + +Tools Telemetry Exporter Plugin. +This plugin exports comprehensive tool invocation telemetry to OpenTelemetry. +""" + +# Standard +from typing import Dict +import json + +# First-Party +from mcpgateway.plugins.framework import (Plugin, + PluginConfig, + PluginContext) +from mcpgateway.plugins.framework.constants import GATEWAY_METADATA, TOOL_METADATA +from mcpgateway.plugins.framework.hooks.tools import ( + ToolPreInvokePayload, + ToolPreInvokeResult, + ToolPostInvokePayload, + ToolPostInvokeResult +) +from mcpgateway.common.models import Tool, Gateway +from mcpgateway.services.logging_service import LoggingService + + +# Initialize logging service first +logging_service = LoggingService() +logger = logging_service.get_logger(__name__) + + +class ToolsTelemetryExporterPlugin(Plugin): + """Export comprehensive tool invocation telemetry to OpenTelemetry.""" + def __init__(self, config: PluginConfig): + super().__init__(config) + self.is_open_telemetry_available = self._is_open_telemetry_available() + self.telemetry_config = config.config + + @staticmethod + def _is_open_telemetry_available() -> bool: + try: + # Third-Party + from opentelemetry import trace + from opentelemetry import context + return True + except ImportError: + logger.warning("ToolsTelemetryExporter: OpenTelemetry is not available. Telemetry export will be disabled.") + return False + + @staticmethod + def _get_base_context_attributes(context: PluginContext) -> Dict: + global_context = context.global_context + return { + "request_id": global_context.request_id or "", + "user": global_context.user or "", + "tenant_id": global_context.tenant_id or "", + "server_id": global_context.server_id or "", + } + + def _get_pre_invoke_context_attributes(self, context: PluginContext) -> Dict: + global_context = context.global_context + tool_metadata: Tool = global_context.metadata.get(TOOL_METADATA) + gateway_metadata: Gateway = global_context.metadata.get(GATEWAY_METADATA) + + return { + **self._get_base_context_attributes(context), + "tool": { + "name": tool_metadata.name or "", + "target_tool_name": tool_metadata.original_name or "", + "description": tool_metadata.description or "", + }, + "gateway": { + "id": gateway_metadata.id or "", + "name": gateway_metadata.name or "", + "target_mcp_server": str(gateway_metadata.url or ""), + } + } + + def _get_post_invoke_context_attributes(self, context: PluginContext) -> Dict: + return { + **self._get_base_context_attributes(context), + } + + async def tool_pre_invoke(self, payload: ToolPreInvokePayload, context: PluginContext) -> ToolPreInvokeResult: + """Capture pre-invocation telemetry for tools. + + Args: + payload: The tool payload containing arguments. + context: Plugin execution context. + + Returns: + Result with potentially modified tool arguments. + """ + logger.info("ToolsTelemetryExporter: Capturing pre-invocation tool telemetry.") + context_attributes = self._get_pre_invoke_context_attributes(context) + + export_attributes = { + "request_id": context_attributes["request_id"], + "user": context_attributes["user"], + "tenant_id": context_attributes["tenant_id"], + "server_id": context_attributes["server_id"], + "gateway.id": context_attributes["gateway"]["id"], + "gateway.name": context_attributes["gateway"]["name"], + "gateway.target_mcp_server": context_attributes["gateway"]["target_mcp_server"], + "tool.name": context_attributes["tool"]["name"], + "tool.target_tool_name": context_attributes["tool"]["target_tool_name"], + "tool.description": context_attributes["tool"]["description"], + "tool.invocation.args": json.dumps(payload.args), + "headers": payload.headers.model_dump_json() + } + + await self._export_telemetry(attributes=export_attributes, span_name="tool.pre_invoke") + return ToolPreInvokeResult(continue_processing=True) + + async def tool_post_invoke(self, payload: ToolPostInvokePayload, context: PluginContext) -> ToolPostInvokeResult: + """Capture post-invocation telemetry. + + Args: + payload: Tool result payload containing the tool name and execution result. + context: Plugin context with state from pre-invoke hook. + + Returns: + ToolPostInvokeResult allowing execution to continue. + """ + logger.info("ToolsTelemetryExporter: Capturing post-invocation tool telemetry.") + context_attributes = self._get_post_invoke_context_attributes(context) + + export_attributes = { + "request_id": context_attributes["request_id"], + "user": context_attributes["user"], + "tenant_id": context_attributes["tenant_id"], + "server_id": context_attributes["server_id"], + } + + result = payload.result if payload.result else {} + has_error = result.get("isError", True) + if self.telemetry_config.get("export_full_payload", False) and not has_error: + max_payload_bytes_size = self.telemetry_config.get("max_payload_bytes_size", 10000) + result_content = result.get("content") + if result_content: + result_content_str = json.dumps(result_content, default=str) + if len(result_content_str) <= max_payload_bytes_size: + export_attributes["tool.invocation.result"] = result_content_str + else: + truncated_content = result_content_str[:max_payload_bytes_size] + export_attributes["tool.invocation.result"] = truncated_content + "..." + else: + export_attributes["tool.invocation.result"] = "" + export_attributes["tool.invocation.has_error"] = has_error + + await self._export_telemetry(attributes=export_attributes, span_name="tool.post_invoke") + return ToolPostInvokeResult(continue_processing=True) + + + async def _export_telemetry(self, attributes: Dict, span_name: str) -> None: + """Export telemetry attributes to OpenTelemetry. + + Args: + attributes: Dictionary of telemetry attributes to export. + span_name: Name of the OpenTelemetry span to create. + """ + if not self.is_open_telemetry_available: + logger.debug("ToolsTelemetryExporter: OpenTelemetry not available. Skipping telemetry export.") + return + + from opentelemetry import trace + + try: + tracer = trace.get_tracer(__name__) + + with tracer.start_as_current_span(span_name) as span: + for key, value in attributes.items(): + span.set_attribute(key, value) + logger.debug(f"ToolsTelemetryExporter: Exported telemetry for span '{span_name}' with attributes: {attributes}") + except Exception as e: + logger.error(f"ToolsTelemetryExporter: Error creating span '{span_name}': {e}", exc_info=True) From 1ad30009c51edffc0db791a643cd156cbbcea212 Mon Sep 17 00:00:00 2001 From: Bar Haim Date: Tue, 2 Dec 2025 14:12:34 +0200 Subject: [PATCH 02/10] Add Tools Telemetry Exporter plugin Signed-off-by: Bar Haim --- plugins/tools_telemetry_exporter/__init__.py | 2 + .../telemetry_exporter.py | 43 ++++++++----------- 2 files changed, 20 insertions(+), 25 deletions(-) diff --git a/plugins/tools_telemetry_exporter/__init__.py b/plugins/tools_telemetry_exporter/__init__.py index 7ece0b2a2..0d20317b7 100644 --- a/plugins/tools_telemetry_exporter/__init__.py +++ b/plugins/tools_telemetry_exporter/__init__.py @@ -1,3 +1,5 @@ +"""Tools Telemetry Exporter Plugin - exports tool invocation telemetry to OpenTelemetry.""" + from plugins.tools_telemetry_exporter.telemetry_exporter import ToolsTelemetryExporterPlugin __all__ = ["ToolsTelemetryExporterPlugin"] diff --git a/plugins/tools_telemetry_exporter/telemetry_exporter.py b/plugins/tools_telemetry_exporter/telemetry_exporter.py index 3a306ae7b..3544bece4 100644 --- a/plugins/tools_telemetry_exporter/telemetry_exporter.py +++ b/plugins/tools_telemetry_exporter/telemetry_exporter.py @@ -8,24 +8,16 @@ """ # Standard -from typing import Dict import json +from typing import Dict # First-Party -from mcpgateway.plugins.framework import (Plugin, - PluginConfig, - PluginContext) +from mcpgateway.common.models import Gateway, Tool +from mcpgateway.plugins.framework import Plugin, PluginConfig, PluginContext from mcpgateway.plugins.framework.constants import GATEWAY_METADATA, TOOL_METADATA -from mcpgateway.plugins.framework.hooks.tools import ( - ToolPreInvokePayload, - ToolPreInvokeResult, - ToolPostInvokePayload, - ToolPostInvokeResult -) -from mcpgateway.common.models import Tool, Gateway +from mcpgateway.plugins.framework.hooks.tools import ToolPostInvokePayload, ToolPostInvokeResult, ToolPreInvokePayload, ToolPreInvokeResult from mcpgateway.services.logging_service import LoggingService - # Initialize logging service first logging_service = LoggingService() logger = logging_service.get_logger(__name__) @@ -33,7 +25,8 @@ class ToolsTelemetryExporterPlugin(Plugin): """Export comprehensive tool invocation telemetry to OpenTelemetry.""" - def __init__(self, config: PluginConfig): + + def __init__(self, config: PluginConfig): super().__init__(config) self.is_open_telemetry_available = self._is_open_telemetry_available() self.telemetry_config = config.config @@ -42,8 +35,8 @@ def __init__(self, config: PluginConfig): def _is_open_telemetry_available() -> bool: try: # Third-Party - from opentelemetry import trace - from opentelemetry import context + from opentelemetry import trace # noqa: F401 # pylint: disable=import-outside-toplevel,unused-import + return True except ImportError: logger.warning("ToolsTelemetryExporter: OpenTelemetry is not available. Telemetry export will be disabled.") @@ -75,7 +68,7 @@ def _get_pre_invoke_context_attributes(self, context: PluginContext) -> Dict: "id": gateway_metadata.id or "", "name": gateway_metadata.name or "", "target_mcp_server": str(gateway_metadata.url or ""), - } + }, } def _get_post_invoke_context_attributes(self, context: PluginContext) -> Dict: @@ -108,7 +101,7 @@ async def tool_pre_invoke(self, payload: ToolPreInvokePayload, context: PluginCo "tool.target_tool_name": context_attributes["tool"]["target_tool_name"], "tool.description": context_attributes["tool"]["description"], "tool.invocation.args": json.dumps(payload.args), - "headers": payload.headers.model_dump_json() + "headers": payload.headers.model_dump_json(), } await self._export_telemetry(attributes=export_attributes, span_name="tool.pre_invoke") @@ -117,13 +110,13 @@ async def tool_pre_invoke(self, payload: ToolPreInvokePayload, context: PluginCo async def tool_post_invoke(self, payload: ToolPostInvokePayload, context: PluginContext) -> ToolPostInvokeResult: """Capture post-invocation telemetry. - Args: - payload: Tool result payload containing the tool name and execution result. - context: Plugin context with state from pre-invoke hook. + Args: + payload: Tool result payload containing the tool name and execution result. + context: Plugin context with state from pre-invoke hook. - Returns: - ToolPostInvokeResult allowing execution to continue. - """ + Returns: + ToolPostInvokeResult allowing execution to continue. + """ logger.info("ToolsTelemetryExporter: Capturing post-invocation tool telemetry.") context_attributes = self._get_post_invoke_context_attributes(context) @@ -153,7 +146,6 @@ async def tool_post_invoke(self, payload: ToolPostInvokePayload, context: Plugin await self._export_telemetry(attributes=export_attributes, span_name="tool.post_invoke") return ToolPostInvokeResult(continue_processing=True) - async def _export_telemetry(self, attributes: Dict, span_name: str) -> None: """Export telemetry attributes to OpenTelemetry. @@ -165,7 +157,8 @@ async def _export_telemetry(self, attributes: Dict, span_name: str) -> None: logger.debug("ToolsTelemetryExporter: OpenTelemetry not available. Skipping telemetry export.") return - from opentelemetry import trace + # Third-Party + from opentelemetry import trace # pylint: disable=import-outside-toplevel try: tracer = trace.get_tracer(__name__) From f8c1c1bb8463f967c1930c5a5b3581a1eb41102a Mon Sep 17 00:00:00 2001 From: Bar Haim Date: Tue, 2 Dec 2025 14:19:09 +0200 Subject: [PATCH 03/10] Add Tools Telemetry Exporter plugin Signed-off-by: Bar Haim --- .../telemetry_exporter.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/plugins/tools_telemetry_exporter/telemetry_exporter.py b/plugins/tools_telemetry_exporter/telemetry_exporter.py index 3544bece4..56ba474b0 100644 --- a/plugins/tools_telemetry_exporter/telemetry_exporter.py +++ b/plugins/tools_telemetry_exporter/telemetry_exporter.py @@ -55,7 +55,7 @@ def _get_base_context_attributes(context: PluginContext) -> Dict: def _get_pre_invoke_context_attributes(self, context: PluginContext) -> Dict: global_context = context.global_context tool_metadata: Tool = global_context.metadata.get(TOOL_METADATA) - gateway_metadata: Gateway = global_context.metadata.get(GATEWAY_METADATA) + target_mcp_server_metadata: Gateway = global_context.metadata.get(GATEWAY_METADATA) return { **self._get_base_context_attributes(context), @@ -64,10 +64,10 @@ def _get_pre_invoke_context_attributes(self, context: PluginContext) -> Dict: "target_tool_name": tool_metadata.original_name or "", "description": tool_metadata.description or "", }, - "gateway": { - "id": gateway_metadata.id or "", - "name": gateway_metadata.name or "", - "target_mcp_server": str(gateway_metadata.url or ""), + "target_mcp_server": { + "id": target_mcp_server_metadata.id or "", + "name": target_mcp_server_metadata.name or "", + "url": str(target_mcp_server_metadata.url or ""), }, } @@ -94,9 +94,9 @@ async def tool_pre_invoke(self, payload: ToolPreInvokePayload, context: PluginCo "user": context_attributes["user"], "tenant_id": context_attributes["tenant_id"], "server_id": context_attributes["server_id"], - "gateway.id": context_attributes["gateway"]["id"], - "gateway.name": context_attributes["gateway"]["name"], - "gateway.target_mcp_server": context_attributes["gateway"]["target_mcp_server"], + "target_mcp_server.id": context_attributes["target_mcp_server"]["id"], + "target_mcp_server.name": context_attributes["target_mcp_server"]["name"], + "target_mcp_server.url": context_attributes["target_mcp_server"]["url"], "tool.name": context_attributes["tool"]["name"], "tool.target_tool_name": context_attributes["tool"]["target_tool_name"], "tool.description": context_attributes["tool"]["description"], From 1a93b3971df336bd6a042b191c9a98dff3249202 Mon Sep 17 00:00:00 2001 From: Bar Haim Date: Tue, 2 Dec 2025 14:27:13 +0200 Subject: [PATCH 04/10] Add Tools Telemetry Exporter plugin Signed-off-by: Bar Haim --- plugins/tools_telemetry_exporter/telemetry_exporter.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/plugins/tools_telemetry_exporter/telemetry_exporter.py b/plugins/tools_telemetry_exporter/telemetry_exporter.py index 56ba474b0..fb68b6f6e 100644 --- a/plugins/tools_telemetry_exporter/telemetry_exporter.py +++ b/plugins/tools_telemetry_exporter/telemetry_exporter.py @@ -162,6 +162,10 @@ async def _export_telemetry(self, attributes: Dict, span_name: str) -> None: try: tracer = trace.get_tracer(__name__) + current_span = trace.get_current_span() + if not current_span or not current_span.is_recording(): + logger.warning("ToolsTelemetryExporter: No active span found. Skipping telemetry export.") + return with tracer.start_as_current_span(span_name) as span: for key, value in attributes.items(): From faf032f03bd7b214e2b84234ce1d73a8c3313859 Mon Sep 17 00:00:00 2001 From: Bar Haim Date: Tue, 2 Dec 2025 14:27:58 +0200 Subject: [PATCH 05/10] Tools Telemetry Exporter disabled by default Signed-off-by: Bar Haim --- plugins/config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/config.yaml b/plugins/config.yaml index b0de682ed..e1b36dca2 100644 --- a/plugins/config.yaml +++ b/plugins/config.yaml @@ -908,7 +908,7 @@ plugins: author: "Bar Haim" hooks: ["tool_pre_invoke", "tool_post_invoke"] tags: ["telemetry", "observability", "opentelemetry", "monitoring"] - mode: "permissive" # enforce | permissive | disabled + mode: "disabled" # enforce | permissive | disabled priority: 200 # Run late to capture all context conditions: [] # Apply to all tools config: From 747e0e6eb5117246cc64106a49f61250bcaaa1ea Mon Sep 17 00:00:00 2001 From: Bar Haim Date: Tue, 2 Dec 2025 14:33:41 +0200 Subject: [PATCH 06/10] Missing docstring Signed-off-by: Bar Haim --- plugins/tools_telemetry_exporter/telemetry_exporter.py | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/tools_telemetry_exporter/telemetry_exporter.py b/plugins/tools_telemetry_exporter/telemetry_exporter.py index fb68b6f6e..aaece24b1 100644 --- a/plugins/tools_telemetry_exporter/telemetry_exporter.py +++ b/plugins/tools_telemetry_exporter/telemetry_exporter.py @@ -27,6 +27,7 @@ class ToolsTelemetryExporterPlugin(Plugin): """Export comprehensive tool invocation telemetry to OpenTelemetry.""" def __init__(self, config: PluginConfig): + """Initialize the ToolsTelemetryExporterPlugin.""" super().__init__(config) self.is_open_telemetry_available = self._is_open_telemetry_available() self.telemetry_config = config.config From d645d1d3e7fb4e613b82befd46e4843bb6dc40ef Mon Sep 17 00:00:00 2001 From: Bar Haim Date: Tue, 2 Dec 2025 14:39:30 +0200 Subject: [PATCH 07/10] Missing docstring Signed-off-by: Bar Haim --- .../telemetry_exporter.py | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/plugins/tools_telemetry_exporter/telemetry_exporter.py b/plugins/tools_telemetry_exporter/telemetry_exporter.py index aaece24b1..18c267605 100644 --- a/plugins/tools_telemetry_exporter/telemetry_exporter.py +++ b/plugins/tools_telemetry_exporter/telemetry_exporter.py @@ -34,6 +34,11 @@ def __init__(self, config: PluginConfig): @staticmethod def _is_open_telemetry_available() -> bool: + """Check if OpenTelemetry is available for import. + + Returns: + True if OpenTelemetry can be imported, False otherwise. + """ try: # Third-Party from opentelemetry import trace # noqa: F401 # pylint: disable=import-outside-toplevel,unused-import @@ -45,6 +50,14 @@ def _is_open_telemetry_available() -> bool: @staticmethod def _get_base_context_attributes(context: PluginContext) -> Dict: + """Extract base context attributes from plugin context. + + Args: + context: Plugin execution context containing global context. + + Returns: + Dictionary with base attributes (request_id, user, tenant_id, server_id). + """ global_context = context.global_context return { "request_id": global_context.request_id or "", @@ -54,6 +67,14 @@ def _get_base_context_attributes(context: PluginContext) -> Dict: } def _get_pre_invoke_context_attributes(self, context: PluginContext) -> Dict: + """Extract pre-invocation context attributes including tool and gateway metadata. + + Args: + context: Plugin execution context containing tool and gateway metadata. + + Returns: + Dictionary with base attributes plus tool and target MCP server details. + """ global_context = context.global_context tool_metadata: Tool = global_context.metadata.get(TOOL_METADATA) target_mcp_server_metadata: Gateway = global_context.metadata.get(GATEWAY_METADATA) @@ -73,6 +94,14 @@ def _get_pre_invoke_context_attributes(self, context: PluginContext) -> Dict: } def _get_post_invoke_context_attributes(self, context: PluginContext) -> Dict: + """Extract post-invocation context attributes. + + Args: + context: Plugin execution context. + + Returns: + Dictionary with base context attributes for post-invocation telemetry. + """ return { **self._get_base_context_attributes(context), } From cca9d69c23b7ec4506cfb8371d9f5c3bb3d02b2a Mon Sep 17 00:00:00 2001 From: Bar Haim Date: Tue, 2 Dec 2025 14:44:54 +0200 Subject: [PATCH 08/10] newline Signed-off-by: Bar Haim --- plugins/config.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/config.yaml b/plugins/config.yaml index e1b36dca2..16a423f02 100644 --- a/plugins/config.yaml +++ b/plugins/config.yaml @@ -914,4 +914,3 @@ plugins: config: export_full_payload: true max_payload_bytes_size: 10000 - From bcff3cae9b9a7bc8a43e93b4465b7635a599cec2 Mon Sep 17 00:00:00 2001 From: Bar Haim Date: Tue, 2 Dec 2025 15:06:20 +0200 Subject: [PATCH 09/10] README Signed-off-by: Bar Haim --- plugins/tools_telemetry_exporter/README.md | 74 ++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 plugins/tools_telemetry_exporter/README.md diff --git a/plugins/tools_telemetry_exporter/README.md b/plugins/tools_telemetry_exporter/README.md new file mode 100644 index 000000000..74e5214bd --- /dev/null +++ b/plugins/tools_telemetry_exporter/README.md @@ -0,0 +1,74 @@ +# Tools Telemetry Exporter Plugin + +> Author: Bar Haim +> Version: 0.1.0 + +Export comprehensive tool invocation telemetry to OpenTelemetry for observability and monitoring. + +## Hooks +- `tool_pre_invoke` +- `tool_post_invoke` + +## Config +```yaml +config: + export_full_payload: true + max_payload_bytes_size: 10000 # 10 KB default +``` + +## Features + +- **Pre-Invocation Telemetry**: Captures request context, tool metadata, target MCP server details, and tool arguments +- **Post-Invocation Telemetry**: Captures request context, tool results (optional), and error status +- **Automatic Payload Truncation**: Large results are truncated to respect size limits +- **Graceful Degradation**: Automatically disables if OpenTelemetry is not available + +## Exported Attributes + +### Pre-Invocation (`tool.pre_invoke`) +- Request metadata: `request_id`, `user`, `tenant_id`, `server_id` +- Target server: `target_mcp_server.id`, `target_mcp_server.name`, `target_mcp_server.url` +- Tool info: `tool.name`, `tool.target_tool_name`, `tool.description` +- Invocation data: `tool.invocation.args`, `headers` + +### Post-Invocation (`tool.post_invoke`) +- Request metadata: `request_id`, `user`, `tenant_id`, `server_id` +- Results: `tool.invocation.result` (if `export_full_payload` is enabled and no error) +- Status: `tool.invocation.has_error` + +## Configuration Options + +| Option | Default | Description | +|--------|---------|-------------| +| `export_full_payload` | `true` | Export full tool results in post-invocation telemetry | +| `max_payload_bytes_size` | `10000` | Maximum payload size in bytes before truncation | + +## Requirements + +OpenTelemetry enabled on MCP context forge (see [Observability Setup](../../docs/docs/manage/observability.md#opentelemetry-external)). + + +## Usage + +```yaml +plugins: + - name: "ToolsTelemetryExporter" + kind: "plugins.tools_telemetry_exporter.telemetry_exporter.ToolsTelemetryExporterPlugin" + hooks: ["tool_pre_invoke", "tool_post_invoke"] + mode: "permissive" + priority: 200 # Run late to capture all context + config: + export_full_payload: true + max_payload_bytes_size: 10000 +``` + +## Limitations + +- Requires active OpenTelemetry tracing to export telemetry +- No local buffering; telemetry exported in real-time only + +## Security Notes + +- Tool arguments are always exported in pre-invocation telemetry +- Consider running PII filter plugin before this plugin to sanitize data +- Disable `export_full_payload` in production for sensitive workloads From e56d9de52620c47a8c2e8fc5495ee463a3c2ad93 Mon Sep 17 00:00:00 2001 From: Bar Haim Date: Tue, 2 Dec 2025 15:08:13 +0200 Subject: [PATCH 10/10] README Signed-off-by: Bar Haim --- docs/docs/using/plugins/plugins.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/docs/using/plugins/plugins.md b/docs/docs/using/plugins/plugins.md index 660afdce3..5d861224f 100644 --- a/docs/docs/using/plugins/plugins.md +++ b/docs/docs/using/plugins/plugins.md @@ -6,6 +6,7 @@ MCP Context Forge provides a comprehensive collection of production-ready plugin - [Security & Safety](#security-safety) - [Reliability & Performance](#reliability-performance) +- [Observability & Monitoring](#observability-monitoring) - [Content Transformation & Formatting](#content-transformation-formatting) - [Content Filtering & Validation](#content-filtering-validation) - [Compliance & Governance](#compliance-governance) @@ -44,6 +45,14 @@ Plugins for improving system reliability, performance, and resource management. | [Response Cache by Prompt](https://github.com/IBM/mcp-context-forge/tree/main/plugins/response_cache_by_prompt) | Native | Advisory response cache using cosine similarity over prompt/input fields with configurable threshold | | [Retry with Backoff](https://github.com/IBM/mcp-context-forge/tree/main/plugins/retry_with_backoff) | Native | Annotates retry/backoff policy in metadata with exponential backoff on specific HTTP status codes | +## Observability & Monitoring + +Plugins for telemetry, tracing, and monitoring tool invocations. + +| Plugin | Type | Description | +|--------|------|-------------| +| [Tools Telemetry Exporter](https://github.com/IBM/mcp-context-forge/tree/main/plugins/tools_telemetry_exporter) | Native | Export comprehensive tool invocation telemetry to OpenTelemetry for observability and monitoring with configurable payload export | + ## Content Transformation & Formatting Plugins for transforming, formatting, and normalizing content.