Add logs page in client
This commit is contained in:
parent
fd8710b897
commit
2c22613c2f
|
@ -6,9 +6,9 @@ import { MetaGuard } from '@ngx-meta/core'
|
||||||
|
|
||||||
import { AdminComponent } from './admin.component'
|
import { AdminComponent } from './admin.component'
|
||||||
import { FollowsRoutes } from './follows'
|
import { FollowsRoutes } from './follows'
|
||||||
import { JobsRoutes } from './jobs/job.routes'
|
|
||||||
import { UsersRoutes } from './users'
|
import { UsersRoutes } from './users'
|
||||||
import { ModerationRoutes } from '@app/+admin/moderation/moderation.routes'
|
import { ModerationRoutes } from '@app/+admin/moderation/moderation.routes'
|
||||||
|
import { SystemRoutes } from '@app/+admin/system'
|
||||||
|
|
||||||
const adminRoutes: Routes = [
|
const adminRoutes: Routes = [
|
||||||
{
|
{
|
||||||
|
@ -25,7 +25,7 @@ const adminRoutes: Routes = [
|
||||||
...FollowsRoutes,
|
...FollowsRoutes,
|
||||||
...UsersRoutes,
|
...UsersRoutes,
|
||||||
...ModerationRoutes,
|
...ModerationRoutes,
|
||||||
...JobsRoutes,
|
...SystemRoutes,
|
||||||
...ConfigRoutes
|
...ConfigRoutes
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,13 +12,13 @@
|
||||||
Moderation
|
Moderation
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a i18n *ngIf="hasJobsRight()" routerLink="/admin/jobs" routerLinkActive="active" class="title-page">
|
|
||||||
Jobs
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<a i18n *ngIf="hasConfigRight()" routerLink="/admin/config" routerLinkActive="active" class="title-page">
|
<a i18n *ngIf="hasConfigRight()" routerLink="/admin/config" routerLinkActive="active" class="title-page">
|
||||||
Configuration
|
Configuration
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
<a i18n *ngIf="hasJobsRight() || hasLogsRight()" routerLink="/admin/system" routerLinkActive="active" class="title-page">
|
||||||
|
System
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="margin-content">
|
<div class="margin-content">
|
||||||
|
|
|
@ -28,6 +28,10 @@ export class AdminComponent {
|
||||||
return this.auth.getUser().hasRight(UserRight.MANAGE_JOBS)
|
return this.auth.getUser().hasRight(UserRight.MANAGE_JOBS)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hasLogsRight () {
|
||||||
|
return this.auth.getUser().hasRight(UserRight.MANAGE_LOGS)
|
||||||
|
}
|
||||||
|
|
||||||
hasConfigRight () {
|
hasConfigRight () {
|
||||||
return this.auth.getUser().hasRight(UserRight.MANAGE_CONFIGURATION)
|
return this.auth.getUser().hasRight(UserRight.MANAGE_CONFIGURATION)
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,20 +7,19 @@ import { AdminRoutingModule } from './admin-routing.module'
|
||||||
import { AdminComponent } from './admin.component'
|
import { AdminComponent } from './admin.component'
|
||||||
import { FollowersListComponent, FollowingAddComponent, FollowsComponent, FollowService } from './follows'
|
import { FollowersListComponent, FollowingAddComponent, FollowsComponent, FollowService } from './follows'
|
||||||
import { FollowingListComponent } from './follows/following-list/following-list.component'
|
import { FollowingListComponent } from './follows/following-list/following-list.component'
|
||||||
import { JobsComponent } from './jobs/job.component'
|
import { UserCreateComponent, UserListComponent, UserPasswordComponent, UsersComponent, UserUpdateComponent } from './users'
|
||||||
import { JobsListComponent } from './jobs/jobs-list/jobs-list.component'
|
|
||||||
import { JobService } from './jobs/shared/job.service'
|
|
||||||
import { UserCreateComponent, UserListComponent, UsersComponent, UserUpdateComponent, UserPasswordComponent } from './users'
|
|
||||||
import {
|
import {
|
||||||
ModerationCommentModalComponent,
|
ModerationCommentModalComponent,
|
||||||
VideoAbuseListComponent,
|
VideoAbuseListComponent,
|
||||||
VideoBlacklistListComponent,
|
VideoAutoBlacklistListComponent,
|
||||||
VideoAutoBlacklistListComponent
|
VideoBlacklistListComponent
|
||||||
} from './moderation'
|
} from './moderation'
|
||||||
import { ModerationComponent } from '@app/+admin/moderation/moderation.component'
|
import { ModerationComponent } from '@app/+admin/moderation/moderation.component'
|
||||||
import { RedundancyCheckboxComponent } from '@app/+admin/follows/shared/redundancy-checkbox.component'
|
import { RedundancyCheckboxComponent } from '@app/+admin/follows/shared/redundancy-checkbox.component'
|
||||||
import { RedundancyService } from '@app/+admin/follows/shared/redundancy.service'
|
import { RedundancyService } from '@app/+admin/follows/shared/redundancy.service'
|
||||||
import { InstanceAccountBlocklistComponent, InstanceServerBlocklistComponent } from '@app/+admin/moderation/instance-blocklist'
|
import { InstanceAccountBlocklistComponent, InstanceServerBlocklistComponent } from '@app/+admin/moderation/instance-blocklist'
|
||||||
|
import { JobsComponent } from '@app/+admin/system/jobs/jobs.component'
|
||||||
|
import { JobService, LogsComponent, LogsService, SystemComponent } from '@app/+admin/system'
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
|
@ -52,8 +51,9 @@ import { InstanceAccountBlocklistComponent, InstanceServerBlocklistComponent } f
|
||||||
InstanceServerBlocklistComponent,
|
InstanceServerBlocklistComponent,
|
||||||
InstanceAccountBlocklistComponent,
|
InstanceAccountBlocklistComponent,
|
||||||
|
|
||||||
|
SystemComponent,
|
||||||
JobsComponent,
|
JobsComponent,
|
||||||
JobsListComponent,
|
LogsComponent,
|
||||||
|
|
||||||
ConfigComponent,
|
ConfigComponent,
|
||||||
EditCustomConfigComponent
|
EditCustomConfigComponent
|
||||||
|
@ -67,6 +67,7 @@ import { InstanceAccountBlocklistComponent, InstanceServerBlocklistComponent } f
|
||||||
FollowService,
|
FollowService,
|
||||||
RedundancyService,
|
RedundancyService,
|
||||||
JobService,
|
JobService,
|
||||||
|
LogsService,
|
||||||
ConfigService
|
ConfigService
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
4
client/src/app/+admin/system/index.ts
Normal file
4
client/src/app/+admin/system/index.ts
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
export * from './jobs'
|
||||||
|
export * from './logs'
|
||||||
|
export * from './system.component'
|
||||||
|
export * from './system.routes'
|
|
@ -1,4 +1,2 @@
|
||||||
export * from './shared'
|
export * from './job.service'
|
||||||
export * from './jobs-list'
|
export * from './jobs.component'
|
||||||
export * from './job.routes'
|
|
||||||
export * from './job.component'
|
|
||||||
|
|
|
@ -5,15 +5,15 @@ import { SortMeta } from 'primeng/primeng'
|
||||||
import { Job } from '../../../../../../shared/index'
|
import { Job } from '../../../../../../shared/index'
|
||||||
import { JobState } from '../../../../../../shared/models'
|
import { JobState } from '../../../../../../shared/models'
|
||||||
import { RestPagination, RestTable } from '../../../shared'
|
import { RestPagination, RestTable } from '../../../shared'
|
||||||
import { JobService } from '../shared'
|
import { JobService } from './job.service'
|
||||||
import { I18n } from '@ngx-translate/i18n-polyfill'
|
import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-jobs-list',
|
selector: 'my-jobs',
|
||||||
templateUrl: './jobs-list.component.html',
|
templateUrl: './jobs.component.html',
|
||||||
styleUrls: [ './jobs-list.component.scss' ]
|
styleUrls: [ './jobs.component.scss' ]
|
||||||
})
|
})
|
||||||
export class JobsListComponent extends RestTable implements OnInit {
|
export class JobsComponent extends RestTable implements OnInit {
|
||||||
private static JOB_STATE_LOCAL_STORAGE_STATE = 'jobs-list-state'
|
private static JOB_STATE_LOCAL_STORAGE_STATE = 'jobs-list-state'
|
||||||
|
|
||||||
jobState: JobState = 'waiting'
|
jobState: JobState = 'waiting'
|
||||||
|
@ -58,12 +58,12 @@ export class JobsListComponent extends RestTable implements OnInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
private loadJobState () {
|
private loadJobState () {
|
||||||
const result = peertubeLocalStorage.getItem(JobsListComponent.JOB_STATE_LOCAL_STORAGE_STATE)
|
const result = peertubeLocalStorage.getItem(JobsComponent.JOB_STATE_LOCAL_STORAGE_STATE)
|
||||||
|
|
||||||
if (result) this.jobState = result as JobState
|
if (result) this.jobState = result as JobState
|
||||||
}
|
}
|
||||||
|
|
||||||
private saveJobState () {
|
private saveJobState () {
|
||||||
peertubeLocalStorage.setItem(JobsListComponent.JOB_STATE_LOCAL_STORAGE_STATE, this.jobState)
|
peertubeLocalStorage.setItem(JobsComponent.JOB_STATE_LOCAL_STORAGE_STATE, this.jobState)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
2
client/src/app/+admin/system/logs/index.ts
Normal file
2
client/src/app/+admin/system/logs/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
export * from './logs.component'
|
||||||
|
export * from './logs.service'
|
21
client/src/app/+admin/system/logs/log-row.model.ts
Normal file
21
client/src/app/+admin/system/logs/log-row.model.ts
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import { LogLevel } from '@shared/models/server/log-level.type'
|
||||||
|
import omit from 'lodash-es/omit'
|
||||||
|
|
||||||
|
export class LogRow {
|
||||||
|
date: Date
|
||||||
|
localeDate: string
|
||||||
|
level: LogLevel
|
||||||
|
message: string
|
||||||
|
meta: string
|
||||||
|
|
||||||
|
constructor (row: any) {
|
||||||
|
this.date = new Date(row.timestamp)
|
||||||
|
this.localeDate = this.date.toLocaleString()
|
||||||
|
this.level = row.level
|
||||||
|
this.message = row.message
|
||||||
|
|
||||||
|
const metaObj = omit(row, 'timestamp', 'level', 'message', 'label')
|
||||||
|
|
||||||
|
if (Object.keys(metaObj).length !== 0) this.meta = JSON.stringify(metaObj, undefined, 2)
|
||||||
|
}
|
||||||
|
}
|
31
client/src/app/+admin/system/logs/logs.component.html
Normal file
31
client/src/app/+admin/system/logs/logs.component.html
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
<div class="header">
|
||||||
|
<div class="peertube-select-container">
|
||||||
|
<select [(ngModel)]="startDate" (ngModelChange)="refresh()">
|
||||||
|
<option *ngFor="let timeChoice of timeChoices" [value]="timeChoice.id">{{ timeChoice.label }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="peertube-select-container">
|
||||||
|
<select [(ngModel)]="level" (ngModelChange)="refresh()">
|
||||||
|
<option *ngFor="let levelChoice of levelChoices" [value]="levelChoice.id">{{ levelChoice.label }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<my-button i18n-label label="Refresh" icon="refresh" (click)="refresh()"></my-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="logs">
|
||||||
|
<div *ngIf="loading">Loading...</div>
|
||||||
|
|
||||||
|
<div #logsElement>
|
||||||
|
<div *ngFor="let log of logs" class="log-row" [ngClass]="{ error: log.level === 'error', warn: log.level === 'warn' }">
|
||||||
|
<span class="log-level">{{ log.level }}</span>
|
||||||
|
|
||||||
|
<span class="log-date">[{{ log.localeDate }}]</span>
|
||||||
|
|
||||||
|
{{ log.message }}
|
||||||
|
|
||||||
|
{{ log.meta }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
48
client/src/app/+admin/system/logs/logs.component.scss
Normal file
48
client/src/app/+admin/system/logs/logs.component.scss
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
@import '_variables';
|
||||||
|
@import '_mixins';
|
||||||
|
|
||||||
|
.logs {
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
max-height: 500px;
|
||||||
|
overflow-y: auto;
|
||||||
|
background: rgba(0, 0, 0, 0.03);
|
||||||
|
padding: 20px;
|
||||||
|
|
||||||
|
.log-row {
|
||||||
|
margin-top: 1px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(0, 0, 0, 0.07);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-level {
|
||||||
|
font-weight: $font-semibold;
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warn {
|
||||||
|
color: $orange-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
color: $red;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
|
||||||
|
.peertube-select-container {
|
||||||
|
@include peertube-select-container(150px);
|
||||||
|
}
|
||||||
|
|
||||||
|
my-button,
|
||||||
|
.peertube-select-container {
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
111
client/src/app/+admin/system/logs/logs.component.ts
Normal file
111
client/src/app/+admin/system/logs/logs.component.ts
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
import { Component, ElementRef, OnInit, ViewChild } from '@angular/core'
|
||||||
|
import { LogsService } from '@app/+admin/system/logs/logs.service'
|
||||||
|
import { Notifier } from '@app/core'
|
||||||
|
import { LogRow } from '@app/+admin/system/logs/log-row.model'
|
||||||
|
import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||||
|
import { LogLevel } from '@shared/models/server/log-level.type'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
templateUrl: './logs.component.html',
|
||||||
|
styleUrls: [ './logs.component.scss' ]
|
||||||
|
})
|
||||||
|
export class LogsComponent implements OnInit {
|
||||||
|
@ViewChild('logsElement') logsElement: ElementRef<HTMLElement>
|
||||||
|
|
||||||
|
loading = false
|
||||||
|
|
||||||
|
logs: LogRow[] = []
|
||||||
|
timeChoices: { id: string, label: string }[] = []
|
||||||
|
levelChoices: { id: LogLevel, label: string }[] = []
|
||||||
|
|
||||||
|
startDate: string
|
||||||
|
level: LogLevel
|
||||||
|
|
||||||
|
constructor (
|
||||||
|
private logsService: LogsService,
|
||||||
|
private notifier: Notifier,
|
||||||
|
private i18n: I18n
|
||||||
|
) { }
|
||||||
|
|
||||||
|
ngOnInit (): void {
|
||||||
|
this.buildTimeChoices()
|
||||||
|
this.buildLevelChoices()
|
||||||
|
|
||||||
|
this.load()
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh () {
|
||||||
|
this.logs = []
|
||||||
|
this.load()
|
||||||
|
}
|
||||||
|
|
||||||
|
load () {
|
||||||
|
this.loading = true
|
||||||
|
|
||||||
|
this.logsService.getLogs(this.level, this.startDate)
|
||||||
|
.subscribe(
|
||||||
|
logs => {
|
||||||
|
this.logs = logs
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
this.logsElement.nativeElement.scrollIntoView({ block: 'end', inline: 'nearest' })
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
err => this.notifier.error(err.message),
|
||||||
|
|
||||||
|
() => this.loading = false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTimeChoices () {
|
||||||
|
const lastHour = new Date()
|
||||||
|
lastHour.setHours(lastHour.getHours() - 1)
|
||||||
|
|
||||||
|
const lastDay = new Date()
|
||||||
|
lastDay.setDate(lastDay.getDate() - 1)
|
||||||
|
|
||||||
|
const lastWeek = new Date()
|
||||||
|
lastWeek.setDate(lastWeek.getDate() - 7)
|
||||||
|
|
||||||
|
this.timeChoices = [
|
||||||
|
{
|
||||||
|
id: lastWeek.toISOString(),
|
||||||
|
label: this.i18n('Last week')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: lastDay.toISOString(),
|
||||||
|
label: this.i18n('Last day')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: lastHour.toISOString(),
|
||||||
|
label: this.i18n('Last hour')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
this.startDate = lastHour.toISOString()
|
||||||
|
}
|
||||||
|
|
||||||
|
buildLevelChoices () {
|
||||||
|
this.levelChoices = [
|
||||||
|
{
|
||||||
|
id: 'debug',
|
||||||
|
label: this.i18n('Debug')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'info',
|
||||||
|
label: this.i18n('Info')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'warn',
|
||||||
|
label: this.i18n('Warning')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'error',
|
||||||
|
label: this.i18n('Error')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
this.level = 'info'
|
||||||
|
}
|
||||||
|
}
|
33
client/src/app/+admin/system/logs/logs.service.ts
Normal file
33
client/src/app/+admin/system/logs/logs.service.ts
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
import { catchError, map } from 'rxjs/operators'
|
||||||
|
import { HttpClient, HttpParams } from '@angular/common/http'
|
||||||
|
import { Injectable } from '@angular/core'
|
||||||
|
import { Observable } from 'rxjs'
|
||||||
|
import { environment } from '../../../../environments/environment'
|
||||||
|
import { RestExtractor, RestService } from '../../../shared'
|
||||||
|
import { LogRow } from '@app/+admin/system/logs/log-row.model'
|
||||||
|
import { LogLevel } from '@shared/models/server/log-level.type'
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class LogsService {
|
||||||
|
private static BASE_JOB_URL = environment.apiUrl + '/api/v1/server/logs'
|
||||||
|
|
||||||
|
constructor (
|
||||||
|
private authHttp: HttpClient,
|
||||||
|
private restService: RestService,
|
||||||
|
private restExtractor: RestExtractor
|
||||||
|
) {}
|
||||||
|
|
||||||
|
getLogs (level: LogLevel, startDate: string, endDate?: string): Observable<any> {
|
||||||
|
let params = new HttpParams()
|
||||||
|
params = params.append('startDate', startDate)
|
||||||
|
params = params.append('level', level)
|
||||||
|
|
||||||
|
if (endDate) params.append('endDate', endDate)
|
||||||
|
|
||||||
|
return this.authHttp.get<any[]>(LogsService.BASE_JOB_URL, { params })
|
||||||
|
.pipe(
|
||||||
|
map(rows => rows.map(r => new LogRow(r))),
|
||||||
|
catchError(err => this.restExtractor.handleError(err))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
11
client/src/app/+admin/system/system.component.html
Normal file
11
client/src/app/+admin/system/system.component.html
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
<div class="admin-sub-header">
|
||||||
|
<div i18n class="form-sub-title">System</div>
|
||||||
|
|
||||||
|
<div class="admin-sub-nav">
|
||||||
|
<a i18n routerLink="jobs" routerLinkActive="active">Jobs</a>
|
||||||
|
|
||||||
|
<a i18n routerLink="logs" routerLinkActive="active">Logs</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<router-outlet></router-outlet>
|
4
client/src/app/+admin/system/system.component.scss
Normal file
4
client/src/app/+admin/system/system.component.scss
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
.form-sub-title {
|
||||||
|
flex-grow: 0;
|
||||||
|
margin-right: 30px;
|
||||||
|
}
|
8
client/src/app/+admin/system/system.component.ts
Normal file
8
client/src/app/+admin/system/system.component.ts
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import { Component } from '@angular/core'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
templateUrl: './system.component.html',
|
||||||
|
styleUrls: [ './system.component.scss' ]
|
||||||
|
})
|
||||||
|
export class SystemComponent {
|
||||||
|
}
|
44
client/src/app/+admin/system/system.routes.ts
Normal file
44
client/src/app/+admin/system/system.routes.ts
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
import { Routes } from '@angular/router'
|
||||||
|
import { UserRightGuard } from '../../core'
|
||||||
|
import { UserRight } from '../../../../../shared'
|
||||||
|
import { JobsComponent } from '@app/+admin/system/jobs/jobs.component'
|
||||||
|
import { LogsComponent } from '@app/+admin/system/logs'
|
||||||
|
import { SystemComponent } from '@app/+admin/system/system.component'
|
||||||
|
|
||||||
|
export const SystemRoutes: Routes = [
|
||||||
|
{
|
||||||
|
path: 'system',
|
||||||
|
component: SystemComponent,
|
||||||
|
data: {
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
redirectTo: 'jobs',
|
||||||
|
pathMatch: 'full'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'jobs',
|
||||||
|
canActivate: [ UserRightGuard ],
|
||||||
|
component: JobsComponent,
|
||||||
|
data: {
|
||||||
|
meta: {
|
||||||
|
userRight: UserRight.MANAGE_JOBS,
|
||||||
|
title: 'Jobs'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'logs',
|
||||||
|
canActivate: [ UserRightGuard ],
|
||||||
|
component: LogsComponent,
|
||||||
|
data: {
|
||||||
|
meta: {
|
||||||
|
userRight: UserRight.MANAGE_LOGS,
|
||||||
|
title: 'Logs'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
|
@ -44,7 +44,8 @@ const icons = {
|
||||||
'folder': require('../../../assets/images/global/folder.html'),
|
'folder': require('../../../assets/images/global/folder.html'),
|
||||||
'administration': require('../../../assets/images/menu/administration.html'),
|
'administration': require('../../../assets/images/menu/administration.html'),
|
||||||
'subscriptions': require('../../../assets/images/menu/subscriptions.html'),
|
'subscriptions': require('../../../assets/images/menu/subscriptions.html'),
|
||||||
'users': require('../../../assets/images/global/users.html')
|
'users': require('../../../assets/images/global/users.html'),
|
||||||
|
'refresh': require('../../../assets/images/global/refresh.html')
|
||||||
}
|
}
|
||||||
|
|
||||||
export type GlobalIconName = keyof typeof icons
|
export type GlobalIconName = keyof typeof icons
|
||||||
|
|
12
client/src/assets/images/global/refresh.html
Normal file
12
client/src/assets/images/global/refresh.html
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
<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">
|
||||||
|
<defs/>
|
||||||
|
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||||
|
<g id="Artboard-4" transform="translate(-224.000000, -1046.000000)" fill="#000000">
|
||||||
|
<g id="Extras" transform="translate(48.000000, 1046.000000)">
|
||||||
|
<g id="refresh" transform="translate(176.000000, 0.000000)">
|
||||||
|
<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"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
|
@ -2,10 +2,8 @@ import * as express from 'express'
|
||||||
import { UserRight } from '../../../../shared/models/users'
|
import { UserRight } from '../../../../shared/models/users'
|
||||||
import { asyncMiddleware, authenticate, ensureUserHasRight } from '../../../middlewares'
|
import { asyncMiddleware, authenticate, ensureUserHasRight } from '../../../middlewares'
|
||||||
import { mtimeSortFilesDesc } from '../../../../shared/utils/logs/logs'
|
import { mtimeSortFilesDesc } from '../../../../shared/utils/logs/logs'
|
||||||
import { readdir } from 'fs-extra'
|
import { readdir, readFile } from 'fs-extra'
|
||||||
import { CONFIG, MAX_LOGS_OUTPUT_CHARACTERS } from '../../../initializers'
|
import { CONFIG, MAX_LOGS_OUTPUT_CHARACTERS } from '../../../initializers'
|
||||||
import { createInterface } from 'readline'
|
|
||||||
import { createReadStream } from 'fs'
|
|
||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
import { getLogsValidator } from '../../../middlewares/validators/logs'
|
import { getLogsValidator } from '../../../middlewares/validators/logs'
|
||||||
import { LogLevel } from '../../../../shared/models/server/log-level.type'
|
import { LogLevel } from '../../../../shared/models/server/log-level.type'
|
||||||
|
@ -36,7 +34,7 @@ async function getLogs (req: express.Request, res: express.Response) {
|
||||||
const endDate = req.query.endDate ? new Date(req.query.endDate) : new Date()
|
const endDate = req.query.endDate ? new Date(req.query.endDate) : new Date()
|
||||||
const level: LogLevel = req.query.level || 'info'
|
const level: LogLevel = req.query.level || 'info'
|
||||||
|
|
||||||
let output = ''
|
let output: string[] = []
|
||||||
|
|
||||||
for (const meta of sortedLogFiles) {
|
for (const meta of sortedLogFiles) {
|
||||||
const path = join(CONFIG.STORAGE.LOG_DIR, meta.file)
|
const path = join(CONFIG.STORAGE.LOG_DIR, meta.file)
|
||||||
|
@ -44,18 +42,19 @@ async function getLogs (req: express.Request, res: express.Response) {
|
||||||
const result = await getOutputFromFile(path, startDate, endDate, level, currentSize)
|
const result = await getOutputFromFile(path, startDate, endDate, level, currentSize)
|
||||||
if (!result.output) break
|
if (!result.output) break
|
||||||
|
|
||||||
output = output + result.output
|
output = result.output.concat(output)
|
||||||
currentSize = result.currentSize
|
currentSize = result.currentSize
|
||||||
|
|
||||||
if (currentSize > MAX_LOGS_OUTPUT_CHARACTERS) break
|
if (currentSize > MAX_LOGS_OUTPUT_CHARACTERS || (result.logTime && result.logTime < startDate.getTime())) break
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.json(output).end()
|
return res.json(output).end()
|
||||||
}
|
}
|
||||||
|
|
||||||
function getOutputFromFile (path: string, startDate: Date, endDate: Date, level: LogLevel, currentSize: number) {
|
async function getOutputFromFile (path: string, startDate: Date, endDate: Date, level: LogLevel, currentSize: number) {
|
||||||
const startTime = startDate.getTime()
|
const startTime = startDate.getTime()
|
||||||
const endTime = endDate.getTime()
|
const endTime = endDate.getTime()
|
||||||
|
let logTime: number
|
||||||
|
|
||||||
const logsLevel: { [ id in LogLevel ]: number } = {
|
const logsLevel: { [ id in LogLevel ]: number } = {
|
||||||
debug: 0,
|
debug: 0,
|
||||||
|
@ -64,27 +63,32 @@ function getOutputFromFile (path: string, startDate: Date, endDate: Date, level:
|
||||||
error: 3
|
error: 3
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Promise<{ output: string, currentSize: number }>(res => {
|
const content = await readFile(path)
|
||||||
const stream = createReadStream(path)
|
const lines = content.toString().split('\n')
|
||||||
let output = ''
|
const output: any[] = []
|
||||||
|
|
||||||
stream.once('close', () => res({ output, currentSize }))
|
for (let i = lines.length - 1; i >= 0; i--) {
|
||||||
|
const line = lines[ i ]
|
||||||
|
let log: any
|
||||||
|
|
||||||
const rl = createInterface({
|
try {
|
||||||
input: stream
|
log = JSON.parse(line)
|
||||||
})
|
} catch {
|
||||||
|
// Maybe there a multiple \n at the end of the file
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
rl.on('line', line => {
|
logTime = new Date(log.timestamp).getTime()
|
||||||
const log = JSON.parse(line)
|
if (logTime >= startTime && logTime <= endTime && logsLevel[ log.level ] >= logsLevel[ level ]) {
|
||||||
|
output.push(log)
|
||||||
|
|
||||||
const logTime = new Date(log.timestamp).getTime()
|
currentSize += line.length
|
||||||
if (logTime >= startTime && logTime <= endTime && logsLevel[log.level] >= logsLevel[level]) {
|
|
||||||
output += line
|
|
||||||
|
|
||||||
currentSize += line.length
|
if (currentSize > MAX_LOGS_OUTPUT_CHARACTERS) break
|
||||||
|
} else if (logTime < startTime) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (currentSize > MAX_LOGS_OUTPUT_CHARACTERS) stream.close()
|
return { currentSize, output: output.reverse(), logTime }
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user