provide-foundation 0.0.0.dev0__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.
Files changed (149) hide show
  1. provide/__init__.py +15 -0
  2. provide/foundation/__init__.py +155 -0
  3. provide/foundation/_version.py +58 -0
  4. provide/foundation/cli/__init__.py +67 -0
  5. provide/foundation/cli/commands/__init__.py +3 -0
  6. provide/foundation/cli/commands/deps.py +71 -0
  7. provide/foundation/cli/commands/logs/__init__.py +63 -0
  8. provide/foundation/cli/commands/logs/generate.py +357 -0
  9. provide/foundation/cli/commands/logs/generate_old.py +569 -0
  10. provide/foundation/cli/commands/logs/query.py +174 -0
  11. provide/foundation/cli/commands/logs/send.py +166 -0
  12. provide/foundation/cli/commands/logs/tail.py +112 -0
  13. provide/foundation/cli/decorators.py +262 -0
  14. provide/foundation/cli/main.py +65 -0
  15. provide/foundation/cli/testing.py +220 -0
  16. provide/foundation/cli/utils.py +210 -0
  17. provide/foundation/config/__init__.py +106 -0
  18. provide/foundation/config/base.py +295 -0
  19. provide/foundation/config/env.py +369 -0
  20. provide/foundation/config/loader.py +311 -0
  21. provide/foundation/config/manager.py +387 -0
  22. provide/foundation/config/schema.py +284 -0
  23. provide/foundation/config/sync.py +281 -0
  24. provide/foundation/config/types.py +78 -0
  25. provide/foundation/config/validators.py +80 -0
  26. provide/foundation/console/__init__.py +29 -0
  27. provide/foundation/console/input.py +364 -0
  28. provide/foundation/console/output.py +178 -0
  29. provide/foundation/context/__init__.py +12 -0
  30. provide/foundation/context/core.py +356 -0
  31. provide/foundation/core.py +20 -0
  32. provide/foundation/crypto/__init__.py +182 -0
  33. provide/foundation/crypto/algorithms.py +111 -0
  34. provide/foundation/crypto/certificates.py +896 -0
  35. provide/foundation/crypto/checksums.py +301 -0
  36. provide/foundation/crypto/constants.py +57 -0
  37. provide/foundation/crypto/hashing.py +265 -0
  38. provide/foundation/crypto/keys.py +188 -0
  39. provide/foundation/crypto/signatures.py +144 -0
  40. provide/foundation/crypto/utils.py +164 -0
  41. provide/foundation/errors/__init__.py +96 -0
  42. provide/foundation/errors/auth.py +73 -0
  43. provide/foundation/errors/base.py +81 -0
  44. provide/foundation/errors/config.py +103 -0
  45. provide/foundation/errors/context.py +299 -0
  46. provide/foundation/errors/decorators.py +484 -0
  47. provide/foundation/errors/handlers.py +360 -0
  48. provide/foundation/errors/integration.py +105 -0
  49. provide/foundation/errors/platform.py +37 -0
  50. provide/foundation/errors/process.py +140 -0
  51. provide/foundation/errors/resources.py +133 -0
  52. provide/foundation/errors/runtime.py +160 -0
  53. provide/foundation/errors/safe_decorators.py +133 -0
  54. provide/foundation/errors/types.py +276 -0
  55. provide/foundation/file/__init__.py +79 -0
  56. provide/foundation/file/atomic.py +157 -0
  57. provide/foundation/file/directory.py +134 -0
  58. provide/foundation/file/formats.py +236 -0
  59. provide/foundation/file/lock.py +175 -0
  60. provide/foundation/file/safe.py +179 -0
  61. provide/foundation/file/utils.py +170 -0
  62. provide/foundation/hub/__init__.py +88 -0
  63. provide/foundation/hub/click_builder.py +310 -0
  64. provide/foundation/hub/commands.py +42 -0
  65. provide/foundation/hub/components.py +640 -0
  66. provide/foundation/hub/decorators.py +244 -0
  67. provide/foundation/hub/info.py +32 -0
  68. provide/foundation/hub/manager.py +446 -0
  69. provide/foundation/hub/registry.py +279 -0
  70. provide/foundation/hub/type_mapping.py +54 -0
  71. provide/foundation/hub/types.py +28 -0
  72. provide/foundation/logger/__init__.py +41 -0
  73. provide/foundation/logger/base.py +22 -0
  74. provide/foundation/logger/config/__init__.py +16 -0
  75. provide/foundation/logger/config/base.py +40 -0
  76. provide/foundation/logger/config/logging.py +394 -0
  77. provide/foundation/logger/config/telemetry.py +188 -0
  78. provide/foundation/logger/core.py +239 -0
  79. provide/foundation/logger/custom_processors.py +172 -0
  80. provide/foundation/logger/emoji/__init__.py +44 -0
  81. provide/foundation/logger/emoji/matrix.py +209 -0
  82. provide/foundation/logger/emoji/sets.py +458 -0
  83. provide/foundation/logger/emoji/types.py +56 -0
  84. provide/foundation/logger/factories.py +56 -0
  85. provide/foundation/logger/processors/__init__.py +13 -0
  86. provide/foundation/logger/processors/main.py +254 -0
  87. provide/foundation/logger/processors/trace.py +113 -0
  88. provide/foundation/logger/ratelimit/__init__.py +31 -0
  89. provide/foundation/logger/ratelimit/limiters.py +294 -0
  90. provide/foundation/logger/ratelimit/processor.py +203 -0
  91. provide/foundation/logger/ratelimit/queue_limiter.py +305 -0
  92. provide/foundation/logger/setup/__init__.py +29 -0
  93. provide/foundation/logger/setup/coordinator.py +138 -0
  94. provide/foundation/logger/setup/emoji_resolver.py +64 -0
  95. provide/foundation/logger/setup/processors.py +85 -0
  96. provide/foundation/logger/setup/testing.py +39 -0
  97. provide/foundation/logger/trace.py +38 -0
  98. provide/foundation/metrics/__init__.py +119 -0
  99. provide/foundation/metrics/otel.py +122 -0
  100. provide/foundation/metrics/simple.py +165 -0
  101. provide/foundation/observability/__init__.py +53 -0
  102. provide/foundation/observability/openobserve/__init__.py +79 -0
  103. provide/foundation/observability/openobserve/auth.py +72 -0
  104. provide/foundation/observability/openobserve/client.py +307 -0
  105. provide/foundation/observability/openobserve/commands.py +357 -0
  106. provide/foundation/observability/openobserve/exceptions.py +41 -0
  107. provide/foundation/observability/openobserve/formatters.py +298 -0
  108. provide/foundation/observability/openobserve/models.py +134 -0
  109. provide/foundation/observability/openobserve/otlp.py +320 -0
  110. provide/foundation/observability/openobserve/search.py +222 -0
  111. provide/foundation/observability/openobserve/streaming.py +235 -0
  112. provide/foundation/platform/__init__.py +44 -0
  113. provide/foundation/platform/detection.py +193 -0
  114. provide/foundation/platform/info.py +157 -0
  115. provide/foundation/process/__init__.py +39 -0
  116. provide/foundation/process/async_runner.py +373 -0
  117. provide/foundation/process/lifecycle.py +406 -0
  118. provide/foundation/process/runner.py +390 -0
  119. provide/foundation/setup/__init__.py +101 -0
  120. provide/foundation/streams/__init__.py +44 -0
  121. provide/foundation/streams/console.py +57 -0
  122. provide/foundation/streams/core.py +65 -0
  123. provide/foundation/streams/file.py +104 -0
  124. provide/foundation/testing/__init__.py +166 -0
  125. provide/foundation/testing/cli.py +227 -0
  126. provide/foundation/testing/crypto.py +163 -0
  127. provide/foundation/testing/fixtures.py +49 -0
  128. provide/foundation/testing/hub.py +23 -0
  129. provide/foundation/testing/logger.py +106 -0
  130. provide/foundation/testing/streams.py +54 -0
  131. provide/foundation/tracer/__init__.py +49 -0
  132. provide/foundation/tracer/context.py +115 -0
  133. provide/foundation/tracer/otel.py +135 -0
  134. provide/foundation/tracer/spans.py +174 -0
  135. provide/foundation/types.py +32 -0
  136. provide/foundation/utils/__init__.py +97 -0
  137. provide/foundation/utils/deps.py +195 -0
  138. provide/foundation/utils/env.py +491 -0
  139. provide/foundation/utils/formatting.py +483 -0
  140. provide/foundation/utils/parsing.py +235 -0
  141. provide/foundation/utils/rate_limiting.py +112 -0
  142. provide/foundation/utils/streams.py +67 -0
  143. provide/foundation/utils/timing.py +93 -0
  144. provide_foundation-0.0.0.dev0.dist-info/METADATA +469 -0
  145. provide_foundation-0.0.0.dev0.dist-info/RECORD +149 -0
  146. provide_foundation-0.0.0.dev0.dist-info/WHEEL +5 -0
  147. provide_foundation-0.0.0.dev0.dist-info/entry_points.txt +2 -0
  148. provide_foundation-0.0.0.dev0.dist-info/licenses/LICENSE +201 -0
  149. provide_foundation-0.0.0.dev0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,23 @@
1
+ """
2
+ Hub Testing Fixtures for Foundation.
3
+
4
+ Provides pytest fixtures for testing hub and component functionality,
5
+ including container directories and component registration scenarios.
6
+ """
7
+
8
+ from pathlib import Path
9
+ import tempfile
10
+
11
+ import pytest
12
+
13
+
14
+ @pytest.fixture(scope="session")
15
+ def default_container_directory():
16
+ """
17
+ Provides a default directory for container operations in tests.
18
+
19
+ This fixture is used by tests that need a temporary directory
20
+ for container-related operations.
21
+ """
22
+ with tempfile.TemporaryDirectory() as tmp_dir:
23
+ yield Path(tmp_dir)
@@ -0,0 +1,106 @@
1
+ #
2
+ # logger.py
3
+ #
4
+ """
5
+ Logger Testing Utilities for Foundation.
6
+
7
+ Provides utilities for resetting logger state, managing configurations,
8
+ and ensuring test isolation for the Foundation logging system.
9
+ """
10
+
11
+ import structlog
12
+
13
+ from provide.foundation.logger.core import (
14
+ _LAZY_SETUP_STATE,
15
+ logger as foundation_logger,
16
+ )
17
+ from provide.foundation.streams.file import reset_streams
18
+
19
+
20
+ def _reset_opentelemetry_providers() -> None:
21
+ """
22
+ Reset OpenTelemetry providers to uninitialized state.
23
+
24
+ This prevents "Overriding of current TracerProvider/MeterProvider" warnings
25
+ and stream closure issues by properly resetting the global providers.
26
+ """
27
+ try:
28
+ # Reset tracing provider by resetting the Once flag
29
+ import opentelemetry.trace as otel_trace
30
+
31
+ if hasattr(otel_trace, "_TRACER_PROVIDER_SET_ONCE"):
32
+ once_obj = otel_trace._TRACER_PROVIDER_SET_ONCE
33
+ if hasattr(once_obj, "_done"):
34
+ once_obj._done = False
35
+ # Reset to NoOpTracerProvider
36
+ from opentelemetry.trace import NoOpTracerProvider
37
+
38
+ otel_trace._TRACER_PROVIDER = NoOpTracerProvider()
39
+ except ImportError:
40
+ # OpenTelemetry tracing not available
41
+ pass
42
+ except Exception:
43
+ # Ignore errors during reset
44
+ pass
45
+
46
+ try:
47
+ # Reset metrics provider by resetting the Once flag
48
+ import opentelemetry.metrics._internal as otel_metrics_internal
49
+
50
+ if hasattr(otel_metrics_internal, "_METER_PROVIDER_SET_ONCE"):
51
+ once_obj = otel_metrics_internal._METER_PROVIDER_SET_ONCE
52
+ if hasattr(once_obj, "_done"):
53
+ once_obj._done = False
54
+ # Reset to NoOpMeterProvider
55
+ from opentelemetry.metrics import NoOpMeterProvider
56
+
57
+ otel_metrics_internal._METER_PROVIDER = NoOpMeterProvider()
58
+ except ImportError:
59
+ # OpenTelemetry metrics not available
60
+ pass
61
+ except Exception:
62
+ # Ignore errors during reset
63
+ pass
64
+
65
+
66
+ def reset_foundation_state() -> None:
67
+ """
68
+ Internal function to reset structlog and Foundation's state.
69
+
70
+ This resets:
71
+ - structlog configuration to defaults
72
+ - Foundation logger state and configuration
73
+ - Stream state back to defaults
74
+ - Lazy setup state tracking
75
+ - OpenTelemetry provider state (if available)
76
+ """
77
+ # Reset structlog to its default unconfigured state
78
+ structlog.reset_defaults()
79
+
80
+ # Reset stream state
81
+ reset_streams()
82
+
83
+ # Reset OpenTelemetry providers to avoid "Overriding" warnings and stream closure
84
+ _reset_opentelemetry_providers()
85
+
86
+ # Reset foundation logger state
87
+ foundation_logger._is_configured_by_setup = False
88
+ foundation_logger._active_config = None
89
+ foundation_logger._active_resolved_emoji_config = None
90
+ _LAZY_SETUP_STATE.update({"done": False, "error": None, "in_progress": False})
91
+
92
+
93
+ def reset_foundation_setup_for_testing() -> None:
94
+ """
95
+ Public test utility to reset Foundation's internal state.
96
+
97
+ This function ensures clean test isolation by resetting all
98
+ Foundation logging state between test runs.
99
+ """
100
+ reset_foundation_state()
101
+
102
+
103
+ __all__ = [
104
+ "reset_foundation_setup_for_testing",
105
+ "reset_foundation_state",
106
+ ]
@@ -0,0 +1,54 @@
1
+ #
2
+ # streams.py
3
+ #
4
+ """
5
+ Stream Testing Utilities for Foundation.
6
+
7
+ Provides utilities for redirecting and managing streams during testing,
8
+ allowing tests to capture and control Foundation's output streams.
9
+ """
10
+
11
+ from typing import TextIO
12
+
13
+ # Import the actual stream management variables
14
+ from provide.foundation.streams.core import get_log_stream
15
+
16
+
17
+ def set_log_stream_for_testing(stream: TextIO | None) -> None:
18
+ """
19
+ Set the log stream for testing purposes.
20
+
21
+ This allows tests to redirect Foundation's log output to a custom stream
22
+ (like StringIO) for capturing and verifying log messages.
23
+
24
+ Args:
25
+ stream: Stream to redirect to, or None to reset to stderr
26
+ """
27
+ # Import the actual implementation from streams.core
28
+ from provide.foundation.streams.core import (
29
+ set_log_stream_for_testing as _set_stream,
30
+ )
31
+
32
+ _set_stream(stream)
33
+
34
+
35
+ def get_current_log_stream() -> TextIO:
36
+ """
37
+ Get the currently active log stream.
38
+
39
+ Returns:
40
+ The current log stream being used by Foundation
41
+ """
42
+ return get_log_stream()
43
+
44
+
45
+ def reset_log_stream() -> None:
46
+ """Reset log stream back to stderr."""
47
+ set_log_stream_for_testing(None)
48
+
49
+
50
+ __all__ = [
51
+ "get_current_log_stream",
52
+ "reset_log_stream",
53
+ "set_log_stream_for_testing",
54
+ ]
@@ -0,0 +1,49 @@
1
+ #
2
+ # __init__.py
3
+ #
4
+ """
5
+ Foundation Tracer Module.
6
+
7
+ Provides distributed tracing functionality with optional OpenTelemetry integration.
8
+ Falls back to simple, lightweight tracing when OpenTelemetry is not available.
9
+ """
10
+
11
+ # OpenTelemetry feature detection
12
+ try:
13
+ from opentelemetry import trace as otel_trace
14
+ from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import (
15
+ OTLPSpanExporter as OTLPGrpcSpanExporter,
16
+ )
17
+ from opentelemetry.exporter.otlp.proto.http.trace_exporter import (
18
+ OTLPSpanExporter as OTLPHttpSpanExporter,
19
+ )
20
+ from opentelemetry.sdk.trace import TracerProvider
21
+ from opentelemetry.sdk.trace.export import BatchSpanProcessor
22
+
23
+ _HAS_OTEL = True
24
+ except ImportError:
25
+ otel_trace = None
26
+ TracerProvider = None
27
+ BatchSpanProcessor = None
28
+ OTLPGrpcSpanExporter = None
29
+ OTLPHttpSpanExporter = None
30
+ _HAS_OTEL = False
31
+
32
+ from provide.foundation.tracer.context import (
33
+ get_current_span,
34
+ get_current_trace_id,
35
+ get_trace_context,
36
+ set_current_span,
37
+ with_span,
38
+ )
39
+ from provide.foundation.tracer.spans import Span
40
+
41
+ __all__ = [
42
+ "_HAS_OTEL", # For internal use
43
+ "Span",
44
+ "get_current_span",
45
+ "get_current_trace_id",
46
+ "get_trace_context",
47
+ "set_current_span",
48
+ "with_span",
49
+ ]
@@ -0,0 +1,115 @@
1
+ #
2
+ # context.py
3
+ #
4
+ """
5
+ Trace context management for Foundation tracer.
6
+ Manages trace context and span hierarchy.
7
+ """
8
+
9
+ import contextvars
10
+ from typing import Any
11
+
12
+ from provide.foundation.tracer.spans import Span
13
+
14
+ # Context variable to track the current span
15
+ _current_span: contextvars.ContextVar[Span | None] = contextvars.ContextVar(
16
+ "current_span", default=None
17
+ )
18
+
19
+ # Context variable to track the current trace ID
20
+ _current_trace_id: contextvars.ContextVar[str | None] = contextvars.ContextVar(
21
+ "current_trace_id", default=None
22
+ )
23
+
24
+
25
+ def get_current_span() -> Span | None:
26
+ """Get the currently active span."""
27
+ return _current_span.get()
28
+
29
+
30
+ def get_current_trace_id() -> str | None:
31
+ """Get the current trace ID."""
32
+ return _current_trace_id.get()
33
+
34
+
35
+ def set_current_span(span: Span | None) -> None:
36
+ """Set the current active span."""
37
+ _current_span.set(span)
38
+ if span:
39
+ _current_trace_id.set(span.trace_id)
40
+
41
+
42
+ def create_child_span(name: str, parent: Span | None = None) -> Span:
43
+ """
44
+ Create a child span.
45
+
46
+ Args:
47
+ name: Name of the span
48
+ parent: Parent span, defaults to current span
49
+
50
+ Returns:
51
+ New child span
52
+ """
53
+ if parent is None:
54
+ parent = get_current_span()
55
+
56
+ if parent:
57
+ return Span(name=name, parent_id=parent.span_id, trace_id=parent.trace_id)
58
+ else:
59
+ return Span(name=name)
60
+
61
+
62
+ class SpanContext:
63
+ """
64
+ Context manager for managing span lifecycle.
65
+
66
+ Automatically sets and clears the current span.
67
+ """
68
+
69
+ def __init__(self, span: Span):
70
+ self.span = span
71
+ self.previous_span: Span | None = None
72
+
73
+ def __enter__(self) -> Span:
74
+ """Enter the span context."""
75
+ self.previous_span = get_current_span()
76
+ set_current_span(self.span)
77
+ return self.span
78
+
79
+ def __exit__(self, exc_type, exc_val, exc_tb):
80
+ """Exit the span context."""
81
+ if exc_type is not None:
82
+ self.span.set_error(f"{exc_type.__name__}: {exc_val}")
83
+ self.span.finish()
84
+ set_current_span(self.previous_span)
85
+
86
+
87
+ def with_span(name: str) -> SpanContext:
88
+ """
89
+ Create a new span context.
90
+
91
+ Args:
92
+ name: Name of the span
93
+
94
+ Returns:
95
+ SpanContext that can be used as a context manager
96
+ """
97
+ span = create_child_span(name)
98
+ return SpanContext(span)
99
+
100
+
101
+ def get_trace_context() -> dict[str, Any]:
102
+ """
103
+ Get the current trace context information.
104
+
105
+ Returns:
106
+ Dictionary with trace context information
107
+ """
108
+ current_span = get_current_span()
109
+ trace_id = get_current_trace_id()
110
+
111
+ return {
112
+ "trace_id": trace_id,
113
+ "span_id": current_span.span_id if current_span else None,
114
+ "span_name": current_span.name if current_span else None,
115
+ }
@@ -0,0 +1,135 @@
1
+ """OpenTelemetry integration for Foundation tracer."""
2
+
3
+ from provide.foundation.logger import get_logger
4
+ from provide.foundation.logger.config.telemetry import TelemetryConfig
5
+
6
+ log = get_logger(__name__)
7
+
8
+ # Feature detection
9
+ try:
10
+ from opentelemetry import trace as otel_trace
11
+ from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import (
12
+ OTLPSpanExporter as OTLPGrpcSpanExporter,
13
+ )
14
+ from opentelemetry.exporter.otlp.proto.http.trace_exporter import (
15
+ OTLPSpanExporter as OTLPHttpSpanExporter,
16
+ )
17
+ from opentelemetry.sdk.resources import Resource
18
+ from opentelemetry.sdk.trace import TracerProvider
19
+ from opentelemetry.sdk.trace.export import BatchSpanProcessor
20
+ from opentelemetry.sdk.trace.sampling import TraceIdRatioBased
21
+
22
+ _HAS_OTEL = True
23
+ except ImportError:
24
+ _HAS_OTEL = False
25
+ # Stub everything for type hints
26
+ otel_trace = None
27
+ TracerProvider = None
28
+ BatchSpanProcessor = None
29
+ Resource = None
30
+ OTLPGrpcSpanExporter = None
31
+ OTLPHttpSpanExporter = None
32
+ TraceIdRatioBased = None
33
+
34
+
35
+ def _require_otel() -> None:
36
+ """Ensure OpenTelemetry is available."""
37
+ if not _HAS_OTEL:
38
+ raise ImportError(
39
+ "OpenTelemetry features require optional dependencies. "
40
+ "Install with: pip install 'provide-foundation[opentelemetry]'"
41
+ )
42
+
43
+
44
+ def setup_opentelemetry_tracing(config: TelemetryConfig) -> None:
45
+ """Setup OpenTelemetry tracing with configuration.
46
+
47
+ Args:
48
+ config: Telemetry configuration
49
+ """
50
+ # Check if tracing is disabled first, before checking dependencies
51
+ if not config.tracing_enabled or config.globally_disabled:
52
+ log.debug("🔍 OpenTelemetry tracing disabled")
53
+ return
54
+
55
+ # Check if OpenTelemetry is available
56
+ if not _HAS_OTEL:
57
+ log.debug("🔍 OpenTelemetry tracing not available (dependencies not installed)")
58
+ return
59
+
60
+ log.debug("🔍🚀 Setting up OpenTelemetry tracing")
61
+
62
+ # Create resource with service information
63
+ resource_attrs = {}
64
+ if config.service_name:
65
+ resource_attrs["service.name"] = config.service_name
66
+ if config.service_version:
67
+ resource_attrs["service.version"] = config.service_version
68
+
69
+ resource = Resource.create(resource_attrs)
70
+
71
+ # Create tracer provider with sampling
72
+ sampler = TraceIdRatioBased(config.trace_sample_rate)
73
+ tracer_provider = TracerProvider(resource=resource, sampler=sampler)
74
+
75
+ # Setup OTLP exporter if endpoint is configured
76
+ if config.otlp_endpoint or config.otlp_traces_endpoint:
77
+ endpoint = config.otlp_traces_endpoint or config.otlp_endpoint
78
+ headers = config.get_otlp_headers_dict()
79
+
80
+ log.debug(f"🔍📤 Configuring OTLP exporter: {endpoint}")
81
+
82
+ # Choose exporter based on protocol
83
+ if config.otlp_protocol == "grpc":
84
+ exporter = OTLPGrpcSpanExporter(
85
+ endpoint=endpoint,
86
+ headers=headers,
87
+ )
88
+ else: # http/protobuf
89
+ exporter = OTLPHttpSpanExporter(
90
+ endpoint=endpoint,
91
+ headers=headers,
92
+ )
93
+
94
+ # Add batch processor
95
+ processor = BatchSpanProcessor(exporter)
96
+ tracer_provider.add_span_processor(processor)
97
+
98
+ log.debug(f"✅ OTLP span exporter configured: {config.otlp_protocol}")
99
+
100
+ # Set the global tracer provider
101
+ otel_trace.set_tracer_provider(tracer_provider)
102
+
103
+ log.info("🔍✅ OpenTelemetry tracing setup complete")
104
+
105
+
106
+ def get_otel_tracer(name: str) -> "otel_trace.Tracer | None":
107
+ """Get OpenTelemetry tracer if available.
108
+
109
+ Args:
110
+ name: Name for the tracer
111
+
112
+ Returns:
113
+ OpenTelemetry tracer or None if not available
114
+ """
115
+ if not _HAS_OTEL:
116
+ return None
117
+
118
+ try:
119
+ return otel_trace.get_tracer(name)
120
+ except Exception:
121
+ return None
122
+
123
+
124
+ def shutdown_opentelemetry() -> None:
125
+ """Shutdown OpenTelemetry tracing."""
126
+ if not _HAS_OTEL:
127
+ return
128
+
129
+ try:
130
+ tracer_provider = otel_trace.get_tracer_provider()
131
+ if hasattr(tracer_provider, "shutdown"):
132
+ tracer_provider.shutdown()
133
+ log.debug("🔍🛑 OpenTelemetry tracer provider shutdown")
134
+ except Exception as e:
135
+ log.warning(f"⚠️ Error shutting down OpenTelemetry: {e}")
@@ -0,0 +1,174 @@
1
+ #
2
+ # spans.py
3
+ #
4
+ """
5
+ Enhanced span implementation for Foundation tracer.
6
+ Provides OpenTelemetry integration when available, falls back to simple tracing.
7
+ """
8
+
9
+ from dataclasses import dataclass, field
10
+ import time
11
+ from typing import Any, Optional
12
+ import uuid
13
+
14
+ from provide.foundation.logger import get_logger
15
+
16
+ log = get_logger(__name__)
17
+
18
+ # OpenTelemetry feature detection
19
+ try:
20
+ from opentelemetry import trace as otel_trace
21
+ from opentelemetry.trace import Status, StatusCode
22
+
23
+ _HAS_OTEL = True
24
+ except ImportError:
25
+ otel_trace = None
26
+ Status = None
27
+ StatusCode = None
28
+ _HAS_OTEL = False
29
+
30
+
31
+ @dataclass
32
+ class Span:
33
+ """
34
+ Enhanced span implementation with optional OpenTelemetry integration.
35
+
36
+ Maintains simple API while providing distributed tracing when OpenTelemetry is available.
37
+ """
38
+
39
+ name: str
40
+ span_id: str = field(default_factory=lambda: str(uuid.uuid4()))
41
+ parent_id: str | None = None
42
+ trace_id: str = field(default_factory=lambda: str(uuid.uuid4()))
43
+ start_time: float = field(default_factory=time.time)
44
+ end_time: float | None = None
45
+ tags: dict[str, Any] = field(default_factory=dict)
46
+ status: str = "ok"
47
+ error: str | None = None
48
+
49
+ # Internal OpenTelemetry span (when available)
50
+ _otel_span: Optional["otel_trace.Span"] = field(
51
+ default=None, init=False, repr=False
52
+ )
53
+ _active: bool = field(default=True, init=False, repr=False)
54
+
55
+ def __post_init__(self) -> None:
56
+ """Initialize span after creation."""
57
+ # Try to create OpenTelemetry span if available
58
+ if _HAS_OTEL:
59
+ try:
60
+ tracer = otel_trace.get_tracer(__name__)
61
+ self._otel_span = tracer.start_span(self.name)
62
+
63
+ log.debug(f"🔍✨ Created OpenTelemetry span: {self.name}")
64
+ except Exception as e:
65
+ log.debug(f"🔍⚠️ Failed to create OpenTelemetry span: {e}")
66
+ self._otel_span = None
67
+
68
+ def set_tag(self, key: str, value: Any) -> None:
69
+ """Set a tag on the span."""
70
+ self.tags[key] = value
71
+
72
+ # Also set on OpenTelemetry span if available
73
+ if self._otel_span and hasattr(self._otel_span, "set_attribute"):
74
+ try:
75
+ self._otel_span.set_attribute(key, value)
76
+ except Exception as e:
77
+ log.debug(f"🔍⚠️ Failed to set OpenTelemetry attribute: {e}")
78
+
79
+ def set_error(self, error: str | Exception) -> None:
80
+ """Mark the span as having an error."""
81
+ self.status = "error"
82
+ self.error = str(error)
83
+
84
+ # Also set on OpenTelemetry span if available
85
+ if self._otel_span and Status and StatusCode:
86
+ try:
87
+ self._otel_span.set_status(Status(StatusCode.ERROR, str(error)))
88
+ self._otel_span.record_exception(
89
+ error if isinstance(error, Exception) else Exception(error)
90
+ )
91
+ except Exception as e:
92
+ log.debug(f"🔍⚠️ Failed to set OpenTelemetry error: {e}")
93
+
94
+ def finish(self) -> None:
95
+ """Finish the span and record end time."""
96
+ if self._active:
97
+ self.end_time = time.time()
98
+ self._active = False
99
+
100
+ # Also finish OpenTelemetry span if available
101
+ if self._otel_span:
102
+ try:
103
+ self._otel_span.end()
104
+ log.debug(f"🔍✅ Finished OpenTelemetry span: {self.name}")
105
+ except Exception as e:
106
+ log.debug(f"🔍⚠️ Failed to finish OpenTelemetry span: {e}")
107
+
108
+ def __enter__(self) -> "Span":
109
+ """Context manager entry."""
110
+ # Set this span as current in OpenTelemetry context if available
111
+ if self._otel_span and _HAS_OTEL:
112
+ try:
113
+ # OpenTelemetry spans are automatically set as current when started
114
+ pass
115
+ except Exception as e:
116
+ log.debug(f"🔍⚠️ Failed to set OpenTelemetry span context: {e}")
117
+
118
+ # Also set in Foundation tracer context
119
+ try:
120
+ from provide.foundation.tracer.context import set_current_span
121
+
122
+ set_current_span(self)
123
+ except Exception as e:
124
+ log.debug(f"🔍⚠️ Failed to set Foundation span context: {e}")
125
+
126
+ return self
127
+
128
+ def __exit__(self, exc_type, exc_val, exc_tb) -> None:
129
+ """Context manager exit."""
130
+ # Handle exceptions
131
+ if exc_type is not None:
132
+ self.set_error(exc_val if exc_val else exc_type.__name__)
133
+
134
+ # Finish the span
135
+ self.finish()
136
+
137
+ # Clear from Foundation tracer context
138
+ try:
139
+ from provide.foundation.tracer.context import set_current_span
140
+
141
+ set_current_span(None)
142
+ except Exception as e:
143
+ log.debug(f"🔍⚠️ Failed to clear Foundation span context: {e}")
144
+
145
+ def duration_ms(self) -> float:
146
+ """Get the duration of the span in milliseconds."""
147
+ if self.end_time is None:
148
+ return (time.time() - self.start_time) * 1000
149
+ return (self.end_time - self.start_time) * 1000
150
+
151
+ def to_dict(self) -> dict[str, Any]:
152
+ """Convert span to dictionary representation."""
153
+ return {
154
+ "name": self.name,
155
+ "span_id": self.span_id,
156
+ "parent_id": self.parent_id,
157
+ "trace_id": self.trace_id,
158
+ "start_time": self.start_time,
159
+ "end_time": self.end_time,
160
+ "duration_ms": self.duration_ms(),
161
+ "tags": self.tags,
162
+ "status": self.status,
163
+ "error": self.error,
164
+ }
165
+
166
+ def __enter__(self):
167
+ """Context manager entry."""
168
+ return self
169
+
170
+ def __exit__(self, exc_type, exc_val, exc_tb):
171
+ """Context manager exit."""
172
+ if exc_type is not None:
173
+ self.set_error(f"{exc_type.__name__}: {exc_val}")
174
+ self.finish()
@@ -0,0 +1,32 @@
1
+ """
2
+ Core type definitions and constants for Foundation.
3
+ """
4
+
5
+ from typing import Literal
6
+
7
+ from provide.foundation.logger.trace import TRACE_LEVEL_NAME, TRACE_LEVEL_NUM
8
+
9
+ LogLevelStr = Literal[
10
+ "CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG", "TRACE", "NOTSET"
11
+ ]
12
+
13
+ _VALID_LOG_LEVEL_TUPLE: tuple[LogLevelStr, ...] = (
14
+ "CRITICAL",
15
+ "ERROR",
16
+ "WARNING",
17
+ "INFO",
18
+ "DEBUG",
19
+ "TRACE",
20
+ "NOTSET",
21
+ )
22
+
23
+ ConsoleFormatterStr = Literal["key_value", "json"]
24
+
25
+ _VALID_FORMATTER_TUPLE: tuple[ConsoleFormatterStr, ...] = ("key_value", "json")
26
+
27
+ __all__ = [
28
+ "TRACE_LEVEL_NAME",
29
+ "TRACE_LEVEL_NUM",
30
+ "ConsoleFormatterStr",
31
+ "LogLevelStr",
32
+ ]