Integrations
Enable usage logs on AWS API Gateway
Stream Amazon API Gateway access logs to Apiable through Kinesis Firehose so Apiable can meter usage per plan and per subscription. This is what turns on usage-based billing and per-subscription analytics.
Usage logs are how Apiable sees what flows through your Amazon API Gateway. You deploy a Kinesis Firehose stream that ships your gateway's access logs to an S3 bucket, point the gateway stage at that stream, and paste a one-line JSON log format. Apiable then meters every call per plan and per subscription. This is the path that turns on usage-based billing and per-subscription analytics.
What do usage logs on AWS do for Apiable?
They give Apiable a per-request record of every API call. Apiable reads those records to attribute usage to the right plan and subscription, which is what usage-based billing and developer analytics run on.
A subscription on AWS already carries an API key and a rate limit. Usage logs add the missing piece: the actual call-by-call traffic, tagged with the plan and subscription it belongs to. Apiable ingests the delivered log files from your bucket and loads them so reports and metered billing can use them.
How do AWS access logs reach Apiable?
Through Amazon Kinesis Firehose. Your gateway stage writes access logs to a Firehose delivery stream, Firehose writes them to an S3 bucket, and Apiable ingests the files from that bucket.
The flow is:
- Your API Gateway stage emits one access log line per request.
- A Firehose delivery stream receives those lines and batches them. The stage writes to the stream directly, or through a CloudWatch Logs subscription filter if its access logs already go to CloudWatch.
- Firehose writes the batched files to an S3 bucket under the prefix
apiable/aws/logs/. - Apiable reads new files from that prefix, attributes each row to a plan and subscription, and loads them for billing and analytics.
Access logs or execution logs?
Access logs only. This distinction matters to security reviewers, so it is worth being precise about it.
Amazon API Gateway produces two different kinds of log, and they are configured separately on the stage:
| Access logs | Execution logs | |
|---|---|---|
| What they contain | One structured summary line per request, carrying exactly the fields named in your log format | API Gateway's verbose internal trace, including request and response bodies, headers, and integration detail |
| Who defines the shape | You do, in the Log Format field | API Gateway does |
| Where they go | Wherever you point them. Here, a Firehose stream, directly or through a CloudWatch log group | A CloudWatch log group in your account |
| Does Apiable use them | Yes | No |
Everything on this page configures Custom access logging only. Apiable never enables execution logging, never reads a CloudWatch execution log group, and has no permission to do either. The gateway role grants API Gateway actions only, with no CloudWatch Logs access at all.
The practical consequence is worth stating plainly: nothing reaches the bucket that is not a key in the log format you paste. Request bodies, response bodies, and headers are not among those keys, so they are never delivered. If your security team wants to review exactly what leaves the account, the log format table below is the complete list. There is no other channel.
How do you deploy the logs bucket and Firehose stream with one click?
Apiable publishes both as CloudFormation templates. You create two stacks from the AWS console, in order: the bucket first, then the stream that writes to it.
- Open the CloudFormation console in the region your API Gateway runs in.
- Choose Create stack, then With new resources.
- Select Amazon S3 URL and paste the template URL for the stack you are creating.
- Fill in the parameters, then create the stack.
Stack 1: the logs bucket
https://apiable-launchstack-templates.s3.amazonaws.com/apiable-logs-bucket/1.0.0/template.yaml| Parameter | What to enter |
|---|---|
TenantName | Your Apiable tenant name, lowercase. The bucket is named apiable-logs-<TenantName>. |
ApiablePartnerAccount | Leave the default. This is the Apiable account that collects the delivered files. The stack grants it access to this bucket only. |
When the stack completes, open its Outputs tab and copy the bucket name, the bucket ARN, and the role ARN. You need the bucket ARN for the next stack, and all three go to Apiable.
Stack 2: the Firehose delivery stream
https://apiable-launchstack-templates.s3.amazonaws.com/apiable-usagelogs-stream/1.1.0/template.yaml| Parameter | What to enter |
|---|---|
LogsBucketArn | The bucket ARN from stack 1, for example arn:aws:s3:::apiable-logs-dev. |
StreamName | Leave the default. The stream is created as amazon-apigateway-usagelogs. |
DestinationPrefix | Leave the default apiable/aws. Apiable reads from apiable/aws/logs/. |
LogSource | Leave the default apigateway_direct if the gateway stage will write access logs straight to the stream, which is what the console steps below set up. Choose cloudwatch_logs only if the stage already sends access logs to a CloudWatch log group and you want a subscription filter to feed the stream from there. See feeding the stream from CloudWatch Logs. |
Copy the delivery stream ARN from the stack's Outputs tab, where it is listed as FirehoseArn. You paste it into the gateway stage when you turn on access logging.
How do you deploy the same components with Terraform?
Use the Apiable Terraform modules if you manage this account with Terraform. They create the same bucket and stream as the one-click stacks. Each module is published as a zip archive beside its template, so terraform init downloads it with no registry and no credentials.
module "apiable_logs_bucket" {
source = "https://apiable-launchstack-templates.s3.amazonaws.com/apiable-logs-bucket/1.0.0/terraform.zip"
name = "your-tenant-name"
}
module "apiable_usagelogs_stream" {
source = "https://apiable-launchstack-templates.s3.amazonaws.com/apiable-usagelogs-stream/1.1.0/terraform.zip"
name = "usagelogs"
logs_bucket_arn = module.apiable_logs_bucket.bucket_arn
}| Module and variable | What to enter |
|---|---|
Bucket name | Your Apiable tenant name, lowercase. The bucket is named apiable-logs-<name>. |
Bucket partner_account | Leave it out. The default is the Apiable account that collects the delivered files. |
Stream name | A name for the stream. usagelogs creates amazon-apigateway-usagelogs, the same name the one-click stack uses. |
Stream logs_bucket_arn | The ARN of the logs bucket. |
Stream log_source | Leave it out for the direct path. Set "cloudwatch_logs" only for the CloudWatch Logs path. As with the stack, changing it later replaces the stream. |
After terraform apply, the bucket module outputs bucket_name, bucket_arn, and s3_assume_role_arn, and the stream module outputs firehose_arn. Send the bucket values to Apiable, and paste firehose_arn into the gateway stage.
If you already have an Apiable logs bucket, do not apply the bucket module for it. Import the existing bucket into Terraform first. Each module's README, inside the archive, lists the import commands.
How do you deploy the same components with the CDK?
Use this route only if you already manage the account with the CDK. It produces the same bucket and stream as the one-click stacks, on the direct path.
Run the two scripts from the public apiable/cdk repository against your AWS account: first the logs bucket, then the Firehose stream that targets it. Bootstrap CDK once first if this is a new account or region.
- Clone
apiable/cdkand install the AWS CDK toolkit. - Bootstrap CDK for your account and region, for example
./cdk-bootstrap.sh 123456789012 eu-central-1. - Deploy the S3 logs bucket. Skip this if you already have one.
- Deploy the Firehose stream, passing the bucket ARN from the previous step.
Deploy the S3 logs bucket
deploy-logs-bucket.sh takes your AWS account ID, region, and a stack name. The stack name becomes part of the bucket, so a stack name of dev produces a bucket named apiable-logs-dev.
./deploy-logs-bucket.sh 123456789012 eu-central-1 devThis creates the stack apiable-<stackname>-logs-bucket and a bucket whose ARN looks like arn:aws:s3:::apiable-logs-<stackname>. Note the bucket name, its ARN, the role ARN, and the region. You will pass the bucket ARN to the next step and send the rest to Apiable.
Deploy the Firehose stream
deploy-usagelogs-stream.sh takes your AWS account ID, region, the logs bucket ARN, and a stack name. It creates a Firehose delivery stream that writes to the bucket under the apiable/aws/logs/ prefix.
./deploy-usagelogs-stream.sh 123456789012 eu-central-1 arn:aws:s3:::apiable-logs-dev devThe stream is named amazon-apigateway-usagelogs-<stackname>, so a stack name of dev produces amazon-apigateway-usagelogs-dev. The name must start with amazon-apigateway-, which is an API Gateway requirement for access log destinations. The script writes the stream's ARN to cdk-outputs.json. Copy that ARN for the gateway stage.
The CDK scripts build the direct path only. For the CloudWatch Logs path, use the one-click stack or the Terraform module.
How do you turn on access logging in the API Gateway console?
In the API Gateway console, open the stage, edit its Logs and Tracing settings, enable custom access logging, paste the Firehose ARN as the destination, and paste the one-line log format. Then save.
- Open the API Gateway console and select your API.
- Go to Stages and select the stage you want to meter.
- Open Logs and Tracing and choose to edit.
- Enable Custom access logging.
- In Access log destination ARN, paste the Firehose stream ARN from the stack outputs, the Terraform output, or
cdk-outputs.json. - In Log Format, paste the one-line JSON format below.
- Save the changes.
How do you feed the stream from CloudWatch Logs?
Create the stream with LogSource set to cloudwatch_logs, keep the stage's access logs going to their CloudWatch log group, and add a subscription filter on that group that sends to the stream. Firehose unwraps the CloudWatch records, so the bucket receives the same rows as on the direct path.
- Create the stream with
LogSourceset tocloudwatch_logsin the one-click stack, orlog_source = "cloudwatch_logs"in the Terraform module. - On the stage, keep Custom access logging pointed at your CloudWatch log group, and set Log Format to the one-line format below. The format is the same on both paths.
- Create an IAM role that CloudWatch Logs can assume, with permission to call
firehose:PutRecordon the stream. - Create a subscription filter on the access-log group, with the Firehose stream as the destination and the role from step 3. Leave the filter pattern empty so every line is forwarded.
What is the exact log format to paste?
Use this JSON object as a single line. The keys are fixed. Apiable reads them by name, so paste the format exactly as it appears here even if some values will not resolve on your gateway.
{"api_id": "$context.apiId","api_key": "$context.identity.apiKey","key_id": "$context.identity.apiKeyId","ip": "$context.identity.sourceIp","method": "$context.httpMethod","uri": "$context.path","response_size": "$context.responseLength","response_status": "$context.status","resource_id": "$context.resourceId","request_id": "$context.requestId","request_latency": "$context.responseLatency","request_time": "$context.requestTimeEpoch","stage": "$context.stage","plan_id": "$context.authorizer.apiable_plan_id","subscription_id": "$context.authorizer.apiable_subscription_id"}| Key | Source | What it carries |
|---|---|---|
api_id | $context.apiId | The REST API the call hit. |
api_key | $context.identity.apiKey | The API key value on the request. Apiable does not use it in this format, so you can omit it. |
key_id | $context.identity.apiKeyId | The ID of the API key on the request. Apiable resolves the subscription from it whenever subscription_id is a dash or missing. |
ip | $context.identity.sourceIp | The caller's source IP. |
method | $context.httpMethod | The HTTP method. |
uri | $context.path | The request path. |
response_size | $context.responseLength | The response size in bytes. |
response_status | $context.status | The HTTP status code. |
resource_id | $context.resourceId | The matched resource. |
request_id | $context.requestId | The unique request ID. |
request_latency | $context.responseLatency | The response latency. |
request_time | $context.requestTimeEpoch | The request time, epoch. |
stage | $context.stage | The stage that served the call. |
plan_id | $context.authorizer.apiable_plan_id | The Apiable plan, from the authorizer. Resolves to a dash if your authorizer does not set it. |
subscription_id | $context.authorizer.apiable_subscription_id | The Apiable subscription, from the authorizer. Resolves to a dash if your authorizer does not set it, and Apiable then resolves the subscription from key_id. |
When does usage start showing up?
After the stream is delivering and Apiable has wired up your bucket, usage appears once calls flow through the metered stage. Firehose batches before it writes, so allow a short delay between a call and the record landing.
The Firehose stream buffers records for up to five minutes, or until 5 MB accumulate, before writing to S3. Once files land under apiable/aws/logs/, Apiable ingests them about once an hour and the usage becomes available to billing and analytics. Allow for both delays when you test, so usage can take a little over an hour to appear. Make a few test calls through the stage, wait for the next ingest, then check Developer Analytics for the subscription.
Troubleshooting
Match what you see to the fix.
| What you see | What to do |
|---|---|
| No usage appears after enabling logging | Confirm the stage has Custom access logging enabled, the destination is the Firehose stream (or, on the CloudWatch Logs path, that a subscription filter sends the log group to the stream), and the log format is pasted as one line. |
| The log format is rejected or logs look broken | The format must be a single line of valid JSON. Re-paste it with no line breaks, using the exact keys above. |
| Files are landing in the bucket but nothing at all is metered | Check that the calls carry an API key, so key_id has a value, or that your authorizer sets subscription_id. Apiable discards a row that has neither, silently and with no AWS-side error. Also confirm Apiable has connected ingestion to your bucket. |
| Calls are logged but not attributed to a subscription | Apiable resolves the subscription from key_id when subscription_id is a dash or missing. Confirm the calls use the API key Apiable issued for the subscription. A key created outside Apiable resolves to no subscription. |
| You copied the format from a README or an older runbook | Some older examples omit key_id, subscription_id, or plan_id. Use the format on this page, which is the one Apiable ingests. |
CloudFormation rejects the LogSource value | The accepted values are apigateway_direct and cloudwatch_logs, lowercase with an underscore. |
Updating the stack to change LogSource fails and rolls back | The path is fixed when the stream is created. Create a second stream with a different StreamName and the new value, move your access logs to it, then delete the old stack. |
| The destination ARN will not accept your Firehose stream | The Firehose stream name must start with amazon-apigateway-. The Apiable templates, modules, and CDK scripts name it that way. A Firehose stream cannot be renamed, so if you created one by hand, create it again with a matching name. |
| Files land in S3 but never get ingested | Send the region, bucket name, bucket ARN, and role ARN to support@apiable.io so Apiable can connect ingestion to your bucket. |
| Usage is metered but does not bill | Usage logs feed metered billing, but the plan decides what is billed. Check the plan's pricing settings in usage-based billing. |