changeset 96:70401cf61e97

[Seobeo] Added logging.
author June Park <parkjune1995@gmail.com>
date Fri, 02 Jan 2026 19:16:17 -0800
parents b51f8cce9170
children 3bdfffaad162
files dowa/dowa.h mrjunejune/BUILD mrjunejune/main.c seobeo/BUILD seobeo/LOGGING.md seobeo/s_linux_network.c seobeo/s_router.c seobeo/s_ssl.c seobeo/s_web.c seobeo/seobeo.h seobeo/seobeo_internal.h
diffstat 11 files changed, 520 insertions(+), 288 deletions(-) [+]
line wrap: on
line diff
--- a/dowa/dowa.h	Fri Jan 02 19:11:35 2026 -0800
+++ b/dowa/dowa.h	Fri Jan 02 19:16:17 2026 -0800
@@ -7,11 +7,6 @@
 #include <assert.h> // mostly for TODO
 #include <stddef.h> // size_t
 
-#include <math.h> // I am not re-writing stuff I guess...
-
-#include <sys/stat.h>
-#include <limits.h>
-
 #include "dowa_internal.h"
 
 // DLAPI macro for DLL export/import
@@ -202,9 +197,9 @@
 
 // --- String Manipulation --- //
 
-DLAPI char      *Dowa_String_Slice(char *from, size_t start, size_t end, Dowa_Arena *p_arena);
-DLAPI char     **Dowa_String_Split(char *from, char *token, int32 from_length, int32 token_length, Dowa_Arena *p_arena);
-DLAPI char      *Dowa_String_Copy_Arena(char *from, Dowa_Arena *p_arena);
+DLAPI char       *Dowa_String_Slice(char *from, size_t start, size_t end, Dowa_Arena *p_arena);
+DLAPI char      **Dowa_String_Split(char *from, char *token, int32 from_length, int32 token_length, Dowa_Arena *p_arena);
+DLAPI char       *Dowa_String_Copy_Arena(char *from, Dowa_Arena *p_arena);
 DLAPI int32       Dowa_String_Pos_Find(const char *p_from, const char *p_value, const size_t from_length, const size_t value_length);
 DLAPI char       *Dowa_String_Find(const char *p_from, const char *p_value, const size_t from_length, const size_t value_length);
 DLAPI int32       Dowa_String_Pos_Find_Char(const char *p_from, int c, int32 from_length);
--- a/mrjunejune/BUILD	Fri Jan 02 19:11:35 2026 -0800
+++ b/mrjunejune/BUILD	Fri Jan 02 19:16:17 2026 -0800
@@ -28,7 +28,14 @@
 cc_binary(
   name = "mrjunejune_server",
   srcs = ["main.c"],
-  deps = ["//seobeo:seobeo_server"], 
+  deps = ["//seobeo:seobeo_server"],
+  data = [":src_files"],
+)
+
+cc_binary(
+  name = "mrjunejune_server_dev",
+  srcs = ["main.c"],
+  deps = ["//seobeo:seobeo_server_dev"],
   data = [":src_files"],
 )
 
@@ -37,6 +44,11 @@
   binary = ":mrjunejune_server",
 )
 
+bundle(
+  name = "mrjunejune_server_dev_bundle",
+  binary = ":mrjunejune_server_dev",
+)
+
 cc_test(
   name = "integration_test",
   srcs = ["test/integration_test.c"],
--- a/mrjunejune/main.c	Fri Jan 02 19:11:35 2026 -0800
+++ b/mrjunejune/main.c	Fri Jan 02 19:16:17 2026 -0800
@@ -28,7 +28,7 @@
 
   int32 token_len = 2;
 
-  while (true)
+  while (1)
   {
     char *start_tag = strstr(cursor, "{{");
     if (!start_tag) break;
--- a/seobeo/BUILD	Fri Jan 02 19:11:35 2026 -0800
+++ b/seobeo/BUILD	Fri Jan 02 19:16:17 2026 -0800
@@ -13,7 +13,7 @@
   visibility = ["//visibility:public"],
 )
 
-# Server-only target (no SSL, no OpenSSL dependency)
+# Server-only target (no SSL, no OpenSSL dependency) - Production
 alias(
   name = "seobeo_server",
   actual = select({
@@ -24,13 +24,23 @@
   visibility = ["//visibility:public"],
 )
 
+# Server-only target (no SSL, no OpenSSL dependency) - Development with Debug Logs
+alias(
+  name = "seobeo_server_dev",
+  actual = select({
+    "//config:macos":  ":seobeo_server_macos_dev",
+    "//config:linux":  ":seobeo_server_linux_dev",
+    "//conditions:default": ":seobeo_server_linux_dev",
+  }),
+  visibility = ["//visibility:public"],
+)
+
 cc_library(
   name = "seobeo_server_macos",
   srcs = [
     "s_linux_network.c",
     "s_web.c",
     "s_ssl.c",
-    "s_router.c",
     "os/s_macos_edge.c",
   ],
   hdrs = [":seobeo_hdrs"],
@@ -45,12 +55,30 @@
 )
 
 cc_library(
+  name = "seobeo_server_macos_dev",
+  srcs = [
+    "s_linux_network.c",
+    "s_web.c",
+    "s_ssl.c",
+    "os/s_macos_edge.c",
+  ],
+  hdrs = [":seobeo_hdrs"],
+  deps = [
+    "//dowa:dowa",
+  ],
+  defines = ["SEOBEO_NO_SSL", "SEOBEO_ENABLE_DEBUG"],
+  target_compatible_with = [
+    "@platforms//os:osx",
+  ],
+  visibility = ["//visibility:public"],
+)
+
+cc_library(
   name = "seobeo_server_linux",
   srcs = [
     "s_linux_network.c",
     "s_web.c",
     "s_ssl.c",
-    "s_router.c",
     "os/s_linux_edge.c",
   ],
   hdrs = [":seobeo_hdrs"],
@@ -64,6 +92,25 @@
   visibility = ["//visibility:public"],
 )
 
+cc_library(
+  name = "seobeo_server_linux_dev",
+  srcs = [
+    "s_linux_network.c",
+    "s_web.c",
+    "s_ssl.c",
+    "os/s_linux_edge.c",
+  ],
+  hdrs = [":seobeo_hdrs"],
+  deps = [
+    "//dowa:dowa",
+  ],
+  defines = ["SEOBEO_NO_SSL", "SEOBEO_ENABLE_DEBUG"],
+  target_compatible_with = [
+    "@platforms//os:linux",
+  ],
+  visibility = ["//visibility:public"],
+)
+
 # Client target with SSL support (full functionality)
 alias(
   name = "seobeo_client",
@@ -81,7 +128,6 @@
     "s_linux_network.c",
     "s_web.c",
     "s_ssl.c",
-    "s_router.c",
     "snapshot_creator.c",
     "os/s_macos_edge.c",
   ],
@@ -102,7 +148,6 @@
     "s_linux_network.c",
     "s_web.c",
     "s_ssl.c",
-    "s_router.c",
     "snapshot_creator.c",
     "os/s_linux_edge.c",
   ],
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/seobeo/LOGGING.md	Fri Jan 02 19:16:17 2026 -0800
@@ -0,0 +1,157 @@
+# Seobeo Logging System
+
+## Overview
+
+All `printf` statements in the seobeo library have been replaced with `Seobeo_Log()` calls for consistent, level-based logging with optional debug mode.
+
+## Log Levels
+
+```c
+typedef enum {
+  SEOBEO_INFO = 0,     // General information (server start, mode, etc.)
+  SEOBEO_WARNING,      // Warnings (SSL not compiled, etc.)
+  SEOBEO_ERROR,        // Errors (allocation failures, connection errors, etc.)
+  SEOBEO_DEBUG,        // Debug messages (only shown in development mode)
+} Seobeo_Log_Level;
+```
+
+## Usage
+
+```c
+Seobeo_Log(SEOBEO_INFO, "Listening on port %s\n", port);
+Seobeo_Log(SEOBEO_ERROR, "Failed to allocate %zu bytes\n", size);
+Seobeo_Log(SEOBEO_DEBUG, "Request line (first %zu bytes)\n", len);
+```
+
+## Debug Mode Control
+
+Debug logs are controlled by the `SEOBEO_ENABLE_DEBUG` macro:
+
+- **Production builds**: Debug logs are disabled (not compiled in)
+- **Development builds**: Debug logs are enabled via `-DSEOBEO_ENABLE_DEBUG`
+
+## Build Targets
+
+### Seobeo Library
+
+#### Production (No Debug Logs)
+```bash
+# macOS
+//seobeo:seobeo_server_macos
+
+# Linux
+//seobeo:seobeo_server_linux
+
+# Platform-agnostic alias
+//seobeo:seobeo_server
+```
+
+#### Development (With Debug Logs)
+```bash
+# macOS
+//seobeo:seobeo_server_macos_dev
+
+# Linux
+//seobeo:seobeo_server_linux_dev
+
+# Platform-agnostic alias
+//seobeo:seobeo_server_dev
+```
+
+### MrJuneJune Server
+
+#### Production Build
+```bash
+# Build
+bazel build //mrjunejune:mrjunejune_server
+
+# Run
+bazel run //mrjunejune:mrjunejune_server
+
+# Bundle (for deployment)
+bazel build //mrjunejune:mrjunejune_server_bundle
+```
+
+#### Development Build (With Debug Logs)
+```bash
+# Build
+bazel build //mrjunejune:mrjunejune_server_dev
+
+# Run
+bazel run //mrjunejune:mrjunejune_server_dev
+
+# Bundle
+bazel build //mrjunejune:mrjunejune_server_dev_bundle
+```
+
+## Log Output Format
+
+All logs are prefixed with their level:
+
+```
+[INFO] Listening on port 6969
+[INFO] Server mode: EDGE
+[ERROR] Failed to allocate 1024 bytes for body
+[DEBUG] Request line (first 200 bytes)
+[DEBUG] sscanf returned 3 (method='GET', path='/', version='HTTP/1.1')
+```
+
+## Changes Summary
+
+### Files Modified
+
+1. **seobeo/s_web.c**
+   - Replaced 26 printf statements with Seobeo_Log
+   - Updated logging to use appropriate levels (INFO, ERROR, DEBUG)
+
+2. **seobeo/s_linux_network.c**
+   - Replaced 10 printf statements with Seobeo_Log
+   - Added proper error and debug logging for network operations
+
+3. **seobeo/s_ssl.c**
+   - Replaced 5 printf/fprintf statements with Seobeo_Log
+   - SSL errors now use SEOBEO_ERROR level
+   - SSL info uses SEOBEO_INFO level
+
+4. **seobeo/BUILD**
+   - Added `seobeo_server_dev` alias for development builds
+   - Added `seobeo_server_macos_dev` with `SEOBEO_ENABLE_DEBUG`
+   - Added `seobeo_server_linux_dev` with `SEOBEO_ENABLE_DEBUG`
+
+5. **mrjunejune/BUILD**
+   - Added `mrjunejune_server_dev` binary target
+   - Added `mrjunejune_server_dev_bundle` for development deployment
+
+## Benefits
+
+1. **Cleaner Production Logs**: Debug messages don't clutter production output
+2. **Better Debugging**: Enable verbose logging during development
+3. **Consistent Format**: All logs follow the same `[LEVEL] message` format
+4. **Conditional Compilation**: Debug code is completely removed from production builds (zero runtime overhead)
+5. **Easy Toggle**: Switch between dev and prod with different build targets
+
+## Examples
+
+### Running in Development Mode
+```bash
+$ bazel run //mrjunejune:mrjunejune_server_dev
+[INFO] Listening on port 6969
+[INFO] Server mode: EDGE
+[DEBUG] Request line (first 30 bytes)
+[DEBUG] sscanf returned 3 (method='GET', path='/', version='HTTP/1.1')
+[DEBUG] Allocating method_copy
+[DEBUG] Allocating version_copy
+[DEBUG] Map now has 2 entries
+[DEBUG] File path: /index.html
+Body Size: 1234
+[DEBUG] Request handled successfully
+```
+
+### Running in Production Mode
+```bash
+$ bazel run //mrjunejune:mrjunejune_server
+[INFO] Listening on port 6969
+[INFO] Server mode: EDGE
+```
+
+Notice that debug messages are completely absent in production mode!
--- a/seobeo/s_linux_network.c	Fri Jan 02 19:11:35 2026 -0800
+++ b/seobeo/s_linux_network.c	Fri Jan 02 19:16:17 2026 -0800
@@ -45,7 +45,7 @@
 
   if (listen(socket_fd, 16) != 0)
   { 
-    printf("Closing: %d\n", socket_fd);
+    Seobeo_Log(SEOBEO_DEBUG, "Closing socket: %d\n", socket_fd);
     perror("listen"); close(socket_fd); return NULL; 
   }
 
@@ -73,7 +73,7 @@
   p_handle->write_buffer_capacity = INITIAL_BUFFER_CAPACITY;
   p_handle->write_buffer_len = 0;
 
-  p_handle->destroyed = false;
+  p_handle->destroyed = FALSE;
 
   return p_handle;
 }
@@ -117,7 +117,7 @@
       return NULL;
     }
   }
