Skip to content

Commit 242fdda

Browse files
committed
feat: add comprehensive logging system and export logs button
- Add utils/logger.py with file + console logging, exception capture - Add Export Logs button to GUI dashboard for easy debugging - Add global Tkinter exception handler to catch all GUI errors - Update launcher with detailed startup logging and error reporting - Logs saved to %APPDATA%\.config\tp-opencode\logs"
1 parent ae0af59 commit 242fdda

4 files changed

Lines changed: 169 additions & 36 deletions

File tree

src/opencode_telegram_bot/gui.py

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
import shutil
88
import sys
99
import threading
10+
import traceback
11+
from datetime import datetime
1012
from pathlib import Path
1113
from typing import Any, Callable
1214

@@ -17,6 +19,7 @@
1719
from opencode_telegram_bot.core.config import Settings, DEFAULT_CONFIG_DIR
1820
from opencode_telegram_bot.utils.scheduler import TaskScheduler
1921
from opencode_telegram_bot.utils.i18n import get_available_locales
22+
from opencode_telegram_bot.utils.logger import setup_logging, log_exception, get_log_contents, LOG_DIR, LOG_FILE
2023

2124
logger = logging.getLogger(__name__)
2225

@@ -348,7 +351,10 @@ def _build(self) -> None:
348351
logging.getLogger("opencode_telegram_bot").addHandler(log_handler)
349352
logging.getLogger("telegram").addHandler(log_handler)
350353

351-
_CTkButtonSecondary(right_panel, text="Clear Logs", command=self._clear_logs, height=36).grid(row=2, column=0, pady=(8, 0), sticky="e")
354+
btn_logs_frame = ctk.CTkFrame(right_panel, fg_color=COLORS["bg"])
355+
btn_logs_frame.grid(row=2, column=0, pady=(8, 0), sticky="e")
356+
_CTkButtonSecondary(btn_logs_frame, text="Clear Logs", command=self._clear_logs, height=36, width=120).pack(side="left", padx=(0, 8))
357+
_CTkButtonSecondary(btn_logs_frame, text="Export Logs", command=self._export_logs, height=36, width=120).pack(side="left")
352358

353359
def _log_callback(self, msg: str) -> None:
354360
self.after(0, lambda: self._append_log(msg))
@@ -360,6 +366,18 @@ def _append_log(self, msg: str) -> None:
360366
def _clear_logs(self) -> None:
361367
self.log_text.delete("1.0", "end")
362368

369+
def _export_logs(self) -> None:
370+
try:
371+
log_content = get_log_contents()
372+
gui_log = self.log_text.get("1.0", "end")
373+
combined = f"=== FILE LOG ({LOG_FILE}) ===\n{log_content}\n\n=== GUI LOG ===\n{gui_log}"
374+
export_file = LOG_DIR / f"tp-opencode-logs-{datetime.now().strftime('%Y%m%d-%H%M%S')}.txt"
375+
export_file.write_text(combined, encoding="utf-8")
376+
self._append_log(f"Logs exported to: {export_file}")
377+
self._append_log("Share this file for debugging support.")
378+
except Exception as exc:
379+
self._append_log(f"Failed to export logs: {exc}")
380+
363381
def _start_status_poll(self) -> None:
364382
self._poll_status()
365383

@@ -486,8 +504,9 @@ async def run():
486504
self.after(0, lambda: self._append_log("Bot polling started."))
487505
while not self._stop_event.is_set():
488506
await asyncio.sleep(0.5)
489-
except Exception as e:
490-
self.after(0, lambda: self._append_log(f"Bot error: {e}"))
507+
except Exception as exc:
508+
err_msg = f"Bot error: {exc}"
509+
self.after(0, lambda m=err_msg: self._append_log(m))
491510
self.after(0, lambda: self.bot_label.configure(text="Bot: Error", text_color=COLORS["error"]))
492511
finally:
493512
try:
@@ -511,8 +530,9 @@ async def run():
511530
asyncio.set_event_loop(loop)
512531
try:
513532
loop.run_until_complete(run())
514-
except Exception as e:
515-
self.after(0, lambda: self._append_log(f"Bot loop error: {e}"))
533+
except Exception as exc:
534+
err_msg = f"Bot loop error: {exc}"
535+
self.after(0, lambda m=err_msg: self._append_log(m))
516536
self.after(0, lambda: self.bot_label.configure(text="Bot: Error", text_color=COLORS["error"]))
517537
finally:
518538
loop.close()
@@ -536,9 +556,9 @@ def fetch():
536556
asyncio.set_event_loop(loop)
537557
providers = loop.run_until_complete(self.client.get_config_providers())
538558
loop.close()
539-
self.after(0, lambda: self._render_models(providers))
540-
except Exception as e:
541-
self.after(0, lambda: self._append_log(f"Failed to load models: {e}"))
559+
self.after(0, lambda p=providers: self._render_models(p))
560+
except Exception as exc:
561+
self.after(0, lambda e=str(exc): self._append_log(f"Failed to load models: {e}"))
542562

