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,357 @@
|
|
1
|
+
"""
|
2
|
+
CLI commands for OpenObserve integration.
|
3
|
+
|
4
|
+
These commands are auto-registered by Foundation's command discovery system.
|
5
|
+
"""
|
6
|
+
|
7
|
+
try:
|
8
|
+
import click
|
9
|
+
|
10
|
+
_HAS_CLICK = True
|
11
|
+
except ImportError:
|
12
|
+
click = None
|
13
|
+
_HAS_CLICK = False
|
14
|
+
|
15
|
+
from provide.foundation.logger import get_logger
|
16
|
+
|
17
|
+
log = get_logger(__name__)
|
18
|
+
|
19
|
+
|
20
|
+
if _HAS_CLICK:
|
21
|
+
from provide.foundation.observability.openobserve import (
|
22
|
+
OpenObserveClient,
|
23
|
+
format_output,
|
24
|
+
search_logs,
|
25
|
+
tail_logs,
|
26
|
+
)
|
27
|
+
|
28
|
+
@click.group("openobserve", help="Query and manage OpenObserve logs")
|
29
|
+
@click.pass_context
|
30
|
+
def openobserve_group(ctx):
|
31
|
+
"""OpenObserve log querying and streaming commands."""
|
32
|
+
# Initialize client and store in context
|
33
|
+
try:
|
34
|
+
client = OpenObserveClient.from_config()
|
35
|
+
ctx.obj = client
|
36
|
+
except Exception as e:
|
37
|
+
log.warning(f"Failed to initialize OpenObserve client: {e}")
|
38
|
+
ctx.obj = None
|
39
|
+
|
40
|
+
@openobserve_group.command("query")
|
41
|
+
@click.option(
|
42
|
+
"--sql",
|
43
|
+
required=True,
|
44
|
+
help="SQL query to execute",
|
45
|
+
)
|
46
|
+
@click.option(
|
47
|
+
"--start",
|
48
|
+
"-s",
|
49
|
+
default="-1h",
|
50
|
+
help="Start time (e.g., -1h, -30m, 2024-01-01)",
|
51
|
+
)
|
52
|
+
@click.option(
|
53
|
+
"--end",
|
54
|
+
"-e",
|
55
|
+
default="now",
|
56
|
+
help="End time (e.g., now, -5m, 2024-01-02)",
|
57
|
+
)
|
58
|
+
@click.option(
|
59
|
+
"--size",
|
60
|
+
"-n",
|
61
|
+
default=100,
|
62
|
+
type=int,
|
63
|
+
help="Number of results to return",
|
64
|
+
)
|
65
|
+
@click.option(
|
66
|
+
"--format",
|
67
|
+
"-f",
|
68
|
+
type=click.Choice(["json", "log", "table", "csv", "summary"]),
|
69
|
+
default="log",
|
70
|
+
help="Output format",
|
71
|
+
)
|
72
|
+
@click.option(
|
73
|
+
"--pretty",
|
74
|
+
is_flag=True,
|
75
|
+
help="Pretty print JSON output",
|
76
|
+
)
|
77
|
+
@click.pass_obj
|
78
|
+
def query_command(client, sql, start, end, size, format, pretty):
|
79
|
+
"""Execute SQL query against OpenObserve logs."""
|
80
|
+
if client is None:
|
81
|
+
click.echo(
|
82
|
+
"OpenObserve not configured. Set OPENOBSERVE_URL, OPENOBSERVE_USER, and OPENOBSERVE_PASSWORD.",
|
83
|
+
err=True,
|
84
|
+
)
|
85
|
+
return 1
|
86
|
+
|
87
|
+
try:
|
88
|
+
response = search_logs(
|
89
|
+
sql=sql,
|
90
|
+
start_time=start,
|
91
|
+
end_time=end,
|
92
|
+
size=size,
|
93
|
+
client=client,
|
94
|
+
)
|
95
|
+
|
96
|
+
output = format_output(response, format_type=format, pretty=pretty)
|
97
|
+
click.echo(output)
|
98
|
+
|
99
|
+
except Exception as e:
|
100
|
+
click.echo(f"Query failed: {e}", err=True)
|
101
|
+
return 1
|
102
|
+
|
103
|
+
@openobserve_group.command("tail")
|
104
|
+
@click.option(
|
105
|
+
"--stream",
|
106
|
+
"-s",
|
107
|
+
default="default",
|
108
|
+
help="Stream name to tail",
|
109
|
+
)
|
110
|
+
@click.option(
|
111
|
+
"--filter",
|
112
|
+
"-f",
|
113
|
+
"filter_sql",
|
114
|
+
help="SQL WHERE clause for filtering (e.g., \"level='ERROR'\")",
|
115
|
+
)
|
116
|
+
@click.option(
|
117
|
+
"--lines",
|
118
|
+
"-n",
|
119
|
+
default=10,
|
120
|
+
type=int,
|
121
|
+
help="Number of initial lines to show",
|
122
|
+
)
|
123
|
+
@click.option(
|
124
|
+
"--follow",
|
125
|
+
"-F",
|
126
|
+
is_flag=True,
|
127
|
+
default=True,
|
128
|
+
help="Follow mode (like tail -f)",
|
129
|
+
)
|
130
|
+
@click.option(
|
131
|
+
"--format",
|
132
|
+
type=click.Choice(["log", "json"]),
|
133
|
+
default="log",
|
134
|
+
help="Output format",
|
135
|
+
)
|
136
|
+
@click.pass_obj
|
137
|
+
def tail_command(client, stream, filter_sql, lines, follow, format):
|
138
|
+
"""Tail logs from OpenObserve (like 'tail -f')."""
|
139
|
+
if client is None:
|
140
|
+
click.echo(
|
141
|
+
"OpenObserve not configured. Set OPENOBSERVE_URL, OPENOBSERVE_USER, and OPENOBSERVE_PASSWORD.",
|
142
|
+
err=True,
|
143
|
+
)
|
144
|
+
return 1
|
145
|
+
|
146
|
+
try:
|
147
|
+
click.echo(f"Tailing logs from stream '{stream}'...")
|
148
|
+
if filter_sql:
|
149
|
+
click.echo(f"Filter: {filter_sql}")
|
150
|
+
|
151
|
+
for log_entry in tail_logs(
|
152
|
+
stream=stream,
|
153
|
+
filter_sql=filter_sql,
|
154
|
+
follow=follow,
|
155
|
+
lines=lines,
|
156
|
+
client=client,
|
157
|
+
):
|
158
|
+
output = format_output(log_entry, format_type=format)
|
159
|
+
click.echo(output)
|
160
|
+
|
161
|
+
except KeyboardInterrupt:
|
162
|
+
click.echo("\nStopped tailing logs.")
|
163
|
+
except Exception as e:
|
164
|
+
click.echo(f"Tail failed: {e}", err=True)
|
165
|
+
return 1
|
166
|
+
|
167
|
+
@openobserve_group.command("errors")
|
168
|
+
@click.option(
|
169
|
+
"--stream",
|
170
|
+
"-s",
|
171
|
+
default="default",
|
172
|
+
help="Stream name to search",
|
173
|
+
)
|
174
|
+
@click.option(
|
175
|
+
"--start",
|
176
|
+
default="-1h",
|
177
|
+
help="Start time",
|
178
|
+
)
|
179
|
+
@click.option(
|
180
|
+
"--size",
|
181
|
+
"-n",
|
182
|
+
default=100,
|
183
|
+
type=int,
|
184
|
+
help="Number of results",
|
185
|
+
)
|
186
|
+
@click.option(
|
187
|
+
"--format",
|
188
|
+
"-f",
|
189
|
+
type=click.Choice(["json", "log", "table", "csv", "summary"]),
|
190
|
+
default="log",
|
191
|
+
help="Output format",
|
192
|
+
)
|
193
|
+
@click.pass_obj
|
194
|
+
def errors_command(client, stream, start, size, format):
|
195
|
+
"""Search for error logs."""
|
196
|
+
if client is None:
|
197
|
+
click.echo("OpenObserve not configured.", err=True)
|
198
|
+
return 1
|
199
|
+
|
200
|
+
try:
|
201
|
+
from provide.foundation.observability.openobserve import search_errors
|
202
|
+
|
203
|
+
response = search_errors(
|
204
|
+
stream=stream,
|
205
|
+
start_time=start,
|
206
|
+
size=size,
|
207
|
+
client=client,
|
208
|
+
)
|
209
|
+
|
210
|
+
if response.total == 0:
|
211
|
+
click.echo("No errors found in the specified time range.")
|
212
|
+
else:
|
213
|
+
output = format_output(response, format_type=format)
|
214
|
+
click.echo(output)
|
215
|
+
|
216
|
+
except Exception as e:
|
217
|
+
click.echo(f"Search failed: {e}", err=True)
|
218
|
+
return 1
|
219
|
+
|
220
|
+
@openobserve_group.command("trace")
|
221
|
+
@click.argument("trace_id")
|
222
|
+
@click.option(
|
223
|
+
"--stream",
|
224
|
+
"-s",
|
225
|
+
default="default",
|
226
|
+
help="Stream name to search",
|
227
|
+
)
|
228
|
+
@click.option(
|
229
|
+
"--format",
|
230
|
+
"-f",
|
231
|
+
type=click.Choice(["json", "log", "table"]),
|
232
|
+
default="log",
|
233
|
+
help="Output format",
|
234
|
+
)
|
235
|
+
@click.pass_obj
|
236
|
+
def trace_command(client, trace_id, stream, format):
|
237
|
+
"""Search for logs by trace ID."""
|
238
|
+
if client is None:
|
239
|
+
click.echo("OpenObserve not configured.", err=True)
|
240
|
+
return 1
|
241
|
+
|
242
|
+
try:
|
243
|
+
from provide.foundation.observability.openobserve import search_by_trace_id
|
244
|
+
|
245
|
+
response = search_by_trace_id(
|
246
|
+
trace_id=trace_id,
|
247
|
+
stream=stream,
|
248
|
+
client=client,
|
249
|
+
)
|
250
|
+
|
251
|
+
if response.total == 0:
|
252
|
+
click.echo(f"No logs found for trace ID: {trace_id}")
|
253
|
+
else:
|
254
|
+
output = format_output(response, format_type=format)
|
255
|
+
click.echo(output)
|
256
|
+
|
257
|
+
except Exception as e:
|
258
|
+
click.echo(f"Search failed: {e}", err=True)
|
259
|
+
return 1
|
260
|
+
|
261
|
+
@openobserve_group.command("streams")
|
262
|
+
@click.pass_obj
|
263
|
+
def streams_command(client):
|
264
|
+
"""List available streams."""
|
265
|
+
if client is None:
|
266
|
+
click.echo("OpenObserve not configured.", err=True)
|
267
|
+
return 1
|
268
|
+
|
269
|
+
try:
|
270
|
+
streams = client.list_streams()
|
271
|
+
|
272
|
+
if not streams:
|
273
|
+
click.echo("No streams found.")
|
274
|
+
else:
|
275
|
+
click.echo("Available streams:")
|
276
|
+
for stream in streams:
|
277
|
+
click.echo(f" - {stream.name} ({stream.stream_type})")
|
278
|
+
if stream.doc_count > 0:
|
279
|
+
click.echo(f" Documents: {stream.doc_count:,}")
|
280
|
+
click.echo(f" Size: {stream.original_size:,} bytes")
|
281
|
+
|
282
|
+
except Exception as e:
|
283
|
+
click.echo(f"Failed to list streams: {e}", err=True)
|
284
|
+
return 1
|
285
|
+
|
286
|
+
@openobserve_group.command("history")
|
287
|
+
@click.option(
|
288
|
+
"--size",
|
289
|
+
"-n",
|
290
|
+
default=20,
|
291
|
+
type=int,
|
292
|
+
help="Number of history entries",
|
293
|
+
)
|
294
|
+
@click.option(
|
295
|
+
"--stream",
|
296
|
+
"-s",
|
297
|
+
help="Filter by stream name",
|
298
|
+
)
|
299
|
+
@click.pass_obj
|
300
|
+
def history_command(client, size, stream):
|
301
|
+
"""View search history."""
|
302
|
+
if client is None:
|
303
|
+
click.echo("OpenObserve not configured.", err=True)
|
304
|
+
return 1
|
305
|
+
|
306
|
+
try:
|
307
|
+
response = client.get_search_history(
|
308
|
+
stream_name=stream,
|
309
|
+
size=size,
|
310
|
+
)
|
311
|
+
|
312
|
+
if response.total == 0:
|
313
|
+
click.echo("No search history found.")
|
314
|
+
else:
|
315
|
+
click.echo(f"Search history ({response.total} entries):")
|
316
|
+
for hit in response.hits:
|
317
|
+
sql = hit.get("sql", "N/A")
|
318
|
+
took = hit.get("took", 0)
|
319
|
+
records = hit.get("scan_records", 0)
|
320
|
+
click.echo(f"\n Query: {sql}")
|
321
|
+
click.echo(f" Time: {took:.2f}ms, Records: {records:,}")
|
322
|
+
|
323
|
+
except Exception as e:
|
324
|
+
click.echo(f"Failed to get history: {e}", err=True)
|
325
|
+
return 1
|
326
|
+
|
327
|
+
@openobserve_group.command("test")
|
328
|
+
@click.pass_obj
|
329
|
+
def test_command(client):
|
330
|
+
"""Test connection to OpenObserve."""
|
331
|
+
if client is None:
|
332
|
+
click.echo("OpenObserve not configured.", err=True)
|
333
|
+
return 1
|
334
|
+
|
335
|
+
click.echo(f"Testing connection to {client.url}...")
|
336
|
+
|
337
|
+
if client.test_connection():
|
338
|
+
click.echo("✅ Connection successful!")
|
339
|
+
click.echo(f"Organization: {client.organization}")
|
340
|
+
click.echo(f"User: {client.username}")
|
341
|
+
else:
|
342
|
+
click.echo("❌ Connection failed!")
|
343
|
+
return 1
|
344
|
+
|
345
|
+
# Export the command group for auto-discovery
|
346
|
+
__all__ = ["openobserve_group"]
|
347
|
+
|
348
|
+
else:
|
349
|
+
# Stub when click is not available
|
350
|
+
def openobserve_group(*args, **kwargs):
|
351
|
+
"""OpenObserve command stub when click is not available."""
|
352
|
+
raise ImportError(
|
353
|
+
"CLI commands require optional dependencies. "
|
354
|
+
"Install with: pip install 'provide-foundation[cli]'"
|
355
|
+
)
|
356
|
+
|
357
|
+
__all__ = []
|
@@ -0,0 +1,41 @@
|
|
1
|
+
"""
|
2
|
+
Custom exceptions for OpenObserve integration.
|
3
|
+
"""
|
4
|
+
|
5
|
+
from provide.foundation.errors import FoundationError
|
6
|
+
|
7
|
+
|
8
|
+
class OpenObserveError(FoundationError):
|
9
|
+
"""Base exception for OpenObserve-related errors."""
|
10
|
+
|
11
|
+
pass
|
12
|
+
|
13
|
+
|
14
|
+
class OpenObserveConnectionError(OpenObserveError):
|
15
|
+
"""Error connecting to OpenObserve API."""
|
16
|
+
|
17
|
+
pass
|
18
|
+
|
19
|
+
|
20
|
+
class OpenObserveAuthenticationError(OpenObserveError):
|
21
|
+
"""Authentication failed with OpenObserve."""
|
22
|
+
|
23
|
+
pass
|
24
|
+
|
25
|
+
|
26
|
+
class OpenObserveQueryError(OpenObserveError):
|
27
|
+
"""Error executing query in OpenObserve."""
|
28
|
+
|
29
|
+
pass
|
30
|
+
|
31
|
+
|
32
|
+
class OpenObserveStreamingError(OpenObserveError):
|
33
|
+
"""Error during streaming operations."""
|
34
|
+
|
35
|
+
pass
|
36
|
+
|
37
|
+
|
38
|
+
class OpenObserveConfigError(OpenObserveError):
|
39
|
+
"""Configuration error for OpenObserve."""
|
40
|
+
|
41
|
+
pass
|
@@ -0,0 +1,298 @@
|
|
1
|
+
"""
|
2
|
+
Output formatting utilities for OpenObserve results.
|
3
|
+
"""
|
4
|
+
|
5
|
+
import csv
|
6
|
+
from datetime import datetime
|
7
|
+
import io
|
8
|
+
import json
|
9
|
+
from typing import Any
|
10
|
+
|
11
|
+
from provide.foundation.observability.openobserve.models import SearchResponse
|
12
|
+
|
13
|
+
|
14
|
+
def format_json(response: SearchResponse | dict[str, Any], pretty: bool = True) -> str:
|
15
|
+
"""Format response as JSON.
|
16
|
+
|
17
|
+
Args:
|
18
|
+
response: Search response or log entry
|
19
|
+
pretty: If True, use pretty printing
|
20
|
+
|
21
|
+
Returns:
|
22
|
+
JSON string
|
23
|
+
"""
|
24
|
+
if isinstance(response, SearchResponse):
|
25
|
+
data = {
|
26
|
+
"hits": response.hits,
|
27
|
+
"total": response.total,
|
28
|
+
"took": response.took,
|
29
|
+
"scan_size": response.scan_size,
|
30
|
+
}
|
31
|
+
else:
|
32
|
+
data = response
|
33
|
+
|
34
|
+
if pretty:
|
35
|
+
return json.dumps(data, indent=2, sort_keys=False)
|
36
|
+
else:
|
37
|
+
return json.dumps(data)
|
38
|
+
|
39
|
+
|
40
|
+
def format_log_line(entry: dict[str, Any]) -> str:
|
41
|
+
"""Format a log entry as a traditional log line.
|
42
|
+
|
43
|
+
Args:
|
44
|
+
entry: Log entry dictionary
|
45
|
+
|
46
|
+
Returns:
|
47
|
+
Formatted log line
|
48
|
+
"""
|
49
|
+
# Extract common fields
|
50
|
+
timestamp = entry.get("_timestamp", 0)
|
51
|
+
level = entry.get("level", "INFO")
|
52
|
+
message = entry.get("message", "")
|
53
|
+
service = entry.get("service", "")
|
54
|
+
|
55
|
+
# Convert timestamp to readable format
|
56
|
+
if timestamp:
|
57
|
+
# Assuming microseconds
|
58
|
+
dt = datetime.fromtimestamp(timestamp / 1_000_000)
|
59
|
+
time_str = dt.strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
|
60
|
+
else:
|
61
|
+
time_str = "unknown"
|
62
|
+
|
63
|
+
# Build log line
|
64
|
+
parts = [time_str, f"[{level:5s}]"]
|
65
|
+
|
66
|
+
if service:
|
67
|
+
parts.append(f"[{service}]")
|
68
|
+
|
69
|
+
parts.append(message)
|
70
|
+
|
71
|
+
# Add additional fields as key=value
|
72
|
+
exclude_fields = {"_timestamp", "level", "message", "service", "_p"}
|
73
|
+
extra_fields = []
|
74
|
+
for key, value in entry.items():
|
75
|
+
if key not in exclude_fields:
|
76
|
+
extra_fields.append(f"{key}={value}")
|
77
|
+
|
78
|
+
if extra_fields:
|
79
|
+
parts.append(f"({', '.join(extra_fields)})")
|
80
|
+
|
81
|
+
return " ".join(parts)
|
82
|
+
|
83
|
+
|
84
|
+
def format_table(response: SearchResponse, columns: list[str] | None = None) -> str:
|
85
|
+
"""Format response as a table.
|
86
|
+
|
87
|
+
Args:
|
88
|
+
response: Search response
|
89
|
+
columns: Specific columns to include (None for all)
|
90
|
+
|
91
|
+
Returns:
|
92
|
+
Table string
|
93
|
+
"""
|
94
|
+
if not response.hits:
|
95
|
+
return "No results found"
|
96
|
+
|
97
|
+
# Determine columns
|
98
|
+
if columns is None:
|
99
|
+
# Get all unique keys from hits
|
100
|
+
all_keys = set()
|
101
|
+
for hit in response.hits:
|
102
|
+
all_keys.update(hit.keys())
|
103
|
+
# Sort columns, putting common ones first
|
104
|
+
priority_cols = ["_timestamp", "level", "service", "message"]
|
105
|
+
columns = []
|
106
|
+
for col in priority_cols:
|
107
|
+
if col in all_keys:
|
108
|
+
columns.append(col)
|
109
|
+
all_keys.remove(col)
|
110
|
+
columns.extend(sorted(all_keys))
|
111
|
+
|
112
|
+
# Filter out internal columns if not explicitly requested
|
113
|
+
if "_p" in columns and "_p" not in (columns or []):
|
114
|
+
columns = [c for c in columns if not c.startswith("_") or c == "_timestamp"]
|
115
|
+
|
116
|
+
# Try to use tabulate if available
|
117
|
+
try:
|
118
|
+
from tabulate import tabulate
|
119
|
+
|
120
|
+
# Prepare data
|
121
|
+
headers = columns
|
122
|
+
rows = []
|
123
|
+
for hit in response.hits:
|
124
|
+
row = []
|
125
|
+
for col in columns:
|
126
|
+
value = hit.get(col, "")
|
127
|
+
# Format timestamp
|
128
|
+
if col == "_timestamp" and value:
|
129
|
+
dt = datetime.fromtimestamp(value / 1_000_000)
|
130
|
+
value = dt.strftime("%Y-%m-%d %H:%M:%S")
|
131
|
+
# Truncate long values
|
132
|
+
value_str = str(value)
|
133
|
+
if len(value_str) > 50:
|
134
|
+
value_str = value_str[:47] + "..."
|
135
|
+
row.append(value_str)
|
136
|
+
rows.append(row)
|
137
|
+
|
138
|
+
return tabulate(rows, headers=headers, tablefmt="grid")
|
139
|
+
|
140
|
+
except ImportError:
|
141
|
+
# Fallback to simple formatting
|
142
|
+
lines = []
|
143
|
+
|
144
|
+
# Header
|
145
|
+
lines.append(" | ".join(columns))
|
146
|
+
lines.append("-" * (len(columns) * 15))
|
147
|
+
|
148
|
+
# Rows
|
149
|
+
for hit in response.hits:
|
150
|
+
row_values = []
|
151
|
+
for col in columns:
|
152
|
+
value = hit.get(col, "")
|
153
|
+
if col == "_timestamp" and value:
|
154
|
+
dt = datetime.fromtimestamp(value / 1_000_000)
|
155
|
+
value = dt.strftime("%H:%M:%S")
|
156
|
+
value_str = str(value)[:12]
|
157
|
+
row_values.append(value_str)
|
158
|
+
lines.append(" | ".join(row_values))
|
159
|
+
|
160
|
+
return "\n".join(lines)
|
161
|
+
|
162
|
+
|
163
|
+
def format_csv(response: SearchResponse, columns: list[str] | None = None) -> str:
|
164
|
+
"""Format response as CSV.
|
165
|
+
|
166
|
+
Args:
|
167
|
+
response: Search response
|
168
|
+
columns: Specific columns to include (None for all)
|
169
|
+
|
170
|
+
Returns:
|
171
|
+
CSV string
|
172
|
+
"""
|
173
|
+
if not response.hits:
|
174
|
+
return ""
|
175
|
+
|
176
|
+
# Determine columns
|
177
|
+
if columns is None:
|
178
|
+
all_keys = set()
|
179
|
+
for hit in response.hits:
|
180
|
+
all_keys.update(hit.keys())
|
181
|
+
columns = sorted(all_keys)
|
182
|
+
|
183
|
+
# Create CSV
|
184
|
+
output = io.StringIO()
|
185
|
+
writer = csv.DictWriter(output, fieldnames=columns, extrasaction="ignore")
|
186
|
+
|
187
|
+
writer.writeheader()
|
188
|
+
for hit in response.hits:
|
189
|
+
# Format timestamp for readability
|
190
|
+
if "_timestamp" in hit:
|
191
|
+
hit = hit.copy()
|
192
|
+
timestamp = hit["_timestamp"]
|
193
|
+
if timestamp:
|
194
|
+
dt = datetime.fromtimestamp(timestamp / 1_000_000)
|
195
|
+
hit["_timestamp"] = dt.isoformat()
|
196
|
+
writer.writerow(hit)
|
197
|
+
|
198
|
+
return output.getvalue()
|
199
|
+
|
200
|
+
|
201
|
+
def format_summary(response: SearchResponse) -> str:
|
202
|
+
"""Format a summary of the search response.
|
203
|
+
|
204
|
+
Args:
|
205
|
+
response: Search response
|
206
|
+
|
207
|
+
Returns:
|
208
|
+
Summary string
|
209
|
+
"""
|
210
|
+
lines = [
|
211
|
+
f"Total hits: {response.total}",
|
212
|
+
f"Returned: {len(response.hits)}",
|
213
|
+
f"Query time: {response.took}ms",
|
214
|
+
f"Scan size: {response.scan_size:,} bytes",
|
215
|
+
]
|
216
|
+
|
217
|
+
if response.trace_id:
|
218
|
+
lines.append(f"Trace ID: {response.trace_id}")
|
219
|
+
|
220
|
+
if response.is_partial:
|
221
|
+
lines.append("⚠️ Results are partial")
|
222
|
+
|
223
|
+
if response.function_error:
|
224
|
+
lines.append("Errors:")
|
225
|
+
for error in response.function_error:
|
226
|
+
lines.append(f" - {error}")
|
227
|
+
|
228
|
+
# Add level distribution if available
|
229
|
+
level_counts = {}
|
230
|
+
for hit in response.hits:
|
231
|
+
level = hit.get("level", "UNKNOWN")
|
232
|
+
level_counts[level] = level_counts.get(level, 0) + 1
|
233
|
+
|
234
|
+
if level_counts:
|
235
|
+
lines.append("\nLevel distribution:")
|
236
|
+
for level, count in sorted(level_counts.items()):
|
237
|
+
lines.append(f" {level}: {count}")
|
238
|
+
|
239
|
+
return "\n".join(lines)
|
240
|
+
|
241
|
+
|
242
|
+
def format_output(
|
243
|
+
response: SearchResponse | dict[str, Any],
|
244
|
+
format_type: str = "log",
|
245
|
+
**kwargs,
|
246
|
+
) -> str:
|
247
|
+
"""Format output based on specified type.
|
248
|
+
|
249
|
+
Args:
|
250
|
+
response: Search response or log entry
|
251
|
+
format_type: Output format (json, log, table, csv, summary)
|
252
|
+
**kwargs: Additional format-specific options
|
253
|
+
|
254
|
+
Returns:
|
255
|
+
Formatted string
|
256
|
+
"""
|
257
|
+
match format_type.lower():
|
258
|
+
case "json":
|
259
|
+
return format_json(response, **kwargs)
|
260
|
+
case "log":
|
261
|
+
if isinstance(response, dict):
|
262
|
+
return format_log_line(response)
|
263
|
+
else:
|
264
|
+
return "\n".join(format_log_line(hit) for hit in response.hits)
|
265
|
+
case "table":
|
266
|
+
if isinstance(response, SearchResponse):
|
267
|
+
return format_table(response, **kwargs)
|
268
|
+
else:
|
269
|
+
# Single entry as table
|
270
|
+
single_response = SearchResponse(
|
271
|
+
hits=[response],
|
272
|
+
total=1,
|
273
|
+
took=0,
|
274
|
+
scan_size=0,
|
275
|
+
)
|
276
|
+
return format_table(single_response, **kwargs)
|
277
|
+
case "csv":
|
278
|
+
if isinstance(response, SearchResponse):
|
279
|
+
return format_csv(response, **kwargs)
|
280
|
+
else:
|
281
|
+
single_response = SearchResponse(
|
282
|
+
hits=[response],
|
283
|
+
total=1,
|
284
|
+
took=0,
|
285
|
+
scan_size=0,
|
286
|
+
)
|
287
|
+
return format_csv(single_response, **kwargs)
|
288
|
+
case "summary":
|
289
|
+
if isinstance(response, SearchResponse):
|
290
|
+
return format_summary(response)
|
291
|
+
else:
|
292
|
+
return "Single log entry (use 'log' or 'json' format for details)"
|
293
|
+
case _:
|
294
|
+
# Default to log format
|
295
|
+
if isinstance(response, dict):
|
296
|
+
return format_log_line(response)
|
297
|
+
else:
|
298
|
+
return "\n".join(format_log_line(hit) for hit in response.hits)
|