diff --git a/.env.example b/.env.example index 0d891df..be26396 100644 --- a/.env.example +++ b/.env.example @@ -76,21 +76,28 @@ APP_ENV=prod LOG_LEVEL=INFO -# ----------------------------------------------------------------------------- -# Frontend (otel-bi-frontend) → frontend/ -# Baked into the image at build time via Docker build-args. -# In Docker Compose these are passed as build args, not runtime env. -# For local dev copy frontend/.env.example to frontend/.env.local instead. -# ----------------------------------------------------------------------------- -VITE_API_BASE_URL=http://localhost:8000 -VITE_OTEL_COLLECTOR_ENDPOINT=http://alloy:4318 -VITE_OTEL_SERVICE_NAME=otel-bi-frontend -VITE_OTEL_SERVICE_NAMESPACE=final-thesis - - # ----------------------------------------------------------------------------- # OpenTelemetry — shared collector endpoint -# Same value goes to Go analytics, Python API, and frontend build arg above +# Used by Go analytics + Python API for their own trace/metric export. +# In-cluster K8s DNS for Alloy. # ----------------------------------------------------------------------------- OTEL_COLLECTOR_ENDPOINT=http://alloy:4318 OTEL_SERVICE_NAMESPACE=final-thesis + +# ----------------------------------------------------------------------------- +# Frontend telemetry — served to the SPA via GET /api/config. +# These are the values the browser-side OTel SDK uses; the SPA reads them at +# runtime so a single frontend image works across all environments. +# ----------------------------------------------------------------------------- + +# Browser-reachable OTLP endpoint. Distinct from OTEL_COLLECTOR_ENDPOINT +# because the browser can't reach in-cluster service DNS. +# Default `/otel` assumes Ingress routes `/otel/v1/traces` → alloy:4318. +# Set to an absolute URL (e.g. https://alloy.example.com) if exposing the +# collector on its own host instead. +FRONTEND_OTEL_COLLECTOR_ENDPOINT=/otel + +# OTel resource attributes for the frontend service +FRONTEND_OTEL_SERVICE_NAME=otel-bi-frontend +FRONTEND_OTEL_SERVICE_NAMESPACE=final-thesis +FRONTEND_DEPLOYMENT_ENVIRONMENT=production diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 9ed2c24..578559f 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -148,9 +148,6 @@ jobs: push: true cache-from: type=registry,ref=${{ env.IMAGE_FRONTEND }}:latest cache-to: type=inline - build-args: | - VITE_API_BASE_URL=${{ vars.API_BASE_URL }} - VITE_OTEL_COLLECTOR_ENDPOINT=${{ vars.OTEL_COLLECTOR_ENDPOINT }} tags: | ${{ env.IMAGE_FRONTEND }}:${{ github.sha }} ${{ env.IMAGE_FRONTEND }}:latest diff --git a/backend/app/core/config.py b/backend/app/core/config.py index b6939f1..454c0d8 100644 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -53,6 +53,14 @@ class Settings(BaseSettings): otel_collector_endpoint: str = "http://localhost:4318" otel_export_timeout_ms: int = 10000 + # Browser-reachable OTLP endpoint — served to the SPA via GET /api/config. + # Distinct from otel_collector_endpoint, which is the backend's own + # in-cluster collector address. + frontend_otel_collector_endpoint: str = "/otel" + frontend_otel_service_name: str = "otel-bi-frontend" + frontend_otel_service_namespace: str = "final-thesis" + frontend_deployment_environment: str = "production" + # Report output — points at the K8s CSI / SMB mountpoint in production report_output_dir: str = "/tmp/otel-bi-reports" diff --git a/backend/app/routers/platform.py b/backend/app/routers/platform.py index 669be15..117b98e 100644 --- a/backend/app/routers/platform.py +++ b/backend/app/routers/platform.py @@ -36,6 +36,10 @@ def frontend_config() -> dict: "oidc_authority": settings.frontend_jwt_issuer_url, "oidc_client_id": settings.frontend_oidc_client_id, "oidc_scope": settings.frontend_oidc_scope, + "otel_collector_endpoint": settings.frontend_otel_collector_endpoint, + "otel_service_name": settings.frontend_otel_service_name, + "otel_service_namespace": settings.frontend_otel_service_namespace, + "deployment_environment": settings.frontend_deployment_environment, } diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 68bb114..54b6224 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -2,12 +2,6 @@ FROM rockylinux/rockylinux:10 AS build RUN dnf install -y nodejs npm && dnf clean all -ARG VITE_API_BASE_URL=http://localhost:8000 -ARG VITE_OTEL_COLLECTOR_ENDPOINT=http://localhost:4318 - -ENV VITE_API_BASE_URL=$VITE_API_BASE_URL \ - VITE_OTEL_COLLECTOR_ENDPOINT=$VITE_OTEL_COLLECTOR_ENDPOINT - WORKDIR /app COPY package.json package-lock.json ./ diff --git a/frontend/src/api/aw.ts b/frontend/src/api/aw.ts index 99c58c2..22a6f64 100644 --- a/frontend/src/api/aw.ts +++ b/frontend/src/api/aw.ts @@ -9,14 +9,13 @@ import type { AWAnomalyPoint, } from "./types"; -const API_BASE = import.meta.env.VITE_API_BASE_URL ?? "http://localhost:8000"; const tracer = trace.getTracer("aw-frontend-api"); async function get(path: string, spanName: string): Promise { return tracer.startActiveSpan(spanName, async (span) => { try { const token = currentAccessToken(); - const resp = await fetch(`${API_BASE}${path}`, { + const resp = await fetch(path, { headers: { Accept: "application/json", ...(token ? { Authorization: `Bearer ${token}` } : {}), diff --git a/frontend/src/api/config.ts b/frontend/src/api/config.ts index e286675..80f74d5 100644 --- a/frontend/src/api/config.ts +++ b/frontend/src/api/config.ts @@ -3,14 +3,16 @@ export type AppConfig = { oidc_authority: string; oidc_client_id: string; oidc_scope: string; + otel_collector_endpoint: string; + otel_service_name: string; + otel_service_namespace: string; + deployment_environment: string; }; -const API_BASE = (import.meta.env.VITE_API_BASE_URL as string | undefined) ?? ""; - let _config: AppConfig | null = null; export async function fetchAppConfig(): Promise { - const resp = await fetch(`${API_BASE}/api/config`); + const resp = await fetch("/api/config"); if (!resp.ok) throw new Error(`Failed to fetch app config: ${resp.status}`); _config = (await resp.json()) as AppConfig; return _config; diff --git a/frontend/src/api/gateway.ts b/frontend/src/api/gateway.ts index 52d3bc2..a4971fd 100644 --- a/frontend/src/api/gateway.ts +++ b/frontend/src/api/gateway.ts @@ -2,7 +2,6 @@ import { SpanStatusCode, trace } from "@opentelemetry/api"; import { currentAccessToken } from "../auth/oidc"; import type { JobExecution, AuditEntry, ExportRecord } from "./types"; -const API_BASE = import.meta.env.VITE_API_BASE_URL ?? "http://localhost:8000"; const tracer = trace.getTracer("gateway-frontend-api"); function authHeaders(): Record { @@ -13,7 +12,7 @@ function authHeaders(): Record { async function get(path: string, spanName: string): Promise { return tracer.startActiveSpan(spanName, async (span) => { try { - const resp = await fetch(`${API_BASE}${path}`, { + const resp = await fetch(path, { headers: { Accept: "application/json", ...authHeaders() }, }); if (!resp.ok) { @@ -36,7 +35,7 @@ async function get(path: string, spanName: string): Promise { async function post(path: string, spanName: string, body: unknown = {}): Promise { return tracer.startActiveSpan(spanName, async (span) => { try { - const resp = await fetch(`${API_BASE}${path}`, { + const resp = await fetch(path, { method: "POST", headers: { "Content-Type": "application/json", diff --git a/frontend/src/api/wwi.ts b/frontend/src/api/wwi.ts index 86fa888..9796914 100644 --- a/frontend/src/api/wwi.ts +++ b/frontend/src/api/wwi.ts @@ -9,7 +9,6 @@ import type { WWIScenario, } from "./types"; -const API_BASE = import.meta.env.VITE_API_BASE_URL ?? "http://localhost:8000"; const tracer = trace.getTracer("wwi-frontend-api"); function authHeaders(): Record { @@ -20,7 +19,7 @@ function authHeaders(): Record { async function get(path: string, spanName: string): Promise { return tracer.startActiveSpan(spanName, async (span) => { try { - const resp = await fetch(`${API_BASE}${path}`, { + const resp = await fetch(path, { headers: { Accept: "application/json", ...authHeaders() }, }); if (!resp.ok) { @@ -43,7 +42,7 @@ async function get(path: string, spanName: string): Promise { async function post(path: string, body: unknown, spanName: string): Promise { return tracer.startActiveSpan(spanName, async (span) => { try { - const resp = await fetch(`${API_BASE}${path}`, { + const resp = await fetch(path, { method: "POST", headers: { "Content-Type": "application/json", diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 8e50dbd..1bc9108 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -11,8 +11,6 @@ import { fetchAppConfig } from "./api/config"; import { AuthProvider } from "./auth/AuthContext"; import { setupTelemetry } from "./telemetry"; -setupTelemetry(); - const queryClient = new QueryClient({ defaultOptions: { queries: { @@ -22,7 +20,13 @@ const queryClient = new QueryClient({ }, }); -fetchAppConfig().then(() => { +fetchAppConfig().then((config) => { + setupTelemetry({ + collectorEndpoint: config.otel_collector_endpoint, + serviceName: config.otel_service_name, + serviceNamespace: config.otel_service_namespace, + deploymentEnvironment: config.deployment_environment, + }); createRoot(document.getElementById("root")!).render( diff --git a/frontend/src/telemetry.ts b/frontend/src/telemetry.ts index de3c958..d4e6fb9 100644 --- a/frontend/src/telemetry.ts +++ b/frontend/src/telemetry.ts @@ -15,25 +15,19 @@ import { UserInteractionInstrumentation } from "@opentelemetry/instrumentation-u import { XMLHttpRequestInstrumentation } from "@opentelemetry/instrumentation-xml-http-request"; import { ZoneContextManager } from "@opentelemetry/context-zone-peer-dep"; +export type TelemetryConfig = { + collectorEndpoint: string; + serviceName: string; + serviceNamespace: string; + deploymentEnvironment: string; +}; + let initialized = false; -function escapeRegExp(value: string): string { - return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); -} - -export function setupTelemetry(): void { +export function setupTelemetry(config: TelemetryConfig): void { if (initialized) return; initialized = true; - const endpoint = - import.meta.env.VITE_OTEL_COLLECTOR_ENDPOINT ?? "http://localhost:4318"; - const serviceName = - import.meta.env.VITE_OTEL_SERVICE_NAME ?? "otel-bi-frontend"; - const serviceNamespace = - import.meta.env.VITE_OTEL_SERVICE_NAMESPACE ?? "final-thesis"; - const apiBaseUrl = - import.meta.env.VITE_API_BASE_URL ?? "http://localhost:8000"; - propagation.setGlobalPropagator( new CompositePropagator({ propagators: [ @@ -45,14 +39,14 @@ export function setupTelemetry(): void { const provider = new WebTracerProvider({ resource: resourceFromAttributes({ - "service.name": serviceName, - "service.namespace": serviceNamespace, - "deployment.environment": import.meta.env.MODE, + "service.name": config.serviceName, + "service.namespace": config.serviceNamespace, + "deployment.environment": config.deploymentEnvironment, }), spanProcessors: [ new BatchSpanProcessor( new OTLPTraceExporter({ - url: `${endpoint}/v1/traces`, + url: `${config.collectorEndpoint}/v1/traces`, }), ), ], @@ -65,11 +59,7 @@ export function setupTelemetry(): void { registerInstrumentations({ instrumentations: [ new DocumentLoadInstrumentation(), - new FetchInstrumentation({ - propagateTraceHeaderCorsUrls: [ - new RegExp(`^${escapeRegExp(apiBaseUrl)}`), - ], - }), + new FetchInstrumentation(), new XMLHttpRequestInstrumentation(), new UserInteractionInstrumentation(), ], diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index b6fdd42..6cc3fd2 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -7,5 +7,8 @@ export default defineConfig({ server: { host: "0.0.0.0", port: 5173, + proxy: { + "/api": "http://localhost:8000", + }, }, });