diff --git a/config/config.tracing.yaml b/config/config.tracing.yaml new file mode 100644 index 00000000..9bd722e0 --- /dev/null +++ b/config/config.tracing.yaml @@ -0,0 +1,104 @@ +# Local Tracing Configuration (Jaeger + Always-On Sampling) +# This config is used by tools/tracing/docker-compose.tracing.yaml via CONFIG_FILE. + +bert_model: + model_id: models/all-MiniLM-L12-v2 + threshold: 0.6 + use_cpu: true + +semantic_cache: + enabled: true + backend_type: "memory" + similarity_threshold: 0.8 + max_entries: 1000 + ttl_seconds: 3600 + eviction_policy: "fifo" + +tools: + enabled: true + top_k: 3 + similarity_threshold: 0.2 + tools_db_path: "config/tools_db.json" + fallback_to_empty: true + +prompt_guard: + enabled: true + use_modernbert: true + model_id: "models/jailbreak_classifier_modernbert-base_model" + threshold: 0.7 + use_cpu: true + jailbreak_mapping_path: "models/jailbreak_classifier_modernbert-base_model/jailbreak_type_mapping.json" + +vllm_endpoints: + - name: "endpoint1" + address: "127.0.0.1" + port: 8000 + weight: 1 + +model_config: + "openai/gpt-oss-20b": + reasoning_family: "gpt-oss" + preferred_endpoints: ["endpoint1"] + pii_policy: + allow_by_default: true + +classifier: + category_model: + model_id: "models/category_classifier_modernbert-base_model" + use_modernbert: true + threshold: 0.6 + use_cpu: true + category_mapping_path: "models/category_classifier_modernbert-base_model/category_mapping.json" + pii_model: + model_id: "models/pii_classifier_modernbert-base_presidio_token_model" + use_modernbert: true + threshold: 0.7 + use_cpu: true + pii_mapping_path: "models/pii_classifier_modernbert-base_presidio_token_model/pii_type_mapping.json" + +categories: + - name: math + system_prompt: "You are a mathematics expert. Provide step-by-step solutions." + model_scores: + - model: openai/gpt-oss-20b + score: 1.0 + use_reasoning: true + - name: other + system_prompt: "You are a helpful assistant." + model_scores: + - model: openai/gpt-oss-20b + score: 0.7 + use_reasoning: false + +default_model: openai/gpt-oss-20b + +reasoning_families: + gpt-oss: + type: "reasoning_effort" + parameter: "reasoning_effort" + +default_reasoning_effort: high + +api: + batch_classification: + max_batch_size: 100 + concurrency_threshold: 5 + max_concurrency: 8 + metrics: + enabled: true + +observability: + tracing: + enabled: true + provider: "opentelemetry" + exporter: + type: "otlp" + endpoint: "jaeger:4317" # Jaeger gRPC OTLP endpoint inside compose network + insecure: true + sampling: + type: "always_on" # Always sample in local/dev for easy debugging + rate: 1.0 + resource: + service_name: "vllm-semantic-router" + service_version: "dev" + deployment_environment: "local" diff --git a/config/config.yaml b/config/config.yaml index 1e2c43d7..6c899a02 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -185,11 +185,11 @@ api: # Observability Configuration observability: tracing: - enabled: false # Enable distributed tracing (default: false) + enabled: true # Enable distributed tracing for docker-compose stack provider: "opentelemetry" # Provider: opentelemetry, openinference, openllmetry exporter: - type: "stdout" # Exporter: otlp, jaeger, zipkin, stdout - endpoint: "localhost:4317" # OTLP endpoint (when type: otlp) + type: "otlp" # Export spans to Jaeger (via OTLP gRPC) + endpoint: "jaeger:4317" # Jaeger collector inside compose network insecure: true # Use insecure connection (no TLS) sampling: type: "always_on" # Sampling: always_on, always_off, probabilistic diff --git a/dashboard/backend/main.go b/dashboard/backend/main.go index 40abd391..c77c2e5c 100644 --- a/dashboard/backend/main.go +++ b/dashboard/backend/main.go @@ -271,6 +271,7 @@ func main() { routerAPI := flag.String("router_api", env("TARGET_ROUTER_API_URL", "http://localhost:8080"), "Router API base URL") routerMetrics := flag.String("router_metrics", env("TARGET_ROUTER_METRICS_URL", "http://localhost:9190/metrics"), "Router metrics URL") openwebuiURL := flag.String("openwebui", env("TARGET_OPENWEBUI_URL", ""), "Open WebUI base URL") + jaegerURL := flag.String("jaeger", env("TARGET_JAEGER_URL", ""), "Jaeger base URL") flag.Parse() @@ -340,13 +341,27 @@ func main() { log.Printf("Warning: Grafana URL not configured") } - // Smart /api/ router: route to Router API or Grafana API based on path + // Jaeger API proxy (needs to be set up early for the smart router below) + var jaegerAPIProxy *httputil.ReverseProxy + if *jaegerURL != "" { + // Create proxy for Jaeger API (no prefix stripping for /api/*) + jaegerAPIProxy, _ = newReverseProxy(*jaegerURL, "", false) + } + + // Smart /api/ router: route to Router API, Jaeger API, or Grafana API based on path mux.HandleFunc("/api/", func(w http.ResponseWriter, r *http.Request) { // If path starts with /api/router/, use Router API proxy if strings.HasPrefix(r.URL.Path, "/api/router/") && routerAPIProxy != nil { routerAPIProxy.ServeHTTP(w, r) return } + // If path is Jaeger API (services, traces, operations, etc.), use Jaeger proxy + if jaegerAPIProxy != nil && (strings.HasPrefix(r.URL.Path, "/api/services") || + strings.HasPrefix(r.URL.Path, "/api/traces") || + strings.HasPrefix(r.URL.Path, "/api/operations")) { + jaegerAPIProxy.ServeHTTP(w, r) + return + } // Otherwise, if Grafana is configured, proxy to Grafana API if grafanaStaticProxy != nil { grafanaStaticProxy.ServeHTTP(w, r) @@ -382,6 +397,31 @@ func main() { log.Printf("Warning: Prometheus URL not configured") } + // Jaeger proxy (optional) - expose full UI under /embedded/jaeger and its static assets under /static/ + if *jaegerURL != "" { + jp, err := newReverseProxy(*jaegerURL, "/embedded/jaeger", false) + if err != nil { + log.Fatalf("jaeger proxy error: %v", err) + } + // Jaeger UI (root UI under /embedded/jaeger) + mux.Handle("/embedded/jaeger", jp) + mux.Handle("/embedded/jaeger/", jp) + + // Jaeger static assets are typically served under /static/* from the same origin + // Provide a passthrough proxy without prefix stripping + jStatic, _ := newReverseProxy(*jaegerURL, "", false) + mux.Handle("/static/", jStatic) + + log.Printf("Jaeger proxy configured: %s; static assets proxied at /static/", *jaegerURL) + } else { + mux.HandleFunc("/embedded/jaeger/", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusServiceUnavailable) + w.Write([]byte(`{"error":"Jaeger not configured","message":"TARGET_JAEGER_URL environment variable is not set"}`)) + }) + log.Printf("Info: Jaeger URL not configured (optional)") + } + // Open WebUI proxy (optional) if *openwebuiURL != "" { op, err := newReverseProxy(*openwebuiURL, "/embedded/openwebui", true) @@ -409,6 +449,9 @@ func main() { if *promURL != "" { log.Printf("Prometheus: %s → /embedded/prometheus/", *promURL) } + if *jaegerURL != "" { + log.Printf("Jaeger: %s → /embedded/jaeger/", *jaegerURL) + } if *openwebuiURL != "" { log.Printf("OpenWebUI: %s → /embedded/openwebui/", *openwebuiURL) } diff --git a/dashboard/frontend/src/App.tsx b/dashboard/frontend/src/App.tsx index fb2a6c65..654b7d45 100644 --- a/dashboard/frontend/src/App.tsx +++ b/dashboard/frontend/src/App.tsx @@ -6,6 +6,7 @@ import MonitoringPage from './pages/MonitoringPage' import ConfigPage from './pages/ConfigPage' import PlaygroundPage from './pages/PlaygroundPage' import TopologyPage from './pages/TopologyPage' +import TracingPage from './pages/TracingPage' import { ConfigSection } from './components/ConfigNav' const App: React.FC = () => { @@ -117,6 +118,17 @@ const App: React.FC = () => { } /> + setConfigSection(section as ConfigSection)} + > + + + } + /> ) diff --git a/dashboard/frontend/src/components/Layout.module.css b/dashboard/frontend/src/components/Layout.module.css index 40e4f223..c4479dc1 100644 --- a/dashboard/frontend/src/components/Layout.module.css +++ b/dashboard/frontend/src/components/Layout.module.css @@ -25,39 +25,11 @@ align-items: center; gap: 0.5rem; padding: 0 0.25rem; - flex-direction: column; + justify-content: center; } .sidebarCollapsed .brandContainer { - flex-direction: column; - gap: 0.75rem; -} - -.collapseButton { - display: flex; - align-items: center; justify-content: center; - width: 32px; - height: 32px; - padding: 6px; - background: transparent; - border: none; - border-radius: var(--radius-md); - cursor: pointer; - color: var(--color-text-secondary); - transition: all var(--transition-fast); - flex-shrink: 0; -} - -.collapseButton:hover { - background-color: var(--color-bg-tertiary); - color: var(--color-text); -} - -.collapseButton svg { - width: 20px; - height: 20px; - flex-shrink: 0; } .brand { @@ -228,41 +200,36 @@ padding: 0 0.5rem; display: flex; align-items: center; - gap: 0.5rem; -} - -.themeToggle { - padding: 0.5rem; - font-size: 1.25rem; - border-radius: var(--radius-md); - transition: background-color var(--transition-fast); - background: transparent; - border: none; - cursor: pointer; - color: var(--color-text); -} - -.themeToggle:hover { - background-color: var(--color-bg-tertiary); + justify-content: center; } -.iconButton { +.collapseButton { display: flex; align-items: center; justify-content: center; - padding: 0.5rem; + width: 32px; + height: 32px; + padding: 6px; + background: transparent; + border: none; border-radius: var(--radius-md); - transition: background-color var(--transition-fast); - color: var(--color-text-secondary); - text-decoration: none; cursor: pointer; + color: var(--color-text-secondary); + transition: all var(--transition-fast); + flex-shrink: 0; } -.iconButton:hover { +.collapseButton:hover { background-color: var(--color-bg-tertiary); color: var(--color-text); } +.collapseButton svg { + width: 20px; + height: 20px; + flex-shrink: 0; +} + .main { flex: 1; display: flex; @@ -292,22 +259,35 @@ flex: 1; } +.headerBrand { + font-size: 1.125rem; + font-weight: 600; + color: var(--color-text); +} + .headerRight { display: flex; align-items: center; - gap: 1.5rem; + gap: 0.75rem; } -.headerLink { +.headerIconButton { + display: flex; + align-items: center; + justify-content: center; + padding: 0.5rem; + font-size: 1.25rem; + border-radius: var(--radius-md); + transition: all var(--transition-fast); + background: transparent; + border: none; + cursor: pointer; color: var(--color-text-secondary); text-decoration: none; - font-size: 0.9375rem; - font-weight: 500; - transition: color var(--transition-fast); - white-space: nowrap; } -.headerLink:hover { +.headerIconButton:hover { + background-color: var(--color-bg-tertiary); color: var(--color-text); } diff --git a/dashboard/frontend/src/components/Layout.tsx b/dashboard/frontend/src/components/Layout.tsx index 98cee2aa..65cc6a72 100644 --- a/dashboard/frontend/src/components/Layout.tsx +++ b/dashboard/frontend/src/components/Layout.tsx @@ -46,26 +46,6 @@ const Layout: React.FC = ({ children, configSection, onConfigSectio
+
+
+
+
+ Semantic Router +
+ - -
-
-
diff --git a/dashboard/frontend/src/pages/TracingPage.tsx b/dashboard/frontend/src/pages/TracingPage.tsx new file mode 100644 index 00000000..db33d677 --- /dev/null +++ b/dashboard/frontend/src/pages/TracingPage.tsx @@ -0,0 +1,76 @@ +import React, { useEffect, useRef, useState } from 'react' +import styles from './MonitoringPage.module.css' + +const TracingPage: React.FC = () => { + const [theme, setTheme] = useState( + document.documentElement.getAttribute('data-theme') || 'dark' + ) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + const iframeRef = useRef(null) + + useEffect(() => { + const observer = new MutationObserver(() => { + const t = document.documentElement.getAttribute('data-theme') || 'dark' + if (t !== theme) { + setTheme(t) + setLoading(true) + } + }) + observer.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] }) + return () => observer.disconnect() + }, [theme]) + + const buildJaegerUrl = () => { + // Default Jaeger landing page; could navigate to search with params later + return `/embedded/jaeger/search?lookback=1h&limit=20&service=vllm-semantic-router` + } + + useEffect(() => { + // Slight delay to ensure iframe renders + const timer = setTimeout(() => setLoading(false), 100) + return () => clearTimeout(timer) + }, [theme]) + + const handleIframeLoad = () => { + setLoading(false) + setError(null) + } + + const handleIframeError = () => { + setLoading(false) + setError('Failed to load Jaeger UI. Please check that Jaeger is running and the proxy is configured.') + } + + return ( +
+ {error && ( +
+ ⚠️ + {error} +
+ )} + +
+ {loading && ( +
+
+

Loading Jaeger UI...

+
+ )} +