servly 0.2.0__py3-none-any.whl → 0.3.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.
- servly/cli.py +67 -92
- servly/logs.py +114 -154
- servly/service.py +3 -1
- {servly-0.2.0.dist-info → servly-0.3.0.dist-info}/METADATA +2 -1
- servly-0.3.0.dist-info/RECORD +9 -0
- servly-0.2.0.dist-info/RECORD +0 -9
- {servly-0.2.0.dist-info → servly-0.3.0.dist-info}/WHEEL +0 -0
- {servly-0.2.0.dist-info → servly-0.3.0.dist-info}/entry_points.txt +0 -0
- {servly-0.2.0.dist-info → servly-0.3.0.dist-info}/top_level.txt +0 -0
servly/cli.py
CHANGED
@@ -1,52 +1,40 @@
|
|
1
1
|
#!/usr/bin/env python3
|
2
2
|
"""
|
3
3
|
Command-line interface for Servly process manager.
|
4
|
+
使用Rich库进行终端输出格式化
|
4
5
|
"""
|
5
6
|
import os
|
6
7
|
import sys
|
7
8
|
import argparse
|
8
9
|
import logging
|
9
10
|
from pathlib import Path
|
10
|
-
from typing import List, Optional
|
11
|
+
from typing import List, Dict, Optional
|
11
12
|
|
12
13
|
from servly.service import ServiceManager
|
13
|
-
from servly.logs import LogManager
|
14
|
-
from servly.logs import
|
15
|
-
from servly.logs import align_colored_text # 导入对齐工具函数
|
14
|
+
from servly.logs import LogManager, Emojis
|
15
|
+
from servly.logs import console, print_header, print_info, print_warning, print_error, print_success, print_service_table
|
16
16
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
def format(self, record):
|
22
|
-
log_message = super().format(record)
|
23
|
-
|
24
|
-
if record.levelno >= logging.ERROR:
|
25
|
-
return f"{Emojis.ERROR} {Colors.BRIGHT_RED}{log_message}{Colors.RESET}"
|
26
|
-
elif record.levelno >= logging.WARNING:
|
27
|
-
return f"{Emojis.WARNING} {Colors.YELLOW}{log_message}{Colors.RESET}"
|
28
|
-
elif record.levelno >= logging.INFO:
|
29
|
-
return f"{Emojis.INFO} {Colors.GREEN}{log_message}{Colors.RESET}"
|
30
|
-
else:
|
31
|
-
return f"{Colors.BRIGHT_BLACK}{log_message}{Colors.RESET}"
|
17
|
+
from rich.logging import RichHandler
|
18
|
+
from rich.traceback import install
|
19
|
+
from rich.markup import escape
|
32
20
|
|
33
|
-
#
|
34
|
-
|
35
|
-
handler.setFormatter(ColoredFormatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s'))
|
36
|
-
logger = logging.getLogger('servly')
|
37
|
-
logger.setLevel(logging.INFO)
|
38
|
-
logger.addHandler(handler)
|
21
|
+
# 安装Rich的异常格式化器
|
22
|
+
install()
|
39
23
|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
24
|
+
# 配置Rich日志处理
|
25
|
+
logging.basicConfig(
|
26
|
+
level=logging.INFO,
|
27
|
+
format="%(message)s",
|
28
|
+
datefmt="[%X]",
|
29
|
+
handlers=[RichHandler(rich_tracebacks=True, markup=True)]
|
30
|
+
)
|
31
|
+
|
32
|
+
logger = logging.getLogger("servly")
|
45
33
|
|
46
34
|
def setup_arg_parser() -> argparse.ArgumentParser:
|
47
35
|
"""设置命令行参数解析器"""
|
48
36
|
parser = argparse.ArgumentParser(
|
49
|
-
description=f"{
|
37
|
+
description=f"{Emojis.SERVICE} Servly - Modern process manager",
|
50
38
|
formatter_class=argparse.RawDescriptionHelpFormatter
|
51
39
|
)
|
52
40
|
|
@@ -91,30 +79,30 @@ def handle_start(manager: ServiceManager, service_name: str) -> bool:
|
|
91
79
|
if service_name == 'all':
|
92
80
|
service_names = manager.get_service_names()
|
93
81
|
if not service_names:
|
94
|
-
|
82
|
+
print_warning("配置中没有定义任何服务。")
|
95
83
|
return False
|
96
84
|
|
97
|
-
print(f"{Emojis.START}
|
85
|
+
console.print(f"{Emojis.START} 正在启动所有服务...", style="running")
|
98
86
|
success = True
|
99
87
|
for name in service_names:
|
100
88
|
if not manager.start(name):
|
101
|
-
|
89
|
+
print_error(f"启动服务 '{name}' 失败")
|
102
90
|
success = False
|
103
91
|
else:
|
104
|
-
|
92
|
+
print_success(f"服务 '{name}' 已成功启动")
|
105
93
|
|
106
94
|
if success:
|
107
|
-
|
95
|
+
print_success("所有服务已成功启动!")
|
108
96
|
else:
|
109
|
-
|
97
|
+
print_warning("有些服务启动失败,请检查日志获取详情。")
|
110
98
|
|
111
99
|
return success
|
112
100
|
else:
|
113
101
|
result = manager.start(service_name)
|
114
102
|
if result:
|
115
|
-
|
103
|
+
print_success(f"服务 '{service_name}' 已成功启动")
|
116
104
|
else:
|
117
|
-
|
105
|
+
print_error(f"启动服务 '{service_name}' 失败")
|
118
106
|
return result
|
119
107
|
|
120
108
|
def handle_stop(manager: ServiceManager, service_name: str) -> bool:
|
@@ -122,30 +110,30 @@ def handle_stop(manager: ServiceManager, service_name: str) -> bool:
|
|
122
110
|
if service_name == 'all':
|
123
111
|
service_names = manager.get_running_services()
|
124
112
|
if not service_names:
|
125
|
-
print(f"{Emojis.INFO}
|
113
|
+
console.print(f"{Emojis.INFO} 当前没有正在运行的服务。", style="dim")
|
126
114
|
return True
|
127
115
|
|
128
|
-
print(f"{Emojis.STOP}
|
116
|
+
console.print(f"{Emojis.STOP} 正在停止所有服务...", style="warning")
|
129
117
|
success = True
|
130
118
|
for name in service_names:
|
131
119
|
if not manager.stop(name):
|
132
|
-
|
120
|
+
print_error(f"停止服务 '{name}' 失败")
|
133
121
|
success = False
|
134
122
|
else:
|
135
|
-
print(f"{Emojis.STOPPED}
|
123
|
+
console.print(f"{Emojis.STOPPED} 服务 '{name}' 已停止", style="stopped")
|
136
124
|
|
137
125
|
if success:
|
138
|
-
print(f"\n{Emojis.STOPPED}
|
126
|
+
console.print(f"\n{Emojis.STOPPED} 所有服务已成功停止!", style="warning")
|
139
127
|
else:
|
140
|
-
|
128
|
+
print_warning("有些服务停止失败,请检查日志获取详情。")
|
141
129
|
|
142
130
|
return success
|
143
131
|
else:
|
144
132
|
result = manager.stop(service_name)
|
145
133
|
if result:
|
146
|
-
print(f"{Emojis.STOPPED}
|
134
|
+
console.print(f"{Emojis.STOPPED} 服务 '{service_name}' 已成功停止", style="warning")
|
147
135
|
else:
|
148
|
-
|
136
|
+
print_error(f"停止服务 '{service_name}' 失败")
|
149
137
|
return result
|
150
138
|
|
151
139
|
def handle_restart(manager: ServiceManager, service_name: str) -> bool:
|
@@ -153,30 +141,30 @@ def handle_restart(manager: ServiceManager, service_name: str) -> bool:
|
|
153
141
|
if service_name == 'all':
|
154
142
|
service_names = manager.get_service_names()
|
155
143
|
if not service_names:
|
156
|
-
|
144
|
+
print_warning("配置中没有定义任何服务。")
|
157
145
|
return False
|
158
146
|
|
159
|
-
print(f"{Emojis.RESTART}
|
147
|
+
console.print(f"{Emojis.RESTART} 正在重启所有服务...", style="restart")
|
160
148
|
success = True
|
161
149
|
for name in service_names:
|
162
150
|
if not manager.restart(name):
|
163
|
-
|
151
|
+
print_error(f"重启服务 '{name}' 失败")
|
164
152
|
success = False
|
165
153
|
else:
|
166
|
-
print(f"{Emojis.RUNNING}
|
154
|
+
console.print(f"{Emojis.RUNNING} 服务 '{name}' 已成功重启", style="restart")
|
167
155
|
|
168
156
|
if success:
|
169
|
-
|
157
|
+
print_success("所有服务已成功重启!")
|
170
158
|
else:
|
171
|
-
|
159
|
+
print_warning("有些服务重启失败,请检查日志获取详情。")
|
172
160
|
|
173
161
|
return success
|
174
162
|
else:
|
175
163
|
result = manager.restart(service_name)
|
176
164
|
if result:
|
177
|
-
print(f"{Emojis.RUNNING}
|
165
|
+
console.print(f"{Emojis.RUNNING} 服务 '{service_name}' 已成功重启", style="restart")
|
178
166
|
else:
|
179
|
-
|
167
|
+
print_error(f"重启服务 '{service_name}' 失败")
|
180
168
|
return result
|
181
169
|
|
182
170
|
def handle_log(manager: ServiceManager, log_manager: LogManager, args) -> bool:
|
@@ -188,15 +176,15 @@ def handle_log(manager: ServiceManager, log_manager: LogManager, args) -> bool:
|
|
188
176
|
if service_name == 'all':
|
189
177
|
services = manager.get_service_names()
|
190
178
|
if not services:
|
191
|
-
|
179
|
+
print_warning("配置中没有定义任何服务。")
|
192
180
|
return False
|
193
181
|
else:
|
194
182
|
if service_name not in manager.get_service_names():
|
195
|
-
|
183
|
+
print_error(f"服务 '{service_name}' 在配置中未找到。")
|
196
184
|
return False
|
197
185
|
services = [service_name]
|
198
186
|
|
199
|
-
#
|
187
|
+
# 使用LogManager查看日志
|
200
188
|
log_manager.tail_logs(services, follow=follow, lines=lines)
|
201
189
|
return True
|
202
190
|
|
@@ -205,50 +193,36 @@ def handle_list(manager: ServiceManager) -> bool:
|
|
205
193
|
service_names = manager.get_service_names()
|
206
194
|
|
207
195
|
if not service_names:
|
208
|
-
|
196
|
+
print_warning("配置中没有定义任何服务。")
|
209
197
|
return True
|
210
198
|
|
211
|
-
|
212
|
-
print(f"\n{Colors.CYAN}{Colors.BOLD}{Emojis.SERVICE} 服务列表{Colors.RESET}")
|
213
|
-
print(f"{Colors.CYAN}{'─' * 60}{Colors.RESET}")
|
214
|
-
|
215
|
-
# 列标题
|
216
|
-
print(f"{Colors.BOLD}{align_colored_text(' 名称', 25)} {align_colored_text('状态', 20)} {align_colored_text('PID', 10)}{Colors.RESET}")
|
217
|
-
print(f"{Colors.BRIGHT_BLACK}{'─' * 60}{Colors.RESET}")
|
199
|
+
print_header("服务列表")
|
218
200
|
|
219
|
-
#
|
201
|
+
# 构建服务列表数据
|
202
|
+
services = []
|
220
203
|
for name in service_names:
|
221
204
|
is_running = manager.is_running(name)
|
222
|
-
pid = manager.get_service_pid(name)
|
205
|
+
pid = manager.get_service_pid(name)
|
223
206
|
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
service_color = Colors.CYAN if is_running else Colors.BRIGHT_BLACK
|
233
|
-
service_text = f"{service_color}{name}{Colors.RESET}"
|
234
|
-
|
235
|
-
# 使用对齐辅助函数确保正确对齐包含颜色代码的文本
|
236
|
-
aligned_service = align_colored_text(f" {service_text}", 25)
|
237
|
-
aligned_status = align_colored_text(status_text, 20)
|
238
|
-
aligned_pid = align_colored_text(pid_text, 10)
|
239
|
-
|
240
|
-
print(f"{aligned_service}{aligned_status}{aligned_pid}")
|
207
|
+
services.append({
|
208
|
+
"name": name,
|
209
|
+
"status": "running" if is_running else "stopped",
|
210
|
+
"pid": pid
|
211
|
+
})
|
212
|
+
|
213
|
+
# 使用Rich表格显示服务列表
|
214
|
+
print_service_table(services)
|
241
215
|
|
242
|
-
print(f"
|
243
|
-
print(f"
|
244
|
-
print()
|
216
|
+
console.print(f"[dim]配置文件: {manager.config_path}[/]")
|
217
|
+
console.print(f"[dim]运行中服务: {len(manager.get_running_services())}/{len(service_names)}[/]")
|
218
|
+
console.print()
|
245
219
|
|
246
220
|
return True
|
247
221
|
|
248
222
|
def main():
|
249
223
|
"""CLI 应用程序入口点"""
|
250
224
|
# 显示头部
|
251
|
-
print_header()
|
225
|
+
print_header("SERVLY - Modern Process Manager")
|
252
226
|
|
253
227
|
parser = setup_arg_parser()
|
254
228
|
args = parser.parse_args()
|
@@ -261,7 +235,7 @@ def main():
|
|
261
235
|
try:
|
262
236
|
service_manager = ServiceManager(config_path=args.config)
|
263
237
|
except Exception as e:
|
264
|
-
|
238
|
+
print_error(f"加载配置文件时出错: {escape(str(e))}")
|
265
239
|
return 1
|
266
240
|
|
267
241
|
# 创建日志管理器
|
@@ -283,10 +257,11 @@ def main():
|
|
283
257
|
parser.print_help()
|
284
258
|
return 1
|
285
259
|
except KeyboardInterrupt:
|
286
|
-
print(f"\n{Emojis.STOP}
|
260
|
+
console.print(f"\n{Emojis.STOP} 操作被用户中断", style="dim")
|
287
261
|
return 1
|
288
262
|
except Exception as e:
|
289
|
-
|
263
|
+
print_error(f"执行命令时出错: {escape(str(e))}")
|
264
|
+
logger.exception("命令执行异常")
|
290
265
|
return 1
|
291
266
|
|
292
267
|
return 0 if success else 1
|
servly/logs.py
CHANGED
@@ -1,56 +1,43 @@
|
|
1
1
|
"""
|
2
2
|
Log management functionality for Servly.
|
3
|
+
使用Rich库进行日志格式化和展示,实现PM2风格的日志效果。
|
3
4
|
"""
|
4
5
|
import os
|
5
6
|
import sys
|
6
7
|
import time
|
7
|
-
import select
|
8
|
-
import fcntl
|
9
|
-
import termios
|
10
|
-
import subprocess
|
11
8
|
import re
|
12
9
|
from pathlib import Path
|
13
10
|
from typing import List, Dict, Optional, Tuple
|
14
|
-
import random
|
15
11
|
|
12
|
+
from rich.console import Console
|
13
|
+
from rich.theme import Theme
|
14
|
+
from rich.text import Text
|
15
|
+
from rich.panel import Panel
|
16
|
+
from rich.table import Table
|
17
|
+
from rich.live import Live
|
16
18
|
|
17
|
-
#
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
BRIGHT_BLACK = "\033[90m"
|
33
|
-
BRIGHT_RED = "\033[91m"
|
34
|
-
BRIGHT_GREEN = "\033[92m"
|
35
|
-
BRIGHT_YELLOW = "\033[93m"
|
36
|
-
BRIGHT_BLUE = "\033[94m"
|
37
|
-
BRIGHT_MAGENTA = "\033[95m"
|
38
|
-
BRIGHT_CYAN = "\033[96m"
|
39
|
-
BRIGHT_WHITE = "\033[97m"
|
40
|
-
|
41
|
-
# 背景色
|
42
|
-
BG_BLACK = "\033[40m"
|
43
|
-
BG_RED = "\033[41m"
|
44
|
-
BG_GREEN = "\033[42m"
|
45
|
-
BG_YELLOW = "\033[43m"
|
46
|
-
BG_BLUE = "\033[44m"
|
47
|
-
BG_MAGENTA = "\033[45m"
|
48
|
-
BG_CYAN = "\033[46m"
|
49
|
-
BG_WHITE = "\033[47m"
|
19
|
+
# 自定义Rich主题
|
20
|
+
custom_theme = Theme({
|
21
|
+
"warning": "yellow",
|
22
|
+
"error": "bold red",
|
23
|
+
"info": "green",
|
24
|
+
"dim": "dim",
|
25
|
+
"stdout_service": "green",
|
26
|
+
"stderr_service": "red",
|
27
|
+
"header": "cyan bold",
|
28
|
+
"subheader": "bright_black",
|
29
|
+
"running": "green",
|
30
|
+
"stopped": "bright_black",
|
31
|
+
"restart": "magenta",
|
32
|
+
"separator": "cyan",
|
33
|
+
})
|
50
34
|
|
35
|
+
# 创建Rich控制台对象
|
36
|
+
console = Console(theme=custom_theme)
|
51
37
|
|
52
38
|
# 服务相关的 emoji
|
53
39
|
class Emojis:
|
40
|
+
"""服务状态相关的emoji图标"""
|
54
41
|
SERVICE = "🔧"
|
55
42
|
START = "🟢"
|
56
43
|
STOP = "🔴"
|
@@ -66,132 +53,102 @@ class Emojis:
|
|
66
53
|
STOPPED = "⛔"
|
67
54
|
LOADING = "⏳"
|
68
55
|
|
56
|
+
# Rich格式化输出工具函数
|
57
|
+
def print_header(title: str):
|
58
|
+
"""打印美化的标题"""
|
59
|
+
console.print()
|
60
|
+
console.rule(f"[header]{Emojis.SERVICE} {title}[/]", style="separator")
|
61
|
+
console.print()
|
69
62
|
|
70
|
-
|
71
|
-
|
72
|
-
""
|
73
|
-
ansi_escape = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])')
|
74
|
-
return ansi_escape.sub('', text)
|
63
|
+
def print_info(message: str):
|
64
|
+
"""打印信息消息"""
|
65
|
+
console.print(f"{Emojis.INFO} {message}", style="info")
|
75
66
|
|
76
|
-
def
|
77
|
-
"""
|
78
|
-
|
79
|
-
# 处理中文等宽字符(在大多数终端中占用两个字符宽度)
|
80
|
-
length = 0
|
81
|
-
for char in text:
|
82
|
-
if ord(char) > 127: # 简单判断是否是非ASCII字符
|
83
|
-
length += 2
|
84
|
-
else:
|
85
|
-
length += 1
|
86
|
-
return length
|
67
|
+
def print_warning(message: str):
|
68
|
+
"""打印警告消息"""
|
69
|
+
console.print(f"{Emojis.WARNING} {message}", style="warning")
|
87
70
|
|
88
|
-
def
|
89
|
-
"""
|
90
|
-
|
71
|
+
def print_error(message: str):
|
72
|
+
"""打印错误消息"""
|
73
|
+
console.print(f"{Emojis.ERROR} {message}", style="error")
|
74
|
+
|
75
|
+
def print_success(message: str):
|
76
|
+
"""打印成功消息"""
|
77
|
+
console.print(f"{Emojis.RUNNING} {message}", style="running")
|
78
|
+
|
79
|
+
def print_service_table(services: List[Dict]):
|
80
|
+
"""打印服务状态表格"""
|
81
|
+
table = Table(show_header=True, header_style="header", expand=True)
|
82
|
+
table.add_column("名称", style="cyan")
|
83
|
+
table.add_column("状态")
|
84
|
+
table.add_column("PID")
|
91
85
|
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
86
|
+
for service in services:
|
87
|
+
name = service["name"]
|
88
|
+
status = service["status"]
|
89
|
+
pid = service["pid"] or "-"
|
96
90
|
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
91
|
+
status_style = "running" if status == "running" else "stopped"
|
92
|
+
status_emoji = Emojis.RUNNING if status == "running" else Emojis.STOPPED
|
93
|
+
status_text = f"{status_emoji} {status.upper()}"
|
94
|
+
|
95
|
+
table.add_row(
|
96
|
+
name,
|
97
|
+
Text(status_text, style=status_style),
|
98
|
+
Text(str(pid), style=status_style)
|
99
|
+
)
|
102
100
|
|
103
|
-
|
104
|
-
|
105
|
-
elif align == 'center':
|
106
|
-
left_padding = padding // 2
|
107
|
-
right_padding = padding - left_padding
|
108
|
-
return ' ' * left_padding + text + ' ' * right_padding
|
109
|
-
else: # left alignment
|
110
|
-
return text + ' ' * padding
|
101
|
+
console.print(table)
|
102
|
+
console.print()
|
111
103
|
|
112
104
|
|
113
105
|
class LogManager:
|
114
|
-
"""
|
106
|
+
"""管理和显示服务日志"""
|
115
107
|
|
116
108
|
def __init__(self, log_dir: Path):
|
117
109
|
self.log_dir = log_dir
|
118
|
-
#
|
119
|
-
self.service_colors = {}
|
120
|
-
self.available_colors = [
|
121
|
-
Colors.GREEN, Colors.YELLOW, Colors.BLUE, Colors.MAGENTA, Colors.CYAN,
|
122
|
-
Colors.BRIGHT_GREEN, Colors.BRIGHT_YELLOW, Colors.BRIGHT_BLUE,
|
123
|
-
Colors.BRIGHT_MAGENTA, Colors.BRIGHT_CYAN
|
124
|
-
]
|
110
|
+
self.default_tail_lines = 15 # 默认展示最后15行日志
|
125
111
|
|
126
112
|
def get_log_files(self, service_name: str) -> Dict[str, Path]:
|
127
|
-
"""
|
113
|
+
"""获取服务的stdout和stderr日志文件路径"""
|
128
114
|
return {
|
129
115
|
'stdout': self.log_dir / f"{service_name}-out.log",
|
130
116
|
'stderr': self.log_dir / f"{service_name}-error.log"
|
131
117
|
}
|
132
118
|
|
133
|
-
def
|
134
|
-
"""
|
135
|
-
|
136
|
-
|
137
|
-
# 如果颜色用完了,就随机分配
|
138
|
-
self.service_colors[service_name] = random.choice([
|
139
|
-
Colors.GREEN, Colors.YELLOW, Colors.BLUE, Colors.MAGENTA, Colors.CYAN
|
140
|
-
])
|
141
|
-
else:
|
142
|
-
# 从可用颜色中选择一个
|
143
|
-
self.service_colors[service_name] = self.available_colors.pop(0)
|
144
|
-
return self.service_colors[service_name]
|
145
|
-
|
146
|
-
def _format_log_header(self, service: str, log_type: str) -> str:
|
147
|
-
"""格式化日志头部,带颜色和 emoji"""
|
148
|
-
service_color = self._get_service_color(service)
|
149
|
-
emoji = Emojis.STDOUT if log_type == 'stdout' else Emojis.STDERR
|
150
|
-
type_color = Colors.GREEN if log_type == 'stdout' else Colors.YELLOW
|
119
|
+
def _parse_log_line(self, line: str) -> Tuple[str, str]:
|
120
|
+
"""解析日志行,提取时间戳和内容"""
|
121
|
+
timestamp = ""
|
122
|
+
content = line.rstrip()
|
151
123
|
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
"""格式化单行日志,带颜色和 emoji"""
|
159
|
-
service_color = self._get_service_color(service)
|
160
|
-
timestamp = time.strftime("%Y-%m-%d %H:%M:%S") if show_timestamp else ""
|
161
|
-
|
162
|
-
# 根据日志类型选择颜色
|
163
|
-
line_color = Colors.RESET
|
164
|
-
if log_type == 'stderr' and ('error' in line.lower() or 'exception' in line.lower()):
|
165
|
-
prefix = f"{Emojis.ERROR} "
|
166
|
-
line_color = Colors.BRIGHT_RED
|
167
|
-
elif log_type == 'stderr':
|
168
|
-
prefix = f"{Emojis.WARNING} "
|
169
|
-
line_color = Colors.YELLOW
|
124
|
+
# 尝试提取时间戳
|
125
|
+
timestamp_match = re.search(r'(\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}:\d{2})', line)
|
126
|
+
if timestamp_match:
|
127
|
+
timestamp = timestamp_match.group(1)
|
128
|
+
# 移除行中已有的时间戳部分
|
129
|
+
content = line.replace(timestamp, "", 1).lstrip().rstrip()
|
170
130
|
else:
|
171
|
-
|
131
|
+
timestamp = time.strftime("%Y-%m-%d %H:%M:%S")
|
172
132
|
|
173
|
-
|
174
|
-
if show_timestamp:
|
175
|
-
return (f"{Colors.BRIGHT_BLACK}[{Emojis.TIME} {timestamp}]{Colors.RESET} "
|
176
|
-
f"{service_color}{service}{Colors.RESET} "
|
177
|
-
f"{prefix}{line_color}{line.rstrip()}{Colors.RESET}")
|
178
|
-
else:
|
179
|
-
return f"{line_color}{line.rstrip()}{Colors.RESET}"
|
133
|
+
return timestamp, content
|
180
134
|
|
181
|
-
def tail_logs(self, service_names: List[str], follow: bool = True, lines: int =
|
135
|
+
def tail_logs(self, service_names: List[str], follow: bool = True, lines: int = None):
|
182
136
|
"""
|
183
|
-
|
137
|
+
显示服务日志
|
184
138
|
|
185
139
|
Args:
|
186
|
-
service_names:
|
187
|
-
follow:
|
188
|
-
lines:
|
140
|
+
service_names: 要查看的服务名称列表
|
141
|
+
follow: 是否实时跟踪日志(类似tail -f)
|
142
|
+
lines: 初始显示的行数,默认为self.default_tail_lines
|
189
143
|
"""
|
144
|
+
if lines is None:
|
145
|
+
lines = self.default_tail_lines
|
146
|
+
|
190
147
|
if not service_names:
|
191
|
-
|
148
|
+
print_warning("未指定要查看日志的服务。")
|
192
149
|
return
|
193
150
|
|
194
|
-
#
|
151
|
+
# 检查日志文件是否存在
|
195
152
|
log_files = []
|
196
153
|
for service in service_names:
|
197
154
|
service_logs = self.get_log_files(service)
|
@@ -199,52 +156,53 @@ class LogManager:
|
|
199
156
|
if log_path.exists():
|
200
157
|
log_files.append((service, log_type, log_path))
|
201
158
|
else:
|
202
|
-
|
203
|
-
print(f"{Emojis.WARNING} {
|
159
|
+
style = "stderr_service" if log_type == "stderr" else "stdout_service"
|
160
|
+
console.print(f"{Emojis.WARNING} 未找到服务 [{style}]{service}[/] 的 {log_type} 日志。", style="warning")
|
204
161
|
|
205
162
|
if not log_files:
|
206
|
-
|
163
|
+
print_warning("未找到指定服务的日志文件。")
|
207
164
|
return
|
208
165
|
|
209
166
|
if follow:
|
167
|
+
# 首先显示最后几行,然后再开始跟踪
|
168
|
+
self._display_recent_logs(log_files, lines)
|
210
169
|
self._follow_logs(log_files)
|
211
170
|
else:
|
212
171
|
self._display_recent_logs(log_files, lines)
|
213
172
|
|
214
173
|
def _display_recent_logs(self, log_files: List[Tuple[str, str, Path]], lines: int):
|
215
|
-
"""
|
174
|
+
"""显示最近的日志行"""
|
216
175
|
for service, log_type, log_path in log_files:
|
217
|
-
|
176
|
+
# PM2风格的标题
|
177
|
+
console.print(f"\n[dim]{log_path} last {lines} lines:[/]")
|
178
|
+
|
218
179
|
try:
|
219
180
|
# 读取最后N行
|
220
181
|
with open(log_path, 'r') as f:
|
221
182
|
content = f.readlines()
|
222
183
|
last_lines = content[-lines:] if len(content) >= lines else content
|
223
184
|
|
224
|
-
#
|
185
|
+
# 打印每一行,PM2格式
|
225
186
|
for line in last_lines:
|
226
|
-
|
187
|
+
timestamp, message = self._parse_log_line(line)
|
188
|
+
style = "stderr_service" if log_type == "stderr" else "stdout_service"
|
189
|
+
console.print(f"[{style}]{service}[/] | {timestamp}: {message}")
|
227
190
|
except Exception as e:
|
228
|
-
|
191
|
+
print_error(f"读取日志文件出错: {str(e)}")
|
229
192
|
|
230
193
|
def _follow_logs(self, log_files: List[Tuple[str, str, Path]]):
|
231
|
-
"""
|
232
|
-
# Dictionary to keep track of file positions
|
194
|
+
"""实时跟踪日志(类似tail -f)"""
|
233
195
|
file_handlers = {}
|
234
196
|
|
235
197
|
try:
|
236
|
-
#
|
198
|
+
# 打开所有日志文件
|
237
199
|
for service, log_type, log_path in log_files:
|
238
200
|
f = open(log_path, 'r')
|
239
|
-
#
|
201
|
+
# 移动到文件末尾
|
240
202
|
f.seek(0, os.SEEK_END)
|
241
203
|
file_handlers[(service, log_type)] = f
|
242
204
|
|
243
|
-
|
244
|
-
print(self._format_log_header(service, log_type))
|
245
|
-
|
246
|
-
print(f"\n{Emojis.LOADING} {Colors.BRIGHT_BLACK}Following logs... (Ctrl+C to stop){Colors.RESET}")
|
247
|
-
print(f"{Colors.BRIGHT_BLACK}{'─' * 60}{Colors.RESET}")
|
205
|
+
console.print(f"\n[dim]正在跟踪日志... (按Ctrl+C停止)[/]")
|
248
206
|
|
249
207
|
while True:
|
250
208
|
has_new_data = False
|
@@ -253,14 +211,16 @@ class LogManager:
|
|
253
211
|
line = f.readline()
|
254
212
|
if line:
|
255
213
|
has_new_data = True
|
256
|
-
|
214
|
+
timestamp, message = self._parse_log_line(line)
|
215
|
+
style = "stderr_service" if log_type == "stderr" else "stdout_service"
|
216
|
+
console.print(f"[{style}]{service}[/] | {timestamp}: {message}")
|
257
217
|
|
258
218
|
if not has_new_data:
|
259
219
|
time.sleep(0.1)
|
260
220
|
|
261
221
|
except KeyboardInterrupt:
|
262
|
-
print(f"\n
|
222
|
+
console.print(f"\n[dim]已停止日志跟踪[/]")
|
263
223
|
finally:
|
264
|
-
#
|
224
|
+
# 关闭所有文件
|
265
225
|
for f in file_handlers.values():
|
266
226
|
f.close()
|
servly/service.py
CHANGED
@@ -17,7 +17,9 @@ class ServiceManager:
|
|
17
17
|
|
18
18
|
def __init__(self, config_path: str = "servly.yml", servly_dir: str = ".servly"):
|
19
19
|
self.config_path = config_path
|
20
|
-
|
20
|
+
# 修改为使用配置文件所在目录
|
21
|
+
config_dir = os.path.dirname(os.path.abspath(config_path))
|
22
|
+
self.servly_dir = Path(config_dir) / servly_dir
|
21
23
|
self.pid_dir = self.servly_dir / "pids"
|
22
24
|
self.log_dir = self.servly_dir / "logs"
|
23
25
|
self._ensure_dirs()
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: servly
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.3.0
|
4
4
|
Summary: simple process manager
|
5
5
|
Author-email: simpxx <simpxx@gmail.com>
|
6
6
|
License: MIT
|
@@ -11,6 +11,7 @@ Classifier: Operating System :: OS Independent
|
|
11
11
|
Requires-Python: >=3.12
|
12
12
|
Description-Content-Type: text/markdown
|
13
13
|
Requires-Dist: pyyaml>=6.0.2
|
14
|
+
Requires-Dist: rich>=14.0.0
|
14
15
|
|
15
16
|
# SERVLY
|
16
17
|
|
@@ -0,0 +1,9 @@
|
|
1
|
+
servly/__init__.py,sha256=HT8I94RyP6_gNoVJWOVSkeOEQIl3QnT60zCP2PJVon0,73
|
2
|
+
servly/cli.py,sha256=YmaKXHRZm77MJnMZ400EkuNayjlBw5UDfM5Y6T2z5x0,9583
|
3
|
+
servly/logs.py,sha256=pGU91fnWKHL5NprGrke7jNrLea23TXr3K3AF7J1KEQM,7802
|
4
|
+
servly/service.py,sha256=c4UrS1smK3NqEsERbCwjQUo1SKSQb1TaIbAzM1lrHeM,8253
|
5
|
+
servly-0.3.0.dist-info/METADATA,sha256=PD-b0BWdppEy69aREDOCDfWvpYf6G5elvHNF28MdPhY,2658
|
6
|
+
servly-0.3.0.dist-info/WHEEL,sha256=lTU6B6eIfYoiQJTZNc-fyaR6BpL6ehTzU3xGYxn2n8k,91
|
7
|
+
servly-0.3.0.dist-info/entry_points.txt,sha256=enHpE2d9DYdzfIWBcnIjuIkcNVk118xghL0AeTUL0Yg,43
|
8
|
+
servly-0.3.0.dist-info/top_level.txt,sha256=JLi7GX0KwWF-mh8i1-lVJ2q4SO1KfI2CNxQk_VNfNZE,7
|
9
|
+
servly-0.3.0.dist-info/RECORD,,
|
servly-0.2.0.dist-info/RECORD
DELETED
@@ -1,9 +0,0 @@
|
|
1
|
-
servly/__init__.py,sha256=HT8I94RyP6_gNoVJWOVSkeOEQIl3QnT60zCP2PJVon0,73
|
2
|
-
servly/cli.py,sha256=HeOOsnaJGMaiSWQ5Z-hkCLjxpmoUfES37hgfTHBuDMk,12142
|
3
|
-
servly/logs.py,sha256=HfC_EByTbgz6MpW8IpemEwpRPPE0xME46zUy2CyJvqU,9779
|
4
|
-
servly/service.py,sha256=5xhSD0HkfQw8xnY5J0GRxyFE8_CHBq5yfatbhL625PM,8123
|
5
|
-
servly-0.2.0.dist-info/METADATA,sha256=YICRthT2IW92tOXYTUmZN1cCGZ3LtWBhtLPIIZADKeA,2630
|
6
|
-
servly-0.2.0.dist-info/WHEEL,sha256=lTU6B6eIfYoiQJTZNc-fyaR6BpL6ehTzU3xGYxn2n8k,91
|
7
|
-
servly-0.2.0.dist-info/entry_points.txt,sha256=enHpE2d9DYdzfIWBcnIjuIkcNVk118xghL0AeTUL0Yg,43
|
8
|
-
servly-0.2.0.dist-info/top_level.txt,sha256=JLi7GX0KwWF-mh8i1-lVJ2q4SO1KfI2CNxQk_VNfNZE,7
|
9
|
-
servly-0.2.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|