Add graphql queries error codes metrics (#12833)

## Context
Added to the existing useGraphQLErrorHandlerHook yoga hook to increment
metrics after all query executions based on their error codes. I
originally wanted to create a new useMetrics hook but most of the error
handling was done in useGraphQLErrorHandlerHook so we decided to keep it
there for now.

<img width="1310" alt="Screenshot 2025-06-24 at 15 58 26"
src="https://github.com/user-attachments/assets/498d3754-851a-4051-a5c2-23ac8253aa6a"
/>
This commit is contained in:
Weiko
2025-06-24 16:13:33 +02:00
committed by GitHub
parent b8fd10e9e8
commit ed11cac5f7
7 changed files with 78 additions and 7 deletions

View File

@ -19,6 +19,7 @@ import { GraphQLConfigModule } from 'src/engine/api/graphql/graphql-config/graph
import { GraphQLConfigService } from 'src/engine/api/graphql/graphql-config/graphql-config.service'; import { GraphQLConfigService } from 'src/engine/api/graphql/graphql-config/graphql-config.service';
import { MetadataGraphQLApiModule } from 'src/engine/api/graphql/metadata-graphql-api.module'; import { MetadataGraphQLApiModule } from 'src/engine/api/graphql/metadata-graphql-api.module';
import { RestApiModule } from 'src/engine/api/rest/rest-api.module'; import { RestApiModule } from 'src/engine/api/rest/rest-api.module';
import { MetricsModule } from 'src/engine/core-modules/metrics/metrics.module';
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module'; import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
import { WorkspaceMetadataCacheModule } from 'src/engine/metadata-modules/workspace-metadata-cache/workspace-metadata-cache.module'; import { WorkspaceMetadataCacheModule } from 'src/engine/metadata-modules/workspace-metadata-cache/workspace-metadata-cache.module';
import { GraphQLHydrateRequestFromTokenMiddleware } from 'src/engine/middlewares/graphql-hydrate-request-from-token.middleware'; import { GraphQLHydrateRequestFromTokenMiddleware } from 'src/engine/middlewares/graphql-hydrate-request-from-token.middleware';
@ -50,7 +51,7 @@ const MIGRATED_REST_METHODS = [
}), }),
GraphQLModule.forRootAsync<YogaDriverConfig>({ GraphQLModule.forRootAsync<YogaDriverConfig>({
driver: YogaDriver, driver: YogaDriver,
imports: [GraphQLConfigModule], imports: [GraphQLConfigModule, MetricsModule],
useClass: GraphQLConfigService, useClass: GraphQLConfigService,
}), }),
TwentyORMModule, TwentyORMModule,

View File

@ -21,6 +21,7 @@ import { CoreEngineModule } from 'src/engine/core-modules/core-engine.module';
import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service'; import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
import { useSentryTracing } from 'src/engine/core-modules/exception-handler/hooks/use-sentry-tracing'; import { useSentryTracing } from 'src/engine/core-modules/exception-handler/hooks/use-sentry-tracing';
import { useGraphQLErrorHandlerHook } from 'src/engine/core-modules/graphql/hooks/use-graphql-error-handler.hook'; import { useGraphQLErrorHandlerHook } from 'src/engine/core-modules/graphql/hooks/use-graphql-error-handler.hook';
import { MetricsService } from 'src/engine/core-modules/metrics/metrics.service';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { User } from 'src/engine/core-modules/user/user.entity'; import { User } from 'src/engine/core-modules/user/user.entity';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
@ -40,6 +41,7 @@ export class GraphQLConfigService
private readonly exceptionHandlerService: ExceptionHandlerService, private readonly exceptionHandlerService: ExceptionHandlerService,
private readonly twentyConfigService: TwentyConfigService, private readonly twentyConfigService: TwentyConfigService,
private readonly moduleRef: ModuleRef, private readonly moduleRef: ModuleRef,
private readonly metricsService: MetricsService,
) {} ) {}
createGqlOptions(): YogaDriverConfig { createGqlOptions(): YogaDriverConfig {
@ -54,6 +56,7 @@ export class GraphQLConfigService
}, },
}), }),
useGraphQLErrorHandlerHook({ useGraphQLErrorHandlerHook({
metricsService: this.metricsService,
exceptionHandlerService: this.exceptionHandlerService, exceptionHandlerService: this.exceptionHandlerService,
}), }),
]; ];

View File

