i18n-title
info="Review the log files for the application and for email checking."
i18n-info>
- <div class="form-check form-switch">
- <input class="form-check-input" type="checkbox" role="switch" [(ngModel)]="autoRefreshEnabled">
- <label class="form-check-label" for="autoRefreshSwitch" i18n>Auto refresh</label>
+ <div class="input-group input-group-sm align-items-center">
+ <div class="input-group input-group-sm me-3">
+ <span class="input-group-text text-muted" i18n>Show</span>
+ <input
+ class="form-control"
+ type="number"
+ min="100"
+ step="100"
+ [(ngModel)]="limit"
+ (ngModelChange)="onLimitChange($event)"
+ style="width: 100px;">
+ <span class="input-group-text text-muted" i18n>lines</span>
+ </div>
+ <div class="form-check form-switch mt-1">
+ <input class="form-check-input" type="checkbox" role="switch" [(ngModel)]="autoRefreshEnabled">
+ <label class="form-check-label" for="autoRefreshSwitch" i18n>Auto refresh</label>
+ </div>
</div>
</pngx-page-header>
})
it('should display logs with first log initially', () => {
- expect(logSpy).toHaveBeenCalledWith('paperless')
+ expect(logSpy).toHaveBeenCalledWith('paperless', 5000)
fixture.detectChanges()
expect(fixture.debugElement.nativeElement.textContent).toContain(
paperless_logs[0]
fixture.debugElement
.queryAll(By.directive(NgbNavLink))[1]
.nativeElement.dispatchEvent(new MouseEvent('click'))
- expect(logSpy).toHaveBeenCalledWith('mail')
+ expect(logSpy).toHaveBeenCalledWith('mail', 5000)
})
it('should handle error with no logs', () => {
jest.advanceTimersByTime(6000)
expect(reloadSpy).toHaveBeenCalledTimes(2)
})
+
+ it('should debounce limit changes before reloading logs', () => {
+ const initialCalls = reloadSpy.mock.calls.length
+ component.onLimitChange(6000)
+ jest.advanceTimersByTime(299)
+ expect(reloadSpy).toHaveBeenCalledTimes(initialCalls)
+ jest.advanceTimersByTime(1)
+ expect(reloadSpy).toHaveBeenCalledTimes(initialCalls + 1)
+ })
})
} from '@angular/core'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap'
-import { filter, takeUntil, timer } from 'rxjs'
+import { Subject, debounceTime, filter, takeUntil, timer } from 'rxjs'
import { LogService } from 'src/app/services/rest/log.service'
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
import { LoadingComponentWithPermissions } from '../../loading-component/loading.component'
public autoRefreshEnabled: boolean = true
+ public limit: number = 5000
+
+ private readonly limitChange$ = new Subject<number>()
+
@ViewChild('logContainer') logContainer: CdkVirtualScrollViewport
ngOnInit(): void {
+ this.limitChange$
+ .pipe(debounceTime(300), takeUntil(this.unsubscribeNotifier))
+ .subscribe(() => this.reloadLogs())
+
this.logService
.list()
.pipe(takeUntil(this.unsubscribeNotifier))
super.ngOnDestroy()
}
+ onLimitChange(limit: number): void {
+ this.limitChange$.next(limit)
+ }
+
reloadLogs() {
this.loading = true
this.logService
- .get(this.activeLog)
+ .get(this.activeLog, this.limit)
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe({
next: (result) => {
- this.logs = this.parseLogsWithLevel(result)
this.loading = false
- this.scrollToBottom()
+ const parsed = this.parseLogsWithLevel(result)
+ const hasChanges =
+ parsed.length !== this.logs.length ||
+ parsed.some((log, idx) => {
+ const current = this.logs[idx]
+ return (
+ !current ||
+ current.message !== log.message ||
+ current.level !== log.level
+ )
+ })
+ if (hasChanges) {
+ this.logs = parsed
+ this.scrollToBottom()
+ }
},
error: () => {
this.logs = []
)
expect(req.request.method).toEqual('GET')
})
+
+ it('should pass limit param on logs get when provided', () => {
+ const id: string = 'mail'
+ const limit: number = 100
+ subscription = service.get(id, limit).subscribe()
+ const req = httpTestingController.expectOne(
+ `${environment.apiBaseUrl}${endpoint}/${id}/?limit=${limit}`
+ )
+ expect(req.request.method).toEqual('GET')
+ })
})
-import { HttpClient } from '@angular/common/http'
+import { HttpClient, HttpParams } from '@angular/common/http'
import { Injectable, inject } from '@angular/core'
import { Observable } from 'rxjs'
import { environment } from 'src/environments/environment'
return this.http.get<string[]>(`${environment.apiBaseUrl}logs/`)
}
- get(id: string): Observable<string[]> {
- return this.http.get<string[]>(`${environment.apiBaseUrl}logs/${id}/`)
+ get(id: string, limit?: number): Observable<string[]> {
+ let params = new HttpParams()
+ if (limit !== undefined) {
+ params = params.set('limit', limit.toString())
+ }
+ return this.http.get<string[]>(`${environment.apiBaseUrl}logs/${id}/`, {
+ params,
+ })
}
}
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertListEqual(response.data, ["test", "test2"])
+ def test_get_log_with_limit(self):
+ log_data = "test1\ntest2\ntest3\n"
+ with (Path(settings.LOGGING_DIR) / "paperless.log").open("w") as f:
+ f.write(log_data)
+ response = self.client.get("/api/logs/paperless/", {"limit": 2})
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertListEqual(response.data, ["test2", "test3"])
+
+ def test_get_log_with_invalid_limit(self):
+ log_data = "test1\ntest2\n"
+ with (Path(settings.LOGGING_DIR) / "paperless.log").open("w") as f:
+ f.write(log_data)
+ response = self.client.get("/api/logs/paperless/", {"limit": "abc"})
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+ response = self.client.get("/api/logs/paperless/", {"limit": -5})
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+
def test_invalid_regex_other_algorithm(self):
for endpoint in ["correspondents", "tags", "document_types"]:
response = self.client.post(
import tempfile
import zipfile
from collections import defaultdict
+from collections import deque
from datetime import datetime
from pathlib import Path
from time import mktime
from rest_framework import serializers
from rest_framework.decorators import action
from rest_framework.exceptions import NotFound
+from rest_framework.exceptions import ValidationError
from rest_framework.filters import OrderingFilter
from rest_framework.filters import SearchFilter
from rest_framework.generics import GenericAPIView
type=OpenApiTypes.STR,
location=OpenApiParameter.PATH,
),
+ OpenApiParameter(
+ name="limit",
+ type=OpenApiTypes.INT,
+ location=OpenApiParameter.QUERY,
+ description="Return only the last N entries from the log file",
+ required=False,
+ ),
],
responses={
(200, "application/json"): serializers.ListSerializer(
if not log_file.is_file():
raise Http404
+ limit_param = request.query_params.get("limit")
+ if limit_param is not None:
+ try:
+ limit = int(limit_param)
+ except (TypeError, ValueError):
+ raise ValidationError({"limit": "Must be a positive integer"})
+ if limit < 1:
+ raise ValidationError({"limit": "Must be a positive integer"})
+ else:
+ limit = None
+
with log_file.open() as f:
- lines = [line.rstrip() for line in f.readlines()]
+ if limit is None:
+ lines = [line.rstrip() for line in f.readlines()]
+ else:
+ lines = [line.rstrip() for line in deque(f, maxlen=limit)]
return Response(lines)