543563
threading.Thread(target=fetch, daemon=True).start()
544564

@@ -591,9 +611,22 @@ def __init__(self) -> None:
591611
ctk.set_appearance_mode("light")
592612
ctk.set_default_color_theme("blue")
593613

614+
setup_logging()
615+
self._logger = logging.getLogger("tp-opencode.gui")
616+
self._logger.info("GUI application starting")
617+
594618
self.settings: Settings | None = None
619+
self._bind_error_handler()
595620
self._build()
596621

622+
def _bind_error_handler(self) -> None:
623+
original_report = self.report_callback_exception
624+
def handle_exception(exc, val, tb):
625+
log_exception(exc, "Tkinter callback")
626+
self._logger.error("Unhandled GUI error: %s", val)
627+
original_report(exc, val, tb)
628+
self.report_callback_exception = handle_exception
629+
597630
def _build(self) -> None:
598631
env_file = DEFAULT_CONFIG_DIR / ".env"
599632
if env_file.exists():

src/opencode_telegram_bot/launcher.py

Lines changed: 45 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,16 @@
11
from __future__ import annotations
22

3-
import logging
43
import shutil
54
import sys
65
import time
6+
import traceback
77
from pathlib import Path
88

99
from dotenv import load_dotenv
1010

1111
from opencode_telegram_bot.api import OpenCodeServer
1212
from opencode_telegram_bot.core.config import DEFAULT_CONFIG_DIR
13-
14-
logger = logging.getLogger("tp-opencode-launcher")
13+
from opencode_telegram_bot.utils.logger import setup_logging, log_exception
1514

1615

1716
def _find_opencode() -> str:
@@ -31,31 +30,57 @@ def _find_opencode() -> str:
3130

3231

3332
def launch() -> None:
33+
logger = setup_logging()
34+
logger.info("=" * 60)
35+
logger.info("tp-opencode starting")
36+
logger.info("Python: %s", sys.version)
37+
logger.info("Platform: %s", sys.platform)
38+
logger.info("Config dir: %s", DEFAULT_CONFIG_DIR)
39+
3440
env_file = DEFAULT_CONFIG_DIR / ".env"
3541
if env_file.exists():
3642
load_dotenv(env_file, override=True)
43+
logger.info("Loaded config from %s", env_file)
44+
else:
45+
logger.warning("No .env file found at %s", env_file)
46+
47+
try:
48+
from opencode_telegram_bot.core.config import Settings
49+
50+
settings = Settings()
51+
logger.info("Settings loaded. API URL: %s", settings.opencode_api_url)
52+
logger.info("Auto-start: %s", settings.opencode_auto_start)
3753

38-
from opencode_telegram_bot.core.config import Settings
54+
opencode_cmd = settings.opencode_command or _find_opencode()
55+
logger.info("OpenCode command: %s", opencode_cmd)
3956

40-
settings = Settings()
41-
opencode_cmd = settings.opencode_command or _find_opencode()
57+
server = OpenCodeServer(
58+
command=opencode_cmd,
59+
work_dir=settings.opencode_work_dir or None,
60+
)
4261

43-
server = OpenCodeServer(
44-
command=opencode_cmd,
45-
work_dir=settings.opencode_work_dir or None,
46-
)
62+
if settings.opencode_auto_start and not server.is_running:
63+
try:
64+
logger.info("Starting OpenCode server...")
65+
server.start()
66+
time.sleep(3)
67+
logger.info("OpenCode server started (PID: %s)", server.get_pid())
68+
except FileNotFoundError:
69+
logger.warning("opencode command not found at '%s'. GUI will still open.", opencode_cmd)
70+
except Exception as e:
71+
log_exception(e, "Starting OpenCode server")
72+
logger.warning("Failed to start OpenCode server. GUI will still open.")
4773

48-
if settings.opencode_auto_start and not server.is_running:
49-
try:
50-
logger.info("Starting OpenCode server...")
51-
server.start()
52-
time.sleep(3)
53-
logger.info("OpenCode server started.")
54-
except FileNotFoundError:
55-
logger.warning("opencode command not found. GUI will still open.")
74+
logger.info("Launching GUI...")
75+
from opencode_telegram_bot.gui import main as gui_main
76+
gui_main()
5677

57-
from opencode_telegram_bot.gui import main as gui_main
58-
gui_main()
78+
except Exception as e:
79+
log_exception(e, "Launcher")
80+
print(f"\nFatal error: {e}")
81+
traceback.print_exc()
82+
input("Press Enter to exit...")
83+
raise
5984

6085

