belt 0.3.2 → 0.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of belt might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/CHANGELOG.md +6 -0
- data/lib/belt/action_router.rb +13 -2
- data/lib/belt/cli/explain_command.rb +110 -0
- data/lib/belt/cli/frontend_env_command.rb +1 -1
- data/lib/belt/cli/frontend_env_map.rb +3 -1
- data/lib/belt/cli/lambda_config_command.rb +7 -2
- data/lib/belt/cli/routes_command.rb +24 -0
- data/lib/belt/cli.rb +4 -1
- data/lib/belt/docs/backups.md +101 -0
- data/lib/belt/docs/console.md +65 -0
- data/lib/belt/docs/controllers.md +155 -0
- data/lib/belt/docs/deployment.md +141 -0
- data/lib/belt/docs/generators.md +128 -0
- data/lib/belt/docs/lambda_handler.md +105 -0
- data/lib/belt/docs/models.md +160 -0
- data/lib/belt/docs/observability.md +94 -0
- data/lib/belt/docs/plugins.md +94 -0
- data/lib/belt/docs/routing.md +138 -0
- data/lib/belt/docs/structure.md +93 -0
- data/lib/belt/route_dsl.rb +17 -0
- data/lib/belt/version.rb +1 -1
- data/lib/templates/new_app/AGENTS.md.erb +140 -19
- data/lib/templates/new_app/config/routes.rb.erb +5 -2
- metadata +13 -1
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# Deployment
|
|
2
|
+
|
|
3
|
+
Belt deploys serverless applications to AWS using Terraform. The CLI wraps
|
|
4
|
+
`terraform init/plan/apply` with conventions for environment management,
|
|
5
|
+
Lambda packaging, and pre-deploy backups.
|
|
6
|
+
|
|
7
|
+
## Quick Deploy
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
belt deploy <env> # init → plan → apply (interactive)
|
|
11
|
+
belt deploy prod --auto # skip confirmation prompt
|
|
12
|
+
belt deploy prod --skip-backup # skip pre-deploy backup
|
|
13
|
+
belt deploy --backup-only # create recovery point without deploying
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## First-Time Setup
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
# 1. Create S3 bucket for Terraform state
|
|
20
|
+
belt setup state
|
|
21
|
+
|
|
22
|
+
# 2. Scaffold an environment
|
|
23
|
+
belt generate environment dev01
|
|
24
|
+
|
|
25
|
+
# 3. Generate DynamoDB table definitions from schema
|
|
26
|
+
belt setup tables dev01
|
|
27
|
+
|
|
28
|
+
# 4. Initialize and deploy
|
|
29
|
+
belt deploy dev01
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Environment Structure
|
|
33
|
+
|
|
34
|
+
Each environment lives in `infrastructure/<env>/`:
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
infrastructure/
|
|
38
|
+
├── modules/
|
|
39
|
+
│ └── main/ # Shared Terraform module
|
|
40
|
+
├── dev01/
|
|
41
|
+
│ ├── main.tf # Module reference + provider
|
|
42
|
+
│ ├── variables.tf # Variable declarations
|
|
43
|
+
│ ├── terraform.tfvars # Environment-specific values
|
|
44
|
+
│ ├── backend.tf # S3 state backend
|
|
45
|
+
│ └── outputs.tf # Exported values
|
|
46
|
+
└── prod/
|
|
47
|
+
└── ...
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## What Gets Deployed
|
|
51
|
+
|
|
52
|
+
The Conveyor Belt Terraform provider reads your Ruby DSL and creates:
|
|
53
|
+
|
|
54
|
+
1. **API Gateway** — HTTP API with routes matching your DSL
|
|
55
|
+
2. **Lambda functions** — packaged Ruby code (one per `gateway`/`function` block)
|
|
56
|
+
3. **IAM roles** — least-privilege policies for DynamoDB table access
|
|
57
|
+
4. **CloudWatch logs** — log groups for each Lambda
|
|
58
|
+
5. **DynamoDB tables** — from your schema definition
|
|
59
|
+
6. **Custom domain** — if configured (Route53 + ACM certificate)
|
|
60
|
+
|
|
61
|
+
## Lambda Packaging
|
|
62
|
+
|
|
63
|
+
Belt packages the `lambda/` directory plus vendored gems. For `path:` gems
|
|
64
|
+
(local development), Belt materializes them into `vendor/cache` automatically.
|
|
65
|
+
|
|
66
|
+
The Lambda entry point is specified in `config/lambda/<name>.yml`:
|
|
67
|
+
|
|
68
|
+
```yaml
|
|
69
|
+
handler: lambda/<name>.lambda_handler
|
|
70
|
+
runtime: ruby3.3
|
|
71
|
+
timeout: 30
|
|
72
|
+
memory: 256
|
|
73
|
+
environment:
|
|
74
|
+
ENVIRONMENT: ${var.environment}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Route Manifests
|
|
78
|
+
|
|
79
|
+
Before deploying, generate the route manifest used at runtime:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
belt routes --namespace api
|
|
83
|
+
# → writes lambda/lib/routes/api_routes.rb
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
This is typically done automatically by `belt deploy`.
|
|
87
|
+
|
|
88
|
+
## Terraform Commands
|
|
89
|
+
|
|
90
|
+
Belt wraps Terraform with environment awareness:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
belt init <env> # terraform init with correct backend
|
|
94
|
+
belt plan <env> # terraform plan
|
|
95
|
+
belt apply <env> # terraform apply
|
|
96
|
+
belt destroy <env> # terraform destroy (careful!)
|
|
97
|
+
belt output <env> # terraform output
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Or use `belt deploy <env>` which runs init → plan → apply in sequence.
|
|
101
|
+
|
|
102
|
+
## Pre-Deploy Backups
|
|
103
|
+
|
|
104
|
+
Configure in `infrastructure/<env>/belt.rb`:
|
|
105
|
+
|
|
106
|
+
```ruby
|
|
107
|
+
Belt.configure do |config|
|
|
108
|
+
config.backups do
|
|
109
|
+
dynamodb :all
|
|
110
|
+
retention snapshots: 90
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Backups run automatically before each deploy (DynamoDB snapshots, Cognito
|
|
116
|
+
exports, S3 syncs). See `belt explain backups` for full documentation.
|
|
117
|
+
|
|
118
|
+
## Environment Variables
|
|
119
|
+
|
|
120
|
+
Set `BELT_ENV` to avoid typing the environment every time:
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
export BELT_ENV=dev01
|
|
124
|
+
belt deploy # uses BELT_ENV
|
|
125
|
+
belt deploy prod # explicit arg wins
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Frontend Deployment
|
|
129
|
+
|
|
130
|
+
For apps with a frontend:
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
belt deploy frontend <env> # build + deploy to S3/CloudFront
|
|
134
|
+
belt frontend env <env> # generate .env from Terraform outputs
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## See Also
|
|
138
|
+
|
|
139
|
+
- `belt explain routing` — how routes map to infrastructure
|
|
140
|
+
- `belt explain backups` — pre-deploy backup configuration
|
|
141
|
+
- `belt doctor` — check system dependencies before deploying
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# Generators
|
|
2
|
+
|
|
3
|
+
Belt generators scaffold code and infrastructure for common patterns.
|
|
4
|
+
Run `belt generate --help` to see all available generators (built-in + plugins).
|
|
5
|
+
|
|
6
|
+
## Built-in Generators
|
|
7
|
+
|
|
8
|
+
### Resource (scaffold)
|
|
9
|
+
|
|
10
|
+
Creates model + controller + routes + schema entry:
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
belt generate resource post title:string body:text user_id:string status:string
|
|
14
|
+
belt g resource comment body:text author:string post_id:string
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
This is the most common generator — it sets up everything for a new REST endpoint.
|
|
18
|
+
|
|
19
|
+
### Model
|
|
20
|
+
|
|
21
|
+
Creates just the model file + schema entry:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
belt generate model post title:string body:text user_id:string
|
|
25
|
+
belt g model payment amount:number currency:string
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### Controller
|
|
29
|
+
|
|
30
|
+
Creates just the controller:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
belt generate controller posts
|
|
34
|
+
belt g controller admin/users
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Environment
|
|
38
|
+
|
|
39
|
+
Scaffolds a new Terraform environment directory:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
belt generate environment staging
|
|
43
|
+
belt g environment prod
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Frontend
|
|
47
|
+
|
|
48
|
+
Adds a frontend framework to the project:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
belt generate frontend react
|
|
52
|
+
belt generate frontend vue
|
|
53
|
+
belt generate frontend svelte
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Views
|
|
57
|
+
|
|
58
|
+
Generates React pages for a resource's REST actions:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
belt generate views post title:string body:text status:string
|
|
62
|
+
belt g views comment body:text author:string
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Auth
|
|
66
|
+
|
|
67
|
+
Sets up Cognito authentication:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
belt generate auth
|
|
71
|
+
belt g auth --provider cognito
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Field Types
|
|
75
|
+
|
|
76
|
+
When specifying fields, use these types:
|
|
77
|
+
|
|
78
|
+
| Type | DynamoDB Type | Notes |
|
|
79
|
+
|------|---------------|-------|
|
|
80
|
+
| `string` | S | Default if no type given |
|
|
81
|
+
| `text` | S | Same as string (semantic hint) |
|
|
82
|
+
| `number` | N | Numeric values |
|
|
83
|
+
| `boolean` | BOOL | True/false |
|
|
84
|
+
| `list` | L | Array values |
|
|
85
|
+
| `map` | M | Nested objects |
|
|
86
|
+
|
|
87
|
+
## Destroying Generated Code
|
|
88
|
+
|
|
89
|
+
Every generator has a matching destroy command:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
belt destroy resource post
|
|
93
|
+
belt destroy model comment
|
|
94
|
+
belt destroy controller admin/users
|
|
95
|
+
belt destroy environment staging
|
|
96
|
+
belt destroy frontend
|
|
97
|
+
belt destroy views post
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Plugin Generators
|
|
101
|
+
|
|
102
|
+
Gems that follow the Belt plugin contract are auto-discovered:
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
belt generate messaging # from belt-messaging gem
|
|
106
|
+
belt generate pay # from belt-pay gem
|
|
107
|
+
belt generate --help # lists all available generators
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Generator Workflow (Common Pattern)
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
# 1. Generate the resource
|
|
114
|
+
belt generate resource order item_id:string quantity:number total:number status:string
|
|
115
|
+
|
|
116
|
+
# 2. Generate DynamoDB table from schema
|
|
117
|
+
belt setup tables dev01
|
|
118
|
+
|
|
119
|
+
# 3. Deploy
|
|
120
|
+
belt deploy dev01
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## See Also
|
|
124
|
+
|
|
125
|
+
- `belt explain models` — how generated models work
|
|
126
|
+
- `belt explain controllers` — how generated controllers work
|
|
127
|
+
- `belt explain routing` — how generated routes work
|
|
128
|
+
- `belt explain deployment` — deploying generated code
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Lambda Handler
|
|
2
|
+
|
|
3
|
+
`Belt::LambdaHandler` is the module you include in your Lambda entry point.
|
|
4
|
+
It provides the `lambda_handler` method that AWS Lambda invokes, wrapping
|
|
5
|
+
your application logic with observability, CORS, and error handling.
|
|
6
|
+
|
|
7
|
+
## Basic Usage
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
require "belt"
|
|
11
|
+
|
|
12
|
+
include Belt::LambdaHandler
|
|
13
|
+
|
|
14
|
+
ROUTER = Belt::ActionRouter.new(routes: Routes::API, gateway: "api")
|
|
15
|
+
|
|
16
|
+
def execute(path:, body:, event:)
|
|
17
|
+
ROUTER.route(event: event, body: body)
|
|
18
|
+
end
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## What It Does
|
|
22
|
+
|
|
23
|
+
When a request arrives, `lambda_handler` automatically:
|
|
24
|
+
|
|
25
|
+
1. **Initializes observability** — structured logging + CloudWatch metrics
|
|
26
|
+
2. **Handles OPTIONS preflight** — returns CORS headers immediately
|
|
27
|
+
3. **Parses the request body** — JSON string → Ruby hash
|
|
28
|
+
4. **Calls your `execute` method** — with `path:`, `body:`, and `event:`
|
|
29
|
+
5. **Catches unhandled errors** — returns a CORS-enabled error response
|
|
30
|
+
6. **Emits metrics** — request count, latency, error count via EMF
|
|
31
|
+
|
|
32
|
+
## The `execute` Method
|
|
33
|
+
|
|
34
|
+
You must define `execute` in your Lambda file. It receives:
|
|
35
|
+
|
|
36
|
+
| Param | Description |
|
|
37
|
+
|-------|-------------|
|
|
38
|
+
| `path:` | The request path (e.g., `/posts/123`) |
|
|
39
|
+
| `body:` | Parsed request body (Hash or nil) |
|
|
40
|
+
| `event:` | Full API Gateway event (for headers, query params, auth context) |
|
|
41
|
+
|
|
42
|
+
Return value should be a response hash: `{ statusCode:, headers:, body: }`.
|
|
43
|
+
Typically you just call `ROUTER.route(...)` which returns the correct format.
|
|
44
|
+
|
|
45
|
+
## Multiple Lambdas
|
|
46
|
+
|
|
47
|
+
If your app has multiple Lambda functions (via `function` in routes):
|
|
48
|
+
|
|
49
|
+
```ruby
|
|
50
|
+
# lambda/worker.rb
|
|
51
|
+
require "belt"
|
|
52
|
+
|
|
53
|
+
include Belt::LambdaHandler
|
|
54
|
+
|
|
55
|
+
ROUTER = Belt::ActionRouter.new(routes: Routes::WORKER, gateway: "worker")
|
|
56
|
+
|
|
57
|
+
def execute(path:, body:, event:)
|
|
58
|
+
ROUTER.route(event: event, body: body)
|
|
59
|
+
end
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Each Lambda entry point gets its own route manifest and gateway name.
|
|
63
|
+
|
|
64
|
+
## Configuration
|
|
65
|
+
|
|
66
|
+
Configure via `config/lambda/<name>.yml`:
|
|
67
|
+
|
|
68
|
+
```yaml
|
|
69
|
+
handler: lambda/api.lambda_handler
|
|
70
|
+
runtime: ruby3.3
|
|
71
|
+
timeout: 30
|
|
72
|
+
memory: 256
|
|
73
|
+
layers:
|
|
74
|
+
- ${var.ruby_layer_arn}
|
|
75
|
+
environment:
|
|
76
|
+
ENVIRONMENT: ${var.environment}
|
|
77
|
+
APP_NAME: my-app
|
|
78
|
+
ERROR_NOTIFICATION_TOPIC_ARN: ${var.sns_topic_arn}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Observability
|
|
82
|
+
|
|
83
|
+
The handler sets up `Belt::Observability::Logger` and `Belt::Observability::Metrics`
|
|
84
|
+
automatically. Use them anywhere in your app:
|
|
85
|
+
|
|
86
|
+
```ruby
|
|
87
|
+
Belt::Observability::Logger.info("Order created", order_id: order.id)
|
|
88
|
+
Belt::Observability::Metrics.track_event("OrderCreated", model: "Order")
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Environment Variables
|
|
92
|
+
|
|
93
|
+
| Variable | Purpose |
|
|
94
|
+
|----------|---------|
|
|
95
|
+
| `ENVIRONMENT` | Controls error verbosity (`dev*`, `local`, `test` = verbose) |
|
|
96
|
+
| `BELT_METRICS_NAMESPACE` | CloudWatch namespace (default: `Belt`) |
|
|
97
|
+
| `ACTION` | Service name for logging |
|
|
98
|
+
| `ERROR_NOTIFICATION_TOPIC_ARN` | SNS topic for error alerts |
|
|
99
|
+
| `CORS_ALLOWED_ORIGINS` | Comma-separated allowed origins |
|
|
100
|
+
|
|
101
|
+
## See Also
|
|
102
|
+
|
|
103
|
+
- `belt explain routing` — how ActionRouter dispatches requests
|
|
104
|
+
- `belt explain controllers` — how dispatched requests are handled
|
|
105
|
+
- `belt explain deployment` — Lambda packaging and configuration
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# Models
|
|
2
|
+
|
|
3
|
+
Belt uses **ActiveItem** as its ORM for DynamoDB. Models inherit from
|
|
4
|
+
`ActiveItem::Base` (typically via an `ApplicationRecord` base class).
|
|
5
|
+
|
|
6
|
+
## Basic Model
|
|
7
|
+
|
|
8
|
+
```ruby
|
|
9
|
+
class Post < ApplicationRecord
|
|
10
|
+
attr_accessor :id, :user_id, :title, :body, :status, :created_at, :updated_at
|
|
11
|
+
|
|
12
|
+
validates :title, presence: true
|
|
13
|
+
validates :status, inclusion: { in: %w[draft published] }, allow_nil: true
|
|
14
|
+
|
|
15
|
+
before_create { self.id ||= SecureRandom.uuid }
|
|
16
|
+
before_save { self.updated_at = Time.now.iso8601 }
|
|
17
|
+
end
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Table Naming
|
|
21
|
+
|
|
22
|
+
Table names are derived from the environment:
|
|
23
|
+
`{APP_NAME}-{ENVIRONMENT}-{pluralized_model}`
|
|
24
|
+
|
|
25
|
+
Example: app "blog", environment "prod", model "Post" → `blog-prod-posts`
|
|
26
|
+
|
|
27
|
+
## Schema Definition
|
|
28
|
+
|
|
29
|
+
Define table structure in `infrastructure/schema.tf.rb`:
|
|
30
|
+
|
|
31
|
+
```ruby
|
|
32
|
+
Belt.application.schema.define do
|
|
33
|
+
model :post do
|
|
34
|
+
partition_key :id, :string
|
|
35
|
+
global_secondary_index :UserIndex, partition_key: :user_id
|
|
36
|
+
global_secondary_index :StatusIndex, partition_key: :status, sort_key: :created_at
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## CRUD Operations
|
|
42
|
+
|
|
43
|
+
### Create
|
|
44
|
+
|
|
45
|
+
```ruby
|
|
46
|
+
post = Post.create!(title: "Hello", body: "World", user_id: "u-123")
|
|
47
|
+
# or
|
|
48
|
+
post = Post.new(title: "Hello")
|
|
49
|
+
post.save!
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Read
|
|
53
|
+
|
|
54
|
+
```ruby
|
|
55
|
+
post = Post.find("post-id-123") # by primary key
|
|
56
|
+
posts = Post.all # scan (use sparingly)
|
|
57
|
+
posts = Post.where(user_id: "u-123", index: "UserIndex")
|
|
58
|
+
post = Post.find_by(user_id: "u-123", index: "UserIndex") # first match
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Update
|
|
62
|
+
|
|
63
|
+
```ruby
|
|
64
|
+
post = Post.find("post-id-123")
|
|
65
|
+
post.update(title: "New Title")
|
|
66
|
+
# or
|
|
67
|
+
post.title = "New Title"
|
|
68
|
+
post.save!
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Delete
|
|
72
|
+
|
|
73
|
+
```ruby
|
|
74
|
+
post = Post.find("post-id-123")
|
|
75
|
+
post.destroy
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Query Patterns
|
|
79
|
+
|
|
80
|
+
### Using Indexes
|
|
81
|
+
|
|
82
|
+
```ruby
|
|
83
|
+
# Query a GSI
|
|
84
|
+
Post.where(user_id: "u-123", index: "UserIndex")
|
|
85
|
+
|
|
86
|
+
# With sort key conditions
|
|
87
|
+
Post.where(
|
|
88
|
+
status: "published",
|
|
89
|
+
created_at: { gte: "2024-01-01" },
|
|
90
|
+
index: "StatusIndex"
|
|
91
|
+
)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Count and Existence
|
|
95
|
+
|
|
96
|
+
```ruby
|
|
97
|
+
Post.count # total items (scan)
|
|
98
|
+
Post.exists?("post-id-123") # check by primary key
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Validations
|
|
102
|
+
|
|
103
|
+
ActiveItem supports ActiveModel-style validations:
|
|
104
|
+
|
|
105
|
+
```ruby
|
|
106
|
+
validates :title, presence: true
|
|
107
|
+
validates :email, format: { with: URI::MailTo::EMAIL_REGEXP }
|
|
108
|
+
validates :status, inclusion: { in: %w[active inactive] }
|
|
109
|
+
validates :age, numericality: { greater_than: 0 }
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Callbacks
|
|
113
|
+
|
|
114
|
+
```ruby
|
|
115
|
+
before_create { self.id ||= SecureRandom.uuid }
|
|
116
|
+
before_save { self.updated_at = Time.now.iso8601 }
|
|
117
|
+
after_create { notify_subscribers }
|
|
118
|
+
before_destroy { cleanup_associations }
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Associations (Manual)
|
|
122
|
+
|
|
123
|
+
DynamoDB doesn't have joins. Model relationships manually:
|
|
124
|
+
|
|
125
|
+
```ruby
|
|
126
|
+
class Post < ApplicationRecord
|
|
127
|
+
def comments
|
|
128
|
+
Comment.where(post_id: id, index: "PostIndex")
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def author
|
|
132
|
+
User.find(user_id)
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Transactions
|
|
138
|
+
|
|
139
|
+
```ruby
|
|
140
|
+
ActiveItem::Transaction.write do |tx|
|
|
141
|
+
tx.put(post)
|
|
142
|
+
tx.put(comment)
|
|
143
|
+
tx.delete(draft)
|
|
144
|
+
end
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Generating Models
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
belt generate model post title:string body:text user_id:string
|
|
151
|
+
belt generate resource comment body:text author:string post_id:string
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
The resource generator creates model + controller + routes + schema entry.
|
|
155
|
+
|
|
156
|
+
## See Also
|
|
157
|
+
|
|
158
|
+
- `belt explain controllers` — using models in controllers
|
|
159
|
+
- `belt explain deployment` — how schema becomes infrastructure
|
|
160
|
+
- `belt explain queries` — advanced DynamoDB query patterns
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# Observability
|
|
2
|
+
|
|
3
|
+
Belt provides structured logging and CloudWatch metrics out of the box.
|
|
4
|
+
These are initialized automatically by `Belt::LambdaHandler` — no setup required.
|
|
5
|
+
|
|
6
|
+
## Logging
|
|
7
|
+
|
|
8
|
+
Access the logger from anywhere:
|
|
9
|
+
|
|
10
|
+
```ruby
|
|
11
|
+
Belt::Observability::Logger.info("Order placed", order_id: "o-123", total: 49.99)
|
|
12
|
+
Belt::Observability::Logger.warn("Retry attempt", attempt: 3, service: "payments")
|
|
13
|
+
Belt::Observability::Logger.error("Payment failed", error: e.message, order_id: "o-123")
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Logs are structured JSON, compatible with CloudWatch Logs Insights:
|
|
17
|
+
|
|
18
|
+
```json
|
|
19
|
+
{
|
|
20
|
+
"level": "INFO",
|
|
21
|
+
"message": "Order placed",
|
|
22
|
+
"order_id": "o-123",
|
|
23
|
+
"total": 49.99,
|
|
24
|
+
"timestamp": "2024-01-15T10:30:00Z",
|
|
25
|
+
"service": "api"
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Metrics
|
|
30
|
+
|
|
31
|
+
Track custom events via CloudWatch Embedded Metric Format (EMF):
|
|
32
|
+
|
|
33
|
+
```ruby
|
|
34
|
+
Belt::Observability::Metrics.track_event("OrderCreated", model: "Order")
|
|
35
|
+
Belt::Observability::Metrics.track_event("PaymentProcessed", amount: 49.99)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Built-in Metrics
|
|
39
|
+
|
|
40
|
+
The Lambda handler automatically emits:
|
|
41
|
+
- `RequestCount` — per invocation
|
|
42
|
+
- `ErrorCount` — on unhandled exceptions
|
|
43
|
+
- `Latency` — request duration in milliseconds
|
|
44
|
+
|
|
45
|
+
### Namespace
|
|
46
|
+
|
|
47
|
+
Metrics are published under the namespace set by `BELT_METRICS_NAMESPACE`
|
|
48
|
+
(defaults to `Belt`). View them in CloudWatch → Metrics → Custom namespaces.
|
|
49
|
+
|
|
50
|
+
## Error Alerting
|
|
51
|
+
|
|
52
|
+
Set `ERROR_NOTIFICATION_TOPIC_ARN` to an SNS topic ARN. Unhandled errors
|
|
53
|
+
will publish a notification with error details (message, backtrace, request context).
|
|
54
|
+
|
|
55
|
+
```yaml
|
|
56
|
+
# config/lambda/api.yml
|
|
57
|
+
environment:
|
|
58
|
+
ERROR_NOTIFICATION_TOPIC_ARN: ${var.sns_topic_arn}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## CloudWatch Logs Insights
|
|
62
|
+
|
|
63
|
+
Query structured logs:
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
fields @timestamp, message, order_id
|
|
67
|
+
| filter level = "ERROR"
|
|
68
|
+
| sort @timestamp desc
|
|
69
|
+
| limit 20
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Viewing Logs Locally
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
belt logs api # tail logs for the "api" Lambda
|
|
76
|
+
belt logs api -f # follow (live tail)
|
|
77
|
+
belt logs api -s 30m # last 30 minutes
|
|
78
|
+
belt logs worker -e prod # specific environment
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Environment Variables
|
|
82
|
+
|
|
83
|
+
| Variable | Purpose |
|
|
84
|
+
|----------|---------|
|
|
85
|
+
| `BELT_METRICS_NAMESPACE` | CloudWatch metrics namespace (default: `Belt`) |
|
|
86
|
+
| `ACTION` | Service name in log entries (falls back to function name) |
|
|
87
|
+
| `ERROR_NOTIFICATION_TOPIC_ARN` | SNS topic for error alerts |
|
|
88
|
+
| `ENVIRONMENT` | Controls verbose errors (`dev*`/`local`/`test` = verbose) |
|
|
89
|
+
|
|
90
|
+
## See Also
|
|
91
|
+
|
|
92
|
+
- `belt explain lambda_handler` — how observability is initialized
|
|
93
|
+
- `belt explain deployment` — configuring environment variables
|
|
94
|
+
- `belt logs --help` — full CLI options for log viewing
|