Skip to content

Request Cost accounting - adding a cost for various api request actions.#32890

Merged
wezell merged 70 commits intomainfrom
issue-32886-request-cost
Jan 7, 2026
Merged

Request Cost accounting - adding a cost for various api request actions.#32890
wezell merged 70 commits intomainfrom
issue-32886-request-cost

Conversation

@wezell
Copy link
Copy Markdown
Member

@wezell wezell commented Aug 1, 2025

This PR Implements a request cost accounting system that tracks API usage and provides optional rate limiting using a leaky token bucket algorithm.

Key Features

Request Cost Tracking

  • Assigns cost values to expensive operations (ES queries, file operations, image processing, etc.)
  • Uses ByteBuddy AOP to intercept annotated methods via @RequestCost
  • Adds x-dotrequest-cost response header showing request cost
  • Periodic logging of request metrics (requests, total cost, avg cost per request)

Optional Rate Limiting

  • Leaky token bucket implementation (LeakyTokenBucketImpl)
  • Adds x-dotratelimit-toks-max response header showing how many tokens remain in bucket and the max bucket value.
  • Configurable via RATE_LIMIT_ENABLED, RATE_LIMIT_REFILL_PER_SECOND, RATE_LIMIT_MAX_BUCKET_SIZE
  • Disabled by default.
  • When enabled, it will subtract the cost of any request from the tokens in the bucket. The bucket will refill at the rate of RATE_LIMIT_REFILL_PER_SECOND. If the bucket is empty, the rate limiter will reject requests with a 429 or To Many Request error when tokens are exhausted.
Screenshot 2026-01-07 at 10 08 45 AM

Accounting Modes

If you are logged in, you can add a query parameter dotAccounting to any request to get an accounting of its cost, e.g.

https://local.dotcms.site:8443/?dotAccounting=LOG
  • NONE - No tracking
  • HEADER - Cost in response header (default)
  • LOG - Detailed logging per operation
  • HTML - Visual report for debugging (admin only)

Configuration with defaults

  REQUEST_COST_ACCOUNTING_ENABLED=true     # Enable/disable cost reporting
  REQUEST_COST_TIME_WINDOW_SECONDS=60      # Cost Logging interval
  RATE_LIMIT_ENABLED=false                 # Enable hard rate limiting
  RATE_LIMIT_REFILL_PER_SECOND=500         # Token refill rate
  RATE_LIMIT_MAX_BUCKET_SIZE=10000         # Max tokens
  RATE_LIMIT_ADD_HEADER_INFO=true          # Adds header which reports on number of tokens remaining

Tests

  • Added unit tests for RequestCostApi, RequestCostFilter, RequestCostRequestListener, LeakyTokenBucketImpl and RequestCostReport for validation.
  • Integration tests verify HTML report behavior and accounting stability across requests.

To Do

  • Create a new LeakyTokenBucketImpl that reads token and bucket information from reddit for use across load balanced systems.
  • Log expensive requests, e.g. log any request > 1000 tokens

ref: #32886

This PR fixes: #32886

…adding a cost for various api request actions.

ref: #32886
Comment thread dotCMS/src/main/java/com/dotcms/cost/RequestCostApi.java Outdated
@github-actions github-actions Bot added the stale label Sep 11, 2025
@github-actions github-actions Bot closed this Sep 19, 2025
@wezell wezell reopened this Oct 7, 2025
@wezell wezell changed the title feat(api costing): this is a poc of how we could do api accounting - adding a cost for various api request actions. feat(api costing): api accounting - adding a cost for various api request actions. Oct 7, 2025
@github-actions github-actions Bot removed the stale label Oct 8, 2025
wezell added 10 commits October 13, 2025 14:12
Implementing request costing.
- Adding annotation for @RequestCost(increment=2)
- RequestCostAPI
- Adding appropriate annotations
- Adding cost reporting

ref: #32890
Implementing request costing.
- Adding Tests

ref: #32890
Implementing request costing.
- Adding Tests

ref: #32890
Implementing request costing.
fixing annotations

