mirror of
https://github.com/vlang/v.git
synced 2025-09-09 07:15:50 -04:00
veb.request_id: new middleware that implements request ID tracking (#23727)
This commit is contained in:
parent
f3d2eb1c24
commit
ab2eb0016c
233
vlib/veb/request_id/README.md
Normal file
233
vlib/veb/request_id/README.md
Normal 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')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
74
vlib/veb/request_id/request_id.v
Normal file
74
vlib/veb/request_id/request_id.v
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
100
vlib/veb/request_id/request_id_test.v
Normal file
100
vlib/veb/request_id/request_id_test.v
Normal 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 == ''
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user