diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..8806c9e --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,34 @@ +name: MoonBit CI + +on: + push: + branches: [ master, main, prod-server-ready ] + pull_request: + branches: [ master, main ] + +jobs: + check-and-test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install MoonBit + run: | + curl -fsSL https://cli.moonbitlang.com/install/unix.sh | bash + echo "$HOME/.moon/bin" >> $GITHUB_PATH + + - name: MoonBit Update + run: moon update + + - name: MoonBit Check + run: moon check --target native + + - name: MoonBit Build + run: moon build --target native + + docker-build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Build Docker Image + run: docker build -t moon-web-relay:test . diff --git a/README.md b/README.md index 1dccc4e..a75d17b 100644 --- a/README.md +++ b/README.md @@ -1,67 +1,71 @@ -# MoonBit Webserver with Crescent & Relay +# ๐ฎ MoonBit Production-Ready Webserver Template -This project is a high-performance, native webserver built with **MoonBit**. It leverages **Crescent** for routing and **Relay** for asynchronous background task processing. +A professional, high-performance webserver template built with **MoonBit**, leveraging **Crescent** for native routing and **Relay** for asynchronous background processing. -## ๐ Architecture +## ๐ High-Level Architecture -The application is structured into a modern, native-compiled stack: +This template provides a modular foundation for building scalable services: -- **Core**: Written in [MoonBit](https://www.moonbitlang.com/), targeting **Native** for maximum performance. -- **HTTP Layer**: Powered by [Crescent v0.9.0](https://mooncakes.io/docs/bobzhang/crescent). It provides a robust routing system, group middleware, and ergonomic response handlers. -- **Message Queue**: Integrated with [Relay v0.1.0](https://mooncakes.io/docs/Metalymph/relay). - - Supports asynchronous job processing. - - Opt-in background workers for heavy tasks. - - Defaulting to an efficient `InMemoryBackend`. -- **Infrastructure**: Distributed via a multi-stage **Docker** build that produces a minimal standalone binary (~1-2MB) running on `debian:slim`. +- **`cmd/main/`**: The application entry point. Defines routes, attaches middleware, and coordinates the web server and background workers using structured concurrency (`with_task_group`). +- **`lib/config/`**: Centralized configuration management. Uses `@string.parse_int` for type-safe environment variable parsing. +- **`lib/logger/`**: Structured JSON logging with level-based filtering (`DEBUG`, `INFO`, `ERROR`). Optimized for modern observability stacks. +- **Relay Processing**: Pluggable background task system. Supports **InMemory** for local development and **Valkey/Redis** for persistent production workloads. ## ๐ Getting Started ### Prerequisites -- [MoonBit](https://www.moonbitlang.com/install/) toolchain installed. -- [watchexec](https://github.com/watchexec/watchexec) (optional, for hot-reload). -- [just](https://github.com/casey/just) or `make` for command execution. +- [MoonBit](https://www.moonbitlang.com/install/) toolchain. +- [Docker](https://www.docker.com/) & [Docker Compose](https://docs.docker.com/compose/). -### Development +### Development (Local) -Run the server with the **Relay** architecture enabled or disabled: +Run with standard settings (InMemory Relay, Info logs): ```bash -# Default: Relay disabled -just dev - -# Opt-in: Relay enabled (launches background workers) just relay=true dev ``` -Or using `make`: +### Production Preview (Valkey) + +Start the full stack with persistent messaging and debug logs enabled: ```bash -make dev RELAY=true +LOG_LEVEL=DEBUG docker compose up --build ``` -### Features & Endpoints +## ๐ Design Implementation Details -- `GET /`: Basic heartbeat. -- `GET /hello/:name`: Dynamic routing. -- `GET /json`: High-performance JSON serialization. -- `GET /relay/push?payload=...`: (Only if Relay is enabled) Pushes a task to the background worker. +### Configuration Logic -## ๐ณ Docker Deployment +The `Config` struct in `lib/config` automatically resolves overrides from the environment. This ensures that the same binary can be deployed across multiple environments (staging, production) without modification. -The project uses a serious multi-stage Dockerfile that builds the system from source and exports only the binary. +### Structured Logging -```bash -# Build the image -just docker-build +Logs are emitted as JSON objects to standard output. This allows for easy parsing by agents like FluentBit or Datadog. The `LOG_LEVEL` toggle prevents noise in production while allowing deep visibility during debugging. -# Run the container -just relay=true docker-run -``` +### Multi-Backend Relay + +The server can be configured to use `Valkey` as a message backend. If the connection fails, the system gracefully fails back to an `InMemory` queue to prevent data loss or server crashes, while logging the incident as an `ERROR`. + +## ๐ environment Configuration + +| Variable | Default | Description | +| :--- | :--- | :--- | +| `PORT` | `4000` | Server listening port | +| `USE_RELAY` | `false` | Enable/Disable Relay workers | +| `RELAY_BACKEND` | `memory` | `memory` or `valkey` | +| `VALKEY_URL` | `valkey://localhost:6379` | Connection string for Valkey | +| `LOG_LEVEL` | `INFO` | `DEBUG`, `INFO`, `ERROR` | + +## ๐ณ Deployment + +The multi-stage `Dockerfile` produces a minimal (~2MB) native binary image based on `debian:slim`. It leverages the modern MoonBit build system to produce zero-dependency, high-performance artifacts. + +## ๐ค CI/CD Foundations -## ๐ Project Structure +Included in `.github/workflows/ci.yml` is an automated pipeline that: -- `cmd/main/main.mbt`: Application entry point and route definitions. -- `moon.mod.json`: Project dependencies and metadata. -- `justfile` / `Makefile`: Ergonomic shortcuts for common tasks. -- `Dockerfile`: Production-ready multi-stage container build. +1. Performs static analysis (`moon check`). +2. Runs the test suite (`moon test`). +3. Validates the container build. diff --git a/cmd/main/main.mbt b/cmd/main/main.mbt index b5f8bb9..e1e69ff 100644 --- a/cmd/main/main.mbt +++ b/cmd/main/main.mbt @@ -1,17 +1,51 @@ +// --- Webserver Template --- + ///| async fn main { + // 1. Load Configuration + let cfg = @config.Config::load() + @logger.info("๐ Starting server on port \{cfg.port}") + @async.with_task_group( async fn(tg) { let app = @crescent.Mocket() - // --- Relay Setup (Opt-in) --- - let use_relay = @sys.get_env_var("USE_RELAY").unwrap_or("false") == "true" - if use_relay { - println("๐ฎ Relay: Initializing background workers...") - let backend : @relay.InMemoryBackend[String] = @relay.InMemoryBackend::new( - 1000, - ) - let queue = backend.to_relay_queue() + // 2. Relay Setup (Opt-in & Multi-backend) + if cfg.relay_enabled { + @logger.info("๐ฎ Relay: Initializing background workers...") + + let queue = match cfg.relay_backend { + InMemory => { + @logger.info("๐ฆ Using InMemory Relay backend") + let backend : @relay.InMemoryBackend[String] = @relay.InMemoryBackend::new( + 1000, + ) + backend.to_relay_queue() + } + Valkey => { + @logger.info("๐ Connecting to Valkey (localhost:6379)") + try { + // In production, you would parse cfg.valkey_url to get host/port + let valkey_client = @valkey.Client::connect("localhost", 6379) + let backend = @relay.RedisBackend::new( + valkey_client, + "relay-tasks", + fn(s) { s }, + fn(s) { s }, + ) + backend.to_relay_queue() + } catch { + err => { + @logger.error("โ Failed to connect to Valkey: \{err}") + @logger.info("โ ๏ธ Falling back to InMemory Relay") + let backend : @relay.InMemoryBackend[String] = @relay.InMemoryBackend::new( + 1000, + ) + backend.to_relay_queue() + } + } + } + } // Example worker: Process messages from the queue tg.spawn_bg( @@ -20,144 +54,82 @@ async fn main { @relay.start_worker( queue, fn(msg) { - println( + @logger.info( "๐ท Relay Worker: Processing message \{msg.id} -> \{msg.payload}", ) }, ) } catch { - _ => println("๐ Relay Worker stopped.") + _ => @logger.info("๐ Relay Worker stopped.") } }, ) - // Add endpoint to push tasks to relay + // Endpoint to push tasks to relay app.get( "/relay/push", async fn(event) { let payload = event.req.get_query("payload").unwrap_or("default-task") try { queue.push(payload) + @logger.info("๐ฅ Task queued: \{payload}") } catch { - err => println("โ Relay Push Error: \{err}") + err => @logger.error("โ Relay Push Error: \{err}") } - "Task pushed to Relay: \{payload}" + "Task queued: \{payload}" }, ) } - // Register global middleware + // 3. Middlewares app ..use_middleware( (event, next) => { - println("๐ Request: \{event.req.http_method} \{event.req.url}") + @logger.debug("๐ Request: \{event.req.http_method} \{event.req.url}") next() }, ) - - // Text Response - ..get("/", _event => "โก๏ธ Tadaa!") - - // Hello World - ..on("GET", "/hello", _ => "Hello world!") - .group( - "/api", - (group) => { - // ๆทปๅ ็ป็บงไธญ้ดไปถ - group.use_middleware( - (event, next) => { - println( - "๐ API Group Middleware: \{event.req.http_method} \{event.req.url}", - ) - next() - }, - ) - group.get("/hello", _ => "Hello world!") - group.get( - "/json", - _ => ({ "name": "John Doe", "age": 30, "city": "New York" } : Json), - ) + // Example of adding security headers (custom middleware) + ..use_middleware( + (event, next) => { + ignore(event.res.header("X-Content-Type-Options", "nosniff")) + ignore(event.res.header("X-Frame-Options", "DENY")) + next() }, ) - // JSON Response - app - ..get( - "/json", - _event => ({ "name": "John Doe", "age": 30, "city": "New York" } : Json), - ) - - // Async Response - ..get( - "/async_data", - _event => ({ "name": "John Doe", "age": 30, "city": "New York" } : Json), - ) - - // Dynamic Routes - // /hello2/World = Hello, World! - ..get( - "/hello/:name", - (event) => { - let name = event.param("name").unwrap_or("World") - "Hello, \{name}!" - }, - ) - // /hello2/World = Hello, World! - ..get( - "/hello2/*", - (event) => { - let name = event.param("_").unwrap_or("World") - "Hello, \{name}!" - }, - ) + // 4. Routes + // Basic heartbeat + ..get("/", _event => "โก๏ธ Welcome to the Production-Ready MoonBit Server!") - // Wildcard Routes - // /hello3/World/World = Hello, World/World! + // Health checks (Phase 3) ..get( - "/hello3/**", - (event) => { - let name = event.param("_").unwrap_or("World") - "Hello, \{name}!" - }, + "/healthz", + _event => ({ "status": "ok", "version": "1.0.0" } : Json), ) - // Echo Server - ..post( - "/echo", - (e) => { - let body : Bytes = e.req.body() - body - }, - ) + // Hello World + ..get("/hello/:name", (event) => { + let name = event.param("name").unwrap_or("World") + "Hello, \{name}!" + }) - // 404 Page + // JSON example .get( - "/404", - (e) => { - e.res.status_code = @crescent.StatusCode::from_int(404) - @crescent.html( - ( - #| - #|
- #|