Domagoj Andrić cdf5b56ce3
Some checks failed
CI / test (push) Successful in 54s
CI / test-analytics (push) Successful in 1m59s
CI / build-frontend (push) Failing after 4m37s
CI / build-backend (push) Failing after 21s
additional fixes for CI
2026-05-11 12:10:36 +02:00
2026-05-11 12:10:36 +02:00
2026-05-11 10:58:46 +02:00
2026-05-11 10:58:46 +02:00
2026-03-20 15:13:33 +01:00
2026-03-20 15:13:33 +01:00
2026-05-11 10:58:46 +02:00

OTel BI App

OpenTelemetry-instrumented business intelligence platform with two MSSQL data warehouse sources (AdventureWorks, WorldWideImporters), a PostgreSQL write store, scheduled analytics jobs, an audit log, per-view CSV/PDF data exports, full XLSX+PDF report generation to a shared network path, and an OIDC-secured React frontend.


Architecture

Browser (React + TypeScript)
  │  OIDC Authorization Code + PKCE
  │  Bearer JWT on every API call
  ▼
api-gateway  :8000           ← public surface; validates JWT; mints internal tokens
  ├── /api/aw/**             → aw-service  :8001  (AdventureWorks DW, MSSQL, read-only)
  ├── /api/wwi/**            → wwi-service :8002  (WorldWideImporters DW, MSSQL, read-only)
  ├── /api/reports/generate  → aggregates both services, writes XLSX + PDF to shared mount
  ├── /api/*/export/**       → streams per-view XLSX or PDF directly to browser
  ├── /api/audit             → reads AuditLog from PostgreSQL
  ├── /api/jobs/**           → proxies job history from services
  └── /api/config            → runtime OIDC config (unauthenticated)

aw-service                   AdventureWorks analytics + APScheduler background jobs
wwi-service                  WorldWideImporters analytics + APScheduler background jobs

PostgreSQL                   Shared write store
  ├── audit_log              Append-only event trail
  ├── job_executions         Scheduled job run history
  ├── export_records         Per-view export metadata
  ├── aw_*                   AW persistence tables
  └── wwi_*                  WWI persistence tables

Grafana Alloy (OTLP/HTTP)    Traces + metrics receiver → Tempo / Prometheus

Features

Feature Description
AW Analytics Sales KPIs, 4-year history, linear-regression forecast, rep scores, product demand
WWI Analytics Sales KPIs, reorder recommendations, supplier reliability scores, business events, what-if scenarios
Scheduled Jobs APScheduler cron jobs in each service; per-job OTel root span; job_executions DB record
Audit Log Append-only audit_log table; every analytics call, job, export, and report is logged
Data Export Per-view download as XLSX or PDF; export_records table; OTel span per download
Full Reports POST /api/reports/generate aggregates all views; writes .xlsx + .pdf to SMB/CSI mount
Runtime OIDC Frontend fetches OIDC config from GET /api/config at boot — no build-time secrets baked into the image
Data Quality Daily DQ checks on both warehouses; results surfaced in job history
OTel Frontend W3C trace propagation; FastAPI, SQLAlchemy, HTTPX auto-instrumentation; manual spans on analytics

Repository Layout

.
├── .gitea/workflows/
│   └── docker-publish.yml   # Gitea Actions: build & push all 4 images (matrix)
├── backend/
│   ├── pyproject.toml
│   ├── uv.lock
│   ├── .env.example
│   ├── shared/
│   │   ├── core/
│   │   │   ├── audit.py     # SharedBase, AuditLog, JobExecution, ExportRecord, helpers
│   │   │   ├── config.py    # Pydantic settings (all services)
│   │   │   ├── export.py    # to_xlsx_bytes(), to_pdf_bytes()
│   │   │   ├── otel.py      # configure_otel(), instrument_fastapi/sqlalchemy/httpx
│   │   │   ├── reports.py   # save_report() → XLSX + PDF on filesystem
│   │   │   └── security.py  # JWT validation, internal token mint/verify
│   │   └── db/
│   │       ├── mssql.py     # create_aw_engine(), create_wwi_engine()
│   │       └── postgres.py  # create_postgres_engine(), create_session_factory()
│   └── services/
│       ├── api_gateway/
│       │   └── main.py
│       ├── aw_service/
│       │   ├── main.py
│       │   ├── analytics.py
│       │   ├── models.py
│       │   └── scheduler.py
│       └── wwi_service/
│           ├── main.py
│           ├── analytics.py
│           ├── models.py
│           └── scheduler.py
├── frontend/
│   ├── Dockerfile
│   ├── src/
│   │   ├── api/
│   │   │   └── config.ts    # fetchAppConfig() / getAppConfig()
│   │   ├── auth/
│   │   │   └── oidc.ts
│   │   └── main.tsx
│   └── ...
└── docker-compose.yml

Prerequisites

Tool Version
Docker + Docker Compose 24+
Python 3.11+ (local dev only)
uv latest (local dev only)
Node.js 20+ (local dev only)
SQL Server AdventureWorks DW + WorldWideImporters DW accessible
PostgreSQL 15+

Installation

git clone <repo-url>
cd zavrsni-rad-otel-app

# Copy and fill in environment variables
cp backend/.env.example backend/.env
$EDITOR backend/.env

# Start everything
docker compose up -d

# View logs
docker compose logs -f api-gateway

The frontend is served at http://localhost:5173 (Vite dev server in the compose file) or the container port if using the production image.

Local Development

Backend:

cd backend

# Install uv if needed
pip install uv

# Create virtual environment and install all dependencies
uv sync

# Copy and edit env
cp .env.example .env
$EDITOR .env

# Run services in separate terminals
uvicorn services.api_gateway.main:app  --host 0.0.0.0 --port 8000 --reload
uvicorn services.aw_service.main:app   --host 0.0.0.0 --port 8001 --reload
uvicorn services.wwi_service.main:app  --host 0.0.0.0 --port 8002 --reload

Frontend:

cd frontend
npm install

# Only non-OIDC vars needed at build time
cp .env.example .env.local
# Set VITE_API_BASE_URL=http://localhost:8000
# OIDC config is fetched at runtime from GET /api/config

npm run dev

Configuration Reference

All backend services share shared/core/config.py (Pydantic BaseSettings). Values are read from environment variables or .env.

Database

Variable Default Description
AW_MSSQL_SERVER AdventureWorks SQL Server host
AW_MSSQL_DATABASE AdventureWorksDW2022 Database name
AW_MSSQL_USERNAME SQL login (read-only recommended)
AW_MSSQL_PASSWORD SQL password
WWI_MSSQL_SERVER WorldWideImporters SQL Server host
WWI_MSSQL_DATABASE WideWorldImportersDW Database name
WWI_MSSQL_USERNAME SQL login
WWI_MSSQL_PASSWORD SQL password
MSSQL_TRUST_SERVER_CERTIFICATE false Set true for self-signed certs (dev only)
POSTGRES_URL PostgreSQL connection URL
POSTGRES_SSLMODE prefer require in production

Security

Variable Default Description
REQUIRE_FRONTEND_AUTH true Enforce JWT validation on public endpoints
FRONTEND_JWT_ISSUER_URL OIDC issuer (e.g. https://sso.example.com/realms/myapp)
FRONTEND_JWT_JWKS_URL JWKS endpoint URL
FRONTEND_JWT_AUDIENCE Expected aud claim
FRONTEND_REQUIRED_SCOPES "" Space-separated required scopes
INTERNAL_SERVICE_SHARED_SECRET Shared secret for internal JWT (≥32 bytes)
INTERNAL_SERVICE_ALLOWED_ISSUERS api-gateway Accepted internal token issuers

OIDC (served to frontend at runtime)

Variable Default Description
OIDC_ENABLED false Enable OIDC in the frontend
OIDC_AUTHORITY "" OIDC provider base URL
FRONTEND_OIDC_CLIENT_ID "" Frontend client ID
FRONTEND_OIDC_SCOPE openid profile email Requested scopes

These are returned by GET /api/config (unauthenticated) and consumed by the frontend at boot. They are never baked into the Docker image.

Analytics

Variable Default Description
DEFAULT_HISTORY_DAYS 365 Sales history look-back
FORECAST_HORIZON_DAYS 30 Forecast horizon
RANKING_DEFAULT_TOP_N 10 Default ranking list length
STORAGE_DEFAULT_LIMIT 100 Default record list page size

Reports / Exports

Variable Default Description
REPORT_OUTPUT_DIR /tmp/otel-bi-reports Directory where full reports are written; mount an SMB/CSI volume here in Kubernetes

Observability

Variable Default Description
OTEL_EXPORTER_OTLP_ENDPOINT http://localhost:4318 OTLP/HTTP endpoint (Grafana Alloy)
OTEL_SERVICE_NAME (per service) Service name in traces
LOG_LEVEL INFO Python log level

Frontend (build-time only)

Variable Description
VITE_API_BASE_URL Gateway base URL seen by the browser
VITE_OTEL_COLLECTOR_ENDPOINT OTLP/HTTP endpoint for frontend traces
VITE_OTEL_SERVICE_NAME Frontend service name in traces
VITE_OTEL_SERVICE_NAMESPACE Frontend service namespace

Scheduled Jobs

Jobs run automatically on startup. All jobs are recorded in job_executions and emit an OTel root span.

Job ID Service Schedule (UTC) What it does
aw.daily.forecast aw-service 02:00 daily Recompute sales forecast
aw.daily.scores aw-service 02:30 daily Recompute rep + product demand scores
aw.daily.data_quality aw-service 03:00 daily Data quality checks on AW DW
wwi.hourly.reorder wwi-service :00 every hour Refresh reorder recommendations
wwi.hourly.events wwi-service :30 every hour Scan for stock level events
wwi.daily.supplier_scores wwi-service 03:30 daily Recompute supplier reliability scores
wwi.daily.data_quality wwi-service 04:00 daily Data quality checks on WWI DW

API Reference

Public (require valid Bearer JWT unless REQUIRE_FRONTEND_AUTH=false)

Method Path Description
GET /api/config Runtime OIDC config (unauthenticated)
GET /api/health Gateway health
GET /api/aw/sales/kpis AW sales KPIs
GET /api/aw/sales/history AW sales history (?days_back=)
GET /api/aw/sales/forecast AW sales forecast (?horizon_days=)
GET /api/aw/reps/scores AW rep scores (?top_n=)
GET /api/aw/products/demand AW product demand (?top_n=)
GET /api/wwi/sales/kpis WWI sales KPIs
GET /api/wwi/stock/recommendations WWI reorder recommendations
GET /api/wwi/suppliers/scores WWI supplier scores (?top_n=)
GET /api/wwi/events WWI business events (?limit=)
GET /api/wwi/scenarios List what-if scenarios
POST /api/wwi/scenarios Create what-if scenario
GET /api/aw/records/forecasts Stored AW forecasts
GET /api/aw/records/rep-scores Stored AW rep scores
GET /api/aw/records/product-demand Stored AW product demand
GET /api/wwi/records/reorder-recommendations Stored WWI reorder records
GET /api/wwi/records/supplier-scores Stored WWI supplier scores
GET /api/audit Audit log (?domain=aw|wwi&limit=)
GET /api/jobs/aw AW job history
GET /api/jobs/wwi WWI job history
POST /api/reports/generate Generate full XLSX + PDF report to shared mount

Per-view Exports (download as attachment)

Method Path Format
GET /api/aw/export/sales-history ?format=xlsx|pdf
GET /api/aw/export/sales-forecast ?format=xlsx|pdf
GET /api/aw/export/rep-scores ?format=xlsx|pdf
GET /api/aw/export/product-demand ?format=xlsx|pdf
GET /api/wwi/export/stock-recommendations ?format=xlsx|pdf
GET /api/wwi/export/supplier-scores ?format=xlsx|pdf
GET /api/wwi/export/business-events ?format=xlsx|pdf

PostgreSQL Schema

Table Purpose Mutability
audit_log Append-only event trail for every analytics call, job, export, report Immutable rows
job_executions One row per scheduled job run; updated on completion/failure Mutable
export_records Metadata for every per-view download Immutable rows
aw_forecasts Persisted AW sales forecast points Append
aw_rep_scores Persisted AW rep score snapshots Append
aw_product_demand Persisted AW product demand snapshots Append
wwi_reorder_recommendations Persisted WWI reorder recommendations Append
wwi_supplier_scores Persisted WWI supplier score snapshots Append
wwi_whatif_scenarios What-if scenario results Append
wwi_business_events Stock-level business events Append

All tables are created automatically on service startup via metadata.create_all() (idempotent).


CI/CD (Gitea Actions)

.gitea/workflows/docker-publish.yml builds and pushes all four images in parallel using a matrix strategy.

Required repository variables (vars.):

Name Example
REGISTRY registry.example.com
IMAGE_PREFIX myorg/otel-bi

Required repository secrets (secrets.):

Name Description
REGISTRY_USERNAME Registry login
REGISTRY_PASSWORD Registry password / token

Images are tagged with sha-<short>, branch name, semver (on tags), and latest on pushes to master. Push is skipped on pull requests.


OTel Coverage

Frontend:

  • W3C traceparent/tracestate propagation on all fetch calls
  • @opentelemetry/instrumentation-document-load
  • @opentelemetry/instrumentation-user-interaction
  • @opentelemetry/instrumentation-fetch / XHR
  • Manual spans around dashboard aggregation

Backend (all services):

  • FastAPI auto-instrumentation (request span per endpoint)
  • SQLAlchemy auto-instrumentation (MSSQL + PostgreSQL)
  • HTTPX auto-instrumentation (service-to-service calls)
  • Manual root spans per scheduled job
  • Manual spans on analytics functions
  • x-trace-id / x-span-id response headers on internal endpoints

Read-Only Guarantee (MSSQL)

  • ApplicationIntent=ReadOnly on all MSSQL connection strings
  • Query layer only accepts SELECT / WITH statements
  • All write operations target PostgreSQL only
  • Use a SQL Server login with SELECT grants only — no DDL or DML permissions needed

Verification Checklist

  1. GET /api/config returns OIDC settings without authentication.
  2. GET /api/health returns {"status": "ok"}.
  3. After login, GET /api/aw/sales/kpis returns data.
  4. GET /api/audit shows rows for the KPI call.
  5. GET /api/jobs/aw shows scheduled job runs (populated after the next cron tick or after ~1 minute if jobs fired on startup).
  6. GET /api/aw/export/sales-forecast?format=xlsx downloads an .xlsx file.
  7. POST /api/reports/generate returns paths to .xlsx and .pdf files; confirm they appear in REPORT_OUTPUT_DIR.
  8. In Grafana Tempo, verify a trace spans api-gateway → aw-service → PostgreSQL/MSSQL.
Description
No description provided
Readme 394 KiB
Languages
Python 39.2%
Go 29.7%
TypeScript 29.2%
CSS 1.3%
Dockerfile 0.5%
Other 0.1%