Skip to content

Commit ae08c94

Browse files
committed
Show logs for all containers
1 parent 9a8e237 commit ae08c94

File tree

10 files changed

+232
-159
lines changed

10 files changed

+232
-159
lines changed

backend/apps/common/routes/get.py

Lines changed: 58 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
from flask import request
2-
31
from kubeflow.kubeflow.crud_backend import api, logging
42

53
from .. import utils, versions
64
from . import bp
75

86
log = logging.getLogger(__name__)
97

8+
KSERVE_CONTAINER = "kserve-container"
9+
1010

1111
@bp.route("/api/namespaces/<namespace>/inferenceservices")
1212
def get_inference_services(namespace):
@@ -21,39 +21,71 @@ def get_inference_services(namespace):
2121
def get_inference_service(namespace, name):
2222
inference_service = api.get_custom_rsrc(**versions.inference_service_gvk(),
2323
namespace=namespace, name=name)
24-
if request.args.get("logs", "false") == "true":
25-
# find the logs
26-
return api.success_response(
27-
"serviceLogs", get_inference_service_logs(inference_service)
28-
)
2924

3025
return api.success_response("inferenceService", inference_service)
3126

3227

33-
def get_inference_service_logs(svc):
34-
namespace = svc["metadata"]["namespace"]
35-
components = request.args.getlist("component")
28+
@bp.route("/api/namespaces/<namespace>/inferenceservices/<name>/components/<component>/pods/containers")
29+
def get_inference_service_containers(namespace: str, name: str, component: str):
30+
"""Get all containers and init-containers for the latest pod of the given component.
31+
32+
The kserve-container will always be the first in the list if it exists
33+
34+
Return:
35+
{
36+
"containers": ["kserve-container", "container2", ...]
37+
}
38+
"""
39+
inference_service = api.get_custom_rsrc(**versions.inference_service_gvk(),
40+
namespace=namespace, name=name)
41+
42+
latest_pod = utils.get_component_latest_pod(inference_service, component)
43+
44+
if latest_pod is None:
45+
return api.failed_response(f"couldn't find latest pod for component: {component}", 404)
3646

37-
log.info(components)
47+
containers = []
48+
for container in latest_pod.spec.init_containers:
49+
containers.append(container.name)
3850

39-
# dictionary{component: [pod-names]}
40-
component_pods_dict = utils.get_inference_service_pods(svc, components)
51+
for container in latest_pod.spec.containers:
52+
containers.append(container.name)
53+
54+
# Make kserve-container always the first container in the list if it exists
55+
try:
56+
idx = containers.index(KSERVE_CONTAINER)
57+
containers.insert(0, containers.pop(idx))
58+
except ValueError:
59+
# kserve-container not found in list
60+
pass
61+
62+
return api.success_response("containers", containers)
63+
64+
65+
@bp.route("/api/namespaces/<namespace>/inferenceservices/<name>/components/<component>/pods/containers"
66+
"/<container>/logs")
67+
def get_container_logs(namespace: str, name: str, component: str, container: str):
68+
"""Get logs for a particular container inside the latest pod of the given component
69+
70+
Logs are split on newline and returned as an array of lines
71+
72+
Return:
73+
{
74+
"logs": ["log\n", "text\n", ...]
75+
}
76+
"""
77+
inference_service = api.get_custom_rsrc(**versions.inference_service_gvk(),
78+
namespace=namespace, name=name)
79+
namespace = inference_service["metadata"]["namespace"]
4180

42-
if len(component_pods_dict.keys()) == 0:
43-
return {}
81+
latest_pod = utils.get_component_latest_pod(inference_service, component)
82+
if latest_pod is None:
83+
return api.failed_response(f"couldn't find latest pod for component: {component}", 404)
4484

45-
resp = {}
46-
logging.info("Component pods: %s", component_pods_dict)
47-
for component, pods in component_pods_dict.items():
48-
if component not in resp:
49-
resp[component] = []
85+
logs = api.get_pod_logs(namespace, latest_pod.metadata.name, container, auth=False)
86+
logs = logs.split("\n")
5087

51-
for pod in pods:
52-
logs = api.get_pod_logs(namespace, pod, "kserve-container",
53-
auth=False)
54-
resp[component].append({"podName": pod,
55-
"logs": logs.split("\n")})
56-
return resp
88+
return api.success_response("logs", logs)
5789

5890

5991
@bp.route("/api/namespaces/<namespace>/knativeServices/<name>")

backend/apps/common/utils.py

Lines changed: 33 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import os
2+
from typing import Dict, Union
23

34
from kubeflow.kubeflow.crud_backend import api, helpers, logging
45

56
log = logging.getLogger(__name__)
67

78
KNATIVE_REVISION_LABEL = "serving.knative.dev/revision"
9+
LATEST_CREATED_REVISION = "latestCreatedRevision"
810
FILE_ABS_PATH = os.path.abspath(os.path.dirname(__file__))
911

