From 4f5d1864556ba3076f00a0b4efc975f0ae8e8a02 Mon Sep 17 00:00:00 2001 From: Adish M Date: Tue, 30 Dec 2025 19:58:45 +0530 Subject: [PATCH] Implement OpenTelemetry middleware initialization and auto-start logic --- server/src/helpers/bootstrap.helper.ts | 33 ++++++++---- server/src/main.ts | 1 + server/src/otel/tracing.ts | 71 ++++++++++++++++++++++++++ 3 files changed, 94 insertions(+), 11 deletions(-) diff --git a/server/src/helpers/bootstrap.helper.ts b/server/src/helpers/bootstrap.helper.ts index 1193c9addf..a55aeed9bf 100644 --- a/server/src/helpers/bootstrap.helper.ts +++ b/server/src/helpers/bootstrap.helper.ts @@ -84,30 +84,41 @@ export async function handleLicensingInit(app: NestExpressApplication, logger: a } /** - * Handles OTEL initialization for Enterprise Edition + * Applies OTEL middleware to Express app + * Note: The OTEL SDK is started at import time in src/otel/tracing.ts + * This function only applies the middleware after the app is created */ export async function initializeOtel(app: NestExpressApplication, logger: any) { // Check if OTEL is enabled if (process.env.ENABLE_OTEL !== 'true') { - logger.log('OTEL disabled (ENABLE_OTEL not set to true)'); + logger.log('⏭️ OTEL disabled (ENABLE_OTEL not set to true)'); return; } try { - logger.log('Initializing OpenTelemetry...'); - const tooljetEdition = getTooljetEdition() as TOOLJET_EDITIONS; - const importPath = await getImportPath(false, tooljetEdition); - // Dynamically import the edition-specific listener - const { otelListener } = await import(`${importPath}/otel/listener`); + if (tooljetEdition !== TOOLJET_EDITIONS.EE) { + logger.log('⏭️ OTEL skipped - not Enterprise Edition'); + return; + } - // Initialize the listener (CE will no-op, EE will set up OTEL) - await otelListener.initialize(app); + logger.log('🔭 Applying OpenTelemetry middleware...'); - logger.log('✅ OpenTelemetry initialization completed'); + // Import otelMiddleware from tracing.ts (use relative path for runtime compatibility) + const { otelMiddleware } = await import('../otel/tracing'); + + // Apply OTEL middleware to Express app + const expressApp = app.getHttpAdapter().getInstance(); + expressApp.use(otelMiddleware); + + logger.log('✅ OpenTelemetry middleware applied successfully'); + logger.log(' - SDK: Already started at import time'); + logger.log(' - Tracing: Enabled'); + logger.log(' - Metrics: Enabled'); + logger.log(' - Auto-instrumentation: Active'); } catch (error) { - logger.error('❌ Failed to initialize OpenTelemetry:', error); + logger.error('❌ Failed to apply OpenTelemetry middleware:', error); // Don't throw - observability should never break the app } } diff --git a/server/src/main.ts b/server/src/main.ts index 75aaa162c9..b118084046 100644 --- a/server/src/main.ts +++ b/server/src/main.ts @@ -1,3 +1,4 @@ +import './otel/tracing'; // CRITICAL: This MUST be the first import to ensure OTEL patches modules before they load import { NestFactory } from '@nestjs/core'; import { NestExpressApplication } from '@nestjs/platform-express'; import { WsAdapter } from '@nestjs/platform-ws'; diff --git a/server/src/otel/tracing.ts b/server/src/otel/tracing.ts index 1ed8a19fdb..d55bf2ab1a 100644 --- a/server/src/otel/tracing.ts +++ b/server/src/otel/tracing.ts @@ -581,3 +581,74 @@ export const decrementActiveSessions = (attributes: { activeSessionsCounter.add(-1, metricAttributes); } }; + +// ============================================================================ +// AUTO-START: Initialize OTEL SDK immediately when this module is imported +// This MUST run before any other modules (http, pg, express, etc.) are loaded +// ============================================================================ + +// Load .env file before checking ENABLE_OTEL +// ConfigModule hasn't loaded yet, so we need to manually load .env +// Use the same pattern as the rest of the codebase (from database-config-utils.ts) +const fs = require('fs'); +const path = require('path'); +const dotenv = require('dotenv'); + +function loadEnvVars() { + const envFilePath = process.env.NODE_ENV === 'test' + ? path.resolve(process.cwd(), '../.env.test') + : path.resolve(process.cwd(), '../.env'); + + if (fs.existsSync(envFilePath)) { + const envConfig = dotenv.parse(fs.readFileSync(envFilePath)); + // Merge with existing process.env (existing env vars take precedence) + Object.assign(process.env, envConfig, process.env); + } +} + +// Load environment variables +loadEnvVars(); + +console.log('[OTEL] Auto-start code reached'); +console.log('[OTEL] ENABLE_OTEL:', process.env.ENABLE_OTEL); + +let isInitialized = false; + +if (process.env.ENABLE_OTEL === 'true' && !isInitialized) { + console.log('[OTEL] Condition met, checking edition...'); + + try { + // Check edition - only EE supports OTEL + // Use relative paths instead of TypeScript aliases for runtime compatibility + const { getTooljetEdition } = require('../helpers/utils.helper'); + const { TOOLJET_EDITIONS } = require('../modules/app/constants'); + + const tooljetEdition = getTooljetEdition(); + console.log('[OTEL] Edition:', tooljetEdition); + + if (tooljetEdition === TOOLJET_EDITIONS.EE) { + console.log('[OTEL] Starting SDK at import time (before any modules load)...'); + + // Start OTEL SDK - this registers instrumentations immediately + // The patches are applied synchronously when instrumentations are registered + startOpenTelemetry() + .then(() => { + isInitialized = true; + console.log('[OTEL] ✅ SDK started successfully'); + }) + .catch((err) => { + console.error('[OTEL] ❌ Failed to start SDK:', err); + // Log the error but don't throw - observability should never break the app + }); + + // Mark as initializing to prevent double initialization + isInitialized = true; + } else { + console.log('[OTEL] ⏭️ Skipping OTEL - not Enterprise Edition'); + } + } catch (error) { + console.error('[OTEL] Error during auto-start:', error); + } +} else { + console.log('[OTEL] Condition NOT met. ENABLE_OTEL:', process.env.ENABLE_OTEL, 'isInitialized:', isInitialized); +}