feat(logging): Added Elastic APM RUM logging. (TV-316)

Squashed commit of the following:

commit 3c4abbe69605caff2a39efafd90550d93e9e1447
Author: Erik Tiekstra <erik.tiekstra@arbetsformedlingen.se>
Date:   Mon Oct 4 15:56:41 2021 +0200

    Updated npm scripts and built/serve shell

commit 00525a666fb6b3146ea5f85c7c3ad741378401de
Author: Erik Tiekstra <erik.tiekstra@arbetsformedlingen.se>
Date:   Mon Oct 4 13:59:12 2021 +0200

    Updated nginx-conf

commit a9945c14cc93eebf8812c075fe8ca67e39ab8ae8
Author: Erik Tiekstra <erik.tiekstra@arbetsformedlingen.se>
Date:   Mon Oct 4 12:59:49 2021 +0200

    Added elastics serverUrl to environment files and fixed nginx-conf

commit 38872cea957ce54c5cb496890e4be88fb019be58
Author: Erik Tiekstra <erik.tiekstra@arbetsformedlingen.se>
Date:   Mon Oct 4 12:49:41 2021 +0200

    Added Elastic APM with error handling

commit d3db5e8703e3b0a1d0d0b24230dc52a64bee252c
Merge: a3bc70e9 d139f750
Author: Erik Tiekstra <erik.tiekstra@arbetsformedlingen.se>
Date:   Mon Oct 4 12:22:16 2021 +0200

    Merge branch 'develop' into feature/TV-316-RUM

commit a3bc70e9420dc5d309325cfcf1221c6760d18c38
Merge: 3f98a66b c2a02dba
Author: Erik Tiekstra <erik.tiekstra@arbetsformedlingen.se>
Date:   Mon Oct 4 09:07:11 2021 +0200

    Merge branch 'develop' into feature/TV-316-RUM

commit 3f98a66bfda3af315c5417e2d2902ab80877b98b
Author: Erik Tiekstra <erik.tiekstra@arbetsformedlingen.se>
Date:   Fri Oct 1 16:05:43 2021 +0200

    Added @elastic/apm-rum-angular to log errors and api-requests
This commit is contained in:
Erik Tiekstra
2021-10-05 07:22:10 +02:00
parent 81ea5611f2
commit 07ec3c4aeb
34 changed files with 343 additions and 240 deletions

View File

