11import { Container , CosmosClient , ItemDefinition , SqlQuerySpec } from '@azure/cosmos' ;
2- import { RecordData } from '@forestadmin/datasource-toolkit' ;
2+ import { Logger , RecordData } from '@forestadmin/datasource-toolkit' ;
33import { randomUUID } from 'crypto' ;
44
55import { OverrideTypeConverter } from '../introspection/builder' ;
@@ -14,6 +14,29 @@ import Serializer from '../utils/serializer';
1414
1515export { 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+
1740export 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