Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 18 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -523,9 +523,9 @@ Users provide a short identifier (e.g., `my-app`), and the system uses it direct

**IAM Role Naming (ADR-116):**
- Pattern: `{role_name_format}.replace("{}", f"{stack_name}-{component}")`
- Components: `aggr` (Lambda), `app`, `admin`, `read`
- Components: `aggr` (Lambda aggregator), `prov` (Lambda provisioner), `app`, `admin`, `read`
- All components ≤ 8 characters (invariant for upgrade safety)
- Default names: `{stack}-aggr`, `{stack}-app`, `{stack}-admin`, `{stack}-read`
- Default names: `{stack}-aggr`, `{stack}-prov`, `{stack}-app`, `{stack}-admin`, `{stack}-read`
- Roles are **opt-in** (set `create_iam_roles=True`)

**IAM Managed Policy Naming (ADR-117):**
Expand Down Expand Up @@ -844,6 +844,22 @@ zae-limiter resource set-defaults gpt-4 -l tpm:50000 -l rpm:500
zae-limiter entity set-limits user-123 --resource gpt-4 -l rpm:1000
```

**`-l` flag format:** `name:rate[/period][:burst]` where `period` defaults to `/min`. Supported periods: `/sec`, `/min`, `/hour`, `/day`.

```bash
# Equivalent: 1000 per minute
-l rpm:1000
-l rpm:1000/min

# Other periods
-l rps:10/sec
-l rph:5000/hour
-l rpd:100000/day

# With burst
-l rpm:1000:1500
```

Each level also has `get-*` and `delete-*` subcommands. Use `zae-limiter resource list` to list resources with defaults. Use `zae-limiter entity list-resources` to list all resources with entity-level configs. Use `zae-limiter entity list --with-custom-limits <resource>` to list entities with custom limits for a specific resource.

**Namespace CLI commands:**
Expand Down
49 changes: 36 additions & 13 deletions docs/api/exceptions.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,22 @@ Exception types raised by zae-limiter.
```
ZAELimiterError (base)
├── RateLimitError
│ ├── RateLimitExceeded
│ └── RateLimiterUnavailable
│ └── RateLimitExceeded
├── EntityError
│ ├── EntityNotFoundError
│ └── EntityExistsError
├── InfrastructureError
│ ├── RateLimiterUnavailable
│ ├── StackCreationError
── StackAlreadyExistsError
│ └── StackAlreadyExistsError
│ ├── InfrastructureNotFoundError
│ └── NamespaceNotFoundError
└── VersionError
├── VersionMismatchError
└── IncompatibleSchemaError
├── VersionError
│ ├── VersionMismatchError
│ └── IncompatibleSchemaError
└── ValidationError
├── InvalidIdentifierError
└── InvalidNameError
```

## Base Exception
Expand All @@ -39,13 +42,6 @@ ZAELimiterError (base)
members_order: source
heading_level: 3

::: zae_limiter.exceptions.RateLimiterUnavailable
options:
show_root_heading: true
show_source: false
members_order: source
heading_level: 3

## Entity Exceptions

::: zae_limiter.exceptions.EntityNotFoundError
Expand All @@ -62,6 +58,13 @@ ZAELimiterError (base)

## Infrastructure Exceptions

::: zae_limiter.exceptions.RateLimiterUnavailable
options:
show_root_heading: true
show_source: false
members_order: source
heading_level: 3

::: zae_limiter.exceptions.StackCreationError
options:
show_root_heading: true
Expand Down Expand Up @@ -101,6 +104,26 @@ ZAELimiterError (base)
show_source: false
heading_level: 3

## Validation Exceptions

::: zae_limiter.exceptions.ValidationError
options:
show_root_heading: true
show_source: false
heading_level: 3

::: zae_limiter.exceptions.InvalidIdentifierError
options:
show_root_heading: true
show_source: false
heading_level: 3

::: zae_limiter.exceptions.InvalidNameError
options:
show_root_heading: true
show_source: false
heading_level: 3

## Exception Handling Examples

### Connection Errors
Expand Down
5 changes: 5 additions & 0 deletions docs/api/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@ zae_limiter/
├── exceptions.py # RateLimitExceeded, RateLimiterUnavailable, ...
├── repository.py # Repository (async DynamoDB operations)
├── sync_repository.py # Generated: SyncRepository
├── repository_builder.py # RepositoryBuilder (fluent async construction)
├── sync_repository_builder.py # Generated: SyncRepositoryBuilder
├── repository_protocol.py # RepositoryProtocol (backend abstraction)
├── sync_repository_protocol.py # Generated: SyncRepositoryProtocol
├── lease.py # Lease (async context manager)
Expand All @@ -119,6 +121,9 @@ zae_limiter/
├── locust.py # Locust load testing integration (RateLimiterUser, RateLimiterSession)
├── local.py # LocalStack management commands
├── cli.py # CLI commands
├── limits_cli.py # Declarative limits CLI (plan, apply, diff, cfn-template)
├── loadtest/ # Load testing infrastructure
├── visualization/ # Usage snapshot formatting and display
└── infra/
├── stack_manager.py # StackManager (async CloudFormation operations)
├── sync_stack_manager.py # Generated: SyncStackManager
Expand Down
2 changes: 1 addition & 1 deletion docs/contributing/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ All data is stored in a single DynamoDB table using a composite key pattern:
| Record Type | PK | SK |
|-------------|----|----|
| Entity metadata | `{ns}/ENTITY#{id}` | `#META` |
| Bucket | `{ns}/ENTITY#{id}` | `#BUCKET#{resource}#{limit_name}` |
| Bucket | `{ns}/ENTITY#{id}` | `#BUCKET#{resource}` |
| Entity config | `{ns}/ENTITY#{id}` | `#CONFIG#{resource}` |
| Resource config | `{ns}/RESOURCE#{resource}` | `#CONFIG` |
| System config | `{ns}/SYSTEM#` | `#CONFIG` |
Expand Down
5 changes: 3 additions & 2 deletions docs/infra/auditing.md
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,7 @@ Audit events are stored in the same DynamoDB table with the following schema:

