From 54cdf512c04aa9017d5f8a772cea3a70b2582244 Mon Sep 17 00:00:00 2001 From: Peter Maquiran Date: Mon, 17 Jun 2024 09:02:10 +0100 Subject: [PATCH] add open telemetry --- angular.json | 2 +- gabinete-digital-fo.code-workspace | 3 + nice.ts | 87 +++++++ package-lock.json | 159 +++++++++++++ package.json | 5 + src/app/home/home.page.ts | 7 +- src/app/pages/agenda/agenda.page.ts | 21 +- .../Agenda/agenda-data-repository.service.ts | 9 +- .../Agenda/mapper/EventListMapper.ts | 12 +- src/app/services/http.service.ts | 9 +- src/app/services/logger/colored/service.ts | 64 +++++ .../capture-log/capture-log.service.spec.ts | 16 ++ .../capture-log/capture-log.service.ts | 122 ++++++++++ .../monitoring/capture-log/worker.worker.ts | 152 ++++++++++++ .../monitoring/opentelemetry/opentelemetry.ts | 33 +++ .../monitoring/opentelemetry/tracer.ts | 223 ++++++++++++++++++ 16 files changed, 910 insertions(+), 14 deletions(-) create mode 100644 nice.ts create mode 100644 src/app/services/logger/colored/service.ts create mode 100644 src/app/services/monitoring/capture-log/capture-log.service.spec.ts create mode 100644 src/app/services/monitoring/capture-log/capture-log.service.ts create mode 100644 src/app/services/monitoring/capture-log/worker.worker.ts create mode 100644 src/app/services/monitoring/opentelemetry/opentelemetry.ts create mode 100644 src/app/services/monitoring/opentelemetry/tracer.ts diff --git a/angular.json b/angular.json index 80af72b60..cd7b53a72 100644 --- a/angular.json +++ b/angular.json @@ -68,7 +68,7 @@ ], "optimization": true, "outputHashing": "all", - "sourceMap": false, + "sourceMap": true, "namedChunks": false, "aot": true, "extractLicenses": true, diff --git a/gabinete-digital-fo.code-workspace b/gabinete-digital-fo.code-workspace index 6ea093890..173cb0292 100644 --- a/gabinete-digital-fo.code-workspace +++ b/gabinete-digital-fo.code-workspace @@ -5,6 +5,9 @@ }, { "path": "../_________________" + }, + { + "path": "../socket-server" } ], "settings": { diff --git a/nice.ts b/nice.ts new file mode 100644 index 000000000..ba3bd7058 --- /dev/null +++ b/nice.ts @@ -0,0 +1,87 @@ +import { Injectable } from '@angular/core'; +import { Observable, Subject, BehaviorSubject } from 'rxjs'; +import { webSocket, WebSocketSubject } from 'rxjs/webSocket'; +import { catchError, retryWhen, tap, delay } from 'rxjs/operators'; + +interface WebSocketMessage { +type: string; +payload: any; +} + +interface WebSocketError { +type: string; +error: any; +} + +@Injectable({ +providedIn: 'root' +}) +export class MessageLiveDataSourceService { + private socket$: WebSocketSubject; + private messageSubject$: Subject; + private connectionStatus$: BehaviorSubject; + private reconnectAttempts = 0; + private readonly maxReconnectAttempts = 5; + + constructor() { + this.messageSubject$ = new Subject(); + this.connectionStatus$ = new BehaviorSubject(false); + } + + public connect(url: string) { + this.socket$ = webSocket(url); + + this.socket$.pipe( + tap({ + error: () => { + this.connectionStatus$.next(false); + } + }), + retryWhen(errors => errors.pipe( + tap(() => { + this.reconnectAttempts++; + if (this.reconnectAttempts >= this.maxReconnectAttempts) { + throw new Error('Max reconnect attempts reached'); + } + }), + delay(1000) + )) + ).subscribe( + (message) => { + this.messageSubject$.next(message); + this.connectionStatus$.next(true); + this.reconnectAttempts = 0; + }, + (err) => { + console.error('WebSocket connection error:', err); + }, + () => { + console.log('WebSocket connection closed'); + this.connectionStatus$.next(false); + } + ); + } + + public sendMessage(message: WebSocketMessage): Observable { + return new Observable(observer => { + this.socket$.next(message); + observer.next(); + observer.complete(); + }).pipe( + catchError(err => { + console.error('Send message error:', err); + return new Observable(observer => { + observer.error({ type: 'SEND_ERROR', error: err }); + }); + }) + ); + } + + public get messages$(): Observable { + return this.messageSubject$.asObservable(); + } + + public get connectionStatus(): Observable { + return this.connectionStatus$.asObservable(); + } +} diff --git a/package-lock.json b/package-lock.json index f29dcf8ef..3a237c9d4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -96,6 +96,11 @@ "@ng-bootstrap/ng-bootstrap": "^9.1.2", "@ngx-translate/core": "^13.0.0", "@ngxs/store": "^3.8.2", + "@opentelemetry/exporter-zipkin": "^1.25.0", + "@opentelemetry/resources": "^1.25.0", + "@opentelemetry/sdk-trace-base": "^1.25.0", + "@opentelemetry/sdk-trace-web": "^1.25.0", + "@opentelemetry/semantic-conventions": "^1.25.0", "@pdftron/pdfjs-express": "^8.0.1", "@sentry/angular": "7.73.0", "@sentry/capacitor": "^0.14.0", @@ -7720,6 +7725,101 @@ "read-package-json-fast": "^2.0.1" } }, + "node_modules/@opentelemetry/api": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "peer": true, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/core": { + "version": "1.25.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.25.0.tgz", + "integrity": "sha512-n0B3s8rrqGrasTgNkXLKXzN0fXo+6IYP7M5b7AMsrZM33f/y6DS6kJ0Btd7SespASWq8bgL3taLo0oe0vB52IQ==", + "dependencies": { + "@opentelemetry/semantic-conventions": "1.25.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-zipkin": { + "version": "1.25.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-zipkin/-/exporter-zipkin-1.25.0.tgz", + "integrity": "sha512-nnhY0e5DHg8BfUSNCQZoGZnGeqz+zMTeEUOh1dfgtaXmF99uM0QPuTa1i2lH+eZqebP8w1WDWZlewu9FUlHqIg==", + "dependencies": { + "@opentelemetry/core": "1.25.0", + "@opentelemetry/resources": "1.25.0", + "@opentelemetry/sdk-trace-base": "1.25.0", + "@opentelemetry/semantic-conventions": "1.25.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0" + } + }, + "node_modules/@opentelemetry/resources": { + "version": "1.25.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.25.0.tgz", + "integrity": "sha512-iHjydPMYJ+Li1auveJCq2rp5U2h6Mhq8BidiyE0jfVlDTFyR1ny8AfJHfmFzJ/RAM8vT8L7T21kcmGybxZC7lQ==", + "dependencies": { + "@opentelemetry/core": "1.25.0", + "@opentelemetry/semantic-conventions": "1.25.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-base": { + "version": "1.25.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.25.0.tgz", + "integrity": "sha512-6+g2fiRQUG39guCsKVeY8ToeuUf3YUnPkN6DXRA1qDmFLprlLvZm9cS6+chgbW70cZJ406FTtSCDnJwxDC5sGQ==", + "dependencies": { + "@opentelemetry/core": "1.25.0", + "@opentelemetry/resources": "1.25.0", + "@opentelemetry/semantic-conventions": "1.25.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-web": { + "version": "1.25.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-web/-/sdk-trace-web-1.25.0.tgz", + "integrity": "sha512-TAWRDRiVOeliE1A99z8idWb4pwEKKY9Vj5aTpLtrF4cvPOl0mPg3ZczvOw/HnpfRsWY0Ra/J3vS5uFSpoqPwEA==", + "dependencies": { + "@opentelemetry/core": "1.25.0", + "@opentelemetry/sdk-trace-base": "1.25.0", + "@opentelemetry/semantic-conventions": "1.25.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/semantic-conventions": { + "version": "1.25.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.25.0.tgz", + "integrity": "sha512-M+kkXKRAIAiAP6qYyesfrC5TOmDpDVtsxuGfPcqd9B/iBrac+E14jYwrgm0yZBUIbIP2OnqC3j+UgkXLm1vxUQ==", + "engines": { + "node": ">=14" + } + }, "node_modules/@pdftron/pdfjs-express": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/@pdftron/pdfjs-express/-/pdfjs-express-8.1.0.tgz", @@ -49492,6 +49592,65 @@ "read-package-json-fast": "^2.0.1" } }, + "@opentelemetry/api": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "peer": true + }, + "@opentelemetry/core": { + "version": "1.25.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.25.0.tgz", + "integrity": "sha512-n0B3s8rrqGrasTgNkXLKXzN0fXo+6IYP7M5b7AMsrZM33f/y6DS6kJ0Btd7SespASWq8bgL3taLo0oe0vB52IQ==", + "requires": { + "@opentelemetry/semantic-conventions": "1.25.0" + } + }, + "@opentelemetry/exporter-zipkin": { + "version": "1.25.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-zipkin/-/exporter-zipkin-1.25.0.tgz", + "integrity": "sha512-nnhY0e5DHg8BfUSNCQZoGZnGeqz+zMTeEUOh1dfgtaXmF99uM0QPuTa1i2lH+eZqebP8w1WDWZlewu9FUlHqIg==", + "requires": { + "@opentelemetry/core": "1.25.0", + "@opentelemetry/resources": "1.25.0", + "@opentelemetry/sdk-trace-base": "1.25.0", + "@opentelemetry/semantic-conventions": "1.25.0" + } + }, + "@opentelemetry/resources": { + "version": "1.25.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.25.0.tgz", + "integrity": "sha512-iHjydPMYJ+Li1auveJCq2rp5U2h6Mhq8BidiyE0jfVlDTFyR1ny8AfJHfmFzJ/RAM8vT8L7T21kcmGybxZC7lQ==", + "requires": { + "@opentelemetry/core": "1.25.0", + "@opentelemetry/semantic-conventions": "1.25.0" + } + }, + "@opentelemetry/sdk-trace-base": { + "version": "1.25.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.25.0.tgz", + "integrity": "sha512-6+g2fiRQUG39guCsKVeY8ToeuUf3YUnPkN6DXRA1qDmFLprlLvZm9cS6+chgbW70cZJ406FTtSCDnJwxDC5sGQ==", + "requires": { + "@opentelemetry/core": "1.25.0", + "@opentelemetry/resources": "1.25.0", + "@opentelemetry/semantic-conventions": "1.25.0" + } + }, + "@opentelemetry/sdk-trace-web": { + "version": "1.25.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-web/-/sdk-trace-web-1.25.0.tgz", + "integrity": "sha512-TAWRDRiVOeliE1A99z8idWb4pwEKKY9Vj5aTpLtrF4cvPOl0mPg3ZczvOw/HnpfRsWY0Ra/J3vS5uFSpoqPwEA==", + "requires": { + "@opentelemetry/core": "1.25.0", + "@opentelemetry/sdk-trace-base": "1.25.0", + "@opentelemetry/semantic-conventions": "1.25.0" + } + }, + "@opentelemetry/semantic-conventions": { + "version": "1.25.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.25.0.tgz", + "integrity": "sha512-M+kkXKRAIAiAP6qYyesfrC5TOmDpDVtsxuGfPcqd9B/iBrac+E14jYwrgm0yZBUIbIP2OnqC3j+UgkXLm1vxUQ==" + }, "@pdftron/pdfjs-express": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/@pdftron/pdfjs-express/-/pdfjs-express-8.1.0.tgz", diff --git a/package.json b/package.json index fa6405ffc..81621ee3d 100644 --- a/package.json +++ b/package.json @@ -112,6 +112,11 @@ "@ng-bootstrap/ng-bootstrap": "^9.1.2", "@ngx-translate/core": "^13.0.0", "@ngxs/store": "^3.8.2", + "@opentelemetry/exporter-zipkin": "^1.25.0", + "@opentelemetry/resources": "^1.25.0", + "@opentelemetry/sdk-trace-base": "^1.25.0", + "@opentelemetry/sdk-trace-web": "^1.25.0", + "@opentelemetry/semantic-conventions": "^1.25.0", "@pdftron/pdfjs-express": "^8.0.1", "@sentry/angular": "7.73.0", "@sentry/capacitor": "^0.14.0", diff --git a/src/app/home/home.page.ts b/src/app/home/home.page.ts index d1b30d812..dd4a86762 100644 --- a/src/app/home/home.page.ts +++ b/src/app/home/home.page.ts @@ -17,6 +17,7 @@ import { RoleIdService } from 'src/app/services/role-id.service'; import { ActiveTabService } from 'src/app/services/active-tab.service'; import { Device } from '@capacitor/device'; import { RouteService } from 'src/app/services/route.service'; +import { CaptureLogService } from 'src/app/services/monitoring/capture-log/capture-log.service'; import { NetworkServiceService, ConnectionStatus } from 'src/app/services/network-service.service'; import { UserSession } from '../models/user.model'; import { PermissionList } from '../models/permission/permissionList'; @@ -90,12 +91,10 @@ export class HomePage implements OnInit { constructor( private router: Router, public modalCtrl: AlertController, - private notificationsService: NotificationsService, public platform: Platform, public p: PermissionService, private backgroundservice: BackgroundService, private storage: Storage, - private eventservice: EventsService, private processservice: ProcessesService, public RouteService: RouteService, private RochetChatConnectorService: RochetChatConnectorService, @@ -103,9 +102,9 @@ export class HomePage implements OnInit { public eventService: EventsService, public ActiveTabService: ActiveTabService, private RoleIdService: RoleIdService, - private modalController: ModalController, private zone: NgZone, public alertController: AlertController, + public CaptureLogService: CaptureLogService // private ChunkService: ChunkService, // private StreamService: StreamService @@ -347,7 +346,7 @@ export class HomePage implements OnInit { logDeviceInfo = async () => { const info = await Device.getInfo(); - /* console.log('DEVICE INFO ',info); */ + console.log('DEVICE INFO ',info); } updateList() { diff --git a/src/app/pages/agenda/agenda.page.ts b/src/app/pages/agenda/agenda.page.ts index 226fff510..7f46ae9f9 100644 --- a/src/app/pages/agenda/agenda.page.ts +++ b/src/app/pages/agenda/agenda.page.ts @@ -39,6 +39,8 @@ import { Observable } from 'rxjs'; import { TableSharedCalendar } from 'src/app/services/Repositorys/Agenda/agenda-local-data-source.service'; import { map } from 'rxjs/operators'; import { EEventFilterStatus } from 'src/app/services/Repositorys/Agenda/model/enums'; +import { TracingType, XTracerAsync } from 'src/app/services/monitoring/opentelemetry/tracer'; +import { isHttpError } from 'src/app/services/http.service'; @Component({ selector: 'app-agenda', templateUrl: './agenda.page.html', @@ -673,7 +675,10 @@ export class AgendaPage implements OnInit { }); }; - async loadRangeEventRun(startTime: Date, endTime: Date) { + @XTracerAsync({name:'AgendaPage/loadRangeEventRun', log: false, bugPrint: false}) + async loadRangeEventRun(startTime: Date, endTime: Date, tracing?: TracingType) { + + tracing.addEvent('load range start') if (SessionStore.user.OwnerCalendars.length == 0 && SessionStore.user.SharedCalendars.length == 0) { return false @@ -707,7 +712,7 @@ export class AgendaPage implements OnInit { endDate: endTime.toISOString(), startDate: startTime.toISOString(), status: EEventFilterStatus.Approved - }) + }, tracing) if(response.isOk()) { let label; @@ -735,7 +740,13 @@ export class AgendaPage implements OnInit { this.myCal.update(); this.myCal.loadEvents(); this.updateEventListBox() + tracing.setAttribute('outcome', 'success') + } else { + + tracing.setAttribute('outcome', 'failed') + tracing.bugFlag() + load++ if (load == (selectedCalendarIds).length) { this.showLoader = false; @@ -750,8 +761,14 @@ export class AgendaPage implements OnInit { } else { /* alert('sekip '+ JSON.stringify(selectedCalendar)) */ console.log('not user', JSON.stringify(selectedCalendar)) + tracing.setAttribute('outcome', 'failed') + tracing.setAttribute('error', 'selectedCalendar.wxUserId') + tracing.bugFlag() } + + tracing.addEvent('load range end') + } } diff --git a/src/app/services/Repositorys/Agenda/agenda-data-repository.service.ts b/src/app/services/Repositorys/Agenda/agenda-data-repository.service.ts index 41612cd57..e0966f3bf 100644 --- a/src/app/services/Repositorys/Agenda/agenda-data-repository.service.ts +++ b/src/app/services/Repositorys/Agenda/agenda-data-repository.service.ts @@ -12,6 +12,8 @@ import { HttpErrorResponse } from '@angular/common/http'; import { EventToApproveDetailsMapper } from './mapper/EventToApproveDetailsMapper'; import { AgendaLocalDataSourceService } from './agenda-local-data-source.service'; import { EEventFilterStatus } from './model/enums'; +import { isHttpError } from '../../http.service'; +import { TracingType } from '../../monitoring/opentelemetry/tracer'; @Injectable({ providedIn: 'root' @@ -68,7 +70,7 @@ export class AgendaDataRepositoryService { } - async EventList({ userId, startDate, endDate, status = EEventFilterStatus.Approved, category = null, type = null, calendarOwnerName = '' }) { + async EventList({ userId, startDate, endDate, status = EEventFilterStatus.Approved, category = null, type = null, calendarOwnerName = '' }, tracing?: TracingType) { try { const result = await this.agendaDataService.getEvents(userId, startDate, endDate, status, category, type).pipe( @@ -78,6 +80,11 @@ export class AgendaDataRepositoryService { )).toPromise() return ok(result) } catch (e) { + if(isHttpError(e)) { + tracing?.setAttribute('status.code', e.status.toString()) + } else { + tracing?.setAttribute('map.error', JSON.stringify(e.error)) + } return err(e as HttpErrorResponse) } } diff --git a/src/app/services/Repositorys/Agenda/mapper/EventListMapper.ts b/src/app/services/Repositorys/Agenda/mapper/EventListMapper.ts index a457b91ed..5e8edb0d6 100644 --- a/src/app/services/Repositorys/Agenda/mapper/EventListMapper.ts +++ b/src/app/services/Repositorys/Agenda/mapper/EventListMapper.ts @@ -1,6 +1,7 @@ import { EventList } from "src/app/models/entiry/agenda/eventList" import { EventListOutputDTO } from "../model/eventListDTOOutput" import { EEventCategory, EEventOwnerType, EEventStatus, EEventType } from "../model/enums"; +import { XTracer } from "src/app/services/monitoring/opentelemetry/tracer"; function getTextInsideParentheses(inputString): string { var startIndex = inputString.indexOf('('); @@ -13,6 +14,7 @@ function getTextInsideParentheses(inputString): string { } export class ListEventMapper { + // @XTracer({name:'ListEventMapper/toDomain', log: false, bugPrint: false}) static toDomain(dto: EventListOutputDTO, calendarOwnerName: string, userId: string): EventList { return dto.map((e) => { @@ -23,7 +25,7 @@ export class ListEventMapper { } else { category = 'Pessoal' } - + let ownerType; if(e.ownerType == EEventOwnerType.PR) { ownerType = 'PR' @@ -32,15 +34,15 @@ export class ListEventMapper { } else if(e.ownerType == EEventOwnerType.Others) { ownerType = 'Other' } - - + + let type; if(e.type == EEventType.Meeting) { type = 'Meeting' } else if (e.type == EEventType.Travel) { type = 'Travel' } - + let status; if(e.status == EEventStatus.Pending) { status = 'Pending' @@ -52,7 +54,7 @@ export class ListEventMapper { status = 'Declined' } - return { + return { "HasAttachments": e.hasAttachments, "IsAllDayEvent": e.isAllDayEvent, "EventId": e.id, diff --git a/src/app/services/http.service.ts b/src/app/services/http.service.ts index fd5f35a4a..d97cb3a1f 100644 --- a/src/app/services/http.service.ts +++ b/src/app/services/http.service.ts @@ -1,4 +1,4 @@ -import { HttpClient, HttpErrorResponse } from '@angular/common/http'; +import { HttpClient, HttpErrorResponse, HttpResponse } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { ok, err, Result } from 'neverthrow'; @@ -49,3 +49,10 @@ export class HttpService { } } } + + + +export const isHttpError = (e: any): e is HttpResponse => { + return Number.isInteger((e as HttpResponse).status); +} + diff --git a/src/app/services/logger/colored/service.ts b/src/app/services/logger/colored/service.ts new file mode 100644 index 000000000..67c36c990 --- /dev/null +++ b/src/app/services/logger/colored/service.ts @@ -0,0 +1,64 @@ + + + +// function getCurrentTime() { +// const now = new Date(); +// const hours = String(now.getHours()).padStart(2, '0'); +// const minutes = String(now.getMinutes()).padStart(2, '0'); +// const seconds = String(now.getSeconds()).padStart(2, '0'); +// const milliseconds = String(now.getMilliseconds()).padStart(3, '0'); +// return `${hours}:${minutes}:${seconds}.${milliseconds}`; +// } + + +// export class Logger { +// private app!: string; + +// constructor() {} + +// static log(message: string): void { +// console.log( +// `[${getCurrentTime()}] %cINFO : `, // Console Message +// 'color: #00897B', // CSS Style +// message +// ); +// } + +// static debug({ message, context, obj = {} }: MessageType): void { +// console.info( +// `[${getCurrentTime()}] %cINFO : `, // Console Message +// 'color: #039BE5', // CSS Style +// Object.assign(obj, { context, createdAt: DateUtils.getISODateString(), message }) +// ); +// } + +// static info({ message, context, obj = {} }: MessageType): void { +// console.info( +// `[${getCurrentTime()}] %cINFO : `, // Console Message +// 'color: #039BE5', // CSS Style +// Object.assign(obj, { context, createdAt: DateUtils.getISODateString(), message }) +// ); +// } + +// static warn({ message, context, obj = {} }: MessageType): void { +// console.warn( +// `[${getCurrentTime()}] %cWARN : `, // Console Message +// 'color: #FB8C00', // CSS Style +// Object.assign(obj, { context, createdAt: DateUtils.getISODateString(), message }) +// ); +// } + +// static error(error: any = "", message?: string, context = ""): void { + +// console.error( +// `[${getCurrentTime()}] %cERROR : `, // Console Message +// 'color: #E53935', // CSS Style +// message+', '+ context, +// '\n', +// error, +// '\n', +// ); +// } + +// fatal(error?: ErrorType, message?: string, context?: string): void {} +// } diff --git a/src/app/services/monitoring/capture-log/capture-log.service.spec.ts b/src/app/services/monitoring/capture-log/capture-log.service.spec.ts new file mode 100644 index 000000000..ee36a2092 --- /dev/null +++ b/src/app/services/monitoring/capture-log/capture-log.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { CaptureLogService } from './capture-log.service'; + +describe('CaptureLogService', () => { + let service: CaptureLogService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(CaptureLogService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/services/monitoring/capture-log/capture-log.service.ts b/src/app/services/monitoring/capture-log/capture-log.service.ts new file mode 100644 index 000000000..8577f2f4e --- /dev/null +++ b/src/app/services/monitoring/capture-log/capture-log.service.ts @@ -0,0 +1,122 @@ +import { Injectable } from '@angular/core'; +import { Device } from '@capacitor/device'; +import { SocketLog } from './worker.worker'; +import { FCM } from '@capacitor-community/fcm'; +import { ActionPerformed, PushNotificationSchema, PushNotifications, Token, } from '@capacitor/push-notifications'; +import { AlertController, Platform } from '@ionic/angular'; +import { AngularFireMessaging } from '@angular/fire/messaging'; + +@Injectable({ + providedIn: 'root' +}) +export class CaptureLogService { + + deviceName = '' + + constructor( + public socket: SocketLog, + private platform: Platform, + private afMessaging: AngularFireMessaging, + ) { + + // this.interceptLogs() + + Device.getInfo().then(e => { + this.deviceName = e.name + }); + + } + + interceptLogs() { + (() => { + const originalConsoleLog = console.log; + const originalConsoleError = console.error; + const originalConsoleWarn = console.warn; + const originalConsoleInfo = console.info; + + console.log = function(...args) { + //sendLogToServer(args.join(' ')); + this.socket.sendMessage({ + type:'sendLog', + payload: { + logType: 'log', + args + } + }) + originalConsoleLog.apply(console, args); + }; + + console.error = function(...args) { + // sendLogToServer('ERROR: ' + args.join(' ')); + + this.socket.sendMessage({ + type:'sendLog', + payload: { + logType: 'error', + args + } + }) + originalConsoleError.apply(console, args); + }; + + console.warn = function(...args) { + this.socket.sendMessage({ + type:'sendLog', + payload: { + logType: 'warn', + args + } + }) + originalConsoleWarn.apply(console, args); + }; + + console.info = function(...args) { + this.socket.sendMessage({ + type:'sendLog', + payload: { + logType: 'info', + args + } + }) + originalConsoleInfo.apply(console, args); + }; + })(); + } + + + async getDeviceToken() { + + return new Promise((resolve, reject) => { + + if (this.platform.is('mobile')) { + if (this.platform.is('ios')) { + FCM.getToken() + .then(r => { + resolve(r.token) + }) + .catch(err => console.log(err)); + } else { + + PushNotifications.addListener('registration', + (token: Token) => { + resolve(token.value) + } + ); + } + + } else { + this.afMessaging.requestToken.subscribe( + (token) => { + resolve(token) + }, + (error) => { + console.error('Permission denied:', error); + } + ); + } + }) + + + } + +} diff --git a/src/app/services/monitoring/capture-log/worker.worker.ts b/src/app/services/monitoring/capture-log/worker.worker.ts new file mode 100644 index 000000000..c46e88887 --- /dev/null +++ b/src/app/services/monitoring/capture-log/worker.worker.ts @@ -0,0 +1,152 @@ +// src/app/worker.worker.ts +// addEventListener('message', ({ data }) => { +// const response = `Worker response to ${data}`; +// postMessage(response); +// }); + + + + +import { Injectable } from '@angular/core'; +import { Observable, Subject, BehaviorSubject } from 'rxjs'; +import { webSocket, WebSocketSubject } from 'rxjs/webSocket'; +import { catchError, retryWhen, tap, delay } from 'rxjs/operators'; + +interface WebSocketMessage { + type: string; + payload: any; +} + +interface WebSocketError { + type: string; + error: any; +} + +@Injectable({ + providedIn: 'root' +}) +export class SocketLog { + private socket$: WebSocketSubject; + private messageSubject$: Subject; + private connectionStatus$: BehaviorSubject; + private reconnectAttempts = 0; + private readonly maxReconnectAttempts = 5; + + constructor() { + this.messageSubject$ = new Subject(); + this.connectionStatus$ = new BehaviorSubject(false); + this.setupVisibilityChangeHandler(); + } + + public connect(url: string) { + this.socket$ = webSocket(url); + + this.socket$.pipe( + tap({ + error: () => { + this.connectionStatus$.next(false); + } + }), + retryWhen(errors => errors.pipe( + tap(() => { + this.reconnectAttempts++; + if (this.reconnectAttempts >= this.maxReconnectAttempts) { + throw new Error('Max reconnect attempts reached'); + } + }), + delay(1000) + )) + ).subscribe( + (message) => { + this.messageSubject$.next(message); + this.connectionStatus$.next(true); + this.reconnectAttempts = 0; + }, + (err) => { + console.error('WebSocket connection error:', err); + }, + () => { + console.log('WebSocket connection closed'); + this.connectionStatus$.next(false); + } + ); + } + + public sendMessage(message: WebSocketMessage): Observable { + return new Observable(observer => { + this.socket$.next(message); + observer.next(); + observer.complete(); + }).pipe( + catchError(err => { + console.error('Send message error:', err); + return new Observable(observer => { + observer.error({ type: 'SEND_ERROR', error: err }); + }); + }) + ); + } + + public get messages$(): Observable { + return this.messageSubject$.asObservable(); + } + + public get connectionStatus(): Observable { + return this.connectionStatus$.asObservable(); + } + + private setupVisibilityChangeHandler() { + if (typeof document !== 'undefined' && typeof document.addEventListener === 'function') { + document.addEventListener('visibilitychange', () => { + if (document.visibilityState === 'visible') { + this.reconnect(); + } + }); + } + } + + private reconnect() { + if (this.socket$) { + const url = (this.socket$ as any)._config.url; + this.establishNewConnection(url); + } + } + + private establishNewConnection(url: string) { + const newSocket$ = webSocket(url); + + newSocket$.pipe( + tap({ + error: () => { + console.error('New WebSocket connection error'); + } + }), + retryWhen(errors => errors.pipe( + tap(() => { + this.reconnectAttempts++; + if (this.reconnectAttempts >= this.maxReconnectAttempts) { + throw new Error('Max reconnect attempts reached'); + } + }), + delay(1000) + )) + ).subscribe( + (message) => { + this.messageSubject$.next(message); + this.connectionStatus$.next(true); + this.reconnectAttempts = 0; + + // Close the old socket and replace it with the new one + this.socket$.complete(); + this.socket$ = newSocket$; + }, + (err) => { + console.error('New WebSocket connection error:', err); + }, + () => { + console.log('New WebSocket connection closed'); + this.connectionStatus$.next(false); + } + ); + } +} diff --git a/src/app/services/monitoring/opentelemetry/opentelemetry.ts b/src/app/services/monitoring/opentelemetry/opentelemetry.ts new file mode 100644 index 000000000..0b96a98cc --- /dev/null +++ b/src/app/services/monitoring/opentelemetry/opentelemetry.ts @@ -0,0 +1,33 @@ +import { ConsoleSpanExporter, SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base'; +import { WebTracerProvider } from '@opentelemetry/sdk-trace-web'; +import { ZipkinExporter } from '@opentelemetry/exporter-zipkin'; +import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions' +import { Resource } from '@opentelemetry/resources'; + +function createProvider(serviceName) { + const provider = new WebTracerProvider({ + resource: new Resource({ + [SemanticResourceAttributes.SERVICE_NAME]: serviceName, + }), + }); + + provider.addSpanProcessor(new SimpleSpanProcessor(new ConsoleSpanExporter())); + provider.addSpanProcessor(new SimpleSpanProcessor(new ZipkinExporter({ + url: 'https://5.180.182.151/zipkin-endpoint/api/v2/spans', + serviceName: serviceName, + // Uncomment and customize the following if needed + // getExportRequestHeaders: () => { + // return { + // foo: 'bar', + // }; + // } + }))); + + provider.register(); + return provider; +} + +// Example usage: +export const OpentelemetryChatProvider = createProvider('FO-chat-service'); +export const OpentelemetryAgendaProvider = createProvider('FO-agenda-service'); +export const OpentelemetryPublicationProvider = createProvider('FO-publication-service'); diff --git a/src/app/services/monitoring/opentelemetry/tracer.ts b/src/app/services/monitoring/opentelemetry/tracer.ts new file mode 100644 index 000000000..b714c4ae3 --- /dev/null +++ b/src/app/services/monitoring/opentelemetry/tracer.ts @@ -0,0 +1,223 @@ +import { v4 as uuidv4 } from 'uuid'; +import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; +import { Tracer, Span } from '@opentelemetry/sdk-trace-base'; +import { OpentelemetryAgendaProvider } from './opentelemetry'; +import { Device, DeviceInfo } from '@capacitor/device'; + +const tracerInstance = OpentelemetryAgendaProvider.getTracer('example-tracer-hole', '111', { + +}) +// const logger: ILoggerAdapter = new ColoredLoggerService() + +let device: DeviceInfo; + +Device.getInfo().then(e => { + device = e +}); + +export function XTracerAsync({name, log, bugPrint}: any) { + return ( + target: unknown, + propertyKey: string, + descriptor: PropertyDescriptor, + ) => { + const originalMethod = descriptor.value; + descriptor.value = async function (...args: unknown[]) { + + const requestId = uuidv4() + const span = tracerInstance.startSpan(name); + + let hasBug:Boolean + + const createTracingInstance = (): TracingType => { + const logs: any[] = [] + + + return { + span: span as any, + tracer: tracerInstance, + tracerId: requestId, + attributes: SemanticAttributes, + setStatus: (status: any) => { + span.setStatus(status); + }, + addEvent: (context: string, message?: any, obj?: any) => { + if(log == true) { + // logger.error(obj, context, message) + } else { + logs.push({context, message: message, obj}) + } + + const value = [JSON.stringify(message)] as any + span.addEvent(context, value); + }, + LocalLogEvent:(context: string, message: any, obj: any) => { + logs.push({context, message, obj}) + }, + setAttribute: (key: string, value: string) => { + span.setAttribute(key, value); + }, + finish: () => { + span.end(); + if(bugPrint && hasBug) { + for(const {context, message, obj} of logs) { + // logger.error(obj, context, message) + } + } + }, + bugFlag:() => { + hasBug = true + }, + createSpan: (name, parent?: any) => { + return tracerInstance.startSpan(name, { root: false }, parent) as Span; + } + } + } + + const tracing = createTracingInstance() + tracing.setAttribute('current.page', window.location.pathname); + tracing.setAttribute('device.name', device.name || device.model) + + args.push(tracing) + + try { + const result = await originalMethod.apply(this, args); + + tracing.finish() + + return result + } catch (e) { + + tracing.finish() + console.error(e); + return false + } + }; + }; +} + + +export function XTracer({name, log, bugPrint}: any) { + return ( + target: unknown, + propertyKey: string, + descriptor: PropertyDescriptor, + ) => { + const originalMethod = descriptor.value; + descriptor.value = function (...args: unknown[]) { + + const requestId = uuidv4() + const span = tracerInstance.startSpan(name); + + let hasBug:Boolean + + const createTracingInstance = (): TracingType => { + const logs: any[] = [] + + + return { + span: span as any, + tracer: tracerInstance, + tracerId: requestId, + attributes: SemanticAttributes, + setStatus: (status: any) => { + span.setStatus(status); + }, + addEvent: (context: string, message?: any, obj?: any) => { + if(log == true) { + // logger.error(obj, context, message) + } else { + logs.push({context, message: message, obj}) + } + + const value = [JSON.stringify(message)] as any + span.addEvent(context, value); + }, + LocalLogEvent:(context: string, message: any, obj: any) => { + logs.push({context, message, obj}) + }, + setAttribute: (key: string, value: any) => { + span.setAttribute(key, value); + }, + finish: () => { + span.end(); + if(bugPrint && hasBug) { + for(const {context, message, obj} of logs) { + // logger.error(obj, context, message) + } + } + }, + bugFlag:() => { + hasBug = true + }, + createSpan: (name, parent?: any) => { + return tracerInstance.startSpan(name, { root: false }, parent) as Span; + } + } + } + + const tracing = createTracingInstance() + tracing.setAttribute('current.page', window.location.pathname); + tracing.setAttribute('device.name', device.name || device.model) + + args.push(tracing) + + try { + const result = originalMethod.apply(this, args); + + tracing.finish() + + return result + } catch (e) { + + tracing.finish() + console.error(e); + return false + } + }; + }; +} + +export type TracingType = { + span: Span; + tracer: Tracer; + tracerId: string; + attributes: typeof SemanticAttributes; + // axios: (config?: AxiosRequestConfig) => AxiosInstance; + setStatus: (status: any) => void; + //logEvent: (name: string, attributesOrStartTime?: AttributeValue | TimeInput) => void; + addEvent: (context: string, message?: any, obj?: any) => void; + setAttribute: (key: string, value: string) => void; + LocalLogEvent: (name: string, attributesOrStartTime: any, obj?:any) => void; + finish: () => void; + bugFlag:() => void; + createSpan:(name, parent?: any) => Span; +}; + +export interface UserInteraction { + readonly params: any; + readonly tracing: TracingType; + readonly user: Pick; + readonly headers: Headers & { authorization: string }; +} + +export type InteractionTrancingInput = Pick; + +export const getPathWithoutUUID = (path: string) => + path.replace( + /[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/, + 'uuid', + ); + + + + + + + + + + + + +