Add metrics for GQL query duration (#516)

* Record GQL query durations by operation name

* Use try finally for timer metric

* Export watcher repo URL in metrics

* Remove unnecessary prefix from repo link

* Update repository label name

---------

Co-authored-by: Prathamesh Musale <prathamesh.musale0@gmail.com>
This commit is contained in:
Nabarun Gogoi 2024-06-06 13:57:33 +05:30 committed by GitHub
parent 7cc61579d8
commit 836fe45aa5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 95 additions and 52 deletions

View File

@ -31,7 +31,7 @@
}, },
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git+https://github.com/cerc-io/watcher-ts.git" "url": "https://github.com/cerc-io/watcher-ts.git"
}, },
"author": "", "author": "",
"license": "AGPL-3.0", "license": "AGPL-3.0",

View File

@ -12,6 +12,7 @@ import {
{{/if}} {{/if}}
gqlTotalQueryCount, gqlTotalQueryCount,
gqlQueryCount, gqlQueryCount,
gqlQueryDuration,
getResultState, getResultState,
IndexerInterface, IndexerInterface,
GraphQLBigInt, GraphQLBigInt,
@ -36,6 +37,20 @@ import { {{query.entityName}} } from './entity/{{query.entityName}}';
const log = debug('vulcanize:resolver'); const log = debug('vulcanize:resolver');
const executeAndRecordMetrics = async (gqlLabel: string, operation: () => Promise<any>) => {
gqlTotalQueryCount.inc(1);
gqlQueryCount.labels(gqlLabel).inc(1);
const endTimer = gqlQueryDuration.labels(gqlLabel).startTimer();
try {
const result = await operation();
return result;
} finally {
endTimer();
}
};
export const createResolvers = async (indexerArg: IndexerInterface, eventWatcher: EventWatcher): Promise<any> => { export const createResolvers = async (indexerArg: IndexerInterface, eventWatcher: EventWatcher): Promise<any> => {
const indexer = indexerArg as Indexer; const indexer = indexerArg as Indexer;
@ -84,14 +99,15 @@ export const createResolvers = async (indexerArg: IndexerInterface, eventWatcher
): Promise<ValueResult> => { ): Promise<ValueResult> => {
log('{{this.name}}', blockHash, contractAddress log('{{this.name}}', blockHash, contractAddress
{{~#each this.params}}, {{this.name~}} {{/each}}); {{~#each this.params}}, {{this.name~}} {{/each}});
gqlTotalQueryCount.inc(1);
gqlQueryCount.labels('{{this.name}}').inc(1);
// Set cache-control hints // Set cache-control hints
// setGQLCacheHints(info, {}, gqlCacheConfig); // setGQLCacheHints(info, {}, gqlCacheConfig);
return indexer.{{this.name}}(blockHash, contractAddress return executeAndRecordMetrics(
{{~#each this.params}}, {{this.name~}} {{/each}}); '{{this.name}}',
async () => indexer.{{this.name}}(blockHash, contractAddress
{{~#each this.params}}, {{this.name~}} {{/each}})
);
}, },
{{/each}} {{/each}}
@ -104,13 +120,14 @@ export const createResolvers = async (indexerArg: IndexerInterface, eventWatcher
info: GraphQLResolveInfo info: GraphQLResolveInfo
) => { ) => {
log('{{this.queryName}}', id, JSON.stringify(block, jsonBigIntStringReplacer)); log('{{this.queryName}}', id, JSON.stringify(block, jsonBigIntStringReplacer));
gqlTotalQueryCount.inc(1);
gqlQueryCount.labels('{{this.queryName}}').inc(1);
// Set cache-control hints // Set cache-control hints
// setGQLCacheHints(info, block, gqlCacheConfig); // setGQLCacheHints(info, block, gqlCacheConfig);
return indexer.getSubgraphEntity({{this.entityName}}, id, block, info); return executeAndRecordMetrics(
'{{this.queryName}}',
async () => indexer.getSubgraphEntity({{this.entityName}}, id, block, info)
);
}, },
{{this.pluralQueryName}}: async ( {{this.pluralQueryName}}: async (
@ -120,73 +137,86 @@ export const createResolvers = async (indexerArg: IndexerInterface, eventWatcher
info: GraphQLResolveInfo info: GraphQLResolveInfo
) => { ) => {
log('{{this.pluralQueryName}}', JSON.stringify(block, jsonBigIntStringReplacer), JSON.stringify(where, jsonBigIntStringReplacer), first, skip, orderBy, orderDirection); log('{{this.pluralQueryName}}', JSON.stringify(block, jsonBigIntStringReplacer), JSON.stringify(where, jsonBigIntStringReplacer), first, skip, orderBy, orderDirection);
gqlTotalQueryCount.inc(1);
gqlQueryCount.labels('{{this.pluralQueryName}}').inc(1);
// Set cache-control hints // Set cache-control hints
// setGQLCacheHints(info, block, gqlCacheConfig); // setGQLCacheHints(info, block, gqlCacheConfig);
return indexer.getSubgraphEntities( return executeAndRecordMetrics(
{{this.entityName}}, '{{this.pluralQueryName}}',
block, async () => indexer.getSubgraphEntities(
where, {{this.entityName}},
{ limit: first, skip, orderBy, orderDirection }, block,
info where,
{ limit: first, skip, orderBy, orderDirection },
info
)
); );
}, },
{{/each}} {{/each}}
events: async (_: any, { blockHash, contractAddress, name }: { blockHash: string, contractAddress: string, name?: string }) => { events: async (_: any, { blockHash, contractAddress, name }: { blockHash: string, contractAddress: string, name?: string }) => {
log('events', blockHash, contractAddress, name); log('events', blockHash, contractAddress, name);
gqlTotalQueryCount.inc(1);
gqlQueryCount.labels('events').inc(1);
const block = await indexer.getBlockProgress(blockHash); return executeAndRecordMetrics(
if (!block || !block.isComplete) { 'events',
throw new Error(`Block hash ${blockHash} number ${block?.blockNumber} not processed yet`); async () => {
} const block = await indexer.getBlockProgress(blockHash);
if (!block || !block.isComplete) {
throw new Error(`Block hash ${blockHash} number ${block?.blockNumber} not processed yet`);
}
const events = await indexer.getEventsByFilter(blockHash, contractAddress, name); const events = await indexer.getEventsByFilter(blockHash, contractAddress, name);
return events.map(event => indexer.getResultEvent(event)); return events.map(event => indexer.getResultEvent(event));
}
);
}, },
eventsInRange: async (_: any, { fromBlockNumber, toBlockNumber }: { fromBlockNumber: number, toBlockNumber: number }) => { eventsInRange: async (_: any, { fromBlockNumber, toBlockNumber }: { fromBlockNumber: number, toBlockNumber: number }) => {
log('eventsInRange', fromBlockNumber, toBlockNumber); log('eventsInRange', fromBlockNumber, toBlockNumber);
gqlTotalQueryCount.inc(1);
gqlQueryCount.labels('eventsInRange').inc(1);
const syncStatus = await indexer.getSyncStatus(); return executeAndRecordMetrics(
'eventsInRange',
async () => {
const syncStatus = await indexer.getSyncStatus();
if (!syncStatus) { if (!syncStatus) {
throw new Error('No blocks processed yet'); throw new Error('No blocks processed yet');
} }
if ((fromBlockNumber < syncStatus.initialIndexedBlockNumber) || (toBlockNumber > syncStatus.latestProcessedBlockNumber)) { if ((fromBlockNumber < syncStatus.initialIndexedBlockNumber) || (toBlockNumber > syncStatus.latestProcessedBlockNumber)) {
throw new Error(`Block range should be between ${syncStatus.initialIndexedBlockNumber} and ${syncStatus.latestProcessedBlockNumber}`); throw new Error(`Block range should be between ${syncStatus.initialIndexedBlockNumber} and ${syncStatus.latestProcessedBlockNumber}`);
} }
const events = await indexer.getEventsInRange(fromBlockNumber, toBlockNumber); const events = await indexer.getEventsInRange(fromBlockNumber, toBlockNumber);
return events.map(event => indexer.getResultEvent(event)); return events.map(event => indexer.getResultEvent(event));
}
);
}, },
getStateByCID: async (_: any, { cid }: { cid: string }) => { getStateByCID: async (_: any, { cid }: { cid: string }) => {
log('getStateByCID', cid); log('getStateByCID', cid);
gqlTotalQueryCount.inc(1);
gqlQueryCount.labels('getStateByCID').inc(1);
const state = await indexer.getStateByCID(cid); return executeAndRecordMetrics(
'getStateByCID',
async () => {
const state = await indexer.getStateByCID(cid);
return state && state.block.isComplete ? getResultState(state) : undefined; return state && state.block.isComplete ? getResultState(state) : undefined;
}
);
}, },
getState: async (_: any, { blockHash, contractAddress, kind }: { blockHash: string, contractAddress: string, kind: string }) => { getState: async (_: any, { blockHash, contractAddress, kind }: { blockHash: string, contractAddress: string, kind: string }) => {
log('getState', blockHash, contractAddress, kind); log('getState', blockHash, contractAddress, kind);
gqlTotalQueryCount.inc(1);
gqlQueryCount.labels('getState').inc(1);
const state = await indexer.getPrevState(blockHash, contractAddress, kind); return executeAndRecordMetrics(
'getState',
async () => {
const state = await indexer.getPrevState(blockHash, contractAddress, kind);
return state && state.block.isComplete ? getResultState(state) : undefined; return state && state.block.isComplete ? getResultState(state) : undefined;
}
);
}, },
{{#if (subgraphPath)}} {{#if (subgraphPath)}}
@ -195,19 +225,21 @@ export const createResolvers = async (indexerArg: IndexerInterface, eventWatcher
{ block = {} }: { block: BlockHeight } { block = {} }: { block: BlockHeight }
) => { ) => {
log('_meta'); log('_meta');
gqlTotalQueryCount.inc(1);
gqlQueryCount.labels('_meta').inc(1);
return indexer.getMetaData(block); return executeAndRecordMetrics(
'_meta',
async () => indexer.getMetaData(block)
);
}, },
{{/if}} {{/if}}
getSyncStatus: async () => { getSyncStatus: async () => {
log('getSyncStatus'); log('getSyncStatus');
gqlTotalQueryCount.inc(1);
gqlQueryCount.labels('getSyncStatus').inc(1);
return indexer.getSyncStatus(); return executeAndRecordMetrics(
'getSyncStatus',
async () => indexer.getSyncStatus()
);
} }
} }
}; };

View File

@ -27,6 +27,13 @@ export const gqlQueryCount = new client.Counter({
registers: [gqlRegistry] registers: [gqlRegistry]
}); });
export const gqlQueryDuration = new client.Gauge({
name: 'gql_query_duration_seconds',
help: 'Duration of GQL queries',
labelNames: ['name'] as const,
registers: [gqlRegistry]
});
// Export metrics on a server // Export metrics on a server
const app: Application = express(); const app: Application = express();

View File

@ -288,8 +288,12 @@ const registerWatcherInfoMetrics = async (): Promise<void> => {
const watcherInfoMetric = new client.Gauge({ const watcherInfoMetric = new client.Gauge({
name: 'watcher_info', name: 'watcher_info',
help: 'Watcher info (static)', help: 'Watcher info (static)',
labelNames: ['version', 'commitHash'] labelNames: ['repository', 'version', 'commitHash']
}); });
watcherInfoMetric.set({ version: pkgJson.version, commitHash: pkgJson.commitHash }, 1); watcherInfoMetric.set({
repository: pkgJson.repository && pkgJson.repository.url.replace(/^git\+/, ''),
version: pkgJson.version,
commitHash: pkgJson.commitHash
}, 1);
}; };