Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Integration with Datadog

[Datadog][datadog] is a popular fully managed observability service. This guide
demonstrates how to set up Cube Cloud to export logs to Datadog.
demonstrates how to set up Cube Cloud to export logs and metrics to Datadog.

## Configuration

Expand Down Expand Up @@ -45,9 +45,36 @@ navigate to <Btn>Logs</Btn> in Datadog and watch the logs coming:

<Screenshot src="https://ucarecdn.com/5e66f3d6-85a7-498c-89e8-41632d1b184c/" />

### Exporting metrics

To export metrics to Datadog, use the same API key from
<Btn>Organization Settings → API Keys</Btn> as configured for logs.

Then, configure the [`datadog_metrics`][vector-datadog-metrics] sink in your
[`vector.toml` configuration file][ref-monitoring-integrations-conf].

Example configuration:

```toml
[sinks.datadog_metrics]
type = "datadog_metrics"
inputs = [
"metrics"
]
default_api_key = "$CUBE_CLOUD_MONITORING_DATADOG_API_KEY"
site = "datadoghq.eu"
```

Again, upon commit the configuration for Vector should take effect in a minute. Then,
navigate to <Btn>Metrics → Summary</Btn> in Datadog and explore the available
metrics. Cube metrics are prefixed with `cube_`, such as `cube_cpu_usage_ratio`,
`cube_memory_usage_ratio`, and `cube_requests_total`.

[datadog]: https://www.datadoghq.com
[datadog-docs-sites]: https://docs.datadoghq.com/getting_started/site/
[vector-datadog-logs]:
https://vector.dev/docs/reference/configuration/sinks/datadog_logs/
[vector-datadog-metrics]:
https://vector.dev/docs/reference/configuration/sinks/datadog_metrics/
[ref-monitoring-integrations]: /product/workspace/monitoring
[ref-monitoring-integrations-conf]: /product/workspace/monitoring#configuration
1 change: 1 addition & 0 deletions docs/pages/product/caching/refreshing-pre-aggregations.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ behavior:
- `CUBEJS_REFRESH_WORKER_CONCURRENCY` (see also `CUBEJS_CONCURRENCY`)
- `CUBEJS_SCHEDULED_REFRESH_QUERIES_PER_APP_ID`
- `CUBEJS_DROP_PRE_AGG_WITHOUT_TOUCH`
- `CUBEJS_PRE_AGGREGATIONS_BACKOFF_MAX_TIME`

## Troubleshooting

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1155,6 +1155,17 @@ This can be overridden for individual pre-aggregations using the
| --------------- | ---------------------- | --------------------- |
| `true`, `false` | `true` | `true` |

## `CUBEJS_PRE_AGGREGATIONS_BACKOFF_MAX_TIME`

The maximum time, in seconds, for exponential backoff for retries when pre-aggregation
builds fail. When a pre-aggregation refresh fails, retries will be executed with
exponentially increasing delays, but the delay will not exceed the value specified by
this variable.

| Possible Values | Default in Development | Default in Production |
| ------------------------- | ---------------------- | --------------------- |
| A valid number in seconds | `600` | `600` |

## `CUBEJS_REFRESH_WORKER`

