mcp-hangar 0.2.0__py3-none-any.whl
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.
- mcp_hangar/__init__.py +139 -0
- mcp_hangar/application/__init__.py +1 -0
- mcp_hangar/application/commands/__init__.py +67 -0
- mcp_hangar/application/commands/auth_commands.py +118 -0
- mcp_hangar/application/commands/auth_handlers.py +296 -0
- mcp_hangar/application/commands/commands.py +59 -0
- mcp_hangar/application/commands/handlers.py +189 -0
- mcp_hangar/application/discovery/__init__.py +21 -0
- mcp_hangar/application/discovery/discovery_metrics.py +283 -0
- mcp_hangar/application/discovery/discovery_orchestrator.py +497 -0
- mcp_hangar/application/discovery/lifecycle_manager.py +315 -0
- mcp_hangar/application/discovery/security_validator.py +414 -0
- mcp_hangar/application/event_handlers/__init__.py +50 -0
- mcp_hangar/application/event_handlers/alert_handler.py +191 -0
- mcp_hangar/application/event_handlers/audit_handler.py +203 -0
- mcp_hangar/application/event_handlers/knowledge_base_handler.py +120 -0
- mcp_hangar/application/event_handlers/logging_handler.py +69 -0
- mcp_hangar/application/event_handlers/metrics_handler.py +152 -0
- mcp_hangar/application/event_handlers/persistent_audit_store.py +217 -0
- mcp_hangar/application/event_handlers/security_handler.py +604 -0
- mcp_hangar/application/mcp/tooling.py +158 -0
- mcp_hangar/application/ports/__init__.py +9 -0
- mcp_hangar/application/ports/observability.py +237 -0
- mcp_hangar/application/queries/__init__.py +52 -0
- mcp_hangar/application/queries/auth_handlers.py +237 -0
- mcp_hangar/application/queries/auth_queries.py +118 -0
- mcp_hangar/application/queries/handlers.py +227 -0
- mcp_hangar/application/read_models/__init__.py +11 -0
- mcp_hangar/application/read_models/provider_views.py +139 -0
- mcp_hangar/application/sagas/__init__.py +11 -0
- mcp_hangar/application/sagas/group_rebalance_saga.py +137 -0
- mcp_hangar/application/sagas/provider_failover_saga.py +266 -0
- mcp_hangar/application/sagas/provider_recovery_saga.py +172 -0
- mcp_hangar/application/services/__init__.py +9 -0
- mcp_hangar/application/services/provider_service.py +208 -0
- mcp_hangar/application/services/traced_provider_service.py +211 -0
- mcp_hangar/bootstrap/runtime.py +328 -0
- mcp_hangar/context.py +178 -0
- mcp_hangar/domain/__init__.py +117 -0
- mcp_hangar/domain/contracts/__init__.py +57 -0
- mcp_hangar/domain/contracts/authentication.py +225 -0
- mcp_hangar/domain/contracts/authorization.py +229 -0
- mcp_hangar/domain/contracts/event_store.py +178 -0
- mcp_hangar/domain/contracts/metrics_publisher.py +59 -0
- mcp_hangar/domain/contracts/persistence.py +383 -0
- mcp_hangar/domain/contracts/provider_runtime.py +146 -0
- mcp_hangar/domain/discovery/__init__.py +20 -0
- mcp_hangar/domain/discovery/conflict_resolver.py +267 -0
- mcp_hangar/domain/discovery/discovered_provider.py +185 -0
- mcp_hangar/domain/discovery/discovery_service.py +412 -0
- mcp_hangar/domain/discovery/discovery_source.py +192 -0
- mcp_hangar/domain/events.py +433 -0
- mcp_hangar/domain/exceptions.py +525 -0
- mcp_hangar/domain/model/__init__.py +70 -0
- mcp_hangar/domain/model/aggregate.py +58 -0
- mcp_hangar/domain/model/circuit_breaker.py +152 -0
- mcp_hangar/domain/model/event_sourced_api_key.py +413 -0
- mcp_hangar/domain/model/event_sourced_provider.py +423 -0
- mcp_hangar/domain/model/event_sourced_role_assignment.py +268 -0
- mcp_hangar/domain/model/health_tracker.py +183 -0
- mcp_hangar/domain/model/load_balancer.py +185 -0
- mcp_hangar/domain/model/provider.py +810 -0
- mcp_hangar/domain/model/provider_group.py +656 -0
- mcp_hangar/domain/model/tool_catalog.py +105 -0
- mcp_hangar/domain/policies/__init__.py +19 -0
- mcp_hangar/domain/policies/provider_health.py +187 -0
- mcp_hangar/domain/repository.py +249 -0
- mcp_hangar/domain/security/__init__.py +85 -0
- mcp_hangar/domain/security/input_validator.py +710 -0
- mcp_hangar/domain/security/rate_limiter.py +387 -0
- mcp_hangar/domain/security/roles.py +237 -0
- mcp_hangar/domain/security/sanitizer.py +387 -0
- mcp_hangar/domain/security/secrets.py +501 -0
- mcp_hangar/domain/services/__init__.py +20 -0
- mcp_hangar/domain/services/audit_service.py +376 -0
- mcp_hangar/domain/services/image_builder.py +328 -0
- mcp_hangar/domain/services/provider_launcher.py +1046 -0
- mcp_hangar/domain/value_objects.py +1138 -0
- mcp_hangar/errors.py +818 -0
- mcp_hangar/fastmcp_server.py +1105 -0
- mcp_hangar/gc.py +134 -0
- mcp_hangar/infrastructure/__init__.py +79 -0
- mcp_hangar/infrastructure/async_executor.py +133 -0
- mcp_hangar/infrastructure/auth/__init__.py +37 -0
- mcp_hangar/infrastructure/auth/api_key_authenticator.py +388 -0
- mcp_hangar/infrastructure/auth/event_sourced_store.py +567 -0
- mcp_hangar/infrastructure/auth/jwt_authenticator.py +360 -0
- mcp_hangar/infrastructure/auth/middleware.py +340 -0
- mcp_hangar/infrastructure/auth/opa_authorizer.py +243 -0
- mcp_hangar/infrastructure/auth/postgres_store.py +659 -0
- mcp_hangar/infrastructure/auth/projections.py +366 -0
- mcp_hangar/infrastructure/auth/rate_limiter.py +311 -0
- mcp_hangar/infrastructure/auth/rbac_authorizer.py +323 -0
- mcp_hangar/infrastructure/auth/sqlite_store.py +624 -0
- mcp_hangar/infrastructure/command_bus.py +112 -0
- mcp_hangar/infrastructure/discovery/__init__.py +110 -0
- mcp_hangar/infrastructure/discovery/docker_source.py +289 -0
- mcp_hangar/infrastructure/discovery/entrypoint_source.py +249 -0
- mcp_hangar/infrastructure/discovery/filesystem_source.py +383 -0
- mcp_hangar/infrastructure/discovery/kubernetes_source.py +247 -0
- mcp_hangar/infrastructure/event_bus.py +260 -0
- mcp_hangar/infrastructure/event_sourced_repository.py +443 -0
- mcp_hangar/infrastructure/event_store.py +396 -0
- mcp_hangar/infrastructure/knowledge_base/__init__.py +259 -0
- mcp_hangar/infrastructure/knowledge_base/contracts.py +202 -0
- mcp_hangar/infrastructure/knowledge_base/memory.py +177 -0
- mcp_hangar/infrastructure/knowledge_base/postgres.py +545 -0
- mcp_hangar/infrastructure/knowledge_base/sqlite.py +513 -0
- mcp_hangar/infrastructure/metrics_publisher.py +36 -0
- mcp_hangar/infrastructure/observability/__init__.py +10 -0
- mcp_hangar/infrastructure/observability/langfuse_adapter.py +534 -0
- mcp_hangar/infrastructure/persistence/__init__.py +33 -0
- mcp_hangar/infrastructure/persistence/audit_repository.py +371 -0
- mcp_hangar/infrastructure/persistence/config_repository.py +398 -0
- mcp_hangar/infrastructure/persistence/database.py +333 -0
- mcp_hangar/infrastructure/persistence/database_common.py +330 -0
- mcp_hangar/infrastructure/persistence/event_serializer.py +280 -0
- mcp_hangar/infrastructure/persistence/event_upcaster.py +166 -0
- mcp_hangar/infrastructure/persistence/in_memory_event_store.py +150 -0
- mcp_hangar/infrastructure/persistence/recovery_service.py +312 -0
- mcp_hangar/infrastructure/persistence/sqlite_event_store.py +386 -0
- mcp_hangar/infrastructure/persistence/unit_of_work.py +409 -0
- mcp_hangar/infrastructure/persistence/upcasters/README.md +13 -0
- mcp_hangar/infrastructure/persistence/upcasters/__init__.py +7 -0
- mcp_hangar/infrastructure/query_bus.py +153 -0
- mcp_hangar/infrastructure/saga_manager.py +401 -0
- mcp_hangar/logging_config.py +209 -0
- mcp_hangar/metrics.py +1007 -0
- mcp_hangar/models.py +31 -0
- mcp_hangar/observability/__init__.py +54 -0
- mcp_hangar/observability/health.py +487 -0
- mcp_hangar/observability/metrics.py +319 -0
- mcp_hangar/observability/tracing.py +433 -0
- mcp_hangar/progress.py +542 -0
- mcp_hangar/retry.py +613 -0
- mcp_hangar/server/__init__.py +120 -0
- mcp_hangar/server/__main__.py +6 -0
- mcp_hangar/server/auth_bootstrap.py +340 -0
- mcp_hangar/server/auth_cli.py +335 -0
- mcp_hangar/server/auth_config.py +305 -0
- mcp_hangar/server/bootstrap.py +735 -0
- mcp_hangar/server/cli.py +161 -0
- mcp_hangar/server/config.py +224 -0
- mcp_hangar/server/context.py +215 -0
- mcp_hangar/server/http_auth_middleware.py +165 -0
- mcp_hangar/server/lifecycle.py +467 -0
- mcp_hangar/server/state.py +117 -0
- mcp_hangar/server/tools/__init__.py +16 -0
- mcp_hangar/server/tools/discovery.py +186 -0
- mcp_hangar/server/tools/groups.py +75 -0
- mcp_hangar/server/tools/health.py +301 -0
- mcp_hangar/server/tools/provider.py +939 -0
- mcp_hangar/server/tools/registry.py +320 -0
- mcp_hangar/server/validation.py +113 -0
- mcp_hangar/stdio_client.py +229 -0
- mcp_hangar-0.2.0.dist-info/METADATA +347 -0
- mcp_hangar-0.2.0.dist-info/RECORD +160 -0
- mcp_hangar-0.2.0.dist-info/WHEEL +4 -0
- mcp_hangar-0.2.0.dist-info/entry_points.txt +2 -0
- mcp_hangar-0.2.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
"""OpenTelemetry tracing for MCP Hangar.
|
|
2
|
+
|
|
3
|
+
Provides distributed tracing with automatic context propagation
|
|
4
|
+
through tool invocations and provider calls.
|
|
5
|
+
|
|
6
|
+
Configuration via environment variables:
|
|
7
|
+
OTEL_EXPORTER_OTLP_ENDPOINT: OTLP endpoint (default: http://localhost:4317)
|
|
8
|
+
OTEL_SERVICE_NAME: Service name (default: mcp-hangar)
|
|
9
|
+
OTEL_TRACES_SAMPLER: Sampler type (default: always_on)
|
|
10
|
+
MCP_TRACING_ENABLED: Enable/disable tracing (default: true)
|
|
11
|
+
|
|
12
|
+
Example:
|
|
13
|
+
from mcp_hangar.observability.tracing import init_tracing, get_tracer
|
|
14
|
+
|
|
15
|
+
# Initialize once at startup
|
|
16
|
+
init_tracing()
|
|
17
|
+
|
|
18
|
+
# Get tracer for module
|
|
19
|
+
tracer = get_tracer(__name__)
|
|
20
|
+
|
|
21
|
+
# Create spans
|
|
22
|
+
with tracer.start_as_current_span("my_operation") as span:
|
|
23
|
+
span.set_attribute("key", "value")
|
|
24
|
+
do_work()
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from contextlib import contextmanager
|
|
28
|
+
from functools import wraps
|
|
29
|
+
import os
|
|
30
|
+
from typing import Any, Callable, Dict, Optional, TypeVar
|
|
31
|
+
|
|
32
|
+
from mcp_hangar.logging_config import get_logger
|
|
33
|
+
|
|
34
|
+
logger = get_logger(__name__)
|
|
35
|
+
|
|
36
|
+
# Type variable for generic decorator
|
|
37
|
+
F = TypeVar("F", bound=Callable[..., Any])
|
|
38
|
+
|
|
39
|
+
# Global state
|
|
40
|
+
_tracer_provider = None
|
|
41
|
+
_initialized = False
|
|
42
|
+
|
|
43
|
+
# Check if OpenTelemetry is available
|
|
44
|
+
try:
|
|
45
|
+
from opentelemetry.sdk.resources import Resource, SERVICE_NAME
|
|
46
|
+
from opentelemetry.sdk.trace.export import BatchSpanProcessor, ConsoleSpanExporter
|
|
47
|
+
from opentelemetry.sdk.trace import TracerProvider
|
|
48
|
+
from opentelemetry import trace
|
|
49
|
+
from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator
|
|
50
|
+
from opentelemetry.trace import Span, Status, StatusCode, Tracer
|
|
51
|
+
|
|
52
|
+
OTEL_AVAILABLE = True
|
|
53
|
+
except ImportError:
|
|
54
|
+
OTEL_AVAILABLE = False
|
|
55
|
+
trace = None
|
|
56
|
+
Tracer = None
|
|
57
|
+
Span = None
|
|
58
|
+
|
|
59
|
+
# Try to import OTLP exporter
|
|
60
|
+
try:
|
|
61
|
+
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
|
|
62
|
+
|
|
63
|
+
OTLP_AVAILABLE = True
|
|
64
|
+
except ImportError:
|
|
65
|
+
OTLP_AVAILABLE = False
|
|
66
|
+
OTLPSpanExporter = None
|
|
67
|
+
|
|
68
|
+
# Try to import Jaeger exporter
|
|
69
|
+
try:
|
|
70
|
+
from opentelemetry.exporter.jaeger.thrift import JaegerExporter
|
|
71
|
+
|
|
72
|
+
JAEGER_AVAILABLE = True
|
|
73
|
+
except ImportError:
|
|
74
|
+
JAEGER_AVAILABLE = False
|
|
75
|
+
JaegerExporter = None
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class NoOpSpan:
|
|
79
|
+
"""No-op span for when tracing is disabled."""
|
|
80
|
+
|
|
81
|
+
def set_attribute(self, key: str, value: Any) -> None:
|
|
82
|
+
pass
|
|
83
|
+
|
|
84
|
+
def set_status(self, status: Any) -> None:
|
|
85
|
+
pass
|
|
86
|
+
|
|
87
|
+
def record_exception(self, exception: Exception) -> None:
|
|
88
|
+
pass
|
|
89
|
+
|
|
90
|
+
def add_event(self, name: str, attributes: Optional[Dict] = None) -> None:
|
|
91
|
+
pass
|
|
92
|
+
|
|
93
|
+
def __enter__(self) -> "NoOpSpan":
|
|
94
|
+
return self
|
|
95
|
+
|
|
96
|
+
def __exit__(self, *args) -> None:
|
|
97
|
+
pass
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class NoOpTracer:
|
|
101
|
+
"""No-op tracer for when tracing is disabled."""
|
|
102
|
+
|
|
103
|
+
def start_as_current_span(self, name: str, **kwargs) -> NoOpSpan:
|
|
104
|
+
return NoOpSpan()
|
|
105
|
+
|
|
106
|
+
@contextmanager
|
|
107
|
+
def start_span(self, name: str, **kwargs):
|
|
108
|
+
yield NoOpSpan()
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
_noop_tracer = NoOpTracer()
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def is_tracing_enabled() -> bool:
|
|
115
|
+
"""Check if tracing is enabled."""
|
|
116
|
+
enabled = os.getenv("MCP_TRACING_ENABLED", "true").lower()
|
|
117
|
+
return enabled in ("true", "1", "yes") and OTEL_AVAILABLE
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def init_tracing(
|
|
121
|
+
service_name: str = "mcp-hangar",
|
|
122
|
+
otlp_endpoint: Optional[str] = None,
|
|
123
|
+
jaeger_host: Optional[str] = None,
|
|
124
|
+
jaeger_port: int = 6831,
|
|
125
|
+
console_export: bool = False,
|
|
126
|
+
) -> bool:
|
|
127
|
+
"""Initialize OpenTelemetry tracing.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
service_name: Service name for traces.
|
|
131
|
+
otlp_endpoint: OTLP collector endpoint (gRPC).
|
|
132
|
+
jaeger_host: Jaeger agent host for UDP export.
|
|
133
|
+
jaeger_port: Jaeger agent port.
|
|
134
|
+
console_export: Enable console span export (for debugging).
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
True if tracing was initialized, False otherwise.
|
|
138
|
+
"""
|
|
139
|
+
global _tracer_provider, _initialized
|
|
140
|
+
|
|
141
|
+
if _initialized:
|
|
142
|
+
logger.debug("tracing_already_initialized")
|
|
143
|
+
return True
|
|
144
|
+
|
|
145
|
+
if not OTEL_AVAILABLE:
|
|
146
|
+
logger.info(
|
|
147
|
+
"tracing_disabled_otel_not_available",
|
|
148
|
+
hint="Install opentelemetry-api and opentelemetry-sdk",
|
|
149
|
+
)
|
|
150
|
+
return False
|
|
151
|
+
|
|
152
|
+
if not is_tracing_enabled():
|
|
153
|
+
logger.info("tracing_disabled_by_config")
|
|
154
|
+
return False
|
|
155
|
+
|
|
156
|
+
try:
|
|
157
|
+
# Get endpoint from env or parameter
|
|
158
|
+
otlp_endpoint = otlp_endpoint or os.getenv("OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4317")
|
|
159
|
+
|
|
160
|
+
# Create resource with service info
|
|
161
|
+
resource = Resource.create(
|
|
162
|
+
{
|
|
163
|
+
SERVICE_NAME: service_name,
|
|
164
|
+
"service.version": _get_version(),
|
|
165
|
+
"deployment.environment": os.getenv("MCP_ENVIRONMENT", "development"),
|
|
166
|
+
}
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
# Create tracer provider
|
|
170
|
+
_tracer_provider = TracerProvider(resource=resource)
|
|
171
|
+
|
|
172
|
+
# Add exporters
|
|
173
|
+
exporters_added = 0
|
|
174
|
+
|
|
175
|
+
# OTLP exporter (preferred)
|
|
176
|
+
if OTLP_AVAILABLE and otlp_endpoint:
|
|
177
|
+
try:
|
|
178
|
+
otlp_exporter = OTLPSpanExporter(endpoint=otlp_endpoint, insecure=True)
|
|
179
|
+
_tracer_provider.add_span_processor(BatchSpanProcessor(otlp_exporter))
|
|
180
|
+
exporters_added += 1
|
|
181
|
+
logger.info("tracing_otlp_exporter_added", endpoint=otlp_endpoint)
|
|
182
|
+
except Exception as e:
|
|
183
|
+
logger.warning("tracing_otlp_exporter_failed", error=str(e))
|
|
184
|
+
|
|
185
|
+
# Jaeger exporter (fallback)
|
|
186
|
+
if JAEGER_AVAILABLE and jaeger_host:
|
|
187
|
+
try:
|
|
188
|
+
jaeger_exporter = JaegerExporter(
|
|
189
|
+
agent_host_name=jaeger_host,
|
|
190
|
+
agent_port=jaeger_port,
|
|
191
|
+
)
|
|
192
|
+
_tracer_provider.add_span_processor(BatchSpanProcessor(jaeger_exporter))
|
|
193
|
+
exporters_added += 1
|
|
194
|
+
logger.info(
|
|
195
|
+
"tracing_jaeger_exporter_added",
|
|
196
|
+
host=jaeger_host,
|
|
197
|
+
port=jaeger_port,
|
|
198
|
+
)
|
|
199
|
+
except Exception as e:
|
|
200
|
+
logger.warning("tracing_jaeger_exporter_failed", error=str(e))
|
|
201
|
+
|
|
202
|
+
# Console exporter (debugging)
|
|
203
|
+
if console_export:
|
|
204
|
+
console_exporter = ConsoleSpanExporter()
|
|
205
|
+
_tracer_provider.add_span_processor(BatchSpanProcessor(console_exporter))
|
|
206
|
+
exporters_added += 1
|
|
207
|
+
logger.info("tracing_console_exporter_added")
|
|
208
|
+
|
|
209
|
+
if exporters_added == 0:
|
|
210
|
+
logger.warning("tracing_no_exporters_configured")
|
|
211
|
+
return False
|
|
212
|
+
|
|
213
|
+
# Set global tracer provider
|
|
214
|
+
trace.set_tracer_provider(_tracer_provider)
|
|
215
|
+
_initialized = True
|
|
216
|
+
|
|
217
|
+
logger.info(
|
|
218
|
+
"tracing_initialized",
|
|
219
|
+
service_name=service_name,
|
|
220
|
+
exporters=exporters_added,
|
|
221
|
+
)
|
|
222
|
+
return True
|
|
223
|
+
|
|
224
|
+
except Exception as e:
|
|
225
|
+
logger.error("tracing_initialization_failed", error=str(e))
|
|
226
|
+
return False
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def shutdown_tracing() -> None:
|
|
230
|
+
"""Shutdown tracing and flush pending spans."""
|
|
231
|
+
global _tracer_provider, _initialized
|
|
232
|
+
|
|
233
|
+
if _tracer_provider is not None:
|
|
234
|
+
try:
|
|
235
|
+
_tracer_provider.shutdown()
|
|
236
|
+
logger.info("tracing_shutdown_complete")
|
|
237
|
+
except Exception as e:
|
|
238
|
+
logger.warning("tracing_shutdown_error", error=str(e))
|
|
239
|
+
finally:
|
|
240
|
+
_tracer_provider = None
|
|
241
|
+
_initialized = False
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def get_tracer(name: str = __name__) -> Any:
|
|
245
|
+
"""Get a tracer instance.
|
|
246
|
+
|
|
247
|
+
Args:
|
|
248
|
+
name: Tracer name (usually __name__).
|
|
249
|
+
|
|
250
|
+
Returns:
|
|
251
|
+
OpenTelemetry tracer or NoOpTracer if disabled.
|
|
252
|
+
"""
|
|
253
|
+
if not _initialized or not OTEL_AVAILABLE:
|
|
254
|
+
return _noop_tracer
|
|
255
|
+
|
|
256
|
+
return trace.get_tracer(name)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def trace_tool_invocation(
|
|
260
|
+
provider_id: str,
|
|
261
|
+
tool_name: str,
|
|
262
|
+
timeout: float,
|
|
263
|
+
) -> Callable[[F], F]:
|
|
264
|
+
"""Decorator to trace tool invocations.
|
|
265
|
+
|
|
266
|
+
Args:
|
|
267
|
+
provider_id: Provider ID.
|
|
268
|
+
tool_name: Tool name.
|
|
269
|
+
timeout: Timeout in seconds.
|
|
270
|
+
|
|
271
|
+
Example:
|
|
272
|
+
@trace_tool_invocation("sqlite", "query", 30.0)
|
|
273
|
+
def invoke_tool(...):
|
|
274
|
+
...
|
|
275
|
+
"""
|
|
276
|
+
|
|
277
|
+
def decorator(func: F) -> F:
|
|
278
|
+
@wraps(func)
|
|
279
|
+
def wrapper(*args, **kwargs):
|
|
280
|
+
tracer = get_tracer(__name__)
|
|
281
|
+
|
|
282
|
+
with tracer.start_as_current_span(
|
|
283
|
+
f"tool.invoke.{tool_name}",
|
|
284
|
+
kind=trace.SpanKind.CLIENT if OTEL_AVAILABLE else None,
|
|
285
|
+
) as span:
|
|
286
|
+
# Set standard attributes
|
|
287
|
+
span.set_attribute("mcp.provider.id", provider_id)
|
|
288
|
+
span.set_attribute("mcp.tool.name", tool_name)
|
|
289
|
+
span.set_attribute("mcp.timeout_seconds", timeout)
|
|
290
|
+
|
|
291
|
+
try:
|
|
292
|
+
result = func(*args, **kwargs)
|
|
293
|
+
span.set_attribute("mcp.result.success", True)
|
|
294
|
+
return result
|
|
295
|
+
except Exception as e:
|
|
296
|
+
span.set_attribute("mcp.result.success", False)
|
|
297
|
+
span.set_attribute("mcp.error.type", type(e).__name__)
|
|
298
|
+
span.set_attribute("mcp.error.message", str(e)[:500])
|
|
299
|
+
span.record_exception(e)
|
|
300
|
+
if OTEL_AVAILABLE:
|
|
301
|
+
span.set_status(Status(StatusCode.ERROR, str(e)))
|
|
302
|
+
raise
|
|
303
|
+
|
|
304
|
+
return wrapper
|
|
305
|
+
|
|
306
|
+
return decorator
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
@contextmanager
|
|
310
|
+
def trace_span(
|
|
311
|
+
name: str,
|
|
312
|
+
attributes: Optional[Dict[str, Any]] = None,
|
|
313
|
+
kind: Optional[str] = None,
|
|
314
|
+
):
|
|
315
|
+
"""Context manager for creating trace spans.
|
|
316
|
+
|
|
317
|
+
Args:
|
|
318
|
+
name: Span name.
|
|
319
|
+
attributes: Initial span attributes.
|
|
320
|
+
kind: Span kind (client, server, producer, consumer, internal).
|
|
321
|
+
|
|
322
|
+
Example:
|
|
323
|
+
with trace_span("my_operation", {"key": "value"}) as span:
|
|
324
|
+
span.add_event("checkpoint_reached")
|
|
325
|
+
do_work()
|
|
326
|
+
"""
|
|
327
|
+
tracer = get_tracer(__name__)
|
|
328
|
+
|
|
329
|
+
span_kind = None
|
|
330
|
+
if OTEL_AVAILABLE and kind:
|
|
331
|
+
kind_map = {
|
|
332
|
+
"client": trace.SpanKind.CLIENT,
|
|
333
|
+
"server": trace.SpanKind.SERVER,
|
|
334
|
+
"producer": trace.SpanKind.PRODUCER,
|
|
335
|
+
"consumer": trace.SpanKind.CONSUMER,
|
|
336
|
+
"internal": trace.SpanKind.INTERNAL,
|
|
337
|
+
}
|
|
338
|
+
span_kind = kind_map.get(kind.lower())
|
|
339
|
+
|
|
340
|
+
with tracer.start_as_current_span(name, kind=span_kind) as span:
|
|
341
|
+
if attributes:
|
|
342
|
+
for key, value in attributes.items():
|
|
343
|
+
span.set_attribute(key, value)
|
|
344
|
+
yield span
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def inject_trace_context(carrier: Dict[str, str]) -> None:
|
|
348
|
+
"""Inject trace context into carrier dict for propagation.
|
|
349
|
+
|
|
350
|
+
Args:
|
|
351
|
+
carrier: Dict to inject trace context into.
|
|
352
|
+
|
|
353
|
+
Example:
|
|
354
|
+
headers = {}
|
|
355
|
+
inject_trace_context(headers)
|
|
356
|
+
# headers now contains traceparent, tracestate
|
|
357
|
+
"""
|
|
358
|
+
if not OTEL_AVAILABLE or not _initialized:
|
|
359
|
+
return
|
|
360
|
+
|
|
361
|
+
propagator = TraceContextTextMapPropagator()
|
|
362
|
+
propagator.inject(carrier)
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def extract_trace_context(carrier: Dict[str, str]) -> Any:
|
|
366
|
+
"""Extract trace context from carrier dict.
|
|
367
|
+
|
|
368
|
+
Args:
|
|
369
|
+
carrier: Dict containing trace context.
|
|
370
|
+
|
|
371
|
+
Returns:
|
|
372
|
+
OpenTelemetry context or None.
|
|
373
|
+
|
|
374
|
+
Example:
|
|
375
|
+
context = extract_trace_context(request.headers)
|
|
376
|
+
with tracer.start_as_current_span("handle", context=context):
|
|
377
|
+
...
|
|
378
|
+
"""
|
|
379
|
+
if not OTEL_AVAILABLE or not _initialized:
|
|
380
|
+
return None
|
|
381
|
+
|
|
382
|
+
propagator = TraceContextTextMapPropagator()
|
|
383
|
+
return propagator.extract(carrier)
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def get_current_trace_id() -> Optional[str]:
|
|
387
|
+
"""Get current trace ID as hex string.
|
|
388
|
+
|
|
389
|
+
Returns:
|
|
390
|
+
Trace ID or None if not in a trace.
|
|
391
|
+
"""
|
|
392
|
+
if not OTEL_AVAILABLE or not _initialized:
|
|
393
|
+
return None
|
|
394
|
+
|
|
395
|
+
span = trace.get_current_span()
|
|
396
|
+
if span is None:
|
|
397
|
+
return None
|
|
398
|
+
|
|
399
|
+
ctx = span.get_span_context()
|
|
400
|
+
if ctx is None or not ctx.is_valid:
|
|
401
|
+
return None
|
|
402
|
+
|
|
403
|
+
return format(ctx.trace_id, "032x")
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def get_current_span_id() -> Optional[str]:
|
|
407
|
+
"""Get current span ID as hex string.
|
|
408
|
+
|
|
409
|
+
Returns:
|
|
410
|
+
Span ID or None if not in a span.
|
|
411
|
+
"""
|
|
412
|
+
if not OTEL_AVAILABLE or not _initialized:
|
|
413
|
+
return None
|
|
414
|
+
|
|
415
|
+
span = trace.get_current_span()
|
|
416
|
+
if span is None:
|
|
417
|
+
return None
|
|
418
|
+
|
|
419
|
+
ctx = span.get_span_context()
|
|
420
|
+
if ctx is None or not ctx.is_valid:
|
|
421
|
+
return None
|
|
422
|
+
|
|
423
|
+
return format(ctx.span_id, "016x")
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def _get_version() -> str:
|
|
427
|
+
"""Get MCP Hangar version."""
|
|
428
|
+
try:
|
|
429
|
+
from mcp_hangar import __version__
|
|
430
|
+
|
|
431
|
+
return __version__
|
|
432
|
+
except (ImportError, AttributeError):
|
|
433
|
+
return "unknown"
|