feat: Enhance logging and CORS handling

- Add console output option to logger
- Implement ISO time conversion for calendar events
- Add OPTIONS method for API and root handlers
- Introduce health check endpoint with uptime and server info
- Implement manual CORS handling in `before_request`
- Add `start_time` to HeroServer for uptime tracking
- Add `ServerLogParams` and `log` method for server logging
This commit is contained in:
Mahmoud-Emad
2025-09-18 17:29:11 +03:00
parent b83aa75e9d
commit f54c57847a
8 changed files with 300 additions and 26 deletions

View File

@@ -1,6 +1,7 @@
module heroserver
import json
import net.http
import veb
import freeflowuniverse.herolib.schemas.jsonrpc
@@ -36,8 +37,41 @@ pub fn (mut server HeroServer) auth_handler(mut ctx Context, action string) !veb
}
}
@['/api/:handler_type'; post]
@['/api/:handler_type'; options; post]
pub fn (mut server HeroServer) api_handler(mut ctx Context, handler_type string) veb.Result {
if ctx.req.method == http.Method.options {
if server.cors_enabled {
origin := ctx.get_header(.origin) or { '' }
if origin != ''
&& (server.allowed_origins.contains('*') || server.allowed_origins.contains(origin)) {
ctx.set_header(.access_control_allow_origin, origin)
ctx.set_header(.access_control_allow_methods, 'GET, HEAD, PATCH, PUT, POST, DELETE, OPTIONS')
ctx.set_header(.access_control_allow_headers, 'Content-Type, Authorization, X-Requested-With')
ctx.set_header(.access_control_allow_credentials, 'true')
ctx.set_header(.vary, 'Origin')
server.log(
message: 'CORS headers set for origin: ${origin}'
)
}
}
return ctx.text('')
}
// Set CORS headers for POST response
if server.cors_enabled {
origin := ctx.get_header(.origin) or { '' }
if origin != ''
&& (server.allowed_origins.contains('*') || server.allowed_origins.contains(origin)) {
ctx.set_header(.access_control_allow_origin, origin)
ctx.set_header(.access_control_allow_credentials, 'true')
ctx.set_header(.vary, 'Origin')
server.log(
message: 'CORS headers set for API POST response, origin: ${origin}'
)
}
}
// TODO: For now, skip authentication for testing
// session_key := ctx.get_header(.authorization) or {
// return ctx.request_error('Missing session key in Authorization header')
@@ -61,6 +95,6 @@ pub fn (mut server HeroServer) api_handler(mut ctx Context, handler_type string)
// Handle the request using the OpenRPC handler
response := handler.handle(request) or { return ctx.server_error('Handler error: ${err}') }
// Return the JSON-RPC response
return ctx.json(response)
ctx.set_header(.content_type, 'application/json')
return ctx.text(response.encode())
}

View File

