5a558eb09e
Backend Tests / discover-tests (push) Waiting to run
Backend Tests / ${{ matrix.name }} (push) Blocked by required conditions
Build and Publish SDK / build-and-publish (push) Waiting to run
Build Opik Docker Images / set-version (push) Waiting to run
Build Opik Docker Images / build-backend (push) Blocked by required conditions
Build Opik Docker Images / build-sandbox-executor-python (push) Blocked by required conditions
Build Opik Docker Images / build-python-backend (push) Blocked by required conditions
Build Opik Docker Images / build-frontend (push) Blocked by required conditions
Build Opik Docker Images / create-git-tag (push) Blocked by required conditions
ClickHouse Migration Cluster Check / validate-clickhouse-migrations (push) Waiting to run
Docs - Publish / run (push) Waiting to run
E2E Tests - Post Merge (v2) / 🧪 E2E v2 Tests (${{ github.event.inputs.tier || 't1' }}) (push) Waiting to run
E2E Tests - Post Merge (v2) / 📢 Slack Notification (push) Blocked by required conditions
Frontend Unit Tests / Test on Node 20 (push) Waiting to run
Guardrails E2E Tests / Select Python version matrix (push) Waiting to run
Guardrails E2E Tests / Guardrails E2E Tests ${{matrix.python_version}} (push) Blocked by required conditions
Guardrails E2E Tests / 📢 Slack Notification (push) Blocked by required conditions
Guardrails Backend Unit Tests / Guardrails Backend Unit Tests (push) Waiting to run
Guardrails Backend Unit Tests / 📢 Slack Notification (push) Blocked by required conditions
SDK Library Integration Tests Runner / Check Secrets (push) Waiting to run
SDK Library Integration Tests Runner / Missed OpenAI API Key Warning (push) Blocked by required conditions
SDK Library Integration Tests Runner / Build (push) Blocked by required conditions
SDK Library Integration Tests Runner / openai_tests (push) Blocked by required conditions
SDK Library Integration Tests Runner / langchain_tests (push) Blocked by required conditions
SDK Library Integration Tests Runner / langchain_legacy_tests (push) Blocked by required conditions
SDK Library Integration Tests Runner / llama_index_tests (push) Blocked by required conditions
SDK Library Integration Tests Runner / anthropic_tests (push) Blocked by required conditions
SDK Library Integration Tests Runner / mistral_tests (push) Blocked by required conditions
SDK Library Integration Tests Runner / groq_tests (push) Blocked by required conditions
SDK Library Integration Tests Runner / aisuite_tests (push) Blocked by required conditions
SDK Library Integration Tests Runner / haystack_tests (push) Blocked by required conditions
SDK Library Integration Tests Runner / dspy_tests (push) Blocked by required conditions
SDK Library Integration Tests Runner / crewai_v0_tests (push) Blocked by required conditions
SDK Library Integration Tests Runner / crewai_v1_tests (push) Blocked by required conditions
SDK Library Integration Tests Runner / genai_tests (push) Blocked by required conditions
SDK Library Integration Tests Runner / adk_tests (push) Blocked by required conditions
SDK Library Integration Tests Runner / adk_legacy_1_3_0_tests (push) Blocked by required conditions
SDK Library Integration Tests Runner / evaluation_metrics_tests (push) Blocked by required conditions
SDK Library Integration Tests Runner / bedrock_tests (push) Blocked by required conditions
SDK Library Integration Tests Runner / litellm_tests (push) Blocked by required conditions
SDK Library Integration Tests Runner / harbor_tests (push) Blocked by required conditions
SDK Library Integration Tests Runner / Slack Notification (push) Blocked by required conditions
Lint Opik Helm Chart / lint-helm-chart (Helm v3.21.0) (push) Waiting to run
Lint Opik Helm Chart / lint-helm-chart (Helm v4.2.0) (push) Waiting to run
Lint Opik Helm Chart / unittest-helm-chart (push) Waiting to run
Lint Opik Helm Chart / render-equality (push) Waiting to run
Opik Optimizer - Unit Tests / Opik Optimizer Unit Tests Python ${{matrix.python_version}} (push) Waiting to run
Python BE E2E Tests / Python BE E2E (push) Waiting to run
Python Backend Tests / run-python-backend-tests (push) Waiting to run
Python SDK Unit Tests / Python SDK Unit Tests ${{matrix.python_version}} (push) Waiting to run
Release Drafter / update_release_draft (push) Waiting to run
SDK E2E Libraries Integration Tests / Check Secrets (push) Waiting to run
SDK E2E Libraries Integration Tests / Missed OpenAI API Key Warning (push) Blocked by required conditions
SDK E2E Libraries Integration Tests / build-opik (push) Blocked by required conditions
SDK E2E Libraries Integration Tests / E2E Lib Integration Python ${{matrix.python_version}} (push) Blocked by required conditions
TypeScript SDK Integration Build & Publish / build-and-publish (opik-gemini) (push) Waiting to run
TypeScript SDK Integration Build & Publish / build-and-publish (opik-langchain) (push) Waiting to run
TypeScript SDK Integration Build & Publish / build-and-publish (opik-openai) (push) Waiting to run
TypeScript SDK Integration Build & Publish / build-and-publish (opik-otel) (push) Waiting to run
TypeScript SDK Integration Build & Publish / build-and-publish (opik-vercel) (push) Waiting to run
TypeScript SDK Library Integration Tests / Check Secrets (push) Waiting to run
TypeScript SDK Library Integration Tests / opik-vercel (Vercel AI SDK / eve) (push) Blocked by required conditions
TypeScript SDK Build & Publish / build-and-publish (push) Waiting to run
TypeScript SDK Unit Tests / Test on Node ${{ matrix.node-version }} (push) Waiting to run
TypeScript SDK Compatibility V1.x E2E Tests / Select Node version matrix (push) Has been cancelled
TypeScript SDK Compatibility V1.x E2E Tests / TypeScript SDK Compatibility V1.x E2E Tests Node ${{matrix.node_version}} (push) Has been cancelled
TypeScript SDK E2E Tests / TypeScript SDK E2E Tests Node ${{matrix.node_version}} (push) Has been cancelled
Opik Optimizer - E2E Tests / build-opik (push) Has been cancelled
TypeScript SDK Compatibility V1.x E2E Tests / build-opik (push) Has been cancelled
Python SDK E2E Tests / Select Python version matrix (push) Has been cancelled
Python SDK E2E Tests / Python SDK E2E Tests ${{matrix.python_version}} (push) Has been cancelled
Python SDK E2E Tests / build-opik (push) Has been cancelled
Python SDK Compatibility V1.x E2E Tests / Select Python version matrix (push) Has been cancelled
Python SDK Compatibility V1.x E2E Tests / Python SDK Compatibility V1.x E2E Tests ${{matrix.python_version}} (push) Has been cancelled
Python SDK Compatibility V1.x E2E Tests / build-opik (push) Has been cancelled
TypeScript SDK E2E Tests / Select Node version matrix (push) Has been cancelled
TypeScript SDK E2E Tests / build-opik (push) Has been cancelled
Opik Optimizer - E2E Tests / Opik Optimizer E2E Tests Python ${{matrix.python_version}} (push) Has been cancelled
Opik Optimizer - E2E Tests / Opik Optimizer Integration Smoke Tests (push) Has been cancelled
🐙 Code Quality / detect (push) Has been cancelled
🐙 Code Quality / lint (${{ matrix.leg.name }}) (push) Has been cancelled
🐙 Code Quality / summary (push) Has been cancelled
1813 lines
68 KiB
Bash
Executable File
1813 lines
68 KiB
Bash
Executable File
#!/bin/bash
|
||
|
||
set -euo pipefail
|
||
|
||
# Opik Development Runner Script
|
||
|
||
# Variables
|
||
DEBUG_MODE=${DEBUG_MODE:-false}
|
||
ORIGINAL_COMMAND="$0 $@"
|
||
|
||
# Local dev defaults to version_2 (matches the bundled config.yml and
|
||
# docker-compose defaults) so a fresh worktree's empty backend doesn't trip
|
||
# the "Workspace upgrade required" pairing screen. Exported here so the
|
||
# JAR-mode backend (start_backend) inherits the same value as docker-compose.
|
||
# Override by exporting TOGGLE_FORCE_WORKSPACE_VERSION=disabled (or
|
||
# version_1) before invoking the script.
|
||
export TOGGLE_FORCE_WORKSPACE_VERSION="${TOGGLE_FORCE_WORKSPACE_VERSION:-version_2}"
|
||
|
||
# Agent Insights read-only freeform SQL is off by default for local dev (the feature is driven by the Ollie agent,
|
||
# which isn't available locally). Set TOGGLE_AGENT_INSIGHTS_ENABLED=true before invoking to opt in: start_backend
|
||
# then provisions the restricted read-only ClickHouse user/profile/policies and the JVM connects with the feature on.
|
||
# Exported here so this single variable controls both the provisioning gate and the JAR-mode backend (config.yml).
|
||
export TOGGLE_AGENT_INSIGHTS_ENABLED="${TOGGLE_AGENT_INSIGHTS_ENABLED:-false}"
|
||
|
||
# Configuration
|
||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" &> /dev/null && pwd)"
|
||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." &> /dev/null && pwd)"
|
||
BACKEND_DIR="$PROJECT_ROOT/apps/opik-backend"
|
||
FRONTEND_DIR="$PROJECT_ROOT/apps/opik-frontend"
|
||
|
||
# Source shared worktree utilities
|
||
WORKTREE_UTILS_ROOT="$PROJECT_ROOT"
|
||
source "$SCRIPT_DIR/worktree-utils.sh"
|
||
init_worktree_ports
|
||
|
||
# Dynamic PID and log file paths (isolated per worktree)
|
||
BACKEND_PID_FILE="/tmp/${RESOURCE_PREFIX}-backend.pid"
|
||
FRONTEND_PID_FILE="/tmp/${RESOURCE_PREFIX}-frontend.pid"
|
||
BACKEND_LOG_FILE="/tmp/${RESOURCE_PREFIX}-backend.log"
|
||
FRONTEND_LOG_FILE="/tmp/${RESOURCE_PREFIX}-frontend.log"
|
||
OLLIE_PID_FILE="/tmp/${RESOURCE_PREFIX}-ollie.pid"
|
||
OLLIE_LOG_FILE="/tmp/${RESOURCE_PREFIX}-ollie.log"
|
||
# Sidecar so --stop can find the ollie repo even without OLLIE_REPO_PATH in scope.
|
||
OLLIE_REPO_PATH_FILE="/tmp/${RESOURCE_PREFIX}-ollie.repo"
|
||
COST_API_PID_FILE="/tmp/${RESOURCE_PREFIX}-cost-api.pid"
|
||
COST_API_LOG_FILE="/tmp/${RESOURCE_PREFIX}-cost-api.log"
|
||
# Sidecar so --stop can find the repo even without AI_COST_BACKEND_PATH in scope.
|
||
COST_API_REPO_PATH_FILE="/tmp/${RESOURCE_PREFIX}-cost-api.repo"
|
||
|
||
# Private ai-spend FE plugin (Opik team only). Auto-detected at
|
||
# $PROJECT_ROOT/../opik-plugin-ai-spend if not set; its src/ is symlinked into
|
||
# the FE plugins folder so it compiles as part of opik. Opt out with
|
||
# AI_SPEND_PLUGIN_PATH= (empty string).
|
||
AI_SPEND_PLUGIN_LINK="$FRONTEND_DIR/src/plugins/ai-spend"
|
||
|
||
# Ollie local dev integration (Opik team only).
|
||
# Auto-detected at $PROJECT_ROOT/../ollie-assist if not explicitly set; see the
|
||
# detect block below. Override with OLLIE_REPO_PATH=/some/other/path; opt out
|
||
# with OLLIE_REPO_PATH= (empty string).
|
||
OLLIE_API_PORT="${OLLIE_API_PORT:-9080}"
|
||
OLLIE_CONSOLE_PORT="${OLLIE_CONSOLE_PORT:-3333}"
|
||
|
||
# ai-cost-backend (cost-api) local dev integration (Opik team only).
|
||
# Auto-detected at $PROJECT_ROOT/../ai-cost-backend if not explicitly set; see
|
||
# the detect block below. Override with AI_COST_BACKEND_PATH=/some/other/path;
|
||
# opt out with AI_COST_BACKEND_PATH= (empty string).
|
||
# Default off a free base + the worktree offset so it never collides with the
|
||
# Opik dev stack (the python-backend already publishes host 8000) and stays
|
||
# unique across worktrees. cost-api's own standalone default is still 8000.
|
||
AI_COST_BACKEND_PORT="${AI_COST_BACKEND_PORT:-$((8400 + PORT_OFFSET))}"
|
||
|
||
# Comet EM stack (comet-backend + comet-react + single-origin proxy).
|
||
# All EM logic lives in dev-runner-em.sh to keep this script focused; it is inert
|
||
# unless PLATFORM_ENABLED=true. Sourced here so its EM_* vars and em_* functions
|
||
# are in scope for the thin hooks below (start_em_stack, em_print_status, …).
|
||
source "$SCRIPT_DIR/dev-runner-em.sh"
|
||
|
||
# Colors for output
|
||
RED='\033[0;31m'
|
||
GREEN='\033[0;32m'
|
||
YELLOW='\033[1;33m'
|
||
BLUE='\033[0;34m'
|
||
NC='\033[0m' # No Color
|
||
|
||
# Logging functions
|
||
log_info() {
|
||
echo -e "${BLUE}[INFO]${NC} $1"
|
||
}
|
||
|
||
log_success() {
|
||
echo -e "${GREEN}[SUCCESS]${NC} $1"
|
||
}
|
||
|
||
log_warning() {
|
||
echo -e "${YELLOW}[WARNING]${NC} $1"
|
||
}
|
||
|
||
log_error() {
|
||
echo -e "${RED}[ERROR]${NC} $1"
|
||
}
|
||
|
||
log_debug() {
|
||
if [ "$DEBUG_MODE" = "true" ]; then
|
||
echo -e "${YELLOW}[DEBUG]${NC} $1"
|
||
fi
|
||
}
|
||
|
||
require_command() {
|
||
if ! command -v "$1" &>/dev/null; then
|
||
log_error "Required command '$1' not found. Please install it."
|
||
exit 1
|
||
fi
|
||
}
|
||
|
||
# Auto-detect a sibling ollie-assist checkout when OLLIE_REPO_PATH is unset
|
||
# entirely. Most Opik devs keep ollie-assist next to opik (e.g.
|
||
# ~/repos/opik and ~/repos/ollie-assist), so defaulting here removes the need
|
||
# to export the variable each session. Explicit values still win; setting
|
||
# OLLIE_REPO_PATH="" opts out (the `unset+set` test below treats empty as
|
||
# "user-provided", so the auto-detect skips).
|
||
if [ -z "${OLLIE_REPO_PATH+set}" ]; then
|
||
_ollie_candidate="$(cd "$PROJECT_ROOT/.." 2>/dev/null && pwd)/ollie-assist"
|
||
if [ -d "$_ollie_candidate" ] && [ -f "$_ollie_candidate/Makefile" ]; then
|
||
OLLIE_REPO_PATH="$_ollie_candidate"
|
||
log_info "Auto-detected ollie-assist checkout at $OLLIE_REPO_PATH (export OLLIE_REPO_PATH= to opt out)"
|
||
fi
|
||
unset _ollie_candidate
|
||
fi
|
||
|
||
# Auto-detect a sibling opik-plugin-ai-spend checkout when AI_SPEND_PLUGIN_PATH
|
||
# is unset entirely (mirrors the ollie-assist convention above). Setting
|
||
# AI_SPEND_PLUGIN_PATH="" opts out.
|
||
if [ -z "${AI_SPEND_PLUGIN_PATH+set}" ]; then
|
||
_ai_spend_candidate="$(cd "$PROJECT_ROOT/.." 2>/dev/null && pwd)/opik-plugin-ai-spend"
|
||
if [ -d "$_ai_spend_candidate/src" ]; then
|
||
AI_SPEND_PLUGIN_PATH="$_ai_spend_candidate"
|
||
log_info "Auto-detected ai-spend plugin checkout at $AI_SPEND_PLUGIN_PATH (export AI_SPEND_PLUGIN_PATH= to opt out)"
|
||
fi
|
||
unset _ai_spend_candidate
|
||
fi
|
||
|
||
# Auto-detect a sibling ai-cost-backend checkout when AI_COST_BACKEND_PATH is
|
||
# unset entirely (mirrors the conventions above). cost-api serves the AI Spend
|
||
# API the FE plugin calls; setting AI_COST_BACKEND_PATH="" opts out.
|
||
if [ -z "${AI_COST_BACKEND_PATH+set}" ]; then
|
||
_cost_api_candidate="$(cd "$PROJECT_ROOT/.." 2>/dev/null && pwd)/ai-cost-backend"
|
||
if [ -d "$_cost_api_candidate" ] && [ -f "$_cost_api_candidate/pyproject.toml" ]; then
|
||
AI_COST_BACKEND_PATH="$_cost_api_candidate"
|
||
log_info "Auto-detected ai-cost-backend checkout at $AI_COST_BACKEND_PATH (export AI_COST_BACKEND_PATH= to opt out)"
|
||
fi
|
||
unset _cost_api_candidate
|
||
fi
|
||
|
||
# Function to find JAR files in target directory
|
||
find_jar_files() {
|
||
local jar_files=()
|
||
while IFS= read -r -d '' jar; do
|
||
jar_files+=("$jar")
|
||
done < <(find target -maxdepth 1 -type f -name 'opik-backend-*.jar' ! -name '*original*' ! -name '*sources*' ! -name '*javadoc*' -print0)
|
||
|
||
if [ "${#jar_files[@]}" -eq 0 ]; then
|
||
return 1 # No JAR files found
|
||
elif [ "${#jar_files[@]}" -eq 1 ]; then
|
||
JAR_FILE="${jar_files[0]}"
|
||
log_info "Using JAR file: $JAR_FILE"
|
||
else
|
||
log_warning "Multiple backend JAR files found in target/:"
|
||
for jar in "${jar_files[@]}"; do
|
||
log_warning " - $jar"
|
||
done
|
||
|
||
# Sort JAR files by version (assuming semantic versioning in filename)
|
||
JAR_FILE=$(printf '%s\n' "${jar_files[@]}" | sort -V | tail -n 1)
|
||
log_warning "Automatically selected JAR with highest version: $JAR_FILE"
|
||
log_warning "To use a different JAR, clean up target/ directory and rebuild"
|
||
fi
|
||
|
||
return 0 # JAR file found and selected
|
||
}
|
||
|
||
# Function to start Docker services (infrastructure or infrastructure + frontend or etc.)
|
||
# Args: $1 = mode (--infra or --local-be or etc.)
|
||
start_docker_services() {
|
||
local mode="$1"
|
||
|
||
log_info "Starting Docker services..."
|
||
cd "$PROJECT_ROOT" || { log_error "Project root directory not found"; exit 1; }
|
||
|
||
if ./opik.sh "$mode"; then
|
||
log_success "Docker services started successfully"
|
||
else
|
||
log_error "Failed to start Docker services"
|
||
exit 1
|
||
fi
|
||
}
|
||
|
||
# Function to stop Docker services (infrastructure or infrastructure + frontend or etc.)
|
||
# Args: $1 = mode (--infra or --local-be or etc.)
|
||
stop_docker_services() {
|
||
local mode="$1"
|
||
|
||
log_info "Stopping Docker services..."
|
||
cd "$PROJECT_ROOT" || { log_error "Project root directory not found"; exit 1; }
|
||
|
||
if ./opik.sh "$mode" --stop; then
|
||
log_success "Docker services stopped"
|
||
else
|
||
log_warning "Failed to stop some Docker services"
|
||
fi
|
||
}
|
||
|
||
# Function to verify Docker services
|
||
# Args: $1 = mode (--infra or --local-be or etc.)
|
||
verify_docker_services() {
|
||
local mode="$1"
|
||
|
||
cd "$PROJECT_ROOT" || { log_error "Project root directory not found"; exit 1; }
|
||
./opik.sh "$mode" --verify >/dev/null 2>&1
|
||
return $?
|
||
}
|
||
|
||
start_local_be_fe() {
|
||
start_docker_services "--local-be-fe"
|
||
}
|
||
|
||
stop_local_be_fe() {
|
||
stop_docker_services "--local-be-fe"
|
||
}
|
||
|
||
verify_local_be_fe() {
|
||
verify_docker_services "--local-be-fe"
|
||
}
|
||
|
||
start_local_be() {
|
||
start_docker_services "--local-be"
|
||
}
|
||
|
||
stop_local_be() {
|
||
stop_docker_services "--local-be"
|
||
}
|
||
|
||
verify_local_be() {
|
||
verify_docker_services "--local-be"
|
||
}
|
||
|
||
# Function to build backend
|
||
build_backend() {
|
||
require_command mvn
|
||
log_info "Building backend (skipping tests)..."
|
||
log_debug "Backend directory: $BACKEND_DIR"
|
||
cd "$BACKEND_DIR" || { log_error "Backend directory not found"; exit 1; }
|
||
|
||
# resolve.skip=true skips swagger, adjust if any future interference
|
||
MAVEN_BUILD_CMD="mvn clean install -T 1C -Dmaven.test.skip=true -Dspotless.skip=true -Dmaven.javadoc.skip=true -Dmaven.source.skip=true -Dmaven.test.compile.skip=true -Dmaven.test.resources.skip=true -Dmaven.compiler.useIncrementalCompilation=false -Dresolve.skip=true"
|
||
log_debug "Running: $MAVEN_BUILD_CMD"
|
||
if $MAVEN_BUILD_CMD; then
|
||
log_success "Backend build completed successfully"
|
||
else
|
||
log_error "Backend build failed"
|
||
exit 1
|
||
fi
|
||
}
|
||
|
||
# Function to build frontend
|
||
build_frontend() {
|
||
require_command npm
|
||
log_info "Building frontend..."
|
||
cd "$FRONTEND_DIR" || { log_error "Frontend directory not found"; exit 1; }
|
||
|
||
if npm install; then
|
||
log_success "Frontend build completed successfully"
|
||
else
|
||
log_error "Frontend build failed"
|
||
exit 1
|
||
fi
|
||
}
|
||
|
||
# Function to lint frontend
|
||
lint_frontend() {
|
||
require_command npm
|
||
log_info "Linting frontend..."
|
||
cd "$FRONTEND_DIR" || { log_error "Frontend directory not found"; exit 1; }
|
||
|
||
if npm run lint:fix; then
|
||
log_success "Frontend linting completed successfully"
|
||
else
|
||
log_error "Frontend linting failed"
|
||
exit 1
|
||
fi
|
||
|
||
log_info "Typechecking frontend..."
|
||
if npm run typecheck; then
|
||
log_success "Frontend typechecking completed successfully"
|
||
else
|
||
log_error "Frontend typechecking failed"
|
||
exit 1
|
||
fi
|
||
|
||
log_info "Validating frontend dependencies..."
|
||
if npm run deps:validate; then
|
||
log_success "Frontend dependency validation completed successfully"
|
||
else
|
||
log_error "Frontend dependency validation failed"
|
||
exit 1
|
||
fi
|
||
}
|
||
|
||
# Function to lint backend
|
||
lint_backend() {
|
||
require_command mvn
|
||
log_info "Linting backend..."
|
||
cd "$BACKEND_DIR" || { log_error "Backend directory not found"; exit 1; }
|
||
|
||
if mvn spotless:apply; then
|
||
log_success "Backend linting completed successfully"
|
||
else
|
||
log_error "Backend linting failed"
|
||
exit 1
|
||
fi
|
||
}
|
||
|
||
print_migrations_recovery_message() {
|
||
log_error "To recover, you may need to clean up Docker volumes (WARNING: ALL DATA WILL BE LOST):"
|
||
log_error " 1. Stop all services: $0 --stop"
|
||
log_error " 2. Clean all data volumes (DANGER): cd $PROJECT_ROOT && ./opik.sh --clean"
|
||
log_error " 3. Run again your current flow: $ORIGINAL_COMMAND"
|
||
}
|
||
|
||
# Function to run database migrations
|
||
run_db_migrations() {
|
||
require_command java
|
||
log_info "Running database migrations..."
|
||
log_debug "Backend directory: $BACKEND_DIR"
|
||
cd "$BACKEND_DIR" || { log_error "Backend directory not found"; exit 1; }
|
||
|
||
# Find and validate the JAR file
|
||
if ! find_jar_files; then
|
||
log_warning "No backend JAR file found in target/. Building backend automatically..."
|
||
build_backend
|
||
|
||
# Re-scan for JAR files after build
|
||
if ! find_jar_files; then
|
||
log_error "Backend build completed but no JAR file found. Build may have failed."
|
||
exit 1
|
||
fi
|
||
fi
|
||
|
||
log_debug "Running migrations with JAR: $JAR_FILE"
|
||
log_debug "Current directory: $(pwd)"
|
||
|
||
# Run MySQL (state DB) migrations
|
||
log_info "Running MySQL (state DB) migrations..."
|
||
# Set the database connection environment variables for MySQL migrations (using dynamic port)
|
||
# Note: STATE_DB_URL format is "host:port/database?params" - the jdbc:mysql:// prefix is added by config.yml
|
||
export STATE_DB_DATABASE_NAME="opik"
|
||
export STATE_DB_URL="localhost:${MYSQL_PORT}/opik?createDatabaseIfNotExist=true&rewriteBatchedStatements=true&connectionTimeZone=UTC&forceConnectionTimeZoneToSession=true"
|
||
log_debug "MySQL connection: localhost:${MYSQL_PORT}"
|
||
if java -jar "$JAR_FILE" db migrate config.yml; then
|
||
log_success "MySQL migrations completed successfully"
|
||
else
|
||
# TODO: dbAnalytics clear-checksums not supported by liquibase-clickhouse yet,
|
||
# this would enable automatic recovery,
|
||
# not worthy adding it only for MySQL as volumes might need pruning anyway
|
||
log_error "MySQL migrations failed"
|
||
print_migrations_recovery_message
|
||
exit 1
|
||
fi
|
||
|
||
# Run ClickHouse (analytics DB) migrations
|
||
log_info "Running ClickHouse (analytics DB) migrations..."
|
||
# Set the database name environment variable for ClickHouse migrations
|
||
export ANALYTICS_DB_DATABASE_NAME="opik"
|
||
# Set the connection URL (using dynamic port, no database suffix to match original behavior)
|
||
export ANALYTICS_DB_MIGRATIONS_URL="jdbc:clickhouse://localhost:${CLICKHOUSE_HTTP_PORT}"
|
||
if java -jar "$JAR_FILE" dbAnalytics migrate config.yml; then
|
||
log_success "ClickHouse migrations completed successfully"
|
||
else
|
||
# TODO: dbAnalytics clear-checksums not supported by liquibase-clickhouse yet,
|
||
# this would enable automatic recovery
|
||
log_error "ClickHouse migrations failed"
|
||
print_migrations_recovery_message
|
||
exit 1
|
||
fi
|
||
|
||
log_success "All database migrations completed successfully"
|
||
}
|
||
|
||
wait_for_backend_ready() {
|
||
require_command curl
|
||
log_info "Waiting for backend to be ready on port ${BACKEND_PORT}..."
|
||
local max_wait=60
|
||
local count=0
|
||
local backend_ready=false
|
||
|
||
while [ $count -lt $max_wait ]; do
|
||
if curl -sf "http://localhost:${BACKEND_PORT}/health-check" >/dev/null 2>&1; then
|
||
backend_ready=true
|
||
break
|
||
fi
|
||
sleep 1
|
||
count=$((count + 1))
|
||
|
||
if ! kill -0 "$BACKEND_PID" 2>/dev/null; then
|
||
log_error "Backend process died while waiting for it to be ready"
|
||
log_error "Check logs: tail -f $BACKEND_LOG_FILE"
|
||
rm -f "$BACKEND_PID_FILE"
|
||
return 1
|
||
fi
|
||
done
|
||
|
||
if [ "$backend_ready" = true ]; then
|
||
log_success "Backend is ready and accepting connections"
|
||
log_info "Backend API: ${GREEN}http://localhost:${BACKEND_PORT}${NC}"
|
||
if [ "$DEBUG_MODE" = "true" ]; then
|
||
log_debug "Debug mode enabled - check logs for detailed output"
|
||
fi
|
||
return 0
|
||
else
|
||
log_error "Backend failed to become ready after ${max_wait}s"
|
||
log_error "Check logs: tail -f $BACKEND_LOG_FILE"
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
# --- Ollie local dev (gated on OLLIE_REPO_PATH) ---------------------------
|
||
# When OLLIE_REPO_PATH points at a valid ollie-assist checkout (auto-detected
|
||
# at $PROJECT_ROOT/../ollie-assist or set explicitly), dev-runner spawns
|
||
# `make dev` in that checkout (docker-compose API on 9080 + console JS dev
|
||
# server on 3333) and wires the frontend to it via
|
||
# VITE_ASSISTANT_SIDEBAR_BASE_URL. Empty / no sibling means no-op — OSS
|
||
# contributors and Opik devs not testing the assistant are
|
||
# unaffected.
|
||
|
||
# Has the user opted into the local ollie integration?
|
||
ollie_enabled() {
|
||
[ -n "${OLLIE_REPO_PATH:-}" ]
|
||
}
|
||
|
||
# Is the ollie process currently alive? Used by start_frontend to decide
|
||
# whether it's safe to point the sidebar at the local service.
|
||
ollie_running() {
|
||
[ -f "$OLLIE_PID_FILE" ] && kill -0 "$(cat "$OLLIE_PID_FILE")" 2>/dev/null
|
||
}
|
||
|
||
# Is ollie's API actually serving requests? Detects an already-running
|
||
# instance (e.g. started outside this dev-runner) so we can reuse it
|
||
# instead of trying to spawn a duplicate on the same ports.
|
||
ollie_healthy() {
|
||
command -v curl >/dev/null 2>&1 || return 1
|
||
curl -sf --max-time 2 "http://localhost:${OLLIE_API_PORT}/healthz" >/dev/null 2>&1
|
||
}
|
||
|
||
# BFS-walk every descendant of $1. `make dev` fans into npm → node and shells
|
||
# out to docker-compose; one level of pgrep -P misses grandchildren. Docker
|
||
# containers are NOT descendants of `make`, so this does NOT touch them —
|
||
# `make dev-stop` is the only correct way to stop those.
|
||
#
|
||
# Returns the descendant PIDs on stdout, one per line. The caller is
|
||
# expected to snapshot BEFORE killing the parent: once the parent exits,
|
||
# children reparent to init/launchd and pgrep -P loses the trail.
|
||
get_descendants() {
|
||
local root="$1"
|
||
local frontier="$root"
|
||
local all=""
|
||
while [ -n "$frontier" ]; do
|
||
local next=""
|
||
for p in $frontier; do
|
||
local kids
|
||
kids=$(pgrep -P "$p" 2>/dev/null || true)
|
||
if [ -n "$kids" ]; then
|
||
next="$next $kids"
|
||
all="$all $kids"
|
||
fi
|
||
done
|
||
frontier=$(echo "$next" | xargs -n1 2>/dev/null | sort -u | xargs 2>/dev/null || true)
|
||
done
|
||
echo "$all" | xargs -n1 2>/dev/null | sort -u | xargs 2>/dev/null || true
|
||
}
|
||
|
||
wait_for_ollie_ready() {
|
||
require_command curl
|
||
local pid="${1:-}"
|
||
log_info "Waiting for ollie to be ready on port ${OLLIE_API_PORT}..."
|
||
local max_wait=120
|
||
local count=0
|
||
local ollie_ready=false
|
||
|
||
while [ $count -lt $max_wait ]; do
|
||
if curl -sf "http://localhost:${OLLIE_API_PORT}/healthz" >/dev/null 2>&1; then
|
||
ollie_ready=true
|
||
break
|
||
fi
|
||
sleep 1
|
||
count=$((count + 1))
|
||
|
||
if [ -n "$pid" ] && ! kill -0 "$pid" 2>/dev/null; then
|
||
log_error "Ollie process died while waiting for it to be ready"
|
||
log_error "Check logs: tail -f $OLLIE_LOG_FILE"
|
||
rm -f "$OLLIE_PID_FILE"
|
||
return 1
|
||
fi
|
||
done
|
||
|
||
if [ "$ollie_ready" = true ]; then
|
||
log_success "Ollie is ready and accepting connections"
|
||
log_info "Ollie API: ${GREEN}http://localhost:${OLLIE_API_PORT}${NC}"
|
||
log_info "Ollie console: ${GREEN}http://localhost:${OLLIE_CONSOLE_PORT}${NC}"
|
||
return 0
|
||
else
|
||
log_error "Ollie failed to become ready after ${max_wait}s"
|
||
log_error "Check logs: tail -f $OLLIE_LOG_FILE"
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
start_ollie_local() {
|
||
if ! ollie_enabled; then
|
||
return 0
|
||
fi
|
||
require_command make
|
||
|
||
if [ ! -d "$OLLIE_REPO_PATH" ]; then
|
||
log_warning "OLLIE_REPO_PATH points to a non-existent directory: $OLLIE_REPO_PATH"
|
||
log_warning "Skipping ollie-assist startup; sidebar will be disabled"
|
||
return 1
|
||
fi
|
||
if [ ! -f "$OLLIE_REPO_PATH/Makefile" ]; then
|
||
log_warning "OLLIE_REPO_PATH has no Makefile: $OLLIE_REPO_PATH"
|
||
log_warning "Skipping ollie-assist startup; sidebar will be disabled"
|
||
return 1
|
||
fi
|
||
|
||
log_info "Starting ollie-assist (make dev) from $OLLIE_REPO_PATH..."
|
||
|
||
# Reuse a healthy ollie started outside this dev-runner (e.g. by a manual
|
||
# `make dev`). Avoids port conflicts on 3333/9080 and lets devs share a
|
||
# single ollie instance across opik worktrees.
|
||
if ollie_healthy; then
|
||
log_success "Ollie is already healthy on port ${OLLIE_API_PORT} — reusing existing instance"
|
||
log_info "Skipping 'make dev'; stop_ollie_local will not be called for this instance"
|
||
# Defensively clear any stale state files from a prior crashed session.
|
||
# Without this, a later --stop would see the leftover PID/sidecar and
|
||
# run `make dev-stop`, killing the external ollie we just chose to reuse.
|
||
rm -f "$OLLIE_PID_FILE" "$OLLIE_REPO_PATH_FILE"
|
||
return 0
|
||
fi
|
||
|
||
if [ -f "$OLLIE_PID_FILE" ]; then
|
||
OLLIE_PID=$(cat "$OLLIE_PID_FILE")
|
||
if kill -0 "$OLLIE_PID" 2>/dev/null; then
|
||
log_warning "Ollie is already running (PID: $OLLIE_PID)"
|
||
return 0
|
||
else
|
||
log_warning "Removing stale ollie PID file (process $OLLIE_PID no longer exists)"
|
||
rm -f "$OLLIE_PID_FILE"
|
||
fi
|
||
fi
|
||
|
||
(
|
||
cd "$OLLIE_REPO_PATH" || exit 1
|
||
nohup make dev > "$OLLIE_LOG_FILE" 2>&1 &
|
||
echo $! > "$OLLIE_PID_FILE"
|
||
)
|
||
printf '%s\n' "$OLLIE_REPO_PATH" > "$OLLIE_REPO_PATH_FILE"
|
||
|
||
OLLIE_PID=$(cat "$OLLIE_PID_FILE")
|
||
log_debug "Ollie process started with PID: $OLLIE_PID"
|
||
|
||
sleep 3
|
||
if ! kill -0 "$OLLIE_PID" 2>/dev/null; then
|
||
log_warning "Ollie failed to start. Check logs: cat $OLLIE_LOG_FILE"
|
||
log_warning "Continuing without sidebar; opik FE+BE will still come up"
|
||
rm -f "$OLLIE_PID_FILE" "$OLLIE_REPO_PATH_FILE"
|
||
return 1
|
||
fi
|
||
|
||
log_success "Ollie process started (PID: $OLLIE_PID)"
|
||
log_info "Ollie logs: tail -f $OLLIE_LOG_FILE"
|
||
if ! wait_for_ollie_ready "$OLLIE_PID"; then
|
||
log_warning "Ollie did not become ready in time; continuing without sidebar"
|
||
# Tear down what we spawned so frontend doesn't point at a half-up service.
|
||
stop_ollie_local >/dev/null 2>&1 || true
|
||
return 1
|
||
fi
|
||
return 0
|
||
}
|
||
|
||
stop_ollie_local() {
|
||
if [ ! -f "$OLLIE_PID_FILE" ] && [ ! -f "$OLLIE_REPO_PATH_FILE" ]; then
|
||
# Nothing to do — quiet skip so OSS / non-ollie runs don't see noise.
|
||
return 0
|
||
fi
|
||
|
||
# Recover the repo path from the sidecar if the caller didn't export it.
|
||
local ollie_repo="${OLLIE_REPO_PATH:-}"
|
||
if [ -z "$ollie_repo" ] && [ -f "$OLLIE_REPO_PATH_FILE" ]; then
|
||
ollie_repo=$(cat "$OLLIE_REPO_PATH_FILE")
|
||
fi
|
||
|
||
if [ -f "$OLLIE_PID_FILE" ]; then
|
||
local ollie_pid
|
||
ollie_pid=$(cat "$OLLIE_PID_FILE")
|
||
if kill -0 "$ollie_pid" 2>/dev/null; then
|
||
log_info "Stopping ollie (PID: $ollie_pid)..."
|
||
|
||
# Snapshot the descendant tree while parent is alive: once make
|
||
# exits, npm/node children reparent to init and pgrep -P loses
|
||
# the trail. We need the snapshot to chase them down later.
|
||
local descendants
|
||
descendants=$(get_descendants "$ollie_pid")
|
||
|
||
kill -TERM "$ollie_pid" 2>/dev/null || true
|
||
if [ -n "$descendants" ]; then
|
||
for p in $descendants; do
|
||
kill -TERM "$p" 2>/dev/null || true
|
||
done
|
||
fi
|
||
|
||
for _ in {1..10}; do
|
||
if ! kill -0 "$ollie_pid" 2>/dev/null; then
|
||
break
|
||
fi
|
||
sleep 1
|
||
done
|
||
|
||
# Force-kill any survivors from the snapshot, even if the root
|
||
# is already gone (orphans reparent to init/launchd and persist).
|
||
if kill -0 "$ollie_pid" 2>/dev/null; then
|
||
log_warning "Force killing ollie tree..."
|
||
kill -9 "$ollie_pid" 2>/dev/null || true
|
||
fi
|
||
if [ -n "$descendants" ]; then
|
||
for p in $descendants; do
|
||
kill -9 "$p" 2>/dev/null || true
|
||
done
|
||
fi
|
||
else
|
||
log_warning "Ollie PID file exists but process is not running (cleaning up stale PID file)"
|
||
fi
|
||
fi
|
||
|
||
# Bring docker-compose stack down so subsequent runs get a clean slate.
|
||
# nohup'd make dev is detached from docker-compose's daemonized containers,
|
||
# so SIGTERM above can't reach them — make dev-stop is the real cleanup.
|
||
if [ -n "$ollie_repo" ] && [ -d "$ollie_repo" ]; then
|
||
log_info "Stopping ollie docker-compose stack..."
|
||
( cd "$ollie_repo" && make dev-stop > /dev/null 2>&1 ) || \
|
||
log_warning "make dev-stop failed; container may still be running"
|
||
elif [ -n "$ollie_repo" ]; then
|
||
log_warning "Recorded OLLIE_REPO_PATH no longer exists: $ollie_repo"
|
||
log_warning "Skipping make dev-stop; docker-compose containers may need manual cleanup"
|
||
fi
|
||
|
||
rm -f "$OLLIE_PID_FILE" "$OLLIE_REPO_PATH_FILE"
|
||
log_success "Ollie stopped"
|
||
}
|
||
|
||
display_ollie_process_status() {
|
||
if [ -f "$OLLIE_PID_FILE" ] && kill -0 "$(cat "$OLLIE_PID_FILE")" 2>/dev/null; then
|
||
echo -e "Ollie: ${GREEN}RUNNING${NC} (PID: $(cat "$OLLIE_PID_FILE"))"
|
||
return 0
|
||
fi
|
||
# Detect reused instance: ollie is healthy but we didn't spawn it,
|
||
# so there's no PID file to point at.
|
||
if ollie_healthy; then
|
||
echo -e "Ollie: ${GREEN}RUNNING${NC} (reused external instance on port ${OLLIE_API_PORT})"
|
||
return 0
|
||
fi
|
||
echo -e "Ollie: ${RED}STOPPED${NC}"
|
||
return 1
|
||
}
|
||
|
||
# --- ai-cost-backend / cost-api local dev (gated on AI_COST_BACKEND_PATH) -----
|
||
# When AI_COST_BACKEND_PATH points at a sibling ai-cost-backend checkout
|
||
# (auto-detected at $PROJECT_ROOT/../ai-cost-backend or set explicitly),
|
||
# dev-runner runs cost-api (uv) with auth disabled, pointed at the dev-runner's
|
||
# own ClickHouse + MySQL, and wires the FE proxy to it via
|
||
# VITE_AI_COST_BACKEND_PORT. The AI Spend FE plugin always calls ai-cost-backend.
|
||
# Empty / no sibling is a no-op — OSS contributors and devs not testing AI Spend are unaffected.
|
||
|
||
cost_api_enabled() {
|
||
[ -n "${AI_COST_BACKEND_PATH:-}" ]
|
||
}
|
||
|
||
cost_api_running() {
|
||
[ -f "$COST_API_PID_FILE" ] && kill -0 "$(cat "$COST_API_PID_FILE")" 2>/dev/null
|
||
}
|
||
|
||
cost_api_healthy() {
|
||
command -v curl >/dev/null 2>&1 || return 1
|
||
curl -sf --max-time 2 "http://localhost:${AI_COST_BACKEND_PORT}/cost-api/ping" >/dev/null 2>&1
|
||
}
|
||
|
||
wait_for_cost_api_ready() {
|
||
require_command curl
|
||
local pid="${1:-}"
|
||
log_info "Waiting for cost-api to be ready on port ${AI_COST_BACKEND_PORT}..."
|
||
local max_wait=60
|
||
local count=0
|
||
while [ $count -lt $max_wait ]; do
|
||
if cost_api_healthy; then
|
||
log_success "cost-api is ready and accepting connections"
|
||
log_info "cost-api: ${GREEN}http://localhost:${AI_COST_BACKEND_PORT}/cost-api/ping${NC}"
|
||
return 0
|
||
fi
|
||
sleep 1
|
||
count=$((count + 1))
|
||
if [ -n "$pid" ] && ! kill -0 "$pid" 2>/dev/null; then
|
||
log_error "cost-api process died while waiting for it to be ready"
|
||
log_error "Check logs: tail -f $COST_API_LOG_FILE"
|
||
rm -f "$COST_API_PID_FILE"
|
||
return 1
|
||
fi
|
||
done
|
||
log_error "cost-api failed to become ready after ${max_wait}s"
|
||
log_error "Check logs: tail -f $COST_API_LOG_FILE"
|
||
return 1
|
||
}
|
||
|
||
start_cost_api_local() {
|
||
if ! cost_api_enabled; then
|
||
return 0
|
||
fi
|
||
# Optional service — a missing uv must not abort the whole dev-runner.
|
||
if ! command -v uv >/dev/null 2>&1; then
|
||
log_warning "uv not found; skipping cost-api startup (AI Spend pages will not load)"
|
||
return 1
|
||
fi
|
||
|
||
if [ ! -d "$AI_COST_BACKEND_PATH" ]; then
|
||
log_warning "AI_COST_BACKEND_PATH points to a non-existent directory: $AI_COST_BACKEND_PATH"
|
||
log_warning "Skipping cost-api startup; AI Spend pages will not load"
|
||
return 1
|
||
fi
|
||
if [ ! -f "$AI_COST_BACKEND_PATH/pyproject.toml" ]; then
|
||
log_warning "AI_COST_BACKEND_PATH has no pyproject.toml: $AI_COST_BACKEND_PATH"
|
||
log_warning "Skipping cost-api startup; AI Spend pages will not load"
|
||
return 1
|
||
fi
|
||
|
||
log_info "Starting cost-api (uv) from $AI_COST_BACKEND_PATH..."
|
||
|
||
# Reuse a healthy cost-api started outside this dev-runner. Avoids a port
|
||
# conflict and lets devs share one instance across opik worktrees.
|
||
# Skip reuse in EM mode: /cost-api/ping is a bare liveness probe and can't
|
||
# tell whether the running instance has AUTH_ENABLED=true, so a reused
|
||
# OSS-mode instance would fail org-scoped /ai-spend/* requests. Start our own.
|
||
if cost_api_healthy; then
|
||
if em_stack_enabled; then
|
||
log_warning "cost-api already healthy on port ${AI_COST_BACKEND_PORT}, but EM mode needs AUTH_ENABLED=true — not reusing it (start our own; will warn if the port is taken)"
|
||
else
|
||
log_success "cost-api is already healthy on port ${AI_COST_BACKEND_PORT} — reusing existing instance"
|
||
# Clear stale state so a later --stop won't kill the reused instance.
|
||
rm -f "$COST_API_PID_FILE" "$COST_API_REPO_PATH_FILE"
|
||
return 0
|
||
fi
|
||
fi
|
||
|
||
if [ -f "$COST_API_PID_FILE" ]; then
|
||
local existing_pid
|
||
existing_pid=$(cat "$COST_API_PID_FILE")
|
||
if kill -0 "$existing_pid" 2>/dev/null; then
|
||
log_warning "cost-api is already running (PID: $existing_pid)"
|
||
return 0
|
||
fi
|
||
log_warning "Removing stale cost-api PID file (process $existing_pid no longer exists)"
|
||
rm -f "$COST_API_PID_FILE"
|
||
fi
|
||
|
||
# Point cost-api at the dev-runner's ClickHouse + MySQL (host-published
|
||
# ports, opik/opik). In platform (EM) mode, cost-api targets local comet-backend
|
||
# (the React service on EM_BACKEND_PORT) and delegates auth to it (cookie /
|
||
# API-key), so org-scoped endpoints resolve the real org. In OSS mode there is
|
||
# no platform to delegate to, so keep auth off and pin to the default workspace.
|
||
local cost_api_auth="false"
|
||
if em_stack_enabled; then
|
||
export PLATFORM_BASE_URL="http://localhost:${EM_BACKEND_PORT}"
|
||
cost_api_auth="true"
|
||
fi
|
||
(
|
||
cd "$AI_COST_BACKEND_PATH" || exit 1
|
||
AUTH_ENABLED="$cost_api_auth" \
|
||
CLICKHOUSE_HOST=localhost CLICKHOUSE_PORT="$CLICKHOUSE_HTTP_PORT" \
|
||
CLICKHOUSE_DATABASE=opik CLICKHOUSE_USER=opik CLICKHOUSE_PASSWORD=opik \
|
||
MYSQL_HOST=localhost MYSQL_PORT="$MYSQL_PORT" \
|
||
MYSQL_DATABASE=opik MYSQL_USER=opik MYSQL_PASSWORD=opik \
|
||
PORT="$AI_COST_BACKEND_PORT" \
|
||
nohup uv run uvicorn cost_api.main:app --host 0.0.0.0 --port "$AI_COST_BACKEND_PORT" \
|
||
> "$COST_API_LOG_FILE" 2>&1 &
|
||
echo $! > "$COST_API_PID_FILE"
|
||
)
|
||
printf '%s\n' "$AI_COST_BACKEND_PATH" > "$COST_API_REPO_PATH_FILE"
|
||
|
||
local cost_api_pid
|
||
cost_api_pid=$(cat "$COST_API_PID_FILE")
|
||
log_debug "cost-api process started with PID: $cost_api_pid"
|
||
|
||
sleep 2
|
||
if ! kill -0 "$cost_api_pid" 2>/dev/null; then
|
||
if grep -q "address already in use" "$COST_API_LOG_FILE" 2>/dev/null; then
|
||
log_warning "cost-api could not bind port ${AI_COST_BACKEND_PORT} (already in use)."
|
||
log_warning "Set AI_COST_BACKEND_PORT to a free port, or stop whatever holds it."
|
||
else
|
||
log_warning "cost-api failed to start. Check logs: cat $COST_API_LOG_FILE"
|
||
fi
|
||
log_warning "Continuing without it; AI Spend pages will not load"
|
||
rm -f "$COST_API_PID_FILE" "$COST_API_REPO_PATH_FILE"
|
||
return 1
|
||
fi
|
||
|
||
log_success "cost-api process started (PID: $cost_api_pid)"
|
||
log_info "cost-api logs: tail -f $COST_API_LOG_FILE"
|
||
if ! wait_for_cost_api_ready "$cost_api_pid"; then
|
||
log_warning "cost-api did not become ready in time; continuing without it"
|
||
stop_cost_api_local >/dev/null 2>&1 || true
|
||
return 1
|
||
fi
|
||
return 0
|
||
}
|
||
|
||
stop_cost_api_local() {
|
||
if [ ! -f "$COST_API_PID_FILE" ] && [ ! -f "$COST_API_REPO_PATH_FILE" ]; then
|
||
# Nothing to do — quiet skip so OSS / non-cost-api runs don't see noise.
|
||
return 0
|
||
fi
|
||
|
||
if [ -f "$COST_API_PID_FILE" ]; then
|
||
local cost_api_pid
|
||
cost_api_pid=$(cat "$COST_API_PID_FILE")
|
||
if kill -0 "$cost_api_pid" 2>/dev/null; then
|
||
log_info "Stopping cost-api (PID: $cost_api_pid)..."
|
||
# `uv run` spawns the uvicorn worker as a child; snapshot the
|
||
# descendant tree before killing the parent so we can chase it down.
|
||
local descendants
|
||
descendants=$(get_descendants "$cost_api_pid")
|
||
kill -TERM "$cost_api_pid" 2>/dev/null || true
|
||
for p in $descendants; do kill -TERM "$p" 2>/dev/null || true; done
|
||
for _ in {1..10}; do
|
||
kill -0 "$cost_api_pid" 2>/dev/null || break
|
||
sleep 1
|
||
done
|
||
if kill -0 "$cost_api_pid" 2>/dev/null; then
|
||
log_warning "Force killing cost-api..."
|
||
kill -9 "$cost_api_pid" 2>/dev/null || true
|
||
fi
|
||
for p in $descendants; do kill -9 "$p" 2>/dev/null || true; done
|
||
else
|
||
log_warning "cost-api PID file exists but process is not running (cleaning up stale PID file)"
|
||
fi
|
||
fi
|
||
|
||
rm -f "$COST_API_PID_FILE" "$COST_API_REPO_PATH_FILE"
|
||
log_success "cost-api stopped"
|
||
}
|
||
|
||
display_cost_api_process_status() {
|
||
if [ -f "$COST_API_PID_FILE" ] && kill -0 "$(cat "$COST_API_PID_FILE")" 2>/dev/null; then
|
||
echo -e "cost-api: ${GREEN}RUNNING${NC} (PID: $(cat "$COST_API_PID_FILE"))"
|
||
return 0
|
||
fi
|
||
if cost_api_healthy; then
|
||
echo -e "cost-api: ${GREEN}RUNNING${NC} (reused external instance on port ${AI_COST_BACKEND_PORT})"
|
||
return 0
|
||
fi
|
||
echo -e "cost-api: ${RED}STOPPED${NC}"
|
||
return 1
|
||
}
|
||
|
||
# Function to start backend
|
||
start_backend() {
|
||
require_command java
|
||
log_info "Starting backend on port ${BACKEND_PORT}..."
|
||
log_debug "Backend directory: $BACKEND_DIR"
|
||
cd "$BACKEND_DIR" || { log_error "Backend directory not found"; exit 1; }
|
||
|
||
# Check if backend is already running
|
||
if [ -f "$BACKEND_PID_FILE" ]; then
|
||
BACKEND_PID=$(cat "$BACKEND_PID_FILE")
|
||
if kill -0 "$BACKEND_PID" 2>/dev/null; then
|
||
log_warning "Backend is already running (PID: $BACKEND_PID)"
|
||
return 0
|
||
else
|
||
log_warning "Removing stale backend PID file (process $BACKEND_PID no longer exists)"
|
||
rm -f "$BACKEND_PID_FILE"
|
||
fi
|
||
fi
|
||
|
||
# Set environment variables
|
||
export CORS=true
|
||
|
||
# Set worktree-specific ports for backend to connect to infrastructure
|
||
export SERVER_APPLICATION_PORT="$BACKEND_PORT"
|
||
export SERVER_ADMIN_PORT="$BACKEND_ADMIN_PORT"
|
||
export STATE_DB_URL="localhost:${MYSQL_PORT}/opik?createDatabaseIfNotExist=true&rewriteBatchedStatements=true&connectionTimeZone=UTC&forceConnectionTimeZoneToSession=true"
|
||
export REDIS_URL="${REDIS_URL:-redis://:opik@localhost:${REDIS_PORT}/0}"
|
||
export ANALYTICS_DB_PORT="${CLICKHOUSE_HTTP_PORT}"
|
||
|
||
# Agent Insights (read-only freeform SQL) — opt-in for local dev. When enabled, provision the restricted
|
||
# read-only ClickHouse user (via the shared script also used by the docker-compose backend container) and export
|
||
# its credentials so the locally-launched backend connects as that user. Default off: behavior unchanged. The
|
||
# script runs against the docker-compose ClickHouse on localhost:${ANALYTICS_DB_PORT} exported just above.
|
||
if [ "${TOGGLE_AGENT_INSIGHTS_ENABLED}" = "true" ]; then
|
||
export ANALYTICS_DB_READ_ONLY_FREEFORM_SQL_USER="${ANALYTICS_DB_READ_ONLY_FREEFORM_SQL_USER:-comet_readonly_freeform_sql_user}"
|
||
export ANALYTICS_DB_READ_ONLY_FREEFORM_SQL_PASS="${ANALYTICS_DB_READ_ONLY_FREEFORM_SQL_PASS:-opik}"
|
||
bash "$BACKEND_DIR/provision_agent_insights_readonly_user.sh"
|
||
log_debug " TOGGLE_AGENT_INSIGHTS_ENABLED=true (read-only CH user: ${ANALYTICS_DB_READ_ONLY_FREEFORM_SQL_USER})"
|
||
fi
|
||
|
||
log_debug "Backend configured with:"
|
||
log_debug " SERVER_APPLICATION_PORT=$SERVER_APPLICATION_PORT"
|
||
log_debug " SERVER_ADMIN_PORT=$SERVER_ADMIN_PORT"
|
||
log_debug " STATE_DB_URL=$STATE_DB_URL"
|
||
log_debug " REDIS_URL=$REDIS_URL"
|
||
log_debug " ANALYTICS_DB_PORT=$ANALYTICS_DB_PORT"
|
||
|
||
# Set debug logging if debug mode is enabled
|
||
if [ "$DEBUG_MODE" = "true" ]; then
|
||
export GENERAL_LOG_LEVEL="DEBUG"
|
||
export OPIK_LOG_LEVEL="DEBUG"
|
||
log_debug "Debug logging enabled - GENERAL_LOG_LEVEL=DEBUG, OPIK_LOG_LEVEL=DEBUG"
|
||
fi
|
||
|
||
# Find and validate the JAR file
|
||
if ! find_jar_files; then
|
||
log_warning "No backend JAR file found in target/. Building backend automatically..."
|
||
build_backend
|
||
|
||
# Re-scan for JAR files after build
|
||
if ! find_jar_files; then
|
||
log_error "Backend build completed but no JAR file found. Build may have failed."
|
||
exit 1
|
||
fi
|
||
fi
|
||
|
||
log_debug "Starting backend with JAR: $JAR_FILE"
|
||
log_debug "Command: java -jar $JAR_FILE server config.yml"
|
||
|
||
# Start backend in background using the JAR file
|
||
nohup java -jar "$JAR_FILE" \
|
||
server config.yml \
|
||
> "$BACKEND_LOG_FILE" 2>&1 &
|
||
|
||
BACKEND_PID=$!
|
||
echo "$BACKEND_PID" > "$BACKEND_PID_FILE"
|
||
|
||
log_debug "Backend process started with PID: $BACKEND_PID"
|
||
|
||
# Wait a bit and check if process is still running
|
||
sleep 3
|
||
if kill -0 "$BACKEND_PID" 2>/dev/null; then
|
||
log_success "Backend process started (PID: $BACKEND_PID)"
|
||
log_info "Backend logs: tail -f $BACKEND_LOG_FILE"
|
||
|
||
if ! wait_for_backend_ready; then
|
||
exit 1
|
||
fi
|
||
else
|
||
log_error "Backend failed to start. Check logs: cat $BACKEND_LOG_FILE"
|
||
rm -f "$BACKEND_PID_FILE"
|
||
exit 1
|
||
fi
|
||
}
|
||
|
||
# Symlink the sibling private ai-spend plugin's source into the FE plugins
|
||
# folder (gitignored) so it compiles as part of opik. Removes the link when no
|
||
# sibling is present, so toggling the checkout is clean.
|
||
sync_ai_spend_plugin_link() {
|
||
if [ -n "${AI_SPEND_PLUGIN_PATH:-}" ] && [ -d "${AI_SPEND_PLUGIN_PATH}/src" ]; then
|
||
ln -sfn "${AI_SPEND_PLUGIN_PATH}/src" "$AI_SPEND_PLUGIN_LINK"
|
||
log_info "Linked ai-spend plugin: $AI_SPEND_PLUGIN_LINK -> ${AI_SPEND_PLUGIN_PATH}/src"
|
||
elif [ -L "$AI_SPEND_PLUGIN_LINK" ]; then
|
||
rm -f "$AI_SPEND_PLUGIN_LINK"
|
||
log_info "Removed ai-spend plugin link (no sibling checkout)"
|
||
fi
|
||
}
|
||
|
||
# Function to start frontend
|
||
start_frontend() {
|
||
require_command npm
|
||
log_info "Starting frontend on port ${FRONTEND_PORT}..."
|
||
log_debug "Frontend directory: $FRONTEND_DIR"
|
||
cd "$FRONTEND_DIR" || { log_error "Frontend directory not found"; exit 1; }
|
||
|
||
# Check if frontend is already running
|
||
if [ -f "$FRONTEND_PID_FILE" ]; then
|
||
FRONTEND_PID=$(cat "$FRONTEND_PID_FILE")
|
||
if kill -0 "$FRONTEND_PID" 2>/dev/null; then
|
||
log_warning "Frontend is already running (PID: $FRONTEND_PID)"
|
||
return 0
|
||
else
|
||
log_warning "Removing stale frontend PID file (process $FRONTEND_PID no longer exists)"
|
||
rm -f "$FRONTEND_PID_FILE"
|
||
fi
|
||
fi
|
||
|
||
# Set debug logging for frontend if debug mode is enabled
|
||
if [ "$DEBUG_MODE" = "true" ]; then
|
||
export NODE_ENV="development"
|
||
log_debug "Frontend debug mode enabled - NODE_ENV=development"
|
||
fi
|
||
|
||
# Set worktree-specific ports for frontend
|
||
export VITE_DEV_PORT="$FRONTEND_PORT"
|
||
export VITE_BACKEND_PORT="$BACKEND_PORT"
|
||
|
||
log_debug "Frontend configured with:"
|
||
log_debug " VITE_DEV_PORT=$VITE_DEV_PORT"
|
||
log_debug " VITE_BACKEND_PORT=$VITE_BACKEND_PORT"
|
||
|
||
# Configure frontend API base URL (defaults to /api in frontend code if not set)
|
||
# The Vite dev server proxy will forward /api/* requests to the backend
|
||
if [ -z "${VITE_BASE_API_URL:-}" ]; then
|
||
log_debug "Frontend API base URL (VITE_BASE_API_URL) not set, will use default from frontend code: /api"
|
||
else
|
||
log_info "Frontend API base URL (VITE_BASE_API_URL) set to: $VITE_BASE_API_URL"
|
||
fi
|
||
|
||
# Enable assistant sidebar only when ollie is actually serving requests.
|
||
# ollie_healthy covers both ollie-we-spawned and ollie-already-running
|
||
# (reused instances). ollie_running alone would miss the reuse case.
|
||
# Always unset first so a stale value from the parent shell / CI / a
|
||
# previous dev-runner invocation cannot leak into the FE process and
|
||
# silently re-enable the sidebar gate.
|
||
unset VITE_ASSISTANT_SIDEBAR_BASE_URL
|
||
if ollie_enabled && ollie_healthy; then
|
||
export VITE_ASSISTANT_SIDEBAR_BASE_URL="http://localhost:${OLLIE_CONSOLE_PORT}"
|
||
log_debug " VITE_ASSISTANT_SIDEBAR_BASE_URL=$VITE_ASSISTANT_SIDEBAR_BASE_URL"
|
||
elif ollie_enabled; then
|
||
log_warning "OLLIE_REPO_PATH is set but ollie healthz is not responding; sidebar disabled"
|
||
fi
|
||
|
||
# Link the private ai-spend plugin (if checked out) and activate it
|
||
# alongside the local development plugin.
|
||
sync_ai_spend_plugin_link
|
||
if [ -L "$AI_SPEND_PLUGIN_LINK" ]; then
|
||
export VITE_FE_PLUGINS="development,ai-spend"
|
||
log_info " VITE_FE_PLUGINS=$VITE_FE_PLUGINS"
|
||
fi
|
||
|
||
# Start the local cost-api (if a sibling checkout exists) and point the FE's
|
||
# ai-spend proxy at it ONLY once it is actually serving. If startup failed
|
||
# (e.g. port conflict), leave VITE_AI_COST_BACKEND_PORT unset so vite.config
|
||
# falls back to its default instead of proxying AI Spend to a dead port.
|
||
start_cost_api_local || log_warning "cost-api startup failed; AI Spend pages will not load"
|
||
if cost_api_healthy; then
|
||
export VITE_AI_COST_BACKEND_PORT="${AI_COST_BACKEND_PORT}"
|
||
log_info " VITE_AI_COST_BACKEND_PORT=$VITE_AI_COST_BACKEND_PORT"
|
||
fi
|
||
|
||
# Opik FE runs in comet mode when the platform stack is enabled; see
|
||
# em_prepare_opik_comet_env / em_opik_vite_args in dev-runner-em.sh
|
||
# (em_opik_vite_args is empty when the platform stack is off).
|
||
log_debug "Starting frontend with: npm run start"
|
||
em_prepare_opik_comet_env
|
||
CI=true nohup npm run start $(em_opik_vite_args) > "$FRONTEND_LOG_FILE" 2>&1 &
|
||
FRONTEND_PID=$!
|
||
echo "$FRONTEND_PID" > "$FRONTEND_PID_FILE"
|
||
|
||
log_debug "Frontend process started with PID: $FRONTEND_PID"
|
||
|
||
# Wait a bit and check if process is still running
|
||
sleep 3
|
||
if kill -0 "$FRONTEND_PID" 2>/dev/null; then
|
||
log_success "Frontend started successfully (PID: $FRONTEND_PID)"
|
||
log_info "Frontend available at: ${GREEN}http://localhost:${FRONTEND_PORT}${NC}"
|
||
log_info "Frontend logs: tail -f $FRONTEND_LOG_FILE"
|
||
else
|
||
log_error "Frontend failed to start. Check logs: cat $FRONTEND_LOG_FILE"
|
||
rm -f "$FRONTEND_PID_FILE"
|
||
exit 1
|
||
fi
|
||
}
|
||
|
||
# Function to stop backend
|
||
stop_backend() {
|
||
if [ -f "$BACKEND_PID_FILE" ]; then
|
||
BACKEND_PID=$(cat "$BACKEND_PID_FILE")
|
||
if kill -0 "$BACKEND_PID" 2>/dev/null; then
|
||
log_info "Stopping backend (PID: $BACKEND_PID)..."
|
||
kill "$BACKEND_PID"
|
||
|
||
# Wait for graceful shutdown
|
||
for _ in {1..10}; do
|
||
if ! kill -0 "$BACKEND_PID" 2>/dev/null; then
|
||
break
|
||
fi
|
||
sleep 1
|
||
done
|
||
|
||
# Force kill if still running
|
||
if kill -0 "$BACKEND_PID" 2>/dev/null; then
|
||
log_warning "Force killing backend..."
|
||
kill -9 "$BACKEND_PID"
|
||
fi
|
||
|
||
log_success "Backend stopped"
|
||
else
|
||
log_warning "Backend PID file exists but process is not running (cleaning up stale PID file)"
|
||
fi
|
||
rm -f "$BACKEND_PID_FILE"
|
||
else
|
||
log_warning "Backend is not running"
|
||
fi
|
||
}
|
||
|
||
# Function to stop frontend
|
||
stop_frontend() {
|
||
if [ -f "$FRONTEND_PID_FILE" ]; then
|
||
FRONTEND_PID=$(cat "$FRONTEND_PID_FILE")
|
||
if kill -0 "$FRONTEND_PID" 2>/dev/null; then
|
||
log_info "Stopping frontend (PID: $FRONTEND_PID)..."
|
||
|
||
# First try to kill just the main process
|
||
kill -TERM "$FRONTEND_PID" 2>/dev/null
|
||
|
||
# Wait for graceful shutdown
|
||
for _ in {1..10}; do
|
||
if ! kill -0 "$FRONTEND_PID" 2>/dev/null; then
|
||
break
|
||
fi
|
||
sleep 1
|
||
done
|
||
|
||
# Force kill if still running (kill main process and find children)
|
||
if kill -0 "$FRONTEND_PID" 2>/dev/null; then
|
||
log_warning "Force killing frontend..."
|
||
kill -9 "$FRONTEND_PID" 2>/dev/null
|
||
|
||
# Also kill any child processes that may still be running
|
||
CHILD_PIDS=$(pgrep -P "$FRONTEND_PID" 2>/dev/null || true)
|
||
if [ -n "$CHILD_PIDS" ]; then
|
||
log_warning "Killing remaining child processes (PIDs: $CHILD_PIDS)..."
|
||
for PID in $CHILD_PIDS; do
|
||
kill -9 "$PID" 2>/dev/null || true
|
||
done
|
||
fi
|
||
fi
|
||
|
||
log_success "Frontend stopped"
|
||
else
|
||
log_warning "Frontend PID file exists but process is not running (cleaning up stale PID file)"
|
||
fi
|
||
rm -f "$FRONTEND_PID_FILE"
|
||
else
|
||
log_warning "Frontend is not running"
|
||
fi
|
||
|
||
# Clean up any orphaned processes by looking for processes with our frontend directory path
|
||
# This is safe and compatible across Unix systems
|
||
ORPHANED_PIDS=$(pgrep -f "$FRONTEND_DIR" 2>/dev/null || true)
|
||
|
||
if [ -n "$ORPHANED_PIDS" ]; then
|
||
for PID in $ORPHANED_PIDS; do
|
||
if [ -n "$PID" ] && kill -0 "$PID" 2>/dev/null; then
|
||
# Get process info to verify it's actually our frontend process
|
||
PROCESS_INFO=$(ps -p "$PID" -o comm,args --no-headers 2>/dev/null || true)
|
||
|
||
# Only kill if it's an npm/node/vite process AND contains our directory path
|
||
if [[ "$PROCESS_INFO" =~ (npm|node|vite) ]] && [[ "$PROCESS_INFO" =~ $FRONTEND_DIR ]]; then
|
||
log_warning "Cleaning up orphaned process: PID $PID - $PROCESS_INFO"
|
||
kill -TERM "$PID" 2>/dev/null || true
|
||
sleep 1
|
||
# Force kill if still running
|
||
if kill -0 "$PID" 2>/dev/null; then
|
||
kill -9 "$PID" 2>/dev/null || true
|
||
fi
|
||
fi
|
||
fi
|
||
done
|
||
fi
|
||
}
|
||
|
||
# Helper function to display backend process status
|
||
# Returns: 0 if running, 1 if stopped
|
||
display_backend_process_status() {
|
||
if [ -f "$BACKEND_PID_FILE" ] && kill -0 "$(cat "$BACKEND_PID_FILE")" 2>/dev/null; then
|
||
echo -e "Backend: ${GREEN}RUNNING${NC} (PID: $(cat "$BACKEND_PID_FILE"))"
|
||
return 0
|
||
else
|
||
echo -e "Backend: ${RED}STOPPED${NC}"
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
# Helper function to display access information
|
||
# Args: $1 = UI URL (e.g., "http://localhost:5174" or "http://localhost:5173")
|
||
# $2 = show manual edit warning (true/false)
|
||
show_access_information() {
|
||
local ui_url="$1"
|
||
local show_manual_edit="${2:-true}"
|
||
|
||
echo ""
|
||
echo -e "${GREEN}🚀 Opik Development Environment is Ready!${NC}"
|
||
echo -e "${BLUE}📊 Access the UI: ${ui_url}${NC}"
|
||
echo -e "${BLUE}🛠️ API ping Endpoint: http://localhost:${BACKEND_PORT}/is-alive/ping${NC}"
|
||
echo ""
|
||
echo -e "${BLUE}ℹ️ SDK Configuration Required:${NC}"
|
||
echo -e "To use the Opik SDK with your local development environment, you MUST configure it to point to your local instance."
|
||
echo ""
|
||
echo -e "${BLUE}Run SDK Configuration Command:${NC}"
|
||
echo " opik configure --use_local"
|
||
|
||
if [ "$show_manual_edit" = true ]; then
|
||
echo " # When prompted:"
|
||
echo " # - Choose 'Local deployment' option"
|
||
echo " # - Enter URL: http://localhost:${BACKEND_PORT}"
|
||
echo ""
|
||
echo -e "${YELLOW}⚠️ IMPORTANT: Manual Configuration File Edit Required!${NC}"
|
||
echo -e "After running 'opik configure', you MUST manually edit the configuration file to remove '/api' from the URL."
|
||
echo ""
|
||
echo -e "${BLUE}Edit the configuration file:${NC}"
|
||
echo " # Open the configuration file, by default: ~/.opik.config"
|
||
echo ""
|
||
echo " # Change this line:"
|
||
echo " url_override = http://localhost:${BACKEND_PORT}/api/"
|
||
echo ""
|
||
echo " # To this (remove '/api'):"
|
||
echo " url_override = http://localhost:${BACKEND_PORT}"
|
||
else
|
||
echo " # When prompted, use URL: ${ui_url}"
|
||
fi
|
||
|
||
echo ""
|
||
echo -e "${BLUE}Alternative - Environment Variables:${NC}"
|
||
# When no manual edit is required (BE-only mode), append /api to the URL
|
||
if [ "$show_manual_edit" = true ]; then
|
||
echo " export OPIK_URL_OVERRIDE='http://localhost:${BACKEND_PORT}'"
|
||
else
|
||
echo " export OPIK_URL_OVERRIDE='${ui_url}/api'"
|
||
fi
|
||
echo " export OPIK_WORKSPACE='default'"
|
||
echo ""
|
||
echo -e "${YELLOW}Important Notes:${NC}"
|
||
|
||
echo " • The configuration file is located at ~/.opik.config by default"
|
||
|
||
if [ "$show_manual_edit" = true ]; then
|
||
echo " • You MUST remove '/api' from the URL for local development"
|
||
fi
|
||
|
||
echo " • Default workspace is 'default'"
|
||
echo " • No API key required for local instances"
|
||
echo ""
|
||
echo -e "${BLUE}📖 For complete configuration documentation, visit:${NC}"
|
||
echo -e " https://www.comet.com/docs/opik/tracing/advanced/sdk_configuration"
|
||
}
|
||
|
||
create_demo_data() {
|
||
local mode="$1"
|
||
|
||
log_info "Creating demo data..."
|
||
cd "$PROJECT_ROOT" || { log_error "Project root directory not found"; return 1; }
|
||
|
||
if ./opik.sh "$mode" --demo-data; then
|
||
log_success "Demo data created"
|
||
return 0
|
||
else
|
||
log_warning "Demo data creation failed, but services are running"
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
# Function to verify services
|
||
verify_services() {
|
||
log_info "=== Opik Development Status (Worktree: ${WORKTREE_ID}, Offset: ${PORT_OFFSET}) ==="
|
||
|
||
local docker_services_running=false
|
||
if verify_local_be_fe; then
|
||
echo -e "Docker Services: ${GREEN}RUNNING${NC}"
|
||
docker_services_running=true
|
||
else
|
||
echo -e "Docker Services: ${RED}STOPPED${NC}"
|
||
fi
|
||
|
||
# Backend process status
|
||
local backend_running=false
|
||
if display_backend_process_status; then
|
||
backend_running=true
|
||
fi
|
||
|
||
# Frontend process status
|
||
local frontend_running=false
|
||
if [ -f "$FRONTEND_PID_FILE" ] && kill -0 "$(cat "$FRONTEND_PID_FILE")" 2>/dev/null; then
|
||
echo -e "Frontend Process: ${GREEN}RUNNING${NC} (PID: $(cat "$FRONTEND_PID_FILE"))"
|
||
frontend_running=true
|
||
else
|
||
echo -e "Frontend Process: ${RED}STOPPED${NC}"
|
||
fi
|
||
|
||
# Ollie local-dev status — surface it only when the user opted in (env
|
||
# var) or a previous run left a tracked PID file. NOTE: we deliberately
|
||
# do NOT probe ollie_healthy here because that adds a 2s curl timeout
|
||
# on every --verify/--restart for OSS contributors who never touch ollie.
|
||
# `|| true` keeps a STOPPED return from aborting verify_services under
|
||
# set -e (mirrors how display_backend_process_status is called in an
|
||
# `if` condition).
|
||
if ollie_enabled || [ -f "$OLLIE_PID_FILE" ]; then
|
||
display_ollie_process_status || true
|
||
fi
|
||
if cost_api_enabled || [ -f "$COST_API_PID_FILE" ]; then
|
||
display_cost_api_process_status || true
|
||
fi
|
||
em_print_status
|
||
|
||
# Show access information if all services are running
|
||
if [ "$docker_services_running" = true ] && [ "$backend_running" = true ] && [ "$frontend_running" = true ]; then
|
||
show_access_information "http://localhost:${FRONTEND_PORT}" true
|
||
fi
|
||
|
||
echo ""
|
||
echo "Logs:"
|
||
echo " Backend Process: tail -f $BACKEND_LOG_FILE"
|
||
echo " Frontend Process: tail -f $FRONTEND_LOG_FILE"
|
||
if ollie_enabled || [ -f "$OLLIE_LOG_FILE" ]; then
|
||
echo " Ollie Process: tail -f $OLLIE_LOG_FILE"
|
||
fi
|
||
if cost_api_enabled || [ -f "$COST_API_LOG_FILE" ]; then
|
||
echo " cost-api Process: tail -f $COST_API_LOG_FILE"
|
||
fi
|
||
em_print_logs
|
||
}
|
||
|
||
# Function to verify BE-only services
|
||
verify_be_only_services() {
|
||
log_info "=== Opik BE-Only Development Status (Worktree: ${WORKTREE_ID}, Offset: ${PORT_OFFSET}) ==="
|
||
|
||
local docker_services_running=false
|
||
if verify_local_be; then
|
||
echo -e "Docker Services: ${GREEN}RUNNING${NC}"
|
||
docker_services_running=true
|
||
else
|
||
echo -e "Docker Services: ${RED}STOPPED${NC}"
|
||
fi
|
||
|
||
# Backend process status
|
||
local backend_running=false
|
||
if display_backend_process_status; then
|
||
backend_running=true
|
||
fi
|
||
|
||
# Show access information if all services are running
|
||
if [ "$docker_services_running" = true ] && [ "$backend_running" = true ]; then
|
||
# In BE-only mode, frontend runs in Docker on standard port 5173
|
||
show_access_information "http://localhost:5173" false
|
||
fi
|
||
|
||
echo ""
|
||
echo "Logs:"
|
||
echo " Backend Process: tail -f $BACKEND_LOG_FILE"
|
||
echo " Frontend: docker logs -f ${RESOURCE_PREFIX}-frontend-1"
|
||
}
|
||
|
||
# Function to start services (without building)
|
||
start_services() {
|
||
log_info "=== Starting Opik Development Environment (Worktree: ${WORKTREE_ID}) ==="
|
||
log_warning "=== Not rebuilding: the latest local changes may not be reflected ==="
|
||
|
||
# Check for port collisions before starting
|
||
if ! check_port_collisions; then
|
||
exit 1
|
||
fi
|
||
|
||
log_info "Step 1/6: Starting Docker services..."
|
||
start_local_be_fe
|
||
log_info "Step 2/6: Running DB migrations..."
|
||
run_db_migrations
|
||
log_info "Step 3/6: Starting backend process..."
|
||
# Platform auth for the Opik backend — full FE+BE flows only (BE-only can't host
|
||
# the EM stack). Set before start_backend so its JVM inherits it. See dev-runner-em.sh.
|
||
em_prepare_opik_backend_auth_env
|
||
start_backend
|
||
log_info "Step 4/6: Starting ollie-assist (optional)..."
|
||
start_ollie_local || log_warning "ollie-assist startup failed; continuing without sidebar"
|
||
log_info "Step 5/6: Starting frontend process..."
|
||
start_frontend
|
||
log_info "Step 6/6: Creating demo data..."
|
||
create_demo_data "--local-be-fe"
|
||
# Comet EM stack (comet-backend + comet-react), gated on PLATFORM_ENABLED.
|
||
start_em_stack
|
||
log_success "=== Start Complete ==="
|
||
verify_services
|
||
}
|
||
|
||
# Function to stop services
|
||
stop_services() {
|
||
log_info "=== Stopping Opik Development Environment ==="
|
||
log_info "Step 1/4: Stopping frontend..."
|
||
stop_frontend
|
||
# Comet EM stack first — it depends on Opik's infra (MySQL/Redis/MinIO).
|
||
stop_em_stack
|
||
log_info "Step 2/4: Stopping ollie-assist (if running)..."
|
||
stop_ollie_local
|
||
stop_cost_api_local
|
||
log_info "Step 3/4: Stopping backend..."
|
||
stop_backend
|
||
log_info "Step 4/4: Stopping Docker services..."
|
||
stop_local_be_fe
|
||
log_success "=== Stop Complete ==="
|
||
}
|
||
|
||
# Function to run migrations
|
||
migrate_services() {
|
||
log_info "=== Running Database Migrations ==="
|
||
log_info "Step 1/3: Starting Docker services..."
|
||
start_local_be_fe
|
||
log_info "Step 2/3: Building backend..."
|
||
build_backend
|
||
log_info "Step 3/3: Running DB migrations..."
|
||
run_db_migrations
|
||
log_success "=== Migrations Complete ==="
|
||
}
|
||
|
||
# Function to restart services (stop, build, start)
|
||
restart_services() {
|
||
log_info "=== Restarting Opik Development Environment (Worktree: ${WORKTREE_ID}) ==="
|
||
log_info "Step 1/12: Stopping frontend process..."
|
||
stop_frontend
|
||
# Comet EM stack first — it depends on Opik's infra (MySQL/Redis/MinIO).
|
||
stop_em_stack
|
||
log_info "Step 2/12: Stopping ollie-assist (if running)..."
|
||
stop_ollie_local
|
||
stop_cost_api_local
|
||
log_info "Step 3/12: Stopping backend process..."
|
||
stop_backend
|
||
log_info "Step 4/12: Stopping Docker services..."
|
||
stop_local_be_fe
|
||
log_info "Step 5/12: Starting Docker services..."
|
||
start_local_be_fe
|
||
log_info "Step 6/12: Building backend..."
|
||
build_backend
|
||
log_info "Step 7/12: Building frontend..."
|
||
build_frontend
|
||
em_restart_build
|
||
log_info "Step 8/12: Running DB migrations..."
|
||
run_db_migrations
|
||
log_info "Step 9/12: Starting backend process..."
|
||
em_prepare_opik_backend_auth_env
|
||
start_backend
|
||
log_info "Step 10/12: Starting ollie-assist (optional)..."
|
||
start_ollie_local || log_warning "ollie-assist startup failed; continuing without sidebar"
|
||
log_info "Step 11/12: Starting frontend process..."
|
||
start_frontend
|
||
log_info "Step 12/12: Creating demo data..."
|
||
create_demo_data "--local-be-fe"
|
||
# Comet EM stack (comet-backend + comet-react), gated on PLATFORM_ENABLED.
|
||
start_em_stack
|
||
log_success "=== Restart Complete ==="
|
||
verify_services
|
||
}
|
||
|
||
# Function for quick restart (only rebuild backend, keep infrastructure running)
|
||
quick_restart_services() {
|
||
log_info "=== Quick Restart (Backend Only) ==="
|
||
|
||
# Check if infrastructure is running, start it if not
|
||
log_info "Step 1/7: Checking Docker infrastructure..."
|
||
if verify_local_be_fe; then
|
||
log_success "Docker infrastructure is already running"
|
||
else
|
||
log_warning "Docker infrastructure is not running, starting it..."
|
||
start_local_be_fe
|
||
log_info "Running DB migrations..."
|
||
run_db_migrations
|
||
fi
|
||
|
||
log_info "Step 2/8: Stopping frontend..."
|
||
stop_frontend
|
||
log_info "Step 3/8: Stopping backend..."
|
||
stop_backend
|
||
log_info "Step 4/8: Building backend..."
|
||
build_backend
|
||
log_info "Step 5/8: Starting backend..."
|
||
em_prepare_opik_backend_auth_env
|
||
start_backend
|
||
|
||
log_info "Step 6/8: Ensuring ollie-assist is running (optional)..."
|
||
if ollie_enabled && ollie_running; then
|
||
log_success "ollie-assist is already running (PID: $(cat "$OLLIE_PID_FILE"))"
|
||
elif ollie_enabled && ollie_healthy; then
|
||
log_success "ollie-assist is healthy on port ${OLLIE_API_PORT} (reused external instance)"
|
||
else
|
||
start_ollie_local || log_warning "ollie-assist startup failed; continuing without sidebar"
|
||
fi
|
||
|
||
# Check if package.json has changed since last npm install
|
||
log_info "Step 7/8: Checking frontend dependencies..."
|
||
local package_json="$FRONTEND_DIR/package.json"
|
||
local package_lock="$FRONTEND_DIR/package-lock.json"
|
||
local node_modules="$FRONTEND_DIR/node_modules"
|
||
|
||
local needs_install=false
|
||
|
||
if [ ! -d "$node_modules" ]; then
|
||
log_info "node_modules not found, will install dependencies"
|
||
needs_install=true
|
||
elif [ ! -f "$package_lock" ]; then
|
||
log_info "package-lock.json not found, will install dependencies"
|
||
needs_install=true
|
||
elif [ "$package_json" -nt "$package_lock" ]; then
|
||
log_info "package.json is newer than package-lock.json, will install dependencies"
|
||
needs_install=true
|
||
else
|
||
log_info "Frontend dependencies are up to date, skipping npm install"
|
||
fi
|
||
|
||
if [ "$needs_install" = true ]; then
|
||
build_frontend
|
||
fi
|
||
|
||
log_info "Step 8/8: Starting frontend..."
|
||
start_frontend
|
||
# Ensure the Comet EM stack is up (gated). Builds the jar only if missing;
|
||
# use --restart or --platform-build to force a comet-backend rebuild.
|
||
start_em_stack
|
||
log_success "=== Quick Restart Complete ==="
|
||
verify_services
|
||
}
|
||
|
||
# Function to start BE-only services (without building)
|
||
start_be_only_services() {
|
||
log_info "=== Starting Opik BE-Only Development Environment (Worktree: ${WORKTREE_ID}) ==="
|
||
log_warning "=== Not rebuilding: the latest local changes may not be reflected ==="
|
||
|
||
# Check for port collisions before starting
|
||
if ! check_port_collisions; then
|
||
exit 1
|
||
fi
|
||
|
||
log_info "Step 1/4: Starting Docker services..."
|
||
start_local_be
|
||
log_info "Step 2/4: Running DB migrations..."
|
||
run_db_migrations
|
||
log_info "Step 3/4: Starting backend process..."
|
||
start_backend
|
||
log_info "Step 4/4: Creating demo data..."
|
||
create_demo_data "--local-be"
|
||
log_success "=== BE-Only Start Complete ==="
|
||
verify_be_only_services
|
||
}
|
||
|
||
# Function to stop BE-only services
|
||
stop_be_only_services() {
|
||
log_info "=== Stopping Opik BE-Only Development Environment ==="
|
||
log_info "Step 1/2: Stopping backend process..."
|
||
stop_backend
|
||
log_info "Step 2/2: Stopping Docker services..."
|
||
stop_local_be
|
||
log_success "=== BE-Only Stop Complete ==="
|
||
}
|
||
|
||
# Function to restart BE-only services (stop, build, start)
|
||
restart_be_only_services() {
|
||
log_info "=== Restarting Opik BE-Only Development Environment (Worktree: ${WORKTREE_ID}) ==="
|
||
log_info "Step 1/7: Stopping backend process..."
|
||
stop_backend
|
||
log_info "Step 2/7: Stopping Docker services..."
|
||
stop_local_be
|
||
log_info "Step 3/7: Starting Docker services..."
|
||
start_local_be
|
||
log_info "Step 4/7: Building backend..."
|
||
build_backend
|
||
log_info "Step 5/7: Running DB migrations..."
|
||
run_db_migrations
|
||
log_info "Step 6/7: Starting backend process..."
|
||
start_backend
|
||
log_info "Step 7/7: Creating demo data..."
|
||
create_demo_data "--local-be"
|
||
log_success "=== BE-Only Restart Complete ==="
|
||
verify_be_only_services
|
||
}
|
||
|
||
# Function to show usage
|
||
show_usage() {
|
||
echo "Usage: $0 [OPTIONS]"
|
||
echo ""
|
||
echo "Standard Mode (BE and FE services as processes):"
|
||
echo " --start - Start Docker infrastructure, and BE and FE processes (without building)"
|
||
echo " --stop - Stop Docker infrastructure, and BE and FE processes"
|
||
echo " --restart - Stop, build, and start Docker infrastructure, and BE and FE processes (DEFAULT IF NO OPTIONS PROVIDED)"
|
||
echo " --quick-restart - Quick restart: stop BE/FE, rebuild BE only, start BE/FE (keeps infrastructure running)"
|
||
echo " --verify - Verify status of Docker infrastructure, and BE and FE processes"
|
||
echo ""
|
||
echo "BE-Only Mode (BE as process, FE in Docker):"
|
||
echo " --be-only-start - Start Docker infrastructure and FE, and backend process (without building)"
|
||
echo " --be-only-stop - Stop Docker infrastructure and FE, and backend process"
|
||
echo " --be-only-restart - Stop, build, and start Docker infrastructure and FE, and backend process"
|
||
echo " --be-only-verify - Verify status of Docker infrastructure and FE, and backend process"
|
||
echo ""
|
||
echo "Other options:"
|
||
echo " --build-be - Build backend"
|
||
echo " --build-fe - Build frontend"
|
||
echo " --platform-build - Build the Comet EM stack (comet-backend jar + comet-react deps)"
|
||
echo " --migrate - Run database migrations"
|
||
echo " --lint-be - Lint backend code"
|
||
echo " --lint-fe - Lint frontend code"
|
||
echo " --debug - Enable debug mode (meant to be combined with other flags)"
|
||
echo " --logs - Show logs for backend and frontend services"
|
||
echo " --help - Show this help message"
|
||
echo ""
|
||
echo "Multi-Worktree Support:"
|
||
echo " This script supports running multiple Opik instances from different git worktrees."
|
||
echo " Each worktree gets unique ports based on a hash of its path."
|
||
echo ""
|
||
echo "Environment Variables:"
|
||
echo " DEBUG_MODE=true - Enable debug mode"
|
||
echo " OPIK_PORT_OFFSET=<n> - Override automatic port offset (0-99)"
|
||
echo " OLLIE_REPO_PATH=<p> - Opik-team only: path to a local ollie-assist checkout."
|
||
echo " Defaults to <opik-root>/../ollie-assist if that path"
|
||
echo " exists and contains a Makefile. dev-runner runs 'make"
|
||
echo " dev' in that repo and enables the assistant sidebar."
|
||
echo " Set to empty string to opt out: OLLIE_REPO_PATH="
|
||
echo " OLLIE_API_PORT=<n> - Override ollie healthcheck/API port (default: 9080)"
|
||
echo " OLLIE_CONSOLE_PORT=<n>- Override ollie console port (default: 3333)"
|
||
echo " AI_COST_BACKEND_PATH=<p> - Opik-team only: path to a local ai-cost-backend"
|
||
echo " (cost-api) checkout. Defaults to <opik-root>/../ai-cost-backend"
|
||
echo " if it exists and contains a pyproject.toml. dev-runner runs"
|
||
echo " cost-api (uv, auth disabled) against the dev ClickHouse+MySQL"
|
||
echo " and serves the AI Spend pages. Opt out: AI_COST_BACKEND_PATH="
|
||
echo " AI_COST_BACKEND_PORT=<n> - Override cost-api port (default: 8400 + worktree offset)"
|
||
em_print_usage
|
||
}
|
||
|
||
# Function to handle unknown options
|
||
handle_unknown_option() {
|
||
local option="$1"
|
||
log_error "Unknown option: $option"
|
||
echo ""
|
||
show_usage
|
||
exit 1
|
||
}
|
||
|
||
# Function to show logs
|
||
show_logs() {
|
||
log_info "=== Recent Logs (Worktree: ${WORKTREE_ID}) ==="
|
||
|
||
if [ -f "$BACKEND_LOG_FILE" ]; then
|
||
echo -e "\n${BLUE}Backend logs (last 20 lines):${NC}"
|
||
tail -20 "$BACKEND_LOG_FILE"
|
||
fi
|
||
|
||
if [ -f "$FRONTEND_LOG_FILE" ]; then
|
||
echo -e "\n${BLUE}Frontend logs (last 20 lines):${NC}"
|
||
tail -20 "$FRONTEND_LOG_FILE"
|
||
fi
|
||
|
||
echo -e "\n${BLUE}To follow logs in real-time:${NC}"
|
||
echo " Backend: tail -f $BACKEND_LOG_FILE"
|
||
echo " Frontend: tail -f $FRONTEND_LOG_FILE"
|
||
}
|
||
|
||
# Function to check if a port is in use
|
||
is_port_in_use() {
|
||
local port="$1"
|
||
if command -v lsof &>/dev/null; then
|
||
lsof -iTCP:"$port" -sTCP:LISTEN -t &>/dev/null
|
||
elif command -v netstat &>/dev/null; then
|
||
netstat -tuln 2>/dev/null | grep -q ":$port "
|
||
elif command -v ss &>/dev/null; then
|
||
ss -tuln 2>/dev/null | grep -q ":$port "
|
||
else
|
||
# Can't check, assume not in use
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
# Function to detect port collisions before starting services
|
||
check_port_collisions() {
|
||
local has_collision=false
|
||
local ports_to_check=(
|
||
"$BACKEND_PORT:Backend"
|
||
"$BACKEND_ADMIN_PORT:Backend Admin"
|
||
"$FRONTEND_PORT:Frontend"
|
||
"$MYSQL_PORT:MySQL"
|
||
"$REDIS_PORT:Redis"
|
||
"$CLICKHOUSE_HTTP_PORT:ClickHouse HTTP"
|
||
"$CLICKHOUSE_NATIVE_PORT:ClickHouse Native"
|
||
"$PYTHON_BACKEND_PORT:Python Backend"
|
||
"$ZOOKEEPER_PORT:Zookeeper"
|
||
"$MINIO_API_PORT:MinIO API"
|
||
"$MINIO_CONSOLE_PORT:MinIO Console"
|
||
)
|
||
|
||
# cost-api is optional and may legitimately reuse a healthy instance already
|
||
# listening on its port, so only flag it when enabled and not already serving.
|
||
if cost_api_enabled && ! cost_api_healthy; then
|
||
ports_to_check+=("$AI_COST_BACKEND_PORT:cost-api")
|
||
fi
|
||
|
||
# EM stack ports (empty unless PLATFORM_ENABLED); see em_collision_ports.
|
||
while IFS= read -r _em_port; do
|
||
[ -n "$_em_port" ] && ports_to_check+=("$_em_port")
|
||
done < <(em_collision_ports)
|
||
|
||
log_info "Checking for port collisions..."
|
||
|
||
for port_info in "${ports_to_check[@]}"; do
|
||
local port="${port_info%%:*}"
|
||
local service="${port_info##*:}"
|
||
|
||
if is_port_in_use "$port"; then
|
||
log_error "Port $port ($service) is already in use"
|
||
has_collision=true
|
||
fi
|
||
done
|
||
|
||
if [ "$has_collision" = true ]; then
|
||
echo ""
|
||
log_error "Port collision detected! Another process is using one or more required ports."
|
||
log_error "This might be caused by:"
|
||
log_error " - Another Opik instance running from a different worktree"
|
||
log_error " - Stale containers from a previous run"
|
||
log_error " - Other services using the same ports"
|
||
echo ""
|
||
log_info "To resolve:"
|
||
log_info " 1. Stop other Opik instances: ./scripts/dev-runner.sh --stop"
|
||
log_info " 2. Use a different port offset: export OPIK_PORT_OFFSET=<0-99>"
|
||
log_info " 3. Check running processes: lsof -i :${BACKEND_PORT}"
|
||
return 1
|
||
fi
|
||
|
||
log_success "No port collisions detected"
|
||
return 0
|
||
}
|
||
|
||
# Parse arguments to handle debug flag
|
||
ARGS=()
|
||
while [[ $# -gt 0 ]]; do
|
||
case $1 in
|
||
--debug)
|
||
DEBUG_MODE=true
|
||
shift # Remove --debug from arguments
|
||
;;
|
||
*)
|
||
ARGS+=("$1") # Keep other arguments
|
||
shift
|
||
;;
|
||
esac
|
||
done
|
||
|
||
# Restore arguments without --debug
|
||
if [ ${#ARGS[@]} -gt 0 ]; then
|
||
set -- "${ARGS[@]}"
|
||
else
|
||
set -- # Clear all arguments
|
||
fi
|
||
|
||
# Show debug mode status
|
||
if [ "$DEBUG_MODE" = "true" ]; then
|
||
log_debug "Debug mode is ENABLED"
|
||
fi
|
||
|
||
# Main script logic
|
||
case "${1:-}" in
|
||
"--build-be")
|
||
build_backend
|
||
;;
|
||
"--build-fe")
|
||
build_frontend
|
||
;;
|
||
"--platform-build")
|
||
em_build || exit 1
|
||
;;
|
||
"--migrate")
|
||
migrate_services
|
||
;;
|
||
"--start")
|
||
start_services
|
||
;;
|
||
"--stop")
|
||
stop_services
|
||
;;
|
||
"--restart")
|
||
restart_services
|
||
;;
|
||
"--quick-restart")
|
||
quick_restart_services
|
||
;;
|
||
"--verify")
|
||
verify_services
|
||
;;
|
||
"--be-only-start")
|
||
start_be_only_services
|
||
;;
|
||
"--be-only-stop")
|
||
stop_be_only_services
|
||
;;
|
||
"--be-only-restart")
|
||
restart_be_only_services
|
||
;;
|
||
"--be-only-verify")
|
||
verify_be_only_services
|
||
;;
|
||
"--logs")
|
||
show_logs
|
||
;;
|
||
"--lint-fe")
|
||
lint_frontend
|
||
;;
|
||
"--lint-be")
|
||
lint_backend
|
||
;;
|
||
"--help")
|
||
show_usage
|
||
;;
|
||
"")
|
||
# Default action: restart
|
||
restart_services
|
||
;;
|
||
*)
|
||
handle_unknown_option "$1"
|
||
;;
|
||
esac
|