Initial commit with project setup and basic structure established.
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
---
|
||||
name: java-bug-finder
|
||||
description: 自动登录远程 Ubuntu 服务器,根据错误码或服务名提取 Java Spring Boot 项目日志,并结合本地代码进行 Bug 诊断和修复建议。适用于 Java + Spring Boot 项目,支持 Docker Compose 和文件日志模式。
|
||||
---
|
||||
|
||||
# Java Bug Finder (Java 故障排查专家)
|
||||
|
||||
本 Skill 用于自动化排查部署在远程服务器上的 Java Spring Boot 应用问题。它能够连接服务器、抓取日志、解析堆栈、定位源码并提供修复方案。
|
||||
|
||||
## 触发场景
|
||||
- 用户要求排查特定服务的错误(如:“看看 order-service 为什么报错”)。
|
||||
- 用户提到特定的错误码(如:“错误码 5002 是怎么回事?”)。
|
||||
- 用户需要分析远程服务器上的 Java 异常堆栈。
|
||||
|
||||
## 工作流
|
||||
|
||||
### 1. 识别服务与服务器
|
||||
当收到指令后,首先读取 `references/service_inventory.md`:
|
||||
- **确认所有节点**: 确定服务部署的所有服务器 IP(如 192.168.3.200 和 192.168.3.230)。**Agent 必须对所有记录的节点执行日志提取**。
|
||||
- 如果用户提供了**服务名**(如 gateway),查找对应行。
|
||||
- 如果用户提供了**错误码**(如 12005),根据 `错误码起始` 判定所属服务(12000-12999 为 user)。
|
||||
- 确认目标 `Docker 服务名`(如 g3fo-gateway-service)或 `日志文件路径`(如 /data/logs/g3fo-user/info/info.log)。
|
||||
- 确认目标 `Server IP`、`Log Method` 以及相关的 `Path/YML`。
|
||||
|
||||
### 2. 获取 SSH 凭据
|
||||
- 检查环境中是否存在凭据(如环境变量)。
|
||||
- 如果没有,以交互方式询问用户:**SSH 用户名** 和 **密码**(或提醒用户配置私钥)。
|
||||
- **注意**:不要在对话中存储密码,仅用于当前会话运行脚本。
|
||||
|
||||
### 3. 提取远程日志
|
||||
使用 Python 脚本 `scripts/fetch_logs.py` 抓取日志。
|
||||
- **全节点提取**: 由于服务部署在多台服务器上(192.168.3.200, 192.168.3.230),**必须同时从所有相关服务器提取日志**,以确保不遗漏错误信息。
|
||||
- **日志级别选择**:
|
||||
- 默认抓取 `info` 级别。
|
||||
- 如果涉及报错排查,优先抓取 `error` 级别。
|
||||
- **路径拼装**: 严格遵循 `/data/logs/g3fo-{service}/{level}/{level}.log`。
|
||||
|
||||
命令模版示例 (需对两台机器分别运行):
|
||||
```bash
|
||||
# 对 Server A 运行
|
||||
python scripts/fetch_logs.py --host 192.168.3.200 --username root --password afe1234 --mode docker --service [SERVICE] --yml-path [YML]
|
||||
|
||||
# 对 Server B 运行
|
||||
python scripts/fetch_logs.py --host 192.168.3.230 --username root --password afe1234 --mode docker --service [SERVICE] --yml-path [YML]
|
||||
```
|
||||
|
||||
### 4. 异常分析与代码定位
|
||||
- **日志解析**:从获取的日志中提取 `Exception`、`Error` 或 `Caused by` 附近的堆栈信息(Stack Trace)。
|
||||
- **网络问题判断**:如果日志分析结论为网络连通性问题(如 `Connection refused`, `ConnectTimeoutException`, `UnknownHostException` 等):
|
||||
- **直接说明**:在报告中直接说明是网络连通性问题。
|
||||
- **连通性测试**:使用 `nc -zv {IP} {Port}` 或 `telnet {IP} {Port}` 到目标服务器进行测试。
|
||||
- **无需深度分析**:这种情况下不需要进一步分析源码或 Nacos 配置。
|
||||
- **源码检索**:提取堆栈中的全限定类名(如 `com.example.service.OrderService`)和行号。
|
||||
- **配置获取**:如需分析配置(如数据库地址、中间件端口等),使用 `scripts/fetch_configs.py`。
|
||||
- 示例:`python scripts/fetch_configs.py --data-id redis.yml`
|
||||
- **本地比对**:使用 `read_file` 或 `codebase_search` 查看本地对应的源码逻辑。
|
||||
|
||||
### 5. 输出诊断报告
|
||||
报告应包含:
|
||||
- **错误类型**:Java 异常类名。
|
||||
- **根本原因**:根据日志和代码逻辑分析得出的结论(如果是网络问题,请务必指出)。
|
||||
- **连通性测试结果**(如适用):展示服务器间网络测试的输出。
|
||||
- **关联代码**:引用本地源码的相关片段。
|
||||
- **修复方案**:具体的代码修改建议或配置调整建议。
|
||||
|
||||
|
||||
## 依赖要求
|
||||
- 本地 Python 环境。
|
||||
- 安装 `paramiko` 库:`pip install paramiko`。
|
||||
- 目标服务器支持 SSH 登录。
|
||||
|
||||
## 注意事项
|
||||
- 如果日志文件过大,建议优先使用脚本自带的 `grep` 过滤功能。
|
||||
- 始终确保本地代码分支与服务器部署版本尽量一致。
|
||||
@@ -0,0 +1,56 @@
|
||||
# Service Inventory (服务清单)
|
||||
|
||||
此文件定义了服务、错误码、端口以及远程服务器之间的映射关系。Agent 将参考此文件来决定连接哪台服务器以及如何提取日志。
|
||||
|
||||
## 服务详细信息表
|
||||
|
||||
> **提示**: 所有服务均部署在开发环境的以下两台服务器上:
|
||||
> - **Server A**: `192.168.3.200` (User: `root`, Pass: `afe1234`)
|
||||
> - **Server B**: `192.168.3.230` (User: `root`, Pass: `afe1234`)
|
||||
|
||||
| 服务名称 | 服务编号 | Redis DB | 错误码起始 | HTTP 端口 | Dubbo 端口 | Docker 服务名 | 日志路径模版 |
|
||||
| :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- |
|
||||
| `gateway` | 0 | 0 | 13000 | - | 28000 | `g3fo-gateway-service` | `/data/logs/g3fo-gateway/{level}/{level}.log` |
|
||||
| `base` | 1 | 1 | 11000 | 18001 | 28001 | `g3fo-base-service` | `/data/logs/g3fo-base/{level}/{level}.log` |
|
||||
| `user` | 2 | 2 | 12000 | 18002 | 28002 | `g3fo-user-service` | `/data/logs/g3fo-user/{level}/{level}.log` |
|
||||
| `admin` | 3 | 3 | 15000 | 18003 | 28003 | `g3fo-admin-service` | `/data/logs/g3fo-admin/{level}/{level}.log` |
|
||||
| `push` | 4 | 0 | nul | 18004 | - | `g3fo-push-service` | `/data/logs/g3fo-push/{level}/{level}.log` |
|
||||
| `product` | 6 | 6 | 16000 | 18006 | 28006 | `g3fo-product-service` | `/data/logs/g3fo-product/{level}/{level}.log` |
|
||||
| `trade` | 7 | 7 | 17000 | 18007 | 28007 | `g3fo-trade-service` | `/data/logs/g3fo-trade/{level}/{level}.log` |
|
||||
| `notification` | 8 | 8 | 18000 | 18008 | 28008 | `g3fo-notification-service` | `/data/logs/g3fo-notification/{level}/{level}.log` |
|
||||
| `dx` | 9 | 9 | 19000 | 18009 | 28009 | `g3fo-dx-service` | `/data/logs/g3fo-dx/{level}/{level}.log` |
|
||||
| `margin` | 10 | 10 | 20000 | 18010 | 28010 | `g3fo-margin-service` | `/data/logs/g3fo-margin/{level}/{level}.log` |
|
||||
| `utility` | - | - | 21000 | 18012 | 28012 | `g3fo-utility-service` | `/data/logs/g3fo-utility/{level}/{level}.log` |
|
||||
|
||||
## 日志获取规则
|
||||
- **开发环境 (Dev)**: 包含 `192.168.3.200` 和 `192.168.3.230`。
|
||||
- **Docker 模式**: 服务名遵循 `g3fo-{service}-service` 规则。
|
||||
- 例如: `gateway` -> `g3fo-gateway-service`
|
||||
- **文件模式**: 日志路径遵循 `/data/logs/g3fo-{service}/{level}/{level}.log` 规则。
|
||||
- **支持级别**: `debug`, `error`, `info`, `warn`
|
||||
|
||||
## 运维配置参考
|
||||
- **错误码范围**: 每个服务的错误码通常从“起始编号”开始,步长为 1000(例如 gateway 为 10000-10999)。
|
||||
- **执行逻辑**: 由于无法确定报错发生的具体机器,**Agent 在接收到指令后应自动依次连接 Server A 和 Server B 提取日志**,并汇总分析。
|
||||
|
||||
## 默认服务器配置
|
||||
- **SSH 目标**: `192.168.3.200`, `192.168.3.230`
|
||||
- **凭据**: `root / afe1234`
|
||||
- **Docker Compose 路径**: `/data/docker-compose/g3fo-docker-compose.yml` (默认路径)
|
||||
|
||||
## Nacos 配置获取 (HTTP 方式)
|
||||
|
||||
可以通过 Nacos 的 OpenAPI 获取配置信息,用于分析配置项(如数据库连接、Redis 地址等)。
|
||||
|
||||
- **基础 URL 示例**: `http://{IP}:8848/nacos/v1/cs/configs`
|
||||
- **默认参数**:
|
||||
- `tenant`: `dev`
|
||||
- `group`: `DEFAULT_GROUP`
|
||||
- **常用 Data ID 列表**:
|
||||
- 服务专用配置: `g3fo-user-dev.yml`, `g3fo-gateway-dev.yml`, `g3fo-notification-dev.yml` 等
|
||||
- 公共组件配置: `common.yml`, `dubbo.yml`, `mysql.yml`, `rocketmq.yml`, `redis.yml`
|
||||
|
||||
- **获取逻辑**:
|
||||
1. 依次尝试 Server A (`192.168.3.200`) 和 Server B (`192.168.3.230`)。
|
||||
2. 如果第一台机器获取成功,则不再尝试第二台。
|
||||
3. 如果两台都获取失败,则报错。
|
||||
@@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env python3
|
||||
import sys
|
||||
import argparse
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
|
||||
def fetch_config(ip, data_id, group, tenant):
|
||||
url = f"http://{ip}:8848/nacos/v1/cs/configs?dataId={data_id}&group={group}&tenant={tenant}"
|
||||
print(f"Trying to fetch config from {url}...")
|
||||
try:
|
||||
with urllib.request.urlopen(url, timeout=5) as response:
|
||||
if response.status == 200:
|
||||
return response.read().decode('utf-8')
|
||||
except urllib.error.URLError as e:
|
||||
print(f"Failed to connect to {ip}: {e}")
|
||||
except Exception as e:
|
||||
print(f"Error fetching config from {ip}: {e}")
|
||||
return None
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Fetch configuration from Nacos via HTTP.")
|
||||
parser.add_argument("--hosts", nargs='+', default=["192.168.3.200", "192.168.3.230"], help="List of Nacos server IPs")
|
||||
parser.add_argument("--data-id", required=True, help="Nacos Data ID (e.g., redis.yml)")
|
||||
parser.add_argument("--group", default="DEFAULT_GROUP", help="Nacos Group (default: DEFAULT_GROUP)")
|
||||
parser.add_argument("--tenant", default="dev", help="Nacos Tenant (default: dev)")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
config_content = None
|
||||
for ip in args.hosts:
|
||||
config_content = fetch_config(ip, args.data_id, args.group, args.tenant)
|
||||
if config_content:
|
||||
print(f"Successfully fetched config from {ip}")
|
||||
break
|
||||
|
||||
if config_content:
|
||||
print("-" * 40)
|
||||
print(f"CONTENT OF {args.data_id}:")
|
||||
print("-" * 40)
|
||||
print(config_content)
|
||||
print("-" * 40)
|
||||
else:
|
||||
print(f"Failed to fetch config '{args.data_id}' from all provided hosts.")
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,115 @@
|
||||
#!/usr/bin/env python3
|
||||
import sys
|
||||
import argparse
|
||||
import paramiko
|
||||
import os
|
||||
|
||||
def fetch_logs(args):
|
||||
ssh = paramiko.SSHClient()
|
||||
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
|
||||
try:
|
||||
# Connect to the server
|
||||
print(f"Connecting to {args.host}...")
|
||||
ssh.connect(
|
||||
hostname=args.host,
|
||||
username=args.username,
|
||||
password=args.password,
|
||||
timeout=10
|
||||
)
|
||||
|
||||
command = ""
|
||||
if args.mode == "docker":
|
||||
since_flag = f'--since "{args.since}"' if args.since else ""
|
||||
until_flag = f'--until "{args.until}"' if args.until else ""
|
||||
tail_flag = f"--tail={args.lines}" if not args.since else ""
|
||||
# docker-compose logs command
|
||||
command = f"docker-compose -f {args.yml_path} logs --no-log-prefix {since_flag} {until_flag} {tail_flag} {args.service}"
|
||||
else:
|
||||
# File mode
|
||||
if args.since or args.until:
|
||||
# Time-based filtering for file mode using awk
|
||||
since_val = args.since if args.since else ""
|
||||
until_val = args.until if args.until else ""
|
||||
|
||||
# Convert relative time (e.g., 10m) to absolute timestamp on server
|
||||
if since_val and any(since_val.endswith(suffix) for suffix in ['s', 'm', 'h', 'd']) and since_val[:-1].isdigit():
|
||||
unit_map = {'s': 'seconds', 'm': 'minutes', 'h': 'hours', 'd': 'days'}
|
||||
unit = unit_map[since_val[-1]]
|
||||
num = since_val[:-1]
|
||||
since_expr = f"\"$(date -d '{num} {unit} ago' '+%Y-%m-%d %H:%M:%S')\""
|
||||
elif since_val:
|
||||
since_expr = f"'{since_val}'"
|
||||
else:
|
||||
since_expr = "''"
|
||||
|
||||
until_expr = f"'{until_val}'" if until_val else "''"
|
||||
|
||||
# awk script that extracts timestamp and compares
|
||||
# It maintains 'in_range' state for multi-line logs (like stack traces)
|
||||
awk_script = (
|
||||
f"awk -v since={since_expr} -v until={until_expr} '"
|
||||
"BEGIN { in_range = (since == \"\"); } "
|
||||
"{ "
|
||||
" if (match($0, /^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}/)) { "
|
||||
" current_time = substr($0, RSTART, 19); "
|
||||
" in_range = (since == \"\" || current_time >= since) && (until == \"\" || current_time <= until); "
|
||||
" } "
|
||||
" if (in_range) print $0; "
|
||||
"}' " + args.log_path
|
||||
)
|
||||
command = awk_script
|
||||
else:
|
||||
# Default tail + grep for errors
|
||||
command = f"tail -n {args.lines} {args.log_path} | grep -E 'Exception|Error|Caused by' -C 50"
|
||||
|
||||
print(f"Executing command: {command}")
|
||||
stdin, stdout, stderr = ssh.exec_command(command)
|
||||
|
||||
output = stdout.read().decode('utf-8', errors='replace')
|
||||
error = stderr.read().decode('utf-8', errors='replace')
|
||||
|
||||
if error and not output:
|
||||
print(f"Error from server:\n{error}")
|
||||
return
|
||||
|
||||
if not output:
|
||||
print("No matching logs found.")
|
||||
return
|
||||
|
||||
# Print a header for the output
|
||||
print("-" * 40)
|
||||
print(f"LOGS FROM {args.host} ({args.service if args.mode == 'docker' else args.log_path})")
|
||||
print("-" * 40)
|
||||
print(output)
|
||||
print("-" * 40)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Failed to fetch logs: {str(e)}")
|
||||
finally:
|
||||
ssh.close()
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Fetch logs from a remote server via SSH.")
|
||||
parser.add_argument("--host", required=True, help="Server IP or hostname")
|
||||
parser.add_argument("--username", required=True, help="SSH username")
|
||||
parser.add_argument("--password", help="SSH password")
|
||||
parser.add_argument("--mode", choices=["docker", "file"], required=True, help="Log extraction mode")
|
||||
parser.add_argument("--service", help="Docker service name (for docker mode)")
|
||||
parser.add_argument("--yml-path", help="Path to docker-compose.yml (for docker mode)")
|
||||
parser.add_argument("--log-path", help="Path to log file (for file mode)")
|
||||
parser.add_argument("--lines", type=int, default=1000, help="Number of lines to fetch/analyze (default 1000)")
|
||||
parser.add_argument("--since", help="Filter logs since this time (e.g., '10m', '2026-01-16 10:10:00')")
|
||||
parser.add_argument("--until", help="Filter logs until this time (e.g., '2026-01-16 10:20:00')")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.mode == "docker" and not (args.service and args.yml_path):
|
||||
parser.error("docker mode requires --service and --yml-path")
|
||||
if args.mode == "file" and not args.log_path:
|
||||
parser.error("file mode requires --log-path")
|
||||
|
||||
fetch_logs(args)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user