| Key | Format | Description |
|-----|--------|-------------|
| PK | `AUDIT#{entity_id}` | Groups events by entity |
| PK | `{ns}/AUDIT#{entity_id}` | Groups events by entity (namespace-prefixed) |
| SK | `#AUDIT#{event_id}` | Sorts by event ID (chronological) |

### Direct DynamoDB Queries
Expand All @@ -327,10 +327,11 @@ dynamodb = boto3.resource("dynamodb")
table = dynamodb.Table("limiter")

# Query all audit events for an entity
# Replace {ns} with your namespace ID (e.g., "default" or an opaque ID)
response = table.query(
KeyConditionExpression="PK = :pk AND begins_with(SK, :sk)",
ExpressionAttributeValues={
":pk": "AUDIT#api-key-123",
":pk": "{ns}/AUDIT#api-key-123",
":sk": "#AUDIT#",
},
ScanIndexForward=False, # Most recent first
Expand Down
68 changes: 47 additions & 21 deletions docs/infra/cloudformation.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,17 @@ The DynamoDB table name is automatically derived from the CloudFormation stack n
| `AuditArchiveGlacierDays` | Number | `90` | Days before Glacier IR transition (1-3650) |
| `EnableTracing` | String | `false` | Enable AWS X-Ray tracing for Lambda |
| `EnableIAMRoles` | String | `true` | Create App/Admin/ReadOnly IAM roles |
| `EnableIAM` | String | `true` | Create all IAM resources (policies + roles) |
| `AggregatorRoleArn` | String | _(empty)_ | External IAM role ARN for aggregator Lambda |
| `AggregatorRoleName` | String | _(empty)_ | Custom name for aggregator Lambda role |
| `AppRoleName` | String | _(empty)_ | Custom name for application IAM role |
| `AdminRoleName` | String | _(empty)_ | Custom name for admin IAM role |
| `ReadOnlyRoleName` | String | _(empty)_ | Custom name for read-only IAM role |
| `ProvisionerRoleName` | String | _(empty)_ | Custom name for provisioner Lambda role |
| `AcquireOnlyPolicyName` | String | _(empty)_ | Custom name for acquire-only policy |
| `FullAccessPolicyName` | String | _(empty)_ | Custom name for full-access policy |
| `ReadOnlyPolicyName` | String | _(empty)_ | Custom name for read-only policy |
| `EnableProvisioner` | String | `true` | Deploy limits provisioner Lambda |
| `EnableDeletionProtection` | String | `false` | Enable DynamoDB table deletion protection |
| `NamespaceAcquirePolicyName` | String | _(empty)_ | Custom name for namespace acquire-only policy |
| `NamespaceFullAccessPolicyName` | String | _(empty)_ | Custom name for namespace full-access policy |
Expand Down Expand Up @@ -152,7 +163,7 @@ GlobalSecondaryIndexes:
KeySchema:
- AttributeName: GSI2PK # RESOURCE#{resource}
KeyType: HASH
- AttributeName: GSI2SK # BUCKET#{entity_id}#{limit_name}
- AttributeName: GSI2SK # BUCKET#{entity_id}
KeyType: RANGE
```

Expand All @@ -166,7 +177,7 @@ GlobalSecondaryIndexes:
- AttributeName: GSI3SK # entity_id
KeyType: RANGE
Projection:
ProjectionType: ALL
ProjectionType: KEYS_ONLY
```

**GSI4** - Namespace item discovery (for `purge_namespace()`):
Expand Down Expand Up @@ -518,25 +529,40 @@ sam deploy --guided

The template exports:

| Output | Description |
|--------|-------------|
| `TableArn` | DynamoDB table ARN |
| `StreamArn` | DynamoDB stream ARN |
| `FunctionArn` | Lambda function ARN |
| `AuditArchiveBucketName` | S3 bucket for audit archives (when enabled) |
| `AuditArchiveBucketArn` | S3 bucket ARN (when enabled) |
| `AcquireOnlyPolicyArn` | IAM policy ARN for acquire-only access |
| `FullAccessPolicyArn` | IAM policy ARN for full access |
| `ReadOnlyPolicyArn` | IAM policy ARN for read-only access |
| `AppRoleArn` | IAM role ARN for application access (when IAM roles enabled) |
| `AppRoleName` | IAM role name for application access |
| `AdminRoleArn` | IAM role ARN for admin access |
| `AdminRoleName` | IAM role name for admin access |
| `ReadOnlyRoleArn` | IAM role ARN for read-only access |
| `ReadOnlyRoleName` | IAM role name for read-only access |
| `NamespaceAcquirePolicyArn` | Namespace-scoped acquire-only policy ARN |
| `NamespaceFullAccessPolicyArn` | Namespace-scoped full-access policy ARN |
| `NamespaceReadOnlyPolicyArn` | Namespace-scoped read-only policy ARN |
| Output | Condition | Description |
|--------|-----------|-------------|
| `TableName` | Always | DynamoDB table name |
| `TableArn` | Always | DynamoDB table ARN |
| `StreamArn` | Always | DynamoDB stream ARN |
| `AggregatorFunctionArn` | Aggregator deployed | Aggregator Lambda function ARN |
| `AggregatorFunctionName` | Aggregator deployed | Aggregator Lambda function name |
| `AggregatorDLQUrl` | Aggregator enabled | Dead Letter Queue URL |
| `AggregatorDLQArn` | Aggregator enabled | Dead Letter Queue ARN |
| `AggregatorDLQAlarmName` | Aggregator alarms | CloudWatch alarm for DLQ monitoring |
| `LambdaErrorRateAlarmName` | Aggregator alarms | CloudWatch alarm for Lambda error rate |
| `LambdaDurationAlarmName` | Aggregator alarms | CloudWatch alarm for Lambda duration |
| `DynamoDBReadThrottleAlarmName` | Alarms enabled | CloudWatch alarm for read throttles |
| `DynamoDBWriteThrottleAlarmName` | Alarms enabled | CloudWatch alarm for write throttles |
| `StreamIteratorAgeAlarmName` | Aggregator alarms | CloudWatch alarm for stream iterator age |
| `AuditArchiveBucketName` | Audit archival | S3 bucket for audit archives |
| `AuditArchiveBucketArn` | Audit archival | S3 bucket ARN |
| `AcquireOnlyPolicyArn` | IAM enabled | IAM policy ARN for acquire-only access |
| `FullAccessPolicyArn` | IAM enabled | IAM policy ARN for full access |
| `ReadOnlyPolicyArn` | IAM enabled | IAM policy ARN for read-only access |
| `NamespaceAcquirePolicyArn` | IAM enabled | Namespace-scoped acquire-only policy ARN |
| `NamespaceFullAccessPolicyArn` | IAM enabled | Namespace-scoped full-access policy ARN |
| `NamespaceReadOnlyPolicyArn` | IAM enabled | Namespace-scoped read-only policy ARN |
| `AppRoleArn` | IAM roles enabled | IAM role ARN for application access |
| `AppRoleName` | IAM roles enabled | IAM role name for application access |
| `AdminRoleArn` | IAM roles enabled | IAM role ARN for admin access |
| `AdminRoleName` | IAM roles enabled | IAM role name for admin access |
| `ReadOnlyRoleArn` | IAM roles enabled | IAM role ARN for read-only access |
| `ReadOnlyRoleName` | IAM roles enabled | IAM role name for read-only access |
| `PermissionBoundaryArn` | Always | Permission boundary ARN (empty if none) |
| `RoleNameFormat` | Always | Role name format template (empty if default) |
| `CodeBucketName` | Audit archival | S3 bucket for deployment artifacts |
| `ProvisionerFunctionArn` | Provisioner deployed | Limits provisioner Lambda function ARN |
| `ProvisionerFunctionName` | Provisioner deployed | Limits provisioner Lambda function name |

!!! tip "Discovering namespace IDs"
Namespace IDs are opaque strings stored in DynamoDB, not CloudFormation outputs.
Expand Down
2 changes: 1 addition & 1 deletion docs/infra/deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ zae-limiter deploy \
| `--pitr-recovery-days` | Point-in-time recovery (1-35 days) | None (disabled) |
| `--enable-audit-archival/--no-audit-archival` | Archive expired audit events to S3 | `true` |
| `--audit-archive-glacier-days` | Days before Glacier IR transition | `90` |
| `--enable-iam-roles/--no-iam-roles` | Create App/Admin/ReadOnly IAM roles | `true` |
| `--create-iam-roles/--no-create-iam-roles` | Create App/Admin/ReadOnly IAM roles | `false` |
| `--enable-deletion-protection/--no-deletion-protection` | Enable DynamoDB table deletion protection | `false` |

For the full list of options, see the [CLI Reference](../cli.md#deploy).
Expand Down
2 changes: 1 addition & 1 deletion docs/infra/production.md
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ This policy is operator-managed (not owned by the zae-limiter CloudFormation sta

- All entity and limit changes are automatically logged
- Track who made changes with optional `principal` parameter
- Events auto-expire after 90 days (configurable via `--audit-ttl-days`)
- Events auto-expire after 90 days (configurable via `--audit-retention-days`)
- **Expired events are archived to S3** for long-term retention
- Archives transition to Glacier IR after 90 days (configurable via `--audit-archive-glacier-days`)
- For compliance requirements, see [Audit Logging Guide](auditing.md)
Expand Down
Loading