Skip to content

Commit c7af33e

Browse files
authored
feat(cosmos): add RU consumption logs (#204)
1 parent 3c95548 commit c7af33e

4 files changed

Lines changed: 761 additions & 17 deletions

File tree

packages/datasource-cosmos/src/index.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { DataSourceFactory, Logger } from '@forestadmin/datasource-toolkit';
44
import CosmosDataSource from './datasource';
55
import { ConfigurationOptions, CosmosDatasourceBuilder } from './introspection/builder';
66
import Introspector, { VirtualArrayCollectionConfig } from './introspection/introspector';
7+
import { configureRuLogging } from './model-builder/model';
78
import { ManualSchemaConfig } from './types/manual-schema';
89
import { convertManualSchemaToModels } from './utils/manual-schema-converter';
910
import VirtualCollectionManager from './virtual-collection-manager';
@@ -59,6 +60,7 @@ export { default as TypeConverter } from './utils/type-converter';
5960
export { default as ModelCosmos } from './model-builder/model';
6061
export { configurePaginationCache, getSharedPaginationCache } from './model-builder/model';
6162
export { configureRetryOptions, getSharedRetryOptions } from './model-builder/model';
63+
export { configureRuLogging, isRuLoggingEnabled } from './model-builder/model';
6264
export { default as PaginationCache } from './utils/pagination-cache';
6365
export {
6466
withRetry,
@@ -169,6 +171,12 @@ export function createCosmosDataSource(
169171
* Allows you to define collections and their fields explicitly
170172
*/
171173
schema?: ManualSchemaConfig;
174+
/**
175+
* Enable logging of Request Unit (RU) consumption for each Cosmos DB operation
176+
* Useful for monitoring and optimizing database costs
177+
* Default: false
178+
*/
179+
logRuConsumption?: boolean;
172180
},
173181
): DataSourceFactory {
174182
return async (logger: Logger) => {
@@ -186,8 +194,14 @@ export function createCosmosDataSource(
186194
introspectionConfig,
187195
disableIntrospection,
188196
schema,
197+
logRuConsumption,
189198
} = options || {};
190199

200+
// Configure RU logging if enabled
201+
if (logRuConsumption) {
202+
configureRuLogging(true, logger);
203+
}
204+
191205
// Apply introspection config defaults
192206
const finalIntrospectionConfig: IntrospectionConfig = {
193207
sampleSize: introspectionConfig?.sampleSize ?? 100,
@@ -281,6 +295,12 @@ export function createCosmosDataSourceForEmulator(
281295
introspectionConfig?: IntrospectionConfig;
282296
disableIntrospection?: boolean;
283297
schema?: ManualSchemaConfig;
298+
/**
299+
* Enable logging of Request Unit (RU) consumption for each Cosmos DB operation
300+
* Useful for monitoring and optimizing database costs
301+
* Default: false
302+
*/
303+
logRuConsumption?: boolean;
284304
},
285305
): DataSourceFactory {
286306
// Default emulator connection details

packages/datasource-cosmos/src/model-builder/model.ts

Lines changed: 64 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Container, CosmosClient, ItemDefinition, SqlQuerySpec } from '@azure/cosmos';
2-
import { RecordData } from '@forestadmin/datasource-toolkit';
2+
import { Logger, RecordData } from '@forestadmin/datasource-toolkit';
33
import { randomUUID } from 'crypto';
44

55
import { OverrideTypeConverter } from '../introspection/builder';
@@ -14,6 +14,29 @@ import Serializer from '../utils/serializer';
1414

1515
export { RetryOptions, configureRetryOptions, getSharedRetryOptions };
1616

17+
/**
18+
* Shared RU logging configuration
19+
*/
20+
let sharedRuLoggingEnabled = false;
21+
let sharedRuLogger: Logger | null = null;
22+
23+
/**
24+
* Configure RU logging for all Cosmos DB operations
25+
* @param enabled Whether to enable RU consumption logging
26+
* @param logger The Forest Admin logger to use for RU logging
27+
*/
28+
export function configureRuLogging(enabled: boolean, logger?: Logger): void {
29+
sharedRuLoggingEnabled = enabled;
30+
sharedRuLogger = logger ?? null;
31+
}
32+
33+
/**
34+
* Check if RU logging is enabled
35+
*/
36+
export function isRuLoggingEnabled(): boolean {
37+
return sharedRuLoggingEnabled;
38+
}
39+
1740
export interface CosmosSchema {
1841
[key: string]: {
1942
type: string;
@@ -154,12 +177,13 @@ export default class ModelCosmos {
154177
};
155178

156179
// eslint-disable-next-line no-await-in-loop -- Sequential execution maintains order
157-
const { resource } = await withRetry(
180+
const response = await withRetry(
158181
() => this.container.items.create(itemToCreate),
159182
this.retryOptions,
160183
);
184+
this.logRuConsumption('create', response.requestCharge);
161185
// Flatten and serialize the response
162-
createdRecords.push(Serializer.serialize(resource));
186+
createdRecords.push(Serializer.serialize(response.resource));
163187
}
164188

165189
return createdRecords;
@@ -174,21 +198,23 @@ export default class ModelCosmos {
174198
for (const { id, partitionKey } of items) {
175199
// Point read: directly fetch item using id + partition key (1 RU)
176200
// eslint-disable-next-line no-await-in-loop -- Sequential maintains consistency
177-
const { resource: existingItem } = await withRetry(
201+
const readResponse = await withRetry(
178202
() => this.container.item(id, partitionKey).read(),
179203
this.retryOptions,
180204
);
205+
this.logRuConsumption('update.read', readResponse.requestCharge);
181206

182-
if (existingItem) {
207+
if (readResponse.resource) {
183208
// Deep merge the unflattened patch with the existing item
184-
const updatedItem = Serializer.deepMerge(existingItem, unflattenedPatch);
209+
const updatedItem = Serializer.deepMerge(readResponse.resource, unflattenedPatch);
185210

186211
// Point update: directly update using id + partition key
187212
// eslint-disable-next-line no-await-in-loop -- Sequential maintains consistency
188-
await withRetry(
213+
const replaceResponse = await withRetry(
189214
() => this.container.item(id, partitionKey).replace(updatedItem),
190215
this.retryOptions,
191216
);
217+
this.logRuConsumption('update.replace', replaceResponse.requestCharge);
192218
}
193219
}
194220
}
@@ -198,7 +224,11 @@ export default class ModelCosmos {
198224
for (const { id, partitionKey } of items) {
199225
// Point delete: directly delete using id + partition key
200226
// eslint-disable-next-line no-await-in-loop -- Sequential maintains consistency
201-
await withRetry(() => this.container.item(id, partitionKey).delete(), this.retryOptions);
227+
const response = await withRetry(
228+
() => this.container.item(id, partitionKey).delete(),
229+
this.retryOptions,
230+
);
231+
this.logRuConsumption('delete', response.requestCharge);
202232
}
203233
}
204234

@@ -222,12 +252,13 @@ export default class ModelCosmos {
222252

223253
// If no pagination parameters, fetch all (backward compatibility)
224254
if (offset === undefined && limit === undefined) {
225-
const { resources } = await withRetry(
255+
const response = await withRetry(
226256
() => this.container.items.query(querySpec, queryOptions).fetchAll(),
227257
this.retryOptions,
228258
);
259+
this.logRuConsumption('query.fetchAll', response.requestCharge);
229260

230-
return resources.map(item => Serializer.serialize(item));
261+
return response.resources.map(item => Serializer.serialize(item));
231262
}
232263

233264
const effectiveOffset = offset || 0;
@@ -281,7 +312,8 @@ export default class ModelCosmos {
281312
// Iterate through pages until we have enough items
282313
// eslint-disable-next-line no-restricted-syntax
283314
for await (const response of queryIterator.getAsyncIterator()) {
284-
const { resources: page, continuationToken } = response;
315+
const { resources: page, continuationToken, requestCharge } = response;
316+
this.logRuConsumption('query.page', requestCharge);
285317

286318
// Store continuation token for future use at regular intervals
287319
const cacheInterval = this.paginationCache.getCacheInterval();
@@ -336,12 +368,13 @@ export default class ModelCosmos {
336368
queryOptions.partitionKey = partitionKey;
337369
}
338370

339-
const { resources } = await withRetry(
371+
const response = await withRetry(
340372
() => this.container.items.query(querySpec, queryOptions).fetchAll(),
341373
this.retryOptions,
342374
);
375+
this.logRuConsumption('aggregate', response.requestCharge);
343376

344-
return resources.map(item => Serializer.serialize(item));
377+
return response.resources.map(item => Serializer.serialize(item));
345378
}
346379

347380
public async count(querySpec?: SqlQuerySpec, partitionKey?: string | number): Promise<number> {
@@ -357,22 +390,24 @@ export default class ModelCosmos {
357390
const countQuery: SqlQuerySpec = {
358391
query: 'SELECT VALUE COUNT(1) FROM c',
359392
};
360-
const { resources } = await withRetry(
393+
const response = await withRetry(
361394
() => this.container.items.query<number>(countQuery, queryOptions).fetchAll(),
362395
this.retryOptions,
363396
);
397+
this.logRuConsumption('count', response.requestCharge);
364398

365-
return resources[0] || 0;
399+
return response.resources[0] || 0;
366400
}
367401

368402
// Convert the query to a COUNT query to avoid fetching all records
369403
const countQuery = this.convertToCountQuery(querySpec);
370-
const { resources } = await withRetry(
404+
const response = await withRetry(
371405
() => this.container.items.query<number>(countQuery, queryOptions).fetchAll(),
372406
this.retryOptions,
373407
);
408+
this.logRuConsumption('count', response.requestCharge);
374409

375-
return resources[0] || 0;
410+
return response.resources[0] || 0;
376411
}
377412

378413
/**
@@ -440,4 +475,16 @@ export default class ModelCosmos {
440475
public getContainerName(): string {
441476
return this.containerName;
442477
}
478+
479+
/**
480+
* Log RU consumption if enabled
481+
*/
482+
private logRuConsumption(operation: string, requestCharge: number | undefined): void {
483+
if (!sharedRuLoggingEnabled || requestCharge === undefined) {
484+
return;
485+
}
486+
487+
const message = `[Cosmos RU] ${this.name}.${operation}: ${requestCharge} RUs`;
488+
sharedRuLogger?.('Info', message);
489+
}
443490
}

0 commit comments

Comments
 (0)