做完了一部分,备份一下,防止以外删除
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
# Data Model: Fiber Middleware Integration
|
||||
|
||||
**Feature**: 001-fiber-middleware-integration
|
||||
**Date**: 2025-11-10
|
||||
**Feature**: 001-fiber-middleware-integration
|
||||
**Date**: 2025-11-10
|
||||
**Phase**: 1 - Design & Contracts
|
||||
|
||||
## Overview
|
||||
@@ -162,9 +162,9 @@ middleware:
|
||||
|
||||
### AuthToken Entity
|
||||
|
||||
**Storage**: Redis key-value pair
|
||||
**Key Format**: `auth:token:{token_string}` (generated via `constants.RedisAuthTokenKey()`)
|
||||
**Value Format**: Plain string containing user ID
|
||||
**Storage**: Redis key-value pair
|
||||
**Key Format**: `auth:token:{token_string}` (generated via `constants.RedisAuthTokenKey()`)
|
||||
**Value Format**: Plain string containing user ID
|
||||
**TTL**: Managed by Redis (set when token is created)
|
||||
|
||||
```go
|
||||
@@ -218,16 +218,16 @@ func RedisAuthTokenKey(token string) string {
|
||||
// Access via: c.Locals("key")
|
||||
|
||||
// Request ID (set by requestid middleware)
|
||||
requestID := c.Locals("requestid").(string) // UUID v4 string
|
||||
requestID := c.Locals(constants.ContextKeyRequestID).(string) // UUID v4 string
|
||||
|
||||
// User ID (set by keyauth middleware after validation)
|
||||
userID, ok := c.Locals("user_id").(string)
|
||||
userID, ok := c.Locals(constants.ContextKeyUserID).(string)
|
||||
if !ok {
|
||||
// Not authenticated
|
||||
}
|
||||
|
||||
// Start time (for duration calculation)
|
||||
startTime := c.Locals("start_time").(time.Time)
|
||||
startTime := c.Locals(constants.ContextKeyStartTime).(time.Time)
|
||||
```
|
||||
|
||||
**Context Keys** (constants in `pkg/constants/constants.go`):
|
||||
@@ -254,8 +254,8 @@ const (
|
||||
|
||||
### Application Log Entry
|
||||
|
||||
**Format**: JSON
|
||||
**Output**: `logs/app.log`
|
||||
**Format**: JSON
|
||||
**Output**: `logs/app.log`
|
||||
**Logger**: Zap (appLogger instance)
|
||||
|
||||
```json
|
||||
@@ -287,17 +287,21 @@ const (
|
||||
```go
|
||||
appLogger.Info("User created successfully",
|
||||
zap.String("request_id", requestID),
|
||||
zap.String("user_id", userID),
|
||||
zap.String(constants.ContextKeyUserID, userID),
|
||||
zap.String("username", username),
|
||||
zap.String("ip", ip),
|
||||
)
|
||||
```
|
||||
|
||||
### Access Log Entry
|
||||
### Access Log Entry Format
|
||||
|
||||
**Format**: JSON
|
||||
**Output**: `logs/access.log`
|
||||
**Purpose**: Records all HTTP requests for audit trail, performance monitoring, and troubleshooting
|
||||
**Format**: JSON (one entry per line)
|
||||
**Output**: `logs/access.log`
|
||||
**Logger**: Zap (accessLogger instance)
|
||||
**Requirement**: Implements spec.md FR-011
|
||||
|
||||
**Complete JSON Schema**:
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -309,22 +313,32 @@ appLogger.Info("User created successfully",
|
||||
"duration_ms": 45.234,
|
||||
"request_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"ip": "192.168.1.100",
|
||||
"user_agent": "Mozilla/5.0...",
|
||||
"user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)...",
|
||||
"user_id": "user-789"
|
||||
}
|
||||
```
|
||||
|
||||
**Fields**:
|
||||
- `timestamp`: ISO 8601 format (RFC3339)
|
||||
- `level`: Always "info" for access logs
|
||||
- `method`: HTTP method (GET, POST, PUT, DELETE)
|
||||
- `path`: Request path
|
||||
- `status`: HTTP status code
|
||||
- `duration_ms`: Request duration in milliseconds
|
||||
- `request_id`: Request correlation ID
|
||||
- `ip`: Client IP address
|
||||
- `user_agent`: User-Agent header
|
||||
- `user_id`: Authenticated user ID (if available, empty if not)
|
||||
**Field Definitions**:
|
||||
|
||||
| Field | Type | Required | Description | Example |
|
||||
|-------|------|----------|-------------|---------|
|
||||
| `timestamp` | string | Yes | ISO 8601 timestamp (RFC3339 with milliseconds) | `2025-11-10T15:30:45.123Z` |
|
||||
| `level` | string | Yes | Log level (always "info" for access logs) | `info` |
|
||||
| `method` | string | Yes | HTTP method | `GET`, `POST`, `PUT`, `DELETE`, `PATCH` |
|
||||
| `path` | string | Yes | Request path (including query params) | `/api/v1/users?page=1` |
|
||||
| `status` | int | Yes | HTTP status code | `200`, `401`, `500` |
|
||||
| `duration_ms` | float64 | Yes | Request processing duration in milliseconds | `45.234` |
|
||||
| `request_id` | string | Yes | UUID v4 request identifier | `550e8400-e29b-41d4-a716-446655440000` |
|
||||
| `ip` | string | Yes | Client IP address | `192.168.1.100` |
|
||||
| `user_agent` | string | Yes | User-Agent header value | `Mozilla/5.0...` |
|
||||
| `user_id` | string | No | Authenticated user ID (empty string if not authenticated) | `user-789` or `""` |
|
||||
|
||||
**Notes**:
|
||||
- All access logs are written at "info" level
|
||||
- `user_id` field is empty string (`""`) for unauthenticated requests
|
||||
- `duration_ms` includes full middleware chain execution time
|
||||
- `path` includes query parameters for complete request tracking
|
||||
- Logged after response is sent (includes actual status code)
|
||||
|
||||
**Zap Usage**:
|
||||
|
||||
@@ -337,7 +351,7 @@ accessLogger.Info("",
|
||||
zap.String("request_id", requestID),
|
||||
zap.String("ip", c.IP()),
|
||||
zap.String("user_agent", c.Get("User-Agent")),
|
||||
zap.String("user_id", userID),
|
||||
zap.String(constants.ContextKeyUserID, userID),
|
||||
)
|
||||
```
|
||||
|
||||
@@ -531,7 +545,7 @@ func Wrap(code int, message string, err error) *AppError {
|
||||
|
||||
### Rate Limit Tracking
|
||||
|
||||
**Storage**: In-memory (default) or Redis (for distributed)
|
||||
**Storage**: In-memory (default) or Redis (for distributed)
|
||||
**Managed by**: Fiber limiter middleware (internal storage)
|
||||
|
||||
```go
|
||||
@@ -625,16 +639,16 @@ func RedisRateLimitKey(ip string) string {
|
||||
2. Recover Middleware (catch panics)
|
||||
↓
|
||||
3. RequestID Middleware (generate UUID v4)
|
||||
→ Store in c.Locals("requestid")
|
||||
→ Store in c.Locals(constants.ContextKeyRequestID)
|
||||
↓
|
||||
4. Logger Middleware (start)
|
||||
→ Store start_time in c.Locals("start_time")
|
||||
→ Store start_time in c.Locals(constants.ContextKeyStartTime)
|
||||
↓
|
||||
5. KeyAuth Middleware
|
||||
→ Extract token from header
|
||||
→ Call TokenValidator.Validate(token)
|
||||
→ Validate with Redis: GET auth:token:{token}
|
||||
→ If valid: Store user_id in c.Locals("user_id")
|
||||
→ If valid: Store user_id in c.Locals(constants.ContextKeyUserID)
|
||||
→ If invalid: Return 401 with error code
|
||||
→ If Redis down: Return 503 with error code
|
||||
↓
|
||||
@@ -643,7 +657,7 @@ func RedisRateLimitKey(ip string) string {
|
||||
→ If exceeded: Return 429 with error code
|
||||
↓
|
||||
7. Handler (business logic)
|
||||
→ Access c.Locals("requestid"), c.Locals("user_id")
|
||||
→ Access c.Locals(constants.ContextKeyRequestID), c.Locals(constants.ContextKeyUserID)
|
||||
→ Process request
|
||||
→ Return response via response.Success() or response.Error()
|
||||
↓
|
||||
@@ -676,3 +690,142 @@ func RedisRateLimitKey(ip string) string {
|
||||
- ✅ No Java-style patterns (no I-prefix, no Impl-suffix)
|
||||
|
||||
**Next**: Generate API contracts (OpenAPI specification)
|
||||
|
||||
---
|
||||
|
||||
## 8. Configuration Validation Rules
|
||||
|
||||
This section defines comprehensive validation constraints for all configuration fields, implementing the requirements in spec.md FR-003.
|
||||
|
||||
### Validation Error Format
|
||||
|
||||
All validation errors MUST follow this format:
|
||||
```
|
||||
"Invalid configuration: {field_path}: {error_reason} (current value: {value}, expected: {constraint})"
|
||||
```
|
||||
|
||||
Example:
|
||||
```
|
||||
"Invalid configuration: server.read_timeout: duration out of range (current value: 1s, expected: 5s-300s)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Server Configuration Validation
|
||||
|
||||
| Field | Type | Required | Constraint | Default | Error Message |
|
||||
|-------|------|----------|------------|---------|---------------|
|
||||
| `server.address` | string | Yes | Non-empty, format `:PORT` or `HOST:PORT` | `:3000` | "server.address: must be non-empty and in format ':PORT' or 'HOST:PORT'" |
|
||||
| `server.read_timeout` | duration | Yes | 5s - 300s | `10s` | "server.read_timeout: duration out of range (expected: 5s-300s)" |
|
||||
| `server.write_timeout` | duration | Yes | 5s - 300s | `10s` | "server.write_timeout: duration out of range (expected: 5s-300s)" |
|
||||
| `server.shutdown_timeout` | duration | Yes | 10s - 120s | `30s` | "server.shutdown_timeout: duration out of range (expected: 10s-120s)" |
|
||||
| `server.prefork` | bool | No | true or false | `false` | "server.prefork: must be boolean (true/false)" |
|
||||
|
||||
---
|
||||
|
||||
### Redis Configuration Validation
|
||||
|
||||
| Field | Type | Required | Constraint | Default | Error Message |
|
||||
|-------|------|----------|------------|---------|---------------|
|
||||
| `redis.address` | string | Yes | Non-empty, format `HOST:PORT` | `localhost:6379` | "redis.address: must be non-empty and in format 'HOST:PORT'" |
|
||||
| `redis.password` | string | No | Any string (empty allowed) | `""` | N/A |
|
||||
| `redis.db` | int | No | 0 - 15 | `0` | "redis.db: database number out of range (expected: 0-15)" |
|
||||
| `redis.pool_size` | int | No | 1 - 1000 | `10` | "redis.pool_size: pool size out of range (expected: 1-1000)" |
|
||||
| `redis.min_idle_conns` | int | No | 0 - pool_size | `5` | "redis.min_idle_conns: must be 0 to pool_size" |
|
||||
| `redis.dial_timeout` | duration | No | 1s - 30s | `5s` | "redis.dial_timeout: timeout out of range (expected: 1s-30s)" |
|
||||
| `redis.read_timeout` | duration | No | 1s - 30s | `3s` | "redis.read_timeout: timeout out of range (expected: 1s-30s)" |
|
||||
| `redis.write_timeout` | duration | No | 1s - 30s | `3s` | "redis.write_timeout: timeout out of range (expected: 1s-30s)" |
|
||||
|
||||
---
|
||||
|
||||
### Logging Configuration Validation
|
||||
|
||||
| Field | Type | Required | Constraint | Default | Error Message |
|
||||
|-------|------|----------|------------|---------|---------------|
|
||||
| `logging.level` | string | No | One of: debug, info, warn, error | `info` | "logging.level: invalid log level (expected: debug, info, warn, error)" |
|
||||
| `logging.development` | bool | No | true or false | `false` | "logging.development: must be boolean (true/false)" |
|
||||
|
||||
#### App Log Validation
|
||||
|
||||
| Field | Type | Required | Constraint | Default | Error Message |
|
||||
|-------|------|----------|------------|---------|---------------|
|
||||
| `logging.app_log.filename` | string | Yes | Non-empty, valid file path | `logs/app.log` | "logging.app_log.filename: must be non-empty valid file path" |
|
||||
| `logging.app_log.max_size` | int | No | 1 - 1000 (MB) | `100` | "logging.app_log.max_size: size out of range (expected: 1-1000 MB)" |
|
||||
| `logging.app_log.max_backups` | int | No | 0 - 999 (0 = keep all) | `30` | "logging.app_log.max_backups: count out of range (expected: 0-999)" |
|
||||
| `logging.app_log.max_age` | int | No | 1 - 365 (days) | `30` | "logging.app_log.max_age: retention period out of range (expected: 1-365 days)" |
|
||||
| `logging.app_log.compress` | bool | No | true or false | `true` | "logging.app_log.compress: must be boolean (true/false)" |
|
||||
|
||||
#### Access Log Validation
|
||||
|
||||
| Field | Type | Required | Constraint | Default | Error Message |
|
||||
|-------|------|----------|------------|---------|---------------|
|
||||
| `logging.access_log.filename` | string | Yes | Non-empty, valid file path | `logs/access.log` | "logging.access_log.filename: must be non-empty valid file path" |
|
||||
| `logging.access_log.max_size` | int | No | 1 - 1000 (MB) | `500` | "logging.access_log.max_size: size out of range (expected: 1-1000 MB)" |
|
||||
| `logging.access_log.max_backups` | int | No | 0 - 999 (0 = keep all) | `90` | "logging.access_log.max_backups: count out of range (expected: 0-999)" |
|
||||
| `logging.access_log.max_age` | int | No | 1 - 365 (days) | `90` | "logging.access_log.max_age: retention period out of range (expected: 1-365 days)" |
|
||||
| `logging.access_log.compress` | bool | No | true or false | `true` | "logging.access_log.compress: must be boolean (true/false)" |
|
||||
|
||||
---
|
||||
|
||||
### Middleware Configuration Validation
|
||||
|
||||
| Field | Type | Required | Constraint | Default | Error Message |
|
||||
|-------|------|----------|------------|---------|---------------|
|
||||
| `middleware.enable_auth` | bool | No | true or false | `true` | "middleware.enable_auth: must be boolean (true/false)" |
|
||||
| `middleware.enable_rate_limiter` | bool | No | true or false | `false` | "middleware.enable_rate_limiter: must be boolean (true/false)" |
|
||||
|
||||
#### Rate Limiter Validation
|
||||
|
||||
| Field | Type | Required | Constraint | Default | Error Message |
|
||||
|-------|------|----------|------------|---------|---------------|
|
||||
| `middleware.rate_limiter.max` | int | No | 1 - 10000 | `30` | "middleware.rate_limiter.max: request limit out of range (expected: 1-10000)" |
|
||||
| `middleware.rate_limiter.expiration` | duration | No | 1s - 1h | `1m` | "middleware.rate_limiter.expiration: window duration out of range (expected: 1s-1h)" |
|
||||
| `middleware.rate_limiter.storage` | string | No | "memory" or "redis" | `memory` | "middleware.rate_limiter.storage: invalid storage type (expected: memory or redis)" |
|
||||
|
||||
---
|
||||
|
||||
### Validation Implementation Guidelines
|
||||
|
||||
**Location**: `pkg/config/loader.go` - `Validate()` function
|
||||
|
||||
**Validation Order**:
|
||||
1. **Required fields check** - Fail fast if critical fields missing
|
||||
2. **Type validation** - Viper handles basic type conversion
|
||||
3. **Range validation** - Check numeric bounds
|
||||
4. **Format validation** - Validate string formats (HOST:PORT, file paths)
|
||||
5. **Cross-field validation** - Check dependencies (e.g., min_idle_conns <= pool_size)
|
||||
|
||||
**Example Implementation Pattern**:
|
||||
|
||||
```go
|
||||
func (c *Config) Validate() error {
|
||||
// Required fields
|
||||
if c.Server.Address == "" {
|
||||
return fmt.Errorf("Invalid configuration: server.address: must be non-empty (current value: empty)")
|
||||
}
|
||||
|
||||
// Range validation
|
||||
if c.Server.ReadTimeout < 5*time.Second || c.Server.ReadTimeout > 300*time.Second {
|
||||
return fmt.Errorf("Invalid configuration: server.read_timeout: duration out of range (current value: %s, expected: 5s-300s)", c.Server.ReadTimeout)
|
||||
}
|
||||
|
||||
// Format validation
|
||||
if !strings.Contains(c.Redis.Address, ":") {
|
||||
return fmt.Errorf("Invalid configuration: redis.address: invalid format (current value: %s, expected: HOST:PORT)", c.Redis.Address)
|
||||
}
|
||||
|
||||
// Cross-field validation
|
||||
if c.Redis.MinIdleConns > c.Redis.PoolSize {
|
||||
return fmt.Errorf("Invalid configuration: redis.min_idle_conns: must not exceed pool_size (current value: %d, pool_size: %d)", c.Redis.MinIdleConns, c.Redis.PoolSize)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
**Testing Requirements**:
|
||||
- Unit tests MUST cover all validation rules (T020 in tasks.md)
|
||||
- Test valid configurations (should pass)
|
||||
- Test each constraint violation (should fail with correct error message)
|
||||
- Test edge cases (min/max boundaries)
|
||||
- Test malformed YAML (should be caught by Viper)
|
||||
|
||||
Reference in New Issue
Block a user