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,79 @@
1
+ """
2
+ OpenObserve integration for Foundation.
3
+
4
+ Provides log querying and streaming capabilities when OpenTelemetry is available.
5
+ """
6
+
7
+ from provide.foundation.observability.openobserve.client import OpenObserveClient
8
+ from provide.foundation.observability.openobserve.exceptions import (
9
+ OpenObserveAuthenticationError,
10
+ OpenObserveConfigError,
11
+ OpenObserveConnectionError,
12
+ OpenObserveError,
13
+ OpenObserveQueryError,
14
+ OpenObserveStreamingError,
15
+ )
16
+ from provide.foundation.observability.openobserve.formatters import (
17
+ format_csv,
18
+ format_json,
19
+ format_log_line,
20
+ format_output,
21
+ format_summary,
22
+ format_table,
23
+ )
24
+ from provide.foundation.observability.openobserve.models import (
25
+ SearchQuery,
26
+ SearchResponse,
27
+ StreamInfo,
28
+ parse_relative_time,
29
+ )
30
+ from provide.foundation.observability.openobserve.search import (
31
+ aggregate_by_level,
32
+ get_current_trace_logs,
33
+ search_by_level,
34
+ search_by_service,
35
+ search_by_trace_id,
36
+ search_errors,
37
+ search_logs,
38
+ )
39
+ from provide.foundation.observability.openobserve.streaming import (
40
+ stream_logs,
41
+ stream_search_http2,
42
+ tail_logs,
43
+ )
44
+
45
+ __all__ = [
46
+ # Client
47
+ "OpenObserveClient",
48
+ # Search functions
49
+ "search_logs",
50
+ "search_by_trace_id",
51
+ "search_by_level",
52
+ "search_errors",
53
+ "search_by_service",
54
+ "aggregate_by_level",
55
+ "get_current_trace_logs",
56
+ # Streaming functions
57
+ "stream_logs",
58
+ "stream_search_http2",
59
+ "tail_logs",
60
+ # Models
61
+ "SearchQuery",
62
+ "SearchResponse",
63
+ "StreamInfo",
64
+ "parse_relative_time",
65
+ # Formatters
66
+ "format_json",
67
+ "format_log_line",
68
+ "format_table",
69
+ "format_csv",
70
+ "format_summary",
71
+ "format_output",
72
+ # Exceptions
73
+ "OpenObserveError",
74
+ "OpenObserveConnectionError",
75
+ "OpenObserveAuthenticationError",
76
+ "OpenObserveQueryError",
77
+ "OpenObserveStreamingError",
78
+ "OpenObserveConfigError",
79
+ ]
@@ -0,0 +1,72 @@
1
+ """
2
+ Authentication handling for OpenObserve.
3
+ """
4
+
5
+ import base64
6
+
7
+ from provide.foundation.observability.openobserve.exceptions import (
8
+ OpenObserveAuthenticationError,
9
+ )
10
+
11
+
12
+ def encode_basic_auth(username: str, password: str) -> str:
13
+ """Encode username and password for Basic authentication.
14
+
15
+ Args:
16
+ username: OpenObserve username
17
+ password: OpenObserve password
18
+
19
+ Returns:
20
+ Base64 encoded auth string
21
+ """
22
+ credentials = f"{username}:{password}"
23
+ encoded = base64.b64encode(credentials.encode()).decode()
24
+ return encoded
25
+
26
+
27
+ def get_auth_headers(username: str | None, password: str | None) -> dict[str, str]:
28
+ """Get authentication headers for OpenObserve API.
29
+
30
+ Args:
31
+ username: OpenObserve username
32
+ password: OpenObserve password
33
+
34
+ Returns:
35
+ Dictionary with Authorization header
36
+
37
+ Raises:
38
+ OpenObserveAuthenticationError: If credentials are missing
39
+ """
40
+ if not username or not password:
41
+ raise OpenObserveAuthenticationError(
42
+ "OpenObserve credentials not configured. "
43
+ "Set OPENOBSERVE_USER and OPENOBSERVE_PASSWORD environment variables."
44
+ )
45
+
46
+ auth_token = encode_basic_auth(username, password)
47
+ return {
48
+ "Authorization": f"Basic {auth_token}",
49
+ "Content-Type": "application/json",
50
+ }
51
+
52
+
53
+ def validate_credentials(username: str | None, password: str | None) -> tuple[str, str]:
54
+ """Validate and return OpenObserve credentials.
55
+
56
+ Args:
57
+ username: OpenObserve username
58
+ password: OpenObserve password
59
+
60
+ Returns:
61
+ Tuple of (username, password)
62
+
63
+ Raises:
64
+ OpenObserveAuthenticationError: If credentials are invalid
65
+ """
66
+ if not username:
67
+ raise OpenObserveAuthenticationError("OpenObserve username is required")
68
+
69
+ if not password:
70
+ raise OpenObserveAuthenticationError("OpenObserve password is required")
71
+
72
+ return username, password
@@ -0,0 +1,307 @@
1
+ """
2
+ OpenObserve API client.
3
+ """
4
+
5
+ from datetime import datetime
6
+ import json
7
+ from typing import Any
8
+ from urllib.parse import urljoin
9
+
10
+ import requests
11
+ from requests.adapters import HTTPAdapter
12
+ from urllib3.util.retry import Retry
13
+
14
+ from provide.foundation.logger import get_logger
15
+ from provide.foundation.observability.openobserve.auth import (
16
+ get_auth_headers,
17
+ validate_credentials,
18
+ )
19
+ from provide.foundation.observability.openobserve.exceptions import (
20
+ OpenObserveConfigError,
21
+ OpenObserveConnectionError,
22
+ OpenObserveQueryError,
23
+ )
24
+ from provide.foundation.observability.openobserve.models import (
25
+ SearchQuery,
26
+ SearchResponse,
27
+ StreamInfo,
28
+ parse_relative_time,
29
+ )
30
+
31
+ log = get_logger(__name__)
32
+
33
+
34
+ class OpenObserveClient:
35
+ """Client for interacting with OpenObserve API."""
36
+
37
+ def __init__(
38
+ self,
39
+ url: str,
40
+ username: str,
41
+ password: str,
42
+ organization: str = "default",
43
+ timeout: int = 30,
44
+ max_retries: int = 3,
45
+ ):
46
+ """Initialize OpenObserve client.
47
+
48
+ Args:
49
+ url: Base URL for OpenObserve API
50
+ username: Username for authentication
51
+ password: Password for authentication
52
+ organization: Organization name (default: "default")
53
+ timeout: Request timeout in seconds
54
+ max_retries: Maximum number of retry attempts
55
+ """
56
+ self.url = url.rstrip("/")
57
+ self.username, self.password = validate_credentials(username, password)
58
+ self.organization = organization
59
+ self.timeout = timeout
60
+
61
+ # Setup session with retry logic
62
+ self.session = requests.Session()
63
+ retry = Retry(
64
+ total=max_retries,
65
+ backoff_factor=0.3,
66
+ status_forcelist=[500, 502, 503, 504],
67
+ )
68
+ adapter = HTTPAdapter(max_retries=retry)
69
+ self.session.mount("http://", adapter)
70
+ self.session.mount("https://", adapter)
71
+
72
+ # Set default headers
73
+ self.session.headers.update(get_auth_headers(self.username, self.password))
74
+
75
+ @classmethod
76
+ def from_config(cls) -> "OpenObserveClient":
77
+ """Create client from TelemetryConfig.
78
+
79
+ Returns:
80
+ Configured OpenObserveClient instance
81
+
82
+ Raises:
83
+ OpenObserveConfigError: If configuration is missing
84
+ """
85
+ from provide.foundation.logger.config import TelemetryConfig
86
+
87
+ config = TelemetryConfig.from_env()
88
+
89
+ if not config.openobserve_url:
90
+ raise OpenObserveConfigError(
91
+ "OpenObserve URL not configured. Set OPENOBSERVE_URL environment variable."
92
+ )
93
+
94
+ if not config.openobserve_user or not config.openobserve_password:
95
+ raise OpenObserveConfigError(
96
+ "OpenObserve credentials not configured. "
97
+ "Set OPENOBSERVE_USER and OPENOBSERVE_PASSWORD environment variables."
98
+ )
99
+
100
+ return cls(
101
+ url=config.openobserve_url,
102
+ username=config.openobserve_user,
103
+ password=config.openobserve_password,
104
+ organization=config.openobserve_org,
105
+ )
106
+
107
+ def _make_request(
108
+ self,
109
+ method: str,
110
+ endpoint: str,
111
+ params: dict[str, Any] | None = None,
112
+ json_data: dict[str, Any] | None = None,
113
+ ) -> dict[str, Any]:
114
+ """Make HTTP request to OpenObserve API.
115
+
116
+ Args:
117
+ method: HTTP method
118
+ endpoint: API endpoint path
119
+ params: Query parameters
120
+ json_data: JSON body data
121
+
122
+ Returns:
123
+ Response data as dictionary
124
+
125
+ Raises:
126
+ OpenObserveConnectionError: On connection errors
127
+ OpenObserveQueryError: On API errors
128
+ """
129
+ url = urljoin(self.url, f"/api/{self.organization}/{endpoint}")
130
+
131
+ try:
132
+ response = self.session.request(
133
+ method=method,
134
+ url=url,
135
+ params=params,
136
+ json=json_data,
137
+ timeout=self.timeout,
138
+ )
139
+
140
+ if response.status_code == 401:
141
+ raise OpenObserveConnectionError(
142
+ "Authentication failed. Check credentials."
143
+ )
144
+
145
+ response.raise_for_status()
146
+
147
+ # Handle empty responses
148
+ if not response.content:
149
+ return {}
150
+
151
+ return response.json()
152
+
153
+ except requests.exceptions.ConnectionError as e:
154
+ raise OpenObserveConnectionError(f"Failed to connect to OpenObserve: {e}")
155
+ except requests.exceptions.Timeout as e:
156
+ raise OpenObserveConnectionError(f"Request timed out: {e}")
157
+ except requests.exceptions.HTTPError as e:
158
+ # Try to extract error message from response
159
+ error_msg = str(e)
160
+ try:
161
+ error_data = e.response.json()
162
+ if "message" in error_data:
163
+ error_msg = error_data["message"]
164
+ except (json.JSONDecodeError, AttributeError):
165
+ pass
166
+ raise OpenObserveQueryError(f"API error: {error_msg}")
167
+ except Exception as e:
168
+ raise OpenObserveQueryError(f"Unexpected error: {e}")
169
+
170
+ def search(
171
+ self,
172
+ sql: str,
173
+ start_time: str | int | None = None,
174
+ end_time: str | int | None = None,
175
+ size: int = 100,
176
+ from_offset: int = 0,
177
+ ) -> SearchResponse:
178
+ """Execute a search query.
179
+
180
+ Args:
181
+ sql: SQL query to execute
182
+ start_time: Start time (relative like "-1h" or microseconds)
183
+ end_time: End time (relative like "now" or microseconds)
184
+ size: Number of results to return
185
+ from_offset: Offset for pagination
186
+
187
+ Returns:
188
+ SearchResponse with results
189
+ """
190
+ # Parse time parameters
191
+ now = datetime.now()
192
+
193
+ if start_time is None:
194
+ start_time = "-1h"
195
+ if end_time is None:
196
+ end_time = "now"
197
+
198
+ start_ts = (
199
+ parse_relative_time(str(start_time), now)
200
+ if isinstance(start_time, str)
201
+ else start_time
202
+ )
203
+ end_ts = (
204
+ parse_relative_time(str(end_time), now)
205
+ if isinstance(end_time, str)
206
+ else end_time
207
+ )
208
+
209
+ # Create query
210
+ query = SearchQuery(
211
+ sql=sql,
212
+ start_time=start_ts,
213
+ end_time=end_ts,
214
+ size=size,
215
+ from_offset=from_offset,
216
+ )
217
+
218
+ log.debug(f"Executing search query: {sql}")
219
+
220
+ # Make request
221
+ response = self._make_request(
222
+ method="POST",
223
+ endpoint="_search",
224
+ params={"is_ui_histogram": "false", "is_multi_stream_search": "false"},
225
+ json_data=query.to_dict(),
226
+ )
227
+
228
+ # Handle errors in response
229
+ if "error" in response:
230
+ raise OpenObserveQueryError(f"Query error: {response['error']}")
231
+
232
+ result = SearchResponse.from_dict(response)
233
+
234
+ # Log any function errors
235
+ if result.function_error:
236
+ for error in result.function_error:
237
+ log.warning(f"Query warning: {error}")
238
+
239
+ log.info(f"Search completed: {len(result.hits)} hits, took {result.took}ms")
240
+
241
+ return result
242
+
243
+ def list_streams(self) -> list[StreamInfo]:
244
+ """List available streams.
245
+
246
+ Returns:
247
+ List of StreamInfo objects
248
+ """
249
+ response = self._make_request(
250
+ method="GET",
251
+ endpoint="streams",
252
+ )
253
+
254
+ streams = []
255
+ if isinstance(response, dict):
256
+ # Response is a dict of stream types to stream lists
257
+ for stream_type, stream_list in response.items():
258
+ if isinstance(stream_list, list):
259
+ for stream_data in stream_list:
260
+ if isinstance(stream_data, dict):
261
+ stream_info = StreamInfo.from_dict(stream_data)
262
+ streams.append(stream_info)
263
+
264
+ return streams
265
+
266
+ def get_search_history(
267
+ self,
268
+ stream_name: str | None = None,
269
+ size: int = 100,
270
+ ) -> SearchResponse:
271
+ """Get search history.
272
+
273
+ Args:
274
+ stream_name: Filter by stream name
275
+ size: Number of history entries to return
276
+
277
+ Returns:
278
+ SearchResponse with history entries
279
+ """
280
+ request_data = {
281
+ "size": size,
282
+ }
283
+
284
+ if stream_name:
285
+ request_data["stream_name"] = stream_name
286
+
287
+ response = self._make_request(
288
+ method="POST",
289
+ endpoint="_search_history",
290
+ json_data=request_data,
291
+ )
292
+
293
+ return SearchResponse.from_dict(response)
294
+
295
+ def test_connection(self) -> bool:
296
+ """Test connection to OpenObserve.
297
+
298
+ Returns:
299
+ True if connection successful
300
+ """
301
+ try:
302
+ # Try to list streams as a simple test
303
+ self.list_streams()
304
+ return True
305
+ except Exception as e:
306
+ log.error(f"Connection test failed: {e}")
307
+ return False