Skip to content

Commit c6d092e

Browse files
Add multi-tenancy support
1 parent 1bfa699 commit c6d092e

File tree

13 files changed

+341
-15
lines changed

13 files changed

+341
-15
lines changed

.github/workflows/run-integration-test.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,12 +47,15 @@ jobs:
4747
echo "STACK_NAME=$stackName" >> "$GITHUB_OUTPUT"
4848
echo "Stack name = $stackName"
4949
sam deploy --stack-name "${stackName}" --parameter-overrides "ParameterKey=SecretToken,ParameterValue=${{ secrets.SECRET_TOKEN }}" "ParameterKey=LambdaRole,ParameterValue=${{ secrets.AWS_LAMBDA_ROLE }}" --no-confirm-changeset --no-progressbar > disable_output
50-
TEST_ENDPOINT=$(sam list stack-outputs --stack-name "${stackName}" --output json | jq -r '.[] | .OutputValue')
50+
TEST_ENDPOINT=$(sam list stack-outputs --stack-name "${stackName}" --output json | jq -r '.[] | select(.OutputKey=="HelloApiEndpoint") | .OutputValue')
51+
TENANT_ID_TEST_FUNCTION=$(sam list stack-outputs --stack-name "${stackName}" --output json | jq -r '.[] | select(.OutputKey=="TenantIdTestFunction") | .OutputValue')
5152
echo "TEST_ENDPOINT=$TEST_ENDPOINT" >> "$GITHUB_OUTPUT"
53+
echo "TENANT_ID_TEST_FUNCTION=$TENANT_ID_TEST_FUNCTION" >> "$GITHUB_OUTPUT"
5254
- name: run test
5355
env:
5456
SECRET_TOKEN: ${{ secrets.SECRET_TOKEN }}
5557
TEST_ENDPOINT: ${{ steps.deploy_stack.outputs.TEST_ENDPOINT }}
58+
TENANT_ID_TEST_FUNCTION: ${{ steps.deploy_stack.outputs.TENANT_ID_TEST_FUNCTION }}
5659
run: cd lambda-integration-tests && cargo test
5760
- name: cleanup
5861
if: always()
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
[package]
2+
name = "basic-tenant-id"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
[dependencies]
7+
lambda_runtime = { path = "../../lambda-runtime" }
8+
serde_json = "1.0"
9+
tokio = { version = "1", features = ["macros"] }
10+
tracing = { version = "0.1", features = ["log"] }
11+
tracing-subscriber = { version = "0.3", default-features = false, features = ["fmt"] }

examples/basic-tenant-id/README.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# Basic Tenant ID Example
2+
3+
This example demonstrates how to access and use tenant ID information in a Lambda function.
4+
5+
## Key Features
6+
7+
- Extracts tenant ID from Lambda runtime headers
8+
- Includes tenant ID in tracing logs
9+
- Returns tenant ID in the response
10+
- Handles cases where tenant ID is not provided
11+
12+
## Usage
13+
14+
The tenant ID is automatically extracted from the `lambda-runtime-aws-tenant-id` header and made available in the Lambda context.
15+
16+
```rust
17+
async fn function_handler(event: LambdaEvent<Value>) -> Result<Value, Error> {
18+
let (event, context) = event.into_parts();
19+
20+
// Access tenant ID from context
21+
match &context.tenant_id {
22+
Some(tenant_id) => println!("Processing for tenant: {}", tenant_id),
23+
None => println!("No tenant ID provided"),
24+
}
25+
26+
// ... rest of function logic
27+
}
28+
```
29+
30+
## Testing
31+
32+
You can test this function locally using cargo lambda:
33+
34+
```bash
35+
cargo lambda invoke --data-ascii '{"test": "data"}'
36+
```
37+
38+
The tenant ID will be None when testing locally unless you set up a mock runtime environment with the appropriate headers.
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
use lambda_runtime::{service_fn, Error, LambdaEvent};
2+
use serde_json::{json, Value};
3+
4+
async fn function_handler(event: LambdaEvent<Value>) -> Result<Value, Error> {
5+
let (event, context) = event.into_parts();
6+
7+
// Access tenant ID from context
8+
let tenant_info = match &context.tenant_id {
9+
Some(tenant_id) => format!("Processing request for tenant: {}", tenant_id),
10+
None => "No tenant ID provided".to_string(),
11+
};
12+
13+
tracing::info!("Request ID: {}", context.request_id);
14+
tracing::info!("Tenant info: {}", tenant_info);
15+
16+
// Include tenant ID in response
17+
let response = json!({
18+
"message": "Hello from Lambda!",
19+
"request_id": context.request_id,
20+
"tenant_id": context.tenant_id,
21+
"input": event
22+
});
23+
24+
Ok(response)
25+
}
26+
27+
#[tokio::main]
28+
async fn main() -> Result<(), Error> {
29+
tracing_subscriber::fmt()
30+
.with_max_level(tracing::Level::INFO)
31+
.with_target(false)
32+
.without_time()
33+
.init();
34+
35+
lambda_runtime::run(service_fn(function_handler)).await
36+
}