If `true`, this instance of Cube will **only** refresh pre-aggregations.
Expand Down
2 changes: 2 additions & 0 deletions docs/pages/product/presentation/embedding.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ Cube supports iframe embedding for [dashboards](/product/presentation/dashboards

- **[Signed Embedding](/product/presentation/embedding/signed-embedding)** – Authentication is handled programmatically using JWT tokens generated by your application. Users authenticate through your application and do not require Cube accounts.

- **[Creator Mode](/product/presentation/embedding/creator-mode)** – Embed the entire Cube application with workbooks-dashboard functionality, allowing users to create and modify dashboards directly within the embedded application.

Additionally, you can use the [Cube Core REST API](/product/apis-integrations/rest-api) directly for headless embedded analytics.

## Types of Embedding
Expand Down
3 changes: 2 additions & 1 deletion docs/pages/product/presentation/embedding/_meta.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
module.exports = {
"private-embedding": "Private embedding",
"signed-embedding": "Signed embedding"
"signed-embedding": "Signed embedding",
"creator-mode": "Creator mode"
}
95 changes: 95 additions & 0 deletions docs/pages/product/presentation/embedding/creator-mode.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# Creator mode

<InfoBox>
Creator mode is currently in **private preview**. To enable this feature for your account, please contact [Cube support](https://cube.dev/contact).
</InfoBox>

Creator mode enables you to embed the entire Cube application with workbooks-dashboard functionality. Users will be able to create and modify their dashboards directly within the embedded application.

## How it works

In creator mode, you embed the full Cube application instead of individual dashboards or chat interfaces. This provides users with the complete Cube experience, including the ability to:

- Create new dashboards
- Modify existing dashboards
- Access all workbook and dashboard functionality
- Build custom analytics experiences

To enable creator mode, you need to pass `creatorMode: true`, `tenantId`, and `tenantName` to the [Generate Session API][ref-generate-session] when generating an embed session. The tenant ID is used to scope all content that the user creates to a specific tenant, while the tenant name is a human-readable friendly name displayed in the user interface.

## Tenant ID and Name

When using creator mode, you must provide both `tenantId` and `tenantName` parameters when generating a session:

- **`tenantId`** – Used to scope all content (dashboards, workbooks, etc.) that users create within the embedded application to a specific tenant. This ensures proper data isolation in multi-tenant scenarios.
- **`tenantName`** – A human-readable friendly name for the tenant that will be displayed in the user interface.

## Using creator mode

To use creator mode and embed an app:

1. **Set the embed type to "App"** in the form
2. **Fill in Deployment ID**, **Tenant ID**, **Tenant Name**, and either **External ID** or **Internal ID** (email)
3. **Click "Generate Session & Embed"** — the request automatically includes `creatorMode: true` for app embeddings
4. The app is embedded at `/embed/d/{deploymentId}/app?session={sessionId}` and displayed in the iframe

Creator mode is enabled automatically when the embed type is "app"; no additional configuration is needed. The tenant ID is required to scope all dashboards and content created by the user to the specific tenant, and the tenant name will be displayed in the user interface.

### Example

```javascript
const API_KEY = "YOUR_API_KEY";
const DEPLOYMENT_ID = 32;
const TENANT_ID = "tenant-123";
const TENANT_NAME = "Acme Corporation";

const session = await fetch(
"https://your-account.cubecloud.dev/api/v1/embed/generate-session",
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Api-Key ${API_KEY}`,
},
body: JSON.stringify({
deploymentId: DEPLOYMENT_ID,
externalId: "user@example.com",
creatorMode: true,
tenantId: TENANT_ID,
tenantName: TENANT_NAME,
}),
},
);