@@ -1,20 +1,23 @@
import { registerLocaleData } from '@angular/common';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import localeSe from '@angular/common/locales/sv';
import { ErrorHandler, LOCALE_ID, NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { ApmErrorHandler } from '@elastic/apm-rum-angular';
import { AuthInterceptor } from '@msfa-interceptors/auth.interceptor';
import { CustomErrorHandler } from '@msfa-interceptors/custom-error-handler.module';
import { CustomErrorHandler } from '@msfa-interceptors/custom-error-handler';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { ToastListModule } from './components/toast-list/toast-list.module';
import { LoggingModule } from './logging.module';
import { AvropModule } from './pages/avrop/avrop.module';
import localeSe from '@angular/common/locales/sv';
import { registerLocaleData } from '@angular/common';
registerLocaleData(localeSe);
@NgModule({
declarations: [AppComponent],
imports: [BrowserModule, HttpClientModule, AppRoutingModule, ToastListModule, AvropModule],
imports: [LoggingModule, BrowserModule, HttpClientModule, AppRoutingModule, ToastListModule, AvropModule],
providers: [
ApmErrorHandler,
{
provide: ErrorHandler,
useClass: CustomErrorHandler,

View File

@@ -0,0 +1,36 @@
import { NgModule } from '@angular/core';
import { ApmModule, ApmService } from '@elastic/apm-rum-angular';
import { Feature } from '@msfa-enums/feature.enum';
import { environment } from '@msfa-environment';
@NgModule({
imports: [ApmModule],
exports: [ApmModule],
})
export class LoggingModule {
private _elasticConfig = environment.elastic;
private _activeFeatures = environment.activeFeatures;
constructor(private apmService: ApmService) {
if (this._elasticConfig && this._activeFeatures.includes(Feature.LOGGING)) {
const { serviceName, serverUrl } = this._elasticConfig;
this.apmService.init({
serviceName,
serverUrl,
environment: this.currentEnvironment,
});
}
}
get currentEnvironment(): string {
const defaultEnvironment = environment.elastic.environment;
const testUrlRegEx = new RegExp(/(?:mina-sidor-fa-)(\w{1,})(?:\.tocp)/g);
const testEnvironment = testUrlRegEx.exec(window.location.origin);
if (testEnvironment?.length) {
return testEnvironment[1].toUpperCase();
}
return defaultEnvironment;
}
}

View File

@@ -6,18 +6,18 @@ import { Observable } from 'rxjs';
import { filter, map } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
providedIn: 'root',
})
export class PeriodiskRedovisningService {
private _apiBaseUrl = `${environment.api.url}`;
public getActivities$(): Observable<Activity[]> { // endpoint ska uppdateras
public getActivities$(): Observable<Activity[]> {
// endpoint ska uppdateras
return this.httpClient.get<{ data: ActivityResponse[] }>(`${this._apiBaseUrl}/activities`).pipe(
filter(response => !!response?.data),
map(({ data }) => data.map(aktivitet => mapResponseToActivity(aktivitet)))
)
);
}
constructor(private httpClient: HttpClient) { }
constructor(private httpClient: HttpClient) {}
}

View File

@@ -9,7 +9,7 @@ import { AuthenticationService } from '@msfa-services/api/authentication.service
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class LogoutComponent implements OnInit {
loginUrl = environment.loginUrl;
loginUrl = environment.ciam.loginUrl;
constructor(private authenticationService: AuthenticationService) {}

View File

@@ -10,4 +10,5 @@ export enum Feature {
ACCESSIBILITY_REPORT,
REPORTING,
SENSITIVE_INFORMATION,
LOGGING,
}

View File

@@ -22,10 +22,10 @@ export class AuthGuard implements CanActivate {
void this.authenticationService.removeLocalStorageData();
if (environment.environment === 'local') {
void this.router.navigateByUrl(environment.loginUrl);
if (environment.ciam.clientId) {
document.location.href = `${environment.ciam.loginUrl}&client_id=${environment.ciam.clientId}&redirect_uri=${window.location.origin}`;
} else {
document.location.href = `${environment.loginUrl}&client_id=${environment.clientId}&redirect_uri=${window.location.origin}`;
void this.router.navigateByUrl(environment.ciam.loginUrl);
}
return of(false);
})

View File

@@ -1,14 +0,0 @@
import { ErrorHandler, Injectable } from '@angular/core';
import { CustomError, errorToCustomError } from '@msfa-models/error/custom-error';
import { ErrorService } from '@msfa-services/error.service';
@Injectable()
export class CustomErrorHandler implements ErrorHandler {
constructor(private errorService: ErrorService) {}
handleError(error: Error & { ngDebugContext: unknown }): void {
const customError: CustomError = errorToCustomError(error);
console.error(error);
this.errorService.add(customError);
}
}

View File

@@ -0,0 +1,22 @@
import { ErrorHandler, Injectable } from '@angular/core';
import { ApmErrorHandler } from '@elastic/apm-rum-angular';
import { Feature } from '@msfa-enums/feature.enum';
import { environment } from '@msfa-environment';
import { CustomError, errorToCustomError } from '@msfa-models/error/custom-error';
import { ErrorService } from '@msfa-services/error.service';
@Injectable()
export class CustomErrorHandler implements ErrorHandler {
private _elasticConfig = environment.elastic;
private _activeFeatures = environment.activeFeatures;
constructor(private errorService: ErrorService, public apmErrorHandler: ApmErrorHandler) {}
handleError(error: Error & { ngDebugContext: unknown }): void {
const customError: CustomError = errorToCustomError(error);
this.errorService.add(customError);
if (this._elasticConfig && this._activeFeatures.includes(Feature.LOGGING)) {
this.apmErrorHandler.handleError(customError);
}
}
}

View File

@@ -1,16 +1,22 @@
import { Feature } from '@msfa-enums/feature.enum';
export interface Environment {
environment: 'api' | 'local' | 'acc' | 'prod';
version?: string;
clientId: string;
loginUrl: string;
logoutUrl: string;
production: boolean;
api: {
url: string;
headers: { [key: string]: string };
skipHeadersOn: string[];
};
ciam: {
clientId?: string;
loginUrl: string;
logoutUrl: string;
};
activeFeatures: Feature[];
elastic?: {
serviceName: string;
serverUrl: string;
environment?: 'DEV' | 'SYS' | 'TEST' | 'ACC' | 'PROD';
};
}

View File

@@ -1,26 +1,24 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { environment } from '@msfa-environment';
import { ActivityResponse } from '@msfa-models/api/activity-response.model';
import { Activity, mapResponseToActivity } from '@msfa-models/activity.model';
import { ActivityResponse } from '@msfa-models/api/activity-response.model';
import { Observable } from 'rxjs';
import { filter, map } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
providedIn: 'root',
})
export class ActivityApiService {
private _apiBaseUrl = `${environment.api.url}`;
public getActivities$(): Observable<Activity[]> { // endpoint ska uppdateras
public getActivities$(): Observable<Activity[]> {
// endpoint ska uppdateras
return this.httpClient.get<{ data: ActivityResponse[] }>(`${this._apiBaseUrl}/aktiviteter`).pipe(
filter(response => !!response?.data),
map(({ data }) => data.map(aktivitet => mapResponseToActivity(aktivitet)))
)
);
}
constructor(private httpClient: HttpClient) { }
constructor(private httpClient: HttpClient) {}
}

View File

@@ -86,10 +86,10 @@ export class AuthenticationService {
logout(): void {
this.removeLocalStorageData();
if (environment.environment === 'local') {
void this.router.navigateByUrl(environment.logoutUrl);
if (environment.ciam.clientId) {
document.location.href = environment.ciam.logoutUrl;
} else {
document.location.href = environment.logoutUrl;
void this.router.navigateByUrl(environment.ciam.logoutUrl);
}
}
}

View File

@@ -13,57 +13,59 @@ import {
KandaAvvikelseKoder,
mapResponseToAndraKandaOrsaker,
mapResponseToOrsaksKoderFranvaro,
OrsaksKoderFranvaro
OrsaksKoderFranvaro,
} from '@msfa-models/orsaks-koder-franvaro.model';
import { ErrorService } from '@msfa-services/error.service';
import { Observable, throwError } from 'rxjs';
import { catchError, filter, map, take } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
providedIn: 'root',
})
export class AvvikelseApiService {
private _apiBaseUrl = `${environment.api.url}/report`;
public getOrsaksKoderFranvaro$(): Observable<OrsaksKoderFranvaro[]> {
return this.httpClient.get<{ data: OrsaksKoderAvvikelseResponse[] }>(`${this._apiBaseUrl}/orsakskoderfranvaro`).pipe(
filter(response => !!response?.data),
map(({ data }) => data.map(orsak => mapResponseToOrsaksKoderFranvaro(orsak)))
)
return this.httpClient
.get<{ data: OrsaksKoderAvvikelseResponse[] }>(`${this._apiBaseUrl}/orsakskoderfranvaro`)
.pipe(
filter(response => !!response?.data),
map(({ data }) => data.map(orsak => mapResponseToOrsaksKoderFranvaro(orsak)))
);
}
public getOrsaksKoderAvvikelse$(): Observable<OrsaksKoderAvvikelse[]> {
return this.httpClient.get<{ data: OrsaksKoderAvvikelseResponse[] }>(`${this._apiBaseUrl}/orsakskoderavvikelse`).pipe(
filter(response => !!response?.data),
map(({ data }) => data.map(avvikelse => mapResponseToOrsaksKoderAvvikelse(avvikelse)))
)
return this.httpClient
.get<{ data: OrsaksKoderAvvikelseResponse[] }>(`${this._apiBaseUrl}/orsakskoderavvikelse`)
.pipe(
filter(response => !!response?.data),
map(({ data }) => data.map(avvikelse => mapResponseToOrsaksKoderAvvikelse(avvikelse)))
);
}
public getAndraKandaOrsaker$(): Observable<KandaAvvikelseKoder[]> {
return this.httpClient.get<{ data: KandaAvvikelseKoderResponse[] }>(`${this._apiBaseUrl}/kandaavvikelsekoder`).pipe(
filter(response => !!response?.data),
map(({ data }) => data.map(annanKandOrsak => mapResponseToAndraKandaOrsaker(annanKandOrsak)))
)
);
}
public getFragorForAvvikelser$(): Observable<FragorForAvvikelser[]> {
return this.httpClient.get<{ data: FragorForAvvikelserResponse[] }>(`${this._apiBaseUrl}/fragorforavvikelser`).pipe(
filter(response => !!response?.data),
map(({ data }) => data.map(fraga => mapResponseToFragorForAvvikelser(fraga)))
)
);
}
public createAvvikelse$(avvikelse: Avvikelse, alternative: string): Observable<Avvikelse> {
return this.httpClient
.post<{ data: AvvikelseRequestData }>(`${this._apiBaseUrl}/${this.setEndPoint(alternative)}`, avvikelse).pipe(
.post<{ data: AvvikelseRequestData }>(`${this._apiBaseUrl}/${this.setEndPoint(alternative)}`, avvikelse)
.pipe(
filter(response => !!response?.data),
take(1),
map(({ data }) => mapAvvikelseRequestDataToAvvikelse(data)),
catchError(error => throwError({ message: error as string, type: ErrorType.API }))
)
);
}
private setEndPoint(alternative: string): string {
@@ -75,12 +77,12 @@ export class AvvikelseApiService {
break;
case Alternative.FRANVARO:
endpoint = 'franvaro';
break
break;
default:
break;
}
return endpoint;
}
constructor(private httpClient: HttpClient, private errorService: ErrorService) { }
constructor(private httpClient: HttpClient, private errorService: ErrorService) {}
}