lambda-integration-tests/Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ aws_lambda_events = { path = "../lambda-events" }
1717
serde_json = "1.0.121"
1818
tokio = { version = "1", features = ["full"] }
1919
serde = { version = "1.0.204", features = ["derive"] }
20+
tracing = "0.1"
2021

2122
[dev-dependencies]
2223
reqwest = { version = "0.12.5", features = ["blocking"] }
@@ -31,3 +32,7 @@ path = "src/helloworld.rs"
3132
[[bin]]
3233
name = "authorizer"
3334
path = "src/authorizer.rs"
35+
36+
[[bin]]
37+
name = "tenant-id-test"
38+
path = "src/tenant_id_test.rs"
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
use lambda_runtime::{service_fn, Error, LambdaEvent};
2+
use serde_json::{json, Value};
3+
4+
async fn function_handler(event: LambdaEvent<Value>) -> Result<Value, Error> {
5+
let (event, context) = event.into_parts();
6+
7+
tracing::info!("Processing request with tenant ID: {:?}", context.tenant_id);
8+
9+
let response = json!({
10+
"statusCode": 200,
11+
"body": json!({
12+
"message": "Tenant ID test successful",
13+
"request_id": context.request_id,
14+
"tenant_id": context.tenant_id,
15+
"input": event
16+
}).to_string()
17+
});
18+
19+
Ok(response)
20+
}
21+
22+
#[tokio::main]
23+
async fn main() -> Result<(), Error> {
24+
lambda_runtime::tracing::init_default_subscriber();
25+
lambda_runtime::run(service_fn(function_handler)).await
26+
}

lambda-integration-tests/template.yaml

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,18 @@ Resources:
4141
Path: /hello
4242
Method: get
4343

