UNPKG

commit-story

Version:

Automated Git Journal System with AI Assistant Context Integration

300 lines (263 loc) 10.1 kB
import fs from 'fs'; import { trace, SpanStatusCode } from '@opentelemetry/api'; import { OTEL } from './telemetry/standards.js'; import { createNarrativeLogger } from './utils/trace-logger.js'; import { shutdownWithTimeout } from './utils/shutdown-helper.js'; // Check if running from test script - only show console traces during testing const isTestScript = process.argv[1]?.includes('test-otel'); // Get configuration synchronously during bootstrap const { debug: isDebugMode, dev: isDevMode } = (() => { try { const configPath = './commit-story.config.json'; if (fs.existsSync(configPath)) { const configData = JSON.parse(fs.readFileSync(configPath, 'utf8')); return { debug: configData.debug === true, dev: configData.dev === true }; } } catch (error) { // Silently ignore config file errors - both modes default to false } return { debug: false, dev: false }; })(); // Only initialize telemetry when dev mode is enabled let sdk = null; /** * Dynamically load OpenTelemetry SDK packages * Returns null if packages are not available (production install) */ async function loadOTelSDK() { try { const modules = await Promise.all([ import('@opentelemetry/sdk-node'), import('@opentelemetry/auto-instrumentations-node'), import('@opentelemetry/sdk-trace-base'), import('@opentelemetry/exporter-trace-otlp-http'), import('@opentelemetry/exporter-metrics-otlp-http'), import('@opentelemetry/sdk-metrics') ]); return { NodeSDK: modules[0].NodeSDK, getNodeAutoInstrumentations: modules[1].getNodeAutoInstrumentations, BatchSpanProcessor: modules[2].BatchSpanProcessor, ConsoleSpanExporter: modules[2].ConsoleSpanExporter, OTLPTraceExporter: modules[3].OTLPTraceExporter, OTLPMetricExporter: modules[4].OTLPMetricExporter, PeriodicExportingMetricReader: modules[5].PeriodicExportingMetricReader, AggregationTemporality: modules[5].AggregationTemporality }; } catch (error) { // SDK packages not available (production install without devDependencies) return null; } } // Instrument conditional telemetry initialization async function initializeTelemetryConditionally() { // We need to use a simple tracer since the SDK might not be initialized yet const simpleTracer = { startActiveSpan: (name, options, fn) => { const startTime = Date.now(); const logger = createNarrativeLogger('initialization.telemetry_setup'); try { logger.start('Telemetry initialization', `Checking telemetry initialization conditions`, { dev_mode: isDevMode, test_script: isTestScript }); const result = fn({ setAttributes: () => {}, setStatus: () => {}, recordException: () => {}, end: () => {} }); const duration = Date.now() - startTime; logger.complete('Telemetry setup', `Telemetry initialization completed in ${duration}ms`, { sdk_initialized: result !== null, initialization_duration_ms: duration }); return result; } catch (error) { const duration = Date.now() - startTime; logger.error('Telemetry setup', 'Error during telemetry initialization', error, { initialization_duration_ms: duration }); throw error; } } }; return simpleTracer.startActiveSpan(OTEL.span.initialization.telemetry(), { attributes: { 'code.function': 'initializeTelemetryConditionally' } }, async (span) => { const logger = createNarrativeLogger('initialization.telemetry_setup'); const startTime = Date.now(); try { logger.decision('Condition check', `Telemetry initialization condition: dev mode = ${isDevMode}`, { dev_mode_enabled: isDevMode, condition_met: isDevMode }); if (!isDevMode) { logger.decision('Skip initialization', 'Telemetry disabled - dev mode is false', { skip_reason: 'dev_mode_disabled', sdk_initialized: false }); return null; } // Try to load SDK packages logger.progress('Loading SDK', 'Attempting to load OpenTelemetry SDK packages', { packages: ['sdk-node', 'auto-instrumentations', 'exporters'] }); const sdkModules = await loadOTelSDK(); if (!sdkModules) { logger.decision('Skip initialization', 'SDK packages not available (production install)', { skip_reason: 'sdk_packages_missing', sdk_initialized: false }); span.setStatus({ code: SpanStatusCode.OK, message: 'SDK packages not available' }); span.end(); return null; } logger.progress('Starting initialization', 'Dev mode enabled and SDK available, initializing telemetry stack', { service_name: 'commit-story-dev', service_version: '1.0.0' }); return initializeSDK(logger, startTime, span, sdkModules); } catch (error) { const initializationDuration = Date.now() - startTime; logger.error('Telemetry initialization', 'Failed to initialize telemetry', error, { initialization_duration_ms: initializationDuration }); span.recordException(error); span.setStatus({ code: SpanStatusCode.ERROR, message: error.message }); span.end(); throw error; } }); } function initializeSDK(logger, startTime, span, sdkModules) { const { NodeSDK, getNodeAutoInstrumentations, BatchSpanProcessor, ConsoleSpanExporter, OTLPTraceExporter, OTLPMetricExporter, PeriodicExportingMetricReader, AggregationTemporality } = sdkModules; logger.progress('OTLP configuration', 'Configuring OTLP exporters for Datadog Agent', { trace_endpoint: 'http://localhost:4318/v1/traces', metrics_endpoint: 'http://localhost:4318/v1/metrics' }); // Create OTLP exporter for Datadog Agent (localhost:4318) const otlpTraceExporter = new OTLPTraceExporter({ url: 'http://localhost:4318/v1/traces', headers: { // Add any required headers for Datadog OTLP ingestion }, }); // Create OTLP metric exporter for Datadog Agent (localhost:4318) const otlpMetricExporter = new OTLPMetricExporter({ url: 'http://localhost:4318/v1/metrics', temporalityPreference: AggregationTemporality.DELTA, // Required for Datadog OTLP ingest headers: { // Add any required headers for Datadog OTLP ingestion }, }); // Build span processors array - conditionally include console exporter const spanProcessors = [ // OTLP exporter for Datadog with batching new BatchSpanProcessor(otlpTraceExporter, { maxExportBatchSize: 10, // Batch for efficiency scheduledDelayMillis: 1000, // 1 second delay for network calls }), ]; // Only add console exporter when running test script if (isTestScript) { const consoleExporter = new ConsoleSpanExporter(); spanProcessors.unshift( new BatchSpanProcessor(consoleExporter, { maxExportBatchSize: 1, // Export immediately for development scheduledDelayMillis: 100, // Minimal delay }) ); } // Initialize Node SDK with dual span processors and metrics sdk = new NodeSDK({ serviceName: 'commit-story-dev', serviceVersion: '1.0.0', spanProcessors, // Configure dual metric readers metricReader: new PeriodicExportingMetricReader({ exporter: otlpMetricExporter, exportIntervalMillis: 5000, // Export every 5 seconds for development (ensures short commits work) }), instrumentations: [ // Auto-instrument common libraries getNodeAutoInstrumentations({ // Disable instrumentations we don't need yet '@opentelemetry/instrumentation-dns': { enabled: false }, '@opentelemetry/instrumentation-net': { enabled: false }, }), ], }); // Initialize tracing sdk.start(); const initializationDuration = Date.now() - startTime; const attrs = OTEL.attrs.initialization.telemetry({ sdkInitialized: true, serviceName: 'commit-story-dev', serviceVersion: '1.0.0', otlpEndpoint: 'http://localhost:4318', consoleOutput: isTestScript, initializationDuration }); span.setAttributes(attrs); span.setStatus({ code: SpanStatusCode.OK, message: 'Telemetry initialized successfully' }); span.end(); logger.complete('SDK initialization', 'OpenTelemetry SDK initialized successfully', { service_name: 'commit-story-dev', traces_enabled: true, metrics_enabled: true, console_output: isTestScript, initialization_duration_ms: initializationDuration }); // Only show initialization messages in test script or debug mode if (isTestScript) { console.log('🔭 OpenTelemetry observability stack initialized:'); console.log(' ✅ Traces: Console + OTLP to Datadog Agent (localhost:4318)'); console.log(' ✅ Metrics: OTLP to Datadog Agent (localhost:4318)'); console.log(' 📊 Service: commit-story-dev'); } else if (isDebugMode) { console.log('OpenTelemetry initialized'); } return sdk; } /** * Initialize telemetry if dev mode is enabled and SDK packages are available * This is the main entry point for telemetry initialization * @returns {Promise<Object|null>} SDK instance or null if not initialized */ export async function initializeTelemetry() { return await initializeTelemetryConditionally(); } /** * Gracefully shutdown telemetry with timeout * @param {Object} options - Shutdown options * @param {number} options.timeoutMs - Maximum time to wait for shutdown (default: 2000ms) * @returns {Promise<{success: boolean, error?: Error}>} Export status */ export async function shutdownTelemetry({ timeoutMs = 2000 } = {}) { if (!sdk) { // Telemetry not initialized (dev mode disabled), nothing to shutdown return { success: true }; } try { await shutdownWithTimeout(() => sdk.shutdown(), timeoutMs, 'Telemetry'); return { success: true }; } catch (error) { return { success: false, error }; } } export default sdk;