@ -7,6 +7,8 @@ import { GraphQLConfigModule } from 'src/engine/api/graphql/graphql-config/graph
import { metadataModuleFactory } from 'src/engine/api/graphql/metadata.module-factory'; import { metadataModuleFactory } from 'src/engine/api/graphql/metadata.module-factory';
import { CacheStorageNamespace } from 'src/engine/core-modules/cache-storage/types/cache-storage-namespace.enum'; import { CacheStorageNamespace } from 'src/engine/core-modules/cache-storage/types/cache-storage-namespace.enum';
import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service'; import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
import { MetricsModule } from 'src/engine/core-modules/metrics/metrics.module';
import { MetricsService } from 'src/engine/core-modules/metrics/metrics.service';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { DataloaderModule } from 'src/engine/dataloaders/dataloader.module'; import { DataloaderModule } from 'src/engine/dataloaders/dataloader.module';
import { DataloaderService } from 'src/engine/dataloaders/dataloader.service'; import { DataloaderService } from 'src/engine/dataloaders/dataloader.service';
@ -19,12 +21,13 @@ import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/wor
GraphQLModule.forRootAsync<YogaDriverConfig>({ GraphQLModule.forRootAsync<YogaDriverConfig>({
driver: YogaDriver, driver: YogaDriver,
useFactory: metadataModuleFactory, useFactory: metadataModuleFactory,
imports: [GraphQLConfigModule, DataloaderModule], imports: [GraphQLConfigModule, DataloaderModule, MetricsModule],
inject: [ inject: [
TwentyConfigService, TwentyConfigService,
ExceptionHandlerService, ExceptionHandlerService,
DataloaderService, DataloaderService,
CacheStorageNamespace.EngineWorkspace, CacheStorageNamespace.EngineWorkspace,
MetricsService,
], ],
}), }),
MetadataEngineModule, MetadataEngineModule,

View File

