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.

Files changed (153) hide show
  1. foundry_mcp/__init__.py +13 -0
  2. foundry_mcp/cli/__init__.py +67 -0
  3. foundry_mcp/cli/__main__.py +9 -0
  4. foundry_mcp/cli/agent.py +96 -0
  5. foundry_mcp/cli/commands/__init__.py +37 -0
  6. foundry_mcp/cli/commands/cache.py +137 -0
  7. foundry_mcp/cli/commands/dashboard.py +148 -0
  8. foundry_mcp/cli/commands/dev.py +446 -0
  9. foundry_mcp/cli/commands/journal.py +377 -0
  10. foundry_mcp/cli/commands/lifecycle.py +274 -0
  11. foundry_mcp/cli/commands/modify.py +824 -0
  12. foundry_mcp/cli/commands/plan.py +640 -0
  13. foundry_mcp/cli/commands/pr.py +393 -0
  14. foundry_mcp/cli/commands/review.py +667 -0
  15. foundry_mcp/cli/commands/session.py +472 -0
  16. foundry_mcp/cli/commands/specs.py +686 -0
  17. foundry_mcp/cli/commands/tasks.py +807 -0
  18. foundry_mcp/cli/commands/testing.py +676 -0
  19. foundry_mcp/cli/commands/validate.py +982 -0
  20. foundry_mcp/cli/config.py +98 -0
  21. foundry_mcp/cli/context.py +298 -0
  22. foundry_mcp/cli/logging.py +212 -0
  23. foundry_mcp/cli/main.py +44 -0
  24. foundry_mcp/cli/output.py +122 -0
  25. foundry_mcp/cli/registry.py +110 -0
  26. foundry_mcp/cli/resilience.py +178 -0
  27. foundry_mcp/cli/transcript.py +217 -0
  28. foundry_mcp/config.py +1454 -0
  29. foundry_mcp/core/__init__.py +144 -0
  30. foundry_mcp/core/ai_consultation.py +1773 -0
  31. foundry_mcp/core/batch_operations.py +1202 -0
  32. foundry_mcp/core/cache.py +195 -0
  33. foundry_mcp/core/capabilities.py +446 -0
  34. foundry_mcp/core/concurrency.py +898 -0
  35. foundry_mcp/core/context.py +540 -0
  36. foundry_mcp/core/discovery.py +1603 -0
  37. foundry_mcp/core/error_collection.py +728 -0
  38. foundry_mcp/core/error_store.py +592 -0
  39. foundry_mcp/core/health.py +749 -0
  40. foundry_mcp/core/intake.py +933 -0
  41. foundry_mcp/core/journal.py +700 -0
  42. foundry_mcp/core/lifecycle.py +412 -0
  43. foundry_mcp/core/llm_config.py +1376 -0
  44. foundry_mcp/core/llm_patterns.py +510 -0
  45. foundry_mcp/core/llm_provider.py +1569 -0
  46. foundry_mcp/core/logging_config.py +374 -0
  47. foundry_mcp/core/metrics_persistence.py +584 -0
  48. foundry_mcp/core/metrics_registry.py +327 -0
  49. foundry_mcp/core/metrics_store.py +641 -0
  50. foundry_mcp/core/modifications.py +224 -0
  51. foundry_mcp/core/naming.py +146 -0
  52. foundry_mcp/core/observability.py +1216 -0
  53. foundry_mcp/core/otel.py +452 -0
  54. foundry_mcp/core/otel_stubs.py +264 -0
  55. foundry_mcp/core/pagination.py +255 -0
  56. foundry_mcp/core/progress.py +387 -0
  57. foundry_mcp/core/prometheus.py +564 -0
  58. foundry_mcp/core/prompts/__init__.py +464 -0
  59. foundry_mcp/core/prompts/fidelity_review.py +691 -0
  60. foundry_mcp/core/prompts/markdown_plan_review.py +515 -0
  61. foundry_mcp/core/prompts/plan_review.py +627 -0
  62. foundry_mcp/core/providers/__init__.py +237 -0
  63. foundry_mcp/core/providers/base.py +515 -0
  64. foundry_mcp/core/providers/claude.py +472 -0
  65. foundry_mcp/core/providers/codex.py +637 -0
  66. foundry_mcp/core/providers/cursor_agent.py +630 -0
  67. foundry_mcp/core/providers/detectors.py +515 -0
  68. foundry_mcp/core/providers/gemini.py +426 -0
  69. foundry_mcp/core/providers/opencode.py +718 -0
  70. foundry_mcp/core/providers/opencode_wrapper.js +308 -0
  71. foundry_mcp/core/providers/package-lock.json +24 -0
  72. foundry_mcp/core/providers/package.json +25 -0
  73. foundry_mcp/core/providers/registry.py +607 -0
  74. foundry_mcp/core/providers/test_provider.py +171 -0
  75. foundry_mcp/core/providers/validation.py +857 -0
  76. foundry_mcp/core/rate_limit.py +427 -0
  77. foundry_mcp/core/research/__init__.py +68 -0
  78. foundry_mcp/core/research/memory.py +528 -0
  79. foundry_mcp/core/research/models.py +1234 -0
  80. foundry_mcp/core/research/providers/__init__.py +40 -0
  81. foundry_mcp/core/research/providers/base.py +242 -0
  82. foundry_mcp/core/research/providers/google.py +507 -0
  83. foundry_mcp/core/research/providers/perplexity.py +442 -0
  84. foundry_mcp/core/research/providers/semantic_scholar.py +544 -0
  85. foundry_mcp/core/research/providers/tavily.py +383 -0
  86. foundry_mcp/core/research/workflows/__init__.py +25 -0
  87. foundry_mcp/core/research/workflows/base.py +298 -0
  88. foundry_mcp/core/research/workflows/chat.py +271 -0
  89. foundry_mcp/core/research/workflows/consensus.py +539 -0
  90. foundry_mcp/core/research/workflows/deep_research.py +4142 -0
  91. foundry_mcp/core/research/workflows/ideate.py +682 -0
  92. foundry_mcp/core/research/workflows/thinkdeep.py +405 -0
  93. foundry_mcp/core/resilience.py +600 -0
  94. foundry_mcp/core/responses.py +1624 -0
  95. foundry_mcp/core/review.py +366 -0
  96. foundry_mcp/core/security.py +438 -0
  97. foundry_mcp/core/spec.py +4119 -0
  98. foundry_mcp/core/task.py +2463 -0
  99. foundry_mcp/core/testing.py +839 -0
  100. foundry_mcp/core/validation.py +2357 -0
  101. foundry_mcp/dashboard/__init__.py +32 -0
  102. foundry_mcp/dashboard/app.py +119 -0
  103. foundry_mcp/dashboard/components/__init__.py +17 -0
  104. foundry_mcp/dashboard/components/cards.py +88 -0
  105. foundry_mcp/dashboard/components/charts.py +177 -0
  106. foundry_mcp/dashboard/components/filters.py +136 -0
  107. foundry_mcp/dashboard/components/tables.py +195 -0
  108. foundry_mcp/dashboard/data/__init__.py +11 -0
  109. foundry_mcp/dashboard/data/stores.py +433 -0
  110. foundry_mcp/dashboard/launcher.py +300 -0
  111. foundry_mcp/dashboard/views/__init__.py +12 -0
  112. foundry_mcp/dashboard/views/errors.py +217 -0
  113. foundry_mcp/dashboard/views/metrics.py +164 -0
  114. foundry_mcp/dashboard/views/overview.py +96 -0
  115. foundry_mcp/dashboard/views/providers.py +83 -0
  116. foundry_mcp/dashboard/views/sdd_workflow.py +255 -0
  117. foundry_mcp/dashboard/views/tool_usage.py +139 -0
  118. foundry_mcp/prompts/__init__.py +9 -0
  119. foundry_mcp/prompts/workflows.py +525 -0
  120. foundry_mcp/resources/__init__.py +9 -0
  121. foundry_mcp/resources/specs.py +591 -0
  122. foundry_mcp/schemas/__init__.py +38 -0
  123. foundry_mcp/schemas/intake-schema.json +89 -0
  124. foundry_mcp/schemas/sdd-spec-schema.json +414 -0
  125. foundry_mcp/server.py +150 -0
  126. foundry_mcp/tools/__init__.py +10 -0
  127. foundry_mcp/tools/unified/__init__.py +92 -0
  128. foundry_mcp/tools/unified/authoring.py +3620 -0
  129. foundry_mcp/tools/unified/context_helpers.py +98 -0
  130. foundry_mcp/tools/unified/documentation_helpers.py +268 -0
  131. foundry_mcp/tools/unified/environment.py +1341 -0
  132. foundry_mcp/tools/unified/error.py +479 -0
  133. foundry_mcp/tools/unified/health.py +225 -0
  134. foundry_mcp/tools/unified/journal.py +841 -0
  135. foundry_mcp/tools/unified/lifecycle.py +640 -0
  136. foundry_mcp/tools/unified/metrics.py +777 -0
  137. foundry_mcp/tools/unified/plan.py +876 -0
  138. foundry_mcp/tools/unified/pr.py +294 -0
  139. foundry_mcp/tools/unified/provider.py +589 -0
  140. foundry_mcp/tools/unified/research.py +1283 -0
  141. foundry_mcp/tools/unified/review.py +1042 -0
  142. foundry_mcp/tools/unified/review_helpers.py +314 -0
  143. foundry_mcp/tools/unified/router.py +102 -0
  144. foundry_mcp/tools/unified/server.py +565 -0
  145. foundry_mcp/tools/unified/spec.py +1283 -0
  146. foundry_mcp/tools/unified/task.py +3846 -0
  147. foundry_mcp/tools/unified/test.py +431 -0
  148. foundry_mcp/tools/unified/verification.py +520 -0
  149. foundry_mcp-0.8.22.dist-info/METADATA +344 -0
  150. foundry_mcp-0.8.22.dist-info/RECORD +153 -0
  151. foundry_mcp-0.8.22.dist-info/WHEEL +4 -0
  152. foundry_mcp-0.8.22.dist-info/entry_points.txt +3 -0
  153. foundry_mcp-0.8.22.dist-info/licenses/LICENSE +21 -0
@@ -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
+ ]