From a586d33908569d089010d7cb30994c7e01fddb31 Mon Sep 17 00:00:00 2001 From: Pieter Hagen Date: Mon, 20 Oct 2025 11:04:13 +0200 Subject: [PATCH 1/7] Sql server changes --- DEMO_WORKFLOW.md | 144 +++++++ cleanup.sh | 30 ++ kubernetes/bundle-k8s-manifest.sh | 161 ------- kubernetes/opentelemetry-demo.yaml | 98 ++++- src/fraud-detection/DEPLOYMENT_CHANGES.md | 288 +++++++++++++ src/fraud-detection/QUICK_START.md | 139 +++++++ src/fraud-detection/READY_TO_DEPLOY.md | 292 +++++++++++++ src/fraud-detection/SINGLE_COMMAND_DEPLOY.md | 392 ++++++++++++++++++ src/fraud-detection/SQL_SERVER_SETUP.md | 322 ++++++++++++++ .../VERSION_2.1.3-sql.2_RELEASE.md | 210 ++++++++++ src/fraud-detection/build.gradle.kts | 4 + .../kubernetes/sqlserver-deployment.yaml | 88 ++++ src/fraud-detection/sql/init-database.sql | 85 ++++ .../kotlin/frauddetection/BadQueryPatterns.kt | 233 +++++++++++ .../kotlin/frauddetection/DatabaseCleanup.kt | 89 ++++ .../kotlin/frauddetection/DatabaseConfig.kt | 149 +++++++ .../kotlin/frauddetection/FraudAnalytics.kt | 258 ++++++++++++ .../frauddetection/OrderLogRepository.kt | 95 +++++ .../kotlin/frauddetection/OrderMutator.kt | 138 ++++++ .../src/main/kotlin/frauddetection/main.kt | 88 +++- src/frontend-proxy/build-proxy.sh | 47 ++- src/frontend-proxy/envoy.tmpl.yaml | 2 +- 22 files changed, 3153 insertions(+), 199 deletions(-) create mode 100644 DEMO_WORKFLOW.md create mode 100755 cleanup.sh delete mode 100755 kubernetes/bundle-k8s-manifest.sh create mode 100644 src/fraud-detection/DEPLOYMENT_CHANGES.md create mode 100644 src/fraud-detection/QUICK_START.md create mode 100644 src/fraud-detection/READY_TO_DEPLOY.md create mode 100644 src/fraud-detection/SINGLE_COMMAND_DEPLOY.md create mode 100644 src/fraud-detection/SQL_SERVER_SETUP.md create mode 100644 src/fraud-detection/VERSION_2.1.3-sql.2_RELEASE.md create mode 100644 src/fraud-detection/kubernetes/sqlserver-deployment.yaml create mode 100644 src/fraud-detection/sql/init-database.sql create mode 100644 src/fraud-detection/src/main/kotlin/frauddetection/BadQueryPatterns.kt create mode 100644 src/fraud-detection/src/main/kotlin/frauddetection/DatabaseCleanup.kt create mode 100644 src/fraud-detection/src/main/kotlin/frauddetection/DatabaseConfig.kt create mode 100644 src/fraud-detection/src/main/kotlin/frauddetection/FraudAnalytics.kt create mode 100644 src/fraud-detection/src/main/kotlin/frauddetection/OrderLogRepository.kt create mode 100644 src/fraud-detection/src/main/kotlin/frauddetection/OrderMutator.kt diff --git a/DEMO_WORKFLOW.md b/DEMO_WORKFLOW.md new file mode 100644 index 0000000000..6d0bb86a9e --- /dev/null +++ b/DEMO_WORKFLOW.md @@ -0,0 +1,144 @@ +# Demo Workflow - Build Up & Tear Down + +Perfect for demos that get built up and torn down frequently! + +## ๐Ÿš€ Deploy (One Command) + +```bash +cd /Users/phagen/GIT/opentelemetry-demo-Splunk +kubectl apply -f kubernetes/opentelemetry-demo.yaml +``` + +Wait ~3 minutes for everything to initialize. + +## ๐Ÿงน Tear Down (One Command) + +```bash +./cleanup.sh +``` + +This script: +- โœ… Deletes all deployments, services, statefulsets +- โœ… Cleans up all PVCs (prevents corrupted data) +- โœ… Ensures fresh start on next deploy + +## What's Configured for Easy Demos + +### SQL Server StatefulSet +**Added automatic PVC cleanup:** +```yaml +persistentVolumeClaimRetentionPolicy: + whenDeleted: Delete # Auto-delete PVC when StatefulSet deleted + whenScaled: Delete # Auto-delete PVC when scaled down +``` + +This means: +- `kubectl delete` will automatically remove the PVC +- No corrupted SQL Server data on next deploy +- Clean slate every time + +### Fraud Detection Service +**Auto-initializes everything:** +- Creates `FraudDetection` database if missing +- Creates `OrderLogs` table if missing +- Logs every Kafka order message + +## Complete Demo Cycle + +### 1. Deploy +```bash +kubectl apply -f kubernetes/opentelemetry-demo.yaml +``` + +### 2. Verify +```bash +# Check pods are running +kubectl get pods -n otel-demo +kubectl get pods -n sql + +# Watch fraud-detection logs +kubectl logs -f -n otel-demo -l app.kubernetes.io/component=fraud-detection +``` + +### 3. Use the Demo +```bash +# Access frontend +kubectl port-forward -n otel-demo svc/frontend 8080:8080 +# Browse to http://localhost:8080 and place orders + +# Access SQL Server +kubectl port-forward -n sql svc/sql-express 1433:1433 +# Connect to localhost:1433 with sa/ChangeMe_SuperStrong123! +``` + +### 4. Query Data +```bash +kubectl exec -it sql-express-0 -n sql -- /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P 'ChangeMe_SuperStrong123!' -Q "SELECT COUNT(*) FROM FraudDetection.dbo.OrderLogs" +``` + +### 5. Tear Down +```bash +./cleanup.sh +``` + +## Troubleshooting + +### SQL Server in CrashLoopBackOff + +**Cause:** Corrupted PVC from previous deployment + +**Fix:** +```bash +kubectl delete statefulset sql-express -n sql +kubectl delete pvc data-sql-express-0 -n sql +kubectl apply -f kubernetes/opentelemetry-demo.yaml +``` + +### Fraud Detection Stuck "waiting for sql-express" + +**Cause:** SQL Server not ready + +**Fix:** +```bash +# Check SQL Server status +kubectl get pods -n sql +kubectl logs sql-express-0 -n sql + +# If SQL Server is crashing, clean and redeploy +./cleanup.sh +kubectl apply -f kubernetes/opentelemetry-demo.yaml +``` + +## Files Modified for Demo Workflow + +### Changed +- `kubernetes/opentelemetry-demo.yaml` + - Line 343-345: Added `persistentVolumeClaimRetentionPolicy` to sql-express StatefulSet + - Line 1372: Updated fraud-detection image to `2.1.3-sql.1` + - Line 1402-1411: Added SQL Server environment variables + - Line 1423-1428: Added wait-for-sqlserver init container + +### Added +- `cleanup.sh` - Automated cleanup script +- `src/fraud-detection/build-fraud-detection.sh` - Build script +- All fraud-detection code for SQL Server logging + +## Quick Reference + +| Action | Command | +|--------|---------| +| Deploy | `kubectl apply -f kubernetes/opentelemetry-demo.yaml` | +| Tear Down | `./cleanup.sh` | +| Watch Logs | `kubectl logs -f -n otel-demo -l app.kubernetes.io/component=fraud-detection` | +| SQL Status | `kubectl get pods -n sql` | +| Access Frontend | `kubectl port-forward -n otel-demo svc/frontend 8080:8080` | +| Access SQL Server | `kubectl port-forward -n sql svc/sql-express 1433:1433` | +| Query Database | See "Query Data" section above | + +## Expected Timeline + +- **Deploy:** 3 minutes +- **Tear Down:** 30 seconds +- **Redeploy:** 2 minutes (with cleanup) + +Perfect for rapid demo cycles! ๐ŸŽฏ diff --git a/cleanup.sh b/cleanup.sh new file mode 100755 index 0000000000..38706b164f --- /dev/null +++ b/cleanup.sh @@ -0,0 +1,30 @@ +#!/bin/bash +# Cleanup script for OpenTelemetry Demo with SQL Server +# This ensures a clean teardown for demos that get built up and torn down often + +set -e + +echo "๐Ÿงน Cleaning up OpenTelemetry Demo..." + +# Delete all resources from the main manifest +echo "Deleting deployments, services, and statefulsets..." +kubectl delete -f kubernetes/opentelemetry-demo.yaml --ignore-not-found=true + +# Give it a moment to process deletions +sleep 2 + +# Delete any remaining PVCs (in case retention policy doesn't work) +echo "Cleaning up PVCs..." +kubectl delete pvc --all -n sql --ignore-not-found=true +kubectl delete pvc --all -n otel-demo --ignore-not-found=true + +# Optional: Delete namespaces for a complete clean +# Uncomment these lines if you want to remove namespaces too +# echo "Deleting namespaces..." +# kubectl delete namespace sql --ignore-not-found=true +# kubectl delete namespace otel-demo --ignore-not-found=true + +echo "โœ… Cleanup complete!" +echo "" +echo "To redeploy, run:" +echo " kubectl apply -f kubernetes/opentelemetry-demo.yaml" diff --git a/kubernetes/bundle-k8s-manifest.sh b/kubernetes/bundle-k8s-manifest.sh deleted file mode 100755 index b7783a7ca5..0000000000 --- a/kubernetes/bundle-k8s-manifest.sh +++ /dev/null @@ -1,161 +0,0 @@ -#!/usr/bin/env bash -# bundle-k8s.sh (bash-compatible) -# Combine *.yml/*.yaml into one multi-document YAML, ordered by kind. - -set -euo pipefail - -print_help() { - cat <<'EOF' -Usage: bundle-k8s.sh -i INPUT_DIR [-o OUTPUT_FILE] [-v VERSION] [-h] - -Combine all Kubernetes manifest YAMLs from INPUT_DIR into OUTPUT_FILE, -ordered by resource kind. - -Required: - -i INPUT_DIR Input directory containing YAML manifests - -Optional: - -o OUTPUT_FILE Output file (default: deployment.yaml) - -v VERSION OpenTelemetry version variable (stored only; not applied yet) - -h Show this help and exit - -Examples: - ./bundle-k8s.sh -i ./manifests - ./bundle-k8s.sh -i ./yamls -o all.yaml -v 1.30.1 -EOF -} - -# Defaults -INPUT_DIR="" -OUTPUT_FILE="deployment.yaml" -OTEL_VERSION="" - -# Parse options (positional args are ignored by design) -while getopts ":hi:o:v:" opt; do - case "$opt" in - h) print_help; exit 0 ;; - i) INPUT_DIR="$OPTARG" ;; - o) OUTPUT_FILE="$OPTARG" ;; - v) OTEL_VERSION="$OPTARG" ;; - \?) echo "Unknown option: -$OPTARG" >&2; print_help; exit 1 ;; - :) echo "Option -$OPTARG requires an argument." >&2; print_help; exit 1 ;; - esac -done - -# Require -i -if [[ -z "${INPUT_DIR}" ]]; then - echo "Error: -i INPUT_DIR is required." >&2 - print_help - exit 1 -fi -if [[ ! -d "${INPUT_DIR}" ]]; then - echo "Input directory not found: ${INPUT_DIR}" >&2 - exit 1 -fi - -# Ensure output directory exists (if a path with directories was provided) -outdir="$(dirname -- "${OUTPUT_FILE}")" -if [[ -n "${outdir}" && "${outdir}" != "." ]]; then - mkdir -p "${outdir}" -fi - -echo "Input dir: ${INPUT_DIR}" -echo "Output file: ${OUTPUT_FILE}" -if [[ -n "${OTEL_VERSION}" ]]; then - echo "OTEL_VERSION set to: ${OTEL_VERSION}" -fi - -# Kind ordering (bash/macOS compatible) -kind_weight() { - local k - k="$(printf '%s' "$1" | tr '[:upper:]' '[:lower:]')" - case "$k" in - customresourcedefinition) echo 10 ;; - namespace) echo 20 ;; - storageclass) echo 25 ;; - priorityclass) echo 28 ;; - serviceaccount) echo 30 ;; - clusterrole) echo 35 ;; - clusterrolebinding) echo 36 ;; - role) echo 37 ;; - rolebinding) echo 38 ;; - podsecuritypolicy) echo 39 ;; - configmap) echo 40 ;; - secret) echo 41 ;; - service) echo 45 ;; - endpointslice|endpoints) echo 46 ;; - ingressclass) echo 47 ;; - ingress) echo 48 ;; - networkpolicy) echo 49 ;; - poddisruptionbudget) echo 50 ;; - daemonset) echo 60 ;; - statefulset) echo 61 ;; - deployment) echo 62 ;; - job) echo 63 ;; - cronjob) echo 64 ;; - horizontalpodautoscaler) echo 70 ;; - destinationrule|virtualservice|gateway) echo 75 ;; - mutatingwebhookconfiguration|validatingwebhookconfiguration) echo 80 ;; - kustomization) echo 99 ;; - *) echo 90 ;; - esac -} - -TMPLIST="$(mktemp)"; trap 'rm -f "$TMPLIST"' EXIT - -# Collect YAML files (handle spaces/newlines safely). Avoid GNU sort -z for macOS. -declare -a files=() -while IFS= read -r -d '' f; do - files+=("$f") -done < <(find "${INPUT_DIR}" -type f \( -iname '*.yaml' -o -iname '*.yml' \) -print0) - -if [[ "${#files[@]}" -eq 0 ]]; then - echo "No YAML files found in ${INPUT_DIR}" >&2 - exit 1 -fi - -# Determine kind and weight for each file -for f in "${files[@]}"; do - kind="$( - LC_ALL=C sed -n 's/^[[:space:]]*[Kk][Ii][Nn][Dd][[:space:]]*:[[:space:]]*\([A-Za-z0-9_.-][A-Za-z0-9_.-]*\).*/\1/p' "$f" | head -n1 - )" - [[ -z "${kind:-}" ]] && kind="Unknown" - w="$(kind_weight "$kind")" - printf "%s\t%s\t%s\n" "$w" "$f" "$kind" >> "$TMPLIST" -done - -# Sort by weight, then filename (portable, no mapfile required) -sorted=() -while IFS= read -r line; do - sorted+=("$line") -done < <(sort -t $'\t' -k1,1n -k2,2 "$TMPLIST") - -{ - echo "# Generated on $(date -u +'%Y-%m-%dT%H:%M:%SZ')" - echo "# Input directory: ${INPUT_DIR}" - if [[ -n "${OTEL_VERSION}" ]]; then - echo "# OpenTelemetry version: ${OTEL_VERSION}" - fi -} > "${OUTPUT_FILE}" - -first=1 -for line in "${sorted[@]}"; do - IFS=$'\t' read -r weight fpath kind <<< "$line" - - if [[ $first -eq 1 ]]; then - first=0 - else - echo "---" >> "${OUTPUT_FILE}" - fi - - echo "# Source: ${fpath}" >> "${OUTPUT_FILE}" - - # Append file contents, stripping a leading UTF-8 BOM if present - if dd if="${fpath}" bs=3 count=1 2>/dev/null | grep -q $'\xEF\xBB\xBF'; then - tail -c +4 "${fpath}" >> "${OUTPUT_FILE}" - else - cat "${fpath}" >> "${OUTPUT_FILE}" - fi -done - -echo "Wrote $(wc -l < "${OUTPUT_FILE}") lines to ${OUTPUT_FILE}" \ No newline at end of file diff --git a/kubernetes/opentelemetry-demo.yaml b/kubernetes/opentelemetry-demo.yaml index 0d17b9d20e..f21ed17e60 100644 --- a/kubernetes/opentelemetry-demo.yaml +++ b/kubernetes/opentelemetry-demo.yaml @@ -84,6 +84,37 @@ data: }, "defaultVariant": "off" }, + "fraudDetectionEnabled": { + "description": "Enable fraud detection analysis on orders", + "state": "ENABLED", + "variants": { + "on": 1, + "off": 0 + }, + "defaultVariant": "on" + }, + "mutateFraudOrders": { + "description": "Percentage of orders to mutate for fraud alerts (0-100%)", + "state": "ENABLED", + "variants": { + "off": 0, + "medium": 40, + "high": 60, + "veryhigh": 90 + }, + "defaultVariant": "off" + }, + "executeBadQueries": { + "description": "Percentage of orders that trigger bad database queries (0-100%)", + "state": "ENABLED", + "variants": { + "off": 0, + "low": 20, + "medium": 40, + "high": 60 + }, + "defaultVariant": "off" + }, "cartFailure": { "description": "Fail cart service", "state": "ENABLED", @@ -340,6 +371,9 @@ metadata: spec: serviceName: sql-express replicas: 1 + persistentVolumeClaimRetentionPolicy: + whenDeleted: Delete + whenScaled: Delete selector: matchLabels: app: sql-express @@ -823,7 +857,7 @@ spec: - name: OTEL_DOTNET_AUTO_TRACES_ENTITYFRAMEWORKCORE_INSTRUMENTATION_ENABLED value: "false" - name: OTEL_RESOURCE_ATTRIBUTES - value: service.name=$(OTEL_SERVICE_NAME),service.namespace=opentelemetry-demo,service.version=2.1.3 + value: service.name=$(OTEL_SERVICE_NAME),service.namespace=opentelemetry-demo,service.version=2.1.3,service.kafka=spanlink - name: SPLUNK_PROFILER_ENABLED value: 'true' - name: SPLUNK_PROFILER_MEMORY_ENABLED @@ -899,7 +933,7 @@ spec: - name: OTEL_LOGS_EXPORTER value: otlp - name: OTEL_RESOURCE_ATTRIBUTES - value: service.name=$(OTEL_SERVICE_NAME),service.namespace=opentelemetry-demo,service.version=2.1.3 + value: service.name=$(OTEL_SERVICE_NAME),service.namespace=opentelemetry-demo,service.version=2.1.3,service.kafka=no - name: SPLUNK_PROFILER_ENABLED value: "true" - name: SPLUNK_PROFILER_MEMORY_ENABLED @@ -985,7 +1019,7 @@ spec: - name: OTEL_EXPORTER_OTLP_ENDPOINT value: http://$(OTEL_COLLECTOR_NAME):4317 - name: OTEL_RESOURCE_ATTRIBUTES - value: service.name=$(OTEL_SERVICE_NAME),service.namespace=opentelemetry-demo,service.version=2.1.3 + value: service.name=$(OTEL_SERVICE_NAME),service.namespace=opentelemetry-demo,service.version=2.1.3,service.kafka=no - name: OTEL_LOGS_EXPORTER value: otlp - name: SPLUNK_PROFILER_ENABLED @@ -1077,7 +1111,7 @@ spec: - name: GOMEMLIMIT value: 16MiB - name: OTEL_RESOURCE_ATTRIBUTES - value: service.name=$(OTEL_SERVICE_NAME),service.namespace=opentelemetry-demo,service.version=2.1.3 + value: service.name=$(OTEL_SERVICE_NAME),service.namespace=opentelemetry-demo,service.version=2.1.3,service.kafka=no resources: limits: memory: 20Mi @@ -1144,7 +1178,7 @@ spec: - name: VERSION value: '2.1.3' - name: OTEL_RESOURCE_ATTRIBUTES - value: service.name=$(OTEL_SERVICE_NAME),service.namespace=opentelemetry-demo,service.version=2.1.3 + value: service.name=$(OTEL_SERVICE_NAME),service.namespace=opentelemetry-demo,service.version=2.1.3,service.kafka=no resources: limits: memory: 20Mi @@ -1208,7 +1242,7 @@ spec: - name: FLAGD_PORT value: "8013" - name: OTEL_RESOURCE_ATTRIBUTES - value: service.name=$(OTEL_SERVICE_NAME),service.namespace=opentelemetry-demo,service.version=2.1.3 + value: service.name=$(OTEL_SERVICE_NAME),service.namespace=opentelemetry-demo,service.version=2.1.3,service.kafka=no resources: limits: memory: 100Mi @@ -1289,8 +1323,8 @@ spec: - containerPort: 4000 name: service env: - - name: OTEL_PROPAGATORS # Disables trace output with none - value: "none" + #- name: OTEL_PROPAGATORS # Disables trace output with none + # value: "none" # - name: OTEL_SERVICE_NAME # valueFrom: # fieldRef: @@ -1369,7 +1403,7 @@ spec: containers: - name: fraud-detection #image: 'ghcr.io/open-telemetry/demo:2.1.3-fraud-detection' - image: ghcr.io/splunk/opentelemetry-demo/otel-fraud-detection:2.1.0 + image: ghcr.io/splunk/opentelemetry-demo/otel-fraud-detection:2.1.3-sql.5 imagePullPolicy: IfNotPresent env: - name: OTEL_SERVICE_NAME @@ -1398,7 +1432,21 @@ spec: - name: OTEL_INSTRUMENTATION_MESSAGING_EXPERIMENTAL_RECEIVE_TELEMETRY_ENABLED value: "true" - name: OTEL_RESOURCE_ATTRIBUTES - value: service.name=$(OTEL_SERVICE_NAME),service.namespace=opentelemetry-demo,service.version=2.1.3 + value: service.name=$(OTEL_SERVICE_NAME),service.namespace=opentelemetry-demo,service.version=2.1.3,service.kafka=spanlink + - name: SQL_SERVER_HOST + value: sql-express.sql.svc.cluster.local + - name: SQL_SERVER_PORT + value: "1433" + - name: SQL_SERVER_DATABASE + value: FraudDetection + - name: SQL_SERVER_USER + value: sa + - name: SQL_SERVER_PASSWORD + value: "ChangeMe_SuperStrong123!" + - name: CLEANUP_RETENTION_DAYS + value: "7" + - name: CLEANUP_INTERVAL_HOURS + value: "24" resources: limits: memory: 450Mi @@ -1410,6 +1458,12 @@ spec: - until nc -z -v -w30 kafka 9092; do echo waiting for kafka; sleep 2; done; image: busybox:latest name: wait-for-kafka + - command: + - sh + - -c + - until nc -z -v -w30 sql-express.sql.svc.cluster.local 1433; do echo waiting for sql-express; sleep 2; done; + image: busybox:latest + name: wait-for-sqlserver volumes: --- # Source: opentelemetry-demo/templates/component.yaml @@ -1494,7 +1548,7 @@ spec: # - name: PUBLIC_OTEL_EXPORTER_OTLP_TRACES_ENDPOINT # value: http://localhost:8080/otlp-http/v1/traces - name: OTEL_RESOURCE_ATTRIBUTES - value: service.name=$(OTEL_SERVICE_NAME),service.namespace=opentelemetry-demo,service.version=2.1.3 + value: service.name=$(OTEL_SERVICE_NAME),service.namespace=opentelemetry-demo,service.version=2.1.3,service.kafka=no - name: SPLUNK_RUM_TOKEN valueFrom: secretKeyRef: @@ -1581,7 +1635,7 @@ spec: - name: FLAGD_PORT value: "8013" - name: FLAGD_UI_HOST - value: flagd-ui + value: flagd - name: FLAGD_UI_PORT value: "4000" - name: FRONTEND_HOST @@ -1603,7 +1657,7 @@ spec: - name: OTEL_COLLECTOR_PORT_HTTP value: "4318" - name: OTEL_RESOURCE_ATTRIBUTES - value: service.name=$(OTEL_SERVICE_NAME),service.namespace=opentelemetry-demo,service.version=2.1.3 + value: service.name=$(OTEL_SERVICE_NAME),service.namespace=opentelemetry-demo,service.version=2.1.3,service.kafka=no - name: FEATURE_AUTH_ENABLED value: "false" # enables auth for /feature - name: FEATURE_USER @@ -1674,7 +1728,7 @@ spec: - name: OTEL_COLLECTOR_HOST value: $(OTEL_COLLECTOR_NAME) - name: OTEL_RESOURCE_ATTRIBUTES - value: service.name=$(OTEL_SERVICE_NAME),service.namespace=opentelemetry-demo,service.version=2.1.3 + value: service.name=$(OTEL_SERVICE_NAME),service.namespace=opentelemetry-demo,service.version=2.1.3,service.kafka=no resources: limits: memory: 50Mi @@ -1742,7 +1796,7 @@ spec: - name: KAFKA_CONTROLLER_QUORUM_VOTERS value: 1@kafka:9093 - name: OTEL_RESOURCE_ATTRIBUTES - value: service.name=$(OTEL_SERVICE_NAME),service.namespace=opentelemetry-demo,service.version=2.1.3 + value: service.name=$(OTEL_SERVICE_NAME),service.namespace=opentelemetry-demo,service.version=2.1.3,service.kafka=no resources: limits: memory: 800Mi @@ -1902,7 +1956,7 @@ spec: - name: OTEL_EXPORTER_OTLP_TRACES_ENDPOINT value: http://$(NODE_IP):4317 - name: OTEL_RESOURCE_ATTRIBUTES - value: service.name=$(OTEL_SERVICE_NAME),service.namespace=opentelemetry-demo,service.version=2.1.3 + value: service.name=$(OTEL_SERVICE_NAME),service.namespace=opentelemetry-demo,service.version=2.1.3,service.kafka=no - name: OTEL_PROFILER_LOGS_ENDPOINT value: http://$(OTEL_COLLECTOR_NAME):4318 - name: SPLUNK_PROFILER_CALL_STACK_INTERVAL @@ -1976,7 +2030,7 @@ spec: - name: POSTGRES_DB value: otel - name: OTEL_RESOURCE_ATTRIBUTES - value: service.name=$(OTEL_SERVICE_NAME),service.namespace=opentelemetry-demo,service.version=2.1.3 + value: service.name=$(OTEL_SERVICE_NAME),service.namespace=opentelemetry-demo,service.version=2.1.3,service.kafka=no resources: limits: memory: 100Mi @@ -2042,7 +2096,7 @@ spec: - name: GOMEMLIMIT value: 16MiB - name: OTEL_RESOURCE_ATTRIBUTES - value: service.name=$(OTEL_SERVICE_NAME),service.namespace=opentelemetry-demo,service.version=2.1.3 + value: service.name=$(OTEL_SERVICE_NAME),service.namespace=opentelemetry-demo,service.version=2.1.3,service.kafka=no resources: limits: memory: 20Mi @@ -2109,7 +2163,7 @@ spec: - name: OTEL_EXPORTER_OTLP_ENDPOINT value: http://$(OTEL_COLLECTOR_NAME):4318 - name: OTEL_RESOURCE_ATTRIBUTES - value: service.name=$(OTEL_SERVICE_NAME),service.namespace=opentelemetry-demo,service.version=2.1.3 + value: service.name=$(OTEL_SERVICE_NAME),service.namespace=opentelemetry-demo,service.version=2.1.3,service.kafka=no resources: limits: memory: 40Mi @@ -2182,7 +2236,7 @@ spec: - name: OTEL_EXPORTER_OTLP_ENDPOINT value: http://$(OTEL_COLLECTOR_NAME):4317 - name: OTEL_RESOURCE_ATTRIBUTES - value: service.name=$(OTEL_SERVICE_NAME),service.namespace=opentelemetry-demo,service.version=2.1.3 + value: service.name=$(OTEL_SERVICE_NAME),service.namespace=opentelemetry-demo,service.version=2.1.3,service.kafka=no - name: OTEL_PROFILER_LOGS_ENDPOINT value: http://$(OTEL_COLLECTOR_NAME):4317 - name: SPLUNK_PROFILER_CALL_STACK_INTERVAL @@ -2248,7 +2302,7 @@ spec: - name: OTEL_EXPORTER_OTLP_ENDPOINT value: http://$(OTEL_COLLECTOR_NAME):4317 - name: OTEL_RESOURCE_ATTRIBUTES - value: service.name=$(OTEL_SERVICE_NAME),service.namespace=opentelemetry-demo,service.version=2.1.3 + value: service.name=$(OTEL_SERVICE_NAME),service.namespace=opentelemetry-demo,service.version=2.1.3,service.kafka=no resources: limits: memory: 20Mi @@ -2302,7 +2356,7 @@ spec: - name: OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE value: cumulative - name: OTEL_RESOURCE_ATTRIBUTES - value: service.name=$(OTEL_SERVICE_NAME),service.namespace=opentelemetry-demo,service.version=2.1.3 + value: service.name=$(OTEL_SERVICE_NAME),service.namespace=opentelemetry-demo,service.version=2.1.3,service.kafka=no resources: limits: memory: 20Mi diff --git a/src/fraud-detection/DEPLOYMENT_CHANGES.md b/src/fraud-detection/DEPLOYMENT_CHANGES.md new file mode 100644 index 0000000000..a7c08a7c5b --- /dev/null +++ b/src/fraud-detection/DEPLOYMENT_CHANGES.md @@ -0,0 +1,288 @@ +# Fraud Detection SQL Server Integration - Deployment Changes + +## Summary + +The fraud-detection service has been updated to log all Kafka messages to the **existing SQL Server Express** deployment in your Kubernetes cluster. + +## Key Findings + +Your `opentelemetry-demo.yaml` already contains a SQL Server Express deployment (lines 303-400): +- **Namespace**: `sql` +- **Service Name**: `sql-express.sql.svc.cluster.local` +- **Secret Name**: `mssql-secrets` +- **Password**: `ChangeMe_SuperStrong123!` (exposed in secret for testing) +- **Resources**: 1Gi-2Gi memory, 250m-1 CPU +- **Storage**: 5Gi PVC via StatefulSet + +## Changes Made to opentelemetry-demo.yaml + +### 1. Fraud Detection Environment Variables (lines 1402-1411) + +**Updated:** +```yaml +- name: SQL_SERVER_HOST + value: sql-express.sql.svc.cluster.local # Changed from 'sqlserver' +- name: SQL_SERVER_PORT + value: "1433" +- name: SQL_SERVER_DATABASE + value: FraudDetection # Will be auto-created +- name: SQL_SERVER_USER + value: sa +- name: SQL_SERVER_PASSWORD + value: "ChangeMe_SuperStrong123!" # Hardcoded for testing (matches existing SQL server) +``` + +**Why:** +- Uses the existing SQL Server Express service instead of creating a new one +- Password matches the existing `mssql-secrets` secret value +- Hardcoded password for testing (you mentioned this is OK) + +### 2. Init Container Added (lines 1423-1428) + +**Added:** +```yaml +- command: + - sh + - -c + - until nc -z -v -w30 sql-express.sql.svc.cluster.local 1433; do echo waiting for sql-express; sleep 2; done; + image: busybox:latest + name: wait-for-sqlserver +``` + +**Why:** +- Ensures SQL Server is ready before fraud-detection starts +- Prevents connection failures on startup +- Matches the pattern used for Kafka init container + +## No Additional SQL Server Deployment Needed + +**You do NOT need to apply the separate sqlserver-deployment.yaml file** I created earlier. The existing SQL Server Express in the main manifest is sufficient. + +## What Happens on Deployment + +1. **SQL Server Express** (already exists) + - Running in `sql` namespace + - Service accessible at `sql-express.sql.svc.cluster.local:1433` + +2. **Fraud Detection** (updated) + - Init container waits for SQL Server to be ready + - On startup, connects to SQL Server + - Auto-creates `FraudDetection` database if it doesn't exist + - Auto-creates `OrderLogs` table with proper schema and indexes + - Begins consuming from Kafka and logging to database + +## Deployment Steps + +### 1. Build the Updated Fraud Detection Image + +```bash +cd /Users/phagen/GIT/opentelemetry-demo-Splunk/src/fraud-detection +./build-fraud-detection.sh + +# Tag with your registry if needed +docker tag fraud-detection:latest YOUR_REGISTRY/fraud-detection:VERSION +docker push YOUR_REGISTRY/fraud-detection:VERSION +``` + +### 2. Update Image Reference (if using custom registry) + +Edit `kubernetes/opentelemetry-demo.yaml` line 1372: +```yaml +# Change from: +image: ghcr.io/splunk/opentelemetry-demo/otel-fraud-detection:2.1.0 + +# To your new image: +image: YOUR_REGISTRY/fraud-detection:VERSION +``` + +### 3. Apply the Updated Deployment + +```bash +cd /Users/phagen/GIT/opentelemetry-demo-Splunk + +# Apply the updated manifest +kubectl apply -f kubernetes/opentelemetry-demo.yaml + +# Or if you only want to update fraud-detection: +kubectl rollout restart deployment/fraud-detection -n otel-demo +``` + +### 4. Verify Deployment + +```bash +# Check fraud-detection pod status +kubectl get pods -n otel-demo -l app.kubernetes.io/component=fraud-detection + +# Watch the logs for successful database initialization +kubectl logs -f -n otel-demo -l app.kubernetes.io/component=fraud-detection + +# Look for these log messages: +# - "Database initialized successfully" +# - "OrderLogs table verified/created successfully" +# - "Consumed record with orderId: ..." +# - "Order logged to database" +``` + +### 5. Check SQL Server + +```bash +# Get SQL Server pod +kubectl get pods -n sql -l app=sql-express + +# Connect to SQL Server +kubectl exec -it sql-express-0 -n sql -- /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P 'ChangeMe_SuperStrong123!' + +# In sqlcmd, run: +# SELECT name FROM sys.databases; +# GO +# USE FraudDetection; +# GO +# SELECT COUNT(*) FROM OrderLogs; +# GO +``` + +## Verification Queries + +Once orders start flowing: + +```sql +-- Port-forward to access SQL Server locally +-- kubectl port-forward -n sql svc/sql-express 1433:1433 + +-- Connect with any SQL client to localhost:1433 +-- Username: sa +-- Password: ChangeMe_SuperStrong123! + +-- View recent orders +USE FraudDetection; +GO + +SELECT TOP 10 + order_id, + shipping_tracking_id, + shipping_city, + shipping_country, + items_count, + consumed_at +FROM OrderLogs +ORDER BY consumed_at DESC; +GO + +-- Count total orders logged +SELECT COUNT(*) as total_orders FROM OrderLogs; +GO + +-- Orders by country +SELECT + shipping_country, + COUNT(*) as order_count +FROM OrderLogs +GROUP BY shipping_country +ORDER BY order_count DESC; +GO +``` + +## Cross-Namespace Communication + +The fraud-detection service (in `otel-demo` namespace) connects to SQL Server (in `sql` namespace) using the fully qualified domain name: + +``` +sql-express.sql.svc.cluster.local +``` + +This works because: +- Kubernetes DNS resolves cross-namespace services +- No NetworkPolicies blocking the traffic (check if needed) +- Both pods can communicate via ClusterIP service + +## Troubleshooting + +### Fraud Detection Can't Connect + +```bash +# Check if SQL Server is running +kubectl get pods -n sql -l app=sql-express + +# Check fraud-detection init container logs +kubectl logs -n otel-demo -l app.kubernetes.io/component=fraud-detection -c wait-for-sqlserver + +# Check main container logs +kubectl logs -n otel-demo -l app.kubernetes.io/component=fraud-detection + +# Test connectivity from fraud-detection pod +kubectl exec -it FRAUD_POD_NAME -n otel-demo -- nc -zv sql-express.sql.svc.cluster.local 1433 +``` + +### Database Not Created + +```bash +# Manually create database +kubectl exec -it sql-express-0 -n sql -- /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P 'ChangeMe_SuperStrong123!' -Q "CREATE DATABASE FraudDetection" + +# Verify +kubectl exec -it sql-express-0 -n sql -- /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P 'ChangeMe_SuperStrong123!' -Q "SELECT name FROM sys.databases" +``` + +### No Orders Being Logged + +1. Check Kafka is producing orders: +```bash +kubectl logs -n otel-demo -l app.kubernetes.io/component=checkout +``` + +2. Check fraud-detection is consuming: +```bash +kubectl logs -n otel-demo -l app.kubernetes.io/component=fraud-detection | grep "Consumed record" +``` + +3. Check for database errors: +```bash +kubectl logs -n otel-demo -l app.kubernetes.io/component=fraud-detection | grep -i "error\|exception\|failed" +``` + +## Files Changed + +### Modified +- `kubernetes/opentelemetry-demo.yaml` - Updated fraud-detection deployment config + +### Code Files (already created) +- `src/fraud-detection/build.gradle.kts` - Added SQL Server dependencies +- `src/fraud-detection/src/main/kotlin/frauddetection/DatabaseConfig.kt` - Database connection +- `src/fraud-detection/src/main/kotlin/frauddetection/OrderLogRepository.kt` - Database operations +- `src/fraud-detection/src/main/kotlin/frauddetection/main.kt` - Integrated database logging + +### Documentation +- `src/fraud-detection/SQL_SERVER_SETUP.md` - Comprehensive setup guide +- `src/fraud-detection/sql/init-database.sql` - Manual database setup script +- `src/fraud-detection/DEPLOYMENT_CHANGES.md` - This file + +### Not Needed +- ~~`src/fraud-detection/kubernetes/sqlserver-deployment.yaml`~~ - Not needed, use existing SQL Server + +## Next Steps + +1. **Build and deploy** the updated fraud-detection service +2. **Generate some orders** through the demo application +3. **Query the database** to verify orders are being logged +4. **Add fraud detection logic** using the logged order data +5. **Create dashboards** to visualize fraud patterns + +## Production Considerations + +For production use, you should: + +1. **Use secrets** instead of hardcoded passwords: + ```yaml + - name: SQL_SERVER_PASSWORD + valueFrom: + secretKeyRef: + name: mssql-secrets + key: SA_PASSWORD + ``` + +2. **Enable SSL/TLS** for database connections +3. **Add resource limits** appropriate for your load +4. **Set up backups** for the SQL Server PVC +5. **Monitor database** performance and connection pool +6. **Implement retry logic** for transient database failures +7. **Add metrics** for database write success/failure rates diff --git a/src/fraud-detection/QUICK_START.md b/src/fraud-detection/QUICK_START.md new file mode 100644 index 0000000000..da3072a738 --- /dev/null +++ b/src/fraud-detection/QUICK_START.md @@ -0,0 +1,139 @@ +# Quick Start - Fraud Detection with SQL Server + +## TL;DR - Deploy Everything + +```bash +cd /Users/phagen/GIT/opentelemetry-demo-Splunk + +# Deploy (one command, everything included) +kubectl apply -f kubernetes/opentelemetry-demo.yaml + +# Watch it start +kubectl get pods -n otel-demo -w +kubectl get pods -n sql -w +``` + +Wait ~3 minutes, then: + +```bash +# View fraud detection logs +kubectl logs -f -n otel-demo -l app.kubernetes.io/component=fraud-detection + +# Access SQL Server +kubectl port-forward -n sql svc/sql-express 1433:1433 +# Connect to localhost:1433 with sa/ChangeMe_SuperStrong123! + +# Generate orders (access frontend) +kubectl port-forward -n otel-demo svc/frontend 8080:8080 +# Browse to http://localhost:8080 and place orders +``` + +## What You Get + +โœ… **SQL Server Express** in `sql` namespace +- Auto-initializes with password `ChangeMe_SuperStrong123!` +- 5Gi persistent storage +- Ready in ~90 seconds + +โœ… **Fraud Detection Service** in `otel-demo` namespace +- Image: `ghcr.io/splunk/opentelemetry-demo/otel-fraud-detection:2.1.3-sql.1` +- **Auto-creates** `FraudDetection` database +- **Auto-creates** `OrderLogs` table with indexes +- Logs every Kafka order message to SQL Server +- Ready in ~150 seconds + +โœ… **Complete OpenTelemetry Demo** +- Frontend, Kafka, Checkout, all services +- Orders flow: Frontend โ†’ Checkout โ†’ Kafka โ†’ Fraud Detection โ†’ SQL Server + +## Verify It's Working + +```bash +# Should see: "Database initialized successfully" +kubectl logs -n otel-demo -l app.kubernetes.io/component=fraud-detection | grep "Database initialized" + +# Should see orders being logged +kubectl logs -n otel-demo -l app.kubernetes.io/component=fraud-detection | grep "logged to database" + +# Query the database +kubectl exec -it sql-express-0 -n sql -- /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P 'ChangeMe_SuperStrong123!' -Q "SELECT COUNT(*) FROM FraudDetection.dbo.OrderLogs" +``` + +## Tear Down + +```bash +# Option 1: Use the cleanup script (RECOMMENDED) +./cleanup.sh + +# Option 2: Manual cleanup +kubectl delete -f kubernetes/opentelemetry-demo.yaml +kubectl delete pvc --all -n sql +kubectl delete pvc --all -n otel-demo +``` + +**Note:** The SQL Server StatefulSet now has `persistentVolumeClaimRetentionPolicy: Delete` configured, so PVCs should auto-delete when you delete the StatefulSet. The cleanup script ensures this happens even if there are issues. + +## Files You Need + +Everything is in one file: +``` +kubernetes/opentelemetry-demo.yaml +``` + +That's it! + +## Image Already Built and Pushed + +``` +ghcr.io/splunk/opentelemetry-demo/otel-fraud-detection:2.1.3-sql.1 +``` + +No build required unless you modify the code. + +## Query Examples + +```sql +-- Connect to SQL Server first +kubectl exec -it sql-express-0 -n sql -- /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P 'ChangeMe_SuperStrong123!' + +-- View recent orders +USE FraudDetection; +GO + +SELECT TOP 10 + order_id, + shipping_city, + shipping_country, + items_count, + consumed_at +FROM OrderLogs +ORDER BY consumed_at DESC; +GO + +-- Count orders by country +SELECT shipping_country, COUNT(*) as count +FROM OrderLogs +GROUP BY shipping_country +ORDER BY count DESC; +GO +``` + +## Timeline + +- **t+0s**: `kubectl apply` starts +- **t+90s**: SQL Server ready +- **t+120s**: Kafka ready +- **t+150s**: Fraud Detection ready, database auto-created +- **t+180s**: First orders logged to database + +## Need Help? + +See detailed guides: +- `SINGLE_COMMAND_DEPLOY.md` - Complete deployment guide +- `DEPLOYMENT_CHANGES.md` - What was changed +- `SQL_SERVER_SETUP.md` - SQL Server details +- `READY_TO_DEPLOY.md` - Build and verification steps + +## That's All! + +One file. One command. Everything works. Perfect for demos! ๐Ÿš€ diff --git a/src/fraud-detection/READY_TO_DEPLOY.md b/src/fraud-detection/READY_TO_DEPLOY.md new file mode 100644 index 0000000000..9725f4bf3d --- /dev/null +++ b/src/fraud-detection/READY_TO_DEPLOY.md @@ -0,0 +1,292 @@ +# Ready to Deploy: Fraud Detection with SQL Server Logging + +## Build Status: โœ… SUCCESS + +**Image Built and Pushed:** +``` +ghcr.io/splunk/opentelemetry-demo/otel-fraud-detection:2.1.3-sql.1 +``` + +**Digest:** +``` +sha256:d000660e4ae80649d431a7ba03557287bc82dae93c862f0b9eccef14ab61450e +``` + +## What's Included in This Build + +### Code Changes +1. **SQL Server JDBC Driver** - Microsoft SQL Server JDBC 12.8.1 +2. **HikariCP Connection Pool** - Enterprise-grade connection pooling +3. **Protobuf JSON Utilities** - For converting OrderResult to JSON +4. **Database Auto-Creation** - Automatically creates database and tables on startup +5. **Kafka Message Logging** - Every order consumed is logged to SQL Server + +### Configuration Updates +- **Image Version**: Updated to `2.1.3-sql.1` in `kubernetes/opentelemetry-demo.yaml` +- **SQL Server Host**: `sql-express.sql.svc.cluster.local` +- **Password**: `ChangeMe_SuperStrong123!` (hardcoded for testing) +- **Init Container**: Added wait-for-sqlserver to ensure DB is ready + +## Deployment Commands + +### Quick Deploy (Apply Everything) +```bash +cd /Users/phagen/GIT/opentelemetry-demo-Splunk + +# Apply the entire manifest (includes existing SQL Server) +kubectl apply -f kubernetes/opentelemetry-demo.yaml +``` + +### Targeted Deploy (Fraud Detection Only) +```bash +# If SQL Server is already running, just update fraud-detection +kubectl rollout restart deployment/fraud-detection -n otel-demo + +# Or delete and recreate +kubectl delete deployment fraud-detection -n otel-demo +kubectl apply -f kubernetes/opentelemetry-demo.yaml +``` + +## Verification Steps + +### 1. Check Pod Status +```bash +# Check fraud-detection is running +kubectl get pods -n otel-demo -l app.kubernetes.io/component=fraud-detection + +# Expected output: +# NAME READY STATUS RESTARTS AGE +# fraud-detection-xxxxxxxxxx-xxxxx 1/1 Running 0 1m +``` + +### 2. Watch Logs +```bash +# Follow fraud-detection logs +kubectl logs -f -n otel-demo -l app.kubernetes.io/component=fraud-detection + +# Look for these SUCCESS indicators: +# โœ“ "Database initialized successfully" +# โœ“ "OrderLogs table verified/created successfully" +# โœ“ "Consumed record with orderId: " +# โœ“ "Order logged to database" +``` + +### 3. Check SQL Server Connection +```bash +# Verify SQL Server is running in sql namespace +kubectl get pods -n sql -l app=sql-express + +# Port-forward to access SQL Server +kubectl port-forward -n sql svc/sql-express 1433:1433 +``` + +### 4. Query the Database +```bash +# Connect using sqlcmd +kubectl exec -it sql-express-0 -n sql -- /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P 'ChangeMe_SuperStrong123!' +``` + +Run in sqlcmd: +```sql +-- Check database exists +SELECT name FROM sys.databases; +GO + +-- Use the database +USE FraudDetection; +GO + +-- Count orders +SELECT COUNT(*) as total_orders FROM OrderLogs; +GO + +-- View recent orders +SELECT TOP 5 + order_id, + shipping_city, + shipping_country, + items_count, + consumed_at +FROM OrderLogs +ORDER BY consumed_at DESC; +GO +``` + +## What Happens When You Deploy + +1. **Init Container Phase** + - `wait-for-kafka` waits for Kafka to be ready (port 9092) + - `wait-for-sqlserver` waits for SQL Server to be ready (port 1433) + +2. **Startup Phase** + - Fraud-detection connects to SQL Server at `sql-express.sql.svc.cluster.local:1433` + - Creates `FraudDetection` database if it doesn't exist + - Creates `OrderLogs` table with proper schema and indexes + - Initializes HikariCP connection pool (2-10 connections) + - Connects to Kafka and subscribes to `orders` topic + +3. **Runtime Phase** + - Consumes messages from Kafka every 100ms + - Parses OrderResult protobuf messages + - Logs each order to database with full details + - Converts order items to JSON for storage + - Records consumption timestamp + +## Database Schema Created + +```sql +CREATE TABLE OrderLogs ( + id BIGINT IDENTITY(1,1) PRIMARY KEY, + order_id NVARCHAR(255) NOT NULL, + shipping_tracking_id NVARCHAR(255), + shipping_cost_currency NVARCHAR(10), + shipping_cost_units BIGINT, + shipping_cost_nanos INT, + shipping_street NVARCHAR(500), + shipping_city NVARCHAR(255), + shipping_state NVARCHAR(255), + shipping_country NVARCHAR(255), + shipping_zip NVARCHAR(50), + items_count INT, + items_json NVARCHAR(MAX), -- Full order details as JSON + consumed_at DATETIME2, -- When message was consumed + created_at DATETIME2 +); + +-- Indexes for performance +CREATE INDEX idx_order_id ON OrderLogs(order_id); +CREATE INDEX idx_consumed_at ON OrderLogs(consumed_at); +``` + +## Expected Logs + +### Successful Startup +``` +Database initialized successfully +OrderLogs table verified/created successfully +Consumed record with orderId: abc123, and updated total count to: 1 +Order abc123 logged to database +``` + +### Feature Flag Handling +``` +FeatureFlag 'kafkaQueueProblems' is enabled, sleeping 1 second +``` + +### Errors (if any) +``` +Failed to initialize database +Failed to log order to database +Exception while logging order to database +``` + +## Troubleshooting + +### Fraud Detection Crashes on Startup + +**Check init container logs:** +```bash +kubectl logs -n otel-demo -c wait-for-sqlserver +kubectl logs -n otel-demo -c wait-for-kafka +``` + +**Check SQL Server is accessible:** +```bash +kubectl exec -it -n otel-demo -- nc -zv sql-express.sql.svc.cluster.local 1433 +``` + +### Database Connection Errors + +**Verify SQL Server password:** +```bash +kubectl get secret mssql-secrets -n sql -o jsonpath='{.data.SA_PASSWORD}' | base64 -d +# Should output: ChangeMe_SuperStrong123! +``` + +**Check SQL Server logs:** +```bash +kubectl logs -n sql sql-express-0 +``` + +### No Orders Being Logged + +**Verify Kafka is producing orders:** +```bash +# Check checkout service is creating orders +kubectl logs -n otel-demo -l app.kubernetes.io/component=checkout | grep -i order +``` + +**Check fraud-detection is consuming:** +```bash +kubectl logs -n otel-demo -l app.kubernetes.io/component=fraud-detection | grep "Consumed record" +``` + +**Check database writes:** +```bash +kubectl logs -n otel-demo -l app.kubernetes.io/component=fraud-detection | grep "logged to database" +``` + +## Next Steps After Deployment + +1. **Generate Test Orders** + - Access the demo frontend + - Place some orders through the UI + - Watch them appear in the database + +2. **Query and Analyze** + ```sql + -- Orders by country + SELECT shipping_country, COUNT(*) as count + FROM OrderLogs + GROUP BY shipping_country + ORDER BY count DESC; + + -- High-value orders + SELECT * FROM OrderLogs + WHERE shipping_cost_units > 20 + ORDER BY consumed_at DESC; + ``` + +3. **Add Fraud Detection Logic** + - Implement scoring in `main.kt` + - Flag suspicious patterns + - Send alerts for fraud + +4. **Create Dashboards** + - Connect BI tools to SQL Server + - Visualize order patterns + - Monitor fraud trends + +## Files Modified + +### Code +- โœ… `src/fraud-detection/build.gradle.kts` - Added dependencies +- โœ… `src/fraud-detection/src/main/kotlin/frauddetection/DatabaseConfig.kt` - New file +- โœ… `src/fraud-detection/src/main/kotlin/frauddetection/OrderLogRepository.kt` - New file +- โœ… `src/fraud-detection/src/main/kotlin/frauddetection/main.kt` - Integrated DB logging + +### Configuration +- โœ… `kubernetes/opentelemetry-demo.yaml` - Updated fraud-detection deployment + +### Documentation +- โœ… `SQL_SERVER_SETUP.md` - Comprehensive setup guide +- โœ… `DEPLOYMENT_CHANGES.md` - Configuration changes +- โœ… `READY_TO_DEPLOY.md` - This file +- โœ… `sql/init-database.sql` - Manual DB initialization script + +## Summary + +Everything is ready to deploy! The fraud-detection service will now: +- โœ… Consume messages from Kafka `orders` topic +- โœ… Parse OrderResult protobuf messages +- โœ… Log every order to SQL Server with full details +- โœ… Store order items as JSON for easy querying +- โœ… Auto-create database and tables on startup +- โœ… Handle errors gracefully with logging + +**Just run:** +```bash +kubectl apply -f kubernetes/opentelemetry-demo.yaml +``` + +And watch the logs to see orders being logged to the database! diff --git a/src/fraud-detection/SINGLE_COMMAND_DEPLOY.md b/src/fraud-detection/SINGLE_COMMAND_DEPLOY.md new file mode 100644 index 0000000000..55396ff044 --- /dev/null +++ b/src/fraud-detection/SINGLE_COMMAND_DEPLOY.md @@ -0,0 +1,392 @@ +# Single Command Deployment Guide + +## โœ… Everything is Self-Contained in One File! + +All components needed for fraud detection with SQL Server logging are included in: +``` +kubernetes/opentelemetry-demo.yaml +``` + +## What Gets Deployed + +When you apply the single YAML file, you get: + +### 1. SQL Server Express (lines 303-400) +- **Namespace**: `sql` +- **Secret**: `mssql-secrets` with password `ChangeMe_SuperStrong123!` +- **StatefulSet**: `sql-express` with 5Gi PVC +- **Service**: `sql-express.sql.svc.cluster.local:1433` +- **Resources**: 1Gi-2Gi memory, 250m-1 CPU +- **Probes**: Readiness and liveness checks + +### 2. Fraud Detection Service (lines 1348-1429) +- **Image**: `ghcr.io/splunk/opentelemetry-demo/otel-fraud-detection:2.1.3-sql.1` +- **Environment**: SQL Server connection configured +- **Init Containers**: + - `wait-for-kafka` - Waits for Kafka to be ready + - `wait-for-sqlserver` - Waits for SQL Server to be ready +- **Functionality**: Consumes Kafka orders and logs to SQL Server + +### 3. All Other Demo Components +- Frontend, Checkout, Payment, Cart, etc. +- Kafka, Flagd, and other services + +## Single Command Deploy + +```bash +cd /Users/phagen/GIT/opentelemetry-demo-Splunk + +# Deploy everything +kubectl apply -f kubernetes/opentelemetry-demo.yaml + +# That's it! One command does it all. +``` + +## What Happens (Automatic Initialization) + +### Phase 1: Infrastructure (0-60 seconds) +``` +1. Namespaces created (otel-demo, sql) +2. Secrets created (mssql-secrets) +3. ConfigMaps created +4. Services created +5. PVCs created (sql-express-data) +``` + +### Phase 2: SQL Server Startup (60-120 seconds) +``` +1. SQL Server StatefulSet starts +2. SQL Server Express initializes +3. SA password configured +4. Readiness probe passes (port 1433 open) +5. Service becomes available: sql-express.sql.svc.cluster.local +``` + +### Phase 3: Kafka & Services (60-180 seconds) +``` +1. Kafka starts up +2. Other demo services start +3. Orders topic created +``` + +### Phase 4: Fraud Detection Startup (120-240 seconds) +``` +1. Init container waits for Kafka (port 9092) +2. Init container waits for SQL Server (port 1433) +3. Main container starts +4. Connects to SQL Server +5. AUTO-CREATES: FraudDetection database +6. AUTO-CREATES: OrderLogs table with indexes +7. Subscribes to Kafka orders topic +8. Starts consuming and logging orders +``` + +## Verify Deployment + +### Quick Status Check +```bash +# Check all pods in otel-demo namespace +kubectl get pods -n otel-demo + +# Check SQL Server in sql namespace +kubectl get pods -n sql + +# Check fraud-detection specifically +kubectl get pods -n otel-demo -l app.kubernetes.io/component=fraud-detection +``` + +### Watch Fraud Detection Logs +```bash +kubectl logs -f -n otel-demo -l app.kubernetes.io/component=fraud-detection + +# Expected output: +# Database initialized successfully +# OrderLogs table verified/created successfully +# Consumed record with orderId: abc123, and updated total count to: 1 +# Order abc123 logged to database +``` + +### Check SQL Server Status +```bash +# Get SQL Server pod +kubectl get pods -n sql -l app=sql-express + +# Expected: +# NAME READY STATUS RESTARTS AGE +# sql-express-0 1/1 Running 0 3m +``` + +### Verify Database Was Auto-Created +```bash +# Connect to SQL Server +kubectl exec -it sql-express-0 -n sql -- /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P 'ChangeMe_SuperStrong123!' + +# Run these commands: +SELECT name FROM sys.databases; +GO +# Should show: FraudDetection + +USE FraudDetection; +GO + +SELECT COUNT(*) as order_count FROM OrderLogs; +GO +# Will show number of orders logged +``` + +## Single Command Teardown + +When you're done with the demo: + +```bash +# Delete everything +kubectl delete -f kubernetes/opentelemetry-demo.yaml + +# Optionally clean up PVCs (will delete SQL Server data) +kubectl delete pvc -n sql --all +kubectl delete pvc -n otel-demo --all + +# Optionally delete namespaces +kubectl delete namespace otel-demo +kubectl delete namespace sql +``` + +## Build โ†’ Deploy โ†’ Test Workflow + +For rapid iteration: + +```bash +# 1. Build new image +cd /Users/phagen/GIT/opentelemetry-demo-Splunk/src/fraud-detection +./build-fraud-detection.sh 2.1.3-sql.1 + +# 2. Deploy (or re-deploy) +cd /Users/phagen/GIT/opentelemetry-demo-Splunk +kubectl apply -f kubernetes/opentelemetry-demo.yaml + +# 3. Watch logs +kubectl logs -f -n otel-demo -l app.kubernetes.io/component=fraud-detection + +# 4. Generate orders (access frontend in browser) +# Then watch them appear in database + +# 5. Query database +kubectl port-forward -n sql svc/sql-express 1433:1433 +# Connect with SQL client to localhost:1433 +``` + +## For Frequent Build/Teardown Cycles + +### Fast Teardown (Keep PVCs) +```bash +# Delete deployments but keep data +kubectl delete deployment,statefulset --all -n otel-demo +kubectl delete statefulset --all -n sql + +# This preserves: +# - PVCs (SQL Server data persists) +# - Secrets (passwords) +# - ConfigMaps +# - Services +``` + +### Fast Redeploy +```bash +# Reapply (much faster since PVCs exist) +kubectl apply -f kubernetes/opentelemetry-demo.yaml + +# SQL Server will: +# - Reattach to existing PVC +# - FraudDetection database already exists +# - OrderLogs table already has data +# - Continue logging new orders +``` + +### Complete Teardown (Delete Everything) +```bash +# Nuclear option - deletes all data +kubectl delete -f kubernetes/opentelemetry-demo.yaml +kubectl delete pvc --all -n otel-demo +kubectl delete pvc --all -n sql + +# Start fresh next time +``` + +## Timeline Expectations + +### First Deployment (Cold Start) +``` +kubectl apply: ~10 seconds +PVC creation: ~30 seconds +SQL Server ready: ~90 seconds +Kafka ready: ~120 seconds +Fraud-detection ready: ~150 seconds +Database auto-created: ~160 seconds +First order logged: ~180 seconds +--- +Total: ~3 minutes +``` + +### Subsequent Deployments (With PVCs) +``` +kubectl apply: ~10 seconds +SQL Server ready: ~30 seconds +Kafka ready: ~60 seconds +Fraud-detection ready: ~90 seconds +Database exists (no creation): ~95 seconds +First order logged: ~120 seconds +--- +Total: ~2 minutes +``` + +### Teardown +``` +kubectl delete: ~30 seconds +PVC cleanup: ~10 seconds (if deleting) +--- +Total: ~40 seconds +``` + +## Troubleshooting + +### Fraud Detection Won't Start + +**Check init containers:** +```bash +POD=$(kubectl get pods -n otel-demo -l app.kubernetes.io/component=fraud-detection -o jsonpath='{.items[0].metadata.name}') + +# Check if waiting for SQL Server +kubectl logs -n otel-demo $POD -c wait-for-sqlserver + +# Check if waiting for Kafka +kubectl logs -n otel-demo $POD -c wait-for-kafka +``` + +**Common causes:** +- SQL Server not ready yet (wait 2-3 minutes) +- PVC provisioning slow (check PVC status) +- Insufficient cluster resources + +### SQL Server Won't Start + +```bash +# Check SQL Server pod +kubectl describe pod sql-express-0 -n sql + +# Check PVC +kubectl get pvc -n sql + +# Common issues: +# - No storage class available +# - Insufficient disk space +# - Security context issues +``` + +**Fix storage class:** +```bash +# List available storage classes +kubectl get sc + +# If none exist, create a local-path one (for testing): +kubectl apply -f https://raw.githubusercontent.com/rancher/local-path-provisioner/master/deploy/local-path-storage.yaml +``` + +### Database Not Created + +```bash +# Check fraud-detection logs for errors +kubectl logs -n otel-demo -l app.kubernetes.io/component=fraud-detection | grep -i "database\|error\|exception" + +# Manually create if needed: +kubectl exec -it sql-express-0 -n sql -- /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P 'ChangeMe_SuperStrong123!' -Q "CREATE DATABASE FraudDetection" +``` + +### No Orders Appearing + +```bash +# 1. Check if orders are being created +kubectl logs -n otel-demo -l app.kubernetes.io/component=checkout | grep -i order + +# 2. Check Kafka +kubectl logs -n otel-demo -l app=kafka + +# 3. Check fraud-detection is consuming +kubectl logs -n otel-demo -l app.kubernetes.io/component=fraud-detection | grep "Consumed record" + +# 4. Check database writes +kubectl logs -n otel-demo -l app.kubernetes.io/component=fraud-detection | grep "logged to database" +``` + +## Advanced: Update Image Without Full Redeploy + +If you only changed fraud-detection code: + +```bash +# Build new image (increment version) +./build-fraud-detection.sh 2.1.3-sql.2 + +# Update the image in YAML +# Edit kubernetes/opentelemetry-demo.yaml line 1372 + +# Restart just fraud-detection +kubectl rollout restart deployment/fraud-detection -n otel-demo + +# Or patch the image directly (no YAML edit needed) +kubectl set image deployment/fraud-detection fraud-detection=ghcr.io/splunk/opentelemetry-demo/otel-fraud-detection:2.1.3-sql.2 -n otel-demo +``` + +## Port Forwarding for Local Access + +### SQL Server +```bash +# Forward SQL Server port +kubectl port-forward -n sql svc/sql-express 1433:1433 + +# Connect with: +# Host: localhost +# Port: 1433 +# User: sa +# Password: ChangeMe_SuperStrong123! +# Database: FraudDetection +``` + +### Frontend +```bash +# Forward frontend (to place orders) +kubectl port-forward -n otel-demo svc/frontend 8080:8080 + +# Access: http://localhost:8080 +``` + +## Validation Checklist + +After deployment, verify: + +- [ ] `kubectl get pods -n sql` shows sql-express-0 Running +- [ ] `kubectl get pods -n otel-demo` shows fraud-detection Running +- [ ] `kubectl logs -n otel-demo -l app.kubernetes.io/component=fraud-detection` shows "Database initialized" +- [ ] SQL Server has FraudDetection database +- [ ] OrderLogs table exists with proper schema +- [ ] Orders are being logged (count increases) + +## Summary + +**Single file contains everything:** +โœ… SQL Server Express with persistent storage +โœ… Fraud Detection service with SQL logging +โœ… All demo components (Frontend, Kafka, etc.) +โœ… Automatic database and table creation +โœ… Init containers ensure proper startup order +โœ… Cross-namespace networking configured + +**Single command deploys:** +```bash +kubectl apply -f kubernetes/opentelemetry-demo.yaml +``` + +**Single command tears down:** +```bash +kubectl delete -f kubernetes/opentelemetry-demo.yaml +``` + +**Perfect for demos that build up and tear down often!** diff --git a/src/fraud-detection/SQL_SERVER_SETUP.md b/src/fraud-detection/SQL_SERVER_SETUP.md new file mode 100644 index 0000000000..cf6c80f683 --- /dev/null +++ b/src/fraud-detection/SQL_SERVER_SETUP.md @@ -0,0 +1,322 @@ +# SQL Server Integration for Fraud Detection Service + +## Overview + +The fraud-detection service now logs every Kafka message to a SQL Server database running in your Kubernetes cluster. Each order message consumed from the `orders` topic is persisted to the `OrderLogs` table with full order details. + +## Architecture + +``` +Kafka (orders topic) โ†’ Fraud Detection Service โ†’ SQL Server (OrderLogs table) +``` + +## Database Schema + +### OrderLogs Table + +| Column | Type | Description | +|--------|------|-------------| +| id | BIGINT (PK) | Auto-increment primary key | +| order_id | NVARCHAR(255) | Order ID from Kafka message | +| shipping_tracking_id | NVARCHAR(255) | Shipping tracking number | +| shipping_cost_currency | NVARCHAR(10) | Currency code (e.g., USD) | +| shipping_cost_units | BIGINT | Whole units of shipping cost | +| shipping_cost_nanos | INT | Nano units of shipping cost | +| shipping_street | NVARCHAR(500) | Shipping street address | +| shipping_city | NVARCHAR(255) | Shipping city | +| shipping_state | NVARCHAR(255) | Shipping state | +| shipping_country | NVARCHAR(255) | Shipping country | +| shipping_zip | NVARCHAR(50) | Shipping ZIP code | +| items_count | INT | Number of items in order | +| items_json | NVARCHAR(MAX) | Full order JSON (protobuf โ†’ JSON) | +| consumed_at | DATETIME2 | Timestamp when message was consumed | +| created_at | DATETIME2 | Timestamp when record was created | + +**Indexes:** +- `idx_order_id` on `order_id` +- `idx_consumed_at` on `consumed_at` +- `idx_shipping_country` on `shipping_country` +- `idx_created_at` on `created_at` + +## Deployment Steps + +### 1. Deploy SQL Server to Kubernetes + +```bash +# Apply SQL Server deployment +kubectl apply -f src/fraud-detection/kubernetes/sqlserver-deployment.yaml + +# Wait for SQL Server to be ready +kubectl wait --for=condition=ready pod -l app=sqlserver -n otel-demo --timeout=300s + +# Check SQL Server pod status +kubectl get pods -n otel-demo -l app=sqlserver +``` + +### 2. Verify SQL Server Connection + +```bash +# Get the SQL Server pod name +SQL_POD=$(kubectl get pods -n otel-demo -l app=sqlserver -o jsonpath='{.items[0].metadata.name}') + +# Connect to SQL Server using sqlcmd +kubectl exec -it $SQL_POD -n otel-demo -- /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P 'YourStrong!Passw0rd' +``` + +Inside sqlcmd: +```sql +SELECT @@VERSION; +GO +``` + +### 3. (Optional) Initialize Database Manually + +The fraud-detection service automatically creates the database and table on startup. However, you can initialize it manually if needed: + +```bash +# Copy the SQL script to the pod +kubectl cp src/fraud-detection/sql/init-database.sql $SQL_POD:/tmp/init-database.sql -n otel-demo + +# Execute the script +kubectl exec -it $SQL_POD -n otel-demo -- /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P 'YourStrong!Passw0rd' -i /tmp/init-database.sql +``` + +### 4. Build and Deploy Fraud Detection Service + +```bash +# Build the new Docker image with SQL Server support +cd src/fraud-detection +./build-fraud-detection.sh + +# Update the image in your Kubernetes deployment +# Edit kubernetes/opentelemetry-demo.yaml and update the fraud-detection image tag + +# Apply the updated deployment (already contains SQL Server env vars) +kubectl apply -f ../../kubernetes/opentelemetry-demo.yaml + +# Check fraud-detection pod logs +kubectl logs -f -n otel-demo -l app.kubernetes.io/component=fraud-detection +``` + +## Environment Variables + +The following environment variables are configured in the fraud-detection deployment: + +| Variable | Value | Description | +|----------|-------|-------------| +| SQL_SERVER_HOST | sqlserver | Kubernetes service name for SQL Server | +| SQL_SERVER_PORT | 1433 | SQL Server port | +| SQL_SERVER_DATABASE | FraudDetection | Database name | +| SQL_SERVER_USER | sa | SQL Server username | +| SQL_SERVER_PASSWORD | (from secret) | SQL Server password from sqlserver-secret | + +## Configuration + +### Change SQL Server Password + +Edit the secret before deploying: + +```bash +# Edit the password in kubernetes/sqlserver-deployment.yaml +# Or create a new secret: +kubectl create secret generic sqlserver-secret \ + --from-literal=password='YourNewStrongPassword123!' \ + -n otel-demo \ + --dry-run=client -o yaml | kubectl apply -f - +``` + +### Adjust SQL Server Resources + +Edit `src/fraud-detection/kubernetes/sqlserver-deployment.yaml`: + +```yaml +resources: + requests: + memory: "2Gi" + cpu: "1000m" + limits: + memory: "4Gi" + cpu: "2000m" +``` + +## Querying the Data + +### Connect to SQL Server + +```bash +# Port-forward to access SQL Server from your local machine +kubectl port-forward -n otel-demo svc/sqlserver 1433:1433 + +# Connect using any SQL Server client: +# - Azure Data Studio +# - SQL Server Management Studio (SSMS) +# - sqlcmd +# - DBeaver + +# Connection details: +# Host: localhost +# Port: 1433 +# Database: FraudDetection +# Username: sa +# Password: YourStrong!Passw0rd +``` + +### Example Queries + +```sql +-- View most recent orders +SELECT TOP 10 * FROM OrderLogs ORDER BY consumed_at DESC; + +-- Count orders by country +SELECT shipping_country, COUNT(*) as order_count +FROM OrderLogs +GROUP BY shipping_country +ORDER BY order_count DESC; + +-- Find high-value orders (shipping cost > $20) +SELECT order_id, shipping_cost_units, shipping_country, consumed_at +FROM OrderLogs +WHERE shipping_cost_units >= 20 +ORDER BY shipping_cost_units DESC; + +-- Order volume by hour +SELECT + DATEPART(HOUR, consumed_at) as hour, + COUNT(*) as order_count +FROM OrderLogs +WHERE consumed_at >= DATEADD(DAY, -1, GETDATE()) +GROUP BY DATEPART(HOUR, consumed_at) +ORDER BY hour; + +-- View full order JSON details +SELECT + order_id, + items_count, + JSON_VALUE(items_json, '$.orderId') as json_order_id, + items_json, + consumed_at +FROM OrderLogs +ORDER BY consumed_at DESC; +``` + +## Monitoring + +### Check Fraud Detection Logs + +```bash +# View logs to confirm database writes +kubectl logs -f -n otel-demo -l app.kubernetes.io/component=fraud-detection + +# Look for these log messages: +# - "Database initialized successfully" +# - "Successfully saved order to database" +# - "Order logged to database" +``` + +### Monitor SQL Server + +```bash +# Check SQL Server pod +kubectl get pods -n otel-demo -l app=sqlserver + +# View SQL Server logs +kubectl logs -n otel-demo -l app=sqlserver + +# Check SQL Server resource usage +kubectl top pod -n otel-demo -l app=sqlserver +``` + +## Troubleshooting + +### Fraud Detection Can't Connect to SQL Server + +1. Check SQL Server is running: +```bash +kubectl get pods -n otel-demo -l app=sqlserver +``` + +2. Verify secret exists: +```bash +kubectl get secret sqlserver-secret -n otel-demo +``` + +3. Check fraud-detection logs for connection errors: +```bash +kubectl logs -n otel-demo -l app.kubernetes.io/component=fraud-detection | grep -i "database\|sql" +``` + +### Database Table Not Created + +The fraud-detection service auto-creates the table on startup. If it fails: + +1. Manually create using the SQL script: +```bash +kubectl exec -it $SQL_POD -n otel-demo -- /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P 'YourStrong!Passw0rd' -Q "CREATE DATABASE FraudDetection" +``` + +2. Check database exists: +```bash +kubectl exec -it $SQL_POD -n otel-demo -- /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P 'YourStrong!Passw0rd' -Q "SELECT name FROM sys.databases" +``` + +### Performance Issues + +If the database writes are slow: + +1. Increase SQL Server resources in `sqlserver-deployment.yaml` +2. Add more database indexes +3. Consider using async writes or batching in the application +4. Check persistent volume performance + +## Code Structure + +### New Files Added + +- `src/main/kotlin/frauddetection/DatabaseConfig.kt` - Database connection pooling with HikariCP +- `src/main/kotlin/frauddetection/OrderLogRepository.kt` - Repository for database operations +- `kubernetes/sqlserver-deployment.yaml` - K8s resources for SQL Server +- `sql/init-database.sql` - Database initialization script + +### Modified Files + +- `build.gradle.kts` - Added SQL Server JDBC and HikariCP dependencies +- `src/main/kotlin/frauddetection/main.kt` - Integrated database logging in Kafka consumer +- `../../kubernetes/opentelemetry-demo.yaml` - Added SQL Server environment variables to fraud-detection + +## Next Steps + +### Fraud Detection Features to Add + +1. **Fraud Scoring**: Analyze orders and calculate fraud probability +2. **Alerting**: Send alerts when suspicious orders detected +3. **Dashboard**: Create visualization of fraud patterns +4. **ML Integration**: Train models on historical order data +5. **Real-time Actions**: Block/hold suspicious orders automatically + +### Example Fraud Detection Logic + +```kotlin +fun calculateFraudScore(order: OrderResult): Double { + var score = 0.0 + + // High-value order + if (order.shippingCost.units > 100) score += 0.3 + + // Multiple items + if (order.itemsCount > 10) score += 0.2 + + // International shipping to high-risk countries + val highRiskCountries = listOf("XX", "YY") // Add actual countries + if (order.shippingAddress.country in highRiskCountries) score += 0.4 + + // Add more rules... + + return score +} +``` + +## References + +- [SQL Server on Kubernetes](https://learn.microsoft.com/en-us/sql/linux/sql-server-linux-kubernetes-deploy) +- [HikariCP Documentation](https://github.com/brettwooldridge/HikariCP) +- [Microsoft JDBC Driver for SQL Server](https://learn.microsoft.com/en-us/sql/connect/jdbc/microsoft-jdbc-driver-for-sql-server) diff --git a/src/fraud-detection/VERSION_2.1.3-sql.2_RELEASE.md b/src/fraud-detection/VERSION_2.1.3-sql.2_RELEASE.md new file mode 100644 index 0000000000..6b5f6c9de9 --- /dev/null +++ b/src/fraud-detection/VERSION_2.1.3-sql.2_RELEASE.md @@ -0,0 +1,210 @@ +# Version 2.1.3-sql.2 Release Notes + +## โœ… Build Complete and Pushed + +**Image:** +``` +ghcr.io/splunk/opentelemetry-demo/otel-fraud-detection:2.1.3-sql.2 +``` + +**Digest:** +``` +sha256:e1d1420470b7670e850c4cafbbd2d7c40d22aa84a585019280253c449f51258d +``` + +## What's Fixed in This Version + +### 1. Database Auto-Creation โœ… +**Problem:** Service crashed because `FraudDetection` database didn't exist +**Solution:** Now connects to `master` database first, creates `FraudDetection` if needed + +### 2. SLF4J Logging Warnings โœ… +**Problem:** SLF4J warnings about missing providers +**Solution:** Added `log4j-slf4j2-impl` bridge dependency + +### 3. PVC Retention Policy โœ… +**Problem:** Corrupted SQL Server data persisted between deployments +**Solution:** Added `persistentVolumeClaimRetentionPolicy: Delete` to StatefulSet + +## Code Changes + +### DatabaseConfig.kt +- Added `createDatabaseIfNotExists()` function +- Connects to `master` database to check if `FraudDetection` exists +- Creates database if missing +- Then connects to `FraudDetection` database for normal operations + +### build.gradle.kts +- Added `log4j-slf4j2-impl:2.25.2` for SLF4J bridge +- Added `protobuf-java-util` for JSON conversion + +### opentelemetry-demo.yaml +- Updated fraud-detection image to `2.1.3-sql.2` +- Added `persistentVolumeClaimRetentionPolicy` to sql-express StatefulSet + +## Deployment Instructions + +### Clean Deployment (Recommended) + +```bash +# 1. Clean up any existing deployment +./cleanup.sh + +# 2. Deploy fresh +kubectl apply -f kubernetes/opentelemetry-demo.yaml + +# 3. Watch fraud-detection start +kubectl logs -f -n otel-demo -l app.kubernetes.io/component=fraud-detection +``` + +### Expected Startup Logs + +``` +Database 'FraudDetection' does not exist. Creating... +Database 'FraudDetection' created successfully +Database connection pool initialized: jdbc:sqlserver://sql-express.sql.svc.cluster.local:1433;databaseName=FraudDetection +OrderLogs table verified/created successfully +Consumed record with orderId: abc123, and updated total count to: 1 +Order abc123 logged to database +``` + +### Update Existing Deployment + +If you already have the demo running: + +```bash +# Option 1: Update just fraud-detection +kubectl set image deployment/fraud-detection fraud-detection=ghcr.io/splunk/opentelemetry-demo/otel-fraud-detection:2.1.3-sql.2 -n otel-demo + +# Option 2: Rollout restart +kubectl rollout restart deployment/fraud-detection -n otel-demo + +# Option 3: Full redeploy +./cleanup.sh +kubectl apply -f kubernetes/opentelemetry-demo.yaml +``` + +## Timeline + +### Clean Deploy +- t+0s: `kubectl apply` starts +- t+90s: SQL Server ready +- t+120s: Kafka ready +- t+150s: Fraud Detection ready +- t+160s: **Database auto-created** โœ… +- t+165s: **Table auto-created** โœ… +- t+180s: First order logged + +### With Corrupted PVC (Old Issue - Now Fixed) +- ~~SQL Server crashes~~ โ†’ Now PVCs auto-delete on teardown + +## Verification + +### 1. Check Pod is Running +```bash +kubectl get pods -n otel-demo -l app.kubernetes.io/component=fraud-detection +# Expected: STATUS=Running +``` + +### 2. Check Logs +```bash +kubectl logs -n otel-demo -l app.kubernetes.io/component=fraud-detection + +# Look for: +# โœ… "Database 'FraudDetection' created successfully" (or "already exists") +# โœ… "Database connection pool initialized" +# โœ… "OrderLogs table verified/created successfully" +# โœ… "Order logged to database" +``` + +### 3. Verify Database +```bash +kubectl exec -it sql-express-0 -n sql -- /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P 'ChangeMe_SuperStrong123!' -Q "SELECT name FROM sys.databases WHERE name='FraudDetection'" + +# Expected output: +# name +# --------------- +# FraudDetection +``` + +### 4. Check Data +```bash +kubectl exec -it sql-express-0 -n sql -- /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P 'ChangeMe_SuperStrong123!' -Q "SELECT COUNT(*) as order_count FROM FraudDetection.dbo.OrderLogs" + +# Should show number of orders logged +``` + +## Troubleshooting + +### If Fraud Detection Still Crashes + +**Check SQL Server is running:** +```bash +kubectl get pods -n sql +# sql-express-0 should be Running, not CrashLoopBackOff +``` + +**If SQL Server is crashing:** +```bash +# Clean and redeploy +./cleanup.sh +kubectl apply -f kubernetes/opentelemetry-demo.yaml +``` + +### If Database Not Created + +**Check logs for errors:** +```bash +kubectl logs -n otel-demo -l app.kubernetes.io/component=fraud-detection | grep -i "database\|error" +``` + +**Manually create if needed:** +```bash +kubectl exec -it sql-express-0 -n sql -- /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P 'ChangeMe_SuperStrong123!' -Q "CREATE DATABASE FraudDetection" +``` + +## Files Modified + +### Code Changes +- `src/fraud-detection/src/main/kotlin/frauddetection/DatabaseConfig.kt` + - Added database auto-creation logic + - Lines 52-78: New `createDatabaseIfNotExists()` function + +- `src/fraud-detection/build.gradle.kts` + - Line 46: Added `log4j-slf4j2-impl` dependency + +### Configuration +- `kubernetes/opentelemetry-demo.yaml` + - Line 1375: Updated image to `2.1.3-sql.2` + - Lines 343-345: Added PVC retention policy + +### Scripts +- `cleanup.sh` - New automated cleanup script + +## What Works Now + +โœ… SQL Server deploys with auto-cleanup PVC policy +โœ… Fraud Detection auto-creates `FraudDetection` database +โœ… Fraud Detection auto-creates `OrderLogs` table +โœ… Logs every Kafka order message to SQL Server +โœ… No SLF4J warnings +โœ… Clean teardown with `./cleanup.sh` +โœ… Perfect for frequent demo cycles + +## Quick Reference + +| Action | Command | +|--------|---------| +| Deploy | `kubectl apply -f kubernetes/opentelemetry-demo.yaml` | +| Teardown | `./cleanup.sh` | +| Logs | `kubectl logs -f -n otel-demo -l app.kubernetes.io/component=fraud-detection` | +| SQL Status | `kubectl get pods -n sql` | +| Query DB | `kubectl exec -it sql-express-0 -n sql -- /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P 'ChangeMe_SuperStrong123!' -Q "SELECT COUNT(*) FROM FraudDetection.dbo.OrderLogs"` | + +## Ready to Deploy! ๐Ÿš€ + +Everything is tested and working. Just run: +```bash +./cleanup.sh +kubectl apply -f kubernetes/opentelemetry-demo.yaml +``` diff --git a/src/fraud-detection/build.gradle.kts b/src/fraud-detection/build.gradle.kts index 6f4656c96e..a417f1d539 100644 --- a/src/fraud-detection/build.gradle.kts +++ b/src/fraud-detection/build.gradle.kts @@ -29,6 +29,7 @@ repositories { dependencies { implementation("com.google.protobuf:protobuf-java:${protobufVersion}") + implementation("com.google.protobuf:protobuf-java-util:${protobufVersion}") testImplementation(kotlin("test")) implementation(kotlin("script-runtime")) implementation("org.apache.kafka:kafka-clients:4.1.0") @@ -42,9 +43,12 @@ dependencies { implementation("io.opentelemetry:opentelemetry-extension-annotations:1.18.0") implementation("org.apache.logging.log4j:log4j-core:2.25.2") implementation("org.slf4j:slf4j-api:2.0.17") + implementation("org.apache.logging.log4j:log4j-slf4j2-impl:2.25.2") implementation("com.google.protobuf:protobuf-kotlin:${protobufVersion}") implementation("dev.openfeature:sdk:1.18.2") implementation("dev.openfeature.contrib.providers:flagd:0.11.15") + implementation("com.microsoft.sqlserver:mssql-jdbc:12.8.1.jre11") + implementation("com.zaxxer:HikariCP:5.1.0") if (JavaVersion.current().isJava9Compatible) { // Workaround for @javax.annotation.Generated diff --git a/src/fraud-detection/kubernetes/sqlserver-deployment.yaml b/src/fraud-detection/kubernetes/sqlserver-deployment.yaml new file mode 100644 index 0000000000..69d028cd5d --- /dev/null +++ b/src/fraud-detection/kubernetes/sqlserver-deployment.yaml @@ -0,0 +1,88 @@ +--- +apiVersion: v1 +kind: Secret +metadata: + name: sqlserver-secret + namespace: otel-demo +type: Opaque +stringData: + password: "YourStrong!Passw0rd" # Change this in production! +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: sqlserver-pvc + namespace: otel-demo +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 5Gi +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: sqlserver + namespace: otel-demo + labels: + app: sqlserver + app.kubernetes.io/part-of: opentelemetry-demo +spec: + replicas: 1 + selector: + matchLabels: + app: sqlserver + template: + metadata: + labels: + app: sqlserver + spec: + containers: + - name: sqlserver + image: mcr.microsoft.com/mssql/server:2022-latest + ports: + - containerPort: 1433 + name: sqlserver + env: + - name: ACCEPT_EULA + value: "Y" + - name: MSSQL_SA_PASSWORD + valueFrom: + secretKeyRef: + name: sqlserver-secret + key: password + - name: MSSQL_PID + value: "Developer" + volumeMounts: + - name: sqlserver-data + mountPath: /var/opt/mssql + resources: + requests: + memory: "2Gi" + cpu: "1000m" + limits: + memory: "4Gi" + cpu: "2000m" + volumes: + - name: sqlserver-data + persistentVolumeClaim: + claimName: sqlserver-pvc +--- +apiVersion: v1 +kind: Service +metadata: + name: sqlserver + namespace: otel-demo + labels: + app: sqlserver + app.kubernetes.io/part-of: opentelemetry-demo +spec: + type: ClusterIP + ports: + - port: 1433 + targetPort: 1433 + protocol: TCP + name: sqlserver + selector: + app: sqlserver diff --git a/src/fraud-detection/sql/init-database.sql b/src/fraud-detection/sql/init-database.sql new file mode 100644 index 0000000000..cdfad3c862 --- /dev/null +++ b/src/fraud-detection/sql/init-database.sql @@ -0,0 +1,85 @@ +-- SQL Server Database Initialization Script +-- This script creates the FraudDetection database and OrderLogs table +-- Note: The table is also auto-created by the fraud-detection service on startup + +-- Create database if it doesn't exist +IF NOT EXISTS (SELECT name FROM sys.databases WHERE name = 'FraudDetection') +BEGIN + CREATE DATABASE FraudDetection; +END +GO + +USE FraudDetection; +GO + +-- Create OrderLogs table +IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'OrderLogs') +BEGIN + CREATE TABLE OrderLogs ( + id BIGINT IDENTITY(1,1) PRIMARY KEY, + order_id NVARCHAR(255) NOT NULL, + shipping_tracking_id NVARCHAR(255), + shipping_cost_currency NVARCHAR(10), + shipping_cost_units BIGINT, + shipping_cost_nanos INT, + shipping_street NVARCHAR(500), + shipping_city NVARCHAR(255), + shipping_state NVARCHAR(255), + shipping_country NVARCHAR(255), + shipping_zip NVARCHAR(50), + items_count INT, + items_json NVARCHAR(MAX), + consumed_at DATETIME2 DEFAULT GETDATE(), + created_at DATETIME2 DEFAULT GETDATE() + ); + + -- Create indexes for better query performance + CREATE INDEX idx_order_id ON OrderLogs(order_id); + CREATE INDEX idx_consumed_at ON OrderLogs(consumed_at); + CREATE INDEX idx_shipping_country ON OrderLogs(shipping_country); + CREATE INDEX idx_created_at ON OrderLogs(created_at); +END +GO + +-- Verify table creation +SELECT + 'OrderLogs table created successfully' AS Status, + COUNT(*) AS RecordCount +FROM OrderLogs; +GO + +-- Example queries to use after data is populated: + +-- Query 1: View most recent orders +-- SELECT TOP 10 * FROM OrderLogs ORDER BY consumed_at DESC; + +-- Query 2: Orders by country +-- SELECT shipping_country, COUNT(*) as order_count +-- FROM OrderLogs +-- GROUP BY shipping_country +-- ORDER BY order_count DESC; + +-- Query 3: Orders with high shipping costs (over $20) +-- SELECT order_id, shipping_cost_currency, shipping_cost_units, shipping_street, shipping_city, shipping_country +-- FROM OrderLogs +-- WHERE shipping_cost_units >= 20 +-- ORDER BY shipping_cost_units DESC; + +-- Query 4: Order volume by hour +-- SELECT +-- DATEPART(HOUR, consumed_at) as hour, +-- COUNT(*) as order_count +-- FROM OrderLogs +-- WHERE consumed_at >= DATEADD(DAY, -1, GETDATE()) +-- GROUP BY DATEPART(HOUR, consumed_at) +-- ORDER BY hour; + +-- Query 5: View full order details with JSON +-- SELECT +-- order_id, +-- shipping_tracking_id, +-- items_count, +-- items_json, +-- consumed_at +-- FROM OrderLogs +-- ORDER BY consumed_at DESC; diff --git a/src/fraud-detection/src/main/kotlin/frauddetection/BadQueryPatterns.kt b/src/fraud-detection/src/main/kotlin/frauddetection/BadQueryPatterns.kt new file mode 100644 index 0000000000..3b3abde1ba --- /dev/null +++ b/src/fraud-detection/src/main/kotlin/frauddetection/BadQueryPatterns.kt @@ -0,0 +1,233 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package frauddetection + +import org.apache.logging.log4j.LogManager +import org.apache.logging.log4j.Logger +import kotlin.random.Random + +/** + * Executes intentionally inefficient database queries to demonstrate + * database monitoring and performance analysis capabilities. + * + * WARNING: These queries are intentionally bad for demo purposes only! + * They demonstrate: + * - Full table scans without indexes + * - N+1 query problems + * - Missing WHERE clauses + * - Inefficient JOIN patterns + * - Excessive data retrieval + */ +class BadQueryPatterns { + private val logger: Logger = LogManager.getLogger(BadQueryPatterns::class.java) + + /** + * Randomly executes one of several intentionally bad query patterns. + * Returns true if a bad query was executed. + * @param executionPercentage The percentage (0-100) chance to execute a bad query + */ + fun maybeExecuteBadQuery(executionPercentage: Int): Boolean { + if (executionPercentage <= 0 || Random.nextInt(100) >= executionPercentage) { + return false + } + + val queryType = Random.nextInt(6) + + try { + when (queryType) { + 0 -> fullTableScanWithoutWhere() + 1 -> selectStarFromLargeTables() + 2 -> inefficientLikeQuery() + 3 -> redundantSubquery() + 4 -> nPlusOnePattern() + 5 -> expensiveAggregationWithoutIndex() + } + return true + } catch (e: Exception) { + logger.error("Error executing bad query pattern $queryType", e) + return false + } + } + + /** + * Pattern 1: Full table scan without WHERE clause + * Impact: Reads entire table, high I/O, slow performance + */ + private fun fullTableScanWithoutWhere() { + DatabaseConfig.getConnection().use { conn -> + val sql = """ + SELECT COUNT(*) as total + FROM OrderLogs + """.trimIndent() + + conn.createStatement().use { stmt -> + stmt.executeQuery(sql).use { rs -> + if (rs.next()) { + val count = rs.getInt("total") + logger.warn("โš ๏ธ BAD QUERY: Full table scan on OrderLogs, total=$count") + } + } + } + } + } + + /** + * Pattern 2: SELECT * from large tables + * Impact: Excessive data transfer, memory usage + */ + private fun selectStarFromLargeTables() { + DatabaseConfig.getConnection().use { conn -> + val sql = """ + SELECT TOP 1000 * + FROM OrderLogs + """.trimIndent() + + conn.createStatement().use { stmt -> + stmt.executeQuery(sql).use { rs -> + var rowCount = 0 + while (rs.next()) { + rowCount++ + // Intentionally retrieve all columns but don't use them + rs.getString("order_id") + rs.getString("items_json") + } + logger.warn("โš ๏ธ BAD QUERY: SELECT * retrieved $rowCount rows with all columns") + } + } + } + } + + /** + * Pattern 3: Inefficient LIKE query without proper indexing + * Impact: Can't use index effectively, full table scan + */ + private fun inefficientLikeQuery() { + DatabaseConfig.getConnection().use { conn -> + val sql = """ + SELECT order_id, shipping_street + FROM OrderLogs + WHERE shipping_street LIKE '%Box%' + """.trimIndent() + + conn.createStatement().use { stmt -> + stmt.executeQuery(sql).use { rs -> + var matchCount = 0 + while (rs.next()) { + matchCount++ + } + logger.warn("โš ๏ธ BAD QUERY: Inefficient LIKE with leading wildcard, matches=$matchCount") + } + } + } + } + + /** + * Pattern 4: Redundant subquery that could be simplified + * Impact: Multiple query executions, inefficient execution plan + */ + private fun redundantSubquery() { + DatabaseConfig.getConnection().use { conn -> + val sql = """ + SELECT o.order_id, o.shipping_cost_units, + (SELECT COUNT(*) FROM FraudAlerts WHERE order_id = o.order_id) as alert_count + FROM OrderLogs o + WHERE o.id > (SELECT MAX(id) - 100 FROM OrderLogs) + """.trimIndent() + + conn.createStatement().use { stmt -> + stmt.executeQuery(sql).use { rs -> + var rowCount = 0 + while (rs.next()) { + rowCount++ + } + logger.warn("โš ๏ธ BAD QUERY: Redundant correlated subquery, processed $rowCount rows") + } + } + } + } + + /** + * Pattern 5: N+1 query problem simulation + * Impact: Multiple round trips to database, high latency + */ + private fun nPlusOnePattern() { + DatabaseConfig.getConnection().use { conn -> + // First query: Get orders + val ordersSql = "SELECT TOP 10 order_id FROM OrderLogs ORDER BY id DESC" + val orderIds = mutableListOf() + + conn.createStatement().use { stmt -> + stmt.executeQuery(ordersSql).use { rs -> + while (rs.next()) { + orderIds.add(rs.getString("order_id")) + } + } + } + + // N queries: Get fraud alerts for each order (N+1 problem!) + var totalQueries = 1 + orderIds.forEach { orderId -> + val alertSql = "SELECT COUNT(*) as cnt FROM FraudAlerts WHERE order_id = ?" + conn.prepareStatement(alertSql).use { stmt -> + stmt.setString(1, orderId) + stmt.executeQuery().use { rs -> + if (rs.next()) { + totalQueries++ + } + } + } + } + + logger.warn("โš ๏ธ BAD QUERY: N+1 problem, executed $totalQueries queries instead of 1 JOIN") + } + } + + /** + * Pattern 6: Expensive aggregation without proper indexing + * Impact: Full table scan for aggregation, high CPU usage + */ + private fun expensiveAggregationWithoutIndex() { + DatabaseConfig.getConnection().use { conn -> + val sql = """ + SELECT + shipping_country, + shipping_city, + COUNT(*) as order_count, + AVG(CAST(shipping_cost_units AS FLOAT)) as avg_shipping_cost, + MAX(items_count) as max_items + FROM OrderLogs + WHERE shipping_country IS NOT NULL + GROUP BY shipping_country, shipping_city + ORDER BY order_count DESC + """.trimIndent() + + conn.createStatement().use { stmt -> + stmt.executeQuery(sql).use { rs -> + var groupCount = 0 + while (rs.next()) { + groupCount++ + } + logger.warn("โš ๏ธ BAD QUERY: Expensive aggregation without index, $groupCount groups") + } + } + } + } + + /** + * Execute a specific bad query pattern by type (for testing) + */ + fun executeBadQueryByType(type: Int) { + when (type) { + 0 -> fullTableScanWithoutWhere() + 1 -> selectStarFromLargeTables() + 2 -> inefficientLikeQuery() + 3 -> redundantSubquery() + 4 -> nPlusOnePattern() + 5 -> expensiveAggregationWithoutIndex() + else -> logger.warn("Unknown bad query type: $type") + } + } +} diff --git a/src/fraud-detection/src/main/kotlin/frauddetection/DatabaseCleanup.kt b/src/fraud-detection/src/main/kotlin/frauddetection/DatabaseCleanup.kt new file mode 100644 index 0000000000..ecd7e6059d --- /dev/null +++ b/src/fraud-detection/src/main/kotlin/frauddetection/DatabaseCleanup.kt @@ -0,0 +1,89 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package frauddetection + +import org.apache.logging.log4j.LogManager +import org.apache.logging.log4j.Logger +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit + +class DatabaseCleanup { + private val logger: Logger = LogManager.getLogger(DatabaseCleanup::class.java) + private val scheduler = Executors.newSingleThreadScheduledExecutor() + + fun startCleanupScheduler(retentionDays: Int = 7, intervalHours: Long = 24) { + logger.info("Starting database cleanup scheduler: retentionDays=$retentionDays, intervalHours=$intervalHours") + + scheduler.scheduleAtFixedRate({ + try { + cleanupOldRecords(retentionDays) + } catch (e: Exception) { + logger.error("Error during scheduled cleanup", e) + } + }, 1, intervalHours, TimeUnit.HOURS) + } + + fun cleanupOldRecords(retentionDays: Int): Int { + return try { + DatabaseConfig.getConnection().use { conn -> + val sql = """ + DELETE FROM OrderLogs + WHERE consumed_at < DATEADD(DAY, -?, GETDATE()) + """.trimIndent() + + conn.prepareStatement(sql).use { stmt -> + stmt.setInt(1, retentionDays) + val deletedCount = stmt.executeUpdate() + + if (deletedCount > 0) { + logger.info("Cleaned up $deletedCount old records (older than $retentionDays days)") + } else { + logger.debug("No old records to clean up") + } + + deletedCount + } + } + } catch (e: Exception) { + logger.error("Failed to cleanup old records", e) + 0 + } + } + + fun cleanupAllRecords(): Int { + return try { + DatabaseConfig.getConnection().use { conn -> + val sql = "TRUNCATE TABLE OrderLogs" + conn.createStatement().use { stmt -> + stmt.execute(sql) + logger.info("Truncated OrderLogs table (all records deleted)") + } + + // Get count that was deleted + val countSql = "SELECT COUNT(*) as cnt FROM OrderLogs" + conn.createStatement().use { stmt -> + val rs = stmt.executeQuery(countSql) + if (rs.next()) rs.getInt("cnt") else 0 + } + } + } catch (e: Exception) { + logger.error("Failed to truncate OrderLogs", e) + 0 + } + } + + fun stop() { + logger.info("Stopping database cleanup scheduler") + scheduler.shutdown() + try { + if (!scheduler.awaitTermination(5, TimeUnit.SECONDS)) { + scheduler.shutdownNow() + } + } catch (e: InterruptedException) { + scheduler.shutdownNow() + } + } +} diff --git a/src/fraud-detection/src/main/kotlin/frauddetection/DatabaseConfig.kt b/src/fraud-detection/src/main/kotlin/frauddetection/DatabaseConfig.kt new file mode 100644 index 0000000000..d9c96bb601 --- /dev/null +++ b/src/fraud-detection/src/main/kotlin/frauddetection/DatabaseConfig.kt @@ -0,0 +1,149 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package frauddetection + +import com.zaxxer.hikari.HikariConfig +import com.zaxxer.hikari.HikariDataSource +import org.apache.logging.log4j.LogManager +import org.apache.logging.log4j.Logger +import java.sql.Connection +import javax.sql.DataSource + +object DatabaseConfig { + private val logger: Logger = LogManager.getLogger(DatabaseConfig::class.java) + + private lateinit var dataSource: HikariDataSource + + fun initialize() { + val host = System.getenv("SQL_SERVER_HOST") ?: "localhost" + val port = System.getenv("SQL_SERVER_PORT") ?: "1433" + val database = System.getenv("SQL_SERVER_DATABASE") ?: "FraudDetection" + val username = System.getenv("SQL_SERVER_USER") ?: "sa" + val password = System.getenv("SQL_SERVER_PASSWORD") ?: throw IllegalStateException("SQL_SERVER_PASSWORD is required") + + // First, connect to master database to create the FraudDetection database if needed + createDatabaseIfNotExists(host, port, database, username, password) + + // Now connect to the FraudDetection database + val jdbcUrl = "jdbc:sqlserver://$host:$port;databaseName=$database;encrypt=false;trustServerCertificate=true" + + val config = HikariConfig().apply { + this.jdbcUrl = jdbcUrl + this.username = username + this.password = password + this.driverClassName = "com.microsoft.sqlserver.jdbc.SQLServerDriver" + maximumPoolSize = 10 + minimumIdle = 2 + connectionTimeout = 30000 + idleTimeout = 600000 + maxLifetime = 1800000 + } + + dataSource = HikariDataSource(config) + logger.info("Database connection pool initialized: $jdbcUrl") + + // Create table if it doesn't exist + createTableIfNotExists() + } + + private fun createDatabaseIfNotExists(host: String, port: String, database: String, username: String, password: String) { + // Connect to master database + val masterUrl = "jdbc:sqlserver://$host:$port;databaseName=master;encrypt=false;trustServerCertificate=true" + + try { + java.sql.DriverManager.getConnection(masterUrl, username, password).use { conn -> + val statement = conn.createStatement() + + // Check if database exists + val checkDbSQL = "SELECT database_id FROM sys.databases WHERE name = '$database'" + val resultSet = statement.executeQuery(checkDbSQL) + + if (!resultSet.next()) { + // Database doesn't exist, create it + logger.info("Database '$database' does not exist. Creating...") + val createDbSQL = "CREATE DATABASE [$database]" + statement.execute(createDbSQL) + logger.info("Database '$database' created successfully") + } else { + logger.info("Database '$database' already exists") + } + } + } catch (e: Exception) { + logger.error("Failed to create database '$database'", e) + throw e + } + } + + fun getConnection(): Connection { + return dataSource.connection + } + + fun close() { + if (::dataSource.isInitialized && !dataSource.isClosed) { + dataSource.close() + logger.info("Database connection pool closed") + } + } + + private fun createTableIfNotExists() { + getConnection().use { conn -> + val statement = conn.createStatement() + + // Create OrderLogs table + val createOrderLogsSQL = """ + IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'OrderLogs') + BEGIN + CREATE TABLE OrderLogs ( + id BIGINT IDENTITY(1,1) PRIMARY KEY, + order_id NVARCHAR(255) NOT NULL, + shipping_tracking_id NVARCHAR(255), + shipping_cost_currency NVARCHAR(10), + shipping_cost_units BIGINT, + shipping_cost_nanos INT, + shipping_street NVARCHAR(500), + shipping_city NVARCHAR(255), + shipping_state NVARCHAR(255), + shipping_country NVARCHAR(255), + shipping_zip NVARCHAR(50), + items_count INT, + items_json NVARCHAR(MAX), + consumed_at DATETIME2 DEFAULT GETDATE(), + created_at DATETIME2 DEFAULT GETDATE() + ); + CREATE INDEX idx_order_id ON OrderLogs(order_id); + CREATE INDEX idx_consumed_at ON OrderLogs(consumed_at); + END + """.trimIndent() + + statement.execute(createOrderLogsSQL) + logger.info("OrderLogs table verified/created successfully") + + // Create FraudAlerts table + val createFraudAlertsSQL = """ + IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'FraudAlerts') + BEGIN + CREATE TABLE FraudAlerts ( + id BIGINT IDENTITY(1,1) PRIMARY KEY, + order_id NVARCHAR(255) NOT NULL, + alert_type NVARCHAR(50) NOT NULL, + severity NVARCHAR(20) NOT NULL, + reason NVARCHAR(MAX), + risk_score FLOAT NOT NULL, + detected_at DATETIME2 DEFAULT GETDATE(), + created_at DATETIME2 DEFAULT GETDATE() + ); + CREATE INDEX idx_fraud_order_id ON FraudAlerts(order_id); + CREATE INDEX idx_fraud_detected_at ON FraudAlerts(detected_at); + CREATE INDEX idx_fraud_severity ON FraudAlerts(severity); + CREATE INDEX idx_fraud_risk_score ON FraudAlerts(risk_score); + END + """.trimIndent() + + statement.execute(createFraudAlertsSQL) + logger.info("FraudAlerts table verified/created successfully") + } + } +} diff --git a/src/fraud-detection/src/main/kotlin/frauddetection/FraudAnalytics.kt b/src/fraud-detection/src/main/kotlin/frauddetection/FraudAnalytics.kt new file mode 100644 index 0000000000..b80cbd5dc9 --- /dev/null +++ b/src/fraud-detection/src/main/kotlin/frauddetection/FraudAnalytics.kt @@ -0,0 +1,258 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package frauddetection + +import org.apache.logging.log4j.LogManager +import org.apache.logging.log4j.Logger +import oteldemo.Demo.OrderResult +import java.sql.Timestamp +import java.time.Instant + +data class FraudAlert( + val orderId: String, + val alertType: String, + val severity: String, + val reason: String, + val riskScore: Double, + val detectedAt: Timestamp = Timestamp.from(Instant.now()) +) + +class FraudAnalytics { + private val logger: Logger = LogManager.getLogger(FraudAnalytics::class.java) + + companion object { + const val SEVERITY_LOW = "LOW" + const val SEVERITY_MEDIUM = "MEDIUM" + const val SEVERITY_HIGH = "HIGH" + const val SEVERITY_CRITICAL = "CRITICAL" + } + + fun analyzeOrder(order: OrderResult): FraudAlert? { + val riskScore = calculateRiskScore(order) + + if (riskScore > 0.5) { + val alert = FraudAlert( + orderId = order.orderId, + alertType = determineAlertType(order, riskScore), + severity = determineSeverity(riskScore), + reason = buildReasonMessage(order, riskScore), + riskScore = riskScore + ) + + logger.warn("Fraud alert generated: orderId=${alert.orderId}, severity=${alert.severity}, score=$riskScore") + saveFraudAlert(alert) + return alert + } + + return null + } + + private fun calculateRiskScore(order: OrderResult): Double { + var score = 0.0 + + // High value order (shipping cost > $50) + if (order.hasShippingCost() && order.shippingCost.units >= 50) { + score += 0.3 + } + + // Very high value (shipping cost > $100) + if (order.hasShippingCost() && order.shippingCost.units >= 100) { + score += 0.3 + } + + // Large quantity of items + if (order.itemsCount > 10) { + score += 0.2 + } + + // Very large quantity + if (order.itemsCount > 20) { + score += 0.2 + } + + // Check for high-risk countries + val highRiskCountries = setOf("XX", "YY", "ZZ") // Placeholder + if (order.hasShippingAddress() && order.shippingAddress.country in highRiskCountries) { + score += 0.4 + } + + // Check shipping to known fraud regions (example pattern) + if (order.hasShippingAddress()) { + val address = order.shippingAddress + // Example: PO Boxes might be suspicious + if (address.streetAddress.contains("P.O. Box", ignoreCase = true) || + address.streetAddress.contains("PO Box", ignoreCase = true)) { + score += 0.15 + } + } + + return score.coerceAtMost(1.0) + } + + private fun determineAlertType(order: OrderResult, riskScore: Double): String { + return when { + order.hasShippingCost() && order.shippingCost.units >= 100 -> "HIGH_VALUE_ORDER" + order.itemsCount > 20 -> "BULK_ORDER" + riskScore >= 0.8 -> "CRITICAL_RISK" + else -> "SUSPICIOUS_PATTERN" + } + } + + private fun determineSeverity(riskScore: Double): String { + return when { + riskScore >= 0.9 -> SEVERITY_CRITICAL + riskScore >= 0.7 -> SEVERITY_HIGH + riskScore >= 0.5 -> SEVERITY_MEDIUM + else -> SEVERITY_LOW + } + } + + private fun buildReasonMessage(order: OrderResult, riskScore: Double): String { + val reasons = mutableListOf() + + if (order.hasShippingCost() && order.shippingCost.units >= 100) { + reasons.add("Very high shipping cost: \$${order.shippingCost.units}") + } else if (order.hasShippingCost() && order.shippingCost.units >= 50) { + reasons.add("High shipping cost: \$${order.shippingCost.units}") + } + + if (order.itemsCount > 20) { + reasons.add("Very large quantity: ${order.itemsCount} items") + } else if (order.itemsCount > 10) { + reasons.add("Large quantity: ${order.itemsCount} items") + } + + if (order.hasShippingAddress()) { + val address = order.shippingAddress + if (address.streetAddress.contains("P.O. Box", ignoreCase = true)) { + reasons.add("PO Box shipping address") + } + } + + if (reasons.isEmpty()) { + reasons.add("Multiple risk factors detected") + } + + return reasons.joinToString("; ") + } + + private fun saveFraudAlert(alert: FraudAlert): Boolean { + return try { + DatabaseConfig.getConnection().use { conn -> + val sql = """ + INSERT INTO FraudAlerts ( + order_id, + alert_type, + severity, + reason, + risk_score, + detected_at + ) VALUES (?, ?, ?, ?, ?, ?) + """.trimIndent() + + conn.prepareStatement(sql).use { stmt -> + stmt.setString(1, alert.orderId) + stmt.setString(2, alert.alertType) + stmt.setString(3, alert.severity) + stmt.setString(4, alert.reason) + stmt.setDouble(5, alert.riskScore) + stmt.setTimestamp(6, alert.detectedAt) + + val rowsAffected = stmt.executeUpdate() + if (rowsAffected > 0) { + logger.info("Saved fraud alert for order ${alert.orderId}") + true + } else { + logger.warn("Failed to save fraud alert for order ${alert.orderId}") + false + } + } + } + } catch (e: Exception) { + logger.error("Error saving fraud alert for order ${alert.orderId}", e) + false + } + } + + // Analytics queries + fun getAlertStats(hours: Int = 24): Map { + return try { + DatabaseConfig.getConnection().use { conn -> + val sql = """ + SELECT + COUNT(*) as total_alerts, + AVG(risk_score) as avg_risk_score, + MAX(risk_score) as max_risk_score, + SUM(CASE WHEN severity = 'CRITICAL' THEN 1 ELSE 0 END) as critical_count, + SUM(CASE WHEN severity = 'HIGH' THEN 1 ELSE 0 END) as high_count, + SUM(CASE WHEN severity = 'MEDIUM' THEN 1 ELSE 0 END) as medium_count, + SUM(CASE WHEN severity = 'LOW' THEN 1 ELSE 0 END) as low_count + FROM FraudAlerts + WHERE detected_at >= DATEADD(HOUR, -?, GETDATE()) + """.trimIndent() + + conn.prepareStatement(sql).use { stmt -> + stmt.setInt(1, hours) + val rs = stmt.executeQuery() + + if (rs.next()) { + mapOf( + "total_alerts" to rs.getInt("total_alerts"), + "avg_risk_score" to rs.getDouble("avg_risk_score"), + "max_risk_score" to rs.getDouble("max_risk_score"), + "critical_count" to rs.getInt("critical_count"), + "high_count" to rs.getInt("high_count"), + "medium_count" to rs.getInt("medium_count"), + "low_count" to rs.getInt("low_count"), + "hours" to hours + ) + } else { + emptyMap() + } + } + } + } catch (e: Exception) { + logger.error("Error getting alert stats", e) + emptyMap() + } + } + + fun getTopRiskyCountries(limit: Int = 10): List> { + return try { + DatabaseConfig.getConnection().use { conn -> + val sql = """ + SELECT TOP (?) + o.shipping_country, + COUNT(fa.id) as alert_count, + AVG(fa.risk_score) as avg_risk_score + FROM FraudAlerts fa + JOIN OrderLogs o ON fa.order_id = o.order_id + WHERE o.shipping_country IS NOT NULL + GROUP BY o.shipping_country + ORDER BY alert_count DESC, avg_risk_score DESC + """.trimIndent() + + conn.prepareStatement(sql).use { stmt -> + stmt.setInt(1, limit) + val rs = stmt.executeQuery() + + val results = mutableListOf>() + while (rs.next()) { + results.add(mapOf( + "country" to rs.getString("shipping_country"), + "alert_count" to rs.getInt("alert_count"), + "avg_risk_score" to rs.getDouble("avg_risk_score") + )) + } + results + } + } + } catch (e: Exception) { + logger.error("Error getting top risky countries", e) + emptyList() + } + } +} diff --git a/src/fraud-detection/src/main/kotlin/frauddetection/OrderLogRepository.kt b/src/fraud-detection/src/main/kotlin/frauddetection/OrderLogRepository.kt new file mode 100644 index 0000000000..1faaad665a --- /dev/null +++ b/src/fraud-detection/src/main/kotlin/frauddetection/OrderLogRepository.kt @@ -0,0 +1,95 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package frauddetection + +import org.apache.logging.log4j.LogManager +import org.apache.logging.log4j.Logger +import oteldemo.Demo.OrderResult +import com.google.protobuf.util.JsonFormat +import java.sql.Timestamp +import java.time.Instant + +class OrderLogRepository { + private val logger: Logger = LogManager.getLogger(OrderLogRepository::class.java) + private val jsonPrinter = JsonFormat.printer() + + fun saveOrder(orderResult: OrderResult): Boolean { + return try { + DatabaseConfig.getConnection().use { conn -> + val sql = """ + INSERT INTO OrderLogs ( + order_id, + shipping_tracking_id, + shipping_cost_currency, + shipping_cost_units, + shipping_cost_nanos, + shipping_street, + shipping_city, + shipping_state, + shipping_country, + shipping_zip, + items_count, + items_json, + consumed_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """.trimIndent() + + conn.prepareStatement(sql).use { stmt -> + stmt.setString(1, orderResult.orderId) + stmt.setString(2, orderResult.shippingTrackingId) + + // Shipping cost + if (orderResult.hasShippingCost()) { + stmt.setString(3, orderResult.shippingCost.currencyCode) + stmt.setLong(4, orderResult.shippingCost.units) + stmt.setInt(5, orderResult.shippingCost.nanos) + } else { + stmt.setNull(3, java.sql.Types.NVARCHAR) + stmt.setNull(4, java.sql.Types.BIGINT) + stmt.setNull(5, java.sql.Types.INTEGER) + } + + // Shipping address + if (orderResult.hasShippingAddress()) { + stmt.setString(6, orderResult.shippingAddress.streetAddress) + stmt.setString(7, orderResult.shippingAddress.city) + stmt.setString(8, orderResult.shippingAddress.state) + stmt.setString(9, orderResult.shippingAddress.country) + stmt.setString(10, orderResult.shippingAddress.zipCode) + } else { + stmt.setNull(6, java.sql.Types.NVARCHAR) + stmt.setNull(7, java.sql.Types.NVARCHAR) + stmt.setNull(8, java.sql.Types.NVARCHAR) + stmt.setNull(9, java.sql.Types.NVARCHAR) + stmt.setNull(10, java.sql.Types.NVARCHAR) + } + + // Items + stmt.setInt(11, orderResult.itemsCount) + + // Convert items to JSON + val itemsJson = jsonPrinter.print(orderResult) + stmt.setString(12, itemsJson) + + // Timestamp + stmt.setTimestamp(13, Timestamp.from(Instant.now())) + + val rowsAffected = stmt.executeUpdate() + if (rowsAffected > 0) { + logger.info("Successfully saved order ${orderResult.orderId} to database") + true + } else { + logger.warn("Failed to save order ${orderResult.orderId} - no rows affected") + false + } + } + } + } catch (e: Exception) { + logger.error("Error saving order ${orderResult.orderId} to database", e) + false + } + } +} diff --git a/src/fraud-detection/src/main/kotlin/frauddetection/OrderMutator.kt b/src/fraud-detection/src/main/kotlin/frauddetection/OrderMutator.kt new file mode 100644 index 0000000000..17bd469e9e --- /dev/null +++ b/src/fraud-detection/src/main/kotlin/frauddetection/OrderMutator.kt @@ -0,0 +1,138 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package frauddetection + +import org.apache.logging.log4j.LogManager +import org.apache.logging.log4j.Logger +import oteldemo.Demo.OrderResult +import oteldemo.Demo.Money +import oteldemo.Demo.Address +import kotlin.random.Random + +/** + * Mutates orders to trigger fraud detection alerts for demo purposes. + * This helps demonstrate fraud detection capabilities by randomly modifying + * order attributes to match fraud patterns. + */ +class OrderMutator { + private val logger: Logger = LogManager.getLogger(OrderMutator::class.java) + + companion object { + // High-risk shipping addresses for demos + private val PO_BOX_ADDRESSES = listOf( + "P.O. Box 12345", + "PO Box 98765", + "P.O. Box 54321", + "PO BOX 11111" + ) + + private val SUSPICIOUS_CITIES = listOf( + "Unknown City", + "Test City", + "Fraud Town" + ) + } + + /** + * Randomly mutates an order to trigger fraud alerts. + * Returns the mutated order if mutation occurred, otherwise returns original. + * @param order The original order + * @param mutationPercentage The percentage (0-100) chance to mutate + */ + fun mutateOrder(order: OrderResult, mutationPercentage: Int): OrderResult { + // Random chance to mutate based on percentage + if (mutationPercentage <= 0 || Random.nextInt(100) >= mutationPercentage) { + return order + } + + val mutationType = Random.nextInt(4) + val builder = order.toBuilder() + + when (mutationType) { + 0 -> mutateToHighValue(builder) + 1 -> mutateToVeryHighValue(builder) + 2 -> mutateToLargeQuantity(builder) + 3 -> mutateToPOBoxAddress(builder) + } + + val mutatedOrder = builder.build() + logger.info("Order ${order.orderId} mutated (type=$mutationType) to trigger fraud detection") + + return mutatedOrder + } + + /** + * Mutate shipping cost to high value ($50-$99) + */ + private fun mutateToHighValue(builder: OrderResult.Builder) { + val highValue = Random.nextLong(50, 100) + val money = Money.newBuilder() + .setCurrencyCode("USD") + .setUnits(highValue) + .setNanos(Random.nextInt(0, 1000000)) + .build() + + builder.shippingCost = money + logger.debug("Mutated to high value: \$${highValue}") + } + + /** + * Mutate shipping cost to very high value ($100-$500) + */ + private fun mutateToVeryHighValue(builder: OrderResult.Builder) { + val veryHighValue = Random.nextLong(100, 501) + val money = Money.newBuilder() + .setCurrencyCode("USD") + .setUnits(veryHighValue) + .setNanos(Random.nextInt(0, 1000000)) + .build() + + builder.shippingCost = money + logger.debug("Mutated to very high value: \$${veryHighValue}") + } + + /** + * Mutate items count to large quantity (11-30 items) + * We duplicate existing items to increase the count + */ + private fun mutateToLargeQuantity(builder: OrderResult.Builder) { + val currentItems = builder.itemsList.toMutableList() + if (currentItems.isEmpty()) { + // Can't mutate if there are no items + return + } + + val targetQuantity = Random.nextInt(11, 31) + + // Duplicate items until we reach target quantity + while (builder.itemsCount < targetQuantity) { + val itemToDuplicate = currentItems.random() + builder.addItems(itemToDuplicate) + } + + logger.debug("Mutated to large quantity: ${builder.itemsCount} items") + } + + /** + * Mutate shipping address to PO Box + */ + private fun mutateToPOBoxAddress(builder: OrderResult.Builder) { + if (builder.hasShippingAddress()) { + val originalAddress = builder.shippingAddress + val poBoxAddress = Address.newBuilder() + .setStreetAddress(PO_BOX_ADDRESSES.random()) + .setCity(SUSPICIOUS_CITIES.random()) + .setState(originalAddress.state) + .setCountry(originalAddress.country) + .setZipCode(originalAddress.zipCode) + .build() + + builder.shippingAddress = poBoxAddress + logger.debug("Mutated to PO Box address: ${poBoxAddress.streetAddress}") + } + } + +} diff --git a/src/fraud-detection/src/main/kotlin/frauddetection/main.kt b/src/fraud-detection/src/main/kotlin/frauddetection/main.kt index bcb480ae77..1d5e4b4924 100644 --- a/src/fraud-detection/src/main/kotlin/frauddetection/main.kt +++ b/src/fraud-detection/src/main/kotlin/frauddetection/main.kt @@ -35,6 +35,15 @@ fun main() { val flagdProvider = FlagdProvider(options) OpenFeatureAPI.getInstance().setProvider(flagdProvider) + // Initialize database connection + try { + DatabaseConfig.initialize() + logger.info("Database initialized successfully") + } catch (e: Exception) { + logger.error("Failed to initialize database", e) + exitProcess(1) + } + val props = Properties() props[KEY_DESERIALIZER_CLASS_CONFIG] = StringDeserializer::class.java.name props[VALUE_DESERIALIZER_CLASS_CONFIG] = ByteArrayDeserializer::class.java.name @@ -49,7 +58,30 @@ fun main() { subscribe(listOf(topic)) } + // Initialize repository and analytics + val orderLogRepository = OrderLogRepository() + val fraudAnalytics = FraudAnalytics() + val databaseCleanup = DatabaseCleanup() + val orderMutator = OrderMutator() + val badQueryPatterns = BadQueryPatterns() + + // Read configuration from environment variables + val cleanupRetentionDays = System.getenv("CLEANUP_RETENTION_DAYS")?.toIntOrNull() ?: 7 + val cleanupIntervalHours = System.getenv("CLEANUP_INTERVAL_HOURS")?.toLongOrNull() ?: 24 + + // Start cleanup scheduler + databaseCleanup.startCleanupScheduler(cleanupRetentionDays, cleanupIntervalHours) + logger.info("Cleanup scheduler started: retentionDays=$cleanupRetentionDays, intervalHours=$cleanupIntervalHours") + var totalCount = 0L + var fraudAlertCount = 0L + + // Add shutdown hook to close database connection + Runtime.getRuntime().addShutdownHook(Thread { + logger.info("Shutting down...") + databaseCleanup.stop() + DatabaseConfig.close() + }) consumer.use { while (true) { @@ -61,8 +93,62 @@ fun main() { logger.info("FeatureFlag 'kafkaQueueProblems' is enabled, sleeping 1 second") Thread.sleep(1000) } - val orders = OrderResult.parseFrom(record.value()) + var orders = OrderResult.parseFrom(record.value()) + + // Mutate orders to trigger fraud alerts if feature flag enabled + if (getFeatureFlagValue("fraudDetectionEnabled") > 0) { + val mutationPercentage = getFeatureFlagValue("mutateFraudOrders") + if (mutationPercentage > 0) { + orders = orderMutator.mutateOrder(orders, mutationPercentage) + } + } + logger.info("Consumed record with orderId: ${orders.orderId}, and updated total count to: $newCount") + + // Save to database + try { + val saved = orderLogRepository.saveOrder(orders) + if (saved) { + logger.info("Order ${orders.orderId} logged to database") + } else { + logger.warn("Failed to log order ${orders.orderId} to database") + } + } catch (e: Exception) { + logger.error("Exception while logging order ${orders.orderId} to database", e) + } + + // Fraud detection (controlled by feature flag) + if (getFeatureFlagValue("fraudDetectionEnabled") > 0) { + try { + val alert = fraudAnalytics.analyzeOrder(orders) + if (alert != null) { + fraudAlertCount++ + logger.warn("๐Ÿšจ FRAUD ALERT #$fraudAlertCount: orderId=${alert.orderId}, severity=${alert.severity}, score=${alert.riskScore}, reason=${alert.reason}") + + // Log stats periodically + if (fraudAlertCount % 10L == 0L) { + val stats = fraudAnalytics.getAlertStats(24) + logger.info("Fraud stats (24h): $stats") + } + } + } catch (e: Exception) { + logger.error("Error during fraud analysis for order ${orders.orderId}", e) + } + } + + // Execute bad query patterns for monitoring demo (controlled by feature flag) + val badQueryPercentage = getFeatureFlagValue("executeBadQueries") + if (badQueryPercentage > 0) { + try { + val executed = badQueryPatterns.maybeExecuteBadQuery(badQueryPercentage) + if (executed) { + logger.info("Executed bad query pattern for monitoring demo") + } + } catch (e: Exception) { + logger.error("Error executing bad query pattern", e) + } + } + newCount } } diff --git a/src/frontend-proxy/build-proxy.sh b/src/frontend-proxy/build-proxy.sh index 6fa5fe15cb..ff86237517 100755 --- a/src/frontend-proxy/build-proxy.sh +++ b/src/frontend-proxy/build-proxy.sh @@ -1,12 +1,12 @@ #!/bin/bash -e # Usage: -# ./build.sh [-cc] +# ./build-proxy.sh [-cc] if [ -z "$1" ]; then echo "โŒ Error: No version provided." - echo "Usage: $0 [-cc] " - exit + echo "Usage: $0 [-cc]" + exit 1 fi VERSION="$1" @@ -15,28 +15,47 @@ CACHE_OPTION="" # Check if the second arg is -cc if [[ "$2" == "-cc" ]]; then CACHE_OPTION="--no-cache" - # Shift all remaining args so $2 becomes RUM_TOKEN - shift fi -URL_PREFIX="$6" - -# Move two directories up from src/recomndation +# Move two directories up from src/frontend-proxy cd "$(dirname "$0")/../.." -# Build command +echo "Building frontend-proxy service version: $VERSION" +echo "Build options: ${CACHE_OPTION:-default caching enabled}" + +# Build without push first DOCKER_CMD=( docker buildx build --platform=linux/amd64,linux/arm64 $CACHE_OPTION --build-arg VERSION="$VERSION" -t ghcr.io/splunk/opentelemetry-demo/otel-frontend-proxy:"$VERSION" - --push + --load -f src/frontend-proxy/Dockerfile + . ) -# Add build context -DOCKER_CMD+=( . ) +echo "Executing build command..." +if ! "${DOCKER_CMD[@]}"; then + echo "โŒ Error: Docker build failed" + exit 1 +fi + +echo "โœ… Build successful" + +# Verify image exists locally +if ! docker image inspect ghcr.io/splunk/opentelemetry-demo/otel-frontend-proxy:"$VERSION" > /dev/null 2>&1; then + echo "โŒ Error: Built image not found locally" + exit 1 +fi + +echo "โœ… Image verified locally" + +# Push the image +echo "Pushing image to registry..." +if ! docker push ghcr.io/splunk/opentelemetry-demo/otel-frontend-proxy:"$VERSION"; then + echo "โŒ Error: Push failed. Check your registry authentication." + exit 1 +fi -# Execute the build -"${DOCKER_CMD[@]}" \ No newline at end of file +echo "โœ… Successfully pushed ghcr.io/splunk/opentelemetry-demo/otel-frontend-proxy:$VERSION" \ No newline at end of file diff --git a/src/frontend-proxy/envoy.tmpl.yaml b/src/frontend-proxy/envoy.tmpl.yaml index 44696a1038..7c20370870 100644 --- a/src/frontend-proxy/envoy.tmpl.yaml +++ b/src/frontend-proxy/envoy.tmpl.yaml @@ -56,7 +56,7 @@ static_resources: - match: { prefix: "/flagservice/" } route: { cluster: flagservice, prefix_rewrite: "/", timeout: 0s } - match: { prefix: "/feature" } - route: { cluster: flagd-ui } + route: { cluster: flagd-ui, prefix_rewrite: "/" } typed_per_filter_config: envoy.filters.http.lua: "@type": type.googleapis.com/envoy.extensions.filters.http.lua.v3.LuaPerRoute From b397a377c3441340c26c5421e331028d935a614b Mon Sep 17 00:00:00 2001 From: Pieter Hagen Date: Mon, 20 Oct 2025 14:13:17 +0200 Subject: [PATCH 2/7] fixed ingress for aws domain and secret --- kubernetes/opentelemetry-demo.yaml | 61 +++++++++++++++++++++--------- 1 file changed, 44 insertions(+), 17 deletions(-) diff --git a/kubernetes/opentelemetry-demo.yaml b/kubernetes/opentelemetry-demo.yaml index f21ed17e60..fec8b49e54 100644 --- a/kubernetes/opentelemetry-demo.yaml +++ b/kubernetes/opentelemetry-demo.yaml @@ -2367,23 +2367,50 @@ spec: volumeMounts: volumes: --- +# ingress format for local demo +# apiVersion: networking.k8s.io/v1 +# kind: Ingress +# metadata: +# name: frontend-proxy-ingress +# # annotations: +# # kubernetes.io/ingress.class: traefik +# spec: +# rules: +# - http: +# paths: +# - path: / +# pathType: Prefix +# backend: +# service: +# name: frontend-proxy +# port: +# number: 8080 +--- +# Ingress format for official demo with AWS domain apiVersion: networking.k8s.io/v1 kind: Ingress metadata: - name: frontend-proxy-ingress - # annotations: - # kubernetes.io/ingress.class: traefik + name: frontend + annotations: + cert-manager.io/cluster-issuer: "letsencrypt-prod" + traefik.ingress.kubernetes.io/router.middlewares: default-custom-headers@kubernetescrd spec: + ingressClassName: traefik + tls: + - hosts: + - example.example.com + secretName: splunk-demo-tls rules: - - http: - paths: - - path: / - pathType: Prefix - backend: - service: - name: frontend-proxy - port: - number: 8080 + - host: example.example.com + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: frontend + port: + number: 80 --- apiVersion: apps/v1 kind: Deployment @@ -2412,11 +2439,11 @@ spec: imagePullPolicy: Always env: - name: RUM_FRONTEND_IP - # valueFrom: - # secretKeyRef: - # name: workshop-secret - # key: url - value: 192.168.3.214 + valueFrom: + secretKeyRef: + name: workshop-secret + key: url + #value: 192.168.3.214 volumeMounts: - name: puppeteer subPath: local-file From 03986eca9b209e76507f203ba2b6b0298ca403ec Mon Sep 17 00:00:00 2001 From: Pieter Hagen Date: Mon, 20 Oct 2025 16:37:53 +0200 Subject: [PATCH 3/7] example otelcol-base.yaml for dbmon --- kubernetes/otel-col-base.yaml | 121 ++++++++++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 kubernetes/otel-col-base.yaml diff --git a/kubernetes/otel-col-base.yaml b/kubernetes/otel-col-base.yaml new file mode 100644 index 0000000000..0fd2c79200 --- /dev/null +++ b/kubernetes/otel-col-base.yaml @@ -0,0 +1,121 @@ +clusterReceiver: + eventsEnabled: true + k8sObjects: + - name: events + mode: watch + namespaces: [default, splunk] + - name: pods + mode: watch + namespaces: [default, splunk] +agent: + extraEnvs: + - name: WORKSHOP_ENVIRONMENT + valueFrom: + secretKeyRef: + name: workshop-secret + key: instance + config: + receivers: + smartagent/http: + type: http + host: dev-astronomy.splunko11y.com + useHTTPS: true + receiver_creator: + receivers: + mysql/online-boutique: + rule: type == "port" && pod.name matches "mysql" && port == 3306 + config: + tls: + insecure: true + username: root + password: root + database: LxvGChW075 + redis: + rule: type == "port" && pod.name matches "redis-cart" && port == 6379 + config: + endpoint: "redis-cart:6379" + collection_interval: 10s + sqlserver: + collection_interval: 10s + username: sa + password: "ChangeMe_SuperStrong123!" + server: sql-express.sql.svc.cluster.local + port: 1433 + resource_attributes: + sqlserver.computer.name: + enabled: true + sqlserver.instance.name: + enabled: true + # ADD to ENABLE Database Monitoring + events: + db.server.query_sample: + enabled: true + db.server.top_query: + enabled: true + + processors: + filter/drop_flagd: + traces: + span: + - attributes["rpc.method"] == "EventStream" + - attributes["rpc.method"] == "ResolveAll" + - attributes["rpc.method"] == "ResolveBoolean" + - attributes["rpc.method"] == "ResolveFloat" + - attributes["rpc.method"] == "ResolveInt" + - attributes["http.url"] == "http://flagd:8016/ofrep/v1/evaluate/flags/loadGeneratorFloodHomepage" + - attributes["url.full"] == "http://flagd:8013/flagd.evaluation.v1.Service/ResolveBoolean" + - attributes["otel.scope.name"] == "flagd.evaluation.v1" + - attributes["url.full"] == "http://flagd:8013/flagd.evaluation.v1.Service/EventStream" + + # Exporters define where the telemetry data is sent to + exporters: + # Exports dbmon events as logs + otlphttp/dbmon: + headers: + X-SF-Token: 3QThm3q899dAU8udj-i4tA + X-splunk-instrumentation-library: dbmon + logs_endpoint: https://ingest.us1.signalfx.com/v3/event + sending_queue: + batch: + flush_timeout: 15s + max_size: 10485760 # 10 MiB + sizer: bytes + + service: + pipelines: + metrics: + exporters: [signalfx] + processors: [memory_limiter, k8sattributes, batch, resourcedetection, resource] + receivers: [hostmetrics, kubeletstats, otlp, sqlserver, receiver_creator, signalfx] + traces: + exporters: [signalfx, otlphttp] + processors: [memory_limiter, filter/drop_flagd, k8sattributes, batch, resourcedetection, resource, resource/add_environment] + receivers: [otlp, jaeger, zipkin] + logs/dbmon: + receivers: + - sqlserver + processors: + - memory_limiter + #- resource/tns + - batch + - resourcedetection + exporters: + - otlphttp/dbmon +logsCollection: + extraFileLogs: + filelog/syslog: + include: [/var/log/syslog] + include_file_path: true + include_file_name: false + resource: + com.splunk.source: /var/log/syslog + host.name: 'EXPR(env("K8S_NODE_NAME"))' + com.splunk.sourcetype: syslog + filelog/auth_log: + include: [/var/log/auth.log] + include_file_path: true + include_file_name: false + resource: + com.splunk.source: /var/log/auth.log + host.name: 'EXPR(env("K8S_NODE_NAME"))' + com.splunk.sourcetype: auth_log From 8d7805298ed2a60ce8f17da5c7bfff9b102ef642 Mon Sep 17 00:00:00 2001 From: Pieter Hagen Date: Mon, 20 Oct 2025 16:56:31 +0200 Subject: [PATCH 4/7] fixed http/https --- kubernetes/opentelemetry-demo.yaml | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/kubernetes/opentelemetry-demo.yaml b/kubernetes/opentelemetry-demo.yaml index fec8b49e54..c9e08e79fb 100644 --- a/kubernetes/opentelemetry-demo.yaml +++ b/kubernetes/opentelemetry-demo.yaml @@ -2491,10 +2491,18 @@ data: function buildBaseUrl() { const raw = (process.env.RUM_FRONTEND_IP || '').trim(); - const base = raw ? (/^https?:\/\//i.test(raw) ? raw : `http://${raw}`) : 'http://localhost'; - return base.replace(/\/+$/, '') + '/'; - } + // If it's empty, default to localhost + if (!raw) return 'https://localhost/'; + + // If it already starts with http:// or https://, keep it exactly as-is + if (/^https?:\/\//i.test(raw)) { + return raw.replace(/\/+$/, '') + '/'; + } + + // Otherwise, prepend https:// + return `https://${raw.replace(/\/+$/, '')}/`; + } function delay(ms) { return new Promise(res => setTimeout(res, ms)); } /** Wait until innerText of the page contains a substring */ From 82113cb048b037565abbb535d4f394a81f2a2e16 Mon Sep 17 00:00:00 2001 From: Pieter Hagen Date: Wed, 22 Oct 2025 11:57:10 +0200 Subject: [PATCH 5/7] fixed db name in connect string --- kubernetes/opentelemetry-demo.yaml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/kubernetes/opentelemetry-demo.yaml b/kubernetes/opentelemetry-demo.yaml index c9e08e79fb..bb5a3e5287 100644 --- a/kubernetes/opentelemetry-demo.yaml +++ b/kubernetes/opentelemetry-demo.yaml @@ -346,12 +346,12 @@ type: Opaque stringData: SA_PASSWORD: "ChangeMe_SuperStrong123!" # Optional: full connection string if you want to inject as a single env var. - DB_CONNECTION_STRING: "Server=tcp:sql-express.sql.svc.cluster.local,1433;Database=MyDb;User Id=sa;Password=ChangeMe_SuperStrong123!;Encrypt=True;TrustServerCertificate=True" + DB_CONNECTION_STRING: "Server=tcp:sql-server-fraud.sql.svc.cluster.local,1433;Database=MyDb;User Id=sa;Password=ChangeMe_SuperStrong123!;Encrypt=True;TrustServerCertificate=True" --- apiVersion: v1 kind: Service metadata: - name: sql-express + name: sql-server-fraud namespace: sql spec: type: ClusterIP @@ -361,26 +361,26 @@ spec: protocol: TCP name: tds selector: - app: sql-express + app: sql-server-fraud --- apiVersion: apps/v1 kind: StatefulSet metadata: - name: sql-express + name: sql-server-fraud namespace: sql spec: - serviceName: sql-express + serviceName: sql-server-fraud replicas: 1 persistentVolumeClaimRetentionPolicy: whenDeleted: Delete whenScaled: Delete selector: matchLabels: - app: sql-express + app: sql-server-fraud template: metadata: labels: - app: sql-express + app: sql-server-fraud spec: securityContext: fsGroup: 10001 # mssql user can write PV @@ -1434,7 +1434,7 @@ spec: - name: OTEL_RESOURCE_ATTRIBUTES value: service.name=$(OTEL_SERVICE_NAME),service.namespace=opentelemetry-demo,service.version=2.1.3,service.kafka=spanlink - name: SQL_SERVER_HOST - value: sql-express.sql.svc.cluster.local + value: sql-server-fraud.sql.svc.cluster.local - name: SQL_SERVER_PORT value: "1433" - name: SQL_SERVER_DATABASE @@ -1461,7 +1461,7 @@ spec: - command: - sh - -c - - until nc -z -v -w30 sql-express.sql.svc.cluster.local 1433; do echo waiting for sql-express; sleep 2; done; + - until nc -z -v -w30 sql-server-fraud.sql.svc.cluster.local 1433; do echo waiting for sql-server-fraud; sleep 2; done; image: busybox:latest name: wait-for-sqlserver volumes: From b7220ceb53d46961b30918587856ce95b36554a6 Mon Sep 17 00:00:00 2001 From: Pieter Hagen Date: Wed, 22 Oct 2025 16:40:19 +0200 Subject: [PATCH 6/7] fix for sql startup --- CHANGELOG.md | 4 +++ kubernetes/opentelemetry-demo.yaml | 40 ++++++++++++++++++++++++------ src/fraud-detection/README.md | 29 ++++++++++++++++++++++ 3 files changed, 66 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f1046c39fe..12d12b1ec9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ the release. ## Unreleased +* [fraud-detection] Add automatic SQL Server database initialization for FraudDetection database + - Added ConfigMap with database initialization script + - Added postStart lifecycle hook to execute init script on SQL Server startup + - Fixed connection string to use correct database name (FraudDetection) * [chore] Use pre-built nginx otel image ([#2614](https://github.com/open-telemetry/opentelemetry-demo/pull/2614)) * [grafana] Update grafana version to 12.2.0 diff --git a/kubernetes/opentelemetry-demo.yaml b/kubernetes/opentelemetry-demo.yaml index bb5a3e5287..e2f4532277 100644 --- a/kubernetes/opentelemetry-demo.yaml +++ b/kubernetes/opentelemetry-demo.yaml @@ -346,7 +346,20 @@ type: Opaque stringData: SA_PASSWORD: "ChangeMe_SuperStrong123!" # Optional: full connection string if you want to inject as a single env var. - DB_CONNECTION_STRING: "Server=tcp:sql-server-fraud.sql.svc.cluster.local,1433;Database=MyDb;User Id=sa;Password=ChangeMe_SuperStrong123!;Encrypt=True;TrustServerCertificate=True" + DB_CONNECTION_STRING: "Server=tcp:sql-server-fraud.sql.svc.cluster.local,1433;Database=FraudDetection;User Id=sa;Password=ChangeMe_SuperStrong123!;Encrypt=True;TrustServerCertificate=True" +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: mssql-init-script + namespace: sql +data: + init-db.sql: | + IF NOT EXISTS (SELECT name FROM sys.databases WHERE name = 'FraudDetection') + BEGIN + CREATE DATABASE FraudDetection; + END + GO --- apiVersion: v1 kind: Service @@ -382,10 +395,6 @@ spec: labels: app: sql-server-fraud spec: - securityContext: - fsGroup: 10001 # mssql user can write PV - runAsUser: 10001 # run container as mssql user - runAsGroup: 0 containers: - name: mssql image: mcr.microsoft.com/mssql/server:2022-latest @@ -406,6 +415,17 @@ spec: volumeMounts: - name: data mountPath: /var/opt/mssql + - name: init-script + mountPath: /docker-entrypoint-initdb.d + lifecycle: + postStart: + exec: + command: + - /bin/bash + - -c + - | + sleep 30 + /opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P "${SA_PASSWORD}" -C -i /docker-entrypoint-initdb.d/init-db.sql resources: requests: cpu: "250m" @@ -423,6 +443,10 @@ spec: port: 1433 initialDelaySeconds: 30 periodSeconds: 20 + volumes: + - name: init-script + configMap: + name: mssql-init-script volumeClaimTemplates: - metadata: name: data @@ -940,8 +964,10 @@ spec: value: "true" - name: SPLUNK_METRICS_ENABLED value: "true" - - name: SPLUNK_METRICS_ENDPOINT - value: "http://$(NODE_IP):9943" + - name: SPLUNK_SNAPSHOT_PROFILE_ENABLED + value: "true" + - name: SPLUNK_SNAPSHOT_SELECTION_PROBABILITY + value: "0.1" - name: OTEL_TRACE_EXPORTER value: otlp - name: OTEL_TRACES_SAMPLER diff --git a/src/fraud-detection/README.md b/src/fraud-detection/README.md index c844776c2e..1787a89909 100644 --- a/src/fraud-detection/README.md +++ b/src/fraud-detection/README.md @@ -3,6 +3,35 @@ This service receives new orders by a Kafka topic and returns cases which are suspected of fraud. +## Dependencies + +This service requires: +- **Kafka**: For receiving order events +- **SQL Server**: For storing fraud detection data in the `FraudDetection` database + +## Kubernetes Deployment + +### SQL Server Database Initialization + +The fraud detection service requires a SQL Server database named `FraudDetection`. The Kubernetes deployment includes automatic database initialization: + +1. **ConfigMap** (`mssql-init-script`): Contains SQL script to create the database +2. **StatefulSet** (`sql-server-fraud`): SQL Server with lifecycle hook that executes the init script +3. **InitContainers**: The fraud detection deployment waits for both Kafka and SQL Server to be ready before starting + +The database is automatically created when the SQL Server pod starts using a `postStart` lifecycle hook that: +- Waits 30 seconds for SQL Server to be fully initialized +- Executes the database creation script via `sqlcmd` + +### Connection Details + +The service connects to SQL Server using these environment variables: +- `SQL_SERVER_HOST`: `sql-server-fraud.sql.svc.cluster.local` +- `SQL_SERVER_PORT`: `1433` +- `SQL_SERVER_DATABASE`: `FraudDetection` +- `SQL_SERVER_USER`: `sa` +- `SQL_SERVER_PASSWORD`: From `mssql-secrets` secret + ## Local Build To build the protos and the service binary, run from the repo root: From ab2d207fd3f6090118405d539c97b8fdaedc95cc Mon Sep 17 00:00:00 2001 From: Pieter Hagen Date: Wed, 22 Oct 2025 17:09:54 +0200 Subject: [PATCH 7/7] Add extar fraud detection queries --- kubernetes/opentelemetry-demo.yaml | 4 +- .../frauddetection/FraudDetectionQueries.kt | 362 ++++++++++++++++++ .../frauddetection/OrderLogRepository.kt | 20 + src/payment/Dockerfile | 8 +- 4 files changed, 391 insertions(+), 3 deletions(-) create mode 100644 src/fraud-detection/src/main/kotlin/frauddetection/FraudDetectionQueries.kt diff --git a/kubernetes/opentelemetry-demo.yaml b/kubernetes/opentelemetry-demo.yaml index e2f4532277..057c2d3908 100644 --- a/kubernetes/opentelemetry-demo.yaml +++ b/kubernetes/opentelemetry-demo.yaml @@ -1429,7 +1429,7 @@ spec: containers: - name: fraud-detection #image: 'ghcr.io/open-telemetry/demo:2.1.3-fraud-detection' - image: ghcr.io/splunk/opentelemetry-demo/otel-fraud-detection:2.1.3-sql.5 + image: ghcr.io/splunk/opentelemetry-demo/otel-fraud-detection:2.1.3.1 imagePullPolicy: IfNotPresent env: - name: OTEL_SERVICE_NAME @@ -1469,6 +1469,8 @@ spec: value: sa - name: SQL_SERVER_PASSWORD value: "ChangeMe_SuperStrong123!" + - name: FRAUD_DETECTION_RATE + value: "80" - name: CLEANUP_RETENTION_DAYS value: "7" - name: CLEANUP_INTERVAL_HOURS diff --git a/src/fraud-detection/src/main/kotlin/frauddetection/FraudDetectionQueries.kt b/src/fraud-detection/src/main/kotlin/frauddetection/FraudDetectionQueries.kt new file mode 100644 index 0000000000..fede89ebab --- /dev/null +++ b/src/fraud-detection/src/main/kotlin/frauddetection/FraudDetectionQueries.kt @@ -0,0 +1,362 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package frauddetection + +import org.apache.logging.log4j.LogManager +import org.apache.logging.log4j.Logger +import kotlin.random.Random + +/** + * Executes SQL-based fraud detection queries with varying latency. + * These queries simulate automatic fraud detection analysis that runs + * after an order is logged, demonstrating realistic database monitoring patterns. + */ +class FraudDetectionQueries { + private val logger: Logger = LogManager.getLogger(FraudDetectionQueries::class.java) + + /** + * Run fraud detection analysis on a newly inserted order. + * Randomly executes 1-3 fraud detection queries with latency variance. + * @param orderId The order ID to analyze + * @return true if any fraud indicators were found + */ + fun analyzeOrder(orderId: String): Boolean { + val numChecks = Random.nextInt(1, 4) // Run 1-3 checks randomly + var fraudDetected = false + + try { + val checksToRun = (0..5).shuffled().take(numChecks) + + checksToRun.forEach { checkType -> + val result = when (checkType) { + 0 -> checkHighValueOrder(orderId) + 1 -> checkDuplicateShippingAddress(orderId) + 2 -> checkRapidOrderVelocity(orderId) + 3 -> checkSuspiciousCountryPattern(orderId) + 4 -> checkAnomalousItemCount(orderId) + 5 -> checkHistoricalFraudPatterns(orderId) + else -> false + } + if (result) fraudDetected = true + } + } catch (e: Exception) { + logger.error("Error during fraud detection for order $orderId", e) + } + + return fraudDetected + } + + /** + * Check 1: High-value order detection with historical comparison + * Latency: 50-200ms (medium complexity query with aggregation) + */ + private fun checkHighValueOrder(orderId: String): Boolean { + DatabaseConfig.getConnection().use { conn -> + // Simulate variable latency + Thread.sleep(Random.nextLong(50, 200)) + + val sql = """ + WITH OrderValue AS ( + SELECT + order_id, + shipping_cost_units, + items_count, + (shipping_cost_units + (items_count * 50)) as estimated_value + FROM OrderLogs + WHERE order_id = ? + ), + AvgValue AS ( + SELECT AVG(shipping_cost_units + (items_count * 50)) as avg_order_value + FROM OrderLogs + WHERE consumed_at >= DATEADD(HOUR, -24, GETDATE()) + ) + SELECT + CASE + WHEN ov.estimated_value > (av.avg_order_value * 3) THEN 1 + ELSE 0 + END as is_high_value + FROM OrderValue ov, AvgValue av + """.trimIndent() + + conn.prepareStatement(sql).use { stmt -> + stmt.setString(1, orderId) + stmt.executeQuery().use { rs -> + if (rs.next() && rs.getInt("is_high_value") == 1) { + logger.warn("๐Ÿ” FRAUD CHECK: High-value order detected for $orderId (>3x avg)") + return true + } + } + } + } + return false + } + + /** + * Check 2: Duplicate shipping address with recent orders + * Latency: 100-300ms (complex string matching and temporal query) + */ + private fun checkDuplicateShippingAddress(orderId: String): Boolean { + DatabaseConfig.getConnection().use { conn -> + // Simulate variable latency + Thread.sleep(Random.nextLong(100, 300)) + + val sql = """ + WITH CurrentOrder AS ( + SELECT shipping_street, shipping_city, shipping_zip + FROM OrderLogs + WHERE order_id = ? + ) + SELECT COUNT(DISTINCT ol.order_id) as duplicate_count + FROM OrderLogs ol, CurrentOrder co + WHERE ol.shipping_street = co.shipping_street + AND ol.shipping_city = co.shipping_city + AND ol.shipping_zip = co.shipping_zip + AND ol.order_id != ? + AND ol.consumed_at >= DATEADD(HOUR, -1, GETDATE()) + """.trimIndent() + + conn.prepareStatement(sql).use { stmt -> + stmt.setString(1, orderId) + stmt.setString(2, orderId) + stmt.executeQuery().use { rs -> + if (rs.next()) { + val dupes = rs.getInt("duplicate_count") + if (dupes >= 3) { + logger.warn("๐Ÿ” FRAUD CHECK: Duplicate shipping address for $orderId ($dupes recent orders)") + insertFraudAlert(orderId, "DUPLICATE_ADDRESS", "MEDIUM", dupes * 0.15) + return true + } + } + } + } + } + return false + } + + /** + * Check 3: Rapid order velocity from same location + * Latency: 80-250ms (temporal aggregation with grouping) + */ + private fun checkRapidOrderVelocity(orderId: String): Boolean { + DatabaseConfig.getConnection().use { conn -> + // Simulate variable latency + Thread.sleep(Random.nextLong(80, 250)) + + val sql = """ + WITH CurrentOrder AS ( + SELECT shipping_city, shipping_state, shipping_country, consumed_at + FROM OrderLogs + WHERE order_id = ? + ) + SELECT + COUNT(*) as order_count, + COUNT(DISTINCT order_id) as unique_orders, + DATEDIFF(MINUTE, MIN(ol.consumed_at), MAX(ol.consumed_at)) as time_span_minutes + FROM OrderLogs ol + INNER JOIN CurrentOrder co ON + ol.shipping_city = co.shipping_city AND + ol.shipping_state = co.shipping_state AND + ol.shipping_country = co.shipping_country + WHERE ol.consumed_at >= DATEADD(MINUTE, -15, GETDATE()) + HAVING COUNT(*) >= 5 + """.trimIndent() + + conn.prepareStatement(sql).use { stmt -> + stmt.setString(1, orderId) + stmt.executeQuery().use { rs -> + if (rs.next()) { + val orderCount = rs.getInt("order_count") + val timeSpan = rs.getInt("time_span_minutes") + if (orderCount >= 5) { + val riskScore = (orderCount / 5.0) * 0.25 + logger.warn("๐Ÿ” FRAUD CHECK: Rapid order velocity for $orderId ($orderCount orders in $timeSpan mins)") + insertFraudAlert(orderId, "RAPID_VELOCITY", "HIGH", riskScore) + return true + } + } + } + } + } + return false + } + + /** + * Check 4: Suspicious country/region pattern analysis + * Latency: 120-350ms (complex geo-pattern with historical joins) + */ + private fun checkSuspiciousCountryPattern(orderId: String): Boolean { + DatabaseConfig.getConnection().use { conn -> + // Simulate variable latency + Thread.sleep(Random.nextLong(120, 350)) + + val sql = """ + WITH OrderCountry AS ( + SELECT shipping_country, shipping_state + FROM OrderLogs + WHERE order_id = ? + ), + CountryStats AS ( + SELECT + shipping_country, + COUNT(*) as total_orders, + AVG(CAST(shipping_cost_units AS FLOAT)) as avg_shipping_cost, + COUNT(DISTINCT shipping_city) as unique_cities + FROM OrderLogs + WHERE consumed_at >= DATEADD(DAY, -7, GETDATE()) + GROUP BY shipping_country + ) + SELECT + cs.total_orders, + cs.avg_shipping_cost, + cs.unique_cities, + CASE + WHEN cs.total_orders < 5 THEN 1 + WHEN cs.avg_shipping_cost > 100 THEN 1 + ELSE 0 + END as is_suspicious + FROM OrderCountry oc + LEFT JOIN CountryStats cs ON oc.shipping_country = cs.shipping_country + """.trimIndent() + + conn.prepareStatement(sql).use { stmt -> + stmt.setString(1, orderId) + stmt.executeQuery().use { rs -> + if (rs.next() && rs.getInt("is_suspicious") == 1) { + val totalOrders = rs.getInt("total_orders") + logger.warn("๐Ÿ” FRAUD CHECK: Suspicious country pattern for $orderId (rare country: $totalOrders orders)") + insertFraudAlert(orderId, "SUSPICIOUS_LOCATION", "MEDIUM", 0.35) + return true + } + } + } + } + return false + } + + /** + * Check 5: Anomalous item count with statistical analysis + * Latency: 60-180ms (statistical aggregation query) + */ + private fun checkAnomalousItemCount(orderId: String): Boolean { + DatabaseConfig.getConnection().use { conn -> + // Simulate variable latency + Thread.sleep(Random.nextLong(60, 180)) + + val sql = """ + WITH CurrentOrder AS ( + SELECT items_count + FROM OrderLogs + WHERE order_id = ? + ), + ItemStats AS ( + SELECT + AVG(CAST(items_count AS FLOAT)) as avg_items, + STDEV(CAST(items_count AS FLOAT)) as stddev_items + FROM OrderLogs + WHERE consumed_at >= DATEADD(DAY, -1, GETDATE()) + ) + SELECT + co.items_count, + is_stat.avg_items, + is_stat.stddev_items, + CASE + WHEN co.items_count > (is_stat.avg_items + (2 * is_stat.stddev_items)) THEN 1 + ELSE 0 + END as is_anomalous + FROM CurrentOrder co, ItemStats is_stat + """.trimIndent() + + conn.prepareStatement(sql).use { stmt -> + stmt.setString(1, orderId) + stmt.executeQuery().use { rs -> + if (rs.next() && rs.getInt("is_anomalous") == 1) { + val itemCount = rs.getInt("items_count") + logger.warn("๐Ÿ” FRAUD CHECK: Anomalous item count for $orderId (count: $itemCount, >2ฯƒ from mean)") + insertFraudAlert(orderId, "ANOMALOUS_ITEMS", "LOW", 0.20) + return true + } + } + } + } + return false + } + + /** + * Check 6: Historical fraud pattern matching with correlated subqueries + * Latency: 150-400ms (expensive multi-table joins and correlation) + */ + private fun checkHistoricalFraudPatterns(orderId: String): Boolean { + DatabaseConfig.getConnection().use { conn -> + // Simulate variable latency + Thread.sleep(Random.nextLong(150, 400)) + + val sql = """ + WITH CurrentOrder AS ( + SELECT shipping_street, shipping_city, shipping_country, items_count + FROM OrderLogs + WHERE order_id = ? + ), + HistoricalFraud AS ( + SELECT DISTINCT fa.order_id, ol.shipping_street, ol.shipping_city + FROM FraudAlerts fa + INNER JOIN OrderLogs ol ON fa.order_id = ol.order_id + WHERE fa.severity IN ('HIGH', 'CRITICAL') + AND fa.detected_at >= DATEADD(DAY, -30, GETDATE()) + ) + SELECT + COUNT(DISTINCT hf.order_id) as matching_fraud_patterns, + STRING_AGG(hf.order_id, ', ') as matching_order_ids + FROM CurrentOrder co + INNER JOIN HistoricalFraud hf ON + (co.shipping_street = hf.shipping_street OR co.shipping_city = hf.shipping_city) + HAVING COUNT(DISTINCT hf.order_id) > 0 + """.trimIndent() + + conn.prepareStatement(sql).use { stmt -> + stmt.setString(1, orderId) + stmt.executeQuery().use { rs -> + if (rs.next()) { + val matchCount = rs.getInt("matching_fraud_patterns") + if (matchCount > 0) { + val riskScore = Math.min(matchCount * 0.30, 0.90) + logger.warn("๐Ÿ” FRAUD CHECK: Historical fraud pattern match for $orderId ($matchCount similar patterns)") + insertFraudAlert(orderId, "HISTORICAL_PATTERN", "HIGH", riskScore) + return true + } + } + } + } + } + return false + } + + /** + * Insert a fraud alert record into the FraudAlerts table + */ + private fun insertFraudAlert(orderId: String, alertType: String, severity: String, riskScore: Double) { + try { + DatabaseConfig.getConnection().use { conn -> + val sql = """ + INSERT INTO FraudAlerts (order_id, alert_type, severity, risk_score, reason) + VALUES (?, ?, ?, ?, ?) + """.trimIndent() + + conn.prepareStatement(sql).use { stmt -> + stmt.setString(1, orderId) + stmt.setString(2, alertType) + stmt.setString(3, severity) + stmt.setDouble(4, riskScore) + stmt.setString(5, "Automatic fraud detection triggered for $alertType") + + stmt.executeUpdate() + logger.info("๐Ÿ“ Fraud alert created for order $orderId: $alertType ($severity, risk: $riskScore)") + } + } + } catch (e: Exception) { + logger.error("Failed to insert fraud alert for order $orderId", e) + } + } +} diff --git a/src/fraud-detection/src/main/kotlin/frauddetection/OrderLogRepository.kt b/src/fraud-detection/src/main/kotlin/frauddetection/OrderLogRepository.kt index 1faaad665a..0474e5c6b4 100644 --- a/src/fraud-detection/src/main/kotlin/frauddetection/OrderLogRepository.kt +++ b/src/fraud-detection/src/main/kotlin/frauddetection/OrderLogRepository.kt @@ -15,6 +15,10 @@ import java.time.Instant class OrderLogRepository { private val logger: Logger = LogManager.getLogger(OrderLogRepository::class.java) private val jsonPrinter = JsonFormat.printer() + private val fraudDetection = FraudDetectionQueries() + + // Configurable fraud detection execution rate (0-100%) + private val fraudDetectionRate = System.getenv("FRAUD_DETECTION_RATE")?.toIntOrNull() ?: 80 fun saveOrder(orderResult: OrderResult): Boolean { return try { @@ -80,6 +84,22 @@ class OrderLogRepository { val rowsAffected = stmt.executeUpdate() if (rowsAffected > 0) { logger.info("Successfully saved order ${orderResult.orderId} to database") + + // Run fraud detection analysis after successful insert + if (kotlin.random.Random.nextInt(100) < fraudDetectionRate) { + try { + logger.info("๐Ÿ” Running fraud detection analysis for order ${orderResult.orderId}") + val fraudDetected = fraudDetection.analyzeOrder(orderResult.orderId) + if (fraudDetected) { + logger.warn("โš ๏ธ Fraud indicators detected for order ${orderResult.orderId}") + } else { + logger.info("โœ“ No fraud indicators found for order ${orderResult.orderId}") + } + } catch (e: Exception) { + logger.error("Error running fraud detection for order ${orderResult.orderId}", e) + } + } + true } else { logger.warn("Failed to save order ${orderResult.orderId} - no rows affected") diff --git a/src/payment/Dockerfile b/src/payment/Dockerfile index ecd2b3fc1b..83cac9054d 100644 --- a/src/payment/Dockerfile +++ b/src/payment/Dockerfile @@ -8,7 +8,9 @@ WORKDIR /usr/src/app/ COPY ./src/payment/package.json package.json COPY ./src/payment/package-lock.json package-lock.json - +RUN rm -rf node_modules/@splunk/otel/prebuilds \ + && export npm_config_build_from_source=true NODE_GYP_BUILD_FORCE=true \ + && npm rebuild @splunk/otel --build-from-source RUN npm ci --omit=dev && chmod -R go+r node_modules/@splunk/otel # ----------------------------------------------------------------------------- @@ -19,7 +21,9 @@ WORKDIR /usr/src/app/ # Create non-root user for security RUN groupadd -r appuser && useradd -r -g appuser appuser - +RUN rm -rf node_modules/@splunk/otel/prebuilds \ + && export npm_config_build_from_source=true NODE_GYP_BUILD_FORCE=true \ + && npm rebuild @splunk/otel --build-from-source COPY --from=builder /usr/src/app/node_modules/ node_modules/ COPY ./pb/demo.proto demo.proto