foundry-mcp 0.8.22__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.
Potentially problematic release.
This version of foundry-mcp might be problematic. Click here for more details.
- foundry_mcp/__init__.py +13 -0
- foundry_mcp/cli/__init__.py +67 -0
- foundry_mcp/cli/__main__.py +9 -0
- foundry_mcp/cli/agent.py +96 -0
- foundry_mcp/cli/commands/__init__.py +37 -0
- foundry_mcp/cli/commands/cache.py +137 -0
- foundry_mcp/cli/commands/dashboard.py +148 -0
- foundry_mcp/cli/commands/dev.py +446 -0
- foundry_mcp/cli/commands/journal.py +377 -0
- foundry_mcp/cli/commands/lifecycle.py +274 -0
- foundry_mcp/cli/commands/modify.py +824 -0
- foundry_mcp/cli/commands/plan.py +640 -0
- foundry_mcp/cli/commands/pr.py +393 -0
- foundry_mcp/cli/commands/review.py +667 -0
- foundry_mcp/cli/commands/session.py +472 -0
- foundry_mcp/cli/commands/specs.py +686 -0
- foundry_mcp/cli/commands/tasks.py +807 -0
- foundry_mcp/cli/commands/testing.py +676 -0
- foundry_mcp/cli/commands/validate.py +982 -0
- foundry_mcp/cli/config.py +98 -0
- foundry_mcp/cli/context.py +298 -0
- foundry_mcp/cli/logging.py +212 -0
- foundry_mcp/cli/main.py +44 -0
- foundry_mcp/cli/output.py +122 -0
- foundry_mcp/cli/registry.py +110 -0
- foundry_mcp/cli/resilience.py +178 -0
- foundry_mcp/cli/transcript.py +217 -0
- foundry_mcp/config.py +1454 -0
- foundry_mcp/core/__init__.py +144 -0
- foundry_mcp/core/ai_consultation.py +1773 -0
- foundry_mcp/core/batch_operations.py +1202 -0
- foundry_mcp/core/cache.py +195 -0
- foundry_mcp/core/capabilities.py +446 -0
- foundry_mcp/core/concurrency.py +898 -0
- foundry_mcp/core/context.py +540 -0
- foundry_mcp/core/discovery.py +1603 -0
- foundry_mcp/core/error_collection.py +728 -0
- foundry_mcp/core/error_store.py +592 -0
- foundry_mcp/core/health.py +749 -0
- foundry_mcp/core/intake.py +933 -0
- foundry_mcp/core/journal.py +700 -0
- foundry_mcp/core/lifecycle.py +412 -0
- foundry_mcp/core/llm_config.py +1376 -0
- foundry_mcp/core/llm_patterns.py +510 -0
- foundry_mcp/core/llm_provider.py +1569 -0
- foundry_mcp/core/logging_config.py +374 -0
- foundry_mcp/core/metrics_persistence.py +584 -0
- foundry_mcp/core/metrics_registry.py +327 -0
- foundry_mcp/core/metrics_store.py +641 -0
- foundry_mcp/core/modifications.py +224 -0
- foundry_mcp/core/naming.py +146 -0
- foundry_mcp/core/observability.py +1216 -0
- foundry_mcp/core/otel.py +452 -0
- foundry_mcp/core/otel_stubs.py +264 -0
- foundry_mcp/core/pagination.py +255 -0
- foundry_mcp/core/progress.py +387 -0
- foundry_mcp/core/prometheus.py +564 -0
- foundry_mcp/core/prompts/__init__.py +464 -0
- foundry_mcp/core/prompts/fidelity_review.py +691 -0
- foundry_mcp/core/prompts/markdown_plan_review.py +515 -0
- foundry_mcp/core/prompts/plan_review.py +627 -0
- foundry_mcp/core/providers/__init__.py +237 -0
- foundry_mcp/core/providers/base.py +515 -0
- foundry_mcp/core/providers/claude.py +472 -0
- foundry_mcp/core/providers/codex.py +637 -0
- foundry_mcp/core/providers/cursor_agent.py +630 -0
- foundry_mcp/core/providers/detectors.py +515 -0
- foundry_mcp/core/providers/gemini.py +426 -0
- foundry_mcp/core/providers/opencode.py +718 -0
- foundry_mcp/core/providers/opencode_wrapper.js +308 -0
- foundry_mcp/core/providers/package-lock.json +24 -0
- foundry_mcp/core/providers/package.json +25 -0
- foundry_mcp/core/providers/registry.py +607 -0
- foundry_mcp/core/providers/test_provider.py +171 -0
- foundry_mcp/core/providers/validation.py +857 -0
- foundry_mcp/core/rate_limit.py +427 -0
- foundry_mcp/core/research/__init__.py +68 -0
- foundry_mcp/core/research/memory.py +528 -0
- foundry_mcp/core/research/models.py +1234 -0
- foundry_mcp/core/research/providers/__init__.py +40 -0
- foundry_mcp/core/research/providers/base.py +242 -0
- foundry_mcp/core/research/providers/google.py +507 -0
- foundry_mcp/core/research/providers/perplexity.py +442 -0
- foundry_mcp/core/research/providers/semantic_scholar.py +544 -0
- foundry_mcp/core/research/providers/tavily.py +383 -0
- foundry_mcp/core/research/workflows/__init__.py +25 -0
- foundry_mcp/core/research/workflows/base.py +298 -0
- foundry_mcp/core/research/workflows/chat.py +271 -0
- foundry_mcp/core/research/workflows/consensus.py +539 -0
- foundry_mcp/core/research/workflows/deep_research.py +4142 -0
- foundry_mcp/core/research/workflows/ideate.py +682 -0
- foundry_mcp/core/research/workflows/thinkdeep.py +405 -0
- foundry_mcp/core/resilience.py +600 -0
- foundry_mcp/core/responses.py +1624 -0
- foundry_mcp/core/review.py +366 -0
- foundry_mcp/core/security.py +438 -0
- foundry_mcp/core/spec.py +4119 -0
- foundry_mcp/core/task.py +2463 -0
- foundry_mcp/core/testing.py +839 -0
- foundry_mcp/core/validation.py +2357 -0
- foundry_mcp/dashboard/__init__.py +32 -0
- foundry_mcp/dashboard/app.py +119 -0
- foundry_mcp/dashboard/components/__init__.py +17 -0
- foundry_mcp/dashboard/components/cards.py +88 -0
- foundry_mcp/dashboard/components/charts.py +177 -0
- foundry_mcp/dashboard/components/filters.py +136 -0
- foundry_mcp/dashboard/components/tables.py +195 -0
- foundry_mcp/dashboard/data/__init__.py +11 -0
- foundry_mcp/dashboard/data/stores.py +433 -0
- foundry_mcp/dashboard/launcher.py +300 -0
- foundry_mcp/dashboard/views/__init__.py +12 -0
- foundry_mcp/dashboard/views/errors.py +217 -0
- foundry_mcp/dashboard/views/metrics.py +164 -0
- foundry_mcp/dashboard/views/overview.py +96 -0
- foundry_mcp/dashboard/views/providers.py +83 -0
- foundry_mcp/dashboard/views/sdd_workflow.py +255 -0
- foundry_mcp/dashboard/views/tool_usage.py +139 -0
- foundry_mcp/prompts/__init__.py +9 -0
- foundry_mcp/prompts/workflows.py +525 -0
- foundry_mcp/resources/__init__.py +9 -0
- foundry_mcp/resources/specs.py +591 -0
- foundry_mcp/schemas/__init__.py +38 -0
- foundry_mcp/schemas/intake-schema.json +89 -0
- foundry_mcp/schemas/sdd-spec-schema.json +414 -0
- foundry_mcp/server.py +150 -0
- foundry_mcp/tools/__init__.py +10 -0
- foundry_mcp/tools/unified/__init__.py +92 -0
- foundry_mcp/tools/unified/authoring.py +3620 -0
- foundry_mcp/tools/unified/context_helpers.py +98 -0
- foundry_mcp/tools/unified/documentation_helpers.py +268 -0
- foundry_mcp/tools/unified/environment.py +1341 -0
- foundry_mcp/tools/unified/error.py +479 -0
- foundry_mcp/tools/unified/health.py +225 -0
- foundry_mcp/tools/unified/journal.py +841 -0
- foundry_mcp/tools/unified/lifecycle.py +640 -0
- foundry_mcp/tools/unified/metrics.py +777 -0
- foundry_mcp/tools/unified/plan.py +876 -0
- foundry_mcp/tools/unified/pr.py +294 -0
- foundry_mcp/tools/unified/provider.py +589 -0
- foundry_mcp/tools/unified/research.py +1283 -0
- foundry_mcp/tools/unified/review.py +1042 -0
- foundry_mcp/tools/unified/review_helpers.py +314 -0
- foundry_mcp/tools/unified/router.py +102 -0
- foundry_mcp/tools/unified/server.py +565 -0
- foundry_mcp/tools/unified/spec.py +1283 -0
- foundry_mcp/tools/unified/task.py +3846 -0
- foundry_mcp/tools/unified/test.py +431 -0
- foundry_mcp/tools/unified/verification.py +520 -0
- foundry_mcp-0.8.22.dist-info/METADATA +344 -0
- foundry_mcp-0.8.22.dist-info/RECORD +153 -0
- foundry_mcp-0.8.22.dist-info/WHEEL +4 -0
- foundry_mcp-0.8.22.dist-info/entry_points.txt +3 -0
- foundry_mcp-0.8.22.dist-info/licenses/LICENSE +21 -0
foundry_mcp/core/otel.py
ADDED
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
"""OpenTelemetry integration with graceful degradation.
|
|
2
|
+
|
|
3
|
+
This module provides OpenTelemetry tracing and metrics integration that
|
|
4
|
+
gracefully falls back to no-op implementations when the optional
|
|
5
|
+
opentelemetry dependencies are not installed.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
from foundry_mcp.core.otel import get_tracer, traced
|
|
9
|
+
|
|
10
|
+
tracer = get_tracer(__name__)
|
|
11
|
+
with tracer.start_as_current_span("my-operation") as span:
|
|
12
|
+
span.set_attribute("key", "value")
|
|
13
|
+
...
|
|
14
|
+
|
|
15
|
+
@traced("my-function")
|
|
16
|
+
def my_function():
|
|
17
|
+
...
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import functools
|
|
23
|
+
import os
|
|
24
|
+
from dataclasses import dataclass, field
|
|
25
|
+
from typing import TYPE_CHECKING, Any, Callable, Optional, TypeVar, Union
|
|
26
|
+
|
|
27
|
+
from foundry_mcp.core.otel_stubs import (
|
|
28
|
+
NoOpMeter,
|
|
29
|
+
NoOpSpan,
|
|
30
|
+
NoOpTracer,
|
|
31
|
+
get_noop_meter,
|
|
32
|
+
get_noop_tracer,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
# Type checking imports for better IDE support
|
|
36
|
+
if TYPE_CHECKING:
|
|
37
|
+
from opentelemetry.metrics import Meter
|
|
38
|
+
from opentelemetry.trace import Span, Tracer
|
|
39
|
+
|
|
40
|
+
# Try to import OpenTelemetry
|
|
41
|
+
try:
|
|
42
|
+
from opentelemetry import metrics as otel_metrics
|
|
43
|
+
from opentelemetry import trace as otel_trace
|
|
44
|
+
from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import (
|
|
45
|
+
OTLPMetricExporter,
|
|
46
|
+
)
|
|
47
|
+
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import (
|
|
48
|
+
OTLPSpanExporter,
|
|
49
|
+
)
|
|
50
|
+
from opentelemetry.sdk.metrics import MeterProvider
|
|
51
|
+
from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
|
|
52
|
+
from opentelemetry.sdk.resources import Resource
|
|
53
|
+
from opentelemetry.sdk.trace import TracerProvider
|
|
54
|
+
from opentelemetry.sdk.trace.export import BatchSpanProcessor
|
|
55
|
+
from opentelemetry.sdk.trace.sampling import TraceIdRatioBased
|
|
56
|
+
|
|
57
|
+
_OPENTELEMETRY_AVAILABLE = True
|
|
58
|
+
except ImportError:
|
|
59
|
+
_OPENTELEMETRY_AVAILABLE = False
|
|
60
|
+
|
|
61
|
+
# Type aliases
|
|
62
|
+
F = TypeVar("F", bound=Callable[..., Any])
|
|
63
|
+
TracerType = Union["Tracer", NoOpTracer]
|
|
64
|
+
MeterType = Union["Meter", NoOpMeter]
|
|
65
|
+
SpanType = Union["Span", NoOpSpan]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# =============================================================================
|
|
69
|
+
# Configuration
|
|
70
|
+
# =============================================================================
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass
|
|
74
|
+
class OTelConfig:
|
|
75
|
+
"""Configuration for OpenTelemetry integration.
|
|
76
|
+
|
|
77
|
+
Attributes:
|
|
78
|
+
enabled: Whether OpenTelemetry is enabled
|
|
79
|
+
otlp_endpoint: OTLP exporter endpoint (default: localhost:4317)
|
|
80
|
+
service_name: Service name for traces and metrics
|
|
81
|
+
sample_rate: Trace sampling rate (0.0 to 1.0)
|
|
82
|
+
export_interval_ms: Metrics export interval in milliseconds
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
enabled: bool = False
|
|
86
|
+
otlp_endpoint: str = "localhost:4317"
|
|
87
|
+
service_name: str = "foundry-mcp"
|
|
88
|
+
sample_rate: float = 1.0
|
|
89
|
+
export_interval_ms: int = 60000
|
|
90
|
+
additional_attributes: dict[str, str] = field(default_factory=dict)
|
|
91
|
+
|
|
92
|
+
@classmethod
|
|
93
|
+
def from_env_and_config(
|
|
94
|
+
cls,
|
|
95
|
+
config: Optional[dict[str, Any]] = None,
|
|
96
|
+
) -> "OTelConfig":
|
|
97
|
+
"""Load configuration from environment variables and optional config dict.
|
|
98
|
+
|
|
99
|
+
Environment variables take precedence over config dict values.
|
|
100
|
+
|
|
101
|
+
Env vars:
|
|
102
|
+
OTEL_ENABLED: "true" or "1" to enable
|
|
103
|
+
OTEL_EXPORTER_OTLP_ENDPOINT: OTLP endpoint
|
|
104
|
+
OTEL_SERVICE_NAME: Service name
|
|
105
|
+
OTEL_TRACE_SAMPLE_RATE: Sample rate (float)
|
|
106
|
+
OTEL_METRIC_EXPORT_INTERVAL: Export interval in ms
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
config: Optional dict with config values (typically from TOML)
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
OTelConfig instance
|
|
113
|
+
"""
|
|
114
|
+
config = config or {}
|
|
115
|
+
|
|
116
|
+
# Parse enabled from env or config
|
|
117
|
+
env_enabled = os.environ.get("OTEL_ENABLED", "").lower()
|
|
118
|
+
if env_enabled:
|
|
119
|
+
enabled = env_enabled in ("true", "1", "yes")
|
|
120
|
+
else:
|
|
121
|
+
enabled = config.get("enabled", False)
|
|
122
|
+
|
|
123
|
+
# Parse endpoint
|
|
124
|
+
otlp_endpoint = os.environ.get(
|
|
125
|
+
"OTEL_EXPORTER_OTLP_ENDPOINT",
|
|
126
|
+
config.get("otlp_endpoint", "localhost:4317"),
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
# Parse service name
|
|
130
|
+
service_name = os.environ.get(
|
|
131
|
+
"OTEL_SERVICE_NAME",
|
|
132
|
+
config.get("service_name", "foundry-mcp"),
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
# Parse sample rate
|
|
136
|
+
sample_rate_str = os.environ.get("OTEL_TRACE_SAMPLE_RATE")
|
|
137
|
+
if sample_rate_str:
|
|
138
|
+
try:
|
|
139
|
+
sample_rate = float(sample_rate_str)
|
|
140
|
+
except ValueError:
|
|
141
|
+
sample_rate = 1.0
|
|
142
|
+
else:
|
|
143
|
+
sample_rate = config.get("sample_rate", 1.0)
|
|
144
|
+
|
|
145
|
+
# Parse export interval
|
|
146
|
+
export_interval_str = os.environ.get("OTEL_METRIC_EXPORT_INTERVAL")
|
|
147
|
+
if export_interval_str:
|
|
148
|
+
try:
|
|
149
|
+
export_interval_ms = int(export_interval_str)
|
|
150
|
+
except ValueError:
|
|
151
|
+
export_interval_ms = 60000
|
|
152
|
+
else:
|
|
153
|
+
export_interval_ms = config.get("export_interval_ms", 60000)
|
|
154
|
+
|
|
155
|
+
# Additional attributes from config
|
|
156
|
+
additional_attributes = config.get("attributes", {})
|
|
157
|
+
|
|
158
|
+
return cls(
|
|
159
|
+
enabled=enabled,
|
|
160
|
+
otlp_endpoint=otlp_endpoint,
|
|
161
|
+
service_name=service_name,
|
|
162
|
+
sample_rate=sample_rate,
|
|
163
|
+
export_interval_ms=export_interval_ms,
|
|
164
|
+
additional_attributes=additional_attributes,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
# =============================================================================
|
|
169
|
+
# Global State
|
|
170
|
+
# =============================================================================
|
|
171
|
+
|
|
172
|
+
_config: Optional[OTelConfig] = None
|
|
173
|
+
_tracer_provider: Any = None
|
|
174
|
+
_meter_provider: Any = None
|
|
175
|
+
_initialized: bool = False
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
# =============================================================================
|
|
179
|
+
# Initialization
|
|
180
|
+
# =============================================================================
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def initialize(config: Optional[OTelConfig] = None) -> bool:
|
|
184
|
+
"""Initialize OpenTelemetry with the given configuration.
|
|
185
|
+
|
|
186
|
+
This function is idempotent - calling it multiple times has no effect
|
|
187
|
+
after the first successful initialization.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
config: OTel configuration. If None, loads from env and defaults.
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
True if OpenTelemetry was initialized, False if using no-ops
|
|
194
|
+
"""
|
|
195
|
+
global _config, _tracer_provider, _meter_provider, _initialized
|
|
196
|
+
|
|
197
|
+
if _initialized:
|
|
198
|
+
return _config is not None and _config.enabled and _OPENTELEMETRY_AVAILABLE
|
|
199
|
+
|
|
200
|
+
_config = config or OTelConfig.from_env_and_config()
|
|
201
|
+
|
|
202
|
+
if not _config.enabled or not _OPENTELEMETRY_AVAILABLE:
|
|
203
|
+
_initialized = True
|
|
204
|
+
return False
|
|
205
|
+
|
|
206
|
+
# Create resource with service info
|
|
207
|
+
resource_attrs = {
|
|
208
|
+
"service.name": _config.service_name,
|
|
209
|
+
"service.version": _get_version(),
|
|
210
|
+
}
|
|
211
|
+
resource_attrs.update(_config.additional_attributes)
|
|
212
|
+
resource = Resource.create(resource_attrs)
|
|
213
|
+
|
|
214
|
+
# Setup tracing
|
|
215
|
+
sampler = TraceIdRatioBased(_config.sample_rate)
|
|
216
|
+
_tracer_provider = TracerProvider(resource=resource, sampler=sampler)
|
|
217
|
+
|
|
218
|
+
# Add OTLP span exporter
|
|
219
|
+
otlp_span_exporter = OTLPSpanExporter(
|
|
220
|
+
endpoint=_config.otlp_endpoint,
|
|
221
|
+
insecure=True, # Use insecure for local development
|
|
222
|
+
)
|
|
223
|
+
span_processor = BatchSpanProcessor(otlp_span_exporter)
|
|
224
|
+
_tracer_provider.add_span_processor(span_processor)
|
|
225
|
+
|
|
226
|
+
# Set as global tracer provider
|
|
227
|
+
otel_trace.set_tracer_provider(_tracer_provider)
|
|
228
|
+
|
|
229
|
+
# Setup metrics
|
|
230
|
+
otlp_metric_exporter = OTLPMetricExporter(
|
|
231
|
+
endpoint=_config.otlp_endpoint,
|
|
232
|
+
insecure=True,
|
|
233
|
+
)
|
|
234
|
+
metric_reader = PeriodicExportingMetricReader(
|
|
235
|
+
otlp_metric_exporter,
|
|
236
|
+
export_interval_millis=_config.export_interval_ms,
|
|
237
|
+
)
|
|
238
|
+
_meter_provider = MeterProvider(resource=resource, metric_readers=[metric_reader])
|
|
239
|
+
|
|
240
|
+
# Set as global meter provider
|
|
241
|
+
otel_metrics.set_meter_provider(_meter_provider)
|
|
242
|
+
|
|
243
|
+
_initialized = True
|
|
244
|
+
return True
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _ensure_initialized() -> None:
|
|
248
|
+
"""Ensure OpenTelemetry is initialized (lazy initialization)."""
|
|
249
|
+
if not _initialized:
|
|
250
|
+
initialize()
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _get_version() -> str:
|
|
254
|
+
"""Get the foundry-mcp version."""
|
|
255
|
+
try:
|
|
256
|
+
from importlib.metadata import version
|
|
257
|
+
|
|
258
|
+
return version("foundry-mcp")
|
|
259
|
+
except Exception:
|
|
260
|
+
return "unknown"
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
# =============================================================================
|
|
264
|
+
# Public API
|
|
265
|
+
# =============================================================================
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def is_available() -> bool:
|
|
269
|
+
"""Check if OpenTelemetry dependencies are available.
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
True if opentelemetry packages are installed
|
|
273
|
+
"""
|
|
274
|
+
return _OPENTELEMETRY_AVAILABLE
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def is_enabled() -> bool:
|
|
278
|
+
"""Check if OpenTelemetry is enabled and initialized.
|
|
279
|
+
|
|
280
|
+
Returns:
|
|
281
|
+
True if OTel is enabled, available, and initialized
|
|
282
|
+
"""
|
|
283
|
+
_ensure_initialized()
|
|
284
|
+
return (
|
|
285
|
+
_config is not None
|
|
286
|
+
and _config.enabled
|
|
287
|
+
and _OPENTELEMETRY_AVAILABLE
|
|
288
|
+
and _initialized
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def get_tracer(name: str = __name__) -> TracerType:
|
|
293
|
+
"""Get a tracer instance.
|
|
294
|
+
|
|
295
|
+
If OpenTelemetry is not available or not enabled, returns a no-op tracer
|
|
296
|
+
that silently ignores all operations.
|
|
297
|
+
|
|
298
|
+
Args:
|
|
299
|
+
name: Tracer name (typically __name__ of the calling module)
|
|
300
|
+
|
|
301
|
+
Returns:
|
|
302
|
+
Tracer instance (real or no-op)
|
|
303
|
+
"""
|
|
304
|
+
_ensure_initialized()
|
|
305
|
+
|
|
306
|
+
if is_enabled():
|
|
307
|
+
return otel_trace.get_tracer(name)
|
|
308
|
+
return get_noop_tracer(name)
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def get_meter(name: str = __name__) -> MeterType:
|
|
312
|
+
"""Get a meter instance for creating metrics.
|
|
313
|
+
|
|
314
|
+
If OpenTelemetry is not available or not enabled, returns a no-op meter
|
|
315
|
+
that silently ignores all operations.
|
|
316
|
+
|
|
317
|
+
Args:
|
|
318
|
+
name: Meter name (typically __name__ of the calling module)
|
|
319
|
+
|
|
320
|
+
Returns:
|
|
321
|
+
Meter instance (real or no-op)
|
|
322
|
+
"""
|
|
323
|
+
_ensure_initialized()
|
|
324
|
+
|
|
325
|
+
if is_enabled():
|
|
326
|
+
return otel_metrics.get_meter(name)
|
|
327
|
+
return get_noop_meter(name)
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def traced(
|
|
331
|
+
name: Optional[str] = None,
|
|
332
|
+
*,
|
|
333
|
+
attributes: Optional[dict[str, Any]] = None,
|
|
334
|
+
record_exception: bool = True,
|
|
335
|
+
set_status_on_exception: bool = True,
|
|
336
|
+
) -> Callable[[F], F]:
|
|
337
|
+
"""Decorator to trace a function with a span.
|
|
338
|
+
|
|
339
|
+
Usage:
|
|
340
|
+
@traced("my-operation")
|
|
341
|
+
def my_function(arg1, arg2):
|
|
342
|
+
...
|
|
343
|
+
|
|
344
|
+
@traced() # Uses function name as span name
|
|
345
|
+
async def my_async_function():
|
|
346
|
+
...
|
|
347
|
+
|
|
348
|
+
Args:
|
|
349
|
+
name: Span name. Defaults to function name if not provided.
|
|
350
|
+
attributes: Additional attributes to set on the span.
|
|
351
|
+
record_exception: Whether to record exceptions on the span.
|
|
352
|
+
set_status_on_exception: Whether to set error status on exception.
|
|
353
|
+
|
|
354
|
+
Returns:
|
|
355
|
+
Decorated function
|
|
356
|
+
"""
|
|
357
|
+
|
|
358
|
+
def decorator(func: F) -> F:
|
|
359
|
+
span_name = name or func.__name__
|
|
360
|
+
tracer = get_tracer(func.__module__)
|
|
361
|
+
|
|
362
|
+
@functools.wraps(func)
|
|
363
|
+
def sync_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
364
|
+
with tracer.start_as_current_span(
|
|
365
|
+
span_name,
|
|
366
|
+
attributes=attributes,
|
|
367
|
+
record_exception=record_exception,
|
|
368
|
+
set_status_on_exception=set_status_on_exception,
|
|
369
|
+
) as span:
|
|
370
|
+
# Add function signature info
|
|
371
|
+
if hasattr(span, "set_attribute"):
|
|
372
|
+
span.set_attribute("code.function", func.__name__)
|
|
373
|
+
span.set_attribute("code.namespace", func.__module__)
|
|
374
|
+
return func(*args, **kwargs)
|
|
375
|
+
|
|
376
|
+
@functools.wraps(func)
|
|
377
|
+
async def async_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
378
|
+
with tracer.start_as_current_span(
|
|
379
|
+
span_name,
|
|
380
|
+
attributes=attributes,
|
|
381
|
+
record_exception=record_exception,
|
|
382
|
+
set_status_on_exception=set_status_on_exception,
|
|
383
|
+
) as span:
|
|
384
|
+
# Add function signature info
|
|
385
|
+
if hasattr(span, "set_attribute"):
|
|
386
|
+
span.set_attribute("code.function", func.__name__)
|
|
387
|
+
span.set_attribute("code.namespace", func.__module__)
|
|
388
|
+
return await func(*args, **kwargs)
|
|
389
|
+
|
|
390
|
+
import asyncio
|
|
391
|
+
|
|
392
|
+
if asyncio.iscoroutinefunction(func):
|
|
393
|
+
return async_wrapper # type: ignore
|
|
394
|
+
return sync_wrapper # type: ignore
|
|
395
|
+
|
|
396
|
+
return decorator
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def shutdown() -> None:
|
|
400
|
+
"""Shutdown OpenTelemetry providers and flush pending data.
|
|
401
|
+
|
|
402
|
+
Call this during application shutdown to ensure all telemetry
|
|
403
|
+
data is exported.
|
|
404
|
+
"""
|
|
405
|
+
global _tracer_provider, _meter_provider, _initialized
|
|
406
|
+
|
|
407
|
+
if _tracer_provider is not None and hasattr(_tracer_provider, "shutdown"):
|
|
408
|
+
try:
|
|
409
|
+
_tracer_provider.shutdown()
|
|
410
|
+
except Exception:
|
|
411
|
+
pass
|
|
412
|
+
_tracer_provider = None
|
|
413
|
+
|
|
414
|
+
if _meter_provider is not None and hasattr(_meter_provider, "shutdown"):
|
|
415
|
+
try:
|
|
416
|
+
_meter_provider.shutdown()
|
|
417
|
+
except Exception:
|
|
418
|
+
pass
|
|
419
|
+
_meter_provider = None
|
|
420
|
+
|
|
421
|
+
_initialized = False
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def get_config() -> Optional[OTelConfig]:
|
|
425
|
+
"""Get the current OTel configuration.
|
|
426
|
+
|
|
427
|
+
Returns:
|
|
428
|
+
Current configuration or None if not initialized
|
|
429
|
+
"""
|
|
430
|
+
return _config
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
# =============================================================================
|
|
434
|
+
# Exports
|
|
435
|
+
# =============================================================================
|
|
436
|
+
|
|
437
|
+
__all__ = [
|
|
438
|
+
# Configuration
|
|
439
|
+
"OTelConfig",
|
|
440
|
+
# Initialization
|
|
441
|
+
"initialize",
|
|
442
|
+
"shutdown",
|
|
443
|
+
# Status
|
|
444
|
+
"is_available",
|
|
445
|
+
"is_enabled",
|
|
446
|
+
"get_config",
|
|
447
|
+
# Tracer/Meter
|
|
448
|
+
"get_tracer",
|
|
449
|
+
"get_meter",
|
|
450
|
+
# Decorator
|
|
451
|
+
"traced",
|
|
452
|
+
]
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
"""No-op stubs for observability when optional dependencies are not installed.
|
|
2
|
+
|
|
3
|
+
This module provides no-op implementations for tracing and metrics interfaces,
|
|
4
|
+
allowing the codebase to use observability features without requiring the
|
|
5
|
+
optional dependencies to be installed. All operations are silently ignored.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from contextlib import contextmanager
|
|
9
|
+
from typing import Any, Iterator, Optional, Sequence
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# =============================================================================
|
|
13
|
+
# Tracing Stubs
|
|
14
|
+
# =============================================================================
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class NoOpSpan:
|
|
18
|
+
"""No-op span that silently ignores all operations."""
|
|
19
|
+
|
|
20
|
+
__slots__ = ("_name",)
|
|
21
|
+
|
|
22
|
+
def __init__(self, name: str = "") -> None:
|
|
23
|
+
self._name = name
|
|
24
|
+
|
|
25
|
+
def __enter__(self) -> "NoOpSpan":
|
|
26
|
+
return self
|
|
27
|
+
|
|
28
|
+
def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
def set_attribute(self, key: str, value: Any) -> None:
|
|
32
|
+
"""No-op: ignores attribute setting."""
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
def set_attributes(self, attributes: dict[str, Any]) -> None:
|
|
36
|
+
"""No-op: ignores attributes setting."""
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
def set_status(self, status: Any, description: Optional[str] = None) -> None:
|
|
40
|
+
"""No-op: ignores status setting."""
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
def record_exception(
|
|
44
|
+
self,
|
|
45
|
+
exception: BaseException,
|
|
46
|
+
attributes: Optional[dict[str, Any]] = None,
|
|
47
|
+
timestamp: Optional[int] = None,
|
|
48
|
+
escaped: bool = False,
|
|
49
|
+
) -> None:
|
|
50
|
+
"""No-op: ignores exception recording."""
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
def add_event(
|
|
54
|
+
self,
|
|
55
|
+
name: str,
|
|
56
|
+
attributes: Optional[dict[str, Any]] = None,
|
|
57
|
+
timestamp: Optional[int] = None,
|
|
58
|
+
) -> None:
|
|
59
|
+
"""No-op: ignores event adding."""
|
|
60
|
+
pass
|
|
61
|
+
|
|
62
|
+
def is_recording(self) -> bool:
|
|
63
|
+
"""No-op spans are never recording."""
|
|
64
|
+
return False
|
|
65
|
+
|
|
66
|
+
def end(self, end_time: Optional[int] = None) -> None:
|
|
67
|
+
"""No-op: ignores span end."""
|
|
68
|
+
pass
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# Singleton instance
|
|
72
|
+
_NOOP_SPAN = NoOpSpan()
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class NoOpTracer:
|
|
76
|
+
"""No-op tracer that returns no-op spans."""
|
|
77
|
+
|
|
78
|
+
__slots__ = ("_name",)
|
|
79
|
+
|
|
80
|
+
def __init__(self, name: str = "") -> None:
|
|
81
|
+
self._name = name
|
|
82
|
+
|
|
83
|
+
@contextmanager
|
|
84
|
+
def start_as_current_span(
|
|
85
|
+
self,
|
|
86
|
+
name: str,
|
|
87
|
+
*,
|
|
88
|
+
context: Any = None,
|
|
89
|
+
kind: Any = None,
|
|
90
|
+
attributes: Optional[dict[str, Any]] = None,
|
|
91
|
+
links: Optional[Sequence[Any]] = None,
|
|
92
|
+
start_time: Optional[int] = None,
|
|
93
|
+
record_exception: bool = True,
|
|
94
|
+
set_status_on_exception: bool = True,
|
|
95
|
+
end_on_exit: bool = True,
|
|
96
|
+
) -> Iterator[NoOpSpan]:
|
|
97
|
+
"""No-op: yields a no-op span."""
|
|
98
|
+
yield _NOOP_SPAN
|
|
99
|
+
|
|
100
|
+
def start_span(
|
|
101
|
+
self,
|
|
102
|
+
name: str,
|
|
103
|
+
*,
|
|
104
|
+
context: Any = None,
|
|
105
|
+
kind: Any = None,
|
|
106
|
+
attributes: Optional[dict[str, Any]] = None,
|
|
107
|
+
links: Optional[Sequence[Any]] = None,
|
|
108
|
+
start_time: Optional[int] = None,
|
|
109
|
+
record_exception: bool = True,
|
|
110
|
+
set_status_on_exception: bool = True,
|
|
111
|
+
) -> NoOpSpan:
|
|
112
|
+
"""No-op: returns a no-op span."""
|
|
113
|
+
return _NOOP_SPAN
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
# Singleton instance
|
|
117
|
+
_NOOP_TRACER = NoOpTracer()
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def get_noop_tracer(name: str = "") -> NoOpTracer:
|
|
121
|
+
"""Get the singleton no-op tracer instance."""
|
|
122
|
+
return _NOOP_TRACER
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
# =============================================================================
|
|
126
|
+
# Metrics Stubs
|
|
127
|
+
# =============================================================================
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class NoOpCounter:
|
|
131
|
+
"""No-op counter that silently ignores all operations."""
|
|
132
|
+
|
|
133
|
+
__slots__ = ("_name",)
|
|
134
|
+
|
|
135
|
+
def __init__(self, name: str = "") -> None:
|
|
136
|
+
self._name = name
|
|
137
|
+
|
|
138
|
+
def add(self, amount: float, attributes: Optional[dict[str, Any]] = None) -> None:
|
|
139
|
+
"""No-op: ignores counter increment."""
|
|
140
|
+
pass
|
|
141
|
+
|
|
142
|
+
def inc(self, attributes: Optional[dict[str, Any]] = None) -> None:
|
|
143
|
+
"""No-op: ignores counter increment by 1."""
|
|
144
|
+
pass
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class NoOpGauge:
|
|
148
|
+
"""No-op gauge that silently ignores all operations."""
|
|
149
|
+
|
|
150
|
+
__slots__ = ("_name",)
|
|
151
|
+
|
|
152
|
+
def __init__(self, name: str = "") -> None:
|
|
153
|
+
self._name = name
|
|
154
|
+
|
|
155
|
+
def set(self, value: float, attributes: Optional[dict[str, Any]] = None) -> None:
|
|
156
|
+
"""No-op: ignores gauge setting."""
|
|
157
|
+
pass
|
|
158
|
+
|
|
159
|
+
def inc(self, attributes: Optional[dict[str, Any]] = None) -> None:
|
|
160
|
+
"""No-op: ignores gauge increment."""
|
|
161
|
+
pass
|
|
162
|
+
|
|
163
|
+
def dec(self, attributes: Optional[dict[str, Any]] = None) -> None:
|
|
164
|
+
"""No-op: ignores gauge decrement."""
|
|
165
|
+
pass
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
class NoOpHistogram:
|
|
169
|
+
"""No-op histogram that silently ignores all operations."""
|
|
170
|
+
|
|
171
|
+
__slots__ = ("_name",)
|
|
172
|
+
|
|
173
|
+
def __init__(self, name: str = "") -> None:
|
|
174
|
+
self._name = name
|
|
175
|
+
|
|
176
|
+
def record(
|
|
177
|
+
self, value: float, attributes: Optional[dict[str, Any]] = None
|
|
178
|
+
) -> None:
|
|
179
|
+
"""No-op: ignores value recording."""
|
|
180
|
+
pass
|
|
181
|
+
|
|
182
|
+
def observe(
|
|
183
|
+
self, value: float, attributes: Optional[dict[str, Any]] = None
|
|
184
|
+
) -> None:
|
|
185
|
+
"""No-op: ignores value observation (alias for record)."""
|
|
186
|
+
pass
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
# Singleton instances
|
|
190
|
+
_NOOP_COUNTER = NoOpCounter()
|
|
191
|
+
_NOOP_GAUGE = NoOpGauge()
|
|
192
|
+
_NOOP_HISTOGRAM = NoOpHistogram()
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
class NoOpMeter:
|
|
196
|
+
"""No-op meter that returns no-op metric instruments."""
|
|
197
|
+
|
|
198
|
+
__slots__ = ("_name",)
|
|
199
|
+
|
|
200
|
+
def __init__(self, name: str = "") -> None:
|
|
201
|
+
self._name = name
|
|
202
|
+
|
|
203
|
+
def create_counter(
|
|
204
|
+
self,
|
|
205
|
+
name: str,
|
|
206
|
+
unit: str = "",
|
|
207
|
+
description: str = "",
|
|
208
|
+
) -> NoOpCounter:
|
|
209
|
+
"""No-op: returns a no-op counter."""
|
|
210
|
+
return _NOOP_COUNTER
|
|
211
|
+
|
|
212
|
+
def create_up_down_counter(
|
|
213
|
+
self,
|
|
214
|
+
name: str,
|
|
215
|
+
unit: str = "",
|
|
216
|
+
description: str = "",
|
|
217
|
+
) -> NoOpCounter:
|
|
218
|
+
"""No-op: returns a no-op counter."""
|
|
219
|
+
return _NOOP_COUNTER
|
|
220
|
+
|
|
221
|
+
def create_gauge(
|
|
222
|
+
self,
|
|
223
|
+
name: str,
|
|
224
|
+
unit: str = "",
|
|
225
|
+
description: str = "",
|
|
226
|
+
) -> NoOpGauge:
|
|
227
|
+
"""No-op: returns a no-op gauge."""
|
|
228
|
+
return _NOOP_GAUGE
|
|
229
|
+
|
|
230
|
+
def create_histogram(
|
|
231
|
+
self,
|
|
232
|
+
name: str,
|
|
233
|
+
unit: str = "",
|
|
234
|
+
description: str = "",
|
|
235
|
+
) -> NoOpHistogram:
|
|
236
|
+
"""No-op: returns a no-op histogram."""
|
|
237
|
+
return _NOOP_HISTOGRAM
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
# Singleton instance
|
|
241
|
+
_NOOP_METER = NoOpMeter()
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def get_noop_meter(name: str = "") -> NoOpMeter:
|
|
245
|
+
"""Get the singleton no-op meter instance."""
|
|
246
|
+
return _NOOP_METER
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
# =============================================================================
|
|
250
|
+
# Convenience exports
|
|
251
|
+
# =============================================================================
|
|
252
|
+
|
|
253
|
+
__all__ = [
|
|
254
|
+
# Tracing
|
|
255
|
+
"NoOpSpan",
|
|
256
|
+
"NoOpTracer",
|
|
257
|
+
"get_noop_tracer",
|
|
258
|
+
# Metrics
|
|
259
|
+
"NoOpCounter",
|
|
260
|
+
"NoOpGauge",
|
|
261
|
+
"NoOpHistogram",
|
|
262
|
+
"NoOpMeter",
|
|
263
|
+
"get_noop_meter",
|
|
264
|
+
]
|