Browse Source

feat: read nginx log

0xJacky 2 years ago
parent
commit
6e3004b0bc

+ 4 - 0
app.example.ini

@@ -5,3 +5,7 @@ JwtSecret =
 Email =
 HTTPChallengePort = 9180
 StartCmd = login
+
+[nginx_log]
+AccessLogPath = /var/log/nginx/access.log
+ErrorLogPath = /var/log/nginx/error.log

+ 10 - 1
frontend/src/routes/index.ts

@@ -8,7 +8,8 @@ import {
     FileOutlined,
     HomeOutlined,
     InfoCircleOutlined,
-    UserOutlined
+    UserOutlined,
+    FileTextOutlined
 } from '@ant-design/icons-vue'
 
 const {$gettext} = gettext
@@ -87,6 +88,14 @@ export const routes = [
                     icon: CodeOutlined
                 }
             },
+            {
+                path: 'nginx_log',
+                name: () => $gettext('Nginx Log'),
+                component: () => import('@/views/nginx_log/NginxLog.vue'),
+                meta: {
+                    icon: FileTextOutlined
+                }
+            },
             {
                 path: 'about',
                 name: () => $gettext('About'),

+ 0 - 1
frontend/src/views/domain/ngx_conf/NgxConfigEditor.vue

@@ -20,7 +20,6 @@ const name = ref(route.params.name)
 function change_tls(r: any) {
     if (r) {
         // deep copy servers[0] to servers[1]
-        console.log(props.ngx_config)
         const server = JSON.parse(JSON.stringify(props.ngx_config.servers[0]))
 
         props.ngx_config.servers.push(server)

+ 93 - 0
frontend/src/views/nginx_log/NginxLog.vue

@@ -0,0 +1,93 @@
+<script setup lang="ts">
+import {useGettext} from 'vue3-gettext'
+import ws from '@/lib/websocket'
+import {nextTick, onMounted, reactive, ref, watch} from 'vue'
+import ReconnectingWebSocket from 'reconnecting-websocket'
+
+const {$gettext} = useGettext()
+
+const logContainer = ref(null)
+
+let websocket: ReconnectingWebSocket | WebSocket
+
+const control = reactive({
+    fetch: 'new'
+})
+
+function openWs() {
+    websocket = ws('/api/nginx_log')
+    websocket.send(JSON.stringify(control))
+    websocket.onopen = () => {
+        (logContainer.value as any as Element).innerHTML = ''
+    }
+    websocket.onmessage = (m: any) => {
+        const para = document.createElement('p')
+        para.appendChild(document.createTextNode(m.data.trim()));
+
+        (logContainer.value as any as Node).appendChild(para);
+
+        (logContainer.value as any as Element).scroll({
+            top: (logContainer.value as any as Element).scrollHeight,
+            left: 0,
+            behavior: 'smooth'
+        })
+    }
+}
+
+onMounted(() => {
+    openWs()
+})
+
+const auto_refresh = ref(true)
+
+watch(auto_refresh, (value) => {
+    if (value) {
+        openWs()
+    } else {
+        websocket.close()
+    }
+})
+
+watch(control, () => {
+    (logContainer.value as any as Element).innerHTML = ''
+    auto_refresh.value = true
+
+    nextTick(() => {
+        websocket.send(JSON.stringify(control))
+    })
+})
+
+</script>
+
+<template>
+    <a-card :title="$gettext('Nginx Log')" :bordered="false">
+        <a-form layout="vertical">
+            <a-form-item :label="$gettext('Auto Refresh')">
+                <a-switch v-model:checked="auto_refresh"/>
+            </a-form-item>
+            <a-form-item :label="$gettext('Fetch')">
+                <a-select v-model:value="control.fetch" style="max-width: 200px">
+                    <a-select-option value="all">All logs</a-select-option>
+                    <a-select-option value="new">New logs</a-select-option>
+                </a-select>
+            </a-form-item>
+        </a-form>
+
+        <a-card>
+            <pre class="nginx-log-container" ref="logContainer"></pre>
+        </a-card>
+    </a-card>
+</template>
+
+<style lang="less">
+.nginx-log-container {
+    height: 60vh;
+    overflow: scroll;
+    padding: 5px;
+
+    p {
+        font-size: 12px;
+        line-height: 1;
+    }
+}
+</style>

+ 3 - 0
go.mod

@@ -32,6 +32,7 @@ require (
 	github.com/gin-contrib/sse v0.1.0 // indirect
 	github.com/go-ole/go-ole v1.2.5 // indirect
 	github.com/golang/protobuf v1.3.4 // indirect
+	github.com/hpcloud/tail v1.0.0 // indirect
 	github.com/jinzhu/inflection v1.0.0 // indirect
 	github.com/jinzhu/now v1.1.2 // indirect
 	github.com/json-iterator/go v1.1.9 // indirect
@@ -49,6 +50,8 @@ require (
 	golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
 	golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c // indirect
 	golang.org/x/text v0.3.4 // indirect
+	gopkg.in/fsnotify.v1 v1.4.7 // indirect
 	gopkg.in/square/go-jose.v2 v2.5.1 // indirect
+	gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
 	gopkg.in/yaml.v2 v2.4.0 // indirect
 )

+ 3 - 0
go.sum

@@ -227,6 +227,7 @@ github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO
 github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
 github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
 github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
+github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
 github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
 github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
 github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df/go.mod h1:QMZY7/J/KSQEhKWFeDesPjMj+wCHReeknARU3wqlyN4=
@@ -692,6 +693,7 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8
 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
+gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
 gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
 gopkg.in/h2non/gock.v1 v1.0.15/go.mod h1:sX4zAkdYX1TRGJ2JY156cFspQn4yRWn6p9EMdODlynE=
 gopkg.in/ini.v1 v1.42.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
@@ -704,6 +706,7 @@ gopkg.in/ns1/ns1-go.v2 v2.4.4/go.mod h1:GMnKY+ZuoJ+lVLL+78uSTjwTz2jMazq6AfGKQOYh
 gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
 gopkg.in/square/go-jose.v2 v2.5.1 h1:7odma5RETjNHWJnR32wx8t+Io4djHE1PqxCFx3iiZ2w=
 gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
 gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
 gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
 gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

+ 8 - 8
server/api/domain.go

@@ -224,8 +224,8 @@ func DisableDomain(c *gin.Context) {
 	}
 
 	// delete auto cert record
-	cert := model.Cert{Domain: c.Param("name")}
-	err = cert.Remove()
+	certModel := model.Cert{Domain: c.Param("name")}
+	err = certModel.Remove()
 	if err != nil {
 		ErrHandler(c, err)
 		return
@@ -265,8 +265,8 @@ func DeleteDomain(c *gin.Context) {
 		return
 	}
 
-	cert := model.Cert{Domain: name}
-	_ = cert.Remove()
+	certModel := model.Cert{Domain: name}
+	_ = certModel.Remove()
 
 	err = os.Remove(availablePath)
 
@@ -284,19 +284,19 @@ func DeleteDomain(c *gin.Context) {
 func AddDomainToAutoCert(c *gin.Context) {
 	domain := c.Param("domain")
 
-	cert, err := model.FirstOrCreateCert(domain)
+	certModel, err := model.FirstOrCreateCert(domain)
 	if err != nil {
 		ErrHandler(c, err)
 		return
 	}
-	c.JSON(http.StatusOK, cert)
+	c.JSON(http.StatusOK, certModel)
 }
 
 func RemoveDomainFromAutoCert(c *gin.Context) {
-	cert := model.Cert{
+	certModel := model.Cert{
 		Domain: c.Param("domain"),
 	}
-	err := cert.Remove()
+	err := certModel.Remove()
 
 	if err != nil {
 		ErrHandler(c, err)

+ 124 - 0
server/api/nginx_log.go

@@ -0,0 +1,124 @@
+package api
+
+import (
+	"encoding/json"
+	"github.com/0xJacky/Nginx-UI/server/settings"
+	"github.com/gin-gonic/gin"
+	"github.com/gorilla/websocket"
+	"github.com/hpcloud/tail"
+	"github.com/pkg/errors"
+	"io"
+	"log"
+	"net/http"
+)
+
+type controlStruct struct {
+	Fetch string `json:"fetch"`
+}
+
+func tailNginxLog(ws *websocket.Conn, controlChan chan controlStruct, errChan chan error) {
+	defer func() {
+		if err := recover(); err != nil {
+			log.Println("tailNginxLog recovery", err)
+			return
+		}
+	}()
+
+	var control controlStruct
+
+	for {
+		var seek tail.SeekInfo
+		if control.Fetch != "all" {
+			seek.Offset = 0
+			seek.Whence = io.SeekEnd
+		}
+
+		// Create a tail
+		t, err := tail.TailFile(
+			settings.NginxLogSettings.AccessLogPath, tail.Config{Follow: true,
+				ReOpen: true, Location: &seek})
+
+		if err != nil {
+			errChan <- errors.Wrap(err, "error NginxAccessLog Tail")
+			return
+		}
+
+		for {
+			var next = false
+			select {
+			case line := <-t.Lines:
+				// Print the text of each received line
+				err = ws.WriteMessage(websocket.TextMessage, []byte(line.Text))
+
+				if err != nil {
+					errChan <- errors.Wrap(err, "error NginxAccessLog write message")
+					return
+				}
+			case control = <-controlChan:
+				log.Println("control change")
+				next = true
+				break
+			}
+			if next {
+				break
+			}
+		}
+	}
+}
+
+func handleLogControl(ws *websocket.Conn, controlChan chan controlStruct, errChan chan error) {
+	defer func() {
+		if err := recover(); err != nil {
+			log.Println("tailNginxLog recovery", err)
+			return
+		}
+	}()
+
+	for {
+		msgType, payload, err := ws.ReadMessage()
+		if err != nil {
+			errChan <- errors.Wrap(err, "error NginxAccessLog read message")
+			return
+		}
+
+		if msgType != websocket.TextMessage {
+			errChan <- errors.New("error NginxAccessLog message type")
+			return
+		}
+
+		var msg controlStruct
+		err = json.Unmarshal(payload, &msg)
+		if err != nil {
+			errChan <- errors.Wrap(err, "Error ReadWsAndWritePty json.Unmarshal")
+			return
+		}
+		controlChan <- msg
+	}
+}
+
+func NginxLog(c *gin.Context) {
+	var upGrader = websocket.Upgrader{
+		CheckOrigin: func(r *http.Request) bool {
+			return true
+		},
+	}
+	// upgrade http to websocket
+	ws, err := upGrader.Upgrade(c.Writer, c.Request, nil)
+	if err != nil {
+		log.Println("[Error] NginxAccessLog Upgrade", err)
+		return
+	}
+
+	defer ws.Close()
+
+	errChan := make(chan error, 1)
+	controlChan := make(chan controlStruct, 1)
+
+	go tailNginxLog(ws, controlChan, errChan)
+	go handleLogControl(ws, controlChan, errChan)
+
+	if err = <-errChan; err != nil {
+		log.Println(err)
+		return
+	}
+}

+ 3 - 0
server/router/routers.go

@@ -92,6 +92,9 @@ func InitRouter() *gin.Engine {
 
 			// pty
 			g.GET("pty", api.Pty)
+
+			// Nginx log
+			g.GET("nginx_log", api.NginxLog)
 		}
 	}
 

+ 12 - 1
server/settings/settings.go

@@ -28,6 +28,11 @@ type Server struct {
 	PageSize          int
 }
 
+type NginxLog struct {
+	AccessLogPath string
+	ErrorLogPath  string
+}
+
 var ServerSettings = &Server{
 	HttpPort:          "9000",
 	RunMode:           "debug",
@@ -38,10 +43,16 @@ var ServerSettings = &Server{
 	PageSize:          10,
 }
 
+var NginxLogSettings = &NginxLog{
+	AccessLogPath: "",
+	ErrorLogPath:  "",
+}
+
 var ConfPath string
 
 var sections = map[string]interface{}{
-	"server": ServerSettings,
+	"server":    ServerSettings,
+	"nginx_log": NginxLogSettings,
 }
 
 func init() {