super-dev 2.0.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.
- super_dev/__init__.py +11 -0
- super_dev/analyzer/__init__.py +34 -0
- super_dev/analyzer/analyzer.py +440 -0
- super_dev/analyzer/detectors.py +511 -0
- super_dev/analyzer/models.py +285 -0
- super_dev/cli.py +3257 -0
- super_dev/config/__init__.py +11 -0
- super_dev/config/frontend.py +557 -0
- super_dev/config/manager.py +281 -0
- super_dev/creators/__init__.py +26 -0
- super_dev/creators/creator.py +134 -0
- super_dev/creators/document_generator.py +2473 -0
- super_dev/creators/frontend_builder.py +371 -0
- super_dev/creators/implementation_builder.py +789 -0
- super_dev/creators/prompt_generator.py +289 -0
- super_dev/creators/requirement_parser.py +354 -0
- super_dev/creators/spec_builder.py +195 -0
- super_dev/deployers/__init__.py +20 -0
- super_dev/deployers/cicd.py +1269 -0
- super_dev/deployers/delivery.py +229 -0
- super_dev/deployers/migration.py +1032 -0
- super_dev/design/__init__.py +74 -0
- super_dev/design/aesthetics.py +530 -0
- super_dev/design/charts.py +396 -0
- super_dev/design/codegen.py +379 -0
- super_dev/design/engine.py +528 -0
- super_dev/design/generator.py +395 -0
- super_dev/design/landing.py +422 -0
- super_dev/design/tech_stack.py +524 -0
- super_dev/design/tokens.py +269 -0
- super_dev/design/ux_guide.py +391 -0
- super_dev/exceptions.py +119 -0
- super_dev/experts/__init__.py +19 -0
- super_dev/experts/service.py +161 -0
- super_dev/integrations/__init__.py +7 -0
- super_dev/integrations/manager.py +264 -0
- super_dev/orchestrator/__init__.py +12 -0
- super_dev/orchestrator/engine.py +958 -0
- super_dev/orchestrator/experts.py +423 -0
- super_dev/orchestrator/knowledge.py +352 -0
- super_dev/orchestrator/quality.py +356 -0
- super_dev/reviewers/__init__.py +17 -0
- super_dev/reviewers/code_review.py +471 -0
- super_dev/reviewers/quality_gate.py +964 -0
- super_dev/reviewers/redteam.py +881 -0
- super_dev/skills/__init__.py +7 -0
- super_dev/skills/manager.py +307 -0
- super_dev/specs/__init__.py +44 -0
- super_dev/specs/generator.py +264 -0
- super_dev/specs/manager.py +428 -0
- super_dev/specs/models.py +348 -0
- super_dev/specs/validator.py +415 -0
- super_dev/utils/__init__.py +11 -0
- super_dev/utils/logger.py +133 -0
- super_dev/web/api.py +1402 -0
- super_dev-2.0.0.dist-info/METADATA +252 -0
- super_dev-2.0.0.dist-info/RECORD +61 -0
- super_dev-2.0.0.dist-info/WHEEL +5 -0
- super_dev-2.0.0.dist-info/entry_points.txt +2 -0
- super_dev-2.0.0.dist-info/licenses/LICENSE +21 -0
- super_dev-2.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Spec-Driven Development 验证器
|
|
3
|
+
|
|
4
|
+
开发:Excellent(11964948@qq.com)
|
|
5
|
+
功能:验证规格格式和结构
|
|
6
|
+
作用:检查 spec.md 和 change 的格式是否符合 OpenSpec 标准
|
|
7
|
+
创建时间:2025-12-30
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class ValidationError:
|
|
17
|
+
"""验证错误"""
|
|
18
|
+
file: str # 文件路径
|
|
19
|
+
line: int = 0 # 行号
|
|
20
|
+
column: int = 0 # 列号
|
|
21
|
+
message: str = "" # 错误消息
|
|
22
|
+
severity: str = "error" # 严重程度: error, warning, info
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class ValidationResult:
|
|
27
|
+
"""验证结果"""
|
|
28
|
+
is_valid: bool
|
|
29
|
+
errors: list[ValidationError] = field(default_factory=list)
|
|
30
|
+
warnings: list[ValidationError] = field(default_factory=list)
|
|
31
|
+
info: list[ValidationError] = field(default_factory=list)
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def total_issues(self) -> int:
|
|
35
|
+
"""总问题数"""
|
|
36
|
+
return len(self.errors) + len(self.warnings)
|
|
37
|
+
|
|
38
|
+
def to_summary(self) -> str:
|
|
39
|
+
"""生成摘要报告"""
|
|
40
|
+
lines = []
|
|
41
|
+
if self.is_valid:
|
|
42
|
+
lines.append("[通过] 规格格式验证通过")
|
|
43
|
+
else:
|
|
44
|
+
lines.append(f"[失败] 发现 {len(self.errors)} 个错误")
|
|
45
|
+
|
|
46
|
+
if self.warnings:
|
|
47
|
+
lines.append(f"[警告] {len(self.warnings)} 个警告")
|
|
48
|
+
|
|
49
|
+
return "\n".join(lines)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class SpecValidator:
|
|
53
|
+
"""规格验证器"""
|
|
54
|
+
|
|
55
|
+
# 规范需求关键词
|
|
56
|
+
REQUIREMENT_KEYWORDS = ["SHALL", "MUST", "SHOULD", "MAY"]
|
|
57
|
+
|
|
58
|
+
# Delta 类型
|
|
59
|
+
DELTA_TYPES = ["ADDED", "MODIFIED", "REMOVED"]
|
|
60
|
+
|
|
61
|
+
def __init__(self, project_dir: Path | str):
|
|
62
|
+
"""初始化验证器"""
|
|
63
|
+
self.project_dir = Path(project_dir).resolve()
|
|
64
|
+
self.specs_dir = self.project_dir / ".super-dev" / "specs"
|
|
65
|
+
self.changes_dir = self.project_dir / ".super-dev" / "changes"
|
|
66
|
+
|
|
67
|
+
def validate_change(self, change_id: str) -> ValidationResult:
|
|
68
|
+
"""验证变更"""
|
|
69
|
+
errors: list[ValidationError] = []
|
|
70
|
+
warnings: list[ValidationError] = []
|
|
71
|
+
info: list[ValidationError] = []
|
|
72
|
+
|
|
73
|
+
change_dir = self.changes_dir / change_id
|
|
74
|
+
if not change_dir.exists():
|
|
75
|
+
errors.append(ValidationError(
|
|
76
|
+
file=str(change_dir),
|
|
77
|
+
message=f"变更目录不存在: {change_id}"
|
|
78
|
+
))
|
|
79
|
+
return ValidationResult(is_valid=False, errors=errors)
|
|
80
|
+
|
|
81
|
+
# 验证 proposal.md
|
|
82
|
+
proposal_file = change_dir / "proposal.md"
|
|
83
|
+
if proposal_file.exists():
|
|
84
|
+
result = self._validate_proposal(proposal_file)
|
|
85
|
+
errors.extend(result.errors)
|
|
86
|
+
warnings.extend(result.warnings)
|
|
87
|
+
info.extend(result.info)
|
|
88
|
+
else:
|
|
89
|
+
warnings.append(ValidationError(
|
|
90
|
+
file=str(proposal_file),
|
|
91
|
+
message="缺少 proposal.md"
|
|
92
|
+
))
|
|
93
|
+
|
|
94
|
+
# 验证 tasks.md
|
|
95
|
+
tasks_file = change_dir / "tasks.md"
|
|
96
|
+
if tasks_file.exists():
|
|
97
|
+
result = self._validate_tasks(tasks_file)
|
|
98
|
+
errors.extend(result.errors)
|
|
99
|
+
warnings.extend(result.warnings)
|
|
100
|
+
info.extend(result.info)
|
|
101
|
+
else:
|
|
102
|
+
warnings.append(ValidationError(
|
|
103
|
+
file=str(tasks_file),
|
|
104
|
+
message="缺少 tasks.md"
|
|
105
|
+
))
|
|
106
|
+
|
|
107
|
+
# 验证 design.md (可选)
|
|
108
|
+
design_file = change_dir / "design.md"
|
|
109
|
+
if design_file.exists():
|
|
110
|
+
result = self._validate_design(design_file)
|
|
111
|
+
errors.extend(result.errors)
|
|
112
|
+
warnings.extend(result.warnings)
|
|
113
|
+
|
|
114
|
+
# 验证 specs/ 目录
|
|
115
|
+
specs_dir = change_dir / "specs"
|
|
116
|
+
if specs_dir.exists():
|
|
117
|
+
for spec_file in specs_dir.rglob("spec.md"):
|
|
118
|
+
result = self._validate_spec_delta(spec_file)
|
|
119
|
+
errors.extend(result.errors)
|
|
120
|
+
warnings.extend(result.warnings)
|
|
121
|
+
info.extend(result.info)
|
|
122
|
+
else:
|
|
123
|
+
warnings.append(ValidationError(
|
|
124
|
+
file=str(specs_dir),
|
|
125
|
+
message="缺少 specs/ 目录"
|
|
126
|
+
))
|
|
127
|
+
|
|
128
|
+
return ValidationResult(
|
|
129
|
+
is_valid=len(errors) == 0,
|
|
130
|
+
errors=errors,
|
|
131
|
+
warnings=warnings,
|
|
132
|
+
info=info
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
def validate_spec(self, spec_name: str) -> ValidationResult:
|
|
136
|
+
"""验证规范"""
|
|
137
|
+
errors: list[ValidationError] = []
|
|
138
|
+
warnings: list[ValidationError] = []
|
|
139
|
+
|
|
140
|
+
spec_file = self.specs_dir / spec_name / "spec.md"
|
|
141
|
+
if not spec_file.exists():
|
|
142
|
+
errors.append(ValidationError(
|
|
143
|
+
file=str(spec_file),
|
|
144
|
+
message=f"规范文件不存在: {spec_name}"
|
|
145
|
+
))
|
|
146
|
+
return ValidationResult(is_valid=False, errors=errors)
|
|
147
|
+
|
|
148
|
+
content = spec_file.read_text(encoding="utf-8")
|
|
149
|
+
lines = content.split("\n")
|
|
150
|
+
|
|
151
|
+
# 检查标题
|
|
152
|
+
has_title = False
|
|
153
|
+
has_purpose = False
|
|
154
|
+
has_requirements = False
|
|
155
|
+
has_req_keyword = False
|
|
156
|
+
|
|
157
|
+
for i, line in enumerate(lines, 1):
|
|
158
|
+
# 检查一级标题
|
|
159
|
+
if line.startswith("# ") and not has_title:
|
|
160
|
+
has_title = True
|
|
161
|
+
if not line[2:].strip():
|
|
162
|
+
errors.append(ValidationError(
|
|
163
|
+
file=str(spec_file),
|
|
164
|
+
line=i,
|
|
165
|
+
message="标题不能为空"
|
|
166
|
+
))
|
|
167
|
+
|
|
168
|
+
# 检查 Purpose 部分
|
|
169
|
+
if line.startswith("## Purpose"):
|
|
170
|
+
has_purpose = True
|
|
171
|
+
|
|
172
|
+
# 检查 Requirements 部分
|
|
173
|
+
if line.startswith("## Requirements"):
|
|
174
|
+
has_requirements = True
|
|
175
|
+
|
|
176
|
+
# 检查需求格式
|
|
177
|
+
if line.startswith("### Requirement:"):
|
|
178
|
+
has_req_keyword = True
|
|
179
|
+
if not line[16:].strip():
|
|
180
|
+
errors.append(ValidationError(
|
|
181
|
+
file=str(spec_file),
|
|
182
|
+
line=i,
|
|
183
|
+
message="需求名称不能为空"
|
|
184
|
+
))
|
|
185
|
+
|
|
186
|
+
# 检查需求关键词
|
|
187
|
+
if any(kw in line for kw in self.REQUIREMENT_KEYWORDS):
|
|
188
|
+
has_req_keyword = True
|
|
189
|
+
|
|
190
|
+
# 检查场景格式
|
|
191
|
+
if line.startswith("#### Scenario:"):
|
|
192
|
+
if not line[14:].strip():
|
|
193
|
+
warnings.append(ValidationError(
|
|
194
|
+
file=str(spec_file),
|
|
195
|
+
line=i,
|
|
196
|
+
severity="warning",
|
|
197
|
+
message="场景描述为空"
|
|
198
|
+
))
|
|
199
|
+
|
|
200
|
+
# 验证结果
|
|
201
|
+
if not has_title:
|
|
202
|
+
errors.append(ValidationError(
|
|
203
|
+
file=str(spec_file),
|
|
204
|
+
message="缺少一级标题"
|
|
205
|
+
))
|
|
206
|
+
|
|
207
|
+
if not has_purpose:
|
|
208
|
+
warnings.append(ValidationError(
|
|
209
|
+
file=str(spec_file),
|
|
210
|
+
severity="warning",
|
|
211
|
+
message="缺少 Purpose 部分"
|
|
212
|
+
))
|
|
213
|
+
|
|
214
|
+
if not has_requirements:
|
|
215
|
+
warnings.append(ValidationError(
|
|
216
|
+
file=str(spec_file),
|
|
217
|
+
severity="warning",
|
|
218
|
+
message="缺少 Requirements 部分"
|
|
219
|
+
))
|
|
220
|
+
|
|
221
|
+
if not has_req_keyword:
|
|
222
|
+
warnings.append(ValidationError(
|
|
223
|
+
file=str(spec_file),
|
|
224
|
+
severity="info",
|
|
225
|
+
message="建议使用 SHALL/MUST/SHOULD/MAY 关键词"
|
|
226
|
+
))
|
|
227
|
+
|
|
228
|
+
return ValidationResult(
|
|
229
|
+
is_valid=len(errors) == 0,
|
|
230
|
+
errors=errors,
|
|
231
|
+
warnings=warnings
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
def _validate_proposal(self, proposal_file: Path) -> ValidationResult:
|
|
235
|
+
"""验证提案文件"""
|
|
236
|
+
errors: list[ValidationError] = []
|
|
237
|
+
warnings: list[ValidationError] = []
|
|
238
|
+
|
|
239
|
+
content = proposal_file.read_text(encoding="utf-8")
|
|
240
|
+
|
|
241
|
+
# 检查必需的章节
|
|
242
|
+
has_title = False
|
|
243
|
+
has_description = False
|
|
244
|
+
|
|
245
|
+
for line in content.split("\n"):
|
|
246
|
+
if re.match(r"^##\s+", line):
|
|
247
|
+
if "description" in line.lower():
|
|
248
|
+
has_description = True
|
|
249
|
+
elif "title" in line.lower() or line.startswith("# "):
|
|
250
|
+
has_title = True
|
|
251
|
+
|
|
252
|
+
if not has_title:
|
|
253
|
+
warnings.append(ValidationError(
|
|
254
|
+
file=str(proposal_file),
|
|
255
|
+
severity="warning",
|
|
256
|
+
message="提案缺少标题"
|
|
257
|
+
))
|
|
258
|
+
|
|
259
|
+
if not has_description:
|
|
260
|
+
warnings.append(ValidationError(
|
|
261
|
+
file=str(proposal_file),
|
|
262
|
+
severity="warning",
|
|
263
|
+
message="提案缺少描述"
|
|
264
|
+
))
|
|
265
|
+
|
|
266
|
+
return ValidationResult(
|
|
267
|
+
is_valid=True,
|
|
268
|
+
errors=errors,
|
|
269
|
+
warnings=warnings
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
def _validate_tasks(self, tasks_file: Path) -> ValidationResult:
|
|
273
|
+
"""验证任务文件"""
|
|
274
|
+
errors: list[ValidationError] = []
|
|
275
|
+
warnings: list[ValidationError] = []
|
|
276
|
+
|
|
277
|
+
content = tasks_file.read_text(encoding="utf-8")
|
|
278
|
+
lines = content.split("\n")
|
|
279
|
+
|
|
280
|
+
task_pattern = re.compile(r'^-\s*\[([ x~_])\]\s*\*\*([\d.]+):\s*([^*]+)\*\*')
|
|
281
|
+
|
|
282
|
+
task_ids = []
|
|
283
|
+
for i, line in enumerate(lines, 1):
|
|
284
|
+
match = task_pattern.match(line)
|
|
285
|
+
if match:
|
|
286
|
+
status_char, task_id, title = match.groups()
|
|
287
|
+
task_ids.append(task_id)
|
|
288
|
+
|
|
289
|
+
# 检查任务标题
|
|
290
|
+
if not title.strip():
|
|
291
|
+
errors.append(ValidationError(
|
|
292
|
+
file=str(tasks_file),
|
|
293
|
+
line=i,
|
|
294
|
+
message="任务标题不能为空"
|
|
295
|
+
))
|
|
296
|
+
|
|
297
|
+
# 检查任务ID格式
|
|
298
|
+
for task_id in task_ids:
|
|
299
|
+
if not re.match(r"^\d+\.\d+$", task_id):
|
|
300
|
+
warnings.append(ValidationError(
|
|
301
|
+
file=str(tasks_file),
|
|
302
|
+
severity="warning",
|
|
303
|
+
message=f"任务ID格式建议为 '数字.数字' 格式: {task_id}"
|
|
304
|
+
))
|
|
305
|
+
|
|
306
|
+
if not task_ids:
|
|
307
|
+
warnings.append(ValidationError(
|
|
308
|
+
file=str(tasks_file),
|
|
309
|
+
severity="warning",
|
|
310
|
+
message="没有找到任何任务"
|
|
311
|
+
))
|
|
312
|
+
|
|
313
|
+
return ValidationResult(
|
|
314
|
+
is_valid=len(errors) == 0,
|
|
315
|
+
errors=errors,
|
|
316
|
+
warnings=warnings
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
def _validate_design(self, design_file: Path) -> ValidationResult:
|
|
320
|
+
"""验证设计文件"""
|
|
321
|
+
# design.md 是可选的,只做基本检查
|
|
322
|
+
content = design_file.read_text(encoding="utf-8")
|
|
323
|
+
if len(content) < 10:
|
|
324
|
+
return ValidationResult(
|
|
325
|
+
is_valid=False,
|
|
326
|
+
errors=[ValidationError(
|
|
327
|
+
file=str(design_file),
|
|
328
|
+
message="设计文件内容过少"
|
|
329
|
+
)]
|
|
330
|
+
)
|
|
331
|
+
return ValidationResult(is_valid=True)
|
|
332
|
+
|
|
333
|
+
def _validate_spec_delta(self, spec_file: Path) -> ValidationResult:
|
|
334
|
+
"""验证规范增量文件"""
|
|
335
|
+
errors: list[ValidationError] = []
|
|
336
|
+
warnings: list[ValidationError] = []
|
|
337
|
+
|
|
338
|
+
content = spec_file.read_text(encoding="utf-8")
|
|
339
|
+
lines = content.split("\n")
|
|
340
|
+
|
|
341
|
+
has_delta_header = False
|
|
342
|
+
current_delta_type = None
|
|
343
|
+
has_requirements = False
|
|
344
|
+
|
|
345
|
+
for i, line in enumerate(lines, 1):
|
|
346
|
+
# 检查 Delta 类型标题
|
|
347
|
+
if line.startswith("## "):
|
|
348
|
+
for delta_type in self.DELTA_TYPES:
|
|
349
|
+
if delta_type in line.upper():
|
|
350
|
+
has_delta_header = True
|
|
351
|
+
current_delta_type = delta_type
|
|
352
|
+
break
|
|
353
|
+
|
|
354
|
+
# 检查需求格式
|
|
355
|
+
if line.startswith("### Requirement:"):
|
|
356
|
+
if not current_delta_type:
|
|
357
|
+
errors.append(ValidationError(
|
|
358
|
+
file=str(spec_file),
|
|
359
|
+
line=i,
|
|
360
|
+
message="需求必须在 Delta 类型 (ADDED/MODIFIED/REMOVED) 下"
|
|
361
|
+
))
|
|
362
|
+
|
|
363
|
+
has_requirements = True
|
|
364
|
+
|
|
365
|
+
if not line[16:].strip():
|
|
366
|
+
errors.append(ValidationError(
|
|
367
|
+
file=str(spec_file),
|
|
368
|
+
line=i,
|
|
369
|
+
message="需求名称不能为空"
|
|
370
|
+
))
|
|
371
|
+
|
|
372
|
+
if not has_delta_header:
|
|
373
|
+
errors.append(ValidationError(
|
|
374
|
+
file=str(spec_file),
|
|
375
|
+
message="缺少 Delta 类型标题 (ADDED/MODIFIED/REMOVED)"
|
|
376
|
+
))
|
|
377
|
+
|
|
378
|
+
if not has_requirements:
|
|
379
|
+
warnings.append(ValidationError(
|
|
380
|
+
file=str(spec_file),
|
|
381
|
+
severity="warning",
|
|
382
|
+
message="没有找到任何需求"
|
|
383
|
+
))
|
|
384
|
+
|
|
385
|
+
return ValidationResult(
|
|
386
|
+
is_valid=len(errors) == 0,
|
|
387
|
+
errors=errors,
|
|
388
|
+
warnings=warnings
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
def validate_all(self) -> ValidationResult:
|
|
392
|
+
"""验证所有变更"""
|
|
393
|
+
all_errors: list[ValidationError] = []
|
|
394
|
+
all_warnings: list[ValidationError] = []
|
|
395
|
+
|
|
396
|
+
if not self.changes_dir.exists():
|
|
397
|
+
return ValidationResult(
|
|
398
|
+
is_valid=False,
|
|
399
|
+
errors=[ValidationError(
|
|
400
|
+
file=str(self.changes_dir),
|
|
401
|
+
message="changes 目录不存在,请先运行 'super-dev spec init'"
|
|
402
|
+
)]
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
for change_dir in self.changes_dir.iterdir():
|
|
406
|
+
if change_dir.is_dir() and not change_dir.name.startswith("."):
|
|
407
|
+
result = self.validate_change(change_dir.name)
|
|
408
|
+
all_errors.extend(result.errors)
|
|
409
|
+
all_warnings.extend(result.warnings)
|
|
410
|
+
|
|
411
|
+
return ValidationResult(
|
|
412
|
+
is_valid=len(all_errors) == 0,
|
|
413
|
+
errors=all_errors,
|
|
414
|
+
warnings=all_warnings
|
|
415
|
+
)
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""
|
|
2
|
+
开发:Excellent(11964948@qq.com)
|
|
3
|
+
功能:统一日志系统
|
|
4
|
+
作用:提供结构化、可配置的日志记录功能
|
|
5
|
+
创建时间:2025-01-29
|
|
6
|
+
最后修改:2025-01-29
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
import sys
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ColoredFormatter(logging.Formatter):
|
|
16
|
+
"""彩色日志格式化器"""
|
|
17
|
+
|
|
18
|
+
# ANSI颜色代码
|
|
19
|
+
COLORS = {
|
|
20
|
+
"DEBUG": "\033[36m", # 青色
|
|
21
|
+
"INFO": "\033[32m", # 绿色
|
|
22
|
+
"WARNING": "\033[33m", # 黄色
|
|
23
|
+
"ERROR": "\033[31m", # 红色
|
|
24
|
+
"CRITICAL": "\033[35m", # 紫色
|
|
25
|
+
}
|
|
26
|
+
RESET = "\033[0m"
|
|
27
|
+
|
|
28
|
+
def format(self, record):
|
|
29
|
+
# 添加颜色
|
|
30
|
+
log_color = self.COLORS.get(record.levelname, self.RESET)
|
|
31
|
+
record.levelname = f"{log_color}{record.levelname}{self.RESET}"
|
|
32
|
+
return super().format(record)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class StructuredFormatter(logging.Formatter):
|
|
36
|
+
"""结构化日志格式化器(JSON风格)"""
|
|
37
|
+
|
|
38
|
+
def format(self, record):
|
|
39
|
+
log_data = {
|
|
40
|
+
"timestamp": datetime.fromtimestamp(record.created).isoformat(),
|
|
41
|
+
"level": record.levelname,
|
|
42
|
+
"logger": record.name,
|
|
43
|
+
"message": record.getMessage(),
|
|
44
|
+
"module": record.module,
|
|
45
|
+
"function": record.funcName,
|
|
46
|
+
"line": record.lineno,
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
# 添加异常信息
|
|
50
|
+
if record.exc_info:
|
|
51
|
+
log_data["exception"] = self.formatException(record.exc_info)
|
|
52
|
+
|
|
53
|
+
# 添加额外字段
|
|
54
|
+
if hasattr(record, "extra_data"):
|
|
55
|
+
log_data["data"] = record.extra_data
|
|
56
|
+
|
|
57
|
+
return str(log_data)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def get_logger(
|
|
61
|
+
name: str,
|
|
62
|
+
level: str = "INFO",
|
|
63
|
+
log_file: Path | None = None,
|
|
64
|
+
use_colors: bool = True,
|
|
65
|
+
structured: bool = False,
|
|
66
|
+
) -> logging.Logger:
|
|
67
|
+
"""
|
|
68
|
+
获取配置好的日志记录器
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
name: 日志记录器名称
|
|
72
|
+
level: 日志级别 (DEBUG, INFO, WARNING, ERROR, CRITICAL)
|
|
73
|
+
log_file: 日志文件路径(可选)
|
|
74
|
+
use_colors: 是否使用彩色输出(仅终端)
|
|
75
|
+
structured: 是否使用结构化格式
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
配置好的日志记录器
|
|
79
|
+
"""
|
|
80
|
+
logger = logging.getLogger(name)
|
|
81
|
+
|
|
82
|
+
# 避免重复添加handler
|
|
83
|
+
if logger.handlers:
|
|
84
|
+
return logger
|
|
85
|
+
|
|
86
|
+
logger.setLevel(getattr(logging, level.upper(), logging.INFO))
|
|
87
|
+
logger.propagate = False # 不传播到父logger
|
|
88
|
+
|
|
89
|
+
# 控制台处理器
|
|
90
|
+
console_handler = logging.StreamHandler(sys.stdout)
|
|
91
|
+
|
|
92
|
+
if use_colors and not structured:
|
|
93
|
+
formatter: logging.Formatter = ColoredFormatter(
|
|
94
|
+
fmt="%(asctime)s - %(name)s - %(levelname)s - %(message)s", datefmt="%Y-%m-%d %H:%M:%S"
|
|
95
|
+
)
|
|
96
|
+
elif structured:
|
|
97
|
+
formatter = StructuredFormatter()
|
|
98
|
+
else:
|
|
99
|
+
formatter = logging.Formatter(
|
|
100
|
+
fmt="%(asctime)s - %(name)s - %(levelname)s - %(message)s", datefmt="%Y-%m-%d %H:%M:%S"
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
console_handler.setFormatter(formatter)
|
|
104
|
+
logger.addHandler(console_handler)
|
|
105
|
+
|
|
106
|
+
# 文件处理器(如果指定)
|
|
107
|
+
if log_file:
|
|
108
|
+
log_file.parent.mkdir(parents=True, exist_ok=True)
|
|
109
|
+
file_handler = logging.FileHandler(log_file, encoding="utf-8")
|
|
110
|
+
file_handler.setFormatter(
|
|
111
|
+
logging.Formatter(
|
|
112
|
+
fmt="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
|
113
|
+
datefmt="%Y-%m-%d %H:%M:%S",
|
|
114
|
+
)
|
|
115
|
+
)
|
|
116
|
+
logger.addHandler(file_handler)
|
|
117
|
+
|
|
118
|
+
return logger
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def log_with_extra(logger: logging.Logger, level: str, message: str, **extra_data):
|
|
122
|
+
"""
|
|
123
|
+
带额外数据的日志记录
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
logger: 日志记录器
|
|
127
|
+
level: 日志级别
|
|
128
|
+
message: 日志消息
|
|
129
|
+
**extra_data: 额外的结构化数据
|
|
130
|
+
"""
|
|
131
|
+
log_func = getattr(logger, level.lower(), logger.info)
|
|
132
|
+
extra = {"extra_data": extra_data} if extra_data else {}
|
|
133
|
+
log_func(message, extra=extra)
|