-  p_handle->connected = true;
+  p_handle->connected = TRUE;
 
   p_handle->host = host != NULL ? strdup(host) : "localhost";
   p_handle->port = strdup(port);
@@ -130,7 +130,7 @@
   p_handle->write_buffer_capacity = INITIAL_BUFFER_CAPACITY;
   p_handle->write_buffer_len = 0;
 
-  p_handle->destroyed = false;
+  p_handle->destroyed = FALSE;
 
   return p_handle;
 }
@@ -159,7 +159,7 @@
 
   p_client_handle->socket               = client_fd;
   p_client_handle->type                 = SEOBEO_STREAM_TYPE_CLIENT;
-  p_client_handle->connected            = true;
+  p_client_handle->connected            = TRUE;
 
   // TODO: support SSL in the future.
   p_client_handle->ssl_ctx              = NULL;
@@ -180,7 +180,7 @@
   p_client_handle->file                  = NULL;
   p_client_handle->text_copy             = NULL;
   p_client_handle->file_name             = NULL;
-  p_client_handle->destroyed             = false;
+  p_client_handle->destroyed             = FALSE;
 
   return p_client_handle;
 }
@@ -189,9 +189,9 @@
 {
   if (!p_handle) return;
 
-  bool expected = false;
+  boolean expected = FALSE;
   // Need to check
-  if (!atomic_compare_exchange_strong(&p_handle->destroyed, &expected, true))
+  if (!atomic_compare_exchange_strong(&p_handle->destroyed, &expected, TRUE))
   {
     return;
   }
@@ -202,7 +202,7 @@
   Seobeo_SSL_Cleanup(p_handle);
 
   if (p_handle->socket) {
-    printf("Closing: %d\n", p_handle->socket);
+    Seobeo_Log(SEOBEO_DEBUG, "Closing handle socket: %d\n", p_handle->socket);
     close(p_handle->socket);
   }
 
@@ -218,7 +218,7 @@
   uint32 total = p_handle->write_buffer_len;
   uint32 sent  = 0;
 
-  printf("Total: %d\n\n", p_handle->write_buffer_len);
+  Seobeo_Log(SEOBEO_DEBUG, "Write buffer total: %d\n", p_handle->write_buffer_len);
 
   while (sent < total)
   {
@@ -230,7 +230,7 @@
       sent += (uint32)n;
     }else
     {
-      printf("socket: %d\n", p_handle->socket);
+      Seobeo_Log(SEOBEO_DEBUG, "Flushing socket: %d\n", p_handle->socket);
       ssize_t n = write(
         p_handle->socket,
         p_handle->write_buffer + sent,
@@ -269,7 +269,7 @@
       if (n==0)
       {
         // DEBUG
-        printf("NONE %d\n", offset);
+        Seobeo_Log(SEOBEO_DEBUG, "Write offset: %d\n", offset);
         break;
       }
       if (n < 0)
@@ -285,27 +285,27 @@
       }
       offset += (uint32)n;
       // DEBUG
-      printf("\n\noffset: %d data_size: %d\n\n", offset, data_size);
+      Seobeo_Log(SEOBEO_DEBUG, "Write completed - offset: %d, data_size: %d\n", offset, data_size);
     }
     // DEBUG
-    printf("\n\nTotal: %d\n", offset);
+    Seobeo_Log(SEOBEO_DEBUG, "Total bytes written: %d\n", offset);
     return 0;
   }
 
   if (!p_handle)
   {
-    printf("[ERROR] p_handle is NULL before memcpy\n");
+    Seobeo_Log(SEOBEO_ERROR, "p_handle is NULL before memcpy\n");
     return -1;
   }
   
   if (!p_handle->write_buffer)
   {
-    printf("[ERROR] p_handle->write_buffer is NULL (len=%zu, size=%zu)\n",
+    Seobeo_Log(SEOBEO_ERROR, "p_handle->write_buffer is NULL (len=%u, size=%u)\n",
             p_handle->write_buffer_len, data_size);
     return -1;
   }
   
-  printf("[DEBUG] memcpy -> dest=%p (write_buffer=%p + offset=%zu), src=%p, size=%zu\n",
+  Seobeo_Log(SEOBEO_DEBUG, "memcpy -> dest=%p (write_buffer=%p + offset=%u), src=%p, size=%u\n",
           p_handle->write_buffer + p_handle->write_buffer_len,
           p_handle->write_buffer,
           p_handle->write_buffer_len,
--- a/seobeo/s_router.c	Fri Jan 02 19:11:35 2026 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,203 +0,0 @@
-#include "seobeo/seobeo.h"
-#include <string.h>
-#include <stdio.h>
-#include <stdlib.h>
-
-struct Seobeo_Route_Struct {
-  char *method; // "GET", "POST", "PUT", "DELETE"
-  char *path_pattern; // "/v1/users/:id/posts/:post_id"
-  Seobeo_Route_Handler handler;
-
-  // Pre-parsed path segments for efficient matching
-  char **path_segments; // ["v1", "users", ":id", "posts", ":post_id"]
-  boolean *is_param; // [false, false, true, false, true]
-  size_t segment_count;
-};
-
-static Seobeo_Route *g_routes = NULL;
-
-void Seobeo_Router_Init()
-{
-  Dowa_Array_Reserve(g_routes, 20);
-}
-
-void Seobeo_Router_Register(const char *method, const char *path_pattern, Seobeo_Route_Handler handler)
-{
-  Seobeo_Route route = {0};
-
-  route.method = strdup(method);
-  route.path_pattern = strdup(path_pattern);
-  route.handler = handler;
-  route.path_segments = Dowa_String_Split(path_pattern, "/", strlen(path_pattern), 1, NULL);
-  route.segment_count = Dowa_Array_Length(route.path_segments);
-  route.is_param = (boolean*)malloc(sizeof(boolean) * route.segment_count);
-
-  for (size_t i = 0; i < route.segment_count; i++)
-    route.is_param[i] = (route.path_segments[i][0] == ':');
-
-  Dowa_Array_Push(g_routes, route);
-}
-
-// Match route and extract path parameters
-static boolean match_route_and_extract(
-  Seobeo_Route *route,
-  const char *request_path,
-  Seobeo_Request_Entry **pp_request_map,
-  Dowa_Arena *p_arena)
-{
-  Dowa_Arena *p_temp_arena = Dowa_Arena_Create(1024);
-  char **request_segments = Dowa_String_Split(request_path, "/", strlen(request_path), 1, p_temp_arena);
-  size_t request_segment_count = Dowa_Array_Length(request_segments);
-  // Check segment count matches
-  if (request_segment_count != route->segment_count)
-  {
-    Dowa_Arena_Free(p_temp_arena);
-    return FALSE;
-  }
-
-  for (size_t i = 0; i < route->segment_count; i++)
-  {
-    // parameters
-    if (route->is_param[i])
-    {
-      char *param_name = route->path_segments[i];  // e.g., ":id"
-      char *param_value = request_segments[i];    // e.g., "123"
-
-      // Should Copy to arena 
-      char *key = Dowa_String_Copy_Arena(param_name, p_arena);
-      char *value = Dowa_String_Copy_Arena(param_value, p_arena);
-      Dowa_HashMap_Push_Arena(*pp_request_map, key, value, p_arena);
-    }
-    else
-    {
-      // Does not match.
-      if (strcmp(route->path_segments[i], request_segments[i]) != 0)
-      {
-        Dowa_Arena_Free(p_temp_arena);
-        return FALSE;
-      }
-    }
-  }
-  
-  Dowa_Arena_Free(p_temp_arena);
-  return TRUE;
-}
-
-Seobeo_Route_Handler Seobeo_Router_Find_Handler(const char *method,
-                         const char *path,
-                         Seobeo_Request_Entry **pp_request_map,
-                         Dowa_Arena *p_arena) {
-  if (g_routes == NULL)
-  {
-    return NULL;
-  }
-
-  size_t route_count = Dowa_Array_Length(g_routes);
-  for (size_t i = 0; i < route_count; i++)
-  {
-    Seobeo_Route *route = &g_routes[i];
-    if (strcmp(route->method, method) != 0)
-    {
-      continue;
-    }
-
-    if (match_route_and_extract(route, path, pp_request_map, p_arena))
-    {
-      return route->handler;
-    }
-  }
-
-  return NULL;
-}
-
-void Seobeo_Router_Send_Response(
-    Seobeo_Handle *p_handle,
-    Seobeo_Request_Entry *p_response_map,
-    Dowa_Arena *p_arena)
-{
-  if (p_response_map == NULL)
-  {
-    char *header = Dowa_Arena_Allocate(p_arena, 1024);
-    Seobeo_Web_Header_Generate(header, HTTP_INTERNAL_ERROR, "text/plain", 21);
-    Seobeo_Handle_Queue(p_handle, (uint8_t*)header, strlen(header));
-    Seobeo_Handle_Queue(p_handle, (uint8_t*)"Internal Server Error", 21);
-    Seobeo_Handle_Flush(p_handle);
-    return;
-  }
-
-  // Header 
-  int status = HTTP_OK;
-  void *p_status_kv = Dowa_HashMap_Get_Ptr(p_response_map, "status");
-  if (p_status_kv)
-  {
-    const char *status_str = ((Seobeo_Request_Entry*)p_status_kv)->value;
-    status = atoi(status_str);
-  }
-
-  // Body 
-  const char *body = "";
-  void *p_body_kv = Dowa_HashMap_Get_Ptr(p_response_map, "body");
-  if (p_body_kv)
-  {
-    body = ((Seobeo_Request_Entry*)p_body_kv)->value;
-  }
-
-  // Default text plain
-  const char *content_type = "text/html";
-  void *p_content_type_kv = Dowa_HashMap_Get_Ptr(p_response_map, "content-type");
-  if (p_content_type_kv)
-  {
-    content_type = ((Seobeo_Request_Entry*)p_content_type_kv)->value;
-  }
-
-  // Check for custom content-length (for binary data)
-  size_t body_length = strlen(body);
-  void *p_content_length_kv = Dowa_HashMap_Get_Ptr(p_response_map, "content-length");
-  if (p_content_length_kv)
-  {
-    const char *content_length_str = ((Seobeo_Request_Entry*)p_content_length_kv)->value;
-    body_length = atoi(content_length_str);
-  }
-
-  // All other types are default thoguht to be headers.
-  char *header = Dowa_Arena_Allocate(p_arena, 4096);
-  Seobeo_Web_Header_Generate(header, status, content_type, body_length);
-  for (int i = 0; i < Dowa_Array_Length(p_response_map); i++)
-  {
-    if (
-      strstr(p_response_map[i].key, "status") ||
-      strstr(p_response_map[i].key, "body") ||
-      strstr(p_response_map[i].key, "content-type") ||
-      strstr(p_response_map[i].key, "content-length")  // Skip custom content-length
-    )
-      continue;
-
-    int32 current_header_len = strlen(header);
-    char *temp = malloc(sizeof(char) * 1024);
-    sprintf(temp, "%s: %s\r\n\r\n", p_response_map[i].key, p_response_map[i].value);
-    memcpy(&header[current_header_len - 2 /* \r\n */], temp, strlen(temp));
-    free(temp);
-  }
-
-  Seobeo_Handle_Queue(p_handle, (uint8_t*)header, strlen(header));
-  Seobeo_Handle_Queue(p_handle, (uint8_t*)body, body_length);  // Use actual body length
-  Seobeo_Handle_Flush(p_handle);
-}
-
-void Seobeo_Router_Destroy()
-{
-  if (g_routes == NULL)
-    return;
-
-  size_t route_count = Dowa_Array_Length(g_routes);
-  for (size_t i = 0; i < route_count; i++)
-  {
-    Seobeo_Route *route = &g_routes[i];
-    if (route->method) free(route->method);
-    if (route->path_pattern) free(route->path_pattern);
-    if (route->path_segments) Dowa_Array_Free(route->path_segments);
-    if (route->is_param) free(route->is_param);
-  }
-  Dowa_Array_Free(g_routes);
-  g_routes = NULL;
-}
--- a/seobeo/s_ssl.c	Fri Jan 02 19:11:35 2026 -0800
+++ b/seobeo/s_ssl.c	Fri Jan 02 19:16:17 2026 -0800
@@ -17,12 +17,12 @@
 {
   if (!p_handle) return -1;
 
-  printf("USE SSL\n\n");
+  Seobeo_Log(SEOBEO_INFO, "Using SSL/TLS encryption\n");
   Seobeo_Web_SSL_Init();
   p_handle->ssl_ctx = SSL_CTX_new(TLS_client_method());
   if (!p_handle->ssl_ctx)
   {
-    fprintf(stderr, "SSL_CTX_new failed\n");
+    Seobeo_Log(SEOBEO_ERROR, "SSL_CTX_new failed\n");
     ERR_print_errors_fp(stderr);
     return -1;
   }
@@ -32,7 +32,7 @@
   p_handle->ssl = SSL_new(p_handle->ssl_ctx);
   if (!p_handle->ssl)
   {
-    fprintf(stderr, "SSL_new failed\n");
+    Seobeo_Log(SEOBEO_ERROR, "SSL_new failed\n");
     ERR_print_errors_fp(stderr);
     SSL_CTX_free(p_handle->ssl_ctx);
     p_handle->ssl_ctx = NULL;
@@ -47,7 +47,7 @@
 
   if (SSL_connect(p_handle->ssl) != 1)
   {
-    fprintf(stderr, "SSL_connect failed\n");
+    Seobeo_Log(SEOBEO_ERROR, "SSL_connect failed\n");
     ERR_print_errors_fp(stderr);
     SSL_free(p_handle->ssl);
     SSL_CTX_free(p_handle->ssl_ctx);
@@ -126,7 +126,7 @@
 int Seobeo_SSL_Setup_Client(Seobeo_Handle *p_handle, const char *host, int socket_fd)
 {
   (void)p_handle; (void)host; (void)socket_fd;
-  fprintf(stderr, "SSL support not compiled in\n");
+  Seobeo_Log(SEOBEO_WARNING, "SSL support not compiled in\n");
   return -1;
 }
 void Seobeo_SSL_Cleanup(Seobeo_Handle *p_handle) { (void)p_handle; }
--- a/seobeo/s_web.c	Fri Jan 02 19:11:35 2026 -0800
+++ b/seobeo/s_web.c	Fri Jan 02 19:16:17 2026 -0800
@@ -93,23 +93,22 @@
 void Seobeo_Web_HandleClientRequest(Seobeo_Handle  *p_cli_handle,
                                     Seobeo_Cache_Entry *p_html_cache)
 {
+  Seobeo_Log(SEOBEO_INFO, "Client is from %s\n", p_cli_handle->host);
   Dowa_Arena *p_request_arena = Dowa_Arena_Create(1*1024*1024); // 1MB for request parsing
   if (!p_request_arena) { perror("Dowa_Arena_Create request"); goto clean_up; }
 
-  Dowa_Arena *p_response_arena = Dowa_Arena_Create(5*1024*1024); // 1MB for response
+  Dowa_Arena *p_response_arena = Dowa_Arena_Create(5*1024*1024); // 5MB for response
   if (!p_response_arena) { perror("Dowa_Arena_Create response"); goto clean_up; }
 
   void *p_response_header = Dowa_Arena_Allocate(p_response_arena, (size_t)1024*5); // 5Kb
   if (!p_response_header) { perror("Dowa_Arena_Allocate"); goto clean_up; }
 
-  // Parse request headers into hashmap using arena
   Seobeo_Request_Entry *p_req_map = NULL;
   int parse_result = Seobeo_Web_Header_Parse(p_cli_handle, &p_req_map, p_request_arena);
 
-  // Treat EAGAIN (return code 1) as success - headers were parsed, body may still be coming
   if (parse_result != 0 && parse_result != 1)
   {
-    printf("ERROR: Seobeo_Web_Header_Parse failed with code %d\n", parse_result);
+    Seobeo_Log(SEOBEO_ERROR, "Seobeo_Web_Header_Parse failed with code %d\n", parse_result);
     fflush(stdout);
     Seobeo_Web_Header_Generate(p_response_header,
                                HTTP_BAD_REQUEST,
@@ -121,19 +120,18 @@
     goto clean_up;
   }
 
-  printf("DEBUG: Parse completed with code %d\n", parse_result);
+  Seobeo_Log(SEOBEO_DEBUG, "Parse completed with code %d\n", parse_result);
   fflush(stdout);
 
-  // Extract method (GET, POST, etc.)
   void *p_method_kv = Dowa_HashMap_Get_Ptr(p_req_map, "HTTP_Method");
   const char *method = p_method_kv ? ((Seobeo_Request_Entry*)p_method_kv)->value : NULL;
 
-  printf("DEBUG: Parsed request, method=%s\n", method ? method : "NULL");
+  Seobeo_Log(SEOBEO_DEBUG, "Parsed request, method=%s\n", method ? method : "NULL");
   fflush(stdout);
 
   if (!method)
   {
-    printf("ERROR: No HTTP method found in request\n");
+    Seobeo_Log(SEOBEO_ERROR, "No HTTP method found in request\n");
     fflush(stdout);
     Seobeo_Web_Header_Generate(p_response_header,
                                HTTP_BAD_REQUEST,
@@ -225,7 +223,7 @@
     else if (strstr(file_path, ".ico")) mime = "image/x-icon";
     else if (strstr(file_path, ".json")) mime = "application/json";
 
-    printf("File path: %s\nBody Size: %zu\n", file_path, body_size);
+    Seobeo_Log(SEOBEO_DEBUG, "File path: %s\nBody Size: %zu\n", file_path, body_size);
 
     Seobeo_Web_Header_Generate(p_response_header,
                                HTTP_OK,
@@ -239,7 +237,7 @@
                         (const uint8*)file_content,
                         (uint32)body_size);
     Seobeo_Handle_Flush(p_cli_handle);
-    printf("DONE\n\n\n");
+    Seobeo_Log(SEOBEO_DEBUG, "Request handled successfully\n");
   }
   else
   {
@@ -254,7 +252,7 @@
   goto clean_up;
 
 clean_up:
-  printf("clean up\n\n");
+  Seobeo_Log(SEOBEO_INFO, "Clean up all Arenas\n");
   if (p_cli_handle)
     Seobeo_Handle_Destroy(p_cli_handle);
   if (p_request_arena)
@@ -295,16 +293,14 @@
   if (first_line_end)
   {
     size_t first_line_len = first_line_end - buf;
-    printf("DEBUG: Request line (first %zu bytes): '", first_line_len > 200 ? 200 : first_line_len);
-    fwrite(buf, 1, first_line_len > 200 ? 200 : first_line_len, stdout);
-    printf("'\n");
+    Seobeo_Log(SEOBEO_DEBUG, "Request line (first %zu bytes)\n", first_line_len > 200 ? 200 : first_line_len);
     fflush(stdout);
   }
 
   // This seems kinda bad ?
   char method[16], path[256], version[16];
   int scan_result = sscanf(buf, "%15s %255s %15s", method, path, version);
-  printf("DEBUG: sscanf returned %d (method='%s', path='%s', version='%s')\n",
+  Seobeo_Log(SEOBEO_DEBUG, "sscanf returned %d (method='%s', path='%s', version='%s')\n",
          scan_result,
          scan_result >= 1 ? method : "N/A",
          scan_result >= 2 ? path : "N/A",
@@ -313,29 +309,29 @@
 
   if (scan_result != 3)
   {
-    printf("ERROR: Failed to parse request line\n");
+    Seobeo_Log(SEOBEO_ERROR, "Failed to parse request line\n");
     fflush(stdout);
     return -1;
   }
 
   // Copy strings to arena and store in hashmap
-  printf("DEBUG: Allocating method_copy\n");
+  Seobeo_Log(SEOBEO_DEBUG, "Allocating method_copy\n");
   fflush(stdout);
   char *method_copy = Dowa_Arena_Allocate(p_arena, strlen(method) + 1);
-  if (!method_copy) { printf("ERROR: Failed to allocate method_copy\n"); return -1; }
+  if (!method_copy) { Seobeo_Log(SEOBEO_ERROR, "Failed to allocate method_copy\n"); return -1; }
   strcpy(method_copy, method);
 
-  printf("DEBUG: Allocating version_copy\n");
+  Seobeo_Log(SEOBEO_DEBUG, "Allocating version_copy\n");
   fflush(stdout);
   char *version_copy = Dowa_Arena_Allocate(p_arena, strlen(version) + 1);
-  if (!version_copy) { printf("ERROR: Failed to allocate version_copy\n"); return -1; }
+  if (!version_copy) { Seobeo_Log(SEOBEO_ERROR, "Failed to allocate version_copy\n"); return -1; }
   strcpy(version_copy, version);
 
-  printf("DEBUG: Pushing HTTP_Method and Version to map\n");
+  Seobeo_Log(SEOBEO_DEBUG, "Pushing HTTP_Method and Version to map\n");
   fflush(stdout);
   Dowa_HashMap_Push_Arena(*pp_map, "HTTP_Method", method_copy, p_arena);
   Dowa_HashMap_Push_Arena(*pp_map, "Version", version_copy, p_arena);
-  printf("DEBUG: Map now has %zu entries\n", Dowa_Array_Length(*pp_map));
+  Seobeo_Log(SEOBEO_DEBUG, "Map now has %zu entries\n", Dowa_Array_Length(*pp_map));
   fflush(stdout);
 
   // 1) Separate raw path and query string
@@ -437,14 +433,14 @@
     const char *content_length_str = ((Seobeo_Request_Entry*)p_cl_kv)->value;
     size_t body_len = atoi(content_length_str);
 
-    printf("DEBUG: Content-Length=%zu, reading body in chunks...\n", body_len);
+    Seobeo_Log(SEOBEO_DEBUG, "Content-Length=%zu, reading body in chunks...\n", body_len);
     fflush(stdout);
 
     // Allocate buffer for entire body
     char *body = Dowa_Arena_Allocate(p_arena, body_len + 1);
     if (!body)
     {
-      printf("ERROR: Failed to allocate %zu bytes for body\n", body_len);
+      Seobeo_Log(SEOBEO_ERROR, "Failed to allocate %zu bytes for body\n", body_len);
       fflush(stdout);
       return -1;
     }
@@ -464,7 +460,7 @@
         total_read += to_copy;
         Seobeo_Handle_Consume(p_handle, (uint32)to_copy);
 
-        printf("DEBUG: Copied %zu bytes, total %zu/%zu\n", to_copy, total_read, body_len);
+        Seobeo_Log(SEOBEO_DEBUG, "Copied %zu bytes, total %zu/%zu\n", to_copy, total_read, body_len);
         fflush(stdout);
       }
 
@@ -474,7 +470,7 @@
         int r = Seobeo_Handle_Read(p_handle);
         if (r < 0)
         {
-          printf("ERROR: Read failed with %d\n", r);
+          Seobeo_Log(SEOBEO_ERROR, "Read failed with %d\n", r);
           fflush(stdout);
           return -1;
         }
@@ -489,7 +485,7 @@
     }
 
     body[body_len] = '\0';
-    printf("DEBUG: Body fully received (%zu bytes)\n", body_len);
+    Seobeo_Log(SEOBEO_DEBUG, "Body fully received (%zu bytes)\n", body_len);
     fflush(stdout);
 
     // Body is arena-allocated
@@ -529,12 +525,12 @@
     Seobeo_Stream_Handle_Server_Create(NULL, port);
   if (p_server_handle->socket < 0) return 1;
 
-  printf("Listening on port %s\n", port);
+  Seobeo_Log(SEOBEO_INFO, "Listening on port %s\n", port);
 
   // Fork‐based fallback
   if (mode == SEOBEO_MODE_FORK)
   {
-    printf("FORK MODE\n");
+    Seobeo_Log(SEOBEO_INFO, "Server mode: FORK\n");
     struct sigaction sa;
     sa.sa_handler = SigchildHandler;
     sigemptyset(&sa.sa_mask);
@@ -558,7 +554,7 @@
 
   if (mode == SEOBEO_MODE_EDGE)
   {
-    printf("EDGE MODE\n");
+    Seobeo_Log(SEOBEO_INFO, "Server mode: EDGE\n");
     Seobeo_Web_Edge(p_server_handle, thread_count, p_html_cache);
   }
 
@@ -600,7 +596,7 @@
   while (1)
   {
     int n = Seobeo_Handle_Read(h);
-    printf("Size: %d\n", n);
+    Seobeo_Log(SEOBEO_DEBUG, "Received size: %d bytes\n", n);
     if (n > 0)
     {
       // TODO: Maybe directly use arena inside of the struct? idk if that is useful or not...
@@ -615,7 +611,7 @@
     {
       // Debug
       // peer closed; we’ve got everything
-      printf("\n\nCLOSED\n\n");
+      Seobeo_Log(SEOBEO_DEBUG, "Connection closed by client\n");
       break;
     }else
     {
@@ -626,8 +622,237 @@
   }
 
   // Debug
-  printf("Request body %s", p_request_body);
+  Seobeo_Log(SEOBEO_DEBUG, "Request body: %s\n", p_request_body);
   Dowa_Arena_Free(p_request_arena);
   Seobeo_Handle_Destroy(h);
   return 0;
 }
+
+/* Router logic */
+struct Seobeo_Route_Struct {
+  char *method; // "GET", "POST", "PUT", "DELETE"
+  char *path_pattern; // "/v1/users/:id/posts/:post_id"
+  Seobeo_Route_Handler handler;
+
+  // Pre-parsed path segments for efficient matching
+  char **path_segments; // ["v1", "users", ":id", "posts", ":post_id"]
+  boolean *is_param; // [false, false, true, false, true]
+  size_t segment_count;
+};
+
+static Seobeo_Route *g_routes = NULL;
+
+void Seobeo_Router_Init()
+{
+  Dowa_Array_Reserve(g_routes, 20);
+}
+
+void Seobeo_Router_Register(const char *method, const char *path_pattern, Seobeo_Route_Handler handler)
+{
+  Seobeo_Route route = {0};
+
+  route.method = strdup(method);
+  route.path_pattern = strdup(path_pattern);
+  route.handler = handler;
+  route.path_segments = Dowa_String_Split(path_pattern, "/", strlen(path_pattern), 1, NULL);
+  route.segment_count = Dowa_Array_Length(route.path_segments);
+  route.is_param = (boolean*)malloc(sizeof(boolean) * route.segment_count);
+
+  for (size_t i = 0; i < route.segment_count; i++)
+    route.is_param[i] = (route.path_segments[i][0] == ':');
+
+  Dowa_Array_Push(g_routes, route);
+}
+
+// Match route and extract path parameters
+static boolean match_route_and_extract(
+  Seobeo_Route *route,
+  const char *request_path,
+  Seobeo_Request_Entry **pp_request_map,
+  Dowa_Arena *p_arena)
+{
+  Dowa_Arena *p_temp_arena = Dowa_Arena_Create(1024);
+  char **request_segments = Dowa_String_Split(request_path, "/", strlen(request_path), 1, p_temp_arena);
+  size_t request_segment_count = Dowa_Array_Length(request_segments);
+  // Check segment count matches
+  if (request_segment_count != route->segment_count)
+  {
+    Dowa_Arena_Free(p_temp_arena);
+    return FALSE;
+  }
+
+  for (size_t i = 0; i < route->segment_count; i++)
+  {
+    // parameters
+    if (route->is_param[i])
+    {
+      char *param_name = route->path_segments[i];  // e.g., ":id"
+      char *param_value = request_segments[i];    // e.g., "123"
+
+      // Should Copy to arena 
+      char *key = Dowa_String_Copy_Arena(param_name, p_arena);
+      char *value = Dowa_String_Copy_Arena(param_value, p_arena);
+      Dowa_HashMap_Push_Arena(*pp_request_map, key, value, p_arena);
+    }
+    else
+    {
+      // Does not match.
+      if (strcmp(route->path_segments[i], request_segments[i]) != 0)
+      {
+        Dowa_Arena_Free(p_temp_arena);
+        return FALSE;
+      }
+    }
+  }
+  
+  Dowa_Arena_Free(p_temp_arena);
+  return TRUE;
+}
+
+Seobeo_Route_Handler Seobeo_Router_Find_Handler(const char *method,
+                         const char *path,
+                         Seobeo_Request_Entry **pp_request_map,
+                         Dowa_Arena *p_arena) {
+  if (g_routes == NULL)
+  {
+    return NULL;
+  }
+
+  size_t route_count = Dowa_Array_Length(g_routes);
+  for (size_t i = 0; i < route_count; i++)
+  {
+    Seobeo_Route *route = &g_routes[i];
+    if (strcmp(route->method, method) != 0)
+    {
+      continue;
+    }
+
+    if (match_route_and_extract(route, path, pp_request_map, p_arena))
+    {
+      return route->handler;
+    }
+  }
+
+  return NULL;
+}
+
+void Seobeo_Router_Send_Response(
+    Seobeo_Handle *p_handle,
+    Seobeo_Request_Entry *p_response_map,
+    Dowa_Arena *p_arena)
+{
+  if (p_response_map == NULL)
+  {
+    char *header = Dowa_Arena_Allocate(p_arena, 1024);
+    Seobeo_Web_Header_Generate(header, HTTP_INTERNAL_ERROR, "text/plain", 21);
+    Seobeo_Handle_Queue(p_handle, (uint8_t*)header, strlen(header));
+    Seobeo_Handle_Queue(p_handle, (uint8_t*)"Internal Server Error", 21);
+    Seobeo_Handle_Flush(p_handle);
+    return;
+  }
+
+  // Header 
+  int status = HTTP_OK;
+  void *p_status_kv = Dowa_HashMap_Get_Ptr(p_response_map, "status");
+  if (p_status_kv)
+  {
+    const char *status_str = ((Seobeo_Request_Entry*)p_status_kv)->value;
+    status = atoi(status_str);
+  }
+
+  // Body 
+  const char *body = "";
+  void *p_body_kv = Dowa_HashMap_Get_Ptr(p_response_map, "body");
+  if (p_body_kv)
+  {
+    body = ((Seobeo_Request_Entry*)p_body_kv)->value;
+  }
+
+  // Default text plain
+  const char *content_type = "text/html";
+  void *p_content_type_kv = Dowa_HashMap_Get_Ptr(p_response_map, "content-type");
+  if (p_content_type_kv)
+  {
+    content_type = ((Seobeo_Request_Entry*)p_content_type_kv)->value;
+  }
+
+  // Check for custom content-length (for binary data)
+  size_t body_length = strlen(body);
+  void *p_content_length_kv = Dowa_HashMap_Get_Ptr(p_response_map, "content-length");
+  if (p_content_length_kv)
+  {
+    const char *content_length_str = ((Seobeo_Request_Entry*)p_content_length_kv)->value;
+    body_length = atoi(content_length_str);
+  }
+
+  // All other types are default thoguht to be headers.
+  char *header = Dowa_Arena_Allocate(p_arena, 4096);
+  Seobeo_Web_Header_Generate(header, status, content_type, body_length);
+  for (int i = 0; i < Dowa_Array_Length(p_response_map); i++)
+  {
+    if (
+      strstr(p_response_map[i].key, "status") ||
+      strstr(p_response_map[i].key, "body") ||
+      strstr(p_response_map[i].key, "content-type") ||
+      strstr(p_response_map[i].key, "content-length")  // Skip custom content-length
+    )
+      continue;
+
+    int32 current_header_len = strlen(header);
+    char *temp = malloc(sizeof(char) * 1024);
+    sprintf(temp, "%s: %s\r\n\r\n", p_response_map[i].key, p_response_map[i].value);
+    memcpy(&header[current_header_len - 2 /* \r\n */], temp, strlen(temp));
+    free(temp);
+  }
+
+  Seobeo_Handle_Queue(p_handle, (uint8_t*)header, strlen(header));
+  Seobeo_Handle_Queue(p_handle, (uint8_t*)body, body_length);  // Use actual body length
+  Seobeo_Handle_Flush(p_handle);
+}
+
+void Seobeo_Router_Destroy()
+{
+  if (g_routes == NULL)
+    return;
+
+  size_t route_count = Dowa_Array_Length(g_routes);
+  for (size_t i = 0; i < route_count; i++)
+  {
+    Seobeo_Route *route = &g_routes[i];
+    if (route->method) free(route->method);
+    if (route->path_pattern) free(route->path_pattern);
+    if (route->path_segments) Dowa_Array_Free(route->path_segments);
+    if (route->is_param) free(route->is_param);
+  }
+  Dowa_Array_Free(g_routes);
+  g_routes = NULL;
+}
+
+static char *Seobeo_Log_Level_String(Seobeo_Log_Level level)
+{
+  switch(level) {
+    case SEOBEO_DEBUG:   return "DEBUG";
+    case SEOBEO_INFO:    return "INFO";
+    case SEOBEO_WARNING: return "WARNING";
+    case SEOBEO_ERROR:   return "ERROR";
+    default:          return "INFO";
+  }
+}
+
+int Seobeo_Log(Seobeo_Log_Level level, const char * restrict format, ...)
+{
+  #ifndef SEOBEO_ENABLE_DEBUG
+  if (level == SEOBEO_DEBUG)
+    return 0;
+  #endif
+
+  int result;
+  va_list args;
+  printf("[%s] ", Seobeo_Log_Level_String(level));
+  va_start(args, format);
+  result = vprintf(format, args);
+  va_end(args);
+
+  return result;
+}
+
--- a/seobeo/seobeo.h	Fri Jan 02 19:11:35 2026 -0800
+++ b/seobeo/seobeo.h	Fri Jan 02 19:16:17 2026 -0800
@@ -8,13 +8,13 @@
  * Library for starting TCP, UDP server and serving static file or path.
  */
 
-// Define DIRECTORY before any includes to enable directory operations in dowa.h
-#ifndef DIRECTORY
-#define DIRECTORY
-#endif
+#include "seobeo/seobeo_internal.h"
 
-#include <stdio.h>
-#include <stdlib.h>
+/* Included in dowa
+  #include <stdio.h>
+  #include <stdlib.h>
+*/
+#include <stdarg.h>
 #include <unistd.h>
 #include <errno.h>
 #include <string.h>
@@ -27,10 +27,7 @@
 #include <signal.h>
 #include <fcntl.h>
 #include <pthread.h>
-#include <stdatomic.h>
-#include <stdbool.h>
 
-#include "seobeo/seobeo_internal.h"
 
 #define INITIAL_BUFFER_CAPACITY 4096
 
@@ -73,8 +70,6 @@
 extern int            Seobeo_Web_Server_Start(const char *folder_path, const char *port, Seobeo_ServerMode mode, int thread_count);
 /* Generic HTTP GET Rquest to given host and port with path. It will mimic chrome. */
 extern int            Seobeo_Web_Client_Get(const char *host, const char *port, const char *path);
-
-// --- Router --- //
 /* Initialize the router system (called automatically by Seobeo_Web_Server_Start) */
 extern void           Seobeo_Router_Init();
 /* Register an API route handler. Call before starting server. */
@@ -99,5 +94,13 @@
 /* Move to read_buffer to front and we already consumed the given amount in the handle. */
 extern void           Seobeo_Handle_Consume(Seobeo_Handle *p_handle, uint32 consumed);
 /* Assign IP4 or IP6 to sockaddr. TODO: Maybe create my own struct for this? */
-extern void           *Seobeo_Get_IP4_Or_IP6(struct sockaddr *sa);
+extern void          *Seobeo_Get_IP4_Or_IP6(struct sockaddr *sa);
+/* Logging */
+typedef enum {
+  SEOBEO_INFO = 0,
+  SEOBEO_WARNING,
+  SEOBEO_ERROR,
+  SEOBEO_DEBUG,
+} Seobeo_Log_Level;
+extern int            Seobeo_Log(Seobeo_Log_Level level, const char *format, ...);
 #endif
--- a/seobeo/seobeo_internal.h	Fri Jan 02 19:11:35 2026 -0800
+++ b/seobeo/seobeo_internal.h	Fri Jan 02 19:16:17 2026 -0800
@@ -11,10 +11,8 @@
 #define SSL_TYPE void
 #endif
 
-#ifndef DIRECTORY
-#define DIRECTORY
-#endif
 #include "dowa/dowa.h"
+#include <stdatomic.h>
 
 typedef enum {
   SEOBEO_STREAM_TYPE_SERVER,