Browse Source

feat: 优化日志配置

tuon 1 year ago
parent
commit
3faf229eb3

+ 1 - 1
package.json

@@ -31,7 +31,7 @@
     "less": "^4.1.3",
     "lodash": "^4.17.21",
     "npm": "^9.8.0",
-    "planning-tools": "^0.1.2",
+    "planning-tools": "^0.1.3",
     "query-string": "^8.1.0",
     "react": "^18.2.0",
     "react-dom": "^18.2.0",

+ 32 - 44
src/config/nginx_form.json

@@ -58,6 +58,12 @@
         ]
       }
     },
+    {
+      "key": "access_log",
+      "title": "访问日志",
+      "type": "access_log",
+      "required": false
+    },
     {
       "key": "proxy_settings",
       "title": "更多代理设置",
@@ -316,9 +322,11 @@
     {
       "key": "error_log",
       "title": "错误日志路径",
-      "type": "string",
-      "value": "/var/log/nginx/error.log notice",
-      "description": "eg. /var/log/nginx/error.log notice"
+      "type": "error_log",
+      "value": {
+        "path": "/var/log/nginx/error.log",
+        "level": "notice"
+      }
     },
     {
       "key": "pid",
@@ -401,42 +409,34 @@
               "placeholder": "日志格式名称,eg. main compression",
               "width": 200,
               "description": "日志格式名称,eg. main compression log1 log2",
-              "trim": false
+              "trim": false,
+              "required": true
             },
             {
               "type": "textarea",
               "key": "content",
               "value": "",
               "title": "日志格式",
+              "required": true,
               "rows": 4,
               "width": 400,
               "trim": false,
               "placeholder": "'$remote_addr - $remote_user [$time_local] \"$request\" '\n                      '$status $body_bytes_sent \"$http_referer\" '\n                      '\"$http_user_agent\" \"$http_x_forwarded_for\"'",
-              "description": "参数                      说明                                         示例\n$remote_addr             客户端地址                                    211.28.65.253\n$remote_user             客户端用户名称                                --\n$time_local              访问时间和时区                                18/Jul/2012:17:00:01 +0800\n$request                 请求的URI和HTTP协议                           \"GET /article-10000.html HTTP/1.1\"\n$http_host               请求地址,即浏览器中你输入的地址(IP或域名)     www.wang.com 192.168.100.100\n$status                  HTTP请求状态                                  200\n$upstream_status         upstream状态                                  200\n$body_bytes_sent         发送给客户端文件内容大小                        1547\n$http_referer            url跳转来源                                   https://www.baidu.com/\n$http_user_agent         用户终端浏览器等信息                           \"Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0; SV1; GTB7.0; .NET4.0C;\n$ssl_protocol            SSL协议版本                                   TLSv1\n$ssl_cipher              交换数据中的算法                               RC4-SHA\n$upstream_addr           后台upstream的地址,即真正提供服务的主机地址     10.10.10.100:80\n$request_time            整个请求的总时间                               0.205\n$upstream_response_time  请求过程中,upstream响应时间                    0.002\n"
+              "description": "参数                      说明                                         示例\n$remote_addr             客户端地址                                    211.28.65.253\n$remote_user             客户端用户名称                                --\n$time_local              访问时间和时区                                18/Jul/2012:17:00:01 +0800\n$request                 请求的URI和HTTP协议                           \"GET /article-10000.html HTTP/1.1\"\n$http_host               请求地址,即浏览器中你输入的地址(IP或域名)     www.wang.com 192.168.100.100\n$status                  HTTP请求状态                                  200\n$upstream_status         upstream状态                                  200\n$body_bytes_sent         发送给客户端文件内容大小                        1547\n$http_referer            url跳转来源                                   https://www.baidu.com/\n$http_user_agent         用户终端浏览器等信息                           \"Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0; SV1; GTB7.0; .NET4.0C;\n$ssl_protocol            SSL协议版本                                   TLSv1\n$ssl_cipher              交换数据中的算法                               RC4-SHA\n$upstream_addr           后台upstream的地址,即真正提供服务的主机地址     10.10.10.100:80\n$request_time            整个请求的总时间                               0.205\n$upstream_response_time  请求过程中,upstream响应时间                    0.002\neg.'$remote_addr - $remote_user [$time_local] \"$request\" '\n                      '$status $body_bytes_sent \"$http_referer\" '\n                      '\"$http_user_agent\" \"$http_x_forwarded_for\"'"
             }
           ]
         },
         {
           "key": "http.access_log",
           "title": "访问日志",
-          "type": "object",
-          "value": "/var/log/nginx/access.log  main",
-          "items": [
-            {
-              "key": "name",
-              "type": "string",
-              "title": "格式标签",
-              "placeholder": "日志格式配置的格式名称",
-              "description": "输入日志格式配置的格式名称",
-              "width": 200
-            },
-            {
-              "key": "path",
-              "type": "string",
-              "title": "日志路径",
-              "width": 250
-            }
-          ]
+          "type": "access_log",
+          "required": false
+        },
+        {
+          "key": "http.error_log",
+          "title": "错误日志",
+          "type": "error_log",
+          "required": false
         },
         {
           "key": "http.sendfile",
@@ -528,6 +528,7 @@
               "rows": 4,
               "width": 400,
               "trim": false,
+              "description": "eg. '$time_local|$remote_addr|$protocol|$status|$bytes_sent|$bytes_received|$session_time|$upstream_addr|$upstream_bytes_sent|$upstream_bytes_received|$upstream_connect_time'",
               "placeholder": "'$time_local|$remote_addr|$protocol|$status|$bytes_sent|$bytes_received|$session_time|$upstream_addr|$upstream_bytes_sent|$upstream_bytes_received|$upstream_connect_time'"
             }
           ]