const data = await session.json();
const sessionId = data.sessionId;
```

### Embedding the app

Use the session ID to embed the full Cube application:

```html
<iframe
title="Cube App"
src="https://your-tenant.cubecloud.dev/embed/d/{deploymentId}/app?session={sessionId}"
width="100%"
height="800"
></iframe>
```

Replace `{deploymentId}` with your deployment ID and `{sessionId}` with the session ID returned from the Generate Session API.

## Example application

For a complete working example of embedding, including creator mode, check out the [cube-embedding-demo](https://github.com/cubedevinc/cube-embedding-demo) repository. This demo application provides:

- A full working example of iframe embedding
- Implementation of signed iframe embedding with session generation
- Support for creator mode with tenant ID and tenant name
- A React-based UI for testing embedding functionality
- Backend server that securely handles API key authentication

You can clone the repository, configure it with your Cube credentials, and run it locally to test embedding functionality or use it as a reference implementation for your own application.

[ref-generate-session]: /product/apis-integrations/embed-apis/generate-session
10 changes: 10 additions & 0 deletions docs/pages/product/presentation/embedding/signed-embedding.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,16 @@ User attributes enable row-level security and personalized chat responses by fil
- Regional managers see data filtered by their city
- Department heads see only their department's metrics

## Example application

For a complete working example of signed embedding, check out the [cube-embedding-demo](https://github.com/cubedevinc/cube-embedding-demo) repository. This demo application provides:

- A full working example of iframe embedding
- Implementation of signed iframe embedding with session generation
- A React-based UI for testing embedding functionality
- Backend server that securely handles API key authentication

You can clone the repository, configure it with your Cube credentials, and run it locally to test embedding functionality or use it as a reference implementation for your own application.

[ref-api-keys]: /product/administration/workspace/api-keys
[ref-generate-session]: /product/apis-integrations/embed-apis/generate-session
7 changes: 7 additions & 0 deletions packages/cubejs-backend-shared/src/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -751,6 +751,13 @@ const variables: Record<string, (...args: any) => any> = {
.default(60 * 60 * 24)
.asIntPositive(),

/**
* Maximum time for exponential backoff for pre-aggs (in seconds)
*/
preAggBackoffMaxTime: (): number => get('CUBEJS_PRE_AGGREGATIONS_BACKOFF_MAX_TIME')
.default(10 * 60)
.asIntPositive(),

/**
* Expire time for touch records
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,8 @@ export class PreAggregations {

private readonly touchTablePersistTime: number;

private readonly preAggBackoffMaxTime: number;

public readonly dropPreAggregationsWithoutTouch: boolean;

private readonly usedTablePersistTime: number;
Expand All @@ -277,6 +279,7 @@ export class PreAggregations {
this.externalDriverFactory = options.externalDriverFactory;
this.structureVersionPersistTime = options.structureVersionPersistTime || 60 * 60 * 24 * 30;
this.touchTablePersistTime = options.touchTablePersistTime || getEnv('touchPreAggregationTimeout');
this.preAggBackoffMaxTime = options.preAggBackoffMaxTime || getEnv('preAggBackoffMaxTime');
this.dropPreAggregationsWithoutTouch = options.dropPreAggregationsWithoutTouch || getEnv('dropPreAggregationsWithoutTouch');
this.usedTablePersistTime = options.usedTablePersistTime || getEnv('dbQueryTimeout');
this.externalRefresh = options.externalRefresh;
Expand Down Expand Up @@ -317,6 +320,11 @@ export class PreAggregations {
return this.queryCache.getKey('SQL_PRE_AGGREGATIONS_TABLES_TOUCH', tableName);
}

protected preAggBackoffRedisKey(tableName: string): string {
// TODO add dataSource?
return this.queryCache.getKey('SQL_PRE_AGGREGATIONS_BACKOFF', tableName);
}

protected refreshEndReachedKey() {
// TODO add dataSource?
return this.queryCache.getKey('SQL_PRE_AGGREGATIONS_REFRESH_END_REACHED', '');
Expand Down Expand Up @@ -372,6 +380,36 @@ export class PreAggregations {
.map(k => k.replace(this.tablesTouchRedisKey(''), ''));
}

public async updatePreAggBackoff(tableName: string, backoffData: { backoffMultiplier: number, nextTimestamp: Date }): Promise<void> {
await this.queryCache.getCacheDriver().set(
this.preAggBackoffRedisKey(tableName),
JSON.stringify(backoffData),
this.preAggBackoffMaxTime
);
}

public async removePreAggBackoff(tableName: string): Promise<void> {
await this.queryCache.getCacheDriver().remove(this.preAggBackoffRedisKey(tableName));
}

public getPreAggBackoffMaxTime(): number {
return this.preAggBackoffMaxTime;
}

public async getPreAggBackoff(tableName: string): Promise<{ backoffMultiplier: number, nextTimestamp: Date } | null> {
const res = await this.queryCache.getCacheDriver().get(this.preAggBackoffRedisKey(tableName));

if (!res) {
return null;
}

const parsed = JSON.parse(res);
return {
backoffMultiplier: parsed.backoffMultiplier,
nextTimestamp: new Date(parsed.nextTimestamp),
};
}

public async updateRefreshEndReached() {
return this.queryCache.getCacheDriver().set(this.refreshEndReachedKey(), new Date().getTime(), this.touchTablePersistTime);
}
Expand Down
44 changes: 43 additions & 1 deletion packages/cubejs-server-core/src/core/RefreshScheduler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -615,7 +615,49 @@ export class RefreshScheduler {
const currentQuery = await queryIterator.current();
if (currentQuery && queryIterator.partitionCounter() % concurrency === workerIndex) {
const orchestratorApi = await this.serverCore.getOrchestratorApi(context);
await orchestratorApi.executeQuery({ ...currentQuery, preAggregationsLoadCacheByDataSource });
const preAggsInstance = orchestratorApi.getQueryOrchestrator().getPreAggregations();
const now = new Date();

const backoffChecks = await Promise.all(
currentQuery.preAggregations.map(p => preAggsInstance.getPreAggBackoff(p.tableName))
);

// Skip execution if any pre-aggregation is still in backoff window
const shouldSkip = backoffChecks.some(backoffData => backoffData && now < backoffData.nextTimestamp);

if (!shouldSkip) {
try {
await orchestratorApi.executeQuery({ ...currentQuery, preAggregationsLoadCacheByDataSource });
} catch (e: any) {
// Check if this is a "Continue wait" error - these are normal queue signals
// For Continue wait errors, re-throw to handle them in the normal flow
if (e.error === 'Continue wait') {
throw e;
}

// Real datasource error - apply exponential backoff
for (const p of currentQuery.preAggregations) {
let backoffData = await preAggsInstance.getPreAggBackoff(p.tableName);

if (backoffData && backoffData.backoffMultiplier > 0) {
const newMultiplier = backoffData.backoffMultiplier * 2;
const delaySeconds = Math.min(newMultiplier, preAggsInstance.getPreAggBackoffMaxTime());

backoffData = {
backoffMultiplier: newMultiplier,
nextTimestamp: new Date(now.valueOf() + delaySeconds * 1000),
};
} else {
backoffData = {
backoffMultiplier: 1,
nextTimestamp: new Date(now.valueOf() + 1000),
};
}

await preAggsInstance.updatePreAggBackoff(p.tableName, backoffData);
}
}
}
}
const hasNext = await queryIterator.advance();
if (!hasNext) {
Expand Down
Loading