diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index 9b89ac0..0000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,92 +0,0 @@ -name: Rust CI/CD - -on: - push: - branches: [ main ] - pull_request: - branches: [ main ] - -env: - CARGO_TERM_COLOR: always - RUST_VERSION: nightly-2024-03-20 # Updated to a newer nightly that includes rustc 1.79.0 - -jobs: - test: - name: Test - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Install system dependencies - run: | - sudo apt-get update - sudo apt-get install -y \ - pkg-config \ - libssl-dev \ - libgtk-3-dev \ - libwebkit2gtk-4.0-dev \ - libayatana-appindicator3-dev \ - librsvg2-dev \ - cmake - - - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@master - with: - toolchain: nightly-2024-03-20 - components: rustfmt, clippy - - - name: Show Rust version - run: rustc --version - - - name: Cache dependencies - uses: actions/cache@v3 - with: - path: | - ~/.cargo/registry - ~/.cargo/git - target - key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} - - - name: Check formatting - run: cargo fmt -- --check - - - name: Clippy - run: cargo clippy -- -D warnings - - - name: Run tests - run: cargo test --all-features - - build: - name: Build - needs: test - runs-on: ubuntu-latest - if: github.ref == 'refs/heads/main' - steps: - - uses: actions/checkout@v4 - - - name: Install system dependencies - run: | - sudo apt-get update - sudo apt-get install -y \ - pkg-config \ - libssl-dev \ - libgtk-3-dev \ - libwebkit2gtk-4.0-dev \ - libayatana-appindicator3-dev \ - librsvg2-dev \ - cmake - - - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@master - with: - toolchain: nightly-2024-03-20 - components: rustfmt, clippy - - - name: Build - run: cargo build --release - - - name: Upload artifacts - uses: actions/upload-artifact@v3 - with: - name: titanium - path: target/release/titanium \ No newline at end of file diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 396e17a..fc4e7f4 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -1,18 +1,18 @@ -name: Docker CI with Enhanced Checks +name: Docker Build and Push on: push: - branches: [ "main" ] + branches: [ main ] + tags: [ 'v*.*.*' ] pull_request: - branches: [ "main" ] + branches: [ main ] env: REGISTRY: ghcr.io IMAGE_NAME: ${{ github.repository }} - RUST_LOG: info jobs: - lint-and-test: + build-and-push: runs-on: ubuntu-latest permissions: contents: read @@ -22,30 +22,14 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 - - name: Install Rust toolchain - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: nightly - override: true - components: rustfmt, clippy - - - name: Rust cache - uses: Swatinem/rust-cache@v2 - - - name: Check formatting - run: cargo fmt -- --check - - - name: Run clippy - run: cargo clippy -- -D warnings - - - name: Run tests - run: cargo test - - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 + with: + platforms: linux/amd64 + buildkitd-flags: --debug - - name: Log in to Registry + - name: Log in to the Container registry + if: github.event_name != 'pull_request' uses: docker/login-action@v3 with: registry: ${{ env.REGISTRY }} @@ -61,36 +45,17 @@ jobs: type=ref,event=branch type=ref,event=pr type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} type=sha - - name: Build and test Docker image + - name: Build and push Docker image uses: docker/build-push-action@v5 with: context: . - platforms: linux/amd64 - push: false + push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} - cache-from: type=gha - cache-to: type=gha,mode=max - build-args: | - RUST_LOG=info - target: builder - load: true - - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - - name: Build and push final image - if: github.event_name != 'pull_request' - uses: docker/build-push-action@v5 - with: - context: . - platforms: linux/amd64,linux/arm64 - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - cache-from: type=gha - cache-to: type=gha,mode=max - build-args: | - RUST_LOG=info \ No newline at end of file + platforms: linux/amd64 + cache-from: type=gha,scope=${{ github.workflow }} + cache-to: type=gha,mode=max,scope=${{ github.workflow }} + outputs: type=docker,dest=/tmp/image.tar \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 0680f0e..04b506b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,5 @@ FROM rustlang/rust:nightly AS builder - RUN apt-get update && \ apt-get install -y \ pkg-config \ @@ -15,20 +14,10 @@ RUN apt-get update && \ WORKDIR /usr/src/app - COPY Cargo.toml Cargo.lock* ./ - -RUN mkdir src && \ - echo "fn main() { println!(\"Initializing build...\"); }" > src/main.rs && \ - cargo build --release && \ - cargo build --tests && \ - rm -rf src - - COPY src src/ - ARG RUST_LOG=info ENV RUST_LOG=${RUST_LOG} @@ -36,12 +25,11 @@ RUN echo "Building application with RUST_LOG=${RUST_LOG}" && \ cargo build --release && \ cargo test --no-run - FROM debian:bookworm-slim - RUN apt-get update && \ apt-get install -y \ + bash \ ca-certificates \ libwebkit2gtk-4.0-37 \ libjavascriptcoregtk-4.0-18 \ @@ -55,13 +43,10 @@ RUN apt-get update && \ libxdo3 && \ rm -rf /var/lib/apt/lists/* - COPY --from=builder /usr/src/app/target/release/titanium /usr/local/bin/ - ENV RUST_LOG=info - COPY <<'EOF' /usr/local/bin/start.sh #!/bin/bash echo "Starting Titanium server with RUST_LOG=${RUST_LOG}" diff --git a/docker-compose.yml b/docker-compose.yml index 21a23fa..0d5d970 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,16 +1,39 @@ services: app: - build: . + image: ${REGISTRY:-ghcr.io}/${IMAGE_NAME:-developerfred/titanium}:${TAG:-latest} + build: + context: . + args: + RUST_LOG: ${RUST_LOG:-debug} ports: - "3000:3000" environment: - - RUST_LOG=info + - RUST_LOG=${RUST_LOG:-debug} + - RUST_BACKTRACE=full + - RUST_LIB_BACKTRACE=1 + logging: + driver: "json-file" + options: + max-size: "100m" + max-file: "5" + compress: "true" + mode: "non-blocking" + tag: "{{.Name}}" + restart: unless-stopped healthcheck: test: ["CMD", "curl", "-f", "http://localhost:3000/health"] interval: 30s timeout: 10s retries: 3 start_period: 40s + deploy: + resources: + limits: + memory: 1G + reservations: + memory: 512M + volumes: + - ./logs:/app/logs test: build: @@ -19,11 +42,23 @@ services: command: cargo test --all-features -- --nocapture environment: - RUST_LOG=debug - - RUST_BACKTRACE=1 + - RUST_BACKTRACE=full + - RUST_LIB_BACKTRACE=1 volumes: - .:/usr/src/app - cargo-cache:/usr/local/cargo/registry + - ./logs:/app/logs + logging: + driver: "json-file" + options: + max-size: "100m" + max-file: "5" + compress: "true" + mode: "non-blocking" + tag: "{{.Name}}" profiles: ["test"] volumes: - cargo-cache: {} \ No newline at end of file + cargo-cache: {} + logs: + driver: local \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 37cc6e0..0a0ab37 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,7 +5,12 @@ use serde::Deserialize; use std::{net::SocketAddr, time::Instant}; use tokio::task::block_in_place; use tower_http::trace::TraceLayer; -use tracing::{info, Level}; +use tracing::{debug, error, info, warn, Level}; +use tracing_subscriber::{ + fmt::format::FmtSpan, + prelude::*, + EnvFilter, +}; use url::Url; #[derive(Debug, Deserialize, PartialEq)] @@ -16,42 +21,76 @@ struct RenderParams { } fn decode_base64_url(encoded: &str) -> Result { + debug!("Attempting to decode base64 URL: {}", encoded); URL_SAFE .decode(encoded) - .map_err(|_| "Invalid base64".to_string()) - .and_then(|bytes| String::from_utf8(bytes).map_err(|_| "Invalid URL encoding".to_string())) + .map_err(|e| { + error!("Base64 decoding error: {}", e); + "Invalid base64".to_string() + }) + .and_then(|bytes| { + String::from_utf8(bytes).map_err(|e| { + error!("UTF-8 decoding error: {}", e); + "Invalid URL encoding".to_string() + }) + }) } fn validate_url(url_str: &str) -> Result { - Url::parse(url_str).map_err(|_| "Invalid URL".to_string()) + debug!("Validating URL: {}", url_str); + Url::parse(url_str).map_err(|e| { + error!("URL parsing error: {}", e); + "Invalid URL".to_string() + }) } -async fn render_html(url: &Url, _width: u32, _height: u32) -> Result, String> { +async fn render_html(url: &Url, width: u32, height: u32) -> Result, String> { + info!("Starting HTML render for URL: {} ({}x{})", url, width, height); + let start = Instant::now(); + let html_content = reqwest::blocking::get(url.as_str()) - .and_then(|response| response.text()) - .map_err(|e| format!("Failed to fetch URL: {}", e))?; + .and_then(|response| { + debug!("Received response from URL: {:?}", response.status()); + response.text() + }) + .map_err(|e| { + error!("Failed to fetch URL {}: {}", url, e); + format!("Failed to fetch URL: {}", e) + })?; let config = Config { stylesheets: Vec::new(), base_url: Some(url.to_string()), }; - tokio::task::spawn_blocking(move || { + let result = tokio::task::spawn_blocking(move || { + debug!("Launching Dioxus renderer"); dioxus_native::launch_static_html_cfg(&html_content, config); Ok(Vec::new()) }) .await - .map_err(|e| format!("Task failed: {}", e))? + .map_err(|e| { + error!("Task execution failed: {}", e); + format!("Task failed: {}", e) + })?; + + let duration = start.elapsed(); + info!("Render completed in {:?}", duration); + Ok(result) } async fn render_url(Query(params): Query) -> impl IntoResponse { let start = Instant::now(); + info!("Received render request with params: {:?}", params); let result = decode_base64_url(¶ms.url) - .and_then(|decoded| validate_url(&decoded)) + .and_then(|decoded| { + debug!("Decoded URL: {}", decoded); + validate_url(&decoded) + }) .and_then(|url| { info!("Starting render for URL: {}", url); - + let result = block_in_place(|| { tokio::runtime::Handle::current().block_on(render_html(&url, params.w, params.h)) }); @@ -63,23 +102,41 @@ async fn render_url(Query(params): Query) -> impl IntoResponse { }); match result { - Ok(png_data) => (StatusCode::OK, png_data).into_response(), - Err(error) => (StatusCode::BAD_REQUEST, error).into_response(), + Ok(png_data) => { + info!("Successfully rendered PNG ({} bytes)", png_data.len()); + (StatusCode::OK, png_data).into_response() + } + Err(error) => { + warn!("Render request failed: {}", error); + (StatusCode::BAD_REQUEST, error).into_response() + } } } async fn health_check() -> impl IntoResponse { + debug!("Health check requested"); (StatusCode::OK, "OK") } #[tokio::main] async fn main() { + // Initialize logging with detailed configuration tracing_subscriber::fmt() - .with_target(false) - .with_level(true) - .with_max_level(Level::INFO) + .with_env_filter( + EnvFilter::try_from_default_env() + .unwrap_or_else(|_| EnvFilter::new("debug")) + ) + .with_target(true) + .with_thread_ids(true) + .with_thread_names(true) + .with_file(true) + .with_line_number(true) + .with_span_events(FmtSpan::FULL) + .with_timer(tracing_subscriber::fmt::time::UtcTime::rfc_3339()) .init(); + info!("Initializing Titanium service"); + let app = Router::new() .route("/render.png", get(render_url)) .route("/health", get(health_check)) @@ -88,8 +145,19 @@ async fn main() { info!("Starting server on port 3000"); let addr = SocketAddr::from(([0, 0, 0, 0], 3000)); - let listener = tokio::net::TcpListener::bind(addr).await.unwrap(); - axum::serve(listener, app).await.unwrap(); + match tokio::net::TcpListener::bind(addr).await { + Ok(listener) => { + info!("Server listening on http://{}", addr); + if let Err(e) = axum::serve(listener, app).await { + error!("Server error: {}", e); + std::process::exit(1); + } + } + Err(e) => { + error!("Failed to bind to address {}: {}", addr, e); + std::process::exit(1); + } + } } #[cfg(test)] @@ -130,4 +198,4 @@ mod tests { let response = health_check().await.into_response(); assert_eq!(response.status(), StatusCode::OK); } -} +} \ No newline at end of file