mirror of
https://github.com/0xJacky/nginx-ui.git
synced 2025-05-12 10:55:51 +02:00
perf: improves performance when loading large logs
This commit is contained in:
parent
d63ad3e989
commit
92fb679b55
4 changed files with 239 additions and 81 deletions
16
frontend/src/api/nginx_log.ts
Normal file
16
frontend/src/api/nginx_log.ts
Normal 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
|
|
@ -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;
|
||||||
|
|
|
@ -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})
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue