veb.request_id: new middleware that implements request ID tracking (#23727)

This commit is contained in:
Anonymous User 2025-02-24 13:54:43 -08:00 committed by GitHub
parent f3d2eb1c24
commit ab2eb0016c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 407 additions and 0 deletions

View File

@ -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')
}
}
```

View File

@ -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
}
}
}

View File

@ -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 == ''
}