44+
TenantIdTestFunction:
45+
Type: AWS::Serverless::Function
46+
Metadata:
47+
BuildMethod: rust-cargolambda
48+
BuildProperties:
49+
Binary: tenant-id-test
50+
Properties:
51+
CodeUri: ./
52+
Handler: bootstrap
53+
Runtime: provided.al2023
54+
Role: !Ref LambdaRole
55+
4456
AuthorizerFunction:
4557
Type: AWS::Serverless::Function
4658
Metadata:
@@ -59,4 +71,7 @@ Resources:
5971
Outputs:
6072
HelloApiEndpoint:
6173
Description: "API Gateway endpoint URL for HelloWorld"
62-
Value: !Sub "https://${API}.execute-api.${AWS::Region}.amazonaws.com/integ-test/hello/"
74+
Value: !Sub "https://${API}.execute-api.${AWS::Region}.amazonaws.com/integ-test/hello/"
75+
TenantIdTestFunction:
76+
Description: "Tenant ID test function name"
77+
Value: !Ref TenantIdTestFunction
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
use serde_json::json;
2+
3+
#[test]
4+
fn test_tenant_id_functionality_in_production() {
5+
let function_name = std::env::var("TENANT_ID_TEST_FUNCTION")
6+
.expect("TENANT_ID_TEST_FUNCTION environment variable not set");
7+
8+
// Test with tenant ID
9+
let payload_with_tenant = json!({
10+
"test": "tenant_id_test",
11+
"message": "Testing with tenant ID"
12+
});
13+
14+
let output = std::process::Command::new("aws")
15+
.args([
16+
"lambda", "invoke",
17+
"--function-name", &function_name,
18+
"--payload", &payload_with_tenant.to_string(),
19+
"--cli-binary-format", "raw-in-base64-out",
20+
"/tmp/tenant_response.json"
21+
])
22+
.output()
23+
.expect("Failed to invoke Lambda function");
24+
25+
assert!(output.status.success(), "Lambda invocation failed: {}",
26+
String::from_utf8_lossy(&output.stderr));
27+
28+
// Read and verify response
29+
let response = std::fs::read_to_string("/tmp/tenant_response.json")
30+
.expect("Failed to read response file");
31+
32+
let response_json: serde_json::Value = serde_json::from_str(&response)
33+
.expect("Failed to parse response JSON");
34+
35+
// Verify the function executed successfully
36+
assert_eq!(response_json["statusCode"], 200);
37+
38+
// Parse the body to check tenant_id field exists (even if null)
39+
let body: serde_json::Value = serde_json::from_str(
40+
response_json["body"].as_str().expect("Body should be a string")
41+
).expect("Failed to parse body JSON");
42+
43+
assert!(body.get("tenant_id").is_some(), "tenant_id field should be present in response");
44+
assert!(body.get("request_id").is_some(), "request_id should be present");
45+
assert_eq!(body["message"], "Tenant ID test successful");
46+
47+
println!("✅ Tenant ID production test passed");
48+
println!("Response: {}", serde_json::to_string_pretty(&response_json).unwrap());
49+
}

lambda-runtime/src/layers/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ mod api_response;
44
mod panic;
55

66
// Publicly available services.
7-
mod trace;
7+
pub mod trace;
88

99
pub(crate) use api_client::RuntimeApiClientService;
1010
pub(crate) use api_response::RuntimeApiResponseService;

lambda-runtime/src/layers/otel.rs

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -72,14 +72,26 @@ where
7272
}
7373

7474
fn call(&mut self, req: LambdaInvocation) -> Self::Future {
75-
let span = tracing::info_span!(
76-
"Lambda function invocation",
77-
"otel.name" = req.context.env_config.function_name,
78-
"otel.kind" = field::Empty,
79-
{ attribute::FAAS_TRIGGER } = &self.otel_attribute_trigger,
80-
{ attribute::FAAS_INVOCATION_ID } = req.context.request_id,
81-
{ attribute::FAAS_COLDSTART } = self.coldstart
82-
);
75+
let span = if let Some(tenant_id) = &req.context.tenant_id {
76+
tracing::info_span!(
77+
"Lambda function invocation",
78+
"otel.name" = req.context.env_config.function_name,
79+
"otel.kind" = field::Empty,
80+
{ attribute::FAAS_TRIGGER } = &self.otel_attribute_trigger,
81+
{ attribute::FAAS_INVOCATION_ID } = req.context.request_id,
82+
{ attribute::FAAS_COLDSTART } = self.coldstart,
83+
"tenant_id" = tenant_id
84+
)
85+
} else {
86+
tracing::info_span!(
87+
"Lambda function invocation",
88+
"otel.name" = req.context.env_config.function_name,
89+
"otel.kind" = field::Empty,
90+
{ attribute::FAAS_TRIGGER } = &self.otel_attribute_trigger,
91+
{ attribute::FAAS_INVOCATION_ID } = req.context.request_id,
92+
{ attribute::FAAS_COLDSTART } = self.coldstart
93+
)
94+
};
8395

8496
// After the first execution, we can set 'coldstart' to false
8597
self.coldstart = false;

0 commit comments

Comments
 (0)