1012
INFERENCESERVICE_TEMPLATE_YAML = os.path.join(
@@ -21,71 +23,54 @@ def load_inference_service_template(**kwargs):
2123
return helpers.load_param_yaml(INFERENCESERVICE_TEMPLATE_YAML, **kwargs)
2224

2325

24-
# helper functions for accessing the logs of an InferenceService
25-
def get_inference_service_pods(svc, components=[]):
26-
"""
27-
Return a dictionary with (endpoint, component) keys,
28-
i.e. ("default", "predictor") and a list of pod names as values
26+
def get_component_latest_pod(svc: Dict, component: str) -> Union[api.client.V1Pod, None]:
27+
"""Get pod of the latest Knative revision for the given component.
28+
29+
Return:
30+
Latest pod: k8s V1Pod
2931
"""
3032
namespace = svc["metadata"]["namespace"]
3133

32-
# dictionary{revisionName: (endpoint, component)}
33-
revisions_dict = get_components_revisions_dict(components, svc)
34+
latest_revision = get_component_latest_revision(svc, component)
3435

35-
if len(revisions_dict.keys()) == 0:
36-
return {}
36+
if latest_revision is None:
37+
return None
3738

3839
pods = api.list_pods(namespace, auth=False).items
39-
component_pods_dict = {}
40+
4041
for pod in pods:
41-
for revision in revisions_dict:
42-
if KNATIVE_REVISION_LABEL not in pod.metadata.labels:
43-
continue
42+
if KNATIVE_REVISION_LABEL not in pod.metadata.labels:
43+
continue
44+
45+
if pod.metadata.labels[KNATIVE_REVISION_LABEL] != latest_revision:
46+
continue
4447

45-
if pod.metadata.labels[KNATIVE_REVISION_LABEL] != revision:
46-
continue
48+
return pod
4749

48-
component = revisions_dict[revision]
49-
curr_pod_names = component_pods_dict.get(component, [])
50-
curr_pod_names.append(pod.metadata.name)
51-
component_pods_dict[component] = curr_pod_names
50+
log.info(f"No pods are found for inference service: {svc['metadata']['name']}")
5251

53-
if len(component_pods_dict.keys()) == 0:
54-
log.info("No pods are found for inference service: %s",
55-
svc["metadata"]["name"])
52+
return None
5653

57-
return component_pods_dict
5854

55+
def get_component_latest_revision(svc: Dict, component: str) -> Union[str, None]:
56+
"""Get the name of the latest created knative revision for the given component.
5957
60-
# FIXME(elikatsis,kimwnasptd): Change the logic of this function according to
61-
# https://github.com/arrikto/dev/issues/867
62-
def get_components_revisions_dict(components, svc):
63-
"""
64-
Return a dictionary{revisionId: component}
58+
Return:
59+
Latest Created Knative Revision: str
6560
"""
6661
status = svc["status"]
67-
revisions_dict = {}
6862

69-
for component in components:
70-
if "components" not in status:
71-
log.info("Component '%s' not in inference service '%s'",
72-
component, svc["metadata"]["name"])
73-
continue
74-
75-
if component not in status["components"]:
76-
log.info("Component '%s' not in inference service '%s'",
77-
component, svc["metadata"]["name"])
78-
continue
63+
if "components" not in status:
64+
log.info(f"Components field not found in status object of {svc['metadata']['name']}")
65+
return None
7966

80-
if "latestReadyRevision" in status["components"][component]:
81-
revision = status["components"][component]["latestReadyRevision"]
67+
if component not in status["components"]:
68+
log.info(f"Component {component} not found in inference service {svc['metadata']['name']}")
69+
return None
8270

83-
revisions_dict[revision] = component
71+
if LATEST_CREATED_REVISION in status["components"][component]:
72+
return status["components"][component][LATEST_CREATED_REVISION]
8473

85-
if len(revisions_dict.keys()) == 0:
86-
log.info(
87-
"No revisions found for the inference service's components: %s",
88-
svc["metadata"]["name"],
89-
)
74+
log.info(f"No {LATEST_CREATED_REVISION} found for the {component} in {svc['metadata']['name']}")
9075

91-
return revisions_dict
76+
return None

frontend/src/app/pages/server-info/logs/logs-viewer/logs-viewer.component.html

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,3 @@
1-
<lib-heading-row [heading]="[heading]" [subHeading]="[subHeading]">
2-
</lib-heading-row>
3-
41
<cdk-virtual-scroll-viewport itemSize="18" class="logs-viewer">
52
<div *cdkVirtualFor="let entry of logs; let index = index" class="log-entry">
63
<span class="number">{{ index }}</span>

frontend/src/app/pages/server-info/logs/logs-viewer/logs-viewer.component.ts

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,10 @@ import {
22
Component,
33
Input,
44
ViewChild,
5-
NgZone,
6-
SimpleChanges,
7-
OnChanges,
85
HostBinding,
9-
ElementRef,
106
AfterViewInit,
117
} from '@angular/core';
128
import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling';
13-
import { take } from 'rxjs/operators';
149

1510
@Component({
1611
selector: 'app-logs-viewer',
@@ -22,8 +17,6 @@ export class LogsViewerComponent implements AfterViewInit {
2217
@ViewChild(CdkVirtualScrollViewport, { static: true })
2318
viewPort: CdkVirtualScrollViewport;
2419

25-
@Input() heading = 'Logs';
26-
@Input() subHeading = 'tit';
2720
@Input() height = '400px';
2821
@Input()
2922
set logs(newLogs: string[]) {
Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
11
import { NgModule } from '@angular/core';
22
import { CommonModule } from '@angular/common';
33
import { ScrollingModule } from '@angular/cdk/scrolling';
4+
import { MatTabsModule } from '@angular/material/tabs';
45
import { LogsViewerComponent } from './logs-viewer.component';
56
import { HeadingSubheadingRowModule } from 'kubeflow';
67

78
@NgModule({
89
declarations: [LogsViewerComponent],
9-
imports: [CommonModule, ScrollingModule, HeadingSubheadingRowModule],
10+
imports: [
11+
CommonModule,
12+
MatTabsModule,
13+
ScrollingModule,
14+
HeadingSubheadingRowModule,
15+
],
1016
exports: [LogsViewerComponent],
1117
})
1218
export class LogsViewerModule {}
Lines changed: 31 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,41 @@
1-
<lib-loading-spinner *ngIf="!logsRequestCompleted"></lib-loading-spinner>
2-
3-
<!--if no logs are present at all then show a warning message-->
4-
<ng-container *ngIf="logsRequestCompleted && !logsNotEmpty">
1+
<ng-container>
52
<lib-panel class="lib-panel">
6-
No logs were found for this InferenceService.
3+
Logs are shown for the latest created revision
74
</lib-panel>
85
</ng-container>
96

10-
<!--logs loaded successfully from the backend-->
11-
<ng-container *ngIf="logsRequestCompleted && logsNotEmpty">
12-
<ng-container *ngIf="loadErrorMsg">
7+
<ng-container>
8+
<mat-tab-group
9+
#componentTabGroup
10+
(selectedTabChange)="componentTabChange(componentTabGroup.selectedIndex)"
11+
class="page-placement"
12+
dynamicHeight
13+
animationDuration="0ms"
14+
>
15+
<mat-tab *ngFor="let component of components" label="{{ component | titlecase }}">
16+
<ng-template matTabContent>
17+
<mat-tab-group
18+
#containerTabGroup
19+
(selectedTabChange)="containerTabChange(containerTabGroup.selectedIndex)"
20+
class="page-placement"
21+
dynamicHeight
22+
animationDuration="0ms"
23+
>
24+
<mat-tab *ngFor="let container of isvcComponents[component].containers" label="{{ container }}"></mat-tab>
25+
</mat-tab-group>
26+
</ng-template>
27+
</mat-tab>
28+
</mat-tab-group>
29+
30+
<lib-loading-spinner *ngIf="!logsRequestCompleted"></lib-loading-spinner>
31+
32+
<ng-container *ngIf="loadErrorMsg.length">
1333
<lib-panel>
1434
{{ loadErrorMsg }}
1535
</lib-panel>
1636
</ng-container>
1737

18-
<div
19-
*ngFor="let podLogs of currLogs?.predictor; trackBy: logsTrackFn"
20-
class="margin-bottom"
21-
>
22-
<app-logs-viewer
23-
heading="Predictor:"
24-
[subHeading]="podLogs.podName"
25-
[logs]="podLogs.logs"
26-
></app-logs-viewer>
27-
</div>
28-
29-
<div
30-
*ngFor="let podLogs of currLogs?.transformer; trackBy: logsTrackFn"
31-
class="margin-bottom"
32-
>
33-
<app-logs-viewer
34-
heading="Transformer:"
35-
[subHeading]="podLogs.podName"
36-
[logs]="podLogs.logs"
37-
></app-logs-viewer>
38-
</div>
39-
40-
<div
41-
*ngFor="let podLogs of currLogs?.explainer; trackBy: logsTrackFn"
42-
class="margin-bottom"
43-
>
44-
<app-logs-viewer
45-
heading="Explainer:"
46-
[subHeading]="podLogs.podName"
47-
[logs]="podLogs.logs"
48-
></app-logs-viewer>
49-
</div>
38+
<app-logs-viewer *ngIf="currLogs.length"
39+
[logs]="currLogs"
40+
></app-logs-viewer>
5041
</ng-container>

0 commit comments

Comments
 (0)