Skip to content

Commit c83a4b5

Browse files
committed
Implement runtime-configurable Grafana endpoints and database names
Signed-off-by: Harshit Nayan <[email protected]> updated README with grafana config Signed-off-by: Harshit Nayan <[email protected]>
1 parent 6be7dc1 commit c83a4b5

File tree

13 files changed

+245
-38
lines changed

13 files changed

+245
-38
lines changed

README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,30 @@ The following is a list of environment variables that can be set for any web app
5252
| CSRF_SAMESITE | Strict| Controls the [SameSite value](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#SameSite) of the CSRF cookie |
5353
| USERID_HEADER | kubeflow-userid | Header in each request that will contain the username of the logged in user |
5454
| USERID_PREFIX | "" | Prefix to remove from the `USERID_HEADER` value to extract the logged in user name |
55+
| GRAFANA_PREFIX | /grafana | Controls the Grafana endpoint prefix for metrics dashboards |
56+
| GRAFANA_CPU_MEMORY_DB | db/knative-serving-revision-cpu-and-memory-usage | Grafana dashboard name for CPU and memory metrics |
57+
| GRAFANA_HTTP_REQUESTS_DB | db/knative-serving-revision-http-requests | Grafana dashboard name for HTTP request metrics |
58+
59+
## Grafana Configuration
60+
61+
The application supports runtime configuration of Grafana endpoints and dashboard names, allowing you to use custom Grafana instances and dashboard configurations without rebuilding the application.
62+
63+
### Configuration API
64+
65+
You can verify your grafana configuration by accessing the `/api/config` endpoint:
66+
67+
```bash
68+
curl http://your-app-url/api/config
69+
```
70+
71+
Expected response:
72+
```json
73+
{
74+
"grafanaPrefix": "/custom-grafana",
75+
"grafanaCpuMemoryDb": "db/custom-cpu-memory-dashboard",
76+
"grafanaHttpRequestsDb": "db/custom-http-requests-dashboard"
77+
}
78+
```
5579

5680
## Development
5781

backend/apps/v1beta1/routes/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@
44

55
bp = Blueprint("default_routes", __name__)
66

7-
from . import post, put # noqa: F401, E402
7+
from . import get, post, put # noqa: F401, E402

backend/apps/v1beta1/routes/get.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
"""GET routes of the backend."""
2+
3+
import os
4+
from flask import jsonify
5+
6+
from kubeflow.kubeflow.crud_backend import api, logging
7+
8+
from . import bp
9+
10+
log = logging.getLogger(__name__)
11+
12+
13+
@bp.route("/api/config", methods=["GET"])
14+
def get_config():
15+
"""Handle retrieval of application configuration."""
16+
try:
17+
config = {
18+
"grafanaPrefix": os.environ.get("GRAFANA_PREFIX", "/grafana"),
19+
"grafanaCpuMemoryDb": os.environ.get("GRAFANA_CPU_MEMORY_DB", "db/knative-serving-revision-cpu-and-memory-usage"),
20+
"grafanaHttpRequestsDb": os.environ.get("GRAFANA_HTTP_REQUESTS_DB", "db/knative-serving-revision-http-requests")
21+
}
22+
23+
log.info("Configuration requested: %s", config)
24+
return jsonify(config)
25+
except Exception as e:
26+
log.error("Error retrieving configuration: %s", str(e))
27+
return api.error_response("message", "Failed to retrieve configuration"), 500

backend/entrypoint.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@
1515
PREFIX = os.environ.get("APP_PREFIX", "/")
1616
APP_VERSION = os.environ.get("APP_VERSION", "v1beta1")
1717

18+
# Grafana configuration
19+
GRAFANA_PREFIX = os.environ.get("GRAFANA_PREFIX", "/grafana")
20+
GRAFANA_CPU_MEMORY_DB = os.environ.get("GRAFANA_CPU_MEMORY_DB", "db/knative-serving-revision-cpu-and-memory-usage")
21+
GRAFANA_HTTP_REQUESTS_DB = os.environ.get("GRAFANA_HTTP_REQUESTS_DB", "db/knative-serving-revision-http-requests")
22+
1823
cfg = config.get_config(BACKEND_MODE)
1924
cfg.PREFIX = PREFIX
2025
cfg.APP_VERSION = APP_VERSION

config/base/kustomization.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ images:
1414
configMapGenerator:
1515
- literals:
1616
- APP_DISABLE_AUTH="True"
17+
- GRAFANA_PREFIX="/grafana"
18+
- GRAFANA_CPU_MEMORY_DB="db/knative-serving-revision-cpu-and-memory-usage"
19+
- GRAFANA_HTTP_REQUESTS_DB="db/knative-serving-revision-http-requests"
1720
name: kserve-models-web-app-config
1821
apiVersion: kustomize.config.k8s.io/v1beta1
1922
kind: Kustomization

frontend/src/app/app.module.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import {
1212
MatSnackBarConfig,
1313
MAT_SNACK_BAR_DEFAULT_OPTIONS,
1414
} from '@angular/material/snack-bar';
15+
import { ConfigService } from './services/config.service';
16+
import { take } from 'rxjs/operators';
1517

1618
/**
1719
* MAT_SNACK_BAR_DEFAULT_OPTIONS values can be found
@@ -39,6 +41,12 @@ const MwaSnackBarConfig: MatSnackBarConfig = {
3941
useFactory: () => configureAce,
4042
multi: true,
4143
},
44+
{
45+
provide: APP_INITIALIZER,
46+
useFactory: (configService: ConfigService) => () => configService.getConfig().pipe(take(1)).toPromise(),
47+
deps: [ConfigService],
48+
multi: true,
49+
},
4250
],
4351
bootstrap: [AppComponent],
4452
})

frontend/src/app/pages/server-info/metrics/metrics.component.ts

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,56 @@
11
import { Component, OnInit, Input } from '@angular/core';
22
import { GrafanaService } from 'src/app/services/grafana.service';
3+
import { ConfigService } from 'src/app/services/config.service';
34
import { InferenceServiceStatus } from 'src/app/types/kfserving/v1beta1';
45
import { GrafanaIframeConfig } from 'src/app/types/grafana';
6+
import { AppConfig } from 'src/app/types/config';
57

68
@Component({
79
selector: 'app-metrics',
810
templateUrl: './metrics.component.html',
911
styleUrls: ['./metrics.component.scss'],
1012
})
11-
export class MetricsComponent {
13+
export class MetricsComponent implements OnInit {
1214
public configs = {
1315
predictor: [],
1416
transformer: [],
1517
explainer: [],
1618
};
1719

20+
private cpuMemoryDb: string;
21+
private httpRequestsDb: string;
22+
1823
@Input() namespace: string;
1924

25+
constructor(private configService: ConfigService) {}
26+
27+
ngOnInit(): void {
28+
this.configService.getConfig().subscribe(
29+
config => {
30+
this.cpuMemoryDb = config.grafanaCpuMemoryDb;
31+
this.httpRequestsDb = config.grafanaHttpRequestsDb;
32+
// Regenerate configs if status is already set
33+
if (this.statusPrv) {
34+
['predictor', 'transformer', 'explainer'].forEach(comp => {
35+
this.configs[comp] = this.generateComponentGraphsConfig(comp);
36+
});
37+
}
38+
},
39+
error => {
40+
console.error('Failed to load configuration for MetricsComponent:', error);
41+
// Use default database names as fallback
42+
this.cpuMemoryDb = 'db/knative-serving-revision-cpu-and-memory-usage';
43+
this.httpRequestsDb = 'db/knative-serving-revision-http-requests';
44+
// Regenerate configs if status is already set
45+
if (this.statusPrv) {
46+
['predictor', 'transformer', 'explainer'].forEach(comp => {
47+
this.configs[comp] = this.generateComponentGraphsConfig(comp);
48+
});
49+
}
50+
}
51+
);
52+
}
53+
2054
@Input()
2155
set status(s: InferenceServiceStatus) {
2256
this.statusPrv = s;
@@ -104,11 +138,12 @@ export class MetricsComponent {
104138
configuration: string,
105139
revision: string,
106140
): GrafanaIframeConfig {
141+
const dashboardUri = this.httpRequestsDb || 'db/knative-serving-revision-http-requests';
107142
return this.generateRevisionGraphConfig(
108143
panelId,
109144
450,
110145
200,
111-
'db/knative-serving-revision-http-requests',
146+
dashboardUri, // Use configurable value with fallback
112147
configuration,
113148
revision,
114149
);
@@ -119,11 +154,12 @@ export class MetricsComponent {
119154
configuration: string,
120155
revision: string,
121156
): GrafanaIframeConfig {
157+
const dashboardUri = this.cpuMemoryDb || 'db/knative-serving-revision-cpu-and-memory-usage';
122158
return this.generateRevisionGraphConfig(
123159
panelId,
124160
450,
125161
200,
126-
'db/knative-serving-revision-cpu-and-memory-usage',
162+
dashboardUri, // Use configurable value with fallback
127163
configuration,
128164
revision,
129165
);

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

Lines changed: 40 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,10 @@ import {
1515
Status,
1616
} from 'kubeflow';
1717
import { MWABackendService } from 'src/app/services/backend.service';
18+
import { ConfigService } from 'src/app/services/config.service';
1819
import { isEqual } from 'lodash';
1920
import { generateDeleteConfig } from '../index/config';
2021
import { HttpClient } from '@angular/common/http';
21-
import { environment } from 'src/environments/environment';
2222
import { InferenceServiceK8s } from 'src/app/types/kfserving/v1beta1';
2323
import {
2424
InferenceServiceOwnedObjects,
@@ -78,6 +78,7 @@ export class ServerInfoComponent implements OnInit, OnDestroy {
7878
private backend: MWABackendService,
7979
private confirmDialog: ConfirmDialogService,
8080
private snack: SnackBarService,
81+
private configService: ConfigService,
8182
) {}
8283

8384
ngOnInit() {
@@ -95,31 +96,16 @@ export class ServerInfoComponent implements OnInit, OnDestroy {
9596

9697
// don't show a METRICS tab if Grafana is not exposed
9798
console.log($localize`Checking if Grafana endpoint is exposed`);
98-
const grafanaApi = environment.grafanaPrefix + '/api/search';
99-
100-
this.http
101-
.get(grafanaApi)
102-
.pipe(timeout(1000))
103-
.subscribe({
104-
next: resp => {
105-
if (!Array.isArray(resp)) {
106-
console.log(
107-
$localize`Response from the Grafana endpoint was not as expected.`,
108-
);
109-
this.grafanaFound = false;
110-
return;
111-
}
112-
113-
console.log(
114-
$localize`Grafana endpoint detected. Will expose a metrics tab.`,
115-
);
116-
this.grafanaFound = true;
117-
},
118-
error: () => {
119-
console.log($localize`Could not detect a Grafana endpoint.`);
120-
this.grafanaFound = false;
121-
},
122-
});
99+
this.configService.getConfig().subscribe(
100+
config => {
101+
this.checkGrafanaAvailability(config.grafanaPrefix);
102+
},
103+
error => {
104+
console.error('Failed to load configuration for ServerInfoComponent:', error);
105+
// Use default prefix as fallback
106+
this.checkGrafanaAvailability('/grafana');
107+
}
108+
);
123109
}
124110

125111
ngOnDestroy() {
@@ -323,6 +309,34 @@ export class ServerInfoComponent implements OnInit, OnDestroy {
323309
}
324310
}
325311

312+
private checkGrafanaAvailability(grafanaPrefix: string): void {
313+
const grafanaApi = grafanaPrefix + '/api/search';
314+
315+
this.http
316+
.get(grafanaApi)
317+
.pipe(timeout(1000))
318+
.subscribe({
319+
next: resp => {
320+
if (!Array.isArray(resp)) {
321+
console.log(
322+
$localize`Response from the Grafana endpoint was not as expected.`,
323+
);
324+
this.grafanaFound = false;
325+
return;
326+
}
327+
328+
console.log(
329+
$localize`Grafana endpoint detected. Will expose a metrics tab.`,
330+
);
331+
this.grafanaFound = true;
332+
},
333+
error: () => {
334+
console.log($localize`Could not detect a Grafana endpoint.`);
335+
this.grafanaFound = false;
336+
},
337+
});
338+
}
339+
326340
private isRawDeployment(svc: InferenceServiceK8s): boolean {
327341
const annotations = svc.metadata?.annotations || {};
328342

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { Injectable } from '@angular/core';
2+
import { HttpClient } from '@angular/common/http';
3+
import { BackendService, SnackBarService } from 'kubeflow';
4+
import { ReplaySubject, Observable, of } from 'rxjs';
5+
import { catchError, map, tap } from 'rxjs/operators';
6+
import { AppConfig } from '../types/config';
7+
8+
@Injectable({
9+
providedIn: 'root',
10+
})
11+
export class ConfigService extends BackendService {
12+
private config$ = new ReplaySubject<AppConfig>(1);
13+
private configLoaded = false;
14+
15+
constructor(public http: HttpClient, public snack: SnackBarService) {
16+
super(http, snack);
17+
this.loadConfig();
18+
}
19+
20+
private loadConfig(): void {
21+
console.log('Loading application configuration');
22+
23+
this.http.get<AppConfig>('api/config').pipe(
24+
catchError(error => {
25+
console.warn('Failed to load config from backend, using defaults:', error);
26+
return of(this.getDefaultConfig());
27+
}),
28+
tap(config => {
29+
console.log('Configuration loaded:', config);
30+
this.configLoaded = true;
31+
})
32+
).subscribe(
33+
config => this.config$.next(config),
34+
error => {
35+
console.error('Error loading configuration:', error);
36+
this.config$.next(this.getDefaultConfig());
37+
this.configLoaded = true;
38+
}
39+
);
40+
}
41+
42+
private getDefaultConfig(): AppConfig {
43+
return {
44+
grafanaPrefix: '/grafana',
45+
grafanaCpuMemoryDb: 'db/knative-serving-revision-cpu-and-memory-usage',
46+
grafanaHttpRequestsDb: 'db/knative-serving-revision-http-requests'
47+
};
48+
}
49+
50+
public getConfig(): Observable<AppConfig> {
51+
return this.config$.asObservable();
52+
}
53+
54+
public isConfigLoaded(): boolean {
55+
return this.configLoaded;
56+
}
57+
58+
public reloadConfig(): void {
59+
this.configLoaded = false;
60+
this.loadConfig();
61+
}
62+
}

0 commit comments

Comments
 (0)