ref: #32890
Implementing request costing.
fixing reporting

ref: #32890
Using an Enum to centralize pricing

ref: #32890
Using an Enum to centralize pricing

ref: #32890
Using an Enum to centralize pricing

ref: #32890
@dotCMS dotCMS deleted a comment from github-actions Bot Oct 15, 2025
@dotCMS dotCMS deleted a comment from github-actions Bot Oct 15, 2025
@wezell wezell requested a review from dsilvam December 23, 2025 13:48
Copy link
Copy Markdown
Member

@sfreudenthaler sfreudenthaler left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

my friend Claude and I agree that this is good to go 🥇 🤖

@wezell wezell added this pull request to the merge queue Jan 7, 2026
Merged via the queue into main with commit f2992b2 Jan 7, 2026
42 checks passed
@wezell wezell deleted the issue-32886-request-cost branch January 7, 2026 17:36
github-merge-queue Bot pushed a commit that referenced this pull request Jan 27, 2026
This PR fixes: #34300


## Cleanup: Remove container variantId usage and use page-level
variantId

### Problem

The codebase was attempting to extract `variantId` from container
objects via `container?.parentPermissionable?.variantId`, but this
property no longer exists after [PR
#32890](#32890). This resulted in
`variantId` always being `undefined` when used in container operations.

Additionally, containers don't actually have `variantId` - it's a
**page-level concept**, not a container-level one. The
`/api/v1/page/copyContent` endpoint expects the **page-level variantId**
in the `DotTreeNode` payload, not a container's variantId.

### Changes

1. **Removed `variantId` from `getContainersData` function**
(`libs/sdk/uve/src/lib/dom/dom.utils.ts`)
- Removed the TODO comment and code attempting to extract `variantId`
from `parentPermissionable.variantId`
   - This property no longer exists in the API response

2. **Removed `variantId` from `EditableContainerData` interface**
(`libs/sdk/types/src/lib/editor/public.ts`)
- Removed the optional `variantId?: string` property since containers
don't have variantId

3. **Updated `getCurrentTreeNode` to use page-level variantId**
(`libs/portlets/edit-ema/portlet/src/lib/store/features/editor/withEditor.ts`)
- Changed from using `container.variantId` to `store.$variantId()` which
provides the page-level variantId
- This ensures the correct variantId is sent to the
`/api/v1/page/copyContent` endpoint

4. **Made `ContainerPayload.variantId` optional**
(`libs/portlets/edit-ema/portlet/src/lib/shared/models.ts`)
- Changed from `variantId: string` to `variantId?: string` with
documentation explaining containers don't have variantId

5. **Updated tests**
(`libs/portlets/edit-ema/portlet/src/lib/store/features/editor/withEditor.spec.ts`)
   - Updated test expectations to verify page-level variantId is used
   - Added test cases to ensure container variantId is ignored

### Impact

- ✅ **Fixes incorrect API calls**: The `/api/v1/page/copyContent`
endpoint now receives the correct page-level variantId instead of
`undefined`
- ✅ **Removes dead code**: Eliminates code attempting to access a
non-existent property
- ✅ **Improves code clarity**: Makes it explicit that variantId is a
page-level concept, not container-level
- ✅ **No breaking changes**: `ContainerPayload.variantId` is now
optional, maintaining backward compatibility

### Testing

- Updated unit tests to verify `getCurrentTreeNode` uses page-level
variantId from `store.$variantId()`
- Added test cases to ensure container variantId is properly ignored
- All existing tests pass

### Related

- Related to issue: [Issue
#34300](#34300)
- Follows up on: [PR #32890](#32890)
which removed `parentPermissionable` from container API responses
github-merge-queue Bot pushed a commit that referenced this pull request Jan 27, 2026
This PR fixes: #34300


## Cleanup: Remove container variantId usage and use page-level
variantId

### Problem

The codebase was attempting to extract `variantId` from container
objects via `container?.parentPermissionable?.variantId`, but this
property no longer exists after [PR
#32890](#32890). This resulted in
`variantId` always being `undefined` when used in container operations.

Additionally, containers don't actually have `variantId` - it's a
**page-level concept**, not a container-level one. The
`/api/v1/page/copyContent` endpoint expects the **page-level variantId**
in the `DotTreeNode` payload, not a container's variantId.

### Changes

1. **Removed `variantId` from `getContainersData` function**
(`libs/sdk/uve/src/lib/dom/dom.utils.ts`)
- Removed the TODO comment and code attempting to extract `variantId`
from `parentPermissionable.variantId`
   - This property no longer exists in the API response

2. **Removed `variantId` from `EditableContainerData` interface**
(`libs/sdk/types/src/lib/editor/public.ts`)
- Removed the optional `variantId?: string` property since containers
don't have variantId

3. **Updated `getCurrentTreeNode` to use page-level variantId**
(`libs/portlets/edit-ema/portlet/src/lib/store/features/editor/withEditor.ts`)
- Changed from using `container.variantId` to `store.$variantId()` which
provides the page-level variantId
- This ensures the correct variantId is sent to the
`/api/v1/page/copyContent` endpoint

4. **Made `ContainerPayload.variantId` optional**
(`libs/portlets/edit-ema/portlet/src/lib/shared/models.ts`)
- Changed from `variantId: string` to `variantId?: string` with
documentation explaining containers don't have variantId

5. **Updated tests**
(`libs/portlets/edit-ema/portlet/src/lib/store/features/editor/withEditor.spec.ts`)
   - Updated test expectations to verify page-level variantId is used
   - Added test cases to ensure container variantId is ignored

### Impact

- ✅ **Fixes incorrect API calls**: The `/api/v1/page/copyContent`
endpoint now receives the correct page-level variantId instead of
`undefined`
- ✅ **Removes dead code**: Eliminates code attempting to access a
non-existent property
- ✅ **Improves code clarity**: Makes it explicit that variantId is a
page-level concept, not container-level
- ✅ **No breaking changes**: `ContainerPayload.variantId` is now
optional, maintaining backward compatibility

### Testing

- Updated unit tests to verify `getCurrentTreeNode` uses page-level
variantId from `store.$variantId()`
- Added test cases to ensure container variantId is properly ignored
- All existing tests pass

### Related

- Related to issue: [Issue
#34300](#34300)
- Follows up on: [PR #32890](#32890)
which removed `parentPermissionable` from container API responses
dsolistorres pushed a commit that referenced this pull request Feb 20, 2026
…ns. (#32890)

This PR Implements a request cost accounting system that tracks API
usage and provides optional rate limiting using a leaky token bucket
algorithm.

## Key Features

### Request Cost Tracking
- Assigns cost values to expensive operations (ES queries, file
operations, image processing, etc.)
  - Uses ByteBuddy AOP to intercept annotated methods via @RequestCost
  - Adds `x-dotrequest-cost` response header showing request cost
- Periodic logging of request metrics (requests, total cost, avg cost
per request)

### Optional Rate Limiting
  - Leaky token bucket implementation (LeakyTokenBucketImpl)
- Adds `x-dotratelimit-toks-max` response header showing how many tokens
remain in bucket and the max bucket value.
- Configurable via `RATE_LIMIT_ENABLED`, `RATE_LIMIT_REFILL_PER_SECOND`,
`RATE_LIMIT_MAX_BUCKET_SIZE`
  - Disabled by default.
- When enabled, it will subtract the cost of any request from the tokens
in the bucket. The bucket will refill at the rate of
`RATE_LIMIT_REFILL_PER_SECOND`. If the bucket is empty, the rate limiter
will reject requests with a `429` or `To Many Request` error when tokens
are exhausted.


<img width="592" height="297" alt="Screenshot 2026-01-07 at 10 08 45 AM"
src="https://github.com/user-attachments/assets/b96b9a75-abc0-478d-bac2-1715af0eb56c"
/>

### Accounting Modes
If you are logged in, you can add a query parameter `dotAccounting` to
any request to get an accounting of its cost, e.g.
```
https://local.dotcms.site:8443/?dotAccounting=LOG
```
  - NONE - No tracking
  - HEADER - Cost in response header (default)
  - LOG - Detailed logging per operation
  - HTML - Visual report for debugging (admin only)

### Configuration with defaults
```
  REQUEST_COST_ACCOUNTING_ENABLED=true     # Enable/disable cost reporting
  REQUEST_COST_TIME_WINDOW_SECONDS=60      # Cost Logging interval
  RATE_LIMIT_ENABLED=false                 # Enable hard rate limiting
  RATE_LIMIT_REFILL_PER_SECOND=500         # Token refill rate
  RATE_LIMIT_MAX_BUCKET_SIZE=10000         # Max tokens
  RATE_LIMIT_ADD_HEADER_INFO=true          # Adds header which reports on number of tokens remaining
```
### Tests

- Added unit tests for RequestCostApi, RequestCostFilter,
RequestCostRequestListener, LeakyTokenBucketImpl and RequestCostReport
for validation.
- Integration tests verify HTML report behavior and accounting stability
across requests.

### To Do
- Create a new LeakyTokenBucketImpl that reads token and bucket
information from reddit for use across load balanced systems.
- Log expensive requests, e.g. log any request > 1000 tokens


ref: #32886
dsolistorres pushed a commit that referenced this pull request Feb 20, 2026
This PR fixes: #34300


## Cleanup: Remove container variantId usage and use page-level
variantId

### Problem

The codebase was attempting to extract `variantId` from container
objects via `container?.parentPermissionable?.variantId`, but this
property no longer exists after [PR
#32890](#32890). This resulted in
`variantId` always being `undefined` when used in container operations.

Additionally, containers don't actually have `variantId` - it's a
**page-level concept**, not a container-level one. The
`/api/v1/page/copyContent` endpoint expects the **page-level variantId**
in the `DotTreeNode` payload, not a container's variantId.

### Changes

1. **Removed `variantId` from `getContainersData` function**
(`libs/sdk/uve/src/lib/dom/dom.utils.ts`)
- Removed the TODO comment and code attempting to extract `variantId`
from `parentPermissionable.variantId`
   - This property no longer exists in the API response

2. **Removed `variantId` from `EditableContainerData` interface**
(`libs/sdk/types/src/lib/editor/public.ts`)
- Removed the optional `variantId?: string` property since containers
don't have variantId

3. **Updated `getCurrentTreeNode` to use page-level variantId**
(`libs/portlets/edit-ema/portlet/src/lib/store/features/editor/withEditor.ts`)
- Changed from using `container.variantId` to `store.$variantId()` which
provides the page-level variantId
- This ensures the correct variantId is sent to the
`/api/v1/page/copyContent` endpoint

4. **Made `ContainerPayload.variantId` optional**
(`libs/portlets/edit-ema/portlet/src/lib/shared/models.ts`)
- Changed from `variantId: string` to `variantId?: string` with
documentation explaining containers don't have variantId

5. **Updated tests**
(`libs/portlets/edit-ema/portlet/src/lib/store/features/editor/withEditor.spec.ts`)
   - Updated test expectations to verify page-level variantId is used
   - Added test cases to ensure container variantId is ignored

### Impact

- ✅ **Fixes incorrect API calls**: The `/api/v1/page/copyContent`
endpoint now receives the correct page-level variantId instead of
`undefined`
- ✅ **Removes dead code**: Eliminates code attempting to access a
non-existent property
- ✅ **Improves code clarity**: Makes it explicit that variantId is a
page-level concept, not container-level
- ✅ **No breaking changes**: `ContainerPayload.variantId` is now
optional, maintaining backward compatibility

### Testing

- Updated unit tests to verify `getCurrentTreeNode` uses page-level
variantId from `store.$variantId()`
- Added test cases to ensure container variantId is properly ignored
- All existing tests pass

### Related

- Related to issue: [Issue
#34300](#34300)
- Follows up on: [PR #32890](#32890)
which removed `parentPermissionable` from container API responses
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

per Request based accounting - calculate cost for each request

4 participants