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,64 @@
1
+ #
2
+ # emoji_resolver.py
3
+ #
4
+ """
5
+ Emoji configuration resolution for Foundation Telemetry.
6
+ Handles the merging and resolution of emoji set configurations from multiple sources.
7
+ """
8
+
9
+ from provide.foundation.logger.config import LoggingConfig
10
+ from provide.foundation.logger.emoji.sets import LEGACY_DAS_EMOJI_SETS
11
+ from provide.foundation.logger.emoji.types import (
12
+ EmojiSet,
13
+ EmojiSetConfig,
14
+ FieldToEmojiMapping,
15
+ )
16
+
17
+ ResolvedEmojiConfig = tuple[list[FieldToEmojiMapping], dict[str, EmojiSet]]
18
+
19
+
20
+ def resolve_active_emoji_config(
21
+ logging_config: LoggingConfig, builtin_emoji_registry: dict[str, EmojiSetConfig]
22
+ ) -> ResolvedEmojiConfig:
23
+ """
24
+ Resolve the active emoji configuration from multiple sources.
25
+
26
+ Args:
27
+ logging_config: The logging configuration
28
+ builtin_emoji_registry: Registry of built-in emoji sets
29
+
30
+ Returns:
31
+ Tuple of (field_definitions, emoji_sets_dict)
32
+ """
33
+ resolved_fields_dict: dict[str, FieldToEmojiMapping] = {}
34
+ resolved_emoji_sets_dict: dict[str, EmojiSet] = {
35
+ s.name: s for s in LEGACY_DAS_EMOJI_SETS
36
+ }
37
+
38
+ emoji_sets_to_process: list[EmojiSetConfig] = [
39
+ builtin_emoji_registry[name]
40
+ for name in logging_config.enabled_emoji_sets
41
+ if name in builtin_emoji_registry
42
+ ]
43
+ emoji_sets_to_process.extend(logging_config.custom_emoji_sets)
44
+ emoji_sets_to_process.sort(key=lambda emoji_set: emoji_set.priority)
45
+
46
+ ordered_log_keys: list[str] = []
47
+ seen_log_keys: set[str] = set()
48
+
49
+ for emoji_set_config in emoji_sets_to_process:
50
+ for emoji_set in emoji_set_config.emoji_sets:
51
+ resolved_emoji_sets_dict[emoji_set.name] = emoji_set
52
+ for field_def in emoji_set_config.field_definitions:
53
+ resolved_fields_dict[field_def.log_key] = field_def
54
+ if field_def.log_key not in seen_log_keys:
55
+ ordered_log_keys.append(field_def.log_key)
56
+ seen_log_keys.add(field_def.log_key)
57
+
58
+ for user_emoji_set in logging_config.user_defined_emoji_sets:
59
+ resolved_emoji_sets_dict[user_emoji_set.name] = user_emoji_set
60
+
61
+ final_ordered_field_definitions = [
62
+ resolved_fields_dict[log_key] for log_key in ordered_log_keys
63
+ ]
64
+ return final_ordered_field_definitions, resolved_emoji_sets_dict
@@ -0,0 +1,85 @@
1
+ #
2
+ # processors.py
3
+ #
4
+ """
5
+ Processor chain building for Foundation Telemetry.
6
+ Handles the assembly of structlog processor chains including emoji processing.
7
+ """
8
+
9
+ from typing import Any, TextIO, cast
10
+
11
+ import structlog
12
+
13
+ from provide.foundation.logger.config import TelemetryConfig
14
+ from provide.foundation.logger.processors import (
15
+ _build_core_processors_list,
16
+ _build_formatter_processors_list,
17
+ )
18
+ from provide.foundation.logger.setup.emoji_resolver import ResolvedEmojiConfig
19
+
20
+
21
+ def build_complete_processor_chain(
22
+ config: TelemetryConfig,
23
+ resolved_emoji_config: ResolvedEmojiConfig,
24
+ log_stream: TextIO,
25
+ ) -> list[Any]:
26
+ """
27
+ Build the complete processor chain for structlog.
28
+
29
+ Args:
30
+ config: Telemetry configuration
31
+ resolved_emoji_config: Resolved emoji configuration
32
+ log_stream: Output stream for logging
33
+
34
+ Returns:
35
+ List of processors for structlog
36
+ """
37
+ core_processors = _build_core_processors_list(config, resolved_emoji_config)
38
+ formatter_processors = _build_formatter_processors_list(config.logging, log_stream)
39
+ return cast(list[Any], core_processors + formatter_processors)
40
+
41
+
42
+ def apply_structlog_configuration(processors: list[Any], log_stream: TextIO) -> None:
43
+ """
44
+ Apply the processor configuration to structlog.
45
+
46
+ Args:
47
+ processors: List of processors to configure
48
+ log_stream: Output stream for logging
49
+ """
50
+ structlog.configure(
51
+ processors=processors,
52
+ logger_factory=structlog.PrintLoggerFactory(file=log_stream),
53
+ wrapper_class=cast(type[structlog.types.BindableLogger], structlog.BoundLogger),
54
+ cache_logger_on_first_use=True,
55
+ )
56
+
57
+
58
+ def configure_structlog_output(
59
+ config: TelemetryConfig,
60
+ resolved_emoji_config: ResolvedEmojiConfig,
61
+ log_stream: TextIO,
62
+ ) -> None:
63
+ """
64
+ Configure structlog with the complete output chain.
65
+
66
+ Args:
67
+ config: Telemetry configuration
68
+ resolved_emoji_config: Resolved emoji configuration
69
+ log_stream: Output stream for logging
70
+ """
71
+ processors = build_complete_processor_chain(
72
+ config, resolved_emoji_config, log_stream
73
+ )
74
+ apply_structlog_configuration(processors, log_stream)
75
+
76
+
77
+ def handle_globally_disabled_setup() -> None:
78
+ """
79
+ Configure structlog for globally disabled telemetry (no-op mode).
80
+ """
81
+ structlog.configure(
82
+ processors=[],
83
+ logger_factory=structlog.ReturnLoggerFactory(),
84
+ cache_logger_on_first_use=True,
85
+ )
@@ -0,0 +1,39 @@
1
+ #
2
+ # testing.py
3
+ #
4
+ """
5
+ Testing utilities for Foundation Telemetry setup.
6
+ Provides functions to reset state and configure test environments.
7
+ """
8
+
9
+ import structlog
10
+
11
+ from provide.foundation.logger.core import (
12
+ _LAZY_SETUP_STATE,
13
+ logger as foundation_logger,
14
+ )
15
+ from provide.foundation.streams.file import reset_streams
16
+
17
+
18
+ def reset_foundation_state() -> None:
19
+ """
20
+ Internal function to reset structlog and Foundation Telemetry's state.
21
+ """
22
+ # Reset structlog configuration
23
+ structlog.reset_defaults()
24
+
25
+ # Reset stream state
26
+ reset_streams()
27
+
28
+ # Reset foundation logger state
29
+ foundation_logger._is_configured_by_setup = False
30
+ foundation_logger._active_config = None
31
+ foundation_logger._active_resolved_emoji_config = None
32
+ _LAZY_SETUP_STATE.update({"done": False, "error": None, "in_progress": False})
33
+
34
+
35
+ def reset_foundation_setup_for_testing() -> None:
36
+ """
37
+ Public test utility to reset Foundation Telemetry's internal state.
38
+ """
39
+ reset_foundation_state()
@@ -0,0 +1,38 @@
1
+ #
2
+ # trace.py
3
+ #
4
+ """
5
+ TRACE log level setup and patching.
6
+
7
+ This module handles the custom TRACE log level implementation,
8
+ including patching the standard library logging module.
9
+ """
10
+
11
+ import logging as stdlib_logging
12
+ from typing import Any, cast
13
+
14
+ # --- TRACE Level Constants ---
15
+ TRACE_LEVEL_NUM: int = 5 # Typically, DEBUG is 10, so TRACE is lower
16
+ """Numeric value for the custom TRACE log level."""
17
+
18
+ TRACE_LEVEL_NAME: str = "TRACE"
19
+ """String name for the custom TRACE log level."""
20
+
21
+ # Add TRACE to standard library logging if it doesn't exist
22
+ if not hasattr(stdlib_logging, TRACE_LEVEL_NAME): # pragma: no cover
23
+ stdlib_logging.addLevelName(TRACE_LEVEL_NUM, TRACE_LEVEL_NAME)
24
+
25
+ def trace(
26
+ self: stdlib_logging.Logger, message: str, *args: object, **kwargs: object
27
+ ) -> None: # pragma: no cover
28
+ if self.isEnabledFor(TRACE_LEVEL_NUM):
29
+ self._log(TRACE_LEVEL_NUM, message, args, **kwargs) # type: ignore[arg-type]
30
+
31
+ if not hasattr(stdlib_logging.Logger, "trace"): # pragma: no cover
32
+ stdlib_logging.Logger.trace = trace # type: ignore[attr-defined]
33
+ if stdlib_logging.root and not hasattr(
34
+ stdlib_logging.root, "trace"
35
+ ): # pragma: no cover
36
+ (cast(Any, stdlib_logging.root)).trace = trace.__get__(
37
+ stdlib_logging.root, stdlib_logging.Logger
38
+ )
@@ -0,0 +1,119 @@
1
+ """
2
+ Foundation Metrics Module.
3
+
4
+ Provides metrics collection with optional OpenTelemetry integration.
5
+ Falls back to simple metrics when OpenTelemetry is not available.
6
+ """
7
+
8
+ # OpenTelemetry feature detection
9
+ try:
10
+ from opentelemetry import metrics as otel_metrics
11
+ from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import (
12
+ OTLPMetricExporter as OTLPGrpcMetricExporter,
13
+ )
14
+ from opentelemetry.exporter.otlp.proto.http.metric_exporter import (
15
+ OTLPMetricExporter as OTLPHttpMetricExporter,
16
+ )
17
+ from opentelemetry.sdk.metrics import MeterProvider
18
+ from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
19
+
20
+ _HAS_OTEL_METRICS = True
21
+ except ImportError:
22
+ otel_metrics = None
23
+ MeterProvider = None
24
+ PeriodicExportingMetricReader = None
25
+ OTLPGrpcMetricExporter = None
26
+ OTLPHttpMetricExporter = None
27
+ _HAS_OTEL_METRICS = False
28
+
29
+ from provide.foundation.metrics.simple import (
30
+ SimpleCounter,
31
+ SimpleGauge,
32
+ SimpleHistogram,
33
+ )
34
+
35
+ # Export the main API
36
+ __all__ = [
37
+ "_HAS_OTEL_METRICS", # For internal use
38
+ "counter",
39
+ "gauge",
40
+ "histogram",
41
+ ]
42
+
43
+ # Global meter instance (will be set during setup)
44
+ _meter = None
45
+
46
+
47
+ def counter(name: str, description: str = "", unit: str = "") -> "SimpleCounter":
48
+ """Create a counter metric.
49
+
50
+ Args:
51
+ name: Name of the counter
52
+ description: Description of what this counter measures
53
+ unit: Unit of measurement
54
+
55
+ Returns:
56
+ Counter instance
57
+ """
58
+ if _HAS_OTEL_METRICS and _meter:
59
+ try:
60
+ otel_counter = _meter.create_counter(
61
+ name=name, description=description, unit=unit
62
+ )
63
+ return SimpleCounter(name, otel_counter=otel_counter)
64
+ except Exception:
65
+ pass
66
+
67
+ return SimpleCounter(name)
68
+
69
+
70
+ def gauge(name: str, description: str = "", unit: str = "") -> "SimpleGauge":
71
+ """Create a gauge metric.
72
+
73
+ Args:
74
+ name: Name of the gauge
75
+ description: Description of what this gauge measures
76
+ unit: Unit of measurement
77
+
78
+ Returns:
79
+ Gauge instance
80
+ """
81
+ if _HAS_OTEL_METRICS and _meter:
82
+ try:
83
+ otel_gauge = _meter.create_up_down_counter(
84
+ name=name, description=description, unit=unit
85
+ )
86
+ return SimpleGauge(name, otel_gauge=otel_gauge)
87
+ except Exception:
88
+ pass
89
+
90
+ return SimpleGauge(name)
91
+
92
+
93
+ def histogram(name: str, description: str = "", unit: str = "") -> "SimpleHistogram":
94
+ """Create a histogram metric.
95
+
96
+ Args:
97
+ name: Name of the histogram
98
+ description: Description of what this histogram measures
99
+ unit: Unit of measurement
100
+
101
+ Returns:
102
+ Histogram instance
103
+ """
104
+ if _HAS_OTEL_METRICS and _meter:
105
+ try:
106
+ otel_histogram = _meter.create_histogram(
107
+ name=name, description=description, unit=unit
108
+ )
109
+ return SimpleHistogram(name, otel_histogram=otel_histogram)
110
+ except Exception:
111
+ pass
112
+
113
+ return SimpleHistogram(name)
114
+
115
+
116
+ def _set_meter(meter) -> None:
117
+ """Set the global meter instance (internal use only)."""
118
+ global _meter
119
+ _meter = meter
@@ -0,0 +1,122 @@
1
+ """OpenTelemetry metrics integration."""
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 metrics as otel_metrics
11
+ from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import (
12
+ OTLPMetricExporter as OTLPGrpcMetricExporter,
13
+ )
14
+ from opentelemetry.exporter.otlp.proto.http.metric_exporter import (
15
+ OTLPMetricExporter as OTLPHttpMetricExporter,
16
+ )
17
+ from opentelemetry.sdk.metrics import MeterProvider
18
+ from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
19
+ from opentelemetry.sdk.resources import Resource
20
+
21
+ _HAS_OTEL_METRICS = True
22
+ except ImportError:
23
+ _HAS_OTEL_METRICS = False
24
+ # Stub everything
25
+ otel_metrics = None
26
+ MeterProvider = None
27
+ PeriodicExportingMetricReader = None
28
+ Resource = None
29
+ OTLPGrpcMetricExporter = None
30
+ OTLPHttpMetricExporter = None
31
+
32
+
33
+ def _require_otel_metrics() -> None:
34
+ """Ensure OpenTelemetry metrics are available."""
35
+ if not _HAS_OTEL_METRICS:
36
+ raise ImportError(
37
+ "OpenTelemetry metrics require optional dependencies. "
38
+ "Install with: pip install 'provide-foundation[opentelemetry]'"
39
+ )
40
+
41
+
42
+ def setup_opentelemetry_metrics(config: TelemetryConfig) -> None:
43
+ """Setup OpenTelemetry metrics with configuration.
44
+
45
+ Args:
46
+ config: Telemetry configuration
47
+ """
48
+ # Check if metrics are disabled first, before checking dependencies
49
+ if not config.metrics_enabled or config.globally_disabled:
50
+ log.debug("📊 OpenTelemetry metrics disabled")
51
+ return
52
+
53
+ # Check if OpenTelemetry metrics are available
54
+ if not _HAS_OTEL_METRICS:
55
+ log.debug("📊 OpenTelemetry metrics not available (dependencies not installed)")
56
+ return
57
+
58
+ log.debug("📊🚀 Setting up OpenTelemetry metrics")
59
+
60
+ # Create resource with service information
61
+ resource_attrs = {}
62
+ if config.service_name:
63
+ resource_attrs["service.name"] = config.service_name
64
+ if config.service_version:
65
+ resource_attrs["service.version"] = config.service_version
66
+
67
+ resource = Resource.create(resource_attrs)
68
+
69
+ # Setup metric readers with OTLP exporters if configured
70
+ readers = []
71
+
72
+ if config.otlp_endpoint:
73
+ endpoint = config.otlp_endpoint
74
+ headers = config.get_otlp_headers_dict()
75
+
76
+ log.debug(f"📊📤 Configuring OTLP metrics exporter: {endpoint}")
77
+
78
+ # Choose exporter based on protocol
79
+ if config.otlp_protocol == "grpc":
80
+ exporter = OTLPGrpcMetricExporter(
81
+ endpoint=endpoint,
82
+ headers=headers,
83
+ )
84
+ else: # http/protobuf
85
+ exporter = OTLPHttpMetricExporter(
86
+ endpoint=endpoint,
87
+ headers=headers,
88
+ )
89
+
90
+ # Create periodic reader (exports every 60 seconds by default)
91
+ reader = PeriodicExportingMetricReader(exporter, export_interval_millis=60000)
92
+ readers.append(reader)
93
+
94
+ log.debug(f"✅ OTLP metrics exporter configured: {config.otlp_protocol}")
95
+
96
+ # Create meter provider
97
+ meter_provider = MeterProvider(resource=resource, metric_readers=readers)
98
+
99
+ # Set the global meter provider
100
+ otel_metrics.set_meter_provider(meter_provider)
101
+
102
+ # Set the global meter for our metrics module
103
+ from provide.foundation.metrics import _set_meter
104
+
105
+ meter = otel_metrics.get_meter(__name__)
106
+ _set_meter(meter)
107
+
108
+ log.info("📊✅ OpenTelemetry metrics setup complete")
109
+
110
+
111
+ def shutdown_opentelemetry_metrics() -> None:
112
+ """Shutdown OpenTelemetry metrics."""
113
+ if not _HAS_OTEL_METRICS:
114
+ return
115
+
116
+ try:
117
+ meter_provider = otel_metrics.get_meter_provider()
118
+ if hasattr(meter_provider, "shutdown"):
119
+ meter_provider.shutdown()
120
+ log.debug("📊🛑 OpenTelemetry meter provider shutdown")
121
+ except Exception as e:
122
+ log.warning(f"⚠️ Error shutting down OpenTelemetry metrics: {e}")
@@ -0,0 +1,165 @@
1
+ """Simple metrics implementations that work with or without OpenTelemetry."""
2
+
3
+ from collections import defaultdict
4
+ from typing import Any
5
+
6
+ from provide.foundation.logger import get_logger
7
+
8
+ log = get_logger(__name__)
9
+
10
+
11
+ class SimpleCounter:
12
+ """Counter metric that increments monotonically."""
13
+
14
+ def __init__(self, name: str, otel_counter: Any | None = None):
15
+ self.name = name
16
+ self._otel_counter = otel_counter
17
+ self._value = 0
18
+ self._labels_values: dict[str, float] = defaultdict(float)
19
+
20
+ def inc(self, value: float = 1, **labels) -> None:
21
+ """Increment the counter.
22
+
23
+ Args:
24
+ value: Amount to increment by (default: 1)
25
+ **labels: Label key-value pairs
26
+ """
27
+ self._value += value
28
+
29
+ # Track per-label values for simple mode
30
+ if labels:
31
+ labels_key = ",".join(f"{k}={v}" for k, v in sorted(labels.items()))
32
+ self._labels_values[labels_key] += value
33
+
34
+ # Use OpenTelemetry counter if available
35
+ if self._otel_counter:
36
+ try:
37
+ self._otel_counter.add(value, attributes=labels)
38
+ except Exception as e:
39
+ log.debug(f"📊⚠️ Failed to record OpenTelemetry counter: {e}")
40
+
41
+ @property
42
+ def value(self) -> float:
43
+ """Get the current counter value."""
44
+ return self._value
45
+
46
+
47
+ class SimpleGauge:
48
+ """Gauge metric that can go up or down."""
49
+
50
+ def __init__(self, name: str, otel_gauge: Any | None = None):
51
+ self.name = name
52
+ self._otel_gauge = otel_gauge
53
+ self._value = 0
54
+ self._labels_values: dict[str, float] = defaultdict(float)
55
+
56
+ def set(self, value: float, **labels) -> None:
57
+ """Set the gauge value.
58
+
59
+ Args:
60
+ value: Value to set
61
+ **labels: Label key-value pairs
62
+ """
63
+ self._value = value
64
+
65
+ # Track per-label values for simple mode
66
+ if labels:
67
+ labels_key = ",".join(f"{k}={v}" for k, v in sorted(labels.items()))
68
+ self._labels_values[labels_key] = value
69
+
70
+ # Use OpenTelemetry gauge if available
71
+ if self._otel_gauge:
72
+ try:
73
+ self._otel_gauge.add(
74
+ value
75
+ - self._labels_values.get(
76
+ ",".join(f"{k}={v}" for k, v in sorted(labels.items()))
77
+ if labels
78
+ else "",
79
+ 0,
80
+ ),
81
+ attributes=labels,
82
+ )
83
+ except Exception as e:
84
+ log.debug(f"📊⚠️ Failed to record OpenTelemetry gauge: {e}")
85
+
86
+ def inc(self, value: float = 1, **labels) -> None:
87
+ """Increment the gauge value.
88
+
89
+ Args:
90
+ value: Amount to increment by
91
+ **labels: Label key-value pairs
92
+ """
93
+ self._value += value
94
+
95
+ if labels:
96
+ labels_key = ",".join(f"{k}={v}" for k, v in sorted(labels.items()))
97
+ self._labels_values[labels_key] += value
98
+
99
+ if self._otel_gauge:
100
+ try:
101
+ self._otel_gauge.add(value, attributes=labels)
102
+ except Exception as e:
103
+ log.debug(f"📊⚠️ Failed to increment OpenTelemetry gauge: {e}")
104
+
105
+ def dec(self, value: float = 1, **labels) -> None:
106
+ """Decrement the gauge value.
107
+
108
+ Args:
109
+ value: Amount to decrement by
110
+ **labels: Label key-value pairs
111
+ """
112
+ self.inc(-value, **labels)
113
+
114
+ @property
115
+ def value(self) -> float:
116
+ """Get the current gauge value."""
117
+ return self._value
118
+
119
+
120
+ class SimpleHistogram:
121
+ """Histogram metric for recording distributions of values."""
122
+
123
+ def __init__(self, name: str, otel_histogram: Any | None = None):
124
+ self.name = name
125
+ self._otel_histogram = otel_histogram
126
+ self._observations: list[float] = []
127
+ self._labels_observations: dict[str, list[float]] = defaultdict(list)
128
+
129
+ def observe(self, value: float, **labels) -> None:
130
+ """Record an observation.
131
+
132
+ Args:
133
+ value: Value to observe
134
+ **labels: Label key-value pairs
135
+ """
136
+ self._observations.append(value)
137
+
138
+ # Track per-label observations for simple mode
139
+ if labels:
140
+ labels_key = ",".join(f"{k}={v}" for k, v in sorted(labels.items()))
141
+ self._labels_observations[labels_key].append(value)
142
+
143
+ # Use OpenTelemetry histogram if available
144
+ if self._otel_histogram:
145
+ try:
146
+ self._otel_histogram.record(value, attributes=labels)
147
+ except Exception as e:
148
+ log.debug(f"📊⚠️ Failed to record OpenTelemetry histogram: {e}")
149
+
150
+ @property
151
+ def count(self) -> int:
152
+ """Get the number of observations."""
153
+ return len(self._observations)
154
+
155
+ @property
156
+ def sum(self) -> float:
157
+ """Get the sum of all observations."""
158
+ return sum(self._observations)
159
+
160
+ @property
161
+ def avg(self) -> float:
162
+ """Get the average of all observations."""
163
+ if not self._observations:
164
+ return 0.0
165
+ return self.sum / self.count
@@ -0,0 +1,53 @@
1
+ """
2
+ Observability module for Foundation.
3
+
4
+ Provides integration with observability platforms like OpenObserve.
5
+ Only available when OpenTelemetry dependencies are installed.
6
+ """
7
+
8
+ # OpenTelemetry feature detection
9
+ try:
10
+ from opentelemetry import trace as otel_trace
11
+
12
+ _HAS_OTEL = True
13
+ except ImportError:
14
+ otel_trace = None
15
+ _HAS_OTEL = False
16
+
17
+ # Only import OpenObserve if OpenTelemetry is available
18
+ if _HAS_OTEL:
19
+ try:
20
+ from provide.foundation.observability.openobserve import (
21
+ OpenObserveClient,
22
+ search_logs,
23
+ stream_logs,
24
+ )
25
+
26
+ # Commands will auto-register if click is available
27
+ try:
28
+ from provide.foundation.observability.openobserve.commands import (
29
+ openobserve_group,
30
+ )
31
+ except ImportError:
32
+ # Click not available, skip command registration
33
+ pass
34
+
35
+ __all__ = [
36
+ "OpenObserveClient",
37
+ "search_logs",
38
+ "stream_logs",
39
+ ]
40
+ except ImportError:
41
+ # OpenObserve module not fully available
42
+ __all__ = []
43
+ else:
44
+ __all__ = []
45
+
46
+
47
+ def is_openobserve_available() -> bool:
48
+ """Check if OpenObserve integration is available.
49
+
50
+ Returns:
51
+ True if OpenTelemetry and OpenObserve are available
52
+ """
53
+ return _HAS_OTEL and "OpenObserveClient" in globals()