@@ -535,34 +536,21 @@
         {
           "key": "stream.access_log",
           "title": "访问日志",
-          "type": "object",
+          "type": "access_log",
           "value": {
-            "name": "tcp_format",
+            "level": "tcp_format",
             "path": "/var/log/nginx/access_stream.log"
           },
-          "items": [
-            {
-              "key": "name",
-              "type": "string",
-              "title": "格式标签",
-              "placeholder": "日志格式配置的格式名称",
-              "description": "输入日志格式配置的格式名称",
-              "width": 200,
-              "value": "tcp_format"
-            },
-            {
-              "key": "path",
-              "type": "string",
-              "title": "日志路径",
-              "width": 250
-            }
-          ]
+          "stream": true
         },
         {
           "key": "stream.error_log",
           "title": "错误日志",
-          "type": "string",
-          "value": "/var/log/nginx/error_stream.log"
+          "type": "error_log",
+          "stream": true,
+          "value": {
+            "path": "/var/log/nginx/error_stream.log"
+          }
         }
       ]
     }

+ 0 - 1
src/pages/nginx/components/access/index.tsx

@@ -36,7 +36,6 @@ export const AccessInput = (props: AutoTypeInputProps)=>{
     }
 
     const renderLines = (values: any)=>{
-        console.log('renderLines',values)
         const results: string[] = [];
         if (Array.isArray(values?.allow)){
             values.allow.forEach((item: any)=>{

+ 1 - 0
src/pages/nginx/components/index.ts

@@ -9,3 +9,4 @@ import './error'
 import './cors'
 import './access'
 import './fastcgi'
+import './log'

+ 47 - 0
src/pages/nginx/components/log/config.json

@@ -0,0 +1,47 @@
+{
+  "error_log": {
+    "title": "error_log",
+    "type": "object",
+    "key": "error_log",
+    "items": [
+      {
+        "title": "path",
+        "key": "path",
+        "type": "string",
+        "placeholder": "default. logs/error.log",
+        "width": 250
+      },
+      {
+        "title": "level",
+        "key": "level",
+        "type": "select",
+        "option": ["debug","info","notice","warn","error","crit","alert","emerg"],
+        "width": 120
+      }
+    ],
+    "description": "日志级别,debug->emerg,级别从低到高。级别越低输出的错误日志就会越多。生产环境建议跳转到warn及以上。否则会有大量的IO请求,耗费系统资源。eg. /var/log/nginx/error.log notice"
+  },
+  "access_log": {
+    "title": "access_log",
+    "type": "object",
+    "key": "access_log",
+    "items": [
+      {
+        "title": "path",
+        "key": "path",
+        "type": "string",
+        "placeholder": "default. logs/access.log",
+        "width": 250
+      },
+      {
+        "title": "level",
+        "key": "level",
+        "type": "select",
+        "option": [],
+        "width": 120,
+        "placeholder": "日志格式"
+      }
+    ],
+    "description": "日志格式需要再http的“日志格式”中进行配置,配置后请先保存再进行选择"
+  }
+}

+ 7 - 0
src/pages/nginx/components/log/index.less

@@ -0,0 +1,7 @@
+.log-input{
+  display: flex;
+  flex-direction: row;
+  .object-input.ant-form-item{
+    border-bottom: solid 1px #d9d9d9;
+  }
+}

+ 148 - 0
src/pages/nginx/components/log/index.tsx

@@ -0,0 +1,148 @@
+/**
+ * @author tuonian
+ * log 标签
+ * @date 2023/8/2
+ */
+import {registerInput} from '../basic'
+import {
+    AutoTypeInputProps,
+    FormColumnType,
+    noRequired,
+    ObjectInput,
+    DataValidatorConfig,
+    AutoColumn
+} from "planning-tools";
+import {Button, Tooltip} from "antd";
+import CONFIG from './config.json'
+import './index.less'
+import {NgxModuleData} from "../input.ts";
+import {useEffect, useMemo, useRef, useState} from "react";
+import {QuestionCircleOutlined} from "@ant-design/icons";
+import {useAppSelector} from "../../../../store";
+import {HttpConfData} from "../../types.ts";
+
+type AccessLogData = {
+    path: string
+    level: string
+}
+
+type ErrorLogProps = AutoTypeInputProps & {
+    columns?: FormColumnType
+}
+export const ErrorLog = ({value, column, onChange, columns}: ErrorLogProps) => {
+
+    const [data,setData] = useState<AccessLogData>()
+    const isInit = useRef(false)
+
+    useEffect(()=>{
+        const initialData = value as NgxModuleData;
+        isInit.current = true;
+        if (initialData?.data){
+            setData(initialData.data)
+        }else if (!noRequired(column.required)){
+            onDataChange(column.value || {})
+        }else {
+            setData(undefined)
+        }
+    },[value, column])
+
+    const logColumn = useMemo(()=>{
+        if (columns){
+            return columns
+        }
+        const col: FormColumnType = {...CONFIG.error_log};
+        col.required = column.required;
+        return col
+    },[columns, column])
+
+    const onDataChange = (item?: AccessLogData)=>{
+        if (!item){
+            onChange?.(item)
+        }else {
+            const ngxData: NgxModuleData = {
+                data: item,
+                lines: [`${logColumn.key}   ${item.path} ${item.level};`]
+            }
+            onChange?.(ngxData)
+        }
+    }
+
+    return (<div className="log-input">
+        <ObjectInput column={logColumn}
+                     onChange={isInit.current ? onDataChange: undefined}
+                     value={data} />
+        <Tooltip title={logColumn.description}>
+            <Button type="link" icon={<QuestionCircleOutlined />}></Button>
+        </Tooltip>
+    </div>)
+}
+
+/**
+ * 如果是stream,添加配置项 stream=true
+ * @param props
+ * @constructor
+ */
+export const AccessLog = (props: AutoTypeInputProps) => {
+
+    const nginx = useAppSelector(state => state.nginx.current);
+
+    const options = useMemo(()=>{
+        if (!nginx?.httpData){
+            return []
+        }
+        try {
+            const httpData = JSON.parse(nginx?.httpData) as HttpConfData ?? {};
+            const list = (props.column as any).stream ? httpData["stream.log_format"] : httpData["http.log_format"] || []
+            return list.filter(item=>item.name && item.content)
+                .map(item=>item.name)
+        }catch (e) {
+            console.log('AccessLog parse httpData fail',e)
+        }
+        return []
+    },[nginx, props.column])
+
+    const col: FormColumnType = {...CONFIG.access_log};
+    col.required = props.column.required;
+    const level = col.items?.find(item=>item.key === 'level');
+    level && (level.option = options)
+    return (<ErrorLog {...props} columns={col} />)
+}
+
+
+registerInput('access_log', AccessLog)
+registerInput('error_log',ErrorLog)
+
+const validateLog = (value: any, config: AutoColumn) => {
+    if (noRequired(config.required) && !value){
+        return
+    }
+    if (!value || !value.data){
+        throw new Error('请配置日志')
+    }
+    const data = value.data as AccessLogData;
+    if (!data.path){
+        throw new Error("请配置日志路径")
+    }
+    if (!data.level ){
+        throw new Error('请选择日志级别')
+    }
+}
+
+const validateAccessLog = (value: any, config: AutoColumn) => {
+    if (noRequired(config.required) && !value){
+        return
+    }
+    if (!value || !value.data){
+        throw new Error('请配置日志')
+    }
+    const data = value.data as AccessLogData;
+    if (!data.path){
+        throw new Error("请配置日志路径")
+    }
+    if (!data.level ){
+        throw new Error('请选择日志格式')
+    }
+}
+
+DataValidatorConfig['error_log'] = validateLog;
+DataValidatorConfig['access_log'] = validateAccessLog;

+ 72 - 43
src/pages/nginx/http/components/HttpConfSync.tsx

@@ -3,7 +3,7 @@
  * @date 2023/7/6
  */
 import {INginx} from "../../../../models/nginx.ts";
-import {Button, Drawer, Input, Tooltip} from "antd";
+import {Button, Drawer, Form, Input, Space, Switch, Tooltip} from "antd";
 import {ChangeEvent, useEffect, useState} from "react";
 import './index.less'
 import {SyncOutlined} from "@ant-design/icons";
@@ -11,60 +11,89 @@ import {NginxApis} from "../../../../api/nginx.ts";
 import {useAppDispatch} from "../../../../store";
 import {NginxActions} from "../../../../store/slice/nginx.ts";
 import {Message} from "planning-tools";
+import {toNginxConf} from "../utils.ts";
 
 type IProps = {
-  nginx?: INginx
+    nginx?: INginx
+    getRealData: () => Promise<any>
 }
-export const HttpConfSync = ({nginx}: IProps)=>{
+export const HttpConfSync = ({nginx, getRealData}: IProps) => {
 
-  const [value,setValue] = useState<string>()
-  const [open,setOpen] = useState(false)
-  const [loading,setLoading] = useState(false)
+    const [value, setValue] = useState<string>()
+    const [open, setOpen] = useState(false)
+    const [loading, setLoading] = useState(false)
+    const [realtime, setRealtime] = useState(false)
 
-  const dispatch = useAppDispatch()
+    const dispatch = useAppDispatch()
+
+    const onSetRealtime = async (checked: boolean)=>{
+        if (!checked){
+            setValue(nginx?.httpConf);
+            setRealtime(false)
+            return
+        }
+        if (!nginx){
+            return
+        }
+        getRealData().then(data=>{
+            const conf = toNginxConf(nginx,data)
+            setValue(conf);
+            setRealtime(true)
+        }).catch(()=>{
+            Message.warning('渲染失败,请检查配置文件是否存在错误提示!')
+        })
+    }
 
 
-  useEffect(()=>{
-    setValue(nginx?.httpConf)
-  },[nginx])
+    useEffect(() => {
+        setValue(nginx?.httpConf)
+    }, [nginx])
 
-  const onChange = (evt: ChangeEvent<HTMLTextAreaElement>)=>{
-    setValue(evt.currentTarget.value)
-  }
+    const onChange = (evt: ChangeEvent<HTMLTextAreaElement>) => {
+        setValue(evt.currentTarget.value)
+    }
 
-  const onSubmitData = ()=>{
-    if (!nginx?.id){
-      return
+    const onSubmitData = () => {
+        if (!nginx?.id) {
+            return
+        }
+        setLoading(true);
+        NginxApis.refreshHttp({id: nginx.id, httpConf: value || '', httpData: nginx.httpData})
+            .then(() => {
+                dispatch(NginxActions.updateNginx({...nginx, httpConf: value}))
+                Message.success("success")
+            })
+            .finally(() => {
+                setLoading(false)
+            })
     }
-    setLoading(true);
-    NginxApis.refreshHttp({ id: nginx.id, httpConf: value || '', httpData: nginx.httpData})
-      .then(()=>{
-        dispatch(NginxActions.updateNginx({...nginx, httpConf: value}))
-        Message.success("success")
-      })
-      .finally(()=>{
-        setLoading(false)
-      })
-  }
 
-  if (!nginx?.id){
-    return null
-  }
+    if (!nginx?.id) {
+        return null
+    }
 
-  return (<>
-    <Button onClick={()=>setOpen(true)}>配置文件</Button>
-    <Drawer title="nginx.conf"
-            open={open}
-            destroyOnClose
-            onClose={()=>setOpen(false)}
-            width={750}
-            className="nginx-conf-drawer"
-            extra={<><Tooltip placement="leftBottom" title={`同步配置文件,注意:直接修改配置文件,将在界面操作“同步”功能后丢失`}>
-              <Button loading={loading} onClick={onSubmitData} icon={<SyncOutlined />}/>
-            </Tooltip></>}
-            >
-      <Input.TextArea onChange={onChange} value={value}  />
-    </Drawer>
+    return (<>
+        <Button onClick={() => setOpen(true)}>配置文件</Button>
+        <Drawer title="nginx.conf"
+                open={open}
+                destroyOnClose
+                onClose={() => setOpen(false)}
+                width={750}
+                className="nginx-conf-drawer"
+                extra={<>
+                <Space>
+                    <Form.Item tooltip="渲染实时数据" style={{marginBottom: 0}} label="实时">
+                        <Switch onChange={checked=>onSetRealtime(checked)}  checked={realtime}/>
+                    </Form.Item>
+                    <Tooltip placement="leftBottom"
+                             title={`上传配置文件,注意:直接修改配置文件,将在界面操作“同步”功能后丢失`}>
+                        <Button danger loading={loading} onClick={onSubmitData} icon={<SyncOutlined />}></Button>
+                    </Tooltip>
+                </Space>
+                </>}
+        >
+            <Input.TextArea onChange={onChange} value={value}/>
+        </Drawer>
     </>)
 
 

+ 7 - 6
src/pages/nginx/http/index.tsx

@@ -26,6 +26,10 @@ export const NginxHttp = () => {
 
   const dispatch = useAppDispatch()
 
+    const onGetRealData = async ()=>{
+        const values = await formRef.current?.onSyncSubmit(true);
+        return  { ...data, ...values };
+    }
   const onSubmitForm = async (sync?: boolean)=>{
     if (!nginx){
       return
@@ -43,7 +47,7 @@ export const NginxHttp = () => {
     postData.httpData = JSON.stringify(saveData);
     postData.httpConf = "";
     setLoading(false)
-    dispatch(NginxActions.updateNginx(postData))
+    dispatch(NginxActions.updateNginx({ httpData: postData.httpData }))
     NginxApis.updateOrAdd(postData)
         .then(()=>{
           Message.success('保存成功!')
@@ -69,10 +73,7 @@ export const NginxHttp = () => {
       httpData: JSON.stringify(nginxData)
     }
     setLoading(true);
-    dispatch(NginxActions.updateNginx({
-      ...nginx,
-      httpConf: nginxConf,
-    }))
+    dispatch(NginxActions.updateNginx(postData))
     NginxApis.refreshHttp(postData)
       .then(()=>{
         Message.success('sync success!');
@@ -108,7 +109,7 @@ export const NginxHttp = () => {
     <div className="page-header">
       <span>nginx.conf配置</span>
       <div style={{flex:1}} />
-      <HttpConfSync nginx={nginx} />
+      <HttpConfSync getRealData={onGetRealData} nginx={nginx} />
       <Button danger loading={loading} onClick={() => onSubmitForm(true)}>
         同步
         <Tooltip placement="left" title="同步配置文件到服务器,如果该server为禁用状态,将从服务器删除该配置文件">

+ 9 - 4
src/pages/nginx/http/utils.ts

@@ -12,7 +12,6 @@ const valueProcessor: {[key:string]: (value:any) => string |string[]} = {
     log_format: (values: any[]) => {
         return values.map(v=>`${v.name}     ${v.content}`)
     },
-    access_log: (values:any) => `${values.path}     ${values.name}`
 }
 
 /**
@@ -96,10 +95,14 @@ export const append2Lines = (prefix: string,lines: string[],key: string, value:
 }
 export const toNginxConf = ( nginx: INginx, data: any)=>{
     const nginxObj: any = toNginxObj(data)
+    console.log('toNginxConf', data, nginxObj)
     const lines: string[] = [];
     lines.push(`user  ${nginxObj.user || 'nginx'};`)
     lines.push(`worker_processes  ${nginxObj.worker_processes || 'auto'};`)
-    lines.push(`error_log  ${nginxObj.error_log};`)
+    if (isNgxModuleValue(nginxObj.error_log)){
+        const logData = nginxObj.error_log as NgxModuleData;
+        logData.lines?.forEach(line=>lines.push(line))
+    }
     lines.push(`pid        ${nginxObj.pid || '/var/run/nginx.pid'};`)
     lines.push(`events {`)
     if (nginxObj.events){
@@ -112,9 +115,11 @@ export const toNginxConf = ( nginx: INginx, data: any)=>{
     lines.push(`http {`)
     if (nginxObj.http){
         Object.keys(nginxObj.http).forEach(k=>{
-          const value = nginxObj.http[k] as never
+          const value: any = nginxObj.http[k] as never
           if (k === 'more'){
-            lines.push(value)
+              (value ?? '').split('\n').filter((item:string)=>!!item).forEach((line: string)=>{
+                  lines.push(`    ${line}`)
+              })
             return;
           }
           if (isNull(value)){

+ 18 - 0
src/pages/nginx/types.ts

@@ -3,3 +3,21 @@ export type NginxRouteParams = {
   sid?: string // server id
   locId?: string // location id
 }
+
+/**
+ * nginx.conf 的配置数据
+ */
+export type HttpConfData = {
+    /**
+     * http的日志格式
+     */
+    'http.log_format': {
+        name: string;
+        content?: string
+    }[]
+    'stream.log_format': {
+        name: string;
+        content?: string
+    }[]
+    [key:string]: any
+}

+ 2 - 1
src/pages/nginx/utils/index.ts

@@ -198,7 +198,8 @@ export const renderServer = (nginx: INginx,origin?: Partial<INginxServer>) => {
   }
 
   if (server.tmp_custom_config){
-    lines.push(server.tmp_custom_config)
+      server.tmp_custom_config.split('\n').filter(line=>!!line)
+          .forEach(line=>lines.push(`    ${line}`))
   }
 
   (server.locations || []).forEach(l=>{

+ 1 - 1
src/styles/index.less

@@ -8,7 +8,7 @@
   flex-direction: column;
   box-sizing: border-box;
   .ant-btn+.ant-btn{
-    margin-left: 10px;
+    margin-left: 3px;
   }
   .ant-alert{
     margin: 10px;

+ 4 - 4
yarn.lock

@@ -4618,10 +4618,10 @@ pify@^4.0.1:
   resolved "https://registry.npmmirror.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231"
   integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==
 
-planning-tools@^0.1.2:
-  version "0.1.2"
-  resolved "https://registry.yarnpkg.com/planning-tools/-/planning-tools-0.1.2.tgz#bda8c97b561602d7f7489af50f111fa526935055"
-  integrity sha512-fOzE/F6yv5m1e/Fd/gjyS3rcQFPTlJCQYX7qEoxqNEv2/RfrHit7GzJm0Y9qe5lhSXKheT8gW9ZAIEu1gRwXww==
+planning-tools@^0.1.3:
+  version "0.1.3"
+  resolved "https://registry.yarnpkg.com/planning-tools/-/planning-tools-0.1.3.tgz#3bcb273cf670d718cc4bb58c39b61e721e91f77c"
+  integrity sha512-lnAwAAiZj/JCX9dLcURzwm379Z/tcu+S5CcCBc9TVK3SYqBS+4HWKp4Llw8vUwwif0mZ1IIlDlreEssuXYNWWQ==
   dependencies:
     "@ant-design/icons" "^5.0.1"
     ace-builds "^1.16.0"