From f789378cabb037c7a7f3563e6165f983a741fd0c Mon Sep 17 00:00:00 2001 From: Jaanus Sellin Date: Thu, 3 Jul 2025 15:32:56 +0300 Subject: [PATCH] feat: add query timer to event store (#10306) I noticed event search, as it is doing `ILIKE` search, is slow sometimes. Lets get some statistics about it. Meanwhile added timers for other interesting queries. --- src/lib/features/events/event-store.ts | 42 ++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/src/lib/features/events/event-store.ts b/src/lib/features/events/event-store.ts index 0d8a8f0ca6..43d86b43b4 100644 --- a/src/lib/features/events/event-store.ts +++ b/src/lib/features/events/event-store.ts @@ -28,6 +28,8 @@ import type { ProjectActivitySchema } from '../../openapi/index.js'; import type { IQueryParam } from '../feature-toggle/types/feature-toggle-strategies-store-type.js'; import { applyGenericQueryParams } from '../feature-search/search-utils.js'; import type { ITag } from '../../tags/index.js'; +import metricsHelper from '../../util/metrics-helper.js'; +import { DB_TIME } from '../../metric-events.js'; const EVENT_COLUMNS = [ 'id', @@ -113,26 +115,38 @@ export class EventStore implements IEventStore { private logger: Logger; + private metricTimer: Function; + // a new DB has to be injected per transaction constructor(db: Db, getLogger: LogProvider) { this.db = db; this.logger = getLogger('event-store'); + this.metricTimer = (action) => + metricsHelper.wrapTimer(this.eventEmitter, DB_TIME, { + store: 'event', + action, + }); } async store(event: IBaseEvent): Promise { + const stopTimer = this.metricTimer('store'); try { await this.db(TABLE) .insert(this.eventToDbRow(event)) .returning(EVENT_COLUMNS); } catch (error: unknown) { this.logger.warn(`Failed to store "${event.type}" event: ${error}`); + } finally { + stopTimer(); } } async count(): Promise { + const stopTimer = this.metricTimer('count'); const count = await this.db(TABLE) .count>() .first(); + stopTimer(); if (!count) { return 0; } @@ -147,8 +161,10 @@ export class EventStore implements IEventStore { queryParams: IQueryParam[], query?: IEventSearchParams['query'], ): Promise { + const stopTimer = this.metricTimer('searchEventsCount'); const searchQuery = this.buildSearchQuery(queryParams, query); const count = await searchQuery.count().first(); + stopTimer(); if (!count) { return 0; } @@ -160,6 +176,7 @@ export class EventStore implements IEventStore { } async batchStore(events: IBaseEvent[]): Promise { + const stopTimer = this.metricTimer('batchStore'); try { await this.db(TABLE).insert( events.map((event) => this.eventToDbRow(event)), @@ -169,10 +186,13 @@ export class EventStore implements IEventStore { `Failed to store events: ${JSON.stringify(events)}`, error, ); + } finally { + stopTimer(); } } async getMaxRevisionId(largerThan: number = 0): Promise { + const stopTimer = this.metricTimer('getMaxRevisionId'); const row = await this.db(TABLE) .max('id') .where((builder) => @@ -193,10 +213,12 @@ export class EventStore implements IEventStore { ) .andWhere('id', '>=', largerThan) .first(); + stopTimer(); return row?.max ?? 0; } async getRevisionRange(start: number, end: number): Promise { + const stopTimer = this.metricTimer('getRevisionRange'); const query = this.db .select(EVENT_COLUMNS) .from(TABLE) @@ -246,6 +268,7 @@ export class EventStore implements IEventStore { } async query(operations: IQueryOperations[]): Promise { + const stopTimer = this.metricTimer('query'); try { let query: Knex.QueryBuilder = this.select(); @@ -271,10 +294,13 @@ export class EventStore implements IEventStore { return rows.map(this.rowToEvent); } catch (e) { return []; + } finally { + stopTimer(); } } async queryCount(operations: IQueryOperations[]): Promise { + const stopTimer = this.metricTimer('queryCount'); try { let query: Knex.QueryBuilder = this.db.count().from(TABLE); @@ -300,6 +326,8 @@ export class EventStore implements IEventStore { return Number.parseInt(queryResult.count || 0); } catch (e) { return 0; + } finally { + stopTimer(); } } @@ -355,6 +383,7 @@ export class EventStore implements IEventStore { } async getEvents(query?: Object): Promise { + const stopTimer = this.metricTimer('getEvents'); try { let qB = this.db .select(EVENT_COLUMNS) @@ -371,6 +400,8 @@ export class EventStore implements IEventStore { return rows.map(this.rowToEvent); } catch (err) { return []; + } finally { + stopTimer(); } } @@ -379,6 +410,7 @@ export class EventStore implements IEventStore { queryParams: IQueryParam[], options?: { withIp?: boolean }, ): Promise { + const stopTimer = this.metricTimer('searchEvents'); const query = this.buildSearchQuery(queryParams, params.query) .select(options?.withIp ? [...EVENT_COLUMNS, 'ip'] : EVENT_COLUMNS) .orderBy([ @@ -396,6 +428,8 @@ export class EventStore implements IEventStore { ); } catch (err) { return []; + } finally { + stopTimer(); } } @@ -420,6 +454,7 @@ export class EventStore implements IEventStore { } async getEventCreators(): Promise> { + const stopTimer = this.metricTimer('getEventCreators'); const query = this.db('events') .distinctOn('events.created_by_user_id') .leftJoin('users', 'users.id', '=', 'events.created_by_user_id') @@ -437,6 +472,7 @@ export class EventStore implements IEventStore { ]); const result = await query; + stopTimer(); return result .filter((row: any) => row.name || row.username || row.email) .map((row: any) => ({ @@ -448,6 +484,7 @@ export class EventStore implements IEventStore { async getProjectRecentEventActivity( project: string, ): Promise { + const stopTimer = this.metricTimer('getProjectRecentEventActivity'); const result = await this.db('events') .select( this.db.raw("TO_CHAR(created_at::date, 'YYYY-MM-DD') AS date"), @@ -462,6 +499,7 @@ export class EventStore implements IEventStore { .groupBy(this.db.raw("TO_CHAR(created_at::date, 'YYYY-MM-DD')")) .orderBy('date', 'asc'); + stopTimer(); return result.map((row) => ({ date: row.date, count: Number(row.count), @@ -531,10 +569,12 @@ export class EventStore implements IEventStore { } async setUnannouncedToAnnounced(): Promise { + const stopTimer = this.metricTimer('setUnannouncedToAnnounced'); const rows = await this.db(TABLE) .update({ announced: true }) .where('announced', false) .returning(EVENT_COLUMNS); + stopTimer(); return rows.map(this.rowToEvent); } @@ -545,6 +585,7 @@ export class EventStore implements IEventStore { } async setCreatedByUserId(batchSize: number): Promise { + const stopTimer = this.metricTimer('setCreatedByUserId'); const API_TOKEN_TABLE = 'api_tokens'; const toUpdate = await this.db(`${TABLE} as e`) @@ -592,6 +633,7 @@ export class EventStore implements IEventStore { }); await Promise.all(updatePromises); + stopTimer(); return toUpdate.length; } }