velune-cli 0.9.0__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.
- velune/__init__.py +5 -0
- velune/__main__.py +6 -0
- velune/cli/__init__.py +5 -0
- velune/cli/app.py +208 -0
- velune/cli/autocomplete.py +80 -0
- velune/cli/banner.py +60 -0
- velune/cli/commands/__init__.py +32 -0
- velune/cli/commands/ask.py +175 -0
- velune/cli/commands/base.py +16 -0
- velune/cli/commands/chat.py +228 -0
- velune/cli/commands/config.py +224 -0
- velune/cli/commands/daemon.py +88 -0
- velune/cli/commands/doctor.py +721 -0
- velune/cli/commands/init.py +170 -0
- velune/cli/commands/mcp.py +82 -0
- velune/cli/commands/memory.py +293 -0
- velune/cli/commands/models.py +683 -0
- velune/cli/commands/preflight.py +95 -0
- velune/cli/commands/run.py +270 -0
- velune/cli/commands/setup.py +184 -0
- velune/cli/commands/workspace.py +249 -0
- velune/cli/context.py +36 -0
- velune/cli/councilmodel_ui.py +199 -0
- velune/cli/display/council_view.py +254 -0
- velune/cli/display/memory_view.py +126 -0
- velune/cli/display/panels.py +35 -0
- velune/cli/display/progress.py +25 -0
- velune/cli/display/themes.py +25 -0
- velune/cli/main.py +15 -0
- velune/cli/model_selector.py +51 -0
- velune/cli/modes.py +86 -0
- velune/cli/pull_ui.py +123 -0
- velune/cli/registry.py +80 -0
- velune/cli/rendering/__init__.py +5 -0
- velune/cli/rendering/error_panel.py +79 -0
- velune/cli/rendering/markdown.py +63 -0
- velune/cli/repl.py +1855 -0
- velune/cli/session_manager.py +71 -0
- velune/cli/slash_commands.py +37 -0
- velune/cli/theme.py +8 -0
- velune/cognition/__init__.py +23 -0
- velune/cognition/agents/__init__.py +7 -0
- velune/cognition/agents/coder.py +209 -0
- velune/cognition/agents/planner.py +156 -0
- velune/cognition/agents/reviewer.py +195 -0
- velune/cognition/arbitrator.py +220 -0
- velune/cognition/architecture.py +415 -0
- velune/cognition/budget.py +65 -0
- velune/cognition/council/__init__.py +47 -0
- velune/cognition/council/base.py +217 -0
- velune/cognition/council/challenger.py +74 -0
- velune/cognition/council/coder.py +79 -0
- velune/cognition/council/critic_agent.py +43 -0
- velune/cognition/council/critic_configs.py +111 -0
- velune/cognition/council/critics.py +41 -0
- velune/cognition/council/debate.py +46 -0
- velune/cognition/council/factory.py +140 -0
- velune/cognition/council/messages.py +56 -0
- velune/cognition/council/planner.py +124 -0
- velune/cognition/council/reviewer.py +74 -0
- velune/cognition/council/synthesizer.py +67 -0
- velune/cognition/council/tiers.py +188 -0
- velune/cognition/council_orchestrator.py +282 -0
- velune/cognition/firewall.py +354 -0
- velune/cognition/module.py +46 -0
- velune/cognition/orchestrator.py +1205 -0
- velune/cognition/personality.py +238 -0
- velune/cognition/state.py +104 -0
- velune/cognition/style_resolver.py +64 -0
- velune/cognition/verification.py +205 -0
- velune/context/__init__.py +28 -0
- velune/context/assembler.py +240 -0
- velune/context/budget.py +97 -0
- velune/context/extractive.py +95 -0
- velune/context/prompt_adaptation.py +480 -0
- velune/context/sections.py +99 -0
- velune/context/token_counter.py +134 -0
- velune/context/utilization.py +33 -0
- velune/context/window.py +63 -0
- velune/core/__init__.py +89 -0
- velune/core/background.py +5 -0
- velune/core/config/__init__.py +37 -0
- velune/core/errors/__init__.py +90 -0
- velune/core/errors/catalog.py +188 -0
- velune/core/errors/execution.py +31 -0
- velune/core/errors/memory.py +25 -0
- velune/core/errors/orchestration.py +31 -0
- velune/core/errors/provider.py +37 -0
- velune/core/event_loop.py +35 -0
- velune/core/logging.py +83 -0
- velune/core/paths.py +165 -0
- velune/core/runtime.py +113 -0
- velune/core/startup_profiler.py +56 -0
- velune/core/task_registry.py +117 -0
- velune/core/trace.py +83 -0
- velune/core/types/__init__.py +48 -0
- velune/core/types/agent.py +53 -0
- velune/core/types/context.py +42 -0
- velune/core/types/inference.py +38 -0
- velune/core/types/memory.py +42 -0
- velune/core/types/model.py +70 -0
- velune/core/types/provider.py +62 -0
- velune/core/types/repository.py +38 -0
- velune/core/types/task.py +61 -0
- velune/core/types/workspace.py +28 -0
- velune/daemon/client.py +13 -0
- velune/daemon/server.py +127 -0
- velune/daemon/transport.py +179 -0
- velune/events.py +204 -0
- velune/execution/__init__.py +22 -0
- velune/execution/benchmarker.py +315 -0
- velune/execution/cancellation.py +53 -0
- velune/execution/checkpointer.py +130 -0
- velune/execution/command_spec.py +165 -0
- velune/execution/diff_preview.py +197 -0
- velune/execution/executor.py +181 -0
- velune/execution/module.py +18 -0
- velune/execution/multi_diff.py +67 -0
- velune/execution/path_guard.py +74 -0
- velune/execution/planner.py +91 -0
- velune/execution/rollback.py +89 -0
- velune/execution/sandbox.py +268 -0
- velune/execution/validator.py +115 -0
- velune/hardware/__init__.py +1 -0
- velune/hardware/detector.py +192 -0
- velune/kernel/__init__.py +55 -0
- velune/kernel/bootstrap.py +125 -0
- velune/kernel/config.py +426 -0
- velune/kernel/entrypoint.py +78 -0
- velune/kernel/health.py +54 -0
- velune/kernel/lifecycle.py +143 -0
- velune/kernel/module.py +17 -0
- velune/kernel/modules.py +23 -0
- velune/kernel/registry.py +96 -0
- velune/kernel/schemas.py +28 -0
- velune/main.py +9 -0
- velune/mcp/__init__.py +9 -0
- velune/mcp/client.py +115 -0
- velune/mcp/config.py +19 -0
- velune/mcp/server.py +624 -0
- velune/memory/__init__.py +32 -0
- velune/memory/compaction.py +506 -0
- velune/memory/embedding_pipeline.py +241 -0
- velune/memory/lifecycle.py +680 -0
- velune/memory/module.py +218 -0
- velune/memory/prioritizer.py +67 -0
- velune/memory/storage/episodic_schema.sql +53 -0
- velune/memory/storage/lancedb_store.py +282 -0
- velune/memory/storage/sqlite_manager.py +369 -0
- velune/memory/storage/sqlite_pool.py +149 -0
- velune/memory/tiers/episodic.py +588 -0
- velune/memory/tiers/graph.py +378 -0
- velune/memory/tiers/lineage.py +416 -0
- velune/memory/tiers/semantic.py +475 -0
- velune/memory/tiers/working.py +168 -0
- velune/memory/vitality.py +132 -0
- velune/models/__init__.py +15 -0
- velune/models/family.py +76 -0
- velune/models/module.py +20 -0
- velune/models/probes.py +192 -0
- velune/models/profile_cache.py +84 -0
- velune/models/profiler.py +108 -0
- velune/models/registry.py +251 -0
- velune/models/scorer.py +233 -0
- velune/models/specializations.py +205 -0
- velune/orchestration/__init__.py +19 -0
- velune/orchestration/engine.py +239 -0
- velune/orchestration/module.py +15 -0
- velune/orchestration/role_assignments.py +82 -0
- velune/orchestration/schemas.py +98 -0
- velune/plugins/__init__.py +20 -0
- velune/plugins/hooks.py +50 -0
- velune/plugins/loader.py +161 -0
- velune/plugins/registry.py +56 -0
- velune/plugins/schemas.py +21 -0
- velune/providers/__init__.py +23 -0
- velune/providers/adapters/anthropic.py +257 -0
- velune/providers/adapters/fireworks.py +115 -0
- velune/providers/adapters/google.py +234 -0
- velune/providers/adapters/groq.py +151 -0
- velune/providers/adapters/huggingface.py +210 -0
- velune/providers/adapters/llamacpp.py +208 -0
- velune/providers/adapters/lmstudio.py +175 -0
- velune/providers/adapters/ollama.py +233 -0
- velune/providers/adapters/openai.py +213 -0
- velune/providers/adapters/openrouter.py +81 -0
- velune/providers/adapters/together.py +134 -0
- velune/providers/adapters/xai.py +60 -0
- velune/providers/base.py +86 -0
- velune/providers/benchmarker.py +138 -0
- velune/providers/discovery/__init__.py +33 -0
- velune/providers/discovery/anthropic.py +79 -0
- velune/providers/discovery/benchmarks.py +44 -0
- velune/providers/discovery/classifier.py +69 -0
- velune/providers/discovery/fireworks.py +95 -0
- velune/providers/discovery/gguf.py +88 -0
- velune/providers/discovery/google.py +95 -0
- velune/providers/discovery/gpu.py +117 -0
- velune/providers/discovery/groq.py +21 -0
- velune/providers/discovery/huggingface.py +67 -0
- velune/providers/discovery/lmstudio.py +80 -0
- velune/providers/discovery/ollama.py +162 -0
- velune/providers/discovery/openai.py +96 -0
- velune/providers/discovery/openrouter.py +113 -0
- velune/providers/discovery/scanner.py +115 -0
- velune/providers/discovery/together.py +114 -0
- velune/providers/discovery/xai.py +57 -0
- velune/providers/health.py +67 -0
- velune/providers/health_monitor.py +169 -0
- velune/providers/keystore.py +142 -0
- velune/providers/local_paths.py +49 -0
- velune/providers/local_resolver.py +229 -0
- velune/providers/module.py +51 -0
- velune/providers/ollama_manager.py +193 -0
- velune/providers/registry.py +220 -0
- velune/providers/router.py +255 -0
- velune/providers/task_classifier.py +288 -0
- velune/py.typed +0 -0
- velune/repository/__init__.py +33 -0
- velune/repository/analyzer.py +127 -0
- velune/repository/ast_parser.py +822 -0
- velune/repository/blast_radius.py +298 -0
- velune/repository/boundary_classifier.py +295 -0
- velune/repository/cognition.py +316 -0
- velune/repository/grapher.py +179 -0
- velune/repository/import_graph.py +263 -0
- velune/repository/incremental_indexer.py +275 -0
- velune/repository/index_state.py +96 -0
- velune/repository/indexer.py +243 -0
- velune/repository/module.py +17 -0
- velune/repository/parser.py +474 -0
- velune/repository/project_type.py +300 -0
- velune/repository/rename_journal.py +287 -0
- velune/repository/scanner.py +193 -0
- velune/repository/schemas.py +102 -0
- velune/repository/symbol_registry.py +365 -0
- velune/repository/tracker.py +252 -0
- velune/retrieval/__init__.py +27 -0
- velune/retrieval/cache.py +110 -0
- velune/retrieval/fast_path.py +391 -0
- velune/retrieval/graph.py +124 -0
- velune/retrieval/hybrid.py +271 -0
- velune/retrieval/keyword.py +131 -0
- velune/retrieval/module.py +26 -0
- velune/retrieval/pipeline.py +303 -0
- velune/retrieval/reranker.py +102 -0
- velune/retrieval/schemas.py +59 -0
- velune/retrieval/slow_path.py +364 -0
- velune/retrieval/vector.py +203 -0
- velune/telemetry/__init__.py +59 -0
- velune/telemetry/cognition.py +267 -0
- velune/telemetry/cost_estimator.py +92 -0
- velune/telemetry/debug.py +304 -0
- velune/telemetry/doctor.py +244 -0
- velune/telemetry/logging.py +286 -0
- velune/telemetry/spans.py +277 -0
- velune/telemetry/token_tracker.py +140 -0
- velune/telemetry/usage_tracker.py +340 -0
- velune/tools/__init__.py +41 -0
- velune/tools/base/registry.py +87 -0
- velune/tools/base/tool.py +63 -0
- velune/tools/code/navigate.py +116 -0
- velune/tools/code/search.py +123 -0
- velune/tools/filesystem/read.py +75 -0
- velune/tools/filesystem/search.py +136 -0
- velune/tools/filesystem/write.py +163 -0
- velune/tools/git/history.py +177 -0
- velune/tools/git/operations.py +122 -0
- velune/tools/git/state.py +121 -0
- velune/tools/module.py +81 -0
- velune/tools/terminal/execute.py +72 -0
- velune/tools/terminal/history.py +47 -0
- velune/tools/web/fetch.py +55 -0
- velune/tools/web/validator.py +122 -0
- velune_cli-0.9.0.dist-info/METADATA +518 -0
- velune_cli-0.9.0.dist-info/RECORD +279 -0
- velune_cli-0.9.0.dist-info/WHEEL +4 -0
- velune_cli-0.9.0.dist-info/entry_points.txt +2 -0
- velune_cli-0.9.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
"""Doctor integration for telemetry and usage analytics."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import structlog
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
from rich.panel import Panel
|
|
11
|
+
from rich.table import Table
|
|
12
|
+
|
|
13
|
+
from velune.telemetry.usage_tracker import get_tracker
|
|
14
|
+
|
|
15
|
+
logger = structlog.get_logger()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def print_provider_health_report(console: Console | None = None) -> dict[str, Any]:
|
|
19
|
+
"""Print provider health and capability manifest report.
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
Dictionary with provider health data
|
|
23
|
+
"""
|
|
24
|
+
if console is None:
|
|
25
|
+
console = Console()
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
from velune.kernel.registry import get_container
|
|
29
|
+
|
|
30
|
+
container = get_container()
|
|
31
|
+
if not container.has("runtime.provider_health_monitor"):
|
|
32
|
+
return {"providers": []}
|
|
33
|
+
monitor = container.get("runtime.provider_health_monitor")
|
|
34
|
+
except (ImportError, AttributeError, KeyError):
|
|
35
|
+
return {"providers": []}
|
|
36
|
+
|
|
37
|
+
manifests = monitor.get_all_manifests()
|
|
38
|
+
|
|
39
|
+
# Print header
|
|
40
|
+
console.print(
|
|
41
|
+
Panel(
|
|
42
|
+
"[bold cyan]🏥 Provider Health[/bold cyan]",
|
|
43
|
+
expand=False,
|
|
44
|
+
)
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
if not manifests:
|
|
48
|
+
console.print("[dim]No provider manifests available[/dim]")
|
|
49
|
+
return {"providers": []}
|
|
50
|
+
|
|
51
|
+
# Create health table
|
|
52
|
+
table = Table(title=None, show_header=True)
|
|
53
|
+
table.add_column("Provider", style="cyan")
|
|
54
|
+
table.add_column("Status", style="green")
|
|
55
|
+
table.add_column("Latency", justify="right", style="yellow")
|
|
56
|
+
table.add_column("Models", justify="right", style="blue")
|
|
57
|
+
table.add_column("Rate Limit", style="magenta")
|
|
58
|
+
|
|
59
|
+
provider_data = []
|
|
60
|
+
for provider_id, manifest in sorted(manifests.items()):
|
|
61
|
+
from velune.core.types.provider import ProviderHealth
|
|
62
|
+
|
|
63
|
+
# Format status with icon
|
|
64
|
+
if manifest.health == ProviderHealth.HEALTHY:
|
|
65
|
+
status_str = "[green]✓ HEALTHY[/green]"
|
|
66
|
+
elif manifest.health == ProviderHealth.DEGRADED:
|
|
67
|
+
status_str = "[yellow]⚠ DEGRADED[/yellow]"
|
|
68
|
+
elif manifest.health == ProviderHealth.UNAVAILABLE:
|
|
69
|
+
status_str = "[red]✗ OFFLINE[/red]"
|
|
70
|
+
else:
|
|
71
|
+
status_str = "[dim]? UNKNOWN[/dim]"
|
|
72
|
+
|
|
73
|
+
# Format latency
|
|
74
|
+
if manifest.estimated_latency_ms > 0:
|
|
75
|
+
latency_str = f"{manifest.estimated_latency_ms}ms"
|
|
76
|
+
else:
|
|
77
|
+
latency_str = "—"
|
|
78
|
+
|
|
79
|
+
# Format model count
|
|
80
|
+
model_count = len(manifest.available_models)
|
|
81
|
+
models_str = str(model_count) if model_count > 0 else "—"
|
|
82
|
+
|
|
83
|
+
# Format rate limit
|
|
84
|
+
if manifest.rate_limit_remaining is not None:
|
|
85
|
+
if manifest.rate_limit_reset_at:
|
|
86
|
+
import time
|
|
87
|
+
|
|
88
|
+
reset_delta = int(manifest.rate_limit_reset_at - time.time())
|
|
89
|
+
reset_min = max(0, reset_delta // 60)
|
|
90
|
+
limit_str = f"{manifest.rate_limit_remaining} remaining (↻ {reset_min}m)"
|
|
91
|
+
else:
|
|
92
|
+
limit_str = f"{manifest.rate_limit_remaining} remaining"
|
|
93
|
+
else:
|
|
94
|
+
limit_str = "N/A (local)"
|
|
95
|
+
|
|
96
|
+
table.add_row(provider_id, status_str, latency_str, models_str, limit_str)
|
|
97
|
+
provider_data.append(
|
|
98
|
+
{
|
|
99
|
+
"provider": provider_id,
|
|
100
|
+
"health": str(manifest.health),
|
|
101
|
+
"latency_ms": manifest.estimated_latency_ms,
|
|
102
|
+
"model_count": model_count,
|
|
103
|
+
"rate_limit": manifest.rate_limit_remaining,
|
|
104
|
+
}
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
console.print(table)
|
|
108
|
+
return {"providers": provider_data}
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def print_telemetry_report(console: Console | None = None) -> dict[str, Any]:
|
|
112
|
+
"""Print telemetry and recent activity report for 'velune doctor'.
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
Dictionary with report data for structured analysis
|
|
116
|
+
"""
|
|
117
|
+
if console is None:
|
|
118
|
+
console = Console()
|
|
119
|
+
|
|
120
|
+
tracker = get_tracker()
|
|
121
|
+
|
|
122
|
+
# Get stats for last 7 days
|
|
123
|
+
stats = tracker.get_stats_last_n_days(days=7)
|
|
124
|
+
|
|
125
|
+
# Get recent sessions
|
|
126
|
+
recent_sessions = tracker.get_recent_sessions(days=7)
|
|
127
|
+
|
|
128
|
+
# Print header
|
|
129
|
+
console.print(
|
|
130
|
+
Panel(
|
|
131
|
+
"[bold cyan]📊 Telemetry & Usage (Last 7 Days)[/bold cyan]",
|
|
132
|
+
expand=False,
|
|
133
|
+
)
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
# Print summary stats
|
|
137
|
+
summary_lines = [
|
|
138
|
+
f"[green]Sessions:[/green] {stats['session_count']}",
|
|
139
|
+
f"[green]Total Tokens:[/green] {stats['total_tokens']:,}",
|
|
140
|
+
f"[green]Completions:[/green] {stats['completion_count']}",
|
|
141
|
+
]
|
|
142
|
+
|
|
143
|
+
if stats["total_cost"]:
|
|
144
|
+
summary_lines.append(f"[green]Estimated Cost:[/green] ${stats['total_cost']:.2f}")
|
|
145
|
+
|
|
146
|
+
if stats["most_used_model"]:
|
|
147
|
+
summary_lines.append(
|
|
148
|
+
f"[green]Most Used Model:[/green] {stats['most_used_model']} "
|
|
149
|
+
f"({stats['most_used_model_tokens']:,} tokens)"
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
console.print("\n".join(summary_lines))
|
|
153
|
+
|
|
154
|
+
# Print recent sessions table if any
|
|
155
|
+
if recent_sessions:
|
|
156
|
+
console.print("\n[bold]Recent Sessions:[/bold]")
|
|
157
|
+
|
|
158
|
+
table = Table(title=None, show_header=True)
|
|
159
|
+
table.add_column("Session ID", style="cyan")
|
|
160
|
+
table.add_column("Start Time", style="green")
|
|
161
|
+
table.add_column("Tokens", justify="right", style="yellow")
|
|
162
|
+
table.add_column("Cost", justify="right", style="magenta")
|
|
163
|
+
table.add_column("Models", style="blue")
|
|
164
|
+
|
|
165
|
+
for session in recent_sessions[:10]: # Show last 10
|
|
166
|
+
# Parse start time
|
|
167
|
+
start_time = datetime.fromisoformat(session.start_time)
|
|
168
|
+
time_str = start_time.strftime("%Y-%m-%d %H:%M")
|
|
169
|
+
|
|
170
|
+
# Format models
|
|
171
|
+
models_str = ", ".join(f"{m}({t:,})" for m, t in session.model_breakdown.items())
|
|
172
|
+
|
|
173
|
+
# Format cost
|
|
174
|
+
cost_str = f"${session.total_cost:.2f}" if session.total_cost else "—"
|
|
175
|
+
|
|
176
|
+
table.add_row(
|
|
177
|
+
session.session_id[:8],
|
|
178
|
+
time_str,
|
|
179
|
+
str(session.total_tokens),
|
|
180
|
+
cost_str,
|
|
181
|
+
models_str,
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
console.print(table)
|
|
185
|
+
|
|
186
|
+
# Print average metrics
|
|
187
|
+
if stats["session_count"] > 0:
|
|
188
|
+
avg_tokens_per_session = stats["total_tokens"] // stats["session_count"]
|
|
189
|
+
avg_cost_per_session = (
|
|
190
|
+
stats["total_cost"] / stats["session_count"] if stats["total_cost"] else None
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
console.print("\n[bold]Averages per Session:[/bold]")
|
|
194
|
+
avg_lines = [f" Tokens: {avg_tokens_per_session:,}"]
|
|
195
|
+
if avg_cost_per_session:
|
|
196
|
+
avg_lines.append(f" Cost: ${avg_cost_per_session:.2f}")
|
|
197
|
+
console.print("\n".join(avg_lines))
|
|
198
|
+
|
|
199
|
+
# Print log location
|
|
200
|
+
from velune.telemetry.logging import get_log_directory
|
|
201
|
+
|
|
202
|
+
log_dir = get_log_directory()
|
|
203
|
+
console.print(f"\n[dim]Logs: {log_dir}[/dim]")
|
|
204
|
+
|
|
205
|
+
# Return structured data
|
|
206
|
+
return {
|
|
207
|
+
"telemetry": {
|
|
208
|
+
"sessions_7d": stats["session_count"],
|
|
209
|
+
"total_tokens_7d": stats["total_tokens"],
|
|
210
|
+
"total_cost_7d": stats["total_cost"],
|
|
211
|
+
"completions_7d": stats["completion_count"],
|
|
212
|
+
"most_used_model": stats["most_used_model"],
|
|
213
|
+
"avg_tokens_per_session": (
|
|
214
|
+
stats["total_tokens"] // stats["session_count"] if stats["session_count"] > 0 else 0
|
|
215
|
+
),
|
|
216
|
+
},
|
|
217
|
+
"recent_sessions": [
|
|
218
|
+
{
|
|
219
|
+
"session_id": s.session_id,
|
|
220
|
+
"tokens": s.total_tokens,
|
|
221
|
+
"cost": s.total_cost,
|
|
222
|
+
"models": list(s.model_breakdown.keys()),
|
|
223
|
+
}
|
|
224
|
+
for s in recent_sessions[:10]
|
|
225
|
+
],
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def get_telemetry_status() -> dict[str, Any]:
|
|
230
|
+
"""Get telemetry status for health checks.
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
Dictionary with status and counts
|
|
234
|
+
"""
|
|
235
|
+
tracker = get_tracker()
|
|
236
|
+
stats = tracker.get_stats_last_n_days(days=7)
|
|
237
|
+
|
|
238
|
+
return {
|
|
239
|
+
"healthy": True,
|
|
240
|
+
"sessions_tracked": stats["session_count"],
|
|
241
|
+
"tokens_tracked": stats["total_tokens"],
|
|
242
|
+
"estimated_cost": stats["total_cost"],
|
|
243
|
+
"database": str(tracker.db_path),
|
|
244
|
+
}
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
"""Production-grade structured logging configuration using structlog.
|
|
2
|
+
|
|
3
|
+
Provides:
|
|
4
|
+
- JSON output to file (rotated daily, 7-day retention)
|
|
5
|
+
- Human-readable console output (unless --json flag)
|
|
6
|
+
- Automatic context binding (session_id, workspace_root, active_model)
|
|
7
|
+
- Log level from config or --debug flag
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import logging
|
|
13
|
+
import logging.handlers
|
|
14
|
+
import sys
|
|
15
|
+
from contextlib import contextmanager
|
|
16
|
+
from datetime import datetime, timedelta
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import TYPE_CHECKING, Any
|
|
19
|
+
|
|
20
|
+
import structlog
|
|
21
|
+
from structlog.processors import (
|
|
22
|
+
CallsiteParameterAdder,
|
|
23
|
+
JSONRenderer,
|
|
24
|
+
TimeStamper,
|
|
25
|
+
add_log_level,
|
|
26
|
+
format_exc_info,
|
|
27
|
+
)
|
|
28
|
+
from structlog.stdlib import ProcessorFormatter, add_logger_name
|
|
29
|
+
from structlog.typing import Processor
|
|
30
|
+
|
|
31
|
+
if TYPE_CHECKING:
|
|
32
|
+
from velune.kernel.config import VeluneConfig
|
|
33
|
+
|
|
34
|
+
logger = structlog.get_logger()
|
|
35
|
+
|
|
36
|
+
# Global state for configure_logging
|
|
37
|
+
_configured = False
|
|
38
|
+
_log_dir: Path | None = None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def configure_logging(
|
|
42
|
+
config: VeluneConfig | None = None,
|
|
43
|
+
debug: bool = False,
|
|
44
|
+
json_output: bool = False,
|
|
45
|
+
) -> None:
|
|
46
|
+
"""Configure structlog with file and console output.
|
|
47
|
+
|
|
48
|
+
MUST be called once before any other initialization in kernel/entrypoint.py.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
config: VeluneConfig with log_level setting
|
|
52
|
+
debug: If True, override to DEBUG level
|
|
53
|
+
json_output: If True, output JSON to console (default: human-readable)
|
|
54
|
+
"""
|
|
55
|
+
global _configured, _log_dir
|
|
56
|
+
|
|
57
|
+
if _configured:
|
|
58
|
+
return
|
|
59
|
+
|
|
60
|
+
_configured = True
|
|
61
|
+
|
|
62
|
+
# Determine log level
|
|
63
|
+
log_level = "DEBUG" if debug else (config.log_level.upper() if config else "INFO")
|
|
64
|
+
numeric_level = getattr(logging, log_level, logging.INFO)
|
|
65
|
+
|
|
66
|
+
# Create log directory
|
|
67
|
+
_log_dir = Path.home() / ".velune" / "logs"
|
|
68
|
+
_log_dir.mkdir(parents=True, exist_ok=True)
|
|
69
|
+
|
|
70
|
+
# Shared processors for structure
|
|
71
|
+
shared_processors: list[Processor] = [
|
|
72
|
+
add_log_level,
|
|
73
|
+
add_logger_name,
|
|
74
|
+
TimeStamper(fmt="iso"),
|
|
75
|
+
CallsiteParameterAdder(
|
|
76
|
+
parameters=[CallsiteParameterAdder.FILENAME, CallsiteParameterAdder.LINENO]
|
|
77
|
+
),
|
|
78
|
+
format_exc_info,
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
# File handler: JSON output
|
|
82
|
+
file_handler = logging.handlers.RotatingFileHandler(
|
|
83
|
+
filename=_log_dir / f"velune-{datetime.now().strftime('%Y-%m-%d')}.log",
|
|
84
|
+
maxBytes=50 * 1024 * 1024, # 50 MB per file
|
|
85
|
+
backupCount=7, # Keep 7 days
|
|
86
|
+
)
|
|
87
|
+
file_handler.setLevel(numeric_level)
|
|
88
|
+
file_handler.setFormatter(
|
|
89
|
+
ProcessorFormatter(
|
|
90
|
+
processor=JSONRenderer(),
|
|
91
|
+
foreign_pre_chain=shared_processors,
|
|
92
|
+
)
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
# Console handler: Human-readable or JSON
|
|
96
|
+
console_handler = logging.StreamHandler(sys.stdout)
|
|
97
|
+
console_handler.setLevel(numeric_level)
|
|
98
|
+
|
|
99
|
+
if json_output:
|
|
100
|
+
console_processor = JSONRenderer()
|
|
101
|
+
else:
|
|
102
|
+
console_processor = _ConsoleRenderer(colors=_supports_color())
|
|
103
|
+
|
|
104
|
+
console_handler.setFormatter(
|
|
105
|
+
ProcessorFormatter(
|
|
106
|
+
processor=console_processor,
|
|
107
|
+
foreign_pre_chain=shared_processors,
|
|
108
|
+
)
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
# Configure root logger
|
|
112
|
+
root_logger = logging.getLogger()
|
|
113
|
+
root_logger.setLevel(numeric_level)
|
|
114
|
+
root_logger.handlers.clear()
|
|
115
|
+
root_logger.addHandler(file_handler)
|
|
116
|
+
root_logger.addHandler(console_handler)
|
|
117
|
+
|
|
118
|
+
# Configure structlog
|
|
119
|
+
structlog.configure(
|
|
120
|
+
processors=shared_processors
|
|
121
|
+
+ [
|
|
122
|
+
structlog.stdlib.ProcessorFormatter.wrap_for_formatter,
|
|
123
|
+
],
|
|
124
|
+
context_class=dict,
|
|
125
|
+
logger_factory=structlog.stdlib.LoggerFactory(),
|
|
126
|
+
cache_logger_on_first_use=True,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
logger.info(
|
|
130
|
+
"Logging configured",
|
|
131
|
+
log_level=log_level,
|
|
132
|
+
file=str(_log_dir / f"velune-{datetime.now().strftime('%Y-%m-%d')}.log"),
|
|
133
|
+
json_console=json_output,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
# Schedule log cleanup (keep only 7 days)
|
|
137
|
+
_cleanup_old_logs()
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def get_log_directory() -> Path:
|
|
141
|
+
"""Get the configured log directory."""
|
|
142
|
+
if _log_dir is None:
|
|
143
|
+
return Path.home() / ".velune" / "logs"
|
|
144
|
+
return _log_dir
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _cleanup_old_logs(retention_days: int = 7) -> None:
|
|
148
|
+
"""Remove log files older than retention_days."""
|
|
149
|
+
log_dir = get_log_directory()
|
|
150
|
+
if not log_dir.exists():
|
|
151
|
+
return
|
|
152
|
+
|
|
153
|
+
cutoff = datetime.now() - timedelta(days=retention_days)
|
|
154
|
+
|
|
155
|
+
for log_file in log_dir.glob("velune-*.log*"):
|
|
156
|
+
try:
|
|
157
|
+
# Parse date from filename: velune-2026-06-12.log
|
|
158
|
+
date_str = log_file.stem.replace("velune-", "").split(".")[0]
|
|
159
|
+
file_date = datetime.strptime(date_str, "%Y-%m-%d")
|
|
160
|
+
|
|
161
|
+
if file_date < cutoff:
|
|
162
|
+
log_file.unlink()
|
|
163
|
+
except (ValueError, OSError):
|
|
164
|
+
# Skip files we can't parse or delete
|
|
165
|
+
pass
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
class _ConsoleRenderer:
|
|
169
|
+
"""Human-readable console output renderer."""
|
|
170
|
+
|
|
171
|
+
def __init__(self, colors: bool = True) -> None:
|
|
172
|
+
self.colors = colors
|
|
173
|
+
|
|
174
|
+
def __call__(self, logger, method_name: str, event_dict: dict[str, Any]) -> str:
|
|
175
|
+
"""Render event dict to human-readable console format."""
|
|
176
|
+
level = event_dict.pop("level", "INFO").upper()
|
|
177
|
+
timestamp = event_dict.pop("timestamp", "")
|
|
178
|
+
logger_name = event_dict.pop("logger_name", "")
|
|
179
|
+
filename = event_dict.pop("filename", "")
|
|
180
|
+
lineno = event_dict.pop("lineno", "")
|
|
181
|
+
|
|
182
|
+
# Build prefix
|
|
183
|
+
prefix_parts = []
|
|
184
|
+
if timestamp:
|
|
185
|
+
prefix_parts.append(f"[{timestamp}]")
|
|
186
|
+
if self.colors:
|
|
187
|
+
level_colored = self._colorize_level(level)
|
|
188
|
+
prefix_parts.append(level_colored)
|
|
189
|
+
else:
|
|
190
|
+
prefix_parts.append(f"[{level}]")
|
|
191
|
+
|
|
192
|
+
prefix = " ".join(prefix_parts)
|
|
193
|
+
|
|
194
|
+
# Main message
|
|
195
|
+
message = event_dict.pop("event", "")
|
|
196
|
+
|
|
197
|
+
# Build context (remaining fields)
|
|
198
|
+
context_parts = []
|
|
199
|
+
for key, value in sorted(event_dict.items()):
|
|
200
|
+
if key.startswith("_"):
|
|
201
|
+
continue
|
|
202
|
+
context_parts.append(f"{key}={value!r}")
|
|
203
|
+
|
|
204
|
+
context = " " + " ".join(context_parts) if context_parts else ""
|
|
205
|
+
|
|
206
|
+
# Source location
|
|
207
|
+
location = f" ({logger_name}:{filename}:{lineno})" if filename and lineno else ""
|
|
208
|
+
|
|
209
|
+
return f"{prefix} {message}{context}{location}"
|
|
210
|
+
|
|
211
|
+
def _colorize_level(self, level: str) -> str:
|
|
212
|
+
"""Add ANSI color codes to log level."""
|
|
213
|
+
colors = {
|
|
214
|
+
"DEBUG": "\033[36m[DEBUG]\033[0m", # Cyan
|
|
215
|
+
"INFO": "\033[32m[INFO]\033[0m", # Green
|
|
216
|
+
"WARNING": "\033[33m[WARNING]\033[0m", # Yellow
|
|
217
|
+
"ERROR": "\033[31m[ERROR]\033[0m", # Red
|
|
218
|
+
"CRITICAL": "\033[35m[CRITICAL]\033[0m", # Magenta
|
|
219
|
+
}
|
|
220
|
+
return colors.get(level, f"[{level}]")
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _supports_color() -> bool:
|
|
224
|
+
"""Check if terminal supports color output."""
|
|
225
|
+
import os
|
|
226
|
+
|
|
227
|
+
# Check if stdout is a tty
|
|
228
|
+
if not hasattr(sys.stdout, "isatty"):
|
|
229
|
+
return False
|
|
230
|
+
|
|
231
|
+
if not sys.stdout.isatty():
|
|
232
|
+
return False
|
|
233
|
+
|
|
234
|
+
# Check environment variables
|
|
235
|
+
term = os.environ.get("TERM", "").lower()
|
|
236
|
+
no_color = os.environ.get("NO_COLOR", "").lower()
|
|
237
|
+
|
|
238
|
+
if no_color:
|
|
239
|
+
return False
|
|
240
|
+
|
|
241
|
+
# Common terminals that support color
|
|
242
|
+
if term in ("dumb", ""):
|
|
243
|
+
return False
|
|
244
|
+
|
|
245
|
+
return True
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def bind_context(**kwargs) -> None:
|
|
249
|
+
"""Bind key-value pairs to the structlog context.
|
|
250
|
+
|
|
251
|
+
These will appear in all subsequent log lines until unbind_context() is called.
|
|
252
|
+
|
|
253
|
+
Example:
|
|
254
|
+
bind_context(session_id="sess-123", workspace_root="/path/to/project")
|
|
255
|
+
"""
|
|
256
|
+
structlog.contextvars.bind_contextvars(**kwargs)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def unbind_context(*keys: str) -> None:
|
|
260
|
+
"""Unbind keys from the structlog context.
|
|
261
|
+
|
|
262
|
+
Example:
|
|
263
|
+
unbind_context("session_id", "workspace_root")
|
|
264
|
+
"""
|
|
265
|
+
structlog.contextvars.unbind_contextvars(*keys)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def clear_context() -> None:
|
|
269
|
+
"""Clear all context variables."""
|
|
270
|
+
structlog.contextvars.clear_contextvars()
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
@contextmanager
|
|
274
|
+
def context_scope(**kwargs) -> Any:
|
|
275
|
+
"""Context manager for temporarily binding context variables.
|
|
276
|
+
|
|
277
|
+
Example:
|
|
278
|
+
with context_scope(span_id="span-456"):
|
|
279
|
+
# All logs here have span_id
|
|
280
|
+
logger.info("Processing")
|
|
281
|
+
"""
|
|
282
|
+
structlog.contextvars.bind_contextvars(**kwargs)
|
|
283
|
+
try:
|
|
284
|
+
yield
|
|
285
|
+
finally:
|
|
286
|
+
structlog.contextvars.unbind_contextvars(*kwargs.keys())
|