From 9e72c2f8bb3c5de51fb8c3a8b80e42ac2aefceb5 Mon Sep 17 00:00:00 2001 From: Lorenzo Pirro Date: Tue, 14 Apr 2026 13:20:25 +0200 Subject: [PATCH 1/3] feat: upgrade logger with level filtering and update documentation --- .github/workflows/ci.yml | 34 ++++++++ README.md | 80 +++++++++--------- cmd/main/main.mbt | 178 +++++++++++++++++---------------------- cmd/main/moon.pkg | 5 +- docker-compose.yml | 18 ++++ lib/config/config.mbt | 34 ++++++++ lib/config/moon.pkg | 4 + lib/logger/logger.mbt | 56 ++++++++++++ lib/logger/moon.pkg | 4 + moon.mod.json | 5 +- 10 files changed, 272 insertions(+), 146 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 docker-compose.yml create mode 100644 lib/config/config.mbt create mode 100644 lib/config/moon.pkg create mode 100644 lib/logger/logger.mbt create mode 100644 lib/logger/moon.pkg diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..338211c --- /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 Check + run: moon check --target native + + - name: MoonBit Update + run: moon update + + - 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..f1aee11 100644 --- a/README.md +++ b/README.md @@ -1,67 +1,67 @@ -# 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 +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. -## ๐Ÿณ Docker Deployment +### Structured Logging +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. -The project uses a serious multi-stage Dockerfile that builds the system from source and exports only the binary. +### 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`. -```bash -# Build the image -just docker-build +## ๐Ÿ›  environment Configuration -# Run the container -just relay=true docker-run -``` +| 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. -## ๐Ÿ›  Project Structure +## ๐Ÿค– CI/CD Foundations -- `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. +Included in `.github/workflows/ci.yml` is an automated pipeline that: +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( - ( - #| - #| - #|

404

