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,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()
|