View File

@@ -17,7 +17,9 @@ export class ErrorService {
constructor(private injector: Injector) {
// Workaround to fix change-detection when using Error interceptor
// See https://stackoverflow.com/a/37793791
setTimeout(() => (this.appRef = this.injector.get(ApplicationRef)));
setTimeout(() => {
this.appRef = this.injector.get(ApplicationRef);
});
}
public add(error: CustomError): void {

View File

@@ -20,4 +20,5 @@ export const ACTIVE_FEATURES_TEST: Feature[] = [
Feature.ACCESSIBILITY_REPORT,
Feature.REPORTING,
Feature.SENSITIVE_INFORMATION,
Feature.LOGGING,
];

View File

@@ -3,7 +3,6 @@ import { ACTIVE_FEATURES_PROD } from './active-features';
import { CIAM_TEST } from './ciam';
export const environment: Environment = {
environment: 'acc',
version: 'acc',
production: true,
api: {
@@ -12,5 +11,10 @@ export const environment: Environment = {
skipHeadersOn: ['assets/'],
},
activeFeatures: [...ACTIVE_FEATURES_PROD],
...CIAM_TEST,
elastic: {
serverUrl: '/logging',
serviceName: 'mina-sidor-fa',
environment: 'ACC',
},
ciam: CIAM_TEST,
};

View File

@@ -1,16 +1,16 @@
import { Feature } from '@msfa-enums/feature.enum';
import { Environment } from '@msfa-models/environment.model';
import { ACTIVE_FEATURES_TEST } from './active-features';
import { CIAM_TEST } from './ciam';
import { CIAM_MOCK } from './ciam';
export const environment: Environment = {
environment: 'api',
version: 'api',
version: 'mock',
production: false,
api: {
url: '/api',
headers: {},
skipHeadersOn: ['assets/'],
skipHeadersOn: [],
},
activeFeatures: [...ACTIVE_FEATURES_TEST],
...CIAM_TEST,
activeFeatures: [...ACTIVE_FEATURES_TEST, Feature.MOCK_LOGIN],
ciam: CIAM_MOCK,
};

View File

@@ -3,7 +3,6 @@ import { ACTIVE_FEATURES_PROD } from './active-features';
import { CIAM_PROD } from './ciam';
export const environment: Environment = {
environment: 'prod',
version: 'prod',
production: true,
api: {
@@ -12,5 +11,10 @@ export const environment: Environment = {
skipHeadersOn: ['assets/'],
},
activeFeatures: [...ACTIVE_FEATURES_PROD],
...CIAM_PROD,
elastic: {
serverUrl: '/logging',
serviceName: 'mina-sidor-fa',
environment: 'PROD',
},
ciam: CIAM_PROD,
};

View File

@@ -1,17 +1,20 @@
import { Feature } from '@msfa-enums/feature.enum';
import { Environment } from '@msfa-models/environment.model';
import { ACTIVE_FEATURES_TEST } from './active-features';
import { CIAM_MOCK } from './ciam';
import { CIAM_TEST } from './ciam';
export const environment: Environment = {
environment: 'local',
version: 'local',
version: 'api',
production: false,
api: {
url: '/api',
headers: {},
skipHeadersOn: [],
skipHeadersOn: ['assets/'],
},
activeFeatures: [...ACTIVE_FEATURES_TEST, Feature.MOCK_LOGIN],
...CIAM_MOCK,
activeFeatures: [...ACTIVE_FEATURES_TEST],
elastic: {
serverUrl: '/logging',
serviceName: 'mina-sidor-fa',
environment: 'DEV',
},
ciam: CIAM_TEST,
};

View File

@@ -2,7 +2,7 @@ import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { defineCustomElements } from '@digi/core/loader';
import { AppModule } from './app/app.module';
import { environment } from './environments/environment';
import { environment } from './environments/environment.mock';
if (environment.production) {
enableProdMode();