@ -9,6 +9,7 @@ import { MetadataGraphQLApiModule } from 'src/engine/api/graphql/metadata-graphq
import { CacheStorageService } from 'src/engine/core-modules/cache-storage/services/cache-storage.service'; import { CacheStorageService } from 'src/engine/core-modules/cache-storage/services/cache-storage.service';
import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service'; import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
import { useGraphQLErrorHandlerHook } from 'src/engine/core-modules/graphql/hooks/use-graphql-error-handler.hook'; import { useGraphQLErrorHandlerHook } from 'src/engine/core-modules/graphql/hooks/use-graphql-error-handler.hook';
import { MetricsService } from 'src/engine/core-modules/metrics/metrics.service';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { DataloaderService } from 'src/engine/dataloaders/dataloader.service'; import { DataloaderService } from 'src/engine/dataloaders/dataloader.service';
import { renderApolloPlayground } from 'src/engine/utils/render-apollo-playground.util'; import { renderApolloPlayground } from 'src/engine/utils/render-apollo-playground.util';
@ -18,6 +19,7 @@ export const metadataModuleFactory = async (
exceptionHandlerService: ExceptionHandlerService, exceptionHandlerService: ExceptionHandlerService,
dataloaderService: DataloaderService, dataloaderService: DataloaderService,
cacheStorageService: CacheStorageService, cacheStorageService: CacheStorageService,
metricsService: MetricsService,
): Promise<YogaDriverConfig> => { ): Promise<YogaDriverConfig> => {
const config: YogaDriverConfig = { const config: YogaDriverConfig = {
autoSchemaFile: true, autoSchemaFile: true,
@ -35,6 +37,7 @@ export const metadataModuleFactory = async (
}, },
}), }),
useGraphQLErrorHandlerHook({ useGraphQLErrorHandlerHook({
metricsService: metricsService,
exceptionHandlerService, exceptionHandlerService,
}), }),
useCachedMetadata({ useCachedMetadata({

View File

@ -1,8 +1,8 @@
import { import {
OnExecuteDoneHookResultOnNextHook,
Plugin,
getDocumentString, getDocumentString,
handleStreamOrSingleExecutionResult, handleStreamOrSingleExecutionResult,
OnExecuteDoneHookResultOnNextHook,
Plugin,
} from '@envelop/core'; } from '@envelop/core';
import { GraphQLError, Kind, OperationDefinitionNode, print } from 'graphql'; import { GraphQLError, Kind, OperationDefinitionNode, print } from 'graphql';
@ -13,8 +13,14 @@ import { generateGraphQLErrorFromError } from 'src/engine/core-modules/graphql/u
import { import {
BaseGraphQLError, BaseGraphQLError,
convertGraphQLErrorToBaseGraphQLError, convertGraphQLErrorToBaseGraphQLError,
ErrorCode,
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util'; } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
import { shouldCaptureException } from 'src/engine/utils/global-exception-handler.util'; import { MetricsService } from 'src/engine/core-modules/metrics/metrics.service';
import { MetricsKeys } from 'src/engine/core-modules/metrics/types/metrics-keys.type';
import {
graphQLErrorCodesToFilter,
shouldCaptureException,
} from 'src/engine/utils/global-exception-handler.util';
const DEFAULT_EVENT_ID_KEY = 'exceptionEventId'; const DEFAULT_EVENT_ID_KEY = 'exceptionEventId';
const SCHEMA_VERSION_HEADER = 'x-schema-version'; const SCHEMA_VERSION_HEADER = 'x-schema-version';
@ -22,6 +28,8 @@ const SCHEMA_MISMATCH_ERROR =
'Your workspace has been updated with a new data model. Please refresh the page.'; 'Your workspace has been updated with a new data model. Please refresh the page.';
type GraphQLErrorHandlerHookOptions = { type GraphQLErrorHandlerHookOptions = {
metricsService: MetricsService;
/** /**
* The exception handler service to use. * The exception handler service to use.
*/ */
@ -81,6 +89,10 @@ export const useGraphQLErrorHandlerHook = <
setResult, setResult,
}) => { }) => {
if (!result.errors || result.errors.length === 0) { if (!result.errors || result.errors.length === 0) {
options.metricsService.incrementCounter({
key: MetricsKeys.GraphqlOperation200,
});
return; return;
} }
@ -105,6 +117,48 @@ export const useGraphQLErrorHandlerHook = <
return originalError; return originalError;
}); });
// Error metrics
const codeToMetricKey: Partial<Record<ErrorCode, MetricsKeys>> = {
[ErrorCode.UNAUTHENTICATED]: MetricsKeys.GraphqlOperation401,
[ErrorCode.FORBIDDEN]: MetricsKeys.GraphqlOperation403,
[ErrorCode.NOT_FOUND]: MetricsKeys.GraphqlOperation404,
[ErrorCode.INTERNAL_SERVER_ERROR]:
MetricsKeys.GraphqlOperation500,
};
const statusToMetricKey: Record<number, MetricsKeys> = {
400: MetricsKeys.GraphqlOperation400,
401: MetricsKeys.GraphqlOperation401,
403: MetricsKeys.GraphqlOperation403,
404: MetricsKeys.GraphqlOperation404,
500: MetricsKeys.GraphqlOperation500,
};
processedErrors.forEach((error) => {
let metricKey: MetricsKeys | undefined;
if (error instanceof BaseGraphQLError) {
const code = error.extensions?.code as ErrorCode;
metricKey = codeToMetricKey[code];
if (!metricKey && graphQLErrorCodesToFilter.includes(code)) {
metricKey = MetricsKeys.GraphqlOperation400;
}
} else if (error instanceof GraphQLError) {
const status = error.extensions?.http?.status as number;
metricKey = statusToMetricKey[status];
}
if (metricKey) {
options.metricsService.incrementCounter({ key: metricKey });
} else {
options.metricsService.incrementCounter({
key: MetricsKeys.GraphqlOperationUnknown,
});
}
});
// Step 2: Send errors to monitoring service (with stack traces) // Step 2: Send errors to monitoring service (with stack traces)
const errorsToCapture = processedErrors.filter( const errorsToCapture = processedErrors.filter(
shouldCaptureException, shouldCaptureException,

View File

@ -15,7 +15,7 @@ export class MetricsService {
shouldStoreInCache = true, shouldStoreInCache = true,
}: { }: {
key: MetricsKeys; key: MetricsKeys;
eventId: string; eventId?: string;
shouldStoreInCache?: boolean; shouldStoreInCache?: boolean;
}) { }) {
//TODO : Define meter name usage in monitoring //TODO : Define meter name usage in monitoring
@ -24,7 +24,7 @@ export class MetricsService {
counter.add(1); counter.add(1);
if (shouldStoreInCache) { if (shouldStoreInCache && eventId) {
this.metricsCacheService.updateCounter(key, [eventId]); this.metricsCacheService.updateCounter(key, [eventId]);
} }
} }

View File

@ -6,6 +6,13 @@ export enum MetricsKeys {
CalendarEventSyncJobFailedInsufficientPermissions = 'calendar-event-sync-job/failed-insufficient-permissions', CalendarEventSyncJobFailedInsufficientPermissions = 'calendar-event-sync-job/failed-insufficient-permissions',
CalendarEventSyncJobFailedUnknown = 'calendar-event-sync-job/failed-unknown', CalendarEventSyncJobFailedUnknown = 'calendar-event-sync-job/failed-unknown',
InvalidCaptcha = 'invalid-captcha', InvalidCaptcha = 'invalid-captcha',
GraphqlOperation200 = 'graphql-operation/200',
GraphqlOperation400 = 'graphql-operation/400',
GraphqlOperation401 = 'graphql-operation/401',
GraphqlOperation403 = 'graphql-operation/403',
GraphqlOperation404 = 'graphql-operation/404',
GraphqlOperation500 = 'graphql-operation/500',
GraphqlOperationUnknown = 'graphql-operation/unknown',
WorkflowRunStartedDatabaseEventTrigger = 'workflow-run/started/database-event-trigger', WorkflowRunStartedDatabaseEventTrigger = 'workflow-run/started/database-event-trigger',
WorkflowRunStartedCronTrigger = 'workflow-run/started/cron-trigger', WorkflowRunStartedCronTrigger = 'workflow-run/started/cron-trigger',
WorkflowRunStartedWebhookTrigger = 'workflow-run/started/webhook-trigger', WorkflowRunStartedWebhookTrigger = 'workflow-run/started/webhook-trigger',