Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions docs/assets/env_example.env
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,10 @@ CHAINLIT_AUTH_SECRET=openrag-dev-secret
# API/auth, so it is the OIDC front door. Zero-config otherwise (same-origin, no
# CORS); VITE_* build-time options are documented in the env vars reference.
# ADMIN_UI_PORT=8081


# GRAFANA_URL=http://localhost:3000/d/openrag-http/openrag-http-metrics
# To serve Grafana through the admin UI, point both URLs at its /grafana/ path.
# GF_SERVER_ROOT_URL=http://localhost:8081/grafana/
# GF_SERVER_SERVE_FROM_SUB_PATH=true
Comment thread
coderabbitai[bot] marked this conversation as resolved.

# ── Ray (kept as-is by the compose stack; see the docs for what each does) ───
RAY_DEDUP_LOGS=0
Expand All @@ -117,4 +119,4 @@ RAY_ENABLE_UV_RUN_RUNTIME_ENV=0
# RAY_memory_monitor_refresh_ms=0

# ── Logging (DEBUG on dev, INFO on prod) ──
LOG_LEVEL=DEBUG
LOG_LEVEL=DEBUG
8 changes: 6 additions & 2 deletions docs/assets/env_linux_gpu.env
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,11 @@ RAY_ENABLE_UV_RUN_RUNTIME_ENV=0 # critical with the newest version of UV
## replace X.X.X.X with localhost (local) or your server IP, and APP_PORT with your FastAPI port

# ADMIN_UI_PORT=8081 # Host port for the admin UI (nginx). Default is 8081.
# GRAFANA_URL=http://localhost:3000/d/openrag-http/openrag-http-metrics
# # Runtime dashboard link. Restart the API after changing it.
# GF_SERVER_ROOT_URL=http://localhost:8081/grafana/
# GF_SERVER_SERVE_FROM_SUB_PATH=true # Serve Grafana through the admin UI's /grafana/ path.
# VITE_API_BASE_URL= # API base baked into the SPA. EMPTY (default) = same-origin via
# # nginx (no CORS). Only set for a browser-direct build.
# VITE_GRAFANA_URL= # Optional Grafana dashboard link on the admin "System" page.
# VITE_APP_NAME=OpenRAG # App display name used in the UI branding.
# VITE_GRAFANA_URL= # Optional build-time fallback for older API deployments.
# VITE_APP_NAME=OpenRAG # App display name used in the UI branding.
5 changes: 4 additions & 1 deletion docs/content/docs/documentation/env_vars.md
Original file line number Diff line number Diff line change
Expand Up @@ -641,9 +641,10 @@ flowchart TD
| Variable | Type | Default | Description |
|----------|------|---------|-------------|
| `ADMIN_UI_PORT` | `number` | `8081` | Host port the admin UI (nginx) is published on. Serves `/app/` and reverse-proxies `/auth`, `/v1`, `/chainlit`, … to the backend, so it is the OIDC front door (`OIDC_REDIRECT_URI` targets this port). Deploy-time (not a `VITE_*` build arg). |
| `GRAFANA_URL` | `string` | `""` | Runtime, browser-reachable URL for the Grafana dashboard opened from **System → Metrics**. Restart the API after changing it. When this is empty or invalid, the action explains how to configure the dashboard instead of opening it. |
| `VITE_API_BASE_URL` | `string` | `""` (same-origin) | API base baked into the SPA. **Empty (default) = same-origin**: nginx reverse-proxies the API over the Docker network, so the UI works on any host/IP with no CORS. Set to an absolute URL only for a browser-direct build — then list the UI's origin in `CORS_EXTRA_ORIGINS`. |
| `VITE_BASE_PATH` | `string` | `/app/` | Sub-path the SPA is served under; must match the nginx `location`. |
| `VITE_GRAFANA_URL` | `string` | `""` | Optional Grafana dashboard link shown on the admin **System** page. |
| `VITE_GRAFANA_URL` | `string` | `""` | Build-time fallback for deployments whose API does not expose `GRAFANA_URL`. New deployments should use the runtime setting instead. |
| `VITE_APP_NAME` | `string` | `OpenRAG` | Application display name used in the UI branding. |
| `VITE_MOCK_API` | `boolean` | `false` | Development only — serves in-browser MSW API mocks when `true`. Ignored in production builds. |