@@ -1,12 +1,113 @@
module heroserver
import veb
import net.http
import freeflowuniverse.herolib.schemas.openrpc
import freeflowuniverse.herolib.schemas.jsonschema
import freeflowuniverse.herolib.schemas.jsonrpc
import time
// Home page handler - returns HTML homepage
@['/']
// Home page handler - returns HTML homepage for GET, handles JSON-RPC for POST
@['/'; get; options; post]
pub fn (mut server HeroServer) home_handler(mut ctx Context) veb.Result {
server.log(
message: 'New request: ${ctx.req.method} /'
)
// Handle CORS preflight OPTIONS request
if ctx.req.method == http.Method.options {
server.log(
message: 'Handling OPTIONS preflight request for root'
)
// Ensure CORS headers are set for OPTIONS response
if server.cors_enabled {
origin := ctx.get_header(.origin) or { '' }
if origin != ''
&& (server.allowed_origins.contains('*') || server.allowed_origins.contains(origin)) {
ctx.set_header(.access_control_allow_origin, origin)
ctx.set_header(.access_control_allow_methods, 'GET, HEAD, PATCH, PUT, POST, DELETE, OPTIONS')
ctx.set_header(.access_control_allow_headers, 'Content-Type, Authorization, X-Requested-With')
ctx.set_header(.access_control_allow_credentials, 'true')
ctx.set_header(.vary, 'Origin')
server.log(
message: 'CORS headers set for origin: ${origin}'
)
}
}
return ctx.text('')
}
// Handle POST requests as JSON-RPC
if ctx.req.method == http.Method.post {
server.log(
message: 'Handling JSON-RPC request at root endpoint'
)
// Set CORS headers for POST response
if server.cors_enabled {
origin := ctx.get_header(.origin) or { '' }
if origin != ''
&& (server.allowed_origins.contains('*') || server.allowed_origins.contains(origin)) {
ctx.set_header(.access_control_allow_origin, origin)
ctx.set_header(.access_control_allow_credentials, 'true')
ctx.set_header(.vary, 'Origin')
server.log(
message: 'CORS headers set for POST response, origin: ${origin}'
)
}
}
// Check if we have handlers
if server.handlers.len == 0 {
return ctx.server_error('No handlers registered')
}
// Use the first registered handler for root requests
handler_name := server.handlers.keys()[0]
server.log(
message: 'Using handler: ${handler_name}'
)
mut handler := server.handlers[handler_name] or {
return ctx.request_error('Handler not found: ${handler_name}')
}
// Parse JSON-RPC request
request := jsonrpc.decode_request(ctx.req.data) or {
server.log(
message: 'Invalid JSON-RPC request: ${err}'
level: .error
)
return ctx.request_error('Invalid JSON-RPC request: ${err}')
}
server.log(
message: 'JSON-RPC method: ${request.method}'
)
// Handle the request using the OpenRPC handler
response := handler.handle(request) or {
server.log(
message: 'Handler error: ${err}'
level: .error
)
return ctx.server_error('Handler error: ${err}')
}
server.log(
message: 'JSON-RPC response sent'
)
ctx.set_header(.content_type, 'application/json')
return ctx.text(response.encode())
}
// Handle GET requests as HTML homepage
server.log(
message: 'Serving HTML homepage'
)
// Create a simple server info structure for the template
server_info := HomePageData{
base_url: get_base_url_from_context(ctx)
@@ -22,6 +123,42 @@ pub fn (mut server HeroServer) home_handler(mut ctx Context) veb.Result {
return ctx.html(html_content)
}
// Health check endpoint
@['/health'; get]
pub fn (mut server HeroServer) health_handler(mut ctx Context) veb.Result {
server.log(
message: 'Health check requested'
)
// Create health status response
current_time := time.now().unix()
uptime := current_time - server.start_time
health_status := {
'status': 'healthy'
'timestamp': current_time.str()
'version': '1.0.0'
'handlers_count': server.handlers.len.str()
'auth_enabled': server.auth_enabled.str()
'cors_enabled': server.cors_enabled.str()
'uptime_seconds': uptime.str()
}
// Set CORS headers if enabled
if server.cors_enabled {
origin := ctx.get_header(.origin) or { '' }
if origin != ''
&& (server.allowed_origins.contains('*') || server.allowed_origins.contains(origin)) {
ctx.set_header(.access_control_allow_origin, origin)
ctx.set_header(.access_control_allow_credentials, 'true')
ctx.set_header(.vary, 'Origin')
}
}
ctx.set_header(.content_type, 'application/json')
return ctx.json(health_status)
}
// JSON server info handler
@['/json/:handler_type']
pub fn (mut server HeroServer) json_handler(mut ctx Context, handler_type string) veb.Result {

View File

@@ -3,6 +3,8 @@ module heroserver
import freeflowuniverse.herolib.crypt.herocrypt
import freeflowuniverse.herolib.schemas.openrpc
import freeflowuniverse.herolib.ui.console
import freeflowuniverse.herolib.core.logger
import time
import veb
// Create a new HeroServer instance
@@ -14,6 +16,12 @@ pub fn new(config HeroServerConfig) !&HeroServer {
herocrypt.new_default()!
}
// Create logger with configurable output
mut server_logger := logger.new(
path: config.log_path
console_output: config.console_output
) or { return error('Failed to create logger: ${err}') }
mut server := &HeroServer{
port: config.port
host: config.host
@@ -24,6 +32,8 @@ pub fn new(config HeroServerConfig) !&HeroServer {
auth_enabled: config.auth_enabled
cors_enabled: config.cors_enabled
allowed_origins: config.allowed_origins.clone()
logger: server_logger
start_time: time.now().unix()
}
console.print_header('HeroServer created on port ${server.port}')
@@ -38,15 +48,8 @@ pub fn (mut server HeroServer) register_handler(handler_type string, handler &op
// Start the server
pub fn (mut server HeroServer) start() ! {
// Configure CORS if enabled
if server.cors_enabled {
console.print_item('CORS enabled for origins: ${server.allowed_origins}')
server.use(veb.cors[Context](veb.CorsOptions{
origins: server.allowed_origins
allowed_methods: [.get, .head, .patch, .put, .post, .delete, .options]
allowed_headers: ['Content-Type', 'Authorization', 'X-Requested-With']
allow_credentials: true
}))
}
// Start VEB server

View File

@@ -2,6 +2,7 @@ module heroserver
import freeflowuniverse.herolib.crypt.herocrypt
import freeflowuniverse.herolib.schemas.openrpc
import freeflowuniverse.herolib.core.logger
import time
import veb
@@ -9,9 +10,13 @@ import veb
@[params]
pub struct HeroServerConfig {
pub mut:
port int = 9977
host string = 'localhost'
auth_enabled bool = true // Whether to enable authentication
port int = 9977
host string = 'localhost'
log_path string = '/tmp/heroserver_logs'
console_output bool = true // Enable console logging by default
// flags
auth_enabled bool = true // Whether to enable authentication
// CORS configuration
cors_enabled bool = true // Whether to enable CORS
allowed_origins []string = ['*'] // Allowed origins for CORS, default allows all
@@ -31,10 +36,33 @@ mut:
challenges map[string]AuthChallenge
cors_enabled bool
allowed_origins []string
logger logger.Logger // Logger instance with dual output
start_time i64 // Server start timestamp for uptime calculation
pub mut:
auth_enabled bool = true // Whether authentication is required
}
// Convenient logging method for the server
@[params]
pub struct ServerLogParams {
pub:
message string
level logger.LogType = .stdout // Default to info level
cat string = 'server' // Default category
}
// Log a message using the server's logger
pub fn (mut server HeroServer) log(params ServerLogParams) {
server.logger.log(
cat: params.cat
log: params.message
logtype: params.level
) or {
// Fallback to console if logging fails
println('[${params.cat}] ${params.message}')
}
}
// Authentication challenge data
pub struct AuthChallenge {
pub mut:
@@ -169,3 +197,21 @@ pub:
pub struct Context {
veb.Context
}
// before_request is called before every request
pub fn (mut server HeroServer) before_request(mut ctx Context) {
// Handle CORS manually
if server.cors_enabled {
origin := ctx.get_header(.origin) or { '' }
// Check if origin is allowed
if origin != ''
&& (server.allowed_origins.contains('*') || server.allowed_origins.contains(origin)) {
ctx.set_header(.access_control_allow_origin, origin)
ctx.set_header(.access_control_allow_methods, 'GET, HEAD, PATCH, PUT, POST, DELETE, OPTIONS')
ctx.set_header(.access_control_allow_headers, 'Content-Type, Authorization, X-Requested-With')
ctx.set_header(.access_control_allow_credentials, 'true')
ctx.set_header(.vary, 'Origin')
}
}
}