diff --git a/vlib/veb/request_id/README.md b/vlib/veb/request_id/README.md new file mode 100644 index 0000000000..ffc93cd7e5 --- /dev/null +++ b/vlib/veb/request_id/README.md @@ -0,0 +1,233 @@ +# Request ID Middleware + +This module implements request ID tracking functionality for V web applications. +Request IDs are unique identifiers assigned to each HTTP request, +which is essential for request tracing, debugging, and maintaining distributed systems. + +## Purpose + +Request IDs help in: +- Tracking requests across distributed systems +- Correlating logs from different services +- Debugging and troubleshooting +- Performance monitoring +- Request chain tracing + +## Usage + +To enable request ID tracking in your veb app, you must embed the `RequestIdContext` +struct in your `Context` struct. + +**Example:** + +```v +import veb +import veb.request_id + +pub struct Context { + veb.Context + request_id.RequestIdContext +} +``` + +### Basic Configuration + +Here's a simple configuration example: + +```v +import rand +import veb.request_id + +const request_id_config = request_id.Config{ + header: 'X-Request-ID' + generator: rand.uuid_v4 +} +``` + +### Middleware Setup + +Enable request ID tracking for all routes or specific routes using veb's middleware +system. + +**Example:** + +```v +import veb +import rand +import veb.request_id + +pub struct Context { + veb.Context + request_id.RequestIdContext +} + +pub struct App { + veb.Middleware[Context] +} + +const request_id_config = request_id.Config{ + header: 'X-Request-ID' + generator: rand.uuid_v4 +} + +fn main() { + mut app := &App{} + // Register the RequestID middleware with custom configuration + app.use(request_id.middleware[Context](request_id_config)) + veb.run[App, Context](mut app, 8080) +} +``` + +### Accessing the Request ID + +You can access the request ID in your route handlers: + +```v okfmt +import veb +import veb.request_id + +fn (app &App) handler(mut ctx Context) veb.Result { + // Get the current request ID + request_id := ctx.request_id + // Use the request ID for logging, etc. + return ctx.text('Request ID: ${request_id}') +} +``` + +## Configuration Options + +The `Config` struct provides several configuration options: + +```v okfmt +pub struct Config { +pub: + next ?fn (ctx &veb.Context) bool + generator fn () string = rand.uuid_v4 + header string = 'X-Request-ID' + allow_empty bool + force bool +} +``` + +### Configuration Options Explained + +- `next`: Optional function to conditionally skip the middleware +- `generator`: Function to generate unique IDs (defaults to UUID v4) +- `header`: HTTP header name for the request ID (defaults to "X-Request-ID") +- `allow_empty`: Whether to allow empty request IDs +- `force`: Whether to generate a new ID even when one already exists + +## Advanced Usage + +### Custom ID Generator + +You can provide your own ID generator function: + +```v +import rand +import veb.request_id + +fn custom_id_generator() string { + return 'custom-prefix-${rand.uuid_v4()}' +} + +config := request_id.Config{ + generator: custom_id_generator +} +``` + +### Conditional Middleware Execution + +Use the `next` function to skip the middleware based on custom logic: + +```v +import veb +import rand +import veb.request_id + +config := request_id.Config{ + next: fn (ctx &veb.Context) bool { + // Skip for health check endpoints + return ctx.req.url.starts_with('/health') + } +} +``` + +### Forcing New IDs + +When you want to ensure a new ID is generated regardless of existing headers: + +```v +import veb.request_id + +config := request_id.Config{ + force: true +} +``` + +## Best Practices + +1. **Consistent Headers**: Use consistent header names across your services +2. **ID Propagation**: Forward request IDs to downstream services +3. **Logging Integration**: Include request IDs in your logging system +4. **ID Format**: Use a reliable ID generator (UUID v4 is recommended) + +## Security Considerations + +While request IDs are not security features, consider these points: +- Don't include sensitive information in request IDs +- Validate request ID format if using custom generators +- Be cautious with request ID length (recommended: 8-128 characters) + +## Examples + +### Basic Integration + +```v +module main + +import veb +import veb.request_id + +pub struct Context { + veb.Context + request_id.RequestIdContext +} + +pub struct App { + veb.Middleware[Context] +} + +@['/request-id'; get] +pub fn (app &App) index(mut ctx Context) veb.Result { + return ctx.text('Current request ID: ${ctx.request_id}') +} + +fn main() { + mut app := &App{} + config := request_id.Config{ + header: 'X-Request-ID' + force: false + allow_empty: false + } + app.use(request_id.middleware[Context](config)) + veb.run[App, Context](mut app, 8080) +} +``` + +### With Custom Generator and Conditional Execution + +```v +import veb +import rand +import veb.request_id + +config := request_id.Config{ + generator: fn () string { + return 'app-${rand.uuid_v4()}' + } + next: fn (ctx &veb.Context) bool { + return ctx.req.url.starts_with('/public') + } +} +``` \ No newline at end of file diff --git a/vlib/veb/request_id/request_id.v b/vlib/veb/request_id/request_id.v new file mode 100644 index 0000000000..40f6541732 --- /dev/null +++ b/vlib/veb/request_id/request_id.v @@ -0,0 +1,74 @@ +module request_id + +import veb +import rand + +@[params] +pub struct Config { +pub: + // Next defines a function to skip this middleware when returned true. + next ?fn (ctx &veb.Context) bool + // Generator defines a function to generate the unique identifier. + generator fn () string = rand.uuid_v4 + // Header is the header key where to get/set the unique request ID. + header string = 'X-Request-ID' + // Allow empty sets whether to allow empty request IDs + allow_empty bool + // Force determines whether to always generate a new ID even if one exists + force bool +} + +@[noinit] +pub struct RequestIdContext { +pub mut: + request_id_config Config + request_id_exempt bool + request_id string +} + +// get_request_id returns the current request ID +pub fn (ctx &RequestIdContext) get_request_id() string { + return ctx.request_id +} + +// middleware returns a handler that you can use with veb's middleware +pub fn middleware[T](config Config) veb.MiddlewareOptions[T] { + return veb.MiddlewareOptions[T]{ + after: false + handler: fn [config] [T](mut ctx T) bool { + if ctx.request_id_exempt { + return true + } + + // Don't execute middleware if Next returns true. + if next := config.next { + if next(ctx) { + return true + } + } + + // Get existing ID from request + mut rid := if !config.force { + ctx.get_custom_header(config.header) or { '' } + } else { + '' + } + + // Generate new ID if needed + if rid == '' || config.force { + rid = config.generator() + } + + // Set ID to response header if we have one + if rid != '' { + ctx.set_custom_header(config.header, rid) + ctx.request_id = rid + } + + // Store config + ctx.request_id_config = config + + return true + } + } +} diff --git a/vlib/veb/request_id/request_id_test.v b/vlib/veb/request_id/request_id_test.v new file mode 100644 index 0000000000..544f867e59 --- /dev/null +++ b/vlib/veb/request_id/request_id_test.v @@ -0,0 +1,100 @@ +module request_id + +import veb + +// Test context that includes our RequestIdContext +struct TestContext { + veb.Context + RequestIdContext +} + +fn test_config_default() { + default_config := Config{} + assert default_config.header == 'X-Request-ID' + assert default_config.allow_empty == false + assert default_config.force == false + assert default_config.next == none + assert default_config.generator != unsafe { nil } +} + +fn test_middleware_handler() { + cfg := Config{ + header: 'Test-Request-ID' + generator: fn () string { + return 'test-123' + } + } + + // Create middleware handler + handler := middleware[TestContext](cfg).handler + + // Create test context + mut ctx := TestContext{} + + // Test handler execution + result := handler(mut ctx) + assert result == true + + // Verify request ID was set + assert ctx.request_id == 'test-123' +} + +fn test_middleware_next_function() { + mut ctx := TestContext{} + + // Test with next function that returns true + skip_cfg := Config{ + next: fn (ctx &veb.Context) bool { + return true + } + } + skip_handler := middleware[TestContext](skip_cfg).handler + + result := skip_handler(mut ctx) + assert result == true + assert ctx.request_id == '' + + // Test with next function that returns false + continue_cfg := Config{ + next: fn (ctx &veb.Context) bool { + return false + } + generator: fn () string { + return 'test-123' + } + } + continue_handler := middleware[TestContext](continue_cfg).handler + + result2 := continue_handler(mut ctx) + assert result2 == true + assert ctx.request_id == 'test-123' +} + +fn test_middleware_force_option() { + mut ctx := TestContext{} + ctx.request_id = 'existing-id' + + force_cfg := Config{ + force: true + generator: fn () string { + return 'forced-id' + } + } + force_handler := middleware[TestContext](force_cfg).handler + + result := force_handler(mut ctx) + assert result == true + assert ctx.request_id == 'forced-id' +} + +fn test_middleware_exempt() { + mut ctx := TestContext{ + request_id_exempt: true + } + + handler := middleware[TestContext](Config{}).handler + + result := handler(mut ctx) + assert result == true + assert ctx.request_id == '' +}