sysom1/docs/custom_diagnosis.md

435 lines
16 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 自定义诊断功能指南
## 1. 诊断流程
![Diagnosis Timing Diagram](https://foruda.gitee.com/images/1688007504907110931/8b0bb03a_643601.png "diagnosis_timing_diagram.png")
![Diagnosis Flow Chart](https://foruda.gitee.com/images/1688007571855894178/d2799f23_643601.png "diagnosis_flow_chart.png")
## 2. 新增一个诊断
### 2.1 新增诊断菜单
首先需要在指定路径下配置新诊断功能的菜单名称
- 未部署源码路径sysom/sysom_web/public/resource/diagnose/v1/locales.json
- (已部署)部署路径:/usr/local/sysom/web/resource/diagnose/v1/locales.json
![输入图片说明](https://foruda.gitee.com/images/1688007372195074930/12f397f5_643601.png "custom_diagnosis_add_menu_item (1).png")
修改配置文件后,刷新网页前端即可看到菜单栏已更新(如果没有更新可以清一下浏览器缓存)
![输入图片说明](https://foruda.gitee.com/images/1688007719900927393/66aa8f4a_643601.png "屏幕截图")
### 2.2 诊断页面配置
在对应目录下新增诊断页面配置,定义诊断所需的参数、诊断结果的数据格式以及诊断结果该如何渲染:
> **注意:诊断页面配置定义了诊断结果的渲染方式和数据格式,后处理脚本返回的结果要和此处匹配**
>
> - 每个面板pannel都有对应的数据格式要求详情请看 [docs/diagnosis_center.md](https://gitee.com/anolis/sysom/blob/master/docs/diagnosis_center.md)
- 未部署源码路径sysom/sysom_web/public/resource/diagnose/v1/custom/demo.json
- (已部署)部署路径:/usr/local/sysom/web/resource/diagnose/v1/custom/demo.json
![诊断页面配置示例](https://foruda.gitee.com/images/1688020894129797837/d5854e7b_643601.png "诊断页面配置示例")
修改配置文件后,刷新网页前端即可看到刚才定义的表单配置已经生效了(如果没有更新可以清一下浏览器缓存)
![诊断页面配置成功](https://foruda.gitee.com/images/1688021098820407834/bfd4b5f9_643601.png "诊断页面配置成功")
### 2.3 定义前处理脚本
在指定目录下定义诊断的前处理脚本(前处理脚本主要负责将输入的参数转换成实际要执行的诊断命令):
- 未部署源码路径sysom/sysom_server/sysom_diagnosis/service_scripts/demo_pre.py
- (已部署)部署路径:/usr/local/sysom/server/sysom_diagnosis/service_scripts/demo_pre.py
#### 2.3.1 前处理脚本的作用
前处理脚本主要的作用是将前端输入的参数转化成**一个或多个**在 Node 节点执行的命令,比如:
> 大多数诊断的前处理脚本只需要返回一个在指定Node执行的命令即可部分诊断可以返回对一组节点执行命令
```json
{
"service_name": "loadtask",
"instance": "127.0.0.1"
}
127.0.0.1 => sysak loadtask -s -g >> /dev/null && cat /var/log/sysak/loadtask/.tmplog
```
#### 2.3.2 基于Python编写前处理脚本建议
- 所有的前处理脚本都应该继承 `DiagnosisPreProcessor` 类,并实现对应的抽象方法:
```python
class DiagnosisPreProcessor(DiagnosisProcessorBase):
"""Pre-processor used to perform: <parms> -> <diagnosis cmd>"""
...
@abstractmethod
def get_diagnosis_cmds(self, params: dict) -> DiagnosisTask:
"""Convert params to diagnosis cmds
params => { "instance": "127.0.0.1", "service_name": "xxx", "time": 15 }
cmds => [
DiagnosisJob(instance="127.0.0.1", command="sysak memleak"),
DiagnosisJob(instance="192.168.0.1", command="sysak memleak")
]
Args:
params (dict): Diagnosis parameters
Returns:
DiagnosisTask: Diagnosis task
"""
```
- 诊断工具开发者可以在前处理脚本中复写 `get_diagnosis_cmds` 来实现从前端参数 `params` 到诊断任务 `DiagnosisTask` 的转换,其中 `DiagnosisTask` 的结构如下:
```python
class FileItem:
def __init__(self, name: str, remote_path: str, local_path: str = "") -> None:
self.name = name
self.remote_path = remote_path
self.local_path = local_path
class DiagnosisJob:
"""每个 DiagnosisJob 代表在一个指定的节点上执行一条命令"""
def __init__(self, instance: str, cmd: str, fetch_file_list: List[FileItem] = []) -> None:
# 目标节点实例
self.instance = instance
# 要在目标节点执行的命令
self.cmd = cmd
# 指定诊断结束后,需要从目标 instance 下载那些文件回来
self.fetch_file_list = fetch_file_list
class DiagnosisTask:
"""Diagnosis Task
每个 DiagnosisTask 代表一组需要执行的 DiagnosisJob并且可以使用
in_order 参数决定是同步还是异步执行多个 DiagnosisJob
Args:
jobs([DiagnosisJob]): Diagnosis jobs
in_order(bool): Whether to execute all jobs in order
False => Concurrent execution of all Jobs
True => Execute each job in turn
offline_mode(bool): Whether to execute in offline mode
offline_results([str]): offline mode diagnosis result
"""
def __init__(
self,
jobs: List[DiagnosisJob] = [],
in_order: bool = False,
offline_mode: bool = False,
offline_results: List[str] = [],
) -> None:
self.jobs = jobs
self.in_order = in_order
self.offline_mode = offline_mode
self.offline_results = offline_results
```
- 本指南的 demo 诊断的前处理脚本实现如下:
> **注意:后处理脚本中后处理类的名字必须为 PreProcessor**
```python
from .base import DiagnosisJob, DiagnosisPreProcessor, DiagnosisTask
class PreProcessor(DiagnosisPreProcessor):
"""Command diagnosis
在目标节点执行 sar 命令采集CPU占用率
Args:
DiagnosisPreProcessor (_type_): _description_
"""
def get_diagnosis_cmds(self, params: dict) -> DiagnosisTask:
command = params.get("command", "")
instance = params.get("instance", None)
if instance is None:
raise Exception("Missing required params: instance")
interval = params.get("interval", 1)
samples = params.get("samples", 10)
command = f"sar -u -t {interval} {samples} | grep all"
command += f' | grep "Average" > /tmp/demo.log && cat /tmp/demo.log'
return DiagnosisTask(
jobs=[
DiagnosisJob(instance=instance, cmd=command)
],
in_order=False,
)
```
#### 2.3.3 使用任意语言编写(不建议)
SysOM的诊断框架同样兼容旧版的语法可以使用任意语言编写前处理脚本只需要保证和诊断名称同名即可。
### 2.4 定义后处理脚本
#### 2.4.1 后处理脚本的作用
后处理脚本主要对命令执行的结果进行汇总处理,并且转换成前端可渲染的数据格式
```json
03:32:17 PM all 0.09 0.00 0.16 0.00 0.00 99.75
03:32:18 PM all 0.41 0.00 0.22 0.03 0.00 99.34
03:32:19 PM all 0.06 0.00 0.16 0.00 0.00 99.78
03:32:20 PM all 0.09 0.00 0.25 0.03 0.00 99.62
03:32:21 PM all 0.16 0.00 0.03 0.00 0.00 99.81
03:32:22 PM all 0.03 0.00 0.03 0.00 0.00 99.94
03:32:23 PM all 0.16 0.00 0.06 0.00 0.00 99.78
03:32:24 PM all 0.16 0.00 0.03 0.00 0.00 99.81
03:32:25 PM all 0.06 0.00 0.06 0.03 0.00 99.84
03:32:26 PM all 0.09 0.00 0.03 0.00 0.00 99.87
{
"code": 0,
"err_msg": "",
"result": {
"cpu_usage_set": {
"data": [
{
"time": "03:32:17 PM",
"user": 0.09,
"idle": 99.75
},
...
]
}
}
}
```
#### 2.4.2 基于Python编写后处理脚本
- 所有的前处理脚本都应该继承 `DiagnosisPreProcessor` 类,并实现对应的抽象方法:
```python
class PostProcessResult:
def __init__(self, code: int, result: str, err_msg: str = "") -> None:
# 后处理脚本执行状态码0表示成功其他值表示后处理脚本出错
self.code = code
# 后处理脚本转换后的结果,用于前端动态渲染
self.result = result
# 如果后处理脚本执行错误,可以在本字段放置错误的原因
self.err_msg = err_msg
class DiagnosisPostProcessor(DiagnosisProcessorBase):
"""Post-processor used to perform: <diagnosis result> -> <Front-end formatted data>
Args:
DiagnosisProcessorBase (_type_): _description_
"""
def __init__(self, service_name: str, **kwargs):
self.service_name = service_name
@abstractmethod
def parse_diagnosis_result(self, results: List[DiagnosisJobResult]) -> dict:
"""Parse diagnosis results to front-end formmated data
return:
code
{
"code": 0, => 后处理脚本执行状态码0表示成功其他值表示后处理脚本出错
"err_msg": "", => 如果后处理脚本执行错误,可以在本字段放置错误的原因
"result": "xxxx" => 后处理脚本转换后的结果,用于前端动态渲染
}
Args:
results (dict): Diagnosis result
"""
```
- 本指南的 demo 诊断的后处理脚本实现如下:
> **注意:后处理脚本中后处理类的名字必须为 PostProcessor **
```python
from typing import List
from .base import DiagnosisJobResult, DiagnosisPostProcessor, PostProcessResult
class PostProcessor(DiagnosisPostProcessor):
def parse_diagnosis_result(self, results: List[DiagnosisJobResult]) -> PostProcessResult:
postprocess_result = PostProcessResult(
code=0,
err_msg="",
result={}
)
log = results[0].stdout
datas = []
for line in log.strip().split("\n"):
values = line.strip().split(" ")
datas.append({
"time": values[0].strip(),
"user": float(values[2].strip()),
"idle": float(values[7].strip())
})
postprocess_result.result = {
"cpu_usage_set": {"data": datas}
}
return postprocess_result
```
## 3. 使用案例
### 3.1 使用离线模式
离线模式支持诊断的前处理脚本直接返回诊断的结果而不需要再某个纳管节点上执行命令进行采集典型的比如可以去读取prometheus中的监控数据并形成离线诊断结果本节展示基于离线模式实现的一个使用案例。
#### 3.1.1 echo 诊断功能说明
基于 SysOM 诊断框架提供的离线模式,开发一个简单的示例诊断,前处理脚本将传入的参数 `value` 作为离线诊断结果返回并且跳过诊断执行节点直接在后处理脚本中将前处理脚本返回的结果ECHO展示。
#### 3.1.2 前处理脚本
```python
from .base import DiagnosisPreProcessor, DiagnosisTask, DiagnosisJob
class PreProcessor(DiagnosisPreProcessor):
"""Command diagnosis
Just invoke command in target instance and get stdout result
Args:
DiagnosisPreProcessor (_type_): _description_
"""
def get_diagnosis_cmds(self, params: dict) -> DiagnosisTask:
value = params.get("value", "")
return DiagnosisTask(
# 离线模式不需要在节点上执行,所以此处 instance 和 cmd 指定为空即可
jobs=[DiagnosisJob(instance="", cmd="")],
# 开启离线模式
offline_mode=True,
# 返回离线诊断的结果(数组中的每一项,最后都会传递到 DiagnosisJobResult.stdout
offline_results=[
value
]
)
```
#### 3.1.3 后处理脚本
```python
from typing import List
from .base import DiagnosisJobResult, DiagnosisPostProcessor, PostProcessResult
class PostProcessor(DiagnosisPostProcessor):
def parse_diagnosis_result(self, results: List[DiagnosisJobResult]) -> PostProcessResult:
postprocess_result = PostProcessResult(
code=0,
err_msg="",
result={}
)
# 提取出上述返回的离线诊断结果并整理成前端可识别的格式进行echo展示
postprocess_result.result = {
"EchoResult": {"data": [{"key": "", "value": results[0].stdout}]}
}
return postprocess_result
```
### 3.2 诊断命令执行结束后需要拉取节点端的文件
部分场景下,在节点端执行完诊断命令后,可能诊断的结果较大,或者分布在多个文件,或者诊断的结果是二进制一类的文件,不是字符类型的结果,因此,在这种场景下需要支持诊断命令执行结束后,将一个或者多个文件拉取回来供后处理脚本分析处理,本节展示诊断命令执行结束后需要拉取节点端的文件的一个使用案例。
#### 3.2.1 get_release 诊断说明
get_release 诊断,在目标节点上执行 `uname -r` 获取内核版本,并且在执行结束后拉取节点端的 `/etc/os-release` 文件,后处理脚本根据终端输出的结果和拉取到的文件进行分析展示
#### 3.2.2 前处理脚本
```python
from .base import DiagnosisJob, DiagnosisPreProcessor, DiagnosisTask, FileItem
class PreProcessor(DiagnosisPreProcessor):
"""Get release info diagnosis
Just invoke command in target instance and get stdout result
Args:
DiagnosisPreProcessor (_type_): _description_
"""
def get_diagnosis_cmds(self, params: dict) -> DiagnosisTask:
# 从前端传递的参数中读取目标实例IP
instance = params.get("instance", "")
return DiagnosisTask(
jobs=[
# 在 instance 上执行 uname -r 获取内核版本
# 并在命令执行结束后,拉取节点端的 /etc/os-release 文件到本地
DiagnosisJob(instance=instance, cmd="uname -r", fetch_file_list=[
FileItem("os-release", remote_path="/etc/os-release")
])
]
)
```
#### 3.2.3 后处理脚本
```python
from typing import List
import json
from .base import DiagnosisJobResult, DiagnosisPostProcessor, PostProcessResult
class PostProcessor(DiagnosisPostProcessor):
def parse_diagnosis_result(self, results: List[DiagnosisJobResult]) -> PostProcessResult:
postprocess_result = PostProcessResult(
code=0,
err_msg="",
result={}
)
with open(results[0].file_list[0].local_path, "r") as f:
# 从本地读取 `/etc/os-release` 文件,提取 release 信息
release_info = f.read()
postprocess_result.result = {
"KernelVersion": {"data": [{"key": "", "value": results[0].stdout}]},
"ReleaseInfo": {"data": [{"key": "", "value": release_info}]}
}
print(json.dumps(postprocess_result.result, indent=4))
return postprocess_result
```
后处理脚本打印的信息如下
```shell
{
"KernelVersion": {
"data": [
{
"key": "",
"value": "5.10.134-14.an8.x86_64\n"
}
]
},
"ReleaseInfo": {
"data": [
{
"key": "",
"value": "NAME=\"Anolis OS\"\nVERSION=\"8.8\"\nID=\"anolis\"\nID_LIKE=\"rhel fedora centos\"\nVERSION_ID=\"8.8\"\nPLATFORM_ID=\"platform:an8\"\nPRETTY_NAME=\"Anolis OS 8.8\"\nANSI_COLOR=\"0;31\"\nHOME_URL=\"https://openanolis.cn/\"\n\n"
}
]
}
}
```