aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2019-04-11 10:05:43 +0200
committerChocobozzz <me@florianbigard.com>2019-04-11 10:14:08 +0200
commit2c22613c2fe6f7f9c8c7de66e42be54b27cc7edd (patch)
tree4692ec7ef34bf418a47624704006e61200e174df
parentfd8710b897a67518d3a61c319e54b6a65ba443ef (diff)
downloadPeerTube-2c22613c2fe6f7f9c8c7de66e42be54b27cc7edd.tar.gz
PeerTube-2c22613c2fe6f7f9c8c7de66e42be54b27cc7edd.tar.zst
PeerTube-2c22613c2fe6f7f9c8c7de66e42be54b27cc7edd.zip
Add logs page in client
-rw-r--r--client/src/app/+admin/admin-routing.module.ts4
-rw-r--r--client/src/app/+admin/admin.component.html8
-rw-r--r--client/src/app/+admin/admin.component.ts4
-rw-r--r--client/src/app/+admin/admin.module.ts15
-rw-r--r--client/src/app/+admin/system/index.ts4
-rw-r--r--client/src/app/+admin/system/jobs/index.ts6
-rw-r--r--client/src/app/+admin/system/jobs/jobs.component.ts14
-rw-r--r--client/src/app/+admin/system/logs/index.ts2
-rw-r--r--client/src/app/+admin/system/logs/log-row.model.ts21
-rw-r--r--client/src/app/+admin/system/logs/logs.component.html31
-rw-r--r--client/src/app/+admin/system/logs/logs.component.scss48
-rw-r--r--client/src/app/+admin/system/logs/logs.component.ts111
-rw-r--r--client/src/app/+admin/system/logs/logs.service.ts33
-rw-r--r--client/src/app/+admin/system/system.component.html11
-rw-r--r--client/src/app/+admin/system/system.component.scss4
-rw-r--r--client/src/app/+admin/system/system.component.ts8
-rw-r--r--client/src/app/+admin/system/system.routes.ts44
-rw-r--r--client/src/app/shared/images/global-icon.component.ts3
-rw-r--r--client/src/assets/images/global/refresh.html12
-rw-r--r--server/controllers/api/server/logs.ts52
20 files changed, 386 insertions, 49 deletions
diff --git a/client/src/app/+admin/admin-routing.module.ts b/client/src/app/+admin/admin-routing.module.ts
index ca31ba585..215da1e4f 100644
--- a/client/src/app/+admin/admin-routing.module.ts
+++ b/client/src/app/+admin/admin-routing.module.ts
@@ -6,9 +6,9 @@ import { MetaGuard } from '@ngx-meta/core'
6 6
7import { AdminComponent } from './admin.component' 7import { AdminComponent } from './admin.component'
8import { FollowsRoutes } from './follows' 8import { FollowsRoutes } from './follows'
9import { JobsRoutes } from './jobs/job.routes'
10import { UsersRoutes } from './users' 9import { UsersRoutes } from './users'
11import { ModerationRoutes } from '@app/+admin/moderation/moderation.routes' 10import { ModerationRoutes } from '@app/+admin/moderation/moderation.routes'
11import { SystemRoutes } from '@app/+admin/system'
12 12
13const adminRoutes: Routes = [ 13const adminRoutes: Routes = [
14 { 14 {
@@ -25,7 +25,7 @@ const adminRoutes: Routes = [
25 ...FollowsRoutes, 25 ...FollowsRoutes,
26 ...UsersRoutes, 26 ...UsersRoutes,
27 ...ModerationRoutes, 27 ...ModerationRoutes,
28 ...JobsRoutes, 28 ...SystemRoutes,
29 ...ConfigRoutes 29 ...ConfigRoutes
30 ] 30 ]
31 } 31 }
diff --git a/client/src/app/+admin/admin.component.html b/client/src/app/+admin/admin.component.html
index 345fb9f5a..065d92509 100644
--- a/client/src/app/+admin/admin.component.html
+++ b/client/src/app/+admin/admin.component.html
@@ -12,13 +12,13 @@
12 Moderation 12 Moderation
13 </a> 13 </a>
14 14
15 <a i18n *ngIf="hasJobsRight()" routerLink="/admin/jobs" routerLinkActive="active" class="title-page">
16 Jobs
17 </a>
18
19 <a i18n *ngIf="hasConfigRight()" routerLink="/admin/config" routerLinkActive="active" class="title-page"> 15 <a i18n *ngIf="hasConfigRight()" routerLink="/admin/config" routerLinkActive="active" class="title-page">
20 Configuration 16 Configuration
21 </a> 17 </a>
18
19 <a i18n *ngIf="hasJobsRight() || hasLogsRight()" routerLink="/admin/system" routerLinkActive="active" class="title-page">
20 System
21 </a>
22 </div> 22 </div>
23 23
24 <div class="margin-content"> 24 <div class="margin-content">
diff --git a/client/src/app/+admin/admin.component.ts b/client/src/app/+admin/admin.component.ts
index b4b807c67..fc775a5a4 100644
--- a/client/src/app/+admin/admin.component.ts
+++ b/client/src/app/+admin/admin.component.ts
@@ -28,6 +28,10 @@ export class AdminComponent {
28 return this.auth.getUser().hasRight(UserRight.MANAGE_JOBS) 28 return this.auth.getUser().hasRight(UserRight.MANAGE_JOBS)
29 } 29 }
30 30
31 hasLogsRight () {
32 return this.auth.getUser().hasRight(UserRight.MANAGE_LOGS)
33 }
34
31 hasConfigRight () { 35 hasConfigRight () {
32 return this.auth.getUser().hasRight(UserRight.MANAGE_CONFIGURATION) 36 return this.auth.getUser().hasRight(UserRight.MANAGE_CONFIGURATION)
33 } 37 }
diff --git a/client/src/app/+admin/admin.module.ts b/client/src/app/+admin/admin.module.ts
index 282d59634..ae0af686b 100644
--- a/client/src/app/+admin/admin.module.ts
+++ b/client/src/app/+admin/admin.module.ts
@@ -7,20 +7,19 @@ import { AdminRoutingModule } from './admin-routing.module'
7import { AdminComponent } from './admin.component' 7import { AdminComponent } from './admin.component'
8import { FollowersListComponent, FollowingAddComponent, FollowsComponent, FollowService } from './follows' 8import { FollowersListComponent, FollowingAddComponent, FollowsComponent, FollowService } from './follows'
9import { FollowingListComponent } from './follows/following-list/following-list.component' 9import { FollowingListComponent } from './follows/following-list/following-list.component'
10import { JobsComponent } from './jobs/job.component' 10import { UserCreateComponent, UserListComponent, UserPasswordComponent, UsersComponent, UserUpdateComponent } from './users'
11import { JobsListComponent } from './jobs/jobs-list/jobs-list.component'
12import { JobService } from './jobs/shared/job.service'
13import { UserCreateComponent, UserListComponent, UsersComponent, UserUpdateComponent, UserPasswordComponent } from './users'
14import { 11import {
15 ModerationCommentModalComponent, 12 ModerationCommentModalComponent,
16 VideoAbuseListComponent, 13 VideoAbuseListComponent,
17 VideoBlacklistListComponent, 14 VideoAutoBlacklistListComponent,
18 VideoAutoBlacklistListComponent 15 VideoBlacklistListComponent
19} from './moderation' 16} from './moderation'
20import { ModerationComponent } from '@app/+admin/moderation/moderation.component' 17import { ModerationComponent } from '@app/+admin/moderation/moderation.component'
21import { RedundancyCheckboxComponent } from '@app/+admin/follows/shared/redundancy-checkbox.component' 18import { RedundancyCheckboxComponent } from '@app/+admin/follows/shared/redundancy-checkbox.component'
22import { RedundancyService } from '@app/+admin/follows/shared/redundancy.service' 19import { RedundancyService } from '@app/+admin/follows/shared/redundancy.service'
23import { InstanceAccountBlocklistComponent, InstanceServerBlocklistComponent } from '@app/+admin/moderation/instance-blocklist' 20import { InstanceAccountBlocklistComponent, InstanceServerBlocklistComponent } from '@app/+admin/moderation/instance-blocklist'
21import { JobsComponent } from '@app/+admin/system/jobs/jobs.component'
22import { JobService, LogsComponent, LogsService, SystemComponent } from '@app/+admin/system'
24 23
25@NgModule({ 24@NgModule({
26 imports: [ 25 imports: [
@@ -52,8 +51,9 @@ import { InstanceAccountBlocklistComponent, InstanceServerBlocklistComponent } f
52 InstanceServerBlocklistComponent, 51 InstanceServerBlocklistComponent,
53 InstanceAccountBlocklistComponent, 52 InstanceAccountBlocklistComponent,
54 53
54 SystemComponent,
55 JobsComponent, 55 JobsComponent,
56 JobsListComponent, 56 LogsComponent,
57 57
58 ConfigComponent, 58 ConfigComponent,
59 EditCustomConfigComponent 59 EditCustomConfigComponent
@@ -67,6 +67,7 @@ import { InstanceAccountBlocklistComponent, InstanceServerBlocklistComponent } f
67 FollowService, 67 FollowService,
68 RedundancyService, 68 RedundancyService,
69 JobService, 69 JobService,
70 LogsService,
70 ConfigService 71 ConfigService
71 ] 72 ]
72}) 73})
diff --git a/client/src/app/+admin/system/index.ts b/client/src/app/+admin/system/index.ts
new file mode 100644
index 000000000..226d999d2
--- /dev/null
+++ b/client/src/app/+admin/system/index.ts
@@ -0,0 +1,4 @@
1export * from './jobs'
2export * from './logs'
3export * from './system.component'
4export * from './system.routes'
diff --git a/client/src/app/+admin/system/jobs/index.ts b/client/src/app/+admin/system/jobs/index.ts
index c0e0cc95d..486a745e4 100644
--- a/client/src/app/+admin/system/jobs/index.ts
+++ b/client/src/app/+admin/system/jobs/index.ts
@@ -1,4 +1,2 @@
1export * from './shared' 1export * from './job.service'
2export * from './jobs-list' 2export * from './jobs.component'
3export * from './job.routes'
4export * from './job.component'
diff --git a/client/src/app/+admin/system/jobs/jobs.component.ts b/client/src/app/+admin/system/jobs/jobs.component.ts
index b265e1dd6..ebfb52779 100644
--- a/client/src/app/+admin/system/jobs/jobs.component.ts
+++ b/client/src/app/+admin/system/jobs/jobs.component.ts
@@ -5,15 +5,15 @@ import { SortMeta } from 'primeng/primeng'
5import { Job } from '../../../../../../shared/index' 5import { Job } from '../../../../../../shared/index'
6import { JobState } from '../../../../../../shared/models' 6import { JobState } from '../../../../../../shared/models'
7import { RestPagination, RestTable } from '../../../shared' 7import { RestPagination, RestTable } from '../../../shared'
8import { JobService } from '../shared' 8import { JobService } from './job.service'
9import { I18n } from '@ngx-translate/i18n-polyfill' 9import { I18n } from '@ngx-translate/i18n-polyfill'
10 10
11@Component({ 11@Component({
12 selector: 'my-jobs-list', 12 selector: 'my-jobs',
13 templateUrl: './jobs-list.component.html', 13 templateUrl: './jobs.component.html',
14 styleUrls: [ './jobs-list.component.scss' ] 14 styleUrls: [ './jobs.component.scss' ]
15}) 15})
16export class JobsListComponent extends RestTable implements OnInit { 16export class JobsComponent extends RestTable implements OnInit {
17 private static JOB_STATE_LOCAL_STORAGE_STATE = 'jobs-list-state' 17 private static JOB_STATE_LOCAL_STORAGE_STATE = 'jobs-list-state'
18 18
19 jobState: JobState = 'waiting' 19 jobState: JobState = 'waiting'
@@ -58,12 +58,12 @@ export class JobsListComponent extends RestTable implements OnInit {
58 } 58 }
59 59
60 private loadJobState () { 60 private loadJobState () {
61 const result = peertubeLocalStorage.getItem(JobsListComponent.JOB_STATE_LOCAL_STORAGE_STATE) 61 const result = peertubeLocalStorage.getItem(JobsComponent.JOB_STATE_LOCAL_STORAGE_STATE)
62 62
63 if (result) this.jobState = result as JobState 63 if (result) this.jobState = result as JobState
64 } 64 }
65 65
66 private saveJobState () { 66 private saveJobState () {
67 peertubeLocalStorage.setItem(JobsListComponent.JOB_STATE_LOCAL_STORAGE_STATE, this.jobState) 67 peertubeLocalStorage.setItem(JobsComponent.JOB_STATE_LOCAL_STORAGE_STATE, this.jobState)
68 } 68 }
69} 69}
diff --git a/client/src/app/+admin/system/logs/index.ts b/client/src/app/+admin/system/logs/index.ts
new file mode 100644
index 000000000..7b56d4237
--- /dev/null
+++ b/client/src/app/+admin/system/logs/index.ts
@@ -0,0 +1,2 @@
1export * from './logs.component'
2export * from './logs.service'
diff --git a/client/src/app/+admin/system/logs/log-row.model.ts b/client/src/app/+admin/system/logs/log-row.model.ts
new file mode 100644
index 000000000..9bc7dafdd
--- /dev/null
+++ b/client/src/app/+admin/system/logs/log-row.model.ts
@@ -0,0 +1,21 @@
1import { LogLevel } from '@shared/models/server/log-level.type'
2import omit from 'lodash-es/omit'
3
4export class LogRow {
5 date: Date
6 localeDate: string
7 level: LogLevel
8 message: string
9 meta: string
10
11 constructor (row: any) {
12 this.date = new Date(row.timestamp)
13 this.localeDate = this.date.toLocaleString()
14 this.level = row.level
15 this.message = row.message
16
17 const metaObj = omit(row, 'timestamp', 'level', 'message', 'label')
18
19 if (Object.keys(metaObj).length !== 0) this.meta = JSON.stringify(metaObj, undefined, 2)
20 }
21}
diff --git a/client/src/app/+admin/system/logs/logs.component.html b/client/src/app/+admin/system/logs/logs.component.html
new file mode 100644
index 000000000..45723a655
--- /dev/null
+++ b/client/src/app/+admin/system/logs/logs.component.html
@@ -0,0 +1,31 @@
1<div class="header">
2 <div class="peertube-select-container">
3 <select [(ngModel)]="startDate" (ngModelChange)="refresh()">
4 <option *ngFor="let timeChoice of timeChoices" [value]="timeChoice.id">{{ timeChoice.label }}</option>
5 </select>
6 </div>
7
8 <div class="peertube-select-container">
9 <select [(ngModel)]="level" (ngModelChange)="refresh()">
10 <option *ngFor="let levelChoice of levelChoices" [value]="levelChoice.id">{{ levelChoice.label }}</option>
11 </select>
12 </div>
13
14 <my-button i18n-label label="Refresh" icon="refresh" (click)="refresh()"></my-button>
15</div>
16
17<div class="logs">
18 <div *ngIf="loading">Loading...</div>
19
20 <div #logsElement>
21 <div *ngFor="let log of logs" class="log-row" [ngClass]="{ error: log.level === 'error', warn: log.level === 'warn' }">
22 <span class="log-level">{{ log.level }}</span>
23
24 <span class="log-date">[{{ log.localeDate }}]</span>
25
26 {{ log.message }}
27
28 {{ log.meta }}
29 </div>
30 </div>
31</div>
diff --git a/client/src/app/+admin/system/logs/logs.component.scss b/client/src/app/+admin/system/logs/logs.component.scss
new file mode 100644
index 000000000..ab00fb5ae
--- /dev/null
+++ b/client/src/app/+admin/system/logs/logs.component.scss
@@ -0,0 +1,48 @@
1@import '_variables';
2@import '_mixins';
3
4.logs {
5 font-family: monospace;
6 font-size: 13px;
7 max-height: 500px;
8 overflow-y: auto;
9 background: rgba(0, 0, 0, 0.03);
10 padding: 20px;
11
12 .log-row {
13 margin-top: 1px;
14
15 &:hover {
16 background: rgba(0, 0, 0, 0.07);
17 }
18 }
19
20 .log-level {
21 font-weight: $font-semibold;
22 margin-right: 5px;
23 }
24
25 .warn {
26 color: $orange-color;
27 }
28
29 .error {
30 color: $red;
31 }
32}
33
34.header {
35 display: flex;
36 justify-content: flex-end;
37 margin-bottom: 10px;
38
39 .peertube-select-container {
40 @include peertube-select-container(150px);
41 }
42
43 my-button,
44 .peertube-select-container {
45 margin-left: 10px;
46 }
47}
48
diff --git a/client/src/app/+admin/system/logs/logs.component.ts b/client/src/app/+admin/system/logs/logs.component.ts
new file mode 100644
index 000000000..17abb8409
--- /dev/null
+++ b/client/src/app/+admin/system/logs/logs.component.ts
@@ -0,0 +1,111 @@
1import { Component, ElementRef, OnInit, ViewChild } from '@angular/core'
2import { LogsService } from '@app/+admin/system/logs/logs.service'
3import { Notifier } from '@app/core'
4import { LogRow } from '@app/+admin/system/logs/log-row.model'
5import { I18n } from '@ngx-translate/i18n-polyfill'
6import { LogLevel } from '@shared/models/server/log-level.type'
7
8@Component({
9 templateUrl: './logs.component.html',
10 styleUrls: [ './logs.component.scss' ]
11})
12export class LogsComponent implements OnInit {
13 @ViewChild('logsElement') logsElement: ElementRef<HTMLElement>
14
15 loading = false
16
17 logs: LogRow[] = []
18 timeChoices: { id: string, label: string }[] = []
19 levelChoices: { id: LogLevel, label: string }[] = []
20
21 startDate: string
22 level: LogLevel
23
24 constructor (
25 private logsService: LogsService,
26 private notifier: Notifier,
27 private i18n: I18n
28 ) { }
29
30 ngOnInit (): void {
31 this.buildTimeChoices()
32 this.buildLevelChoices()
33
34 this.load()
35 }
36
37 refresh () {
38 this.logs = []
39 this.load()
40 }
41
42 load () {
43 this.loading = true
44
45 this.logsService.getLogs(this.level, this.startDate)
46 .subscribe(
47 logs => {
48 this.logs = logs
49
50 setTimeout(() => {
51 this.logsElement.nativeElement.scrollIntoView({ block: 'end', inline: 'nearest' })
52 })
53 },
54
55 err => this.notifier.error(err.message),
56
57 () => this.loading = false
58 )
59 }
60
61 buildTimeChoices () {
62 const lastHour = new Date()
63 lastHour.setHours(lastHour.getHours() - 1)
64
65 const lastDay = new Date()
66 lastDay.setDate(lastDay.getDate() - 1)
67
68 const lastWeek = new Date()
69 lastWeek.setDate(lastWeek.getDate() - 7)
70
71 this.timeChoices = [
72 {
73 id: lastWeek.toISOString(),
74 label: this.i18n('Last week')
75 },
76 {
77 id: lastDay.toISOString(),
78 label: this.i18n('Last day')
79 },
80 {
81 id: lastHour.toISOString(),
82 label: this.i18n('Last hour')
83 }
84 ]
85
86 this.startDate = lastHour.toISOString()
87 }
88
89 buildLevelChoices () {
90 this.levelChoices = [
91 {
92 id: 'debug',
93 label: this.i18n('Debug')
94 },
95 {
96 id: 'info',
97 label: this.i18n('Info')
98 },
99 {
100 id: 'warn',
101 label: this.i18n('Warning')
102 },
103 {
104 id: 'error',
105 label: this.i18n('Error')
106 }
107 ]
108
109 this.level = 'info'
110 }
111}
diff --git a/client/src/app/+admin/system/logs/logs.service.ts b/client/src/app/+admin/system/logs/logs.service.ts
new file mode 100644
index 000000000..4db79a1fa
--- /dev/null
+++ b/client/src/app/+admin/system/logs/logs.service.ts
@@ -0,0 +1,33 @@
1import { catchError, map } from 'rxjs/operators'
2import { HttpClient, HttpParams } from '@angular/common/http'
3import { Injectable } from '@angular/core'
4import { Observable } from 'rxjs'
5import { environment } from '../../../../environments/environment'
6import { RestExtractor, RestService } from '../../../shared'
7import { LogRow } from '@app/+admin/system/logs/log-row.model'
8import { LogLevel } from '@shared/models/server/log-level.type'
9
10@Injectable()
11export class LogsService {
12 private static BASE_JOB_URL = environment.apiUrl + '/api/v1/server/logs'
13
14 constructor (
15 private authHttp: HttpClient,
16 private restService: RestService,
17 private restExtractor: RestExtractor
18 ) {}
19
20 getLogs (level: LogLevel, startDate: string, endDate?: string): Observable<any> {
21 let params = new HttpParams()
22 params = params.append('startDate', startDate)
23 params = params.append('level', level)
24
25 if (endDate) params.append('endDate', endDate)
26
27 return this.authHttp.get<any[]>(LogsService.BASE_JOB_URL, { params })
28 .pipe(
29 map(rows => rows.map(r => new LogRow(r))),
30 catchError(err => this.restExtractor.handleError(err))
31 )
32 }
33}
diff --git a/client/src/app/+admin/system/system.component.html b/client/src/app/+admin/system/system.component.html
new file mode 100644
index 000000000..345a101e6
--- /dev/null
+++ b/client/src/app/+admin/system/system.component.html
@@ -0,0 +1,11 @@
1<div class="admin-sub-header">
2 <div i18n class="form-sub-title">System</div>
3
4 <div class="admin-sub-nav">
5 <a i18n routerLink="jobs" routerLinkActive="active">Jobs</a>
6
7 <a i18n routerLink="logs" routerLinkActive="active">Logs</a>
8 </div>
9</div>
10
11<router-outlet></router-outlet>
diff --git a/client/src/app/+admin/system/system.component.scss b/client/src/app/+admin/system/system.component.scss
new file mode 100644
index 000000000..766d7853b
--- /dev/null
+++ b/client/src/app/+admin/system/system.component.scss
@@ -0,0 +1,4 @@
1.form-sub-title {
2 flex-grow: 0;
3 margin-right: 30px;
4}
diff --git a/client/src/app/+admin/system/system.component.ts b/client/src/app/+admin/system/system.component.ts
new file mode 100644
index 000000000..992d9c8af
--- /dev/null
+++ b/client/src/app/+admin/system/system.component.ts
@@ -0,0 +1,8 @@
1import { Component } from '@angular/core'
2
3@Component({
4 templateUrl: './system.component.html',
5 styleUrls: [ './system.component.scss' ]
6})
7export class SystemComponent {
8}
diff --git a/client/src/app/+admin/system/system.routes.ts b/client/src/app/+admin/system/system.routes.ts
new file mode 100644
index 000000000..e6d45b760
--- /dev/null
+++ b/client/src/app/+admin/system/system.routes.ts
@@ -0,0 +1,44 @@
1import { Routes } from '@angular/router'
2import { UserRightGuard } from '../../core'
3import { UserRight } from '../../../../../shared'
4import { JobsComponent } from '@app/+admin/system/jobs/jobs.component'
5import { LogsComponent } from '@app/+admin/system/logs'
6import { SystemComponent } from '@app/+admin/system/system.component'
7
8export const SystemRoutes: Routes = [
9 {
10 path: 'system',
11 component: SystemComponent,
12 data: {
13 },
14 children: [
15 {
16 path: '',
17 redirectTo: 'jobs',
18 pathMatch: 'full'
19 },
20 {
21 path: 'jobs',
22 canActivate: [ UserRightGuard ],
23 component: JobsComponent,
24 data: {
25 meta: {
26 userRight: UserRight.MANAGE_JOBS,
27 title: 'Jobs'
28 }
29 }
30 },
31 {
32 path: 'logs',
33 canActivate: [ UserRightGuard ],
34 component: LogsComponent,
35 data: {
36 meta: {
37 userRight: UserRight.MANAGE_LOGS,
38 title: 'Logs'
39 }
40 }
41 }
42 ]
43 }
44]
diff --git a/client/src/app/shared/images/global-icon.component.ts b/client/src/app/shared/images/global-icon.component.ts
index bd5b11bb0..03cf3d7ae 100644
--- a/client/src/app/shared/images/global-icon.component.ts
+++ b/client/src/app/shared/images/global-icon.component.ts
@@ -44,7 +44,8 @@ const icons = {
44 'folder': require('../../../assets/images/global/folder.html'), 44 'folder': require('../../../assets/images/global/folder.html'),
45 'administration': require('../../../assets/images/menu/administration.html'), 45 'administration': require('../../../assets/images/menu/administration.html'),
46 'subscriptions': require('../../../assets/images/menu/subscriptions.html'), 46 'subscriptions': require('../../../assets/images/menu/subscriptions.html'),
47 'users': require('../../../assets/images/global/users.html') 47 'users': require('../../../assets/images/global/users.html'),
48 'refresh': require('../../../assets/images/global/refresh.html')
48} 49}
49 50
50export type GlobalIconName = keyof typeof icons 51export type GlobalIconName = keyof typeof icons
diff --git a/client/src/assets/images/global/refresh.html b/client/src/assets/images/global/refresh.html
new file mode 100644
index 000000000..421ab343d
--- /dev/null
+++ b/client/src/assets/images/global/refresh.html
@@ -0,0 +1,12 @@
1<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
2 <defs/>
3 <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
4 <g id="Artboard-4" transform="translate(-224.000000, -1046.000000)" fill="#000000">
5 <g id="Extras" transform="translate(48.000000, 1046.000000)">
6 <g id="refresh" transform="translate(176.000000, 0.000000)">
7 <path d="M20.9995201,13.0312796 L20.9999519,13.0312796 C20.9830843,17.9874565 16.960132,22 12,22 C7.02943725,22 3,17.9705627 3,13 C3,8.0398348 7.01259713,4.01686187 11.9688198,4.00005287 L11.9688198,6.00006796 C8.11716976,6.01686496 5,9.14440548 5,13 C5,16.8659932 8.13400675,20 12,20 C15.8555614,20 18.9830812,16.8828839 18.9999316,13.0312796 L19.0004799,13.0312796 C19.0001607,13.0208922 19,13.0104649 19,13 C19,12.4477153 19.4477153,12 20,12 C20.5522847,12 21,12.4477153 21,13 C21,13.0104649 20.9998393,13.0208922 20.9995201,13.0312796 Z M12,9 L12,1 L16,5 L12,9 Z" id="Combined-Shape"/>
8 </g>
9 </g>
10 </g>
11 </g>
12</svg>
diff --git a/server/controllers/api/server/logs.ts b/server/controllers/api/server/logs.ts
index c551c67e3..5fa3c8787 100644
--- a/server/controllers/api/server/logs.ts
+++ b/server/controllers/api/server/logs.ts
@@ -2,10 +2,8 @@ import * as express from 'express'
2import { UserRight } from '../../../../shared/models/users' 2import { UserRight } from '../../../../shared/models/users'
3import { asyncMiddleware, authenticate, ensureUserHasRight } from '../../../middlewares' 3import { asyncMiddleware, authenticate, ensureUserHasRight } from '../../../middlewares'
4import { mtimeSortFilesDesc } from '../../../../shared/utils/logs/logs' 4import { mtimeSortFilesDesc } from '../../../../shared/utils/logs/logs'
5import { readdir } from 'fs-extra' 5import { readdir, readFile } from 'fs-extra'
6import { CONFIG, MAX_LOGS_OUTPUT_CHARACTERS } from '../../../initializers' 6import { CONFIG, MAX_LOGS_OUTPUT_CHARACTERS } from '../../../initializers'
7import { createInterface } from 'readline'
8import { createReadStream } from 'fs'
9import { join } from 'path' 7import { join } from 'path'
10import { getLogsValidator } from '../../../middlewares/validators/logs' 8import { getLogsValidator } from '../../../middlewares/validators/logs'
11import { LogLevel } from '../../../../shared/models/server/log-level.type' 9import { LogLevel } from '../../../../shared/models/server/log-level.type'
@@ -36,7 +34,7 @@ async function getLogs (req: express.Request, res: express.Response) {
36 const endDate = req.query.endDate ? new Date(req.query.endDate) : new Date() 34 const endDate = req.query.endDate ? new Date(req.query.endDate) : new Date()
37 const level: LogLevel = req.query.level || 'info' 35 const level: LogLevel = req.query.level || 'info'
38 36
39 let output = '' 37 let output: string[] = []
40 38
41 for (const meta of sortedLogFiles) { 39 for (const meta of sortedLogFiles) {
42 const path = join(CONFIG.STORAGE.LOG_DIR, meta.file) 40 const path = join(CONFIG.STORAGE.LOG_DIR, meta.file)
@@ -44,18 +42,19 @@ async function getLogs (req: express.Request, res: express.Response) {
44 const result = await getOutputFromFile(path, startDate, endDate, level, currentSize) 42 const result = await getOutputFromFile(path, startDate, endDate, level, currentSize)
45 if (!result.output) break 43 if (!result.output) break
46 44
47 output = output + result.output 45 output = result.output.concat(output)
48 currentSize = result.currentSize 46 currentSize = result.currentSize
49 47
50 if (currentSize > MAX_LOGS_OUTPUT_CHARACTERS) break 48 if (currentSize > MAX_LOGS_OUTPUT_CHARACTERS || (result.logTime && result.logTime < startDate.getTime())) break
51 } 49 }
52 50
53 return res.json(output).end() 51 return res.json(output).end()
54} 52}
55 53
56function getOutputFromFile (path: string, startDate: Date, endDate: Date, level: LogLevel, currentSize: number) { 54async function getOutputFromFile (path: string, startDate: Date, endDate: Date, level: LogLevel, currentSize: number) {
57 const startTime = startDate.getTime() 55 const startTime = startDate.getTime()
58 const endTime = endDate.getTime() 56 const endTime = endDate.getTime()
57 let logTime: number
59 58
60 const logsLevel: { [ id in LogLevel ]: number } = { 59 const logsLevel: { [ id in LogLevel ]: number } = {
61 debug: 0, 60 debug: 0,
@@ -64,27 +63,32 @@ function getOutputFromFile (path: string, startDate: Date, endDate: Date, level:
64 error: 3 63 error: 3
65 } 64 }
66 65
67 return new Promise<{ output: string, currentSize: number }>(res => { 66 const content = await readFile(path)
68 const stream = createReadStream(path) 67 const lines = content.toString().split('\n')
69 let output = '' 68 const output: any[] = []
70 69
71 stream.once('close', () => res({ output, currentSize })) 70 for (let i = lines.length - 1; i >= 0; i--) {
71 const line = lines[ i ]
72 let log: any
72 73
73 const rl = createInterface({ 74 try {
74 input: stream 75 log = JSON.parse(line)
75 }) 76 } catch {
77 // Maybe there a multiple \n at the end of the file
78 continue
79 }
76 80
77 rl.on('line', line => { 81 logTime = new Date(log.timestamp).getTime()
78 const log = JSON.parse(line) 82 if (logTime >= startTime && logTime <= endTime && logsLevel[ log.level ] >= logsLevel[ level ]) {
83 output.push(log)
79 84
80 const logTime = new Date(log.timestamp).getTime() 85 currentSize += line.length
81 if (logTime >= startTime && logTime <= endTime && logsLevel[log.level] >= logsLevel[level]) {
82 output += line
83 86
84 currentSize += line.length 87 if (currentSize > MAX_LOGS_OUTPUT_CHARACTERS) break
88 } else if (logTime < startTime) {
89 break
90 }
91 }
85 92
86 if (currentSize > MAX_LOGS_OUTPUT_CHARACTERS) stream.close() 93 return { currentSize, output: output.reverse(), logTime }
87 }
88 })
89 })
90} 94}