Skip to content

Commit

Permalink
refactor tenant app logs functionality (#29)
Browse files Browse the repository at this point in the history
  • Loading branch information
reey authored Oct 27, 2021
1 parent 621b6b2 commit e133e7a
Show file tree
Hide file tree
Showing 10 changed files with 151 additions and 113 deletions.
1 change: 1 addition & 0 deletions apps/administration/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"rightDrawer": true,
"tabsHorizontal": true,
"dynamicOptionsUrl": "/apps/public/public-options/options.json",
"hideWarnings": "duringDevelopment",
"contentSecurityPolicy": "base-uri 'none'; default-src 'self' 'unsafe-inline' http: https: ws: wss:; connect-src 'self' *.billwerk.com http: https: ws: wss:; script-src 'self' open.mapquestapi.com *.twitter.com *.twimg.com *.aptrinsic.com *.billwerk.com 'unsafe-inline' 'unsafe-eval' data:; style-src * 'unsafe-inline' blob:; img-src * data:; font-src * data:; frame-src *;"
},
"cli": {}
Expand Down
1 change: 1 addition & 0 deletions apps/subtenant-management/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"icon": {
"class": "c8y-icon-sub-tenants"
},
"hideWarnings": "duringDevelopment",
"contentSecurityPolicy": "base-uri 'none'; default-src 'self' 'unsafe-inline' http: https: ws: wss:; connect-src 'self' *.billwerk.com http: https: ws: wss:; script-src 'self' open.mapquestapi.com *.twitter.com *.twimg.com *.aptrinsic.com *.billwerk.com 'unsafe-inline' 'unsafe-eval' data:; style-src * 'unsafe-inline' blob:; img-src * data:; font-src * data:; frame-src *;"
},
"cli": {}
Expand Down
46 changes: 46 additions & 0 deletions src/models/application-mo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { IManagedObject } from '@c8y/client';

export interface ILastUpdated {
date: {
$date: string;
};
offset: number;
}

export interface IInstanceDetails {
lastUpdated: ILastUpdated;
memoryInBytes: number;
scheduled: boolean;
restarts: number;
cpuInMillis: number;
}

export interface IApplicationStatus {
lastUpdated: ILastUpdated;
instances: {
[instanceName: string]: IInstanceDetails;
};
details: {
desired: number;
aggregatedResources: {
memory: string;
cpu: string;
};
active: number;
restarts: number;
};
status: string;
}

export interface IApplicationManagedObjectAdditions {
c8y_Status: IApplicationStatus;
applicationOwner: string;
c8y_Subscriptions: {
[tenantId: string]: IApplicationStatus;
};
applicationId: string;
name: string;
c8y_SupportedLogs?: string[];
}

export type IApplicationManagedObject = IManagedObject & IApplicationManagedObjectAdditions;
31 changes: 16 additions & 15 deletions src/modules/tenant-app-logs/apps-of-tenant-tab.factory.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Injectable } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { IApplication } from '@c8y/client';
import { Tab, TabFactory } from '@c8y/ngx-components';
import { IApplicationManagedObject } from '@models/application-mo';
import { merge, Observable } from 'rxjs';
import { filter, map, take, timeout } from 'rxjs/operators';
import { AppsOfTenantService } from './apps-of-tenant.service';
Expand Down Expand Up @@ -33,23 +33,24 @@ export class AppsOfTenantTabFactory implements TabFactory {
if (!tenantId) {
return [];
}
const apps = await this.appsOfTenant.getCachedAppsOfTenant(tenantId);
const apps = await this.appsOfTenant.getCachedAppsSupportingLogs(tenantId);
const tabs = this.mapAppsToTabs(apps, tenantId);
return tabs;
}

private mapAppsToTabs(apps: IApplication[], tenantId?: string): Tab[] {
return apps
.filter(
(app: IApplication & { manifest: { isolation: string } }) =>
app.type === 'MICROSERVICE' && (app.owner.tenant.id === tenantId || app.manifest.isolation === 'PER_TENANT')
)
.map((app) => {
return {
label: `Logs: ${app.name || app.contextPath || 'unknown'}`,
path: `tenants/${tenantId}/app-log/${app.id}`,
icon: 'file-text'
} as Tab;
});
private mapAppsToTabs(apps: IApplicationManagedObject[], tenantId: string): Tab[] {
const tabs = new Array<Tab>();
apps.forEach((app) => {
if (app.c8y_Status && app.c8y_Status.instances) {
Object.keys(app.c8y_Status.instances).forEach((instance, index) => {
tabs.push({
label: `Logs: ${app.name || 'unknown'} #${index + 1}`,
path: `tenants/${tenantId}/app-log/${app.applicationId}/${instance}`,
icon: 'file-text'
} as Tab);
});
}
});
return tabs;
}
}
38 changes: 19 additions & 19 deletions src/modules/tenant-app-logs/apps-of-tenant.service.ts
Original file line number Diff line number Diff line change
@@ -1,42 +1,42 @@
import { Injectable } from '@angular/core';
import { IApplication } from '@c8y/client';
import { IApplicationManagedObject } from '@models/application-mo';
import { FakeMicroserviceService } from '@services/fake-microservice.service';

