Skill 61 · AWS Marketplace Metering
Subchapter 61.9
references/query-patterns.mdMarkdown16 KBView on GitHub
python3 ./scripts/query_metering.py --region us-east-1 --days 30 --query-type top_customers --top-n 10Total usage for last 7 days:
python3 ./scripts/query_metering.py --region us-east-1 --query-type summaryAll failures last 7 days:
python3 ./scripts/query_metering.py --region us-east-1 --query-type list_failuresSpecific customer detail:
python3 ./scripts/query_metering.py --region us-east-1 --customer-id 123456789012 --query-type detailCount by dimension:
python3 ./scripts/query_metering.py --region us-east-1 --dimension fgt_cnf_hours --query-type countAWS Marketplace metering data is available in multiple tiers. Choose based on your needs:
| Tier | Latency | Retention | Cost | Shows |
|---|---|---|---|---|
| 1. CloudTrail Event History | ~15 min | 90 days | Free (always on) | Submitted records (raw API calls) |
| 2. Seller Reports (AMMP) | ~24 hours | Full history | Free | Billed records with revenue data |
| 3. DDB Metering Table (PITR export) | Real-time | Configurable | Low (DDB + S3 storage) | All submitted records from deployed stack |
| 4. CloudTrail trail → S3 | ~5-15 min | Indefinite (your S3 lifecycle) | Low (S3 storage; first trail is free) | Raw BatchMeterUsage/ResolveCustomer API events (the same submissions view as Event History, but retained beyond 90 days) |
Two complementary options (either or both), depending on whether you want the raw API-call audit record or the pipeline’s stored records:
BatchMeterUsage/ResolveCustomer call — the SAME submissions view as Event History — but retained INDEFINITELY per your S3 lifecycle (Event History alone only keeps 90 days). Query the delivered logs with Athena. This is the durable, low-cost audit trail for “what did I submit” beyond 90 days. (Use a plain trail-to-S3, NOT CloudTrail Lake — Lake adds cost/setup with no benefit here.)aggregated_usage table via DynamoDB PITR to S3 and query with Athena — full history of the records the pipeline persisted (see Tier 3 below).
Note: neither is BILLED revenue — for billed/disbursed amounts use Seller Reports (Tier 2). CloudTrail (trail or Event History) shows ALL submissions incl. rejected/duplicate.Use this for real-time queries (last 90 days, ~15 min delay). Event History is ALWAYS ON — do
NOT use CloudTrail Lake / create-event-data-store for metering lookups (no data store is
needed; Lake adds cost and setup for no benefit here):
aws cloudtrail lookup-events \
--lookup-attributes AttributeKey=EventSource,AttributeValue=metering-marketplace.amazonaws.com \
--region <SELLER_METERING_REGION> \
--start-time $(date -d '7 days ago' -u +%Y-%m-%dT%H:%M:%SZ) \
--end-time $(date -u +%Y-%m-%dT%H:%M:%SZ) \
--max-results 50Key facts:
--next-token from response to paginate (see Pagination section below)eventSource=metering-marketplace.amazonaws.com (CloudTrail eventSource — note the hyphen; distinct from the CLI service name meteringmarketplace)responseElements — check results (per-record status) vs unprocessedRecords FIRST; a record in unprocessedRecords was not accepted and should be retried. CloudTrail lowercases response field names (results/status/meteringRecordId/unprocessedRecords); the direct SDK BatchMeterUsage response is PascalCase (Results/Status/MeteringRecordId/UnprocessedRecords).Use Seller Reports when:
If the seller deployed the metering stack (from the setting-up-marketplace-metering skill), their DynamoDB MeteringRecords table stores all submitted records with Point-in-Time Recovery enabled:
Use DDB PITR when:
CloudTrail Event History (Tier 1) is always on but only retains 90 days. For a durable
record of the raw BatchMeterUsage/ResolveCustomer API calls beyond that, create a CloudTrail
trail that delivers events to an S3 bucket:
metering-marketplace API calls are
management events, so no data-event config is needed. The first trail per account is free;
you pay only S3 storage (apply an S3 lifecycle policy to age/expire objects).eventsource = 'metering-marketplace.amazonaws.com' — the SAME submissions view as
Event History (results[].status, unprocessedRecords, request-level exceptions), but with
the retention YOU choose.create-event-data-store) — Lake adds
cost and setup for no benefit for this audit use.Use a CloudTrail trail to S3 when:
Use these patterns when the seller needs real-time data (last 90 days). MUST use the region where BatchMeterUsage is called (ask the seller — do not assume us-east-1).
aws cloudtrail lookup-events \
--lookup-attributes AttributeKey=EventSource,AttributeValue=metering-marketplace.amazonaws.com \
--region <SELLER_METERING_REGION> \
--start-time $(date -d '7 days ago' -u +%Y-%m-%dT%H:%M:%SZ) \
--end-time $(date -u +%Y-%m-%dT%H:%M:%SZ) \
--max-results 50 --output json | \
python3 -c "
import sys, json
data = json.load(sys.stdin)
for event in data.get('Events', []):
ce = json.loads(event.get('CloudTrailEvent', '{}'))
results = ce.get('responseElements', {}).get('results', [])
for r in results:
if r.get('status') != 'Success':
ur = r.get('usageRecord', {})
print(f\" {r['status']} | account={ur.get('customerAWSAccountId','?')} dim={ur.get('dimension','?')} qty={ur.get('quantity','?')}\")
"Look for these failure statuses:
CustomerNotSubscribed — customer’s subscription is inactive, do NOT retryDuplicateRecord — first-write-wins and REPORTED; the original quantity stays billed. A resubmit with a different quantity is NOT billed (under-billing risk). Not simply “benign”; safe only for an identical retryTimestampOutOfBoundsException — timestamp older than 24 hours (rejects entire batch)aws cloudtrail lookup-events \
--lookup-attributes AttributeKey=EventSource,AttributeValue=metering-marketplace.amazonaws.com \
--region <SELLER_METERING_REGION> \
--start-time $(date -d '30 days ago' -u +%Y-%m-%dT%H:%M:%SZ) \
--max-results 50 --output json | \
python3 -c "
import sys, json
from collections import Counter
data = json.load(sys.stdin)
usage = Counter()
for event in data.get('Events', []):
ce = json.loads(event.get('CloudTrailEvent', '{}'))
for record in ce.get('requestParameters', {}).get('usageRecords', []):
acct = record.get('customerAWSAccountId', 'unknown')
qty = record.get('quantity', 0)
usage[acct] += qty
print('Top 10 buyers by usage:')
for acct, total in usage.most_common(10):
print(f' {acct}: {total} units')
"Note: This only covers one page (50 events). For complete results, paginate with --next-token (see Pagination section) or use --max-pages with the query script.
aws cloudtrail lookup-events \
--lookup-attributes AttributeKey=EventSource,AttributeValue=metering-marketplace.amazonaws.com \
--region <SELLER_METERING_REGION> \
--start-time $(date -d '7 days ago' -u +%Y-%m-%dT%H:%M:%SZ) \
--max-results 50 --output json | \
python3 -c "
import sys, json
TARGET = '123456789012' # replace with buyer's AWS account ID
data = json.load(sys.stdin)
for event in data.get('Events', []):
ce = json.loads(event.get('CloudTrailEvent', '{}'))
for record in ce.get('requestParameters', {}).get('usageRecords', []):
if record.get('customerAWSAccountId') == TARGET:
print(f\" {event['EventTime']} | dim={record.get('dimension')} qty={record.get('quantity')} licenseArn={record.get('licenseArn','N/A')}\")
for result in ce.get('responseElements', {}).get('results', []):
ur = result.get('usageRecord', {})
if ur.get('customerAWSAccountId') == TARGET:
print(f\" → status={result.get('status')} meteringRecordId={result.get('meteringRecordId','N/A')}\")
"aws cloudtrail lookup-events \
--lookup-attributes AttributeKey=EventSource,AttributeValue=metering-marketplace.amazonaws.com \
--region <SELLER_METERING_REGION> \
--start-time $(date -d '30 days ago' -u +%Y-%m-%dT%H:%M:%SZ) \
--max-results 50 --output json | \
python3 -c "
import sys, json
from collections import Counter
data = json.load(sys.stdin)
by_dim = Counter()
for event in data.get('Events', []):
ce = json.loads(event.get('CloudTrailEvent', '{}'))
for record in ce.get('requestParameters', {}).get('usageRecords', []):
dim = record.get('dimension', 'unknown')
qty = record.get('quantity', 0)
by_dim[dim] += qty
print('Usage by dimension:')
for dim, total in by_dim.most_common():
print(f' {dim}: {total} units')
"aws cloudtrail lookup-events \
--lookup-attributes AttributeKey=EventSource,AttributeValue=metering-marketplace.amazonaws.com \
--region <SELLER_METERING_REGION> \
--start-time $(date -d '1 day ago' -u +%Y-%m-%dT%H:%M:%SZ) \
--max-results 50 --output json | \
python3 -c "
import sys, json
TARGET_ID = '<METERING_RECORD_ID>' # from BatchMeterUsage response
data = json.load(sys.stdin)
for event in data.get('Events', []):
ce = json.loads(event.get('CloudTrailEvent', '{}'))
for result in ce.get('responseElements', {}).get('results', []):
if result.get('meteringRecordId') == TARGET_ID:
print(f\"Found: status={result['status']}\")
print(f\" Record: {json.dumps(result['usageRecord'], default=str)}\")
break
"Verification path (3 methods):
Results vs UnprocessedRecords in the direct BatchMeterUsage SDK/API response (PascalCase)meteringRecordId — CloudTrail responseElements is lowercase (results/status/meteringRecordId/unprocessedRecords)# First page
RESULT=$(aws cloudtrail lookup-events \
--lookup-attributes AttributeKey=EventSource,AttributeValue=metering-marketplace.amazonaws.com \
--region <SELLER_METERING_REGION> --max-results 50 --output json)
TOKEN=$(echo $RESULT | python3 -c "import sys,json; r=json.load(sys.stdin); print(r.get('NextToken',''))")
# Subsequent pages
while [ -n "$TOKEN" ]; do
RESULT=$(aws cloudtrail lookup-events \
--lookup-attributes AttributeKey=EventSource,AttributeValue=metering-marketplace.amazonaws.com \
--region <SELLER_METERING_REGION> --max-results 50 --next-token "$TOKEN" --output json)
TOKEN=$(echo $RESULT | python3 -c "import sys,json; r=json.load(sys.stdin); print(r.get('NextToken',''))")
doneFor large result sets (full month of data), consider:
> results.json) for large result sets--max-pages to cap memory usage and run multiple bounded queries with date rangesThree most common causes:
eventSource=metering-marketplace.amazonaws.com (not aws-marketplace or marketplace).This is expected. CloudTrail shows all submitted records — including:
DuplicateRecord — first-write-wins; the original quantity was billed (a differing-quantity resubmit is not billed — an under-billing risk, not simply “benign”)CustomerNotSubscribed — rejected, never billedSeller Reports show only successfully billed amounts. The difference between these two views is the rejected/duplicate records.
This means your dimension name does not match exactly (case-sensitive). There is NO runtime error — usage is silently dropped. Call aws marketplace-catalog describe-entity to verify the exact dimension key. Common mistakes: camelCase vs snake_case, extra spaces, typos.
--max-pages to cap the number of API calls (each page = 50 events)--limit to cap the number of retained records for detail queries--start-date and --end-date to narrow the time window