perf: improves performance when loading large logs

This commit is contained in:
0xJacky 2022-09-02 18:39:05 +08:00
parent d63ad3e989
commit 92fb679b55
No known key found for this signature in database
GPG key ID: B6E4A6E4A561BAF0
4 changed files with 239 additions and 81 deletions

View file

@ -0,0 +1,16 @@
import http from '@/lib/http'
interface IData {
type: string
conf_name: string
server_idx: number
directive_idx: number
}
const nginx_log = {
page(page = 0, data: IData) {
return http.post('/nginx_log?page=' + page, data)
}
}
export default nginx_log

View file

@ -5,6 +5,8 @@ import {nextTick, onMounted, onUnmounted, reactive, ref, watch} from 'vue'
import ReconnectingWebSocket from 'reconnecting-websocket' import ReconnectingWebSocket from 'reconnecting-websocket'
import {useRoute, useRouter} from 'vue-router' import {useRoute, useRouter} from 'vue-router'
import FooterToolBar from '@/components/FooterToolbar/FooterToolBar.vue' import FooterToolBar from '@/components/FooterToolbar/FooterToolBar.vue'
import nginx_log from '@/api/nginx_log'
import {debounce} from 'lodash'
const {$gettext} = useGettext() const {$gettext} = useGettext()
@ -18,7 +20,6 @@ function logType() {
} }
const control = reactive({ const control = reactive({
fetch: 'new',
type: logType(), type: logType(),
conf_name: route.query.conf_name, conf_name: route.query.conf_name,
server_idx: parseInt(route.query.server_idx as string), server_idx: parseInt(route.query.server_idx as string),
@ -30,26 +31,51 @@ function openWs() {
websocket.onopen = () => { websocket.onopen = () => {
websocket.send(JSON.stringify({ websocket.send(JSON.stringify({
...control, ...control
fetch: 'new'
})) }))
} }
websocket.onmessage = (m: any) => { websocket.onmessage = (m: any) => {
addLog(m.data)
}
}
function addLog(data: string, prepend: boolean = false) {
const para = document.createElement('p') const para = document.createElement('p')
para.appendChild(document.createTextNode(m.data.trim())); para.appendChild(document.createTextNode(data.trim()))
(logContainer.value as any as Node).appendChild(para); const node = (logContainer.value as any as Node)
(logContainer.value as any as Element).scroll({ if (prepend) {
top: (logContainer.value as any as Element).scrollHeight, node.insertBefore(para, node.firstChild)
} else {
node.appendChild(para)
}
const elem = (logContainer.value as any as Element)
elem.scroll({
top: elem.scrollHeight,
left: 0, left: 0,
behavior: 'smooth'
}) })
} }
const page = ref(0)
function init() {
nginx_log.page(0, {
conf_name: (route.query.conf_name as string),
type: logType(),
server_idx: 0,
directive_idx: 0
}).then(r => {
page.value = r.page - 1
r.content.split('\n').forEach((v: string) => {
addLog(v)
})
})
} }
onMounted(() => { onMounted(() => {
init()
openWs() openWs()
}) })
@ -66,6 +92,8 @@ watch(auto_refresh, (value) => {
}) })
watch(route, () => { watch(route, () => {
init()
control.type = logType(); control.type = logType();
(logContainer.value as any as Element).innerHTML = '' (logContainer.value as any as Element).innerHTML = ''
@ -88,6 +116,31 @@ onUnmounted(() => {
}) })
const router = useRouter() const router = useRouter()
const loading = ref(false)
function on_scroll_log() {
if (!loading.value && page.value > 0) {
loading.value = true
const elem = (logContainer.value as any as Element)
if (elem.scrollTop / elem.scrollHeight < 0.333) {
nginx_log.page(page.value, {
conf_name: (route.query.conf_name as string),
type: logType(),
server_idx: 0,
directive_idx: 0
}).then(r => {
page.value = r.page - 1
r.content.split('\n').forEach((v: string) => {
addLog(v, true)
})
}).finally(() => {
loading.value = false
})
} else {
loading.value = false
}
}
}
</script> </script>
@ -97,20 +150,11 @@ const router = useRouter()
<a-form-item :label="$gettext('Auto Refresh')"> <a-form-item :label="$gettext('Auto Refresh')">
<a-switch v-model:checked="auto_refresh"/> <a-switch v-model:checked="auto_refresh"/>
</a-form-item> </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">
<translate>All logs</translate>
</a-select-option>
<a-select-option value="new">
<translate>New logs</translate>
</a-select-option>
</a-select>
</a-form-item>
</a-form> </a-form>
<a-card> <a-card>
<pre class="nginx-log-container" ref="logContainer"></pre> <pre class="nginx-log-container" ref="logContainer"
@scroll="debounce(on_scroll_log,100, null)()"></pre>
</a-card> </a-card>
</a-card> </a-card>
<footer-tool-bar v-if="control.type==='site'"> <footer-tool-bar v-if="control.type==='site'">
@ -125,6 +169,7 @@ const router = useRouter()
height: 60vh; height: 60vh;
overflow: scroll; overflow: scroll;
padding: 5px; padding: 5px;
margin-bottom: 0;
p { p {
font-size: 12px; font-size: 12px;

View file

@ -8,54 +8,119 @@ import (
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
"github.com/hpcloud/tail" "github.com/hpcloud/tail"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/spf13/cast"
"io" "io"
"log" "log"
"net/http" "net/http"
"os"
"path/filepath" "path/filepath"
) )
const (
PageSize = 128 * 1024
)
type controlStruct struct { type controlStruct struct {
Fetch string `json:"fetch"`
Type string `json:"type"` Type string `json:"type"`
ConfName string `json:"conf_name"` ConfName string `json:"conf_name"`
ServerIdx int `json:"server_idx"` ServerIdx int `json:"server_idx"`
DirectiveIdx int `json:"directive_idx"` DirectiveIdx int `json:"directive_idx"`
} }
func tailNginxLog(ws *websocket.Conn, controlChan chan controlStruct, errChan chan error) { type nginxLogPageResp struct {
defer func() { Content string `json:"content"`
if err := recover(); err != nil { Page int64 `json:"page"`
log.Println("tailNginxLog recovery", err) }
_ = ws.WriteMessage(websocket.TextMessage, err.([]byte))
func GetNginxLogPage(c *gin.Context) {
page := cast.ToInt64(c.Query("page"))
if page < 0 {
page = 0
}
var control controlStruct
if !BindAndValid(c, &control) {
return return
} }
}()
control := <-controlChan logPath, err := getLogPath(&control)
for { if err != nil {
var seek tail.SeekInfo log.Println("error GetNginxLogPage", err)
if control.Fetch != "all" { return
seek.Offset = 0
seek.Whence = io.SeekEnd
} }
var logPath string
f, err := os.Open(logPath)
if err != nil {
c.JSON(http.StatusOK, nginxLogPageResp{})
log.Println("error GetNginxLogPage open file", err)
return
}
logFileStat, err := os.Stat(logPath)
if err != nil {
c.JSON(http.StatusOK, nginxLogPageResp{})
log.Println("error GetNginxLogPage stat", err)
return
}
totalPage := logFileStat.Size() / PageSize
if logFileStat.Size()%PageSize > 0 {
totalPage++
}
var buf []byte
var offset int64
if page == 0 {
page = totalPage
}
buf = make([]byte, PageSize)
offset = (page - 1) * PageSize
// seek
_, err = f.Seek(offset, io.SeekStart)
if err != nil && err != io.EOF {
c.JSON(http.StatusOK, nginxLogPageResp{})
log.Println("error GetNginxLogPage seek", err)
return
}
n, err := f.Read(buf)
if err != nil && err != io.EOF {
c.JSON(http.StatusOK, nginxLogPageResp{})
log.Println("error GetNginxLogPage read buf", err)
return
}
c.JSON(http.StatusOK, nginxLogPageResp{
Page: page,
Content: string(buf[:n]),
})
}
func getLogPath(control *controlStruct) (logPath string, err error) {
switch control.Type { switch control.Type {
case "site": case "site":
var config *nginx.NgxConfig
path := filepath.Join(nginx.GetNginxConfPath("sites-available"), control.ConfName) path := filepath.Join(nginx.GetNginxConfPath("sites-available"), control.ConfName)
config, err := nginx.ParseNgxConfig(path) config, err = nginx.ParseNgxConfig(path)
if err != nil { if err != nil {
errChan <- errors.Wrap(err, "error parsing ngx config") err = errors.Wrap(err, "error parsing ngx config")
return return
} }
if control.ServerIdx >= len(config.Servers) { if control.ServerIdx >= len(config.Servers) {
errChan <- errors.New("serverIdx out of range") err = errors.New("serverIdx out of range")
return return
} }
if control.DirectiveIdx >= len(config.Servers[control.ServerIdx].Directives) { if control.DirectiveIdx >= len(config.Servers[control.ServerIdx].Directives) {
errChan <- errors.New("DirectiveIdx out of range") err = errors.New("DirectiveIdx out of range")
return return
} }
@ -65,12 +130,12 @@ func tailNginxLog(ws *websocket.Conn, controlChan chan controlStruct, errChan ch
case "access_log", "error_log": case "access_log", "error_log":
// ok // ok
default: default:
errChan <- errors.New("directive.Params neither access_log nor error_log") err = errors.New("directive.Params neither access_log nor error_log")
return return
} }
if directive.Params == "" { if directive.Params == "" {
errChan <- errors.New("directive.Params is empty") err = errors.New("directive.Params is empty")
return return
} }
@ -78,7 +143,7 @@ func tailNginxLog(ws *websocket.Conn, controlChan chan controlStruct, errChan ch
case "error": case "error":
if settings.NginxLogSettings.ErrorLogPath == "" { if settings.NginxLogSettings.ErrorLogPath == "" {
errChan <- errors.New("settings.NginxLogSettings.ErrorLogPath is empty," + err = errors.New("settings.NginxLogSettings.ErrorLogPath is empty," +
" see https://github.com/0xJacky/nginx-ui/wiki/Nginx-Log-Configuration for more information") " see https://github.com/0xJacky/nginx-ui/wiki/Nginx-Log-Configuration for more information")
return return
} }
@ -86,13 +151,44 @@ func tailNginxLog(ws *websocket.Conn, controlChan chan controlStruct, errChan ch
default: default:
if settings.NginxLogSettings.AccessLogPath == "" { if settings.NginxLogSettings.AccessLogPath == "" {
errChan <- errors.New("settings.NginxLogSettings.AccessLogPath is empty," + err = errors.New("settings.NginxLogSettings.AccessLogPath is empty," +
" see https://github.com/0xJacky/nginx-ui/wiki/Nginx-Log-Configuration for more information") " see https://github.com/0xJacky/nginx-ui/wiki/Nginx-Log-Configuration for more information")
return return
} }
logPath = settings.NginxLogSettings.AccessLogPath logPath = settings.NginxLogSettings.AccessLogPath
} }
return
}
func tailNginxLog(ws *websocket.Conn, controlChan chan controlStruct, errChan chan error) {
defer func() {
if err := recover(); err != nil {
log.Println("tailNginxLog recovery", err)
err = ws.WriteMessage(websocket.TextMessage, err.([]byte))
if err != nil {
log.Println(err)
return
}
return
}
}()
control := <-controlChan
for {
logPath, err := getLogPath(&control)
if err != nil {
errChan <- err
return
}
seek := tail.SeekInfo{
Offset: 0,
Whence: io.SeekEnd,
}
// Create a tail // Create a tail
t, err := tail.TailFile(logPath, tail.Config{Follow: true, t, err := tail.TailFile(logPath, tail.Config{Follow: true,
ReOpen: true, Location: &seek}) ReOpen: true, Location: &seek})

View file

@ -95,6 +95,7 @@ func InitRouter() *gin.Engine {
// Nginx log // Nginx log
g.GET("nginx_log", api.NginxLog) g.GET("nginx_log", api.NginxLog)
g.POST("nginx_log", api.GetNginxLogPage)
} }
} }