@Injectable({
providedIn: 'root'
})
export class AppsOfTenantService {
private promiseMap = new Map<string, Promise<IApplication[]>>();
private promiseMapAppStatus = new Map<string, Promise<IApplicationManagedObject[]>>();
constructor(private credService: FakeMicroserviceService) {}

async getCachedAppsOfTenant(tenantId: string): Promise<IApplication[]> {
let cachedResponse = this.promiseMap.get(tenantId);
async getCachedAppsSupportingLogs(tenantId: string): Promise<IApplicationManagedObject[]> {
let cachedResponse = this.promiseMapAppStatus.get(tenantId);
if (!cachedResponse) {
this.promiseMap.clear();
cachedResponse = this.getAllAppsOfTenant(tenantId);
this.promiseMap.set(tenantId, cachedResponse);
this.promiseMapAppStatus.clear();
cachedResponse = this.getAppsSupportingLogs(tenantId);
this.promiseMapAppStatus.set(tenantId, cachedResponse);
}
const apps = await cachedResponse;
return apps;
}

private async getAllAppsOfTenant(tenantId: string): Promise<IApplication[]> {
const credentials = await this.credService.prepareCachedDummyMicroserviceForAllSubtenants();
const clients = await this.credService.createClients(credentials);
const client = clients.find((tmp) => tmp.core.tenant === tenantId);

if (!client) {
throw Error(`No client for tenant: ${tenantId} available.`);
}
const apps = new Array<IApplication>();
let response = await client.application.list({ pageSize: 100 });
private async getAppsSupportingLogs(tenantId: string): Promise<IApplicationManagedObject[]> {
const client = await this.credService.getClientForTenant(tenantId);
const apps = new Array<IApplicationManagedObject>();
let response = await client.inventory.list({
pageSize: 2000,
query: `type eq 'c8y_Application_*' and has(c8y_SupportedLogs)`
});
while (response.data.length) {
apps.push(...response.data);
apps.push(...(response.data as IApplicationManagedObject[]));
if (response.data.length < response.paging.pageSize) {
break;
}
response = await response.paging.next();
}
return apps;
const filteredApps = apps.filter(
(app) => app.c8y_SupportedLogs && Array.isArray(app.c8y_SupportedLogs) && app.c8y_SupportedLogs.includes('syslog')
);
return filteredApps;
}
}
48 changes: 18 additions & 30 deletions src/modules/tenant-app-logs/microservice-logs.service.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Injectable } from '@angular/core';
import { Client, IApplication, IManagedObject } from '@c8y/client';
import { Client, IApplication } from '@c8y/client';
import { IMicroserviceLog } from '@models/microservice-log';
import { FakeMicroserviceService } from '@services/fake-microservice.service';

Expand All @@ -9,37 +9,22 @@ import { FakeMicroserviceService } from '@services/fake-microservice.service';
export class MicroserviceLogsService {
constructor(private credService: FakeMicroserviceService) {}

async loadLog(appId: string, tenantId: string): Promise<{ app: IApplication; logs: IMicroserviceLog[] }> {
const client = await this.credService.getClientForTenant(tenantId);

const { data: app } = await client.application.detail(appId);

const { data: instanceDetails } = await client.inventory.list({ type: `c8y_Application_${app.id}` });
let logs: IMicroserviceLog[] = [];
if (instanceDetails.length) {
const instanceDetail = instanceDetails[0];
logs = await this.loadLogsFromInstanceDetails(client, appId, instanceDetail);
}

return { app, logs };
}

private async loadLogsFromInstanceDetails(
client: Client,
public async loadLogsAndAppForSpecificInstance(
tenantId: string,
appId: string,
instanceDetails: IManagedObject
): Promise<IMicroserviceLog[]> {
if (instanceDetails.c8y_Status && instanceDetails.c8y_Status.instances) {
const instances = Object.keys(instanceDetails.c8y_Status.instances);
const logs = await Promise.all(
instances.map((instanceName) => this.loadLogsForSpecificInstance(client, appId, instanceName))
);
return logs;
}
return [];
instanceName: string,
dateFrom?: string
): Promise<{ app: IApplication; log: IMicroserviceLog }> {
const client = await this.credService.getClientForTenant(tenantId);
return Promise.all([
this.loadLogForSpecificInstance(client, appId, instanceName, dateFrom),
client.application.detail(appId)
]).then(([log, app]) => {
return { log, app: app.data };
});
}

private async loadLogsForSpecificInstance(
private async loadLogForSpecificInstance(
client: Client,
appId: string,
instanceName: string,
Expand All @@ -51,6 +36,9 @@ export class MicroserviceLogsService {
const logResponse = await client.core.fetch(endpoint, {
headers: { Accept: 'application/vnd.com.nsn.cumulocity.applicationLogs+json;charset=UTF-8;ver=0.9' }
} as RequestInit);
if (logResponse.status !== 200) {
throw Error('Failed');
}
const json: IMicroserviceLog = await logResponse.json();
json.instanceName = instanceName;
return json;
Expand All @@ -59,7 +47,7 @@ export class MicroserviceLogsService {
async downloadLogFile(tenantId: string, appId: string, instanceName: string): Promise<void> {
const client = await this.credService.getClientForTenant(tenantId);
const dateFrom = new Date(0).toISOString();
const log = await this.loadLogsForSpecificInstance(client, appId, instanceName, dateFrom);
const log = await this.loadLogForSpecificInstance(client, appId, instanceName, dateFrom);
const logText = log.logs;
const blob = new File([logText], `${instanceName}.log`, { type: 'text/plain' });
const url = window.URL.createObjectURL(blob);
Expand Down
38 changes: 14 additions & 24 deletions src/modules/tenant-app-logs/tenant-app-logs.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,31 +9,21 @@

<div *ngIf="!loading; else loadingIndicator" class="card content-fullpage">
<div class="d-flex d-col fit-h fit-w">
<div class="card-block overflow-visible">
<div class="flex-row">
<div class="form-group">
<label for="exampleSelect">Instance name</label>
<div class="c8y-select-wrapper">
<select id="exampleSelect" class="form-control">
<option *ngFor="let log of logs" [selected]="log.instanceName === selectedLog.instanceName">{{ log.instanceName }}</option>
</select>
<span></span>
</div>
</div>
</div>
</div>
<div
class="card-block fit-h flex-grow"
>
<textarea
class="fit-h fit-w"
autocorrect="off"
autocapitalize="off"
spellcheck="false"
[ngModel]="selectedLog?.logs"
readonly
></textarea>
<div class="card-header separator">
<h4 class="card-title">{{selectedLog?.instanceName}}</h4>
</div>
<div
class="card-block fit-h flex-grow"
>
<textarea
class="fit-h fit-w"
autocorrect="off"
autocapitalize="off"
spellcheck="false"
[ngModel]="selectedLog?.logs"
readonly
></textarea>
</div>
</div>
</div>

Expand Down
32 changes: 12 additions & 20 deletions src/modules/tenant-app-logs/tenant-app-logs.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,30 +14,28 @@ import { MicroserviceLogsService } from './microservice-logs.service';
export class TenantAppLogsComponent implements OnDestroy {
loading = true;
appId: string;
instanceName: string;
tenantId: string;

app: IApplication;

logs: IMicroserviceLog[] = [];
selectedLog: IMicroserviceLog;
client: Client;

private paramSubs = new Subscription();

constructor(private route: ActivatedRoute, private msLogs: MicroserviceLogsService, private alert: AlertService) {
const appId$ = this.route.params.pipe(
filter((tmp) => tmp.appId),
map((tmp) => tmp.appId)
);
const currentRouteParams$ = this.route.params.pipe(filter((tmp) => tmp.appId && tmp.instanceName));
const tenantId$ = this.route.parent.params.pipe(
filter((tmp) => tmp.id),
map((tmp) => tmp.id)
);
this.paramSubs = combineLatest([appId$, tenantId$]).subscribe(([appId, tenantId]) => {
this.appId = appId;
this.tenantId = tenantId;
this.loadLog(appId, tenantId);
});
this.paramSubs = combineLatest([currentRouteParams$, tenantId$]).subscribe(
([{ appId, instanceName }, tenantId]) => {
this.appId = appId;
this.tenantId = tenantId;
this.loadLog(tenantId, appId, instanceName);
}
);
}

ngOnDestroy(): void {
Expand All @@ -46,20 +44,14 @@ export class TenantAppLogsComponent implements OnDestroy {
}
}

async loadLog(appId: string, tenantId: string): Promise<void> {
async loadLog(tenantId: string, appId: string, instanceName: string): Promise<void> {
this.loading = true;
try {
const { app, logs } = await this.msLogs.loadLog(appId, tenantId);
const { app, log } = await this.msLogs.loadLogsAndAppForSpecificInstance(tenantId, appId, instanceName);
this.app = app;
this.logs = logs;
if (logs.length) {
this.selectedLog = this.logs[0];
} else {
this.alert.warning(`No logs found for app ${appId}`);
}
this.selectedLog = log;
} catch (e) {
this.app = undefined;
this.logs = [];
this.selectedLog = undefined;
this.alert.danger(`Failed to load logs for app: ${appId}`);
}
Expand Down
2 changes: 1 addition & 1 deletion src/modules/tenant-app-logs/tenant-app-logs.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { HOOK_MICROSERVICE_ROLE } from '@services/fake-microservice.service';
{
provide: HOOK_ONCE_ROUTE,
useValue: {
path: 'app-log/:appId',
path: 'app-log/:appId/:instanceName',
context: ViewContext.Tenant,
component: TenantAppLogsComponent,
tabs: []
Expand Down
Loading

0 comments on commit e133e7a

Please sign in to comment.