Remove retarded build time variables
All checks were successful
CI / test (push) Successful in 54s
CI / test-analytics (push) Successful in 2m2s
CI / build-api (push) Successful in 3m1s
CI / build-frontend (push) Successful in 1m58s
CI / build-analytics (push) Successful in 41s

This commit is contained in:
2026-05-11 17:00:17 +02:00
parent 5cbc1d50fd
commit b1de6284f7
12 changed files with 65 additions and 59 deletions

View File

@@ -76,21 +76,28 @@ APP_ENV=prod
LOG_LEVEL=INFO 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 # 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_COLLECTOR_ENDPOINT=http://alloy:4318
OTEL_SERVICE_NAMESPACE=final-thesis 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

View File

@@ -148,9 +148,6 @@ jobs:
push: true push: true
cache-from: type=registry,ref=${{ env.IMAGE_FRONTEND }}:latest cache-from: type=registry,ref=${{ env.IMAGE_FRONTEND }}:latest
cache-to: type=inline cache-to: type=inline
build-args: |
VITE_API_BASE_URL=${{ vars.API_BASE_URL }}
VITE_OTEL_COLLECTOR_ENDPOINT=${{ vars.OTEL_COLLECTOR_ENDPOINT }}
tags: | tags: |
${{ env.IMAGE_FRONTEND }}:${{ github.sha }} ${{ env.IMAGE_FRONTEND }}:${{ github.sha }}
${{ env.IMAGE_FRONTEND }}:latest ${{ env.IMAGE_FRONTEND }}:latest

View File

@@ -53,6 +53,14 @@ class Settings(BaseSettings):
otel_collector_endpoint: str = "http://localhost:4318" otel_collector_endpoint: str = "http://localhost:4318"
otel_export_timeout_ms: int = 10000 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 — points at the K8s CSI / SMB mountpoint in production
report_output_dir: str = "/tmp/otel-bi-reports" report_output_dir: str = "/tmp/otel-bi-reports"

View File

@@ -36,6 +36,10 @@ def frontend_config() -> dict:
"oidc_authority": settings.frontend_jwt_issuer_url, "oidc_authority": settings.frontend_jwt_issuer_url,
"oidc_client_id": settings.frontend_oidc_client_id, "oidc_client_id": settings.frontend_oidc_client_id,
"oidc_scope": settings.frontend_oidc_scope, "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,
} }

View File

@@ -2,12 +2,6 @@ FROM rockylinux/rockylinux:10 AS build
RUN dnf install -y nodejs npm && dnf clean all 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 WORKDIR /app
COPY package.json package-lock.json ./ COPY package.json package-lock.json ./

View File

