jwa(front): Add LOGS tab to the notebook details page (kubeflow/kubeflow#6779)

* jwa(back): Get a notebook's logs

Add logic in the backend to fetch the logs of a notebook's
underlying pod.

Signed-off-by: Orfeas Kourkakis <orfeas@arrikto.com>

* jwa(front): Add LOGS tab to Notebook details page

 - Create Logs tab to show the logs of the notebook's underlying
   pod.
 - Create Logs Viewer component in Kubeflow common library exactly
   like the one we have in MWA.

Signed-off-by: Orfeas Kourkakis <orfeas@arrikto.com>

Signed-off-by: Orfeas Kourkakis <orfeas@arrikto.com>
This commit is contained in:
Orfeas Kourkakis 2022-11-29 17:28:42 +02:00 committed by GitHub
parent 7c9be4729a
commit 03c240a5d5
12 changed files with 207 additions and 1 deletions

View File

@ -10,7 +10,7 @@ def list_pods(namespace, auth=True, label_selector = None):
def get_pod_logs(namespace, pod, container, auth=True):
if auth:
authz.ensure_authorized("read", "", "v1", "pods", namespace, "logs")
authz.ensure_authorized("get", "", "v1", "pods", namespace, "log")
return v1_core.read_namespaced_pod_log(
namespace=namespace, name=pod, container=container

View File

@ -92,6 +92,9 @@ export * from './lib/logs-viewer/logs-viewer.component';
export * from './lib/help-popover/help-popover.component';
export * from './lib/help-popover/help-popover.module';
export * from './lib/logs-viewer/logs-viewer.module';
export * from './lib/logs-viewer/logs-viewer.component';
export * from './lib/content-list-item/content-list-item.component';
export * from './lib/content-list-item/content-list-item.module';

View File

@ -78,6 +78,15 @@ def get_notebook_pod(notebook_name, namespace):
@bp.route("/api/namespaces/<namespace>/notebooks/<notebook_name>/pod/<pod_name>/logs")
def get_pod_logs(namespace, notebook_name, pod_name):
container = notebook_name
logs = api.get_pod_logs(namespace, pod_name, container)
return api.success_response(
"logs", logs.split("\n"),
)
@bp.route("/api/gpus")
def get_gpu_vendors():

View File

@ -0,0 +1,18 @@
<lib-loading-spinner *ngIf="logsLoading"></lib-loading-spinner>
<!--if no logs are present at all then show a warning message-->
<ng-container *ngIf="!logsLoading && logsEmpty">
<lib-panel *ngIf="!pod"> No logs were found for this Notebook. </lib-panel>
<lib-panel *ngIf="pod" icon="error" color="warn">
Error: {{ loadErrorMsg }}
</lib-panel>
</ng-container>
<ng-container *ngIf="!logsLoading && !logsEmpty">
<lib-logs-viewer
class="logs-viewer"
heading="Notebook Pod Logs"
[subHeading]="''"
[logs]="logs"
></lib-logs-viewer>
</ng-container>

View File

@ -0,0 +1,9 @@
lib-panel {
display: block;
margin-top: 1rem;
}
.logs-viewer {
margin-bottom: 2rem;
margin-top: 1rem;
}

View File

@ -0,0 +1,38 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { LoadingSpinnerModule, PollerService } from 'kubeflow';
import { JWABackendService } from 'src/app/services/backend.service';
import { of } from 'rxjs';
import { LogsComponent } from './logs.component';
const JWABackendServiceStub: Partial<JWABackendService> = {
getPodDefaults: () => of(),
};
const PollerServiceStub: Partial<PollerService> = {
exponential: () => of(),
};
describe('LogsComponent', () => {
let component: LogsComponent;
let fixture: ComponentFixture<LogsComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [LogsComponent],
providers: [
{ provide: JWABackendService, useValue: JWABackendServiceStub },
{ provide: PollerService, useValue: PollerServiceStub },
],
imports: [LoadingSpinnerModule],
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(LogsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,85 @@
import { Component, Input, OnDestroy, OnInit } from '@angular/core';
import { JWABackendService } from 'src/app/services/backend.service';
import { Subscription } from 'rxjs';
import { PollerService } from 'kubeflow';
import { V1Pod } from '@kubernetes/client-node';
@Component({
selector: 'app-logs',
templateUrl: './logs.component.html',
styleUrls: ['./logs.component.scss'],
})
export class LogsComponent implements OnInit, OnDestroy {
public goToBottom = true;
public logs: string[];
public logsRequestCompleted = false;
public loadErrorMsg = '';
private pollingSub: Subscription;
private prvPod: V1Pod;
@Input() podRequestCompleted = false;
@Input()
set pod(pod: V1Pod) {
this.prvPod = pod;
if (!pod) {
this.logs = null;
this.logsRequestCompleted = true;
return;
}
this.poll(pod);
}
get pod() {
return this.prvPod;
}
private poll(pod: V1Pod) {
if (this.pollingSub) {
this.pollingSub.unsubscribe();
}
const request = this.backend.getPodLogs(pod);
this.pollingSub = this.poller.exponential(request).subscribe(
logs => {
this.logs = logs;
this.logsRequestCompleted = true;
this.loadErrorMsg = '';
},
error => {
this.logs = null;
this.logsRequestCompleted = true;
this.loadErrorMsg = error;
},
);
}
get logsLoading(): boolean {
if (!this.podRequestCompleted) {
return true;
} else if (this.podRequestCompleted && !this.logsRequestCompleted) {
return true;
} else {
return false;
}
}
get logsEmpty(): boolean {
return this.logs ? false : true;
}
constructor(
public backend: JWABackendService,
public poller: PollerService,
) {}
ngOnInit() {}
ngOnDestroy() {
if (this.pollingSub) {
this.pollingSub.unsubscribe();
}
}
}

View File

@ -0,0 +1,20 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { LogsComponent } from './logs.component';
import {
HeadingSubheadingRowModule,
KubeflowModule,
LogsViewerModule,
} from 'kubeflow';
@NgModule({
declarations: [LogsComponent],
imports: [
CommonModule,
KubeflowModule,
HeadingSubheadingRowModule,
LogsViewerModule,
],
exports: [LogsComponent],
})
export class LogsModule {}

View File

@ -32,6 +32,16 @@
></app-overview>
</ng-template>
</mat-tab>
<mat-tab label="LOGS">
<ng-template matTabContent>
<app-logs
[pod]="notebookPod"
[podRequestCompleted]="podRequestCompleted"
></app-logs>
</ng-template>
</mat-tab>
</mat-tab-group>
</ng-container>
</div>

View File

@ -7,6 +7,7 @@ import { MatIconModule } from '@angular/material/icon';
import { MatDividerModule } from '@angular/material/divider';
import { NotebookPageComponent } from './notebook-page.component';
import { OverviewModule } from './overview/overview.module';
import { LogsModule } from './logs/logs.module';
import { RouterModule } from '@angular/router';
@NgModule({
@ -19,6 +20,7 @@ import { RouterModule } from '@angular/router';
MatTabsModule,
OverviewModule,
MatProgressSpinnerModule,
LogsModule,
RouterModule,
],
})

View File

@ -76,6 +76,17 @@ export class JWABackendService extends BackendService {
);
}
public getPodLogs(pod: V1Pod): Observable<string[]> {
const namespace = pod.metadata.namespace;
const notebookName = pod.metadata.labels['notebook-name'];
const podName = pod.metadata.name;
const url = `api/namespaces/${namespace}/notebooks/${notebookName}/pod/${podName}/logs`;
return this.http.get<JWABackendResponse>(url).pipe(
catchError(error => this.handleErrorExtended(error, [404, 400])),
map((resp: JWABackendResponse) => resp.logs),
);
}
public getConfig(): Observable<Config> {
const url = `api/config`;

View File

@ -8,6 +8,7 @@ import { PvcResponseObject } from './volume';
export interface JWABackendResponse extends BackendResponse {
notebook?: NotebookRawObject;
notebooks?: NotebookResponseObject[];
logs: string[];
pvcs?: PvcResponseObject[];
config?: Config;
poddefaults?: PodDefault[];