docs consolidation and others
This commit is contained in:
370
docs/modules/router/guides/GETTING_STARTED.md
Normal file
370
docs/modules/router/guides/GETTING_STARTED.md
Normal file
@@ -0,0 +1,370 @@
|
||||
# Getting Started with StellaOps Router
|
||||
|
||||
This guide walks you through building your first microservice with the StellaOps Router framework.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- .NET 10 SDK
|
||||
- Docker (optional, for containerized deployment)
|
||||
|
||||
## Step 1: Create the Microservice Project
|
||||
|
||||
```bash
|
||||
mkdir MyService
|
||||
cd MyService
|
||||
dotnet new console -n MyService
|
||||
cd MyService
|
||||
```
|
||||
|
||||
Add the required packages:
|
||||
|
||||
```bash
|
||||
dotnet add package StellaOps.Microservice
|
||||
dotnet add package StellaOps.Router.Transport.InMemory
|
||||
```
|
||||
|
||||
## Step 2: Define Your Data Models
|
||||
|
||||
Create `Models/OrderModels.cs`:
|
||||
|
||||
```csharp
|
||||
namespace MyService.Models;
|
||||
|
||||
public sealed class CreateOrderRequest
|
||||
{
|
||||
public required string CustomerId { get; init; }
|
||||
public required decimal Amount { get; init; }
|
||||
public string? Description { get; init; }
|
||||
}
|
||||
|
||||
public sealed class CreateOrderResponse
|
||||
{
|
||||
public required string OrderId { get; init; }
|
||||
public DateTimeOffset CreatedAt { get; init; }
|
||||
public string Status { get; init; } = "created";
|
||||
}
|
||||
|
||||
public sealed class Order
|
||||
{
|
||||
public required string OrderId { get; init; }
|
||||
public required string CustomerId { get; init; }
|
||||
public required decimal Amount { get; init; }
|
||||
public string? Description { get; init; }
|
||||
public DateTimeOffset CreatedAt { get; init; }
|
||||
}
|
||||
```
|
||||
|
||||
## Step 3: Create Endpoints
|
||||
|
||||
Create `Endpoints/CreateOrderEndpoint.cs`:
|
||||
|
||||
```csharp
|
||||
using MyService.Models;
|
||||
using StellaOps.Microservice;
|
||||
|
||||
namespace MyService.Endpoints;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new order.
|
||||
/// </summary>
|
||||
[StellaEndpoint("POST", "/orders", TimeoutSeconds = 30)]
|
||||
public sealed class CreateOrderEndpoint : IStellaEndpoint<CreateOrderRequest, CreateOrderResponse>
|
||||
{
|
||||
private readonly ILogger<CreateOrderEndpoint> _logger;
|
||||
|
||||
public CreateOrderEndpoint(ILogger<CreateOrderEndpoint> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public Task<CreateOrderResponse> HandleAsync(
|
||||
CreateOrderRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var orderId = $"ORD-{Guid.NewGuid():N}"[..16];
|
||||
|
||||
_logger.LogInformation(
|
||||
"Created order {OrderId} for customer {CustomerId}, amount: {Amount}",
|
||||
orderId, request.CustomerId, request.Amount);
|
||||
|
||||
return Task.FromResult(new CreateOrderResponse
|
||||
{
|
||||
OrderId = orderId,
|
||||
CreatedAt = DateTimeOffset.UtcNow
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Create `Endpoints/GetOrderEndpoint.cs`:
|
||||
|
||||
```csharp
|
||||
using MyService.Models;
|
||||
using StellaOps.Microservice;
|
||||
|
||||
namespace MyService.Endpoints;
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves an order by ID.
|
||||
/// </summary>
|
||||
[StellaEndpoint("GET", "/orders/{id}", TimeoutSeconds = 10)]
|
||||
public sealed class GetOrderEndpoint : IRawStellaEndpoint
|
||||
{
|
||||
private readonly ILogger<GetOrderEndpoint> _logger;
|
||||
|
||||
public GetOrderEndpoint(ILogger<GetOrderEndpoint> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public Task<RawResponse> HandleAsync(
|
||||
RawRequestContext context,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var orderId = context.PathParameters["id"];
|
||||
|
||||
_logger.LogInformation("Fetching order {OrderId}", orderId);
|
||||
|
||||
// In a real app, you'd look up the order from a database
|
||||
var order = new Order
|
||||
{
|
||||
OrderId = orderId,
|
||||
CustomerId = "CUST-001",
|
||||
Amount = 99.99m,
|
||||
Description = "Sample order",
|
||||
CreatedAt = DateTimeOffset.UtcNow.AddHours(-1)
|
||||
};
|
||||
|
||||
var json = System.Text.Json.JsonSerializer.Serialize(order);
|
||||
var body = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(json));
|
||||
|
||||
var headers = new HeaderCollection();
|
||||
headers.Set("Content-Type", "application/json");
|
||||
|
||||
return Task.FromResult(new RawResponse
|
||||
{
|
||||
StatusCode = 200,
|
||||
Headers = headers,
|
||||
Body = body
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Step 4: Configure the Service
|
||||
|
||||
Update `Program.cs`:
|
||||
|
||||
```csharp
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MyService.Endpoints;
|
||||
using StellaOps.Microservice;
|
||||
using StellaOps.Router.Common.Enums;
|
||||
using StellaOps.Router.Transport.InMemory;
|
||||
|
||||
var builder = Host.CreateApplicationBuilder(args);
|
||||
|
||||
// Configure logging
|
||||
builder.Logging.AddConsole();
|
||||
builder.Logging.SetMinimumLevel(LogLevel.Debug);
|
||||
|
||||
// Configure the microservice
|
||||
builder.Services.AddStellaMicroservice(options =>
|
||||
{
|
||||
options.ServiceName = "order-service";
|
||||
options.Version = "1.0.0";
|
||||
options.Region = "local";
|
||||
options.InstanceId = $"order-{Environment.MachineName}";
|
||||
|
||||
// Connect to gateway (use InMemory for development)
|
||||
options.Routers =
|
||||
[
|
||||
new RouterEndpointConfig
|
||||
{
|
||||
Host = "localhost",
|
||||
Port = 5100,
|
||||
TransportType = TransportType.InMemory
|
||||
}
|
||||
];
|
||||
});
|
||||
|
||||
// Register transport
|
||||
builder.Services.AddInMemoryTransport();
|
||||
|
||||
// Register endpoint handlers
|
||||
builder.Services.AddScoped<CreateOrderEndpoint>();
|
||||
builder.Services.AddScoped<GetOrderEndpoint>();
|
||||
|
||||
var host = builder.Build();
|
||||
|
||||
Console.WriteLine("Order Service starting...");
|
||||
Console.WriteLine("Endpoints:");
|
||||
Console.WriteLine(" POST /orders");
|
||||
Console.WriteLine(" GET /orders/{id}");
|
||||
|
||||
await host.RunAsync();
|
||||
```
|
||||
|
||||
## Step 5: Create the Gateway
|
||||
|
||||
Create a separate project for the gateway:
|
||||
|
||||
```bash
|
||||
cd ..
|
||||
dotnet new web -n MyGateway
|
||||
cd MyGateway
|
||||
dotnet add package StellaOps.Router.Gateway
|
||||
dotnet add package StellaOps.Router.Transport.InMemory
|
||||
```
|
||||
|
||||
Update `Program.cs`:
|
||||
|
||||
```csharp
|
||||
using StellaOps.Router.Gateway;
|
||||
using StellaOps.Router.Gateway.Authorization;
|
||||
using StellaOps.Router.Gateway.DependencyInjection;
|
||||
using StellaOps.Router.Transport.InMemory;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// Add gateway services
|
||||
builder.Services.AddRouterGateway(builder.Configuration);
|
||||
|
||||
// Add InMemory transport for development
|
||||
builder.Services.AddInMemoryTransport();
|
||||
|
||||
// No-op authorization for demo (use real auth in production)
|
||||
builder.Services.AddNoOpAuthorityIntegration();
|
||||
builder.Services.AddAuthentication();
|
||||
|
||||
// OpenAPI
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// Middleware pipeline
|
||||
app.UseAuthentication();
|
||||
app.UseClaimsAuthorization();
|
||||
|
||||
// OpenAPI endpoint (aggregates from all microservices)
|
||||
app.MapRouterOpenApi();
|
||||
|
||||
// Health check
|
||||
app.MapGet("/health", () => Results.Ok(new { status = "healthy" }));
|
||||
|
||||
// Router gateway handles all other routes
|
||||
app.UseRouterGateway();
|
||||
|
||||
Console.WriteLine("Gateway starting on http://localhost:5000");
|
||||
app.Run("http://localhost:5000");
|
||||
```
|
||||
|
||||
## Step 6: Run the Services
|
||||
|
||||
Open two terminals:
|
||||
|
||||
**Terminal 1 - Gateway:**
|
||||
```bash
|
||||
cd MyGateway
|
||||
dotnet run
|
||||
```
|
||||
|
||||
**Terminal 2 - Microservice:**
|
||||
```bash
|
||||
cd MyService
|
||||
dotnet run
|
||||
```
|
||||
|
||||
## Step 7: Test the API
|
||||
|
||||
Create an order:
|
||||
```bash
|
||||
curl -X POST http://localhost:5000/orders \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"customerId": "CUST-001", "amount": 99.99}'
|
||||
```
|
||||
|
||||
Response:
|
||||
```json
|
||||
{
|
||||
"orderId": "ORD-abc123def456",
|
||||
"createdAt": "2024-01-15T10:30:00Z",
|
||||
"status": "created"
|
||||
}
|
||||
```
|
||||
|
||||
Get an order:
|
||||
```bash
|
||||
curl http://localhost:5000/orders/ORD-abc123def456
|
||||
```
|
||||
|
||||
## Step 8: Add Validation (Optional)
|
||||
|
||||
Add JSON Schema validation to your endpoints:
|
||||
|
||||
```csharp
|
||||
using StellaOps.Microservice;
|
||||
using StellaOps.Microservice.Validation;
|
||||
|
||||
[StellaEndpoint("POST", "/orders", TimeoutSeconds = 30)]
|
||||
[ValidateSchema] // Enables JSON Schema validation
|
||||
public sealed class CreateOrderEndpoint : IStellaEndpoint<CreateOrderRequest, CreateOrderResponse>
|
||||
{
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
The source generator will create a JSON Schema based on your request type's properties and validate incoming requests automatically.
|
||||
|
||||
## Step 9: Add Streaming Support (Optional)
|
||||
|
||||
For large file uploads or real-time data:
|
||||
|
||||
```csharp
|
||||
[StellaEndpoint("POST", "/orders/{id}/documents", SupportsStreaming = true)]
|
||||
public sealed class UploadDocumentEndpoint : IRawStellaEndpoint
|
||||
{
|
||||
public async Task<RawResponse> HandleAsync(
|
||||
RawRequestContext context,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var orderId = context.PathParameters["id"];
|
||||
var contentType = context.Headers["Content-Type"] ?? "application/octet-stream";
|
||||
|
||||
// Stream the body directly without buffering
|
||||
await using var bodyStream = context.Body;
|
||||
var totalBytes = 0L;
|
||||
|
||||
var buffer = new byte[8192];
|
||||
int bytesRead;
|
||||
while ((bytesRead = await bodyStream.ReadAsync(buffer, cancellationToken)) > 0)
|
||||
{
|
||||
totalBytes += bytesRead;
|
||||
// Process chunk...
|
||||
}
|
||||
|
||||
var headers = new HeaderCollection();
|
||||
headers.Set("Content-Type", "application/json");
|
||||
|
||||
var response = $$"""{"orderId":"{{orderId}}","bytesReceived":{{totalBytes}}}""";
|
||||
return new RawResponse
|
||||
{
|
||||
StatusCode = 200,
|
||||
Headers = headers,
|
||||
Body = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(response))
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
- **Production Deployment**: Switch from InMemory to TCP or TLS transport
|
||||
- **Authentication**: Integrate with the Authority module for OAuth/OIDC
|
||||
- **Rate Limiting**: Configure rate limits in router.yaml
|
||||
- **Observability**: Add OpenTelemetry tracing
|
||||
- **Testing**: Write integration tests using the Router.Testing library
|
||||
|
||||
See the [examples](../examples/) directory for complete working examples.
|
||||
122
docs/modules/router/guides/rate-limiting-config.md
Normal file
122
docs/modules/router/guides/rate-limiting-config.md
Normal file
@@ -0,0 +1,122 @@
|
||||
# Router Rate Limiting
|
||||
|
||||
Router rate limiting is a **gateway-owned** control plane feature implemented in `StellaOps.Router.Gateway`. It enforces limits centrally so microservices do not implement ad-hoc HTTP throttling.
|
||||
|
||||
## Behavior
|
||||
|
||||
When a request is denied the Router returns:
|
||||
- `429 Too Many Requests`
|
||||
- `Retry-After: <seconds>`
|
||||
- `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` (Unix seconds)
|
||||
- JSON body:
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "rate_limit_exceeded",
|
||||
"message": "Rate limit exceeded. Try again in 12 seconds.",
|
||||
"retryAfter": 12,
|
||||
"limit": 100,
|
||||
"current": 101,
|
||||
"window": 60,
|
||||
"scope": "environment"
|
||||
}
|
||||
```
|
||||
|
||||
## Model
|
||||
|
||||
Two scopes exist:
|
||||
- **Instance (`for_instance`)**: in-memory sliding window; protects a single Router process.
|
||||
- **Environment (`for_environment`)**: Valkey-backed fixed window; protects the whole environment across Router instances.
|
||||
|
||||
Environment checks are gated by an **activation threshold** (`process_back_pressure_when_more_than_per_5min`) to avoid unnecessary Valkey calls at low traffic.
|
||||
|
||||
## Configuration
|
||||
|
||||
Configuration is under the `rate_limiting` root.
|
||||
|
||||
### Minimal (instance only)
|
||||
|
||||
```yaml
|
||||
rate_limiting:
|
||||
process_back_pressure_when_more_than_per_5min: 5000
|
||||
|
||||
for_instance:
|
||||
rules:
|
||||
- per_seconds: 60
|
||||
max_requests: 600
|
||||
```
|
||||
|
||||
### Environment (Valkey)
|
||||
|
||||
```yaml
|
||||
rate_limiting:
|
||||
process_back_pressure_when_more_than_per_5min: 0 # always check environment
|
||||
|
||||
for_environment:
|
||||
valkey_connection: "valkey.stellaops.local:6379"
|
||||
valkey_bucket: "stella-router-rate-limit"
|
||||
|
||||
circuit_breaker:
|
||||
failure_threshold: 5
|
||||
timeout_seconds: 30
|
||||
half_open_timeout: 10
|
||||
|
||||
rules:
|
||||
- per_seconds: 60
|
||||
max_requests: 600
|
||||
```
|
||||
|
||||
### Rule stacking (AND logic)
|
||||
|
||||
Multiple rules on the same target are evaluated with **AND** semantics:
|
||||
|
||||
```yaml
|
||||
rate_limiting:
|
||||
for_environment:
|
||||
rules:
|
||||
- per_seconds: 1
|
||||
max_requests: 10
|
||||
- per_seconds: 3600
|
||||
max_requests: 3000
|
||||
```
|
||||
|
||||
If any rule is exceeded the request is denied. The Router returns the **most restrictive** `Retry-After` among violated rules.
|
||||
|
||||
### Microservice overrides
|
||||
|
||||
Overrides are **replacement**, not merge:
|
||||
|
||||
```yaml
|
||||
rate_limiting:
|
||||
for_environment:
|
||||
rules:
|
||||
- per_seconds: 60
|
||||
max_requests: 600
|
||||
|
||||
microservices:
|
||||
scanner:
|
||||
rules:
|
||||
- per_seconds: 10
|
||||
max_requests: 50
|
||||
```
|
||||
|
||||
### Route overrides
|
||||
|
||||
Route-level configuration is under:
|
||||
|
||||
`rate_limiting.for_environment.microservices.<microservice>.routes.<route_name>`
|
||||
|
||||
See `docs/modules/router/guides/rate-limiting-routes.md` for match types and specificity rules.
|
||||
|
||||
## Notes
|
||||
|
||||
- If `rules` is present, it takes precedence over legacy single-window keys (`per_seconds`, `max_requests`, `allow_*`).
|
||||
- For allowed requests, headers represent the **smallest window** rule for deterministic, low-cardinality output (not a full multi-rule snapshot).
|
||||
- If Valkey is unavailable, environment limiting is **fail-open** (instance limits still apply).
|
||||
|
||||
## Testing
|
||||
|
||||
- Unit tests: `dotnet test StellaOps.Router.slnx -c Release`
|
||||
- Valkey integration tests (Docker required): `STELLAOPS_INTEGRATION_TESTS=true dotnet test StellaOps.Router.slnx -c Release --filter FullyQualifiedName~ValkeyRateLimitStoreIntegrationTests`
|
||||
- k6 load tests: `tests/load/router-rate-limiting-load-test.js` (see `tests/load/README.md`)
|
||||
|
||||
90
docs/modules/router/guides/rate-limiting-routes.md
Normal file
90
docs/modules/router/guides/rate-limiting-routes.md
Normal file
@@ -0,0 +1,90 @@
|
||||
# Per-Route Rate Limiting (Router)
|
||||
|
||||
This document describes **per-route** rate limiting configuration for the Router gateway (`StellaOps.Router.Gateway`).
|
||||
|
||||
## Overview
|
||||
|
||||
Per-route rate limiting lets you apply different limits to specific HTTP paths **within the same microservice**.
|
||||
|
||||
Configuration is nested as:
|
||||
|
||||
`rate_limiting.for_environment.microservices.<microservice>.routes.<route_name>`
|
||||
|
||||
## Configuration
|
||||
|
||||
### Example (rules + routes)
|
||||
|
||||
```yaml
|
||||
rate_limiting:
|
||||
for_environment:
|
||||
valkey_connection: "valkey.stellaops.local:6379"
|
||||
valkey_bucket: "stella-router-rate-limit"
|
||||
|
||||
# Default environment rules (used when no microservice override exists)
|
||||
rules:
|
||||
- per_seconds: 60
|
||||
max_requests: 600
|
||||
|
||||
microservices:
|
||||
scanner:
|
||||
# Default rules for the microservice (used when no route override exists)
|
||||
rules:
|
||||
- per_seconds: 60
|
||||
max_requests: 600
|
||||
|
||||
routes:
|
||||
scan_submit:
|
||||
pattern: "/api/scans"
|
||||
match_type: exact
|
||||
rules:
|
||||
- per_seconds: 10
|
||||
max_requests: 50
|
||||
|
||||
scan_status:
|
||||
pattern: "/api/scans/*"
|
||||
match_type: prefix
|
||||
rules:
|
||||
- per_seconds: 1
|
||||
max_requests: 100
|
||||
|
||||
scan_by_id:
|
||||
pattern: "^/api/scans/[a-f0-9-]+$"
|
||||
match_type: regex
|
||||
rules:
|
||||
- per_seconds: 1
|
||||
max_requests: 50
|
||||
```
|
||||
|
||||
### Match types
|
||||
|
||||
`match_type` supports:
|
||||
|
||||
- `exact`: exact path match (case-insensitive), ignoring a trailing `/`.
|
||||
- `prefix`: literal prefix match; patterns commonly end with `*` (e.g. `/api/scans/*`).
|
||||
- `regex`: regular expression (compiled at startup; invalid regex fails fast).
|
||||
|
||||
### Specificity rules
|
||||
|
||||
When multiple routes match a path, the most specific match wins:
|
||||
|
||||
1. `exact`
|
||||
2. `prefix` (longest prefix wins)
|
||||
3. `regex` (longest pattern wins)
|
||||
|
||||
## Inheritance (resolution)
|
||||
|
||||
Rate limiting rules resolve with **replacement** semantics:
|
||||
|
||||
- `routes.<route_name>.rules` replaces the microservice rules.
|
||||
- `microservices.<name>.rules` replaces the environment rules.
|
||||
- If a level provides no rules, the next-less-specific level applies.
|
||||
|
||||
## Notes
|
||||
|
||||
- Per-route rate limiting applies at the **environment** scope (Valkey-backed).
|
||||
- The Router returns `429 Too Many Requests` and a `Retry-After` header when a limit is exceeded.
|
||||
|
||||
## See also
|
||||
|
||||
- `docs/modules/router/guides/rate-limiting-config.md` (full configuration guide)
|
||||
- `docs/modules/router/rate-limiting.md` (module dossier)
|
||||
Reference in New Issue
Block a user