Skip to content

Commit

Permalink
Latency Metrics tracking (#13)
Browse files Browse the repository at this point in the history
  • Loading branch information
joseffffff committed Jun 9, 2024
1 parent d792e86 commit 1916650
Show file tree
Hide file tree
Showing 6 changed files with 209 additions and 66 deletions.
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -213,3 +213,18 @@ await orm.updateAll(entitiesToUpdate);
- **Remarks**:
- It internally retrieves sheet data to ensure proper alignment of data and checking which row needs to update.
- Quota retries are automatically handled to manage API rate limits.

### `metrics()`

Returns an object that contains request latencies, grouped by type of request.

Example of method response:

```typescript
{
FETCH_SHEET_DATA: [432, 551, 901],
SHEET_APPEND: [302, 104]
}
```

Check [MetricOperations](./src/metrics/MetricOperation.ts) class to see possible keys.
171 changes: 105 additions & 66 deletions src/GoogleSpreadsheetsOrm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { ParsedSpreadsheetCellValue } from './Query';
import { Serializer } from './serialization/Serializer';
import { GoogleSheetClientProvider } from './GoogleSheetClientProvider';
import { Logger } from './utils/Logger';
import { google, sheets_v4 } from 'googleapis';
import { sheets_v4 } from 'googleapis';
import { FieldType } from './serialization/FieldType';
import { JsonFieldSerializer } from './serialization/JsonFieldSerializer';
import { DateFieldSerializer } from './serialization/DateFieldSerializer';
Expand All @@ -12,25 +12,30 @@ import { GaxiosResponse } from 'gaxios';
import { GoogleSpreadsheetOrmError } from './errors/GoogleSpreadsheetOrmError';
import { Options } from './Options';
import { BaseModel } from './BaseModel';
import { Metrics, MilliSecondsByOperation } from './metrics/Metrics';
import { MetricOperation } from './metrics/MetricOperation';
import Schema$ValueRange = sheets_v4.Schema$ValueRange;

export class GoogleSpreadsheetsOrm<T extends BaseModel> {
private readonly logger: Logger;
private readonly sheetsClientProvider: GoogleSheetClientProvider;
private readonly serializers: Map<string, Serializer<unknown>> = new Map();
private readonly serializers: Map<string, Serializer<unknown>>;

private readonly instantiator: (rawRowObject: object) => T;
private readonly metricsCollector: Metrics;

constructor(private readonly options: Options<T>) {
this.logger = new Logger(options.verbose);
this.sheetsClientProvider = GoogleSheetClientProvider.fromOptions(options, this.logger);

this.serializers = new Map<string, Serializer<unknown>>();
this.serializers.set(FieldType.JSON, new JsonFieldSerializer());
this.serializers.set(FieldType.DATE, new DateFieldSerializer(this.logger));
this.serializers.set(FieldType.BOOLEAN, new BooleanSerializer());
this.serializers.set(FieldType.NUMBER, new NumberSerializer());

this.instantiator = options.instantiator ?? (r => r as T);
this.metricsCollector = new Metrics();
}

/**
Expand Down Expand Up @@ -136,15 +141,17 @@ export class GoogleSpreadsheetsOrm<T extends BaseModel> {
);

await this.sheetsClientProvider.handleQuotaRetries(sheetsClient =>
sheetsClient.spreadsheets.values.append({
spreadsheetId: this.options.spreadsheetId,
range: this.options.sheet,
insertDataOption: 'INSERT_ROWS',
valueInputOption: 'USER_ENTERED',
requestBody: {
values: entitiesDatabaseArrays,
},
}),
this.metricsCollector.trackExecutionTime(MetricOperation.SHEET_APPEND, () =>
sheetsClient.spreadsheets.values.append({
spreadsheetId: this.options.spreadsheetId,
range: this.options.sheet,
insertDataOption: 'INSERT_ROWS',
valueInputOption: 'USER_ENTERED',
requestBody: {
values: entitiesDatabaseArrays,
},
}),
),
);
}

Expand Down Expand Up @@ -188,21 +195,23 @@ export class GoogleSpreadsheetsOrm<T extends BaseModel> {
const sheetId = await this.fetchSheetDetails().then(sheetDetails => sheetDetails.properties?.sheetId);

await this.sheetsClientProvider.handleQuotaRetries(sheetsClient =>
sheetsClient.spreadsheets.batchUpdate({
spreadsheetId: this.options.spreadsheetId,
requestBody: {
requests: rowNumbers.map(rowNumber => ({
deleteDimension: {
range: {
sheetId,
dimension: 'ROWS',
startIndex: rowNumber - 1, // index, not a rowNumber here, so -1
endIndex: rowNumber, // exclusive, to delete just one row
this.metricsCollector.trackExecutionTime(MetricOperation.SHEET_DELETE, () =>
sheetsClient.spreadsheets.batchUpdate({
spreadsheetId: this.options.spreadsheetId,
requestBody: {
requests: rowNumbers.map(rowNumber => ({
deleteDimension: {
range: {
sheetId,
dimension: 'ROWS',
startIndex: rowNumber - 1, // index, not a rowNumber here, so -1
endIndex: rowNumber, // exclusive, to delete just one row
},
},
},
})),
},
}),
})),
},
}),
),
);
}

Expand All @@ -229,31 +238,56 @@ export class GoogleSpreadsheetsOrm<T extends BaseModel> {
const { headers, data } = await this.findSheetData();

await this.sheetsClientProvider.handleQuotaRetries(sheetsClient =>
sheetsClient.spreadsheets.values.batchUpdate({
spreadsheetId: this.options.spreadsheetId,
requestBody: {
valueInputOption: 'USER_ENTERED',
includeValuesInResponse: false,
data: entities.map(entity => {
const rowNumber = this.rowNumber(data, entity.id);
const range = this.buildRangeToUpdate(headers, rowNumber);
const entityAsSheetArray = this.toSheetArrayFromHeaders(entity, headers);

return {
range,
values: [entityAsSheetArray],
};
}),
},
}),
this.metricsCollector.trackExecutionTime(MetricOperation.SHEET_UPDATE, () =>
sheetsClient.spreadsheets.values.batchUpdate({
spreadsheetId: this.options.spreadsheetId,
requestBody: {
valueInputOption: 'USER_ENTERED',
includeValuesInResponse: false,
data: entities.map(entity => {
const rowNumber = this.rowNumber(data, entity.id);
const range = this.buildRangeToUpdate(headers, rowNumber);
const entityAsSheetArray = this.toSheetArrayFromHeaders(entity, headers);

return {
range,
values: [entityAsSheetArray],
};
}),
},
}),
),
);
}

/**
* Returns an object that contains request latencies, grouped by type of request.
*
* @returns An object that contains request latencies of the different requests performed to sheets API,
* grouped by type of request.
*
* @see {@link MetricOperation}
*
* @example
* ```ts
* {
* FETCH_SHEET_DATA: [432, 551, 901],
* SHEET_APPEND: [302, 104]
* }
* ```
*/
public metrics(): MilliSecondsByOperation {
return this.metricsCollector.toObject();
}

private async fetchSheetDetails(): Promise<sheets_v4.Schema$Sheet> {
const sheets = await this.sheetsClientProvider.handleQuotaRetries(sheetsClient =>
sheetsClient.spreadsheets.get({
spreadsheetId: this.options.spreadsheetId,
}),
const sheets: GaxiosResponse<sheets_v4.Schema$Spreadsheet> = await this.sheetsClientProvider.handleQuotaRetries(
sheetsClient =>
this.metricsCollector.trackExecutionTime(MetricOperation.FETCH_SHEET_DETAILS, () =>
sheetsClient.spreadsheets.get({
spreadsheetId: this.options.spreadsheetId,
}),
),
);

const sheetDetails: sheets_v4.Schema$Sheet | undefined = sheets.data.sheets?.find(
Expand Down Expand Up @@ -331,32 +365,37 @@ export class GoogleSpreadsheetsOrm<T extends BaseModel> {
}

private async allSheetData(): Promise<string[][]> {
return this.sheetsClientProvider.handleQuotaRetries(async sheetsClient => {
this.logger.log(`Querying all sheet data table=${this.options.sheet}`);
const db: GaxiosResponse<Schema$ValueRange> = await sheetsClient.spreadsheets.values.get({
spreadsheetId: this.options.spreadsheetId,
range: this.options.sheet,
});
return db.data.values as string[][];
});
return this.sheetsClientProvider.handleQuotaRetries(async sheetsClient =>
this.metricsCollector.trackExecutionTime(MetricOperation.FETCH_SHEET_DATA, async () => {
this.logger.log(`Querying all sheet data table=${this.options.sheet}`);
const db: GaxiosResponse<Schema$ValueRange> = await sheetsClient.spreadsheets.values.get({
spreadsheetId: this.options.spreadsheetId,
range: this.options.sheet,
});
return db.data.values as string[][];
}),
);
}

private async sheetHeaders(): Promise<string[]> {
return this.sheetsClientProvider.handleQuotaRetries(async sheetsClient => {
this.logger.log(`Reading headers from table=${this.options.sheet}`);
const db: GaxiosResponse<Schema$ValueRange> = await sheetsClient.spreadsheets.values.get({
spreadsheetId: this.options.spreadsheetId,
range: `${this.options.sheet}!A1:1`, // users!A1:1
});
return this.sheetsClientProvider.handleQuotaRetries(async sheetsClient =>
this.metricsCollector.trackExecutionTime(MetricOperation.FETCH_SHEET_HEADERS, async () => {
this.logger.log(`Reading headers from table=${this.options.sheet}`);

const values = db.data.values;
const db: GaxiosResponse<Schema$ValueRange> = await sheetsClient.spreadsheets.values.get({
spreadsheetId: this.options.spreadsheetId,
range: `${this.options.sheet}!A1:1`, // users!A1:1
});

if (values && values.length > 0) {
return values[0] as string[];
}
const values = db.data.values;

throw new GoogleSpreadsheetOrmError(`Headers row not present in sheet ${this.options.sheet}`);
});
if (values && values.length > 0) {
return values[0] as string[];
}

throw new GoogleSpreadsheetOrmError(`Headers row not present in sheet ${this.options.sheet}`);
}),
);
}

private buildRangeToUpdate(headers: string[], rowNumber: number): string {
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export * from './GoogleSpreadsheetsOrm';
export * from './Options';
export * from './Castings';
export { BaseModel } from './BaseModel';
export { MetricOperation } from './metrics/MetricOperation';
20 changes: 20 additions & 0 deletions src/metrics/MetricOperation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
export enum MetricOperation {
FETCH_SHEET_DATA = 'FETCH_SHEET_DATA',
FETCH_SHEET_DETAILS = 'FETCH_SHEET_DETAILS',
/**
* Includes delete & deleteAll operations
*/
FETCH_SHEET_HEADERS = 'FETCH_SHEET_HEADERS',
/**
* Includes create & createAll operations.
*/
SHEET_APPEND = 'SHEET_APPEND',
/**
* Includes delete & deleteAll operations.
*/
SHEET_DELETE = 'SHEET_DELETE',
/**
* Includes update & updateAll operations.
*/
SHEET_UPDATE = 'SHEET_UPDATE',
}
29 changes: 29 additions & 0 deletions src/metrics/Metrics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { MetricOperation } from './MetricOperation';

export type MilliSecondsByOperation = {
[x: string]: number[];
};

export class Metrics {
constructor(private readonly msByOperation: Map<MetricOperation, number[]> = new Map<MetricOperation, number[]>()) {}

public async trackExecutionTime<T>(op: MetricOperation, func: () => Promise<T>): Promise<T> {
const startTime = Date.now();
const result = await func();
const executionTime = Date.now() - startTime;
this.report(op, executionTime);
return result;
}

private report(op: MetricOperation, millis: number): void {
if (this.msByOperation.has(op)) {
this.msByOperation.get(op)!.push(millis);
} else {
this.msByOperation.set(op, [millis]);
}
}

public toObject(): MilliSecondsByOperation {
return Object.fromEntries(this.msByOperation.entries());
}
}
Loading

0 comments on commit 1916650

Please sign in to comment.