Expand Down Expand Up @@ -712,3 +713,5 @@ Read only by the opt-in monitoring compose file (`infra/compose/monitoring.docke
|----------|------|---------|-------------|
| `GRAFANA_ADMIN_USER` | `str` | `admin` | Grafana admin username. |
| `GRAFANA_ADMIN_PASSWORD` | `str` | _(required)_ | Grafana admin password — compose refuses to start the monitoring profile if unset. |
| `GF_SERVER_ROOT_URL` | `str` | `http://localhost:3000` | Browser-facing Grafana root URL. Set this to the admin UI's `/grafana/` URL when using its proxy. |
| `GF_SERVER_SERVE_FROM_SUB_PATH` | `bool` | `false` | Set to `true` when `GF_SERVER_ROOT_URL` includes the `/grafana/` subpath. |
1 change: 1 addition & 0 deletions infra/charts/openrag-stack/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,7 @@ env:
ENABLE_RAY_SERVE: "true"
RAY_SERVE_NUM_REPLICAS: "4"
RAY_SERVE_PORT: "80"
GRAFANA_URL: ""

WITH_CHAINLIT_UI: "false"
SAVE_UPLOADED_FILES: "false"
Expand Down
8 changes: 5 additions & 3 deletions infra/compose/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,10 @@ CHAINLIT_AUTH_SECRET=openrag-dev-secret
# API/auth, so it is the OIDC front door. Zero-config otherwise (same-origin, no
# CORS); VITE_* build-time options are documented in the env vars reference.
# ADMIN_UI_PORT=8081


# GRAFANA_URL=http://localhost:3000/d/openrag-http/openrag-http-metrics
# To serve Grafana through the admin UI, point both URLs at its /grafana/ path.
# GF_SERVER_ROOT_URL=http://localhost:8081/grafana/
# GF_SERVER_SERVE_FROM_SUB_PATH=true
Comment thread
coderabbitai[bot] marked this conversation as resolved.

# ── Ray (kept as-is by the compose stack; see the docs for what each does) ───
RAY_DEDUP_LOGS=0
Expand All @@ -117,4 +119,4 @@ RAY_ENABLE_UV_RUN_RUNTIME_ENV=0
# RAY_memory_monitor_refresh_ms=0

# ── Logging (DEBUG on dev, INFO on prod) ──
LOG_LEVEL=DEBUG
LOG_LEVEL=DEBUG
2 changes: 1 addition & 1 deletion infra/compose/docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ services:
# to CORS_EXTRA_ORIGINS on the API.
VITE_API_BASE_URL: ${VITE_API_BASE_URL:-}
VITE_BASE_PATH: "/app/"
# Optional Grafana dashboard link on the admin "System" page; branding name.
# Build-time Grafana fallback; prefer runtime GRAFANA_URL in .env.
VITE_GRAFANA_URL: ${VITE_GRAFANA_URL:-}
VITE_APP_NAME: ${VITE_APP_NAME:-OpenRAG}
restart: unless-stopped
Expand Down
38 changes: 19 additions & 19 deletions infra/compose/grafana/dashboards/gpu-metrics.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,15 @@
{
"current": {},
"datasource": { "type": "prometheus", "uid": "prometheus" },
"definition": "label_values(nvidia_gpu_duty_cycle, uuid)",
"definition": "label_values(nvidia_smi_utilization_gpu_ratio, uuid)",
"hide": 0,
"includeAll": true,
"label": "GPU",
"multi": true,
"name": "gpu",
"options": [],
"query": {
"query": "label_values(nvidia_gpu_duty_cycle, uuid)",
"query": "label_values(nvidia_smi_utilization_gpu_ratio, uuid)",
"refId": "StandardVariableQuery"
},
"refresh": 2,
Expand Down Expand Up @@ -74,8 +74,8 @@
},
"targets": [
{
"expr": "nvidia_gpu_duty_cycle{uuid=~\"$gpu\"}",
"legendFormat": "GPU {{ name }} ({{ uuid }})",
"expr": "nvidia_smi_utilization_gpu_ratio{uuid=~\"$gpu\"} * 100",
"legendFormat": "GPU {{ uuid }}",
Comment thread
aditykris marked this conversation as resolved.
Outdated
"refId": "A"
}
],
Expand Down Expand Up @@ -122,13 +122,13 @@
},
"targets": [
{
"expr": "nvidia_gpu_memory_total_bytes{uuid=~\"$gpu\"}",
"legendFormat": "Total — {{ name }}",
"expr": "nvidia_smi_memory_total_bytes{uuid=~\"$gpu\"}",
"legendFormat": "Total — {{ uuid }}",
"refId": "A"
},
{
"expr": "nvidia_gpu_memory_used_bytes{uuid=~\"$gpu\"}",
"legendFormat": "Used — {{ name }}",
"expr": "nvidia_smi_memory_used_bytes{uuid=~\"$gpu\"}",
"legendFormat": "Used — {{ uuid }}",
"refId": "B"
}
],
Expand Down Expand Up @@ -164,8 +164,8 @@
},
"targets": [
{
"expr": "nvidia_gpu_duty_cycle{uuid=~\"$gpu\"}",
"legendFormat": "{{ name }}",
"expr": "nvidia_smi_utilization_gpu_ratio{uuid=~\"$gpu\"} * 100",
"legendFormat": "{{ uuid }}",
"refId": "A"
}
],
Expand Down Expand Up @@ -202,8 +202,8 @@
},
"targets": [
{
"expr": "(nvidia_gpu_memory_used_bytes{uuid=~\"$gpu\"} / nvidia_gpu_memory_total_bytes{uuid=~\"$gpu\"}) * 100",
"legendFormat": "{{ name }}",
"expr": "(nvidia_smi_memory_used_bytes{uuid=~\"$gpu\"} / nvidia_smi_memory_total_bytes{uuid=~\"$gpu\"}) * 100",
"legendFormat": "{{ uuid }}",
"refId": "A"
}
],
Expand Down Expand Up @@ -251,8 +251,8 @@
},
"targets": [
{
"expr": "nvidia_gpu_temperature_celsius{uuid=~\"$gpu\"}",
"legendFormat": "{{ name }} ({{ uuid }})",
"expr": "nvidia_smi_temperature_gpu{uuid=~\"$gpu\"}",
"legendFormat": "{{ uuid }}",
"refId": "A"
}
],
Expand Down Expand Up @@ -298,8 +298,8 @@
},
"targets": [
{
"expr": "nvidia_gpu_power_draw_watts{uuid=~\"$gpu\"}",
"legendFormat": "{{ name }} ({{ uuid }})",
"expr": "nvidia_smi_power_draw_watts{uuid=~\"$gpu\"}",
"legendFormat": "{{ uuid }}",
"refId": "A"
}
],
Expand All @@ -310,7 +310,7 @@
},
{
"id": 7,
"title": "GPU Fan Speed",
"title": "GPU Memory Controller Utilization (%)",
"type": "timeseries",
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 16 },
"datasource": { "type": "prometheus", "uid": "prometheus" },
Expand Down Expand Up @@ -347,8 +347,8 @@
},
"targets": [
{
"expr": "nvidia_gpu_fan_speed_percent{uuid=~\"$gpu\"}",
"legendFormat": "{{ name }} ({{ uuid }})",
"expr": "nvidia_smi_utilization_memory_ratio{uuid=~\"$gpu\"} * 100",
"legendFormat": "{{ uuid }}",
"refId": "A"
}
],
Expand Down
12 changes: 6 additions & 6 deletions infra/compose/monitoring.docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,13 @@ services:
image: grafana/grafana:11.2.2
container_name: openrag-grafana
ports:
- "3000:3000"
- "127.0.0.1:3000:3000"
environment:
- GF_SECURITY_ADMIN_USER=${GRAFANA_ADMIN_USER:-admin}
- GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_ADMIN_PASSWORD:?Set GRAFANA_ADMIN_PASSWORD in .env}
- GF_USERS_ALLOW_SIGN_UP=false
- GF_SERVER_ROOT_URL=${GF_SERVER_ROOT_URL:-http://localhost:3000}
- GF_SERVER_SERVE_FROM_SUB_PATH=${GF_SERVER_SERVE_FROM_SUB_PATH:-false}
volumes:
- ./grafana/provisioning:/etc/grafana/provisioning:ro
- ./grafana/dashboards:/var/lib/grafana/dashboards:ro
Expand Down Expand Up @@ -55,15 +57,13 @@ services:
restart: unless-stopped

nvidia-gpu-exporter:
image: utkuozdemir/nvidia_gpu_exporter:1.2.0
image: utkuozdemir/nvidia_gpu_exporter:1.13.1
container_name: openrag-nvidia-gpu-exporter
ports:
# Localhost only: scraped over the compose network by name.
- "127.0.0.1:9835:9835"
volumes:
- /usr/lib/x86_64-linux-gnu/libnvidia-ml.so:/usr/lib/x86_64-linux-gnu/libnvidia-ml.so:ro
- /usr/lib/x86_64-linux-gnu/libnvidia-ml.so.1:/usr/lib/x86_64-linux-gnu/libnvidia-ml.so.1:ro
- /usr/bin/nvidia-smi:/usr/bin/nvidia-smi:ro
environment:
- NVIDIA_DRIVER_CAPABILITIES=utility
deploy:
resources:
reservations:
Expand Down
25 changes: 25 additions & 0 deletions infra/compose/nginx/openrag-admin.conf
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,31 @@ server {
try_files $uri $uri/ /app/index.html;
}

# Keep Grafana on the same browser-facing origin as the admin UI.
location = /grafana {
return 301 /grafana/;
}

location /grafana/ {
resolver 127.0.0.11 valid=30s ipv6=off;
set $grafana_upstream grafana;

proxy_pass http://$grafana_upstream:3000;

proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-Host $http_host;
proxy_set_header X-Forwarded-Proto $forwarded_scheme;
proxy_set_header X-Forwarded-Prefix /grafana;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;

proxy_read_timeout 300s;
}

# Everything else is the OpenRag API (and /chainlit, /auth, /v1, …).
location / {
# Resolve the backend at request time via Docker's embedded DNS so nginx
Expand Down
6 changes: 1 addition & 5 deletions infra/compose/prometheus/prometheus.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,4 @@ scrape_configs:
type: Bearer
credentials_file: /etc/prometheus/openrag_token
static_configs:
- targets: ["host.docker.internal:8000"]

- job_name: "ray"
static_configs:
- targets: ["host.docker.internal:8080"]
- targets: ["openrag:8080"]
2 changes: 1 addition & 1 deletion infra/docker/api.Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ RUN useradd --uid ${APP_UID} --gid 0 --no-log-init --no-create-home \
/app/.venv /app/openrag.egg-info /opt/uv/cache \
/app/openrag/.files /app/openrag/.chainlit \
&& chgrp -R 0 /app /opt/uv \
&& chmod -R g-w /app /opt/uv \
&& chmod -R g+rX,g-w /app /opt/uv \
&& chmod -R g=u /app/home /app/data /app/db /app/logs /app/model_weights \
/app/.venv /app/openrag.egg-info /opt/uv/cache \
/app/openrag/.files /app/openrag/.chainlit
Expand Down
2 changes: 2 additions & 0 deletions openrag/api/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
from api.routers.user.health import router as health_router
from api.routers.user.search import router as search_router
from api.runtime_flags import WITH_CHAINLIT_UI, WITH_OPENAI_API
from api.runtime_ui import get_grafana_url
from core.config import load_config
from core.utils.banner import print_startup_banner
from core.utils.logging import get_logger
Expand Down Expand Up @@ -340,6 +341,7 @@ def get_config():
**redact_secrets(jsonable_encoder(settings)),
"super_admin_mode": SUPER_ADMIN_MODE,
"chainlit_enabled": WITH_CHAINLIT_UI,
"grafana_url": get_grafana_url(),
}


Expand Down
22 changes: 22 additions & 0 deletions openrag/api/runtime_ui.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
"""Runtime configuration exposed to the browser-facing Admin UI."""

from __future__ import annotations

import os
from urllib.parse import urlparse


def get_grafana_url() -> str | None:
"""Return a safe Grafana destination configured for this deployment."""
value = os.getenv("GRAFANA_URL", "").strip()
if not value:
return None

if value.startswith("/") and not value.startswith("//"):
return value
Comment thread
coderabbitai[bot] marked this conversation as resolved.

parsed = urlparse(value)
Comment thread
hedhoud marked this conversation as resolved.
Outdated
if parsed.scheme in {"http", "https"} and parsed.netloc:
return value
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

return None
33 changes: 33 additions & 0 deletions tests/unit/api/test_runtime_ui.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
"""Tests for deployment-provided Admin UI destinations."""

import pytest
from api.runtime_ui import get_grafana_url


@pytest.mark.parametrize(
("value", "expected"),
[
(None, None),
("", None),
(" ", None),
(
"https://grafana.example/d/openrag-http/openrag-http-metrics",
"https://grafana.example/d/openrag-http/openrag-http-metrics",
),
(
" http://localhost:3000/d/openrag-http/openrag-http-metrics ",
"http://localhost:3000/d/openrag-http/openrag-http-metrics",
),
("/grafana/d/openrag-http/openrag-http-metrics", "/grafana/d/openrag-http/openrag-http-metrics"),
("//untrusted.example/dashboard", None),
("javascript:alert(1)", None),
("grafana.example/dashboard", None),
],
)
def test_grafana_url_accepts_only_browser_safe_destinations(monkeypatch, value, expected):
if value is None:
monkeypatch.delenv("GRAFANA_URL", raising=False)
else:
monkeypatch.setenv("GRAFANA_URL", value)

assert get_grafana_url() == expected
1 change: 1 addition & 0 deletions ui/.env.example
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
VITE_API_BASE_URL=http://localhost:8000
# Build-time fallback for frontend-only development. Production uses GRAFANA_URL on the API.
VITE_GRAFANA_URL=http://localhost:3000/d/system-overview
4 changes: 3 additions & 1 deletion ui/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ The UI discovers the auth mode (`token` / `oidc`) from the backend at runtime, n
|-----|---------|
| `VITE_API_BASE_URL` | OpenRAG API base (empty = same origin) |
| `VITE_BASE_PATH` | base path when served under a sub-path (default `/`) |
| `VITE_GRAFANA_URL` | Grafana dashboard link shown on the admin System page |
| `VITE_GRAFANA_URL` | Build-time fallback for the Grafana dashboard link |
| `VITE_APP_NAME` | app display name / branding (default `OpenRAG`) |
| `VITE_MOCK_API` | `true` to serve MSW mocks in dev |

Production deployments should set `GRAFANA_URL` on the OpenRAG API. The Admin UI reads it at runtime, so the same prebuilt UI image can point to different dashboards.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
8 changes: 7 additions & 1 deletion ui/src/lib/api/system.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ export interface VersionResponse {
version: string;
}

export interface SystemConfig extends Record<string, unknown> {
chainlit_enabled?: boolean;
grafana_url?: string | null;
super_admin_mode?: boolean;
}

export interface RayActor {
actor_id: string;
name: string;
Expand All @@ -29,7 +35,7 @@ export function getVersion() {
}

export function getConfig() {
return request<Record<string, unknown>>("/config");
return request<SystemConfig>("/config");
}

export function getMetrics() {
Expand Down
Loading
Loading