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,134 @@
1
+ """
2
+ Data models for OpenObserve API requests and responses.
3
+ """
4
+
5
+ from dataclasses import dataclass, field
6
+ from datetime import datetime
7
+ from typing import Any
8
+
9
+
10
+ @dataclass
11
+ class SearchQuery:
12
+ """Search query parameters for OpenObserve."""
13
+
14
+ sql: str
15
+ start_time: int # Microseconds since epoch
16
+ end_time: int # Microseconds since epoch
17
+ from_offset: int = 0
18
+ size: int = 100
19
+
20
+ def to_dict(self) -> dict[str, Any]:
21
+ """Convert to API request format."""
22
+ return {
23
+ "query": {
24
+ "sql": self.sql,
25
+ "start_time": self.start_time,
26
+ "end_time": self.end_time,
27
+ "from": self.from_offset,
28
+ "size": self.size,
29
+ }
30
+ }
31
+
32
+
33
+ @dataclass
34
+ class SearchResponse:
35
+ """Response from OpenObserve search API."""
36
+
37
+ hits: list[dict[str, Any]]
38
+ total: int
39
+ took: int # Milliseconds
40
+ scan_size: int
41
+ trace_id: str | None = None
42
+ from_offset: int = 0
43
+ size: int = 0
44
+ is_partial: bool = False
45
+ function_error: list[str] = field(default_factory=list)
46
+
47
+ @classmethod
48
+ def from_dict(cls, data: dict[str, Any]) -> "SearchResponse":
49
+ """Create from API response."""
50
+ return cls(
51
+ hits=data.get("hits", []),
52
+ total=data.get("total", 0),
53
+ took=data.get("took", 0),
54
+ scan_size=data.get("scan_size", 0),
55
+ trace_id=data.get("trace_id"),
56
+ from_offset=data.get("from", 0),
57
+ size=data.get("size", 0),
58
+ is_partial=data.get("is_partial", False),
59
+ function_error=data.get("function_error", []),
60
+ )
61
+
62
+
63
+ @dataclass
64
+ class StreamInfo:
65
+ """Information about an OpenObserve stream."""
66
+
67
+ name: str
68
+ storage_type: str
69
+ stream_type: str
70
+ doc_count: int = 0
71
+ compressed_size: int = 0
72
+ original_size: int = 0
73
+
74
+ @classmethod
75
+ def from_dict(cls, data: dict[str, Any]) -> "StreamInfo":
76
+ """Create from API response."""
77
+ return cls(
78
+ name=data.get("name", ""),
79
+ storage_type=data.get("storage_type", ""),
80
+ stream_type=data.get("stream_type", ""),
81
+ doc_count=data.get("stats", {}).get("doc_count", 0),
82
+ compressed_size=data.get("stats", {}).get("compressed_size", 0),
83
+ original_size=data.get("stats", {}).get("original_size", 0),
84
+ )
85
+
86
+
87
+ def parse_relative_time(time_str: str, now: datetime | None = None) -> int:
88
+ """Parse relative time strings like '-1h', '-30m' to microseconds since epoch.
89
+
90
+ Args:
91
+ time_str: Time string (e.g., '-1h', '-30m', 'now')
92
+ now: Current time (for testing), defaults to datetime.now()
93
+
94
+ Returns:
95
+ Microseconds since epoch
96
+ """
97
+ from datetime import timedelta
98
+
99
+ if now is None:
100
+ now = datetime.now()
101
+
102
+ if time_str == "now":
103
+ return int(now.timestamp() * 1_000_000)
104
+
105
+ if time_str.startswith("-"):
106
+ # Parse relative time
107
+ value = time_str[1:]
108
+ if value.endswith("h"):
109
+ delta = timedelta(hours=int(value[:-1]))
110
+ elif value.endswith("m"):
111
+ delta = timedelta(minutes=int(value[:-1]))
112
+ elif value.endswith("s"):
113
+ delta = timedelta(seconds=int(value[:-1]))
114
+ elif value.endswith("d"):
115
+ delta = timedelta(days=int(value[:-1]))
116
+ else:
117
+ # Assume seconds if no unit
118
+ delta = timedelta(seconds=int(value))
119
+
120
+ target_time = now - delta
121
+ return int(target_time.timestamp() * 1_000_000)
122
+
123
+ # Try to parse as timestamp
124
+ try:
125
+ timestamp = int(time_str)
126
+ # If it's already in microseconds (large number), return as-is
127
+ if timestamp > 1_000_000_000_000:
128
+ return timestamp
129
+ # Otherwise assume seconds and convert
130
+ return timestamp * 1_000_000
131
+ except ValueError:
132
+ # Try to parse as ISO datetime
133
+ dt = datetime.fromisoformat(time_str)
134
+ return int(dt.timestamp() * 1_000_000)
@@ -0,0 +1,320 @@
1
+ """
2
+ OTLP integration for sending logs to OpenObserve.
3
+ """
4
+
5
+ from datetime import datetime
6
+ import json
7
+ from typing import Any
8
+
9
+ from provide.foundation.logger import get_logger
10
+ from provide.foundation.observability.openobserve.client import OpenObserveClient
11
+
12
+ log = get_logger(__name__)
13
+
14
+ # OpenTelemetry feature detection
15
+ try:
16
+ from opentelemetry import trace
17
+ from opentelemetry.exporter.otlp.proto.http._log_exporter import OTLPLogExporter
18
+ from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler
19
+ from opentelemetry.sdk._logs.export import BatchLogRecordProcessor
20
+ from opentelemetry.sdk.resources import Resource
21
+ from opentelemetry.semconv.resource import ResourceAttributes
22
+
23
+ _HAS_OTEL_LOGS = True
24
+ except ImportError:
25
+ _HAS_OTEL_LOGS = False
26
+
27
+
28
+ def send_log_otlp(
29
+ message: str,
30
+ level: str = "INFO",
31
+ service: str | None = None,
32
+ attributes: dict[str, Any] | None = None,
33
+ ) -> bool:
34
+ """Send a log via OTLP if available.
35
+
36
+ Args:
37
+ message: Log message
38
+ level: Log level
39
+ service: Service name (uses config if not provided)
40
+ attributes: Additional attributes
41
+
42
+ Returns:
43
+ True if sent successfully via OTLP, False otherwise
44
+ """
45
+ if not _HAS_OTEL_LOGS:
46
+ return False
47
+
48
+ try:
49
+ from provide.foundation.logger.config import TelemetryConfig
50
+
51
+ config = TelemetryConfig.from_env()
52
+
53
+ if not config.otlp_endpoint:
54
+ return False
55
+
56
+ # Create resource with service info
57
+ resource_attrs = {
58
+ ResourceAttributes.SERVICE_NAME: service
59
+ or config.service_name
60
+ or "foundation",
61
+ }
62
+ if config.service_version:
63
+ resource_attrs[ResourceAttributes.SERVICE_VERSION] = config.service_version
64
+
65
+ resource = Resource.create(resource_attrs)
66
+
67
+ # Configure OTLP exporter
68
+ headers = config.get_otlp_headers_dict()
69
+ if config.openobserve_org:
70
+ # Add organization header for OpenObserve
71
+ headers["organization"] = config.openobserve_org
72
+ headers["stream-name"] = config.openobserve_stream
73
+
74
+ # Determine endpoint for logs
75
+ if config.otlp_traces_endpoint:
76
+ # Replace /traces with /logs
77
+ logs_endpoint = config.otlp_traces_endpoint.replace(
78
+ "/v1/traces", "/v1/logs"
79
+ )
80
+ else:
81
+ logs_endpoint = f"{config.otlp_endpoint}/v1/logs"
82
+
83
+ exporter = OTLPLogExporter(
84
+ endpoint=logs_endpoint,
85
+ headers=headers,
86
+ )
87
+
88
+ # Create logger provider
89
+ logger_provider = LoggerProvider(resource=resource)
90
+ logger_provider.add_log_record_processor(BatchLogRecordProcessor(exporter))
91
+
92
+ # Get logger and emit log
93
+ otel_logger = logger_provider.get_logger(__name__)
94
+
95
+ # Add trace context if available
96
+ log_attrs = attributes or {}
97
+ current_span = trace.get_current_span()
98
+ if current_span and current_span.is_recording():
99
+ span_context = current_span.get_span_context()
100
+ log_attrs["trace_id"] = f"{span_context.trace_id:032x}"
101
+ log_attrs["span_id"] = f"{span_context.span_id:016x}"
102
+
103
+ # Map level to severity number
104
+ severity_map = {
105
+ "TRACE": 1,
106
+ "DEBUG": 5,
107
+ "INFO": 9,
108
+ "WARN": 13,
109
+ "WARNING": 13,
110
+ "ERROR": 17,
111
+ "FATAL": 21,
112
+ "CRITICAL": 21,
113
+ }
114
+ severity = severity_map.get(level.upper(), 9)
115
+
116
+ # Emit log record
117
+ otel_logger.emit(
118
+ severity_number=severity,
119
+ severity_text=level.upper(),
120
+ body=message,
121
+ attributes=log_attrs,
122
+ )
123
+
124
+ # Force flush to ensure delivery
125
+ logger_provider.force_flush()
126
+
127
+ log.debug(f"Sent log via OTLP: {message[:50]}...")
128
+ return True
129
+
130
+ except Exception as e:
131
+ log.debug(f"Failed to send via OTLP: {e}")
132
+ return False
133
+
134
+
135
+ def send_log_bulk(
136
+ message: str,
137
+ level: str = "INFO",
138
+ service: str | None = None,
139
+ attributes: dict[str, Any] | None = None,
140
+ client: OpenObserveClient | None = None,
141
+ ) -> bool:
142
+ """Send a log via OpenObserve bulk API.
143
+
144
+ Args:
145
+ message: Log message
146
+ level: Log level
147
+ service: Service name
148
+ attributes: Additional attributes
149
+ client: OpenObserve client (creates new if not provided)
150
+
151
+ Returns:
152
+ True if sent successfully
153
+ """
154
+ try:
155
+ if client is None:
156
+ client = OpenObserveClient.from_config()
157
+
158
+ from provide.foundation.logger.config import TelemetryConfig
159
+
160
+ config = TelemetryConfig.from_env()
161
+
162
+ # Build log entry
163
+ log_entry = {
164
+ "_timestamp": int(datetime.now().timestamp() * 1_000_000),
165
+ "level": level.upper(),
166
+ "message": message,
167
+ "service": service or config.service_name or "foundation",
168
+ }
169
+
170
+ # Add attributes
171
+ if attributes:
172
+ log_entry.update(attributes)
173
+
174
+ # Add trace context if available
175
+ try:
176
+ from opentelemetry import trace
177
+
178
+ current_span = trace.get_current_span()
179
+ if current_span and current_span.is_recording():
180
+ span_context = current_span.get_span_context()
181
+ log_entry["trace_id"] = f"{span_context.trace_id:032x}"
182
+ log_entry["span_id"] = f"{span_context.span_id:016x}"
183
+ except ImportError:
184
+ pass
185
+
186
+ # Try Foundation's tracer context
187
+ try:
188
+ from provide.foundation.tracer.context import (
189
+ get_current_span,
190
+ get_current_trace_id,
191
+ )
192
+
193
+ span = get_current_span()
194
+ if span:
195
+ log_entry["trace_id"] = span.trace_id
196
+ log_entry["span_id"] = span.span_id
197
+ elif trace_id := get_current_trace_id():
198
+ log_entry["trace_id"] = trace_id
199
+ except ImportError:
200
+ pass
201
+
202
+ # Format as bulk request
203
+ stream = config.openobserve_stream
204
+ bulk_data = (
205
+ json.dumps({"index": {"_index": stream}})
206
+ + "\n"
207
+ + json.dumps(log_entry)
208
+ + "\n"
209
+ )
210
+
211
+ # Send via bulk API
212
+ import requests
213
+
214
+ # Build URL - check if client.url already includes /api/{org}
215
+ if f"/api/{client.organization}" in client.url:
216
+ url = f"{client.url}/_bulk"
217
+ else:
218
+ url = f"{client.url}/api/{client.organization}/_bulk"
219
+
220
+ response = requests.post(
221
+ url,
222
+ headers=client.session.headers,
223
+ data=bulk_data,
224
+ timeout=client.timeout,
225
+ )
226
+
227
+ if response.status_code == 200:
228
+ log.debug(f"Sent log via bulk API: {message[:50]}...")
229
+ return True
230
+ else:
231
+ log.debug(f"Failed to send via bulk API: {response.status_code}")
232
+ return False
233
+
234
+ except Exception as e:
235
+ log.debug(f"Failed to send via bulk API: {e}")
236
+ return False
237
+
238
+
239
+ def send_log(
240
+ message: str,
241
+ level: str = "INFO",
242
+ service: str | None = None,
243
+ attributes: dict[str, Any] | None = None,
244
+ prefer_otlp: bool = True,
245
+ client: OpenObserveClient | None = None,
246
+ ) -> bool:
247
+ """Send a log using OTLP if available, otherwise bulk API.
248
+
249
+ Args:
250
+ message: Log message
251
+ level: Log level
252
+ service: Service name
253
+ attributes: Additional attributes
254
+ prefer_otlp: Try OTLP first if True
255
+ client: OpenObserve client for bulk API
256
+
257
+ Returns:
258
+ True if sent successfully
259
+ """
260
+ # Try OTLP first if preferred and available
261
+ if prefer_otlp and _HAS_OTEL_LOGS:
262
+ if send_log_otlp(message, level, service, attributes):
263
+ return True
264
+
265
+ # Fall back to bulk API
266
+ return send_log_bulk(message, level, service, attributes, client)
267
+
268
+
269
+ def create_otlp_logger_provider() -> Any | None:
270
+ """Create an OTLP logger provider for continuous logging.
271
+
272
+ Returns:
273
+ LoggerProvider if OTLP is available and configured, None otherwise
274
+ """
275
+ if not _HAS_OTEL_LOGS:
276
+ return None
277
+
278
+ try:
279
+ from provide.foundation.logger.config import TelemetryConfig
280
+
281
+ config = TelemetryConfig.from_env()
282
+
283
+ if not config.otlp_endpoint:
284
+ return None
285
+
286
+ # Create resource
287
+ resource_attrs = {
288
+ ResourceAttributes.SERVICE_NAME: config.service_name or "foundation",
289
+ }
290
+ if config.service_version:
291
+ resource_attrs[ResourceAttributes.SERVICE_VERSION] = config.service_version
292
+
293
+ resource = Resource.create(resource_attrs)
294
+
295
+ # Configure exporter
296
+ headers = config.get_otlp_headers_dict()
297
+ if config.openobserve_org:
298
+ headers["organization"] = config.openobserve_org
299
+ headers["stream-name"] = config.openobserve_stream
300
+
301
+ logs_endpoint = f"{config.otlp_endpoint}/v1/logs"
302
+ if config.otlp_traces_endpoint:
303
+ logs_endpoint = config.otlp_traces_endpoint.replace(
304
+ "/v1/traces", "/v1/logs"
305
+ )
306
+
307
+ exporter = OTLPLogExporter(
308
+ endpoint=logs_endpoint,
309
+ headers=headers,
310
+ )
311
+
312
+ # Create provider
313
+ logger_provider = LoggerProvider(resource=resource)
314
+ logger_provider.add_log_record_processor(BatchLogRecordProcessor(exporter))
315
+
316
+ return logger_provider
317
+
318
+ except Exception as e:
319
+ log.debug(f"Failed to create OTLP logger provider: {e}")
320
+ return None
@@ -0,0 +1,222 @@
1
+ """
2
+ Search operations for OpenObserve.
3
+ """
4
+
5
+
6
+ from provide.foundation.logger import get_logger
7
+ from provide.foundation.observability.openobserve.client import OpenObserveClient
8
+ from provide.foundation.observability.openobserve.models import SearchResponse
9
+
10
+ log = get_logger(__name__)
11
+
12
+
13
+ def search_logs(
14
+ sql: str,
15
+ start_time: str | int | None = None,
16
+ end_time: str | int | None = None,
17
+ size: int = 100,
18
+ client: OpenObserveClient | None = None,
19
+ ) -> SearchResponse:
20
+ """Search logs in OpenObserve.
21
+
22
+ Args:
23
+ sql: SQL query to execute
24
+ start_time: Start time (relative like "-1h" or microseconds)
25
+ end_time: End time (relative like "now" or microseconds)
26
+ size: Number of results to return
27
+ client: OpenObserve client (creates new if not provided)
28
+
29
+ Returns:
30
+ SearchResponse with results
31
+ """
32
+ if client is None:
33
+ client = OpenObserveClient.from_config()
34
+
35
+ return client.search(
36
+ sql=sql,
37
+ start_time=start_time,
38
+ end_time=end_time,
39
+ size=size,
40
+ )
41
+
42
+
43
+ def search_by_trace_id(
44
+ trace_id: str,
45
+ stream: str = "default",
46
+ client: OpenObserveClient | None = None,
47
+ ) -> SearchResponse:
48
+ """Search for logs by trace ID.
49
+
50
+ Args:
51
+ trace_id: Trace ID to search for
52
+ stream: Stream name to search in
53
+ client: OpenObserve client (creates new if not provided)
54
+
55
+ Returns:
56
+ SearchResponse with matching logs
57
+ """
58
+ sql = (
59
+ f"SELECT * FROM {stream} WHERE trace_id = '{trace_id}' ORDER BY _timestamp ASC"
60
+ )
61
+ return search_logs(sql=sql, start_time="-24h", client=client)
62
+
63
+
64
+ def search_by_level(
65
+ level: str,
66
+ stream: str = "default",
67
+ start_time: str | int | None = None,
68
+ end_time: str | int | None = None,
69
+ size: int = 100,
70
+ client: OpenObserveClient | None = None,
71
+ ) -> SearchResponse:
72
+ """Search for logs by level.
73
+
74
+ Args:
75
+ level: Log level to filter (ERROR, WARN, INFO, DEBUG, etc.)
76
+ stream: Stream name to search in
77
+ start_time: Start time
78
+ end_time: End time
79
+ size: Number of results
80
+ client: OpenObserve client
81
+
82
+ Returns:
83
+ SearchResponse with matching logs
84
+ """
85
+ sql = f"SELECT * FROM {stream} WHERE level = '{level}' ORDER BY _timestamp DESC"
86
+ return search_logs(
87
+ sql=sql,
88
+ start_time=start_time,
89
+ end_time=end_time,
90
+ size=size,
91
+ client=client,
92
+ )
93
+
94
+
95
+ def search_errors(
96
+ stream: str = "default",
97
+ start_time: str | int | None = None,
98
+ size: int = 100,
99
+ client: OpenObserveClient | None = None,
100
+ ) -> SearchResponse:
101
+ """Search for error logs.
102
+
103
+ Args:
104
+ stream: Stream name to search in
105
+ start_time: Start time
106
+ size: Number of results
107
+ client: OpenObserve client
108
+
109
+ Returns:
110
+ SearchResponse with error logs
111
+ """
112
+ return search_by_level(
113
+ level="ERROR",
114
+ stream=stream,
115
+ start_time=start_time,
116
+ size=size,
117
+ client=client,
118
+ )
119
+
120
+
121
+ def search_by_service(
122
+ service: str,
123
+ stream: str = "default",
124
+ start_time: str | int | None = None,
125
+ end_time: str | int | None = None,
126
+ size: int = 100,
127
+ client: OpenObserveClient | None = None,
128
+ ) -> SearchResponse:
129
+ """Search for logs by service name.
130
+
131
+ Args:
132
+ service: Service name to filter
133
+ stream: Stream name to search in
134
+ start_time: Start time
135
+ end_time: End time
136
+ size: Number of results
137
+ client: OpenObserve client
138
+
139
+ Returns:
140
+ SearchResponse with matching logs
141
+ """
142
+ sql = f"SELECT * FROM {stream} WHERE service = '{service}' ORDER BY _timestamp DESC"
143
+ return search_logs(
144
+ sql=sql,
145
+ start_time=start_time,
146
+ end_time=end_time,
147
+ size=size,
148
+ client=client,
149
+ )
150
+
151
+
152
+ def aggregate_by_level(
153
+ stream: str = "default",
154
+ start_time: str | int | None = None,
155
+ end_time: str | int | None = None,
156
+ client: OpenObserveClient | None = None,
157
+ ) -> dict[str, int]:
158
+ """Get count of logs by level.
159
+
160
+ Args:
161
+ stream: Stream name to search in
162
+ start_time: Start time
163
+ end_time: End time
164
+ client: OpenObserve client
165
+
166
+ Returns:
167
+ Dictionary mapping level to count
168
+ """
169
+ sql = f"SELECT level, COUNT(*) as count FROM {stream} GROUP BY level"
170
+ response = search_logs(
171
+ sql=sql,
172
+ start_time=start_time,
173
+ end_time=end_time,
174
+ size=1000,
175
+ client=client,
176
+ )
177
+
178
+ result = {}
179
+ for hit in response.hits:
180
+ level = hit.get("level", "UNKNOWN")
181
+ count = hit.get("count", 0)
182
+ result[level] = count
183
+
184
+ return result
185
+
186
+
187
+ def get_current_trace_logs(
188
+ stream: str = "default",
189
+ client: OpenObserveClient | None = None,
190
+ ) -> SearchResponse | None:
191
+ """Get logs for the current active trace.
192
+
193
+ Args:
194
+ stream: Stream name to search in
195
+ client: OpenObserve client
196
+
197
+ Returns:
198
+ SearchResponse with logs for current trace, or None if no active trace
199
+ """
200
+ # Try to get current trace ID from OpenTelemetry
201
+ try:
202
+ from opentelemetry import trace
203
+
204
+ current_span = trace.get_current_span()
205
+ if current_span and current_span.is_recording():
206
+ span_context = current_span.get_span_context()
207
+ trace_id = f"{span_context.trace_id:032x}"
208
+ return search_by_trace_id(trace_id, stream=stream, client=client)
209
+ except ImportError:
210
+ pass
211
+
212
+ # Try to get from Foundation tracer
213
+ try:
214
+ from provide.foundation.tracer.context import get_current_trace_id
215
+
216
+ trace_id = get_current_trace_id()
217
+ if trace_id:
218
+ return search_by_trace_id(trace_id, stream=stream, client=client)
219
+ except ImportError:
220
+ pass
221
+
222
+ return None