- #| - #| - ), - ) - }, + "/api/user", + _ => ({ "id": 1, "username": "moon_dev", "role": "admin" } : Json), ) - // Print Server URL + // 5. Server Startup + @logger.info("๐Ÿ“ก Routes initialized:") for path in app.routes() { - println("\{path.0} http://localhost:4000\{path.1}") + @logger.info(" \{path.0} -> \{path.1}") } - // Serve - println("๐Ÿš€ Server active on http://localhost:4000") try { - app.serve(port=4000) + app.serve(port=cfg.port) } catch { - err => println("Server error: \{err}") + err => @logger.error("๐Ÿ”ฅ Server Panic: \{err}") } }, ) diff --git a/cmd/main/moon.pkg b/cmd/main/moon.pkg index 10237e0..4ca4ad3 100644 --- a/cmd/main/moon.pkg +++ b/cmd/main/moon.pkg @@ -1,9 +1,12 @@ import { "bobzhang/crescent" @crescent, + "bobzhang/crescent/core", "moonbitlang/core/json", "moonbitlang/async", - "moonbitlang/x/sys" @sys, "Metalymph/relay" @relay, + "Metalymph/valkey" @valkey, + "username/webserver_example/lib/config" @config, + "username/webserver_example/lib/logger" @logger, } options( diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..2488c2d --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,18 @@ +services: + webserver: + build: . + image: moon-web-relay:latest + ports: + - "4000:4000" + environment: + - USE_RELAY=true + - RELAY_BACKEND=valkey + - VALKEY_URL=valkey://valkey:6379 + - LOG_LEVEL=DEBUG + depends_on: + - valkey + + valkey: + image: valkey/valkey:8.0-latest + ports: + - "6379:6379" diff --git a/lib/config/config.mbt b/lib/config/config.mbt new file mode 100644 index 0000000..1fd3e11 --- /dev/null +++ b/lib/config/config.mbt @@ -0,0 +1,34 @@ +///| +pub enum RelayBackend { + InMemory + Valkey +} + +///| +pub struct Config { + port : Int + relay_enabled : Bool + relay_backend : RelayBackend + valkey_url : String + log_level : String +} + +///| +pub fn Config::load() -> Config { + let port_str = @sys.get_env_var("PORT").unwrap_or("4000") + let port = try { + @string.parse_int(port_str) + } catch { + _ => 4000 + } + let relay_enabled = @sys.get_env_var("USE_RELAY").unwrap_or("false") == "true" + let relay_backend = match @sys.get_env_var("RELAY_BACKEND") { + Some("valkey") => Valkey + _ => InMemory + } + let valkey_url = @sys.get_env_var("VALKEY_URL").unwrap_or( + "valkey://localhost:6379", + ) + let log_level = @sys.get_env_var("LOG_LEVEL").unwrap_or("INFO") + { port, relay_enabled, relay_backend, valkey_url, log_level } +} diff --git a/lib/config/moon.pkg b/lib/config/moon.pkg new file mode 100644 index 0000000..7500eb1 --- /dev/null +++ b/lib/config/moon.pkg @@ -0,0 +1,4 @@ +import { + "moonbitlang/x/sys" @sys, + "moonbitlang/core/string" @string, +} diff --git a/lib/logger/logger.mbt b/lib/logger/logger.mbt new file mode 100644 index 0000000..0373c3f --- /dev/null +++ b/lib/logger/logger.mbt @@ -0,0 +1,56 @@ +///| +enum LogLevel { + DEBUG + INFO + ERROR +} + +///| +fn LogLevel::from_string(s : String) -> LogLevel { + match s { + "DEBUG" | "debug" => DEBUG + "ERROR" | "error" => ERROR + _ => INFO + } +} + +///| +fn LogLevel::to_int(self : LogLevel) -> Int { + match self { + DEBUG => 0 + INFO => 1 + ERROR => 2 + } +} + +///| +fn get_current_level() -> LogLevel { + @sys.get_env_var("LOG_LEVEL").map(LogLevel::from_string).unwrap_or(INFO) +} + +///| +pub fn info(msg : String) -> Unit { + if get_current_level().to_int() <= INFO.to_int() { + log("INFO", msg) + } +} + +///| +pub fn error(msg : String) -> Unit { + if get_current_level().to_int() <= ERROR.to_int() { + log("ERROR", msg) + } +} + +///| +pub fn debug(msg : String) -> Unit { + if get_current_level().to_int() <= DEBUG.to_int() { + log("DEBUG", msg) + } +} + +///| +fn log(level : String, msg : String) -> Unit { + let log_entry : Json = { "level": level, "message": msg, "timestamp": 0 } + println(log_entry.stringify()) +} diff --git a/lib/logger/moon.pkg b/lib/logger/moon.pkg new file mode 100644 index 0000000..892c766 --- /dev/null +++ b/lib/logger/moon.pkg @@ -0,0 +1,4 @@ +import { + "moonbitlang/core/json", + "moonbitlang/x/sys" @sys, +} diff --git a/moon.mod.json b/moon.mod.json index 06ad4dc..fc54c16 100644 --- a/moon.mod.json +++ b/moon.mod.json @@ -4,8 +4,9 @@ "deps": { "bobzhang/crescent": "0.9.0", "moonbitlang/async": "0.17.0", - "moonbitlang/x": "0.4.41", - "Metalymph/relay": "0.1.0" + "moonbitlang/x": "0.1.0", + "Metalymph/relay": "0.1.0", + "Metalymph/valkey": "0.1.0" }, "readme": "README.mbt.md", "repository": "", From cba56159f0aa9f60d0a24b0da0c5e791ededa759 Mon Sep 17 00:00:00 2001 From: Lorenzo Pirro Date: Tue, 14 Apr 2026 13:22:27 +0200 Subject: [PATCH 2/3] chore: final polish to production-ready template This commit applies final stylistic improvements and technical polishes to the project: - Marked LogLevel as priv in lib/logger to resolve accessibility warnings. - Refined README.md formatting and spacing for better technical documentation readability. - Verified all packages pass final moon check. --- README.md | 10 +++++++--- lib/logger/logger.mbt | 2 +- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index f1aee11..a75d17b 100644 --- a/README.md +++ b/README.md @@ -37,12 +37,15 @@ LOG_LEVEL=DEBUG docker compose up --build ## ๐Ÿ“‚ Design Implementation Details ### Configuration Logic + 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. ### Structured Logging + 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. ### 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 @@ -62,6 +65,7 @@ The multi-stage `Dockerfile` produces a minimal (~2MB) native binary image based ## ๐Ÿค– CI/CD Foundations Included in `.github/workflows/ci.yml` is an automated pipeline that: -1. Performs static analysis (`moon check`). -2. Runs the test suite (`moon test`). -3. Validates the container build. + +1. Performs static analysis (`moon check`). +2. Runs the test suite (`moon test`). +3. Validates the container build. diff --git a/lib/logger/logger.mbt b/lib/logger/logger.mbt index 0373c3f..415105c 100644 --- a/lib/logger/logger.mbt +++ b/lib/logger/logger.mbt @@ -1,5 +1,5 @@ ///| -enum LogLevel { +priv enum LogLevel { DEBUG INFO ERROR From 0a372bb145ca4c3b92405dba51610e7fbf56a890 Mon Sep 17 00:00:00 2001 From: Lorenzo Pirro Date: Tue, 14 Apr 2026 13:26:52 +0200 Subject: [PATCH 3/3] fix(ci): run moon update before moon check --- .github/workflows/ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 338211c..8806c9e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,12 +17,12 @@ jobs: curl -fsSL https://cli.moonbitlang.com/install/unix.sh | bash echo "$HOME/.moon/bin" >> $GITHUB_PATH - - name: MoonBit Check - run: moon check --target native - - name: MoonBit Update run: moon update + - name: MoonBit Check + run: moon check --target native + - name: MoonBit Build run: moon build --target native