6186
if __name__ == "__main__":
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
from __future__ import annotations
2+
3+
import logging
4+
import sys
5+
import traceback
6+
from datetime import datetime
7+
from pathlib import Path
8+
9+
from opencode_telegram_bot.core.config import DEFAULT_CONFIG_DIR
10+
11+
LOG_DIR = DEFAULT_CONFIG_DIR / "logs"
12+
LOG_DIR.mkdir(parents=True, exist_ok=True)
13+
LOG_FILE = LOG_DIR / f"tp-opencode-{datetime.now().strftime('%Y-%m-%d')}.log"
14+
15+
16+
def setup_logging(level: str = "DEBUG") -> logging.Logger:
17+
root = logging.getLogger("tp-opencode")
18+
root.setLevel(getattr(logging, level.upper(), logging.DEBUG))
19+
20+
if root.handlers:
21+
return root
22+
23+
fmt = logging.Formatter(
24+
"%(asctime)s [%(levelname)s] %(name)s (%(filename)s:%(lineno)d): %(message)s",
25+
datefmt="%Y-%m-%d %H:%M:%S",
26+
)
27+
28+
fh = logging.FileHandler(LOG_FILE, encoding="utf-8")
29+
fh.setLevel(logging.DEBUG)
30+
fh.setFormatter(fmt)
31+
root.addHandler(fh)
32+
33+
ch = logging.StreamHandler(sys.stdout)
34+
ch.setLevel(logging.INFO)
35+
ch.setFormatter(fmt)
36+
root.addHandler(ch)
37+
38+
return root
39+
40+
41+
def log_exception(exc: Exception, context: str = "") -> None:
42+
root = logging.getLogger("tp-opencode")
43+
tb = traceback.format_exc()
44+
root.error("=== EXCEPTION ===")
45+
if context:
46+
root.error("Context: %s", context)
47+
root.error("Type: %s", type(exc).__name__)
48+
root.error("Message: %s", str(exc))
49+
root.error("Traceback:\n%s", tb)
50+
root.error("=== END EXCEPTION ===")
51+
52+
53+
def get_log_contents(max_lines: int = 500) -> str:
54+
if not LOG_FILE.exists():
55+
return "No log file found."
56+
lines = LOG_FILE.read_text(encoding="utf-8").splitlines()
57+
return "\n".join(lines[-max_lines:])

src/opencode_telegram_bot/utils/scheduler.py

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
from __future__ import annotations
22

3+
import asyncio
4+
import inspect
35
import logging
46
from typing import Any, Callable
57

6-
from apscheduler.schedulers.asyncio import AsyncIOScheduler
8+
from apscheduler.schedulers.background import BackgroundScheduler
79
from apscheduler.triggers.interval import IntervalTrigger
810
from apscheduler.triggers.cron import CronTrigger
911

@@ -15,21 +17,37 @@ class TaskScheduler:
1517

1618
def __init__(self, max_tasks: int = 10) -> None:
1719
self.max_tasks = max_tasks
18-
self._scheduler = AsyncIOScheduler()
20+
self._scheduler = BackgroundScheduler(daemon=True)
1921
self._tasks: dict[str, dict[str, Any]] = {}
2022
self._callbacks: dict[str, Callable] = {}
2123

2224
def start(self) -> None:
23-
self._scheduler.start()
24-
logger.info("Task scheduler started")
25+
if not self._scheduler.running:
26+
self._scheduler.start()
27+
logger.info("Task scheduler started")
2528

2629
def stop(self) -> None:
27-
self._scheduler.shutdown(wait=False)
28-
logger.info("Task scheduler stopped")
30+
if self._scheduler.running:
31+
self._scheduler.shutdown(wait=False)
32+
logger.info("Task scheduler stopped")
2933

3034
def register_callback(self, name: str, callback: Callable) -> None:
3135
self._callbacks[name] = callback
3236

37+
def _wrap_callback(self, callback: Callable) -> Callable:
38+
if inspect.iscoroutinefunction(callback):
39+
def async_wrapper(*args, **kwargs):
40+
try:
41+
loop = asyncio.new_event_loop()
42+
asyncio.set_event_loop(loop)
43+
loop.run_until_complete(callback(*args, **kwargs))
44+
except Exception:
45+
logger.exception("Scheduled async task failed")
46+
finally:
47+
loop.close()
48+
return async_wrapper
49+
return callback
50+
3351
def add_interval_task(
3452
self,
3553
task_id: str,
@@ -51,7 +69,7 @@ def add_interval_task(
5169

5270
trigger = IntervalTrigger(minutes=interval_minutes)
5371
self._scheduler.add_job(
54-
callback,
72+
self._wrap_callback(callback),
5573
trigger,
5674
args=[prompt, project_id, model_provider, model_id],
5775
id=task_id,
@@ -101,7 +119,7 @@ def add_cron_task(
101119
return False
102120

103121
self._scheduler.add_job(
104-
callback,
122+
self._wrap_callback(callback),
105123
trigger,
106124
args=[prompt, project_id, model_provider, model_id],
107125
id=task_id,

0 commit comments

Comments
 (0)