Add event log in Library > Errors

Signed-off-by: Michael Mayer <michael@liquidbytes.net>
This commit is contained in:
Michael Mayer
2020-07-01 17:16:24 +02:00
parent e466d51c3a
commit f576b000b7
7 changed files with 171 additions and 4 deletions

View File

@@ -292,6 +292,14 @@
</v-list-tile-title>
</v-list-tile-content>
</v-list-tile>
<v-list-tile to="/library/errors" @click="" class="nav-errors">
<v-list-tile-content>
<v-list-tile-title>
<translate key="Errors">Errors</translate>
</v-list-tile-title>
</v-list-tile-content>
</v-list-tile>
</v-list-group>
<template v-if="!config.disableSettings">

View File

@@ -0,0 +1,107 @@
<template>
<div class="p-page p-page-errors" v-infinite-scroll="loadMore" :infinite-scroll-disabled="scrollDisabled"
:infinite-scroll-distance="10" :infinite-scroll-listen-for-event="'scrollRefresh'">
<v-toolbar flat color="secondary">
<v-toolbar-title>
<translate>Event Log</translate>
</v-toolbar-title>
<v-spacer></v-spacer>
<v-btn icon @click.stop="reload" class="action-reload">
<v-icon>refresh</v-icon>
</v-btn>
</v-toolbar>
<v-list two-line>
<v-list-tile
v-for="(err, index) in errors" :key="index"
avatar
@click=""
>
<v-list-tile-avatar>
<v-icon>{{ err.Level }}</v-icon>
</v-list-tile-avatar>
<v-list-tile-content>
<v-list-tile-title>{{ err.Message }}</v-list-tile-title>
<v-list-tile-sub-title>{{ formatTime(err.Time) }}</v-list-tile-sub-title>
</v-list-tile-content>
</v-list-tile>
</v-list>
</div>
</template>
<script>
import {DateTime} from "luxon";
import Api from "common/api";
export default {
name: 'p-page-errors',
data() {
return {
errors: [],
dirty: false,
results: [],
scrollDisabled: false,
pageSize: 100,
offset: 0,
page: 0,
loading: false,
};
},
methods: {
reload() {
if (this.loading) {
return;
}
this.page = 0;
this.offset = 0;
this.scrollDisabled = false;
this.loadMore();
},
loadMore() {
if (this.loading || this.scrollDisabled) return;
this.loading = true;
this.scrollDisabled = true;
const count = this.dirty ? (this.page + 2) * this.pageSize : this.pageSize;
const offset = this.dirty ? 0 : this.offset;
const params = {
count: count,
offset: offset,
};
Api.get("errors", {params}).then((resp) => {
if (!resp.data) {
resp.data = [];
}
if (offset === 0) {
this.errors = resp.data;
} else {
this.errors = this.errors.concat(resp.data);
}
this.scrollDisabled = (resp.data.length < count);
if (!this.scrollDisabled) {
this.offset = offset + count;
this.page++;
}
}).finally(() => this.loading = false)
},
level(s) {
return s.substr(0, 4).toUpperCase();
},
formatTime(s) {
return DateTime.fromISO(s).toFormat("yyyy-LL-dd HH:mm:ss");
},
},
created() {
this.loadMore();
},
};
</script>

View File

@@ -33,6 +33,7 @@ import Albums from "pages/albums.vue";
import AlbumPhotos from "pages/album/photos.vue";
import Places from "pages/places.vue";
import Files from "pages/library/files.vue";
import Errors from "pages/library/errors.vue";
import Labels from "pages/labels.vue";
import People from "pages/people.vue";
import Library from "pages/library.vue";
@@ -200,6 +201,12 @@ export default [
meta: {title: "Hidden Files", auth: true},
props: {staticFilter: {hidden: true}},
},
{
name: "errors",
path: "/library/errors",
component: Errors,
meta: {title: c.name, auth: true},
},
{
name: "labels",
path: "/labels",

View File

@@ -2,9 +2,12 @@ package api
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"github.com/photoprism/photoprism/internal/acl"
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/pkg/txt"
)
@@ -28,3 +31,28 @@ var (
ErrNotFound = gin.H{"code": http.StatusNotFound, "error": "Not found"}
ErrInvalidPassword = gin.H{"code": http.StatusBadRequest, "error": "Invalid password, please try again"}
)
func GetErrors(router *gin.RouterGroup) {
router.GET("/errors", func(c *gin.Context) {
s := Auth(SessionID(c), acl.ResourceLogs, acl.ActionSearch)
if s.Invalid() {
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
return
}
limit := txt.Int(c.Query("count"))
offset := txt.Int(c.Query("offset"))
if resp, err := query.Errors(limit, offset); err != nil {
c.AbortWithStatusJSON(400, gin.H{"error": txt.UcFirst(err.Error())})
return
} else {
c.Header("X-Count", strconv.Itoa(len(resp)))
c.Header("X-Limit", strconv.Itoa(limit))
c.Header("X-Offset", strconv.Itoa(offset))
c.JSON(http.StatusOK, resp)
}
})
}

View File

@@ -9,12 +9,14 @@ import (
// Error represents an error message log.
type Error struct {
ID uint `gorm:"primary_key"`
ErrorTime time.Time `sql:"index"`
ErrorLevel string `gorm:"type:varbinary(32)"`
ErrorMessage string `gorm:"type:varbinary(2048)"`
ID uint `gorm:"primary_key" json:"ID" yaml:"ID"`
ErrorTime time.Time `sql:"index" json:"Time" yaml:"Time"`
ErrorLevel string `gorm:"type:varbinary(32)" json:"Level" yaml:"Level"`
ErrorMessage string `gorm:"type:varbinary(2048)" json:"Message" yaml:"Message"`
}
type Errors []Error
// SaveErrorMessages subscribes to error logs and stored them in the errors table.
func SaveErrorMessages() {
s := event.Subscribe("log.*")

14
internal/query/errors.go Normal file
View File

@@ -0,0 +1,14 @@
package query
import (
"github.com/photoprism/photoprism/internal/entity"
)
// Errors returns the error log.
func Errors(limit, offset int) (results entity.Errors, err error) {
stmt := Db()
err = stmt.Order("error_time DESC").Limit(limit).Offset(offset).Find(&results).Error
return results, err
}

View File

@@ -111,6 +111,7 @@ func registerRoutes(router *gin.Engine, conf *config.Config) {
api.GetSettings(v1)
api.SaveSettings(v1)
api.ChangePassword(v1)
api.GetErrors(v1)
api.GetSvg(v1)