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.
- provide/__init__.py +15 -0
- provide/foundation/__init__.py +155 -0
- provide/foundation/_version.py +58 -0
- provide/foundation/cli/__init__.py +67 -0
- provide/foundation/cli/commands/__init__.py +3 -0
- provide/foundation/cli/commands/deps.py +71 -0
- provide/foundation/cli/commands/logs/__init__.py +63 -0
- provide/foundation/cli/commands/logs/generate.py +357 -0
- provide/foundation/cli/commands/logs/generate_old.py +569 -0
- provide/foundation/cli/commands/logs/query.py +174 -0
- provide/foundation/cli/commands/logs/send.py +166 -0
- provide/foundation/cli/commands/logs/tail.py +112 -0
- provide/foundation/cli/decorators.py +262 -0
- provide/foundation/cli/main.py +65 -0
- provide/foundation/cli/testing.py +220 -0
- provide/foundation/cli/utils.py +210 -0
- provide/foundation/config/__init__.py +106 -0
- provide/foundation/config/base.py +295 -0
- provide/foundation/config/env.py +369 -0
- provide/foundation/config/loader.py +311 -0
- provide/foundation/config/manager.py +387 -0
- provide/foundation/config/schema.py +284 -0
- provide/foundation/config/sync.py +281 -0
- provide/foundation/config/types.py +78 -0
- provide/foundation/config/validators.py +80 -0
- provide/foundation/console/__init__.py +29 -0
- provide/foundation/console/input.py +364 -0
- provide/foundation/console/output.py +178 -0
- provide/foundation/context/__init__.py +12 -0
- provide/foundation/context/core.py +356 -0
- provide/foundation/core.py +20 -0
- provide/foundation/crypto/__init__.py +182 -0
- provide/foundation/crypto/algorithms.py +111 -0
- provide/foundation/crypto/certificates.py +896 -0
- provide/foundation/crypto/checksums.py +301 -0
- provide/foundation/crypto/constants.py +57 -0
- provide/foundation/crypto/hashing.py +265 -0
- provide/foundation/crypto/keys.py +188 -0
- provide/foundation/crypto/signatures.py +144 -0
- provide/foundation/crypto/utils.py +164 -0
- provide/foundation/errors/__init__.py +96 -0
- provide/foundation/errors/auth.py +73 -0
- provide/foundation/errors/base.py +81 -0
- provide/foundation/errors/config.py +103 -0
- provide/foundation/errors/context.py +299 -0
- provide/foundation/errors/decorators.py +484 -0
- provide/foundation/errors/handlers.py +360 -0
- provide/foundation/errors/integration.py +105 -0
- provide/foundation/errors/platform.py +37 -0
- provide/foundation/errors/process.py +140 -0
- provide/foundation/errors/resources.py +133 -0
- provide/foundation/errors/runtime.py +160 -0
- provide/foundation/errors/safe_decorators.py +133 -0
- provide/foundation/errors/types.py +276 -0
- provide/foundation/file/__init__.py +79 -0
- provide/foundation/file/atomic.py +157 -0
- provide/foundation/file/directory.py +134 -0
- provide/foundation/file/formats.py +236 -0
- provide/foundation/file/lock.py +175 -0
- provide/foundation/file/safe.py +179 -0
- provide/foundation/file/utils.py +170 -0
- provide/foundation/hub/__init__.py +88 -0
- provide/foundation/hub/click_builder.py +310 -0
- provide/foundation/hub/commands.py +42 -0
- provide/foundation/hub/components.py +640 -0
- provide/foundation/hub/decorators.py +244 -0
- provide/foundation/hub/info.py +32 -0
- provide/foundation/hub/manager.py +446 -0
- provide/foundation/hub/registry.py +279 -0
- provide/foundation/hub/type_mapping.py +54 -0
- provide/foundation/hub/types.py +28 -0
- provide/foundation/logger/__init__.py +41 -0
- provide/foundation/logger/base.py +22 -0
- provide/foundation/logger/config/__init__.py +16 -0
- provide/foundation/logger/config/base.py +40 -0
- provide/foundation/logger/config/logging.py +394 -0
- provide/foundation/logger/config/telemetry.py +188 -0
- provide/foundation/logger/core.py +239 -0
- provide/foundation/logger/custom_processors.py +172 -0
- provide/foundation/logger/emoji/__init__.py +44 -0
- provide/foundation/logger/emoji/matrix.py +209 -0
- provide/foundation/logger/emoji/sets.py +458 -0
- provide/foundation/logger/emoji/types.py +56 -0
- provide/foundation/logger/factories.py +56 -0
- provide/foundation/logger/processors/__init__.py +13 -0
- provide/foundation/logger/processors/main.py +254 -0
- provide/foundation/logger/processors/trace.py +113 -0
- provide/foundation/logger/ratelimit/__init__.py +31 -0
- provide/foundation/logger/ratelimit/limiters.py +294 -0
- provide/foundation/logger/ratelimit/processor.py +203 -0
- provide/foundation/logger/ratelimit/queue_limiter.py +305 -0
- provide/foundation/logger/setup/__init__.py +29 -0
- provide/foundation/logger/setup/coordinator.py +138 -0
- provide/foundation/logger/setup/emoji_resolver.py +64 -0
- provide/foundation/logger/setup/processors.py +85 -0
- provide/foundation/logger/setup/testing.py +39 -0
- provide/foundation/logger/trace.py +38 -0
- provide/foundation/metrics/__init__.py +119 -0
- provide/foundation/metrics/otel.py +122 -0
- provide/foundation/metrics/simple.py +165 -0
- provide/foundation/observability/__init__.py +53 -0
- provide/foundation/observability/openobserve/__init__.py +79 -0
- provide/foundation/observability/openobserve/auth.py +72 -0
- provide/foundation/observability/openobserve/client.py +307 -0
- provide/foundation/observability/openobserve/commands.py +357 -0
- provide/foundation/observability/openobserve/exceptions.py +41 -0
- provide/foundation/observability/openobserve/formatters.py +298 -0
- provide/foundation/observability/openobserve/models.py +134 -0
- provide/foundation/observability/openobserve/otlp.py +320 -0
- provide/foundation/observability/openobserve/search.py +222 -0
- provide/foundation/observability/openobserve/streaming.py +235 -0
- provide/foundation/platform/__init__.py +44 -0
- provide/foundation/platform/detection.py +193 -0
- provide/foundation/platform/info.py +157 -0
- provide/foundation/process/__init__.py +39 -0
- provide/foundation/process/async_runner.py +373 -0
- provide/foundation/process/lifecycle.py +406 -0
- provide/foundation/process/runner.py +390 -0
- provide/foundation/setup/__init__.py +101 -0
- provide/foundation/streams/__init__.py +44 -0
- provide/foundation/streams/console.py +57 -0
- provide/foundation/streams/core.py +65 -0
- provide/foundation/streams/file.py +104 -0
- provide/foundation/testing/__init__.py +166 -0
- provide/foundation/testing/cli.py +227 -0
- provide/foundation/testing/crypto.py +163 -0
- provide/foundation/testing/fixtures.py +49 -0
- provide/foundation/testing/hub.py +23 -0
- provide/foundation/testing/logger.py +106 -0
- provide/foundation/testing/streams.py +54 -0
- provide/foundation/tracer/__init__.py +49 -0
- provide/foundation/tracer/context.py +115 -0
- provide/foundation/tracer/otel.py +135 -0
- provide/foundation/tracer/spans.py +174 -0
- provide/foundation/types.py +32 -0
- provide/foundation/utils/__init__.py +97 -0
- provide/foundation/utils/deps.py +195 -0
- provide/foundation/utils/env.py +491 -0
- provide/foundation/utils/formatting.py +483 -0
- provide/foundation/utils/parsing.py +235 -0
- provide/foundation/utils/rate_limiting.py +112 -0
- provide/foundation/utils/streams.py +67 -0
- provide/foundation/utils/timing.py +93 -0
- provide_foundation-0.0.0.dev0.dist-info/METADATA +469 -0
- provide_foundation-0.0.0.dev0.dist-info/RECORD +149 -0
- provide_foundation-0.0.0.dev0.dist-info/WHEEL +5 -0
- provide_foundation-0.0.0.dev0.dist-info/entry_points.txt +2 -0
- provide_foundation-0.0.0.dev0.dist-info/licenses/LICENSE +201 -0
- 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
|
+
]
|