refactor(core): Move all code related to onServerStarted into InternalHooks (no-changelog) (#8500)

This commit is contained in:
कारतोफ्फेलस्क्रिप्ट™ 2024-01-31 13:29:17 +01:00 committed by GitHub
parent 0e9a5a2ab2
commit 839dd96c7d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 131 additions and 175 deletions

View file

@ -31,7 +31,6 @@ import type { WorkflowExecute } from 'n8n-core';
import type PCancelable from 'p-cancelable'; import type PCancelable from 'p-cancelable';
import type { DatabaseType } from '@db/types';
import type { AuthProviderType } from '@db/entities/AuthIdentity'; import type { AuthProviderType } from '@db/entities/AuthIdentity';
import type { SharedCredentials } from '@db/entities/SharedCredentials'; import type { SharedCredentials } from '@db/entities/SharedCredentials';
import type { TagEntity } from '@db/entities/TagEntity'; import type { TagEntity } from '@db/entities/TagEntity';
@ -267,37 +266,6 @@ export interface IWebhookManager {
executeWebhook(req: WebhookRequest, res: Response): Promise<IResponseCallbackData>; executeWebhook(req: WebhookRequest, res: Response): Promise<IResponseCallbackData>;
} }
export interface IDiagnosticInfo {
versionCli: string;
databaseType: DatabaseType;
notificationsEnabled: boolean;
disableProductionWebhooksOnMainProcess: boolean;
systemInfo: {
os: {
type?: string;
version?: string;
};
memory?: number;
cpus: {
count?: number;
model?: string;
speed?: number;
};
};
executionVariables: {
[key: string]: string | number | boolean | undefined;
};
deploymentType: string;
binaryDataMode: string;
smtp_set_up: boolean;
ldap_allowed: boolean;
saml_enabled: boolean;
binary_data_s3: boolean;
multi_main_setup_enabled: boolean;
licensePlanName?: string;
licenseTenantId?: number;
}
export interface ITelemetryUserDeletionData { export interface ITelemetryUserDeletionData {
user_id: string; user_id: string;
target_user_old_status: 'active' | 'invited'; target_user_old_status: 'active' | 'invited';

View file

@ -1,5 +1,6 @@
import { Service } from 'typedi'; import { Service } from 'typedi';
import { snakeCase } from 'change-case'; import { snakeCase } from 'change-case';
import os from 'node:os';
import { get as pslGet } from 'psl'; import { get as pslGet } from 'psl';
import type { import type {
AuthenticationMethod, AuthenticationMethod,
@ -13,20 +14,22 @@ import type {
import { TelemetryHelpers } from 'n8n-workflow'; import { TelemetryHelpers } from 'n8n-workflow';
import { InstanceSettings } from 'n8n-core'; import { InstanceSettings } from 'n8n-core';
import config from '@/config';
import { N8N_VERSION } from '@/constants'; import { N8N_VERSION } from '@/constants';
import type { AuthProviderType } from '@db/entities/AuthIdentity'; import type { AuthProviderType } from '@db/entities/AuthIdentity';
import type { GlobalRole, User } from '@db/entities/User'; import type { GlobalRole, User } from '@db/entities/User';
import type { ExecutionMetadata } from '@db/entities/ExecutionMetadata'; import type { ExecutionMetadata } from '@db/entities/ExecutionMetadata';
import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository'; import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository';
import { WorkflowRepository } from '@db/repositories/workflow.repository';
import { MessageEventBus, type EventPayloadWorkflow } from '@/eventbus'; import { MessageEventBus, type EventPayloadWorkflow } from '@/eventbus';
import { determineFinalExecutionStatus } from '@/executionLifecycleHooks/shared/sharedHookFunctions'; import { determineFinalExecutionStatus } from '@/executionLifecycleHooks/shared/sharedHookFunctions';
import type { import type {
IDiagnosticInfo,
ITelemetryUserDeletionData, ITelemetryUserDeletionData,
IWorkflowDb, IWorkflowDb,
IExecutionTrackProperties, IExecutionTrackProperties,
IWorkflowExecutionDataProcess, IWorkflowExecutionDataProcess,
} from '@/Interfaces'; } from '@/Interfaces';
import { License } from '@/License';
import { EventsService } from '@/services/events.service'; import { EventsService } from '@/services/events.service';
import { NodeTypes } from '@/NodeTypes'; import { NodeTypes } from '@/NodeTypes';
import { Telemetry } from '@/telemetry'; import { Telemetry } from '@/telemetry';
@ -50,12 +53,14 @@ function userToPayload(user: User): {
@Service() @Service()
export class InternalHooks { export class InternalHooks {
constructor( constructor(
private telemetry: Telemetry, private readonly telemetry: Telemetry,
private nodeTypes: NodeTypes, private readonly nodeTypes: NodeTypes,
private sharedWorkflowRepository: SharedWorkflowRepository, private readonly sharedWorkflowRepository: SharedWorkflowRepository,
private readonly workflowRepository: WorkflowRepository,
eventsService: EventsService, eventsService: EventsService,
private readonly instanceSettings: InstanceSettings, private readonly instanceSettings: InstanceSettings,
private readonly eventBus: MessageEventBus, private readonly eventBus: MessageEventBus,
private readonly license: License,
) { ) {
eventsService.on( eventsService.on(
'telemetry.onFirstProductionWorkflowSuccess', 'telemetry.onFirstProductionWorkflowSuccess',
@ -71,31 +76,69 @@ export class InternalHooks {
await this.telemetry.init(); await this.telemetry.init();
} }
async onServerStarted( async onServerStarted(): Promise<unknown[]> {
diagnosticInfo: IDiagnosticInfo, const cpus = os.cpus();
earliestWorkflowCreatedAt?: Date, const binaryDataConfig = config.getEnv('binaryDataManager');
): Promise<unknown[]> {
const isS3Selected = config.getEnv('binaryDataManager.mode') === 's3';
const isS3Available = config.getEnv('binaryDataManager.availableModes').includes('s3');
const isS3Licensed = this.license.isBinaryDataS3Licensed();
const authenticationMethod = config.getEnv('userManagement.authenticationMethod');
const info = { const info = {
version_cli: diagnosticInfo.versionCli, version_cli: N8N_VERSION,
db_type: diagnosticInfo.databaseType, db_type: config.getEnv('database.type'),
n8n_version_notifications_enabled: diagnosticInfo.notificationsEnabled, n8n_version_notifications_enabled: config.getEnv('versionNotifications.enabled'),
n8n_disable_production_main_process: diagnosticInfo.disableProductionWebhooksOnMainProcess, n8n_disable_production_main_process: config.getEnv(
system_info: diagnosticInfo.systemInfo, 'endpoints.disableProductionWebhooksOnMainProcess',
execution_variables: diagnosticInfo.executionVariables, ),
n8n_deployment_type: diagnosticInfo.deploymentType, system_info: {
n8n_binary_data_mode: diagnosticInfo.binaryDataMode, os: {
smtp_set_up: diagnosticInfo.smtp_set_up, type: os.type(),
ldap_allowed: diagnosticInfo.ldap_allowed, version: os.version(),
saml_enabled: diagnosticInfo.saml_enabled, },
license_plan_name: diagnosticInfo.licensePlanName, memory: os.totalmem() / 1024,
license_tenant_id: diagnosticInfo.licenseTenantId, cpus: {
count: cpus.length,
model: cpus[0].model,
speed: cpus[0].speed,
},
},
execution_variables: {
executions_mode: config.getEnv('executions.mode'),
executions_timeout: config.getEnv('executions.timeout'),
executions_timeout_max: config.getEnv('executions.maxTimeout'),
executions_data_save_on_error: config.getEnv('executions.saveDataOnError'),
executions_data_save_on_success: config.getEnv('executions.saveDataOnSuccess'),
executions_data_save_on_progress: config.getEnv('executions.saveExecutionProgress'),
executions_data_save_manual_executions: config.getEnv(
'executions.saveDataManualExecutions',
),
executions_data_prune: config.getEnv('executions.pruneData'),
executions_data_max_age: config.getEnv('executions.pruneDataMaxAge'),
},
n8n_deployment_type: config.getEnv('deployment.type'),
n8n_binary_data_mode: binaryDataConfig.mode,
smtp_set_up: config.getEnv('userManagement.emails.mode') === 'smtp',
ldap_allowed: authenticationMethod === 'ldap',
saml_enabled: authenticationMethod === 'saml',
license_plan_name: this.license.getPlanName(),
license_tenant_id: config.getEnv('license.tenantId'),
binary_data_s3: isS3Available && isS3Selected && isS3Licensed,
multi_main_setup_enabled: config.getEnv('multiMainSetup.enabled'),
}; };
const firstWorkflow = await this.workflowRepository.findOne({
select: ['createdAt'],
order: { createdAt: 'ASC' },
where: {},
});
return await Promise.all([ return await Promise.all([
this.telemetry.identify(info), this.telemetry.identify(info),
this.telemetry.track('Instance started', { this.telemetry.track('Instance started', {
...info, ...info,
earliest_workflow_created: earliestWorkflowCreatedAt, earliest_workflow_created: firstWorkflow?.createdAt,
}), }),
]); ]);
} }

View file

@ -7,7 +7,6 @@ import { Container, Service } from 'typedi';
import assert from 'assert'; import assert from 'assert';
import { exec as callbackExec } from 'child_process'; import { exec as callbackExec } from 'child_process';
import { access as fsAccess } from 'fs/promises'; import { access as fsAccess } from 'fs/promises';
import os from 'os';
import { join as pathJoin } from 'path'; import { join as pathJoin } from 'path';
import { promisify } from 'util'; import { promisify } from 'util';
import cookieParser from 'cookie-parser'; import cookieParser from 'cookie-parser';
@ -54,7 +53,7 @@ import { WorkflowStatisticsController } from '@/controllers/workflowStatistics.c
import { ExternalSecretsController } from '@/ExternalSecrets/ExternalSecrets.controller.ee'; import { ExternalSecretsController } from '@/ExternalSecrets/ExternalSecrets.controller.ee';
import { ExecutionsController } from '@/executions/executions.controller'; import { ExecutionsController } from '@/executions/executions.controller';
import { isApiEnabled, loadPublicApiVersions } from '@/PublicApi'; import { isApiEnabled, loadPublicApiVersions } from '@/PublicApi';
import type { ICredentialsOverwrite, IDiagnosticInfo } from '@/Interfaces'; import type { ICredentialsOverwrite } from '@/Interfaces';
import { CredentialsOverwrites } from '@/CredentialsOverwrites'; import { CredentialsOverwrites } from '@/CredentialsOverwrites';
import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials'; import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
import * as ResponseHelper from '@/ResponseHelper'; import * as ResponseHelper from '@/ResponseHelper';
@ -69,19 +68,12 @@ import { AbstractServer } from './AbstractServer';
import { PostHogClient } from './posthog'; import { PostHogClient } from './posthog';
import { MessageEventBus } from '@/eventbus'; import { MessageEventBus } from '@/eventbus';
import { InternalHooks } from './InternalHooks'; import { InternalHooks } from './InternalHooks';
import { License } from './License';
import { SamlController } from './sso/saml/routes/saml.controller.ee'; import { SamlController } from './sso/saml/routes/saml.controller.ee';
import { SamlService } from './sso/saml/saml.service.ee'; import { SamlService } from './sso/saml/saml.service.ee';
import { VariablesController } from './environments/variables/variables.controller.ee'; import { VariablesController } from './environments/variables/variables.controller.ee';
import {
isLdapCurrentAuthenticationMethod,
isSamlCurrentAuthenticationMethod,
} from './sso/ssoHelpers';
import { SourceControlService } from '@/environments/sourceControl/sourceControl.service.ee'; import { SourceControlService } from '@/environments/sourceControl/sourceControl.service.ee';
import { SourceControlController } from '@/environments/sourceControl/sourceControl.controller.ee'; import { SourceControlController } from '@/environments/sourceControl/sourceControl.controller.ee';
import { WorkflowRepository } from '@db/repositories/workflow.repository';
import { handleMfaDisable, isMfaFeatureEnabled } from './Mfa/helpers'; import { handleMfaDisable, isMfaFeatureEnabled } from './Mfa/helpers';
import type { FrontendService } from './services/frontend.service'; import type { FrontendService } from './services/frontend.service';
import { ActiveWorkflowsController } from './controllers/activeWorkflows.controller'; import { ActiveWorkflowsController } from './controllers/activeWorkflows.controller';
@ -129,71 +121,11 @@ export class Server extends AbstractServer {
await super.start(); await super.start();
this.logger.debug(`Server ID: ${this.uniqueInstanceId}`); this.logger.debug(`Server ID: ${this.uniqueInstanceId}`);
const cpus = os.cpus();
const binaryDataConfig = config.getEnv('binaryDataManager');
const isS3Selected = config.getEnv('binaryDataManager.mode') === 's3';
const isS3Available = config.getEnv('binaryDataManager.availableModes').includes('s3');
const isS3Licensed = Container.get(License).isBinaryDataS3Licensed();
const diagnosticInfo: IDiagnosticInfo = {
databaseType: config.getEnv('database.type'),
disableProductionWebhooksOnMainProcess: config.getEnv(
'endpoints.disableProductionWebhooksOnMainProcess',
),
notificationsEnabled: config.getEnv('versionNotifications.enabled'),
versionCli: N8N_VERSION,
systemInfo: {
os: {
type: os.type(),
version: os.version(),
},
memory: os.totalmem() / 1024,
cpus: {
count: cpus.length,
model: cpus[0].model,
speed: cpus[0].speed,
},
},
executionVariables: {
executions_mode: config.getEnv('executions.mode'),
executions_timeout: config.getEnv('executions.timeout'),
executions_timeout_max: config.getEnv('executions.maxTimeout'),
executions_data_save_on_error: config.getEnv('executions.saveDataOnError'),
executions_data_save_on_success: config.getEnv('executions.saveDataOnSuccess'),
executions_data_save_on_progress: config.getEnv('executions.saveExecutionProgress'),
executions_data_save_manual_executions: config.getEnv(
'executions.saveDataManualExecutions',
),
executions_data_prune: config.getEnv('executions.pruneData'),
executions_data_max_age: config.getEnv('executions.pruneDataMaxAge'),
},
deploymentType: config.getEnv('deployment.type'),
binaryDataMode: binaryDataConfig.mode,
smtp_set_up: config.getEnv('userManagement.emails.mode') === 'smtp',
ldap_allowed: isLdapCurrentAuthenticationMethod(),
saml_enabled: isSamlCurrentAuthenticationMethod(),
binary_data_s3: isS3Available && isS3Selected && isS3Licensed,
multi_main_setup_enabled: config.getEnv('multiMainSetup.enabled'),
licensePlanName: Container.get(License).getPlanName(),
licenseTenantId: config.getEnv('license.tenantId'),
};
if (inDevelopment && process.env.N8N_DEV_RELOAD === 'true') { if (inDevelopment && process.env.N8N_DEV_RELOAD === 'true') {
void this.loadNodesAndCredentials.setupHotReload(); void this.loadNodesAndCredentials.setupHotReload();
} }
void Container.get(WorkflowRepository) void Container.get(InternalHooks).onServerStarted();
.findOne({
select: ['createdAt'],
order: { createdAt: 'ASC' },
where: {},
})
.then(
async (workflow) =>
await Container.get(InternalHooks).onServerStarted(diagnosticInfo, workflow?.createdAt),
);
Container.get(CollaborationService); Container.get(CollaborationService);
} }

View file

@ -1,67 +1,80 @@
import { Telemetry } from '@/telemetry';
import { InternalHooks } from '@/InternalHooks';
import { mockInstance } from '../shared/mocking';
import type { IDiagnosticInfo } from '@/Interfaces';
import { mock } from 'jest-mock-extended'; import { mock } from 'jest-mock-extended';
import { N8N_VERSION } from '@/constants';
import { InternalHooks } from '@/InternalHooks';
import type { License } from '@/License';
import type { Telemetry } from '@/telemetry';
jest.mock('@/telemetry'); jest.mock('@/eventbus');
jest.mock('node:os', () => ({
let internalHooks: InternalHooks; tmpdir: () => '',
let telemetry: Telemetry; cpus: () => [{ model: 'MIPS R3000', speed: 40_000_000 }],
type: () => 'TempleOS',
version: () => '5.03',
totalmem: () => 1024 * 1024,
}));
describe('InternalHooks', () => { describe('InternalHooks', () => {
beforeAll(() => { const telemetry = mock<Telemetry>();
telemetry = mockInstance(Telemetry); const license = mock<License>();
internalHooks = new InternalHooks(telemetry, mock(), mock(), mock(), mock(), mock()); const internalHooks = new InternalHooks(
}); telemetry,
mock(),
mock(),
mock(),
mock(),
mock(),
mock(),
license,
);
beforeEach(() => jest.clearAllMocks());
it('Should be defined', () => { it('Should be defined', () => {
expect(internalHooks).toBeDefined(); expect(internalHooks).toBeDefined();
}); });
it('Should forward license plan name and tenant id to identify when provided', async () => { it('Should forward license plan name and tenant id to identify when provided', async () => {
const licensePlanName = 'license-plan-name'; license.getPlanName.mockReturnValue('Best Plan');
const licenseTenantId = 1001;
const diagnosticInfo: IDiagnosticInfo = { await internalHooks.onServerStarted();
versionCli: '1.2.3',
databaseType: 'sqlite', expect(telemetry.identify).toHaveBeenCalledWith({
notificationsEnabled: true, version_cli: N8N_VERSION,
disableProductionWebhooksOnMainProcess: false, db_type: 'sqlite',
systemInfo: { n8n_version_notifications_enabled: true,
os: {}, n8n_disable_production_main_process: false,
cpus: {}, system_info: {
memory: 1024,
os: {
type: 'TempleOS',
version: '5.03',
}, },
executionVariables: {}, cpus: {
deploymentType: 'testing', count: 1,
binaryDataMode: 'default', model: 'MIPS R3000',
smtp_set_up: false, speed: 40000000,
ldap_allowed: true, },
saml_enabled: true, },
licensePlanName, execution_variables: {
licenseTenantId, executions_data_max_age: 336,
executions_data_prune: true,
executions_data_save_manual_executions: true,
executions_data_save_on_error: 'all',
executions_data_save_on_progress: false,
executions_data_save_on_success: 'all',
executions_mode: 'regular',
executions_timeout: -1,
executions_timeout_max: 3600,
},
n8n_deployment_type: 'default',
n8n_binary_data_mode: 'default',
smtp_set_up: true,
ldap_allowed: false,
saml_enabled: false,
license_plan_name: 'Best Plan',
license_tenant_id: 1,
binary_data_s3: false, binary_data_s3: false,
multi_main_setup_enabled: false, multi_main_setup_enabled: false,
}; });
const parameters = {
version_cli: diagnosticInfo.versionCli,
db_type: diagnosticInfo.databaseType,
n8n_version_notifications_enabled: diagnosticInfo.notificationsEnabled,
n8n_disable_production_main_process: diagnosticInfo.disableProductionWebhooksOnMainProcess,
system_info: diagnosticInfo.systemInfo,
execution_variables: diagnosticInfo.executionVariables,
n8n_deployment_type: diagnosticInfo.deploymentType,
n8n_binary_data_mode: diagnosticInfo.binaryDataMode,
smtp_set_up: diagnosticInfo.smtp_set_up,
ldap_allowed: diagnosticInfo.ldap_allowed,
saml_enabled: diagnosticInfo.saml_enabled,
license_plan_name: diagnosticInfo.licensePlanName,
license_tenant_id: diagnosticInfo.licenseTenantId,
};
await internalHooks.onServerStarted(diagnosticInfo);
expect(telemetry.identify).toHaveBeenCalledWith(parameters);
}); });
}); });