-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(cb2-14315): ngrx based approach for internal logging system
- Loading branch information
1 parent
4c6eca6
commit 97276ad
Showing
24 changed files
with
694 additions
and
20 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
110 changes: 110 additions & 0 deletions
110
src/app/interceptors/response-logger/__tests__/response-logger.interceptor.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
import { HttpErrorResponse, HttpHandler, HttpRequest, HttpResponse } from '@angular/common/http'; | ||
import { TestBed } from '@angular/core/testing'; | ||
import { ResponseLoggerInterceptor } from '@interceptors/response-logger/response-logger.interceptor'; | ||
import { LogType } from '@models/logs/logs.model'; | ||
import { MockStore, provideMockStore } from '@ngrx/store/testing'; | ||
import { LogsProvider } from '@services/logs/logs.service'; | ||
import { id, initialState } from '@store/user/user-service.reducer'; | ||
import { of, throwError } from 'rxjs'; | ||
|
||
describe('Interceptor: ResponseLoggerInterceptor', () => { | ||
let interceptor: ResponseLoggerInterceptor; | ||
let logsProvider: jest.Mocked<LogsProvider>; | ||
let mockStore: MockStore; | ||
const mockThreshold = 10000; | ||
const mockReq = new HttpRequest('GET', 'https://example.com'); | ||
const mockNext = { | ||
handle: jest.fn(), | ||
} as jest.Mocked<HttpHandler>; | ||
|
||
beforeEach(() => { | ||
const logsProviderMock = { | ||
dispatchLog: jest.fn(), | ||
}; | ||
|
||
TestBed.configureTestingModule({ | ||
providers: [ | ||
ResponseLoggerInterceptor, | ||
{ provide: LogsProvider, useValue: logsProviderMock }, | ||
provideMockStore({ initialState }), | ||
], | ||
}); | ||
|
||
interceptor = TestBed.inject(ResponseLoggerInterceptor); | ||
logsProvider = TestBed.inject(LogsProvider) as jest.Mocked<LogsProvider>; | ||
mockStore = TestBed.inject(MockStore); | ||
|
||
jest.spyOn(interceptor, 'threshold', 'get').mockReturnValue(mockThreshold); | ||
|
||
mockStore.overrideSelector(id, 'test-oid'); | ||
}); | ||
|
||
it('should be created', () => { | ||
expect(interceptor).toBeTruthy(); | ||
}); | ||
|
||
it('should calculate request duration', () => { | ||
expect(interceptor.getRequestDuration(11000, 10000)).toEqual(1000); | ||
}); | ||
|
||
it('should not log when request is for local assets', (done) => { | ||
const localReq = new HttpRequest('GET', 'assets/test.json'); | ||
mockNext.handle.mockReturnValue(of(new HttpResponse())); | ||
|
||
interceptor.intercept(localReq, mockNext).subscribe(() => { | ||
expect(logsProvider.dispatchLog).not.toHaveBeenCalled(); | ||
done(); | ||
}); | ||
}); | ||
|
||
it('should log successful responses', (done) => { | ||
const mockResponse = new HttpResponse({ status: 200, statusText: 'OK', url: 'https://example.com' }); | ||
mockNext.handle.mockReturnValue(of(mockResponse)); | ||
|
||
interceptor.intercept(mockReq, mockNext).subscribe(() => { | ||
expect(logsProvider.dispatchLog).toHaveBeenCalledWith({ | ||
type: LogType.INFO, | ||
message: 'test-oid - 200 OK for API call to https://example.com', | ||
timestamp: expect.any(Number), | ||
}); | ||
done(); | ||
}); | ||
}); | ||
|
||
it('should log when request is slower than threshold', (done) => { | ||
const mockResponse = new HttpResponse({ status: 200, statusText: 'OK', url: 'https://example.com' }); | ||
mockNext.handle.mockReturnValue(of(mockResponse)); | ||
|
||
jest.spyOn(interceptor as any, 'getRequestDuration').mockReturnValue(mockThreshold + 1); | ||
|
||
interceptor.intercept(mockReq, mockNext).subscribe(() => { | ||
expect(logsProvider.dispatchLog).toHaveBeenCalledWith({ | ||
timestamp: expect.any(Number), | ||
oid: 'test-oid', | ||
type: LogType.WARN, | ||
detail: 'Long Request', | ||
message: 'Request to https://example.com is taking longer than 10 seconds', | ||
requestDurationInMs: mockThreshold + 1, | ||
}); | ||
done(); | ||
}); | ||
}); | ||
|
||
it('should log errors', (done) => { | ||
const mockError = new HttpErrorResponse({ status: 404, statusText: 'Not Found', url: 'https://example.com' }); | ||
mockNext.handle.mockReturnValue(throwError(() => mockError)); | ||
|
||
interceptor.intercept(mockReq, mockNext).subscribe({ | ||
error: () => { | ||
expect(logsProvider.dispatchLog).toHaveBeenCalledWith({ | ||
type: LogType.ERROR, | ||
message: 'test-oid - Http failure response for https://example.com: 404 Not Found', | ||
status: 404, | ||
errors: undefined, | ||
timestamp: expect.any(Number), | ||
}); | ||
done(); | ||
}, | ||
}); | ||
}); | ||
}); |
80 changes: 80 additions & 0 deletions
80
src/app/interceptors/response-logger/response-logger.interceptor.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
import { | ||
HttpErrorResponse, | ||
HttpEvent, | ||
HttpHandler, | ||
HttpInterceptor, | ||
HttpRequest, | ||
HttpResponse, | ||
} from '@angular/common/http'; | ||
import { Injectable, inject } from '@angular/core'; | ||
import { LogType } from '@models/logs/logs.model'; | ||
import { Store } from '@ngrx/store'; | ||
import { LogsProvider } from '@services/logs/logs.service'; | ||
import { id } from '@store/user/user-service.reducer'; | ||
import { get } from 'lodash'; | ||
import { Observable, catchError, tap, throwError } from 'rxjs'; | ||
|
||
@Injectable() | ||
export class ResponseLoggerInterceptor implements HttpInterceptor { | ||
private logsProvider = inject(LogsProvider); | ||
private oid = inject(Store).selectSignal(id); | ||
|
||
intercept<T>(request: HttpRequest<T>, next: HttpHandler): Observable<HttpEvent<unknown>> { | ||
const start = Date.now(); | ||
|
||
return next.handle(request).pipe( | ||
tap((event) => { | ||
// skip logging for local files | ||
if (request.url.includes('assets/') && request.url.endsWith('.json')) return; | ||
|
||
const finish = Date.now(); | ||
|
||
if (event instanceof HttpResponse) { | ||
this.logsProvider.dispatchLog({ | ||
type: LogType.INFO, | ||
message: `${this.oid()} - ${event.status} ${event.statusText} for API call to ${event.url}`, | ||
timestamp: Date.now(), | ||
}); | ||
} | ||
|
||
const requestDuration = this.getRequestDuration(finish, start); | ||
|
||
// if the request took longer than the threshold, log the request | ||
if (requestDuration > this.threshold) { | ||
this.logsProvider.dispatchLog({ | ||
timestamp: Date.now(), | ||
oid: this.oid(), | ||
type: LogType.WARN, | ||
detail: 'Long Request', | ||
message: `Request to ${request.url} is taking longer than ${this.threshold / 1000} seconds`, | ||
requestDurationInMs: requestDuration, | ||
}); | ||
} | ||
}), | ||
catchError((err) => { | ||
const status = err instanceof HttpErrorResponse ? err.status : 0; | ||
|
||
const message = err instanceof HttpErrorResponse || err instanceof Error ? err.message : JSON.stringify(err); | ||
|
||
this.logsProvider.dispatchLog({ | ||
type: LogType.ERROR, | ||
message: `${this.oid()} - ${message}`, | ||
status, | ||
errors: get(err, 'error.errors', undefined), | ||
timestamp: Date.now(), | ||
}); | ||
|
||
return throwError(() => err); | ||
}) | ||
); | ||
} | ||
|
||
get threshold(): number { | ||
return 10_000; // 10 seconds in milliseconds | ||
} | ||
|
||
// separated out to allow simplified spies in tests | ||
getRequestDuration(finish: number, start: number): number { | ||
return finish - start; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
export type LogsModel = Log[]; | ||
|
||
export type Log = { | ||
type: string; | ||
message: string; | ||
timestamp: number; | ||
[propName: string]: unknown; | ||
}; | ||
|
||
export enum LogType { | ||
DEBUG = 'debug', | ||
INFO = 'info', | ||
WARN = 'warn', | ||
ERROR = 'error', | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.