@@ -9,14 +9,13 @@ import type {
AWAnomalyPoint, AWAnomalyPoint,
} from "./types"; } from "./types";
const API_BASE = import.meta.env.VITE_API_BASE_URL ?? "http://localhost:8000";
const tracer = trace.getTracer("aw-frontend-api"); const tracer = trace.getTracer("aw-frontend-api");
async function get<T>(path: string, spanName: string): Promise<T> { async function get<T>(path: string, spanName: string): Promise<T> {
return tracer.startActiveSpan(spanName, async (span) => { return tracer.startActiveSpan(spanName, async (span) => {
try { try {
const token = currentAccessToken(); const token = currentAccessToken();
const resp = await fetch(`${API_BASE}${path}`, { const resp = await fetch(path, {
headers: { headers: {
Accept: "application/json", Accept: "application/json",
...(token ? { Authorization: `Bearer ${token}` } : {}), ...(token ? { Authorization: `Bearer ${token}` } : {}),

View File

@@ -3,14 +3,16 @@ export type AppConfig = {
oidc_authority: string; oidc_authority: string;
oidc_client_id: string; oidc_client_id: string;
oidc_scope: 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; let _config: AppConfig | null = null;
export async function fetchAppConfig(): Promise<AppConfig> { export async function fetchAppConfig(): Promise<AppConfig> {
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}`); if (!resp.ok) throw new Error(`Failed to fetch app config: ${resp.status}`);
_config = (await resp.json()) as AppConfig; _config = (await resp.json()) as AppConfig;
return _config; return _config;

View File

@@ -2,7 +2,6 @@ import { SpanStatusCode, trace } from "@opentelemetry/api";
import { currentAccessToken } from "../auth/oidc"; import { currentAccessToken } from "../auth/oidc";
import type { JobExecution, AuditEntry, ExportRecord } from "./types"; 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"); const tracer = trace.getTracer("gateway-frontend-api");
function authHeaders(): Record<string, string> { function authHeaders(): Record<string, string> {
@@ -13,7 +12,7 @@ function authHeaders(): Record<string, string> {
async function get<T>(path: string, spanName: string): Promise<T> { async function get<T>(path: string, spanName: string): Promise<T> {
return tracer.startActiveSpan(spanName, async (span) => { return tracer.startActiveSpan(spanName, async (span) => {
try { try {
const resp = await fetch(`${API_BASE}${path}`, { const resp = await fetch(path, {
headers: { Accept: "application/json", ...authHeaders() }, headers: { Accept: "application/json", ...authHeaders() },
}); });
if (!resp.ok) { if (!resp.ok) {
@@ -36,7 +35,7 @@ async function get<T>(path: string, spanName: string): Promise<T> {
async function post<T>(path: string, spanName: string, body: unknown = {}): Promise<T> { async function post<T>(path: string, spanName: string, body: unknown = {}): Promise<T> {
return tracer.startActiveSpan(spanName, async (span) => { return tracer.startActiveSpan(spanName, async (span) => {
try { try {
const resp = await fetch(`${API_BASE}${path}`, { const resp = await fetch(path, {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",

View File

@@ -9,7 +9,6 @@ import type {
WWIScenario, WWIScenario,
} from "./types"; } from "./types";
const API_BASE = import.meta.env.VITE_API_BASE_URL ?? "http://localhost:8000";
const tracer = trace.getTracer("wwi-frontend-api"); const tracer = trace.getTracer("wwi-frontend-api");
function authHeaders(): Record<string, string> { function authHeaders(): Record<string, string> {
@@ -20,7 +19,7 @@ function authHeaders(): Record<string, string> {
async function get<T>(path: string, spanName: string): Promise<T> { async function get<T>(path: string, spanName: string): Promise<T> {
return tracer.startActiveSpan(spanName, async (span) => { return tracer.startActiveSpan(spanName, async (span) => {
try { try {
const resp = await fetch(`${API_BASE}${path}`, { const resp = await fetch(path, {
headers: { Accept: "application/json", ...authHeaders() }, headers: { Accept: "application/json", ...authHeaders() },
}); });
if (!resp.ok) { if (!resp.ok) {
@@ -43,7 +42,7 @@ async function get<T>(path: string, spanName: string): Promise<T> {
async function post<T>(path: string, body: unknown, spanName: string): Promise<T> { async function post<T>(path: string, body: unknown, spanName: string): Promise<T> {
return tracer.startActiveSpan(spanName, async (span) => { return tracer.startActiveSpan(spanName, async (span) => {
try { try {
const resp = await fetch(`${API_BASE}${path}`, { const resp = await fetch(path, {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",

View File

@@ -11,8 +11,6 @@ import { fetchAppConfig } from "./api/config";
import { AuthProvider } from "./auth/AuthContext"; import { AuthProvider } from "./auth/AuthContext";
import { setupTelemetry } from "./telemetry"; import { setupTelemetry } from "./telemetry";
setupTelemetry();
const queryClient = new QueryClient({ const queryClient = new QueryClient({
defaultOptions: { defaultOptions: {
queries: { 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( createRoot(document.getElementById("root")!).render(
<StrictMode> <StrictMode>
<BrowserRouter> <BrowserRouter>

View File

@@ -15,25 +15,19 @@ import { UserInteractionInstrumentation } from "@opentelemetry/instrumentation-u
import { XMLHttpRequestInstrumentation } from "@opentelemetry/instrumentation-xml-http-request"; import { XMLHttpRequestInstrumentation } from "@opentelemetry/instrumentation-xml-http-request";
import { ZoneContextManager } from "@opentelemetry/context-zone-peer-dep"; import { ZoneContextManager } from "@opentelemetry/context-zone-peer-dep";
export type TelemetryConfig = {
collectorEndpoint: string;
serviceName: string;
serviceNamespace: string;
deploymentEnvironment: string;
};
let initialized = false; let initialized = false;
function escapeRegExp(value: string): string { export function setupTelemetry(config: TelemetryConfig): void {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
export function setupTelemetry(): void {
if (initialized) return; if (initialized) return;
initialized = true; 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( propagation.setGlobalPropagator(
new CompositePropagator({ new CompositePropagator({
propagators: [ propagators: [
@@ -45,14 +39,14 @@ export function setupTelemetry(): void {
const provider = new WebTracerProvider({ const provider = new WebTracerProvider({
resource: resourceFromAttributes({ resource: resourceFromAttributes({
"service.name": serviceName, "service.name": config.serviceName,
"service.namespace": serviceNamespace, "service.namespace": config.serviceNamespace,
"deployment.environment": import.meta.env.MODE, "deployment.environment": config.deploymentEnvironment,
}), }),
spanProcessors: [ spanProcessors: [
new BatchSpanProcessor( new BatchSpanProcessor(
new OTLPTraceExporter({ new OTLPTraceExporter({
url: `${endpoint}/v1/traces`, url: `${config.collectorEndpoint}/v1/traces`,
}), }),
), ),
], ],
@@ -65,11 +59,7 @@ export function setupTelemetry(): void {
registerInstrumentations({ registerInstrumentations({
instrumentations: [ instrumentations: [
new DocumentLoadInstrumentation(), new DocumentLoadInstrumentation(),
new FetchInstrumentation({ new FetchInstrumentation(),
propagateTraceHeaderCorsUrls: [
new RegExp(`^${escapeRegExp(apiBaseUrl)}`),
],
}),
new XMLHttpRequestInstrumentation(), new XMLHttpRequestInstrumentation(),
new UserInteractionInstrumentation(), new UserInteractionInstrumentation(),
], ],

View File

@@ -7,5 +7,8 @@ export default defineConfig({
server: { server: {
host: "0.0.0.0", host: "0.0.0.0",
port: 5173, port: 5173,
proxy: {
"/api": "http://localhost:8000",
},
}, },
}); });