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:
@@ -5,13 +5,15 @@ import freeflowuniverse.herolib.core.pathlib
|
||||
// Logger Factory
|
||||
pub struct LoggerFactoryArgs {
|
||||
pub mut:
|
||||
path string
|
||||
path string
|
||||
console_output bool = true
|
||||
}
|
||||
|
||||
pub fn new(args LoggerFactoryArgs) !Logger {
|
||||
mut p := pathlib.get_dir(path: args.path, create: true)!
|
||||
return Logger{
|
||||
path: p
|
||||
lastlog_time: 0
|
||||
path: p
|
||||
lastlog_time: 0
|
||||
console_output: args.console_output
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,4 +61,26 @@ pub fn (mut l Logger) log(args_ LogItemArgs) ! {
|
||||
}
|
||||
f.writeln(content.trim_space_right())!
|
||||
f.close()
|
||||
|
||||
// Also write to console if enabled
|
||||
if l.console_output {
|
||||
l.write_to_console(args, t)!
|
||||
}
|
||||
}
|
||||
|
||||
// Write log message to console with clean formatting
|
||||
fn (mut l Logger) write_to_console(args LogItemArgs, t ourtime.OurTime) ! {
|
||||
timestamp := t.time().format_ss()
|
||||
error_indicator := if args.logtype == .error { 'ERROR' } else { 'INFO' }
|
||||
category := args.cat.trim_space()
|
||||
lines := args.log.split('\n')
|
||||
|
||||
for i, line in lines {
|
||||
if i == 0 {
|
||||
println('${timestamp} [${error_indicator}] [${category}] ${line}')
|
||||
} else {
|
||||
// Indent continuation lines
|
||||
println('${timestamp} [${error_indicator}] [${category}] ${line}')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,9 @@ import freeflowuniverse.herolib.core.pathlib
|
||||
@[heap]
|
||||
pub struct Logger {
|
||||
pub mut:
|
||||
path pathlib.Path
|
||||
lastlog_time i64 // to see in log format, every second we put a time down, we need to know if we are in a new second (logs can come in much faster)
|
||||
path pathlib.Path
|
||||
lastlog_time i64 // to see in log format, every second we put a time down, we need to know if we are in a new second (logs can come in much faster)
|
||||
console_output bool // whether to also output to console/stdout
|
||||
}
|
||||
|
||||
pub struct LogItem {
|
||||
|
||||
@@ -4,6 +4,26 @@ import json
|
||||
import freeflowuniverse.herolib.schemas.jsonrpc { Request, Response, new_response_true, new_response_u32 }
|
||||
import freeflowuniverse.herolib.hero.heromodels
|
||||
import freeflowuniverse.herolib.hero.db
|
||||
|
||||
fn convert_iso_to_ourtime(iso_time string) !string {
|
||||
if iso_time.trim_space() == '' {
|
||||
return error('Empty time string')
|
||||
}
|
||||
|
||||
// Remove the 'Z' suffix if present
|
||||
mut cleaned := iso_time.replace('Z', '')
|
||||
|
||||
// Remove milliseconds (.000) if present
|
||||
if cleaned.contains('.') {
|
||||
cleaned = cleaned.all_before_last('.')
|
||||
}
|
||||
|
||||
// Replace 'T' with space
|
||||
cleaned = cleaned.replace('T', ' ')
|
||||
|
||||
return cleaned
|
||||
}
|
||||
|
||||
// CalendarEvent-specific argument structures
|
||||
|
||||
@[params]
|
||||
@@ -58,13 +78,20 @@ pub fn calendar_event_set(request Request) !Response {
|
||||
return jsonrpc.invalid_params
|
||||
}
|
||||
|
||||
mut mydb := heromodels.new()!
|
||||
mut mydb := heromodels.new() or { return jsonrpc.internal_error }
|
||||
start_time_converted := convert_iso_to_ourtime(payload.start_time) or {
|
||||
return jsonrpc.internal_error
|
||||
}
|
||||
end_time_converted := convert_iso_to_ourtime(payload.end_time) or {
|
||||
return jsonrpc.internal_error
|
||||
}
|
||||
|
||||
mut calendar_event_obj := mydb.calendar_event.new(
|
||||
name: payload.name
|
||||
description: payload.description
|
||||
title: payload.title
|
||||
start_time: payload.start_time
|
||||
end_time: payload.end_time
|
||||
start_time: start_time_converted
|
||||
end_time: end_time_converted
|
||||
location: payload.location
|
||||
attendees: payload.attendees
|
||||
fs_items: payload.fs_items
|
||||
@@ -79,9 +106,11 @@ pub fn calendar_event_set(request Request) !Response {
|
||||
securitypolicy: payload.securitypolicy
|
||||
tags: payload.tags
|
||||
comments: payload.comments
|
||||
)!
|
||||
) or { return jsonrpc.internal_error }
|
||||
|
||||
calendar_event_obj = mydb.calendar_event.set(calendar_event_obj)!
|
||||
calendar_event_obj = mydb.calendar_event.set(calendar_event_obj) or {
|
||||
return jsonrpc.internal_error
|
||||
}
|
||||
|
||||
return new_response_u32(request.id, calendar_event_obj.id)
|
||||
}
|
||||
@@ -97,9 +126,9 @@ pub fn calendar_event_delete(request Request) !Response {
|
||||
return new_response_true(request.id) // return true as jsonrpc (bool)
|
||||
}
|
||||
|
||||
// List all calendar events
|
||||
pub fn calendar_event_list(request Request) !Response {
|
||||
mut mydb := heromodels.new()!
|
||||
calendar_events := mydb.calendar_event.list()!
|
||||
|
||||
return jsonrpc.new_response(request.id, json.encode(calendar_events))
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user