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,174 @@
|
|
1
|
+
"""
|
2
|
+
Query logs command for Foundation CLI.
|
3
|
+
"""
|
4
|
+
|
5
|
+
try:
|
6
|
+
import click
|
7
|
+
|
8
|
+
_HAS_CLICK = True
|
9
|
+
except ImportError:
|
10
|
+
click = None
|
11
|
+
_HAS_CLICK = False
|
12
|
+
|
13
|
+
from provide.foundation.logger import get_logger
|
14
|
+
|
15
|
+
log = get_logger(__name__)
|
16
|
+
|
17
|
+
|
18
|
+
if _HAS_CLICK:
|
19
|
+
|
20
|
+
@click.command("query")
|
21
|
+
@click.option(
|
22
|
+
"--sql",
|
23
|
+
help="SQL query to execute (if not provided, builds from other options)",
|
24
|
+
)
|
25
|
+
@click.option(
|
26
|
+
"--current-trace",
|
27
|
+
is_flag=True,
|
28
|
+
help="Query logs for the current active trace",
|
29
|
+
)
|
30
|
+
@click.option(
|
31
|
+
"--trace-id",
|
32
|
+
help="Query logs for a specific trace ID",
|
33
|
+
)
|
34
|
+
@click.option(
|
35
|
+
"--level",
|
36
|
+
type=click.Choice(["TRACE", "DEBUG", "INFO", "WARN", "ERROR", "CRITICAL"]),
|
37
|
+
help="Filter by log level",
|
38
|
+
)
|
39
|
+
@click.option(
|
40
|
+
"--service",
|
41
|
+
help="Filter by service name",
|
42
|
+
)
|
43
|
+
@click.option(
|
44
|
+
"--last",
|
45
|
+
help="Time range (e.g., 1h, 30m, 5m)",
|
46
|
+
default="1h",
|
47
|
+
)
|
48
|
+
@click.option(
|
49
|
+
"--stream",
|
50
|
+
default="default",
|
51
|
+
help="Stream to query",
|
52
|
+
)
|
53
|
+
@click.option(
|
54
|
+
"--size",
|
55
|
+
"-n",
|
56
|
+
type=int,
|
57
|
+
default=100,
|
58
|
+
help="Number of results",
|
59
|
+
)
|
60
|
+
@click.option(
|
61
|
+
"--format",
|
62
|
+
"-f",
|
63
|
+
type=click.Choice(["json", "log", "table", "csv", "summary"]),
|
64
|
+
default="log",
|
65
|
+
help="Output format",
|
66
|
+
)
|
67
|
+
@click.pass_context
|
68
|
+
def query_command(
|
69
|
+
ctx, sql, current_trace, trace_id, level, service, last, stream, size, format
|
70
|
+
):
|
71
|
+
"""Query logs from OpenObserve.
|
72
|
+
|
73
|
+
Examples:
|
74
|
+
# Query recent logs
|
75
|
+
foundation logs query --last 30m
|
76
|
+
|
77
|
+
# Query errors
|
78
|
+
foundation logs query --level ERROR --last 1h
|
79
|
+
|
80
|
+
# Query by current trace
|
81
|
+
foundation logs query --current-trace
|
82
|
+
|
83
|
+
# Query by specific trace
|
84
|
+
foundation logs query --trace-id abc123def456
|
85
|
+
|
86
|
+
# Query by service
|
87
|
+
foundation logs query --service auth-service --last 15m
|
88
|
+
|
89
|
+
# Custom SQL query
|
90
|
+
foundation logs query --sql "SELECT * FROM default WHERE duration_ms > 1000"
|
91
|
+
"""
|
92
|
+
from provide.foundation.observability.openobserve import (
|
93
|
+
format_output,
|
94
|
+
search_logs,
|
95
|
+
)
|
96
|
+
|
97
|
+
client = ctx.obj.get("client")
|
98
|
+
if not client:
|
99
|
+
click.echo("Error: OpenObserve not configured.", err=True)
|
100
|
+
return 1
|
101
|
+
|
102
|
+
# Build SQL query if not provided
|
103
|
+
if not sql:
|
104
|
+
# Handle current trace
|
105
|
+
if current_trace:
|
106
|
+
try:
|
107
|
+
# Try OpenTelemetry first
|
108
|
+
from opentelemetry import trace
|
109
|
+
|
110
|
+
current_span = trace.get_current_span()
|
111
|
+
if current_span and current_span.is_recording():
|
112
|
+
span_context = current_span.get_span_context()
|
113
|
+
trace_id = f"{span_context.trace_id:032x}"
|
114
|
+
else:
|
115
|
+
# Try Foundation tracer
|
116
|
+
from provide.foundation.tracer.context import (
|
117
|
+
get_current_trace_id,
|
118
|
+
)
|
119
|
+
|
120
|
+
trace_id = get_current_trace_id()
|
121
|
+
if not trace_id:
|
122
|
+
click.echo("No active trace found.", err=True)
|
123
|
+
return 1
|
124
|
+
except ImportError:
|
125
|
+
click.echo("Tracing not available.", err=True)
|
126
|
+
return 1
|
127
|
+
|
128
|
+
# Build WHERE clause
|
129
|
+
conditions = []
|
130
|
+
if trace_id:
|
131
|
+
conditions.append(f"trace_id = '{trace_id}'")
|
132
|
+
if level:
|
133
|
+
conditions.append(f"level = '{level}'")
|
134
|
+
if service:
|
135
|
+
conditions.append(f"service = '{service}'")
|
136
|
+
|
137
|
+
where_clause = f"WHERE {' AND '.join(conditions)}" if conditions else ""
|
138
|
+
sql = f"SELECT * FROM {stream} {where_clause} ORDER BY _timestamp DESC LIMIT {size}"
|
139
|
+
|
140
|
+
# Execute query
|
141
|
+
try:
|
142
|
+
response = search_logs(
|
143
|
+
sql=sql,
|
144
|
+
start_time=f"-{last}" if last else "-1h",
|
145
|
+
end_time="now",
|
146
|
+
size=size,
|
147
|
+
client=client,
|
148
|
+
)
|
149
|
+
|
150
|
+
# Format and display results
|
151
|
+
if response.total == 0:
|
152
|
+
click.echo("No logs found matching the query.")
|
153
|
+
else:
|
154
|
+
output = format_output(response, format_type=format)
|
155
|
+
click.echo(output)
|
156
|
+
|
157
|
+
# Show summary for non-summary formats
|
158
|
+
if format != "summary":
|
159
|
+
click.echo(
|
160
|
+
f"\n📊 Found {response.total} logs, showing {len(response.hits)}"
|
161
|
+
)
|
162
|
+
|
163
|
+
except Exception as e:
|
164
|
+
click.echo(f"Query failed: {e}", err=True)
|
165
|
+
return 1
|
166
|
+
|
167
|
+
else:
|
168
|
+
|
169
|
+
def query_command(*args, **kwargs):
|
170
|
+
"""Query command stub when click is not available."""
|
171
|
+
raise ImportError(
|
172
|
+
"CLI commands require optional dependencies. "
|
173
|
+
"Install with: pip install 'provide-foundation[cli]'"
|
174
|
+
)
|
@@ -0,0 +1,166 @@
|
|
1
|
+
"""
|
2
|
+
Send logs command for Foundation CLI.
|
3
|
+
"""
|
4
|
+
|
5
|
+
import json
|
6
|
+
import sys
|
7
|
+
|
8
|
+
try:
|
9
|
+
import click
|
10
|
+
|
11
|
+
_HAS_CLICK = True
|
12
|
+
except ImportError:
|
13
|
+
click = None
|
14
|
+
_HAS_CLICK = False
|
15
|
+
|
16
|
+
from provide.foundation.logger import get_logger
|
17
|
+
|
18
|
+
log = get_logger(__name__)
|
19
|
+
|
20
|
+
|
21
|
+
if _HAS_CLICK:
|
22
|
+
|
23
|
+
@click.command("send")
|
24
|
+
@click.option(
|
25
|
+
"--message",
|
26
|
+
"-m",
|
27
|
+
help="Log message to send (reads from stdin if not provided)",
|
28
|
+
)
|
29
|
+
@click.option(
|
30
|
+
"--level",
|
31
|
+
"-l",
|
32
|
+
type=click.Choice(["TRACE", "DEBUG", "INFO", "WARN", "ERROR", "CRITICAL"]),
|
33
|
+
default="INFO",
|
34
|
+
help="Log level",
|
35
|
+
)
|
36
|
+
@click.option(
|
37
|
+
"--service",
|
38
|
+
"-s",
|
39
|
+
help="Service name (uses config default if not provided)",
|
40
|
+
)
|
41
|
+
@click.option(
|
42
|
+
"--json",
|
43
|
+
"-j",
|
44
|
+
"json_attrs",
|
45
|
+
help="Additional attributes as JSON",
|
46
|
+
)
|
47
|
+
@click.option(
|
48
|
+
"--attr",
|
49
|
+
"-a",
|
50
|
+
multiple=True,
|
51
|
+
help="Additional attributes as key=value pairs",
|
52
|
+
)
|
53
|
+
@click.option(
|
54
|
+
"--trace-id",
|
55
|
+
help="Explicit trace ID to use",
|
56
|
+
)
|
57
|
+
@click.option(
|
58
|
+
"--span-id",
|
59
|
+
help="Explicit span ID to use",
|
60
|
+
)
|
61
|
+
@click.option(
|
62
|
+
"--otlp/--bulk",
|
63
|
+
"use_otlp",
|
64
|
+
default=True,
|
65
|
+
help="Use OTLP (default) or bulk API",
|
66
|
+
)
|
67
|
+
@click.pass_context
|
68
|
+
def send_command(
|
69
|
+
ctx, message, level, service, json_attrs, attr, trace_id, span_id, use_otlp
|
70
|
+
):
|
71
|
+
"""Send a log entry to OpenObserve.
|
72
|
+
|
73
|
+
Examples:
|
74
|
+
# Send a simple log
|
75
|
+
foundation logs send -m "User logged in" -l INFO
|
76
|
+
|
77
|
+
# Send with attributes
|
78
|
+
foundation logs send -m "Payment processed" --attr user_id=123 --attr amount=99.99
|
79
|
+
|
80
|
+
# Send from stdin
|
81
|
+
echo "Application started" | foundation logs send -l INFO
|
82
|
+
|
83
|
+
# Send with JSON attributes
|
84
|
+
foundation logs send -m "Error occurred" -j '{"error_code": 500, "path": "/api/users"}'
|
85
|
+
"""
|
86
|
+
from provide.foundation.observability.openobserve.otlp import send_log
|
87
|
+
|
88
|
+
# Get message from stdin if not provided
|
89
|
+
if not message:
|
90
|
+
if sys.stdin.isatty():
|
91
|
+
click.echo(
|
92
|
+
"Error: No message provided. Use -m or pipe input.", err=True
|
93
|
+
)
|
94
|
+
return 1
|
95
|
+
message = sys.stdin.read().strip()
|
96
|
+
if not message:
|
97
|
+
click.echo("Error: Empty message from stdin.", err=True)
|
98
|
+
return 1
|
99
|
+
|
100
|
+
# Build attributes
|
101
|
+
attributes = {}
|
102
|
+
|
103
|
+
# Add JSON attributes
|
104
|
+
if json_attrs:
|
105
|
+
try:
|
106
|
+
attributes.update(json.loads(json_attrs))
|
107
|
+
except json.JSONDecodeError as e:
|
108
|
+
click.echo(f"Error: Invalid JSON attributes: {e}", err=True)
|
109
|
+
return 1
|
110
|
+
|
111
|
+
# Add key=value attributes
|
112
|
+
for kv in attr:
|
113
|
+
if "=" not in kv:
|
114
|
+
click.echo(
|
115
|
+
f"Error: Invalid attribute format '{kv}'. Use key=value.", err=True
|
116
|
+
)
|
117
|
+
return 1
|
118
|
+
key, value = kv.split("=", 1)
|
119
|
+
# Try to parse value as number
|
120
|
+
try:
|
121
|
+
if "." in value:
|
122
|
+
value = float(value)
|
123
|
+
else:
|
124
|
+
value = int(value)
|
125
|
+
except ValueError:
|
126
|
+
pass # Keep as string
|
127
|
+
attributes[key] = value
|
128
|
+
|
129
|
+
# Add explicit trace/span IDs if provided
|
130
|
+
if trace_id:
|
131
|
+
attributes["trace_id"] = trace_id
|
132
|
+
if span_id:
|
133
|
+
attributes["span_id"] = span_id
|
134
|
+
|
135
|
+
# Send the log
|
136
|
+
try:
|
137
|
+
client = ctx.obj.get("client")
|
138
|
+
success = send_log(
|
139
|
+
message=message,
|
140
|
+
level=level,
|
141
|
+
service=service,
|
142
|
+
attributes=attributes if attributes else None,
|
143
|
+
prefer_otlp=use_otlp,
|
144
|
+
client=client,
|
145
|
+
)
|
146
|
+
|
147
|
+
if success:
|
148
|
+
click.echo(
|
149
|
+
f"✅ Log sent successfully via {'OTLP' if use_otlp else 'bulk API'}"
|
150
|
+
)
|
151
|
+
else:
|
152
|
+
click.echo("❌ Failed to send log", err=True)
|
153
|
+
return 1
|
154
|
+
|
155
|
+
except Exception as e:
|
156
|
+
click.echo(f"Error: {e}", err=True)
|
157
|
+
return 1
|
158
|
+
|
159
|
+
else:
|
160
|
+
|
161
|
+
def send_command(*args, **kwargs):
|
162
|
+
"""Send command stub when click is not available."""
|
163
|
+
raise ImportError(
|
164
|
+
"CLI commands require optional dependencies. "
|
165
|
+
"Install with: pip install 'provide-foundation[cli]'"
|
166
|
+
)
|
@@ -0,0 +1,112 @@
|
|
1
|
+
"""
|
2
|
+
Tail logs command for Foundation CLI.
|
3
|
+
"""
|
4
|
+
|
5
|
+
try:
|
6
|
+
import click
|
7
|
+
|
8
|
+
_HAS_CLICK = True
|
9
|
+
except ImportError:
|
10
|
+
click = None
|
11
|
+
_HAS_CLICK = False
|
12
|
+
|
13
|
+
from provide.foundation.logger import get_logger
|
14
|
+
|
15
|
+
log = get_logger(__name__)
|
16
|
+
|
17
|
+
|
18
|
+
if _HAS_CLICK:
|
19
|
+
|
20
|
+
@click.command("tail")
|
21
|
+
@click.option(
|
22
|
+
"--stream",
|
23
|
+
"-s",
|
24
|
+
default="default",
|
25
|
+
help="Stream to tail",
|
26
|
+
)
|
27
|
+
@click.option(
|
28
|
+
"--filter",
|
29
|
+
"-f",
|
30
|
+
"filter_sql",
|
31
|
+
help="SQL WHERE clause for filtering",
|
32
|
+
)
|
33
|
+
@click.option(
|
34
|
+
"--lines",
|
35
|
+
"-n",
|
36
|
+
type=int,
|
37
|
+
default=10,
|
38
|
+
help="Number of initial lines to show",
|
39
|
+
)
|
40
|
+
@click.option(
|
41
|
+
"--follow/--no-follow",
|
42
|
+
"-F/-N",
|
43
|
+
default=True,
|
44
|
+
help="Follow mode (like tail -f)",
|
45
|
+
)
|
46
|
+
@click.option(
|
47
|
+
"--format",
|
48
|
+
type=click.Choice(["log", "json"]),
|
49
|
+
default="log",
|
50
|
+
help="Output format",
|
51
|
+
)
|
52
|
+
@click.pass_context
|
53
|
+
def tail_command(ctx, stream, filter_sql, lines, follow, format):
|
54
|
+
"""Tail logs in real-time (like 'tail -f').
|
55
|
+
|
56
|
+
Examples:
|
57
|
+
# Tail all logs
|
58
|
+
foundation logs tail
|
59
|
+
|
60
|
+
# Tail error logs only
|
61
|
+
foundation logs tail --filter "level='ERROR'"
|
62
|
+
|
63
|
+
# Tail specific service
|
64
|
+
foundation logs tail --filter "service='auth-service'"
|
65
|
+
|
66
|
+
# Show last 20 lines and exit
|
67
|
+
foundation logs tail -n 20 --no-follow
|
68
|
+
|
69
|
+
# Tail with JSON output
|
70
|
+
foundation logs tail --format json
|
71
|
+
"""
|
72
|
+
from provide.foundation.observability.openobserve import (
|
73
|
+
format_output,
|
74
|
+
tail_logs,
|
75
|
+
)
|
76
|
+
|
77
|
+
client = ctx.obj.get("client")
|
78
|
+
if not client:
|
79
|
+
click.echo("Error: OpenObserve not configured.", err=True)
|
80
|
+
return 1
|
81
|
+
|
82
|
+
try:
|
83
|
+
click.echo(f"📡 Tailing logs from stream '{stream}'...")
|
84
|
+
if filter_sql:
|
85
|
+
click.echo(f" Filter: {filter_sql}")
|
86
|
+
click.echo(" Press Ctrl+C to stop\n")
|
87
|
+
|
88
|
+
# Tail logs
|
89
|
+
for log_entry in tail_logs(
|
90
|
+
stream=stream,
|
91
|
+
filter_sql=filter_sql,
|
92
|
+
follow=follow,
|
93
|
+
lines=lines,
|
94
|
+
client=client,
|
95
|
+
):
|
96
|
+
output = format_output(log_entry, format_type=format)
|
97
|
+
click.echo(output)
|
98
|
+
|
99
|
+
except KeyboardInterrupt:
|
100
|
+
click.echo("\n✋ Stopped tailing logs.")
|
101
|
+
except Exception as e:
|
102
|
+
click.echo(f"Tail failed: {e}", err=True)
|
103
|
+
return 1
|
104
|
+
|
105
|
+
else:
|
106
|
+
|
107
|
+
def tail_command(*args, **kwargs):
|
108
|
+
"""Tail command stub when click is not available."""
|
109
|
+
raise ImportError(
|
110
|
+
"CLI commands require optional dependencies. "
|
111
|
+
"Install with: pip install 'provide-foundation[cli]'"
|
112
|
+
)
|
@@ -0,0 +1,262 @@
|
|
1
|
+
"""Standard CLI decorators for consistent option handling."""
|
2
|
+
|
3
|
+
from collections.abc import Callable
|
4
|
+
import functools
|
5
|
+
from pathlib import Path
|
6
|
+
import sys
|
7
|
+
from typing import Any, TypeVar
|
8
|
+
|
9
|
+
try:
|
10
|
+
import click
|
11
|
+
except ImportError:
|
12
|
+
click = None
|
13
|
+
|
14
|
+
from provide.foundation.context import Context
|
15
|
+
|
16
|
+
F = TypeVar("F", bound=Callable[..., Any])
|
17
|
+
|
18
|
+
# Standard log level choices
|
19
|
+
LOG_LEVELS = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
|
20
|
+
|
21
|
+
|
22
|
+
def logging_options(f: F) -> F:
|
23
|
+
"""
|
24
|
+
Add standard logging options to a Click command.
|
25
|
+
|
26
|
+
Adds:
|
27
|
+
- --log-level/-l: Set logging verbosity (DEBUG, INFO, WARNING, ERROR, CRITICAL)
|
28
|
+
- --log-file: Write logs to file
|
29
|
+
- --log-format: Choose log output format (json, text, key_value)
|
30
|
+
"""
|
31
|
+
f = click.option(
|
32
|
+
"--log-level",
|
33
|
+
"-l",
|
34
|
+
type=click.Choice(LOG_LEVELS, case_sensitive=False),
|
35
|
+
default=None,
|
36
|
+
envvar="PROVIDE_LOG_LEVEL",
|
37
|
+
help="Set the logging level",
|
38
|
+
)(f)
|
39
|
+
f = click.option(
|
40
|
+
"--log-file",
|
41
|
+
type=click.Path(dir_okay=False, writable=True, path_type=Path),
|
42
|
+
default=None,
|
43
|
+
envvar="PROVIDE_LOG_FILE",
|
44
|
+
help="Write logs to file",
|
45
|
+
)(f)
|
46
|
+
f = click.option(
|
47
|
+
"--log-format",
|
48
|
+
type=click.Choice(["json", "text", "key_value"], case_sensitive=False),
|
49
|
+
default="key_value",
|
50
|
+
envvar="PROVIDE_LOG_FORMAT",
|
51
|
+
help="Log output format",
|
52
|
+
)(f)
|
53
|
+
return f
|
54
|
+
|
55
|
+
|
56
|
+
def config_options(f: F) -> F:
|
57
|
+
"""
|
58
|
+
Add configuration file options to a Click command.
|
59
|
+
|
60
|
+
Adds:
|
61
|
+
- --config/-c: Path to configuration file
|
62
|
+
- --profile/-p: Configuration profile to use
|
63
|
+
"""
|
64
|
+
f = click.option(
|
65
|
+
"--config",
|
66
|
+
"-c",
|
67
|
+
type=click.Path(exists=True, dir_okay=False, path_type=Path),
|
68
|
+
default=None,
|
69
|
+
envvar="PROVIDE_CONFIG_FILE",
|
70
|
+
help="Path to configuration file",
|
71
|
+
)(f)
|
72
|
+
f = click.option(
|
73
|
+
"--profile",
|
74
|
+
"-p",
|
75
|
+
default=None,
|
76
|
+
envvar="PROVIDE_PROFILE",
|
77
|
+
help="Configuration profile to use",
|
78
|
+
)(f)
|
79
|
+
return f
|
80
|
+
|
81
|
+
|
82
|
+
def output_options(f: F) -> F:
|
83
|
+
"""
|
84
|
+
Add output formatting options to a Click command.
|
85
|
+
|
86
|
+
Adds:
|
87
|
+
- --json: Output in JSON format
|
88
|
+
- --no-color: Disable colored output
|
89
|
+
- --no-emoji: Disable emoji in output
|
90
|
+
"""
|
91
|
+
f = click.option(
|
92
|
+
"--json",
|
93
|
+
"json_output",
|
94
|
+
is_flag=True,
|
95
|
+
default=None,
|
96
|
+
envvar="PROVIDE_JSON_OUTPUT",
|
97
|
+
help="Output in JSON format",
|
98
|
+
)(f)
|
99
|
+
f = click.option(
|
100
|
+
"--no-color",
|
101
|
+
is_flag=True,
|
102
|
+
default=False,
|
103
|
+
envvar="PROVIDE_NO_COLOR",
|
104
|
+
help="Disable colored output",
|
105
|
+
)(f)
|
106
|
+
f = click.option(
|
107
|
+
"--no-emoji",
|
108
|
+
is_flag=True,
|
109
|
+
default=False,
|
110
|
+
envvar="PROVIDE_NO_EMOJI",
|
111
|
+
help="Disable emoji in output",
|
112
|
+
)(f)
|
113
|
+
return f
|
114
|
+
|
115
|
+
|
116
|
+
def flexible_options(f: F) -> F:
|
117
|
+
"""
|
118
|
+
Apply flexible CLI options that can be used at any command level.
|
119
|
+
|
120
|
+
Combines logging_options and config_options for consistent
|
121
|
+
control at both group and command levels.
|
122
|
+
"""
|
123
|
+
f = logging_options(f)
|
124
|
+
f = config_options(f)
|
125
|
+
return f
|
126
|
+
|
127
|
+
|
128
|
+
def standard_options(f: F) -> F:
|
129
|
+
"""
|
130
|
+
Apply all standard CLI options.
|
131
|
+
|
132
|
+
Combines logging_options, config_options, and output_options.
|
133
|
+
|
134
|
+
Note: Consider using flexible_options for better granular control.
|
135
|
+
This decorator is maintained for backward compatibility.
|
136
|
+
"""
|
137
|
+
f = logging_options(f)
|
138
|
+
f = config_options(f)
|
139
|
+
f = output_options(f)
|
140
|
+
return f
|
141
|
+
|
142
|
+
|
143
|
+
def error_handler(f: F) -> F:
|
144
|
+
"""
|
145
|
+
Decorator to handle errors consistently in CLI commands.
|
146
|
+
|
147
|
+
Catches exceptions and formats them appropriately based on
|
148
|
+
debug mode and output format.
|
149
|
+
"""
|
150
|
+
|
151
|
+
@functools.wraps(f)
|
152
|
+
def wrapper(*args, **kwargs):
|
153
|
+
click.get_current_context()
|
154
|
+
debug = kwargs.get("debug", False)
|
155
|
+
json_output = kwargs.get("json_output", False)
|
156
|
+
|
157
|
+
try:
|
158
|
+
return f(*args, **kwargs)
|
159
|
+
except click.ClickException:
|
160
|
+
# Let Click handle its own exceptions
|
161
|
+
raise
|
162
|
+
except KeyboardInterrupt:
|
163
|
+
if not json_output:
|
164
|
+
click.secho("\nInterrupted by user", fg="yellow", err=True)
|
165
|
+
sys.exit(130) # Standard exit code for SIGINT
|
166
|
+
except Exception as e:
|
167
|
+
if debug:
|
168
|
+
# In debug mode, show full traceback
|
169
|
+
raise
|
170
|
+
|
171
|
+
if json_output:
|
172
|
+
import json
|
173
|
+
|
174
|
+
error_data = {
|
175
|
+
"error": str(e),
|
176
|
+
"type": type(e).__name__,
|
177
|
+
}
|
178
|
+
click.echo(json.dumps(error_data), err=True)
|
179
|
+
else:
|
180
|
+
click.secho(f"Error: {e}", fg="red", err=True)
|
181
|
+
|
182
|
+
sys.exit(1)
|
183
|
+
|
184
|
+
return wrapper
|
185
|
+
|
186
|
+
|
187
|
+
def pass_context(f: F) -> F:
|
188
|
+
"""
|
189
|
+
Decorator to pass the foundation Context to a command.
|
190
|
+
|
191
|
+
Creates or retrieves a Context from Click's context object
|
192
|
+
and passes it as the first argument to the decorated function.
|
193
|
+
"""
|
194
|
+
|
195
|
+
@functools.wraps(f)
|
196
|
+
@click.pass_context
|
197
|
+
def wrapper(ctx: click.Context, *args, **kwargs):
|
198
|
+
# Get or create foundation context
|
199
|
+
if not hasattr(ctx, "obj") or ctx.obj is None:
|
200
|
+
ctx.obj = Context()
|
201
|
+
elif not isinstance(ctx.obj, Context):
|
202
|
+
# If obj exists but isn't a Context, wrap it
|
203
|
+
if isinstance(ctx.obj, dict):
|
204
|
+
ctx.obj = Context.from_dict(ctx.obj)
|
205
|
+
else:
|
206
|
+
# Store existing obj and create new Context
|
207
|
+
old_obj = ctx.obj
|
208
|
+
ctx.obj = Context()
|
209
|
+
ctx.obj._cli_data = old_obj
|
210
|
+
|
211
|
+
# Update context from command options
|
212
|
+
if kwargs.get("log_level"):
|
213
|
+
ctx.obj.log_level = kwargs["log_level"]
|
214
|
+
if kwargs.get("log_file"):
|
215
|
+
# Ensure log_file is a Path object, as expected by Context
|
216
|
+
ctx.obj.log_file = Path(kwargs["log_file"])
|
217
|
+
if "log_format" in kwargs and kwargs["log_format"] is not None:
|
218
|
+
ctx.obj.log_format = kwargs["log_format"]
|
219
|
+
if "json_output" in kwargs and kwargs["json_output"] is not None:
|
220
|
+
ctx.obj.json_output = kwargs["json_output"]
|
221
|
+
if "no_color" in kwargs and kwargs["no_color"] is not None:
|
222
|
+
ctx.obj.no_color = kwargs["no_color"]
|
223
|
+
if "no_emoji" in kwargs and kwargs["no_emoji"] is not None:
|
224
|
+
ctx.obj.no_emoji = kwargs["no_emoji"]
|
225
|
+
if kwargs.get("profile"):
|
226
|
+
ctx.obj.profile = kwargs["profile"]
|
227
|
+
if kwargs.get("config"):
|
228
|
+
ctx.obj.load_config(kwargs["config"])
|
229
|
+
|
230
|
+
# Remove these from kwargs to avoid duplicate arguments
|
231
|
+
for key in [
|
232
|
+
"log_level",
|
233
|
+
"log_file",
|
234
|
+
"log_format",
|
235
|
+
"json_output",
|
236
|
+
"no_color",
|
237
|
+
"no_emoji",
|
238
|
+
"profile",
|
239
|
+
"config",
|
240
|
+
]:
|
241
|
+
kwargs.pop(key, None)
|
242
|
+
|
243
|
+
return f(ctx.obj, *args, **kwargs)
|
244
|
+
|
245
|
+
return wrapper
|
246
|
+
|
247
|
+
|
248
|
+
def version_option(version: str | None = None, prog_name: str | None = None):
|
249
|
+
"""
|
250
|
+
Add a --version option to display version information.
|
251
|
+
|
252
|
+
Args:
|
253
|
+
version: Version string to display
|
254
|
+
prog_name: Program name to display
|
255
|
+
"""
|
256
|
+
|
257
|
+
def decorator(f: F) -> F:
|
258
|
+
return click.version_option(
|
259
|
+
version=version, prog_name=prog_name, message="%(prog)s version %(version)s"
|
260
|
+
)(f)
|
261
|
+
|
262
|
+
return decorator
|