set up metrics collecting with open telemetry (#11236)
Done : - move metrics and health cache services from health module to metrics module - refactor metrics counter from specific method to set up from enum keys - add OpenTelemetry (Otel) instrumentation for metrics - set up Otel SDK to send metrics to Otel collector To do later : - implement Otel instrumentation for traces + plug Sentry on top
This commit is contained in:
@ -21,10 +21,6 @@ export const SettingsAdminHealthAccountSyncCountersTable = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const items = [
|
const items = [
|
||||||
{
|
|
||||||
label: 'Not Synced',
|
|
||||||
value: details.counters.NOT_SYNCED,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
label: 'Active Sync',
|
label: 'Active Sync',
|
||||||
value: details.counters.ACTIVE,
|
value: details.counters.ACTIVE,
|
||||||
|
|||||||
@ -40,6 +40,7 @@ FRONTEND_URL=http://localhost:3001
|
|||||||
# LOGGER_DRIVER=console
|
# LOGGER_DRIVER=console
|
||||||
# LOGGER_IS_BUFFER_ENABLED=true
|
# LOGGER_IS_BUFFER_ENABLED=true
|
||||||
# EXCEPTION_HANDLER_DRIVER=sentry
|
# EXCEPTION_HANDLER_DRIVER=sentry
|
||||||
|
# METER_DRIVER=opentelemetry,console
|
||||||
# SENTRY_ENVIRONMENT=main
|
# SENTRY_ENVIRONMENT=main
|
||||||
# SENTRY_RELEASE=latest
|
# SENTRY_RELEASE=latest
|
||||||
# SENTRY_DSN=https://xxx@xxx.ingest.sentry.io/xxx
|
# SENTRY_DSN=https://xxx@xxx.ingest.sentry.io/xxx
|
||||||
|
|||||||
@ -25,6 +25,9 @@
|
|||||||
"@nestjs/devtools-integration": "^0.1.6",
|
"@nestjs/devtools-integration": "^0.1.6",
|
||||||
"@nestjs/graphql": "patch:@nestjs/graphql@12.1.1#./patches/@nestjs+graphql+12.1.1.patch",
|
"@nestjs/graphql": "patch:@nestjs/graphql@12.1.1#./patches/@nestjs+graphql+12.1.1.patch",
|
||||||
"@node-saml/passport-saml": "^5.0.0",
|
"@node-saml/passport-saml": "^5.0.0",
|
||||||
|
"@opentelemetry/api": "^1.9.0",
|
||||||
|
"@opentelemetry/exporter-metrics-otlp-http": "^0.200.0",
|
||||||
|
"@opentelemetry/sdk-metrics": "^2.0.0",
|
||||||
"@ptc-org/nestjs-query-graphql": "patch:@ptc-org/nestjs-query-graphql@4.2.0#./patches/@ptc-org+nestjs-query-graphql+4.2.0.patch",
|
"@ptc-org/nestjs-query-graphql": "patch:@ptc-org/nestjs-query-graphql@4.2.0#./patches/@ptc-org+nestjs-query-graphql+4.2.0.patch",
|
||||||
"@revertdotdev/revert-react": "^0.0.21",
|
"@revertdotdev/revert-react": "^0.0.21",
|
||||||
"@sentry/nestjs": "^8.30.0",
|
"@sentry/nestjs": "^8.30.0",
|
||||||
|
|||||||
@ -30,9 +30,9 @@ import { FeatureFlag } from 'src/engine/core-modules/feature-flag/feature-flag.e
|
|||||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||||
import { FileUploadModule } from 'src/engine/core-modules/file/file-upload/file-upload.module';
|
import { FileUploadModule } from 'src/engine/core-modules/file/file-upload/file-upload.module';
|
||||||
import { GuardRedirectModule } from 'src/engine/core-modules/guard-redirect/guard-redirect.module';
|
import { GuardRedirectModule } from 'src/engine/core-modules/guard-redirect/guard-redirect.module';
|
||||||
import { HealthModule } from 'src/engine/core-modules/health/health.module';
|
|
||||||
import { JwtModule } from 'src/engine/core-modules/jwt/jwt.module';
|
import { JwtModule } from 'src/engine/core-modules/jwt/jwt.module';
|
||||||
import { KeyValuePair } from 'src/engine/core-modules/key-value-pair/key-value-pair.entity';
|
import { KeyValuePair } from 'src/engine/core-modules/key-value-pair/key-value-pair.entity';
|
||||||
|
import { MetricsModule } from 'src/engine/core-modules/metrics/metrics.module';
|
||||||
import { OnboardingModule } from 'src/engine/core-modules/onboarding/onboarding.module';
|
import { OnboardingModule } from 'src/engine/core-modules/onboarding/onboarding.module';
|
||||||
import { WorkspaceSSOModule } from 'src/engine/core-modules/sso/sso.module';
|
import { WorkspaceSSOModule } from 'src/engine/core-modules/sso/sso.module';
|
||||||
import { WorkspaceSSOIdentityProvider } from 'src/engine/core-modules/sso/workspace-sso-identity-provider.entity';
|
import { WorkspaceSSOIdentityProvider } from 'src/engine/core-modules/sso/workspace-sso-identity-provider.entity';
|
||||||
@ -90,7 +90,7 @@ import { JwtAuthStrategy } from './strategies/jwt.auth.strategy';
|
|||||||
WorkspaceInvitationModule,
|
WorkspaceInvitationModule,
|
||||||
EmailVerificationModule,
|
EmailVerificationModule,
|
||||||
GuardRedirectModule,
|
GuardRedirectModule,
|
||||||
HealthModule,
|
MetricsModule,
|
||||||
PermissionsModule,
|
PermissionsModule,
|
||||||
UserRoleModule,
|
UserRoleModule,
|
||||||
],
|
],
|
||||||
|
|||||||
@ -7,13 +7,14 @@ import {
|
|||||||
import { GqlExecutionContext } from '@nestjs/graphql';
|
import { GqlExecutionContext } from '@nestjs/graphql';
|
||||||
|
|
||||||
import { CaptchaService } from 'src/engine/core-modules/captcha/captcha.service';
|
import { CaptchaService } from 'src/engine/core-modules/captcha/captcha.service';
|
||||||
import { HealthCacheService } from 'src/engine/core-modules/health/health-cache.service';
|
import { MetricsService } from 'src/engine/core-modules/metrics/metrics.service';
|
||||||
|
import { MetricsKeys } from 'src/engine/core-modules/metrics/types/metrics-keys.type';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CaptchaGuard implements CanActivate {
|
export class CaptchaGuard implements CanActivate {
|
||||||
constructor(
|
constructor(
|
||||||
private captchaService: CaptchaService,
|
private captchaService: CaptchaService,
|
||||||
private healthCacheService: HealthCacheService,
|
private metricsService: MetricsService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||||
@ -26,7 +27,10 @@ export class CaptchaGuard implements CanActivate {
|
|||||||
if (result.success) {
|
if (result.success) {
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
await this.healthCacheService.updateInvalidCaptchaCache(token);
|
await this.metricsService.incrementCounter({
|
||||||
|
key: MetricsKeys.InvalidCaptcha,
|
||||||
|
eventId: token,
|
||||||
|
});
|
||||||
|
|
||||||
throw new BadRequestException(
|
throw new BadRequestException(
|
||||||
'Invalid Captcha, please try another device',
|
'Invalid Captcha, please try another device',
|
||||||
|
|||||||
@ -54,61 +54,67 @@ export const ENVIRONMENT_VARIABLES_GROUP_METADATA: Record<
|
|||||||
'By default, exceptions are sent to the logs. This should be enough for most self-hosting use-cases. For our cloud app we use Sentry.',
|
'By default, exceptions are sent to the logs. This should be enough for most self-hosting use-cases. For our cloud app we use Sentry.',
|
||||||
isHiddenOnLoad: true,
|
isHiddenOnLoad: true,
|
||||||
},
|
},
|
||||||
[EnvironmentVariablesGroup.Other]: {
|
[EnvironmentVariablesGroup.Metering]: {
|
||||||
position: 900,
|
position: 900,
|
||||||
|
description:
|
||||||
|
'By default, metrics are sent to the console. OpenTelemetry collector can be set up for self-hosting use-cases.',
|
||||||
|
isHiddenOnLoad: true,
|
||||||
|
},
|
||||||
|
[EnvironmentVariablesGroup.Other]: {
|
||||||
|
position: 1000,
|
||||||
description:
|
description:
|
||||||
"The variables in this section are mostly used for internal purposes (running our Cloud offering), but shouldn't usually be required for a simple self-hosted instance",
|
"The variables in this section are mostly used for internal purposes (running our Cloud offering), but shouldn't usually be required for a simple self-hosted instance",
|
||||||
isHiddenOnLoad: true,
|
isHiddenOnLoad: true,
|
||||||
},
|
},
|
||||||
[EnvironmentVariablesGroup.BillingConfig]: {
|
[EnvironmentVariablesGroup.BillingConfig]: {
|
||||||
position: 1000,
|
position: 1100,
|
||||||
description:
|
description:
|
||||||
'We use Stripe in our Cloud app to charge customers. Not relevant to Self-hosters.',
|
'We use Stripe in our Cloud app to charge customers. Not relevant to Self-hosters.',
|
||||||
isHiddenOnLoad: true,
|
isHiddenOnLoad: true,
|
||||||
},
|
},
|
||||||
[EnvironmentVariablesGroup.CaptchaConfig]: {
|
[EnvironmentVariablesGroup.CaptchaConfig]: {
|
||||||
position: 1100,
|
position: 1200,
|
||||||
description:
|
description:
|
||||||
'This protects critical endpoints like login and signup with a captcha to prevent bot attacks. Likely unnecessary for self-hosting scenarios.',
|
'This protects critical endpoints like login and signup with a captcha to prevent bot attacks. Likely unnecessary for self-hosting scenarios.',
|
||||||
isHiddenOnLoad: true,
|
isHiddenOnLoad: true,
|
||||||
},
|
},
|
||||||
[EnvironmentVariablesGroup.CloudflareConfig]: {
|
[EnvironmentVariablesGroup.CloudflareConfig]: {
|
||||||
position: 1200,
|
position: 1300,
|
||||||
description: '',
|
description: '',
|
||||||
isHiddenOnLoad: true,
|
isHiddenOnLoad: true,
|
||||||
},
|
},
|
||||||
[EnvironmentVariablesGroup.LLM]: {
|
[EnvironmentVariablesGroup.LLM]: {
|
||||||
position: 1300,
|
position: 1400,
|
||||||
description:
|
description:
|
||||||
'Configure the LLM provider and model to use for the app. This is experimental and not linked to any public feature.',
|
'Configure the LLM provider and model to use for the app. This is experimental and not linked to any public feature.',
|
||||||
isHiddenOnLoad: true,
|
isHiddenOnLoad: true,
|
||||||
},
|
},
|
||||||
[EnvironmentVariablesGroup.ServerlessConfig]: {
|
[EnvironmentVariablesGroup.ServerlessConfig]: {
|
||||||
position: 1400,
|
position: 1500,
|
||||||
description:
|
description:
|
||||||
'In our multi-tenant cloud app, we offload untrusted custom code from workflows to a serverless system (Lambda) for enhanced security and scalability. Self-hosters with a single tenant can typically ignore this configuration.',
|
'In our multi-tenant cloud app, we offload untrusted custom code from workflows to a serverless system (Lambda) for enhanced security and scalability. Self-hosters with a single tenant can typically ignore this configuration.',
|
||||||
isHiddenOnLoad: true,
|
isHiddenOnLoad: true,
|
||||||
},
|
},
|
||||||
[EnvironmentVariablesGroup.SSL]: {
|
[EnvironmentVariablesGroup.SSL]: {
|
||||||
position: 1500,
|
position: 1600,
|
||||||
description:
|
description:
|
||||||
'Configure this if you want to setup SSL on your server or full end-to-end encryption. If you just want basic HTTPS, a simple setup like Cloudflare in flexible mode might be easier.',
|
'Configure this if you want to setup SSL on your server or full end-to-end encryption. If you just want basic HTTPS, a simple setup like Cloudflare in flexible mode might be easier.',
|
||||||
isHiddenOnLoad: true,
|
isHiddenOnLoad: true,
|
||||||
},
|
},
|
||||||
[EnvironmentVariablesGroup.SupportChatConfig]: {
|
[EnvironmentVariablesGroup.SupportChatConfig]: {
|
||||||
position: 1600,
|
position: 1700,
|
||||||
description:
|
description:
|
||||||
'We use this to setup a small support chat on the bottom left. Currently powered by Front.',
|
'We use this to setup a small support chat on the bottom left. Currently powered by Front.',
|
||||||
isHiddenOnLoad: true,
|
isHiddenOnLoad: true,
|
||||||
},
|
},
|
||||||
[EnvironmentVariablesGroup.AnalyticsConfig]: {
|
[EnvironmentVariablesGroup.AnalyticsConfig]: {
|
||||||
position: 1700,
|
position: 1800,
|
||||||
description:
|
description:
|
||||||
'We’re running a test to perform analytics within the app. This will evolve.',
|
'We’re running a test to perform analytics within the app. This will evolve.',
|
||||||
isHiddenOnLoad: true,
|
isHiddenOnLoad: true,
|
||||||
},
|
},
|
||||||
[EnvironmentVariablesGroup.TokensDuration]: {
|
[EnvironmentVariablesGroup.TokensDuration]: {
|
||||||
position: 1800,
|
position: 1900,
|
||||||
description:
|
description:
|
||||||
'These have been set to sensible default so you probably don’t need to change them unless you have a specific use-case.',
|
'These have been set to sensible default so you probably don’t need to change them unless you have a specific use-case.',
|
||||||
isHiddenOnLoad: true,
|
isHiddenOnLoad: true,
|
||||||
|
|||||||
@ -0,0 +1,21 @@
|
|||||||
|
import { Transform } from 'class-transformer';
|
||||||
|
|
||||||
|
import { MeterDriver } from 'src/engine/core-modules/metrics/types/meter-driver.type';
|
||||||
|
|
||||||
|
export const CastToMeterDriverArray = () =>
|
||||||
|
Transform(({ value }: { value: string }) => toMeterDriverArray(value));
|
||||||
|
|
||||||
|
const toMeterDriverArray = (value: string | undefined) => {
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
const rawMeterDrivers = value.split(',').map((driver) => driver.trim());
|
||||||
|
const isInvalid = rawMeterDrivers.some(
|
||||||
|
(driver) => !Object.values(MeterDriver).includes(driver as MeterDriver),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isInvalid) {
|
||||||
|
return rawMeterDrivers;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
@ -6,6 +6,7 @@ export enum EnvironmentVariablesGroup {
|
|||||||
MicrosoftAuth = 'microsoft-auth',
|
MicrosoftAuth = 'microsoft-auth',
|
||||||
EmailSettings = 'email-settings',
|
EmailSettings = 'email-settings',
|
||||||
Logging = 'logging',
|
Logging = 'logging',
|
||||||
|
Metering = 'metering',
|
||||||
ExceptionHandler = 'exception-handler',
|
ExceptionHandler = 'exception-handler',
|
||||||
Other = 'other',
|
Other = 'other',
|
||||||
BillingConfig = 'billing-config',
|
BillingConfig = 'billing-config',
|
||||||
|
|||||||
@ -25,6 +25,7 @@ import { LLMTracingDriver } from 'src/engine/core-modules/llm-tracing/interfaces
|
|||||||
import { CaptchaDriverType } from 'src/engine/core-modules/captcha/interfaces';
|
import { CaptchaDriverType } from 'src/engine/core-modules/captcha/interfaces';
|
||||||
import { CastToBoolean } from 'src/engine/core-modules/environment/decorators/cast-to-boolean.decorator';
|
import { CastToBoolean } from 'src/engine/core-modules/environment/decorators/cast-to-boolean.decorator';
|
||||||
import { CastToLogLevelArray } from 'src/engine/core-modules/environment/decorators/cast-to-log-level-array.decorator';
|
import { CastToLogLevelArray } from 'src/engine/core-modules/environment/decorators/cast-to-log-level-array.decorator';
|
||||||
|
import { CastToMeterDriverArray } from 'src/engine/core-modules/environment/decorators/cast-to-meter-driver.decorator';
|
||||||
import { CastToPositiveNumber } from 'src/engine/core-modules/environment/decorators/cast-to-positive-number.decorator';
|
import { CastToPositiveNumber } from 'src/engine/core-modules/environment/decorators/cast-to-positive-number.decorator';
|
||||||
import { EnvironmentVariablesMetadata } from 'src/engine/core-modules/environment/decorators/environment-variables-metadata.decorator';
|
import { EnvironmentVariablesMetadata } from 'src/engine/core-modules/environment/decorators/environment-variables-metadata.decorator';
|
||||||
import { IsAWSRegion } from 'src/engine/core-modules/environment/decorators/is-aws-region.decorator';
|
import { IsAWSRegion } from 'src/engine/core-modules/environment/decorators/is-aws-region.decorator';
|
||||||
@ -36,6 +37,7 @@ import { EnvironmentVariablesGroup } from 'src/engine/core-modules/environment/e
|
|||||||
import { ExceptionHandlerDriver } from 'src/engine/core-modules/exception-handler/interfaces';
|
import { ExceptionHandlerDriver } from 'src/engine/core-modules/exception-handler/interfaces';
|
||||||
import { StorageDriverType } from 'src/engine/core-modules/file-storage/interfaces';
|
import { StorageDriverType } from 'src/engine/core-modules/file-storage/interfaces';
|
||||||
import { LoggerDriverType } from 'src/engine/core-modules/logger/interfaces';
|
import { LoggerDriverType } from 'src/engine/core-modules/logger/interfaces';
|
||||||
|
import { MeterDriver } from 'src/engine/core-modules/metrics/types/meter-driver.type';
|
||||||
import { ServerlessDriverType } from 'src/engine/core-modules/serverless/serverless.interface';
|
import { ServerlessDriverType } from 'src/engine/core-modules/serverless/serverless.interface';
|
||||||
|
|
||||||
export class EnvironmentVariables {
|
export class EnvironmentVariables {
|
||||||
@ -585,6 +587,22 @@ export class EnvironmentVariables {
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
LOG_LEVELS: LogLevel[] = ['log', 'error', 'warn'];
|
LOG_LEVELS: LogLevel[] = ['log', 'error', 'warn'];
|
||||||
|
|
||||||
|
@EnvironmentVariablesMetadata({
|
||||||
|
group: EnvironmentVariablesGroup.Metering,
|
||||||
|
description: 'Driver used for collect metrics (OpenTelemetry or Console)',
|
||||||
|
})
|
||||||
|
@CastToMeterDriverArray()
|
||||||
|
@IsOptional()
|
||||||
|
METER_DRIVER: MeterDriver[] = [MeterDriver.Console];
|
||||||
|
|
||||||
|
@EnvironmentVariablesMetadata({
|
||||||
|
group: EnvironmentVariablesGroup.Metering,
|
||||||
|
description: 'Endpoint URL for the OpenTelemetry collector',
|
||||||
|
})
|
||||||
|
@ValidateIf((env) => env.METER_DRIVER.includes(MeterDriver.OpenTelemetry))
|
||||||
|
@IsOptional()
|
||||||
|
OTLP_COLLECTOR_ENDPOINT_URL: string;
|
||||||
|
|
||||||
@EnvironmentVariablesMetadata({
|
@EnvironmentVariablesMetadata({
|
||||||
group: EnvironmentVariablesGroup.ExceptionHandler,
|
group: EnvironmentVariablesGroup.ExceptionHandler,
|
||||||
description: 'Driver used for logging (only console for now)',
|
description: 'Driver used for logging (only console for now)',
|
||||||
|
|||||||
@ -1,30 +0,0 @@
|
|||||||
import { Test, TestingModule } from '@nestjs/testing';
|
|
||||||
|
|
||||||
import { MetricsController } from 'src/engine/core-modules/health/controllers/metrics.controller';
|
|
||||||
import { HealthCacheService } from 'src/engine/core-modules/health/health-cache.service';
|
|
||||||
|
|
||||||
describe('MetricsController', () => {
|
|
||||||
let metricsController: MetricsController;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
const testingModule: TestingModule = await Test.createTestingModule({
|
|
||||||
controllers: [MetricsController],
|
|
||||||
providers: [
|
|
||||||
{
|
|
||||||
provide: HealthCacheService,
|
|
||||||
useValue: {
|
|
||||||
getMessageChannelSyncJobByStatusCounter: jest.fn(),
|
|
||||||
getCalendarChannelSyncJobByStatusCounter: jest.fn(),
|
|
||||||
getInvalidCaptchaCounter: jest.fn(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}).compile();
|
|
||||||
|
|
||||||
metricsController = testingModule.get<MetricsController>(MetricsController);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be defined', () => {
|
|
||||||
expect(metricsController).toBeDefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -1,28 +0,0 @@
|
|||||||
import { Controller, Get } from '@nestjs/common';
|
|
||||||
|
|
||||||
import { HealthCacheService } from 'src/engine/core-modules/health/health-cache.service';
|
|
||||||
import { HealthCounterCacheKeys } from 'src/engine/core-modules/health/types/health-counter-cache-keys.type';
|
|
||||||
|
|
||||||
@Controller('metrics')
|
|
||||||
export class MetricsController {
|
|
||||||
constructor(private readonly healthCacheService: HealthCacheService) {}
|
|
||||||
|
|
||||||
@Get('/message-channel-sync-job-by-status-counter')
|
|
||||||
getMessageChannelSyncJobByStatusCounter() {
|
|
||||||
return this.healthCacheService.countChannelSyncJobByStatus(
|
|
||||||
HealthCounterCacheKeys.MessageChannelSyncJobByStatus,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get('/invalid-captcha-counter')
|
|
||||||
getInvalidCaptchaCounter() {
|
|
||||||
return this.healthCacheService.getInvalidCaptchaCounter();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get('/calendar-channel-sync-job-by-status-counter')
|
|
||||||
getCalendarChannelSyncJobByStatusCounter() {
|
|
||||||
return this.healthCacheService.countChannelSyncJobByStatus(
|
|
||||||
HealthCounterCacheKeys.CalendarEventSyncJobByStatus,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,139 +0,0 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
|
|
||||||
import { InjectCacheStorage } from 'src/engine/core-modules/cache-storage/decorators/cache-storage.decorator';
|
|
||||||
import { CacheStorageService } from 'src/engine/core-modules/cache-storage/services/cache-storage.service';
|
|
||||||
import { CacheStorageNamespace } from 'src/engine/core-modules/cache-storage/types/cache-storage-namespace.enum';
|
|
||||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
|
||||||
import { AccountSyncJobByStatusCounter } from 'src/engine/core-modules/health/types/account-sync-metrics.types';
|
|
||||||
import { HealthCounterCacheKeys } from 'src/engine/core-modules/health/types/health-counter-cache-keys.type';
|
|
||||||
import { CalendarChannelSyncStatus } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
|
|
||||||
import { MessageChannelSyncStatus } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
|
|
||||||
|
|
||||||
const CACHE_BUCKET_DURATION_MS = 15000; // 15 seconds window for each cache bucket
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class HealthCacheService {
|
|
||||||
private readonly healthMetricsTimeWindowInMinutes: number;
|
|
||||||
private readonly healthCacheTtl: number;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
@InjectCacheStorage(CacheStorageNamespace.EngineHealth)
|
|
||||||
private readonly cacheStorage: CacheStorageService,
|
|
||||||
private readonly environmentService: EnvironmentService,
|
|
||||||
) {
|
|
||||||
this.healthMetricsTimeWindowInMinutes = this.environmentService.get(
|
|
||||||
'HEALTH_METRICS_TIME_WINDOW_IN_MINUTES',
|
|
||||||
);
|
|
||||||
this.healthCacheTtl = this.healthMetricsTimeWindowInMinutes * 60000 * 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
private getCacheBucketStartTimestamp(timestamp: number): number {
|
|
||||||
return (
|
|
||||||
Math.floor(timestamp / CACHE_BUCKET_DURATION_MS) *
|
|
||||||
CACHE_BUCKET_DURATION_MS
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private getCacheKeyWithTimestamp(key: string, timestamp?: number): string {
|
|
||||||
const currentIntervalTimestamp =
|
|
||||||
timestamp ?? this.getCacheBucketStartTimestamp(Date.now());
|
|
||||||
|
|
||||||
return `${key}:${currentIntervalTimestamp}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private getLastCacheBucketStartTimestampsFromDate(
|
|
||||||
cacheBucketsCount: number,
|
|
||||||
date: number = Date.now(),
|
|
||||||
): number[] {
|
|
||||||
const currentIntervalTimestamp = this.getCacheBucketStartTimestamp(date);
|
|
||||||
|
|
||||||
return Array.from(
|
|
||||||
{ length: cacheBucketsCount },
|
|
||||||
(_, i) => currentIntervalTimestamp - i * CACHE_BUCKET_DURATION_MS,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateMessageOrCalendarChannelSyncJobByStatusCache(
|
|
||||||
key: HealthCounterCacheKeys,
|
|
||||||
status: MessageChannelSyncStatus | CalendarChannelSyncStatus,
|
|
||||||
messageChannelIds: string[],
|
|
||||||
) {
|
|
||||||
return await this.cacheStorage.setAdd(
|
|
||||||
this.getCacheKeyWithTimestamp(`${key}:${status}`),
|
|
||||||
messageChannelIds,
|
|
||||||
this.healthCacheTtl,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async countChannelSyncJobByStatus(
|
|
||||||
key:
|
|
||||||
| HealthCounterCacheKeys.MessageChannelSyncJobByStatus
|
|
||||||
| HealthCounterCacheKeys.CalendarEventSyncJobByStatus,
|
|
||||||
timeWindowInSeconds: number = this.healthMetricsTimeWindowInMinutes * 60,
|
|
||||||
): Promise<AccountSyncJobByStatusCounter> {
|
|
||||||
if ((timeWindowInSeconds * 1000) % CACHE_BUCKET_DURATION_MS !== 0) {
|
|
||||||
throw new Error(
|
|
||||||
`Time window must be divisible by ${CACHE_BUCKET_DURATION_MS}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const now = Date.now();
|
|
||||||
const countByStatus = {} as AccountSyncJobByStatusCounter;
|
|
||||||
const statuses =
|
|
||||||
key === HealthCounterCacheKeys.MessageChannelSyncJobByStatus
|
|
||||||
? Object.values(MessageChannelSyncStatus).filter(
|
|
||||||
(status) => status !== MessageChannelSyncStatus.ONGOING,
|
|
||||||
)
|
|
||||||
: Object.values(CalendarChannelSyncStatus).filter(
|
|
||||||
(status) => status !== CalendarChannelSyncStatus.ONGOING,
|
|
||||||
);
|
|
||||||
|
|
||||||
const cacheBuckets =
|
|
||||||
timeWindowInSeconds / (CACHE_BUCKET_DURATION_MS / 1000);
|
|
||||||
|
|
||||||
for (const status of statuses) {
|
|
||||||
const cacheKeys = this.computeTimeStampedCacheKeys(
|
|
||||||
`${key}:${status}`,
|
|
||||||
cacheBuckets,
|
|
||||||
now,
|
|
||||||
);
|
|
||||||
|
|
||||||
const channelIdsCount =
|
|
||||||
await this.cacheStorage.countAllSetMembers(cacheKeys);
|
|
||||||
|
|
||||||
countByStatus[status] = channelIdsCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
return countByStatus;
|
|
||||||
}
|
|
||||||
|
|
||||||
computeTimeStampedCacheKeys(
|
|
||||||
key: string,
|
|
||||||
cacheBucketsCount: number,
|
|
||||||
date: number = Date.now(),
|
|
||||||
) {
|
|
||||||
return this.getLastCacheBucketStartTimestampsFromDate(
|
|
||||||
cacheBucketsCount,
|
|
||||||
date,
|
|
||||||
).map((timestamp) => this.getCacheKeyWithTimestamp(key, timestamp));
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateInvalidCaptchaCache(captchaToken: string) {
|
|
||||||
return await this.cacheStorage.setAdd(
|
|
||||||
this.getCacheKeyWithTimestamp(HealthCounterCacheKeys.InvalidCaptcha),
|
|
||||||
[captchaToken],
|
|
||||||
this.healthCacheTtl,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getInvalidCaptchaCounter(
|
|
||||||
timeWindowInSeconds: number = this.healthMetricsTimeWindowInMinutes * 60,
|
|
||||||
) {
|
|
||||||
return await this.cacheStorage.countAllSetMembers(
|
|
||||||
this.computeTimeStampedCacheKeys(
|
|
||||||
HealthCounterCacheKeys.InvalidCaptcha,
|
|
||||||
timeWindowInSeconds / (CACHE_BUCKET_DURATION_MS / 1000),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -3,14 +3,12 @@ import { TerminusModule } from '@nestjs/terminus';
|
|||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
|
||||||
import { HealthController } from 'src/engine/core-modules/health/controllers/health.controller';
|
import { HealthController } from 'src/engine/core-modules/health/controllers/health.controller';
|
||||||
import { MetricsController } from 'src/engine/core-modules/health/controllers/metrics.controller';
|
|
||||||
import { AppHealthIndicator } from 'src/engine/core-modules/health/indicators/app.health';
|
import { AppHealthIndicator } from 'src/engine/core-modules/health/indicators/app.health';
|
||||||
|
import { MetricsModule } from 'src/engine/core-modules/metrics/metrics.module';
|
||||||
import { RedisClientModule } from 'src/engine/core-modules/redis-client/redis-client.module';
|
import { RedisClientModule } from 'src/engine/core-modules/redis-client/redis-client.module';
|
||||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||||
import { WorkspaceMigrationModule } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.module';
|
import { WorkspaceMigrationModule } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.module';
|
||||||
|
|
||||||
import { HealthCacheService } from './health-cache.service';
|
|
||||||
|
|
||||||
import { ConnectedAccountHealth } from './indicators/connected-account.health';
|
import { ConnectedAccountHealth } from './indicators/connected-account.health';
|
||||||
import { DatabaseHealthIndicator } from './indicators/database.health';
|
import { DatabaseHealthIndicator } from './indicators/database.health';
|
||||||
import { RedisHealthIndicator } from './indicators/redis.health';
|
import { RedisHealthIndicator } from './indicators/redis.health';
|
||||||
@ -21,10 +19,10 @@ import { WorkerHealthIndicator } from './indicators/worker.health';
|
|||||||
RedisClientModule,
|
RedisClientModule,
|
||||||
WorkspaceMigrationModule,
|
WorkspaceMigrationModule,
|
||||||
TypeOrmModule.forFeature([Workspace], 'core'),
|
TypeOrmModule.forFeature([Workspace], 'core'),
|
||||||
|
MetricsModule,
|
||||||
],
|
],
|
||||||
controllers: [HealthController, MetricsController],
|
controllers: [HealthController],
|
||||||
providers: [
|
providers: [
|
||||||
HealthCacheService,
|
|
||||||
DatabaseHealthIndicator,
|
DatabaseHealthIndicator,
|
||||||
RedisHealthIndicator,
|
RedisHealthIndicator,
|
||||||
WorkerHealthIndicator,
|
WorkerHealthIndicator,
|
||||||
@ -32,7 +30,6 @@ import { WorkerHealthIndicator } from './indicators/worker.health';
|
|||||||
AppHealthIndicator,
|
AppHealthIndicator,
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
HealthCacheService,
|
|
||||||
DatabaseHealthIndicator,
|
DatabaseHealthIndicator,
|
||||||
RedisHealthIndicator,
|
RedisHealthIndicator,
|
||||||
WorkerHealthIndicator,
|
WorkerHealthIndicator,
|
||||||
|
|||||||
@ -4,19 +4,19 @@ import { Test, TestingModule } from '@nestjs/testing';
|
|||||||
import { HEALTH_ERROR_MESSAGES } from 'src/engine/core-modules/health/constants/health-error-messages.constants';
|
import { HEALTH_ERROR_MESSAGES } from 'src/engine/core-modules/health/constants/health-error-messages.constants';
|
||||||
import { HEALTH_INDICATORS_TIMEOUT } from 'src/engine/core-modules/health/constants/health-indicators-timeout.conts';
|
import { HEALTH_INDICATORS_TIMEOUT } from 'src/engine/core-modules/health/constants/health-indicators-timeout.conts';
|
||||||
import { METRICS_FAILURE_RATE_THRESHOLD } from 'src/engine/core-modules/health/constants/metrics-failure-rate-threshold.const';
|
import { METRICS_FAILURE_RATE_THRESHOLD } from 'src/engine/core-modules/health/constants/metrics-failure-rate-threshold.const';
|
||||||
import { HealthCacheService } from 'src/engine/core-modules/health/health-cache.service';
|
|
||||||
import { ConnectedAccountHealth } from 'src/engine/core-modules/health/indicators/connected-account.health';
|
import { ConnectedAccountHealth } from 'src/engine/core-modules/health/indicators/connected-account.health';
|
||||||
|
import { MetricsService } from 'src/engine/core-modules/metrics/metrics.service';
|
||||||
import { CalendarChannelSyncStatus } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
|
import { CalendarChannelSyncStatus } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
|
||||||
import { MessageChannelSyncStatus } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
|
import { MessageChannelSyncStatus } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
|
||||||
|
|
||||||
describe('ConnectedAccountHealth', () => {
|
describe('ConnectedAccountHealth', () => {
|
||||||
let service: ConnectedAccountHealth;
|
let service: ConnectedAccountHealth;
|
||||||
let healthCacheService: jest.Mocked<HealthCacheService>;
|
let metricsService: jest.Mocked<MetricsService>;
|
||||||
let healthIndicatorService: jest.Mocked<HealthIndicatorService>;
|
let healthIndicatorService: jest.Mocked<HealthIndicatorService>;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
healthCacheService = {
|
metricsService = {
|
||||||
countChannelSyncJobByStatus: jest.fn(),
|
groupMetrics: jest.fn(),
|
||||||
} as any;
|
} as any;
|
||||||
|
|
||||||
healthIndicatorService = {
|
healthIndicatorService = {
|
||||||
@ -41,8 +41,8 @@ describe('ConnectedAccountHealth', () => {
|
|||||||
providers: [
|
providers: [
|
||||||
ConnectedAccountHealth,
|
ConnectedAccountHealth,
|
||||||
{
|
{
|
||||||
provide: HealthCacheService,
|
provide: MetricsService,
|
||||||
useValue: healthCacheService,
|
useValue: metricsService,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: HealthIndicatorService,
|
provide: HealthIndicatorService,
|
||||||
@ -64,7 +64,7 @@ describe('ConnectedAccountHealth', () => {
|
|||||||
|
|
||||||
describe('message sync health', () => {
|
describe('message sync health', () => {
|
||||||
it('should return up status when no message sync jobs are present', async () => {
|
it('should return up status when no message sync jobs are present', async () => {
|
||||||
healthCacheService.countChannelSyncJobByStatus
|
metricsService.groupMetrics
|
||||||
.mockResolvedValueOnce({
|
.mockResolvedValueOnce({
|
||||||
[MessageChannelSyncStatus.NOT_SYNCED]: 0,
|
[MessageChannelSyncStatus.NOT_SYNCED]: 0,
|
||||||
[MessageChannelSyncStatus.ACTIVE]: 0,
|
[MessageChannelSyncStatus.ACTIVE]: 0,
|
||||||
@ -92,7 +92,7 @@ describe('ConnectedAccountHealth', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it(`should return down status when message sync failure rate is above ${METRICS_FAILURE_RATE_THRESHOLD}%`, async () => {
|
it(`should return down status when message sync failure rate is above ${METRICS_FAILURE_RATE_THRESHOLD}%`, async () => {
|
||||||
healthCacheService.countChannelSyncJobByStatus
|
metricsService.groupMetrics
|
||||||
.mockResolvedValueOnce({
|
.mockResolvedValueOnce({
|
||||||
[MessageChannelSyncStatus.NOT_SYNCED]: 0,
|
[MessageChannelSyncStatus.NOT_SYNCED]: 0,
|
||||||
[MessageChannelSyncStatus.ACTIVE]: 1,
|
[MessageChannelSyncStatus.ACTIVE]: 1,
|
||||||
@ -122,7 +122,7 @@ describe('ConnectedAccountHealth', () => {
|
|||||||
|
|
||||||
describe('calendar sync health', () => {
|
describe('calendar sync health', () => {
|
||||||
it('should return up status when no calendar sync jobs are present', async () => {
|
it('should return up status when no calendar sync jobs are present', async () => {
|
||||||
healthCacheService.countChannelSyncJobByStatus
|
metricsService.groupMetrics
|
||||||
.mockResolvedValueOnce({
|
.mockResolvedValueOnce({
|
||||||
[MessageChannelSyncStatus.NOT_SYNCED]: 0,
|
[MessageChannelSyncStatus.NOT_SYNCED]: 0,
|
||||||
[MessageChannelSyncStatus.ACTIVE]: 0,
|
[MessageChannelSyncStatus.ACTIVE]: 0,
|
||||||
@ -150,7 +150,7 @@ describe('ConnectedAccountHealth', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it(`should return down status when calendar sync failure rate is above ${METRICS_FAILURE_RATE_THRESHOLD}%`, async () => {
|
it(`should return down status when calendar sync failure rate is above ${METRICS_FAILURE_RATE_THRESHOLD}%`, async () => {
|
||||||
healthCacheService.countChannelSyncJobByStatus
|
metricsService.groupMetrics
|
||||||
.mockResolvedValueOnce({
|
.mockResolvedValueOnce({
|
||||||
[MessageChannelSyncStatus.NOT_SYNCED]: 0,
|
[MessageChannelSyncStatus.NOT_SYNCED]: 0,
|
||||||
[MessageChannelSyncStatus.ACTIVE]: 1,
|
[MessageChannelSyncStatus.ACTIVE]: 1,
|
||||||
@ -180,7 +180,7 @@ describe('ConnectedAccountHealth', () => {
|
|||||||
|
|
||||||
describe('timeout handling', () => {
|
describe('timeout handling', () => {
|
||||||
it('should handle message sync timeout', async () => {
|
it('should handle message sync timeout', async () => {
|
||||||
healthCacheService.countChannelSyncJobByStatus
|
metricsService.groupMetrics
|
||||||
.mockResolvedValueOnce(
|
.mockResolvedValueOnce(
|
||||||
new Promise((resolve) =>
|
new Promise((resolve) =>
|
||||||
setTimeout(resolve, HEALTH_INDICATORS_TIMEOUT + 100),
|
setTimeout(resolve, HEALTH_INDICATORS_TIMEOUT + 100),
|
||||||
@ -207,7 +207,7 @@ describe('ConnectedAccountHealth', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should handle calendar sync timeout', async () => {
|
it('should handle calendar sync timeout', async () => {
|
||||||
healthCacheService.countChannelSyncJobByStatus
|
metricsService.groupMetrics
|
||||||
.mockResolvedValueOnce({
|
.mockResolvedValueOnce({
|
||||||
[MessageChannelSyncStatus.NOT_SYNCED]: 0,
|
[MessageChannelSyncStatus.NOT_SYNCED]: 0,
|
||||||
[MessageChannelSyncStatus.ACTIVE]: 1,
|
[MessageChannelSyncStatus.ACTIVE]: 1,
|
||||||
@ -236,7 +236,7 @@ describe('ConnectedAccountHealth', () => {
|
|||||||
|
|
||||||
describe('combined health check', () => {
|
describe('combined health check', () => {
|
||||||
it('should return combined status with both checks healthy', async () => {
|
it('should return combined status with both checks healthy', async () => {
|
||||||
healthCacheService.countChannelSyncJobByStatus
|
metricsService.groupMetrics
|
||||||
.mockResolvedValueOnce({
|
.mockResolvedValueOnce({
|
||||||
[MessageChannelSyncStatus.NOT_SYNCED]: 0,
|
[MessageChannelSyncStatus.NOT_SYNCED]: 0,
|
||||||
[MessageChannelSyncStatus.ACTIVE]: 8,
|
[MessageChannelSyncStatus.ACTIVE]: 8,
|
||||||
@ -256,7 +256,7 @@ describe('ConnectedAccountHealth', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return down status when both syncs fail', async () => {
|
it('should return down status when both syncs fail', async () => {
|
||||||
healthCacheService.countChannelSyncJobByStatus
|
metricsService.groupMetrics
|
||||||
.mockResolvedValueOnce({
|
.mockResolvedValueOnce({
|
||||||
[MessageChannelSyncStatus.NOT_SYNCED]: 0,
|
[MessageChannelSyncStatus.NOT_SYNCED]: 0,
|
||||||
[MessageChannelSyncStatus.ACTIVE]: 1,
|
[MessageChannelSyncStatus.ACTIVE]: 1,
|
||||||
|
|||||||
@ -6,15 +6,17 @@ import {
|
|||||||
|
|
||||||
import { HEALTH_ERROR_MESSAGES } from 'src/engine/core-modules/health/constants/health-error-messages.constants';
|
import { HEALTH_ERROR_MESSAGES } from 'src/engine/core-modules/health/constants/health-error-messages.constants';
|
||||||
import { METRICS_FAILURE_RATE_THRESHOLD } from 'src/engine/core-modules/health/constants/metrics-failure-rate-threshold.const';
|
import { METRICS_FAILURE_RATE_THRESHOLD } from 'src/engine/core-modules/health/constants/metrics-failure-rate-threshold.const';
|
||||||
import { HealthCacheService } from 'src/engine/core-modules/health/health-cache.service';
|
|
||||||
import { HealthCounterCacheKeys } from 'src/engine/core-modules/health/types/health-counter-cache-keys.type';
|
|
||||||
import { withHealthCheckTimeout } from 'src/engine/core-modules/health/utils/health-check-timeout.util';
|
import { withHealthCheckTimeout } from 'src/engine/core-modules/health/utils/health-check-timeout.util';
|
||||||
|
import {
|
||||||
|
CALENDAR_SYNC_METRICS_BY_STATUS,
|
||||||
|
MESSAGE_SYNC_METRICS_BY_STATUS,
|
||||||
|
} from 'src/engine/core-modules/metrics/constants/account-sync-metrics-by-status.constant';
|
||||||
|
import { MetricsService } from 'src/engine/core-modules/metrics/metrics.service';
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ConnectedAccountHealth {
|
export class ConnectedAccountHealth {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly healthIndicatorService: HealthIndicatorService,
|
private readonly healthIndicatorService: HealthIndicatorService,
|
||||||
private readonly healthCacheService: HealthCacheService,
|
private readonly metricsService: MetricsService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
private async checkMessageSyncHealth(): Promise<HealthIndicatorResult> {
|
private async checkMessageSyncHealth(): Promise<HealthIndicatorResult> {
|
||||||
@ -22,9 +24,7 @@ export class ConnectedAccountHealth {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const counters = await withHealthCheckTimeout(
|
const counters = await withHealthCheckTimeout(
|
||||||
this.healthCacheService.countChannelSyncJobByStatus(
|
this.metricsService.groupMetrics(MESSAGE_SYNC_METRICS_BY_STATUS),
|
||||||
HealthCounterCacheKeys.MessageChannelSyncJobByStatus,
|
|
||||||
),
|
|
||||||
HEALTH_ERROR_MESSAGES.MESSAGE_SYNC_TIMEOUT,
|
HEALTH_ERROR_MESSAGES.MESSAGE_SYNC_TIMEOUT,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -73,9 +73,7 @@ export class ConnectedAccountHealth {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const counters = await withHealthCheckTimeout(
|
const counters = await withHealthCheckTimeout(
|
||||||
this.healthCacheService.countChannelSyncJobByStatus(
|
this.metricsService.groupMetrics(CALENDAR_SYNC_METRICS_BY_STATUS),
|
||||||
HealthCounterCacheKeys.CalendarEventSyncJobByStatus,
|
|
||||||
),
|
|
||||||
HEALTH_ERROR_MESSAGES.CALENDAR_SYNC_TIMEOUT,
|
HEALTH_ERROR_MESSAGES.CALENDAR_SYNC_TIMEOUT,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +0,0 @@
|
|||||||
export enum HealthCounterCacheKeys {
|
|
||||||
MessageChannelSyncJobByStatus = 'message-channel-sync-job-by-status',
|
|
||||||
InvalidCaptcha = 'invalid-captcha',
|
|
||||||
CalendarEventSyncJobByStatus = 'calendar-event-sync-job-by-status',
|
|
||||||
}
|
|
||||||
@ -0,0 +1,31 @@
|
|||||||
|
import { MetricsKeys } from 'src/engine/core-modules/metrics/types/metrics-keys.type';
|
||||||
|
|
||||||
|
export const MESSAGE_SYNC_METRICS_BY_STATUS = [
|
||||||
|
{
|
||||||
|
name: 'ACTIVE',
|
||||||
|
cacheKey: MetricsKeys.MessageChannelSyncJobActive,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'FAILED_UNKNOWN',
|
||||||
|
cacheKey: MetricsKeys.MessageChannelSyncJobFailedUnknown,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'FAILED_INSUFFICIENT_PERMISSIONS',
|
||||||
|
cacheKey: MetricsKeys.MessageChannelSyncJobFailedInsufficientPermissions,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const CALENDAR_SYNC_METRICS_BY_STATUS = [
|
||||||
|
{
|
||||||
|
name: 'ACTIVE',
|
||||||
|
cacheKey: MetricsKeys.CalendarEventSyncJobActive,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'FAILED_UNKNOWN',
|
||||||
|
cacheKey: MetricsKeys.CalendarEventSyncJobFailedUnknown,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'FAILED_INSUFFICIENT_PERMISSIONS',
|
||||||
|
cacheKey: MetricsKeys.CalendarEventSyncJobFailedInsufficientPermissions,
|
||||||
|
},
|
||||||
|
];
|
||||||
@ -0,0 +1,94 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { InjectCacheStorage } from 'src/engine/core-modules/cache-storage/decorators/cache-storage.decorator';
|
||||||
|
import { CacheStorageService } from 'src/engine/core-modules/cache-storage/services/cache-storage.service';
|
||||||
|
import { CacheStorageNamespace } from 'src/engine/core-modules/cache-storage/types/cache-storage-namespace.enum';
|
||||||
|
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||||
|
import { MetricsKeys } from 'src/engine/core-modules/metrics/types/metrics-keys.type';
|
||||||
|
|
||||||
|
const CACHE_BUCKET_DURATION_MS = 15000; // 15 seconds window for each cache bucket
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class MetricsCacheService {
|
||||||
|
private readonly healthMetricsTimeWindowInMinutes: number;
|
||||||
|
private readonly healthCacheTtl: number;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@InjectCacheStorage(CacheStorageNamespace.EngineHealth)
|
||||||
|
private readonly cacheStorage: CacheStorageService,
|
||||||
|
private readonly environmentService: EnvironmentService,
|
||||||
|
) {
|
||||||
|
this.healthMetricsTimeWindowInMinutes = this.environmentService.get(
|
||||||
|
'HEALTH_METRICS_TIME_WINDOW_IN_MINUTES',
|
||||||
|
);
|
||||||
|
this.healthCacheTtl = this.healthMetricsTimeWindowInMinutes * 60000 * 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getCacheBucketStartTimestamp(timestamp: number): number {
|
||||||
|
return (
|
||||||
|
Math.floor(timestamp / CACHE_BUCKET_DURATION_MS) *
|
||||||
|
CACHE_BUCKET_DURATION_MS
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getCacheKeyWithTimestamp(key: string, timestamp?: number): string {
|
||||||
|
const currentIntervalTimestamp =
|
||||||
|
timestamp ?? this.getCacheBucketStartTimestamp(Date.now());
|
||||||
|
|
||||||
|
return `${key}:${currentIntervalTimestamp}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getLastCacheBucketStartTimestampsFromDate(
|
||||||
|
cacheBucketsCount: number,
|
||||||
|
date: number,
|
||||||
|
): number[] {
|
||||||
|
const currentIntervalTimestamp = this.getCacheBucketStartTimestamp(date);
|
||||||
|
|
||||||
|
return Array.from(
|
||||||
|
{ length: cacheBucketsCount },
|
||||||
|
(_, i) => currentIntervalTimestamp - i * CACHE_BUCKET_DURATION_MS,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateCounter(key: MetricsKeys, items: string[]) {
|
||||||
|
return await this.cacheStorage.setAdd(
|
||||||
|
this.getCacheKeyWithTimestamp(key),
|
||||||
|
items,
|
||||||
|
this.healthCacheTtl,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async computeCount({
|
||||||
|
key,
|
||||||
|
timeWindowInSeconds = this.healthMetricsTimeWindowInMinutes * 60,
|
||||||
|
date = Date.now(),
|
||||||
|
}: {
|
||||||
|
key: MetricsKeys;
|
||||||
|
timeWindowInSeconds?: number;
|
||||||
|
date?: number;
|
||||||
|
}): Promise<number> {
|
||||||
|
if ((timeWindowInSeconds * 1000) % CACHE_BUCKET_DURATION_MS !== 0) {
|
||||||
|
throw new Error(
|
||||||
|
`Time window must be divisible by ${CACHE_BUCKET_DURATION_MS}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const cacheBuckets =
|
||||||
|
timeWindowInSeconds / (CACHE_BUCKET_DURATION_MS / 1000);
|
||||||
|
|
||||||
|
const cacheKeys = this.computeTimeStampedCacheKeys(key, cacheBuckets, date);
|
||||||
|
|
||||||
|
return await this.cacheStorage.countAllSetMembers(cacheKeys);
|
||||||
|
}
|
||||||
|
|
||||||
|
computeTimeStampedCacheKeys(
|
||||||
|
key: string,
|
||||||
|
cacheBucketsCount: number,
|
||||||
|
date: number,
|
||||||
|
) {
|
||||||
|
return this.getLastCacheBucketStartTimestampsFromDate(
|
||||||
|
cacheBucketsCount,
|
||||||
|
date,
|
||||||
|
).map((timestamp) => this.getCacheKeyWithTimestamp(key, timestamp));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { MetricsCacheService } from 'src/engine/core-modules/metrics/metrics-cache.service';
|
||||||
|
import { MetricsService } from 'src/engine/core-modules/metrics/metrics.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
providers: [MetricsService, MetricsCacheService],
|
||||||
|
exports: [MetricsService, MetricsCacheService],
|
||||||
|
})
|
||||||
|
export class MetricsModule {}
|
||||||
@ -0,0 +1,70 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { metrics } from '@opentelemetry/api';
|
||||||
|
|
||||||
|
import { MetricsCacheService } from 'src/engine/core-modules/metrics/metrics-cache.service';
|
||||||
|
import { MetricsKeys } from 'src/engine/core-modules/metrics/types/metrics-keys.type';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class MetricsService {
|
||||||
|
constructor(private readonly metricsCacheService: MetricsCacheService) {}
|
||||||
|
|
||||||
|
async incrementCounter({
|
||||||
|
key,
|
||||||
|
eventId,
|
||||||
|
shouldStoreInCache = true,
|
||||||
|
}: {
|
||||||
|
key: MetricsKeys;
|
||||||
|
eventId: string;
|
||||||
|
shouldStoreInCache?: boolean;
|
||||||
|
}) {
|
||||||
|
//TODO : Define meter name usage in monitoring
|
||||||
|
const meter = metrics.getMeter('twenty-server');
|
||||||
|
const counter = meter.createCounter(key);
|
||||||
|
|
||||||
|
counter.add(1);
|
||||||
|
|
||||||
|
if (shouldStoreInCache) {
|
||||||
|
this.metricsCacheService.updateCounter(key, [eventId]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async batchIncrementCounter({
|
||||||
|
key,
|
||||||
|
eventIds,
|
||||||
|
shouldStoreInCache = true,
|
||||||
|
}: {
|
||||||
|
key: MetricsKeys;
|
||||||
|
eventIds: string[];
|
||||||
|
shouldStoreInCache?: boolean;
|
||||||
|
}) {
|
||||||
|
//TODO : Define meter name usage in monitoring
|
||||||
|
const meter = metrics.getMeter('twenty-server');
|
||||||
|
const counter = meter.createCounter(key);
|
||||||
|
|
||||||
|
counter.add(eventIds.length);
|
||||||
|
|
||||||
|
if (shouldStoreInCache) {
|
||||||
|
this.metricsCacheService.updateCounter(key, eventIds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async groupMetrics(
|
||||||
|
metrics: { name: string; cacheKey: MetricsKeys }[],
|
||||||
|
): Promise<Record<string, number>> {
|
||||||
|
const groupedMetrics: Record<string, number> = {};
|
||||||
|
|
||||||
|
const date = Date.now();
|
||||||
|
|
||||||
|
for (const metric of metrics) {
|
||||||
|
const metricValue = await this.metricsCacheService.computeCount({
|
||||||
|
key: metric.cacheKey,
|
||||||
|
date,
|
||||||
|
});
|
||||||
|
|
||||||
|
groupedMetrics[metric.name] = metricValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return groupedMetrics;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,4 @@
|
|||||||
|
export enum MeterDriver {
|
||||||
|
OpenTelemetry = 'opentelemetry',
|
||||||
|
Console = 'console',
|
||||||
|
}
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
export enum MetricsKeys {
|
||||||
|
MessageChannelSyncJobActive = 'message-channel-sync-job/active',
|
||||||
|
MessageChannelSyncJobFailedInsufficientPermissions = 'message-channel-sync-job/failed-insufficient-permissions',
|
||||||
|
MessageChannelSyncJobFailedUnknown = 'message-channel-sync-job/failed-unknown',
|
||||||
|
CalendarEventSyncJobActive = 'calendar-event-sync-job/active',
|
||||||
|
CalendarEventSyncJobFailedInsufficientPermissions = 'calendar-event-sync-job/failed-insufficient-permissions',
|
||||||
|
CalendarEventSyncJobFailedUnknown = 'calendar-event-sync-job/failed-unknown',
|
||||||
|
InvalidCaptcha = 'invalid-captcha',
|
||||||
|
}
|
||||||
@ -1,10 +1,27 @@
|
|||||||
import * as Sentry from '@sentry/nestjs';
|
import process from 'process';
|
||||||
|
|
||||||
|
import opentelemetry from '@opentelemetry/api';
|
||||||
|
import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-http';
|
||||||
|
import {
|
||||||
|
ConsoleMetricExporter,
|
||||||
|
MeterProvider,
|
||||||
|
PeriodicExportingMetricReader,
|
||||||
|
} from '@opentelemetry/sdk-metrics';
|
||||||
|
import * as Sentry from '@sentry/node';
|
||||||
import { nodeProfilingIntegration } from '@sentry/profiling-node';
|
import { nodeProfilingIntegration } from '@sentry/profiling-node';
|
||||||
|
|
||||||
import { NodeEnvironment } from 'src/engine/core-modules/environment/interfaces/node-environment.interface';
|
import { NodeEnvironment } from 'src/engine/core-modules/environment/interfaces/node-environment.interface';
|
||||||
|
|
||||||
import { ExceptionHandlerDriver } from 'src/engine/core-modules/exception-handler/interfaces';
|
import { ExceptionHandlerDriver } from 'src/engine/core-modules/exception-handler/interfaces';
|
||||||
|
import { MeterDriver } from 'src/engine/core-modules/metrics/types/meter-driver.type';
|
||||||
import { WorkspaceCacheKeys } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
|
import { WorkspaceCacheKeys } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
|
||||||
|
import { parseArrayEnvVar } from 'src/utils/parse-array-env-var';
|
||||||
|
|
||||||
|
const meterDrivers = parseArrayEnvVar(
|
||||||
|
process.env.METER_DRIVER,
|
||||||
|
Object.values(MeterDriver),
|
||||||
|
[MeterDriver.Console],
|
||||||
|
);
|
||||||
|
|
||||||
if (process.env.EXCEPTION_HANDLER_DRIVER === ExceptionHandlerDriver.Sentry) {
|
if (process.env.EXCEPTION_HANDLER_DRIVER === ExceptionHandlerDriver.Sentry) {
|
||||||
Sentry.init({
|
Sentry.init({
|
||||||
@ -29,3 +46,34 @@ if (process.env.EXCEPTION_HANDLER_DRIVER === ExceptionHandlerDriver.Sentry) {
|
|||||||
debug: process.env.NODE_ENV === NodeEnvironment.development,
|
debug: process.env.NODE_ENV === NodeEnvironment.development,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Meter setup
|
||||||
|
|
||||||
|
const meterProvider = new MeterProvider({
|
||||||
|
readers: [
|
||||||
|
...(meterDrivers.includes(MeterDriver.Console)
|
||||||
|
? [
|
||||||
|
new PeriodicExportingMetricReader({
|
||||||
|
exporter: new ConsoleMetricExporter(),
|
||||||
|
exportIntervalMillis: 10000,
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
...(meterDrivers.includes(MeterDriver.OpenTelemetry)
|
||||||
|
? [
|
||||||
|
new PeriodicExportingMetricReader({
|
||||||
|
exporter: new OTLPMetricExporter({
|
||||||
|
url: process.env.OTLP_COLLECTOR_METRICS_ENDPOINT_URL,
|
||||||
|
}),
|
||||||
|
exportIntervalMillis: 10000,
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
opentelemetry.metrics.setGlobalMeterProvider(meterProvider);
|
||||||
|
|
||||||
|
process.on('SIGTERM', async () => {
|
||||||
|
await meterProvider.shutdown();
|
||||||
|
});
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
|||||||
|
|
||||||
import { BillingModule } from 'src/engine/core-modules/billing/billing.module';
|
import { BillingModule } from 'src/engine/core-modules/billing/billing.module';
|
||||||
import { FeatureFlag } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
import { FeatureFlag } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
||||||
import { HealthModule } from 'src/engine/core-modules/health/health.module';
|
import { MetricsModule } from 'src/engine/core-modules/metrics/metrics.module';
|
||||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||||
import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity';
|
import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity';
|
||||||
import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module';
|
import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module';
|
||||||
@ -50,7 +50,7 @@ import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/sta
|
|||||||
RefreshTokensManagerModule,
|
RefreshTokensManagerModule,
|
||||||
ConnectedAccountModule,
|
ConnectedAccountModule,
|
||||||
CalendarCommonModule,
|
CalendarCommonModule,
|
||||||
HealthModule,
|
MetricsModule,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
CalendarChannelSyncStatusService,
|
CalendarChannelSyncStatusService,
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { Module } from '@nestjs/common';
|
|||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
|
||||||
import { FeatureFlag } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
import { FeatureFlag } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
||||||
import { HealthModule } from 'src/engine/core-modules/health/health.module';
|
import { MetricsModule } from 'src/engine/core-modules/metrics/metrics.module';
|
||||||
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
|
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
|
||||||
import { CalendarChannelSyncStatusService } from 'src/modules/calendar/common/services/calendar-channel-sync-status.service';
|
import { CalendarChannelSyncStatusService } from 'src/modules/calendar/common/services/calendar-channel-sync-status.service';
|
||||||
import { ConnectedAccountModule } from 'src/modules/connected-account/connected-account.module';
|
import { ConnectedAccountModule } from 'src/modules/connected-account/connected-account.module';
|
||||||
@ -12,7 +12,7 @@ import { ConnectedAccountModule } from 'src/modules/connected-account/connected-
|
|||||||
WorkspaceDataSourceModule,
|
WorkspaceDataSourceModule,
|
||||||
TypeOrmModule.forFeature([FeatureFlag], 'core'),
|
TypeOrmModule.forFeature([FeatureFlag], 'core'),
|
||||||
ConnectedAccountModule,
|
ConnectedAccountModule,
|
||||||
HealthModule,
|
MetricsModule,
|
||||||
],
|
],
|
||||||
providers: [CalendarChannelSyncStatusService],
|
providers: [CalendarChannelSyncStatusService],
|
||||||
exports: [CalendarChannelSyncStatusService],
|
exports: [CalendarChannelSyncStatusService],
|
||||||
|
|||||||
@ -5,8 +5,8 @@ import { Any } from 'typeorm';
|
|||||||
import { InjectCacheStorage } from 'src/engine/core-modules/cache-storage/decorators/cache-storage.decorator';
|
import { InjectCacheStorage } from 'src/engine/core-modules/cache-storage/decorators/cache-storage.decorator';
|
||||||
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 { 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 { HealthCacheService } from 'src/engine/core-modules/health/health-cache.service';
|
import { MetricsService } from 'src/engine/core-modules/metrics/metrics.service';
|
||||||
import { HealthCounterCacheKeys } from 'src/engine/core-modules/health/types/health-counter-cache-keys.type';
|
import { MetricsKeys } from 'src/engine/core-modules/metrics/types/metrics-keys.type';
|
||||||
import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
|
import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
|
||||||
import {
|
import {
|
||||||
CalendarChannelSyncStage,
|
CalendarChannelSyncStage,
|
||||||
@ -24,7 +24,7 @@ export class CalendarChannelSyncStatusService {
|
|||||||
@InjectCacheStorage(CacheStorageNamespace.ModuleCalendar)
|
@InjectCacheStorage(CacheStorageNamespace.ModuleCalendar)
|
||||||
private readonly cacheStorage: CacheStorageService,
|
private readonly cacheStorage: CacheStorageService,
|
||||||
private readonly accountsToReconnectService: AccountsToReconnectService,
|
private readonly accountsToReconnectService: AccountsToReconnectService,
|
||||||
private readonly healthCacheService: HealthCacheService,
|
private readonly metricsService: MetricsService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public async scheduleFullCalendarEventListFetch(
|
public async scheduleFullCalendarEventListFetch(
|
||||||
@ -179,11 +179,10 @@ export class CalendarChannelSyncStatusService {
|
|||||||
|
|
||||||
await this.schedulePartialCalendarEventListFetch(calendarChannelIds);
|
await this.schedulePartialCalendarEventListFetch(calendarChannelIds);
|
||||||
|
|
||||||
await this.healthCacheService.updateMessageOrCalendarChannelSyncJobByStatusCache(
|
await this.metricsService.batchIncrementCounter({
|
||||||
HealthCounterCacheKeys.CalendarEventSyncJobByStatus,
|
key: MetricsKeys.CalendarEventSyncJobActive,
|
||||||
CalendarChannelSyncStatus.ACTIVE,
|
eventIds: calendarChannelIds,
|
||||||
calendarChannelIds,
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async markAsFailedUnknownAndFlushCalendarEventsToImport(
|
public async markAsFailedUnknownAndFlushCalendarEventsToImport(
|
||||||
@ -210,11 +209,10 @@ export class CalendarChannelSyncStatusService {
|
|||||||
syncStage: CalendarChannelSyncStage.FAILED,
|
syncStage: CalendarChannelSyncStage.FAILED,
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.healthCacheService.updateMessageOrCalendarChannelSyncJobByStatusCache(
|
await this.metricsService.batchIncrementCounter({
|
||||||
HealthCounterCacheKeys.CalendarEventSyncJobByStatus,
|
key: MetricsKeys.CalendarEventSyncJobFailedUnknown,
|
||||||
CalendarChannelSyncStatus.FAILED_UNKNOWN,
|
eventIds: calendarChannelIds,
|
||||||
calendarChannelIds,
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async markAsFailedInsufficientPermissionsAndFlushCalendarEventsToImport(
|
public async markAsFailedInsufficientPermissionsAndFlushCalendarEventsToImport(
|
||||||
@ -266,11 +264,10 @@ export class CalendarChannelSyncStatusService {
|
|||||||
workspaceId,
|
workspaceId,
|
||||||
);
|
);
|
||||||
|
|
||||||
await this.healthCacheService.updateMessageOrCalendarChannelSyncJobByStatusCache(
|
await this.metricsService.batchIncrementCounter({
|
||||||
HealthCounterCacheKeys.CalendarEventSyncJobByStatus,
|
key: MetricsKeys.CalendarEventSyncJobFailedInsufficientPermissions,
|
||||||
CalendarChannelSyncStatus.FAILED_INSUFFICIENT_PERMISSIONS,
|
eventIds: calendarChannelIds,
|
||||||
calendarChannelIds,
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async addToAccountsToReconnect(
|
private async addToAccountsToReconnect(
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { Module } from '@nestjs/common';
|
|||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
|
||||||
import { FeatureFlag } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
import { FeatureFlag } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
||||||
import { HealthModule } from 'src/engine/core-modules/health/health.module';
|
import { MetricsModule } from 'src/engine/core-modules/metrics/metrics.module';
|
||||||
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
|
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
|
||||||
import { ConnectedAccountModule } from 'src/modules/connected-account/connected-account.module';
|
import { ConnectedAccountModule } from 'src/modules/connected-account/connected-account.module';
|
||||||
import { MessageChannelSyncStatusService } from 'src/modules/messaging/common/services/message-channel-sync-status.service';
|
import { MessageChannelSyncStatusService } from 'src/modules/messaging/common/services/message-channel-sync-status.service';
|
||||||
@ -12,7 +12,7 @@ import { MessageChannelSyncStatusService } from 'src/modules/messaging/common/se
|
|||||||
WorkspaceDataSourceModule,
|
WorkspaceDataSourceModule,
|
||||||
TypeOrmModule.forFeature([FeatureFlag], 'core'),
|
TypeOrmModule.forFeature([FeatureFlag], 'core'),
|
||||||
ConnectedAccountModule,
|
ConnectedAccountModule,
|
||||||
HealthModule,
|
MetricsModule,
|
||||||
],
|
],
|
||||||
providers: [MessageChannelSyncStatusService],
|
providers: [MessageChannelSyncStatusService],
|
||||||
exports: [MessageChannelSyncStatusService],
|
exports: [MessageChannelSyncStatusService],
|
||||||
|
|||||||
@ -5,8 +5,8 @@ import { Any } from 'typeorm';
|
|||||||
import { InjectCacheStorage } from 'src/engine/core-modules/cache-storage/decorators/cache-storage.decorator';
|
import { InjectCacheStorage } from 'src/engine/core-modules/cache-storage/decorators/cache-storage.decorator';
|
||||||
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 { 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 { HealthCacheService } from 'src/engine/core-modules/health/health-cache.service';
|
import { MetricsService } from 'src/engine/core-modules/metrics/metrics.service';
|
||||||
import { HealthCounterCacheKeys } from 'src/engine/core-modules/health/types/health-counter-cache-keys.type';
|
import { MetricsKeys } from 'src/engine/core-modules/metrics/types/metrics-keys.type';
|
||||||
import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
|
import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
|
||||||
import { AccountsToReconnectService } from 'src/modules/connected-account/services/accounts-to-reconnect.service';
|
import { AccountsToReconnectService } from 'src/modules/connected-account/services/accounts-to-reconnect.service';
|
||||||
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
|
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
|
||||||
@ -24,7 +24,7 @@ export class MessageChannelSyncStatusService {
|
|||||||
private readonly cacheStorage: CacheStorageService,
|
private readonly cacheStorage: CacheStorageService,
|
||||||
private readonly twentyORMManager: TwentyORMManager,
|
private readonly twentyORMManager: TwentyORMManager,
|
||||||
private readonly accountsToReconnectService: AccountsToReconnectService,
|
private readonly accountsToReconnectService: AccountsToReconnectService,
|
||||||
private readonly healthCacheService: HealthCacheService,
|
private readonly metricsService: MetricsService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public async scheduleFullMessageListFetch(messageChannelIds: string[]) {
|
public async scheduleFullMessageListFetch(messageChannelIds: string[]) {
|
||||||
@ -152,11 +152,10 @@ export class MessageChannelSyncStatusService {
|
|||||||
syncedAt: new Date().toISOString(),
|
syncedAt: new Date().toISOString(),
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.healthCacheService.updateMessageOrCalendarChannelSyncJobByStatusCache(
|
await this.metricsService.batchIncrementCounter({
|
||||||
HealthCounterCacheKeys.MessageChannelSyncJobByStatus,
|
key: MetricsKeys.MessageChannelSyncJobActive,
|
||||||
MessageChannelSyncStatus.ACTIVE,
|
eventIds: messageChannelIds,
|
||||||
messageChannelIds,
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async markAsMessagesImportOngoing(messageChannelIds: string[]) {
|
public async markAsMessagesImportOngoing(messageChannelIds: string[]) {
|
||||||
@ -199,11 +198,10 @@ export class MessageChannelSyncStatusService {
|
|||||||
syncStatus: MessageChannelSyncStatus.FAILED_UNKNOWN,
|
syncStatus: MessageChannelSyncStatus.FAILED_UNKNOWN,
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.healthCacheService.updateMessageOrCalendarChannelSyncJobByStatusCache(
|
await this.metricsService.batchIncrementCounter({
|
||||||
HealthCounterCacheKeys.MessageChannelSyncJobByStatus,
|
key: MetricsKeys.MessageChannelSyncJobFailedUnknown,
|
||||||
MessageChannelSyncStatus.FAILED_UNKNOWN,
|
eventIds: messageChannelIds,
|
||||||
messageChannelIds,
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async markAsFailedInsufficientPermissionsAndFlushMessagesToImport(
|
public async markAsFailedInsufficientPermissionsAndFlushMessagesToImport(
|
||||||
@ -230,11 +228,10 @@ export class MessageChannelSyncStatusService {
|
|||||||
syncStatus: MessageChannelSyncStatus.FAILED_INSUFFICIENT_PERMISSIONS,
|
syncStatus: MessageChannelSyncStatus.FAILED_INSUFFICIENT_PERMISSIONS,
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.healthCacheService.updateMessageOrCalendarChannelSyncJobByStatusCache(
|
await this.metricsService.batchIncrementCounter({
|
||||||
HealthCounterCacheKeys.MessageChannelSyncJobByStatus,
|
key: MetricsKeys.MessageChannelSyncJobFailedInsufficientPermissions,
|
||||||
MessageChannelSyncStatus.FAILED_INSUFFICIENT_PERMISSIONS,
|
eventIds: messageChannelIds,
|
||||||
messageChannelIds,
|
});
|
||||||
);
|
|
||||||
|
|
||||||
const connectedAccountRepository =
|
const connectedAccountRepository =
|
||||||
await this.twentyORMManager.getRepository<ConnectedAccountWorkspaceEntity>(
|
await this.twentyORMManager.getRepository<ConnectedAccountWorkspaceEntity>(
|
||||||
|
|||||||
@ -3,8 +3,8 @@ import { NestFactory } from '@nestjs/core';
|
|||||||
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 { LoggerService } from 'src/engine/core-modules/logger/logger.service';
|
import { LoggerService } from 'src/engine/core-modules/logger/logger.service';
|
||||||
import { shouldFilterException } from 'src/engine/utils/global-exception-handler.util';
|
import { shouldFilterException } from 'src/engine/utils/global-exception-handler.util';
|
||||||
import { QueueWorkerModule } from 'src/queue-worker/queue-worker.module';
|
|
||||||
import 'src/instrument';
|
import 'src/instrument';
|
||||||
|
import { QueueWorkerModule } from 'src/queue-worker/queue-worker.module';
|
||||||
|
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
let exceptionHandlerService: ExceptionHandlerService | undefined;
|
let exceptionHandlerService: ExceptionHandlerService | undefined;
|
||||||
|
|||||||
13
packages/twenty-server/src/utils/parse-array-env-var.ts
Normal file
13
packages/twenty-server/src/utils/parse-array-env-var.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
export const parseArrayEnvVar = <T>(
|
||||||
|
envVar: string | undefined,
|
||||||
|
expectedValues: T[],
|
||||||
|
defaultValues: T[],
|
||||||
|
): T[] => {
|
||||||
|
if (!envVar) return defaultValues;
|
||||||
|
|
||||||
|
const values = envVar
|
||||||
|
.split(',')
|
||||||
|
.filter((item) => expectedValues.includes(item as T)) as T[];
|
||||||
|
|
||||||
|
return values.length > 0 ? values : defaultValues;
|
||||||
|
};
|
||||||
@ -279,7 +279,7 @@ yarn command:prod cron:calendar:ongoing-stale
|
|||||||
['SERVERLESS_LAMBDA_SECRET_ACCESS_KEY', '', 'Optional depending on the authentication method'],
|
['SERVERLESS_LAMBDA_SECRET_ACCESS_KEY', '', 'Optional depending on the authentication method'],
|
||||||
]}></ArticleTable>
|
]}></ArticleTable>
|
||||||
|
|
||||||
### Logging
|
### Logging and Observability
|
||||||
|
|
||||||
<ArticleTable options={[
|
<ArticleTable options={[
|
||||||
['LOGGER_DRIVER', 'console', "Currently, only supports 'console'"],
|
['LOGGER_DRIVER', 'console', "Currently, only supports 'console'"],
|
||||||
@ -290,6 +290,8 @@ yarn command:prod cron:calendar:ongoing-stale
|
|||||||
['SENTRY_RELEASE', 'latest', 'The sentry release used if sentry logging driver is selected'],
|
['SENTRY_RELEASE', 'latest', 'The sentry release used if sentry logging driver is selected'],
|
||||||
['SENTRY_DSN', 'https://xxx@xxx.ingest.sentry.io/xxx', 'The sentry logging endpoint used if sentry logging driver is selected'],
|
['SENTRY_DSN', 'https://xxx@xxx.ingest.sentry.io/xxx', 'The sentry logging endpoint used if sentry logging driver is selected'],
|
||||||
['SENTRY_FRONT_DSN', 'https://xxx@xxx.ingest.sentry.io/xxx', 'The sentry logging endpoint used by the frontend if sentry logging driver is selected'],
|
['SENTRY_FRONT_DSN', 'https://xxx@xxx.ingest.sentry.io/xxx', 'The sentry logging endpoint used by the frontend if sentry logging driver is selected'],
|
||||||
|
['METER_DRIVER', 'console', "The meter driver can be: 'console' and/or 'opentelemetry' "],
|
||||||
|
['OTLP_COLLECTOR_ENDPOINT_URL', '', 'The OpenTelemetry collector endpoint collects metrics if opentelemetry meter driver is selected. The collector has to be set separately.'],
|
||||||
]}></ArticleTable>
|
]}></ArticleTable>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
162
yarn.lock
162
yarn.lock
@ -11885,6 +11885,15 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@opentelemetry/api-logs@npm:0.200.0":
|
||||||
|
version: 0.200.0
|
||||||
|
resolution: "@opentelemetry/api-logs@npm:0.200.0"
|
||||||
|
dependencies:
|
||||||
|
"@opentelemetry/api": "npm:^1.3.0"
|
||||||
|
checksum: 10c0/c6bc3cfba35c69411f294519d93d0ff9f603517030d1162839ee42ac22ed1b0235edaf71d00cabc40125f813d8b4dc830d14315afcebcef138c1df560eaa5c91
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@opentelemetry/api-logs@npm:0.52.1":
|
"@opentelemetry/api-logs@npm:0.52.1":
|
||||||
version: 0.52.1
|
version: 0.52.1
|
||||||
resolution: "@opentelemetry/api-logs@npm:0.52.1"
|
resolution: "@opentelemetry/api-logs@npm:0.52.1"
|
||||||
@ -11903,7 +11912,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@opentelemetry/api@npm:^1.0.0, @opentelemetry/api@npm:^1.8, @opentelemetry/api@npm:^1.9.0":
|
"@opentelemetry/api@npm:^1.0.0, @opentelemetry/api@npm:^1.3.0, @opentelemetry/api@npm:^1.8, @opentelemetry/api@npm:^1.9.0":
|
||||||
version: 1.9.0
|
version: 1.9.0
|
||||||
resolution: "@opentelemetry/api@npm:1.9.0"
|
resolution: "@opentelemetry/api@npm:1.9.0"
|
||||||
checksum: 10c0/9aae2fe6e8a3a3eeb6c1fdef78e1939cf05a0f37f8a4fae4d6bf2e09eb1e06f966ece85805626e01ba5fab48072b94f19b835449e58b6d26720ee19a58298add
|
checksum: 10c0/9aae2fe6e8a3a3eeb6c1fdef78e1939cf05a0f37f8a4fae4d6bf2e09eb1e06f966ece85805626e01ba5fab48072b94f19b835449e58b6d26720ee19a58298add
|
||||||
@ -11930,6 +11939,32 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@opentelemetry/core@npm:2.0.0":
|
||||||
|
version: 2.0.0
|
||||||
|
resolution: "@opentelemetry/core@npm:2.0.0"
|
||||||
|
dependencies:
|
||||||
|
"@opentelemetry/semantic-conventions": "npm:^1.29.0"
|
||||||
|
peerDependencies:
|
||||||
|
"@opentelemetry/api": ">=1.0.0 <1.10.0"
|
||||||
|
checksum: 10c0/d2cc6d8a955305b9de15cc36135e5d5b0f0405fead8bbd4de51433f2d05369af0a3bcb2c6fe7fe6d9e61b0db782511bcadc5d93ed906027d4c00d5c2e3575a24
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"@opentelemetry/exporter-metrics-otlp-http@npm:^0.200.0":
|
||||||
|
version: 0.200.0
|
||||||
|
resolution: "@opentelemetry/exporter-metrics-otlp-http@npm:0.200.0"
|
||||||
|
dependencies:
|
||||||
|
"@opentelemetry/core": "npm:2.0.0"
|
||||||
|
"@opentelemetry/otlp-exporter-base": "npm:0.200.0"
|
||||||
|
"@opentelemetry/otlp-transformer": "npm:0.200.0"
|
||||||
|
"@opentelemetry/resources": "npm:2.0.0"
|
||||||
|
"@opentelemetry/sdk-metrics": "npm:2.0.0"
|
||||||
|
peerDependencies:
|
||||||
|
"@opentelemetry/api": ^1.3.0
|
||||||
|
checksum: 10c0/b94e99a481d3a156ed28f23ea9a72b594613081892ef889fd31be686d32daaa299e961bf173cdc3dcc5235a59f70f6437dd4d21c0256d638c669999c7912c20f
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@opentelemetry/instrumentation-connect@npm:0.39.0":
|
"@opentelemetry/instrumentation-connect@npm:0.39.0":
|
||||||
version: 0.39.0
|
version: 0.39.0
|
||||||
resolution: "@opentelemetry/instrumentation-connect@npm:0.39.0"
|
resolution: "@opentelemetry/instrumentation-connect@npm:0.39.0"
|
||||||
@ -12205,6 +12240,35 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@opentelemetry/otlp-exporter-base@npm:0.200.0":
|
||||||
|
version: 0.200.0
|
||||||
|
resolution: "@opentelemetry/otlp-exporter-base@npm:0.200.0"
|
||||||
|
dependencies:
|
||||||
|
"@opentelemetry/core": "npm:2.0.0"
|
||||||
|
"@opentelemetry/otlp-transformer": "npm:0.200.0"
|
||||||
|
peerDependencies:
|
||||||
|
"@opentelemetry/api": ^1.3.0
|
||||||
|
checksum: 10c0/3283c12bffc3156a41d9c16c097966e8418781a1d779250334f3d5b4f864be1aeac69fecfdf489abc95578dc36098dc0e026e5a48eb19ee170d72ef89b94f0e9
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"@opentelemetry/otlp-transformer@npm:0.200.0":
|
||||||
|
version: 0.200.0
|
||||||
|
resolution: "@opentelemetry/otlp-transformer@npm:0.200.0"
|
||||||
|
dependencies:
|
||||||
|
"@opentelemetry/api-logs": "npm:0.200.0"
|
||||||
|
"@opentelemetry/core": "npm:2.0.0"
|
||||||
|
"@opentelemetry/resources": "npm:2.0.0"
|
||||||
|
"@opentelemetry/sdk-logs": "npm:0.200.0"
|
||||||
|
"@opentelemetry/sdk-metrics": "npm:2.0.0"
|
||||||
|
"@opentelemetry/sdk-trace-base": "npm:2.0.0"
|
||||||
|
protobufjs: "npm:^7.3.0"
|
||||||
|
peerDependencies:
|
||||||
|
"@opentelemetry/api": ^1.3.0
|
||||||
|
checksum: 10c0/4f5383fad48c62e17824df91f6944b0376cb17f7b132b11d62fa5cf46747f224c980960209c85669b6e341a131f94586c6ad52bc1a6d2fb8d5295e23b460600c
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@opentelemetry/redis-common@npm:^0.36.2":
|
"@opentelemetry/redis-common@npm:^0.36.2":
|
||||||
version: 0.36.2
|
version: 0.36.2
|
||||||
resolution: "@opentelemetry/redis-common@npm:0.36.2"
|
resolution: "@opentelemetry/redis-common@npm:0.36.2"
|
||||||
@ -12224,6 +12288,43 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@opentelemetry/resources@npm:2.0.0":
|
||||||
|
version: 2.0.0
|
||||||
|
resolution: "@opentelemetry/resources@npm:2.0.0"
|
||||||
|
dependencies:
|
||||||
|
"@opentelemetry/core": "npm:2.0.0"
|
||||||
|
"@opentelemetry/semantic-conventions": "npm:^1.29.0"
|
||||||
|
peerDependencies:
|
||||||
|
"@opentelemetry/api": ">=1.3.0 <1.10.0"
|
||||||
|
checksum: 10c0/2f331ff8268ef7168e8f24312fd7505900693c0ea302f6025937e94c157b8173ee54f5d5a737c06b956da721aa63443ac520f530cade880ef3cd40a2a25c702c
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"@opentelemetry/sdk-logs@npm:0.200.0":
|
||||||
|
version: 0.200.0
|
||||||
|
resolution: "@opentelemetry/sdk-logs@npm:0.200.0"
|
||||||
|
dependencies:
|
||||||
|
"@opentelemetry/api-logs": "npm:0.200.0"
|
||||||
|
"@opentelemetry/core": "npm:2.0.0"
|
||||||
|
"@opentelemetry/resources": "npm:2.0.0"
|
||||||
|
peerDependencies:
|
||||||
|
"@opentelemetry/api": ">=1.4.0 <1.10.0"
|
||||||
|
checksum: 10c0/031dc40dd012fad102e5c8c0c9bdbbce051dbc7fcc2e05e003f959aeb34d252dc3595b353ea2a9f900ff40f45d19cb4c8f7ab95a9faa01391f6b415c7780c786
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"@opentelemetry/sdk-metrics@npm:2.0.0, @opentelemetry/sdk-metrics@npm:^2.0.0":
|
||||||
|
version: 2.0.0
|
||||||
|
resolution: "@opentelemetry/sdk-metrics@npm:2.0.0"
|
||||||
|
dependencies:
|
||||||
|
"@opentelemetry/core": "npm:2.0.0"
|
||||||
|
"@opentelemetry/resources": "npm:2.0.0"
|
||||||
|
peerDependencies:
|
||||||
|
"@opentelemetry/api": ">=1.9.0 <1.10.0"
|
||||||
|
checksum: 10c0/9a3c87738671f29a496a39d65b3ab0829b52d0f31c0be662ea575a8f77bc5444044fd01513c891abdff6bf6344a08730e18f79253a85e68962669f3e1fa12e72
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@opentelemetry/sdk-metrics@npm:^1.9.1":
|
"@opentelemetry/sdk-metrics@npm:^1.9.1":
|
||||||
version: 1.26.0
|
version: 1.26.0
|
||||||
resolution: "@opentelemetry/sdk-metrics@npm:1.26.0"
|
resolution: "@opentelemetry/sdk-metrics@npm:1.26.0"
|
||||||
@ -12236,6 +12337,19 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@opentelemetry/sdk-trace-base@npm:2.0.0":
|
||||||
|
version: 2.0.0
|
||||||
|
resolution: "@opentelemetry/sdk-trace-base@npm:2.0.0"
|
||||||
|
dependencies:
|
||||||
|
"@opentelemetry/core": "npm:2.0.0"
|
||||||
|
"@opentelemetry/resources": "npm:2.0.0"
|
||||||
|
"@opentelemetry/semantic-conventions": "npm:^1.29.0"
|
||||||
|
peerDependencies:
|
||||||
|
"@opentelemetry/api": ">=1.3.0 <1.10.0"
|
||||||
|
checksum: 10c0/c63cc052741e4cc01d084c883e24a1c0792f081a242e14e5cf526d5a3d96bac5974006fa0d8f902bd04f34ed9ce95a0d0f01b7fdb37fcc813cea9f818f2b8f43
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@opentelemetry/sdk-trace-base@npm:^1.22, @opentelemetry/sdk-trace-base@npm:^1.26.0":
|
"@opentelemetry/sdk-trace-base@npm:^1.22, @opentelemetry/sdk-trace-base@npm:^1.26.0":
|
||||||
version: 1.26.0
|
version: 1.26.0
|
||||||
resolution: "@opentelemetry/sdk-trace-base@npm:1.26.0"
|
resolution: "@opentelemetry/sdk-trace-base@npm:1.26.0"
|
||||||
@ -12256,6 +12370,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@opentelemetry/semantic-conventions@npm:^1.29.0":
|
||||||
|
version: 1.30.0
|
||||||
|
resolution: "@opentelemetry/semantic-conventions@npm:1.30.0"
|
||||||
|
checksum: 10c0/0bf99552e3b4b7e8b7eb504b678d52f59c6f259df88e740a2011a0d858e523d36fee86047ae1b7f45849c77f00f970c3059ba58e0a06a7d47d6f01dbe8c455bd
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@opentelemetry/sql-common@npm:^0.40.1":
|
"@opentelemetry/sql-common@npm:^0.40.1":
|
||||||
version: 0.40.1
|
version: 0.40.1
|
||||||
resolution: "@opentelemetry/sql-common@npm:0.40.1"
|
resolution: "@opentelemetry/sql-common@npm:0.40.1"
|
||||||
@ -22204,6 +22325,15 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@types/node@npm:>=13.7.0":
|
||||||
|
version: 22.13.14
|
||||||
|
resolution: "@types/node@npm:22.13.14"
|
||||||
|
dependencies:
|
||||||
|
undici-types: "npm:~6.20.0"
|
||||||
|
checksum: 10c0/fa2ab5b8277bfbcc86c42e46a3ea9871b0d559894cc9d955685d17178c9499f0b1bf03d1d1ea8d92ef2dda818988f4035acb8abf9dc15423a998fa56173ab804
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@types/node@npm:^10.1.0":
|
"@types/node@npm:^10.1.0":
|
||||||
version: 10.17.60
|
version: 10.17.60
|
||||||
resolution: "@types/node@npm:10.17.60"
|
resolution: "@types/node@npm:10.17.60"
|
||||||
@ -40434,6 +40564,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"long@npm:^5.0.0":
|
||||||
|
version: 5.3.1
|
||||||
|
resolution: "long@npm:5.3.1"
|
||||||
|
checksum: 10c0/8726994c6359bb7162fb94563e14c3f9c0f0eeafd90ec654738f4f144a5705756d36a873c442f172ee2a4b51e08d14ab99765b49aa1fb994c5ba7fe12057bca2
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"longest-streak@npm:^2.0.0":
|
"longest-streak@npm:^2.0.0":
|
||||||
version: 2.0.4
|
version: 2.0.4
|
||||||
resolution: "longest-streak@npm:2.0.4"
|
resolution: "longest-streak@npm:2.0.4"
|
||||||
@ -47139,6 +47276,26 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"protobufjs@npm:^7.3.0":
|
||||||
|
version: 7.4.0
|
||||||
|
resolution: "protobufjs@npm:7.4.0"
|
||||||
|
dependencies:
|
||||||
|
"@protobufjs/aspromise": "npm:^1.1.2"
|
||||||
|
"@protobufjs/base64": "npm:^1.1.2"
|
||||||
|
"@protobufjs/codegen": "npm:^2.0.4"
|
||||||
|
"@protobufjs/eventemitter": "npm:^1.1.0"
|
||||||
|
"@protobufjs/fetch": "npm:^1.1.0"
|
||||||
|
"@protobufjs/float": "npm:^1.0.2"
|
||||||
|
"@protobufjs/inquire": "npm:^1.1.0"
|
||||||
|
"@protobufjs/path": "npm:^1.1.2"
|
||||||
|
"@protobufjs/pool": "npm:^1.1.0"
|
||||||
|
"@protobufjs/utf8": "npm:^1.1.0"
|
||||||
|
"@types/node": "npm:>=13.7.0"
|
||||||
|
long: "npm:^5.0.0"
|
||||||
|
checksum: 10c0/a5460a63fe596523b9a067cbce39a6b310d1a71750fda261f076535662aada97c24450e18c5bc98a27784f70500615904ff1227e1742183509f0db4fdede669b
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"proxy-addr@npm:~2.0.7":
|
"proxy-addr@npm:~2.0.7":
|
||||||
version: 2.0.7
|
version: 2.0.7
|
||||||
resolution: "proxy-addr@npm:2.0.7"
|
resolution: "proxy-addr@npm:2.0.7"
|
||||||
@ -53021,6 +53178,9 @@ __metadata:
|
|||||||
"@nestjs/graphql": "patch:@nestjs/graphql@12.1.1#./patches/@nestjs+graphql+12.1.1.patch"
|
"@nestjs/graphql": "patch:@nestjs/graphql@12.1.1#./patches/@nestjs+graphql+12.1.1.patch"
|
||||||
"@node-saml/passport-saml": "npm:^5.0.0"
|
"@node-saml/passport-saml": "npm:^5.0.0"
|
||||||
"@nx/js": "npm:18.3.3"
|
"@nx/js": "npm:18.3.3"
|
||||||
|
"@opentelemetry/api": "npm:^1.9.0"
|
||||||
|
"@opentelemetry/exporter-metrics-otlp-http": "npm:^0.200.0"
|
||||||
|
"@opentelemetry/sdk-metrics": "npm:^2.0.0"
|
||||||
"@ptc-org/nestjs-query-graphql": "patch:@ptc-org/nestjs-query-graphql@4.2.0#./patches/@ptc-org+nestjs-query-graphql+4.2.0.patch"
|
"@ptc-org/nestjs-query-graphql": "patch:@ptc-org/nestjs-query-graphql@4.2.0#./patches/@ptc-org+nestjs-query-graphql+4.2.0.patch"
|
||||||
"@revertdotdev/revert-react": "npm:^0.0.21"
|
"@revertdotdev/revert-react": "npm:^0.0.21"
|
||||||
"@sentry/nestjs": "npm:^8.30.0"
|
"@sentry/nestjs": "npm:^8.30.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user